diff --git a/homeassistant/components/alarmdecoder/icons.json b/homeassistant/components/alarmdecoder/icons.json index ccb89749d2d8..4a50e43d2776 100644 --- a/homeassistant/components/alarmdecoder/icons.json +++ b/homeassistant/components/alarmdecoder/icons.json @@ -11,7 +11,7 @@ "service": "mdi:dialpad" }, "alarm_toggle_chime": { - "service": "mdi:abc" + "service": "mdi:bell-ring" } } } diff --git a/homeassistant/components/alexa_devices/strings.json b/homeassistant/components/alexa_devices/strings.json index 48868b522810..29a62e185544 100644 --- a/homeassistant/components/alexa_devices/strings.json +++ b/homeassistant/components/alexa_devices/strings.json @@ -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}" }, diff --git a/homeassistant/components/comelit/config_flow.py b/homeassistant/components/comelit/config_flow.py index abb6164fcfed..7ba50ad7cb51 100644 --- a/homeassistant/components/comelit/config_flow.py +++ b/homeassistant/components/comelit/config_flow.py @@ -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() diff --git a/homeassistant/components/currencylayer/sensor.py b/homeassistant/components/currencylayer/sensor.py index 832a856f51a9..ad1481a4e7c1 100644 --- a/homeassistant/components/currencylayer/sensor.py +++ b/homeassistant/components/currencylayer/sensor.py @@ -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.""" diff --git a/homeassistant/components/dnsip/manifest.json b/homeassistant/components/dnsip/manifest.json index b383ccd9049d..04c5699b9aa7 100644 --- a/homeassistant/components/dnsip/manifest.json +++ b/homeassistant/components/dnsip/manifest.json @@ -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"] } diff --git a/homeassistant/components/evohome/icons.json b/homeassistant/components/evohome/icons.json index fa688c66a569..19ab3f7951d1 100644 --- a/homeassistant/components/evohome/icons.json +++ b/homeassistant/components/evohome/icons.json @@ -23,7 +23,7 @@ "service": "mdi:refresh" }, "set_dhw_override": { - "service": "mdi:water-heater" + "service": "mdi:water-boiler" }, "set_system_mode": { "service": "mdi:pencil" diff --git a/homeassistant/components/fing/utils.py b/homeassistant/components/fing/utils.py index a37733687ad1..97e1ae675ff3 100644 --- a/homeassistant/components/fing/utils.py +++ b/homeassistant/components/fing/utils.py @@ -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" diff --git a/homeassistant/components/fumis/icons.json b/homeassistant/components/fumis/icons.json index f8298d2b1da6..8370e26f3143 100644 --- a/homeassistant/components/fumis/icons.json +++ b/homeassistant/components/fumis/icons.json @@ -2,7 +2,7 @@ "entity": { "button": { "sync_clock": { - "default": "mdi:clock-sync" + "default": "mdi:clock-check" } }, "number": { diff --git a/homeassistant/components/habitica/services.py b/homeassistant/components/habitica/services.py index ee909d1177de..85e7c87e3cc7 100644 --- a/homeassistant/components/habitica/services.py +++ b/homeassistant/components/habitica/services.py @@ -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: diff --git a/homeassistant/components/homeassistant_connect_zbt2/hardware.py b/homeassistant/components/homeassistant_connect_zbt2/hardware.py index 0d45e0554076..76355b1e281d 100644 --- a/homeassistant/components/homeassistant_connect_zbt2/hardware.py +++ b/homeassistant/components/homeassistant_connect_zbt2/hardware.py @@ -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, diff --git a/homeassistant/components/hue/v2/device_trigger.py b/homeassistant/components/hue/v2/device_trigger.py index c35093a9f9c6..10ac11ea0282 100644 --- a/homeassistant/components/hue/v2/device_trigger.py +++ b/homeassistant/components/hue/v2/device_trigger.py @@ -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 diff --git a/homeassistant/components/hunterdouglas_powerview/cover.py b/homeassistant/components/hunterdouglas_powerview/cover.py index b78d0be08653..3d12a9430dfc 100644 --- a/homeassistant/components/hunterdouglas_powerview/cover.py +++ b/homeassistant/components/hunterdouglas_powerview/cover.py @@ -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.""" diff --git a/homeassistant/components/insteon/__init__.py b/homeassistant/components/insteon/__init__.py index de0177a4f970..18f4c808345b 100644 --- a/homeassistant/components/insteon/__init__.py +++ b/homeassistant/components/insteon/__init__.py @@ -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" ) diff --git a/homeassistant/components/jewish_calendar/icons.json b/homeassistant/components/jewish_calendar/icons.json index b3997bcc9e8f..75d370b09199 100644 --- a/homeassistant/components/jewish_calendar/icons.json +++ b/homeassistant/components/jewish_calendar/icons.json @@ -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" } }, diff --git a/homeassistant/components/keba/icons.json b/homeassistant/components/keba/icons.json index 7a07cceab4af..c084eec230f6 100644 --- a/homeassistant/components/keba/icons.json +++ b/homeassistant/components/keba/icons.json @@ -7,7 +7,7 @@ "service": "mdi:lock-open" }, "disable": { - "service": "mdi:fash-off" + "service": "mdi:flash-off" }, "enable": { "service": "mdi:flash" diff --git a/homeassistant/components/liebherr/icons.json b/homeassistant/components/liebherr/icons.json index 0aa3f37c7e2b..f94cfab3a581 100644 --- a/homeassistant/components/liebherr/icons.json +++ b/homeassistant/components/liebherr/icons.json @@ -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" } } }, diff --git a/homeassistant/components/lutron_caseta/binary_sensor.py b/homeassistant/components/lutron_caseta/binary_sensor.py index d643141bd6a6..03099473a649 100644 --- a/homeassistant/components/lutron_caseta/binary_sensor.py +++ b/homeassistant/components/lutron_caseta/binary_sensor.py @@ -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 diff --git a/homeassistant/components/lyric/const.py b/homeassistant/components/lyric/const.py index 6d973b56181c..3acc4b008108 100644 --- a/homeassistant/components/lyric/const.py +++ b/homeassistant/components/lyric/const.py @@ -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" diff --git a/homeassistant/components/lyric/manifest.json b/homeassistant/components/lyric/manifest.json index 62e5683722f2..2a935c09f2dd 100644 --- a/homeassistant/components/lyric/manifest.json +++ b/homeassistant/components/lyric/manifest.json @@ -22,5 +22,5 @@ "integration_type": "hub", "iot_class": "cloud_polling", "loggers": ["aiolyric"], - "requirements": ["aiolyric==2.0.2"] + "requirements": ["aiolyric==2.1.1"] } diff --git a/homeassistant/components/matter/icons.json b/homeassistant/components/matter/icons.json index e5645a7fcdd7..dc5f2a960619 100644 --- a/homeassistant/components/matter/icons.json +++ b/homeassistant/components/matter/icons.json @@ -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", diff --git a/homeassistant/components/miele/const.py b/homeassistant/components/miele/const.py index b0461c8d3e77..804eb95e04b8 100644 --- a/homeassistant/components/miele/const.py +++ b/homeassistant/components/miele/const.py @@ -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 diff --git a/homeassistant/components/miele/strings.json b/homeassistant/components/miele/strings.json index 8ce6cc1b81df..f48fe9ece66b 100644 --- a/homeassistant/components/miele/strings.json +++ b/homeassistant/components/miele/strings.json @@ -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", diff --git a/homeassistant/components/openai_conversation/entity.py b/homeassistant/components/openai_conversation/entity.py index bf7d76339171..ebd329982489 100644 --- a/homeassistant/components/openai_conversation/entity.py +++ b/homeassistant/components/openai_conversation/entity.py @@ -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) diff --git a/homeassistant/components/openhome/config_flow.py b/homeassistant/components/openhome/config_flow.py index 9cd6a79f0129..ac8fff479002 100644 --- a/homeassistant/components/openhome/config_flow.py +++ b/homeassistant/components/openhome/config_flow.py @@ -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( diff --git a/homeassistant/components/rainmachine/icons.json b/homeassistant/components/rainmachine/icons.json index d84e91d7c0cb..785b5a599a03 100644 --- a/homeassistant/components/rainmachine/icons.json +++ b/homeassistant/components/rainmachine/icons.json @@ -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": { diff --git a/homeassistant/components/renault/manifest.json b/homeassistant/components/renault/manifest.json index 5639bd778426..781e86e54fcd 100644 --- a/homeassistant/components/renault/manifest.json +++ b/homeassistant/components/renault/manifest.json @@ -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"] } diff --git a/homeassistant/components/roborock/manifest.json b/homeassistant/components/roborock/manifest.json index 49f080240920..636b3c902578 100644 --- a/homeassistant/components/roborock/manifest.json +++ b/homeassistant/components/roborock/manifest.json @@ -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" ] } diff --git a/homeassistant/components/shelly/media_player.py b/homeassistant/components/shelly/media_player.py index 5a2f79de76a8..d909a453383b 100644 --- a/homeassistant/components/shelly/media_player.py +++ b/homeassistant/components/shelly/media_player.py @@ -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 diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index c36f6d7b5e20..cdb2985ef6f6 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -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", diff --git a/homeassistant/components/solarlog/strings.json b/homeassistant/components/solarlog/strings.json index ba66e5988d32..e038fbdc530c 100644 --- a/homeassistant/components/solarlog/strings.json +++ b/homeassistant/components/solarlog/strings.json @@ -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." } } diff --git a/homeassistant/components/system_bridge/__init__.py b/homeassistant/components/system_bridge/__init__.py index b1274b2c2e80..cde9defd6d8f 100644 --- a/homeassistant/components/system_bridge/__init__.py +++ b/homeassistant/components/system_bridge/__init__.py @@ -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" }, ) diff --git a/homeassistant/components/tedee/coordinator.py b/homeassistant/components/tedee/coordinator.py index 693f6234873f..7af61747d312 100644 --- a/homeassistant/components/tedee/coordinator.py +++ b/homeassistant/components/tedee/coordinator.py @@ -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: diff --git a/homeassistant/components/teslemetry/icons.json b/homeassistant/components/teslemetry/icons.json index 5065c4649d82..3c4cadd5d573 100644 --- a/homeassistant/components/teslemetry/icons.json +++ b/homeassistant/components/teslemetry/icons.json @@ -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" diff --git a/homeassistant/components/weatherflow_cloud/icons.json b/homeassistant/components/weatherflow_cloud/icons.json index 221ac699372b..fad61b8e3fb2 100644 --- a/homeassistant/components/weatherflow_cloud/icons.json +++ b/homeassistant/components/weatherflow_cloud/icons.json @@ -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" } }, diff --git a/homeassistant/components/wled/manifest.json b/homeassistant/components/wled/manifest.json index 37352376050d..fe7d16d968fb 100644 --- a/homeassistant/components/wled/manifest.json +++ b/homeassistant/components/wled/manifest.json @@ -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."] } diff --git a/homeassistant/components/wyoming/assist_satellite.py b/homeassistant/components/wyoming/assist_satellite.py index 40d1b0a5a8b8..d6ac29bccddb 100644 --- a/homeassistant/components/wyoming/assist_satellite.py +++ b/homeassistant/components/wyoming/assist_satellite.py @@ -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. diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 28bca8f634a1..99f745e4248b 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -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: diff --git a/homeassistant/const.py b/homeassistant/const.py index deacb43706c1..ce0ce5d97066 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -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) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 49eadf3b8917..4558dd04db0b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -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 diff --git a/pyproject.toml b/pyproject.toml index 58aaf7f0fe84..23453eaf5c99 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 diff --git a/requirements.txt b/requirements.txt index 8d6f411a247b..29fc0aee0388 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/requirements_all.txt b/requirements_all.txt index 12f61e9ed75d..653a1c84761f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f91d9aa49fd7..00e7d1a56ab8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 diff --git a/script/hassfest/requirements.py b/script/hassfest/requirements.py index 0ca3aaeb46ca..a07dfaf2007e 100644 --- a/script/hassfest/requirements.py +++ b/script/hassfest/requirements.py @@ -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"}, diff --git a/tests/components/habitica/test_services.py b/tests/components/habitica/test_services.py index aebc54a8b6c2..5edce7fa0e0b 100644 --- a/tests/components/habitica/test_services.py +++ b/tests/components/habitica/test_services.py @@ -1766,6 +1766,12 @@ async def test_create_todo( }, Task(everyX=5), ), + ( + { + ATTR_INTERVAL: 0, + }, + Task(everyX=0), + ), ( { ATTR_FREQUENCY: "weekly", diff --git a/tests/components/homeassistant_connect_zbt2/test_hardware.py b/tests/components/homeassistant_connect_zbt2/test_hardware.py index a0bd22e1c932..6192f2d7bb43 100644 --- a/tests/components/homeassistant_connect_zbt2/test_hardware.py +++ b/tests/components/homeassistant_connect_zbt2/test_hardware.py @@ -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", + } + ] + } diff --git a/tests/components/hue/test_device_trigger_v2.py b/tests/components/hue/test_device_trigger_v2.py index dd5d855c1bc9..e2565701d223 100644 --- a/tests/components/hue/test_device_trigger_v2.py +++ b/tests/components/hue/test_device_trigger_v2.py @@ -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 == [] diff --git a/tests/components/lutron_caseta/test_binary_sensor.py b/tests/components/lutron_caseta/test_binary_sensor.py index fa7393b52fed..92fae498586d 100644 --- a/tests/components/lutron_caseta/test_binary_sensor.py +++ b/tests/components/lutron_caseta/test_binary_sensor.py @@ -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 diff --git a/tests/components/openai_conversation/test_conversation.py b/tests/components/openai_conversation/test_conversation.py index c9a545c80659..95e625cf74ac 100644 --- a/tests/components/openai_conversation/test_conversation.py +++ b/tests/components/openai_conversation/test_conversation.py @@ -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, diff --git a/tests/components/openhome/test_config_flow.py b/tests/components/openhome/test_config_flow.py index 6430b8610e90..9485d7d27a05 100644 --- a/tests/components/openhome/test_config_flow.py +++ b/tests/components/openhome/test_config_flow.py @@ -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" diff --git a/tests/components/renault/snapshots/test_button.ambr b/tests/components/renault/snapshots/test_button.ambr index 90f8b92ef1f0..dc8f676ae3ad 100644 --- a/tests/components/renault/snapshots/test_button.ambr +++ b/tests/components/renault/snapshots/test_button.ambr @@ -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': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - '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': , - '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': , - 'entity_id': 'button.reg_captur_fuel_flash_lights', - 'last_changed': , - 'last_reported': , - 'last_updated': , - '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': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - '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': , - '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': , - 'entity_id': 'button.reg_captur_fuel_sound_horn', - 'last_changed': , - 'last_reported': , - 'last_updated': , - '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': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - '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': , - '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': , - 'entity_id': 'button.reg_captur_phev_flash_lights', - 'last_changed': , - 'last_reported': , - 'last_updated': , - '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': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - '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': , - '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': , - 'entity_id': 'button.reg_captur_phev_sound_horn', - 'last_changed': , - 'last_reported': , - 'last_updated': , - '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': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - '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': , - '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': , - 'entity_id': 'button.reg_captur_phev_start_charge', - 'last_changed': , - 'last_reported': , - 'last_updated': , - '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': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - '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': , - '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': , - 'entity_id': 'button.reg_captur_phev_stop_charge', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- # name: test_buttons[megane_e_tech][button.reg_meg_0_flash_lights-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ diff --git a/tests/components/roborock/snapshots/test_sensor.ambr b/tests/components/roborock/snapshots/test_sensor.ambr index 541b94addd0d..13d2b785b1d1 100644 --- a/tests/components/roborock/snapshots/test_sensor.ambr +++ b/tests/components/roborock/snapshots/test_sensor.ambr @@ -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', diff --git a/tests/components/shelly/test_media_player.py b/tests/components/shelly/test_media_player.py index 844ac9742472..f31ac9fc19ef 100644 --- a/tests/components/shelly/test_media_player.py +++ b/tests/components/shelly/test_media_player.py @@ -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( diff --git a/tests/components/wled/snapshots/test_diagnostics.ambr b/tests/components/wled/snapshots/test_diagnostics.ambr index 853e10c8f56b..cbcea893ee7e 100644 --- a/tests/components/wled/snapshots/test_diagnostics.ambr +++ b/tests/components/wled/snapshots/test_diagnostics.ambr @@ -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', diff --git a/tests/components/wled/snapshots/test_light.ambr b/tests/components/wled/snapshots/test_light.ambr index 1d49979e407d..004776fee540 100644 --- a/tests/components/wled/snapshots/test_light.ambr +++ b/tests/components/wled/snapshots/test_light.ambr @@ -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', diff --git a/tests/components/wyoming/test_satellite.py b/tests/components/wyoming/test_satellite.py index f61126787c28..727263da947d 100644 --- a/tests/components/wyoming/test_satellite.py +++ b/tests/components/wyoming/test_satellite.py @@ -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: diff --git a/tests/components/zha/test_init.py b/tests/components/zha/test_init.py index 9701f645c2c1..1c04ca2ff638 100644 --- a/tests/components/zha/test_init.py +++ b/tests/components/zha/test_init.py @@ -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",