"""Support for Huawei LTE sensors.""" from __future__ import annotations from bisect import bisect from collections.abc import Callable, Sequence from dataclasses import dataclass, field from datetime import datetime, timedelta import logging import re from huawei_lte_api.enums.net import NetworkModeEnum from homeassistant.components.sensor import ( DOMAIN as SENSOR_DOMAIN, SensorDeviceClass, SensorEntity, SensorEntityDescription, SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( PERCENTAGE, EntityCategory, UnitOfDataRate, UnitOfFrequency, UnitOfInformation, UnitOfTime, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from . import HuaweiLteBaseEntityWithDevice from .const import ( DOMAIN, KEY_DEVICE_INFORMATION, KEY_DEVICE_SIGNAL, KEY_MONITORING_CHECK_NOTIFICATIONS, KEY_MONITORING_MONTH_STATISTICS, KEY_MONITORING_STATUS, KEY_MONITORING_TRAFFIC_STATISTICS, KEY_NET_CURRENT_PLMN, KEY_NET_NET_MODE, KEY_SMS_SMS_COUNT, SENSOR_KEYS, ) _LOGGER = logging.getLogger(__name__) def format_default(value: StateType) -> tuple[StateType, str | None]: """Format value.""" unit = None if value is not None: # Clean up value and infer unit, e.g. -71dBm, 15 dB if match := re.match( r"([>=<]*)(?P.+?)\s*(?P[a-zA-Z]+)\s*$", str(value) ): try: value = float(match.group("value")) unit = match.group("unit") except ValueError: pass return value, unit def format_freq_mhz(value: StateType) -> tuple[StateType, UnitOfFrequency]: """Format a frequency value for which source is in tenths of MHz.""" return ( float(value) / 10 if value is not None else None, UnitOfFrequency.MEGAHERTZ, ) def format_last_reset_elapsed_seconds(value: str | None) -> datetime | None: """Convert elapsed seconds to last reset datetime.""" if value is None: return None try: last_reset = datetime.now() - timedelta(seconds=int(value)) last_reset.replace(microsecond=0) return last_reset except ValueError: return None def signal_icon(limits: Sequence[int], value: StateType) -> str: """Get signal icon.""" return ( "mdi:signal-cellular-outline", "mdi:signal-cellular-1", "mdi:signal-cellular-2", "mdi:signal-cellular-3", )[bisect(limits, value if value is not None else -1000)] def bandwidth_icon(limits: Sequence[int], value: StateType) -> str: """Get bandwidth icon.""" return ( "mdi:speedometer-slow", "mdi:speedometer-medium", "mdi:speedometer", )[bisect(limits, value if value is not None else -1000)] @dataclass class HuaweiSensorGroup: """Class describing Huawei LTE sensor groups.""" descriptions: dict[str, HuaweiSensorEntityDescription] include: re.Pattern[str] | None = None exclude: re.Pattern[str] | None = None @dataclass class HuaweiSensorEntityDescription(SensorEntityDescription): """Class describing Huawei LTE sensor entities.""" # HuaweiLteSensor does not support UNDEFINED or None, # restrict the type to str. name: str = "" format_fn: Callable[[str], tuple[StateType, str | None]] = format_default icon_fn: Callable[[StateType], str] | None = None device_class_fn: Callable[[StateType], SensorDeviceClass | None] | None = None last_reset_item: str | None = None last_reset_format_fn: Callable[[str | None], datetime | None] | None = None SENSOR_META: dict[str, HuaweiSensorGroup] = { # # Device information # KEY_DEVICE_INFORMATION: HuaweiSensorGroup( include=re.compile(r"^(WanIP.*Address|uptime)$", re.IGNORECASE), descriptions={ "uptime": HuaweiSensorEntityDescription( key="uptime", name="Uptime", icon="mdi:timer-outline", native_unit_of_measurement=UnitOfTime.SECONDS, device_class=SensorDeviceClass.DURATION, entity_category=EntityCategory.DIAGNOSTIC, ), "WanIPAddress": HuaweiSensorEntityDescription( key="WanIPAddress", name="WAN IP address", icon="mdi:ip", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=True, ), "WanIPv6Address": HuaweiSensorEntityDescription( key="WanIPv6Address", name="WAN IPv6 address", icon="mdi:ip", entity_category=EntityCategory.DIAGNOSTIC, ), }, ), # # Signal # KEY_DEVICE_SIGNAL: HuaweiSensorGroup( descriptions={ "arfcn": HuaweiSensorEntityDescription( key="arfcn", name="ARFCN", entity_category=EntityCategory.DIAGNOSTIC, ), "band": HuaweiSensorEntityDescription( key="band", name="Band", entity_category=EntityCategory.DIAGNOSTIC, ), "bsic": HuaweiSensorEntityDescription( key="bsic", name="Base station identity code", entity_category=EntityCategory.DIAGNOSTIC, ), "cell_id": HuaweiSensorEntityDescription( key="cell_id", name="Cell ID", icon="mdi:transmission-tower", entity_category=EntityCategory.DIAGNOSTIC, ), "cqi0": HuaweiSensorEntityDescription( key="cqi0", name="CQI 0", icon="mdi:speedometer", entity_category=EntityCategory.DIAGNOSTIC, ), "cqi1": HuaweiSensorEntityDescription( key="cqi1", name="CQI 1", icon="mdi:speedometer", ), "dl_mcs": HuaweiSensorEntityDescription( key="dl_mcs", name="Downlink MCS", entity_category=EntityCategory.DIAGNOSTIC, ), "dlbandwidth": HuaweiSensorEntityDescription( key="dlbandwidth", name="Downlink bandwidth", icon_fn=lambda x: bandwidth_icon((8, 15), x), entity_category=EntityCategory.DIAGNOSTIC, ), "dlfrequency": HuaweiSensorEntityDescription( key="dlfrequency", name="Downlink frequency", device_class=SensorDeviceClass.FREQUENCY, entity_category=EntityCategory.DIAGNOSTIC, ), "earfcn": HuaweiSensorEntityDescription( key="earfcn", name="EARFCN", entity_category=EntityCategory.DIAGNOSTIC, ), "ecio": HuaweiSensorEntityDescription( key="ecio", name="EC/IO", device_class=SensorDeviceClass.SIGNAL_STRENGTH, # https://wiki.teltonika.lt/view/EC/IO icon_fn=lambda x: signal_icon((-20, -10, -6), x), state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, ), "enodeb_id": HuaweiSensorEntityDescription( key="enodeb_id", name="eNodeB ID", entity_category=EntityCategory.DIAGNOSTIC, ), "lac": HuaweiSensorEntityDescription( key="lac", name="LAC", icon="mdi:map-marker", entity_category=EntityCategory.DIAGNOSTIC, ), "ltedlfreq": HuaweiSensorEntityDescription( key="ltedlfreq", name="LTE downlink frequency", format_fn=format_freq_mhz, suggested_display_precision=0, device_class=SensorDeviceClass.FREQUENCY, entity_category=EntityCategory.DIAGNOSTIC, ), "lteulfreq": HuaweiSensorEntityDescription( key="lteulfreq", name="LTE uplink frequency", format_fn=format_freq_mhz, suggested_display_precision=0, device_class=SensorDeviceClass.FREQUENCY, entity_category=EntityCategory.DIAGNOSTIC, ), "mode": HuaweiSensorEntityDescription( key="mode", name="Mode", format_fn=lambda x: ( {"0": "2G", "2": "3G", "7": "4G"}.get(x), None, ), icon_fn=lambda x: ( { "2G": "mdi:signal-2g", "3G": "mdi:signal-3g", "4G": "mdi:signal-4g", }.get(str(x), "mdi:signal") ), entity_category=EntityCategory.DIAGNOSTIC, ), "pci": HuaweiSensorEntityDescription( key="pci", name="PCI", icon="mdi:transmission-tower", entity_category=EntityCategory.DIAGNOSTIC, ), "plmn": HuaweiSensorEntityDescription( key="plmn", name="PLMN", entity_category=EntityCategory.DIAGNOSTIC, ), "rac": HuaweiSensorEntityDescription( key="rac", name="RAC", icon="mdi:map-marker", entity_category=EntityCategory.DIAGNOSTIC, ), "rrc_status": HuaweiSensorEntityDescription( key="rrc_status", name="RRC status", entity_category=EntityCategory.DIAGNOSTIC, ), "rscp": HuaweiSensorEntityDescription( key="rscp", name="RSCP", device_class=SensorDeviceClass.SIGNAL_STRENGTH, # https://wiki.teltonika.lt/view/RSCP icon_fn=lambda x: signal_icon((-95, -85, -75), x), state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, ), "rsrp": HuaweiSensorEntityDescription( key="rsrp", name="RSRP", device_class=SensorDeviceClass.SIGNAL_STRENGTH, # http://www.lte-anbieter.info/technik/rsrp.php icon_fn=lambda x: signal_icon((-110, -95, -80), x), state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=True, ), "rsrq": HuaweiSensorEntityDescription( key="rsrq", name="RSRQ", device_class=SensorDeviceClass.SIGNAL_STRENGTH, # http://www.lte-anbieter.info/technik/rsrq.php icon_fn=lambda x: signal_icon((-11, -8, -5), x), state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=True, ), "rssi": HuaweiSensorEntityDescription( key="rssi", name="RSSI", device_class=SensorDeviceClass.SIGNAL_STRENGTH, # https://eyesaas.com/wi-fi-signal-strength/ icon_fn=lambda x: signal_icon((-80, -70, -60), x), state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=True, ), "sinr": HuaweiSensorEntityDescription( key="sinr", name="SINR", device_class=SensorDeviceClass.SIGNAL_STRENGTH, # http://www.lte-anbieter.info/technik/sinr.php icon_fn=lambda x: signal_icon((0, 5, 10), x), state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=True, ), "tac": HuaweiSensorEntityDescription( key="tac", name="TAC", icon="mdi:map-marker", entity_category=EntityCategory.DIAGNOSTIC, ), "tdd": HuaweiSensorEntityDescription( key="tdd", name="TDD", entity_category=EntityCategory.DIAGNOSTIC, ), "transmode": HuaweiSensorEntityDescription( key="transmode", name="Transmission mode", entity_category=EntityCategory.DIAGNOSTIC, ), "txpower": HuaweiSensorEntityDescription( key="txpower", name="Transmit power", # The value we get from the API tends to consist of several, e.g. # PPusch:15dBm PPucch:2dBm PSrs:42dBm PPrach:1dBm # Present as SIGNAL_STRENGTH only if it was parsed to a number. # We could try to parse this to separate component sensors sometime. device_class_fn=lambda x: ( SensorDeviceClass.SIGNAL_STRENGTH if isinstance(x, (float, int)) else None ), entity_category=EntityCategory.DIAGNOSTIC, ), "ul_mcs": HuaweiSensorEntityDescription( key="ul_mcs", name="Uplink MCS", entity_category=EntityCategory.DIAGNOSTIC, ), "ulbandwidth": HuaweiSensorEntityDescription( key="ulbandwidth", name="Uplink bandwidth", icon_fn=lambda x: bandwidth_icon((8, 15), x), entity_category=EntityCategory.DIAGNOSTIC, ), "ulfrequency": HuaweiSensorEntityDescription( key="ulfrequency", name="Uplink frequency", device_class=SensorDeviceClass.FREQUENCY, entity_category=EntityCategory.DIAGNOSTIC, ), } ), # # Monitoring # KEY_MONITORING_CHECK_NOTIFICATIONS: HuaweiSensorGroup( exclude=re.compile( r"^(onlineupdatestatus|smsstoragefull)$", re.IGNORECASE, ), descriptions={ "UnreadMessage": HuaweiSensorEntityDescription( key="UnreadMessage", name="SMS unread", icon="mdi:email-arrow-left" ), }, ), KEY_MONITORING_MONTH_STATISTICS: HuaweiSensorGroup( exclude=re.compile( r"^(currentday|month)(duration|lastcleartime)$", re.IGNORECASE ), descriptions={ "CurrentDayUsed": HuaweiSensorEntityDescription( key="CurrentDayUsed", name="Current day transfer", native_unit_of_measurement=UnitOfInformation.BYTES, device_class=SensorDeviceClass.DATA_SIZE, icon="mdi:arrow-up-down-bold", state_class=SensorStateClass.TOTAL, last_reset_item="CurrentDayDuration", last_reset_format_fn=format_last_reset_elapsed_seconds, ), "CurrentMonthDownload": HuaweiSensorEntityDescription( key="CurrentMonthDownload", name="Current month download", native_unit_of_measurement=UnitOfInformation.BYTES, device_class=SensorDeviceClass.DATA_SIZE, icon="mdi:download", state_class=SensorStateClass.TOTAL, last_reset_item="MonthDuration", last_reset_format_fn=format_last_reset_elapsed_seconds, ), "CurrentMonthUpload": HuaweiSensorEntityDescription( key="CurrentMonthUpload", name="Current month upload", native_unit_of_measurement=UnitOfInformation.BYTES, device_class=SensorDeviceClass.DATA_SIZE, icon="mdi:upload", state_class=SensorStateClass.TOTAL, last_reset_item="MonthDuration", last_reset_format_fn=format_last_reset_elapsed_seconds, ), }, ), KEY_MONITORING_STATUS: HuaweiSensorGroup( include=re.compile( r"^(batterypercent|currentwifiuser|(primary|secondary).*dns)$", re.IGNORECASE, ), descriptions={ "BatteryPercent": HuaweiSensorEntityDescription( key="BatteryPercent", name="Battery", device_class=SensorDeviceClass.BATTERY, native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, ), "CurrentWifiUser": HuaweiSensorEntityDescription( key="CurrentWifiUser", name="WiFi clients connected", icon="mdi:wifi", state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, ), "PrimaryDns": HuaweiSensorEntityDescription( key="PrimaryDns", name="Primary DNS server", icon="mdi:ip", entity_category=EntityCategory.DIAGNOSTIC, ), "PrimaryIPv6Dns": HuaweiSensorEntityDescription( key="PrimaryIPv6Dns", name="Primary IPv6 DNS server", icon="mdi:ip", entity_category=EntityCategory.DIAGNOSTIC, ), "SecondaryDns": HuaweiSensorEntityDescription( key="SecondaryDns", name="Secondary DNS server", icon="mdi:ip", entity_category=EntityCategory.DIAGNOSTIC, ), "SecondaryIPv6Dns": HuaweiSensorEntityDescription( key="SecondaryIPv6Dns", name="Secondary IPv6 DNS server", icon="mdi:ip", entity_category=EntityCategory.DIAGNOSTIC, ), }, ), KEY_MONITORING_TRAFFIC_STATISTICS: HuaweiSensorGroup( exclude=re.compile(r"^showtraffic$", re.IGNORECASE), descriptions={ "CurrentConnectTime": HuaweiSensorEntityDescription( key="CurrentConnectTime", name="Current connection duration", native_unit_of_measurement=UnitOfTime.SECONDS, device_class=SensorDeviceClass.DURATION, icon="mdi:timer-outline", ), "CurrentDownload": HuaweiSensorEntityDescription( key="CurrentDownload", name="Current connection download", native_unit_of_measurement=UnitOfInformation.BYTES, device_class=SensorDeviceClass.DATA_SIZE, icon="mdi:download", state_class=SensorStateClass.TOTAL_INCREASING, ), "CurrentDownloadRate": HuaweiSensorEntityDescription( key="CurrentDownloadRate", name="Current download rate", native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND, device_class=SensorDeviceClass.DATA_RATE, icon="mdi:download", state_class=SensorStateClass.MEASUREMENT, ), "CurrentUpload": HuaweiSensorEntityDescription( key="CurrentUpload", name="Current connection upload", native_unit_of_measurement=UnitOfInformation.BYTES, device_class=SensorDeviceClass.DATA_SIZE, icon="mdi:upload", state_class=SensorStateClass.TOTAL_INCREASING, ), "CurrentUploadRate": HuaweiSensorEntityDescription( key="CurrentUploadRate", name="Current upload rate", native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND, device_class=SensorDeviceClass.DATA_RATE, icon="mdi:upload", state_class=SensorStateClass.MEASUREMENT, ), "TotalConnectTime": HuaweiSensorEntityDescription( key="TotalConnectTime", name="Total connected duration", native_unit_of_measurement=UnitOfTime.SECONDS, device_class=SensorDeviceClass.DURATION, icon="mdi:timer-outline", state_class=SensorStateClass.TOTAL_INCREASING, ), "TotalDownload": HuaweiSensorEntityDescription( key="TotalDownload", name="Total download", native_unit_of_measurement=UnitOfInformation.BYTES, device_class=SensorDeviceClass.DATA_SIZE, icon="mdi:download", state_class=SensorStateClass.TOTAL_INCREASING, ), "TotalUpload": HuaweiSensorEntityDescription( key="TotalUpload", name="Total upload", native_unit_of_measurement=UnitOfInformation.BYTES, device_class=SensorDeviceClass.DATA_SIZE, icon="mdi:upload", state_class=SensorStateClass.TOTAL_INCREASING, ), }, ), # # Network # KEY_NET_CURRENT_PLMN: HuaweiSensorGroup( exclude=re.compile(r"^(Rat|ShortName|Spn)$", re.IGNORECASE), descriptions={ "FullName": HuaweiSensorEntityDescription( key="FullName", name="Operator name", entity_category=EntityCategory.DIAGNOSTIC, ), "Numeric": HuaweiSensorEntityDescription( key="Numeric", name="Operator code", entity_category=EntityCategory.DIAGNOSTIC, ), "State": HuaweiSensorEntityDescription( key="State", name="Operator search mode", format_fn=lambda x: ( {"0": "Auto", "1": "Manual"}.get(x), None, ), entity_category=EntityCategory.DIAGNOSTIC, ), }, ), KEY_NET_NET_MODE: HuaweiSensorGroup( include=re.compile(r"^NetworkMode$", re.IGNORECASE), descriptions={ "NetworkMode": HuaweiSensorEntityDescription( key="NetworkMode", name="Preferred mode", format_fn=lambda x: ( { NetworkModeEnum.MODE_AUTO.value: "4G/3G/2G", NetworkModeEnum.MODE_4G_3G_AUTO.value: "4G/3G", NetworkModeEnum.MODE_4G_2G_AUTO.value: "4G/2G", NetworkModeEnum.MODE_4G_ONLY.value: "4G", NetworkModeEnum.MODE_3G_2G_AUTO.value: "3G/2G", NetworkModeEnum.MODE_3G_ONLY.value: "3G", NetworkModeEnum.MODE_2G_ONLY.value: "2G", }.get(x), None, ), entity_category=EntityCategory.DIAGNOSTIC, ), }, ), # # SMS # KEY_SMS_SMS_COUNT: HuaweiSensorGroup( descriptions={ "LocalDeleted": HuaweiSensorEntityDescription( key="LocalDeleted", name="SMS deleted (device)", icon="mdi:email-minus", ), "LocalDraft": HuaweiSensorEntityDescription( key="LocalDraft", name="SMS drafts (device)", icon="mdi:email-arrow-right-outline", ), "LocalInbox": HuaweiSensorEntityDescription( key="LocalInbox", name="SMS inbox (device)", icon="mdi:email", ), "LocalMax": HuaweiSensorEntityDescription( key="LocalMax", name="SMS capacity (device)", icon="mdi:email", ), "LocalOutbox": HuaweiSensorEntityDescription( key="LocalOutbox", name="SMS outbox (device)", icon="mdi:email-arrow-right", ), "LocalUnread": HuaweiSensorEntityDescription( key="LocalUnread", name="SMS unread (device)", icon="mdi:email-arrow-left", ), "SimDraft": HuaweiSensorEntityDescription( key="SimDraft", name="SMS drafts (SIM)", icon="mdi:email-arrow-right-outline", ), "SimInbox": HuaweiSensorEntityDescription( key="SimInbox", name="SMS inbox (SIM)", icon="mdi:email", ), "SimMax": HuaweiSensorEntityDescription( key="SimMax", name="SMS capacity (SIM)", icon="mdi:email", ), "SimOutbox": HuaweiSensorEntityDescription( key="SimOutbox", name="SMS outbox (SIM)", icon="mdi:email-arrow-right", ), "SimUnread": HuaweiSensorEntityDescription( key="SimUnread", name="SMS unread (SIM)", icon="mdi:email-arrow-left", ), "SimUsed": HuaweiSensorEntityDescription( key="SimUsed", name="SMS messages (SIM)", icon="mdi:email-arrow-left", ), }, ), } async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up from config entry.""" router = hass.data[DOMAIN].routers[config_entry.entry_id] sensors: list[Entity] = [] for key in SENSOR_KEYS: if not (items := router.data.get(key)): continue if key_meta := SENSOR_META.get(key): if key_meta.include: items = filter(key_meta.include.search, items) if key_meta.exclude: items = [x for x in items if not key_meta.exclude.search(x)] for item in items: sensors.append( HuaweiLteSensor( router, key, item, SENSOR_META[key].descriptions.get( item, HuaweiSensorEntityDescription(key=item) ), ) ) async_add_entities(sensors, True) @dataclass class HuaweiLteSensor(HuaweiLteBaseEntityWithDevice, SensorEntity): """Huawei LTE sensor entity.""" key: str item: str entity_description: HuaweiSensorEntityDescription _state: StateType = field(default=None, init=False) _unit: str | None = field(default=None, init=False) _last_reset: datetime | None = field(default=None, init=False) def __post_init__(self) -> None: """Initialize remaining attributes.""" self._attr_name = self.entity_description.name or self.item async def async_added_to_hass(self) -> None: """Subscribe to needed data on add.""" await super().async_added_to_hass() self.router.subscriptions[self.key].append(f"{SENSOR_DOMAIN}/{self.item}") if self.entity_description.last_reset_item: self.router.subscriptions[self.key].append( f"{SENSOR_DOMAIN}/{self.entity_description.last_reset_item}" ) async def async_will_remove_from_hass(self) -> None: """Unsubscribe from needed data on remove.""" await super().async_will_remove_from_hass() self.router.subscriptions[self.key].remove(f"{SENSOR_DOMAIN}/{self.item}") if self.entity_description.last_reset_item: self.router.subscriptions[self.key].remove( f"{SENSOR_DOMAIN}/{self.entity_description.last_reset_item}" ) @property def _device_unique_id(self) -> str: return f"{self.key}.{self.item}" @property def native_value(self) -> StateType: """Return sensor state.""" return self._state @property def native_unit_of_measurement(self) -> str | None: """Return sensor's unit of measurement.""" return self.entity_description.native_unit_of_measurement or self._unit @property def icon(self) -> str | None: """Return icon for sensor.""" if self.entity_description.icon_fn: return self.entity_description.icon_fn(self.state) return self.entity_description.icon @property def device_class(self) -> SensorDeviceClass | None: """Return device class for sensor.""" if self.entity_description.device_class_fn: # Note: using self.state could infloop here. return self.entity_description.device_class_fn(self.native_value) return super().device_class @property def last_reset(self) -> datetime | None: """Return the time when the sensor was last reset, if any.""" return self._last_reset async def async_update(self) -> None: """Update state.""" try: value = self.router.data[self.key][self.item] except KeyError: _LOGGER.debug("%s[%s] not in data", self.key, self.item) value = None last_reset = None if ( self.entity_description.last_reset_item and self.entity_description.last_reset_format_fn ): try: last_reset_value = self.router.data[self.key][ self.entity_description.last_reset_item ] except KeyError: _LOGGER.debug( "%s[%s] not in data", self.key, self.entity_description.last_reset_item, ) else: last_reset = self.entity_description.last_reset_format_fn( last_reset_value ) self._state, self._unit = self.entity_description.format_fn(value) self._last_reset = last_reset self._available = value is not None