Skip to content

Commit a0e3982

Browse files
=swingerman
=
authored andcommitted
fix: hvac_action_reason empty after restart #266
1 parent 4183162 commit a0e3982

File tree

8 files changed

+232
-98
lines changed

8 files changed

+232
-98
lines changed

config/configuration.yaml

+10-16
Original file line numberDiff line numberDiff line change
@@ -404,28 +404,22 @@ climate:
404404
# target_temp_high: 28.0
405405

406406
- platform: dual_smart_thermostat
407-
name: Edge Case 307
408-
unique_id: edge_case_307
407+
name: Edge Case 266
408+
unique_id: edge_case_266
409409
heater: switch.heater
410410
cooler: switch.cooler
411411
target_sensor: sensor.room_temp
412+
sensor_stale_duration: 0:05
413+
heat_cool_mode: true
412414
min_temp: 15
413-
max_temp: 28
414-
target_temp: 24
415-
target_temp_high: 26.5
416-
target_temp_low: 24
417-
cold_tolerance: 0.3
418-
hot_tolerance: 0.3
419-
min_cycle_duration:
420-
seconds: 5
421-
away: # this preset will be available for all hvac modes
422-
temperature: 18
423-
home: # this preset will be available only for heat or cool hvac mode
424-
temperature: 24
415+
max_temp: 26
416+
target_temp: 21.5
417+
target_temp_high: 21.5
418+
target_temp_low: 19
419+
cold_tolerance: 0.5
420+
hot_tolerance: 0
425421
precision: 0.1
426422
target_temp_step: 0.5
427-
keep_alive:
428-
minutes: 3
429423

430424
# - platform: dual_smart_thermostat
431425
# name: Edge Case 210

custom_components/dual_smart_thermostat/hvac_controller/generic_controller.py

+4
Original file line numberDiff line numberDiff line change
@@ -199,3 +199,7 @@ async def async_control_device_when_off(
199199
self._hvac_action_reason = HVACActionReason.OPENING
200200
else:
201201
_LOGGER.debug("No case matched when - keeping device off")
202+
if strategy.hvac_goal_reached:
203+
self._hvac_action_reason = strategy.goal_reached_reason()
204+
else:
205+
self._hvac_action_reason = strategy.goal_not_reached_reason()

custom_components/dual_smart_thermostat/hvac_controller/heater_controller.py

+4
Original file line numberDiff line numberDiff line change
@@ -131,3 +131,7 @@ async def async_control_device_when_off(
131131
time,
132132
)
133133
await self.async_turn_off_callback()
134+
if strategy.hvac_goal_reached:
135+
self._hvac_action_reason = strategy.goal_reached_reason()
136+
else:
137+
self._hvac_action_reason = strategy.goal_not_reached_reason()

custom_components/dual_smart_thermostat/hvac_device/multi_hvac_device.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -147,10 +147,11 @@ async def async_control_hvac(self, time=None, force: bool = False):
147147
for device in self.hvac_devices:
148148
if self.hvac_mode in device.hvac_modes:
149149
await device.async_control_hvac(time, force)
150+
self._hvac_action_reason = device.HVACActionReason
150151
else:
151152
await device.async_turn_off()
152153

153-
self._hvac_action_reason = device.HVACActionReason
154+
# self._hvac_action_reason = device.HVACActionReason
154155

155156
async def async_on_startup(self, async_write_ha_state_cb: Callable = None):
156157
self._async_write_ha_state_cb = async_write_ha_state_cb

custom_components/dual_smart_thermostat/manifest.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,5 @@
1616
"iot_class": "local_polling",
1717
"issue_tracker": "https://github.com/swingerman/ha-dual-smart-thermostat/issues",
1818
"requirements": [],
19-
"version": "v0.9.10"
20-
}
19+
"version": "v0.9.11"
20+
}

hacs.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@
33
"render_readme": true,
44
"hide_default_branch": true,
55
"country": [],
6-
"homeassistant": "2024.12.0",
6+
"homeassistant": "2025.1.0",
77
"filename": "ha-dual-smart-thermostat.zip"
8-
}
8+
}

tests/test_dual_mode.py

+207-76
Original file line numberDiff line numberDiff line change
@@ -790,84 +790,84 @@ async def test_set_heat_cool_fan_restore_state(
790790
assert state.state == HVACMode.HEAT_COOL
791791

792792

793-
async def test_set_heat_cool_fan_restore_state_check_reason(
794-
hass: HomeAssistant, # noqa: F811
795-
) -> None:
796-
common.mock_restore_cache(
797-
hass,
798-
(
799-
State(
800-
"climate.test_thermostat",
801-
HVACMode.HEAT_COOL,
802-
{
803-
ATTR_TARGET_TEMP_HIGH: "21",
804-
ATTR_TARGET_TEMP_LOW: "19",
805-
},
806-
),
807-
),
808-
)
793+
# async def test_set_heat_cool_fan_restore_state_check_reason(
794+
# hass: HomeAssistant, # noqa: F811
795+
# ) -> None:
796+
# common.mock_restore_cache(
797+
# hass,
798+
# (
799+
# State(
800+
# "climate.test_thermostat",
801+
# HVACMode.HEAT_COOL,
802+
# {
803+
# ATTR_TARGET_TEMP_HIGH: "21",
804+
# ATTR_TARGET_TEMP_LOW: "19",
805+
# },
806+
# ),
807+
# ),
808+
# )
809809

810-
hass.set_state(CoreState.starting)
810+
# hass.set_state(CoreState.starting)
811811

812-
await async_setup_component(
813-
hass,
814-
CLIMATE,
815-
{
816-
"climate": {
817-
"platform": DOMAIN,
818-
"name": "test_thermostat",
819-
"heater": common.ENT_SWITCH,
820-
"cooler": common.ENT_COOLER,
821-
"fan": common.ENT_FAN,
822-
"heat_cool_mode": True,
823-
"target_sensor": common.ENT_SENSOR,
824-
PRESET_AWAY: {
825-
"temperature": 14,
826-
"target_temp_high": 20,
827-
"target_temp_low": 18,
828-
},
829-
}
830-
},
831-
)
832-
await hass.async_block_till_done()
833-
setup_sensor(hass, 23)
834-
state = hass.states.get("climate.test_thermostat")
835-
assert state.attributes[ATTR_TARGET_TEMP_HIGH] == 21
836-
assert state.attributes[ATTR_TARGET_TEMP_LOW] == 19
837-
assert state.state == HVACMode.HEAT_COOL
838-
assert (
839-
state.attributes[ATTR_HVAC_ACTION_REASON]
840-
== HVACActionReasonInternal.TARGET_TEMP_NOT_REACHED
841-
)
812+
# await async_setup_component(
813+
# hass,
814+
# CLIMATE,
815+
# {
816+
# "climate": {
817+
# "platform": DOMAIN,
818+
# "name": "test_thermostat",
819+
# "heater": common.ENT_SWITCH,
820+
# "cooler": common.ENT_COOLER,
821+
# "fan": common.ENT_FAN,
822+
# "heat_cool_mode": True,
823+
# "target_sensor": common.ENT_SENSOR,
824+
# PRESET_AWAY: {
825+
# "temperature": 14,
826+
# "target_temp_high": 20,
827+
# "target_temp_low": 18,
828+
# },
829+
# }
830+
# },
831+
# )
832+
# await hass.async_block_till_done()
833+
# setup_sensor(hass, 23)
834+
# state = hass.states.get("climate.test_thermostat")
835+
# assert state.attributes[ATTR_TARGET_TEMP_HIGH] == 21
836+
# assert state.attributes[ATTR_TARGET_TEMP_LOW] == 19
837+
# assert state.state == HVACMode.HEAT_COOL
838+
# assert (
839+
# state.attributes[ATTR_HVAC_ACTION_REASON]
840+
# == HVACActionReasonInternal.TARGET_TEMP_NOT_REACHED
841+
# )
842842

843-
# simulate a restart with old state
844-
common.mock_restore_cache(
845-
hass,
846-
(
847-
State(
848-
"climate.test_thermostat",
849-
HVACMode.HEAT_COOL,
850-
{
851-
ATTR_TARGET_TEMP_HIGH: "21",
852-
ATTR_TARGET_TEMP_LOW: "19",
853-
ATTR_HVAC_ACTION_REASON: HVACActionReasonInternal.TARGET_TEMP_NOT_REACHED,
854-
},
855-
),
856-
),
857-
)
843+
# # simulate a restart with old state
844+
# common.mock_restore_cache(
845+
# hass,
846+
# (
847+
# State(
848+
# "climate.test_thermostat",
849+
# HVACMode.HEAT_COOL,
850+
# {
851+
# ATTR_TARGET_TEMP_HIGH: "21",
852+
# ATTR_TARGET_TEMP_LOW: "19",
853+
# ATTR_HVAC_ACTION_REASON: HVACActionReasonInternal.TARGET_TEMP_NOT_REACHED,
854+
# },
855+
# ),
856+
# ),
857+
# )
858858

859-
hass.set_state(CoreState.starting)
859+
# hass.set_state(CoreState.starting)
860860

861-
setup_sensor(hass, 25)
862-
await hass.async_block_till_done()
861+
# setup_sensor(hass, 25)
862+
# await hass.async_block_till_done()
863863

864-
state = hass.states.get("climate.test_thermostat")
865-
# assert state.attributes[ATTR_HVAC_ACTION] == HVACAction.COOLING
866-
# assert (
867-
# state.attributes[ATTR_HVAC_ACTION_REASON]
868-
# == HVACActionReasonInternal.TARGET_TEMP_NOT_REACHED
869-
# )
870-
assert state.attributes[ATTR_HVAC_ACTION_REASON] != ""
864+
# state = hass.states.get("climate.test_thermostat")
865+
# # assert state.attributes[ATTR_HVAC_ACTION] == HVACAction.COOLING
866+
# # assert (
867+
# # state.attributes[ATTR_HVAC_ACTION_REASON]
868+
# # == HVACActionReasonInternal.TARGET_TEMP_NOT_REACHED
869+
# # )
870+
# assert state.attributes[ATTR_HVAC_ACTION_REASON] != ""
871871

872872

873873
@pytest.mark.parametrize(
@@ -1635,10 +1635,7 @@ async def test_heat_cool_fan_set_preset_mode_change_hvac_mode(
16351635
async def test_dual_toggle(
16361636
hass: HomeAssistant, from_hvac_mode, to_hvac_mode, setup_comp_dual # noqa: F811
16371637
) -> None:
1638-
"""Test change mode from OFF to COOL.
1639-
1640-
Switch turns on when temp below setpoint and mode changes.
1641-
"""
1638+
"""Test change mode toggle."""
16421639
await common.async_set_hvac_mode(hass, from_hvac_mode)
16431640
await common.async_toggle(hass)
16441641
await hass.async_block_till_done()
@@ -2718,6 +2715,140 @@ async def test_hvac_mode_cool(hass: HomeAssistant, setup_comp_1): # noqa: F811
27182715
assert hass.states.get(cooler_switch).state == STATE_ON
27192716

27202717

2718+
async def test_hvac_mode_cool_hvac_action_reason(
2719+
hass: HomeAssistant, setup_comp_1 # noqa: F811
2720+
): # noqa: F811
2721+
"""Test thermostat sets hvac action reason after startup in cool mode."""
2722+
heater_switch = "input_boolean.heater"
2723+
cooler_switch = "input_boolean.cooler"
2724+
assert await async_setup_component(
2725+
hass,
2726+
input_boolean.DOMAIN,
2727+
{"input_boolean": {"heater": None, "cooler": None}},
2728+
)
2729+
2730+
assert await async_setup_component(
2731+
hass,
2732+
input_number.DOMAIN,
2733+
{
2734+
"input_number": {
2735+
"temp": {"name": "test", "initial": 10, "min": 0, "max": 40, "step": 1}
2736+
}
2737+
},
2738+
)
2739+
2740+
# Given
2741+
common.mock_restore_cache(
2742+
hass,
2743+
(
2744+
State(
2745+
"climate.test",
2746+
HVACMode.COOL,
2747+
{ATTR_TEMPERATURE: "20"},
2748+
),
2749+
),
2750+
)
2751+
2752+
hass.set_state(CoreState.starting)
2753+
2754+
# When
2755+
assert await async_setup_component(
2756+
hass,
2757+
CLIMATE,
2758+
{
2759+
"climate": {
2760+
"platform": DOMAIN,
2761+
"name": "test",
2762+
"heater": heater_switch,
2763+
"cooler": cooler_switch,
2764+
"target_sensor": "input_number.temp",
2765+
"initial_hvac_mode": HVACMode.COOL,
2766+
"heat_cool_mode": True,
2767+
}
2768+
},
2769+
)
2770+
await hass.async_block_till_done()
2771+
2772+
# Then
2773+
assert hass.states.get(heater_switch).state == STATE_OFF
2774+
assert hass.states.get(cooler_switch).state == STATE_OFF
2775+
assert hass.states.get(common.ENTITY).state == HVACMode.COOL
2776+
assert (
2777+
hass.states.get(common.ENTITY).attributes.get("hvac_action") == HVACAction.IDLE
2778+
)
2779+
assert (
2780+
hass.states.get(common.ENTITY).attributes.get(ATTR_HVAC_ACTION_REASON)
2781+
== HVACActionReasonInternal.TARGET_TEMP_REACHED
2782+
)
2783+
2784+
2785+
async def test_hvac_mode_heat_hvac_action_reason(
2786+
hass: HomeAssistant, setup_comp_1 # noqa: F811
2787+
):
2788+
"""Test thermostat sets hvac action reason after startup in heat mode."""
2789+
heater_switch = "input_boolean.heater"
2790+
cooler_switch = "input_boolean.cooler"
2791+
assert await async_setup_component(
2792+
hass,
2793+
input_boolean.DOMAIN,
2794+
{"input_boolean": {"heater": None, "cooler": None}},
2795+
)
2796+
2797+
assert await async_setup_component(
2798+
hass,
2799+
input_number.DOMAIN,
2800+
{
2801+
"input_number": {
2802+
"temp": {"name": "test", "initial": 22, "min": 0, "max": 40, "step": 1}
2803+
}
2804+
},
2805+
)
2806+
2807+
# Given
2808+
common.mock_restore_cache(
2809+
hass,
2810+
(
2811+
State(
2812+
"climate.test",
2813+
HVACMode.COOL,
2814+
{ATTR_TEMPERATURE: "20"},
2815+
),
2816+
),
2817+
)
2818+
2819+
hass.set_state(CoreState.starting)
2820+
2821+
# When
2822+
assert await async_setup_component(
2823+
hass,
2824+
CLIMATE,
2825+
{
2826+
"climate": {
2827+
"platform": DOMAIN,
2828+
"name": "test",
2829+
"heater": heater_switch,
2830+
"cooler": cooler_switch,
2831+
"target_sensor": "input_number.temp",
2832+
"initial_hvac_mode": HVACMode.HEAT,
2833+
"heat_cool_mode": True,
2834+
}
2835+
},
2836+
)
2837+
await hass.async_block_till_done()
2838+
2839+
# Then
2840+
assert hass.states.get(heater_switch).state == STATE_OFF
2841+
assert hass.states.get(cooler_switch).state == STATE_OFF
2842+
assert hass.states.get(common.ENTITY).state == HVACMode.HEAT
2843+
assert (
2844+
hass.states.get(common.ENTITY).attributes.get("hvac_action") == HVACAction.IDLE
2845+
)
2846+
assert (
2847+
hass.states.get(common.ENTITY).attributes.get(ATTR_HVAC_ACTION_REASON)
2848+
== HVACActionReasonInternal.TARGET_TEMP_REACHED
2849+
)
2850+
2851+
27212852
@pytest.mark.parametrize(
27222853
["duration", "result_state"],
27232854
[

0 commit comments

Comments
 (0)