mirror of
https://github.com/home-assistant/core.git
synced 2026-01-09 09:07:16 +01:00
Compare commits
23 Commits
homevolt
...
condition_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45f1fc32e2 | ||
|
|
1e95598c96 | ||
|
|
e963adfdf0 | ||
|
|
fd7bbc68c6 | ||
|
|
9281ab018c | ||
|
|
80baf86e23 | ||
|
|
db497b23fe | ||
|
|
a2fb8f5a72 | ||
|
|
6953bd4599 | ||
|
|
225be65f71 | ||
|
|
7b0463f763 | ||
|
|
4d305b657a | ||
|
|
d5a553c8c7 | ||
|
|
9169b68254 | ||
|
|
fde9bd95d5 | ||
|
|
e4db8ff86e | ||
|
|
a084e51345 | ||
|
|
00381e6dfd | ||
|
|
b6d493696a | ||
|
|
5f0500c3cd | ||
|
|
c61a63cc6f | ||
|
|
5445a4f40f | ||
|
|
2888cacc3f |
1
CODEOWNERS
generated
1
CODEOWNERS
generated
@@ -1803,6 +1803,7 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/waqi/ @joostlek
|
||||
/homeassistant/components/water_heater/ @home-assistant/core
|
||||
/tests/components/water_heater/ @home-assistant/core
|
||||
/homeassistant/components/waterfurnace/ @sdague @masterkoppa
|
||||
/homeassistant/components/watergate/ @adam-the-hero
|
||||
/tests/components/watergate/ @adam-the-hero
|
||||
/homeassistant/components/watson_tts/ @rutkai
|
||||
|
||||
@@ -11,6 +11,7 @@ from homeassistant.const import CONF_HOST, CONF_MAC, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||
|
||||
from .const import CONF_USE_SSL
|
||||
from .coordinator import BraviaTVConfigEntry, BraviaTVCoordinator
|
||||
|
||||
PLATFORMS: Final[list[Platform]] = [
|
||||
@@ -26,11 +27,12 @@ async def async_setup_entry(
|
||||
"""Set up a config entry."""
|
||||
host = config_entry.data[CONF_HOST]
|
||||
mac = config_entry.data[CONF_MAC]
|
||||
ssl = config_entry.data.get(CONF_USE_SSL, False)
|
||||
|
||||
session = async_create_clientsession(
|
||||
hass, cookie_jar=CookieJar(unsafe=True, quote_cookie=False)
|
||||
)
|
||||
client = BraviaClient(host, mac, session=session)
|
||||
client = BraviaClient(host, mac, session=session, ssl=ssl)
|
||||
coordinator = BraviaTVCoordinator(
|
||||
hass=hass,
|
||||
config_entry=config_entry,
|
||||
|
||||
@@ -28,6 +28,7 @@ from .const import (
|
||||
ATTR_MODEL,
|
||||
CONF_NICKNAME,
|
||||
CONF_USE_PSK,
|
||||
CONF_USE_SSL,
|
||||
DOMAIN,
|
||||
NICKNAME_PREFIX,
|
||||
)
|
||||
@@ -46,11 +47,12 @@ class BraviaTVConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
def create_client(self) -> None:
|
||||
"""Create Bravia TV client from config."""
|
||||
host = self.device_config[CONF_HOST]
|
||||
ssl = self.device_config[CONF_USE_SSL]
|
||||
session = async_create_clientsession(
|
||||
self.hass,
|
||||
cookie_jar=CookieJar(unsafe=True, quote_cookie=False),
|
||||
)
|
||||
self.client = BraviaClient(host=host, session=session)
|
||||
self.client = BraviaClient(host=host, session=session, ssl=ssl)
|
||||
|
||||
async def gen_instance_ids(self) -> tuple[str, str]:
|
||||
"""Generate client_id and nickname."""
|
||||
@@ -123,10 +125,10 @@ class BraviaTVConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle authorize step."""
|
||||
self.create_client()
|
||||
|
||||
if user_input is not None:
|
||||
self.device_config[CONF_USE_PSK] = user_input[CONF_USE_PSK]
|
||||
self.device_config[CONF_USE_SSL] = user_input[CONF_USE_SSL]
|
||||
self.create_client()
|
||||
if user_input[CONF_USE_PSK]:
|
||||
return await self.async_step_psk()
|
||||
return await self.async_step_pin()
|
||||
@@ -136,6 +138,7 @@ class BraviaTVConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_USE_PSK, default=False): bool,
|
||||
vol.Required(CONF_USE_SSL, default=False): bool,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
@@ -12,6 +12,7 @@ ATTR_MODEL: Final = "model"
|
||||
|
||||
CONF_NICKNAME: Final = "nickname"
|
||||
CONF_USE_PSK: Final = "use_psk"
|
||||
CONF_USE_SSL: Final = "use_ssl"
|
||||
|
||||
DOMAIN: Final = "braviatv"
|
||||
LEGACY_CLIENT_ID: Final = "HomeAssistant"
|
||||
|
||||
@@ -15,9 +15,10 @@
|
||||
"step": {
|
||||
"authorize": {
|
||||
"data": {
|
||||
"use_psk": "Use PSK authentication"
|
||||
"use_psk": "Use PSK authentication",
|
||||
"use_ssl": "Use SSL connection"
|
||||
},
|
||||
"description": "Make sure that «Control remotely» is enabled on your TV, go to: \nSettings -> Network -> Remote device settings -> Control remotely. \n\nThere are two authorization methods: PIN code or PSK (Pre-Shared Key). \nAuthorization via PSK is recommended as more stable.",
|
||||
"description": "Make sure that «Control remotely» is enabled on your TV. Go to: \nSettings -> Network -> Remote device settings -> Control remotely. \n\nThere are two authorization methods: PIN code or PSK (Pre-Shared Key). \nAuthorization via PSK is recommended, as it is more stable. \n\nUse an SSL connection only if your TV supports this connection type.",
|
||||
"title": "Authorize Sony Bravia TV"
|
||||
},
|
||||
"confirm": {
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
||||
"integration_type": "entity",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==3.5.0", "home-assistant-intents==2026.1.1"]
|
||||
"requirements": ["hassil==3.5.0", "home-assistant-intents==2026.1.6"]
|
||||
}
|
||||
|
||||
@@ -116,6 +116,8 @@ class IsraelRailEntitySensor(
|
||||
@property
|
||||
def native_value(self) -> StateType | datetime:
|
||||
"""Return the state of the sensor."""
|
||||
if self.entity_description.index >= len(self.coordinator.data):
|
||||
return None
|
||||
return self.entity_description.value_fn(
|
||||
self.coordinator.data[self.entity_description.index]
|
||||
)
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["jvcprojector"],
|
||||
"requirements": ["pyjvcprojector==1.1.2"]
|
||||
"requirements": ["pyjvcprojector==1.1.3"]
|
||||
}
|
||||
|
||||
@@ -41,6 +41,13 @@ COMMANDS = {
|
||||
"mode_1": const.REMOTE_MODE_1,
|
||||
"mode_2": const.REMOTE_MODE_2,
|
||||
"mode_3": const.REMOTE_MODE_3,
|
||||
"mode_4": const.REMOTE_MODE_4,
|
||||
"mode_5": const.REMOTE_MODE_5,
|
||||
"mode_6": const.REMOTE_MODE_6,
|
||||
"mode_7": const.REMOTE_MODE_7,
|
||||
"mode_8": const.REMOTE_MODE_8,
|
||||
"mode_9": const.REMOTE_MODE_9,
|
||||
"mode_10": const.REMOTE_MODE_10,
|
||||
"lens_ap": const.REMOTE_LENS_AP,
|
||||
"gamma": const.REMOTE_GAMMA,
|
||||
"color_temp": const.REMOTE_COLOR_TEMP,
|
||||
|
||||
@@ -2,23 +2,23 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import openevsewifi
|
||||
from openevsehttp.__main__ import OpenEVSE
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryError
|
||||
|
||||
type OpenEVSEConfigEntry = ConfigEntry[openevsewifi.Charger]
|
||||
type OpenEVSEConfigEntry = ConfigEntry[OpenEVSE]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: OpenEVSEConfigEntry) -> bool:
|
||||
"""Set up openevse from a config entry."""
|
||||
|
||||
entry.runtime_data = openevsewifi.Charger(entry.data[CONF_HOST])
|
||||
entry.runtime_data = OpenEVSE(entry.data[CONF_HOST])
|
||||
try:
|
||||
await hass.async_add_executor_job(entry.runtime_data.getStatus)
|
||||
except AttributeError as ex:
|
||||
await entry.runtime_data.test_and_get()
|
||||
except TimeoutError as ex:
|
||||
raise ConfigEntryError("Unable to connect to charger") from ex
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, [Platform.SENSOR])
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from typing import Any
|
||||
|
||||
import openevsewifi
|
||||
from openevsehttp.__main__ import OpenEVSE
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
@@ -20,13 +20,13 @@ class OpenEVSEConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
async def check_status(self, host: str) -> bool:
|
||||
"""Check if we can connect to the OpenEVSE charger."""
|
||||
|
||||
charger = openevsewifi.Charger(host)
|
||||
charger = OpenEVSE(host)
|
||||
try:
|
||||
result = await self.hass.async_add_executor_job(charger.getStatus)
|
||||
except AttributeError:
|
||||
await charger.test_and_get()
|
||||
except TimeoutError:
|
||||
return False
|
||||
else:
|
||||
return result is not None
|
||||
return True
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/openevse",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["openevsewifi"],
|
||||
"loggers": ["openevsehttp"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["openevsewifi==1.1.2"]
|
||||
"requirements": ["python-openevse-http==0.2.1"]
|
||||
}
|
||||
|
||||
@@ -4,8 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
import openevsewifi
|
||||
from requests import RequestException
|
||||
from openevsehttp.__main__ import OpenEVSE
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
@@ -175,7 +174,7 @@ class OpenEVSESensor(SensorEntity):
|
||||
def __init__(
|
||||
self,
|
||||
host: str,
|
||||
charger: openevsewifi.Charger,
|
||||
charger: OpenEVSE,
|
||||
description: SensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
@@ -183,25 +182,28 @@ class OpenEVSESensor(SensorEntity):
|
||||
self.host = host
|
||||
self.charger = charger
|
||||
|
||||
def update(self) -> None:
|
||||
async def async_update(self) -> None:
|
||||
"""Get the monitored data from the charger."""
|
||||
try:
|
||||
sensor_type = self.entity_description.key
|
||||
if sensor_type == "status":
|
||||
self._attr_native_value = self.charger.getStatus()
|
||||
elif sensor_type == "charge_time":
|
||||
self._attr_native_value = self.charger.getChargeTimeElapsed() / 60
|
||||
elif sensor_type == "ambient_temp":
|
||||
self._attr_native_value = self.charger.getAmbientTemperature()
|
||||
elif sensor_type == "ir_temp":
|
||||
self._attr_native_value = self.charger.getIRTemperature()
|
||||
elif sensor_type == "rtc_temp":
|
||||
self._attr_native_value = self.charger.getRTCTemperature()
|
||||
elif sensor_type == "usage_session":
|
||||
self._attr_native_value = float(self.charger.getUsageSession()) / 1000
|
||||
elif sensor_type == "usage_total":
|
||||
self._attr_native_value = float(self.charger.getUsageTotal()) / 1000
|
||||
else:
|
||||
self._attr_native_value = "Unknown"
|
||||
except (RequestException, ValueError, KeyError):
|
||||
await self.charger.update()
|
||||
except TimeoutError:
|
||||
_LOGGER.warning("Could not update status for %s", self.name)
|
||||
return
|
||||
|
||||
sensor_type = self.entity_description.key
|
||||
if sensor_type == "status":
|
||||
self._attr_native_value = self.charger.status
|
||||
elif sensor_type == "charge_time":
|
||||
self._attr_native_value = self.charger.charge_time_elapsed / 60
|
||||
elif sensor_type == "ambient_temp":
|
||||
self._attr_native_value = self.charger.ambient_temperature
|
||||
elif sensor_type == "ir_temp":
|
||||
self._attr_native_value = self.charger.ir_temperature
|
||||
elif sensor_type == "rtc_temp":
|
||||
self._attr_native_value = self.charger.rtc_temperature
|
||||
elif sensor_type == "usage_session":
|
||||
self._attr_native_value = float(self.charger.usage_session) / 1000
|
||||
elif sensor_type == "usage_total":
|
||||
self._attr_native_value = float(self.charger.usage_total) / 1000
|
||||
else:
|
||||
self._attr_native_value = "Unknown"
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "Enter the IP Address of your openevse. Should match the address you used to set it up."
|
||||
"host": "Enter the IP address of your OpenEVSE. Should match the address you used to set it up."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -453,10 +453,6 @@ async def _async_generate_memory_profile(hass: HomeAssistant, call: ServiceCall)
|
||||
# Imports deferred to avoid loading modules
|
||||
# in memory since usually only one part of this
|
||||
# integration is used at a time
|
||||
if sys.version_info >= (3, 14):
|
||||
raise HomeAssistantError(
|
||||
"Memory profiling is not supported on Python 3.14. Please use Python 3.13."
|
||||
)
|
||||
from guppy import hpy # noqa: PLC0415
|
||||
|
||||
start_time = int(time.time() * 1000000)
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"quality_scale": "internal",
|
||||
"requirements": [
|
||||
"pyprof2calltree==1.4.5",
|
||||
"guppy3==3.1.5;python_version<'3.14'",
|
||||
"guppy3==3.1.6",
|
||||
"objgraph==3.5.0"
|
||||
],
|
||||
"single_config_entry": true
|
||||
|
||||
@@ -128,8 +128,9 @@ class RingCam(RingEntity[RingDoorBell], Camera):
|
||||
self._device = self._get_coordinator_data().get_video_device(
|
||||
self._device.device_api_id
|
||||
)
|
||||
|
||||
history_data = self._device.last_history
|
||||
if history_data:
|
||||
if history_data and self._device.has_subscription:
|
||||
self._last_event = history_data[0]
|
||||
# will call async_update to update the attributes and get the
|
||||
# video url from the api
|
||||
@@ -154,8 +155,16 @@ class RingCam(RingEntity[RingDoorBell], Camera):
|
||||
self, width: int | None = None, height: int | None = None
|
||||
) -> bytes | None:
|
||||
"""Return a still image response from the camera."""
|
||||
if self._video_url is None:
|
||||
if not self._device.has_subscription:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="no_subscription",
|
||||
)
|
||||
return None
|
||||
|
||||
key = (width, height)
|
||||
if not (image := self._images.get(key)) and self._video_url is not None:
|
||||
if not (image := self._images.get(key)):
|
||||
image = await ffmpeg.async_get_image(
|
||||
self.hass,
|
||||
self._video_url,
|
||||
|
||||
@@ -151,6 +151,9 @@
|
||||
"api_timeout": {
|
||||
"message": "Timeout communicating with Ring API"
|
||||
},
|
||||
"no_subscription": {
|
||||
"message": "Ring Protect subscription required for snapshots"
|
||||
},
|
||||
"sdp_m_line_index_required": {
|
||||
"message": "Error negotiating stream for {device}"
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"loggers": ["roborock"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": [
|
||||
"python-roborock==4.2.0",
|
||||
"python-roborock==4.2.1",
|
||||
"vacuum-map-parser-roborock==0.1.4"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -391,15 +391,6 @@ Q7_B01_SENSOR_DESCRIPTIONS = [
|
||||
translation_key="mop_life_time_left",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
RoborockSensorDescriptionB01(
|
||||
key="total_cleaning_time",
|
||||
value_fn=lambda data: data.real_clean_time,
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||
suggested_unit_of_measurement=UnitOfTime.HOURS,
|
||||
translation_key="total_cleaning_time",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/sentry",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["sentry-sdk==1.45.1"]
|
||||
"requirements": ["sentry-sdk==2.48.0"]
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aioshelly"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aioshelly==13.23.0"],
|
||||
"requirements": ["aioshelly==13.23.1"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"name": "shelly*",
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["solarlog_cli"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["solarlog_cli==0.6.1"]
|
||||
"requirements": ["solarlog_cli==0.7.0"]
|
||||
}
|
||||
|
||||
@@ -80,10 +80,6 @@ class TelegramNotificationService(BaseNotificationService):
|
||||
def send_message(self, message="", **kwargs):
|
||||
"""Send a message to a user."""
|
||||
service_data = {ATTR_TARGET: kwargs.get(ATTR_TARGET, self._chat_id)}
|
||||
if ATTR_TITLE in kwargs:
|
||||
service_data.update({ATTR_TITLE: kwargs.get(ATTR_TITLE)})
|
||||
if message:
|
||||
service_data.update({ATTR_MESSAGE: message})
|
||||
data = kwargs.get(ATTR_DATA)
|
||||
|
||||
# Set message tag
|
||||
@@ -161,6 +157,12 @@ class TelegramNotificationService(BaseNotificationService):
|
||||
)
|
||||
|
||||
# Send message
|
||||
|
||||
if ATTR_TITLE in kwargs:
|
||||
service_data.update({ATTR_TITLE: kwargs.get(ATTR_TITLE)})
|
||||
if message:
|
||||
service_data.update({ATTR_MESSAGE: message})
|
||||
|
||||
_LOGGER.debug(
|
||||
"TELEGRAM NOTIFIER calling %s.send_message with %s",
|
||||
TELEGRAM_BOT_DOMAIN,
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/tibber",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["tibber"],
|
||||
"requirements": ["pyTibber==0.34.0"]
|
||||
"requirements": ["pyTibber==0.34.1"]
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["uiprotect", "unifi_discovery"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["uiprotect==7.33.3", "unifi-discovery==1.2.0"],
|
||||
"requirements": ["uiprotect==8.0.0", "unifi-discovery==1.2.0"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Ubiquiti Networks",
|
||||
|
||||
@@ -4,6 +4,7 @@ import logging
|
||||
|
||||
from pyvesync.base_devices import VeSyncHumidifier
|
||||
from pyvesync.base_devices.fan_base import VeSyncFanBase
|
||||
from pyvesync.base_devices.fryer_base import VeSyncFryer
|
||||
from pyvesync.base_devices.outlet_base import VeSyncOutlet
|
||||
from pyvesync.base_devices.purifier_base import VeSyncPurifier
|
||||
from pyvesync.base_devices.vesyncbasedevice import VeSyncBaseDevice
|
||||
@@ -62,3 +63,9 @@ def is_purifier(device: VeSyncBaseDevice) -> bool:
|
||||
"""Check if the device represents an air purifier."""
|
||||
|
||||
return isinstance(device, VeSyncPurifier)
|
||||
|
||||
|
||||
def is_air_fryer(device: VeSyncBaseDevice) -> bool:
|
||||
"""Check if the device represents an air fryer."""
|
||||
|
||||
return isinstance(device, VeSyncFryer)
|
||||
|
||||
@@ -62,3 +62,14 @@ OUTLET_NIGHT_LIGHT_LEVEL_ON = "on"
|
||||
PURIFIER_NIGHT_LIGHT_LEVEL_DIM = "dim"
|
||||
PURIFIER_NIGHT_LIGHT_LEVEL_OFF = "off"
|
||||
PURIFIER_NIGHT_LIGHT_LEVEL_ON = "on"
|
||||
|
||||
AIR_FRYER_MODE_MAP = {
|
||||
"cookend": "cooking_end",
|
||||
"cooking": "cooking",
|
||||
"cookstop": "cooking_stop",
|
||||
"heating": "heating",
|
||||
"preheatend": "preheat_end",
|
||||
"preheatstop": "preheat_stop",
|
||||
"pullout": "pull_out",
|
||||
"standby": "standby",
|
||||
}
|
||||
|
||||
@@ -23,14 +23,15 @@ from homeassistant.const import (
|
||||
UnitOfEnergy,
|
||||
UnitOfPower,
|
||||
UnitOfTemperature,
|
||||
UnitOfTime,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from .common import is_humidifier, is_outlet, rgetattr
|
||||
from .const import VS_DEVICES, VS_DISCOVERY
|
||||
from .common import is_air_fryer, is_humidifier, is_outlet, rgetattr
|
||||
from .const import AIR_FRYER_MODE_MAP, VS_DEVICES, VS_DISCOVERY
|
||||
from .coordinator import VesyncConfigEntry, VeSyncDataCoordinator
|
||||
from .entity import VeSyncBaseEntity
|
||||
|
||||
@@ -47,6 +48,8 @@ class VeSyncSensorEntityDescription(SensorEntityDescription):
|
||||
|
||||
exists_fn: Callable[[VeSyncBaseDevice], bool]
|
||||
|
||||
use_device_temperature_unit: bool = False
|
||||
|
||||
|
||||
SENSORS: tuple[VeSyncSensorEntityDescription, ...] = (
|
||||
VeSyncSensorEntityDescription(
|
||||
@@ -167,6 +170,59 @@ SENSORS: tuple[VeSyncSensorEntityDescription, ...] = (
|
||||
exists_fn=lambda device: is_humidifier(device)
|
||||
and device.state.temperature is not None,
|
||||
),
|
||||
VeSyncSensorEntityDescription(
|
||||
key="cook_status",
|
||||
translation_key="cook_status",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
value_fn=lambda device: AIR_FRYER_MODE_MAP.get(
|
||||
device.state.cook_status.lower(), device.state.cook_status.lower()
|
||||
),
|
||||
exists_fn=is_air_fryer,
|
||||
options=[
|
||||
"cooking_end",
|
||||
"cooking",
|
||||
"cooking_stop",
|
||||
"heating",
|
||||
"preheat_end",
|
||||
"preheat_stop",
|
||||
"pull_out",
|
||||
"standby",
|
||||
],
|
||||
),
|
||||
VeSyncSensorEntityDescription(
|
||||
key="current_temp",
|
||||
translation_key="current_temp",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
use_device_temperature_unit=True,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda device: device.state.current_temp,
|
||||
exists_fn=is_air_fryer,
|
||||
),
|
||||
VeSyncSensorEntityDescription(
|
||||
key="cook_set_temp",
|
||||
translation_key="cook_set_temp",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
use_device_temperature_unit=True,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda device: device.state.cook_set_temp,
|
||||
exists_fn=is_air_fryer,
|
||||
),
|
||||
VeSyncSensorEntityDescription(
|
||||
key="cook_set_time",
|
||||
translation_key="cook_set_time",
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||
value_fn=lambda device: device.state.cook_set_time,
|
||||
exists_fn=is_air_fryer,
|
||||
),
|
||||
VeSyncSensorEntityDescription(
|
||||
key="preheat_set_time",
|
||||
translation_key="preheat_set_time",
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||
value_fn=lambda device: device.state.preheat_set_time,
|
||||
exists_fn=is_air_fryer,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -232,3 +288,13 @@ class VeSyncSensorEntity(VeSyncBaseEntity, SensorEntity):
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the state of the sensor."""
|
||||
return self.entity_description.value_fn(self.device)
|
||||
|
||||
@property
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
"""Return the unit the value was reported in by the sensor."""
|
||||
if self.entity_description.use_device_temperature_unit:
|
||||
if self.device.temp_unit == "celsius":
|
||||
return UnitOfTemperature.CELSIUS
|
||||
if self.device.temp_unit == "fahrenheit":
|
||||
return UnitOfTemperature.FAHRENHEIT
|
||||
return super().native_unit_of_measurement
|
||||
|
||||
@@ -92,9 +92,31 @@
|
||||
"air_quality": {
|
||||
"name": "Air quality"
|
||||
},
|
||||
"cook_set_temp": {
|
||||
"name": "Cooking set temperature"
|
||||
},
|
||||
"cook_set_time": {
|
||||
"name": "Cooking set time"
|
||||
},
|
||||
"cook_status": {
|
||||
"name": "Cooking status",
|
||||
"state": {
|
||||
"cooking": "Cooking",
|
||||
"cooking_end": "Cooking finished",
|
||||
"cooking_stop": "Cooking stopped",
|
||||
"heating": "Preheating",
|
||||
"preheat_end": "Preheating finished",
|
||||
"preheat_stop": "Preheating stopped",
|
||||
"pull_out": "Drawer pulled out",
|
||||
"standby": "[%key:common::state::standby%]"
|
||||
}
|
||||
},
|
||||
"current_power": {
|
||||
"name": "Current power"
|
||||
},
|
||||
"current_temp": {
|
||||
"name": "Current temperature"
|
||||
},
|
||||
"current_voltage": {
|
||||
"name": "Current voltage"
|
||||
},
|
||||
@@ -112,6 +134,9 @@
|
||||
},
|
||||
"filter_life": {
|
||||
"name": "Filter lifetime"
|
||||
},
|
||||
"preheat_set_time": {
|
||||
"name": "Preheating set time"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
{
|
||||
"domain": "waterfurnace",
|
||||
"name": "WaterFurnace",
|
||||
"codeowners": [],
|
||||
"codeowners": ["@sdague", "@masterkoppa"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/waterfurnace",
|
||||
"integration_type": "device",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["waterfurnace"],
|
||||
"quality_scale": "legacy",
|
||||
|
||||
@@ -7468,7 +7468,7 @@
|
||||
},
|
||||
"waterfurnace": {
|
||||
"name": "WaterFurnace",
|
||||
"integration_type": "hub",
|
||||
"integration_type": "device",
|
||||
"config_flow": false,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
|
||||
@@ -40,7 +40,7 @@ hass-nabucasa==1.7.0
|
||||
hassil==3.5.0
|
||||
home-assistant-bluetooth==1.13.1
|
||||
home-assistant-frontend==20251229.1
|
||||
home-assistant-intents==2026.1.1
|
||||
home-assistant-intents==2026.1.6
|
||||
httpx==0.28.1
|
||||
ifaddr==0.2.0
|
||||
Jinja2==3.1.6
|
||||
@@ -226,3 +226,6 @@ gql<4.0.0
|
||||
|
||||
# Pin pytest-rerunfailures to prevent accidental breaks
|
||||
pytest-rerunfailures==16.0.1
|
||||
|
||||
# Fixes detected blocking call to load_default_certs https://github.com/home-assistant/core/issues/157475
|
||||
aiomqtt>=2.5.0
|
||||
|
||||
2
requirements.txt
generated
2
requirements.txt
generated
@@ -27,7 +27,7 @@ ha-ffmpeg==3.2.2
|
||||
hass-nabucasa==1.7.0
|
||||
hassil==3.5.0
|
||||
home-assistant-bluetooth==1.13.1
|
||||
home-assistant-intents==2026.1.1
|
||||
home-assistant-intents==2026.1.6
|
||||
httpx==0.28.1
|
||||
ifaddr==0.2.0
|
||||
Jinja2==3.1.6
|
||||
|
||||
24
requirements_all.txt
generated
24
requirements_all.txt
generated
@@ -390,7 +390,7 @@ aiorussound==4.9.0
|
||||
aioruuvigateway==0.1.0
|
||||
|
||||
# homeassistant.components.shelly
|
||||
aioshelly==13.23.0
|
||||
aioshelly==13.23.1
|
||||
|
||||
# homeassistant.components.skybell
|
||||
aioskybell==22.7.0
|
||||
@@ -1145,7 +1145,7 @@ growattServer==1.7.1
|
||||
gspread==5.5.0
|
||||
|
||||
# homeassistant.components.profiler
|
||||
guppy3==3.1.5;python_version<'3.14'
|
||||
guppy3==3.1.6
|
||||
|
||||
# homeassistant.components.iaqualink
|
||||
h2==4.3.0
|
||||
@@ -1216,7 +1216,7 @@ holidays==0.84
|
||||
home-assistant-frontend==20251229.1
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2026.1.1
|
||||
home-assistant-intents==2026.1.6
|
||||
|
||||
# homeassistant.components.gentex_homelink
|
||||
homelink-integration-api==0.0.1
|
||||
@@ -1662,9 +1662,6 @@ openai==2.11.0
|
||||
# homeassistant.components.openerz
|
||||
openerz-api==0.3.0
|
||||
|
||||
# homeassistant.components.openevse
|
||||
openevsewifi==1.1.2
|
||||
|
||||
# homeassistant.components.openhome
|
||||
openhomedevice==2.2.0
|
||||
|
||||
@@ -1867,7 +1864,7 @@ pyRFXtrx==0.31.1
|
||||
pySDCP==1
|
||||
|
||||
# homeassistant.components.tibber
|
||||
pyTibber==0.34.0
|
||||
pyTibber==0.34.1
|
||||
|
||||
# homeassistant.components.dlink
|
||||
pyW215==0.8.0
|
||||
@@ -2133,7 +2130,7 @@ pyitachip2ir==0.0.7
|
||||
pyituran==0.1.5
|
||||
|
||||
# homeassistant.components.jvc_projector
|
||||
pyjvcprojector==1.1.2
|
||||
pyjvcprojector==1.1.3
|
||||
|
||||
# homeassistant.components.kaleidescape
|
||||
pykaleidescape==1.0.2
|
||||
@@ -2558,6 +2555,9 @@ python-open-router==0.3.3
|
||||
# homeassistant.components.swiss_public_transport
|
||||
python-opendata-transport==0.5.0
|
||||
|
||||
# homeassistant.components.openevse
|
||||
python-openevse-http==0.2.1
|
||||
|
||||
# homeassistant.components.opensky
|
||||
python-opensky==1.0.1
|
||||
|
||||
@@ -2581,7 +2581,7 @@ python-rabbitair==0.0.8
|
||||
python-ripple-api==0.0.3
|
||||
|
||||
# homeassistant.components.roborock
|
||||
python-roborock==4.2.0
|
||||
python-roborock==4.2.1
|
||||
|
||||
# homeassistant.components.smarttub
|
||||
python-smarttub==0.0.46
|
||||
@@ -2838,7 +2838,7 @@ sensoterra==2.0.1
|
||||
sentence-stream==1.2.0
|
||||
|
||||
# homeassistant.components.sentry
|
||||
sentry-sdk==1.45.1
|
||||
sentry-sdk==2.48.0
|
||||
|
||||
# homeassistant.components.homeassistant_hardware
|
||||
# homeassistant.components.zha
|
||||
@@ -2896,7 +2896,7 @@ solaredge-local==0.2.3
|
||||
solaredge-web==0.0.1
|
||||
|
||||
# homeassistant.components.solarlog
|
||||
solarlog_cli==0.6.1
|
||||
solarlog_cli==0.7.0
|
||||
|
||||
# homeassistant.components.solax
|
||||
solax==3.2.3
|
||||
@@ -3078,7 +3078,7 @@ typedmonarchmoney==0.4.4
|
||||
uasiren==0.0.1
|
||||
|
||||
# homeassistant.components.unifiprotect
|
||||
uiprotect==7.33.3
|
||||
uiprotect==8.0.0
|
||||
|
||||
# homeassistant.components.landisgyr_heat_meter
|
||||
ultraheat-api==0.5.7
|
||||
|
||||
24
requirements_test_all.txt
generated
24
requirements_test_all.txt
generated
@@ -375,7 +375,7 @@ aiorussound==4.9.0
|
||||
aioruuvigateway==0.1.0
|
||||
|
||||
# homeassistant.components.shelly
|
||||
aioshelly==13.23.0
|
||||
aioshelly==13.23.1
|
||||
|
||||
# homeassistant.components.skybell
|
||||
aioskybell==22.7.0
|
||||
@@ -1015,7 +1015,7 @@ growattServer==1.7.1
|
||||
gspread==5.5.0
|
||||
|
||||
# homeassistant.components.profiler
|
||||
guppy3==3.1.5;python_version<'3.14'
|
||||
guppy3==3.1.6
|
||||
|
||||
# homeassistant.components.iaqualink
|
||||
h2==4.3.0
|
||||
@@ -1074,7 +1074,7 @@ holidays==0.84
|
||||
home-assistant-frontend==20251229.1
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2026.1.1
|
||||
home-assistant-intents==2026.1.6
|
||||
|
||||
# homeassistant.components.gentex_homelink
|
||||
homelink-integration-api==0.0.1
|
||||
@@ -1445,9 +1445,6 @@ openai==2.11.0
|
||||
# homeassistant.components.openerz
|
||||
openerz-api==0.3.0
|
||||
|
||||
# homeassistant.components.openevse
|
||||
openevsewifi==1.1.2
|
||||
|
||||
# homeassistant.components.openhome
|
||||
openhomedevice==2.2.0
|
||||
|
||||
@@ -1598,7 +1595,7 @@ pyHomee==1.3.8
|
||||
pyRFXtrx==0.31.1
|
||||
|
||||
# homeassistant.components.tibber
|
||||
pyTibber==0.34.0
|
||||
pyTibber==0.34.1
|
||||
|
||||
# homeassistant.components.dlink
|
||||
pyW215==0.8.0
|
||||
@@ -1804,7 +1801,7 @@ pyisy==3.4.1
|
||||
pyituran==0.1.5
|
||||
|
||||
# homeassistant.components.jvc_projector
|
||||
pyjvcprojector==1.1.2
|
||||
pyjvcprojector==1.1.3
|
||||
|
||||
# homeassistant.components.kaleidescape
|
||||
pykaleidescape==1.0.2
|
||||
@@ -2148,6 +2145,9 @@ python-open-router==0.3.3
|
||||
# homeassistant.components.swiss_public_transport
|
||||
python-opendata-transport==0.5.0
|
||||
|
||||
# homeassistant.components.openevse
|
||||
python-openevse-http==0.2.1
|
||||
|
||||
# homeassistant.components.opensky
|
||||
python-opensky==1.0.1
|
||||
|
||||
@@ -2168,7 +2168,7 @@ python-pooldose==0.8.1
|
||||
python-rabbitair==0.0.8
|
||||
|
||||
# homeassistant.components.roborock
|
||||
python-roborock==4.2.0
|
||||
python-roborock==4.2.1
|
||||
|
||||
# homeassistant.components.smarttub
|
||||
python-smarttub==0.0.46
|
||||
@@ -2380,7 +2380,7 @@ sensoterra==2.0.1
|
||||
sentence-stream==1.2.0
|
||||
|
||||
# homeassistant.components.sentry
|
||||
sentry-sdk==1.45.1
|
||||
sentry-sdk==2.48.0
|
||||
|
||||
# homeassistant.components.homeassistant_hardware
|
||||
# homeassistant.components.zha
|
||||
@@ -2423,7 +2423,7 @@ soco==0.30.14
|
||||
solaredge-web==0.0.1
|
||||
|
||||
# homeassistant.components.solarlog
|
||||
solarlog_cli==0.6.1
|
||||
solarlog_cli==0.7.0
|
||||
|
||||
# homeassistant.components.solax
|
||||
solax==3.2.3
|
||||
@@ -2572,7 +2572,7 @@ typedmonarchmoney==0.4.4
|
||||
uasiren==0.0.1
|
||||
|
||||
# homeassistant.components.unifiprotect
|
||||
uiprotect==7.33.3
|
||||
uiprotect==8.0.0
|
||||
|
||||
# homeassistant.components.landisgyr_heat_meter
|
||||
ultraheat-api==0.5.7
|
||||
|
||||
@@ -217,6 +217,9 @@ gql<4.0.0
|
||||
|
||||
# Pin pytest-rerunfailures to prevent accidental breaks
|
||||
pytest-rerunfailures==16.0.1
|
||||
|
||||
# Fixes detected blocking call to load_default_certs https://github.com/home-assistant/core/issues/157475
|
||||
aiomqtt>=2.5.0
|
||||
"""
|
||||
|
||||
GENERATED_MESSAGE = (
|
||||
|
||||
@@ -10,6 +10,8 @@ from homeassistant.const import (
|
||||
ATTR_DEVICE_ID,
|
||||
ATTR_FLOOR_ID,
|
||||
ATTR_LABEL_ID,
|
||||
CONF_ABOVE,
|
||||
CONF_BELOW,
|
||||
CONF_ENTITY_ID,
|
||||
CONF_OPTIONS,
|
||||
CONF_PLATFORM,
|
||||
@@ -25,6 +27,12 @@ from homeassistant.helpers import (
|
||||
floor_registry as fr,
|
||||
label_registry as lr,
|
||||
)
|
||||
from homeassistant.helpers.trigger import (
|
||||
CONF_LOWER_LIMIT,
|
||||
CONF_THRESHOLD_TYPE,
|
||||
CONF_UPPER_LIMIT,
|
||||
ThresholdType,
|
||||
)
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import MockConfigEntry, mock_device_registry
|
||||
@@ -164,6 +172,88 @@ class StateDescription(TypedDict):
|
||||
count: int
|
||||
|
||||
|
||||
class ConditionStateDescription(TypedDict):
|
||||
"""Test state and expected service call count."""
|
||||
|
||||
included: _StateDescription
|
||||
excluded: _StateDescription
|
||||
count: int
|
||||
valid: bool
|
||||
|
||||
|
||||
def parametrize_condition_states(
|
||||
*,
|
||||
condition: str,
|
||||
condition_options: dict[str, Any] | None = None,
|
||||
target_states: list[str | None | tuple[str | None, dict]],
|
||||
other_states: list[str | None | tuple[str | None, dict]],
|
||||
additional_attributes: dict | None = None,
|
||||
) -> list[tuple[str, dict[str, Any], list[ConditionStateDescription]]]:
|
||||
"""Parametrize states and expected service call counts.
|
||||
|
||||
The target_states and other_states iterables are either iterables of
|
||||
states or iterables of (state, attributes) tuples.
|
||||
|
||||
Returns a list of tuples with (trigger, list of states),
|
||||
where states is a list of ConditionStateDescription dicts.
|
||||
"""
|
||||
|
||||
additional_attributes = additional_attributes or {}
|
||||
condition_options = condition_options or {}
|
||||
|
||||
def state_with_attributes(
|
||||
state: str | None | tuple[str | None, dict], count: int, valid: bool
|
||||
) -> ConditionStateDescription:
|
||||
"""Return (state, attributes) dict."""
|
||||
if isinstance(state, str) or state is None:
|
||||
return {
|
||||
"included": {
|
||||
"state": state,
|
||||
"attributes": additional_attributes,
|
||||
},
|
||||
"excluded": {
|
||||
"state": state,
|
||||
"attributes": {},
|
||||
},
|
||||
"count": count,
|
||||
"valid": valid,
|
||||
}
|
||||
return {
|
||||
"included": {
|
||||
"state": state[0],
|
||||
"attributes": state[1] | additional_attributes,
|
||||
},
|
||||
"excluded": {
|
||||
"state": state[0],
|
||||
"attributes": state[1],
|
||||
},
|
||||
"count": count,
|
||||
"valid": valid,
|
||||
}
|
||||
|
||||
return [
|
||||
(
|
||||
condition,
|
||||
condition_options,
|
||||
list(
|
||||
itertools.chain(
|
||||
(state_with_attributes(None, 0, False),),
|
||||
(state_with_attributes(STATE_UNAVAILABLE, 0, False),),
|
||||
(state_with_attributes(STATE_UNKNOWN, 0, False),),
|
||||
(
|
||||
state_with_attributes(other_state, 0, True)
|
||||
for other_state in other_states
|
||||
),
|
||||
(
|
||||
state_with_attributes(target_state, 1, True)
|
||||
for target_state in target_states
|
||||
),
|
||||
)
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def parametrize_trigger_states(
|
||||
*,
|
||||
trigger: str,
|
||||
@@ -194,7 +284,7 @@ def parametrize_trigger_states(
|
||||
|
||||
def state_with_attributes(
|
||||
state: str | None | tuple[str | None, dict], count: int
|
||||
) -> dict:
|
||||
) -> StateDescription:
|
||||
"""Return (state, attributes) dict."""
|
||||
if isinstance(state, str) or state is None:
|
||||
return {
|
||||
@@ -343,6 +433,123 @@ def parametrize_trigger_states(
|
||||
return tests
|
||||
|
||||
|
||||
def parametrize_numerical_attribute_changed_trigger_states(
|
||||
trigger: str, state: str, attribute: str
|
||||
) -> list[tuple[str, dict[str, Any], list[StateDescription]]]:
|
||||
"""Parametrize states and expected service call counts for numerical changed triggers."""
|
||||
return [
|
||||
*parametrize_trigger_states(
|
||||
trigger=trigger,
|
||||
trigger_options={},
|
||||
target_states=[
|
||||
(state, {attribute: 0}),
|
||||
(state, {attribute: 50}),
|
||||
(state, {attribute: 100}),
|
||||
],
|
||||
other_states=[(state, {attribute: None})],
|
||||
retrigger_on_target_state=True,
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger=trigger,
|
||||
trigger_options={CONF_ABOVE: 10},
|
||||
target_states=[
|
||||
(state, {attribute: 50}),
|
||||
(state, {attribute: 100}),
|
||||
],
|
||||
other_states=[
|
||||
(state, {attribute: None}),
|
||||
(state, {attribute: 0}),
|
||||
],
|
||||
retrigger_on_target_state=True,
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger=trigger,
|
||||
trigger_options={CONF_BELOW: 90},
|
||||
target_states=[
|
||||
(state, {attribute: 0}),
|
||||
(state, {attribute: 50}),
|
||||
],
|
||||
other_states=[
|
||||
(state, {attribute: None}),
|
||||
(state, {attribute: 100}),
|
||||
],
|
||||
retrigger_on_target_state=True,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def parametrize_numerical_attribute_crossed_threshold_trigger_states(
|
||||
trigger: str, state: str, attribute: str
|
||||
) -> list[tuple[str, dict[str, Any], list[StateDescription]]]:
|
||||
"""Parametrize states and expected service call counts for numerical crossed threshold triggers."""
|
||||
return [
|
||||
*parametrize_trigger_states(
|
||||
trigger=trigger,
|
||||
trigger_options={
|
||||
CONF_THRESHOLD_TYPE: ThresholdType.BETWEEN,
|
||||
CONF_LOWER_LIMIT: 10,
|
||||
CONF_UPPER_LIMIT: 90,
|
||||
},
|
||||
target_states=[
|
||||
(state, {attribute: 50}),
|
||||
(state, {attribute: 60}),
|
||||
],
|
||||
other_states=[
|
||||
(state, {attribute: None}),
|
||||
(state, {attribute: 0}),
|
||||
(state, {attribute: 100}),
|
||||
],
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger=trigger,
|
||||
trigger_options={
|
||||
CONF_THRESHOLD_TYPE: ThresholdType.OUTSIDE,
|
||||
CONF_LOWER_LIMIT: 10,
|
||||
CONF_UPPER_LIMIT: 90,
|
||||
},
|
||||
target_states=[
|
||||
(state, {attribute: 0}),
|
||||
(state, {attribute: 100}),
|
||||
],
|
||||
other_states=[
|
||||
(state, {attribute: None}),
|
||||
(state, {attribute: 50}),
|
||||
(state, {attribute: 60}),
|
||||
],
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger=trigger,
|
||||
trigger_options={
|
||||
CONF_THRESHOLD_TYPE: ThresholdType.ABOVE,
|
||||
CONF_LOWER_LIMIT: 10,
|
||||
},
|
||||
target_states=[
|
||||
(state, {attribute: 50}),
|
||||
(state, {attribute: 100}),
|
||||
],
|
||||
other_states=[
|
||||
(state, {attribute: None}),
|
||||
(state, {attribute: 0}),
|
||||
],
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger=trigger,
|
||||
trigger_options={
|
||||
CONF_THRESHOLD_TYPE: ThresholdType.BELOW,
|
||||
CONF_UPPER_LIMIT: 90,
|
||||
},
|
||||
target_states=[
|
||||
(state, {attribute: 0}),
|
||||
(state, {attribute: 50}),
|
||||
],
|
||||
other_states=[
|
||||
(state, {attribute: None}),
|
||||
(state, {attribute: 100}),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
async def arm_trigger(
|
||||
hass: HomeAssistant,
|
||||
trigger: str,
|
||||
|
||||
@@ -13,6 +13,7 @@ import pytest
|
||||
from homeassistant.components.braviatv.const import (
|
||||
CONF_NICKNAME,
|
||||
CONF_USE_PSK,
|
||||
CONF_USE_SSL,
|
||||
DOMAIN,
|
||||
NICKNAME_PREFIX,
|
||||
)
|
||||
@@ -131,7 +132,7 @@ async def test_ssdp_discovery(hass: HomeAssistant) -> None:
|
||||
assert result["step_id"] == "authorize"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_USE_PSK: False}
|
||||
result["flow_id"], user_input={CONF_USE_PSK: False, CONF_USE_SSL: False}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
@@ -148,6 +149,7 @@ async def test_ssdp_discovery(hass: HomeAssistant) -> None:
|
||||
CONF_HOST: "bravia-host",
|
||||
CONF_PIN: "1234",
|
||||
CONF_USE_PSK: False,
|
||||
CONF_USE_SSL: False,
|
||||
CONF_MAC: "AA:BB:CC:DD:EE:FF",
|
||||
CONF_CLIENT_ID: uuid,
|
||||
CONF_NICKNAME: f"{NICKNAME_PREFIX} {uuid[:6]}",
|
||||
@@ -307,8 +309,17 @@ async def test_duplicate_error(hass: HomeAssistant) -> None:
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_create_entry(hass: HomeAssistant) -> None:
|
||||
"""Test that entry is added correctly with PIN auth."""
|
||||
@pytest.mark.parametrize(
|
||||
("use_psk", "use_ssl"),
|
||||
[
|
||||
(True, False),
|
||||
(False, False),
|
||||
(True, True),
|
||||
(False, True),
|
||||
],
|
||||
)
|
||||
async def test_create_entry(hass: HomeAssistant, use_psk, use_ssl) -> None:
|
||||
"""Test that entry is added correctly."""
|
||||
uuid = await instance_id.async_get(hass)
|
||||
|
||||
with (
|
||||
@@ -328,14 +339,14 @@ async def test_create_entry(hass: HomeAssistant) -> None:
|
||||
assert result["step_id"] == "authorize"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_USE_PSK: False}
|
||||
result["flow_id"], user_input={CONF_USE_PSK: use_psk, CONF_USE_SSL: use_ssl}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "pin"
|
||||
assert result["step_id"] == "psk" if use_psk else "pin"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_PIN: "1234"}
|
||||
result["flow_id"], user_input={CONF_PIN: "secret"}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
@@ -343,50 +354,18 @@ async def test_create_entry(hass: HomeAssistant) -> None:
|
||||
assert result["title"] == "BRAVIA TV-Model"
|
||||
assert result["data"] == {
|
||||
CONF_HOST: "bravia-host",
|
||||
CONF_PIN: "1234",
|
||||
CONF_USE_PSK: False,
|
||||
CONF_MAC: "AA:BB:CC:DD:EE:FF",
|
||||
CONF_CLIENT_ID: uuid,
|
||||
CONF_NICKNAME: f"{NICKNAME_PREFIX} {uuid[:6]}",
|
||||
}
|
||||
|
||||
|
||||
async def test_create_entry_psk(hass: HomeAssistant) -> None:
|
||||
"""Test that entry is added correctly with PSK auth."""
|
||||
with (
|
||||
patch("pybravia.BraviaClient.connect"),
|
||||
patch("pybravia.BraviaClient.set_wol_mode"),
|
||||
patch(
|
||||
"pybravia.BraviaClient.get_system_info",
|
||||
return_value=BRAVIA_SYSTEM_INFO,
|
||||
),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "bravia-host"}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "authorize"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_USE_PSK: True}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "psk"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_PIN: "mypsk"}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["result"].unique_id == "very_unique_string"
|
||||
assert result["title"] == "BRAVIA TV-Model"
|
||||
assert result["data"] == {
|
||||
CONF_HOST: "bravia-host",
|
||||
CONF_PIN: "mypsk",
|
||||
CONF_USE_PSK: True,
|
||||
CONF_PIN: "secret",
|
||||
CONF_USE_PSK: use_psk,
|
||||
CONF_USE_SSL: use_ssl,
|
||||
CONF_MAC: "AA:BB:CC:DD:EE:FF",
|
||||
**(
|
||||
{
|
||||
CONF_CLIENT_ID: uuid,
|
||||
CONF_NICKNAME: f"{NICKNAME_PREFIX} {uuid[:6]}",
|
||||
}
|
||||
if not use_psk
|
||||
else {}
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -20,25 +20,19 @@ from homeassistant.components.climate.trigger import CONF_HVAC_MODE
|
||||
from homeassistant.const import (
|
||||
ATTR_LABEL_ID,
|
||||
ATTR_TEMPERATURE,
|
||||
CONF_ABOVE,
|
||||
CONF_BELOW,
|
||||
CONF_ENTITY_ID,
|
||||
CONF_OPTIONS,
|
||||
CONF_TARGET,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.helpers.trigger import (
|
||||
CONF_LOWER_LIMIT,
|
||||
CONF_THRESHOLD_TYPE,
|
||||
CONF_UPPER_LIMIT,
|
||||
ThresholdType,
|
||||
async_validate_trigger_config,
|
||||
)
|
||||
from homeassistant.helpers.trigger import async_validate_trigger_config
|
||||
|
||||
from tests.components import (
|
||||
StateDescription,
|
||||
arm_trigger,
|
||||
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,
|
||||
@@ -153,123 +147,6 @@ async def test_climate_trigger_validation(
|
||||
)
|
||||
|
||||
|
||||
def parametrize_xxx_changed_trigger_states(
|
||||
trigger: str, attribute: str
|
||||
) -> list[tuple[str, dict[str, Any], list[StateDescription]]]:
|
||||
"""Parametrize states and expected service call counts for xxx_changed triggers."""
|
||||
return [
|
||||
*parametrize_trigger_states(
|
||||
trigger=trigger,
|
||||
trigger_options={},
|
||||
target_states=[
|
||||
(HVACMode.AUTO, {attribute: 0}),
|
||||
(HVACMode.AUTO, {attribute: 50}),
|
||||
(HVACMode.AUTO, {attribute: 100}),
|
||||
],
|
||||
other_states=[(HVACMode.AUTO, {attribute: None})],
|
||||
retrigger_on_target_state=True,
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger=trigger,
|
||||
trigger_options={CONF_ABOVE: 10},
|
||||
target_states=[
|
||||
(HVACMode.AUTO, {attribute: 50}),
|
||||
(HVACMode.AUTO, {attribute: 100}),
|
||||
],
|
||||
other_states=[
|
||||
(HVACMode.AUTO, {attribute: None}),
|
||||
(HVACMode.AUTO, {attribute: 0}),
|
||||
],
|
||||
retrigger_on_target_state=True,
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger=trigger,
|
||||
trigger_options={CONF_BELOW: 90},
|
||||
target_states=[
|
||||
(HVACMode.AUTO, {attribute: 0}),
|
||||
(HVACMode.AUTO, {attribute: 50}),
|
||||
],
|
||||
other_states=[
|
||||
(HVACMode.AUTO, {attribute: None}),
|
||||
(HVACMode.AUTO, {attribute: 100}),
|
||||
],
|
||||
retrigger_on_target_state=True,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def parametrize_xxx_crossed_threshold_trigger_states(
|
||||
trigger: str, attribute: str
|
||||
) -> list[tuple[str, dict[str, Any], list[StateDescription]]]:
|
||||
"""Parametrize states and expected service call counts for xxx_crossed_threshold triggers."""
|
||||
return [
|
||||
*parametrize_trigger_states(
|
||||
trigger=trigger,
|
||||
trigger_options={
|
||||
CONF_THRESHOLD_TYPE: ThresholdType.BETWEEN,
|
||||
CONF_LOWER_LIMIT: 10,
|
||||
CONF_UPPER_LIMIT: 90,
|
||||
},
|
||||
target_states=[
|
||||
(HVACMode.AUTO, {attribute: 50}),
|
||||
(HVACMode.AUTO, {attribute: 60}),
|
||||
],
|
||||
other_states=[
|
||||
(HVACMode.AUTO, {attribute: None}),
|
||||
(HVACMode.AUTO, {attribute: 0}),
|
||||
(HVACMode.AUTO, {attribute: 100}),
|
||||
],
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger=trigger,
|
||||
trigger_options={
|
||||
CONF_THRESHOLD_TYPE: ThresholdType.OUTSIDE,
|
||||
CONF_LOWER_LIMIT: 10,
|
||||
CONF_UPPER_LIMIT: 90,
|
||||
},
|
||||
target_states=[
|
||||
(HVACMode.AUTO, {attribute: 0}),
|
||||
(HVACMode.AUTO, {attribute: 100}),
|
||||
],
|
||||
other_states=[
|
||||
(HVACMode.AUTO, {attribute: None}),
|
||||
(HVACMode.AUTO, {attribute: 50}),
|
||||
(HVACMode.AUTO, {attribute: 60}),
|
||||
],
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger=trigger,
|
||||
trigger_options={
|
||||
CONF_THRESHOLD_TYPE: ThresholdType.ABOVE,
|
||||
CONF_LOWER_LIMIT: 10,
|
||||
},
|
||||
target_states=[
|
||||
(HVACMode.AUTO, {attribute: 50}),
|
||||
(HVACMode.AUTO, {attribute: 100}),
|
||||
],
|
||||
other_states=[
|
||||
(HVACMode.AUTO, {attribute: None}),
|
||||
(HVACMode.AUTO, {attribute: 0}),
|
||||
],
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger=trigger,
|
||||
trigger_options={
|
||||
CONF_THRESHOLD_TYPE: ThresholdType.BELOW,
|
||||
CONF_UPPER_LIMIT: 90,
|
||||
},
|
||||
target_states=[
|
||||
(HVACMode.AUTO, {attribute: 0}),
|
||||
(HVACMode.AUTO, {attribute: 50}),
|
||||
],
|
||||
other_states=[
|
||||
(HVACMode.AUTO, {attribute: None}),
|
||||
(HVACMode.AUTO, {attribute: 100}),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_experimental_triggers_conditions")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
||||
@@ -351,29 +228,37 @@ async def test_climate_state_trigger_behavior_any(
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "states"),
|
||||
[
|
||||
*parametrize_xxx_changed_trigger_states(
|
||||
"climate.current_humidity_changed", ATTR_CURRENT_HUMIDITY
|
||||
*parametrize_numerical_attribute_changed_trigger_states(
|
||||
"climate.current_humidity_changed", HVACMode.AUTO, ATTR_CURRENT_HUMIDITY
|
||||
),
|
||||
*parametrize_xxx_changed_trigger_states(
|
||||
"climate.current_temperature_changed", ATTR_CURRENT_TEMPERATURE
|
||||
*parametrize_numerical_attribute_changed_trigger_states(
|
||||
"climate.current_temperature_changed",
|
||||
HVACMode.AUTO,
|
||||
ATTR_CURRENT_TEMPERATURE,
|
||||
),
|
||||
*parametrize_xxx_changed_trigger_states(
|
||||
"climate.target_humidity_changed", ATTR_HUMIDITY
|
||||
*parametrize_numerical_attribute_changed_trigger_states(
|
||||
"climate.target_humidity_changed", HVACMode.AUTO, ATTR_HUMIDITY
|
||||
),
|
||||
*parametrize_xxx_changed_trigger_states(
|
||||
"climate.target_temperature_changed", ATTR_TEMPERATURE
|
||||
*parametrize_numerical_attribute_changed_trigger_states(
|
||||
"climate.target_temperature_changed", HVACMode.AUTO, ATTR_TEMPERATURE
|
||||
),
|
||||
*parametrize_xxx_crossed_threshold_trigger_states(
|
||||
"climate.current_humidity_crossed_threshold", ATTR_CURRENT_HUMIDITY
|
||||
*parametrize_numerical_attribute_crossed_threshold_trigger_states(
|
||||
"climate.current_humidity_crossed_threshold",
|
||||
HVACMode.AUTO,
|
||||
ATTR_CURRENT_HUMIDITY,
|
||||
),
|
||||
*parametrize_xxx_crossed_threshold_trigger_states(
|
||||
"climate.current_temperature_crossed_threshold", ATTR_CURRENT_TEMPERATURE
|
||||
*parametrize_numerical_attribute_crossed_threshold_trigger_states(
|
||||
"climate.current_temperature_crossed_threshold",
|
||||
HVACMode.AUTO,
|
||||
ATTR_CURRENT_TEMPERATURE,
|
||||
),
|
||||
*parametrize_xxx_crossed_threshold_trigger_states(
|
||||
"climate.target_humidity_crossed_threshold", ATTR_HUMIDITY
|
||||
*parametrize_numerical_attribute_crossed_threshold_trigger_states(
|
||||
"climate.target_humidity_crossed_threshold", HVACMode.AUTO, ATTR_HUMIDITY
|
||||
),
|
||||
*parametrize_xxx_crossed_threshold_trigger_states(
|
||||
"climate.target_temperature_crossed_threshold", ATTR_TEMPERATURE
|
||||
*parametrize_numerical_attribute_crossed_threshold_trigger_states(
|
||||
"climate.target_temperature_crossed_threshold",
|
||||
HVACMode.AUTO,
|
||||
ATTR_TEMPERATURE,
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger="climate.started_cooling",
|
||||
@@ -512,17 +397,23 @@ async def test_climate_state_trigger_behavior_first(
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "states"),
|
||||
[
|
||||
*parametrize_xxx_crossed_threshold_trigger_states(
|
||||
"climate.current_humidity_crossed_threshold", ATTR_CURRENT_HUMIDITY
|
||||
*parametrize_numerical_attribute_crossed_threshold_trigger_states(
|
||||
"climate.current_humidity_crossed_threshold",
|
||||
HVACMode.AUTO,
|
||||
ATTR_CURRENT_HUMIDITY,
|
||||
),
|
||||
*parametrize_xxx_crossed_threshold_trigger_states(
|
||||
"climate.current_temperature_crossed_threshold", ATTR_CURRENT_TEMPERATURE
|
||||
*parametrize_numerical_attribute_crossed_threshold_trigger_states(
|
||||
"climate.current_temperature_crossed_threshold",
|
||||
HVACMode.AUTO,
|
||||
ATTR_CURRENT_TEMPERATURE,
|
||||
),
|
||||
*parametrize_xxx_crossed_threshold_trigger_states(
|
||||
"climate.target_humidity_crossed_threshold", ATTR_HUMIDITY
|
||||
*parametrize_numerical_attribute_crossed_threshold_trigger_states(
|
||||
"climate.target_humidity_crossed_threshold", HVACMode.AUTO, ATTR_HUMIDITY
|
||||
),
|
||||
*parametrize_xxx_crossed_threshold_trigger_states(
|
||||
"climate.target_temperature_crossed_threshold", ATTR_TEMPERATURE
|
||||
*parametrize_numerical_attribute_crossed_threshold_trigger_states(
|
||||
"climate.target_temperature_crossed_threshold",
|
||||
HVACMode.AUTO,
|
||||
ATTR_TEMPERATURE,
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger="climate.started_cooling",
|
||||
@@ -661,17 +552,23 @@ async def test_climate_state_trigger_behavior_last(
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "states"),
|
||||
[
|
||||
*parametrize_xxx_crossed_threshold_trigger_states(
|
||||
"climate.current_humidity_crossed_threshold", ATTR_CURRENT_HUMIDITY
|
||||
*parametrize_numerical_attribute_crossed_threshold_trigger_states(
|
||||
"climate.current_humidity_crossed_threshold",
|
||||
HVACMode.AUTO,
|
||||
ATTR_CURRENT_HUMIDITY,
|
||||
),
|
||||
*parametrize_xxx_crossed_threshold_trigger_states(
|
||||
"climate.current_temperature_crossed_threshold", ATTR_CURRENT_TEMPERATURE
|
||||
*parametrize_numerical_attribute_crossed_threshold_trigger_states(
|
||||
"climate.current_temperature_crossed_threshold",
|
||||
HVACMode.AUTO,
|
||||
ATTR_CURRENT_TEMPERATURE,
|
||||
),
|
||||
*parametrize_xxx_crossed_threshold_trigger_states(
|
||||
"climate.target_humidity_crossed_threshold", ATTR_HUMIDITY
|
||||
*parametrize_numerical_attribute_crossed_threshold_trigger_states(
|
||||
"climate.target_humidity_crossed_threshold", HVACMode.AUTO, ATTR_HUMIDITY
|
||||
),
|
||||
*parametrize_xxx_crossed_threshold_trigger_states(
|
||||
"climate.target_temperature_crossed_threshold", ATTR_TEMPERATURE
|
||||
*parametrize_numerical_attribute_crossed_threshold_trigger_states(
|
||||
"climate.target_temperature_crossed_threshold",
|
||||
HVACMode.AUTO,
|
||||
ATTR_TEMPERATURE,
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger="climate.started_cooling",
|
||||
|
||||
@@ -11,25 +11,14 @@ from homeassistant.components.humidifier.const import (
|
||||
ATTR_CURRENT_HUMIDITY,
|
||||
HumidifierAction,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_LABEL_ID,
|
||||
CONF_ABOVE,
|
||||
CONF_BELOW,
|
||||
CONF_ENTITY_ID,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
)
|
||||
from homeassistant.const import ATTR_LABEL_ID, CONF_ENTITY_ID, STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.helpers.trigger import (
|
||||
CONF_LOWER_LIMIT,
|
||||
CONF_THRESHOLD_TYPE,
|
||||
CONF_UPPER_LIMIT,
|
||||
ThresholdType,
|
||||
)
|
||||
|
||||
from tests.components import (
|
||||
StateDescription,
|
||||
arm_trigger,
|
||||
parametrize_numerical_attribute_changed_trigger_states,
|
||||
parametrize_numerical_attribute_crossed_threshold_trigger_states,
|
||||
parametrize_target_entities,
|
||||
parametrize_trigger_states,
|
||||
set_or_remove_state,
|
||||
@@ -82,123 +71,6 @@ async def test_humidifier_triggers_gated_by_labs_flag(
|
||||
) in caplog.text
|
||||
|
||||
|
||||
def parametrize_xxx_changed_trigger_states(
|
||||
trigger: str, attribute: str
|
||||
) -> list[tuple[str, dict[str, Any], list[StateDescription]]]:
|
||||
"""Parametrize states and expected service call counts for xxx_changed triggers."""
|
||||
return [
|
||||
*parametrize_trigger_states(
|
||||
trigger=trigger,
|
||||
trigger_options={},
|
||||
target_states=[
|
||||
(STATE_ON, {attribute: 0}),
|
||||
(STATE_ON, {attribute: 50}),
|
||||
(STATE_ON, {attribute: 100}),
|
||||
],
|
||||
other_states=[(STATE_ON, {attribute: None})],
|
||||
retrigger_on_target_state=True,
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger=trigger,
|
||||
trigger_options={CONF_ABOVE: 10},
|
||||
target_states=[
|
||||
(STATE_ON, {attribute: 50}),
|
||||
(STATE_ON, {attribute: 100}),
|
||||
],
|
||||
other_states=[
|
||||
(STATE_ON, {attribute: None}),
|
||||
(STATE_ON, {attribute: 0}),
|
||||
],
|
||||
retrigger_on_target_state=True,
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger=trigger,
|
||||
trigger_options={CONF_BELOW: 90},
|
||||
target_states=[
|
||||
(STATE_ON, {attribute: 0}),
|
||||
(STATE_ON, {attribute: 50}),
|
||||
],
|
||||
other_states=[
|
||||
(STATE_ON, {attribute: None}),
|
||||
(STATE_ON, {attribute: 100}),
|
||||
],
|
||||
retrigger_on_target_state=True,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def parametrize_xxx_crossed_threshold_trigger_states(
|
||||
trigger: str, attribute: str
|
||||
) -> list[tuple[str, dict[str, Any], list[StateDescription]]]:
|
||||
"""Parametrize states and expected service call counts for xxx_crossed_threshold triggers."""
|
||||
return [
|
||||
*parametrize_trigger_states(
|
||||
trigger=trigger,
|
||||
trigger_options={
|
||||
CONF_THRESHOLD_TYPE: ThresholdType.BETWEEN,
|
||||
CONF_LOWER_LIMIT: 10,
|
||||
CONF_UPPER_LIMIT: 90,
|
||||
},
|
||||
target_states=[
|
||||
(STATE_ON, {attribute: 50}),
|
||||
(STATE_ON, {attribute: 60}),
|
||||
],
|
||||
other_states=[
|
||||
(STATE_ON, {attribute: None}),
|
||||
(STATE_ON, {attribute: 0}),
|
||||
(STATE_ON, {attribute: 100}),
|
||||
],
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger=trigger,
|
||||
trigger_options={
|
||||
CONF_THRESHOLD_TYPE: ThresholdType.OUTSIDE,
|
||||
CONF_LOWER_LIMIT: 10,
|
||||
CONF_UPPER_LIMIT: 90,
|
||||
},
|
||||
target_states=[
|
||||
(STATE_ON, {attribute: 0}),
|
||||
(STATE_ON, {attribute: 100}),
|
||||
],
|
||||
other_states=[
|
||||
(STATE_ON, {attribute: None}),
|
||||
(STATE_ON, {attribute: 50}),
|
||||
(STATE_ON, {attribute: 60}),
|
||||
],
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger=trigger,
|
||||
trigger_options={
|
||||
CONF_THRESHOLD_TYPE: ThresholdType.ABOVE,
|
||||
CONF_LOWER_LIMIT: 10,
|
||||
},
|
||||
target_states=[
|
||||
(STATE_ON, {attribute: 50}),
|
||||
(STATE_ON, {attribute: 100}),
|
||||
],
|
||||
other_states=[
|
||||
(STATE_ON, {attribute: None}),
|
||||
(STATE_ON, {attribute: 0}),
|
||||
],
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger=trigger,
|
||||
trigger_options={
|
||||
CONF_THRESHOLD_TYPE: ThresholdType.BELOW,
|
||||
CONF_UPPER_LIMIT: 90,
|
||||
},
|
||||
target_states=[
|
||||
(STATE_ON, {attribute: 0}),
|
||||
(STATE_ON, {attribute: 50}),
|
||||
],
|
||||
other_states=[
|
||||
(STATE_ON, {attribute: None}),
|
||||
(STATE_ON, {attribute: 100}),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_experimental_triggers_conditions")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
||||
@@ -265,11 +137,13 @@ async def test_humidifier_state_trigger_behavior_any(
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "states"),
|
||||
[
|
||||
*parametrize_xxx_changed_trigger_states(
|
||||
"humidifier.current_humidity_changed", ATTR_CURRENT_HUMIDITY
|
||||
*parametrize_numerical_attribute_changed_trigger_states(
|
||||
"humidifier.current_humidity_changed", STATE_ON, ATTR_CURRENT_HUMIDITY
|
||||
),
|
||||
*parametrize_xxx_crossed_threshold_trigger_states(
|
||||
"humidifier.current_humidity_crossed_threshold", ATTR_CURRENT_HUMIDITY
|
||||
*parametrize_numerical_attribute_crossed_threshold_trigger_states(
|
||||
"humidifier.current_humidity_crossed_threshold",
|
||||
STATE_ON,
|
||||
ATTR_CURRENT_HUMIDITY,
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger="humidifier.started_drying",
|
||||
@@ -386,8 +260,10 @@ async def test_humidifier_state_trigger_behavior_first(
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "states"),
|
||||
[
|
||||
*parametrize_xxx_crossed_threshold_trigger_states(
|
||||
"humidifier.current_humidity_crossed_threshold", ATTR_CURRENT_HUMIDITY
|
||||
*parametrize_numerical_attribute_crossed_threshold_trigger_states(
|
||||
"humidifier.current_humidity_crossed_threshold",
|
||||
STATE_ON,
|
||||
ATTR_CURRENT_HUMIDITY,
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger="humidifier.started_drying",
|
||||
@@ -504,8 +380,10 @@ async def test_humidifier_state_trigger_behavior_last(
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "states"),
|
||||
[
|
||||
*parametrize_xxx_crossed_threshold_trigger_states(
|
||||
"humidifier.current_humidity_crossed_threshold", ATTR_CURRENT_HUMIDITY
|
||||
*parametrize_numerical_attribute_crossed_threshold_trigger_states(
|
||||
"humidifier.current_humidity_crossed_threshold",
|
||||
STATE_ON,
|
||||
ATTR_CURRENT_HUMIDITY,
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger="humidifier.started_drying",
|
||||
|
||||
@@ -7,7 +7,7 @@ from unittest.mock import AsyncMock
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.const import STATE_UNAVAILABLE
|
||||
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
@@ -66,3 +66,43 @@ async def test_fail_query(
|
||||
assert len(hass.states.async_entity_ids()) == 6
|
||||
departure_sensor = hass.states.get("sensor.mock_title_departure")
|
||||
assert departure_sensor.state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
async def test_no_departures(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
mock_israelrail: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test handling when there are no departures available."""
|
||||
await init_integration(hass, mock_config_entry)
|
||||
assert len(hass.states.async_entity_ids()) == 6
|
||||
|
||||
# Simulate no departures (e.g., after-hours)
|
||||
mock_israelrail.query.return_value = []
|
||||
|
||||
await goto_future(hass, freezer)
|
||||
|
||||
# All sensors should still exist
|
||||
assert len(hass.states.async_entity_ids()) == 6
|
||||
|
||||
# Departure sensors should have unknown state (None)
|
||||
departure_sensor = hass.states.get("sensor.mock_title_departure")
|
||||
assert departure_sensor.state == STATE_UNKNOWN
|
||||
|
||||
departure_sensor_1 = hass.states.get("sensor.mock_title_departure_1")
|
||||
assert departure_sensor_1.state == STATE_UNKNOWN
|
||||
|
||||
departure_sensor_2 = hass.states.get("sensor.mock_title_departure_2")
|
||||
assert departure_sensor_2.state == STATE_UNKNOWN
|
||||
|
||||
# Non-departure sensors (platform, trains, train_number) also access index 0
|
||||
# and should have unknown state when no departures available
|
||||
platform_sensor = hass.states.get("sensor.mock_title_platform")
|
||||
assert platform_sensor.state == STATE_UNKNOWN
|
||||
|
||||
trains_sensor = hass.states.get("sensor.mock_title_trains")
|
||||
assert trains_sensor.state == STATE_UNKNOWN
|
||||
|
||||
train_number_sensor = hass.states.get("sensor.mock_title_train_number")
|
||||
assert train_number_sensor.state == STATE_UNKNOWN
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Test light conditions."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
@@ -13,24 +14,18 @@ from homeassistant.const import (
|
||||
CONF_TARGET,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.components import (
|
||||
ConditionStateDescription,
|
||||
parametrize_condition_states,
|
||||
parametrize_target_entities,
|
||||
set_or_remove_state,
|
||||
target_entities,
|
||||
)
|
||||
|
||||
INVALID_STATES = [
|
||||
{"state": STATE_UNAVAILABLE, "attributes": {}},
|
||||
{"state": STATE_UNKNOWN, "attributes": {}},
|
||||
{"state": None, "attributes": {}},
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
|
||||
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
|
||||
@@ -76,15 +71,15 @@ async def setup_automation_with_light_condition(
|
||||
)
|
||||
|
||||
|
||||
async def has_call_after_trigger(
|
||||
async def calls_after_trigger(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> bool:
|
||||
"""Check if there are service calls after the trigger event."""
|
||||
) -> int:
|
||||
"""Return number of service calls after the trigger event."""
|
||||
hass.bus.async_fire("test_event")
|
||||
await hass.async_block_till_done()
|
||||
has_calls = len(service_calls) == 1
|
||||
num_calls = len(service_calls)
|
||||
service_calls.clear()
|
||||
return has_calls
|
||||
return num_calls
|
||||
|
||||
|
||||
@pytest.fixture(name="enable_experimental_triggers_conditions")
|
||||
@@ -125,17 +120,17 @@ async def test_light_conditions_gated_by_labs_flag(
|
||||
parametrize_target_entities("light"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("condition", "target_state", "other_state"),
|
||||
("condition", "condition_options", "states"),
|
||||
[
|
||||
(
|
||||
"light.is_on",
|
||||
{"state": STATE_ON, "attributes": {}},
|
||||
{"state": STATE_OFF, "attributes": {}},
|
||||
*parametrize_condition_states(
|
||||
condition="light.is_on",
|
||||
target_states=[STATE_ON],
|
||||
other_states=[STATE_OFF],
|
||||
),
|
||||
(
|
||||
"light.is_off",
|
||||
{"state": STATE_OFF, "attributes": {}},
|
||||
{"state": STATE_ON, "attributes": {}},
|
||||
*parametrize_condition_states(
|
||||
condition="light.is_off",
|
||||
target_states=[STATE_OFF],
|
||||
other_states=[STATE_ON],
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -148,15 +143,15 @@ async def test_light_state_condition_behavior_any(
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
condition: str,
|
||||
target_state: str,
|
||||
other_state: str,
|
||||
condition_options: dict[str, Any],
|
||||
states: list[ConditionStateDescription],
|
||||
) -> None:
|
||||
"""Test the light state condition with the 'any' behavior."""
|
||||
other_entity_ids = set(target_lights) - {entity_id}
|
||||
|
||||
# Set all lights, including the tested light, to the initial state
|
||||
for eid in target_lights:
|
||||
set_or_remove_state(hass, eid, other_state)
|
||||
set_or_remove_state(hass, eid, states[0]["included"])
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await setup_automation_with_light_condition(
|
||||
@@ -167,38 +162,23 @@ async def test_light_state_condition_behavior_any(
|
||||
)
|
||||
|
||||
# Set state for switches to ensure that they don't impact the condition
|
||||
for eid in target_switches:
|
||||
set_or_remove_state(hass, eid, other_state)
|
||||
await hass.async_block_till_done()
|
||||
assert not await has_call_after_trigger(hass, service_calls)
|
||||
for state in states:
|
||||
for eid in target_switches:
|
||||
set_or_remove_state(hass, eid, state["included"])
|
||||
await hass.async_block_till_done()
|
||||
assert await calls_after_trigger(hass, service_calls) == 0
|
||||
|
||||
for eid in target_switches:
|
||||
set_or_remove_state(hass, eid, target_state)
|
||||
await hass.async_block_till_done()
|
||||
assert not await has_call_after_trigger(hass, service_calls)
|
||||
for state in states:
|
||||
included_state = state["included"]
|
||||
set_or_remove_state(hass, entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
assert await calls_after_trigger(hass, service_calls) == state["count"]
|
||||
|
||||
# Set one light to the condition state -> condition pass
|
||||
set_or_remove_state(hass, entity_id, target_state)
|
||||
assert await has_call_after_trigger(hass, service_calls)
|
||||
|
||||
# Set all remaining lights to the condition state -> condition pass
|
||||
for eid in other_entity_ids:
|
||||
set_or_remove_state(hass, eid, target_state)
|
||||
assert await has_call_after_trigger(hass, service_calls)
|
||||
|
||||
for invalid_state in INVALID_STATES:
|
||||
# Set one light to the invalid state -> condition pass if there are
|
||||
# other lights in the condition state
|
||||
set_or_remove_state(hass, entity_id, invalid_state)
|
||||
assert await has_call_after_trigger(hass, service_calls) == bool(
|
||||
entities_in_target - 1
|
||||
)
|
||||
|
||||
for invalid_state in INVALID_STATES:
|
||||
# Set all lights to invalid state -> condition fail
|
||||
for eid in other_entity_ids:
|
||||
set_or_remove_state(hass, eid, invalid_state)
|
||||
assert not await has_call_after_trigger(hass, service_calls)
|
||||
# Check if changing other lights also passes the condition
|
||||
for other_entity_id in other_entity_ids:
|
||||
set_or_remove_state(hass, other_entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
assert await calls_after_trigger(hass, service_calls) == state["count"]
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_experimental_triggers_conditions")
|
||||
@@ -207,17 +187,17 @@ async def test_light_state_condition_behavior_any(
|
||||
parametrize_target_entities("light"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("condition", "target_state", "other_state"),
|
||||
("condition", "condition_options", "states"),
|
||||
[
|
||||
(
|
||||
"light.is_on",
|
||||
{"state": STATE_ON, "attributes": {}},
|
||||
{"state": STATE_OFF, "attributes": {}},
|
||||
*parametrize_condition_states(
|
||||
condition="light.is_on",
|
||||
target_states=[STATE_ON],
|
||||
other_states=[STATE_OFF],
|
||||
),
|
||||
(
|
||||
"light.is_off",
|
||||
{"state": STATE_OFF, "attributes": {}},
|
||||
{"state": STATE_ON, "attributes": {}},
|
||||
*parametrize_condition_states(
|
||||
condition="light.is_off",
|
||||
target_states=[STATE_OFF],
|
||||
other_states=[STATE_ON],
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -229,8 +209,8 @@ async def test_light_state_condition_behavior_all(
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
condition: str,
|
||||
target_state: str,
|
||||
other_state: str,
|
||||
condition_options: dict[str, Any],
|
||||
states: list[ConditionStateDescription],
|
||||
) -> None:
|
||||
"""Test the light state condition with the 'all' behavior."""
|
||||
# Set state for two switches to ensure that they don't impact the condition
|
||||
@@ -241,7 +221,7 @@ async def test_light_state_condition_behavior_all(
|
||||
|
||||
# Set all lights, including the tested light, to the initial state
|
||||
for eid in target_lights:
|
||||
set_or_remove_state(hass, eid, other_state)
|
||||
set_or_remove_state(hass, eid, states[0]["included"])
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await setup_automation_with_light_condition(
|
||||
@@ -251,27 +231,22 @@ async def test_light_state_condition_behavior_all(
|
||||
behavior="all",
|
||||
)
|
||||
|
||||
# No lights on the condition state
|
||||
assert not await has_call_after_trigger(hass, service_calls)
|
||||
for state in states:
|
||||
included_state = state["included"]
|
||||
|
||||
# Set one light to the condition state -> condition fail
|
||||
set_or_remove_state(hass, entity_id, target_state)
|
||||
assert await has_call_after_trigger(hass, service_calls) == (
|
||||
entities_in_target == 1
|
||||
)
|
||||
set_or_remove_state(hass, entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
# The condition passes if all entities are either in a target state or invalid
|
||||
assert await calls_after_trigger(hass, service_calls) == (
|
||||
(not state["valid"]) or (state["count"] if entities_in_target == 1 else 0)
|
||||
)
|
||||
|
||||
# Set all remaining lights to the condition state -> condition pass
|
||||
for eid in other_entity_ids:
|
||||
set_or_remove_state(hass, eid, target_state)
|
||||
assert await has_call_after_trigger(hass, service_calls)
|
||||
for other_entity_id in other_entity_ids:
|
||||
set_or_remove_state(hass, other_entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
for invalid_state in INVALID_STATES:
|
||||
# Set one light to the invalid state -> condition still pass
|
||||
set_or_remove_state(hass, entity_id, invalid_state)
|
||||
assert await has_call_after_trigger(hass, service_calls)
|
||||
|
||||
for invalid_state in INVALID_STATES:
|
||||
# Set all lights to unavailable -> condition passes
|
||||
for eid in other_entity_ids:
|
||||
set_or_remove_state(hass, eid, invalid_state)
|
||||
assert await has_call_after_trigger(hass, service_calls)
|
||||
# The condition passes if all entities are either in a target state or invalid
|
||||
assert (
|
||||
await calls_after_trigger(hass, service_calls) == (not state["valid"])
|
||||
or state["count"]
|
||||
)
|
||||
|
||||
@@ -7,25 +7,14 @@ from unittest.mock import patch
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS
|
||||
from homeassistant.const import (
|
||||
ATTR_LABEL_ID,
|
||||
CONF_ABOVE,
|
||||
CONF_BELOW,
|
||||
CONF_ENTITY_ID,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
)
|
||||
from homeassistant.const import ATTR_LABEL_ID, CONF_ENTITY_ID, STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.helpers.trigger import (
|
||||
CONF_LOWER_LIMIT,
|
||||
CONF_THRESHOLD_TYPE,
|
||||
CONF_UPPER_LIMIT,
|
||||
ThresholdType,
|
||||
)
|
||||
|
||||
from tests.components import (
|
||||
StateDescription,
|
||||
arm_trigger,
|
||||
parametrize_numerical_attribute_changed_trigger_states,
|
||||
parametrize_numerical_attribute_crossed_threshold_trigger_states,
|
||||
parametrize_target_entities,
|
||||
parametrize_trigger_states,
|
||||
set_or_remove_state,
|
||||
@@ -76,122 +65,6 @@ async def test_light_triggers_gated_by_labs_flag(
|
||||
) in caplog.text
|
||||
|
||||
|
||||
def parametrize_xxx_changed_trigger_states(
|
||||
trigger: str, attribute: str
|
||||
) -> list[tuple[str, dict[str, Any], list[StateDescription]]]:
|
||||
"""Parametrize states and expected service call counts for xxx_changed triggers."""
|
||||
return [
|
||||
*parametrize_trigger_states(
|
||||
trigger=trigger,
|
||||
target_states=[
|
||||
(STATE_ON, {attribute: 0}),
|
||||
(STATE_ON, {attribute: 50}),
|
||||
(STATE_ON, {attribute: 100}),
|
||||
],
|
||||
other_states=[(STATE_ON, {attribute: None})],
|
||||
retrigger_on_target_state=True,
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger=trigger,
|
||||
trigger_options={CONF_ABOVE: 10},
|
||||
target_states=[
|
||||
(STATE_ON, {attribute: 50}),
|
||||
(STATE_ON, {attribute: 100}),
|
||||
],
|
||||
other_states=[
|
||||
(STATE_ON, {attribute: None}),
|
||||
(STATE_ON, {attribute: 0}),
|
||||
],
|
||||
retrigger_on_target_state=True,
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger=trigger,
|
||||
trigger_options={CONF_BELOW: 90},
|
||||
target_states=[
|
||||
(STATE_ON, {attribute: 0}),
|
||||
(STATE_ON, {attribute: 50}),
|
||||
],
|
||||
other_states=[
|
||||
(STATE_ON, {attribute: None}),
|
||||
(STATE_ON, {attribute: 100}),
|
||||
],
|
||||
retrigger_on_target_state=True,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def parametrize_xxx_crossed_threshold_trigger_states(
|
||||
trigger: str, attribute: str
|
||||
) -> list[tuple[str, dict[str, Any], list[StateDescription]]]:
|
||||
"""Parametrize states and expected service call counts for xxx_crossed_threshold triggers."""
|
||||
return [
|
||||
*parametrize_trigger_states(
|
||||
trigger=trigger,
|
||||
trigger_options={
|
||||
CONF_THRESHOLD_TYPE: ThresholdType.BETWEEN,
|
||||
CONF_LOWER_LIMIT: 10,
|
||||
CONF_UPPER_LIMIT: 90,
|
||||
},
|
||||
target_states=[
|
||||
(STATE_ON, {attribute: 50}),
|
||||
(STATE_ON, {attribute: 60}),
|
||||
],
|
||||
other_states=[
|
||||
(STATE_ON, {attribute: None}),
|
||||
(STATE_ON, {attribute: 0}),
|
||||
(STATE_ON, {attribute: 100}),
|
||||
],
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger=trigger,
|
||||
trigger_options={
|
||||
CONF_THRESHOLD_TYPE: ThresholdType.OUTSIDE,
|
||||
CONF_LOWER_LIMIT: 10,
|
||||
CONF_UPPER_LIMIT: 90,
|
||||
},
|
||||
target_states=[
|
||||
(STATE_ON, {attribute: 0}),
|
||||
(STATE_ON, {attribute: 100}),
|
||||
],
|
||||
other_states=[
|
||||
(STATE_ON, {attribute: None}),
|
||||
(STATE_ON, {attribute: 50}),
|
||||
(STATE_ON, {attribute: 60}),
|
||||
],
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger=trigger,
|
||||
trigger_options={
|
||||
CONF_THRESHOLD_TYPE: ThresholdType.ABOVE,
|
||||
CONF_LOWER_LIMIT: 10,
|
||||
},
|
||||
target_states=[
|
||||
(STATE_ON, {attribute: 50}),
|
||||
(STATE_ON, {attribute: 100}),
|
||||
],
|
||||
other_states=[
|
||||
(STATE_ON, {attribute: None}),
|
||||
(STATE_ON, {attribute: 0}),
|
||||
],
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger=trigger,
|
||||
trigger_options={
|
||||
CONF_THRESHOLD_TYPE: ThresholdType.BELOW,
|
||||
CONF_UPPER_LIMIT: 90,
|
||||
},
|
||||
target_states=[
|
||||
(STATE_ON, {attribute: 0}),
|
||||
(STATE_ON, {attribute: 50}),
|
||||
],
|
||||
other_states=[
|
||||
(STATE_ON, {attribute: None}),
|
||||
(STATE_ON, {attribute: 100}),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_experimental_triggers_conditions")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
||||
@@ -258,11 +131,11 @@ async def test_light_state_trigger_behavior_any(
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "states"),
|
||||
[
|
||||
*parametrize_xxx_changed_trigger_states(
|
||||
"light.brightness_changed", ATTR_BRIGHTNESS
|
||||
*parametrize_numerical_attribute_changed_trigger_states(
|
||||
"light.brightness_changed", STATE_ON, ATTR_BRIGHTNESS
|
||||
),
|
||||
*parametrize_xxx_crossed_threshold_trigger_states(
|
||||
"light.brightness_crossed_threshold", ATTR_BRIGHTNESS
|
||||
*parametrize_numerical_attribute_crossed_threshold_trigger_states(
|
||||
"light.brightness_crossed_threshold", STATE_ON, ATTR_BRIGHTNESS
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -369,8 +242,8 @@ async def test_light_state_trigger_behavior_first(
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "states"),
|
||||
[
|
||||
*parametrize_xxx_crossed_threshold_trigger_states(
|
||||
"light.brightness_crossed_threshold", ATTR_BRIGHTNESS
|
||||
*parametrize_numerical_attribute_crossed_threshold_trigger_states(
|
||||
"light.brightness_crossed_threshold", STATE_ON, ATTR_BRIGHTNESS
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -477,8 +350,8 @@ async def test_light_state_trigger_behavior_last(
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "states"),
|
||||
[
|
||||
*parametrize_xxx_crossed_threshold_trigger_states(
|
||||
"light.brightness_crossed_threshold", ATTR_BRIGHTNESS
|
||||
*parametrize_numerical_attribute_crossed_threshold_trigger_states(
|
||||
"light.brightness_crossed_threshold", STATE_ON, ATTR_BRIGHTNESS
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -16,22 +16,23 @@ def mock_charger() -> Generator[MagicMock]:
|
||||
"""Create a mock OpenEVSE charger."""
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.openevse.openevsewifi.Charger",
|
||||
"homeassistant.components.openevse.OpenEVSE",
|
||||
autospec=True,
|
||||
) as mock,
|
||||
patch(
|
||||
"homeassistant.components.openevse.config_flow.openevsewifi.Charger",
|
||||
"homeassistant.components.openevse.config_flow.OpenEVSE",
|
||||
new=mock,
|
||||
),
|
||||
):
|
||||
charger = mock.return_value
|
||||
charger.getStatus.return_value = "Charging"
|
||||
charger.getChargeTimeElapsed.return_value = 3600 # 60 minutes in seconds
|
||||
charger.getAmbientTemperature.return_value = 25.5
|
||||
charger.getIRTemperature.return_value = 30.2
|
||||
charger.getRTCTemperature.return_value = 28.7
|
||||
charger.getUsageSession.return_value = 15000 # 15 kWh in Wh
|
||||
charger.getUsageTotal.return_value = 500000 # 500 kWh in Wh
|
||||
charger.update = AsyncMock()
|
||||
charger.status = "Charging"
|
||||
charger.charge_time_elapsed = 3600 # 60 minutes in seconds
|
||||
charger.ambient_temperature = 25.5
|
||||
charger.ir_temperature = 30.2
|
||||
charger.rtc_temperature = 28.7
|
||||
charger.usage_session = 15000 # 15 kWh in Wh
|
||||
charger.usage_total = 500000 # 500 kWh in Wh
|
||||
charger.charging_current = 32.0
|
||||
yield charger
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ async def test_user_flow_flaky(
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
mock_charger.getStatus.side_effect = AttributeError
|
||||
mock_charger.test_and_get.side_effect = TimeoutError
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_HOST: "10.0.0.131"},
|
||||
@@ -54,7 +54,7 @@ async def test_user_flow_flaky(
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {"host": "cannot_connect"}
|
||||
|
||||
mock_charger.getStatus.side_effect = "Charging"
|
||||
mock_charger.test_and_get.side_effect = None
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_HOST: "10.0.0.131"},
|
||||
@@ -112,7 +112,7 @@ async def test_import_flow_bad(
|
||||
mock_setup_entry: AsyncMock,
|
||||
) -> None:
|
||||
"""Test import flow with bad charger."""
|
||||
mock_charger.getStatus.side_effect = AttributeError
|
||||
mock_charger.test_and_get.side_effect = TimeoutError
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_IMPORT}, data={CONF_HOST: "10.0.0.131"}
|
||||
|
||||
@@ -6,7 +6,6 @@ import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
import socket
|
||||
import sys
|
||||
from unittest.mock import patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
@@ -73,9 +72,6 @@ async def test_basic_usage(hass: HomeAssistant, tmp_path: Path) -> None:
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
sys.version_info >= (3, 14), reason="not yet available on Python 3.14"
|
||||
)
|
||||
async def test_memory_usage(hass: HomeAssistant, tmp_path: Path) -> None:
|
||||
"""Test we can setup and the service is registered."""
|
||||
test_dir = tmp_path / "profiles"
|
||||
@@ -107,24 +103,6 @@ async def test_memory_usage(hass: HomeAssistant, tmp_path: Path) -> None:
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 14), reason="still works on python 3.13")
|
||||
async def test_memory_usage_py313(hass: HomeAssistant, tmp_path: Path) -> None:
|
||||
"""Test raise an error on python3.13."""
|
||||
entry = MockConfigEntry(domain=DOMAIN)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert hass.services.has_service(DOMAIN, SERVICE_MEMORY)
|
||||
with pytest.raises(
|
||||
HomeAssistantError,
|
||||
match="Memory profiling is not supported on Python 3.14. Please use Python 3.13.",
|
||||
):
|
||||
await hass.services.async_call(
|
||||
DOMAIN, SERVICE_MEMORY, {CONF_SECONDS: 0.000001}, blocking=True
|
||||
)
|
||||
|
||||
|
||||
async def test_object_growth_logging(
|
||||
hass: HomeAssistant,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
|
||||
@@ -325,6 +325,38 @@ async def test_camera_image(
|
||||
assert image.content == SMALLEST_VALID_JPEG_BYTES
|
||||
|
||||
|
||||
async def test_camera_live_view_no_subscription(
|
||||
hass: HomeAssistant,
|
||||
mock_ring_client,
|
||||
mock_ring_devices,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test live view camera skips recording URL when no subscription."""
|
||||
await setup_platform(hass, Platform.CAMERA)
|
||||
|
||||
front_camera_mock = mock_ring_devices.get_device(765432)
|
||||
# Set device to not have subscription
|
||||
front_camera_mock.has_subscription = False
|
||||
|
||||
state = hass.states.get("camera.front_live_view")
|
||||
assert state is not None
|
||||
|
||||
# Reset mock call counts
|
||||
front_camera_mock.async_recording_url.reset_mock()
|
||||
|
||||
# Trigger coordinator update
|
||||
freezer.tick(SCAN_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
# For cameras without subscription, recording URL should NOT be fetched
|
||||
front_camera_mock.async_recording_url.assert_not_called()
|
||||
|
||||
# Requesting an image without subscription should raise an error
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await async_get_image(hass, "camera.front_live_view")
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_camera_stream_attributes(
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -751,62 +751,6 @@
|
||||
'state': 'sweep_moping',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.roborock_q7_total_cleaning_time-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.roborock_q7_total_cleaning_time',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 2,
|
||||
}),
|
||||
'sensor.private': dict({
|
||||
'suggested_unit_of_measurement': <UnitOfTime.HOURS: 'h'>,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.DURATION: 'duration'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Total cleaning time',
|
||||
'platform': 'roborock',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'total_cleaning_time',
|
||||
'unique_id': 'total_cleaning_time_q7_duid',
|
||||
'unit_of_measurement': <UnitOfTime.HOURS: 'h'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.roborock_q7_total_cleaning_time-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'duration',
|
||||
'friendly_name': 'Roborock Q7 Total cleaning time',
|
||||
'unit_of_measurement': <UnitOfTime.HOURS: 'h'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.roborock_q7_total_cleaning_time',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '50.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.roborock_s7_2_battery-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
"""The tests for the telegram.notify platform."""
|
||||
|
||||
from unittest.mock import patch
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, call, patch
|
||||
|
||||
from homeassistant import config as hass_config
|
||||
from homeassistant.components import notify
|
||||
from homeassistant.components.notify import ATTR_DATA, ATTR_MESSAGE, ATTR_TITLE
|
||||
from homeassistant.components.telegram import DOMAIN
|
||||
from homeassistant.const import SERVICE_RELOAD
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, ServiceRegistry
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
@@ -54,3 +56,108 @@ async def test_reload_notify(
|
||||
issue_id="migrate_notify",
|
||||
)
|
||||
assert len(issue_registry.issues) == 1
|
||||
|
||||
|
||||
async def test_notify(hass: HomeAssistant) -> None:
|
||||
"""Test notify."""
|
||||
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
notify.DOMAIN,
|
||||
{
|
||||
notify.DOMAIN: [
|
||||
{
|
||||
"name": DOMAIN,
|
||||
"platform": DOMAIN,
|
||||
"chat_id": 1,
|
||||
},
|
||||
]
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
original_call = ServiceRegistry.async_call
|
||||
with patch(
|
||||
"homeassistant.core.ServiceRegistry.async_call", new_callable=AsyncMock
|
||||
) as mock_service_call:
|
||||
# setup mock
|
||||
|
||||
async def call_service(*args, **kwargs) -> Any:
|
||||
if args[0] == notify.DOMAIN:
|
||||
return await original_call(
|
||||
hass.services, args[0], args[1], args[2], kwargs["blocking"]
|
||||
)
|
||||
return AsyncMock()
|
||||
|
||||
mock_service_call.side_effect = call_service
|
||||
|
||||
# test send message
|
||||
|
||||
data: dict[str, Any] = {"title": "mock title", "message": "mock message"}
|
||||
await hass.services.async_call(
|
||||
notify.DOMAIN,
|
||||
DOMAIN,
|
||||
{ATTR_TITLE: "mock title", ATTR_MESSAGE: "mock message"},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_service_call.mock_calls == [
|
||||
call(
|
||||
"notify",
|
||||
"telegram",
|
||||
data,
|
||||
blocking=True,
|
||||
),
|
||||
call(
|
||||
"telegram_bot",
|
||||
"send_message",
|
||||
{"target": 1, "title": "mock title", "message": "mock message"},
|
||||
False,
|
||||
None,
|
||||
None,
|
||||
False,
|
||||
),
|
||||
]
|
||||
|
||||
mock_service_call.reset_mock()
|
||||
|
||||
# test send file
|
||||
|
||||
data = {
|
||||
ATTR_TITLE: "mock title",
|
||||
ATTR_MESSAGE: "mock message",
|
||||
ATTR_DATA: {
|
||||
"photo": {"url": "https://mock/photo.jpg", "caption": "mock caption"}
|
||||
},
|
||||
}
|
||||
|
||||
await hass.services.async_call(
|
||||
notify.DOMAIN,
|
||||
DOMAIN,
|
||||
data,
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_service_call.mock_calls == [
|
||||
call(
|
||||
"notify",
|
||||
"telegram",
|
||||
data,
|
||||
blocking=True,
|
||||
),
|
||||
call(
|
||||
"telegram_bot",
|
||||
"send_photo",
|
||||
{
|
||||
"target": 1,
|
||||
"url": "https://mock/photo.jpg",
|
||||
"caption": "mock caption",
|
||||
},
|
||||
False,
|
||||
None,
|
||||
None,
|
||||
False,
|
||||
),
|
||||
]
|
||||
|
||||
@@ -77,6 +77,20 @@ DEVICE_FIXTURES: dict[str, list[tuple[str, str, str]]] = {
|
||||
"Humidifier 6000s": [
|
||||
("post", "/cloud/v2/deviceManaged/bypassV2", "humidifier-6000s-detail.json")
|
||||
],
|
||||
"CS158-AF Air Fryer Standby": [
|
||||
(
|
||||
"post",
|
||||
"/cloud/v1/deviceManaged/bypass",
|
||||
"air-fryer-CS158-AF-detail-standby.json",
|
||||
)
|
||||
],
|
||||
"CS158-AF Air Fryer Cooking": [
|
||||
(
|
||||
"post",
|
||||
"/cloud/v1/deviceManaged/bypass",
|
||||
"air-fryer-CS158-AF-detail-cooking.json",
|
||||
)
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"traceId": "1234",
|
||||
"code": 0,
|
||||
"msg": "request success",
|
||||
"module": null,
|
||||
"stacktrace": null,
|
||||
"result": {
|
||||
"returnStatus": {
|
||||
"curentTemp": 17,
|
||||
"cookSetTemp": 180,
|
||||
"mode": "manual",
|
||||
"cookSetTime": 15,
|
||||
"cookLastTime": 10,
|
||||
"cookStatus": "cooking",
|
||||
"tempUnit": "celsius",
|
||||
"accountId": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"traceId": "1234",
|
||||
"code": 0,
|
||||
"msg": "request success",
|
||||
"module": null,
|
||||
"stacktrace": null,
|
||||
"result": {
|
||||
"returnStatus": {
|
||||
"cookStatus": "standby"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -231,6 +231,56 @@
|
||||
"wifiMac": "00:10:f0:aa:bb:cc",
|
||||
"mistLevel": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"deviceRegion": "EU",
|
||||
"isOwner": true,
|
||||
"authKey": null,
|
||||
"deviceName": "CS158-AF Air Fryer Standby",
|
||||
"deviceImg": "https://image.vesync.com/defaultImages/deviceDefaultImages/wifi_airfryer_cs158-af_eu_240.png",
|
||||
"cid": "CS158Standby",
|
||||
"deviceStatus": "off",
|
||||
"connectionStatus": "online",
|
||||
"connectionType": "wifi",
|
||||
"deviceType": "CS158-AF",
|
||||
"type": "SKA",
|
||||
"uuid": "##_REDACTED_##",
|
||||
"configModule": "WiFi_AirFryer_CS158-AF_EU",
|
||||
"macID": null,
|
||||
"mode": null,
|
||||
"speed": null,
|
||||
"currentFirmVersion": null,
|
||||
"subDeviceNo": null,
|
||||
"subDeviceType": null,
|
||||
"deviceFirstSetupTime": "Dec 3, 2025 1:52:19 PM",
|
||||
"subDeviceList": null,
|
||||
"extension": null,
|
||||
"deviceProp": null
|
||||
},
|
||||
{
|
||||
"deviceRegion": "EU",
|
||||
"isOwner": true,
|
||||
"authKey": null,
|
||||
"deviceName": "CS158-AF Air Fryer Cooking",
|
||||
"deviceImg": "https://image.vesync.com/defaultImages/deviceDefaultImages/wifi_airfryer_cs158-af_eu_240.png",
|
||||
"cid": "CS158Cooking",
|
||||
"deviceStatus": "off",
|
||||
"connectionStatus": "online",
|
||||
"connectionType": "wifi",
|
||||
"deviceType": "CS158-AF",
|
||||
"type": "SKA",
|
||||
"uuid": "##_REDACTED_##",
|
||||
"configModule": "WiFi_AirFryer_CS158-AF_EU",
|
||||
"macID": null,
|
||||
"mode": null,
|
||||
"speed": null,
|
||||
"currentFirmVersion": null,
|
||||
"subDeviceNo": null,
|
||||
"subDeviceType": null,
|
||||
"deviceFirstSetupTime": "Dec 3, 2025 1:52:19 PM",
|
||||
"subDeviceList": null,
|
||||
"extension": null,
|
||||
"deviceProp": null
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -147,6 +147,80 @@
|
||||
list([
|
||||
])
|
||||
# ---
|
||||
# name: test_sensor_state[CS158-AF Air Fryer Cooking][devices]
|
||||
list([
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
'config_entries': <ANY>,
|
||||
'config_entries_subentries': <ANY>,
|
||||
'configuration_url': None,
|
||||
'connections': set({
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'entry_type': None,
|
||||
'hw_version': None,
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'vesync',
|
||||
'CS158Cooking',
|
||||
),
|
||||
}),
|
||||
'labels': set({
|
||||
}),
|
||||
'manufacturer': 'VeSync',
|
||||
'model': 'CS158-AF',
|
||||
'model_id': None,
|
||||
'name': 'CS158-AF Air Fryer Cooking',
|
||||
'name_by_user': None,
|
||||
'primary_config_entry': <ANY>,
|
||||
'serial_number': None,
|
||||
'sw_version': None,
|
||||
'via_device_id': None,
|
||||
}),
|
||||
])
|
||||
# ---
|
||||
# name: test_sensor_state[CS158-AF Air Fryer Cooking][entities]
|
||||
list([
|
||||
])
|
||||
# ---
|
||||
# name: test_sensor_state[CS158-AF Air Fryer Standby][devices]
|
||||
list([
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
'config_entries': <ANY>,
|
||||
'config_entries_subentries': <ANY>,
|
||||
'configuration_url': None,
|
||||
'connections': set({
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'entry_type': None,
|
||||
'hw_version': None,
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'vesync',
|
||||
'CS158Standby',
|
||||
),
|
||||
}),
|
||||
'labels': set({
|
||||
}),
|
||||
'manufacturer': 'VeSync',
|
||||
'model': 'CS158-AF',
|
||||
'model_id': None,
|
||||
'name': 'CS158-AF Air Fryer Standby',
|
||||
'name_by_user': None,
|
||||
'primary_config_entry': <ANY>,
|
||||
'serial_number': None,
|
||||
'sw_version': None,
|
||||
'via_device_id': None,
|
||||
}),
|
||||
])
|
||||
# ---
|
||||
# name: test_sensor_state[CS158-AF Air Fryer Standby][entities]
|
||||
list([
|
||||
])
|
||||
# ---
|
||||
# name: test_sensor_state[Dimmable Light][devices]
|
||||
list([
|
||||
DeviceRegistryEntrySnapshot({
|
||||
|
||||
@@ -399,6 +399,80 @@
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
# name: test_fan_state[CS158-AF Air Fryer Cooking][devices]
|
||||
list([
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
'config_entries': <ANY>,
|
||||
'config_entries_subentries': <ANY>,
|
||||
'configuration_url': None,
|
||||
'connections': set({
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'entry_type': None,
|
||||
'hw_version': None,
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'vesync',
|
||||
'CS158Cooking',
|
||||
),
|
||||
}),
|
||||
'labels': set({
|
||||
}),
|
||||
'manufacturer': 'VeSync',
|
||||
'model': 'CS158-AF',
|
||||
'model_id': None,
|
||||
'name': 'CS158-AF Air Fryer Cooking',
|
||||
'name_by_user': None,
|
||||
'primary_config_entry': <ANY>,
|
||||
'serial_number': None,
|
||||
'sw_version': None,
|
||||
'via_device_id': None,
|
||||
}),
|
||||
])
|
||||
# ---
|
||||
# name: test_fan_state[CS158-AF Air Fryer Cooking][entities]
|
||||
list([
|
||||
])
|
||||
# ---
|
||||
# name: test_fan_state[CS158-AF Air Fryer Standby][devices]
|
||||
list([
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
'config_entries': <ANY>,
|
||||
'config_entries_subentries': <ANY>,
|
||||
'configuration_url': None,
|
||||
'connections': set({
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'entry_type': None,
|
||||
'hw_version': None,
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'vesync',
|
||||
'CS158Standby',
|
||||
),
|
||||
}),
|
||||
'labels': set({
|
||||
}),
|
||||
'manufacturer': 'VeSync',
|
||||
'model': 'CS158-AF',
|
||||
'model_id': None,
|
||||
'name': 'CS158-AF Air Fryer Standby',
|
||||
'name_by_user': None,
|
||||
'primary_config_entry': <ANY>,
|
||||
'serial_number': None,
|
||||
'sw_version': None,
|
||||
'via_device_id': None,
|
||||
}),
|
||||
])
|
||||
# ---
|
||||
# name: test_fan_state[CS158-AF Air Fryer Standby][entities]
|
||||
list([
|
||||
])
|
||||
# ---
|
||||
# name: test_fan_state[Dimmable Light][devices]
|
||||
list([
|
||||
DeviceRegistryEntrySnapshot({
|
||||
|
||||
@@ -147,6 +147,80 @@
|
||||
list([
|
||||
])
|
||||
# ---
|
||||
# name: test_humidifier_state[CS158-AF Air Fryer Cooking][devices]
|
||||
list([
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
'config_entries': <ANY>,
|
||||
'config_entries_subentries': <ANY>,
|
||||
'configuration_url': None,
|
||||
'connections': set({
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'entry_type': None,
|
||||
'hw_version': None,
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'vesync',
|
||||
'CS158Cooking',
|
||||
),
|
||||
}),
|
||||
'labels': set({
|
||||
}),
|
||||
'manufacturer': 'VeSync',
|
||||
'model': 'CS158-AF',
|
||||
'model_id': None,
|
||||
'name': 'CS158-AF Air Fryer Cooking',
|
||||
'name_by_user': None,
|
||||
'primary_config_entry': <ANY>,
|
||||
'serial_number': None,
|
||||
'sw_version': None,
|
||||
'via_device_id': None,
|
||||
}),
|
||||
])
|
||||
# ---
|
||||
# name: test_humidifier_state[CS158-AF Air Fryer Cooking][entities]
|
||||
list([
|
||||
])
|
||||
# ---
|
||||
# name: test_humidifier_state[CS158-AF Air Fryer Standby][devices]
|
||||
list([
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
'config_entries': <ANY>,
|
||||
'config_entries_subentries': <ANY>,
|
||||
'configuration_url': None,
|
||||
'connections': set({
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'entry_type': None,
|
||||
'hw_version': None,
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'vesync',
|
||||
'CS158Standby',
|
||||
),
|
||||
}),
|
||||
'labels': set({
|
||||
}),
|
||||
'manufacturer': 'VeSync',
|
||||
'model': 'CS158-AF',
|
||||
'model_id': None,
|
||||
'name': 'CS158-AF Air Fryer Standby',
|
||||
'name_by_user': None,
|
||||
'primary_config_entry': <ANY>,
|
||||
'serial_number': None,
|
||||
'sw_version': None,
|
||||
'via_device_id': None,
|
||||
}),
|
||||
])
|
||||
# ---
|
||||
# name: test_humidifier_state[CS158-AF Air Fryer Standby][entities]
|
||||
list([
|
||||
])
|
||||
# ---
|
||||
# name: test_humidifier_state[Dimmable Light][devices]
|
||||
list([
|
||||
DeviceRegistryEntrySnapshot({
|
||||
|
||||
@@ -147,6 +147,80 @@
|
||||
list([
|
||||
])
|
||||
# ---
|
||||
# name: test_light_state[CS158-AF Air Fryer Cooking][devices]
|
||||
list([
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
'config_entries': <ANY>,
|
||||
'config_entries_subentries': <ANY>,
|
||||
'configuration_url': None,
|
||||
'connections': set({
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'entry_type': None,
|
||||
'hw_version': None,
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'vesync',
|
||||
'CS158Cooking',
|
||||
),
|
||||
}),
|
||||
'labels': set({
|
||||
}),
|
||||
'manufacturer': 'VeSync',
|
||||
'model': 'CS158-AF',
|
||||
'model_id': None,
|
||||
'name': 'CS158-AF Air Fryer Cooking',
|
||||
'name_by_user': None,
|
||||
'primary_config_entry': <ANY>,
|
||||
'serial_number': None,
|
||||
'sw_version': None,
|
||||
'via_device_id': None,
|
||||
}),
|
||||
])
|
||||
# ---
|
||||
# name: test_light_state[CS158-AF Air Fryer Cooking][entities]
|
||||
list([
|
||||
])
|
||||
# ---
|
||||
# name: test_light_state[CS158-AF Air Fryer Standby][devices]
|
||||
list([
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
'config_entries': <ANY>,
|
||||
'config_entries_subentries': <ANY>,
|
||||
'configuration_url': None,
|
||||
'connections': set({
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'entry_type': None,
|
||||
'hw_version': None,
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'vesync',
|
||||
'CS158Standby',
|
||||
),
|
||||
}),
|
||||
'labels': set({
|
||||
}),
|
||||
'manufacturer': 'VeSync',
|
||||
'model': 'CS158-AF',
|
||||
'model_id': None,
|
||||
'name': 'CS158-AF Air Fryer Standby',
|
||||
'name_by_user': None,
|
||||
'primary_config_entry': <ANY>,
|
||||
'serial_number': None,
|
||||
'sw_version': None,
|
||||
'via_device_id': None,
|
||||
}),
|
||||
])
|
||||
# ---
|
||||
# name: test_light_state[CS158-AF Air Fryer Standby][entities]
|
||||
list([
|
||||
])
|
||||
# ---
|
||||
# name: test_light_state[Dimmable Light][devices]
|
||||
list([
|
||||
DeviceRegistryEntrySnapshot({
|
||||
|
||||
@@ -587,6 +587,628 @@
|
||||
'state': '5',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_state[CS158-AF Air Fryer Cooking][devices]
|
||||
list([
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
'config_entries': <ANY>,
|
||||
'config_entries_subentries': <ANY>,
|
||||
'configuration_url': None,
|
||||
'connections': set({
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'entry_type': None,
|
||||
'hw_version': None,
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'vesync',
|
||||
'CS158Cooking',
|
||||
),
|
||||
}),
|
||||
'labels': set({
|
||||
}),
|
||||
'manufacturer': 'VeSync',
|
||||
'model': 'CS158-AF',
|
||||
'model_id': None,
|
||||
'name': 'CS158-AF Air Fryer Cooking',
|
||||
'name_by_user': None,
|
||||
'primary_config_entry': <ANY>,
|
||||
'serial_number': None,
|
||||
'sw_version': None,
|
||||
'via_device_id': None,
|
||||
}),
|
||||
])
|
||||
# ---
|
||||
# name: test_sensor_state[CS158-AF Air Fryer Cooking][entities]
|
||||
list([
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.cs158_af_air_fryer_cooking_current_temperature',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 1,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Current temperature',
|
||||
'platform': 'vesync',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'current_temp',
|
||||
'unique_id': 'CS158Cooking-current_temp',
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.cs158_af_air_fryer_cooking_cooking_set_temperature',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 1,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Cooking set temperature',
|
||||
'platform': 'vesync',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'cook_set_temp',
|
||||
'unique_id': 'CS158Cooking-cook_set_temp',
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.cs158_af_air_fryer_cooking_cooking_set_time',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 2,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.DURATION: 'duration'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Cooking set time',
|
||||
'platform': 'vesync',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'cook_set_time',
|
||||
'unique_id': 'CS158Cooking-cook_set_time',
|
||||
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
|
||||
}),
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.cs158_af_air_fryer_cooking_preheating_set_time',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 2,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.DURATION: 'duration'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Preheating set time',
|
||||
'platform': 'vesync',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'preheat_set_time',
|
||||
'unique_id': 'CS158Cooking-preheat_set_time',
|
||||
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
|
||||
}),
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'cooking_end',
|
||||
'cooking',
|
||||
'cooking_stop',
|
||||
'heating',
|
||||
'preheat_end',
|
||||
'preheat_stop',
|
||||
'pull_out',
|
||||
'standby',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.cs158_af_air_fryer_cooking_cooking_status',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Cooking status',
|
||||
'platform': 'vesync',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'cook_status',
|
||||
'unique_id': 'CS158Cooking-cook_status',
|
||||
'unit_of_measurement': None,
|
||||
}),
|
||||
])
|
||||
# ---
|
||||
# name: test_sensor_state[CS158-AF Air Fryer Cooking][sensor.cs158_af_air_fryer_cooking_cooking_set_temperature]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'temperature',
|
||||
'friendly_name': 'CS158-AF Air Fryer Cooking Cooking set temperature',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.cs158_af_air_fryer_cooking_cooking_set_temperature',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '180',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_state[CS158-AF Air Fryer Cooking][sensor.cs158_af_air_fryer_cooking_cooking_set_time]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'duration',
|
||||
'friendly_name': 'CS158-AF Air Fryer Cooking Cooking set time',
|
||||
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.cs158_af_air_fryer_cooking_cooking_set_time',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '15',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_state[CS158-AF Air Fryer Cooking][sensor.cs158_af_air_fryer_cooking_cooking_status]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'enum',
|
||||
'friendly_name': 'CS158-AF Air Fryer Cooking Cooking status',
|
||||
'options': list([
|
||||
'cooking_end',
|
||||
'cooking',
|
||||
'cooking_stop',
|
||||
'heating',
|
||||
'preheat_end',
|
||||
'preheat_stop',
|
||||
'pull_out',
|
||||
'standby',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.cs158_af_air_fryer_cooking_cooking_status',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'cooking',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_state[CS158-AF Air Fryer Cooking][sensor.cs158_af_air_fryer_cooking_current_temperature]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'temperature',
|
||||
'friendly_name': 'CS158-AF Air Fryer Cooking Current temperature',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.cs158_af_air_fryer_cooking_current_temperature',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '17',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_state[CS158-AF Air Fryer Cooking][sensor.cs158_af_air_fryer_cooking_preheating_set_time]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'duration',
|
||||
'friendly_name': 'CS158-AF Air Fryer Cooking Preheating set time',
|
||||
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.cs158_af_air_fryer_cooking_preheating_set_time',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_state[CS158-AF Air Fryer Standby][devices]
|
||||
list([
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
'config_entries': <ANY>,
|
||||
'config_entries_subentries': <ANY>,
|
||||
'configuration_url': None,
|
||||
'connections': set({
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'entry_type': None,
|
||||
'hw_version': None,
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'vesync',
|
||||
'CS158Standby',
|
||||
),
|
||||
}),
|
||||
'labels': set({
|
||||
}),
|
||||
'manufacturer': 'VeSync',
|
||||
'model': 'CS158-AF',
|
||||
'model_id': None,
|
||||
'name': 'CS158-AF Air Fryer Standby',
|
||||
'name_by_user': None,
|
||||
'primary_config_entry': <ANY>,
|
||||
'serial_number': None,
|
||||
'sw_version': None,
|
||||
'via_device_id': None,
|
||||
}),
|
||||
])
|
||||
# ---
|
||||
# name: test_sensor_state[CS158-AF Air Fryer Standby][entities]
|
||||
list([
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.cs158_af_air_fryer_standby_current_temperature',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Current temperature',
|
||||
'platform': 'vesync',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'current_temp',
|
||||
'unique_id': 'CS158Standby-current_temp',
|
||||
'unit_of_measurement': None,
|
||||
}),
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.cs158_af_air_fryer_standby_cooking_set_temperature',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Cooking set temperature',
|
||||
'platform': 'vesync',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'cook_set_temp',
|
||||
'unique_id': 'CS158Standby-cook_set_temp',
|
||||
'unit_of_measurement': None,
|
||||
}),
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.cs158_af_air_fryer_standby_cooking_set_time',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 2,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.DURATION: 'duration'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Cooking set time',
|
||||
'platform': 'vesync',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'cook_set_time',
|
||||
'unique_id': 'CS158Standby-cook_set_time',
|
||||
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
|
||||
}),
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.cs158_af_air_fryer_standby_preheating_set_time',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 2,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.DURATION: 'duration'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Preheating set time',
|
||||
'platform': 'vesync',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'preheat_set_time',
|
||||
'unique_id': 'CS158Standby-preheat_set_time',
|
||||
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
|
||||
}),
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'cooking_end',
|
||||
'cooking',
|
||||
'cooking_stop',
|
||||
'heating',
|
||||
'preheat_end',
|
||||
'preheat_stop',
|
||||
'pull_out',
|
||||
'standby',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.cs158_af_air_fryer_standby_cooking_status',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Cooking status',
|
||||
'platform': 'vesync',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'cook_status',
|
||||
'unique_id': 'CS158Standby-cook_status',
|
||||
'unit_of_measurement': None,
|
||||
}),
|
||||
])
|
||||
# ---
|
||||
# name: test_sensor_state[CS158-AF Air Fryer Standby][sensor.cs158_af_air_fryer_standby_cooking_set_temperature]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'temperature',
|
||||
'friendly_name': 'CS158-AF Air Fryer Standby Cooking set temperature',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.cs158_af_air_fryer_standby_cooking_set_temperature',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_state[CS158-AF Air Fryer Standby][sensor.cs158_af_air_fryer_standby_cooking_set_time]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'duration',
|
||||
'friendly_name': 'CS158-AF Air Fryer Standby Cooking set time',
|
||||
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.cs158_af_air_fryer_standby_cooking_set_time',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_state[CS158-AF Air Fryer Standby][sensor.cs158_af_air_fryer_standby_cooking_status]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'enum',
|
||||
'friendly_name': 'CS158-AF Air Fryer Standby Cooking status',
|
||||
'options': list([
|
||||
'cooking_end',
|
||||
'cooking',
|
||||
'cooking_stop',
|
||||
'heating',
|
||||
'preheat_end',
|
||||
'preheat_stop',
|
||||
'pull_out',
|
||||
'standby',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.cs158_af_air_fryer_standby_cooking_status',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'standby',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_state[CS158-AF Air Fryer Standby][sensor.cs158_af_air_fryer_standby_current_temperature]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'temperature',
|
||||
'friendly_name': 'CS158-AF Air Fryer Standby Current temperature',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.cs158_af_air_fryer_standby_current_temperature',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_state[CS158-AF Air Fryer Standby][sensor.cs158_af_air_fryer_standby_preheating_set_time]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'duration',
|
||||
'friendly_name': 'CS158-AF Air Fryer Standby Preheating set time',
|
||||
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.cs158_af_air_fryer_standby_preheating_set_time',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_state[Dimmable Light][devices]
|
||||
list([
|
||||
DeviceRegistryEntrySnapshot({
|
||||
|
||||
@@ -469,6 +469,80 @@
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
# name: test_switch_state[CS158-AF Air Fryer Cooking][devices]
|
||||
list([
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
'config_entries': <ANY>,
|
||||
'config_entries_subentries': <ANY>,
|
||||
'configuration_url': None,
|
||||
'connections': set({
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'entry_type': None,
|
||||
'hw_version': None,
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'vesync',
|
||||
'CS158Cooking',
|
||||
),
|
||||
}),
|
||||
'labels': set({
|
||||
}),
|
||||
'manufacturer': 'VeSync',
|
||||
'model': 'CS158-AF',
|
||||
'model_id': None,
|
||||
'name': 'CS158-AF Air Fryer Cooking',
|
||||
'name_by_user': None,
|
||||
'primary_config_entry': <ANY>,
|
||||
'serial_number': None,
|
||||
'sw_version': None,
|
||||
'via_device_id': None,
|
||||
}),
|
||||
])
|
||||
# ---
|
||||
# name: test_switch_state[CS158-AF Air Fryer Cooking][entities]
|
||||
list([
|
||||
])
|
||||
# ---
|
||||
# name: test_switch_state[CS158-AF Air Fryer Standby][devices]
|
||||
list([
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
'config_entries': <ANY>,
|
||||
'config_entries_subentries': <ANY>,
|
||||
'configuration_url': None,
|
||||
'connections': set({
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'entry_type': None,
|
||||
'hw_version': None,
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'vesync',
|
||||
'CS158Standby',
|
||||
),
|
||||
}),
|
||||
'labels': set({
|
||||
}),
|
||||
'manufacturer': 'VeSync',
|
||||
'model': 'CS158-AF',
|
||||
'model_id': None,
|
||||
'name': 'CS158-AF Air Fryer Standby',
|
||||
'name_by_user': None,
|
||||
'primary_config_entry': <ANY>,
|
||||
'serial_number': None,
|
||||
'sw_version': None,
|
||||
'via_device_id': None,
|
||||
}),
|
||||
])
|
||||
# ---
|
||||
# name: test_switch_state[CS158-AF Air Fryer Standby][entities]
|
||||
list([
|
||||
])
|
||||
# ---
|
||||
# name: test_switch_state[Dimmable Light][devices]
|
||||
list([
|
||||
DeviceRegistryEntrySnapshot({
|
||||
|
||||
@@ -383,6 +383,198 @@
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
# name: test_update_state[CS158-AF Air Fryer Cooking][devices]
|
||||
list([
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
'config_entries': <ANY>,
|
||||
'config_entries_subentries': <ANY>,
|
||||
'configuration_url': None,
|
||||
'connections': set({
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'entry_type': None,
|
||||
'hw_version': None,
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'vesync',
|
||||
'CS158Cooking',
|
||||
),
|
||||
}),
|
||||
'labels': set({
|
||||
}),
|
||||
'manufacturer': 'VeSync',
|
||||
'model': 'CS158-AF',
|
||||
'model_id': None,
|
||||
'name': 'CS158-AF Air Fryer Cooking',
|
||||
'name_by_user': None,
|
||||
'primary_config_entry': <ANY>,
|
||||
'serial_number': None,
|
||||
'sw_version': None,
|
||||
'via_device_id': None,
|
||||
}),
|
||||
])
|
||||
# ---
|
||||
# name: test_update_state[CS158-AF Air Fryer Cooking][entities]
|
||||
list([
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'update',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'update.cs158_af_air_fryer_cooking_firmware',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <UpdateDeviceClass.FIRMWARE: 'firmware'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Firmware',
|
||||
'platform': 'vesync',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'CS158Cooking',
|
||||
'unit_of_measurement': None,
|
||||
}),
|
||||
])
|
||||
# ---
|
||||
# name: test_update_state[CS158-AF Air Fryer Cooking][update.cs158_af_air_fryer_cooking_firmware]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'auto_update': False,
|
||||
'device_class': 'firmware',
|
||||
'display_precision': 0,
|
||||
'entity_picture': 'https://brands.home-assistant.io/_/vesync/icon.png',
|
||||
'friendly_name': 'CS158-AF Air Fryer Cooking Firmware',
|
||||
'in_progress': False,
|
||||
'installed_version': None,
|
||||
'latest_version': None,
|
||||
'release_summary': None,
|
||||
'release_url': None,
|
||||
'skipped_version': None,
|
||||
'supported_features': <UpdateEntityFeature: 0>,
|
||||
'title': None,
|
||||
'update_percentage': None,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'update.cs158_af_air_fryer_cooking_firmware',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_update_state[CS158-AF Air Fryer Standby][devices]
|
||||
list([
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
'config_entries': <ANY>,
|
||||
'config_entries_subentries': <ANY>,
|
||||
'configuration_url': None,
|
||||
'connections': set({
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'entry_type': None,
|
||||
'hw_version': None,
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'vesync',
|
||||
'CS158Standby',
|
||||
),
|
||||
}),
|
||||
'labels': set({
|
||||
}),
|
||||
'manufacturer': 'VeSync',
|
||||
'model': 'CS158-AF',
|
||||
'model_id': None,
|
||||
'name': 'CS158-AF Air Fryer Standby',
|
||||
'name_by_user': None,
|
||||
'primary_config_entry': <ANY>,
|
||||
'serial_number': None,
|
||||
'sw_version': None,
|
||||
'via_device_id': None,
|
||||
}),
|
||||
])
|
||||
# ---
|
||||
# name: test_update_state[CS158-AF Air Fryer Standby][entities]
|
||||
list([
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'update',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'update.cs158_af_air_fryer_standby_firmware',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <UpdateDeviceClass.FIRMWARE: 'firmware'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Firmware',
|
||||
'platform': 'vesync',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'CS158Standby',
|
||||
'unit_of_measurement': None,
|
||||
}),
|
||||
])
|
||||
# ---
|
||||
# name: test_update_state[CS158-AF Air Fryer Standby][update.cs158_af_air_fryer_standby_firmware]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'auto_update': False,
|
||||
'device_class': 'firmware',
|
||||
'display_precision': 0,
|
||||
'entity_picture': 'https://brands.home-assistant.io/_/vesync/icon.png',
|
||||
'friendly_name': 'CS158-AF Air Fryer Standby Firmware',
|
||||
'in_progress': False,
|
||||
'installed_version': None,
|
||||
'latest_version': None,
|
||||
'release_summary': None,
|
||||
'release_url': None,
|
||||
'skipped_version': None,
|
||||
'supported_features': <UpdateEntityFeature: 0>,
|
||||
'title': None,
|
||||
'update_percentage': None,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'update.cs158_af_air_fryer_standby_firmware',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_update_state[Dimmable Light][devices]
|
||||
list([
|
||||
DeviceRegistryEntrySnapshot({
|
||||
|
||||
@@ -12,3 +12,4 @@ TEST_PASSWORD = "fake_password"
|
||||
TEST_TYPE = DeviceType.SERCOMM
|
||||
TEST_URL = f"https://{TEST_HOST}"
|
||||
TEST_USERNAME = "fake_username"
|
||||
TEST_SERIAL_NUMBER = "m123456789"
|
||||
|
||||
@@ -19,6 +19,7 @@ from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import setup_integration
|
||||
from .const import TEST_SERIAL_NUMBER
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
@@ -51,7 +52,7 @@ async def test_pressing_button(
|
||||
await hass.services.async_call(
|
||||
BUTTON_DOMAIN,
|
||||
SERVICE_PRESS,
|
||||
{ATTR_ENTITY_ID: "button.vodafone_station_m123456789_restart"},
|
||||
{ATTR_ENTITY_ID: f"button.vodafone_station_{TEST_SERIAL_NUMBER}_restart"},
|
||||
blocking=True,
|
||||
)
|
||||
mock_vodafone_station_router.restart_router.assert_called_once()
|
||||
@@ -84,7 +85,7 @@ async def test_button_fails(
|
||||
await hass.services.async_call(
|
||||
BUTTON_DOMAIN,
|
||||
SERVICE_PRESS,
|
||||
{ATTR_ENTITY_ID: "button.vodafone_station_m123456789_restart"},
|
||||
{ATTR_ENTITY_ID: f"button.vodafone_station_{TEST_SERIAL_NUMBER}_restart"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import setup_integration
|
||||
from .const import TEST_SERIAL_NUMBER
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
|
||||
|
||||
@@ -52,7 +53,9 @@ async def test_active_connection_type(
|
||||
"""Test device connection type."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
active_connection_entity = "sensor.vodafone_station_m123456789_active_connection"
|
||||
active_connection_entity = (
|
||||
f"sensor.vodafone_station_{TEST_SERIAL_NUMBER}_active_connection"
|
||||
)
|
||||
|
||||
assert (state := hass.states.get(active_connection_entity))
|
||||
assert state.state == STATE_UNKNOWN
|
||||
@@ -80,7 +83,7 @@ async def test_uptime(
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
uptime = "2024-11-19T20:19:00+00:00"
|
||||
uptime_entity = "sensor.vodafone_station_m123456789_uptime"
|
||||
uptime_entity = f"sensor.vodafone_station_{TEST_SERIAL_NUMBER}_uptime"
|
||||
|
||||
assert (state := hass.states.get(uptime_entity))
|
||||
assert state.state == uptime
|
||||
@@ -119,5 +122,7 @@ async def test_coordinator_client_connector_error(
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert (state := hass.states.get("sensor.vodafone_station_m123456789_uptime"))
|
||||
assert (
|
||||
state := hass.states.get(f"sensor.vodafone_station_{TEST_SERIAL_NUMBER}_uptime")
|
||||
)
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
Reference in New Issue
Block a user