mirror of
https://github.com/home-assistant/core.git
synced 2026-03-16 16:02:06 +01:00
Compare commits
4 Commits
setpoint_c
...
scop-actio
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2294b7dfee | ||
|
|
30aec4d2ab | ||
|
|
335abd7002 | ||
|
|
3b3f0e9240 |
2
.github/workflows/builder.yml
vendored
2
.github/workflows/builder.yml
vendored
@@ -614,7 +614,7 @@ jobs:
|
||||
|
||||
- name: Generate artifact attestation
|
||||
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
||||
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
|
||||
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
|
||||
with:
|
||||
subject-name: ${{ env.HASSFEST_IMAGE_NAME }}
|
||||
subject-digest: ${{ steps.push.outputs.digest }}
|
||||
|
||||
@@ -13,6 +13,6 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["acme", "hass_nabucasa", "snitun"],
|
||||
"requirements": ["hass-nabucasa==1.15.0", "openai==2.21.0"],
|
||||
"requirements": ["hass-nabucasa==2.0.0", "openai==2.21.0"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -7,7 +7,12 @@ import aiohttp
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_TOKEN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.exceptions import (
|
||||
ConfigEntryAuthFailed,
|
||||
ConfigEntryNotReady,
|
||||
OAuth2TokenRequestError,
|
||||
OAuth2TokenRequestReauthError,
|
||||
)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
OAuth2Session,
|
||||
@@ -39,11 +44,11 @@ async def async_setup_entry(
|
||||
session = OAuth2Session(hass, entry, implementation)
|
||||
try:
|
||||
await session.async_ensure_token_valid()
|
||||
except aiohttp.ClientResponseError as err:
|
||||
if 400 <= err.status < 500:
|
||||
raise ConfigEntryAuthFailed(
|
||||
"OAuth session is not valid, reauth required"
|
||||
) from err
|
||||
except OAuth2TokenRequestReauthError as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
"OAuth session is not valid, reauth required"
|
||||
) from err
|
||||
except OAuth2TokenRequestError as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
except aiohttp.ClientError as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
@@ -140,14 +140,6 @@
|
||||
"pump_status": {
|
||||
"default": "mdi:pump"
|
||||
},
|
||||
"setpoint_change_source": {
|
||||
"default": "mdi:hand-back-right",
|
||||
"state": {
|
||||
"external": "mdi:webhook",
|
||||
"manual": "mdi:hand-back-right",
|
||||
"schedule": "mdi:calendar-clock"
|
||||
}
|
||||
},
|
||||
"tank_percentage": {
|
||||
"default": "mdi:water-boiler"
|
||||
},
|
||||
|
||||
@@ -183,13 +183,6 @@ EVSE_FAULT_STATE_MAP = {
|
||||
clusters.EnergyEvse.Enums.FaultStateEnum.kOther: "other",
|
||||
}
|
||||
|
||||
SETPOINT_CHANGE_SOURCE_MAP = {
|
||||
clusters.Thermostat.Enums.SetpointChangeSourceEnum.kManual: "manual",
|
||||
clusters.Thermostat.Enums.SetpointChangeSourceEnum.kSchedule: "schedule",
|
||||
clusters.Thermostat.Enums.SetpointChangeSourceEnum.kExternal: "external",
|
||||
clusters.Thermostat.Enums.SetpointChangeSourceEnum.kUnknownEnumValue: None,
|
||||
}
|
||||
|
||||
PUMP_CONTROL_MODE_MAP = {
|
||||
clusters.PumpConfigurationAndControl.Enums.ControlModeEnum.kConstantSpeed: "constant_speed",
|
||||
clusters.PumpConfigurationAndControl.Enums.ControlModeEnum.kConstantPressure: "constant_pressure",
|
||||
@@ -1586,48 +1579,4 @@ DISCOVERY_SCHEMAS = [
|
||||
required_attributes=(clusters.DoorLock.Attributes.DoorClosedEvents,),
|
||||
featuremap_contains=clusters.DoorLock.Bitmaps.Feature.kDoorPositionSensor,
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
entity_description=MatterSensorEntityDescription(
|
||||
key="SetpointChangeSource",
|
||||
translation_key="setpoint_change_source",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
state_class=None,
|
||||
options=[x for x in SETPOINT_CHANGE_SOURCE_MAP.values() if x is not None],
|
||||
device_to_ha=lambda x: SETPOINT_CHANGE_SOURCE_MAP[x],
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(clusters.Thermostat.Attributes.SetpointChangeSource,),
|
||||
device_type=(device_types.Thermostat, device_types.RoomAirConditioner),
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
entity_description=MatterSensorEntityDescription(
|
||||
key="SetpointChangeSourceTimestamp",
|
||||
translation_key="setpoint_change_timestamp",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
state_class=None,
|
||||
device_to_ha=matter_epoch_seconds_to_utc,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(
|
||||
clusters.Thermostat.Attributes.SetpointChangeSourceTimestamp,
|
||||
),
|
||||
device_type=(device_types.Thermostat, device_types.RoomAirConditioner),
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
entity_description=MatterSensorEntityDescription(
|
||||
key="ThermostatSetpointChangeAmount",
|
||||
translation_key="setpoint_change_amount",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
suggested_display_precision=1,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
device_to_ha=lambda x: x / TEMPERATURE_SCALING_FACTOR,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(clusters.Thermostat.Attributes.SetpointChangeAmount,),
|
||||
device_type=(device_types.Thermostat, device_types.RoomAirConditioner),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -555,20 +555,6 @@
|
||||
"rms_voltage": {
|
||||
"name": "Effective voltage"
|
||||
},
|
||||
"setpoint_change_amount": {
|
||||
"name": "Last change amount"
|
||||
},
|
||||
"setpoint_change_source": {
|
||||
"name": "Last change source",
|
||||
"state": {
|
||||
"external": "External",
|
||||
"manual": "Manual",
|
||||
"schedule": "Schedule"
|
||||
}
|
||||
},
|
||||
"setpoint_change_timestamp": {
|
||||
"name": "Last change"
|
||||
},
|
||||
"switch_current_position": {
|
||||
"name": "Current switch position"
|
||||
},
|
||||
|
||||
@@ -2,13 +2,16 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass, field
|
||||
from enum import IntEnum
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
from zwave_js_server.const import CommandClass
|
||||
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,
|
||||
NotificationEvent,
|
||||
NotificationType,
|
||||
SmokeAlarmNotificationEvent,
|
||||
@@ -29,6 +32,10 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .entity import NewZwaveDiscoveryInfo, ZWaveBaseEntity
|
||||
from .helpers import (
|
||||
get_opening_state_notification_value,
|
||||
is_opening_state_notification_value,
|
||||
)
|
||||
from .models import (
|
||||
NewZWaveDiscoverySchema,
|
||||
ValueType,
|
||||
@@ -59,6 +66,42 @@ NOTIFICATION_WEATHER = "16"
|
||||
NOTIFICATION_IRRIGATION = "17"
|
||||
NOTIFICATION_GAS = "18"
|
||||
|
||||
# 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
|
||||
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.
|
||||
class OpeningState(IntEnum):
|
||||
"""Opening state values exposed by Access Control notifications."""
|
||||
|
||||
CLOSED = 0
|
||||
OPEN = 1
|
||||
TILTED = 2
|
||||
|
||||
|
||||
# parse_opening_state helpers for the DEPRECATED legacy Access Control binary sensors.
|
||||
def _legacy_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:
|
||||
"""Return if Opening state represents open."""
|
||||
return opening_state is OpeningState.OPEN
|
||||
|
||||
|
||||
def _legacy_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:
|
||||
"""Return if Opening state represents tilted."""
|
||||
return opening_state is OpeningState.TILTED
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class NotificationZWaveJSEntityDescription(BinarySensorEntityDescription):
|
||||
@@ -82,6 +125,14 @@ class NewNotificationZWaveJSEntityDescription(BinarySensorEntityDescription):
|
||||
state_key: str
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class OpeningStateZWaveJSEntityDescription(BinarySensorEntityDescription):
|
||||
"""Describe a legacy Access Control binary sensor that derives state from Opening state."""
|
||||
|
||||
state_key: int
|
||||
parse_opening_state: Callable[[OpeningState], bool]
|
||||
|
||||
|
||||
# Mappings for Notification sensors
|
||||
# https://github.com/zwave-js/specs/blob/master/Registries/Notification%20Command%20Class%2C%20list%20of%20assigned%20Notifications.xlsx
|
||||
#
|
||||
@@ -127,6 +178,7 @@ class NewNotificationZWaveJSEntityDescription(BinarySensorEntityDescription):
|
||||
# to use the new discovery schema and we've removed the old discovery code.
|
||||
MIGRATED_NOTIFICATION_TYPES = {
|
||||
NotificationType.SMOKE_ALARM,
|
||||
NotificationType.ACCESS_CONTROL,
|
||||
}
|
||||
|
||||
NOTIFICATION_SENSOR_MAPPINGS: tuple[NotificationZWaveJSEntityDescription, ...] = (
|
||||
@@ -202,26 +254,6 @@ NOTIFICATION_SENSOR_MAPPINGS: tuple[NotificationZWaveJSEntityDescription, ...] =
|
||||
key=NOTIFICATION_WATER,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
NotificationZWaveJSEntityDescription(
|
||||
# NotificationType 6: Access Control - State Id's 1, 2, 3, 4 (Lock)
|
||||
key=NOTIFICATION_ACCESS_CONTROL,
|
||||
states={1, 2, 3, 4},
|
||||
device_class=BinarySensorDeviceClass.LOCK,
|
||||
),
|
||||
NotificationZWaveJSEntityDescription(
|
||||
# NotificationType 6: Access Control - State Id's 11 (Lock jammed)
|
||||
key=NOTIFICATION_ACCESS_CONTROL,
|
||||
states={11},
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
NotificationZWaveJSEntityDescription(
|
||||
# NotificationType 6: Access Control - State Id 22 (door/window open)
|
||||
key=NOTIFICATION_ACCESS_CONTROL,
|
||||
not_states={23},
|
||||
states={22},
|
||||
device_class=BinarySensorDeviceClass.DOOR,
|
||||
),
|
||||
NotificationZWaveJSEntityDescription(
|
||||
# NotificationType 7: Home Security - State Id's 1, 2 (intrusion)
|
||||
key=NOTIFICATION_HOME_SECURITY,
|
||||
@@ -364,6 +396,10 @@ 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.
|
||||
if is_opening_state_notification_value(info.primary_value):
|
||||
return False
|
||||
return len(info.primary_value.metadata.states) > 1
|
||||
|
||||
|
||||
@@ -406,6 +442,13 @@ 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 ZWaveLegacyDoorStateBinarySensor
|
||||
):
|
||||
entities.append(
|
||||
ZWaveLegacyDoorStateBinarySensor(config_entry, driver, info)
|
||||
)
|
||||
elif isinstance(info, NewZwaveDiscoveryInfo):
|
||||
pass # other entity classes are not migrated yet
|
||||
elif info.platform_hint == "notification":
|
||||
@@ -542,6 +585,51 @@ class ZWaveNotificationBinarySensor(ZWaveBaseEntity, BinarySensorEntity):
|
||||
return int(self.info.primary_value.value) == int(self.state_key)
|
||||
|
||||
|
||||
class ZWaveLegacyDoorStateBinarySensor(ZWaveBaseEntity, BinarySensorEntity):
|
||||
"""DEPRECATED: Legacy door state binary sensors.
|
||||
|
||||
These entities exist purely for backwards compatibility with users who had
|
||||
door state binary sensors before the Opening state value was introduced.
|
||||
They are disabled by default when the Opening state value is present and
|
||||
should not be extended. State is derived from the Opening state notification
|
||||
value using the parse_opening_state function defined on the entity description.
|
||||
"""
|
||||
|
||||
entity_description: OpeningStateZWaveJSEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config_entry: ZwaveJSConfigEntry,
|
||||
driver: Driver,
|
||||
info: NewZwaveDiscoveryInfo,
|
||||
) -> None:
|
||||
"""Initialize a legacy Door state binary sensor entity."""
|
||||
super().__init__(config_entry, driver, info)
|
||||
opening_state_value = get_opening_state_notification_value(self.info.node)
|
||||
assert opening_state_value is not None # guaranteed by required_values schema
|
||||
self._opening_state_value_id = opening_state_value.value_id
|
||||
self.watched_value_ids.add(opening_state_value.value_id)
|
||||
self._attr_unique_id = (
|
||||
f"{self._attr_unique_id}.{self.entity_description.state_key}"
|
||||
)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return if the sensor is on or off."""
|
||||
value = self.info.node.values.get(self._opening_state_value_id)
|
||||
if value is None:
|
||||
return None
|
||||
opening_state = value.value
|
||||
if opening_state is None:
|
||||
return None
|
||||
try:
|
||||
return self.entity_description.parse_opening_state(
|
||||
OpeningState(int(opening_state))
|
||||
)
|
||||
except TypeError, ValueError:
|
||||
return None
|
||||
|
||||
|
||||
class ZWavePropertyBinarySensor(ZWaveBaseEntity, BinarySensorEntity):
|
||||
"""Representation of a Z-Wave binary_sensor from a property."""
|
||||
|
||||
@@ -586,7 +674,392 @@ class ZWaveConfigParameterBinarySensor(ZWaveBooleanBinarySensor):
|
||||
)
|
||||
|
||||
|
||||
OPENING_STATE_NOTIFICATION_SCHEMA = ZWaveValueDiscoverySchema(
|
||||
command_class={CommandClass.NOTIFICATION},
|
||||
property={"Access Control"},
|
||||
property_key={"Opening state"},
|
||||
type={ValueType.NUMBER},
|
||||
any_available_cc_specific={
|
||||
(CC_SPECIFIC_NOTIFICATION_TYPE, NotificationType.ACCESS_CONTROL)
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
DISCOVERY_SCHEMAS: list[NewZWaveDiscoverySchema] = [
|
||||
NewZWaveDiscoverySchema(
|
||||
platform=Platform.BINARY_SENSOR,
|
||||
primary_value=ZWaveValueDiscoverySchema(
|
||||
command_class={CommandClass.NOTIFICATION},
|
||||
property={"Access Control"},
|
||||
property_key={"Lock state"},
|
||||
type={ValueType.NUMBER},
|
||||
any_available_states_keys={1, 2, 3, 4},
|
||||
any_available_cc_specific={
|
||||
(CC_SPECIFIC_NOTIFICATION_TYPE, NotificationType.ACCESS_CONTROL)
|
||||
},
|
||||
),
|
||||
allow_multi=True,
|
||||
entity_description=NotificationZWaveJSEntityDescription(
|
||||
# NotificationType 6: Access Control - State Id's 1, 2, 3, 4 (Lock)
|
||||
key=NOTIFICATION_ACCESS_CONTROL,
|
||||
states={1, 2, 3, 4},
|
||||
device_class=BinarySensorDeviceClass.LOCK,
|
||||
),
|
||||
entity_class=ZWaveNotificationBinarySensor,
|
||||
),
|
||||
NewZWaveDiscoverySchema(
|
||||
platform=Platform.BINARY_SENSOR,
|
||||
primary_value=ZWaveValueDiscoverySchema(
|
||||
command_class={CommandClass.NOTIFICATION},
|
||||
property={"Access Control"},
|
||||
property_key={"Lock state"},
|
||||
type={ValueType.NUMBER},
|
||||
any_available_states_keys={11},
|
||||
any_available_cc_specific={
|
||||
(CC_SPECIFIC_NOTIFICATION_TYPE, NotificationType.ACCESS_CONTROL)
|
||||
},
|
||||
),
|
||||
entity_description=NotificationZWaveJSEntityDescription(
|
||||
# NotificationType 6: Access Control - State Id's 11 (Lock jammed)
|
||||
key=NOTIFICATION_ACCESS_CONTROL,
|
||||
states={11},
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
entity_class=ZWaveNotificationBinarySensor,
|
||||
),
|
||||
# -------------------------------------------------------------------
|
||||
# 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.
|
||||
# All schemas below use ZWaveLegacyDoorStateBinarySensor and are
|
||||
# disabled by default (entity_registry_enabled_default=False).
|
||||
# -------------------------------------------------------------------
|
||||
NewZWaveDiscoverySchema(
|
||||
platform=Platform.BINARY_SENSOR,
|
||||
primary_value=ZWaveValueDiscoverySchema(
|
||||
command_class={CommandClass.NOTIFICATION},
|
||||
property={"Access Control"},
|
||||
property_key={"Door state (simple)"},
|
||||
type={ValueType.NUMBER},
|
||||
any_available_states_keys={
|
||||
AccessControlNotificationEvent.DOOR_STATE_WINDOW_DOOR_IS_OPEN
|
||||
},
|
||||
any_available_cc_specific={
|
||||
(CC_SPECIFIC_NOTIFICATION_TYPE, NotificationType.ACCESS_CONTROL)
|
||||
},
|
||||
),
|
||||
required_values=[OPENING_STATE_NOTIFICATION_SCHEMA],
|
||||
allow_multi=True,
|
||||
entity_description=OpeningStateZWaveJSEntityDescription(
|
||||
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,
|
||||
device_class=BinarySensorDeviceClass.DOOR,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
entity_class=ZWaveLegacyDoorStateBinarySensor,
|
||||
),
|
||||
NewZWaveDiscoverySchema(
|
||||
platform=Platform.BINARY_SENSOR,
|
||||
primary_value=ZWaveValueDiscoverySchema(
|
||||
command_class={CommandClass.NOTIFICATION},
|
||||
property={"Access Control"},
|
||||
property_key={"Door state (simple)"},
|
||||
type={ValueType.NUMBER},
|
||||
any_available_states_keys={
|
||||
AccessControlNotificationEvent.DOOR_STATE_WINDOW_DOOR_IS_CLOSED
|
||||
},
|
||||
any_available_cc_specific={
|
||||
(CC_SPECIFIC_NOTIFICATION_TYPE, NotificationType.ACCESS_CONTROL)
|
||||
},
|
||||
),
|
||||
required_values=[OPENING_STATE_NOTIFICATION_SCHEMA],
|
||||
allow_multi=True,
|
||||
entity_description=OpeningStateZWaveJSEntityDescription(
|
||||
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,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
entity_class=ZWaveLegacyDoorStateBinarySensor,
|
||||
),
|
||||
NewZWaveDiscoverySchema(
|
||||
platform=Platform.BINARY_SENSOR,
|
||||
primary_value=ZWaveValueDiscoverySchema(
|
||||
command_class={CommandClass.NOTIFICATION},
|
||||
property={"Access Control"},
|
||||
property_key={"Door state"},
|
||||
type={ValueType.NUMBER},
|
||||
any_available_states_keys={
|
||||
AccessControlNotificationEvent.DOOR_STATE_WINDOW_DOOR_IS_OPEN
|
||||
},
|
||||
any_available_cc_specific={
|
||||
(CC_SPECIFIC_NOTIFICATION_TYPE, NotificationType.ACCESS_CONTROL)
|
||||
},
|
||||
),
|
||||
required_values=[OPENING_STATE_NOTIFICATION_SCHEMA],
|
||||
allow_multi=True,
|
||||
entity_description=OpeningStateZWaveJSEntityDescription(
|
||||
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,
|
||||
device_class=BinarySensorDeviceClass.DOOR,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
entity_class=ZWaveLegacyDoorStateBinarySensor,
|
||||
),
|
||||
NewZWaveDiscoverySchema(
|
||||
platform=Platform.BINARY_SENSOR,
|
||||
primary_value=ZWaveValueDiscoverySchema(
|
||||
command_class={CommandClass.NOTIFICATION},
|
||||
property={"Access Control"},
|
||||
property_key={"Door state"},
|
||||
type={ValueType.NUMBER},
|
||||
any_available_states_keys={
|
||||
AccessControlNotificationEvent.DOOR_STATE_WINDOW_DOOR_IS_CLOSED
|
||||
},
|
||||
any_available_cc_specific={
|
||||
(CC_SPECIFIC_NOTIFICATION_TYPE, NotificationType.ACCESS_CONTROL)
|
||||
},
|
||||
),
|
||||
required_values=[OPENING_STATE_NOTIFICATION_SCHEMA],
|
||||
allow_multi=True,
|
||||
entity_description=OpeningStateZWaveJSEntityDescription(
|
||||
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,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
entity_class=ZWaveLegacyDoorStateBinarySensor,
|
||||
),
|
||||
NewZWaveDiscoverySchema(
|
||||
platform=Platform.BINARY_SENSOR,
|
||||
primary_value=ZWaveValueDiscoverySchema(
|
||||
command_class={CommandClass.NOTIFICATION},
|
||||
property={"Access Control"},
|
||||
property_key={"Door state"},
|
||||
type={ValueType.NUMBER},
|
||||
any_available_states_keys={ACCESS_CONTROL_DOOR_STATE_OPEN_REGULAR},
|
||||
any_available_cc_specific={
|
||||
(CC_SPECIFIC_NOTIFICATION_TYPE, NotificationType.ACCESS_CONTROL)
|
||||
},
|
||||
),
|
||||
required_values=[OPENING_STATE_NOTIFICATION_SCHEMA],
|
||||
allow_multi=True,
|
||||
entity_description=OpeningStateZWaveJSEntityDescription(
|
||||
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,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
entity_class=ZWaveLegacyDoorStateBinarySensor,
|
||||
),
|
||||
NewZWaveDiscoverySchema(
|
||||
platform=Platform.BINARY_SENSOR,
|
||||
primary_value=ZWaveValueDiscoverySchema(
|
||||
command_class={CommandClass.NOTIFICATION},
|
||||
property={"Access Control"},
|
||||
property_key={"Door state"},
|
||||
type={ValueType.NUMBER},
|
||||
any_available_states_keys={ACCESS_CONTROL_DOOR_STATE_OPEN_TILT},
|
||||
any_available_cc_specific={
|
||||
(CC_SPECIFIC_NOTIFICATION_TYPE, NotificationType.ACCESS_CONTROL)
|
||||
},
|
||||
),
|
||||
required_values=[OPENING_STATE_NOTIFICATION_SCHEMA],
|
||||
allow_multi=True,
|
||||
entity_description=OpeningStateZWaveJSEntityDescription(
|
||||
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,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
entity_class=ZWaveLegacyDoorStateBinarySensor,
|
||||
),
|
||||
NewZWaveDiscoverySchema(
|
||||
platform=Platform.BINARY_SENSOR,
|
||||
primary_value=ZWaveValueDiscoverySchema(
|
||||
command_class={CommandClass.NOTIFICATION},
|
||||
property={"Access Control"},
|
||||
property_key={"Door tilt state"},
|
||||
type={ValueType.NUMBER},
|
||||
any_available_states_keys={OpeningState.OPEN},
|
||||
any_available_cc_specific={
|
||||
(CC_SPECIFIC_NOTIFICATION_TYPE, NotificationType.ACCESS_CONTROL)
|
||||
},
|
||||
),
|
||||
required_values=[OPENING_STATE_NOTIFICATION_SCHEMA],
|
||||
allow_multi=True,
|
||||
entity_description=OpeningStateZWaveJSEntityDescription(
|
||||
key="legacy_access_control_door_tilt_state_tilted",
|
||||
name="Window/door is tilted",
|
||||
state_key=OpeningState.OPEN,
|
||||
parse_opening_state=_legacy_is_tilted,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
entity_class=ZWaveLegacyDoorStateBinarySensor,
|
||||
),
|
||||
# -------------------------------------------------------------------
|
||||
# Access Control door/window binary sensors for devices that do NOT have the
|
||||
# new "Opening state" notification value. These replace the old-style discovery
|
||||
# that used NOTIFICATION_SENSOR_MAPPINGS.
|
||||
#
|
||||
# Each property_key uses two schemas so that only the "open" state entity gets
|
||||
# device_class=DOOR, while the other state entities (e.g. "closed") do not.
|
||||
# The first schema uses allow_multi=True so it does not consume the value, allowing
|
||||
# the second schema to also match and create entities for the remaining states.
|
||||
NewZWaveDiscoverySchema(
|
||||
platform=Platform.BINARY_SENSOR,
|
||||
primary_value=ZWaveValueDiscoverySchema(
|
||||
command_class={CommandClass.NOTIFICATION},
|
||||
property={"Access Control"},
|
||||
property_key={"Door state (simple)"},
|
||||
type={ValueType.NUMBER},
|
||||
any_available_states_keys={
|
||||
AccessControlNotificationEvent.DOOR_STATE_WINDOW_DOOR_IS_OPEN
|
||||
},
|
||||
any_available_cc_specific={
|
||||
(CC_SPECIFIC_NOTIFICATION_TYPE, NotificationType.ACCESS_CONTROL)
|
||||
},
|
||||
),
|
||||
absent_values=[OPENING_STATE_NOTIFICATION_SCHEMA],
|
||||
allow_multi=True,
|
||||
entity_description=NotificationZWaveJSEntityDescription(
|
||||
key=NOTIFICATION_ACCESS_CONTROL,
|
||||
states={AccessControlNotificationEvent.DOOR_STATE_WINDOW_DOOR_IS_OPEN},
|
||||
device_class=BinarySensorDeviceClass.DOOR,
|
||||
),
|
||||
entity_class=ZWaveNotificationBinarySensor,
|
||||
),
|
||||
NewZWaveDiscoverySchema(
|
||||
platform=Platform.BINARY_SENSOR,
|
||||
primary_value=ZWaveValueDiscoverySchema(
|
||||
command_class={CommandClass.NOTIFICATION},
|
||||
property={"Access Control"},
|
||||
property_key={"Door state (simple)"},
|
||||
type={ValueType.NUMBER},
|
||||
any_available_states_keys={
|
||||
AccessControlNotificationEvent.DOOR_STATE_WINDOW_DOOR_IS_OPEN
|
||||
},
|
||||
any_available_cc_specific={
|
||||
(CC_SPECIFIC_NOTIFICATION_TYPE, NotificationType.ACCESS_CONTROL)
|
||||
},
|
||||
),
|
||||
absent_values=[OPENING_STATE_NOTIFICATION_SCHEMA],
|
||||
entity_description=NotificationZWaveJSEntityDescription(
|
||||
key=NOTIFICATION_ACCESS_CONTROL,
|
||||
not_states={AccessControlNotificationEvent.DOOR_STATE_WINDOW_DOOR_IS_OPEN},
|
||||
),
|
||||
entity_class=ZWaveNotificationBinarySensor,
|
||||
),
|
||||
NewZWaveDiscoverySchema(
|
||||
platform=Platform.BINARY_SENSOR,
|
||||
primary_value=ZWaveValueDiscoverySchema(
|
||||
command_class={CommandClass.NOTIFICATION},
|
||||
property={"Access Control"},
|
||||
property_key={"Door state"},
|
||||
type={ValueType.NUMBER},
|
||||
any_available_states_keys={
|
||||
AccessControlNotificationEvent.DOOR_STATE_WINDOW_DOOR_IS_OPEN
|
||||
},
|
||||
any_available_cc_specific={
|
||||
(CC_SPECIFIC_NOTIFICATION_TYPE, NotificationType.ACCESS_CONTROL)
|
||||
},
|
||||
),
|
||||
absent_values=[OPENING_STATE_NOTIFICATION_SCHEMA],
|
||||
allow_multi=True,
|
||||
entity_description=NotificationZWaveJSEntityDescription(
|
||||
key=NOTIFICATION_ACCESS_CONTROL,
|
||||
states={AccessControlNotificationEvent.DOOR_STATE_WINDOW_DOOR_IS_OPEN},
|
||||
device_class=BinarySensorDeviceClass.DOOR,
|
||||
),
|
||||
entity_class=ZWaveNotificationBinarySensor,
|
||||
),
|
||||
NewZWaveDiscoverySchema(
|
||||
platform=Platform.BINARY_SENSOR,
|
||||
primary_value=ZWaveValueDiscoverySchema(
|
||||
command_class={CommandClass.NOTIFICATION},
|
||||
property={"Access Control"},
|
||||
property_key={"Door state"},
|
||||
type={ValueType.NUMBER},
|
||||
any_available_states_keys={
|
||||
AccessControlNotificationEvent.DOOR_STATE_WINDOW_DOOR_IS_OPEN
|
||||
},
|
||||
any_available_cc_specific={
|
||||
(CC_SPECIFIC_NOTIFICATION_TYPE, NotificationType.ACCESS_CONTROL)
|
||||
},
|
||||
),
|
||||
absent_values=[OPENING_STATE_NOTIFICATION_SCHEMA],
|
||||
entity_description=NotificationZWaveJSEntityDescription(
|
||||
key=NOTIFICATION_ACCESS_CONTROL,
|
||||
not_states={AccessControlNotificationEvent.DOOR_STATE_WINDOW_DOOR_IS_OPEN},
|
||||
),
|
||||
entity_class=ZWaveNotificationBinarySensor,
|
||||
),
|
||||
NewZWaveDiscoverySchema(
|
||||
platform=Platform.BINARY_SENSOR,
|
||||
primary_value=ZWaveValueDiscoverySchema(
|
||||
command_class={CommandClass.NOTIFICATION},
|
||||
property={"Access Control"},
|
||||
property_key={"Door tilt state"},
|
||||
type={ValueType.NUMBER},
|
||||
any_available_states_keys={OpeningState.OPEN},
|
||||
any_available_cc_specific={
|
||||
(CC_SPECIFIC_NOTIFICATION_TYPE, NotificationType.ACCESS_CONTROL)
|
||||
},
|
||||
),
|
||||
absent_values=[OPENING_STATE_NOTIFICATION_SCHEMA],
|
||||
entity_description=NotificationZWaveJSEntityDescription(
|
||||
key=NOTIFICATION_ACCESS_CONTROL,
|
||||
states={OpeningState.OPEN},
|
||||
),
|
||||
entity_class=ZWaveNotificationBinarySensor,
|
||||
),
|
||||
NewZWaveDiscoverySchema(
|
||||
platform=Platform.BINARY_SENSOR,
|
||||
primary_value=ZWaveValueDiscoverySchema(
|
||||
command_class={CommandClass.NOTIFICATION},
|
||||
property={"Access Control"},
|
||||
type={ValueType.NUMBER},
|
||||
any_available_cc_specific={
|
||||
(CC_SPECIFIC_NOTIFICATION_TYPE, NotificationType.ACCESS_CONTROL)
|
||||
},
|
||||
),
|
||||
allow_multi=True,
|
||||
entity_description=NotificationZWaveJSEntityDescription(
|
||||
# NotificationType 6: Access Control - All other notification values.
|
||||
# not_states excludes states already handled by more specific schemas above,
|
||||
# so this catch-all only fires for genuinely unhandled property keys
|
||||
# (e.g. barrier, keypad, credential events).
|
||||
key=NOTIFICATION_ACCESS_CONTROL,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
not_states={
|
||||
0,
|
||||
# Lock state values (Lock state schemas consume the value when state 11 is
|
||||
# available, but may not when state 11 is absent)
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
11,
|
||||
# Door state (simple) / Door state values
|
||||
AccessControlNotificationEvent.DOOR_STATE_WINDOW_DOOR_IS_OPEN,
|
||||
AccessControlNotificationEvent.DOOR_STATE_WINDOW_DOOR_IS_CLOSED,
|
||||
ACCESS_CONTROL_DOOR_STATE_OPEN_REGULAR,
|
||||
ACCESS_CONTROL_DOOR_STATE_OPEN_TILT,
|
||||
},
|
||||
),
|
||||
entity_class=ZWaveNotificationBinarySensor,
|
||||
),
|
||||
# -------------------------------------------------------------------
|
||||
NewZWaveDiscoverySchema(
|
||||
# Hoppe eHandle ConnectSense (0x0313:0x0701:0x0002) - window tilt sensor.
|
||||
# The window tilt state is exposed as a binary sensor that is disabled by default
|
||||
|
||||
@@ -207,3 +207,7 @@ COVER_TILT_PROPERTY_KEYS: set[str | int | None] = {
|
||||
WindowCoveringPropertyKey.VERTICAL_SLATS_ANGLE,
|
||||
WindowCoveringPropertyKey.VERTICAL_SLATS_ANGLE_NO_POSITION,
|
||||
}
|
||||
|
||||
# notification
|
||||
NOTIFICATION_ACCESS_CONTROL_PROPERTY = "Access Control"
|
||||
OPENING_STATE_PROPERTY_KEY = "Opening state"
|
||||
|
||||
@@ -16,6 +16,10 @@ from zwave_js_server.const import (
|
||||
ConfigurationValueType,
|
||||
LogLevel,
|
||||
)
|
||||
from zwave_js_server.const.command_class.notification import (
|
||||
CC_SPECIFIC_NOTIFICATION_TYPE,
|
||||
NotificationType,
|
||||
)
|
||||
from zwave_js_server.model.controller import Controller, ProvisioningEntry
|
||||
from zwave_js_server.model.driver import Driver
|
||||
from zwave_js_server.model.log_config import LogConfig
|
||||
@@ -53,6 +57,8 @@ from .const import (
|
||||
DOMAIN,
|
||||
LIB_LOGGER,
|
||||
LOGGER,
|
||||
NOTIFICATION_ACCESS_CONTROL_PROPERTY,
|
||||
OPENING_STATE_PROPERTY_KEY,
|
||||
)
|
||||
from .models import ZwaveJSConfigEntry
|
||||
|
||||
@@ -126,6 +132,37 @@ def get_value_of_zwave_value(value: ZwaveValue | None) -> Any | None:
|
||||
return value.value if value else None
|
||||
|
||||
|
||||
def _get_notification_type(value: ZwaveValue) -> int | None:
|
||||
"""Return the notification type for a value, if available."""
|
||||
return value.metadata.cc_specific.get(CC_SPECIFIC_NOTIFICATION_TYPE)
|
||||
|
||||
|
||||
def is_opening_state_notification_value(value: ZwaveValue) -> bool:
|
||||
"""Return if the value is the Access Control Opening state notification."""
|
||||
if (
|
||||
value.command_class != CommandClass.NOTIFICATION
|
||||
or _get_notification_type(value) != NotificationType.ACCESS_CONTROL
|
||||
):
|
||||
return False
|
||||
|
||||
return (
|
||||
value.property_ == NOTIFICATION_ACCESS_CONTROL_PROPERTY
|
||||
and value.property_key == OPENING_STATE_PROPERTY_KEY
|
||||
)
|
||||
|
||||
|
||||
def get_opening_state_notification_value(node: ZwaveNode) -> ZwaveValue | None:
|
||||
"""Return the Access Control Opening state value for a node."""
|
||||
value_id = get_value_id_str(
|
||||
node,
|
||||
CommandClass.NOTIFICATION,
|
||||
NOTIFICATION_ACCESS_CONTROL_PROPERTY,
|
||||
None,
|
||||
OPENING_STATE_PROPERTY_KEY,
|
||||
)
|
||||
return node.values.get(value_id)
|
||||
|
||||
|
||||
async def async_enable_statistics(driver: Driver) -> None:
|
||||
"""Enable statistics on the driver."""
|
||||
await driver.async_enable_statistics("Home Assistant", HA_VERSION)
|
||||
|
||||
@@ -859,13 +859,22 @@ class ZWaveListSensor(ZwaveSensor):
|
||||
)
|
||||
|
||||
# Entity class attributes
|
||||
# Notification sensors have the following name mapping (variables are property
|
||||
# keys, name is property)
|
||||
# Notification sensors use the notification event label as the name
|
||||
# (property_key_name/metadata.label, falling back to property_name)
|
||||
# https://github.com/zwave-js/node-zwave-js/blob/master/packages/config/config/notifications.json
|
||||
self._attr_name = self.generate_name(
|
||||
alternate_value_name=self.info.primary_value.property_name,
|
||||
additional_info=[self.info.primary_value.property_key_name],
|
||||
)
|
||||
if info.platform_hint == "notification":
|
||||
self._attr_name = self.generate_name(
|
||||
alternate_value_name=(
|
||||
info.primary_value.property_key_name
|
||||
or info.primary_value.metadata.label
|
||||
or info.primary_value.property_name
|
||||
)
|
||||
)
|
||||
else:
|
||||
self._attr_name = self.generate_name(
|
||||
alternate_value_name=info.primary_value.property_name,
|
||||
additional_info=[info.primary_value.property_key_name],
|
||||
)
|
||||
if self.info.primary_value.metadata.states:
|
||||
self._attr_device_class = SensorDeviceClass.ENUM
|
||||
self._attr_options = list(info.primary_value.metadata.states.values())
|
||||
|
||||
@@ -37,7 +37,7 @@ fnv-hash-fast==1.6.0
|
||||
go2rtc-client==0.4.0
|
||||
ha-ffmpeg==3.2.2
|
||||
habluetooth==5.9.1
|
||||
hass-nabucasa==1.15.0
|
||||
hass-nabucasa==2.0.0
|
||||
hassil==3.5.0
|
||||
home-assistant-bluetooth==1.13.1
|
||||
home-assistant-frontend==20260304.0
|
||||
|
||||
@@ -51,7 +51,7 @@ dependencies = [
|
||||
"fnv-hash-fast==1.6.0",
|
||||
# hass-nabucasa is imported by helpers which don't depend on the cloud
|
||||
# integration
|
||||
"hass-nabucasa==1.15.0",
|
||||
"hass-nabucasa==2.0.0",
|
||||
# When bumping httpx, please check the version pins of
|
||||
# httpcore, anyio, and h11 in gen_requirements_all
|
||||
"httpx==0.28.1",
|
||||
|
||||
2
requirements.txt
generated
2
requirements.txt
generated
@@ -25,7 +25,7 @@ cronsim==2.7
|
||||
cryptography==46.0.5
|
||||
fnv-hash-fast==1.6.0
|
||||
ha-ffmpeg==3.2.2
|
||||
hass-nabucasa==1.15.0
|
||||
hass-nabucasa==2.0.0
|
||||
hassil==3.5.0
|
||||
home-assistant-bluetooth==1.13.1
|
||||
home-assistant-intents==2026.3.3
|
||||
|
||||
2
requirements_all.txt
generated
2
requirements_all.txt
generated
@@ -1176,7 +1176,7 @@ habluetooth==5.9.1
|
||||
hanna-cloud==0.0.7
|
||||
|
||||
# homeassistant.components.cloud
|
||||
hass-nabucasa==1.15.0
|
||||
hass-nabucasa==2.0.0
|
||||
|
||||
# homeassistant.components.splunk
|
||||
hass-splunk==0.1.4
|
||||
|
||||
2
requirements_test_all.txt
generated
2
requirements_test_all.txt
generated
@@ -1046,7 +1046,7 @@ habluetooth==5.9.1
|
||||
hanna-cloud==0.0.7
|
||||
|
||||
# homeassistant.components.cloud
|
||||
hass-nabucasa==1.15.0
|
||||
hass-nabucasa==2.0.0
|
||||
|
||||
# homeassistant.components.splunk
|
||||
hass-splunk==0.1.4
|
||||
|
||||
@@ -122,7 +122,6 @@ FORBIDDEN_PACKAGE_EXCEPTIONS: dict[str, dict[str, set[str]]] = {
|
||||
# pyblackbird > pyserial-asyncio
|
||||
"pyblackbird": {"pyserial-asyncio"}
|
||||
},
|
||||
"cloud": {"hass-nabucasa": {"async-timeout"}, "snitun": {"async-timeout"}},
|
||||
"cmus": {
|
||||
# https://github.com/mtreinish/pycmus/issues/4
|
||||
# pycmus > pbr > setuptools
|
||||
|
||||
@@ -4,7 +4,7 @@ from collections.abc import Awaitable, Callable, Coroutine
|
||||
import http
|
||||
import time
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from freezegun import freeze_time
|
||||
from gspread.exceptions import APIError
|
||||
@@ -29,7 +29,12 @@ from homeassistant.components.google_sheets.services import (
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.exceptions import (
|
||||
HomeAssistantError,
|
||||
OAuth2TokenRequestReauthError,
|
||||
OAuth2TokenRequestTransientError,
|
||||
ServiceValidationError,
|
||||
)
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
@@ -199,6 +204,64 @@ async def test_expired_token_refresh_failure(
|
||||
assert entries[0].state is expected_state
|
||||
|
||||
|
||||
async def test_setup_oauth_reauth_error(
|
||||
hass: HomeAssistant, config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test a token refresh reauth error puts the config entry in setup error state."""
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
assert await async_setup_component(hass, APPLICATION_CREDENTIALS_DOMAIN, {})
|
||||
await async_import_client_credential(
|
||||
hass,
|
||||
DOMAIN,
|
||||
ClientCredential("client-id", "client-secret"),
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
with (
|
||||
patch.object(config_entry, "async_start_reauth") as mock_async_start_reauth,
|
||||
patch(
|
||||
"homeassistant.components.google_sheets.OAuth2Session.async_ensure_token_valid",
|
||||
side_effect=OAuth2TokenRequestReauthError(
|
||||
domain=DOMAIN, request_info=Mock()
|
||||
),
|
||||
),
|
||||
):
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.SETUP_ERROR
|
||||
mock_async_start_reauth.assert_called_once_with(hass)
|
||||
|
||||
|
||||
async def test_setup_oauth_transient_error(
|
||||
hass: HomeAssistant, config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test a token refresh transient error sets the config entry to retry setup."""
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
assert await async_setup_component(hass, APPLICATION_CREDENTIALS_DOMAIN, {})
|
||||
await async_import_client_credential(
|
||||
hass,
|
||||
DOMAIN,
|
||||
ClientCredential("client-id", "client-secret"),
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.google_sheets.OAuth2Session.async_ensure_token_valid",
|
||||
side_effect=OAuth2TokenRequestTransientError(
|
||||
domain=DOMAIN, request_info=Mock()
|
||||
),
|
||||
):
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("add_created_column_param", "expected_row"),
|
||||
[
|
||||
|
||||
@@ -4075,171 +4075,6 @@
|
||||
'state': '100',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[eve_thermo_v5][sensor.eve_thermo_20ecd1701_last_change-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.eve_thermo_20ecd1701_last_change',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Last change',
|
||||
'platform': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'setpoint_change_timestamp',
|
||||
'unique_id': '00000000000004D2-000000000000000C-MatterNodeDevice-1-SetpointChangeSourceTimestamp-513-50',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[eve_thermo_v5][sensor.eve_thermo_20ecd1701_last_change-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'timestamp',
|
||||
'friendly_name': 'Eve Thermo 20ECD1701 Last change',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.eve_thermo_20ecd1701_last_change',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[eve_thermo_v5][sensor.eve_thermo_20ecd1701_last_change_amount-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.eve_thermo_20ecd1701_last_change_amount',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 1,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Last change amount',
|
||||
'platform': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'setpoint_change_amount',
|
||||
'unique_id': '00000000000004D2-000000000000000C-MatterNodeDevice-1-ThermostatSetpointChangeAmount-513-49',
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[eve_thermo_v5][sensor.eve_thermo_20ecd1701_last_change_amount-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'temperature',
|
||||
'friendly_name': 'Eve Thermo 20ECD1701 Last change amount',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.eve_thermo_20ecd1701_last_change_amount',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '0.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[eve_thermo_v5][sensor.eve_thermo_20ecd1701_last_change_source-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'manual',
|
||||
'schedule',
|
||||
'external',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.eve_thermo_20ecd1701_last_change_source',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Last change source',
|
||||
'platform': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'setpoint_change_source',
|
||||
'unique_id': '00000000000004D2-000000000000000C-MatterNodeDevice-1-SetpointChangeSource-513-48',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[eve_thermo_v5][sensor.eve_thermo_20ecd1701_last_change_source-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'enum',
|
||||
'friendly_name': 'Eve Thermo 20ECD1701 Last change source',
|
||||
'options': list([
|
||||
'manual',
|
||||
'schedule',
|
||||
'external',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.eve_thermo_20ecd1701_last_change_source',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'manual',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[eve_thermo_v5][sensor.eve_thermo_20ecd1701_temperature-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
@@ -11105,171 +10940,6 @@
|
||||
'state': '25',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[mock_thermostat][sensor.mock_thermostat_last_change-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.mock_thermostat_last_change',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Last change',
|
||||
'platform': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'setpoint_change_timestamp',
|
||||
'unique_id': '00000000000004D2-0000000000000096-MatterNodeDevice-1-SetpointChangeSourceTimestamp-513-50',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[mock_thermostat][sensor.mock_thermostat_last_change-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'timestamp',
|
||||
'friendly_name': 'Mock Thermostat Last change',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.mock_thermostat_last_change',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '2025-01-01T00:00:00+00:00',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[mock_thermostat][sensor.mock_thermostat_last_change_amount-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.mock_thermostat_last_change_amount',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 1,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Last change amount',
|
||||
'platform': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'setpoint_change_amount',
|
||||
'unique_id': '00000000000004D2-0000000000000096-MatterNodeDevice-1-ThermostatSetpointChangeAmount-513-49',
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[mock_thermostat][sensor.mock_thermostat_last_change_amount-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'temperature',
|
||||
'friendly_name': 'Mock Thermostat Last change amount',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.mock_thermostat_last_change_amount',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '1.5',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[mock_thermostat][sensor.mock_thermostat_last_change_source-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'manual',
|
||||
'schedule',
|
||||
'external',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.mock_thermostat_last_change_source',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Last change source',
|
||||
'platform': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'setpoint_change_source',
|
||||
'unique_id': '00000000000004D2-0000000000000096-MatterNodeDevice-1-SetpointChangeSource-513-48',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[mock_thermostat][sensor.mock_thermostat_last_change_source-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'enum',
|
||||
'friendly_name': 'Mock Thermostat Last change source',
|
||||
'options': list([
|
||||
'manual',
|
||||
'schedule',
|
||||
'external',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.mock_thermostat_last_change_source',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'manual',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[mock_thermostat][sensor.mock_thermostat_outdoor_temperature-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
|
||||
@@ -233,100 +233,6 @@ async def test_eve_thermo_sensor(
|
||||
assert state.state == "18.0"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("node_fixture", ["eve_thermo_v5"])
|
||||
async def test_eve_thermo_v5_setpoint_change_source(
|
||||
hass: HomeAssistant,
|
||||
matter_client: MagicMock,
|
||||
matter_node: MatterNode,
|
||||
) -> None:
|
||||
"""Test Eve Thermo v5 SetpointChangeSource sensor."""
|
||||
entity_id = "sensor.eve_thermo_20ecd1701_last_change_source"
|
||||
|
||||
# Initial state and options
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == "manual"
|
||||
assert state.attributes["options"] == ["manual", "schedule", "external"]
|
||||
|
||||
# Change to schedule
|
||||
set_node_attribute(matter_node, 1, 513, 48, 1)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == "schedule"
|
||||
|
||||
# Change to external
|
||||
set_node_attribute(matter_node, 1, 513, 48, 2)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == "external"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("node_fixture", ["eve_thermo_v5"])
|
||||
async def test_eve_thermo_v5_setpoint_change_timestamp(
|
||||
hass: HomeAssistant,
|
||||
matter_client: MagicMock,
|
||||
matter_node: MatterNode,
|
||||
) -> None:
|
||||
"""Test Eve Thermo v5 SetpointChangeSourceTimestamp sensor."""
|
||||
entity_id = "sensor.eve_thermo_20ecd1701_last_change"
|
||||
|
||||
# Initial is unknown per snapshot
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == "unknown"
|
||||
|
||||
# Update to 2024-01-01 00:00:00+00:00 (Matter epoch seconds since 2000)
|
||||
set_node_attribute(matter_node, 1, 513, 50, 757382400)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == "2024-01-01T00:00:00+00:00"
|
||||
|
||||
# Set to zero should yield unknown
|
||||
set_node_attribute(matter_node, 1, 513, 50, 0)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == "unknown"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("node_fixture", ["eve_thermo_v5"])
|
||||
async def test_eve_thermo_v5_setpoint_change_amount(
|
||||
hass: HomeAssistant,
|
||||
matter_client: MagicMock,
|
||||
matter_node: MatterNode,
|
||||
) -> None:
|
||||
"""Test Eve Thermo v5 SetpointChangeAmount sensor."""
|
||||
entity_id = "sensor.eve_thermo_20ecd1701_last_change_amount"
|
||||
|
||||
# Initial per snapshot
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == "0.0"
|
||||
|
||||
# Update to 2.0°C (200 in Matter units)
|
||||
set_node_attribute(matter_node, 1, 513, 49, 200)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == "2.0"
|
||||
|
||||
# Update to -0.5°C (-50 in Matter units)
|
||||
set_node_attribute(matter_node, 1, 513, 49, -50)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == "-0.5"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("node_fixture", ["longan_link_thermostat"])
|
||||
async def test_thermostat_outdoor(
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -105,6 +105,33 @@
|
||||
},
|
||||
"value": false
|
||||
},
|
||||
{
|
||||
"commandClass": 113,
|
||||
"commandClassName": "Notification",
|
||||
"property": "Access Control",
|
||||
"propertyKey": "Opening state",
|
||||
"propertyName": "Access Control",
|
||||
"propertyKeyName": "Opening state",
|
||||
"ccVersion": 8,
|
||||
"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
|
||||
},
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"commandClass": 113,
|
||||
"commandClassName": "Notification",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"""Test the Z-Wave JS binary sensor platform."""
|
||||
|
||||
import copy
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from zwave_js_server.event import Event
|
||||
@@ -31,6 +33,94 @@ from .common import (
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
|
||||
def _add_door_tilt_state_value(node_state: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Return a node state with a Door tilt state notification value added."""
|
||||
updated_state = copy.deepcopy(node_state)
|
||||
updated_state["values"].append(
|
||||
{
|
||||
"commandClass": 113,
|
||||
"commandClassName": "Notification",
|
||||
"property": "Access Control",
|
||||
"propertyKey": "Door tilt state",
|
||||
"propertyName": "Access Control",
|
||||
"propertyKeyName": "Door tilt state",
|
||||
"ccVersion": 8,
|
||||
"metadata": {
|
||||
"type": "number",
|
||||
"readable": True,
|
||||
"writeable": False,
|
||||
"label": "Door tilt state",
|
||||
"ccSpecific": {"notificationType": 6},
|
||||
"min": 0,
|
||||
"max": 255,
|
||||
"states": {
|
||||
"0": "Window/door is not tilted",
|
||||
"1": "Window/door is tilted",
|
||||
},
|
||||
"stateful": True,
|
||||
"secret": False,
|
||||
},
|
||||
"value": 0,
|
||||
}
|
||||
)
|
||||
return updated_state
|
||||
|
||||
|
||||
def _add_barrier_status_value(node_state: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Return a node state with a Barrier status Access Control notification value added."""
|
||||
updated_state = copy.deepcopy(node_state)
|
||||
updated_state["values"].append(
|
||||
{
|
||||
"commandClass": 113,
|
||||
"commandClassName": "Notification",
|
||||
"property": "Access Control",
|
||||
"propertyKey": "Barrier status",
|
||||
"propertyName": "Access Control",
|
||||
"propertyKeyName": "Barrier status",
|
||||
"ccVersion": 8,
|
||||
"metadata": {
|
||||
"type": "number",
|
||||
"readable": True,
|
||||
"writeable": False,
|
||||
"label": "Barrier status",
|
||||
"ccSpecific": {"notificationType": 6},
|
||||
"min": 0,
|
||||
"max": 255,
|
||||
"states": {
|
||||
"0": "idle",
|
||||
"64": "Barrier performing initialization process",
|
||||
"72": "Barrier safety beam obstacle",
|
||||
},
|
||||
"stateful": True,
|
||||
"secret": False,
|
||||
},
|
||||
"value": 0,
|
||||
}
|
||||
)
|
||||
return updated_state
|
||||
|
||||
|
||||
def _add_lock_state_notification_states(node_state: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Return a node state with Access Control lock state notification states 1-4."""
|
||||
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") == "Lock state"
|
||||
):
|
||||
value_data["metadata"].setdefault("states", {}).update(
|
||||
{
|
||||
"1": "Manual lock operation",
|
||||
"2": "Manual unlock operation",
|
||||
"3": "RF lock operation",
|
||||
"4": "RF unlock operation",
|
||||
}
|
||||
)
|
||||
break
|
||||
return updated_state
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def platforms() -> list[str]:
|
||||
"""Fixture to specify platforms to test."""
|
||||
@@ -305,6 +395,322 @@ async def test_property_sensor_door_status(
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
|
||||
async def test_opening_state_notification_does_not_create_binary_sensors(
|
||||
hass: HomeAssistant,
|
||||
client,
|
||||
hoppe_ehandle_connectsense_state,
|
||||
) -> None:
|
||||
"""Test Opening state does not fan out into per-state binary sensors."""
|
||||
# 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)
|
||||
state["values"] = [
|
||||
v
|
||||
for v in state["values"]
|
||||
if v.get("commandClass") != 48 # Binary Sensor CC
|
||||
]
|
||||
node = Node(client, 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()
|
||||
|
||||
assert not hass.states.async_all("binary_sensor")
|
||||
|
||||
|
||||
async def test_opening_state_disables_legacy_window_door_notification_sensors(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
client,
|
||||
hoppe_ehandle_connectsense_state,
|
||||
) -> None:
|
||||
"""Test Opening state disables legacy Access Control window/door sensors."""
|
||||
node = Node(
|
||||
client,
|
||||
_add_door_tilt_state_value(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()
|
||||
|
||||
legacy_entries = [
|
||||
entry
|
||||
for entry in entity_registry.entities.values()
|
||||
if entry.domain == "binary_sensor"
|
||||
and entry.platform == "zwave_js"
|
||||
and (
|
||||
entry.original_name
|
||||
in {
|
||||
"Window/door is open",
|
||||
"Window/door is closed",
|
||||
"Window/door is open in regular position",
|
||||
"Window/door is open in tilt position",
|
||||
}
|
||||
or (
|
||||
entry.original_name == "Window/door is tilted"
|
||||
and entry.original_device_class != BinarySensorDeviceClass.WINDOW
|
||||
)
|
||||
)
|
||||
]
|
||||
|
||||
assert len(legacy_entries) == 7
|
||||
assert all(
|
||||
entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION
|
||||
for entry in legacy_entries
|
||||
)
|
||||
assert all(hass.states.get(entry.entity_id) is None for entry in legacy_entries)
|
||||
|
||||
|
||||
async def test_reenabled_legacy_door_state_entity_follows_opening_state(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
client,
|
||||
hoppe_ehandle_connectsense_state,
|
||||
) -> None:
|
||||
"""Test a re-enabled legacy Door state entity derives state from Opening state."""
|
||||
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()
|
||||
|
||||
legacy_entry = next(
|
||||
entry
|
||||
for entry in entity_registry.entities.values()
|
||||
if entry.platform == "zwave_js"
|
||||
and entry.original_name == "Window/door is open in tilt position"
|
||||
)
|
||||
|
||||
entity_registry.async_update_entity(legacy_entry.entity_id, disabled_by=None)
|
||||
async_fire_time_changed(
|
||||
hass,
|
||||
dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(legacy_entry.entity_id)
|
||||
assert state
|
||||
assert 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": 2,
|
||||
"prevValue": 0,
|
||||
"propertyName": "Access Control",
|
||||
"propertyKeyName": "Opening state",
|
||||
},
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
state = hass.states.get(legacy_entry.entity_id)
|
||||
assert state
|
||||
assert state.state == STATE_ON
|
||||
|
||||
|
||||
async def test_legacy_door_state_entities_follow_opening_state(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
client,
|
||||
hoppe_ehandle_connectsense_state,
|
||||
) -> None:
|
||||
"""Test all legacy door state entities correctly derive state from Opening state."""
|
||||
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()
|
||||
|
||||
# Re-enable all 6 legacy door state entities.
|
||||
legacy_names = {
|
||||
"Window/door is open",
|
||||
"Window/door is closed",
|
||||
"Window/door is open in regular position",
|
||||
"Window/door is open in tilt position",
|
||||
}
|
||||
legacy_entries = [
|
||||
e
|
||||
for e in entity_registry.entities.values()
|
||||
if e.domain == "binary_sensor"
|
||||
and e.platform == "zwave_js"
|
||||
and e.original_name in legacy_names
|
||||
]
|
||||
assert len(legacy_entries) == 6
|
||||
for legacy_entry in legacy_entries:
|
||||
entity_registry.async_update_entity(legacy_entry.entity_id, disabled_by=None)
|
||||
|
||||
async_fire_time_changed(
|
||||
hass,
|
||||
dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# With Opening state = 0 (Closed), all "open" entities should be OFF and
|
||||
# all "closed" entities should be ON.
|
||||
open_entries = [
|
||||
e for e in legacy_entries if e.original_name == "Window/door is open"
|
||||
]
|
||||
closed_entries = [
|
||||
e for e in legacy_entries if e.original_name == "Window/door is closed"
|
||||
]
|
||||
open_regular_entries = [
|
||||
e
|
||||
for e in legacy_entries
|
||||
if e.original_name == "Window/door is open in regular position"
|
||||
]
|
||||
open_tilt_entries = [
|
||||
e
|
||||
for e in legacy_entries
|
||||
if e.original_name == "Window/door is open in tilt position"
|
||||
]
|
||||
|
||||
for e in open_entries + open_regular_entries + open_tilt_entries:
|
||||
state = hass.states.get(e.entity_id)
|
||||
assert state, f"{e.entity_id} should have a state"
|
||||
assert state.state == STATE_OFF, (
|
||||
f"{e.entity_id} ({e.original_name}) should be OFF when Opening state=Closed"
|
||||
)
|
||||
for e in closed_entries:
|
||||
state = hass.states.get(e.entity_id)
|
||||
assert state, f"{e.entity_id} should have a state"
|
||||
assert state.state == STATE_ON, (
|
||||
f"{e.entity_id} ({e.original_name}) should be ON when Opening state=Closed"
|
||||
)
|
||||
|
||||
# Update Opening state to 1 (Open).
|
||||
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()
|
||||
|
||||
# All "open" entities should now be ON, "closed" OFF, "tilt" OFF.
|
||||
for e in open_entries + open_regular_entries:
|
||||
state = hass.states.get(e.entity_id)
|
||||
assert state, f"{e.entity_id} should have a state"
|
||||
assert state.state == STATE_ON, (
|
||||
f"{e.entity_id} ({e.original_name}) should be ON when Opening state=Open"
|
||||
)
|
||||
for e in closed_entries + open_tilt_entries:
|
||||
state = hass.states.get(e.entity_id)
|
||||
assert state, f"{e.entity_id} should have a state"
|
||||
assert state.state == STATE_OFF, (
|
||||
f"{e.entity_id} ({e.original_name}) should be OFF when Opening state=Open"
|
||||
)
|
||||
|
||||
|
||||
async def test_access_control_lock_state_notification_sensors(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
client,
|
||||
lock_august_asl03_state,
|
||||
) -> None:
|
||||
"""Test Access Control lock state notification sensors from new discovery schemas."""
|
||||
node = Node(client, _add_lock_state_notification_states(lock_august_asl03_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()
|
||||
|
||||
lock_state_entities = [
|
||||
state
|
||||
for state in hass.states.async_all("binary_sensor")
|
||||
if state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.LOCK
|
||||
]
|
||||
assert len(lock_state_entities) == 4
|
||||
assert all(state.state == STATE_OFF for state in lock_state_entities)
|
||||
|
||||
jammed_entry = next(
|
||||
entry
|
||||
for entry in entity_registry.entities.values()
|
||||
if entry.domain == "binary_sensor"
|
||||
and entry.platform == "zwave_js"
|
||||
and entry.original_name == "Lock jammed"
|
||||
)
|
||||
assert jammed_entry.original_device_class == BinarySensorDeviceClass.PROBLEM
|
||||
assert jammed_entry.entity_category == EntityCategory.DIAGNOSTIC
|
||||
|
||||
jammed_state = hass.states.get(jammed_entry.entity_id)
|
||||
assert jammed_state
|
||||
assert jammed_state.state == STATE_OFF
|
||||
|
||||
|
||||
async def test_access_control_catch_all_with_opening_state_present(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
client,
|
||||
hoppe_ehandle_connectsense_state,
|
||||
) -> None:
|
||||
"""Test that unrelated Access Control values are discovered even when Opening state is present."""
|
||||
node = Node(
|
||||
client,
|
||||
_add_barrier_status_value(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()
|
||||
|
||||
# The two non-idle barrier states should each become a diagnostic binary sensor
|
||||
barrier_entries = [
|
||||
reg_entry
|
||||
for reg_entry in entity_registry.entities.values()
|
||||
if reg_entry.domain == "binary_sensor"
|
||||
and reg_entry.platform == "zwave_js"
|
||||
and reg_entry.entity_category == EntityCategory.DIAGNOSTIC
|
||||
and reg_entry.original_name
|
||||
and "barrier" in reg_entry.original_name.lower()
|
||||
]
|
||||
assert len(barrier_entries) == 2, (
|
||||
f"Expected 2 barrier status sensors, got {[e.original_name for e in barrier_entries]}"
|
||||
)
|
||||
for reg_entry in barrier_entries:
|
||||
state = hass.states.get(reg_entry.entity_id)
|
||||
assert state is not None
|
||||
assert state.state == STATE_OFF
|
||||
|
||||
|
||||
async def test_config_parameter_binary_sensor(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
|
||||
@@ -10,6 +10,7 @@ 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,
|
||||
@@ -777,6 +778,37 @@ async def test_unit_change(hass: HomeAssistant, zp3111, client, integration) ->
|
||||
assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
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