Compare commits

...

39 Commits

Author SHA1 Message Date
Bram Kragten
b8f23bb388 2026.1.1 (#160771) 2026-01-12 11:51:56 +01:00
Bram Kragten
e238d67818 Bump version to 2026.1.1 2026-01-12 11:14:27 +01:00
Joost Lekkerkerker
992a9bdd3b Fix fitbit icon (#160750) 2026-01-12 11:14:06 +01:00
Duco Sebel
ceaae1c1cc Bump python-homewizard-energy to 10.0.1 (#160736) 2026-01-12 11:14:05 +01:00
Erwin Douna
1c163c92dc Bump pytado 0.18.16 (#160724) 2026-01-12 11:14:04 +01:00
Josef Zweck
a42aa9372c Fix missing key for brew by weight in lamarzocco (#160722) 2026-01-12 11:14:03 +01:00
Ernst Klamer
013592bd54 Revert bthome-ble back to 3.16.0 to fix missing data (#160694) 2026-01-12 11:14:02 +01:00
Michael Hansen
2101bae095 Bump pysilero-vad to 3.2.0 (#160691) 2026-01-12 11:14:01 +01:00
Clifford Roche
cfa1107135 Bump greeclimate to 2.1.1 (#160683) 2026-01-12 11:14:00 +01:00
Paul Tarjan
a269ef660a Bump pyhik to 0.4.0 (#160654) 2026-01-12 11:13:59 +01:00
Bram Kragten
c43c4f17e9 Update frontend to 20260107.1 (#160644) 2026-01-12 11:13:58 +01:00
Jordan Harvey
de25e6af51 Bump pynintendoparental to 2.3.2 (#160626)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-01-12 11:13:57 +01:00
Martin Hjelmare
18d3629b6c Fix Z-Wave creating notification binary sensor for idle state (#160604) 2026-01-12 11:13:56 +01:00
Arie Catsman
50c477a408 Change device class to energy_storage for some enphase_envoy battery entities (#160603) 2026-01-12 11:13:55 +01:00
Daniel Hjelseth Høyer
ea9cd7d905 Better handling of ratelimiting from Tibber (#160599)
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-12 11:13:54 +01:00
Tom Matheussen
2bf4ac20ea Add missing segment speed icons for WLED (#160597) 2026-01-12 11:13:53 +01:00
Brett Adams
94ff881897 Fix config flow bug in Tesla Fleet (#160591) 2026-01-12 11:13:52 +01:00
tronikos
2975b3c1b9 Bump opower to 0.16.1 (#160588) 2026-01-12 11:13:52 +01:00
Johann Kellerman
0143c4ff85 Bump pysma to 1.1.0 (#160583) 2026-01-12 11:13:51 +01:00
Brett Adams
f59566d20b Fix Climate signal in Teslemetry (#160571) 2026-01-12 11:13:49 +01:00
Thomas55555
395f0ad2a7 Bump google-air-quality-api to 2.1.2 (#160561) 2026-01-12 11:13:48 +01:00
Michael
2af1fc6759 Fix for older Fritzbox models which do not support smarthome triggers (#160555) 2026-01-12 11:13:47 +01:00
Michael Hansen
c1e7122d1c Bump pysilero-vad to 3.1.0 (#160554) 2026-01-12 11:13:46 +01:00
Maciej Bieniek
e5624b1224 Fix AttributeError for missing/incomplete health data in Tractive (#160553) 2026-01-12 11:13:45 +01:00
Brett Adams
6e380bafca Catch any migration failures in Teslemetry (#160549) 2026-01-12 11:13:45 +01:00
puddly
bb9fd94430 Bump serialx to v0.6.2 (#160545) 2026-01-12 11:13:44 +01:00
Michael Hansen
07bc5d5c6b Revert "Update voluptuous and voluptuous-openapi" (#160530) 2026-01-12 11:13:43 +01:00
Jan Bouwhuis
651b7116dd Bump Intergas Incomfort-client to v0.6.11 (#160520) 2026-01-12 11:13:42 +01:00
Bram Kragten
34438bd039 Fix trigger selectors (#160519) 2026-01-12 11:13:41 +01:00
wollew
7b53b8691c fix rain sensor for some rare velux windows (#160504) 2026-01-12 11:13:40 +01:00
Erik Montnemery
8748d6f200 Bump python-otbr-api to 2.7.1 (#160496) 2026-01-12 11:13:39 +01:00
osohotwateriot
8d95511650 Add Nettleie optimization option (#160494) 2026-01-12 11:13:38 +01:00
epenet
9aa5953a86 Fix Requirement parsing in RequirementsManager (#160485) 2026-01-12 11:13:37 +01:00
ElCruncharino
5ccdfda747 Add asyncio-level timeout to Backblaze B2 uploads (#160468) 2026-01-12 11:13:36 +01:00
Dan Čermák
00ad44cb91 Fix JSON serialization of time objects in anthropic tool results (#160459)
Co-authored-by: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com>
2026-01-12 11:13:35 +01:00
Mick Vleeshouwer
b7519cd880 Bump pyOverkiz to 1.19.4 (#160457) 2026-01-12 11:13:34 +01:00
TheJulianJES
ac44769539 Bump ZHA to 0.0.84 (#160440) 2026-01-12 11:13:33 +01:00
Sid
9e95b80805 Bump eheimdigital to 1.5.0 (#160312) 2026-01-12 11:13:31 +01:00
Paul Tarjan
50086ca5c7 Fix Hikvision NVR binary sensors not being detected (#160254)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 11:13:30 +01:00
63 changed files with 613 additions and 196 deletions

View File

@@ -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"] != (

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,5 +8,5 @@
"iot_class": "local_push",
"loggers": ["pyhik"],
"quality_scale": "legacy",
"requirements": ["pyHik==0.3.4"]
"requirements": ["pyHik==0.4.0"]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,5 +9,5 @@
"iot_class": "cloud_polling",
"loggers": ["opower"],
"quality_scale": "bronze",
"requirements": ["opower==0.16.0"]
"requirements": ["opower==0.16.1"]
}

View File

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

View File

@@ -58,6 +58,7 @@
"state": {
"advanced": "Advanced",
"gridcompany": "Grid company",
"nettleie": "Nettleie",
"off": "[%key:common::state::off%]",
"oso": "OSO",
"smartcompany": "Smart company"

View File

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

View File

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

View File

@@ -16,5 +16,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["pysma"],
"requirements": ["pysma==1.0.2"]
"requirements": ["pysma==1.1.0"]
}

View File

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

View File

@@ -79,6 +79,7 @@ class OAuth2FlowHandler(
session = async_get_clientsession(self.hass)
self.api = TeslaFleetApi(
access_token="",
session=session,
server=server,
partner_scope=True,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,6 +9,9 @@
}
},
"number": {
"segment_speed": {
"default": "mdi:speedometer"
},
"speed": {
"default": "mdi:speedometer"
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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