"""Support for Huawei LTE sensors.""" import logging import re from typing import Optional import attr from homeassistant.components.sensor import ( DEVICE_CLASS_SIGNAL_STRENGTH, DOMAIN as SENSOR_DOMAIN, ) from homeassistant.const import ( CONF_URL, DATA_BYTES, DATA_RATE_BYTES_PER_SECOND, STATE_UNKNOWN, TIME_SECONDS, ) from . import HuaweiLteBaseEntity from .const import ( DOMAIN, KEY_DEVICE_INFORMATION, KEY_DEVICE_SIGNAL, 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__) SENSOR_META = { KEY_DEVICE_INFORMATION: dict( include=re.compile(r"^WanIP.*Address$", re.IGNORECASE) ), (KEY_DEVICE_INFORMATION, "WanIPAddress"): dict( name="WAN IP address", icon="mdi:ip", enabled_default=True ), (KEY_DEVICE_INFORMATION, "WanIPv6Address"): dict( name="WAN IPv6 address", icon="mdi:ip" ), (KEY_DEVICE_SIGNAL, "band"): dict(name="Band"), (KEY_DEVICE_SIGNAL, "cell_id"): dict(name="Cell ID"), (KEY_DEVICE_SIGNAL, "dl_mcs"): dict(name="Downlink MCS"), (KEY_DEVICE_SIGNAL, "dlbandwidth"): dict( name="Downlink bandwidth", icon=lambda x: (x is None or x < 8) and "mdi:speedometer-slow" or x < 15 and "mdi:speedometer-medium" or "mdi:speedometer", ), (KEY_DEVICE_SIGNAL, "earfcn"): dict(name="EARFCN"), (KEY_DEVICE_SIGNAL, "lac"): dict(name="LAC", icon="mdi:map-marker"), (KEY_DEVICE_SIGNAL, "plmn"): dict(name="PLMN"), (KEY_DEVICE_SIGNAL, "rac"): dict(name="RAC", icon="mdi:map-marker"), (KEY_DEVICE_SIGNAL, "rrc_status"): dict(name="RRC status"), (KEY_DEVICE_SIGNAL, "tac"): dict(name="TAC", icon="mdi:map-marker"), (KEY_DEVICE_SIGNAL, "tdd"): dict(name="TDD"), (KEY_DEVICE_SIGNAL, "txpower"): dict( name="Transmit power", device_class=DEVICE_CLASS_SIGNAL_STRENGTH, ), (KEY_DEVICE_SIGNAL, "ul_mcs"): dict(name="Uplink MCS"), (KEY_DEVICE_SIGNAL, "ulbandwidth"): dict( name="Uplink bandwidth", icon=lambda x: (x is None or x < 8) and "mdi:speedometer-slow" or x < 15 and "mdi:speedometer-medium" or "mdi:speedometer", ), (KEY_DEVICE_SIGNAL, "mode"): dict( name="Mode", formatter=lambda x: ({"0": "2G", "2": "3G", "7": "4G"}.get(x, "Unknown"), None), ), (KEY_DEVICE_SIGNAL, "pci"): dict(name="PCI"), (KEY_DEVICE_SIGNAL, "rsrq"): dict( name="RSRQ", device_class=DEVICE_CLASS_SIGNAL_STRENGTH, # http://www.lte-anbieter.info/technik/rsrq.php icon=lambda x: (x is None or x < -11) and "mdi:signal-cellular-outline" or x < -8 and "mdi:signal-cellular-1" or x < -5 and "mdi:signal-cellular-2" or "mdi:signal-cellular-3", enabled_default=True, ), (KEY_DEVICE_SIGNAL, "rsrp"): dict( name="RSRP", device_class=DEVICE_CLASS_SIGNAL_STRENGTH, # http://www.lte-anbieter.info/technik/rsrp.php icon=lambda x: (x is None or x < -110) and "mdi:signal-cellular-outline" or x < -95 and "mdi:signal-cellular-1" or x < -80 and "mdi:signal-cellular-2" or "mdi:signal-cellular-3", enabled_default=True, ), (KEY_DEVICE_SIGNAL, "rssi"): dict( name="RSSI", device_class=DEVICE_CLASS_SIGNAL_STRENGTH, # https://eyesaas.com/wi-fi-signal-strength/ icon=lambda x: (x is None or x < -80) and "mdi:signal-cellular-outline" or x < -70 and "mdi:signal-cellular-1" or x < -60 and "mdi:signal-cellular-2" or "mdi:signal-cellular-3", enabled_default=True, ), (KEY_DEVICE_SIGNAL, "sinr"): dict( name="SINR", device_class=DEVICE_CLASS_SIGNAL_STRENGTH, # http://www.lte-anbieter.info/technik/sinr.php icon=lambda x: (x is None or x < 0) and "mdi:signal-cellular-outline" or x < 5 and "mdi:signal-cellular-1" or x < 10 and "mdi:signal-cellular-2" or "mdi:signal-cellular-3", enabled_default=True, ), (KEY_DEVICE_SIGNAL, "rscp"): dict( name="RSCP", device_class=DEVICE_CLASS_SIGNAL_STRENGTH, # https://wiki.teltonika.lt/view/RSCP icon=lambda x: (x is None or x < -95) and "mdi:signal-cellular-outline" or x < -85 and "mdi:signal-cellular-1" or x < -75 and "mdi:signal-cellular-2" or "mdi:signal-cellular-3", ), (KEY_DEVICE_SIGNAL, "ecio"): dict( name="EC/IO", device_class=DEVICE_CLASS_SIGNAL_STRENGTH, # https://wiki.teltonika.lt/view/EC/IO icon=lambda x: (x is None or x < -20) and "mdi:signal-cellular-outline" or x < -10 and "mdi:signal-cellular-1" or x < -6 and "mdi:signal-cellular-2" or "mdi:signal-cellular-3", ), KEY_MONITORING_MONTH_STATISTICS: dict( exclude=re.compile(r"^month(duration|lastcleartime)$", re.IGNORECASE) ), (KEY_MONITORING_MONTH_STATISTICS, "CurrentMonthDownload"): dict( name="Current month download", unit=DATA_BYTES, icon="mdi:download" ), (KEY_MONITORING_MONTH_STATISTICS, "CurrentMonthUpload"): dict( name="Current month upload", unit=DATA_BYTES, icon="mdi:upload" ), KEY_MONITORING_STATUS: dict( include=re.compile( r"^(currentwifiuser|(primary|secondary).*dns)$", re.IGNORECASE ) ), (KEY_MONITORING_STATUS, "CurrentWifiUser"): dict( name="WiFi clients connected", icon="mdi:wifi" ), (KEY_MONITORING_STATUS, "PrimaryDns"): dict( name="Primary DNS server", icon="mdi:ip" ), (KEY_MONITORING_STATUS, "SecondaryDns"): dict( name="Secondary DNS server", icon="mdi:ip" ), (KEY_MONITORING_STATUS, "PrimaryIPv6Dns"): dict( name="Primary IPv6 DNS server", icon="mdi:ip" ), (KEY_MONITORING_STATUS, "SecondaryIPv6Dns"): dict( name="Secondary IPv6 DNS server", icon="mdi:ip" ), KEY_MONITORING_TRAFFIC_STATISTICS: dict( exclude=re.compile(r"^showtraffic$", re.IGNORECASE) ), (KEY_MONITORING_TRAFFIC_STATISTICS, "CurrentConnectTime"): dict( name="Current connection duration", unit=TIME_SECONDS, icon="mdi:timer-outline" ), (KEY_MONITORING_TRAFFIC_STATISTICS, "CurrentDownload"): dict( name="Current connection download", unit=DATA_BYTES, icon="mdi:download" ), (KEY_MONITORING_TRAFFIC_STATISTICS, "CurrentDownloadRate"): dict( name="Current download rate", unit=DATA_RATE_BYTES_PER_SECOND, icon="mdi:download", ), (KEY_MONITORING_TRAFFIC_STATISTICS, "CurrentUpload"): dict( name="Current connection upload", unit=DATA_BYTES, icon="mdi:upload" ), (KEY_MONITORING_TRAFFIC_STATISTICS, "CurrentUploadRate"): dict( name="Current upload rate", unit=DATA_RATE_BYTES_PER_SECOND, icon="mdi:upload", ), (KEY_MONITORING_TRAFFIC_STATISTICS, "TotalConnectTime"): dict( name="Total connected duration", unit=TIME_SECONDS, icon="mdi:timer-outline" ), (KEY_MONITORING_TRAFFIC_STATISTICS, "TotalDownload"): dict( name="Total download", unit=DATA_BYTES, icon="mdi:download" ), (KEY_MONITORING_TRAFFIC_STATISTICS, "TotalUpload"): dict( name="Total upload", unit=DATA_BYTES, icon="mdi:upload" ), KEY_NET_CURRENT_PLMN: dict( exclude=re.compile(r"^(Rat|ShortName|Spn)$", re.IGNORECASE) ), (KEY_NET_CURRENT_PLMN, "State"): dict( name="Operator search mode", formatter=lambda x: ({"0": "Auto", "1": "Manual"}.get(x, "Unknown"), None), ), (KEY_NET_CURRENT_PLMN, "FullName"): dict( name="Operator name", ), (KEY_NET_CURRENT_PLMN, "Numeric"): dict( name="Operator code", ), KEY_NET_NET_MODE: dict(include=re.compile(r"^NetworkMode$", re.IGNORECASE)), (KEY_NET_NET_MODE, "NetworkMode"): dict( name="Preferred mode", formatter=lambda x: ( { "00": "4G/3G/2G", "01": "2G", "02": "3G", "03": "4G", "0301": "4G/2G", "0302": "4G/3G", "0201": "3G/2G", }.get(x, "Unknown"), None, ), ), (KEY_SMS_SMS_COUNT, "LocalDeleted"): dict( name="SMS deleted (device)", icon="mdi:email-minus", ), (KEY_SMS_SMS_COUNT, "LocalDraft"): dict( name="SMS drafts (device)", icon="mdi:email-send-outline", ), (KEY_SMS_SMS_COUNT, "LocalInbox"): dict( name="SMS inbox (device)", icon="mdi:email", ), (KEY_SMS_SMS_COUNT, "LocalMax"): dict( name="SMS capacity (device)", icon="mdi:email", ), (KEY_SMS_SMS_COUNT, "LocalOutbox"): dict( name="SMS outbox (device)", icon="mdi:email-send", ), (KEY_SMS_SMS_COUNT, "LocalUnread"): dict( name="SMS unread (device)", icon="mdi:email-receive", ), (KEY_SMS_SMS_COUNT, "SimDraft"): dict( name="SMS drafts (SIM)", icon="mdi:email-send-outline", ), (KEY_SMS_SMS_COUNT, "SimInbox"): dict( name="SMS inbox (SIM)", icon="mdi:email", ), (KEY_SMS_SMS_COUNT, "SimMax"): dict( name="SMS capacity (SIM)", icon="mdi:email", ), (KEY_SMS_SMS_COUNT, "SimOutbox"): dict( name="SMS outbox (SIM)", icon="mdi:email-send", ), (KEY_SMS_SMS_COUNT, "SimUnread"): dict( name="SMS unread (SIM)", icon="mdi:email-receive", ), (KEY_SMS_SMS_COUNT, "SimUsed"): dict( name="SMS messages (SIM)", icon="mdi:email-receive", ), } async def async_setup_entry(hass, config_entry, async_add_entities): """Set up from config entry.""" router = hass.data[DOMAIN].routers[config_entry.data[CONF_URL]] sensors = [] for key in SENSOR_KEYS: items = router.data.get(key) if not items: continue key_meta = SENSOR_META.get(key) if key_meta: include = key_meta.get("include") if include: items = filter(include.search, items) exclude = key_meta.get("exclude") if exclude: items = [x for x in items if not exclude.search(x)] for item in items: sensors.append( HuaweiLteSensor(router, key, item, SENSOR_META.get((key, item), {})) ) async_add_entities(sensors, True) def format_default(value): """Format value.""" unit = None if value is not None: # Clean up value and infer unit, e.g. -71dBm, 15 dB match = re.match( r"([>=<]*)(?P.+?)\s*(?P[a-zA-Z]+)\s*$", str(value) ) if match: try: value = float(match.group("value")) unit = match.group("unit") except ValueError: pass return value, unit @attr.s class HuaweiLteSensor(HuaweiLteBaseEntity): """Huawei LTE sensor entity.""" key: str = attr.ib() item: str = attr.ib() meta: dict = attr.ib() _state = attr.ib(init=False, default=STATE_UNKNOWN) _unit: str = attr.ib(init=False) async def async_added_to_hass(self): """Subscribe to needed data on add.""" await super().async_added_to_hass() self.router.subscriptions[self.key].add(f"{SENSOR_DOMAIN}/{self.item}") async def async_will_remove_from_hass(self): """Unsubscribe from needed data on remove.""" await super().async_will_remove_from_hass() self.router.subscriptions[self.key].remove(f"{SENSOR_DOMAIN}/{self.item}") @property def _entity_name(self) -> str: return self.meta.get("name", self.item) @property def _device_unique_id(self) -> str: return f"{self.key}.{self.item}" @property def state(self): """Return sensor state.""" return self._state @property def device_class(self) -> Optional[str]: """Return sensor device class.""" return self.meta.get("device_class") @property def unit_of_measurement(self): """Return sensor's unit of measurement.""" return self.meta.get("unit", self._unit) @property def icon(self): """Return icon for sensor.""" icon = self.meta.get("icon") if callable(icon): return icon(self.state) return icon @property def entity_registry_enabled_default(self) -> bool: """Return if the entity should be enabled when first added to the entity registry.""" return bool(self.meta.get("enabled_default")) async def async_update(self): """Update state.""" try: value = self.router.data[self.key][self.item] except KeyError: _LOGGER.debug("%s[%s] not in data", self.key, self.item) self._available = False return self._available = True formatter = self.meta.get("formatter") if not callable(formatter): formatter = format_default self._state, self._unit = formatter(value) async def async_setup_platform(*args, **kwargs): """Old no longer used way to set up Huawei LTE sensors.""" _LOGGER.warning( "Loading and configuring as a platform is no longer supported or " "required, convert to enabling/disabling available entities" )