Compare commits

..

15 Commits

Author SHA1 Message Date
Josef Zweck
35878bb203 Bump onedrive-personal-sdk to 0.1.7 (#165401) 2026-03-12 21:59:40 +01:00
Arie Catsman
e14d88ff55 Bump pyenphase to 2.4.6 (#165402) 2026-03-12 20:06:49 +00:00
Erwin Douna
d04efbfe48 Add platinum badge to Portainer (#165048)
Co-authored-by: Ariel Ebersberger <31776703+justanotherariel@users.noreply.github.com>
2026-03-12 19:30:31 +01:00
AlCalzone
3f35cd5cd2 Remove Z-Wave Installer panel (#165388)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: AlCalzone <17641229+AlCalzone@users.noreply.github.com>
2026-03-12 17:30:28 +01:00
AlCalzone
86ffd58665 Instruct AI to add type annotations to tests (#165386)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-12 17:10:30 +01:00
prana-dev-official
6206392b28 Bump prana-local-api to 0.12.0 (#165394) 2026-03-12 17:05:26 +01:00
dvdinth
b7c36c707f Add IntelliClima Sensor platform (#163901)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Norbert Rittel <norbert@rittel.de>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-03-12 16:33:34 +01:00
Joakim Sørensen
973c32b99d Add latency results if available to the support package (#165377) 2026-03-12 10:44:08 +01:00
Erik Montnemery
951775bea6 Add window triggers (#165230) 2026-03-12 10:18:42 +01:00
Artur Pragacz
0f2dbdf4f4 Fix logging of unavailable entities in entity call (#165370) 2026-03-12 09:53:30 +01:00
Jan-Philipp Benecke
443ff7efe1 Bump aiowebdav2 to 0.6.2 (#165353) 2026-03-12 08:17:41 +01:00
Jeef
0ee6b954df Bump intellifire4py to 4.4.0 (#165356) 2026-03-12 08:15:48 +01:00
Norbert Rittel
5681acf0e1 Sentence-case "API token" and "username/password" in growatt (#165368) 2026-03-12 07:49:35 +01:00
Andres Ruiz
a94458b8bc Bump waterfurnace version v1.6.2 (#165348) 2026-03-12 07:49:12 +01:00
Josef Zweck
f3c38ba2d3 Add "cleaning_up" stage to backup (#165349) 2026-03-12 07:28:17 +01:00
47 changed files with 1375 additions and 105 deletions

View File

@@ -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.

View File

@@ -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
View File

@@ -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

View File

@@ -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.

View File

@@ -161,6 +161,7 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
"text",
"update",
"vacuum",
"window",
}

View File

@@ -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:

View File

@@ -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()

View File

@@ -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."

View File

@@ -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"
}

View File

@@ -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(

View 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)

View File

@@ -12,5 +12,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["intellifire4py"],
"requirements": ["intellifire4py==4.3.1"]
"requirements": ["intellifire4py==4.4.0"]
}

View File

@@ -140,7 +140,6 @@ MQTT_ATTRIBUTES_BLOCKED = {
"entity_registry_enabled_default",
"extra_state_attributes",
"force_update",
"group_entities",
"icon",
"friendly_name",
"should_poll",

View File

@@ -10,5 +10,5 @@
"iot_class": "cloud_polling",
"loggers": ["onedrive_personal_sdk"],
"quality_scale": "platinum",
"requirements": ["onedrive-personal-sdk==0.1.6"]
"requirements": ["onedrive-personal-sdk==0.1.7"]
}

View File

@@ -10,5 +10,5 @@
"iot_class": "cloud_polling",
"loggers": ["onedrive_personal_sdk"],
"quality_scale": "platinum",
"requirements": ["onedrive-personal-sdk==0.1.6"]
"requirements": ["onedrive-personal-sdk==0.1.7"]
}

View File

@@ -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"]
}

View File

@@ -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

View File

@@ -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

View File

@@ -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."

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["waterfurnace"],
"quality_scale": "legacy",
"requirements": ["waterfurnace==1.5.1"]
"requirements": ["waterfurnace==1.6.2"]
}

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["aiowebdav2"],
"quality_scale": "bronze",
"requirements": ["aiowebdav2==0.6.1"]
"requirements": ["aiowebdav2==0.6.2"]
}

View 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

View File

@@ -0,0 +1,10 @@
{
"triggers": {
"closed": {
"trigger": "mdi:window-closed"
},
"opened": {
"trigger": "mdi:window-open"
}
}
}

View 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"
}

View 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"
}
}
}

View 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

View 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

View File

@@ -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(

View File

@@ -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(
{

View File

@@ -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"

View File

@@ -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

12
requirements_all.txt generated
View File

@@ -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
@@ -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
@@ -1676,7 +1676,7 @@ ondilo==0.5.0
# homeassistant.components.onedrive
# homeassistant.components.onedrive_for_business
onedrive-personal-sdk==0.1.6
onedrive-personal-sdk==0.1.7
# homeassistant.components.onvif
onvif-zeep-async==4.0.4
@@ -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

View File

@@ -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
@@ -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
@@ -1462,7 +1462,7 @@ ondilo==0.5.0
# homeassistant.components.onedrive
# homeassistant.components.onedrive_for_business
onedrive-personal-sdk==0.1.6
onedrive-personal-sdk==0.1.7
# homeassistant.components.onvif
onvif-zeep-async==4.0.4
@@ -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

View File

@@ -123,6 +123,7 @@ NO_IOT_CLASS = [
"web_rtc",
"webhook",
"websocket_api",
"window",
"zone",
]

View File

@@ -2157,6 +2157,7 @@ NO_QUALITY_SCALE = [
"web_rtc",
"webhook",
"websocket_api",
"window",
"zone",
]

View File

@@ -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()

View File

@@ -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(

View File

@@ -87,6 +87,13 @@
</details>
## Latency by location
Location | Latency (ms)
--- | ---
Earth | 13.37
Moon | N/A
## Installed packages
<details><summary>Installed packages</summary>

View File

@@ -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"})

View File

@@ -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,

View 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',
})
# ---

View 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

View File

@@ -0,0 +1 @@
"""Tests for the window integration."""

View 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

View File

@@ -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,

View File

@@ -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:

View File

@@ -98,6 +98,7 @@
'weather',
'web_rtc',
'websocket_api',
'window',
'zone',
})
# ---
@@ -199,6 +200,7 @@
'weather',
'web_rtc',
'websocket_api',
'window',
'zone',
})
# ---