Skip to content

Commit ab90b65

Browse files
Fix CI failures: mypy, coverage, and coordinator config_entry
- Add type: ignore[attr-defined] comments for TURN_ON/TURN_OFF ClimateEntityFeature attributes that aren't in homeassistant-stubs - Pass config_entry to parent DataUpdateCoordinator.__init__() to fix config_entry being None in coordinator methods - Add comprehensive tests for coordinator remote temperature features - Coverage increased from 82.15% to 87.42%
1 parent b28fba8 commit ab90b65

3 files changed

Lines changed: 239 additions & 11 deletions

File tree

custom_components/mitsubishi/climate.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,8 @@ class MitsubishiClimate(MitsubishiEntity, ClimateEntity):
118118

119119
_attr_supported_features = (
120120
0
121-
| ClimateEntityFeature.TURN_ON
122-
| ClimateEntityFeature.TURN_OFF
121+
| ClimateEntityFeature.TURN_ON # type: ignore[attr-defined]
122+
| ClimateEntityFeature.TURN_OFF # type: ignore[attr-defined]
123123
| ClimateEntityFeature.TARGET_TEMPERATURE
124124
| ClimateEntityFeature.FAN_MODE
125125
| ClimateEntityFeature.SWING_MODE

custom_components/mitsubishi/coordinator.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,19 +33,18 @@ def __init__(
3333
scan_interval: int = DEFAULT_SCAN_INTERVAL,
3434
) -> None:
3535
"""Initialize."""
36-
self.controller = controller
37-
self.config_entry = config_entry
38-
self.unit_info = None # Will be populated on first update or by config flow
39-
# Load persisted remote temperature mode (AC doesn't report it, so we track it)
40-
self._remote_temp_mode = config_entry.options.get(CONF_REMOTE_TEMP_MODE, False)
41-
self._startup_mode_applied = False
42-
4336
super().__init__(
4437
hass,
4538
_LOGGER,
4639
name=DOMAIN,
40+
config_entry=config_entry,
4741
update_interval=timedelta(seconds=scan_interval),
4842
)
43+
self.controller = controller
44+
self.unit_info = None # Will be populated on first update or by config flow
45+
# Load persisted remote temperature mode (AC doesn't report it, so we track it)
46+
self._remote_temp_mode = config_entry.options.get(CONF_REMOTE_TEMP_MODE, False)
47+
self._startup_mode_applied = False
4948

5049
def set_remote_temp_mode(self, enabled: bool) -> None:
5150
"""Set whether remote temperature mode is enabled and persist to storage."""

tests/test_coordinator.py

Lines changed: 231 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,16 @@
44
from unittest.mock import AsyncMock, patch
55

66
import pytest
7+
from homeassistant.const import CONF_HOST, STATE_UNAVAILABLE, STATE_UNKNOWN
78
from homeassistant.helpers.update_coordinator import UpdateFailed
8-
9-
from custom_components.mitsubishi.const import DEFAULT_SCAN_INTERVAL, DOMAIN
9+
from pytest_homeassistant_custom_component.common import MockConfigEntry
10+
11+
from custom_components.mitsubishi.const import (
12+
CONF_EXPERIMENTAL_FEATURES,
13+
CONF_EXTERNAL_TEMP_ENTITY,
14+
DEFAULT_SCAN_INTERVAL,
15+
DOMAIN,
16+
)
1017
from custom_components.mitsubishi.coordinator import MitsubishiDataUpdateCoordinator
1118

1219

@@ -124,3 +131,225 @@ async def test_coordinator_set_remote_temp_mode(
124131

125132
coordinator.set_remote_temp_mode(False)
126133
assert coordinator.remote_temp_mode is False
134+
135+
136+
@pytest.fixture
137+
def mock_config_entry_experimental():
138+
"""Create a mock config entry with experimental features enabled."""
139+
return MockConfigEntry(
140+
domain=DOMAIN,
141+
title="Test Mitsubishi AC",
142+
unique_id="00:11:22:33:44:57",
143+
data={
144+
CONF_HOST: "192.168.1.100",
145+
"encryption_key": "unregistered",
146+
"admin_username": "admin",
147+
"admin_password": "password",
148+
"scan_interval": 30,
149+
},
150+
options={
151+
CONF_EXPERIMENTAL_FEATURES: True,
152+
CONF_EXTERNAL_TEMP_ENTITY: "sensor.room_temp",
153+
},
154+
entry_id="test_entry_exp_coord_id",
155+
)
156+
157+
158+
@pytest.mark.asyncio
159+
async def test_coordinator_experimental_features_enabled(
160+
hass, mock_mitsubishi_controller, mock_config_entry_experimental
161+
):
162+
"""Test experimental_features_enabled property when enabled."""
163+
# Need to add config entry to hass first for options to work
164+
mock_config_entry_experimental.add_to_hass(hass)
165+
166+
coordinator = MitsubishiDataUpdateCoordinator(
167+
hass, mock_mitsubishi_controller, mock_config_entry_experimental
168+
)
169+
170+
assert coordinator.experimental_features_enabled is True
171+
172+
173+
@pytest.mark.asyncio
174+
async def test_async_update_data_with_experimental_first_update_internal(
175+
hass, mock_mitsubishi_controller, mock_config_entry_experimental
176+
):
177+
"""Test _async_update_data with experimental features on first update in internal mode."""
178+
mock_config_entry_experimental.add_to_hass(hass)
179+
180+
coordinator = MitsubishiDataUpdateCoordinator(
181+
hass, mock_mitsubishi_controller, mock_config_entry_experimental
182+
)
183+
# Ensure remote mode is off
184+
coordinator._remote_temp_mode = False
185+
186+
with patch.object(
187+
hass,
188+
"async_add_executor_job",
189+
AsyncMock(return_value=mock_mitsubishi_controller.state),
190+
):
191+
result = await coordinator._async_update_data()
192+
193+
assert result == mock_mitsubishi_controller.state
194+
assert coordinator._startup_mode_applied is True
195+
196+
197+
@pytest.mark.asyncio
198+
async def test_async_update_data_with_experimental_subsequent_update_remote(
199+
hass, mock_mitsubishi_controller, mock_config_entry_experimental
200+
):
201+
"""Test _async_update_data with experimental features on subsequent update in remote mode."""
202+
mock_config_entry_experimental.add_to_hass(hass)
203+
204+
coordinator = MitsubishiDataUpdateCoordinator(
205+
hass, mock_mitsubishi_controller, mock_config_entry_experimental
206+
)
207+
# Simulate first update already done
208+
coordinator._startup_mode_applied = True
209+
coordinator._remote_temp_mode = True
210+
211+
# Set up a mock state for the external entity
212+
hass.states.async_set("sensor.room_temp", "22.5")
213+
214+
with patch.object(
215+
hass,
216+
"async_add_executor_job",
217+
AsyncMock(return_value=mock_mitsubishi_controller.state),
218+
):
219+
result = await coordinator._async_update_data()
220+
221+
assert result == mock_mitsubishi_controller.state
222+
223+
224+
@pytest.mark.asyncio
225+
async def test_send_remote_temperature_no_entity_configured(hass, mock_mitsubishi_controller):
226+
"""Test _send_remote_temperature when no external entity is configured."""
227+
config_entry = MockConfigEntry(
228+
domain=DOMAIN,
229+
title="Test Mitsubishi AC",
230+
unique_id="00:11:22:33:44:58",
231+
data={CONF_HOST: "192.168.1.100"},
232+
options={CONF_EXPERIMENTAL_FEATURES: True}, # No CONF_EXTERNAL_TEMP_ENTITY
233+
entry_id="test_no_entity_id",
234+
)
235+
config_entry.add_to_hass(hass)
236+
237+
coordinator = MitsubishiDataUpdateCoordinator(hass, mock_mitsubishi_controller, config_entry)
238+
coordinator._remote_temp_mode = True
239+
240+
with patch.object(hass, "async_add_executor_job", AsyncMock()) as mock_executor:
241+
await coordinator._send_remote_temperature()
242+
243+
# Should call set_current_temperature with None to fall back
244+
mock_executor.assert_called_once()
245+
assert coordinator._remote_temp_mode is False # Should be disabled
246+
247+
248+
@pytest.mark.asyncio
249+
async def test_send_remote_temperature_entity_not_found(
250+
hass, mock_mitsubishi_controller, mock_config_entry_experimental
251+
):
252+
"""Test _send_remote_temperature when external entity is not found."""
253+
mock_config_entry_experimental.add_to_hass(hass)
254+
255+
coordinator = MitsubishiDataUpdateCoordinator(
256+
hass, mock_mitsubishi_controller, mock_config_entry_experimental
257+
)
258+
coordinator._remote_temp_mode = True
259+
# sensor.room_temp is not set in hass.states, so get() returns None
260+
261+
with patch.object(hass, "async_add_executor_job", AsyncMock()) as mock_executor:
262+
await coordinator._send_remote_temperature()
263+
264+
# Should call set_current_temperature with None to fall back
265+
mock_executor.assert_called_once()
266+
assert coordinator._remote_temp_mode is False
267+
268+
269+
@pytest.mark.asyncio
270+
async def test_send_remote_temperature_entity_unavailable(
271+
hass, mock_mitsubishi_controller, mock_config_entry_experimental
272+
):
273+
"""Test _send_remote_temperature when external entity is unavailable."""
274+
mock_config_entry_experimental.add_to_hass(hass)
275+
276+
# Set the entity state to unavailable
277+
hass.states.async_set("sensor.room_temp", STATE_UNAVAILABLE)
278+
279+
coordinator = MitsubishiDataUpdateCoordinator(
280+
hass, mock_mitsubishi_controller, mock_config_entry_experimental
281+
)
282+
coordinator._remote_temp_mode = True
283+
284+
with patch.object(hass, "async_add_executor_job", AsyncMock()) as mock_executor:
285+
await coordinator._send_remote_temperature()
286+
287+
mock_executor.assert_called_once()
288+
assert coordinator._remote_temp_mode is False
289+
290+
291+
@pytest.mark.asyncio
292+
async def test_send_remote_temperature_entity_unknown(
293+
hass, mock_mitsubishi_controller, mock_config_entry_experimental
294+
):
295+
"""Test _send_remote_temperature when external entity is unknown."""
296+
mock_config_entry_experimental.add_to_hass(hass)
297+
298+
# Set the entity state to unknown
299+
hass.states.async_set("sensor.room_temp", STATE_UNKNOWN)
300+
301+
coordinator = MitsubishiDataUpdateCoordinator(
302+
hass, mock_mitsubishi_controller, mock_config_entry_experimental
303+
)
304+
coordinator._remote_temp_mode = True
305+
306+
with patch.object(hass, "async_add_executor_job", AsyncMock()) as mock_executor:
307+
await coordinator._send_remote_temperature()
308+
309+
mock_executor.assert_called_once()
310+
assert coordinator._remote_temp_mode is False
311+
312+
313+
@pytest.mark.asyncio
314+
async def test_send_remote_temperature_invalid_value(
315+
hass, mock_mitsubishi_controller, mock_config_entry_experimental
316+
):
317+
"""Test _send_remote_temperature when temperature value is invalid."""
318+
mock_config_entry_experimental.add_to_hass(hass)
319+
320+
# Set the entity state to an invalid value
321+
hass.states.async_set("sensor.room_temp", "not_a_number")
322+
323+
coordinator = MitsubishiDataUpdateCoordinator(
324+
hass, mock_mitsubishi_controller, mock_config_entry_experimental
325+
)
326+
coordinator._remote_temp_mode = True
327+
328+
with patch.object(hass, "async_add_executor_job", AsyncMock()) as mock_executor:
329+
await coordinator._send_remote_temperature()
330+
331+
mock_executor.assert_called_once()
332+
assert coordinator._remote_temp_mode is False
333+
334+
335+
@pytest.mark.asyncio
336+
async def test_send_remote_temperature_success(
337+
hass, mock_mitsubishi_controller, mock_config_entry_experimental
338+
):
339+
"""Test _send_remote_temperature with valid temperature."""
340+
mock_config_entry_experimental.add_to_hass(hass)
341+
342+
# Set a valid temperature state
343+
hass.states.async_set("sensor.room_temp", "21.5")
344+
345+
coordinator = MitsubishiDataUpdateCoordinator(
346+
hass, mock_mitsubishi_controller, mock_config_entry_experimental
347+
)
348+
coordinator._remote_temp_mode = True
349+
350+
with patch.object(hass, "async_add_executor_job", AsyncMock()) as mock_executor:
351+
await coordinator._send_remote_temperature()
352+
353+
mock_executor.assert_called_once()
354+
# Remote mode should remain enabled on success
355+
assert coordinator._remote_temp_mode is True

0 commit comments

Comments
 (0)