diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 3c926db79d4..069bb64011d 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -7,9 +7,9 @@ "quality_scale": "internal", "requirements": [ "bleak==0.19.2", - "bleak-retry-connector==2.10.1", + "bleak-retry-connector==2.10.2", "bluetooth-adapters==0.12.0", - "bluetooth-auto-recovery==0.5.5", + "bluetooth-auto-recovery==1.0.0", "bluetooth-data-tools==0.3.0", "dbus-fast==1.75.0" ], diff --git a/homeassistant/components/bluetooth/scanner.py b/homeassistant/components/bluetooth/scanner.py index 09032715c74..d56c3f76c4c 100644 --- a/homeassistant/components/bluetooth/scanner.py +++ b/homeassistant/components/bluetooth/scanner.py @@ -130,6 +130,7 @@ class HaScanner(BaseHaScanner): new_info_callback: Callable[[BluetoothServiceInfoBleak], None], ) -> None: """Init bluetooth discovery.""" + self.mac_address = address source = address if address != DEFAULT_ADDRESS else adapter or SOURCE_LOCAL super().__init__(hass, source, adapter) self.mode = mode @@ -375,7 +376,7 @@ class HaScanner(BaseHaScanner): # so we log at debug level. If we later come up with a repair # strategy, we will change this to raise a repair issue as well. _LOGGER.debug("%s: adapter stopped responding; executing reset", self.name) - result = await async_reset_adapter(self.adapter) + result = await async_reset_adapter(self.adapter, self.mac_address) _LOGGER.debug("%s: adapter reset result: %s", self.name, result) async def async_stop(self) -> None: diff --git a/homeassistant/components/bluetooth/util.py b/homeassistant/components/bluetooth/util.py index c2336dd7af0..e3f44daff2b 100644 --- a/homeassistant/components/bluetooth/util.py +++ b/homeassistant/components/bluetooth/util.py @@ -36,9 +36,9 @@ def async_load_history_from_system( } -async def async_reset_adapter(adapter: str | None) -> bool | None: +async def async_reset_adapter(adapter: str | None, mac_address: str) -> bool | None: """Reset the adapter.""" if adapter and adapter.startswith("hci"): adapter_id = int(adapter[3:]) - return await recover_adapter(adapter_id) + return await recover_adapter(adapter_id, mac_address) return False diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index 00688f642ad..6661614b9cb 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -3,7 +3,7 @@ "name": "Google Cast", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/cast", - "requirements": ["pychromecast==13.0.2"], + "requirements": ["pychromecast==13.0.3"], "after_dependencies": [ "cloud", "http", diff --git a/homeassistant/components/cloud/repairs.py b/homeassistant/components/cloud/repairs.py index 0d217521c21..bf2df23aca9 100644 --- a/homeassistant/components/cloud/repairs.py +++ b/homeassistant/components/cloud/repairs.py @@ -13,7 +13,7 @@ from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import issue_registry as ir from .const import DOMAIN -from .subscription import async_subscription_info +from .subscription import async_migrate_paypal_agreement, async_subscription_info BACKOFF_TIME = 5 MAX_RETRIES = 60 # This allows for 10 minutes of retries @@ -68,13 +68,13 @@ class LegacySubscriptionRepairFlow(RepairsFlow): async def async_step_change_plan(self, _: None = None) -> FlowResult: """Wait for the user to authorize the app installation.""" + cloud: Cloud = self.hass.data[DOMAIN] + async def _async_wait_for_plan_change() -> None: flow_manager = repairs_flow_manager(self.hass) # We can not get here without a flow manager assert flow_manager is not None - cloud: Cloud = self.hass.data[DOMAIN] - retries = 0 while retries < MAX_RETRIES: self._data = await async_subscription_info(cloud) @@ -90,9 +90,10 @@ class LegacySubscriptionRepairFlow(RepairsFlow): if not self.wait_task: self.wait_task = self.hass.async_create_task(_async_wait_for_plan_change()) + migration = await async_migrate_paypal_agreement(cloud) return self.async_external_step( step_id="change_plan", - url="https://account.nabucasa.com/", + url=migration["url"] if migration else "https://account.nabucasa.com/", ) await self.wait_task diff --git a/homeassistant/components/cloud/subscription.py b/homeassistant/components/cloud/subscription.py index 9a2e5bd87cf..4c18b4f0253 100644 --- a/homeassistant/components/cloud/subscription.py +++ b/homeassistant/components/cloud/subscription.py @@ -1,6 +1,7 @@ """Subscription information.""" from __future__ import annotations +import asyncio import logging from typing import Any @@ -18,7 +19,28 @@ async def async_subscription_info(cloud: Cloud) -> dict[str, Any] | None: try: async with async_timeout.timeout(REQUEST_TIMEOUT): return await cloud_api.async_subscription_info(cloud) + except asyncio.TimeoutError: + _LOGGER.error( + "A timeout of %s was reached while trying to fetch subscription information", + REQUEST_TIMEOUT, + ) except ClientError: _LOGGER.error("Failed to fetch subscription information") return None + + +async def async_migrate_paypal_agreement(cloud: Cloud) -> dict[str, Any] | None: + """Migrate a paypal agreement from legacy.""" + try: + async with async_timeout.timeout(REQUEST_TIMEOUT): + return await cloud_api.async_migrate_paypal_agreement(cloud) + except asyncio.TimeoutError: + _LOGGER.error( + "A timeout of %s was reached while trying to start agreement migration", + REQUEST_TIMEOUT, + ) + except ClientError as exception: + _LOGGER.error("Failed to start agreement migration - %s", exception) + + return None diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index 40d170db3b3..43bd0bfeeb0 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations from abc import ABC, abstractmethod +from xml.etree.ElementTree import ParseError from pyfritzhome import Fritzhome, FritzhomeDevice, LoginError from pyfritzhome.devicetypes.fritzhomeentitybase import FritzhomeEntityBase @@ -43,7 +44,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: CONF_CONNECTIONS: fritz, } - coordinator = FritzboxDataUpdateCoordinator(hass, entry) + try: + await hass.async_add_executor_job(fritz.update_templates) + except ParseError: + LOGGER.debug("Disable smarthome templates") + has_templates = False + else: + LOGGER.debug("Enable smarthome templates") + has_templates = True + + coordinator = FritzboxDataUpdateCoordinator(hass, entry, has_templates) await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/fritzbox/coordinator.py b/homeassistant/components/fritzbox/coordinator.py index 6bc3bac623f..80087adf9ac 100644 --- a/homeassistant/components/fritzbox/coordinator.py +++ b/homeassistant/components/fritzbox/coordinator.py @@ -3,7 +3,6 @@ from __future__ import annotations from dataclasses import dataclass from datetime import timedelta -from xml.etree.ElementTree import ParseError from pyfritzhome import Fritzhome, FritzhomeDevice, LoginError from pyfritzhome.devicetypes import FritzhomeTemplate @@ -30,17 +29,14 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat configuration_url: str - def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + def __init__( + self, hass: HomeAssistant, entry: ConfigEntry, has_templates: bool + ) -> None: """Initialize the Fritzbox Smarthome device coordinator.""" self.entry = entry self.fritz: Fritzhome = hass.data[DOMAIN][self.entry.entry_id][CONF_CONNECTIONS] self.configuration_url = self.fritz.get_prefixed_host() - self.has_templates = True - try: - hass.async_add_executor_job(self.fritz.update_templates) - except ParseError: - LOGGER.info("Disable smarthome templates") - self.has_templates = False + self.has_templates = has_templates super().__init__( hass, diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 57a31456c3c..e1c12272db4 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20221208.0"], + "requirements": ["home-assistant-frontend==20221212.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/components/homekit/type_remotes.py b/homeassistant/components/homekit/type_remotes.py index fb808eff8b0..9cc90a6cc28 100644 --- a/homeassistant/components/homekit/type_remotes.py +++ b/homeassistant/components/homekit/type_remotes.py @@ -18,7 +18,7 @@ from homeassistant.const import ( SERVICE_TURN_ON, STATE_ON, ) -from homeassistant.core import callback +from homeassistant.core import State, callback from .accessories import TYPES, HomeAccessory from .const import ( @@ -96,7 +96,7 @@ class RemoteInputSelectAccessory(HomeAccessory): self.sources = [] self.support_select_source = False if features & required_feature: - sources = state.attributes.get(source_list_key, []) + sources = self._get_ordered_source_list_from_state(state) if len(sources) > MAXIMUM_SOURCES: _LOGGER.warning( "%s: Reached maximum number of sources (%s)", @@ -143,6 +143,21 @@ class RemoteInputSelectAccessory(HomeAccessory): serv_input.configure_char(CHAR_CURRENT_VISIBILITY_STATE, value=False) _LOGGER.debug("%s: Added source %s", self.entity_id, source) + def _get_ordered_source_list_from_state(self, state: State) -> list[str]: + """Return ordered source list while preserving order with duplicates removed. + + Some integrations have duplicate sources in the source list + which will make the source list conflict as HomeKit requires + unique source names. + """ + seen = set() + sources: list[str] = [] + for source in state.attributes.get(self.source_list_key, []): + if source not in seen: + sources.append(source) + seen.add(source) + return sources + @abstractmethod def set_on_off(self, value): """Move switch state to value if call came from HomeKit.""" @@ -169,7 +184,7 @@ class RemoteInputSelectAccessory(HomeAccessory): self.char_input_source.set_value(index) return - possible_sources = new_state.attributes.get(self.source_list_key, []) + possible_sources = self._get_ordered_source_list_from_state(new_state) if source in possible_sources: index = possible_sources.index(source) if index >= MAXIMUM_SOURCES: diff --git a/homeassistant/components/overkiz/manifest.json b/homeassistant/components/overkiz/manifest.json index 49a65b78c06..864c34714aa 100644 --- a/homeassistant/components/overkiz/manifest.json +++ b/homeassistant/components/overkiz/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "integration_type": "hub", "documentation": "https://www.home-assistant.io/integrations/overkiz", - "requirements": ["pyoverkiz==1.7.1"], + "requirements": ["pyoverkiz==1.7.2"], "zeroconf": [ { "type": "_kizbox._tcp.local.", diff --git a/homeassistant/components/overkiz/water_heater_entities/domestic_hot_water_production.py b/homeassistant/components/overkiz/water_heater_entities/domestic_hot_water_production.py index 49524e19373..2a8d93bf8eb 100644 --- a/homeassistant/components/overkiz/water_heater_entities/domestic_hot_water_production.py +++ b/homeassistant/components/overkiz/water_heater_entities/domestic_hot_water_production.py @@ -305,14 +305,6 @@ class DomesticHotWaterProduction(OverkizEntity, WaterHeaterEntity): OverkizCommand.SET_BOOST_MODE, OverkizCommand.OFF ) - if self.executor.has_command(OverkizCommand.SET_BOOST_MODE_DURATION): - await self.executor.async_execute_command( - OverkizCommand.SET_BOOST_MODE_DURATION, 0 - ) - await self.executor.async_execute_command( - OverkizCommand.REFRESH_BOOST_MODE_DURATION - ) - if self.executor.has_command(OverkizCommand.SET_CURRENT_OPERATING_MODE): current_operating_mode = self.executor.select_state( OverkizState.CORE_OPERATING_MODE @@ -331,5 +323,10 @@ class DomesticHotWaterProduction(OverkizEntity, WaterHeaterEntity): OverkizCommand.SET_DHW_MODE, self.overkiz_to_operation_mode[operation_mode] ) + if self.executor.has_command(OverkizCommand.REFRESH_BOOST_MODE_DURATION): + await self.executor.async_execute_command( + OverkizCommand.REFRESH_BOOST_MODE_DURATION + ) + if self.executor.has_command(OverkizCommand.REFRESH_DHW_MODE): await self.executor.async_execute_command(OverkizCommand.REFRESH_DHW_MODE) diff --git a/homeassistant/components/sleepiq/config_flow.py b/homeassistant/components/sleepiq/config_flow.py index 16034b64e8b..77806a1f977 100644 --- a/homeassistant/components/sleepiq/config_flow.py +++ b/homeassistant/components/sleepiq/config_flow.py @@ -85,7 +85,7 @@ class SleepIQFlowHandler(ConfigFlow, domain=DOMAIN): self._reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] ) - return await self.async_step_reauth_confirm(dict(entry_data)) + return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 50a30142bc5..ef5dbb91346 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -7,7 +7,7 @@ "bellows==0.34.5", "pyserial==3.5", "pyserial-asyncio==0.6", - "zha-quirks==0.0.88", + "zha-quirks==0.0.89", "zigpy-deconz==0.19.2", "zigpy==0.52.3", "zigpy-xbee==0.16.2", diff --git a/homeassistant/const.py b/homeassistant/const.py index c99e823ca59..74e090d166e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 12 -PATCH_VERSION: Final = "3" +PATCH_VERSION: Final = "4" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 6e8f3a511dd..f9c4c4dd2f6 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,10 +10,10 @@ atomicwrites-homeassistant==1.4.1 attrs==21.2.0 awesomeversion==22.9.0 bcrypt==3.1.7 -bleak-retry-connector==2.10.1 +bleak-retry-connector==2.10.2 bleak==0.19.2 bluetooth-adapters==0.12.0 -bluetooth-auto-recovery==0.5.5 +bluetooth-auto-recovery==1.0.0 bluetooth-data-tools==0.3.0 certifi>=2021.5.30 ciso8601==2.2.0 @@ -22,7 +22,7 @@ dbus-fast==1.75.0 fnvhash==0.1.0 hass-nabucasa==0.61.0 home-assistant-bluetooth==1.8.1 -home-assistant-frontend==20221208.0 +home-assistant-frontend==20221212.0 httpx==0.23.1 ifaddr==0.1.7 janus==1.0.0 diff --git a/pyproject.toml b/pyproject.toml index 125c7b77e9e..8c8399bf425 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.12.3" +version = "2022.12.4" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" diff --git a/requirements_all.txt b/requirements_all.txt index 802db40dfd5..b088ec3104a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -422,7 +422,7 @@ bimmer_connected==0.10.4 bizkaibus==0.1.1 # homeassistant.components.bluetooth -bleak-retry-connector==2.10.1 +bleak-retry-connector==2.10.2 # homeassistant.components.bluetooth bleak==0.19.2 @@ -450,7 +450,7 @@ bluemaestro-ble==0.2.0 bluetooth-adapters==0.12.0 # homeassistant.components.bluetooth -bluetooth-auto-recovery==0.5.5 +bluetooth-auto-recovery==1.0.0 # homeassistant.components.bluetooth # homeassistant.components.led_ble @@ -884,7 +884,7 @@ hole==0.7.0 holidays==0.17.2 # homeassistant.components.frontend -home-assistant-frontend==20221208.0 +home-assistant-frontend==20221212.0 # homeassistant.components.home_connect homeconnect==0.7.2 @@ -1504,7 +1504,7 @@ pycfdns==2.0.1 pychannels==1.2.3 # homeassistant.components.cast -pychromecast==13.0.2 +pychromecast==13.0.3 # homeassistant.components.pocketcasts pycketcasts==1.0.1 @@ -1812,7 +1812,7 @@ pyotgw==2.1.3 pyotp==2.7.0 # homeassistant.components.overkiz -pyoverkiz==1.7.1 +pyoverkiz==1.7.2 # homeassistant.components.openweathermap pyowm==3.2.0 @@ -2639,7 +2639,7 @@ zengge==0.2 zeroconf==0.39.4 # homeassistant.components.zha -zha-quirks==0.0.88 +zha-quirks==0.0.89 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0fd60298193..3f857e1839b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -346,7 +346,7 @@ bellows==0.34.5 bimmer_connected==0.10.4 # homeassistant.components.bluetooth -bleak-retry-connector==2.10.1 +bleak-retry-connector==2.10.2 # homeassistant.components.bluetooth bleak==0.19.2 @@ -364,7 +364,7 @@ bluemaestro-ble==0.2.0 bluetooth-adapters==0.12.0 # homeassistant.components.bluetooth -bluetooth-auto-recovery==0.5.5 +bluetooth-auto-recovery==1.0.0 # homeassistant.components.bluetooth # homeassistant.components.led_ble @@ -664,7 +664,7 @@ hole==0.7.0 holidays==0.17.2 # homeassistant.components.frontend -home-assistant-frontend==20221208.0 +home-assistant-frontend==20221212.0 # homeassistant.components.home_connect homeconnect==0.7.2 @@ -1077,7 +1077,7 @@ pybravia==0.2.3 pycfdns==2.0.1 # homeassistant.components.cast -pychromecast==13.0.2 +pychromecast==13.0.3 # homeassistant.components.comfoconnect pycomfoconnect==0.4 @@ -1289,7 +1289,7 @@ pyotgw==2.1.3 pyotp==2.7.0 # homeassistant.components.overkiz -pyoverkiz==1.7.1 +pyoverkiz==1.7.2 # homeassistant.components.openweathermap pyowm==3.2.0 @@ -1840,7 +1840,7 @@ zamg==0.1.1 zeroconf==0.39.4 # homeassistant.components.zha -zha-quirks==0.0.88 +zha-quirks==0.0.89 # homeassistant.components.zha zigpy-deconz==0.19.2 diff --git a/tests/components/cloud/test_repairs.py b/tests/components/cloud/test_repairs.py index 052cdde0d0d..a7f8b2332d7 100644 --- a/tests/components/cloud/test_repairs.py +++ b/tests/components/cloud/test_repairs.py @@ -88,6 +88,10 @@ async def test_legacy_subscription_repair_flow( "https://accounts.nabucasa.com/payments/subscription_info", json={"provider": None}, ) + aioclient_mock.post( + "https://accounts.nabucasa.com/payments/migrate_paypal_agreement", + json={"url": "https://paypal.com"}, + ) cloud_repairs.async_manage_legacy_subscription_issue(hass, {"provider": "legacy"}) repair_issue = issue_registry.async_get_issue( @@ -133,7 +137,7 @@ async def test_legacy_subscription_repair_flow( "flow_id": flow_id, "handler": DOMAIN, "step_id": "change_plan", - "url": "https://account.nabucasa.com/", + "url": "https://paypal.com", "description_placeholders": None, } @@ -161,8 +165,15 @@ async def test_legacy_subscription_repair_flow( async def test_legacy_subscription_repair_flow_timeout( hass: HomeAssistant, hass_client: Callable[..., Awaitable[ClientSession]], + mock_auth: Generator[None, AsyncMock, None], + aioclient_mock: AiohttpClientMocker, ): """Test timeout flow of the fix flow for legacy subscription.""" + aioclient_mock.post( + "https://accounts.nabucasa.com/payments/migrate_paypal_agreement", + status=403, + ) + issue_registry: ir.IssueRegistry = ir.async_get(hass) cloud_repairs.async_manage_legacy_subscription_issue(hass, {"provider": "legacy"}) diff --git a/tests/components/cloud/test_subscription.py b/tests/components/cloud/test_subscription.py new file mode 100644 index 00000000000..4dac93e92a3 --- /dev/null +++ b/tests/components/cloud/test_subscription.py @@ -0,0 +1,61 @@ +"""Test cloud subscription functions.""" +import asyncio +from unittest.mock import AsyncMock, Mock + +from hass_nabucasa import Cloud +import pytest + +from homeassistant.components.cloud.subscription import ( + async_migrate_paypal_agreement, + async_subscription_info, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from tests.test_util.aiohttp import AiohttpClientMocker + + +@pytest.fixture(name="mocked_cloud") +def mocked_cloud_object(hass: HomeAssistant) -> Cloud: + """Mock cloud object.""" + return Mock( + accounts_server="accounts.nabucasa.com", + auth=Mock(async_check_token=AsyncMock()), + websession=async_get_clientsession(hass), + ) + + +async def test_fetching_subscription_with_timeout_error( + aioclient_mock: AiohttpClientMocker, + caplog: pytest.LogCaptureFixture, + mocked_cloud: Cloud, +): + """Test that we handle timeout error.""" + aioclient_mock.get( + "https://accounts.nabucasa.com/payments/subscription_info", + exc=asyncio.TimeoutError(), + ) + + assert await async_subscription_info(mocked_cloud) is None + assert ( + "A timeout of 10 was reached while trying to fetch subscription information" + in caplog.text + ) + + +async def test_migrate_paypal_agreement_with_timeout_error( + aioclient_mock: AiohttpClientMocker, + caplog: pytest.LogCaptureFixture, + mocked_cloud: Cloud, +): + """Test that we handle timeout error.""" + aioclient_mock.post( + "https://accounts.nabucasa.com/payments/migrate_paypal_agreement", + exc=asyncio.TimeoutError(), + ) + + assert await async_migrate_paypal_agreement(mocked_cloud) is None + assert ( + "A timeout of 10 was reached while trying to start agreement migration" + in caplog.text + ) diff --git a/tests/components/homekit/test_type_media_players.py b/tests/components/homekit/test_type_media_players.py index 30b9bc77f5d..e815a25ee7d 100644 --- a/tests/components/homekit/test_type_media_players.py +++ b/tests/components/homekit/test_type_media_players.py @@ -512,3 +512,48 @@ async def test_media_player_television_max_sources(hass, hk_driver, events, capl ) await hass.async_block_till_done() assert acc.char_input_source.value == 0 + + +async def test_media_player_television_duplicate_sources( + hass, hk_driver, events, caplog +): + """Test if television accessory with duplicate sources.""" + entity_id = "media_player.television" + sources = ["MUSIC", "HDMI", "SCREEN MIRRORING", "HDMI", "MUSIC"] + hass.states.async_set( + entity_id, + None, + { + ATTR_DEVICE_CLASS: MediaPlayerDeviceClass.TV, + ATTR_SUPPORTED_FEATURES: 3469, + ATTR_MEDIA_VOLUME_MUTED: False, + ATTR_INPUT_SOURCE: "HDMI", + ATTR_INPUT_SOURCE_LIST: sources, + }, + ) + await hass.async_block_till_done() + acc = TelevisionMediaPlayer(hass, hk_driver, "MediaPlayer", entity_id, 2, None) + await acc.run() + await hass.async_block_till_done() + + assert acc.aid == 2 + assert acc.category == 31 # Television + + assert acc.char_active.value == 0 + assert acc.char_remote_key.value == 0 + assert acc.char_input_source.value == 1 + assert acc.char_mute.value is False + + hass.states.async_set( + entity_id, + None, + { + ATTR_DEVICE_CLASS: MediaPlayerDeviceClass.TV, + ATTR_SUPPORTED_FEATURES: 3469, + ATTR_MEDIA_VOLUME_MUTED: False, + ATTR_INPUT_SOURCE: "MUSIC", + ATTR_INPUT_SOURCE_LIST: sources, + }, + ) + await hass.async_block_till_done() + assert acc.char_input_source.value == 0