Compare commits

...

7 Commits

Author SHA1 Message Date
Jean-Marc Collin
76382ebb35 issue #325 - restore self-regulation errors after restart (#366)
* issue #325 - creates regulation_algo in post_init only

* Remove github pages deployment

---------

Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-01-26 18:38:36 +01:00
Paulo Ferreira de Castro
90f9a0e1e3 Change log level of "Window auto event is ignored" message from info to debug (#350) 2024-01-26 14:13:56 +01:00
Paulo Ferreira de Castro
ed977b53cd Add more type hints in the thermostat classes and selected files (#364) 2024-01-26 10:51:25 +01:00
Jean-Marc Collin
5d453393f8 Change incompatilibity 2024-01-24 07:14:06 +00:00
Frederic Seiler
d2f2ab7804 Typo fix (#362) 2024-01-24 07:37:02 +01:00
Jean-Marc Collin
b0b6d0478d Incompatibility with Sonoff TRVZB 2024-01-24 06:32:26 +00:00
Jean-Marc Collin
f8a2c9baa9 FIX default value for regulation valve 2024-01-21 18:43:05 +00:00
10 changed files with 143 additions and 97 deletions

View File

@@ -42,9 +42,8 @@ jobs:
- name: Generate HTML Coverage Report - name: Generate HTML Coverage Report
run: coverage html run: coverage html
# - name: Deploy to GitHub Pages
- name: Deploy to GitHub Pages # uses: peaceiris/actions-gh-pages@v3
uses: peaceiris/actions-gh-pages@v3 # with:
with: # github_token: ${{ secrets.GITHUB_TOKEN }}
github_token: ${{ secrets.GITHUB_TOKEN }} # publish_dir: ./htmlcov
publish_dir: ./htmlcov

View File

@@ -151,6 +151,7 @@ Certains thermostat de type TRV sont réputés incompatibles avec le Versatile T
2. Les thermostats « Homematic » (et éventuellement Homematic IP) sont connus pour rencontrer des problèmes avec le Versatile Thermostat en raison des limitations du protocole RF sous-jacent. Ce problème se produit particulièrement lorsque vous essayez de contrôler plusieurs thermostats Homematic à la fois dans une seule instance de VTherm. Afin de réduire la charge du cycle de service, vous pouvez par ex. regroupez les thermostats avec des procédures spécifiques à Homematic (par exemple en utilisant un thermostat mural) et laissez Versatile Thermostat contrôler uniquement le thermostat mural directement. Une autre option consiste à contrôler un seul thermostat et à propager les changements de mode CVC et de température par un automatisme, 2. Les thermostats « Homematic » (et éventuellement Homematic IP) sont connus pour rencontrer des problèmes avec le Versatile Thermostat en raison des limitations du protocole RF sous-jacent. Ce problème se produit particulièrement lorsque vous essayez de contrôler plusieurs thermostats Homematic à la fois dans une seule instance de VTherm. Afin de réduire la charge du cycle de service, vous pouvez par ex. regroupez les thermostats avec des procédures spécifiques à Homematic (par exemple en utilisant un thermostat mural) et laissez Versatile Thermostat contrôler uniquement le thermostat mural directement. Une autre option consiste à contrôler un seul thermostat et à propager les changements de mode CVC et de température par un automatisme,
3. les thermostats de type Heatzy qui ne supportent pas les commandes de type set_temperature 3. les thermostats de type Heatzy qui ne supportent pas les commandes de type set_temperature
4. les thermostats de type Rointe ont tendance a se réveiller tout seul. Le reste fonctionne normalement. 4. les thermostats de type Rointe ont tendance a se réveiller tout seul. Le reste fonctionne normalement.
5. les TRV de type Aqara SRTS-A01 qui n'ont pas le retour d'état `hvac_action` permettant de savoir si elle chauffe ou pas. Donc les retours d'état sont faussés, le reste à l'air fonctionnel.
# Pourquoi une nouvelle implémentation du thermostat ? # Pourquoi une nouvelle implémentation du thermostat ?

View File

@@ -151,6 +151,7 @@ Some TRV type thermostats are known to be incompatible with the Versatile Thermo
2. "Homematic" (and possible Homematic IP) thermostats are known to have problems with Versatile Thermostats because of limitations of the underlying RF protocol. This problem especially occurs when trying to control several Homematic thermostats at once in one Versatile Thermostat instance. In order to reduce duty cycle load, you may e.g. group thermostats with Homematic-specific procedures (e.g. using a wall thermostat) and let Versatile Thermostat only control the wall thermostat directly. Another option is to control only one thermostat and propagate the changes in HVAC mode and temperature by an automation. 2. "Homematic" (and possible Homematic IP) thermostats are known to have problems with Versatile Thermostats because of limitations of the underlying RF protocol. This problem especially occurs when trying to control several Homematic thermostats at once in one Versatile Thermostat instance. In order to reduce duty cycle load, you may e.g. group thermostats with Homematic-specific procedures (e.g. using a wall thermostat) and let Versatile Thermostat only control the wall thermostat directly. Another option is to control only one thermostat and propagate the changes in HVAC mode and temperature by an automation.
3. Thermostat of type Heatzy which doesn't supports the set_temperature command. 3. Thermostat of type Heatzy which doesn't supports the set_temperature command.
4. Thermostats of type Rointe tends to awake alone even if VTherm turns it off. Others functions works fine. 4. Thermostats of type Rointe tends to awake alone even if VTherm turns it off. Others functions works fine.
5. TRV of type Aqara SRTS-A01 which doesn't have the return state `hvac_action` allowing to know if it is heating or not. So return states are not available. Others features, seems to work normally.
# Why another thermostat implementation ? # Why another thermostat implementation ?

View File

@@ -6,6 +6,8 @@ import math
import logging import logging
from datetime import timedelta, datetime from datetime import timedelta, datetime
from types import MappingProxyType
from typing import Any
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from homeassistant.core import ( from homeassistant.core import (
@@ -20,10 +22,12 @@ from homeassistant.components.climate import ClimateEntity
from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
from homeassistant.helpers.typing import EventType as HASSEventType
from homeassistant.helpers.event import ( from homeassistant.helpers.event import (
async_track_state_change_event, async_track_state_change_event,
async_call_later, async_call_later,
EventStateChangedData,
) )
from homeassistant.exceptions import ConditionError from homeassistant.exceptions import ConditionError
@@ -134,6 +138,7 @@ from .open_window_algorithm import WindowOpenDetectionAlgorithm
from .ema import ExponentialMovingAverage from .ema import ExponentialMovingAverage
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ConfigData = MappingProxyType[str, Any]
def get_tz(hass: HomeAssistant): def get_tz(hass: HomeAssistant):
@@ -197,7 +202,13 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
) )
) )
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None: def __init__(
self,
hass: HomeAssistant,
unique_id: str,
name: str,
entry_infos: ConfigData,
):
"""Initialize the thermostat.""" """Initialize the thermostat."""
super().__init__() super().__init__()
@@ -262,7 +273,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
self._last_change_time = None self._last_change_time = None
self._underlyings = [] self._underlyings: list[UnderlyingEntity] = []
self._ema_temp = None self._ema_temp = None
self._ema_algo = None self._ema_algo = None
@@ -276,7 +287,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
self.post_init(entry_infos) self.post_init(entry_infos)
def clean_central_config_doublon(self, config_entry, central_config) -> dict: def clean_central_config_doublon(
self, config_entry: ConfigData, central_config: ConfigEntry | None
) -> dict[str, Any]:
"""Removes all values from config with are concerned by central_config""" """Removes all values from config with are concerned by central_config"""
def clean_one(cfg, schema: vol.Schema): def clean_one(cfg, schema: vol.Schema):
@@ -322,7 +335,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
return entry_infos return entry_infos
def post_init(self, config_entry): def post_init(self, config_entry: ConfigData):
"""Finish the initialization of the thermostast""" """Finish the initialization of the thermostast"""
_LOGGER.info( _LOGGER.info(
@@ -345,7 +358,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
self._attr_target_temperature_step = step self._attr_target_temperature_step = step
# convert entry_infos into usable attributes # convert entry_infos into usable attributes
presets = {} presets: dict[str, Any] = {}
items = CONF_PRESETS_WITH_AC.items() if self._ac_mode else CONF_PRESETS.items() items = CONF_PRESETS_WITH_AC.items() if self._ac_mode else CONF_PRESETS.items()
for key, value in items: for key, value in items:
_LOGGER.debug("looking for key=%s, value=%s", key, value) _LOGGER.debug("looking for key=%s, value=%s", key, value)
@@ -357,7 +370,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
self._attr_max_temp if self._ac_mode else self._attr_min_temp self._attr_max_temp if self._ac_mode else self._attr_min_temp
) )
presets_away = {} presets_away: dict[str, Any] = {}
items = ( items = (
CONF_PRESETS_AWAY_WITH_AC.items() CONF_PRESETS_AWAY_WITH_AC.items()
if self._ac_mode if self._ac_mode
@@ -805,7 +818,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
def init_underlyings(self): def init_underlyings(self):
"""Initialize all underlyings. Should be overriden if necessary""" """Initialize all underlyings. Should be overriden if necessary"""
def restore_specific_previous_state(self, old_state): def restore_specific_previous_state(self, old_state: State):
"""Should be overriden in each specific thermostat """Should be overriden in each specific thermostat
if a specific previous state or attribute should be if a specific previous state or attribute should be
restored restored
@@ -886,7 +899,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
self._hvac_mode, self._hvac_mode,
) )
def __str__(self): def __str__(self) -> str:
return f"VersatileThermostat-{self.name}" return f"VersatileThermostat-{self.name}"
@property @property
@@ -916,19 +929,19 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
) )
@property @property
def unique_id(self): def unique_id(self) -> str:
return self._unique_id return self._unique_id
@property @property
def should_poll(self): def should_poll(self) -> bool:
return False return False
@property @property
def name(self): def name(self) -> str:
return self._name return self._name
@property @property
def hvac_modes(self): def hvac_modes(self) -> list[HVACMode]:
"""List of available operation modes.""" """List of available operation modes."""
return self._hvac_list return self._hvac_list
@@ -1016,17 +1029,17 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
return self._is_used_by_central_boiler return self._is_used_by_central_boiler
@property @property
def target_temperature(self): def target_temperature(self) -> float | None:
"""Return the temperature we try to reach.""" """Return the temperature we try to reach."""
return self._target_temp return self._target_temp
@property @property
def supported_features(self): def supported_features(self) -> ClimateEntityFeature:
"""Return the list of supported features.""" """Return the list of supported features."""
return self._support_flags return self._support_flags
@property @property
def is_device_active(self): def is_device_active(self) -> bool:
"""Returns true if one underlying is active""" """Returns true if one underlying is active"""
for under in self._underlyings: for under in self._underlyings:
if under.is_device_active: if under.is_device_active:
@@ -1034,7 +1047,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
return False return False
@property @property
def current_temperature(self): def current_temperature(self) -> float | None:
"""Return the sensor temperature.""" """Return the sensor temperature."""
return self._cur_temp return self._cur_temp
@@ -1203,7 +1216,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
"""Turn auxiliary heater off.""" """Turn auxiliary heater off."""
raise NotImplementedError() raise NotImplementedError()
async def async_set_hvac_mode(self, hvac_mode, need_control_heating=True): async def async_set_hvac_mode(self, hvac_mode: HVACMode, need_control_heating=True):
"""Set new target hvac mode.""" """Set new target hvac mode."""
_LOGGER.info("%s - Set hvac mode: %s", self, hvac_mode) _LOGGER.info("%s - Set hvac mode: %s", self, hvac_mode)
@@ -1239,7 +1252,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
self.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": self._hvac_mode}) self.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": self._hvac_mode})
@overrides @overrides
async def async_set_preset_mode(self, preset_mode, overwrite_saved_preset=True): async def async_set_preset_mode(
self, preset_mode: str, overwrite_saved_preset=True
):
"""Set new preset mode.""" """Set new preset mode."""
await self._async_set_preset_mode_internal( await self._async_set_preset_mode_internal(
preset_mode, force=False, overwrite_saved_preset=overwrite_saved_preset preset_mode, force=False, overwrite_saved_preset=overwrite_saved_preset
@@ -1247,7 +1262,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
await self.async_control_heating(force=True) await self.async_control_heating(force=True)
async def _async_set_preset_mode_internal( async def _async_set_preset_mode_internal(
self, preset_mode, force=False, overwrite_saved_preset=True self, preset_mode: str, force=False, overwrite_saved_preset=True
): ):
"""Set new preset mode.""" """Set new preset mode."""
_LOGGER.info("%s - Set preset_mode: %s force=%s", self, preset_mode, force) _LOGGER.info("%s - Set preset_mode: %s force=%s", self, preset_mode, force)
@@ -1296,13 +1311,13 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
self.send_event(EventType.PRESET_EVENT, {"preset": self._attr_preset_mode}) self.send_event(EventType.PRESET_EVENT, {"preset": self._attr_preset_mode})
def reset_last_change_time( def reset_last_change_time(
self, old_preset_mode=None self, old_preset_mode: str | None = None
): # pylint: disable=unused-argument ): # pylint: disable=unused-argument
"""Reset to now the last change time""" """Reset to now the last change time"""
self._last_change_time = datetime.now(tz=self._current_tz) self._last_change_time = datetime.now(tz=self._current_tz)
_LOGGER.debug("%s - last_change_time is now %s", self, self._last_change_time) _LOGGER.debug("%s - last_change_time is now %s", self, self._last_change_time)
def reset_last_temperature_time(self, old_preset_mode=None): def reset_last_temperature_time(self, old_preset_mode: str | None = None):
"""Reset to now the last temperature time if conditions are satisfied""" """Reset to now the last temperature time if conditions are satisfied"""
if ( if (
self._attr_preset_mode not in HIDDEN_PRESETS self._attr_preset_mode not in HIDDEN_PRESETS
@@ -1312,7 +1327,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
self._last_ext_temperature_measure self._last_ext_temperature_measure
) = datetime.now(tz=self._current_tz) ) = datetime.now(tz=self._current_tz)
def find_preset_temp(self, preset_mode): def find_preset_temp(self, preset_mode: str):
"""Find the right temperature of a preset considering the presence if configured""" """Find the right temperature of a preset considering the presence if configured"""
if preset_mode is None or preset_mode == "none": if preset_mode is None or preset_mode == "none":
return ( return (
@@ -1348,11 +1363,11 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
else: else:
return self._presets_away[self.get_preset_away_name(preset_mode)] return self._presets_away[self.get_preset_away_name(preset_mode)]
def get_preset_away_name(self, preset_mode): def get_preset_away_name(self, preset_mode: str) -> str:
"""Get the preset name in away mode (when presence is off)""" """Get the preset name in away mode (when presence is off)"""
return preset_mode + PRESET_AWAY_SUFFIX return preset_mode + PRESET_AWAY_SUFFIX
async def async_set_fan_mode(self, fan_mode): async def async_set_fan_mode(self, fan_mode: str):
"""Set new target fan mode.""" """Set new target fan mode."""
_LOGGER.info("%s - Set fan mode: %s", self, fan_mode) _LOGGER.info("%s - Set fan mode: %s", self, fan_mode)
return return
@@ -1362,7 +1377,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
_LOGGER.info("%s - Set fan mode: %s", self, humidity) _LOGGER.info("%s - Set fan mode: %s", self, humidity)
return return
async def async_set_swing_mode(self, swing_mode): async def async_set_swing_mode(self, swing_mode: str):
"""Set new target swing operation.""" """Set new target swing operation."""
_LOGGER.info("%s - Set fan mode: %s", self, swing_mode) _LOGGER.info("%s - Set fan mode: %s", self, swing_mode)
return return
@@ -1379,14 +1394,14 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
self.reset_last_change_time() self.reset_last_change_time()
await self.async_control_heating(force=True) await self.async_control_heating(force=True)
async def _async_internal_set_temperature(self, temperature): async def _async_internal_set_temperature(self, temperature: float):
"""Set the target temperature and the target temperature of underlying climate if any """Set the target temperature and the target temperature of underlying climate if any
For testing purpose you can pass an event_timestamp. For testing purpose you can pass an event_timestamp.
""" """
self._target_temp = temperature self._target_temp = temperature
return return
def get_state_date_or_now(self, state: State): def get_state_date_or_now(self, state: State) -> datetime:
"""Extract the last_changed state from State or return now if not available""" """Extract the last_changed state from State or return now if not available"""
return ( return (
state.last_changed.astimezone(self._current_tz) state.last_changed.astimezone(self._current_tz)
@@ -1394,7 +1409,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
else datetime.now(tz=self._current_tz) else datetime.now(tz=self._current_tz)
) )
def get_last_updated_date_or_now(self, state: State): def get_last_updated_date_or_now(self, state: State) -> datetime:
"""Extract the last_changed state from State or return now if not available""" """Extract the last_changed state from State or return now if not available"""
return ( return (
state.last_updated.astimezone(self._current_tz) state.last_updated.astimezone(self._current_tz)
@@ -1679,7 +1694,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
_LOGGER.error("Unable to update external temperature from sensor: %s", ex) _LOGGER.error("Unable to update external temperature from sensor: %s", ex)
@callback @callback
async def _async_power_changed(self, event): async def _async_power_changed(self, event: HASSEventType[EventStateChangedData]):
"""Handle power changes.""" """Handle power changes."""
_LOGGER.debug("Thermostat %s - Receive new Power event", self.name) _LOGGER.debug("Thermostat %s - Receive new Power event", self.name)
_LOGGER.debug(event) _LOGGER.debug(event)
@@ -1705,7 +1720,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
_LOGGER.error("Unable to update current_power from sensor: %s", ex) _LOGGER.error("Unable to update current_power from sensor: %s", ex)
@callback @callback
async def _async_max_power_changed(self, event): async def _async_max_power_changed(
self, event: HASSEventType[EventStateChangedData]
):
"""Handle power max changes.""" """Handle power max changes."""
_LOGGER.debug("Thermostat %s - Receive new Power Max event", self.name) _LOGGER.debug("Thermostat %s - Receive new Power Max event", self.name)
_LOGGER.debug(event) _LOGGER.debug(event)
@@ -1730,7 +1747,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
_LOGGER.error("Unable to update current_power from sensor: %s", ex) _LOGGER.error("Unable to update current_power from sensor: %s", ex)
@callback @callback
async def _async_presence_changed(self, event): async def _async_presence_changed(
self, event: HASSEventType[EventStateChangedData]
):
"""Handle presence changes.""" """Handle presence changes."""
new_state = event.data.get("new_state") new_state = event.data.get("new_state")
_LOGGER.info( _LOGGER.info(
@@ -1746,7 +1765,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
await self._async_update_presence(new_state.state) await self._async_update_presence(new_state.state)
await self.async_control_heating(force=True) await self.async_control_heating(force=True)
async def _async_update_presence(self, new_state): async def _async_update_presence(self, new_state: str):
_LOGGER.info("%s - Updating presence. New state is %s", self, new_state) _LOGGER.info("%s - Updating presence. New state is %s", self, new_state)
self._presence_state = ( self._presence_state = (
STATE_ON if new_state in (STATE_ON, STATE_HOME) else STATE_OFF STATE_ON if new_state in (STATE_ON, STATE_HOME) else STATE_OFF
@@ -1859,7 +1878,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
) )
if self.window_bypass_state or not self.is_window_auto_enabled: if self.window_bypass_state or not self.is_window_auto_enabled:
_LOGGER.info( _LOGGER.debug(
"%s - Window auto event is ignored because bypass is ON or window auto detection is disabled", "%s - Window auto event is ignored because bypass is ON or window auto detection is disabled",
self, self,
) )
@@ -2044,7 +2063,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
return self._overpowering_state return self._overpowering_state
async def check_central_mode(self, new_central_mode, old_central_mode) -> None: async def check_central_mode(
self, new_central_mode: str | None, old_central_mode: str | None
):
"""Take into account a central mode change""" """Take into account a central mode change"""
if not self.is_controlled_by_central_mode: if not self.is_controlled_by_central_mode:
self._last_central_mode = None self._last_central_mode = None
@@ -2334,7 +2355,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
else: # default is to turn_off else: # default is to turn_off
await self.async_set_hvac_mode(HVACMode.OFF) await self.async_set_hvac_mode(HVACMode.OFF)
async def async_control_heating(self, force=False, _=None): async def async_control_heating(self, force=False, _=None) -> bool:
"""The main function used to run the calculation at each cycle""" """The main function used to run the calculation at each cycle"""
_LOGGER.debug( _LOGGER.debug(
@@ -2402,7 +2423,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
def update_custom_attributes(self): def update_custom_attributes(self):
"""Update the custom extra attributes for the entity""" """Update the custom extra attributes for the entity"""
self._attr_extra_state_attributes: dict(str, str) = { self._attr_extra_state_attributes: dict[str, Any] = {
"is_on": self.is_on, "is_on": self.is_on,
"hvac_action": self.hvac_action, "hvac_action": self.hvac_action,
"hvac_mode": self.hvac_mode, "hvac_mode": self.hvac_mode,
@@ -2483,7 +2504,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
""" """
_LOGGER.info("%s - The config entry have been updated") _LOGGER.info("%s - The config entry have been updated")
async def service_set_presence(self, presence): async def service_set_presence(self, presence: str):
"""Called by a service call: """Called by a service call:
service: versatile_thermostat.set_presence service: versatile_thermostat.set_presence
data: data:
@@ -2496,7 +2517,10 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
await self.async_control_heating(force=True) await self.async_control_heating(force=True)
async def service_set_preset_temperature( async def service_set_preset_temperature(
self, preset, temperature=None, temperature_away=None self,
preset: str,
temperature: float | None = None,
temperature_away: float | None = None,
): ):
"""Called by a service call: """Called by a service call:
service: versatile_thermostat.set_preset_temperature service: versatile_thermostat.set_preset_temperature
@@ -2534,7 +2558,12 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
) )
await self.async_control_heating(force=True) await self.async_control_heating(force=True)
async def service_set_security(self, delay_min, min_on_percent, default_on_percent): async def service_set_security(
self,
delay_min: int | None,
min_on_percent: float | None,
default_on_percent: float | None,
):
"""Called by a service call: """Called by a service call:
service: versatile_thermostat.set_security service: versatile_thermostat.set_security
data: data:
@@ -2564,7 +2593,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
await self.async_control_heating() await self.async_control_heating()
self.update_custom_attributes() self.update_custom_attributes()
async def service_set_window_bypass_state(self, window_bypass): async def service_set_window_bypass_state(self, window_bypass: bool):
"""Called by a service call: """Called by a service call:
service: versatile_thermostat.set_window_bypass service: versatile_thermostat.set_window_bypass
data: data:

View File

@@ -166,7 +166,7 @@ STEP_THERMOSTAT_VALVE = vol.Schema( # pylint: disable=invalid-name
] ]
), ),
vol.Optional(CONF_AC_MODE, default=False): cv.boolean, vol.Optional(CONF_AC_MODE, default=False): cv.boolean,
vol.Optional(CONF_AUTO_REGULATION_DTEMP, default=0.5): vol.Coerce(float), vol.Optional(CONF_AUTO_REGULATION_DTEMP, default=10): vol.Coerce(float),
vol.Optional(CONF_AUTO_REGULATION_PERIOD_MIN, default=5): cv.positive_int, vol.Optional(CONF_AUTO_REGULATION_PERIOD_MIN, default=5): cv.positive_int,
} }
) )

View File

@@ -14,8 +14,10 @@ from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from custom_components.versatile_thermostat.base_thermostat import (
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat BaseThermostat,
ConfigData,
)
from .const import ( from .const import (
DOMAIN, DOMAIN,
DEVICE_MANUFACTURER, DEVICE_MANUFACTURER,
@@ -57,7 +59,9 @@ async def async_setup_entry(
class CentralModeSelect(SelectEntity, RestoreEntity): class CentralModeSelect(SelectEntity, RestoreEntity):
"""Representation of the central mode choice""" """Representation of the central mode choice"""
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None: def __init__(
self, hass: HomeAssistant, unique_id: str, name: str, entry_infos: ConfigData
):
"""Initialize the energy sensor""" """Initialize the energy sensor"""
self._config_id = unique_id self._config_id = unique_id
self._device_name = entry_infos.get(CONF_NAME) self._device_name = entry_infos.get(CONF_NAME)
@@ -67,7 +71,7 @@ class CentralModeSelect(SelectEntity, RestoreEntity):
self._attr_current_option = CENTRAL_MODE_AUTO self._attr_current_option = CENTRAL_MODE_AUTO
@property @property
def icon(self) -> str | None: def icon(self) -> str:
return "mdi:form-select" return "mdi:form-select"
@property @property
@@ -116,7 +120,7 @@ class CentralModeSelect(SelectEntity, RestoreEntity):
self._attr_current_option = option self._attr_current_option = option
await self.notify_central_mode_change(old_central_mode=old_option) await self.notify_central_mode_change(old_central_mode=old_option)
async def notify_central_mode_change(self, old_central_mode=None): async def notify_central_mode_change(self, old_central_mode: str | None = None):
"""Notify all VTherm that the central_mode have change""" """Notify all VTherm that the central_mode have change"""
# Update all VTherm states # Update all VTherm states
component: EntityComponent[ClimateEntity] = self.hass.data[CLIMATE_DOMAIN] component: EntityComponent[ClimateEntity] = self.hass.data[CLIMATE_DOMAIN]
@@ -130,5 +134,5 @@ class CentralModeSelect(SelectEntity, RestoreEntity):
self._attr_current_option, old_central_mode self._attr_current_option, old_central_mode
) )
def __str__(self): def __str__(self) -> str:
return f"VersatileThermostat-{self.name}" return f"VersatileThermostat-{self.name}"

View File

@@ -284,7 +284,7 @@ class ValveOpenPercentSensor(VersatileThermostatBaseEntity, SensorEntity):
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None: def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
"""Initialize the energy sensor""" """Initialize the energy sensor"""
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME)) super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
self._attr_name = "Vave open percent" self._attr_name = "Valve open percent"
self._attr_unique_id = f"{self._device_name}_valve_open_percent" self._attr_unique_id = f"{self._device_name}_valve_open_percent"
@callback @callback

View File

@@ -3,12 +3,13 @@
import logging import logging
from datetime import timedelta, datetime from datetime import timedelta, datetime
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, State, callback
from homeassistant.helpers.event import ( from homeassistant.helpers.event import (
async_track_state_change_event, async_track_state_change_event,
async_track_time_interval, async_track_time_interval,
EventStateChangedData,
) )
from homeassistant.helpers.typing import EventType as HASSEventType
from homeassistant.components.climate import ( from homeassistant.components.climate import (
HVACAction, HVACAction,
HVACMode, HVACMode,
@@ -16,7 +17,7 @@ from homeassistant.components.climate import (
) )
from .commons import NowClass, round_to_nearest from .commons import NowClass, round_to_nearest
from .base_thermostat import BaseThermostat from .base_thermostat import BaseThermostat, ConfigData
from .pi_algorithm import PITemperatureRegulator from .pi_algorithm import PITemperatureRegulator
from .const import ( from .const import (
@@ -59,19 +60,19 @@ _LOGGER = logging.getLogger(__name__)
class ThermostatOverClimate(BaseThermostat): class ThermostatOverClimate(BaseThermostat):
"""Representation of a base class for a Versatile Thermostat over a climate""" """Representation of a base class for a Versatile Thermostat over a climate"""
_auto_regulation_mode: str = None _auto_regulation_mode: str | None = None
_regulation_algo = None _regulation_algo = None
_regulated_target_temp: float = None _regulated_target_temp: float | None = None
_auto_regulation_dtemp: float = None _auto_regulation_dtemp: float | None = None
_auto_regulation_period_min: int = None _auto_regulation_period_min: int | None = None
_last_regulation_change: datetime = None _last_regulation_change: datetime | None = None
# The fan mode configured in configEntry # The fan mode configured in configEntry
_auto_fan_mode: str = None _auto_fan_mode: str | None = None
# The current fan mode (could be change by service call) # The current fan mode (could be change by service call)
_current_auto_fan_mode: str = None _current_auto_fan_mode: str | None = None
# The fan_mode name depending of the current_mode # The fan_mode name depending of the current_mode
_auto_activated_fan_mode: str = None _auto_activated_fan_mode: str | None = None
_auto_deactivated_fan_mode: str = None _auto_deactivated_fan_mode: str | None = None
_entity_component_unrecorded_attributes = ( _entity_component_unrecorded_attributes = (
BaseThermostat._entity_component_unrecorded_attributes.union( BaseThermostat._entity_component_unrecorded_attributes.union(
@@ -94,7 +95,9 @@ class ThermostatOverClimate(BaseThermostat):
) )
) )
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None: def __init__(
self, hass: HomeAssistant, unique_id: str, name: str, entry_infos: ConfigData
):
"""Initialize the thermostat over switch.""" """Initialize the thermostat over switch."""
# super.__init__ calls post_init at the end. So it must be called after regulation initialization # super.__init__ calls post_init at the end. So it must be called after regulation initialization
super().__init__(hass, unique_id, name, entry_infos) super().__init__(hass, unique_id, name, entry_infos)
@@ -127,7 +130,7 @@ class ThermostatOverClimate(BaseThermostat):
return HVACAction.OFF return HVACAction.OFF
@overrides @overrides
async def _async_internal_set_temperature(self, temperature): async def _async_internal_set_temperature(self, temperature: float):
"""Set the target temperature and the target temperature of underlying climate if any""" """Set the target temperature and the target temperature of underlying climate if any"""
await super()._async_internal_set_temperature(temperature) await super()._async_internal_set_temperature(temperature)
@@ -239,7 +242,7 @@ class ThermostatOverClimate(BaseThermostat):
await self.async_set_fan_mode(self._auto_deactivated_fan_mode) await self.async_set_fan_mode(self._auto_deactivated_fan_mode)
@overrides @overrides
def post_init(self, config_entry): def post_init(self, config_entry: ConfigData):
"""Initialize the Thermostat""" """Initialize the Thermostat"""
super().post_init(config_entry) super().post_init(config_entry)
@@ -281,7 +284,7 @@ class ThermostatOverClimate(BaseThermostat):
else CONF_AUTO_FAN_NONE else CONF_AUTO_FAN_NONE
) )
def choose_auto_regulation_mode(self, auto_regulation_mode): def choose_auto_regulation_mode(self, auto_regulation_mode: str):
"""Choose or change the regulation mode""" """Choose or change the regulation mode"""
self._auto_regulation_mode = auto_regulation_mode self._auto_regulation_mode = auto_regulation_mode
if self._auto_regulation_mode == CONF_AUTO_REGULATION_LIGHT: if self._auto_regulation_mode == CONF_AUTO_REGULATION_LIGHT:
@@ -357,7 +360,7 @@ class ThermostatOverClimate(BaseThermostat):
self.target_temperature, 0, 0, 0, 0, 0.1, 0 self.target_temperature, 0, 0, 0, 0, 0.1, 0
) )
def choose_auto_fan_mode(self, auto_fan_mode): def choose_auto_fan_mode(self, auto_fan_mode: str):
"""Choose the correct fan mode depending of the underlying capacities and the configuration""" """Choose the correct fan mode depending of the underlying capacities and the configuration"""
self._current_auto_fan_mode = auto_fan_mode self._current_auto_fan_mode = auto_fan_mode
@@ -369,7 +372,7 @@ class ThermostatOverClimate(BaseThermostat):
self._auto_activated_fan_mode = self._auto_deactivated_fan_mode = None self._auto_activated_fan_mode = self._auto_deactivated_fan_mode = None
return return
def find_fan_mode(fan_modes, fan_mode) -> str: def find_fan_mode(fan_modes: list[str], fan_mode: str) -> str | None:
"""Return the fan_mode if it exist of None if not""" """Return the fan_mode if it exist of None if not"""
try: try:
return fan_mode if fan_modes.index(fan_mode) >= 0 else None return fan_mode if fan_modes.index(fan_mode) >= 0 else None
@@ -427,10 +430,11 @@ class ThermostatOverClimate(BaseThermostat):
) )
# init auto_regulation_mode # init auto_regulation_mode
self.choose_auto_regulation_mode(self._auto_regulation_mode) # Issue 325 - do only once (in post_init and not here)
# self.choose_auto_regulation_mode(self._auto_regulation_mode)
@overrides @overrides
def restore_specific_previous_state(self, old_state): def restore_specific_previous_state(self, old_state: State):
"""Restore my specific attributes from previous state""" """Restore my specific attributes from previous state"""
old_error = old_state.attributes.get("regulation_accumulated_error") old_error = old_state.attributes.get("regulation_accumulated_error")
if old_error: if old_error:
@@ -542,7 +546,7 @@ class ThermostatOverClimate(BaseThermostat):
) )
@callback @callback
async def _async_climate_changed(self, event): async def _async_climate_changed(self, event: HASSEventType[EventStateChangedData]):
"""Handle unerdlying climate state changes. """Handle unerdlying climate state changes.
This method takes the underlying values and update the VTherm with them. This method takes the underlying values and update the VTherm with them.
To avoid loops (issues #121 #101 #95 #99), we discard the event if it is received To avoid loops (issues #121 #101 #95 #99), we discard the event if it is received
@@ -552,7 +556,7 @@ class ThermostatOverClimate(BaseThermostat):
which is important for feedaback and which cannot generates loops. which is important for feedaback and which cannot generates loops.
""" """
async def end_climate_changed(changes): async def end_climate_changed(changes: bool):
"""To end the event management""" """To end the event management"""
if changes: if changes:
self.async_write_ha_state() self.async_write_ha_state()
@@ -745,7 +749,7 @@ class ThermostatOverClimate(BaseThermostat):
await end_climate_changed(changes) await end_climate_changed(changes)
@overrides @overrides
async def async_control_heating(self, force=False, _=None): async def async_control_heating(self, force=False, _=None) -> bool:
"""The main function used to run the calculation at each cycle""" """The main function used to run the calculation at each cycle"""
ret = await super().async_control_heating(force, _) ret = await super().async_control_heating(force, _)
@@ -757,27 +761,27 @@ class ThermostatOverClimate(BaseThermostat):
return ret return ret
@property @property
def auto_regulation_mode(self): def auto_regulation_mode(self) -> str | None:
"""Get the regulation mode""" """Get the regulation mode"""
return self._auto_regulation_mode return self._auto_regulation_mode
@property @property
def auto_fan_mode(self): def auto_fan_mode(self) -> str | None:
"""Get the auto fan mode""" """Get the auto fan mode"""
return self._auto_fan_mode return self._auto_fan_mode
@property @property
def regulated_target_temp(self): def regulated_target_temp(self) -> float | None:
"""Get the regulated target temperature""" """Get the regulated target temperature"""
return self._regulated_target_temp return self._regulated_target_temp
@property @property
def is_regulated(self): def is_regulated(self) -> bool:
"""Check if the ThermostatOverClimate is regulated""" """Check if the ThermostatOverClimate is regulated"""
return self.auto_regulation_mode != CONF_AUTO_REGULATION_NONE return self.auto_regulation_mode != CONF_AUTO_REGULATION_NONE
@property @property
def hvac_modes(self): def hvac_modes(self) -> list[HVACMode]:
"""List of available operation modes.""" """List of available operation modes."""
if self.underlying_entity(0): if self.underlying_entity(0):
return self.underlying_entity(0).hvac_modes return self.underlying_entity(0).hvac_modes
@@ -944,7 +948,7 @@ class ThermostatOverClimate(BaseThermostat):
await under.async_turn_aux_heat_off() await under.async_turn_aux_heat_off()
@overrides @overrides
async def async_set_fan_mode(self, fan_mode): async def async_set_fan_mode(self, fan_mode: str):
"""Set new target fan mode.""" """Set new target fan mode."""
_LOGGER.info("%s - Set fan mode: %s", self, fan_mode) _LOGGER.info("%s - Set fan mode: %s", self, fan_mode)
if fan_mode is None: if fan_mode is None:
@@ -977,7 +981,7 @@ class ThermostatOverClimate(BaseThermostat):
self._swing_mode = swing_mode self._swing_mode = swing_mode
self.async_write_ha_state() self.async_write_ha_state()
async def service_set_auto_regulation_mode(self, auto_regulation_mode): async def service_set_auto_regulation_mode(self, auto_regulation_mode: str):
"""Called by a service call: """Called by a service call:
service: versatile_thermostat.set_auto_regulation_mode service: versatile_thermostat.set_auto_regulation_mode
data: data:
@@ -1006,7 +1010,7 @@ class ThermostatOverClimate(BaseThermostat):
await self._send_regulated_temperature() await self._send_regulated_temperature()
self.update_custom_attributes() self.update_custom_attributes()
async def service_set_auto_fan_mode(self, auto_fan_mode): async def service_set_auto_fan_mode(self, auto_fan_mode: str):
"""Called by a service call: """Called by a service call:
service: versatile_thermostat.set_auto_fan_mode service: versatile_thermostat.set_auto_fan_mode
data: data:

View File

@@ -3,7 +3,11 @@
""" A climate over switch classe """ """ A climate over switch classe """
import logging import logging
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.event import (
async_track_state_change_event,
EventStateChangedData,
)
from homeassistant.helpers.typing import EventType as HASSEventType
from homeassistant.components.climate import HVACMode from homeassistant.components.climate import HVACMode
from .const import ( from .const import (
@@ -15,7 +19,7 @@ from .const import (
overrides, overrides,
) )
from .base_thermostat import BaseThermostat from .base_thermostat import BaseThermostat, ConfigData
from .underlyings import UnderlyingSwitch from .underlyings import UnderlyingSwitch
from .prop_algorithm import PropAlgorithm from .prop_algorithm import PropAlgorithm
@@ -51,7 +55,7 @@ class ThermostatOverSwitch(BaseThermostat):
# def __init__(self, hass: HomeAssistant, unique_id, name, config_entry) -> None: # def __init__(self, hass: HomeAssistant, unique_id, name, config_entry) -> None:
# """Initialize the thermostat over switch.""" # """Initialize the thermostat over switch."""
# super().__init__(hass, unique_id, name, config_entry) # super().__init__(hass, unique_id, name, config_entry)
_is_inversed: bool = None _is_inversed: bool | None = None
@property @property
def is_over_switch(self) -> bool: def is_over_switch(self) -> bool:
@@ -72,7 +76,7 @@ class ThermostatOverSwitch(BaseThermostat):
return None return None
@overrides @overrides
def post_init(self, config_entry): def post_init(self, config_entry: ConfigData):
"""Initialize the Thermostat""" """Initialize the Thermostat"""
super().post_init(config_entry) super().post_init(config_entry)
@@ -200,7 +204,7 @@ class ThermostatOverSwitch(BaseThermostat):
) )
@callback @callback
def _async_switch_changed(self, event): def _async_switch_changed(self, event: HASSEventType[EventStateChangedData]):
"""Handle heater switch state changes.""" """Handle heater switch state changes."""
new_state = event.data.get("new_state") new_state = event.data.get("new_state")
old_state = event.data.get("old_state") old_state = event.data.get("old_state")

View File

@@ -6,11 +6,13 @@ from datetime import timedelta, datetime
from homeassistant.helpers.event import ( from homeassistant.helpers.event import (
async_track_state_change_event, async_track_state_change_event,
async_track_time_interval, async_track_time_interval,
EventStateChangedData,
) )
from homeassistant.helpers.typing import EventType as HASSEventType
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.components.climate import HVACMode from homeassistant.components.climate import HVACMode
from .base_thermostat import BaseThermostat from .base_thermostat import BaseThermostat, ConfigData
from .prop_algorithm import PropAlgorithm from .prop_algorithm import PropAlgorithm
from .const import ( from .const import (
@@ -55,12 +57,14 @@ class ThermostatOverValve(BaseThermostat):
) )
) )
def __init__(self, hass: HomeAssistant, unique_id, name, config_entry) -> None: def __init__(
self, hass: HomeAssistant, unique_id: str, name: str, config_entry: ConfigData
):
"""Initialize the thermostat over switch.""" """Initialize the thermostat over switch."""
self._valve_open_percent: int = 0 self._valve_open_percent: int = 0
self._last_calculation_timestamp: datetime = None self._last_calculation_timestamp: datetime | None = None
self._auto_regulation_dpercent: float = None self._auto_regulation_dpercent: float | None = None
self._auto_regulation_period_min: int = None self._auto_regulation_period_min: int | None = None
# Call to super must be done after initialization because it calls post_init at the end # Call to super must be done after initialization because it calls post_init at the end
super().__init__(hass, unique_id, name, config_entry) super().__init__(hass, unique_id, name, config_entry)
@@ -79,7 +83,7 @@ class ThermostatOverValve(BaseThermostat):
return self._valve_open_percent return self._valve_open_percent
@overrides @overrides
def post_init(self, config_entry): def post_init(self, config_entry: ConfigData):
"""Initialize the Thermostat""" """Initialize the Thermostat"""
super().post_init(config_entry) super().post_init(config_entry)
@@ -144,7 +148,7 @@ class ThermostatOverValve(BaseThermostat):
) )
@callback @callback
async def _async_valve_changed(self, event): async def _async_valve_changed(self, event: HASSEventType[EventStateChangedData]):
"""Handle unerdlying valve state changes. """Handle unerdlying valve state changes.
This method just log the change. It changes nothing to avoid loops. This method just log the change. It changes nothing to avoid loops.
""" """