Enable Zwave notification sensors by default (#125326)

* Enable Zwave notification sensors by default

* Enable Zwave notification sensors by default

* Enable Zwave notification sensors by default

* Enable Zwave notification sensors by default

* Enable Zwave notification sensors by default

* Enable Zwave notification sensors by default

* Enable Zwave notification sensors by default

* Enable Zwave notification sensors by default

* Fix the check to (dis)allow discovering a value multiple times

* Prevent discovery of duplicate Notification CC sensors

* alarm sensors disabled by default

* one more fix

* Update diagnostics tests

---------

Co-authored-by: Marcel van der Veldt <m.vanderveldt@outlook.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Joost Lekkerkerker 2024-09-25 11:53:42 +02:00 committed by GitHub
parent 771575cfc5
commit bebd1dc235
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 3508 additions and 116 deletions

View File

@ -248,6 +248,16 @@ BOOLEAN_SENSOR_MAPPINGS: dict[int, BinarySensorEntityDescription] = {
} }
@callback
def is_valid_notification_binary_sensor(
info: ZwaveDiscoveryInfo,
) -> bool | NotificationZWaveJSEntityDescription:
"""Return if the notification CC Value is valid as binary sensor."""
if not info.primary_value.metadata.states:
return False
return len(info.primary_value.metadata.states) > 1
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: ConfigEntry,
@ -264,16 +274,18 @@ async def async_setup_entry(
entities: list[BinarySensorEntity] = [] entities: list[BinarySensorEntity] = []
if info.platform_hint == "notification": if info.platform_hint == "notification":
# ensure the notification CC Value is valid as binary sensor
if not is_valid_notification_binary_sensor(info):
return
# Get all sensors from Notification CC states # Get all sensors from Notification CC states
for state_key in info.primary_value.metadata.states: for state_key in info.primary_value.metadata.states:
# ignore idle key (0) # ignore idle key (0)
if state_key == "0": if state_key == "0":
continue continue
# get (optional) description for this state
notification_description: ( notification_description: (
NotificationZWaveJSEntityDescription | None NotificationZWaveJSEntityDescription | None
) = None ) = None
for description in NOTIFICATION_SENSOR_MAPPINGS: for description in NOTIFICATION_SENSOR_MAPPINGS:
if ( if (
int(description.key) int(description.key)
@ -289,7 +301,6 @@ async def async_setup_entry(
and notification_description.off_state == state_key and notification_description.off_state == state_key
): ):
continue continue
entities.append( entities.append(
ZWaveNotificationBinarySensor( ZWaveNotificationBinarySensor(
config_entry, driver, info, state_key, notification_description config_entry, driver, info, state_key, notification_description

View File

@ -80,7 +80,7 @@ def get_device_entities(
er.async_get(hass), device.id, include_disabled_entities=True er.async_get(hass), device.id, include_disabled_entities=True
) )
entities = [] entities = []
for entry in entity_entries: for entry in sorted(entity_entries):
# Skip entities that are not part of this integration # Skip entities that are not part of this integration
if entry.config_entry_id != config_entry.entry_id: if entry.config_entry_id != config_entry.entry_id:
continue continue

View File

@ -885,17 +885,6 @@ DISCOVERY_SCHEMAS = [
type={ValueType.BOOLEAN}, type={ValueType.BOOLEAN},
), ),
), ),
ZWaveDiscoverySchema(
platform=Platform.BINARY_SENSOR,
hint="notification",
primary_value=ZWaveValueDiscoverySchema(
command_class={
CommandClass.NOTIFICATION,
},
type={ValueType.NUMBER},
),
allow_multi=True,
),
# binary sensor for Indicator CC # binary sensor for Indicator CC
ZWaveDiscoverySchema( ZWaveDiscoverySchema(
platform=Platform.BINARY_SENSOR, platform=Platform.BINARY_SENSOR,
@ -957,19 +946,6 @@ DISCOVERY_SCHEMAS = [
), ),
data_template=NumericSensorDataTemplate(), data_template=NumericSensorDataTemplate(),
), ),
# special list sensors (Notification CC)
ZWaveDiscoverySchema(
platform=Platform.SENSOR,
hint="list_sensor",
primary_value=ZWaveValueDiscoverySchema(
command_class={
CommandClass.NOTIFICATION,
},
type={ValueType.NUMBER},
),
allow_multi=True,
entity_registry_enabled_default=False,
),
# number for Indicator CC (exclude property keys 3-5) # number for Indicator CC (exclude property keys 3-5)
ZWaveDiscoverySchema( ZWaveDiscoverySchema(
platform=Platform.NUMBER, platform=Platform.NUMBER,
@ -1196,6 +1172,7 @@ DISCOVERY_SCHEMAS = [
type={ValueType.NUMBER}, type={ValueType.NUMBER},
any_available_states={(0, "idle")}, any_available_states={(0, "idle")},
), ),
allow_multi=True,
), ),
# event # event
# stateful = False # stateful = False
@ -1218,6 +1195,43 @@ DISCOVERY_SCHEMAS = [
), ),
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
), ),
ZWaveDiscoverySchema(
platform=Platform.BINARY_SENSOR,
hint="notification",
primary_value=ZWaveValueDiscoverySchema(
command_class={
CommandClass.NOTIFICATION,
},
type={ValueType.NUMBER},
),
# set allow-multi to true because some of the notification sensors
# can not be mapped to a binary sensor and must be handled as a regular sensor
allow_multi=True,
),
# alarmType, alarmLevel (Notification CC)
ZWaveDiscoverySchema(
platform=Platform.SENSOR,
hint="notification_alarm",
primary_value=ZWaveValueDiscoverySchema(
command_class={
CommandClass.NOTIFICATION,
},
property={"alarmType", "alarmLevel"},
type={ValueType.NUMBER},
),
entity_registry_enabled_default=False,
),
# fallback sensors within Notification CC
ZWaveDiscoverySchema(
platform=Platform.SENSOR,
hint="notification",
primary_value=ZWaveValueDiscoverySchema(
command_class={
CommandClass.NOTIFICATION,
},
type={ValueType.NUMBER},
),
),
] ]
@ -1237,8 +1251,11 @@ def async_discover_single_value(
value: ZwaveValue, device: DeviceEntry, discovered_value_ids: dict[str, set[str]] value: ZwaveValue, device: DeviceEntry, discovered_value_ids: dict[str, set[str]]
) -> Generator[ZwaveDiscoveryInfo]: ) -> Generator[ZwaveDiscoveryInfo]:
"""Run discovery on a single ZWave value and return matching schema info.""" """Run discovery on a single ZWave value and return matching schema info."""
discovered_value_ids[device.id].add(value.value_id)
for schema in DISCOVERY_SCHEMAS: for schema in DISCOVERY_SCHEMAS:
# abort if attribute(s) already discovered
if value.value_id in discovered_value_ids[device.id]:
continue
# check manufacturer_id, product_id, product_type # check manufacturer_id, product_id, product_type
if ( if (
( (
@ -1342,10 +1359,9 @@ def async_discover_single_value(
entity_category=schema.entity_category, entity_category=schema.entity_category,
) )
# prevent re-discovery of the (primary) value if not allowed
if not schema.allow_multi: if not schema.allow_multi:
# return early since this value may not be discovered discovered_value_ids[device.id].add(value.value_id)
# by other schemas/platforms
return
if value.command_class == CommandClass.CONFIGURATION: if value.command_class == CommandClass.CONFIGURATION:
yield from async_discover_single_configuration_value( yield from async_discover_single_configuration_value(

View File

@ -51,6 +51,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import UNDEFINED, StateType from homeassistant.helpers.typing import UNDEFINED, StateType
from .binary_sensor import is_valid_notification_binary_sensor
from .const import ( from .const import (
ATTR_METER_TYPE, ATTR_METER_TYPE,
ATTR_METER_TYPE_NAME, ATTR_METER_TYPE_NAME,
@ -580,7 +581,10 @@ async def async_setup_entry(
data.unit_of_measurement, data.unit_of_measurement,
) )
) )
elif info.platform_hint == "list_sensor": elif info.platform_hint == "notification":
# prevent duplicate entities for values that are already represented as binary sensors
if is_valid_notification_binary_sensor(info):
return
entities.append( entities.append(
ZWaveListSensor(config_entry, driver, info, entity_description) ZWaveListSensor(config_entry, driver, info, entity_description)
) )

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,11 @@
"""Test the Z-Wave JS diagnostics.""" """Test the Z-Wave JS diagnostics."""
import copy import copy
from typing import Any, cast
from unittest.mock import patch from unittest.mock import patch
import pytest import pytest
from syrupy.assertion import SnapshotAssertion
from zwave_js_server.const import CommandClass from zwave_js_server.const import CommandClass
from zwave_js_server.event import Event from zwave_js_server.event import Event
from zwave_js_server.model.node import Node from zwave_js_server.model.node import Node
@ -13,7 +15,6 @@ from homeassistant.components.zwave_js.diagnostics import (
ZwaveValueMatcher, ZwaveValueMatcher,
async_get_device_diagnostics, async_get_device_diagnostics,
) )
from homeassistant.components.zwave_js.discovery import async_discover_node_values
from homeassistant.components.zwave_js.helpers import ( from homeassistant.components.zwave_js.helpers import (
get_device_id, get_device_id,
get_value_id_from_unique_id, get_value_id_from_unique_id,
@ -58,6 +59,7 @@ async def test_device_diagnostics(
integration, integration,
hass_client: ClientSessionGenerator, hass_client: ClientSessionGenerator,
version_state, version_state,
snapshot: SnapshotAssertion,
) -> None: ) -> None:
"""Test the device level diagnostics data dump.""" """Test the device level diagnostics data dump."""
device = device_registry.async_get_device( device = device_registry.async_get_device(
@ -113,18 +115,18 @@ async def test_device_diagnostics(
# Entities that are created outside of discovery (e.g. node status sensor and # Entities that are created outside of discovery (e.g. node status sensor and
# ping button) as well as helper entities created from other integrations should # ping button) as well as helper entities created from other integrations should
# not be in dump. # not be in dump.
assert len(diagnostics_data["entities"]) == len( assert diagnostics_data == snapshot
list(async_discover_node_values(multisensor_6, device, {device.id: set()}))
)
assert any( assert any(
entity.entity_id == "test.unrelated_entity" entity_entry.entity_id == "test.unrelated_entity"
for entity in er.async_entries_for_device(entity_registry, device.id) for entity_entry in er.async_entries_for_device(entity_registry, device.id)
) )
# Explicitly check that the entity that is not part of this config entry is not # Explicitly check that the entity that is not part of this config entry is not
# in the dump. # in the dump.
diagnostics_entities = cast(list[dict[str, Any]], diagnostics_data["entities"])
assert not any( assert not any(
entity["entity_id"] == "test.unrelated_entity" entity["entity_id"] == "test.unrelated_entity"
for entity in diagnostics_data["entities"] for entity in diagnostics_entities
) )
assert diagnostics_data["state"] == { assert diagnostics_data["state"] == {
**multisensor_6.data, **multisensor_6.data,
@ -171,6 +173,7 @@ async def test_device_diagnostics_missing_primary_value(
entity_id = "sensor.multisensor_6_air_temperature" entity_id = "sensor.multisensor_6_air_temperature"
entry = entity_registry.async_get(entity_id) entry = entity_registry.async_get(entity_id)
assert entry
# check that the primary value for the entity exists in the diagnostics # check that the primary value for the entity exists in the diagnostics
diagnostics_data = await get_diagnostics_for_device( diagnostics_data = await get_diagnostics_for_device(
@ -180,9 +183,8 @@ async def test_device_diagnostics_missing_primary_value(
value = multisensor_6.values.get(get_value_id_from_unique_id(entry.unique_id)) value = multisensor_6.values.get(get_value_id_from_unique_id(entry.unique_id))
assert value assert value
air_entity = next( diagnostics_entities = cast(list[dict[str, Any]], diagnostics_data["entities"])
x for x in diagnostics_data["entities"] if x["entity_id"] == entity_id air_entity = next(x for x in diagnostics_entities if x["entity_id"] == entity_id)
)
assert air_entity["value_id"] == value.value_id assert air_entity["value_id"] == value.value_id
assert air_entity["primary_value"] == { assert air_entity["primary_value"] == {
@ -218,9 +220,8 @@ async def test_device_diagnostics_missing_primary_value(
hass, hass_client, integration, device hass, hass_client, integration, device
) )
air_entity = next( diagnostics_entities = cast(list[dict[str, Any]], diagnostics_data["entities"])
x for x in diagnostics_data["entities"] if x["entity_id"] == entity_id air_entity = next(x for x in diagnostics_entities if x["entity_id"] == entity_id)
)
assert air_entity["value_id"] == value.value_id assert air_entity["value_id"] == value.value_id
assert air_entity["primary_value"] is None assert air_entity["primary_value"] is None
@ -266,5 +267,6 @@ async def test_device_diagnostics_secret_value(
diagnostics_data = await get_diagnostics_for_device( diagnostics_data = await get_diagnostics_for_device(
hass, hass_client, integration, device hass, hass_client, integration, device
) )
test_value = _find_ultraviolet_val(diagnostics_data["state"]) diagnostics_node_state = cast(dict[str, Any], diagnostics_data["state"])
test_value = _find_ultraviolet_val(diagnostics_node_state)
assert test_value["value"] == REDACTED assert test_value["value"] == REDACTED

View File

@ -1574,13 +1574,9 @@ async def test_disabled_entity_on_value_removed(
hass: HomeAssistant, entity_registry: er.EntityRegistry, zp3111, client, integration hass: HomeAssistant, entity_registry: er.EntityRegistry, zp3111, client, integration
) -> None: ) -> None:
"""Test that when entity primary values are removed the entity is removed.""" """Test that when entity primary values are removed the entity is removed."""
# re-enable this default-disabled entity
sensor_cover_entity = "sensor.4_in_1_sensor_home_security_cover_status"
idle_cover_status_button_entity = ( idle_cover_status_button_entity = (
"button.4_in_1_sensor_idle_home_security_cover_status" "button.4_in_1_sensor_idle_home_security_cover_status"
) )
entity_registry.async_update_entity(entity_id=sensor_cover_entity, disabled_by=None)
await hass.async_block_till_done()
# must reload the integration when enabling an entity # must reload the integration when enabling an entity
await hass.config_entries.async_unload(integration.entry_id) await hass.config_entries.async_unload(integration.entry_id)
@ -1591,10 +1587,6 @@ async def test_disabled_entity_on_value_removed(
await hass.async_block_till_done() await hass.async_block_till_done()
assert integration.state is ConfigEntryState.LOADED assert integration.state is ConfigEntryState.LOADED
state = hass.states.get(sensor_cover_entity)
assert state
assert state.state != STATE_UNAVAILABLE
state = hass.states.get(idle_cover_status_button_entity) state = hass.states.get(idle_cover_status_button_entity)
assert state assert state
assert state.state != STATE_UNAVAILABLE assert state.state != STATE_UNAVAILABLE
@ -1688,10 +1680,6 @@ async def test_disabled_entity_on_value_removed(
assert state assert state
assert state.state == STATE_UNAVAILABLE assert state.state == STATE_UNAVAILABLE
state = hass.states.get(sensor_cover_entity)
assert state
assert state.state == STATE_UNAVAILABLE
state = hass.states.get(idle_cover_status_button_entity) state = hass.states.get(idle_cover_status_button_entity)
assert state assert state
assert state.state == STATE_UNAVAILABLE assert state.state == STATE_UNAVAILABLE
@ -1707,7 +1695,6 @@ async def test_disabled_entity_on_value_removed(
| { | {
battery_level_entity, battery_level_entity,
binary_cover_entity, binary_cover_entity,
sensor_cover_entity,
idle_cover_status_button_entity, idle_cover_status_button_entity,
} }
== new_unavailable_entities == new_unavailable_entities

View File

@ -9,7 +9,6 @@ from zwave_js_server.exceptions import FailedZWaveCommand
from zwave_js_server.model.node import Node from zwave_js_server.model.node import Node
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
ATTR_OPTIONS,
ATTR_STATE_CLASS, ATTR_STATE_CLASS,
SensorDeviceClass, SensorDeviceClass,
SensorStateClass, SensorStateClass,
@ -54,7 +53,6 @@ from .common import (
ENERGY_SENSOR, ENERGY_SENSOR,
HUMIDITY_SENSOR, HUMIDITY_SENSOR,
METER_ENERGY_SENSOR, METER_ENERGY_SENSOR,
NOTIFICATION_MOTION_SENSOR,
POWER_SENSOR, POWER_SENSOR,
VOLTAGE_SENSOR, VOLTAGE_SENSOR,
) )
@ -227,60 +225,6 @@ async def test_basic_cc_sensor(
assert state.state == "255.0" assert state.state == "255.0"
async def test_disabled_notification_sensor(
hass: HomeAssistant, entity_registry: er.EntityRegistry, multisensor_6, integration
) -> None:
"""Test sensor is created from Notification CC and is disabled."""
entity_entry = entity_registry.async_get(NOTIFICATION_MOTION_SENSOR)
assert entity_entry
assert entity_entry.disabled
assert entity_entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION
# Test enabling entity
updated_entry = entity_registry.async_update_entity(
entity_entry.entity_id, disabled_by=None
)
assert updated_entry != entity_entry
assert updated_entry.disabled is False
# reload integration and check if entity is correctly there
await hass.config_entries.async_reload(integration.entry_id)
await hass.async_block_till_done()
state = hass.states.get(NOTIFICATION_MOTION_SENSOR)
assert state.state == "Motion detection"
assert state.attributes[ATTR_VALUE] == 8
assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.ENUM
assert state.attributes[ATTR_OPTIONS] == ["idle", "Motion detection"]
event = Event(
"value updated",
{
"source": "node",
"event": "value updated",
"nodeId": multisensor_6.node_id,
"args": {
"commandClassName": "Notification",
"commandClass": 113,
"endpoint": 0,
"property": "Home Security",
"propertyKey": "Motion sensor status",
"newValue": None,
"prevValue": 0,
"propertyName": "Home Security",
"propertyKeyName": "Motion sensor status",
},
},
)
multisensor_6.receive_event(event)
await hass.async_block_till_done()
state = hass.states.get(NOTIFICATION_MOTION_SENSOR)
assert state
assert state.state == STATE_UNKNOWN
async def test_config_parameter_sensor( async def test_config_parameter_sensor(
hass: HomeAssistant, hass: HomeAssistant,
entity_registry: er.EntityRegistry, entity_registry: er.EntityRegistry,