This commit is contained in:
Franck Nijhof
2025-12-08 22:35:39 +01:00
committed by GitHub
49 changed files with 410 additions and 361 deletions
@@ -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"]
}
@@ -23,5 +23,5 @@
"winter_mode": {}
},
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20251203.1"]
"requirements": ["home-assistant-frontend==20251203.2"]
}
@@ -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"
}
},
@@ -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"
@@ -7,7 +7,7 @@ from typing import Final
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 @@ UPDATE_INTERVAL: Final = timedelta(hours=1)
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 @@ class GoogleAirQualityUpdateCoordinator(DataUpdateCoordinator[AirQualityData]):
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(
@@ -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"]
}
@@ -4,7 +4,7 @@ from collections.abc import Callable
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 @@ PARALLEL_UPDATES = 0
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, ...] = (
+10 -3
View File
@@ -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
+15 -24
View File
@@ -6,7 +6,7 @@ from enum import Enum
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.core import HomeAssistant
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()
get_available_color_modes(light)
await light.poll_state()
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:
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 @@ class HueBleConfigFlow(ConfigFlow, domain=DOMAIN):
CONF_NAME: self._discovery_info.name,
CONF_MAC: self._discovery_info.address,
"url_pairing_mode": URL_PAIRING_MODE,
"url_factory_reset": URL_FACTORY_RESET,
},
)
@@ -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"
+1 -1
View File
@@ -113,7 +113,7 @@ class HueBLELight(LightEntity):
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."""
@@ -15,5 +15,5 @@
"iot_class": "local_push",
"loggers": ["bleak", "HueBLE"],
"quality_scale": "bronze",
"requirements": ["HueBLE==1.0.8"]
"requirements": ["HueBLE==2.1.0"]
}
@@ -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})."
}
}
}
@@ -98,7 +98,11 @@ class LutronCasetaSmartAwaySwitch(LutronCasetaEntity, SwitchEntity):
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."""
@@ -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
}
+27 -16
View File
@@ -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):
+1 -1
View File
@@ -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."]
}
@@ -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",
@@ -163,9 +163,6 @@ class MusicAssistantConfigFlow(ConfigFlow, domain=DOMAIN):
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 @@ class MusicAssistantConfigFlow(ConfigFlow, domain=DOMAIN):
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
+11 -22
View File
@@ -20,7 +20,7 @@ from roborock.devices.device_manager import UserParams, create_device_manager
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)
@@ -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"
]
}
@@ -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
@@ -422,6 +422,9 @@ async def async_setup_entry(
},
),
supports_response=SupportsResponse.ONLY,
description_placeholders={
"syntax_keys_documentation_url": "http://robotjs.io/docs/syntax#keys"
},
)
hass.services.async_register(
@@ -24,7 +24,7 @@ from homeassistant.helpers import config_validation as cv
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 @@ class SystemBridgeConfigFlow(
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 @@ class SystemBridgeConfigFlow(
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 @@ class SystemBridgeConfigFlow(
data_schema=STEP_AUTHENTICATE_DATA_SCHEMA,
description_placeholders={
"name": self._name,
"syntax_keys_documentation_url": SYNTAX_KEYS_DOCUMENTATION_URL,
},
errors=errors,
)
@@ -4,8 +4,6 @@ from typing import Final
from systembridgemodels.modules import Module
SYNTAX_KEYS_DOCUMENTATION_URL = "http://robotjs.io/docs/syntax#keys"
DOMAIN = "system_bridge"
MODULES: Final[list[Module]] = [
+12 -2
View File
@@ -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
@@ -12,6 +12,7 @@ import voluptuous as vol
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()
):
@@ -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"
}
},
@@ -630,7 +630,7 @@ class XboxSource(MediaSource):
title=image.type,
can_play=True,
can_expand=False,
thumbnail=image.url,
thumbnail=to_https(image.url),
)
for image in game.images
]
+4 -4
View File
@@ -63,6 +63,7 @@ from .device import YeelightDevice
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 @@ def _async_setup_services(hass: HomeAssistant):
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 @@ def _async_setup_services(hass: HomeAssistant):
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,
@@ -306,9 +306,6 @@ class ZWaveServices:
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 @@ class ZWaveServices:
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(
+1 -1
View File
@@ -17,7 +17,7 @@ if TYPE_CHECKING:
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)
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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."
+7 -7
View File
@@ -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
+7 -7
View File
@@ -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
@@ -4,7 +4,7 @@ from collections.abc import AsyncGenerator, Generator
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
@@ -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
@@ -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)
+29 -80
View File
@@ -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 . import HUE_BLE_SERVICE_INFO, TEST_DEVICE_MAC, TEST_DEVICE_NAME
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(
+12 -11
View File
@@ -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 @@ from tests.components.bluetooth import generate_ble_device
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
+2 -2
View File
@@ -113,13 +113,13 @@ class MockBridge:
"""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."""
@@ -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',
@@ -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,
+15 -16
View File
@@ -25,6 +25,7 @@ from roborock.data import (
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 @@ class FakeDevice(RoborockDevice):
"""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")
+31 -3
View File
@@ -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.common import MockConfigEntry
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
+69
View File
@@ -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
+2 -1
View File
@@ -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"],
),
],
)
@@ -259,7 +259,7 @@
'media_class': <MediaClass.VIDEO: 'video'>,
'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/0',
'media_content_type': <MediaClass.VIDEO: 'video'>,
'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': <MediaClass.VIDEO: 'video'>,
'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/1',
'media_content_type': <MediaClass.VIDEO: 'video'>,
'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': <MediaClass.VIDEO: 'video'>,
'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/2',
'media_content_type': <MediaClass.VIDEO: 'video'>,
'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': <MediaClass.VIDEO: 'video'>,
'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/3',
'media_content_type': <MediaClass.VIDEO: 'video'>,
'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': <MediaClass.VIDEO: 'video'>,
'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/4',
'media_content_type': <MediaClass.VIDEO: 'video'>,
'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': <MediaClass.VIDEO: 'video'>,
'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/5',
'media_content_type': <MediaClass.VIDEO: 'video'>,
'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': <MediaClass.VIDEO: 'video'>,
'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/6',
'media_content_type': <MediaClass.VIDEO: 'video'>,
'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': <MediaClass.VIDEO: 'video'>,
'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/7',
'media_content_type': <MediaClass.VIDEO: 'video'>,
'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': <MediaClass.VIDEO: 'video'>,
'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/8',
'media_content_type': <MediaClass.VIDEO: 'video'>,
'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': <MediaClass.VIDEO: 'video'>,
'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/9',
'media_content_type': <MediaClass.VIDEO: 'video'>,
'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': <MediaClass.VIDEO: 'video'>,
'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/10',
'media_content_type': <MediaClass.VIDEO: 'video'>,
'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': <MediaClass.VIDEO: 'video'>,
'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/11',
'media_content_type': <MediaClass.VIDEO: 'video'>,
'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': <MediaClass.VIDEO: 'video'>,
'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/12',
'media_content_type': <MediaClass.VIDEO: 'video'>,
'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': <MediaClass.VIDEO: 'video'>,
'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/13',
'media_content_type': <MediaClass.VIDEO: 'video'>,
'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': <MediaClass.VIDEO: 'video'>,
'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/14',
'media_content_type': <MediaClass.VIDEO: 'video'>,
'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': <MediaClass.VIDEO: 'video'>,
'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/15',
'media_content_type': <MediaClass.VIDEO: 'video'>,
'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': <MediaClass.VIDEO: 'video'>,
'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/16',
'media_content_type': <MediaClass.VIDEO: 'video'>,
'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': <MediaClass.VIDEO: 'video'>,
'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/17',
'media_content_type': <MediaClass.VIDEO: 'video'>,
'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': <MediaClass.VIDEO: 'video'>,
'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/18',
'media_content_type': <MediaClass.VIDEO: 'video'>,
'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': <MediaClass.VIDEO: 'video'>,
'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/19',
'media_content_type': <MediaClass.VIDEO: 'video'>,
'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': <MediaClass.VIDEO: 'video'>,
'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/20',
'media_content_type': <MediaClass.VIDEO: 'video'>,
'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': <MediaClass.VIDEO: 'video'>,
'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/21',
'media_content_type': <MediaClass.VIDEO: 'video'>,
'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': <MediaClass.VIDEO: 'video'>,
'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/22',
'media_content_type': <MediaClass.VIDEO: 'video'>,
'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': <MediaClass.VIDEO: 'video'>,
'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/23',
'media_content_type': <MediaClass.VIDEO: 'video'>,
'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',
}),
]),