Files
versatile_thermostat/custom_components/versatile_thermostat/thermostat_switch.py
Jean-Marc Collin 3529607948 Issue #645 add sonoff trvzb (#651)
* With Sonoff configuration ok

* Fix configuration

* Next (not finished)

* With 1rst implementation of VTherm TRVZB and underlying

* Work in simuated environment

* Fix Testus

* Release

* Fix release name

* Add #602 - implement a max_on_percent setting

* Calculate offset_calibration as room_temp - local_temp
Fix hvac_action calculation

* Fix hvac_action
Fix offset_calibration=room_temp - (local_temp - current_offset)

* Fix underlying target is not updated

* Issue #655 - combine motion and presence

* Fix Valve testus. Improve sending the open percent to valve

* Update custom_components/versatile_thermostat/translations/en.json

Co-authored-by: Alexander Dransfield <2844540+alexdrans@users.noreply.github.com>

* Indus step1

* Step 2 - renaming. All tests ok

* Step 2: manual tests ok

* First unit test ok

* Test multi ok

* All tests ok. Add a multi test for climate with valve regulation

* With testu for config_flow ok

* Documentation (not finished)

* Fix #661 - central boiler doesn't starts with Sonoff TRVZB

* Remove // testing

* Fix exception when there is no offset at all

* Fix class attributes and instance attributes mixing

* Documentation 2

* Documentation 3

* Documentation ++

* documentation

* Try to fix the central boiler calculation

* Fix #669

* Documentation ++

* Documentation ok for FR

* Readme FR|EN

---------

Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
Co-authored-by: Alexander Dransfield <2844540+alexdrans@users.noreply.github.com>
2024-12-07 19:05:55 +01:00

224 lines
7.4 KiB
Python

# pylint: disable=line-too-long, abstract-method
""" A climate over switch classe """
import logging
from homeassistant.core import Event, callback
from homeassistant.helpers.event import (
async_track_state_change_event,
EventStateChangedData,
)
from homeassistant.core import HomeAssistant
from homeassistant.components.climate import HVACMode
from .const import (
CONF_UNDERLYING_LIST,
CONF_HEATER_KEEP_ALIVE,
CONF_INVERSE_SWITCH,
overrides,
)
from .base_thermostat import BaseThermostat, ConfigData
from .underlyings import UnderlyingSwitch
from .prop_algorithm import PropAlgorithm
_LOGGER = logging.getLogger(__name__)
class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
"""Representation of a base class for a Versatile Thermostat over a switch."""
_entity_component_unrecorded_attributes = (
BaseThermostat._entity_component_unrecorded_attributes.union(
frozenset(
{
"is_over_switch",
"is_inversed",
"underlying_entities",
"on_time_sec",
"off_time_sec",
"cycle_min",
"function",
"tpi_coef_int",
"tpi_coef_ext",
"power_percent",
"calculated_on_percent",
}
)
)
)
def __init__(self, hass: HomeAssistant, unique_id, name, config_entry) -> None:
"""Initialize the thermostat over switch."""
self._is_inversed: bool | None = None
super().__init__(hass, unique_id, name, config_entry)
@property
def is_over_switch(self) -> bool:
"""True if the Thermostat is over_switch"""
return True
@property
def is_inversed(self) -> bool:
"""True if the switch is inversed (for pilot wire and diode)"""
return self._is_inversed is True
@property
def power_percent(self) -> float | None:
"""Get the current on_percent value"""
if self._prop_algorithm:
return round(self._prop_algorithm.on_percent * 100, 0)
else:
return None
@overrides
def post_init(self, config_entry: ConfigData):
"""Initialize the Thermostat"""
super().post_init(config_entry)
self._prop_algorithm = PropAlgorithm(
self._proportional_function,
self._tpi_coef_int,
self._tpi_coef_ext,
self._cycle_min,
self._minimal_activation_delay,
self.name,
max_on_percent=self._max_on_percent,
)
lst_switches = config_entry.get(CONF_UNDERLYING_LIST)
delta_cycle = self._cycle_min * 60 / len(lst_switches)
for idx, switch in enumerate(lst_switches):
self._underlyings.append(
UnderlyingSwitch(
hass=self._hass,
thermostat=self,
switch_entity_id=switch,
initial_delay_sec=idx * delta_cycle,
keep_alive_sec=config_entry.get(CONF_HEATER_KEEP_ALIVE, 0),
)
)
self._is_inversed = config_entry.get(CONF_INVERSE_SWITCH) is True
self._should_relaunch_control_heating = False
@overrides
async def async_added_to_hass(self):
"""Run when entity about to be added."""
_LOGGER.debug("Calling async_added_to_hass")
await super().async_added_to_hass()
# Add listener to all underlying entities
for switch in self._underlyings:
self.async_on_remove(
async_track_state_change_event(
self.hass, [switch.entity_id], self._async_switch_changed
)
)
switch.startup()
self.hass.create_task(self.async_control_heating())
@overrides
def update_custom_attributes(self):
"""Custom attributes"""
super().update_custom_attributes()
under0: UnderlyingSwitch = self._underlyings[0]
self._attr_extra_state_attributes["is_over_switch"] = self.is_over_switch
self._attr_extra_state_attributes["is_inversed"] = self.is_inversed
self._attr_extra_state_attributes["keep_alive_sec"] = under0.keep_alive_sec
self._attr_extra_state_attributes["underlying_entities"] = [
underlying.entity_id for underlying in self._underlyings
]
self._attr_extra_state_attributes[
"on_percent"
] = self._prop_algorithm.on_percent
self._attr_extra_state_attributes["power_percent"] = self.power_percent
self._attr_extra_state_attributes[
"on_time_sec"
] = self._prop_algorithm.on_time_sec
self._attr_extra_state_attributes[
"off_time_sec"
] = self._prop_algorithm.off_time_sec
self._attr_extra_state_attributes["cycle_min"] = self._cycle_min
self._attr_extra_state_attributes["function"] = self._proportional_function
self._attr_extra_state_attributes["tpi_coef_int"] = self._tpi_coef_int
self._attr_extra_state_attributes["tpi_coef_ext"] = self._tpi_coef_ext
self._attr_extra_state_attributes[
"calculated_on_percent"
] = self._prop_algorithm.calculated_on_percent
self.async_write_ha_state()
_LOGGER.debug(
"%s - Calling update_custom_attributes: %s",
self,
self._attr_extra_state_attributes,
)
@overrides
def recalculate(self):
"""A utility function to force the calculation of a the algo and
update the custom attributes and write the state
"""
_LOGGER.debug("%s - recalculate all", self)
self._prop_algorithm.calculate(
self._target_temp,
self._cur_temp,
self._cur_ext_temp,
self._hvac_mode or HVACMode.OFF,
)
self.update_custom_attributes()
# already done bu update_custom_attributes
# self.async_write_ha_state()
@overrides
def incremente_energy(self):
"""increment the energy counter if device is active"""
if self.hvac_mode == HVACMode.OFF:
return
added_energy = 0
if not self.is_over_climate and self.mean_cycle_power is not None:
added_energy = self.mean_cycle_power * float(self._cycle_min) / 60.0
if self._total_energy is None:
self._total_energy = added_energy
_LOGGER.debug(
"%s - incremente_energy set energy is %s",
self,
self._total_energy,
)
else:
self._total_energy += added_energy
_LOGGER.debug(
"%s - incremente_energy increment energy is %s",
self,
self._total_energy,
)
self.update_custom_attributes()
_LOGGER.debug(
"%s - added energy is %.3f . Total energy is now: %.3f",
self,
added_energy,
self._total_energy,
)
@callback
def _async_switch_changed(self, event: Event[EventStateChangedData]):
"""Handle heater switch state changes."""
new_state = event.data.get("new_state")
old_state = event.data.get("old_state")
if new_state is None:
return
if old_state is None:
self.hass.create_task(self._check_initial_state())
self.async_write_ha_state()
self.update_custom_attributes()