From a00071270a1018e9e970bb23f1ef9180e83fce9b Mon Sep 17 00:00:00 2001 From: Jason Cheatham Date: Mon, 20 Jun 2022 11:41:47 -0400 Subject: [PATCH] Create entities for unused device attributes Many hubitat devices may have "unused" attributes -- attributes not handled by any existing entity class. Create disabled generic sensor entities for these unused attributes. Users may enable these entites and customize them using HA's customization functionality. --- .../hubitat/alarm_control_panel.py | 18 ++++- custom_components/hubitat/binary_sensor.py | 7 +- custom_components/hubitat/climate.py | 20 ++++- custom_components/hubitat/cover.py | 11 ++- custom_components/hubitat/device.py | 9 ++- custom_components/hubitat/fan.py | 12 ++- custom_components/hubitat/hub.py | 32 ++++---- custom_components/hubitat/light.py | 17 ++++- custom_components/hubitat/lock.py | 15 +++- custom_components/hubitat/select.py | 7 +- custom_components/hubitat/sensor.py | 73 ++++++++++++++++++- custom_components/hubitat/switch.py | 7 +- custom_components/hubitat/types.py | 12 ++- 13 files changed, 208 insertions(+), 32 deletions(-) diff --git a/custom_components/hubitat/alarm_control_panel.py b/custom_components/hubitat/alarm_control_panel.py index 6763e45..014ea17 100644 --- a/custom_components/hubitat/alarm_control_panel.py +++ b/custom_components/hubitat/alarm_control_panel.py @@ -28,7 +28,7 @@ ) from hubitatmaker.types import Device from logging import getLogger -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Sequence from homeassistant.components.alarm_control_panel import ( SUPPORT_ALARM_ARM_AWAY, @@ -60,10 +60,26 @@ _LOGGER = getLogger(__name__) +_device_attrs = ( + HE_ATTR_ALARM, + HE_ATTR_CODE_CHANGED, + HE_ATTR_CODE_LENGTH, + HE_ATTR_ENTRY_DELAY, + HE_ATTR_EXIT_DELAY, + HE_ATTR_LOCK_CODES, + HE_ATTR_MAX_CODES, + HE_ATTR_SECURITY_KEYPAD, +) + class HubitatSecurityKeypad(HubitatEntity, AlarmControlPanelEntity): """Representation of a Hubitat security keypad.""" + @property + def device_attrs(self) -> Optional[Sequence[str]]: + """Return this entity's associated attributes""" + return _device_attrs + @property def alarm(self) -> Optional[str]: """Alarm status.""" diff --git a/custom_components/hubitat/binary_sensor.py b/custom_components/hubitat/binary_sensor.py index 22a6c7b..2ceaeda 100644 --- a/custom_components/hubitat/binary_sensor.py +++ b/custom_components/hubitat/binary_sensor.py @@ -11,7 +11,7 @@ Device, ) import re -from typing import Dict, List, Optional, Tuple, Type +from typing import Dict, List, Optional, Sequence, Tuple, Type from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, @@ -48,6 +48,11 @@ class HubitatBinarySensor(HubitatEntity, BinarySensorEntity): _attribute: str _device_class: str + @property + def device_attrs(self) -> Optional[Sequence[str]]: + """Return this entity's associated attributes""" + return (self._attribute,) + @property def is_on(self) -> bool: """Return True if this sensor is on/active.""" diff --git a/custom_components/hubitat/climate.py b/custom_components/hubitat/climate.py index 7f6cf54..900f33d 100644 --- a/custom_components/hubitat/climate.py +++ b/custom_components/hubitat/climate.py @@ -15,7 +15,7 @@ CMD_SET_HEATING_SETPOINT, Device, ) -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Sequence from custom_components.hubitat.const import TEMP_C, TEMP_F @@ -110,10 +110,28 @@ FAN_MODE_CIRCULATE = "circulate" HASS_FAN_MODES = [FAN_ON, FAN_AUTO] +_device_attrs = ( + ATTR_COOLING_SETPOINT, + ATTR_FAN_MODE, + ATTR_HEATING_SETPOINT, + ATTR_HUMIDITY, + ATTR_MODE, + ATTR_NEST_MODE, + ATTR_OPERATING_STATE, + ATTR_PRESENCE, + ATTR_TEMP, + ATTR_TEMP_UNIT, +) + class HubitatThermostat(HubitatEntity, ClimateEntity): """Representation of a Hubitat switch.""" + @property + def device_attrs(self) -> Optional[Sequence[str]]: + """Return this entity's associated attributes""" + return _device_attrs + @property def current_humidity(self) -> Optional[int]: """Return the current humidity.""" diff --git a/custom_components/hubitat/cover.py b/custom_components/hubitat/cover.py index 558298a..103b4d6 100644 --- a/custom_components/hubitat/cover.py +++ b/custom_components/hubitat/cover.py @@ -17,7 +17,7 @@ Device, ) from logging import getLogger -from typing import Any, Dict, List, Optional, Tuple, Type +from typing import Any, Dict, List, Optional, Sequence, Tuple, Type from homeassistant.components.cover import ( ATTR_POSITION as HA_ATTR_POSITION, @@ -46,6 +46,15 @@ class HubitatCover(HubitatEntity, CoverEntity): _features: int _device_class: Optional[str] + @property + def device_attrs(self) -> Optional[Sequence[str]]: + """Return this entity's associated attributes""" + return ( + self._attribute, + ATTR_LEVEL, + ATTR_POSITION, + ) + @property def device_class(self) -> Optional[str]: """Return this sensor's device class.""" diff --git a/custom_components/hubitat/device.py b/custom_components/hubitat/device.py index 8f3de6e..b1724da 100644 --- a/custom_components/hubitat/device.py +++ b/custom_components/hubitat/device.py @@ -5,14 +5,13 @@ from logging import getLogger from typing import Any, Dict, List, Optional, Union, cast -from custom_components.hubitat.hub import Hub -from custom_components.hubitat.types import Removable, UpdateableEntity - from homeassistant.core import callback from homeassistant.helpers import device_registry from homeassistant.helpers.entity import DeviceInfo from .const import DOMAIN +from .hub import Hub +from .types import Removable, UpdateableEntity from .util import get_hub_device_id _LOGGER = getLogger(__name__) @@ -138,6 +137,10 @@ def __init__(self, hub: Hub, device: Device, temp: Optional[bool] = False) -> No if not temp: self._hub.add_device_listener(self._device.id, self.handle_event) + @property + def device_attrs(self) -> Optional[str]: + return None + @property def should_poll(self) -> bool: # Hubitat will push device updates diff --git a/custom_components/hubitat/fan.py b/custom_components/hubitat/fan.py index a923b85..20ce329 100644 --- a/custom_components/hubitat/fan.py +++ b/custom_components/hubitat/fan.py @@ -15,7 +15,7 @@ Device, ) from logging import getLogger -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Sequence from homeassistant.components.fan import SUPPORT_SET_SPEED, FanEntity from homeassistant.config_entries import ConfigEntry @@ -27,10 +27,20 @@ _LOGGER = getLogger(__name__) +_device_attrs = ( + ATTR_SWITCH, + ATTR_SPEED, +) + class HubitatFan(HubitatEntity, FanEntity): """Representation of a Hubitat fan.""" + @property + def device_attrs(self) -> Optional[Sequence[str]]: + """Return this entity's associated attributes""" + return _device_attrs + @property def is_on(self) -> bool: if CAP_SWITCH in self._device.capabilities: diff --git a/custom_components/hubitat/hub.py b/custom_components/hubitat/hub.py index 0d5aad1..0cef42f 100644 --- a/custom_components/hubitat/hub.py +++ b/custom_components/hubitat/hub.py @@ -5,7 +5,20 @@ from ssl import SSLContext from typing import Callable, Mapping, Optional, Sequence, Union, cast -from custom_components.hubitat.const import ( +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_HIDDEN, + CONF_ACCESS_TOKEN, + CONF_HOST, + CONF_ID, + CONF_TEMPERATURE_UNIT, + DEVICE_CLASS_TEMPERATURE, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry +from homeassistant.helpers.device_registry import DeviceEntry + +from .const import ( ATTR_ATTRIBUTE, ATTR_HA_DEVICE_ID, ATTR_HUB, @@ -20,21 +33,8 @@ TEMP_F, TRIGGER_CAPABILITIES, ) -from custom_components.hubitat.types import Removable, UpdateableEntity -from custom_components.hubitat.util import get_hub_device_id, get_hub_short_id - -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_HIDDEN, - CONF_ACCESS_TOKEN, - CONF_HOST, - CONF_ID, - CONF_TEMPERATURE_UNIT, - DEVICE_CLASS_TEMPERATURE, -) -from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry -from homeassistant.helpers.device_registry import DeviceEntry +from .types import Removable, UpdateableEntity +from .util import get_hub_device_id, get_hub_short_id _LOGGER = getLogger(__name__) diff --git a/custom_components/hubitat/light.py b/custom_components/hubitat/light.py index 9c35811..4e5ea05 100644 --- a/custom_components/hubitat/light.py +++ b/custom_components/hubitat/light.py @@ -25,7 +25,7 @@ import json from logging import getLogger import re -from typing import Any, Dict, List, Optional, Tuple, Union +from typing import Any, Dict, List, Optional, Sequence, Tuple, Union from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -64,10 +64,25 @@ _LOGGER = getLogger(__name__) +_device_attrs = ( + HE_ATTR_COLOR_MODE, + HE_ATTR_COLOR_NAME, + HE_ATTR_COLOR_TEMP, + HE_ATTR_HUE, + HE_ATTR_LEVEL, + HE_ATTR_SATURATION, + HE_ATTR_SWITCH, +) + class HubitatLight(HubitatEntity, LightEntity): """Representation of a Hubitat light.""" + @property + def device_attrs(self) -> Optional[Sequence[str]]: + """Return this entity's associated attributes""" + return _device_attrs + @property def color_mode(self) -> Optional[str]: """Return this light's color mode.""" diff --git a/custom_components/hubitat/lock.py b/custom_components/hubitat/lock.py index ca47f94..b1e2033 100644 --- a/custom_components/hubitat/lock.py +++ b/custom_components/hubitat/lock.py @@ -13,7 +13,7 @@ STATE_LOCKED, Device, ) -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Optional, Sequence, Union from homeassistant.components.lock import LockEntity from homeassistant.config_entries import ConfigEntry @@ -24,10 +24,23 @@ from .entities import create_and_add_entities from .types import EntityAdder +_device_attrs = ( + HM_ATTR_CODE_LENGTH, + HM_ATTR_LAST_CODE_NAME, + HM_ATTR_LOCK, + HM_ATTR_LOCK_CODES, + HM_ATTR_MAX_CODES, +) + class HubitatLock(HubitatEntity, LockEntity): """Representation of a Hubitat lock.""" + @property + def device_attrs(self) -> Optional[Sequence[str]]: + """Return this entity's associated attributes""" + return _device_attrs + @property def code_format(self) -> Optional[str]: """Regex for code format or None if no code is required.""" diff --git a/custom_components/hubitat/select.py b/custom_components/hubitat/select.py index 2ada9e0..b1be76c 100644 --- a/custom_components/hubitat/select.py +++ b/custom_components/hubitat/select.py @@ -1,4 +1,4 @@ -from typing import Any, List, Optional, Union +from typing import Any, List, Optional, Sequence, Union from homeassistant.components.select import SelectEntity from homeassistant.config_entries import ConfigEntry @@ -15,6 +15,11 @@ class HubitatSelect(HubitatEntity, SelectEntity): _options: List[str] _device_class: str + @property + def device_attrs(self) -> Optional[Sequence[str]]: + """Return this entity's associated attributes""" + return (self._attribute,) + @property def device_class(self) -> Optional[str]: """Return this select's device class.""" diff --git a/custom_components/hubitat/sensor.py b/custom_components/hubitat/sensor.py index 62e2bcc..6b5f228 100644 --- a/custom_components/hubitat/sensor.py +++ b/custom_components/hubitat/sensor.py @@ -15,7 +15,7 @@ ) from hubitatmaker.types import Device from logging import getLogger -from typing import Any, Dict, List, Optional, Tuple, Type, Union +from typing import Any, Dict, List, Optional, Sequence, Tuple, Type, Union from homeassistant.components.sensor import ( DEVICE_CLASS_BATTERY, @@ -57,9 +57,39 @@ class HubitatSensor(HubitatEntity): """A generic Hubitat sensor.""" _attribute: str - _attribute_name: Optional[str] + _attribute_name: Optional[str] = None _units: str - _device_class: Optional[str] + _device_class: Optional[str] = None + _enabled_default: Optional[bool] = None + + def __init__( + self, + *args: Any, + attribute: Optional[str] = None, + attribute_name: Optional[str] = None, + units: Optional[str] = None, + device_class: Optional[str] = None, + enabled_default: Optional[bool] = None, + **kwargs: Any, + ): + """Initialize a battery sensor.""" + super().__init__(*args, **kwargs) + + if attribute is not None: + self._attribute = attribute + if attribute_name is not None: + self._attribute_name = attribute_name + if units is not None: + self._units = units + if device_class is not None: + self._device_class = device_class + if enabled_default is not None: + self._enabled_default = enabled_default + + @property + def device_attrs(self) -> Optional[Sequence[str]]: + """Return this entity's associated attributes""" + return (self._attribute,) @property def device_class(self) -> Optional[str]: @@ -100,6 +130,13 @@ def unit_of_measurement(self) -> Optional[str]: except AttributeError: return None + @property + def entity_registry_enabled_default(self) -> bool: + """Update sensors are disabled by default.""" + if self._enabled_default is not None: + return self._enabled_default + return True + class HubitatBatterySensor(HubitatSensor): """A battery sensor.""" @@ -333,6 +370,36 @@ def is_sensor( hass, entry, async_add_entities, "sensor", attr[1], is_sensor ) + # Create sensor entities for any attributes that don't correspond to known + # sensor types + unknown_entities: List[HubitatEntity] = [] + hub = get_hub(hass, entry.entry_id) + + for id in hub.devices: + device = hub.devices[id] + device_entities = [e for e in hub.entities if e.device_id == id] + used_device_attrs: set[str] = set() + for entity in device_entities: + if entity.device_attrs is not None: + for attr in entity.device_attrs: + used_device_attrs.add(attr) + for attr in device.attributes: + if attr not in used_device_attrs: + unknown_entities.append( + HubitatSensor( + hub=hub, + device=device, + attribute=attr, + enabled_default=False, + device_class="unknown", + ) + ) + _LOGGER.debug(f"Adding unknown entity for {device.id}:{attr}") + + if len(unknown_entities) > 0: + hub.add_entities(unknown_entities) + async_add_entities(unknown_entities) + def add_hub_entities( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: EntityAdder diff --git a/custom_components/hubitat/switch.py b/custom_components/hubitat/switch.py index 31f20a8..ab6f43a 100644 --- a/custom_components/hubitat/switch.py +++ b/custom_components/hubitat/switch.py @@ -15,7 +15,7 @@ ) from logging import getLogger import re -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Sequence import voluptuous as vol from homeassistant.components.switch import ( @@ -47,6 +47,11 @@ class HubitatSwitch(HubitatEntity, SwitchEntity): _attribute: str + @property + def device_attrs(self) -> Optional[Sequence[str]]: + """Return this entity's associated attributes""" + return ("switch", "power") + @property def is_on(self) -> bool: """Return True if the switch is on.""" diff --git a/custom_components/hubitat/types.py b/custom_components/hubitat/types.py index 5c58b10..d27637d 100644 --- a/custom_components/hubitat/types.py +++ b/custom_components/hubitat/types.py @@ -1,4 +1,4 @@ -from typing import Callable, Iterable, Protocol +from typing import Callable, Iterable, List, Optional, Protocol from homeassistant.helpers.entity import Entity @@ -10,6 +10,16 @@ def update_state(self): """Update the entity state in HA""" raise Exception("Must be implemented in a sublcass") + @property + def device_attrs(self) -> Optional[List[str]]: + """Return the device attributes associated with this entity""" + raise Exception("Must be implemented in a sublcass") + + @property + def device_id(self) -> str: + """Return the Hubitat device ID associated with this entity""" + raise Exception("Must be implemented in a sublcass") + class Removable(Protocol): async def async_will_remove_from_hass(self):