mirror of
https://github.com/home-assistant/core.git
synced 2026-01-14 11:37:25 +01:00
Compare commits
61 Commits
2026.1.0b4
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8f23bb388 | ||
|
|
e238d67818 | ||
|
|
992a9bdd3b | ||
|
|
ceaae1c1cc | ||
|
|
1c163c92dc | ||
|
|
a42aa9372c | ||
|
|
013592bd54 | ||
|
|
2101bae095 | ||
|
|
cfa1107135 | ||
|
|
a269ef660a | ||
|
|
c43c4f17e9 | ||
|
|
de25e6af51 | ||
|
|
18d3629b6c | ||
|
|
50c477a408 | ||
|
|
ea9cd7d905 | ||
|
|
2bf4ac20ea | ||
|
|
94ff881897 | ||
|
|
2975b3c1b9 | ||
|
|
0143c4ff85 | ||
|
|
f59566d20b | ||
|
|
395f0ad2a7 | ||
|
|
2af1fc6759 | ||
|
|
c1e7122d1c | ||
|
|
e5624b1224 | ||
|
|
6e380bafca | ||
|
|
bb9fd94430 | ||
|
|
07bc5d5c6b | ||
|
|
651b7116dd | ||
|
|
34438bd039 | ||
|
|
7b53b8691c | ||
|
|
8748d6f200 | ||
|
|
8d95511650 | ||
|
|
9aa5953a86 | ||
|
|
5ccdfda747 | ||
|
|
00ad44cb91 | ||
|
|
b7519cd880 | ||
|
|
ac44769539 | ||
|
|
9e95b80805 | ||
|
|
50086ca5c7 | ||
|
|
49086b2a76 | ||
|
|
1f28fe9933 | ||
|
|
4465aa264c | ||
|
|
2c1bc96161 | ||
|
|
7127159a5b | ||
|
|
9f0eb6f077 | ||
|
|
da19cc06e3 | ||
|
|
fd92377cf2 | ||
|
|
c201938b8b | ||
|
|
b3765204b1 | ||
|
|
786257e051 | ||
|
|
9559634151 | ||
|
|
cf12ed8f08 | ||
|
|
e213f49c75 | ||
|
|
09c7cc113a | ||
|
|
e1e7e039a9 | ||
|
|
05a0f0d23f | ||
|
|
d3853019eb | ||
|
|
ccbaac55b3 | ||
|
|
771292ced9 | ||
|
|
5d4262e8b3 | ||
|
|
d96da9a639 |
@@ -69,6 +69,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import device_registry as dr, llm
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.json import json_dumps
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from . import AnthropicConfigEntry
|
||||
@@ -193,7 +194,7 @@ def _convert_content(
|
||||
tool_result_block = ToolResultBlockParam(
|
||||
type="tool_result",
|
||||
tool_use_id=content.tool_call_id,
|
||||
content=json.dumps(content.tool_result),
|
||||
content=json_dumps(content.tool_result),
|
||||
)
|
||||
external_tool = False
|
||||
if not messages or messages[-1]["role"] != (
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["pysilero-vad==3.0.1", "pyspeex-noise==1.0.2"]
|
||||
"requirements": ["pysilero-vad==3.2.0", "pyspeex-noise==1.0.2"]
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -36,6 +36,10 @@ _LOGGER = logging.getLogger(__name__)
|
||||
# Cache TTL for backup list (in seconds)
|
||||
CACHE_TTL = 300
|
||||
|
||||
# Timeout for upload operations (in seconds)
|
||||
# This prevents uploads from hanging indefinitely
|
||||
UPLOAD_TIMEOUT = 43200 # 12 hours (matches B2 HTTP timeout)
|
||||
|
||||
|
||||
def suggested_filenames(backup: AgentBackup) -> tuple[str, str]:
|
||||
"""Return the suggested filenames for the backup and metadata files."""
|
||||
@@ -329,13 +333,28 @@ class BackblazeBackupAgent(BackupAgent):
|
||||
_LOGGER.debug("Uploading backup file %s with streaming", filename)
|
||||
try:
|
||||
content_type, _ = mimetypes.guess_type(filename)
|
||||
file_version = await self._hass.async_add_executor_job(
|
||||
self._upload_unbound_stream_sync,
|
||||
reader,
|
||||
filename,
|
||||
content_type or "application/x-tar",
|
||||
file_info,
|
||||
file_version = await asyncio.wait_for(
|
||||
self._hass.async_add_executor_job(
|
||||
self._upload_unbound_stream_sync,
|
||||
reader,
|
||||
filename,
|
||||
content_type or "application/x-tar",
|
||||
file_info,
|
||||
),
|
||||
timeout=UPLOAD_TIMEOUT,
|
||||
)
|
||||
except TimeoutError:
|
||||
_LOGGER.error(
|
||||
"Upload of %s timed out after %s seconds", filename, UPLOAD_TIMEOUT
|
||||
)
|
||||
reader.abort()
|
||||
raise BackupAgentError(
|
||||
f"Upload timed out after {UPLOAD_TIMEOUT} seconds"
|
||||
) from None
|
||||
except asyncio.CancelledError:
|
||||
_LOGGER.warning("Upload of %s was cancelled", filename)
|
||||
reader.abort()
|
||||
raise
|
||||
finally:
|
||||
reader.close()
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ from homeassistant.const import CONF_HOST, CONF_MAC, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||
|
||||
from .const import CONF_USE_SSL
|
||||
from .coordinator import BraviaTVConfigEntry, BraviaTVCoordinator
|
||||
|
||||
PLATFORMS: Final[list[Platform]] = [
|
||||
@@ -26,11 +27,12 @@ async def async_setup_entry(
|
||||
"""Set up a config entry."""
|
||||
host = config_entry.data[CONF_HOST]
|
||||
mac = config_entry.data[CONF_MAC]
|
||||
ssl = config_entry.data.get(CONF_USE_SSL, False)
|
||||
|
||||
session = async_create_clientsession(
|
||||
hass, cookie_jar=CookieJar(unsafe=True, quote_cookie=False)
|
||||
)
|
||||
client = BraviaClient(host, mac, session=session)
|
||||
client = BraviaClient(host, mac, session=session, ssl=ssl)
|
||||
coordinator = BraviaTVCoordinator(
|
||||
hass=hass,
|
||||
config_entry=config_entry,
|
||||
|
||||
@@ -28,6 +28,7 @@ from .const import (
|
||||
ATTR_MODEL,
|
||||
CONF_NICKNAME,
|
||||
CONF_USE_PSK,
|
||||
CONF_USE_SSL,
|
||||
DOMAIN,
|
||||
NICKNAME_PREFIX,
|
||||
)
|
||||
@@ -46,11 +47,12 @@ class BraviaTVConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
def create_client(self) -> None:
|
||||
"""Create Bravia TV client from config."""
|
||||
host = self.device_config[CONF_HOST]
|
||||
ssl = self.device_config[CONF_USE_SSL]
|
||||
session = async_create_clientsession(
|
||||
self.hass,
|
||||
cookie_jar=CookieJar(unsafe=True, quote_cookie=False),
|
||||
)
|
||||
self.client = BraviaClient(host=host, session=session)
|
||||
self.client = BraviaClient(host=host, session=session, ssl=ssl)
|
||||
|
||||
async def gen_instance_ids(self) -> tuple[str, str]:
|
||||
"""Generate client_id and nickname."""
|
||||
@@ -123,10 +125,10 @@ class BraviaTVConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle authorize step."""
|
||||
self.create_client()
|
||||
|
||||
if user_input is not None:
|
||||
self.device_config[CONF_USE_PSK] = user_input[CONF_USE_PSK]
|
||||
self.device_config[CONF_USE_SSL] = user_input[CONF_USE_SSL]
|
||||
self.create_client()
|
||||
if user_input[CONF_USE_PSK]:
|
||||
return await self.async_step_psk()
|
||||
return await self.async_step_pin()
|
||||
@@ -136,6 +138,7 @@ class BraviaTVConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_USE_PSK, default=False): bool,
|
||||
vol.Required(CONF_USE_SSL, default=False): bool,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
@@ -12,6 +12,7 @@ ATTR_MODEL: Final = "model"
|
||||
|
||||
CONF_NICKNAME: Final = "nickname"
|
||||
CONF_USE_PSK: Final = "use_psk"
|
||||
CONF_USE_SSL: Final = "use_ssl"
|
||||
|
||||
DOMAIN: Final = "braviatv"
|
||||
LEGACY_CLIENT_ID: Final = "HomeAssistant"
|
||||
|
||||
@@ -15,9 +15,10 @@
|
||||
"step": {
|
||||
"authorize": {
|
||||
"data": {
|
||||
"use_psk": "Use PSK authentication"
|
||||
"use_psk": "Use PSK authentication",
|
||||
"use_ssl": "Use SSL connection"
|
||||
},
|
||||
"description": "Make sure that «Control remotely» is enabled on your TV, go to: \nSettings -> Network -> Remote device settings -> Control remotely. \n\nThere are two authorization methods: PIN code or PSK (Pre-Shared Key). \nAuthorization via PSK is recommended as more stable.",
|
||||
"description": "Make sure that «Control remotely» is enabled on your TV. Go to: \nSettings -> Network -> Remote device settings -> Control remotely. \n\nThere are two authorization methods: PIN code or PSK (Pre-Shared Key). \nAuthorization via PSK is recommended, as it is more stable. \n\nUse an SSL connection only if your TV supports this connection type.",
|
||||
"title": "Authorize Sony Bravia TV"
|
||||
},
|
||||
"confirm": {
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/bthome",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["bthome-ble==3.17.0"]
|
||||
"requirements": ["bthome-ble==3.16.0"]
|
||||
}
|
||||
|
||||
@@ -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)]
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -19,6 +19,10 @@
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
@@ -27,14 +31,11 @@
|
||||
- input_number
|
||||
- number
|
||||
- sensor
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
translation_key: number_or_entity
|
||||
|
||||
.trigger_threshold_type: &trigger_threshold_type
|
||||
required: true
|
||||
default: above
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["eheimdigital"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["eheimdigital==1.4.0"],
|
||||
"requirements": ["eheimdigital==1.5.0"],
|
||||
"zeroconf": [
|
||||
{ "name": "eheimdigital._http._tcp.local.", "type": "_http._tcp.local." }
|
||||
]
|
||||
|
||||
@@ -783,7 +783,7 @@ ENCHARGE_AGGREGATE_SENSORS = (
|
||||
translation_key="available_energy",
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
device_class=SensorDeviceClass.ENERGY_STORAGE,
|
||||
value_fn=attrgetter("available_energy"),
|
||||
),
|
||||
EnvoyEnchargeAggregateSensorEntityDescription(
|
||||
@@ -791,14 +791,14 @@ ENCHARGE_AGGREGATE_SENSORS = (
|
||||
translation_key="reserve_energy",
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
device_class=SensorDeviceClass.ENERGY_STORAGE,
|
||||
value_fn=attrgetter("backup_reserve"),
|
||||
),
|
||||
EnvoyEnchargeAggregateSensorEntityDescription(
|
||||
key="max_capacity",
|
||||
translation_key="max_capacity",
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
device_class=SensorDeviceClass.ENERGY_STORAGE,
|
||||
value_fn=attrgetter("max_available_capacity"),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -461,7 +461,7 @@ FITBIT_RESOURCES_LIST: Final[tuple[FitbitSensorEntityDescription, ...]] = (
|
||||
key="sleep/timeInBed",
|
||||
translation_key="sleep_time_in_bed",
|
||||
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||
icon="mdi:hotel",
|
||||
icon="mdi:bed",
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
scope=FitbitScope.SLEEP,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
|
||||
@@ -77,9 +77,14 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat
|
||||
)
|
||||
LOGGER.debug("enable smarthome templates: %s", self.has_templates)
|
||||
|
||||
self.has_triggers = await self.hass.async_add_executor_job(
|
||||
self.fritz.has_triggers
|
||||
)
|
||||
try:
|
||||
self.has_triggers = await self.hass.async_add_executor_job(
|
||||
self.fritz.has_triggers
|
||||
)
|
||||
except HTTPError:
|
||||
# Fritz!OS < 7.39 just don't have this api endpoint
|
||||
# so we need to fetch the HTTPError here and assume no triggers
|
||||
self.has_triggers = False
|
||||
LOGGER.debug("enable smarthome triggers: %s", self.has_triggers)
|
||||
|
||||
self.configuration_url = self.fritz.get_prefixed_host()
|
||||
|
||||
@@ -23,5 +23,5 @@
|
||||
"winter_mode": {}
|
||||
},
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20251229.1"]
|
||||
"requirements": ["home-assistant-frontend==20260107.1"]
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["google_air_quality_api"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["google_air_quality_api==2.0.2"]
|
||||
"requirements": ["google_air_quality_api==2.1.2"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/gree",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["greeclimate"],
|
||||
"requirements": ["greeclimate==2.1.0"]
|
||||
"requirements": ["greeclimate==2.1.1"]
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
from pyhik.constants import SENSOR_MAP
|
||||
from pyhik.hikvision import HikCamera
|
||||
import requests
|
||||
|
||||
@@ -70,13 +71,33 @@ async def async_setup_entry(hass: HomeAssistant, entry: HikvisionConfigEntry) ->
|
||||
device_type=device_type,
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
"Device %s (type=%s) initial event_states: %s",
|
||||
device_name,
|
||||
device_type,
|
||||
camera.current_event_states,
|
||||
)
|
||||
|
||||
# For NVRs or devices with no detected events, try to fetch events from ISAPI
|
||||
# Use broader notification methods for NVRs since they often use 'record' etc.
|
||||
if device_type == "NVR" or not camera.current_event_states:
|
||||
nvr_notification_methods = {"center", "HTTP", "record", "email", "beep"}
|
||||
|
||||
def fetch_and_inject_nvr_events() -> None:
|
||||
"""Fetch and inject NVR events in a single executor job."""
|
||||
if nvr_events := camera.get_event_triggers():
|
||||
camera.inject_events(nvr_events)
|
||||
nvr_events = camera.get_event_triggers(nvr_notification_methods)
|
||||
_LOGGER.debug("NVR events fetched with extended methods: %s", nvr_events)
|
||||
if nvr_events:
|
||||
# Map raw event type names to friendly names using SENSOR_MAP
|
||||
mapped_events: dict[str, list[int]] = {}
|
||||
for event_type, channels in nvr_events.items():
|
||||
friendly_name = SENSOR_MAP.get(event_type.lower(), event_type)
|
||||
if friendly_name in mapped_events:
|
||||
mapped_events[friendly_name].extend(channels)
|
||||
else:
|
||||
mapped_events[friendly_name] = list(channels)
|
||||
_LOGGER.debug("Mapped NVR events: %s", mapped_events)
|
||||
camera.inject_events(mapped_events)
|
||||
|
||||
await hass.async_add_executor_job(fetch_and_inject_nvr_events)
|
||||
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyhik"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["pyHik==0.3.4"]
|
||||
"requirements": ["pyHik==0.4.0"]
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/homeassistant_hardware",
|
||||
"integration_type": "system",
|
||||
"requirements": [
|
||||
"serialx==0.5.0",
|
||||
"serialx==0.6.2",
|
||||
"universal-silabs-flasher==0.1.2",
|
||||
"ha-silabs-firmware-client==0.3.0"
|
||||
]
|
||||
|
||||
@@ -13,6 +13,6 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["homewizard_energy"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["python-homewizard-energy==10.0.0"],
|
||||
"requirements": ["python-homewizard-energy==10.0.1"],
|
||||
"zeroconf": ["_hwenergy._tcp.local.", "_homewizard._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -19,6 +19,10 @@
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
@@ -27,14 +31,11 @@
|
||||
- input_number
|
||||
- number
|
||||
- sensor
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
translation_key: number_or_entity
|
||||
|
||||
.trigger_threshold_type: &trigger_threshold_type
|
||||
required: true
|
||||
default: above
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
|
||||
@@ -12,5 +12,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["incomfortclient"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["incomfort-client==0.6.10"]
|
||||
"requirements": ["incomfort-client==0.6.11"]
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
@@ -256,6 +256,8 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
|
||||
supported_fn=(
|
||||
lambda coordinator: coordinator.device.dashboard.model_name
|
||||
in (ModelName.LINEA_MINI, ModelName.LINEA_MINI_R)
|
||||
and WidgetType.CM_BREW_BY_WEIGHT_DOSES
|
||||
in coordinator.device.dashboard.config
|
||||
),
|
||||
),
|
||||
LaMarzoccoNumberEntityDescription(
|
||||
@@ -289,6 +291,8 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
|
||||
supported_fn=(
|
||||
lambda coordinator: coordinator.device.dashboard.model_name
|
||||
in (ModelName.LINEA_MINI, ModelName.LINEA_MINI_R)
|
||||
and WidgetType.CM_BREW_BY_WEIGHT_DOSES
|
||||
in coordinator.device.dashboard.config
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -149,6 +149,8 @@ ENTITIES: tuple[LaMarzoccoSelectEntityDescription, ...] = (
|
||||
supported_fn=(
|
||||
lambda coordinator: coordinator.device.dashboard.model_name
|
||||
in (ModelName.LINEA_MINI, ModelName.LINEA_MINI_R)
|
||||
and WidgetType.CM_BREW_BY_WEIGHT_DOSES
|
||||
in coordinator.device.dashboard.config
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -19,6 +19,10 @@
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
@@ -27,10 +31,6 @@
|
||||
- input_number
|
||||
- number
|
||||
- sensor
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
translation_key: number_or_entity
|
||||
|
||||
turned_on: *trigger_common
|
||||
@@ -48,6 +48,7 @@ brightness_crossed_threshold:
|
||||
behavior: *trigger_behavior
|
||||
threshold_type:
|
||||
required: true
|
||||
default: above
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pynintendoauth", "pynintendoparental"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pynintendoauth==1.0.2", "pynintendoparental==2.3.0"]
|
||||
"requirements": ["pynintendoauth==1.0.2", "pynintendoparental==2.3.2"]
|
||||
}
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["opower"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["opower==0.16.0"]
|
||||
"requirements": ["opower==0.16.1"]
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ SENSOR_TYPES: dict[str, OSOEnergySensorEntityDescription] = {
|
||||
key="optimization_mode",
|
||||
translation_key="optimization_mode",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=["off", "oso", "gridcompany", "smartcompany", "advanced"],
|
||||
options=["off", "oso", "gridcompany", "smartcompany", "advanced", "nettleie"],
|
||||
value_fn=lambda entity_data: entity_data.state.lower(),
|
||||
),
|
||||
"power_load": OSOEnergySensorEntityDescription(
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
"state": {
|
||||
"advanced": "Advanced",
|
||||
"gridcompany": "Grid company",
|
||||
"nettleie": "Nettleie",
|
||||
"off": "[%key:common::state::off%]",
|
||||
"oso": "OSO",
|
||||
"smartcompany": "Smart company"
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/otbr",
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["python-otbr-api==2.7.0"]
|
||||
"requirements": ["python-otbr-api==2.7.1"]
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["boto3", "botocore", "pyhumps", "pyoverkiz", "s3transfer"],
|
||||
"requirements": ["pyoverkiz==1.19.3"],
|
||||
"requirements": ["pyoverkiz==1.19.4"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"name": "gateway*",
|
||||
|
||||
@@ -128,8 +128,9 @@ class RingCam(RingEntity[RingDoorBell], Camera):
|
||||
self._device = self._get_coordinator_data().get_video_device(
|
||||
self._device.device_api_id
|
||||
)
|
||||
|
||||
history_data = self._device.last_history
|
||||
if history_data:
|
||||
if history_data and self._device.has_subscription:
|
||||
self._last_event = history_data[0]
|
||||
# will call async_update to update the attributes and get the
|
||||
# video url from the api
|
||||
@@ -154,8 +155,16 @@ class RingCam(RingEntity[RingDoorBell], Camera):
|
||||
self, width: int | None = None, height: int | None = None
|
||||
) -> bytes | None:
|
||||
"""Return a still image response from the camera."""
|
||||
if self._video_url is None:
|
||||
if not self._device.has_subscription:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="no_subscription",
|
||||
)
|
||||
return None
|
||||
|
||||
key = (width, height)
|
||||
if not (image := self._images.get(key)) and self._video_url is not None:
|
||||
if not (image := self._images.get(key)):
|
||||
image = await ffmpeg.async_get_image(
|
||||
self.hass,
|
||||
self._video_url,
|
||||
|
||||
@@ -151,6 +151,9 @@
|
||||
"api_timeout": {
|
||||
"message": "Timeout communicating with Ring API"
|
||||
},
|
||||
"no_subscription": {
|
||||
"message": "Ring Protect subscription required for snapshots"
|
||||
},
|
||||
"sdp_m_line_index_required": {
|
||||
"message": "Error negotiating stream for {device}"
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"loggers": ["roborock"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": [
|
||||
"python-roborock==4.2.0",
|
||||
"python-roborock==4.2.1",
|
||||
"vacuum-map-parser-roborock==0.1.4"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -391,15 +391,6 @@ Q7_B01_SENSOR_DESCRIPTIONS = [
|
||||
translation_key="mop_life_time_left",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
RoborockSensorDescriptionB01(
|
||||
key="total_cleaning_time",
|
||||
value_fn=lambda data: data.real_clean_time,
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||
suggested_unit_of_measurement=UnitOfTime.HOURS,
|
||||
translation_key="total_cleaning_time",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -16,5 +16,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pysma"],
|
||||
"requirements": ["pysma==1.0.2"]
|
||||
"requirements": ["pysma==1.1.0"]
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["PyTado"],
|
||||
"requirements": ["python-tado==0.18.15"]
|
||||
"requirements": ["python-tado==0.18.16"]
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -79,6 +79,7 @@ class OAuth2FlowHandler(
|
||||
|
||||
session = async_get_clientsession(self.hass)
|
||||
self.api = TeslaFleetApi(
|
||||
access_token="",
|
||||
session=session,
|
||||
server=server,
|
||||
partner_scope=True,
|
||||
|
||||
@@ -5,6 +5,7 @@ from collections.abc import Callable
|
||||
from typing import Final
|
||||
|
||||
from aiohttp import ClientResponseError
|
||||
from aiohttp.client_exceptions import ClientError
|
||||
from tesla_fleet_api.const import Scope
|
||||
from tesla_fleet_api.exceptions import (
|
||||
Forbidden,
|
||||
@@ -315,7 +316,7 @@ async def async_migrate_entry(
|
||||
data = await Teslemetry(session, access_token).migrate_to_oauth(
|
||||
CLIENT_ID, access_token, hass.config.location_name
|
||||
)
|
||||
except ClientResponseError as e:
|
||||
except (ClientError, TypeError) as e:
|
||||
raise ConfigEntryAuthFailed from e
|
||||
|
||||
# Add auth_implementation for OAuth2 flow compatibility
|
||||
|
||||
@@ -291,9 +291,7 @@ class TeslemetryStreamingClimateEntity(
|
||||
)
|
||||
)
|
||||
self.async_on_remove(
|
||||
self.vehicle.stream_vehicle.listen_HvacACEnabled(
|
||||
self._async_handle_hvac_ac_enabled
|
||||
)
|
||||
self.vehicle.stream_vehicle.listen_HvacPower(self._async_handle_hvac_power)
|
||||
)
|
||||
self.async_on_remove(
|
||||
self.vehicle.stream_vehicle.listen_ClimateKeeperMode(
|
||||
@@ -335,9 +333,13 @@ class TeslemetryStreamingClimateEntity(
|
||||
self._attr_current_temperature = data
|
||||
self.async_write_ha_state()
|
||||
|
||||
def _async_handle_hvac_ac_enabled(self, data: bool | None):
|
||||
def _async_handle_hvac_power(self, data: str | None):
|
||||
self._attr_hvac_mode = (
|
||||
None if data is None else HVACMode.HEAT_COOL if data else HVACMode.OFF
|
||||
None
|
||||
if data is None
|
||||
else HVACMode.HEAT_COOL
|
||||
if data == "On"
|
||||
else HVACMode.OFF
|
||||
)
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/thread",
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["python-otbr-api==2.7.0", "pyroute2==0.7.5"],
|
||||
"requirements": ["python-otbr-api==2.7.1", "pyroute2==0.7.5"],
|
||||
"single_config_entry": true,
|
||||
"zeroconf": ["_meshcop._udp.local."]
|
||||
}
|
||||
|
||||
@@ -250,6 +250,12 @@ class TibberDataAPICoordinator(DataUpdateCoordinator[dict[str, TibberDevice]]):
|
||||
async def _async_update_data(self) -> dict[str, TibberDevice]:
|
||||
"""Fetch the latest device capabilities from the Tibber Data API."""
|
||||
client = await self._async_get_client()
|
||||
devices: dict[str, TibberDevice] = await client.update_devices()
|
||||
try:
|
||||
devices: dict[str, TibberDevice] = await client.update_devices()
|
||||
except tibber.exceptions.RateLimitExceededError as err:
|
||||
raise UpdateFailed(
|
||||
f"Rate limit exceeded, retry after {err.retry_after} seconds",
|
||||
retry_after=err.retry_after,
|
||||
) from err
|
||||
self._build_sensor_lookup(devices)
|
||||
return devices
|
||||
|
||||
@@ -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.4"]
|
||||
}
|
||||
|
||||
@@ -338,8 +338,8 @@ class TractiveClient:
|
||||
# Handle both structures for compatibility
|
||||
data = event.get("content", event)
|
||||
|
||||
activity = data.get("activity", {})
|
||||
sleep = data.get("sleep", {})
|
||||
activity = data.get("activity") or {}
|
||||
sleep = data.get("sleep") or {}
|
||||
|
||||
payload = {
|
||||
ATTR_DAILY_GOAL: activity.get("minutesGoal"),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -74,6 +74,8 @@ class VeluxRainSensor(VeluxEntity, BinarySensorEntity):
|
||||
|
||||
self._attr_available = True
|
||||
|
||||
# Velux windows with rain sensors report an opening limitation of 93 or 100 (Velux GPU) when rain is detected.
|
||||
# So far, only 93 and 100 have been observed in practice, documentation on this is non-existent AFAIK.
|
||||
self._attr_is_on = limitation.min_value in {93, 100}
|
||||
# Velux windows with rain sensors report an opening limitation when rain is detected.
|
||||
# So far we've seen 89, 91, 93 (most cases) or 100 (Velux GPU). It probably makes sense to
|
||||
# assume that any large enough limitation (we use >=89) means rain is detected.
|
||||
# Documentation on this is non-existent AFAIK.
|
||||
self._attr_is_on = limitation.min_value >= 89
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"segment_speed": {
|
||||
"default": "mdi:speedometer"
|
||||
},
|
||||
"speed": {
|
||||
"default": "mdi:speedometer"
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"universal_silabs_flasher",
|
||||
"serialx"
|
||||
],
|
||||
"requirements": ["zha==0.0.82", "serialx==0.5.0"],
|
||||
"requirements": ["zha==0.0.84", "serialx==0.6.2"],
|
||||
"usb": [
|
||||
{
|
||||
"description": "*2652*",
|
||||
|
||||
@@ -654,6 +654,7 @@ DISCOVERY_SCHEMAS: list[NewZWaveDiscoverySchema] = [
|
||||
key=NOTIFICATION_SMOKE_ALARM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
not_states={
|
||||
0,
|
||||
SmokeAlarmNotificationEvent.SENSOR_STATUS_SMOKE_DETECTED_LOCATION_PROVIDED,
|
||||
SmokeAlarmNotificationEvent.SENSOR_STATUS_SMOKE_DETECTED,
|
||||
SmokeAlarmNotificationEvent.MAINTENANCE_STATUS_REPLACEMENT_REQUIRED,
|
||||
|
||||
@@ -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 = "1"
|
||||
__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)
|
||||
|
||||
@@ -683,7 +683,7 @@ NUMERICAL_ATTRIBUTE_CROSSED_THRESHOLD_SCHEMA = ENTITY_STATE_TRIGGER_SCHEMA.exten
|
||||
),
|
||||
vol.Optional(CONF_LOWER_LIMIT): _number_or_entity,
|
||||
vol.Optional(CONF_UPPER_LIMIT): _number_or_entity,
|
||||
vol.Required(CONF_THRESHOLD_TYPE): ThresholdType,
|
||||
vol.Required(CONF_THRESHOLD_TYPE): vol.Coerce(ThresholdType),
|
||||
},
|
||||
_validate_range(CONF_LOWER_LIMIT, CONF_UPPER_LIMIT),
|
||||
_validate_limits_for_threshold_type,
|
||||
|
||||
@@ -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.1
|
||||
home-assistant-intents==2026.1.6
|
||||
httpx==0.28.1
|
||||
ifaddr==0.2.0
|
||||
Jinja2==3.1.6
|
||||
@@ -56,7 +56,7 @@ PyJWT==2.10.1
|
||||
PyNaCl==1.6.0
|
||||
pyOpenSSL==25.3.0
|
||||
pyserial==3.5
|
||||
pysilero-vad==3.0.1
|
||||
pysilero-vad==3.2.0
|
||||
pyspeex-noise==1.0.2
|
||||
python-slugify==8.0.4
|
||||
PyTurboJPEG==1.8.0
|
||||
@@ -70,9 +70,9 @@ typing-extensions>=4.15.0,<5.0
|
||||
ulid-transform==1.5.2
|
||||
urllib3>=2.0
|
||||
uv==0.9.17
|
||||
voluptuous-openapi==0.3.0
|
||||
voluptuous-openapi==0.2.0
|
||||
voluptuous-serialize==2.7.0
|
||||
voluptuous==0.16.0
|
||||
voluptuous==0.15.2
|
||||
webrtc-models==0.3.0
|
||||
yarl==1.22.0
|
||||
zeroconf==0.148.0
|
||||
@@ -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
|
||||
|
||||
@@ -9,8 +9,6 @@ import logging
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
from packaging.requirements import Requirement
|
||||
|
||||
from .core import HomeAssistant, callback
|
||||
from .exceptions import HomeAssistantError
|
||||
from .helpers import singleton
|
||||
@@ -260,8 +258,13 @@ class RequirementsManager:
|
||||
"""
|
||||
if DEPRECATED_PACKAGES or self.hass.config.skip_pip_packages:
|
||||
all_requirements = {
|
||||
requirement_string: Requirement(requirement_string)
|
||||
requirement_string: requirement_details
|
||||
for requirement_string in requirements
|
||||
if (
|
||||
requirement_details := pkg_util.parse_requirement_safe(
|
||||
requirement_string
|
||||
)
|
||||
)
|
||||
}
|
||||
if DEPRECATED_PACKAGES:
|
||||
for requirement_string, requirement_details in all_requirements.items():
|
||||
@@ -272,9 +275,12 @@ class RequirementsManager:
|
||||
"" if is_built_in else "custom ",
|
||||
name,
|
||||
f"has requirement '{requirement_string}' which {reason}",
|
||||
f"This will stop working in Home Assistant {breaks_in_ha_version}, please"
|
||||
if breaks_in_ha_version
|
||||
else "Please",
|
||||
(
|
||||
"This will stop working in Home Assistant "
|
||||
f"{breaks_in_ha_version}, please"
|
||||
if breaks_in_ha_version
|
||||
else "Please"
|
||||
),
|
||||
async_suggest_report_issue(
|
||||
self.hass, integration_domain=name
|
||||
),
|
||||
|
||||
@@ -44,6 +44,39 @@ def get_installed_versions(specifiers: set[str]) -> set[str]:
|
||||
return {specifier for specifier in specifiers if is_installed(specifier)}
|
||||
|
||||
|
||||
def parse_requirement_safe(requirement_str: str) -> Requirement | None:
|
||||
"""Parse a requirement string into a Requirement object.
|
||||
|
||||
expected input is a pip compatible package specifier (requirement string)
|
||||
e.g. "package==1.0.0" or "package>=1.0.0,<2.0.0" or "package@git+https://..."
|
||||
|
||||
For backward compatibility, it also accepts a URL with a fragment
|
||||
e.g. "git+https://github.com/pypa/pip#pip>=1"
|
||||
|
||||
Returns None on a badly-formed requirement string.
|
||||
"""
|
||||
try:
|
||||
return Requirement(requirement_str)
|
||||
except InvalidRequirement:
|
||||
if "#" not in requirement_str:
|
||||
_LOGGER.error("Invalid requirement '%s'", requirement_str)
|
||||
return None
|
||||
|
||||
# This is likely a URL with a fragment
|
||||
# example: git+https://github.com/pypa/pip#pip>=1
|
||||
|
||||
# fragment support was originally used to install zip files, and
|
||||
# we no longer do this in Home Assistant. However, custom
|
||||
# components started using it to install packages from git
|
||||
# urls which would make it would be a breaking change to
|
||||
# remove it.
|
||||
try:
|
||||
return Requirement(urlparse(requirement_str).fragment)
|
||||
except InvalidRequirement:
|
||||
_LOGGER.error("Invalid requirement '%s'", requirement_str)
|
||||
return None
|
||||
|
||||
|
||||
def is_installed(requirement_str: str) -> bool:
|
||||
"""Check if a package is installed and will be loaded when we import it.
|
||||
|
||||
@@ -56,26 +89,8 @@ def is_installed(requirement_str: str) -> bool:
|
||||
Returns True when the requirement is met.
|
||||
Returns False when the package is not installed or doesn't meet req.
|
||||
"""
|
||||
try:
|
||||
req = Requirement(requirement_str)
|
||||
except InvalidRequirement:
|
||||
if "#" not in requirement_str:
|
||||
_LOGGER.error("Invalid requirement '%s'", requirement_str)
|
||||
return False
|
||||
|
||||
# This is likely a URL with a fragment
|
||||
# example: git+https://github.com/pypa/pip#pip>=1
|
||||
|
||||
# fragment support was originally used to install zip files, and
|
||||
# we no longer do this in Home Assistant. However, custom
|
||||
# components started using it to install packages from git
|
||||
# urls which would make it would be a breaking change to
|
||||
# remove it.
|
||||
try:
|
||||
req = Requirement(urlparse(requirement_str).fragment)
|
||||
except InvalidRequirement:
|
||||
_LOGGER.error("Invalid requirement '%s'", requirement_str)
|
||||
return False
|
||||
if (req := parse_requirement_safe(requirement_str)) is None:
|
||||
return False
|
||||
|
||||
try:
|
||||
if (installed_version := version(req.name)) is None:
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2026.1.0b4"
|
||||
version = "2026.1.1"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
@@ -76,9 +76,9 @@ dependencies = [
|
||||
"ulid-transform==1.5.2",
|
||||
"urllib3>=2.0",
|
||||
"uv==0.9.17",
|
||||
"voluptuous==0.16.0",
|
||||
"voluptuous==0.15.2",
|
||||
"voluptuous-serialize==2.7.0",
|
||||
"voluptuous-openapi==0.3.0",
|
||||
"voluptuous-openapi==0.2.0",
|
||||
"yarl==1.22.0",
|
||||
"webrtc-models==0.3.0",
|
||||
"zeroconf==0.148.0",
|
||||
|
||||
8
requirements.txt
generated
8
requirements.txt
generated
@@ -27,7 +27,7 @@ ha-ffmpeg==3.2.2
|
||||
hass-nabucasa==1.7.0
|
||||
hassil==3.5.0
|
||||
home-assistant-bluetooth==1.13.1
|
||||
home-assistant-intents==2026.1.1
|
||||
home-assistant-intents==2026.1.6
|
||||
httpx==0.28.1
|
||||
ifaddr==0.2.0
|
||||
Jinja2==3.1.6
|
||||
@@ -40,7 +40,7 @@ propcache==0.4.1
|
||||
psutil-home-assistant==0.0.1
|
||||
PyJWT==2.10.1
|
||||
pyOpenSSL==25.3.0
|
||||
pysilero-vad==3.0.1
|
||||
pysilero-vad==3.2.0
|
||||
pyspeex-noise==1.0.2
|
||||
python-slugify==8.0.4
|
||||
PyTurboJPEG==1.8.0
|
||||
@@ -54,9 +54,9 @@ typing-extensions>=4.15.0,<5.0
|
||||
ulid-transform==1.5.2
|
||||
urllib3>=2.0
|
||||
uv==0.9.17
|
||||
voluptuous-openapi==0.3.0
|
||||
voluptuous-openapi==0.2.0
|
||||
voluptuous-serialize==2.7.0
|
||||
voluptuous==0.16.0
|
||||
voluptuous==0.15.2
|
||||
webrtc-models==0.3.0
|
||||
yarl==1.22.0
|
||||
zeroconf==0.148.0
|
||||
|
||||
46
requirements_all.txt
generated
46
requirements_all.txt
generated
@@ -703,7 +703,7 @@ brottsplatskartan==1.0.5
|
||||
brunt==1.2.0
|
||||
|
||||
# homeassistant.components.bthome
|
||||
bthome-ble==3.17.0
|
||||
bthome-ble==3.16.0
|
||||
|
||||
# homeassistant.components.bt_home_hub_5
|
||||
bthomehub5-devicelist==0.1.1
|
||||
@@ -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
|
||||
@@ -854,7 +854,7 @@ ecoaliface==0.4.0
|
||||
egauge-async==0.4.0
|
||||
|
||||
# homeassistant.components.eheimdigital
|
||||
eheimdigital==1.4.0
|
||||
eheimdigital==1.5.0
|
||||
|
||||
# homeassistant.components.ekeybionyx
|
||||
ekey-bionyxpy==1.0.1
|
||||
@@ -1105,7 +1105,7 @@ google-nest-sdm==9.1.2
|
||||
google-photos-library-api==0.12.1
|
||||
|
||||
# homeassistant.components.google_air_quality
|
||||
google_air_quality_api==2.0.2
|
||||
google_air_quality_api==2.1.2
|
||||
|
||||
# homeassistant.components.slide
|
||||
# homeassistant.components.slide_local
|
||||
@@ -1127,7 +1127,7 @@ gpiozero==1.6.2
|
||||
gps3==0.33.3
|
||||
|
||||
# homeassistant.components.gree
|
||||
greeclimate==2.1.0
|
||||
greeclimate==2.1.1
|
||||
|
||||
# homeassistant.components.greeneye_monitor
|
||||
greeneye_monitor==3.0.3
|
||||
@@ -1213,10 +1213,10 @@ hole==0.9.0
|
||||
holidays==0.84
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20251229.1
|
||||
home-assistant-frontend==20260107.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
|
||||
@@ -1282,7 +1282,7 @@ imeon_inverter_api==0.4.0
|
||||
imgw_pib==1.6.0
|
||||
|
||||
# homeassistant.components.incomfort
|
||||
incomfort-client==0.6.10
|
||||
incomfort-client==0.6.11
|
||||
|
||||
# homeassistant.components.influxdb
|
||||
influxdb-client==1.48.0
|
||||
@@ -1684,7 +1684,7 @@ openwrt-luci-rpc==1.1.17
|
||||
openwrt-ubus-rpc==0.0.2
|
||||
|
||||
# homeassistant.components.opower
|
||||
opower==0.16.0
|
||||
opower==0.16.1
|
||||
|
||||
# homeassistant.components.oralb
|
||||
oralb-ble==1.0.2
|
||||
@@ -1855,7 +1855,7 @@ pyElectra==1.2.4
|
||||
pyEmby==1.10
|
||||
|
||||
# homeassistant.components.hikvision
|
||||
pyHik==0.3.4
|
||||
pyHik==0.4.0
|
||||
|
||||
# homeassistant.components.homee
|
||||
pyHomee==1.3.8
|
||||
@@ -1867,7 +1867,7 @@ pyRFXtrx==0.31.1
|
||||
pySDCP==1
|
||||
|
||||
# homeassistant.components.tibber
|
||||
pyTibber==0.34.0
|
||||
pyTibber==0.34.4
|
||||
|
||||
# homeassistant.components.dlink
|
||||
pyW215==0.8.0
|
||||
@@ -2238,7 +2238,7 @@ pynina==0.3.6
|
||||
pynintendoauth==1.0.2
|
||||
|
||||
# homeassistant.components.nintendo_parental_controls
|
||||
pynintendoparental==2.3.0
|
||||
pynintendoparental==2.3.2
|
||||
|
||||
# homeassistant.components.nobo_hub
|
||||
pynobo==1.8.1
|
||||
@@ -2291,7 +2291,7 @@ pyotgw==2.2.2
|
||||
pyotp==2.9.0
|
||||
|
||||
# homeassistant.components.overkiz
|
||||
pyoverkiz==1.19.3
|
||||
pyoverkiz==1.19.4
|
||||
|
||||
# homeassistant.components.palazzetti
|
||||
pypalazzetti==0.1.20
|
||||
@@ -2409,13 +2409,13 @@ pysiaalarm==3.1.1
|
||||
pysignalclirestapi==0.3.24
|
||||
|
||||
# homeassistant.components.assist_pipeline
|
||||
pysilero-vad==3.0.1
|
||||
pysilero-vad==3.2.0
|
||||
|
||||
# homeassistant.components.sky_hub
|
||||
pyskyqhub==0.1.4
|
||||
|
||||
# homeassistant.components.sma
|
||||
pysma==1.0.2
|
||||
pysma==1.1.0
|
||||
|
||||
# homeassistant.components.smappee
|
||||
pysmappee==0.2.29
|
||||
@@ -2520,7 +2520,7 @@ python-google-weather-api==0.0.4
|
||||
python-homeassistant-analytics==0.9.0
|
||||
|
||||
# homeassistant.components.homewizard
|
||||
python-homewizard-energy==10.0.0
|
||||
python-homewizard-energy==10.0.1
|
||||
|
||||
# homeassistant.components.hp_ilo
|
||||
python-hpilo==4.4.3
|
||||
@@ -2563,7 +2563,7 @@ python-opensky==1.0.1
|
||||
|
||||
# homeassistant.components.otbr
|
||||
# homeassistant.components.thread
|
||||
python-otbr-api==2.7.0
|
||||
python-otbr-api==2.7.1
|
||||
|
||||
# homeassistant.components.overseerr
|
||||
python-overseerr==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
|
||||
@@ -2593,7 +2593,7 @@ python-snoo==0.8.3
|
||||
python-songpal==0.16.2
|
||||
|
||||
# homeassistant.components.tado
|
||||
python-tado==0.18.15
|
||||
python-tado==0.18.16
|
||||
|
||||
# homeassistant.components.technove
|
||||
python-technove==2.0.0
|
||||
@@ -2842,7 +2842,7 @@ sentry-sdk==1.45.1
|
||||
|
||||
# homeassistant.components.homeassistant_hardware
|
||||
# homeassistant.components.zha
|
||||
serialx==0.5.0
|
||||
serialx==0.6.2
|
||||
|
||||
# homeassistant.components.sfr_box
|
||||
sfrbox-api==0.1.0
|
||||
@@ -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.84
|
||||
|
||||
# homeassistant.components.zhong_hong
|
||||
zhong-hong-hvac==1.0.13
|
||||
|
||||
46
requirements_test_all.txt
generated
46
requirements_test_all.txt
generated
@@ -633,7 +633,7 @@ brottsplatskartan==1.0.5
|
||||
brunt==1.2.0
|
||||
|
||||
# homeassistant.components.bthome
|
||||
bthome-ble==3.17.0
|
||||
bthome-ble==3.16.0
|
||||
|
||||
# homeassistant.components.buienradar
|
||||
buienradar==1.0.6
|
||||
@@ -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
|
||||
@@ -754,7 +754,7 @@ easyenergy==2.1.2
|
||||
egauge-async==0.4.0
|
||||
|
||||
# homeassistant.components.eheimdigital
|
||||
eheimdigital==1.4.0
|
||||
eheimdigital==1.5.0
|
||||
|
||||
# homeassistant.components.ekeybionyx
|
||||
ekey-bionyxpy==1.0.1
|
||||
@@ -981,7 +981,7 @@ google-nest-sdm==9.1.2
|
||||
google-photos-library-api==0.12.1
|
||||
|
||||
# homeassistant.components.google_air_quality
|
||||
google_air_quality_api==2.0.2
|
||||
google_air_quality_api==2.1.2
|
||||
|
||||
# homeassistant.components.slide
|
||||
# homeassistant.components.slide_local
|
||||
@@ -1000,7 +1000,7 @@ govee-local-api==2.3.0
|
||||
gps3==0.33.3
|
||||
|
||||
# homeassistant.components.gree
|
||||
greeclimate==2.1.0
|
||||
greeclimate==2.1.1
|
||||
|
||||
# homeassistant.components.greeneye_monitor
|
||||
greeneye_monitor==3.0.3
|
||||
@@ -1071,10 +1071,10 @@ hole==0.9.0
|
||||
holidays==0.84
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20251229.1
|
||||
home-assistant-frontend==20260107.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
|
||||
@@ -1128,7 +1128,7 @@ imeon_inverter_api==0.4.0
|
||||
imgw_pib==1.6.0
|
||||
|
||||
# homeassistant.components.incomfort
|
||||
incomfort-client==0.6.10
|
||||
incomfort-client==0.6.11
|
||||
|
||||
# homeassistant.components.influxdb
|
||||
influxdb-client==1.48.0
|
||||
@@ -1455,7 +1455,7 @@ openrgb-python==0.3.6
|
||||
openwebifpy==4.3.1
|
||||
|
||||
# homeassistant.components.opower
|
||||
opower==0.16.0
|
||||
opower==0.16.1
|
||||
|
||||
# homeassistant.components.oralb
|
||||
oralb-ble==1.0.2
|
||||
@@ -1586,7 +1586,7 @@ pyDuotecno==2024.10.1
|
||||
pyElectra==1.2.4
|
||||
|
||||
# homeassistant.components.hikvision
|
||||
pyHik==0.3.4
|
||||
pyHik==0.4.0
|
||||
|
||||
# homeassistant.components.homee
|
||||
pyHomee==1.3.8
|
||||
@@ -1595,7 +1595,7 @@ pyHomee==1.3.8
|
||||
pyRFXtrx==0.31.1
|
||||
|
||||
# homeassistant.components.tibber
|
||||
pyTibber==0.34.0
|
||||
pyTibber==0.34.4
|
||||
|
||||
# homeassistant.components.dlink
|
||||
pyW215==0.8.0
|
||||
@@ -1888,7 +1888,7 @@ pynina==0.3.6
|
||||
pynintendoauth==1.0.2
|
||||
|
||||
# homeassistant.components.nintendo_parental_controls
|
||||
pynintendoparental==2.3.0
|
||||
pynintendoparental==2.3.2
|
||||
|
||||
# homeassistant.components.nobo_hub
|
||||
pynobo==1.8.1
|
||||
@@ -1935,7 +1935,7 @@ pyotgw==2.2.2
|
||||
pyotp==2.9.0
|
||||
|
||||
# homeassistant.components.overkiz
|
||||
pyoverkiz==1.19.3
|
||||
pyoverkiz==1.19.4
|
||||
|
||||
# homeassistant.components.palazzetti
|
||||
pypalazzetti==0.1.20
|
||||
@@ -2032,10 +2032,10 @@ pysiaalarm==3.1.1
|
||||
pysignalclirestapi==0.3.24
|
||||
|
||||
# homeassistant.components.assist_pipeline
|
||||
pysilero-vad==3.0.1
|
||||
pysilero-vad==3.2.0
|
||||
|
||||
# homeassistant.components.sma
|
||||
pysma==1.0.2
|
||||
pysma==1.1.0
|
||||
|
||||
# homeassistant.components.smappee
|
||||
pysmappee==0.2.29
|
||||
@@ -2113,7 +2113,7 @@ python-google-weather-api==0.0.4
|
||||
python-homeassistant-analytics==0.9.0
|
||||
|
||||
# homeassistant.components.homewizard
|
||||
python-homewizard-energy==10.0.0
|
||||
python-homewizard-energy==10.0.1
|
||||
|
||||
# homeassistant.components.izone
|
||||
python-izone==1.2.9
|
||||
@@ -2150,7 +2150,7 @@ python-opensky==1.0.1
|
||||
|
||||
# homeassistant.components.otbr
|
||||
# homeassistant.components.thread
|
||||
python-otbr-api==2.7.0
|
||||
python-otbr-api==2.7.1
|
||||
|
||||
# homeassistant.components.overseerr
|
||||
python-overseerr==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
|
||||
@@ -2177,7 +2177,7 @@ python-snoo==0.8.3
|
||||
python-songpal==0.16.2
|
||||
|
||||
# homeassistant.components.tado
|
||||
python-tado==0.18.15
|
||||
python-tado==0.18.16
|
||||
|
||||
# homeassistant.components.technove
|
||||
python-technove==2.0.0
|
||||
@@ -2381,7 +2381,7 @@ sentry-sdk==1.45.1
|
||||
|
||||
# homeassistant.components.homeassistant_hardware
|
||||
# homeassistant.components.zha
|
||||
serialx==0.5.0
|
||||
serialx==0.6.2
|
||||
|
||||
# homeassistant.components.sfr_box
|
||||
sfrbox-api==0.1.0
|
||||
@@ -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.84
|
||||
|
||||
# homeassistant.components.zwave_js
|
||||
zwave-js-server-python==0.67.1
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -309,12 +309,12 @@
|
||||
'type': 'text',
|
||||
}),
|
||||
dict({
|
||||
'content': '{"success": true, "response": "Lights are off."}',
|
||||
'content': '{"success":true,"response":"Lights are off."}',
|
||||
'tool_use_id': 'mock-tool-call-id',
|
||||
'type': 'tool_result',
|
||||
}),
|
||||
dict({
|
||||
'content': '{"success": false, "response": "Not enough milk."}',
|
||||
'content': '{"success":false,"response":"Not enough milk."}',
|
||||
'tool_use_id': 'mock-tool-call-id-2',
|
||||
'type': 'tool_result',
|
||||
}),
|
||||
@@ -462,6 +462,62 @@
|
||||
}),
|
||||
])
|
||||
# ---
|
||||
# name: test_history_conversion[content6]
|
||||
list([
|
||||
dict({
|
||||
'content': 'What time is it?',
|
||||
'role': 'user',
|
||||
}),
|
||||
dict({
|
||||
'content': list([
|
||||
dict({
|
||||
'text': 'Let me check the time for you.',
|
||||
'type': 'text',
|
||||
}),
|
||||
dict({
|
||||
'id': 'mock-tool-call-id',
|
||||
'input': dict({
|
||||
}),
|
||||
'name': 'GetCurrentTime',
|
||||
'type': 'tool_use',
|
||||
}),
|
||||
]),
|
||||
'role': 'assistant',
|
||||
}),
|
||||
dict({
|
||||
'content': list([
|
||||
dict({
|
||||
'content': '{"speech_slots":{"time":"14:30:00"},"message":"Current time retrieved"}',
|
||||
'tool_use_id': 'mock-tool-call-id',
|
||||
'type': 'tool_result',
|
||||
}),
|
||||
]),
|
||||
'role': 'user',
|
||||
}),
|
||||
dict({
|
||||
'content': list([
|
||||
dict({
|
||||
'text': 'It is currently 2:30 PM.',
|
||||
'type': 'text',
|
||||
}),
|
||||
]),
|
||||
'role': 'assistant',
|
||||
}),
|
||||
dict({
|
||||
'content': 'Are you sure?',
|
||||
'role': 'user',
|
||||
}),
|
||||
dict({
|
||||
'content': list([
|
||||
dict({
|
||||
'text': 'Yes, I am sure!',
|
||||
'type': 'text',
|
||||
}),
|
||||
]),
|
||||
'role': 'assistant',
|
||||
}),
|
||||
])
|
||||
# ---
|
||||
# name: test_redacted_thinking
|
||||
list([
|
||||
dict({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Tests for the Anthropic integration."""
|
||||
|
||||
import datetime
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
@@ -317,7 +318,7 @@ async def test_function_exception(
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"content": '{"error": "HomeAssistantError", "error_text": "Test tool exception"}',
|
||||
"content": '{"error":"HomeAssistantError","error_text":"Test tool exception"}',
|
||||
"tool_use_id": "toolu_0123456789AbCdEfGhIjKlM",
|
||||
"type": "tool_result",
|
||||
}
|
||||
@@ -893,6 +894,34 @@ async def test_web_search(
|
||||
),
|
||||
),
|
||||
],
|
||||
[
|
||||
conversation.chat_log.SystemContent("You are a helpful assistant."),
|
||||
conversation.chat_log.UserContent("What time is it?"),
|
||||
conversation.chat_log.AssistantContent(
|
||||
agent_id="conversation.claude_conversation",
|
||||
content="Let me check the time for you.",
|
||||
tool_calls=[
|
||||
llm.ToolInput(
|
||||
id="mock-tool-call-id",
|
||||
tool_name="GetCurrentTime",
|
||||
tool_args={},
|
||||
),
|
||||
],
|
||||
),
|
||||
conversation.chat_log.ToolResultContent(
|
||||
agent_id="conversation.claude_conversation",
|
||||
tool_call_id="mock-tool-call-id",
|
||||
tool_name="GetCurrentTime",
|
||||
tool_result={
|
||||
"speech_slots": {"time": datetime.time(14, 30, 0)},
|
||||
"message": "Current time retrieved",
|
||||
},
|
||||
),
|
||||
conversation.chat_log.AssistantContent(
|
||||
agent_id="conversation.claude_conversation",
|
||||
content="It is currently 2:30 PM.",
|
||||
),
|
||||
],
|
||||
],
|
||||
)
|
||||
async def test_history_conversion(
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Backblaze B2 backup agent tests."""
|
||||
|
||||
import asyncio
|
||||
from collections.abc import AsyncGenerator
|
||||
from io import StringIO
|
||||
import json
|
||||
@@ -863,3 +864,94 @@ async def test_metadata_downloads_are_sequential(
|
||||
assert response["success"]
|
||||
# Verify downloads were sequential (max 1 at a time)
|
||||
assert max_concurrent == 1
|
||||
|
||||
|
||||
async def test_upload_timeout(
|
||||
hass_client: ClientSessionGenerator,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test upload timeout handling."""
|
||||
client = await hass_client()
|
||||
|
||||
mock_file_info = Mock()
|
||||
mock_file_info.delete = Mock()
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.backup.manager.BackupManager.async_get_backup",
|
||||
return_value=TEST_BACKUP,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.backup.manager.read_backup",
|
||||
return_value=TEST_BACKUP,
|
||||
),
|
||||
patch("pathlib.Path.open") as mocked_open,
|
||||
patch(
|
||||
"homeassistant.components.backblaze_b2.backup.BackblazeBackupAgent._upload_unbound_stream_sync",
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.backblaze_b2.backup.asyncio.wait_for",
|
||||
side_effect=TimeoutError,
|
||||
),
|
||||
patch.object(
|
||||
BucketSimulator,
|
||||
"get_file_info_by_name",
|
||||
return_value=mock_file_info,
|
||||
),
|
||||
caplog.at_level(logging.ERROR),
|
||||
):
|
||||
mocked_open.return_value.read = Mock(side_effect=[b"test", b""])
|
||||
resp = await client.post(
|
||||
f"/api/backup/upload?agent_id={DOMAIN}.{mock_config_entry.entry_id}",
|
||||
data={"file": StringIO("test")},
|
||||
)
|
||||
|
||||
assert resp.status == 201
|
||||
assert any("timed out" in msg for msg in caplog.messages)
|
||||
|
||||
|
||||
async def test_upload_cancelled(
|
||||
hass_client: ClientSessionGenerator,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test upload cancellation handling."""
|
||||
client = await hass_client()
|
||||
|
||||
mock_file_info = Mock()
|
||||
mock_file_info.delete = Mock()
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.backup.manager.BackupManager.async_get_backup",
|
||||
return_value=TEST_BACKUP,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.backup.manager.read_backup",
|
||||
return_value=TEST_BACKUP,
|
||||
),
|
||||
patch("pathlib.Path.open") as mocked_open,
|
||||
patch(
|
||||
"homeassistant.components.backblaze_b2.backup.BackblazeBackupAgent._upload_unbound_stream_sync",
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.backblaze_b2.backup.asyncio.wait_for",
|
||||
side_effect=asyncio.CancelledError,
|
||||
),
|
||||
patch.object(
|
||||
BucketSimulator,
|
||||
"get_file_info_by_name",
|
||||
return_value=mock_file_info,
|
||||
),
|
||||
caplog.at_level(logging.WARNING),
|
||||
):
|
||||
mocked_open.return_value.read = Mock(side_effect=[b"test", b""])
|
||||
resp = await client.post(
|
||||
f"/api/backup/upload?agent_id={DOMAIN}.{mock_config_entry.entry_id}",
|
||||
data={"file": StringIO("test")},
|
||||
)
|
||||
|
||||
# CancelledError propagates up and causes a 500 error
|
||||
assert resp.status == 500
|
||||
assert any("cancelled" in msg for msg in caplog.messages)
|
||||
|
||||
@@ -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 {}
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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]),
|
||||
),
|
||||
|
||||
@@ -9432,7 +9432,7 @@
|
||||
'suggested_display_precision': 0,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': 'energy',
|
||||
'original_device_class': 'energy_storage',
|
||||
'original_icon': None,
|
||||
'original_name': 'Available battery energy',
|
||||
'platform': 'enphase_envoy',
|
||||
@@ -9445,7 +9445,7 @@
|
||||
}),
|
||||
'state': dict({
|
||||
'attributes': dict({
|
||||
'device_class': 'energy',
|
||||
'device_class': 'energy_storage',
|
||||
'friendly_name': 'Envoy <<envoyserial>> Available battery energy',
|
||||
'state_class': 'measurement',
|
||||
'unit_of_measurement': 'Wh',
|
||||
@@ -9482,7 +9482,7 @@
|
||||
'suggested_display_precision': 0,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': 'energy',
|
||||
'original_device_class': 'energy_storage',
|
||||
'original_icon': None,
|
||||
'original_name': 'Reserve battery energy',
|
||||
'platform': 'enphase_envoy',
|
||||
@@ -9495,7 +9495,7 @@
|
||||
}),
|
||||
'state': dict({
|
||||
'attributes': dict({
|
||||
'device_class': 'energy',
|
||||
'device_class': 'energy_storage',
|
||||
'friendly_name': 'Envoy <<envoyserial>> Reserve battery energy',
|
||||
'state_class': 'measurement',
|
||||
'unit_of_measurement': 'Wh',
|
||||
@@ -9530,7 +9530,7 @@
|
||||
'suggested_display_precision': 0,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': 'energy',
|
||||
'original_device_class': 'energy_storage',
|
||||
'original_icon': None,
|
||||
'original_name': 'Battery capacity',
|
||||
'platform': 'enphase_envoy',
|
||||
@@ -9543,7 +9543,7 @@
|
||||
}),
|
||||
'state': dict({
|
||||
'attributes': dict({
|
||||
'device_class': 'energy',
|
||||
'device_class': 'energy_storage',
|
||||
'friendly_name': 'Envoy <<envoyserial>> Battery capacity',
|
||||
'unit_of_measurement': 'Wh',
|
||||
}),
|
||||
|
||||
@@ -3802,7 +3802,7 @@
|
||||
'suggested_display_precision': 0,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
|
||||
'original_device_class': <SensorDeviceClass.ENERGY_STORAGE: 'energy_storage'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Available battery energy',
|
||||
'platform': 'enphase_envoy',
|
||||
@@ -3817,7 +3817,7 @@
|
||||
# name: test_sensor[envoy_acb_batt][sensor.envoy_1234_available_battery_energy-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'energy',
|
||||
'device_class': 'energy_storage',
|
||||
'friendly_name': 'Envoy 1234 Available battery energy',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfEnergy.WATT_HOUR: 'Wh'>,
|
||||
@@ -3968,7 +3968,7 @@
|
||||
'suggested_display_precision': 0,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
|
||||
'original_device_class': <SensorDeviceClass.ENERGY_STORAGE: 'energy_storage'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Battery capacity',
|
||||
'platform': 'enphase_envoy',
|
||||
@@ -3983,7 +3983,7 @@
|
||||
# name: test_sensor[envoy_acb_batt][sensor.envoy_1234_battery_capacity-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'energy',
|
||||
'device_class': 'energy_storage',
|
||||
'friendly_name': 'Envoy 1234 Battery capacity',
|
||||
'unit_of_measurement': <UnitOfEnergy.WATT_HOUR: 'Wh'>,
|
||||
}),
|
||||
@@ -7480,7 +7480,7 @@
|
||||
'suggested_display_precision': 0,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
|
||||
'original_device_class': <SensorDeviceClass.ENERGY_STORAGE: 'energy_storage'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Reserve battery energy',
|
||||
'platform': 'enphase_envoy',
|
||||
@@ -7495,7 +7495,7 @@
|
||||
# name: test_sensor[envoy_acb_batt][sensor.envoy_1234_reserve_battery_energy-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'energy',
|
||||
'device_class': 'energy_storage',
|
||||
'friendly_name': 'Envoy 1234 Reserve battery energy',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfEnergy.WATT_HOUR: 'Wh'>,
|
||||
@@ -9055,7 +9055,7 @@
|
||||
'suggested_display_precision': 0,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
|
||||
'original_device_class': <SensorDeviceClass.ENERGY_STORAGE: 'energy_storage'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Available battery energy',
|
||||
'platform': 'enphase_envoy',
|
||||
@@ -9070,7 +9070,7 @@
|
||||
# name: test_sensor[envoy_eu_batt][sensor.envoy_1234_available_battery_energy-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'energy',
|
||||
'device_class': 'energy_storage',
|
||||
'friendly_name': 'Envoy 1234 Available battery energy',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfEnergy.WATT_HOUR: 'Wh'>,
|
||||
@@ -9221,7 +9221,7 @@
|
||||
'suggested_display_precision': 0,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
|
||||
'original_device_class': <SensorDeviceClass.ENERGY_STORAGE: 'energy_storage'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Battery capacity',
|
||||
'platform': 'enphase_envoy',
|
||||
@@ -9236,7 +9236,7 @@
|
||||
# name: test_sensor[envoy_eu_batt][sensor.envoy_1234_battery_capacity-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'energy',
|
||||
'device_class': 'energy_storage',
|
||||
'friendly_name': 'Envoy 1234 Battery capacity',
|
||||
'unit_of_measurement': <UnitOfEnergy.WATT_HOUR: 'Wh'>,
|
||||
}),
|
||||
@@ -12733,7 +12733,7 @@
|
||||
'suggested_display_precision': 0,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
|
||||
'original_device_class': <SensorDeviceClass.ENERGY_STORAGE: 'energy_storage'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Reserve battery energy',
|
||||
'platform': 'enphase_envoy',
|
||||
@@ -12748,7 +12748,7 @@
|
||||
# name: test_sensor[envoy_eu_batt][sensor.envoy_1234_reserve_battery_energy-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'energy',
|
||||
'device_class': 'energy_storage',
|
||||
'friendly_name': 'Envoy 1234 Reserve battery energy',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfEnergy.WATT_HOUR: 'Wh'>,
|
||||
@@ -14711,7 +14711,7 @@
|
||||
'suggested_display_precision': 0,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
|
||||
'original_device_class': <SensorDeviceClass.ENERGY_STORAGE: 'energy_storage'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Available battery energy',
|
||||
'platform': 'enphase_envoy',
|
||||
@@ -14726,7 +14726,7 @@
|
||||
# name: test_sensor[envoy_metered_batt_relay][sensor.envoy_1234_available_battery_energy-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'energy',
|
||||
'device_class': 'energy_storage',
|
||||
'friendly_name': 'Envoy 1234 Available battery energy',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfEnergy.WATT_HOUR: 'Wh'>,
|
||||
@@ -15054,7 +15054,7 @@
|
||||
'suggested_display_precision': 0,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
|
||||
'original_device_class': <SensorDeviceClass.ENERGY_STORAGE: 'energy_storage'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Battery capacity',
|
||||
'platform': 'enphase_envoy',
|
||||
@@ -15069,7 +15069,7 @@
|
||||
# name: test_sensor[envoy_metered_batt_relay][sensor.envoy_1234_battery_capacity-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'energy',
|
||||
'device_class': 'energy_storage',
|
||||
'friendly_name': 'Envoy 1234 Battery capacity',
|
||||
'unit_of_measurement': <UnitOfEnergy.WATT_HOUR: 'Wh'>,
|
||||
}),
|
||||
@@ -21725,7 +21725,7 @@
|
||||
'suggested_display_precision': 0,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
|
||||
'original_device_class': <SensorDeviceClass.ENERGY_STORAGE: 'energy_storage'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Reserve battery energy',
|
||||
'platform': 'enphase_envoy',
|
||||
@@ -21740,7 +21740,7 @@
|
||||
# name: test_sensor[envoy_metered_batt_relay][sensor.envoy_1234_reserve_battery_energy-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'energy',
|
||||
'device_class': 'energy_storage',
|
||||
'friendly_name': 'Envoy 1234 Reserve battery energy',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfEnergy.WATT_HOUR: 'Wh'>,
|
||||
|
||||
@@ -1264,3 +1264,35 @@ async def test_fw_update(
|
||||
|
||||
assert "firmware changed from: " in caplog.text
|
||||
assert "to: 0.0.0, reloading enphase envoy integration" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("mock_envoy"),
|
||||
[
|
||||
"envoy",
|
||||
"envoy_1p_metered",
|
||||
"envoy_eu_batt",
|
||||
"envoy_metered_batt_relay",
|
||||
"envoy_nobatt_metered_3p",
|
||||
"envoy_tot_cons_metered",
|
||||
"envoy_acb_batt",
|
||||
],
|
||||
indirect=["mock_envoy"],
|
||||
)
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_no_state_class_warnings(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_envoy: AsyncMock,
|
||||
entity_registry: er.EntityRegistry,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test enphase_envoy sensor creation does not result in deviceclass/state_class warnings."""
|
||||
logging.getLogger("homeassistant.components.enphase_envoy").setLevel(logging.DEBUG)
|
||||
with patch("homeassistant.components.enphase_envoy.PLATFORMS", [Platform.SENSOR]):
|
||||
await setup_integration(hass, config_entry)
|
||||
|
||||
# Simple test to verify no sensor device class / state class mismatch warning is reported
|
||||
#
|
||||
# assert "which is impossible considering" not in caplog.text
|
||||
assert "create a bug report at" not in caplog.text
|
||||
|
||||
@@ -281,7 +281,7 @@
|
||||
'attribution': 'Data provided by Fitbit.com',
|
||||
'device_class': 'duration',
|
||||
'friendly_name': 'First L. Sleep time in bed',
|
||||
'icon': 'mdi:hotel',
|
||||
'icon': 'mdi:bed',
|
||||
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
|
||||
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
|
||||
}),
|
||||
|
||||
@@ -6,9 +6,11 @@ from datetime import timedelta
|
||||
from unittest.mock import Mock
|
||||
|
||||
from pyfritzhome import LoginError
|
||||
import pytest
|
||||
from requests.exceptions import ConnectionError, HTTPError
|
||||
|
||||
from homeassistant.components.fritzbox.const import DOMAIN
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import CONF_DEVICES
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -20,6 +22,8 @@ from . import (
|
||||
FritzDeviceSensorMock,
|
||||
FritzDeviceSwitchMock,
|
||||
FritzEntityBaseMock,
|
||||
FritzTriggerMock,
|
||||
setup_config_entry,
|
||||
)
|
||||
from .const import MOCK_CONFIG
|
||||
|
||||
@@ -184,3 +188,27 @@ async def test_coordinator_workaround_sub_units_without_main_device(
|
||||
assert len(device_entries) == 2
|
||||
assert device_entries[0].identifiers == {(DOMAIN, "good_device")}
|
||||
assert device_entries[1].identifiers == {(DOMAIN, "bad_device")}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "side_effect", "switch_entity_count"),
|
||||
[
|
||||
(None, None, 0),
|
||||
(None, HTTPError(), 0),
|
||||
(FritzTriggerMock(), None, 1),
|
||||
],
|
||||
)
|
||||
async def test_coordinator_has_triggers(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
fritz: Mock,
|
||||
trigger: Mock | None,
|
||||
side_effect: Exception | None,
|
||||
switch_entity_count: int,
|
||||
) -> None:
|
||||
"""Test coordinator has_triggers property."""
|
||||
fritz().has_triggers.side_effect = side_effect
|
||||
assert await setup_config_entry(
|
||||
hass, MOCK_CONFIG[DOMAIN][CONF_DEVICES][0], fritz=fritz, trigger=trigger
|
||||
)
|
||||
assert len(hass.states.async_all(SWITCH_DOMAIN)) == switch_entity_count
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
491
tests/components/roborock/snapshots/test_binary_sensor.ambr
Normal file
491
tests/components/roborock/snapshots/test_binary_sensor.ambr
Normal 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
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
]
|
||||
|
||||
@@ -321,7 +321,7 @@ async def test_select_streaming(
|
||||
"vin": VEHICLE_DATA_ALT["response"]["vin"],
|
||||
"data": {
|
||||
Signal.INSIDE_TEMP: 26,
|
||||
Signal.HVAC_AC_ENABLED: True,
|
||||
Signal.HVAC_POWER: "HvacPowerStateOn",
|
||||
Signal.CLIMATE_KEEPER_MODE: "ClimateKeeperModeOn",
|
||||
Signal.RIGHT_HAND_DRIVE: True,
|
||||
Signal.HVAC_LEFT_TEMPERATURE_REQUEST: 22,
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
"""Test init of Tractive integration."""
|
||||
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from aiotractive.exceptions import TractiveError, UnauthorizedError
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.tractive.const import DOMAIN
|
||||
from homeassistant.components.tractive.const import (
|
||||
ATTR_DAILY_GOAL,
|
||||
ATTR_MINUTES_ACTIVE,
|
||||
ATTR_MINUTES_DAY_SLEEP,
|
||||
ATTR_MINUTES_NIGHT_SLEEP,
|
||||
ATTR_MINUTES_REST,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, STATE_UNAVAILABLE
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -161,3 +169,50 @@ async def test_server_unavailable(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get(entity_id).state != STATE_UNAVAILABLE
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("sleep_data"), [None, {}, {"unexpected": 123}])
|
||||
async def test_missing_sleep_data(
|
||||
hass: HomeAssistant,
|
||||
mock_tractive_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
sleep_data: dict[str, Any] | None,
|
||||
) -> None:
|
||||
"""Test for missing sleep data."""
|
||||
event = {"petId": "pet_id_123", "sleep": sleep_data}
|
||||
|
||||
await init_integration(hass, mock_config_entry)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.tractive.async_dispatcher_send"
|
||||
) as async_dispatcher_send_mock:
|
||||
mock_tractive_client.send_health_overview_event(mock_config_entry, event)
|
||||
|
||||
assert async_dispatcher_send_mock.call_count == 1
|
||||
payload = async_dispatcher_send_mock.mock_calls[0][1][2]
|
||||
assert payload[ATTR_MINUTES_DAY_SLEEP] is None
|
||||
assert payload[ATTR_MINUTES_NIGHT_SLEEP] is None
|
||||
assert payload[ATTR_MINUTES_REST] is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("activity_data"), [None, {}, {"unexpected": 123}])
|
||||
async def test_missing_activity_data(
|
||||
hass: HomeAssistant,
|
||||
mock_tractive_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
activity_data: dict[str, Any] | None,
|
||||
) -> None:
|
||||
"""Test for missing activity data."""
|
||||
event = {"petId": "pet_id_123", "activity": activity_data}
|
||||
|
||||
await init_integration(hass, mock_config_entry)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.tractive.async_dispatcher_send"
|
||||
) as async_dispatcher_send_mock:
|
||||
mock_tractive_client.send_health_overview_event(mock_config_entry, event)
|
||||
|
||||
assert async_dispatcher_send_mock.call_count == 1
|
||||
payload = async_dispatcher_send_mock.mock_calls[0][1][2]
|
||||
assert payload[ATTR_DAILY_GOAL] is None
|
||||
assert payload[ATTR_MINUTES_ACTIVE] is None
|
||||
|
||||
@@ -49,15 +49,29 @@ async def test_rain_sensor_state(
|
||||
assert state is not None
|
||||
assert state.state == STATE_ON
|
||||
|
||||
# simulate rain detected (other Velux models report 93)
|
||||
# simulate rain detected (most Velux models report 93)
|
||||
mock_window.get_limitation.return_value.min_value = 93
|
||||
await update_polled_entities(hass, freezer)
|
||||
state = hass.states.get(test_entity_id)
|
||||
assert state is not None
|
||||
assert state.state == STATE_ON
|
||||
|
||||
# simulate rain detected (other Velux models report 89)
|
||||
mock_window.get_limitation.return_value.min_value = 89
|
||||
await update_polled_entities(hass, freezer)
|
||||
state = hass.states.get(test_entity_id)
|
||||
assert state is not None
|
||||
assert state.state == STATE_ON
|
||||
|
||||
# simulate other limits which do not indicate rain detected
|
||||
mock_window.get_limitation.return_value.min_value = 88
|
||||
await update_polled_entities(hass, freezer)
|
||||
state = hass.states.get(test_entity_id)
|
||||
assert state is not None
|
||||
assert state.state == STATE_OFF
|
||||
|
||||
# simulate no rain detected again
|
||||
mock_window.get_limitation.return_value.min_value = 95
|
||||
mock_window.get_limitation.return_value.min_value = 0
|
||||
await update_polled_entities(hass, freezer)
|
||||
state = hass.states.get(test_entity_id)
|
||||
assert state is not None
|
||||
@@ -144,7 +158,7 @@ async def test_rain_sensor_unavailability(
|
||||
|
||||
# Simulate recovery
|
||||
mock_window.get_limitation.side_effect = None
|
||||
mock_window.get_limitation.return_value.min_value = 95
|
||||
mock_window.get_limitation.return_value.min_value = 0
|
||||
await update_polled_entities(hass, freezer)
|
||||
|
||||
# Entity should be available again
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -383,6 +383,13 @@ async def test_smoke_co_notification_sensors(
|
||||
assert entity_entry
|
||||
assert entity_entry.entity_category == EntityCategory.DIAGNOSTIC
|
||||
|
||||
# Test that no idle states are created as entities
|
||||
entity_id = "binary_sensor.zcombo_g_smoke_co_alarm_idle"
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is None
|
||||
entity_entry = entity_registry.async_get(entity_id)
|
||||
assert entity_entry is None
|
||||
|
||||
# Test state updates for smoke alarm
|
||||
event = Event(
|
||||
type="value updated",
|
||||
|
||||
@@ -40,7 +40,6 @@ from homeassistant.helpers.trigger import (
|
||||
CONF_UPPER_LIMIT,
|
||||
DATA_PLUGGABLE_ACTIONS,
|
||||
PluggableAction,
|
||||
ThresholdType,
|
||||
Trigger,
|
||||
TriggerActionRunner,
|
||||
_async_get_trigger_platform,
|
||||
@@ -1387,25 +1386,26 @@ async def test_numerical_state_attribute_changed_error_handling(
|
||||
("trigger_options", "expected_result"),
|
||||
[
|
||||
# Valid configurations
|
||||
# Don't use the enum in tests to allow testing validation of strings when the source is JSON or YAML
|
||||
(
|
||||
{CONF_THRESHOLD_TYPE: ThresholdType.ABOVE, CONF_LOWER_LIMIT: 10},
|
||||
{CONF_THRESHOLD_TYPE: "above", CONF_LOWER_LIMIT: 10},
|
||||
does_not_raise(),
|
||||
),
|
||||
(
|
||||
{CONF_THRESHOLD_TYPE: ThresholdType.ABOVE, CONF_LOWER_LIMIT: "sensor.test"},
|
||||
{CONF_THRESHOLD_TYPE: "above", CONF_LOWER_LIMIT: "sensor.test"},
|
||||
does_not_raise(),
|
||||
),
|
||||
(
|
||||
{CONF_THRESHOLD_TYPE: ThresholdType.BELOW, CONF_UPPER_LIMIT: 90},
|
||||
{CONF_THRESHOLD_TYPE: "below", CONF_UPPER_LIMIT: 90},
|
||||
does_not_raise(),
|
||||
),
|
||||
(
|
||||
{CONF_THRESHOLD_TYPE: ThresholdType.BELOW, CONF_UPPER_LIMIT: "sensor.test"},
|
||||
{CONF_THRESHOLD_TYPE: "below", CONF_UPPER_LIMIT: "sensor.test"},
|
||||
does_not_raise(),
|
||||
),
|
||||
(
|
||||
{
|
||||
CONF_THRESHOLD_TYPE: ThresholdType.BETWEEN,
|
||||
CONF_THRESHOLD_TYPE: "between",
|
||||
CONF_LOWER_LIMIT: 10,
|
||||
CONF_UPPER_LIMIT: 90,
|
||||
},
|
||||
@@ -1413,7 +1413,7 @@ async def test_numerical_state_attribute_changed_error_handling(
|
||||
),
|
||||
(
|
||||
{
|
||||
CONF_THRESHOLD_TYPE: ThresholdType.BETWEEN,
|
||||
CONF_THRESHOLD_TYPE: "between",
|
||||
CONF_LOWER_LIMIT: 10,
|
||||
CONF_UPPER_LIMIT: "sensor.test",
|
||||
},
|
||||
@@ -1421,7 +1421,7 @@ async def test_numerical_state_attribute_changed_error_handling(
|
||||
),
|
||||
(
|
||||
{
|
||||
CONF_THRESHOLD_TYPE: ThresholdType.BETWEEN,
|
||||
CONF_THRESHOLD_TYPE: "between",
|
||||
CONF_LOWER_LIMIT: "sensor.test",
|
||||
CONF_UPPER_LIMIT: 90,
|
||||
},
|
||||
@@ -1429,7 +1429,7 @@ async def test_numerical_state_attribute_changed_error_handling(
|
||||
),
|
||||
(
|
||||
{
|
||||
CONF_THRESHOLD_TYPE: ThresholdType.BETWEEN,
|
||||
CONF_THRESHOLD_TYPE: "between",
|
||||
CONF_LOWER_LIMIT: "sensor.test",
|
||||
CONF_UPPER_LIMIT: "sensor.test",
|
||||
},
|
||||
@@ -1437,7 +1437,7 @@ async def test_numerical_state_attribute_changed_error_handling(
|
||||
),
|
||||
(
|
||||
{
|
||||
CONF_THRESHOLD_TYPE: ThresholdType.OUTSIDE,
|
||||
CONF_THRESHOLD_TYPE: "outside",
|
||||
CONF_LOWER_LIMIT: 10,
|
||||
CONF_UPPER_LIMIT: 90,
|
||||
},
|
||||
@@ -1445,7 +1445,7 @@ async def test_numerical_state_attribute_changed_error_handling(
|
||||
),
|
||||
(
|
||||
{
|
||||
CONF_THRESHOLD_TYPE: ThresholdType.OUTSIDE,
|
||||
CONF_THRESHOLD_TYPE: "outside",
|
||||
CONF_LOWER_LIMIT: 10,
|
||||
CONF_UPPER_LIMIT: "sensor.test",
|
||||
},
|
||||
@@ -1453,7 +1453,7 @@ async def test_numerical_state_attribute_changed_error_handling(
|
||||
),
|
||||
(
|
||||
{
|
||||
CONF_THRESHOLD_TYPE: ThresholdType.OUTSIDE,
|
||||
CONF_THRESHOLD_TYPE: "outside",
|
||||
CONF_LOWER_LIMIT: "sensor.test",
|
||||
CONF_UPPER_LIMIT: 90,
|
||||
},
|
||||
@@ -1461,7 +1461,7 @@ async def test_numerical_state_attribute_changed_error_handling(
|
||||
),
|
||||
(
|
||||
{
|
||||
CONF_THRESHOLD_TYPE: ThresholdType.OUTSIDE,
|
||||
CONF_THRESHOLD_TYPE: "outside",
|
||||
CONF_LOWER_LIMIT: "sensor.test",
|
||||
CONF_UPPER_LIMIT: "sensor.test",
|
||||
},
|
||||
@@ -1481,58 +1481,58 @@ async def test_numerical_state_attribute_changed_error_handling(
|
||||
),
|
||||
(
|
||||
# Must provide lower limit for ABOVE
|
||||
{CONF_THRESHOLD_TYPE: ThresholdType.ABOVE},
|
||||
{CONF_THRESHOLD_TYPE: "above"},
|
||||
pytest.raises(vol.Invalid),
|
||||
),
|
||||
(
|
||||
# Must provide lower limit for ABOVE
|
||||
{CONF_THRESHOLD_TYPE: ThresholdType.ABOVE, CONF_UPPER_LIMIT: 90},
|
||||
{CONF_THRESHOLD_TYPE: "above", CONF_UPPER_LIMIT: 90},
|
||||
pytest.raises(vol.Invalid),
|
||||
),
|
||||
(
|
||||
# Must provide upper limit for BELOW
|
||||
{CONF_THRESHOLD_TYPE: ThresholdType.BELOW},
|
||||
{CONF_THRESHOLD_TYPE: "below"},
|
||||
pytest.raises(vol.Invalid),
|
||||
),
|
||||
(
|
||||
# Must provide upper limit for BELOW
|
||||
{CONF_THRESHOLD_TYPE: ThresholdType.BELOW, CONF_LOWER_LIMIT: 10},
|
||||
{CONF_THRESHOLD_TYPE: "below", CONF_LOWER_LIMIT: 10},
|
||||
pytest.raises(vol.Invalid),
|
||||
),
|
||||
(
|
||||
# Must provide upper and lower limits for BETWEEN
|
||||
{CONF_THRESHOLD_TYPE: ThresholdType.BETWEEN},
|
||||
{CONF_THRESHOLD_TYPE: "between"},
|
||||
pytest.raises(vol.Invalid),
|
||||
),
|
||||
(
|
||||
# Must provide upper and lower limits for BETWEEN
|
||||
{CONF_THRESHOLD_TYPE: ThresholdType.BETWEEN, CONF_LOWER_LIMIT: 10},
|
||||
{CONF_THRESHOLD_TYPE: "between", CONF_LOWER_LIMIT: 10},
|
||||
pytest.raises(vol.Invalid),
|
||||
),
|
||||
(
|
||||
# Must provide upper and lower limits for BETWEEN
|
||||
{CONF_THRESHOLD_TYPE: ThresholdType.BETWEEN, CONF_UPPER_LIMIT: 90},
|
||||
{CONF_THRESHOLD_TYPE: "between", CONF_UPPER_LIMIT: 90},
|
||||
pytest.raises(vol.Invalid),
|
||||
),
|
||||
(
|
||||
# Must provide upper and lower limits for OUTSIDE
|
||||
{CONF_THRESHOLD_TYPE: ThresholdType.OUTSIDE},
|
||||
{CONF_THRESHOLD_TYPE: "outside"},
|
||||
pytest.raises(vol.Invalid),
|
||||
),
|
||||
(
|
||||
# Must provide upper and lower limits for OUTSIDE
|
||||
{CONF_THRESHOLD_TYPE: ThresholdType.OUTSIDE, CONF_LOWER_LIMIT: 10},
|
||||
{CONF_THRESHOLD_TYPE: "outside", CONF_LOWER_LIMIT: 10},
|
||||
pytest.raises(vol.Invalid),
|
||||
),
|
||||
(
|
||||
# Must provide upper and lower limits for OUTSIDE
|
||||
{CONF_THRESHOLD_TYPE: ThresholdType.OUTSIDE, CONF_UPPER_LIMIT: 90},
|
||||
{CONF_THRESHOLD_TYPE: "outside", CONF_UPPER_LIMIT: 90},
|
||||
pytest.raises(vol.Invalid),
|
||||
),
|
||||
(
|
||||
# Must be valid entity id
|
||||
{
|
||||
CONF_THRESHOLD_TYPE: ThresholdType.BETWEEN,
|
||||
CONF_THRESHOLD_TYPE: "between",
|
||||
CONF_ABOVE: "cat",
|
||||
CONF_BELOW: "dog",
|
||||
},
|
||||
@@ -1541,7 +1541,7 @@ async def test_numerical_state_attribute_changed_error_handling(
|
||||
(
|
||||
# Above must be smaller than below
|
||||
{
|
||||
CONF_THRESHOLD_TYPE: ThresholdType.BETWEEN,
|
||||
CONF_THRESHOLD_TYPE: "between",
|
||||
CONF_ABOVE: 90,
|
||||
CONF_BELOW: 10,
|
||||
},
|
||||
|
||||
@@ -661,11 +661,12 @@ async def test_discovery_requirements_dhcp(hass: HomeAssistant) -> None:
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("requirement", "is_built_in", "deprecation_info"),
|
||||
("requirement", "is_built_in", "deprecation_prefix", "deprecation_info"),
|
||||
[
|
||||
(
|
||||
"hello",
|
||||
True,
|
||||
"Detected that integration",
|
||||
"which is deprecated for testing. This will stop working in Home Assistant"
|
||||
" 2020.12, please create a bug report at https://github.com/home-assistant/"
|
||||
"core/issues?q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+test_component%22",
|
||||
@@ -673,6 +674,7 @@ async def test_discovery_requirements_dhcp(hass: HomeAssistant) -> None:
|
||||
(
|
||||
"hello>=1.0.0",
|
||||
False,
|
||||
"Detected that custom integration",
|
||||
"which is deprecated for testing. This will stop working in Home Assistant"
|
||||
" 2020.12, please create a bug report at https://github.com/home-assistant/"
|
||||
"core/issues?q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+test_component%22",
|
||||
@@ -680,6 +682,7 @@ async def test_discovery_requirements_dhcp(hass: HomeAssistant) -> None:
|
||||
(
|
||||
"pyserial-asyncio",
|
||||
False,
|
||||
"Detected that custom integration",
|
||||
"which should be replaced by pyserial-asyncio-fast. This will stop"
|
||||
" working in Home Assistant 2026.7, please create a bug report at "
|
||||
"https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue+"
|
||||
@@ -688,6 +691,7 @@ async def test_discovery_requirements_dhcp(hass: HomeAssistant) -> None:
|
||||
(
|
||||
"pyserial-asyncio>=0.6",
|
||||
True,
|
||||
"Detected that integration",
|
||||
"which should be replaced by pyserial-asyncio-fast. This will stop"
|
||||
" working in Home Assistant 2026.7, please create a bug report at "
|
||||
"https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue+"
|
||||
@@ -699,6 +703,7 @@ async def test_install_deprecated_package(
|
||||
hass: HomeAssistant,
|
||||
requirement: str,
|
||||
is_built_in: bool,
|
||||
deprecation_prefix: str,
|
||||
deprecation_info: str,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
@@ -710,10 +715,16 @@ async def test_install_deprecated_package(
|
||||
patch("homeassistant.util.package.install_package", return_value=True),
|
||||
):
|
||||
await async_process_requirements(
|
||||
hass, "test_component", [requirement], is_built_in
|
||||
hass,
|
||||
"test_component",
|
||||
[
|
||||
requirement,
|
||||
"git+https://github.com/user/project.git@1.2.3",
|
||||
],
|
||||
is_built_in,
|
||||
)
|
||||
|
||||
assert (
|
||||
f"Detected that {'' if is_built_in else 'custom '}integration "
|
||||
f"'test_component' has requirement '{requirement}' {deprecation_info}"
|
||||
f"{deprecation_prefix} 'test_component'"
|
||||
f" has requirement '{requirement}' {deprecation_info}"
|
||||
) in caplog.text
|
||||
|
||||
Reference in New Issue
Block a user