Skip to content

Commit

Permalink
fix: toggling fan_hot_tolerance malfunction
Browse files Browse the repository at this point in the history
Fixes #264
  • Loading branch information
= committed Aug 11, 2024
1 parent ad32463 commit 6e98294
Show file tree
Hide file tree
Showing 6 changed files with 137 additions and 35 deletions.
3 changes: 2 additions & 1 deletion custom_components/dual_smart_thermostat/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -581,7 +581,7 @@ async def _async_startup(*_) -> None:
self.environment.update_floor_temp_from_state(floor_sensor_state)
self.async_write_ha_state()

await self.hvac_device.async_on_startup()
await self.hvac_device.async_on_startup(self.async_write_ha_state)

if self.hass.state == CoreState.running:
await _async_startup()
Expand Down Expand Up @@ -1162,6 +1162,7 @@ async def _async_control_climate(self, time=None, force=False) -> None:

async with self._temp_lock:
await self.hvac_device.async_control_hvac(time, force)

_LOGGER.info(
"updating HVACActionReason: %s", self.hvac_device.HVACActionReason
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
from typing import Callable

from homeassistant.components.climate import HVACMode
from homeassistant.const import STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN
Expand Down Expand Up @@ -52,9 +53,16 @@ def __init__(
if self.fan_device is None or self.cooler_device is None:
_LOGGER.error("Fan or cooler device is not found")

self._set_fan_hot_tolerance_on_state()

def _set_fan_hot_tolerance_on_state(self):
if self._features.fan_hot_tolerance_on_entity is not None:
_LOGGER.debug(
"Setting fan_hot_tolerance_on state: %s",
self.hass.states.get(self._features.fan_hot_tolerance_on_entity).state,
)
self._fan_hot_tolerance_on = (
self.hass.states.get(self._features.fan_hot_tolerance_on_entity)
self.hass.states.get(self._features.fan_hot_tolerance_on_entity).state
== STATE_ON
)
else:
Expand All @@ -71,8 +79,8 @@ def hvac_mode(self, hvac_mode: HVACMode): # noqa: F811
self._hvac_mode = hvac_mode
self.set_sub_devices_hvac_mode(hvac_mode)

async def async_on_startup(self):
await super().async_on_startup()
async def async_on_startup(self, async_write_ha_state_cb: Callable = None) -> None:
await super().async_on_startup(async_write_ha_state_cb)

if self._features.fan_hot_tolerance_on_entity is not None:
self.async_on_remove(
Expand All @@ -90,7 +98,7 @@ async def _async_fan_hot_tolerance_on_changed(

new_state = data["new_state"]

_LOGGER.debug("Fan hot tolerance state changed: %s", new_state)
_LOGGER.debug("Fan hot tolerance on changed: %s", new_state)

if new_state is None or new_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN):
self._fan_hot_tolerance_on = True
Expand All @@ -100,7 +108,8 @@ async def _async_fan_hot_tolerance_on_changed(

_LOGGER.debug("fan_hot_tolerance_on is %s", self._fan_hot_tolerance_on)

await self.async_control_hvac(force=True)
await self.async_control_hvac()
self._async_write_ha_state_cb()

async def _async_check_device_initial_state(self) -> None:
"""Prevent the device from keep running if HVACMode.OFF."""
Expand All @@ -109,6 +118,11 @@ async def _async_check_device_initial_state(self) -> None:
async def async_control_hvac(self, time=None, force=False):
_LOGGER.info({self.__class__.__name__})
_LOGGER.debug("hvac_mode: %s", self._hvac_mode)
self._set_fan_hot_tolerance_on_state()
_LOGGER.debug(
"async_control_hvac fan_hot_tolerance_on: %s", self._fan_hot_tolerance_on
)

match self._hvac_mode:
case HVACMode.COOL:
if self._fan_on_with_cooler:
Expand Down Expand Up @@ -141,6 +155,8 @@ async def async_control_hvac(self, time=None, force=False):
_LOGGER.debug(
"fan_hot_tolerance_on: %s", self._fan_hot_tolerance_on
)
_LOGGER.debug("force_override: %s", force_override)

self.fan_device.hvac_mode = HVACMode.FAN_ONLY
await self.fan_device.async_control_hvac(time, force_override)
await self.cooler_device.async_turn_off()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from datetime import timedelta
import logging
from typing import Callable

from homeassistant.components.climate import HVACMode
from homeassistant.components.valve import DOMAIN as VALVE_DOMAIN, ValveEntityFeature
Expand Down Expand Up @@ -237,8 +238,11 @@ async def async_control_hvac(self, time=None, force=False):
self._hvac_action_reason = self.hvac_controller.hvac_action_reason
self.hvac_power.update_hvac_power(self.strategy, self.target_env_attr)

async def async_on_startup(self):
async def async_on_startup(self, async_write_ha_state_cb: Callable = None):

self._async_write_ha_state_cb = async_write_ha_state_cb
entity_state = self.hass.states.get(self.entity_id)

if entity_state and entity_state.state not in (
STATE_UNAVAILABLE,
STATE_UNKNOWN,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
from typing import Callable

from homeassistant.components.climate import HVACAction, HVACMode
from homeassistant.core import Context, HomeAssistant, callback
Expand Down Expand Up @@ -151,9 +152,10 @@ async def async_control_hvac(self, time=None, force: bool = False):

self._hvac_action_reason = device.HVACActionReason

async def async_on_startup(self):
async def async_on_startup(self, async_write_ha_state_cb: Callable = None):
self._async_write_ha_state_cb = async_write_ha_state_cb
for device in self.hvac_devices:
await device.async_on_startup()
await device.async_on_startup(async_write_ha_state_cb)

async def async_turn_on(self):
await self.async_control_hvac(force=True)
Expand Down
6 changes: 4 additions & 2 deletions tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1065,9 +1065,11 @@ def setup_boolean(hass: HomeAssistant, entity, state) -> None:
hass.states.async_set(entity, state)


def setup_switch(hass: HomeAssistant, is_on: bool) -> None:
def setup_switch(
hass: HomeAssistant, is_on: bool, entity_id: str = common.ENT_SWITCH
) -> None:
"""Set up the test switch."""
hass.states.async_set(common.ENT_SWITCH, STATE_ON if is_on else STATE_OFF)
hass.states.async_set(entity_id, STATE_ON if is_on else STATE_OFF)
calls = []

@callback
Expand Down
125 changes: 101 additions & 24 deletions tests/test_fan_mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -2619,37 +2619,114 @@ async def test_set_target_temp_ac_on_after_fan_tolerance_toggle_off(
hass.states.get(common.ENTITY).attributes["hvac_action"] == HVACAction.COOLING
)

# setup_fan_heat_tolerance_toggle(hass, True)
# await hass.async_block_till_done()
calls = setup_switch(hass, True, cooler_switch)
setup_fan_heat_tolerance_toggle(hass, True)

# assert hass.states.get(cooler_switch).state == STATE_OFF
# assert hass.states.get(fan_switch).state == STATE_ON
# assert hass.states.get(common.ENTITY).attributes["hvac_action"] == HVACAction.FAN
await hass.async_block_till_done()

_LOGGER.debug("after fan_hot_tolerance_toggle on")
_LOGGER.debug("call 1: %s ", calls[0])
_LOGGER.debug("call 2: %s ", calls[1])

# # within hot_tolerance and fan_hot_tolerance
# setup_sensor(hass, 20.5)
# await hass.async_block_till_done()
assert len(calls) == 2
call1 = calls[0]
assert call1.domain == HASS_DOMAIN
assert call1.service == SERVICE_TURN_ON
assert call1.data["entity_id"] == fan_switch

# assert hass.states.get(cooler_switch).state == STATE_OFF
# assert hass.states.get(fan_switch).state == STATE_ON
# assert hass.states.get(common.ENTITY).attributes["hvac_action"] == HVACAction.FAN
call2 = calls[1]
assert call2.domain == HASS_DOMAIN
assert call2.service == SERVICE_TURN_OFF
assert call2.data["entity_id"] == cooler_switch

# # within hot_tolerance and fan_hot_tolerance
# setup_sensor(hass, 20.7)
# await hass.async_block_till_done()
# if toggling in idle state not turningon anything
setup_sensor(hass, 20)
calls = setup_switch(hass, False, cooler_switch)
setup_fan_heat_tolerance_toggle(hass, False)
await hass.async_block_till_done()

# assert hass.states.get(cooler_switch).state == STATE_OFF
# assert hass.states.get(fan_switch).state == STATE_ON
assert len(calls) == 0

setup_fan_heat_tolerance_toggle(hass, True)
await hass.async_block_till_done()

assert len(calls) == 0

# # outside fan_hot_tolerance, within hot_tolerance
# setup_sensor(hass, 20.8)
# await hass.async_block_till_done()

# assert hass.states.get(cooler_switch).state == STATE_ON
# assert hass.states.get(fan_switch).state == STATE_OFF
# assert (
# hass.states.get(common.ENTITY).attributes["hvac_action"] == HVACAction.COOLING
# )
async def test_set_target_temp_ac_on_after_fan_tolerance_toggle_when_idle(
hass: HomeAssistant, setup_comp_1 # noqa: F811
) -> None:
cooler_switch = "input_boolean.test"
fan_switch = "input_boolean.fan"
fan_hot_tolerance_toggle = common.ENT_FAN_HOT_TOLERNACE_TOGGLE

assert await async_setup_component(
hass,
input_boolean.DOMAIN,
{
"input_boolean": {
"test": None,
"fan": None,
"test_fan_hot_tolerance_toggle": None,
}
},
)

assert await async_setup_component(
hass,
input_number.DOMAIN,
{
"input_number": {
"temp": {"name": "test", "initial": 10, "min": 0, "max": 40, "step": 1}
}
},
)

assert await async_setup_component(
hass,
CLIMATE,
{
"climate": {
"platform": DOMAIN,
"name": "test",
"cold_tolerance": 0.2,
"hot_tolerance": 0.2,
"fan_hot_tolerance_toggle": fan_hot_tolerance_toggle,
"ac_mode": True,
"heater": cooler_switch,
"target_sensor": common.ENT_SENSOR,
"fan": fan_switch,
"fan_hot_tolerance": 0.5,
"initial_hvac_mode": HVACMode.OFF,
}
},
)
await hass.async_block_till_done()

await common.async_set_hvac_mode(hass, HVACMode.COOL)
await common.async_set_temperature(hass, 20)
setup_fan_heat_tolerance_toggle(hass, False)
calls = setup_switch(hass, False, cooler_switch)

# below hot_tolerance
setup_sensor(hass, 20)
await hass.async_block_till_done()

assert len(calls) == 0
assert hass.states.get(cooler_switch).state == STATE_OFF
assert hass.states.get(fan_switch).state == STATE_OFF
assert hass.states.get(common.ENTITY).attributes["hvac_action"] == HVACAction.IDLE

# within hot_tolerance and fan_hot_tolerance
# calls = setup_switch(hass, False, cooler_switch)
setup_fan_heat_tolerance_toggle(hass, True)
await hass.async_block_till_done()

assert len(calls) == 0
assert hass.states.get(cooler_switch).state == STATE_OFF
assert hass.states.get(fan_switch).state == STATE_OFF

assert hass.states.get(common.ENTITY).attributes["hvac_action"] == HVACAction.IDLE


async def test_set_target_temp_ac_on_ignore_fan_tolerance(
Expand Down

0 comments on commit 6e98294

Please sign in to comment.