From b62425728ba10c89b957b383d13b8c36d30863c7 Mon Sep 17 00:00:00 2001 From: = <=> Date: Wed, 25 May 2022 21:20:15 +0000 Subject: [PATCH] fix: correctly handles and communicates hvac modes Fixes #20 --- .devcontainer/configuration.yaml | 21 +++++++ .../dual_smart_thermostat/climate.py | 60 +++++++++++++------ .../dual_smart_thermostat/const.py | 11 ++++ hacs.json | 3 - tests/test_thermostat.py | 2 +- 5 files changed, 76 insertions(+), 21 deletions(-) diff --git a/.devcontainer/configuration.yaml b/.devcontainer/configuration.yaml index 263caf5..68038d1 100644 --- a/.devcontainer/configuration.yaml +++ b/.devcontainer/configuration.yaml @@ -151,6 +151,27 @@ climate: away_temp: 16 precision: 0.1 + - platform: generic_thermostat + name: generic one + unique_id: generic_cool + heater: switch.cooler + ac_mode: true + target_sensor: sensor.room_temp + min_temp: 15 + max_temp: 28 + target_temp: 23 + target_temp_high: 26 + target_temp_low: 23 + cold_tolerance: 0.3 + hot_tolerance: 0 + min_cycle_duration: + seconds: 5 + keep_alive: + minutes: 3 + # initial_hvac_mode: "off" + away_temp: 16 + precision: 0.1 + logger: default: error logs: diff --git a/custom_components/dual_smart_thermostat/climate.py b/custom_components/dual_smart_thermostat/climate.py index 418262d..3349a57 100644 --- a/custom_components/dual_smart_thermostat/climate.py +++ b/custom_components/dual_smart_thermostat/climate.py @@ -69,6 +69,7 @@ DEFAULT_MAX_FLOOR_TEMP, DEFAULT_NAME, DEFAULT_TOLERANCE, + HVACAction, HVACMode, ) @@ -439,14 +440,14 @@ def hvac_action(self): Need to be one of CURRENT_HVAC_*. """ if self._hvac_mode == HVACMode.OFF: - return HVACMode.OFF + return HVACAction.OFF if not self._is_device_active: - return HVACMode.IDLE + return HVACAction.IDLE if self.ac_mode: - return HVACMode.COOL + return HVACAction.COOLING if self._is_cooler_active: - return HVACMode.COOL - return HVACMode.HEAT + return HVACAction.COOLING + return HVACAction.HEATING @property def target_temperature(self): @@ -649,6 +650,9 @@ async def _async_control_heating(self, time=None, force=False): too_cold = self._is_too_cold() too_hot = self._is_too_hot() + if self.cooler_entity_id and self._is_cooler_active: + await self._async_cooler_turn_off() + if self._is_device_active: if too_hot or self._is_floor_hot or self._is_opening_open: _LOGGER.info("Turning off heater %s", self.heater_entity_id) @@ -689,29 +693,32 @@ async def _async_control_cooling(self, time=None, force=False): too_cold = self._is_too_cold() too_hot = self._is_too_hot() + cooler_entity = self.heater_entity_id + + if not self.ac_mode and self.cooler_entity_id: + cooler_entity = self.cooler_entity_id + if self._is_heter_active: + await self._async_heater_turn_off() + if self._is_device_active: if too_cold or self._is_opening_open: - _LOGGER.info("Turning off cooler %s", self.heater_entity_id) - await self._async_heater_turn_off() + _LOGGER.info("Turning off cooler %s", cooler_entity) + await self._async_switch_turn_off(cooler_entity) elif time is not None and not self._is_opening_open: # The time argument is passed only in keep-alive case _LOGGER.info( "Keep-alive - Turning on cooler (from active) %s", - self.heater_entity_id, + cooler_entity, ) - await self._async_heater_turn_on() + await self._async_switch_turn_on(cooler_entity) else: if too_hot and not self._is_opening_open: - _LOGGER.info( - "Turning on cooler (from inactive) %s", self.heater_entity_id - ) - await self._async_heater_turn_on() + _LOGGER.info("Turning on cooler (from inactive) %s", cooler_entity) + await self._async_switch_turn_on(cooler_entity) elif time is not None or self._is_opening_open or self._is_floor_hot: # The time argument is passed only in keep-alive case - _LOGGER.info( - "Keep-alive - Turning off cooler %s", self.heater_entity_id - ) - await self._async_heater_turn_off() + _LOGGER.info("Keep-alive - Turning off cooler %s", cooler_entity) + await self._async_switch_turn_off(cooler_entity) async def _async_control_heat_cool(self, time=None, force=False): """Check if we need to turn heating on or off.""" @@ -795,6 +802,11 @@ def _is_device_active(self): and self.hass.states.is_state(self.cooler_entity_id, STATE_ON) ) + @property + def _is_heter_active(self): + """If the toggleable device is currently active.""" + return self.hass.states.is_state(self.heater_entity_id, STATE_ON) + @property def _is_cooler_active(self): """If the toggleable cooler device is currently active.""" @@ -840,6 +852,20 @@ async def _async_cooler_turn_off(self): HA_DOMAIN, SERVICE_TURN_OFF, data, context=self._context ) + async def _async_switch_turn_off(self, entity_id): + """Turn toggleable device off.""" + data = {ATTR_ENTITY_ID: entity_id} + await self.hass.services.async_call( + HA_DOMAIN, SERVICE_TURN_OFF, data, context=self._context + ) + + async def _async_switch_turn_on(self, entity_id): + """Turn toggleable device off.""" + data = {ATTR_ENTITY_ID: entity_id} + await self.hass.services.async_call( + HA_DOMAIN, SERVICE_TURN_ON, data, context=self._context + ) + async def async_set_preset_mode(self, preset_mode: str): """Set new preset mode.""" if preset_mode == PRESET_AWAY and not self._is_away: diff --git a/custom_components/dual_smart_thermostat/const.py b/custom_components/dual_smart_thermostat/const.py index 34ecf19..b0a152b 100644 --- a/custom_components/dual_smart_thermostat/const.py +++ b/custom_components/dual_smart_thermostat/const.py @@ -58,3 +58,14 @@ class HVACMode(StrEnum): FAN_ONLY = "fan_only" IDLE = "idle" + + +class HVACAction(StrEnum): + """HVAC action for climate devices.""" + + COOLING = "cooling" + DRYING = "drying" + FAN = "fan" + HEATING = "heating" + IDLE = "idle" + OFF = "off" diff --git a/hacs.json b/hacs.json index 867aa26..08b746f 100644 --- a/hacs.json +++ b/hacs.json @@ -2,9 +2,6 @@ "name": "Dual Smart Thermostat", "render_readme": true, "hide_default_branch": true, - "domains": [ - "climate" - ], "country": [], "homeassistant": "0.118.0", "filename": "ha-dual-smart-thermostat.zip" diff --git a/tests/test_thermostat.py b/tests/test_thermostat.py index 6e2b8bf..0a3c64c 100644 --- a/tests/test_thermostat.py +++ b/tests/test_thermostat.py @@ -301,8 +301,8 @@ async def test_heater_cooler_switch_hvac_modes(hass, setup_comp_1): _setup_sensor(hass, temp_input, 26) await hass.async_block_till_done() - await async_set_hvac_mode(hass, "all", HVACMode.HEAT) + await async_set_hvac_mode(hass, "all", HVACMode.HEAT) assert hass.states.get("climate.test").state == HVAC_MODE_HEAT await async_set_hvac_mode(hass, "all", HVACMode.COOL)