Skip to content

Commit

Permalink
ignore state changes if they are None
Browse files Browse the repository at this point in the history
Can happen when the controller goes offline/online. We shouldn't forget
the stored data in that case.

nielsfaber#24

e.g. if the controller goes offline, we don't want to forget what we had
before
  • Loading branch information
iainlane committed Jan 29, 2024
1 parent 2fc2083 commit ff1d170
Showing 1 changed file with 170 additions and 63 deletions.
233 changes: 170 additions & 63 deletions custom_components/zoned_heating/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,7 @@
ATTR_TEMPERATURE,
Platform,
)
from homeassistant.core import (
HomeAssistant,
callback
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.entity import ToggleEntity
Expand Down Expand Up @@ -51,18 +48,30 @@ async def async_setup_entry(
controller = config_entry.options.get(const.CONF_CONTROLLER)
zones = config_entry.options.get(const.CONF_ZONES, [])
max_setpoint = config_entry.options.get(const.CONF_MAX_SETPOINT)
controller_delay_time = config_entry.options.get(const.CONF_CONTROLLER_DELAY_TIME, const.DEFAULT_CONTROLLER_DELAY_TIME)

async_add_entities([
ZonedHeaterSwitch(hass, controller, zones, max_setpoint, controller_delay_time)
])
controller_delay_time = config_entry.options.get(
const.CONF_CONTROLLER_DELAY_TIME, const.DEFAULT_CONTROLLER_DELAY_TIME
)

async_add_entities(
[
ZonedHeaterSwitch(
hass, controller, zones, max_setpoint, controller_delay_time
)
]
)


class ZonedHeaterSwitch(ToggleEntity, RestoreEntity):

_attr_name = "Zoned Heating"

def __init__(self, hass, controller_entity, zone_entities, max_setpoint, controller_delay_time):
def __init__(
self,
hass,
controller_entity,
zone_entities,
max_setpoint,
controller_delay_time,
):
self.hass = hass
self._controller_entity = controller_entity
self._zone_entities = zone_entities
Expand All @@ -86,17 +95,29 @@ async def async_added_to_hass(self):
if state:
self._enabled = state.state == STATE_ON
self._override_active = state.attributes.get(const.ATTR_OVERRIDE_ACTIVE)
self._temperature_increase = state.attributes.get(const.ATTR_TEMPERATURE_INCREASE)
self._stored_controller_setpoint = state.attributes.get(const.ATTR_STORED_CONTROLLER_SETPOINT)
self._stored_controller_state = state.attributes.get(const.ATTR_STORED_CONTROLLER_STATE)
self._temperature_increase = state.attributes.get(
const.ATTR_TEMPERATURE_INCREASE
)
self._stored_controller_setpoint = state.attributes.get(
const.ATTR_STORED_CONTROLLER_SETPOINT
)
self._stored_controller_state = state.attributes.get(
const.ATTR_STORED_CONTROLLER_STATE
)
else:
self._enabled = True

if self._enabled:
await self.async_start_state_listeners()
_LOGGER.debug("Zoned heating initialized. enabled={}, override_active={}, temperature_increase={}, stored_controller_setpoint={}, stored_controller_state={}".format(
self._enabled, self._override_active, self._temperature_increase, self._stored_controller_setpoint, self._stored_controller_state
))
_LOGGER.debug(
"Zoned heating initialized. enabled={}, override_active={}, temperature_increase={}, stored_controller_setpoint={}, stored_controller_state={}".format(
self._enabled,
self._override_active,
self._temperature_increase,
self._stored_controller_setpoint,
self._stored_controller_state,
)
)
await self.async_calculate_override()

async def async_will_remove_from_hass(self):
Expand Down Expand Up @@ -155,7 +176,7 @@ async def async_start_state_listeners(self):
self.hass,
self._zone_entities,
self.async_zone_state_changed,
)
),
]

async def async_stop_state_listeners(self):
Expand All @@ -167,22 +188,71 @@ async def async_stop_state_listeners(self):
async def async_controller_state_changed(self, entity, old_state, new_state):
"""fired when controller entity changes"""
if self._ignore_controller_state_change_timer:
_LOGGER.debug("Ignoring controller state change, ignore timer active. old_state={}, new_state={}".format(old_state, new_state))
_LOGGER.debug(
"Ignoring controller state change, ignore timer active. old_state={}, new_state={}".format(
old_state, new_state
)
)
return
if not self._override_active:
_LOGGER.debug("Ignoring controller state change, override not active. old_state={}, new_state={}".format(old_state, new_state))
_LOGGER.debug(
"Ignoring controller state change, override not active. old_state={}, new_state={}".format(
old_state, new_state
)
)
return

old_state = parse_state(old_state)
new_state = parse_state(new_state)

if new_state[ATTR_TEMPERATURE] is None:
# controller setpoint is not set, nothing to do
_LOGGER.debug(
"Ignoring controller state change, controller setpoint not set (device going offline?) old_state={}, new_state={}".format(
old_state, new_state
)
)
return

if old_state[ATTR_TEMPERATURE] is None:
# controller setpoint is not set, nothing to do
_LOGGER.debug(
"Ignoring controller state change, old controller setpoint was not set (device coming online?). old_state={}, new_state={}".format(
old_state, new_state
)
)
return

if new_state[ATTR_HVAC_MODE] is None:
# controller mode is not set, nothing to do
_LOGGER.debug(
"Ignoring controller state change, controller mode not set (device going offline?). old_state={}, new_state={}".format(
old_state, new_state
)
)
return

if old_state[ATTR_HVAC_MODE] is None:
# controller mode is not set, nothing to do
_LOGGER.debug(
"Ignoring controller state change, old controller mode not set (device coming online?). old_state={}, new_state={}".format(
old_state, new_state
)
)
return

if new_state[ATTR_TEMPERATURE] != old_state[ATTR_TEMPERATURE]:
# if controller setpoint has changed, make sure to store it
_LOGGER.debug("Storing controller setpoint={}".format(new_state[ATTR_TEMPERATURE]))
_LOGGER.debug(
"Storing controller setpoint={}".format(new_state[ATTR_TEMPERATURE])
)
self._stored_controller_setpoint = new_state[ATTR_TEMPERATURE]
self.async_write_ha_state()

if new_state[ATTR_HVAC_MODE] != old_state[ATTR_HVAC_MODE] and new_state[ATTR_HVAC_MODE] == HVACAction.OFF:
if (
new_state[ATTR_HVAC_MODE] != old_state[ATTR_HVAC_MODE]
and new_state[ATTR_HVAC_MODE] == HVACAction.OFF
):
_LOGGER.debug("Controller was turned off, disable zones")
await self.async_turn_off_zones()

Expand All @@ -193,24 +263,29 @@ async def async_zone_state_changed(self, entity, old_state, new_state):
new_state = parse_state(new_state)

if (
old_state[ATTR_TEMPERATURE] != new_state[ATTR_TEMPERATURE] and
isinstance(new_state[ATTR_TEMPERATURE], float) and
isinstance(new_state[ATTR_CURRENT_TEMPERATURE], float)
old_state[ATTR_TEMPERATURE] != new_state[ATTR_TEMPERATURE]
and isinstance(new_state[ATTR_TEMPERATURE], float)
and isinstance(new_state[ATTR_CURRENT_TEMPERATURE], float)
):
# setpoint of a zone was updated, check whether controller needs to be updated
_LOGGER.debug("Zone {} updated: setpoint={}".format(entity, new_state[ATTR_TEMPERATURE]))
_LOGGER.debug(
"Zone {} updated: setpoint={}".format(
entity, new_state[ATTR_TEMPERATURE]
)
)
await self.async_calculate_override()

if old_state[ATTR_HVAC_ACTION] != new_state[ATTR_HVAC_ACTION]:
# action of a zone was updated, check whether controller needs to be updated
_LOGGER.debug("Zone {} updated: action={}".format(entity, new_state[ATTR_HVAC_ACTION]))
_LOGGER.debug(
"Zone {} updated: action={}".format(entity, new_state[ATTR_HVAC_ACTION])
)
await self.async_calculate_override()

async def async_calculate_override(self):
"""calculate whether override should be active and determine setpoint"""
states = [
parse_state(self.hass.states.get(entity))
for entity in self._zone_entities
parse_state(self.hass.states.get(entity)) for entity in self._zone_entities
]

temperature_increase_per_state = [
Expand All @@ -227,18 +302,24 @@ async def async_calculate_override(self):
override_active = temperature_increase > 0

if (not self._override_active and not override_active) or (
self._temperature_increase == temperature_increase and
override_active == self._override_active
self._temperature_increase == temperature_increase
and override_active == self._override_active
):
_LOGGER.debug("Override not changed, nothing to do. self.override_active={}, override_active={}, self.temperature_increase={}, temperature_increase={}".format(
self._override_active, override_active, self._temperature_increase, temperature_increase
))
_LOGGER.debug(
"Override not changed, nothing to do. self.override_active={}, override_active={}, self.temperature_increase={}, temperature_increase={}".format(
self._override_active,
override_active,
self._temperature_increase,
temperature_increase,
)
)
# nothing to do
return

_LOGGER.debug(
"Updated override temperature_increase={}, override_active={}"
.format(temperature_increase, override_active)
"Updated override temperature_increase={}, override_active={}".format(
temperature_increase, override_active
)
)

if override_active and not self._override_active:
Expand All @@ -264,9 +345,13 @@ async def async_start_override_mode(self, temperature_increase: float):
# uupdate to heat mode if needed
await self._ignore_controller_state_changes()
if compute_domain(self._controller_entity) == Platform.CLIMATE:
await async_set_hvac_mode(self.hass, self._controller_entity, HVACMode.HEAT)
await async_set_hvac_mode(
self.hass, self._controller_entity, HVACMode.HEAT
)
elif compute_domain(self._controller_entity) == Platform.SWITCH:
await async_set_switch_state(self.hass, self._controller_entity, STATE_ON)
await async_set_switch_state(
self.hass, self._controller_entity, STATE_ON
)

await self.async_update_override_setpoint(temperature_increase)

Expand All @@ -280,22 +365,36 @@ async def async_stop_override_mode(self):

current_state = parse_state(self.hass.states.get(self.entity_id))

_LOGGER.debug("Stopping override mode and restoring previous settings. self._stored_controller_state={}, self._stored_controller_setpoint={}, current_state[ATTR_HVAC_MODE]={}, current_state[ATTR_TEMPERATURE]={}".format(
self._stored_controller_state, self._stored_controller_setpoint, current_state[ATTR_HVAC_MODE], current_state[ATTR_TEMPERATURE]
))
_LOGGER.debug(
"Stopping override mode and restoring previous settings. self._stored_controller_state={}, self._stored_controller_setpoint={}, current_state[ATTR_HVAC_MODE]={}, current_state[ATTR_TEMPERATURE]={}".format(
self._stored_controller_state,
self._stored_controller_setpoint,
current_state[ATTR_HVAC_MODE],
current_state[ATTR_TEMPERATURE],
)
)

if current_state[ATTR_HVAC_MODE] != self._stored_controller_state and self._stored_controller_state is not None:
if (
current_state[ATTR_HVAC_MODE] != self._stored_controller_state
and self._stored_controller_state is not None
):
if compute_domain(self._controller_entity) == Platform.CLIMATE:
await async_set_hvac_mode(self.hass, self._controller_entity, self._stored_controller_state)
await async_set_hvac_mode(
self.hass, self._controller_entity, self._stored_controller_state
)
elif compute_domain(self._controller_entity) == Platform.SWITCH:
await async_set_switch_state(self.hass, self._controller_entity, self._stored_controller_state)
await async_set_switch_state(
self.hass, self._controller_entity, self._stored_controller_state
)

if (
current_state[ATTR_TEMPERATURE] != self._stored_controller_setpoint and
isinstance(self._stored_controller_setpoint, float) and
compute_domain(self._controller_entity) == Platform.CLIMATE
current_state[ATTR_TEMPERATURE] != self._stored_controller_setpoint
and isinstance(self._stored_controller_setpoint, float)
and compute_domain(self._controller_entity) == Platform.CLIMATE
):
await async_set_temperature(self.hass, self._controller_entity, self._stored_controller_setpoint)
await async_set_temperature(
self.hass, self._controller_entity, self._stored_controller_setpoint
)

_LOGGER.debug("Forgetting stored controller state")
self._stored_controller_setpoint = None
Expand All @@ -308,43 +407,51 @@ async def async_update_override_setpoint(self, temperature_increase: float):
self._temperature_increase = temperature_increase

controller_setpoint = 0
if (
self._stored_controller_state == HVACMode.HEAT and
isinstance(self._stored_controller_setpoint, float)
):
if self._stored_controller_state == HVACMode.HEAT and isinstance(
self._stored_controller_setpoint, float
):
controller_setpoint = self._stored_controller_setpoint

controller_state = self.hass.states.get(self._controller_entity)
current_state = parse_state(controller_state)
override_setpoint = 0

if isinstance(current_state[ATTR_CURRENT_TEMPERATURE], float):
override_setpoint = min([
current_state[ATTR_CURRENT_TEMPERATURE] + temperature_increase,
self._max_setpoint
])
override_setpoint = min(
[
current_state[ATTR_CURRENT_TEMPERATURE] + temperature_increase,
self._max_setpoint,
]
)
# else:
# TBD: mirror setpoint of zone to controller
# TBD: mirror setpoint of zone to controller

new_setpoint = max([override_setpoint, controller_setpoint])

if (
new_setpoint != current_state[ATTR_TEMPERATURE] and
compute_domain(self._controller_entity) == Platform.CLIMATE
new_setpoint != current_state[ATTR_TEMPERATURE]
and compute_domain(self._controller_entity) == Platform.CLIMATE
):
setpoint_resolution = controller_state.attributes.get(ATTR_TARGET_TEMP_STEP, 0.5)
new_setpoint = round(new_setpoint / setpoint_resolution) * setpoint_resolution
setpoint_resolution = controller_state.attributes.get(
ATTR_TARGET_TEMP_STEP, 0.5
)
new_setpoint = (
round(new_setpoint / setpoint_resolution) * setpoint_resolution
)
_LOGGER.debug("Updating override setpoint={}".format(new_setpoint))
await self._ignore_controller_state_changes()
await async_set_temperature(self.hass, self._controller_entity, new_setpoint)
await async_set_temperature(
self.hass, self._controller_entity, new_setpoint
)

@callback
async def async_turn_off_zones(self):
"""turn off all zones"""
entity_list = [
entity
for entity in self._zone_entities
if parse_state(self.hass.states.get(entity))[ATTR_HVAC_MODE] == HVACMode.HEAT
if parse_state(self.hass.states.get(entity))[ATTR_HVAC_MODE]
== HVACMode.HEAT
]
if not len(entity_list):
return
Expand Down

0 comments on commit ff1d170

Please sign in to comment.