import asyncio from asyncio import Task from datetime import datetime, timedelta, timezone from homeassistant.components.sensor import SensorEntity, SensorStateClass from homeassistant.const import * from homeassistant.core import callback from homeassistant.helpers.restore_state import RestoreEntity from . import DOMAIN from .core.converters import Converter, STAT_GLOBALS from .core.device import XDevice from .core.entity import XEntity from .core.gateway import XGateway SCAN_INTERVAL = timedelta(seconds=60) async def async_setup_entry(hass, entry, add_entities): def setup(gateway: XGateway, device: XDevice, conv: Converter): if conv.attr in device.entities: entity: XEntity = device.entities[conv.attr] entity.gw = gateway elif conv.attr == "action": entity = XiaomiAction(gateway, device, conv) elif conv.attr in STAT_GLOBALS: entity = XiaomiStats(gateway, device, conv) else: entity = XiaomiSensor(gateway, device, conv) add_entities([entity]) gw: XGateway = hass.data[DOMAIN][entry.entry_id] gw.add_setup(__name__, setup) UNITS = { "battery": PERCENTAGE, "humidity": PERCENTAGE, # zb light and motion and ble flower - lux "illuminance": LIGHT_LUX, "power": POWER_WATT, "voltage": ELECTRIC_POTENTIAL_VOLT, "current": ELECTRIC_CURRENT_AMPERE, "pressure": PRESSURE_HPA, "temperature": TEMP_CELSIUS, "energy": ENERGY_KILO_WATT_HOUR, "chip_temperature": TEMP_CELSIUS, "conductivity": CONDUCTIVITY, "gas_density": "% LEL", "idle_time": TIME_SECONDS, "linkquality": "lqi", "max_power": POWER_WATT, "moisture": PERCENTAGE, "msg_received": "msg", "msg_missed": "msg", "new_resets": "rst", "resets": "rst", "rssi": SIGNAL_STRENGTH_DECIBELS_MILLIWATT, "smoke_density": "% obs/ft", "supply": PERCENTAGE, "tvoc": CONCENTRATION_PARTS_PER_BILLION, # "link_quality": "lqi", # "rssi": "dBm", # "msg_received": "msg", # "msg_missed": "msg", # "unresponsive": "times" } # https://developers.home-assistant.io/docs/core/entity/sensor/#long-term-statistics STATE_CLASSES = { "energy": SensorStateClass.TOTAL_INCREASING, } class XiaomiBaseSensor(XEntity, SensorEntity): def __init__(self, gateway: 'XGateway', device: XDevice, conv: Converter): XEntity.__init__(self, gateway, device, conv) if self.attr in UNITS: # by default all sensors with units is measurement sensors self._attr_state_class = SensorStateClass.MEASUREMENT self._attr_native_unit_of_measurement = UNITS[self.attr] if self.attr in STATE_CLASSES: self._attr_state_class = STATE_CLASSES[self.attr] @callback def async_set_state(self, data: dict): if self.attr in data: self._attr_native_value = data[self.attr] for k, v in data.items(): if k in self.subscribed_attrs and k != self.attr: self._attr_extra_state_attributes[k] = v class XiaomiSensor(XiaomiBaseSensor, RestoreEntity): @callback def async_set_state(self, data: dict): if self.attr in data: self._attr_extra_state_attributes["native_value"] = data[self.attr] XiaomiBaseSensor.async_set_state(self, data) @callback def async_restore_last_state(self, state: str, attrs: dict): """Restore previous state.""" self._attr_native_value = attrs.get("native_value", state) for k, v in attrs.items(): if k in self.subscribed_attrs or k == "native_value": self._attr_extra_state_attributes[k] = v async def async_update(self): await self.device_read(self.subscribed_attrs) class XiaomiStats(XiaomiBaseSensor): @property def available(self): return True @callback def async_update_available(self): super().async_update_available() self._attr_extra_state_attributes["available"] = self.available async def async_added_to_hass(self): await super().async_added_to_hass() data = {"available": self.available} if self.device.decode_ts: data[self.attr] = datetime.fromtimestamp( self.device.decode_ts, timezone.utc ) if self.device.nwk: data["ieee"] = self.device.mac data["nwk"] = self.device.nwk else: data["mac"] = self.device.mac self.async_set_state(data) class XiaomiAction(XiaomiBaseSensor): _attr_native_value = "" native_attrs: dict = None clear_task: Task = None async def clear_state(self): await asyncio.sleep(.3) self._attr_native_value = "" self.async_write_ha_state() async def async_added_to_hass(self): await XEntity.async_added_to_hass(self) self.native_attrs = self._attr_extra_state_attributes async def async_will_remove_from_hass(self): if self.clear_task: self.clear_task.cancel() if self.native_value != "": self._attr_native_value = "" self.async_write_ha_state() await XEntity.async_will_remove_from_hass(self) @callback def async_set_state(self, data: dict): # fix 1.4.7_0115 heartbeat error (has button in heartbeat) if "battery" in data or self.attr not in data or not self.hass: return if self.clear_task: self.clear_task.cancel() self._attr_native_value = data[self.attr] if self.native_attrs: self._attr_extra_state_attributes = {**self.native_attrs, **data} else: self._attr_extra_state_attributes = data # repeat event from Aqara integration self.hass.bus.async_fire("xiaomi_aqara.click", { "entity_id": self.entity_id, "click_type": self.native_value }) self.clear_task = self.hass.loop.create_task(self.clear_state())