mirror of
https://github.com/home-assistant/core.git
synced 2026-03-14 06:52:04 +01:00
Compare commits
21 Commits
setpoint_c
...
remove-cod
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47781b9aa6 | ||
|
|
e14d88ff55 | ||
|
|
d04efbfe48 | ||
|
|
3f35cd5cd2 | ||
|
|
86ffd58665 | ||
|
|
6206392b28 | ||
|
|
b7c36c707f | ||
|
|
973c32b99d | ||
|
|
951775bea6 | ||
|
|
0f2dbdf4f4 | ||
|
|
443ff7efe1 | ||
|
|
0ee6b954df | ||
|
|
5681acf0e1 | ||
|
|
a94458b8bc | ||
|
|
f3c38ba2d3 | ||
|
|
c1acd1d860 | ||
|
|
f4748aa63d | ||
|
|
31f4f618cc | ||
|
|
30aec4d2ab | ||
|
|
335abd7002 | ||
|
|
3b3f0e9240 |
5
.github/copilot-instructions.md
vendored
5
.github/copilot-instructions.md
vendored
@@ -18,6 +18,11 @@ This repository contains the core of Home Assistant, a Python 3 based home autom
|
||||
|
||||
- Python 3.14 explicitly allows `except TypeA, TypeB:` without parentheses.
|
||||
|
||||
## Testing
|
||||
|
||||
When writing or modifying tests, ensure all test function parameters have type annotations.
|
||||
Prefer concrete types (for example, `HomeAssistant`, `MockConfigEntry`, etc.) over `Any`.
|
||||
|
||||
## Good practices
|
||||
|
||||
Integrations with Platinum or Gold level in the Integration Quality Scale reflect a high standard of code quality and maintainability. When looking for examples of something, these are good places to start. The level is indicated in the manifest.json of the integration.
|
||||
|
||||
@@ -15,6 +15,11 @@ This repository contains the core of Home Assistant, a Python 3 based home autom
|
||||
|
||||
- Python 3.14 explicitly allows `except TypeA, TypeB:` without parentheses.
|
||||
|
||||
## Testing
|
||||
|
||||
When writing or modifying tests, ensure all test function parameters have type annotations.
|
||||
Prefer concrete types (for example, `HomeAssistant`, `MockConfigEntry`, etc.) over `Any`.
|
||||
|
||||
## Good practices
|
||||
|
||||
Integrations with Platinum or Gold level in the Integration Quality Scale reflect a high standard of code quality and maintainability. When looking for examples of something, these are good places to start. The level is indicated in the manifest.json of the integration.
|
||||
|
||||
2
CODEOWNERS
generated
2
CODEOWNERS
generated
@@ -1905,6 +1905,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/wiffi/ @mampfes
|
||||
/homeassistant/components/wilight/ @leofig-rj
|
||||
/tests/components/wilight/ @leofig-rj
|
||||
/homeassistant/components/window/ @home-assistant/core
|
||||
/tests/components/window/ @home-assistant/core
|
||||
/homeassistant/components/wirelesstag/ @sergeymaysak
|
||||
/homeassistant/components/withings/ @joostlek
|
||||
/tests/components/withings/ @joostlek
|
||||
|
||||
@@ -245,6 +245,7 @@ DEFAULT_INTEGRATIONS = {
|
||||
"garage_door",
|
||||
"gate",
|
||||
"humidity",
|
||||
"window",
|
||||
}
|
||||
DEFAULT_INTEGRATIONS_RECOVERY_MODE = {
|
||||
# These integrations are set up if recovery mode is activated.
|
||||
|
||||
@@ -161,6 +161,7 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
|
||||
"text",
|
||||
"update",
|
||||
"vacuum",
|
||||
"window",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -144,6 +144,7 @@ class CreateBackupStage(StrEnum):
|
||||
ADDONS = "addons"
|
||||
AWAIT_ADDON_RESTARTS = "await_addon_restarts"
|
||||
DOCKER_CONFIG = "docker_config"
|
||||
CLEANING_UP = "cleaning_up"
|
||||
FINISHING_FILE = "finishing_file"
|
||||
FOLDERS = "folders"
|
||||
HOME_ASSISTANT = "home_assistant"
|
||||
@@ -1290,6 +1291,13 @@ class BackupManager:
|
||||
)
|
||||
# delete old backups more numerous than copies
|
||||
# try this regardless of agent errors above
|
||||
self.async_on_backup_event(
|
||||
CreateBackupEvent(
|
||||
reason=None,
|
||||
stage=CreateBackupStage.CLEANING_UP,
|
||||
state=CreateBackupState.IN_PROGRESS,
|
||||
)
|
||||
)
|
||||
await delete_backups_exceeding_configured_count(self)
|
||||
|
||||
finally:
|
||||
|
||||
@@ -516,6 +516,8 @@ class DownloadSupportPackageView(HomeAssistantView):
|
||||
hass_info: dict[str, Any],
|
||||
domains_info: dict[str, dict[str, str]],
|
||||
) -> str:
|
||||
cloud = hass.data[DATA_CLOUD]
|
||||
|
||||
def get_domain_table_markdown(domain_info: dict[str, Any]) -> str:
|
||||
if len(domain_info) == 0:
|
||||
return "No information available\n"
|
||||
@@ -572,6 +574,15 @@ class DownloadSupportPackageView(HomeAssistantView):
|
||||
"</details>\n\n"
|
||||
)
|
||||
|
||||
# Add stored latency response if available
|
||||
if locations := cloud.remote.latency_by_location:
|
||||
markdown += "## Latency by location\n\n"
|
||||
markdown += "Location | Latency (ms)\n"
|
||||
markdown += "--- | ---\n"
|
||||
for location in sorted(locations):
|
||||
markdown += f"{location} | {locations[location]['avg'] or 'N/A'}\n"
|
||||
markdown += "\n"
|
||||
|
||||
# Add installed packages section
|
||||
try:
|
||||
installed_packages = await async_get_installed_packages()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyenphase"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pyenphase==2.4.5"],
|
||||
"requirements": ["pyenphase==2.4.6"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_enphase-envoy._tcp.local."
|
||||
|
||||
@@ -179,7 +179,9 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
|
||||
return PRESET_HOLIDAY
|
||||
if self.data.summer_active:
|
||||
return PRESET_SUMMER
|
||||
if self.data.target_temperature == ON_API_TEMPERATURE:
|
||||
if self.data.target_temperature == ON_API_TEMPERATURE or getattr(
|
||||
self.data, "boost_active", False
|
||||
):
|
||||
return PRESET_BOOST
|
||||
if self.data.target_temperature == self.data.comfort_temperature:
|
||||
return PRESET_COMFORT
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -38,16 +38,16 @@
|
||||
"token_auth": {
|
||||
"data": {
|
||||
"region": "[%key:component::growatt_server::config::step::password_auth::data::region%]",
|
||||
"token": "API Token"
|
||||
"token": "API token"
|
||||
},
|
||||
"description": "Token authentication is only supported for MIN/TLX devices. For other device types, please use username/password authentication.",
|
||||
"title": "Enter your API token"
|
||||
},
|
||||
"user": {
|
||||
"description": "Note: API Token authentication is currently only supported for MIN/TLX devices. For other device types, please use Username & Password authentication.",
|
||||
"description": "Note: Token authentication is currently only supported for MIN/TLX devices. For other device types, please use username/password authentication.",
|
||||
"menu_options": {
|
||||
"password_auth": "Username & Password",
|
||||
"token_auth": "API Token (MIN/TLX only)"
|
||||
"password_auth": "Username/password",
|
||||
"token_auth": "API token (MIN/TLX only)"
|
||||
},
|
||||
"title": "Choose authentication method"
|
||||
}
|
||||
|
||||
@@ -225,10 +225,6 @@
|
||||
"description": "System is unsupported because Home Assistant cannot determine when an Internet connection is available. For troubleshooting information, select Learn more.",
|
||||
"title": "Unsupported system - Connectivity check disabled"
|
||||
},
|
||||
"unsupported_content_trust": {
|
||||
"description": "System is unsupported because Home Assistant cannot verify content being run is trusted and not modified by attackers. For troubleshooting information, select Learn more.",
|
||||
"title": "Unsupported system - Content-trust check disabled"
|
||||
},
|
||||
"unsupported_dbus": {
|
||||
"description": "System is unsupported because D-Bus is working incorrectly. Many things fail without this as Supervisor cannot communicate with the host. For troubleshooting information, select Learn more.",
|
||||
"title": "Unsupported system - D-Bus issues"
|
||||
@@ -281,10 +277,6 @@
|
||||
"description": "System is unsupported because additional software outside the Home Assistant ecosystem has been detected. For troubleshooting information, select Learn more.",
|
||||
"title": "Unsupported system - Unsupported software"
|
||||
},
|
||||
"unsupported_source_mods": {
|
||||
"description": "System is unsupported because Supervisor source code has been modified. For troubleshooting information, select Learn more.",
|
||||
"title": "Unsupported system - Supervisor source modifications"
|
||||
},
|
||||
"unsupported_supervisor_version": {
|
||||
"description": "System is unsupported because an out-of-date version of Supervisor is in use and auto-update has been disabled. For troubleshooting information, select Learn more.",
|
||||
"title": "Unsupported system - Supervisor version"
|
||||
|
||||
@@ -9,7 +9,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from .const import LOGGER
|
||||
from .coordinator import IntelliClimaConfigEntry, IntelliClimaCoordinator
|
||||
|
||||
PLATFORMS = [Platform.FAN, Platform.SELECT]
|
||||
PLATFORMS = [Platform.FAN, Platform.SELECT, Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
|
||||
101
homeassistant/components/intelliclima/sensor.py
Normal file
101
homeassistant/components/intelliclima/sensor.py
Normal file
@@ -0,0 +1,101 @@
|
||||
"""Sensor platform for IntelliClima VMC."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from pyintelliclima.intelliclima_types import IntelliClimaECO
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
PERCENTAGE,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import IntelliClimaConfigEntry, IntelliClimaCoordinator
|
||||
from .entity import IntelliClimaECOEntity
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class IntelliClimaSensorEntityDescription(SensorEntityDescription):
|
||||
"""Describes a sensor entity."""
|
||||
|
||||
value_fn: Callable[[IntelliClimaECO], int | float | str | None]
|
||||
|
||||
|
||||
INTELLICLIMA_SENSORS: tuple[IntelliClimaSensorEntityDescription, ...] = (
|
||||
IntelliClimaSensorEntityDescription(
|
||||
key="temperature",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
value_fn=lambda device_data: float(device_data.tamb),
|
||||
),
|
||||
IntelliClimaSensorEntityDescription(
|
||||
key="humidity",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
value_fn=lambda device_data: float(device_data.rh),
|
||||
),
|
||||
IntelliClimaSensorEntityDescription(
|
||||
key="voc",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS,
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
||||
value_fn=lambda device_data: float(device_data.voc_state),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: IntelliClimaConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up a IntelliClima Sensors."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
entities: list[IntelliClimaSensor] = [
|
||||
IntelliClimaSensor(
|
||||
coordinator=coordinator, device=ecocomfort2, description=description
|
||||
)
|
||||
for ecocomfort2 in coordinator.data.ecocomfort2_devices.values()
|
||||
for description in INTELLICLIMA_SENSORS
|
||||
]
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class IntelliClimaSensor(IntelliClimaECOEntity, SensorEntity):
|
||||
"""Extends IntelliClimaEntity with Sensor specific logic."""
|
||||
|
||||
entity_description: IntelliClimaSensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: IntelliClimaCoordinator,
|
||||
device: IntelliClimaECO,
|
||||
description: IntelliClimaSensorEntityDescription,
|
||||
) -> None:
|
||||
"""Class initializer."""
|
||||
super().__init__(coordinator, device)
|
||||
|
||||
self.entity_description = description
|
||||
|
||||
self._attr_unique_id = f"{device.id}_{description.key}"
|
||||
|
||||
@property
|
||||
def native_value(self) -> int | float | str | None:
|
||||
"""Use this to get the correct value."""
|
||||
return self.entity_description.value_fn(self._device_data)
|
||||
@@ -12,5 +12,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["intellifire4py"],
|
||||
"requirements": ["intellifire4py==4.3.1"]
|
||||
"requirements": ["intellifire4py==4.4.0"]
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -72,6 +72,7 @@ ABBREVIATIONS = {
|
||||
"fan_mode_stat_t": "fan_mode_state_topic",
|
||||
"frc_upd": "force_update",
|
||||
"g_tpl": "green_template",
|
||||
"grp": "group",
|
||||
"hs_cmd_t": "hs_command_topic",
|
||||
"hs_cmd_tpl": "hs_command_template",
|
||||
"hs_stat_t": "hs_state_topic",
|
||||
|
||||
@@ -10,6 +10,7 @@ from homeassistant.helpers import config_validation as cv
|
||||
from .const import (
|
||||
CONF_COMMAND_TOPIC,
|
||||
CONF_ENCODING,
|
||||
CONF_GROUP,
|
||||
CONF_QOS,
|
||||
CONF_RETAIN,
|
||||
CONF_STATE_TOPIC,
|
||||
@@ -23,6 +24,7 @@ from .util import valid_publish_topic, valid_qos_schema, valid_subscribe_topic
|
||||
SCHEMA_BASE = {
|
||||
vol.Optional(CONF_QOS, default=DEFAULT_QOS): valid_qos_schema,
|
||||
vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): cv.string,
|
||||
vol.Optional(CONF_GROUP): vol.All(cv.ensure_list, [cv.string]),
|
||||
}
|
||||
|
||||
MQTT_BASE_SCHEMA = vol.Schema(SCHEMA_BASE)
|
||||
|
||||
@@ -109,6 +109,7 @@ CONF_FLASH_TIME_SHORT = "flash_time_short"
|
||||
CONF_GET_POSITION_TEMPLATE = "position_template"
|
||||
CONF_GET_POSITION_TOPIC = "position_topic"
|
||||
CONF_GREEN_TEMPLATE = "green_template"
|
||||
CONF_GROUP = "group"
|
||||
CONF_HS_COMMAND_TEMPLATE = "hs_command_template"
|
||||
CONF_HS_COMMAND_TOPIC = "hs_command_topic"
|
||||
CONF_HS_STATE_TOPIC = "hs_state_topic"
|
||||
|
||||
@@ -48,6 +48,7 @@ from homeassistant.helpers.event import (
|
||||
async_track_device_registry_updated_event,
|
||||
async_track_entity_registry_updated_event,
|
||||
)
|
||||
from homeassistant.helpers.group import IntegrationSpecificGroup
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
from homeassistant.helpers.service_info.mqtt import ReceivePayloadType
|
||||
from homeassistant.helpers.typing import (
|
||||
@@ -78,6 +79,7 @@ from .const import (
|
||||
CONF_ENABLED_BY_DEFAULT,
|
||||
CONF_ENCODING,
|
||||
CONF_ENTITY_PICTURE,
|
||||
CONF_GROUP,
|
||||
CONF_HW_VERSION,
|
||||
CONF_IDENTIFIERS,
|
||||
CONF_JSON_ATTRS_TEMPLATE,
|
||||
@@ -133,6 +135,7 @@ MQTT_ATTRIBUTES_BLOCKED = {
|
||||
"device_class",
|
||||
"device_info",
|
||||
"entity_category",
|
||||
"entity_id",
|
||||
"entity_picture",
|
||||
"entity_registry_enabled_default",
|
||||
"extra_state_attributes",
|
||||
@@ -460,7 +463,7 @@ def async_setup_entity_entry_helper(
|
||||
|
||||
|
||||
class MqttAttributesMixin(Entity):
|
||||
"""Mixin used for platforms that support JSON attributes."""
|
||||
"""Mixin used for platforms that support JSON attributes and group entities."""
|
||||
|
||||
_attributes_extra_blocked: frozenset[str] = frozenset()
|
||||
_attr_tpl: Callable[[ReceivePayloadType], ReceivePayloadType] | None = None
|
||||
@@ -468,10 +471,13 @@ class MqttAttributesMixin(Entity):
|
||||
[MessageCallbackType, set[str] | None, ReceiveMessage], None
|
||||
]
|
||||
_process_update_extra_state_attributes: Callable[[dict[str, Any]], None]
|
||||
group: IntegrationSpecificGroup | None
|
||||
|
||||
def __init__(self, config: ConfigType) -> None:
|
||||
"""Initialize the JSON attributes mixin."""
|
||||
"""Initialize the JSON attributes and handle group entities."""
|
||||
self._attributes_sub_state: dict[str, EntitySubscription] = {}
|
||||
if CONF_GROUP in config:
|
||||
self.group = IntegrationSpecificGroup(self, config[CONF_GROUP])
|
||||
self._attributes_config = config
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
@@ -482,6 +488,16 @@ class MqttAttributesMixin(Entity):
|
||||
|
||||
def attributes_prepare_discovery_update(self, config: DiscoveryInfoType) -> None:
|
||||
"""Handle updated discovery message."""
|
||||
if CONF_GROUP in config:
|
||||
if self.group is not None:
|
||||
self.group.member_unique_ids = config[CONF_GROUP]
|
||||
else:
|
||||
_LOGGER.info(
|
||||
"Group member update received for entity %s, "
|
||||
"but this entity was not initialized with the `group` option. "
|
||||
"Reload the MQTT integration or restart Home Assistant to activate"
|
||||
)
|
||||
|
||||
self._attributes_config = config
|
||||
self._attributes_prepare_subscribe_topics()
|
||||
|
||||
@@ -543,7 +559,7 @@ class MqttAttributesMixin(Entity):
|
||||
_LOGGER.warning("Erroneous JSON: %s", payload)
|
||||
else:
|
||||
if isinstance(json_dict, dict):
|
||||
filtered_dict = {
|
||||
filtered_dict: dict[str, Any] = {
|
||||
k: v
|
||||
for k, v in json_dict.items()
|
||||
if k not in MQTT_ATTRIBUTES_BLOCKED
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/portainer",
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "bronze",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pyportainer==1.0.33"]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import Any
|
||||
|
||||
from prana_local_api_client.exceptions import PranaApiCommunicationError
|
||||
from prana_local_api_client.models.prana_device_info import PranaDeviceInfo
|
||||
from prana_local_api_client.prana_api_client import PranaLocalApiClient
|
||||
from prana_local_api_client.prana_local_api_client import PranaLocalApiClient
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
|
||||
@@ -12,7 +12,7 @@ from prana_local_api_client.exceptions import (
|
||||
)
|
||||
from prana_local_api_client.models.prana_device_info import PranaDeviceInfo
|
||||
from prana_local_api_client.models.prana_state import PranaState
|
||||
from prana_local_api_client.prana_api_client import PranaLocalApiClient
|
||||
from prana_local_api_client.prana_local_api_client import PranaLocalApiClient
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["prana-api-client==0.10.0"],
|
||||
"requirements": ["prana-api-client==0.12.0"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_prana._tcp.local."
|
||||
|
||||
@@ -217,9 +217,6 @@
|
||||
"energy_left": {
|
||||
"default": "mdi:battery"
|
||||
},
|
||||
"energy_remaining": {
|
||||
"default": "mdi:battery-medium"
|
||||
},
|
||||
"generator_power": {
|
||||
"default": "mdi:generator-stationary"
|
||||
},
|
||||
|
||||
@@ -299,14 +299,6 @@ BATTERY_DESCRIPTIONS: tuple[TessieSensorEntityDescription, ...] = (
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
suggested_display_precision=2,
|
||||
),
|
||||
TessieSensorEntityDescription(
|
||||
key="energy_remaining",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY_STORAGE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
TessieSensorEntityDescription(
|
||||
key="lifetime_energy_used",
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
|
||||
@@ -458,9 +458,6 @@
|
||||
"energy_left": {
|
||||
"name": "Energy left"
|
||||
},
|
||||
"energy_remaining": {
|
||||
"name": "Energy remaining"
|
||||
},
|
||||
"generator_energy_exported": {
|
||||
"name": "Generator exported"
|
||||
},
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["waterfurnace"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["waterfurnace==1.5.1"]
|
||||
"requirements": ["waterfurnace==1.6.2"]
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aiowebdav2"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["aiowebdav2==0.6.1"]
|
||||
"requirements": ["aiowebdav2==0.6.2"]
|
||||
}
|
||||
|
||||
17
homeassistant/components/window/__init__.py
Normal file
17
homeassistant/components/window/__init__.py
Normal file
@@ -0,0 +1,17 @@
|
||||
"""Integration for window triggers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
DOMAIN = "window"
|
||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||
|
||||
__all__ = []
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the component."""
|
||||
return True
|
||||
10
homeassistant/components/window/icons.json
Normal file
10
homeassistant/components/window/icons.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"triggers": {
|
||||
"closed": {
|
||||
"trigger": "mdi:window-closed"
|
||||
},
|
||||
"opened": {
|
||||
"trigger": "mdi:window-open"
|
||||
}
|
||||
}
|
||||
}
|
||||
8
homeassistant/components/window/manifest.json
Normal file
8
homeassistant/components/window/manifest.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"domain": "window",
|
||||
"name": "Window",
|
||||
"codeowners": ["@home-assistant/core"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/window",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal"
|
||||
}
|
||||
38
homeassistant/components/window/strings.json
Normal file
38
homeassistant/components/window/strings.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"common": {
|
||||
"trigger_behavior_description": "The behavior of the targeted windows to trigger on.",
|
||||
"trigger_behavior_name": "Behavior"
|
||||
},
|
||||
"selector": {
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "Any",
|
||||
"first": "First",
|
||||
"last": "Last"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Window",
|
||||
"triggers": {
|
||||
"closed": {
|
||||
"description": "Triggers after one or more windows close.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::window::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::window::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Window closed"
|
||||
},
|
||||
"opened": {
|
||||
"description": "Triggers after one or more windows open.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::window::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::window::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Window opened"
|
||||
}
|
||||
}
|
||||
}
|
||||
36
homeassistant/components/window/trigger.py
Normal file
36
homeassistant/components/window/trigger.py
Normal file
@@ -0,0 +1,36 @@
|
||||
"""Provides triggers for windows."""
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DOMAIN as BINARY_SENSOR_DOMAIN,
|
||||
BinarySensorDeviceClass,
|
||||
)
|
||||
from homeassistant.components.cover import (
|
||||
DOMAIN as COVER_DOMAIN,
|
||||
CoverDeviceClass,
|
||||
make_cover_closed_trigger,
|
||||
make_cover_opened_trigger,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.trigger import Trigger
|
||||
|
||||
DEVICE_CLASSES_WINDOW: dict[str, str] = {
|
||||
BINARY_SENSOR_DOMAIN: BinarySensorDeviceClass.WINDOW,
|
||||
COVER_DOMAIN: CoverDeviceClass.WINDOW,
|
||||
}
|
||||
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"opened": make_cover_opened_trigger(
|
||||
device_classes=DEVICE_CLASSES_WINDOW,
|
||||
domains={BINARY_SENSOR_DOMAIN, COVER_DOMAIN},
|
||||
),
|
||||
"closed": make_cover_closed_trigger(
|
||||
device_classes=DEVICE_CLASSES_WINDOW,
|
||||
domains={BINARY_SENSOR_DOMAIN, COVER_DOMAIN},
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
|
||||
"""Return the triggers for windows."""
|
||||
return TRIGGERS
|
||||
29
homeassistant/components/window/triggers.yaml
Normal file
29
homeassistant/components/window/triggers.yaml
Normal file
@@ -0,0 +1,29 @@
|
||||
.trigger_common_fields: &trigger_common_fields
|
||||
behavior:
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: trigger_behavior
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
|
||||
closed:
|
||||
fields: *trigger_common_fields
|
||||
target:
|
||||
entity:
|
||||
- domain: binary_sensor
|
||||
device_class: window
|
||||
- domain: cover
|
||||
device_class: window
|
||||
|
||||
opened:
|
||||
fields: *trigger_common_fields
|
||||
target:
|
||||
entity:
|
||||
- domain: binary_sensor
|
||||
device_class: window
|
||||
- domain: cover
|
||||
device_class: window
|
||||
@@ -9,7 +9,6 @@ import logging
|
||||
from typing import Any
|
||||
|
||||
from awesomeversion import AwesomeVersion
|
||||
import voluptuous as vol
|
||||
from zwave_js_server.client import Client as ZwaveClient
|
||||
from zwave_js_server.const import CommandClass, RemoveNodeReason
|
||||
from zwave_js_server.exceptions import (
|
||||
@@ -94,7 +93,6 @@ from .const import (
|
||||
CONF_ADDON_S2_UNAUTHENTICATED_KEY,
|
||||
CONF_ADDON_SOCKET,
|
||||
CONF_DATA_COLLECTION_OPTED_IN,
|
||||
CONF_INSTALLER_MODE,
|
||||
CONF_INTEGRATION_CREATED_ADDON,
|
||||
CONF_KEEP_OLD_DEVICES,
|
||||
CONF_LR_S2_ACCESS_CONTROL_KEY,
|
||||
@@ -138,16 +136,8 @@ from .services import async_setup_services
|
||||
CONNECT_TIMEOUT = 10
|
||||
DRIVER_READY_TIMEOUT = 60
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_INSTALLER_MODE, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
MIN_CONTROLLER_FIRMWARE_SDK_VERSION = AwesomeVersion("6.50.0")
|
||||
|
||||
PLATFORMS = [
|
||||
@@ -171,7 +161,6 @@ PLATFORMS = [
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Z-Wave JS component."""
|
||||
hass.data[DOMAIN] = config.get(DOMAIN, {})
|
||||
for entry in hass.config_entries.async_entries(DOMAIN):
|
||||
if not isinstance(entry.unique_id, str):
|
||||
hass.config_entries.async_update_entry(
|
||||
|
||||
@@ -84,7 +84,6 @@ from .const import (
|
||||
ATTR_PARAMETERS,
|
||||
ATTR_WAIT_FOR_RESULT,
|
||||
CONF_DATA_COLLECTION_OPTED_IN,
|
||||
CONF_INSTALLER_MODE,
|
||||
DOMAIN,
|
||||
EVENT_DEVICE_ADDED_TO_REGISTRY,
|
||||
LOGGER,
|
||||
@@ -476,7 +475,6 @@ def async_register_api(hass: HomeAssistant) -> None:
|
||||
websocket_api.async_register_command(hass, websocket_hard_reset_controller)
|
||||
websocket_api.async_register_command(hass, websocket_node_capabilities)
|
||||
websocket_api.async_register_command(hass, websocket_invoke_cc_api)
|
||||
websocket_api.async_register_command(hass, websocket_get_integration_settings)
|
||||
websocket_api.async_register_command(hass, websocket_backup_nvm)
|
||||
websocket_api.async_register_command(hass, websocket_restore_nvm)
|
||||
hass.http.register_view(FirmwareUploadView(dr.async_get(hass)))
|
||||
@@ -2965,28 +2963,6 @@ async def websocket_invoke_cc_api(
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required(TYPE): "zwave_js/get_integration_settings",
|
||||
}
|
||||
)
|
||||
def websocket_get_integration_settings(
|
||||
hass: HomeAssistant,
|
||||
connection: ActiveConnection,
|
||||
msg: dict[str, Any],
|
||||
) -> None:
|
||||
"""Get Z-Wave JS integration wide configuration."""
|
||||
connection.send_result(
|
||||
msg[ID],
|
||||
{
|
||||
# list explicitly to avoid leaking other keys and to set default
|
||||
CONF_INSTALLER_MODE: hass.data[DOMAIN].get(CONF_INSTALLER_MODE, False),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -25,7 +25,6 @@ CONF_ADDON_S2_UNAUTHENTICATED_KEY = "s2_unauthenticated_key"
|
||||
CONF_ADDON_LR_S2_ACCESS_CONTROL_KEY = "lr_s2_access_control_key"
|
||||
CONF_ADDON_LR_S2_AUTHENTICATED_KEY = "lr_s2_authenticated_key"
|
||||
CONF_ADDON_SOCKET = "socket"
|
||||
CONF_INSTALLER_MODE = "installer_mode"
|
||||
CONF_INTEGRATION_CREATED_ADDON = "integration_created_addon"
|
||||
CONF_KEEP_OLD_DEVICES = "keep_old_devices"
|
||||
CONF_NETWORK_KEY = "network_key"
|
||||
@@ -207,3 +206,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())
|
||||
|
||||
@@ -782,6 +782,8 @@ async def entity_service_call(
|
||||
all_referenced,
|
||||
)
|
||||
|
||||
entity_candidates = [e for e in entity_candidates if e.available]
|
||||
|
||||
if not target_all_entities:
|
||||
assert referenced is not None
|
||||
# Only report on explicit referenced entities
|
||||
@@ -792,9 +794,6 @@ async def entity_service_call(
|
||||
|
||||
entities: list[Entity] = []
|
||||
for entity in entity_candidates:
|
||||
if not entity.available:
|
||||
continue
|
||||
|
||||
# Skip entities that don't have the required device class.
|
||||
if (
|
||||
entity_device_classes is not None
|
||||
|
||||
@@ -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
|
||||
|
||||
12
requirements_all.txt
generated
12
requirements_all.txt
generated
@@ -443,7 +443,7 @@ aiowaqi==3.1.0
|
||||
aiowatttime==0.1.1
|
||||
|
||||
# homeassistant.components.webdav
|
||||
aiowebdav2==0.6.1
|
||||
aiowebdav2==0.6.2
|
||||
|
||||
# homeassistant.components.webostv
|
||||
aiowebostv==0.7.5
|
||||
@@ -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
|
||||
@@ -1322,7 +1322,7 @@ inkbird-ble==1.1.1
|
||||
insteon-frontend-home-assistant==0.6.1
|
||||
|
||||
# homeassistant.components.intellifire
|
||||
intellifire4py==4.3.1
|
||||
intellifire4py==4.4.0
|
||||
|
||||
# homeassistant.components.iometer
|
||||
iometer==0.4.0
|
||||
@@ -1794,7 +1794,7 @@ poolsense==0.0.8
|
||||
powerfox==2.1.1
|
||||
|
||||
# homeassistant.components.prana
|
||||
prana-api-client==0.10.0
|
||||
prana-api-client==0.12.0
|
||||
|
||||
# homeassistant.components.reddit
|
||||
praw==7.5.0
|
||||
@@ -2071,7 +2071,7 @@ pyegps==0.2.5
|
||||
pyemoncms==0.1.3
|
||||
|
||||
# homeassistant.components.enphase_envoy
|
||||
pyenphase==2.4.5
|
||||
pyenphase==2.4.6
|
||||
|
||||
# homeassistant.components.envisalink
|
||||
pyenvisalink==4.7
|
||||
@@ -3247,7 +3247,7 @@ wallbox==0.9.0
|
||||
watchdog==6.0.0
|
||||
|
||||
# homeassistant.components.waterfurnace
|
||||
waterfurnace==1.5.1
|
||||
waterfurnace==1.6.2
|
||||
|
||||
# homeassistant.components.watergate
|
||||
watergate-local-api==2025.1.0
|
||||
|
||||
12
requirements_test_all.txt
generated
12
requirements_test_all.txt
generated
@@ -428,7 +428,7 @@ aiowaqi==3.1.0
|
||||
aiowatttime==0.1.1
|
||||
|
||||
# homeassistant.components.webdav
|
||||
aiowebdav2==0.6.1
|
||||
aiowebdav2==0.6.2
|
||||
|
||||
# homeassistant.components.webostv
|
||||
aiowebostv==0.7.5
|
||||
@@ -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
|
||||
@@ -1171,7 +1171,7 @@ inkbird-ble==1.1.1
|
||||
insteon-frontend-home-assistant==0.6.1
|
||||
|
||||
# homeassistant.components.intellifire
|
||||
intellifire4py==4.3.1
|
||||
intellifire4py==4.4.0
|
||||
|
||||
# homeassistant.components.iometer
|
||||
iometer==0.4.0
|
||||
@@ -1552,7 +1552,7 @@ poolsense==0.0.8
|
||||
powerfox==2.1.1
|
||||
|
||||
# homeassistant.components.prana
|
||||
prana-api-client==0.10.0
|
||||
prana-api-client==0.12.0
|
||||
|
||||
# homeassistant.components.reddit
|
||||
praw==7.5.0
|
||||
@@ -1775,7 +1775,7 @@ pyegps==0.2.5
|
||||
pyemoncms==0.1.3
|
||||
|
||||
# homeassistant.components.enphase_envoy
|
||||
pyenphase==2.4.5
|
||||
pyenphase==2.4.6
|
||||
|
||||
# homeassistant.components.everlights
|
||||
pyeverlights==0.1.0
|
||||
@@ -2735,7 +2735,7 @@ wallbox==0.9.0
|
||||
watchdog==6.0.0
|
||||
|
||||
# homeassistant.components.waterfurnace
|
||||
waterfurnace==1.5.1
|
||||
waterfurnace==1.6.2
|
||||
|
||||
# homeassistant.components.watergate
|
||||
watergate-local-api==2025.1.0
|
||||
|
||||
@@ -123,6 +123,7 @@ NO_IOT_CLASS = [
|
||||
"web_rtc",
|
||||
"webhook",
|
||||
"websocket_api",
|
||||
"window",
|
||||
"zone",
|
||||
]
|
||||
|
||||
|
||||
@@ -2157,6 +2157,7 @@ NO_QUALITY_SCALE = [
|
||||
"web_rtc",
|
||||
"webhook",
|
||||
"websocket_api",
|
||||
"window",
|
||||
"zone",
|
||||
]
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -604,6 +604,14 @@ async def test_initiate_backup(
|
||||
result = await ws_client.receive_json()
|
||||
while "uploaded_bytes" in result["event"]:
|
||||
result = await ws_client.receive_json()
|
||||
assert result["event"] == {
|
||||
"manager_state": BackupManagerState.CREATE_BACKUP,
|
||||
"reason": None,
|
||||
"stage": CreateBackupStage.CLEANING_UP,
|
||||
"state": CreateBackupState.IN_PROGRESS,
|
||||
}
|
||||
|
||||
result = await ws_client.receive_json()
|
||||
assert result["event"] == {
|
||||
"manager_state": BackupManagerState.CREATE_BACKUP,
|
||||
"reason": None,
|
||||
@@ -854,6 +862,14 @@ async def test_initiate_backup_with_agent_error(
|
||||
result = await ws_client.receive_json()
|
||||
while "uploaded_bytes" in result["event"]:
|
||||
result = await ws_client.receive_json()
|
||||
assert result["event"] == {
|
||||
"manager_state": BackupManagerState.CREATE_BACKUP,
|
||||
"reason": None,
|
||||
"stage": CreateBackupStage.CLEANING_UP,
|
||||
"state": CreateBackupState.IN_PROGRESS,
|
||||
}
|
||||
|
||||
result = await ws_client.receive_json()
|
||||
assert result["event"] == {
|
||||
"manager_state": BackupManagerState.CREATE_BACKUP,
|
||||
"reason": "upload_failed",
|
||||
@@ -3549,6 +3565,14 @@ async def test_initiate_backup_per_agent_encryption(
|
||||
result = await ws_client.receive_json()
|
||||
while "uploaded_bytes" in result["event"]:
|
||||
result = await ws_client.receive_json()
|
||||
assert result["event"] == {
|
||||
"manager_state": BackupManagerState.CREATE_BACKUP,
|
||||
"reason": None,
|
||||
"stage": CreateBackupStage.CLEANING_UP,
|
||||
"state": CreateBackupState.IN_PROGRESS,
|
||||
}
|
||||
|
||||
result = await ws_client.receive_json()
|
||||
assert result["event"] == {
|
||||
"manager_state": BackupManagerState.CREATE_BACKUP,
|
||||
"reason": None,
|
||||
@@ -3783,7 +3807,7 @@ async def test_upload_progress_event(
|
||||
result = await ws_client.receive_json()
|
||||
assert result["event"]["stage"] == CreateBackupStage.UPLOAD_TO_AGENTS
|
||||
|
||||
# Collect all upload progress events until the final state event
|
||||
# Collect all upload progress events until the finishing backup stage event
|
||||
progress_events = []
|
||||
result = await ws_client.receive_json()
|
||||
while "uploaded_bytes" in result["event"]:
|
||||
@@ -3801,6 +3825,9 @@ async def test_upload_progress_event(
|
||||
assert len(local_progress) == 1
|
||||
assert local_progress[0]["uploaded_bytes"] == local_progress[0]["total_bytes"]
|
||||
|
||||
assert result["event"]["stage"] == CreateBackupStage.CLEANING_UP
|
||||
|
||||
result = await ws_client.receive_json()
|
||||
assert result["event"]["state"] == CreateBackupState.COMPLETED
|
||||
|
||||
result = await ws_client.receive_json()
|
||||
|
||||
@@ -66,6 +66,7 @@ async def cloud_fixture() -> AsyncGenerator[MagicMock]:
|
||||
certificate_status=None,
|
||||
instance_domain=None,
|
||||
is_connected=False,
|
||||
latency_by_location={},
|
||||
)
|
||||
mock_cloud.auth = MagicMock(spec=CognitoAuth)
|
||||
mock_cloud.iot = MagicMock(
|
||||
|
||||
@@ -87,6 +87,13 @@
|
||||
|
||||
</details>
|
||||
|
||||
## Latency by location
|
||||
|
||||
Location | Latency (ms)
|
||||
--- | ---
|
||||
Earth | 13.37
|
||||
Moon | N/A
|
||||
|
||||
## Installed packages
|
||||
|
||||
<details><summary>Installed packages</summary>
|
||||
|
||||
@@ -1907,6 +1907,10 @@ async def test_download_support_package(
|
||||
|
||||
cloud.remote.snitun_server = "us-west-1"
|
||||
cloud.remote.certificate_status = CertificateStatus.READY
|
||||
cloud.remote.latency_by_location = {
|
||||
"Earth": {"avg": 13.37},
|
||||
"Moon": {"avg": None},
|
||||
}
|
||||
cloud.expiration_date = dt_util.parse_datetime("2025-01-17T11:19:31.0+00:00")
|
||||
|
||||
await cloud.client.async_system_message({"region": "xx-earth-616"})
|
||||
|
||||
@@ -114,6 +114,7 @@ class FritzDeviceClimateMock(FritzEntityBaseMock):
|
||||
has_thermostat = True
|
||||
has_blind = False
|
||||
holiday_active = False
|
||||
boost_active = False
|
||||
lock = "fake_locked"
|
||||
present = True
|
||||
summer_active = False
|
||||
|
||||
@@ -442,7 +442,7 @@ async def test_preset_mode_update(hass: HomeAssistant, fritz: Mock) -> None:
|
||||
assert state
|
||||
assert state.attributes[ATTR_PRESET_MODE] == PRESET_ECO
|
||||
|
||||
# test boost preset
|
||||
# test boost preset by special temp
|
||||
device.target_temperature = 127 # special temp from the api
|
||||
next_update = dt_util.utcnow() + timedelta(seconds=200)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
@@ -453,6 +453,18 @@ async def test_preset_mode_update(hass: HomeAssistant, fritz: Mock) -> None:
|
||||
assert state
|
||||
assert state.attributes[ATTR_PRESET_MODE] == PRESET_BOOST
|
||||
|
||||
# test boost preset by boost_active
|
||||
device.target_temperature = 21
|
||||
device.boost_active = True
|
||||
next_update = dt_util.utcnow() + timedelta(seconds=200)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
|
||||
assert fritz().update_devices.call_count == 5
|
||||
assert state
|
||||
assert state.attributes[ATTR_PRESET_MODE] == PRESET_BOOST
|
||||
|
||||
|
||||
async def test_discover_new_device(hass: HomeAssistant, fritz: Mock) -> None:
|
||||
"""Test adding new discovered devices during runtime."""
|
||||
|
||||
@@ -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"),
|
||||
[
|
||||
|
||||
@@ -984,6 +984,14 @@ async def test_reader_writer_create(
|
||||
response = await client.receive_json()
|
||||
while "uploaded_bytes" in response["event"]:
|
||||
response = await client.receive_json()
|
||||
assert response["event"] == {
|
||||
"manager_state": "create_backup",
|
||||
"reason": None,
|
||||
"stage": "cleaning_up",
|
||||
"state": "in_progress",
|
||||
}
|
||||
|
||||
response = await client.receive_json()
|
||||
assert response["event"] == {
|
||||
"manager_state": "create_backup",
|
||||
"reason": None,
|
||||
@@ -1101,6 +1109,14 @@ async def test_reader_writer_create_addon_folder_error(
|
||||
response = await client.receive_json()
|
||||
while "uploaded_bytes" in response["event"]:
|
||||
response = await client.receive_json()
|
||||
assert response["event"] == {
|
||||
"manager_state": "create_backup",
|
||||
"reason": None,
|
||||
"stage": "cleaning_up",
|
||||
"state": "in_progress",
|
||||
}
|
||||
|
||||
response = await client.receive_json()
|
||||
assert response["event"] == {
|
||||
"manager_state": "create_backup",
|
||||
"reason": None,
|
||||
@@ -1221,6 +1237,14 @@ async def test_reader_writer_create_report_progress(
|
||||
response = await client.receive_json()
|
||||
while "uploaded_bytes" in response["event"]:
|
||||
response = await client.receive_json()
|
||||
assert response["event"] == {
|
||||
"manager_state": "create_backup",
|
||||
"reason": None,
|
||||
"stage": "cleaning_up",
|
||||
"state": "in_progress",
|
||||
}
|
||||
|
||||
response = await client.receive_json()
|
||||
assert response["event"] == {
|
||||
"manager_state": "create_backup",
|
||||
"reason": None,
|
||||
@@ -1286,6 +1310,14 @@ async def test_reader_writer_create_job_done(
|
||||
response = await client.receive_json()
|
||||
while "uploaded_bytes" in response["event"]:
|
||||
response = await client.receive_json()
|
||||
assert response["event"] == {
|
||||
"manager_state": "create_backup",
|
||||
"reason": None,
|
||||
"stage": "cleaning_up",
|
||||
"state": "in_progress",
|
||||
}
|
||||
|
||||
response = await client.receive_json()
|
||||
assert response["event"] == {
|
||||
"manager_state": "create_backup",
|
||||
"reason": None,
|
||||
@@ -1552,6 +1584,14 @@ async def test_reader_writer_create_per_agent_encryption(
|
||||
response = await client.receive_json()
|
||||
while "uploaded_bytes" in response["event"]:
|
||||
response = await client.receive_json()
|
||||
assert response["event"] == {
|
||||
"manager_state": "create_backup",
|
||||
"reason": None,
|
||||
"stage": "cleaning_up",
|
||||
"state": "in_progress",
|
||||
}
|
||||
|
||||
response = await client.receive_json()
|
||||
assert response["event"] == {
|
||||
"manager_state": "create_backup",
|
||||
"reason": None,
|
||||
@@ -1728,10 +1768,51 @@ async def test_reader_writer_create_missing_reference_error(
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("hassio_client", "setup_backup_integration")
|
||||
@pytest.mark.parametrize("exception", [SupervisorError("Boom!"), Exception("Boom!")])
|
||||
@pytest.mark.parametrize(
|
||||
("method", "download_call_count", "remove_call_count"),
|
||||
[("download_backup", 1, 1), ("remove_backup", 1, 1)],
|
||||
(
|
||||
"exception",
|
||||
"method",
|
||||
"download_call_count",
|
||||
"remove_call_count",
|
||||
"expected_events_before_failed",
|
||||
),
|
||||
[
|
||||
(
|
||||
SupervisorError("Boom!"),
|
||||
"download_backup",
|
||||
1,
|
||||
1,
|
||||
[],
|
||||
),
|
||||
(
|
||||
Exception("Boom!"),
|
||||
"download_backup",
|
||||
1,
|
||||
1,
|
||||
[
|
||||
{
|
||||
"manager_state": "create_backup",
|
||||
"reason": None,
|
||||
"stage": "cleaning_up",
|
||||
"state": "in_progress",
|
||||
}
|
||||
],
|
||||
),
|
||||
(
|
||||
SupervisorError("Boom!"),
|
||||
"remove_backup",
|
||||
1,
|
||||
1,
|
||||
[],
|
||||
),
|
||||
(
|
||||
Exception("Boom!"),
|
||||
"remove_backup",
|
||||
1,
|
||||
1,
|
||||
[],
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_reader_writer_create_download_remove_error(
|
||||
hass: HomeAssistant,
|
||||
@@ -1741,6 +1822,7 @@ async def test_reader_writer_create_download_remove_error(
|
||||
method: str,
|
||||
download_call_count: int,
|
||||
remove_call_count: int,
|
||||
expected_events_before_failed: list[dict[str, str]],
|
||||
) -> None:
|
||||
"""Test download and remove error when generating a backup."""
|
||||
client = await hass_ws_client(hass)
|
||||
@@ -1807,6 +1889,9 @@ async def test_reader_writer_create_download_remove_error(
|
||||
response = await client.receive_json()
|
||||
while "uploaded_bytes" in response["event"]:
|
||||
response = await client.receive_json()
|
||||
for expected_event in expected_events_before_failed:
|
||||
assert response["event"] == expected_event
|
||||
response = await client.receive_json()
|
||||
assert response["event"] == {
|
||||
"manager_state": "create_backup",
|
||||
"reason": "upload_failed",
|
||||
@@ -1974,6 +2059,14 @@ async def test_reader_writer_create_remote_backup(
|
||||
response = await client.receive_json()
|
||||
while "uploaded_bytes" in response["event"]:
|
||||
response = await client.receive_json()
|
||||
assert response["event"] == {
|
||||
"manager_state": "create_backup",
|
||||
"reason": None,
|
||||
"stage": "cleaning_up",
|
||||
"state": "in_progress",
|
||||
}
|
||||
|
||||
response = await client.receive_json()
|
||||
assert response["event"] == {
|
||||
"manager_state": "create_backup",
|
||||
"reason": None,
|
||||
|
||||
205
tests/components/intelliclima/snapshots/test_sensor.ambr
Normal file
205
tests/components/intelliclima/snapshots/test_sensor.ambr
Normal file
@@ -0,0 +1,205 @@
|
||||
# serializer version: 1
|
||||
# name: test_all_sensor_entities.6
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
'config_entries': <ANY>,
|
||||
'config_entries_subentries': <ANY>,
|
||||
'configuration_url': None,
|
||||
'connections': set({
|
||||
tuple(
|
||||
'bluetooth',
|
||||
'00:11:22:33:44:55',
|
||||
),
|
||||
tuple(
|
||||
'mac',
|
||||
'00:11:22:33:44:55',
|
||||
),
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'entry_type': None,
|
||||
'hw_version': None,
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'intelliclima',
|
||||
'56789',
|
||||
),
|
||||
}),
|
||||
'labels': set({
|
||||
}),
|
||||
'manufacturer': 'Fantini Cosmi',
|
||||
'model': 'ECOCOMFORT 2.0',
|
||||
'model_id': None,
|
||||
'name': 'Test VMC',
|
||||
'name_by_user': None,
|
||||
'primary_config_entry': <ANY>,
|
||||
'serial_number': '11223344',
|
||||
'sw_version': '0.6.8',
|
||||
'via_device_id': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_sensor_entities[sensor.test_vmc_humidity-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.test_vmc_humidity',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Humidity',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.HUMIDITY: 'humidity'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Humidity',
|
||||
'platform': 'intelliclima',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': '56789_humidity',
|
||||
'unit_of_measurement': '%',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_sensor_entities[sensor.test_vmc_humidity-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'humidity',
|
||||
'friendly_name': 'Test VMC Humidity',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': '%',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.test_vmc_humidity',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '65.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_sensor_entities[sensor.test_vmc_temperature-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.test_vmc_temperature',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Temperature',
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 1,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Temperature',
|
||||
'platform': 'intelliclima',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': '56789_temperature',
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_sensor_entities[sensor.test_vmc_temperature-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'temperature',
|
||||
'friendly_name': 'Test VMC Temperature',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.test_vmc_temperature',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '16.2',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_sensor_entities[sensor.test_vmc_volatile_organic_compounds_parts-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.test_vmc_volatile_organic_compounds_parts',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Volatile organic compounds parts',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS: 'volatile_organic_compounds_parts'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Volatile organic compounds parts',
|
||||
'platform': 'intelliclima',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': '56789_voc',
|
||||
'unit_of_measurement': 'ppm',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_sensor_entities[sensor.test_vmc_volatile_organic_compounds_parts-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'volatile_organic_compounds_parts',
|
||||
'friendly_name': 'Test VMC Volatile organic compounds parts',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'ppm',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.test_vmc_volatile_organic_compounds_parts',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '89.0',
|
||||
})
|
||||
# ---
|
||||
58
tests/components/intelliclima/test_sensor.py
Normal file
58
tests/components/intelliclima/test_sensor.py
Normal file
@@ -0,0 +1,58 @@
|
||||
"""Test IntelliClima Sensors."""
|
||||
|
||||
from collections.abc import AsyncGenerator
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def setup_intelliclima_sensor_only(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_cloud_interface: AsyncMock,
|
||||
) -> AsyncGenerator[None]:
|
||||
"""Set up IntelliClima integration with only the sensor platform."""
|
||||
with (
|
||||
patch("homeassistant.components.intelliclima.PLATFORMS", [Platform.SENSOR]),
|
||||
):
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
# Let tests run against this initialized state
|
||||
yield
|
||||
|
||||
|
||||
async def test_all_sensor_entities(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
mock_cloud_interface: AsyncMock,
|
||||
) -> None:
|
||||
"""Test all entities."""
|
||||
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
# There should be exactly three sensor entities
|
||||
sensor_entries = [
|
||||
entry
|
||||
for entry in entity_registry.entities.values()
|
||||
if entry.platform == "intelliclima" and entry.domain == SENSOR_DOMAIN
|
||||
]
|
||||
assert len(sensor_entries) == 3
|
||||
|
||||
entity_entry = sensor_entries[0]
|
||||
# Device should exist and match snapshot
|
||||
assert entity_entry.device_id
|
||||
assert (device_entry := device_registry.async_get(entity_entry.device_id))
|
||||
assert device_entry == snapshot
|
||||
@@ -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,
|
||||
|
||||
@@ -82,6 +82,7 @@ light:
|
||||
"""
|
||||
|
||||
import copy
|
||||
import json
|
||||
from typing import Any
|
||||
from unittest.mock import call, patch
|
||||
|
||||
@@ -100,6 +101,7 @@ from homeassistant.const import (
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.util.json import json_loads
|
||||
|
||||
from .common import (
|
||||
@@ -169,6 +171,70 @@ COLOR_MODES_CONFIG = {
|
||||
}
|
||||
}
|
||||
|
||||
GROUP_MEMBER_1_TOPIC = "homeassistant/light/member_1/config"
|
||||
GROUP_MEMBER_2_TOPIC = "homeassistant/light/member_2/config"
|
||||
GROUP_MEMBER_3_TOPIC = "homeassistant/light/member_3/config"
|
||||
GROUP_TOPIC = "homeassistant/light/group/config"
|
||||
GROUP_DISCOVERY_MEMBER_1_CONFIG = json.dumps(
|
||||
{
|
||||
"schema": "json",
|
||||
"command_topic": "test-command-topic-member1",
|
||||
"unique_id": "very_unique_member1",
|
||||
"name": "member1",
|
||||
"default_entity_id": "light.member1",
|
||||
}
|
||||
)
|
||||
GROUP_DISCOVERY_MEMBER_2_CONFIG = json.dumps(
|
||||
{
|
||||
"schema": "json",
|
||||
"command_topic": "test-command-topic-member2",
|
||||
"unique_id": "very_unique_member2",
|
||||
"name": "member2",
|
||||
"default_entity_id": "light.member2",
|
||||
}
|
||||
)
|
||||
GROUP_DISCOVERY_MEMBER_3_CONFIG = json.dumps(
|
||||
{
|
||||
"schema": "json",
|
||||
"command_topic": "test-command-topic-member3",
|
||||
"unique_id": "very_unique_member3",
|
||||
"name": "member3",
|
||||
"default_entity_id": "light.member3",
|
||||
}
|
||||
)
|
||||
GROUP_DISCOVERY_LIGHT_GROUP_CONFIG = json.dumps(
|
||||
{
|
||||
"schema": "json",
|
||||
"command_topic": "test-command-topic-group",
|
||||
"state_topic": "test-state-topic-group",
|
||||
"unique_id": "very_unique_group",
|
||||
"name": "group",
|
||||
"default_entity_id": "light.group",
|
||||
"group": ["very_unique_member1", "very_unique_member2"],
|
||||
}
|
||||
)
|
||||
GROUP_DISCOVERY_LIGHT_GROUP_CONFIG_EXPANDED = json.dumps(
|
||||
{
|
||||
"schema": "json",
|
||||
"command_topic": "test-command-topic-group",
|
||||
"state_topic": "test-state-topic-group",
|
||||
"unique_id": "very_unique_group",
|
||||
"name": "group",
|
||||
"default_entity_id": "light.group",
|
||||
"group": ["very_unique_member1", "very_unique_member2", "very_unique_member3"],
|
||||
}
|
||||
)
|
||||
GROUP_DISCOVERY_LIGHT_GROUP_CONFIG_NO_GROUP = json.dumps(
|
||||
{
|
||||
"schema": "json",
|
||||
"command_topic": "test-command-topic-group",
|
||||
"state_topic": "test-state-topic-group",
|
||||
"unique_id": "very_unique_group",
|
||||
"name": "group",
|
||||
"default_entity_id": "light.group",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class JsonValidator:
|
||||
"""Helper to compare JSON."""
|
||||
@@ -1859,6 +1925,144 @@ async def test_white_scale(
|
||||
assert state.attributes.get("brightness") == 129
|
||||
|
||||
|
||||
async def test_light_group_discovery_members_before_group(
|
||||
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
|
||||
) -> None:
|
||||
"""Test the discovery of a light group and linked entity IDs.
|
||||
|
||||
The members are discovered first, so they are known in the entity registry.
|
||||
"""
|
||||
await mqtt_mock_entry()
|
||||
# Discover light group members
|
||||
async_fire_mqtt_message(hass, GROUP_MEMBER_1_TOPIC, GROUP_DISCOVERY_MEMBER_1_CONFIG)
|
||||
async_fire_mqtt_message(hass, GROUP_MEMBER_2_TOPIC, GROUP_DISCOVERY_MEMBER_2_CONFIG)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Discover group
|
||||
async_fire_mqtt_message(hass, GROUP_TOPIC, GROUP_DISCOVERY_LIGHT_GROUP_CONFIG)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get("light.member1") is not None
|
||||
assert hass.states.get("light.member2") is not None
|
||||
group_state = hass.states.get("light.group")
|
||||
assert group_state is not None
|
||||
assert group_state.attributes.get("group_entities") == [
|
||||
"light.member1",
|
||||
"light.member2",
|
||||
]
|
||||
|
||||
# Now create and discover a new member
|
||||
async_fire_mqtt_message(hass, GROUP_MEMBER_3_TOPIC, GROUP_DISCOVERY_MEMBER_3_CONFIG)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Update the group discovery
|
||||
async_fire_mqtt_message(
|
||||
hass, GROUP_TOPIC, GROUP_DISCOVERY_LIGHT_GROUP_CONFIG_EXPANDED
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get("light.member1") is not None
|
||||
assert hass.states.get("light.member2") is not None
|
||||
assert hass.states.get("light.member3") is not None
|
||||
group_state = hass.states.get("light.group")
|
||||
assert group_state is not None
|
||||
assert group_state.attributes.get("group_entities") == [
|
||||
"light.member1",
|
||||
"light.member2",
|
||||
"light.member3",
|
||||
]
|
||||
|
||||
|
||||
async def test_light_group_discovery_group_before_members(
|
||||
hass: HomeAssistant,
|
||||
mqtt_mock_entry: MqttMockHAClientGenerator,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test the discovery of a light group and linked entity IDs.
|
||||
|
||||
The group is discovered first, so the group members are
|
||||
not (all) known yet in the entity registry.
|
||||
The entity property should be updated as soon as member entities
|
||||
are discovered, updated or removed.
|
||||
"""
|
||||
await mqtt_mock_entry()
|
||||
|
||||
# Discover group
|
||||
async_fire_mqtt_message(hass, GROUP_TOPIC, GROUP_DISCOVERY_LIGHT_GROUP_CONFIG)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Discover light group members
|
||||
async_fire_mqtt_message(hass, GROUP_MEMBER_1_TOPIC, GROUP_DISCOVERY_MEMBER_1_CONFIG)
|
||||
async_fire_mqtt_message(hass, GROUP_MEMBER_2_TOPIC, GROUP_DISCOVERY_MEMBER_2_CONFIG)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get("light.member1") is not None
|
||||
assert hass.states.get("light.member2") is not None
|
||||
|
||||
group_state = hass.states.get("light.group")
|
||||
assert group_state is not None
|
||||
assert group_state.attributes.get("group_entities") == [
|
||||
"light.member1",
|
||||
"light.member2",
|
||||
]
|
||||
|
||||
# Remove member 1
|
||||
async_fire_mqtt_message(hass, GROUP_MEMBER_1_TOPIC, "")
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get("light.member1") is None
|
||||
assert hass.states.get("light.member2") is not None
|
||||
|
||||
group_state = hass.states.get("light.group")
|
||||
assert group_state is not None
|
||||
assert group_state.attributes.get("group_entities") == ["light.member2"]
|
||||
|
||||
# Rename member 2
|
||||
entity_registry.async_update_entity(
|
||||
"light.member2", new_entity_id="light.member2_updated"
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
group_state = hass.states.get("light.group")
|
||||
assert group_state is not None
|
||||
assert group_state.attributes.get("group_entities") == ["light.member2_updated"]
|
||||
|
||||
|
||||
async def test_update_discovery_with_members_without_init(
|
||||
hass: HomeAssistant,
|
||||
mqtt_mock_entry: MqttMockHAClientGenerator,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test the discovery update of a light group and linked entity IDs."""
|
||||
await mqtt_mock_entry()
|
||||
# Discover light group members
|
||||
async_fire_mqtt_message(hass, GROUP_MEMBER_1_TOPIC, GROUP_DISCOVERY_MEMBER_1_CONFIG)
|
||||
async_fire_mqtt_message(hass, GROUP_MEMBER_2_TOPIC, GROUP_DISCOVERY_MEMBER_2_CONFIG)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Discover group without members
|
||||
async_fire_mqtt_message(
|
||||
hass, GROUP_TOPIC, GROUP_DISCOVERY_LIGHT_GROUP_CONFIG_NO_GROUP
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get("light.member1") is not None
|
||||
assert hass.states.get("light.member2") is not None
|
||||
group_state = hass.states.get("light.group")
|
||||
assert group_state is not None
|
||||
assert group_state.attributes.get("group_entities") is None
|
||||
|
||||
# Update the discovery with group members
|
||||
async_fire_mqtt_message(hass, GROUP_TOPIC, GROUP_DISCOVERY_LIGHT_GROUP_CONFIG)
|
||||
await hass.async_block_till_done()
|
||||
assert "Group member update received for entity" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"hass_config",
|
||||
[
|
||||
@@ -2040,7 +2244,7 @@ async def test_custom_availability_payload(
|
||||
)
|
||||
|
||||
|
||||
async def test_setting_attribute_via_mqtt_json_message(
|
||||
async def test_setting_attribute_via_mqtt_json_message_single_light(
|
||||
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
|
||||
) -> None:
|
||||
"""Test the setting of attribute via MQTT with JSON payload."""
|
||||
@@ -2049,6 +2253,54 @@ async def test_setting_attribute_via_mqtt_json_message(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"hass_config",
|
||||
[
|
||||
help_custom_config(
|
||||
light.DOMAIN,
|
||||
DEFAULT_CONFIG,
|
||||
(
|
||||
{
|
||||
"unique_id": "very_unique_member_1",
|
||||
"name": "Part 1",
|
||||
"default_entity_id": "light.member_1",
|
||||
},
|
||||
{
|
||||
"unique_id": "very_unique_member_2",
|
||||
"name": "Part 2",
|
||||
"default_entity_id": "light.member_2",
|
||||
},
|
||||
{
|
||||
"unique_id": "very_unique_group",
|
||||
"name": "My group",
|
||||
"default_entity_id": "light.my_group",
|
||||
"json_attributes_topic": "attr-topic",
|
||||
"group": [
|
||||
"very_unique_member_1",
|
||||
"very_unique_member_2",
|
||||
"member_3_not_exists",
|
||||
],
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
async def test_setting_attribute_via_mqtt_json_message_light_group(
|
||||
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
|
||||
) -> None:
|
||||
"""Test the setting of attribute via MQTT with JSON payload."""
|
||||
await mqtt_mock_entry()
|
||||
|
||||
async_fire_mqtt_message(hass, "attr-topic", '{ "val": "100" }')
|
||||
state = hass.states.get("light.my_group")
|
||||
|
||||
assert state and state.attributes.get("val") == "100"
|
||||
assert state.attributes.get("group_entities") == [
|
||||
"light.member_1",
|
||||
"light.member_2",
|
||||
]
|
||||
|
||||
|
||||
async def test_setting_blocked_attribute_via_mqtt_json_message(
|
||||
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
|
||||
) -> None:
|
||||
|
||||
@@ -3082,63 +3082,6 @@
|
||||
'state': '46.92',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.test_energy_remaining_2-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': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.test_energy_remaining_2',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Energy remaining',
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 1,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENERGY_STORAGE: 'energy_storage'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Energy remaining',
|
||||
'platform': 'tessie',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'energy_remaining',
|
||||
'unique_id': 'VINVINVIN-energy_remaining',
|
||||
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.test_energy_remaining_2-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'energy_storage',
|
||||
'friendly_name': 'Test Energy remaining',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.test_energy_remaining_2',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '55.2',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.test_inside_temperature-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
|
||||
1
tests/components/window/__init__.py
Normal file
1
tests/components/window/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Tests for the window integration."""
|
||||
646
tests/components/window/test_trigger.py
Normal file
646
tests/components/window/test_trigger.py
Normal file
@@ -0,0 +1,646 @@
|
||||
"""Test window trigger."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.cover import ATTR_IS_CLOSED, CoverState
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_CLASS,
|
||||
ATTR_LABEL_ID,
|
||||
CONF_ENTITY_ID,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
|
||||
from tests.components import (
|
||||
TriggerStateDescription,
|
||||
arm_trigger,
|
||||
parametrize_target_entities,
|
||||
parametrize_trigger_states,
|
||||
set_or_remove_state,
|
||||
target_entities,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def target_binary_sensors(hass: HomeAssistant) -> dict[str, list[str]]:
|
||||
"""Create multiple binary sensor entities associated with different targets."""
|
||||
return await target_entities(hass, "binary_sensor")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def target_covers(hass: HomeAssistant) -> dict[str, list[str]]:
|
||||
"""Create multiple cover entities associated with different targets."""
|
||||
return await target_entities(hass, "cover")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"trigger_key",
|
||||
[
|
||||
"window.opened",
|
||||
"window.closed",
|
||||
],
|
||||
)
|
||||
async def test_window_triggers_gated_by_labs_flag(
|
||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, trigger_key: str
|
||||
) -> None:
|
||||
"""Test the window triggers are gated by the labs flag."""
|
||||
await arm_trigger(hass, trigger_key, None, {ATTR_LABEL_ID: "test_label"})
|
||||
assert (
|
||||
"Unnamed automation failed to setup triggers and has been disabled: Trigger "
|
||||
f"'{trigger_key}' requires the experimental 'New triggers and conditions' "
|
||||
"feature to be enabled in Home Assistant Labs settings (feature flag: "
|
||||
"'new_triggers_conditions')"
|
||||
) in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("binary_sensor"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "states"),
|
||||
[
|
||||
*parametrize_trigger_states(
|
||||
trigger="window.opened",
|
||||
target_states=[STATE_ON],
|
||||
other_states=[STATE_OFF],
|
||||
additional_attributes={ATTR_DEVICE_CLASS: "window"},
|
||||
trigger_from_none=False,
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger="window.closed",
|
||||
target_states=[STATE_OFF],
|
||||
other_states=[STATE_ON],
|
||||
additional_attributes={ATTR_DEVICE_CLASS: "window"},
|
||||
trigger_from_none=False,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_window_trigger_binary_sensor_behavior_any(
|
||||
hass: HomeAssistant,
|
||||
service_calls: list[ServiceCall],
|
||||
target_binary_sensors: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
trigger: str,
|
||||
trigger_options: dict[str, Any],
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test window trigger fires for binary_sensor entities with device_class window."""
|
||||
other_entity_ids = set(target_binary_sensors["included"]) - {entity_id}
|
||||
excluded_entity_ids = set(target_binary_sensors["excluded"]) - {entity_id}
|
||||
|
||||
for eid in target_binary_sensors["included"]:
|
||||
set_or_remove_state(hass, eid, states[0]["included"])
|
||||
await hass.async_block_till_done()
|
||||
for eid in excluded_entity_ids:
|
||||
set_or_remove_state(hass, eid, states[0]["excluded"])
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await arm_trigger(hass, trigger, {}, trigger_target_config)
|
||||
|
||||
for state in states[1:]:
|
||||
excluded_state = state["excluded"]
|
||||
included_state = state["included"]
|
||||
set_or_remove_state(hass, entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == state["count"]
|
||||
for service_call in service_calls:
|
||||
assert service_call.data[CONF_ENTITY_ID] == entity_id
|
||||
service_calls.clear()
|
||||
|
||||
for other_entity_id in other_entity_ids:
|
||||
set_or_remove_state(hass, other_entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
for excluded_entity_id in excluded_entity_ids:
|
||||
set_or_remove_state(hass, excluded_entity_id, excluded_state)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == (entities_in_target - 1) * state["count"]
|
||||
service_calls.clear()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("cover"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "states"),
|
||||
[
|
||||
*parametrize_trigger_states(
|
||||
trigger="window.opened",
|
||||
target_states=[
|
||||
(CoverState.OPEN, {ATTR_IS_CLOSED: False}),
|
||||
(CoverState.OPENING, {ATTR_IS_CLOSED: False}),
|
||||
],
|
||||
other_states=[
|
||||
(CoverState.CLOSED, {ATTR_IS_CLOSED: True}),
|
||||
(CoverState.CLOSING, {ATTR_IS_CLOSED: True}),
|
||||
],
|
||||
extra_invalid_states=[
|
||||
(CoverState.OPEN, {ATTR_IS_CLOSED: None}),
|
||||
(CoverState.OPEN, {}),
|
||||
],
|
||||
additional_attributes={ATTR_DEVICE_CLASS: "window"},
|
||||
trigger_from_none=False,
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger="window.closed",
|
||||
target_states=[
|
||||
(CoverState.CLOSED, {ATTR_IS_CLOSED: True}),
|
||||
(CoverState.CLOSING, {ATTR_IS_CLOSED: True}),
|
||||
],
|
||||
other_states=[
|
||||
(CoverState.OPEN, {ATTR_IS_CLOSED: False}),
|
||||
(CoverState.OPENING, {ATTR_IS_CLOSED: False}),
|
||||
(CoverState.CLOSING, {ATTR_IS_CLOSED: False}),
|
||||
],
|
||||
extra_invalid_states=[
|
||||
(CoverState.OPEN, {ATTR_IS_CLOSED: None}),
|
||||
(CoverState.OPEN, {}),
|
||||
],
|
||||
additional_attributes={ATTR_DEVICE_CLASS: "window"},
|
||||
trigger_from_none=False,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_window_trigger_cover_behavior_any(
|
||||
hass: HomeAssistant,
|
||||
service_calls: list[ServiceCall],
|
||||
target_covers: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
trigger: str,
|
||||
trigger_options: dict[str, Any],
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test window trigger fires for cover entities with device_class window."""
|
||||
other_entity_ids = set(target_covers["included"]) - {entity_id}
|
||||
excluded_entity_ids = set(target_covers["excluded"]) - {entity_id}
|
||||
|
||||
for eid in target_covers["included"]:
|
||||
set_or_remove_state(hass, eid, states[0]["included"])
|
||||
await hass.async_block_till_done()
|
||||
for eid in excluded_entity_ids:
|
||||
set_or_remove_state(hass, eid, states[0]["excluded"])
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await arm_trigger(hass, trigger, {}, trigger_target_config)
|
||||
|
||||
for state in states[1:]:
|
||||
excluded_state = state["excluded"]
|
||||
included_state = state["included"]
|
||||
set_or_remove_state(hass, entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == state["count"]
|
||||
for service_call in service_calls:
|
||||
assert service_call.data[CONF_ENTITY_ID] == entity_id
|
||||
service_calls.clear()
|
||||
|
||||
for other_entity_id in other_entity_ids:
|
||||
set_or_remove_state(hass, other_entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
for excluded_entity_id in excluded_entity_ids:
|
||||
set_or_remove_state(hass, excluded_entity_id, excluded_state)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == (entities_in_target - 1) * state["count"]
|
||||
service_calls.clear()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("binary_sensor"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "states"),
|
||||
[
|
||||
*parametrize_trigger_states(
|
||||
trigger="window.opened",
|
||||
target_states=[STATE_ON],
|
||||
other_states=[STATE_OFF],
|
||||
additional_attributes={ATTR_DEVICE_CLASS: "window"},
|
||||
trigger_from_none=False,
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger="window.closed",
|
||||
target_states=[STATE_OFF],
|
||||
other_states=[STATE_ON],
|
||||
additional_attributes={ATTR_DEVICE_CLASS: "window"},
|
||||
trigger_from_none=False,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_window_trigger_binary_sensor_behavior_first(
|
||||
hass: HomeAssistant,
|
||||
service_calls: list[ServiceCall],
|
||||
target_binary_sensors: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
trigger: str,
|
||||
trigger_options: dict[str, Any],
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test window trigger fires on the first binary_sensor state change."""
|
||||
other_entity_ids = set(target_binary_sensors["included"]) - {entity_id}
|
||||
excluded_entity_ids = set(target_binary_sensors["excluded"]) - {entity_id}
|
||||
|
||||
for eid in target_binary_sensors["included"]:
|
||||
set_or_remove_state(hass, eid, states[0]["included"])
|
||||
await hass.async_block_till_done()
|
||||
for eid in excluded_entity_ids:
|
||||
set_or_remove_state(hass, eid, states[0]["excluded"])
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await arm_trigger(hass, trigger, {"behavior": "first"}, trigger_target_config)
|
||||
|
||||
for state in states[1:]:
|
||||
excluded_state = state["excluded"]
|
||||
included_state = state["included"]
|
||||
set_or_remove_state(hass, entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == state["count"]
|
||||
for service_call in service_calls:
|
||||
assert service_call.data[CONF_ENTITY_ID] == entity_id
|
||||
service_calls.clear()
|
||||
|
||||
for other_entity_id in other_entity_ids:
|
||||
set_or_remove_state(hass, other_entity_id, excluded_state)
|
||||
await hass.async_block_till_done()
|
||||
for excluded_entity_id in excluded_entity_ids:
|
||||
set_or_remove_state(hass, excluded_entity_id, excluded_state)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("binary_sensor"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "states"),
|
||||
[
|
||||
*parametrize_trigger_states(
|
||||
trigger="window.opened",
|
||||
target_states=[STATE_ON],
|
||||
other_states=[STATE_OFF],
|
||||
additional_attributes={ATTR_DEVICE_CLASS: "window"},
|
||||
trigger_from_none=False,
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger="window.closed",
|
||||
target_states=[STATE_OFF],
|
||||
other_states=[STATE_ON],
|
||||
additional_attributes={ATTR_DEVICE_CLASS: "window"},
|
||||
trigger_from_none=False,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_window_trigger_binary_sensor_behavior_last(
|
||||
hass: HomeAssistant,
|
||||
service_calls: list[ServiceCall],
|
||||
target_binary_sensors: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
trigger: str,
|
||||
trigger_options: dict[str, Any],
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test window trigger fires when the last binary_sensor changes state."""
|
||||
other_entity_ids = set(target_binary_sensors["included"]) - {entity_id}
|
||||
excluded_entity_ids = set(target_binary_sensors["excluded"]) - {entity_id}
|
||||
|
||||
for eid in target_binary_sensors["included"]:
|
||||
set_or_remove_state(hass, eid, states[0]["included"])
|
||||
await hass.async_block_till_done()
|
||||
for eid in excluded_entity_ids:
|
||||
set_or_remove_state(hass, eid, states[0]["excluded"])
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await arm_trigger(hass, trigger, {"behavior": "last"}, trigger_target_config)
|
||||
|
||||
for state in states[1:]:
|
||||
excluded_state = state["excluded"]
|
||||
included_state = state["included"]
|
||||
for other_entity_id in other_entity_ids:
|
||||
set_or_remove_state(hass, other_entity_id, excluded_state)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
set_or_remove_state(hass, entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == state["count"]
|
||||
for service_call in service_calls:
|
||||
assert service_call.data[CONF_ENTITY_ID] == entity_id
|
||||
service_calls.clear()
|
||||
|
||||
for excluded_entity_id in excluded_entity_ids:
|
||||
set_or_remove_state(hass, excluded_entity_id, excluded_state)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("cover"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "states"),
|
||||
[
|
||||
*parametrize_trigger_states(
|
||||
trigger="window.opened",
|
||||
target_states=[
|
||||
(CoverState.OPEN, {ATTR_IS_CLOSED: False}),
|
||||
(CoverState.OPENING, {ATTR_IS_CLOSED: False}),
|
||||
],
|
||||
other_states=[
|
||||
(CoverState.CLOSED, {ATTR_IS_CLOSED: True}),
|
||||
(CoverState.CLOSING, {ATTR_IS_CLOSED: True}),
|
||||
],
|
||||
extra_invalid_states=[
|
||||
(CoverState.OPEN, {ATTR_IS_CLOSED: None}),
|
||||
(CoverState.OPEN, {}),
|
||||
],
|
||||
additional_attributes={ATTR_DEVICE_CLASS: "window"},
|
||||
trigger_from_none=False,
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger="window.closed",
|
||||
target_states=[
|
||||
(CoverState.CLOSED, {ATTR_IS_CLOSED: True}),
|
||||
(CoverState.CLOSING, {ATTR_IS_CLOSED: True}),
|
||||
],
|
||||
other_states=[
|
||||
(CoverState.OPEN, {ATTR_IS_CLOSED: False}),
|
||||
(CoverState.OPENING, {ATTR_IS_CLOSED: False}),
|
||||
(CoverState.CLOSING, {ATTR_IS_CLOSED: False}),
|
||||
],
|
||||
extra_invalid_states=[
|
||||
(CoverState.OPEN, {ATTR_IS_CLOSED: None}),
|
||||
(CoverState.OPEN, {}),
|
||||
],
|
||||
additional_attributes={ATTR_DEVICE_CLASS: "window"},
|
||||
trigger_from_none=False,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_window_trigger_cover_behavior_first(
|
||||
hass: HomeAssistant,
|
||||
service_calls: list[ServiceCall],
|
||||
target_covers: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
trigger: str,
|
||||
trigger_options: dict[str, Any],
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test window trigger fires on the first cover state change."""
|
||||
other_entity_ids = set(target_covers["included"]) - {entity_id}
|
||||
excluded_entity_ids = set(target_covers["excluded"]) - {entity_id}
|
||||
|
||||
for eid in target_covers["included"]:
|
||||
set_or_remove_state(hass, eid, states[0]["included"])
|
||||
await hass.async_block_till_done()
|
||||
for eid in excluded_entity_ids:
|
||||
set_or_remove_state(hass, eid, states[0]["excluded"])
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await arm_trigger(hass, trigger, {"behavior": "first"}, trigger_target_config)
|
||||
|
||||
for state in states[1:]:
|
||||
excluded_state = state["excluded"]
|
||||
included_state = state["included"]
|
||||
set_or_remove_state(hass, entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == state["count"]
|
||||
for service_call in service_calls:
|
||||
assert service_call.data[CONF_ENTITY_ID] == entity_id
|
||||
service_calls.clear()
|
||||
|
||||
for other_entity_id in other_entity_ids:
|
||||
set_or_remove_state(hass, other_entity_id, excluded_state)
|
||||
await hass.async_block_till_done()
|
||||
for excluded_entity_id in excluded_entity_ids:
|
||||
set_or_remove_state(hass, excluded_entity_id, excluded_state)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("cover"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "states"),
|
||||
[
|
||||
*parametrize_trigger_states(
|
||||
trigger="window.opened",
|
||||
target_states=[
|
||||
(CoverState.OPEN, {ATTR_IS_CLOSED: False}),
|
||||
(CoverState.OPENING, {ATTR_IS_CLOSED: False}),
|
||||
],
|
||||
other_states=[
|
||||
(CoverState.CLOSED, {ATTR_IS_CLOSED: True}),
|
||||
(CoverState.CLOSING, {ATTR_IS_CLOSED: True}),
|
||||
],
|
||||
extra_invalid_states=[
|
||||
(CoverState.OPEN, {ATTR_IS_CLOSED: None}),
|
||||
(CoverState.OPEN, {}),
|
||||
],
|
||||
additional_attributes={ATTR_DEVICE_CLASS: "window"},
|
||||
trigger_from_none=False,
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger="window.closed",
|
||||
target_states=[
|
||||
(CoverState.CLOSED, {ATTR_IS_CLOSED: True}),
|
||||
(CoverState.CLOSING, {ATTR_IS_CLOSED: True}),
|
||||
],
|
||||
other_states=[
|
||||
(CoverState.OPEN, {ATTR_IS_CLOSED: False}),
|
||||
(CoverState.OPENING, {ATTR_IS_CLOSED: False}),
|
||||
(CoverState.CLOSING, {ATTR_IS_CLOSED: False}),
|
||||
],
|
||||
extra_invalid_states=[
|
||||
(CoverState.OPEN, {ATTR_IS_CLOSED: None}),
|
||||
(CoverState.OPEN, {}),
|
||||
],
|
||||
additional_attributes={ATTR_DEVICE_CLASS: "window"},
|
||||
trigger_from_none=False,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_window_trigger_cover_behavior_last(
|
||||
hass: HomeAssistant,
|
||||
service_calls: list[ServiceCall],
|
||||
target_covers: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
trigger: str,
|
||||
trigger_options: dict[str, Any],
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test window trigger fires when the last cover changes state."""
|
||||
other_entity_ids = set(target_covers["included"]) - {entity_id}
|
||||
excluded_entity_ids = set(target_covers["excluded"]) - {entity_id}
|
||||
|
||||
for eid in target_covers["included"]:
|
||||
set_or_remove_state(hass, eid, states[0]["included"])
|
||||
await hass.async_block_till_done()
|
||||
for eid in excluded_entity_ids:
|
||||
set_or_remove_state(hass, eid, states[0]["excluded"])
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await arm_trigger(hass, trigger, {"behavior": "last"}, trigger_target_config)
|
||||
|
||||
for state in states[1:]:
|
||||
excluded_state = state["excluded"]
|
||||
included_state = state["included"]
|
||||
for other_entity_id in other_entity_ids:
|
||||
set_or_remove_state(hass, other_entity_id, excluded_state)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
set_or_remove_state(hass, entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == state["count"]
|
||||
for service_call in service_calls:
|
||||
assert service_call.data[CONF_ENTITY_ID] == entity_id
|
||||
service_calls.clear()
|
||||
|
||||
for excluded_entity_id in excluded_entity_ids:
|
||||
set_or_remove_state(hass, excluded_entity_id, excluded_state)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"trigger_key",
|
||||
"binary_sensor_initial",
|
||||
"binary_sensor_target",
|
||||
"cover_initial",
|
||||
"cover_initial_is_closed",
|
||||
"cover_target",
|
||||
"cover_target_is_closed",
|
||||
),
|
||||
[
|
||||
(
|
||||
"window.opened",
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
CoverState.CLOSED,
|
||||
True,
|
||||
CoverState.OPEN,
|
||||
False,
|
||||
),
|
||||
(
|
||||
"window.closed",
|
||||
STATE_ON,
|
||||
STATE_OFF,
|
||||
CoverState.OPEN,
|
||||
False,
|
||||
CoverState.CLOSED,
|
||||
True,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_window_trigger_excludes_non_window_device_class(
|
||||
hass: HomeAssistant,
|
||||
service_calls: list[ServiceCall],
|
||||
trigger_key: str,
|
||||
binary_sensor_initial: str,
|
||||
binary_sensor_target: str,
|
||||
cover_initial: str,
|
||||
cover_initial_is_closed: bool,
|
||||
cover_target: str,
|
||||
cover_target_is_closed: bool,
|
||||
) -> None:
|
||||
"""Test window trigger does not fire for entities without device_class window."""
|
||||
entity_id_window = "binary_sensor.test_window"
|
||||
entity_id_door = "binary_sensor.test_door"
|
||||
entity_id_cover_window = "cover.test_window"
|
||||
entity_id_cover_door = "cover.test_door"
|
||||
|
||||
# Set initial states
|
||||
hass.states.async_set(
|
||||
entity_id_window, binary_sensor_initial, {ATTR_DEVICE_CLASS: "window"}
|
||||
)
|
||||
hass.states.async_set(
|
||||
entity_id_door, binary_sensor_initial, {ATTR_DEVICE_CLASS: "door"}
|
||||
)
|
||||
hass.states.async_set(
|
||||
entity_id_cover_window,
|
||||
cover_initial,
|
||||
{ATTR_DEVICE_CLASS: "window", ATTR_IS_CLOSED: cover_initial_is_closed},
|
||||
)
|
||||
hass.states.async_set(
|
||||
entity_id_cover_door,
|
||||
cover_initial,
|
||||
{ATTR_DEVICE_CLASS: "door", ATTR_IS_CLOSED: cover_initial_is_closed},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await arm_trigger(
|
||||
hass,
|
||||
trigger_key,
|
||||
{},
|
||||
{
|
||||
CONF_ENTITY_ID: [
|
||||
entity_id_window,
|
||||
entity_id_door,
|
||||
entity_id_cover_window,
|
||||
entity_id_cover_door,
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
# Window binary_sensor changes - should trigger
|
||||
hass.states.async_set(
|
||||
entity_id_window, binary_sensor_target, {ATTR_DEVICE_CLASS: "window"}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id_window
|
||||
service_calls.clear()
|
||||
|
||||
# Door binary_sensor changes - should NOT trigger (wrong device class)
|
||||
hass.states.async_set(
|
||||
entity_id_door, binary_sensor_target, {ATTR_DEVICE_CLASS: "door"}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
# Cover window changes - should trigger
|
||||
hass.states.async_set(
|
||||
entity_id_cover_window,
|
||||
cover_target,
|
||||
{ATTR_DEVICE_CLASS: "window", ATTR_IS_CLOSED: cover_target_is_closed},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id_cover_window
|
||||
service_calls.clear()
|
||||
|
||||
# Door cover changes - should NOT trigger (wrong device class)
|
||||
hass.states.async_set(
|
||||
entity_id_cover_door,
|
||||
cover_target,
|
||||
{ATTR_DEVICE_CLASS: "door", ATTR_IS_CLOSED: cover_target_is_closed},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
@@ -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",
|
||||
|
||||
@@ -94,13 +94,11 @@ from homeassistant.components.zwave_js.const import (
|
||||
ATTR_PARAMETERS,
|
||||
ATTR_WAIT_FOR_RESULT,
|
||||
CONF_DATA_COLLECTION_OPTED_IN,
|
||||
CONF_INSTALLER_MODE,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.components.zwave_js.helpers import get_device_id
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import MockConfigEntry, MockUser
|
||||
from tests.typing import ClientSessionGenerator, WebSocketGenerator
|
||||
@@ -5397,36 +5395,6 @@ async def test_invoke_cc_api(
|
||||
assert msg["error"] == {"code": "NotFoundError", "message": ""}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config", "installer_mode"), [({}, False), ({CONF_INSTALLER_MODE: True}, True)]
|
||||
)
|
||||
async def test_get_integration_settings(
|
||||
config: dict[str, Any],
|
||||
installer_mode: bool,
|
||||
hass: HomeAssistant,
|
||||
client: MagicMock,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
) -> None:
|
||||
"""Test that the get_integration_settings WS API call works."""
|
||||
ws_client = await hass_ws_client(hass)
|
||||
|
||||
entry = MockConfigEntry(domain="zwave_js", data={"url": "ws://test.org"})
|
||||
entry.add_to_hass(hass)
|
||||
assert await async_setup_component(hass, DOMAIN, {DOMAIN: config})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await ws_client.send_json_auto_id(
|
||||
{
|
||||
TYPE: "zwave_js/get_integration_settings",
|
||||
}
|
||||
)
|
||||
msg = await ws_client.receive_json()
|
||||
assert msg["success"]
|
||||
assert msg["result"] == {
|
||||
CONF_INSTALLER_MODE: installer_mode,
|
||||
}
|
||||
|
||||
|
||||
async def test_backup_nvm(
|
||||
hass: HomeAssistant,
|
||||
integration,
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -2411,6 +2411,28 @@ async def test_entity_service_call_warn_referenced(
|
||||
) in caplog.text
|
||||
|
||||
|
||||
async def test_entity_service_call_warn_unavailable(
|
||||
hass: HomeAssistant,
|
||||
mock_entities: dict[str, MockEntity],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test that explicitly referenced unavailable entities are logged."""
|
||||
mock_entities["light.kitchen"] = MockEntity(
|
||||
entity_id="light.kitchen", available=False
|
||||
)
|
||||
|
||||
call = ServiceCall(
|
||||
hass,
|
||||
"test_domain",
|
||||
"test_service",
|
||||
{"entity_id": ["light.kitchen"]},
|
||||
)
|
||||
await service.entity_service_call(hass, mock_entities, "", call)
|
||||
assert (
|
||||
"Referenced entities light.kitchen are missing or not currently available"
|
||||
) in caplog.text
|
||||
|
||||
|
||||
async def test_async_extract_entities_warn_referenced(
|
||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
'weather',
|
||||
'web_rtc',
|
||||
'websocket_api',
|
||||
'window',
|
||||
'zone',
|
||||
})
|
||||
# ---
|
||||
@@ -199,6 +200,7 @@
|
||||
'weather',
|
||||
'web_rtc',
|
||||
'websocket_api',
|
||||
'window',
|
||||
'zone',
|
||||
})
|
||||
# ---
|
||||
|
||||
Reference in New Issue
Block a user