mirror of
https://github.com/home-assistant/core.git
synced 2026-03-18 08:52:03 +01:00
Compare commits
18 Commits
gha-builde
...
add_temper
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
316fb2808a | ||
|
|
77fec15352 | ||
|
|
ec62d8ebb2 | ||
|
|
67e6bc523e | ||
|
|
c8ce4eb32d | ||
|
|
22aca8b7af | ||
|
|
770864082f | ||
|
|
14545660e2 | ||
|
|
836353015b | ||
|
|
c57ffd4d78 | ||
|
|
cbebfdf149 | ||
|
|
d8ed9ca66f | ||
|
|
5caf8a5b83 | ||
|
|
c05210683e | ||
|
|
aa8dd4bb66 | ||
|
|
ee7d6157d9 | ||
|
|
adec1d128c | ||
|
|
0a2fc97696 |
2
CODEOWNERS
generated
2
CODEOWNERS
generated
@@ -1699,6 +1699,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/tellduslive/ @fredrike
|
||||
/homeassistant/components/teltonika/ @karlbeecken
|
||||
/tests/components/teltonika/ @karlbeecken
|
||||
/homeassistant/components/temperature/ @home-assistant/core
|
||||
/tests/components/temperature/ @home-assistant/core
|
||||
/homeassistant/components/template/ @Petro31 @home-assistant/core
|
||||
/tests/components/template/ @Petro31 @home-assistant/core
|
||||
/homeassistant/components/tesla_fleet/ @Bre77
|
||||
|
||||
@@ -247,6 +247,7 @@ DEFAULT_INTEGRATIONS = {
|
||||
"humidity",
|
||||
"motion",
|
||||
"occupancy",
|
||||
"temperature",
|
||||
"window",
|
||||
}
|
||||
DEFAULT_INTEGRATIONS_RECOVERY_MODE = {
|
||||
|
||||
@@ -46,19 +46,10 @@ async def async_setup_entry(
|
||||
api.AsyncConfigEntryAuth(aiohttp_client.async_get_clientsession(hass), session)
|
||||
)
|
||||
|
||||
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
|
||||
coordinator = AladdinConnectCoordinator(hass, entry, client)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
entry.runtime_data = {
|
||||
door.unique_id: AladdinConnectCoordinator(hass, entry, client, door)
|
||||
for door in doors
|
||||
}
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
@@ -100,7 +91,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)
|
||||
all_device_ids = set(config_entry.runtime_data.data)
|
||||
|
||||
for device_entry in device_entries:
|
||||
device_id: str | None = None
|
||||
|
||||
@@ -11,22 +11,24 @@ 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[dict[str, AladdinConnectCoordinator]]
|
||||
type AladdinConnectConfigEntry = ConfigEntry[AladdinConnectCoordinator]
|
||||
SCAN_INTERVAL = timedelta(seconds=15)
|
||||
|
||||
|
||||
class AladdinConnectCoordinator(DataUpdateCoordinator[GarageDoor]):
|
||||
class AladdinConnectCoordinator(DataUpdateCoordinator[dict[str, 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__(
|
||||
@@ -37,18 +39,16 @@ class AladdinConnectCoordinator(DataUpdateCoordinator[GarageDoor]):
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
self.client = client
|
||||
self.data = garage_door
|
||||
|
||||
async def _async_update_data(self) -> GarageDoor:
|
||||
async def _async_update_data(self) -> dict[str, GarageDoor]:
|
||||
"""Fetch data from the Aladdin Connect API."""
|
||||
try:
|
||||
await self.client.update_door(self.data.device_id, self.data.door_number)
|
||||
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
|
||||
except aiohttp.ClientError as err:
|
||||
raise UpdateFailed(f"Error communicating with API: {err}") from err
|
||||
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
|
||||
|
||||
return {door.unique_id: door for door in doors}
|
||||
|
||||
@@ -7,7 +7,7 @@ from typing import Any
|
||||
import aiohttp
|
||||
|
||||
from homeassistant.components.cover import CoverDeviceClass, CoverEntity
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
@@ -24,11 +24,22 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the cover platform."""
|
||||
coordinators = entry.runtime_data
|
||||
coordinator = entry.runtime_data
|
||||
known_devices: set[str] = set()
|
||||
|
||||
async_add_entities(
|
||||
AladdinCoverEntity(coordinator) for coordinator in coordinators.values()
|
||||
)
|
||||
@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))
|
||||
|
||||
|
||||
class AladdinCoverEntity(AladdinConnectEntity, CoverEntity):
|
||||
@@ -38,10 +49,10 @@ class AladdinCoverEntity(AladdinConnectEntity, CoverEntity):
|
||||
_attr_supported_features = SUPPORTED_FEATURES
|
||||
_attr_name = None
|
||||
|
||||
def __init__(self, coordinator: AladdinConnectCoordinator) -> None:
|
||||
def __init__(self, coordinator: AladdinConnectCoordinator, door_id: str) -> None:
|
||||
"""Initialize the Aladdin Connect cover."""
|
||||
super().__init__(coordinator)
|
||||
self._attr_unique_id = coordinator.data.unique_id
|
||||
super().__init__(coordinator, door_id)
|
||||
self._attr_unique_id = door_id
|
||||
|
||||
async def async_open_cover(self, **kwargs: Any) -> None:
|
||||
"""Issue open command to cover."""
|
||||
@@ -66,16 +77,16 @@ class AladdinCoverEntity(AladdinConnectEntity, CoverEntity):
|
||||
@property
|
||||
def is_closed(self) -> bool | None:
|
||||
"""Update is closed attribute."""
|
||||
if (status := self.coordinator.data.status) is None:
|
||||
if (status := self.door.status) is None:
|
||||
return None
|
||||
return status == "closed"
|
||||
|
||||
@property
|
||||
def is_closing(self) -> bool | None:
|
||||
"""Update is closing attribute."""
|
||||
return self.coordinator.data.status == "closing"
|
||||
return self.door.status == "closing"
|
||||
|
||||
@property
|
||||
def is_opening(self) -> bool | None:
|
||||
"""Update is opening attribute."""
|
||||
return self.coordinator.data.status == "opening"
|
||||
return self.door.status == "opening"
|
||||
|
||||
@@ -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": 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,
|
||||
"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,
|
||||
}
|
||||
for uid, coordinator in config_entry.runtime_data.items()
|
||||
for uid, door in config_entry.runtime_data.data.items()
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""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
|
||||
@@ -14,17 +15,28 @@ class AladdinConnectEntity(CoordinatorEntity[AladdinConnectCoordinator]):
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, coordinator: AladdinConnectCoordinator) -> None:
|
||||
def __init__(self, coordinator: AladdinConnectCoordinator, door_id: str) -> None:
|
||||
"""Initialize Aladdin Connect entity."""
|
||||
super().__init__(coordinator)
|
||||
device = coordinator.data
|
||||
self._door_id = door_id
|
||||
door = self.door
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, device.unique_id)},
|
||||
identifiers={(DOMAIN, door.unique_id)},
|
||||
manufacturer="Aladdin Connect",
|
||||
name=device.name,
|
||||
name=door.name,
|
||||
)
|
||||
self._device_id = device.device_id
|
||||
self._number = device.door_number
|
||||
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]
|
||||
|
||||
@property
|
||||
def client(self) -> AladdinConnectClient:
|
||||
|
||||
@@ -57,7 +57,7 @@ rules:
|
||||
docs-supported-functions: done
|
||||
docs-troubleshooting: done
|
||||
docs-use-cases: done
|
||||
dynamic-devices: todo
|
||||
dynamic-devices: done
|
||||
entity-category: done
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default: done
|
||||
|
||||
@@ -14,7 +14,7 @@ from homeassistant.components.sensor import (
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import PERCENTAGE, EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import AladdinConnectConfigEntry, AladdinConnectCoordinator
|
||||
@@ -49,13 +49,24 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Aladdin Connect sensor devices."""
|
||||
coordinators = entry.runtime_data
|
||||
coordinator = entry.runtime_data
|
||||
known_devices: set[str] = set()
|
||||
|
||||
async_add_entities(
|
||||
AladdinConnectSensor(coordinator, description)
|
||||
for coordinator in coordinators.values()
|
||||
for description in SENSOR_TYPES
|
||||
)
|
||||
@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))
|
||||
|
||||
|
||||
class AladdinConnectSensor(AladdinConnectEntity, SensorEntity):
|
||||
@@ -66,14 +77,15 @@ class AladdinConnectSensor(AladdinConnectEntity, SensorEntity):
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AladdinConnectCoordinator,
|
||||
door_id: str,
|
||||
entity_description: AladdinConnectSensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the Aladdin Connect sensor."""
|
||||
super().__init__(coordinator)
|
||||
super().__init__(coordinator, door_id)
|
||||
self.entity_description = entity_description
|
||||
self._attr_unique_id = f"{coordinator.data.unique_id}-{entity_description.key}"
|
||||
self._attr_unique_id = f"{door_id}-{entity_description.key}"
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
"""Return the state of the sensor."""
|
||||
return self.entity_description.value_fn(self.coordinator.data)
|
||||
return self.entity_description.value_fn(self.door)
|
||||
|
||||
@@ -161,6 +161,7 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
|
||||
"schedule",
|
||||
"siren",
|
||||
"switch",
|
||||
"temperature",
|
||||
"text",
|
||||
"update",
|
||||
"vacuum",
|
||||
|
||||
@@ -9,9 +9,12 @@ 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
|
||||
|
||||
|
||||
@@ -23,10 +26,10 @@ async def async_setup_entry(
|
||||
"""Set up the Demo config entry."""
|
||||
async_add_entities(
|
||||
[
|
||||
DemoValve("Front Garden", ValveState.OPEN),
|
||||
DemoValve("Orchard", ValveState.CLOSED),
|
||||
DemoValve("Back Garden", ValveState.CLOSED, position=70),
|
||||
DemoValve("Trees", ValveState.CLOSED, position=30),
|
||||
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),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -34,17 +37,24 @@ 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_name = name
|
||||
self._attr_unique_id = unique_id
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, unique_id)},
|
||||
name=name,
|
||||
)
|
||||
if moveable:
|
||||
self._attr_supported_features = (
|
||||
ValveEntityFeature.OPEN | ValveEntityFeature.CLOSE
|
||||
|
||||
@@ -18,6 +18,8 @@ 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",
|
||||
@@ -185,6 +187,7 @@ 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",
|
||||
|
||||
@@ -1484,6 +1484,7 @@ 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)
|
||||
@@ -1586,6 +1587,10 @@ 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:
|
||||
|
||||
@@ -10,12 +10,13 @@ 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
|
||||
from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_NAME, CONF_UNIQUE_ID
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
@@ -27,7 +28,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 ReceiveMessage
|
||||
from .models import MqttCommandTemplate, ReceiveMessage
|
||||
from .schemas import MQTT_ENTITY_COMMON_SCHEMA
|
||||
from .util import valid_publish_topic
|
||||
|
||||
@@ -52,6 +53,9 @@ 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"
|
||||
@@ -137,8 +141,39 @@ MQTT_VACUUM_ATTRIBUTES_BLOCKED = frozenset(
|
||||
MQTT_VACUUM_DOCS_URL = "https://www.home-assistant.io/integrations/vacuum.mqtt/"
|
||||
|
||||
|
||||
PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend(
|
||||
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(
|
||||
{
|
||||
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]
|
||||
),
|
||||
@@ -164,7 +199,10 @@ PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend(
|
||||
}
|
||||
).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
|
||||
|
||||
DISCOVERY_SCHEMA = PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.ALLOW_EXTRA)
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -191,9 +229,11 @@ 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__(
|
||||
@@ -229,6 +269,23 @@ 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)
|
||||
@@ -246,6 +303,20 @@ 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)
|
||||
@@ -277,6 +348,19 @@ 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:
|
||||
|
||||
@@ -14,7 +14,7 @@ from .coordinator import PranaConfigEntry, PranaCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS = [Platform.FAN, Platform.SENSOR, Platform.SWITCH]
|
||||
PLATFORMS = [Platform.FAN, Platform.NUMBER, Platform.SENSOR, Platform.SWITCH]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: PranaConfigEntry) -> bool:
|
||||
|
||||
@@ -21,8 +21,8 @@ from homeassistant.util.percentage import (
|
||||
)
|
||||
from homeassistant.util.scaling import int_states_in_range
|
||||
|
||||
from . import PranaConfigEntry
|
||||
from .entity import PranaBaseEntity, PranaCoordinator
|
||||
from .coordinator import PranaConfigEntry, PranaCoordinator
|
||||
from .entity import PranaBaseEntity
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
@@ -8,6 +8,20 @@
|
||||
"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"
|
||||
|
||||
80
homeassistant/components/prana/number.py
Normal file
80
homeassistant/components/prana/number.py
Normal file
@@ -0,0 +1,80 @@
|
||||
"""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()
|
||||
@@ -21,8 +21,8 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import PranaConfigEntry
|
||||
from .entity import PranaBaseEntity, PranaCoordinator
|
||||
from .coordinator import PranaConfigEntry, PranaCoordinator
|
||||
from .entity import PranaBaseEntity
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
@@ -49,6 +49,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"display_brightness": {
|
||||
"name": "Display brightness"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"inside_temperature": {
|
||||
"name": "Inside temperature"
|
||||
|
||||
@@ -9,7 +9,7 @@ from homeassistant.components.switch import SwitchEntity, SwitchEntityDescriptio
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import PranaConfigEntry, PranaCoordinator
|
||||
from .coordinator import PranaConfigEntry, PranaCoordinator
|
||||
from .entity import PranaBaseEntity
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
@@ -4,10 +4,11 @@ from __future__ import annotations
|
||||
|
||||
import mimetypes
|
||||
|
||||
from aiodns.error import DNSError
|
||||
import pycountry
|
||||
from radios import FilterBy, Order, RadioBrowser, Station
|
||||
from radios import FilterBy, Order, RadioBrowser, RadioBrowserError, Station
|
||||
|
||||
from homeassistant.components.media_player import MediaClass, MediaType
|
||||
from homeassistant.components.media_player import BrowseError, MediaClass, MediaType
|
||||
from homeassistant.components.media_source import (
|
||||
BrowseMediaSource,
|
||||
MediaSource,
|
||||
@@ -15,6 +16,7 @@ from homeassistant.components.media_source import (
|
||||
PlayMedia,
|
||||
Unresolvable,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.util.location import vincenty
|
||||
|
||||
@@ -55,9 +57,20 @@ class RadioMediaSource(MediaSource):
|
||||
|
||||
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia:
|
||||
"""Resolve selected Radio station to a streaming URL."""
|
||||
radios = self.radios
|
||||
|
||||
station = await radios.station(uuid=item.identifier)
|
||||
if self.entry.state != ConfigEntryState.LOADED:
|
||||
raise Unresolvable(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="config_entry_not_ready",
|
||||
)
|
||||
radios = self.radios
|
||||
try:
|
||||
station = await radios.station(uuid=item.identifier)
|
||||
except (DNSError, RadioBrowserError) as e:
|
||||
raise Unresolvable(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="radio_browser_error",
|
||||
) from e
|
||||
if not station:
|
||||
raise Unresolvable("Radio station is no longer available")
|
||||
|
||||
@@ -74,25 +87,37 @@ class RadioMediaSource(MediaSource):
|
||||
item: MediaSourceItem,
|
||||
) -> BrowseMediaSource:
|
||||
"""Return media."""
|
||||
|
||||
if self.entry.state != ConfigEntryState.LOADED:
|
||||
raise BrowseError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="config_entry_not_ready",
|
||||
)
|
||||
radios = self.radios
|
||||
|
||||
return BrowseMediaSource(
|
||||
domain=DOMAIN,
|
||||
identifier=None,
|
||||
media_class=MediaClass.CHANNEL,
|
||||
media_content_type=MediaType.MUSIC,
|
||||
title=self.entry.title,
|
||||
can_play=False,
|
||||
can_expand=True,
|
||||
children_media_class=MediaClass.DIRECTORY,
|
||||
children=[
|
||||
*await self._async_build_popular(radios, item),
|
||||
*await self._async_build_by_tag(radios, item),
|
||||
*await self._async_build_by_language(radios, item),
|
||||
*await self._async_build_local(radios, item),
|
||||
*await self._async_build_by_country(radios, item),
|
||||
],
|
||||
)
|
||||
try:
|
||||
return BrowseMediaSource(
|
||||
domain=DOMAIN,
|
||||
identifier=None,
|
||||
media_class=MediaClass.CHANNEL,
|
||||
media_content_type=MediaType.MUSIC,
|
||||
title=self.entry.title,
|
||||
can_play=False,
|
||||
can_expand=True,
|
||||
children_media_class=MediaClass.DIRECTORY,
|
||||
children=[
|
||||
*await self._async_build_popular(radios, item),
|
||||
*await self._async_build_by_tag(radios, item),
|
||||
*await self._async_build_by_language(radios, item),
|
||||
*await self._async_build_local(radios, item),
|
||||
*await self._async_build_by_country(radios, item),
|
||||
],
|
||||
)
|
||||
except (DNSError, RadioBrowserError) as e:
|
||||
raise BrowseError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="radio_browser_error",
|
||||
) from e
|
||||
|
||||
@callback
|
||||
@staticmethod
|
||||
|
||||
@@ -5,5 +5,13 @@
|
||||
"description": "Do you want to add Radio Browser to Home Assistant?"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"config_entry_not_ready": {
|
||||
"message": "Radio Browser integration is not ready"
|
||||
},
|
||||
"radio_browser_error": {
|
||||
"message": "Error occurred while communicating with Radio Browser"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"loggers": ["roborock"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": [
|
||||
"python-roborock==4.20.0",
|
||||
"python-roborock==4.25.0",
|
||||
"vacuum-map-parser-roborock==0.1.4"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, datetime
|
||||
from datetime import 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.dt import utcnow
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import DOMAIN, TYPE_ASTRONOMICAL
|
||||
|
||||
@@ -50,7 +50,7 @@ async def async_setup_entry(
|
||||
|
||||
|
||||
def get_season(
|
||||
current_date: date, hemisphere: str, season_tracking_type: str
|
||||
current_datetime: datetime, hemisphere: str, season_tracking_type: str
|
||||
) -> str | None:
|
||||
"""Calculate the current season."""
|
||||
|
||||
@@ -58,22 +58,36 @@ def get_season(
|
||||
return None
|
||||
|
||||
if season_tracking_type == TYPE_ASTRONOMICAL:
|
||||
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()
|
||||
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)
|
||||
)
|
||||
else:
|
||||
spring_start = datetime(2017, 3, 1).replace(year=current_date.year)
|
||||
spring_start = current_datetime.replace(
|
||||
month=3, day=1, hour=0, minute=0, second=0, microsecond=0
|
||||
)
|
||||
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_date < summer_start:
|
||||
if spring_start <= current_datetime < summer_start:
|
||||
season = STATE_SPRING
|
||||
elif summer_start <= current_date < autumn_start:
|
||||
elif summer_start <= current_datetime < autumn_start:
|
||||
season = STATE_SUMMER
|
||||
elif autumn_start <= current_date < winter_start:
|
||||
elif autumn_start <= current_datetime < winter_start:
|
||||
season = STATE_AUTUMN
|
||||
|
||||
# If user is located in the southern hemisphere swap the season
|
||||
@@ -104,6 +118,4 @@ class SeasonSensorEntity(SensorEntity):
|
||||
|
||||
def update(self) -> None:
|
||||
"""Update season."""
|
||||
self._attr_native_value = get_season(
|
||||
utcnow().replace(tzinfo=None), self.hemisphere, self.type
|
||||
)
|
||||
self._attr_native_value = get_season(dt_util.now(), self.hemisphere, self.type)
|
||||
|
||||
17
homeassistant/components/temperature/__init__.py
Normal file
17
homeassistant/components/temperature/__init__.py
Normal file
@@ -0,0 +1,17 @@
|
||||
"""Integration for temperature triggers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
DOMAIN = "temperature"
|
||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||
|
||||
__all__ = []
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the component."""
|
||||
return True
|
||||
10
homeassistant/components/temperature/icons.json
Normal file
10
homeassistant/components/temperature/icons.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"triggers": {
|
||||
"changed": {
|
||||
"trigger": "mdi:thermometer"
|
||||
},
|
||||
"crossed_threshold": {
|
||||
"trigger": "mdi:thermometer"
|
||||
}
|
||||
}
|
||||
}
|
||||
8
homeassistant/components/temperature/manifest.json
Normal file
8
homeassistant/components/temperature/manifest.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"domain": "temperature",
|
||||
"name": "Temperature",
|
||||
"codeowners": ["@home-assistant/core"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/temperature",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal"
|
||||
}
|
||||
82
homeassistant/components/temperature/strings.json
Normal file
82
homeassistant/components/temperature/strings.json
Normal file
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"common": {
|
||||
"trigger_behavior_description": "The behavior of the targeted entities to trigger on.",
|
||||
"trigger_behavior_name": "Behavior"
|
||||
},
|
||||
"selector": {
|
||||
"number_or_entity": {
|
||||
"choices": {
|
||||
"entity": "Entity",
|
||||
"number": "Number"
|
||||
}
|
||||
},
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "Any",
|
||||
"first": "First",
|
||||
"last": "Last"
|
||||
}
|
||||
},
|
||||
"trigger_temperature_unit": {
|
||||
"options": {
|
||||
"celsius": "Celsius (°C)",
|
||||
"fahrenheit": "Fahrenheit (°F)"
|
||||
}
|
||||
},
|
||||
"trigger_threshold_type": {
|
||||
"options": {
|
||||
"above": "Above",
|
||||
"below": "Below",
|
||||
"between": "Between",
|
||||
"outside": "Outside"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Temperature",
|
||||
"triggers": {
|
||||
"changed": {
|
||||
"description": "Triggers when the temperature changes.",
|
||||
"fields": {
|
||||
"above": {
|
||||
"description": "Only trigger when temperature is above this value.",
|
||||
"name": "Above"
|
||||
},
|
||||
"below": {
|
||||
"description": "Only trigger when temperature is below this value.",
|
||||
"name": "Below"
|
||||
},
|
||||
"unit": {
|
||||
"description": "The unit of temperature for the trigger values. Defaults to the system unit.",
|
||||
"name": "Unit"
|
||||
}
|
||||
},
|
||||
"name": "Temperature changed"
|
||||
},
|
||||
"crossed_threshold": {
|
||||
"description": "Triggers when the temperature crosses a threshold.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::temperature::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::temperature::common::trigger_behavior_name%]"
|
||||
},
|
||||
"lower_limit": {
|
||||
"description": "The lower limit of the threshold.",
|
||||
"name": "Lower limit"
|
||||
},
|
||||
"threshold_type": {
|
||||
"description": "The type of threshold to use.",
|
||||
"name": "Threshold type"
|
||||
},
|
||||
"unit": {
|
||||
"description": "[%key:component::temperature::triggers::changed::fields::unit::description%]",
|
||||
"name": "[%key:component::temperature::triggers::changed::fields::unit::name%]"
|
||||
},
|
||||
"upper_limit": {
|
||||
"description": "The upper limit of the threshold.",
|
||||
"name": "Upper limit"
|
||||
}
|
||||
},
|
||||
"name": "Temperature crossed threshold"
|
||||
}
|
||||
}
|
||||
}
|
||||
185
homeassistant/components/temperature/trigger.py
Normal file
185
homeassistant/components/temperature/trigger.py
Normal file
@@ -0,0 +1,185 @@
|
||||
"""Provides triggers for temperature."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_CURRENT_TEMPERATURE as CLIMATE_ATTR_CURRENT_TEMPERATURE,
|
||||
DOMAIN as CLIMATE_DOMAIN,
|
||||
)
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass
|
||||
from homeassistant.components.water_heater import (
|
||||
ATTR_CURRENT_TEMPERATURE as WATER_HEATER_ATTR_CURRENT_TEMPERATURE,
|
||||
DOMAIN as WATER_HEATER_DOMAIN,
|
||||
)
|
||||
from homeassistant.components.weather import (
|
||||
ATTR_WEATHER_TEMPERATURE,
|
||||
ATTR_WEATHER_TEMPERATURE_UNIT,
|
||||
DOMAIN as WEATHER_DOMAIN,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_UNIT_OF_MEASUREMENT,
|
||||
CONF_ABOVE,
|
||||
CONF_BELOW,
|
||||
CONF_OPTIONS,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, State, split_entity_id
|
||||
from homeassistant.helpers.automation import NumericalDomainSpec
|
||||
from homeassistant.helpers.trigger import (
|
||||
ATTR_BEHAVIOR,
|
||||
BEHAVIOR_ANY,
|
||||
BEHAVIOR_FIRST,
|
||||
BEHAVIOR_LAST,
|
||||
CONF_LOWER_LIMIT,
|
||||
CONF_THRESHOLD_TYPE,
|
||||
CONF_UPPER_LIMIT,
|
||||
ENTITY_STATE_TRIGGER_SCHEMA,
|
||||
EntityNumericalStateChangedTriggerBase,
|
||||
EntityNumericalStateCrossedThresholdTriggerBase,
|
||||
EntityNumericalStateTriggerBase,
|
||||
ThresholdType,
|
||||
Trigger,
|
||||
TriggerConfig,
|
||||
_number_or_entity,
|
||||
_validate_limits_for_threshold_type,
|
||||
_validate_range,
|
||||
)
|
||||
from homeassistant.util.unit_conversion import TemperatureConverter
|
||||
|
||||
CONF_UNIT = "unit"
|
||||
|
||||
_UNIT_MAP = {
|
||||
"celsius": UnitOfTemperature.CELSIUS,
|
||||
"fahrenheit": UnitOfTemperature.FAHRENHEIT,
|
||||
}
|
||||
|
||||
|
||||
def _validate_temperature_unit(value: str) -> str:
|
||||
"""Convert temperature unit option to UnitOfTemperature."""
|
||||
if value in _UNIT_MAP:
|
||||
return _UNIT_MAP[value]
|
||||
raise vol.Invalid(f"Unknown temperature unit: {value}")
|
||||
|
||||
|
||||
_UNIT_VALIDATOR = _validate_temperature_unit
|
||||
|
||||
TEMPERATURE_CHANGED_TRIGGER_SCHEMA = ENTITY_STATE_TRIGGER_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_OPTIONS): vol.All(
|
||||
{
|
||||
vol.Optional(CONF_ABOVE): _number_or_entity,
|
||||
vol.Optional(CONF_BELOW): _number_or_entity,
|
||||
vol.Optional(CONF_UNIT): _UNIT_VALIDATOR,
|
||||
},
|
||||
_validate_range(CONF_ABOVE, CONF_BELOW),
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
TEMPERATURE_CROSSED_THRESHOLD_TRIGGER_SCHEMA = ENTITY_STATE_TRIGGER_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_OPTIONS): vol.All(
|
||||
{
|
||||
vol.Required(ATTR_BEHAVIOR, default=BEHAVIOR_ANY): vol.In(
|
||||
[BEHAVIOR_FIRST, BEHAVIOR_LAST, BEHAVIOR_ANY]
|
||||
),
|
||||
vol.Optional(CONF_LOWER_LIMIT): _number_or_entity,
|
||||
vol.Optional(CONF_UPPER_LIMIT): _number_or_entity,
|
||||
vol.Required(CONF_THRESHOLD_TYPE): vol.Coerce(ThresholdType),
|
||||
vol.Optional(CONF_UNIT): _UNIT_VALIDATOR,
|
||||
},
|
||||
_validate_range(CONF_LOWER_LIMIT, CONF_UPPER_LIMIT),
|
||||
_validate_limits_for_threshold_type,
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
TEMPERATURE_DOMAIN_SPECS = {
|
||||
CLIMATE_DOMAIN: NumericalDomainSpec(
|
||||
value_source=CLIMATE_ATTR_CURRENT_TEMPERATURE,
|
||||
),
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
),
|
||||
WATER_HEATER_DOMAIN: NumericalDomainSpec(
|
||||
value_source=WATER_HEATER_ATTR_CURRENT_TEMPERATURE
|
||||
),
|
||||
WEATHER_DOMAIN: NumericalDomainSpec(
|
||||
value_source=ATTR_WEATHER_TEMPERATURE,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class _TemperatureTriggerMixin(EntityNumericalStateTriggerBase):
|
||||
"""Mixin for temperature triggers providing entity filtering, value extraction, and unit conversion."""
|
||||
|
||||
_domain_specs = TEMPERATURE_DOMAIN_SPECS
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize the trigger."""
|
||||
super().__init__(hass, config)
|
||||
self._trigger_unit: str = self._options.get(
|
||||
CONF_UNIT, hass.config.units.temperature_unit
|
||||
)
|
||||
|
||||
def _get_entity_unit(self, state: State) -> str | None:
|
||||
"""Get the temperature unit of an entity from its state."""
|
||||
domain = split_entity_id(state.entity_id)[0]
|
||||
if domain == SENSOR_DOMAIN:
|
||||
return state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||
if domain == WEATHER_DOMAIN:
|
||||
return state.attributes.get(
|
||||
ATTR_WEATHER_TEMPERATURE_UNIT,
|
||||
self._hass.config.units.temperature_unit,
|
||||
)
|
||||
# Climate and water_heater: show_temp converts to system unit
|
||||
return self._hass.config.units.temperature_unit
|
||||
|
||||
def _get_tracked_value(self, state: State) -> Any:
|
||||
"""Get the temperature value converted to the trigger's configured unit."""
|
||||
raw_value = super()._get_tracked_value(state)
|
||||
if raw_value is None:
|
||||
return None
|
||||
|
||||
entity_unit = self._get_entity_unit(state)
|
||||
if entity_unit is None or entity_unit == self._trigger_unit:
|
||||
return raw_value
|
||||
|
||||
try:
|
||||
return TemperatureConverter.convert(
|
||||
float(raw_value), entity_unit, self._trigger_unit
|
||||
)
|
||||
except TypeError, ValueError:
|
||||
return raw_value # Let the base class converter handle the error
|
||||
|
||||
|
||||
class TemperatureChangedTrigger(
|
||||
_TemperatureTriggerMixin, EntityNumericalStateChangedTriggerBase
|
||||
):
|
||||
"""Trigger for temperature value changes across multiple domains."""
|
||||
|
||||
_schema = TEMPERATURE_CHANGED_TRIGGER_SCHEMA
|
||||
|
||||
|
||||
class TemperatureCrossedThresholdTrigger(
|
||||
_TemperatureTriggerMixin, EntityNumericalStateCrossedThresholdTriggerBase
|
||||
):
|
||||
"""Trigger for temperature value crossing a threshold across multiple domains."""
|
||||
|
||||
_schema = TEMPERATURE_CROSSED_THRESHOLD_TRIGGER_SCHEMA
|
||||
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"changed": TemperatureChangedTrigger,
|
||||
"crossed_threshold": TemperatureCrossedThresholdTrigger,
|
||||
}
|
||||
|
||||
|
||||
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
|
||||
"""Return the triggers for temperature."""
|
||||
return TRIGGERS
|
||||
75
homeassistant/components/temperature/triggers.yaml
Normal file
75
homeassistant/components/temperature/triggers.yaml
Normal file
@@ -0,0 +1,75 @@
|
||||
.trigger_common_fields:
|
||||
behavior: &trigger_behavior
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: trigger_behavior
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
|
||||
.number_or_entity: &number_or_entity
|
||||
required: false
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
domain:
|
||||
- input_number
|
||||
- number
|
||||
- sensor
|
||||
translation_key: number_or_entity
|
||||
|
||||
.trigger_threshold_type: &trigger_threshold_type
|
||||
required: true
|
||||
default: above
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- above
|
||||
- below
|
||||
- between
|
||||
- outside
|
||||
translation_key: trigger_threshold_type
|
||||
|
||||
.trigger_unit: &trigger_unit
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- celsius
|
||||
- fahrenheit
|
||||
translation_key: trigger_temperature_unit
|
||||
|
||||
.trigger_target: &trigger_target
|
||||
entity:
|
||||
- domain: sensor
|
||||
device_class: temperature
|
||||
- domain: climate
|
||||
- domain: water_heater
|
||||
- domain: weather
|
||||
|
||||
changed:
|
||||
target: *trigger_target
|
||||
fields:
|
||||
above: *number_or_entity
|
||||
below: *number_or_entity
|
||||
unit: *trigger_unit
|
||||
|
||||
crossed_threshold:
|
||||
target: *trigger_target
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
threshold_type: *trigger_threshold_type
|
||||
lower_limit: *number_or_entity
|
||||
upper_limit: *number_or_entity
|
||||
unit: *trigger_unit
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Iterable
|
||||
import copy
|
||||
import dataclasses
|
||||
import datetime
|
||||
import logging
|
||||
@@ -28,7 +29,6 @@ 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[JsonValueType] | None], None]] | None = None
|
||||
_update_listeners: list[Callable[[list[TodoItem]], None]] | None = None
|
||||
|
||||
@property
|
||||
def state(self) -> int | None:
|
||||
@@ -281,13 +281,9 @@ class TodoListEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
@final
|
||||
@callback
|
||||
def async_subscribe_updates(
|
||||
self,
|
||||
listener: Callable[[list[JsonValueType] | None], None],
|
||||
self, listener: Callable[[list[TodoItem]], None]
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Subscribe to To-do list item updates.
|
||||
|
||||
Called by websocket API.
|
||||
"""
|
||||
"""Subscribe to To-do list item updates."""
|
||||
if self._update_listeners is None:
|
||||
self._update_listeners = []
|
||||
self._update_listeners.append(listener)
|
||||
@@ -306,9 +302,7 @@ class TodoListEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
if not self._update_listeners:
|
||||
return
|
||||
|
||||
todo_items: list[JsonValueType] = [
|
||||
dataclasses.asdict(item) for item in self.todo_items or ()
|
||||
]
|
||||
todo_items = [copy.copy(item) for item in self.todo_items or []]
|
||||
for listener in self._update_listeners:
|
||||
listener(todo_items)
|
||||
|
||||
@@ -341,13 +335,13 @@ async def websocket_handle_subscribe_todo_items(
|
||||
return
|
||||
|
||||
@callback
|
||||
def todo_item_listener(todo_items: list[JsonValueType] | None) -> None:
|
||||
def todo_item_listener(todo_items: list[TodoItem]) -> None:
|
||||
"""Push updated To-do list items to websocket."""
|
||||
connection.send_message(
|
||||
websocket_api.event_message(
|
||||
msg["id"],
|
||||
{
|
||||
"items": todo_items,
|
||||
"items": [dataclasses.asdict(item) for item in todo_items],
|
||||
},
|
||||
)
|
||||
)
|
||||
@@ -357,7 +351,7 @@ async def websocket_handle_subscribe_todo_items(
|
||||
)
|
||||
connection.send_result(msg["id"])
|
||||
|
||||
# Push an initial forecast update
|
||||
# Push an initial list update
|
||||
entity.async_update_listeners()
|
||||
|
||||
|
||||
|
||||
2
requirements_all.txt
generated
2
requirements_all.txt
generated
@@ -2651,7 +2651,7 @@ python-rabbitair==0.0.8
|
||||
python-ripple-api==0.0.3
|
||||
|
||||
# homeassistant.components.roborock
|
||||
python-roborock==4.20.0
|
||||
python-roborock==4.25.0
|
||||
|
||||
# homeassistant.components.smarttub
|
||||
python-smarttub==0.0.47
|
||||
|
||||
2
requirements_test_all.txt
generated
2
requirements_test_all.txt
generated
@@ -2247,7 +2247,7 @@ python-pooldose==0.8.6
|
||||
python-rabbitair==0.0.8
|
||||
|
||||
# homeassistant.components.roborock
|
||||
python-roborock==4.20.0
|
||||
python-roborock==4.25.0
|
||||
|
||||
# homeassistant.components.smarttub
|
||||
python-smarttub==0.0.47
|
||||
|
||||
@@ -120,6 +120,7 @@ NO_IOT_CLASS = [
|
||||
"system_health",
|
||||
"system_log",
|
||||
"tag",
|
||||
"temperature",
|
||||
"timer",
|
||||
"trace",
|
||||
"web_rtc",
|
||||
|
||||
@@ -2149,6 +2149,7 @@ NO_QUALITY_SCALE = [
|
||||
"system_health",
|
||||
"system_log",
|
||||
"tag",
|
||||
"temperature",
|
||||
"timer",
|
||||
"trace",
|
||||
"usage_prediction",
|
||||
|
||||
@@ -125,7 +125,7 @@ async def test_cover_unavailable(
|
||||
assert state is not None
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
mock_aladdin_connect_api.update_door.side_effect = aiohttp.ClientError()
|
||||
mock_aladdin_connect_api.get_doors.side_effect = aiohttp.ClientError()
|
||||
freezer.tick(15)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@@ -5,16 +5,17 @@ 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
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
|
||||
from . import init_integration
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
|
||||
async def test_setup_entry(
|
||||
@@ -137,3 +138,49 @@ 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
|
||||
|
||||
@@ -49,7 +49,7 @@ async def test_sensor_unavailable(
|
||||
assert state is not None
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
mock_aladdin_connect_api.update_door.side_effect = aiohttp.ClientError()
|
||||
mock_aladdin_connect_api.get_doors.side_effect = aiohttp.ClientError()
|
||||
freezer.tick(15)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -129,32 +129,17 @@ 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."""
|
||||
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(
|
||||
await assert_condition_behavior_any(
|
||||
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,
|
||||
target=condition_target_config,
|
||||
behavior="any",
|
||||
condition_options=condition_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
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(
|
||||
@@ -236,29 +221,13 @@ 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."""
|
||||
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(
|
||||
await assert_condition_behavior_all(
|
||||
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,
|
||||
target=condition_target_config,
|
||||
behavior="all",
|
||||
condition_options=condition_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
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"]
|
||||
|
||||
@@ -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,32 +83,17 @@ async def test_assist_satellite_state_condition_behavior_any(
|
||||
states: list[ConditionStateDescription],
|
||||
) -> None:
|
||||
"""Test the assist satellite state condition with the 'any' behavior."""
|
||||
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(
|
||||
await assert_condition_behavior_any(
|
||||
hass,
|
||||
target_entities=target_assist_satellites,
|
||||
condition_target_config=condition_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
condition=condition,
|
||||
target=condition_target_config,
|
||||
behavior="any",
|
||||
condition_options=condition_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
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(
|
||||
@@ -151,29 +136,13 @@ async def test_assist_satellite_state_condition_behavior_all(
|
||||
states: list[ConditionStateDescription],
|
||||
) -> None:
|
||||
"""Test the assist satellite state condition with the 'all' behavior."""
|
||||
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(
|
||||
await assert_condition_behavior_all(
|
||||
hass,
|
||||
target_entities=target_assist_satellites,
|
||||
condition_target_config=condition_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
condition=condition,
|
||||
target=condition_target_config,
|
||||
behavior="all",
|
||||
condition_options=condition_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
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"]
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
'fide': None,
|
||||
'followers': 2,
|
||||
'is_streamer': False,
|
||||
'joined': '2026-02-20T11:48:14',
|
||||
'last_online': '2026-03-06T13:32:59',
|
||||
'joined': '2026-02-20T10:48:14',
|
||||
'last_online': '2026-03-06T12:32:59',
|
||||
'location': 'Utrecht',
|
||||
'name': 'Joost',
|
||||
'player_id': 532748851,
|
||||
|
||||
@@ -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,32 +85,17 @@ async def test_climate_state_condition_behavior_any(
|
||||
states: list[ConditionStateDescription],
|
||||
) -> None:
|
||||
"""Test the climate state condition with the 'any' behavior."""
|
||||
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(
|
||||
await assert_condition_behavior_any(
|
||||
hass,
|
||||
target_entities=target_climates,
|
||||
condition_target_config=condition_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
condition=condition,
|
||||
target=condition_target_config,
|
||||
behavior="any",
|
||||
condition_options=condition_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
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(
|
||||
@@ -150,33 +135,17 @@ async def test_climate_state_condition_behavior_all(
|
||||
states: list[ConditionStateDescription],
|
||||
) -> None:
|
||||
"""Test the climate state condition with the 'all' behavior."""
|
||||
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(
|
||||
await assert_condition_behavior_all(
|
||||
hass,
|
||||
target_entities=target_climates,
|
||||
condition_target_config=condition_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
condition=condition,
|
||||
target=condition_target_config,
|
||||
behavior="all",
|
||||
condition_options=condition_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
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(
|
||||
@@ -214,32 +183,17 @@ async def test_climate_attribute_condition_behavior_any(
|
||||
states: list[ConditionStateDescription],
|
||||
) -> None:
|
||||
"""Test the climate attribute condition with the 'any' behavior."""
|
||||
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(
|
||||
await assert_condition_behavior_any(
|
||||
hass,
|
||||
target_entities=target_climates,
|
||||
condition_target_config=condition_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
condition=condition,
|
||||
target=condition_target_config,
|
||||
behavior="any",
|
||||
condition_options=condition_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
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(
|
||||
@@ -277,29 +231,13 @@ async def test_climate_attribute_condition_behavior_all(
|
||||
states: list[ConditionStateDescription],
|
||||
) -> None:
|
||||
"""Test the climate attribute condition with the 'all' behavior."""
|
||||
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(
|
||||
await assert_condition_behavior_all(
|
||||
hass,
|
||||
target_entities=target_climates,
|
||||
condition_target_config=condition_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
condition=condition,
|
||||
target=condition_target_config,
|
||||
behavior="all",
|
||||
condition_options=condition_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
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"]
|
||||
|
||||
@@ -864,6 +864,98 @@ 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"]) - {entity_id}
|
||||
excluded_entity_ids = set(target_entities["excluded"]) - {entity_id}
|
||||
|
||||
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"])
|
||||
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"]
|
||||
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"]
|
||||
|
||||
|
||||
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"]) - {entity_id}
|
||||
excluded_entity_ids = set(target_entities["excluded"]) - {entity_id}
|
||||
|
||||
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"])
|
||||
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"]
|
||||
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"]
|
||||
|
||||
|
||||
async def assert_trigger_behavior_any(
|
||||
hass: HomeAssistant,
|
||||
*,
|
||||
|
||||
@@ -10,12 +10,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,
|
||||
parametrize_condition_states_all,
|
||||
parametrize_condition_states_any,
|
||||
parametrize_target_entities,
|
||||
set_or_remove_state,
|
||||
target_entities,
|
||||
)
|
||||
|
||||
@@ -100,38 +101,17 @@ async def test_cover_condition_behavior_any(
|
||||
states: list[ConditionStateDescription],
|
||||
) -> None:
|
||||
"""Test cover condition with the 'any' behavior."""
|
||||
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(
|
||||
await assert_condition_behavior_any(
|
||||
hass,
|
||||
target_entities=target_covers,
|
||||
condition_target_config=condition_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
condition=condition,
|
||||
target=condition_target_config,
|
||||
behavior="any",
|
||||
condition_options=condition_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
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(
|
||||
@@ -184,40 +164,17 @@ async def test_cover_condition_behavior_all(
|
||||
states: list[ConditionStateDescription],
|
||||
) -> None:
|
||||
"""Test cover condition with the 'all' behavior."""
|
||||
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(
|
||||
await assert_condition_behavior_all(
|
||||
hass,
|
||||
target_entities=target_covers,
|
||||
condition_target_config=condition_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
condition=condition,
|
||||
target=condition_target_config,
|
||||
behavior="all",
|
||||
condition_options=condition_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
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(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""The tests for the Demo valve platform."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from datetime import timedelta
|
||||
from unittest.mock import patch
|
||||
|
||||
@@ -17,10 +18,9 @@ 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 async_capture_events, async_fire_time_changed
|
||||
from tests.common import MockConfigEntry, 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
|
||||
async def valve_only() -> None:
|
||||
def valve_only() -> Generator[None]:
|
||||
"""Enable only the valve platform."""
|
||||
with patch(
|
||||
"homeassistant.components.demo.COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM",
|
||||
@@ -38,11 +38,12 @@ async def valve_only() -> None:
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
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}}
|
||||
)
|
||||
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)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
@@ -50,6 +51,7 @@ async def setup_comp(hass: HomeAssistant, valve_only: 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()
|
||||
|
||||
@@ -63,9 +65,11 @@ 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
|
||||
|
||||
|
||||
@@ -73,6 +77,7 @@ 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()
|
||||
|
||||
@@ -83,15 +88,18 @@ 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%
|
||||
@@ -102,6 +110,7 @@ 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):
|
||||
@@ -110,6 +119,7 @@ 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
|
||||
|
||||
@@ -121,6 +131,7 @@ 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):
|
||||
@@ -129,6 +140,7 @@ 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
|
||||
|
||||
|
||||
@@ -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,32 +70,17 @@ async def test_device_tracker_state_condition_behavior_any(
|
||||
states: list[ConditionStateDescription],
|
||||
) -> None:
|
||||
"""Test the device tracker state condition with the 'any' behavior."""
|
||||
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(
|
||||
await assert_condition_behavior_any(
|
||||
hass,
|
||||
target_entities=target_device_trackers,
|
||||
condition_target_config=condition_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
condition=condition,
|
||||
target=condition_target_config,
|
||||
behavior="any",
|
||||
condition_options=condition_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
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(
|
||||
@@ -128,29 +113,13 @@ async def test_device_tracker_state_condition_behavior_all(
|
||||
states: list[ConditionStateDescription],
|
||||
) -> None:
|
||||
"""Test the device tracker state condition with the 'all' behavior."""
|
||||
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(
|
||||
await assert_condition_behavior_all(
|
||||
hass,
|
||||
target_entities=target_device_trackers,
|
||||
condition_target_config=condition_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
condition=condition,
|
||||
target=condition_target_config,
|
||||
behavior="all",
|
||||
condition_options=condition_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
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"]
|
||||
|
||||
@@ -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,32 +73,17 @@ async def test_humidifier_state_condition_behavior_any(
|
||||
states: list[ConditionStateDescription],
|
||||
) -> None:
|
||||
"""Test the humidifier state condition with the 'any' behavior."""
|
||||
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(
|
||||
await assert_condition_behavior_any(
|
||||
hass,
|
||||
target_entities=target_humidifiers,
|
||||
condition_target_config=condition_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
condition=condition,
|
||||
target=condition_target_config,
|
||||
behavior="any",
|
||||
condition_options=condition_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
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(
|
||||
@@ -131,33 +116,17 @@ async def test_humidifier_state_condition_behavior_all(
|
||||
states: list[ConditionStateDescription],
|
||||
) -> None:
|
||||
"""Test the humidifier state condition with the 'all' behavior."""
|
||||
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(
|
||||
await assert_condition_behavior_all(
|
||||
hass,
|
||||
target_entities=target_humidifiers,
|
||||
condition_target_config=condition_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
condition=condition,
|
||||
target=condition_target_config,
|
||||
behavior="all",
|
||||
condition_options=condition_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
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(
|
||||
@@ -190,32 +159,17 @@ async def test_humidifier_attribute_condition_behavior_any(
|
||||
states: list[ConditionStateDescription],
|
||||
) -> None:
|
||||
"""Test the humidifier attribute condition with the 'any' behavior."""
|
||||
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(
|
||||
await assert_condition_behavior_any(
|
||||
hass,
|
||||
target_entities=target_humidifiers,
|
||||
condition_target_config=condition_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
condition=condition,
|
||||
target=condition_target_config,
|
||||
behavior="any",
|
||||
condition_options=condition_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
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(
|
||||
@@ -248,29 +202,13 @@ async def test_humidifier_attribute_condition_behavior_all(
|
||||
states: list[ConditionStateDescription],
|
||||
) -> None:
|
||||
"""Test the humidifier attribute condition with the 'all' behavior."""
|
||||
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(
|
||||
await assert_condition_behavior_all(
|
||||
hass,
|
||||
target_entities=target_humidifiers,
|
||||
condition_target_config=condition_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
condition=condition,
|
||||
target=condition_target_config,
|
||||
behavior="all",
|
||||
condition_options=condition_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
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"]
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
# serializer version: 1
|
||||
# name: test_button[1][button.bk1600_enable_standby_mode-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
@@ -50,8 +51,9 @@
|
||||
# ---
|
||||
# name: test_button[2][button.cms_sf2000_enable_standby_mode-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
|
||||
@@ -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,32 +89,17 @@ async def test_lawn_mower_state_condition_behavior_any(
|
||||
states: list[ConditionStateDescription],
|
||||
) -> None:
|
||||
"""Test the lawn mower state condition with the 'any' behavior."""
|
||||
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(
|
||||
await assert_condition_behavior_any(
|
||||
hass,
|
||||
target_entities=target_lawn_mowers,
|
||||
condition_target_config=condition_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
condition=condition,
|
||||
target=condition_target_config,
|
||||
behavior="any",
|
||||
condition_options=condition_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
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(
|
||||
@@ -162,29 +147,13 @@ async def test_lawn_mower_state_condition_behavior_all(
|
||||
states: list[ConditionStateDescription],
|
||||
) -> None:
|
||||
"""Test the lawn mower state condition with the 'all' behavior."""
|
||||
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(
|
||||
await assert_condition_behavior_all(
|
||||
hass,
|
||||
target_entities=target_lawn_mowers,
|
||||
condition_target_config=condition_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
condition=condition,
|
||||
target=condition_target_config,
|
||||
behavior="all",
|
||||
condition_options=condition_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
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"]
|
||||
|
||||
@@ -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,32 +83,17 @@ async def test_lock_state_condition_behavior_any(
|
||||
states: list[ConditionStateDescription],
|
||||
) -> None:
|
||||
"""Test the lock state condition with the 'any' behavior."""
|
||||
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(
|
||||
await assert_condition_behavior_any(
|
||||
hass,
|
||||
target_entities=target_locks,
|
||||
condition_target_config=condition_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
condition=condition,
|
||||
target=condition_target_config,
|
||||
behavior="any",
|
||||
condition_options=condition_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
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(
|
||||
@@ -151,29 +136,13 @@ async def test_lock_state_condition_behavior_all(
|
||||
states: list[ConditionStateDescription],
|
||||
) -> None:
|
||||
"""Test the lock state condition with the 'all' behavior."""
|
||||
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(
|
||||
await assert_condition_behavior_all(
|
||||
hass,
|
||||
target_entities=target_locks,
|
||||
condition_target_config=condition_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
condition=condition,
|
||||
target=condition_target_config,
|
||||
behavior="all",
|
||||
condition_options=condition_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
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"]
|
||||
|
||||
@@ -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,32 +101,17 @@ async def test_media_player_state_condition_behavior_any(
|
||||
states: list[ConditionStateDescription],
|
||||
) -> None:
|
||||
"""Test the media player state condition with the 'any' behavior."""
|
||||
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(
|
||||
await assert_condition_behavior_any(
|
||||
hass,
|
||||
target_entities=target_media_players,
|
||||
condition_target_config=condition_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
condition=condition,
|
||||
target=condition_target_config,
|
||||
behavior="any",
|
||||
condition_options=condition_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
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(
|
||||
@@ -186,29 +171,13 @@ async def test_media_player_state_condition_behavior_all(
|
||||
states: list[ConditionStateDescription],
|
||||
) -> None:
|
||||
"""Test the media player state condition with the 'all' behavior."""
|
||||
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(
|
||||
await assert_condition_behavior_all(
|
||||
hass,
|
||||
target_entities=target_media_players,
|
||||
condition_target_config=condition_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
condition=condition,
|
||||
target=condition_target_config,
|
||||
behavior="all",
|
||||
condition_options=condition_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
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"]
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from copy import deepcopy
|
||||
import json
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import call, patch
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -30,6 +30,7 @@ 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,
|
||||
@@ -63,7 +64,11 @@ from .common import (
|
||||
|
||||
from tests.common import async_fire_mqtt_message
|
||||
from tests.components.vacuum import common
|
||||
from tests.typing import MqttMockHAClientGenerator, MqttMockPahoClient
|
||||
from tests.typing import (
|
||||
MqttMockHAClientGenerator,
|
||||
MqttMockPahoClient,
|
||||
WebSocketGenerator,
|
||||
)
|
||||
|
||||
COMMAND_TOPIC = "vacuum/command"
|
||||
SEND_COMMAND_TOPIC = "vacuum/send_command"
|
||||
@@ -82,6 +87,27 @@ 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(
|
||||
@@ -294,6 +320,347 @@ 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
|
||||
|
||||
@@ -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,32 +70,17 @@ async def test_person_state_condition_behavior_any(
|
||||
states: list[ConditionStateDescription],
|
||||
) -> None:
|
||||
"""Test the person state condition with the 'any' behavior."""
|
||||
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(
|
||||
await assert_condition_behavior_any(
|
||||
hass,
|
||||
target_entities=target_persons,
|
||||
condition_target_config=condition_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
condition=condition,
|
||||
target=condition_target_config,
|
||||
behavior="any",
|
||||
condition_options=condition_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
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(
|
||||
@@ -128,29 +113,13 @@ async def test_person_state_condition_behavior_all(
|
||||
states: list[ConditionStateDescription],
|
||||
) -> None:
|
||||
"""Test the person state condition with the 'all' behavior."""
|
||||
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(
|
||||
await assert_condition_behavior_all(
|
||||
hass,
|
||||
target_entities=target_persons,
|
||||
condition_target_config=condition_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
condition=condition,
|
||||
target=condition_target_config,
|
||||
behavior="all",
|
||||
condition_options=condition_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
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"]
|
||||
|
||||
60
tests/components/prana/snapshots/test_number.ambr
Normal file
60
tests/components/prana/snapshots/test_number.ambr
Normal file
@@ -0,0 +1,60 @@
|
||||
# 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',
|
||||
})
|
||||
# ---
|
||||
76
tests/components/prana/test_number.py
Normal file
76
tests/components/prana/test_number.py
Normal file
@@ -0,0 +1,76 @@
|
||||
"""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)
|
||||
@@ -8,6 +8,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.radio_browser.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
@@ -39,10 +40,15 @@ async def init_integration(
|
||||
) -> MockConfigEntry:
|
||||
"""Set up the Radio Browser integration for testing."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
with patch(
|
||||
"homeassistant.components.radio_browser.RadioBrowser",
|
||||
autospec=True,
|
||||
):
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
return mock_config_entry
|
||||
|
||||
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
"""Tests for radio_browser media_source."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from aiodns.error import DNSError
|
||||
import pytest
|
||||
from radios import FilterBy, Order
|
||||
from radios import FilterBy, Order, RadioBrowserError
|
||||
|
||||
from homeassistant.components import media_source
|
||||
from homeassistant.components.media_player import BrowseError
|
||||
from homeassistant.components.radio_browser.media_source import async_get_media_source
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
DOMAIN = "radio_browser"
|
||||
|
||||
|
||||
@@ -71,3 +76,113 @@ async def test_browsing_local(
|
||||
assert other_browse is not None
|
||||
assert other_browse.title == "My Radios"
|
||||
assert len(other_browse.children) == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"exception",
|
||||
[DNSError, RadioBrowserError],
|
||||
)
|
||||
async def test_browsing_exceptions(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
exception: Exception,
|
||||
) -> None:
|
||||
"""Test browsing exceptions."""
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.radio_browser.RadioBrowser",
|
||||
autospec=True,
|
||||
) as mock_browser:
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
mock_browser.return_value.stations.side_effect = exception
|
||||
with pytest.raises(BrowseError) as exc_info:
|
||||
await media_source.async_browse_media(
|
||||
hass, f"{media_source.URI_SCHEME}{DOMAIN}/popular"
|
||||
)
|
||||
assert exc_info.value.translation_key == "radio_browser_error"
|
||||
|
||||
|
||||
async def test_browsing_not_ready(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test browsing config entry not ready."""
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.radio_browser.RadioBrowser",
|
||||
autospec=True,
|
||||
) as mock_browser:
|
||||
mock_browser.return_value.stats.side_effect = RadioBrowserError
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state == ConfigEntryState.SETUP_RETRY
|
||||
|
||||
with pytest.raises(BrowseError) as exc_info:
|
||||
await media_source.async_browse_media(
|
||||
hass, f"{media_source.URI_SCHEME}{DOMAIN}/popular"
|
||||
)
|
||||
assert exc_info.value.translation_key == "config_entry_not_ready"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"exception",
|
||||
[DNSError, RadioBrowserError],
|
||||
)
|
||||
async def test_resolve_media_exceptions(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
exception: Exception,
|
||||
) -> None:
|
||||
"""Test resolving media exceptions."""
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.radio_browser.RadioBrowser",
|
||||
autospec=True,
|
||||
) as mock_browser:
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
mock_browser.return_value.station.side_effect = exception
|
||||
with pytest.raises(media_source.Unresolvable) as exc_info:
|
||||
await media_source.async_resolve_media(
|
||||
hass, f"{media_source.URI_SCHEME}{DOMAIN}/123456", None
|
||||
)
|
||||
assert exc_info.value.translation_key == "radio_browser_error"
|
||||
|
||||
|
||||
async def test_resolve_media_not_ready(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test resolving media config entry not ready."""
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.radio_browser.RadioBrowser",
|
||||
autospec=True,
|
||||
) as mock_browser:
|
||||
mock_browser.return_value.stats.side_effect = RadioBrowserError
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state == ConfigEntryState.SETUP_RETRY
|
||||
|
||||
with pytest.raises(media_source.Unresolvable) as exc_info:
|
||||
await media_source.async_resolve_media(
|
||||
hass, f"{media_source.URI_SCHEME}{DOMAIN}/123456", None
|
||||
)
|
||||
assert exc_info.value.translation_key == "config_entry_not_ready"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""The tests for the Season integration."""
|
||||
|
||||
from datetime import datetime
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from freezegun import freeze_time
|
||||
import pytest
|
||||
@@ -20,6 +21,8 @@ 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
|
||||
|
||||
@@ -44,25 +47,25 @@ HEMISPHERE_EMPTY = {
|
||||
}
|
||||
|
||||
NORTHERN_PARAMETERS = [
|
||||
(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),
|
||||
(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),
|
||||
]
|
||||
|
||||
SOUTHERN_PARAMETERS = [
|
||||
(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),
|
||||
(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),
|
||||
]
|
||||
|
||||
|
||||
@@ -154,7 +157,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)):
|
||||
with freeze_time(datetime(2017, 9, 3, 0, 0, tzinfo=UTC)):
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@@ -165,3 +168,43 @@ 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
|
||||
|
||||
@@ -68,6 +68,7 @@ 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",
|
||||
|
||||
@@ -0,0 +1,331 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
{
|
||||
"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": {}
|
||||
}
|
||||
@@ -2325,6 +2325,57 @@
|
||||
'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([
|
||||
|
||||
@@ -549,3 +549,53 @@
|
||||
'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',
|
||||
})
|
||||
# ---
|
||||
|
||||
@@ -1270,6 +1270,37 @@
|
||||
'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,
|
||||
|
||||
@@ -727,8 +727,9 @@
|
||||
# ---
|
||||
# name: test_all_entities[da_rvc_map_01011][select.robot_vacuum_sound_mode-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
|
||||
@@ -14360,6 +14360,60 @@
|
||||
'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([
|
||||
|
||||
@@ -1499,6 +1499,56 @@
|
||||
'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([
|
||||
|
||||
1
tests/components/temperature/__init__.py
Normal file
1
tests/components/temperature/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Tests for the temperature integration."""
|
||||
892
tests/components/temperature/test_trigger.py
Normal file
892
tests/components/temperature/test_trigger.py
Normal file
@@ -0,0 +1,892 @@
|
||||
"""Test temperature trigger."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_CURRENT_TEMPERATURE as CLIMATE_ATTR_CURRENT_TEMPERATURE,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.components.water_heater import (
|
||||
ATTR_CURRENT_TEMPERATURE as WATER_HEATER_ATTR_CURRENT_TEMPERATURE,
|
||||
)
|
||||
from homeassistant.components.weather import (
|
||||
ATTR_WEATHER_TEMPERATURE,
|
||||
ATTR_WEATHER_TEMPERATURE_UNIT,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_CLASS,
|
||||
ATTR_UNIT_OF_MEASUREMENT,
|
||||
CONF_ENTITY_ID,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
|
||||
from tests.components.common import (
|
||||
TriggerStateDescription,
|
||||
arm_trigger,
|
||||
assert_trigger_behavior_any,
|
||||
assert_trigger_behavior_first,
|
||||
assert_trigger_behavior_last,
|
||||
assert_trigger_gated_by_labs_flag,
|
||||
parametrize_numerical_attribute_changed_trigger_states,
|
||||
parametrize_numerical_attribute_crossed_threshold_trigger_states,
|
||||
parametrize_numerical_state_value_changed_trigger_states,
|
||||
parametrize_numerical_state_value_crossed_threshold_trigger_states,
|
||||
parametrize_target_entities,
|
||||
target_entities,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def target_sensors(hass: HomeAssistant) -> dict[str, list[str]]:
|
||||
"""Create multiple sensor entities associated with different targets."""
|
||||
return await target_entities(hass, "sensor")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def target_climates(hass: HomeAssistant) -> dict[str, list[str]]:
|
||||
"""Create multiple climate entities associated with different targets."""
|
||||
return await target_entities(hass, "climate")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def target_water_heaters(hass: HomeAssistant) -> dict[str, list[str]]:
|
||||
"""Create multiple water_heater entities associated with different targets."""
|
||||
return await target_entities(hass, "water_heater")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def target_weathers(hass: HomeAssistant) -> dict[str, list[str]]:
|
||||
"""Create multiple weather entities associated with different targets."""
|
||||
return await target_entities(hass, "weather")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"trigger_key",
|
||||
[
|
||||
"temperature.changed",
|
||||
"temperature.crossed_threshold",
|
||||
],
|
||||
)
|
||||
async def test_temperature_triggers_gated_by_labs_flag(
|
||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, trigger_key: str
|
||||
) -> None:
|
||||
"""Test the temperature triggers are gated by the labs flag."""
|
||||
await assert_trigger_gated_by_labs_flag(hass, caplog, trigger_key)
|
||||
|
||||
|
||||
# --- Sensor domain tests (value in state.state) ---
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("sensor"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "states"),
|
||||
[
|
||||
*parametrize_numerical_state_value_changed_trigger_states(
|
||||
"temperature.changed", "temperature"
|
||||
),
|
||||
*parametrize_numerical_state_value_crossed_threshold_trigger_states(
|
||||
"temperature.crossed_threshold", "temperature"
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_temperature_trigger_sensor_behavior_any(
|
||||
hass: HomeAssistant,
|
||||
service_calls: list[ServiceCall],
|
||||
target_sensors: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
trigger: str,
|
||||
trigger_options: dict[str, Any],
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test temperature trigger fires for sensor entities with device_class temperature."""
|
||||
await assert_trigger_behavior_any(
|
||||
hass,
|
||||
service_calls=service_calls,
|
||||
target_entities=target_sensors,
|
||||
trigger_target_config=trigger_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
trigger=trigger,
|
||||
trigger_options=trigger_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("sensor"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "states"),
|
||||
[
|
||||
*parametrize_numerical_state_value_crossed_threshold_trigger_states(
|
||||
"temperature.crossed_threshold", "temperature"
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_temperature_trigger_sensor_crossed_threshold_behavior_first(
|
||||
hass: HomeAssistant,
|
||||
service_calls: list[ServiceCall],
|
||||
target_sensors: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
trigger: str,
|
||||
trigger_options: dict[str, Any],
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test temperature crossed_threshold trigger fires on the first sensor state change."""
|
||||
await assert_trigger_behavior_first(
|
||||
hass,
|
||||
service_calls=service_calls,
|
||||
target_entities=target_sensors,
|
||||
trigger_target_config=trigger_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
trigger=trigger,
|
||||
trigger_options=trigger_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("sensor"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "states"),
|
||||
[
|
||||
*parametrize_numerical_state_value_crossed_threshold_trigger_states(
|
||||
"temperature.crossed_threshold", "temperature"
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_temperature_trigger_sensor_crossed_threshold_behavior_last(
|
||||
hass: HomeAssistant,
|
||||
service_calls: list[ServiceCall],
|
||||
target_sensors: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
trigger: str,
|
||||
trigger_options: dict[str, Any],
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test temperature crossed_threshold trigger fires when the last sensor changes state."""
|
||||
await assert_trigger_behavior_last(
|
||||
hass,
|
||||
service_calls=service_calls,
|
||||
target_entities=target_sensors,
|
||||
trigger_target_config=trigger_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
trigger=trigger,
|
||||
trigger_options=trigger_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
|
||||
# --- Climate domain tests (value in current_temperature attribute) ---
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("climate"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "states"),
|
||||
[
|
||||
*parametrize_numerical_attribute_changed_trigger_states(
|
||||
"temperature.changed", HVACMode.AUTO, CLIMATE_ATTR_CURRENT_TEMPERATURE
|
||||
),
|
||||
*parametrize_numerical_attribute_crossed_threshold_trigger_states(
|
||||
"temperature.crossed_threshold",
|
||||
HVACMode.AUTO,
|
||||
CLIMATE_ATTR_CURRENT_TEMPERATURE,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_temperature_trigger_climate_behavior_any(
|
||||
hass: HomeAssistant,
|
||||
service_calls: list[ServiceCall],
|
||||
target_climates: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
trigger: str,
|
||||
trigger_options: dict[str, Any],
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test temperature trigger fires for climate entities."""
|
||||
await assert_trigger_behavior_any(
|
||||
hass,
|
||||
service_calls=service_calls,
|
||||
target_entities=target_climates,
|
||||
trigger_target_config=trigger_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
trigger=trigger,
|
||||
trigger_options=trigger_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("climate"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "states"),
|
||||
[
|
||||
*parametrize_numerical_attribute_crossed_threshold_trigger_states(
|
||||
"temperature.crossed_threshold",
|
||||
HVACMode.AUTO,
|
||||
CLIMATE_ATTR_CURRENT_TEMPERATURE,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_temperature_trigger_climate_crossed_threshold_behavior_first(
|
||||
hass: HomeAssistant,
|
||||
service_calls: list[ServiceCall],
|
||||
target_climates: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
trigger: str,
|
||||
trigger_options: dict[str, Any],
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test temperature crossed_threshold trigger fires on the first climate state change."""
|
||||
await assert_trigger_behavior_first(
|
||||
hass,
|
||||
service_calls=service_calls,
|
||||
target_entities=target_climates,
|
||||
trigger_target_config=trigger_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
trigger=trigger,
|
||||
trigger_options=trigger_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("climate"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "states"),
|
||||
[
|
||||
*parametrize_numerical_attribute_crossed_threshold_trigger_states(
|
||||
"temperature.crossed_threshold",
|
||||
HVACMode.AUTO,
|
||||
CLIMATE_ATTR_CURRENT_TEMPERATURE,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_temperature_trigger_climate_crossed_threshold_behavior_last(
|
||||
hass: HomeAssistant,
|
||||
service_calls: list[ServiceCall],
|
||||
target_climates: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
trigger: str,
|
||||
trigger_options: dict[str, Any],
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test temperature crossed_threshold trigger fires when the last climate changes state."""
|
||||
await assert_trigger_behavior_last(
|
||||
hass,
|
||||
service_calls=service_calls,
|
||||
target_entities=target_climates,
|
||||
trigger_target_config=trigger_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
trigger=trigger,
|
||||
trigger_options=trigger_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
|
||||
# --- Water heater domain tests (value in current_temperature attribute) ---
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("water_heater"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "states"),
|
||||
[
|
||||
*parametrize_numerical_attribute_changed_trigger_states(
|
||||
"temperature.changed",
|
||||
"eco",
|
||||
WATER_HEATER_ATTR_CURRENT_TEMPERATURE,
|
||||
),
|
||||
*parametrize_numerical_attribute_crossed_threshold_trigger_states(
|
||||
"temperature.crossed_threshold",
|
||||
"eco",
|
||||
WATER_HEATER_ATTR_CURRENT_TEMPERATURE,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_temperature_trigger_water_heater_behavior_any(
|
||||
hass: HomeAssistant,
|
||||
service_calls: list[ServiceCall],
|
||||
target_water_heaters: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
trigger: str,
|
||||
trigger_options: dict[str, Any],
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test temperature trigger fires for water_heater entities."""
|
||||
await assert_trigger_behavior_any(
|
||||
hass,
|
||||
service_calls=service_calls,
|
||||
target_entities=target_water_heaters,
|
||||
trigger_target_config=trigger_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
trigger=trigger,
|
||||
trigger_options=trigger_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("water_heater"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "states"),
|
||||
[
|
||||
*parametrize_numerical_attribute_crossed_threshold_trigger_states(
|
||||
"temperature.crossed_threshold",
|
||||
"eco",
|
||||
WATER_HEATER_ATTR_CURRENT_TEMPERATURE,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_temperature_trigger_water_heater_crossed_threshold_behavior_first(
|
||||
hass: HomeAssistant,
|
||||
service_calls: list[ServiceCall],
|
||||
target_water_heaters: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
trigger: str,
|
||||
trigger_options: dict[str, Any],
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test temperature crossed_threshold trigger fires on the first water_heater state change."""
|
||||
await assert_trigger_behavior_first(
|
||||
hass,
|
||||
service_calls=service_calls,
|
||||
target_entities=target_water_heaters,
|
||||
trigger_target_config=trigger_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
trigger=trigger,
|
||||
trigger_options=trigger_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("water_heater"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "states"),
|
||||
[
|
||||
*parametrize_numerical_attribute_crossed_threshold_trigger_states(
|
||||
"temperature.crossed_threshold",
|
||||
"eco",
|
||||
WATER_HEATER_ATTR_CURRENT_TEMPERATURE,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_temperature_trigger_water_heater_crossed_threshold_behavior_last(
|
||||
hass: HomeAssistant,
|
||||
service_calls: list[ServiceCall],
|
||||
target_water_heaters: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
trigger: str,
|
||||
trigger_options: dict[str, Any],
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test temperature crossed_threshold trigger fires when the last water_heater changes state."""
|
||||
await assert_trigger_behavior_last(
|
||||
hass,
|
||||
service_calls=service_calls,
|
||||
target_entities=target_water_heaters,
|
||||
trigger_target_config=trigger_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
trigger=trigger,
|
||||
trigger_options=trigger_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
|
||||
# --- Weather domain tests (value in temperature attribute) ---
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("weather"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "states"),
|
||||
[
|
||||
*parametrize_numerical_attribute_changed_trigger_states(
|
||||
"temperature.changed", "sunny", ATTR_WEATHER_TEMPERATURE
|
||||
),
|
||||
*parametrize_numerical_attribute_crossed_threshold_trigger_states(
|
||||
"temperature.crossed_threshold",
|
||||
"sunny",
|
||||
ATTR_WEATHER_TEMPERATURE,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_temperature_trigger_weather_behavior_any(
|
||||
hass: HomeAssistant,
|
||||
service_calls: list[ServiceCall],
|
||||
target_weathers: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
trigger: str,
|
||||
trigger_options: dict[str, Any],
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test temperature trigger fires for weather entities."""
|
||||
await assert_trigger_behavior_any(
|
||||
hass,
|
||||
service_calls=service_calls,
|
||||
target_entities=target_weathers,
|
||||
trigger_target_config=trigger_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
trigger=trigger,
|
||||
trigger_options=trigger_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("weather"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "states"),
|
||||
[
|
||||
*parametrize_numerical_attribute_crossed_threshold_trigger_states(
|
||||
"temperature.crossed_threshold",
|
||||
"sunny",
|
||||
ATTR_WEATHER_TEMPERATURE,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_temperature_trigger_weather_crossed_threshold_behavior_first(
|
||||
hass: HomeAssistant,
|
||||
service_calls: list[ServiceCall],
|
||||
target_weathers: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
trigger: str,
|
||||
trigger_options: dict[str, Any],
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test temperature crossed_threshold trigger fires on the first weather state change."""
|
||||
await assert_trigger_behavior_first(
|
||||
hass,
|
||||
service_calls=service_calls,
|
||||
target_entities=target_weathers,
|
||||
trigger_target_config=trigger_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
trigger=trigger,
|
||||
trigger_options=trigger_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("weather"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "states"),
|
||||
[
|
||||
*parametrize_numerical_attribute_crossed_threshold_trigger_states(
|
||||
"temperature.crossed_threshold",
|
||||
"sunny",
|
||||
ATTR_WEATHER_TEMPERATURE,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_temperature_trigger_weather_crossed_threshold_behavior_last(
|
||||
hass: HomeAssistant,
|
||||
service_calls: list[ServiceCall],
|
||||
target_weathers: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
trigger: str,
|
||||
trigger_options: dict[str, Any],
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test temperature crossed_threshold trigger fires when the last weather changes state."""
|
||||
await assert_trigger_behavior_last(
|
||||
hass,
|
||||
service_calls=service_calls,
|
||||
target_entities=target_weathers,
|
||||
trigger_target_config=trigger_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
trigger=trigger,
|
||||
trigger_options=trigger_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
|
||||
# --- Device class exclusion test ---
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"trigger_key",
|
||||
"trigger_options",
|
||||
"sensor_initial",
|
||||
"sensor_target",
|
||||
),
|
||||
[
|
||||
(
|
||||
"temperature.changed",
|
||||
{},
|
||||
"20",
|
||||
"25",
|
||||
),
|
||||
(
|
||||
"temperature.crossed_threshold",
|
||||
{"threshold_type": "above", "lower_limit": 10},
|
||||
"5",
|
||||
"20",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_temperature_trigger_excludes_non_temperature_sensor(
|
||||
hass: HomeAssistant,
|
||||
service_calls: list[ServiceCall],
|
||||
trigger_key: str,
|
||||
trigger_options: dict[str, Any],
|
||||
sensor_initial: str,
|
||||
sensor_target: str,
|
||||
) -> None:
|
||||
"""Test temperature trigger does not fire for sensor entities without device_class temperature."""
|
||||
entity_id_temperature = "sensor.test_temperature"
|
||||
entity_id_humidity = "sensor.test_humidity"
|
||||
|
||||
# Set initial states
|
||||
hass.states.async_set(
|
||||
entity_id_temperature, sensor_initial, {ATTR_DEVICE_CLASS: "temperature"}
|
||||
)
|
||||
hass.states.async_set(
|
||||
entity_id_humidity, sensor_initial, {ATTR_DEVICE_CLASS: "humidity"}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await arm_trigger(
|
||||
hass,
|
||||
trigger_key,
|
||||
trigger_options,
|
||||
{
|
||||
CONF_ENTITY_ID: [
|
||||
entity_id_temperature,
|
||||
entity_id_humidity,
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
# Temperature sensor changes - should trigger
|
||||
hass.states.async_set(
|
||||
entity_id_temperature, sensor_target, {ATTR_DEVICE_CLASS: "temperature"}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id_temperature
|
||||
service_calls.clear()
|
||||
|
||||
# Humidity sensor changes - should NOT trigger (wrong device class)
|
||||
hass.states.async_set(
|
||||
entity_id_humidity, sensor_target, {ATTR_DEVICE_CLASS: "humidity"}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
|
||||
# --- Unit conversion tests ---
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
async def test_temperature_trigger_unit_conversion_sensor_celsius_to_fahrenheit(
|
||||
hass: HomeAssistant,
|
||||
service_calls: list[ServiceCall],
|
||||
) -> None:
|
||||
"""Test temperature trigger converts sensor value from °C to °F for threshold comparison."""
|
||||
entity_id = "sensor.test_temp"
|
||||
|
||||
# Sensor reports in °C, trigger configured in °F with threshold above 70°F
|
||||
hass.states.async_set(
|
||||
entity_id,
|
||||
"20",
|
||||
{
|
||||
ATTR_DEVICE_CLASS: "temperature",
|
||||
ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await arm_trigger(
|
||||
hass,
|
||||
"temperature.crossed_threshold",
|
||||
{
|
||||
"threshold_type": "above",
|
||||
"lower_limit": 70,
|
||||
"unit": "fahrenheit",
|
||||
},
|
||||
{CONF_ENTITY_ID: [entity_id]},
|
||||
)
|
||||
|
||||
# 20°C = 68°F, which is below 70°F - should NOT trigger
|
||||
hass.states.async_set(
|
||||
entity_id,
|
||||
"20",
|
||||
{
|
||||
ATTR_DEVICE_CLASS: "temperature",
|
||||
ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
# 22°C = 71.6°F, which is above 70°F - should trigger
|
||||
hass.states.async_set(
|
||||
entity_id,
|
||||
"22",
|
||||
{
|
||||
ATTR_DEVICE_CLASS: "temperature",
|
||||
ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
service_calls.clear()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
async def test_temperature_trigger_unit_conversion_sensor_fahrenheit_to_celsius(
|
||||
hass: HomeAssistant,
|
||||
service_calls: list[ServiceCall],
|
||||
) -> None:
|
||||
"""Test temperature trigger converts sensor value from °F to °C for threshold comparison."""
|
||||
entity_id = "sensor.test_temp"
|
||||
|
||||
# Sensor reports in °F, trigger configured in °C with threshold above 25°C
|
||||
hass.states.async_set(
|
||||
entity_id,
|
||||
"70",
|
||||
{
|
||||
ATTR_DEVICE_CLASS: "temperature",
|
||||
ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.FAHRENHEIT,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await arm_trigger(
|
||||
hass,
|
||||
"temperature.crossed_threshold",
|
||||
{
|
||||
"threshold_type": "above",
|
||||
"lower_limit": 25,
|
||||
"unit": "celsius",
|
||||
},
|
||||
{CONF_ENTITY_ID: [entity_id]},
|
||||
)
|
||||
|
||||
# 70°F = 21.1°C, which is below 25°C - should NOT trigger
|
||||
hass.states.async_set(
|
||||
entity_id,
|
||||
"70",
|
||||
{
|
||||
ATTR_DEVICE_CLASS: "temperature",
|
||||
ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.FAHRENHEIT,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
# 80°F = 26.7°C, which is above 25°C - should trigger
|
||||
hass.states.async_set(
|
||||
entity_id,
|
||||
"80",
|
||||
{
|
||||
ATTR_DEVICE_CLASS: "temperature",
|
||||
ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.FAHRENHEIT,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
service_calls.clear()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
async def test_temperature_trigger_unit_conversion_changed(
|
||||
hass: HomeAssistant,
|
||||
service_calls: list[ServiceCall],
|
||||
) -> None:
|
||||
"""Test temperature changed trigger with unit conversion and above/below limits."""
|
||||
entity_id = "sensor.test_temp"
|
||||
|
||||
# Sensor reports in °C, trigger configured in °F: above 68°F (20°C), below 77°F (25°C)
|
||||
hass.states.async_set(
|
||||
entity_id,
|
||||
"18",
|
||||
{
|
||||
ATTR_DEVICE_CLASS: "temperature",
|
||||
ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await arm_trigger(
|
||||
hass,
|
||||
"temperature.changed",
|
||||
{
|
||||
"above": 68,
|
||||
"below": 77,
|
||||
"unit": "fahrenheit",
|
||||
},
|
||||
{CONF_ENTITY_ID: [entity_id]},
|
||||
)
|
||||
|
||||
# 18°C = 64.4°F, below 68°F - should NOT trigger
|
||||
hass.states.async_set(
|
||||
entity_id,
|
||||
"19",
|
||||
{
|
||||
ATTR_DEVICE_CLASS: "temperature",
|
||||
ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
# 22°C = 71.6°F, between 68°F and 77°F - should trigger
|
||||
hass.states.async_set(
|
||||
entity_id,
|
||||
"22",
|
||||
{
|
||||
ATTR_DEVICE_CLASS: "temperature",
|
||||
ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
service_calls.clear()
|
||||
|
||||
# 26°C = 78.8°F, above 77°F - should NOT trigger
|
||||
hass.states.async_set(
|
||||
entity_id,
|
||||
"26",
|
||||
{
|
||||
ATTR_DEVICE_CLASS: "temperature",
|
||||
ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
async def test_temperature_trigger_unit_conversion_weather(
|
||||
hass: HomeAssistant,
|
||||
service_calls: list[ServiceCall],
|
||||
) -> None:
|
||||
"""Test temperature trigger with unit conversion for weather entities."""
|
||||
entity_id = "weather.test"
|
||||
|
||||
# Weather reports temperature in °F, trigger configured in °C with threshold above 25°C
|
||||
hass.states.async_set(
|
||||
entity_id,
|
||||
"sunny",
|
||||
{
|
||||
ATTR_WEATHER_TEMPERATURE: 70,
|
||||
ATTR_WEATHER_TEMPERATURE_UNIT: UnitOfTemperature.FAHRENHEIT,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await arm_trigger(
|
||||
hass,
|
||||
"temperature.crossed_threshold",
|
||||
{
|
||||
"threshold_type": "above",
|
||||
"lower_limit": 25,
|
||||
"unit": "celsius",
|
||||
},
|
||||
{CONF_ENTITY_ID: [entity_id]},
|
||||
)
|
||||
|
||||
# 70°F = 21.1°C, below 25°C - should NOT trigger
|
||||
hass.states.async_set(
|
||||
entity_id,
|
||||
"sunny",
|
||||
{
|
||||
ATTR_WEATHER_TEMPERATURE: 70,
|
||||
ATTR_WEATHER_TEMPERATURE_UNIT: UnitOfTemperature.FAHRENHEIT,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
# 80°F = 26.7°C, above 25°C - should trigger
|
||||
hass.states.async_set(
|
||||
entity_id,
|
||||
"sunny",
|
||||
{
|
||||
ATTR_WEATHER_TEMPERATURE: 80,
|
||||
ATTR_WEATHER_TEMPERATURE_UNIT: UnitOfTemperature.FAHRENHEIT,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
service_calls.clear()
|
||||
@@ -1231,3 +1231,53 @@ 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
|
||||
|
||||
@@ -10,6 +10,7 @@ from homeassistant.components.vacuum import (
|
||||
ATTR_FAN_SPEED,
|
||||
ATTR_PARAMS,
|
||||
DOMAIN,
|
||||
SERVICE_CLEAN_AREA,
|
||||
SERVICE_CLEAN_SPOT,
|
||||
SERVICE_LOCATE,
|
||||
SERVICE_PAUSE,
|
||||
@@ -82,6 +83,25 @@ 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."""
|
||||
|
||||
@@ -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,32 +89,17 @@ async def test_vacuum_state_condition_behavior_any(
|
||||
states: list[ConditionStateDescription],
|
||||
) -> None:
|
||||
"""Test the vacuum state condition with the 'any' behavior."""
|
||||
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(
|
||||
await assert_condition_behavior_any(
|
||||
hass,
|
||||
target_entities=target_vacuums,
|
||||
condition_target_config=condition_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
condition=condition,
|
||||
target=condition_target_config,
|
||||
behavior="any",
|
||||
condition_options=condition_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
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(
|
||||
@@ -162,29 +147,13 @@ async def test_vacuum_state_condition_behavior_all(
|
||||
states: list[ConditionStateDescription],
|
||||
) -> None:
|
||||
"""Test the vacuum state condition with the 'all' behavior."""
|
||||
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(
|
||||
await assert_condition_behavior_all(
|
||||
hass,
|
||||
target_entities=target_vacuums,
|
||||
condition_target_config=condition_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
condition=condition,
|
||||
target=condition_target_config,
|
||||
behavior="all",
|
||||
condition_options=condition_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
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"]
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
'system_health',
|
||||
'system_log',
|
||||
'tag',
|
||||
'temperature',
|
||||
'text',
|
||||
'time',
|
||||
'timer',
|
||||
@@ -190,6 +191,7 @@
|
||||
'system_health',
|
||||
'system_log',
|
||||
'tag',
|
||||
'temperature',
|
||||
'text',
|
||||
'time',
|
||||
'timer',
|
||||
|
||||
Reference in New Issue
Block a user