Add homekit_controller thread node capabilties diagnostic sensor (#76120)
This commit is contained in:
parent
842cc060f8
commit
72a0ca4871
6 changed files with 104 additions and 1 deletions
|
@ -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",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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",
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue