""" Implement the Sensor entities of this implementation """ from __future__ import annotations from datetime import datetime, timedelta, timezone import logging from typing import Any, Mapping from home_connect_async import Appliance, HomeConnect, Events, HealthStatus from homeassistant.components.sensor import SensorEntity from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType from .common import Configuration, EntityBase, EntityManager from .const import ( CONF_TRANSLATION_MODE_SERVER, DEVICE_ICON_MAP, DOMAIN, CONF_TRANSLATION_MODE, HOME_CONNECT_DEVICE, ) _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigType, async_add_entities: AddEntitiesCallback,) -> None: """Add sensors for passed config_entry in HA""" #homeconnect: HomeConnect = hass.data[DOMAIN]["homeconnect"] entry_conf:Configuration = hass.data[DOMAIN][config_entry.entry_id] homeconnect:HomeConnect = entry_conf["homeconnect"] entity_manager = EntityManager(async_add_entities, "Sensor") def add_appliance(appliance: Appliance) -> None: if appliance.selected_program: conf = entry_conf.get_config({"program_type": "selected"}) device = ProgramSensor(appliance, None, conf) entity_manager.add(device) if appliance.active_program: conf = entry_conf.get_config({"program_type": "active"}) device = ProgramSensor(appliance, None, conf) entity_manager.add(device) conf = entry_conf.get_config() if appliance.selected_program and appliance.selected_program.options: for option in appliance.selected_program.options.values(): if not isinstance(option.value, bool): device = ProgramOptionSensor(appliance, option.key, conf) entity_manager.add(device) if appliance.active_program and appliance.active_program.options: for option in appliance.active_program.options.values(): if not isinstance(option.value, bool): device = ProgramOptionSensor(appliance, option.key, conf) entity_manager.add(device) if appliance.status: for (key, value) in appliance.status.items(): device = None if not isinstance(value.value, bool) and conf.get_entity_setting(key, "type") != "Boolean": # should be a binary sensor if it has a boolean value if "temperature" in key.lower(): conf.set_entity_setting(key,"class","temperature") device = StatusSensor(appliance, key, conf) entity_manager.add(device) if appliance.settings: for setting in appliance.settings.values(): conf = entry_conf.get_config() if setting.type != "Boolean" and not isinstance(setting.value, bool) and conf.get_entity_setting(setting.key, "type") != "Boolean": device = SettingsSensor(appliance, setting.key, conf) entity_manager.add(device) entity_manager.register() def remove_appliance(appliance: Appliance) -> None: entity_manager.remove_appliance(appliance) # First add the global home connect status sensor async_add_entities( [ HomeConnectStatusSensor(homeconnect, "" if entry_conf["primary_config_entry"] else "_"+config_entry.entry_id) ] ) # Subscribe for events and register the existing appliances homeconnect.register_callback(add_appliance, [Events.PAIRED, Events.DATA_CHANGED, Events.PROGRAM_STARTED, Events.PROGRAM_SELECTED]) homeconnect.register_callback(remove_appliance, Events.DEPAIRED) for appliance in homeconnect.appliances.values(): add_appliance(appliance) class ProgramSensor(EntityBase, SensorEntity): """Selected program sensor""" @property def unique_id(self) -> str: return f"{self.haId}_{self._conf['program_type']}_program" @property def name_ext(self) -> str: return f"{self._conf['program_type'].capitalize()} Program" @property def translation_key(self) -> str: return "programs" @property def icon(self) -> str: if self._appliance.type in DEVICE_ICON_MAP: return DEVICE_ICON_MAP[self._appliance.type] return None @property def device_class(self) -> str: return f"{DOMAIN}__programs" @property def native_value(self): """Return the state of the sensor.""" prog = self._appliance.selected_program if self._conf["program_type"] == "selected" else self._appliance.active_program if prog: if (prog.name and self._conf[CONF_TRANSLATION_MODE] == CONF_TRANSLATION_MODE_SERVER): return prog.name return prog.key return None async def async_on_update(self, appliance: Appliance, key: str, value) -> None: _LOGGER.debug("Updating sensor %s => %s", self.unique_id, self.native_value) self.async_write_ha_state() class ProgramOptionSensor(EntityBase, SensorEntity): """Special active program sensor""" @property def device_class(self) -> str: if self.has_entity_setting("class"): return self.get_entity_setting("class") return f"{DOMAIN}__options" @property def translation_key(self) -> str: return "options" @property def icon(self) -> str: return self.get_entity_setting("icon", "mdi:office-building-cog") @property def name_ext(self) -> str: if self._appliance.selected_program and self._key in self._appliance.selected_program.options: return self._appliance.selected_program.options[self._key].name if self._appliance.active_program and self._key in self._appliance.active_program.options: return self._appliance.active_program.options[self._key].name return None @property def available(self) -> bool: return ( ( self._appliance.selected_program and (self._key in self._appliance.selected_program.options) ) or ( self._appliance.active_program and (self._key in self._appliance.active_program.options) ) ) and super().available @property def internal_unit(self) -> str | None: """Get the original unit before manipulations""" unit = None t = None if self._appliance.active_program and self._key in self._appliance.active_program.options: t = self._appliance.active_program.options[self._key].type unit = self._appliance.active_program.options[self._key].unit elif self._appliance.selected_program and self._key in self._appliance.selected_program.options: t = self._appliance.selected_program.options[self._key].type unit = self._appliance.selected_program.options[self._key].unit if unit is None and t in ["Double", "Float", "Int"]: return "" return unit @property def native_unit_of_measurement(self) -> str | None: if self.has_entity_setting("unit"): return self.get_entity_setting("unit") unit = self.internal_unit if unit == "gram": return "kg" return unit @property def native_value(self): """Return the state of the sensor.""" program = ( self._appliance.active_program if self._appliance.active_program else self._appliance.selected_program ) if program is None: return None if self._key not in program.options: _LOGGER.debug("Option key %s is missing from program", self._key) return None option = program.options[self._key] if self.device_class == "timestamp": return datetime.now(timezone.utc).astimezone() + timedelta( seconds=option.value ) if self.device_class and "timespan" in self.device_class: m, s = divmod(option.value, 60) h, m = divmod(m, 60) return f"{h}:{m:02d}" if self.internal_unit == "gram": return round(option.value / 1000, 1) if ( option.displayvalue and self._conf[CONF_TRANSLATION_MODE] == CONF_TRANSLATION_MODE_SERVER ): return option.displayvalue if ( isinstance(option.value, str) and self._conf[CONF_TRANSLATION_MODE] == CONF_TRANSLATION_MODE_SERVER ): if option.value.endswith(".Off"): return "Off" if option.value.endswith(".On"): return "On" return option.value async def async_on_update(self, appliance: Appliance, key: str, value) -> None: self.async_write_ha_state() class StatusSensor(EntityBase, SensorEntity): """Status sensor""" @property def device_class(self) -> str: return f"{DOMAIN}__status" @property def translation_key(self) -> str: return "statuses" @property def name_ext(self) -> str: if self._key in self._appliance.status: status = self._appliance.status[self._key] if status: return status.name return None @property def icon(self) -> str: return self.get_entity_setting("icon", "mdi:gauge-full") @property def native_unit_of_measurement(self) -> str | None: if self.has_entity_setting("unit"): return self.get_entity_setting("unit") status = self._appliance.status.get(self._key) if status: return status.unit return None @property def native_value(self): """Return the state of the sensor.""" status = self._appliance.status.get(self._key) if status: if status.displayvalue and self._conf[CONF_TRANSLATION_MODE] == CONF_TRANSLATION_MODE_SERVER: return status.displayvalue return status.value return None async def async_on_update(self, appliance: Appliance, key: str, value) -> None: self.async_write_ha_state() class SettingsSensor(EntityBase, SensorEntity): """Settings sensor""" @property def device_class(self) -> str: return f"{DOMAIN}__settings" @property def translation_key(self) -> str: return "settings" @property def name_ext(self) -> str: if self._key in self._appliance.settings: setting = self._appliance.settings[self._key] if setting: return setting.name return None @property def icon(self) -> str: return self.get_entity_setting("icon", "mdi:tune") @property def native_unit_of_measurement(self) -> str | None: if self.has_entity_setting("unit"): return self.get_entity_setting("unit") setting = self._appliance.settings.get(self._key) if setting: if setting.unit is None and setting.type in ["Double", "Float", "Int"]: return "" return setting.unit return None @property def native_value(self): """Return the state of the sensor.""" setting = self._appliance.settings.get(self._key) if setting: if setting.displayvalue and self._conf[CONF_TRANSLATION_MODE] == CONF_TRANSLATION_MODE_SERVER: return setting.displayvalue return setting.value return None async def async_on_update(self, appliance: Appliance, key: str, value) -> None: self.async_write_ha_state() class HomeConnectStatusSensor(SensorEntity): """Global Home Connect status sensor""" should_poll = True _attr_has_entity_name = True def __init__(self, homeconnect: HomeConnect, name_suffix:str) -> None: self._homeconnect = homeconnect self._name_suffix = name_suffix self.entity_id = f"home_connect.{self.unique_id}" @property def device_info(self): """Return information to link this entity with the correct device.""" return HOME_CONNECT_DEVICE @property def unique_id(self) -> str: return "homeconnect_status" + self._name_suffix @property def translation_key(self) -> str: return "homeconnect_status" @property def available(self) -> bool: return True @property def native_value(self): return self._homeconnect.health.get_status().name @property def extra_state_attributes(self) -> Mapping[str, Any] | None: return { "blocked_until": self._homeconnect.health.get_blocked_until(), "blocked_for": self._homeconnect.health.get_block_time_str(), }