diff --git a/homeassistant/components/asuswrt/manifest.json b/homeassistant/components/asuswrt/manifest.json index 3f2872bf25868..1ebeb596044f2 100644 --- a/homeassistant/components/asuswrt/manifest.json +++ b/homeassistant/components/asuswrt/manifest.json @@ -7,5 +7,5 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["aioasuswrt", "asusrouter", "asyncssh"], - "requirements": ["aioasuswrt==1.5.1", "asusrouter==1.21.0"] + "requirements": ["aioasuswrt==1.5.1", "asusrouter==1.21.1"] } diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index fdb72daaab6f9..ffe4bc713e149 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -23,5 +23,5 @@ "winter_mode": {} }, "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20251203.1"] + "requirements": ["home-assistant-frontend==20251203.2"] } diff --git a/homeassistant/components/frontend/strings.json b/homeassistant/components/frontend/strings.json index f483608895cc3..8cb083f7da2cd 100644 --- a/homeassistant/components/frontend/strings.json +++ b/homeassistant/components/frontend/strings.json @@ -1,9 +1,9 @@ { "preview_features": { "winter_mode": { - "description": "Adds falling snowflakes on your screen. Get your home ready for winter! ❄️", - "disable_confirmation": "Snowflakes will no longer fall on your screen. You can re-enable this at any time in labs settings.", - "enable_confirmation": "Snowflakes will start falling on your screen. You can turn this off at any time in labs settings.", + "description": "Adds falling snowflakes on your screen. Get your home ready for winter! ❄️\n\nIf you have animations disabled in your device accessibility settings, this feature will not work.", + "disable_confirmation": "Snowflakes will no longer fall on your screen. You can re-enable this at any time in Labs settings.", + "enable_confirmation": "Snowflakes will start falling on your screen. You can turn this off at any time in Labs settings.", "name": "Winter mode" } }, diff --git a/homeassistant/components/google_air_quality/config_flow.py b/homeassistant/components/google_air_quality/config_flow.py index d1da307c866bd..1ca5cd36d4ef9 100644 --- a/homeassistant/components/google_air_quality/config_flow.py +++ b/homeassistant/components/google_air_quality/config_flow.py @@ -51,9 +51,9 @@ async def _validate_input( description_placeholders: dict[str, str], ) -> bool: try: - await api.async_air_quality( + await api.async_get_current_conditions( lat=user_input[CONF_LOCATION][CONF_LATITUDE], - long=user_input[CONF_LOCATION][CONF_LONGITUDE], + lon=user_input[CONF_LOCATION][CONF_LONGITUDE], ) except GoogleAirQualityApiError as err: errors["base"] = "cannot_connect" diff --git a/homeassistant/components/google_air_quality/coordinator.py b/homeassistant/components/google_air_quality/coordinator.py index 9daaf21ae9e20..2c22214ebb1c7 100644 --- a/homeassistant/components/google_air_quality/coordinator.py +++ b/homeassistant/components/google_air_quality/coordinator.py @@ -7,7 +7,7 @@ from google_air_quality_api.api import GoogleAirQualityApi from google_air_quality_api.exceptions import GoogleAirQualityApiError -from google_air_quality_api.model import AirQualityData +from google_air_quality_api.model import AirQualityCurrentConditionsData from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE @@ -23,7 +23,9 @@ type GoogleAirQualityConfigEntry = ConfigEntry[GoogleAirQualityRuntimeData] -class GoogleAirQualityUpdateCoordinator(DataUpdateCoordinator[AirQualityData]): +class GoogleAirQualityUpdateCoordinator( + DataUpdateCoordinator[AirQualityCurrentConditionsData] +): """Coordinator for fetching Google AirQuality data.""" config_entry: GoogleAirQualityConfigEntry @@ -48,10 +50,10 @@ def __init__( self.lat = subentry.data[CONF_LATITUDE] self.long = subentry.data[CONF_LONGITUDE] - async def _async_update_data(self) -> AirQualityData: + async def _async_update_data(self) -> AirQualityCurrentConditionsData: """Fetch air quality data for this coordinate.""" try: - return await self.client.async_air_quality(self.lat, self.long) + return await self.client.async_get_current_conditions(self.lat, self.long) except GoogleAirQualityApiError as ex: _LOGGER.debug("Cannot fetch air quality data: %s", str(ex)) raise UpdateFailed( diff --git a/homeassistant/components/google_air_quality/manifest.json b/homeassistant/components/google_air_quality/manifest.json index 66845cd4b6892..22789aceb9269 100644 --- a/homeassistant/components/google_air_quality/manifest.json +++ b/homeassistant/components/google_air_quality/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["google_air_quality_api"], "quality_scale": "bronze", - "requirements": ["google_air_quality_api==1.1.3"] + "requirements": ["google_air_quality_api==2.0.0"] } diff --git a/homeassistant/components/google_air_quality/sensor.py b/homeassistant/components/google_air_quality/sensor.py index 7d72edf57aee9..c48d6771976d2 100644 --- a/homeassistant/components/google_air_quality/sensor.py +++ b/homeassistant/components/google_air_quality/sensor.py @@ -4,7 +4,7 @@ from dataclasses import dataclass import logging -from google_air_quality_api.model import AirQualityData +from google_air_quality_api.model import AirQualityCurrentConditionsData from homeassistant.components.sensor import ( SensorDeviceClass, @@ -33,15 +33,17 @@ class AirQualitySensorEntityDescription(SensorEntityDescription): """Describes Air Quality sensor entity.""" - exists_fn: Callable[[AirQualityData], bool] = lambda _: True - options_fn: Callable[[AirQualityData], list[str] | None] = lambda _: None - value_fn: Callable[[AirQualityData], StateType] - native_unit_of_measurement_fn: Callable[[AirQualityData], str | None] = ( + exists_fn: Callable[[AirQualityCurrentConditionsData], bool] = lambda _: True + options_fn: Callable[[AirQualityCurrentConditionsData], list[str] | None] = ( lambda _: None ) - translation_placeholders_fn: Callable[[AirQualityData], dict[str, str]] | None = ( - None - ) + value_fn: Callable[[AirQualityCurrentConditionsData], StateType] + native_unit_of_measurement_fn: Callable[ + [AirQualityCurrentConditionsData], str | None + ] = lambda _: None + translation_placeholders_fn: ( + Callable[[AirQualityCurrentConditionsData], dict[str, str]] | None + ) = None AIR_QUALITY_SENSOR_TYPES: tuple[AirQualitySensorEntityDescription, ...] = ( diff --git a/homeassistant/components/hue_ble/__init__.py b/homeassistant/components/hue_ble/__init__.py index 06a4a738b4627..25d20136e8fca 100644 --- a/homeassistant/components/hue_ble/__init__.py +++ b/homeassistant/components/hue_ble/__init__.py @@ -2,7 +2,7 @@ import logging -from HueBLE import HueBleLight +from HueBLE import ConnectionError, HueBleError, HueBleLight from homeassistant.components.bluetooth import ( async_ble_device_from_address, @@ -38,8 +38,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: HueBLEConfigEntry) -> bo light = HueBleLight(ble_device) - if not await light.connect() or not await light.poll_state(): - raise ConfigEntryNotReady("Device found but unable to connect.") + try: + await light.connect() + await light.poll_state() + except ConnectionError as e: + raise ConfigEntryNotReady("Device found but unable to connect.") from e + except HueBleError as e: + raise ConfigEntryNotReady( + "Device found and connected but unable to poll values from it." + ) from e entry.runtime_data = light diff --git a/homeassistant/components/hue_ble/config_flow.py b/homeassistant/components/hue_ble/config_flow.py index e7b4409c78924..6d3df824b172a 100644 --- a/homeassistant/components/hue_ble/config_flow.py +++ b/homeassistant/components/hue_ble/config_flow.py @@ -6,7 +6,7 @@ import logging from typing import Any -from HueBLE import HueBleLight +from HueBLE import ConnectionError, HueBleError, HueBleLight, PairingError import voluptuous as vol from homeassistant.components import bluetooth @@ -20,7 +20,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr -from .const import DOMAIN, URL_PAIRING_MODE +from .const import DOMAIN, URL_FACTORY_RESET, URL_PAIRING_MODE from .light import get_available_color_modes _LOGGER = logging.getLogger(__name__) @@ -41,32 +41,22 @@ async def validate_input(hass: HomeAssistant, address: str) -> Error | None: try: light = HueBleLight(ble_device) - await light.connect() - - if light.authenticated is None: - _LOGGER.warning( - "Unable to determine if light authenticated, proceeding anyway" - ) - elif not light.authenticated: - return Error.INVALID_AUTH - - if not light.connected: - return Error.CANNOT_CONNECT - - try: - get_available_color_modes(light) - except HomeAssistantError: - return Error.NOT_SUPPORTED - - _, errors = await light.poll_state() - if len(errors) != 0: - _LOGGER.warning("Errors raised when connecting to light: %s", errors) - return Error.CANNOT_CONNECT - - except Exception: + get_available_color_modes(light) + await light.poll_state() + + except ConnectionError as e: + _LOGGER.exception("Error connecting to light") + return ( + Error.INVALID_AUTH + if type(e.__cause__) is PairingError + else Error.CANNOT_CONNECT + ) + except HueBleError: _LOGGER.exception("Unexpected error validating light connection") return Error.UNKNOWN + except HomeAssistantError: + return Error.NOT_SUPPORTED else: return None finally: @@ -129,6 +119,7 @@ async def async_step_confirm( CONF_NAME: self._discovery_info.name, CONF_MAC: self._discovery_info.address, "url_pairing_mode": URL_PAIRING_MODE, + "url_factory_reset": URL_FACTORY_RESET, }, ) diff --git a/homeassistant/components/hue_ble/const.py b/homeassistant/components/hue_ble/const.py index 741c8e31070e1..25edceb0683e2 100644 --- a/homeassistant/components/hue_ble/const.py +++ b/homeassistant/components/hue_ble/const.py @@ -2,3 +2,4 @@ DOMAIN = "hue_ble" URL_PAIRING_MODE = "https://www.home-assistant.io/integrations/hue_ble#initial-setup" +URL_FACTORY_RESET = "https://www.philips-hue.com/en-gb/support/article/how-to-factory-reset-philips-hue-lights/000004" diff --git a/homeassistant/components/hue_ble/light.py b/homeassistant/components/hue_ble/light.py index 434c5cb90926f..18ab878acdf24 100644 --- a/homeassistant/components/hue_ble/light.py +++ b/homeassistant/components/hue_ble/light.py @@ -113,7 +113,7 @@ def _state_change_callback(self) -> None: async def async_update(self) -> None: """Fetch latest state from light and make available via properties.""" - await self._api.poll_state(run_callbacks=True) + await self._api.poll_state() async def async_turn_on(self, **kwargs: Any) -> None: """Set properties then turn the light on.""" diff --git a/homeassistant/components/hue_ble/manifest.json b/homeassistant/components/hue_ble/manifest.json index feb0c45cbbc5e..ce3b4b4e585b2 100644 --- a/homeassistant/components/hue_ble/manifest.json +++ b/homeassistant/components/hue_ble/manifest.json @@ -15,5 +15,5 @@ "iot_class": "local_push", "loggers": ["bleak", "HueBLE"], "quality_scale": "bronze", - "requirements": ["HueBLE==1.0.8"] + "requirements": ["HueBLE==2.1.0"] } diff --git a/homeassistant/components/hue_ble/strings.json b/homeassistant/components/hue_ble/strings.json index 72e6b2dab84b3..bbae80573f3dc 100644 --- a/homeassistant/components/hue_ble/strings.json +++ b/homeassistant/components/hue_ble/strings.json @@ -14,7 +14,7 @@ }, "step": { "confirm": { - "description": "Do you want to set up {name} ({mac})?. Make sure the light is [made discoverable to voice assistants]({url_pairing_mode})." + "description": "Do you want to set up {name} ({mac})?. Make sure the light is [made discoverable to voice assistants]({url_pairing_mode}) or has been [factory reset]({url_factory_reset})." } } } diff --git a/homeassistant/components/lutron_caseta/switch.py b/homeassistant/components/lutron_caseta/switch.py index e8f2d2e0f0b91..f1845b2ac1208 100644 --- a/homeassistant/components/lutron_caseta/switch.py +++ b/homeassistant/components/lutron_caseta/switch.py @@ -98,7 +98,11 @@ def unique_id(self) -> str: async def async_added_to_hass(self) -> None: """Register callbacks.""" await super().async_added_to_hass() - self._smartbridge.add_smart_away_subscriber(self._handle_bridge_update) + self._smartbridge.add_smart_away_subscriber(self._handle_smart_away_update) + + def _handle_smart_away_update(self, smart_away_state: str | None = None) -> None: + """Handle updated smart away state from the bridge.""" + self.async_write_ha_state() async def async_turn_on(self, **kwargs: Any) -> None: """Turn Smart Away on.""" diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index 4323d177e9d5c..be37b8913a7dd 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -8,6 +8,6 @@ "iot_class": "calculated", "loggers": ["yt_dlp"], "quality_scale": "internal", - "requirements": ["yt-dlp[default]==2025.11.12"], + "requirements": ["yt-dlp[default]==2025.12.08"], "single_config_entry": true } diff --git a/homeassistant/components/miele/const.py b/homeassistant/components/miele/const.py index eb332baea606a..4a18a340ff238 100644 --- a/homeassistant/components/miele/const.py +++ b/homeassistant/components/miele/const.py @@ -191,6 +191,7 @@ class ProgramPhaseWashingMachine(MieleEnum, missing_to_none=True): drying = 280 disinfecting = 285 flex_load_active = 11047 + automatic_start = 11044 class ProgramPhaseTumbleDryer(MieleEnum, missing_to_none=True): @@ -451,19 +452,19 @@ class WashingMachineProgramId(MieleEnum, missing_to_none=True): """Program Id codes for washing machines.""" no_program = 0, -1 - cottons = 1 + cottons = 1, 10001 minimum_iron = 3 - delicates = 4 - woollens = 8 - silks = 9 + delicates = 4, 10022 + woollens = 8, 10040 + silks = 9, 10042 starch = 17 - rinse = 18 - drain_spin = 21 - curtains = 22 - shirts = 23 + rinse = 18, 10058 + drain_spin = 21, 10036 + curtains = 22, 10055 + shirts = 23, 10038 denim = 24, 123 - proofing = 27 - sportswear = 29 + proofing = 27, 10057 + sportswear = 29, 10052 automatic_plus = 31 outerwear = 37 pillows = 39 @@ -472,19 +473,29 @@ class WashingMachineProgramId(MieleEnum, missing_to_none=True): rinse_out_lint = 48 # washer-dryer dark_garments = 50 separate_rinse_starch = 52 - first_wash = 53 + first_wash = 53, 10053 cottons_hygiene = 69 steam_care = 75 # washer-dryer freshen_up = 76 # washer-dryer - trainers = 77 - clean_machine = 91 - down_duvets = 95 - express_20 = 122 + trainers = 77, 10056 + clean_machine = 91, 10067 + down_duvets = 95, 10050 + express_20 = 122, 10029 down_filled_items = 129 cottons_eco = 133 quick_power_wash = 146, 10031 eco_40_60 = 190, 10007 - normal = 10001 + bed_linen = 10047 + easy_care = 10016 + dark_jeans = 10048 + outdoor_garments = 10049 + game_pieces = 10070 + stuffed_toys = 10069 + pre_ironing = 10059 + trainers_refresh = 10066 + smartmatic = 10068 + cottonrepair = 10065 + powerfresh = 10075 class DishWasherProgramId(MieleEnum, missing_to_none=True): diff --git a/homeassistant/components/miele/manifest.json b/homeassistant/components/miele/manifest.json index c8c2adcb7706d..a6b82fc884b15 100644 --- a/homeassistant/components/miele/manifest.json +++ b/homeassistant/components/miele/manifest.json @@ -9,7 +9,7 @@ "iot_class": "cloud_push", "loggers": ["pymiele"], "quality_scale": "platinum", - "requirements": ["pymiele==0.6.0"], + "requirements": ["pymiele==0.6.1"], "single_config_entry": true, "zeroconf": ["_mieleathome._tcp.local."] } diff --git a/homeassistant/components/miele/strings.json b/homeassistant/components/miele/strings.json index 8afe6166ebc60..6d55ba528408e 100644 --- a/homeassistant/components/miele/strings.json +++ b/homeassistant/components/miele/strings.json @@ -411,6 +411,7 @@ "cook_bacon": "Cook bacon", "cool_air": "Cool air", "corn_on_the_cob": "Corn on the cob", + "cottonrepair": "CottonRepair", "cottons": "Cottons", "cottons_eco": "Cottons ECO", "cottons_hygiene": "Cottons hygiene", @@ -440,6 +441,7 @@ "custom_program_8": "Custom program 8", "custom_program_9": "Custom program 9", "dark_garments": "Dark garments", + "dark_jeans": "Dark/jeans", "dark_mixed_grain_bread": "Dark mixed grain bread", "decrystallise_honey": "Decrystallize honey", "defrost": "Defrost", @@ -457,6 +459,7 @@ "drop_cookies_2_trays": "Drop cookies (2 trays)", "duck": "Duck", "dutch_hash": "Dutch hash", + "easy_care": "Easy care", "eco": "ECO", "eco_40_60": "ECO 40-60", "eco_fan_heat": "ECO fan heat", @@ -487,6 +490,7 @@ "fruit_streusel_cake": "Fruit streusel cake", "fruit_tea": "Fruit tea", "full_grill": "Full grill", + "game_pieces": "Game pieces", "gentle": "Gentle", "gentle_denim": "Gentle denim", "gentle_minimum_iron": "Gentle minimum iron", @@ -607,6 +611,7 @@ "oats_cracked": "Oats (cracked)", "oats_whole": "Oats (whole)", "osso_buco": "Osso buco", + "outdoor_garments": "Outdoor garments", "outerwear": "Outerwear", "oyster_mushroom_diced": "Oyster mushroom (diced)", "oyster_mushroom_strips": "Oyster mushroom (strips)", @@ -713,8 +718,10 @@ "potatoes_waxy_whole_small": "Potatoes (waxy, whole, small)", "poularde_breast": "Poularde breast", "poularde_whole": "Poularde (whole)", + "power_fresh": "PowerFresh", "power_wash": "PowerWash", "prawns": "Prawns", + "pre_ironing": "Pre-ironing", "proofing": "Proofing", "prove_15_min": "Prove for 15 min", "prove_30_min": "Prove for 30 min", @@ -807,6 +814,7 @@ "simiao_rapid_steam_cooking": "Simiao (rapid steam cooking)", "simiao_steam_cooking": "Simiao (steam cooking)", "small_shrimps": "Small shrimps", + "smartmatic": "SmartMatic", "smoothing": "Smoothing", "snow_pea": "Snow pea", "soak": "Soak", @@ -833,6 +841,7 @@ "sterilize_crockery": "Sterilize crockery", "stollen": "Stollen", "stuffed_cabbage": "Stuffed cabbage", + "stuffed_toys": "Stuffed toys", "sweat_onions": "Sweat onions", "swede_cut_into_batons": "Swede (cut into batons)", "swede_diced": "Swede (diced)", @@ -855,6 +864,7 @@ "top_heat": "Top heat", "tortellini_fresh": "Tortellini (fresh)", "trainers": "Trainers", + "trainers_refresh": "Trainers refresh", "treacle_sponge_pudding_one_large": "Treacle sponge pudding (one large)", "treacle_sponge_pudding_several_small": "Treacle sponge pudding (several small)", "trout": "Trout", @@ -935,6 +945,7 @@ "2nd_grinding": "2nd grinding", "2nd_pre_brewing": "2nd pre-brewing", "anti_crease": "Anti-crease", + "automatic_start": "Automatic start", "blocked_brushes": "Brushes blocked", "blocked_drive_wheels": "Drive wheels blocked", "blocked_front_wheel": "Front wheel blocked", diff --git a/homeassistant/components/music_assistant/config_flow.py b/homeassistant/components/music_assistant/config_flow.py index 95d67b439e69b..226a4dda28f7c 100644 --- a/homeassistant/components/music_assistant/config_flow.py +++ b/homeassistant/components/music_assistant/config_flow.py @@ -163,9 +163,6 @@ async def async_step_hassio( LOGGER.exception("Unexpected exception during add-on discovery") return self.async_abort(reason="unknown") - if not server_info.onboard_done: - return self.async_abort(reason="server_not_ready") - # We trust the token from hassio discovery and validate it during setup self.token = discovery_info.config["auth_token"] @@ -226,11 +223,6 @@ async def async_step_zeroconf( LOGGER.debug("Ignoring add-on server in zeroconf discovery") return self.async_abort(reason="already_discovered_addon") - # Ignore servers that have not completed onboarding yet - if not server_info.onboard_done: - LOGGER.debug("Ignoring server that hasn't completed onboarding") - return self.async_abort(reason="server_not_ready") - self.url = server_info.base_url self.server_info = server_info diff --git a/homeassistant/components/roborock/__init__.py b/homeassistant/components/roborock/__init__.py index ece5a62e48d36..5d67233f7bc7d 100644 --- a/homeassistant/components/roborock/__init__.py +++ b/homeassistant/components/roborock/__init__.py @@ -20,7 +20,7 @@ from roborock.map.map_parser import MapParserConfig from homeassistant.const import CONF_USERNAME, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import HomeAssistant +from homeassistant.core import Event, HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -99,10 +99,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: RoborockConfigEntry) -> translation_domain=DOMAIN, translation_key="home_data_fail", ) from err + + async def shutdown_roborock(_: Event | None = None) -> None: + await asyncio.gather(device_manager.close(), cache.flush()) + + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown_roborock) + ) + entry.async_on_unload(shutdown_roborock) + devices = await device_manager.get_devices() _LOGGER.debug("Device manager found %d devices", len(devices)) - for device in devices: - entry.async_on_unload(device.close) coordinators = await asyncio.gather( *build_setup_functions(hass, entry, devices, user_data), @@ -124,25 +131,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: RoborockConfigEntry) -> translation_domain=DOMAIN, translation_key="no_coordinators", ) - valid_coordinators = RoborockCoordinators(v1_coords, a01_coords) - - async def on_stop(_: Any) -> None: - _LOGGER.debug("Shutting down roborock") - await asyncio.gather( - *( - coordinator.async_shutdown() - for coordinator in valid_coordinators.values() - ), - cache.flush(), - ) - - entry.async_on_unload( - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, - on_stop, - ) - ) - entry.runtime_data = valid_coordinators + entry.runtime_data = RoborockCoordinators(v1_coords, a01_coords) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/roborock/manifest.json b/homeassistant/components/roborock/manifest.json index 17c0d9852ef1d..090a498b751fc 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==3.10.2", + "python-roborock==3.10.10", "vacuum-map-parser-roborock==0.1.4" ] } diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 79f3914451597..d1b3ba34b910d 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -170,6 +170,9 @@ async def _async_setup_block_entry( device_entry = dev_reg.async_get_device( connections={(CONNECTION_NETWORK_MAC, dr.format_mac(entry.unique_id))}, ) + # https://github.com/home-assistant/core/pull/48076 + if device_entry and entry.entry_id not in device_entry.config_entries: + device_entry = None sleep_period = entry.data.get(CONF_SLEEP_PERIOD) runtime_data = entry.runtime_data @@ -280,6 +283,9 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ShellyConfigEntry) device_entry = dev_reg.async_get_device( connections={(CONNECTION_NETWORK_MAC, dr.format_mac(entry.unique_id))}, ) + # https://github.com/home-assistant/core/pull/48076 + if device_entry and entry.entry_id not in device_entry.config_entries: + device_entry = None sleep_period = entry.data.get(CONF_SLEEP_PERIOD) runtime_data = entry.runtime_data diff --git a/homeassistant/components/system_bridge/__init__.py b/homeassistant/components/system_bridge/__init__.py index e1ee57e42b298..574c5399ff826 100644 --- a/homeassistant/components/system_bridge/__init__.py +++ b/homeassistant/components/system_bridge/__init__.py @@ -422,6 +422,9 @@ async def handle_send_text(service_call: ServiceCall) -> ServiceResponse: }, ), supports_response=SupportsResponse.ONLY, + description_placeholders={ + "syntax_keys_documentation_url": "http://robotjs.io/docs/syntax#keys" + }, ) hass.services.async_register( diff --git a/homeassistant/components/system_bridge/config_flow.py b/homeassistant/components/system_bridge/config_flow.py index fae75f4087e6b..bf6057a27bb1f 100644 --- a/homeassistant/components/system_bridge/config_flow.py +++ b/homeassistant/components/system_bridge/config_flow.py @@ -24,7 +24,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo -from .const import DATA_WAIT_TIMEOUT, DOMAIN, SYNTAX_KEYS_DOCUMENTATION_URL +from .const import DATA_WAIT_TIMEOUT, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -134,9 +134,6 @@ async def async_step_user( return self.async_show_form( step_id="user", data_schema=STEP_USER_DATA_SCHEMA, - description_placeholders={ - "syntax_keys_documentation_url": SYNTAX_KEYS_DOCUMENTATION_URL - }, ) errors, info = await _async_get_info(self.hass, user_input) @@ -151,9 +148,6 @@ async def async_step_user( step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors, - description_placeholders={ - "syntax_keys_documentation_url": SYNTAX_KEYS_DOCUMENTATION_URL - }, ) async def async_step_authenticate( @@ -185,7 +179,6 @@ async def async_step_authenticate( data_schema=STEP_AUTHENTICATE_DATA_SCHEMA, description_placeholders={ "name": self._name, - "syntax_keys_documentation_url": SYNTAX_KEYS_DOCUMENTATION_URL, }, errors=errors, ) diff --git a/homeassistant/components/system_bridge/const.py b/homeassistant/components/system_bridge/const.py index 72160b86daae7..235d7e6b9869d 100644 --- a/homeassistant/components/system_bridge/const.py +++ b/homeassistant/components/system_bridge/const.py @@ -4,8 +4,6 @@ from systembridgemodels.modules import Module -SYNTAX_KEYS_DOCUMENTATION_URL = "http://robotjs.io/docs/syntax#keys" - DOMAIN = "system_bridge" MODULES: Final[list[Module]] = [ diff --git a/homeassistant/components/template/config.py b/homeassistant/components/template/config.py index bc343a8d145d6..7b4038375bf88 100644 --- a/homeassistant/components/template/config.py +++ b/homeassistant/components/template/config.py @@ -2,6 +2,7 @@ from collections.abc import Callable from contextlib import suppress +import itertools import logging from typing import Any @@ -346,12 +347,21 @@ async def async_validate_config_section( async def async_validate_config(hass: HomeAssistant, config: ConfigType) -> ConfigType: """Validate config.""" - if DOMAIN not in config: + + configs = [] + for key in config: + if DOMAIN not in key: + continue + + if key == DOMAIN or (key.startswith(DOMAIN) and len(key.split()) > 1): + configs.append(cv.ensure_list(config[key])) + + if not configs: return config config_sections = [] - for cfg in cv.ensure_list(config[DOMAIN]): + for cfg in itertools.chain(*configs): try: template_config: TemplateConfig = await async_validate_config_section( hass, cfg diff --git a/homeassistant/components/template/helpers.py b/homeassistant/components/template/helpers.py index 5f66ed86b2010..bf408100a00ed 100644 --- a/homeassistant/components/template/helpers.py +++ b/homeassistant/components/template/helpers.py @@ -12,6 +12,7 @@ from homeassistant.components import blueprint from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_ENTITY_PICTURE_TEMPLATE, CONF_FRIENDLY_NAME, CONF_ICON, @@ -133,6 +134,9 @@ def rewrite_legacy_to_modern_config( """Rewrite legacy config.""" entity_cfg = {**entity_cfg} + # Remove deprecated entity_id field from legacy syntax + entity_cfg.pop(ATTR_ENTITY_ID, None) + for from_key, to_key in itertools.chain( LEGACY_FIELDS.items(), extra_legacy_fields.items() ): diff --git a/homeassistant/components/teslemetry/strings.json b/homeassistant/components/teslemetry/strings.json index a39120b22d33c..53bb56526e51d 100644 --- a/homeassistant/components/teslemetry/strings.json +++ b/homeassistant/components/teslemetry/strings.json @@ -1358,7 +1358,7 @@ "name": "Energy Site" }, "tou_settings": { - "description": "See {time_use_url} for details.", + "description": "See {time_of_use_url} for details.", "name": "Settings" } }, diff --git a/homeassistant/components/xbox/media_source.py b/homeassistant/components/xbox/media_source.py index a224fc56eb8b9..777901405ba71 100644 --- a/homeassistant/components/xbox/media_source.py +++ b/homeassistant/components/xbox/media_source.py @@ -630,7 +630,7 @@ def _build_media_items_promotional( title=image.type, can_play=True, can_expand=False, - thumbnail=image.url, + thumbnail=to_https(image.url), ) for image in game.images ] diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 7c11cc52fbd92..b2eaed79917e3 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -63,6 +63,7 @@ from .entity import YeelightEntity _LOGGER = logging.getLogger(__name__) +_EXAMPLES_URL = "https://yeelight.readthedocs.io/en/stable/flow.html" ATTR_MINUTES = "minutes" ATTR_KELVIN = "kelvin" @@ -384,7 +385,8 @@ async def _async_set_auto_delay_off_scene(entity, service_call): SERVICE_SCHEMA_START_FLOW, _async_start_flow, description_placeholders={ - "flow_objects_urls": "https://yeelight.readthedocs.io/en/stable/yeelight.html#flow-objects" + "examples_url": _EXAMPLES_URL, + "flow_objects_urls": "https://yeelight.readthedocs.io/en/stable/yeelight.html#flow-objects", }, ) platform.async_register_entity_service( @@ -402,9 +404,7 @@ async def _async_set_auto_delay_off_scene(entity, service_call): SERVICE_SET_COLOR_FLOW_SCENE, SERVICE_SCHEMA_SET_COLOR_FLOW_SCENE, _async_set_color_flow_scene, - description_placeholders={ - "examples_url": "https://yeelight.readthedocs.io/en/stable/flow.html" - }, + description_placeholders={"examples_url": _EXAMPLES_URL}, ) platform.async_register_entity_service( SERVICE_SET_AUTO_DELAY_OFF_SCENE, diff --git a/homeassistant/components/zwave_js/services.py b/homeassistant/components/zwave_js/services.py index 316b58ce628a8..1fdc630ea9775 100644 --- a/homeassistant/components/zwave_js/services.py +++ b/homeassistant/components/zwave_js/services.py @@ -306,9 +306,6 @@ def validate_entities(val: dict[str, Any]) -> dict[str, Any]: has_at_least_one_node, ), ), - description_placeholders={ - "api_docs_url": "https://zwave-js.github.io/node-zwave-js/#/api/CCs/index" - }, ) self._hass.services.async_register( @@ -455,6 +452,9 @@ def validate_entities(val: dict[str, Any]) -> dict[str, Any]: has_at_least_one_node, ), ), + description_placeholders={ + "api_docs_url": "https://zwave-js.github.io/node-zwave-js/#/api/CCs/index" + }, ) self._hass.services.async_register( diff --git a/homeassistant/const.py b/homeassistant/const.py index a879d819ae6a9..2f93fc0ebabce 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -17,7 +17,7 @@ APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 12 -PATCH_VERSION: Final = "1" +PATCH_VERSION: Final = "2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 6e2f970486962..e95428225aa6f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -39,7 +39,7 @@ habluetooth==5.7.0 hass-nabucasa==1.7.0 hassil==3.5.0 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20251203.1 +home-assistant-frontend==20251203.2 home-assistant-intents==2025.12.2 httpx==0.28.1 ifaddr==0.2.0 diff --git a/pyproject.toml b/pyproject.toml index 7c148daca4c00..ecb7ee52ba23b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.12.1" +version = "2025.12.2" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." diff --git a/requirements_all.txt b/requirements_all.txt index 722c74a0b5352..24d87fcbe50fe 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -22,7 +22,7 @@ HAP-python==5.0.0 HATasmota==0.10.1 # homeassistant.components.hue_ble -HueBLE==1.0.8 +HueBLE==2.1.0 # homeassistant.components.mastodon Mastodon.py==2.1.2 @@ -537,7 +537,7 @@ arris-tg2492lg==2.2.0 asmog==0.0.6 # homeassistant.components.asuswrt -asusrouter==1.21.0 +asusrouter==1.21.1 # homeassistant.components.dlna_dmr # homeassistant.components.dlna_dms @@ -1090,7 +1090,7 @@ google-nest-sdm==9.1.2 google-photos-library-api==0.12.1 # homeassistant.components.google_air_quality -google_air_quality_api==1.1.3 +google_air_quality_api==2.0.0 # homeassistant.components.slide # homeassistant.components.slide_local @@ -1198,7 +1198,7 @@ hole==0.9.0 holidays==0.84 # homeassistant.components.frontend -home-assistant-frontend==20251203.1 +home-assistant-frontend==20251203.2 # homeassistant.components.conversation home-assistant-intents==2025.12.2 @@ -2187,7 +2187,7 @@ pymeteoclimatic==0.1.0 pymicro-vad==1.0.1 # homeassistant.components.miele -pymiele==0.6.0 +pymiele==0.6.1 # homeassistant.components.xiaomi_tv pymitv==1.4.3 @@ -2557,7 +2557,7 @@ python-rabbitair==0.0.8 python-ripple-api==0.0.3 # homeassistant.components.roborock -python-roborock==3.10.2 +python-roborock==3.10.10 # homeassistant.components.smarttub python-smarttub==0.0.45 @@ -3228,7 +3228,7 @@ youless-api==2.2.0 youtubeaio==2.1.1 # homeassistant.components.media_extractor -yt-dlp[default]==2025.11.12 +yt-dlp[default]==2025.12.08 # homeassistant.components.zabbix zabbix-utils==2.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d980afd1e10fd..82a4e8e68b4e6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -22,7 +22,7 @@ HAP-python==5.0.0 HATasmota==0.10.1 # homeassistant.components.hue_ble -HueBLE==1.0.8 +HueBLE==2.1.0 # homeassistant.components.mastodon Mastodon.py==2.1.2 @@ -504,7 +504,7 @@ aranet4==2.5.1 arcam-fmj==1.8.2 # homeassistant.components.asuswrt -asusrouter==1.21.0 +asusrouter==1.21.1 # homeassistant.components.dlna_dmr # homeassistant.components.dlna_dms @@ -966,7 +966,7 @@ google-nest-sdm==9.1.2 google-photos-library-api==0.12.1 # homeassistant.components.google_air_quality -google_air_quality_api==1.1.3 +google_air_quality_api==2.0.0 # homeassistant.components.slide # homeassistant.components.slide_local @@ -1056,7 +1056,7 @@ hole==0.9.0 holidays==0.84 # homeassistant.components.frontend -home-assistant-frontend==20251203.1 +home-assistant-frontend==20251203.2 # homeassistant.components.conversation home-assistant-intents==2025.12.2 @@ -1840,7 +1840,7 @@ pymeteoclimatic==0.1.0 pymicro-vad==1.0.1 # homeassistant.components.miele -pymiele==0.6.0 +pymiele==0.6.1 # homeassistant.components.mochad pymochad==0.2.0 @@ -2135,7 +2135,7 @@ python-pooldose==0.7.8 python-rabbitair==0.0.8 # homeassistant.components.roborock -python-roborock==3.10.2 +python-roborock==3.10.10 # homeassistant.components.smarttub python-smarttub==0.0.45 @@ -2686,7 +2686,7 @@ youless-api==2.2.0 youtubeaio==2.1.1 # homeassistant.components.media_extractor -yt-dlp[default]==2025.11.12 +yt-dlp[default]==2025.12.08 # homeassistant.components.zamg zamg==0.3.6 diff --git a/tests/components/google_air_quality/conftest.py b/tests/components/google_air_quality/conftest.py index 899301e7f57b1..0153860d15848 100644 --- a/tests/components/google_air_quality/conftest.py +++ b/tests/components/google_air_quality/conftest.py @@ -4,7 +4,7 @@ from typing import Any from unittest.mock import AsyncMock, Mock, patch -from google_air_quality_api.model import AirQualityData +from google_air_quality_api.model import AirQualityCurrentConditionsData import pytest from homeassistant.components.google_air_quality import CONF_REFERRER @@ -81,7 +81,9 @@ def mock_client_api() -> Generator[Mock]: ), ): api = mock_api.return_value - api.async_air_quality.return_value = AirQualityData.from_dict(responses) + api.async_get_current_conditions.return_value = ( + AirQualityCurrentConditionsData.from_dict(responses) + ) yield api diff --git a/tests/components/google_air_quality/test_config_flow.py b/tests/components/google_air_quality/test_config_flow.py index a8aad84d5aa4e..4ab2dc707abe0 100644 --- a/tests/components/google_air_quality/test_config_flow.py +++ b/tests/components/google_air_quality/test_config_flow.py @@ -68,7 +68,7 @@ async def test_create_entry( }, ) - mock_api.async_air_quality.assert_called_once_with(lat=10.1, long=20.1) + mock_api.async_get_current_conditions.assert_called_once_with(lat=10.1, lon=20.1) _assert_create_entry_result(result) assert len(mock_setup_entry.mock_calls) == 1 @@ -101,7 +101,7 @@ async def test_form_with_referrer( }, ) - mock_api.async_air_quality.assert_called_once_with(lat=10.1, long=20.1) + mock_api.async_get_current_conditions.assert_called_once_with(lat=10.1, lon=20.1) _assert_create_entry_result(result, expected_referrer="test-referrer") assert len(mock_setup_entry.mock_calls) == 1 @@ -126,7 +126,7 @@ async def test_form_exceptions( DOMAIN, context={"source": SOURCE_USER} ) - mock_api.async_air_quality.side_effect = api_exception + mock_api.async_get_current_conditions.side_effect = api_exception result = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -154,7 +154,7 @@ async def test_form_exceptions( # FlowResultType.CREATE_ENTRY or FlowResultType.ABORT so # we can show the config flow is able to recover from an error. - mock_api.async_air_quality.side_effect = None + mock_api.async_get_current_conditions.side_effect = None result = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -196,7 +196,7 @@ async def test_form_api_key_already_configured( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" - assert mock_api.async_air_quality.call_count == 0 + assert mock_api.async_get_current_conditions.call_count == 0 async def test_form_location_already_configured( @@ -224,7 +224,7 @@ async def test_form_location_already_configured( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" - assert mock_api.async_air_quality.call_count == 0 + assert mock_api.async_get_current_conditions.call_count == 0 async def test_form_not_already_configured( @@ -251,7 +251,9 @@ async def test_form_not_already_configured( }, ) - mock_api.async_air_quality.assert_called_once_with(lat=10.1002, long=20.0998) + mock_api.async_get_current_conditions.assert_called_once_with( + lat=10.1002, lon=20.0998 + ) assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Google Air Quality" @@ -281,7 +283,7 @@ async def test_subentry_flow( await hass.async_block_till_done() # After initial setup for 1 subentry, each API is called once - assert mock_api.async_air_quality.call_count == 1 + assert mock_api.async_get_current_conditions.call_count == 1 result = await hass.config_entries.subentries.async_init( (mock_config_entry.entry_id, "location"), @@ -313,7 +315,7 @@ async def test_subentry_flow( # Initial setup: 1 of each API call # Subentry flow validation: 1 current conditions call # Reload with 2 subentries: 2 of each API call - assert mock_api.async_air_quality.call_count == 1 + 1 + 2 + assert mock_api.async_get_current_conditions.call_count == 1 + 1 + 2 entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id) assert len(entry.subentries) == 2 diff --git a/tests/components/google_air_quality/test_init.py b/tests/components/google_air_quality/test_init.py index 777180384ac3f..5f0053071c55d 100644 --- a/tests/components/google_air_quality/test_init.py +++ b/tests/components/google_air_quality/test_init.py @@ -33,7 +33,7 @@ async def test_config_not_ready( ) -> None: """Test for setup failure if an API call fails.""" mock_config_entry.add_to_hass(hass) - mock_api.async_air_quality.side_effect = GoogleAirQualityApiError() + mock_api.async_get_current_conditions.side_effect = GoogleAirQualityApiError() await hass.config_entries.async_setup(mock_config_entry.entry_id) diff --git a/tests/components/hue_ble/test_config_flow.py b/tests/components/hue_ble/test_config_flow.py index ea08a3fa656d8..62a88b3fbdcd9 100644 --- a/tests/components/hue_ble/test_config_flow.py +++ b/tests/components/hue_ble/test_config_flow.py @@ -2,11 +2,16 @@ from unittest.mock import AsyncMock, PropertyMock, patch +from HueBLE import ConnectionError, HueBleError, PairingError import pytest from homeassistant import config_entries from homeassistant.components.hue_ble.config_flow import Error -from homeassistant.components.hue_ble.const import DOMAIN, URL_PAIRING_MODE +from homeassistant.components.hue_ble.const import ( + DOMAIN, + URL_FACTORY_RESET, + URL_PAIRING_MODE, +) from homeassistant.config_entries import SOURCE_BLUETOOTH from homeassistant.const import CONF_MAC, CONF_NAME from homeassistant.core import HomeAssistant @@ -18,22 +23,13 @@ from tests.common import MockConfigEntry from tests.components.bluetooth import BLEDevice, generate_ble_device +AUTH_ERROR = ConnectionError() +AUTH_ERROR.__cause__ = PairingError() + -@pytest.mark.parametrize( - ("mock_authenticated"), - [ - (True,), - (None), - ], - ids=[ - "normal", - "unknown_auth", - ], -) async def test_bluetooth_form( hass: HomeAssistant, mock_setup_entry: AsyncMock, - mock_authenticated: bool | None, ) -> None: """Test bluetooth discovery form.""" @@ -48,6 +44,7 @@ async def test_bluetooth_form( CONF_NAME: TEST_DEVICE_NAME, CONF_MAC: TEST_DEVICE_MAC, "url_pairing_mode": URL_PAIRING_MODE, + "url_factory_reset": URL_FACTORY_RESET, } with ( @@ -65,17 +62,7 @@ async def test_bluetooth_form( ), patch( "homeassistant.components.hue_ble.config_flow.HueBleLight.poll_state", - side_effect=[(True, [])], - ), - patch( - "homeassistant.components.hue_ble.config_flow.HueBleLight.connected", - new_callable=PropertyMock, - return_value=True, - ), - patch( - "homeassistant.components.hue_ble.config_flow.HueBleLight.authenticated", - new_callable=PropertyMock, - return_value=mock_authenticated, + side_effect=[True], ), ): result = await hass.config_entries.flow.async_configure( @@ -96,8 +83,6 @@ async def test_bluetooth_form( "mock_return_device", "mock_scanner_count", "mock_connect", - "mock_authenticated", - "mock_connected", "mock_support_on_off", "mock_poll_state", "error", @@ -106,71 +91,57 @@ async def test_bluetooth_form( ( None, 0, + None, True, - True, - True, - True, - (True, []), + None, Error.NO_SCANNERS, ), ( None, 1, + None, True, - True, - True, - True, - (True, []), + None, Error.NOT_FOUND, ), ( generate_ble_device(TEST_DEVICE_NAME, TEST_DEVICE_MAC), 1, + AUTH_ERROR, True, - False, - True, - True, - (True, []), + None, Error.INVALID_AUTH, ), ( generate_ble_device(TEST_DEVICE_NAME, TEST_DEVICE_MAC), 1, + ConnectionError, True, - True, - False, - True, - (True, []), + None, Error.CANNOT_CONNECT, ), ( generate_ble_device(TEST_DEVICE_NAME, TEST_DEVICE_MAC), 1, - True, - True, - True, + None, False, - (True, []), + None, Error.NOT_SUPPORTED, ), ( generate_ble_device(TEST_DEVICE_NAME, TEST_DEVICE_MAC), 1, + None, True, - True, - True, - True, - (True, ["ERROR!"]), - Error.CANNOT_CONNECT, + HueBleError, + Error.UNKNOWN, ), ( generate_ble_device(TEST_DEVICE_NAME, TEST_DEVICE_MAC), 1, - Exception, + HueBleError, + None, None, - True, - True, - (True, []), Error.UNKNOWN, ), ], @@ -189,11 +160,9 @@ async def test_bluetooth_form_exception( mock_setup_entry: AsyncMock, mock_return_device: BLEDevice | None, mock_scanner_count: int, - mock_connect: Exception | bool, - mock_authenticated: bool | None, - mock_connected: bool, + mock_connect: Exception | None, mock_support_on_off: bool, - mock_poll_state: Exception | tuple[bool, list[Exception]], + mock_poll_state: Exception | None, error: Error, ) -> None: """Test bluetooth discovery form with errors.""" @@ -228,16 +197,6 @@ async def test_bluetooth_form_exception( "homeassistant.components.hue_ble.config_flow.HueBleLight.poll_state", side_effect=[mock_poll_state], ), - patch( - "homeassistant.components.hue_ble.config_flow.HueBleLight.connected", - new_callable=PropertyMock, - return_value=mock_connected, - ), - patch( - "homeassistant.components.hue_ble.config_flow.HueBleLight.authenticated", - new_callable=PropertyMock, - return_value=mock_authenticated, - ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -262,17 +221,7 @@ async def test_bluetooth_form_exception( ), patch( "homeassistant.components.hue_ble.config_flow.HueBleLight.poll_state", - side_effect=[(True, [])], - ), - patch( - "homeassistant.components.hue_ble.config_flow.HueBleLight.connected", - new_callable=PropertyMock, - return_value=True, - ), - patch( - "homeassistant.components.hue_ble.config_flow.HueBleLight.authenticated", - new_callable=PropertyMock, - return_value=True, + side_effect=[True], ), ): result = await hass.config_entries.flow.async_configure( diff --git a/tests/components/hue_ble/test_init.py b/tests/components/hue_ble/test_init.py index aa70ab6865202..6265e9c7c61b9 100644 --- a/tests/components/hue_ble/test_init.py +++ b/tests/components/hue_ble/test_init.py @@ -3,6 +3,7 @@ from unittest.mock import patch from bleak.backends.device import BLEDevice +from HueBLE import ConnectionError, HueBleError import pytest from homeassistant.components.hue_ble.const import DOMAIN @@ -29,29 +30,29 @@ None, 2, True, - True, + None, "The light was not found.", ), ( None, 0, True, - True, + None, "No Bluetooth scanners are available to search for the light.", ), ( generate_ble_device(TEST_DEVICE_MAC, TEST_DEVICE_NAME), 2, False, - True, + ConnectionError, "Device found but unable to connect.", ), ( generate_ble_device(TEST_DEVICE_MAC, TEST_DEVICE_NAME), 2, True, - False, - "Device found but unable to connect.", + HueBleError, + "Device found and connected but unable to poll values from it.", ), ], ids=["no_device", "no_scanners", "error_connect", "error_poll"], @@ -61,8 +62,8 @@ async def test_setup_error( caplog: pytest.LogCaptureFixture, ble_device: BLEDevice | None, scanner_count: int, - connect_result: bool, - poll_state_result: bool, + connect_result: Exception | None, + poll_state_result: Exception | None, message: str, ) -> None: """Test that ConfigEntryNotReady is raised if there is an error condition.""" @@ -80,11 +81,11 @@ async def test_setup_error( ), patch( "homeassistant.components.hue_ble.HueBleLight.connect", - return_value=connect_result, + side_effect=[connect_result], ), patch( "homeassistant.components.hue_ble.HueBleLight.poll_state", - return_value=poll_state_result, + side_effect=[poll_state_result], ), ): assert await async_setup_component(hass, DOMAIN, {}) is True @@ -111,11 +112,11 @@ async def test_setup( ), patch( "homeassistant.components.hue_ble.HueBleLight.connect", - return_value=True, + return_value=None, ), patch( "homeassistant.components.hue_ble.HueBleLight.poll_state", - return_value=True, + return_value=None, ), ): assert await async_setup_component(hass, DOMAIN, {}) is True diff --git a/tests/components/lutron_caseta/__init__.py b/tests/components/lutron_caseta/__init__.py index 28738445fc653..9315c55bbd161 100644 --- a/tests/components/lutron_caseta/__init__.py +++ b/tests/components/lutron_caseta/__init__.py @@ -113,13 +113,13 @@ async def _activate(self): """Activate smart away.""" self.smart_away_state = "Enabled" for callback in self._smart_away_subscribers: - callback() + callback(self.smart_away_state) async def _deactivate(self): """Deactivate smart away.""" self.smart_away_state = "Disabled" for callback in self._smart_away_subscribers: - callback() + callback(self.smart_away_state) async def connect(self): """Connect the mock bridge.""" diff --git a/tests/components/miele/snapshots/test_sensor.ambr b/tests/components/miele/snapshots/test_sensor.ambr index c2efb3c6d83b6..4d8e94d3f826a 100644 --- a/tests/components/miele/snapshots/test_sensor.ambr +++ b/tests/components/miele/snapshots/test_sensor.ambr @@ -4141,27 +4141,34 @@ 'capabilities': dict({ 'options': list([ 'automatic_plus', + 'bed_linen', 'clean_machine', 'cool_air', + 'cottonrepair', 'cottons', 'cottons_eco', 'cottons_hygiene', 'curtains', 'dark_garments', + 'dark_jeans', 'delicates', 'denim', 'down_duvets', 'down_filled_items', 'drain_spin', + 'easy_care', 'eco_40_60', 'express_20', 'first_wash', 'freshen_up', + 'game_pieces', 'minimum_iron', 'no_program', - 'normal', + 'outdoor_garments', 'outerwear', 'pillows', + 'powerfresh', + 'pre_ironing', 'proofing', 'quick_power_wash', 'rinse', @@ -4169,10 +4176,13 @@ 'separate_rinse_starch', 'shirts', 'silks', + 'smartmatic', 'sportswear', 'starch', 'steam_care', + 'stuffed_toys', 'trainers', + 'trainers_refresh', 'warm_air', 'woollens', ]), @@ -4213,27 +4223,34 @@ 'friendly_name': 'Washing machine Program', 'options': list([ 'automatic_plus', + 'bed_linen', 'clean_machine', 'cool_air', + 'cottonrepair', 'cottons', 'cottons_eco', 'cottons_hygiene', 'curtains', 'dark_garments', + 'dark_jeans', 'delicates', 'denim', 'down_duvets', 'down_filled_items', 'drain_spin', + 'easy_care', 'eco_40_60', 'express_20', 'first_wash', 'freshen_up', + 'game_pieces', 'minimum_iron', 'no_program', - 'normal', + 'outdoor_garments', 'outerwear', 'pillows', + 'powerfresh', + 'pre_ironing', 'proofing', 'quick_power_wash', 'rinse', @@ -4241,10 +4258,13 @@ 'separate_rinse_starch', 'shirts', 'silks', + 'smartmatic', 'sportswear', 'starch', 'steam_care', + 'stuffed_toys', 'trainers', + 'trainers_refresh', 'warm_air', 'woollens', ]), @@ -4265,6 +4285,7 @@ 'capabilities': dict({ 'options': list([ 'anti_crease', + 'automatic_start', 'cleaning', 'cooling_down', 'disinfecting', @@ -4322,6 +4343,7 @@ 'friendly_name': 'Washing machine Program phase', 'options': list([ 'anti_crease', + 'automatic_start', 'cleaning', 'cooling_down', 'disinfecting', @@ -6489,27 +6511,34 @@ 'capabilities': dict({ 'options': list([ 'automatic_plus', + 'bed_linen', 'clean_machine', 'cool_air', + 'cottonrepair', 'cottons', 'cottons_eco', 'cottons_hygiene', 'curtains', 'dark_garments', + 'dark_jeans', 'delicates', 'denim', 'down_duvets', 'down_filled_items', 'drain_spin', + 'easy_care', 'eco_40_60', 'express_20', 'first_wash', 'freshen_up', + 'game_pieces', 'minimum_iron', 'no_program', - 'normal', + 'outdoor_garments', 'outerwear', 'pillows', + 'powerfresh', + 'pre_ironing', 'proofing', 'quick_power_wash', 'rinse', @@ -6517,10 +6546,13 @@ 'separate_rinse_starch', 'shirts', 'silks', + 'smartmatic', 'sportswear', 'starch', 'steam_care', + 'stuffed_toys', 'trainers', + 'trainers_refresh', 'warm_air', 'woollens', ]), @@ -6561,27 +6593,34 @@ 'friendly_name': 'Washing machine Program', 'options': list([ 'automatic_plus', + 'bed_linen', 'clean_machine', 'cool_air', + 'cottonrepair', 'cottons', 'cottons_eco', 'cottons_hygiene', 'curtains', 'dark_garments', + 'dark_jeans', 'delicates', 'denim', 'down_duvets', 'down_filled_items', 'drain_spin', + 'easy_care', 'eco_40_60', 'express_20', 'first_wash', 'freshen_up', + 'game_pieces', 'minimum_iron', 'no_program', - 'normal', + 'outdoor_garments', 'outerwear', 'pillows', + 'powerfresh', + 'pre_ironing', 'proofing', 'quick_power_wash', 'rinse', @@ -6589,10 +6628,13 @@ 'separate_rinse_starch', 'shirts', 'silks', + 'smartmatic', 'sportswear', 'starch', 'steam_care', + 'stuffed_toys', 'trainers', + 'trainers_refresh', 'warm_air', 'woollens', ]), @@ -6613,6 +6655,7 @@ 'capabilities': dict({ 'options': list([ 'anti_crease', + 'automatic_start', 'cleaning', 'cooling_down', 'disinfecting', @@ -6670,6 +6713,7 @@ 'friendly_name': 'Washing machine Program phase', 'options': list([ 'anti_crease', + 'automatic_start', 'cleaning', 'cooling_down', 'disinfecting', diff --git a/tests/components/music_assistant/test_config_flow.py b/tests/components/music_assistant/test_config_flow.py index 48609fb0ae19f..e4352506f7de7 100644 --- a/tests/components/music_assistant/test_config_flow.py +++ b/tests/components/music_assistant/test_config_flow.py @@ -544,28 +544,6 @@ async def test_hassio_flow_errors( assert result["reason"] == error_reason -async def test_hassio_flow_server_not_ready( - hass: HomeAssistant, - mock_get_server_info: AsyncMock, -) -> None: - """Test hassio discovery flow when server onboarding is not complete.""" - server_info = ServerInfoMessage.from_json( - await async_load_fixture(hass, "server_info_message.json", DOMAIN) - ) - server_info.onboard_done = False - mock_get_server_info.return_value = server_info - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_HASSIO}, - data=HASSIO_DATA, - ) - await hass.async_block_till_done() - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "server_not_ready" - - async def test_zeroconf_addon_server_ignored( hass: HomeAssistant, mock_get_server_info: AsyncMock, @@ -587,27 +565,6 @@ async def test_zeroconf_addon_server_ignored( assert result["reason"] == "already_discovered_addon" -async def test_zeroconf_server_not_ready_ignored( - hass: HomeAssistant, - mock_get_server_info: AsyncMock, -) -> None: - """Test zeroconf discovery ignores servers that haven't completed onboarding.""" - not_ready_zeroconf_data = deepcopy(ZEROCONF_DATA) - not_ready_zeroconf_data.properties["onboard_done"] = ( - "False" # Zeroconf properties are strings - ) - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_ZEROCONF}, - data=not_ready_zeroconf_data, - ) - await hass.async_block_till_done() - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "server_not_ready" - - async def test_zeroconf_old_schema_addon_not_ignored( hass: HomeAssistant, mock_get_server_info: AsyncMock, @@ -635,33 +592,6 @@ async def test_zeroconf_old_schema_addon_not_ignored( assert result["step_id"] == "discovery_confirm" -async def test_zeroconf_old_schema_not_ready_not_ignored( - hass: HomeAssistant, - mock_get_server_info: AsyncMock, -) -> None: - """Test zeroconf discovery does NOT ignore not-ready servers with old schema version.""" - old_schema_not_ready_data = deepcopy(ZEROCONF_DATA) - old_schema_version = AUTH_SCHEMA_VERSION - 1 - old_schema_not_ready_data.properties["schema_version"] = str(old_schema_version) - old_schema_not_ready_data.properties["min_supported_schema_version"] = str( - old_schema_version - ) - old_schema_not_ready_data.properties["onboard_done"] = ( - "False" # Zeroconf properties are strings - ) - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_ZEROCONF}, - data=old_schema_not_ready_data, - ) - await hass.async_block_till_done() - - # Should proceed to discovery_confirm, not abort - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "discovery_confirm" - - async def test_user_flow_with_auth_required( hass: HomeAssistant, mock_get_server_info: AsyncMock, diff --git a/tests/components/roborock/conftest.py b/tests/components/roborock/conftest.py index ef568cad8e043..17637b3bc4d6c 100644 --- a/tests/components/roborock/conftest.py +++ b/tests/components/roborock/conftest.py @@ -25,6 +25,7 @@ ZeoState, ) from roborock.devices.device import RoborockDevice +from roborock.devices.device_manager import DeviceManager from roborock.devices.traits.v1 import PropertiesApi from roborock.devices.traits.v1.clean_summary import CleanSummaryTrait from roborock.devices.traits.v1.command import CommandTrait @@ -134,18 +135,6 @@ async def close(self) -> None: """Close the device.""" -class FakeDeviceManager: - """A fake device manager that returns a list of devices.""" - - def __init__(self, devices: list[RoborockDevice]) -> None: - """Initialize the fake device manager.""" - self._devices = devices - - async def get_devices(self) -> list[RoborockDevice]: - """Return the list of devices.""" - return self._devices - - def make_mock_trait( trait_spec: type[V1TraitMixin] | None = None, dataclass_template: RoborockBase | None = None, @@ -348,16 +337,26 @@ def fake_vacuum_command_fixture( return command_trait +@pytest.fixture(name="device_manager") +def device_manager_fixture( + fake_devices: list[FakeDevice], +) -> AsyncMock: + """Fixture to create a fake device manager.""" + device_manager = AsyncMock(spec=DeviceManager) + device_manager.get_devices = AsyncMock(return_value=fake_devices) + return device_manager + + @pytest.fixture(name="fake_create_device_manager", autouse=True) def fake_create_device_manager_fixture( - fake_devices: list[FakeDevice], -) -> Generator[Mock]: + device_manager: AsyncMock, +) -> None: """Fixture to create a fake device manager.""" with patch( "homeassistant.components.roborock.create_device_manager", ) as mock_create_device_manager: - mock_create_device_manager.return_value = FakeDeviceManager(fake_devices) - yield mock_create_device_manager + mock_create_device_manager.return_value = device_manager + yield @pytest.fixture(name="config_entry_data") diff --git a/tests/components/roborock/test_init.py b/tests/components/roborock/test_init.py index 66932405105e0..4773badbe14ff 100644 --- a/tests/components/roborock/test_init.py +++ b/tests/components/roborock/test_init.py @@ -2,7 +2,7 @@ import pathlib from typing import Any -from unittest.mock import patch +from unittest.mock import AsyncMock, patch import pytest from roborock import ( @@ -26,14 +26,42 @@ from tests.typing import ClientSessionGenerator -async def test_unload_entry(hass: HomeAssistant, setup_entry: MockConfigEntry) -> None: - """Test unloading roboorck integration.""" +async def test_unload_entry( + hass: HomeAssistant, + setup_entry: MockConfigEntry, + device_manager: AsyncMock, +) -> None: + """Test unloading roborock integration.""" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert setup_entry.state is ConfigEntryState.LOADED + + assert device_manager.get_devices.called + assert not device_manager.close.called + + # Unload the config entry and verify that the device manager is closed assert await hass.config_entries.async_unload(setup_entry.entry_id) await hass.async_block_till_done() assert setup_entry.state is ConfigEntryState.NOT_LOADED + assert device_manager.close.called + + +async def test_home_assistant_stop( + hass: HomeAssistant, + setup_entry: MockConfigEntry, + device_manager: AsyncMock, +) -> None: + """Test shutting down Home Assistant.""" + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert setup_entry.state is ConfigEntryState.LOADED + + assert not device_manager.close.called + + # Perform Home Assistant stop and verify that device manager is closed + await hass.async_stop() + + assert device_manager.close.called + async def test_reauth_started( hass: HomeAssistant, mock_roborock_entry: MockConfigEntry diff --git a/tests/components/template/test_config.py b/tests/components/template/test_config.py index 5764fd6c5d40e..7cfb43cafc856 100644 --- a/tests/components/template/test_config.py +++ b/tests/components/template/test_config.py @@ -475,3 +475,72 @@ async def test_invalid_schema_raises_issue( assert issue.domain == "template" assert issue.severity == ir.IssueSeverity.WARNING + + +async def test_multiple_configuration_keys( + hass: HomeAssistant, +) -> None: + """Test multiple configurations keys create entities.""" + await async_setup_component( + hass, + "template", + { + "template": [{"binary_sensor": [{"name": "Foo", "state": "{{ True }}"}]}], + "template mytemplates": [ + { + "sensor": [ + {"name": "Foo", "state": "{{ 'bar' }}"}, + {"name": "Bar", "state": "{{ 'foo' }}"}, + ] + } + ], + "template y": [ + { + "cover": [ + { + "name": "Shades Curtain", + "unique_id": "shades_curtain", + "open_cover": [], + "close_cover": [], + "stop_cover": [], + } + ] + }, + { + "cover": [ + { + "open_cover": { + "target": {"entity_id": ["cover.shades_curtain"]}, + "action": "cover.close_cover", + }, + "close_cover": { + "target": {"entity_id": ["cover.shades_curtain"]}, + "action": "cover.open_cover", + }, + "stop_cover": { + "target": {"entity_id": ["cover.shades_curtain"]}, + "action": "cover.stop_cover", + }, + "default_entity_id": "cover.shades_reversed", + "icon": "{% set s = states('cover.shades_curtain') %}\n{% if s == 'open' %}\n mdi:curtains-closed\n{% else %}\n mdi:curtains\n{% endif %}", + "name": "Shades Reversed", + "unique_id": "c0223bcb-32c6-430e-a2c1-3545f8031796", + "state": "{% set s = states('cover.shades_curtain') %}\n{% if s == 'open' %}\n closed\n{% elif s == 'closed' %}\n open\n{% elif s == 'opening' %}\n closing\n{% elif s == 'closing' %}\n opening\n{% else %}\n unknown\n{% endif %}", + } + ] + }, + ], + }, + ) + await hass.async_block_till_done() + + for entity_id, expected in ( + ("binary_sensor.foo", "on"), + ("sensor.foo", "bar"), + ("sensor.bar", "foo"), + ("cover.shades_curtain", "unknown"), + ("cover.shades_reversed", "unknown"), + ): + state = hass.states.get(entity_id) + assert state + assert state.state == expected diff --git a/tests/components/template/test_helpers.py b/tests/components/template/test_helpers.py index 5b27532e8bdb4..ebe1ed6822c1a 100644 --- a/tests/components/template/test_helpers.py +++ b/tests/components/template/test_helpers.py @@ -832,13 +832,14 @@ async def test_legacy_deprecation( "sensors": { "some_sensor": { "friendly_name": "Sensor", + "entity_id": "sensor.some_sensor", "device_class": "timestamp", "value_template": "{{ now().isoformat() }}", } }, }, }, - ["device_class: timestamp"], + ["device_class: timestamp", "entity_id: sensor.some_sensor"], ), ], ) diff --git a/tests/components/xbox/snapshots/test_media_source.ambr b/tests/components/xbox/snapshots/test_media_source.ambr index 1f3118ccb03af..7320f30476bf3 100644 --- a/tests/components/xbox/snapshots/test_media_source.ambr +++ b/tests/components/xbox/snapshots/test_media_source.ambr @@ -259,7 +259,7 @@ 'media_class': , 'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/0', 'media_content_type': , - 'thumbnail': 'http://store-images.s-microsoft.com/image/apps.35725.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.c4bf34f8-ad40-4af3-914e-a85e75a76bed', + 'thumbnail': 'https://store-images.s-microsoft.com/image/apps.35725.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.c4bf34f8-ad40-4af3-914e-a85e75a76bed', 'title': 'Screenshot', }), dict({ @@ -270,7 +270,7 @@ 'media_class': , 'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/1', 'media_content_type': , - 'thumbnail': 'http://store-images.s-microsoft.com/image/apps.64736.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.6491fb2f-52e7-4129-bcbd-d23a67117ae0', + 'thumbnail': 'https://store-images.s-microsoft.com/image/apps.64736.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.6491fb2f-52e7-4129-bcbd-d23a67117ae0', 'title': 'BrandedKeyArt', }), dict({ @@ -281,7 +281,7 @@ 'media_class': , 'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/2', 'media_content_type': , - 'thumbnail': 'http://store-images.s-microsoft.com/image/apps.55545.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.4c2daefb-fbf6-4b90-b392-bf8ecc39a92e', + 'thumbnail': 'https://store-images.s-microsoft.com/image/apps.55545.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.4c2daefb-fbf6-4b90-b392-bf8ecc39a92e', 'title': 'TitledHeroArt', }), dict({ @@ -292,7 +292,7 @@ 'media_class': , 'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/3', 'media_content_type': , - 'thumbnail': 'http://store-images.s-microsoft.com/image/apps.22570.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.bf29284d-808a-4e4a-beaa-6621c9898d0e', + 'thumbnail': 'https://store-images.s-microsoft.com/image/apps.22570.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.bf29284d-808a-4e4a-beaa-6621c9898d0e', 'title': 'Poster', }), dict({ @@ -303,7 +303,7 @@ 'media_class': , 'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/4', 'media_content_type': , - 'thumbnail': 'http://store-images.s-microsoft.com/image/apps.55545.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.4c2daefb-fbf6-4b90-b392-bf8ecc39a92e', + 'thumbnail': 'https://store-images.s-microsoft.com/image/apps.55545.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.4c2daefb-fbf6-4b90-b392-bf8ecc39a92e', 'title': 'SuperHeroArt', }), dict({ @@ -314,7 +314,7 @@ 'media_class': , 'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/5', 'media_content_type': , - 'thumbnail': 'http://store-images.s-microsoft.com/image/apps.45451.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.3abf2cc3-00cc-417d-a93d-97110cdfb261', + 'thumbnail': 'https://store-images.s-microsoft.com/image/apps.45451.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.3abf2cc3-00cc-417d-a93d-97110cdfb261', 'title': 'BoxArt', }), dict({ @@ -325,7 +325,7 @@ 'media_class': , 'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/6', 'media_content_type': , - 'thumbnail': 'http://store-images.s-microsoft.com/image/apps.35072.13670972585585116.70570f0d-17aa-4f97-b692-5412fa183673.25a97451-9369-4f6b-b66b-3427913235eb', + 'thumbnail': 'https://store-images.s-microsoft.com/image/apps.35072.13670972585585116.70570f0d-17aa-4f97-b692-5412fa183673.25a97451-9369-4f6b-b66b-3427913235eb', 'title': 'Logo', }), dict({ @@ -336,7 +336,7 @@ 'media_class': , 'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/7', 'media_content_type': , - 'thumbnail': 'http://store-images.s-microsoft.com/image/apps.45451.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.3abf2cc3-00cc-417d-a93d-97110cdfb261', + 'thumbnail': 'https://store-images.s-microsoft.com/image/apps.45451.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.3abf2cc3-00cc-417d-a93d-97110cdfb261', 'title': 'FeaturePromotionalSquareArt', }), dict({ @@ -347,7 +347,7 @@ 'media_class': , 'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/8', 'media_content_type': , - 'thumbnail': 'http://store-images.s-microsoft.com/image/apps.38628.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.c2a205af-5146-405b-b2b7-56845351f1f3', + 'thumbnail': 'https://store-images.s-microsoft.com/image/apps.38628.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.c2a205af-5146-405b-b2b7-56845351f1f3', 'title': 'Screenshot', }), dict({ @@ -358,7 +358,7 @@ 'media_class': , 'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/9', 'media_content_type': , - 'thumbnail': 'http://store-images.s-microsoft.com/image/apps.22150.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.b147895c-e947-424d-a731-faefc8c9906a', + 'thumbnail': 'https://store-images.s-microsoft.com/image/apps.22150.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.b147895c-e947-424d-a731-faefc8c9906a', 'title': 'Screenshot', }), dict({ @@ -369,7 +369,7 @@ 'media_class': , 'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/10', 'media_content_type': , - 'thumbnail': 'http://store-images.s-microsoft.com/image/apps.37559.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.479d2dc1-db2d-4ffa-8c54-a2bebb093ec6', + 'thumbnail': 'https://store-images.s-microsoft.com/image/apps.37559.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.479d2dc1-db2d-4ffa-8c54-a2bebb093ec6', 'title': 'Screenshot', }), dict({ @@ -380,7 +380,7 @@ 'media_class': , 'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/11', 'media_content_type': , - 'thumbnail': 'http://store-images.s-microsoft.com/image/apps.32737.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.6a16ae3e-2918-46e9-90d9-232c79cb9d9d', + 'thumbnail': 'https://store-images.s-microsoft.com/image/apps.32737.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.6a16ae3e-2918-46e9-90d9-232c79cb9d9d', 'title': 'Screenshot', }), dict({ @@ -391,7 +391,7 @@ 'media_class': , 'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/12', 'media_content_type': , - 'thumbnail': 'http://store-images.s-microsoft.com/image/apps.57046.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.0c0dd072-aa27-4e83-9010-474dfbb42277', + 'thumbnail': 'https://store-images.s-microsoft.com/image/apps.57046.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.0c0dd072-aa27-4e83-9010-474dfbb42277', 'title': 'Screenshot', }), dict({ @@ -402,7 +402,7 @@ 'media_class': , 'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/13', 'media_content_type': , - 'thumbnail': 'http://store-images.s-microsoft.com/image/apps.19315.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.6293a7b7-07ca-4df0-9eea-6018285a0a8d', + 'thumbnail': 'https://store-images.s-microsoft.com/image/apps.19315.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.6293a7b7-07ca-4df0-9eea-6018285a0a8d', 'title': 'Screenshot', }), dict({ @@ -413,7 +413,7 @@ 'media_class': , 'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/14', 'media_content_type': , - 'thumbnail': 'http://store-images.s-microsoft.com/image/apps.23374.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.66498a73-52f5-4247-a1e2-d3c84b9b315d', + 'thumbnail': 'https://store-images.s-microsoft.com/image/apps.23374.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.66498a73-52f5-4247-a1e2-d3c84b9b315d', 'title': 'Screenshot', }), dict({ @@ -424,7 +424,7 @@ 'media_class': , 'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/15', 'media_content_type': , - 'thumbnail': 'http://store-images.s-microsoft.com/image/apps.64646.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.83182b76-4294-496d-90a7-f4e31e7aa80a', + 'thumbnail': 'https://store-images.s-microsoft.com/image/apps.64646.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.83182b76-4294-496d-90a7-f4e31e7aa80a', 'title': 'Screenshot', }), dict({ @@ -435,7 +435,7 @@ 'media_class': , 'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/16', 'media_content_type': , - 'thumbnail': 'http://store-images.s-microsoft.com/image/apps.24470.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.72d2abc3-aa69-4aeb-960b-6f6d25f498e4', + 'thumbnail': 'https://store-images.s-microsoft.com/image/apps.24470.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.72d2abc3-aa69-4aeb-960b-6f6d25f498e4', 'title': 'Screenshot', }), dict({ @@ -446,7 +446,7 @@ 'media_class': , 'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/17', 'media_content_type': , - 'thumbnail': 'http://store-images.s-microsoft.com/image/apps.15604.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.27cee011-660b-49a4-bd33-38db6fff5226', + 'thumbnail': 'https://store-images.s-microsoft.com/image/apps.15604.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.27cee011-660b-49a4-bd33-38db6fff5226', 'title': 'Screenshot', }), dict({ @@ -457,7 +457,7 @@ 'media_class': , 'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/18', 'media_content_type': , - 'thumbnail': 'http://store-images.s-microsoft.com/image/apps.39987.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.be285efe-78f8-4984-9d28-9159881bacd4', + 'thumbnail': 'https://store-images.s-microsoft.com/image/apps.39987.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.be285efe-78f8-4984-9d28-9159881bacd4', 'title': 'Screenshot', }), dict({ @@ -468,7 +468,7 @@ 'media_class': , 'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/19', 'media_content_type': , - 'thumbnail': 'http://store-images.s-microsoft.com/image/apps.38206.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.2409803d-7378-4a69-a10b-1574ac42b98b', + 'thumbnail': 'https://store-images.s-microsoft.com/image/apps.38206.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.2409803d-7378-4a69-a10b-1574ac42b98b', 'title': 'Screenshot', }), dict({ @@ -479,7 +479,7 @@ 'media_class': , 'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/20', 'media_content_type': , - 'thumbnail': 'http://store-images.s-microsoft.com/image/apps.14938.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.ef6ee72c-4beb-45ec-bd10-6235bd6a7c7f', + 'thumbnail': 'https://store-images.s-microsoft.com/image/apps.14938.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.ef6ee72c-4beb-45ec-bd10-6235bd6a7c7f', 'title': 'Screenshot', }), dict({ @@ -490,7 +490,7 @@ 'media_class': , 'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/21', 'media_content_type': , - 'thumbnail': 'http://store-images.s-microsoft.com/image/apps.12835.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.6165ee24-df01-44f5-80fe-7411f9366d1c', + 'thumbnail': 'https://store-images.s-microsoft.com/image/apps.12835.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.6165ee24-df01-44f5-80fe-7411f9366d1c', 'title': 'Screenshot', }), dict({ @@ -501,7 +501,7 @@ 'media_class': , 'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/22', 'media_content_type': , - 'thumbnail': 'http://store-images.s-microsoft.com/image/apps.40786.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.b7607a0d-0101-4864-9bf8-ad889f820489', + 'thumbnail': 'https://store-images.s-microsoft.com/image/apps.40786.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.b7607a0d-0101-4864-9bf8-ad889f820489', 'title': 'Screenshot', }), dict({ @@ -512,7 +512,7 @@ 'media_class': , 'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/23', 'media_content_type': , - 'thumbnail': 'http://store-images.s-microsoft.com/image/apps.55686.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.ecbb0e91-36a9-4f76-ab1e-5a5de009840e', + 'thumbnail': 'https://store-images.s-microsoft.com/image/apps.55686.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.ecbb0e91-36a9-4f76-ab1e-5a5de009840e', 'title': 'Screenshot', }), ]),