From f7da58d84148bc59492dde1c6e284f2b6e7d2384 Mon Sep 17 00:00:00 2001 From: Jean-Marc Collin Date: Sun, 4 Feb 2024 20:06:44 +0000 Subject: [PATCH] Add temp entities initialization --- .../versatile_thermostat/commons.py | 9 + .../versatile_thermostat/const.py | 12 +- .../versatile_thermostat/number.py | 156 +++++++++++++++++- 3 files changed, 167 insertions(+), 10 deletions(-) diff --git a/custom_components/versatile_thermostat/commons.py b/custom_components/versatile_thermostat/commons.py index 13b1477..17ac829 100644 --- a/custom_components/versatile_thermostat/commons.py +++ b/custom_components/versatile_thermostat/commons.py @@ -1,4 +1,5 @@ """ Some usefull commons class """ + # pylint: disable=line-too-long import logging @@ -182,6 +183,9 @@ class VersatileThermostatBaseEntity(Entity): """Returns my climate if found""" if not self._my_climate: self._my_climate = self.find_my_versatile_thermostat() + if self._my_climate: + # Only the first time + self.my_climate_is_initialized() return self._my_climate @property @@ -238,6 +242,11 @@ class VersatileThermostatBaseEntity(Entity): await try_find_climate(None) + @callback + def my_climate_is_initialized(self): + """Called when the associated climate is initialized""" + return + @callback async def async_my_climate_changed( self, event: Event diff --git a/custom_components/versatile_thermostat/const.py b/custom_components/versatile_thermostat/const.py index b022960..1db60af 100644 --- a/custom_components/versatile_thermostat/const.py +++ b/custom_components/versatile_thermostat/const.py @@ -39,12 +39,14 @@ HIDDEN_PRESETS = [PRESET_POWER, PRESET_SECURITY] DOMAIN = "versatile_thermostat" +# The order is important. +# NUMBER should be after CLIMATE, PLATFORMS: list[Platform] = [ - Platform.NUMBER, Platform.SELECT, Platform.CLIMATE, Platform.SENSOR, Platform.BINARY_SENSOR, + Platform.NUMBER, ] CONF_HEATER = "heater_entity_id" @@ -361,7 +363,9 @@ CENTRAL_MODES = [ class RegulationParamSlow: """Light parameters for slow latency regulation""" - kp: float = 0.2 # 20% of the current internal regulation offset are caused by the current difference of target temperature and room temperature + kp: float = ( + 0.2 # 20% of the current internal regulation offset are caused by the current difference of target temperature and room temperature + ) ki: float = ( 0.8 / 288.0 ) # 80% of the current internal regulation offset are caused by the average offset of the past 24 hours @@ -369,7 +373,9 @@ class RegulationParamSlow: 1.0 / 25.0 ) # this will add 1°C to the offset when it's 25°C colder outdoor than indoor offset_max: float = 2.0 # limit to a final offset of -2°C to +2°C - stabilization_threshold: float = 0.0 # this needs to be disabled as otherwise the long term accumulated error will always be reset when the temp briefly crosses from/to below/above the target + stabilization_threshold: float = ( + 0.0 # this needs to be disabled as otherwise the long term accumulated error will always be reset when the temp briefly crosses from/to below/above the target + ) accumulated_error_threshold: float = ( 2.0 * 288 ) # this allows up to 2°C long term offset in both directions diff --git a/custom_components/versatile_thermostat/number.py b/custom_components/versatile_thermostat/number.py index 416f955..0ccd1f7 100644 --- a/custom_components/versatile_thermostat/number.py +++ b/custom_components/versatile_thermostat/number.py @@ -2,11 +2,27 @@ """ Implements the VersatileThermostat select component """ import logging +from typing import Any # from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.core import HomeAssistant, CoreState # , callback -from homeassistant.components.number import NumberEntity, NumberMode +from homeassistant.components.number import ( + NumberEntity, + NumberMode, + NumberDeviceClass, + ATTR_MAX, + ATTR_MIN, + ATTR_STEP, + ATTR_MODE, +) +from homeassistant.components.climate import ( + PRESET_BOOST, + PRESET_COMFORT, + PRESET_ECO, +) +from homeassistant.components.sensor import UnitOfTemperature + from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.restore_state import RestoreEntity @@ -14,6 +30,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from custom_components.versatile_thermostat.vtherm_api import VersatileThermostatAPI +from .commons import VersatileThermostatBaseEntity + from .const import ( DOMAIN, DEVICE_MANUFACTURER, @@ -21,9 +39,32 @@ from .const import ( CONF_THERMOSTAT_TYPE, CONF_THERMOSTAT_CENTRAL_CONFIG, CONF_ADD_CENTRAL_BOILER_CONTROL, + CONF_TEMP_MIN, + CONF_TEMP_MAX, + CONF_STEP_TEMPERATURE, + CONF_AC_MODE, + PRESET_FROST_PROTECTION, + PRESET_ECO_AC, + PRESET_COMFORT_AC, + PRESET_BOOST_AC, + PRESET_AC_SUFFIX, + CONF_PRESETS_VALUES, + CONF_PRESETS_WITH_AC_VALUES, + # CONF_PRESETS_AWAY_VALUES, + # CONF_PRESETS_AWAY_WITH_AC_VALUES, overrides, ) +PRESET_ICON_MAPPING = { + PRESET_FROST_PROTECTION + "_temp": "mdi:snowflake-thermometer", + PRESET_ECO + "_temp": "mdi:leaf", + PRESET_COMFORT + "_temp": "mdi:sofa", + PRESET_BOOST + "_temp": "mdi:rocket-launch", + PRESET_ECO_AC + "_temp": "mdi:leaf-circle-outline", + PRESET_COMFORT_AC + "_temp": "mdi:sofa-outline", + PRESET_BOOST_AC + "_temp": "mdi:rocket-launch-outline", +} + _LOGGER = logging.getLogger(__name__) @@ -42,17 +83,30 @@ async def async_setup_entry( vt_type = entry.data.get(CONF_THERMOSTAT_TYPE) is_central_boiler = entry.data.get(CONF_ADD_CENTRAL_BOILER_CONTROL) - if vt_type != CONF_THERMOSTAT_CENTRAL_CONFIG or not is_central_boiler: - return + entities = [] - entities = [ - ActivateBoilerThresholdNumber(hass, unique_id, name, entry.data), - ] + if vt_type != CONF_THERMOSTAT_CENTRAL_CONFIG or not is_central_boiler: + for preset in CONF_PRESETS_VALUES: + entities.append( + TemperatureNumber(hass, unique_id, preset, preset, entry.data) + ) + + if entry.data.get(CONF_AC_MODE, False): + for preset in CONF_PRESETS_WITH_AC_VALUES: + entities.append( + TemperatureNumber(hass, unique_id, preset, preset, entry.data) + ) + else: + entities.append( + ActivateBoilerThresholdNumber(hass, unique_id, name, entry.data) + ) async_add_entities(entities, True) -class ActivateBoilerThresholdNumber(NumberEntity, RestoreEntity): +class ActivateBoilerThresholdNumber( + NumberEntity, RestoreEntity +): # pylint: disable=abstract-method """Representation of the threshold of the number of VTherm which should be active to activate the boiler""" @@ -115,3 +169,91 @@ class ActivateBoilerThresholdNumber(NumberEntity, RestoreEntity): def __str__(self): return f"VersatileThermostat-{self.name}" + + +class TemperatureNumber( # pylint: disable=abstract-method + VersatileThermostatBaseEntity, NumberEntity, RestoreEntity +): + """Representation of one temperature number""" + + def __init__( + self, hass: HomeAssistant, unique_id, name, preset_name, entry_infos + ) -> None: + """Initialize the temperature with entry_infos if available. Else + the restoration will do the trick.""" + super().__init__(hass, unique_id, entry_infos.get(CONF_NAME)) + + split = name.split("_") + self._attr_name = split[0] + if "_" + split[1] == PRESET_AC_SUFFIX: + self._attr_name = self._attr_name + " AC" + + self._attr_name = self._attr_name + " temperature" + + self._attr_unique_id = f"{self._device_name}_{name}" + self._attr_device_class = NumberDeviceClass.TEMPERATURE + self._attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS + + # Initialize the values if included into the entry_infos. This will do + # the temperature migration. + # TODO see if this should be replace by the central config if any + temp = None + if temp := entry_infos.get(preset_name, None): + self._attr_value = self._attr_native_value = temp + + self._attr_mode = NumberMode.BOX + self._preset_name = preset_name + + self._attr_native_step = entry_infos.get(CONF_STEP_TEMPERATURE, 0.5) + self._attr_native_min_value = entry_infos.get(CONF_TEMP_MIN) + self._attr_native_max_value = entry_infos.get(CONF_TEMP_MAX) + + @property + def icon(self) -> str | None: + return PRESET_ICON_MAPPING[self._preset_name] + + @overrides + async def async_added_to_hass(self) -> None: + await super().async_added_to_hass() + + old_state: CoreState = await self.async_get_last_state() + _LOGGER.debug( + "%s - Calling async_added_to_hass old_state is %s", self, old_state + ) + if old_state is not None: + self._attr_value = self._attr_native_value = float(old_state.state) + + @overrides + def my_climate_is_initialized(self): + """Called when the associated climate is initialized""" + self._attr_native_step = self.my_climate.target_temperature_step + self._attr_native_min_value = self.my_climate.min_temp + self._attr_native_max_value = self.my_climate.max_temp + return + + # @overrides + # @property + # def native_step(self) -> float | None: + # """The native step""" + # return self.my_climate.target_temperature_step + + @overrides + def set_native_value(self, value: float) -> None: + """Change the value""" + float_value = float(value) + old_value = float(self._attr_native_value) + + if float_value == old_value: + return + + self._attr_value = self._attr_native_value = float_value + + def __str__(self): + return f"VersatileThermostat-{self.name}" + + @property + def native_unit_of_measurement(self) -> str | None: + """The unit of measurement""" + if not self.my_climate: + return UnitOfTemperature.CELSIUS + return self.my_climate.temperature_unit