Compare commits

...

28 Commits

Author SHA1 Message Date
Franck Nijhof 73c9edd3e8 Ran gen_requirements_all 2026-05-22 16:18:20 +00:00
Franck Nijhof 18f30bd97b Bump version to 2026.5.4 2026-05-22 16:06:32 +00:00
Manu eae6e79b61 Fix dead link in System Bridge service action (#171855) 2026-05-22 16:04:27 +00:00
Franck Nijhof 5bb42801d9 Fix Hue device trigger crash for devices removed from bridge (#171844) 2026-05-22 16:04:25 +00:00
Franck Nijhof 98271265d3 Fix OpenHome config flow crash when UDN is a list (#171841) 2026-05-22 16:04:23 +00:00
Franck Nijhof 92d20477bc Register Insteon modem device before platform setup (#171839) 2026-05-22 16:04:21 +00:00
Franck Nijhof 9352a0057e Fix invalid MDI icon references (#171831)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-22 16:04:19 +00:00
Franck Nijhof 5fb874277a Fix Lutron Caseta battery sensor crash on unsupported devices (#171829) 2026-05-22 16:04:17 +00:00
Franck Nijhof d65f605398 Fix ZBT-2 hardware page crash when entry data is missing VID (#171828) 2026-05-22 16:04:16 +00:00
Simone Chemelli 7e5b448f70 Add missing exception translation keys in alexa_devices (#171749) 2026-05-22 16:04:14 +00:00
Simone Chemelli ef5da5ef36 Fix exception translation placeholder mismatches in comelit (#171748) 2026-05-22 16:04:11 +00:00
epenet 410f00c4ed Bump renault-api to 0.5.10 (#171692) 2026-05-22 15:58:41 +00:00
Kamil Breguła 33c205dc04 Bump wled to 0.23.0 and remove backoff exception (#171622) 2026-05-22 15:58:39 +00:00
dontinelli 267b3e279d Fix update error message key in solarlog (#171611) 2026-05-22 15:58:37 +00:00
Maciej Bieniek 9c1cd8093d Fix media_image_hash and validate the MIME type in the Shelly media player (#171585) 2026-05-22 15:58:35 +00:00
Josef Zweck 201c0c2470 Fix string ref for tedee (#171548) 2026-05-22 15:58:33 +00:00
Franck Nijhof 281d6e0e8b Fix Wyoming satellite crash when TTS is not configured (#171513) 2026-05-22 15:58:31 +00:00
Franck Nijhof 88746534a4 Fix PowerView cover crash when shade position is unavailable (#171471) 2026-05-22 15:58:29 +00:00
Franck Nijhof 135f91c3c5 Fix habitica ignoring zero values for interval and streak (#171468) 2026-05-22 15:58:27 +00:00
Franck Nijhof 49d8dc88d9 Fix SmartThings crash when timestamp attribute is None (#171467) 2026-05-22 15:58:25 +00:00
epenet a7a2c1eb02 Bump renault-api to 0.5.9 (#171428) 2026-05-22 15:58:23 +00:00
J. Nick Koston 6596f956d2 Bump aiodns to 4.0.4 (#171420)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: frenck <195327+frenck@users.noreply.github.com>
2026-05-22 15:57:27 +00:00
TheJulianJES 9d8859833b Fix ZHA blocking minor version downgrades (#171319) 2026-05-22 15:48:41 +00:00
Aidan Timson 65a4c10660 Bump aiolyric to 2.1.1, Update OAuth URL for lyric (#171181) 2026-05-22 15:48:39 +00:00
Åke Strandberg 1737b50558 Add missing Miele Dishwasher codes (#171175) 2026-05-22 15:48:37 +00:00
Luke Lashley 614c7006f6 Bump python-roborock to 5.12.0 (#171112)
Co-authored-by: Robert Resch <robert@resch.dev>
2026-05-22 15:46:06 +00:00
Jonathan Segev 8c901cc405 Bump aiolyric to 2.1.0 (#171007)
Co-authored-by: Erwin Douna <e.douna@gmail.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-05-22 15:46:04 +00:00
Franck Nijhof 5d0fdfd38b Apply web search citation stripping for GPT-5.x models in OpenAI conversation (#170956) 2026-05-22 15:46:02 +00:00
57 changed files with 570 additions and 508 deletions
@@ -11,7 +11,7 @@
"service": "mdi:dialpad"
},
"alarm_toggle_chime": {
"service": "mdi:abc"
"service": "mdi:bell-ring"
}
}
}
@@ -102,6 +102,9 @@
"entry_not_loaded": {
"message": "Entry not loaded: {entry}"
},
"invalid_auth": {
"message": "Invalid authentication credentials: {error}"
},
"invalid_device_id": {
"message": "Invalid device ID specified: {device_id}"
},
@@ -70,7 +70,6 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
raise InvalidAuth(
translation_domain=DOMAIN,
translation_key="cannot_authenticate",
translation_placeholders={"error": repr(err)},
) from err
finally:
await api.logout()
@@ -63,7 +63,7 @@ class CurrencylayerSensor(SensorEntity):
"""Implementing the Currencylayer sensor."""
_attr_attribution = "Data provided by currencylayer.com"
_attr_icon = "mdi:currency"
_attr_icon = "mdi:currency-usd"
def __init__(self, rest: CurrencylayerData, base: str, quote: str) -> None:
"""Initialize the sensor."""
+1 -1
View File
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/dnsip",
"integration_type": "service",
"iot_class": "cloud_polling",
"requirements": ["aiodns==4.0.3"]
"requirements": ["aiodns==4.0.4"]
}
+1 -1
View File
@@ -23,7 +23,7 @@
"service": "mdi:refresh"
},
"set_dhw_override": {
"service": "mdi:water-heater"
"service": "mdi:water-boiler"
},
"set_system_mode": {
"service": "mdi:pencil"
+1 -1
View File
@@ -16,7 +16,7 @@ class DeviceType(Enum):
GAME_CONSOLE = "mdi:nintendo-game-boy"
STREAMING_DONGLE = "mdi:cast"
LOUDSPEAKER = SOUND_SYSTEM = STB = SATELLITE = MUSIC = "mdi:speaker"
DISC_PLAYER = "mdi:disk-player"
DISC_PLAYER = "mdi:disc-player"
REMOTE_CONTROL = "mdi:remote-tv"
RADIO = "mdi:radio"
PHOTO_CAMERA = PHOTOS = "mdi:camera"
+1 -1
View File
@@ -2,7 +2,7 @@
"entity": {
"button": {
"sync_clock": {
"default": "mdi:clock-sync"
"default": "mdi:clock-check"
}
},
"number": {
@@ -808,10 +808,10 @@ async def _create_or_update_task(call: ServiceCall) -> ServiceResponse: # noqa:
data["daysOfMonth"] = [start_date.day]
data["weeksOfMonth"] = []
if interval := call.data.get(ATTR_INTERVAL):
if (interval := call.data.get(ATTR_INTERVAL)) is not None:
data["everyX"] = interval
if streak := call.data.get(ATTR_STREAK):
if (streak := call.data.get(ATTR_STREAK)) is not None:
data["streak"] = streak
try:
@@ -21,7 +21,9 @@ EXPECTED_ENTRY_VERSION = (
@callback
def async_info(hass: HomeAssistant) -> list[HardwareInfo]:
"""Return board info."""
entries = hass.config_entries.async_entries(DOMAIN)
entries = hass.config_entries.async_entries(
DOMAIN, include_ignore=False, include_disabled=False
)
return [
HardwareInfo(
board=None,
@@ -89,6 +89,8 @@ def async_get_triggers(
# Get Hue device id from device identifier
hue_dev_id = get_hue_device_id(device_entry)
if hue_dev_id is None or hue_dev_id not in api.devices:
return []
# extract triggers from all button resources of this Hue device
triggers: list[dict[str, Any]] = []
model_id = api.devices[hue_dev_id].product_data.product_name
@@ -135,6 +135,11 @@ class PowerViewShadeBase(ShadeEntity, CoverEntity):
"""
return self._is_hard_wired
@property
def available(self) -> bool:
"""Return True if shade position data is available."""
return super().available and self.positions.primary is not None
@property
def extra_state_attributes(self) -> dict[str, str]:
"""Return the state attributes."""
+2 -2
View File
@@ -118,6 +118,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
device = devices.add_x10_device(housecode, unitcode, x10_type, steps)
create_insteon_device(hass, devices.modem, entry.entry_id)
await hass.config_entries.async_forward_entry_setups(entry, INSTEON_PLATFORMS)
for address in devices:
@@ -131,8 +133,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
register_new_device_callback(hass)
async_setup_services(hass)
create_insteon_device(hass, devices.modem, entry.entry_id)
entry.async_create_background_task(
hass, async_get_device_config(hass, entry), "insteon-get-device-config"
)
@@ -1,7 +1,7 @@
{
"entity": {
"binary_sensor": {
"erev_shabbat_hag": { "default": "mdi:candle-light" },
"erev_shabbat_hag": { "default": "mdi:candle" },
"issur_melacha_in_effect": { "default": "mdi:power-plug-off" },
"motzei_shabbat_hag": { "default": "mdi:fire" }
},
+1 -1
View File
@@ -7,7 +7,7 @@
"service": "mdi:lock-open"
},
"disable": {
"service": "mdi:fash-off"
"service": "mdi:flash-off"
},
"enable": {
"service": "mdi:flash"
+4 -4
View File
@@ -28,25 +28,25 @@
"ice_maker": {
"default": "mdi:cube-outline",
"state": {
"off": "mdi:cube-outline-off"
"off": "mdi:cube-off-outline"
}
},
"ice_maker_bottom_zone": {
"default": "mdi:cube-outline",
"state": {
"off": "mdi:cube-outline-off"
"off": "mdi:cube-off-outline"
}
},
"ice_maker_middle_zone": {
"default": "mdi:cube-outline",
"state": {
"off": "mdi:cube-outline-off"
"off": "mdi:cube-off-outline"
}
},
"ice_maker_top_zone": {
"default": "mdi:cube-outline",
"state": {
"off": "mdi:cube-outline-off"
"off": "mdi:cube-off-outline"
}
}
},
@@ -5,7 +5,7 @@ from __future__ import annotations
from datetime import timedelta
from typing import Any
from pylutron_caseta import OCCUPANCY_GROUP_OCCUPIED
from pylutron_caseta import OCCUPANCY_GROUP_OCCUPIED, BridgeResponseError
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
@@ -133,7 +133,11 @@ class LutronCasetaBatterySensor(LutronCasetaEntity, BinarySensorEntity):
async def async_update(self) -> None:
"""Fetch the latest battery status from the bridge."""
status = await self._smartbridge.get_battery_status(self.device_id)
try:
status = await self._smartbridge.get_battery_status(self.device_id)
except BridgeResponseError:
self._attr_is_on = None
return
normalized_status = status.strip().casefold() if status else None
if normalized_status == BATTERY_STATUS_LOW:
self._attr_is_on = True
+2 -2
View File
@@ -5,8 +5,8 @@ from aiolyric.exceptions import LyricAuthenticationException, LyricException
DOMAIN = "lyric"
OAUTH2_AUTHORIZE = "https://api.honeywell.com/oauth2/authorize"
OAUTH2_TOKEN = "https://api.honeywell.com/oauth2/token"
OAUTH2_AUTHORIZE = "https://api.honeywellhome.com/oauth2/authorize"
OAUTH2_TOKEN = "https://api.honeywellhome.com/oauth2/token"
PRESET_NO_HOLD = "NoHold"
PRESET_TEMPORARY_HOLD = "TemporaryHold"
+1 -1
View File
@@ -22,5 +22,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["aiolyric"],
"requirements": ["aiolyric==2.0.2"]
"requirements": ["aiolyric==2.1.1"]
}
+1 -1
View File
@@ -102,7 +102,7 @@
"default": "mdi:home-lightning-bolt"
},
"eve_weather_trend": {
"default": "mdi:weather",
"default": "mdi:weather-cloudy",
"state": {
"cloudy": "mdi:weather-cloudy",
"rainy": "mdi:weather-rainy",
+3 -1
View File
@@ -511,7 +511,9 @@ class DishWasherProgramId(MieleEnum, missing_to_none=True):
tall_items = 17, 42
glasses_warm = 19
quick_intense = 21
normal = 30
normal = 23, 30
pre_wash = 24
pot_rests_and_filters = 25
power_wash = 44, 204
comfort_wash = 203
comfort_wash_plus = 209
@@ -709,6 +709,7 @@
"pork_tenderloin_medaillons_4_cm": "Pork tenderloin (medaillons, 4 cm)",
"pork_tenderloin_medaillons_5_cm": "Pork tenderloin (medaillons, 5 cm)",
"pork_with_crackling": "Pork with crackling",
"pot_rests_and_filters": "Pot rests and filters",
"potato_cheese_gratin": "Potato cheese gratin",
"potato_dumplings_half_half_boil_in_bag": "Potato dumplings (half/half, boil-in-bag)",
"potato_dumplings_half_half_deep_frozen": "Potato dumplings (half/half, deep-frozen)",
@@ -751,6 +752,7 @@
"powerfresh": "PowerFresh",
"prawns": "Prawns",
"pre_ironing": "Pre-ironing",
"pre_wash": "Pre-wash",
"proofing": "Proofing",
"prove_15_min": "Prove for 15 min",
"prove_30_min": "Prove for 30 min",
@@ -597,8 +597,8 @@ class OpenAIBaseLLMEntity(Entity):
)
)
if "reasoning" not in model_args:
# Reasoning models handle this correctly with just a prompt
if not model_args["model"].startswith("o"):
# o-series models handle this correctly with just a prompt
remove_citations = True
tools.append(web_search)
@@ -37,11 +37,15 @@ class OpenhomeConfigFlow(ConfigFlow, domain=DOMAIN):
_LOGGER.debug("async_step_ssdp: Incomplete discovery, ignoring")
return self.async_abort(reason="incomplete_discovery")
_LOGGER.debug(
"async_step_ssdp: setting unique id %s", discovery_info.upnp[ATTR_UPNP_UDN]
)
udn = discovery_info.upnp[ATTR_UPNP_UDN]
if isinstance(udn, list):
if not udn:
return self.async_abort(reason="incomplete_discovery")
udn = udn[0]
await self.async_set_unique_id(discovery_info.upnp[ATTR_UPNP_UDN])
_LOGGER.debug("async_step_ssdp: setting unique id %s", udn)
await self.async_set_unique_id(udn)
self._abort_if_unique_id_configured({CONF_HOST: discovery_info.ssdp_location})
_LOGGER.debug(
+16 -16
View File
@@ -29,29 +29,29 @@
}
},
"sensor": {
"translation_key_0": {
"default": "mdi:abc"
"flow_sensor_clicks_cubic_meter": {
"default": "mdi:water-pump"
},
"translation_key_1": {
"default": "mdi:abc"
"flow_sensor_consumed_liters": {
"default": "mdi:water-pump"
},
"translation_key_2": {
"default": "mdi:abc"
"flow_sensor_leak_clicks": {
"default": "mdi:pipe-leak"
},
"translation_key_3": {
"default": "mdi:abc"
"flow_sensor_leak_volume": {
"default": "mdi:pipe-leak"
},
"translation_key_4": {
"default": "mdi:abc"
"flow_sensor_start_index": {
"default": "mdi:water-pump"
},
"translation_key_5": {
"default": "mdi:abc"
"flow_sensor_watering_clicks": {
"default": "mdi:water-pump"
},
"translation_key_6": {
"default": "mdi:abc"
"last_leak_detected": {
"default": "mdi:pipe-leak"
},
"translation_key_7": {
"default": "mdi:abc"
"rain_sensor_rain_start": {
"default": "mdi:weather-pouring"
}
},
"switch": {
@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["renault_api"],
"quality_scale": "silver",
"requirements": ["renault-api==0.5.8"]
"requirements": ["renault-api==0.5.10"]
}
@@ -20,7 +20,7 @@
"loggers": ["roborock"],
"quality_scale": "silver",
"requirements": [
"python-roborock==5.5.1",
"python-roborock==5.12.0",
"vacuum-map-parser-roborock==0.1.4"
]
}
+43 -10
View File
@@ -4,6 +4,7 @@ from __future__ import annotations
import base64
import binascii
import contextlib
from dataclasses import dataclass
import datetime
import hashlib
@@ -40,6 +41,15 @@ from .utils import get_device_entry_gen
CONTENT_TYPE_AUDIO = "audio"
CONTENT_TYPE_RADIO = "radio"
ALLOWED_IMAGE_MIME_TYPES: Final = frozenset(
{
"image/gif",
"image/jpeg",
"image/png",
"image/webp",
}
)
PARALLEL_UPDATES = 0
@@ -104,6 +114,9 @@ class ShellyRpcMediaPlayer(ShellyRpcAttributeEntity, MediaPlayerEntity):
_last_media_position: int | None = None
_last_media_position_updated_at: datetime.datetime | None = None
_cached_thumb: str | None = None
_cached_thumb_result: tuple[bytes, str] | None = None
def __init__(
self,
coordinator: ShellyRpcCoordinator,
@@ -217,9 +230,11 @@ class ShellyRpcMediaPlayer(ShellyRpcAttributeEntity, MediaPlayerEntity):
@property
def media_image_hash(self) -> str | None:
"""Hash value for media image."""
if (thumb := self._media_meta.get("thumb")) and thumb.startswith("data"):
return hashlib.sha256(thumb.encode("utf-8")).hexdigest()[:16]
return super().media_image_hash
thumb = self._media_meta.get("thumb")
if not thumb or self._decode_image_data(thumb) is None:
return super().media_image_hash
return hashlib.sha256(thumb.encode("utf-8")).hexdigest()[:16]
def _get_updated_media_position(self) -> int | None:
"""Return the current playback position and update its timestamp."""
@@ -237,15 +252,11 @@ class ShellyRpcMediaPlayer(ShellyRpcAttributeEntity, MediaPlayerEntity):
async def async_get_media_image(self) -> tuple[bytes | None, str | None]:
"""Fetch media image of current playing track."""
thumb = self._media_meta["thumb"]
try:
prefix, image_data = thumb.split(",", 1)
image = base64.b64decode(image_data, validate=True)
mime = prefix.split(";", 1)[0].rsplit(":", 1)[-1]
except binascii.Error, ValueError:
thumb = self._media_meta.get("thumb")
if not thumb or (result := self._decode_image_data(thumb)) is None:
return await super().async_get_media_image()
return image, mime
return result
@rpc_call
async def async_media_play(self) -> None:
@@ -436,3 +447,25 @@ class ShellyRpcMediaPlayer(ShellyRpcAttributeEntity, MediaPlayerEntity):
translation_key="unsupported_media_type",
translation_placeholders={"media_type": str(media_type)},
)
def _decode_image_data(self, thumb: str) -> tuple[bytes, str] | None:
"""Return image_bytes and mime_type for a valid image data or None."""
if thumb == self._cached_thumb:
return self._cached_thumb_result
result: tuple[bytes, str] | None = None
if thumb.startswith("data"):
try:
prefix, image_data = thumb.split(",", 1)
mime = prefix.split(";", 1)[0].rsplit(":", 1)[-1]
except IndexError, ValueError:
pass
else:
if mime in ALLOWED_IMAGE_MIME_TYPES:
with contextlib.suppress(binascii.Error):
result = base64.b64decode(image_data, validate=True), mime
self._cached_thumb = thumb
self._cached_thumb_result = result
return result
@@ -396,7 +396,7 @@ CAPABILITY_TO_SENSORS: dict[
key=Attribute.COMPLETION_TIME,
translation_key="completion_time",
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=dt_util.parse_datetime,
value_fn=lambda value: dt_util.parse_datetime(value) if value else None,
)
],
},
@@ -449,7 +449,7 @@ CAPABILITY_TO_SENSORS: dict[
key=Attribute.COMPLETION_TIME,
translation_key="completion_time",
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=dt_util.parse_datetime,
value_fn=lambda value: dt_util.parse_datetime(value) if value else None,
)
],
},
@@ -567,7 +567,7 @@ CAPABILITY_TO_SENSORS: dict[
key=Attribute.GAS_METER_TIME,
translation_key="gas_meter_time",
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=dt_util.parse_datetime,
value_fn=lambda value: dt_util.parse_datetime(value) if value else None,
)
],
Attribute.GAS_METER_VOLUME: [
@@ -728,7 +728,7 @@ CAPABILITY_TO_SENSORS: dict[
key=Attribute.COMPLETION_TIME,
translation_key="completion_time",
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=dt_util.parse_datetime,
value_fn=lambda value: dt_util.parse_datetime(value) if value else None,
component_fn=lambda component: component == "cavity-01",
component_translation_key={
"cavity-01": "oven_completion_time_cavity_01",
@@ -1198,7 +1198,7 @@ CAPABILITY_TO_SENSORS: dict[
key=Attribute.COMPLETION_TIME,
translation_key="completion_time",
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=dt_util.parse_datetime,
value_fn=lambda value: dt_util.parse_datetime(value) if value else None,
component_fn=lambda component: component == "sub",
component_translation_key={
"sub": "washer_sub_completion_time",
@@ -145,7 +145,7 @@
"config_entry_not_ready": {
"message": "Error while loading the config entry."
},
"update_error": {
"update_failed": {
"message": "Error while updating data from the API."
}
}
@@ -425,7 +425,7 @@ async def async_setup_entry(
),
supports_response=SupportsResponse.ONLY,
description_placeholders={
"syntax_keys_documentation_url": "http://robotjs.io/docs/syntax#keys"
"syntax_keys_documentation_url": "https://robotjs.dev/docs/syntax#keys"
},
)
@@ -100,7 +100,7 @@ class TedeeApiCoordinator(DataUpdateCoordinator[dict[int, TedeeLock]]):
except TedeeLocalAuthException as ex:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="authentification_failed",
translation_key="authentication_failed",
) from ex
except TedeeDataUpdateException as ex:
@@ -497,7 +497,7 @@
"default": "mdi:battery-clock"
},
"forward_collision_warning": {
"default": "mdi:car-crash",
"default": "mdi:car-emergency",
"state": {
"average": "mdi:alert-circle",
"early": "mdi:alert-octagon",
@@ -634,7 +634,7 @@
"default": "mdi:key"
},
"pedal_position": {
"default": "mdi:pedestal"
"default": "mdi:gauge"
},
"powershare_hours_left": {
"default": "mdi:clock-time-eight-outline"
@@ -794,7 +794,7 @@
"service": "mdi:calendar-plus"
},
"add_precondition_schedule": {
"service": "mdi:hvac-outline"
"service": "mdi:hvac"
},
"navigation_gps_request": {
"service": "mdi:crosshairs-gps"
@@ -803,7 +803,7 @@
"service": "mdi:calendar-minus"
},
"remove_precondition_schedule": {
"service": "mdi:hvac-off-outline"
"service": "mdi:hvac-off"
},
"set_scheduled_charging": {
"service": "mdi:timeline-clock-outline"
@@ -65,7 +65,7 @@
"state": {
"lightning": "mdi:weather-lightning-rainy",
"rain": "mdi:weather-rainy",
"rain_snow": "mdi:weather-snoy-rainy",
"rain_snow": "mdi:weather-snowy-rainy",
"snow": "mdi:weather-snowy"
}
},
+1 -1
View File
@@ -7,6 +7,6 @@
"integration_type": "device",
"iot_class": "local_push",
"quality_scale": "platinum",
"requirements": ["wled==0.22.0"],
"requirements": ["wled==0.23.0"],
"zeroconf": ["_wled._tcp.local."]
}
@@ -195,7 +195,7 @@ class WyomingAssistSatellite(WyomingSatelliteEntity, AssistSatelliteEntity):
return
if event.type == assist_pipeline.PipelineEventType.RUN_START:
if event.data and (tts_output := event.data["tts_output"]):
if event.data and (tts_output := event.data.get("tts_output")):
# Get stream token early.
# If "tts_start_streaming" is True in INTENT_PROGRESS event, we
# can start streaming TTS before the TTS_END event.
+2 -5
View File
@@ -310,11 +310,8 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
config_entry.minor_version,
)
if (config_entry.version, config_entry.minor_version) > (
ZhaConfigFlowHandler.VERSION,
ZhaConfigFlowHandler.MINOR_VERSION,
):
# This means the user has downgraded from a future version
if config_entry.version > ZhaConfigFlowHandler.VERSION:
# This means the user has downgraded from a future major version
return False
if config_entry.version == 1:
+1 -1
View File
@@ -17,7 +17,7 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2026
MINOR_VERSION: Final = 5
PATCH_VERSION: Final = "3"
PATCH_VERSION: Final = "4"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 14, 2)
+1 -1
View File
@@ -2,7 +2,7 @@
aiodhcpwatcher==1.2.1
aiodiscover==2.7.1
aiodns==4.0.3
aiodns==4.0.4
aiogithubapi==26.0.0
aiohttp-asyncmdnsresolver==0.1.1
aiohttp-fast-zlib==0.3.0
+2 -2
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2026.5.3"
version = "2026.5.4"
license = "Apache-2.0"
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
description = "Open-source home automation platform running on Python 3."
@@ -23,7 +23,7 @@ classifiers = [
]
requires-python = ">=3.14.2"
dependencies = [
"aiodns==4.0.3",
"aiodns==4.0.4",
# aiogithubapi is needed by frontend; frontend is unconditionally imported at
# module level in `bootstrap.py` and its requirements thus need to be in
# requirements.txt to ensure they are always installed
+1 -1
View File
@@ -3,7 +3,7 @@
-c homeassistant/package_constraints.txt
# Home Assistant Core
aiodns==4.0.3
aiodns==4.0.4
aiogithubapi==26.0.0
aiohttp-asyncmdnsresolver==0.1.1
aiohttp-fast-zlib==0.3.0
+5 -5
View File
@@ -233,7 +233,7 @@ aiodhcpwatcher==1.2.1
aiodiscover==2.7.1
# homeassistant.components.dnsip
aiodns==4.0.3
aiodns==4.0.4
# homeassistant.components.eafm
aioeafm==0.1.2
@@ -321,7 +321,7 @@ aiolifx==1.2.1
aiolookin==1.0.0
# homeassistant.components.lyric
aiolyric==2.0.2
aiolyric==2.1.1
# homeassistant.components.mealie
aiomealie==1.2.4
@@ -2675,7 +2675,7 @@ python-rabbitair==0.0.8
python-ripple-api==0.0.3
# homeassistant.components.roborock
python-roborock==5.5.1
python-roborock==5.12.0
# homeassistant.components.smarttub
python-smarttub==0.0.47
@@ -2835,7 +2835,7 @@ refoss-ha==1.2.5
regenmaschine==2024.03.0
# homeassistant.components.renault
renault-api==0.5.8
renault-api==0.5.10
# homeassistant.components.renson
renson-endura-delta==1.7.2
@@ -3328,7 +3328,7 @@ wiim==0.1.2
wirelesstagpy==0.8.1
# homeassistant.components.wled
wled==0.22.0
wled==0.23.0
# homeassistant.components.wolflink
wolf-comm==0.0.48
+5 -5
View File
@@ -224,7 +224,7 @@ aiodhcpwatcher==1.2.1
aiodiscover==2.7.1
# homeassistant.components.dnsip
aiodns==4.0.3
aiodns==4.0.4
# homeassistant.components.eafm
aioeafm==0.1.2
@@ -306,7 +306,7 @@ aiolifx==1.2.1
aiolookin==1.0.0
# homeassistant.components.lyric
aiolyric==2.0.2
aiolyric==2.1.1
# homeassistant.components.mealie
aiomealie==1.2.4
@@ -2280,7 +2280,7 @@ python-qube-heatpump==1.8.0
python-rabbitair==0.0.8
# homeassistant.components.roborock
python-roborock==5.5.1
python-roborock==5.12.0
# homeassistant.components.smarttub
python-smarttub==0.0.47
@@ -2419,7 +2419,7 @@ refoss-ha==1.2.5
regenmaschine==2024.03.0
# homeassistant.components.renault
renault-api==0.5.8
renault-api==0.5.10
# homeassistant.components.renson
renson-endura-delta==1.7.2
@@ -2828,7 +2828,7 @@ wiffi==1.1.2
wiim==0.1.2
# homeassistant.components.wled
wled==0.22.0
wled==0.23.0
# homeassistant.components.wolflink
wolf-comm==0.0.48
-2
View File
@@ -285,8 +285,6 @@ FORBIDDEN_PACKAGE_FILES_EXCEPTIONS = {
"lacrosse": {"homeassistant": {"pylacrosse"}},
# ???
"linode": {"homeassistant": {"linode-api"}},
# https://github.com/timmo001/aiolyric
"lyric": {"homeassistant": {"aiolyric"}},
# https://github.com/microBeesTech/pythonSDK/
"microbees": {
"homeassistant": {"microbeespy"},
@@ -1766,6 +1766,12 @@ async def test_create_todo(
},
Task(everyX=5),
),
(
{
ATTR_INTERVAL: 0,
},
Task(everyX=0),
),
(
{
ATTR_FREQUENCY: "weekly",
@@ -2,6 +2,7 @@
from homeassistant.components.homeassistant_connect_zbt2.const import DOMAIN
from homeassistant.components.usb import DOMAIN as USB_DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
@@ -65,3 +66,66 @@ async def test_hardware_info(
}
]
}
async def test_hardware_info_ignored_entry(
hass: HomeAssistant, hass_ws_client: WebSocketGenerator, addon_store_info
) -> None:
"""Test ignored discovery entries don't crash hardware info.
Regression test for https://github.com/home-assistant/core/issues/170270
"""
assert await async_setup_component(hass, USB_DOMAIN, {})
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
# Setup the normal entry so the hardware platform is loaded
normal_entry = MockConfigEntry(
data=CONFIG_ENTRY_DATA,
domain=DOMAIN,
options={},
title="Home Assistant Connect ZBT-2",
unique_id="normal_1",
version=1,
minor_version=1,
)
normal_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(normal_entry.entry_id)
# Setup an ignored config entry without USB data
ignored_entry = MockConfigEntry(
data={},
domain=DOMAIN,
options={},
title="Home Assistant Connect ZBT-2",
unique_id="ignored_1",
version=1,
minor_version=2,
source="ignore",
)
ignored_entry.add_to_hass(hass)
assert ignored_entry.state is ConfigEntryState.NOT_LOADED
client = await hass_ws_client(hass)
await client.send_json({"id": 1, "type": "hardware/info"})
msg = await client.receive_json()
assert msg["id"] == 1
assert msg["success"]
assert msg["result"] == {
"hardware": [
{
"board": None,
"config_entries": [normal_entry.entry_id],
"dongle": {
"vid": "303A",
"pid": "4001",
"serial_number": "80B54EEFAE18",
"manufacturer": "Nabu Casa",
"description": "ZBT-2",
},
"name": "Home Assistant Connect ZBT-2",
"url": "https://support.nabucasa.com/hc/en-us/categories/24734620813469-Home-Assistant-Connect-ZBT-1",
}
]
}
@@ -116,3 +116,30 @@ async def test_get_triggers(
]
assert triggers == unordered(expected_triggers)
async def test_get_triggers_for_removed_device(
hass: HomeAssistant,
mock_bridge_v2: Mock,
v2_resources_test_data: JsonArrayType,
device_registry: dr.DeviceRegistry,
) -> None:
"""Test triggers for a device removed from the bridge.
Regression test for https://github.com/home-assistant/core/issues/152937
"""
await mock_bridge_v2.api.load_test_data(v2_resources_test_data)
await setup_platform(
hass, mock_bridge_v2, [Platform.BINARY_SENSOR, Platform.SENSOR]
)
# Create a device entry with a Hue ID that doesn't exist on the bridge
orphaned_device = device_registry.async_get_or_create(
config_entry_id=mock_bridge_v2.config_entry.entry_id,
identifiers={(hue.DOMAIN, "non-existent-hue-device-id")},
)
triggers = await async_get_device_automations(
hass, DeviceAutomationType.TRIGGER, orphaned_device.id
)
assert triggers == []
@@ -1,9 +1,10 @@
"""Tests for the Lutron Caseta binary sensors."""
from typing import Any
from unittest.mock import AsyncMock
from unittest.mock import AsyncMock, MagicMock
from freezegun.api import FrozenDateTimeFactory
from pylutron_caseta import BridgeResponseError
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from homeassistant.components.lutron_caseta.binary_sensor import SCAN_INTERVAL
@@ -114,3 +115,32 @@ async def test_battery_sensor_updates_on_schedule(
assert unknown_state is not None
assert unknown_state.state == STATE_UNKNOWN
assert instance.get_battery_status.await_count == 3
async def test_battery_sensor_handles_bridge_response_error(
hass: HomeAssistant,
) -> None:
"""Test battery sensor handles BridgeResponseError gracefully.
Regression test for https://github.com/home-assistant/core/issues/169965
"""
instance = MockBridge()
def factory(*args: Any, **kwargs: Any) -> MockBridge:
"""Return the mock bridge instance."""
return instance
mock_response = MagicMock()
mock_response.Header.StatusCode = "404 NotFound"
instance.get_battery_status = AsyncMock(
side_effect=BridgeResponseError(mock_response)
)
await async_setup_integration(hass, factory)
await hass.async_block_till_done()
state = hass.states.get(
"binary_sensor.basement_bedroom_basement_bedroom_left_shade_battery"
)
assert state is not None
assert state.state == STATE_UNKNOWN
@@ -659,6 +659,52 @@ async def test_web_search(
assert mock_create_stream.mock_calls[1][2]["input"][1:] == snapshot
async def test_web_search_remove_citations_gpt5(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_init_component,
mock_create_stream,
mock_chat_log: MockChatLog, # noqa: F811
) -> None:
"""Test that citations are stripped for GPT-5 models with inline_citations disabled."""
subentry = next(iter(mock_config_entry.subentries.values()))
hass.config_entries.async_update_subentry(
mock_config_entry,
subentry,
data={
**subentry.data,
CONF_CHAT_MODEL: "gpt-5-mini",
CONF_WEB_SEARCH: True,
CONF_WEB_SEARCH_INLINE_CITATIONS: False,
},
)
await hass.config_entries.async_reload(mock_config_entry.entry_id)
message = [
"The match ended 0-2",
" ([legaseriea.it](https://www.legaseriea.it/))",
".",
]
mock_create_stream.return_value = [
(
*create_web_search_item(id="ws_A", output_index=0),
*create_message_item(id="msg_A", text=message, output_index=1),
)
]
result = await conversation.async_converse(
hass,
"What was the score?",
mock_chat_log.conversation_id,
Context(),
agent_id="conversation.openai_conversation",
)
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
# Citation should be stripped from the response
assert result.response.speech["plain"]["speech"] == "The match ended 0-2."
async def test_code_interpreter(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
@@ -116,3 +116,54 @@ async def test_host_updated(hass: HomeAssistant) -> None:
assert result["reason"] == "already_configured"
assert entry.data[CONF_HOST] == MOCK_SSDP_LOCATION
async def test_ssdp_udn_as_list(hass: HomeAssistant) -> None:
"""Test SSDP discovery when UDN is a list instead of a string.
Regression test for https://github.com/home-assistant/core/issues/171837
"""
list_udn_discovery = SsdpServiceInfo(
ssdp_usn="usn",
ssdp_st="st",
ssdp_location=MOCK_SSDP_LOCATION,
upnp={
ATTR_UPNP_FRIENDLY_NAME: MOCK_FRIENDLY_NAME,
ATTR_UPNP_UDN: [MOCK_UDN, "uuid:other"],
},
)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={CONF_SOURCE: SOURCE_SSDP},
data=list_udn_discovery,
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "confirm"
assert result["description_placeholders"] == {CONF_NAME: MOCK_FRIENDLY_NAME}
result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result2["type"] is FlowResultType.CREATE_ENTRY
assert result2["title"] == MOCK_FRIENDLY_NAME
assert result2["data"] == {CONF_HOST: MOCK_SSDP_LOCATION}
async def test_ssdp_udn_as_empty_list(hass: HomeAssistant) -> None:
"""Test SSDP discovery when UDN is an empty list."""
empty_udn_discovery = SsdpServiceInfo(
ssdp_usn="usn",
ssdp_st="st",
ssdp_location=MOCK_SSDP_LOCATION,
upnp={
ATTR_UPNP_FRIENDLY_NAME: MOCK_FRIENDLY_NAME,
ATTR_UPNP_UDN: [],
},
)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={CONF_SOURCE: SOURCE_SSDP},
data=empty_udn_discovery,
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "incomplete_discovery"
@@ -999,106 +999,6 @@
'state': 'unknown',
})
# ---
# name: test_buttons[captur_fuel][button.reg_captur_fuel_flash_lights-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': None,
'entity_id': 'button.reg_captur_fuel_flash_lights',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Flash lights',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Flash lights',
'platform': 'renault',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'flash_lights',
'unique_id': 'vf1capturfuelvin_flash_lights',
'unit_of_measurement': None,
})
# ---
# name: test_buttons[captur_fuel][button.reg_captur_fuel_flash_lights-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'REG-CAPTUR-FUEL Flash lights',
}),
'context': <ANY>,
'entity_id': 'button.reg_captur_fuel_flash_lights',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_buttons[captur_fuel][button.reg_captur_fuel_sound_horn-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': None,
'entity_id': 'button.reg_captur_fuel_sound_horn',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Sound horn',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Sound horn',
'platform': 'renault',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'sound_horn',
'unique_id': 'vf1capturfuelvin_sound_horn',
'unit_of_measurement': None,
})
# ---
# name: test_buttons[captur_fuel][button.reg_captur_fuel_sound_horn-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'REG-CAPTUR-FUEL Sound horn',
}),
'context': <ANY>,
'entity_id': 'button.reg_captur_fuel_sound_horn',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_buttons[captur_fuel][button.reg_captur_fuel_start_air_conditioner-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
@@ -1149,106 +1049,6 @@
'state': 'unknown',
})
# ---
# name: test_buttons[captur_phev][button.reg_captur_phev_flash_lights-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': None,
'entity_id': 'button.reg_captur_phev_flash_lights',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Flash lights',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Flash lights',
'platform': 'renault',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'flash_lights',
'unique_id': 'vf1capturphevvin_flash_lights',
'unit_of_measurement': None,
})
# ---
# name: test_buttons[captur_phev][button.reg_captur_phev_flash_lights-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'REG-CAPTUR_PHEV Flash lights',
}),
'context': <ANY>,
'entity_id': 'button.reg_captur_phev_flash_lights',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_buttons[captur_phev][button.reg_captur_phev_sound_horn-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': None,
'entity_id': 'button.reg_captur_phev_sound_horn',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Sound horn',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Sound horn',
'platform': 'renault',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'sound_horn',
'unique_id': 'vf1capturphevvin_sound_horn',
'unit_of_measurement': None,
})
# ---
# name: test_buttons[captur_phev][button.reg_captur_phev_sound_horn-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'REG-CAPTUR_PHEV Sound horn',
}),
'context': <ANY>,
'entity_id': 'button.reg_captur_phev_sound_horn',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_buttons[captur_phev][button.reg_captur_phev_start_air_conditioner-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
@@ -1299,106 +1099,6 @@
'state': 'unknown',
})
# ---
# name: test_buttons[captur_phev][button.reg_captur_phev_start_charge-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': None,
'entity_id': 'button.reg_captur_phev_start_charge',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Start charge',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Start charge',
'platform': 'renault',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'start_charge',
'unique_id': 'vf1capturphevvin_start_charge',
'unit_of_measurement': None,
})
# ---
# name: test_buttons[captur_phev][button.reg_captur_phev_start_charge-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'REG-CAPTUR_PHEV Start charge',
}),
'context': <ANY>,
'entity_id': 'button.reg_captur_phev_start_charge',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_buttons[captur_phev][button.reg_captur_phev_stop_charge-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': None,
'entity_id': 'button.reg_captur_phev_stop_charge',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Stop charge',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Stop charge',
'platform': 'renault',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'stop_charge',
'unique_id': 'vf1capturphevvin_stop_charge',
'unit_of_measurement': None,
})
# ---
# name: test_buttons[captur_phev][button.reg_captur_phev_stop_charge-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'REG-CAPTUR_PHEV Stop charge',
}),
'context': <ANY>,
'entity_id': 'button.reg_captur_phev_stop_charge',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_buttons[megane_e_tech][button.reg_meg_0_flash_lights-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
@@ -1753,7 +1753,10 @@
'capabilities': dict({
'options': list([
'ok',
'no_dustbin_or_filter',
'auto_empty_dock_fan_error',
'duct_blockage',
'auto_empty_dock_voltage_error',
'water_empty',
'waste_water_tank_full',
'maintenance_brush_jammed',
@@ -1799,7 +1802,10 @@
'friendly_name': 'Roborock S7 2 Dock Dock error',
'options': list([
'ok',
'no_dustbin_or_filter',
'auto_empty_dock_fan_error',
'duct_blockage',
'auto_empty_dock_voltage_error',
'water_empty',
'waste_water_tank_full',
'maintenance_brush_jammed',
@@ -2958,7 +2964,10 @@
'capabilities': dict({
'options': list([
'ok',
'no_dustbin_or_filter',
'auto_empty_dock_fan_error',
'duct_blockage',
'auto_empty_dock_voltage_error',
'water_empty',
'waste_water_tank_full',
'maintenance_brush_jammed',
@@ -3004,7 +3013,10 @@
'friendly_name': 'Roborock S7 MaxV Dock Dock error',
'options': list([
'ok',
'no_dustbin_or_filter',
'auto_empty_dock_fan_error',
'duct_blockage',
'auto_empty_dock_voltage_error',
'water_empty',
'waste_water_tank_full',
'maintenance_brush_jammed',
+100 -9
View File
@@ -1,6 +1,7 @@
"""Tests for Shelly media player platform."""
from copy import deepcopy
from http import HTTPStatus
from unittest.mock import Mock
from aioshelly.const import MODEL_WALL_DISPLAY
@@ -367,7 +368,7 @@ async def test_get_image_http(
await init_integration(hass, 2, model=MODEL_WALL_DISPLAY)
state = hass.states.get(ENTITY_ID)
assert (state := hass.states.get(ENTITY_ID)) is not None
assert "entity_picture_local" not in state.attributes
client = await hass_client_no_auth()
@@ -378,13 +379,54 @@ async def test_get_image_http(
assert isinstance(content, bytes)
async def test_get_image_http_base64_decode_error(
@pytest.mark.parametrize(
"invalid_thumb",
[
"data:image/webp;base64,0",
"data invalid",
"data:video/mpg;base64,AAAA",
],
)
async def test_get_image_http_stale_url_after_thumb_invalidated(
hass: HomeAssistant,
mock_rpc_device: Mock,
monkeypatch: pytest.MonkeyPatch,
hass_client_no_auth: ClientSessionGenerator,
invalid_thumb: str,
) -> None:
"""Test get image via http command base64 decode error."""
"""Test image proxy with a stale URL after the thumb becomes invalid."""
status = deepcopy(mock_rpc_device.status)
status["media"] = STATUS_AUDIO_FILE
monkeypatch.setattr(mock_rpc_device, "status", status)
await init_integration(hass, 2, model=MODEL_WALL_DISPLAY)
assert (state := hass.states.get(ENTITY_ID)) is not None
entity_picture = state.attributes["entity_picture"]
monkeypatch.setitem(
mock_rpc_device.status["media"]["playback"]["media_meta"],
"thumb",
invalid_thumb,
)
mock_rpc_device.mock_update()
await hass.async_block_till_done()
assert (state := hass.states.get(ENTITY_ID)) is not None
assert "entity_picture" not in state.attributes
client = await hass_client_no_auth()
resp = await client.get(entity_picture)
assert resp.status == HTTPStatus.INTERNAL_SERVER_ERROR
async def test_entity_picture_absent_base64_data_invalid(
hass: HomeAssistant,
mock_rpc_device: Mock,
monkeypatch: pytest.MonkeyPatch,
hass_client: ClientSessionGenerator,
) -> None:
"""Test that entity_picture is absent when base64 data is invalid."""
status = deepcopy(mock_rpc_device.status)
status["media"] = STATUS_AUDIO_FILE
status["media"]["playback"]["media_meta"]["thumb"] = "data:image/webp;base64,0"
@@ -392,15 +434,64 @@ async def test_get_image_http_base64_decode_error(
await init_integration(hass, 2, model=MODEL_WALL_DISPLAY)
state = hass.states.get(ENTITY_ID)
assert "entity_picture_local" not in state.attributes
assert (state := hass.states.get(ENTITY_ID)) is not None
assert "entity_picture" not in state.attributes
client = await hass_client_no_auth()
client = await hass_client()
resp = await client.get(f"/api/media_player_proxy/{ENTITY_ID}")
assert resp.status == HTTPStatus.INTERNAL_SERVER_ERROR
resp = await client.get(state.attributes["entity_picture"])
content = await resp.read()
assert isinstance(content, bytes)
@pytest.mark.parametrize(
"invalid_thumb",
[
"data invalid",
"lorem ipsum",
],
)
async def test_entity_picture_absent_thumb_string_invalid(
hass: HomeAssistant,
mock_rpc_device: Mock,
monkeypatch: pytest.MonkeyPatch,
hass_client: ClientSessionGenerator,
invalid_thumb: str,
) -> None:
"""Test that entity_picture is absent when thumb string has invalid format."""
status = deepcopy(mock_rpc_device.status)
status["media"] = STATUS_AUDIO_FILE
status["media"]["playback"]["media_meta"]["thumb"] = invalid_thumb
monkeypatch.setattr(mock_rpc_device, "status", status)
await init_integration(hass, 2, model=MODEL_WALL_DISPLAY)
assert (state := hass.states.get(ENTITY_ID)) is not None
assert "entity_picture" not in state.attributes
client = await hass_client()
resp = await client.get(f"/api/media_player_proxy/{ENTITY_ID}")
assert resp.status == HTTPStatus.INTERNAL_SERVER_ERROR
async def test_entity_picture_absent_mime_type_not_allowed(
hass: HomeAssistant,
mock_rpc_device: Mock,
monkeypatch: pytest.MonkeyPatch,
hass_client: ClientSessionGenerator,
) -> None:
"""Test that entity_picture is absent when MIME type is not allowed."""
status = deepcopy(mock_rpc_device.status)
status["media"] = STATUS_AUDIO_FILE
status["media"]["playback"]["media_meta"]["thumb"] = "data:video/mpg;base64,0"
monkeypatch.setattr(mock_rpc_device, "status", status)
await init_integration(hass, 2, model=MODEL_WALL_DISPLAY)
assert (state := hass.states.get(ENTITY_ID)) is not None
assert "entity_picture" not in state.attributes
client = await hass_client()
resp = await client.get(f"/api/media_player_proxy/{ENTITY_ID}")
assert resp.status == HTTPStatus.INTERNAL_SERVER_ERROR
async def test_rpc_media_player_browse_media_root(
@@ -20,7 +20,6 @@
'111': 'Chunchun',
'112': 'Dancing Shadows',
'113': 'Washing Machine',
'114': 'RSVD',
'115': 'Blends',
'116': 'TV Simulator',
'117': 'Dynamic Smooth',
@@ -51,7 +50,6 @@
'14': 'Theater Rainbow',
'140': 'Waterfall',
'141': 'Freqpixels',
'142': 'RSVD',
'143': 'Noisefire',
'144': 'Puddlepeak',
'145': 'Noisemove',
@@ -61,7 +59,6 @@
'149': 'Firenoise',
'15': 'Running',
'150': 'Squared Swirl',
'151': 'RSVD',
'152': 'DNA',
'153': 'Matrix',
'154': 'Metaballs',
@@ -72,7 +69,6 @@
'159': 'DJ Light',
'16': 'Saw',
'160': 'Funky Plank',
'161': 'RSVD',
'162': 'Pulser',
'163': 'Blurz',
'164': 'Drift',
@@ -80,10 +76,7 @@
'166': 'Sun Radiation',
'167': 'Colored Bursts',
'168': 'Julia',
'169': 'RSVD',
'17': 'Twinkle',
'170': 'RSVD',
'171': 'RSVD',
'172': 'Game Of Life',
'173': 'Tartan',
'174': 'Polar Lights',
@@ -138,7 +131,6 @@
'50': 'Two Dots',
'51': 'Fairytwinkle',
'52': 'Running Dual',
'53': 'RSVD',
'54': 'Chase 3',
'55': 'Tri Wipe',
'56': 'Tri Fade',
@@ -227,6 +219,7 @@
'product': 'FOSS',
'str': False,
'udpport': 21324,
'umpalcount': 0,
'uptime': 966,
'ver': '0.14.4',
'vid': '2405180',
@@ -60,7 +60,6 @@
'Two Dots',
'Fairytwinkle',
'Running Dual',
'RSVD',
'Chase 3',
'Tri Wipe',
'Tri Fade',
@@ -149,7 +148,6 @@
'GEQ',
'Waterfall',
'Freqpixels',
'RSVD',
'Noisefire',
'Puddlepeak',
'Noisemove',
@@ -158,7 +156,6 @@
'Ripple Peak',
'Firenoise',
'Squared Swirl',
'RSVD',
'DNA',
'Matrix',
'Metaballs',
@@ -168,7 +165,6 @@
'Gravfreq',
'DJ Light',
'Funky Plank',
'RSVD',
'Pulser',
'Blurz',
'Drift',
@@ -176,9 +172,6 @@
'Sun Radiation',
'Colored Bursts',
'Julia',
'RSVD',
'RSVD',
'RSVD',
'Game Of Life',
'Tartan',
'Polar Lights',
@@ -293,7 +286,6 @@
'Two Dots',
'Fairytwinkle',
'Running Dual',
'RSVD',
'Chase 3',
'Tri Wipe',
'Tri Fade',
@@ -382,7 +374,6 @@
'GEQ',
'Waterfall',
'Freqpixels',
'RSVD',
'Noisefire',
'Puddlepeak',
'Noisemove',
@@ -391,7 +382,6 @@
'Ripple Peak',
'Firenoise',
'Squared Swirl',
'RSVD',
'DNA',
'Matrix',
'Metaballs',
@@ -401,7 +391,6 @@
'Gravfreq',
'DJ Light',
'Funky Plank',
'RSVD',
'Pulser',
'Blurz',
'Drift',
@@ -409,9 +398,6 @@
'Sun Radiation',
'Colored Bursts',
'Julia',
'RSVD',
'RSVD',
'RSVD',
'Game Of Life',
'Tartan',
'Polar Lights',
@@ -520,7 +506,6 @@
'Two Dots',
'Fairytwinkle',
'Running Dual',
'RSVD',
'Chase 3',
'Tri Wipe',
'Tri Fade',
@@ -581,7 +566,6 @@
'Chunchun',
'Dancing Shadows',
'Washing Machine',
'RSVD',
'Blends',
'TV Simulator',
'Dynamic Smooth',
@@ -609,7 +593,6 @@
'GEQ',
'Waterfall',
'Freqpixels',
'RSVD',
'Noisefire',
'Puddlepeak',
'Noisemove',
@@ -618,7 +601,6 @@
'Ripple Peak',
'Firenoise',
'Squared Swirl',
'RSVD',
'DNA',
'Matrix',
'Metaballs',
@@ -628,7 +610,6 @@
'Gravfreq',
'DJ Light',
'Funky Plank',
'RSVD',
'Pulser',
'Blurz',
'Drift',
@@ -636,9 +617,6 @@
'Sun Radiation',
'Colored Bursts',
'Julia',
'RSVD',
'RSVD',
'RSVD',
'Game Of Life',
'Tartan',
'Polar Lights',
@@ -749,7 +727,6 @@
'Two Dots',
'Fairytwinkle',
'Running Dual',
'RSVD',
'Chase 3',
'Tri Wipe',
'Tri Fade',
@@ -810,7 +787,6 @@
'Chunchun',
'Dancing Shadows',
'Washing Machine',
'RSVD',
'Blends',
'TV Simulator',
'Dynamic Smooth',
@@ -838,7 +814,6 @@
'GEQ',
'Waterfall',
'Freqpixels',
'RSVD',
'Noisefire',
'Puddlepeak',
'Noisemove',
@@ -847,7 +822,6 @@
'Ripple Peak',
'Firenoise',
'Squared Swirl',
'RSVD',
'DNA',
'Matrix',
'Metaballs',
@@ -857,7 +831,6 @@
'Gravfreq',
'DJ Light',
'Funky Plank',
'RSVD',
'Pulser',
'Blurz',
'Drift',
@@ -865,9 +838,6 @@
'Sun Radiation',
'Colored Bursts',
'Julia',
'RSVD',
'RSVD',
'RSVD',
'Game Of Life',
'Tartan',
'Polar Lights',
@@ -1032,7 +1002,6 @@
'Two Dots',
'Fairytwinkle',
'Running Dual',
'RSVD',
'Chase 3',
'Tri Wipe',
'Tri Fade',
@@ -1093,7 +1062,6 @@
'Chunchun',
'Dancing Shadows',
'Washing Machine',
'RSVD',
'Blends',
'TV Simulator',
'Dynamic Smooth',
@@ -1121,7 +1089,6 @@
'GEQ',
'Waterfall',
'Freqpixels',
'RSVD',
'Noisefire',
'Puddlepeak',
'Noisemove',
@@ -1130,7 +1097,6 @@
'Ripple Peak',
'Firenoise',
'Squared Swirl',
'RSVD',
'DNA',
'Matrix',
'Metaballs',
@@ -1140,7 +1106,6 @@
'Gravfreq',
'DJ Light',
'Funky Plank',
'RSVD',
'Pulser',
'Blurz',
'Drift',
@@ -1148,9 +1113,6 @@
'Sun Radiation',
'Colored Bursts',
'Julia',
'RSVD',
'RSVD',
'RSVD',
'Game Of Life',
'Tartan',
'Polar Lights',
@@ -1261,7 +1223,6 @@
'Two Dots',
'Fairytwinkle',
'Running Dual',
'RSVD',
'Chase 3',
'Tri Wipe',
'Tri Fade',
@@ -1322,7 +1283,6 @@
'Chunchun',
'Dancing Shadows',
'Washing Machine',
'RSVD',
'Blends',
'TV Simulator',
'Dynamic Smooth',
@@ -1350,7 +1310,6 @@
'GEQ',
'Waterfall',
'Freqpixels',
'RSVD',
'Noisefire',
'Puddlepeak',
'Noisemove',
@@ -1359,7 +1318,6 @@
'Ripple Peak',
'Firenoise',
'Squared Swirl',
'RSVD',
'DNA',
'Matrix',
'Metaballs',
@@ -1369,7 +1327,6 @@
'Gravfreq',
'DJ Light',
'Funky Plank',
'RSVD',
'Pulser',
'Blurz',
'Drift',
@@ -1377,9 +1334,6 @@
'Sun Radiation',
'Colored Bursts',
'Julia',
'RSVD',
'RSVD',
'RSVD',
'Game Of Life',
'Tartan',
'Polar Lights',
@@ -1484,7 +1438,6 @@
'Two Dots',
'Fairytwinkle',
'Running Dual',
'RSVD',
'Chase 3',
'Tri Wipe',
'Tri Fade',
@@ -1545,7 +1498,6 @@
'Chunchun',
'Dancing Shadows',
'Washing Machine',
'RSVD',
'Blends',
'TV Simulator',
'Dynamic Smooth',
@@ -1573,7 +1525,6 @@
'GEQ',
'Waterfall',
'Freqpixels',
'RSVD',
'Noisefire',
'Puddlepeak',
'Noisemove',
@@ -1582,7 +1533,6 @@
'Ripple Peak',
'Firenoise',
'Squared Swirl',
'RSVD',
'DNA',
'Matrix',
'Metaballs',
@@ -1592,7 +1542,6 @@
'Gravfreq',
'DJ Light',
'Funky Plank',
'RSVD',
'Pulser',
'Blurz',
'Drift',
@@ -1600,9 +1549,6 @@
'Sun Radiation',
'Colored Bursts',
'Julia',
'RSVD',
'RSVD',
'RSVD',
'Game Of Life',
'Tartan',
'Polar Lights',
@@ -1713,7 +1659,6 @@
'Two Dots',
'Fairytwinkle',
'Running Dual',
'RSVD',
'Chase 3',
'Tri Wipe',
'Tri Fade',
@@ -1774,7 +1719,6 @@
'Chunchun',
'Dancing Shadows',
'Washing Machine',
'RSVD',
'Blends',
'TV Simulator',
'Dynamic Smooth',
@@ -1802,7 +1746,6 @@
'GEQ',
'Waterfall',
'Freqpixels',
'RSVD',
'Noisefire',
'Puddlepeak',
'Noisemove',
@@ -1811,7 +1754,6 @@
'Ripple Peak',
'Firenoise',
'Squared Swirl',
'RSVD',
'DNA',
'Matrix',
'Metaballs',
@@ -1821,7 +1763,6 @@
'Gravfreq',
'DJ Light',
'Funky Plank',
'RSVD',
'Pulser',
'Blurz',
'Drift',
@@ -1829,9 +1770,6 @@
'Sun Radiation',
'Colored Bursts',
'Julia',
'RSVD',
'RSVD',
'RSVD',
'Game Of Life',
'Tartan',
'Polar Lights',
@@ -1936,7 +1874,6 @@
'Two Dots',
'Fairytwinkle',
'Running Dual',
'RSVD',
'Chase 3',
'Tri Wipe',
'Tri Fade',
@@ -1997,7 +1934,6 @@
'Chunchun',
'Dancing Shadows',
'Washing Machine',
'RSVD',
'Blends',
'TV Simulator',
'Dynamic Smooth',
@@ -2025,7 +1961,6 @@
'GEQ',
'Waterfall',
'Freqpixels',
'RSVD',
'Noisefire',
'Puddlepeak',
'Noisemove',
@@ -2034,7 +1969,6 @@
'Ripple Peak',
'Firenoise',
'Squared Swirl',
'RSVD',
'DNA',
'Matrix',
'Metaballs',
@@ -2044,7 +1978,6 @@
'Gravfreq',
'DJ Light',
'Funky Plank',
'RSVD',
'Pulser',
'Blurz',
'Drift',
@@ -2052,9 +1985,6 @@
'Sun Radiation',
'Colored Bursts',
'Julia',
'RSVD',
'RSVD',
'RSVD',
'Game Of Life',
'Tartan',
'Polar Lights',
@@ -2165,7 +2095,6 @@
'Two Dots',
'Fairytwinkle',
'Running Dual',
'RSVD',
'Chase 3',
'Tri Wipe',
'Tri Fade',
@@ -2226,7 +2155,6 @@
'Chunchun',
'Dancing Shadows',
'Washing Machine',
'RSVD',
'Blends',
'TV Simulator',
'Dynamic Smooth',
@@ -2254,7 +2182,6 @@
'GEQ',
'Waterfall',
'Freqpixels',
'RSVD',
'Noisefire',
'Puddlepeak',
'Noisemove',
@@ -2263,7 +2190,6 @@
'Ripple Peak',
'Firenoise',
'Squared Swirl',
'RSVD',
'DNA',
'Matrix',
'Metaballs',
@@ -2273,7 +2199,6 @@
'Gravfreq',
'DJ Light',
'Funky Plank',
'RSVD',
'Pulser',
'Blurz',
'Drift',
@@ -2281,9 +2206,6 @@
'Sun Radiation',
'Colored Bursts',
'Julia',
'RSVD',
'RSVD',
'RSVD',
'Game Of Life',
'Tartan',
'Polar Lights',
@@ -2388,7 +2310,6 @@
'Two Dots',
'Fairytwinkle',
'Running Dual',
'RSVD',
'Chase 3',
'Tri Wipe',
'Tri Fade',
@@ -2449,7 +2370,6 @@
'Chunchun',
'Dancing Shadows',
'Washing Machine',
'RSVD',
'Blends',
'TV Simulator',
'Dynamic Smooth',
@@ -2477,7 +2397,6 @@
'GEQ',
'Waterfall',
'Freqpixels',
'RSVD',
'Noisefire',
'Puddlepeak',
'Noisemove',
@@ -2486,7 +2405,6 @@
'Ripple Peak',
'Firenoise',
'Squared Swirl',
'RSVD',
'DNA',
'Matrix',
'Metaballs',
@@ -2496,7 +2414,6 @@
'Gravfreq',
'DJ Light',
'Funky Plank',
'RSVD',
'Pulser',
'Blurz',
'Drift',
@@ -2504,9 +2421,6 @@
'Sun Radiation',
'Colored Bursts',
'Julia',
'RSVD',
'RSVD',
'RSVD',
'Game Of Life',
'Tartan',
'Polar Lights',
@@ -2617,7 +2531,6 @@
'Two Dots',
'Fairytwinkle',
'Running Dual',
'RSVD',
'Chase 3',
'Tri Wipe',
'Tri Fade',
@@ -2678,7 +2591,6 @@
'Chunchun',
'Dancing Shadows',
'Washing Machine',
'RSVD',
'Blends',
'TV Simulator',
'Dynamic Smooth',
@@ -2706,7 +2618,6 @@
'GEQ',
'Waterfall',
'Freqpixels',
'RSVD',
'Noisefire',
'Puddlepeak',
'Noisemove',
@@ -2715,7 +2626,6 @@
'Ripple Peak',
'Firenoise',
'Squared Swirl',
'RSVD',
'DNA',
'Matrix',
'Metaballs',
@@ -2725,7 +2635,6 @@
'Gravfreq',
'DJ Light',
'Funky Plank',
'RSVD',
'Pulser',
'Blurz',
'Drift',
@@ -2733,9 +2642,6 @@
'Sun Radiation',
'Colored Bursts',
'Julia',
'RSVD',
'RSVD',
'RSVD',
'Game Of Life',
'Tartan',
'Polar Lights',
@@ -820,6 +820,57 @@ async def test_on_pipeline_event_ignores_disconnected_client(
assert not mock_client.error_event.is_set()
async def test_run_start_without_tts(
hass: HomeAssistant,
) -> None:
"""Test RUN_START event without tts_output does not crash.
Regression test for https://github.com/home-assistant/core/issues/165734
"""
events: list[Event] = [
RunPipeline(
start_stage=PipelineStage.WAKE, end_stage=PipelineStage.TTS
).event(),
]
pipeline_event = asyncio.Event()
def _async_pipeline_from_audio_stream(*args: Any, **kwargs: Any) -> None:
pipeline_event.set()
with (
patch(
"homeassistant.components.wyoming.data.load_wyoming_info",
return_value=SATELLITE_INFO,
),
patch(
"homeassistant.components.wyoming.assist_satellite.AsyncTcpClient",
SatelliteAsyncTcpClient(events),
) as mock_client,
patch(
"homeassistant.components.assist_satellite.entity.async_pipeline_from_audio_stream",
wraps=_async_pipeline_from_audio_stream,
) as mock_run_pipeline,
):
await setup_config_entry(hass)
async with asyncio.timeout(1):
await pipeline_event.wait()
await mock_client.connect_event.wait()
await mock_client.run_satellite_event.wait()
event_callback = mock_run_pipeline.call_args.kwargs["event_callback"]
# Fire RUN_START without tts_output (TTS not configured)
# must not raise KeyError
event_callback(
assist_pipeline.PipelineEvent(
assist_pipeline.PipelineEventType.RUN_START,
{"pipeline": "test", "language": "en"},
)
)
async def test_announce_raises_when_client_disconnected(
hass: HomeAssistant,
) -> None:
+34
View File
@@ -21,6 +21,7 @@ from homeassistant.components.homeassistant_hardware.helpers import (
async_register_firmware_update_in_progress,
)
from homeassistant.components.usb import USBDevice
from homeassistant.components.zha.config_flow import ZhaConfigFlowHandler
from homeassistant.components.zha.const import (
CONF_BAUDRATE,
CONF_FLOW_CONTROL,
@@ -181,6 +182,39 @@ async def test_migration_v5_explicit_socket_port(
assert config_entry.data[CONF_DEVICE][CONF_DEVICE_PATH] == new_path
@pytest.mark.parametrize(
("version_delta", "minor_delta", "expected_state"),
[
pytest.param(0, 1, ConfigEntryState.LOADED, id="minor_allowed"),
pytest.param(1, 0, ConfigEntryState.MIGRATION_ERROR, id="major_blocked"),
],
)
@patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True))
async def test_migration_version_downgrade_guard(
version_delta: int,
minor_delta: int,
expected_state: ConfigEntryState,
hass: HomeAssistant,
config_entry: MockConfigEntry,
) -> None:
"""Test the config version downgrade guard."""
future_version = ZhaConfigFlowHandler.VERSION + version_delta
future_minor_version = ZhaConfigFlowHandler.MINOR_VERSION + minor_delta
config_entry.add_to_hass(hass)
hass.config_entries.async_update_entry(
config_entry,
version=future_version,
minor_version=future_minor_version,
)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is expected_state
assert config_entry.version == future_version
assert config_entry.minor_version == future_minor_version
@pytest.mark.parametrize(
(
"radio_type",