mirror of
https://github.com/home-assistant/core.git
synced 2026-01-13 02:57:37 +01:00
Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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==20260107.0"]
|
||||
"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"]
|
||||
}
|
||||
|
||||
@@ -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*",
|
||||
|
||||
@@ -16,5 +16,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pysma"],
|
||||
"requirements": ["pysma==1.0.2"]
|
||||
"requirements": ["pysma==1.1.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"]
|
||||
}
|
||||
|
||||
@@ -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.1"]
|
||||
"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"),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.83", "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 = "0"
|
||||
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,7 +39,7 @@ habluetooth==5.8.0
|
||||
hass-nabucasa==1.7.0
|
||||
hassil==3.5.0
|
||||
home-assistant-bluetooth==1.13.1
|
||||
home-assistant-frontend==20260107.0
|
||||
home-assistant-frontend==20260107.1
|
||||
home-assistant-intents==2026.1.6
|
||||
httpx==0.28.1
|
||||
ifaddr==0.2.0
|
||||
@@ -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
|
||||
|
||||
@@ -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.0"
|
||||
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",
|
||||
|
||||
6
requirements.txt
generated
6
requirements.txt
generated
@@ -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
|
||||
|
||||
36
requirements_all.txt
generated
36
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
|
||||
@@ -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,7 +1213,7 @@ hole==0.9.0
|
||||
holidays==0.84
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20260107.0
|
||||
home-assistant-frontend==20260107.1
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2026.1.6
|
||||
@@ -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.1
|
||||
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
|
||||
@@ -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
|
||||
@@ -3277,7 +3277,7 @@ zeroconf==0.148.0
|
||||
zeversolar==0.3.2
|
||||
|
||||
# homeassistant.components.zha
|
||||
zha==0.0.83
|
||||
zha==0.0.84
|
||||
|
||||
# homeassistant.components.zhong_hong
|
||||
zhong-hong-hvac==1.0.13
|
||||
|
||||
36
requirements_test_all.txt
generated
36
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
|
||||
@@ -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,7 +1071,7 @@ hole==0.9.0
|
||||
holidays==0.84
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20260107.0
|
||||
home-assistant-frontend==20260107.1
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2026.1.6
|
||||
@@ -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.1
|
||||
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
|
||||
@@ -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
|
||||
@@ -2738,7 +2738,7 @@ zeroconf==0.148.0
|
||||
zeversolar==0.3.2
|
||||
|
||||
# homeassistant.components.zha
|
||||
zha==0.0.83
|
||||
zha==0.0.84
|
||||
|
||||
# homeassistant.components.zwave_js
|
||||
zwave-js-server-python==0.67.1
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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