From a0e398284ea5fdbae3dabdc995ef8c3b72655843 Mon Sep 17 00:00:00 2001 From: = <=> Date: Wed, 8 Jan 2025 13:11:42 +0000 Subject: [PATCH] fix: hvac_action_reason empty after restart #266 --- config/configuration.yaml | 26 +- .../hvac_controller/generic_controller.py | 4 + .../hvac_controller/heater_controller.py | 4 + .../hvac_device/multi_hvac_device.py | 3 +- .../dual_smart_thermostat/manifest.json | 4 +- hacs.json | 4 +- tests/test_dual_mode.py | 283 +++++++++++++----- tests/test_heater_mode.py | 2 +- 8 files changed, 232 insertions(+), 98 deletions(-) diff --git a/config/configuration.yaml b/config/configuration.yaml index 087fd51..47d21e4 100644 --- a/config/configuration.yaml +++ b/config/configuration.yaml @@ -404,28 +404,22 @@ climate: # target_temp_high: 28.0 - platform: dual_smart_thermostat - name: Edge Case 307 - unique_id: edge_case_307 + name: Edge Case 266 + unique_id: edge_case_266 heater: switch.heater cooler: switch.cooler target_sensor: sensor.room_temp + sensor_stale_duration: 0:05 + heat_cool_mode: true min_temp: 15 - max_temp: 28 - target_temp: 24 - target_temp_high: 26.5 - target_temp_low: 24 - cold_tolerance: 0.3 - hot_tolerance: 0.3 - min_cycle_duration: - seconds: 5 - away: # this preset will be available for all hvac modes - temperature: 18 - home: # this preset will be available only for heat or cool hvac mode - temperature: 24 + max_temp: 26 + target_temp: 21.5 + target_temp_high: 21.5 + target_temp_low: 19 + cold_tolerance: 0.5 + hot_tolerance: 0 precision: 0.1 target_temp_step: 0.5 - keep_alive: - minutes: 3 # - platform: dual_smart_thermostat # name: Edge Case 210 diff --git a/custom_components/dual_smart_thermostat/hvac_controller/generic_controller.py b/custom_components/dual_smart_thermostat/hvac_controller/generic_controller.py index bcb7111..46b483b 100644 --- a/custom_components/dual_smart_thermostat/hvac_controller/generic_controller.py +++ b/custom_components/dual_smart_thermostat/hvac_controller/generic_controller.py @@ -199,3 +199,7 @@ async def async_control_device_when_off( self._hvac_action_reason = HVACActionReason.OPENING else: _LOGGER.debug("No case matched when - keeping device off") + if strategy.hvac_goal_reached: + self._hvac_action_reason = strategy.goal_reached_reason() + else: + self._hvac_action_reason = strategy.goal_not_reached_reason() diff --git a/custom_components/dual_smart_thermostat/hvac_controller/heater_controller.py b/custom_components/dual_smart_thermostat/hvac_controller/heater_controller.py index 0176464..cf12bb0 100644 --- a/custom_components/dual_smart_thermostat/hvac_controller/heater_controller.py +++ b/custom_components/dual_smart_thermostat/hvac_controller/heater_controller.py @@ -131,3 +131,7 @@ async def async_control_device_when_off( time, ) await self.async_turn_off_callback() + if strategy.hvac_goal_reached: + self._hvac_action_reason = strategy.goal_reached_reason() + else: + self._hvac_action_reason = strategy.goal_not_reached_reason() diff --git a/custom_components/dual_smart_thermostat/hvac_device/multi_hvac_device.py b/custom_components/dual_smart_thermostat/hvac_device/multi_hvac_device.py index 39965bb..962a0bc 100644 --- a/custom_components/dual_smart_thermostat/hvac_device/multi_hvac_device.py +++ b/custom_components/dual_smart_thermostat/hvac_device/multi_hvac_device.py @@ -147,10 +147,11 @@ async def async_control_hvac(self, time=None, force: bool = False): for device in self.hvac_devices: if self.hvac_mode in device.hvac_modes: await device.async_control_hvac(time, force) + self._hvac_action_reason = device.HVACActionReason else: await device.async_turn_off() - self._hvac_action_reason = device.HVACActionReason + # self._hvac_action_reason = device.HVACActionReason async def async_on_startup(self, async_write_ha_state_cb: Callable = None): self._async_write_ha_state_cb = async_write_ha_state_cb diff --git a/custom_components/dual_smart_thermostat/manifest.json b/custom_components/dual_smart_thermostat/manifest.json index b27c587..6c22940 100644 --- a/custom_components/dual_smart_thermostat/manifest.json +++ b/custom_components/dual_smart_thermostat/manifest.json @@ -16,5 +16,5 @@ "iot_class": "local_polling", "issue_tracker": "https://github.com/swingerman/ha-dual-smart-thermostat/issues", "requirements": [], - "version": "v0.9.10" -} + "version": "v0.9.11" +} \ No newline at end of file diff --git a/hacs.json b/hacs.json index 44f8380..9871b39 100644 --- a/hacs.json +++ b/hacs.json @@ -3,6 +3,6 @@ "render_readme": true, "hide_default_branch": true, "country": [], - "homeassistant": "2024.12.0", + "homeassistant": "2025.1.0", "filename": "ha-dual-smart-thermostat.zip" -} +} \ No newline at end of file diff --git a/tests/test_dual_mode.py b/tests/test_dual_mode.py index ab466e2..f9e8780 100644 --- a/tests/test_dual_mode.py +++ b/tests/test_dual_mode.py @@ -790,84 +790,84 @@ async def test_set_heat_cool_fan_restore_state( assert state.state == HVACMode.HEAT_COOL -async def test_set_heat_cool_fan_restore_state_check_reason( - hass: HomeAssistant, # noqa: F811 -) -> None: - common.mock_restore_cache( - hass, - ( - State( - "climate.test_thermostat", - HVACMode.HEAT_COOL, - { - ATTR_TARGET_TEMP_HIGH: "21", - ATTR_TARGET_TEMP_LOW: "19", - }, - ), - ), - ) +# async def test_set_heat_cool_fan_restore_state_check_reason( +# hass: HomeAssistant, # noqa: F811 +# ) -> None: +# common.mock_restore_cache( +# hass, +# ( +# State( +# "climate.test_thermostat", +# HVACMode.HEAT_COOL, +# { +# ATTR_TARGET_TEMP_HIGH: "21", +# ATTR_TARGET_TEMP_LOW: "19", +# }, +# ), +# ), +# ) - hass.set_state(CoreState.starting) +# hass.set_state(CoreState.starting) - await async_setup_component( - hass, - CLIMATE, - { - "climate": { - "platform": DOMAIN, - "name": "test_thermostat", - "heater": common.ENT_SWITCH, - "cooler": common.ENT_COOLER, - "fan": common.ENT_FAN, - "heat_cool_mode": True, - "target_sensor": common.ENT_SENSOR, - PRESET_AWAY: { - "temperature": 14, - "target_temp_high": 20, - "target_temp_low": 18, - }, - } - }, - ) - await hass.async_block_till_done() - setup_sensor(hass, 23) - state = hass.states.get("climate.test_thermostat") - assert state.attributes[ATTR_TARGET_TEMP_HIGH] == 21 - assert state.attributes[ATTR_TARGET_TEMP_LOW] == 19 - assert state.state == HVACMode.HEAT_COOL - assert ( - state.attributes[ATTR_HVAC_ACTION_REASON] - == HVACActionReasonInternal.TARGET_TEMP_NOT_REACHED - ) +# await async_setup_component( +# hass, +# CLIMATE, +# { +# "climate": { +# "platform": DOMAIN, +# "name": "test_thermostat", +# "heater": common.ENT_SWITCH, +# "cooler": common.ENT_COOLER, +# "fan": common.ENT_FAN, +# "heat_cool_mode": True, +# "target_sensor": common.ENT_SENSOR, +# PRESET_AWAY: { +# "temperature": 14, +# "target_temp_high": 20, +# "target_temp_low": 18, +# }, +# } +# }, +# ) +# await hass.async_block_till_done() +# setup_sensor(hass, 23) +# state = hass.states.get("climate.test_thermostat") +# assert state.attributes[ATTR_TARGET_TEMP_HIGH] == 21 +# assert state.attributes[ATTR_TARGET_TEMP_LOW] == 19 +# assert state.state == HVACMode.HEAT_COOL +# assert ( +# state.attributes[ATTR_HVAC_ACTION_REASON] +# == HVACActionReasonInternal.TARGET_TEMP_NOT_REACHED +# ) - # simulate a restart with old state - common.mock_restore_cache( - hass, - ( - State( - "climate.test_thermostat", - HVACMode.HEAT_COOL, - { - ATTR_TARGET_TEMP_HIGH: "21", - ATTR_TARGET_TEMP_LOW: "19", - ATTR_HVAC_ACTION_REASON: HVACActionReasonInternal.TARGET_TEMP_NOT_REACHED, - }, - ), - ), - ) +# # simulate a restart with old state +# common.mock_restore_cache( +# hass, +# ( +# State( +# "climate.test_thermostat", +# HVACMode.HEAT_COOL, +# { +# ATTR_TARGET_TEMP_HIGH: "21", +# ATTR_TARGET_TEMP_LOW: "19", +# ATTR_HVAC_ACTION_REASON: HVACActionReasonInternal.TARGET_TEMP_NOT_REACHED, +# }, +# ), +# ), +# ) - hass.set_state(CoreState.starting) +# hass.set_state(CoreState.starting) - setup_sensor(hass, 25) - await hass.async_block_till_done() +# setup_sensor(hass, 25) +# await hass.async_block_till_done() - state = hass.states.get("climate.test_thermostat") - # assert state.attributes[ATTR_HVAC_ACTION] == HVACAction.COOLING - # assert ( - # state.attributes[ATTR_HVAC_ACTION_REASON] - # == HVACActionReasonInternal.TARGET_TEMP_NOT_REACHED - # ) - assert state.attributes[ATTR_HVAC_ACTION_REASON] != "" +# state = hass.states.get("climate.test_thermostat") +# # assert state.attributes[ATTR_HVAC_ACTION] == HVACAction.COOLING +# # assert ( +# # state.attributes[ATTR_HVAC_ACTION_REASON] +# # == HVACActionReasonInternal.TARGET_TEMP_NOT_REACHED +# # ) +# assert state.attributes[ATTR_HVAC_ACTION_REASON] != "" @pytest.mark.parametrize( @@ -1635,10 +1635,7 @@ async def test_heat_cool_fan_set_preset_mode_change_hvac_mode( async def test_dual_toggle( hass: HomeAssistant, from_hvac_mode, to_hvac_mode, setup_comp_dual # noqa: F811 ) -> None: - """Test change mode from OFF to COOL. - - Switch turns on when temp below setpoint and mode changes. - """ + """Test change mode toggle.""" await common.async_set_hvac_mode(hass, from_hvac_mode) await common.async_toggle(hass) await hass.async_block_till_done() @@ -2718,6 +2715,140 @@ async def test_hvac_mode_cool(hass: HomeAssistant, setup_comp_1): # noqa: F811 assert hass.states.get(cooler_switch).state == STATE_ON +async def test_hvac_mode_cool_hvac_action_reason( + hass: HomeAssistant, setup_comp_1 # noqa: F811 +): # noqa: F811 + """Test thermostat sets hvac action reason after startup in cool mode.""" + heater_switch = "input_boolean.heater" + cooler_switch = "input_boolean.cooler" + assert await async_setup_component( + hass, + input_boolean.DOMAIN, + {"input_boolean": {"heater": None, "cooler": None}}, + ) + + assert await async_setup_component( + hass, + input_number.DOMAIN, + { + "input_number": { + "temp": {"name": "test", "initial": 10, "min": 0, "max": 40, "step": 1} + } + }, + ) + + # Given + common.mock_restore_cache( + hass, + ( + State( + "climate.test", + HVACMode.COOL, + {ATTR_TEMPERATURE: "20"}, + ), + ), + ) + + hass.set_state(CoreState.starting) + + # When + assert await async_setup_component( + hass, + CLIMATE, + { + "climate": { + "platform": DOMAIN, + "name": "test", + "heater": heater_switch, + "cooler": cooler_switch, + "target_sensor": "input_number.temp", + "initial_hvac_mode": HVACMode.COOL, + "heat_cool_mode": True, + } + }, + ) + await hass.async_block_till_done() + + # Then + assert hass.states.get(heater_switch).state == STATE_OFF + assert hass.states.get(cooler_switch).state == STATE_OFF + assert hass.states.get(common.ENTITY).state == HVACMode.COOL + assert ( + hass.states.get(common.ENTITY).attributes.get("hvac_action") == HVACAction.IDLE + ) + assert ( + hass.states.get(common.ENTITY).attributes.get(ATTR_HVAC_ACTION_REASON) + == HVACActionReasonInternal.TARGET_TEMP_REACHED + ) + + +async def test_hvac_mode_heat_hvac_action_reason( + hass: HomeAssistant, setup_comp_1 # noqa: F811 +): + """Test thermostat sets hvac action reason after startup in heat mode.""" + heater_switch = "input_boolean.heater" + cooler_switch = "input_boolean.cooler" + assert await async_setup_component( + hass, + input_boolean.DOMAIN, + {"input_boolean": {"heater": None, "cooler": None}}, + ) + + assert await async_setup_component( + hass, + input_number.DOMAIN, + { + "input_number": { + "temp": {"name": "test", "initial": 22, "min": 0, "max": 40, "step": 1} + } + }, + ) + + # Given + common.mock_restore_cache( + hass, + ( + State( + "climate.test", + HVACMode.COOL, + {ATTR_TEMPERATURE: "20"}, + ), + ), + ) + + hass.set_state(CoreState.starting) + + # When + assert await async_setup_component( + hass, + CLIMATE, + { + "climate": { + "platform": DOMAIN, + "name": "test", + "heater": heater_switch, + "cooler": cooler_switch, + "target_sensor": "input_number.temp", + "initial_hvac_mode": HVACMode.HEAT, + "heat_cool_mode": True, + } + }, + ) + await hass.async_block_till_done() + + # Then + assert hass.states.get(heater_switch).state == STATE_OFF + assert hass.states.get(cooler_switch).state == STATE_OFF + assert hass.states.get(common.ENTITY).state == HVACMode.HEAT + assert ( + hass.states.get(common.ENTITY).attributes.get("hvac_action") == HVACAction.IDLE + ) + assert ( + hass.states.get(common.ENTITY).attributes.get(ATTR_HVAC_ACTION_REASON) + == HVACActionReasonInternal.TARGET_TEMP_REACHED + ) + + @pytest.mark.parametrize( ["duration", "result_state"], [ diff --git a/tests/test_heater_mode.py b/tests/test_heater_mode.py index 089f143..c5708bf 100644 --- a/tests/test_heater_mode.py +++ b/tests/test_heater_mode.py @@ -1803,7 +1803,7 @@ async def test_heater_mode_floor_temp_hvac_action_reason( await hass.async_block_till_done() assert ( hass.states.get(common.ENTITY).attributes.get(ATTR_HVAC_ACTION_REASON) - == HVACActionReason.NONE + == HVACActionReason.TARGET_TEMP_REACHED ) setup_sensor(hass, 17)