This commit is contained in:
Paulus Schoutsen 2022-12-12 22:36:56 -05:00 committed by GitHub
commit 05c429bcd7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 211 additions and 52 deletions

View File

@ -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"
],

View File

@ -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:

View File

@ -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

View File

@ -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",

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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,

View File

@ -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",

View File

@ -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:

View File

@ -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.",

View File

@ -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)

View File

@ -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

View File

@ -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",

View File

@ -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)

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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"})

View File

@ -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
)

View File

@ -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