Compare commits

..

22 Commits

Author SHA1 Message Date
Bram Kragten
49086b2a76 2026.1.0 (#159957) 2026-01-07 18:38:10 +01:00
Bram Kragten
1f28fe9933 Bump version to 2026.1.0 2026-01-07 17:46:04 +01:00
Bram Kragten
4465aa264c Update frontend to 20260107.0 (#160434) 2026-01-07 17:45:41 +01:00
Robert Resch
2c1bc96161 Bump deebot-client to 17.0.1 (#160428) 2026-01-07 17:45:40 +01:00
Joost Lekkerkerker
7127159a5b Make Watts depend on the cloud integration (#160424) 2026-01-07 17:45:38 +01:00
Abílio Costa
9f0eb6f077 Support target triggers in automation relation extraction (#160369) 2026-01-07 17:45:37 +01:00
Paul Bottein
da19cc06e3 Fix hvac_mode validation in climate.hvac_mode_changed trigger (#160364) 2026-01-07 17:45:36 +01:00
Bram Kragten
fd92377cf2 Bump version to 2026.1.0b5 2026-01-07 14:53:13 +01:00
Robert Resch
c201938b8b Constraint aiomqtt>=2.5.0 to fix blocking call (#160410)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-07 14:51:49 +01:00
Luke Lashley
b3765204b1 Bump python-roborock to 4.2.1 (#160398) 2026-01-07 14:48:27 +01:00
Luke Lashley
786257e051 Remove q7 total cleaning time for Roborock (#160399) 2026-01-07 14:47:47 +01:00
Allen Porter
9559634151 Update roborock binary sensor tests with snapshots (#159981) 2026-01-07 14:47:41 +01:00
Allen Porter
cf12ed8f08 Improve roborock test accuracy/robustness (#160021) 2026-01-07 14:45:53 +01:00
Michael Hansen
e213f49c75 Bump intents to 2026.1.6 (#160389) 2026-01-07 14:42:00 +01:00
Raphael Hehl
09c7cc113a Bump uiprotect to 8.0.0 (#160384)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2026-01-07 14:41:59 +01:00
dontinelli
e1e7e039a9 Bump solarlog_cli to 0.7.0 (#160382) 2026-01-07 14:41:58 +01:00
Daniel Hjelseth Høyer
05a0f0d23f Bump pyTibber to 0.34.1 (#160380)
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-07 14:41:57 +01:00
Artem Draft
d3853019eb Add SSL support in Bravia TV (#160373)
Co-authored-by: Maciej Bieniek <bieniu@users.noreply.github.com>
2026-01-07 14:41:55 +01:00
hanwg
ccbaac55b3 Fix schema validation error in Telegram (#160367) 2026-01-07 14:41:54 +01:00
Xiangxuan Qu
771292ced9 Fix IndexError in Israel Rail sensor when no departures available (#160351)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-01-07 14:41:53 +01:00
TheJulianJES
5d4262e8b3 Bump ZHA to 0.0.83 (#160342) 2026-01-07 14:41:52 +01:00
Paul Tarjan
d96da9a639 Fix Ring integration log flooding for accounts without subscription (#158012)
Co-authored-by: Robert Resch <robert@resch.dev>
2026-01-07 14:41:51 +01:00
40 changed files with 4431 additions and 1119 deletions

View File

@@ -7,7 +7,7 @@ import asyncio
from collections.abc import Callable, Mapping
from dataclasses import dataclass
import logging
from typing import Any, Protocol, cast
from typing import Any, Literal, Protocol, cast
from propcache.api import cached_property
import voluptuous as vol
@@ -16,7 +16,10 @@ from homeassistant.components import labs, websocket_api
from homeassistant.components.blueprint import CONF_USE_BLUEPRINT
from homeassistant.components.labs import async_listen as async_labs_listen
from homeassistant.const import (
ATTR_AREA_ID,
ATTR_ENTITY_ID,
ATTR_FLOOR_ID,
ATTR_LABEL_ID,
ATTR_MODE,
ATTR_NAME,
CONF_ACTIONS,
@@ -30,6 +33,7 @@ from homeassistant.const import (
CONF_OPTIONS,
CONF_PATH,
CONF_PLATFORM,
CONF_TARGET,
CONF_TRIGGERS,
CONF_VARIABLES,
CONF_ZONE,
@@ -588,20 +592,32 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity):
"""Return True if entity is on."""
return self._async_detach_triggers is not None or self._is_enabled
@property
@cached_property
def referenced_labels(self) -> set[str]:
"""Return a set of referenced labels."""
return self.action_script.referenced_labels
referenced = self.action_script.referenced_labels
@property
for conf in self._trigger_config:
referenced |= set(_get_targets_from_trigger_config(conf, ATTR_LABEL_ID))
return referenced
@cached_property
def referenced_floors(self) -> set[str]:
"""Return a set of referenced floors."""
return self.action_script.referenced_floors
referenced = self.action_script.referenced_floors
for conf in self._trigger_config:
referenced |= set(_get_targets_from_trigger_config(conf, ATTR_FLOOR_ID))
return referenced
@cached_property
def referenced_areas(self) -> set[str]:
"""Return a set of referenced areas."""
return self.action_script.referenced_areas
referenced = self.action_script.referenced_areas
for conf in self._trigger_config:
referenced |= set(_get_targets_from_trigger_config(conf, ATTR_AREA_ID))
return referenced
@property
def referenced_blueprint(self) -> str | None:
@@ -1209,6 +1225,9 @@ def _trigger_extract_devices(trigger_conf: dict) -> list[str]:
if trigger_conf[CONF_PLATFORM] == "tag" and CONF_DEVICE_ID in trigger_conf:
return trigger_conf[CONF_DEVICE_ID] # type: ignore[no-any-return]
if target_devices := _get_targets_from_trigger_config(trigger_conf, CONF_DEVICE_ID):
return target_devices
return []
@@ -1239,9 +1258,28 @@ def _trigger_extract_entities(trigger_conf: dict) -> list[str]:
):
return [trigger_conf[CONF_EVENT_DATA][CONF_ENTITY_ID]]
if target_entities := _get_targets_from_trigger_config(
trigger_conf, CONF_ENTITY_ID
):
return target_entities
return []
@callback
def _get_targets_from_trigger_config(
config: dict,
target: Literal["entity_id", "device_id", "area_id", "floor_id", "label_id"],
) -> list[str]:
"""Extract targets from a target config."""
if not (target_conf := config.get(CONF_TARGET)):
return []
if not (targets := target_conf.get(target)):
return []
return [targets] if isinstance(targets, str) else targets
@websocket_api.websocket_command({"type": "automation/config", "entity_id": str})
def websocket_config(
hass: HomeAssistant,

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

@@ -33,7 +33,7 @@ HVAC_MODE_CHANGED_TRIGGER_SCHEMA = ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST.extend
{
vol.Required(CONF_OPTIONS): {
vol.Required(CONF_HVAC_MODE): vol.All(
cv.ensure_list, vol.Length(min=1), [HVACMode]
cv.ensure_list, vol.Length(min=1), [vol.Coerce(HVACMode)]
),
},
}

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

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
"requirements": ["py-sucks==0.9.11", "deebot-client==17.0.0"]
"requirements": ["py-sucks==0.9.11", "deebot-client==17.0.1"]
}

View File

@@ -23,5 +23,5 @@
"winter_mode": {}
},
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20251229.1"]
"requirements": ["home-assistant-frontend==20260107.0"]
}

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

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

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

@@ -3,7 +3,7 @@
"name": "Watts Vision +",
"codeowners": ["@theobld-ww", "@devender-verma-ww", "@ssi-spyro"],
"config_flow": true,
"dependencies": ["application_credentials"],
"dependencies": ["application_credentials", "cloud"],
"documentation": "https://www.home-assistant.io/integrations/watts",
"iot_class": "cloud_polling",
"quality_scale": "bronze",

View File

@@ -23,7 +23,7 @@
"universal_silabs_flasher",
"serialx"
],
"requirements": ["zha==0.0.82", "serialx==0.5.0"],
"requirements": ["zha==0.0.83", "serialx==0.5.0"],
"usb": [
{
"description": "*2652*",

View File

@@ -17,7 +17,7 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2026
MINOR_VERSION: Final = 1
PATCH_VERSION: Final = "0b4"
PATCH_VERSION: Final = "0"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2)

View File

@@ -39,8 +39,8 @@ habluetooth==5.8.0
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-frontend==20260107.0
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

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2026.1.0b4"
version = "2026.1.0"
license = "Apache-2.0"
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
description = "Open-source home automation platform running on Python 3."

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

16
requirements_all.txt generated
View File

@@ -782,7 +782,7 @@ debugpy==1.8.17
decora-wifi==1.4
# homeassistant.components.ecovacs
deebot-client==17.0.0
deebot-client==17.0.1
# homeassistant.components.ihc
# homeassistant.components.namecheapdns
@@ -1213,10 +1213,10 @@ hole==0.9.0
holidays==0.84
# homeassistant.components.frontend
home-assistant-frontend==20251229.1
home-assistant-frontend==20260107.0
# 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
@@ -1867,7 +1867,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
@@ -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
@@ -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
@@ -3277,7 +3277,7 @@ zeroconf==0.148.0
zeversolar==0.3.2
# homeassistant.components.zha
zha==0.0.82
zha==0.0.83
# homeassistant.components.zhong_hong
zhong-hong-hvac==1.0.13

View File

@@ -691,7 +691,7 @@ dbus-fast==3.1.2
debugpy==1.8.17
# homeassistant.components.ecovacs
deebot-client==17.0.0
deebot-client==17.0.1
# homeassistant.components.ihc
# homeassistant.components.namecheapdns
@@ -1071,10 +1071,10 @@ hole==0.9.0
holidays==0.84
# homeassistant.components.frontend
home-assistant-frontend==20251229.1
home-assistant-frontend==20260107.0
# 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
@@ -1595,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
@@ -2165,7 +2165,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
@@ -2420,7 +2420,7 @@ soco==0.30.13
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
@@ -2569,7 +2569,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
@@ -2738,7 +2738,7 @@ zeroconf==0.148.0
zeversolar==0.3.2
# homeassistant.components.zha
zha==0.0.82
zha==0.0.83
# homeassistant.components.zwave_js
zwave-js-server-python==0.67.1

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

@@ -2232,6 +2232,202 @@ async def test_extraction_functions(
assert automation.blueprint_in_automation(hass, "automation.test3") is None
async def test_extraction_functions_with_targets(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test extraction functions with targets in triggers.
This test verifies that targets specified in trigger configurations
(using new-style triggers that support target) are properly extracted for
entity, device, area, floor, and label references.
"""
config_entry = MockConfigEntry(domain="fake_integration", data={})
config_entry.mock_state(hass, ConfigEntryState.LOADED)
config_entry.add_to_hass(hass)
trigger_device = device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, "00:00:00:00:00:01")},
)
await async_setup_component(hass, "homeassistant", {})
await async_setup_component(
hass, "scene", {"scene": {"name": "test", "entities": {}}}
)
await hass.async_block_till_done()
# Enable the new_triggers_conditions feature flag to allow new-style triggers
assert await async_setup_component(hass, "labs", {})
ws_client = await hass_ws_client(hass)
await ws_client.send_json_auto_id(
{
"type": "labs/update",
"domain": "automation",
"preview_feature": "new_triggers_conditions",
"enabled": True,
}
)
msg = await ws_client.receive_json()
assert msg["success"]
await hass.async_block_till_done()
assert await async_setup_component(
hass,
DOMAIN,
{
DOMAIN: [
{
"alias": "test1",
"triggers": [
# Single entity_id in target
{
"trigger": "scene.activated",
"target": {"entity_id": "scene.target_entity"},
},
# Multiple entity_ids in target
{
"trigger": "scene.activated",
"target": {
"entity_id": [
"scene.target_entity_list1",
"scene.target_entity_list2",
]
},
},
# Single device_id in target
{
"trigger": "scene.activated",
"target": {"device_id": trigger_device.id},
},
# Multiple device_ids in target
{
"trigger": "scene.activated",
"target": {
"device_id": [
"target-device-1",
"target-device-2",
]
},
},
# Single area_id in target
{
"trigger": "scene.activated",
"target": {"area_id": "area-target-single"},
},
# Multiple area_ids in target
{
"trigger": "scene.activated",
"target": {"area_id": ["area-target-1", "area-target-2"]},
},
# Single floor_id in target
{
"trigger": "scene.activated",
"target": {"floor_id": "floor-target-single"},
},
# Multiple floor_ids in target
{
"trigger": "scene.activated",
"target": {
"floor_id": ["floor-target-1", "floor-target-2"]
},
},
# Single label_id in target
{
"trigger": "scene.activated",
"target": {"label_id": "label-target-single"},
},
# Multiple label_ids in target
{
"trigger": "scene.activated",
"target": {
"label_id": ["label-target-1", "label-target-2"]
},
},
# Combined targets
{
"trigger": "scene.activated",
"target": {
"entity_id": "scene.combined_entity",
"device_id": "combined-device",
"area_id": "combined-area",
"floor_id": "combined-floor",
"label_id": "combined-label",
},
},
],
"conditions": [],
"actions": [
{
"action": "test.script",
"data": {"entity_id": "light.action_entity"},
},
],
},
]
},
)
# Test entity extraction from trigger targets
assert set(automation.entities_in_automation(hass, "automation.test1")) == {
"scene.target_entity",
"scene.target_entity_list1",
"scene.target_entity_list2",
"scene.combined_entity",
"light.action_entity",
}
# Test device extraction from trigger targets
assert set(automation.devices_in_automation(hass, "automation.test1")) == {
trigger_device.id,
"target-device-1",
"target-device-2",
"combined-device",
}
# Test area extraction from trigger targets
assert set(automation.areas_in_automation(hass, "automation.test1")) == {
"area-target-single",
"area-target-1",
"area-target-2",
"combined-area",
}
# Test floor extraction from trigger targets
assert set(automation.floors_in_automation(hass, "automation.test1")) == {
"floor-target-single",
"floor-target-1",
"floor-target-2",
"combined-floor",
}
# Test label extraction from trigger targets
assert set(automation.labels_in_automation(hass, "automation.test1")) == {
"label-target-single",
"label-target-1",
"label-target-2",
"combined-label",
}
# Test automations_with_* functions
assert set(automation.automations_with_entity(hass, "scene.target_entity")) == {
"automation.test1"
}
assert set(automation.automations_with_device(hass, trigger_device.id)) == {
"automation.test1"
}
assert set(automation.automations_with_area(hass, "area-target-single")) == {
"automation.test1"
}
assert set(automation.automations_with_floor(hass, "floor-target-single")) == {
"automation.test1"
}
assert set(automation.automations_with_label(hass, "label-target-single")) == {
"automation.test1"
}
async def test_logbook_humanify_automation_triggered_event(hass: HomeAssistant) -> None:
"""Test humanifying Automation Trigger event."""
hass.config.components.add("recorder")

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

@@ -105,12 +105,12 @@ async def test_climate_triggers_gated_by_labs_flag(
# Valid configurations
(
"climate.hvac_mode_changed",
{CONF_HVAC_MODE: [HVACMode.HEAT, HVACMode.COOL]},
{CONF_HVAC_MODE: ["heat", "cool"]},
does_not_raise(),
),
(
"climate.hvac_mode_changed",
{CONF_HVAC_MODE: HVACMode.HEAT},
{CONF_HVAC_MODE: "heat"},
does_not_raise(),
),
# Invalid configurations
@@ -305,7 +305,7 @@ def parametrize_xxx_crossed_threshold_trigger_states(
[
*parametrize_climate_trigger_states(
trigger="climate.hvac_mode_changed",
trigger_options={CONF_HVAC_MODE: [HVACMode.HEAT, HVACMode.COOL]},
trigger_options={CONF_HVAC_MODE: ["heat", "cool"]},
target_states=[HVACMode.HEAT, HVACMode.COOL],
other_states=other_states([HVACMode.HEAT, HVACMode.COOL]),
),
@@ -465,7 +465,7 @@ async def test_climate_state_attribute_trigger_behavior_any(
[
*parametrize_climate_trigger_states(
trigger="climate.hvac_mode_changed",
trigger_options={CONF_HVAC_MODE: [HVACMode.HEAT, HVACMode.COOL]},
trigger_options={CONF_HVAC_MODE: ["heat", "cool"]},
target_states=[HVACMode.HEAT, HVACMode.COOL],
other_states=other_states([HVACMode.HEAT, HVACMode.COOL]),
),
@@ -615,7 +615,7 @@ async def test_climate_state_attribute_trigger_behavior_first(
[
*parametrize_climate_trigger_states(
trigger="climate.hvac_mode_changed",
trigger_options={CONF_HVAC_MODE: [HVACMode.HEAT, HVACMode.COOL]},
trigger_options={CONF_HVAC_MODE: ["heat", "cool"]},
target_states=[HVACMode.HEAT, HVACMode.COOL],
other_states=other_states([HVACMode.HEAT, HVACMode.COOL]),
),

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

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

@@ -148,6 +148,20 @@ class FakeDevice(RoborockDevice):
"""Close the device."""
def set_trait_attributes(
trait: AsyncMock,
dataclass_template: RoborockBase,
init_none: bool = False,
) -> None:
"""Set attributes on a mock roborock trait."""
template_copy = deepcopy(dataclass_template)
for attr_name in dir(template_copy):
if attr_name.startswith("_"):
continue
attr_value = getattr(template_copy, attr_name) if not init_none else None
setattr(trait, attr_name, attr_value)
def make_mock_trait(
trait_spec: type[V1TraitMixin] | None = None,
dataclass_template: RoborockBase | None = None,
@@ -156,12 +170,14 @@ def make_mock_trait(
trait = AsyncMock(spec=trait_spec or V1TraitMixin)
if dataclass_template is not None:
# Copy all attributes and property methods (e.g. computed properties)
template_copy = deepcopy(dataclass_template)
for attr_name in dir(template_copy):
if attr_name.startswith("_"):
continue
setattr(trait, attr_name, getattr(template_copy, attr_name))
trait.refresh = AsyncMock()
# on the first call to refresh(). The object starts uninitialized.
set_trait_attributes(trait, dataclass_template, init_none=True)
async def refresh() -> None:
if dataclass_template is not None:
set_trait_attributes(trait, dataclass_template)
trait.refresh = AsyncMock(side_effect=refresh)
return trait

View File

@@ -0,0 +1,491 @@
# serializer version: 1
# name: test_binary_sensors[binary_sensor.roborock_s7_2_charging-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': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.roborock_s7_2_charging',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.BATTERY_CHARGING: 'battery_charging'>,
'original_icon': None,
'original_name': 'Charging',
'platform': 'roborock',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'battery_charging_device_2',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensors[binary_sensor.roborock_s7_2_charging-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'battery_charging',
'friendly_name': 'Roborock S7 2 Charging',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.roborock_s7_2_charging',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_binary_sensors[binary_sensor.roborock_s7_2_cleaning-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': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.roborock_s7_2_cleaning',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.RUNNING: 'running'>,
'original_icon': None,
'original_name': 'Cleaning',
'platform': 'roborock',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'in_cleaning',
'unique_id': 'in_cleaning_device_2',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensors[binary_sensor.roborock_s7_2_cleaning-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'running',
'friendly_name': 'Roborock S7 2 Cleaning',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.roborock_s7_2_cleaning',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_binary_sensors[binary_sensor.roborock_s7_2_mop_attached-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': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.roborock_s7_2_mop_attached',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.CONNECTIVITY: 'connectivity'>,
'original_icon': None,
'original_name': 'Mop attached',
'platform': 'roborock',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'mop_attached',
'unique_id': 'water_box_carriage_status_device_2',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensors[binary_sensor.roborock_s7_2_mop_attached-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'connectivity',
'friendly_name': 'Roborock S7 2 Mop attached',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.roborock_s7_2_mop_attached',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_binary_sensors[binary_sensor.roborock_s7_2_water_box_attached-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': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.roborock_s7_2_water_box_attached',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.CONNECTIVITY: 'connectivity'>,
'original_icon': None,
'original_name': 'Water box attached',
'platform': 'roborock',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'water_box_attached',
'unique_id': 'water_box_status_device_2',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensors[binary_sensor.roborock_s7_2_water_box_attached-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'connectivity',
'friendly_name': 'Roborock S7 2 Water box attached',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.roborock_s7_2_water_box_attached',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_binary_sensors[binary_sensor.roborock_s7_2_water_shortage-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': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.roborock_s7_2_water_shortage',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
'original_icon': None,
'original_name': 'Water shortage',
'platform': 'roborock',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'water_shortage',
'unique_id': 'water_shortage_device_2',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensors[binary_sensor.roborock_s7_2_water_shortage-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'problem',
'friendly_name': 'Roborock S7 2 Water shortage',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.roborock_s7_2_water_shortage',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_binary_sensors[binary_sensor.roborock_s7_maxv_charging-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': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.roborock_s7_maxv_charging',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.BATTERY_CHARGING: 'battery_charging'>,
'original_icon': None,
'original_name': 'Charging',
'platform': 'roborock',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'battery_charging_abc123',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensors[binary_sensor.roborock_s7_maxv_charging-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'battery_charging',
'friendly_name': 'Roborock S7 MaxV Charging',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.roborock_s7_maxv_charging',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_binary_sensors[binary_sensor.roborock_s7_maxv_cleaning-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': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.roborock_s7_maxv_cleaning',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.RUNNING: 'running'>,
'original_icon': None,
'original_name': 'Cleaning',
'platform': 'roborock',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'in_cleaning',
'unique_id': 'in_cleaning_abc123',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensors[binary_sensor.roborock_s7_maxv_cleaning-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'running',
'friendly_name': 'Roborock S7 MaxV Cleaning',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.roborock_s7_maxv_cleaning',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_binary_sensors[binary_sensor.roborock_s7_maxv_mop_attached-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': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.roborock_s7_maxv_mop_attached',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.CONNECTIVITY: 'connectivity'>,
'original_icon': None,
'original_name': 'Mop attached',
'platform': 'roborock',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'mop_attached',
'unique_id': 'water_box_carriage_status_abc123',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensors[binary_sensor.roborock_s7_maxv_mop_attached-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'connectivity',
'friendly_name': 'Roborock S7 MaxV Mop attached',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.roborock_s7_maxv_mop_attached',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_binary_sensors[binary_sensor.roborock_s7_maxv_water_box_attached-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': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.roborock_s7_maxv_water_box_attached',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.CONNECTIVITY: 'connectivity'>,
'original_icon': None,
'original_name': 'Water box attached',
'platform': 'roborock',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'water_box_attached',
'unique_id': 'water_box_status_abc123',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensors[binary_sensor.roborock_s7_maxv_water_box_attached-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'connectivity',
'friendly_name': 'Roborock S7 MaxV Water box attached',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.roborock_s7_maxv_water_box_attached',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_binary_sensors[binary_sensor.roborock_s7_maxv_water_shortage-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': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.roborock_s7_maxv_water_shortage',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
'original_icon': None,
'original_name': 'Water shortage',
'platform': 'roborock',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'water_shortage',
'unique_id': 'water_shortage_abc123',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensors[binary_sensor.roborock_s7_maxv_water_shortage-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'problem',
'friendly_name': 'Roborock S7 MaxV Water shortage',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.roborock_s7_maxv_water_shortage',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,13 @@
"""Test Roborock Binary Sensor."""
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from tests.common import MockConfigEntry
from tests.common import MockConfigEntry, snapshot_platform
@pytest.fixture
@@ -15,17 +17,10 @@ def platforms() -> list[Platform]:
async def test_binary_sensors(
hass: HomeAssistant, setup_entry: MockConfigEntry
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
setup_entry: MockConfigEntry,
snapshot: SnapshotAssertion,
) -> None:
"""Test binary sensors and check test values are correctly set."""
assert len(hass.states.async_all("binary_sensor")) == 10
assert hass.states.get("binary_sensor.roborock_s7_maxv_mop_attached").state == "on"
assert (
hass.states.get("binary_sensor.roborock_s7_maxv_water_box_attached").state
== "on"
)
assert (
hass.states.get("binary_sensor.roborock_s7_maxv_water_shortage").state == "off"
)
assert hass.states.get("binary_sensor.roborock_s7_maxv_cleaning").state == "off"
assert hass.states.get("binary_sensor.roborock_s7_maxv_charging").state == "on"
await snapshot_platform(hass, entity_registry, snapshot, setup_entry.entry_id)

View File

@@ -5,8 +5,9 @@ from syrupy.assertion import SnapshotAssertion
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from tests.common import MockConfigEntry
from tests.common import MockConfigEntry, snapshot_platform
@pytest.fixture
@@ -17,8 +18,9 @@ def platforms() -> list[Platform]:
async def test_sensors(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
setup_entry: MockConfigEntry,
snapshot: SnapshotAssertion,
) -> None:
"""Test sensors and check test values are correctly set."""
assert snapshot == hass.states.async_all("sensor")
await snapshot_platform(hass, entity_registry, snapshot, setup_entry.entry_id)

View File

@@ -31,7 +31,8 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.setup import async_setup_component
from .conftest import FakeDevice
from .conftest import FakeDevice, set_trait_attributes
from .mock_data import STATUS
from tests.common import MockConfigEntry
@@ -132,8 +133,14 @@ async def test_resume_cleaning(
vacuum_command: Mock,
) -> None:
"""Test resuming clean on start button when a clean is paused."""
fake_vacuum.v1_properties.status.in_cleaning = in_cleaning_int
fake_vacuum.v1_properties.status.in_returning = in_returning_int
async def refresh_properties() -> None:
set_trait_attributes(fake_vacuum.v1_properties.status, STATUS)
fake_vacuum.v1_properties.status.in_cleaning = in_cleaning_int
fake_vacuum.v1_properties.status.in_returning = in_returning_int
fake_vacuum.v1_properties.status.refresh.side_effect = refresh_properties
await async_setup_component(hass, DOMAIN, {})
vacuum = hass.states.get(ENTITY_ID)
assert vacuum

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

@@ -101,3 +101,16 @@ def mock_config_entry() -> MockConfigEntry:
entry_id="01J0BC4QM2YBRP6H5G933CETI8",
unique_id=TEST_USER_ID,
)
@pytest.fixture(name="skip_cloud", autouse=True)
def skip_cloud_fixture():
"""Skip setting up cloud.
Cloud already has its own tests for account link.
We do not need to test it here as we only need to test our
usage of the oauth2 helpers.
"""
with patch("homeassistant.components.cloud.async_setup", return_value=True):
yield