Init temperature number for central configuration + testus ok

This commit is contained in:
Jean-Marc Collin
2024-03-03 15:03:20 +00:00
parent 4478d65ad4
commit ea6f2d5579
14 changed files with 1201 additions and 153 deletions

View File

@@ -11,11 +11,11 @@
// "postCreateCommand": "container install",
"postCreateCommand": "./container dev-setup",
"mounts": [
"source=${localEnv:HOME}/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached",
// uncomment this to get the versatile-thermostat-ui-card
"source=${localEnv:HOME}/SugarSync/Projets/home-assistant/versatile-thermostat-ui-card/dist,target=/workspaces/versatile_thermostat/config/www/community/versatile-thermostat-ui-card,type=bind,consistency=cached"
],
"mounts": [
"source=${localEnv:HOME}/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached",
// uncomment this to get the versatile-thermostat-ui-card
"source=${localEnv:HOME}/SugarSync/Projets/home-assistant/versatile-thermostat-ui-card/dist,target=/workspaces/versatile_thermostat/config/www/community/versatile-thermostat-ui-card,type=bind,consistency=cached"
],
"customizations": {
"vscode": {
@@ -65,4 +65,4 @@
}
}
}
}
}

View File

@@ -15,7 +15,5 @@
// "/home/vscode/core",
"/workspaces/versatile_thermostat/custom_components/versatile_thermostat",
"/home/vscode/.local/lib/python3.12/site-packages/homeassistant"
],
"python.experiments.optOutFrom": ["pythonTestAdapter"],
"python.formatting.provider": "none"
]
}

View File

@@ -8,10 +8,10 @@ import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.const import SERVICE_RELOAD
from homeassistant.const import SERVICE_RELOAD, EVENT_HOMEASSISTANT_STARTED
from homeassistant.config_entries import ConfigEntry, ConfigType
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, CoreState, callback
from .base_thermostat import BaseThermostat
@@ -82,15 +82,27 @@ async def async_setup(
hass.data.setdefault(DOMAIN, {})
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
# L'argument config contient votre fichier configuration.yaml
vtherm_config = config.get(DOMAIN)
if vtherm_config is not None:
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
api.set_global_config(vtherm_config)
else:
_LOGGER.info("No global config from configuration.yaml available")
# Listen HA starts to initialize all links between
@callback
async def _async_startup_internal(*_):
_LOGGER.info(
"VersatileThermostat - HA is started, initialize all links between VTherm entities"
)
await api.init_vtherm_links()
if hass.state == CoreState.running:
await _async_startup_internal()
else:
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, _async_startup_internal)
hass.helpers.service.async_register_admin_service(
DOMAIN,
SERVICE_RELOAD,

View File

@@ -82,9 +82,9 @@ from .const import (
CONF_NO_MOTION_PRESET,
CONF_DEVICE_POWER,
CONF_PRESETS,
CONF_PRESETS_AWAY,
CONF_PRESETS_WITH_AC,
CONF_PRESETS_AWAY_WITH_AC,
# CONF_PRESETS_AWAY,
# CONF_PRESETS_WITH_AC,
# CONF_PRESETS_AWAY_WITH_AC,
CONF_CYCLE_MIN,
CONF_PROP_FUNCTION,
CONF_TPI_COEF_INT,
@@ -137,6 +137,8 @@ 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]
@@ -285,6 +287,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
self._last_central_mode = None
self._is_used_by_central_boiler = False
self._support_flags = None
self._attr_preset_modes: list[str] | None
self.post_init(entry_infos)
def clean_central_config_doublon(
@@ -358,33 +363,36 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
self._attr_target_temperature_step = step
# convert entry_infos into usable attributes
presets: dict[str, Any] = {}
items = CONF_PRESETS_WITH_AC.items() if self._ac_mode else CONF_PRESETS.items()
for key, value in items:
_LOGGER.debug("looking for key=%s, value=%s", key, value)
if value in entry_infos:
presets[key] = entry_infos.get(value)
else:
_LOGGER.debug("value %s not found in Entry", value)
presets[key] = (
self._attr_max_temp if self._ac_mode else self._attr_min_temp
)
# 354 - presets are now initializesd by number entities
# presets: dict[str, Any] = {}
# items = CONF_PRESETS_WITH_AC.items() if self._ac_mode else CONF_PRESETS.items()
# for key, value in items:
# _LOGGER.debug("looking for key=%s, value=%s", key, value)
# if value in entry_infos:
# presets[key] = entry_infos.get(value)
# else:
# _LOGGER.debug("value %s not found in Entry", value)
# presets[key] = (
# self._attr_max_temp if self._ac_mode else self._attr_min_temp
# )
presets_away: dict[str, Any] = {}
items = (
CONF_PRESETS_AWAY_WITH_AC.items()
if self._ac_mode
else CONF_PRESETS_AWAY.items()
)
for key, value in items:
_LOGGER.debug("looking for key=%s, value=%s", key, value)
if value in entry_infos:
presets_away[key] = entry_infos.get(value)
else:
_LOGGER.debug("value %s not found in Entry", value)
presets_away[key] = (
self._attr_max_temp if self._ac_mode else self._attr_min_temp
)
# presets_away: dict[str, Any] = {}
# items = (
# CONF_PRESETS_AWAY_WITH_AC.items()
# if self._ac_mode
# else CONF_PRESETS_AWAY.items()
# )
# for key, value in items:
# _LOGGER.debug("looking for key=%s, value=%s", key, value)
# if value in entry_infos:
# presets_away[key] = entry_infos.get(value)
# else:
# _LOGGER.debug("value %s not found in Entry", value)
# presets_away[key] = (
# self._attr_max_temp if self._ac_mode else self._attr_min_temp
# )
self._attr_preset_modes: list[str] | None
if self._window_call_cancel is not None:
self._window_call_cancel()
@@ -462,15 +470,10 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
self._support_flags = SUPPORT_FLAGS
self._presets = presets
self._presets_away = presets_away
# Preset will be initialized from Number entities
self._presets: dict[str, Any] = {} # presets
self._presets_away: dict[str, Any] = {} # presets_away
_LOGGER.debug(
"%s - presets are set to: %s, away: %s",
self,
self._presets,
self._presets_away,
)
# Will be restored if possible
self._attr_preset_mode = PRESET_NONE
self._saved_preset_mode = PRESET_NONE
@@ -534,24 +537,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
self._overpowering_state = None
self._presence_state = None
# Calculate all possible presets
self._attr_preset_modes = [PRESET_NONE]
if len(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")
if self._motion_on:
self._attr_preset_modes.append(PRESET_ACTIVITY)
self._total_energy = 0
# Read the parameter from configuration.yaml if it exists
@@ -658,6 +643,41 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
await self.async_startup()
# TODO remove this
def init_temperature_preset(self, preset, temperature, is_ac, is_away):
"""Initialize the internal temperature preset
from the Number entity which holds the temperature"""
if temperature is None or preset is None:
return
if is_away:
self._presets_away[preset] = temperature
else:
self._presets[preset] = temperature
_LOGGER.debug(
"%s - presets are set to: %s, away: %s",
self,
self._presets,
self._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")
def remove_thermostat(self):
"""Called when the thermostat will be removed"""
_LOGGER.info("%s - Removing thermostat", self)
@@ -852,10 +872,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
old_preset_mode = old_state.attributes.get(ATTR_PRESET_MODE)
# Never restore a Power or Security preset
if (
old_preset_mode in self._attr_preset_modes
and old_preset_mode not in HIDDEN_PRESETS
):
if old_preset_mode not in HIDDEN_PRESETS:
# old_preset_mode in self._attr_preset_modes
self._attr_preset_mode = old_state.attributes.get(ATTR_PRESET_MODE)
self.save_preset_mode()
else:
@@ -1323,9 +1341,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
self._attr_preset_mode not in HIDDEN_PRESETS
and old_preset_mode not in HIDDEN_PRESETS
):
self._last_temperature_measure = (
self._last_ext_temperature_measure
) = datetime.now(tz=self._current_tz)
self._last_temperature_measure = self._last_ext_temperature_measure = (
datetime.now(tz=self._current_tz)
)
def find_preset_temp(self, preset_mode: str):
"""Find the right temperature of a preset considering the presence if configured"""
@@ -1344,9 +1362,11 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
return self._power_temp
if preset_mode == PRESET_ACTIVITY:
return self._presets[
self._motion_preset
if self._motion_state == STATE_ON
else self._no_motion_preset
(
self._motion_preset
if self._motion_state == STATE_ON
else self._no_motion_preset
)
]
else:
# Select _ac presets if in COOL Mode (or over_switch with _ac_mode)
@@ -1359,9 +1379,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
STATE_ON,
STATE_HOME,
]:
return self._presets[preset_mode]
return self._presets.get(preset_mode, 0)
else:
return self._presets_away[self.get_preset_away_name(preset_mode)]
return self._presets_away.get(self.get_preset_away_name(preset_mode), 0)
def get_preset_away_name(self, preset_mode: str) -> str:
"""Get the preset name in away mode (when presence is off)"""
@@ -1812,9 +1832,11 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
await self._async_internal_set_temperature(
self._presets[
self._motion_preset
if self._motion_state == STATE_ON
else self._no_motion_preset
(
self._motion_preset
if self._motion_state == STATE_ON
else self._no_motion_preset
)
]
)
_LOGGER.debug(
@@ -2431,21 +2453,21 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
"type": self._thermostat_type,
"is_controlled_by_central_mode": self.is_controlled_by_central_mode,
"last_central_mode": self.last_central_mode,
"frost_temp": self._presets[PRESET_FROST_PROTECTION],
"eco_temp": self._presets[PRESET_ECO],
"boost_temp": self._presets[PRESET_BOOST],
"comfort_temp": self._presets[PRESET_COMFORT],
"frost_temp": self._presets.get(PRESET_FROST_PROTECTION, 0),
"eco_temp": self._presets.get(PRESET_ECO, 0),
"boost_temp": self._presets.get(PRESET_BOOST, 0),
"comfort_temp": self._presets.get(PRESET_COMFORT, 0),
"frost_away_temp": self._presets_away.get(
self.get_preset_away_name(PRESET_FROST_PROTECTION)
self.get_preset_away_name(PRESET_FROST_PROTECTION), 0
),
"eco_away_temp": self._presets_away.get(
self.get_preset_away_name(PRESET_ECO)
self.get_preset_away_name(PRESET_ECO), 0
),
"boost_away_temp": self._presets_away.get(
self.get_preset_away_name(PRESET_BOOST)
self.get_preset_away_name(PRESET_BOOST), 0
),
"comfort_away_temp": self._presets_away.get(
self.get_preset_away_name(PRESET_COMFORT)
self.get_preset_away_name(PRESET_COMFORT), 0
),
"power_temp": self._power_temp,
"target_temperature_step": self.target_temperature_step,
@@ -2626,8 +2648,19 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
def send_event(self, event_type: EventType, data: dict):
"""Send an event"""
send_vtherm_event(self._hass, event_type=event_type, entity=self, data=data)
# _LOGGER.info("%s - Sending event %s with data: %s", self, event_type, data)
# data["entity_id"] = self.entity_id
# data["name"] = self.name
# data["state_attributes"] = self.state_attributes
# self._hass.bus.fire(event_type.value, data)
def get_temperature_number_entities(self, config_entry: ConfigData):
"""Creates all TemperatureNumber depending of the configuration of the Climate"""
# 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,
)
return entity

View File

@@ -1,4 +1,5 @@
""" Implements the VersatileThermostat climate component """
import logging
@@ -75,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], True)
async_add_entities([entity, entity.get_temperature_number_entities(entry)], True)
# Add services
platform = entity_platform.async_get_current_platform()

View File

@@ -2,7 +2,6 @@
""" 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
@@ -11,10 +10,7 @@ from homeassistant.components.number import (
NumberEntity,
NumberMode,
NumberDeviceClass,
ATTR_MAX,
ATTR_MIN,
ATTR_STEP,
ATTR_MODE,
DOMAIN as NUMBER_DOMAIN,
)
from homeassistant.components.climate import (
PRESET_BOOST,
@@ -29,7 +25,7 @@ from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from custom_components.versatile_thermostat.vtherm_api import VersatileThermostatAPI
from .vtherm_api import VersatileThermostatAPI
from .commons import VersatileThermostatBaseEntity
from .const import (
@@ -50,8 +46,8 @@ from .const import (
PRESET_AC_SUFFIX,
CONF_PRESETS_VALUES,
CONF_PRESETS_WITH_AC_VALUES,
# CONF_PRESETS_AWAY_VALUES,
# CONF_PRESETS_AWAY_WITH_AC_VALUES,
CONF_PRESETS_AWAY_VALUES,
CONF_PRESETS_AWAY_WITH_AC_VALUES,
overrides,
)
@@ -63,6 +59,13 @@ PRESET_ICON_MAPPING = {
PRESET_ECO_AC + "_temp": "mdi:leaf-circle-outline",
PRESET_COMFORT_AC + "_temp": "mdi:sofa-outline",
PRESET_BOOST_AC + "_temp": "mdi:rocket-launch-outline",
PRESET_FROST_PROTECTION + "_away_temp": "mdi:snowflake-thermometer",
PRESET_ECO + "_away_temp": "mdi:leaf",
PRESET_COMFORT + "_away_temp": "mdi:sofa",
PRESET_BOOST + "_away_temp": "mdi:rocket-launch",
PRESET_ECO_AC + "_away_temp": "mdi:leaf-circle-outline",
PRESET_COMFORT_AC + "_away_temp": "mdi:sofa-outline",
PRESET_BOOST_AC + "_away_temp": "mdi:rocket-launch-outline",
}
_LOGGER = logging.getLogger(__name__)
@@ -85,21 +88,54 @@ async def async_setup_entry(
entities = []
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 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
# )
# )
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)
)
# 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
# )
# )
else:
entities.append(
ActivateBoilerThresholdNumber(hass, unique_id, name, entry.data)
)
for preset in CONF_PRESETS_VALUES:
entities.append(
CentralConfigTemperatureNumber(
hass, unique_id, preset, 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
)
)
for preset in CONF_PRESETS_AWAY_VALUES:
entities.append(
CentralConfigTemperatureNumber(
hass, unique_id, preset, 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
)
)
async_add_entities(entities, True)
@@ -171,25 +207,173 @@ class ActivateBoilerThresholdNumber(
return f"VersatileThermostat-{self.name}"
class CentralConfigTemperatureNumber(NumberEntity, RestoreEntity):
"""Representation of one temperature number"""
_attr_has_entity_name = True
# _attr_translation_key = "temperature"
def __init__(
self,
hass: HomeAssistant,
unique_id,
name,
preset_name,
is_ac,
is_away,
entry_infos,
) -> None:
"""Initialize the temperature with entry_infos if available. Else
the restoration will do the trick."""
self._config_id = unique_id
self._device_name = entry_infos.get(CONF_NAME)
# self._attr_name = name
self._attr_translation_key = preset_name
# self._attr_translation_placeholders = {
# "preset": preset_name,
# "ac": "-AC" if is_ac else "",
# "away": "-AWAY" if is_away else "",
# }
self.entity_id = f"{NUMBER_DOMAIN}.central_configuration_{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
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. Else the temperature will be restored from
# 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):
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
@property
def icon(self) -> str | None:
return PRESET_ICON_MAPPING[self._preset_name]
@property
def device_info(self) -> DeviceInfo:
"""Return the device info."""
return DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, self._config_id)},
name=self._device_name,
manufacturer=DEVICE_MANUFACTURER,
model=DOMAIN,
)
@overrides
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)
# Restore value from previous one if exists
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):
self._attr_value = self._attr_native_value = value
except ValueError:
pass
@overrides
async def async_set_native_value(self, value: float) -> None:
"""Change the value"""
# TODO implements the native value change -> reload values for all central config
# based VTherm
# if self.my_climate is None:
# _LOGGER.warning(
# "%s - cannot change temperature because VTherm is not initialized", self
# )
# return
#
# 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
#
# self.async_write_ha_state()
#
# # Update the VTherm
# self.hass.create_task(
# self.my_climate.service_set_preset_temperature(
# self._preset_name.replace("_temp", ""), self._attr_native_value, None
# )
# )
def __str__(self):
return f"VersatileThermostat-{self.name}"
@property
def native_unit_of_measurement(self) -> str | None:
"""The unit of measurement"""
# TODO Kelvin ? It seems not because all internal values are stored in
# ° Celsius but only the render in front can be in °K depending on the
# user configuration.
return UnitOfTemperature.CELSIUS
class TemperatureNumber( # pylint: disable=abstract-method
VersatileThermostatBaseEntity, NumberEntity, RestoreEntity
):
"""Representation of one temperature number"""
_attr_has_entity_name = True
_attr_translation_key = "temperature"
def __init__(
self, hass: HomeAssistant, unique_id, name, preset_name, entry_infos
self,
hass: HomeAssistant,
unique_id,
name,
preset_name,
is_ac,
is_away,
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 = split[0]
# if "_" + split[1] == PRESET_AC_SUFFIX:
# self._attr_name = self._attr_name + " AC"
self._attr_name = self._attr_name + " temperature"
# 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_device_class = NumberDeviceClass.TEMPERATURE
self._attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS
@@ -203,6 +387,8 @@ class TemperatureNumber( # pylint: disable=abstract-method
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)
@@ -220,8 +406,11 @@ class TemperatureNumber( # pylint: disable=abstract-method
_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)
try:
if old_state is not None and (value := float(old_state.state) > 0):
self._attr_value = self._attr_native_value = value
except ValueError:
pass
@overrides
def my_climate_is_initialized(self):
@@ -229,6 +418,11 @@ class TemperatureNumber( # pylint: disable=abstract-method
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
# Initialize the internal temp value of VTherm
self.my_climate.init_temperature_preset(
self._preset_name, self._attr_native_value, self._is_ac, self._is_away
)
return
# @overrides
@@ -238,8 +432,15 @@ class TemperatureNumber( # pylint: disable=abstract-method
# return self.my_climate.target_temperature_step
@overrides
def set_native_value(self, value: float) -> None:
async def async_set_native_value(self, value: float) -> None:
"""Change the value"""
if self.my_climate is None:
_LOGGER.warning(
"%s - cannot change temperature because VTherm is not initialized", self
)
return
float_value = float(value)
old_value = float(self._attr_native_value)
@@ -248,6 +449,15 @@ class TemperatureNumber( # pylint: disable=abstract-method
self._attr_value = self._attr_native_value = float_value
self.async_write_ha_state()
# Update the VTherm
self.hass.create_task(
self.my_climate.service_set_preset_temperature(
self._preset_name.replace("_temp", ""), self._attr_native_value, None
)
)
def __str__(self):
return f"VersatileThermostat-{self.name}"

View File

@@ -537,6 +537,56 @@
}
}
}
},
"number": {
"temperature": {
"name": "{preset}{ac}-{away} [%key:component::sensor::entity_component::temperature::name%]"
},
"frost_temp": {
"name": "Frost"
},
"eco_temp": {
"name": "Eco"
},
"comfort_temp": {
"name": "Comfort"
},
"boost_temp": {
"name": "Boost"
},
"frost_ac_temp": {
"name": "Frost ac"
},
"eco_ac_temp": {
"name": "Eco ac"
},
"comfort_ac_temp": {
"name": "Comfort ac"
},
"boost_ac_temp": {
"name": "Boost ac"
},
"frost_away_temp": {
"name": "Frost away"
},
"eco_away_temp": {
"name": "Eco away"
},
"comfort_away_temp": {
"name": "Comfort away"
},
"boost_away_temp": {
"name": "Boost away"
},
"eco_ac_away_temp": {
"name": "Eco ac away"
},
"comfort_ac_away_temp": {
"name": "Comfort ac away"
},
"boost_ac_away_temp": {
"name": "Boost ac away"
}
}
}
}

View File

@@ -0,0 +1,197 @@
# pylint: disable=unused-argument
""" Implements the VersatileThermostat select component """
import logging
# from homeassistant.const import EVENT_HOMEASSISTANT_START
from homeassistant.core import HomeAssistant, CoreState # , callback
from homeassistant.components.number import (
NumberEntity,
NumberMode,
NumberDeviceClass,
)
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
from .const import (
DOMAIN,
DEVICE_MANUFACTURER,
CONF_NAME,
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__)
class TemperatureNumber(NumberEntity, RestoreEntity):
"""Representation of one temperature number"""
_attr_has_entity_name = True
_attr_translation_key = "temperature"
def __init__(
self,
hass: HomeAssistant,
unique_id,
name,
preset_name,
is_ac,
is_away,
entry_infos: ConfigEntry,
) -> 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))
self.my_climate = None
self._unique_id = unique_id
self._device_name = 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 = preset_name + " new 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}_{self._attr_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._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]
@property
def device_info(self) -> DeviceInfo:
"""Return the device info."""
return DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, self._unique_id)},
name=self._device_name,
manufacturer=DEVICE_MANUFACTURER,
model=DOMAIN,
)
@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
)
try:
if old_state is not None and (value := float(old_state.state) > 0):
self._attr_value = self._attr_native_value = value
except ValueError:
pass
@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
# Initialize the internal temp value of VTherm
self.my_climate.init_temperature_preset(
self._preset_name, self._attr_native_value, self._is_ac, self._is_away
)
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"""
if self.my_climate is None:
_LOGGER.warning(
"%s - cannot change temperature because VTherm is not initialized", self
)
return
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
self.async_write_ha_state()
# Update the VTherm
self.hass.create_task(
self.my_climate.service_set_preset_temperature(
self._preset_name.replace("_temp", ""), self._attr_native_value, None
)
)
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

View File

@@ -537,6 +537,56 @@
}
}
}
},
"number": {
"temperature": {
"name": "{preset}{ac}-{away} [%key:component::sensor::entity_component::temperature::name%]"
},
"frost_temp": {
"name": "Frost"
},
"eco_temp": {
"name": "Eco"
},
"comfort_temp": {
"name": "Comfort"
},
"boost_temp": {
"name": "Boost"
},
"frost_ac_temp": {
"name": "Frost ac"
},
"eco_ac_temp": {
"name": "Eco ac"
},
"comfort_ac_temp": {
"name": "Comfort ac"
},
"boost_ac_temp": {
"name": "Boost ac"
},
"frost_away_temp": {
"name": "Frost away"
},
"eco_away_temp": {
"name": "Eco away"
},
"comfort_away_temp": {
"name": "Comfort away"
},
"boost_away_temp": {
"name": "Boost away"
},
"eco_ac_away_temp": {
"name": "Eco ac away"
},
"comfort_ac_away_temp": {
"name": "Comfort ac away"
},
"boost_ac_away_temp": {
"name": "Boost ac away"
}
}
}
}

View File

@@ -186,7 +186,7 @@
},
"presence": {
"title": "Gestion de la présence",
"description": "Donnez un capteur de présence (true si quelqu'un est présent) et les températures cibles à utiliser en cas d'absence.",
"description": "Donnez un capteur de présence (true si quelqu'un est présent) et les températures cibles à utiliser en cas d'abs.",
"data": {
"presence_sensor_entity_id": "Capteur de présence",
"eco_away_temp": "preset Eco",
@@ -200,13 +200,13 @@
},
"data_description": {
"presence_sensor_entity_id": "Id d'entité du capteur de présence",
"eco_away_temp": "Température en preset Eco en cas d'absence",
"comfort_away_temp": "Température en preset Comfort en cas d'absence",
"boost_away_temp": "Température en preset Boost en cas d'absence",
"frost_away_temp": "Température en preset Hors-gel en cas d'absence",
"eco_ac_away_temp": "Température en preset Eco en cas d'absence en mode AC",
"comfort_ac_away_temp": "Température en preset Comfort en cas d'absence en mode AC",
"boost_ac_away_temp": "Température en preset Boost en cas d'absence en mode AC",
"eco_away_temp": "Température en preset Eco en cas d'abs",
"comfort_away_temp": "Température en preset Comfort en cas d'abs",
"boost_away_temp": "Température en preset Boost en cas d'abs",
"frost_away_temp": "Température en preset Hors-gel en cas d'abs",
"eco_ac_away_temp": "Température en preset Eco en cas d'abs en mode AC",
"comfort_ac_away_temp": "Température en preset Comfort en cas d'abs en mode AC",
"boost_ac_away_temp": "Température en preset Boost en cas d'abs en mode AC",
"use_presence_central_config": "Cochez pour utiliser la configuration centrale de la présence. Décochez et saisissez les attributs pour utiliser une configuration spécifique de la présence"
}
},
@@ -431,7 +431,7 @@
},
"presence": {
"title": "Présence - {name}",
"description": "Donnez un capteur de présence (true si quelqu'un est présent) et les températures cibles à utiliser en cas d'absence.",
"description": "Donnez un capteur de présence (true si quelqu'un est présent) et les températures cibles à utiliser en cas d'abs.",
"data": {
"presence_sensor_entity_id": "Capteur de présence",
"eco_away_temp": "preset Eco",
@@ -445,13 +445,13 @@
},
"data_description": {
"presence_sensor_entity_id": "Id d'entité du capteur de présence",
"eco_away_temp": "Température en preset Eco en cas d'absence",
"comfort_away_temp": "Température en preset Comfort en cas d'absence",
"boost_away_temp": "Température en preset Boost en cas d'absence",
"frost_away_temp": "Température en preset Hors-gel en cas d'absence",
"eco_ac_away_temp": "Température en preset Eco en cas d'absence en mode AC",
"comfort_ac_away_temp": "Température en preset Comfort en cas d'absence en mode AC",
"boost_ac_away_temp": "Température en preset Boost en cas d'absence en mode AC",
"eco_away_temp": "Température en preset Eco en cas d'abs",
"comfort_away_temp": "Température en preset Comfort en cas d'abs",
"boost_away_temp": "Température en preset Boost en cas d'abs",
"frost_away_temp": "Température en preset Hors-gel en cas d'abs",
"eco_ac_away_temp": "Température en preset Eco en cas d'abs en mode AC",
"comfort_ac_away_temp": "Température en preset Comfort en cas d'abs en mode AC",
"boost_ac_away_temp": "Température en preset Boost en cas d'abs en mode AC",
"use_presence_central_config": "Cochez pour utiliser la configuration centrale de la présence. Décochez et saisissez les attributs pour utiliser une configuration spécifique de la présence"
}
},
@@ -555,6 +555,56 @@
}
}
}
},
"number": {
"temperature": {
"name": "{preset}{ac}-{away} [%key:component::sensor::entity_component::temperature::name%]"
},
"frost_temp": {
"name": "Hors gel "
},
"eco_temp": {
"name": "Eco"
},
"comfort_temp": {
"name": "Confort"
},
"boost_temp": {
"name": "Boost"
},
"frost_ac_temp": {
"name": "Hors gel clim"
},
"eco_ac_temp": {
"name": "Eco clim"
},
"comfort_ac_temp": {
"name": "Confort clim"
},
"boost_ac_temp": {
"name": "Boost clim"
},
"frost_away_temp": {
"name": "Hors gel abs"
},
"eco_away_temp": {
"name": "Eco abs"
},
"comfort_away_temp": {
"name": "Confort abs"
},
"boost_away_temp": {
"name": "Boost abs"
},
"eco_ac_away_temp": {
"name": "Eco clim abs"
},
"comfort_ac_away_temp": {
"name": "Confort clim abs"
},
"boost_ac_away_temp": {
"name": "Boost clim abs"
}
}
}
}

View File

@@ -4,6 +4,10 @@ import logging
from homeassistant.core import HomeAssistant
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 .const import (
DOMAIN,
CONF_AUTO_REGULATION_EXPERT,
@@ -52,19 +56,24 @@ class VersatileThermostatAPI(dict):
self._central_boiler_entity = None
self._threshold_number_entity = None
self._nb_active_number_entity = None
self._central_configuration = None
# A dict that will store all Number entities which holds the temperature
self._number_temperatures = dict()
def find_central_configuration(self):
"""Search for a central configuration"""
for config_entry in VersatileThermostatAPI._hass.config_entries.async_entries(
DOMAIN
):
if (
config_entry.data.get(CONF_THERMOSTAT_TYPE)
== CONF_THERMOSTAT_CENTRAL_CONFIG
):
central_config = config_entry
return central_config
return None
if not self._central_configuration:
for (
config_entry
) in VersatileThermostatAPI._hass.config_entries.async_entries(DOMAIN):
if (
config_entry.data.get(CONF_THERMOSTAT_TYPE)
== CONF_THERMOSTAT_CENTRAL_CONFIG
):
self._central_configuration = config_entry
break
# return self._central_configuration
return self._central_configuration
def add_entry(self, entry: ConfigEntry):
"""Add a new entry"""
@@ -108,14 +117,51 @@ class VersatileThermostatAPI(dict):
"""register the two number entities needed for boiler activation"""
self._threshold_number_entity = threshold_number_entity
# If sensor and threshold number are initialized, reload the listener
if self._nb_active_number_entity and self._central_boiler_entity:
self._hass.async_add_job(self.reload_central_boiler_binary_listener)
# if self._nb_active_number_entity and self._central_boiler_entity:
# self._hass.async_add_job(self.reload_central_boiler_binary_listener)
def register_nb_device_active_boiler(self, nb_active_number_entity):
"""register the two number entities needed for boiler activation"""
self._nb_active_number_entity = nb_active_number_entity
if self._threshold_number_entity and self._central_boiler_entity:
self._hass.async_add_job(self.reload_central_boiler_binary_listener)
# if self._threshold_number_entity and self._central_boiler_entity:
# self._hass.async_add_job(self.reload_central_boiler_binary_listener)
def register_temperature_number(
self,
config_id: str,
preset_name: str,
number_entity: NumberEntity,
):
"""Register the NumberEntity for a particular device / preset."""
# Search for device_name into the _number_temperatures dict
if not self._number_temperatures.get(config_id):
self._number_temperatures[config_id] = dict()
self._number_temperatures.get(config_id)[preset_name] = number_entity
def get_temperature_number_value(self, config_id, preset_name) -> float | None:
"""Returns the value of a previously registred NumberEntity which represent
a temperature. If no NumberEntity was previously registred, then returns None"""
entities = self._number_temperatures.get(config_id, None)
if entities:
entity = entities.get(preset_name, None)
if entity:
return entity.state
return None
async def init_vtherm_links(self):
"""INitialize all VTherms entities links
This method is called when HA is fully started (and all entities should be initialized)
"""
await self.reload_central_boiler_binary_listener()
await self.reload_central_boiler_entities_list()
# Initialization of all preset for all VTherm
component: EntityComponent[ClimateEntity] = self._hass.data.get(
CLIMATE_DOMAIN, None
)
if component:
for entity in component.entities:
await entity.init_presets(self.find_central_configuration())
async def reload_central_boiler_binary_listener(self):
"""Reloads the BinarySensor entity which listen to the number of

View File

@@ -25,5 +25,9 @@ fi
## without resulting to symlinks.
export PYTHONPATH="${PYTHONPATH}:${PWD}/custom_components"
## Link custom_components into config
rm -f ${PWD}/config/custom_components
ln -s ${PWD}/custom_components ${PWD}/config/
# Start Home Assistant
hass --config "${PWD}/config" --debug

View File

@@ -528,6 +528,7 @@ def search_entity(hass: HomeAssistant, entity_id, domain) -> Entity:
"""Search and return the entity in the domain"""
component = hass.data[domain]
for entity in component.entities:
_LOGGER.debug("Found %s entity: %s", domain, entity.entity_id)
if entity.entity_id == entity_id:
return entity
return None

396
tests/test_temp_number.py Normal file
View File

@@ -0,0 +1,396 @@
""" Test the NumberEntity taht holds the temperature of a VTherm or of a Central configuration """
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long
# from unittest.mock import patch, call
# from datetime import datetime, timedelta
from homeassistant.core import HomeAssistant
from homeassistant.config_entries import ConfigEntryState
# 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 pytest_homeassistant_custom_component.common import MockConfigEntry
# from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
from custom_components.versatile_thermostat.vtherm_api import VersatileThermostatAPI
from .commons import *
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_add_number_for_central_config(
hass: HomeAssistant, skip_hass_states_is_state
):
"""Test the construction of a central configuration and the
creation and registration of the NumberEntity which holds
the temperature initialized from config_entry"""
vtherm_api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
temps = {
"frost_temp": 10,
"eco_temp": 17.1,
"comfort_temp": 18.1,
"boost_temp": 19.1,
"eco_ac_temp": 25.1,
"comfort_ac_temp": 23.1,
"boost_ac_temp": 21.1,
"frost_away_temp": 15.1,
"eco_away_temp": 15.2,
"comfort_away_temp": 15.3,
"boost_away_temp": 15.4,
"eco_ac_away_temp": 30.5,
"comfort_ac_away_temp": 30.6,
"boost_ac_away_temp": 30.7,
}
central_config_entry = MockConfigEntry(
domain=DOMAIN,
title="TheCentralConfigMockName",
unique_id="centralConfigUniqueId",
data={
CONF_NAME: CENTRAL_CONFIG_NAME,
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CENTRAL_CONFIG,
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_central_ext_temp_sensor",
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
CONF_TPI_COEF_INT: 0.5,
CONF_TPI_COEF_EXT: 0.02,
CONF_WINDOW_DELAY: 15,
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 4,
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 1,
CONF_WINDOW_AUTO_MAX_DURATION: 31,
CONF_MOTION_DELAY: 31,
CONF_MOTION_OFF_DELAY: 301,
CONF_MOTION_PRESET: "boost",
CONF_NO_MOTION_PRESET: "frost",
CONF_POWER_SENSOR: "sensor.mock_central_power_sensor",
CONF_MAX_POWER_SENSOR: "sensor.mock_central_max_power_sensor",
CONF_PRESET_POWER: 14,
CONF_MINIMAL_ACTIVATION_DELAY: 11,
CONF_SECURITY_DELAY_MIN: 61,
CONF_SECURITY_MIN_ON_PERCENT: 0.5,
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.2,
CONF_ADD_CENTRAL_BOILER_CONTROL: False,
}
| temps,
)
central_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(central_config_entry.entry_id)
assert central_config_entry.state is ConfigEntryState.LOADED
# We search for NumberEntities
for preset_name, value in temps.items():
temp_entity = search_entity(
hass,
"number.central_configuration_" + preset_name,
NUMBER_DOMAIN,
)
assert temp_entity
assert temp_entity.state == value
# This test is dependent to translation en.json. If translations change
# this may fails
assert (
temp_entity.name.lower()
== preset_name.replace("_temp", "")
.replace("_ac", " ac")
.replace("_away", " away")
.lower()
)
# Find temp Number into vtherm_api
val = vtherm_api.get_temperature_number_value(
config_id=central_config_entry.entry_id, preset_name=preset_name
)
assert val == value
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_add_number_for_central_config_without_temp(
hass: HomeAssistant, skip_hass_states_is_state
):
"""Test the construction of a central configuration and the
creation and registration of the NumberEntity which holds
the temperature not intialized from confif_entry.
In non AC_MODE the value should be initialized to the MIN"""
vtherm_api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
# Default is min Value in non AC_MODE
temps = {
"frost_temp": 15.0,
"eco_temp": 15.0,
"comfort_temp": 15.0,
"boost_temp": 15.0,
"eco_ac_temp": 15.0,
"comfort_ac_temp": 15.0,
"boost_ac_temp": 15.0,
"frost_away_temp": 15.0,
"eco_away_temp": 15.0,
"comfort_away_temp": 15.0,
"boost_away_temp": 15.0,
"eco_ac_away_temp": 15.0,
"comfort_ac_away_temp": 15.0,
"boost_ac_away_temp": 15.0,
}
central_config_entry = MockConfigEntry(
domain=DOMAIN,
title="TheCentralConfigMockName",
unique_id="centralConfigUniqueId",
data={
CONF_NAME: CENTRAL_CONFIG_NAME,
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CENTRAL_CONFIG,
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_central_ext_temp_sensor",
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
CONF_TPI_COEF_INT: 0.5,
CONF_TPI_COEF_EXT: 0.02,
CONF_WINDOW_DELAY: 15,
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 4,
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 1,
CONF_WINDOW_AUTO_MAX_DURATION: 31,
CONF_MOTION_DELAY: 31,
CONF_MOTION_OFF_DELAY: 301,
CONF_MOTION_PRESET: "boost",
CONF_NO_MOTION_PRESET: "frost",
CONF_POWER_SENSOR: "sensor.mock_central_power_sensor",
CONF_MAX_POWER_SENSOR: "sensor.mock_central_max_power_sensor",
CONF_PRESET_POWER: 14,
CONF_MINIMAL_ACTIVATION_DELAY: 11,
CONF_SECURITY_DELAY_MIN: 61,
CONF_SECURITY_MIN_ON_PERCENT: 0.5,
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.2,
CONF_ADD_CENTRAL_BOILER_CONTROL: False,
},
# | temps,
)
central_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(central_config_entry.entry_id)
assert central_config_entry.state is ConfigEntryState.LOADED
# We search for NumberEntities
for preset_name, value in temps.items():
temp_entity = search_entity(
hass,
"number.central_configuration_" + preset_name,
NUMBER_DOMAIN,
)
assert temp_entity
assert temp_entity.state == value
# This test is dependent to translation en.json. If translations change
# this may fails
assert (
temp_entity.name.lower()
== preset_name.replace("_temp", "")
.replace("_ac", " ac")
.replace("_away", " away")
.lower()
)
# Find temp Number into vtherm_api
val = vtherm_api.get_temperature_number_value(
config_id=central_config_entry.entry_id, preset_name=preset_name
)
assert val == value
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_add_number_for_central_config_without_temp_ac_mode(
hass: HomeAssistant, skip_hass_states_is_state
):
"""Test the construction of a central configuration and the
creation and registration of the NumberEntity which holds
the temperature not intialized from confif_entry.
In AC_MODE the defaul value should the MAX"""
vtherm_api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
# Default is min Value in non AC_MODE
temps = {
"frost_temp": 30.0,
"eco_temp": 30.0,
"comfort_temp": 30.0,
"boost_temp": 30.0,
"eco_ac_temp": 30.0,
"comfort_ac_temp": 30.0,
"boost_ac_temp": 30.0,
"frost_away_temp": 30.0,
"eco_away_temp": 30.0,
"comfort_away_temp": 30.0,
"boost_away_temp": 30.0,
"eco_ac_away_temp": 30.0,
"comfort_ac_away_temp": 30.0,
"boost_ac_away_temp": 30.0,
}
central_config_entry = MockConfigEntry(
domain=DOMAIN,
title="TheCentralConfigMockName",
unique_id="centralConfigUniqueId",
data={
CONF_NAME: CENTRAL_CONFIG_NAME,
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CENTRAL_CONFIG,
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_central_ext_temp_sensor",
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
CONF_AC_MODE: True,
CONF_TPI_COEF_INT: 0.5,
CONF_TPI_COEF_EXT: 0.02,
CONF_WINDOW_DELAY: 15,
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 4,
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 1,
CONF_WINDOW_AUTO_MAX_DURATION: 31,
CONF_MOTION_DELAY: 31,
CONF_MOTION_OFF_DELAY: 301,
CONF_MOTION_PRESET: "boost",
CONF_NO_MOTION_PRESET: "frost",
CONF_POWER_SENSOR: "sensor.mock_central_power_sensor",
CONF_MAX_POWER_SENSOR: "sensor.mock_central_max_power_sensor",
CONF_PRESET_POWER: 14,
CONF_MINIMAL_ACTIVATION_DELAY: 11,
CONF_SECURITY_DELAY_MIN: 61,
CONF_SECURITY_MIN_ON_PERCENT: 0.5,
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.2,
CONF_ADD_CENTRAL_BOILER_CONTROL: False,
},
# | temps,
)
central_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(central_config_entry.entry_id)
assert central_config_entry.state is ConfigEntryState.LOADED
# We search for NumberEntities
for preset_name, value in temps.items():
temp_entity = search_entity(
hass,
"number.central_configuration_" + preset_name,
NUMBER_DOMAIN,
)
assert temp_entity
assert temp_entity.state == value
# This test is dependent to translation en.json. If translations change
# this may fails
assert (
temp_entity.name.lower()
== preset_name.replace("_temp", "")
.replace("_ac", " ac")
.replace("_away", " away")
.lower()
)
# Find temp Number into vtherm_api
val = vtherm_api.get_temperature_number_value(
config_id=central_config_entry.entry_id, preset_name=preset_name
)
assert val == value
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_add_number_for_central_config_without_temp_restore(
hass: HomeAssistant, skip_hass_states_is_state
):
"""Test the construction of a central configuration and the
creation and registration of the NumberEntity which holds
the temperature not intialized from confif_entry"""
vtherm_api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
# Default is min Value in non AC_MODE
temps = {
"frost_temp": 23.0,
"eco_temp": 23.0,
"comfort_temp": 23.0,
"boost_temp": 23.0,
"eco_ac_temp": 23.0,
"comfort_ac_temp": 23.0,
"boost_ac_temp": 23.0,
"frost_away_temp": 23.0,
"eco_away_temp": 23.0,
"comfort_away_temp": 23.0,
"boost_away_temp": 23.0,
"eco_ac_away_temp": 23.0,
"comfort_ac_away_temp": 23.0,
"boost_ac_away_temp": 23.0,
}
central_config_entry = MockConfigEntry(
domain=DOMAIN,
title="TheCentralConfigMockName",
unique_id="centralConfigUniqueId",
data={
CONF_NAME: CENTRAL_CONFIG_NAME,
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CENTRAL_CONFIG,
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_central_ext_temp_sensor",
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
CONF_AC_MODE: False,
CONF_TPI_COEF_INT: 0.5,
CONF_TPI_COEF_EXT: 0.02,
CONF_WINDOW_DELAY: 15,
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 4,
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 1,
CONF_WINDOW_AUTO_MAX_DURATION: 31,
CONF_MOTION_DELAY: 31,
CONF_MOTION_OFF_DELAY: 301,
CONF_MOTION_PRESET: "boost",
CONF_NO_MOTION_PRESET: "frost",
CONF_POWER_SENSOR: "sensor.mock_central_power_sensor",
CONF_MAX_POWER_SENSOR: "sensor.mock_central_max_power_sensor",
CONF_PRESET_POWER: 14,
CONF_MINIMAL_ACTIVATION_DELAY: 11,
CONF_SECURITY_DELAY_MIN: 61,
CONF_SECURITY_MIN_ON_PERCENT: 0.5,
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.2,
CONF_ADD_CENTRAL_BOILER_CONTROL: False,
},
# | temps,
)
with patch(
"homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state",
return_value=State(entity_id="number.mock_valve", state="23"),
):
central_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(central_config_entry.entry_id)
assert central_config_entry.state is ConfigEntryState.LOADED
# We search for NumberEntities
for preset_name, value in temps.items():
temp_entity = search_entity(
hass,
"number.central_configuration_" + preset_name,
NUMBER_DOMAIN,
)
assert temp_entity
assert temp_entity.state == value
# This test is dependent to translation en.json. If translations change
# this may fails
assert (
temp_entity.name.lower()
== preset_name.replace("_temp", "")
.replace("_ac", " ac")
.replace("_away", " away")
.lower()
)
# Find temp Number into vtherm_api
val = vtherm_api.get_temperature_number_value(
config_id=central_config_entry.entry_id, preset_name=preset_name
)
assert val == value