Compare commits

...

23 Commits

Author SHA1 Message Date
Erik
45f1fc32e2 Address comments 2026-01-08 09:36:34 +01:00
Erik
1e95598c96 Add helper for creating entity condition tests 2026-01-07 14:58:50 +01:00
Norbert Rittel
e963adfdf0 Fix capitalization in openevse data_description string (#160423) 2026-01-07 14:53:19 +01:00
Simone Chemelli
fd7bbc68c6 Bump aioshelly to 13.23.1 (#160420) 2026-01-07 14:49:18 +01:00
Robert Resch
9281ab018c Constraint aiomqtt>=2.5.0 to fix blocking call (#160410)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-07 14:21:49 +01:00
Andres Ruiz
80baf86e23 Add codeowners and integration_type for waterfurnace (#160397) 2026-01-07 13:12:58 +01:00
Simone Chemelli
db497b23fe Small cleanup for Vodafone Station tests (#160415) 2026-01-07 12:50:12 +01:00
cdnninja
a2fb8f5a72 Add Vesync Air Fryer Sensors (#160170)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-01-07 12:41:34 +01:00
hanwg
6953bd4599 Fix schema validation error in Telegram (#160367) 2026-01-07 12:27:17 +01:00
Xiangxuan Qu
225be65f71 Fix IndexError in Israel Rail sensor when no departures available (#160351)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-01-07 12:22:39 +01:00
momala454
7b0463f763 Add additional lens modes 4 to 10 to JVC projector remote (#159657)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-01-07 12:22:19 +01:00
Luke Lashley
4d305b657a Bump python-roborock to 4.2.1 (#160398) 2026-01-07 11:23:40 +01:00
Paul Tarjan
d5a553c8c7 Fix Ring integration log flooding for accounts without subscription (#158012)
Co-authored-by: Robert Resch <robert@resch.dev>
2026-01-07 11:14:05 +01:00
Ivan Dlugos
9169b68254 Bump sentry-sdk to 2.48.0 (#159415)
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-07 11:05:38 +01:00
Colin
fde9bd95d5 Replace openevse backend library (#160325) 2026-01-07 10:25:15 +01:00
Marc Mueller
e4db8ff86e Update guppy3 to 3.1.6 (#160356) 2026-01-07 10:11:01 +01:00
Erik Montnemery
a084e51345 Add test helpers for numerical state triggers (#160308)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2026-01-07 08:53:35 +01:00
Luke Lashley
00381e6dfd Remove q7 total cleaning time for Roborock (#160399) 2026-01-06 20:27:09 -08:00
Michael Hansen
b6d493696a Bump intents to 2026.1.6 (#160389) 2026-01-06 17:11:54 -06:00
Artem Draft
5f0500c3cd Add SSL support in Bravia TV (#160373)
Co-authored-by: Maciej Bieniek <bieniu@users.noreply.github.com>
2026-01-06 23:59:47 +01:00
dontinelli
c61a63cc6f Bump solarlog_cli to 0.7.0 (#160382) 2026-01-06 23:59:16 +01:00
Raphael Hehl
5445a4f40f Bump uiprotect to 8.0.0 (#160384)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2026-01-06 23:57:19 +01:00
Daniel Hjelseth Høyer
2888cacc3f Bump pyTibber to 0.34.1 (#160380)
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-06 23:56:26 +01:00
64 changed files with 2103 additions and 770 deletions

1
CODEOWNERS generated
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["jvcprojector"],
"requirements": ["pyjvcprojector==1.1.2"]
"requirements": ["pyjvcprojector==1.1.3"]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7468,7 +7468,7 @@
},
"waterfurnace": {
"name": "WaterFurnace",
"integration_type": "hub",
"integration_type": "device",
"config_flow": false,
"iot_class": "cloud_polling"
},

View File

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

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

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

View File

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

View File

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

View File

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

View File

@@ -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 {}
),
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,12 @@
{
"traceId": "1234",
"code": 0,
"msg": "request success",
"module": null,
"stacktrace": null,
"result": {
"returnStatus": {
"cookStatus": "standby"
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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