From 4700ad7817af0997ef052ba4693758c58e1e5585 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Fri, 24 Nov 2023 20:07:17 +0100 Subject: [PATCH] Add HVACMode.OFF to Plugwise Adam (#103360) --- homeassistant/components/plugwise/climate.py | 79 +++++++--- .../components/plugwise/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../plugwise/fixtures/adam_jip/all_data.json | 2 +- .../fixtures/m_adam_cooling/all_data.json | 16 +- tests/components/plugwise/test_climate.py | 147 +++++++++++++++--- tests/components/plugwise/test_select.py | 26 +++- 8 files changed, 222 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/plugwise/climate.py b/homeassistant/components/plugwise/climate.py index 42004ce7088..efad1b7466b 100644 --- a/homeassistant/components/plugwise/climate.py +++ b/homeassistant/components/plugwise/climate.py @@ -46,6 +46,8 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity): _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_translation_key = DOMAIN + _previous_mode: str = "heating" + def __init__( self, coordinator: PlugwiseDataUpdateCoordinator, @@ -55,10 +57,15 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity): super().__init__(coordinator, device_id) self._attr_extra_state_attributes = {} self._attr_unique_id = f"{device_id}-climate" - + self.cdr_gateway = coordinator.data.gateway + gateway_id: str = coordinator.data.gateway["gateway_id"] + self.gateway_data = coordinator.data.devices[gateway_id] # Determine supported features self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE - if self.coordinator.data.gateway["cooling_present"]: + if ( + self.cdr_gateway["cooling_present"] + and self.cdr_gateway["smile_name"] != "Adam" + ): self._attr_supported_features = ( ClimateEntityFeature.TARGET_TEMPERATURE_RANGE ) @@ -73,6 +80,20 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity): self.device["thermostat"]["resolution"], 0.1 ) + def _previous_action_mode(self, coordinator: PlugwiseDataUpdateCoordinator) -> None: + """Return the previous action-mode when the regulation-mode is not heating or cooling. + + Helper for set_hvac_mode(). + """ + # When no cooling available, _previous_mode is always heating + if ( + "regulation_modes" in self.gateway_data + and "cooling" in self.gateway_data["regulation_modes"] + ): + mode = self.gateway_data["select_regulation_mode"] + if mode in ("cooling", "heating"): + self._previous_mode = mode + @property def current_temperature(self) -> float: """Return the current temperature.""" @@ -105,33 +126,46 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity): @property def hvac_mode(self) -> HVACMode: - """Return HVAC operation ie. auto, heat, or heat_cool mode.""" + """Return HVAC operation ie. auto, cool, heat, heat_cool, or off mode.""" if (mode := self.device.get("mode")) is None or mode not in self.hvac_modes: return HVACMode.HEAT return HVACMode(mode) @property def hvac_modes(self) -> list[HVACMode]: - """Return the list of available HVACModes.""" - hvac_modes = [HVACMode.HEAT] - if self.coordinator.data.gateway["cooling_present"]: - hvac_modes = [HVACMode.HEAT_COOL] + """Return a list of available HVACModes.""" + hvac_modes: list[HVACMode] = [] + if "regulation_modes" in self.gateway_data: + hvac_modes.append(HVACMode.OFF) if self.device["available_schedules"] != ["None"]: hvac_modes.append(HVACMode.AUTO) + if self.cdr_gateway["cooling_present"]: + if "regulation_modes" in self.gateway_data: + if self.gateway_data["select_regulation_mode"] == "cooling": + hvac_modes.append(HVACMode.COOL) + if self.gateway_data["select_regulation_mode"] == "heating": + hvac_modes.append(HVACMode.HEAT) + else: + hvac_modes.append(HVACMode.HEAT_COOL) + else: + hvac_modes.append(HVACMode.HEAT) + return hvac_modes @property - def hvac_action(self) -> HVACAction | None: + def hvac_action(self) -> HVACAction: """Return the current running hvac operation if supported.""" - heater: str | None = self.coordinator.data.gateway["heater_id"] - if heater: - heater_data = self.coordinator.data.devices[heater] - if heater_data["binary_sensors"]["heating_state"]: - return HVACAction.HEATING - if heater_data["binary_sensors"].get("cooling_state"): - return HVACAction.COOLING + # Keep track of the previous action-mode + self._previous_action_mode(self.coordinator) + + heater: str = self.coordinator.data.gateway["heater_id"] + heater_data = self.coordinator.data.devices[heater] + if heater_data["binary_sensors"]["heating_state"]: + return HVACAction.HEATING + if heater_data["binary_sensors"].get("cooling_state", False): + return HVACAction.COOLING return HVACAction.IDLE @@ -168,9 +202,18 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity): if hvac_mode not in self.hvac_modes: raise HomeAssistantError("Unsupported hvac_mode") - await self.coordinator.api.set_schedule_state( - self.device["location"], "on" if hvac_mode == HVACMode.AUTO else "off" - ) + if hvac_mode == self.hvac_mode: + return + + if hvac_mode == HVACMode.OFF: + await self.coordinator.api.set_regulation_mode(hvac_mode) + else: + await self.coordinator.api.set_schedule_state( + self.device["location"], + "on" if hvac_mode == HVACMode.AUTO else "off", + ) + if self.hvac_mode == HVACMode.OFF: + await self.coordinator.api.set_regulation_mode(self._previous_mode) @plugwise_command async def async_set_preset_mode(self, preset_mode: str) -> None: diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index 1155aaffdf8..74b196b6edd 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -7,6 +7,6 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["crcmod", "plugwise"], - "requirements": ["plugwise==0.33.2"], + "requirements": ["plugwise==0.34.0"], "zeroconf": ["_plugwise._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 5787e1a8554..bdb0fcf2388 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1477,7 +1477,7 @@ plexauth==0.0.6 plexwebsocket==0.0.14 # homeassistant.components.plugwise -plugwise==0.33.2 +plugwise==0.34.0 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3e9c1a19c2f..3037c994658 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1135,7 +1135,7 @@ plexauth==0.0.6 plexwebsocket==0.0.14 # homeassistant.components.plugwise -plugwise==0.33.2 +plugwise==0.34.0 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/tests/components/plugwise/fixtures/adam_jip/all_data.json b/tests/components/plugwise/fixtures/adam_jip/all_data.json index bc1bc9c8c0c..dacee20c644 100644 --- a/tests/components/plugwise/fixtures/adam_jip/all_data.json +++ b/tests/components/plugwise/fixtures/adam_jip/all_data.json @@ -8,7 +8,7 @@ "firmware": "2016-10-27T02:00:00+02:00", "hardware": "255", "location": "06aecb3d00354375924f50c47af36bd2", - "mode": "heat", + "mode": "off", "model": "Lisa", "name": "Slaapkamer", "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], diff --git a/tests/components/plugwise/fixtures/m_adam_cooling/all_data.json b/tests/components/plugwise/fixtures/m_adam_cooling/all_data.json index 126852e945d..624547155a3 100644 --- a/tests/components/plugwise/fixtures/m_adam_cooling/all_data.json +++ b/tests/components/plugwise/fixtures/m_adam_cooling/all_data.json @@ -55,22 +55,20 @@ "available_schedules": ["Weekschema", "Badkamer", "Test"], "dev_class": "thermostat", "location": "f2bf9048bef64cc5b6d5110154e33c81", - "mode": "heat_cool", + "mode": "cool", "model": "ThermoTouch", "name": "Anna", "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], "select_schedule": "Weekschema", "selected_schedule": "None", "sensors": { - "setpoint_high": 23.5, - "setpoint_low": 4.0, + "setpoint": 23.5, "temperature": 25.8 }, "thermostat": { "lower_bound": 1.0, "resolution": 0.01, - "setpoint_high": 23.5, - "setpoint_low": 4.0, + "setpoint": 23.5, "upper_bound": 35.0 }, "vendor": "Plugwise" @@ -115,9 +113,8 @@ "select_schedule": "Badkamer", "sensors": { "battery": 56, - "setpoint_high": 23.5, - "setpoint_low": 20.0, - "temperature": 239 + "setpoint": 23.5, + "temperature": 23.9 }, "temperature_offset": { "lower_bound": -2.0, @@ -128,8 +125,7 @@ "thermostat": { "lower_bound": 0.0, "resolution": 0.01, - "setpoint_high": 25.0, - "setpoint_low": 19.0, + "setpoint": 25.0, "upper_bound": 99.9 }, "vendor": "Plugwise", diff --git a/tests/components/plugwise/test_climate.py b/tests/components/plugwise/test_climate.py index 2d9885637df..8b4c4d5a745 100644 --- a/tests/components/plugwise/test_climate.py +++ b/tests/components/plugwise/test_climate.py @@ -13,6 +13,10 @@ from homeassistant.util.dt import utcnow from tests.common import MockConfigEntry, async_fire_time_changed +HA_PLUGWISE_SMILE_ASYNC_UPDATE = ( + "homeassistant.components.plugwise.coordinator.Smile.async_update" +) + async def test_adam_climate_entity_attributes( hass: HomeAssistant, mock_smile_adam: MagicMock, init_integration: MockConfigEntry @@ -21,7 +25,7 @@ async def test_adam_climate_entity_attributes( state = hass.states.get("climate.zone_lisa_wk") assert state assert state.state == HVACMode.AUTO - assert state.attributes["hvac_modes"] == [HVACMode.HEAT, HVACMode.AUTO] + assert state.attributes["hvac_modes"] == [HVACMode.AUTO, HVACMode.HEAT] # hvac_action is not asserted as the fixture is not in line with recent firmware functionality assert "preset_modes" in state.attributes @@ -39,7 +43,7 @@ async def test_adam_climate_entity_attributes( state = hass.states.get("climate.zone_thermostat_jessie") assert state assert state.state == HVACMode.AUTO - assert state.attributes["hvac_modes"] == [HVACMode.HEAT, HVACMode.AUTO] + assert state.attributes["hvac_modes"] == [HVACMode.AUTO, HVACMode.HEAT] # hvac_action is not asserted as the fixture is not in line with recent firmware functionality assert "preset_modes" in state.attributes @@ -62,13 +66,21 @@ async def test_adam_2_climate_entity_attributes( assert state assert state.state == HVACMode.HEAT assert state.attributes["hvac_action"] == "heating" - assert state.attributes["hvac_modes"] == [HVACMode.HEAT, HVACMode.AUTO] + assert state.attributes["hvac_modes"] == [ + HVACMode.OFF, + HVACMode.AUTO, + HVACMode.HEAT, + ] state = hass.states.get("climate.lisa_badkamer") assert state assert state.state == HVACMode.AUTO assert state.attributes["hvac_action"] == "heating" - assert state.attributes["hvac_modes"] == [HVACMode.HEAT, HVACMode.AUTO] + assert state.attributes["hvac_modes"] == [ + HVACMode.OFF, + HVACMode.AUTO, + HVACMode.HEAT, + ] async def test_adam_3_climate_entity_attributes( @@ -78,11 +90,58 @@ async def test_adam_3_climate_entity_attributes( state = hass.states.get("climate.anna") assert state - assert state.state == HVACMode.HEAT_COOL + assert state.state == HVACMode.COOL assert state.attributes["hvac_action"] == "cooling" assert state.attributes["hvac_modes"] == [ - HVACMode.HEAT_COOL, + HVACMode.OFF, HVACMode.AUTO, + HVACMode.COOL, + ] + data = mock_smile_adam_3.async_update.return_value + data.devices["da224107914542988a88561b4452b0f6"][ + "select_regulation_mode" + ] = "heating" + data.devices["ad4838d7d35c4d6ea796ee12ae5aedf8"]["mode"] = "heat" + data.devices["056ee145a816487eaa69243c3280f8bf"]["binary_sensors"][ + "cooling_state" + ] = False + data.devices["056ee145a816487eaa69243c3280f8bf"]["binary_sensors"][ + "heating_state" + ] = True + with patch(HA_PLUGWISE_SMILE_ASYNC_UPDATE, return_value=data): + async_fire_time_changed(hass, utcnow() + timedelta(minutes=1)) + await hass.async_block_till_done() + state = hass.states.get("climate.anna") + assert state + assert state.state == HVACMode.HEAT + assert state.attributes["hvac_action"] == "heating" + assert state.attributes["hvac_modes"] == [ + HVACMode.OFF, + HVACMode.AUTO, + HVACMode.HEAT, + ] + data = mock_smile_adam_3.async_update.return_value + data.devices["da224107914542988a88561b4452b0f6"][ + "select_regulation_mode" + ] = "cooling" + data.devices["ad4838d7d35c4d6ea796ee12ae5aedf8"]["mode"] = "cool" + data.devices["056ee145a816487eaa69243c3280f8bf"]["binary_sensors"][ + "cooling_state" + ] = True + data.devices["056ee145a816487eaa69243c3280f8bf"]["binary_sensors"][ + "heating_state" + ] = False + with patch(HA_PLUGWISE_SMILE_ASYNC_UPDATE, return_value=data): + async_fire_time_changed(hass, utcnow() + timedelta(minutes=1)) + await hass.async_block_till_done() + state = hass.states.get("climate.anna") + assert state + assert state.state == HVACMode.COOL + assert state.attributes["hvac_action"] == "cooling" + assert state.attributes["hvac_modes"] == [ + HVACMode.OFF, + HVACMode.AUTO, + HVACMode.COOL, ] @@ -173,6 +232,60 @@ async def test_adam_climate_entity_climate_changes( ) +async def test_adam_climate_off_mode_change( + hass: HomeAssistant, + mock_smile_adam_4: MagicMock, + init_integration: MockConfigEntry, +) -> None: + """Test handling of user requests in adam climate device environment.""" + state = hass.states.get("climate.slaapkamer") + assert state + assert state.state == HVACMode.OFF + await hass.services.async_call( + "climate", + "set_hvac_mode", + { + "entity_id": "climate.slaapkamer", + "hvac_mode": "heat", + }, + blocking=True, + ) + assert mock_smile_adam_4.set_schedule_state.call_count == 1 + assert mock_smile_adam_4.set_regulation_mode.call_count == 1 + mock_smile_adam_4.set_regulation_mode.assert_called_with("heating") + + state = hass.states.get("climate.kinderkamer") + assert state + assert state.state == HVACMode.HEAT + await hass.services.async_call( + "climate", + "set_hvac_mode", + { + "entity_id": "climate.kinderkamer", + "hvac_mode": "off", + }, + blocking=True, + ) + assert mock_smile_adam_4.set_schedule_state.call_count == 1 + assert mock_smile_adam_4.set_regulation_mode.call_count == 2 + mock_smile_adam_4.set_regulation_mode.assert_called_with("off") + + state = hass.states.get("climate.logeerkamer") + assert state + assert state.state == HVACMode.HEAT + await hass.services.async_call( + "climate", + "set_hvac_mode", + { + "entity_id": "climate.logeerkamer", + "hvac_mode": "heat", + }, + blocking=True, + ) + assert mock_smile_adam_4.set_schedule_state.call_count == 1 + assert mock_smile_adam_4.set_regulation_mode.call_count == 2 + + async def test_anna_climate_entity_attributes( hass: HomeAssistant, mock_smile_anna: MagicMock, @@ -183,10 +296,7 @@ async def test_anna_climate_entity_attributes( assert state assert state.state == HVACMode.AUTO assert state.attributes["hvac_action"] == "heating" - assert state.attributes["hvac_modes"] == [ - HVACMode.HEAT, - HVACMode.AUTO, - ] + assert state.attributes["hvac_modes"] == [HVACMode.AUTO, HVACMode.HEAT] assert "no_frost" in state.attributes["preset_modes"] assert "home" in state.attributes["preset_modes"] @@ -211,8 +321,8 @@ async def test_anna_2_climate_entity_attributes( assert state.state == HVACMode.AUTO assert state.attributes["hvac_action"] == "cooling" assert state.attributes["hvac_modes"] == [ - HVACMode.HEAT_COOL, HVACMode.AUTO, + HVACMode.HEAT_COOL, ] assert state.attributes["supported_features"] == 18 assert state.attributes["target_temp_high"] == 24.0 @@ -230,8 +340,8 @@ async def test_anna_3_climate_entity_attributes( assert state.state == HVACMode.AUTO assert state.attributes["hvac_action"] == "idle" assert state.attributes["hvac_modes"] == [ - HVACMode.HEAT_COOL, HVACMode.AUTO, + HVACMode.HEAT_COOL, ] @@ -270,10 +380,8 @@ async def test_anna_climate_entity_climate_changes( {"entity_id": "climate.anna", "hvac_mode": "auto"}, blocking=True, ) - assert mock_smile_anna.set_schedule_state.call_count == 1 - mock_smile_anna.set_schedule_state.assert_called_with( - "c784ee9fdab44e1395b8dee7d7a497d5", "on" - ) + # hvac_mode is already auto so not called. + assert mock_smile_anna.set_schedule_state.call_count == 0 await hass.services.async_call( "climate", @@ -281,16 +389,13 @@ async def test_anna_climate_entity_climate_changes( {"entity_id": "climate.anna", "hvac_mode": "heat"}, blocking=True, ) - assert mock_smile_anna.set_schedule_state.call_count == 2 + assert mock_smile_anna.set_schedule_state.call_count == 1 mock_smile_anna.set_schedule_state.assert_called_with( "c784ee9fdab44e1395b8dee7d7a497d5", "off" ) data = mock_smile_anna.async_update.return_value data.devices["3cb70739631c4d17a86b8b12e8a5161b"]["available_schedules"] = ["None"] - with patch( - "homeassistant.components.plugwise.coordinator.Smile.async_update", - return_value=data, - ): + with patch(HA_PLUGWISE_SMILE_ASYNC_UPDATE, return_value=data): async_fire_time_changed(hass, utcnow() + timedelta(minutes=1)) await hass.async_block_till_done() state = hass.states.get("climate.anna") diff --git a/tests/components/plugwise/test_select.py b/tests/components/plugwise/test_select.py index 9df20a5ffc8..f1220a07a2b 100644 --- a/tests/components/plugwise/test_select.py +++ b/tests/components/plugwise/test_select.py @@ -16,7 +16,7 @@ from tests.common import MockConfigEntry async def test_adam_select_entities( hass: HomeAssistant, mock_smile_adam: MagicMock, init_integration: MockConfigEntry ) -> None: - """Test a select.""" + """Test a thermostat Select.""" state = hass.states.get("select.zone_lisa_wk_thermostat_schedule") assert state @@ -44,3 +44,27 @@ async def test_adam_change_select_entity( "on", "Badkamer Schema", ) + + +async def test_adam_select_regulation_mode( + hass: HomeAssistant, mock_smile_adam_3: MagicMock, init_integration: MockConfigEntry +) -> None: + """Test a regulation_mode select. + + Also tests a change in climate _previous mode. + """ + + state = hass.states.get("select.adam_regulation_mode") + assert state + assert state.state == "cooling" + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + "entity_id": "select.adam_regulation_mode", + "option": "heating", + }, + blocking=True, + ) + assert mock_smile_adam_3.set_regulation_mode.call_count == 1 + mock_smile_adam_3.set_regulation_mode.assert_called_with("heating")