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_SO2: "sensor",
CharacteristicsTypes.DENSITY_VOC: "sensor", CharacteristicsTypes.DENSITY_VOC: "sensor",
CharacteristicsTypes.IDENTIFY: "button", CharacteristicsTypes.IDENTIFY: "button",
CharacteristicsTypes.THREAD_NODE_CAPABILITIES: "sensor",
} }

View file

@ -5,6 +5,7 @@ from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from aiohomekit.model.characteristics import Characteristic, CharacteristicsTypes from aiohomekit.model.characteristics import Characteristic, CharacteristicsTypes
from aiohomekit.model.characteristics.const import ThreadNodeCapabilities
from aiohomekit.model.services import Service, ServicesTypes from aiohomekit.model.services import Service, ServicesTypes
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
@ -27,6 +28,7 @@ from homeassistant.const import (
TEMP_CELSIUS, TEMP_CELSIUS,
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
@ -40,6 +42,45 @@ class HomeKitSensorEntityDescription(SensorEntityDescription):
"""Describes Homekit sensor.""" """Describes Homekit sensor."""
probe: Callable[[Characteristic], bool] | None = None 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] = { SIMPLE_SENSOR: dict[str, HomeKitSensorEntityDescription] = {
@ -195,6 +236,13 @@ SIMPLE_SENSOR: dict[str, HomeKitSensorEntityDescription] = {
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, 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 @property
def native_value(self) -> str | int | float: def native_value(self) -> str | int | float:
"""Return the current sensor value.""" """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 = { 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, entity_category=EntityCategory.DIAGNOSTIC,
state="unknown", 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.""" """Basic checks for HomeKit sensor."""
from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.characteristics import CharacteristicsTypes
from aiohomekit.model.characteristics.const import ThreadNodeCapabilities
from aiohomekit.model.services import ServicesTypes from aiohomekit.model.services import ServicesTypes
from aiohomekit.protocol.statuscodes import HapStatusCode 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 homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
from tests.components.homekit_controller.common import Helper, setup_test_component 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 # Energy sensor has non-responsive characteristics so should be unavailable
state = await energy_helper.poll_and_get_state() state = await energy_helper.poll_and_get_state()
assert state.state == "unavailable" 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"