Add homekit_controller thread node capabilties diagnostic sensor (#76120)

This commit is contained in:
Jc2k 2022-08-03 22:03:10 +01:00 committed by GitHub
parent 842cc060f8
commit 72a0ca4871
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 104 additions and 1 deletions

View file

@ -88,6 +88,7 @@ CHARACTERISTIC_PLATFORMS = {
CharacteristicsTypes.DENSITY_SO2: "sensor",
CharacteristicsTypes.DENSITY_VOC: "sensor",
CharacteristicsTypes.IDENTIFY: "button",
CharacteristicsTypes.THREAD_NODE_CAPABILITIES: "sensor",
}

View file

@ -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 = {

View file

@ -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"
}
}
}

View file

@ -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"
}
}
}

View file

@ -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",
),
],
),
)

View file

@ -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"