mirror of
https://github.com/home-assistant/core.git
synced 2026-04-02 00:20:30 +01:00
Convert Z-Wave Opening state to separate Open/Closed and Tilted sensors (#166635)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -17,18 +17,28 @@ from zwave_js_server.const.command_class.notification import (
|
||||
SmokeAlarmNotificationEvent,
|
||||
)
|
||||
from zwave_js_server.model.driver import Driver
|
||||
from zwave_js_server.model.value import Value as ZwaveValue
|
||||
|
||||
from homeassistant.components.automation import automations_with_entity
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DOMAIN as BINARY_SENSOR_DOMAIN,
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.components.script import scripts_with_entity
|
||||
from homeassistant.const import EntityCategory, Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.issue_registry import (
|
||||
IssueSeverity,
|
||||
async_create_issue,
|
||||
async_delete_issue,
|
||||
)
|
||||
from homeassistant.helpers.start import async_at_started
|
||||
|
||||
from .const import DOMAIN
|
||||
from .entity import NewZwaveDiscoveryInfo, ZWaveBaseEntity
|
||||
@@ -72,8 +82,7 @@ ACCESS_CONTROL_DOOR_STATE_OPEN_REGULAR = 5632
|
||||
ACCESS_CONTROL_DOOR_STATE_OPEN_TILT = 5633
|
||||
|
||||
|
||||
# Numeric State values used by the "Opening state" notification variable.
|
||||
# This is only needed temporarily until the legacy Access Control door state binary sensors are removed.
|
||||
# Numeric State values used by the Opening state notification variable.
|
||||
class OpeningState(IntEnum):
|
||||
"""Opening state values exposed by Access Control notifications."""
|
||||
|
||||
@@ -82,23 +91,23 @@ class OpeningState(IntEnum):
|
||||
TILTED = 2
|
||||
|
||||
|
||||
# parse_opening_state helpers for the DEPRECATED legacy Access Control binary sensors.
|
||||
def _legacy_is_closed(opening_state: OpeningState) -> bool:
|
||||
# parse_opening_state helpers.
|
||||
def _opening_state_is_closed(opening_state: OpeningState) -> bool:
|
||||
"""Return if Opening state represents closed."""
|
||||
return opening_state is OpeningState.CLOSED
|
||||
|
||||
|
||||
def _legacy_is_open(opening_state: OpeningState) -> bool:
|
||||
def _opening_state_is_open(opening_state: OpeningState) -> bool:
|
||||
"""Return if Opening state represents open."""
|
||||
return opening_state is OpeningState.OPEN
|
||||
|
||||
|
||||
def _legacy_is_open_or_tilted(opening_state: OpeningState) -> bool:
|
||||
def _opening_state_is_open_or_tilted(opening_state: OpeningState) -> bool:
|
||||
"""Return if Opening state represents open or tilted."""
|
||||
return opening_state in (OpeningState.OPEN, OpeningState.TILTED)
|
||||
|
||||
|
||||
def _legacy_is_tilted(opening_state: OpeningState) -> bool:
|
||||
def _opening_state_is_tilted(opening_state: OpeningState) -> bool:
|
||||
"""Return if Opening state represents tilted."""
|
||||
return opening_state is OpeningState.TILTED
|
||||
|
||||
@@ -127,12 +136,51 @@ class NewNotificationZWaveJSEntityDescription(BinarySensorEntityDescription):
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class OpeningStateZWaveJSEntityDescription(BinarySensorEntityDescription):
|
||||
"""Describe a legacy Access Control binary sensor that derives state from Opening state."""
|
||||
"""Describe an Access Control binary sensor that derives state from Opening state."""
|
||||
|
||||
state_key: int
|
||||
parse_opening_state: Callable[[OpeningState], bool]
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class LegacyDoorStateRepairDescription:
|
||||
"""Describe how a legacy door state entity should be migrated."""
|
||||
|
||||
issue_translation_key: str
|
||||
replacement_state_key: OpeningState
|
||||
|
||||
|
||||
LEGACY_DOOR_STATE_REPAIR_DESCRIPTIONS: dict[str, LegacyDoorStateRepairDescription] = {
|
||||
"legacy_access_control_door_state_simple_open": LegacyDoorStateRepairDescription(
|
||||
issue_translation_key="deprecated_legacy_door_open_state",
|
||||
replacement_state_key=OpeningState.OPEN,
|
||||
),
|
||||
"legacy_access_control_door_state_open": LegacyDoorStateRepairDescription(
|
||||
issue_translation_key="deprecated_legacy_door_open_state",
|
||||
replacement_state_key=OpeningState.OPEN,
|
||||
),
|
||||
"legacy_access_control_door_state_open_regular": LegacyDoorStateRepairDescription(
|
||||
issue_translation_key="deprecated_legacy_door_open_state",
|
||||
replacement_state_key=OpeningState.OPEN,
|
||||
),
|
||||
"legacy_access_control_door_state_open_tilt": LegacyDoorStateRepairDescription(
|
||||
issue_translation_key="deprecated_legacy_door_tilt_state",
|
||||
replacement_state_key=OpeningState.TILTED,
|
||||
),
|
||||
"legacy_access_control_door_tilt_state_tilted": LegacyDoorStateRepairDescription(
|
||||
issue_translation_key="deprecated_legacy_door_tilt_state",
|
||||
replacement_state_key=OpeningState.TILTED,
|
||||
),
|
||||
}
|
||||
|
||||
LEGACY_DOOR_STATE_REPAIR_ISSUE_KEYS = frozenset(
|
||||
{
|
||||
description.issue_translation_key
|
||||
for description in LEGACY_DOOR_STATE_REPAIR_DESCRIPTIONS.values()
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# Mappings for Notification sensors
|
||||
# https://github.com/zwave-js/specs/blob/master/Registries/Notification%20Command%20Class%2C%20list%20of%20assigned%20Notifications.xlsx
|
||||
#
|
||||
@@ -389,6 +437,9 @@ BOOLEAN_SENSOR_MAPPINGS: dict[tuple[int, int | str], BinarySensorEntityDescripti
|
||||
}
|
||||
|
||||
|
||||
# This can likely be removed once the legacy notification binary sensor
|
||||
# discovery path is gone and Opening state is handled only by the dedicated
|
||||
# discovery schemas below.
|
||||
@callback
|
||||
def is_valid_notification_binary_sensor(
|
||||
info: ZwaveDiscoveryInfo | NewZwaveDiscoveryInfo,
|
||||
@@ -396,13 +447,111 @@ def is_valid_notification_binary_sensor(
|
||||
"""Return if the notification CC Value is valid as binary sensor."""
|
||||
if not info.primary_value.metadata.states:
|
||||
return False
|
||||
# Access Control - Opening state is exposed as a single enum sensor instead
|
||||
# of fanning out one binary sensor per state.
|
||||
# Opening state is handled by dedicated discovery schemas
|
||||
if is_opening_state_notification_value(info.primary_value):
|
||||
return False
|
||||
return len(info.primary_value.metadata.states) > 1
|
||||
|
||||
|
||||
@callback
|
||||
def _async_delete_legacy_entity_repairs(hass: HomeAssistant, entity_id: str) -> None:
|
||||
"""Delete all stale legacy door state repair issues for an entity."""
|
||||
for issue_key in LEGACY_DOOR_STATE_REPAIR_ISSUE_KEYS:
|
||||
async_delete_issue(hass, DOMAIN, f"{issue_key}.{entity_id}")
|
||||
|
||||
|
||||
@callback
|
||||
def _async_check_legacy_entity_repair(
|
||||
hass: HomeAssistant,
|
||||
driver: Driver,
|
||||
entity: ZWaveLegacyDoorStateBinarySensor,
|
||||
) -> None:
|
||||
"""Schedule a repair issue check once HA has fully started."""
|
||||
|
||||
@callback
|
||||
def _async_do_check(hass: HomeAssistant) -> None:
|
||||
"""Create or delete a repair issue for a deprecated legacy door state entity."""
|
||||
ent_reg = er.async_get(hass)
|
||||
if entity.unique_id is None:
|
||||
return
|
||||
entity_id = ent_reg.async_get_entity_id(
|
||||
BINARY_SENSOR_DOMAIN, DOMAIN, entity.unique_id
|
||||
)
|
||||
if entity_id is None:
|
||||
return
|
||||
|
||||
repair_description = LEGACY_DOOR_STATE_REPAIR_DESCRIPTIONS.get(
|
||||
entity.entity_description.key
|
||||
)
|
||||
if repair_description is None:
|
||||
_async_delete_legacy_entity_repairs(hass, entity_id)
|
||||
return
|
||||
|
||||
entity_entry = ent_reg.async_get(entity_id)
|
||||
if entity_entry is None or entity_entry.disabled:
|
||||
_async_delete_legacy_entity_repairs(hass, entity_id)
|
||||
return
|
||||
|
||||
entity_automations = automations_with_entity(hass, entity_id)
|
||||
entity_scripts = scripts_with_entity(hass, entity_id)
|
||||
if not entity_automations and not entity_scripts:
|
||||
_async_delete_legacy_entity_repairs(hass, entity_id)
|
||||
return
|
||||
|
||||
opening_state_value = get_opening_state_notification_value(
|
||||
entity.info.node, entity.info.primary_value.endpoint
|
||||
)
|
||||
if opening_state_value is None:
|
||||
_async_delete_legacy_entity_repairs(hass, entity_id)
|
||||
return
|
||||
|
||||
replacement_unique_id = (
|
||||
f"{driver.controller.home_id}.{opening_state_value.value_id}."
|
||||
f"{repair_description.replacement_state_key}"
|
||||
)
|
||||
replacement_entity_id = ent_reg.async_get_entity_id(
|
||||
BINARY_SENSOR_DOMAIN, DOMAIN, replacement_unique_id
|
||||
)
|
||||
if replacement_entity_id is None:
|
||||
_async_delete_legacy_entity_repairs(hass, entity_id)
|
||||
return
|
||||
|
||||
items = []
|
||||
for domain, entity_ids in (
|
||||
("automation", entity_automations),
|
||||
("script", entity_scripts),
|
||||
):
|
||||
for eid in entity_ids:
|
||||
item = ent_reg.async_get(eid)
|
||||
if item:
|
||||
items.append(
|
||||
f"- [{item.name or item.original_name or eid}]"
|
||||
f"(/config/{domain}/edit/{item.unique_id})"
|
||||
)
|
||||
else:
|
||||
items.append(f"- {eid}")
|
||||
|
||||
async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
f"{repair_description.issue_translation_key}.{entity_id}",
|
||||
is_fixable=False,
|
||||
is_persistent=False,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key=repair_description.issue_translation_key,
|
||||
translation_placeholders={
|
||||
"entity_id": entity_id,
|
||||
"entity_name": (
|
||||
entity_entry.name or entity_entry.original_name or entity_id
|
||||
),
|
||||
"replacement_entity_id": replacement_entity_id,
|
||||
"items": "\n".join(items),
|
||||
},
|
||||
)
|
||||
|
||||
async_at_started(hass, _async_do_check)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ZwaveJSConfigEntry,
|
||||
@@ -442,13 +591,21 @@ async def async_setup_entry(
|
||||
and info.entity_class is ZWaveBooleanBinarySensor
|
||||
):
|
||||
entities.append(ZWaveBooleanBinarySensor(config_entry, driver, info))
|
||||
elif (
|
||||
isinstance(info, NewZwaveDiscoveryInfo)
|
||||
and info.entity_class is ZWaveOpeningStateBinarySensor
|
||||
and isinstance(
|
||||
info.entity_description, OpeningStateZWaveJSEntityDescription
|
||||
)
|
||||
):
|
||||
entities.append(ZWaveOpeningStateBinarySensor(config_entry, driver, info))
|
||||
elif (
|
||||
isinstance(info, NewZwaveDiscoveryInfo)
|
||||
and info.entity_class is ZWaveLegacyDoorStateBinarySensor
|
||||
):
|
||||
entities.append(
|
||||
ZWaveLegacyDoorStateBinarySensor(config_entry, driver, info)
|
||||
)
|
||||
entity = ZWaveLegacyDoorStateBinarySensor(config_entry, driver, info)
|
||||
entities.append(entity)
|
||||
_async_check_legacy_entity_repair(hass, driver, entity)
|
||||
elif isinstance(info, NewZwaveDiscoveryInfo):
|
||||
pass # other entity classes are not migrated yet
|
||||
elif info.platform_hint == "notification":
|
||||
@@ -632,6 +789,69 @@ class ZWaveLegacyDoorStateBinarySensor(ZWaveBaseEntity, BinarySensorEntity):
|
||||
return None
|
||||
|
||||
|
||||
class ZWaveOpeningStateBinarySensor(ZWaveBaseEntity, BinarySensorEntity):
|
||||
"""Representation of a binary sensor derived from Opening state."""
|
||||
|
||||
entity_description: OpeningStateZWaveJSEntityDescription
|
||||
_known_states: set[str]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config_entry: ZwaveJSConfigEntry,
|
||||
driver: Driver,
|
||||
info: NewZwaveDiscoveryInfo,
|
||||
) -> None:
|
||||
"""Initialize an Opening state binary sensor entity."""
|
||||
super().__init__(config_entry, driver, info)
|
||||
self._known_states = set(info.primary_value.metadata.states or ())
|
||||
self._attr_unique_id = (
|
||||
f"{self._attr_unique_id}.{self.entity_description.state_key}"
|
||||
)
|
||||
|
||||
@callback
|
||||
def should_rediscover_on_metadata_update(self) -> bool:
|
||||
"""Check if metadata states require adding the Tilt entity."""
|
||||
return (
|
||||
# Open and Tilt entities share the same underlying Opening state value.
|
||||
# Only let the main Open entity trigger rediscovery when Tilt first
|
||||
# appears so we can add the missing sibling without recreating the
|
||||
# main entity and losing its registry customizations.
|
||||
str(OpeningState.TILTED) not in self._known_states
|
||||
and str(OpeningState.TILTED)
|
||||
in set(self.info.primary_value.metadata.states or ())
|
||||
and self.entity_description.state_key == OpeningState.OPEN
|
||||
)
|
||||
|
||||
async def _async_remove_and_rediscover(self, value: ZwaveValue) -> None:
|
||||
"""Trigger re-discovery while preserving the main Opening state entity."""
|
||||
assert self.device_entry is not None
|
||||
controller_events = (
|
||||
self.config_entry.runtime_data.driver_events.controller_events
|
||||
)
|
||||
|
||||
# Unlike the base implementation, keep this entity in place so its
|
||||
# registry entry and user customizations survive metadata rediscovery.
|
||||
controller_events.discovered_value_ids[self.device_entry.id].discard(
|
||||
value.value_id
|
||||
)
|
||||
node_events = controller_events.node_events
|
||||
value_updates_disc_info = node_events.value_updates_disc_info[
|
||||
value.node.node_id
|
||||
]
|
||||
node_events.async_on_value_added(value_updates_disc_info, value)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return if the sensor is on or off."""
|
||||
value = self.info.primary_value.value
|
||||
if value is None:
|
||||
return None
|
||||
try:
|
||||
return self.entity_description.parse_opening_state(OpeningState(int(value)))
|
||||
except TypeError, ValueError:
|
||||
return None
|
||||
|
||||
|
||||
class ZWavePropertyBinarySensor(ZWaveBaseEntity, BinarySensorEntity):
|
||||
"""Representation of a Z-Wave binary_sensor from a property."""
|
||||
|
||||
@@ -730,11 +950,54 @@ DISCOVERY_SCHEMAS: list[NewZWaveDiscoverySchema] = [
|
||||
),
|
||||
entity_class=ZWaveNotificationBinarySensor,
|
||||
),
|
||||
NewZWaveDiscoverySchema(
|
||||
platform=Platform.BINARY_SENSOR,
|
||||
primary_value=ZWaveValueDiscoverySchema(
|
||||
command_class={CommandClass.NOTIFICATION},
|
||||
property={"Access Control"},
|
||||
property_key={"Opening state"},
|
||||
type={ValueType.NUMBER},
|
||||
any_available_states_keys={OpeningState.TILTED},
|
||||
any_available_cc_specific={
|
||||
(CC_SPECIFIC_NOTIFICATION_TYPE, NotificationType.ACCESS_CONTROL)
|
||||
},
|
||||
),
|
||||
# Also derive the main binary sensor from the same value ID
|
||||
allow_multi=True,
|
||||
entity_description=OpeningStateZWaveJSEntityDescription(
|
||||
key="access_control_opening_state_tilted",
|
||||
name="Tilt",
|
||||
state_key=OpeningState.TILTED,
|
||||
parse_opening_state=_opening_state_is_tilted,
|
||||
),
|
||||
entity_class=ZWaveOpeningStateBinarySensor,
|
||||
),
|
||||
NewZWaveDiscoverySchema(
|
||||
platform=Platform.BINARY_SENSOR,
|
||||
primary_value=ZWaveValueDiscoverySchema(
|
||||
command_class={CommandClass.NOTIFICATION},
|
||||
property={"Access Control"},
|
||||
property_key={"Opening state"},
|
||||
type={ValueType.NUMBER},
|
||||
any_available_states_keys={OpeningState.OPEN},
|
||||
any_available_cc_specific={
|
||||
(CC_SPECIFIC_NOTIFICATION_TYPE, NotificationType.ACCESS_CONTROL)
|
||||
},
|
||||
),
|
||||
entity_description=OpeningStateZWaveJSEntityDescription(
|
||||
key="access_control_opening_state_open",
|
||||
state_key=OpeningState.OPEN,
|
||||
parse_opening_state=_opening_state_is_open_or_tilted,
|
||||
device_class=BinarySensorDeviceClass.DOOR,
|
||||
),
|
||||
entity_class=ZWaveOpeningStateBinarySensor,
|
||||
),
|
||||
# -------------------------------------------------------------------
|
||||
# DEPRECATED legacy Access Control door/window binary sensors.
|
||||
# These schemas exist only for backwards compatibility with users who
|
||||
# already have these entities registered. New integrations should use
|
||||
# the Opening state enum sensor instead. Do not add new schemas here.
|
||||
# the dedicated Opening state binary sensors instead. Do not add new
|
||||
# schemas here.
|
||||
# All schemas below use ZWaveLegacyDoorStateBinarySensor and are
|
||||
# disabled by default (entity_registry_enabled_default=False).
|
||||
# -------------------------------------------------------------------
|
||||
@@ -758,7 +1021,7 @@ DISCOVERY_SCHEMAS: list[NewZWaveDiscoverySchema] = [
|
||||
key="legacy_access_control_door_state_simple_open",
|
||||
name="Window/door is open",
|
||||
state_key=AccessControlNotificationEvent.DOOR_STATE_WINDOW_DOOR_IS_OPEN,
|
||||
parse_opening_state=_legacy_is_open_or_tilted,
|
||||
parse_opening_state=_opening_state_is_open_or_tilted,
|
||||
device_class=BinarySensorDeviceClass.DOOR,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
@@ -784,7 +1047,7 @@ DISCOVERY_SCHEMAS: list[NewZWaveDiscoverySchema] = [
|
||||
key="legacy_access_control_door_state_simple_closed",
|
||||
name="Window/door is closed",
|
||||
state_key=AccessControlNotificationEvent.DOOR_STATE_WINDOW_DOOR_IS_CLOSED,
|
||||
parse_opening_state=_legacy_is_closed,
|
||||
parse_opening_state=_opening_state_is_closed,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
entity_class=ZWaveLegacyDoorStateBinarySensor,
|
||||
@@ -809,7 +1072,7 @@ DISCOVERY_SCHEMAS: list[NewZWaveDiscoverySchema] = [
|
||||
key="legacy_access_control_door_state_open",
|
||||
name="Window/door is open",
|
||||
state_key=AccessControlNotificationEvent.DOOR_STATE_WINDOW_DOOR_IS_OPEN,
|
||||
parse_opening_state=_legacy_is_open,
|
||||
parse_opening_state=_opening_state_is_open,
|
||||
device_class=BinarySensorDeviceClass.DOOR,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
@@ -835,7 +1098,7 @@ DISCOVERY_SCHEMAS: list[NewZWaveDiscoverySchema] = [
|
||||
key="legacy_access_control_door_state_closed",
|
||||
name="Window/door is closed",
|
||||
state_key=AccessControlNotificationEvent.DOOR_STATE_WINDOW_DOOR_IS_CLOSED,
|
||||
parse_opening_state=_legacy_is_closed,
|
||||
parse_opening_state=_opening_state_is_closed,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
entity_class=ZWaveLegacyDoorStateBinarySensor,
|
||||
@@ -858,7 +1121,7 @@ DISCOVERY_SCHEMAS: list[NewZWaveDiscoverySchema] = [
|
||||
key="legacy_access_control_door_state_open_regular",
|
||||
name="Window/door is open in regular position",
|
||||
state_key=ACCESS_CONTROL_DOOR_STATE_OPEN_REGULAR,
|
||||
parse_opening_state=_legacy_is_open,
|
||||
parse_opening_state=_opening_state_is_open,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
entity_class=ZWaveLegacyDoorStateBinarySensor,
|
||||
@@ -881,7 +1144,7 @@ DISCOVERY_SCHEMAS: list[NewZWaveDiscoverySchema] = [
|
||||
key="legacy_access_control_door_state_open_tilt",
|
||||
name="Window/door is open in tilt position",
|
||||
state_key=ACCESS_CONTROL_DOOR_STATE_OPEN_TILT,
|
||||
parse_opening_state=_legacy_is_tilted,
|
||||
parse_opening_state=_opening_state_is_tilted,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
entity_class=ZWaveLegacyDoorStateBinarySensor,
|
||||
@@ -904,7 +1167,7 @@ DISCOVERY_SCHEMAS: list[NewZWaveDiscoverySchema] = [
|
||||
key="legacy_access_control_door_tilt_state_tilted",
|
||||
name="Window/door is tilted",
|
||||
state_key=OpeningState.OPEN,
|
||||
parse_opening_state=_legacy_is_tilted,
|
||||
parse_opening_state=_opening_state_is_tilted,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
entity_class=ZWaveLegacyDoorStateBinarySensor,
|
||||
|
||||
@@ -303,6 +303,14 @@
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_legacy_door_open_state": {
|
||||
"description": "The binary sensor `{entity_id}` is deprecated because it has been replaced with the binary sensor `{replacement_entity_id}`.\n\nThe entity was found in the following automations or scripts:\n{items}\n\nPlease update the above automations or scripts to use the binary sensor `{replacement_entity_id}` and disable the binary sensor `{entity_id}` and then restart Home Assistant, to fix this issue.\n\nNote that `{replacement_entity_id}` is on when the door or window is open or tilted.",
|
||||
"title": "Deprecation: {entity_name}"
|
||||
},
|
||||
"deprecated_legacy_door_tilt_state": {
|
||||
"description": "The binary sensor `{entity_id}` is deprecated because it has been replaced with the binary sensor `{replacement_entity_id}`.\n\nThe entity was found in the following automations or scripts:\n{items}\n\nPlease update the above automations or scripts to use the binary sensor `{replacement_entity_id}` and disable the binary sensor `{entity_id}` and then restart Home Assistant, to fix this issue.\n\nNote that `{replacement_entity_id}` is on only when the door or window is tilted.",
|
||||
"title": "Deprecation: {entity_name}"
|
||||
},
|
||||
"device_config_file_changed": {
|
||||
"fix_flow": {
|
||||
"abort": {
|
||||
|
||||
@@ -9,7 +9,12 @@ import pytest
|
||||
from zwave_js_server.event import Event
|
||||
from zwave_js_server.model.node import Node
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
|
||||
from homeassistant.components import automation
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DOMAIN as BINARY_SENSOR_DOMAIN,
|
||||
BinarySensorDeviceClass,
|
||||
)
|
||||
from homeassistant.components.zwave_js.const import DOMAIN
|
||||
from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_CLASS,
|
||||
@@ -20,7 +25,9 @@ from homeassistant.const import (
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers import entity_registry as er, issue_registry as ir
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .common import (
|
||||
@@ -144,6 +151,22 @@ def _add_lock_state_notification_states(node_state: dict[str, Any]) -> dict[str,
|
||||
return updated_state
|
||||
|
||||
|
||||
def _set_opening_state_metadata_states(
|
||||
node_state: dict[str, Any], states: dict[str, str]
|
||||
) -> dict[str, Any]:
|
||||
"""Return a node state with updated Opening state metadata states."""
|
||||
updated_state = copy.deepcopy(node_state)
|
||||
for value_data in updated_state["values"]:
|
||||
if (
|
||||
value_data.get("commandClass") == 113
|
||||
and value_data.get("property") == "Access Control"
|
||||
and value_data.get("propertyKey") == "Opening state"
|
||||
):
|
||||
value_data["metadata"]["states"] = states
|
||||
break
|
||||
return updated_state
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def platforms() -> list[str]:
|
||||
"""Fixture to specify platforms to test."""
|
||||
@@ -418,12 +441,12 @@ async def test_property_sensor_door_status(
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
|
||||
async def test_opening_state_notification_does_not_create_binary_sensors(
|
||||
async def test_opening_state_creates_open_binary_sensor(
|
||||
hass: HomeAssistant,
|
||||
client,
|
||||
hoppe_ehandle_connectsense_state,
|
||||
) -> None:
|
||||
"""Test Opening state does not fan out into per-state binary sensors."""
|
||||
"""Test Opening state creates the Open binary sensor."""
|
||||
# The eHandle fixture has a Binary Sensor CC value for tilt, which we
|
||||
# want to ignore in the assertion below
|
||||
state = copy.deepcopy(hoppe_ehandle_connectsense_state)
|
||||
@@ -440,7 +463,12 @@ async def test_opening_state_notification_does_not_create_binary_sensors(
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert not hass.states.async_all("binary_sensor")
|
||||
open_state = hass.states.get("binary_sensor.ehandle_connectsense")
|
||||
assert open_state is not None
|
||||
assert open_state.state == STATE_OFF
|
||||
assert open_state.attributes[ATTR_DEVICE_CLASS] == BinarySensorDeviceClass.DOOR
|
||||
|
||||
assert hass.states.get("binary_sensor.ehandle_connectsense_tilt") is None
|
||||
|
||||
|
||||
async def test_opening_state_disables_legacy_window_door_notification_sensors(
|
||||
@@ -476,7 +504,7 @@ async def test_opening_state_disables_legacy_window_door_notification_sensors(
|
||||
}
|
||||
or (
|
||||
entry.original_name == "Window/door is tilted"
|
||||
and entry.original_device_class != BinarySensorDeviceClass.WINDOW
|
||||
and entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION
|
||||
)
|
||||
)
|
||||
]
|
||||
@@ -488,6 +516,162 @@ async def test_opening_state_disables_legacy_window_door_notification_sensors(
|
||||
)
|
||||
assert all(hass.states.get(entry.entity_id) is None for entry in legacy_entries)
|
||||
|
||||
open_state = hass.states.get("binary_sensor.ehandle_connectsense")
|
||||
assert open_state is not None
|
||||
assert open_state.state == STATE_OFF
|
||||
assert open_state.attributes[ATTR_DEVICE_CLASS] == BinarySensorDeviceClass.DOOR
|
||||
|
||||
|
||||
async def test_opening_state_binary_sensors_with_tilted(
|
||||
hass: HomeAssistant,
|
||||
client,
|
||||
hoppe_ehandle_connectsense_state,
|
||||
) -> None:
|
||||
"""Test Opening state creates Open and Tilt binary sensors when supported."""
|
||||
node = Node(
|
||||
client,
|
||||
_set_opening_state_metadata_states(
|
||||
hoppe_ehandle_connectsense_state,
|
||||
{"0": "Closed", "1": "Open", "2": "Tilted"},
|
||||
),
|
||||
)
|
||||
client.driver.controller.nodes[node.node_id] = node
|
||||
|
||||
entry = MockConfigEntry(domain="zwave_js", data={"url": "ws://test.org"})
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
open_entity_id = "binary_sensor.ehandle_connectsense"
|
||||
tilted_entity_id = "binary_sensor.ehandle_connectsense_tilt"
|
||||
|
||||
open_state = hass.states.get(open_entity_id)
|
||||
tilted_state = hass.states.get(tilted_entity_id)
|
||||
assert open_state is not None
|
||||
assert tilted_state is not None
|
||||
assert open_state.attributes[ATTR_DEVICE_CLASS] == BinarySensorDeviceClass.DOOR
|
||||
assert ATTR_DEVICE_CLASS not in tilted_state.attributes
|
||||
assert open_state.state == STATE_OFF
|
||||
assert tilted_state.state == STATE_OFF
|
||||
|
||||
node.receive_event(
|
||||
Event(
|
||||
type="value updated",
|
||||
data={
|
||||
"source": "node",
|
||||
"event": "value updated",
|
||||
"nodeId": node.node_id,
|
||||
"args": {
|
||||
"commandClassName": "Notification",
|
||||
"commandClass": 113,
|
||||
"endpoint": 0,
|
||||
"property": "Access Control",
|
||||
"propertyKey": "Opening state",
|
||||
"newValue": 1,
|
||||
"prevValue": 0,
|
||||
"propertyName": "Access Control",
|
||||
"propertyKeyName": "Opening state",
|
||||
},
|
||||
},
|
||||
)
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get(open_entity_id).state == STATE_ON
|
||||
assert hass.states.get(tilted_entity_id).state == STATE_OFF
|
||||
|
||||
node.receive_event(
|
||||
Event(
|
||||
type="value updated",
|
||||
data={
|
||||
"source": "node",
|
||||
"event": "value updated",
|
||||
"nodeId": node.node_id,
|
||||
"args": {
|
||||
"commandClassName": "Notification",
|
||||
"commandClass": 113,
|
||||
"endpoint": 0,
|
||||
"property": "Access Control",
|
||||
"propertyKey": "Opening state",
|
||||
"newValue": 2,
|
||||
"prevValue": 1,
|
||||
"propertyName": "Access Control",
|
||||
"propertyKeyName": "Opening state",
|
||||
},
|
||||
},
|
||||
)
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get(open_entity_id).state == STATE_ON
|
||||
assert hass.states.get(tilted_entity_id).state == STATE_ON
|
||||
|
||||
|
||||
async def test_opening_state_tilted_appears_via_metadata_update(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
client,
|
||||
hoppe_ehandle_connectsense_state,
|
||||
) -> None:
|
||||
"""Test tilt binary sensor is added without recreating the main entity."""
|
||||
node = Node(client, hoppe_ehandle_connectsense_state)
|
||||
client.driver.controller.nodes[node.node_id] = node
|
||||
|
||||
entry = MockConfigEntry(domain="zwave_js", data={"url": "ws://test.org"})
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
open_entity_id = "binary_sensor.ehandle_connectsense"
|
||||
tilted_entity_id = "binary_sensor.ehandle_connectsense_tilt"
|
||||
open_entry = entity_registry.async_get(open_entity_id)
|
||||
assert open_entry is not None
|
||||
|
||||
assert hass.states.get(open_entity_id) is not None
|
||||
assert hass.states.get(tilted_entity_id) is None
|
||||
|
||||
node.receive_event(
|
||||
Event(
|
||||
"metadata updated",
|
||||
{
|
||||
"source": "node",
|
||||
"event": "metadata updated",
|
||||
"nodeId": node.node_id,
|
||||
"args": {
|
||||
"commandClassName": "Notification",
|
||||
"commandClass": 113,
|
||||
"endpoint": 0,
|
||||
"property": "Access Control",
|
||||
"propertyKey": "Opening state",
|
||||
"propertyName": "Access Control",
|
||||
"propertyKeyName": "Opening state",
|
||||
"metadata": {
|
||||
"type": "number",
|
||||
"readable": True,
|
||||
"writeable": False,
|
||||
"label": "Opening state",
|
||||
"ccSpecific": {"notificationType": 6},
|
||||
"min": 0,
|
||||
"max": 255,
|
||||
"states": {
|
||||
"0": "Closed",
|
||||
"1": "Open",
|
||||
"2": "Tilted",
|
||||
},
|
||||
"stateful": True,
|
||||
"secret": False,
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get(open_entity_id) is not None
|
||||
tilted_state = hass.states.get(tilted_entity_id)
|
||||
assert tilted_state is not None
|
||||
assert entity_registry.async_get(open_entity_id) == open_entry
|
||||
|
||||
|
||||
async def test_reenabled_legacy_door_state_entity_follows_opening_state(
|
||||
hass: HomeAssistant,
|
||||
@@ -983,3 +1167,347 @@ async def test_hoppe_ehandle_connectsense(
|
||||
assert entry.original_name == "Window/door is tilted"
|
||||
assert entry.original_device_class == BinarySensorDeviceClass.WINDOW
|
||||
assert entry.disabled_by is None, "Entity should be enabled by default"
|
||||
|
||||
|
||||
async def test_legacy_door_open_state_repair_issue(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
client: MagicMock,
|
||||
hoppe_ehandle_connectsense_state: NodeDataType,
|
||||
) -> None:
|
||||
"""Test an open-state legacy entity creates the open-state repair issue."""
|
||||
node = Node(client, hoppe_ehandle_connectsense_state)
|
||||
client.driver.controller.nodes[node.node_id] = node
|
||||
home_id = client.driver.controller.home_id
|
||||
|
||||
entity_entry = entity_registry.async_get_or_create(
|
||||
BINARY_SENSOR_DOMAIN,
|
||||
DOMAIN,
|
||||
f"{home_id}.20-113-0-Access Control-Door state.22",
|
||||
suggested_object_id="ehandle_connectsense_window_door_is_open",
|
||||
original_name="Window/door is open",
|
||||
)
|
||||
entity_id = entity_entry.entity_id
|
||||
|
||||
entry = MockConfigEntry(domain="zwave_js", data={"url": "ws://test.org"})
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (
|
||||
issue_registry.async_get_issue(
|
||||
DOMAIN, f"deprecated_legacy_door_open_state.{entity_id}"
|
||||
)
|
||||
is None
|
||||
)
|
||||
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"id": "test_automation",
|
||||
"alias": "test",
|
||||
"trigger": {"platform": "state", "entity_id": entity_id},
|
||||
"action": {
|
||||
"action": "automation.turn_on",
|
||||
"target": {"entity_id": "automation.test_automation"},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
issue = issue_registry.async_get_issue(
|
||||
DOMAIN, f"deprecated_legacy_door_open_state.{entity_id}"
|
||||
)
|
||||
assert issue is not None
|
||||
assert issue.translation_key == "deprecated_legacy_door_open_state"
|
||||
assert issue.translation_placeholders["entity_id"] == entity_id
|
||||
assert issue.translation_placeholders["entity_name"] == "Window/door is open"
|
||||
assert (
|
||||
issue.translation_placeholders["replacement_entity_id"]
|
||||
== "binary_sensor.ehandle_connectsense"
|
||||
)
|
||||
assert "test" in issue.translation_placeholders["items"]
|
||||
|
||||
|
||||
async def test_legacy_door_tilt_state_repair_issue(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
client: MagicMock,
|
||||
hoppe_ehandle_connectsense_state: NodeDataType,
|
||||
) -> None:
|
||||
"""Test a tilt-state legacy entity creates the tilt-state repair issue."""
|
||||
node = Node(
|
||||
client,
|
||||
_set_opening_state_metadata_states(
|
||||
hoppe_ehandle_connectsense_state,
|
||||
{"0": "Closed", "1": "Open", "2": "Tilted"},
|
||||
),
|
||||
)
|
||||
client.driver.controller.nodes[node.node_id] = node
|
||||
home_id = client.driver.controller.home_id
|
||||
|
||||
entity_entry = entity_registry.async_get_or_create(
|
||||
BINARY_SENSOR_DOMAIN,
|
||||
DOMAIN,
|
||||
f"{home_id}.20-113-0-Access Control-Door state.5633",
|
||||
suggested_object_id="ehandle_connectsense_window_door_is_open_in_tilt_position",
|
||||
original_name="Window/door is open in tilt position",
|
||||
)
|
||||
entity_id = entity_entry.entity_id
|
||||
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"id": "test_automation",
|
||||
"alias": "test",
|
||||
"trigger": {"platform": "state", "entity_id": entity_id},
|
||||
"action": {
|
||||
"action": "automation.turn_on",
|
||||
"target": {"entity_id": "automation.test_automation"},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
entry = MockConfigEntry(domain="zwave_js", data={"url": "ws://test.org"})
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
issue = issue_registry.async_get_issue(
|
||||
DOMAIN, f"deprecated_legacy_door_tilt_state.{entity_id}"
|
||||
)
|
||||
assert issue is not None
|
||||
assert issue.translation_key == "deprecated_legacy_door_tilt_state"
|
||||
assert issue.translation_placeholders["entity_id"] == entity_id
|
||||
assert (
|
||||
issue.translation_placeholders["entity_name"]
|
||||
== "Window/door is open in tilt position"
|
||||
)
|
||||
assert (
|
||||
issue.translation_placeholders["replacement_entity_id"]
|
||||
== "binary_sensor.ehandle_connectsense_tilt"
|
||||
)
|
||||
assert "test" in issue.translation_placeholders["items"]
|
||||
|
||||
|
||||
async def test_legacy_door_open_state_no_repair_issue_when_disabled(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
client: MagicMock,
|
||||
hoppe_ehandle_connectsense_state: NodeDataType,
|
||||
) -> None:
|
||||
"""Test no repair issue is created when the legacy entity is disabled."""
|
||||
node = Node(client, hoppe_ehandle_connectsense_state)
|
||||
client.driver.controller.nodes[node.node_id] = node
|
||||
home_id = client.driver.controller.home_id
|
||||
|
||||
entity_entry = entity_registry.async_get_or_create(
|
||||
BINARY_SENSOR_DOMAIN,
|
||||
DOMAIN,
|
||||
f"{home_id}.20-113-0-Access Control-Door state.22",
|
||||
suggested_object_id="ehandle_connectsense_window_door_is_open",
|
||||
original_name="Window/door is open",
|
||||
disabled_by=er.RegistryEntryDisabler.INTEGRATION,
|
||||
)
|
||||
entity_id = entity_entry.entity_id
|
||||
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"id": "test_automation",
|
||||
"alias": "test",
|
||||
"trigger": {"platform": "state", "entity_id": entity_id},
|
||||
"action": {
|
||||
"action": "automation.turn_on",
|
||||
"target": {"entity_id": "automation.test_automation"},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
entry = MockConfigEntry(domain="zwave_js", data={"url": "ws://test.org"})
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (
|
||||
issue_registry.async_get_issue(
|
||||
DOMAIN, f"deprecated_legacy_door_open_state.{entity_id}"
|
||||
)
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
async def test_legacy_closed_door_state_does_not_create_repair_issue(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
client: MagicMock,
|
||||
hoppe_ehandle_connectsense_state: NodeDataType,
|
||||
) -> None:
|
||||
"""Test closed-state legacy entities are excluded from repair issues."""
|
||||
node = Node(client, hoppe_ehandle_connectsense_state)
|
||||
client.driver.controller.nodes[node.node_id] = node
|
||||
home_id = client.driver.controller.home_id
|
||||
|
||||
entity_entry = entity_registry.async_get_or_create(
|
||||
BINARY_SENSOR_DOMAIN,
|
||||
DOMAIN,
|
||||
f"{home_id}.20-113-0-Access Control-Door state.23",
|
||||
suggested_object_id="ehandle_connectsense_window_door_is_closed",
|
||||
original_name="Window/door is closed",
|
||||
)
|
||||
entity_id = entity_entry.entity_id
|
||||
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"id": "test_automation",
|
||||
"alias": "test",
|
||||
"trigger": {"platform": "state", "entity_id": entity_id},
|
||||
"action": {
|
||||
"action": "automation.turn_on",
|
||||
"target": {"entity_id": "automation.test_automation"},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
entry = MockConfigEntry(domain="zwave_js", data={"url": "ws://test.org"})
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (
|
||||
issue_registry.async_get_issue(
|
||||
DOMAIN, f"deprecated_legacy_door_open_state.{entity_id}"
|
||||
)
|
||||
is None
|
||||
)
|
||||
assert (
|
||||
issue_registry.async_get_issue(
|
||||
DOMAIN, f"deprecated_legacy_door_tilt_state.{entity_id}"
|
||||
)
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
async def test_hoppe_custom_tilt_sensor_no_repair_issue(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
client: MagicMock,
|
||||
hoppe_ehandle_connectsense_state: NodeDataType,
|
||||
) -> None:
|
||||
"""Test no repair issue for the custom Binary Sensor CC tilt entity."""
|
||||
node = Node(client, hoppe_ehandle_connectsense_state)
|
||||
client.driver.controller.nodes[node.node_id] = node
|
||||
home_id = client.driver.controller.home_id
|
||||
|
||||
entity_entry = entity_registry.async_get_or_create(
|
||||
BINARY_SENSOR_DOMAIN,
|
||||
DOMAIN,
|
||||
f"{home_id}.20-48-0-Tilt",
|
||||
suggested_object_id="ehandle_connectsense_window_door_is_tilted",
|
||||
original_name="Window/door is tilted",
|
||||
)
|
||||
entity_id = entity_entry.entity_id
|
||||
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"id": "test_automation",
|
||||
"alias": "test",
|
||||
"trigger": {"platform": "state", "entity_id": entity_id},
|
||||
"action": {
|
||||
"action": "automation.turn_on",
|
||||
"target": {"entity_id": "automation.test_automation"},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
entry = MockConfigEntry(domain="zwave_js", data={"url": "ws://test.org"})
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (
|
||||
issue_registry.async_get_issue(
|
||||
DOMAIN, f"deprecated_legacy_door_tilt_state.{entity_id}"
|
||||
)
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
async def test_legacy_door_open_state_stale_repair_issue_cleaned_up(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
client: MagicMock,
|
||||
hoppe_ehandle_connectsense_state: NodeDataType,
|
||||
) -> None:
|
||||
"""Test stale open-state repair issues are deleted when no references remain."""
|
||||
node = Node(client, hoppe_ehandle_connectsense_state)
|
||||
client.driver.controller.nodes[node.node_id] = node
|
||||
home_id = client.driver.controller.home_id
|
||||
|
||||
entity_entry = entity_registry.async_get_or_create(
|
||||
BINARY_SENSOR_DOMAIN,
|
||||
DOMAIN,
|
||||
f"{home_id}.20-113-0-Access Control-Door state.22",
|
||||
suggested_object_id="ehandle_connectsense_window_door_is_open",
|
||||
original_name="Window/door is open",
|
||||
)
|
||||
entity_id = entity_entry.entity_id
|
||||
|
||||
async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
f"deprecated_legacy_door_open_state.{entity_id}",
|
||||
is_fixable=False,
|
||||
is_persistent=False,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_legacy_door_open_state",
|
||||
translation_placeholders={
|
||||
"entity_id": entity_id,
|
||||
"entity_name": "Window/door is open",
|
||||
"replacement_entity_id": "binary_sensor.ehandle_connectsense",
|
||||
"items": "- [test](/config/automation/edit/test_automation)",
|
||||
},
|
||||
)
|
||||
assert (
|
||||
issue_registry.async_get_issue(
|
||||
DOMAIN, f"deprecated_legacy_door_open_state.{entity_id}"
|
||||
)
|
||||
is not None
|
||||
)
|
||||
|
||||
entry = MockConfigEntry(domain="zwave_js", data={"url": "ws://test.org"})
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (
|
||||
issue_registry.async_get_issue(
|
||||
DOMAIN, f"deprecated_legacy_door_open_state.{entity_id}"
|
||||
)
|
||||
is None
|
||||
)
|
||||
|
||||
@@ -11,7 +11,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,
|
||||
@@ -895,137 +894,6 @@ async def test_new_sensor_invalid_scale(
|
||||
mock_schedule_reload.assert_called_once_with(integration.entry_id)
|
||||
|
||||
|
||||
async def test_opening_state_sensor(
|
||||
hass: HomeAssistant,
|
||||
client,
|
||||
hoppe_ehandle_connectsense_state,
|
||||
) -> None:
|
||||
"""Test Opening state is exposed as an enum sensor."""
|
||||
node = Node(client, hoppe_ehandle_connectsense_state)
|
||||
client.driver.controller.nodes[node.node_id] = node
|
||||
|
||||
entry = MockConfigEntry(domain="zwave_js", data={"url": "ws://test.org"})
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("sensor.ehandle_connectsense_opening_state")
|
||||
assert state
|
||||
assert state.state == "Closed"
|
||||
assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.ENUM
|
||||
assert state.attributes[ATTR_OPTIONS] == ["Closed", "Open"]
|
||||
assert state.attributes[ATTR_VALUE] == 0
|
||||
|
||||
# Make sure we're not accidentally creating enum sensors for legacy
|
||||
# Door/Window notification variables.
|
||||
legacy_sensor_ids = [
|
||||
"sensor.ehandle_connectsense_door_state",
|
||||
"sensor.ehandle_connectsense_door_state_simple",
|
||||
]
|
||||
for entity_id in legacy_sensor_ids:
|
||||
assert hass.states.get(entity_id) is None
|
||||
|
||||
|
||||
async def test_opening_state_sensor_metadata_options_change(
|
||||
hass: HomeAssistant,
|
||||
hoppe_ehandle_connectsense: Node,
|
||||
integration: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test Opening state sensor is rediscovered when metadata options change."""
|
||||
entity_id = "sensor.ehandle_connectsense_opening_state"
|
||||
node = hoppe_ehandle_connectsense
|
||||
|
||||
# Verify initial state with 2 options
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == "Closed"
|
||||
assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.ENUM
|
||||
assert state.attributes[ATTR_OPTIONS] == ["Closed", "Open"]
|
||||
|
||||
# Simulate metadata update adding "Tilted" state
|
||||
event = Event(
|
||||
"metadata updated",
|
||||
{
|
||||
"source": "node",
|
||||
"event": "metadata updated",
|
||||
"nodeId": node.node_id,
|
||||
"args": {
|
||||
"commandClassName": "Notification",
|
||||
"commandClass": 113,
|
||||
"endpoint": 0,
|
||||
"property": "Access Control",
|
||||
"propertyKey": "Opening state",
|
||||
"propertyName": "Access Control",
|
||||
"propertyKeyName": "Opening state",
|
||||
"metadata": {
|
||||
"type": "number",
|
||||
"readable": True,
|
||||
"writeable": False,
|
||||
"label": "Opening state",
|
||||
"ccSpecific": {"notificationType": 6},
|
||||
"min": 0,
|
||||
"max": 255,
|
||||
"states": {
|
||||
"0": "Closed",
|
||||
"1": "Open",
|
||||
"2": "Tilted",
|
||||
},
|
||||
"stateful": True,
|
||||
"secret": False,
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
node.receive_event(event)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Entity should be rediscovered with 3 options
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.attributes[ATTR_OPTIONS] == ["Closed", "Open", "Tilted"]
|
||||
|
||||
# Simulate metadata update removing "Tilted" state
|
||||
event = Event(
|
||||
"metadata updated",
|
||||
{
|
||||
"source": "node",
|
||||
"event": "metadata updated",
|
||||
"nodeId": node.node_id,
|
||||
"args": {
|
||||
"commandClassName": "Notification",
|
||||
"commandClass": 113,
|
||||
"endpoint": 0,
|
||||
"property": "Access Control",
|
||||
"propertyKey": "Opening state",
|
||||
"propertyName": "Access Control",
|
||||
"propertyKeyName": "Opening state",
|
||||
"metadata": {
|
||||
"type": "number",
|
||||
"readable": True,
|
||||
"writeable": False,
|
||||
"label": "Opening state",
|
||||
"ccSpecific": {"notificationType": 6},
|
||||
"min": 0,
|
||||
"max": 255,
|
||||
"states": {
|
||||
"0": "Closed",
|
||||
"1": "Open",
|
||||
},
|
||||
"stateful": True,
|
||||
"secret": False,
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
node.receive_event(event)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Entity should be rediscovered with 2 options again
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.attributes[ATTR_OPTIONS] == ["Closed", "Open"]
|
||||
|
||||
|
||||
CONTROLLER_STATISTICS_ENTITY_PREFIX = "sensor.z_stick_gen5_usb_controller_"
|
||||
# controller statistics with initial state of 0
|
||||
CONTROLLER_STATISTICS_SUFFIXES = {
|
||||
|
||||
Reference in New Issue
Block a user