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>
pull/126725/head
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(
hass: HomeAssistant,
config_entry: ConfigEntry,
@ -264,16 +274,18 @@ async def async_setup_entry(
entities: list[BinarySensorEntity] = []
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
for state_key in info.primary_value.metadata.states:
# ignore idle key (0)
if state_key == "0":
continue
# get (optional) description for this state
notification_description: (
NotificationZWaveJSEntityDescription | None
) = None
for description in NOTIFICATION_SENSOR_MAPPINGS:
if (
int(description.key)
@ -289,7 +301,6 @@ async def async_setup_entry(
and notification_description.off_state == state_key
):
continue
entities.append(
ZWaveNotificationBinarySensor(
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
)
entities = []
for entry in entity_entries:
for entry in sorted(entity_entries):
# Skip entities that are not part of this integration
if entry.config_entry_id != config_entry.entry_id:
continue

View File

@ -885,17 +885,6 @@ DISCOVERY_SCHEMAS = [
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
ZWaveDiscoverySchema(
platform=Platform.BINARY_SENSOR,
@ -957,19 +946,6 @@ DISCOVERY_SCHEMAS = [
),
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)
ZWaveDiscoverySchema(
platform=Platform.NUMBER,
@ -1196,6 +1172,7 @@ DISCOVERY_SCHEMAS = [
type={ValueType.NUMBER},
any_available_states={(0, "idle")},
),
allow_multi=True,
),
# event
# stateful = False
@ -1218,6 +1195,43 @@ DISCOVERY_SCHEMAS = [
),
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]]
) -> Generator[ZwaveDiscoveryInfo]:
"""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:
# abort if attribute(s) already discovered
if value.value_id in discovered_value_ids[device.id]:
continue
# check manufacturer_id, product_id, product_type
if (
(
@ -1342,10 +1359,9 @@ def async_discover_single_value(
entity_category=schema.entity_category,
)
# prevent re-discovery of the (primary) value if not allowed
if not schema.allow_multi:
# return early since this value may not be discovered
# by other schemas/platforms
return
discovered_value_ids[device.id].add(value.value_id)
if value.command_class == CommandClass.CONFIGURATION:
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.typing import UNDEFINED, StateType
from .binary_sensor import is_valid_notification_binary_sensor
from .const import (
ATTR_METER_TYPE,
ATTR_METER_TYPE_NAME,
@ -580,7 +581,10 @@ async def async_setup_entry(
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(
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."""
import copy
from typing import Any, cast
from unittest.mock import patch
import pytest
from syrupy.assertion import SnapshotAssertion
from zwave_js_server.const import CommandClass
from zwave_js_server.event import Event
from zwave_js_server.model.node import Node
@ -13,7 +15,6 @@ from homeassistant.components.zwave_js.diagnostics import (
ZwaveValueMatcher,
async_get_device_diagnostics,
)
from homeassistant.components.zwave_js.discovery import async_discover_node_values
from homeassistant.components.zwave_js.helpers import (
get_device_id,
get_value_id_from_unique_id,
@ -58,6 +59,7 @@ async def test_device_diagnostics(
integration,
hass_client: ClientSessionGenerator,
version_state,
snapshot: SnapshotAssertion,
) -> None:
"""Test the device level diagnostics data dump."""
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
# ping button) as well as helper entities created from other integrations should
# not be in dump.
assert len(diagnostics_data["entities"]) == len(
list(async_discover_node_values(multisensor_6, device, {device.id: set()}))
)
assert diagnostics_data == snapshot
assert any(
entity.entity_id == "test.unrelated_entity"
for entity in er.async_entries_for_device(entity_registry, device.id)
entity_entry.entity_id == "test.unrelated_entity"
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
# in the dump.
diagnostics_entities = cast(list[dict[str, Any]], diagnostics_data["entities"])
assert not any(
entity["entity_id"] == "test.unrelated_entity"
for entity in diagnostics_data["entities"]
for entity in diagnostics_entities
)
assert diagnostics_data["state"] == {
**multisensor_6.data,
@ -171,6 +173,7 @@ async def test_device_diagnostics_missing_primary_value(
entity_id = "sensor.multisensor_6_air_temperature"
entry = entity_registry.async_get(entity_id)
assert entry
# check that the primary value for the entity exists in the diagnostics
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))
assert value
air_entity = next(
x for x in diagnostics_data["entities"] if x["entity_id"] == entity_id
)
diagnostics_entities = cast(list[dict[str, Any]], diagnostics_data["entities"])
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["primary_value"] == {
@ -218,9 +220,8 @@ async def test_device_diagnostics_missing_primary_value(
hass, hass_client, integration, device
)
air_entity = next(
x for x in diagnostics_data["entities"] if x["entity_id"] == entity_id
)
diagnostics_entities = cast(list[dict[str, Any]], diagnostics_data["entities"])
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["primary_value"] is None
@ -266,5 +267,6 @@ async def test_device_diagnostics_secret_value(
diagnostics_data = await get_diagnostics_for_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

View File

@ -1574,13 +1574,9 @@ async def test_disabled_entity_on_value_removed(
hass: HomeAssistant, entity_registry: er.EntityRegistry, zp3111, client, integration
) -> None:
"""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 = (
"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
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()
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)
assert state
assert state.state != STATE_UNAVAILABLE
@ -1688,10 +1680,6 @@ async def test_disabled_entity_on_value_removed(
assert state
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)
assert state
assert state.state == STATE_UNAVAILABLE
@ -1707,7 +1695,6 @@ async def test_disabled_entity_on_value_removed(
| {
battery_level_entity,
binary_cover_entity,
sensor_cover_entity,
idle_cover_status_button_entity,
}
== 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 homeassistant.components.sensor import (
ATTR_OPTIONS,
ATTR_STATE_CLASS,
SensorDeviceClass,
SensorStateClass,
@ -54,7 +53,6 @@ from .common import (
ENERGY_SENSOR,
HUMIDITY_SENSOR,
METER_ENERGY_SENSOR,
NOTIFICATION_MOTION_SENSOR,
POWER_SENSOR,
VOLTAGE_SENSOR,
)
@ -227,60 +225,6 @@ async def test_basic_cc_sensor(
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(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,