Set Matter battery sensors as diagnostic (#95794)

* Set matter battery sensor to diagnostic

* Update tests

* Use new eve contact sensor dump across the board

* Assert entity category

* Complete typing
This commit is contained in:
Martin Hjelmare 2023-07-04 14:54:37 +02:00 committed by GitHub
parent 6964a2112a
commit 02192ddf82
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 455 additions and 120 deletions

View file

@ -13,7 +13,7 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntityDescription, BinarySensorEntityDescription,
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform from homeassistant.const import EntityCategory, Platform
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
@ -99,6 +99,7 @@ DISCOVERY_SCHEMAS = [
entity_description=MatterBinarySensorEntityDescription( entity_description=MatterBinarySensorEntityDescription(
key="BatteryChargeLevel", key="BatteryChargeLevel",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC,
measurement_to_ha=lambda x: x measurement_to_ha=lambda x: x
!= clusters.PowerSource.Enums.BatChargeLevelEnum.kOk, != clusters.PowerSource.Enums.BatChargeLevelEnum.kOk,
), ),

View file

@ -16,6 +16,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
LIGHT_LUX, LIGHT_LUX,
PERCENTAGE, PERCENTAGE,
EntityCategory,
Platform, Platform,
UnitOfPressure, UnitOfPressure,
UnitOfTemperature, UnitOfTemperature,
@ -127,6 +128,7 @@ DISCOVERY_SCHEMAS = [
key="PowerSource", key="PowerSource",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.BATTERY, device_class=SensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC,
# value has double precision # value has double precision
measurement_to_ha=lambda x: int(x / 2), measurement_to_ha=lambda x: int(x / 2),
), ),

View file

@ -5,12 +5,15 @@ import asyncio
from collections.abc import AsyncGenerator, Generator from collections.abc import AsyncGenerator, Generator
from unittest.mock import AsyncMock, MagicMock, patch from unittest.mock import AsyncMock, MagicMock, patch
from matter_server.client.models.node import MatterNode
from matter_server.common.const import SCHEMA_VERSION from matter_server.common.const import SCHEMA_VERSION
from matter_server.common.models import ServerInfoMessage from matter_server.common.models import ServerInfoMessage
import pytest import pytest
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from .common import setup_integration_with_node_fixture
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
MOCK_FABRIC_ID = 12341234 MOCK_FABRIC_ID = 12341234
@ -210,3 +213,21 @@ def update_addon_fixture() -> Generator[AsyncMock, None, None]:
"homeassistant.components.hassio.addon_manager.async_update_addon" "homeassistant.components.hassio.addon_manager.async_update_addon"
) as update_addon: ) as update_addon:
yield update_addon yield update_addon
@pytest.fixture(name="door_lock")
async def door_lock_fixture(
hass: HomeAssistant, matter_client: MagicMock
) -> MatterNode:
"""Fixture for a door lock node."""
return await setup_integration_with_node_fixture(hass, "door-lock", matter_client)
@pytest.fixture(name="eve_contact_sensor_node")
async def eve_contact_sensor_node_fixture(
hass: HomeAssistant, matter_client: MagicMock
) -> MatterNode:
"""Fixture for a contact sensor node."""
return await setup_integration_with_node_fixture(
hass, "eve-contact-sensor", matter_client
)

View file

@ -1,90 +0,0 @@
{
"node_id": 1,
"date_commissioned": "2022-11-29T21:23:48.485051",
"last_interview": "2022-11-29T21:23:48.485057",
"interview_version": 2,
"attributes": {
"0/29/0": [
{
"deviceType": 22,
"revision": 1
}
],
"0/29/1": [
4, 29, 31, 40, 42, 43, 44, 48, 49, 50, 51, 52, 53, 54, 55, 59, 60, 62, 63,
64, 65
],
"0/29/2": [41],
"0/29/3": [1],
"0/29/65532": 0,
"0/29/65533": 1,
"0/29/65528": [],
"0/29/65529": [],
"0/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533],
"0/40/0": 1,
"0/40/1": "Nabu Casa",
"0/40/2": 65521,
"0/40/3": "Mock ContactSensor",
"0/40/4": 32768,
"0/40/5": "Mock Contact sensor",
"0/40/6": "XX",
"0/40/7": 0,
"0/40/8": "v1.0",
"0/40/9": 1,
"0/40/10": "v1.0",
"0/40/11": "20221206",
"0/40/12": "",
"0/40/13": "",
"0/40/14": "",
"0/40/15": "TEST_SN",
"0/40/16": false,
"0/40/17": true,
"0/40/18": "mock-contact-sensor",
"0/40/19": {
"caseSessionsPerFabric": 3,
"subscriptionsPerFabric": 3
},
"0/40/65532": 0,
"0/40/65533": 1,
"0/40/65528": [],
"0/40/65529": [],
"0/40/65531": [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
65528, 65529, 65531, 65532, 65533
],
"1/3/0": 0,
"1/3/1": 2,
"1/3/65532": 0,
"1/3/65533": 4,
"1/3/65528": [],
"1/3/65529": [0, 64],
"1/3/65531": [0, 1, 65528, 65529, 65531, 65532, 65533],
"1/29/0": [
{
"deviceType": 21,
"revision": 1
}
],
"1/29/1": [
3, 4, 5, 6, 7, 8, 15, 29, 30, 37, 47, 59, 64, 65, 69, 80, 257, 258, 259,
512, 513, 514, 516, 768, 1024, 1026, 1027, 1028, 1029, 1030, 1283, 1284,
1285, 1286, 1287, 1288, 1289, 1290, 1291, 1292, 1293, 1294, 2820,
4294048773
],
"1/29/2": [],
"1/29/3": [],
"1/29/65532": 0,
"1/29/65533": 1,
"1/29/65528": [],
"1/29/65529": [],
"1/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533],
"1/69/0": true,
"1/69/65532": 0,
"1/69/65533": 1,
"1/69/65528": [],
"1/69/65529": [],
"1/69/65531": [0, 65528, 65529, 65531, 65532, 65533]
},
"available": true,
"attribute_subscriptions": []
}

View file

@ -0,0 +1,343 @@
{
"node_id": 1,
"date_commissioned": "2023-07-02T14:06:45.190550",
"last_interview": "2023-07-02T14:06:45.190553",
"interview_version": 4,
"available": true,
"is_bridge": false,
"attributes": {
"0/53/65532": 15,
"0/53/11": 26,
"0/53/3": 4895,
"0/53/47": 0,
"0/53/8": [
{
"extAddress": 12872547289273451492,
"rloc16": 1024,
"routerId": 1,
"nextHop": 0,
"pathCost": 0,
"LQIIn": 3,
"LQIOut": 3,
"age": 142,
"allocated": true,
"linkEstablished": true
}
],
"0/53/29": 1556,
"0/53/9": 2040160480,
"0/53/15": 1,
"0/53/40": 519,
"0/53/7": [
{
"extAddress": 12872547289273451492,
"age": 654,
"rloc16": 1024,
"linkFrameCounter": 738,
"mleFrameCounter": 418,
"lqi": 3,
"averageRssi": -50,
"lastRssi": -51,
"frameErrorRate": 5,
"messageErrorRate": 0,
"rxOnWhenIdle": true,
"fullThreadDevice": true,
"fullNetworkData": true,
"isChild": false
}
],
"0/53/33": 66,
"0/53/18": 1,
"0/53/45": 0,
"0/53/21": 0,
"0/53/36": 0,
"0/53/44": 0,
"0/53/50": 0,
"0/53/60": "AB//wA==",
"0/53/10": 68,
"0/53/53": 0,
"0/53/65528": [],
"0/53/4": 5980345540157460411,
"0/53/19": 1,
"0/53/62": [0, 0, 0, 0],
"0/53/54": 2,
"0/53/49": 0,
"0/53/23": 2597,
"0/53/20": 0,
"0/53/28": 1059,
"0/53/24": 17,
"0/53/22": 2614,
"0/53/17": 0,
"0/53/32": 0,
"0/53/14": 1,
"0/53/26": 2597,
"0/53/37": 0,
"0/53/65529": [0],
"0/53/34": 1,
"0/53/2": "MyHome1425454932",
"0/53/6": 0,
"0/53/43": 0,
"0/53/25": 2597,
"0/53/30": 0,
"0/53/41": 1,
"0/53/55": 4,
"0/53/42": 520,
"0/53/52": 0,
"0/53/61": {
"activeTimestampPresent": true,
"pendingTimestampPresent": false,
"masterKeyPresent": true,
"networkNamePresent": true,
"extendedPanIdPresent": true,
"meshLocalPrefixPresent": true,
"delayPresent": false,
"panIdPresent": true,
"channelPresent": true,
"pskcPresent": true,
"securityPolicyPresent": true,
"channelMaskPresent": true
},
"0/53/48": 3,
"0/53/39": 529,
"0/53/35": 0,
"0/53/38": 0,
"0/53/31": 0,
"0/53/51": 0,
"0/53/65533": 1,
"0/53/59": {
"rotationTime": 672,
"flags": 8335
},
"0/53/46": 0,
"0/53/5": "QP1S/nSVYwAA",
"0/53/13": 1,
"0/53/27": 17,
"0/53/1": 2,
"0/53/0": 25,
"0/53/65531": [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38,
39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 59,
60, 61, 62, 65528, 65529, 65531, 65532, 65533
],
"0/53/12": 121,
"0/53/16": 0,
"0/42/0": [
{
"providerNodeID": 1773685588,
"endpoint": 0,
"fabricIndex": 1
}
],
"0/42/65528": [],
"0/42/65533": 1,
"0/42/1": true,
"0/42/2": 1,
"0/42/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533],
"0/42/3": null,
"0/42/65532": 0,
"0/42/65529": [0],
"0/48/65532": 0,
"0/48/65528": [1, 3, 5],
"0/48/1": {
"failSafeExpiryLengthSeconds": 60,
"maxCumulativeFailsafeSeconds": 900
},
"0/48/4": true,
"0/48/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533],
"0/48/2": 0,
"0/48/0": 0,
"0/48/3": 0,
"0/48/65529": [0, 2, 4],
"0/48/65533": 1,
"0/31/4": 3,
"0/31/65529": [],
"0/31/3": 3,
"0/31/65533": 1,
"0/31/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533],
"0/31/1": [],
"0/31/0": [
{
"privilege": 0,
"authMode": 0,
"subjects": null,
"targets": null,
"fabricIndex": 1
},
{
"privilege": 0,
"authMode": 0,
"subjects": null,
"targets": null,
"fabricIndex": 2
},
{
"privilege": 5,
"authMode": 2,
"subjects": [112233],
"targets": null,
"fabricIndex": 3
}
],
"0/31/65532": 0,
"0/31/65528": [],
"0/31/2": 4,
"0/49/2": 10,
"0/49/65528": [1, 5, 7],
"0/49/65533": 1,
"0/49/1": [
{
"networkID": "Uv50lWMtT7s=",
"connected": true
}
],
"0/49/3": 20,
"0/49/7": null,
"0/49/0": 1,
"0/49/6": null,
"0/49/65529": [0, 3, 4, 6, 8],
"0/49/5": 0,
"0/49/4": true,
"0/49/65531": [0, 1, 2, 3, 4, 5, 6, 7, 65528, 65529, 65531, 65532, 65533],
"0/49/65532": 2,
"0/63/0": [],
"0/63/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533],
"0/63/65528": [2, 5],
"0/63/1": [],
"0/63/3": 3,
"0/63/65532": 0,
"0/63/65533": 1,
"0/63/65529": [0, 1, 3, 4],
"0/63/2": 3,
"0/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533],
"0/29/65529": [],
"0/29/65532": 0,
"0/29/3": [1],
"0/29/2": [41],
"0/29/65533": 1,
"0/29/0": [
{
"deviceType": 22,
"revision": 1
}
],
"0/29/1": [29, 31, 40, 42, 46, 48, 49, 51, 53, 60, 62, 63],
"0/29/65528": [],
"0/51/65531": [0, 1, 2, 3, 5, 6, 7, 8, 65528, 65529, 65531, 65532, 65533],
"0/51/0": [
{
"name": "ieee802154",
"isOperational": true,
"offPremiseServicesReachableIPv4": null,
"offPremiseServicesReachableIPv6": null,
"hardwareAddress": "YtmXHFJ/dhk=",
"IPv4Addresses": [],
"IPv6Addresses": [
"/RG+U41GAABynlpPU50e5g==",
"/oAAAAAAAABg2ZccUn92GQ==",
"/VL+dJVjAAB1cwmi02rvTA=="
],
"type": 4
}
],
"0/51/65529": [0],
"0/51/7": [],
"0/51/3": 0,
"0/51/65533": 1,
"0/51/2": 653,
"0/51/6": [],
"0/51/1": 1,
"0/51/8": false,
"0/51/65532": 0,
"0/51/65528": [],
"0/51/5": [],
"0/40/9": 6650,
"0/40/65529": [],
"0/40/4": 77,
"0/40/1": "Eve Systems",
"0/40/5": "",
"0/40/15": "QV26L1A16199",
"0/40/8": "1.1",
"0/40/6": "**REDACTED**",
"0/40/3": "Eve Door",
"0/40/19": {
"caseSessionsPerFabric": 3,
"subscriptionsPerFabric": 3
},
"0/40/2": 4874,
"0/40/65532": 0,
"0/40/65528": [],
"0/40/65531": [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 18, 19, 65528, 65529, 65531, 65532,
65533
],
"0/40/7": 1,
"0/40/10": "3.2.1",
"0/40/0": 1,
"0/40/65533": 1,
"0/40/18": "4D97F6015F8E39C1",
"0/46/65529": [],
"0/46/0": [1],
"0/46/65528": [],
"0/46/65531": [0, 65528, 65529, 65531, 65532, 65533],
"0/46/65532": 0,
"0/46/65533": 1,
"0/60/65532": 0,
"0/60/0": 0,
"0/60/65528": [],
"0/60/65531": [0, 1, 2, 65528, 65529, 65531, 65532, 65533],
"0/60/2": null,
"0/60/65529": [0, 1, 2],
"0/60/1": null,
"0/60/65533": 1,
"1/69/65529": [],
"1/69/65528": [],
"1/69/65531": [0, 65528, 65529, 65531, 65532, 65533],
"1/69/65533": 1,
"1/69/65532": 0,
"1/69/0": false,
"1/29/65529": [],
"1/29/1": [3, 29, 47, 69, 319486977],
"1/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533],
"1/29/65533": 1,
"1/29/0": [
{
"deviceType": 21,
"revision": 1
}
],
"1/29/65528": [],
"1/29/65532": 0,
"1/29/2": [],
"1/29/3": [],
"1/47/65531": [
0, 1, 2, 11, 12, 14, 15, 16, 18, 19, 25, 65528, 65529, 65531, 65532, 65533
],
"1/47/15": false,
"1/47/25": 1,
"1/47/2": "Battery",
"1/47/18": [],
"1/47/1": 0,
"1/47/14": 0,
"1/47/65533": 1,
"1/47/12": 200,
"1/47/19": "",
"1/47/11": 3558,
"1/47/65528": [],
"1/47/65529": [],
"1/47/0": 1,
"1/47/16": 2,
"1/47/65532": 10,
"1/3/65528": [],
"1/3/65529": [0],
"1/3/1": 2,
"1/3/0": 0,
"1/3/65531": [0, 1, 65528, 65529, 65531, 65532, 65533],
"1/3/65532": 0,
"1/3/65533": 4
},
"attribute_subscriptions": [
[1, 69, 0],
[1, 47, 12]
]
}

View file

@ -1,10 +1,16 @@
"""Test Matter binary sensors.""" """Test Matter binary sensors."""
from unittest.mock import MagicMock from collections.abc import Generator
from unittest.mock import MagicMock, patch
from matter_server.client.models.node import MatterNode from matter_server.client.models.node import MatterNode
import pytest import pytest
from homeassistant.components.matter.binary_sensor import (
DISCOVERY_SCHEMAS as BINARY_SENSOR_SCHEMAS,
)
from homeassistant.const import EntityCategory, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from .common import ( from .common import (
set_node_attribute, set_node_attribute,
@ -13,14 +19,16 @@ from .common import (
) )
@pytest.fixture(name="contact_sensor_node") @pytest.fixture(autouse=True)
async def contact_sensor_node_fixture( def binary_sensor_platform() -> Generator[None, None, None]:
hass: HomeAssistant, matter_client: MagicMock """Load only the binary sensor platform."""
) -> MatterNode: with patch(
"""Fixture for a contact sensor node.""" "homeassistant.components.matter.discovery.DISCOVERY_SCHEMAS",
return await setup_integration_with_node_fixture( new={
hass, "contact-sensor", matter_client Platform.BINARY_SENSOR: BINARY_SENSOR_SCHEMAS,
) },
):
yield
# This tests needs to be adjusted to remove lingering tasks # This tests needs to be adjusted to remove lingering tasks
@ -28,22 +36,23 @@ async def contact_sensor_node_fixture(
async def test_contact_sensor( async def test_contact_sensor(
hass: HomeAssistant, hass: HomeAssistant,
matter_client: MagicMock, matter_client: MagicMock,
contact_sensor_node: MatterNode, eve_contact_sensor_node: MatterNode,
) -> None: ) -> None:
"""Test contact sensor.""" """Test contact sensor."""
state = hass.states.get("binary_sensor.mock_contact_sensor_door") entity_id = "binary_sensor.eve_door_door"
assert state state = hass.states.get(entity_id)
assert state.state == "off"
set_node_attribute(contact_sensor_node, 1, 69, 0, False)
await trigger_subscription_callback(
hass, matter_client, data=(contact_sensor_node.node_id, "1/69/0", False)
)
state = hass.states.get("binary_sensor.mock_contact_sensor_door")
assert state assert state
assert state.state == "on" assert state.state == "on"
set_node_attribute(eve_contact_sensor_node, 1, 69, 0, True)
await trigger_subscription_callback(
hass, matter_client, data=(eve_contact_sensor_node.node_id, "1/69/0", True)
)
state = hass.states.get(entity_id)
assert state
assert state.state == "off"
@pytest.fixture(name="occupancy_sensor_node") @pytest.fixture(name="occupancy_sensor_node")
async def occupancy_sensor_node_fixture( async def occupancy_sensor_node_fixture(
@ -75,3 +84,32 @@ async def test_occupancy_sensor(
state = hass.states.get("binary_sensor.mock_occupancy_sensor_occupancy") state = hass.states.get("binary_sensor.mock_occupancy_sensor_occupancy")
assert state assert state
assert state.state == "off" assert state.state == "off"
# This tests needs to be adjusted to remove lingering tasks
@pytest.mark.parametrize("expected_lingering_tasks", [True])
async def test_battery_sensor(
hass: HomeAssistant,
matter_client: MagicMock,
door_lock: MatterNode,
) -> None:
"""Test battery sensor."""
entity_id = "binary_sensor.mock_door_lock_battery"
state = hass.states.get(entity_id)
assert state
assert state.state == "off"
set_node_attribute(door_lock, 1, 47, 14, 1)
await trigger_subscription_callback(
hass, matter_client, data=(door_lock.node_id, "1/47/14", 1)
)
state = hass.states.get(entity_id)
assert state
assert state.state == "on"
entity_registry = er.async_get(hass)
entry = entity_registry.async_get(entity_id)
assert entry
assert entry.entity_category == EntityCategory.DIAGNOSTIC

View file

@ -16,19 +16,10 @@ from homeassistant.core import HomeAssistant
from .common import ( from .common import (
set_node_attribute, set_node_attribute,
setup_integration_with_node_fixture,
trigger_subscription_callback, trigger_subscription_callback,
) )
@pytest.fixture(name="door_lock")
async def door_lock_fixture(
hass: HomeAssistant, matter_client: MagicMock
) -> MatterNode:
"""Fixture for a door lock node."""
return await setup_integration_with_node_fixture(hass, "door-lock", matter_client)
# This tests needs to be adjusted to remove lingering tasks # This tests needs to be adjusted to remove lingering tasks
@pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_tasks", [True])
async def test_lock( async def test_lock(

View file

@ -4,7 +4,9 @@ from unittest.mock import MagicMock
from matter_server.client.models.node import MatterNode from matter_server.client.models.node import MatterNode
import pytest import pytest
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from .common import ( from .common import (
set_node_attribute, set_node_attribute,
@ -179,3 +181,30 @@ async def test_temperature_sensor(
state = hass.states.get("sensor.mock_temperature_sensor_temperature") state = hass.states.get("sensor.mock_temperature_sensor_temperature")
assert state assert state
assert state.state == "25.0" assert state.state == "25.0"
# This tests needs to be adjusted to remove lingering tasks
@pytest.mark.parametrize("expected_lingering_tasks", [True])
async def test_battery_sensor(
hass: HomeAssistant,
matter_client: MagicMock,
eve_contact_sensor_node: MatterNode,
) -> None:
"""Test battery sensor."""
entity_id = "sensor.eve_door_battery"
state = hass.states.get(entity_id)
assert state
assert state.state == "100"
set_node_attribute(eve_contact_sensor_node, 1, 47, 12, 100)
await trigger_subscription_callback(hass, matter_client)
state = hass.states.get(entity_id)
assert state
assert state.state == "50"
entity_registry = er.async_get(hass)
entry = entity_registry.async_get(entity_id)
assert entry
assert entry.entity_category == EntityCategory.DIAGNOSTIC