diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index ecfad477d00..a6810c10d99 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -5,7 +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.characteristics.const import ThreadNodeCapabilities, ThreadStatus from aiohomekit.model.services import Service, ServicesTypes from homeassistant.components.sensor import ( @@ -83,6 +83,54 @@ def thread_node_capability_to_str(char: Characteristic) -> str: return "none" +def thread_status_to_str(char: Characteristic) -> str: + """ + Return the thread status as a string. + + The underlying value is a bitmask, but we want to turn that to + a human readable string. So we check the flags in order. E.g. BORDER_ROUTER implies + ROUTER, so its more important to show that value. + """ + + val = ThreadStatus(char.value) + + if val & ThreadStatus.BORDER_ROUTER: + # Device has joined the Thread network and is participating + # in routing between mesh nodes. + # It's also the border router - bridging the thread network + # to WiFI/Ethernet/etc + return "border_router" + + if val & ThreadStatus.LEADER: + # Device has joined the Thread network and is participating + # in routing between mesh nodes. + # It's also the leader. There's only one leader and it manages + # which nodes are routers. + return "leader" + + if val & ThreadStatus.ROUTER: + # Device has joined the Thread network and is participating + # in routing between mesh nodes. + return "router" + + if val & ThreadStatus.CHILD: + # Device has joined the Thread network as a child + # It's not participating in routing between mesh nodes + return "child" + + if val & ThreadStatus.JOINING: + # Device is currently joining its Thread network + return "joining" + + if val & ThreadStatus.DETACHED: + # Device is currently unable to reach its Thread network + return "detached" + + # Must be ThreadStatus.DISABLED + # Device is not currently connected to Thread and will not try to. + return "disabled" + + SIMPLE_SENSOR: dict[str, HomeKitSensorEntityDescription] = { CharacteristicsTypes.VENDOR_CONNECTSENSE_ENERGY_WATT: HomeKitSensorEntityDescription( key=CharacteristicsTypes.VENDOR_CONNECTSENSE_ENERGY_WATT, @@ -243,6 +291,13 @@ SIMPLE_SENSOR: dict[str, HomeKitSensorEntityDescription] = { entity_category=EntityCategory.DIAGNOSTIC, format=thread_node_capability_to_str, ), + CharacteristicsTypes.THREAD_STATUS: HomeKitSensorEntityDescription( + key=CharacteristicsTypes.THREAD_STATUS, + name="Thread Status", + device_class="homekit_controller__thread_status", + entity_category=EntityCategory.DIAGNOSTIC, + format=thread_status_to_str, + ), } diff --git a/homeassistant/components/homekit_controller/strings.sensor.json b/homeassistant/components/homekit_controller/strings.sensor.json index 4a77fa1668a..ceede708572 100644 --- a/homeassistant/components/homekit_controller/strings.sensor.json +++ b/homeassistant/components/homekit_controller/strings.sensor.json @@ -7,6 +7,15 @@ "minimal": "Minimal End Device", "sleepy": "Sleepy End Device", "none": "None" + }, + "homekit_controller__thread_status": { + "border_router": "Border Router", + "leader": "Leader", + "router": "Router", + "child": "Child", + "joining": "Joining", + "detached": "Detached", + "disabled": "Disabled" } } } diff --git a/homeassistant/components/homekit_controller/translations/sensor.en.json b/homeassistant/components/homekit_controller/translations/sensor.en.json index b1f8a0a8128..acfa6ab2824 100644 --- a/homeassistant/components/homekit_controller/translations/sensor.en.json +++ b/homeassistant/components/homekit_controller/translations/sensor.en.json @@ -7,6 +7,15 @@ "none": "None", "router_eligible": "Router Eligible End Device", "sleepy": "Sleepy End Device" + }, + "homekit_controller__thread_status": { + "border_router": "Border Router", + "child": "Child", + "detached": "Detached", + "disabled": "Disabled", + "joining": "Joining", + "leader": "Leader", + "router": "Router" } } } \ 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 086027f2427..550c3a328d0 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 @@ -57,6 +57,13 @@ async def test_nanoleaf_nl55_setup(hass): entity_category=EntityCategory.DIAGNOSTIC, state="border_router_capable", ), + EntityTestInfo( + entity_id="sensor.nanoleaf_strip_3b32_thread_status", + friendly_name="Nanoleaf Strip 3B32 Thread Status", + unique_id="homekit-AAAA011111111111-aid:1-sid:31-cid:117", + entity_category=EntityCategory.DIAGNOSTIC, + state="border_router", + ), ], ), ) diff --git a/tests/components/homekit_controller/test_sensor.py b/tests/components/homekit_controller/test_sensor.py index 79ae29c7434..21112937939 100644 --- a/tests/components/homekit_controller/test_sensor.py +++ b/tests/components/homekit_controller/test_sensor.py @@ -1,11 +1,12 @@ """Basic checks for HomeKit sensor.""" from aiohomekit.model.characteristics import CharacteristicsTypes -from aiohomekit.model.characteristics.const import ThreadNodeCapabilities +from aiohomekit.model.characteristics.const import ThreadNodeCapabilities, ThreadStatus from aiohomekit.model.services import ServicesTypes from aiohomekit.protocol.statuscodes import HapStatusCode from homeassistant.components.homekit_controller.sensor import ( thread_node_capability_to_str, + thread_status_to_str, ) from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass @@ -337,3 +338,14 @@ def test_thread_node_caps_to_str(): 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" + + +def test_thread_status_to_str(): + """Test all values of this enum get a translatable string.""" + assert thread_status_to_str(ThreadStatus.BORDER_ROUTER) == "border_router" + assert thread_status_to_str(ThreadStatus.LEADER) == "leader" + assert thread_status_to_str(ThreadStatus.ROUTER) == "router" + assert thread_status_to_str(ThreadStatus.CHILD) == "child" + assert thread_status_to_str(ThreadStatus.JOINING) == "joining" + assert thread_status_to_str(ThreadStatus.DETACHED) == "detached" + assert thread_status_to_str(ThreadStatus.DISABLED) == "disabled"