Skip to content

Commit f9c1e81

Browse files
authored
Improve error handling and add tests to senz climate (home-assistant#156544)
1 parent 0549d11 commit f9c1e81

File tree

4 files changed

+313
-6
lines changed

4 files changed

+313
-6
lines changed

homeassistant/components/senz/climate.py

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from typing import Any
66

77
from aiosenz import MODE_AUTO, Thermostat
8+
from httpx import RequestError
89

910
from homeassistant.components.climate import (
1011
ClimateEntity,
@@ -14,6 +15,7 @@
1415
)
1516
from homeassistant.const import ATTR_TEMPERATURE, PRECISION_TENTHS, UnitOfTemperature
1617
from homeassistant.core import HomeAssistant, callback
18+
from homeassistant.exceptions import HomeAssistantError
1719
from homeassistant.helpers.device_registry import DeviceInfo
1820
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
1921
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -81,7 +83,7 @@ def target_temperature(self) -> float:
8183
@property
8284
def available(self) -> bool:
8385
"""Return True if the thermostat is available."""
84-
return self._thermostat.online
86+
return super().available and self._thermostat.online
8587

8688
@property
8789
def hvac_mode(self) -> HVACMode:
@@ -97,14 +99,32 @@ def hvac_action(self) -> HVACAction:
9799

98100
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
99101
"""Set new target hvac mode."""
100-
if hvac_mode == HVACMode.AUTO:
101-
await self._thermostat.auto()
102-
else:
103-
await self._thermostat.manual()
102+
try:
103+
if hvac_mode == HVACMode.AUTO:
104+
await self._thermostat.auto()
105+
else:
106+
await self._thermostat.manual()
107+
except RequestError as err:
108+
raise HomeAssistantError(
109+
translation_domain=DOMAIN,
110+
translation_key="set_attribute_error",
111+
translation_placeholders={
112+
"attribute": "hvac mode",
113+
},
114+
) from err
104115
await self.coordinator.async_request_refresh()
105116

106117
async def async_set_temperature(self, **kwargs: Any) -> None:
107118
"""Set new target temperature."""
108119
temp: float = kwargs[ATTR_TEMPERATURE]
109-
await self._thermostat.manual(temp)
120+
try:
121+
await self._thermostat.manual(temp)
122+
except RequestError as err:
123+
raise HomeAssistantError(
124+
translation_domain=DOMAIN,
125+
translation_key="set_attribute_error",
126+
translation_placeholders={
127+
"attribute": "target temperature",
128+
},
129+
) from err
110130
await self.coordinator.async_request_refresh()

homeassistant/components/senz/strings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@
4242
},
4343
"oauth2_implementation_unavailable": {
4444
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
45+
},
46+
"set_attribute_error": {
47+
"message": "Failed to set {attribute} on the device."
4548
}
4649
}
4750
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# serializer version: 1
2+
# name: test_climate_snapshot[climate.test_room_1-entry]
3+
EntityRegistryEntrySnapshot({
4+
'aliases': set({
5+
}),
6+
'area_id': None,
7+
'capabilities': dict({
8+
'hvac_modes': list([
9+
<HVACMode.HEAT: 'heat'>,
10+
<HVACMode.AUTO: 'auto'>,
11+
]),
12+
'max_temp': 35,
13+
'min_temp': 5,
14+
}),
15+
'config_entry_id': <ANY>,
16+
'config_subentry_id': <ANY>,
17+
'device_class': None,
18+
'device_id': <ANY>,
19+
'disabled_by': None,
20+
'domain': 'climate',
21+
'entity_category': None,
22+
'entity_id': 'climate.test_room_1',
23+
'has_entity_name': True,
24+
'hidden_by': None,
25+
'icon': None,
26+
'id': <ANY>,
27+
'labels': set({
28+
}),
29+
'name': None,
30+
'options': dict({
31+
}),
32+
'original_device_class': None,
33+
'original_icon': None,
34+
'original_name': None,
35+
'platform': 'senz',
36+
'previous_unique_id': None,
37+
'suggested_object_id': None,
38+
'supported_features': <ClimateEntityFeature: 1>,
39+
'translation_key': None,
40+
'unique_id': '1001',
41+
'unit_of_measurement': None,
42+
})
43+
# ---
44+
# name: test_climate_snapshot[climate.test_room_1-state]
45+
StateSnapshot({
46+
'attributes': ReadOnlyDict({
47+
'current_temperature': 18.4,
48+
'friendly_name': 'Test room 1',
49+
'hvac_action': <HVACAction.HEATING: 'heating'>,
50+
'hvac_modes': list([
51+
<HVACMode.HEAT: 'heat'>,
52+
<HVACMode.AUTO: 'auto'>,
53+
]),
54+
'max_temp': 35,
55+
'min_temp': 5,
56+
'supported_features': <ClimateEntityFeature: 1>,
57+
'temperature': 19.0,
58+
}),
59+
'context': <ANY>,
60+
'entity_id': 'climate.test_room_1',
61+
'last_changed': <ANY>,
62+
'last_reported': <ANY>,
63+
'last_updated': <ANY>,
64+
'state': 'heat',
65+
})
66+
# ---
67+
# name: test_climate_snapshot[climate.test_room_2-entry]
68+
EntityRegistryEntrySnapshot({
69+
'aliases': set({
70+
}),
71+
'area_id': None,
72+
'capabilities': dict({
73+
'hvac_modes': list([
74+
<HVACMode.HEAT: 'heat'>,
75+
<HVACMode.AUTO: 'auto'>,
76+
]),
77+
'max_temp': 35,
78+
'min_temp': 5,
79+
}),
80+
'config_entry_id': <ANY>,
81+
'config_subentry_id': <ANY>,
82+
'device_class': None,
83+
'device_id': <ANY>,
84+
'disabled_by': None,
85+
'domain': 'climate',
86+
'entity_category': None,
87+
'entity_id': 'climate.test_room_2',
88+
'has_entity_name': True,
89+
'hidden_by': None,
90+
'icon': None,
91+
'id': <ANY>,
92+
'labels': set({
93+
}),
94+
'name': None,
95+
'options': dict({
96+
}),
97+
'original_device_class': None,
98+
'original_icon': None,
99+
'original_name': None,
100+
'platform': 'senz',
101+
'previous_unique_id': None,
102+
'suggested_object_id': None,
103+
'supported_features': <ClimateEntityFeature: 1>,
104+
'translation_key': None,
105+
'unique_id': '1002',
106+
'unit_of_measurement': None,
107+
})
108+
# ---
109+
# name: test_climate_snapshot[climate.test_room_2-state]
110+
StateSnapshot({
111+
'attributes': ReadOnlyDict({
112+
'current_temperature': 9.3,
113+
'friendly_name': 'Test room 2',
114+
'hvac_action': <HVACAction.IDLE: 'idle'>,
115+
'hvac_modes': list([
116+
<HVACMode.HEAT: 'heat'>,
117+
<HVACMode.AUTO: 'auto'>,
118+
]),
119+
'max_temp': 35,
120+
'min_temp': 5,
121+
'supported_features': <ClimateEntityFeature: 1>,
122+
'temperature': 6.0,
123+
}),
124+
'context': <ANY>,
125+
'entity_id': 'climate.test_room_2',
126+
'last_changed': <ANY>,
127+
'last_reported': <ANY>,
128+
'last_updated': <ANY>,
129+
'state': 'auto',
130+
})
131+
# ---
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
"""Test Senz climate platform."""
2+
3+
from unittest.mock import MagicMock, patch
4+
5+
from httpx import RequestError
6+
import pytest
7+
from syrupy.assertion import SnapshotAssertion
8+
9+
from homeassistant.components.climate import (
10+
ATTR_HVAC_MODE,
11+
DOMAIN as CLIMATE_DOMAIN,
12+
HVACMode,
13+
)
14+
from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, Platform
15+
from homeassistant.core import HomeAssistant
16+
from homeassistant.exceptions import HomeAssistantError
17+
from homeassistant.helpers import entity_registry as er
18+
19+
from . import setup_integration
20+
21+
from tests.common import MockConfigEntry, snapshot_platform
22+
23+
TEST_DOMAIN = CLIMATE_DOMAIN
24+
TEST_ENTITY_ID = "climate.test_room_1"
25+
SERVICE_SET_TEMPERATURE = "set_temperature"
26+
SERVICE_SET_HVAC_MODE = "set_hvac_mode"
27+
28+
29+
async def test_climate_snapshot(
30+
hass: HomeAssistant,
31+
mock_senz_client: MagicMock,
32+
mock_config_entry: MockConfigEntry,
33+
snapshot: SnapshotAssertion,
34+
entity_registry: er.EntityRegistry,
35+
) -> None:
36+
"""Test climate setup for cloud connection."""
37+
with patch("homeassistant.components.senz.PLATFORMS", [Platform.CLIMATE]):
38+
await setup_integration(hass, mock_config_entry)
39+
40+
await snapshot_platform(
41+
hass, entity_registry, snapshot, mock_config_entry.entry_id
42+
)
43+
44+
45+
async def test_set_target(
46+
hass: HomeAssistant,
47+
mock_senz_client: MagicMock,
48+
mock_config_entry: MockConfigEntry,
49+
) -> None:
50+
"""Test setting of target temperature."""
51+
52+
with (
53+
patch("homeassistant.components.senz.PLATFORMS", [Platform.CLIMATE]),
54+
patch(
55+
"homeassistant.components.senz.Thermostat.manual", return_value=None
56+
) as mock_manual,
57+
):
58+
await setup_integration(hass, mock_config_entry)
59+
await hass.services.async_call(
60+
TEST_DOMAIN,
61+
SERVICE_SET_TEMPERATURE,
62+
{ATTR_ENTITY_ID: TEST_ENTITY_ID, ATTR_TEMPERATURE: 17},
63+
blocking=True,
64+
)
65+
mock_manual.assert_called_once_with(17.0)
66+
67+
68+
async def test_set_target_fail(
69+
hass: HomeAssistant,
70+
mock_senz_client: MagicMock,
71+
mock_config_entry: MockConfigEntry,
72+
) -> None:
73+
"""Test that failed set_temperature is handled."""
74+
75+
with (
76+
patch("homeassistant.components.senz.PLATFORMS", [Platform.CLIMATE]),
77+
patch(
78+
"homeassistant.components.senz.Thermostat.manual",
79+
side_effect=RequestError("API error"),
80+
) as mock_manual,
81+
):
82+
await setup_integration(hass, mock_config_entry)
83+
with pytest.raises(
84+
HomeAssistantError, match="Failed to set target temperature on the device"
85+
):
86+
await hass.services.async_call(
87+
TEST_DOMAIN,
88+
SERVICE_SET_TEMPERATURE,
89+
{ATTR_ENTITY_ID: TEST_ENTITY_ID, ATTR_TEMPERATURE: 17},
90+
blocking=True,
91+
)
92+
mock_manual.assert_called_once()
93+
94+
95+
@pytest.mark.parametrize(
96+
("mode", "manual_count", "auto_count"),
97+
[(HVACMode.HEAT, 1, 0), (HVACMode.AUTO, 0, 1)],
98+
)
99+
async def test_set_hvac_mode(
100+
hass: HomeAssistant,
101+
mock_senz_client: MagicMock,
102+
mock_config_entry: MockConfigEntry,
103+
mode: str,
104+
manual_count: int,
105+
auto_count: int,
106+
) -> None:
107+
"""Test setting of hvac mode."""
108+
109+
with (
110+
patch("homeassistant.components.senz.PLATFORMS", [Platform.CLIMATE]),
111+
patch(
112+
"homeassistant.components.senz.Thermostat.manual", return_value=None
113+
) as mock_manual,
114+
patch(
115+
"homeassistant.components.senz.Thermostat.auto", return_value=None
116+
) as mock_auto,
117+
):
118+
await setup_integration(hass, mock_config_entry)
119+
await hass.services.async_call(
120+
TEST_DOMAIN,
121+
SERVICE_SET_HVAC_MODE,
122+
{ATTR_ENTITY_ID: TEST_ENTITY_ID, ATTR_HVAC_MODE: mode},
123+
blocking=True,
124+
)
125+
assert mock_manual.call_count == manual_count
126+
assert mock_auto.call_count == auto_count
127+
128+
129+
async def test_set_hvac_mode_fail(
130+
hass: HomeAssistant,
131+
mock_senz_client: MagicMock,
132+
mock_config_entry: MockConfigEntry,
133+
) -> None:
134+
"""Test that failed set_hvac_mode is handled."""
135+
136+
with (
137+
patch("homeassistant.components.senz.PLATFORMS", [Platform.CLIMATE]),
138+
patch(
139+
"homeassistant.components.senz.Thermostat.manual",
140+
side_effect=RequestError("API error"),
141+
) as mock_manual,
142+
):
143+
await setup_integration(hass, mock_config_entry)
144+
with pytest.raises(
145+
HomeAssistantError, match="Failed to set hvac mode on the device"
146+
):
147+
await hass.services.async_call(
148+
TEST_DOMAIN,
149+
SERVICE_SET_HVAC_MODE,
150+
{ATTR_ENTITY_ID: TEST_ENTITY_ID, ATTR_HVAC_MODE: HVACMode.HEAT},
151+
blocking=True,
152+
)
153+
mock_manual.assert_called_once()

0 commit comments

Comments
 (0)