mirror of
https://github.com/home-assistant/core.git
synced 2026-08-14 01:02:51 +01:00
Improve Z-Wave heat alarm binary sensors discovery (#177486)
This commit is contained in:
@@ -10,6 +10,7 @@ from zwave_js_server.const.command_class.lock import DOOR_STATUS_PROPERTY
|
||||
from zwave_js_server.const.command_class.notification import (
|
||||
CC_SPECIFIC_NOTIFICATION_TYPE,
|
||||
AccessControlNotificationEvent,
|
||||
HeatAlarmNotificationEvent,
|
||||
NotificationEvent,
|
||||
NotificationType,
|
||||
PowerManagementNotificationEvent,
|
||||
@@ -76,6 +77,17 @@ NOTIFICATION_WEATHER = "16"
|
||||
NOTIFICATION_IRRIGATION = "17"
|
||||
NOTIFICATION_GAS = "18"
|
||||
|
||||
# Generic device classes where heat notifications represent primary
|
||||
# functionality rather than diagnostic.
|
||||
# https://github.com/zwave-js/backlog/issues/119#issuecomment-3096168116
|
||||
HEAT_PRIMARY_DEVICE_CLASSES: set[str | int] = {
|
||||
"Appliance",
|
||||
"Notification Sensor",
|
||||
"Thermostat",
|
||||
"Multilevel Sensor",
|
||||
"Alarm Sensor",
|
||||
}
|
||||
|
||||
# Deprecated/legacy synthetic Access Control door state notification
|
||||
# event IDs that don't exist in zwave-js-server
|
||||
ACCESS_CONTROL_DOOR_STATE_OPEN_REGULAR = 5632
|
||||
@@ -188,12 +200,6 @@ LEGACY_DOOR_STATE_REPAIR_ISSUE_KEYS = frozenset(
|
||||
# The catch all description should not have a device class and be marked as diagnostic.
|
||||
#
|
||||
# The following notifications have been moved to diagnostic:
|
||||
# Smoke Alarm
|
||||
# - Alarm silenced
|
||||
# - Replacement required
|
||||
# - Replacement required, End-of-life
|
||||
# - Maintenance required, planned periodic inspection
|
||||
# - Maintenance required, dust in device
|
||||
# CO Alarm
|
||||
# - Carbon monoxide test
|
||||
# - Replacement required
|
||||
@@ -206,16 +212,6 @@ LEGACY_DOOR_STATE_REPAIR_ISSUE_KEYS = frozenset(
|
||||
# - Replacement required, End-of-life
|
||||
# - Alarm silenced
|
||||
# - Maintenance required, planned periodic inspection
|
||||
# Heat Alarm
|
||||
# - Rapid temperature rise (location provided)
|
||||
# - Rapid temperature rise
|
||||
# - Rapid temperature fall (location provided)
|
||||
# - Rapid temperature fall
|
||||
# - Heat alarm test
|
||||
# - Alarm silenced
|
||||
# - Replacement required, End-of-life
|
||||
# - Maintenance required, dust in device
|
||||
# - Maintenance required, planned periodic inspection
|
||||
|
||||
# Water Alarm
|
||||
# - Replace water filter
|
||||
@@ -227,6 +223,7 @@ LEGACY_DOOR_STATE_REPAIR_ISSUE_KEYS = frozenset(
|
||||
MIGRATED_NOTIFICATION_TYPES = {
|
||||
NotificationType.SMOKE_ALARM,
|
||||
NotificationType.ACCESS_CONTROL,
|
||||
NotificationType.HEAT_ALARM,
|
||||
NotificationType.POWER_MANAGEMENT,
|
||||
}
|
||||
|
||||
@@ -267,24 +264,6 @@ NOTIFICATION_SENSOR_MAPPINGS: tuple[NotificationZWaveJSEntityDescription, ...] =
|
||||
key=NOTIFICATION_CARBON_DIOXIDE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
NotificationZWaveJSEntityDescription(
|
||||
# NotificationType 4: Heat - State Id's 1, 2, 5, 6 (heat/underheat)
|
||||
key=NOTIFICATION_HEAT,
|
||||
states={1, 2, 5, 6},
|
||||
device_class=BinarySensorDeviceClass.HEAT,
|
||||
),
|
||||
NotificationZWaveJSEntityDescription(
|
||||
# NotificationType 4: Heat - State ID's 8, A, B
|
||||
key=NOTIFICATION_HEAT,
|
||||
states={8, 10, 11},
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
NotificationZWaveJSEntityDescription(
|
||||
# NotificationType 4: Heat - All other State Id's
|
||||
key=NOTIFICATION_HEAT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
NotificationZWaveJSEntityDescription(
|
||||
# NotificationType 5: Water - State Id's 1, 2, 3, 4, 6, 7, 8, 9, 0A
|
||||
key=NOTIFICATION_WATER,
|
||||
@@ -1597,4 +1576,129 @@ DISCOVERY_SCHEMAS: list[NewZWaveDiscoverySchema] = [
|
||||
),
|
||||
entity_class=ZWaveNotificationBinarySensor,
|
||||
),
|
||||
NewZWaveDiscoverySchema(
|
||||
platform=Platform.BINARY_SENSOR,
|
||||
primary_value=ZWaveValueDiscoverySchema(
|
||||
command_class={
|
||||
CommandClass.NOTIFICATION,
|
||||
},
|
||||
type={ValueType.NUMBER},
|
||||
any_available_states_keys={
|
||||
HeatAlarmNotificationEvent.HEAT_SENSOR_STATUS_OVERHEAT_DETECTED_LOCATION_PROVIDED,
|
||||
HeatAlarmNotificationEvent.HEAT_SENSOR_STATUS_OVERHEAT_DETECTED,
|
||||
HeatAlarmNotificationEvent.HEAT_SENSOR_STATUS_UNDERHEAT_DETECTED_LOCATION_PROVIDED,
|
||||
HeatAlarmNotificationEvent.HEAT_SENSOR_STATUS_UNDERHEAT_DETECTED,
|
||||
},
|
||||
any_available_cc_specific={
|
||||
(CC_SPECIFIC_NOTIFICATION_TYPE, NotificationType.HEAT_ALARM)
|
||||
},
|
||||
),
|
||||
device_class_generic=HEAT_PRIMARY_DEVICE_CLASSES,
|
||||
allow_multi=True,
|
||||
entity_description=NotificationZWaveJSEntityDescription(
|
||||
# NotificationType 4: Heat - overheat/underheat detected (primary)
|
||||
key=NOTIFICATION_HEAT,
|
||||
states={
|
||||
HeatAlarmNotificationEvent.HEAT_SENSOR_STATUS_OVERHEAT_DETECTED_LOCATION_PROVIDED,
|
||||
HeatAlarmNotificationEvent.HEAT_SENSOR_STATUS_OVERHEAT_DETECTED,
|
||||
HeatAlarmNotificationEvent.HEAT_SENSOR_STATUS_UNDERHEAT_DETECTED_LOCATION_PROVIDED,
|
||||
HeatAlarmNotificationEvent.HEAT_SENSOR_STATUS_UNDERHEAT_DETECTED,
|
||||
},
|
||||
device_class=BinarySensorDeviceClass.HEAT,
|
||||
),
|
||||
entity_class=ZWaveNotificationBinarySensor,
|
||||
),
|
||||
NewZWaveDiscoverySchema(
|
||||
platform=Platform.BINARY_SENSOR,
|
||||
primary_value=ZWaveValueDiscoverySchema(
|
||||
command_class={
|
||||
CommandClass.NOTIFICATION,
|
||||
},
|
||||
type={ValueType.NUMBER},
|
||||
any_available_states_keys={
|
||||
HeatAlarmNotificationEvent.HEAT_SENSOR_STATUS_OVERHEAT_DETECTED_LOCATION_PROVIDED,
|
||||
HeatAlarmNotificationEvent.HEAT_SENSOR_STATUS_OVERHEAT_DETECTED,
|
||||
HeatAlarmNotificationEvent.HEAT_SENSOR_STATUS_UNDERHEAT_DETECTED_LOCATION_PROVIDED,
|
||||
HeatAlarmNotificationEvent.HEAT_SENSOR_STATUS_UNDERHEAT_DETECTED,
|
||||
},
|
||||
any_available_cc_specific={
|
||||
(CC_SPECIFIC_NOTIFICATION_TYPE, NotificationType.HEAT_ALARM)
|
||||
},
|
||||
),
|
||||
not_device_class_generic=HEAT_PRIMARY_DEVICE_CLASSES,
|
||||
allow_multi=True,
|
||||
entity_description=NotificationZWaveJSEntityDescription(
|
||||
# NotificationType 4: Heat - overheat/underheat detected (diagnostic)
|
||||
key=NOTIFICATION_HEAT,
|
||||
states={
|
||||
HeatAlarmNotificationEvent.HEAT_SENSOR_STATUS_OVERHEAT_DETECTED_LOCATION_PROVIDED,
|
||||
HeatAlarmNotificationEvent.HEAT_SENSOR_STATUS_OVERHEAT_DETECTED,
|
||||
HeatAlarmNotificationEvent.HEAT_SENSOR_STATUS_UNDERHEAT_DETECTED_LOCATION_PROVIDED,
|
||||
HeatAlarmNotificationEvent.HEAT_SENSOR_STATUS_UNDERHEAT_DETECTED,
|
||||
},
|
||||
device_class=BinarySensorDeviceClass.HEAT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
entity_class=ZWaveNotificationBinarySensor,
|
||||
),
|
||||
NewZWaveDiscoverySchema(
|
||||
platform=Platform.BINARY_SENSOR,
|
||||
primary_value=ZWaveValueDiscoverySchema(
|
||||
command_class={
|
||||
CommandClass.NOTIFICATION,
|
||||
},
|
||||
type={ValueType.NUMBER},
|
||||
any_available_states_keys={
|
||||
HeatAlarmNotificationEvent.MAINTENANCE_STATUS_REPLACEMENT_REQUIRED_END_OF_LIFE,
|
||||
HeatAlarmNotificationEvent.DUST_IN_DEVICE_STATUS_MAINTENANCE_REQUIRED_DUST_IN_DEVICE,
|
||||
HeatAlarmNotificationEvent.PERIODIC_INSPECTION_STATUS_MAINTENANCE_REQUIRED_PLANNED_PERIODIC_INSPECTION,
|
||||
},
|
||||
any_available_cc_specific={
|
||||
(CC_SPECIFIC_NOTIFICATION_TYPE, NotificationType.HEAT_ALARM)
|
||||
},
|
||||
),
|
||||
allow_multi=True,
|
||||
entity_description=NotificationZWaveJSEntityDescription(
|
||||
# NotificationType 4: Heat - replacement/dust/inspection
|
||||
key=NOTIFICATION_HEAT,
|
||||
states={
|
||||
HeatAlarmNotificationEvent.MAINTENANCE_STATUS_REPLACEMENT_REQUIRED_END_OF_LIFE,
|
||||
HeatAlarmNotificationEvent.DUST_IN_DEVICE_STATUS_MAINTENANCE_REQUIRED_DUST_IN_DEVICE,
|
||||
HeatAlarmNotificationEvent.PERIODIC_INSPECTION_STATUS_MAINTENANCE_REQUIRED_PLANNED_PERIODIC_INSPECTION,
|
||||
},
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
entity_class=ZWaveNotificationBinarySensor,
|
||||
),
|
||||
NewZWaveDiscoverySchema(
|
||||
platform=Platform.BINARY_SENSOR,
|
||||
primary_value=ZWaveValueDiscoverySchema(
|
||||
command_class={
|
||||
CommandClass.NOTIFICATION,
|
||||
},
|
||||
type={ValueType.NUMBER},
|
||||
any_available_cc_specific={
|
||||
(CC_SPECIFIC_NOTIFICATION_TYPE, NotificationType.HEAT_ALARM)
|
||||
},
|
||||
),
|
||||
allow_multi=True,
|
||||
entity_description=NotificationZWaveJSEntityDescription(
|
||||
# NotificationType 4: Heat - All other State Id's
|
||||
# Rapid rise and fall are events, and not states.
|
||||
key=NOTIFICATION_HEAT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
not_states={
|
||||
HeatAlarmNotificationEvent.IDLE,
|
||||
HeatAlarmNotificationEvent.HEAT_SENSOR_STATUS_OVERHEAT_DETECTED_LOCATION_PROVIDED,
|
||||
HeatAlarmNotificationEvent.HEAT_SENSOR_STATUS_OVERHEAT_DETECTED,
|
||||
HeatAlarmNotificationEvent.HEAT_SENSOR_STATUS_UNDERHEAT_DETECTED_LOCATION_PROVIDED,
|
||||
HeatAlarmNotificationEvent.HEAT_SENSOR_STATUS_UNDERHEAT_DETECTED,
|
||||
HeatAlarmNotificationEvent.MAINTENANCE_STATUS_REPLACEMENT_REQUIRED_END_OF_LIFE,
|
||||
HeatAlarmNotificationEvent.DUST_IN_DEVICE_STATUS_MAINTENANCE_REQUIRED_DUST_IN_DEVICE,
|
||||
HeatAlarmNotificationEvent.PERIODIC_INSPECTION_STATUS_MAINTENANCE_REQUIRED_PLANNED_PERIODIC_INSPECTION,
|
||||
},
|
||||
),
|
||||
entity_class=ZWaveNotificationBinarySensor,
|
||||
),
|
||||
]
|
||||
|
||||
@@ -110,6 +110,8 @@ class ZWaveDiscoverySchema:
|
||||
firmware_version_range: FirmwareVersionRange | None = None
|
||||
# [optional] the node's generic device class must match ANY of these values
|
||||
device_class_generic: set[str] | None = None
|
||||
# [optional] the node's or endpoint's generic device class must NOT match ANY of these values
|
||||
not_device_class_generic: set[str] | None = None
|
||||
# [optional] the node's specific device class must match ANY of these values
|
||||
device_class_specific: set[str] | None = None
|
||||
# [optional] additional values that ALL need to be present
|
||||
@@ -1323,6 +1325,29 @@ def async_discover_single_value(
|
||||
):
|
||||
continue
|
||||
|
||||
# check not_device_class_generic
|
||||
# Skip the schema if the endpoint's or the node's generic device class
|
||||
# matches any of the excluded values.
|
||||
if schema.not_device_class_generic and (
|
||||
(
|
||||
(endpoint := value.endpoint) is not None
|
||||
and (node_endpoint := value.node.endpoints.get(endpoint)) is not None
|
||||
and (device_class := node_endpoint.device_class) is not None
|
||||
and any(
|
||||
device_class.generic.label == val
|
||||
for val in schema.not_device_class_generic
|
||||
)
|
||||
)
|
||||
or (
|
||||
(device_class := value.node.device_class) is not None
|
||||
and any(
|
||||
device_class.generic.label == val
|
||||
for val in schema.not_device_class_generic
|
||||
)
|
||||
)
|
||||
):
|
||||
continue
|
||||
|
||||
# check device_class_specific
|
||||
# If the value has an endpoint but it is missing on the node
|
||||
# we can't match the endpoint device class to the schema device class.
|
||||
|
||||
@@ -191,6 +191,8 @@ class NewZWaveDiscoverySchema:
|
||||
device_class_basic: set[str | int] | None = None
|
||||
# [optional] the node's generic device class must match ANY of these values
|
||||
device_class_generic: set[str | int] | None = None
|
||||
# [optional] the node's or endpoint's generic device class must NOT match ANY of these values
|
||||
not_device_class_generic: set[str | int] | None = None
|
||||
# [optional] the node's specific device class must match ANY of these values
|
||||
device_class_specific: set[str | int] | None = None
|
||||
# [optional] additional values that ALL need to be present
|
||||
|
||||
@@ -212,6 +212,14 @@ def ring_keypad_state_fixture() -> dict[str, Any]:
|
||||
return load_json_object_fixture("ring_keypad_state.json", DOMAIN)
|
||||
|
||||
|
||||
@pytest.fixture(name="zooz_zac36_titan_valve_actuator_state")
|
||||
def zooz_zac36_titan_valve_actuator_state_fixture() -> dict[str, Any]:
|
||||
"""Load the Zooz ZAC36 Titan valve actuator node state fixture data."""
|
||||
return load_json_object_fixture(
|
||||
"zooz_zac36_titan_valve_actuator_state.json", DOMAIN
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(name="nortek_thermostat_state", scope="package")
|
||||
def nortek_thermostat_state_fixture() -> dict[str, Any]:
|
||||
"""Load the nortek thermostat node state fixture data."""
|
||||
@@ -978,6 +986,16 @@ def ring_keypad_fixture(client: MagicMock, ring_keypad_state: NodeDataType) -> N
|
||||
return node
|
||||
|
||||
|
||||
@pytest.fixture(name="zooz_zac36_titan_valve_actuator")
|
||||
def zooz_zac36_titan_valve_actuator_fixture(
|
||||
client: MagicMock, zooz_zac36_titan_valve_actuator_state: NodeDataType
|
||||
) -> Node:
|
||||
"""Mock a Zooz ZAC36 Titan valve actuator node."""
|
||||
node = Node(client, zooz_zac36_titan_valve_actuator_state)
|
||||
client.driver.controller.nodes[node.node_id] = node
|
||||
return node
|
||||
|
||||
|
||||
@pytest.fixture(name="integration")
|
||||
async def integration_fixture(
|
||||
hass: HomeAssistant,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -380,6 +380,45 @@ async def test_power_management_mains_disconnected_sensor(
|
||||
assert entity_entry.entity_category is EntityCategory.DIAGNOSTIC
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("zooz_zac36_titan_valve_actuator", "integration")
|
||||
@pytest.mark.parametrize(
|
||||
"entity_id",
|
||||
[
|
||||
"binary_sensor.titan_water_valve_actuator_overheat_detected",
|
||||
"binary_sensor.titan_water_valve_actuator_underheat_detected",
|
||||
],
|
||||
)
|
||||
async def test_heat_notification_sensor_diagnostic(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
entity_id: str,
|
||||
) -> None:
|
||||
"""Test overheat/underheat is diagnostic for non-heat-relevant device classes."""
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.attributes[ATTR_DEVICE_CLASS] == BinarySensorDeviceClass.HEAT
|
||||
|
||||
entity_entry = entity_registry.async_get(entity_id)
|
||||
assert entity_entry
|
||||
assert entity_entry.entity_category is EntityCategory.DIAGNOSTIC
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("climate_heatit_z_trm6", "integration")
|
||||
async def test_heat_notification_sensor_primary(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test overheat/underheat is primary for heat-relevant generic device classes."""
|
||||
entity_id = "binary_sensor.floor_thermostat_overheat_detected"
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.attributes[ATTR_DEVICE_CLASS] == BinarySensorDeviceClass.HEAT
|
||||
|
||||
entity_entry = entity_registry.async_get(entity_id)
|
||||
assert entity_entry
|
||||
assert entity_entry.entity_category is None
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("wallmote_central_scene", "integration")
|
||||
async def test_power_management_battery_charging_sensor(
|
||||
hass: HomeAssistant,
|
||||
|
||||
Reference in New Issue
Block a user