With calculation of VTherm temp entities + test ok

This commit is contained in:
Jean-Marc Collin
2024-03-03 19:06:59 +00:00
parent ea6f2d5579
commit 156d19666c
7 changed files with 524 additions and 83 deletions
@@ -137,8 +137,6 @@ from .prop_algorithm import PropAlgorithm
from .open_window_algorithm import WindowOpenDetectionAlgorithm
from .ema import ExponentialMovingAverage
from .temp_number import TemperatureNumber
_LOGGER = logging.getLogger(__name__)
ConfigData = MappingProxyType[str, Any]
@@ -216,6 +214,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
super().__init__()
self._hass = hass
self._entry_infos = None
self._attr_extra_state_attributes = {}
self._unique_id = unique_id
@@ -288,6 +287,10 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
self._is_used_by_central_boiler = False
self._support_flags = None
# Preset will be initialized from Number entities
self._presets: dict[str, Any] = {} # presets
self._presets_away: dict[str, Any] = {} # presets_away
self._attr_preset_modes: list[str] | None
self.post_init(entry_infos)
@@ -356,6 +359,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
_LOGGER.info("%s - The merged configuration is %s", self, entry_infos)
self._entry_infos = entry_infos
self._ac_mode = entry_infos.get(CONF_AC_MODE) is True
self._attr_max_temp = entry_infos.get(CONF_TEMP_MAX)
self._attr_min_temp = entry_infos.get(CONF_TEMP_MIN)
@@ -2649,18 +2654,68 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
"""Send an event"""
send_vtherm_event(self._hass, event_type=event_type, entity=self, data=data)
def get_temperature_number_entities(self, config_entry: ConfigData):
"""Creates all TemperatureNumber depending of the configuration of the Climate"""
async def init_presets(self, central_config):
"""Init all presets of the VTherm"""
# If preset central config is used and central config is set , take the presets from central config
vtherm_api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api()
# TODO add the list of preset we want to use in the VTherm. Here we will suppose all preset will be available
entity = TemperatureNumber(
self._hass,
unique_id=config_entry.entry_id,
name=config_entry.data.get(CONF_NAME),
preset_name="comfort",
is_ac=False,
is_away=False,
entry_infos=config_entry.data,
presets: dict[str, Any] = {}
presets_away: dict[str, Any] = {}
def calculate_presets(items, use_central_conf_key):
presets: dict[str, Any] = {}
config_id = self._unique_id
if (
central_config
and self._entry_infos.get(use_central_conf_key, False) is True
):
config_id = central_config.entry_id
for key, preset_name in items:
_LOGGER.debug("looking for key=%s, preset_name=%s", key, preset_name)
value = vtherm_api.get_temperature_number_value(
config_id=config_id, preset_name=preset_name
)
if value is not None:
presets[key] = value
else:
_LOGGER.debug("preset_name %s not found in VTherm API", preset_name)
presets[key] = (
self._attr_max_temp if self._ac_mode else self._attr_min_temp
)
return presets
# Calculate all presets
presets = calculate_presets(
CONF_PRESETS_WITH_AC.items() if self._ac_mode else CONF_PRESETS.items(),
CONF_USE_PRESETS_CENTRAL_CONFIG,
)
return entity
if self._entry_infos.get(CONF_USE_PRESENCE_FEATURE) is True:
presets_away = calculate_presets(
(
CONF_PRESETS_AWAY_WITH_AC.items()
if self._ac_mode
else CONF_PRESETS_AWAY.items()
),
CONF_USE_PRESENCE_CENTRAL_CONFIG,
)
# aggregate all available presets now
self._presets: dict[str, Any] = presets
self._presets_away: dict[str, Any] = presets_away
# Calculate all possible presets
self._attr_preset_modes = [PRESET_NONE]
if len(self._presets):
self._support_flags = SUPPORT_FLAGS | ClimateEntityFeature.PRESET_MODE
for key, _ in CONF_PRESETS.items():
if self.find_preset_temp(key) > 0:
self._attr_preset_modes.append(key)
_LOGGER.debug(
"After adding presets, preset_modes to %s", self._attr_preset_modes
)
else:
_LOGGER.debug("No preset_modes")
@@ -7,11 +7,11 @@ from homeassistant.core import (
HomeAssistant,
callback,
Event,
CoreState,
# CoreState,
HomeAssistantError,
)
from homeassistant.const import STATE_ON, STATE_OFF, EVENT_HOMEASSISTANT_START
from homeassistant.const import STATE_ON, STATE_OFF # , EVENT_HOMEASSISTANT_START
from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
from homeassistant.helpers.event import async_track_state_change_event
@@ -386,17 +386,18 @@ class CentralBoilerBinarySensor(BinarySensorEntity):
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self._hass)
api.register_central_boiler(self)
@callback
async def _async_startup_internal(*_):
_LOGGER.debug("%s - Calling async_startup_internal", self)
await self.listen_nb_active_vtherm_entity()
if self.hass.state == CoreState.running:
await _async_startup_internal()
else:
self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_START, _async_startup_internal
)
# Should be not more needed and replaced by vtherm_api.init_vtherm_links
# @callback
# async def _async_startup_internal(*_):
# _LOGGER.debug("%s - Calling async_startup_internal", self)
# await self.listen_nb_active_vtherm_entity()
#
# if self.hass.state == CoreState.running:
# await _async_startup_internal()
# else:
# self.hass.bus.async_listen_once(
# EVENT_HOMEASSISTANT_START, _async_startup_internal
# )
async def listen_nb_active_vtherm_entity(self):
"""Initialize the listening of state change of VTherms"""
@@ -76,7 +76,7 @@ async def async_setup_entry(
elif vt_type == CONF_THERMOSTAT_VALVE:
entity = ThermostatOverValve(hass, unique_id, name, entry.data)
async_add_entities([entity, entity.get_temperature_number_entities(entry)], True)
async_add_entities([entity], True)
# Add services
platform = entity_platform.async_get_current_platform()
@@ -23,7 +23,7 @@ from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import ensure_unique_string, slugify
from .vtherm_api import VersatileThermostatAPI
from .commons import VersatileThermostatBaseEntity
@@ -48,6 +48,9 @@ from .const import (
CONF_PRESETS_WITH_AC_VALUES,
CONF_PRESETS_AWAY_VALUES,
CONF_PRESETS_AWAY_WITH_AC_VALUES,
CONF_USE_PRESETS_CENTRAL_CONFIG,
CONF_USE_PRESENCE_CENTRAL_CONFIG,
CONF_USE_PRESENCE_FEATURE,
overrides,
)
@@ -84,28 +87,45 @@ async def async_setup_entry(
unique_id = entry.entry_id
name = entry.data.get(CONF_NAME)
vt_type = entry.data.get(CONF_THERMOSTAT_TYPE)
is_central_boiler = entry.data.get(CONF_ADD_CENTRAL_BOILER_CONTROL)
# is_central_boiler = entry.data.get(CONF_ADD_CENTRAL_BOILER_CONTROL)
entities = []
if vt_type != CONF_THERMOSTAT_CENTRAL_CONFIG:
if not is_central_boiler:
pass
# for preset in CONF_PRESETS_VALUES:
# entities.append(
# TemperatureNumber(
# hass, unique_id, preset, preset, False, False, entry.data
# )
# )
# Creates non central temperature entities
if not entry.data.get(CONF_USE_PRESETS_CENTRAL_CONFIG, False):
for preset in CONF_PRESETS_VALUES:
entities.append(
TemperatureNumber(
hass, unique_id, name, preset, False, False, entry.data
)
)
if entry.data.get(CONF_AC_MODE, False):
for preset in CONF_PRESETS_WITH_AC_VALUES:
entities.append(
TemperatureNumber(
hass, unique_id, name, preset, True, False, entry.data
)
)
# TODO
# if entry.data.get(CONF_AC_MODE, False):
# for preset in CONF_PRESETS_WITH_AC_VALUES:
# entities.append(
# TemperatureNumber(
# hass, unique_id, preset, preset, True, False, entry.data
# )
# )
if entry.data.get(
CONF_USE_PRESENCE_FEATURE, False
) is True and not entry.data.get(CONF_USE_PRESENCE_CENTRAL_CONFIG, False):
for preset in CONF_PRESETS_AWAY_VALUES:
entities.append(
TemperatureNumber(
hass, unique_id, name, preset, False, True, entry.data
)
)
if entry.data.get(CONF_AC_MODE, False):
for preset in CONF_PRESETS_AWAY_WITH_AC_VALUES:
entities.append(
TemperatureNumber(
hass, unique_id, name, preset, True, True, entry.data
)
)
# For central config only
else:
entities.append(
ActivateBoilerThresholdNumber(hass, unique_id, name, entry.data)
@@ -113,27 +133,27 @@ async def async_setup_entry(
for preset in CONF_PRESETS_VALUES:
entities.append(
CentralConfigTemperatureNumber(
hass, unique_id, preset, preset, False, False, entry.data
hass, unique_id, name, preset, False, False, entry.data
)
)
for preset in CONF_PRESETS_WITH_AC_VALUES:
entities.append(
CentralConfigTemperatureNumber(
hass, unique_id, preset, preset, True, False, entry.data
hass, unique_id, name, preset, True, False, entry.data
)
)
for preset in CONF_PRESETS_AWAY_VALUES:
entities.append(
CentralConfigTemperatureNumber(
hass, unique_id, preset, preset, False, True, entry.data
hass, unique_id, name, preset, False, True, entry.data
)
)
for preset in CONF_PRESETS_AWAY_WITH_AC_VALUES:
entities.append(
CentralConfigTemperatureNumber(
hass, unique_id, preset, preset, True, True, entry.data
hass, unique_id, name, preset, True, True, entry.data
)
)
@@ -211,7 +231,6 @@ class CentralConfigTemperatureNumber(NumberEntity, RestoreEntity):
"""Representation of one temperature number"""
_attr_has_entity_name = True
# _attr_translation_key = "temperature"
def __init__(
self,
@@ -227,7 +246,7 @@ class CentralConfigTemperatureNumber(NumberEntity, RestoreEntity):
the restoration will do the trick."""
self._config_id = unique_id
self._device_name = entry_infos.get(CONF_NAME)
self._device_name = name
# self._attr_name = name
self._attr_translation_key = preset_name
@@ -236,7 +255,8 @@ class CentralConfigTemperatureNumber(NumberEntity, RestoreEntity):
# "ac": "-AC" if is_ac else "",
# "away": "-AWAY" if is_away else "",
# }
self.entity_id = f"{NUMBER_DOMAIN}.central_configuration_{preset_name}"
# self.entity_id = f"{NUMBER_DOMAIN}.central_configuration_{preset_name}"
self.entity_id = f"{NUMBER_DOMAIN}.{slugify(name)}_{preset_name}"
self._attr_unique_id = f"central_configuration_{preset_name}"
self._attr_device_class = NumberDeviceClass.TEMPERATURE
self._attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS
@@ -250,7 +270,7 @@ class CentralConfigTemperatureNumber(NumberEntity, RestoreEntity):
# previous value
# TODO remove this after the next major release and just keep the init min/max
temp = None
if temp := entry_infos.get(preset_name, None):
if (temp := entry_infos.get(preset_name, None)) is not None:
self._attr_value = self._attr_native_value = temp
else:
if entry_infos.get(CONF_AC_MODE) is True:
@@ -346,7 +366,6 @@ class TemperatureNumber( # pylint: disable=abstract-method
"""Representation of one temperature number"""
_attr_has_entity_name = True
_attr_translation_key = "temperature"
def __init__(
self,
@@ -360,40 +379,41 @@ class TemperatureNumber( # pylint: disable=abstract-method
) -> 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))
super().__init__(hass, unique_id, name)
split = name.split("_")
# self._attr_name = split[0]
# if "_" + split[1] == PRESET_AC_SUFFIX:
# self._attr_name = self._attr_name + " AC"
self._attr_translation_key = preset_name
self.entity_id = f"{NUMBER_DOMAIN}.{slugify(name)}_{preset_name}"
# self._attr_name = self._attr_name + " temperature"
self._attr_translation_placeholders = {
"preset": preset_name,
"ac": "-AC" if is_ac else "",
"away": "-AWAY" if is_away else "",
}
self._attr_unique_id = f"{self._device_name}_{name}"
# self._attr_translation_placeholders = {
# "preset": preset_name,
# "ac": "-AC" if is_ac else "",
# "away": "-AWAY" if is_away else "",
# }
self._attr_unique_id = f"{self._device_name}_{preset_name}"
self._attr_device_class = NumberDeviceClass.TEMPERATURE
self._attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS
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)
# 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):
if (temp := entry_infos.get(preset_name, None)) is not None:
self._attr_value = self._attr_native_value = temp
else:
if entry_infos.get(CONF_AC_MODE) is True:
self._attr_native_value = self._attr_native_max_value
else:
self._attr_native_value = self._attr_native_min_value
self._attr_mode = NumberMode.BOX
self._preset_name = preset_name
self._is_away = is_away
self._is_ac = is_ac
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]
@@ -402,12 +422,16 @@ class TemperatureNumber( # pylint: disable=abstract-method
async def async_added_to_hass(self) -> None:
await super().async_added_to_hass()
# register the temp entity for this device and preset
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self.hass)
api.register_temperature_number(self._config_id, self._preset_name, self)
old_state: CoreState = await self.async_get_last_state()
_LOGGER.debug(
"%s - Calling async_added_to_hass old_state is %s", self, old_state
)
try:
if old_state is not None and (value := float(old_state.state) > 0):
if old_state is not None and ((value := float(old_state.state)) > 0):
self._attr_value = self._attr_native_value = value
except ValueError:
pass
@@ -425,12 +449,6 @@ class TemperatureNumber( # pylint: disable=abstract-method
)
return
# @overrides
# @property
# def native_step(self) -> float | None:
# """The native step"""
# return self.my_climate.target_temperature_step
@overrides
async def async_set_native_value(self, value: float) -> None:
"""Change the value"""
@@ -6,7 +6,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN
from homeassistant.components.number import NumberEntity, DOMAIN as NUMBER_DOMAIN
from homeassistant.components.number import NumberEntity
from .const import (
DOMAIN,