Compare commits

..

46 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
Bram Kragten
49086b2a76 2026.1.0 (#159957) 2026-01-07 18:38:10 +01:00
Bram Kragten
1f28fe9933 Bump version to 2026.1.0 2026-01-07 17:46:04 +01:00
Bram Kragten
4465aa264c Update frontend to 20260107.0 (#160434) 2026-01-07 17:45:41 +01:00
Robert Resch
2c1bc96161 Bump deebot-client to 17.0.1 (#160428) 2026-01-07 17:45:40 +01:00
Joost Lekkerkerker
7127159a5b Make Watts depend on the cloud integration (#160424) 2026-01-07 17:45:38 +01:00
Abílio Costa
9f0eb6f077 Support target triggers in automation relation extraction (#160369) 2026-01-07 17:45:37 +01:00
Paul Bottein
da19cc06e3 Fix hvac_mode validation in climate.hvac_mode_changed trigger (#160364) 2026-01-07 17:45:36 +01:00
70 changed files with 876 additions and 212 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

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

View File

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

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

View File

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

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

View File

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

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

View File

@@ -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 = "0b5"
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==20251229.1
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.0b5"
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

38
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
@@ -782,7 +782,7 @@ debugpy==1.8.17
decora-wifi==1.4
# homeassistant.components.ecovacs
deebot-client==17.0.0
deebot-client==17.0.1
# homeassistant.components.ihc
# homeassistant.components.namecheapdns
@@ -854,7 +854,7 @@ ecoaliface==0.4.0
egauge-async==0.4.0
# homeassistant.components.eheimdigital
eheimdigital==1.4.0
eheimdigital==1.5.0
# homeassistant.components.ekeybionyx
ekey-bionyxpy==1.0.1
@@ -1105,7 +1105,7 @@ google-nest-sdm==9.1.2
google-photos-library-api==0.12.1
# homeassistant.components.google_air_quality
google_air_quality_api==2.0.2
google_air_quality_api==2.1.2
# homeassistant.components.slide
# homeassistant.components.slide_local
@@ -1127,7 +1127,7 @@ gpiozero==1.6.2
gps3==0.33.3
# homeassistant.components.gree
greeclimate==2.1.0
greeclimate==2.1.1
# homeassistant.components.greeneye_monitor
greeneye_monitor==3.0.3
@@ -1213,7 +1213,7 @@ hole==0.9.0
holidays==0.84
# homeassistant.components.frontend
home-assistant-frontend==20251229.1
home-assistant-frontend==20260107.1
# homeassistant.components.conversation
home-assistant-intents==2026.1.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
@@ -691,7 +691,7 @@ dbus-fast==3.1.2
debugpy==1.8.17
# homeassistant.components.ecovacs
deebot-client==17.0.0
deebot-client==17.0.1
# homeassistant.components.ihc
# homeassistant.components.namecheapdns
@@ -754,7 +754,7 @@ easyenergy==2.1.2
egauge-async==0.4.0
# homeassistant.components.eheimdigital
eheimdigital==1.4.0
eheimdigital==1.5.0
# homeassistant.components.ekeybionyx
ekey-bionyxpy==1.0.1
@@ -981,7 +981,7 @@ google-nest-sdm==9.1.2
google-photos-library-api==0.12.1
# homeassistant.components.google_air_quality
google_air_quality_api==2.0.2
google_air_quality_api==2.1.2
# homeassistant.components.slide
# homeassistant.components.slide_local
@@ -1000,7 +1000,7 @@ govee-local-api==2.3.0
gps3==0.33.3
# homeassistant.components.gree
greeclimate==2.1.0
greeclimate==2.1.1
# homeassistant.components.greeneye_monitor
greeneye_monitor==3.0.3
@@ -1071,7 +1071,7 @@ hole==0.9.0
holidays==0.84
# homeassistant.components.frontend
home-assistant-frontend==20251229.1
home-assistant-frontend==20260107.1
# homeassistant.components.conversation
home-assistant-intents==2026.1.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

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

View File

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

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

View File

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

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

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