""" 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)