Add GeneralDiagnostics sensors and fault binary sensors to Matter integration (#169830)

This commit is contained in:
markvp
2026-05-27 21:07:08 +02:00
committed by GitHub
parent 1e457600f1
commit adeb352079
6 changed files with 11430 additions and 1 deletions
@@ -556,4 +556,48 @@ DISCOVERY_SCHEMAS = [
featuremap_contains=clusters.Thermostat.Bitmaps.Feature.kOccupancy,
allow_multi=True,
),
# GeneralDiagnostics active fault sensors
MatterDiscoverySchema(
platform=Platform.BINARY_SENSOR,
entity_description=MatterBinarySensorEntityDescription(
key="GeneralDiagnosticsActiveHardwareFaults",
translation_key="active_hardware_faults",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
device_to_ha=bool,
),
entity_class=MatterBinarySensor,
required_attributes=(
clusters.GeneralDiagnostics.Attributes.ActiveHardwareFaults,
),
),
MatterDiscoverySchema(
platform=Platform.BINARY_SENSOR,
entity_description=MatterBinarySensorEntityDescription(
key="GeneralDiagnosticsActiveRadioFaults",
translation_key="active_radio_faults",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
device_to_ha=bool,
),
entity_class=MatterBinarySensor,
required_attributes=(clusters.GeneralDiagnostics.Attributes.ActiveRadioFaults,),
),
MatterDiscoverySchema(
platform=Platform.BINARY_SENSOR,
entity_description=MatterBinarySensorEntityDescription(
key="GeneralDiagnosticsActiveNetworkFaults",
translation_key="active_network_faults",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
device_to_ha=bool,
),
entity_class=MatterBinarySensor,
required_attributes=(
clusters.GeneralDiagnostics.Attributes.ActiveNetworkFaults,
),
),
]
+53
View File
@@ -137,6 +137,17 @@ RVC_OPERATIONAL_STATE_ERROR_MAP = {
_rvc_err.kNavigationSensorObscured: ("navigation_sensor_obscured"),
}
BOOT_REASON_MAP = {
clusters.GeneralDiagnostics.Enums.BootReasonEnum.kUnspecified: "unspecified",
clusters.GeneralDiagnostics.Enums.BootReasonEnum.kPowerOnReboot: "power_on_reboot",
clusters.GeneralDiagnostics.Enums.BootReasonEnum.kBrownOutReset: "brown_out_reset",
clusters.GeneralDiagnostics.Enums.BootReasonEnum.kSoftwareWatchdogReset: "software_watchdog_reset",
clusters.GeneralDiagnostics.Enums.BootReasonEnum.kHardwareWatchdogReset: "hardware_watchdog_reset",
clusters.GeneralDiagnostics.Enums.BootReasonEnum.kSoftwareUpdateCompleted: "software_update_completed",
clusters.GeneralDiagnostics.Enums.BootReasonEnum.kSoftwareReset: "software_reset",
clusters.GeneralDiagnostics.Enums.BootReasonEnum.kUnknownEnumValue: None,
}
BOOST_STATE_MAP = {
clusters.WaterHeaterManagement.Enums.BoostStateEnum.kInactive: "inactive",
clusters.WaterHeaterManagement.Enums.BoostStateEnum.kActive: "active",
@@ -1575,4 +1586,46 @@ DISCOVERY_SCHEMAS = [
required_attributes=(clusters.DoorLock.Attributes.DoorClosedEvents,),
featuremap_contains=clusters.DoorLock.Bitmaps.Feature.kDoorPositionSensor,
),
# GeneralDiagnostics cluster sensors
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="GeneralDiagnosticsRebootCount",
translation_key="reboot_count",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
state_class=SensorStateClass.TOTAL_INCREASING,
),
entity_class=MatterSensor,
required_attributes=(clusters.GeneralDiagnostics.Attributes.RebootCount,),
),
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="GeneralDiagnosticsUpTime",
translation_key="uptime",
device_class=SensorDeviceClass.UPTIME,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
device_to_ha=lambda uptime: dt_util.utcnow() - timedelta(seconds=uptime),
),
entity_class=MatterSensor,
required_attributes=(clusters.GeneralDiagnostics.Attributes.UpTime,),
),
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="GeneralDiagnosticsBootReason",
translation_key="boot_reason",
device_class=SensorDeviceClass.ENUM,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
options=[
reason for reason in BOOT_REASON_MAP.values() if reason is not None
],
device_to_ha=BOOT_REASON_MAP.get,
),
entity_class=MatterSensor,
required_attributes=(clusters.GeneralDiagnostics.Attributes.BootReason,),
),
]
@@ -47,6 +47,15 @@
},
"entity": {
"binary_sensor": {
"active_hardware_faults": {
"name": "Hardware faults"
},
"active_network_faults": {
"name": "Network faults"
},
"active_radio_faults": {
"name": "Radio faults"
},
"actuator": {
"name": "Actuator"
},
@@ -408,6 +417,18 @@
"battery_voltage": {
"name": "Battery voltage"
},
"boot_reason": {
"name": "Boot reason",
"state": {
"brown_out_reset": "Brownout reset",
"hardware_watchdog_reset": "Hardware watchdog reset",
"power_on_reboot": "Power-on reboot",
"software_reset": "Software reset",
"software_update_completed": "Software update completed",
"software_watchdog_reset": "Software watchdog reset",
"unspecified": "Unspecified"
}
},
"contamination_state": {
"name": "Contamination state",
"state": {
@@ -576,6 +597,9 @@
"reactive_current": {
"name": "Reactive current"
},
"reboot_count": {
"name": "Reboot count"
},
"rms_current": {
"name": "Effective current"
},
@@ -600,6 +624,9 @@
"medium": "[%key:common::state::medium%]"
}
},
"uptime": {
"name": "Uptime"
},
"valve_position": {
"name": "Valve position"
},
File diff suppressed because it is too large Load Diff
+99 -1
View File
@@ -12,7 +12,7 @@ from syrupy.assertion import SnapshotAssertion
from homeassistant.components.matter.binary_sensor import (
DISCOVERY_SCHEMAS as BINARY_SENSOR_SCHEMAS,
)
from homeassistant.const import Platform
from homeassistant.const import EntityCategory, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
@@ -733,3 +733,101 @@ async def test_co_detector(
state = hass.states.get("binary_sensor.smart_co_sensor_carbon_monoxide")
assert state
assert state.state == "off"
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
@pytest.mark.parametrize("node_fixture", ["device_diagnostics"])
async def test_general_diagnostics_fault_sensors(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
matter_client: MagicMock,
matter_node: MatterNode,
) -> None:
"""Test GeneralDiagnostics active fault binary sensors."""
# ActiveHardwareFaults (cluster 51, attr 5) = [] (no faults)
entity_id = "binary_sensor.m5stamp_lighting_app_hardware_faults"
state = hass.states.get(entity_id)
assert state
assert state.state == "off"
entry = entity_registry.async_get(entity_id)
assert entry
assert entry.entity_category is EntityCategory.DIAGNOSTIC
# Simulate hardware fault
set_node_attribute(matter_node, 0, 51, 5, [1])
await trigger_subscription_callback(hass, matter_client)
state = hass.states.get(entity_id)
assert state
assert state.state == "on"
# Clear faults
set_node_attribute(matter_node, 0, 51, 5, [])
await trigger_subscription_callback(hass, matter_client)
state = hass.states.get(entity_id)
assert state
assert state.state == "off"
# ActiveRadioFaults (cluster 51, attr 6) = [] (no faults)
state = hass.states.get("binary_sensor.m5stamp_lighting_app_radio_faults")
assert state
assert state.state == "off"
set_node_attribute(matter_node, 0, 51, 6, [1])
await trigger_subscription_callback(hass, matter_client)
state = hass.states.get("binary_sensor.m5stamp_lighting_app_radio_faults")
assert state
assert state.state == "on"
set_node_attribute(matter_node, 0, 51, 6, [])
await trigger_subscription_callback(hass, matter_client)
state = hass.states.get("binary_sensor.m5stamp_lighting_app_radio_faults")
assert state
assert state.state == "off"
# ActiveNetworkFaults (cluster 51, attr 7) = [] (no faults)
state = hass.states.get("binary_sensor.m5stamp_lighting_app_network_faults")
assert state
assert state.state == "off"
set_node_attribute(matter_node, 0, 51, 7, [1])
await trigger_subscription_callback(hass, matter_client)
state = hass.states.get("binary_sensor.m5stamp_lighting_app_network_faults")
assert state
assert state.state == "on"
set_node_attribute(matter_node, 0, 51, 7, [])
await trigger_subscription_callback(hass, matter_client)
state = hass.states.get("binary_sensor.m5stamp_lighting_app_network_faults")
assert state
assert state.state == "off"
entry = entity_registry.async_get(
"binary_sensor.m5stamp_lighting_app_hardware_faults"
)
assert entry
assert entry.entity_category is EntityCategory.DIAGNOSTIC
@pytest.mark.parametrize("node_fixture", ["device_diagnostics"])
async def test_general_diagnostics_fault_sensors_disabled_by_default(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
matter_client: MagicMock,
matter_node: MatterNode,
) -> None:
"""Test GeneralDiagnostics fault binary sensors are disabled by default."""
for entity_id in (
"binary_sensor.m5stamp_lighting_app_hardware_faults",
"binary_sensor.m5stamp_lighting_app_radio_faults",
"binary_sensor.m5stamp_lighting_app_network_faults",
):
entry = entity_registry.async_get(entity_id)
assert entry, f"Expected {entity_id} to be registered"
assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION
+73
View File
@@ -885,3 +885,76 @@ async def test_bridged_device_reachable_updates_availability(
state = hass.states.get(entity_id)
assert state
assert state.state != STATE_UNAVAILABLE
@pytest.mark.freeze_time("2025-01-01T14:00:00+00:00")
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
@pytest.mark.parametrize("node_fixture", ["device_diagnostics"])
async def test_general_diagnostics_sensors(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
matter_client: MagicMock,
matter_node: MatterNode,
) -> None:
"""Test GeneralDiagnostics cluster sensors."""
# RebootCount (cluster 51, attr 1) = 3
state = hass.states.get("sensor.m5stamp_lighting_app_reboot_count")
assert state
assert state.state == "3"
set_node_attribute(matter_node, 0, 51, 1, 5)
await trigger_subscription_callback(hass, matter_client)
state = hass.states.get("sensor.m5stamp_lighting_app_reboot_count")
assert state
assert state.state == "5"
entry = entity_registry.async_get("sensor.m5stamp_lighting_app_reboot_count")
assert entry
assert entry.entity_category == EntityCategory.DIAGNOSTIC
# UpTime (cluster 51, attr 2) = 213 seconds → boot at now - 213s
state = hass.states.get("sensor.m5stamp_lighting_app_uptime")
assert state
assert state.state == "2025-01-01T13:56:27+00:00"
set_node_attribute(matter_node, 0, 51, 2, 3600)
await trigger_subscription_callback(hass, matter_client)
state = hass.states.get("sensor.m5stamp_lighting_app_uptime")
assert state
assert state.state == "2025-01-01T13:00:00+00:00"
# BootReason (cluster 51, attr 4) = 1 (PowerOnReboot)
state = hass.states.get("sensor.m5stamp_lighting_app_boot_reason")
assert state
assert state.state == "power_on_reboot"
set_node_attribute(matter_node, 0, 51, 4, 6)
await trigger_subscription_callback(hass, matter_client)
state = hass.states.get("sensor.m5stamp_lighting_app_boot_reason")
assert state
assert state.state == "software_reset"
entry = entity_registry.async_get("sensor.m5stamp_lighting_app_boot_reason")
assert entry
assert entry.entity_category == EntityCategory.DIAGNOSTIC
@pytest.mark.parametrize("node_fixture", ["device_diagnostics"])
async def test_general_diagnostics_sensors_disabled_by_default(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
matter_client: MagicMock,
matter_node: MatterNode,
) -> None:
"""Test GeneralDiagnostics sensors are disabled by default."""
for entity_id in (
"sensor.m5stamp_lighting_app_reboot_count",
"sensor.m5stamp_lighting_app_uptime",
"sensor.m5stamp_lighting_app_boot_reason",
):
entry = entity_registry.async_get(entity_id)
assert entry, f"Expected {entity_id} to be registered"
assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION