mirror of
https://github.com/home-assistant/core.git
synced 2026-09-04 18:15:04 -05:00
2025.12.2 (#158274)
This commit is contained in:
@@ -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, ...] = (
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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]] = [
|
||||
|
||||
@@ -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
|
||||
]
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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."
|
||||
|
||||
Generated
+7
-7
@@ -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
|
||||
|
||||
Generated
+7
-7
@@ -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)
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
}),
|
||||
]),
|
||||
|
||||
Reference in New Issue
Block a user