diff --git a/homeassistant/components/hive/manifest.json b/homeassistant/components/hive/manifest.json index 53af76c7482a0..4938e1dc1ad7f 100644 --- a/homeassistant/components/hive/manifest.json +++ b/homeassistant/components/hive/manifest.json @@ -9,5 +9,5 @@ }, "iot_class": "cloud_polling", "loggers": ["apyhiveapi"], - "requirements": ["pyhive-integration==1.0.6"] + "requirements": ["pyhive-integration==1.0.7"] } diff --git a/homeassistant/components/hyperion/camera.py b/homeassistant/components/hyperion/camera.py index ae9c9ba90256f..bd96c9667ad58 100644 --- a/homeassistant/components/hyperion/camera.py +++ b/homeassistant/components/hyperion/camera.py @@ -13,6 +13,7 @@ from aiohttp import web from hyperion import client from hyperion.const import ( + KEY_DATA, KEY_IMAGE, KEY_IMAGE_STREAM, KEY_LEDCOLORS, @@ -155,7 +156,8 @@ async def _update_imagestream(self, img: dict[str, Any] | None = None) -> None: """Update Hyperion components.""" if not img: return - img_data = img.get(KEY_RESULT, {}).get(KEY_IMAGE) + # Prefer KEY_DATA (Hyperion server >= 2.1.1); fall back to KEY_RESULT for older server versions + img_data = img.get(KEY_DATA, img.get(KEY_RESULT, {})).get(KEY_IMAGE) if not img_data or not img_data.startswith(IMAGE_STREAM_JPG_SENTINEL): return async with self._image_cond: diff --git a/homeassistant/components/music_assistant/config_flow.py b/homeassistant/components/music_assistant/config_flow.py index 3426a08852a99..bfb9cb3b0bfb4 100644 --- a/homeassistant/components/music_assistant/config_flow.py +++ b/homeassistant/components/music_assistant/config_flow.py @@ -13,7 +13,7 @@ from music_assistant_models.api import ServerInfoMessage import voluptuous as vol -from homeassistant.config_entries import SOURCE_IGNORE, ConfigFlow, ConfigFlowResult +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client @@ -21,21 +21,14 @@ from .const import DOMAIN, LOGGER -DEFAULT_URL = "http://mass.local:8095" DEFAULT_TITLE = "Music Assistant" +DEFAULT_URL = "http://mass.local:8095" -def get_manual_schema(user_input: dict[str, Any]) -> vol.Schema: - """Return a schema for the manual step.""" - default_url = user_input.get(CONF_URL, DEFAULT_URL) - return vol.Schema( - { - vol.Required(CONF_URL, default=default_url): str, - } - ) +STEP_USER_SCHEMA = vol.Schema({vol.Required(CONF_URL): str}) -async def get_server_info(hass: HomeAssistant, url: str) -> ServerInfoMessage: +async def _get_server_info(hass: HomeAssistant, url: str) -> ServerInfoMessage: """Validate the user input allows us to connect.""" async with MusicAssistantClient( url, aiohttp_client.async_get_clientsession(hass) @@ -52,25 +45,17 @@ class MusicAssistantConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Set up flow instance.""" - self.server_info: ServerInfoMessage | None = None + self.url: str | None = None async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle a manual configuration.""" errors: dict[str, str] = {} + if user_input is not None: try: - self.server_info = await get_server_info( - self.hass, user_input[CONF_URL] - ) - await self.async_set_unique_id( - self.server_info.server_id, raise_on_progress=False - ) - self._abort_if_unique_id_configured( - updates={CONF_URL: user_input[CONF_URL]}, - reload_on_update=True, - ) + server_info = await _get_server_info(self.hass, user_input[CONF_URL]) except CannotConnect: errors["base"] = "cannot_connect" except InvalidServerVersion: @@ -79,68 +64,49 @@ async def async_step_user( LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: + await self.async_set_unique_id( + server_info.server_id, raise_on_progress=False + ) + self._abort_if_unique_id_configured( + updates={CONF_URL: user_input[CONF_URL]} + ) + return self.async_create_entry( title=DEFAULT_TITLE, - data={ - CONF_URL: user_input[CONF_URL], - }, + data={CONF_URL: user_input[CONF_URL]}, ) - return self.async_show_form( - step_id="user", data_schema=get_manual_schema(user_input), errors=errors - ) + suggested_values = user_input + if suggested_values is None: + suggested_values = {CONF_URL: DEFAULT_URL} - return self.async_show_form(step_id="user", data_schema=get_manual_schema({})) + return self.async_show_form( + step_id="user", + data_schema=self.add_suggested_values_to_schema( + STEP_USER_SCHEMA, suggested_values + ), + errors=errors, + ) async def async_step_zeroconf( self, discovery_info: ZeroconfServiceInfo ) -> ConfigFlowResult: - """Handle a discovered Mass server. - - This flow is triggered by the Zeroconf component. It will check if the - host is already configured and delegate to the import step if not. - """ - # abort if discovery info is not what we expect - if "server_id" not in discovery_info.properties: - return self.async_abort(reason="missing_server_id") - - self.server_info = ServerInfoMessage.from_dict(discovery_info.properties) - await self.async_set_unique_id(self.server_info.server_id) + """Handle a zeroconf discovery for a Music Assistant server.""" + try: + server_info = ServerInfoMessage.from_dict(discovery_info.properties) + except LookupError: + return self.async_abort(reason="invalid_discovery_info") - # Check if we already have a config entry for this server_id - existing_entry = self.hass.config_entries.async_entry_for_domain_unique_id( - DOMAIN, self.server_info.server_id - ) + self.url = server_info.base_url - if existing_entry: - # If the entry was ignored or disabled, don't make any changes - if existing_entry.source == SOURCE_IGNORE or existing_entry.disabled_by: - return self.async_abort(reason="already_configured") + await self.async_set_unique_id(server_info.server_id) + self._abort_if_unique_id_configured(updates={CONF_URL: self.url}) - # Test connectivity to the current URL first - current_url = existing_entry.data[CONF_URL] - try: - await get_server_info(self.hass, current_url) - # Current URL is working, no need to update - return self.async_abort(reason="already_configured") - except CannotConnect: - # Current URL is not working, update to the discovered URL - # and continue to discovery confirm - self.hass.config_entries.async_update_entry( - existing_entry, - data={**existing_entry.data, CONF_URL: self.server_info.base_url}, - ) - # Schedule reload since URL changed - self.hass.config_entries.async_schedule_reload(existing_entry.entry_id) - else: - # No existing entry, proceed with normal flow - self._abort_if_unique_id_configured() - - # Test connectivity to the discovered URL try: - await get_server_info(self.hass, self.server_info.base_url) + await _get_server_info(self.hass, self.url) except CannotConnect: return self.async_abort(reason="cannot_connect") + return await self.async_step_discovery_confirm() async def async_step_discovery_confirm( @@ -148,16 +114,16 @@ async def async_step_discovery_confirm( ) -> ConfigFlowResult: """Handle user-confirmation of discovered server.""" if TYPE_CHECKING: - assert self.server_info is not None + assert self.url is not None + if user_input is not None: return self.async_create_entry( title=DEFAULT_TITLE, - data={ - CONF_URL: self.server_info.base_url, - }, + data={CONF_URL: self.url}, ) + self._set_confirm_only() return self.async_show_form( step_id="discovery_confirm", - description_placeholders={"url": self.server_info.base_url}, + description_placeholders={"url": self.url}, ) diff --git a/homeassistant/components/switchbot/__init__.py b/homeassistant/components/switchbot/__init__.py index cd8a8b08f1d33..04f69548a4244 100644 --- a/homeassistant/components/switchbot/__init__.py +++ b/homeassistant/components/switchbot/__init__.py @@ -75,6 +75,7 @@ SupportedModels.HUBMINI_MATTER.value: [Platform.SENSOR], SupportedModels.CIRCULATOR_FAN.value: [Platform.FAN, Platform.SENSOR], SupportedModels.S10_VACUUM.value: [Platform.VACUUM, Platform.SENSOR], + SupportedModels.S20_VACUUM.value: [Platform.VACUUM, Platform.SENSOR], SupportedModels.K10_VACUUM.value: [Platform.VACUUM, Platform.SENSOR], SupportedModels.K10_PRO_VACUUM.value: [Platform.VACUUM, Platform.SENSOR], SupportedModels.K10_PRO_COMBO_VACUUM.value: [Platform.VACUUM, Platform.SENSOR], @@ -102,6 +103,10 @@ SupportedModels.RELAY_SWITCH_2PM.value: [Platform.SWITCH, Platform.SENSOR], SupportedModels.GARAGE_DOOR_OPENER.value: [Platform.COVER, Platform.SENSOR], SupportedModels.CLIMATE_PANEL.value: [Platform.SENSOR, Platform.BINARY_SENSOR], + SupportedModels.SMART_THERMOSTAT_RADIATOR.value: [ + Platform.CLIMATE, + Platform.SENSOR, + ], } CLASS_BY_DEVICE = { SupportedModels.CEILING_LIGHT.value: switchbot.SwitchbotCeilingLight, @@ -119,6 +124,7 @@ SupportedModels.ROLLER_SHADE.value: switchbot.SwitchbotRollerShade, SupportedModels.CIRCULATOR_FAN.value: switchbot.SwitchbotFan, SupportedModels.S10_VACUUM.value: switchbot.SwitchbotVacuum, + SupportedModels.S20_VACUUM.value: switchbot.SwitchbotVacuum, SupportedModels.K10_VACUUM.value: switchbot.SwitchbotVacuum, SupportedModels.K10_PRO_VACUUM.value: switchbot.SwitchbotVacuum, SupportedModels.K10_PRO_COMBO_VACUUM.value: switchbot.SwitchbotVacuum, @@ -136,6 +142,7 @@ SupportedModels.PLUG_MINI_EU.value: switchbot.SwitchbotRelaySwitch, SupportedModels.RELAY_SWITCH_2PM.value: switchbot.SwitchbotRelaySwitch2PM, SupportedModels.GARAGE_DOOR_OPENER.value: switchbot.SwitchbotGarageDoorOpener, + SupportedModels.SMART_THERMOSTAT_RADIATOR.value: switchbot.SwitchbotSmartThermostatRadiator, } diff --git a/homeassistant/components/switchbot/climate.py b/homeassistant/components/switchbot/climate.py new file mode 100644 index 0000000000000..79b05388d2264 --- /dev/null +++ b/homeassistant/components/switchbot/climate.py @@ -0,0 +1,140 @@ +"""Support for Switchbot Climate devices.""" + +from __future__ import annotations + +import logging +from typing import Any + +import switchbot +from switchbot import ( + ClimateAction as SwitchBotClimateAction, + ClimateMode as SwitchBotClimateMode, +) + +from homeassistant.components.climate import ( + ClimateEntity, + ClimateEntityFeature, + HVACAction, + HVACMode, +) +from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from .coordinator import SwitchbotConfigEntry +from .entity import SwitchbotEntity, exception_handler + +SWITCHBOT_CLIMATE_TO_HASS_HVAC_MODE = { + SwitchBotClimateMode.HEAT: HVACMode.HEAT, + SwitchBotClimateMode.OFF: HVACMode.OFF, +} + +HASS_HVAC_MODE_TO_SWITCHBOT_CLIMATE = { + HVACMode.HEAT: SwitchBotClimateMode.HEAT, + HVACMode.OFF: SwitchBotClimateMode.OFF, +} + +SWITCHBOT_ACTION_TO_HASS_HVAC_ACTION = { + SwitchBotClimateAction.HEATING: HVACAction.HEATING, + SwitchBotClimateAction.IDLE: HVACAction.IDLE, + SwitchBotClimateAction.OFF: HVACAction.OFF, +} + +_LOGGER = logging.getLogger(__name__) +PARALLEL_UPDATES = 0 + + +async def async_setup_entry( + hass: HomeAssistant, + entry: SwitchbotConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up Switchbot climate based on a config entry.""" + coordinator = entry.runtime_data + async_add_entities([SwitchBotClimateEntity(coordinator)]) + + +class SwitchBotClimateEntity(SwitchbotEntity, ClimateEntity): + """Representation of a Switchbot Climate device.""" + + _device: switchbot.SwitchbotDevice + _attr_supported_features = ( + ClimateEntityFeature.PRESET_MODE + | ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON + ) + _attr_target_temperature_step = 0.5 + _attr_temperature_unit = UnitOfTemperature.CELSIUS + _attr_translation_key = "climate" + _attr_name = None + + @property + def min_temp(self) -> float: + """Return the minimum temperature.""" + return self._device.min_temperature + + @property + def max_temp(self) -> float: + """Return the maximum temperature.""" + return self._device.max_temperature + + @property + def preset_modes(self) -> list[str] | None: + """Return the list of available preset modes.""" + return self._device.preset_modes + + @property + def preset_mode(self) -> str | None: + """Return the current preset mode.""" + return self._device.preset_mode + + @property + def hvac_mode(self) -> HVACMode | None: + """Return the current HVAC mode.""" + return SWITCHBOT_CLIMATE_TO_HASS_HVAC_MODE.get( + self._device.hvac_mode, HVACMode.OFF + ) + + @property + def hvac_modes(self) -> list[HVACMode]: + """Return the list of available HVAC modes.""" + return [ + SWITCHBOT_CLIMATE_TO_HASS_HVAC_MODE[mode] + for mode in self._device.hvac_modes + ] + + @property + def hvac_action(self) -> HVACAction | None: + """Return the current HVAC action.""" + return SWITCHBOT_ACTION_TO_HASS_HVAC_ACTION.get( + self._device.hvac_action, HVACAction.OFF + ) + + @property + def current_temperature(self) -> float | None: + """Return the current temperature.""" + return self._device.current_temperature + + @property + def target_temperature(self) -> float | None: + """Return the temperature we try to reach.""" + return self._device.target_temperature + + @exception_handler + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: + """Set new HVAC mode.""" + return await self._device.set_hvac_mode( + HASS_HVAC_MODE_TO_SWITCHBOT_CLIMATE[hvac_mode] + ) + + @exception_handler + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set new preset mode.""" + return await self._device.set_preset_mode(preset_mode) + + @exception_handler + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set new target temperature.""" + temperature = kwargs.get(ATTR_TEMPERATURE) + return await self._device.set_target_temperature(temperature) diff --git a/homeassistant/components/switchbot/const.py b/homeassistant/components/switchbot/const.py index 4c29ed7d67298..b2677fee38c97 100644 --- a/homeassistant/components/switchbot/const.py +++ b/homeassistant/components/switchbot/const.py @@ -58,6 +58,8 @@ class SupportedModels(StrEnum): K11_PLUS_VACUUM = "k11+_vacuum" GARAGE_DOOR_OPENER = "garage_door_opener" CLIMATE_PANEL = "climate_panel" + SMART_THERMOSTAT_RADIATOR = "smart_thermostat_radiator" + S20_VACUUM = "s20_vacuum" CONNECTABLE_SUPPORTED_MODEL_TYPES = { @@ -78,6 +80,7 @@ class SupportedModels(StrEnum): SwitchbotModel.CIRCULATOR_FAN: SupportedModels.CIRCULATOR_FAN, SwitchbotModel.K20_VACUUM: SupportedModels.K20_VACUUM, SwitchbotModel.S10_VACUUM: SupportedModels.S10_VACUUM, + SwitchbotModel.S20_VACUUM: SupportedModels.S20_VACUUM, SwitchbotModel.K10_VACUUM: SupportedModels.K10_VACUUM, SwitchbotModel.K10_PRO_VACUUM: SupportedModels.K10_PRO_VACUUM, SwitchbotModel.K10_PRO_COMBO_VACUUM: SupportedModels.K10_PRO_COMBO_VACUUM, @@ -95,6 +98,7 @@ class SupportedModels(StrEnum): SwitchbotModel.K11_VACUUM: SupportedModels.K11_PLUS_VACUUM, SwitchbotModel.GARAGE_DOOR_OPENER: SupportedModels.GARAGE_DOOR_OPENER, SwitchbotModel.CLIMATE_PANEL: SupportedModels.CLIMATE_PANEL, + SwitchbotModel.SMART_THERMOSTAT_RADIATOR: SupportedModels.SMART_THERMOSTAT_RADIATOR, } NON_CONNECTABLE_SUPPORTED_MODEL_TYPES = { @@ -132,6 +136,7 @@ class SupportedModels(StrEnum): SwitchbotModel.PLUG_MINI_EU, SwitchbotModel.RELAY_SWITCH_2PM, SwitchbotModel.GARAGE_DOOR_OPENER, + SwitchbotModel.SMART_THERMOSTAT_RADIATOR, } ENCRYPTED_SWITCHBOT_MODEL_TO_CLASS: dict[ @@ -153,6 +158,7 @@ class SupportedModels(StrEnum): SwitchbotModel.PLUG_MINI_EU: switchbot.SwitchbotRelaySwitch, SwitchbotModel.RELAY_SWITCH_2PM: switchbot.SwitchbotRelaySwitch2PM, SwitchbotModel.GARAGE_DOOR_OPENER: switchbot.SwitchbotRelaySwitch, + SwitchbotModel.SMART_THERMOSTAT_RADIATOR: switchbot.SwitchbotSmartThermostatRadiator, } HASS_SENSOR_TYPE_TO_SWITCHBOT_MODEL = { diff --git a/homeassistant/components/switchbot/icons.json b/homeassistant/components/switchbot/icons.json index db0ccd3f49c41..856b4bc1f983b 100644 --- a/homeassistant/components/switchbot/icons.json +++ b/homeassistant/components/switchbot/icons.json @@ -1,5 +1,18 @@ { "entity": { + "climate": { + "climate": { + "state_attributes": { + "preset_mode": { + "state": { + "manual": "mdi:hand-back-right", + "off": "mdi:hvac-off", + "schedule": "mdi:calendar-clock" + } + } + } + } + }, "fan": { "air_purifier": { "default": "mdi:air-purifier", diff --git a/homeassistant/components/switchbot/strings.json b/homeassistant/components/switchbot/strings.json index 91b3ba7b85f00..06b8732f9ccd3 100644 --- a/homeassistant/components/switchbot/strings.json +++ b/homeassistant/components/switchbot/strings.json @@ -100,6 +100,19 @@ "name": "Unlocked alarm" } }, + "climate": { + "climate": { + "state_attributes": { + "preset_mode": { + "state": { + "manual": "[%key:common::state::manual%]", + "off": "[%key:common::state::off%]", + "schedule": "Schedule" + } + } + } + } + }, "cover": { "cover": { "state_attributes": { diff --git a/homeassistant/components/tplink/climate.py b/homeassistant/components/tplink/climate.py index 45e4575b4e316..c8e7dee8d7375 100644 --- a/homeassistant/components/tplink/climate.py +++ b/homeassistant/components/tplink/climate.py @@ -181,15 +181,14 @@ def _async_update_attrs(self) -> bool: HVACMode.HEAT if self._thermostat_module.state else HVACMode.OFF ) - if ( - self._thermostat_module.mode not in STATE_TO_ACTION - and self._attr_hvac_action is not HVACAction.OFF - ): - _LOGGER.warning( - "Unknown thermostat state, defaulting to OFF: %s", - self._thermostat_module.mode, - ) - self._attr_hvac_action = HVACAction.OFF + if self._thermostat_module.mode not in STATE_TO_ACTION: + # Report a warning on the first non-default unknown mode + if self._attr_hvac_action is not HVACAction.OFF: + _LOGGER.warning( + "Unknown thermostat state, defaulting to OFF: %s", + self._thermostat_module.mode, + ) + self._attr_hvac_action = HVACAction.OFF return True self._attr_hvac_action = STATE_TO_ACTION[self._thermostat_module.mode] diff --git a/homeassistant/components/ukraine_alarm/__init__.py b/homeassistant/components/ukraine_alarm/__init__.py index 3658b82162577..c5cdd3bfb3eaa 100644 --- a/homeassistant/components/ukraine_alarm/__init__.py +++ b/homeassistant/components/ukraine_alarm/__init__.py @@ -2,13 +2,23 @@ from __future__ import annotations +import logging +from typing import TYPE_CHECKING + +import aiohttp +from uasiren.client import Client + from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_NAME, CONF_REGION from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN, PLATFORMS from .coordinator import UkraineAlarmDataUpdateCoordinator +_LOGGER = logging.getLogger(__name__) + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Ukraine Alarm as config entry.""" @@ -30,3 +40,56 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok + + +async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Migrate old entry.""" + _LOGGER.debug("Migrating from version %s", config_entry.version) + + if config_entry.version == 1: + # Version 1 had states as first-class selections + # Version 2 only allows states w/o districts, districts and communities + region_id = config_entry.data[CONF_REGION] + + websession = async_get_clientsession(hass) + try: + regions_data = await Client(websession).get_regions() + except (aiohttp.ClientError, TimeoutError) as err: + _LOGGER.warning( + "Could not migrate config entry %s: failed to fetch current regions: %s", + config_entry.entry_id, + err, + ) + return False + + if TYPE_CHECKING: + assert isinstance(regions_data, dict) + + state_with_districts = None + for state in regions_data["states"]: + if state["regionId"] == region_id and state.get("regionChildIds"): + state_with_districts = state + break + + if state_with_districts: + ir.async_create_issue( + hass, + DOMAIN, + f"deprecated_state_region_{config_entry.entry_id}", + is_fixable=False, + issue_domain=DOMAIN, + severity=ir.IssueSeverity.WARNING, + translation_key="deprecated_state_region", + translation_placeholders={ + "region_name": config_entry.data.get(CONF_NAME, region_id), + }, + ) + + return False + + hass.config_entries.async_update_entry(config_entry, version=2) + _LOGGER.info("Migration to version %s successful", 2) + return True + + _LOGGER.error("Unknown version %s", config_entry.version) + return False diff --git a/homeassistant/components/ukraine_alarm/config_flow.py b/homeassistant/components/ukraine_alarm/config_flow.py index 12059124fa2f3..c65b1a3713f54 100644 --- a/homeassistant/components/ukraine_alarm/config_flow.py +++ b/homeassistant/components/ukraine_alarm/config_flow.py @@ -21,7 +21,7 @@ class UkraineAlarmConfigFlow(ConfigFlow, domain=DOMAIN): """Config flow for Ukraine Alarm.""" - VERSION = 1 + VERSION = 2 def __init__(self) -> None: """Initialize a new UkraineAlarmConfigFlow.""" @@ -112,7 +112,7 @@ async def _handle_pick_region( return await self._async_finish_flow() regions = {} - if self.selected_region: + if self.selected_region and step_id != "district": regions[self.selected_region["regionId"]] = self.selected_region[ "regionName" ] diff --git a/homeassistant/components/ukraine_alarm/strings.json b/homeassistant/components/ukraine_alarm/strings.json index a8c54ba934bcc..5a586c19fb8c9 100644 --- a/homeassistant/components/ukraine_alarm/strings.json +++ b/homeassistant/components/ukraine_alarm/strings.json @@ -13,19 +13,19 @@ "data": { "region": "[%key:component::ukraine_alarm::config::step::user::data::region%]" }, - "description": "If you want to monitor not only state and district, choose its specific community" + "description": "Choose the district you selected above or select a specific community within that district" }, "district": { "data": { "region": "[%key:component::ukraine_alarm::config::step::user::data::region%]" }, - "description": "If you want to monitor not only state, choose its specific district" + "description": "Choose a district to monitor within the selected state" }, "user": { "data": { "region": "Region" }, - "description": "Choose state to monitor" + "description": "Choose a state" } } }, @@ -50,5 +50,11 @@ "name": "Urban fights" } } + }, + "issues": { + "deprecated_state_region": { + "description": "The region `{region_name}` is a state-level region, which is no longer supported. Please remove this integration entry and add it again, selecting a district or community instead of the entire state.", + "title": "State-level region monitoring is no longer supported" + } } } diff --git a/homeassistant/components/velux/__init__.py b/homeassistant/components/velux/__init__.py index e53514ae19fc1..56398469b8537 100644 --- a/homeassistant/components/velux/__init__.py +++ b/homeassistant/components/velux/__init__.py @@ -1,17 +1,20 @@ """Support for VELUX KLF 200 devices.""" +from __future__ import annotations + from pyvlx import PyVLX, PyVLXException from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.helpers import device_registry as dr from .const import DOMAIN, LOGGER, PLATFORMS type VeluxConfigEntry = ConfigEntry[PyVLX] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: VeluxConfigEntry) -> bool: """Set up the velux component.""" host = entry.data[CONF_HOST] password = entry.data[CONF_PASSWORD] @@ -27,6 +30,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.runtime_data = pyvlx + device_registry = dr.async_get(hass) + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, f"gateway_{entry.entry_id}")}, + name="KLF 200 Gateway", + manufacturer="Velux", + model="KLF 200", + hw_version=( + str(pyvlx.klf200.version.hardwareversion) if pyvlx.klf200.version else None + ), + sw_version=( + str(pyvlx.klf200.version.softwareversion) if pyvlx.klf200.version else None + ), + ) + async def on_hass_stop(event): """Close connection when hass stops.""" LOGGER.debug("Velux interface terminated") @@ -46,6 +64,6 @@ async def async_reboot_gateway(service_call: ServiceCall) -> None: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: VeluxConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/velux/binary_sensor.py b/homeassistant/components/velux/binary_sensor.py index c5dd12ad2ef9e..d956e55c4599c 100644 --- a/homeassistant/components/velux/binary_sensor.py +++ b/homeassistant/components/velux/binary_sensor.py @@ -24,14 +24,14 @@ async def async_setup_entry( hass: HomeAssistant, - config: VeluxConfigEntry, + config_entry: VeluxConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up rain sensor(s) for Velux platform.""" - pyvlx = config.runtime_data + pyvlx = config_entry.runtime_data async_add_entities( - VeluxRainSensor(node, config.entry_id) + VeluxRainSensor(node, config_entry.entry_id) for node in pyvlx.nodes if isinstance(node, Window) and node.rain_sensor ) diff --git a/homeassistant/components/velux/cover.py b/homeassistant/components/velux/cover.py index 6207611403c43..5a371d6061ab6 100644 --- a/homeassistant/components/velux/cover.py +++ b/homeassistant/components/velux/cover.py @@ -32,13 +32,13 @@ async def async_setup_entry( hass: HomeAssistant, - config: VeluxConfigEntry, + config_entry: VeluxConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up cover(s) for Velux platform.""" - pyvlx = config.runtime_data + pyvlx = config_entry.runtime_data async_add_entities( - VeluxCover(node, config.entry_id) + VeluxCover(node, config_entry.entry_id) for node in pyvlx.nodes if isinstance(node, OpeningDevice) ) diff --git a/homeassistant/components/velux/entity.py b/homeassistant/components/velux/entity.py index fa06598f97958..27c3d57e37be7 100644 --- a/homeassistant/components/velux/entity.py +++ b/homeassistant/components/velux/entity.py @@ -18,22 +18,23 @@ class VeluxEntity(Entity): def __init__(self, node: Node, config_entry_id: str) -> None: """Initialize the Velux device.""" self.node = node - self._attr_unique_id = ( + unique_id = ( node.serial_number if node.serial_number else f"{config_entry_id}_{node.node_id}" ) + self._attr_unique_id = unique_id + self._attr_device_info = DeviceInfo( identifiers={ ( DOMAIN, - node.serial_number - if node.serial_number - else f"{config_entry_id}_{node.node_id}", + unique_id, ) }, name=node.name if node.name else f"#{node.node_id}", serial_number=node.serial_number, + via_device=(DOMAIN, f"gateway_{config_entry_id}"), ) @callback diff --git a/homeassistant/components/velux/light.py b/homeassistant/components/velux/light.py index 6006cd93a4f24..f6b6eaecce0a2 100644 --- a/homeassistant/components/velux/light.py +++ b/homeassistant/components/velux/light.py @@ -18,13 +18,13 @@ async def async_setup_entry( hass: HomeAssistant, - config: VeluxConfigEntry, + config_entry: VeluxConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up light(s) for Velux platform.""" - pyvlx = config.runtime_data + pyvlx = config_entry.runtime_data async_add_entities( - VeluxLight(node, config.entry_id) + VeluxLight(node, config_entry.entry_id) for node in pyvlx.nodes if isinstance(node, LighteningDevice) ) diff --git a/homeassistant/components/velux/scene.py b/homeassistant/components/velux/scene.py index f844070736be9..8067dc51130f1 100644 --- a/homeassistant/components/velux/scene.py +++ b/homeassistant/components/velux/scene.py @@ -15,11 +15,11 @@ async def async_setup_entry( hass: HomeAssistant, - config: VeluxConfigEntry, + config_entry: VeluxConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the scenes for Velux platform.""" - pyvlx = config.runtime_data + pyvlx = config_entry.runtime_data entities = [VeluxScene(scene) for scene in pyvlx.scenes] async_add_entities(entities) diff --git a/homeassistant/components/vicare/sensor.py b/homeassistant/components/vicare/sensor.py index 82764f88019f1..ca204ff47a584 100644 --- a/homeassistant/components/vicare/sensor.py +++ b/homeassistant/components/vicare/sensor.py @@ -58,6 +58,7 @@ get_compressors, get_device_serial, is_supported, + normalize_state, ) _LOGGER = logging.getLogger(__name__) @@ -1086,7 +1087,7 @@ class ViCareSensorEntityDescription(SensorEntityDescription, ViCareRequiredKeysM ViCareSensorEntityDescription( key="compressor_phase", translation_key="compressor_phase", - value_getter=lambda api: api.getPhase(), + value_getter=lambda api: normalize_state(api.getPhase()), entity_category=EntityCategory.DIAGNOSTIC, ), ) diff --git a/homeassistant/components/vicare/strings.json b/homeassistant/components/vicare/strings.json index d90adaa7fb535..17eed6fae9842 100644 --- a/homeassistant/components/vicare/strings.json +++ b/homeassistant/components/vicare/strings.json @@ -213,7 +213,18 @@ "name": "Compressor hours load class 5" }, "compressor_phase": { - "name": "Compressor phase" + "name": "Compressor phase", + "state": { + "cooling": "[%key:component::climate::entity_component::_::state_attributes::hvac_action::state::cooling%]", + "defrost": "[%key:component::climate::entity_component::_::state_attributes::hvac_action::state::defrosting%]", + "heating": "[%key:component::climate::entity_component::_::state_attributes::hvac_action::state::heating%]", + "off": "[%key:common::state::off%]", + "passive_defrost": "Passive defrosting", + "pause": "[%key:common::state::idle%]", + "preparing": "Preparing", + "preparing_defrost": "Preparing defrost", + "ready": "[%key:common::state::idle%]" + } }, "compressor_starts": { "name": "Compressor starts" diff --git a/homeassistant/components/vicare/utils.py b/homeassistant/components/vicare/utils.py index ef018a60f1618..4d831cf625a12 100644 --- a/homeassistant/components/vicare/utils.py +++ b/homeassistant/components/vicare/utils.py @@ -133,3 +133,8 @@ def get_compressors(device: PyViCareDevice) -> list[PyViCareHeatingDeviceCompone def filter_state(state: str) -> str | None: """Return the state if not 'nothing' or 'unknown'.""" return None if state in ("nothing", "unknown") else state + + +def normalize_state(state: str) -> str: + """Return the state with underscores instead of hyphens.""" + return state.replace("-", "_") diff --git a/homeassistant/components/xbox/entity.py b/homeassistant/components/xbox/entity.py index 5a04aeec17853..84e296f58eaad 100644 --- a/homeassistant/components/xbox/entity.py +++ b/homeassistant/components/xbox/entity.py @@ -152,7 +152,7 @@ def check_deprecated_entity( return False -def profile_pic(person: Person, _: Title | None) -> str | None: +def profile_pic(person: Person, _: Title | None = None) -> str | None: """Return the gamer pic.""" # Xbox sometimes returns a domain that uses a wrong certificate which diff --git a/homeassistant/components/xbox/icons.json b/homeassistant/components/xbox/icons.json index 48abb1c207395..4383aa30df63f 100644 --- a/homeassistant/components/xbox/icons.json +++ b/homeassistant/components/xbox/icons.json @@ -29,6 +29,19 @@ "gamer_score": { "default": "mdi:alpha-g-circle" }, + "in_party": { + "default": "mdi:headset", + "state": { + "0": "mdi:headset-off" + } + }, + "join_restrictions": { + "default": "mdi:account-voice-off", + "state": { + "invite_only": "mdi:email-newsletter", + "joinable": "mdi:account-multiple-plus-outline" + } + }, "last_online": { "default": "mdi:account-clock" }, diff --git a/homeassistant/components/xbox/media_source.py b/homeassistant/components/xbox/media_source.py index a208e3b56c499..bcefa6829a91a 100644 --- a/homeassistant/components/xbox/media_source.py +++ b/homeassistant/components/xbox/media_source.py @@ -2,67 +2,85 @@ from __future__ import annotations -from contextlib import suppress -from dataclasses import dataclass +import logging +from typing import TYPE_CHECKING -from pydantic import ValidationError -from pythonxbox.api.client import XboxLiveClient -from pythonxbox.api.provider.catalog.models import FieldsTemplate, Image -from pythonxbox.api.provider.gameclips.models import GameclipsResponse -from pythonxbox.api.provider.screenshots.models import ScreenshotResponse -from pythonxbox.api.provider.smartglass.models import InstalledPackage +from httpx import HTTPStatusError, RequestError, TimeoutException +from pythonxbox.api.provider.titlehub.models import Image, Title, TitleFields -from homeassistant.components.media_player import MediaClass +from homeassistant.components.media_player import BrowseError, MediaClass from homeassistant.components.media_source import ( BrowseMediaSource, MediaSource, MediaSourceItem, PlayMedia, + Unresolvable, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util -from .browse_media import _find_media_image +from .binary_sensor import profile_pic from .const import DOMAIN from .coordinator import XboxConfigEntry +_LOGGER = logging.getLogger(__name__) + +ATTR_GAMECLIPS = "gameclips" +ATTR_SCREENSHOTS = "screenshots" +ATTR_GAME_MEDIA = "game_media" +ATTR_COMMUNITY_GAMECLIPS = "community_gameclips" +ATTR_COMMUNITY_SCREENSHOTS = "community_screenshots" + +MAP_TITLE = { + ATTR_GAMECLIPS: "Gameclips", + ATTR_SCREENSHOTS: "Screenshots", + ATTR_GAME_MEDIA: "Game media", + ATTR_COMMUNITY_GAMECLIPS: "Community gameclips", + ATTR_COMMUNITY_SCREENSHOTS: "Community screenshots", +} + MIME_TYPE_MAP = { - "gameclips": "video/mp4", - "screenshots": "image/png", + ATTR_GAMECLIPS: "video/mp4", + ATTR_COMMUNITY_GAMECLIPS: "video/mp4", + ATTR_SCREENSHOTS: "image/png", + ATTR_COMMUNITY_SCREENSHOTS: "image/png", } MEDIA_CLASS_MAP = { - "gameclips": MediaClass.VIDEO, - "screenshots": MediaClass.IMAGE, + ATTR_GAMECLIPS: MediaClass.VIDEO, + ATTR_COMMUNITY_GAMECLIPS: MediaClass.VIDEO, + ATTR_SCREENSHOTS: MediaClass.IMAGE, + ATTR_COMMUNITY_SCREENSHOTS: MediaClass.IMAGE, + ATTR_GAME_MEDIA: MediaClass.IMAGE, } +SEPARATOR = "/" + -async def async_get_media_source(hass: HomeAssistant): +async def async_get_media_source(hass: HomeAssistant) -> XboxSource: """Set up Xbox media source.""" - entry: XboxConfigEntry = hass.config_entries.async_entries(DOMAIN)[0] - client = entry.runtime_data.client - return XboxSource(hass, client) + return XboxSource(hass) -@callback -def async_parse_identifier( - item: MediaSourceItem, -) -> tuple[str, str, str]: - """Parse identifier.""" - identifier = item.identifier or "" - start = ["", "", ""] - items = identifier.lstrip("/").split("~~", 2) - return tuple(items + start[len(items) :]) # type: ignore[return-value] +class XboxMediaSourceIdentifier: + """Media item identifier.""" -@dataclass -class XboxMediaItem: - """Represents gameclip/screenshot media.""" + xuid = title_id = media_type = media_id = "" - caption: str - thumbnail: str - uri: str - media_class: str + def __init__(self, item: MediaSourceItem) -> None: + """Initialize identifier.""" + if item.identifier is not None: + self.xuid, _, self.title_id = (item.identifier).partition(SEPARATOR) + self.title_id, _, self.media_type = (self.title_id).partition(SEPARATOR) + self.media_type, _, self.media_id = (self.media_type).partition(SEPARATOR) + + def __str__(self) -> str: + """Build identifier.""" + + return SEPARATOR.join( + [i for i in (self.xuid, self.title_id, self.media_type, self.media_id) if i] + ) class XboxSource(MediaSource): @@ -70,202 +88,573 @@ class XboxSource(MediaSource): name: str = "Xbox Game Media" - def __init__(self, hass: HomeAssistant, client: XboxLiveClient) -> None: + def __init__(self, hass: HomeAssistant) -> None: """Initialize Xbox source.""" super().__init__(DOMAIN) - - self.hass: HomeAssistant = hass - self.client: XboxLiveClient = client + self.hass = hass async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: """Resolve media to a url.""" - _, category, url = async_parse_identifier(item) - kind = category.split("#", 1)[1] - return PlayMedia(url, MIME_TYPE_MAP[kind]) + identifier = XboxMediaSourceIdentifier(item) + + if not (entries := self.hass.config_entries.async_loaded_entries(DOMAIN)): + raise Unresolvable( + translation_domain=DOMAIN, + translation_key="xbox_not_configured", + ) + try: + entry: XboxConfigEntry = next( + e for e in entries if e.unique_id == identifier.xuid + ) + except StopIteration as e: + raise Unresolvable( + translation_domain=DOMAIN, + translation_key="account_not_configured", + ) from e + + client = entry.runtime_data.client + + if identifier.media_type in (ATTR_GAMECLIPS, ATTR_COMMUNITY_GAMECLIPS): + try: + if identifier.media_type == ATTR_GAMECLIPS: + gameclips_response = ( + await client.gameclips.get_recent_clips_by_xuid( + identifier.xuid, identifier.title_id, max_items=999 + ) + ) + else: + gameclips_response = ( + await client.gameclips.get_recent_community_clips_by_title_id( + identifier.title_id + ) + ) + except TimeoutException as e: + raise Unresolvable( + translation_domain=DOMAIN, + translation_key="timeout_exception", + ) from e + except (RequestError, HTTPStatusError) as e: + _LOGGER.debug("Xbox exception:", exc_info=True) + raise Unresolvable( + translation_domain=DOMAIN, + translation_key="request_exception", + ) from e + gameclips = gameclips_response.game_clips + try: + clip = next( + g for g in gameclips if g.game_clip_id == identifier.media_id + ) + except StopIteration as e: + raise Unresolvable( + translation_domain=DOMAIN, + translation_key="media_not_found", + ) from e + return PlayMedia(clip.game_clip_uris[0].uri, MIME_TYPE_MAP[ATTR_GAMECLIPS]) + + if identifier.media_type in (ATTR_SCREENSHOTS, ATTR_COMMUNITY_SCREENSHOTS): + try: + if identifier.media_type == ATTR_SCREENSHOTS: + screenshot_response = ( + await client.screenshots.get_recent_screenshots_by_xuid( + identifier.xuid, identifier.title_id, max_items=999 + ) + ) + else: + screenshot_response = await client.screenshots.get_recent_community_screenshots_by_title_id( + identifier.title_id + ) + except TimeoutException as e: + raise Unresolvable( + translation_domain=DOMAIN, + translation_key="timeout_exception", + ) from e + except (RequestError, HTTPStatusError) as e: + _LOGGER.debug("Xbox exception:", exc_info=True) + raise Unresolvable( + translation_domain=DOMAIN, + translation_key="request_exception", + ) from e + screenshots = screenshot_response.screenshots + try: + img = next( + s for s in screenshots if s.screenshot_id == identifier.media_id + ) + except StopIteration as e: + raise Unresolvable( + translation_domain=DOMAIN, + translation_key="media_not_found", + ) from e + return PlayMedia( + img.screenshot_uris[0].uri, MIME_TYPE_MAP[identifier.media_type] + ) + if identifier.media_type == ATTR_GAME_MEDIA: + try: + images = ( + (await client.titlehub.get_title_info(identifier.title_id)) + .titles[0] + .images + ) + except TimeoutException as e: + raise Unresolvable( + translation_domain=DOMAIN, + translation_key="timeout_exception", + ) from e + except (RequestError, HTTPStatusError) as e: + _LOGGER.debug("Xbox exception:", exc_info=True) + raise Unresolvable( + translation_domain=DOMAIN, + translation_key="request_exception", + ) from e + if images is not None: + try: + return PlayMedia( + images[int(identifier.media_id)].url, + MIME_TYPE_MAP[ATTR_SCREENSHOTS], + ) + except (ValueError, IndexError): + pass + + raise Unresolvable( + translation_domain=DOMAIN, + translation_key="media_not_found", + ) async def async_browse_media(self, item: MediaSourceItem) -> BrowseMediaSource: """Return media.""" - title, category, _ = async_parse_identifier(item) + if not (entries := self.hass.config_entries.async_loaded_entries(DOMAIN)): + raise BrowseError( + translation_domain=DOMAIN, + translation_key="xbox_not_configured", + ) - if not title: - return await self._build_game_library() + # if there is only one entry we can directly jump to it + if not item.identifier and len(entries) > 1: + return BrowseMediaSource( + domain=DOMAIN, + identifier=None, + media_class=MediaClass.DIRECTORY, + media_content_type=MediaClass.IMAGE, + title="Xbox Game Media", + can_play=False, + can_expand=True, + children=[*await self._build_accounts(entries)], + children_media_class=MediaClass.DIRECTORY, + ) - if not category: - return _build_categories(title) + identifier = XboxMediaSourceIdentifier(item) + if not identifier.xuid and len(entries) == 1: + if TYPE_CHECKING: + assert entries[0].unique_id + identifier.xuid = entries[0].unique_id - return await self._build_media_items(title, category) + try: + entry: XboxConfigEntry = next( + e for e in entries if e.unique_id == identifier.xuid + ) + except StopIteration as e: + raise BrowseError( + translation_domain=DOMAIN, + translation_key="account_not_configured", + ) from e + + if not identifier.title_id: + return await self._build_game_library(entry) + + if not identifier.media_type: + return await self._build_game_title(entry, identifier) + + return await self._build_game_media(entry, identifier) + + async def _build_accounts( + self, entries: list[XboxConfigEntry] + ) -> list[BrowseMediaSource]: + """List Xbox accounts.""" + + return [ + BrowseMediaSource( + domain=DOMAIN, + identifier=entry.unique_id, + media_class=MediaClass.DIRECTORY, + media_content_type=MediaClass.DIRECTORY, + title=entry.title, + can_play=False, + can_expand=True, + thumbnail=gamerpic(entry), + ) + for entry in entries + ] - async def _build_game_library(self): - """Display installed games across all consoles.""" - apps = await self.client.smartglass.get_installed_apps() - games = { - game.one_store_product_id: game - for game in apps.result - if game.is_game and game.title_id - } + async def _build_game_library(self, entry: XboxConfigEntry) -> BrowseMediaSource: + """Display played games.""" - app_details = await self.client.catalog.get_products( - games.keys(), - FieldsTemplate.BROWSE, + return BrowseMediaSource( + domain=DOMAIN, + identifier=entry.unique_id, + media_class=MediaClass.DIRECTORY, + media_content_type=MediaClass.DIRECTORY, + title=f"Xbox / {entry.title}", + can_play=False, + can_expand=True, + children=[*await self._build_games(entry)], + children_media_class=MediaClass.GAME, ) - images = { - prod.product_id: prod.localized_properties[0].images - for prod in app_details.products - } + async def _build_games(self, entry: XboxConfigEntry) -> list[BrowseMediaSource]: + """List Xbox games for the selected account.""" + + client = entry.runtime_data.client + if TYPE_CHECKING: + assert entry.unique_id + fields = [ + TitleFields.ACHIEVEMENT, + TitleFields.STATS, + TitleFields.IMAGE, + ] + try: + games = await client.titlehub.get_title_history( + entry.unique_id, fields, max_items=999 + ) + except TimeoutException as e: + raise BrowseError( + translation_domain=DOMAIN, + translation_key="timeout_exception", + ) from e + except (RequestError, HTTPStatusError) as e: + _LOGGER.debug("Xbox exception:", exc_info=True) + raise BrowseError( + translation_domain=DOMAIN, + translation_key="request_exception", + ) from e + + return [ + BrowseMediaSource( + domain=DOMAIN, + identifier=f"{entry.unique_id}/{game.title_id}", + media_class=MediaClass.GAME, + media_content_type=MediaClass.GAME, + title=game.name, + can_play=False, + can_expand=True, + children_media_class=MediaClass.DIRECTORY, + thumbnail=game_thumbnail(game.images or []), + ) + for game in games.titles + if game.achievement and game.achievement.source_version != 0 + ] + + async def _build_game_title( + self, entry: XboxConfigEntry, identifier: XboxMediaSourceIdentifier + ) -> BrowseMediaSource: + """Display game title.""" + client = entry.runtime_data.client + try: + game = (await client.titlehub.get_title_info(identifier.title_id)).titles[0] + except TimeoutException as e: + raise BrowseError( + translation_domain=DOMAIN, + translation_key="timeout_exception", + ) from e + except (RequestError, HTTPStatusError) as e: + _LOGGER.debug("Xbox exception:", exc_info=True) + raise BrowseError( + translation_domain=DOMAIN, + translation_key="request_exception", + ) from e return BrowseMediaSource( domain=DOMAIN, - identifier="", - media_class=MediaClass.DIRECTORY, - media_content_type="", - title="Xbox Game Media", + identifier=str(identifier), + media_class=MediaClass.GAME, + media_content_type=MediaClass.GAME, + title=f"Xbox / {entry.title} / {game.name}", can_play=False, can_expand=True, - children=[_build_game_item(game, images) for game in games.values()], - children_media_class=MediaClass.GAME, + children=[*self._build_categories(identifier)], + children_media_class=MediaClass.DIRECTORY, ) - async def _build_media_items(self, title, category): - """Fetch requested gameclip/screenshot media.""" - title_id, _, thumbnail = title.split("#", 2) - owner, kind = category.split("#", 1) - - items: list[XboxMediaItem] = [] - with suppress(ValidationError): # Unexpected API response - if kind == "gameclips": - if owner == "my": - response: GameclipsResponse = ( - await self.client.gameclips.get_recent_clips_by_xuid( - self.client.xuid, title_id - ) - ) - elif owner == "community": - response: GameclipsResponse = await self.client.gameclips.get_recent_community_clips_by_title_id( - title_id - ) - else: - return None - items = [ - XboxMediaItem( - item.user_caption - or dt_util.as_local(item.date_recorded).strftime( - "%b. %d, %Y %I:%M %p" - ), - item.thumbnails[0].uri, - item.game_clip_uris[0].uri, - MediaClass.VIDEO, - ) - for item in response.game_clips - ] - elif kind == "screenshots": - if owner == "my": - response: ScreenshotResponse = ( - await self.client.screenshots.get_recent_screenshots_by_xuid( - self.client.xuid, title_id - ) - ) - elif owner == "community": - response: ScreenshotResponse = await self.client.screenshots.get_recent_community_screenshots_by_title_id( - title_id - ) - else: - return None - items = [ - XboxMediaItem( - item.user_caption - or dt_util.as_local(item.date_taken).strftime( - "%b. %d, %Y %I:%M%p" - ), - item.thumbnails[0].uri, - item.screenshot_uris[0].uri, - MediaClass.IMAGE, - ) - for item in response.screenshots - ] + def _build_categories( + self, identifier: XboxMediaSourceIdentifier + ) -> list[BrowseMediaSource]: + """List media categories.""" + + return [ + BrowseMediaSource( + domain=DOMAIN, + identifier=f"{identifier}/{media_type}", + media_class=MediaClass.DIRECTORY, + media_content_type=MediaClass.DIRECTORY, + title=MAP_TITLE[media_type], + can_play=False, + can_expand=True, + children_media_class=MEDIA_CLASS_MAP[media_type], + ) + for media_type in ( + ATTR_GAMECLIPS, + ATTR_SCREENSHOTS, + ATTR_COMMUNITY_GAMECLIPS, + ATTR_COMMUNITY_SCREENSHOTS, + ATTR_GAME_MEDIA, + ) + ] + + async def _build_game_media( + self, entry: XboxConfigEntry, identifier: XboxMediaSourceIdentifier + ) -> BrowseMediaSource: + """List game media.""" + client = entry.runtime_data.client + try: + game = (await client.titlehub.get_title_info(identifier.title_id)).titles[0] + except TimeoutException as e: + raise BrowseError( + translation_domain=DOMAIN, + translation_key="timeout_exception", + ) from e + except (RequestError, HTTPStatusError) as e: + _LOGGER.debug("Xbox exception:", exc_info=True) + raise BrowseError( + translation_domain=DOMAIN, + translation_key="request_exception", + ) from e return BrowseMediaSource( domain=DOMAIN, - identifier=f"{title}~~{category}", - media_class=MediaClass.DIRECTORY, - media_content_type="", - title=f"{owner.title()} {kind.title()}", + identifier=str(identifier), + media_class=MEDIA_CLASS_MAP[identifier.media_type], + media_content_type=MediaClass.DIRECTORY, + title=f"Xbox / {entry.title} / {game.name} / {MAP_TITLE[identifier.media_type]}", can_play=False, can_expand=True, - children=[_build_media_item(title, category, item) for item in items], - children_media_class=MEDIA_CLASS_MAP[kind], - thumbnail=thumbnail, + children=[ + *await self._build_media_items_gameclips(entry, identifier) + + await self._build_media_items_community_gameclips(entry, identifier) + + await self._build_media_items_screenshots(entry, identifier) + + await self._build_media_items_community_screenshots(entry, identifier) + + self._build_media_items_promotional(identifier, game) + ], + children_media_class=MEDIA_CLASS_MAP[identifier.media_type], ) + async def _build_media_items_gameclips( + self, entry: XboxConfigEntry, identifier: XboxMediaSourceIdentifier + ) -> list[BrowseMediaSource]: + """List media items.""" + client = entry.runtime_data.client + + if identifier.media_type != ATTR_GAMECLIPS: + return [] + try: + gameclips = ( + await client.gameclips.get_recent_clips_by_xuid( + identifier.xuid, identifier.title_id, max_items=999 + ) + ).game_clips + except TimeoutException as e: + raise BrowseError( + translation_domain=DOMAIN, + translation_key="timeout_exception", + ) from e + except (RequestError, HTTPStatusError) as e: + _LOGGER.debug("Xbox exception:", exc_info=True) + raise BrowseError( + translation_domain=DOMAIN, + translation_key="request_exception", + ) from e + + return [ + BrowseMediaSource( + domain=DOMAIN, + identifier=f"{identifier}/{gameclip.game_clip_id}", + media_class=MediaClass.VIDEO, + media_content_type=MediaClass.VIDEO, + title=( + f"{gameclip.user_caption}" + f"{' | ' if gameclip.user_caption else ''}" + f"{dt_util.get_age(gameclip.date_recorded)}" + ), + can_play=True, + can_expand=False, + thumbnail=gameclip.thumbnails[0].uri, + ) + for gameclip in gameclips + ] + + async def _build_media_items_community_gameclips( + self, entry: XboxConfigEntry, identifier: XboxMediaSourceIdentifier + ) -> list[BrowseMediaSource]: + """List media items.""" + client = entry.runtime_data.client + + if identifier.media_type != ATTR_COMMUNITY_GAMECLIPS: + return [] + try: + gameclips = ( + await client.gameclips.get_recent_community_clips_by_title_id( + identifier.title_id + ) + ).game_clips + except TimeoutException as e: + raise BrowseError( + translation_domain=DOMAIN, + translation_key="timeout_exception", + ) from e + except (RequestError, HTTPStatusError) as e: + _LOGGER.debug("Xbox exception:", exc_info=True) + raise BrowseError( + translation_domain=DOMAIN, + translation_key="request_exception", + ) from e + + return [ + BrowseMediaSource( + domain=DOMAIN, + identifier=f"{identifier}/{gameclip.game_clip_id}", + media_class=MediaClass.VIDEO, + media_content_type=MediaClass.VIDEO, + title=( + f"{gameclip.user_caption}" + f"{' | ' if gameclip.user_caption else ''}" + f"{dt_util.get_age(gameclip.date_recorded)}" + ), + can_play=True, + can_expand=False, + thumbnail=gameclip.thumbnails[0].uri, + ) + for gameclip in gameclips + ] + + async def _build_media_items_screenshots( + self, entry: XboxConfigEntry, identifier: XboxMediaSourceIdentifier + ) -> list[BrowseMediaSource]: + """List media items.""" + client = entry.runtime_data.client + + if identifier.media_type != ATTR_SCREENSHOTS: + return [] + try: + screenshots = ( + await client.screenshots.get_recent_screenshots_by_xuid( + identifier.xuid, identifier.title_id, max_items=999 + ) + ).screenshots + except TimeoutException as e: + raise BrowseError( + translation_domain=DOMAIN, + translation_key="timeout_exception", + ) from e + except (RequestError, HTTPStatusError) as e: + _LOGGER.debug("Xbox exception:", exc_info=True) + raise BrowseError( + translation_domain=DOMAIN, + translation_key="request_exception", + ) from e + + return [ + BrowseMediaSource( + domain=DOMAIN, + identifier=f"{identifier}/{screenshot.screenshot_id}", + media_class=MediaClass.VIDEO, + media_content_type=MediaClass.VIDEO, + title=( + f"{screenshot.user_caption}" + f"{' | ' if screenshot.user_caption else ''}" + f"{dt_util.get_age(screenshot.date_taken)} | {screenshot.resolution_height}p" + ), + can_play=True, + can_expand=False, + thumbnail=screenshot.thumbnails[0].uri, + ) + for screenshot in screenshots + ] + + async def _build_media_items_community_screenshots( + self, entry: XboxConfigEntry, identifier: XboxMediaSourceIdentifier + ) -> list[BrowseMediaSource]: + """List media items.""" + client = entry.runtime_data.client + + if identifier.media_type != ATTR_COMMUNITY_SCREENSHOTS: + return [] + try: + screenshots = ( + await client.screenshots.get_recent_community_screenshots_by_title_id( + identifier.title_id + ) + ).screenshots + except TimeoutException as e: + raise BrowseError( + translation_domain=DOMAIN, + translation_key="timeout_exception", + ) from e + except (RequestError, HTTPStatusError) as e: + _LOGGER.debug("Xbox exception:", exc_info=True) + raise BrowseError( + translation_domain=DOMAIN, + translation_key="request_exception", + ) from e + + return [ + BrowseMediaSource( + domain=DOMAIN, + identifier=f"{identifier}/{screenshot.screenshot_id}", + media_class=MediaClass.VIDEO, + media_content_type=MediaClass.VIDEO, + title=( + f"{screenshot.user_caption}" + f"{' | ' if screenshot.user_caption else ''}" + f"{dt_util.get_age(screenshot.date_taken)} | {screenshot.resolution_height}p" + ), + can_play=True, + can_expand=False, + thumbnail=screenshot.thumbnails[0].uri, + ) + for screenshot in screenshots + ] + + def _build_media_items_promotional( + self, identifier: XboxMediaSourceIdentifier, game: Title + ) -> list[BrowseMediaSource]: + """List promotional game media.""" + + if identifier.media_type != ATTR_GAME_MEDIA: + return [] -def _build_game_item(item: InstalledPackage, images: dict[str, list[Image]]): - """Build individual game.""" - thumbnail = "" - image = _find_media_image(images.get(item.one_store_product_id, [])) # type: ignore[arg-type] - if image is not None: - thumbnail = image.uri - if thumbnail[0] == "/": - thumbnail = f"https:{thumbnail}" - - return BrowseMediaSource( - domain=DOMAIN, - identifier=f"{item.title_id}#{item.name}#{thumbnail}", - media_class=MediaClass.GAME, - media_content_type="", - title=item.name, - can_play=False, - can_expand=True, - children_media_class=MediaClass.DIRECTORY, - thumbnail=thumbnail, - ) - - -def _build_categories(title): - """Build base categories for Xbox media.""" - _, name, thumbnail = title.split("#", 2) - base = BrowseMediaSource( - domain=DOMAIN, - identifier=f"{title}", - media_class=MediaClass.GAME, - media_content_type="", - title=name, - can_play=False, - can_expand=True, - children=[], - children_media_class=MediaClass.DIRECTORY, - thumbnail=thumbnail, - ) - - owners = ["my", "community"] - kinds = ["gameclips", "screenshots"] - for owner in owners: - for kind in kinds: - base.children.append( + return ( + [ BrowseMediaSource( domain=DOMAIN, - identifier=f"{title}~~{owner}#{kind}", - media_class=MediaClass.DIRECTORY, - media_content_type="", - title=f"{owner.title()} {kind.title()}", - can_play=False, - can_expand=True, - children_media_class=MEDIA_CLASS_MAP[kind], + identifier=f"{identifier}/{game.images.index(image)}", + media_class=MediaClass.VIDEO, + media_content_type=MediaClass.VIDEO, + title=image.type, + can_play=True, + can_expand=False, + thumbnail=image.url, ) - ) + for image in game.images + ] + if game.images + else [] + ) + + +def gamerpic(config_entry: XboxConfigEntry) -> str | None: + """Return gamerpic.""" + coordinator = config_entry.runtime_data + if TYPE_CHECKING: + assert config_entry.unique_id + person = coordinator.data.presence[coordinator.client.xuid] + return profile_pic(person) + + +def game_thumbnail(images: list[Image]) -> str | None: + """Return the title image.""" + + for img_type in ("BrandedKeyArt", "Poster", "BoxArt"): + if match := next( + (i for i in images if i.type == img_type), + None, + ): + return match.url - return base - - -def _build_media_item(title: str, category: str, item: XboxMediaItem): - """Build individual media item.""" - kind = category.split("#", 1)[1] - return BrowseMediaSource( - domain=DOMAIN, - identifier=f"{title}~~{category}~~{item.uri}", - media_class=item.media_class, - media_content_type=MIME_TYPE_MAP[kind], - title=item.caption, - can_play=True, - can_expand=False, - thumbnail=item.thumbnail, - ) + return None diff --git a/homeassistant/components/xbox/sensor.py b/homeassistant/components/xbox/sensor.py index 8485c83cf0bf8..69bd9d4f5046c 100644 --- a/homeassistant/components/xbox/sensor.py +++ b/homeassistant/components/xbox/sensor.py @@ -24,6 +24,11 @@ from .coordinator import XboxConfigEntry from .entity import XboxBaseEntity, XboxBaseEntityDescription, check_deprecated_entity +MAP_JOIN_RESTRICTIONS = { + "local": "invite_only", + "followed": "joinable", +} + class XboxSensor(StrEnum): """Xbox sensor.""" @@ -37,6 +42,8 @@ class XboxSensor(StrEnum): FOLLOWER = "follower" NOW_PLAYING = "now_playing" FRIENDS = "friends" + IN_PARTY = "in_party" + JOIN_RESTRICTIONS = "join_restrictions" @dataclass(kw_only=True, frozen=True) @@ -95,6 +102,18 @@ def now_playing_attributes(_: Person, title: Title | None) -> dict[str, Any]: return attributes +def join_restrictions(person: Person, _: Title | None = None) -> str | None: + """Join restrictions for current party the user is in.""" + + return ( + MAP_JOIN_RESTRICTIONS.get( + person.multiplayer_summary.party_details[0].join_restriction + ) + if person.multiplayer_summary and person.multiplayer_summary.party_details + else None + ) + + def title_logo(_: Person, title: Title | None) -> str | None: """Get the game logo.""" @@ -159,6 +178,22 @@ def title_logo(_: Person, title: Title | None) -> str | None: translation_key=XboxSensor.FRIENDS, value_fn=lambda x, _: x.detail.friend_count if x.detail else None, ), + XboxSensorEntityDescription( + key=XboxSensor.IN_PARTY, + translation_key=XboxSensor.IN_PARTY, + value_fn=( + lambda x, _: x.multiplayer_summary.in_party + if x.multiplayer_summary + else None + ), + ), + XboxSensorEntityDescription( + key=XboxSensor.JOIN_RESTRICTIONS, + translation_key=XboxSensor.JOIN_RESTRICTIONS, + value_fn=join_restrictions, + device_class=SensorDeviceClass.ENUM, + options=list(MAP_JOIN_RESTRICTIONS.values()), + ), ) diff --git a/homeassistant/components/xbox/strings.json b/homeassistant/components/xbox/strings.json index 70a8751fc6beb..61e958f0d1dab 100644 --- a/homeassistant/components/xbox/strings.json +++ b/homeassistant/components/xbox/strings.json @@ -73,6 +73,17 @@ "name": "Gamerscore", "unit_of_measurement": "points" }, + "in_party": { + "name": "In party", + "unit_of_measurement": "[%key:component::xbox::entity::sensor::following::unit_of_measurement%]" + }, + "join_restrictions": { + "name": "Party join restrictions", + "state": { + "invite_only": "Invite-only", + "joinable": "Joinable" + } + }, "last_online": { "name": "Last online" }, @@ -98,6 +109,12 @@ } }, "exceptions": { + "account_not_configured": { + "message": "The Xbox account is not configured." + }, + "media_not_found": { + "message": "The requested media could not be found." + }, "oauth2_implementation_unavailable": { "message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]" }, @@ -106,6 +123,9 @@ }, "timeout_exception": { "message": "Failed to connect to Xbox Network due to a connection timeout" + }, + "xbox_not_configured": { + "message": "The Xbox integration is not configured." } } } diff --git a/homeassistant/components/zwave_js/binary_sensor.py b/homeassistant/components/zwave_js/binary_sensor.py index fcb62ba9a8029..b6e27b1080005 100644 --- a/homeassistant/components/zwave_js/binary_sensor.py +++ b/homeassistant/components/zwave_js/binary_sensor.py @@ -393,9 +393,7 @@ def async_add_binary_sensor( and is_valid_notification_binary_sensor(info) ): entities.extend( - ZWaveNotificationBinarySensor( - config_entry, driver, info, state_key, info.entity_description - ) + ZWaveNotificationBinarySensor(config_entry, driver, info, state_key) for state_key in info.primary_value.metadata.states if int(state_key) not in info.entity_description.not_states and ( diff --git a/requirements_all.txt b/requirements_all.txt index 97552b93540a3..bcbf36669f676 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2050,7 +2050,7 @@ pyhaversion==22.8.0 pyheos==1.0.6 # homeassistant.components.hive -pyhive-integration==1.0.6 +pyhive-integration==1.0.7 # homeassistant.components.homematic pyhomematic==0.1.77 diff --git a/requirements_test.txt b/requirements_test.txt index 3159ddfa73f82..ba5502b4ff8b8 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -32,7 +32,7 @@ pytest-timeout==2.4.0 pytest-unordered==0.7.0 pytest-picked==0.5.1 pytest-xdist==3.8.0 -pytest==8.4.2 +pytest==9.0.0 requests-mock==1.12.1 respx==0.22.0 syrupy==5.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 95fd612c2788d..49608e835a1ca 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1709,7 +1709,7 @@ pyhaversion==22.8.0 pyheos==1.0.6 # homeassistant.components.hive -pyhive-integration==1.0.6 +pyhive-integration==1.0.7 # homeassistant.components.homematic pyhomematic==0.1.77 diff --git a/script/scaffold/templates/config_flow_oauth2/integration/__init__.py b/script/scaffold/templates/config_flow_oauth2/integration/__init__.py index 38f628cd18f3d..20d018fafe593 100644 --- a/script/scaffold/templates/config_flow_oauth2/integration/__init__.py +++ b/script/scaffold/templates/config_flow_oauth2/integration/__init__.py @@ -6,9 +6,11 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow +from homeassistant.helpers import aiohttp_client from homeassistant.helpers.config_entry_oauth2_flow import ( ImplementationUnavailableError, + OAuth2Session, + async_get_config_entry_implementation, ) from . import api @@ -26,17 +28,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: New_NameConfigEntry) -> bool: """Set up NEW_NAME from a config entry.""" try: - implementation = ( - await config_entry_oauth2_flow.async_get_config_entry_implementation( - hass, entry - ) - ) + implementation = await async_get_config_entry_implementation(hass, entry) except ImplementationUnavailableError as err: raise ConfigEntryNotReady( "OAuth2 implementation temporarily unavailable, will retry" ) from err - session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation) + session = OAuth2Session(hass, entry, implementation) # If using a requests-based API lib entry.runtime_data = api.ConfigEntryAuth(hass, session) diff --git a/tests/components/bosch_alarm/snapshots/test_alarm_control_panel.ambr b/tests/components/bosch_alarm/snapshots/test_alarm_control_panel.ambr index ea50a006de01c..54e012d72c30a 100644 --- a/tests/components/bosch_alarm/snapshots/test_alarm_control_panel.ambr +++ b/tests/components/bosch_alarm/snapshots/test_alarm_control_panel.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_alarm_control_panel[None-amax_3000][alarm_control_panel.area1-entry] +# name: test_alarm_control_panel[amax_3000-None][alarm_control_panel.area1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -34,7 +34,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_alarm_control_panel[None-amax_3000][alarm_control_panel.area1-state] +# name: test_alarm_control_panel[amax_3000-None][alarm_control_panel.area1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'changed_by': None, @@ -51,7 +51,7 @@ 'state': 'disarmed', }) # --- -# name: test_alarm_control_panel[None-b5512][alarm_control_panel.area1-entry] +# name: test_alarm_control_panel[b5512-None][alarm_control_panel.area1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -86,7 +86,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_alarm_control_panel[None-b5512][alarm_control_panel.area1-state] +# name: test_alarm_control_panel[b5512-None][alarm_control_panel.area1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'changed_by': None, @@ -103,7 +103,7 @@ 'state': 'disarmed', }) # --- -# name: test_alarm_control_panel[None-solution_3000][alarm_control_panel.area1-entry] +# name: test_alarm_control_panel[solution_3000-None][alarm_control_panel.area1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -138,7 +138,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_alarm_control_panel[None-solution_3000][alarm_control_panel.area1-state] +# name: test_alarm_control_panel[solution_3000-None][alarm_control_panel.area1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'changed_by': None, diff --git a/tests/components/bosch_alarm/snapshots/test_binary_sensor.ambr b/tests/components/bosch_alarm/snapshots/test_binary_sensor.ambr index 7e1604127e284..2646a6affa7fe 100644 --- a/tests/components/bosch_alarm/snapshots/test_binary_sensor.ambr +++ b/tests/components/bosch_alarm/snapshots/test_binary_sensor.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_binary_sensor[None-amax_3000][binary_sensor.area1_area_ready_to_arm_away-entry] +# name: test_binary_sensor[amax_3000-None][binary_sensor.area1_area_ready_to_arm_away-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -34,7 +34,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-amax_3000][binary_sensor.area1_area_ready_to_arm_away-state] +# name: test_binary_sensor[amax_3000-None][binary_sensor.area1_area_ready_to_arm_away-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Area1 Area ready to arm away', @@ -47,7 +47,7 @@ 'state': 'on', }) # --- -# name: test_binary_sensor[None-amax_3000][binary_sensor.area1_area_ready_to_arm_home-entry] +# name: test_binary_sensor[amax_3000-None][binary_sensor.area1_area_ready_to_arm_home-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -82,7 +82,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-amax_3000][binary_sensor.area1_area_ready_to_arm_home-state] +# name: test_binary_sensor[amax_3000-None][binary_sensor.area1_area_ready_to_arm_home-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Area1 Area ready to arm home', @@ -95,7 +95,7 @@ 'state': 'on', }) # --- -# name: test_binary_sensor[None-amax_3000][binary_sensor.bedroom-entry] +# name: test_binary_sensor[amax_3000-None][binary_sensor.bedroom-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -130,7 +130,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-amax_3000][binary_sensor.bedroom-state] +# name: test_binary_sensor[amax_3000-None][binary_sensor.bedroom-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Bedroom', @@ -143,7 +143,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_ac_failure-entry] +# name: test_binary_sensor[amax_3000-None][binary_sensor.bosch_amax_3000_ac_failure-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -178,7 +178,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_ac_failure-state] +# name: test_binary_sensor[amax_3000-None][binary_sensor.bosch_amax_3000_ac_failure-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', @@ -192,7 +192,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_battery-entry] +# name: test_binary_sensor[amax_3000-None][binary_sensor.bosch_amax_3000_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -227,7 +227,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_battery-state] +# name: test_binary_sensor[amax_3000-None][binary_sensor.bosch_amax_3000_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', @@ -241,7 +241,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_battery_missing-entry] +# name: test_binary_sensor[amax_3000-None][binary_sensor.bosch_amax_3000_battery_missing-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -276,7 +276,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_battery_missing-state] +# name: test_binary_sensor[amax_3000-None][binary_sensor.bosch_amax_3000_battery_missing-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', @@ -290,7 +290,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_crc_failure_in_panel_configuration-entry] +# name: test_binary_sensor[amax_3000-None][binary_sensor.bosch_amax_3000_crc_failure_in_panel_configuration-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -325,7 +325,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_crc_failure_in_panel_configuration-state] +# name: test_binary_sensor[amax_3000-None][binary_sensor.bosch_amax_3000_crc_failure_in_panel_configuration-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', @@ -339,7 +339,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_failure_to_call_rps_since_last_rps_connection-entry] +# name: test_binary_sensor[amax_3000-None][binary_sensor.bosch_amax_3000_failure_to_call_rps_since_last_rps_connection-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -374,7 +374,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_failure_to_call_rps_since_last_rps_connection-state] +# name: test_binary_sensor[amax_3000-None][binary_sensor.bosch_amax_3000_failure_to_call_rps_since_last_rps_connection-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Bosch AMAX 3000 Failure to call RPS since last RPS connection', @@ -387,7 +387,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_log_overflow-entry] +# name: test_binary_sensor[amax_3000-None][binary_sensor.bosch_amax_3000_log_overflow-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -422,7 +422,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_log_overflow-state] +# name: test_binary_sensor[amax_3000-None][binary_sensor.bosch_amax_3000_log_overflow-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', @@ -436,7 +436,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_log_threshold_reached-entry] +# name: test_binary_sensor[amax_3000-None][binary_sensor.bosch_amax_3000_log_threshold_reached-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -471,7 +471,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_log_threshold_reached-state] +# name: test_binary_sensor[amax_3000-None][binary_sensor.bosch_amax_3000_log_threshold_reached-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', @@ -485,7 +485,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_phone_line_failure-entry] +# name: test_binary_sensor[amax_3000-None][binary_sensor.bosch_amax_3000_phone_line_failure-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -520,7 +520,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_phone_line_failure-state] +# name: test_binary_sensor[amax_3000-None][binary_sensor.bosch_amax_3000_phone_line_failure-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'connectivity', @@ -534,7 +534,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_point_bus_failure_since_last_rps_connection-entry] +# name: test_binary_sensor[amax_3000-None][binary_sensor.bosch_amax_3000_point_bus_failure_since_last_rps_connection-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -569,7 +569,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_point_bus_failure_since_last_rps_connection-state] +# name: test_binary_sensor[amax_3000-None][binary_sensor.bosch_amax_3000_point_bus_failure_since_last_rps_connection-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', @@ -583,7 +583,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_problem-entry] +# name: test_binary_sensor[amax_3000-None][binary_sensor.bosch_amax_3000_problem-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -618,7 +618,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_problem-state] +# name: test_binary_sensor[amax_3000-None][binary_sensor.bosch_amax_3000_problem-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', @@ -632,7 +632,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_sdi_failure_since_last_rps_connection-entry] +# name: test_binary_sensor[amax_3000-None][binary_sensor.bosch_amax_3000_sdi_failure_since_last_rps_connection-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -667,7 +667,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_sdi_failure_since_last_rps_connection-state] +# name: test_binary_sensor[amax_3000-None][binary_sensor.bosch_amax_3000_sdi_failure_since_last_rps_connection-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', @@ -681,7 +681,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_user_code_tamper_since_last_rps_connection-entry] +# name: test_binary_sensor[amax_3000-None][binary_sensor.bosch_amax_3000_user_code_tamper_since_last_rps_connection-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -716,7 +716,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_user_code_tamper_since_last_rps_connection-state] +# name: test_binary_sensor[amax_3000-None][binary_sensor.bosch_amax_3000_user_code_tamper_since_last_rps_connection-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', @@ -730,7 +730,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-amax_3000][binary_sensor.co_detector-entry] +# name: test_binary_sensor[amax_3000-None][binary_sensor.co_detector-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -765,7 +765,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-amax_3000][binary_sensor.co_detector-state] +# name: test_binary_sensor[amax_3000-None][binary_sensor.co_detector-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'CO Detector', @@ -778,7 +778,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-amax_3000][binary_sensor.door-entry] +# name: test_binary_sensor[amax_3000-None][binary_sensor.door-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -813,7 +813,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-amax_3000][binary_sensor.door-state] +# name: test_binary_sensor[amax_3000-None][binary_sensor.door-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Door', @@ -826,7 +826,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-amax_3000][binary_sensor.glassbreak_sensor-entry] +# name: test_binary_sensor[amax_3000-None][binary_sensor.glassbreak_sensor-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -861,7 +861,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-amax_3000][binary_sensor.glassbreak_sensor-state] +# name: test_binary_sensor[amax_3000-None][binary_sensor.glassbreak_sensor-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Glassbreak Sensor', @@ -874,7 +874,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-amax_3000][binary_sensor.motion_detector-entry] +# name: test_binary_sensor[amax_3000-None][binary_sensor.motion_detector-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -909,7 +909,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-amax_3000][binary_sensor.motion_detector-state] +# name: test_binary_sensor[amax_3000-None][binary_sensor.motion_detector-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Motion Detector', @@ -922,7 +922,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-amax_3000][binary_sensor.smoke_detector-entry] +# name: test_binary_sensor[amax_3000-None][binary_sensor.smoke_detector-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -957,7 +957,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-amax_3000][binary_sensor.smoke_detector-state] +# name: test_binary_sensor[amax_3000-None][binary_sensor.smoke_detector-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Smoke Detector', @@ -970,7 +970,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-amax_3000][binary_sensor.window-entry] +# name: test_binary_sensor[amax_3000-None][binary_sensor.window-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1005,7 +1005,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-amax_3000][binary_sensor.window-state] +# name: test_binary_sensor[amax_3000-None][binary_sensor.window-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Window', @@ -1018,7 +1018,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-b5512][binary_sensor.area1_area_ready_to_arm_away-entry] +# name: test_binary_sensor[b5512-None][binary_sensor.area1_area_ready_to_arm_away-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1053,7 +1053,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-b5512][binary_sensor.area1_area_ready_to_arm_away-state] +# name: test_binary_sensor[b5512-None][binary_sensor.area1_area_ready_to_arm_away-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Area1 Area ready to arm away', @@ -1066,7 +1066,7 @@ 'state': 'on', }) # --- -# name: test_binary_sensor[None-b5512][binary_sensor.area1_area_ready_to_arm_home-entry] +# name: test_binary_sensor[b5512-None][binary_sensor.area1_area_ready_to_arm_home-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1101,7 +1101,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-b5512][binary_sensor.area1_area_ready_to_arm_home-state] +# name: test_binary_sensor[b5512-None][binary_sensor.area1_area_ready_to_arm_home-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Area1 Area ready to arm home', @@ -1114,7 +1114,7 @@ 'state': 'on', }) # --- -# name: test_binary_sensor[None-b5512][binary_sensor.bedroom-entry] +# name: test_binary_sensor[b5512-None][binary_sensor.bedroom-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1149,7 +1149,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-b5512][binary_sensor.bedroom-state] +# name: test_binary_sensor[b5512-None][binary_sensor.bedroom-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Bedroom', @@ -1162,7 +1162,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_ac_failure-entry] +# name: test_binary_sensor[b5512-None][binary_sensor.bosch_b5512_us1b_ac_failure-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1197,7 +1197,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_ac_failure-state] +# name: test_binary_sensor[b5512-None][binary_sensor.bosch_b5512_us1b_ac_failure-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', @@ -1211,7 +1211,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_battery-entry] +# name: test_binary_sensor[b5512-None][binary_sensor.bosch_b5512_us1b_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1246,7 +1246,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_battery-state] +# name: test_binary_sensor[b5512-None][binary_sensor.bosch_b5512_us1b_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', @@ -1260,7 +1260,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_battery_missing-entry] +# name: test_binary_sensor[b5512-None][binary_sensor.bosch_b5512_us1b_battery_missing-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1295,7 +1295,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_battery_missing-state] +# name: test_binary_sensor[b5512-None][binary_sensor.bosch_b5512_us1b_battery_missing-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', @@ -1309,7 +1309,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_crc_failure_in_panel_configuration-entry] +# name: test_binary_sensor[b5512-None][binary_sensor.bosch_b5512_us1b_crc_failure_in_panel_configuration-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1344,7 +1344,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_crc_failure_in_panel_configuration-state] +# name: test_binary_sensor[b5512-None][binary_sensor.bosch_b5512_us1b_crc_failure_in_panel_configuration-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', @@ -1358,7 +1358,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_failure_to_call_rps_since_last_rps_connection-entry] +# name: test_binary_sensor[b5512-None][binary_sensor.bosch_b5512_us1b_failure_to_call_rps_since_last_rps_connection-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1393,7 +1393,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_failure_to_call_rps_since_last_rps_connection-state] +# name: test_binary_sensor[b5512-None][binary_sensor.bosch_b5512_us1b_failure_to_call_rps_since_last_rps_connection-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Bosch B5512 (US1B) Failure to call RPS since last RPS connection', @@ -1406,7 +1406,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_log_overflow-entry] +# name: test_binary_sensor[b5512-None][binary_sensor.bosch_b5512_us1b_log_overflow-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1441,7 +1441,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_log_overflow-state] +# name: test_binary_sensor[b5512-None][binary_sensor.bosch_b5512_us1b_log_overflow-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', @@ -1455,7 +1455,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_log_threshold_reached-entry] +# name: test_binary_sensor[b5512-None][binary_sensor.bosch_b5512_us1b_log_threshold_reached-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1490,7 +1490,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_log_threshold_reached-state] +# name: test_binary_sensor[b5512-None][binary_sensor.bosch_b5512_us1b_log_threshold_reached-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', @@ -1504,7 +1504,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_phone_line_failure-entry] +# name: test_binary_sensor[b5512-None][binary_sensor.bosch_b5512_us1b_phone_line_failure-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1539,7 +1539,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_phone_line_failure-state] +# name: test_binary_sensor[b5512-None][binary_sensor.bosch_b5512_us1b_phone_line_failure-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'connectivity', @@ -1553,7 +1553,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_point_bus_failure_since_last_rps_connection-entry] +# name: test_binary_sensor[b5512-None][binary_sensor.bosch_b5512_us1b_point_bus_failure_since_last_rps_connection-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1588,7 +1588,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_point_bus_failure_since_last_rps_connection-state] +# name: test_binary_sensor[b5512-None][binary_sensor.bosch_b5512_us1b_point_bus_failure_since_last_rps_connection-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', @@ -1602,7 +1602,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_problem-entry] +# name: test_binary_sensor[b5512-None][binary_sensor.bosch_b5512_us1b_problem-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1637,7 +1637,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_problem-state] +# name: test_binary_sensor[b5512-None][binary_sensor.bosch_b5512_us1b_problem-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', @@ -1651,7 +1651,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_sdi_failure_since_last_rps_connection-entry] +# name: test_binary_sensor[b5512-None][binary_sensor.bosch_b5512_us1b_sdi_failure_since_last_rps_connection-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1686,7 +1686,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_sdi_failure_since_last_rps_connection-state] +# name: test_binary_sensor[b5512-None][binary_sensor.bosch_b5512_us1b_sdi_failure_since_last_rps_connection-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', @@ -1700,7 +1700,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_user_code_tamper_since_last_rps_connection-entry] +# name: test_binary_sensor[b5512-None][binary_sensor.bosch_b5512_us1b_user_code_tamper_since_last_rps_connection-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1735,7 +1735,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_user_code_tamper_since_last_rps_connection-state] +# name: test_binary_sensor[b5512-None][binary_sensor.bosch_b5512_us1b_user_code_tamper_since_last_rps_connection-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', @@ -1749,7 +1749,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-b5512][binary_sensor.co_detector-entry] +# name: test_binary_sensor[b5512-None][binary_sensor.co_detector-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1784,7 +1784,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-b5512][binary_sensor.co_detector-state] +# name: test_binary_sensor[b5512-None][binary_sensor.co_detector-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'CO Detector', @@ -1797,7 +1797,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-b5512][binary_sensor.door-entry] +# name: test_binary_sensor[b5512-None][binary_sensor.door-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1832,7 +1832,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-b5512][binary_sensor.door-state] +# name: test_binary_sensor[b5512-None][binary_sensor.door-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Door', @@ -1845,7 +1845,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-b5512][binary_sensor.glassbreak_sensor-entry] +# name: test_binary_sensor[b5512-None][binary_sensor.glassbreak_sensor-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1880,7 +1880,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-b5512][binary_sensor.glassbreak_sensor-state] +# name: test_binary_sensor[b5512-None][binary_sensor.glassbreak_sensor-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Glassbreak Sensor', @@ -1893,7 +1893,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-b5512][binary_sensor.motion_detector-entry] +# name: test_binary_sensor[b5512-None][binary_sensor.motion_detector-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1928,7 +1928,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-b5512][binary_sensor.motion_detector-state] +# name: test_binary_sensor[b5512-None][binary_sensor.motion_detector-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Motion Detector', @@ -1941,7 +1941,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-b5512][binary_sensor.smoke_detector-entry] +# name: test_binary_sensor[b5512-None][binary_sensor.smoke_detector-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1976,7 +1976,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-b5512][binary_sensor.smoke_detector-state] +# name: test_binary_sensor[b5512-None][binary_sensor.smoke_detector-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Smoke Detector', @@ -1989,7 +1989,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-b5512][binary_sensor.window-entry] +# name: test_binary_sensor[b5512-None][binary_sensor.window-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2024,7 +2024,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-b5512][binary_sensor.window-state] +# name: test_binary_sensor[b5512-None][binary_sensor.window-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Window', @@ -2037,7 +2037,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-solution_3000][binary_sensor.area1_area_ready_to_arm_away-entry] +# name: test_binary_sensor[solution_3000-None][binary_sensor.area1_area_ready_to_arm_away-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2072,7 +2072,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-solution_3000][binary_sensor.area1_area_ready_to_arm_away-state] +# name: test_binary_sensor[solution_3000-None][binary_sensor.area1_area_ready_to_arm_away-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Area1 Area ready to arm away', @@ -2085,7 +2085,7 @@ 'state': 'on', }) # --- -# name: test_binary_sensor[None-solution_3000][binary_sensor.area1_area_ready_to_arm_home-entry] +# name: test_binary_sensor[solution_3000-None][binary_sensor.area1_area_ready_to_arm_home-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2120,7 +2120,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-solution_3000][binary_sensor.area1_area_ready_to_arm_home-state] +# name: test_binary_sensor[solution_3000-None][binary_sensor.area1_area_ready_to_arm_home-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Area1 Area ready to arm home', @@ -2133,7 +2133,7 @@ 'state': 'on', }) # --- -# name: test_binary_sensor[None-solution_3000][binary_sensor.bedroom-entry] +# name: test_binary_sensor[solution_3000-None][binary_sensor.bedroom-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2168,7 +2168,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-solution_3000][binary_sensor.bedroom-state] +# name: test_binary_sensor[solution_3000-None][binary_sensor.bedroom-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Bedroom', @@ -2181,7 +2181,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_ac_failure-entry] +# name: test_binary_sensor[solution_3000-None][binary_sensor.bosch_solution_3000_ac_failure-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2216,7 +2216,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_ac_failure-state] +# name: test_binary_sensor[solution_3000-None][binary_sensor.bosch_solution_3000_ac_failure-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', @@ -2230,7 +2230,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_battery-entry] +# name: test_binary_sensor[solution_3000-None][binary_sensor.bosch_solution_3000_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2265,7 +2265,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_battery-state] +# name: test_binary_sensor[solution_3000-None][binary_sensor.bosch_solution_3000_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', @@ -2279,7 +2279,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_battery_missing-entry] +# name: test_binary_sensor[solution_3000-None][binary_sensor.bosch_solution_3000_battery_missing-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2314,7 +2314,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_battery_missing-state] +# name: test_binary_sensor[solution_3000-None][binary_sensor.bosch_solution_3000_battery_missing-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', @@ -2328,7 +2328,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_crc_failure_in_panel_configuration-entry] +# name: test_binary_sensor[solution_3000-None][binary_sensor.bosch_solution_3000_crc_failure_in_panel_configuration-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2363,7 +2363,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_crc_failure_in_panel_configuration-state] +# name: test_binary_sensor[solution_3000-None][binary_sensor.bosch_solution_3000_crc_failure_in_panel_configuration-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', @@ -2377,7 +2377,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_failure_to_call_rps_since_last_rps_connection-entry] +# name: test_binary_sensor[solution_3000-None][binary_sensor.bosch_solution_3000_failure_to_call_rps_since_last_rps_connection-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2412,7 +2412,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_failure_to_call_rps_since_last_rps_connection-state] +# name: test_binary_sensor[solution_3000-None][binary_sensor.bosch_solution_3000_failure_to_call_rps_since_last_rps_connection-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Bosch Solution 3000 Failure to call RPS since last RPS connection', @@ -2425,7 +2425,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_log_overflow-entry] +# name: test_binary_sensor[solution_3000-None][binary_sensor.bosch_solution_3000_log_overflow-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2460,7 +2460,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_log_overflow-state] +# name: test_binary_sensor[solution_3000-None][binary_sensor.bosch_solution_3000_log_overflow-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', @@ -2474,7 +2474,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_log_threshold_reached-entry] +# name: test_binary_sensor[solution_3000-None][binary_sensor.bosch_solution_3000_log_threshold_reached-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2509,7 +2509,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_log_threshold_reached-state] +# name: test_binary_sensor[solution_3000-None][binary_sensor.bosch_solution_3000_log_threshold_reached-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', @@ -2523,7 +2523,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_phone_line_failure-entry] +# name: test_binary_sensor[solution_3000-None][binary_sensor.bosch_solution_3000_phone_line_failure-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2558,7 +2558,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_phone_line_failure-state] +# name: test_binary_sensor[solution_3000-None][binary_sensor.bosch_solution_3000_phone_line_failure-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'connectivity', @@ -2572,7 +2572,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_point_bus_failure_since_last_rps_connection-entry] +# name: test_binary_sensor[solution_3000-None][binary_sensor.bosch_solution_3000_point_bus_failure_since_last_rps_connection-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2607,7 +2607,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_point_bus_failure_since_last_rps_connection-state] +# name: test_binary_sensor[solution_3000-None][binary_sensor.bosch_solution_3000_point_bus_failure_since_last_rps_connection-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', @@ -2621,7 +2621,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_problem-entry] +# name: test_binary_sensor[solution_3000-None][binary_sensor.bosch_solution_3000_problem-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2656,7 +2656,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_problem-state] +# name: test_binary_sensor[solution_3000-None][binary_sensor.bosch_solution_3000_problem-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', @@ -2670,7 +2670,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_sdi_failure_since_last_rps_connection-entry] +# name: test_binary_sensor[solution_3000-None][binary_sensor.bosch_solution_3000_sdi_failure_since_last_rps_connection-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2705,7 +2705,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_sdi_failure_since_last_rps_connection-state] +# name: test_binary_sensor[solution_3000-None][binary_sensor.bosch_solution_3000_sdi_failure_since_last_rps_connection-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', @@ -2719,7 +2719,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_user_code_tamper_since_last_rps_connection-entry] +# name: test_binary_sensor[solution_3000-None][binary_sensor.bosch_solution_3000_user_code_tamper_since_last_rps_connection-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2754,7 +2754,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_user_code_tamper_since_last_rps_connection-state] +# name: test_binary_sensor[solution_3000-None][binary_sensor.bosch_solution_3000_user_code_tamper_since_last_rps_connection-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', @@ -2768,7 +2768,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-solution_3000][binary_sensor.co_detector-entry] +# name: test_binary_sensor[solution_3000-None][binary_sensor.co_detector-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2803,7 +2803,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-solution_3000][binary_sensor.co_detector-state] +# name: test_binary_sensor[solution_3000-None][binary_sensor.co_detector-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'CO Detector', @@ -2816,7 +2816,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-solution_3000][binary_sensor.door-entry] +# name: test_binary_sensor[solution_3000-None][binary_sensor.door-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2851,7 +2851,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-solution_3000][binary_sensor.door-state] +# name: test_binary_sensor[solution_3000-None][binary_sensor.door-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Door', @@ -2864,7 +2864,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-solution_3000][binary_sensor.glassbreak_sensor-entry] +# name: test_binary_sensor[solution_3000-None][binary_sensor.glassbreak_sensor-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2899,7 +2899,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-solution_3000][binary_sensor.glassbreak_sensor-state] +# name: test_binary_sensor[solution_3000-None][binary_sensor.glassbreak_sensor-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Glassbreak Sensor', @@ -2912,7 +2912,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-solution_3000][binary_sensor.motion_detector-entry] +# name: test_binary_sensor[solution_3000-None][binary_sensor.motion_detector-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2947,7 +2947,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-solution_3000][binary_sensor.motion_detector-state] +# name: test_binary_sensor[solution_3000-None][binary_sensor.motion_detector-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Motion Detector', @@ -2960,7 +2960,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-solution_3000][binary_sensor.smoke_detector-entry] +# name: test_binary_sensor[solution_3000-None][binary_sensor.smoke_detector-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2995,7 +2995,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-solution_3000][binary_sensor.smoke_detector-state] +# name: test_binary_sensor[solution_3000-None][binary_sensor.smoke_detector-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Smoke Detector', @@ -3008,7 +3008,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-solution_3000][binary_sensor.window-entry] +# name: test_binary_sensor[solution_3000-None][binary_sensor.window-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -3043,7 +3043,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-solution_3000][binary_sensor.window-state] +# name: test_binary_sensor[solution_3000-None][binary_sensor.window-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Window', diff --git a/tests/components/bosch_alarm/snapshots/test_sensor.ambr b/tests/components/bosch_alarm/snapshots/test_sensor.ambr index dc229c15918a3..c83f4b99cb1ad 100644 --- a/tests/components/bosch_alarm/snapshots/test_sensor.ambr +++ b/tests/components/bosch_alarm/snapshots/test_sensor.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_sensor[None-amax_3000][sensor.area1_burglary_alarm_issues-entry] +# name: test_sensor[amax_3000-None][sensor.area1_burglary_alarm_issues-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -34,7 +34,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_sensor[None-amax_3000][sensor.area1_burglary_alarm_issues-state] +# name: test_sensor[amax_3000-None][sensor.area1_burglary_alarm_issues-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Area1 Burglary alarm issues', @@ -47,7 +47,7 @@ 'state': 'no_issues', }) # --- -# name: test_sensor[None-amax_3000][sensor.area1_faulting_points-entry] +# name: test_sensor[amax_3000-None][sensor.area1_faulting_points-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -82,7 +82,7 @@ 'unit_of_measurement': 'points', }) # --- -# name: test_sensor[None-amax_3000][sensor.area1_faulting_points-state] +# name: test_sensor[amax_3000-None][sensor.area1_faulting_points-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Area1 Faulting points', @@ -96,7 +96,7 @@ 'state': '0', }) # --- -# name: test_sensor[None-amax_3000][sensor.area1_fire_alarm_issues-entry] +# name: test_sensor[amax_3000-None][sensor.area1_fire_alarm_issues-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -131,7 +131,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_sensor[None-amax_3000][sensor.area1_fire_alarm_issues-state] +# name: test_sensor[amax_3000-None][sensor.area1_fire_alarm_issues-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Area1 Fire alarm issues', @@ -144,7 +144,7 @@ 'state': 'no_issues', }) # --- -# name: test_sensor[None-amax_3000][sensor.area1_gas_alarm_issues-entry] +# name: test_sensor[amax_3000-None][sensor.area1_gas_alarm_issues-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -179,7 +179,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_sensor[None-amax_3000][sensor.area1_gas_alarm_issues-state] +# name: test_sensor[amax_3000-None][sensor.area1_gas_alarm_issues-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Area1 Gas alarm issues', @@ -192,7 +192,7 @@ 'state': 'no_issues', }) # --- -# name: test_sensor[None-b5512][sensor.area1_burglary_alarm_issues-entry] +# name: test_sensor[b5512-None][sensor.area1_burglary_alarm_issues-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -227,7 +227,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_sensor[None-b5512][sensor.area1_burglary_alarm_issues-state] +# name: test_sensor[b5512-None][sensor.area1_burglary_alarm_issues-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Area1 Burglary alarm issues', @@ -240,7 +240,7 @@ 'state': 'no_issues', }) # --- -# name: test_sensor[None-b5512][sensor.area1_faulting_points-entry] +# name: test_sensor[b5512-None][sensor.area1_faulting_points-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -275,7 +275,7 @@ 'unit_of_measurement': 'points', }) # --- -# name: test_sensor[None-b5512][sensor.area1_faulting_points-state] +# name: test_sensor[b5512-None][sensor.area1_faulting_points-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Area1 Faulting points', @@ -289,7 +289,7 @@ 'state': '0', }) # --- -# name: test_sensor[None-b5512][sensor.area1_fire_alarm_issues-entry] +# name: test_sensor[b5512-None][sensor.area1_fire_alarm_issues-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -324,7 +324,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_sensor[None-b5512][sensor.area1_fire_alarm_issues-state] +# name: test_sensor[b5512-None][sensor.area1_fire_alarm_issues-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Area1 Fire alarm issues', @@ -337,7 +337,7 @@ 'state': 'no_issues', }) # --- -# name: test_sensor[None-b5512][sensor.area1_gas_alarm_issues-entry] +# name: test_sensor[b5512-None][sensor.area1_gas_alarm_issues-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -372,7 +372,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_sensor[None-b5512][sensor.area1_gas_alarm_issues-state] +# name: test_sensor[b5512-None][sensor.area1_gas_alarm_issues-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Area1 Gas alarm issues', @@ -385,7 +385,7 @@ 'state': 'no_issues', }) # --- -# name: test_sensor[None-solution_3000][sensor.area1_burglary_alarm_issues-entry] +# name: test_sensor[solution_3000-None][sensor.area1_burglary_alarm_issues-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -420,7 +420,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_sensor[None-solution_3000][sensor.area1_burglary_alarm_issues-state] +# name: test_sensor[solution_3000-None][sensor.area1_burglary_alarm_issues-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Area1 Burglary alarm issues', @@ -433,7 +433,7 @@ 'state': 'no_issues', }) # --- -# name: test_sensor[None-solution_3000][sensor.area1_faulting_points-entry] +# name: test_sensor[solution_3000-None][sensor.area1_faulting_points-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -468,7 +468,7 @@ 'unit_of_measurement': 'points', }) # --- -# name: test_sensor[None-solution_3000][sensor.area1_faulting_points-state] +# name: test_sensor[solution_3000-None][sensor.area1_faulting_points-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Area1 Faulting points', @@ -482,7 +482,7 @@ 'state': '0', }) # --- -# name: test_sensor[None-solution_3000][sensor.area1_fire_alarm_issues-entry] +# name: test_sensor[solution_3000-None][sensor.area1_fire_alarm_issues-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -517,7 +517,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_sensor[None-solution_3000][sensor.area1_fire_alarm_issues-state] +# name: test_sensor[solution_3000-None][sensor.area1_fire_alarm_issues-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Area1 Fire alarm issues', @@ -530,7 +530,7 @@ 'state': 'no_issues', }) # --- -# name: test_sensor[None-solution_3000][sensor.area1_gas_alarm_issues-entry] +# name: test_sensor[solution_3000-None][sensor.area1_gas_alarm_issues-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -565,7 +565,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_sensor[None-solution_3000][sensor.area1_gas_alarm_issues-state] +# name: test_sensor[solution_3000-None][sensor.area1_gas_alarm_issues-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Area1 Gas alarm issues', diff --git a/tests/components/bosch_alarm/snapshots/test_switch.ambr b/tests/components/bosch_alarm/snapshots/test_switch.ambr index f9e4d063e5068..760ce7e28922c 100644 --- a/tests/components/bosch_alarm/snapshots/test_switch.ambr +++ b/tests/components/bosch_alarm/snapshots/test_switch.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_switch[None-amax_3000][switch.main_door_locked-entry] +# name: test_switch[amax_3000-None][switch.main_door_locked-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -34,7 +34,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_switch[None-amax_3000][switch.main_door_locked-state] +# name: test_switch[amax_3000-None][switch.main_door_locked-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Main Door Locked', @@ -47,7 +47,7 @@ 'state': 'on', }) # --- -# name: test_switch[None-amax_3000][switch.main_door_momentarily_unlocked-entry] +# name: test_switch[amax_3000-None][switch.main_door_momentarily_unlocked-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -82,7 +82,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_switch[None-amax_3000][switch.main_door_momentarily_unlocked-state] +# name: test_switch[amax_3000-None][switch.main_door_momentarily_unlocked-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Main Door Momentarily unlocked', @@ -95,7 +95,7 @@ 'state': 'off', }) # --- -# name: test_switch[None-amax_3000][switch.main_door_secured-entry] +# name: test_switch[amax_3000-None][switch.main_door_secured-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -130,7 +130,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_switch[None-amax_3000][switch.main_door_secured-state] +# name: test_switch[amax_3000-None][switch.main_door_secured-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Main Door Secured', @@ -143,7 +143,7 @@ 'state': 'off', }) # --- -# name: test_switch[None-amax_3000][switch.output_a-entry] +# name: test_switch[amax_3000-None][switch.output_a-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -178,7 +178,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_switch[None-amax_3000][switch.output_a-state] +# name: test_switch[amax_3000-None][switch.output_a-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Output A', @@ -191,7 +191,7 @@ 'state': 'off', }) # --- -# name: test_switch[None-b5512][switch.main_door_locked-entry] +# name: test_switch[b5512-None][switch.main_door_locked-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -226,7 +226,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_switch[None-b5512][switch.main_door_locked-state] +# name: test_switch[b5512-None][switch.main_door_locked-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Main Door Locked', @@ -239,7 +239,7 @@ 'state': 'on', }) # --- -# name: test_switch[None-b5512][switch.main_door_momentarily_unlocked-entry] +# name: test_switch[b5512-None][switch.main_door_momentarily_unlocked-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -274,7 +274,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_switch[None-b5512][switch.main_door_momentarily_unlocked-state] +# name: test_switch[b5512-None][switch.main_door_momentarily_unlocked-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Main Door Momentarily unlocked', @@ -287,7 +287,7 @@ 'state': 'off', }) # --- -# name: test_switch[None-b5512][switch.main_door_secured-entry] +# name: test_switch[b5512-None][switch.main_door_secured-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -322,7 +322,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_switch[None-b5512][switch.main_door_secured-state] +# name: test_switch[b5512-None][switch.main_door_secured-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Main Door Secured', @@ -335,7 +335,7 @@ 'state': 'off', }) # --- -# name: test_switch[None-b5512][switch.output_a-entry] +# name: test_switch[b5512-None][switch.output_a-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -370,7 +370,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_switch[None-b5512][switch.output_a-state] +# name: test_switch[b5512-None][switch.output_a-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Output A', @@ -383,7 +383,7 @@ 'state': 'off', }) # --- -# name: test_switch[None-solution_3000][switch.main_door_locked-entry] +# name: test_switch[solution_3000-None][switch.main_door_locked-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -418,7 +418,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_switch[None-solution_3000][switch.main_door_locked-state] +# name: test_switch[solution_3000-None][switch.main_door_locked-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Main Door Locked', @@ -431,7 +431,7 @@ 'state': 'on', }) # --- -# name: test_switch[None-solution_3000][switch.main_door_momentarily_unlocked-entry] +# name: test_switch[solution_3000-None][switch.main_door_momentarily_unlocked-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -466,7 +466,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_switch[None-solution_3000][switch.main_door_momentarily_unlocked-state] +# name: test_switch[solution_3000-None][switch.main_door_momentarily_unlocked-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Main Door Momentarily unlocked', @@ -479,7 +479,7 @@ 'state': 'off', }) # --- -# name: test_switch[None-solution_3000][switch.main_door_secured-entry] +# name: test_switch[solution_3000-None][switch.main_door_secured-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -514,7 +514,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_switch[None-solution_3000][switch.main_door_secured-state] +# name: test_switch[solution_3000-None][switch.main_door_secured-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Main Door Secured', @@ -527,7 +527,7 @@ 'state': 'off', }) # --- -# name: test_switch[None-solution_3000][switch.output_a-entry] +# name: test_switch[solution_3000-None][switch.output_a-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -562,7 +562,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_switch[None-solution_3000][switch.output_a-state] +# name: test_switch[solution_3000-None][switch.output_a-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Output A', diff --git a/tests/components/google_generative_ai_conversation/conftest.py b/tests/components/google_generative_ai_conversation/conftest.py index 6c5d70139e289..655be39b90c2c 100644 --- a/tests/components/google_generative_ai_conversation/conftest.py +++ b/tests/components/google_generative_ai_conversation/conftest.py @@ -1,6 +1,6 @@ """Tests helpers.""" -from collections.abc import Generator +from collections.abc import AsyncGenerator, Generator from unittest.mock import AsyncMock, Mock, patch import pytest @@ -108,13 +108,14 @@ async def mock_config_entry_with_google_search( @pytest.fixture async def mock_init_component( hass: HomeAssistant, mock_config_entry: ConfigEntry -) -> None: +) -> AsyncGenerator[None]: """Initialize integration.""" with patch("google.genai.models.AsyncModels.get"): assert await async_setup_component( hass, "google_generative_ai_conversation", {} ) await hass.async_block_till_done() + yield @pytest.fixture(autouse=True) diff --git a/tests/components/homewizard/conftest.py b/tests/components/homewizard/conftest.py index c6098342d259a..b6946d1a68f4b 100644 --- a/tests/components/homewizard/conftest.py +++ b/tests/components/homewizard/conftest.py @@ -41,6 +41,16 @@ def mock_homewizardenergy( "homeassistant.components.homewizard.config_flow.HomeWizardEnergyV1", new=homewizard, ), + patch( + "homeassistant.components.homewizard.has_v2_api", + autospec=True, + return_value=False, + ), + patch( + "homeassistant.components.homewizard.config_flow.has_v2_api", + autospec=True, + return_value=False, + ), ): client = homewizard.return_value diff --git a/tests/components/matter/conftest.py b/tests/components/matter/conftest.py index 7901045f4a2d1..2b2ba5dac0c57 100644 --- a/tests/components/matter/conftest.py +++ b/tests/components/matter/conftest.py @@ -96,6 +96,7 @@ async def integration_fixture( "eve_energy_plug", "eve_energy_plug_patched", "eve_thermo", + "eve_shutter", "eve_weather_sensor", "extended_color_light", "extractor_hood", diff --git a/tests/components/matter/fixtures/nodes/eve_shutter.json b/tests/components/matter/fixtures/nodes/eve_shutter.json new file mode 100644 index 0000000000000..6e87cd695261b --- /dev/null +++ b/tests/components/matter/fixtures/nodes/eve_shutter.json @@ -0,0 +1,617 @@ +{ + "node_id": 148, + "date_commissioned": "2025-11-07T16:57:31.360667", + "last_interview": "2025-11-07T16:57:31.360690", + "interview_version": 6, + "available": true, + "is_bridge": false, + "attributes": { + "0/29/0": [ + { + "0": 18, + "1": 1 + }, + { + "0": 22, + "1": 3 + } + ], + "0/29/1": [29, 31, 40, 42, 48, 49, 50, 51, 52, 53, 56, 60, 62, 63], + "0/29/2": [41], + "0/29/3": [1], + "0/29/65532": 0, + "0/29/65533": 2, + "0/29/65528": [], + "0/29/65529": [], + "0/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "0/31/0": [ + { + "1": 5, + "2": 2, + "3": [112233], + "4": null, + "254": 3 + } + ], + "0/31/1": [], + "0/31/2": 10, + "0/31/3": 3, + "0/31/4": 5, + "0/31/65532": 1, + "0/31/65533": 2, + "0/31/65528": [], + "0/31/65529": [], + "0/31/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533], + "0/40/0": 18, + "0/40/1": "Eve Systems", + "0/40/2": 4874, + "0/40/3": "Eve Shutter Switch 20ECI1701", + "0/40/4": 96, + "0/40/5": "", + "0/40/6": "**REDACTED**", + "0/40/7": 1, + "0/40/8": "1.1", + "0/40/9": 10203, + "0/40/10": "3.6.1", + "0/40/15": "**********", + "0/40/18": "**********", + "0/40/19": { + "0": 3, + "1": 3 + }, + "0/40/21": 17039616, + "0/40/22": 1, + "0/40/65532": 0, + "0/40/65533": 4, + "0/40/65528": [], + "0/40/65529": [], + "0/40/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 18, 19, 21, 22, 65528, 65529, 65531, + 65532, 65533 + ], + "0/42/0": [], + "0/42/1": true, + "0/42/2": 1, + "0/42/3": null, + "0/42/65532": 0, + "0/42/65533": 1, + "0/42/65528": [], + "0/42/65529": [0], + "0/42/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "0/48/0": 0, + "0/48/1": { + "0": 60, + "1": 900 + }, + "0/48/2": 0, + "0/48/3": 0, + "0/48/4": true, + "0/48/65532": 0, + "0/48/65533": 2, + "0/48/65528": [1, 3, 5], + "0/48/65529": [0, 2, 4], + "0/48/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533], + "0/49/0": 1, + "0/49/1": [ + { + "0": "p0jbsOzJRNw=", + "1": true + } + ], + "0/49/2": 10, + "0/49/3": 20, + "0/49/4": true, + "0/49/5": 0, + "0/49/6": "p0jbsOzJRNw=", + "0/49/7": null, + "0/49/9": 10, + "0/49/10": 5, + "0/49/65532": 2, + "0/49/65533": 2, + "0/49/65528": [1, 5, 7], + "0/49/65529": [0, 3, 4, 6, 8], + "0/49/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 9, 10, 65528, 65529, 65531, 65532, 65533 + ], + "0/50/65532": 0, + "0/50/65533": 1, + "0/50/65528": [1], + "0/50/65529": [0], + "0/50/65531": [65528, 65529, 65531, 65532, 65533], + "0/51/0": [ + { + "0": "ieee802154", + "1": true, + "2": null, + "3": null, + "4": "Wi5/8pP0edY=", + "5": [], + "6": [], + "7": 4 + } + ], + "0/51/1": 1, + "0/51/2": 213, + "0/51/3": 0, + "0/51/5": [], + "0/51/6": [], + "0/51/7": [], + "0/51/8": false, + "0/51/65532": 0, + "0/51/65533": 2, + "0/51/65528": [2], + "0/51/65529": [0, 1], + "0/51/65531": [0, 1, 2, 3, 5, 6, 7, 8, 65528, 65529, 65531, 65532, 65533], + "0/52/1": 10104, + "0/52/2": 2008, + "0/52/65532": 0, + "0/52/65533": 1, + "0/52/65528": [], + "0/52/65529": [], + "0/52/65531": [1, 2, 65528, 65529, 65531, 65532, 65533], + "0/53/0": 25, + "0/53/1": 5, + "0/53/2": "MyHome", + "0/53/3": 4660, + "0/53/4": 12054125955590472924, + "0/53/5": "QP0ADbgAoAAA", + "0/53/6": 0, + "0/53/7": [ + { + "0": 12864791528929066571, + "1": 28, + "2": 11264, + "3": 411672, + "4": 11555, + "5": 3, + "6": -53, + "7": -53, + "8": 44, + "9": 0, + "10": true, + "11": true, + "12": true, + "13": false + }, + { + "0": 13438285129078731668, + "1": 16, + "2": 18432, + "3": 50641, + "4": 10901, + "5": 1, + "6": -89, + "7": -89, + "8": 0, + "9": 0, + "10": true, + "11": true, + "12": true, + "13": false + }, + { + "0": 8265194500311707858, + "1": 51, + "2": 24576, + "3": 75011, + "4": 10782, + "5": 2, + "6": -84, + "7": -84, + "8": 0, + "9": 0, + "10": true, + "11": true, + "12": true, + "13": false + }, + { + "0": 14318601490803184919, + "1": 16, + "2": 27648, + "3": 310236, + "4": 10937, + "5": 3, + "6": -50, + "7": -50, + "8": 20, + "9": 0, + "10": true, + "11": true, + "12": true, + "13": false + }, + { + "0": 2202349555917590819, + "1": 22, + "2": 45056, + "3": 86183, + "4": 25554, + "5": 3, + "6": -78, + "7": -85, + "8": 3, + "9": 0, + "10": true, + "11": true, + "12": true, + "13": false + }, + { + "0": 4206032556233211940, + "1": 63, + "2": 53248, + "3": 80879, + "4": 10668, + "5": 3, + "6": -78, + "7": -77, + "8": 3, + "9": 0, + "10": true, + "11": true, + "12": true, + "13": false + }, + { + "0": 7085268071783685380, + "1": 15, + "2": 54272, + "3": 4269, + "4": 3159, + "5": 3, + "6": -76, + "7": -74, + "8": 0, + "9": 0, + "10": true, + "11": true, + "12": true, + "13": false + }, + { + "0": 10848996971365580420, + "1": 17, + "2": 60416, + "3": 318410, + "4": 10506, + "5": 3, + "6": -61, + "7": -62, + "8": 43, + "9": 0, + "10": true, + "11": true, + "12": true, + "13": false + } + ], + "0/53/8": [ + { + "0": 12864791528929066571, + "1": 11264, + "2": 11, + "3": 27, + "4": 1, + "5": 3, + "6": 3, + "7": 28, + "8": true, + "9": true + }, + { + "0": 13438285129078731668, + "1": 18432, + "2": 18, + "3": 53, + "4": 1, + "5": 1, + "6": 2, + "7": 16, + "8": true, + "9": true + }, + { + "0": 8265194500311707858, + "1": 24576, + "2": 24, + "3": 52, + "4": 1, + "5": 2, + "6": 3, + "7": 51, + "8": true, + "9": true + }, + { + "0": 14318601490803184919, + "1": 27648, + "2": 27, + "3": 11, + "4": 1, + "5": 3, + "6": 3, + "7": 16, + "8": true, + "9": true + }, + { + "0": 6498271992183290326, + "1": 40960, + "2": 40, + "3": 63, + "4": 0, + "5": 0, + "6": 0, + "7": 0, + "8": true, + "9": false + }, + { + "0": 2202349555917590819, + "1": 45056, + "2": 44, + "3": 27, + "4": 1, + "5": 3, + "6": 2, + "7": 22, + "8": true, + "9": true + }, + { + "0": 4206032556233211940, + "1": 53248, + "2": 52, + "3": 59, + "4": 1, + "5": 3, + "6": 3, + "7": 63, + "8": true, + "9": true + }, + { + "0": 7085268071783685380, + "1": 54272, + "2": 53, + "3": 27, + "4": 2, + "5": 3, + "6": 3, + "7": 15, + "8": true, + "9": true + }, + { + "0": 10848996971365580420, + "1": 60416, + "2": 59, + "3": 11, + "4": 1, + "5": 3, + "6": 3, + "7": 17, + "8": true, + "9": true + } + ], + "0/53/9": 1938283056, + "0/53/10": 68, + "0/53/11": 65, + "0/53/12": 8, + "0/53/13": 27, + "0/53/14": 1, + "0/53/15": 1, + "0/53/16": 1, + "0/53/17": 0, + "0/53/18": 1, + "0/53/19": 1, + "0/53/20": 0, + "0/53/21": 0, + "0/53/22": 759, + "0/53/23": 737, + "0/53/24": 22, + "0/53/25": 737, + "0/53/26": 737, + "0/53/27": 22, + "0/53/28": 759, + "0/53/29": 0, + "0/53/30": 0, + "0/53/31": 0, + "0/53/32": 0, + "0/53/33": 529, + "0/53/34": 0, + "0/53/35": 0, + "0/53/36": 0, + "0/53/37": 0, + "0/53/38": 0, + "0/53/39": 3405, + "0/53/40": 275, + "0/53/41": 126, + "0/53/42": 392, + "0/53/43": 0, + "0/53/44": 0, + "0/53/45": 0, + "0/53/46": 0, + "0/53/47": 0, + "0/53/48": 2796, + "0/53/49": 9, + "0/53/50": 18, + "0/53/51": 10, + "0/53/52": 0, + "0/53/53": 0, + "0/53/54": 70, + "0/53/55": 110, + "0/53/59": { + "0": 672, + "1": 143 + }, + "0/53/60": "AB//4A==", + "0/53/61": { + "0": true, + "1": false, + "2": true, + "3": true, + "4": true, + "5": true, + "6": false, + "7": true, + "8": true, + "9": true, + "10": true, + "11": true + }, + "0/53/62": [], + "0/53/65532": 15, + "0/53/65533": 3, + "0/53/65528": [], + "0/53/65529": [0], + "0/53/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, + 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, + 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 59, + 60, 61, 62, 65528, 65529, 65531, 65532, 65533 + ], + "0/56/0": 815849852639528, + "0/56/1": 2, + "0/56/2": 2, + "0/56/3": null, + "0/56/5": [ + { + "0": 3600, + "1": 0, + "2": "Europe/Paris" + } + ], + "0/56/6": [ + { + "0": 0, + "1": 0, + "2": 828061200000000 + }, + { + "0": 3600, + "1": 828061200000000, + "2": 846205200000000 + } + ], + "0/56/7": 815853452640810, + "0/56/8": 2, + "0/56/10": 2, + "0/56/11": 2, + "0/56/65532": 9, + "0/56/65533": 2, + "0/56/65528": [3], + "0/56/65529": [0, 1, 2, 4], + "0/56/65531": [ + 0, 1, 2, 3, 5, 6, 7, 8, 10, 11, 65528, 65529, 65531, 65532, 65533 + ], + "0/60/0": 0, + "0/60/1": null, + "0/60/2": null, + "0/60/65532": 1, + "0/60/65533": 1, + "0/60/65528": [], + "0/60/65529": [0, 1, 2], + "0/60/65531": [0, 1, 2, 65528, 65529, 65531, 65532, 65533], + "0/62/0": [ + { + "1": "FTABAQEkAgE3AyQTAhgmBIAigScmBYAlTTo3BiQVAiQRlBgkBwEkCAEwCUEEQ2q1XJVV19WnxHHSfOUdx9bDmdDqjNtb9YgZA2j76IaZCChToVK6aKvw+YxIPL3mgzVfD08t2wHpcNjyBIAYFjcKNQEoARgkAgE2AwQCBAEYMAQUXObIaHWU7+qbdq7roNf1TweBIfMwBRS5+zzv8ZPGnI9mC3wH9vq10JnwlhgwC0BByE+Cvdi+klStM4F55ptZC4sE7IRIzqFHEUa2CZY2k7uTFPj9Yo1YzWgpnNJlAc0vnGXdN9E7B6yttZk4tSkZGA==", + "2": "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQTAhgkBwEkCAEwCUEE/DujEcdTsX19xbxX+KuKKWiMaA5D9u99P/pVxIOmscd2BA2PadEMNnjvtPOpf+WE2Zxar4rby1IfAClGUUuQrTcKNQEpARgkAmAwBBS5+zzv8ZPGnI9mC3wH9vq10JnwljAFFPT6p93JKGcb7g+rTWnA6evF2EdGGDALQGkPpvsbkAFEbfPN6H3Kf23R0zzmW/gpAA3kgaL6wKB2Ofm+Tmylw22qM536Kj8mOMwaV0EL1dCCGcuxF98aL6gY", + "254": 3 + } + ], + "0/62/1": [ + { + "1": "BBmX+KwLR5HGlVNbvlC+dO8Jv9fPthHiTfGpUzi2JJADX5az6GxBAFn02QKHwLcZHyh+lh9faf6rf38/nPYF7/M=", + "2": 4939, + "3": 2, + "4": 148, + "5": "ha-freebox", + "254": 3 + } + ], + "0/62/2": 5, + "0/62/3": 3, + "0/62/4": [ + "FTABAQAkAgE3AyYUyakYCSYVj6gLsxgmBBHhoDAkBQA3BiYUyakYCSYVj6gLsxgkBwEkCAEwCUEEgYwxrTB+tyiEGfrRwjlXTG34MiQtJXbg5Qqd0ohdRW7MfwYY7vZiX/0h9hI8MqUralFaVPcnghAP0MSJm1YrqTcKNQEpARgkAmAwBBS3BS9aJzt+p6i28Nj+trB2Uu+vdzAFFLcFL1onO36nqLbw2P62sHZS7693GDALQKhZq5zQ3AYFGQVcWu+OD8c4yQyTpkGu09UkZu0SXSjWU0Onq7U6RnfhEnsCTZeNC3TB25octZQPnoe4yQyMhOMY", + "FTABAQAkAgE3AycUQhmZbaIbYjokFQIYJgRWZLcqJAUANwYnFEIZmW2iG2I6JBUCGCQHASQIATAJQQT2AlKGW/kOMjqayzeO0md523/fuhrhGEUU91uQpTiKo0I7wcPpKnmrwfQNPX6g0kEQl+VGaXa3e22lzfu5Tzp0Nwo1ASkBGCQCYDAEFOOMk13ScMKuT2hlaydi1yEJnhTqMAUU44yTXdJwwq5PaGVrJ2LXIQmeFOoYMAtAv2jJd1qd5miXbYesH1XrJ+vgyY0hzGuZ78N6Jw4Cb1oN1sLSpA+PNM0u7+hsEqcSvvn2eSV8EaRR+hg5YQjHDxg=", + "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQUARgkBwEkCAEwCUEEGZf4rAtHkcaVU1u+UL507wm/18+2EeJN8alTOLYkkANflrPobEEAWfTZAofAtxkfKH6WH19p/qt/fz+c9gXv8zcKNQEpARgkAmAwBBT0+qfdyShnG+4Pq01pwOnrxdhHRjAFFPT6p93JKGcb7g+rTWnA6evF2EdGGDALQPVrsFnfFplsQGV5m5EUua+rmo9hAr+OP1bvaifdLqiEIn3uXLTLoKmVUkPImRL2Fb+xcMEAqR2p7RM6ZlFCR20Y" + ], + "0/62/5": 3, + "0/62/65532": 0, + "0/62/65533": 1, + "0/62/65528": [1, 3, 5, 8], + "0/62/65529": [0, 2, 4, 6, 7, 9, 10, 11], + "0/62/65531": [0, 1, 2, 3, 4, 5, 65528, 65529, 65531, 65532, 65533], + "0/63/0": [], + "0/63/1": [], + "0/63/2": 4, + "0/63/3": 3, + "0/63/65532": 0, + "0/63/65533": 2, + "0/63/65528": [2, 5], + "0/63/65529": [0, 1, 3, 4], + "0/63/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "1/3/0": 0, + "1/3/1": 2, + "1/3/65532": 0, + "1/3/65533": 5, + "1/3/65528": [], + "1/3/65529": [0], + "1/3/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], + "1/4/0": 128, + "1/4/65532": 1, + "1/4/65533": 4, + "1/4/65528": [0, 1, 2, 3], + "1/4/65529": [0, 1, 2, 3, 4, 5], + "1/4/65531": [0, 65528, 65529, 65531, 65532, 65533], + "1/29/0": [ + { + "0": 514, + "1": 3 + } + ], + "1/29/1": [3, 4, 29, 258, 319486977], + "1/29/2": [], + "1/29/3": [], + "1/29/65532": 0, + "1/29/65533": 2, + "1/29/65528": [], + "1/29/65529": [], + "1/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "1/258/0": 0, + "1/258/7": 9, + "1/258/10": 0, + "1/258/11": 0, + "1/258/13": 0, + "1/258/14": 0, + "1/258/23": 0, + "1/258/26": 0, + "1/258/65532": 5, + "1/258/65533": 5, + "1/258/65528": [], + "1/258/65529": [0, 1, 2, 5], + "1/258/65531": [ + 0, 7, 10, 11, 13, 14, 23, 26, 65528, 65529, 65531, 65532, 65533 + ], + "1/319486977/319422464": "AAJgAAsCAAADAuEnBAxCSzM2TjJBMDEyMDWcAQD/BAECAKD5AQEdAQj/BCUCvg7wAcPxAf/vHwEAAAD//7mFDWkAAAAAzzAOaQAAAAAAZAAAAAAAAAD6AQDzFQHDAP8KFAAAAAEAAAAAAAAAAAAAAF0EAAAAAP4JEagIAABuCwAARQUFAAAAAEZUBW0jLA8AAEIGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASQYFDAgQgAFEEQUMAAUDPAAAAOEpPkKHWiu/RxEFAiTUJG4lRyZ4AAAAPAAAAEgGBQAAAAAASgYFAAAAAAD/CyIJEAAAAAAAAAAA", + "1/319486977/319422466": "2AAAAM0AAABxXL4uBiUBKQEaARcBGAEZAQMAABAAAAAAAgAAAAEAAA==", + "1/319486977/319422467": "", + "1/319486977/319422479": false, + "1/319486977/319422480": false, + "1/319486977/319422481": false, + "1/319486977/319422482": 40960, + "1/319486977/65532": 0, + "1/319486977/65533": 1, + "1/319486977/65528": [], + "1/319486977/65529": [], + "1/319486977/65531": [ + 65528, 65529, 65531, 319422464, 319422465, 319422466, 319422467, + 319422468, 319422469, 319422479, 319422480, 319422481, 319422482, 65532, + 65533 + ] + }, + "attribute_subscriptions": [] +} diff --git a/tests/components/matter/snapshots/test_button.ambr b/tests/components/matter/snapshots/test_button.ambr index dd72c7e2e8170..e44e3641fa8a4 100644 --- a/tests/components/matter/snapshots/test_button.ambr +++ b/tests/components/matter/snapshots/test_button.ambr @@ -1271,6 +1271,55 @@ 'state': 'unknown', }) # --- +# name: test_buttons[eve_shutter][button.eve_shutter_switch_20eci1701_identify-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.eve_shutter_switch_20eci1701_identify', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Identify', + 'platform': 'matter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000000000004D2-0000000000000094-MatterNodeDevice-1-IdentifyButton-3-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_buttons[eve_shutter][button.eve_shutter_switch_20eci1701_identify-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'identify', + 'friendly_name': 'Eve Shutter Switch 20ECI1701 Identify', + }), + 'context': , + 'entity_id': 'button.eve_shutter_switch_20eci1701_identify', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_buttons[eve_thermo][button.eve_thermo_identify-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/matter/snapshots/test_cover.ambr b/tests/components/matter/snapshots/test_cover.ambr index 9efe31ed42828..58006ea61c891 100644 --- a/tests/components/matter/snapshots/test_cover.ambr +++ b/tests/components/matter/snapshots/test_cover.ambr @@ -1,4 +1,55 @@ # serializer version: 1 +# name: test_covers[eve_shutter][cover.eve_shutter_switch_20eci1701-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'cover', + 'entity_category': None, + 'entity_id': 'cover.eve_shutter_switch_20eci1701', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'matter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '00000000000004D2-0000000000000094-MatterNodeDevice-1-MatterCoverPositionAwareLift-258-10', + 'unit_of_measurement': None, + }) +# --- +# name: test_covers[eve_shutter][cover.eve_shutter_switch_20eci1701-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_position': 100, + 'device_class': 'shade', + 'friendly_name': 'Eve Shutter Switch 20ECI1701', + 'supported_features': , + }), + 'context': , + 'entity_id': 'cover.eve_shutter_switch_20eci1701', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'open', + }) +# --- # name: test_covers[window_covering_full][cover.mock_full_window_covering-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/matter/snapshots/test_sensor.ambr b/tests/components/matter/snapshots/test_sensor.ambr index 68c0c61199836..089db73bc151a 100644 --- a/tests/components/matter/snapshots/test_sensor.ambr +++ b/tests/components/matter/snapshots/test_sensor.ambr @@ -4582,6 +4582,55 @@ 'state': '220.0', }) # --- +# name: test_sensors[eve_shutter][sensor.eve_shutter_switch_20eci1701_target_opening_position-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.eve_shutter_switch_20eci1701_target_opening_position', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Target opening position', + 'platform': 'matter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'window_covering_target_position', + 'unique_id': '00000000000004D2-0000000000000094-MatterNodeDevice-1-TargetPositionLiftPercent100ths-258-11', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[eve_shutter][sensor.eve_shutter_switch_20eci1701_target_opening_position-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Eve Shutter Switch 20ECI1701 Target opening position', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.eve_shutter_switch_20eci1701_target_opening_position', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- # name: test_sensors[eve_thermo][sensor.eve_thermo_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/music_assistant/conftest.py b/tests/components/music_assistant/conftest.py index 5eefccbcda9e9..73b25760d2519 100644 --- a/tests/components/music_assistant/conftest.py +++ b/tests/components/music_assistant/conftest.py @@ -23,7 +23,7 @@ def mock_get_server_info() -> Generator[AsyncMock]: """Mock the function to get server info.""" with patch( - "homeassistant.components.music_assistant.config_flow.get_server_info" + "homeassistant.components.music_assistant.config_flow._get_server_info" ) as mock_get_server_info: mock_get_server_info.return_value = ServerInfoMessage.from_json( load_fixture("server_info_message.json", DOMAIN) diff --git a/tests/components/music_assistant/test_config_flow.py b/tests/components/music_assistant/test_config_flow.py index c9cb465b7c715..e2a4773861e47 100644 --- a/tests/components/music_assistant/test_config_flow.py +++ b/tests/components/music_assistant/test_config_flow.py @@ -66,7 +66,7 @@ async def test_full_flow( assert result["result"].unique_id == "1234" -async def test_zero_conf_flow( +async def test_zeroconf_flow( hass: HomeAssistant, mock_get_server_info: AsyncMock, ) -> None: @@ -90,21 +90,21 @@ async def test_zero_conf_flow( assert result["result"].unique_id == "1234" -async def test_zero_conf_missing_server_id( +async def test_zeroconf_invalid_discovery_info( hass: HomeAssistant, mock_get_server_info: AsyncMock, ) -> None: - """Test zeroconf flow with missing server id.""" - bad_zero_conf_data = deepcopy(ZEROCONF_DATA) - bad_zero_conf_data.properties.pop("server_id") + """Test zeroconf flow with invalid discovery info.""" + bad_zeroconf_data = deepcopy(ZEROCONF_DATA) + bad_zeroconf_data.properties.pop("server_id") result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data=bad_zero_conf_data, + data=bad_zeroconf_data, ) await hass.async_block_till_done() assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "missing_server_id" + assert result["reason"] == "invalid_discovery_info" async def test_duplicate_user( @@ -324,46 +324,6 @@ async def test_zeroconf_existing_entry_working_url( assert mock_config_entry.data[CONF_URL] == "http://localhost:8095" -async def test_zeroconf_existing_entry_broken_url( - hass: HomeAssistant, - mock_get_server_info: AsyncMock, - mock_config_entry: MockConfigEntry, -) -> None: - """Test zeroconf flow when existing entry has broken URL.""" - mock_config_entry.add_to_hass(hass) - - # Create modified zeroconf data with different base_url - modified_zeroconf_data = deepcopy(ZEROCONF_DATA) - modified_zeroconf_data.properties["base_url"] = "http://discovered-working-url:8095" - - # Mock server info with the discovered URL - server_info = ServerInfoMessage.from_json( - await async_load_fixture(hass, "server_info_message.json", DOMAIN) - ) - server_info.base_url = "http://discovered-working-url:8095" - mock_get_server_info.return_value = server_info - - # First call (testing current URL) should fail, second call (testing discovered URL) should succeed - mock_get_server_info.side_effect = [ - CannotConnect("cannot_connect"), # Current URL fails - server_info, # Discovered URL works - ] - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_ZEROCONF}, - data=modified_zeroconf_data, - ) - await hass.async_block_till_done() - - # Should proceed to discovery confirm because current URL is broken - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "discovery_confirm" - # Verify the URL was updated in the config entry - updated_entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id) - assert updated_entry.data[CONF_URL] == "http://discovered-working-url:8095" - - async def test_zeroconf_existing_entry_ignored( hass: HomeAssistant, mock_get_server_info: AsyncMock, @@ -396,7 +356,3 @@ async def test_zeroconf_existing_entry_ignored( # Should abort because entry was ignored (respect user's choice) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" - # Verify the ignored entry was not modified - ignored_entry = hass.config_entries.async_get_entry(ignored_config_entry.entry_id) - assert ignored_entry.data == {} # Still no URL field - assert ignored_entry.source == SOURCE_IGNORE diff --git a/tests/components/openai_conversation/test_ai_task.py b/tests/components/openai_conversation/test_ai_task.py index 77d878bcfa136..7ef1be132556a 100644 --- a/tests/components/openai_conversation/test_ai_task.py +++ b/tests/components/openai_conversation/test_ai_task.py @@ -3,7 +3,6 @@ from pathlib import Path from unittest.mock import AsyncMock, patch -from freezegun import freeze_time import httpx from openai import PermissionDeniedError import pytest @@ -212,7 +211,7 @@ async def test_generate_data_with_attachments( @pytest.mark.usefixtures("mock_init_component") -@freeze_time("2025-06-14 22:59:00") +@pytest.mark.freeze_time("2025-06-14 22:59:00") @pytest.mark.parametrize("image_model", ["gpt-image-1", "gpt-image-1-mini"]) async def test_generate_image( hass: HomeAssistant, diff --git a/tests/components/pglab/test_sensor.py b/tests/components/pglab/test_sensor.py index 0991d6bd814a8..bb42fdc154e9a 100644 --- a/tests/components/pglab/test_sensor.py +++ b/tests/components/pglab/test_sensor.py @@ -2,7 +2,6 @@ import json -from freezegun import freeze_time import pytest from syrupy.assertion import SnapshotAssertion @@ -14,7 +13,7 @@ from tests.typing import MqttMockHAClient -@freeze_time("2024-02-26 01:21:34") +@pytest.mark.freeze_time("2024-02-26 01:21:34") @pytest.mark.parametrize( "sensor_suffix", [ diff --git a/tests/components/plaato/__init__.py b/tests/components/plaato/__init__.py index 5a2b2a68d442f..dac4d341790c6 100644 --- a/tests/components/plaato/__init__.py +++ b/tests/components/plaato/__init__.py @@ -1,55 +1 @@ """Tests for the Plaato integration.""" - -from unittest.mock import patch - -from freezegun import freeze_time -from pyplaato.models.airlock import PlaatoAirlock -from pyplaato.models.device import PlaatoDeviceType -from pyplaato.models.keg import PlaatoKeg - -from homeassistant.components.plaato.const import ( - CONF_DEVICE_NAME, - CONF_DEVICE_TYPE, - CONF_USE_WEBHOOK, - DOMAIN, -) -from homeassistant.const import CONF_TOKEN -from homeassistant.core import HomeAssistant - -from tests.common import MockConfigEntry - -# Note: It would be good to replace this test data -# with actual data from the API -AIRLOCK_DATA = {} -KEG_DATA = {} - - -@freeze_time("2024-05-24 12:00:00", tz_offset=0) -async def init_integration( - hass: HomeAssistant, device_type: PlaatoDeviceType -) -> MockConfigEntry: - """Mock integration setup.""" - with ( - patch( - "homeassistant.components.plaato.coordinator.Plaato.get_airlock_data", - return_value=PlaatoAirlock(AIRLOCK_DATA), - ), - patch( - "homeassistant.components.plaato.coordinator.Plaato.get_keg_data", - return_value=PlaatoKeg(KEG_DATA), - ), - ): - entry = MockConfigEntry( - domain=DOMAIN, - data={ - CONF_USE_WEBHOOK: False, - CONF_TOKEN: "valid_token", - CONF_DEVICE_TYPE: device_type, - CONF_DEVICE_NAME: "device_name", - }, - entry_id="123456", - ) - entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - return entry diff --git a/tests/components/plaato/conftest.py b/tests/components/plaato/conftest.py new file mode 100644 index 0000000000000..6b882393fd04b --- /dev/null +++ b/tests/components/plaato/conftest.py @@ -0,0 +1,62 @@ +"""Test fixtures for the Plaato integration.""" + +from collections.abc import AsyncGenerator +from unittest.mock import patch + +from pyplaato.models.airlock import PlaatoAirlock +from pyplaato.models.device import PlaatoDeviceType +from pyplaato.models.keg import PlaatoKeg +import pytest + +from homeassistant.components.plaato.const import ( + CONF_DEVICE_NAME, + CONF_DEVICE_TYPE, + CONF_USE_WEBHOOK, + DOMAIN, +) +from homeassistant.const import CONF_TOKEN, Platform +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +# Note: It would be good to replace this test data +# with actual data from the API +AIRLOCK_DATA = {} +KEG_DATA = {} + + +@pytest.fixture +async def init_integration( + hass: HomeAssistant, + device_type: PlaatoDeviceType, + platform: Platform, +) -> AsyncGenerator[MockConfigEntry]: + """Mock integration setup.""" + with ( + patch( + "homeassistant.components.plaato.PLATFORMS", + [platform], + ), + patch( + "homeassistant.components.plaato.coordinator.Plaato.get_airlock_data", + return_value=PlaatoAirlock(AIRLOCK_DATA), + ), + patch( + "homeassistant.components.plaato.coordinator.Plaato.get_keg_data", + return_value=PlaatoKeg(KEG_DATA), + ), + ): + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_USE_WEBHOOK: False, + CONF_TOKEN: "valid_token", + CONF_DEVICE_TYPE: device_type, + CONF_DEVICE_NAME: "device_name", + }, + entry_id="123456", + ) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + yield entry diff --git a/tests/components/plaato/test_binary_sensor.py b/tests/components/plaato/test_binary_sensor.py index 5542c79e8eaa9..17cdda5cae866 100644 --- a/tests/components/plaato/test_binary_sensor.py +++ b/tests/components/plaato/test_binary_sensor.py @@ -1,7 +1,5 @@ """Tests for the plaato binary sensors.""" -from unittest.mock import patch - from pyplaato.models.device import PlaatoDeviceType import pytest from syrupy.assertion import SnapshotAssertion @@ -10,24 +8,23 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from . import init_integration +from tests.common import MockConfigEntry, snapshot_platform + -from tests.common import snapshot_platform +@pytest.fixture +def platform() -> Platform: + """Fixture to specify platform.""" + return Platform.BINARY_SENSOR # note: PlaatoDeviceType.Airlock does not provide binary sensors @pytest.mark.parametrize("device_type", [PlaatoDeviceType.Keg]) +@pytest.mark.freeze_time("2024-05-24 12:00:00", tz_offset=0) async def test_binary_sensors( hass: HomeAssistant, entity_registry: er.EntityRegistry, + init_integration: MockConfigEntry, snapshot: SnapshotAssertion, - device_type: PlaatoDeviceType, ) -> None: """Test binary sensors.""" - with patch( - "homeassistant.components.plaato.PLATFORMS", - [Platform.BINARY_SENSOR], - ): - entry = await init_integration(hass, device_type) - - await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id) + await snapshot_platform(hass, entity_registry, snapshot, init_integration.entry_id) diff --git a/tests/components/plaato/test_sensor.py b/tests/components/plaato/test_sensor.py index 63e9255faa0d9..865e2d0078116 100644 --- a/tests/components/plaato/test_sensor.py +++ b/tests/components/plaato/test_sensor.py @@ -1,7 +1,5 @@ """Tests for the plaato sensors.""" -from unittest.mock import patch - from pyplaato.models.device import PlaatoDeviceType import pytest from syrupy.assertion import SnapshotAssertion @@ -10,25 +8,24 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from . import init_integration +from tests.common import MockConfigEntry, snapshot_platform + -from tests.common import snapshot_platform +@pytest.fixture +def platform() -> Platform: + """Fixture to specify platform.""" + return Platform.SENSOR @pytest.mark.parametrize( "device_type", [PlaatoDeviceType.Airlock, PlaatoDeviceType.Keg] ) +@pytest.mark.freeze_time("2024-05-24 12:00:00", tz_offset=0) async def test_sensors( hass: HomeAssistant, entity_registry: er.EntityRegistry, + init_integration: MockConfigEntry, snapshot: SnapshotAssertion, - device_type: PlaatoDeviceType, ) -> None: """Test sensors.""" - with patch( - "homeassistant.components.plaato.PLATFORMS", - [Platform.SENSOR], - ): - entry = await init_integration(hass, device_type) - - await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id) + await snapshot_platform(hass, entity_registry, snapshot, init_integration.entry_id) diff --git a/tests/components/playstation_network/test_notify.py b/tests/components/playstation_network/test_notify.py index f883ee77f9ef3..393afe92bdca0 100644 --- a/tests/components/playstation_network/test_notify.py +++ b/tests/components/playstation_network/test_notify.py @@ -3,7 +3,6 @@ from collections.abc import AsyncGenerator from unittest.mock import MagicMock, patch -from freezegun.api import freeze_time from psnawp_api.core.psnawp_exceptions import ( PSNAWPClientError, PSNAWPForbiddenError, @@ -63,7 +62,7 @@ async def test_notify_platform( "notify.testuser_direct_message_publicuniversalfriend", ], ) -@freeze_time("2025-07-28T00:00:00+00:00") +@pytest.mark.freeze_time("2025-07-28T00:00:00+00:00") @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_send_message( hass: HomeAssistant, diff --git a/tests/components/sensibo/test_button.py b/tests/components/sensibo/test_button.py index 2d93fd81ed772..8de99bc0a3d57 100644 --- a/tests/components/sensibo/test_button.py +++ b/tests/components/sensibo/test_button.py @@ -5,7 +5,6 @@ from datetime import timedelta from unittest.mock import MagicMock -from freezegun import freeze_time from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion @@ -27,7 +26,7 @@ from tests.common import async_fire_time_changed, snapshot_platform -@freeze_time("2022-03-12T15:24:26+00:00") +@pytest.mark.freeze_time("2022-03-12T15:24:26+00:00") @pytest.mark.usefixtures("entity_registry_enabled_by_default") @pytest.mark.parametrize( "load_platforms", diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index 3daa0903a0092..8123cbc5f1605 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -9,7 +9,6 @@ from typing import Any from unittest.mock import patch -from freezegun.api import freeze_time import pytest from homeassistant.components import sensor @@ -477,7 +476,7 @@ async def test_restore_sensor_save_state( assert type(extra_data["native_value"]) is native_value_type -@freeze_time("2020-02-08 15:00:00") +@pytest.mark.freeze_time("2020-02-08 15:00:00") async def test_restore_sensor_save_state_frozen_time_datetime( hass: HomeAssistant, hass_storage: dict[str, Any], @@ -505,7 +504,7 @@ async def test_restore_sensor_save_state_frozen_time_datetime( assert type(extra_data["native_value"]) is dict -@freeze_time("2020-02-08 15:00:00") +@pytest.mark.freeze_time("2020-02-08 15:00:00") async def test_restore_sensor_save_state_frozen_time_date( hass: HomeAssistant, hass_storage: dict[str, Any], diff --git a/tests/components/sleep_as_android/test_event.py b/tests/components/sleep_as_android/test_event.py index 89f21e2033527..737cf0916d289 100644 --- a/tests/components/sleep_as_android/test_event.py +++ b/tests/components/sleep_as_android/test_event.py @@ -4,7 +4,6 @@ from http import HTTPStatus from unittest.mock import patch -from freezegun.api import freeze_time import pytest from syrupy.assertion import SnapshotAssertion @@ -28,7 +27,7 @@ def event_only() -> Generator[None]: @pytest.mark.usefixtures("entity_registry_enabled_by_default") -@freeze_time("2025-01-01T03:30:00.000Z") +@pytest.mark.freeze_time("2025-01-01T03:30:00.000Z") async def test_setup( hass: HomeAssistant, config_entry: MockConfigEntry, @@ -129,7 +128,7 @@ async def test_setup( ], ) @pytest.mark.usefixtures("entity_registry_enabled_by_default") -@freeze_time("2025-01-01T03:30:00.000+00:00") +@pytest.mark.freeze_time("2025-01-01T03:30:00.000+00:00") async def test_webhook_event( hass: HomeAssistant, config_entry: MockConfigEntry, diff --git a/tests/components/smhi/test_weather.py b/tests/components/smhi/test_weather.py index fab78d5893542..45e34be650f38 100644 --- a/tests/components/smhi/test_weather.py +++ b/tests/components/smhi/test_weather.py @@ -3,7 +3,6 @@ from datetime import datetime, timedelta from unittest.mock import MagicMock -from freezegun import freeze_time from freezegun.api import FrozenDateTimeFactory from pysmhi import ( SMHIFirePointForecast, @@ -66,7 +65,7 @@ async def test_setup_hass( "to_load", [1], ) -@freeze_time(datetime(2023, 8, 7, 1, tzinfo=dt_util.UTC)) +@pytest.mark.freeze_time(datetime(2023, 8, 7, 1, tzinfo=dt_util.UTC)) async def test_clear_night( hass: HomeAssistant, mock_client: SMHIPointForecast, diff --git a/tests/components/snoo/test_event.py b/tests/components/snoo/test_event.py index 41cb386a599a3..3fc778699076c 100644 --- a/tests/components/snoo/test_event.py +++ b/tests/components/snoo/test_event.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock -from freezegun import freeze_time +import pytest from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant @@ -11,7 +11,7 @@ from .const import MOCK_SNOO_DATA -@freeze_time("2025-01-01 12:00:00") +@pytest.mark.freeze_time("2025-01-01 12:00:00") async def test_events(hass: HomeAssistant, bypass_api: AsyncMock) -> None: """Test events and check test values are correctly set.""" await async_init_integration(hass) @@ -26,7 +26,7 @@ async def test_events(hass: HomeAssistant, bypass_api: AsyncMock) -> None: ) -@freeze_time("2025-01-01 12:00:00") +@pytest.mark.freeze_time("2025-01-01 12:00:00") async def test_events_data_on_startup( hass: HomeAssistant, bypass_api: AsyncMock ) -> None: diff --git a/tests/components/squeezebox/test_config_flow.py b/tests/components/squeezebox/test_config_flow.py index 18c1fe2d7aefc..edd65b36bec42 100644 --- a/tests/components/squeezebox/test_config_flow.py +++ b/tests/components/squeezebox/test_config_flow.py @@ -4,6 +4,7 @@ from unittest.mock import patch from pysqueezebox import Server +import pytest from homeassistant import config_entries from homeassistant.components.squeezebox.const import ( @@ -96,6 +97,7 @@ async def test_user_form(hass: HomeAssistant) -> None: assert len(mock_setup_entry.mock_calls) == 1 +@pytest.mark.usefixtures("mock_setup_entry") async def test_options_form(hass: HomeAssistant) -> None: """Test we can configure options.""" entry = MockConfigEntry( @@ -605,8 +607,8 @@ async def test_dhcp_discovery_no_server_found(hass: HomeAssistant) -> None: # Now try to complete the edit step with full schema with patch( - "homeassistant.components.squeezebox.config_flow.async_discover", - mock_failed_discover, + "pysqueezebox.Server.async_query", + return_value=None, ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -621,7 +623,7 @@ async def test_dhcp_discovery_no_server_found(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.FORM assert result["step_id"] == "edit" - assert result["errors"] == {"base": "unknown"} + assert result["errors"] == {"base": "cannot_connect"} async def test_dhcp_discovery_existing_player( diff --git a/tests/components/squeezebox/test_init.py b/tests/components/squeezebox/test_init.py index a39a30200381b..bb7b3a4d7452e 100644 --- a/tests/components/squeezebox/test_init.py +++ b/tests/components/squeezebox/test_init.py @@ -26,6 +26,13 @@ def squeezebox_media_player_platform(): yield +@pytest.fixture(autouse=True) +def mock_discovery(): + """Mock discovery of squeezebox players.""" + with patch("homeassistant.components.squeezebox.media_player.async_discover"): + yield + + async def test_init_api_fail( hass: HomeAssistant, config_entry: MockConfigEntry, diff --git a/tests/components/squeezebox/test_media_player.py b/tests/components/squeezebox/test_media_player.py index d04e68f25189c..8e889b8c12e18 100644 --- a/tests/components/squeezebox/test_media_player.py +++ b/tests/components/squeezebox/test_media_player.py @@ -86,6 +86,13 @@ def squeezebox_media_player_platform(): yield +@pytest.fixture(autouse=True) +def mock_discovery(): + """Mock discovery of squeezebox players.""" + with patch("homeassistant.components.squeezebox.media_player.async_discover"): + yield + + async def test_entity_registry( hass: HomeAssistant, entity_registry: EntityRegistry, diff --git a/tests/components/switchbot/__init__.py b/tests/components/switchbot/__init__.py index 4a9fcd772b20c..16550d955e97a 100644 --- a/tests/components/switchbot/__init__.py +++ b/tests/components/switchbot/__init__.py @@ -1200,3 +1200,51 @@ def make_advertisement( connectable=True, tx_power=-127, ) + + +SMART_THERMOSTAT_RADIATOR_SERVICE_INFO = BluetoothServiceInfoBleak( + name="Smart Thermostat Radiator", + manufacturer_data={2409: b"\xb0\xe9\xfe\xa2T|6\xe4\x00\x9c\xa3A\x00"}, + service_data={ + "0000fd3d-0000-1000-8000-00805f9b34fb": b"\x00 d\x00\x116@", + }, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + address="AA:BB:CC:DD:EE:FF", + rssi=-60, + source="local", + advertisement=generate_advertisement_data( + local_name="Smart Thermostat Radiator", + manufacturer_data={2409: b"\xb0\xe9\xfe\xa2T|6\xe4\x00\x9c\xa3A\x00"}, + service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"\x00 d\x00\x116@"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + ), + device=generate_ble_device("AA:BB:CC:DD:EE:FF", "Smart Thermostat Radiator"), + time=0, + connectable=True, + tx_power=-127, +) + + +S20_VACUUM_SERVICE_INFO = BluetoothServiceInfoBleak( + name="S20 Vacuum", + manufacturer_data={2409: b"\xb0\xe9\xfe\xc3\x1a!:\x01\x11\x1e\x00\x00d\x03"}, + service_data={ + "0000fd3d-0000-1000-8000-00805f9b34fb": b"\x00\x00d\x00\x10\xe0P", + }, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + address="AA:BB:CC:DD:EE:FF", + rssi=-60, + source="local", + advertisement=generate_advertisement_data( + local_name="S20 Vacuum", + manufacturer_data={2409: b"\xb0\xe9\xfe\xc3\x1a!:\x01\x11\x1e\x00\x00d\x03"}, + service_data={ + "0000fd3d-0000-1000-8000-00805f9b34fb": b"\x00\x00d\x00\x10\xe0P", + }, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + ), + device=generate_ble_device("AA:BB:CC:DD:EE:FF", "S20 Vacuum"), + time=0, + connectable=True, + tx_power=-127, +) diff --git a/tests/components/switchbot/test_climate.py b/tests/components/switchbot/test_climate.py new file mode 100644 index 0000000000000..585c72e7bbdb4 --- /dev/null +++ b/tests/components/switchbot/test_climate.py @@ -0,0 +1,118 @@ +"""Tests for the Switchbot climate integration.""" + +from collections.abc import Callable +from unittest.mock import AsyncMock, patch + +import pytest +from switchbot import SwitchbotOperationError + +from homeassistant.components.climate import ( + DOMAIN as CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + SERVICE_SET_PRESET_MODE, + SERVICE_SET_TEMPERATURE, + HVACMode, +) +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError + +from . import SMART_THERMOSTAT_RADIATOR_SERVICE_INFO + +from tests.common import MockConfigEntry +from tests.components.bluetooth import inject_bluetooth_service_info + + +@pytest.mark.parametrize( + ("service", "service_data", "mock_method"), + [ + (SERVICE_SET_HVAC_MODE, {"hvac_mode": HVACMode.HEAT}, "set_hvac_mode"), + (SERVICE_SET_PRESET_MODE, {"preset_mode": "manual"}, "set_preset_mode"), + (SERVICE_SET_TEMPERATURE, {"temperature": 22}, "set_target_temperature"), + ], +) +async def test_smart_thermostat_radiator_controlling( + hass: HomeAssistant, + mock_entry_encrypted_factory: Callable[[str], MockConfigEntry], + service: str, + service_data: dict, + mock_method: str, +) -> None: + """Test controlling the smart thermostat radiator with different services.""" + inject_bluetooth_service_info(hass, SMART_THERMOSTAT_RADIATOR_SERVICE_INFO) + + entry = mock_entry_encrypted_factory("smart_thermostat_radiator") + entity_id = "climate.test_name" + entry.add_to_hass(hass) + + mocked_instance = AsyncMock(return_value=True) + mocked_none_instance = AsyncMock(return_value=None) + with patch.multiple( + "homeassistant.components.switchbot.climate.switchbot.SwitchbotSmartThermostatRadiator", + get_basic_info=mocked_none_instance, + update=mocked_none_instance, + **{mock_method: mocked_instance}, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + await hass.services.async_call( + CLIMATE_DOMAIN, + service, + {**service_data, ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + mocked_instance.assert_awaited_once() + + +@pytest.mark.parametrize( + ("service", "service_data", "mock_method"), + [ + (SERVICE_SET_HVAC_MODE, {"hvac_mode": HVACMode.HEAT}, "set_hvac_mode"), + (SERVICE_SET_PRESET_MODE, {"preset_mode": "manual"}, "set_preset_mode"), + (SERVICE_SET_TEMPERATURE, {"temperature": 22}, "set_target_temperature"), + ], +) +@pytest.mark.parametrize( + ("exception", "error_message"), + [ + ( + SwitchbotOperationError("Operation failed"), + "An error occurred while performing the action: Operation failed", + ), + ], +) +async def test_exception_handling_smart_thermostat_radiator_service( + hass: HomeAssistant, + mock_entry_encrypted_factory: Callable[[str], MockConfigEntry], + service: str, + service_data: dict, + mock_method: str, + exception: Exception, + error_message: str, +) -> None: + """Test exception handling for smart thermostat radiator service with exception.""" + inject_bluetooth_service_info(hass, SMART_THERMOSTAT_RADIATOR_SERVICE_INFO) + + entry = mock_entry_encrypted_factory("smart_thermostat_radiator") + entry.add_to_hass(hass) + entity_id = "climate.test_name" + + mocked_none_instance = AsyncMock(return_value=None) + with patch.multiple( + "homeassistant.components.switchbot.climate.switchbot.SwitchbotSmartThermostatRadiator", + get_basic_info=mocked_none_instance, + update=mocked_none_instance, + **{mock_method: AsyncMock(side_effect=exception)}, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + with pytest.raises(HomeAssistantError, match=error_message): + await hass.services.async_call( + CLIMATE_DOMAIN, + service, + {**service_data, ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) diff --git a/tests/components/switchbot/test_vacuum.py b/tests/components/switchbot/test_vacuum.py index 5cc579db99c2c..34af12e0ea6de 100644 --- a/tests/components/switchbot/test_vacuum.py +++ b/tests/components/switchbot/test_vacuum.py @@ -21,6 +21,7 @@ K11_PLUS_VACUUM_SERVICE_INFO, K20_VACUUM_SERVICE_INFO, S10_VACUUM_SERVICE_INFO, + S20_VACUUM_SERVICE_INFO, ) from tests.common import MockConfigEntry @@ -36,6 +37,7 @@ ("k10_vacuum", K10_VACUUM_SERVICE_INFO), ("k10_pro_vacuum", K10_PRO_VACUUM_SERVICE_INFO), ("k11+_vacuum", K11_PLUS_VACUUM_SERVICE_INFO), + ("s20_vacuum", S20_VACUUM_SERVICE_INFO), ], ) @pytest.mark.parametrize( diff --git a/tests/components/telegram_bot/test_notify.py b/tests/components/telegram_bot/test_notify.py index 969eef568b9d6..98e9549111098 100644 --- a/tests/components/telegram_bot/test_notify.py +++ b/tests/components/telegram_bot/test_notify.py @@ -3,7 +3,7 @@ from datetime import datetime from unittest.mock import AsyncMock, patch -from freezegun.api import freeze_time +import pytest from telegram import Chat, Message from telegram.constants import ChatType, ParseMode @@ -19,7 +19,7 @@ from tests.common import async_capture_events -@freeze_time("2025-01-09T12:00:00+00:00") +@pytest.mark.freeze_time("2025-01-09T12:00:00+00:00") async def test_send_message( hass: HomeAssistant, webhook_platform: None, diff --git a/tests/components/tomorrowio/test_weather.py b/tests/components/tomorrowio/test_weather.py index 4443c65492953..a9dd13be22690 100644 --- a/tests/components/tomorrowio/test_weather.py +++ b/tests/components/tomorrowio/test_weather.py @@ -244,7 +244,7 @@ async def test_v4_weather_legacy_entities(hass: HomeAssistant) -> None: ("service"), [SERVICE_GET_FORECASTS], ) -@freeze_time(datetime(2021, 3, 6, 23, 59, 59, tzinfo=dt_util.UTC)) +@pytest.mark.freeze_time(datetime(2021, 3, 6, 23, 59, 59, tzinfo=dt_util.UTC)) async def test_v4_forecast_service( hass: HomeAssistant, snapshot: SnapshotAssertion, diff --git a/tests/components/tplink/test_climate.py b/tests/components/tplink/test_climate.py index 6d5b498b922d8..4d4e005fc16ef 100644 --- a/tests/components/tplink/test_climate.py +++ b/tests/components/tplink/test_climate.py @@ -225,6 +225,14 @@ async def test_unknown_mode( assert state.attributes[ATTR_HVAC_ACTION] == HVACAction.OFF assert "Unknown thermostat state, defaulting to OFF" in caplog.text + # Second update, make sure the warning is not logged again + caplog.clear() + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30)) + await hass.async_block_till_done() + state = hass.states.get(ENTITY_ID) + assert state.attributes[ATTR_HVAC_ACTION] == HVACAction.OFF + assert "Unknown thermostat state, defaulting to OFF" not in caplog.text + async def test_missing_feature_attributes( hass: HomeAssistant, diff --git a/tests/components/ukraine_alarm/__init__.py b/tests/components/ukraine_alarm/__init__.py index 228594b3d0c76..7a539cc0a6e0e 100644 --- a/tests/components/ukraine_alarm/__init__.py +++ b/tests/components/ukraine_alarm/__init__.py @@ -1 +1,27 @@ """Tests for the Ukraine Alarm integration.""" + + +def _region(rid, recurse=0, depth=0): + """Create a test region with optional nested structure.""" + if depth == 0: + name_prefix = "State" + elif depth == 1: + name_prefix = "District" + else: + name_prefix = "Community" + + name = f"{name_prefix} {rid}" + region = {"regionId": rid, "regionName": name, "regionChildIds": []} + + if not recurse: + return region + + for i in range(1, 4): + region["regionChildIds"].append(_region(f"{rid}.{i}", recurse - 1, depth + 1)) + + return region + + +REGIONS = { + "states": [_region(f"{i}", i - 1) for i in range(1, 4)], +} diff --git a/tests/components/ukraine_alarm/test_config_flow.py b/tests/components/ukraine_alarm/test_config_flow.py index de9bdd618deea..3350a95fd7145 100644 --- a/tests/components/ukraine_alarm/test_config_flow.py +++ b/tests/components/ukraine_alarm/test_config_flow.py @@ -12,32 +12,9 @@ from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from tests.common import MockConfigEntry - - -def _region(rid, recurse=0, depth=0): - if depth == 0: - name_prefix = "State" - elif depth == 1: - name_prefix = "District" - else: - name_prefix = "Community" - - name = f"{name_prefix} {rid}" - region = {"regionId": rid, "regionName": name, "regionChildIds": []} - - if not recurse: - return region +from . import REGIONS - for i in range(1, 4): - region["regionChildIds"].append(_region(f"{rid}.{i}", recurse - 1, depth + 1)) - - return region - - -REGIONS = { - "states": [_region(f"{i}", i - 1) for i in range(1, 4)], -} +from tests.common import MockConfigEntry @pytest.fixture(autouse=True) @@ -51,37 +28,6 @@ def mock_get_regions() -> Generator[AsyncMock]: yield mock_get -async def test_state(hass: HomeAssistant) -> None: - """Test we can create entry for state.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] is FlowResultType.FORM - - result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result2["type"] is FlowResultType.FORM - - with patch( - "homeassistant.components.ukraine_alarm.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result3 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "region": "1", - }, - ) - await hass.async_block_till_done() - - assert result3["type"] is FlowResultType.CREATE_ENTRY - assert result3["title"] == "State 1" - assert result3["data"] == { - "region": "1", - "name": result3["title"], - } - assert len(mock_setup_entry.mock_calls) == 1 - - async def test_state_district(hass: HomeAssistant) -> None: """Test we can create entry for state + district.""" result = await hass.config_entries.flow.async_init( @@ -121,45 +67,6 @@ async def test_state_district(hass: HomeAssistant) -> None: assert len(mock_setup_entry.mock_calls) == 1 -async def test_state_district_pick_region(hass: HomeAssistant) -> None: - """Test we can create entry for region which has districts.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] is FlowResultType.FORM - - result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result2["type"] is FlowResultType.FORM - - result3 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "region": "2", - }, - ) - assert result3["type"] is FlowResultType.FORM - - with patch( - "homeassistant.components.ukraine_alarm.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result4 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "region": "2", - }, - ) - await hass.async_block_till_done() - - assert result4["type"] is FlowResultType.CREATE_ENTRY - assert result4["title"] == "State 2" - assert result4["data"] == { - "region": "2", - "name": result4["title"], - } - assert len(mock_setup_entry.mock_calls) == 1 - - async def test_state_district_community(hass: HomeAssistant) -> None: """Test we can create entry for state + district + community.""" result = await hass.config_entries.flow.async_init( diff --git a/tests/components/ukraine_alarm/test_init.py b/tests/components/ukraine_alarm/test_init.py new file mode 100644 index 0000000000000..f1b762339f35c --- /dev/null +++ b/tests/components/ukraine_alarm/test_init.py @@ -0,0 +1,70 @@ +"""Test the Ukraine Alarm integration initialization.""" + +from unittest.mock import patch + +from homeassistant.components.ukraine_alarm.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir + +from . import REGIONS + +from tests.common import MockConfigEntry + + +async def test_migration_v1_to_v2_state_without_districts( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, +) -> None: + """Test migration allows states without districts.""" + entry = MockConfigEntry( + domain=DOMAIN, + version=1, + data={"region": "1", "name": "State 1"}, + unique_id="1", + ) + entry.add_to_hass(hass) + + with ( + patch( + "homeassistant.components.ukraine_alarm.Client.get_regions", + return_value=REGIONS, + ), + patch( + "homeassistant.components.ukraine_alarm.Client.get_alerts", + return_value=[{"activeAlerts": []}], + ), + ): + result = await hass.config_entries.async_setup(entry.entry_id) + assert result is True + assert entry.version == 2 + + assert ( + DOMAIN, + f"deprecated_state_region_{entry.entry_id}", + ) not in issue_registry.issues + + +async def test_migration_v1_to_v2_state_with_districts_fails( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, +) -> None: + """Test migration rejects states with districts.""" + entry = MockConfigEntry( + domain=DOMAIN, + version=1, + data={"region": "2", "name": "State 2"}, + unique_id="2", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.ukraine_alarm.Client.get_regions", + return_value=REGIONS, + ): + result = await hass.config_entries.async_setup(entry.entry_id) + assert result is False + + assert ( + DOMAIN, + f"deprecated_state_region_{entry.entry_id}", + ) in issue_registry.issues diff --git a/tests/components/utility_meter/test_diagnostics.py b/tests/components/utility_meter/test_diagnostics.py index 88521a91b7fad..d32294bceb0b3 100644 --- a/tests/components/utility_meter/test_diagnostics.py +++ b/tests/components/utility_meter/test_diagnostics.py @@ -1,58 +1,22 @@ """Test Utility Meter diagnostics.""" -from aiohttp.test_utils import TestClient -from freezegun import freeze_time import pytest from syrupy.assertion import SnapshotAssertion from syrupy.filters import props -from homeassistant.auth.models import Credentials from homeassistant.components.utility_meter.const import DOMAIN from homeassistant.components.utility_meter.sensor import ATTR_LAST_RESET from homeassistant.core import HomeAssistant, State -from tests.common import ( - CLIENT_ID, - MockConfigEntry, - MockUser, - mock_restore_cache_with_extra_data, -) +from tests.common import MockConfigEntry, mock_restore_cache_with_extra_data from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator -async def generate_new_hass_access_token( - hass: HomeAssistant, hass_admin_user: MockUser, hass_admin_credential: Credentials -) -> str: - """Return an access token to access Home Assistant.""" - await hass.auth.async_link_user(hass_admin_user, hass_admin_credential) - - refresh_token = await hass.auth.async_create_refresh_token( - hass_admin_user, CLIENT_ID, credential=hass_admin_credential - ) - return hass.auth.async_create_access_token(refresh_token) - - -def _get_test_client_generator( - hass: HomeAssistant, aiohttp_client: ClientSessionGenerator, new_token: str -): - """Return a test client generator."".""" - - async def auth_client() -> TestClient: - return await aiohttp_client( - hass.http.app, headers={"Authorization": f"Bearer {new_token}"} - ) - - return auth_client - - -@freeze_time("2024-04-06 00:00:00+00:00") -@pytest.mark.usefixtures("socket_enabled") +@pytest.mark.freeze_time("2024-04-06 00:00:00+00:00") async def test_diagnostics( hass: HomeAssistant, - aiohttp_client: ClientSessionGenerator, - hass_admin_user: MockUser, - hass_admin_credential: Credentials, + hass_client: ClientSessionGenerator, snapshot: SnapshotAssertion, ) -> None: """Test generating diagnostics for a config entry.""" @@ -130,15 +94,6 @@ async def test_diagnostics( assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - # Since we are freezing time only when we enter this test, we need to - # manually create a new token and clients since the token created by - # the fixtures would not be valid. - new_token = await generate_new_hass_access_token( - hass, hass_admin_user, hass_admin_credential - ) - - diag = await get_diagnostics_for_config_entry( - hass, _get_test_client_generator(hass, aiohttp_client, new_token), config_entry - ) + diag = await get_diagnostics_for_config_entry(hass, hass_client, config_entry) assert diag == snapshot(exclude=props("entry_id", "created_at", "modified_at")) diff --git a/tests/components/velux/test_binary_sensor.py b/tests/components/velux/test_binary_sensor.py index f48a9102dfbbe..3d7745b4d42e6 100644 --- a/tests/components/velux/test_binary_sensor.py +++ b/tests/components/velux/test_binary_sensor.py @@ -5,6 +5,7 @@ from freezegun.api import FrozenDateTimeFactory import pytest +from homeassistant.components.velux import DOMAIN from homeassistant.const import STATE_OFF, STATE_ON, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceRegistry @@ -95,3 +96,11 @@ async def test_rain_sensor_device_association( # Verify device has correct identifiers assert ("velux", mock_window.serial_number) in device_entry.identifiers assert device_entry.name == mock_window.name + + # Verify via_device is gateway + assert device_entry.via_device_id is not None + via_device_entry = device_registry.async_get(device_entry.via_device_id) + assert via_device_entry is not None + assert via_device_entry.identifiers == { + (DOMAIN, f"gateway_{mock_config_entry.entry_id}") + } diff --git a/tests/components/wake_word/test_init.py b/tests/components/wake_word/test_init.py index 402793be92677..316cafd05a840 100644 --- a/tests/components/wake_word/test_init.py +++ b/tests/components/wake_word/test_init.py @@ -6,7 +6,6 @@ from pathlib import Path from unittest.mock import patch -from freezegun import freeze_time import pytest from homeassistant.components import wake_word @@ -170,7 +169,7 @@ async def test_config_entry_unload( assert config_entry.state is ConfigEntryState.NOT_LOADED -@freeze_time("2023-06-22 10:30:00+00:00") +@pytest.mark.freeze_time("2023-06-22 10:30:00+00:00") @pytest.mark.parametrize( ("wake_word_id", "expected_ww", "expected_phrase"), [ diff --git a/tests/components/xbox/conftest.py b/tests/components/xbox/conftest.py index cc49b4b9709af..b4d210d2b2604 100644 --- a/tests/components/xbox/conftest.py +++ b/tests/components/xbox/conftest.py @@ -6,7 +6,9 @@ import pytest from pythonxbox.api.provider.catalog.models import CatalogResponse +from pythonxbox.api.provider.gameclips.models import GameclipsResponse from pythonxbox.api.provider.people.models import PeopleResponse +from pythonxbox.api.provider.screenshots.models import ScreenshotResponse from pythonxbox.api.provider.smartglass.models import ( SmartglassConsoleList, SmartglassConsoleStatus, @@ -125,6 +127,33 @@ def mock_xbox_live_client() -> Generator[AsyncMock]: client.titlehub.get_title_info.return_value = TitleHubResponse( **load_json_object_fixture("titlehub_titleinfo.json", DOMAIN) ) + client.titlehub.get_title_history.return_value = TitleHubResponse( + **load_json_object_fixture("titlehub_titlehistory.json", DOMAIN) + ) + client.gameclips = AsyncMock() + client.gameclips.get_recent_clips_by_xuid.return_value = GameclipsResponse( + **load_json_object_fixture("gameclips_recent_xuid.json", DOMAIN) + ) + client.gameclips.get_recent_community_clips_by_title_id.return_value = ( + GameclipsResponse( + **load_json_object_fixture( + "gameclips_community_recent_xuid.json", DOMAIN + ) + ) + ) + client.screenshots = AsyncMock() + client.screenshots.get_recent_screenshots_by_xuid.return_value = ( + ScreenshotResponse( + **load_json_object_fixture("screenshots_recent_xuid.json", DOMAIN) + ) + ) + client.screenshots.get_recent_community_screenshots_by_title_id.return_value = ( + ScreenshotResponse( + **load_json_object_fixture( + "screenshots_community_recent_xuid.json", DOMAIN + ) + ) + ) client.xuid = "271958441785640" diff --git a/tests/components/xbox/fixtures/gameclips_community_recent_xuid.json b/tests/components/xbox/fixtures/gameclips_community_recent_xuid.json new file mode 100644 index 0000000000000..b67745676a776 --- /dev/null +++ b/tests/components/xbox/fixtures/gameclips_community_recent_xuid.json @@ -0,0 +1,306 @@ +{ + "gameClips": [ + { + "gameClipId": "0829f20b-604b-483e-89ff-d37d1071cd70", + "state": 6, + "datePublished": "2025-11-06T10:21:42.965555Z", + "dateRecorded": "2025-11-06T09:20:31Z", + "lastModified": "2025-11-06T10:21:42.965555Z", + "userCaption": "", + "type": 8, + "durationInSeconds": 241, + "scid": "d1adc8aa-0a31-4407-90f2-7e9b54b0347c", + "titleId": 1297287135, + "rating": 0.0, + "ratingCount": 0, + "views": 0, + "titleData": "", + "systemProperties": "bcf6f66d-0663-44d0-bc5f-08e6d478e2410;", + "savedByUser": true, + "achievementId": "", + "greatestMomentId": "", + "thumbnails": [ + { + "uri": "https://gameclipscontent-t3011.media.xboxlive.com/xuid-2535458333395495-public/0829f20b-604b-483e-89ff-d37d1071cd70_Thumbnail.PNG", + "fileSize": 0, + "thumbnailType": 1 + }, + { + "uri": "https://gameclipscontent-t3011.media.xboxlive.com/xuid-2535458333395495-public/0829f20b-604b-483e-89ff-d37d1071cd70_Thumbnail.PNG", + "fileSize": 0, + "thumbnailType": 2 + } + ], + "gameClipUris": [ + { + "uri": "https://gameclipscontent-d3011.media.xboxlive.com/xuid-2535458333395495-private/0829f20b-604b-483e-89ff-d37d1071cd70.MP4?skoid=2938738c-0e58-4f21-9b82-98081ade42e2&sktid=68cd85cc-e0b3-43c8-ba3c-67686dbf8a67&skt=2025-11-06T08%3A21%3A55Z&ske=2025-11-07T08%3A21%3A55Z&sks=b&skv=2025-05-05&sv=2025-05-05&st=2025-11-06T10%3A06%3A42Z&se=2125-11-06T10%3A21%3A42Z&sr=b&sp=r&sig=RIGvLFCUB5u4%2F2T8XZiCbpf2QNuUkbwyAQPv2NVGCoY%3D&__gda__=1762438393_b3aeadeb04f94410cc7223369a9052b6", + "fileSize": 247412686, + "uriType": 2, + "expiration": "2025-11-06T14:13:13.4974877Z" + } + ], + "xuid": "2535458333395495", + "clipName": "", + "titleName": "Blue Dragon", + "gameClipLocale": "de-DE", + "clipContentAttributes": 0, + "deviceType": "Durango", + "commentCount": 0, + "likeCount": 0, + "shareCount": 0, + "partialViews": 0 + }, + { + "gameClipId": "6fa2731a-8b58-4aa6-848c-4bf15734358b", + "state": 6, + "datePublished": "2025-11-06T10:20:41.3010531Z", + "dateRecorded": "2025-11-06T09:13:23Z", + "lastModified": "2025-11-06T10:20:41.3010531Z", + "userCaption": "", + "type": 8, + "durationInSeconds": 240, + "scid": "d1adc8aa-0a31-4407-90f2-7e9b54b0347c", + "titleId": 1297287135, + "rating": 0.0, + "ratingCount": 0, + "views": 0, + "titleData": "", + "systemProperties": "d3767e1c-5a0c-4155-a503-e5355eded9850;", + "savedByUser": true, + "achievementId": "", + "greatestMomentId": "", + "thumbnails": [ + { + "uri": "https://gameclipscontent-t3021.media.xboxlive.com/xuid-2535458333395495-public/6fa2731a-8b58-4aa6-848c-4bf15734358b_Thumbnail.PNG", + "fileSize": 0, + "thumbnailType": 1 + }, + { + "uri": "https://gameclipscontent-t3021.media.xboxlive.com/xuid-2535458333395495-public/6fa2731a-8b58-4aa6-848c-4bf15734358b_Thumbnail.PNG", + "fileSize": 0, + "thumbnailType": 2 + } + ], + "gameClipUris": [ + { + "uri": "https://gameclipscontent-d3021.media.xboxlive.com/xuid-2535458333395495-private/6fa2731a-8b58-4aa6-848c-4bf15734358b.MP4?skoid=2938738c-0e58-4f21-9b82-98081ade42e2&sktid=68cd85cc-e0b3-43c8-ba3c-67686dbf8a67&skt=2025-11-06T08%3A20%3A51Z&ske=2025-11-07T08%3A20%3A51Z&sks=b&skv=2025-05-05&sv=2025-05-05&st=2025-11-06T10%3A05%3A41Z&se=2125-11-06T10%3A20%3A41Z&sr=b&sp=r&sig=s%2FWDtmE2cnAwl9iJJFcch3knbRlkxkALoinHQwCnNP0%3D&__gda__=1762438393_eb8a56c3f482d00099045aa892a2aa05", + "fileSize": 222153409, + "uriType": 2, + "expiration": "2025-11-06T14:13:13.4974877Z" + } + ], + "xuid": "2535458333395495", + "clipName": "", + "titleName": "Blue Dragon", + "gameClipLocale": "de-DE", + "clipContentAttributes": 0, + "deviceType": "Durango", + "commentCount": 0, + "likeCount": 0, + "shareCount": 0, + "partialViews": 0 + }, + { + "gameClipId": "37956427-4118-4b3e-aecb-f3f96ef34e6e", + "state": 6, + "datePublished": "2025-11-06T10:20:07.3264687Z", + "dateRecorded": "2025-11-06T09:40:34Z", + "lastModified": "2025-11-06T10:20:07.3264687Z", + "userCaption": "", + "type": 8, + "durationInSeconds": 235, + "scid": "d1adc8aa-0a31-4407-90f2-7e9b54b0347c", + "titleId": 1297287135, + "rating": 0.0, + "ratingCount": 0, + "views": 0, + "titleData": "", + "systemProperties": "581c6615-64c7-4c5c-a2ae-693724a590ea0;", + "savedByUser": true, + "achievementId": "", + "greatestMomentId": "", + "thumbnails": [ + { + "uri": "https://gameclipscontent-t2014.media.xboxlive.com/xuid-2535458333395495-public/37956427-4118-4b3e-aecb-f3f96ef34e6e_Thumbnail.PNG", + "fileSize": 0, + "thumbnailType": 1 + }, + { + "uri": "https://gameclipscontent-t2014.media.xboxlive.com/xuid-2535458333395495-public/37956427-4118-4b3e-aecb-f3f96ef34e6e_Thumbnail.PNG", + "fileSize": 0, + "thumbnailType": 2 + } + ], + "gameClipUris": [ + { + "uri": "https://gameclipscontent-d2014.media.xboxlive.com/xuid-2535458333395495-private/37956427-4118-4b3e-aecb-f3f96ef34e6e.MP4?skoid=2938738c-0e58-4f21-9b82-98081ade42e2&sktid=68cd85cc-e0b3-43c8-ba3c-67686dbf8a67&skt=2025-11-06T08%3A23%3A54Z&ske=2025-11-07T08%3A23%3A54Z&sks=b&skv=2025-05-05&sv=2025-05-05&st=2025-11-06T10%3A05%3A07Z&se=2125-11-06T10%3A20%3A07Z&sr=b&sp=r&sig=C262odufd8%2FCLr2rfnLii3BkrvfdMDq6fc7DhugurNo%3D&__gda__=1762438393_c6e390b226209b3dc8c230619b21c1e2", + "fileSize": 132755687, + "uriType": 2, + "expiration": "2025-11-06T14:13:13.4974877Z" + } + ], + "xuid": "2535458333395495", + "clipName": "", + "titleName": "Blue Dragon", + "gameClipLocale": "de-DE", + "clipContentAttributes": 0, + "deviceType": "Durango", + "commentCount": 0, + "likeCount": 0, + "shareCount": 0, + "partialViews": 0 + }, + { + "gameClipId": "6aecd078-be33-4d52-853a-a78c0c678b45", + "state": 6, + "datePublished": "2025-11-06T10:14:51.5814436Z", + "dateRecorded": "2025-11-06T09:09:04Z", + "lastModified": "2025-11-06T10:14:51.5814436Z", + "userCaption": "", + "type": 8, + "durationInSeconds": 236, + "scid": "d1adc8aa-0a31-4407-90f2-7e9b54b0347c", + "titleId": 1297287135, + "rating": 0.0, + "ratingCount": 0, + "views": 1, + "titleData": "", + "systemProperties": "82683d6d-a589-4823-8d53-914495c1ed1f0;", + "savedByUser": true, + "achievementId": "", + "greatestMomentId": "", + "thumbnails": [ + { + "uri": "https://gameclipscontent-t3020.media.xboxlive.com/xuid-2535458333395495-public/6aecd078-be33-4d52-853a-a78c0c678b45_Thumbnail.PNG", + "fileSize": 0, + "thumbnailType": 1 + }, + { + "uri": "https://gameclipscontent-t3020.media.xboxlive.com/xuid-2535458333395495-public/6aecd078-be33-4d52-853a-a78c0c678b45_Thumbnail.PNG", + "fileSize": 0, + "thumbnailType": 2 + } + ], + "gameClipUris": [ + { + "uri": "https://gameclipscontent-d3020.media.xboxlive.com/xuid-2535458333395495-private/6aecd078-be33-4d52-853a-a78c0c678b45.MP4?skoid=2938738c-0e58-4f21-9b82-98081ade42e2&sktid=68cd85cc-e0b3-43c8-ba3c-67686dbf8a67&skt=2025-11-06T08%3A03%3A12Z&ske=2025-11-07T08%3A03%3A12Z&sks=b&skv=2025-05-05&sv=2025-05-05&st=2025-11-06T09%3A59%3A51Z&se=2125-11-06T10%3A14%3A51Z&sr=b&sp=r&sig=%2B5f%2FWXL4GrazXoMnf%2FkYr9cFKVLhjKaO3Xqgtfbyynk%3D&__gda__=1762438393_75d2d2fa2d78c3b09efc7ad984d32652", + "fileSize": 224659316, + "uriType": 2, + "expiration": "2025-11-06T14:13:13.4974877Z" + } + ], + "xuid": "2535458333395495", + "clipName": "", + "titleName": "Blue Dragon", + "gameClipLocale": "de-DE", + "clipContentAttributes": 0, + "deviceType": "Durango", + "commentCount": 0, + "likeCount": 0, + "shareCount": 0, + "partialViews": 0 + }, + { + "gameClipId": "84bdc7a5-fa53-4348-9502-4cd804906f3b", + "state": 6, + "datePublished": "2025-11-06T10:13:31.2003126Z", + "dateRecorded": "2025-11-06T09:04:54Z", + "lastModified": "2025-11-06T10:13:31.2003126Z", + "userCaption": "", + "type": 8, + "durationInSeconds": 241, + "scid": "d1adc8aa-0a31-4407-90f2-7e9b54b0347c", + "titleId": 1297287135, + "rating": 0.0, + "ratingCount": 0, + "views": 1, + "titleData": "", + "systemProperties": "74a47fb9-18b9-4f7b-9396-2b35a22e56940;", + "savedByUser": true, + "achievementId": "", + "greatestMomentId": "", + "thumbnails": [ + { + "uri": "https://gameclipscontent-t3019.media.xboxlive.com/xuid-2535458333395495-public/84bdc7a5-fa53-4348-9502-4cd804906f3b_Thumbnail.PNG", + "fileSize": 0, + "thumbnailType": 1 + }, + { + "uri": "https://gameclipscontent-t3019.media.xboxlive.com/xuid-2535458333395495-public/84bdc7a5-fa53-4348-9502-4cd804906f3b_Thumbnail.PNG", + "fileSize": 0, + "thumbnailType": 2 + } + ], + "gameClipUris": [ + { + "uri": "https://gameclipscontent-d3019.media.xboxlive.com/xuid-2535458333395495-private/84bdc7a5-fa53-4348-9502-4cd804906f3b.MP4?skoid=2938738c-0e58-4f21-9b82-98081ade42e2&sktid=68cd85cc-e0b3-43c8-ba3c-67686dbf8a67&skt=2025-11-06T08%3A15%3A51Z&ske=2025-11-07T08%3A15%3A51Z&sks=b&skv=2025-05-05&sv=2025-05-05&st=2025-11-06T09%3A58%3A31Z&se=2125-11-06T10%3A13%3A31Z&sr=b&sp=r&sig=ApVvSe1MvO6oNDZ7hXOfULno0ppsWZfYKLT1uvK33kM%3D&__gda__=1762438393_7ef9df98bb4d508c0f685c8a53d020f2", + "fileSize": 192633589, + "uriType": 2, + "expiration": "2025-11-06T14:13:13.4974877Z" + } + ], + "xuid": "2535458333395495", + "clipName": "", + "titleName": "Blue Dragon", + "gameClipLocale": "de-DE", + "clipContentAttributes": 0, + "deviceType": "Durango", + "commentCount": 0, + "likeCount": 0, + "shareCount": 0, + "partialViews": 0 + }, + + { + "gameClipId": "c06597b0-28ce-4d2f-856d-b7cde9a40f4a", + "state": 6, + "datePublished": "2025-10-22T05:45:10.0466564Z", + "dateRecorded": "2025-10-22T05:34:33Z", + "lastModified": "2025-10-22T05:45:10.0466564Z", + "userCaption": "", + "type": 8, + "durationInSeconds": 118, + "scid": "d1adc8aa-0a31-4407-90f2-7e9b54b0347c", + "titleId": 1297287135, + "rating": 0.0, + "ratingCount": 0, + "views": 1, + "titleData": "", + "systemProperties": "9fdf1bc5-e66b-4712-b826-9f7bab0d40550;", + "savedByUser": true, + "achievementId": "", + "greatestMomentId": "", + "thumbnails": [ + { + "uri": "https://gameclipscontent-t2002.media.xboxlive.com/xuid-2535451917097448-public/c06597b0-28ce-4d2f-856d-b7cde9a40f4a_Thumbnail.PNG", + "fileSize": 0, + "thumbnailType": 1 + }, + { + "uri": "https://gameclipscontent-t2002.media.xboxlive.com/xuid-2535451917097448-public/c06597b0-28ce-4d2f-856d-b7cde9a40f4a_Thumbnail.PNG", + "fileSize": 0, + "thumbnailType": 2 + } + ], + "gameClipUris": [ + { + "uri": "https://gameclipscontent-d2002.media.xboxlive.com/xuid-2535451917097448-private/c06597b0-28ce-4d2f-856d-b7cde9a40f4a.MP4?sv=2024-08-04&si=DefaultAccess&sr=b&sig=jFfGvWMCJokdjj76LbDeX%2BlIb0P6isx6YmwT8epJoco%3D&__gda__=1762438393_721759b6969e54700792a9b4e5998408", + "fileSize": 71018911, + "uriType": 2, + "expiration": "2025-11-06T14:13:13.4974877Z" + } + ], + "xuid": "2535451917097448", + "clipName": "", + "titleName": "Blue Dragon", + "gameClipLocale": "en-GB", + "clipContentAttributes": 0, + "deviceType": "Edmonton", + "commentCount": 0, + "likeCount": 0, + "shareCount": 0, + "partialViews": 0 + } + ], + "pagingInfo": { "continuationToken": "abcde_vwxyzZAAAAA2" } +} diff --git a/tests/components/xbox/fixtures/gameclips_recent_xuid.json b/tests/components/xbox/fixtures/gameclips_recent_xuid.json new file mode 100644 index 0000000000000..a54eea2851380 --- /dev/null +++ b/tests/components/xbox/fixtures/gameclips_recent_xuid.json @@ -0,0 +1,107 @@ +{ + "gameClips": [ + { + "gameClipId": "44b7e94d-b3e9-4b97-be73-417be9091e93", + "state": 6, + "datePublished": "2017-11-10T07:12:27.4353527Z", + "dateRecorded": "2017-11-10T06:29:39Z", + "lastModified": "2017-11-10T07:12:27.4353527Z", + "userCaption": "", + "type": 8, + "durationInSeconds": 28, + "scid": "d1adc8aa-0a31-4407-90f2-7e9b54b0347c", + "titleId": 822567888, + "rating": 0.0, + "ratingCount": 0, + "views": 102, + "titleData": "", + "systemProperties": "6a7e1238-2f7b-4c66-835e-c7c799e310490;", + "savedByUser": true, + "achievementId": "", + "greatestMomentId": "", + "thumbnails": [ + { + "uri": "https://gameclipscontent-t3017.xboxlive.com/xuid-2669321029139235-public/44b7e94d-b3e9-4b97-be73-417be9091e93_Thumbnail.PNG", + "fileSize": 0, + "thumbnailType": 1 + }, + { + "uri": "https://gameclipscontent-t3017.xboxlive.com/xuid-2669321029139235-public/44b7e94d-b3e9-4b97-be73-417be9091e93_Thumbnail.PNG", + "fileSize": 0, + "thumbnailType": 2 + } + ], + "gameClipUris": [ + { + "uri": "https://gameclipscontent-d3017.xboxlive.com/xuid-2669321029139235-private/44b7e94d-b3e9-4b97-be73-417be9091e93.MP4?sv=2015-12-11&sr=b&si=DefaultAccess&sig=lRkjf6USrukD5PCVp3PonFdwX5fD%2FmuLPWpRkIS2ydw%3D&__gda__=1522241341_d651111247d5ca9c1bb8b9244a8cd361", + "fileSize": 43595475, + "uriType": 2, + "expiration": "2018-03-28T12:49:01.5001828Z" + } + ], + "xuid": "2669321029139235", + "clipName": "", + "titleName": "Danger Zone", + "gameClipLocale": "en-US", + "clipContentAttributes": 0, + "deviceType": "Scorpio", + "commentCount": 0, + "likeCount": 2, + "shareCount": 0, + "partialViews": 0 + }, + { + "gameClipId": "f87cc6ac-c291-4998-9124-d8b36c059b6a", + "state": 6, + "datePublished": "2016-08-16T05:51:40.1354298Z", + "dateRecorded": "2016-08-16T05:40:30Z", + "lastModified": "2016-08-16T05:51:40.1354298Z", + "userCaption": "", + "type": 8, + "durationInSeconds": 27, + "scid": "f9d30100-7748-4d1a-b508-524805a3c308", + "titleId": 94618376, + "rating": 0.0, + "ratingCount": 0, + "views": 653, + "titleData": "", + "systemProperties": "56d4ac4e-efcd-4808-8d53-7583859558a4;", + "savedByUser": true, + "achievementId": "", + "greatestMomentId": "", + "thumbnails": [ + { + "uri": "https://gameclipscontent-t2015.xboxlive.com/00097bbbbbbbbb23-f87cc6ac-c291-4998-9124-d8b36c059b6a-public/Thumbnail_Small.PNG?sv=2015-12-11&sr=b&si=DefaultAccess&sig=ikXCzETBJSBPvS8W%2FxoWTCJL%2FGY6PxKpQ0VG6qktnCQ%3D", + "fileSize": 159667, + "thumbnailType": 1 + }, + { + "uri": "https://gameclipscontent-t2015.xboxlive.com/00097bbbbbbbbb23-f87cc6ac-c291-4998-9124-d8b36c059b6a-public/Thumbnail_Large.PNG?sv=2015-12-11&sr=b&si=DefaultAccess&sig=y1S%2BxzFVrgmTql2ZuYDpe6EtL5Jxu%2BhBVMnb%2Ff7ZTMA%3D", + "fileSize": 522262, + "thumbnailType": 2 + } + ], + "gameClipUris": [ + { + "uri": "https://gameclipscontent-d2015.xboxlive.com/asset-d5448dbd-f45e-46ab-ae4e-a2e205a70e7c/GameClip-Original.MP4?sv=2015-12-11&sr=b&si=DefaultAccess&sig=ArSoLvy9EnQeBthGW6%2FbasedHHk0Jb6iXjI3EMq8oh8%3D&__gda__=1522241341_69f67a7a3533626ae90b52845664dc0c", + "fileSize": 12662239, + "uriType": 2, + "expiration": "2018-03-28T12:49:01.5001828Z" + } + ], + "xuid": "2669321029139235", + "clipName": "", + "titleName": "Overwatch: Origins Edition", + "gameClipLocale": "en-US", + "clipContentAttributes": 0, + "deviceType": "XboxOne", + "commentCount": 5, + "likeCount": 62, + "shareCount": 2, + "partialViews": 0 + } + ], + "pagingInfo": { + "continuationToken": "abcde_vwxyzGQAAAA2" + } +} diff --git a/tests/components/xbox/fixtures/people_batch.json b/tests/components/xbox/fixtures/people_batch.json index 295922ed9c8ac..b1565f134b984 100644 --- a/tests/components/xbox/fixtures/people_batch.json +++ b/tests/components/xbox/fixtures/people_batch.json @@ -33,8 +33,21 @@ "search": null, "titleHistory": null, "multiplayerSummary": { - "inMultiplayerSession": 0, - "inParty": 0 + "joinableActivities": [], + "partyDetails": [ + { + "sessionRef": { + "scid": "7ffcfe63-a320-4086-85bb-a1734bef7a30", + "templateName": "chat", + "name": "d3804c40-ac93-4c7d-9414-6cb419931105" + }, + "status": "active", + "visibility": "open", + "joinRestriction": "followed", + "accepted": 2 + } + ], + "inParty": 2 }, "recentPlayer": null, "follower": null, diff --git a/tests/components/xbox/fixtures/people_batch2.json b/tests/components/xbox/fixtures/people_batch2.json new file mode 100644 index 0000000000000..c82aa0e4352d6 --- /dev/null +++ b/tests/components/xbox/fixtures/people_batch2.json @@ -0,0 +1,99 @@ +{ + "people": [ + { + "xuid": "277923030577271", + "isFavorite": false, + "isFollowingCaller": true, + "isFollowedByCaller": true, + "isIdentityShared": false, + "addedDateTimeUtc": null, + "displayName": null, + "realName": "Test Test", + "displayPicRaw": "https://images-eds-ssl.xboxlive.com/image?url=z951ykn43p4FqWbbFvR2Ec.8vbDhj8G2Xe7JngaTToBrrCmIEEXHC9UNrdJ6P7KIwuPiuIs6TLDV4WsQAGzSwgbaLlf_CiIdb0jJCiBao9CB40sjycuA5l0Jf.hfuP8l&format=png", + "showUserAsAvatar": "2", + "gamertag": "Iqnavs", + "gamerScore": "48336", + "modernGamertag": "Iqnavs", + "modernGamertagSuffix": "", + "uniqueModernGamertag": "Iqnavs", + "xboxOneRep": "GoodPlayer", + "presenceState": "Online", + "presenceText": "Online", + "presenceDevices": null, + "isFriend": true, + "isFriendRequestReceived": false, + "isFriendRequestSent": false, + "isBroadcasting": false, + "isCloaked": null, + "isQuarantined": false, + "isXbox360Gamerpic": false, + "lastSeenDateTimeUtc": null, + "suggestion": null, + "recommendation": null, + "search": null, + "titleHistory": null, + "multiplayerSummary": { + "inMultiplayerSession": 0, + "inParty": 0 + }, + "recentPlayer": null, + "follower": null, + "preferredColor": { + "primaryColor": "193e91", + "secondaryColor": "101836", + "tertiaryColor": "102c69" + }, + "presenceDetails": [ + { + "IsBroadcasting": false, + "Device": "WindowsOneCore", + "PresenceText": "Online", + "State": "Active", + "TitleId": "1022622766", + "TitleType": null, + "IsPrimary": true, + "IsGame": false, + "RichPresenceText": null + } + ], + "titlePresence": null, + "titleSummaries": null, + "presenceTitleIds": null, + "detail": { + "accountTier": "Gold", + "bio": null, + "isVerified": false, + "location": null, + "tenure": null, + "watermarks": [], + "blocked": false, + "mute": false, + "followerCount": 78, + "followingCount": 83, + "hasGamePass": false, + "isFriend": true, + "canBeFriended": true, + "canBeFollowed": true, + "friendCount": 150, + "isFriendRequestReceived": false, + "isFriendRequestSent": false, + "isFriendListShared": true, + "isFollowingCaller": false, + "isFollowedByCaller": false, + "isFavorite": true + }, + "communityManagerTitles": null, + "socialManager": null, + "broadcast": null, + "tournamentSummary": null, + "avatar": null, + "linkedAccounts": [], + "colorTheme": "gamerpicblur", + "preferredFlag": "", + "preferredPlatforms": [] + } + ], + "recommendationSummary": null, + "friendFinderState": null, + "accountLinkDetails": null +} diff --git a/tests/components/xbox/fixtures/screenshots_community_recent_xuid.json b/tests/components/xbox/fixtures/screenshots_community_recent_xuid.json new file mode 100644 index 0000000000000..20436dd3890a5 --- /dev/null +++ b/tests/components/xbox/fixtures/screenshots_community_recent_xuid.json @@ -0,0 +1,334 @@ +{ + "screenshots": [ + { + "screenshotId": "5eaea123-0111-4682-9e86-34737b09d82f", + "resolutionHeight": 1080, + "resolutionWidth": 1920, + "state": 6, + "datePublished": "2025-11-06T11:20:51.1249885Z", + "dateTaken": "2025-11-06T11:20:28Z", + "lastModified": "2025-11-06T11:20:28Z", + "userCaption": "", + "type": 8, + "scid": "d1adc8aa-0a31-4407-90f2-7e9b54b0347c", + "titleId": 1297287135, + "rating": 0.0, + "ratingCount": 0, + "views": 0, + "titleData": "", + "systemProperties": "8137cdc8-e1ce-467a-81c6-c54843da69fe0;", + "savedByUser": true, + "achievementId": "", + "greatestMomentId": null, + "thumbnails": [ + { + "uri": "https://screenshotscontent-t2001.media.xboxlive.com/xuid-2535422966774043-public/5eaea123-0111-4682-9e86-34737b09d82f_Thumbnail.PNG", + "fileSize": 0, + "thumbnailType": 1 + }, + { + "uri": "https://screenshotscontent-t2001.media.xboxlive.com/xuid-2535422966774043-public/5eaea123-0111-4682-9e86-34737b09d82f_Thumbnail.PNG", + "fileSize": 0, + "thumbnailType": 2 + } + ], + "screenshotUris": [ + { + "uri": "https://screenshotscontent-d2001.media.xboxlive.com/xuid-2535422966774043-private/5eaea123-0111-4682-9e86-34737b09d82f.PNG?skoid=296fcea0-0bf0-4a22-abf7-16b3524eba1b&sktid=68cd85cc-e0b3-43c8-ba3c-67686dbf8a67&skt=2025-11-06T10%3A14%3A57Z&ske=2025-11-07T10%3A14%3A57Z&sks=b&skv=2025-05-05&sv=2025-05-05&st=2025-11-06T11%3A05%3A51Z&se=2125-11-06T11%3A20%3A51Z&sr=b&sp=r&sig=gT9Pv1EIw1PS7G8wpzDvlqAPciTyhYc5W1tsnh6lfss%3D", + "fileSize": 2349225, + "uriType": 2, + "expiration": "2025-11-06T14:04:08.7266397Z" + } + ], + "xuid": "2535422966774043", + "screenshotName": "", + "titleName": "Blue Dragon", + "screenshotLocale": "en-GB", + "screenshotContentAttributes": 0, + "deviceType": "Durango" + }, + { + "screenshotId": "504a78e5-be24-4020-a245-77cb528e91ea", + "resolutionHeight": 1080, + "resolutionWidth": 1920, + "state": 6, + "datePublished": "2025-11-06T10:50:59.7540201Z", + "dateTaken": "2025-11-06T10:50:51Z", + "lastModified": "2025-11-06T10:50:51Z", + "userCaption": "", + "type": 8, + "scid": "d1adc8aa-0a31-4407-90f2-7e9b54b0347c", + "titleId": 1297287135, + "rating": 0.0, + "ratingCount": 0, + "views": 0, + "titleData": "", + "systemProperties": "c610cd37-1f5f-4bc6-ab0b-9b8c47583a250;", + "savedByUser": true, + "achievementId": "", + "greatestMomentId": null, + "thumbnails": [ + { + "uri": "https://screenshotscontent-t5002.media.xboxlive.com/xuid-2535422966774043-public/504a78e5-be24-4020-a245-77cb528e91ea_Thumbnail.PNG", + "fileSize": 0, + "thumbnailType": 1 + }, + { + "uri": "https://screenshotscontent-t5002.media.xboxlive.com/xuid-2535422966774043-public/504a78e5-be24-4020-a245-77cb528e91ea_Thumbnail.PNG", + "fileSize": 0, + "thumbnailType": 2 + } + ], + "screenshotUris": [ + { + "uri": "https://screenshotscontent-d5002.media.xboxlive.com/xuid-2535422966774043-private/504a78e5-be24-4020-a245-77cb528e91ea.PNG?skoid=296fcea0-0bf0-4a22-abf7-16b3524eba1b&sktid=68cd85cc-e0b3-43c8-ba3c-67686dbf8a67&skt=2025-11-06T10%3A21%3A06Z&ske=2025-11-07T10%3A21%3A06Z&sks=b&skv=2025-05-05&sv=2025-05-05&st=2025-11-06T10%3A35%3A59Z&se=2125-11-06T10%3A50%3A59Z&sr=b&sp=r&sig=TqUUNeuAzHawaXBTFfSVuUzuXbGOMgrDu0Q2VBTFd5U%3D", + "fileSize": 54462, + "uriType": 2, + "expiration": "2025-11-06T14:04:08.7266419Z" + } + ], + "xuid": "2535422966774043", + "screenshotName": "", + "titleName": "Blue Dragon", + "screenshotLocale": "en-GB", + "screenshotContentAttributes": 0, + "deviceType": "Durango" + }, + { + "screenshotId": "fc4cf7c4-edfc-4e7b-ba8f-d7e97dff8ea8", + "resolutionHeight": 1080, + "resolutionWidth": 1920, + "state": 6, + "datePublished": "2025-11-06T05:37:55.1207147Z", + "dateTaken": "2025-11-06T05:37:40Z", + "lastModified": "2025-11-06T05:37:40Z", + "userCaption": "", + "type": 8, + "scid": "d1adc8aa-0a31-4407-90f2-7e9b54b0347c", + "titleId": 1297287135, + "rating": 0.0, + "ratingCount": 0, + "views": 0, + "titleData": "", + "systemProperties": "f6bf16b6-bf2b-4403-914c-fec54131d29d0;", + "savedByUser": true, + "achievementId": "", + "greatestMomentId": null, + "thumbnails": [ + { + "uri": "https://screenshotscontent-t5001.media.xboxlive.com/xuid-2535459412956058-public/fc4cf7c4-edfc-4e7b-ba8f-d7e97dff8ea8_Thumbnail.PNG", + "fileSize": 0, + "thumbnailType": 1 + }, + { + "uri": "https://screenshotscontent-t5001.media.xboxlive.com/xuid-2535459412956058-public/fc4cf7c4-edfc-4e7b-ba8f-d7e97dff8ea8_Thumbnail.PNG", + "fileSize": 0, + "thumbnailType": 2 + } + ], + "screenshotUris": [ + { + "uri": "https://screenshotscontent-d5001.media.xboxlive.com/xuid-2535459412956058-private/fc4cf7c4-edfc-4e7b-ba8f-d7e97dff8ea8.PNG?skoid=296fcea0-0bf0-4a22-abf7-16b3524eba1b&sktid=68cd85cc-e0b3-43c8-ba3c-67686dbf8a67&skt=2025-11-06T04%3A15%3A01Z&ske=2025-11-07T04%3A15%3A01Z&sks=b&skv=2025-05-05&sv=2025-05-05&st=2025-11-06T05%3A22%3A55Z&se=2125-11-06T05%3A37%3A55Z&sr=b&sp=r&sig=2p%2FE%2BdQmb4%2BUuWpUjDPlE5en5ls6x6F%2BvxODH210kmA%3D", + "fileSize": 1302220, + "uriType": 2, + "expiration": "2025-11-06T14:04:08.7266431Z" + } + ], + "xuid": "2535459412956058", + "screenshotName": "", + "titleName": "Blue Dragon", + "screenshotLocale": "en-GB", + "screenshotContentAttributes": 0, + "deviceType": "Durango" + }, + { + "screenshotId": "6a719527-a141-4586-8d44-26fec3f1b693", + "resolutionHeight": 720, + "resolutionWidth": 1280, + "state": 6, + "datePublished": "2025-11-06T03:35:14.1020875Z", + "dateTaken": "2025-11-06T03:34:23Z", + "lastModified": "2025-11-06T03:34:23Z", + "userCaption": "", + "type": 8, + "scid": "d1adc8aa-0a31-4407-90f2-7e9b54b0347c", + "titleId": 1297287135, + "rating": 0.0, + "ratingCount": 0, + "views": 0, + "titleData": "", + "systemProperties": "3e259c89-4c16-47be-bc8f-ad88d31462860;", + "savedByUser": true, + "achievementId": "", + "greatestMomentId": null, + "thumbnails": [ + { + "uri": "https://screenshotscontent-t4002.media.xboxlive.com/xuid-2535451380716241-public/6a719527-a141-4586-8d44-26fec3f1b693_Thumbnail.PNG", + "fileSize": 0, + "thumbnailType": 1 + }, + { + "uri": "https://screenshotscontent-t4002.media.xboxlive.com/xuid-2535451380716241-public/6a719527-a141-4586-8d44-26fec3f1b693_Thumbnail.PNG", + "fileSize": 0, + "thumbnailType": 2 + } + ], + "screenshotUris": [ + { + "uri": "https://screenshotscontent-d4002.media.xboxlive.com/xuid-2535451380716241-private/6a719527-a141-4586-8d44-26fec3f1b693.PNG?skoid=296fcea0-0bf0-4a22-abf7-16b3524eba1b&sktid=68cd85cc-e0b3-43c8-ba3c-67686dbf8a67&skt=2025-11-06T02%3A17%3A22Z&ske=2025-11-07T02%3A17%3A22Z&sks=b&skv=2025-05-05&sv=2025-05-05&st=2025-11-06T03%3A20%3A14Z&se=2125-11-06T03%3A35%3A14Z&sr=b&sp=r&sig=TKhvjaEYIVTOW4YSUaW%2B9JgyVDolB7%2BkGV9%2B6oG6%2Bio%3D", + "fileSize": 1007077, + "uriType": 2, + "expiration": "2025-11-06T14:04:08.726644Z" + } + ], + "xuid": "2535451380716241", + "screenshotName": "", + "titleName": "Blue Dragon", + "screenshotLocale": "en-GB", + "screenshotContentAttributes": 0, + "deviceType": "Scarlett" + }, + { + "screenshotId": "12e4d6f4-c33c-4907-9bd8-ae58a3677c10", + "resolutionHeight": 2160, + "resolutionWidth": 3840, + "state": 6, + "datePublished": "2025-11-06T01:08:43.31578Z", + "dateTaken": "2025-11-06T01:08:35Z", + "lastModified": "2025-11-06T01:08:35Z", + "userCaption": "", + "type": 8, + "scid": "d1adc8aa-0a31-4407-90f2-7e9b54b0347c", + "titleId": 1297287135, + "rating": 0.0, + "ratingCount": 0, + "views": 0, + "titleData": "", + "systemProperties": "bababbba-d6c5-44b6-9bf7-fdebfacf87880;", + "savedByUser": true, + "achievementId": "", + "greatestMomentId": null, + "thumbnails": [ + { + "uri": "https://screenshotscontent-t2001.media.xboxlive.com/xuid-2533274961401245-public/12e4d6f4-c33c-4907-9bd8-ae58a3677c10_Thumbnail.PNG", + "fileSize": 0, + "thumbnailType": 1 + }, + { + "uri": "https://screenshotscontent-t2001.media.xboxlive.com/xuid-2533274961401245-public/12e4d6f4-c33c-4907-9bd8-ae58a3677c10_Thumbnail.PNG", + "fileSize": 0, + "thumbnailType": 2 + } + ], + "screenshotUris": [ + { + "uri": "https://screenshotscontent-d2001.media.xboxlive.com/xuid-2533274961401245-private/12e4d6f4-c33c-4907-9bd8-ae58a3677c10.PNG?skoid=296fcea0-0bf0-4a22-abf7-16b3524eba1b&sktid=68cd85cc-e0b3-43c8-ba3c-67686dbf8a67&skt=2025-11-05T23%3A58%3A48Z&ske=2025-11-06T23%3A58%3A48Z&sks=b&skv=2025-05-05&sv=2025-05-05&st=2025-11-06T00%3A53%3A43Z&se=2125-11-06T01%3A08%3A43Z&sr=b&sp=r&sig=Cf37YpcFCeZF3GJyvB0ribatqs2sR6g79oKm55Yjs3A%3D", + "fileSize": 3769222, + "uriType": 2, + "expiration": "2025-11-06T14:04:08.7266493Z" + } + ], + "xuid": "2533274961401245", + "screenshotName": "", + "titleName": "Blue Dragon", + "screenshotLocale": "en-GB", + "screenshotContentAttributes": 0, + "deviceType": "Scarlett" + }, + { + "screenshotId": "e5b3298e-12fd-48b7-9561-aa75da69bf65", + "resolutionHeight": 1080, + "resolutionWidth": 1920, + "state": 6, + "datePublished": "2025-11-04T02:24:57.1824355Z", + "dateTaken": "2025-11-04T00:17:01Z", + "lastModified": "2025-11-04T00:17:01Z", + "userCaption": "", + "type": 8, + "scid": "d1adc8aa-0a31-4407-90f2-7e9b54b0347c", + "titleId": 1297287135, + "rating": 0.0, + "ratingCount": 0, + "views": 0, + "titleData": "", + "systemProperties": "4458d2bc-e7f5-4028-bf2a-40abc22c668a0;", + "savedByUser": true, + "achievementId": "", + "greatestMomentId": null, + "thumbnails": [ + { + "uri": "https://screenshotscontent-t5001.media.xboxlive.com/xuid-2533274962064319-public/e5b3298e-12fd-48b7-9561-aa75da69bf65_Thumbnail.PNG", + "fileSize": 0, + "thumbnailType": 1 + }, + { + "uri": "https://screenshotscontent-t5001.media.xboxlive.com/xuid-2533274962064319-public/e5b3298e-12fd-48b7-9561-aa75da69bf65_Thumbnail.PNG", + "fileSize": 0, + "thumbnailType": 2 + } + ], + "screenshotUris": [ + { + "uri": "https://screenshotscontent-d5001.media.xboxlive.com/xuid-2533274962064319-private/e5b3298e-12fd-48b7-9561-aa75da69bf65.PNG?skoid=296fcea0-0bf0-4a22-abf7-16b3524eba1b&sktid=68cd85cc-e0b3-43c8-ba3c-67686dbf8a67&skt=2025-11-04T01%3A18%3A33Z&ske=2025-11-05T01%3A18%3A33Z&sks=b&skv=2025-05-05&sv=2025-05-05&st=2025-11-04T02%3A09%3A57Z&se=2125-11-04T02%3A24%3A57Z&sr=b&sp=r&sig=KRaWxez3bQGGig8EjlqeCUvMywuIB5rEfsl%2Bkku2WY8%3D", + "fileSize": 2015230, + "uriType": 2, + "expiration": "2025-11-06T14:04:08.7267369Z" + } + ], + "xuid": "2533274962064319", + "screenshotName": "", + "titleName": "Blue Dragon", + "screenshotLocale": "en-GB", + "screenshotContentAttributes": 0, + "deviceType": "Scarlett" + }, + { + "screenshotId": "5f53eca2-6558-4301-a619-02b66bfce450", + "resolutionHeight": 1080, + "resolutionWidth": 1920, + "state": 6, + "datePublished": "2025-11-04T02:24:57.1679507Z", + "dateTaken": "2025-11-04T00:25:03Z", + "lastModified": "2025-11-04T00:25:03Z", + "userCaption": "", + "type": 8, + "scid": "d1adc8aa-0a31-4407-90f2-7e9b54b0347c", + "titleId": 1297287135, + "rating": 0.0, + "ratingCount": 0, + "views": 0, + "titleData": "", + "systemProperties": "ca8ba01f-8e45-4355-8ab0-f736e31d13920;", + "savedByUser": true, + "achievementId": "", + "greatestMomentId": null, + "thumbnails": [ + { + "uri": "https://screenshotscontent-t5001.media.xboxlive.com/xuid-2533274962064319-public/5f53eca2-6558-4301-a619-02b66bfce450_Thumbnail.PNG", + "fileSize": 0, + "thumbnailType": 1 + }, + { + "uri": "https://screenshotscontent-t5001.media.xboxlive.com/xuid-2533274962064319-public/5f53eca2-6558-4301-a619-02b66bfce450_Thumbnail.PNG", + "fileSize": 0, + "thumbnailType": 2 + } + ], + "screenshotUris": [ + { + "uri": "https://screenshotscontent-d5001.media.xboxlive.com/xuid-2533274962064319-private/5f53eca2-6558-4301-a619-02b66bfce450.PNG?skoid=296fcea0-0bf0-4a22-abf7-16b3524eba1b&sktid=68cd85cc-e0b3-43c8-ba3c-67686dbf8a67&skt=2025-11-04T01%3A18%3A33Z&ske=2025-11-05T01%3A18%3A33Z&sks=b&skv=2025-05-05&sv=2025-05-05&st=2025-11-04T02%3A09%3A57Z&se=2125-11-04T02%3A24%3A57Z&sr=b&sp=r&sig=Wz8oFkdBnZ5iGYxecyZGn0b5VlnZs5JRo%2FxFxVLCrTA%3D", + "fileSize": 393494, + "uriType": 2, + "expiration": "2025-11-06T14:04:08.7267378Z" + } + ], + "xuid": "2533274962064319", + "screenshotName": "", + "titleName": "Blue Dragon", + "screenshotLocale": "en-GB", + "screenshotContentAttributes": 0, + "deviceType": "Scarlett" + } + ], + "pagingInfo": { "continuationToken": "abcde_vwxyzZAAAAA2" } +} diff --git a/tests/components/xbox/fixtures/screenshots_recent_xuid.json b/tests/components/xbox/fixtures/screenshots_recent_xuid.json new file mode 100644 index 0000000000000..f2c45a3fcdd61 --- /dev/null +++ b/tests/components/xbox/fixtures/screenshots_recent_xuid.json @@ -0,0 +1,54 @@ +{ + "screenshots": [ + { + "screenshotId": "41593644-be22-43d6-b224-c7bebe14076e", + "resolutionHeight": 1080, + "resolutionWidth": 1920, + "state": 6, + "datePublished": "2015-11-11T06:11:58.6404578Z", + "dateTaken": "2015-11-11T06:10:16Z", + "lastModified": "2015-11-11T06:10:16Z", + "userCaption": "", + "type": 8, + "scid": "1370999b-fca2-4c53-8ec5-73493bcb67e5", + "titleId": 219630713, + "rating": 0.0, + "ratingCount": 0, + "views": 413, + "titleData": "", + "systemProperties": "e1fad752-787f-4be7-b89d-ba84e06f59a6;", + "savedByUser": true, + "achievementId": "", + "greatestMomentId": null, + "thumbnails": [ + { + "uri": "https://screenshotscontent-t5001.xboxlive.com/00097bbbbbbbbb23-41593644-be22-43d6-b224-c7bebe14076e/Thumbnail_Small.PNG?sv=2015-12-11&sr=b&si=DefaultAccess&sig=TcI8NNFJlcGykmjTFzeFKgUz7E9g%2FHKZqSNUoYAlZOM%3D", + "fileSize": 95044, + "thumbnailType": 1 + }, + { + "uri": "https://screenshotscontent-t5001.xboxlive.com/00097bbbbbbbbb23-41593644-be22-43d6-b224-c7bebe14076e/Thumbnail_Large.PNG?sv=2015-12-11&sr=b&si=DefaultAccess&sig=W4ZlmlL7Zd%2FubVoWMojXIfmTiodcpTezppxvsEeNOlI%3D", + "fileSize": 340052, + "thumbnailType": 2 + } + ], + "screenshotUris": [ + { + "uri": "https://screenshotscontent-d5001.xboxlive.com/00097bbbbbbbbb23-41593644-be22-43d6-b224-c7bebe14076e/Screenshot-Original.png?sv=2015-12-11&sr=b&si=DefaultAccess&sig=ALKo3DE2HXqBTlpdyynIrH6RPKIECOF7zwotH%2Bb30Ts%3D", + "fileSize": 1790858, + "uriType": 2, + "expiration": "2018-03-29T17:46:58.5342894Z" + } + ], + "xuid": "2669321029139235", + "screenshotName": "", + "titleName": "Halo 5: Guardians", + "screenshotLocale": "en-US", + "screenshotContentAttributes": 0, + "deviceType": "XboxOne" + } + ], + "pagingInfo": { + "continuationToken": null + } +} diff --git a/tests/components/xbox/fixtures/titlehub_titlehistory.json b/tests/components/xbox/fixtures/titlehub_titlehistory.json new file mode 100644 index 0000000000000..2903bd064e9a5 --- /dev/null +++ b/tests/components/xbox/fixtures/titlehub_titlehistory.json @@ -0,0 +1,335 @@ +{ + "xuid": "2719584417856940", + "titles": [ + { + "titleId": "1297287135", + "pfn": null, + "bingId": "66acd000-77fe-1000-9115-d8024d5307df", + "windowsPhoneProductId": null, + "name": "Blue Dragon", + "type": "Game", + "devices": ["Xbox360", "XboxOne", "XboxSeries"], + "displayImage": "http://store-images.s-microsoft.com/image/apps.45451.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.3abf2cc3-00cc-417d-a93d-97110cdfb261", + "mediaItemType": "Xbox360Game", + "modernTitleId": "644793037", + "isBundle": false, + "achievement": { + "currentAchievements": 3, + "totalAchievements": 43, + "currentGamerscore": 15, + "totalGamerscore": 1000, + "progressPercentage": 2.0, + "sourceVersion": 1 + }, + "stats": { "sourceVersion": 0 }, + "gamePass": null, + "images": [ + { + "url": "http://store-images.s-microsoft.com/image/apps.35725.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.c4bf34f8-ad40-4af3-914e-a85e75a76bed", + "type": "Screenshot", + "caption": "" + }, + { + "url": "http://store-images.s-microsoft.com/image/apps.64736.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.6491fb2f-52e7-4129-bcbd-d23a67117ae0", + "type": "BrandedKeyArt", + "caption": "" + }, + { + "url": "http://store-images.s-microsoft.com/image/apps.55545.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.4c2daefb-fbf6-4b90-b392-bf8ecc39a92e", + "type": "TitledHeroArt", + "caption": "" + }, + { + "url": "http://store-images.s-microsoft.com/image/apps.22570.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.bf29284d-808a-4e4a-beaa-6621c9898d0e", + "type": "Poster", + "caption": "" + }, + { + "url": "http://store-images.s-microsoft.com/image/apps.55545.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.4c2daefb-fbf6-4b90-b392-bf8ecc39a92e", + "type": "SuperHeroArt", + "caption": "" + }, + { + "url": "http://store-images.s-microsoft.com/image/apps.45451.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.3abf2cc3-00cc-417d-a93d-97110cdfb261", + "type": "BoxArt", + "caption": "" + }, + { + "url": "http://store-images.s-microsoft.com/image/apps.45451.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.3abf2cc3-00cc-417d-a93d-97110cdfb261", + "type": "FeaturePromotionalSquareArt", + "caption": "" + }, + { + "url": "http://store-images.s-microsoft.com/image/apps.38628.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.c2a205af-5146-405b-b2b7-56845351f1f3", + "type": "Screenshot", + "caption": "" + }, + { + "url": "http://store-images.s-microsoft.com/image/apps.22150.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.b147895c-e947-424d-a731-faefc8c9906a", + "type": "Screenshot", + "caption": "" + }, + { + "url": "http://store-images.s-microsoft.com/image/apps.37559.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.479d2dc1-db2d-4ffa-8c54-a2bebb093ec6", + "type": "Screenshot", + "caption": "" + }, + { + "url": "http://store-images.s-microsoft.com/image/apps.32737.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.6a16ae3e-2918-46e9-90d9-232c79cb9d9d", + "type": "Screenshot", + "caption": "" + }, + { + "url": "http://store-images.s-microsoft.com/image/apps.57046.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.0c0dd072-aa27-4e83-9010-474dfbb42277", + "type": "Screenshot", + "caption": "" + }, + { + "url": "http://store-images.s-microsoft.com/image/apps.19315.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.6293a7b7-07ca-4df0-9eea-6018285a0a8d", + "type": "Screenshot", + "caption": "" + }, + { + "url": "http://store-images.s-microsoft.com/image/apps.23374.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.66498a73-52f5-4247-a1e2-d3c84b9b315d", + "type": "Screenshot", + "caption": "" + }, + { + "url": "http://store-images.s-microsoft.com/image/apps.64646.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.83182b76-4294-496d-90a7-f4e31e7aa80a", + "type": "Screenshot", + "caption": "" + }, + { + "url": "http://store-images.s-microsoft.com/image/apps.24470.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.72d2abc3-aa69-4aeb-960b-6f6d25f498e4", + "type": "Screenshot", + "caption": "" + }, + { + "url": "http://store-images.s-microsoft.com/image/apps.15604.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.27cee011-660b-49a4-bd33-38db6fff5226", + "type": "Screenshot", + "caption": "" + }, + { + "url": "http://store-images.s-microsoft.com/image/apps.39987.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.be285efe-78f8-4984-9d28-9159881bacd4", + "type": "Screenshot", + "caption": "" + }, + { + "url": "http://store-images.s-microsoft.com/image/apps.38206.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.2409803d-7378-4a69-a10b-1574ac42b98b", + "type": "Screenshot", + "caption": "" + }, + { + "url": "http://store-images.s-microsoft.com/image/apps.14938.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.ef6ee72c-4beb-45ec-bd10-6235bd6a7c7f", + "type": "Screenshot", + "caption": "" + }, + { + "url": "http://store-images.s-microsoft.com/image/apps.12835.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.6165ee24-df01-44f5-80fe-7411f9366d1c", + "type": "Screenshot", + "caption": "" + }, + { + "url": "http://store-images.s-microsoft.com/image/apps.40786.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.b7607a0d-0101-4864-9bf8-ad889f820489", + "type": "Screenshot", + "caption": "" + }, + { + "url": "http://store-images.s-microsoft.com/image/apps.55686.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.ecbb0e91-36a9-4f76-ab1e-5a5de009840e", + "type": "Screenshot", + "caption": "" + } + ], + "titleHistory": { + "lastTimePlayed": "2025-10-30T13:56:20.61Z", + "visible": true, + "canHide": false + }, + "titleRecord": null, + "detail": null, + "friendsWhoPlayed": null, + "alternateTitleIds": null, + "contentBoards": null, + "xboxLiveTier": "Full" + }, + { + "titleId": "1560034050", + "pfn": "Ubisoft.6f7d78df-dd91-4d2a-b038-367b2b266f46_ngz4m417e0mpw", + "bingId": "56e06243-00dc-49c4-a214-20af83036f97", + "windowsPhoneProductId": "088ef83e-6beb-49de-9e13-294ea701b210", + "name": "Assassin's Creed® Syndicate", + "type": "Game", + "devices": ["XboxOne"], + "displayImage": "http://store-images.s-microsoft.com/image/apps.23100.63309362003335928.4079e21b-b00f-4446-a680-6bf9c0eb0158.7f8406d5-3130-4d85-b3e3-a5e0e2f8ac49", + "mediaItemType": "Application", + "modernTitleId": "1560034050", + "isBundle": false, + "achievement": { + "currentAchievements": 22, + "totalAchievements": 0, + "currentGamerscore": 465, + "totalGamerscore": 1300, + "progressPercentage": 36.0, + "sourceVersion": 2 + }, + "stats": { + "sourceVersion": 2 + }, + "gamePass": { + "isGamePass": false + }, + "images": [ + { + "url": "http://store-images.s-microsoft.com/image/apps.45099.63309362003335928.4079e21b-b00f-4446-a680-6bf9c0eb0158.3a88a700-8351-45e7-b430-dc75a7a9d9b8", + "type": "Screenshot" + }, + { + "url": "http://store-images.s-microsoft.com/image/apps.46246.63309362003335928.4079e21b-b00f-4446-a680-6bf9c0eb0158.c976135a-831a-4cf6-a39b-f01c633567bc", + "type": "BrandedKeyArt" + }, + { + "url": "http://store-images.s-microsoft.com/image/apps.9155.63309362003335928.4079e21b-b00f-4446-a680-6bf9c0eb0158.66c76ab3-66b2-4eb4-884a-647d73cb1617", + "type": "TitledHeroArt" + }, + { + "url": "http://store-images.s-microsoft.com/image/apps.9961.63309362003335928.4079e21b-b00f-4446-a680-6bf9c0eb0158.f69e9d47-aef9-4aba-9297-915f02c744e9", + "type": "Poster" + }, + { + "url": "http://store-images.s-microsoft.com/image/apps.61147.63309362003335928.4079e21b-b00f-4446-a680-6bf9c0eb0158.cd2dd80e-a2bc-4a19-adf0-7018ee45b2cd", + "type": "SuperHeroArt" + }, + { + "url": "http://store-images.s-microsoft.com/image/apps.23100.63309362003335928.4079e21b-b00f-4446-a680-6bf9c0eb0158.7f8406d5-3130-4d85-b3e3-a5e0e2f8ac49", + "type": "BoxArt" + }, + { + "url": "http://store-images.s-microsoft.com/image/apps.47207.63309362003335928.4079e21b-b00f-4446-a680-6bf9c0eb0158.3329df5e-a071-41d6-b584-c03fc3706fe8", + "type": "FeaturePromotionalSquareArt" + }, + { + "url": "http://store-images.s-microsoft.com/image/apps.31444.63309362003335928.4079e21b-b00f-4446-a680-6bf9c0eb0158.5295c5a8-a2d7-4407-ab1c-6fea8d77b0b9", + "type": "Screenshot" + }, + { + "url": "http://store-images.s-microsoft.com/image/apps.16645.63309362003335928.4079e21b-b00f-4446-a680-6bf9c0eb0158.040cba15-6110-4d0f-8da7-a2883b298f44", + "type": "Screenshot" + } + ], + "titleHistory": { + "lastTimePlayed": "2020-10-08T01:55:12.4411936Z", + "visible": true, + "canHide": false + }, + "detail": { + "attributes": [ + { + "applicablePlatforms": null, + "maximum": null, + "minimum": null, + "name": "BroadcastSupport" + }, + { + "applicablePlatforms": null, + "maximum": null, + "minimum": null, + "name": "XboxLive" + } + ], + "availabilities": [ + { + "Actions": [ + "Details", + "Fulfill", + "Purchase", + "Browse", + "Curate", + "Redeem" + ], + "AvailabilityId": "9ZDCVZ019WLR", + "Platforms": ["Xbox"], + "SkuId": "0001" + } + ], + "capabilities": [], + "description": "London, 1868. In the heart of the Industrial Revolution, lead your underworld organization and grow your influence to fight those who exploit the less privileged in the name of progress:\r\n\r\n• Champion justice\r\nAs Jacob Frye, a young and reckless Assassin, use your skills to help those trampled by the march of progress. From freeing exploited children used as slave labour in factories, to stealing precious assets from enemy boats, you will stop at nothing to bring justice back to London’s streets.\r\n\r\n• Command London’s underworld\r\nTo reclaim London for the people, you will need an army. As a gang leader, strengthen your stronghold and rally rival gang members to your cause, in order to take back the capital from the Templars’ hold.\r\n\r\n• A new dynamic fighting system\r\nIn Assassin’s Creed Syndicate, action is fast-paced and brutal. As a master of combat, combine powerful multi-kills and countermoves to strike your enemies down.\r\n\r\n• A whole new arsenal\r\nChoose your own way to fight enemies. Take advantage of the Rope Launcher technology to be as stealthy as ever and strike with your Hidden Blade. Or choose the kukri knife and the brass knuckles to get the drop on your enemies.\r\n\r\n• A new age of transportation\r\nIn London, the systemic vehicles offer an ever-changing environment. Drive carriages to chase your target, use your parkour skills to engage in epic fights atop high-speed trains, or make your own way amongst the boats of the River Thames.\r\n\r\n• A vast open world\r\nTravel the city at the height of the Industrial Revolution and meet iconic historical figures. From Westminster to Whitechapel, you will come across Darwin, Dickens, Queen Victoria… and many more.", + "developerName": "Ubisoft Quebec", + "publisherName": "Ubisoft", + "minAge": 16, + "releaseDate": "2015-10-23T00:00:00Z", + "shortDescription": "“9/10” Gamespot\n\nLondon, 1868. In the heart of the Industrial Revolution, play as Jacob Frye - a brash and charismatic Assassin.\n\nOn a mission to defeat those who exploit the less privileged in the name of progress, lead your underworld organization, fight like never before in a massive open-world and discover new features inspired by the dawning modern age.", + "vuiDisplayName": null, + "xboxLiveGoldRequired": false + }, + "friendsWhoPlayed": { + "people": [ + { + "xuid": "2533274810951322", + "isFavorite": false, + "displayPicRaw": "https://images-eds-ssl.xboxlive.com/image?url=z951ykn43p4FqWbbFvR2Ec.8vbDhj8G2Xe7JngaTToBrrCmIEEXHC9UNrdJ6P7KIm2RQQtfSX9jMSatOYowOzEjYigCkXhTNbce1rv4TMJkvAA49vtg8MpFHt6Pum4yf&format=png", + "useAvatar": true, + "isCurrentlyPlaying": false, + "gamertag": "cavsbballin23", + "lastTimePlayed": "2018-07-03T23:39:26.9942864Z", + "presenceState": "Offline", + "preferredColor": { + "primaryColor": "1073d6", + "secondaryColor": "133157", + "tertiaryColor": "134e8a" + } + }, + { + "xuid": "2533274829739812", + "isFavorite": false, + "displayPicRaw": "https://images-eds-ssl.xboxlive.com/image?url=wHwbXKif8cus8csoZ03RW_ES.ojiJijNBGRVUbTnZKsoCCCkjlsEJrrMqDkYqs3MrHRhRzH2.GdQgHRZi_rY_v9q_t0POXG6iEqqweb.6Fh7_jOUyRHgH8TW9apXc.w08pRmE5ZgIlhBWHgor1YXeQQQ5CqjqDPWUROGdwJDAvo-&format=png", + "useAvatar": false, + "isCurrentlyPlaying": false, + "gamertag": "FLUFFY JOK3R", + "lastTimePlayed": "2018-04-29T03:42:10.4244998Z", + "presenceState": "Offline", + "preferredColor": { + "primaryColor": "e31123", + "secondaryColor": "471010", + "tertiaryColor": "851414" + } + }, + { + "xuid": "2533274826800072", + "isFavorite": false, + "displayPicRaw": "https://images-eds-ssl.xboxlive.com/image?url=Hr2eiH8yWKd4q_oa.xgbMsBMaML4RHmTUbpHkaZTnVU5KRB4bTh8MR86WWWB5GR.vzh0E9kRjckB83CjpwGCrUWEsXRb90OAITc4F4D6jNOPvNH6pV0L8c1lNSoi0gzbsaJ3._X_vFSIGtf8TCZsYn2hoN3c57jwNTxfTwUyiO0-&format=png", + "useAvatar": true, + "isCurrentlyPlaying": false, + "gamertag": "ThumbTacular", + "lastTimePlayed": "2017-10-15T11:36:05.1343310Z", + "presenceState": "Offline", + "preferredColor": { + "primaryColor": "193e91", + "secondaryColor": "101836", + "tertiaryColor": "102c69" + } + }, + { + "xuid": "2533274842759744", + "isFavorite": false, + "displayPicRaw": "https://images-eds-ssl.xboxlive.com/image?url=wHwbXKif8cus8csoZ03RW3apWESZjav65Yncai8aRmVbSlZ3zqRpg1sdxEje_JmFoVefUwnJqOE159pIT3JzYd9blbzFeNzxgMKTwNLyj1Hq3Vb.8avA4NoeHlgaLPP2oI5233eY7w3bgRSMcnELusaPWGxm5L469OLjNCQQ20s-&format=png", + "useAvatar": true, + "isCurrentlyPlaying": false, + "gamertag": "Blvck Clovd", + "lastTimePlayed": "2016-01-14T05:54:34.0000000Z", + "presenceState": "Offline", + "preferredColor": { + "primaryColor": "677488", + "secondaryColor": "222B38", + "tertiaryColor": "3e4b61" + } + } + ], + "currentlyPlayingCount": 16, + "havePlayedCount": 4 + }, + "alternateTitleIds": [], + "contentBoards": null, + "xboxLiveTier": "Full" + } + ] +} diff --git a/tests/components/xbox/snapshots/test_media_source.ambr b/tests/components/xbox/snapshots/test_media_source.ambr new file mode 100644 index 0000000000000..24dc1709c5e17 --- /dev/null +++ b/tests/components/xbox/snapshots/test_media_source.ambr @@ -0,0 +1,668 @@ +# serializer version: 1 +# name: test_browse_media[category_view] + dict({ + 'can_expand': True, + 'can_play': False, + 'can_search': False, + 'children': list([ + dict({ + 'can_expand': True, + 'can_play': False, + 'can_search': False, + 'children_media_class': , + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640/1297287135/gameclips', + 'media_content_type': , + 'thumbnail': None, + 'title': 'Gameclips', + }), + dict({ + 'can_expand': True, + 'can_play': False, + 'can_search': False, + 'children_media_class': , + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640/1297287135/screenshots', + 'media_content_type': , + 'thumbnail': None, + 'title': 'Screenshots', + }), + dict({ + 'can_expand': True, + 'can_play': False, + 'can_search': False, + 'children_media_class': , + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640/1297287135/community_gameclips', + 'media_content_type': , + 'thumbnail': None, + 'title': 'Community gameclips', + }), + dict({ + 'can_expand': True, + 'can_play': False, + 'can_search': False, + 'children_media_class': , + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640/1297287135/community_screenshots', + 'media_content_type': , + 'thumbnail': None, + 'title': 'Community screenshots', + }), + dict({ + 'can_expand': True, + 'can_play': False, + 'can_search': False, + 'children_media_class': , + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media', + 'media_content_type': , + 'thumbnail': None, + 'title': 'Game media', + }), + ]), + 'children_media_class': , + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640/1297287135', + 'media_content_type': , + 'not_shown': 0, + 'thumbnail': None, + 'title': 'Xbox / GSR Ae / Blue Dragon', + }) +# --- +# name: test_browse_media[community_gameclips_view] + dict({ + 'can_expand': True, + 'can_play': False, + 'can_search': False, + 'children': list([ + dict({ + 'can_expand': False, + 'can_play': True, + 'can_search': False, + 'children_media_class': None, + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640/1297287135/community_gameclips/0829f20b-604b-483e-89ff-d37d1071cd70', + 'media_content_type': , + 'thumbnail': 'https://gameclipscontent-t3011.media.xboxlive.com/xuid-2535458333395495-public/0829f20b-604b-483e-89ff-d37d1071cd70_Thumbnail.PNG', + 'title': '8 hours', + }), + dict({ + 'can_expand': False, + 'can_play': True, + 'can_search': False, + 'children_media_class': None, + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640/1297287135/community_gameclips/6fa2731a-8b58-4aa6-848c-4bf15734358b', + 'media_content_type': , + 'thumbnail': 'https://gameclipscontent-t3021.media.xboxlive.com/xuid-2535458333395495-public/6fa2731a-8b58-4aa6-848c-4bf15734358b_Thumbnail.PNG', + 'title': '8 hours', + }), + dict({ + 'can_expand': False, + 'can_play': True, + 'can_search': False, + 'children_media_class': None, + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640/1297287135/community_gameclips/37956427-4118-4b3e-aecb-f3f96ef34e6e', + 'media_content_type': , + 'thumbnail': 'https://gameclipscontent-t2014.media.xboxlive.com/xuid-2535458333395495-public/37956427-4118-4b3e-aecb-f3f96ef34e6e_Thumbnail.PNG', + 'title': '8 hours', + }), + dict({ + 'can_expand': False, + 'can_play': True, + 'can_search': False, + 'children_media_class': None, + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640/1297287135/community_gameclips/6aecd078-be33-4d52-853a-a78c0c678b45', + 'media_content_type': , + 'thumbnail': 'https://gameclipscontent-t3020.media.xboxlive.com/xuid-2535458333395495-public/6aecd078-be33-4d52-853a-a78c0c678b45_Thumbnail.PNG', + 'title': '8 hours', + }), + dict({ + 'can_expand': False, + 'can_play': True, + 'can_search': False, + 'children_media_class': None, + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640/1297287135/community_gameclips/84bdc7a5-fa53-4348-9502-4cd804906f3b', + 'media_content_type': , + 'thumbnail': 'https://gameclipscontent-t3019.media.xboxlive.com/xuid-2535458333395495-public/84bdc7a5-fa53-4348-9502-4cd804906f3b_Thumbnail.PNG', + 'title': '8 hours', + }), + dict({ + 'can_expand': False, + 'can_play': True, + 'can_search': False, + 'children_media_class': None, + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640/1297287135/community_gameclips/c06597b0-28ce-4d2f-856d-b7cde9a40f4a', + 'media_content_type': , + 'thumbnail': 'https://gameclipscontent-t2002.media.xboxlive.com/xuid-2535451917097448-public/c06597b0-28ce-4d2f-856d-b7cde9a40f4a_Thumbnail.PNG', + 'title': '15 days', + }), + ]), + 'children_media_class': , + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640/1297287135/community_gameclips', + 'media_content_type': , + 'not_shown': 0, + 'thumbnail': None, + 'title': 'Xbox / GSR Ae / Blue Dragon / Community gameclips', + }) +# --- +# name: test_browse_media[community_screenshots_view] + dict({ + 'can_expand': True, + 'can_play': False, + 'can_search': False, + 'children': list([ + dict({ + 'can_expand': False, + 'can_play': True, + 'can_search': False, + 'children_media_class': None, + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640/1297287135/community_screenshots/5eaea123-0111-4682-9e86-34737b09d82f', + 'media_content_type': , + 'thumbnail': 'https://screenshotscontent-t2001.media.xboxlive.com/xuid-2535422966774043-public/5eaea123-0111-4682-9e86-34737b09d82f_Thumbnail.PNG', + 'title': '6 hours | 1080p', + }), + dict({ + 'can_expand': False, + 'can_play': True, + 'can_search': False, + 'children_media_class': None, + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640/1297287135/community_screenshots/504a78e5-be24-4020-a245-77cb528e91ea', + 'media_content_type': , + 'thumbnail': 'https://screenshotscontent-t5002.media.xboxlive.com/xuid-2535422966774043-public/504a78e5-be24-4020-a245-77cb528e91ea_Thumbnail.PNG', + 'title': '6 hours | 1080p', + }), + dict({ + 'can_expand': False, + 'can_play': True, + 'can_search': False, + 'children_media_class': None, + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640/1297287135/community_screenshots/fc4cf7c4-edfc-4e7b-ba8f-d7e97dff8ea8', + 'media_content_type': , + 'thumbnail': 'https://screenshotscontent-t5001.media.xboxlive.com/xuid-2535459412956058-public/fc4cf7c4-edfc-4e7b-ba8f-d7e97dff8ea8_Thumbnail.PNG', + 'title': '12 hours | 1080p', + }), + dict({ + 'can_expand': False, + 'can_play': True, + 'can_search': False, + 'children_media_class': None, + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640/1297287135/community_screenshots/6a719527-a141-4586-8d44-26fec3f1b693', + 'media_content_type': , + 'thumbnail': 'https://screenshotscontent-t4002.media.xboxlive.com/xuid-2535451380716241-public/6a719527-a141-4586-8d44-26fec3f1b693_Thumbnail.PNG', + 'title': '14 hours | 720p', + }), + dict({ + 'can_expand': False, + 'can_play': True, + 'can_search': False, + 'children_media_class': None, + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640/1297287135/community_screenshots/12e4d6f4-c33c-4907-9bd8-ae58a3677c10', + 'media_content_type': , + 'thumbnail': 'https://screenshotscontent-t2001.media.xboxlive.com/xuid-2533274961401245-public/12e4d6f4-c33c-4907-9bd8-ae58a3677c10_Thumbnail.PNG', + 'title': '16 hours | 2160p', + }), + dict({ + 'can_expand': False, + 'can_play': True, + 'can_search': False, + 'children_media_class': None, + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640/1297287135/community_screenshots/e5b3298e-12fd-48b7-9561-aa75da69bf65', + 'media_content_type': , + 'thumbnail': 'https://screenshotscontent-t5001.media.xboxlive.com/xuid-2533274962064319-public/e5b3298e-12fd-48b7-9561-aa75da69bf65_Thumbnail.PNG', + 'title': '3 days | 1080p', + }), + dict({ + 'can_expand': False, + 'can_play': True, + 'can_search': False, + 'children_media_class': None, + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640/1297287135/community_screenshots/5f53eca2-6558-4301-a619-02b66bfce450', + 'media_content_type': , + 'thumbnail': 'https://screenshotscontent-t5001.media.xboxlive.com/xuid-2533274962064319-public/5f53eca2-6558-4301-a619-02b66bfce450_Thumbnail.PNG', + 'title': '3 days | 1080p', + }), + ]), + 'children_media_class': , + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640/1297287135/community_screenshots', + 'media_content_type': , + 'not_shown': 0, + 'thumbnail': None, + 'title': 'Xbox / GSR Ae / Blue Dragon / Community screenshots', + }) +# --- +# name: test_browse_media[game_media_view] + dict({ + 'can_expand': True, + 'can_play': False, + 'can_search': False, + 'children': list([ + dict({ + 'can_expand': False, + 'can_play': True, + 'can_search': False, + 'children_media_class': None, + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/0', + 'media_content_type': , + 'thumbnail': 'http://store-images.s-microsoft.com/image/apps.35725.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.c4bf34f8-ad40-4af3-914e-a85e75a76bed', + 'title': 'Screenshot', + }), + dict({ + 'can_expand': False, + 'can_play': True, + 'can_search': False, + 'children_media_class': None, + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/1', + 'media_content_type': , + 'thumbnail': 'http://store-images.s-microsoft.com/image/apps.64736.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.6491fb2f-52e7-4129-bcbd-d23a67117ae0', + 'title': 'BrandedKeyArt', + }), + dict({ + 'can_expand': False, + 'can_play': True, + 'can_search': False, + 'children_media_class': None, + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/2', + 'media_content_type': , + 'thumbnail': 'http://store-images.s-microsoft.com/image/apps.55545.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.4c2daefb-fbf6-4b90-b392-bf8ecc39a92e', + 'title': 'TitledHeroArt', + }), + dict({ + 'can_expand': False, + 'can_play': True, + 'can_search': False, + 'children_media_class': None, + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/3', + 'media_content_type': , + 'thumbnail': 'http://store-images.s-microsoft.com/image/apps.22570.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.bf29284d-808a-4e4a-beaa-6621c9898d0e', + 'title': 'Poster', + }), + dict({ + 'can_expand': False, + 'can_play': True, + 'can_search': False, + 'children_media_class': None, + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/4', + 'media_content_type': , + 'thumbnail': 'http://store-images.s-microsoft.com/image/apps.55545.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.4c2daefb-fbf6-4b90-b392-bf8ecc39a92e', + 'title': 'SuperHeroArt', + }), + dict({ + 'can_expand': False, + 'can_play': True, + 'can_search': False, + 'children_media_class': None, + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/5', + 'media_content_type': , + 'thumbnail': 'http://store-images.s-microsoft.com/image/apps.45451.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.3abf2cc3-00cc-417d-a93d-97110cdfb261', + 'title': 'BoxArt', + }), + dict({ + 'can_expand': False, + 'can_play': True, + 'can_search': False, + 'children_media_class': None, + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/6', + 'media_content_type': , + 'thumbnail': 'http://store-images.s-microsoft.com/image/apps.35072.13670972585585116.70570f0d-17aa-4f97-b692-5412fa183673.25a97451-9369-4f6b-b66b-3427913235eb', + 'title': 'Logo', + }), + dict({ + 'can_expand': False, + 'can_play': True, + 'can_search': False, + 'children_media_class': None, + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/7', + 'media_content_type': , + 'thumbnail': 'http://store-images.s-microsoft.com/image/apps.45451.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.3abf2cc3-00cc-417d-a93d-97110cdfb261', + 'title': 'FeaturePromotionalSquareArt', + }), + dict({ + 'can_expand': False, + 'can_play': True, + 'can_search': False, + 'children_media_class': None, + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/8', + 'media_content_type': , + 'thumbnail': 'http://store-images.s-microsoft.com/image/apps.38628.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.c2a205af-5146-405b-b2b7-56845351f1f3', + 'title': 'Screenshot', + }), + dict({ + 'can_expand': False, + 'can_play': True, + 'can_search': False, + 'children_media_class': None, + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/9', + 'media_content_type': , + 'thumbnail': 'http://store-images.s-microsoft.com/image/apps.22150.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.b147895c-e947-424d-a731-faefc8c9906a', + 'title': 'Screenshot', + }), + dict({ + 'can_expand': False, + 'can_play': True, + 'can_search': False, + 'children_media_class': None, + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/10', + 'media_content_type': , + 'thumbnail': 'http://store-images.s-microsoft.com/image/apps.37559.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.479d2dc1-db2d-4ffa-8c54-a2bebb093ec6', + 'title': 'Screenshot', + }), + dict({ + 'can_expand': False, + 'can_play': True, + 'can_search': False, + 'children_media_class': None, + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/11', + 'media_content_type': , + 'thumbnail': 'http://store-images.s-microsoft.com/image/apps.32737.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.6a16ae3e-2918-46e9-90d9-232c79cb9d9d', + 'title': 'Screenshot', + }), + dict({ + 'can_expand': False, + 'can_play': True, + 'can_search': False, + 'children_media_class': None, + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/12', + 'media_content_type': , + 'thumbnail': 'http://store-images.s-microsoft.com/image/apps.57046.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.0c0dd072-aa27-4e83-9010-474dfbb42277', + 'title': 'Screenshot', + }), + dict({ + 'can_expand': False, + 'can_play': True, + 'can_search': False, + 'children_media_class': None, + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/13', + 'media_content_type': , + 'thumbnail': 'http://store-images.s-microsoft.com/image/apps.19315.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.6293a7b7-07ca-4df0-9eea-6018285a0a8d', + 'title': 'Screenshot', + }), + dict({ + 'can_expand': False, + 'can_play': True, + 'can_search': False, + 'children_media_class': None, + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/14', + 'media_content_type': , + 'thumbnail': 'http://store-images.s-microsoft.com/image/apps.23374.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.66498a73-52f5-4247-a1e2-d3c84b9b315d', + 'title': 'Screenshot', + }), + dict({ + 'can_expand': False, + 'can_play': True, + 'can_search': False, + 'children_media_class': None, + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/15', + 'media_content_type': , + 'thumbnail': 'http://store-images.s-microsoft.com/image/apps.64646.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.83182b76-4294-496d-90a7-f4e31e7aa80a', + 'title': 'Screenshot', + }), + dict({ + 'can_expand': False, + 'can_play': True, + 'can_search': False, + 'children_media_class': None, + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/16', + 'media_content_type': , + 'thumbnail': 'http://store-images.s-microsoft.com/image/apps.24470.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.72d2abc3-aa69-4aeb-960b-6f6d25f498e4', + 'title': 'Screenshot', + }), + dict({ + 'can_expand': False, + 'can_play': True, + 'can_search': False, + 'children_media_class': None, + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/17', + 'media_content_type': , + 'thumbnail': 'http://store-images.s-microsoft.com/image/apps.15604.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.27cee011-660b-49a4-bd33-38db6fff5226', + 'title': 'Screenshot', + }), + dict({ + 'can_expand': False, + 'can_play': True, + 'can_search': False, + 'children_media_class': None, + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/18', + 'media_content_type': , + 'thumbnail': 'http://store-images.s-microsoft.com/image/apps.39987.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.be285efe-78f8-4984-9d28-9159881bacd4', + 'title': 'Screenshot', + }), + dict({ + 'can_expand': False, + 'can_play': True, + 'can_search': False, + 'children_media_class': None, + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/19', + 'media_content_type': , + 'thumbnail': 'http://store-images.s-microsoft.com/image/apps.38206.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.2409803d-7378-4a69-a10b-1574ac42b98b', + 'title': 'Screenshot', + }), + dict({ + 'can_expand': False, + 'can_play': True, + 'can_search': False, + 'children_media_class': None, + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/20', + 'media_content_type': , + 'thumbnail': 'http://store-images.s-microsoft.com/image/apps.14938.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.ef6ee72c-4beb-45ec-bd10-6235bd6a7c7f', + 'title': 'Screenshot', + }), + dict({ + 'can_expand': False, + 'can_play': True, + 'can_search': False, + 'children_media_class': None, + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/21', + 'media_content_type': , + 'thumbnail': 'http://store-images.s-microsoft.com/image/apps.12835.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.6165ee24-df01-44f5-80fe-7411f9366d1c', + 'title': 'Screenshot', + }), + dict({ + 'can_expand': False, + 'can_play': True, + 'can_search': False, + 'children_media_class': None, + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/22', + 'media_content_type': , + 'thumbnail': 'http://store-images.s-microsoft.com/image/apps.40786.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.b7607a0d-0101-4864-9bf8-ad889f820489', + 'title': 'Screenshot', + }), + dict({ + 'can_expand': False, + 'can_play': True, + 'can_search': False, + 'children_media_class': None, + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media/23', + 'media_content_type': , + 'thumbnail': 'http://store-images.s-microsoft.com/image/apps.55686.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.ecbb0e91-36a9-4f76-ab1e-5a5de009840e', + 'title': 'Screenshot', + }), + ]), + 'children_media_class': , + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640/1297287135/game_media', + 'media_content_type': , + 'not_shown': 0, + 'thumbnail': None, + 'title': 'Xbox / GSR Ae / Blue Dragon / Game media', + }) +# --- +# name: test_browse_media[gameclips_view] + dict({ + 'can_expand': True, + 'can_play': False, + 'can_search': False, + 'children': list([ + dict({ + 'can_expand': False, + 'can_play': True, + 'can_search': False, + 'children_media_class': None, + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640/1297287135/gameclips/44b7e94d-b3e9-4b97-be73-417be9091e93', + 'media_content_type': , + 'thumbnail': 'https://gameclipscontent-t3017.xboxlive.com/xuid-2669321029139235-public/44b7e94d-b3e9-4b97-be73-417be9091e93_Thumbnail.PNG', + 'title': '8 years', + }), + dict({ + 'can_expand': False, + 'can_play': True, + 'can_search': False, + 'children_media_class': None, + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640/1297287135/gameclips/f87cc6ac-c291-4998-9124-d8b36c059b6a', + 'media_content_type': , + 'thumbnail': 'https://gameclipscontent-t2015.xboxlive.com/00097bbbbbbbbb23-f87cc6ac-c291-4998-9124-d8b36c059b6a-public/Thumbnail_Small.PNG?sv=2015-12-11&sr=b&si=DefaultAccess&sig=ikXCzETBJSBPvS8W%2FxoWTCJL%2FGY6PxKpQ0VG6qktnCQ%3D', + 'title': '9 years', + }), + ]), + 'children_media_class': , + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640/1297287135/gameclips', + 'media_content_type': , + 'not_shown': 0, + 'thumbnail': None, + 'title': 'Xbox / GSR Ae / Blue Dragon / Gameclips', + }) +# --- +# name: test_browse_media[games_view] + dict({ + 'can_expand': True, + 'can_play': False, + 'can_search': False, + 'children': list([ + dict({ + 'can_expand': True, + 'can_play': False, + 'can_search': False, + 'children_media_class': , + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640/1297287135', + 'media_content_type': , + 'thumbnail': 'http://store-images.s-microsoft.com/image/apps.64736.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.6491fb2f-52e7-4129-bcbd-d23a67117ae0', + 'title': 'Blue Dragon', + }), + dict({ + 'can_expand': True, + 'can_play': False, + 'can_search': False, + 'children_media_class': , + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640/1560034050', + 'media_content_type': , + 'thumbnail': 'http://store-images.s-microsoft.com/image/apps.46246.63309362003335928.4079e21b-b00f-4446-a680-6bf9c0eb0158.c976135a-831a-4cf6-a39b-f01c633567bc', + 'title': "Assassin's Creed® Syndicate", + }), + ]), + 'children_media_class': , + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640', + 'media_content_type': , + 'not_shown': 0, + 'thumbnail': None, + 'title': 'Xbox / GSR Ae', + }) +# --- +# name: test_browse_media[screenshots_view] + dict({ + 'can_expand': True, + 'can_play': False, + 'can_search': False, + 'children': list([ + dict({ + 'can_expand': False, + 'can_play': True, + 'can_search': False, + 'children_media_class': None, + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640/1297287135/screenshots/41593644-be22-43d6-b224-c7bebe14076e', + 'media_content_type': , + 'thumbnail': 'https://screenshotscontent-t5001.xboxlive.com/00097bbbbbbbbb23-41593644-be22-43d6-b224-c7bebe14076e/Thumbnail_Small.PNG?sv=2015-12-11&sr=b&si=DefaultAccess&sig=TcI8NNFJlcGykmjTFzeFKgUz7E9g%2FHKZqSNUoYAlZOM%3D', + 'title': '10 years | 1080p', + }), + ]), + 'children_media_class': , + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640/1297287135/screenshots', + 'media_content_type': , + 'not_shown': 0, + 'thumbnail': None, + 'title': 'Xbox / GSR Ae / Blue Dragon / Screenshots', + }) +# --- +# name: test_browse_media_accounts + dict({ + 'can_expand': True, + 'can_play': False, + 'can_search': False, + 'children': list([ + dict({ + 'can_expand': True, + 'can_play': False, + 'can_search': False, + 'children_media_class': None, + 'media_class': , + 'media_content_id': 'media-source://xbox/271958441785640', + 'media_content_type': , + 'thumbnail': 'https://images-eds-ssl.xboxlive.com/image?url=wHwbXKif8cus8csoZ03RW_ES.ojiJijNBGRVUbTnZKsoCCCkjlsEJrrMqDkYqs3M0aLOK2kxE9mbLm9M2.R0stAQYoDsGCDJxqDzG9WF3oa4rOCjEK7DbZXdBmBWnMrfErA3M_Q4y_mUTEQLqSAEeYFGlGeCXYsccnQMvEecxRg-&format=png', + 'title': 'GSR Ae', + }), + dict({ + 'can_expand': True, + 'can_play': False, + 'can_search': False, + 'children_media_class': None, + 'media_class': , + 'media_content_id': 'media-source://xbox/277923030577271', + 'media_content_type': , + 'thumbnail': 'https://images-eds-ssl.xboxlive.com/image?url=z951ykn43p4FqWbbFvR2Ec.8vbDhj8G2Xe7JngaTToBrrCmIEEXHC9UNrdJ6P7KIwuPiuIs6TLDV4WsQAGzSwgbaLlf_CiIdb0jJCiBao9CB40sjycuA5l0Jf.hfuP8l&format=png', + 'title': 'Iqnavs', + }), + ]), + 'children_media_class': , + 'media_class': , + 'media_content_id': 'media-source://xbox', + 'media_content_type': , + 'not_shown': 0, + 'thumbnail': None, + 'title': 'Xbox Game Media', + }) +# --- diff --git a/tests/components/xbox/snapshots/test_sensor.ambr b/tests/components/xbox/snapshots/test_sensor.ambr index b6c27ea8a6fbd..b8422dfdfa66f 100644 --- a/tests/components/xbox/snapshots/test_sensor.ambr +++ b/tests/components/xbox/snapshots/test_sensor.ambr @@ -195,6 +195,55 @@ 'state': '3802', }) # --- +# name: test_sensors[sensor.erics273_in_party-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.erics273_in_party', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'In party', + 'platform': 'xbox', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': '2533274913657542_in_party', + 'unit_of_measurement': 'people', + }) +# --- +# name: test_sensors[sensor.erics273_in_party-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'erics273 In party', + 'unit_of_measurement': 'people', + }), + 'context': , + 'entity_id': 'sensor.erics273_in_party', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- # name: test_sensors[sensor.erics273_last_online-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -301,6 +350,64 @@ 'state': 'unknown', }) # --- +# name: test_sensors[sensor.erics273_party_join_restrictions-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'invite_only', + 'joinable', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.erics273_party_join_restrictions', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Party join restrictions', + 'platform': 'xbox', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': '2533274913657542_join_restrictions', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.erics273_party_join_restrictions-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'erics273 Party join restrictions', + 'options': list([ + 'invite_only', + 'joinable', + ]), + }), + 'context': , + 'entity_id': 'sensor.erics273_party_join_restrictions', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_sensors[sensor.erics273_status-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -545,6 +652,55 @@ 'state': '27750', }) # --- +# name: test_sensors[sensor.gsr_ae_in_party-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.gsr_ae_in_party', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'In party', + 'platform': 'xbox', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': '271958441785640_in_party', + 'unit_of_measurement': 'people', + }) +# --- +# name: test_sensors[sensor.gsr_ae_in_party-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'GSR Ae In party', + 'unit_of_measurement': 'people', + }), + 'context': , + 'entity_id': 'sensor.gsr_ae_in_party', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2', + }) +# --- # name: test_sensors[sensor.gsr_ae_last_online-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -652,6 +808,64 @@ 'state': 'Blue Dragon', }) # --- +# name: test_sensors[sensor.gsr_ae_party_join_restrictions-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'invite_only', + 'joinable', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.gsr_ae_party_join_restrictions', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Party join restrictions', + 'platform': 'xbox', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': '271958441785640_join_restrictions', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.gsr_ae_party_join_restrictions-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'GSR Ae Party join restrictions', + 'options': list([ + 'invite_only', + 'joinable', + ]), + }), + 'context': , + 'entity_id': 'sensor.gsr_ae_party_join_restrictions', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'joinable', + }) +# --- # name: test_sensors[sensor.gsr_ae_status-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -896,6 +1110,55 @@ 'state': '27210', }) # --- +# name: test_sensors[sensor.ikken_hissatsuu_in_party-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ikken_hissatsuu_in_party', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'In party', + 'platform': 'xbox', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': '2533274838782903_in_party', + 'unit_of_measurement': 'people', + }) +# --- +# name: test_sensors[sensor.ikken_hissatsuu_in_party-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Ikken Hissatsuu In party', + 'unit_of_measurement': 'people', + }), + 'context': , + 'entity_id': 'sensor.ikken_hissatsuu_in_party', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- # name: test_sensors[sensor.ikken_hissatsuu_last_online-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -1002,6 +1265,64 @@ 'state': 'unknown', }) # --- +# name: test_sensors[sensor.ikken_hissatsuu_party_join_restrictions-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'invite_only', + 'joinable', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ikken_hissatsuu_party_join_restrictions', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Party join restrictions', + 'platform': 'xbox', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': '2533274838782903_join_restrictions', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.ikken_hissatsuu_party_join_restrictions-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Ikken Hissatsuu Party join restrictions', + 'options': list([ + 'invite_only', + 'joinable', + ]), + }), + 'context': , + 'entity_id': 'sensor.ikken_hissatsuu_party_join_restrictions', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'joinable', + }) +# --- # name: test_sensors[sensor.ikken_hissatsuu_status-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/xbox/test_media_source.py b/tests/components/xbox/test_media_source.py new file mode 100644 index 0000000000000..9c4e2a4fbe027 --- /dev/null +++ b/tests/components/xbox/test_media_source.py @@ -0,0 +1,428 @@ +"""Tests for the Xbox media source platform.""" + +import httpx +import pytest +from pythonxbox.api.provider.people.models import PeopleResponse +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.media_player import BrowseError +from homeassistant.components.media_source import ( + URI_SCHEME, + Unresolvable, + async_browse_media, + async_resolve_media, +) +from homeassistant.components.xbox.const import DOMAIN +from homeassistant.config_entries import ConfigEntryDisabler, ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import AsyncMock, MockConfigEntry, async_load_json_object_fixture +from tests.typing import MagicMock + + +@pytest.fixture(autouse=True) +async def setup_media_source(hass: HomeAssistant) -> None: + """Setup media source component.""" + + await async_setup_component(hass, "media_source", {}) + + +@pytest.mark.usefixtures("xbox_live_client") +@pytest.mark.freeze_time("2025-11-06T17:12:27") +async def test_browse_media( + hass: HomeAssistant, + config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test browsing media.""" + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + assert ( + await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}") + ).as_dict() == snapshot(name="games_view") + + assert ( + await async_browse_media( + hass, f"{URI_SCHEME}{DOMAIN}/271958441785640/1297287135" + ) + ).as_dict() == snapshot(name="category_view") + + assert ( + await async_browse_media( + hass, f"{URI_SCHEME}{DOMAIN}/271958441785640/1297287135/gameclips" + ) + ).as_dict() == snapshot(name="gameclips_view") + + assert ( + await async_browse_media( + hass, f"{URI_SCHEME}{DOMAIN}/271958441785640/1297287135/screenshots" + ) + ).as_dict() == snapshot(name="screenshots_view") + + assert ( + await async_browse_media( + hass, f"{URI_SCHEME}{DOMAIN}/271958441785640/1297287135/game_media" + ) + ).as_dict() == snapshot(name="game_media_view") + + assert ( + await async_browse_media( + hass, f"{URI_SCHEME}{DOMAIN}/271958441785640/1297287135/community_gameclips" + ) + ).as_dict() == snapshot(name="community_gameclips_view") + + assert ( + await async_browse_media( + hass, + f"{URI_SCHEME}{DOMAIN}/271958441785640/1297287135/community_screenshots", + ) + ).as_dict() == snapshot(name="community_screenshots_view") + + +async def test_browse_media_accounts( + hass: HomeAssistant, + config_entry: MockConfigEntry, + xbox_live_client: AsyncMock, + snapshot: SnapshotAssertion, +) -> None: + """Test browsing media we get account view if more than 1 account is configured.""" + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + xbox_live_client.people.get_friends_by_xuid.return_value = PeopleResponse( + **(await async_load_json_object_fixture(hass, "people_batch2.json", DOMAIN)) # type: ignore[reportArgumentType] + ) + + config_entry2 = MockConfigEntry( + domain=DOMAIN, + title="Iqnavs", + data={ + "auth_implementation": "cloud", + "token": { + "access_token": "1234567890", + "expires_at": 1760697327.7298331, + "expires_in": 3600, + "refresh_token": "0987654321", + "scope": "XboxLive.signin XboxLive.offline_access", + "service": "xbox", + "token_type": "bearer", + "user_id": "AAAAAAAAAAAAAAAAAAAAA", + }, + }, + unique_id="277923030577271", + minor_version=2, + ) + config_entry2.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry2.entry_id) + await hass.async_block_till_done() + + assert config_entry2.state is ConfigEntryState.LOADED + + assert len(hass.config_entries.async_loaded_entries(DOMAIN)) == 2 + + assert ( + await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}") + ).as_dict() == snapshot + + +@pytest.mark.parametrize( + ("media_content_id", "provider", "method"), + [ + ("", "titlehub", "get_title_history"), + ("/271958441785640", "titlehub", "get_title_history"), + ("/271958441785640/1297287135", "titlehub", "get_title_info"), + ( + "/271958441785640/1297287135/gameclips", + "gameclips", + "get_recent_clips_by_xuid", + ), + ( + "/271958441785640/1297287135/screenshots", + "screenshots", + "get_recent_screenshots_by_xuid", + ), + ( + "/271958441785640/1297287135/game_media", + "titlehub", + "get_title_info", + ), + ], +) +@pytest.mark.parametrize( + "exception", + [ + httpx.HTTPStatusError("", request=MagicMock(), response=httpx.Response(500)), + httpx.RequestError(""), + httpx.TimeoutException(""), + ], +) +async def test_browse_media_exceptions( + hass: HomeAssistant, + config_entry: MockConfigEntry, + xbox_live_client: AsyncMock, + media_content_id: str, + provider: str, + method: str, + exception: Exception, +) -> None: + """Test browsing media exceptions.""" + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + provider = getattr(xbox_live_client, provider) + getattr(provider, method).side_effect = exception + + with pytest.raises(BrowseError): + await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}{media_content_id}") + + +@pytest.mark.usefixtures("xbox_live_client") +async def test_browse_media_not_configured_exception( + hass: HomeAssistant, +) -> None: + """Test browsing media integration not configured exception.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + title="Mock title", + data={ + "auth_implementation": "cloud", + "token": { + "access_token": "1234567890", + "expires_at": 1760697327.7298331, + "expires_in": 3600, + "refresh_token": "0987654321", + "scope": "XboxLive.signin XboxLive.offline_access", + "service": "xbox", + "token_type": "bearer", + "user_id": "AAAAAAAAAAAAAAAAAAAAA", + }, + }, + unique_id="2533274838782903", + disabled_by=ConfigEntryDisabler.USER, + minor_version=2, + ) + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.NOT_LOADED + + with pytest.raises(BrowseError, match="The Xbox integration is not configured"): + await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}") + + +@pytest.mark.usefixtures("xbox_live_client") +async def test_browse_media_account_not_configured_exception( + hass: HomeAssistant, + config_entry: MockConfigEntry, +) -> None: + """Test browsing media account not configured exception.""" + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + with pytest.raises(BrowseError): + await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/2533274838782903") + + +@pytest.mark.parametrize( + ("media_content_id", "url", "mime_type"), + [ + ( + "/271958441785640/1297287135/screenshots/41593644-be22-43d6-b224-c7bebe14076e", + "https://screenshotscontent-d5001.xboxlive.com/00097bbbbbbbbb23-41593644-be22-43d6-b224-c7bebe14076e/Screenshot-Original.png?sv=2015-12-11&sr=b&si=DefaultAccess&sig=ALKo3DE2HXqBTlpdyynIrH6RPKIECOF7zwotH%2Bb30Ts%3D", + "image/png", + ), + ( + "/271958441785640/1297287135/gameclips/f87cc6ac-c291-4998-9124-d8b36c059b6a", + "https://gameclipscontent-d2015.xboxlive.com/asset-d5448dbd-f45e-46ab-ae4e-a2e205a70e7c/GameClip-Original.MP4?sv=2015-12-11&sr=b&si=DefaultAccess&sig=ArSoLvy9EnQeBthGW6%2FbasedHHk0Jb6iXjI3EMq8oh8%3D&__gda__=1522241341_69f67a7a3533626ae90b52845664dc0c", + "video/mp4", + ), + ( + "/271958441785640/1297287135/game_media/0", + "http://store-images.s-microsoft.com/image/apps.35725.65457035095819016.56f55216-1bb9-40aa-8796-068cf3075fc1.c4bf34f8-ad40-4af3-914e-a85e75a76bed", + "image/png", + ), + ], + ids=["screenshot", "gameclips", "game_media"], +) +@pytest.mark.usefixtures("xbox_live_client") +async def test_resolve_media( + hass: HomeAssistant, + config_entry: MockConfigEntry, + media_content_id: str, + url: str, + mime_type: str, +) -> None: + """Test resolve media.""" + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + media = await async_resolve_media( + hass, + f"{URI_SCHEME}{DOMAIN}{media_content_id}", + None, + ) + assert media.url == url + assert media.mime_type == mime_type + + +@pytest.mark.parametrize( + ("media_content_id", "provider", "method"), + [ + ( + "/271958441785640/1297287135/screenshots/41593644-be22-43d6-b224-c7bebe14076e", + "screenshots", + "get_recent_screenshots_by_xuid", + ), + ( + "/271958441785640/1297287135/gameclips/f87cc6ac-c291-4998-9124-d8b36c059b6a", + "gameclips", + "get_recent_clips_by_xuid", + ), + ( + "/271958441785640/1297287135/game_media/0", + "titlehub", + "get_title_info", + ), + ], +) +@pytest.mark.parametrize( + "exception", + [ + httpx.HTTPStatusError("", request=MagicMock(), response=httpx.Response(500)), + httpx.RequestError(""), + httpx.TimeoutException(""), + ], +) +async def test_resolve_media_exceptions( + hass: HomeAssistant, + config_entry: MockConfigEntry, + xbox_live_client: AsyncMock, + media_content_id: str, + provider: str, + method: str, + exception: Exception, +) -> None: + """Test resolve media exceptions.""" + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + provider = getattr(xbox_live_client, provider) + getattr(provider, method).side_effect = exception + + with pytest.raises(Unresolvable): + await async_resolve_media( + hass, + f"{URI_SCHEME}{DOMAIN}{media_content_id}", + None, + ) + + +@pytest.mark.parametrize(("media_type"), ["screenshots", "gameclips", "game_media"]) +@pytest.mark.usefixtures("xbox_live_client") +async def test_resolve_media_not_found_exceptions( + hass: HomeAssistant, + config_entry: MockConfigEntry, + media_type: str, +) -> None: + """Test resolve media not found exceptions.""" + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + with pytest.raises(Unresolvable, match="The requested media could not be found"): + await async_resolve_media( + hass, + f"{URI_SCHEME}{DOMAIN}/271958441785640/1297287135/{media_type}/12345", + None, + ) + + +@pytest.mark.usefixtures("xbox_live_client") +async def test_resolve_media_not_configured( + hass: HomeAssistant, +) -> None: + """Test resolve media integration not configured exception.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + title="Mock title", + data={ + "auth_implementation": "cloud", + "token": { + "access_token": "1234567890", + "expires_at": 1760697327.7298331, + "expires_in": 3600, + "refresh_token": "0987654321", + "scope": "XboxLive.signin XboxLive.offline_access", + "service": "xbox", + "token_type": "bearer", + "user_id": "AAAAAAAAAAAAAAAAAAAAA", + }, + }, + unique_id="2533274838782903", + disabled_by=ConfigEntryDisabler.USER, + ) + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.NOT_LOADED + + with pytest.raises(Unresolvable, match="The Xbox integration is not configured"): + await async_resolve_media( + hass, + f"{URI_SCHEME}{DOMAIN}/2533274838782903", + None, + ) + + +@pytest.mark.usefixtures("xbox_live_client") +async def test_resolve_media_account_not_configured( + hass: HomeAssistant, + config_entry: MockConfigEntry, +) -> None: + """Test resolve media account not configured exception.""" + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + with pytest.raises(Unresolvable, match="The Xbox account is not configured"): + await async_resolve_media( + hass, + f"{URI_SCHEME}{DOMAIN}/2533274838782903", + None, + ) diff --git a/tests/components/yale_smart_alarm/test_button.py b/tests/components/yale_smart_alarm/test_button.py index cb28e60ab22af..781ee614864c4 100644 --- a/tests/components/yale_smart_alarm/test_button.py +++ b/tests/components/yale_smart_alarm/test_button.py @@ -4,7 +4,6 @@ from unittest.mock import Mock -from freezegun import freeze_time import pytest from syrupy.assertion import SnapshotAssertion from yalesmartalarmclient.exceptions import UnknownError @@ -18,7 +17,7 @@ from tests.common import MockConfigEntry, snapshot_platform -@freeze_time("2024-04-29T18:00:00.612351+00:00") +@pytest.mark.freeze_time("2024-04-29T18:00:00.612351+00:00") @pytest.mark.parametrize( "load_platforms", [[Platform.BUTTON]], diff --git a/tests/components/zha/test_button.py b/tests/components/zha/test_button.py index 8c41a914f5a62..ad88e84fd3aac 100644 --- a/tests/components/zha/test_button.py +++ b/tests/components/zha/test_button.py @@ -3,7 +3,6 @@ from collections.abc import Callable, Coroutine from unittest.mock import patch -from freezegun import freeze_time import pytest from zigpy.const import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE from zigpy.device import Device @@ -46,23 +45,29 @@ def button_platform_only(): @pytest.fixture -async def setup_zha_integration( - hass: HomeAssistant, setup_zha: Callable[..., Coroutine[None]] -): - """Set up ZHA component.""" - - # if we call this in the test itself the test hangs forever - await setup_zha() +def speed_up_radio_mgr(): + """Speed up the radio manager connection time by removing delays. + + This fixture replaces the fixture in conftest.py by patching the connect + and shutdown delays to 0 to allow waiting for the patched delays when + running tests with time frozen, which otherwise blocks forever. + """ + with ( + patch("homeassistant.components.zha.radio_manager.CONNECT_DELAY_S", 0), + patch("zha.application.gateway.SHUT_DOWN_DELAY_S", 0), + ): + yield -@freeze_time("2021-11-04 17:37:00", tz_offset=-1) +@pytest.mark.freeze_time("2021-11-04 17:37:00", tz_offset=-1) async def test_button( hass: HomeAssistant, entity_registry: er.EntityRegistry, - setup_zha_integration, # pylint: disable=unused-argument + setup_zha: Callable[..., Coroutine[None]], zigpy_device_mock: Callable[..., Device], ) -> None: """Test ZHA button platform.""" + await setup_zha() gateway = get_zha_gateway(hass) gateway_proxy: ZHAGatewayProxy = get_zha_gateway_proxy(hass) diff --git a/tests/components/zha/test_websocket_api.py b/tests/components/zha/test_websocket_api.py index df945b8afc2bb..34152a3f36b5d 100644 --- a/tests/components/zha/test_websocket_api.py +++ b/tests/components/zha/test_websocket_api.py @@ -8,7 +8,6 @@ from typing import TYPE_CHECKING from unittest.mock import ANY, AsyncMock, MagicMock, call, patch -from freezegun import freeze_time import pytest import voluptuous as vol from zha.application.const import ( @@ -94,6 +93,21 @@ def required_platform_only(): yield +@pytest.fixture +def speed_up_radio_mgr(): + """Speed up the radio manager connection time by removing delays. + + This fixture replaces the fixture in conftest.py by patching the connect + and shutdown delays to 0 to allow waiting for the patched delays when + running tests with time frozen, which otherwise blocks forever. + """ + with ( + patch("homeassistant.components.zha.radio_manager.CONNECT_DELAY_S", 0), + patch("zha.application.gateway.SHUT_DOWN_DELAY_S", 0), + ): + yield + + @pytest.fixture async def zha_client( hass: HomeAssistant, @@ -217,7 +231,7 @@ async def test_device_cluster_commands(zha_client) -> None: assert command[TYPE] is not None -@freeze_time("2023-09-23 20:16:00+00:00") +@pytest.mark.freeze_time("2023-09-23 20:16:00+00:00") async def test_list_devices(zha_client) -> None: """Test getting ZHA devices.""" await zha_client.send_json({ID: 5, TYPE: "zha/devices"})