Skip to content

Commit

Permalink
fix: hvac_action_reason empty after restart #266
Browse files Browse the repository at this point in the history
  • Loading branch information
= authored and swingerman committed Jan 8, 2025
1 parent 4183162 commit a0e3982
Show file tree
Hide file tree
Showing 8 changed files with 232 additions and 98 deletions.
26 changes: 10 additions & 16 deletions config/configuration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions custom_components/dual_smart_thermostat/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
4 changes: 2 additions & 2 deletions hacs.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
283 changes: 207 additions & 76 deletions tests/test_dual_mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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"],
[
Expand Down
Loading

0 comments on commit a0e3982

Please sign in to comment.