Compare commits

...

10 Commits

Author SHA1 Message Date
prana-dev-official
0a2fc97696 Import improvement for Prana integration (#165805) 2026-03-17 16:28:53 +01:00
Joost Lekkerkerker
447d616097 Add select for SmartThings RVC sound mode (#164519) 2026-03-17 15:57:59 +01:00
Norbert Rittel
d3102e718d Consistenly sentence-case "API token" in habitica (#165369)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-03-17 14:30:39 +00:00
Josef Zweck
69ee49735a Remove support for homeassistant.update_entity from mold_indicator (#165797) 2026-03-17 15:26:22 +01:00
Daniel Hjelseth Høyer
35a99dd4a4 Fix Tibber update token (#164295)
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-03-17 15:11:51 +01:00
Ariel Ebersberger
51c3397be8 Refactor wemo integration to use async service action handlers (#165794)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-17 15:07:00 +01:00
Brett Adams
57f0fd2ed2 Tesla Fleet: fix malformed energy live response handling (#165101) 2026-03-17 15:04:35 +01:00
Erik Montnemery
fa7a216afe Use return value from target_entities directly in condition tests (#165791) 2026-03-17 14:55:17 +01:00
Josef Zweck
20f4426e1d Fix mold_indicator sensor update (#158996) 2026-03-17 14:28:50 +01:00
Erik Montnemery
ba30563772 Deduplicate tests testing triggers in mode last (#165789) 2026-03-17 14:28:10 +01:00
63 changed files with 1262 additions and 1311 deletions

View File

@@ -89,18 +89,18 @@
"step": {
"advanced": {
"data": {
"api_key": "API Token",
"api_key": "API token",
"api_user": "User ID",
"url": "[%key:common::config_flow::data::url%]",
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
},
"data_description": {
"api_key": "API Token of the Habitica account",
"api_key": "API token of the Habitica account",
"api_user": "User ID of your Habitica account",
"url": "URL of the Habitica installation to connect to. Defaults to `{default_url}`",
"verify_ssl": "Enable SSL certificate verification for secure connections. Disable only if connecting to a Habitica instance using a self-signed certificate"
},
"description": "You can retrieve your `User ID` and `API Token` from [**Settings -> Site Data**]({site_data}) on Habitica or the instance you want to connect to",
"description": "You can retrieve your 'User ID' and 'API token' from [**Settings -> Site Data**]({site_data}) on Habitica or the instance you want to connect to",
"title": "[%key:component::habitica::config::step::user::menu_options::advanced%]"
},
"login": {
@@ -126,7 +126,7 @@
"api_key": "[%key:component::habitica::config::step::advanced::data_description::api_key%]"
},
"description": "Enter your new API token below. You can find it in Habitica under 'Settings -> Site Data'",
"name": "Re-authorize via API Token"
"name": "Re-authorize via API token"
},
"reauth_login": {
"data": {

View File

@@ -203,105 +203,80 @@ class MoldIndicator(SensorEntity):
def _async_setup_sensor(self) -> None:
"""Set up the sensor and start tracking state changes."""
@callback
def mold_indicator_sensors_state_listener(
event: Event[EventStateChangedData],
) -> None:
"""Handle for state changes for dependent sensors."""
new_state = event.data["new_state"]
old_state = event.data["old_state"]
entity = event.data["entity_id"]
_LOGGER.debug(
"Sensor state change for %s that had old state %s and new state %s",
entity,
old_state,
new_state,
)
if self._update_sensor(entity, old_state, new_state):
if self._preview_callback:
calculated_state = self._async_calculate_state()
self._preview_callback(
calculated_state.state, calculated_state.attributes
)
# only write state to the state machine if we are not in preview mode
else:
self.async_schedule_update_ha_state(True)
@callback
def mold_indicator_startup() -> None:
"""Add listeners and get 1st state."""
_LOGGER.debug("Startup for %s", self.entity_id)
self.async_on_remove(
async_track_state_change_event(
self.hass,
list(self._entities.values()),
mold_indicator_sensors_state_listener,
self._entities.values(),
self._async_mold_indicator_sensor_state_listener,
)
)
# Replay current state of source entities
for entity_id in self._entities.values():
state = self.hass.states.get(entity_id)
state_event: Event[EventStateChangedData] = Event(
"", {"entity_id": entity_id, "new_state": state, "old_state": None}
)
self._async_mold_indicator_sensor_state_listener(
state_event, update_state=False
)
# Read initial state
indoor_temp = self.hass.states.get(self._entities[CONF_INDOOR_TEMP])
outdoor_temp = self.hass.states.get(self._entities[CONF_OUTDOOR_TEMP])
indoor_hum = self.hass.states.get(self._entities[CONF_INDOOR_HUMIDITY])
self._recalculate()
schedule_update = self._update_sensor(
self._entities[CONF_INDOOR_TEMP], None, indoor_temp
)
if self._preview_callback:
calculated_state = self._async_calculate_state()
self._preview_callback(calculated_state.state, calculated_state.attributes)
schedule_update = (
False
if not self._update_sensor(
self._entities[CONF_OUTDOOR_TEMP], None, outdoor_temp
)
else schedule_update
)
@callback
def _async_mold_indicator_sensor_state_listener(
self, event: Event[EventStateChangedData], update_state: bool = True
) -> None:
"""Handle state changes for dependent sensors."""
entity_id = event.data["entity_id"]
new_state = event.data["new_state"]
schedule_update = (
False
if not self._update_sensor(
self._entities[CONF_INDOOR_HUMIDITY], None, indoor_hum
)
else schedule_update
)
_LOGGER.debug(
"Sensor state change for %s that had old state %s and new state %s",
entity_id,
event.data["old_state"],
new_state,
)
if schedule_update and not self._preview_callback:
self.async_schedule_update_ha_state(True)
if self._preview_callback:
# re-calculate dewpoint and mold indicator
self._calc_dewpoint()
self._calc_moldindicator()
if self._attr_native_value is None:
self._attr_available = False
else:
self._attr_available = True
calculated_state = self._async_calculate_state()
self._preview_callback(
calculated_state.state, calculated_state.attributes
)
mold_indicator_startup()
def _update_sensor(
self, entity: str, old_state: State | None, new_state: State | None
) -> bool:
"""Update information based on new sensor states."""
_LOGGER.debug("Sensor update for %s", entity)
if new_state is None:
return False
# If old_state is not set and new state is unknown then it means
# that the sensor just started up
if old_state is None and new_state.state == STATE_UNKNOWN:
return False
if entity == self._entities[CONF_INDOOR_TEMP]:
# update state depending on which sensor changed
if entity_id == self._entities[CONF_INDOOR_TEMP]:
self._indoor_temp = self._get_temperature_from_state(new_state)
elif entity == self._entities[CONF_OUTDOOR_TEMP]:
elif entity_id == self._entities[CONF_OUTDOOR_TEMP]:
self._outdoor_temp = self._get_temperature_from_state(new_state)
elif entity == self._entities[CONF_INDOOR_HUMIDITY]:
elif entity_id == self._entities[CONF_INDOOR_HUMIDITY]:
self._indoor_hum = self._get_humidity_from_state(new_state)
return True
if not update_state:
return
self._recalculate()
if self._preview_callback:
calculated_state = self._async_calculate_state()
self._preview_callback(calculated_state.state, calculated_state.attributes)
# only write state to the state machine if we are not in preview mode
else:
self.async_write_ha_state()
@callback
def _recalculate(self) -> None:
"""Recalculate mold indicator from cached sensor values."""
# Check if all sensors are available
if None in (self._indoor_temp, self._indoor_hum, self._outdoor_temp):
self._attr_available = False
self._attr_native_value = None
self._dewpoint = None
self._crit_temp = None
return
# Calculate dewpoint and mold indicator
self._calc_dewpoint()
self._calc_moldindicator()
self._attr_available = self._attr_native_value is not None
def _get_value_from_state(
self,
@@ -376,26 +351,6 @@ class MoldIndicator(SensorEntity):
return self._get_value_from_state(state, validate_humidity)
async def async_update(self) -> None:
"""Calculate latest state."""
_LOGGER.debug("Update state for %s", self.entity_id)
# check all sensors
if None in (self._indoor_temp, self._indoor_hum, self._outdoor_temp):
self._attr_available = False
self._dewpoint = None
self._crit_temp = None
return
# re-calculate dewpoint and mold indicator
self._calc_dewpoint()
self._calc_moldindicator()
if self._attr_native_value is None:
self._attr_available = False
self._dewpoint = None
self._crit_temp = None
else:
self._attr_available = True
def _calc_dewpoint(self) -> None:
"""Calculate the dewpoint for the indoor air."""
# Use magnus approximation to calculate the dew point

View File

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

View File

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

View File

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

View File

@@ -105,6 +105,9 @@
"robot_cleaner_driving_mode": {
"default": "mdi:car-cog"
},
"robot_cleaner_sound_mode": {
"default": "mdi:bell-cog"
},
"robot_cleaner_water_spray_level": {
"default": "mdi:spray-bottle"
},

View File

@@ -26,6 +26,12 @@ LAMP_TO_HA = {
"off": "off",
}
SOUND_MODE_TO_HA = {
"voice": "voice",
"beep": "tone",
"mute": "mute",
}
DRIVING_MODE_TO_HA = {
"areaThenWalls": "area_then_walls",
"wallFirst": "walls_first",
@@ -244,6 +250,16 @@ CAPABILITIES_TO_SELECT: dict[Capability | str, SmartThingsSelectDescription] = {
entity_category=EntityCategory.CONFIG,
value_is_integer=True,
),
Capability.SAMSUNG_CE_ROBOT_CLEANER_SYSTEM_SOUND_MODE: SmartThingsSelectDescription(
key=Capability.SAMSUNG_CE_ROBOT_CLEANER_SYSTEM_SOUND_MODE,
translation_key="robot_cleaner_sound_mode",
options_attribute=Attribute.SUPPORTED_SOUND_MODES,
status_attribute=Attribute.SOUND_MODE,
command=Command.SET_SOUND_MODE,
options_map=SOUND_MODE_TO_HA,
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
),
Capability.SAMSUNG_CE_ROBOT_CLEANER_CLEANING_TYPE: SmartThingsSelectDescription(
key=Capability.SAMSUNG_CE_ROBOT_CLEANER_CLEANING_TYPE,
translation_key="robot_cleaner_cleaning_type",

View File

@@ -254,6 +254,14 @@
"walls_first": "Walls first"
}
},
"robot_cleaner_sound_mode": {
"name": "Sound mode",
"state": {
"mute": "Mute",
"tone": "Tone",
"voice": "Voice"
}
},
"robot_cleaner_water_spray_level": {
"name": "Water level",
"state": {

View File

@@ -195,9 +195,22 @@ class TeslaFleetEnergySiteLiveCoordinator(DataUpdateCoordinator[dict[str, Any]])
except TeslaFleetError as e:
raise UpdateFailed(e.message) from e
if not isinstance(data, dict):
LOGGER.debug(
"%s got unexpected live status response type: %s",
self.name,
type(data).__name__,
)
return self.data
# Convert Wall Connectors from array to dict
wall_connectors = data.get("wall_connectors")
if not isinstance(wall_connectors, list):
wall_connectors = []
data["wall_connectors"] = {
wc["din"]: wc for wc in (data.get("wall_connectors") or [])
wc["din"]: wc
for wc in wall_connectors
if isinstance(wc, dict) and "din" in wc
}
self.updated_once = True

View File

@@ -2,9 +2,10 @@
from __future__ import annotations
from datetime import timedelta
import asyncio
from datetime import datetime, timedelta
import logging
from typing import TYPE_CHECKING, cast
from typing import TYPE_CHECKING, TypedDict, cast
from aiohttp.client_exceptions import ClientError
import tibber
@@ -38,6 +39,58 @@ FIVE_YEARS = 5 * 365 * 24
_LOGGER = logging.getLogger(__name__)
class TibberHomeData(TypedDict):
"""Data for a Tibber home used by the price sensor."""
currency: str
price_unit: str
current_price: float | None
current_price_time: datetime | None
intraday_price_ranking: float | None
max_price: float
avg_price: float
min_price: float
off_peak_1: float
peak: float
off_peak_2: float
month_cost: float | None
peak_hour: float | None
peak_hour_time: datetime | None
month_cons: float | None
app_nickname: str | None
grid_company: str | None
estimated_annual_consumption: int | None
def _build_home_data(home: tibber.TibberHome) -> TibberHomeData:
"""Build TibberHomeData from a TibberHome for the price sensor."""
current_price, last_updated, price_rank = home.current_price_data()
attributes = home.current_attributes()
result: TibberHomeData = {
"currency": home.currency,
"price_unit": home.price_unit,
"current_price": current_price,
"current_price_time": last_updated,
"intraday_price_ranking": price_rank,
"max_price": attributes["max_price"],
"avg_price": attributes["avg_price"],
"min_price": attributes["min_price"],
"off_peak_1": attributes["off_peak_1"],
"peak": attributes["peak"],
"off_peak_2": attributes["off_peak_2"],
"month_cost": home.month_cost,
"peak_hour": home.peak_hour,
"peak_hour_time": home.peak_hour_time,
"month_cons": home.month_cons,
"app_nickname": home.info["viewer"]["home"].get("appNickname"),
"grid_company": home.info["viewer"]["home"]["meteringPointData"]["gridCompany"],
"estimated_annual_consumption": home.info["viewer"]["home"][
"meteringPointData"
]["estimatedAnnualConsumption"],
}
return result
class TibberDataCoordinator(DataUpdateCoordinator[None]):
"""Handle Tibber data and insert statistics."""
@@ -57,13 +110,16 @@ class TibberDataCoordinator(DataUpdateCoordinator[None]):
name=f"Tibber {tibber_connection.name}",
update_interval=timedelta(minutes=20),
)
self._tibber_connection = tibber_connection
async def _async_update_data(self) -> None:
"""Update data via API."""
tibber_connection = await self.config_entry.runtime_data.async_get_client(
self.hass
)
try:
await self._tibber_connection.fetch_consumption_data_active_homes()
await self._tibber_connection.fetch_production_data_active_homes()
await tibber_connection.fetch_consumption_data_active_homes()
await tibber_connection.fetch_production_data_active_homes()
await self._insert_statistics()
except tibber.RetryableHttpExceptionError as err:
raise UpdateFailed(f"Error communicating with API ({err.status})") from err
@@ -75,7 +131,10 @@ class TibberDataCoordinator(DataUpdateCoordinator[None]):
async def _insert_statistics(self) -> None:
"""Insert Tibber statistics."""
for home in self._tibber_connection.get_homes():
tibber_connection = await self.config_entry.runtime_data.async_get_client(
self.hass
)
for home in tibber_connection.get_homes():
sensors: list[tuple[str, bool, str | None, str]] = []
if home.hourly_consumption_data:
sensors.append(
@@ -194,6 +253,76 @@ class TibberDataCoordinator(DataUpdateCoordinator[None]):
async_add_external_statistics(self.hass, metadata, statistics)
class TibberPriceCoordinator(DataUpdateCoordinator[dict[str, TibberHomeData]]):
"""Handle Tibber price data and insert statistics."""
config_entry: TibberConfigEntry
def __init__(
self,
hass: HomeAssistant,
config_entry: TibberConfigEntry,
) -> None:
"""Initialize the price coordinator."""
super().__init__(
hass,
_LOGGER,
config_entry=config_entry,
name=f"{DOMAIN} price",
update_interval=timedelta(minutes=1),
)
def _seconds_until_next_15_minute(self) -> float:
"""Return seconds until the next 15-minute boundary (0, 15, 30, 45) in UTC."""
now = dt_util.utcnow()
next_minute = ((now.minute // 15) + 1) * 15
if next_minute >= 60:
next_run = now.replace(minute=0, second=0, microsecond=0) + timedelta(
hours=1
)
else:
next_run = now.replace(
minute=next_minute, second=0, microsecond=0, tzinfo=dt_util.UTC
)
return (next_run - now).total_seconds()
async def _async_update_data(self) -> dict[str, TibberHomeData]:
"""Update data via API and return per-home data for sensors."""
tibber_connection = await self.config_entry.runtime_data.async_get_client(
self.hass
)
active_homes = tibber_connection.get_homes(only_active=True)
try:
await asyncio.gather(
tibber_connection.fetch_consumption_data_active_homes(),
tibber_connection.fetch_production_data_active_homes(),
)
now = dt_util.now()
homes_to_update = [
home
for home in active_homes
if (
(last_data_timestamp := home.last_data_timestamp) is None
or (last_data_timestamp - now).total_seconds() < 11 * 3600
)
]
if homes_to_update:
await asyncio.gather(
*(home.update_info_and_price_info() for home in homes_to_update)
)
except tibber.RetryableHttpExceptionError as err:
raise UpdateFailed(f"Error communicating with API ({err.status})") from err
except tibber.FatalHttpExceptionError as err:
raise UpdateFailed(f"Error communicating with API ({err.status})") from err
result = {home.home_id: _build_home_data(home) for home in active_homes}
self.update_interval = timedelta(seconds=self._seconds_until_next_15_minute())
return result
class TibberDataAPICoordinator(DataUpdateCoordinator[dict[str, TibberDevice]]):
"""Fetch and cache Tibber Data API device capabilities."""

View File

@@ -3,10 +3,8 @@
from __future__ import annotations
from collections.abc import Callable
import datetime
from datetime import timedelta
import logging
from random import randrange
from typing import Any
import aiohttp
@@ -42,18 +40,20 @@ from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)
from homeassistant.util import Throttle, dt as dt_util
from homeassistant.util import dt as dt_util
from .const import DOMAIN, MANUFACTURER, TibberConfigEntry
from .coordinator import TibberDataAPICoordinator, TibberDataCoordinator
from .coordinator import (
TibberDataAPICoordinator,
TibberDataCoordinator,
TibberPriceCoordinator,
)
_LOGGER = logging.getLogger(__name__)
ICON = "mdi:currency-usd"
SCAN_INTERVAL = timedelta(minutes=1)
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5)
PARALLEL_UPDATES = 0
TWENTY_MINUTES = 20 * 60
RT_SENSORS_UNIQUE_ID_MIGRATION = {
"accumulated_consumption_last_hour": "accumulated consumption current hour",
@@ -610,6 +610,7 @@ async def _async_setup_graphql_sensors(
entity_registry = er.async_get(hass)
coordinator: TibberDataCoordinator | None = None
price_coordinator: TibberPriceCoordinator | None = None
entities: list[TibberSensor] = []
for home in tibber_connection.get_homes(only_active=False):
try:
@@ -626,7 +627,9 @@ async def _async_setup_graphql_sensors(
raise PlatformNotReady from err
if home.has_active_subscription:
entities.append(TibberSensorElPrice(home))
if price_coordinator is None:
price_coordinator = TibberPriceCoordinator(hass, entry)
entities.append(TibberSensorElPrice(price_coordinator, home))
if coordinator is None:
coordinator = TibberDataCoordinator(hass, entry, tibber_connection)
entities.extend(
@@ -737,19 +740,21 @@ class TibberSensor(SensorEntity):
return device_info
class TibberSensorElPrice(TibberSensor):
class TibberSensorElPrice(TibberSensor, CoordinatorEntity[TibberPriceCoordinator]):
"""Representation of a Tibber sensor for el price."""
_attr_state_class = SensorStateClass.MEASUREMENT
_attr_translation_key = "electricity_price"
def __init__(self, tibber_home: TibberHome) -> None:
def __init__(
self,
coordinator: TibberPriceCoordinator,
tibber_home: TibberHome,
) -> None:
"""Initialize the sensor."""
super().__init__(tibber_home=tibber_home)
self._last_updated: datetime.datetime | None = None
self._spread_load_constant = randrange(TWENTY_MINUTES)
super().__init__(coordinator=coordinator, tibber_home=tibber_home)
self._attr_available = False
self._attr_native_unit_of_measurement = tibber_home.price_unit
self._attr_extra_state_attributes = {
"app_nickname": None,
"grid_company": None,
@@ -768,51 +773,38 @@ class TibberSensorElPrice(TibberSensor):
self._device_name = self._home_name
async def async_update(self) -> None:
"""Get the latest data and updates the states."""
now = dt_util.now()
if (
not self._tibber_home.last_data_timestamp
or (self._tibber_home.last_data_timestamp - now).total_seconds()
< 10 * 3600 - self._spread_load_constant
or not self.available
):
_LOGGER.debug("Asking for new data")
await self._fetch_data()
elif (
self._tibber_home.price_total
and self._last_updated
and self._last_updated.hour == now.hour
and now - self._last_updated < timedelta(minutes=15)
and self._tibber_home.last_data_timestamp
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
data = self.coordinator.data
if not data or (
(home_data := data.get(self._tibber_home.home_id)) is None
or (current_price := home_data.get("current_price")) is None
):
self._attr_available = False
self.async_write_ha_state()
return
res = self._tibber_home.current_price_data()
self._attr_native_value, self._last_updated, price_rank = res
self._attr_extra_state_attributes["intraday_price_ranking"] = price_rank
attrs = self._tibber_home.current_attributes()
self._attr_extra_state_attributes.update(attrs)
self._attr_available = self._attr_native_value is not None
self._attr_native_unit_of_measurement = self._tibber_home.price_unit
@Throttle(MIN_TIME_BETWEEN_UPDATES)
async def _fetch_data(self) -> None:
_LOGGER.debug("Fetching data")
try:
await self._tibber_home.update_info_and_price_info()
except TimeoutError, aiohttp.ClientError:
return
data = self._tibber_home.info["viewer"]["home"]
self._attr_extra_state_attributes["app_nickname"] = data["appNickname"]
self._attr_extra_state_attributes["grid_company"] = data["meteringPointData"][
"gridCompany"
self._attr_native_unit_of_measurement = home_data.get(
"price_unit", self._tibber_home.price_unit
)
self._attr_native_value = current_price
self._attr_extra_state_attributes["intraday_price_ranking"] = home_data.get(
"intraday_price_ranking"
)
self._attr_extra_state_attributes["max_price"] = home_data["max_price"]
self._attr_extra_state_attributes["avg_price"] = home_data["avg_price"]
self._attr_extra_state_attributes["min_price"] = home_data["min_price"]
self._attr_extra_state_attributes["off_peak_1"] = home_data["off_peak_1"]
self._attr_extra_state_attributes["peak"] = home_data["peak"]
self._attr_extra_state_attributes["off_peak_2"] = home_data["off_peak_2"]
self._attr_extra_state_attributes["app_nickname"] = home_data["app_nickname"]
self._attr_extra_state_attributes["grid_company"] = home_data["grid_company"]
self._attr_extra_state_attributes["estimated_annual_consumption"] = home_data[
"estimated_annual_consumption"
]
self._attr_extra_state_attributes["estimated_annual_consumption"] = data[
"meteringPointData"
]["estimatedAnnualConsumption"]
self._attr_available = True
self.async_write_ha_state()
class TibberDataSensor(TibberSensor, CoordinatorEntity[TibberDataCoordinator]):

View File

@@ -2,9 +2,9 @@
from __future__ import annotations
from collections.abc import Generator
import contextlib
from collections.abc import Callable
import logging
from typing import Any
from pywemo.exceptions import ActionException
@@ -64,23 +64,20 @@ class WemoEntity(CoordinatorEntity[DeviceCoordinator]):
"""Return the device info."""
return self._device_info
@contextlib.contextmanager
def _wemo_call_wrapper(self, message: str) -> Generator[None]:
"""Wrap calls to the device that change its state.
async def _async_wemo_call(self, message: str, action: Callable[[], Any]) -> None:
"""Run a WeMo device action in the executor and update listeners.
1. Takes care of making available=False when communications with the
device fails.
2. Ensures all entities sharing the same coordinator are aware of
updates to the device state.
Handles errors from the device and ensures all entities sharing the
same coordinator are aware of updates to the device state.
"""
try:
yield
await self.hass.async_add_executor_job(action)
except ActionException as err:
_LOGGER.warning("Could not %s for %s (%s)", message, self.name, err)
self.coordinator.last_exception = err
self.coordinator.last_update_success = False # Used for self.available.
self.coordinator.last_update_success = False
finally:
self.hass.add_job(self.coordinator.async_update_listeners)
self.coordinator.async_update_listeners()
class WemoBinaryStateEntity(WemoEntity):

View File

@@ -3,6 +3,7 @@
from __future__ import annotations
from datetime import timedelta
import functools as ft
import math
from typing import Any
@@ -60,14 +61,16 @@ async def async_setup_entry(
platform = entity_platform.async_get_current_platform()
# This will call WemoHumidifier.set_humidity(target_humidity=VALUE)
# This will call WemoHumidifier.async_set_humidity(target_humidity=VALUE)
platform.async_register_entity_service(
SERVICE_SET_HUMIDITY, SET_HUMIDITY_SCHEMA, WemoHumidifier.set_humidity.__name__
SERVICE_SET_HUMIDITY,
SET_HUMIDITY_SCHEMA,
WemoHumidifier.async_set_humidity.__name__,
)
# This will call WemoHumidifier.reset_filter_life()
# This will call WemoHumidifier.async_reset_filter_life()
platform.async_register_entity_service(
SERVICE_RESET_FILTER_LIFE, None, WemoHumidifier.reset_filter_life.__name__
SERVICE_RESET_FILTER_LIFE, None, WemoHumidifier.async_reset_filter_life.__name__
)
@@ -124,25 +127,26 @@ class WemoHumidifier(WemoBinaryStateEntity, FanEntity):
self._last_fan_on_mode = self.wemo.fan_mode
super()._handle_coordinator_update()
def turn_on(
async def async_turn_on(
self,
percentage: int | None = None,
preset_mode: str | None = None,
**kwargs: Any,
) -> None:
"""Turn the fan on."""
self._set_percentage(percentage)
await self._async_set_percentage(percentage)
def turn_off(self, **kwargs: Any) -> None:
"""Turn the switch off."""
with self._wemo_call_wrapper("turn off"):
self.wemo.set_state(FanMode.Off)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the fan off."""
await self._async_wemo_call(
"turn off", ft.partial(self.wemo.set_state, FanMode.Off)
)
def set_percentage(self, percentage: int) -> None:
async def async_set_percentage(self, percentage: int) -> None:
"""Set the fan_mode of the Humidifier."""
self._set_percentage(percentage)
await self._async_set_percentage(percentage)
def _set_percentage(self, percentage: int | None) -> None:
async def _async_set_percentage(self, percentage: int | None) -> None:
if percentage is None:
named_speed = self._last_fan_on_mode
elif percentage == 0:
@@ -152,10 +156,11 @@ class WemoHumidifier(WemoBinaryStateEntity, FanEntity):
math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage))
)
with self._wemo_call_wrapper("set speed"):
self.wemo.set_state(named_speed)
await self._async_wemo_call(
"set speed", ft.partial(self.wemo.set_state, named_speed)
)
def set_humidity(self, target_humidity: float) -> None:
async def async_set_humidity(self, target_humidity: float) -> None:
"""Set the target humidity level for the Humidifier."""
if target_humidity < 50:
pywemo_humidity = DesiredHumidity.FortyFivePercent
@@ -168,10 +173,10 @@ class WemoHumidifier(WemoBinaryStateEntity, FanEntity):
elif target_humidity >= 100:
pywemo_humidity = DesiredHumidity.OneHundredPercent
with self._wemo_call_wrapper("set humidity"):
self.wemo.set_humidity(pywemo_humidity)
await self._async_wemo_call(
"set humidity", ft.partial(self.wemo.set_humidity, pywemo_humidity)
)
def reset_filter_life(self) -> None:
async def async_reset_filter_life(self) -> None:
"""Reset the filter life to 100%."""
with self._wemo_call_wrapper("reset filter life"):
self.wemo.reset_filter_life()
await self._async_wemo_call("reset filter life", self.wemo.reset_filter_life)

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
import functools as ft
from typing import Any, cast
from pywemo import Bridge, BridgeLight, Dimmer
@@ -166,7 +167,7 @@ class WemoLight(WemoEntity, LightEntity):
"""Return true if device is on."""
return self.light.state.get("onoff", WEMO_OFF) != WEMO_OFF
def turn_on(self, **kwargs: Any) -> None:
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the light on."""
xy_color = None
@@ -184,7 +185,7 @@ class WemoLight(WemoEntity, LightEntity):
"force_update": False,
}
with self._wemo_call_wrapper("turn on"):
def _turn_on() -> None:
if xy_color is not None:
self.light.set_color(xy_color, transition=transition_time)
@@ -195,12 +196,14 @@ class WemoLight(WemoEntity, LightEntity):
self.light.turn_on(**turn_on_kwargs)
def turn_off(self, **kwargs: Any) -> None:
await self._async_wemo_call("turn on", _turn_on)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the light off."""
transition_time = int(kwargs.get(ATTR_TRANSITION, 0))
with self._wemo_call_wrapper("turn off"):
self.light.turn_off(transition=transition_time)
await self._async_wemo_call(
"turn off", ft.partial(self.light.turn_off, transition=transition_time)
)
class WemoDimmer(WemoBinaryStateEntity, LightEntity):
@@ -216,20 +219,19 @@ class WemoDimmer(WemoBinaryStateEntity, LightEntity):
wemo_brightness: int = self.wemo.get_brightness()
return int((wemo_brightness * 255) / 100)
def turn_on(self, **kwargs: Any) -> None:
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the dimmer on."""
# Wemo dimmer switches use a range of [0, 100] to control
# brightness. Level 255 might mean to set it to previous value
if ATTR_BRIGHTNESS in kwargs:
brightness = kwargs[ATTR_BRIGHTNESS]
brightness = int((brightness / 255) * 100)
with self._wemo_call_wrapper("set brightness"):
self.wemo.set_brightness(brightness)
await self._async_wemo_call(
"set brightness", ft.partial(self.wemo.set_brightness, brightness)
)
else:
with self._wemo_call_wrapper("turn on"):
self.wemo.on()
await self._async_wemo_call("turn on", self.wemo.on)
def turn_off(self, **kwargs: Any) -> None:
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the dimmer off."""
with self._wemo_call_wrapper("turn off"):
self.wemo.off()
await self._async_wemo_call("turn off", self.wemo.off)

View File

@@ -119,12 +119,10 @@ class WemoSwitch(WemoBinaryStateEntity, SwitchEntity):
return "mdi:coffee"
return None
def turn_on(self, **kwargs: Any) -> None:
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the switch on."""
with self._wemo_call_wrapper("turn on"):
self.wemo.on()
await self._async_wemo_call("turn on", self.wemo.on)
def turn_off(self, **kwargs: Any) -> None:
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the switch off."""
with self._wemo_call_wrapper("turn off"):
self.wemo.off()
await self._async_wemo_call("turn off", self.wemo.off)

View File

@@ -25,9 +25,9 @@ from tests.components.common import (
@pytest.fixture
async def target_alarm_control_panels(hass: HomeAssistant) -> list[str]:
async def target_alarm_control_panels(hass: HomeAssistant) -> dict[str, list[str]]:
"""Create multiple alarm_control_panel entities associated with different targets."""
return (await target_entities(hass, "alarm_control_panel"))["included"]
return await target_entities(hass, "alarm_control_panel")
@pytest.mark.parametrize(
@@ -120,7 +120,7 @@ async def test_alarm_control_panel_conditions_gated_by_labs_flag(
)
async def test_alarm_control_panel_state_condition_behavior_any(
hass: HomeAssistant,
target_alarm_control_panels: list[str],
target_alarm_control_panels: dict[str, list[str]],
condition_target_config: dict,
entity_id: str,
entities_in_target: int,
@@ -129,10 +129,10 @@ 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) - {entity_id}
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:
for eid in target_alarm_control_panels["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
@@ -227,7 +227,7 @@ async def test_alarm_control_panel_state_condition_behavior_any(
)
async def test_alarm_control_panel_state_condition_behavior_all(
hass: HomeAssistant,
target_alarm_control_panels: list[str],
target_alarm_control_panels: dict[str, list[str]],
condition_target_config: dict,
entity_id: str,
entities_in_target: int,
@@ -236,10 +236,10 @@ 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) - {entity_id}
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:
for eid in target_alarm_control_panels["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()

View File

@@ -8,19 +8,18 @@ from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntityFeature,
AlarmControlPanelState,
)
from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_ENTITY_ID
from homeassistant.const import ATTR_SUPPORTED_FEATURES
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,
other_states,
parametrize_target_entities,
parametrize_trigger_states,
set_or_remove_state,
target_entities,
)
@@ -331,25 +330,14 @@ async def test_alarm_control_panel_state_trigger_behavior_last(
states: list[TriggerStateDescription],
) -> None:
"""Test that the alarm_control_panel state trigger fires when the last alarm_control_panel changes to a specific state."""
other_entity_ids = set(target_alarm_control_panels["included"]) - {entity_id}
# Set all alarm control panels, including the tested one, 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()
await arm_trigger(hass, trigger, {"behavior": "last"}, trigger_target_config)
for state in states[1:]:
included_state = state["included"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == 0
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == state["count"]
for service_call in service_calls:
assert service_call.data[CONF_ENTITY_ID] == entity_id
service_calls.clear()
await assert_trigger_behavior_last(
hass,
service_calls=service_calls,
target_entities=target_alarm_control_panels,
trigger_target_config=trigger_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
trigger=trigger,
trigger_options=trigger_options,
states=states,
)

View File

@@ -21,9 +21,9 @@ from tests.components.common import (
@pytest.fixture
async def target_assist_satellites(hass: HomeAssistant) -> list[str]:
async def target_assist_satellites(hass: HomeAssistant) -> dict[str, list[str]]:
"""Create multiple assist satellite entities associated with different targets."""
return (await target_entities(hass, "assist_satellite"))["included"]
return await target_entities(hass, "assist_satellite")
@pytest.mark.parametrize(
@@ -74,7 +74,7 @@ async def test_assist_satellite_conditions_gated_by_labs_flag(
)
async def test_assist_satellite_state_condition_behavior_any(
hass: HomeAssistant,
target_assist_satellites: list[str],
target_assist_satellites: dict[str, list[str]],
condition_target_config: dict,
entity_id: str,
entities_in_target: int,
@@ -83,10 +83,10 @@ 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) - {entity_id}
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:
for eid in target_assist_satellites["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
@@ -142,7 +142,7 @@ async def test_assist_satellite_state_condition_behavior_any(
)
async def test_assist_satellite_state_condition_behavior_all(
hass: HomeAssistant,
target_assist_satellites: list[str],
target_assist_satellites: dict[str, list[str]],
condition_target_config: dict,
entity_id: str,
entities_in_target: int,
@@ -151,10 +151,10 @@ 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) - {entity_id}
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:
for eid in target_assist_satellites["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()

View File

@@ -5,19 +5,17 @@ from typing import Any
import pytest
from homeassistant.components.assist_satellite.entity import AssistSatelliteState
from homeassistant.const import CONF_ENTITY_ID
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,
other_states,
parametrize_target_entities,
parametrize_trigger_states,
set_or_remove_state,
target_entities,
)
@@ -196,25 +194,14 @@ async def test_assist_satellite_state_trigger_behavior_last(
states: list[TriggerStateDescription],
) -> None:
"""Test that the assist_satellite state trigger fires when the last assist_satellite changes to a specific state."""
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()
await arm_trigger(hass, trigger, {"behavior": "last"}, trigger_target_config)
for state in states[1:]:
included_state = state["included"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == 0
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == state["count"]
for service_call in service_calls:
assert service_call.data[CONF_ENTITY_ID] == entity_id
service_calls.clear()
await assert_trigger_behavior_last(
hass,
service_calls=service_calls,
target_entities=target_assist_satellites,
trigger_target_config=trigger_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
trigger=trigger,
trigger_options=trigger_options,
states=states,
)

View File

@@ -25,9 +25,9 @@ from tests.components.common import (
@pytest.fixture
async def target_climates(hass: HomeAssistant) -> list[str]:
async def target_climates(hass: HomeAssistant) -> dict[str, list[str]]:
"""Create multiple climate entities associated with different targets."""
return (await target_entities(hass, "climate"))["included"]
return await target_entities(hass, "climate")
@pytest.mark.parametrize(
@@ -76,7 +76,7 @@ async def test_climate_conditions_gated_by_labs_flag(
)
async def test_climate_state_condition_behavior_any(
hass: HomeAssistant,
target_climates: list[str],
target_climates: dict[str, list[str]],
condition_target_config: dict,
entity_id: str,
entities_in_target: int,
@@ -85,10 +85,10 @@ 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) - {entity_id}
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:
for eid in target_climates["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
@@ -141,7 +141,7 @@ async def test_climate_state_condition_behavior_any(
)
async def test_climate_state_condition_behavior_all(
hass: HomeAssistant,
target_climates: list[str],
target_climates: dict[str, list[str]],
condition_target_config: dict,
entity_id: str,
entities_in_target: int,
@@ -150,10 +150,10 @@ 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) - {entity_id}
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:
for eid in target_climates["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
@@ -205,7 +205,7 @@ async def test_climate_state_condition_behavior_all(
)
async def test_climate_attribute_condition_behavior_any(
hass: HomeAssistant,
target_climates: list[str],
target_climates: dict[str, list[str]],
condition_target_config: dict,
entity_id: str,
entities_in_target: int,
@@ -214,10 +214,10 @@ 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) - {entity_id}
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:
for eid in target_climates["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
@@ -268,7 +268,7 @@ async def test_climate_attribute_condition_behavior_any(
)
async def test_climate_attribute_condition_behavior_all(
hass: HomeAssistant,
target_climates: list[str],
target_climates: dict[str, list[str]],
condition_target_config: dict,
entity_id: str,
entities_in_target: int,
@@ -277,10 +277,10 @@ 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) - {entity_id}
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:
for eid in target_climates["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()

View File

@@ -24,16 +24,15 @@ from homeassistant.helpers.trigger import async_validate_trigger_config
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,
other_states,
parametrize_numerical_attribute_changed_trigger_states,
parametrize_numerical_attribute_crossed_threshold_trigger_states,
parametrize_target_entities,
parametrize_trigger_states,
set_or_remove_state,
target_entities,
)
@@ -409,31 +408,18 @@ async def test_climate_state_trigger_behavior_last(
states: list[TriggerStateDescription],
) -> None:
"""Test that the climate state trigger fires when the last climate changes to a specific state."""
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()
await arm_trigger(
hass, trigger, {"behavior": "last"} | trigger_options, trigger_target_config
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,
)
for state in states[1:]:
included_state = state["included"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == 0
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == state["count"]
for service_call in service_calls:
assert service_call.data[CONF_ENTITY_ID] == entity_id
service_calls.clear()
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
@@ -480,27 +466,14 @@ async def test_climate_state_attribute_trigger_behavior_last(
states: list[tuple[tuple[str, dict], int]],
) -> None:
"""Test that the climate state trigger fires when the last climate state changes to a specific state."""
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()
await arm_trigger(
hass, trigger, {"behavior": "last"} | trigger_options, trigger_target_config
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,
)
for state in states[1:]:
included_state = state["included"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == 0
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == state["count"]
for service_call in service_calls:
assert service_call.data[CONF_ENTITY_ID] == entity_id
service_calls.clear()

View File

@@ -953,3 +953,51 @@ async def assert_trigger_behavior_first(
set_or_remove_state(hass, excluded_entity_id, excluded_state)
await hass.async_block_till_done()
assert len(service_calls) == 0
async def assert_trigger_behavior_last(
hass: HomeAssistant,
*,
service_calls: list[ServiceCall],
target_entities: 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 trigger fires in mode last."""
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()
await arm_trigger(
hass, trigger, {"behavior": "last"} | trigger_options, trigger_target_config
)
for state in states[1:]:
excluded_state = state["excluded"]
included_state = state["included"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == 0
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == state["count"]
for service_call in service_calls:
assert service_call.data[CONF_ENTITY_ID] == entity_id
service_calls.clear()
for excluded_entity_id in excluded_entity_ids:
set_or_remove_state(hass, excluded_entity_id, excluded_state)
await hass.async_block_till_done()
assert len(service_calls) == 0

View File

@@ -13,10 +13,10 @@ from tests.components.common import (
arm_trigger,
assert_trigger_behavior_any,
assert_trigger_behavior_first,
assert_trigger_behavior_last,
assert_trigger_gated_by_labs_flag,
parametrize_target_entities,
parametrize_trigger_states,
set_or_remove_state,
target_entities,
)
@@ -259,37 +259,17 @@ async def test_cover_trigger_behavior_last(
states: list[TriggerStateDescription],
) -> None:
"""Test cover trigger fires when the last cover changes state."""
other_entity_ids = set(target_covers["included"]) - {entity_id}
excluded_entity_ids = set(target_covers["excluded"]) - {entity_id}
for eid in target_covers["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
for eid in excluded_entity_ids:
set_or_remove_state(hass, eid, states[0]["excluded"])
await hass.async_block_till_done()
await arm_trigger(hass, trigger, {"behavior": "last"}, trigger_target_config)
for state in states[1:]:
excluded_state = state["excluded"]
included_state = state["included"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, excluded_state)
await hass.async_block_till_done()
assert len(service_calls) == 0
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == state["count"]
for service_call in service_calls:
assert service_call.data[CONF_ENTITY_ID] == entity_id
service_calls.clear()
for excluded_entity_id in excluded_entity_ids:
set_or_remove_state(hass, excluded_entity_id, excluded_state)
await hass.async_block_till_done()
assert len(service_calls) == 0
await assert_trigger_behavior_last(
hass,
service_calls=service_calls,
target_entities=target_covers,
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")

View File

@@ -20,9 +20,9 @@ from tests.components.common import (
@pytest.fixture
async def target_device_trackers(hass: HomeAssistant) -> list[str]:
async def target_device_trackers(hass: HomeAssistant) -> dict[str, list[str]]:
"""Create multiple device tracker entities associated with different targets."""
return (await target_entities(hass, "device_tracker"))["included"]
return await target_entities(hass, "device_tracker")
@pytest.mark.parametrize(
@@ -61,7 +61,7 @@ async def test_device_tracker_conditions_gated_by_labs_flag(
)
async def test_device_tracker_state_condition_behavior_any(
hass: HomeAssistant,
target_device_trackers: list[str],
target_device_trackers: dict[str, list[str]],
condition_target_config: dict,
entity_id: str,
entities_in_target: int,
@@ -70,10 +70,10 @@ 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) - {entity_id}
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:
for eid in target_device_trackers["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
@@ -119,7 +119,7 @@ async def test_device_tracker_state_condition_behavior_any(
)
async def test_device_tracker_state_condition_behavior_all(
hass: HomeAssistant,
target_device_trackers: list[str],
target_device_trackers: dict[str, list[str]],
condition_target_config: dict,
entity_id: str,
entities_in_target: int,
@@ -128,10 +128,10 @@ 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) - {entity_id}
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:
for eid in target_device_trackers["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()

View File

@@ -4,18 +4,17 @@ from typing import Any
import pytest
from homeassistant.const import CONF_ENTITY_ID, STATE_HOME, STATE_NOT_HOME
from homeassistant.const import STATE_HOME, STATE_NOT_HOME
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_target_entities,
parametrize_trigger_states,
set_or_remove_state,
target_entities,
)
@@ -161,25 +160,14 @@ async def test_device_tracker_state_trigger_behavior_last(
states: list[TriggerStateDescription],
) -> None:
"""Test that the device_tracker home triggers when the last device_tracker changes to a specific state."""
other_entity_ids = set(target_device_trackers["included"]) - {entity_id}
# Set all device_trackers, including the tested device_tracker, 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()
await arm_trigger(hass, trigger, {"behavior": "last"}, trigger_target_config)
for state in states[1:]:
included_state = state["included"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == 0
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == state["count"]
for service_call in service_calls:
assert service_call.data[CONF_ENTITY_ID] == entity_id
service_calls.clear()
await assert_trigger_behavior_last(
hass,
service_calls=service_calls,
target_entities=target_device_trackers,
trigger_target_config=trigger_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
trigger=trigger,
trigger_options=trigger_options,
states=states,
)

View File

@@ -13,10 +13,10 @@ from tests.components.common import (
arm_trigger,
assert_trigger_behavior_any,
assert_trigger_behavior_first,
assert_trigger_behavior_last,
assert_trigger_gated_by_labs_flag,
parametrize_target_entities,
parametrize_trigger_states,
set_or_remove_state,
target_entities,
)
@@ -251,37 +251,17 @@ async def test_door_trigger_binary_sensor_behavior_last(
states: list[TriggerStateDescription],
) -> None:
"""Test door trigger fires when the last binary_sensor changes state."""
other_entity_ids = set(target_binary_sensors["included"]) - {entity_id}
excluded_entity_ids = set(target_binary_sensors["excluded"]) - {entity_id}
for eid in target_binary_sensors["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
for eid in excluded_entity_ids:
set_or_remove_state(hass, eid, states[0]["excluded"])
await hass.async_block_till_done()
await arm_trigger(hass, trigger, {"behavior": "last"}, trigger_target_config)
for state in states[1:]:
excluded_state = state["excluded"]
included_state = state["included"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, excluded_state)
await hass.async_block_till_done()
assert len(service_calls) == 0
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == state["count"]
for service_call in service_calls:
assert service_call.data[CONF_ENTITY_ID] == entity_id
service_calls.clear()
for excluded_entity_id in excluded_entity_ids:
set_or_remove_state(hass, excluded_entity_id, excluded_state)
await hass.async_block_till_done()
assert len(service_calls) == 0
await assert_trigger_behavior_last(
hass,
service_calls=service_calls,
target_entities=target_binary_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")
@@ -411,37 +391,17 @@ async def test_door_trigger_cover_behavior_last(
states: list[TriggerStateDescription],
) -> None:
"""Test door trigger fires when the last cover changes state."""
other_entity_ids = set(target_covers["included"]) - {entity_id}
excluded_entity_ids = set(target_covers["excluded"]) - {entity_id}
for eid in target_covers["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
for eid in excluded_entity_ids:
set_or_remove_state(hass, eid, states[0]["excluded"])
await hass.async_block_till_done()
await arm_trigger(hass, trigger, {"behavior": "last"}, trigger_target_config)
for state in states[1:]:
excluded_state = state["excluded"]
included_state = state["included"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, excluded_state)
await hass.async_block_till_done()
assert len(service_calls) == 0
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == state["count"]
for service_call in service_calls:
assert service_call.data[CONF_ENTITY_ID] == entity_id
service_calls.clear()
for excluded_entity_id in excluded_entity_ids:
set_or_remove_state(hass, excluded_entity_id, excluded_state)
await hass.async_block_till_done()
assert len(service_calls) == 0
await assert_trigger_behavior_last(
hass,
service_calls=service_calls,
target_entities=target_covers,
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")

View File

@@ -20,19 +20,19 @@ from tests.components.common import (
@pytest.fixture
async def target_fans(hass: HomeAssistant) -> list[str]:
async def target_fans(hass: HomeAssistant) -> dict[str, list[str]]:
"""Create multiple fan entities associated with different targets."""
return (await target_entities(hass, "fan"))["included"]
return await target_entities(hass, "fan")
@pytest.fixture
async def target_switches(hass: HomeAssistant) -> list[str]:
async def target_switches(hass: HomeAssistant) -> dict[str, list[str]]:
"""Create multiple switch entities associated with different targets.
Note: The switches are used to ensure that only fan entities are considered
in the condition evaluation and not other toggle entities.
"""
return (await target_entities(hass, "switch"))["included"]
return await target_entities(hass, "switch")
@pytest.mark.parametrize(
@@ -71,8 +71,8 @@ async def test_fan_conditions_gated_by_labs_flag(
)
async def test_fan_state_condition_behavior_any(
hass: HomeAssistant,
target_fans: list[str],
target_switches: list[str],
target_fans: dict[str, list[str]],
target_switches: dict[str, list[str]],
condition_target_config: dict,
entity_id: str,
entities_in_target: int,
@@ -81,10 +81,10 @@ async def test_fan_state_condition_behavior_any(
states: list[ConditionStateDescription],
) -> None:
"""Test the fan state condition with the 'any' behavior."""
other_entity_ids = set(target_fans) - {entity_id}
other_entity_ids = set(target_fans["included"]) - {entity_id}
# Set all fans, including the tested fan, to the initial state
for eid in target_fans:
for eid in target_fans["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
@@ -97,7 +97,7 @@ async def test_fan_state_condition_behavior_any(
# Set state for switches to ensure that they don't impact the condition
for state in states:
for eid in target_switches:
for eid in target_switches["included"]:
set_or_remove_state(hass, eid, state["included"])
await hass.async_block_till_done()
assert condition(hass) is False
@@ -137,7 +137,7 @@ async def test_fan_state_condition_behavior_any(
)
async def test_fan_state_condition_behavior_all(
hass: HomeAssistant,
target_fans: list[str],
target_fans: dict[str, list[str]],
condition_target_config: dict,
entity_id: str,
entities_in_target: int,
@@ -150,10 +150,10 @@ async def test_fan_state_condition_behavior_all(
hass.states.async_set("switch.label_switch_1", STATE_OFF)
hass.states.async_set("switch.label_switch_2", STATE_ON)
other_entity_ids = set(target_fans) - {entity_id}
other_entity_ids = set(target_fans["included"]) - {entity_id}
# Set all fans, including the tested fan, to the initial state
for eid in target_fans:
for eid in target_fans["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()

View File

@@ -4,18 +4,17 @@ from typing import Any
import pytest
from homeassistant.const import CONF_ENTITY_ID, STATE_OFF, STATE_ON
from homeassistant.const import STATE_OFF, STATE_ON
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_target_entities,
parametrize_trigger_states,
set_or_remove_state,
target_entities,
)
@@ -162,25 +161,14 @@ async def test_fan_state_trigger_behavior_last(
states: list[TriggerStateDescription],
) -> None:
"""Test that the fan state trigger fires when the last fan changes to a specific state."""
other_entity_ids = set(target_fans["included"]) - {entity_id}
# Set all fans, including the tested fan, to the initial state
for eid in target_fans["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
await arm_trigger(hass, trigger, {"behavior": "last"}, trigger_target_config)
for state in states[1:]:
included_state = state["included"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == 0
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == state["count"]
for service_call in service_calls:
assert service_call.data[CONF_ENTITY_ID] == entity_id
service_calls.clear()
await assert_trigger_behavior_last(
hass,
service_calls=service_calls,
target_entities=target_fans,
trigger_target_config=trigger_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
trigger=trigger,
trigger_options=trigger_options,
states=states,
)

View File

@@ -13,10 +13,10 @@ from tests.components.common import (
arm_trigger,
assert_trigger_behavior_any,
assert_trigger_behavior_first,
assert_trigger_behavior_last,
assert_trigger_gated_by_labs_flag,
parametrize_target_entities,
parametrize_trigger_states,
set_or_remove_state,
target_entities,
)
@@ -251,37 +251,17 @@ async def test_garage_door_trigger_binary_sensor_behavior_last(
states: list[TriggerStateDescription],
) -> None:
"""Test garage door trigger fires when the last binary_sensor changes state."""
other_entity_ids = set(target_binary_sensors["included"]) - {entity_id}
excluded_entity_ids = set(target_binary_sensors["excluded"]) - {entity_id}
for eid in target_binary_sensors["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
for eid in excluded_entity_ids:
set_or_remove_state(hass, eid, states[0]["excluded"])
await hass.async_block_till_done()
await arm_trigger(hass, trigger, {"behavior": "last"}, trigger_target_config)
for state in states[1:]:
excluded_state = state["excluded"]
included_state = state["included"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, excluded_state)
await hass.async_block_till_done()
assert len(service_calls) == 0
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == state["count"]
for service_call in service_calls:
assert service_call.data[CONF_ENTITY_ID] == entity_id
service_calls.clear()
for excluded_entity_id in excluded_entity_ids:
set_or_remove_state(hass, excluded_entity_id, excluded_state)
await hass.async_block_till_done()
assert len(service_calls) == 0
await assert_trigger_behavior_last(
hass,
service_calls=service_calls,
target_entities=target_binary_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")
@@ -411,37 +391,17 @@ async def test_garage_door_trigger_cover_behavior_last(
states: list[TriggerStateDescription],
) -> None:
"""Test garage door trigger fires when the last cover changes state."""
other_entity_ids = set(target_covers["included"]) - {entity_id}
excluded_entity_ids = set(target_covers["excluded"]) - {entity_id}
for eid in target_covers["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
for eid in excluded_entity_ids:
set_or_remove_state(hass, eid, states[0]["excluded"])
await hass.async_block_till_done()
await arm_trigger(hass, trigger, {"behavior": "last"}, trigger_target_config)
for state in states[1:]:
excluded_state = state["excluded"]
included_state = state["included"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, excluded_state)
await hass.async_block_till_done()
assert len(service_calls) == 0
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == state["count"]
for service_call in service_calls:
assert service_call.data[CONF_ENTITY_ID] == entity_id
service_calls.clear()
for excluded_entity_id in excluded_entity_ids:
set_or_remove_state(hass, excluded_entity_id, excluded_state)
await hass.async_block_till_done()
assert len(service_calls) == 0
await assert_trigger_behavior_last(
hass,
service_calls=service_calls,
target_entities=target_covers,
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")

View File

@@ -13,10 +13,10 @@ from tests.components.common import (
arm_trigger,
assert_trigger_behavior_any,
assert_trigger_behavior_first,
assert_trigger_behavior_last,
assert_trigger_gated_by_labs_flag,
parametrize_target_entities,
parametrize_trigger_states,
set_or_remove_state,
target_entities,
)
@@ -238,37 +238,17 @@ async def test_gate_trigger_cover_behavior_last(
states: list[TriggerStateDescription],
) -> None:
"""Test gate trigger fires when the last cover changes state."""
other_entity_ids = set(target_covers["included"]) - {entity_id}
excluded_entity_ids = set(target_covers["excluded"]) - {entity_id}
for eid in target_covers["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
for eid in excluded_entity_ids:
set_or_remove_state(hass, eid, states[0]["excluded"])
await hass.async_block_till_done()
await arm_trigger(hass, trigger, {"behavior": "last"}, trigger_target_config)
for state in states[1:]:
excluded_state = state["excluded"]
included_state = state["included"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, excluded_state)
await hass.async_block_till_done()
assert len(service_calls) == 0
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == state["count"]
for service_call in service_calls:
assert service_call.data[CONF_ENTITY_ID] == entity_id
service_calls.clear()
for excluded_entity_id in excluded_entity_ids:
set_or_remove_state(hass, excluded_entity_id, excluded_state)
await hass.async_block_till_done()
assert len(service_calls) == 0
await assert_trigger_behavior_last(
hass,
service_calls=service_calls,
target_entities=target_covers,
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")

View File

@@ -21,9 +21,9 @@ from tests.components.common import (
@pytest.fixture
async def target_humidifiers(hass: HomeAssistant) -> list[str]:
async def target_humidifiers(hass: HomeAssistant) -> dict[str, list[str]]:
"""Create multiple humidifier entities associated with different targets."""
return (await target_entities(hass, "humidifier"))["included"]
return await target_entities(hass, "humidifier")
@pytest.mark.parametrize(
@@ -64,7 +64,7 @@ async def test_humidifier_conditions_gated_by_labs_flag(
)
async def test_humidifier_state_condition_behavior_any(
hass: HomeAssistant,
target_humidifiers: list[str],
target_humidifiers: dict[str, list[str]],
condition_target_config: dict,
entity_id: str,
entities_in_target: int,
@@ -73,10 +73,10 @@ 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) - {entity_id}
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:
for eid in target_humidifiers["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
@@ -122,7 +122,7 @@ async def test_humidifier_state_condition_behavior_any(
)
async def test_humidifier_state_condition_behavior_all(
hass: HomeAssistant,
target_humidifiers: list[str],
target_humidifiers: dict[str, list[str]],
condition_target_config: dict,
entity_id: str,
entities_in_target: int,
@@ -131,10 +131,10 @@ 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) - {entity_id}
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:
for eid in target_humidifiers["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
@@ -181,7 +181,7 @@ async def test_humidifier_state_condition_behavior_all(
)
async def test_humidifier_attribute_condition_behavior_any(
hass: HomeAssistant,
target_humidifiers: list[str],
target_humidifiers: dict[str, list[str]],
condition_target_config: dict,
entity_id: str,
entities_in_target: int,
@@ -190,10 +190,10 @@ 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) - {entity_id}
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:
for eid in target_humidifiers["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
@@ -239,7 +239,7 @@ async def test_humidifier_attribute_condition_behavior_any(
)
async def test_humidifier_attribute_condition_behavior_all(
hass: HomeAssistant,
target_humidifiers: list[str],
target_humidifiers: dict[str, list[str]],
condition_target_config: dict,
entity_id: str,
entities_in_target: int,
@@ -248,10 +248,10 @@ 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) - {entity_id}
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:
for eid in target_humidifiers["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()

View File

@@ -5,18 +5,17 @@ from typing import Any
import pytest
from homeassistant.components.humidifier.const import ATTR_ACTION, HumidifierAction
from homeassistant.const import CONF_ENTITY_ID, STATE_OFF, STATE_ON
from homeassistant.const import STATE_OFF, STATE_ON
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_target_entities,
parametrize_trigger_states,
set_or_remove_state,
target_entities,
)
@@ -255,28 +254,17 @@ async def test_humidifier_state_trigger_behavior_last(
states: list[TriggerStateDescription],
) -> None:
"""Test that the humidifier state trigger fires when the last humidifier changes to a specific state."""
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()
await arm_trigger(hass, trigger, {"behavior": "last"}, trigger_target_config)
for state in states[1:]:
included_state = state["included"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == 0
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == state["count"]
for service_call in service_calls:
assert service_call.data[CONF_ENTITY_ID] == entity_id
service_calls.clear()
await assert_trigger_behavior_last(
hass,
service_calls=service_calls,
target_entities=target_humidifiers,
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")
@@ -311,27 +299,14 @@ async def test_humidifier_state_attribute_trigger_behavior_last(
states: list[tuple[tuple[str, dict], int]],
) -> None:
"""Test that the humidifier state trigger fires when the last humidifier state changes to a specific state."""
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()
await arm_trigger(
hass, trigger, {"behavior": "last"} | trigger_options, trigger_target_config
await assert_trigger_behavior_last(
hass,
service_calls=service_calls,
target_entities=target_humidifiers,
trigger_target_config=trigger_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
trigger=trigger,
trigger_options=trigger_options,
states=states,
)
for state in states[1:]:
included_state = state["included"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == 0
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == state["count"]
for service_call in service_calls:
assert service_call.data[CONF_ENTITY_ID] == entity_id
service_calls.clear()

View File

@@ -20,13 +20,13 @@ from tests.components.common import (
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,
set_or_remove_state,
target_entities,
)
@@ -176,30 +176,18 @@ async def test_humidity_trigger_sensor_crossed_threshold_behavior_last(
states: list[TriggerStateDescription],
) -> None:
"""Test humidity crossed_threshold trigger fires when the last sensor changes state."""
other_entity_ids = set(target_sensors["included"]) - {entity_id}
for eid in target_sensors["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
await arm_trigger(
hass, trigger, {"behavior": "last"} | trigger_options, trigger_target_config
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,
)
for state in states[1:]:
included_state = state["included"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == 0
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == state["count"]
for service_call in service_calls:
assert service_call.data[CONF_ENTITY_ID] == entity_id
service_calls.clear()
# --- Climate domain tests (value in current_humidity attribute) ---
@@ -314,30 +302,18 @@ async def test_humidity_trigger_climate_crossed_threshold_behavior_last(
states: list[TriggerStateDescription],
) -> None:
"""Test humidity crossed_threshold trigger fires when the last climate changes state."""
other_entity_ids = set(target_climates["included"]) - {entity_id}
for eid in target_climates["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
await arm_trigger(
hass, trigger, {"behavior": "last"} | trigger_options, trigger_target_config
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,
)
for state in states[1:]:
included_state = state["included"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == 0
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == state["count"]
for service_call in service_calls:
assert service_call.data[CONF_ENTITY_ID] == entity_id
service_calls.clear()
# --- Humidifier domain tests (value in current_humidity attribute) ---
@@ -452,30 +428,18 @@ async def test_humidity_trigger_humidifier_crossed_threshold_behavior_last(
states: list[TriggerStateDescription],
) -> None:
"""Test humidity crossed_threshold trigger fires when the last humidifier changes state."""
other_entity_ids = set(target_humidifiers["included"]) - {entity_id}
for eid in target_humidifiers["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
await arm_trigger(
hass, trigger, {"behavior": "last"} | trigger_options, trigger_target_config
await assert_trigger_behavior_last(
hass,
service_calls=service_calls,
target_entities=target_humidifiers,
trigger_target_config=trigger_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
trigger=trigger,
trigger_options=trigger_options,
states=states,
)
for state in states[1:]:
included_state = state["included"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == 0
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == state["count"]
for service_call in service_calls:
assert service_call.data[CONF_ENTITY_ID] == entity_id
service_calls.clear()
# --- Weather domain tests (value in humidity attribute) ---
@@ -590,30 +554,18 @@ async def test_humidity_trigger_weather_crossed_threshold_behavior_last(
states: list[TriggerStateDescription],
) -> None:
"""Test humidity crossed_threshold trigger fires when the last weather changes state."""
other_entity_ids = set(target_weathers["included"]) - {entity_id}
for eid in target_weathers["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
await arm_trigger(
hass, trigger, {"behavior": "last"} | trigger_options, trigger_target_config
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,
)
for state in states[1:]:
included_state = state["included"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == 0
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == state["count"]
for service_call in service_calls:
assert service_call.data[CONF_ENTITY_ID] == entity_id
service_calls.clear()
# --- Device class exclusion test ---

View File

@@ -5,18 +5,17 @@ from typing import Any
import pytest
from homeassistant.components.input_boolean import DOMAIN
from homeassistant.const import CONF_ENTITY_ID, STATE_OFF, STATE_ON
from homeassistant.const import STATE_OFF, STATE_ON
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_target_entities,
parametrize_trigger_states,
set_or_remove_state,
target_entities,
)
@@ -163,25 +162,14 @@ async def test_input_boolean_state_trigger_behavior_last(
states: list[TriggerStateDescription],
) -> None:
"""Test that the input_boolean state trigger fires when the last input_boolean changes to a specific state."""
other_entity_ids = set(target_input_booleans["included"]) - {entity_id}
# Set all input_booleans, including the tested one, to the initial state
for eid in target_input_booleans["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
await arm_trigger(hass, trigger, {"behavior": "last"}, trigger_target_config)
for state in states[1:]:
included_state = state["included"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == 0
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == state["count"]
for service_call in service_calls:
assert service_call.data[CONF_ENTITY_ID] == entity_id
service_calls.clear()
await assert_trigger_behavior_last(
hass,
service_calls=service_calls,
target_entities=target_input_booleans,
trigger_target_config=trigger_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
trigger=trigger,
trigger_options=trigger_options,
states=states,
)

View File

@@ -21,9 +21,9 @@ from tests.components.common import (
@pytest.fixture
async def target_lawn_mowers(hass: HomeAssistant) -> list[str]:
async def target_lawn_mowers(hass: HomeAssistant) -> dict[str, list[str]]:
"""Create multiple lawn mower entities associated with different targets."""
return (await target_entities(hass, "lawn_mower"))["included"]
return await target_entities(hass, "lawn_mower")
@pytest.mark.parametrize(
@@ -80,7 +80,7 @@ async def test_lawn_mower_conditions_gated_by_labs_flag(
)
async def test_lawn_mower_state_condition_behavior_any(
hass: HomeAssistant,
target_lawn_mowers: list[str],
target_lawn_mowers: dict[str, list[str]],
condition_target_config: dict,
entity_id: str,
entities_in_target: int,
@@ -89,10 +89,10 @@ 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) - {entity_id}
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:
for eid in target_lawn_mowers["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
@@ -153,7 +153,7 @@ async def test_lawn_mower_state_condition_behavior_any(
)
async def test_lawn_mower_state_condition_behavior_all(
hass: HomeAssistant,
target_lawn_mowers: list[str],
target_lawn_mowers: dict[str, list[str]],
condition_target_config: dict,
entity_id: str,
entities_in_target: int,
@@ -162,10 +162,10 @@ 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) - {entity_id}
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:
for eid in target_lawn_mowers["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()

View File

@@ -5,19 +5,17 @@ from typing import Any
import pytest
from homeassistant.components.lawn_mower import LawnMowerActivity
from homeassistant.const import CONF_ENTITY_ID
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,
other_states,
parametrize_target_entities,
parametrize_trigger_states,
set_or_remove_state,
target_entities,
)
@@ -212,25 +210,14 @@ async def test_lawn_mower_state_trigger_behavior_last(
states: list[TriggerStateDescription],
) -> None:
"""Test that the lawn_mower state trigger fires when the last lawn_mower changes to a specific state."""
other_entity_ids = set(target_lawn_mowers["included"]) - {entity_id}
# Set all lawn mowers, including the tested one, 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()
await arm_trigger(hass, trigger, {"behavior": "last"}, trigger_target_config)
for state in states[1:]:
included_state = state["included"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == 0
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == state["count"]
for service_call in service_calls:
assert service_call.data[CONF_ENTITY_ID] == entity_id
service_calls.clear()
await assert_trigger_behavior_last(
hass,
service_calls=service_calls,
target_entities=target_lawn_mowers,
trigger_target_config=trigger_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
trigger=trigger,
trigger_options=trigger_options,
states=states,
)

View File

@@ -20,19 +20,19 @@ from tests.components.common import (
@pytest.fixture
async def target_lights(hass: HomeAssistant) -> list[str]:
async def target_lights(hass: HomeAssistant) -> dict[str, list[str]]:
"""Create multiple light entities associated with different targets."""
return (await target_entities(hass, "light"))["included"]
return await target_entities(hass, "light")
@pytest.fixture
async def target_switches(hass: HomeAssistant) -> list[str]:
async def target_switches(hass: HomeAssistant) -> dict[str, list[str]]:
"""Create multiple switch entities associated with different targets.
Note: The switches are used to ensure that only light entities are considered
in the condition evaluation and not other toggle entities.
"""
return (await target_entities(hass, "switch"))["included"]
return await target_entities(hass, "switch")
@pytest.mark.parametrize(
@@ -71,8 +71,8 @@ async def test_light_conditions_gated_by_labs_flag(
)
async def test_light_state_condition_behavior_any(
hass: HomeAssistant,
target_lights: list[str],
target_switches: list[str],
target_lights: dict[str, list[str]],
target_switches: dict[str, list[str]],
condition_target_config: dict,
entity_id: str,
entities_in_target: int,
@@ -81,10 +81,10 @@ async def test_light_state_condition_behavior_any(
states: list[ConditionStateDescription],
) -> None:
"""Test the light state condition with the 'any' behavior."""
other_entity_ids = set(target_lights) - {entity_id}
other_entity_ids = set(target_lights["included"]) - {entity_id}
# Set all lights, including the tested light, to the initial state
for eid in target_lights:
for eid in target_lights["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
@@ -97,7 +97,7 @@ async def test_light_state_condition_behavior_any(
# Set state for switches to ensure that they don't impact the condition
for state in states:
for eid in target_switches:
for eid in target_switches["included"]:
set_or_remove_state(hass, eid, state["included"])
await hass.async_block_till_done()
assert condition(hass) is False
@@ -137,7 +137,7 @@ async def test_light_state_condition_behavior_any(
)
async def test_light_state_condition_behavior_all(
hass: HomeAssistant,
target_lights: list[str],
target_lights: dict[str, list[str]],
condition_target_config: dict,
entity_id: str,
entities_in_target: int,
@@ -150,10 +150,10 @@ async def test_light_state_condition_behavior_all(
hass.states.async_set("switch.label_switch_1", STATE_OFF)
hass.states.async_set("switch.label_switch_2", STATE_ON)
other_entity_ids = set(target_lights) - {entity_id}
other_entity_ids = set(target_lights["included"]) - {entity_id}
# Set all lights, including the tested light, to the initial state
for eid in target_lights:
for eid in target_lights["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()

View File

@@ -5,13 +5,7 @@ from typing import Any
import pytest
from homeassistant.components.light import ATTR_BRIGHTNESS
from homeassistant.const import (
CONF_ABOVE,
CONF_BELOW,
CONF_ENTITY_ID,
STATE_OFF,
STATE_ON,
)
from homeassistant.const import CONF_ABOVE, CONF_BELOW, STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers.trigger import (
CONF_LOWER_LIMIT,
@@ -22,13 +16,12 @@ from homeassistant.helpers.trigger import (
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_target_entities,
parametrize_trigger_states,
set_or_remove_state,
target_entities,
)
@@ -381,28 +374,17 @@ async def test_light_state_trigger_behavior_last(
states: list[TriggerStateDescription],
) -> None:
"""Test that the light state trigger fires when the last light changes to a specific state."""
other_entity_ids = set(target_lights["included"]) - {entity_id}
# Set all lights, including the tested light, to the initial state
for eid in target_lights["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
await arm_trigger(hass, trigger, {"behavior": "last"}, trigger_target_config)
for state in states[1:]:
included_state = state["included"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == 0
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == state["count"]
for service_call in service_calls:
assert service_call.data[CONF_ENTITY_ID] == entity_id
service_calls.clear()
await assert_trigger_behavior_last(
hass,
service_calls=service_calls,
target_entities=target_lights,
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")
@@ -430,27 +412,14 @@ async def test_light_state_attribute_trigger_behavior_last(
states: list[tuple[tuple[str, dict], int]],
) -> None:
"""Test that the light state trigger fires when the last light state changes to a specific state."""
other_entity_ids = set(target_lights["included"]) - {entity_id}
# Set all lights, including the tested light, to the initial state
for eid in target_lights["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
await arm_trigger(
hass, trigger, {"behavior": "last"} | trigger_options, trigger_target_config
await assert_trigger_behavior_last(
hass,
service_calls=service_calls,
target_entities=target_lights,
trigger_target_config=trigger_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
trigger=trigger,
trigger_options=trigger_options,
states=states,
)
for state in states[1:]:
included_state = state["included"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == 0
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == state["count"]
for service_call in service_calls:
assert service_call.data[CONF_ENTITY_ID] == entity_id
service_calls.clear()

View File

@@ -21,9 +21,9 @@ from tests.components.common import (
@pytest.fixture
async def target_locks(hass: HomeAssistant) -> list[str]:
async def target_locks(hass: HomeAssistant) -> dict[str, list[str]]:
"""Create multiple lock entities associated with different targets."""
return (await target_entities(hass, "lock"))["included"]
return await target_entities(hass, "lock")
@pytest.mark.parametrize(
@@ -74,7 +74,7 @@ async def test_lock_conditions_gated_by_labs_flag(
)
async def test_lock_state_condition_behavior_any(
hass: HomeAssistant,
target_locks: list[str],
target_locks: dict[str, list[str]],
condition_target_config: dict,
entity_id: str,
entities_in_target: int,
@@ -83,10 +83,10 @@ 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) - {entity_id}
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:
for eid in target_locks["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
@@ -142,7 +142,7 @@ async def test_lock_state_condition_behavior_any(
)
async def test_lock_state_condition_behavior_all(
hass: HomeAssistant,
target_locks: list[str],
target_locks: dict[str, list[str]],
condition_target_config: dict,
entity_id: str,
entities_in_target: int,
@@ -151,10 +151,10 @@ 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) - {entity_id}
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:
for eid in target_locks["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()

View File

@@ -5,19 +5,17 @@ from typing import Any
import pytest
from homeassistant.components.lock import DOMAIN, LockState
from homeassistant.const import CONF_ENTITY_ID
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,
other_states,
parametrize_target_entities,
parametrize_trigger_states,
set_or_remove_state,
target_entities,
)
@@ -196,25 +194,14 @@ async def test_lock_state_trigger_behavior_last(
states: list[TriggerStateDescription],
) -> None:
"""Test that the lock state trigger fires when the last lock changes to a specific state."""
other_entity_ids = set(target_locks["included"]) - {entity_id}
# Set all locks, including the tested one, 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()
await arm_trigger(hass, trigger, {"behavior": "last"}, trigger_target_config)
for state in states[1:]:
included_state = state["included"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == 0
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == state["count"]
for service_call in service_calls:
assert service_call.data[CONF_ENTITY_ID] == entity_id
service_calls.clear()
await assert_trigger_behavior_last(
hass,
service_calls=service_calls,
target_entities=target_locks,
trigger_target_config=trigger_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
trigger=trigger,
trigger_options=trigger_options,
states=states,
)

View File

@@ -21,9 +21,9 @@ from tests.components.common import (
@pytest.fixture
async def target_media_players(hass: HomeAssistant) -> list[str]:
async def target_media_players(hass: HomeAssistant) -> dict[str, list[str]]:
"""Create multiple media player entities associated with different targets."""
return (await target_entities(hass, "media_player"))["included"]
return await target_entities(hass, "media_player")
@pytest.mark.parametrize(
@@ -92,7 +92,7 @@ async def test_media_player_conditions_gated_by_labs_flag(
)
async def test_media_player_state_condition_behavior_any(
hass: HomeAssistant,
target_media_players: list[str],
target_media_players: dict[str, list[str]],
condition_target_config: dict,
entity_id: str,
entities_in_target: int,
@@ -101,10 +101,10 @@ 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) - {entity_id}
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:
for eid in target_media_players["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
@@ -177,7 +177,7 @@ async def test_media_player_state_condition_behavior_any(
)
async def test_media_player_state_condition_behavior_all(
hass: HomeAssistant,
target_media_players: list[str],
target_media_players: dict[str, list[str]],
condition_target_config: dict,
entity_id: str,
entities_in_target: int,
@@ -186,10 +186,10 @@ 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) - {entity_id}
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:
for eid in target_media_players["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()

View File

@@ -5,18 +5,16 @@ from typing import Any
import pytest
from homeassistant.components.media_player import MediaPlayerState
from homeassistant.const import CONF_ENTITY_ID
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_target_entities,
parametrize_trigger_states,
set_or_remove_state,
target_entities,
)
@@ -171,25 +169,14 @@ async def test_media_player_state_trigger_behavior_last(
states: list[TriggerStateDescription],
) -> None:
"""Test that the media player state trigger fires when the last media player changes to a specific state."""
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()
await arm_trigger(hass, trigger, {"behavior": "last"}, trigger_target_config)
for state in states[1:]:
included_state = state["included"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == 0
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == state["count"]
for service_call in service_calls:
assert service_call.data[CONF_ENTITY_ID] == entity_id
service_calls.clear()
await assert_trigger_behavior_last(
hass,
service_calls=service_calls,
target_entities=target_media_players,
trigger_target_config=trigger_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
trigger=trigger,
trigger_options=trigger_options,
states=states,
)

View File

@@ -214,6 +214,7 @@ async def test_unknown_sensor(hass: HomeAssistant) -> None:
)
await hass.async_block_till_done()
await hass.async_start()
await hass.async_block_till_done()
hass.states.async_set(
"test.indoortemp",
@@ -292,6 +293,7 @@ async def test_sensor_changed(hass: HomeAssistant) -> None:
)
await hass.async_block_till_done()
await hass.async_start()
await hass.async_block_till_done()
hass.states.async_set(
"test.indoortemp", "30", {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}
@@ -310,3 +312,126 @@ async def test_sensor_changed(hass: HomeAssistant) -> None:
)
await hass.async_block_till_done()
assert hass.states.get("sensor.mold_indicator").state == "23"
@pytest.mark.parametrize("new_state", [STATE_UNAVAILABLE, STATE_UNKNOWN])
async def test_unavailable_sensor_recovery(hass: HomeAssistant, new_state: str) -> None:
"""Test recovery when sensor becomes unavailable/unknown and then available again."""
assert await async_setup_component(
hass,
sensor.DOMAIN,
{
"sensor": {
"platform": "mold_indicator",
"indoor_temp_sensor": "test.indoortemp",
"outdoor_temp_sensor": "test.outdoortemp",
"indoor_humidity_sensor": "test.indoorhumidity",
"calibration_factor": 2.0,
}
},
)
await hass.async_block_till_done()
await hass.async_start()
await hass.async_block_till_done()
# Initial state should be valid
moldind = hass.states.get("sensor.mold_indicator")
assert moldind
assert moldind.state == "68"
# Set indoor temp to unavailable
hass.states.async_set(
"test.indoortemp",
new_state,
{ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS},
)
await hass.async_block_till_done()
moldind = hass.states.get("sensor.mold_indicator")
assert moldind
assert moldind.state == STATE_UNAVAILABLE
assert moldind.attributes.get(ATTR_DEWPOINT) is None
assert moldind.attributes.get(ATTR_CRITICAL_TEMP) is None
# Recover by setting a valid value - should immediately work
hass.states.async_set(
"test.indoortemp", "20", {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}
)
await hass.async_block_till_done()
moldind = hass.states.get("sensor.mold_indicator")
assert moldind
assert moldind.state == "68"
assert moldind.attributes.get(ATTR_DEWPOINT) is not None
assert moldind.attributes.get(ATTR_CRITICAL_TEMP) is not None
async def test_all_sensors_unavailable_recovery(hass: HomeAssistant) -> None:
"""Test recovery when all sensors become unavailable and then available again."""
assert await async_setup_component(
hass,
sensor.DOMAIN,
{
"sensor": {
"platform": "mold_indicator",
"indoor_temp_sensor": "test.indoortemp",
"outdoor_temp_sensor": "test.outdoortemp",
"indoor_humidity_sensor": "test.indoorhumidity",
"calibration_factor": 2.0,
}
},
)
await hass.async_block_till_done()
await hass.async_start()
await hass.async_block_till_done()
# Initial state should be valid
moldind = hass.states.get("sensor.mold_indicator")
assert moldind
assert moldind.state == "68"
# Set all sensors to unavailable
hass.states.async_set(
"test.indoortemp",
STATE_UNAVAILABLE,
{ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS},
)
hass.states.async_set(
"test.outdoortemp",
STATE_UNAVAILABLE,
{ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS},
)
hass.states.async_set(
"test.indoorhumidity",
STATE_UNAVAILABLE,
{ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE},
)
await hass.async_block_till_done()
moldind = hass.states.get("sensor.mold_indicator")
assert moldind
assert moldind.state == STATE_UNAVAILABLE
# Recover all sensors one by one
hass.states.async_set(
"test.indoortemp", "20", {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}
)
await hass.async_block_till_done()
moldind = hass.states.get("sensor.mold_indicator")
assert moldind
assert moldind.state == STATE_UNAVAILABLE # Still unavailable, needs all sensors
hass.states.async_set(
"test.outdoortemp", "10", {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}
)
await hass.async_block_till_done()
moldind = hass.states.get("sensor.mold_indicator")
assert moldind
assert moldind.state == STATE_UNAVAILABLE # Still unavailable, needs humidity
hass.states.async_set(
"test.indoorhumidity", "50", {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE}
)
await hass.async_block_till_done()
moldind = hass.states.get("sensor.mold_indicator")
assert moldind
assert moldind.state == "68" # Now should recover fully
assert moldind.attributes.get(ATTR_DEWPOINT) is not None
assert moldind.attributes.get(ATTR_CRITICAL_TEMP) is not None

View File

@@ -12,10 +12,10 @@ from tests.components.common import (
arm_trigger,
assert_trigger_behavior_any,
assert_trigger_behavior_first,
assert_trigger_behavior_last,
assert_trigger_gated_by_labs_flag,
parametrize_target_entities,
parametrize_trigger_states,
set_or_remove_state,
target_entities,
)
@@ -174,37 +174,17 @@ async def test_motion_trigger_binary_sensor_behavior_last(
states: list[TriggerStateDescription],
) -> None:
"""Test motion trigger fires when the last binary_sensor changes state."""
other_entity_ids = set(target_binary_sensors["included"]) - {entity_id}
excluded_entity_ids = set(target_binary_sensors["excluded"]) - {entity_id}
for eid in target_binary_sensors["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
for eid in excluded_entity_ids:
set_or_remove_state(hass, eid, states[0]["excluded"])
await hass.async_block_till_done()
await arm_trigger(hass, trigger, {"behavior": "last"}, trigger_target_config)
for state in states[1:]:
excluded_state = state["excluded"]
included_state = state["included"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, excluded_state)
await hass.async_block_till_done()
assert len(service_calls) == 0
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == state["count"]
for service_call in service_calls:
assert service_call.data[CONF_ENTITY_ID] == entity_id
service_calls.clear()
for excluded_entity_id in excluded_entity_ids:
set_or_remove_state(hass, excluded_entity_id, excluded_state)
await hass.async_block_till_done()
assert len(service_calls) == 0
await assert_trigger_behavior_last(
hass,
service_calls=service_calls,
target_entities=target_binary_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,
)
# --- Device class exclusion tests ---

View File

@@ -12,10 +12,10 @@ from tests.components.common import (
arm_trigger,
assert_trigger_behavior_any,
assert_trigger_behavior_first,
assert_trigger_behavior_last,
assert_trigger_gated_by_labs_flag,
parametrize_target_entities,
parametrize_trigger_states,
set_or_remove_state,
target_entities,
)
@@ -174,37 +174,17 @@ async def test_occupancy_trigger_binary_sensor_behavior_last(
states: list[TriggerStateDescription],
) -> None:
"""Test occupancy trigger fires when the last binary_sensor changes state."""
other_entity_ids = set(target_binary_sensors["included"]) - {entity_id}
excluded_entity_ids = set(target_binary_sensors["excluded"]) - {entity_id}
for eid in target_binary_sensors["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
for eid in excluded_entity_ids:
set_or_remove_state(hass, eid, states[0]["excluded"])
await hass.async_block_till_done()
await arm_trigger(hass, trigger, {"behavior": "last"}, trigger_target_config)
for state in states[1:]:
excluded_state = state["excluded"]
included_state = state["included"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, excluded_state)
await hass.async_block_till_done()
assert len(service_calls) == 0
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == state["count"]
for service_call in service_calls:
assert service_call.data[CONF_ENTITY_ID] == entity_id
service_calls.clear()
for excluded_entity_id in excluded_entity_ids:
set_or_remove_state(hass, excluded_entity_id, excluded_state)
await hass.async_block_till_done()
assert len(service_calls) == 0
await assert_trigger_behavior_last(
hass,
service_calls=service_calls,
target_entities=target_binary_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,
)
# --- Device class exclusion tests ---

View File

@@ -20,9 +20,9 @@ from tests.components.common import (
@pytest.fixture
async def target_persons(hass: HomeAssistant) -> list[str]:
async def target_persons(hass: HomeAssistant) -> dict[str, list[str]]:
"""Create multiple person entities associated with different targets."""
return (await target_entities(hass, "person"))["included"]
return await target_entities(hass, "person")
@pytest.mark.parametrize(
@@ -61,7 +61,7 @@ async def test_person_conditions_gated_by_labs_flag(
)
async def test_person_state_condition_behavior_any(
hass: HomeAssistant,
target_persons: list[str],
target_persons: dict[str, list[str]],
condition_target_config: dict,
entity_id: str,
entities_in_target: int,
@@ -70,10 +70,10 @@ 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) - {entity_id}
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:
for eid in target_persons["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
@@ -119,7 +119,7 @@ async def test_person_state_condition_behavior_any(
)
async def test_person_state_condition_behavior_all(
hass: HomeAssistant,
target_persons: list[str],
target_persons: dict[str, list[str]],
condition_target_config: dict,
entity_id: str,
entities_in_target: int,
@@ -128,10 +128,10 @@ 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) - {entity_id}
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:
for eid in target_persons["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()

View File

@@ -5,18 +5,17 @@ from typing import Any
import pytest
from homeassistant.components.person.const import DOMAIN
from homeassistant.const import CONF_ENTITY_ID, STATE_HOME, STATE_NOT_HOME
from homeassistant.const import STATE_HOME, STATE_NOT_HOME
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_target_entities,
parametrize_trigger_states,
set_or_remove_state,
target_entities,
)
@@ -162,25 +161,14 @@ async def test_person_state_trigger_behavior_last(
states: list[TriggerStateDescription],
) -> None:
"""Test that the person home triggers when the last person changes to a specific state."""
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()
await arm_trigger(hass, trigger, {"behavior": "last"}, trigger_target_config)
for state in states[1:]:
included_state = state["included"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == 0
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == state["count"]
for service_call in service_calls:
assert service_call.data[CONF_ENTITY_ID] == entity_id
service_calls.clear()
await assert_trigger_behavior_last(
hass,
service_calls=service_calls,
target_entities=target_persons,
trigger_target_config=trigger_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
trigger=trigger,
trigger_options=trigger_options,
states=states,
)

View File

@@ -5,18 +5,17 @@ from typing import Any
import pytest
from homeassistant.components.remote import DOMAIN
from homeassistant.const import CONF_ENTITY_ID, STATE_OFF, STATE_ON
from homeassistant.const import STATE_OFF, STATE_ON
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_target_entities,
parametrize_trigger_states,
set_or_remove_state,
target_entities,
)
@@ -160,25 +159,14 @@ async def test_remote_state_trigger_behavior_last(
states: list[TriggerStateDescription],
) -> None:
"""Test that the remote triggers when the last remote changes to a specific state."""
other_entity_ids = set(target_remotes["included"]) - {entity_id}
# Set all remotes, including the tested remote, to the initial state
for eid in target_remotes["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
await arm_trigger(hass, trigger, {"behavior": "last"}, trigger_target_config)
for state in states[1:]:
included_state = state["included"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == 0
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == state["count"]
for service_call in service_calls:
assert service_call.data[CONF_ENTITY_ID] == entity_id
service_calls.clear()
await assert_trigger_behavior_last(
hass,
service_calls=service_calls,
target_entities=target_remotes,
trigger_target_config=trigger_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
trigger=trigger,
trigger_options=trigger_options,
states=states,
)

View File

@@ -28,10 +28,10 @@ from tests.components.common import (
arm_trigger,
assert_trigger_behavior_any,
assert_trigger_behavior_first,
assert_trigger_behavior_last,
assert_trigger_gated_by_labs_flag,
parametrize_target_entities,
parametrize_trigger_states,
set_or_remove_state,
target_entities,
)
@@ -178,28 +178,17 @@ async def test_schedule_state_trigger_behavior_last(
states: list[TriggerStateDescription],
) -> None:
"""Test that the schedule state trigger fires when the last schedule changes to a specific state."""
other_entity_ids = set(target_schedules["included"]) - {entity_id}
# Set all schedules, including the tested one, to the initial state
for eid in target_schedules["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
await arm_trigger(hass, trigger, {"behavior": "last"}, trigger_target_config)
for state in states[1:]:
included_state = state["included"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == 0
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == state["count"]
for service_call in service_calls:
assert service_call.data[CONF_ENTITY_ID] == entity_id
service_calls.clear()
await assert_trigger_behavior_last(
hass,
service_calls=service_calls,
target_entities=target_schedules,
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")

View File

@@ -20,19 +20,19 @@ from tests.components.common import (
@pytest.fixture
async def target_sirens(hass: HomeAssistant) -> list[str]:
async def target_sirens(hass: HomeAssistant) -> dict[str, list[str]]:
"""Create multiple siren entities associated with different targets."""
return (await target_entities(hass, "siren"))["included"]
return await target_entities(hass, "siren")
@pytest.fixture
async def target_switches(hass: HomeAssistant) -> list[str]:
async def target_switches(hass: HomeAssistant) -> dict[str, list[str]]:
"""Create multiple switch entities associated with different targets.
Note: The switches are used to ensure that only siren entities are considered
in the condition evaluation and not other toggle entities.
"""
return (await target_entities(hass, "switch"))["included"]
return await target_entities(hass, "switch")
@pytest.mark.parametrize(
@@ -71,8 +71,8 @@ async def test_siren_conditions_gated_by_labs_flag(
)
async def test_siren_state_condition_behavior_any(
hass: HomeAssistant,
target_sirens: list[str],
target_switches: list[str],
target_sirens: dict[str, list[str]],
target_switches: dict[str, list[str]],
condition_target_config: dict,
entity_id: str,
entities_in_target: int,
@@ -81,10 +81,10 @@ async def test_siren_state_condition_behavior_any(
states: list[ConditionStateDescription],
) -> None:
"""Test the siren state condition with the 'any' behavior."""
other_entity_ids = set(target_sirens) - {entity_id}
other_entity_ids = set(target_sirens["included"]) - {entity_id}
# Set all sirens, including the tested siren, to the initial state
for eid in target_sirens:
for eid in target_sirens["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
@@ -97,7 +97,7 @@ async def test_siren_state_condition_behavior_any(
# Set state for switches to ensure that they don't impact the condition
for state in states:
for eid in target_switches:
for eid in target_switches["included"]:
set_or_remove_state(hass, eid, state["included"])
await hass.async_block_till_done()
assert condition(hass) is False
@@ -137,7 +137,7 @@ async def test_siren_state_condition_behavior_any(
)
async def test_siren_state_condition_behavior_all(
hass: HomeAssistant,
target_sirens: list[str],
target_sirens: dict[str, list[str]],
condition_target_config: dict,
entity_id: str,
entities_in_target: int,
@@ -149,10 +149,10 @@ async def test_siren_state_condition_behavior_all(
# Set state for two switches to ensure that they don't impact the condition
hass.states.async_set("switch.label_switch_1", STATE_OFF)
hass.states.async_set("switch.label_switch_2", STATE_ON)
other_entity_ids = set(target_sirens) - {entity_id}
other_entity_ids = set(target_sirens["included"]) - {entity_id}
# Set all sirens, including the tested siren, to the initial state
for eid in target_sirens:
for eid in target_sirens["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()

View File

@@ -5,18 +5,17 @@ from typing import Any
import pytest
from homeassistant.components.siren import DOMAIN
from homeassistant.const import CONF_ENTITY_ID, STATE_OFF, STATE_ON
from homeassistant.const import STATE_OFF, STATE_ON
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_target_entities,
parametrize_trigger_states,
set_or_remove_state,
target_entities,
)
@@ -163,25 +162,14 @@ async def test_siren_state_trigger_behavior_last(
states: list[TriggerStateDescription],
) -> None:
"""Test that the siren state trigger fires when the last siren changes to a specific state."""
other_entity_ids = set(target_sirens["included"]) - {entity_id}
# Set all sirens, including the tested one, to the initial state
for eid in target_sirens["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
await arm_trigger(hass, trigger, {"behavior": "last"}, trigger_target_config)
for state in states[1:]:
included_state = state["included"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == 0
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == state["count"]
for service_call in service_calls:
assert service_call.data[CONF_ENTITY_ID] == entity_id
service_calls.clear()
await assert_trigger_behavior_last(
hass,
service_calls=service_calls,
target_entities=target_sirens,
trigger_target_config=trigger_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
trigger=trigger,
trigger_options=trigger_options,
states=states,
)

View File

@@ -725,6 +725,66 @@
'state': 'medium',
})
# ---
# name: test_all_entities[da_rvc_map_01011][select.robot_vacuum_sound_mode-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'mute',
'tone',
'voice',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'select.robot_vacuum_sound_mode',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Sound mode',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Sound mode',
'platform': 'smartthings',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'robot_cleaner_sound_mode',
'unique_id': '01b28624-5907-c8bc-0325-8ad23f03a637_main_samsungce.robotCleanerSystemSoundMode_soundMode_soundMode',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_rvc_map_01011][select.robot_vacuum_sound_mode-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Robot Vacuum Sound mode',
'options': list([
'mute',
'tone',
'voice',
]),
}),
'context': <ANY>,
'entity_id': 'select.robot_vacuum_sound_mode',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'tone',
})
# ---
# name: test_all_entities[da_rvc_map_01011][select.robot_vacuum_water_level-entry]
EntityRegistryEntrySnapshot({
'aliases': list([

View File

@@ -20,19 +20,19 @@ from tests.components.common import (
@pytest.fixture
async def target_lights(hass: HomeAssistant) -> list[str]:
async def target_lights(hass: HomeAssistant) -> dict[str, list[str]]:
"""Create multiple light entities associated with different targets.
Note: The lights are used to ensure that only switch entities are considered
in the condition evaluation and not other toggle entities.
"""
return (await target_entities(hass, "light"))["included"]
return await target_entities(hass, "light")
@pytest.fixture
async def target_switches(hass: HomeAssistant) -> list[str]:
async def target_switches(hass: HomeAssistant) -> dict[str, list[str]]:
"""Create multiple switch entities associated with different targets."""
return (await target_entities(hass, "switch"))["included"]
return await target_entities(hass, "switch")
@pytest.mark.parametrize(
@@ -71,8 +71,8 @@ async def test_switch_conditions_gated_by_labs_flag(
)
async def test_switch_state_condition_behavior_any(
hass: HomeAssistant,
target_lights: list[str],
target_switches: list[str],
target_lights: dict[str, list[str]],
target_switches: dict[str, list[str]],
condition_target_config: dict,
entity_id: str,
entities_in_target: int,
@@ -81,10 +81,10 @@ async def test_switch_state_condition_behavior_any(
states: list[ConditionStateDescription],
) -> None:
"""Test the switch state condition with the 'any' behavior."""
other_entity_ids = set(target_switches) - {entity_id}
other_entity_ids = set(target_switches["included"]) - {entity_id}
# Set all switches, including the tested switch, to the initial state
for eid in target_switches:
for eid in target_switches["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
@@ -97,7 +97,7 @@ async def test_switch_state_condition_behavior_any(
# Set state for lights to ensure that they don't impact the condition
for state in states:
for eid in target_lights:
for eid in target_lights["included"]:
set_or_remove_state(hass, eid, state["included"])
await hass.async_block_till_done()
assert condition(hass) is False
@@ -137,7 +137,7 @@ async def test_switch_state_condition_behavior_any(
)
async def test_switch_state_condition_behavior_all(
hass: HomeAssistant,
target_switches: list[str],
target_switches: dict[str, list[str]],
condition_target_config: dict,
entity_id: str,
entities_in_target: int,
@@ -150,10 +150,10 @@ async def test_switch_state_condition_behavior_all(
hass.states.async_set("switch.label_switch_1", STATE_OFF)
hass.states.async_set("switch.label_switch_2", STATE_ON)
other_entity_ids = set(target_switches) - {entity_id}
other_entity_ids = set(target_switches["included"]) - {entity_id}
# Set all switches, including the tested switch, to the initial state
for eid in target_switches:
for eid in target_switches["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()

View File

@@ -5,18 +5,17 @@ from typing import Any
import pytest
from homeassistant.components.switch import DOMAIN
from homeassistant.const import CONF_ENTITY_ID, STATE_OFF, STATE_ON
from homeassistant.const import STATE_OFF, STATE_ON
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_target_entities,
parametrize_trigger_states,
set_or_remove_state,
target_entities,
)
@@ -163,25 +162,14 @@ async def test_switch_state_trigger_behavior_last(
states: list[TriggerStateDescription],
) -> None:
"""Test that the switch state trigger fires when the last switch changes to a specific state."""
other_entity_ids = set(target_switches["included"]) - {entity_id}
# Set all switches, including the tested one, to the initial state
for eid in target_switches["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
await arm_trigger(hass, trigger, {"behavior": "last"}, trigger_target_config)
for state in states[1:]:
included_state = state["included"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == 0
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == state["count"]
for service_call in service_calls:
assert service_call.data[CONF_ENTITY_ID] == entity_id
service_calls.clear()
await assert_trigger_behavior_last(
hass,
service_calls=service_calls,
target_entities=target_switches,
trigger_target_config=trigger_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
trigger=trigger,
trigger_options=trigger_options,
states=states,
)

View File

@@ -41,7 +41,7 @@ from homeassistant.helpers.config_entry_oauth2_flow import (
from . import setup_platform
from .conftest import create_config_entry
from .const import VEHICLE_ASLEEP, VEHICLE_DATA_ALT
from .const import LIVE_STATUS, VEHICLE_ASLEEP, VEHICLE_DATA_ALT
from tests.common import MockConfigEntry, async_fire_time_changed
@@ -352,6 +352,42 @@ async def test_energy_live_refresh_error(
assert normal_config_entry.state is state
async def test_energy_live_refresh_bad_response(
hass: HomeAssistant,
normal_config_entry: MockConfigEntry,
mock_live_status: AsyncMock,
) -> None:
"""Test coordinator refresh with malformed live status payload."""
bad_live_status = deepcopy(LIVE_STATUS)
bad_live_status["response"] = "site data is unavailable"
mock_live_status.side_effect = None
mock_live_status.return_value = bad_live_status
await setup_platform(hass, normal_config_entry)
assert normal_config_entry.state is ConfigEntryState.LOADED
assert (state := hass.states.get("sensor.test_battery_level"))
assert state.state != "unavailable"
async def test_energy_live_refresh_bad_wall_connectors(
hass: HomeAssistant,
normal_config_entry: MockConfigEntry,
mock_live_status: AsyncMock,
) -> None:
"""Test coordinator refresh with malformed wall connector payload."""
bad_live_status = deepcopy(LIVE_STATUS)
bad_live_status["response"]["wall_connectors"] = "site data is unavailable"
mock_live_status.side_effect = None
mock_live_status.return_value = bad_live_status
await setup_platform(hass, normal_config_entry)
assert normal_config_entry.state is ConfigEntryState.LOADED
assert (state := hass.states.get("sensor.test_battery_level"))
assert state.state != "unavailable"
# Test Energy Site Coordinator
@pytest.mark.parametrize(("side_effect", "state"), ERRORS)
async def test_energy_site_refresh_error(

View File

@@ -2,7 +2,7 @@
from __future__ import annotations
from unittest.mock import AsyncMock
from unittest.mock import AsyncMock, MagicMock
import pytest
@@ -10,12 +10,97 @@ from homeassistant.components.recorder import Recorder
from homeassistant.components.tibber.const import DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_component import async_update_entity
from homeassistant.util import dt as dt_util
from .conftest import create_tibber_device
from tests.common import MockConfigEntry
def _create_home(*, current_price: float | None = 1.25) -> MagicMock:
"""Create a mocked Tibber home with an active subscription."""
home = MagicMock()
home.home_id = "home-id"
home.name = "Home"
home.currency = "NOK"
home.price_unit = "NOK/kWh"
home.has_active_subscription = True
home.has_real_time_consumption = False
home.last_data_timestamp = None
home.update_info = AsyncMock(return_value=None)
home.update_info_and_price_info = AsyncMock(return_value=None)
home.current_price_data = MagicMock(
return_value=(current_price, dt_util.utcnow(), 0.4)
)
home.current_attributes = MagicMock(
return_value={
"max_price": 1.8,
"avg_price": 1.2,
"min_price": 0.8,
"off_peak_1": 0.9,
"peak": 1.7,
"off_peak_2": 1.0,
}
)
home.month_cost = 111.1
home.peak_hour = 2.5
home.peak_hour_time = dt_util.utcnow()
home.month_cons = 222.2
home.hourly_consumption_data = []
home.hourly_production_data = []
home.info = {
"viewer": {
"home": {
"appNickname": "Home",
"address": {"address1": "Street 1"},
"meteringPointData": {
"gridCompany": "GridCo",
"estimatedAnnualConsumption": 12000,
},
}
}
}
return home
async def test_price_sensor_state_unit_and_attributes(
recorder_mock: Recorder,
hass: HomeAssistant,
config_entry: MockConfigEntry,
tibber_mock: MagicMock,
setup_credentials: None,
entity_registry: er.EntityRegistry,
) -> None:
"""Test price sensor state and attributes."""
home = _create_home(current_price=1.25)
tibber_mock.get_homes.return_value = [home]
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
entity_id = entity_registry.async_get_entity_id("sensor", DOMAIN, home.home_id)
assert entity_id is not None
await async_update_entity(hass, entity_id)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state is not None
assert float(state.state) == 1.25
assert state.attributes["unit_of_measurement"] == "NOK/kWh"
assert state.attributes["app_nickname"] == "Home"
assert state.attributes["grid_company"] == "GridCo"
assert state.attributes["estimated_annual_consumption"] == 12000
assert state.attributes["intraday_price_ranking"] == 0.4
assert state.attributes["max_price"] == 1.8
assert state.attributes["avg_price"] == 1.2
assert state.attributes["min_price"] == 0.8
assert state.attributes["off_peak_1"] == 0.9
assert state.attributes["peak"] == 1.7
assert state.attributes["off_peak_2"] == 1.0
async def test_data_api_sensors_are_created(
recorder_mock: Recorder,
hass: HomeAssistant,

View File

@@ -24,6 +24,10 @@ async def test_async_setup_entry(
tibber_connection.fetch_production_data_active_homes.return_value = None
tibber_connection.get_homes = mock_get_homes
runtime_data = AsyncMock()
runtime_data.async_get_client.return_value = tibber_connection
config_entry.runtime_data = runtime_data
coordinator = TibberDataCoordinator(hass, config_entry, tibber_connection)
await coordinator._async_update_data()
await async_wait_recording_done(hass)

View File

@@ -5,18 +5,17 @@ from typing import Any
import pytest
from homeassistant.components.update import DOMAIN
from homeassistant.const import CONF_ENTITY_ID, STATE_OFF, STATE_ON
from homeassistant.const import STATE_OFF, STATE_ON
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_target_entities,
parametrize_trigger_states,
set_or_remove_state,
target_entities,
)
@@ -147,25 +146,14 @@ async def test_update_state_trigger_behavior_last(
states: list[TriggerStateDescription],
) -> None:
"""Test that the update state trigger fires when the last update changes to a specific state."""
other_entity_ids = set(target_updates["included"]) - {entity_id}
# Set all updates, including the tested one, to the initial state
for eid in target_updates["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
await arm_trigger(hass, trigger, {"behavior": "last"}, trigger_target_config)
for state in states[1:]:
included_state = state["included"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == 0
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == state["count"]
for service_call in service_calls:
assert service_call.data[CONF_ENTITY_ID] == entity_id
service_calls.clear()
await assert_trigger_behavior_last(
hass,
service_calls=service_calls,
target_entities=target_updates,
trigger_target_config=trigger_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
trigger=trigger,
trigger_options=trigger_options,
states=states,
)

View File

@@ -21,9 +21,9 @@ from tests.components.common import (
@pytest.fixture
async def target_vacuums(hass: HomeAssistant) -> list[str]:
async def target_vacuums(hass: HomeAssistant) -> dict[str, list[str]]:
"""Create multiple vacuum entities associated with different targets."""
return (await target_entities(hass, "vacuum"))["included"]
return await target_entities(hass, "vacuum")
@pytest.mark.parametrize(
@@ -80,7 +80,7 @@ async def test_vacuum_conditions_gated_by_labs_flag(
)
async def test_vacuum_state_condition_behavior_any(
hass: HomeAssistant,
target_vacuums: list[str],
target_vacuums: dict[str, list[str]],
condition_target_config: dict,
entity_id: str,
entities_in_target: int,
@@ -89,10 +89,10 @@ 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) - {entity_id}
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:
for eid in target_vacuums["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
@@ -153,7 +153,7 @@ async def test_vacuum_state_condition_behavior_any(
)
async def test_vacuum_state_condition_behavior_all(
hass: HomeAssistant,
target_vacuums: list[str],
target_vacuums: dict[str, list[str]],
condition_target_config: dict,
entity_id: str,
entities_in_target: int,
@@ -162,10 +162,10 @@ 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) - {entity_id}
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:
for eid in target_vacuums["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()

View File

@@ -5,19 +5,17 @@ from typing import Any
import pytest
from homeassistant.components.vacuum import VacuumActivity
from homeassistant.const import CONF_ENTITY_ID
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,
other_states,
parametrize_target_entities,
parametrize_trigger_states,
set_or_remove_state,
target_entities,
)
@@ -212,25 +210,14 @@ async def test_vacuum_state_trigger_behavior_last(
states: list[TriggerStateDescription],
) -> None:
"""Test that the vacuum state trigger fires when the last vacuum changes to a specific state."""
other_entity_ids = set(target_vacuums["included"]) - {entity_id}
# Set all vacuums, including the tested one, 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()
await arm_trigger(hass, trigger, {"behavior": "last"}, trigger_target_config)
for state in states[1:]:
included_state = state["included"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == 0
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == state["count"]
for service_call in service_calls:
assert service_call.data[CONF_ENTITY_ID] == entity_id
service_calls.clear()
await assert_trigger_behavior_last(
hass,
service_calls=service_calls,
target_entities=target_vacuums,
trigger_target_config=trigger_target_config,
entity_id=entity_id,
entities_in_target=entities_in_target,
trigger=trigger,
trigger_options=trigger_options,
states=states,
)

View File

@@ -146,11 +146,6 @@ async def test_avaliable_after_update(
{ATTR_ENTITY_ID: [wemo_entity.entity_id]},
blocking=True,
)
# _wemo_call_wrapper schedules async_update_listeners via hass.add_job
# from the executor thread, which goes through two levels of call_soon
# before the entity state is written.
await hass.async_block_till_done()
await hass.async_block_till_done()
assert hass.states.get(wemo_entity.entity_id).state == STATE_UNAVAILABLE

View File

@@ -73,11 +73,6 @@ async def test_turn_on_brightness(
{ATTR_ENTITY_ID: [wemo_entity.entity_id], ATTR_BRIGHTNESS: 204},
blocking=True,
)
# _wemo_call_wrapper schedules async_update_listeners via hass.add_job
# from the executor thread, which goes through two levels of call_soon
# before the entity state is written.
await hass.async_block_till_done()
await hass.async_block_till_done()
pywemo_device.set_brightness.assert_called_once_with(80)
states = hass.states.get(wemo_entity.entity_id)

View File

@@ -13,10 +13,10 @@ from tests.components.common import (
arm_trigger,
assert_trigger_behavior_any,
assert_trigger_behavior_first,
assert_trigger_behavior_last,
assert_trigger_gated_by_labs_flag,
parametrize_target_entities,
parametrize_trigger_states,
set_or_remove_state,
target_entities,
)
@@ -251,37 +251,17 @@ async def test_window_trigger_binary_sensor_behavior_last(
states: list[TriggerStateDescription],
) -> None:
"""Test window trigger fires when the last binary_sensor changes state."""
other_entity_ids = set(target_binary_sensors["included"]) - {entity_id}
excluded_entity_ids = set(target_binary_sensors["excluded"]) - {entity_id}
for eid in target_binary_sensors["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
for eid in excluded_entity_ids:
set_or_remove_state(hass, eid, states[0]["excluded"])
await hass.async_block_till_done()
await arm_trigger(hass, trigger, {"behavior": "last"}, trigger_target_config)
for state in states[1:]:
excluded_state = state["excluded"]
included_state = state["included"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, excluded_state)
await hass.async_block_till_done()
assert len(service_calls) == 0
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == state["count"]
for service_call in service_calls:
assert service_call.data[CONF_ENTITY_ID] == entity_id
service_calls.clear()
for excluded_entity_id in excluded_entity_ids:
set_or_remove_state(hass, excluded_entity_id, excluded_state)
await hass.async_block_till_done()
assert len(service_calls) == 0
await assert_trigger_behavior_last(
hass,
service_calls=service_calls,
target_entities=target_binary_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")
@@ -411,37 +391,17 @@ async def test_window_trigger_cover_behavior_last(
states: list[TriggerStateDescription],
) -> None:
"""Test window trigger fires when the last cover changes state."""
other_entity_ids = set(target_covers["included"]) - {entity_id}
excluded_entity_ids = set(target_covers["excluded"]) - {entity_id}
for eid in target_covers["included"]:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
for eid in excluded_entity_ids:
set_or_remove_state(hass, eid, states[0]["excluded"])
await hass.async_block_till_done()
await arm_trigger(hass, trigger, {"behavior": "last"}, trigger_target_config)
for state in states[1:]:
excluded_state = state["excluded"]
included_state = state["included"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, excluded_state)
await hass.async_block_till_done()
assert len(service_calls) == 0
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == state["count"]
for service_call in service_calls:
assert service_call.data[CONF_ENTITY_ID] == entity_id
service_calls.clear()
for excluded_entity_id in excluded_entity_ids:
set_or_remove_state(hass, excluded_entity_id, excluded_state)
await hass.async_block_till_done()
assert len(service_calls) == 0
await assert_trigger_behavior_last(
hass,
service_calls=service_calls,
target_entities=target_covers,
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")