Compare commits

..

5 Commits

Author SHA1 Message Date
Paul Bottein
bfdab0a219 Remove unused mock 2026-03-17 17:18:33 +01:00
Paul Bottein
1af85be44f Parametrize tests 2026-03-17 17:16:47 +01:00
Paul Bottein
1e7210c7e0 Fix import order 2026-03-17 17:16:47 +01:00
Copilot
551d52dd3c Fix state_attr_translated to preserve original type for non-string attributes (#165320)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: piitaya <5878303+piitaya@users.noreply.github.com>
2026-03-17 17:16:47 +01:00
Paul Bottein
29d3e71627 Add state_attr_translated template filter and function 2026-03-17 17:16:46 +01:00
72 changed files with 1336 additions and 2397 deletions

View File

@@ -46,10 +46,19 @@ async def async_setup_entry(
api.AsyncConfigEntryAuth(aiohttp_client.async_get_clientsession(hass), session)
)
coordinator = AladdinConnectCoordinator(hass, entry, client)
await coordinator.async_config_entry_first_refresh()
try:
doors = await client.get_doors()
except aiohttp.ClientResponseError as err:
if 400 <= err.status < 500:
raise ConfigEntryAuthFailed(err) from err
raise ConfigEntryNotReady from err
except aiohttp.ClientError as err:
raise ConfigEntryNotReady from err
entry.runtime_data = coordinator
entry.runtime_data = {
door.unique_id: AladdinConnectCoordinator(hass, entry, client, door)
for door in doors
}
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@@ -91,7 +100,7 @@ def remove_stale_devices(
device_entries = dr.async_entries_for_config_entry(
device_registry, config_entry.entry_id
)
all_device_ids = set(config_entry.runtime_data.data)
all_device_ids = set(config_entry.runtime_data)
for device_entry in device_entries:
device_id: str | None = None

View File

@@ -11,24 +11,22 @@ from genie_partner_sdk.model import GarageDoor
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
_LOGGER = logging.getLogger(__name__)
type AladdinConnectConfigEntry = ConfigEntry[AladdinConnectCoordinator]
type AladdinConnectConfigEntry = ConfigEntry[dict[str, AladdinConnectCoordinator]]
SCAN_INTERVAL = timedelta(seconds=15)
class AladdinConnectCoordinator(DataUpdateCoordinator[dict[str, GarageDoor]]):
class AladdinConnectCoordinator(DataUpdateCoordinator[GarageDoor]):
"""Coordinator for Aladdin Connect integration."""
config_entry: AladdinConnectConfigEntry
def __init__(
self,
hass: HomeAssistant,
entry: AladdinConnectConfigEntry,
client: AladdinConnectClient,
garage_door: GarageDoor,
) -> None:
"""Initialize the coordinator."""
super().__init__(
@@ -39,16 +37,18 @@ class AladdinConnectCoordinator(DataUpdateCoordinator[dict[str, GarageDoor]]):
update_interval=SCAN_INTERVAL,
)
self.client = client
self.data = garage_door
async def _async_update_data(self) -> dict[str, GarageDoor]:
async def _async_update_data(self) -> GarageDoor:
"""Fetch data from the Aladdin Connect API."""
try:
doors = await self.client.get_doors()
except aiohttp.ClientResponseError as err:
if 400 <= err.status < 500:
raise ConfigEntryAuthFailed(err) from err
raise UpdateFailed(f"Error communicating with API: {err}") from err
await self.client.update_door(self.data.device_id, self.data.door_number)
except aiohttp.ClientError as err:
raise UpdateFailed(f"Error communicating with API: {err}") from err
return {door.unique_id: door for door in doors}
self.data.status = self.client.get_door_status(
self.data.device_id, self.data.door_number
)
self.data.battery_level = self.client.get_battery_status(
self.data.device_id, self.data.door_number
)
return self.data

View File

@@ -7,7 +7,7 @@ from typing import Any
import aiohttp
from homeassistant.components.cover import CoverDeviceClass, CoverEntity
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -24,22 +24,11 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the cover platform."""
coordinator = entry.runtime_data
known_devices: set[str] = set()
coordinators = entry.runtime_data
@callback
def _async_add_new_devices() -> None:
"""Detect and add entities for new doors."""
current_devices = set(coordinator.data)
new_devices = current_devices - known_devices
if new_devices:
known_devices.update(new_devices)
async_add_entities(
AladdinCoverEntity(coordinator, door_id) for door_id in new_devices
)
_async_add_new_devices()
entry.async_on_unload(coordinator.async_add_listener(_async_add_new_devices))
async_add_entities(
AladdinCoverEntity(coordinator) for coordinator in coordinators.values()
)
class AladdinCoverEntity(AladdinConnectEntity, CoverEntity):
@@ -49,10 +38,10 @@ class AladdinCoverEntity(AladdinConnectEntity, CoverEntity):
_attr_supported_features = SUPPORTED_FEATURES
_attr_name = None
def __init__(self, coordinator: AladdinConnectCoordinator, door_id: str) -> None:
def __init__(self, coordinator: AladdinConnectCoordinator) -> None:
"""Initialize the Aladdin Connect cover."""
super().__init__(coordinator, door_id)
self._attr_unique_id = door_id
super().__init__(coordinator)
self._attr_unique_id = coordinator.data.unique_id
async def async_open_cover(self, **kwargs: Any) -> None:
"""Issue open command to cover."""
@@ -77,16 +66,16 @@ class AladdinCoverEntity(AladdinConnectEntity, CoverEntity):
@property
def is_closed(self) -> bool | None:
"""Update is closed attribute."""
if (status := self.door.status) is None:
if (status := self.coordinator.data.status) is None:
return None
return status == "closed"
@property
def is_closing(self) -> bool | None:
"""Update is closing attribute."""
return self.door.status == "closing"
return self.coordinator.data.status == "closing"
@property
def is_opening(self) -> bool | None:
"""Update is opening attribute."""
return self.door.status == "opening"
return self.coordinator.data.status == "opening"

View File

@@ -20,13 +20,13 @@ async def async_get_config_entry_diagnostics(
"config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT),
"doors": {
uid: {
"device_id": door.device_id,
"door_number": door.door_number,
"name": door.name,
"status": door.status,
"link_status": door.link_status,
"battery_level": door.battery_level,
"device_id": coordinator.data.device_id,
"door_number": coordinator.data.door_number,
"name": coordinator.data.name,
"status": coordinator.data.status,
"link_status": coordinator.data.link_status,
"battery_level": coordinator.data.battery_level,
}
for uid, door in config_entry.runtime_data.data.items()
for uid, coordinator in config_entry.runtime_data.items()
},
}

View File

@@ -1,7 +1,6 @@
"""Base class for Aladdin Connect entities."""
from genie_partner_sdk.client import AladdinConnectClient
from genie_partner_sdk.model import GarageDoor
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -15,28 +14,17 @@ class AladdinConnectEntity(CoordinatorEntity[AladdinConnectCoordinator]):
_attr_has_entity_name = True
def __init__(self, coordinator: AladdinConnectCoordinator, door_id: str) -> None:
def __init__(self, coordinator: AladdinConnectCoordinator) -> None:
"""Initialize Aladdin Connect entity."""
super().__init__(coordinator)
self._door_id = door_id
door = self.door
device = coordinator.data
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, door.unique_id)},
identifiers={(DOMAIN, device.unique_id)},
manufacturer="Aladdin Connect",
name=door.name,
name=device.name,
)
self._device_id = door.device_id
self._number = door.door_number
@property
def available(self) -> bool:
"""Return True if entity is available."""
return super().available and self._door_id in self.coordinator.data
@property
def door(self) -> GarageDoor:
"""Return the garage door data."""
return self.coordinator.data[self._door_id]
self._device_id = device.device_id
self._number = device.door_number
@property
def client(self) -> AladdinConnectClient:

View File

@@ -57,7 +57,7 @@ rules:
docs-supported-functions: done
docs-troubleshooting: done
docs-use-cases: done
dynamic-devices: done
dynamic-devices: todo
entity-category: done
entity-device-class: done
entity-disabled-by-default: done

View File

@@ -14,7 +14,7 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.const import PERCENTAGE, EntityCategory
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import AladdinConnectConfigEntry, AladdinConnectCoordinator
@@ -49,24 +49,13 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Aladdin Connect sensor devices."""
coordinator = entry.runtime_data
known_devices: set[str] = set()
coordinators = entry.runtime_data
@callback
def _async_add_new_devices() -> None:
"""Detect and add entities for new doors."""
current_devices = set(coordinator.data)
new_devices = current_devices - known_devices
if new_devices:
known_devices.update(new_devices)
async_add_entities(
AladdinConnectSensor(coordinator, door_id, description)
for door_id in new_devices
for description in SENSOR_TYPES
)
_async_add_new_devices()
entry.async_on_unload(coordinator.async_add_listener(_async_add_new_devices))
async_add_entities(
AladdinConnectSensor(coordinator, description)
for coordinator in coordinators.values()
for description in SENSOR_TYPES
)
class AladdinConnectSensor(AladdinConnectEntity, SensorEntity):
@@ -77,15 +66,14 @@ class AladdinConnectSensor(AladdinConnectEntity, SensorEntity):
def __init__(
self,
coordinator: AladdinConnectCoordinator,
door_id: str,
entity_description: AladdinConnectSensorEntityDescription,
) -> None:
"""Initialize the Aladdin Connect sensor."""
super().__init__(coordinator, door_id)
super().__init__(coordinator)
self.entity_description = entity_description
self._attr_unique_id = f"{door_id}-{entity_description.key}"
self._attr_unique_id = f"{coordinator.data.unique_id}-{entity_description.key}"
@property
def native_value(self) -> float | None:
"""Return the state of the sensor."""
return self.entity_description.value_fn(self.door)
return self.entity_description.value_fn(self.coordinator.data)

View File

@@ -9,12 +9,9 @@ from typing import Any
from homeassistant.components.valve import ValveEntity, ValveEntityFeature, ValveState
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.event import async_track_utc_time_change
from . import DOMAIN
OPEN_CLOSE_DELAY = 2 # Used to give a realistic open/close experience in frontend
@@ -26,10 +23,10 @@ async def async_setup_entry(
"""Set up the Demo config entry."""
async_add_entities(
[
DemoValve("valve_1", "Front Garden", ValveState.OPEN),
DemoValve("valve_2", "Orchard", ValveState.CLOSED),
DemoValve("valve_3", "Back Garden", ValveState.CLOSED, position=70),
DemoValve("valve_4", "Trees", ValveState.CLOSED, position=30),
DemoValve("Front Garden", ValveState.OPEN),
DemoValve("Orchard", ValveState.CLOSED),
DemoValve("Back Garden", ValveState.CLOSED, position=70),
DemoValve("Trees", ValveState.CLOSED, position=30),
]
)
@@ -37,24 +34,17 @@ async def async_setup_entry(
class DemoValve(ValveEntity):
"""Representation of a Demo valve."""
_attr_has_entity_name = True
_attr_name = None
_attr_should_poll = False
def __init__(
self,
unique_id: str,
name: str,
state: str,
moveable: bool = True,
position: int | None = None,
) -> None:
"""Initialize the valve."""
self._attr_unique_id = unique_id
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, unique_id)},
name=name,
)
self._attr_name = name
if moveable:
self._attr_supported_features = (
ValveEntityFeature.OPEN | ValveEntityFeature.CLOSE

View File

@@ -18,8 +18,6 @@ ABBREVIATIONS = {
"bri_stat_t": "brightness_state_topic",
"bri_tpl": "brightness_template",
"bri_val_tpl": "brightness_value_template",
"cln_segmnts_cmd_t": "clean_segments_command_topic",
"cln_segmnts_cmd_tpl": "clean_segments_command_template",
"clr_temp_cmd_tpl": "color_temp_command_template",
"clrm_stat_t": "color_mode_state_topic",
"clrm_val_tpl": "color_mode_value_template",
@@ -187,7 +185,6 @@ ABBREVIATIONS = {
"rgbww_cmd_t": "rgbww_command_topic",
"rgbww_stat_t": "rgbww_state_topic",
"rgbww_val_tpl": "rgbww_value_template",
"segmnts": "segments",
"send_cmd_t": "send_command_topic",
"send_if_off": "send_if_off",
"set_fan_spd_t": "set_fan_speed_topic",

View File

@@ -1484,7 +1484,6 @@ class MqttEntity(
self._config = config
self._setup_from_config(self._config)
self._setup_common_attributes_from_config(self._config)
self._process_entity_update()
# Prepare MQTT subscriptions
self.attributes_prepare_discovery_update(config)
@@ -1587,10 +1586,6 @@ class MqttEntity(
def _setup_from_config(self, config: ConfigType) -> None:
"""(Re)Setup the entity."""
@callback
def _process_entity_update(self) -> None:
"""Process an entity discovery update."""
@abstractmethod
@callback
def _prepare_subscribe_topics(self) -> None:

View File

@@ -10,13 +10,12 @@ import voluptuous as vol
from homeassistant.components import vacuum
from homeassistant.components.vacuum import (
ENTITY_ID_FORMAT,
Segment,
StateVacuumEntity,
VacuumActivity,
VacuumEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_NAME, CONF_UNIQUE_ID
from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_NAME
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -28,7 +27,7 @@ from . import subscription
from .config import MQTT_BASE_SCHEMA
from .const import CONF_COMMAND_TOPIC, CONF_RETAIN, CONF_STATE_TOPIC
from .entity import MqttEntity, async_setup_entity_entry_helper
from .models import MqttCommandTemplate, ReceiveMessage
from .models import ReceiveMessage
from .schemas import MQTT_ENTITY_COMMON_SCHEMA
from .util import valid_publish_topic
@@ -53,9 +52,6 @@ POSSIBLE_STATES: dict[str, VacuumActivity] = {
STATE_CLEANING: VacuumActivity.CLEANING,
}
CONF_SEGMENTS = "segments"
CONF_CLEAN_SEGMENTS_COMMAND_TOPIC = "clean_segments_command_topic"
CONF_CLEAN_SEGMENTS_COMMAND_TEMPLATE = "clean_segments_command_template"
CONF_SUPPORTED_FEATURES = ATTR_SUPPORTED_FEATURES
CONF_PAYLOAD_TURN_ON = "payload_turn_on"
CONF_PAYLOAD_TURN_OFF = "payload_turn_off"
@@ -141,39 +137,8 @@ MQTT_VACUUM_ATTRIBUTES_BLOCKED = frozenset(
MQTT_VACUUM_DOCS_URL = "https://www.home-assistant.io/integrations/vacuum.mqtt/"
def validate_clean_area_config(config: ConfigType) -> ConfigType:
"""Check for a valid configuration and check segments."""
if (config[CONF_SEGMENTS] and CONF_CLEAN_SEGMENTS_COMMAND_TOPIC not in config) or (
not config[CONF_SEGMENTS] and CONF_CLEAN_SEGMENTS_COMMAND_TOPIC in config
):
raise vol.Invalid(
f"Options `{CONF_SEGMENTS}` and "
f"`{CONF_CLEAN_SEGMENTS_COMMAND_TOPIC}` must be defined together"
)
segments: list[str]
if segments := config[CONF_SEGMENTS]:
if not config.get(CONF_UNIQUE_ID):
raise vol.Invalid(
f"Option `{CONF_SEGMENTS}` requires `{CONF_UNIQUE_ID}` to be configured"
)
unique_segments: set[str] = set()
for segment in segments:
segment_id, _, _ = segment.partition(".")
if not segment_id or segment_id in unique_segments:
raise vol.Invalid(
f"The `{CONF_SEGMENTS}` option contains an invalid or non-"
f"unique segment ID '{segment_id}'. Got {segments}"
)
unique_segments.add(segment_id)
return config
_BASE_SCHEMA = MQTT_BASE_SCHEMA.extend(
PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend(
{
vol.Optional(CONF_SEGMENTS, default=[]): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_CLEAN_SEGMENTS_COMMAND_TOPIC): valid_publish_topic,
vol.Optional(CONF_CLEAN_SEGMENTS_COMMAND_TEMPLATE): cv.template,
vol.Optional(CONF_FAN_SPEED_LIST, default=[]): vol.All(
cv.ensure_list, [cv.string]
),
@@ -199,10 +164,7 @@ _BASE_SCHEMA = MQTT_BASE_SCHEMA.extend(
}
).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
PLATFORM_SCHEMA_MODERN = vol.All(_BASE_SCHEMA, validate_clean_area_config)
DISCOVERY_SCHEMA = vol.All(
_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA), validate_clean_area_config
)
DISCOVERY_SCHEMA = PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.ALLOW_EXTRA)
async def async_setup_entry(
@@ -229,11 +191,9 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity):
_entity_id_format = ENTITY_ID_FORMAT
_attributes_extra_blocked = MQTT_VACUUM_ATTRIBUTES_BLOCKED
_segments: list[Segment]
_command_topic: str | None
_set_fan_speed_topic: str | None
_send_command_topic: str | None
_clean_segments_command_topic: str
_payloads: dict[str, str | None]
def __init__(
@@ -269,23 +229,6 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity):
self._attr_supported_features = _strings_to_services(
supported_feature_strings, STRING_TO_SERVICE
)
if config[CONF_SEGMENTS] and CONF_CLEAN_SEGMENTS_COMMAND_TOPIC in config:
self._attr_supported_features |= VacuumEntityFeature.CLEAN_AREA
segments: list[str] = config[CONF_SEGMENTS]
self._segments = [
Segment(id=segment_id, name=name or segment_id)
for segment_id, _, name in [
segment.partition(".") for segment in segments
]
]
self._clean_segments_command_topic = config[
CONF_CLEAN_SEGMENTS_COMMAND_TOPIC
]
self._clean_segments_command_template = MqttCommandTemplate(
config.get(CONF_CLEAN_SEGMENTS_COMMAND_TEMPLATE),
entity=self,
).async_render
self._attr_fan_speed_list = config[CONF_FAN_SPEED_LIST]
self._command_topic = config.get(CONF_COMMAND_TOPIC)
self._set_fan_speed_topic = config.get(CONF_SET_FAN_SPEED_TOPIC)
@@ -303,20 +246,6 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity):
)
}
@callback
def _process_entity_update(self) -> None:
"""Check vacuum segments with registry entry."""
if (
self._attr_supported_features & VacuumEntityFeature.CLEAN_AREA
and (last_seen := self.last_seen_segments) is not None
and {s.id: s for s in last_seen} != {s.id: s for s in self._segments}
):
self.async_create_segments_issue()
async def mqtt_async_added_to_hass(self) -> None:
"""Check vacuum segments with registry entry."""
self._process_entity_update()
def _update_state_attributes(self, payload: dict[str, Any]) -> None:
"""Update the entity state attributes."""
self._state_attrs.update(payload)
@@ -348,19 +277,6 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity):
"""(Re)Subscribe to topics."""
subscription.async_subscribe_topics_internal(self.hass, self._sub_state)
async def async_clean_segments(self, segment_ids: list[str], **kwargs: Any) -> None:
"""Perform an area clean."""
await self.async_publish_with_config(
self._clean_segments_command_topic,
self._clean_segments_command_template(
json_dumps(segment_ids), {"value": segment_ids}
),
)
async def async_get_segments(self) -> list[Segment]:
"""Return the available segments."""
return self._segments
async def _async_publish_command(self, feature: VacuumEntityFeature) -> None:
"""Publish a command."""
if self._command_topic is None:

View File

@@ -14,7 +14,7 @@ from .coordinator import PranaConfigEntry, PranaCoordinator
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.FAN, Platform.NUMBER, Platform.SENSOR, Platform.SWITCH]
PLATFORMS = [Platform.FAN, Platform.SENSOR, Platform.SWITCH]
async def async_setup_entry(hass: HomeAssistant, entry: PranaConfigEntry) -> bool:

View File

@@ -8,20 +8,6 @@
"default": "mdi:arrow-expand-left"
}
},
"number": {
"display_brightness": {
"default": "mdi:brightness-6",
"state": {
"0": "mdi:brightness-2",
"1": "mdi:brightness-4",
"2": "mdi:brightness-4",
"3": "mdi:brightness-5",
"4": "mdi:brightness-5",
"5": "mdi:brightness-7",
"6": "mdi:brightness-7"
}
}
},
"sensor": {
"inside_temperature": {
"default": "mdi:home-thermometer"

View File

@@ -1,80 +0,0 @@
"""Number platform for Prana integration."""
from collections.abc import Callable
from dataclasses import dataclass
from enum import StrEnum
from typing import Any
from homeassistant.components.number import (
NumberEntity,
NumberEntityDescription,
NumberMode,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import PranaConfigEntry, PranaCoordinator
from .entity import PranaBaseEntity
PARALLEL_UPDATES = 1
class PranaNumberType(StrEnum):
"""Enumerates Prana number types exposed by the device API."""
DISPLAY_BRIGHTNESS = "display_brightness"
@dataclass(frozen=True, kw_only=True)
class PranaNumberEntityDescription(NumberEntityDescription):
"""Description of a Prana number entity."""
key: PranaNumberType
value_fn: Callable[[PranaCoordinator], float | None]
set_value_fn: Callable[[Any, float], Any]
ENTITIES: tuple[PranaNumberEntityDescription, ...] = (
PranaNumberEntityDescription(
key=PranaNumberType.DISPLAY_BRIGHTNESS,
translation_key="display_brightness",
native_min_value=0,
native_max_value=6,
native_step=1,
mode=NumberMode.SLIDER,
entity_category=EntityCategory.CONFIG,
value_fn=lambda coord: coord.data.brightness,
set_value_fn=lambda api, val: api.set_brightness(
0 if val == 0 else 2 ** (int(val) - 1)
),
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: PranaConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Prana number entities from a config entry."""
async_add_entities(
PranaNumber(entry.runtime_data, entity_description)
for entity_description in ENTITIES
)
class PranaNumber(PranaBaseEntity, NumberEntity):
"""Representation of a Prana number entity."""
entity_description: PranaNumberEntityDescription
@property
def native_value(self) -> float | None:
"""Return the entity value."""
return self.entity_description.value_fn(self.coordinator)
async def async_set_native_value(self, value: float) -> None:
"""Set new value."""
await self.entity_description.set_value_fn(self.coordinator.api_client, value)
await self.coordinator.async_refresh()

View File

@@ -49,11 +49,6 @@
}
}
},
"number": {
"display_brightness": {
"name": "Display brightness"
}
},
"sensor": {
"inside_temperature": {
"name": "Inside temperature"

View File

@@ -20,7 +20,7 @@
"loggers": ["roborock"],
"quality_scale": "silver",
"requirements": [
"python-roborock==4.25.0",
"python-roborock==4.20.0",
"vacuum-map-parser-roborock==0.1.4"
]
}

View File

@@ -2,7 +2,7 @@
from __future__ import annotations
from datetime import datetime
from datetime import date, datetime
import ephem
@@ -12,7 +12,7 @@ from homeassistant.const import CONF_TYPE
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import dt as dt_util
from homeassistant.util.dt import utcnow
from .const import DOMAIN, TYPE_ASTRONOMICAL
@@ -50,7 +50,7 @@ async def async_setup_entry(
def get_season(
current_datetime: datetime, hemisphere: str, season_tracking_type: str
current_date: date, hemisphere: str, season_tracking_type: str
) -> str | None:
"""Calculate the current season."""
@@ -58,36 +58,22 @@ def get_season(
return None
if season_tracking_type == TYPE_ASTRONOMICAL:
spring_start = (
ephem.next_equinox(str(current_datetime.year))
.datetime()
.replace(tzinfo=dt_util.UTC)
)
summer_start = (
ephem.next_solstice(str(current_datetime.year))
.datetime()
.replace(tzinfo=dt_util.UTC)
)
autumn_start = (
ephem.next_equinox(spring_start).datetime().replace(tzinfo=dt_util.UTC)
)
winter_start = (
ephem.next_solstice(summer_start).datetime().replace(tzinfo=dt_util.UTC)
)
spring_start = ephem.next_equinox(str(current_date.year)).datetime()
summer_start = ephem.next_solstice(str(current_date.year)).datetime()
autumn_start = ephem.next_equinox(spring_start).datetime()
winter_start = ephem.next_solstice(summer_start).datetime()
else:
spring_start = current_datetime.replace(
month=3, day=1, hour=0, minute=0, second=0, microsecond=0
)
spring_start = datetime(2017, 3, 1).replace(year=current_date.year)
summer_start = spring_start.replace(month=6)
autumn_start = spring_start.replace(month=9)
winter_start = spring_start.replace(month=12)
season = STATE_WINTER
if spring_start <= current_datetime < summer_start:
if spring_start <= current_date < summer_start:
season = STATE_SPRING
elif summer_start <= current_datetime < autumn_start:
elif summer_start <= current_date < autumn_start:
season = STATE_SUMMER
elif autumn_start <= current_datetime < winter_start:
elif autumn_start <= current_date < winter_start:
season = STATE_AUTUMN
# If user is located in the southern hemisphere swap the season
@@ -118,4 +104,6 @@ class SeasonSensorEntity(SensorEntity):
def update(self) -> None:
"""Update season."""
self._attr_native_value = get_season(dt_util.now(), self.hemisphere, self.type)
self._attr_native_value = get_season(
utcnow().replace(tzinfo=None), self.hemisphere, self.type
)

View File

@@ -3,7 +3,6 @@
from __future__ import annotations
from collections.abc import Callable, Iterable
import copy
import dataclasses
import datetime
import logging
@@ -29,6 +28,7 @@ from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import dt as dt_util
from homeassistant.util.json import JsonValueType
from .const import (
ATTR_DESCRIPTION,
@@ -240,7 +240,7 @@ class TodoListEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
"""An entity that represents a To-do list."""
_attr_todo_items: list[TodoItem] | None = None
_update_listeners: list[Callable[[list[TodoItem]], None]] | None = None
_update_listeners: list[Callable[[list[JsonValueType] | None], None]] | None = None
@property
def state(self) -> int | None:
@@ -281,9 +281,13 @@ class TodoListEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
@final
@callback
def async_subscribe_updates(
self, listener: Callable[[list[TodoItem]], None]
self,
listener: Callable[[list[JsonValueType] | None], None],
) -> CALLBACK_TYPE:
"""Subscribe to To-do list item updates."""
"""Subscribe to To-do list item updates.
Called by websocket API.
"""
if self._update_listeners is None:
self._update_listeners = []
self._update_listeners.append(listener)
@@ -302,7 +306,9 @@ class TodoListEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
if not self._update_listeners:
return
todo_items = [copy.copy(item) for item in self.todo_items or []]
todo_items: list[JsonValueType] = [
dataclasses.asdict(item) for item in self.todo_items or ()
]
for listener in self._update_listeners:
listener(todo_items)
@@ -335,13 +341,13 @@ async def websocket_handle_subscribe_todo_items(
return
@callback
def todo_item_listener(todo_items: list[TodoItem]) -> None:
def todo_item_listener(todo_items: list[JsonValueType] | None) -> None:
"""Push updated To-do list items to websocket."""
connection.send_message(
websocket_api.event_message(
msg["id"],
{
"items": [dataclasses.asdict(item) for item in todo_items],
"items": todo_items,
},
)
)
@@ -351,7 +357,7 @@ async def websocket_handle_subscribe_todo_items(
)
connection.send_result(msg["id"])
# Push an initial list update
# Push an initial forecast update
entity.async_update_listeners()

View File

@@ -8,6 +8,7 @@ import collections.abc
from collections.abc import Callable, Generator, Iterable
from copy import deepcopy
from datetime import datetime, timedelta
from enum import Enum
from functools import cache, lru_cache, partial, wraps
import json
import logging
@@ -57,7 +58,10 @@ from homeassistant.core import (
from homeassistant.exceptions import TemplateError
from homeassistant.helpers import entity_registry as er, location as loc_helper
from homeassistant.helpers.singleton import singleton
from homeassistant.helpers.translation import async_translate_state
from homeassistant.helpers.translation import (
async_translate_state,
async_translate_state_attr,
)
from homeassistant.helpers.typing import TemplateVarsType
from homeassistant.util import convert, location as location_util
from homeassistant.util.async_ import run_callback_threadsafe
@@ -807,6 +811,48 @@ class StateTranslated:
return "<template StateTranslated>"
class StateAttrTranslated:
"""Class to represent a translated state attribute value in a template."""
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize."""
self._hass = hass
def __call__(self, entity_id: str, attribute: str) -> Any:
"""Retrieve translated state attribute value if available."""
state = _get_state_if_valid(self._hass, entity_id)
if state is None:
return None
attr_value = state.attributes.get(attribute)
if attr_value is None:
return None
if not isinstance(attr_value, str | Enum):
return attr_value
domain = state.domain
device_class = state.attributes.get("device_class")
entry = er.async_get(self._hass).async_get(entity_id)
platform = None if entry is None else entry.platform
translation_key = None if entry is None else entry.translation_key
return async_translate_state_attr(
self._hass,
str(attr_value),
domain,
platform,
translation_key,
device_class,
attribute,
)
def __repr__(self) -> str:
"""Representation of Translated state attribute."""
return "<template StateAttrTranslated>"
class DomainStates:
"""Class to expose a specific HA domain as attributes."""
@@ -1989,6 +2035,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
"is_state_attr",
"is_state",
"state_attr",
"state_attr_translated",
"state_translated",
"states",
]
@@ -2036,9 +2083,11 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
self.globals["is_state_attr"] = hassfunction(is_state_attr)
self.globals["is_state"] = hassfunction(is_state)
self.globals["state_attr"] = hassfunction(state_attr)
self.globals["state_attr_translated"] = StateAttrTranslated(hass)
self.globals["state_translated"] = StateTranslated(hass)
self.globals["states"] = AllStates(hass)
self.filters["state_attr"] = self.globals["state_attr"]
self.filters["state_attr_translated"] = self.globals["state_attr_translated"]
self.filters["state_translated"] = self.globals["state_translated"]
self.filters["states"] = self.globals["states"]
self.tests["is_state_attr"] = hassfunction(is_state_attr, pass_eval_context)
@@ -2047,7 +2096,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
def is_safe_callable(self, obj):
"""Test if callback is safe."""
return isinstance(
obj, (AllStates, StateTranslated)
obj, (AllStates, StateAttrTranslated, StateTranslated)
) or super().is_safe_callable(obj)
def is_safe_attribute(self, obj, attr, value):

View File

@@ -492,3 +492,43 @@ def async_translate_state(
return translations[localize_key]
return state
@callback
def async_translate_state_attr(
hass: HomeAssistant,
attr_value: str,
domain: str,
platform: str | None,
translation_key: str | None,
device_class: str | None,
attribute_name: str,
) -> str:
"""Translate provided state attribute value using cached translations for currently selected language."""
language = hass.config.language
if platform is not None and translation_key is not None:
localize_key = (
f"component.{platform}.entity.{domain}"
f".{translation_key}.state_attributes.{attribute_name}"
f".state.{attr_value}"
)
translations = async_get_cached_translations(hass, language, "entity")
if localize_key in translations:
return translations[localize_key]
translations = async_get_cached_translations(hass, language, "entity_component")
if device_class is not None:
localize_key = (
f"component.{domain}.entity_component.{device_class}"
f".state_attributes.{attribute_name}.state.{attr_value}"
)
if localize_key in translations:
return translations[localize_key]
localize_key = (
f"component.{domain}.entity_component._"
f".state_attributes.{attribute_name}.state.{attr_value}"
)
if localize_key in translations:
return translations[localize_key]
return attr_value

2
requirements_all.txt generated
View File

@@ -2651,7 +2651,7 @@ python-rabbitair==0.0.8
python-ripple-api==0.0.3
# homeassistant.components.roborock
python-roborock==4.25.0
python-roborock==4.20.0
# homeassistant.components.smarttub
python-smarttub==0.0.47

View File

@@ -2247,7 +2247,7 @@ python-pooldose==0.8.6
python-rabbitair==0.0.8
# homeassistant.components.roborock
python-roborock==4.25.0
python-roborock==4.20.0
# homeassistant.components.smarttub
python-smarttub==0.0.47

View File

@@ -125,7 +125,7 @@ async def test_cover_unavailable(
assert state is not None
assert state.state != STATE_UNAVAILABLE
mock_aladdin_connect_api.get_doors.side_effect = aiohttp.ClientError()
mock_aladdin_connect_api.update_door.side_effect = aiohttp.ClientError()
freezer.tick(15)
async_fire_time_changed(hass)
await hass.async_block_till_done()

View File

@@ -5,17 +5,16 @@ from unittest.mock import AsyncMock, patch
from aiohttp import ClientConnectionError, RequestInfo
from aiohttp.client_exceptions import ClientResponseError
from freezegun.api import FrozenDateTimeFactory
import pytest
from homeassistant.components.aladdin_connect import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers import device_registry as dr
from . import init_integration
from tests.common import MockConfigEntry, async_fire_time_changed
from tests.common import MockConfigEntry
async def test_setup_entry(
@@ -138,49 +137,3 @@ async def test_remove_stale_devices(
)
assert len(device_entries) == 1
assert device_entries[0].identifiers == {(DOMAIN, "test_device_id-1")}
async def test_dynamic_devices(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
mock_config_entry: MockConfigEntry,
mock_aladdin_connect_api: AsyncMock,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test new devices are automatically discovered on coordinator refresh."""
await init_integration(hass, mock_config_entry)
# Initially one door -> one cover entity + one sensor entity
device_entries = dr.async_entries_for_config_entry(
device_registry, mock_config_entry.entry_id
)
assert len(device_entries) == 1
assert hass.states.get("cover.test_door") is not None
# Simulate a new door appearing on the API
mock_door_2 = AsyncMock()
mock_door_2.device_id = "test_device_id_2"
mock_door_2.door_number = 1
mock_door_2.name = "Test Door 2"
mock_door_2.status = "open"
mock_door_2.link_status = "connected"
mock_door_2.battery_level = 80
mock_door_2.unique_id = f"{mock_door_2.device_id}-{mock_door_2.door_number}"
existing_door = mock_aladdin_connect_api.get_doors.return_value[0]
mock_aladdin_connect_api.get_doors.return_value = [existing_door, mock_door_2]
# Trigger coordinator refresh
freezer.tick(15)
async_fire_time_changed(hass)
await hass.async_block_till_done()
# Now two devices should exist
device_entries = dr.async_entries_for_config_entry(
device_registry, mock_config_entry.entry_id
)
assert len(device_entries) == 2
# New cover entity should exist
assert hass.states.get("cover.test_door_2") is not None

View File

@@ -49,7 +49,7 @@ async def test_sensor_unavailable(
assert state is not None
assert state.state != STATE_UNAVAILABLE
mock_aladdin_connect_api.get_doors.side_effect = aiohttp.ClientError()
mock_aladdin_connect_api.update_door.side_effect = aiohttp.ClientError()
freezer.tick(15)
async_fire_time_changed(hass)
await hass.async_block_till_done()

View File

@@ -13,13 +13,13 @@ from homeassistant.core import HomeAssistant
from tests.components.common import (
ConditionStateDescription,
assert_condition_behavior_all,
assert_condition_behavior_any,
assert_condition_gated_by_labs_flag,
create_target_condition,
other_states,
parametrize_condition_states_all,
parametrize_condition_states_any,
parametrize_target_entities,
set_or_remove_state,
target_entities,
)
@@ -78,7 +78,7 @@ async def test_alarm_control_panel_conditions_gated_by_labs_flag(
condition="alarm_control_panel.is_armed_away",
target_states=[AlarmControlPanelState.ARMED_AWAY],
other_states=other_states(AlarmControlPanelState.ARMED_AWAY),
required_filter_attributes={
additional_attributes={
ATTR_SUPPORTED_FEATURES: AlarmControlPanelEntityFeature.ARM_AWAY
},
),
@@ -86,7 +86,7 @@ async def test_alarm_control_panel_conditions_gated_by_labs_flag(
condition="alarm_control_panel.is_armed_home",
target_states=[AlarmControlPanelState.ARMED_HOME],
other_states=other_states(AlarmControlPanelState.ARMED_HOME),
required_filter_attributes={
additional_attributes={
ATTR_SUPPORTED_FEATURES: AlarmControlPanelEntityFeature.ARM_HOME
},
),
@@ -94,7 +94,7 @@ async def test_alarm_control_panel_conditions_gated_by_labs_flag(
condition="alarm_control_panel.is_armed_night",
target_states=[AlarmControlPanelState.ARMED_NIGHT],
other_states=other_states(AlarmControlPanelState.ARMED_NIGHT),
required_filter_attributes={
additional_attributes={
ATTR_SUPPORTED_FEATURES: AlarmControlPanelEntityFeature.ARM_NIGHT
},
),
@@ -102,7 +102,7 @@ async def test_alarm_control_panel_conditions_gated_by_labs_flag(
condition="alarm_control_panel.is_armed_vacation",
target_states=[AlarmControlPanelState.ARMED_VACATION],
other_states=other_states(AlarmControlPanelState.ARMED_VACATION),
required_filter_attributes={
additional_attributes={
ATTR_SUPPORTED_FEATURES: AlarmControlPanelEntityFeature.ARM_VACATION
},
),
@@ -129,17 +129,32 @@ async def test_alarm_control_panel_state_condition_behavior_any(
states: list[ConditionStateDescription],
) -> None:
"""Test the alarm_control_panel state condition with the 'any' behavior."""
await assert_condition_behavior_any(
other_entity_ids = set(target_alarm_control_panels["included"]) - {entity_id}
# Set all alarm_control_panels, including the tested alarm_control_panel, to the initial state
for eid in target_alarm_control_panels["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
condition = await create_target_condition(
hass,
target_entities=target_alarm_control_panels,
condition_target_config=condition_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
condition=condition,
condition_options=condition_options,
states=states,
target=condition_target_config,
behavior="any",
)
for state in states:
included_state = state["included"]
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
# Check if changing other alarm_control_panels also passes the condition
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
@@ -170,7 +185,7 @@ async def test_alarm_control_panel_state_condition_behavior_any(
condition="alarm_control_panel.is_armed_away",
target_states=[AlarmControlPanelState.ARMED_AWAY],
other_states=other_states(AlarmControlPanelState.ARMED_AWAY),
required_filter_attributes={
additional_attributes={
ATTR_SUPPORTED_FEATURES: AlarmControlPanelEntityFeature.ARM_AWAY
},
),
@@ -178,7 +193,7 @@ async def test_alarm_control_panel_state_condition_behavior_any(
condition="alarm_control_panel.is_armed_home",
target_states=[AlarmControlPanelState.ARMED_HOME],
other_states=other_states(AlarmControlPanelState.ARMED_HOME),
required_filter_attributes={
additional_attributes={
ATTR_SUPPORTED_FEATURES: AlarmControlPanelEntityFeature.ARM_HOME
},
),
@@ -186,7 +201,7 @@ async def test_alarm_control_panel_state_condition_behavior_any(
condition="alarm_control_panel.is_armed_night",
target_states=[AlarmControlPanelState.ARMED_NIGHT],
other_states=other_states(AlarmControlPanelState.ARMED_NIGHT),
required_filter_attributes={
additional_attributes={
ATTR_SUPPORTED_FEATURES: AlarmControlPanelEntityFeature.ARM_NIGHT
},
),
@@ -194,7 +209,7 @@ async def test_alarm_control_panel_state_condition_behavior_any(
condition="alarm_control_panel.is_armed_vacation",
target_states=[AlarmControlPanelState.ARMED_VACATION],
other_states=other_states(AlarmControlPanelState.ARMED_VACATION),
required_filter_attributes={
additional_attributes={
ATTR_SUPPORTED_FEATURES: AlarmControlPanelEntityFeature.ARM_VACATION
},
),
@@ -221,13 +236,29 @@ async def test_alarm_control_panel_state_condition_behavior_all(
states: list[ConditionStateDescription],
) -> None:
"""Test the alarm_control_panel state condition with the 'all' behavior."""
await assert_condition_behavior_all(
other_entity_ids = set(target_alarm_control_panels["included"]) - {entity_id}
# Set all alarm_control_panels, including the tested alarm_control_panel, to the initial state
for eid in target_alarm_control_panels["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
condition = await create_target_condition(
hass,
target_entities=target_alarm_control_panels,
condition_target_config=condition_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
condition=condition,
condition_options=condition_options,
states=states,
target=condition_target_config,
behavior="all",
)
for state in states:
included_state = state["included"]
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true_first_entity"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]

View File

@@ -78,7 +78,7 @@ async def test_alarm_control_panel_triggers_gated_by_labs_flag(
trigger="alarm_control_panel.armed_away",
target_states=[AlarmControlPanelState.ARMED_AWAY],
other_states=other_states(AlarmControlPanelState.ARMED_AWAY),
required_filter_attributes={
additional_attributes={
ATTR_SUPPORTED_FEATURES: AlarmControlPanelEntityFeature.ARM_AWAY
},
trigger_from_none=False,
@@ -87,7 +87,7 @@ async def test_alarm_control_panel_triggers_gated_by_labs_flag(
trigger="alarm_control_panel.armed_home",
target_states=[AlarmControlPanelState.ARMED_HOME],
other_states=other_states(AlarmControlPanelState.ARMED_HOME),
required_filter_attributes={
additional_attributes={
ATTR_SUPPORTED_FEATURES: AlarmControlPanelEntityFeature.ARM_HOME
},
trigger_from_none=False,
@@ -96,7 +96,7 @@ async def test_alarm_control_panel_triggers_gated_by_labs_flag(
trigger="alarm_control_panel.armed_night",
target_states=[AlarmControlPanelState.ARMED_NIGHT],
other_states=other_states(AlarmControlPanelState.ARMED_NIGHT),
required_filter_attributes={
additional_attributes={
ATTR_SUPPORTED_FEATURES: AlarmControlPanelEntityFeature.ARM_NIGHT
},
trigger_from_none=False,
@@ -105,7 +105,7 @@ async def test_alarm_control_panel_triggers_gated_by_labs_flag(
trigger="alarm_control_panel.armed_vacation",
target_states=[AlarmControlPanelState.ARMED_VACATION],
other_states=other_states(AlarmControlPanelState.ARMED_VACATION),
required_filter_attributes={
additional_attributes={
ATTR_SUPPORTED_FEATURES: AlarmControlPanelEntityFeature.ARM_VACATION
},
trigger_from_none=False,
@@ -176,7 +176,7 @@ async def test_alarm_control_panel_state_trigger_behavior_any(
trigger="alarm_control_panel.armed_away",
target_states=[AlarmControlPanelState.ARMED_AWAY],
other_states=other_states(AlarmControlPanelState.ARMED_AWAY),
required_filter_attributes={
additional_attributes={
ATTR_SUPPORTED_FEATURES: AlarmControlPanelEntityFeature.ARM_AWAY
},
trigger_from_none=False,
@@ -185,7 +185,7 @@ async def test_alarm_control_panel_state_trigger_behavior_any(
trigger="alarm_control_panel.armed_home",
target_states=[AlarmControlPanelState.ARMED_HOME],
other_states=other_states(AlarmControlPanelState.ARMED_HOME),
required_filter_attributes={
additional_attributes={
ATTR_SUPPORTED_FEATURES: AlarmControlPanelEntityFeature.ARM_HOME
},
trigger_from_none=False,
@@ -194,7 +194,7 @@ async def test_alarm_control_panel_state_trigger_behavior_any(
trigger="alarm_control_panel.armed_night",
target_states=[AlarmControlPanelState.ARMED_NIGHT],
other_states=other_states(AlarmControlPanelState.ARMED_NIGHT),
required_filter_attributes={
additional_attributes={
ATTR_SUPPORTED_FEATURES: AlarmControlPanelEntityFeature.ARM_NIGHT
},
trigger_from_none=False,
@@ -203,7 +203,7 @@ async def test_alarm_control_panel_state_trigger_behavior_any(
trigger="alarm_control_panel.armed_vacation",
target_states=[AlarmControlPanelState.ARMED_VACATION],
other_states=other_states(AlarmControlPanelState.ARMED_VACATION),
required_filter_attributes={
additional_attributes={
ATTR_SUPPORTED_FEATURES: AlarmControlPanelEntityFeature.ARM_VACATION
},
trigger_from_none=False,
@@ -274,7 +274,7 @@ async def test_alarm_control_panel_state_trigger_behavior_first(
trigger="alarm_control_panel.armed_away",
target_states=[AlarmControlPanelState.ARMED_AWAY],
other_states=other_states(AlarmControlPanelState.ARMED_AWAY),
required_filter_attributes={
additional_attributes={
ATTR_SUPPORTED_FEATURES: AlarmControlPanelEntityFeature.ARM_AWAY
},
trigger_from_none=False,
@@ -283,7 +283,7 @@ async def test_alarm_control_panel_state_trigger_behavior_first(
trigger="alarm_control_panel.armed_home",
target_states=[AlarmControlPanelState.ARMED_HOME],
other_states=other_states(AlarmControlPanelState.ARMED_HOME),
required_filter_attributes={
additional_attributes={
ATTR_SUPPORTED_FEATURES: AlarmControlPanelEntityFeature.ARM_HOME
},
trigger_from_none=False,
@@ -292,7 +292,7 @@ async def test_alarm_control_panel_state_trigger_behavior_first(
trigger="alarm_control_panel.armed_night",
target_states=[AlarmControlPanelState.ARMED_NIGHT],
other_states=other_states(AlarmControlPanelState.ARMED_NIGHT),
required_filter_attributes={
additional_attributes={
ATTR_SUPPORTED_FEATURES: AlarmControlPanelEntityFeature.ARM_NIGHT
},
trigger_from_none=False,
@@ -301,7 +301,7 @@ async def test_alarm_control_panel_state_trigger_behavior_first(
trigger="alarm_control_panel.armed_vacation",
target_states=[AlarmControlPanelState.ARMED_VACATION],
other_states=other_states(AlarmControlPanelState.ARMED_VACATION),
required_filter_attributes={
additional_attributes={
ATTR_SUPPORTED_FEATURES: AlarmControlPanelEntityFeature.ARM_VACATION
},
trigger_from_none=False,

View File

@@ -9,13 +9,13 @@ from homeassistant.core import HomeAssistant
from tests.components.common import (
ConditionStateDescription,
assert_condition_behavior_all,
assert_condition_behavior_any,
assert_condition_gated_by_labs_flag,
create_target_condition,
other_states,
parametrize_condition_states_all,
parametrize_condition_states_any,
parametrize_target_entities,
set_or_remove_state,
target_entities,
)
@@ -83,17 +83,32 @@ async def test_assist_satellite_state_condition_behavior_any(
states: list[ConditionStateDescription],
) -> None:
"""Test the assist satellite state condition with the 'any' behavior."""
await assert_condition_behavior_any(
other_entity_ids = set(target_assist_satellites["included"]) - {entity_id}
# Set all assist satellites, including the tested one, to the initial state
for eid in target_assist_satellites["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
condition = await create_target_condition(
hass,
target_entities=target_assist_satellites,
condition_target_config=condition_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
condition=condition,
condition_options=condition_options,
states=states,
target=condition_target_config,
behavior="any",
)
for state in states:
included_state = state["included"]
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
# Check if changing other assist satellites also passes the condition
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
@@ -136,13 +151,29 @@ async def test_assist_satellite_state_condition_behavior_all(
states: list[ConditionStateDescription],
) -> None:
"""Test the assist satellite state condition with the 'all' behavior."""
await assert_condition_behavior_all(
other_entity_ids = set(target_assist_satellites["included"]) - {entity_id}
# Set all assist satellites, including the tested one, to the initial state
for eid in target_assist_satellites["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
condition = await create_target_condition(
hass,
target_entities=target_assist_satellites,
condition_target_config=condition_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
condition=condition,
condition_options=condition_options,
states=states,
target=condition_target_config,
behavior="all",
)
for state in states:
included_state = state["included"]
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true_first_entity"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]

View File

@@ -40,19 +40,36 @@ async def test_button_triggers_gated_by_labs_flag(
(
"button.pressed",
[
{"included": {"state": None, "attributes": {}}, "count": 0},
{
"included_target_state": {"state": None, "attributes": {}},
"count": 0,
},
{
"included_target_state": {
"included": {
"state": "2021-01-01T23:59:59+00:00",
"attributes": {},
},
"count": 0,
},
{
"included_target_state": {
"included": {
"state": "2022-01-01T23:59:59+00:00",
"attributes": {},
},
"count": 1,
},
],
),
(
"button.pressed",
[
{"included": {"state": "foo", "attributes": {}}, "count": 0},
{
"included": {
"state": "2021-01-01T23:59:59+00:00",
"attributes": {},
},
"count": 1,
},
{
"included": {
"state": "2022-01-01T23:59:59+00:00",
"attributes": {},
},
@@ -64,54 +81,25 @@ async def test_button_triggers_gated_by_labs_flag(
"button.pressed",
[
{
"included_target_state": {"state": "foo", "attributes": {}},
"included": {"state": STATE_UNAVAILABLE, "attributes": {}},
"count": 0,
},
{
"included_target_state": {
"state": "2021-01-01T23:59:59+00:00",
"attributes": {},
},
"count": 1,
},
{
"included_target_state": {
"state": "2022-01-01T23:59:59+00:00",
"attributes": {},
},
"count": 1,
},
],
),
(
"button.pressed",
[
{
"included_target_state": {
"state": STATE_UNAVAILABLE,
"attributes": {},
},
"count": 0,
},
{
"included_target_state": {
"included": {
"state": "2021-01-01T23:59:59+00:00",
"attributes": {},
},
"count": 0,
},
{
"included_target_state": {
"included": {
"state": "2022-01-01T23:59:59+00:00",
"attributes": {},
},
"count": 1,
},
{
"included_target_state": {
"state": STATE_UNAVAILABLE,
"attributes": {},
},
"included": {"state": STATE_UNAVAILABLE, "attributes": {}},
"count": 0,
},
],
@@ -119,28 +107,22 @@ async def test_button_triggers_gated_by_labs_flag(
(
"button.pressed",
[
{"included": {"state": STATE_UNKNOWN, "attributes": {}}, "count": 0},
{
"included_target_state": {"state": STATE_UNKNOWN, "attributes": {}},
"count": 0,
},
{
"included_target_state": {
"included": {
"state": "2021-01-01T23:59:59+00:00",
"attributes": {},
},
"count": 1,
},
{
"included_target_state": {
"included": {
"state": "2022-01-01T23:59:59+00:00",
"attributes": {},
},
"count": 1,
},
{
"included_target_state": {"state": STATE_UNKNOWN, "attributes": {}},
"count": 0,
},
{"included": {"state": STATE_UNKNOWN, "attributes": {}}, "count": 0},
],
),
],
@@ -156,17 +138,17 @@ async def test_button_state_trigger_behavior_any(
states: list[TriggerStateDescription],
) -> None:
"""Test that the button state trigger fires when any button state changes to a specific state."""
other_entity_ids = set(target_buttons["included_entities"]) - {entity_id}
other_entity_ids = set(target_buttons["included"]) - {entity_id}
# Set all buttons, including the tested button, to the initial state
for eid in target_buttons["included_entities"]:
set_or_remove_state(hass, eid, states[0]["included_target_state"])
for eid in target_buttons["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
await arm_trigger(hass, trigger, None, trigger_target_config)
for state in states[1:]:
included_state = state["included_target_state"]
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"]

View File

@@ -8,8 +8,8 @@
'fide': None,
'followers': 2,
'is_streamer': False,
'joined': '2026-02-20T10:48:14',
'last_online': '2026-03-06T12:32:59',
'joined': '2026-02-20T11:48:14',
'last_online': '2026-03-06T13:32:59',
'location': 'Utrecht',
'name': 'Joost',
'player_id': 532748851,

View File

@@ -13,13 +13,13 @@ from homeassistant.core import HomeAssistant
from tests.components.common import (
ConditionStateDescription,
assert_condition_behavior_all,
assert_condition_behavior_any,
assert_condition_gated_by_labs_flag,
create_target_condition,
other_states,
parametrize_condition_states_all,
parametrize_condition_states_any,
parametrize_target_entities,
set_or_remove_state,
target_entities,
)
@@ -85,17 +85,32 @@ async def test_climate_state_condition_behavior_any(
states: list[ConditionStateDescription],
) -> None:
"""Test the climate state condition with the 'any' behavior."""
await assert_condition_behavior_any(
other_entity_ids = set(target_climates["included"]) - {entity_id}
# Set all climates, including the tested climate, to the initial state
for eid in target_climates["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
condition = await create_target_condition(
hass,
target_entities=target_climates,
condition_target_config=condition_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
condition=condition,
condition_options=condition_options,
states=states,
target=condition_target_config,
behavior="any",
)
for state in states:
included_state = state["included"]
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
# Check if changing other climates also passes the condition
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
@@ -135,17 +150,33 @@ async def test_climate_state_condition_behavior_all(
states: list[ConditionStateDescription],
) -> None:
"""Test the climate state condition with the 'all' behavior."""
await assert_condition_behavior_all(
other_entity_ids = set(target_climates["included"]) - {entity_id}
# Set all climates, including the tested climate, to the initial state
for eid in target_climates["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
condition = await create_target_condition(
hass,
target_entities=target_climates,
condition_target_config=condition_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
condition=condition,
condition_options=condition_options,
states=states,
target=condition_target_config,
behavior="all",
)
for state in states:
included_state = state["included"]
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true_first_entity"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
@@ -183,17 +214,32 @@ async def test_climate_attribute_condition_behavior_any(
states: list[ConditionStateDescription],
) -> None:
"""Test the climate attribute condition with the 'any' behavior."""
await assert_condition_behavior_any(
other_entity_ids = set(target_climates["included"]) - {entity_id}
# Set all climates, including the tested climate, to the initial state
for eid in target_climates["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
condition = await create_target_condition(
hass,
target_entities=target_climates,
condition_target_config=condition_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
condition=condition,
condition_options=condition_options,
states=states,
target=condition_target_config,
behavior="any",
)
for state in states:
included_state = state["included"]
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
# Check if changing other climates also passes the condition
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
@@ -231,13 +277,29 @@ async def test_climate_attribute_condition_behavior_all(
states: list[ConditionStateDescription],
) -> None:
"""Test the climate attribute condition with the 'all' behavior."""
await assert_condition_behavior_all(
other_entity_ids = set(target_climates["included"]) - {entity_id}
# Set all climates, including the tested climate, to the initial state
for eid in target_climates["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
condition = await create_target_condition(
hass,
target_entities=target_climates,
condition_target_config=condition_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
condition=condition,
condition_options=condition_options,
states=states,
target=condition_target_config,
behavior="all",
)
for state in states:
included_state = state["included"]
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true_first_entity"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]

View File

@@ -133,7 +133,7 @@ async def target_entities(hass: HomeAssistant, domain: str) -> dict[str, list[st
# Return all available entities
return {
"included_entities": [
"included": [
f"{domain}.standalone_{domain}",
f"{domain}.standalone2_{domain}",
f"{domain}.label_{domain}",
@@ -141,7 +141,7 @@ async def target_entities(hass: HomeAssistant, domain: str) -> dict[str, list[st
f"{domain}.device_{domain}",
f"{domain}.device2_{domain}",
],
"excluded_entities": [
"excluded": [
f"{domain}.standalone_{domain}_excluded",
f"{domain}.label_{domain}_excluded",
f"{domain}.area_{domain}_excluded",
@@ -207,7 +207,7 @@ def _parametrize_condition_states(
condition_options: dict[str, Any] | None = None,
target_states: list[str | None | tuple[str | None, dict]],
other_states: list[str | None | tuple[str | None, dict]],
required_filter_attributes: dict | None,
additional_attributes: dict | None,
condition_true_if_invalid: bool,
) -> list[tuple[str, dict[str, Any], list[ConditionStateDescription]]]:
"""Parametrize states and expected condition evaluations.
@@ -219,7 +219,7 @@ def _parametrize_condition_states(
where states is a list of ConditionStateDescription dicts.
"""
required_filter_attributes = required_filter_attributes or {}
additional_attributes = additional_attributes or {}
condition_options = condition_options or {}
def state_with_attributes(
@@ -230,11 +230,11 @@ def _parametrize_condition_states(
"""Return ConditionStateDescription dict."""
if isinstance(state, str) or state is None:
return {
"included_target_state": {
"included": {
"state": state,
"attributes": required_filter_attributes,
"attributes": additional_attributes,
},
"excluded_target_state": {
"excluded": {
"state": state,
"attributes": {},
},
@@ -242,11 +242,11 @@ def _parametrize_condition_states(
"condition_true_first_entity": condition_true_first_entity,
}
return {
"included_target_state": {
"included": {
"state": state[0],
"attributes": state[1] | required_filter_attributes,
"attributes": state[1] | additional_attributes,
},
"excluded_target_state": {
"excluded": {
"state": state[0],
"attributes": state[1],
},
@@ -299,7 +299,7 @@ def parametrize_condition_states_any(
condition_options: dict[str, Any] | None = None,
target_states: list[str | None | tuple[str | None, dict]],
other_states: list[str | None | tuple[str | None, dict]],
required_filter_attributes: dict | None = None,
additional_attributes: dict | None = None,
) -> list[tuple[str, dict[str, Any], list[ConditionStateDescription]]]:
"""Parametrize states and expected condition evaluations.
@@ -315,7 +315,7 @@ def parametrize_condition_states_any(
condition_options=condition_options,
target_states=target_states,
other_states=other_states,
required_filter_attributes=required_filter_attributes,
additional_attributes=additional_attributes,
condition_true_if_invalid=False,
)
@@ -326,7 +326,7 @@ def parametrize_condition_states_all(
condition_options: dict[str, Any] | None = None,
target_states: list[str | None | tuple[str | None, dict]],
other_states: list[str | None | tuple[str | None, dict]],
required_filter_attributes: dict | None = None,
additional_attributes: dict | None = None,
) -> list[tuple[str, dict[str, Any], list[ConditionStateDescription]]]:
"""Parametrize states and expected condition evaluations.
@@ -342,7 +342,7 @@ def parametrize_condition_states_all(
condition_options=condition_options,
target_states=target_states,
other_states=other_states,
required_filter_attributes=required_filter_attributes,
additional_attributes=additional_attributes,
condition_true_if_invalid=True,
)
@@ -354,7 +354,7 @@ def parametrize_trigger_states(
target_states: list[str | None | tuple[str | None, dict]],
other_states: list[str | None | tuple[str | None, dict]],
extra_invalid_states: list[str | None | tuple[str | None, dict]] | None = None,
required_filter_attributes: dict | None = None,
additional_attributes: dict | None = None,
trigger_from_none: bool = True,
retrigger_on_target_state: bool = False,
) -> list[tuple[str, dict[str, Any], list[TriggerStateDescription]]]:
@@ -377,7 +377,7 @@ def parametrize_trigger_states(
extra_invalid_states = extra_invalid_states or []
invalid_states = [STATE_UNAVAILABLE, STATE_UNKNOWN, *extra_invalid_states]
required_filter_attributes = required_filter_attributes or {}
additional_attributes = additional_attributes or {}
trigger_options = trigger_options or {}
def state_with_attributes(
@@ -386,23 +386,23 @@ def parametrize_trigger_states(
"""Return TriggerStateDescription dict."""
if isinstance(state, str) or state is None:
return {
"included_target_state": {
"included": {
"state": state,
"attributes": required_filter_attributes,
"attributes": additional_attributes,
},
"excluded_target_state": {
"state": state if required_filter_attributes else None,
"excluded": {
"state": state if additional_attributes else None,
"attributes": {},
},
"count": count,
}
return {
"included_target_state": {
"included": {
"state": state[0],
"attributes": state[1] | required_filter_attributes,
"attributes": state[1] | additional_attributes,
},
"excluded_target_state": {
"state": state[0] if required_filter_attributes else None,
"excluded": {
"state": state[0] if additional_attributes else None,
"attributes": state[1],
},
"count": count,
@@ -644,14 +644,14 @@ def parametrize_numerical_state_value_changed_trigger_states(
"""
from homeassistant.const import ATTR_DEVICE_CLASS # noqa: PLC0415
required_filter_attributes = {ATTR_DEVICE_CLASS: device_class}
additional_attributes = {ATTR_DEVICE_CLASS: device_class}
return [
*parametrize_trigger_states(
trigger=trigger,
trigger_options={},
target_states=["0", "50", "100"],
other_states=["none"],
required_filter_attributes=required_filter_attributes,
additional_attributes=additional_attributes,
retrigger_on_target_state=True,
trigger_from_none=False,
),
@@ -660,7 +660,7 @@ def parametrize_numerical_state_value_changed_trigger_states(
trigger_options={CONF_ABOVE: 10},
target_states=["50", "100"],
other_states=["none", "0"],
required_filter_attributes=required_filter_attributes,
additional_attributes=additional_attributes,
retrigger_on_target_state=True,
trigger_from_none=False,
),
@@ -669,7 +669,7 @@ def parametrize_numerical_state_value_changed_trigger_states(
trigger_options={CONF_BELOW: 90},
target_states=["0", "50"],
other_states=["none", "100"],
required_filter_attributes=required_filter_attributes,
additional_attributes=additional_attributes,
retrigger_on_target_state=True,
trigger_from_none=False,
),
@@ -687,7 +687,7 @@ def parametrize_numerical_state_value_crossed_threshold_trigger_states(
"""
from homeassistant.const import ATTR_DEVICE_CLASS # noqa: PLC0415
required_filter_attributes = {ATTR_DEVICE_CLASS: device_class}
additional_attributes = {ATTR_DEVICE_CLASS: device_class}
return [
*parametrize_trigger_states(
trigger=trigger,
@@ -698,7 +698,7 @@ def parametrize_numerical_state_value_crossed_threshold_trigger_states(
},
target_states=["50", "60"],
other_states=["none", "0", "100"],
required_filter_attributes=required_filter_attributes,
additional_attributes=additional_attributes,
trigger_from_none=False,
),
*parametrize_trigger_states(
@@ -710,7 +710,7 @@ def parametrize_numerical_state_value_crossed_threshold_trigger_states(
},
target_states=["0", "100"],
other_states=["none", "50", "60"],
required_filter_attributes=required_filter_attributes,
additional_attributes=additional_attributes,
trigger_from_none=False,
),
*parametrize_trigger_states(
@@ -721,7 +721,7 @@ def parametrize_numerical_state_value_crossed_threshold_trigger_states(
},
target_states=["50", "100"],
other_states=["none", "0"],
required_filter_attributes=required_filter_attributes,
additional_attributes=additional_attributes,
trigger_from_none=False,
),
*parametrize_trigger_states(
@@ -732,7 +732,7 @@ def parametrize_numerical_state_value_crossed_threshold_trigger_states(
},
target_states=["0", "50"],
other_states=["none", "100"],
required_filter_attributes=required_filter_attributes,
additional_attributes=additional_attributes,
trigger_from_none=False,
),
]
@@ -864,98 +864,6 @@ async def assert_trigger_gated_by_labs_flag(
) in caplog.text
async def assert_condition_behavior_any(
hass: HomeAssistant,
*,
target_entities: dict[str, list[str]],
condition_target_config: dict,
entity_id: str,
entities_in_target: int,
condition: str,
condition_options: dict[str, Any],
states: list[ConditionStateDescription],
) -> None:
"""Test condition with the 'any' behavior."""
other_entity_ids = set(target_entities["included_entities"]) - {entity_id}
excluded_entity_ids = set(target_entities["excluded_entities"]) - {entity_id}
for eid in target_entities["included_entities"]:
set_or_remove_state(hass, eid, states[0]["included_target_state"])
await hass.async_block_till_done()
for eid in excluded_entity_ids:
set_or_remove_state(hass, eid, states[0]["excluded_target_state"])
await hass.async_block_till_done()
condition = await create_target_condition(
hass,
condition=condition,
target=condition_target_config,
behavior="any",
)
for state in states:
included_state = state["included_target_state"]
excluded_state = state["excluded_target_state"]
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
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 condition(hass) == state["condition_true"]
async def assert_condition_behavior_all(
hass: HomeAssistant,
*,
target_entities: dict[str, list[str]],
condition_target_config: dict,
entity_id: str,
entities_in_target: int,
condition: str,
condition_options: dict[str, Any],
states: list[ConditionStateDescription],
) -> None:
"""Test condition with the 'all' behavior."""
other_entity_ids = set(target_entities["included_entities"]) - {entity_id}
excluded_entity_ids = set(target_entities["excluded_entities"]) - {entity_id}
for eid in target_entities["included_entities"]:
set_or_remove_state(hass, eid, states[0]["included_target_state"])
await hass.async_block_till_done()
for eid in excluded_entity_ids:
set_or_remove_state(hass, eid, states[0]["excluded_target_state"])
await hass.async_block_till_done()
condition = await create_target_condition(
hass,
condition=condition,
target=condition_target_config,
behavior="all",
)
for state in states:
included_state = state["included_target_state"]
excluded_state = state["excluded_target_state"]
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true_first_entity"]
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 condition(hass) == state["condition_true"]
async def assert_trigger_behavior_any(
hass: HomeAssistant,
*,
@@ -969,21 +877,21 @@ async def assert_trigger_behavior_any(
states: list[TriggerStateDescription],
) -> None:
"""Test trigger fires in mode any."""
other_entity_ids = set(target_entities["included_entities"]) - {entity_id}
excluded_entity_ids = set(target_entities["excluded_entities"]) - {entity_id}
other_entity_ids = set(target_entities["included"]) - {entity_id}
excluded_entity_ids = set(target_entities["excluded"]) - {entity_id}
for eid in target_entities["included_entities"]:
set_or_remove_state(hass, eid, states[0]["included_target_state"])
for eid in target_entities["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_target_state"])
set_or_remove_state(hass, eid, states[0]["excluded"])
await hass.async_block_till_done()
await arm_trigger(hass, trigger, trigger_options, trigger_target_config)
for state in states[1:]:
excluded_state = state["excluded_target_state"]
included_state = state["included_target_state"]
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"]
@@ -1014,14 +922,14 @@ async def assert_trigger_behavior_first(
states: list[TriggerStateDescription],
) -> None:
"""Test trigger fires in mode first."""
other_entity_ids = set(target_entities["included_entities"]) - {entity_id}
excluded_entity_ids = set(target_entities["excluded_entities"]) - {entity_id}
other_entity_ids = set(target_entities["included"]) - {entity_id}
excluded_entity_ids = set(target_entities["excluded"]) - {entity_id}
for eid in target_entities["included_entities"]:
set_or_remove_state(hass, eid, states[0]["included_target_state"])
for eid in target_entities["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_target_state"])
set_or_remove_state(hass, eid, states[0]["excluded"])
await hass.async_block_till_done()
await arm_trigger(
@@ -1029,8 +937,8 @@ async def assert_trigger_behavior_first(
)
for state in states[1:]:
excluded_state = state["excluded_target_state"]
included_state = state["included_target_state"]
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"]
@@ -1060,14 +968,14 @@ async def assert_trigger_behavior_last(
states: list[TriggerStateDescription],
) -> None:
"""Test trigger fires in mode last."""
other_entity_ids = set(target_entities["included_entities"]) - {entity_id}
excluded_entity_ids = set(target_entities["excluded_entities"]) - {entity_id}
other_entity_ids = set(target_entities["included"]) - {entity_id}
excluded_entity_ids = set(target_entities["excluded"]) - {entity_id}
for eid in target_entities["included_entities"]:
set_or_remove_state(hass, eid, states[0]["included_target_state"])
for eid in target_entities["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_target_state"])
set_or_remove_state(hass, eid, states[0]["excluded"])
await hass.async_block_till_done()
await arm_trigger(
@@ -1075,8 +983,8 @@ async def assert_trigger_behavior_last(
)
for state in states[1:]:
excluded_state = state["excluded_target_state"]
included_state = state["included_target_state"]
excluded_state = state["excluded"]
included_state = state["included"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()

View File

@@ -10,13 +10,12 @@ from homeassistant.core import HomeAssistant
from tests.components.common import (
ConditionStateDescription,
assert_condition_behavior_all,
assert_condition_behavior_any,
assert_condition_gated_by_labs_flag,
create_target_condition,
parametrize_condition_states_all,
parametrize_condition_states_any,
parametrize_target_entities,
set_or_remove_state,
target_entities,
)
@@ -72,7 +71,7 @@ async def test_cover_conditions_gated_by_labs_flag(
(CoverState.CLOSED, {ATTR_IS_CLOSED: True}),
(CoverState.CLOSING, {ATTR_IS_CLOSED: True}),
],
required_filter_attributes={ATTR_DEVICE_CLASS: device_class},
additional_attributes={ATTR_DEVICE_CLASS: device_class},
),
*parametrize_condition_states_any(
condition=is_closed_key,
@@ -85,7 +84,7 @@ async def test_cover_conditions_gated_by_labs_flag(
(CoverState.OPENING, {ATTR_IS_CLOSED: False}),
(CoverState.CLOSING, {ATTR_IS_CLOSED: False}),
],
required_filter_attributes={ATTR_DEVICE_CLASS: device_class},
additional_attributes={ATTR_DEVICE_CLASS: device_class},
),
)
],
@@ -101,17 +100,38 @@ async def test_cover_condition_behavior_any(
states: list[ConditionStateDescription],
) -> None:
"""Test cover condition with the 'any' behavior."""
await assert_condition_behavior_any(
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()
condition = await create_target_condition(
hass,
target_entities=target_covers,
condition_target_config=condition_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
condition=condition,
condition_options=condition_options,
states=states,
target=condition_target_config,
behavior="any",
)
for state in states:
included_state = state["included"]
excluded_state = state["excluded"]
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
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 condition(hass) == state["condition_true"]
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
@@ -135,7 +155,7 @@ async def test_cover_condition_behavior_any(
(CoverState.CLOSED, {ATTR_IS_CLOSED: True}),
(CoverState.CLOSING, {ATTR_IS_CLOSED: True}),
],
required_filter_attributes={ATTR_DEVICE_CLASS: device_class},
additional_attributes={ATTR_DEVICE_CLASS: device_class},
),
*parametrize_condition_states_all(
condition=is_closed_key,
@@ -148,7 +168,7 @@ async def test_cover_condition_behavior_any(
(CoverState.OPENING, {ATTR_IS_CLOSED: False}),
(CoverState.CLOSING, {ATTR_IS_CLOSED: False}),
],
required_filter_attributes={ATTR_DEVICE_CLASS: device_class},
additional_attributes={ATTR_DEVICE_CLASS: device_class},
),
)
],
@@ -164,17 +184,40 @@ async def test_cover_condition_behavior_all(
states: list[ConditionStateDescription],
) -> None:
"""Test cover condition with the 'all' behavior."""
await assert_condition_behavior_all(
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()
condition = await create_target_condition(
hass,
target_entities=target_covers,
condition_target_config=condition_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
condition=condition,
condition_options=condition_options,
states=states,
target=condition_target_config,
behavior="all",
)
for state in states:
included_state = state["included"]
excluded_state = state["excluded"]
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true_first_entity"]
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 condition(hass) == state["condition_true"]
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(

View File

@@ -75,7 +75,7 @@ async def test_cover_triggers_gated_by_labs_flag(
(CoverState.OPEN, {ATTR_IS_CLOSED: None}),
(CoverState.OPEN, {}),
],
required_filter_attributes={ATTR_DEVICE_CLASS: device_class},
additional_attributes={ATTR_DEVICE_CLASS: device_class},
trigger_from_none=False,
),
*parametrize_trigger_states(
@@ -93,7 +93,7 @@ async def test_cover_triggers_gated_by_labs_flag(
(CoverState.OPEN, {ATTR_IS_CLOSED: None}),
(CoverState.OPEN, {}),
],
required_filter_attributes={ATTR_DEVICE_CLASS: device_class},
additional_attributes={ATTR_DEVICE_CLASS: device_class},
trigger_from_none=False,
),
)
@@ -149,7 +149,7 @@ async def test_cover_trigger_behavior_any(
(CoverState.OPEN, {ATTR_IS_CLOSED: None}),
(CoverState.OPEN, {}),
],
required_filter_attributes={ATTR_DEVICE_CLASS: device_class},
additional_attributes={ATTR_DEVICE_CLASS: device_class},
trigger_from_none=False,
),
*parametrize_trigger_states(
@@ -167,7 +167,7 @@ async def test_cover_trigger_behavior_any(
(CoverState.OPEN, {ATTR_IS_CLOSED: None}),
(CoverState.OPEN, {}),
],
required_filter_attributes={ATTR_DEVICE_CLASS: device_class},
additional_attributes={ATTR_DEVICE_CLASS: device_class},
trigger_from_none=False,
),
)
@@ -223,7 +223,7 @@ async def test_cover_trigger_behavior_first(
(CoverState.OPEN, {ATTR_IS_CLOSED: None}),
(CoverState.OPEN, {}),
],
required_filter_attributes={ATTR_DEVICE_CLASS: device_class},
additional_attributes={ATTR_DEVICE_CLASS: device_class},
trigger_from_none=False,
),
*parametrize_trigger_states(
@@ -241,7 +241,7 @@ async def test_cover_trigger_behavior_first(
(CoverState.OPEN, {ATTR_IS_CLOSED: None}),
(CoverState.OPEN, {}),
],
required_filter_attributes={ATTR_DEVICE_CLASS: device_class},
additional_attributes={ATTR_DEVICE_CLASS: device_class},
trigger_from_none=False,
),
)

View File

@@ -1,6 +1,5 @@
"""The tests for the Demo valve platform."""
from collections.abc import Generator
from datetime import timedelta
from unittest.mock import patch
@@ -18,9 +17,10 @@ from homeassistant.components.valve import (
)
from homeassistant.const import ATTR_ENTITY_ID, EVENT_STATE_CHANGED, Platform
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util
from tests.common import MockConfigEntry, async_capture_events, async_fire_time_changed
from tests.common import async_capture_events, async_fire_time_changed
FRONT_GARDEN = "valve.front_garden"
ORCHARD = "valve.orchard"
@@ -28,7 +28,7 @@ BACK_GARDEN = "valve.back_garden"
@pytest.fixture
def valve_only() -> Generator[None]:
async def valve_only() -> None:
"""Enable only the valve platform."""
with patch(
"homeassistant.components.demo.COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM",
@@ -38,12 +38,11 @@ def valve_only() -> Generator[None]:
@pytest.fixture(autouse=True)
async def setup_comp(hass: HomeAssistant, valve_only: None) -> None:
"""Set up demo component from config entry."""
config_entry = MockConfigEntry(domain=DOMAIN)
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
async def setup_comp(hass: HomeAssistant, valve_only: None):
"""Set up demo component."""
assert await async_setup_component(
hass, VALVE_DOMAIN, {VALVE_DOMAIN: {"platform": DOMAIN}}
)
await hass.async_block_till_done()
@@ -51,7 +50,6 @@ async def setup_comp(hass: HomeAssistant, valve_only: None) -> None:
async def test_closing(hass: HomeAssistant) -> None:
"""Test the closing of a valve."""
state = hass.states.get(FRONT_GARDEN)
assert state is not None
assert state.state == ValveState.OPEN
await hass.async_block_till_done()
@@ -65,11 +63,9 @@ async def test_closing(hass: HomeAssistant) -> None:
await hass.async_block_till_done()
assert state_changes[0].data["entity_id"] == FRONT_GARDEN
assert state_changes[0].data["new_state"] is not None
assert state_changes[0].data["new_state"].state == ValveState.CLOSING
assert state_changes[1].data["entity_id"] == FRONT_GARDEN
assert state_changes[1].data["new_state"] is not None
assert state_changes[1].data["new_state"].state == ValveState.CLOSED
@@ -77,7 +73,6 @@ async def test_closing(hass: HomeAssistant) -> None:
async def test_opening(hass: HomeAssistant) -> None:
"""Test the opening of a valve."""
state = hass.states.get(ORCHARD)
assert state is not None
assert state.state == ValveState.CLOSED
await hass.async_block_till_done()
@@ -88,18 +83,15 @@ async def test_opening(hass: HomeAssistant) -> None:
await hass.async_block_till_done()
assert state_changes[0].data["entity_id"] == ORCHARD
assert state_changes[0].data["new_state"] is not None
assert state_changes[0].data["new_state"].state == ValveState.OPENING
assert state_changes[1].data["entity_id"] == ORCHARD
assert state_changes[1].data["new_state"] is not None
assert state_changes[1].data["new_state"].state == ValveState.OPEN
async def test_set_valve_position(hass: HomeAssistant) -> None:
"""Test moving the valve to a specific position."""
state = hass.states.get(BACK_GARDEN)
assert state is not None
assert state.attributes[ATTR_CURRENT_POSITION] == 70
# close to 10%
@@ -110,7 +102,6 @@ async def test_set_valve_position(hass: HomeAssistant) -> None:
blocking=True,
)
state = hass.states.get(BACK_GARDEN)
assert state is not None
assert state.state == ValveState.CLOSING
for _ in range(6):
@@ -119,7 +110,6 @@ async def test_set_valve_position(hass: HomeAssistant) -> None:
await hass.async_block_till_done()
state = hass.states.get(BACK_GARDEN)
assert state is not None
assert state.attributes[ATTR_CURRENT_POSITION] == 10
assert state.state == ValveState.OPEN
@@ -131,7 +121,6 @@ async def test_set_valve_position(hass: HomeAssistant) -> None:
blocking=True,
)
state = hass.states.get(BACK_GARDEN)
assert state is not None
assert state.state == ValveState.OPENING
for _ in range(7):
@@ -140,7 +129,6 @@ async def test_set_valve_position(hass: HomeAssistant) -> None:
await hass.async_block_till_done()
state = hass.states.get(BACK_GARDEN)
assert state is not None
assert state.attributes[ATTR_CURRENT_POSITION] == 80
assert state.state == ValveState.OPEN

View File

@@ -9,12 +9,12 @@ from homeassistant.core import HomeAssistant
from tests.components.common import (
ConditionStateDescription,
assert_condition_behavior_all,
assert_condition_behavior_any,
assert_condition_gated_by_labs_flag,
create_target_condition,
parametrize_condition_states_all,
parametrize_condition_states_any,
parametrize_target_entities,
set_or_remove_state,
target_entities,
)
@@ -70,17 +70,32 @@ async def test_device_tracker_state_condition_behavior_any(
states: list[ConditionStateDescription],
) -> None:
"""Test the device tracker state condition with the 'any' behavior."""
await assert_condition_behavior_any(
other_entity_ids = set(target_device_trackers["included"]) - {entity_id}
# Set all device trackers, including the tested one, to the initial state
for eid in target_device_trackers["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
condition = await create_target_condition(
hass,
target_entities=target_device_trackers,
condition_target_config=condition_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
condition=condition,
condition_options=condition_options,
states=states,
target=condition_target_config,
behavior="any",
)
for state in states:
included_state = state["included"]
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
# Check if changing other device trackers also passes the condition
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
@@ -113,13 +128,29 @@ async def test_device_tracker_state_condition_behavior_all(
states: list[ConditionStateDescription],
) -> None:
"""Test the device tracker state condition with the 'all' behavior."""
await assert_condition_behavior_all(
other_entity_ids = set(target_device_trackers["included"]) - {entity_id}
# Set all device trackers, including the tested one, to the initial state
for eid in target_device_trackers["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
condition = await create_target_condition(
hass,
target_entities=target_device_trackers,
condition_target_config=condition_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
condition=condition,
condition_options=condition_options,
states=states,
target=condition_target_config,
behavior="all",
)
for state in states:
included_state = state["included"]
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true_first_entity"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]

View File

@@ -59,14 +59,14 @@ async def test_door_triggers_gated_by_labs_flag(
trigger="door.opened",
target_states=[STATE_ON],
other_states=[STATE_OFF],
required_filter_attributes={ATTR_DEVICE_CLASS: "door"},
additional_attributes={ATTR_DEVICE_CLASS: "door"},
trigger_from_none=False,
),
*parametrize_trigger_states(
trigger="door.closed",
target_states=[STATE_OFF],
other_states=[STATE_ON],
required_filter_attributes={ATTR_DEVICE_CLASS: "door"},
additional_attributes={ATTR_DEVICE_CLASS: "door"},
trigger_from_none=False,
),
],
@@ -118,7 +118,7 @@ async def test_door_trigger_binary_sensor_behavior_any(
(CoverState.OPEN, {ATTR_IS_CLOSED: None}),
(CoverState.OPEN, {}),
],
required_filter_attributes={ATTR_DEVICE_CLASS: "door"},
additional_attributes={ATTR_DEVICE_CLASS: "door"},
trigger_from_none=False,
),
*parametrize_trigger_states(
@@ -136,7 +136,7 @@ async def test_door_trigger_binary_sensor_behavior_any(
(CoverState.OPEN, {ATTR_IS_CLOSED: None}),
(CoverState.OPEN, {}),
],
required_filter_attributes={ATTR_DEVICE_CLASS: "door"},
additional_attributes={ATTR_DEVICE_CLASS: "door"},
trigger_from_none=False,
),
],
@@ -178,14 +178,14 @@ async def test_door_trigger_cover_behavior_any(
trigger="door.opened",
target_states=[STATE_ON],
other_states=[STATE_OFF],
required_filter_attributes={ATTR_DEVICE_CLASS: "door"},
additional_attributes={ATTR_DEVICE_CLASS: "door"},
trigger_from_none=False,
),
*parametrize_trigger_states(
trigger="door.closed",
target_states=[STATE_OFF],
other_states=[STATE_ON],
required_filter_attributes={ATTR_DEVICE_CLASS: "door"},
additional_attributes={ATTR_DEVICE_CLASS: "door"},
trigger_from_none=False,
),
],
@@ -227,14 +227,14 @@ async def test_door_trigger_binary_sensor_behavior_first(
trigger="door.opened",
target_states=[STATE_ON],
other_states=[STATE_OFF],
required_filter_attributes={ATTR_DEVICE_CLASS: "door"},
additional_attributes={ATTR_DEVICE_CLASS: "door"},
trigger_from_none=False,
),
*parametrize_trigger_states(
trigger="door.closed",
target_states=[STATE_OFF],
other_states=[STATE_ON],
required_filter_attributes={ATTR_DEVICE_CLASS: "door"},
additional_attributes={ATTR_DEVICE_CLASS: "door"},
trigger_from_none=False,
),
],
@@ -286,7 +286,7 @@ async def test_door_trigger_binary_sensor_behavior_last(
(CoverState.OPEN, {ATTR_IS_CLOSED: None}),
(CoverState.OPEN, {}),
],
required_filter_attributes={ATTR_DEVICE_CLASS: "door"},
additional_attributes={ATTR_DEVICE_CLASS: "door"},
trigger_from_none=False,
),
*parametrize_trigger_states(
@@ -304,7 +304,7 @@ async def test_door_trigger_binary_sensor_behavior_last(
(CoverState.OPEN, {ATTR_IS_CLOSED: None}),
(CoverState.OPEN, {}),
],
required_filter_attributes={ATTR_DEVICE_CLASS: "door"},
additional_attributes={ATTR_DEVICE_CLASS: "door"},
trigger_from_none=False,
),
],
@@ -356,7 +356,7 @@ async def test_door_trigger_cover_behavior_first(
(CoverState.OPEN, {ATTR_IS_CLOSED: None}),
(CoverState.OPEN, {}),
],
required_filter_attributes={ATTR_DEVICE_CLASS: "door"},
additional_attributes={ATTR_DEVICE_CLASS: "door"},
trigger_from_none=False,
),
*parametrize_trigger_states(
@@ -374,7 +374,7 @@ async def test_door_trigger_cover_behavior_first(
(CoverState.OPEN, {ATTR_IS_CLOSED: None}),
(CoverState.OPEN, {}),
],
required_filter_attributes={ATTR_DEVICE_CLASS: "door"},
additional_attributes={ATTR_DEVICE_CLASS: "door"},
trigger_from_none=False,
),
],

View File

@@ -81,11 +81,11 @@ async def test_fan_state_condition_behavior_any(
states: list[ConditionStateDescription],
) -> None:
"""Test the fan state condition with the 'any' behavior."""
other_entity_ids = set(target_fans["included_entities"]) - {entity_id}
other_entity_ids = set(target_fans["included"]) - {entity_id}
# Set all fans, including the tested fan, to the initial state
for eid in target_fans["included_entities"]:
set_or_remove_state(hass, eid, states[0]["included_target_state"])
for eid in target_fans["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
condition = await create_target_condition(
@@ -97,13 +97,13 @@ async def test_fan_state_condition_behavior_any(
# Set state for switches to ensure that they don't impact the condition
for state in states:
for eid in target_switches["included_entities"]:
set_or_remove_state(hass, eid, state["included_target_state"])
for eid in target_switches["included"]:
set_or_remove_state(hass, eid, state["included"])
await hass.async_block_till_done()
assert condition(hass) is False
for state in states:
included_state = state["included_target_state"]
included_state = state["included"]
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
@@ -150,11 +150,11 @@ async def test_fan_state_condition_behavior_all(
hass.states.async_set("switch.label_switch_1", STATE_OFF)
hass.states.async_set("switch.label_switch_2", STATE_ON)
other_entity_ids = set(target_fans["included_entities"]) - {entity_id}
other_entity_ids = set(target_fans["included"]) - {entity_id}
# Set all fans, including the tested fan, to the initial state
for eid in target_fans["included_entities"]:
set_or_remove_state(hass, eid, states[0]["included_target_state"])
for eid in target_fans["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
condition = await create_target_condition(
@@ -165,7 +165,7 @@ async def test_fan_state_condition_behavior_all(
)
for state in states:
included_state = state["included_target_state"]
included_state = state["included"]
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()

View File

@@ -59,14 +59,14 @@ async def test_garage_door_triggers_gated_by_labs_flag(
trigger="garage_door.opened",
target_states=[STATE_ON],
other_states=[STATE_OFF],
required_filter_attributes={ATTR_DEVICE_CLASS: "garage_door"},
additional_attributes={ATTR_DEVICE_CLASS: "garage_door"},
trigger_from_none=False,
),
*parametrize_trigger_states(
trigger="garage_door.closed",
target_states=[STATE_OFF],
other_states=[STATE_ON],
required_filter_attributes={ATTR_DEVICE_CLASS: "garage_door"},
additional_attributes={ATTR_DEVICE_CLASS: "garage_door"},
trigger_from_none=False,
),
],
@@ -118,7 +118,7 @@ async def test_garage_door_trigger_binary_sensor_behavior_any(
(CoverState.OPEN, {ATTR_IS_CLOSED: None}),
(CoverState.OPEN, {}),
],
required_filter_attributes={ATTR_DEVICE_CLASS: "garage"},
additional_attributes={ATTR_DEVICE_CLASS: "garage"},
trigger_from_none=False,
),
*parametrize_trigger_states(
@@ -136,7 +136,7 @@ async def test_garage_door_trigger_binary_sensor_behavior_any(
(CoverState.OPEN, {ATTR_IS_CLOSED: None}),
(CoverState.OPEN, {}),
],
required_filter_attributes={ATTR_DEVICE_CLASS: "garage"},
additional_attributes={ATTR_DEVICE_CLASS: "garage"},
trigger_from_none=False,
),
],
@@ -178,14 +178,14 @@ async def test_garage_door_trigger_cover_behavior_any(
trigger="garage_door.opened",
target_states=[STATE_ON],
other_states=[STATE_OFF],
required_filter_attributes={ATTR_DEVICE_CLASS: "garage_door"},
additional_attributes={ATTR_DEVICE_CLASS: "garage_door"},
trigger_from_none=False,
),
*parametrize_trigger_states(
trigger="garage_door.closed",
target_states=[STATE_OFF],
other_states=[STATE_ON],
required_filter_attributes={ATTR_DEVICE_CLASS: "garage_door"},
additional_attributes={ATTR_DEVICE_CLASS: "garage_door"},
trigger_from_none=False,
),
],
@@ -227,14 +227,14 @@ async def test_garage_door_trigger_binary_sensor_behavior_first(
trigger="garage_door.opened",
target_states=[STATE_ON],
other_states=[STATE_OFF],
required_filter_attributes={ATTR_DEVICE_CLASS: "garage_door"},
additional_attributes={ATTR_DEVICE_CLASS: "garage_door"},
trigger_from_none=False,
),
*parametrize_trigger_states(
trigger="garage_door.closed",
target_states=[STATE_OFF],
other_states=[STATE_ON],
required_filter_attributes={ATTR_DEVICE_CLASS: "garage_door"},
additional_attributes={ATTR_DEVICE_CLASS: "garage_door"},
trigger_from_none=False,
),
],
@@ -286,7 +286,7 @@ async def test_garage_door_trigger_binary_sensor_behavior_last(
(CoverState.OPEN, {ATTR_IS_CLOSED: None}),
(CoverState.OPEN, {}),
],
required_filter_attributes={ATTR_DEVICE_CLASS: "garage"},
additional_attributes={ATTR_DEVICE_CLASS: "garage"},
trigger_from_none=False,
),
*parametrize_trigger_states(
@@ -304,7 +304,7 @@ async def test_garage_door_trigger_binary_sensor_behavior_last(
(CoverState.OPEN, {ATTR_IS_CLOSED: None}),
(CoverState.OPEN, {}),
],
required_filter_attributes={ATTR_DEVICE_CLASS: "garage"},
additional_attributes={ATTR_DEVICE_CLASS: "garage"},
trigger_from_none=False,
),
],
@@ -356,7 +356,7 @@ async def test_garage_door_trigger_cover_behavior_first(
(CoverState.OPEN, {ATTR_IS_CLOSED: None}),
(CoverState.OPEN, {}),
],
required_filter_attributes={ATTR_DEVICE_CLASS: "garage"},
additional_attributes={ATTR_DEVICE_CLASS: "garage"},
trigger_from_none=False,
),
*parametrize_trigger_states(
@@ -374,7 +374,7 @@ async def test_garage_door_trigger_cover_behavior_first(
(CoverState.OPEN, {ATTR_IS_CLOSED: None}),
(CoverState.OPEN, {}),
],
required_filter_attributes={ATTR_DEVICE_CLASS: "garage"},
additional_attributes={ATTR_DEVICE_CLASS: "garage"},
trigger_from_none=False,
),
],

View File

@@ -63,7 +63,7 @@ async def test_gate_triggers_gated_by_labs_flag(
(CoverState.OPEN, {ATTR_IS_CLOSED: None}),
(CoverState.OPEN, {}),
],
required_filter_attributes={ATTR_DEVICE_CLASS: "gate"},
additional_attributes={ATTR_DEVICE_CLASS: "gate"},
trigger_from_none=False,
),
*parametrize_trigger_states(
@@ -81,7 +81,7 @@ async def test_gate_triggers_gated_by_labs_flag(
(CoverState.OPEN, {ATTR_IS_CLOSED: None}),
(CoverState.OPEN, {}),
],
required_filter_attributes={ATTR_DEVICE_CLASS: "gate"},
additional_attributes={ATTR_DEVICE_CLASS: "gate"},
trigger_from_none=False,
),
],
@@ -133,7 +133,7 @@ async def test_gate_trigger_cover_behavior_any(
(CoverState.OPEN, {ATTR_IS_CLOSED: None}),
(CoverState.OPEN, {}),
],
required_filter_attributes={ATTR_DEVICE_CLASS: "gate"},
additional_attributes={ATTR_DEVICE_CLASS: "gate"},
trigger_from_none=False,
),
*parametrize_trigger_states(
@@ -151,7 +151,7 @@ async def test_gate_trigger_cover_behavior_any(
(CoverState.OPEN, {ATTR_IS_CLOSED: None}),
(CoverState.OPEN, {}),
],
required_filter_attributes={ATTR_DEVICE_CLASS: "gate"},
additional_attributes={ATTR_DEVICE_CLASS: "gate"},
trigger_from_none=False,
),
],
@@ -203,7 +203,7 @@ async def test_gate_trigger_cover_behavior_first(
(CoverState.OPEN, {ATTR_IS_CLOSED: None}),
(CoverState.OPEN, {}),
],
required_filter_attributes={ATTR_DEVICE_CLASS: "gate"},
additional_attributes={ATTR_DEVICE_CLASS: "gate"},
trigger_from_none=False,
),
*parametrize_trigger_states(
@@ -221,7 +221,7 @@ async def test_gate_trigger_cover_behavior_first(
(CoverState.OPEN, {ATTR_IS_CLOSED: None}),
(CoverState.OPEN, {}),
],
required_filter_attributes={ATTR_DEVICE_CLASS: "gate"},
additional_attributes={ATTR_DEVICE_CLASS: "gate"},
trigger_from_none=False,
),
],

View File

@@ -10,12 +10,12 @@ from homeassistant.core import HomeAssistant
from tests.components.common import (
ConditionStateDescription,
assert_condition_behavior_all,
assert_condition_behavior_any,
assert_condition_gated_by_labs_flag,
create_target_condition,
parametrize_condition_states_all,
parametrize_condition_states_any,
parametrize_target_entities,
set_or_remove_state,
target_entities,
)
@@ -73,17 +73,32 @@ async def test_humidifier_state_condition_behavior_any(
states: list[ConditionStateDescription],
) -> None:
"""Test the humidifier state condition with the 'any' behavior."""
await assert_condition_behavior_any(
other_entity_ids = set(target_humidifiers["included"]) - {entity_id}
# Set all humidifiers, including the tested humidifier, to the initial state
for eid in target_humidifiers["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
condition = await create_target_condition(
hass,
target_entities=target_humidifiers,
condition_target_config=condition_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
condition=condition,
condition_options=condition_options,
states=states,
target=condition_target_config,
behavior="any",
)
for state in states:
included_state = state["included"]
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
# Check if changing other humidifiers also passes the condition
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
@@ -116,17 +131,33 @@ async def test_humidifier_state_condition_behavior_all(
states: list[ConditionStateDescription],
) -> None:
"""Test the humidifier state condition with the 'all' behavior."""
await assert_condition_behavior_all(
other_entity_ids = set(target_humidifiers["included"]) - {entity_id}
# Set all humidifiers, including the tested humidifier, to the initial state
for eid in target_humidifiers["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
condition = await create_target_condition(
hass,
target_entities=target_humidifiers,
condition_target_config=condition_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
condition=condition,
condition_options=condition_options,
states=states,
target=condition_target_config,
behavior="all",
)
for state in states:
included_state = state["included"]
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true_first_entity"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
@@ -159,17 +190,32 @@ async def test_humidifier_attribute_condition_behavior_any(
states: list[ConditionStateDescription],
) -> None:
"""Test the humidifier attribute condition with the 'any' behavior."""
await assert_condition_behavior_any(
other_entity_ids = set(target_humidifiers["included"]) - {entity_id}
# Set all humidifiers, including the tested humidifier, to the initial state
for eid in target_humidifiers["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
condition = await create_target_condition(
hass,
target_entities=target_humidifiers,
condition_target_config=condition_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
condition=condition,
condition_options=condition_options,
states=states,
target=condition_target_config,
behavior="any",
)
for state in states:
included_state = state["included"]
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
# Check if changing other humidifiers also passes the condition
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
@@ -202,13 +248,29 @@ async def test_humidifier_attribute_condition_behavior_all(
states: list[ConditionStateDescription],
) -> None:
"""Test the humidifier attribute condition with the 'all' behavior."""
await assert_condition_behavior_all(
other_entity_ids = set(target_humidifiers["included"]) - {entity_id}
# Set all humidifiers, including the tested humidifier, to the initial state
for eid in target_humidifiers["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
condition = await create_target_condition(
hass,
target_entities=target_humidifiers,
condition_target_config=condition_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
condition=condition,
condition_options=condition_options,
states=states,
target=condition_target_config,
behavior="all",
)
for state in states:
included_state = state["included"]
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true_first_entity"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]

View File

@@ -1,9 +1,8 @@
# serializer version: 1
# name: test_button[1][button.bk1600_enable_standby_mode-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
@@ -51,9 +50,8 @@
# ---
# name: test_button[2][button.cms_sf2000_enable_standby_mode-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,

View File

@@ -9,13 +9,13 @@ from homeassistant.core import HomeAssistant
from tests.components.common import (
ConditionStateDescription,
assert_condition_behavior_all,
assert_condition_behavior_any,
assert_condition_gated_by_labs_flag,
create_target_condition,
other_states,
parametrize_condition_states_all,
parametrize_condition_states_any,
parametrize_target_entities,
set_or_remove_state,
target_entities,
)
@@ -89,17 +89,32 @@ async def test_lawn_mower_state_condition_behavior_any(
states: list[ConditionStateDescription],
) -> None:
"""Test the lawn mower state condition with the 'any' behavior."""
await assert_condition_behavior_any(
other_entity_ids = set(target_lawn_mowers["included"]) - {entity_id}
# Set all lawn mowers, including the tested lawn mower, to the initial state
for eid in target_lawn_mowers["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
condition = await create_target_condition(
hass,
target_entities=target_lawn_mowers,
condition_target_config=condition_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
condition=condition,
condition_options=condition_options,
states=states,
target=condition_target_config,
behavior="any",
)
for state in states:
included_state = state["included"]
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
# Check if changing other lawn mowers also passes the condition
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
@@ -147,13 +162,29 @@ async def test_lawn_mower_state_condition_behavior_all(
states: list[ConditionStateDescription],
) -> None:
"""Test the lawn mower state condition with the 'all' behavior."""
await assert_condition_behavior_all(
other_entity_ids = set(target_lawn_mowers["included"]) - {entity_id}
# Set all lawn mowers, including the tested lawn mower, to the initial state
for eid in target_lawn_mowers["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
condition = await create_target_condition(
hass,
target_entities=target_lawn_mowers,
condition_target_config=condition_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
condition=condition,
condition_options=condition_options,
states=states,
target=condition_target_config,
behavior="all",
)
for state in states:
included_state = state["included"]
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true_first_entity"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]

View File

@@ -81,11 +81,11 @@ async def test_light_state_condition_behavior_any(
states: list[ConditionStateDescription],
) -> None:
"""Test the light state condition with the 'any' behavior."""
other_entity_ids = set(target_lights["included_entities"]) - {entity_id}
other_entity_ids = set(target_lights["included"]) - {entity_id}
# Set all lights, including the tested light, to the initial state
for eid in target_lights["included_entities"]:
set_or_remove_state(hass, eid, states[0]["included_target_state"])
for eid in target_lights["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
condition = await create_target_condition(
@@ -97,13 +97,13 @@ async def test_light_state_condition_behavior_any(
# Set state for switches to ensure that they don't impact the condition
for state in states:
for eid in target_switches["included_entities"]:
set_or_remove_state(hass, eid, state["included_target_state"])
for eid in target_switches["included"]:
set_or_remove_state(hass, eid, state["included"])
await hass.async_block_till_done()
assert condition(hass) is False
for state in states:
included_state = state["included_target_state"]
included_state = state["included"]
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
@@ -150,11 +150,11 @@ async def test_light_state_condition_behavior_all(
hass.states.async_set("switch.label_switch_1", STATE_OFF)
hass.states.async_set("switch.label_switch_2", STATE_ON)
other_entity_ids = set(target_lights["included_entities"]) - {entity_id}
other_entity_ids = set(target_lights["included"]) - {entity_id}
# Set all lights, including the tested light, to the initial state
for eid in target_lights["included_entities"]:
set_or_remove_state(hass, eid, states[0]["included_target_state"])
for eid in target_lights["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
condition = await create_target_condition(
@@ -165,7 +165,7 @@ async def test_light_state_condition_behavior_all(
)
for state in states:
included_state = state["included_target_state"]
included_state = state["included"]
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()

View File

@@ -9,13 +9,13 @@ from homeassistant.core import HomeAssistant
from tests.components.common import (
ConditionStateDescription,
assert_condition_behavior_all,
assert_condition_behavior_any,
assert_condition_gated_by_labs_flag,
create_target_condition,
other_states,
parametrize_condition_states_all,
parametrize_condition_states_any,
parametrize_target_entities,
set_or_remove_state,
target_entities,
)
@@ -83,17 +83,32 @@ async def test_lock_state_condition_behavior_any(
states: list[ConditionStateDescription],
) -> None:
"""Test the lock state condition with the 'any' behavior."""
await assert_condition_behavior_any(
other_entity_ids = set(target_locks["included"]) - {entity_id}
# Set all locks, including the tested lock, to the initial state
for eid in target_locks["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
condition = await create_target_condition(
hass,
target_entities=target_locks,
condition_target_config=condition_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
condition=condition,
condition_options=condition_options,
states=states,
target=condition_target_config,
behavior="any",
)
for state in states:
included_state = state["included"]
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
# Check if changing other locks also passes the condition
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
@@ -136,13 +151,29 @@ async def test_lock_state_condition_behavior_all(
states: list[ConditionStateDescription],
) -> None:
"""Test the lock state condition with the 'all' behavior."""
await assert_condition_behavior_all(
other_entity_ids = set(target_locks["included"]) - {entity_id}
# Set all locks, including the tested lock, to the initial state
for eid in target_locks["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
condition = await create_target_condition(
hass,
target_entities=target_locks,
condition_target_config=condition_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
condition=condition,
condition_options=condition_options,
states=states,
target=condition_target_config,
behavior="all",
)
for state in states:
included_state = state["included"]
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true_first_entity"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]

View File

@@ -9,13 +9,13 @@ from homeassistant.core import HomeAssistant
from tests.components.common import (
ConditionStateDescription,
assert_condition_behavior_all,
assert_condition_behavior_any,
assert_condition_gated_by_labs_flag,
create_target_condition,
other_states,
parametrize_condition_states_all,
parametrize_condition_states_any,
parametrize_target_entities,
set_or_remove_state,
target_entities,
)
@@ -101,17 +101,32 @@ async def test_media_player_state_condition_behavior_any(
states: list[ConditionStateDescription],
) -> None:
"""Test the media player state condition with the 'any' behavior."""
await assert_condition_behavior_any(
other_entity_ids = set(target_media_players["included"]) - {entity_id}
# Set all media players, including the tested media player, to the initial state
for eid in target_media_players["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
condition = await create_target_condition(
hass,
target_entities=target_media_players,
condition_target_config=condition_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
condition=condition,
condition_options=condition_options,
states=states,
target=condition_target_config,
behavior="any",
)
for state in states:
included_state = state["included"]
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
# Check if changing other media players also passes the condition
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
@@ -171,13 +186,29 @@ async def test_media_player_state_condition_behavior_all(
states: list[ConditionStateDescription],
) -> None:
"""Test the media player state condition with the 'all' behavior."""
await assert_condition_behavior_all(
other_entity_ids = set(target_media_players["included"]) - {entity_id}
# Set all media players, including the tested media player, to the initial state
for eid in target_media_players["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
condition = await create_target_condition(
hass,
target_entities=target_media_players,
condition_target_config=condition_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
condition=condition,
condition_options=condition_options,
states=states,
target=condition_target_config,
behavior="all",
)
for state in states:
included_state = state["included"]
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true_first_entity"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]

View File

@@ -52,14 +52,14 @@ async def test_motion_triggers_gated_by_labs_flag(
trigger="motion.detected",
target_states=[STATE_ON],
other_states=[STATE_OFF],
required_filter_attributes={ATTR_DEVICE_CLASS: "motion"},
additional_attributes={ATTR_DEVICE_CLASS: "motion"},
trigger_from_none=False,
),
*parametrize_trigger_states(
trigger="motion.cleared",
target_states=[STATE_OFF],
other_states=[STATE_ON],
required_filter_attributes={ATTR_DEVICE_CLASS: "motion"},
additional_attributes={ATTR_DEVICE_CLASS: "motion"},
trigger_from_none=False,
),
],
@@ -101,14 +101,14 @@ async def test_motion_trigger_binary_sensor_behavior_any(
trigger="motion.detected",
target_states=[STATE_ON],
other_states=[STATE_OFF],
required_filter_attributes={ATTR_DEVICE_CLASS: "motion"},
additional_attributes={ATTR_DEVICE_CLASS: "motion"},
trigger_from_none=False,
),
*parametrize_trigger_states(
trigger="motion.cleared",
target_states=[STATE_OFF],
other_states=[STATE_ON],
required_filter_attributes={ATTR_DEVICE_CLASS: "motion"},
additional_attributes={ATTR_DEVICE_CLASS: "motion"},
trigger_from_none=False,
),
],
@@ -150,14 +150,14 @@ async def test_motion_trigger_binary_sensor_behavior_first(
trigger="motion.detected",
target_states=[STATE_ON],
other_states=[STATE_OFF],
required_filter_attributes={ATTR_DEVICE_CLASS: "motion"},
additional_attributes={ATTR_DEVICE_CLASS: "motion"},
trigger_from_none=False,
),
*parametrize_trigger_states(
trigger="motion.cleared",
target_states=[STATE_OFF],
other_states=[STATE_ON],
required_filter_attributes={ATTR_DEVICE_CLASS: "motion"},
additional_attributes={ATTR_DEVICE_CLASS: "motion"},
trigger_from_none=False,
),
],

View File

@@ -3,7 +3,7 @@
from copy import deepcopy
import json
from typing import Any
from unittest.mock import call, patch
from unittest.mock import patch
import pytest
@@ -30,7 +30,6 @@ from homeassistant.components.vacuum import (
from homeassistant.const import CONF_NAME, ENTITY_MATCH_ALL, STATE_UNKNOWN
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er, issue_registry as ir
from .common import (
help_custom_config,
@@ -64,11 +63,7 @@ from .common import (
from tests.common import async_fire_mqtt_message
from tests.components.vacuum import common
from tests.typing import (
MqttMockHAClientGenerator,
MqttMockPahoClient,
WebSocketGenerator,
)
from tests.typing import MqttMockHAClientGenerator, MqttMockPahoClient
COMMAND_TOPIC = "vacuum/command"
SEND_COMMAND_TOPIC = "vacuum/send_command"
@@ -87,27 +82,6 @@ DEFAULT_CONFIG = {
}
}
CONFIG_CLEAN_SEGMENTS_1 = {
mqtt.DOMAIN: {
vacuum.DOMAIN: {
"name": "test",
"unique_id": "veryunique",
"segments": ["Livingroom", "Kitchen"],
"clean_segments_command_topic": "vacuum/clean_segment",
}
}
}
CONFIG_CLEAN_SEGMENTS_2 = {
mqtt.DOMAIN: {
vacuum.DOMAIN: {
"name": "test",
"unique_id": "veryunique",
"segments": ["1.Livingroom", "2.Kitchen"],
"clean_segments_command_topic": "vacuum/clean_segment",
}
}
}
DEFAULT_CONFIG_2 = {mqtt.DOMAIN: {vacuum.DOMAIN: {"name": "test"}}}
CONFIG_ALL_SERVICES = help_custom_config(
@@ -320,347 +294,6 @@ async def test_command_without_command_topic(
mqtt_mock.async_publish.reset_mock()
@pytest.mark.parametrize("hass_config", [CONFIG_CLEAN_SEGMENTS_1])
async def test_clean_segments_initial_setup_without_repair_issue(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
) -> None:
"""Test cleanable segments initial setup does not fire repair flow."""
await mqtt_mock_entry()
issue_registry = ir.async_get(hass)
assert len(issue_registry.issues) == 0
@pytest.mark.parametrize("hass_config", [CONFIG_CLEAN_SEGMENTS_1])
async def test_clean_segments_command_without_id(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
entity_registry: er.EntityRegistry,
mqtt_mock_entry: MqttMockHAClientGenerator,
) -> None:
"""Test cleanable segments without ID."""
config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0]
entity_registry.async_get_or_create(
vacuum.DOMAIN,
mqtt.DOMAIN,
"veryunique",
config_entry=config_entry,
suggested_object_id="test",
)
entity_registry.async_update_entity_options(
"vacuum.test",
vacuum.DOMAIN,
{
"area_mapping": {"Nabu Casa": ["Kitchen", "Livingroom"]},
"last_seen_segments": [
{"id": "Livingroom", "name": "Livingroom"},
{"id": "Kitchen", "name": "Kitchen"},
],
},
)
mqtt_mock = await mqtt_mock_entry()
await hass.async_block_till_done()
issue_registry = ir.async_get(hass)
# We do not expect a repair flow
assert len(issue_registry.issues) == 0
state = hass.states.get("vacuum.test")
assert state.state == STATE_UNKNOWN
await common.async_clean_area(hass, ["Nabu Casa"], entity_id="vacuum.test")
assert (
call("vacuum/clean_segment", '["Kitchen","Livingroom"]', 0, False)
in mqtt_mock.async_publish.mock_calls
)
client = await hass_ws_client(hass)
await client.send_json_auto_id(
{"type": "vacuum/get_segments", "entity_id": "vacuum.test"}
)
msg = await client.receive_json()
assert msg["success"]
assert msg["result"]["segments"] == [
{"id": "Livingroom", "name": "Livingroom", "group": None},
{"id": "Kitchen", "name": "Kitchen", "group": None},
]
@pytest.mark.parametrize("hass_config", [CONFIG_CLEAN_SEGMENTS_2])
async def test_clean_segments_command_with_id(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
entity_registry: er.EntityRegistry,
mqtt_mock_entry: MqttMockHAClientGenerator,
) -> None:
"""Test cleanable segments with ID."""
mqtt_mock = await mqtt_mock_entry()
# Set the area mapping
entity_registry.async_update_entity_options(
"vacuum.test",
vacuum.DOMAIN,
{
"area_mapping": {"Livingroom": ["1"], "Kitchen": ["2"]},
"last_seen_segments": [
{"id": "1", "name": "Livingroom"},
{"id": "2", "name": "Kitchen"},
],
},
)
await hass.async_block_till_done()
state = hass.states.get("vacuum.test")
assert state.state == STATE_UNKNOWN
await common.async_clean_area(hass, ["Kitchen"], entity_id="vacuum.test")
assert (
call("vacuum/clean_segment", '["2"]', 0, False)
in mqtt_mock.async_publish.mock_calls
)
client = await hass_ws_client(hass)
await client.send_json_auto_id(
{"type": "vacuum/get_segments", "entity_id": "vacuum.test"}
)
msg = await client.receive_json()
assert msg["success"]
assert msg["result"]["segments"] == [
{"id": "1", "name": "Livingroom", "group": None},
{"id": "2", "name": "Kitchen", "group": None},
]
async def test_clean_segments_command_update(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
entity_registry: er.EntityRegistry,
mqtt_mock_entry: MqttMockHAClientGenerator,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test cleanable segments update via discovery."""
# Prepare original entity config entry
config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0]
entity_registry.async_get_or_create(
vacuum.DOMAIN,
mqtt.DOMAIN,
"veryunique",
config_entry=config_entry,
suggested_object_id="test",
)
entity_registry.async_update_entity_options(
"vacuum.test",
vacuum.DOMAIN,
{
"area_mapping": {"Livingroom": ["1"], "Kitchen": ["2"]},
"last_seen_segments": [
{"id": "1", "name": "Livingroom"},
{"id": "2", "name": "Kitchen"},
],
},
)
await mqtt_mock_entry()
# Do initial discovery
config1 = CONFIG_CLEAN_SEGMENTS_2[mqtt.DOMAIN][vacuum.DOMAIN]
payload1 = json.dumps(config1)
config_topic = "homeassistant/vacuum/bla/config"
async_fire_mqtt_message(hass, config_topic, payload1)
await hass.async_block_till_done()
state = hass.states.get("vacuum.test")
assert state.state == STATE_UNKNOWN
issue_registry = ir.async_get(hass)
# We do not expect a repair flow
assert len(issue_registry.issues) == 0
# Update the segments
config2 = config1.copy()
config2["segments"] = ["1.Livingroom", "2.Kitchen", "3.Diningroom"]
payload2 = json.dumps(config2)
async_fire_mqtt_message(hass, config_topic, payload2)
await hass.async_block_till_done()
# A repair flow should start
assert len(issue_registry.issues) == 1
client = await hass_ws_client(hass)
await client.send_json_auto_id(
{"type": "vacuum/get_segments", "entity_id": "vacuum.test"}
)
msg = await client.receive_json()
assert msg["success"]
assert msg["result"]["segments"] == [
{"id": "1", "name": "Livingroom", "group": None},
{"id": "2", "name": "Kitchen", "group": None},
{"id": "3", "name": "Diningroom", "group": None},
]
# Test update with a non-unique segment list fails
config3 = config1.copy()
config3["segments"] = ["1.Livingroom", "2.Kitchen", "2.Diningroom"]
payload3 = json.dumps(config3)
async_fire_mqtt_message(hass, config_topic, payload3)
await hass.async_block_till_done()
assert (
"Error 'The `segments` option contains an invalid or non-unique segment ID '2'"
in caplog.text
)
@pytest.mark.parametrize(
"hass_config",
[
{
mqtt.DOMAIN: {
vacuum.DOMAIN: {
"name": "test",
"unique_id": "veryunique",
"segments": ["Livingroom", "Kitchen", "Kitchen"],
"clean_segments_command_topic": "vacuum/clean_segment",
}
}
},
{
mqtt.DOMAIN: {
vacuum.DOMAIN: {
"name": "test",
"unique_id": "veryunique",
"segments": ["Livingroom", "Kitchen", ""],
"clean_segments_command_topic": "vacuum/clean_segment",
}
}
},
{
mqtt.DOMAIN: {
vacuum.DOMAIN: {
"name": "test",
"unique_id": "veryunique",
"segments": ["1.Livingroom", "1.Kitchen"],
"clean_segments_command_topic": "vacuum/clean_segment",
}
}
},
{
mqtt.DOMAIN: {
vacuum.DOMAIN: {
"name": "test",
"unique_id": "veryunique",
"segments": ["1.Livingroom", "1.Kitchen", ".Diningroom"],
"clean_segments_command_topic": "vacuum/clean_segment",
}
}
},
],
)
async def test_non_unique_segments(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test with non-unique list of cleanable segments with valid segment IDs."""
await mqtt_mock_entry()
assert (
"The `segments` option contains an invalid or non-unique segment ID"
in caplog.text
)
@pytest.mark.usefixtures("hass")
@pytest.mark.parametrize(
("hass_config", "error_message"),
[
(
help_custom_config(
vacuum.DOMAIN,
DEFAULT_CONFIG,
({"clean_segments_command_topic": "test-topic"},),
),
"Options `segments` and "
"`clean_segments_command_topic` must be defined together",
),
(
help_custom_config(
vacuum.DOMAIN,
DEFAULT_CONFIG,
({"segments": ["Livingroom"]},),
),
"Options `segments` and "
"`clean_segments_command_topic` must be defined together",
),
(
help_custom_config(
vacuum.DOMAIN,
DEFAULT_CONFIG,
(
{
"segments": ["Livingroom"],
"clean_segments_command_topic": "test-topic",
},
),
),
"Option `segments` requires `unique_id` to be configured",
),
],
)
async def test_clean_segments_config_validation(
mqtt_mock_entry: MqttMockHAClientGenerator,
error_message: str,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test status clean segment config validation."""
await mqtt_mock_entry()
assert error_message in caplog.text
@pytest.mark.parametrize(
"hass_config",
[
help_custom_config(
vacuum.DOMAIN,
CONFIG_CLEAN_SEGMENTS_2,
({"clean_segments_command_template": "{{ ';'.join(value) }}"},),
)
],
)
async def test_clean_segments_command_with_id_and_command_template(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
entity_registry: er.EntityRegistry,
mqtt_mock_entry: MqttMockHAClientGenerator,
) -> None:
"""Test clean segments with command template."""
mqtt_mock = await mqtt_mock_entry()
entity_registry.async_update_entity_options(
"vacuum.test",
vacuum.DOMAIN,
{
"area_mapping": {"Livingroom": ["1"], "Kitchen": ["2"]},
"last_seen_segments": [
{"id": "1", "name": "Livingroom"},
{"id": "2", "name": "Kitchen"},
],
},
)
await hass.async_block_till_done()
state = hass.states.get("vacuum.test")
assert state.state == STATE_UNKNOWN
await common.async_clean_area(
hass, ["Livingroom", "Kitchen"], entity_id="vacuum.test"
)
assert (
call("vacuum/clean_segment", "1;2", 0, False)
in mqtt_mock.async_publish.mock_calls
)
client = await hass_ws_client(hass)
await client.send_json_auto_id(
{"type": "vacuum/get_segments", "entity_id": "vacuum.test"}
)
msg = await client.receive_json()
assert msg["success"]
assert msg["result"]["segments"] == [
{"id": "1", "name": "Livingroom", "group": None},
{"id": "2", "name": "Kitchen", "group": None},
]
@pytest.mark.parametrize("hass_config", [CONFIG_ALL_SERVICES])
async def test_status(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator

View File

@@ -52,14 +52,14 @@ async def test_occupancy_triggers_gated_by_labs_flag(
trigger="occupancy.detected",
target_states=[STATE_ON],
other_states=[STATE_OFF],
required_filter_attributes={ATTR_DEVICE_CLASS: "occupancy"},
additional_attributes={ATTR_DEVICE_CLASS: "occupancy"},
trigger_from_none=False,
),
*parametrize_trigger_states(
trigger="occupancy.cleared",
target_states=[STATE_OFF],
other_states=[STATE_ON],
required_filter_attributes={ATTR_DEVICE_CLASS: "occupancy"},
additional_attributes={ATTR_DEVICE_CLASS: "occupancy"},
trigger_from_none=False,
),
],
@@ -101,14 +101,14 @@ async def test_occupancy_trigger_binary_sensor_behavior_any(
trigger="occupancy.detected",
target_states=[STATE_ON],
other_states=[STATE_OFF],
required_filter_attributes={ATTR_DEVICE_CLASS: "occupancy"},
additional_attributes={ATTR_DEVICE_CLASS: "occupancy"},
trigger_from_none=False,
),
*parametrize_trigger_states(
trigger="occupancy.cleared",
target_states=[STATE_OFF],
other_states=[STATE_ON],
required_filter_attributes={ATTR_DEVICE_CLASS: "occupancy"},
additional_attributes={ATTR_DEVICE_CLASS: "occupancy"},
trigger_from_none=False,
),
],
@@ -150,14 +150,14 @@ async def test_occupancy_trigger_binary_sensor_behavior_first(
trigger="occupancy.detected",
target_states=[STATE_ON],
other_states=[STATE_OFF],
required_filter_attributes={ATTR_DEVICE_CLASS: "occupancy"},
additional_attributes={ATTR_DEVICE_CLASS: "occupancy"},
trigger_from_none=False,
),
*parametrize_trigger_states(
trigger="occupancy.cleared",
target_states=[STATE_OFF],
other_states=[STATE_ON],
required_filter_attributes={ATTR_DEVICE_CLASS: "occupancy"},
additional_attributes={ATTR_DEVICE_CLASS: "occupancy"},
trigger_from_none=False,
),
],

View File

@@ -9,12 +9,12 @@ from homeassistant.core import HomeAssistant
from tests.components.common import (
ConditionStateDescription,
assert_condition_behavior_all,
assert_condition_behavior_any,
assert_condition_gated_by_labs_flag,
create_target_condition,
parametrize_condition_states_all,
parametrize_condition_states_any,
parametrize_target_entities,
set_or_remove_state,
target_entities,
)
@@ -70,17 +70,32 @@ async def test_person_state_condition_behavior_any(
states: list[ConditionStateDescription],
) -> None:
"""Test the person state condition with the 'any' behavior."""
await assert_condition_behavior_any(
other_entity_ids = set(target_persons["included"]) - {entity_id}
# Set all persons, including the tested person, to the initial state
for eid in target_persons["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
condition = await create_target_condition(
hass,
target_entities=target_persons,
condition_target_config=condition_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
condition=condition,
condition_options=condition_options,
states=states,
target=condition_target_config,
behavior="any",
)
for state in states:
included_state = state["included"]
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
# Check if changing other persons also passes the condition
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
@@ -113,13 +128,29 @@ async def test_person_state_condition_behavior_all(
states: list[ConditionStateDescription],
) -> None:
"""Test the person state condition with the 'all' behavior."""
await assert_condition_behavior_all(
other_entity_ids = set(target_persons["included"]) - {entity_id}
# Set all persons, including the tested person, to the initial state
for eid in target_persons["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
condition = await create_target_condition(
hass,
target_entities=target_persons,
condition_target_config=condition_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
condition=condition,
condition_options=condition_options,
states=states,
target=condition_target_config,
behavior="all",
)
for state in states:
included_state = state["included"]
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true_first_entity"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]

View File

@@ -1,60 +0,0 @@
# serializer version: 1
# name: test_numbers[number.prana_recuperator_display_brightness-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
'max': 6,
'min': 0,
'mode': <NumberMode.SLIDER: 'slider'>,
'step': 1,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'number.prana_recuperator_display_brightness',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Display brightness',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Display brightness',
'platform': 'prana',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'display_brightness',
'unique_id': 'ECC9FFE0E574_display_brightness',
'unit_of_measurement': None,
})
# ---
# name: test_numbers[number.prana_recuperator_display_brightness-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'PRANA RECUPERATOR Display brightness',
'max': 6,
'min': 0,
'mode': <NumberMode.SLIDER: 'slider'>,
'step': 1,
}),
'context': <ANY>,
'entity_id': 'number.prana_recuperator_display_brightness',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '6',
})
# ---

View File

@@ -1,76 +0,0 @@
"""Integration-style tests for Prana numbers."""
from unittest.mock import MagicMock, patch
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.number import (
ATTR_VALUE,
DOMAIN as NUMBER_DOMAIN,
SERVICE_SET_VALUE,
)
from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import async_init_integration
from tests.common import MockConfigEntry, snapshot_platform
async def test_numbers(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
mock_prana_api: MagicMock,
snapshot: SnapshotAssertion,
) -> None:
"""Test the Prana numbers snapshot."""
with patch("homeassistant.components.prana.PLATFORMS", [Platform.NUMBER]):
await async_init_integration(hass, mock_config_entry)
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
@pytest.mark.parametrize(
("input_value", "expected_api_value"),
[
(0.0, 0), # 0 -> 0
(1.0, 1), # 2^(1-1) -> 1
(2.0, 2), # 2^(2-1) -> 2
(3.0, 4), # 2^(3-1) -> 4
(4.0, 8), # 2^(4-1) -> 8
(5.0, 16), # 2^(5-1) -> 16
(6.0, 32), # 2^(6-1) -> 32
],
)
async def test_number_actions(
hass: HomeAssistant,
mock_prana_api: MagicMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
input_value: float,
expected_api_value: int,
) -> None:
"""Test setting number values calls the API with correct math conversion."""
await async_init_integration(hass, mock_config_entry)
entries = er.async_entries_for_config_entry(
entity_registry, mock_config_entry.entry_id
)
assert entries
target = "number.prana_recuperator_display_brightness"
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
{
ATTR_ENTITY_ID: target,
ATTR_VALUE: input_value,
},
blocking=True,
)
mock_prana_api.set_brightness.assert_called_with(expected_api_value)

View File

@@ -40,19 +40,36 @@ async def test_scene_triggers_gated_by_labs_flag(
(
"scene.activated",
[
{"included": {"state": None, "attributes": {}}, "count": 0},
{
"included_target_state": {"state": None, "attributes": {}},
"count": 0,
},
{
"included_target_state": {
"included": {
"state": "2021-01-01T23:59:59+00:00",
"attributes": {},
},
"count": 0,
},
{
"included_target_state": {
"included": {
"state": "2022-01-01T23:59:59+00:00",
"attributes": {},
},
"count": 1,
},
],
),
(
"scene.activated",
[
{"included": {"state": "foo", "attributes": {}}, "count": 0},
{
"included": {
"state": "2021-01-01T23:59:59+00:00",
"attributes": {},
},
"count": 1,
},
{
"included": {
"state": "2022-01-01T23:59:59+00:00",
"attributes": {},
},
@@ -64,54 +81,25 @@ async def test_scene_triggers_gated_by_labs_flag(
"scene.activated",
[
{
"included_target_state": {"state": "foo", "attributes": {}},
"included": {"state": STATE_UNAVAILABLE, "attributes": {}},
"count": 0,
},
{
"included_target_state": {
"state": "2021-01-01T23:59:59+00:00",
"attributes": {},
},
"count": 1,
},
{
"included_target_state": {
"state": "2022-01-01T23:59:59+00:00",
"attributes": {},
},
"count": 1,
},
],
),
(
"scene.activated",
[
{
"included_target_state": {
"state": STATE_UNAVAILABLE,
"attributes": {},
},
"count": 0,
},
{
"included_target_state": {
"included": {
"state": "2021-01-01T23:59:59+00:00",
"attributes": {},
},
"count": 0,
},
{
"included_target_state": {
"included": {
"state": "2022-01-01T23:59:59+00:00",
"attributes": {},
},
"count": 1,
},
{
"included_target_state": {
"state": STATE_UNAVAILABLE,
"attributes": {},
},
"included": {"state": STATE_UNAVAILABLE, "attributes": {}},
"count": 0,
},
],
@@ -119,28 +107,22 @@ async def test_scene_triggers_gated_by_labs_flag(
(
"scene.activated",
[
{"included": {"state": STATE_UNKNOWN, "attributes": {}}, "count": 0},
{
"included_target_state": {"state": STATE_UNKNOWN, "attributes": {}},
"count": 0,
},
{
"included_target_state": {
"included": {
"state": "2021-01-01T23:59:59+00:00",
"attributes": {},
},
"count": 1,
},
{
"included_target_state": {
"included": {
"state": "2022-01-01T23:59:59+00:00",
"attributes": {},
},
"count": 1,
},
{
"included_target_state": {"state": STATE_UNKNOWN, "attributes": {}},
"count": 0,
},
{"included": {"state": STATE_UNKNOWN, "attributes": {}}, "count": 0},
],
),
],
@@ -156,17 +138,17 @@ async def test_scene_state_trigger_behavior_any(
states: list[TriggerStateDescription],
) -> None:
"""Test that the scene state trigger fires when any scene state changes to a specific state."""
other_entity_ids = set(target_scenes["included_entities"]) - {entity_id}
other_entity_ids = set(target_scenes["included"]) - {entity_id}
# Set all scenes, including the tested scene, to the initial state
for eid in target_scenes["included_entities"]:
set_or_remove_state(hass, eid, states[0]["included_target_state"])
for eid in target_scenes["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
await arm_trigger(hass, trigger, None, trigger_target_config)
for state in states[1:]:
included_state = state["included_target_state"]
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"]

View File

@@ -1,7 +1,6 @@
"""The tests for the Season integration."""
from datetime import datetime
from zoneinfo import ZoneInfo
from freezegun import freeze_time
import pytest
@@ -21,8 +20,6 @@ from homeassistant.components.sensor import ATTR_OPTIONS, SensorDeviceClass
from homeassistant.const import ATTR_DEVICE_CLASS, CONF_TYPE, STATE_UNKNOWN
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.entity_component import async_update_entity
from homeassistant.util.dt import UTC
from tests.common import MockConfigEntry
@@ -47,25 +44,25 @@ HEMISPHERE_EMPTY = {
}
NORTHERN_PARAMETERS = [
(TYPE_ASTRONOMICAL, datetime(2017, 9, 3, 0, 0, tzinfo=UTC), STATE_SUMMER),
(TYPE_METEOROLOGICAL, datetime(2017, 8, 13, 0, 0, tzinfo=UTC), STATE_SUMMER),
(TYPE_ASTRONOMICAL, datetime(2017, 9, 23, 0, 0, tzinfo=UTC), STATE_AUTUMN),
(TYPE_METEOROLOGICAL, datetime(2017, 9, 3, 0, 0, tzinfo=UTC), STATE_AUTUMN),
(TYPE_ASTRONOMICAL, datetime(2017, 12, 25, 0, 0, tzinfo=UTC), STATE_WINTER),
(TYPE_METEOROLOGICAL, datetime(2017, 12, 3, 0, 0, tzinfo=UTC), STATE_WINTER),
(TYPE_ASTRONOMICAL, datetime(2017, 4, 1, 0, 0, tzinfo=UTC), STATE_SPRING),
(TYPE_METEOROLOGICAL, datetime(2017, 3, 3, 0, 0, tzinfo=UTC), STATE_SPRING),
(TYPE_ASTRONOMICAL, datetime(2017, 9, 3, 0, 0), STATE_SUMMER),
(TYPE_METEOROLOGICAL, datetime(2017, 8, 13, 0, 0), STATE_SUMMER),
(TYPE_ASTRONOMICAL, datetime(2017, 9, 23, 0, 0), STATE_AUTUMN),
(TYPE_METEOROLOGICAL, datetime(2017, 9, 3, 0, 0), STATE_AUTUMN),
(TYPE_ASTRONOMICAL, datetime(2017, 12, 25, 0, 0), STATE_WINTER),
(TYPE_METEOROLOGICAL, datetime(2017, 12, 3, 0, 0), STATE_WINTER),
(TYPE_ASTRONOMICAL, datetime(2017, 4, 1, 0, 0), STATE_SPRING),
(TYPE_METEOROLOGICAL, datetime(2017, 3, 3, 0, 0), STATE_SPRING),
]
SOUTHERN_PARAMETERS = [
(TYPE_ASTRONOMICAL, datetime(2017, 12, 25, 0, 0, tzinfo=UTC), STATE_SUMMER),
(TYPE_METEOROLOGICAL, datetime(2017, 12, 3, 0, 0, tzinfo=UTC), STATE_SUMMER),
(TYPE_ASTRONOMICAL, datetime(2017, 4, 1, 0, 0, tzinfo=UTC), STATE_AUTUMN),
(TYPE_METEOROLOGICAL, datetime(2017, 3, 3, 0, 0, tzinfo=UTC), STATE_AUTUMN),
(TYPE_ASTRONOMICAL, datetime(2017, 9, 3, 0, 0, tzinfo=UTC), STATE_WINTER),
(TYPE_METEOROLOGICAL, datetime(2017, 8, 13, 0, 0, tzinfo=UTC), STATE_WINTER),
(TYPE_ASTRONOMICAL, datetime(2017, 9, 23, 0, 0, tzinfo=UTC), STATE_SPRING),
(TYPE_METEOROLOGICAL, datetime(2017, 9, 3, 0, 0, tzinfo=UTC), STATE_SPRING),
(TYPE_ASTRONOMICAL, datetime(2017, 12, 25, 0, 0), STATE_SUMMER),
(TYPE_METEOROLOGICAL, datetime(2017, 12, 3, 0, 0), STATE_SUMMER),
(TYPE_ASTRONOMICAL, datetime(2017, 4, 1, 0, 0), STATE_AUTUMN),
(TYPE_METEOROLOGICAL, datetime(2017, 3, 3, 0, 0), STATE_AUTUMN),
(TYPE_ASTRONOMICAL, datetime(2017, 9, 3, 0, 0), STATE_WINTER),
(TYPE_METEOROLOGICAL, datetime(2017, 8, 13, 0, 0), STATE_WINTER),
(TYPE_ASTRONOMICAL, datetime(2017, 9, 23, 0, 0), STATE_SPRING),
(TYPE_METEOROLOGICAL, datetime(2017, 9, 3, 0, 0), STATE_SPRING),
]
@@ -157,7 +154,7 @@ async def test_season_equator(
hass.config.latitude = HEMISPHERE_EQUATOR["homeassistant"]["latitude"]
mock_config_entry.add_to_hass(hass)
with freeze_time(datetime(2017, 9, 3, 0, 0, tzinfo=UTC)):
with freeze_time(datetime(2017, 9, 3, 0, 0)):
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
@@ -168,43 +165,3 @@ async def test_season_equator(
entry = entity_registry.async_get("sensor.season")
assert entry
assert entry.unique_id == mock_config_entry.entry_id
async def test_season_local_midnight(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test that season changes at local midnight, not UTC."""
await hass.config.async_set_time_zone("Australia/Sydney")
hass.config.latitude = HEMISPHERE_SOUTHERN["homeassistant"]["latitude"]
mock_config_entry.add_to_hass(hass)
hass.config_entries.async_update_entry(
mock_config_entry,
unique_id=TYPE_METEOROLOGICAL,
data={CONF_TYPE: TYPE_METEOROLOGICAL},
)
sydney_tz = ZoneInfo("Australia/Sydney")
# The day before autumn starts, at 23:59:59 local time (summer)
day_before = datetime(2017, 2, 28, 23, 59, 59, tzinfo=sydney_tz)
with freeze_time(day_before):
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("sensor.season")
assert state
assert state.state == STATE_SUMMER
# Exactly midnight local time (autumn)
midnight = datetime(2017, 3, 1, 0, 0, 0, tzinfo=sydney_tz)
with freeze_time(midnight):
await async_update_entity(hass, "sensor.season")
await hass.async_block_till_done()
state = hass.states.get("sensor.season")
assert state
assert state.state == STATE_AUTUMN

View File

@@ -81,11 +81,11 @@ async def test_siren_state_condition_behavior_any(
states: list[ConditionStateDescription],
) -> None:
"""Test the siren state condition with the 'any' behavior."""
other_entity_ids = set(target_sirens["included_entities"]) - {entity_id}
other_entity_ids = set(target_sirens["included"]) - {entity_id}
# Set all sirens, including the tested siren, to the initial state
for eid in target_sirens["included_entities"]:
set_or_remove_state(hass, eid, states[0]["included_target_state"])
for eid in target_sirens["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
condition = await create_target_condition(
@@ -97,13 +97,13 @@ async def test_siren_state_condition_behavior_any(
# Set state for switches to ensure that they don't impact the condition
for state in states:
for eid in target_switches["included_entities"]:
set_or_remove_state(hass, eid, state["included_target_state"])
for eid in target_switches["included"]:
set_or_remove_state(hass, eid, state["included"])
await hass.async_block_till_done()
assert condition(hass) is False
for state in states:
included_state = state["included_target_state"]
included_state = state["included"]
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
@@ -149,11 +149,11 @@ async def test_siren_state_condition_behavior_all(
# Set state for two switches to ensure that they don't impact the condition
hass.states.async_set("switch.label_switch_1", STATE_OFF)
hass.states.async_set("switch.label_switch_2", STATE_ON)
other_entity_ids = set(target_sirens["included_entities"]) - {entity_id}
other_entity_ids = set(target_sirens["included"]) - {entity_id}
# Set all sirens, including the tested siren, to the initial state
for eid in target_sirens["included_entities"]:
set_or_remove_state(hass, eid, states[0]["included_target_state"])
for eid in target_sirens["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
condition = await create_target_condition(
@@ -164,7 +164,7 @@ async def test_siren_state_condition_behavior_all(
)
for state in states:
included_state = state["included_target_state"]
included_state = state["included"]
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()

View File

@@ -68,7 +68,6 @@ DEVICE_FIXTURES = [
"da_wm_wm_100002",
"da_wm_wm_000001",
"da_wm_wm_000001_1",
"da_wm_mf_01001",
"da_wm_sc_000001",
"da_wm_dw_01011",
"da_rvc_normal_000001",

View File

@@ -1,331 +0,0 @@
{
"components": {
"main": {
"refresh": {},
"execute": {
"data": {
"value": null
}
},
"samsungce.deviceIdentification": {
"micomAssayCode": {
"value": "20349241",
"timestamp": "2026-03-17T09:56:51.548Z"
},
"modelName": {
"value": null
},
"serialNumber": {
"value": null
},
"serialNumberExtra": {
"value": null
},
"releaseCountry": {
"value": null
},
"modelClassificationCode": {
"value": "3A000000001511000A90020200000000",
"timestamp": "2026-03-17T09:56:51.548Z"
},
"description": {
"value": "AMF-WW-TP1-22-COMMON_FT-MF/DC92-03492A_0001",
"timestamp": "2026-03-17T09:56:51.548Z"
},
"releaseYear": {
"value": 21,
"timestamp": "2025-06-16T00:39:32.549Z"
},
"binaryId": {
"value": "AMF-WW-TP1-22-COMMON",
"timestamp": "2026-03-17T09:56:51.547Z"
}
},
"switch": {
"switch": {
"value": "on",
"timestamp": "2026-03-17T09:56:51.608Z"
}
},
"sec.wifiConfiguration": {
"autoReconnection": {
"value": null
},
"minVersion": {
"value": null
},
"supportedWiFiFreq": {
"value": null
},
"supportedAuthType": {
"value": null
},
"protocolType": {
"value": null
}
},
"samsungce.microfiberFilterOperatingState": {
"operatingState": {
"value": "ready",
"timestamp": "2026-03-17T10:27:51.168Z"
},
"supportedJobStates": {
"value": [
"none",
"filtering",
"bypassing",
"waiting",
"stopping",
"sensing"
],
"timestamp": "2026-03-17T07:49:18.985Z"
},
"supportedOperatingStates": {
"value": ["ready", "running", "paused"],
"timestamp": "2026-03-17T07:49:18.985Z"
},
"microfiberFilterJobState": {
"value": "waiting",
"timestamp": "2026-03-17T10:27:51.168Z"
}
},
"samsungce.selfCheck": {
"result": {
"value": null
},
"supportedActions": {
"value": null
},
"progress": {
"value": null
},
"errors": {
"value": null
},
"status": {
"value": null
}
},
"samsungce.softwareVersion": {
"versions": {
"value": [
{
"id": "0",
"swType": "Software",
"versionNumber": "03334A230323(A603)",
"description": "AMF-WW-TP1-22-COMMON|20349241|3A000000001511000A90020200000000"
},
{
"id": "1",
"swType": "Firmware",
"versionNumber": "23051057,FFFFFFFF",
"description": "Firmware_1_DB_20349241230510571FFFFFFFFFFFFFFFFFFFFFFFFFFE(018020349241FFFFFFFF_30000000)(FileDown:0)(Type:0)"
}
],
"timestamp": "2026-03-17T09:56:51.548Z"
}
},
"samsungce.microfiberFilterSettings": {
"bypassMode": {
"value": "disabled",
"timestamp": "2026-03-17T09:56:51.095Z"
}
},
"ocf": {
"st": {
"value": null
},
"mndt": {
"value": null
},
"mnfv": {
"value": "AMF-WW-TP1-22-COMMON_30230323",
"timestamp": "2025-06-18T08:56:52.092Z"
},
"mnhw": {
"value": "Realtek",
"timestamp": "2025-06-18T08:56:52.092Z"
},
"di": {
"value": "42e80b4d-24c4-a810-11b3-f90375c56a39",
"timestamp": "2025-06-18T08:56:52.092Z"
},
"mnsl": {
"value": "http://www.samsung.com",
"timestamp": "2025-06-18T08:56:52.092Z"
},
"dmv": {
"value": "res.1.1.0,sh.1.1.0",
"timestamp": "2025-06-18T08:56:52.092Z"
},
"n": {
"value": "[microfiber] Samsung",
"timestamp": "2025-06-18T08:56:52.092Z"
},
"mnmo": {
"value": "AMF-WW-TP1-22-COMMON|20349241|3A000000001511000A90020200000000",
"timestamp": "2025-06-18T08:56:52.092Z"
},
"vid": {
"value": "DA-WM-MF-01001",
"timestamp": "2025-06-18T08:56:52.092Z"
},
"mnmn": {
"value": "Samsung Electronics",
"timestamp": "2025-06-18T08:56:52.092Z"
},
"mnml": {
"value": "http://www.samsung.com",
"timestamp": "2025-06-18T08:56:52.092Z"
},
"mnpv": {
"value": "DAWIT 2.0",
"timestamp": "2025-06-18T08:56:52.092Z"
},
"mnos": {
"value": "TizenRT 3.1",
"timestamp": "2025-06-18T08:56:52.092Z"
},
"pi": {
"value": "42e80b4d-24c4-a810-11b3-f90375c56a39",
"timestamp": "2025-06-18T08:56:52.092Z"
},
"icv": {
"value": "core.1.1.0",
"timestamp": "2025-06-18T08:56:52.092Z"
}
},
"custom.disabledCapabilities": {
"disabledCapabilities": {
"value": [],
"timestamp": "2026-03-17T09:19:46.018Z"
}
},
"samsungce.driverVersion": {
"versionNumber": {
"value": 25040101,
"timestamp": "2025-06-16T01:24:28.272Z"
}
},
"samsungce.softwareUpdate": {
"targetModule": {
"value": {},
"timestamp": "2026-03-17T09:56:51.716Z"
},
"otnDUID": {
"value": "MTCHUODPC4IYE",
"timestamp": "2026-03-17T09:56:51.548Z"
},
"lastUpdatedDate": {
"value": null
},
"availableModules": {
"value": [],
"timestamp": "2026-03-17T09:56:51.548Z"
},
"newVersionAvailable": {
"value": false,
"timestamp": "2026-03-17T09:56:51.548Z"
},
"operatingState": {
"value": null
},
"progress": {
"value": null
}
},
"sec.diagnosticsInformation": {
"logType": {
"value": ["errCode", "dump"],
"timestamp": "2026-03-17T09:56:51.548Z"
},
"endpoint": {
"value": "SSM",
"timestamp": "2026-03-17T09:56:51.548Z"
},
"minVersion": {
"value": "1.0",
"timestamp": "2026-03-17T09:56:51.548Z"
},
"signinPermission": {
"value": null
},
"setupId": {
"value": "WM0",
"timestamp": "2026-03-17T09:56:51.548Z"
},
"protocolType": {
"value": "wifi_https",
"timestamp": "2026-03-17T09:56:51.548Z"
},
"tsId": {
"value": null
},
"mnId": {
"value": "0AJT",
"timestamp": "2026-03-17T09:56:51.548Z"
},
"dumpType": {
"value": "file",
"timestamp": "2026-03-17T09:56:51.548Z"
}
},
"samsungce.bladeFilter": {
"bladeFilterStatus": {
"value": null
},
"bladeFilterLastResetDate": {
"value": null
},
"bladeFilterUsage": {
"value": null
},
"bladeFilterResetType": {
"value": null
},
"bladeFilterUsageStep": {
"value": null
},
"bladeFilterCapacity": {
"value": null
}
},
"samsungce.microfiberFilterStatus": {
"supportedStatus": {
"value": ["blockage", "normal"],
"timestamp": "2026-03-17T07:49:18.985Z"
},
"status": {
"value": "normal",
"timestamp": "2026-03-17T07:49:18.985Z"
}
},
"custom.waterFilter": {
"waterFilterUsageStep": {
"value": 1,
"timestamp": "2026-03-17T09:19:46.018Z"
},
"waterFilterResetType": {
"value": ["replaceable"],
"timestamp": "2026-03-17T09:19:46.018Z"
},
"waterFilterCapacity": {
"value": 12,
"unit": "Hour",
"timestamp": "2026-03-17T09:19:46.018Z"
},
"waterFilterLastResetDate": {
"value": null
},
"waterFilterUsage": {
"value": 78,
"timestamp": "2026-03-17T10:17:49.492Z"
},
"waterFilterStatus": {
"value": "normal",
"timestamp": "2026-03-17T09:19:46.018Z"
}
}
}
}
}

View File

@@ -1,126 +0,0 @@
{
"items": [
{
"deviceId": "42e80b4d-24c4-a810-11b3-f90375c56a39",
"name": "[microfiber] Samsung",
"label": "Filtro in microfibra",
"manufacturerName": "Samsung Electronics",
"presentationId": "DA-WM-MF-01001",
"deviceManufacturerCode": "Samsung Electronics",
"locationId": "dc3da2dd-adb0-41b9-9367-9dafc2637386",
"ownerId": "98f99f44-0f42-c20c-a48f-e53c911e27c7",
"roomId": "4696910e-f24d-4831-817b-b8b6b49ed885",
"deviceTypeName": "x.com.st.d.microfiberfilter",
"components": [
{
"id": "main",
"label": "main",
"capabilities": [
{
"id": "ocf",
"version": 1
},
{
"id": "execute",
"version": 1
},
{
"id": "refresh",
"version": 1
},
{
"id": "switch",
"version": 1
},
{
"id": "samsungce.bladeFilter",
"version": 1
},
{
"id": "samsungce.deviceIdentification",
"version": 1
},
{
"id": "samsungce.driverVersion",
"version": 1
},
{
"id": "samsungce.microfiberFilterOperatingState",
"version": 1
},
{
"id": "samsungce.microfiberFilterSettings",
"version": 1
},
{
"id": "samsungce.microfiberFilterStatus",
"version": 1
},
{
"id": "samsungce.selfCheck",
"version": 1
},
{
"id": "samsungce.softwareUpdate",
"version": 1
},
{
"id": "samsungce.softwareVersion",
"version": 1
},
{
"id": "sec.diagnosticsInformation",
"version": 1
},
{
"id": "sec.wifiConfiguration",
"version": 1
},
{
"id": "custom.disabledCapabilities",
"version": 1
},
{
"id": "custom.waterFilter",
"version": 1
}
],
"categories": [
{
"name": "MicroFiberFilter",
"categoryType": "manufacturer"
}
],
"optional": false
}
],
"createTime": "2025-05-06T11:48:20.516Z",
"profile": {
"id": "b40c8b41-e933-334b-8597-d721a881e2ee"
},
"ocf": {
"ocfDeviceType": "x.com.st.d.microfiberfilter",
"name": "[microfiber] Samsung",
"specVersion": "core.1.1.0",
"verticalDomainSpecVersion": "res.1.1.0,sh.1.1.0",
"manufacturerName": "Samsung Electronics",
"modelNumber": "AMF-WW-TP1-22-COMMON|20349241|3A000000001511000A90020200000000",
"platformVersion": "DAWIT 2.0",
"platformOS": "TizenRT 3.1",
"hwVersion": "Realtek",
"firmwareVersion": "AMF-WW-TP1-22-COMMON_30230323",
"vendorId": "DA-WM-MF-01001",
"vendorResourceClientServerVersion": "Realtek Release 3.1.220727",
"lastSignupTime": "2025-05-06T11:48:20.456199900Z",
"transferCandidate": false,
"additionalAuthCodeRequired": false
},
"type": "OCF",
"restrictionTier": 0,
"allowed": null,
"executionContext": "CLOUD",
"relationships": []
}
],
"_links": {}
}

View File

@@ -2325,57 +2325,6 @@
'state': 'off',
})
# ---
# name: test_all_entities[da_wm_mf_01001][binary_sensor.filtro_in_microfibra_filter_status-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.filtro_in_microfibra_filter_status',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Filter status',
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
'original_icon': None,
'original_name': 'Filter status',
'platform': 'smartthings',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'filter_status',
'unique_id': '42e80b4d-24c4-a810-11b3-f90375c56a39_main_custom.waterFilter_waterFilterStatus_waterFilterStatus',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_wm_mf_01001][binary_sensor.filtro_in_microfibra_filter_status-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'problem',
'friendly_name': 'Filtro in microfibra Filter status',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.filtro_in_microfibra_filter_status',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[da_wm_sc_000001][binary_sensor.airdresser_child_lock-entry]
EntityRegistryEntrySnapshot({
'aliases': list([

View File

@@ -549,53 +549,3 @@
'state': 'unknown',
})
# ---
# name: test_all_entities[da_wm_mf_01001][button.filtro_in_microfibra_reset_water_filter-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'button.filtro_in_microfibra_reset_water_filter',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Reset water filter',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Reset water filter',
'platform': 'smartthings',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'reset_water_filter',
'unique_id': '42e80b4d-24c4-a810-11b3-f90375c56a39_main_custom.waterFilter_resetWaterFilter',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_wm_mf_01001][button.filtro_in_microfibra_reset_water_filter-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Filtro in microfibra Reset water filter',
}),
'context': <ANY>,
'entity_id': 'button.filtro_in_microfibra_reset_water_filter',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---

View File

@@ -1270,37 +1270,6 @@
'via_device_id': None,
})
# ---
# name: test_devices[da_wm_mf_01001]
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'config_entries_subentries': <ANY>,
'configuration_url': 'https://account.smartthings.com',
'connections': set({
}),
'disabled_by': None,
'entry_type': None,
'hw_version': 'Realtek',
'id': <ANY>,
'identifiers': set({
tuple(
'smartthings',
'42e80b4d-24c4-a810-11b3-f90375c56a39',
),
}),
'labels': set({
}),
'manufacturer': 'Samsung Electronics',
'model': 'AMF-WW-TP1-22-COMMON',
'model_id': None,
'name': 'Filtro in microfibra',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'sw_version': 'AMF-WW-TP1-22-COMMON_30230323',
'via_device_id': None,
})
# ---
# name: test_devices[da_wm_sc_000001]
DeviceRegistryEntrySnapshot({
'area_id': None,

View File

@@ -727,9 +727,8 @@
# ---
# name: test_all_entities[da_rvc_map_01011][select.robot_vacuum_sound_mode-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([

View File

@@ -14360,60 +14360,6 @@
'state': '1336.2',
})
# ---
# name: test_all_entities[da_wm_mf_01001][sensor.filtro_in_microfibra_water_filter_usage-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'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.filtro_in_microfibra_water_filter_usage',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Water filter usage',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Water filter usage',
'platform': 'smartthings',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'water_filter_usage',
'unique_id': '42e80b4d-24c4-a810-11b3-f90375c56a39_main_custom.waterFilter_waterFilterUsage_waterFilterUsage',
'unit_of_measurement': '%',
})
# ---
# name: test_all_entities[da_wm_mf_01001][sensor.filtro_in_microfibra_water_filter_usage-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Filtro in microfibra Water filter usage',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.filtro_in_microfibra_water_filter_usage',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '78',
})
# ---
# name: test_all_entities[da_wm_sc_000001][sensor.airdresser_completion_time-entry]
EntityRegistryEntrySnapshot({
'aliases': list([

View File

@@ -1499,56 +1499,6 @@
'state': 'off',
})
# ---
# name: test_all_entities[da_wm_mf_01001][switch.filtro_in_microfibra-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'switch',
'entity_category': None,
'entity_id': 'switch.filtro_in_microfibra',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': None,
'platform': 'smartthings',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '42e80b4d-24c4-a810-11b3-f90375c56a39_main_switch_switch_switch',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_wm_mf_01001][switch.filtro_in_microfibra-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Filtro in microfibra',
}),
'context': <ANY>,
'entity_id': 'switch.filtro_in_microfibra',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_all_entities[da_wm_sc_000001][switch.airdresser_auto_cycle_link-entry]
EntityRegistryEntrySnapshot({
'aliases': list([

View File

@@ -81,11 +81,11 @@ async def test_switch_state_condition_behavior_any(
states: list[ConditionStateDescription],
) -> None:
"""Test the switch state condition with the 'any' behavior."""
other_entity_ids = set(target_switches["included_entities"]) - {entity_id}
other_entity_ids = set(target_switches["included"]) - {entity_id}
# Set all switches, including the tested switch, to the initial state
for eid in target_switches["included_entities"]:
set_or_remove_state(hass, eid, states[0]["included_target_state"])
for eid in target_switches["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
condition = await create_target_condition(
@@ -97,13 +97,13 @@ async def test_switch_state_condition_behavior_any(
# Set state for lights to ensure that they don't impact the condition
for state in states:
for eid in target_lights["included_entities"]:
set_or_remove_state(hass, eid, state["included_target_state"])
for eid in target_lights["included"]:
set_or_remove_state(hass, eid, state["included"])
await hass.async_block_till_done()
assert condition(hass) is False
for state in states:
included_state = state["included_target_state"]
included_state = state["included"]
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
@@ -150,11 +150,11 @@ async def test_switch_state_condition_behavior_all(
hass.states.async_set("switch.label_switch_1", STATE_OFF)
hass.states.async_set("switch.label_switch_2", STATE_ON)
other_entity_ids = set(target_switches["included_entities"]) - {entity_id}
other_entity_ids = set(target_switches["included"]) - {entity_id}
# Set all switches, including the tested switch, to the initial state
for eid in target_switches["included_entities"]:
set_or_remove_state(hass, eid, states[0]["included_target_state"])
for eid in target_switches["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
condition = await create_target_condition(
@@ -165,7 +165,7 @@ async def test_switch_state_condition_behavior_all(
)
for state in states:
included_state = state["included_target_state"]
included_state = state["included"]
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()

View File

@@ -40,75 +40,39 @@ async def test_text_triggers_gated_by_labs_flag(
(
"text.changed",
[
{
"included_target_state": {"state": None, "attributes": {}},
"count": 0,
},
{
"included_target_state": {"state": "bar", "attributes": {}},
"count": 0,
},
{
"included_target_state": {"state": "baz", "attributes": {}},
"count": 1,
},
{"included": {"state": None, "attributes": {}}, "count": 0},
{"included": {"state": "bar", "attributes": {}}, "count": 0},
{"included": {"state": "baz", "attributes": {}}, "count": 1},
],
),
(
"text.changed",
[
{
"included_target_state": {"state": "foo", "attributes": {}},
"count": 0,
},
{
"included_target_state": {"state": "bar", "attributes": {}},
"count": 1,
},
{
"included_target_state": {"state": "baz", "attributes": {}},
"count": 1,
},
{"included": {"state": "foo", "attributes": {}}, "count": 0},
{"included": {"state": "bar", "attributes": {}}, "count": 1},
{"included": {"state": "baz", "attributes": {}}, "count": 1},
],
),
(
"text.changed",
[
{
"included_target_state": {"state": "foo", "attributes": {}},
"count": 0,
},
{"included": {"state": "foo", "attributes": {}}, "count": 0},
# empty string
{"included_target_state": {"state": "", "attributes": {}}, "count": 1},
{
"included_target_state": {"state": "baz", "attributes": {}},
"count": 1,
},
{"included": {"state": "", "attributes": {}}, "count": 1},
{"included": {"state": "baz", "attributes": {}}, "count": 1},
],
),
(
"text.changed",
[
{
"included_target_state": {
"state": STATE_UNAVAILABLE,
"attributes": {},
},
"included": {"state": STATE_UNAVAILABLE, "attributes": {}},
"count": 0,
},
{"included": {"state": "bar", "attributes": {}}, "count": 0},
{"included": {"state": "baz", "attributes": {}}, "count": 1},
{
"included_target_state": {"state": "bar", "attributes": {}},
"count": 0,
},
{
"included_target_state": {"state": "baz", "attributes": {}},
"count": 1,
},
{
"included_target_state": {
"state": STATE_UNAVAILABLE,
"attributes": {},
},
"included": {"state": STATE_UNAVAILABLE, "attributes": {}},
"count": 0,
},
],
@@ -116,22 +80,10 @@ async def test_text_triggers_gated_by_labs_flag(
(
"text.changed",
[
{
"included_target_state": {"state": STATE_UNKNOWN, "attributes": {}},
"count": 0,
},
{
"included_target_state": {"state": "bar", "attributes": {}},
"count": 0,
},
{
"included_target_state": {"state": "baz", "attributes": {}},
"count": 1,
},
{
"included_target_state": {"state": STATE_UNKNOWN, "attributes": {}},
"count": 0,
},
{"included": {"state": STATE_UNKNOWN, "attributes": {}}, "count": 0},
{"included": {"state": "bar", "attributes": {}}, "count": 0},
{"included": {"state": "baz", "attributes": {}}, "count": 1},
{"included": {"state": STATE_UNKNOWN, "attributes": {}}, "count": 0},
],
),
],
@@ -147,17 +99,17 @@ async def test_text_state_trigger_behavior_any(
states: list[TriggerStateDescription],
) -> None:
"""Test that the text state trigger fires when any text state changes to a specific state."""
other_entity_ids = set(target_texts["included_entities"]) - {entity_id}
other_entity_ids = set(target_texts["included"]) - {entity_id}
# Set all texts, including the tested text, to the initial state
for eid in target_texts["included_entities"]:
set_or_remove_state(hass, eid, states[0]["included_target_state"])
for eid in target_texts["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
await arm_trigger(hass, trigger, None, trigger_target_config)
for state in states[1:]:
included_state = state["included_target_state"]
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"]

View File

@@ -1231,53 +1231,3 @@ async def test_list_todo_items_extended_fields(
]
}
}
async def test_async_subscribe_updates(
hass: HomeAssistant, test_entity: TodoListEntity
) -> None:
"""Test async_subscribe_updates delivers list updates to listeners."""
await create_mock_platform(hass, [test_entity])
received_updates: list[list[TodoItem]] = []
def listener(items: list[TodoItem]) -> None:
received_updates.append(items)
unsub = test_entity.async_subscribe_updates(listener)
# Trigger an update
test_entity.async_write_ha_state()
assert len(received_updates) == 1
items = received_updates[0]
assert len(items) == 2
assert isinstance(items[0], TodoItem)
assert items[0].summary == "Item #1"
assert items[0].uid == "1"
assert items[0].status == TodoItemStatus.NEEDS_ACTION
assert isinstance(items[1], TodoItem)
assert items[1].summary == "Item #2"
assert items[1].uid == "2"
assert items[1].status == TodoItemStatus.COMPLETED
# Verify items are copies (not the same objects)
assert items[0] is not test_entity.todo_items[0]
assert items[1] is not test_entity.todo_items[1]
# Add a new item and trigger update
test_entity._attr_todo_items = [
*test_entity._attr_todo_items,
TodoItem(summary="Item #3", uid="3", status=TodoItemStatus.NEEDS_ACTION),
]
test_entity.async_write_ha_state()
assert len(received_updates) == 2
items = received_updates[1]
assert len(items) == 3
assert items[2].summary == "Item #3"
# Unsubscribe and verify no more updates
unsub()
test_entity.async_write_ha_state()
assert len(received_updates) == 2

View File

@@ -10,7 +10,6 @@ from homeassistant.components.vacuum import (
ATTR_FAN_SPEED,
ATTR_PARAMS,
DOMAIN,
SERVICE_CLEAN_AREA,
SERVICE_CLEAN_SPOT,
SERVICE_LOCATE,
SERVICE_PAUSE,
@@ -83,25 +82,6 @@ async def async_locate(hass: HomeAssistant, entity_id: str = ENTITY_MATCH_ALL) -
await hass.services.async_call(DOMAIN, SERVICE_LOCATE, data, blocking=True)
def clean_area(
hass: HomeAssistant, cleaning_area_id: list[str], entity_id: str = ENTITY_MATCH_ALL
) -> None:
"""Tell all or specified vacuum to perform an area clean."""
hass.add_job(async_clean_area, hass, cleaning_area_id, entity_id)
async def async_clean_area(
hass: HomeAssistant,
cleaning_area_id: list[str],
entity_id: str = ENTITY_MATCH_ALL,
) -> None:
"""Tell all or specified vacuum to perform an area clean."""
data = {"cleaning_area_id": cleaning_area_id}
if entity_id:
data[ATTR_ENTITY_ID] = entity_id
await hass.services.async_call(DOMAIN, SERVICE_CLEAN_AREA, data, blocking=True)
@bind_hass
def clean_spot(hass: HomeAssistant, entity_id: str = ENTITY_MATCH_ALL) -> None:
"""Tell all or specified vacuum to perform a spot clean-up."""

View File

@@ -9,13 +9,13 @@ from homeassistant.core import HomeAssistant
from tests.components.common import (
ConditionStateDescription,
assert_condition_behavior_all,
assert_condition_behavior_any,
assert_condition_gated_by_labs_flag,
create_target_condition,
other_states,
parametrize_condition_states_all,
parametrize_condition_states_any,
parametrize_target_entities,
set_or_remove_state,
target_entities,
)
@@ -89,17 +89,32 @@ async def test_vacuum_state_condition_behavior_any(
states: list[ConditionStateDescription],
) -> None:
"""Test the vacuum state condition with the 'any' behavior."""
await assert_condition_behavior_any(
other_entity_ids = set(target_vacuums["included"]) - {entity_id}
# Set all vacuums, including the tested vacuum, to the initial state
for eid in target_vacuums["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
condition = await create_target_condition(
hass,
target_entities=target_vacuums,
condition_target_config=condition_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
condition=condition,
condition_options=condition_options,
states=states,
target=condition_target_config,
behavior="any",
)
for state in states:
included_state = state["included"]
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
# Check if changing other vacuums also passes the condition
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
@@ -147,13 +162,29 @@ async def test_vacuum_state_condition_behavior_all(
states: list[ConditionStateDescription],
) -> None:
"""Test the vacuum state condition with the 'all' behavior."""
await assert_condition_behavior_all(
other_entity_ids = set(target_vacuums["included"]) - {entity_id}
# Set all vacuums, including the tested vacuum, to the initial state
for eid in target_vacuums["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
condition = await create_target_condition(
hass,
target_entities=target_vacuums,
condition_target_config=condition_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
condition=condition,
condition_options=condition_options,
states=states,
target=condition_target_config,
behavior="all",
)
for state in states:
included_state = state["included"]
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true_first_entity"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]

View File

@@ -59,14 +59,14 @@ async def test_window_triggers_gated_by_labs_flag(
trigger="window.opened",
target_states=[STATE_ON],
other_states=[STATE_OFF],
required_filter_attributes={ATTR_DEVICE_CLASS: "window"},
additional_attributes={ATTR_DEVICE_CLASS: "window"},
trigger_from_none=False,
),
*parametrize_trigger_states(
trigger="window.closed",
target_states=[STATE_OFF],
other_states=[STATE_ON],
required_filter_attributes={ATTR_DEVICE_CLASS: "window"},
additional_attributes={ATTR_DEVICE_CLASS: "window"},
trigger_from_none=False,
),
],
@@ -118,7 +118,7 @@ async def test_window_trigger_binary_sensor_behavior_any(
(CoverState.OPEN, {ATTR_IS_CLOSED: None}),
(CoverState.OPEN, {}),
],
required_filter_attributes={ATTR_DEVICE_CLASS: "window"},
additional_attributes={ATTR_DEVICE_CLASS: "window"},
trigger_from_none=False,
),
*parametrize_trigger_states(
@@ -136,7 +136,7 @@ async def test_window_trigger_binary_sensor_behavior_any(
(CoverState.OPEN, {ATTR_IS_CLOSED: None}),
(CoverState.OPEN, {}),
],
required_filter_attributes={ATTR_DEVICE_CLASS: "window"},
additional_attributes={ATTR_DEVICE_CLASS: "window"},
trigger_from_none=False,
),
],
@@ -178,14 +178,14 @@ async def test_window_trigger_cover_behavior_any(
trigger="window.opened",
target_states=[STATE_ON],
other_states=[STATE_OFF],
required_filter_attributes={ATTR_DEVICE_CLASS: "window"},
additional_attributes={ATTR_DEVICE_CLASS: "window"},
trigger_from_none=False,
),
*parametrize_trigger_states(
trigger="window.closed",
target_states=[STATE_OFF],
other_states=[STATE_ON],
required_filter_attributes={ATTR_DEVICE_CLASS: "window"},
additional_attributes={ATTR_DEVICE_CLASS: "window"},
trigger_from_none=False,
),
],
@@ -227,14 +227,14 @@ async def test_window_trigger_binary_sensor_behavior_first(
trigger="window.opened",
target_states=[STATE_ON],
other_states=[STATE_OFF],
required_filter_attributes={ATTR_DEVICE_CLASS: "window"},
additional_attributes={ATTR_DEVICE_CLASS: "window"},
trigger_from_none=False,
),
*parametrize_trigger_states(
trigger="window.closed",
target_states=[STATE_OFF],
other_states=[STATE_ON],
required_filter_attributes={ATTR_DEVICE_CLASS: "window"},
additional_attributes={ATTR_DEVICE_CLASS: "window"},
trigger_from_none=False,
),
],
@@ -286,7 +286,7 @@ async def test_window_trigger_binary_sensor_behavior_last(
(CoverState.OPEN, {ATTR_IS_CLOSED: None}),
(CoverState.OPEN, {}),
],
required_filter_attributes={ATTR_DEVICE_CLASS: "window"},
additional_attributes={ATTR_DEVICE_CLASS: "window"},
trigger_from_none=False,
),
*parametrize_trigger_states(
@@ -304,7 +304,7 @@ async def test_window_trigger_binary_sensor_behavior_last(
(CoverState.OPEN, {ATTR_IS_CLOSED: None}),
(CoverState.OPEN, {}),
],
required_filter_attributes={ATTR_DEVICE_CLASS: "window"},
additional_attributes={ATTR_DEVICE_CLASS: "window"},
trigger_from_none=False,
),
],
@@ -356,7 +356,7 @@ async def test_window_trigger_cover_behavior_first(
(CoverState.OPEN, {ATTR_IS_CLOSED: None}),
(CoverState.OPEN, {}),
],
required_filter_attributes={ATTR_DEVICE_CLASS: "window"},
additional_attributes={ATTR_DEVICE_CLASS: "window"},
trigger_from_none=False,
),
*parametrize_trigger_states(
@@ -374,7 +374,7 @@ async def test_window_trigger_cover_behavior_first(
(CoverState.OPEN, {ATTR_IS_CLOSED: None}),
(CoverState.OPEN, {}),
],
required_filter_attributes={ATTR_DEVICE_CLASS: "window"},
additional_attributes={ATTR_DEVICE_CLASS: "window"},
trigger_from_none=False,
),
],

View File

@@ -1043,6 +1043,136 @@ async def test_state_translated(
assert result == "unknown"
async def test_state_attr_translated(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None:
"""Test state_attr_translated method."""
await translation._async_get_translations_cache(hass).async_load("en", set())
hass.states.async_set(
"climate.living_room",
"heat",
attributes={"fan_mode": "auto", "hvac_action": "heating"},
)
hass.states.async_set(
"switch.test",
"on",
attributes={"some_attr": "some_value", "numeric_attr": 42, "bool_attr": True},
)
result = render(
hass,
'{{ state_attr_translated("switch.test", "some_attr") }}',
)
assert result == "some_value"
# Non-string attributes should be returned as-is without type conversion
result = render(
hass,
'{{ state_attr_translated("switch.test", "numeric_attr") }}',
)
assert result == 42
assert isinstance(result, int)
result = render(
hass,
'{{ state_attr_translated("switch.test", "bool_attr") }}',
)
assert result is True
result = render(
hass,
'{{ state_attr_translated("climate.non_existent", "fan_mode") }}',
)
assert result is None
with pytest.raises(TemplateError):
render(hass, '{{ state_attr_translated("-invalid", "fan_mode") }}')
result = render(
hass,
'{{ state_attr_translated("climate.living_room", "non_existent") }}',
)
assert result is None
@pytest.mark.parametrize(
(
"entity_id",
"attribute",
"translations",
"expected_result",
),
[
(
"climate.test_platform_5678",
"fan_mode",
{
"component.test_platform.entity.climate.my_climate.state_attributes.fan_mode.state.auto": "Platform Automatic",
},
"Platform Automatic",
),
(
"climate.living_room",
"fan_mode",
{
"component.climate.entity_component._.state_attributes.fan_mode.state.auto": "Automatic",
},
"Automatic",
),
(
"climate.living_room",
"hvac_action",
{
"component.climate.entity_component._.state_attributes.hvac_action.state.heating": "Heating",
},
"Heating",
),
],
)
async def test_state_attr_translated_translation_lookups(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
entity_id: str,
attribute: str,
translations: dict[str, str],
expected_result: str,
) -> None:
"""Test state_attr_translated translation lookups."""
await translation._async_get_translations_cache(hass).async_load("en", set())
hass.states.async_set(
"climate.living_room",
"heat",
attributes={"fan_mode": "auto", "hvac_action": "heating"},
)
config_entry = MockConfigEntry(domain="climate")
config_entry.add_to_hass(hass)
entity_registry.async_get_or_create(
"climate",
"test_platform",
"5678",
config_entry=config_entry,
translation_key="my_climate",
)
hass.states.async_set(
"climate.test_platform_5678",
"heat",
attributes={"fan_mode": "auto"},
)
with patch(
"homeassistant.helpers.translation.async_get_cached_translations",
return_value=translations,
):
result = render(
hass,
f'{{{{ state_attr_translated("{entity_id}", "{attribute}") }}}}',
)
assert result == expected_result
def test_has_value(hass: HomeAssistant) -> None:
"""Test has_value method."""
hass.states.async_set("test.value1", 1)

View File

@@ -683,6 +683,92 @@ async def test_translate_state(hass: HomeAssistant) -> None:
assert result == "on"
async def test_translate_state_attr(hass: HomeAssistant) -> None:
"""Test the state attribute translation helper."""
with patch(
"homeassistant.helpers.translation.async_get_cached_translations",
return_value={
"component.platform.entity.climate.translation_key.state_attributes.fan_mode.state.auto": "TRANSLATED"
},
) as mock:
result = translation.async_translate_state_attr(
hass,
"auto",
"climate",
"platform",
"translation_key",
None,
"fan_mode",
)
mock.assert_called_once_with(hass, hass.config.language, "entity")
assert result == "TRANSLATED"
with patch(
"homeassistant.helpers.translation.async_get_cached_translations",
return_value={
"component.climate.entity_component.device_class.state_attributes.fan_mode.state.auto": "TRANSLATED"
},
) as mock:
result = translation.async_translate_state_attr(
hass,
"auto",
"climate",
"platform",
None,
"device_class",
"fan_mode",
)
mock.assert_called_once_with(hass, hass.config.language, "entity_component")
assert result == "TRANSLATED"
with patch(
"homeassistant.helpers.translation.async_get_cached_translations",
return_value={
"component.climate.entity_component._.state_attributes.fan_mode.state.auto": "TRANSLATED"
},
) as mock:
result = translation.async_translate_state_attr(
hass, "auto", "climate", "platform", None, None, "fan_mode"
)
mock.assert_called_once_with(hass, hass.config.language, "entity_component")
assert result == "TRANSLATED"
with patch(
"homeassistant.helpers.translation.async_get_cached_translations",
return_value={},
) as mock:
result = translation.async_translate_state_attr(
hass, "auto", "climate", "platform", None, None, "fan_mode"
)
mock.assert_has_calls(
[
call(hass, hass.config.language, "entity_component"),
]
)
assert result == "auto"
with patch(
"homeassistant.helpers.translation.async_get_cached_translations",
return_value={},
) as mock:
result = translation.async_translate_state_attr(
hass,
"auto",
"climate",
"platform",
"translation_key",
"device_class",
"fan_mode",
)
mock.assert_has_calls(
[
call(hass, hass.config.language, "entity"),
call(hass, hass.config.language, "entity_component"),
]
)
assert result == "auto"
async def test_get_translations_still_has_title_without_translations_files(
hass: HomeAssistant, mock_config_flows
) -> None: