import asyncio import time from typing import Optional from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, SensorStateClass, ) from homeassistant.const import ( PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, UnitOfElectricCurrent, UnitOfElectricPotential, UnitOfEnergy, UnitOfPower, UnitOfTemperature, ) from homeassistant.util import dt from .core.const import DOMAIN from .core.entity import XEntity from .core.ewelink import SIGNAL_ADD_ENTITIES, XRegistry PARALLEL_UPDATES = 0 # fix entity_platform parallel_updates Semaphore async def async_setup_entry(hass, config_entry, add_entities): ewelink: XRegistry = hass.data[DOMAIN][config_entry.entry_id] ewelink.dispatcher_connect( SIGNAL_ADD_ENTITIES, lambda x: add_entities([e for e in x if isinstance(e, SensorEntity)]), ) DEVICE_CLASSES = { "battery": SensorDeviceClass.BATTERY, "battery_voltage": SensorDeviceClass.VOLTAGE, "current": SensorDeviceClass.CURRENT, "humidity": SensorDeviceClass.HUMIDITY, "outdoor_temp": SensorDeviceClass.TEMPERATURE, "power": SensorDeviceClass.POWER, "rssi": SensorDeviceClass.SIGNAL_STRENGTH, "temperature": SensorDeviceClass.TEMPERATURE, "voltage": SensorDeviceClass.VOLTAGE, } UNITS = { "battery": PERCENTAGE, "battery_voltage": UnitOfElectricPotential.VOLT, "current": UnitOfElectricCurrent.AMPERE, "humidity": PERCENTAGE, "outdoor_temp": UnitOfTemperature.CELSIUS, "power": UnitOfPower.WATT, "rssi": SIGNAL_STRENGTH_DECIBELS_MILLIWATT, "temperature": UnitOfTemperature.CELSIUS, "voltage": UnitOfElectricPotential.VOLT, } class XSensor(XEntity, SensorEntity): """Class can convert string sensor value to float, multiply it and round if needed. Also class can filter incoming values using zigbee-like reporting logic: min report interval, max report interval, reportable change value. """ multiply: float = None round: int = None report_ts = None report_mint = None report_maxt = None report_delta = None report_value = None def __init__(self, ewelink: XRegistry, device: dict): if self.param and self.uid is None: self.uid = self.param default_class = ( self.uid[:-2] if self.uid.endswith(("_1", "_2", "_3", "_4")) else self.uid ) self._attr_device_class = DEVICE_CLASSES.get(default_class) if default_class in UNITS: # by default all sensors with units is measurement sensors self._attr_state_class = SensorStateClass.MEASUREMENT self._attr_native_unit_of_measurement = UNITS[default_class] XEntity.__init__(self, ewelink, device) reporting = device.get("reporting", {}).get(self.uid) if reporting: self.report_mint, self.report_maxt, self.report_delta = reporting self.report_ts = time.time() self._attr_should_poll = True def set_state(self, params: dict = None, value: float = None): if params: value = params[self.param] if self.native_unit_of_measurement and isinstance(value, str): try: # https://github.com/AlexxIT/SonoffLAN/issues/1061 value = float(value) except Exception: return if self.multiply: value *= self.multiply if self.round is not None: # convert to int when round is zero value = round(value, self.round or None) if self.report_ts is not None: ts = time.time() try: if (ts - self.report_ts < self.report_mint) or ( ts - self.report_ts < self.report_maxt and abs(value - self.native_value) <= self.report_delta ): self.report_value = value return self.report_value = None except Exception: pass self.report_ts = ts self._attr_native_value = value async def async_update(self): if self.report_value is not None: XSensor.set_state(self, value=self.report_value) class XTemperatureTH(XSensor): params = {"currentTemperature", "temperature"} uid = "temperature" def set_state(self, params: dict = None, value: float = None): try: # can be int, float, str or undefined value = params.get("currentTemperature") or params["temperature"] value = float(value) # filter zero values # https://github.com/AlexxIT/SonoffLAN/issues/110 # filter wrong values # https://github.com/AlexxIT/SonoffLAN/issues/683 if value != 0 and -270 < value < 270: XSensor.set_state(self, value=round(value, 1)) except Exception: XSensor.set_state(self) class XHumidityTH(XSensor): params = {"currentHumidity", "humidity"} uid = "humidity" def set_state(self, params: dict = None, value: float = None): try: value = params.get("currentHumidity") or params["humidity"] value = float(value) # filter zero values # https://github.com/AlexxIT/SonoffLAN/issues/110 if value != 0: XSensor.set_state(self, value=value) except Exception: XSensor.set_state(self) class XEnergySensor(XEntity, SensorEntity): get_params = None next_ts = 0 _attr_device_class = SensorDeviceClass.ENERGY _attr_entity_registry_enabled_default = False _attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR _attr_state_class = SensorStateClass.TOTAL_INCREASING _attr_should_poll = True def __init__(self, ewelink: XRegistry, device: dict): XEntity.__init__(self, ewelink, device) reporting = device.get("reporting", {}) self.report_dt, self.report_history = reporting.get(self.uid) or (3600, 0) @staticmethod def decode_energy(value: str) -> Optional[list]: try: return [ round( int(value[i : i + 2], 16) + int(value[i + 3], 10) * 0.1 + int(value[i + 5], 10) * 0.01, 2, ) for i in range(0, len(value), 6) ] except Exception: return None def set_state(self, params: dict): history = self.decode_energy(params[self.param]) if not history: return self._attr_native_value = history[0] if self.report_history: self._attr_extra_state_attributes = { "history": history[0 : self.report_history] } async def async_update(self): ts = time.time() if ts < self.next_ts or not self.available or not self.ewelink.cloud.online: return ok = await self.ewelink.send_cloud(self.device, self.get_params, query=False) if ok == "online": self.next_ts = ts + self.report_dt class XEnergySensorDualR3(XEnergySensor, SensorEntity): @staticmethod def decode_energy(value: str) -> Optional[list]: try: return [ round( int(value[i : i + 2], 16) + int(value[i + 2 : i + 4], 10) * 0.01, 2 ) for i in range(0, len(value), 4) ] except Exception: return None class XEnergySensorPOWR3(XEnergySensor, SensorEntity): @staticmethod def decode_energy(value: str) -> Optional[list]: try: return [ round(int(value[i], 16) + int(value[i + 1 : i + 3], 10) * 0.01, 2) for i in range(0, len(value), 3) ] except Exception: return None async def async_update(self): ts = time.time() if ts < self.next_ts or not self.available: return # POWR3 support LAN energy request (POST /zeroconf/getHoursKwh) ok = await self.ewelink.send(self.device, self.get_params, timeout_lan=5) if ok == "online": self.next_ts = ts + self.report_dt class XEnergyTotal(XSensor): _attr_device_class = SensorDeviceClass.ENERGY _attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR _attr_state_class = SensorStateClass.TOTAL class XTemperatureNS(XSensor): params = {"temperature", "tempCorrection"} uid = "temperature" def set_state(self, params: dict = None, value: float = None): if params: # cache updated in XClimateNS entity cache = self.device["params"] value = cache["temperature"] + cache.get("tempCorrection", 0) XSensor.set_state(self, value=value) class XOutdoorTempNS(XSensor): param = "HMI_outdoorTemp" uid = "outdoor_temp" # noinspection PyMethodOverriding def set_state(self, params: dict): try: value = params[self.param] self._attr_native_value = value["current"] mint, maxt = value["range"].split(",") self._attr_extra_state_attributes = { "temp_min": int(mint), "temp_max": int(maxt), } except Exception: pass class XWiFiDoorBattery(XSensor): param = "battery" uid = "battery_voltage" def internal_available(self) -> bool: # device with buggy online status return self.ewelink.cloud.online BUTTON_STATES = ["single", "double", "hold"] class XRemoteButton(XEntity, SensorEntity): _attr_native_value = "" def __init__(self, ewelink: XRegistry, device: dict): XEntity.__init__(self, ewelink, device) self.params = {"key"} def set_state(self, params: dict): button = params.get("outlet") key = BUTTON_STATES[params["key"]] self._attr_native_value = ( f"button_{button + 1}_{key}" if button is not None else key ) asyncio.create_task(self.clear_state()) async def clear_state(self): await asyncio.sleep(0.5) self._attr_native_value = "" self._async_write_ha_state() class XT5Action(XEntity, SensorEntity): uid = "action" _attr_native_value = "" def __init__(self, ewelink: XRegistry, device: dict): XEntity.__init__(self, ewelink, device) self.params = {"triggerType", "slide"} def set_state(self, params: dict): if params.get("triggerType") == 2: self._attr_native_value = "touch" asyncio.create_task(self.clear_state()) # fix https://github.com/AlexxIT/SonoffLAN/issues/1252 if (slide := params.get("slide")) and len(params) == 1: self._attr_native_value = f"slide_{slide}" asyncio.create_task(self.clear_state()) async def clear_state(self): await asyncio.sleep(0.5) self._attr_native_value = "" self._async_write_ha_state() class XUnknown(XEntity, SensorEntity): _attr_device_class = SensorDeviceClass.TIMESTAMP def internal_update(self, params: dict = None): self._attr_native_value = dt.utcnow() if params is not None: params.pop("bindInfos", None) self._attr_extra_state_attributes = params if self.hass: self._async_write_ha_state()