From 72a0ca4871c7a639e3a6be73792aeb26ebe25dc6 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Wed, 3 Aug 2022 22:03:10 +0100 Subject: [PATCH] Add homekit_controller thread node capabilties diagnostic sensor (#76120) --- .../components/homekit_controller/const.py | 1 + .../components/homekit_controller/sensor.py | 53 ++++++++++++++++++- .../homekit_controller/strings.sensor.json | 12 +++++ .../translations/sensor.en.json | 12 +++++ .../test_nanoleaf_strip_nl55.py | 7 +++ .../homekit_controller/test_sensor.py | 20 +++++++ 6 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/homekit_controller/strings.sensor.json create mode 100644 homeassistant/components/homekit_controller/translations/sensor.en.json diff --git a/homeassistant/components/homekit_controller/const.py b/homeassistant/components/homekit_controller/const.py index c27527d3638..5ea8205260e 100644 --- a/homeassistant/components/homekit_controller/const.py +++ b/homeassistant/components/homekit_controller/const.py @@ -88,6 +88,7 @@ CHARACTERISTIC_PLATFORMS = { CharacteristicsTypes.DENSITY_SO2: "sensor", CharacteristicsTypes.DENSITY_VOC: "sensor", CharacteristicsTypes.IDENTIFY: "button", + CharacteristicsTypes.THREAD_NODE_CAPABILITIES: "sensor", } diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index cddcbc59cde..ecfad477d00 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -5,6 +5,7 @@ from collections.abc import Callable from dataclasses import dataclass from aiohomekit.model.characteristics import Characteristic, CharacteristicsTypes +from aiohomekit.model.characteristics.const import ThreadNodeCapabilities from aiohomekit.model.services import Service, ServicesTypes from homeassistant.components.sensor import ( @@ -27,6 +28,7 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType @@ -40,6 +42,45 @@ class HomeKitSensorEntityDescription(SensorEntityDescription): """Describes Homekit sensor.""" probe: Callable[[Characteristic], bool] | None = None + format: Callable[[Characteristic], str] | None = None + + +def thread_node_capability_to_str(char: Characteristic) -> str: + """ + Return the thread device type as a string. + + The underlying value is a bitmask, but we want to turn that to + a human readable string. Some devices will have multiple capabilities. + For example, an NL55 is SLEEPY | MINIMAL. In that case we return the + "best" capability. + + https://openthread.io/guides/thread-primer/node-roles-and-types + """ + + val = ThreadNodeCapabilities(char.value) + + if val & ThreadNodeCapabilities.BORDER_ROUTER_CAPABLE: + # can act as a bridge between thread network and e.g. WiFi + return "border_router_capable" + + if val & ThreadNodeCapabilities.ROUTER_ELIGIBLE: + # radio always on, can be a router + return "router_eligible" + + if val & ThreadNodeCapabilities.FULL: + # radio always on, but can't be a router + return "full" + + if val & ThreadNodeCapabilities.MINIMAL: + # transceiver always on, does not need to poll for messages from its parent + return "minimal" + + if val & ThreadNodeCapabilities.SLEEPY: + # normally disabled, wakes on occasion to poll for messages from its parent + return "sleepy" + + # Device has no known thread capabilities + return "none" SIMPLE_SENSOR: dict[str, HomeKitSensorEntityDescription] = { @@ -195,6 +236,13 @@ SIMPLE_SENSOR: dict[str, HomeKitSensorEntityDescription] = { state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, ), + CharacteristicsTypes.THREAD_NODE_CAPABILITIES: HomeKitSensorEntityDescription( + key=CharacteristicsTypes.THREAD_NODE_CAPABILITIES, + name="Thread Capabilities", + device_class="homekit_controller__thread_node_capabilities", + entity_category=EntityCategory.DIAGNOSTIC, + format=thread_node_capability_to_str, + ), } @@ -399,7 +447,10 @@ class SimpleSensor(CharacteristicEntity, SensorEntity): @property def native_value(self) -> str | int | float: """Return the current sensor value.""" - return self._char.value + val = self._char.value + if self.entity_description.format: + return self.entity_description.format(val) + return val ENTITY_TYPES = { diff --git a/homeassistant/components/homekit_controller/strings.sensor.json b/homeassistant/components/homekit_controller/strings.sensor.json new file mode 100644 index 00000000000..d7d8e888a98 --- /dev/null +++ b/homeassistant/components/homekit_controller/strings.sensor.json @@ -0,0 +1,12 @@ +{ + "state": { + "homekit_controller__thread_node_capabilities": { + "border_router_capable": "Border Router Capable", + "router_eligible": "Router Eligible End Device", + "full": "Full End Device", + "minimal": "Minimal End Device", + "sleepy": "Sleepy End Device", + "none": "None" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/sensor.en.json b/homeassistant/components/homekit_controller/translations/sensor.en.json new file mode 100644 index 00000000000..b1f8a0a8128 --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/sensor.en.json @@ -0,0 +1,12 @@ +{ + "state": { + "homekit_controller__thread_node_capabilities": { + "border_router_capable": "Border Router Capable", + "full": "Full End Device", + "minimal": "Minimal End Device", + "none": "None", + "router_eligible": "Router Eligible End Device", + "sleepy": "Sleepy End Device" + } + } +} \ No newline at end of file diff --git a/tests/components/homekit_controller/specific_devices/test_nanoleaf_strip_nl55.py b/tests/components/homekit_controller/specific_devices/test_nanoleaf_strip_nl55.py index 7e6a9bb672b..086027f2427 100644 --- a/tests/components/homekit_controller/specific_devices/test_nanoleaf_strip_nl55.py +++ b/tests/components/homekit_controller/specific_devices/test_nanoleaf_strip_nl55.py @@ -50,6 +50,13 @@ async def test_nanoleaf_nl55_setup(hass): entity_category=EntityCategory.DIAGNOSTIC, state="unknown", ), + EntityTestInfo( + entity_id="sensor.nanoleaf_strip_3b32_thread_capabilities", + friendly_name="Nanoleaf Strip 3B32 Thread Capabilities", + unique_id="homekit-AAAA011111111111-aid:1-sid:31-cid:115", + entity_category=EntityCategory.DIAGNOSTIC, + state="border_router_capable", + ), ], ), ) diff --git a/tests/components/homekit_controller/test_sensor.py b/tests/components/homekit_controller/test_sensor.py index 836da1e466f..c2a466d3997 100644 --- a/tests/components/homekit_controller/test_sensor.py +++ b/tests/components/homekit_controller/test_sensor.py @@ -1,8 +1,12 @@ """Basic checks for HomeKit sensor.""" from aiohomekit.model.characteristics import CharacteristicsTypes +from aiohomekit.model.characteristics.const import ThreadNodeCapabilities from aiohomekit.model.services import ServicesTypes from aiohomekit.protocol.statuscodes import HapStatusCode +from homeassistant.components.homekit_controller.sensor import ( + thread_node_capability_to_str, +) from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass from tests.components.homekit_controller.common import Helper, setup_test_component @@ -315,3 +319,19 @@ async def test_sensor_unavailable(hass, utcnow): # Energy sensor has non-responsive characteristics so should be unavailable state = await energy_helper.poll_and_get_state() assert state.state == "unavailable" + + +def test_thread_node_caps_to_str(): + """Test all values of this enum get a translatable string.""" + assert ( + thread_node_capability_to_str(ThreadNodeCapabilities.BORDER_ROUTER_CAPABLE) + == "border_router_capable" + ) + assert ( + thread_node_capability_to_str(ThreadNodeCapabilities.ROUTER_ELIGIBLE) + == "router_eligible" + ) + assert thread_node_capability_to_str(ThreadNodeCapabilities.FULL) == "full" + assert thread_node_capability_to_str(ThreadNodeCapabilities.MINIMAL) == "minimal" + assert thread_node_capability_to_str(ThreadNodeCapabilities.SLEEPY) == "sleepy" + assert thread_node_capability_to_str(ThreadNodeCapabilities(128)) == "none"