Files
HomeAssistantVS/custom_components/versatile_thermostat/commons.py
T
2026-06-16 10:33:21 -04:00

197 lines
6.4 KiB
Python

""" Some usefull commons class """
# pylint: disable=line-too-long
import logging
from vtherm_api.log_collector import get_vtherm_logger
import warnings
from homeassistant.core import HomeAssistant
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers import entity_registry as er
from .const import ServiceConfigurationError, DOMAIN
_LOGGER = get_vtherm_logger(__name__)
def round_to_nearest(n: float, x: float) -> float:
"""Round a number to the nearest x (which should be decimal but not null)
Example:
nombre1 = 3.2
nombre2 = 4.7
x = 0.3
nombre_arrondi1 = round_to_nearest(nombre1, x)
nombre_arrondi2 = round_to_nearest(nombre2, x)
print(nombre_arrondi1) # Output: 3.3
print(nombre_arrondi2) # Output: 4.6
"""
assert x > 0
return round(n * (1 / x)) / (1 / x)
def check_and_extract_service_configuration(service_config) -> dict:
"""Raise a ServiceConfigurationError. In return you have a dict formatted like follows.
Example if you call with 'climate.central_boiler/climate.set_temperature/temperature:10':
{
"service_domain": "climate",
"service_name": "set_temperature",
"entity_id": "climate.central_boiler",
"entity_domain": "climate",
"entity_name": "central_boiler",
"data": {
"temperature": "10"
},
"attribute_name": "temperature",
"attribute_value: "10"
}
For this example 'switch.central_boiler/switch.turn_off' you will have this:
{
"service_domain": "switch",
"service_name": "turn_off",
"entity_id": "switch.central_boiler",
"entity_domain": "switch",
"entity_name": "central_boiler",
"data": { },
}
All values are striped (white space are removed) and are string
"""
ret = {}
if service_config is None:
return ret
parties = service_config.split("/")
if len(parties) < 2:
raise ServiceConfigurationError(
f"Incorrect service configuration. Service {service_config} should be formatted with: 'entity_name/service_name[/data]'. See README for more information."
)
entity_id = parties[0]
service_name = parties[1]
service_infos = service_name.split(".")
if len(service_infos) != 2:
raise ServiceConfigurationError(
f"Incorrect service configuration. The service {service_config} should be formatted like: 'domain.service_name' (ex: 'switch.turn_on'). See README for more information."
)
ret.update(
{
"service_domain": service_infos[0].strip(),
"service_name": service_infos[1].strip(),
}
)
entity_infos = entity_id.split(".")
if len(entity_infos) != 2:
raise ServiceConfigurationError(
f"Incorrect service configuration. The entity_id {entity_id} should be formatted like: 'domain.entity_name' (ex: 'switch.central_boiler_switch'). See README for more information."
)
ret.update(
{
"entity_domain": entity_infos[0].strip(),
"entity_name": entity_infos[1].strip(),
"entity_id": entity_id.strip(),
}
)
if len(parties) == 3:
data = parties[2]
if len(data) > 0:
data_infos = None
data_infos = data.split(":")
if (
len(data_infos) != 2
or len(data_infos[0]) <= 0
or len(data_infos[1]) <= 0
):
raise ServiceConfigurationError(
f"Incorrect service configuration. The data {data} should be formatted like: 'attribute:value' (ex: 'value:25'). See README for more information."
)
ret.update(
{
"attribute_name": data_infos[0].strip(),
"attribute_value": data_infos[1].strip(),
"data": {data_infos[0].strip(): data_infos[1].strip()},
}
)
else:
raise ServiceConfigurationError(
f"Incorrect service configuration. The data {data} should be formatted like: 'attribute:value' (ex: 'value:25'). See README for more information."
)
else:
ret.update({"data": {}})
_LOGGER.debug(
"check_and_extract_service_configuration(%s) gives '%s'", service_config, ret
)
return ret
def deprecated(message):
"""A decorator to indicate that the method/attribut is deprecated"""
def decorator(func):
def wrapper(*args, **kwargs):
warnings.warn(
f"{func.__name__} is deprecated: {message}",
DeprecationWarning,
stacklevel=2,
)
return func(*args, **kwargs)
return wrapper
return decorator
def write_event_log(logger: logging.Logger, vtherm: "BaseThermostat", message: str):
"""Write an event log entry for the thermostat."""
logger.info("%s - ---------------------> NEW EVENT: %s --------------------------------------------------------------", vtherm, message)
async def cleanup_orphan_entity(
hass: HomeAssistant,
entry: ConfigEntry,
domain: str,
device_name: str,
unique_id_suffix: str,
) -> None:
"""Remove an orphan entity from entity registry if it exists but is no longer needed.
This generic function can be used for any entity type that needs to be
conditionally created/removed based on configuration changes.
Args:
hass: The Home Assistant instance.
entry: The config entry for the thermostat.
domain: The entity domain (e.g., "sensor", "binary_sensor", "switch").
device_name: The device name used to build the unique_id.
unique_id_suffix: The suffix appended to the device name (e.g., "auto_tpi_learning").
"""
registry = er.async_get(hass)
# Build the expected unique_id for the entity
expected_unique_id = f"{device_name}_{unique_id_suffix}"
# Find entity by unique_id within this config entry
entity_id = registry.async_get_entity_id(
domain, DOMAIN, expected_unique_id
)
if entity_id:
entity_entry = registry.async_get(entity_id)
if entity_entry and entity_entry.config_entry_id == entry.entry_id:
_LOGGER.debug(
"Removing orphan %s entity %s from registry (feature disabled)",
domain,
entity_id
)
registry.async_remove(entity_id)