From d3831bae4ea59f4058b7e5bf39bdba3e0777d5de Mon Sep 17 00:00:00 2001 From: Tom Brien Date: Thu, 8 Aug 2024 11:26:03 +0100 Subject: [PATCH 001/271] Add support for v3 Coinbase API (#116345) * Add support for v3 Coinbase API * Add deps * Move tests --- homeassistant/components/coinbase/__init__.py | 103 ++++++++++++++---- .../components/coinbase/config_flow.py | 45 +++++--- homeassistant/components/coinbase/const.py | 11 +- .../components/coinbase/manifest.json | 2 +- homeassistant/components/coinbase/sensor.py | 69 +++++++----- requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/coinbase/common.py | 68 +++++++++++- tests/components/coinbase/const.py | 28 +++++ .../coinbase/snapshots/test_diagnostics.ambr | 33 ++---- tests/components/coinbase/test_config_flow.py | 90 ++++++++++++++- 11 files changed, 359 insertions(+), 96 deletions(-) diff --git a/homeassistant/components/coinbase/__init__.py b/homeassistant/components/coinbase/__init__.py index 0a34168b4ee..0181c12a2e7 100644 --- a/homeassistant/components/coinbase/__init__.py +++ b/homeassistant/components/coinbase/__init__.py @@ -5,7 +5,9 @@ from __future__ import annotations from datetime import timedelta import logging -from coinbase.wallet.client import Client +from coinbase.rest import RESTClient +from coinbase.rest.rest_base import HTTPError +from coinbase.wallet.client import Client as LegacyClient from coinbase.wallet.error import AuthenticationError from homeassistant.config_entries import ConfigEntry @@ -15,8 +17,23 @@ from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.util import Throttle from .const import ( + ACCOUNT_IS_VAULT, + API_ACCOUNT_AMOUNT, + API_ACCOUNT_AVALIABLE, + API_ACCOUNT_BALANCE, + API_ACCOUNT_CURRENCY, + API_ACCOUNT_CURRENCY_CODE, + API_ACCOUNT_HOLD, API_ACCOUNT_ID, - API_ACCOUNTS_DATA, + API_ACCOUNT_NAME, + API_ACCOUNT_VALUE, + API_ACCOUNTS, + API_DATA, + API_RATES_CURRENCY, + API_RESOURCE_TYPE, + API_TYPE_VAULT, + API_V3_ACCOUNT_ID, + API_V3_TYPE_VAULT, CONF_CURRENCIES, CONF_EXCHANGE_BASE, CONF_EXCHANGE_RATES, @@ -59,9 +76,16 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: def create_and_update_instance(entry: ConfigEntry) -> CoinbaseData: """Create and update a Coinbase Data instance.""" - client = Client(entry.data[CONF_API_KEY], entry.data[CONF_API_TOKEN]) + if "organizations" not in entry.data[CONF_API_KEY]: + client = LegacyClient(entry.data[CONF_API_KEY], entry.data[CONF_API_TOKEN]) + version = "v2" + else: + client = RESTClient( + api_key=entry.data[CONF_API_KEY], api_secret=entry.data[CONF_API_TOKEN] + ) + version = "v3" base_rate = entry.options.get(CONF_EXCHANGE_BASE, "USD") - instance = CoinbaseData(client, base_rate) + instance = CoinbaseData(client, base_rate, version) instance.update() return instance @@ -86,42 +110,83 @@ async def update_listener(hass: HomeAssistant, config_entry: ConfigEntry) -> Non registry.async_remove(entity.entity_id) -def get_accounts(client): +def get_accounts(client, version): """Handle paginated accounts.""" response = client.get_accounts() - accounts = response[API_ACCOUNTS_DATA] - next_starting_after = response.pagination.next_starting_after - - while next_starting_after: - response = client.get_accounts(starting_after=next_starting_after) - accounts += response[API_ACCOUNTS_DATA] + if version == "v2": + accounts = response[API_DATA] next_starting_after = response.pagination.next_starting_after - return accounts + while next_starting_after: + response = client.get_accounts(starting_after=next_starting_after) + accounts += response[API_DATA] + next_starting_after = response.pagination.next_starting_after + + return [ + { + API_ACCOUNT_ID: account[API_ACCOUNT_ID], + API_ACCOUNT_NAME: account[API_ACCOUNT_NAME], + API_ACCOUNT_CURRENCY: account[API_ACCOUNT_CURRENCY][ + API_ACCOUNT_CURRENCY_CODE + ], + API_ACCOUNT_AMOUNT: account[API_ACCOUNT_BALANCE][API_ACCOUNT_AMOUNT], + ACCOUNT_IS_VAULT: account[API_RESOURCE_TYPE] == API_TYPE_VAULT, + } + for account in accounts + ] + + accounts = response[API_ACCOUNTS] + while response["has_next"]: + response = client.get_accounts(cursor=response["cursor"]) + accounts += response["accounts"] + + return [ + { + API_ACCOUNT_ID: account[API_V3_ACCOUNT_ID], + API_ACCOUNT_NAME: account[API_ACCOUNT_NAME], + API_ACCOUNT_CURRENCY: account[API_ACCOUNT_CURRENCY], + API_ACCOUNT_AMOUNT: account[API_ACCOUNT_AVALIABLE][API_ACCOUNT_VALUE] + + account[API_ACCOUNT_HOLD][API_ACCOUNT_VALUE], + ACCOUNT_IS_VAULT: account[API_RESOURCE_TYPE] == API_V3_TYPE_VAULT, + } + for account in accounts + ] class CoinbaseData: """Get the latest data and update the states.""" - def __init__(self, client, exchange_base): + def __init__(self, client, exchange_base, version): """Init the coinbase data object.""" self.client = client self.accounts = None self.exchange_base = exchange_base self.exchange_rates = None - self.user_id = self.client.get_current_user()[API_ACCOUNT_ID] + if version == "v2": + self.user_id = self.client.get_current_user()[API_ACCOUNT_ID] + else: + self.user_id = ( + "v3_" + client.get_portfolios()["portfolios"][0][API_V3_ACCOUNT_ID] + ) + self.api_version = version @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from coinbase.""" try: - self.accounts = get_accounts(self.client) - self.exchange_rates = self.client.get_exchange_rates( - currency=self.exchange_base - ) - except AuthenticationError as coinbase_error: + self.accounts = get_accounts(self.client, self.api_version) + if self.api_version == "v2": + self.exchange_rates = self.client.get_exchange_rates( + currency=self.exchange_base + ) + else: + self.exchange_rates = self.client.get( + "/v2/exchange-rates", + params={API_RATES_CURRENCY: self.exchange_base}, + )[API_DATA] + except (AuthenticationError, HTTPError) as coinbase_error: _LOGGER.error( "Authentication error connecting to coinbase: %s", coinbase_error ) diff --git a/homeassistant/components/coinbase/config_flow.py b/homeassistant/components/coinbase/config_flow.py index 623d5cf6731..616fdaf8f7a 100644 --- a/homeassistant/components/coinbase/config_flow.py +++ b/homeassistant/components/coinbase/config_flow.py @@ -5,7 +5,9 @@ from __future__ import annotations import logging from typing import Any -from coinbase.wallet.client import Client +from coinbase.rest import RESTClient +from coinbase.rest.rest_base import HTTPError +from coinbase.wallet.client import Client as LegacyClient from coinbase.wallet.error import AuthenticationError import voluptuous as vol @@ -15,18 +17,17 @@ from homeassistant.config_entries import ( ConfigFlowResult, OptionsFlow, ) -from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN +from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN, CONF_API_VERSION from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from . import get_accounts from .const import ( + ACCOUNT_IS_VAULT, API_ACCOUNT_CURRENCY, - API_ACCOUNT_CURRENCY_CODE, + API_DATA, API_RATES, - API_RESOURCE_TYPE, - API_TYPE_VAULT, CONF_CURRENCIES, CONF_EXCHANGE_BASE, CONF_EXCHANGE_PRECISION, @@ -49,8 +50,11 @@ STEP_USER_DATA_SCHEMA = vol.Schema( def get_user_from_client(api_key, api_token): """Get the user name from Coinbase API credentials.""" - client = Client(api_key, api_token) - return client.get_current_user() + if "organizations" not in api_key: + client = LegacyClient(api_key, api_token) + return client.get_current_user()["name"] + client = RESTClient(api_key=api_key, api_secret=api_token) + return client.get_portfolios()["portfolios"][0]["name"] async def validate_api(hass: HomeAssistant, data): @@ -60,11 +64,13 @@ async def validate_api(hass: HomeAssistant, data): user = await hass.async_add_executor_job( get_user_from_client, data[CONF_API_KEY], data[CONF_API_TOKEN] ) - except AuthenticationError as error: - if "api key" in str(error): + except (AuthenticationError, HTTPError) as error: + if "api key" in str(error) or " 401 Client Error" in str(error): _LOGGER.debug("Coinbase rejected API credentials due to an invalid API key") raise InvalidKey from error - if "invalid signature" in str(error): + if "invalid signature" in str( + error + ) or "'Could not deserialize key data" in str(error): _LOGGER.debug( "Coinbase rejected API credentials due to an invalid API secret" ) @@ -73,8 +79,8 @@ async def validate_api(hass: HomeAssistant, data): raise InvalidAuth from error except ConnectionError as error: raise CannotConnect from error - - return {"title": user["name"]} + api_version = "v3" if "organizations" in data[CONF_API_KEY] else "v2" + return {"title": user, "api_version": api_version} async def validate_options(hass: HomeAssistant, config_entry: ConfigEntry, options): @@ -82,14 +88,20 @@ async def validate_options(hass: HomeAssistant, config_entry: ConfigEntry, optio client = hass.data[DOMAIN][config_entry.entry_id].client - accounts = await hass.async_add_executor_job(get_accounts, client) + accounts = await hass.async_add_executor_job( + get_accounts, client, config_entry.data.get("api_version", "v2") + ) accounts_currencies = [ - account[API_ACCOUNT_CURRENCY][API_ACCOUNT_CURRENCY_CODE] + account[API_ACCOUNT_CURRENCY] for account in accounts - if account[API_RESOURCE_TYPE] != API_TYPE_VAULT + if not account[ACCOUNT_IS_VAULT] ] - available_rates = await hass.async_add_executor_job(client.get_exchange_rates) + if config_entry.data.get("api_version", "v2") == "v2": + available_rates = await hass.async_add_executor_job(client.get_exchange_rates) + else: + resp = await hass.async_add_executor_job(client.get, "/v2/exchange-rates") + available_rates = resp[API_DATA] if CONF_CURRENCIES in options: for currency in options[CONF_CURRENCIES]: if currency not in accounts_currencies: @@ -134,6 +146,7 @@ class CoinbaseConfigFlow(ConfigFlow, domain=DOMAIN): _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: + user_input[CONF_API_VERSION] = info["api_version"] return self.async_create_entry(title=info["title"], data=user_input) return self.async_show_form( step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors diff --git a/homeassistant/components/coinbase/const.py b/homeassistant/components/coinbase/const.py index f5c75e3f926..0f47d4bc208 100644 --- a/homeassistant/components/coinbase/const.py +++ b/homeassistant/components/coinbase/const.py @@ -1,5 +1,7 @@ """Constants used for Coinbase.""" +ACCOUNT_IS_VAULT = "is_vault" + CONF_CURRENCIES = "account_balance_currencies" CONF_EXCHANGE_BASE = "exchange_base" CONF_EXCHANGE_RATES = "exchange_rate_currencies" @@ -10,18 +12,25 @@ DOMAIN = "coinbase" # Constants for data returned by Coinbase API API_ACCOUNT_AMOUNT = "amount" +API_ACCOUNT_AVALIABLE = "available_balance" API_ACCOUNT_BALANCE = "balance" API_ACCOUNT_CURRENCY = "currency" API_ACCOUNT_CURRENCY_CODE = "code" +API_ACCOUNT_HOLD = "hold" API_ACCOUNT_ID = "id" API_ACCOUNT_NATIVE_BALANCE = "balance" API_ACCOUNT_NAME = "name" -API_ACCOUNTS_DATA = "data" +API_ACCOUNT_VALUE = "value" +API_ACCOUNTS = "accounts" +API_DATA = "data" API_RATES = "rates" +API_RATES_CURRENCY = "currency" API_RESOURCE_PATH = "resource_path" API_RESOURCE_TYPE = "type" API_TYPE_VAULT = "vault" API_USD = "USD" +API_V3_ACCOUNT_ID = "uuid" +API_V3_TYPE_VAULT = "ACCOUNT_TYPE_VAULT" WALLETS = { "1INCH": "1INCH", diff --git a/homeassistant/components/coinbase/manifest.json b/homeassistant/components/coinbase/manifest.json index 515fe9f9abb..be632b5e856 100644 --- a/homeassistant/components/coinbase/manifest.json +++ b/homeassistant/components/coinbase/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/coinbase", "iot_class": "cloud_polling", "loggers": ["coinbase"], - "requirements": ["coinbase==2.1.0"] + "requirements": ["coinbase==2.1.0", "coinbase-advanced-py==1.2.2"] } diff --git a/homeassistant/components/coinbase/sensor.py b/homeassistant/components/coinbase/sensor.py index 83c63fa55fb..d3f3c81fb0c 100644 --- a/homeassistant/components/coinbase/sensor.py +++ b/homeassistant/components/coinbase/sensor.py @@ -12,15 +12,12 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import CoinbaseData from .const import ( + ACCOUNT_IS_VAULT, API_ACCOUNT_AMOUNT, - API_ACCOUNT_BALANCE, API_ACCOUNT_CURRENCY, - API_ACCOUNT_CURRENCY_CODE, API_ACCOUNT_ID, API_ACCOUNT_NAME, API_RATES, - API_RESOURCE_TYPE, - API_TYPE_VAULT, CONF_CURRENCIES, CONF_EXCHANGE_PRECISION, CONF_EXCHANGE_PRECISION_DEFAULT, @@ -31,6 +28,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) ATTR_NATIVE_BALANCE = "Balance in native currency" +ATTR_API_VERSION = "API Version" CURRENCY_ICONS = { "BTC": "mdi:currency-btc", @@ -56,9 +54,9 @@ async def async_setup_entry( entities: list[SensorEntity] = [] provided_currencies: list[str] = [ - account[API_ACCOUNT_CURRENCY][API_ACCOUNT_CURRENCY_CODE] + account[API_ACCOUNT_CURRENCY] for account in instance.accounts - if account[API_RESOURCE_TYPE] != API_TYPE_VAULT + if not account[ACCOUNT_IS_VAULT] ] desired_currencies: list[str] = [] @@ -73,6 +71,11 @@ async def async_setup_entry( ) for currency in desired_currencies: + _LOGGER.debug( + "Attempting to set up %s account sensor with %s API", + currency, + instance.api_version, + ) if currency not in provided_currencies: _LOGGER.warning( ( @@ -85,12 +88,17 @@ async def async_setup_entry( entities.append(AccountSensor(instance, currency)) if CONF_EXCHANGE_RATES in config_entry.options: - entities.extend( - ExchangeRateSensor( - instance, rate, exchange_base_currency, exchange_precision + for rate in config_entry.options[CONF_EXCHANGE_RATES]: + _LOGGER.debug( + "Attempting to set up %s account sensor with %s API", + rate, + instance.api_version, + ) + entities.append( + ExchangeRateSensor( + instance, rate, exchange_base_currency, exchange_precision + ) ) - for rate in config_entry.options[CONF_EXCHANGE_RATES] - ) async_add_entities(entities) @@ -105,26 +113,21 @@ class AccountSensor(SensorEntity): self._coinbase_data = coinbase_data self._currency = currency for account in coinbase_data.accounts: - if ( - account[API_ACCOUNT_CURRENCY][API_ACCOUNT_CURRENCY_CODE] != currency - or account[API_RESOURCE_TYPE] == API_TYPE_VAULT - ): + if account[API_ACCOUNT_CURRENCY] != currency or account[ACCOUNT_IS_VAULT]: continue self._attr_name = f"Coinbase {account[API_ACCOUNT_NAME]}" self._attr_unique_id = ( f"coinbase-{account[API_ACCOUNT_ID]}-wallet-" - f"{account[API_ACCOUNT_CURRENCY][API_ACCOUNT_CURRENCY_CODE]}" + f"{account[API_ACCOUNT_CURRENCY]}" ) - self._attr_native_value = account[API_ACCOUNT_BALANCE][API_ACCOUNT_AMOUNT] - self._attr_native_unit_of_measurement = account[API_ACCOUNT_CURRENCY][ - API_ACCOUNT_CURRENCY_CODE - ] + self._attr_native_value = account[API_ACCOUNT_AMOUNT] + self._attr_native_unit_of_measurement = account[API_ACCOUNT_CURRENCY] self._attr_icon = CURRENCY_ICONS.get( - account[API_ACCOUNT_CURRENCY][API_ACCOUNT_CURRENCY_CODE], + account[API_ACCOUNT_CURRENCY], DEFAULT_COIN_ICON, ) self._native_balance = round( - float(account[API_ACCOUNT_BALANCE][API_ACCOUNT_AMOUNT]) + float(account[API_ACCOUNT_AMOUNT]) / float(coinbase_data.exchange_rates[API_RATES][currency]), 2, ) @@ -144,21 +147,26 @@ class AccountSensor(SensorEntity): """Return the state attributes of the sensor.""" return { ATTR_NATIVE_BALANCE: f"{self._native_balance} {self._coinbase_data.exchange_base}", + ATTR_API_VERSION: self._coinbase_data.api_version, } def update(self) -> None: """Get the latest state of the sensor.""" + _LOGGER.debug( + "Updating %s account sensor with %s API", + self._currency, + self._coinbase_data.api_version, + ) self._coinbase_data.update() for account in self._coinbase_data.accounts: if ( - account[API_ACCOUNT_CURRENCY][API_ACCOUNT_CURRENCY_CODE] - != self._currency - or account[API_RESOURCE_TYPE] == API_TYPE_VAULT + account[API_ACCOUNT_CURRENCY] != self._currency + or account[ACCOUNT_IS_VAULT] ): continue - self._attr_native_value = account[API_ACCOUNT_BALANCE][API_ACCOUNT_AMOUNT] + self._attr_native_value = account[API_ACCOUNT_AMOUNT] self._native_balance = round( - float(account[API_ACCOUNT_BALANCE][API_ACCOUNT_AMOUNT]) + float(account[API_ACCOUNT_AMOUNT]) / float(self._coinbase_data.exchange_rates[API_RATES][self._currency]), 2, ) @@ -202,8 +210,13 @@ class ExchangeRateSensor(SensorEntity): def update(self) -> None: """Get the latest state of the sensor.""" + _LOGGER.debug( + "Updating %s rate sensor with %s API", + self._currency, + self._coinbase_data.api_version, + ) self._coinbase_data.update() self._attr_native_value = round( - 1 / float(self._coinbase_data.exchange_rates.rates[self._currency]), + 1 / float(self._coinbase_data.exchange_rates[API_RATES][self._currency]), self._precision, ) diff --git a/requirements_all.txt b/requirements_all.txt index b8f50d328f1..940c58d77f7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -660,6 +660,9 @@ clearpasspy==1.0.2 # homeassistant.components.sinch clx-sdk-xms==1.0.0 +# homeassistant.components.coinbase +coinbase-advanced-py==1.2.2 + # homeassistant.components.coinbase coinbase==2.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f6602bf082b..d8086da5056 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -562,6 +562,9 @@ cached_ipaddress==0.3.0 # homeassistant.components.caldav caldav==1.3.9 +# homeassistant.components.coinbase +coinbase-advanced-py==1.2.2 + # homeassistant.components.coinbase coinbase==2.1.0 diff --git a/tests/components/coinbase/common.py b/tests/components/coinbase/common.py index 3421c4ce838..2768b6a2cd4 100644 --- a/tests/components/coinbase/common.py +++ b/tests/components/coinbase/common.py @@ -5,13 +5,14 @@ from homeassistant.components.coinbase.const import ( CONF_EXCHANGE_RATES, DOMAIN, ) -from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN +from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN, CONF_API_VERSION from .const import ( GOOD_CURRENCY_2, GOOD_EXCHANGE_RATE, GOOD_EXCHANGE_RATE_2, MOCK_ACCOUNTS_RESPONSE, + MOCK_ACCOUNTS_RESPONSE_V3, ) from tests.common import MockConfigEntry @@ -54,6 +55,33 @@ def mocked_get_accounts(_, **kwargs): return MockGetAccounts(**kwargs) +class MockGetAccountsV3: + """Mock accounts with pagination.""" + + def __init__(self, cursor=""): + """Init mocked object, forced to return two at a time.""" + ids = [account["uuid"] for account in MOCK_ACCOUNTS_RESPONSE_V3] + start = ids.index(cursor) if cursor else 0 + + has_next = (target_end := start + 2) < len(MOCK_ACCOUNTS_RESPONSE_V3) + end = target_end if has_next else -1 + next_cursor = ids[end] if has_next else ids[-1] + self.accounts = { + "accounts": MOCK_ACCOUNTS_RESPONSE_V3[start:end], + "has_next": has_next, + "cursor": next_cursor, + } + + def __getitem__(self, item): + """Handle subscript request.""" + return self.accounts[item] + + +def mocked_get_accounts_v3(_, **kwargs): + """Return simplified accounts using mock.""" + return MockGetAccountsV3(**kwargs) + + def mock_get_current_user(): """Return a simplified mock user.""" return { @@ -74,6 +102,19 @@ def mock_get_exchange_rates(): } +def mock_get_portfolios(): + """Return a mocked list of Coinbase portfolios.""" + return { + "portfolios": [ + { + "name": "Default", + "uuid": "123456", + "type": "DEFAULT", + } + ] + } + + async def init_mock_coinbase(hass, currencies=None, rates=None): """Init Coinbase integration for testing.""" config_entry = MockConfigEntry( @@ -93,3 +134,28 @@ async def init_mock_coinbase(hass, currencies=None, rates=None): await hass.async_block_till_done() return config_entry + + +async def init_mock_coinbase_v3(hass, currencies=None, rates=None): + """Init Coinbase integration for testing.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + entry_id="080272b77a4f80c41b94d7cdc86fd826", + unique_id=None, + title="Test User v3", + data={ + CONF_API_KEY: "organizations/123456", + CONF_API_TOKEN: "AbCDeF", + CONF_API_VERSION: "v3", + }, + options={ + CONF_CURRENCIES: currencies or [], + CONF_EXCHANGE_RATES: rates or [], + }, + ) + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + return config_entry diff --git a/tests/components/coinbase/const.py b/tests/components/coinbase/const.py index dcd14555ca3..5fbba11eb2d 100644 --- a/tests/components/coinbase/const.py +++ b/tests/components/coinbase/const.py @@ -31,3 +31,31 @@ MOCK_ACCOUNTS_RESPONSE = [ "type": "fiat", }, ] + +MOCK_ACCOUNTS_RESPONSE_V3 = [ + { + "uuid": "123456789", + "name": "BTC Wallet", + "currency": GOOD_CURRENCY, + "available_balance": {"value": "0.00001", "currency": GOOD_CURRENCY}, + "type": "ACCOUNT_TYPE_CRYPTO", + "hold": {"value": "0", "currency": GOOD_CURRENCY}, + }, + { + "uuid": "abcdefg", + "name": "BTC Vault", + "currency": GOOD_CURRENCY, + "available_balance": {"value": "100.00", "currency": GOOD_CURRENCY}, + "type": "ACCOUNT_TYPE_VAULT", + "hold": {"value": "0", "currency": GOOD_CURRENCY}, + }, + { + "uuid": "987654321", + "name": "USD Wallet", + "currency": GOOD_CURRENCY_2, + "available_balance": {"value": "9.90", "currency": GOOD_CURRENCY_2}, + "type": "ACCOUNT_TYPE_FIAT", + "ready": True, + "hold": {"value": "0", "currency": GOOD_CURRENCY_2}, + }, +] diff --git a/tests/components/coinbase/snapshots/test_diagnostics.ambr b/tests/components/coinbase/snapshots/test_diagnostics.ambr index 9079a7682c8..4f9e75dc38b 100644 --- a/tests/components/coinbase/snapshots/test_diagnostics.ambr +++ b/tests/components/coinbase/snapshots/test_diagnostics.ambr @@ -3,40 +3,25 @@ dict({ 'accounts': list([ dict({ - 'balance': dict({ - 'amount': '**REDACTED**', - 'currency': 'BTC', - }), - 'currency': dict({ - 'code': 'BTC', - }), + 'amount': '**REDACTED**', + 'currency': 'BTC', 'id': '**REDACTED**', + 'is_vault': False, 'name': 'BTC Wallet', - 'type': 'wallet', }), dict({ - 'balance': dict({ - 'amount': '**REDACTED**', - 'currency': 'BTC', - }), - 'currency': dict({ - 'code': 'BTC', - }), + 'amount': '**REDACTED**', + 'currency': 'BTC', 'id': '**REDACTED**', + 'is_vault': True, 'name': 'BTC Vault', - 'type': 'vault', }), dict({ - 'balance': dict({ - 'amount': '**REDACTED**', - 'currency': 'USD', - }), - 'currency': dict({ - 'code': 'USD', - }), + 'amount': '**REDACTED**', + 'currency': 'USD', 'id': '**REDACTED**', + 'is_vault': False, 'name': 'USD Wallet', - 'type': 'fiat', }), ]), 'entry': dict({ diff --git a/tests/components/coinbase/test_config_flow.py b/tests/components/coinbase/test_config_flow.py index f213392bb1e..aa2c6208e0f 100644 --- a/tests/components/coinbase/test_config_flow.py +++ b/tests/components/coinbase/test_config_flow.py @@ -14,15 +14,18 @@ from homeassistant.components.coinbase.const import ( CONF_EXCHANGE_RATES, DOMAIN, ) -from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN +from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN, CONF_API_VERSION from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from .common import ( init_mock_coinbase, + init_mock_coinbase_v3, mock_get_current_user, mock_get_exchange_rates, + mock_get_portfolios, mocked_get_accounts, + mocked_get_accounts_v3, ) from .const import BAD_CURRENCY, BAD_EXCHANGE_RATE, GOOD_CURRENCY, GOOD_EXCHANGE_RATE @@ -53,16 +56,17 @@ async def test_form(hass: HomeAssistant) -> None: ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - { - CONF_API_KEY: "123456", - CONF_API_TOKEN: "AbCDeF", - }, + {CONF_API_KEY: "123456", CONF_API_TOKEN: "AbCDeF"}, ) await hass.async_block_till_done() assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Test User" - assert result2["data"] == {CONF_API_KEY: "123456", CONF_API_TOKEN: "AbCDeF"} + assert result2["data"] == { + CONF_API_KEY: "123456", + CONF_API_TOKEN: "AbCDeF", + CONF_API_VERSION: "v2", + } assert len(mock_setup_entry.mock_calls) == 1 @@ -314,3 +318,77 @@ async def test_option_catch_all_exception(hass: HomeAssistant) -> None: assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} + + +async def test_form_v3(hass: HomeAssistant) -> None: + """Test we get the form.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {} + + with ( + patch("coinbase.rest.RESTClient.get_accounts", new=mocked_get_accounts_v3), + patch( + "coinbase.rest.RESTClient.get_portfolios", + return_value=mock_get_portfolios(), + ), + patch( + "coinbase.rest.RESTBase.get", + return_value={"data": mock_get_exchange_rates()}, + ), + patch( + "homeassistant.components.coinbase.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "organizations/123456", CONF_API_TOKEN: "AbCDeF"}, + ) + await hass.async_block_till_done() + + assert result2["type"] is FlowResultType.CREATE_ENTRY + assert result2["title"] == "Default" + assert result2["data"] == { + CONF_API_KEY: "organizations/123456", + CONF_API_TOKEN: "AbCDeF", + CONF_API_VERSION: "v3", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_option_form_v3(hass: HomeAssistant) -> None: + """Test we handle a good wallet currency option.""" + + with ( + patch("coinbase.rest.RESTClient.get_accounts", new=mocked_get_accounts_v3), + patch( + "coinbase.rest.RESTClient.get_portfolios", + return_value=mock_get_portfolios(), + ), + patch( + "coinbase.rest.RESTBase.get", + return_value={"data": mock_get_exchange_rates()}, + ), + patch( + "homeassistant.components.coinbase.update_listener" + ) as mock_update_listener, + ): + config_entry = await init_mock_coinbase_v3(hass) + await hass.async_block_till_done() + result = await hass.config_entries.options.async_init(config_entry.entry_id) + await hass.async_block_till_done() + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_CURRENCIES: [GOOD_CURRENCY], + CONF_EXCHANGE_RATES: [GOOD_EXCHANGE_RATE], + CONF_EXCHANGE_PRECISION: 5, + }, + ) + assert result2["type"] is FlowResultType.CREATE_ENTRY + await hass.async_block_till_done() + assert len(mock_update_listener.mock_calls) == 1 From de7af575c5d418675c8af427bb7efc3842424a4f Mon Sep 17 00:00:00 2001 From: Evgeny <940893+freekode@users.noreply.github.com> Date: Sat, 10 Aug 2024 17:01:26 +0200 Subject: [PATCH 002/271] Bump OpenWeatherMap to 0.1.1 (#120178) * add owm modes * fix tests * fix modes * remove sensors * Update homeassistant/components/openweathermap/sensor.py Co-authored-by: Joost Lekkerkerker --------- Co-authored-by: Joost Lekkerkerker --- .../components/openweathermap/__init__.py | 7 +-- .../components/openweathermap/const.py | 13 +++-- .../components/openweathermap/coordinator.py | 16 +++++-- .../components/openweathermap/manifest.json | 2 +- .../components/openweathermap/sensor.py | 27 +++++++---- .../components/openweathermap/utils.py | 4 +- .../components/openweathermap/weather.py | 48 +++++++++++++------ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../openweathermap/test_config_flow.py | 26 +++++----- 10 files changed, 97 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/openweathermap/__init__.py b/homeassistant/components/openweathermap/__init__.py index 7aea6aafe20..747b93179bc 100644 --- a/homeassistant/components/openweathermap/__init__.py +++ b/homeassistant/components/openweathermap/__init__.py @@ -5,7 +5,7 @@ from __future__ import annotations from dataclasses import dataclass import logging -from pyopenweathermap import OWMClient +from pyopenweathermap import create_owm_client from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -33,6 +33,7 @@ class OpenweathermapData: """Runtime data definition.""" name: str + mode: str coordinator: WeatherUpdateCoordinator @@ -52,7 +53,7 @@ async def async_setup_entry( else: async_delete_issue(hass, entry.entry_id) - owm_client = OWMClient(api_key, mode, lang=language) + owm_client = create_owm_client(api_key, mode, lang=language) weather_coordinator = WeatherUpdateCoordinator( owm_client, latitude, longitude, hass ) @@ -61,7 +62,7 @@ async def async_setup_entry( entry.async_on_unload(entry.add_update_listener(async_update_options)) - entry.runtime_data = OpenweathermapData(name, weather_coordinator) + entry.runtime_data = OpenweathermapData(name, mode, weather_coordinator) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/openweathermap/const.py b/homeassistant/components/openweathermap/const.py index 6c9997fc061..d34125a2405 100644 --- a/homeassistant/components/openweathermap/const.py +++ b/homeassistant/components/openweathermap/const.py @@ -58,10 +58,17 @@ FORECAST_MODE_DAILY = "daily" FORECAST_MODE_FREE_DAILY = "freedaily" FORECAST_MODE_ONECALL_HOURLY = "onecall_hourly" FORECAST_MODE_ONECALL_DAILY = "onecall_daily" -OWM_MODE_V25 = "v2.5" +OWM_MODE_FREE_CURRENT = "current" +OWM_MODE_FREE_FORECAST = "forecast" OWM_MODE_V30 = "v3.0" -OWM_MODES = [OWM_MODE_V30, OWM_MODE_V25] -DEFAULT_OWM_MODE = OWM_MODE_V30 +OWM_MODE_V25 = "v2.5" +OWM_MODES = [ + OWM_MODE_FREE_CURRENT, + OWM_MODE_FREE_FORECAST, + OWM_MODE_V30, + OWM_MODE_V25, +] +DEFAULT_OWM_MODE = OWM_MODE_FREE_CURRENT LANGUAGES = [ "af", diff --git a/homeassistant/components/openweathermap/coordinator.py b/homeassistant/components/openweathermap/coordinator.py index 0f99af5ad64..f7672a1290b 100644 --- a/homeassistant/components/openweathermap/coordinator.py +++ b/homeassistant/components/openweathermap/coordinator.py @@ -86,8 +86,14 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): """Format the weather response correctly.""" _LOGGER.debug("OWM weather response: %s", weather_report) + current_weather = ( + self._get_current_weather_data(weather_report.current) + if weather_report.current is not None + else {} + ) + return { - ATTR_API_CURRENT: self._get_current_weather_data(weather_report.current), + ATTR_API_CURRENT: current_weather, ATTR_API_HOURLY_FORECAST: [ self._get_hourly_forecast_weather_data(item) for item in weather_report.hourly_forecast @@ -122,6 +128,8 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): } def _get_hourly_forecast_weather_data(self, forecast: HourlyWeatherForecast): + uv_index = float(forecast.uv_index) if forecast.uv_index is not None else None + return Forecast( datetime=forecast.date_time.isoformat(), condition=self._get_condition(forecast.condition.id), @@ -134,12 +142,14 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): wind_speed=forecast.wind_speed, native_wind_gust_speed=forecast.wind_gust, wind_bearing=forecast.wind_bearing, - uv_index=float(forecast.uv_index), + uv_index=uv_index, precipitation_probability=round(forecast.precipitation_probability * 100), precipitation=self._calc_precipitation(forecast.rain, forecast.snow), ) def _get_daily_forecast_weather_data(self, forecast: DailyWeatherForecast): + uv_index = float(forecast.uv_index) if forecast.uv_index is not None else None + return Forecast( datetime=forecast.date_time.isoformat(), condition=self._get_condition(forecast.condition.id), @@ -153,7 +163,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): wind_speed=forecast.wind_speed, native_wind_gust_speed=forecast.wind_gust, wind_bearing=forecast.wind_bearing, - uv_index=float(forecast.uv_index), + uv_index=uv_index, precipitation_probability=round(forecast.precipitation_probability * 100), precipitation=round(forecast.rain + forecast.snow, 2), ) diff --git a/homeassistant/components/openweathermap/manifest.json b/homeassistant/components/openweathermap/manifest.json index e2c809cf385..199e750ad4f 100644 --- a/homeassistant/components/openweathermap/manifest.json +++ b/homeassistant/components/openweathermap/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/openweathermap", "iot_class": "cloud_polling", "loggers": ["pyopenweathermap"], - "requirements": ["pyopenweathermap==0.0.9"] + "requirements": ["pyopenweathermap==0.1.1"] } diff --git a/homeassistant/components/openweathermap/sensor.py b/homeassistant/components/openweathermap/sensor.py index 89905e99ed9..46789f4b3d2 100644 --- a/homeassistant/components/openweathermap/sensor.py +++ b/homeassistant/components/openweathermap/sensor.py @@ -19,6 +19,7 @@ from homeassistant.const import ( UnitOfVolumetricFlux, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType @@ -47,6 +48,7 @@ from .const import ( DEFAULT_NAME, DOMAIN, MANUFACTURER, + OWM_MODE_FREE_FORECAST, ) from .coordinator import WeatherUpdateCoordinator @@ -161,16 +163,23 @@ async def async_setup_entry( name = domain_data.name weather_coordinator = domain_data.coordinator - entities: list[AbstractOpenWeatherMapSensor] = [ - OpenWeatherMapSensor( - name, - f"{config_entry.unique_id}-{description.key}", - description, - weather_coordinator, + if domain_data.mode == OWM_MODE_FREE_FORECAST: + entity_registry = er.async_get(hass) + entries = er.async_entries_for_config_entry( + entity_registry, config_entry.entry_id + ) + for entry in entries: + entity_registry.async_remove(entry.entity_id) + else: + async_add_entities( + OpenWeatherMapSensor( + name, + f"{config_entry.unique_id}-{description.key}", + description, + weather_coordinator, + ) + for description in WEATHER_SENSOR_TYPES ) - for description in WEATHER_SENSOR_TYPES - ] - async_add_entities(entities) class AbstractOpenWeatherMapSensor(SensorEntity): diff --git a/homeassistant/components/openweathermap/utils.py b/homeassistant/components/openweathermap/utils.py index 7f2391b21a1..ba5378fb31c 100644 --- a/homeassistant/components/openweathermap/utils.py +++ b/homeassistant/components/openweathermap/utils.py @@ -2,7 +2,7 @@ from typing import Any -from pyopenweathermap import OWMClient, RequestError +from pyopenweathermap import RequestError, create_owm_client from homeassistant.const import CONF_LANGUAGE, CONF_MODE @@ -16,7 +16,7 @@ async def validate_api_key(api_key, mode): api_key_valid = None errors, description_placeholders = {}, {} try: - owm_client = OWMClient(api_key, mode) + owm_client = create_owm_client(api_key, mode) api_key_valid = await owm_client.validate_key() except RequestError as error: errors["base"] = "cannot_connect" diff --git a/homeassistant/components/openweathermap/weather.py b/homeassistant/components/openweathermap/weather.py index 62b15218233..3a134a0ee26 100644 --- a/homeassistant/components/openweathermap/weather.py +++ b/homeassistant/components/openweathermap/weather.py @@ -8,6 +8,7 @@ from homeassistant.components.weather import ( WeatherEntityFeature, ) from homeassistant.const import ( + UnitOfLength, UnitOfPrecipitationDepth, UnitOfPressure, UnitOfSpeed, @@ -29,6 +30,7 @@ from .const import ( ATTR_API_HUMIDITY, ATTR_API_PRESSURE, ATTR_API_TEMPERATURE, + ATTR_API_VISIBILITY_DISTANCE, ATTR_API_WIND_BEARING, ATTR_API_WIND_GUST, ATTR_API_WIND_SPEED, @@ -36,6 +38,9 @@ from .const import ( DEFAULT_NAME, DOMAIN, MANUFACTURER, + OWM_MODE_FREE_FORECAST, + OWM_MODE_V25, + OWM_MODE_V30, ) from .coordinator import WeatherUpdateCoordinator @@ -48,10 +53,11 @@ async def async_setup_entry( """Set up OpenWeatherMap weather entity based on a config entry.""" domain_data = config_entry.runtime_data name = domain_data.name + mode = domain_data.mode weather_coordinator = domain_data.coordinator unique_id = f"{config_entry.unique_id}" - owm_weather = OpenWeatherMapWeather(name, unique_id, weather_coordinator) + owm_weather = OpenWeatherMapWeather(name, unique_id, mode, weather_coordinator) async_add_entities([owm_weather], False) @@ -66,11 +72,13 @@ class OpenWeatherMapWeather(SingleCoordinatorWeatherEntity[WeatherUpdateCoordina _attr_native_pressure_unit = UnitOfPressure.HPA _attr_native_temperature_unit = UnitOfTemperature.CELSIUS _attr_native_wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND + _attr_native_visibility_unit = UnitOfLength.METERS def __init__( self, name: str, unique_id: str, + mode: str, weather_coordinator: WeatherUpdateCoordinator, ) -> None: """Initialize the sensor.""" @@ -83,59 +91,71 @@ class OpenWeatherMapWeather(SingleCoordinatorWeatherEntity[WeatherUpdateCoordina manufacturer=MANUFACTURER, name=DEFAULT_NAME, ) - self._attr_supported_features = ( - WeatherEntityFeature.FORECAST_DAILY | WeatherEntityFeature.FORECAST_HOURLY - ) + + if mode in (OWM_MODE_V30, OWM_MODE_V25): + self._attr_supported_features = ( + WeatherEntityFeature.FORECAST_DAILY + | WeatherEntityFeature.FORECAST_HOURLY + ) + elif mode == OWM_MODE_FREE_FORECAST: + self._attr_supported_features = WeatherEntityFeature.FORECAST_HOURLY @property def condition(self) -> str | None: """Return the current condition.""" - return self.coordinator.data[ATTR_API_CURRENT][ATTR_API_CONDITION] + return self.coordinator.data[ATTR_API_CURRENT].get(ATTR_API_CONDITION) @property def cloud_coverage(self) -> float | None: """Return the Cloud coverage in %.""" - return self.coordinator.data[ATTR_API_CURRENT][ATTR_API_CLOUDS] + return self.coordinator.data[ATTR_API_CURRENT].get(ATTR_API_CLOUDS) @property def native_apparent_temperature(self) -> float | None: """Return the apparent temperature.""" - return self.coordinator.data[ATTR_API_CURRENT][ATTR_API_FEELS_LIKE_TEMPERATURE] + return self.coordinator.data[ATTR_API_CURRENT].get( + ATTR_API_FEELS_LIKE_TEMPERATURE + ) @property def native_temperature(self) -> float | None: """Return the temperature.""" - return self.coordinator.data[ATTR_API_CURRENT][ATTR_API_TEMPERATURE] + return self.coordinator.data[ATTR_API_CURRENT].get(ATTR_API_TEMPERATURE) @property def native_pressure(self) -> float | None: """Return the pressure.""" - return self.coordinator.data[ATTR_API_CURRENT][ATTR_API_PRESSURE] + return self.coordinator.data[ATTR_API_CURRENT].get(ATTR_API_PRESSURE) @property def humidity(self) -> float | None: """Return the humidity.""" - return self.coordinator.data[ATTR_API_CURRENT][ATTR_API_HUMIDITY] + return self.coordinator.data[ATTR_API_CURRENT].get(ATTR_API_HUMIDITY) @property def native_dew_point(self) -> float | None: """Return the dew point.""" - return self.coordinator.data[ATTR_API_CURRENT][ATTR_API_DEW_POINT] + return self.coordinator.data[ATTR_API_CURRENT].get(ATTR_API_DEW_POINT) @property def native_wind_gust_speed(self) -> float | None: """Return the wind gust speed.""" - return self.coordinator.data[ATTR_API_CURRENT][ATTR_API_WIND_GUST] + return self.coordinator.data[ATTR_API_CURRENT].get(ATTR_API_WIND_GUST) @property def native_wind_speed(self) -> float | None: """Return the wind speed.""" - return self.coordinator.data[ATTR_API_CURRENT][ATTR_API_WIND_SPEED] + return self.coordinator.data[ATTR_API_CURRENT].get(ATTR_API_WIND_SPEED) @property def wind_bearing(self) -> float | str | None: """Return the wind bearing.""" - return self.coordinator.data[ATTR_API_CURRENT][ATTR_API_WIND_BEARING] + return self.coordinator.data[ATTR_API_CURRENT].get(ATTR_API_WIND_BEARING) + + @property + def visibility(self) -> float | str | None: + """Return visibility.""" + return self.coordinator.data[ATTR_API_CURRENT].get(ATTR_API_VISIBILITY_DISTANCE) @callback def _async_forecast_daily(self) -> list[Forecast] | None: diff --git a/requirements_all.txt b/requirements_all.txt index 940c58d77f7..4ce36e96b94 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2071,7 +2071,7 @@ pyombi==0.1.10 pyopenuv==2023.02.0 # homeassistant.components.openweathermap -pyopenweathermap==0.0.9 +pyopenweathermap==0.1.1 # homeassistant.components.opnsense pyopnsense==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d8086da5056..6d7214ecab6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1655,7 +1655,7 @@ pyoctoprintapi==0.1.12 pyopenuv==2023.02.0 # homeassistant.components.openweathermap -pyopenweathermap==0.0.9 +pyopenweathermap==0.1.1 # homeassistant.components.opnsense pyopnsense==0.4.0 diff --git a/tests/components/openweathermap/test_config_flow.py b/tests/components/openweathermap/test_config_flow.py index be02a6b01a9..f18aa432e2f 100644 --- a/tests/components/openweathermap/test_config_flow.py +++ b/tests/components/openweathermap/test_config_flow.py @@ -45,7 +45,7 @@ CONFIG = { VALID_YAML_CONFIG = {CONF_API_KEY: "foo"} -def _create_mocked_owm_client(is_valid: bool): +def _create_mocked_owm_factory(is_valid: bool): current_weather = CurrentWeather( date_time=datetime.fromtimestamp(1714063536, tz=UTC), temperature=6.84, @@ -118,18 +118,18 @@ def _create_mocked_owm_client(is_valid: bool): def mock_owm_client(): """Mock config_flow OWMClient.""" with patch( - "homeassistant.components.openweathermap.OWMClient", - ) as owm_client_mock: - yield owm_client_mock + "homeassistant.components.openweathermap.create_owm_client", + ) as mock: + yield mock @pytest.fixture(name="config_flow_owm_client_mock") def mock_config_flow_owm_client(): """Mock config_flow OWMClient.""" with patch( - "homeassistant.components.openweathermap.utils.OWMClient", - ) as config_flow_owm_client_mock: - yield config_flow_owm_client_mock + "homeassistant.components.openweathermap.utils.create_owm_client", + ) as mock: + yield mock async def test_successful_config_flow( @@ -138,7 +138,7 @@ async def test_successful_config_flow( config_flow_owm_client_mock, ) -> None: """Test that the form is served with valid input.""" - mock = _create_mocked_owm_client(True) + mock = _create_mocked_owm_factory(True) owm_client_mock.return_value = mock config_flow_owm_client_mock.return_value = mock @@ -177,7 +177,7 @@ async def test_abort_config_flow( config_flow_owm_client_mock, ) -> None: """Test that the form is served with same data.""" - mock = _create_mocked_owm_client(True) + mock = _create_mocked_owm_factory(True) owm_client_mock.return_value = mock config_flow_owm_client_mock.return_value = mock @@ -200,7 +200,7 @@ async def test_config_flow_options_change( config_flow_owm_client_mock, ) -> None: """Test that the options form.""" - mock = _create_mocked_owm_client(True) + mock = _create_mocked_owm_factory(True) owm_client_mock.return_value = mock config_flow_owm_client_mock.return_value = mock @@ -261,7 +261,7 @@ async def test_form_invalid_api_key( config_flow_owm_client_mock, ) -> None: """Test that the form is served with no input.""" - config_flow_owm_client_mock.return_value = _create_mocked_owm_client(False) + config_flow_owm_client_mock.return_value = _create_mocked_owm_factory(False) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) @@ -269,7 +269,7 @@ async def test_form_invalid_api_key( assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_api_key"} - config_flow_owm_client_mock.return_value = _create_mocked_owm_client(True) + config_flow_owm_client_mock.return_value = _create_mocked_owm_factory(True) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONFIG ) @@ -282,7 +282,7 @@ async def test_form_api_call_error( config_flow_owm_client_mock, ) -> None: """Test setting up with api call error.""" - config_flow_owm_client_mock.return_value = _create_mocked_owm_client(True) + config_flow_owm_client_mock.return_value = _create_mocked_owm_factory(True) config_flow_owm_client_mock.side_effect = RequestError("oops") result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONFIG From ec08a85aa0332c37c65a406d62d844ec0faf6833 Mon Sep 17 00:00:00 2001 From: fustom Date: Thu, 8 Aug 2024 18:49:47 +0200 Subject: [PATCH 003/271] Fix limit and order property for transmission integration (#123305) --- homeassistant/components/transmission/coordinator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/transmission/coordinator.py b/homeassistant/components/transmission/coordinator.py index d6b5b695656..e0930bd9e9e 100644 --- a/homeassistant/components/transmission/coordinator.py +++ b/homeassistant/components/transmission/coordinator.py @@ -55,12 +55,12 @@ class TransmissionDataUpdateCoordinator(DataUpdateCoordinator[SessionStats]): @property def limit(self) -> int: """Return limit.""" - return self.config_entry.data.get(CONF_LIMIT, DEFAULT_LIMIT) + return self.config_entry.options.get(CONF_LIMIT, DEFAULT_LIMIT) @property def order(self) -> str: """Return order.""" - return self.config_entry.data.get(CONF_ORDER, DEFAULT_ORDER) + return self.config_entry.options.get(CONF_ORDER, DEFAULT_ORDER) async def _async_update_data(self) -> SessionStats: """Update transmission data.""" From 6fddef2dc5b57c29e15d66d3f0ab2d76c3fd3f6d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 8 Aug 2024 01:56:40 -0500 Subject: [PATCH 004/271] Fix doorbird with externally added events (#123313) --- homeassistant/components/doorbird/device.py | 2 +- tests/components/doorbird/fixtures/favorites.json | 4 ++++ tests/components/doorbird/test_button.py | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/doorbird/device.py b/homeassistant/components/doorbird/device.py index 7cd45487464..adcb441f458 100644 --- a/homeassistant/components/doorbird/device.py +++ b/homeassistant/components/doorbird/device.py @@ -195,7 +195,7 @@ class ConfiguredDoorBird: title: str | None = data.get("title") if not title or not title.startswith("Home Assistant"): continue - event = title.split("(")[1].strip(")") + event = title.partition("(")[2].strip(")") if input_type := favorite_input_type.get(identifier): events.append(DoorbirdEvent(event, input_type)) elif input_type := default_event_types.get(event): diff --git a/tests/components/doorbird/fixtures/favorites.json b/tests/components/doorbird/fixtures/favorites.json index c56f79c0300..50dddb850a5 100644 --- a/tests/components/doorbird/fixtures/favorites.json +++ b/tests/components/doorbird/fixtures/favorites.json @@ -7,6 +7,10 @@ "1": { "title": "Home Assistant (mydoorbird_motion)", "value": "http://127.0.0.1:8123/api/doorbird/mydoorbird_motion?token=01J2F4B97Y7P1SARXEJ6W07EKD" + }, + "2": { + "title": "externally added event", + "value": "http://127.0.0.1/" } } } diff --git a/tests/components/doorbird/test_button.py b/tests/components/doorbird/test_button.py index 2131e3d6133..cb4bab656ee 100644 --- a/tests/components/doorbird/test_button.py +++ b/tests/components/doorbird/test_button.py @@ -49,4 +49,4 @@ async def test_reset_favorites_button( DOMAIN, SERVICE_PRESS, {ATTR_ENTITY_ID: reset_entity_id}, blocking=True ) assert hass.states.get(reset_entity_id).state != STATE_UNKNOWN - assert doorbird_entry.api.delete_favorite.call_count == 2 + assert doorbird_entry.api.delete_favorite.call_count == 3 From 9bfc8f6e27256b03ed138fb720206018b961673f Mon Sep 17 00:00:00 2001 From: Noah Husby <32528627+noahhusby@users.noreply.github.com> Date: Thu, 8 Aug 2024 02:56:02 -0400 Subject: [PATCH 005/271] Bump aiorussound to 2.2.2 (#123319) --- homeassistant/components/russound_rio/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/russound_rio/manifest.json b/homeassistant/components/russound_rio/manifest.json index be5dd86793f..e7bb99010ee 100644 --- a/homeassistant/components/russound_rio/manifest.json +++ b/homeassistant/components/russound_rio/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/russound_rio", "iot_class": "local_push", "loggers": ["aiorussound"], - "requirements": ["aiorussound==2.2.0"] + "requirements": ["aiorussound==2.2.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4ce36e96b94..92576dc6b0a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -350,7 +350,7 @@ aioridwell==2024.01.0 aioruckus==0.34 # homeassistant.components.russound_rio -aiorussound==2.2.0 +aiorussound==2.2.2 # homeassistant.components.ruuvi_gateway aioruuvigateway==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6d7214ecab6..2b31401e2f0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -332,7 +332,7 @@ aioridwell==2024.01.0 aioruckus==0.34 # homeassistant.components.russound_rio -aiorussound==2.2.0 +aiorussound==2.2.2 # homeassistant.components.ruuvi_gateway aioruuvigateway==0.1.0 From a3db6bc8fa90319c79232e6e9f4766121ffd9165 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 8 Aug 2024 17:30:39 +0200 Subject: [PATCH 006/271] Revert "Fix blocking I/O while validating config schema" (#123377) --- homeassistant/config.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/homeassistant/config.py b/homeassistant/config.py index 18c833d4c75..948ab342e79 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -817,9 +817,7 @@ async def async_process_ha_core_config(hass: HomeAssistant, config: dict) -> Non This method is a coroutine. """ - # CORE_CONFIG_SCHEMA is not async safe since it uses vol.IsDir - # so we need to run it in an executor job. - config = await hass.async_add_executor_job(CORE_CONFIG_SCHEMA, config) + config = CORE_CONFIG_SCHEMA(config) # Only load auth during startup. if not hasattr(hass, "auth"): @@ -1535,15 +1533,9 @@ async def async_process_component_config( return IntegrationConfigInfo(None, config_exceptions) # No custom config validator, proceed with schema validation - if config_schema := getattr(component, "CONFIG_SCHEMA", None): + if hasattr(component, "CONFIG_SCHEMA"): try: - if domain in config: - # cv.isdir, cv.isfile, cv.isdevice are not async - # friendly so we need to run this in executor - schema = await hass.async_add_executor_job(config_schema, config) - else: - schema = config_schema(config) - return IntegrationConfigInfo(schema, []) + return IntegrationConfigInfo(component.CONFIG_SCHEMA(config), []) except vol.Invalid as exc: exc_info = ConfigExceptionInfo( exc, From ab0597da7b7ef1b1c21b6544529d75c8fe2d1b6f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 8 Aug 2024 23:19:12 -0500 Subject: [PATCH 007/271] Ensure legacy event foreign key is removed from the states table when a previous rebuild failed (#123388) * Ensure legacy event foreign key is removed from the states table If the system ran out of disk space removing the FK, it would fail. #121938 fixed that to try again, however that PR was made ineffective by #122069 since it will never reach the check. To solve this, the migration version is incremented to 2, and the migration is no longer marked as done unless the rebuild /fk removal is successful. * fix logic for mysql * fix test * asserts * coverage * coverage * narrow test * fixes * split tests * should have skipped * fixture must be used --- .../components/recorder/migration.py | 24 +- tests/components/recorder/test_migrate.py | 14 +- .../components/recorder/test_v32_migration.py | 346 ++++++++++++++++++ 3 files changed, 368 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 2932ea484c9..a41de07e243 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -632,7 +632,7 @@ def _update_states_table_with_foreign_key_options( def _drop_foreign_key_constraints( session_maker: Callable[[], Session], engine: Engine, table: str, column: str -) -> list[tuple[str, str, ReflectedForeignKeyConstraint]]: +) -> tuple[bool, list[tuple[str, str, ReflectedForeignKeyConstraint]]]: """Drop foreign key constraints for a table on specific columns.""" inspector = sqlalchemy.inspect(engine) dropped_constraints = [ @@ -649,6 +649,7 @@ def _drop_foreign_key_constraints( if foreign_key["name"] and foreign_key["constrained_columns"] == [column] ] + fk_remove_ok = True for drop in drops: with session_scope(session=session_maker()) as session: try: @@ -660,8 +661,9 @@ def _drop_foreign_key_constraints( TABLE_STATES, column, ) + fk_remove_ok = False - return dropped_constraints + return fk_remove_ok, dropped_constraints def _restore_foreign_key_constraints( @@ -1481,7 +1483,7 @@ class _SchemaVersion44Migrator(_SchemaVersionMigrator, target_version=44): for column in columns for dropped_constraint in _drop_foreign_key_constraints( self.session_maker, self.engine, table, column - ) + )[1] ] _LOGGER.debug("Dropped foreign key constraints: %s", dropped_constraints) @@ -1956,14 +1958,15 @@ def cleanup_legacy_states_event_ids(instance: Recorder) -> bool: if instance.dialect_name == SupportedDialect.SQLITE: # SQLite does not support dropping foreign key constraints # so we have to rebuild the table - rebuild_sqlite_table(session_maker, instance.engine, States) + fk_remove_ok = rebuild_sqlite_table(session_maker, instance.engine, States) else: - _drop_foreign_key_constraints( + fk_remove_ok, _ = _drop_foreign_key_constraints( session_maker, instance.engine, TABLE_STATES, "event_id" ) - _drop_index(session_maker, "states", LEGACY_STATES_EVENT_ID_INDEX) - instance.use_legacy_events_index = False - _mark_migration_done(session, EventIDPostMigration) + if fk_remove_ok: + _drop_index(session_maker, "states", LEGACY_STATES_EVENT_ID_INDEX) + instance.use_legacy_events_index = False + _mark_migration_done(session, EventIDPostMigration) return True @@ -2419,6 +2422,7 @@ class EventIDPostMigration(BaseRunTimeMigration): migration_id = "event_id_post_migration" task = MigrationTask + migration_version = 2 @staticmethod def migrate_data(instance: Recorder) -> bool: @@ -2469,7 +2473,7 @@ def _mark_migration_done( def rebuild_sqlite_table( session_maker: Callable[[], Session], engine: Engine, table: type[Base] -) -> None: +) -> bool: """Rebuild an SQLite table. This must only be called after all migrations are complete @@ -2524,8 +2528,10 @@ def rebuild_sqlite_table( # Swallow the exception since we do not want to ever raise # an integrity error as it would cause the database # to be discarded and recreated from scratch + return False else: _LOGGER.warning("Rebuilding SQLite table %s finished", orig_name) + return True finally: with session_scope(session=session_maker()) as session: # Step 12 - Re-enable foreign keys diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index dc99ddefa3b..e55793caad7 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -748,7 +748,7 @@ def test_rebuild_sqlite_states_table(recorder_db_url: str) -> None: session.add(States(state="on")) session.commit() - migration.rebuild_sqlite_table(session_maker, engine, States) + assert migration.rebuild_sqlite_table(session_maker, engine, States) is True with session_scope(session=session_maker()) as session: assert session.query(States).count() == 1 @@ -776,13 +776,13 @@ def test_rebuild_sqlite_states_table_missing_fails( session.connection().execute(text("DROP TABLE states")) session.commit() - migration.rebuild_sqlite_table(session_maker, engine, States) + assert migration.rebuild_sqlite_table(session_maker, engine, States) is False assert "Error recreating SQLite table states" in caplog.text caplog.clear() # Now rebuild the events table to make sure the database did not # get corrupted - migration.rebuild_sqlite_table(session_maker, engine, Events) + assert migration.rebuild_sqlite_table(session_maker, engine, Events) is True with session_scope(session=session_maker()) as session: assert session.query(Events).count() == 1 @@ -812,7 +812,7 @@ def test_rebuild_sqlite_states_table_extra_columns( text("ALTER TABLE states ADD COLUMN extra_column TEXT") ) - migration.rebuild_sqlite_table(session_maker, engine, States) + assert migration.rebuild_sqlite_table(session_maker, engine, States) is True assert "Error recreating SQLite table states" not in caplog.text with session_scope(session=session_maker()) as session: @@ -905,7 +905,7 @@ def test_drop_restore_foreign_key_constraints(recorder_db_url: str) -> None: for table, column in constraints_to_recreate for dropped_constraint in migration._drop_foreign_key_constraints( session_maker, engine, table, column - ) + )[1] ] assert dropped_constraints_1 == expected_dropped_constraints[db_engine] @@ -917,7 +917,7 @@ def test_drop_restore_foreign_key_constraints(recorder_db_url: str) -> None: for table, column in constraints_to_recreate for dropped_constraint in migration._drop_foreign_key_constraints( session_maker, engine, table, column - ) + )[1] ] assert dropped_constraints_2 == [] @@ -936,7 +936,7 @@ def test_drop_restore_foreign_key_constraints(recorder_db_url: str) -> None: for table, column in constraints_to_recreate for dropped_constraint in migration._drop_foreign_key_constraints( session_maker, engine, table, column - ) + )[1] ] assert dropped_constraints_3 == expected_dropped_constraints[db_engine] diff --git a/tests/components/recorder/test_v32_migration.py b/tests/components/recorder/test_v32_migration.py index 9956fec8a09..5266e55851c 100644 --- a/tests/components/recorder/test_v32_migration.py +++ b/tests/components/recorder/test_v32_migration.py @@ -7,6 +7,7 @@ from unittest.mock import patch import pytest from sqlalchemy import create_engine, inspect +from sqlalchemy.exc import OperationalError, SQLAlchemyError from sqlalchemy.orm import Session from homeassistant.components import recorder @@ -444,3 +445,348 @@ async def test_migrate_can_resume_ix_states_event_id_removed( assert await instance.async_add_executor_job(_get_event_id_foreign_keys) is None await hass.async_stop() + + +@pytest.mark.usefixtures("skip_by_db_engine") +@pytest.mark.skip_on_db_engine(["mysql", "postgresql"]) +@pytest.mark.parametrize("enable_migrate_event_ids", [True]) +@pytest.mark.parametrize("persistent_database", [True]) +@pytest.mark.usefixtures("hass_storage") # Prevent test hass from writing to storage +async def test_out_of_disk_space_while_rebuild_states_table( + async_test_recorder: RecorderInstanceGenerator, + caplog: pytest.LogCaptureFixture, + recorder_db_url: str, +) -> None: + """Test that we can recover from out of disk space while rebuilding the states table. + + This case tests the migration still happens if + ix_states_event_id is removed from the states table. + """ + importlib.import_module(SCHEMA_MODULE) + old_db_schema = sys.modules[SCHEMA_MODULE] + now = dt_util.utcnow() + one_second_past = now - timedelta(seconds=1) + mock_state = State( + "sensor.test", + "old", + {"last_reset": now.isoformat()}, + last_changed=one_second_past, + last_updated=now, + ) + state_changed_event = Event( + EVENT_STATE_CHANGED, + { + "entity_id": "sensor.test", + "old_state": None, + "new_state": mock_state, + }, + EventOrigin.local, + time_fired_timestamp=now.timestamp(), + ) + custom_event = Event( + "custom_event", + {"entity_id": "sensor.custom"}, + EventOrigin.local, + time_fired_timestamp=now.timestamp(), + ) + number_of_migrations = 5 + + def _get_event_id_foreign_keys(): + assert instance.engine is not None + return next( + ( + fk # type: ignore[misc] + for fk in inspect(instance.engine).get_foreign_keys("states") + if fk["constrained_columns"] == ["event_id"] + ), + None, + ) + + def _get_states_index_names(): + with session_scope(hass=hass) as session: + return inspect(session.connection()).get_indexes("states") + + with ( + patch.object(recorder, "db_schema", old_db_schema), + patch.object( + recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION + ), + patch.object(core, "StatesMeta", old_db_schema.StatesMeta), + patch.object(core, "EventTypes", old_db_schema.EventTypes), + patch.object(core, "EventData", old_db_schema.EventData), + patch.object(core, "States", old_db_schema.States), + patch.object(core, "Events", old_db_schema.Events), + patch(CREATE_ENGINE_TARGET, new=_create_engine_test), + patch("homeassistant.components.recorder.Recorder._post_migrate_entity_ids"), + patch( + "homeassistant.components.recorder.migration.cleanup_legacy_states_event_ids" + ), + ): + async with ( + async_test_home_assistant() as hass, + async_test_recorder(hass) as instance, + ): + await hass.async_block_till_done() + await async_wait_recording_done(hass) + await async_wait_recording_done(hass) + + def _add_data(): + with session_scope(hass=hass) as session: + session.add(old_db_schema.Events.from_event(custom_event)) + session.add(old_db_schema.States.from_event(state_changed_event)) + + await instance.async_add_executor_job(_add_data) + await hass.async_block_till_done() + await instance.async_block_till_done() + + await instance.async_add_executor_job( + migration._drop_index, + instance.get_session, + "states", + "ix_states_event_id", + ) + + states_indexes = await instance.async_add_executor_job( + _get_states_index_names + ) + states_index_names = {index["name"] for index in states_indexes} + assert instance.use_legacy_events_index is True + assert ( + await instance.async_add_executor_job(_get_event_id_foreign_keys) + is not None + ) + + await hass.async_stop() + await hass.async_block_till_done() + + assert "ix_states_entity_id_last_updated_ts" in states_index_names + + # Simulate out of disk space while rebuilding the states table by + # - patching CreateTable to raise SQLAlchemyError for SQLite + # - patching DropConstraint to raise InternalError for MySQL and PostgreSQL + with ( + patch( + "homeassistant.components.recorder.migration.CreateTable", + side_effect=SQLAlchemyError, + ), + patch( + "homeassistant.components.recorder.migration.DropConstraint", + side_effect=OperationalError( + None, None, OSError("No space left on device") + ), + ), + ): + async with ( + async_test_home_assistant() as hass, + async_test_recorder(hass) as instance, + ): + await hass.async_block_till_done() + + # We need to wait for all the migration tasks to complete + # before we can check the database. + for _ in range(number_of_migrations): + await instance.async_block_till_done() + await async_wait_recording_done(hass) + + states_indexes = await instance.async_add_executor_job( + _get_states_index_names + ) + states_index_names = {index["name"] for index in states_indexes} + assert instance.use_legacy_events_index is True + assert "Error recreating SQLite table states" in caplog.text + assert await instance.async_add_executor_job(_get_event_id_foreign_keys) + + await hass.async_stop() + + # Now run it again to verify the table rebuild tries again + caplog.clear() + async with ( + async_test_home_assistant() as hass, + async_test_recorder(hass) as instance, + ): + await hass.async_block_till_done() + + # We need to wait for all the migration tasks to complete + # before we can check the database. + for _ in range(number_of_migrations): + await instance.async_block_till_done() + await async_wait_recording_done(hass) + + states_indexes = await instance.async_add_executor_job(_get_states_index_names) + states_index_names = {index["name"] for index in states_indexes} + assert instance.use_legacy_events_index is False + assert "ix_states_entity_id_last_updated_ts" not in states_index_names + assert "ix_states_event_id" not in states_index_names + assert "Rebuilding SQLite table states finished" in caplog.text + assert await instance.async_add_executor_job(_get_event_id_foreign_keys) is None + + await hass.async_stop() + + +@pytest.mark.usefixtures("skip_by_db_engine") +@pytest.mark.skip_on_db_engine(["sqlite"]) +@pytest.mark.parametrize("enable_migrate_event_ids", [True]) +@pytest.mark.parametrize("persistent_database", [True]) +@pytest.mark.usefixtures("hass_storage") # Prevent test hass from writing to storage +async def test_out_of_disk_space_while_removing_foreign_key( + async_test_recorder: RecorderInstanceGenerator, + caplog: pytest.LogCaptureFixture, + recorder_db_url: str, +) -> None: + """Test that we can recover from out of disk space while removing the foreign key. + + This case tests the migration still happens if + ix_states_event_id is removed from the states table. + """ + importlib.import_module(SCHEMA_MODULE) + old_db_schema = sys.modules[SCHEMA_MODULE] + now = dt_util.utcnow() + one_second_past = now - timedelta(seconds=1) + mock_state = State( + "sensor.test", + "old", + {"last_reset": now.isoformat()}, + last_changed=one_second_past, + last_updated=now, + ) + state_changed_event = Event( + EVENT_STATE_CHANGED, + { + "entity_id": "sensor.test", + "old_state": None, + "new_state": mock_state, + }, + EventOrigin.local, + time_fired_timestamp=now.timestamp(), + ) + custom_event = Event( + "custom_event", + {"entity_id": "sensor.custom"}, + EventOrigin.local, + time_fired_timestamp=now.timestamp(), + ) + number_of_migrations = 5 + + def _get_event_id_foreign_keys(): + assert instance.engine is not None + return next( + ( + fk # type: ignore[misc] + for fk in inspect(instance.engine).get_foreign_keys("states") + if fk["constrained_columns"] == ["event_id"] + ), + None, + ) + + def _get_states_index_names(): + with session_scope(hass=hass) as session: + return inspect(session.connection()).get_indexes("states") + + with ( + patch.object(recorder, "db_schema", old_db_schema), + patch.object( + recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION + ), + patch.object(core, "StatesMeta", old_db_schema.StatesMeta), + patch.object(core, "EventTypes", old_db_schema.EventTypes), + patch.object(core, "EventData", old_db_schema.EventData), + patch.object(core, "States", old_db_schema.States), + patch.object(core, "Events", old_db_schema.Events), + patch(CREATE_ENGINE_TARGET, new=_create_engine_test), + patch("homeassistant.components.recorder.Recorder._post_migrate_entity_ids"), + patch( + "homeassistant.components.recorder.migration.cleanup_legacy_states_event_ids" + ), + ): + async with ( + async_test_home_assistant() as hass, + async_test_recorder(hass) as instance, + ): + await hass.async_block_till_done() + await async_wait_recording_done(hass) + await async_wait_recording_done(hass) + + def _add_data(): + with session_scope(hass=hass) as session: + session.add(old_db_schema.Events.from_event(custom_event)) + session.add(old_db_schema.States.from_event(state_changed_event)) + + await instance.async_add_executor_job(_add_data) + await hass.async_block_till_done() + await instance.async_block_till_done() + + await instance.async_add_executor_job( + migration._drop_index, + instance.get_session, + "states", + "ix_states_event_id", + ) + + states_indexes = await instance.async_add_executor_job( + _get_states_index_names + ) + states_index_names = {index["name"] for index in states_indexes} + assert instance.use_legacy_events_index is True + assert ( + await instance.async_add_executor_job(_get_event_id_foreign_keys) + is not None + ) + + await hass.async_stop() + await hass.async_block_till_done() + + assert "ix_states_entity_id_last_updated_ts" in states_index_names + + # Simulate out of disk space while removing the foreign key from the states table by + # - patching DropConstraint to raise InternalError for MySQL and PostgreSQL + with ( + patch( + "homeassistant.components.recorder.migration.DropConstraint", + side_effect=OperationalError( + None, None, OSError("No space left on device") + ), + ), + ): + async with ( + async_test_home_assistant() as hass, + async_test_recorder(hass) as instance, + ): + await hass.async_block_till_done() + + # We need to wait for all the migration tasks to complete + # before we can check the database. + for _ in range(number_of_migrations): + await instance.async_block_till_done() + await async_wait_recording_done(hass) + + states_indexes = await instance.async_add_executor_job( + _get_states_index_names + ) + states_index_names = {index["name"] for index in states_indexes} + assert instance.use_legacy_events_index is True + assert await instance.async_add_executor_job(_get_event_id_foreign_keys) + + await hass.async_stop() + + # Now run it again to verify the table rebuild tries again + caplog.clear() + async with ( + async_test_home_assistant() as hass, + async_test_recorder(hass) as instance, + ): + await hass.async_block_till_done() + + # We need to wait for all the migration tasks to complete + # before we can check the database. + for _ in range(number_of_migrations): + await instance.async_block_till_done() + await async_wait_recording_done(hass) + + states_indexes = await instance.async_add_executor_job(_get_states_index_names) + states_index_names = {index["name"] for index in states_indexes} + assert instance.use_legacy_events_index is False + assert "ix_states_entity_id_last_updated_ts" not in states_index_names + assert "ix_states_event_id" not in states_index_names + assert await instance.async_add_executor_job(_get_event_id_foreign_keys) is None + + await hass.async_stop() From 1ed0a89303774ae789c341b2ab30e81994aba736 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 8 Aug 2024 17:07:22 -0500 Subject: [PATCH 008/271] Bump aiohttp to 3.10.2 (#123394) --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 472134fea37..43fade21d1f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodhcpwatcher==1.0.2 aiodiscover==2.1.0 aiodns==3.2.0 aiohttp-fast-zlib==0.1.1 -aiohttp==3.10.1 +aiohttp==3.10.2 aiohttp_cors==0.7.0 aiozoneinfo==0.2.1 astral==2.2 diff --git a/pyproject.toml b/pyproject.toml index dc943b0832a..36bc214554b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ classifiers = [ requires-python = ">=3.12.0" dependencies = [ "aiodns==3.2.0", - "aiohttp==3.10.1", + "aiohttp==3.10.2", "aiohttp_cors==0.7.0", "aiohttp-fast-zlib==0.1.1", "aiozoneinfo==0.2.1", diff --git a/requirements.txt b/requirements.txt index 1beefe73914..e1bded8b335 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ # Home Assistant Core aiodns==3.2.0 -aiohttp==3.10.1 +aiohttp==3.10.2 aiohttp_cors==0.7.0 aiohttp-fast-zlib==0.1.1 aiozoneinfo==0.2.1 From 670c4cacfa7ff527d4b9565b4c2ebb52999974d7 Mon Sep 17 00:00:00 2001 From: dupondje Date: Sat, 10 Aug 2024 10:40:11 +0200 Subject: [PATCH 009/271] Also migrate dsmr entries for devices with correct serial (#123407) dsmr: also migrate entries for devices with correct serial When the dsmr code could not find the serial_nr for the gas meter, it creates the gas meter device with the entry_id as identifier. But when there is a correct serial_nr, it will use that as identifier for the dsmr gas device. Now the migration code did not take this into account, so migration to the new name failed since it didn't look for the device with correct serial_nr. This commit fixes this and adds a test for this. --- homeassistant/components/dsmr/sensor.py | 67 +++++++------- tests/components/dsmr/test_mbus_migration.py | 95 ++++++++++++++++++++ 2 files changed, 129 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index b298ed5bfc0..77c40c5c292 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -431,41 +431,42 @@ def rename_old_gas_to_mbus( ) -> None: """Rename old gas sensor to mbus variant.""" dev_reg = dr.async_get(hass) - device_entry_v1 = dev_reg.async_get_device(identifiers={(DOMAIN, entry.entry_id)}) - if device_entry_v1 is not None: - device_id = device_entry_v1.id + for dev_id in (mbus_device_id, entry.entry_id): + device_entry_v1 = dev_reg.async_get_device(identifiers={(DOMAIN, dev_id)}) + if device_entry_v1 is not None: + device_id = device_entry_v1.id - ent_reg = er.async_get(hass) - entries = er.async_entries_for_device(ent_reg, device_id) + ent_reg = er.async_get(hass) + entries = er.async_entries_for_device(ent_reg, device_id) - for entity in entries: - if entity.unique_id.endswith( - "belgium_5min_gas_meter_reading" - ) or entity.unique_id.endswith("hourly_gas_meter_reading"): - try: - ent_reg.async_update_entity( - entity.entity_id, - new_unique_id=mbus_device_id, - device_id=mbus_device_id, - ) - except ValueError: - LOGGER.debug( - "Skip migration of %s because it already exists", - entity.entity_id, - ) - else: - LOGGER.debug( - "Migrated entity %s from unique id %s to %s", - entity.entity_id, - entity.unique_id, - mbus_device_id, - ) - # Cleanup old device - dev_entities = er.async_entries_for_device( - ent_reg, device_id, include_disabled_entities=True - ) - if not dev_entities: - dev_reg.async_remove_device(device_id) + for entity in entries: + if entity.unique_id.endswith( + "belgium_5min_gas_meter_reading" + ) or entity.unique_id.endswith("hourly_gas_meter_reading"): + try: + ent_reg.async_update_entity( + entity.entity_id, + new_unique_id=mbus_device_id, + device_id=mbus_device_id, + ) + except ValueError: + LOGGER.debug( + "Skip migration of %s because it already exists", + entity.entity_id, + ) + else: + LOGGER.debug( + "Migrated entity %s from unique id %s to %s", + entity.entity_id, + entity.unique_id, + mbus_device_id, + ) + # Cleanup old device + dev_entities = er.async_entries_for_device( + ent_reg, device_id, include_disabled_entities=True + ) + if not dev_entities: + dev_reg.async_remove_device(device_id) def is_supported_description( diff --git a/tests/components/dsmr/test_mbus_migration.py b/tests/components/dsmr/test_mbus_migration.py index 20b3d253f39..7c7d182aa97 100644 --- a/tests/components/dsmr/test_mbus_migration.py +++ b/tests/components/dsmr/test_mbus_migration.py @@ -219,6 +219,101 @@ async def test_migrate_hourly_gas_to_mbus( ) +async def test_migrate_gas_with_devid_to_mbus( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + dsmr_connection_fixture: tuple[MagicMock, MagicMock, MagicMock], +) -> None: + """Test migration of unique_id.""" + (connection_factory, transport, protocol) = dsmr_connection_fixture + + mock_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="/dev/ttyUSB0", + data={ + "port": "/dev/ttyUSB0", + "dsmr_version": "5B", + "serial_id": "1234", + "serial_id_gas": "37464C4F32313139303333373331", + }, + options={ + "time_between_update": 0, + }, + ) + + mock_entry.add_to_hass(hass) + + old_unique_id = "37464C4F32313139303333373331_belgium_5min_gas_meter_reading" + + device = device_registry.async_get_or_create( + config_entry_id=mock_entry.entry_id, + identifiers={(DOMAIN, "37464C4F32313139303333373331")}, + name="Gas Meter", + ) + await hass.async_block_till_done() + + entity: er.RegistryEntry = entity_registry.async_get_or_create( + suggested_object_id="gas_meter_reading", + disabled_by=None, + domain=SENSOR_DOMAIN, + platform=DOMAIN, + device_id=device.id, + unique_id=old_unique_id, + config_entry=mock_entry, + ) + assert entity.unique_id == old_unique_id + await hass.async_block_till_done() + + telegram = Telegram() + telegram.add( + MBUS_DEVICE_TYPE, + CosemObject((0, 1), [{"value": "003", "unit": ""}]), + "MBUS_DEVICE_TYPE", + ) + telegram.add( + MBUS_EQUIPMENT_IDENTIFIER, + CosemObject( + (0, 1), + [{"value": "37464C4F32313139303333373331", "unit": ""}], + ), + "MBUS_EQUIPMENT_IDENTIFIER", + ) + telegram.add( + MBUS_METER_READING, + MBusObject( + (0, 1), + [ + {"value": datetime.datetime.fromtimestamp(1551642213)}, + {"value": Decimal(745.695), "unit": "m3"}, + ], + ), + "MBUS_METER_READING", + ) + + assert await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + telegram_callback = connection_factory.call_args_list[0][0][2] + + # simulate a telegram pushed from the smartmeter and parsed by dsmr_parser + telegram_callback(telegram) + + # after receiving telegram entities need to have the chance to be created + await hass.async_block_till_done() + + assert ( + entity_registry.async_get_entity_id(SENSOR_DOMAIN, DOMAIN, old_unique_id) + is None + ) + assert ( + entity_registry.async_get_entity_id( + SENSOR_DOMAIN, DOMAIN, "37464C4F32313139303333373331" + ) + == "sensor.gas_meter_reading" + ) + + async def test_migrate_gas_to_mbus_exists( hass: HomeAssistant, entity_registry: er.EntityRegistry, From b147ca6c5bd0bfb2b93eb1c16b4c244f3f3ddf5f Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Sat, 10 Aug 2024 04:33:13 +1000 Subject: [PATCH 010/271] Add missing logger to Tessie (#123413) --- homeassistant/components/tessie/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tessie/manifest.json b/homeassistant/components/tessie/manifest.json index 6059072c239..c921921a0ca 100644 --- a/homeassistant/components/tessie/manifest.json +++ b/homeassistant/components/tessie/manifest.json @@ -5,7 +5,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tessie", "iot_class": "cloud_polling", - "loggers": ["tessie"], + "loggers": ["tessie", "tesla-fleet-api"], "quality_scale": "platinum", "requirements": ["tessie-api==0.1.1", "tesla-fleet-api==0.7.3"] } From fd77058def692d6f2b3fcd1ba1c6458e7b7123f1 Mon Sep 17 00:00:00 2001 From: Matrix Date: Fri, 9 Aug 2024 18:21:49 +0800 Subject: [PATCH 011/271] Bump YoLink API to 0.4.7 (#123441) --- homeassistant/components/yolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yolink/manifest.json b/homeassistant/components/yolink/manifest.json index ceb4e4ceff3..78b553d7978 100644 --- a/homeassistant/components/yolink/manifest.json +++ b/homeassistant/components/yolink/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["auth", "application_credentials"], "documentation": "https://www.home-assistant.io/integrations/yolink", "iot_class": "cloud_push", - "requirements": ["yolink-api==0.4.6"] + "requirements": ["yolink-api==0.4.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index 92576dc6b0a..1e9a4dce5d3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2965,7 +2965,7 @@ yeelight==0.7.14 yeelightsunflower==0.0.10 # homeassistant.components.yolink -yolink-api==0.4.6 +yolink-api==0.4.7 # homeassistant.components.youless youless-api==2.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2b31401e2f0..fa258bb45c1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2342,7 +2342,7 @@ yalexs==6.4.3 yeelight==0.7.14 # homeassistant.components.yolink -yolink-api==0.4.6 +yolink-api==0.4.7 # homeassistant.components.youless youless-api==2.1.2 From a8b1eb34f3fbee8f0ec9a9f5c92ef20fd7356561 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 9 Aug 2024 17:18:42 +0200 Subject: [PATCH 012/271] Support action YAML syntax in old-style notify groups (#123457) --- homeassistant/components/group/notify.py | 33 ++++++++++++++++++-- tests/components/group/test_notify.py | 39 ++++++++++++++++++++++-- 2 files changed, 67 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/group/notify.py b/homeassistant/components/group/notify.py index 8294b55be5e..ecbfec0bdb8 100644 --- a/homeassistant/components/group/notify.py +++ b/homeassistant/components/group/notify.py @@ -22,8 +22,9 @@ from homeassistant.components.notify import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, - ATTR_SERVICE, + CONF_ACTION, CONF_ENTITIES, + CONF_SERVICE, STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant, callback @@ -36,11 +37,37 @@ from .entity import GroupEntity CONF_SERVICES = "services" + +def _backward_compat_schema(value: Any | None) -> Any: + """Backward compatibility for notify service schemas.""" + + if not isinstance(value, dict): + return value + + # `service` has been renamed to `action` + if CONF_SERVICE in value: + if CONF_ACTION in value: + raise vol.Invalid( + "Cannot specify both 'service' and 'action'. Please use 'action' only." + ) + value[CONF_ACTION] = value.pop(CONF_SERVICE) + + return value + + PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( { vol.Required(CONF_SERVICES): vol.All( cv.ensure_list, - [{vol.Required(ATTR_SERVICE): cv.slug, vol.Optional(ATTR_DATA): dict}], + [ + vol.All( + _backward_compat_schema, + { + vol.Required(CONF_ACTION): cv.slug, + vol.Optional(ATTR_DATA): dict, + }, + ) + ], ) } ) @@ -88,7 +115,7 @@ class GroupNotifyPlatform(BaseNotificationService): tasks.append( asyncio.create_task( self.hass.services.async_call( - DOMAIN, entity[ATTR_SERVICE], sending_payload, blocking=True + DOMAIN, entity[CONF_ACTION], sending_payload, blocking=True ) ) ) diff --git a/tests/components/group/test_notify.py b/tests/components/group/test_notify.py index 2595b211dae..bbf2d98b492 100644 --- a/tests/components/group/test_notify.py +++ b/tests/components/group/test_notify.py @@ -122,7 +122,7 @@ async def test_send_message_with_data(hass: HomeAssistant, tmp_path: Path) -> No "services": [ {"service": "test_service1"}, { - "service": "test_service2", + "action": "test_service2", "data": { "target": "unnamed device", "data": {"test": "message", "default": "default"}, @@ -202,6 +202,41 @@ async def test_send_message_with_data(hass: HomeAssistant, tmp_path: Path) -> No ) +async def test_invalid_configuration( + hass: HomeAssistant, tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """Test failing to set up group with an invalid configuration.""" + assert await async_setup_component( + hass, + "group", + {}, + ) + await hass.async_block_till_done() + + group_setup = [ + { + "platform": "group", + "name": "My invalid notification group", + "services": [ + { + "service": "test_service1", + "action": "test_service2", + "data": { + "target": "unnamed device", + "data": {"test": "message", "default": "default"}, + }, + }, + ], + } + ] + await help_setup_notify(hass, tmp_path, {"service1": 1, "service2": 2}, group_setup) + assert not hass.services.has_service("notify", "my_invalid_notification_group") + assert ( + "Invalid config for 'notify' from integration 'group':" + " Cannot specify both 'service' and 'action'." in caplog.text + ) + + async def test_reload_notify(hass: HomeAssistant, tmp_path: Path) -> None: """Verify we can reload the notify service.""" assert await async_setup_component( @@ -219,7 +254,7 @@ async def test_reload_notify(hass: HomeAssistant, tmp_path: Path) -> None: { "name": "group_notify", "platform": "group", - "services": [{"service": "test_service1"}], + "services": [{"action": "test_service1"}], } ], ) From 3d3879b0db07c6b6dcf201f11be5e00e607977aa Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 9 Aug 2024 10:31:55 -0400 Subject: [PATCH 013/271] Bump ZHA library to 0.0.29 (#123464) * Bump zha to 0.0.29 * Pass the Core timezone to ZHA * Add a unit test --- homeassistant/components/zha/__init__.py | 19 ++++++++++++++++-- homeassistant/components/zha/helpers.py | 2 ++ homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/zha/test_init.py | 23 +++++++++++++++++++++- 6 files changed, 44 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index fc573b19ab1..1897b741d87 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -2,6 +2,7 @@ import contextlib import logging +from zoneinfo import ZoneInfo import voluptuous as vol from zha.application.const import BAUD_RATES, RadioType @@ -12,8 +13,13 @@ from zigpy.config import CONF_DATABASE, CONF_DEVICE, CONF_DEVICE_PATH from zigpy.exceptions import NetworkSettingsInconsistent, TransientConnectionError from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_TYPE, EVENT_HOMEASSISTANT_STOP, Platform -from homeassistant.core import Event, HomeAssistant +from homeassistant.const import ( + CONF_TYPE, + EVENT_CORE_CONFIG_UPDATE, + EVENT_HOMEASSISTANT_STOP, + Platform, +) +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv @@ -204,6 +210,15 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_shutdown) ) + @callback + def update_config(event: Event) -> None: + """Handle Core config update.""" + zha_gateway.config.local_timezone = ZoneInfo(hass.config.time_zone) + + config_entry.async_on_unload( + hass.bus.async_listen(EVENT_CORE_CONFIG_UPDATE, update_config) + ) + await ha_zha_data.gateway_proxy.async_initialize_devices_and_entities() await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) async_dispatcher_send(hass, SIGNAL_ADD_ENTITIES) diff --git a/homeassistant/components/zha/helpers.py b/homeassistant/components/zha/helpers.py index 0691e2429d1..35a794e8631 100644 --- a/homeassistant/components/zha/helpers.py +++ b/homeassistant/components/zha/helpers.py @@ -15,6 +15,7 @@ import re import time from types import MappingProxyType from typing import TYPE_CHECKING, Any, Concatenate, NamedTuple, ParamSpec, TypeVar, cast +from zoneinfo import ZoneInfo import voluptuous as vol from zha.application.const import ( @@ -1273,6 +1274,7 @@ def create_zha_config(hass: HomeAssistant, ha_zha_data: HAZHAData) -> ZHAData: quirks_configuration=quirks_config, device_overrides=overrides_config, ), + local_timezone=ZoneInfo(hass.config.time_zone), ) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 4a597b0233c..385b95c8058 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -21,7 +21,7 @@ "zha", "universal_silabs_flasher" ], - "requirements": ["universal-silabs-flasher==0.0.22", "zha==0.0.28"], + "requirements": ["universal-silabs-flasher==0.0.22", "zha==0.0.29"], "usb": [ { "vid": "10C4", diff --git a/requirements_all.txt b/requirements_all.txt index 1e9a4dce5d3..6aa33a477a7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2989,7 +2989,7 @@ zeroconf==0.132.2 zeversolar==0.3.1 # homeassistant.components.zha -zha==0.0.28 +zha==0.0.29 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fa258bb45c1..759ea373a08 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2363,7 +2363,7 @@ zeroconf==0.132.2 zeversolar==0.3.1 # homeassistant.components.zha -zha==0.0.28 +zha==0.0.29 # homeassistant.components.zwave_js zwave-js-server-python==0.57.0 diff --git a/tests/components/zha/test_init.py b/tests/components/zha/test_init.py index aa68d688799..00fc3afd0ea 100644 --- a/tests/components/zha/test_init.py +++ b/tests/components/zha/test_init.py @@ -3,6 +3,7 @@ import asyncio import typing from unittest.mock import AsyncMock, Mock, patch +import zoneinfo import pytest from zigpy.application import ControllerApplication @@ -16,7 +17,7 @@ from homeassistant.components.zha.const import ( CONF_USB_PATH, DOMAIN, ) -from homeassistant.components.zha.helpers import get_zha_data +from homeassistant.components.zha.helpers import get_zha_data, get_zha_gateway from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, MAJOR_VERSION, @@ -288,3 +289,23 @@ async def test_shutdown_on_ha_stop( await hass.async_block_till_done() assert len(mock_shutdown.mock_calls) == 1 + + +async def test_timezone_update( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_zigpy_connect: ControllerApplication, +) -> None: + """Test that the ZHA gateway timezone is updated when HA timezone changes.""" + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + gateway = get_zha_gateway(hass) + + assert hass.config.time_zone == "US/Pacific" + assert gateway.config.local_timezone == zoneinfo.ZoneInfo("US/Pacific") + + await hass.config.async_update(time_zone="America/New_York") + + assert hass.config.time_zone == "America/New_York" + assert gateway.config.local_timezone == zoneinfo.ZoneInfo("America/New_York") From 44e58a8c877cc26931b769208e2aeb6f0e29d2de Mon Sep 17 00:00:00 2001 From: Steve Easley Date: Fri, 9 Aug 2024 12:51:50 -0400 Subject: [PATCH 014/271] Bump pyjvcprojector to 1.0.12 to fix blocking call (#123473) --- homeassistant/components/jvc_projector/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/jvc_projector/manifest.json b/homeassistant/components/jvc_projector/manifest.json index d3e1bf3d940..5d83e937494 100644 --- a/homeassistant/components/jvc_projector/manifest.json +++ b/homeassistant/components/jvc_projector/manifest.json @@ -7,5 +7,5 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["jvcprojector"], - "requirements": ["pyjvcprojector==1.0.11"] + "requirements": ["pyjvcprojector==1.0.12"] } diff --git a/requirements_all.txt b/requirements_all.txt index 6aa33a477a7..e0189ff6e69 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1945,7 +1945,7 @@ pyisy==3.1.14 pyitachip2ir==0.0.7 # homeassistant.components.jvc_projector -pyjvcprojector==1.0.11 +pyjvcprojector==1.0.12 # homeassistant.components.kaleidescape pykaleidescape==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 759ea373a08..d8b1a58a148 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1550,7 +1550,7 @@ pyiss==1.0.1 pyisy==3.1.14 # homeassistant.components.jvc_projector -pyjvcprojector==1.0.11 +pyjvcprojector==1.0.12 # homeassistant.components.kaleidescape pykaleidescape==1.0.1 From d3f8fce788601b7e7c3582198e9febeb7addac39 Mon Sep 17 00:00:00 2001 From: Jake Martin Date: Fri, 9 Aug 2024 17:52:07 +0100 Subject: [PATCH 015/271] Bump monzopy to 1.3.2 (#123480) --- homeassistant/components/monzo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/monzo/manifest.json b/homeassistant/components/monzo/manifest.json index 8b816457004..d9d17eb8abc 100644 --- a/homeassistant/components/monzo/manifest.json +++ b/homeassistant/components/monzo/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["application_credentials"], "documentation": "https://www.home-assistant.io/integrations/monzo", "iot_class": "cloud_polling", - "requirements": ["monzopy==1.3.0"] + "requirements": ["monzopy==1.3.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index e0189ff6e69..c47319efd07 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1354,7 +1354,7 @@ moat-ble==0.1.1 moehlenhoff-alpha2==1.3.1 # homeassistant.components.monzo -monzopy==1.3.0 +monzopy==1.3.2 # homeassistant.components.mopeka mopeka-iot-ble==0.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d8b1a58a148..fc718c2b68f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1120,7 +1120,7 @@ moat-ble==0.1.1 moehlenhoff-alpha2==1.3.1 # homeassistant.components.monzo -monzopy==1.3.0 +monzopy==1.3.2 # homeassistant.components.mopeka mopeka-iot-ble==0.8.0 From fb3eae54ea491c5384bc45fd3cd68ba49de7f753 Mon Sep 17 00:00:00 2001 From: Louis Christ Date: Fri, 9 Aug 2024 19:36:58 +0200 Subject: [PATCH 016/271] Fix startup blocked by bluesound integration (#123483) --- homeassistant/components/bluesound/media_player.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index dc09feaed63..c1b662fcddc 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -317,21 +317,24 @@ class BluesoundPlayer(MediaPlayerEntity): await self.async_update_status() except (TimeoutError, ClientError): - _LOGGER.error("Node %s:%s is offline, retrying later", self.name, self.port) + _LOGGER.error("Node %s:%s is offline, retrying later", self.host, self.port) await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT) self.start_polling() except CancelledError: - _LOGGER.debug("Stopping the polling of node %s:%s", self.name, self.port) + _LOGGER.debug("Stopping the polling of node %s:%s", self.host, self.port) except Exception: - _LOGGER.exception("Unexpected error in %s:%s", self.name, self.port) + _LOGGER.exception("Unexpected error in %s:%s", self.host, self.port) raise async def async_added_to_hass(self) -> None: """Start the polling task.""" await super().async_added_to_hass() - self._polling_task = self.hass.async_create_task(self._start_poll_command()) + self._polling_task = self.hass.async_create_background_task( + self._start_poll_command(), + name=f"bluesound.polling_{self.host}:{self.port}", + ) async def async_will_remove_from_hass(self) -> None: """Stop the polling task.""" From c4f6f1e3d89bc106b66a0a36367884b37939433b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 9 Aug 2024 20:30:39 +0200 Subject: [PATCH 017/271] Update frontend to 20240809.0 (#123485) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index de423ee9ac6..035b087e481 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20240806.1"] + "requirements": ["home-assistant-frontend==20240809.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 43fade21d1f..e34b398ac3a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -31,7 +31,7 @@ habluetooth==3.1.3 hass-nabucasa==0.81.1 hassil==1.7.4 home-assistant-bluetooth==1.12.2 -home-assistant-frontend==20240806.1 +home-assistant-frontend==20240809.0 home-assistant-intents==2024.8.7 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index c47319efd07..0ed985c16a3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1096,7 +1096,7 @@ hole==0.8.0 holidays==0.53 # homeassistant.components.frontend -home-assistant-frontend==20240806.1 +home-assistant-frontend==20240809.0 # homeassistant.components.conversation home-assistant-intents==2024.8.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fc718c2b68f..875e019250a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -919,7 +919,7 @@ hole==0.8.0 holidays==0.53 # homeassistant.components.frontend -home-assistant-frontend==20240806.1 +home-assistant-frontend==20240809.0 # homeassistant.components.conversation home-assistant-intents==2024.8.7 From bdb2e1e2e95f6627630e4e387a4331e1b41caba8 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sat, 10 Aug 2024 08:07:08 -0400 Subject: [PATCH 018/271] Bump zha lib to 0.0.30 (#123499) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 385b95c8058..bb1480b43e1 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -21,7 +21,7 @@ "zha", "universal_silabs_flasher" ], - "requirements": ["universal-silabs-flasher==0.0.22", "zha==0.0.29"], + "requirements": ["universal-silabs-flasher==0.0.22", "zha==0.0.30"], "usb": [ { "vid": "10C4", diff --git a/requirements_all.txt b/requirements_all.txt index 0ed985c16a3..c4ecd005dfa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2989,7 +2989,7 @@ zeroconf==0.132.2 zeversolar==0.3.1 # homeassistant.components.zha -zha==0.0.29 +zha==0.0.30 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 875e019250a..1b6aceb6e7b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2363,7 +2363,7 @@ zeroconf==0.132.2 zeversolar==0.3.1 # homeassistant.components.zha -zha==0.0.29 +zha==0.0.30 # homeassistant.components.zwave_js zwave-js-server-python==0.57.0 From dfb59469cfe0f89ca65d7d8e7d69aff7ac213cef Mon Sep 17 00:00:00 2001 From: Duco Sebel <74970928+DCSBL@users.noreply.github.com> Date: Sat, 10 Aug 2024 17:01:17 +0200 Subject: [PATCH 019/271] Bumb python-homewizard-energy to 6.2.0 (#123514) --- homeassistant/components/homewizard/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homewizard/manifest.json b/homeassistant/components/homewizard/manifest.json index 474d63e943d..dbad91b1fb8 100644 --- a/homeassistant/components/homewizard/manifest.json +++ b/homeassistant/components/homewizard/manifest.json @@ -7,6 +7,6 @@ "iot_class": "local_polling", "loggers": ["homewizard_energy"], "quality_scale": "platinum", - "requirements": ["python-homewizard-energy==v6.1.1"], + "requirements": ["python-homewizard-energy==v6.2.0"], "zeroconf": ["_hwenergy._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index c4ecd005dfa..8297560e5a0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2283,7 +2283,7 @@ python-gitlab==1.6.0 python-homeassistant-analytics==0.7.0 # homeassistant.components.homewizard -python-homewizard-energy==v6.1.1 +python-homewizard-energy==v6.2.0 # homeassistant.components.hp_ilo python-hpilo==4.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1b6aceb6e7b..bff546c98d6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1807,7 +1807,7 @@ python-fullykiosk==0.0.14 python-homeassistant-analytics==0.7.0 # homeassistant.components.homewizard -python-homewizard-energy==v6.1.1 +python-homewizard-energy==v6.2.0 # homeassistant.components.izone python-izone==1.2.9 From 4a75c55a8f6234272712938d17766b52cfc40b46 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sat, 10 Aug 2024 18:01:15 +0200 Subject: [PATCH 020/271] Fix cleanup of old orphan device entries in AVM Fritz!Tools (#123516) fix cleanup of old orphan device entries --- homeassistant/components/fritz/coordinator.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/fritz/coordinator.py b/homeassistant/components/fritz/coordinator.py index 592bf37084e..e97b988c391 100644 --- a/homeassistant/components/fritz/coordinator.py +++ b/homeassistant/components/fritz/coordinator.py @@ -653,8 +653,6 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]): entities: list[er.RegistryEntry] = er.async_entries_for_config_entry( entity_reg, config_entry.entry_id ) - - orphan_macs: set[str] = set() for entity in entities: entry_mac = entity.unique_id.split("_")[0] if ( @@ -662,17 +660,16 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]): or "_internet_access" in entity.unique_id ) and entry_mac not in device_hosts: _LOGGER.info("Removing orphan entity entry %s", entity.entity_id) - orphan_macs.add(entry_mac) entity_reg.async_remove(entity.entity_id) device_reg = dr.async_get(self.hass) - orphan_connections = { - (CONNECTION_NETWORK_MAC, dr.format_mac(mac)) for mac in orphan_macs + valid_connections = { + (CONNECTION_NETWORK_MAC, dr.format_mac(mac)) for mac in device_hosts } for device in dr.async_entries_for_config_entry( device_reg, config_entry.entry_id ): - if any(con in device.connections for con in orphan_connections): + if not any(con in device.connections for con in valid_connections): _LOGGER.debug("Removing obsolete device entry %s", device.name) device_reg.async_update_device( device.id, remove_config_entry_id=config_entry.entry_id From fe2e6c37f49d480de5f23699abd36d1d0f33cee7 Mon Sep 17 00:00:00 2001 From: Matt Way Date: Sat, 10 Aug 2024 21:06:29 +1000 Subject: [PATCH 021/271] Bump pydaikin to 2.13.2 (#123519) --- homeassistant/components/daikin/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/daikin/manifest.json b/homeassistant/components/daikin/manifest.json index 827deb27add..c5cb6064d88 100644 --- a/homeassistant/components/daikin/manifest.json +++ b/homeassistant/components/daikin/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/daikin", "iot_class": "local_polling", "loggers": ["pydaikin"], - "requirements": ["pydaikin==2.13.1"], + "requirements": ["pydaikin==2.13.2"], "zeroconf": ["_dkapi._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 8297560e5a0..3f4407d6f54 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1789,7 +1789,7 @@ pycsspeechtts==1.0.8 # pycups==1.9.73 # homeassistant.components.daikin -pydaikin==2.13.1 +pydaikin==2.13.2 # homeassistant.components.danfoss_air pydanfossair==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bff546c98d6..6164a5803e5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1436,7 +1436,7 @@ pycoolmasternet-async==0.1.5 pycsspeechtts==1.0.8 # homeassistant.components.daikin -pydaikin==2.13.1 +pydaikin==2.13.2 # homeassistant.components.deconz pydeconz==116 From 4fdb11b0d8b5aab7397de4d00dab8e6550d78362 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 10 Aug 2024 18:31:17 +0200 Subject: [PATCH 022/271] Bump AirGradient to 0.8.0 (#123527) --- homeassistant/components/airgradient/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airgradient/manifest.json b/homeassistant/components/airgradient/manifest.json index efb18ae5752..fed4fafdc74 100644 --- a/homeassistant/components/airgradient/manifest.json +++ b/homeassistant/components/airgradient/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/airgradient", "integration_type": "device", "iot_class": "local_polling", - "requirements": ["airgradient==0.7.1"], + "requirements": ["airgradient==0.8.0"], "zeroconf": ["_airgradient._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 3f4407d6f54..c017f83d132 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -410,7 +410,7 @@ aiowithings==3.0.2 aioymaps==1.2.5 # homeassistant.components.airgradient -airgradient==0.7.1 +airgradient==0.8.0 # homeassistant.components.airly airly==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6164a5803e5..3000c0adacd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -392,7 +392,7 @@ aiowithings==3.0.2 aioymaps==1.2.5 # homeassistant.components.airgradient -airgradient==0.7.1 +airgradient==0.8.0 # homeassistant.components.airly airly==1.1.0 From 723b7bd5326dd22542ae38d12bb5ea3873167e7a Mon Sep 17 00:00:00 2001 From: cnico Date: Sat, 10 Aug 2024 17:01:49 +0200 Subject: [PATCH 023/271] Upgrade chacon_dio_api to version 1.2.0 (#123528) Upgrade api version 1.2.0 with the first user feedback improvement --- homeassistant/components/chacon_dio/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/chacon_dio/manifest.json b/homeassistant/components/chacon_dio/manifest.json index d077b130da9..c0f4059e798 100644 --- a/homeassistant/components/chacon_dio/manifest.json +++ b/homeassistant/components/chacon_dio/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/chacon_dio", "iot_class": "cloud_push", "loggers": ["dio_chacon_api"], - "requirements": ["dio-chacon-wifi-api==1.1.0"] + "requirements": ["dio-chacon-wifi-api==1.2.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index c017f83d132..80bd24480fe 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -735,7 +735,7 @@ devolo-home-control-api==0.18.3 devolo-plc-api==1.4.1 # homeassistant.components.chacon_dio -dio-chacon-wifi-api==1.1.0 +dio-chacon-wifi-api==1.2.0 # homeassistant.components.directv directv==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3000c0adacd..f3abaff9622 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -628,7 +628,7 @@ devolo-home-control-api==0.18.3 devolo-plc-api==1.4.1 # homeassistant.components.chacon_dio -dio-chacon-wifi-api==1.1.0 +dio-chacon-wifi-api==1.2.0 # homeassistant.components.directv directv==0.4.0 From 2ef337ec2e519e7563a499c8b90dde602fe4413e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 10 Aug 2024 18:41:57 +0200 Subject: [PATCH 024/271] Bump version to 2024.8.1 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 402f57a4f8b..c76bcdaf4b8 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0" +PATCH_VERSION: Final = "1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 36bc214554b..726141ed827 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.8.0" +version = "2024.8.1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From e7ae5c5c241687fff0ded7e54114a6cec3963d33 Mon Sep 17 00:00:00 2001 From: wittypluck Date: Sun, 11 Aug 2024 19:14:43 +0200 Subject: [PATCH 025/271] Avoid Exception on Glances missing key (#114628) * Handle case of sensors removed server side * Update available state on value update * Set uptime to None if key is missing * Replace _attr_available by _data_valid --- .../components/glances/coordinator.py | 10 ++--- homeassistant/components/glances/sensor.py | 25 ++++++------ tests/components/glances/test_sensor.py | 38 +++++++++++++++++++ 3 files changed, 56 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/glances/coordinator.py b/homeassistant/components/glances/coordinator.py index 4e5bdcc1543..8882b097ba9 100644 --- a/homeassistant/components/glances/coordinator.py +++ b/homeassistant/components/glances/coordinator.py @@ -45,15 +45,13 @@ class GlancesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): except exceptions.GlancesApiError as err: raise UpdateFailed from err # Update computed values - uptime: datetime | None = self.data["computed"]["uptime"] if self.data else None + uptime: datetime | None = None up_duration: timedelta | None = None - if up_duration := parse_duration(data.get("uptime")): + if "uptime" in data and (up_duration := parse_duration(data["uptime"])): + uptime = self.data["computed"]["uptime"] if self.data else None # Update uptime if previous value is None or previous uptime is bigger than # new uptime (i.e. server restarted) - if ( - self.data is None - or self.data["computed"]["uptime_duration"] > up_duration - ): + if uptime is None or self.data["computed"]["uptime_duration"] > up_duration: uptime = utcnow() - up_duration data["computed"] = {"uptime_duration": up_duration, "uptime": uptime} return data or {} diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index a1cb8e47b9d..59eba69d60a 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -325,6 +325,7 @@ class GlancesSensor(CoordinatorEntity[GlancesDataUpdateCoordinator], SensorEntit entity_description: GlancesSensorEntityDescription _attr_has_entity_name = True + _data_valid: bool = False def __init__( self, @@ -351,14 +352,7 @@ class GlancesSensor(CoordinatorEntity[GlancesDataUpdateCoordinator], SensorEntit @property def available(self) -> bool: """Set sensor unavailable when native value is invalid.""" - if super().available: - return ( - not self._numeric_state_expected - or isinstance(value := self.native_value, (int, float)) - or isinstance(value, str) - and value.isnumeric() - ) - return False + return super().available and self._data_valid @callback def _handle_coordinator_update(self) -> None: @@ -368,10 +362,19 @@ class GlancesSensor(CoordinatorEntity[GlancesDataUpdateCoordinator], SensorEntit def _update_native_value(self) -> None: """Update sensor native value from coordinator data.""" - data = self.coordinator.data[self.entity_description.type] - if dict_val := data.get(self._sensor_label): + data = self.coordinator.data.get(self.entity_description.type) + if data and (dict_val := data.get(self._sensor_label)): self._attr_native_value = dict_val.get(self.entity_description.key) - elif self.entity_description.key in data: + elif data and (self.entity_description.key in data): self._attr_native_value = data.get(self.entity_description.key) else: self._attr_native_value = None + self._update_data_valid() + + def _update_data_valid(self) -> None: + self._data_valid = self._attr_native_value is not None and ( + not self._numeric_state_expected + or isinstance(self._attr_native_value, (int, float)) + or isinstance(self._attr_native_value, str) + and self._attr_native_value.isnumeric() + ) diff --git a/tests/components/glances/test_sensor.py b/tests/components/glances/test_sensor.py index 7dee47680ed..8e0367a712c 100644 --- a/tests/components/glances/test_sensor.py +++ b/tests/components/glances/test_sensor.py @@ -7,6 +7,7 @@ from freezegun.api import FrozenDateTimeFactory from syrupy import SnapshotAssertion from homeassistant.components.glances.const import DOMAIN +from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -71,3 +72,40 @@ async def test_uptime_variation( async_fire_time_changed(hass) await hass.async_block_till_done() assert hass.states.get("sensor.0_0_0_0_uptime").state == "2024-02-15T12:49:52+00:00" + + +async def test_sensor_removed( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_api: AsyncMock, + entity_registry: er.EntityRegistry, +) -> None: + """Test sensor removed server side.""" + + # Init with reference time + freezer.move_to(MOCK_REFERENCE_DATE) + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_INPUT, entry_id="test") + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get("sensor.0_0_0_0_ssl_disk_used").state != STATE_UNAVAILABLE + assert hass.states.get("sensor.0_0_0_0_memory_use").state != STATE_UNAVAILABLE + assert hass.states.get("sensor.0_0_0_0_uptime").state != STATE_UNAVAILABLE + + # Remove some sensors from Glances API data + mock_data = HA_SENSOR_DATA.copy() + mock_data.pop("fs") + mock_data.pop("mem") + mock_data.pop("uptime") + mock_api.return_value.get_ha_sensor_data = AsyncMock(return_value=mock_data) + + # Server stops providing some sensors, so state should switch to Unavailable + freezer.move_to(MOCK_REFERENCE_DATE + timedelta(minutes=2)) + freezer.tick(delta=timedelta(seconds=120)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert hass.states.get("sensor.0_0_0_0_ssl_disk_used").state == STATE_UNAVAILABLE + assert hass.states.get("sensor.0_0_0_0_memory_use").state == STATE_UNAVAILABLE + assert hass.states.get("sensor.0_0_0_0_uptime").state == STATE_UNAVAILABLE From 742c7ba23f4ef76e15d1894c7b9f7d67f2a0126d Mon Sep 17 00:00:00 2001 From: ilan <31193909+iloveicedgreentea@users.noreply.github.com> Date: Wed, 14 Aug 2024 16:06:57 -0400 Subject: [PATCH 026/271] Fix Madvr sensor values on startup (#122479) * fix: add startup values * fix: update snap * fix: use native value to show None --- homeassistant/components/madvr/sensor.py | 13 ++++++++++++- tests/components/madvr/test_sensors.py | 13 +++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/madvr/sensor.py b/homeassistant/components/madvr/sensor.py index 6f0933ac879..047b8bb83e6 100644 --- a/homeassistant/components/madvr/sensor.py +++ b/homeassistant/components/madvr/sensor.py @@ -277,4 +277,15 @@ class MadvrSensor(MadVREntity, SensorEntity): @property def native_value(self) -> float | str | None: """Return the state of the sensor.""" - return self.entity_description.value_fn(self.coordinator) + val = self.entity_description.value_fn(self.coordinator) + # check if sensor is enum + if self.entity_description.device_class == SensorDeviceClass.ENUM: + if ( + self.entity_description.options + and val in self.entity_description.options + ): + return val + # return None for values that are not in the options + return None + + return val diff --git a/tests/components/madvr/test_sensors.py b/tests/components/madvr/test_sensors.py index 25dcc1cdcca..ddc01fc737a 100644 --- a/tests/components/madvr/test_sensors.py +++ b/tests/components/madvr/test_sensors.py @@ -93,3 +93,16 @@ async def test_sensor_setup_and_states( # test get_temperature ValueError assert get_temperature(None, "temp_key") is None + + # test startup placeholder values + update_callback({"outgoing_bit_depth": "0bit"}) + await hass.async_block_till_done() + assert ( + hass.states.get("sensor.madvr_envy_outgoing_bit_depth").state == STATE_UNKNOWN + ) + + update_callback({"outgoing_color_space": "?"}) + await hass.async_block_till_done() + assert ( + hass.states.get("sensor.madvr_envy_outgoing_color_space").state == STATE_UNKNOWN + ) From f9ae2b4453b1a6cd150c6458105783e6b47eaa35 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 14 Aug 2024 09:31:37 +0200 Subject: [PATCH 027/271] Drop violating rows before adding foreign constraints in DB schema 44 migration (#123454) * Drop violating rows before adding foreign constraints * Don't delete rows with null-references * Only delete rows when integrityerror is caught * Move restore of dropped foreign key constraints to a separate migration step * Use aliases for tables * Update homeassistant/components/recorder/migration.py * Update test * Don't use alias for table we're deleting from, improve test * Fix MySQL * Update instead of deleting in case of self references * Improve log messages * Batch updates * Add workaround for unsupported LIMIT in PostgreSQL * Simplify --------- Co-authored-by: J. Nick Koston --- .../components/recorder/db_schema.py | 2 +- .../components/recorder/migration.py | 239 +++++++++++++++--- tests/components/recorder/test_migrate.py | 115 +++++++-- 3 files changed, 304 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/recorder/db_schema.py b/homeassistant/components/recorder/db_schema.py index 8d4cc29d9be..dd293ed6bc2 100644 --- a/homeassistant/components/recorder/db_schema.py +++ b/homeassistant/components/recorder/db_schema.py @@ -77,7 +77,7 @@ class LegacyBase(DeclarativeBase): """Base class for tables, used for schema migration.""" -SCHEMA_VERSION = 44 +SCHEMA_VERSION = 45 _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index a41de07e243..55856dcf449 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -669,33 +669,177 @@ def _drop_foreign_key_constraints( def _restore_foreign_key_constraints( session_maker: Callable[[], Session], engine: Engine, - dropped_constraints: list[tuple[str, str, ReflectedForeignKeyConstraint]], + foreign_columns: list[tuple[str, str, str | None, str | None]], ) -> None: """Restore foreign key constraints.""" - for table, column, dropped_constraint in dropped_constraints: + for table, column, foreign_table, foreign_column in foreign_columns: constraints = Base.metadata.tables[table].foreign_key_constraints for constraint in constraints: if constraint.column_keys == [column]: break else: - _LOGGER.info( - "Did not find a matching constraint for %s", dropped_constraint - ) + _LOGGER.info("Did not find a matching constraint for %s.%s", table, column) continue + if TYPE_CHECKING: + assert foreign_table is not None + assert foreign_column is not None + # AddConstraint mutates the constraint passed to it, we need to # undo that to avoid changing the behavior of the table schema. # https://github.com/sqlalchemy/sqlalchemy/blob/96f1172812f858fead45cdc7874abac76f45b339/lib/sqlalchemy/sql/ddl.py#L746-L748 create_rule = constraint._create_rule # noqa: SLF001 add_constraint = AddConstraint(constraint) # type: ignore[no-untyped-call] constraint._create_rule = create_rule # noqa: SLF001 + try: + _add_constraint(session_maker, add_constraint, table, column) + except IntegrityError: + _LOGGER.exception( + ( + "Could not update foreign options in %s table, will delete " + "violations and try again" + ), + table, + ) + _delete_foreign_key_violations( + session_maker, engine, table, column, foreign_table, foreign_column + ) + _add_constraint(session_maker, add_constraint, table, column) - with session_scope(session=session_maker()) as session: - try: - connection = session.connection() - connection.execute(add_constraint) - except (InternalError, OperationalError): - _LOGGER.exception("Could not update foreign options in %s table", table) + +def _add_constraint( + session_maker: Callable[[], Session], + add_constraint: AddConstraint, + table: str, + column: str, +) -> None: + """Add a foreign key constraint.""" + _LOGGER.warning( + "Adding foreign key constraint to %s.%s. " + "Note: this can take several minutes on large databases and slow " + "machines. Please be patient!", + table, + column, + ) + with session_scope(session=session_maker()) as session: + try: + connection = session.connection() + connection.execute(add_constraint) + except (InternalError, OperationalError): + _LOGGER.exception("Could not update foreign options in %s table", table) + + +def _delete_foreign_key_violations( + session_maker: Callable[[], Session], + engine: Engine, + table: str, + column: str, + foreign_table: str, + foreign_column: str, +) -> None: + """Remove rows which violate the constraints.""" + if engine.dialect.name not in (SupportedDialect.MYSQL, SupportedDialect.POSTGRESQL): + raise RuntimeError( + f"_delete_foreign_key_violations not supported for {engine.dialect.name}" + ) + + _LOGGER.warning( + "Rows in table %s where %s references non existing %s.%s will be %s. " + "Note: this can take several minutes on large databases and slow " + "machines. Please be patient!", + table, + column, + foreign_table, + foreign_column, + "set to NULL" if table == foreign_table else "deleted", + ) + + result: CursorResult | None = None + if table == foreign_table: + # In case of a foreign reference to the same table, we set invalid + # references to NULL instead of deleting as deleting rows may + # cause additional invalid references to be created. This is to handle + # old_state_id referencing a missing state. + if engine.dialect.name == SupportedDialect.MYSQL: + while result is None or result.rowcount > 0: + with session_scope(session=session_maker()) as session: + # The subquery (SELECT {foreign_column} from {foreign_table}) is + # to be compatible with old MySQL versions which do not allow + # referencing the table being updated in the WHERE clause. + result = session.connection().execute( + text( + f"UPDATE {table} as t1 " # noqa: S608 + f"SET {column} = NULL " + "WHERE (" + f"t1.{column} IS NOT NULL AND " + "NOT EXISTS " + "(SELECT 1 " + f"FROM (SELECT {foreign_column} from {foreign_table}) AS t2 " + f"WHERE t2.{foreign_column} = t1.{column})) " + "LIMIT 100000;" + ) + ) + elif engine.dialect.name == SupportedDialect.POSTGRESQL: + while result is None or result.rowcount > 0: + with session_scope(session=session_maker()) as session: + # PostgreSQL does not support LIMIT in UPDATE clauses, so we + # update matches from a limited subquery instead. + result = session.connection().execute( + text( + f"UPDATE {table} " # noqa: S608 + f"SET {column} = NULL " + f"WHERE {column} in " + f"(SELECT {column} from {table} as t1 " + "WHERE (" + f"t1.{column} IS NOT NULL AND " + "NOT EXISTS " + "(SELECT 1 " + f"FROM {foreign_table} AS t2 " + f"WHERE t2.{foreign_column} = t1.{column})) " + "LIMIT 100000);" + ) + ) + return + + if engine.dialect.name == SupportedDialect.MYSQL: + while result is None or result.rowcount > 0: + with session_scope(session=session_maker()) as session: + result = session.connection().execute( + # We don't use an alias for the table we're deleting from, + # support of the form `DELETE FROM table AS t1` was added in + # MariaDB 11.6 and is not supported by MySQL. Those engines + # instead support the from `DELETE t1 from table AS t1` which + # is not supported by PostgreSQL and undocumented for MariaDB. + text( + f"DELETE FROM {table} " # noqa: S608 + "WHERE (" + f"{table}.{column} IS NOT NULL AND " + "NOT EXISTS " + "(SELECT 1 " + f"FROM {foreign_table} AS t2 " + f"WHERE t2.{foreign_column} = {table}.{column})) " + "LIMIT 100000;" + ) + ) + elif engine.dialect.name == SupportedDialect.POSTGRESQL: + while result is None or result.rowcount > 0: + with session_scope(session=session_maker()) as session: + # PostgreSQL does not support LIMIT in DELETE clauses, so we + # delete matches from a limited subquery instead. + result = session.connection().execute( + text( + f"DELETE FROM {table} " # noqa: S608 + f"WHERE {column} in " + f"(SELECT {column} from {table} as t1 " + "WHERE (" + f"t1.{column} IS NOT NULL AND " + "NOT EXISTS " + "(SELECT 1 " + f"FROM {foreign_table} AS t2 " + f"WHERE t2.{foreign_column} = t1.{column})) " + "LIMIT 100000);" + ) + ) @database_job_retry_wrapper("Apply migration update", 10) @@ -1459,6 +1603,38 @@ class _SchemaVersion43Migrator(_SchemaVersionMigrator, target_version=43): ) +FOREIGN_COLUMNS = ( + ( + "events", + ("data_id", "event_type_id"), + ( + ("data_id", "event_data", "data_id"), + ("event_type_id", "event_types", "event_type_id"), + ), + ), + ( + "states", + ("event_id", "old_state_id", "attributes_id", "metadata_id"), + ( + ("event_id", None, None), + ("old_state_id", "states", "state_id"), + ("attributes_id", "state_attributes", "attributes_id"), + ("metadata_id", "states_meta", "metadata_id"), + ), + ), + ( + "statistics", + ("metadata_id",), + (("metadata_id", "statistics_meta", "id"),), + ), + ( + "statistics_short_term", + ("metadata_id",), + (("metadata_id", "statistics_meta", "id"),), + ), +) + + class _SchemaVersion44Migrator(_SchemaVersionMigrator, target_version=44): def _apply_update(self) -> None: """Version specific update method.""" @@ -1471,24 +1647,14 @@ class _SchemaVersion44Migrator(_SchemaVersionMigrator, target_version=44): else "" ) # First drop foreign key constraints - foreign_columns = ( - ("events", ("data_id", "event_type_id")), - ("states", ("event_id", "old_state_id", "attributes_id", "metadata_id")), - ("statistics", ("metadata_id",)), - ("statistics_short_term", ("metadata_id",)), - ) - dropped_constraints = [ - dropped_constraint - for table, columns in foreign_columns - for column in columns - for dropped_constraint in _drop_foreign_key_constraints( - self.session_maker, self.engine, table, column - )[1] - ] - _LOGGER.debug("Dropped foreign key constraints: %s", dropped_constraints) + for table, columns, _ in FOREIGN_COLUMNS: + for column in columns: + _drop_foreign_key_constraints( + self.session_maker, self.engine, table, column + ) # Then modify the constrained columns - for table, columns in foreign_columns: + for table, columns, _ in FOREIGN_COLUMNS: _modify_columns( self.session_maker, self.engine, @@ -1518,9 +1684,24 @@ class _SchemaVersion44Migrator(_SchemaVersionMigrator, target_version=44): table, [f"{column} {BIG_INTEGER_SQL} {identity_sql}"], ) - # Finally restore dropped constraints + + +class _SchemaVersion45Migrator(_SchemaVersionMigrator, target_version=45): + def _apply_update(self) -> None: + """Version specific update method.""" + # We skip this step for SQLITE, it doesn't have differently sized integers + if self.engine.dialect.name == SupportedDialect.SQLITE: + return + + # Restore constraints dropped in migration to schema version 44 _restore_foreign_key_constraints( - self.session_maker, self.engine, dropped_constraints + self.session_maker, + self.engine, + [ + (table, column, foreign_table, foreign_column) + for table, _, foreign_mappings in FOREIGN_COLUMNS + for column, foreign_table, foreign_column in foreign_mappings + ], ) diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index e55793caad7..988eade29b6 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -831,9 +831,9 @@ def test_drop_restore_foreign_key_constraints(recorder_db_url: str) -> None: """ constraints_to_recreate = ( - ("events", "data_id"), - ("states", "event_id"), # This won't be found - ("states", "old_state_id"), + ("events", "data_id", "event_data", "data_id"), + ("states", "event_id", None, None), # This won't be found + ("states", "old_state_id", "states", "state_id"), ) db_engine = recorder_db_url.partition("://")[0] @@ -902,7 +902,7 @@ def test_drop_restore_foreign_key_constraints(recorder_db_url: str) -> None: session_maker = Mock(return_value=session) dropped_constraints_1 = [ dropped_constraint - for table, column in constraints_to_recreate + for table, column, _, _ in constraints_to_recreate for dropped_constraint in migration._drop_foreign_key_constraints( session_maker, engine, table, column )[1] @@ -914,7 +914,7 @@ def test_drop_restore_foreign_key_constraints(recorder_db_url: str) -> None: session_maker = Mock(return_value=session) dropped_constraints_2 = [ dropped_constraint - for table, column in constraints_to_recreate + for table, column, _, _ in constraints_to_recreate for dropped_constraint in migration._drop_foreign_key_constraints( session_maker, engine, table, column )[1] @@ -925,7 +925,7 @@ def test_drop_restore_foreign_key_constraints(recorder_db_url: str) -> None: with Session(engine) as session: session_maker = Mock(return_value=session) migration._restore_foreign_key_constraints( - session_maker, engine, dropped_constraints_1 + session_maker, engine, constraints_to_recreate ) # Check we do find the constrained columns again (they are restored) @@ -933,7 +933,7 @@ def test_drop_restore_foreign_key_constraints(recorder_db_url: str) -> None: session_maker = Mock(return_value=session) dropped_constraints_3 = [ dropped_constraint - for table, column in constraints_to_recreate + for table, column, _, _ in constraints_to_recreate for dropped_constraint in migration._drop_foreign_key_constraints( session_maker, engine, table, column )[1] @@ -951,21 +951,7 @@ def test_restore_foreign_key_constraints_with_error( This is not supported on SQLite """ - constraints_to_restore = [ - ( - "events", - "data_id", - { - "comment": None, - "constrained_columns": ["data_id"], - "name": "events_data_id_fkey", - "options": {}, - "referred_columns": ["data_id"], - "referred_schema": None, - "referred_table": "event_data", - }, - ), - ] + constraints_to_restore = [("events", "data_id", "event_data", "data_id")] connection = Mock() connection.execute = Mock(side_effect=InternalError(None, None, None)) @@ -981,3 +967,88 @@ def test_restore_foreign_key_constraints_with_error( ) assert "Could not update foreign options in events table" in caplog.text + + +@pytest.mark.skip_on_db_engine(["sqlite"]) +@pytest.mark.usefixtures("skip_by_db_engine") +def test_restore_foreign_key_constraints_with_integrity_error( + recorder_db_url: str, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test we can drop and then restore foreign keys. + + This is not supported on SQLite + """ + + constraints = ( + ("events", "data_id", "event_data", "data_id", Events), + ("states", "old_state_id", "states", "state_id", States), + ) + + engine = create_engine(recorder_db_url) + db_schema.Base.metadata.create_all(engine) + + # Drop constraints + with Session(engine) as session: + session_maker = Mock(return_value=session) + for table, column, _, _, _ in constraints: + migration._drop_foreign_key_constraints( + session_maker, engine, table, column + ) + + # Add rows violating the constraints + with Session(engine) as session: + for _, column, _, _, table_class in constraints: + session.add(table_class(**{column: 123})) + session.add(table_class()) + # Insert a States row referencing the row with an invalid foreign reference + session.add(States(old_state_id=1)) + session.commit() + + # Check we could insert the rows + with Session(engine) as session: + assert session.query(Events).count() == 2 + assert session.query(States).count() == 3 + + # Restore constraints + to_restore = [ + (table, column, foreign_table, foreign_column) + for table, column, foreign_table, foreign_column, _ in constraints + ] + with Session(engine) as session: + session_maker = Mock(return_value=session) + migration._restore_foreign_key_constraints(session_maker, engine, to_restore) + + # Check the violating row has been deleted from the Events table + with Session(engine) as session: + assert session.query(Events).count() == 1 + assert session.query(States).count() == 3 + + engine.dispose() + + assert ( + "Could not update foreign options in events table, " + "will delete violations and try again" + ) in caplog.text + + +def test_delete_foreign_key_violations_unsupported_engine( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test calling _delete_foreign_key_violations with an unsupported engine.""" + + connection = Mock() + connection.execute = Mock(side_effect=InternalError(None, None, None)) + session = Mock() + session.connection = Mock(return_value=connection) + instance = Mock() + instance.get_session = Mock(return_value=session) + engine = Mock() + engine.dialect = Mock() + engine.dialect.name = "sqlite" + + session_maker = Mock(return_value=session) + with pytest.raises( + RuntimeError, match="_delete_foreign_key_violations not supported for sqlite" + ): + migration._delete_foreign_key_violations(session_maker, engine, "", "", "", "") From 059d3eed986373fd54405c1a2bb947a481177a64 Mon Sep 17 00:00:00 2001 From: "Phill (pssc)" Date: Thu, 15 Aug 2024 09:03:03 +0100 Subject: [PATCH 028/271] Handle Yamaha ValueError (#123547) * fix yamaha remove info logging * ruff * fix yamnaha supress rxv.find UnicodeDecodeError * fix formatting * make more realistic * make more realistic and use parms * add value error after more feedback * ruff format * Update homeassistant/components/yamaha/media_player.py Co-authored-by: Martin Hjelmare * remove unused method * add more debugging * Increase discovery timeout add more debug allow config to overrite dicovery for name --------- Co-authored-by: Martin Hjelmare --- homeassistant/components/yamaha/const.py | 1 + .../components/yamaha/media_player.py | 28 +++++++++++++++---- tests/components/yamaha/test_media_player.py | 20 +++++++++---- 3 files changed, 37 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/yamaha/const.py b/homeassistant/components/yamaha/const.py index 492babe9657..1cdb619b6ef 100644 --- a/homeassistant/components/yamaha/const.py +++ b/homeassistant/components/yamaha/const.py @@ -1,6 +1,7 @@ """Constants for the Yamaha component.""" DOMAIN = "yamaha" +DISCOVER_TIMEOUT = 3 KNOWN_ZONES = "known_zones" CURSOR_TYPE_DOWN = "down" CURSOR_TYPE_LEFT = "left" diff --git a/homeassistant/components/yamaha/media_player.py b/homeassistant/components/yamaha/media_player.py index a8200ea3373..fd47bcec041 100644 --- a/homeassistant/components/yamaha/media_player.py +++ b/homeassistant/components/yamaha/media_player.py @@ -31,6 +31,7 @@ from .const import ( CURSOR_TYPE_RIGHT, CURSOR_TYPE_SELECT, CURSOR_TYPE_UP, + DISCOVER_TIMEOUT, DOMAIN, KNOWN_ZONES, SERVICE_ENABLE_OUTPUT, @@ -125,18 +126,33 @@ def _discovery(config_info): elif config_info.host is None: _LOGGER.debug("Config No Host Supplied Zones") zones = [] - for recv in rxv.find(): + for recv in rxv.find(DISCOVER_TIMEOUT): zones.extend(recv.zone_controllers()) else: _LOGGER.debug("Config Zones") zones = None # Fix for upstream issues in rxv.find() with some hardware. - with contextlib.suppress(AttributeError): - for recv in rxv.find(): + with contextlib.suppress(AttributeError, ValueError): + for recv in rxv.find(DISCOVER_TIMEOUT): + _LOGGER.debug( + "Found Serial %s %s %s", + recv.serial_number, + recv.ctrl_url, + recv.zone, + ) if recv.ctrl_url == config_info.ctrl_url: - _LOGGER.debug("Config Zones Matched %s", config_info.ctrl_url) - zones = recv.zone_controllers() + _LOGGER.debug( + "Config Zones Matched Serial %s: %s", + recv.ctrl_url, + recv.serial_number, + ) + zones = rxv.RXV( + config_info.ctrl_url, + friendly_name=config_info.name, + serial_number=recv.serial_number, + model_name=recv.model_name, + ).zone_controllers() break if not zones: @@ -170,7 +186,7 @@ async def async_setup_platform( entities = [] for zctrl in zone_ctrls: - _LOGGER.debug("Receiver zone: %s", zctrl.zone) + _LOGGER.debug("Receiver zone: %s serial %s", zctrl.zone, zctrl.serial_number) if config_info.zone_ignore and zctrl.zone in config_info.zone_ignore: _LOGGER.debug("Ignore receiver zone: %s %s", config_info.name, zctrl.zone) continue diff --git a/tests/components/yamaha/test_media_player.py b/tests/components/yamaha/test_media_player.py index 804b800aaef..6a5729a70b3 100644 --- a/tests/components/yamaha/test_media_player.py +++ b/tests/components/yamaha/test_media_player.py @@ -86,17 +86,25 @@ async def test_setup_host(hass: HomeAssistant, device, device2, main_zone) -> No assert state.state == "off" -async def test_setup_attribute_error(hass: HomeAssistant, device, main_zone) -> None: - """Test set up integration encountering an Attribute Error.""" +@pytest.mark.parametrize( + ("error"), + [ + AttributeError, + ValueError, + UnicodeDecodeError("", b"", 1, 0, ""), + ], +) +async def test_setup_find_errors(hass: HomeAssistant, device, main_zone, error) -> None: + """Test set up integration encountering an Error.""" - with patch("rxv.find", side_effect=AttributeError): + with patch("rxv.find", side_effect=error): assert await async_setup_component(hass, MP_DOMAIN, CONFIG) await hass.async_block_till_done() - state = hass.states.get("media_player.yamaha_receiver_main_zone") + state = hass.states.get("media_player.yamaha_receiver_main_zone") - assert state is not None - assert state.state == "off" + assert state is not None + assert state.state == "off" async def test_setup_no_host(hass: HomeAssistant, device, main_zone) -> None: From c886587915fba4f56b41bc957ff54cc6e6720442 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 10 Aug 2024 15:09:18 -0500 Subject: [PATCH 029/271] Bump aiohttp to 3.10.3 (#123549) --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e34b398ac3a..05b2bebceea 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodhcpwatcher==1.0.2 aiodiscover==2.1.0 aiodns==3.2.0 aiohttp-fast-zlib==0.1.1 -aiohttp==3.10.2 +aiohttp==3.10.3 aiohttp_cors==0.7.0 aiozoneinfo==0.2.1 astral==2.2 diff --git a/pyproject.toml b/pyproject.toml index 726141ed827..39e1cb4d221 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ classifiers = [ requires-python = ">=3.12.0" dependencies = [ "aiodns==3.2.0", - "aiohttp==3.10.2", + "aiohttp==3.10.3", "aiohttp_cors==0.7.0", "aiohttp-fast-zlib==0.1.1", "aiozoneinfo==0.2.1", diff --git a/requirements.txt b/requirements.txt index e1bded8b335..7a4b0bd6d09 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ # Home Assistant Core aiodns==3.2.0 -aiohttp==3.10.2 +aiohttp==3.10.3 aiohttp_cors==0.7.0 aiohttp-fast-zlib==0.1.1 aiozoneinfo==0.2.1 From 6b81fa89d31ff270f62305533ade63e783c96956 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Mon, 12 Aug 2024 08:55:24 +0200 Subject: [PATCH 030/271] Update knx-frontend to 2024.8.9.225351 (#123557) --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 62364f641f4..6974ee300f5 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -13,7 +13,7 @@ "requirements": [ "xknx==3.0.0", "xknxproject==3.7.1", - "knx-frontend==2024.8.6.211307" + "knx-frontend==2024.8.9.225351" ], "single_config_entry": true } diff --git a/requirements_all.txt b/requirements_all.txt index 80bd24480fe..afe207ea53e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1219,7 +1219,7 @@ kiwiki-client==0.1.1 knocki==0.3.1 # homeassistant.components.knx -knx-frontend==2024.8.6.211307 +knx-frontend==2024.8.9.225351 # homeassistant.components.konnected konnected==1.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f3abaff9622..b5741a1f8f3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1015,7 +1015,7 @@ kegtron-ble==0.4.0 knocki==0.3.1 # homeassistant.components.knx -knx-frontend==2024.8.6.211307 +knx-frontend==2024.8.9.225351 # homeassistant.components.konnected konnected==1.2.0 From e2f4aa893f669d724d9f81c0122b0e7c0987af49 Mon Sep 17 00:00:00 2001 From: Noah Husby <32528627+noahhusby@users.noreply.github.com> Date: Mon, 12 Aug 2024 15:45:05 -0400 Subject: [PATCH 031/271] Fix secondary russound controller discovery failure (#123590) --- .../components/russound_rio/manifest.json | 2 +- .../components/russound_rio/media_player.py | 17 ++++++++++++++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/russound_rio/manifest.json b/homeassistant/components/russound_rio/manifest.json index e7bb99010ee..67a01239615 100644 --- a/homeassistant/components/russound_rio/manifest.json +++ b/homeassistant/components/russound_rio/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/russound_rio", "iot_class": "local_push", "loggers": ["aiorussound"], - "requirements": ["aiorussound==2.2.2"] + "requirements": ["aiorussound==2.2.3"] } diff --git a/homeassistant/components/russound_rio/media_player.py b/homeassistant/components/russound_rio/media_player.py index 1489f12e59c..ff0d9e006c0 100644 --- a/homeassistant/components/russound_rio/media_player.py +++ b/homeassistant/components/russound_rio/media_player.py @@ -128,11 +128,18 @@ class RussoundZoneDevice(MediaPlayerEntity): self._zone = zone self._sources = sources self._attr_name = zone.name - self._attr_unique_id = f"{self._controller.mac_address}-{zone.device_str()}" + primary_mac_address = ( + self._controller.mac_address + or self._controller.parent_controller.mac_address + ) + self._attr_unique_id = f"{primary_mac_address}-{zone.device_str()}" + device_identifier = ( + self._controller.mac_address + or f"{primary_mac_address}-{self._controller.controller_id}" + ) self._attr_device_info = DeviceInfo( # Use MAC address of Russound device as identifier - identifiers={(DOMAIN, self._controller.mac_address)}, - connections={(CONNECTION_NETWORK_MAC, self._controller.mac_address)}, + identifiers={(DOMAIN, device_identifier)}, manufacturer="Russound", name=self._controller.controller_type, model=self._controller.controller_type, @@ -143,6 +150,10 @@ class RussoundZoneDevice(MediaPlayerEntity): DOMAIN, self._controller.parent_controller.mac_address, ) + else: + self._attr_device_info["connections"] = { + (CONNECTION_NETWORK_MAC, self._controller.mac_address) + } for flag, feature in MP_FEATURES_BY_FLAG.items(): if flag in zone.instance.supported_features: self._attr_supported_features |= feature diff --git a/requirements_all.txt b/requirements_all.txt index afe207ea53e..755a06a849f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -350,7 +350,7 @@ aioridwell==2024.01.0 aioruckus==0.34 # homeassistant.components.russound_rio -aiorussound==2.2.2 +aiorussound==2.2.3 # homeassistant.components.ruuvi_gateway aioruuvigateway==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b5741a1f8f3..fb91e994d1d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -332,7 +332,7 @@ aioridwell==2024.01.0 aioruckus==0.34 # homeassistant.components.russound_rio -aiorussound==2.2.2 +aiorussound==2.2.3 # homeassistant.components.ruuvi_gateway aioruuvigateway==0.1.0 From d98d0cdad090b55953e9037462152ba41fdab20a Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 12 Aug 2024 09:11:44 +0200 Subject: [PATCH 032/271] Change WoL to be secondary on device info (#123591) --- homeassistant/components/wake_on_lan/button.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/wake_on_lan/button.py b/homeassistant/components/wake_on_lan/button.py index 39c4511868d..87135a61380 100644 --- a/homeassistant/components/wake_on_lan/button.py +++ b/homeassistant/components/wake_on_lan/button.py @@ -15,8 +15,6 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN - _LOGGER = logging.getLogger(__name__) @@ -62,9 +60,8 @@ class WolButton(ButtonEntity): self._attr_unique_id = dr.format_mac(mac_address) self._attr_device_info = dr.DeviceInfo( connections={(dr.CONNECTION_NETWORK_MAC, self._attr_unique_id)}, - identifiers={(DOMAIN, self._attr_unique_id)}, - manufacturer="Wake on LAN", - name=name, + default_manufacturer="Wake on LAN", + default_name=name, ) async def async_press(self) -> None: From 725e2f16f5ee341db04bc0df1e16aba99e7b864d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 12 Aug 2024 07:54:57 -0500 Subject: [PATCH 033/271] Ensure HomeKit connection is kept alive for devices that timeout too quickly (#123601) --- .../homekit_controller/connection.py | 36 ++++++++++++++----- .../homekit_controller/test_connection.py | 15 ++++---- 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 0d21ff9ba1d..4da907daf3e 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -845,21 +845,41 @@ class HKDevice: async def async_update(self, now: datetime | None = None) -> None: """Poll state of all entities attached to this bridge/accessory.""" + to_poll = self.pollable_characteristics + accessories = self.entity_map.accessories + if ( - len(self.entity_map.accessories) == 1 + len(accessories) == 1 and self.available - and not (self.pollable_characteristics - self.watchable_characteristics) + and not (to_poll - self.watchable_characteristics) and self.pairing.is_available and await self.pairing.controller.async_reachable( self.unique_id, timeout=5.0 ) ): # If its a single accessory and all chars are watchable, - # we don't need to poll. - _LOGGER.debug("Accessory is reachable, skip polling: %s", self.unique_id) - return + # only poll the firmware version to keep the connection alive + # https://github.com/home-assistant/core/issues/123412 + # + # Firmware revision is used here since iOS does this to keep camera + # connections alive, and the goal is to not regress + # https://github.com/home-assistant/core/issues/116143 + # by polling characteristics that are not normally polled frequently + # and may not be tested by the device vendor. + # + _LOGGER.debug( + "Accessory is reachable, limiting poll to firmware version: %s", + self.unique_id, + ) + first_accessory = accessories[0] + accessory_info = first_accessory.services.first( + service_type=ServicesTypes.ACCESSORY_INFORMATION + ) + assert accessory_info is not None + firmware_iid = accessory_info[CharacteristicsTypes.FIRMWARE_REVISION].iid + to_poll = {(first_accessory.aid, firmware_iid)} - if not self.pollable_characteristics: + if not to_poll: self.async_update_available_state() _LOGGER.debug( "HomeKit connection not polling any characteristics: %s", self.unique_id @@ -892,9 +912,7 @@ class HKDevice: _LOGGER.debug("Starting HomeKit device update: %s", self.unique_id) try: - new_values_dict = await self.get_characteristics( - self.pollable_characteristics - ) + new_values_dict = await self.get_characteristics(to_poll) except AccessoryNotFoundError: # Not only did the connection fail, but also the accessory is not # visible on the network. diff --git a/tests/components/homekit_controller/test_connection.py b/tests/components/homekit_controller/test_connection.py index 60ef0b1c547..8d3cc02fab9 100644 --- a/tests/components/homekit_controller/test_connection.py +++ b/tests/components/homekit_controller/test_connection.py @@ -344,10 +344,10 @@ async def test_thread_provision_migration_failed(hass: HomeAssistant) -> None: assert config_entry.data["Connection"] == "BLE" -async def test_skip_polling_all_watchable_accessory_mode( +async def test_poll_firmware_version_only_all_watchable_accessory_mode( hass: HomeAssistant, get_next_aid: Callable[[], int] ) -> None: - """Test that we skip polling if available and all chars are watchable accessory mode.""" + """Test that we only poll firmware if available and all chars are watchable accessory mode.""" def _create_accessory(accessory): service = accessory.add_service(ServicesTypes.LIGHTBULB, name="TestDevice") @@ -370,7 +370,10 @@ async def test_skip_polling_all_watchable_accessory_mode( # Initial state is that the light is off state = await helper.poll_and_get_state() assert state.state == STATE_OFF - assert mock_get_characteristics.call_count == 0 + assert mock_get_characteristics.call_count == 2 + # Verify only firmware version is polled + assert mock_get_characteristics.call_args_list[0][0][0] == {(1, 7)} + assert mock_get_characteristics.call_args_list[1][0][0] == {(1, 7)} # Test device goes offline helper.pairing.available = False @@ -382,16 +385,16 @@ async def test_skip_polling_all_watchable_accessory_mode( state = await helper.poll_and_get_state() assert state.state == STATE_UNAVAILABLE # Tries twice before declaring unavailable - assert mock_get_characteristics.call_count == 2 + assert mock_get_characteristics.call_count == 4 # Test device comes back online helper.pairing.available = True state = await helper.poll_and_get_state() assert state.state == STATE_OFF - assert mock_get_characteristics.call_count == 3 + assert mock_get_characteristics.call_count == 6 # Next poll should not happen because its a single # accessory, available, and all chars are watchable state = await helper.poll_and_get_state() assert state.state == STATE_OFF - assert mock_get_characteristics.call_count == 3 + assert mock_get_characteristics.call_count == 8 From 9bf8c5a54b537996bb0ccff29448c2db8cd1afee Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 11 Aug 2024 19:56:12 +0200 Subject: [PATCH 034/271] Bump `aioshelly` to version 11.2.0 (#123602) Bump aioshelly to version 11.2.0 --- homeassistant/components/shelly/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index 1e65a51733d..c742b45632c 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -9,7 +9,7 @@ "iot_class": "local_push", "loggers": ["aioshelly"], "quality_scale": "platinum", - "requirements": ["aioshelly==11.1.0"], + "requirements": ["aioshelly==11.2.0"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 755a06a849f..7d62156071a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -359,7 +359,7 @@ aioruuvigateway==0.1.0 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==11.1.0 +aioshelly==11.2.0 # homeassistant.components.skybell aioskybell==22.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fb91e994d1d..143a36059c1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -341,7 +341,7 @@ aioruuvigateway==0.1.0 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==11.1.0 +aioshelly==11.2.0 # homeassistant.components.skybell aioskybell==22.7.0 From d512f327c53177157221820c829a603b06ea9c93 Mon Sep 17 00:00:00 2001 From: kingy444 Date: Wed, 14 Aug 2024 20:31:18 +1000 Subject: [PATCH 035/271] Bump pydaikin to 2.13.4 (#123623) * bump pydaikin to 2.13.3 * bump pydaikin to 2.13.4 --- homeassistant/components/daikin/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/daikin/manifest.json b/homeassistant/components/daikin/manifest.json index c5cb6064d88..0d93c0e25ad 100644 --- a/homeassistant/components/daikin/manifest.json +++ b/homeassistant/components/daikin/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/daikin", "iot_class": "local_polling", "loggers": ["pydaikin"], - "requirements": ["pydaikin==2.13.2"], + "requirements": ["pydaikin==2.13.4"], "zeroconf": ["_dkapi._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 7d62156071a..845e50d9ea1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1789,7 +1789,7 @@ pycsspeechtts==1.0.8 # pycups==1.9.73 # homeassistant.components.daikin -pydaikin==2.13.2 +pydaikin==2.13.4 # homeassistant.components.danfoss_air pydanfossair==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 143a36059c1..10fee5812bc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1436,7 +1436,7 @@ pycoolmasternet-async==0.1.5 pycsspeechtts==1.0.8 # homeassistant.components.daikin -pydaikin==2.13.2 +pydaikin==2.13.4 # homeassistant.components.deconz pydeconz==116 From c269d572596b74c53a74ec73ea4d0b912a980bf1 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Tue, 13 Aug 2024 12:15:58 +0100 Subject: [PATCH 036/271] System Bridge package updates (#123657) --- homeassistant/components/system_bridge/manifest.json | 2 +- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/system_bridge/manifest.json b/homeassistant/components/system_bridge/manifest.json index 80527de75cd..e886bcad150 100644 --- a/homeassistant/components/system_bridge/manifest.json +++ b/homeassistant/components/system_bridge/manifest.json @@ -10,6 +10,6 @@ "iot_class": "local_push", "loggers": ["systembridgeconnector"], "quality_scale": "silver", - "requirements": ["systembridgeconnector==4.1.0", "systembridgemodels==4.1.0"], + "requirements": ["systembridgeconnector==4.1.5", "systembridgemodels==4.2.4"], "zeroconf": ["_system-bridge._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 845e50d9ea1..fee381a20a3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2703,10 +2703,10 @@ switchbot-api==2.2.1 synology-srm==0.2.0 # homeassistant.components.system_bridge -systembridgeconnector==4.1.0 +systembridgeconnector==4.1.5 # homeassistant.components.system_bridge -systembridgemodels==4.1.0 +systembridgemodels==4.2.4 # homeassistant.components.tailscale tailscale==0.6.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 10fee5812bc..24998d6164f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2137,10 +2137,10 @@ surepy==0.9.0 switchbot-api==2.2.1 # homeassistant.components.system_bridge -systembridgeconnector==4.1.0 +systembridgeconnector==4.1.5 # homeassistant.components.system_bridge -systembridgemodels==4.1.0 +systembridgemodels==4.2.4 # homeassistant.components.tailscale tailscale==0.6.1 From a23b063922454bdd386dc7ffddd1a4529fc608eb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 12 Aug 2024 09:02:23 -0500 Subject: [PATCH 037/271] Bump aiohomekit to 3.2.2 (#123669) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 476d17d3515..007153aceaf 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -14,6 +14,6 @@ "documentation": "https://www.home-assistant.io/integrations/homekit_controller", "iot_class": "local_push", "loggers": ["aiohomekit", "commentjson"], - "requirements": ["aiohomekit==3.2.1"], + "requirements": ["aiohomekit==3.2.2"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index fee381a20a3..19d976481e2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -255,7 +255,7 @@ aioguardian==2022.07.0 aioharmony==0.2.10 # homeassistant.components.homekit_controller -aiohomekit==3.2.1 +aiohomekit==3.2.2 # homeassistant.components.hue aiohue==4.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 24998d6164f..0aeb0a61597 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -240,7 +240,7 @@ aioguardian==2022.07.0 aioharmony==0.2.10 # homeassistant.components.homekit_controller -aiohomekit==3.2.1 +aiohomekit==3.2.2 # homeassistant.components.hue aiohue==4.7.2 From 5ea447ba484a8f2b00f2957d32dbbbc5ec35a3c7 Mon Sep 17 00:00:00 2001 From: Cyrill Raccaud Date: Mon, 12 Aug 2024 17:01:06 +0200 Subject: [PATCH 038/271] Fix startup block from Swiss public transport (#123704) --- homeassistant/components/swiss_public_transport/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/swiss_public_transport/__init__.py b/homeassistant/components/swiss_public_transport/__init__.py index 1242c95269e..83b47d64f17 100644 --- a/homeassistant/components/swiss_public_transport/__init__.py +++ b/homeassistant/components/swiss_public_transport/__init__.py @@ -44,7 +44,7 @@ async def async_setup_entry( translation_key="request_timeout", translation_placeholders={ "config_title": entry.title, - "error": e, + "error": str(e), }, ) from e except OpendataTransportError as e: @@ -54,7 +54,7 @@ async def async_setup_entry( translation_placeholders={ **PLACEHOLDERS, "config_title": entry.title, - "error": e, + "error": str(e), }, ) from e From 050e2c94046e7cf4f8a9722aa4b6ba0105c42df4 Mon Sep 17 00:00:00 2001 From: David Knowles Date: Mon, 12 Aug 2024 13:01:07 -0400 Subject: [PATCH 039/271] Bump pyschlage to 2024.8.0 (#123714) --- homeassistant/components/schlage/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/schlage/manifest.json b/homeassistant/components/schlage/manifest.json index c6dfc443bb8..5619cf7b312 100644 --- a/homeassistant/components/schlage/manifest.json +++ b/homeassistant/components/schlage/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/schlage", "iot_class": "cloud_polling", - "requirements": ["pyschlage==2024.6.0"] + "requirements": ["pyschlage==2024.8.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 19d976481e2..5e97c9a7c96 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2160,7 +2160,7 @@ pysabnzbd==1.1.1 pysaj==0.0.16 # homeassistant.components.schlage -pyschlage==2024.6.0 +pyschlage==2024.8.0 # homeassistant.components.sensibo pysensibo==1.0.36 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0aeb0a61597..6c745b82097 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1723,7 +1723,7 @@ pyrympro==0.0.8 pysabnzbd==1.1.1 # homeassistant.components.schlage -pyschlage==2024.6.0 +pyschlage==2024.8.0 # homeassistant.components.sensibo pysensibo==1.0.36 From e3cb9c084420f0fe196c28fbead8b070dfb7eb95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Mon, 12 Aug 2024 18:59:31 +0200 Subject: [PATCH 040/271] Update AEMET-OpenData to v0.5.4 (#123716) --- homeassistant/components/aemet/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aemet/manifest.json b/homeassistant/components/aemet/manifest.json index d2e5c5fdc5a..3696e16b437 100644 --- a/homeassistant/components/aemet/manifest.json +++ b/homeassistant/components/aemet/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/aemet", "iot_class": "cloud_polling", "loggers": ["aemet_opendata"], - "requirements": ["AEMET-OpenData==0.5.3"] + "requirements": ["AEMET-OpenData==0.5.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5e97c9a7c96..68a3e5886b8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -4,7 +4,7 @@ -r requirements.txt # homeassistant.components.aemet -AEMET-OpenData==0.5.3 +AEMET-OpenData==0.5.4 # homeassistant.components.honeywell AIOSomecomfort==0.0.25 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6c745b82097..21591a97477 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -4,7 +4,7 @@ -r requirements_test.txt # homeassistant.components.aemet -AEMET-OpenData==0.5.3 +AEMET-OpenData==0.5.4 # homeassistant.components.honeywell AIOSomecomfort==0.0.25 From bc021dbbc6c3479e56861f7f3ea95b166a88e8d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Mon, 12 Aug 2024 18:57:34 +0200 Subject: [PATCH 041/271] Update aioairzone-cloud to v0.6.2 (#123719) --- homeassistant/components/airzone_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airzone_cloud/manifest.json b/homeassistant/components/airzone_cloud/manifest.json index 362973ae833..b691770e934 100644 --- a/homeassistant/components/airzone_cloud/manifest.json +++ b/homeassistant/components/airzone_cloud/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/airzone_cloud", "iot_class": "cloud_push", "loggers": ["aioairzone_cloud"], - "requirements": ["aioairzone-cloud==0.6.1"] + "requirements": ["aioairzone-cloud==0.6.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 68a3e5886b8..be4d377ae60 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -176,7 +176,7 @@ aio-georss-gdacs==0.9 aioairq==0.3.2 # homeassistant.components.airzone_cloud -aioairzone-cloud==0.6.1 +aioairzone-cloud==0.6.2 # homeassistant.components.airzone aioairzone==0.8.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 21591a97477..356732ebf3b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -164,7 +164,7 @@ aio-georss-gdacs==0.9 aioairq==0.3.2 # homeassistant.components.airzone_cloud -aioairzone-cloud==0.6.1 +aioairzone-cloud==0.6.2 # homeassistant.components.airzone aioairzone==0.8.1 From 17bb00727dde66f9b472c0f7270da688b44fde4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Mon, 12 Aug 2024 18:56:54 +0200 Subject: [PATCH 042/271] Update aioqsw to v0.4.1 (#123721) --- homeassistant/components/qnap_qsw/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/qnap_qsw/manifest.json b/homeassistant/components/qnap_qsw/manifest.json index b8c62133193..d34848346b7 100644 --- a/homeassistant/components/qnap_qsw/manifest.json +++ b/homeassistant/components/qnap_qsw/manifest.json @@ -11,5 +11,5 @@ "documentation": "https://www.home-assistant.io/integrations/qnap_qsw", "iot_class": "local_polling", "loggers": ["aioqsw"], - "requirements": ["aioqsw==0.4.0"] + "requirements": ["aioqsw==0.4.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index be4d377ae60..5c1067cc1f9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -335,7 +335,7 @@ aiopvpc==4.2.2 aiopyarr==23.4.0 # homeassistant.components.qnap_qsw -aioqsw==0.4.0 +aioqsw==0.4.1 # homeassistant.components.rainforest_raven aioraven==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 356732ebf3b..d4e728bf669 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -317,7 +317,7 @@ aiopvpc==4.2.2 aiopyarr==23.4.0 # homeassistant.components.qnap_qsw -aioqsw==0.4.0 +aioqsw==0.4.1 # homeassistant.components.rainforest_raven aioraven==0.7.0 From 10846dc97b822dafc86a47f55a4eee6962047dcd Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 12 Aug 2024 16:38:59 -0400 Subject: [PATCH 043/271] Bump ZHA lib to 0.0.31 (#123743) --- homeassistant/components/zha/entity.py | 2 +- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index 6db0ffad964..348e545f1c4 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -62,7 +62,7 @@ class ZHAEntity(LogMixin, RestoreEntity, Entity): @property def available(self) -> bool: """Return entity availability.""" - return self.entity_data.device_proxy.device.available + return self.entity_data.entity.available @property def device_info(self) -> DeviceInfo: diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index bb1480b43e1..a5e57fcb1ec 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -21,7 +21,7 @@ "zha", "universal_silabs_flasher" ], - "requirements": ["universal-silabs-flasher==0.0.22", "zha==0.0.30"], + "requirements": ["universal-silabs-flasher==0.0.22", "zha==0.0.31"], "usb": [ { "vid": "10C4", diff --git a/requirements_all.txt b/requirements_all.txt index 5c1067cc1f9..a5d0f59ab3f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2989,7 +2989,7 @@ zeroconf==0.132.2 zeversolar==0.3.1 # homeassistant.components.zha -zha==0.0.30 +zha==0.0.31 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d4e728bf669..f3a7fb70fc2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2363,7 +2363,7 @@ zeroconf==0.132.2 zeversolar==0.3.1 # homeassistant.components.zha -zha==0.0.30 +zha==0.0.31 # homeassistant.components.zwave_js zwave-js-server-python==0.57.0 From 17f59a5665f4ce68780f08365c9ce7e6aa21a1c3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 12 Aug 2024 23:23:34 +0200 Subject: [PATCH 044/271] Update wled to 0.20.2 (#123746) --- homeassistant/components/wled/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wled/manifest.json b/homeassistant/components/wled/manifest.json index efeb414438d..71939127356 100644 --- a/homeassistant/components/wled/manifest.json +++ b/homeassistant/components/wled/manifest.json @@ -7,6 +7,6 @@ "integration_type": "device", "iot_class": "local_push", "quality_scale": "platinum", - "requirements": ["wled==0.20.1"], + "requirements": ["wled==0.20.2"], "zeroconf": ["_wled._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index a5d0f59ab3f..4005ce94614 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2918,7 +2918,7 @@ wiffi==1.1.2 wirelesstagpy==0.8.1 # homeassistant.components.wled -wled==0.20.1 +wled==0.20.2 # homeassistant.components.wolflink wolf-comm==0.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f3a7fb70fc2..0c5d856b26a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2301,7 +2301,7 @@ whois==0.9.27 wiffi==1.1.2 # homeassistant.components.wled -wled==0.20.1 +wled==0.20.2 # homeassistant.components.wolflink wolf-comm==0.0.9 From 396ef7a6420497b50d5b4872cc71cb8f39ac09ff Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Tue, 13 Aug 2024 08:50:02 +0200 Subject: [PATCH 045/271] Fix error message in html5 (#123749) --- homeassistant/components/html5/notify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/html5/notify.py b/homeassistant/components/html5/notify.py index 798589d2807..8082ca37aa3 100644 --- a/homeassistant/components/html5/notify.py +++ b/homeassistant/components/html5/notify.py @@ -533,7 +533,7 @@ class HTML5NotificationService(BaseNotificationService): elif response.status_code > 399: _LOGGER.error( "There was an issue sending the notification %s: %s", - response.status, + response.status_code, response.text, ) From 5b6c6141c5399f637013e742fa76874277c4a5b9 Mon Sep 17 00:00:00 2001 From: Ian Date: Tue, 13 Aug 2024 02:51:41 -0400 Subject: [PATCH 046/271] Bump py-nextbusnext to 2.0.4 (#123750) --- homeassistant/components/nextbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nextbus/manifest.json b/homeassistant/components/nextbus/manifest.json index 27fec1bfba9..d22ba66d860 100644 --- a/homeassistant/components/nextbus/manifest.json +++ b/homeassistant/components/nextbus/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/nextbus", "iot_class": "cloud_polling", "loggers": ["py_nextbus"], - "requirements": ["py-nextbusnext==2.0.3"] + "requirements": ["py-nextbusnext==2.0.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4005ce94614..9e3e9a770a3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1653,7 +1653,7 @@ py-madvr2==1.6.29 py-melissa-climate==2.1.4 # homeassistant.components.nextbus -py-nextbusnext==2.0.3 +py-nextbusnext==2.0.4 # homeassistant.components.nightscout py-nightscout==1.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0c5d856b26a..ddb616a51a1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1345,7 +1345,7 @@ py-madvr2==1.6.29 py-melissa-climate==2.1.4 # homeassistant.components.nextbus -py-nextbusnext==2.0.3 +py-nextbusnext==2.0.4 # homeassistant.components.nightscout py-nightscout==1.2.2 From 63f28ae2fe50fcc73ce0d8c5932095833615a17f Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 12 Aug 2024 23:47:47 -0700 Subject: [PATCH 047/271] Bump python-nest-sdm to 4.0.6 (#123762) --- homeassistant/components/nest/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index d3ba571e65a..fbe5ddb6534 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -20,5 +20,5 @@ "iot_class": "cloud_push", "loggers": ["google_nest_sdm"], "quality_scale": "platinum", - "requirements": ["google-nest-sdm==4.0.5"] + "requirements": ["google-nest-sdm==4.0.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index 9e3e9a770a3..08294d73398 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -986,7 +986,7 @@ google-cloud-texttospeech==2.16.3 google-generativeai==0.6.0 # homeassistant.components.nest -google-nest-sdm==4.0.5 +google-nest-sdm==4.0.6 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ddb616a51a1..7f622fd3dbf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -833,7 +833,7 @@ google-cloud-pubsub==2.13.11 google-generativeai==0.6.0 # homeassistant.components.nest -google-nest-sdm==4.0.5 +google-nest-sdm==4.0.6 # homeassistant.components.google_travel_time googlemaps==2.5.1 From f2e42eafc79ba7d1c4fb1032b3de40b5088bae29 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Tue, 13 Aug 2024 13:28:37 +0200 Subject: [PATCH 048/271] Update xknx to 3.1.0 and fix climate read only mode (#123776) --- homeassistant/components/knx/climate.py | 29 ++++++-- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/knx/test_climate.py | 84 ++++++++++++++++++++++ 5 files changed, 109 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index 9abc9023617..abce143c760 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -5,7 +5,11 @@ from __future__ import annotations from typing import Any from xknx import XKNX -from xknx.devices import Climate as XknxClimate, ClimateMode as XknxClimateMode +from xknx.devices import ( + Climate as XknxClimate, + ClimateMode as XknxClimateMode, + Device as XknxDevice, +) from xknx.dpt.dpt_20 import HVACControllerMode from homeassistant import config_entries @@ -241,12 +245,9 @@ class KNXClimate(KnxYamlEntity, ClimateEntity): if self._device.supports_on_off and not self._device.is_on: return HVACMode.OFF if self._device.mode is not None and self._device.mode.supports_controller_mode: - hvac_mode = CONTROLLER_MODES.get( + return CONTROLLER_MODES.get( self._device.mode.controller_mode, self.default_hvac_mode ) - if hvac_mode is not HVACMode.OFF: - self._last_hvac_mode = hvac_mode - return hvac_mode return self.default_hvac_mode @property @@ -261,11 +262,15 @@ class KNXClimate(KnxYamlEntity, ClimateEntity): if self._device.supports_on_off: if not ha_controller_modes: - ha_controller_modes.append(self.default_hvac_mode) + ha_controller_modes.append(self._last_hvac_mode) ha_controller_modes.append(HVACMode.OFF) hvac_modes = list(set(filter(None, ha_controller_modes))) - return hvac_modes if hvac_modes else [self.default_hvac_mode] + return ( + hvac_modes + if hvac_modes + else [self.hvac_mode] # mode read-only -> fall back to only current mode + ) @property def hvac_action(self) -> HVACAction | None: @@ -354,3 +359,13 @@ class KNXClimate(KnxYamlEntity, ClimateEntity): self._device.mode.unregister_device_updated_cb(self.after_update_callback) self._device.mode.xknx.devices.async_remove(self._device.mode) await super().async_will_remove_from_hass() + + def after_update_callback(self, _device: XknxDevice) -> None: + """Call after device was updated.""" + if self._device.mode is not None and self._device.mode.supports_controller_mode: + hvac_mode = CONTROLLER_MODES.get( + self._device.mode.controller_mode, self.default_hvac_mode + ) + if hvac_mode is not HVACMode.OFF: + self._last_hvac_mode = hvac_mode + super().after_update_callback(_device) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 6974ee300f5..9ecf687d6b9 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -11,7 +11,7 @@ "loggers": ["xknx", "xknxproject"], "quality_scale": "platinum", "requirements": [ - "xknx==3.0.0", + "xknx==3.1.0", "xknxproject==3.7.1", "knx-frontend==2024.8.9.225351" ], diff --git a/requirements_all.txt b/requirements_all.txt index 08294d73398..0f42af47476 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2933,7 +2933,7 @@ xbox-webapi==2.0.11 xiaomi-ble==0.30.2 # homeassistant.components.knx -xknx==3.0.0 +xknx==3.1.0 # homeassistant.components.knx xknxproject==3.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7f622fd3dbf..feee17b9452 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2316,7 +2316,7 @@ xbox-webapi==2.0.11 xiaomi-ble==0.30.2 # homeassistant.components.knx -xknx==3.0.0 +xknx==3.1.0 # homeassistant.components.knx xknxproject==3.7.1 diff --git a/tests/components/knx/test_climate.py b/tests/components/knx/test_climate.py index 77eeeef3559..9f198b48bd4 100644 --- a/tests/components/knx/test_climate.py +++ b/tests/components/knx/test_climate.py @@ -231,6 +231,90 @@ async def test_climate_hvac_mode( assert hass.states.get("climate.test").state == "cool" +async def test_climate_heat_cool_read_only( + hass: HomeAssistant, knx: KNXTestKit +) -> None: + """Test KNX climate hvac mode.""" + heat_cool_state_ga = "3/3/3" + await knx.setup_integration( + { + ClimateSchema.PLATFORM: { + CONF_NAME: "test", + ClimateSchema.CONF_TEMPERATURE_ADDRESS: "1/2/3", + ClimateSchema.CONF_TARGET_TEMPERATURE_ADDRESS: "1/2/4", + ClimateSchema.CONF_TARGET_TEMPERATURE_STATE_ADDRESS: "1/2/5", + ClimateSchema.CONF_HEAT_COOL_STATE_ADDRESS: heat_cool_state_ga, + } + } + ) + # read states state updater + # StateUpdater semaphore allows 2 concurrent requests + await knx.assert_read("1/2/3") + await knx.assert_read("1/2/5") + # StateUpdater initialize state + await knx.receive_response("1/2/3", RAW_FLOAT_20_0) + await knx.receive_response("1/2/5", RAW_FLOAT_20_0) + await knx.assert_read(heat_cool_state_ga) + await knx.receive_response(heat_cool_state_ga, True) # heat + + state = hass.states.get("climate.test") + assert state.state == "heat" + assert state.attributes["hvac_modes"] == ["heat"] + assert state.attributes["hvac_action"] == "heating" + + await knx.receive_write(heat_cool_state_ga, False) # cool + state = hass.states.get("climate.test") + assert state.state == "cool" + assert state.attributes["hvac_modes"] == ["cool"] + assert state.attributes["hvac_action"] == "cooling" + + +async def test_climate_heat_cool_read_only_on_off( + hass: HomeAssistant, knx: KNXTestKit +) -> None: + """Test KNX climate hvac mode.""" + on_off_ga = "2/2/2" + heat_cool_state_ga = "3/3/3" + await knx.setup_integration( + { + ClimateSchema.PLATFORM: { + CONF_NAME: "test", + ClimateSchema.CONF_TEMPERATURE_ADDRESS: "1/2/3", + ClimateSchema.CONF_TARGET_TEMPERATURE_ADDRESS: "1/2/4", + ClimateSchema.CONF_TARGET_TEMPERATURE_STATE_ADDRESS: "1/2/5", + ClimateSchema.CONF_ON_OFF_ADDRESS: on_off_ga, + ClimateSchema.CONF_HEAT_COOL_STATE_ADDRESS: heat_cool_state_ga, + } + } + ) + # read states state updater + # StateUpdater semaphore allows 2 concurrent requests + await knx.assert_read("1/2/3") + await knx.assert_read("1/2/5") + # StateUpdater initialize state + await knx.receive_response("1/2/3", RAW_FLOAT_20_0) + await knx.receive_response("1/2/5", RAW_FLOAT_20_0) + await knx.assert_read(heat_cool_state_ga) + await knx.receive_response(heat_cool_state_ga, True) # heat + + state = hass.states.get("climate.test") + assert state.state == "off" + assert set(state.attributes["hvac_modes"]) == {"off", "heat"} + assert state.attributes["hvac_action"] == "off" + + await knx.receive_write(heat_cool_state_ga, False) # cool + state = hass.states.get("climate.test") + assert state.state == "off" + assert set(state.attributes["hvac_modes"]) == {"off", "cool"} + assert state.attributes["hvac_action"] == "off" + + await knx.receive_write(on_off_ga, True) + state = hass.states.get("climate.test") + assert state.state == "cool" + assert set(state.attributes["hvac_modes"]) == {"off", "cool"} + assert state.attributes["hvac_action"] == "cooling" + + async def test_climate_preset_mode( hass: HomeAssistant, knx: KNXTestKit, entity_registry: er.EntityRegistry ) -> None: From ff4e5859cf008efb8081350c8527507c9f4b30b6 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Thu, 15 Aug 2024 10:52:55 +0200 Subject: [PATCH 049/271] Fix KNX UI Light color temperature DPT (#123778) --- homeassistant/components/knx/light.py | 4 +-- tests/components/knx/test_light.py | 44 +++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index a2ce8f8d2cb..0caa3f0a799 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -226,7 +226,7 @@ def _create_ui_light(xknx: XKNX, knx_config: ConfigType, name: str) -> XknxLight group_address_color_temp_state = None color_temperature_type = ColorTemperatureType.UINT_2_BYTE if ga_color_temp := knx_config.get(CONF_GA_COLOR_TEMP): - if ga_color_temp[CONF_DPT] == ColorTempModes.RELATIVE: + if ga_color_temp[CONF_DPT] == ColorTempModes.RELATIVE.value: group_address_tunable_white = ga_color_temp[CONF_GA_WRITE] group_address_tunable_white_state = [ ga_color_temp[CONF_GA_STATE], @@ -239,7 +239,7 @@ def _create_ui_light(xknx: XKNX, knx_config: ConfigType, name: str) -> XknxLight ga_color_temp[CONF_GA_STATE], *ga_color_temp[CONF_GA_PASSIVE], ] - if ga_color_temp[CONF_DPT] == ColorTempModes.ABSOLUTE_FLOAT: + if ga_color_temp[CONF_DPT] == ColorTempModes.ABSOLUTE_FLOAT.value: color_temperature_type = ColorTemperatureType.FLOAT_2_BYTE _color_dpt = get_dpt(CONF_GA_COLOR) diff --git a/tests/components/knx/test_light.py b/tests/components/knx/test_light.py index 04f849bb555..e2e4a673a0d 100644 --- a/tests/components/knx/test_light.py +++ b/tests/components/knx/test_light.py @@ -5,6 +5,7 @@ from __future__ import annotations from datetime import timedelta from freezegun.api import FrozenDateTimeFactory +import pytest from xknx.core import XknxConnectionState from xknx.devices.light import Light as XknxLight @@ -1174,3 +1175,46 @@ async def test_light_ui_create( await knx.receive_response("2/2/2", True) state = hass.states.get("light.test") assert state.state is STATE_ON + + +@pytest.mark.parametrize( + ("color_temp_mode", "raw_ct"), + [ + ("7.600", (0x10, 0x68)), + ("9", (0x46, 0x69)), + ("5.001", (0x74,)), + ], +) +async def test_light_ui_color_temp( + hass: HomeAssistant, + knx: KNXTestKit, + create_ui_entity: KnxEntityGenerator, + color_temp_mode: str, + raw_ct: tuple[int, ...], +) -> None: + """Test creating a switch.""" + await knx.setup_integration({}) + await create_ui_entity( + platform=Platform.LIGHT, + entity_data={"name": "test"}, + knx_data={ + "ga_switch": {"write": "1/1/1", "state": "2/2/2"}, + "ga_color_temp": { + "write": "3/3/3", + "dpt": color_temp_mode, + }, + "_light_color_mode_schema": "default", + "sync_state": True, + }, + ) + await knx.assert_read("2/2/2", True) + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": "light.test", ATTR_COLOR_TEMP_KELVIN: 4200}, + blocking=True, + ) + await knx.assert_write("3/3/3", raw_ct) + state = hass.states.get("light.test") + assert state.state is STATE_ON + assert state.attributes[ATTR_COLOR_TEMP_KELVIN] == pytest.approx(4200, abs=1) From 81fabb1bfad0013de55b3b61f14ae423843b0791 Mon Sep 17 00:00:00 2001 From: Louis Christ Date: Tue, 13 Aug 2024 12:55:01 +0200 Subject: [PATCH 050/271] Fix status update loop in bluesound integration (#123790) * Fix retry loop for status update * Use 'available' instead of _is_online * Fix tests --- .../components/bluesound/media_player.py | 37 ++++++++++--------- tests/components/bluesound/conftest.py | 35 ++++++++++++++++-- .../components/bluesound/test_config_flow.py | 4 +- 3 files changed, 55 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index c1b662fcddc..92f47977ee5 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -244,7 +244,6 @@ class BluesoundPlayer(MediaPlayerEntity): self._status: Status | None = None self._inputs: list[Input] = [] self._presets: list[Preset] = [] - self._is_online = False self._muted = False self._master: BluesoundPlayer | None = None self._is_master = False @@ -312,20 +311,24 @@ class BluesoundPlayer(MediaPlayerEntity): async def _start_poll_command(self): """Loop which polls the status of the player.""" - try: - while True: + while True: + try: await self.async_update_status() - - except (TimeoutError, ClientError): - _LOGGER.error("Node %s:%s is offline, retrying later", self.host, self.port) - await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT) - self.start_polling() - - except CancelledError: - _LOGGER.debug("Stopping the polling of node %s:%s", self.host, self.port) - except Exception: - _LOGGER.exception("Unexpected error in %s:%s", self.host, self.port) - raise + except (TimeoutError, ClientError): + _LOGGER.error( + "Node %s:%s is offline, retrying later", self.host, self.port + ) + await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT) + except CancelledError: + _LOGGER.debug( + "Stopping the polling of node %s:%s", self.host, self.port + ) + return + except Exception: + _LOGGER.exception( + "Unexpected error in %s:%s, retrying later", self.host, self.port + ) + await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT) async def async_added_to_hass(self) -> None: """Start the polling task.""" @@ -348,7 +351,7 @@ class BluesoundPlayer(MediaPlayerEntity): async def async_update(self) -> None: """Update internal status of the entity.""" - if not self._is_online: + if not self.available: return with suppress(TimeoutError): @@ -365,7 +368,7 @@ class BluesoundPlayer(MediaPlayerEntity): try: status = await self._player.status(etag=etag, poll_timeout=120, timeout=125) - self._is_online = True + self._attr_available = True self._last_status_update = dt_util.utcnow() self._status = status @@ -394,7 +397,7 @@ class BluesoundPlayer(MediaPlayerEntity): self.async_write_ha_state() except (TimeoutError, ClientError): - self._is_online = False + self._attr_available = False self._last_status_update = None self._status = None self.async_write_ha_state() diff --git a/tests/components/bluesound/conftest.py b/tests/components/bluesound/conftest.py index 02c73bcd62f..096db055b45 100644 --- a/tests/components/bluesound/conftest.py +++ b/tests/components/bluesound/conftest.py @@ -3,7 +3,7 @@ from collections.abc import Generator from unittest.mock import AsyncMock, patch -from pyblu import SyncStatus +from pyblu import Status, SyncStatus import pytest from homeassistant.components.bluesound.const import DOMAIN @@ -39,6 +39,35 @@ def sync_status() -> SyncStatus: ) +@pytest.fixture +def status() -> Status: + """Return a status object.""" + return Status( + etag="etag", + input_id=None, + service=None, + state="playing", + shuffle=False, + album=None, + artist=None, + name=None, + image=None, + volume=10, + volume_db=22.3, + mute=False, + mute_volume=None, + mute_volume_db=None, + seconds=2, + total_seconds=123.1, + can_seek=False, + sleep=0, + group_name=None, + group_volume=None, + indexing=False, + stream_url=None, + ) + + @pytest.fixture def mock_setup_entry() -> Generator[AsyncMock, None, None]: """Override async_setup_entry.""" @@ -65,7 +94,7 @@ def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry: @pytest.fixture -def mock_player() -> Generator[AsyncMock]: +def mock_player(status: Status) -> Generator[AsyncMock]: """Mock the player.""" with ( patch( @@ -78,7 +107,7 @@ def mock_player() -> Generator[AsyncMock]: ): player = mock_player.return_value player.__aenter__.return_value = player - player.status.return_value = None + player.status.return_value = status player.sync_status.return_value = SyncStatus( etag="etag", id="1.1.1.1:11000", diff --git a/tests/components/bluesound/test_config_flow.py b/tests/components/bluesound/test_config_flow.py index 32f36fcea58..8fecba7017d 100644 --- a/tests/components/bluesound/test_config_flow.py +++ b/tests/components/bluesound/test_config_flow.py @@ -41,7 +41,7 @@ async def test_user_flow_success( async def test_user_flow_cannot_connect( - hass: HomeAssistant, mock_player: AsyncMock + hass: HomeAssistant, mock_player: AsyncMock, mock_setup_entry: AsyncMock ) -> None: """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( @@ -76,6 +76,8 @@ async def test_user_flow_cannot_connect( CONF_PORT: 11000, } + mock_setup_entry.assert_called_once() + async def test_user_flow_aleady_configured( hass: HomeAssistant, From 6234deeee13fc4a66e773dedbe6256defdd91939 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Tue, 13 Aug 2024 22:21:48 +0200 Subject: [PATCH 051/271] Bump py-synologydsm-api to 2.4.5 (#123815) bump py-synologydsm-api to 2.4.5 --- homeassistant/components/synology_dsm/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/synology_dsm/manifest.json b/homeassistant/components/synology_dsm/manifest.json index b1133fd61ad..9d977609d14 100644 --- a/homeassistant/components/synology_dsm/manifest.json +++ b/homeassistant/components/synology_dsm/manifest.json @@ -7,7 +7,7 @@ "documentation": "https://www.home-assistant.io/integrations/synology_dsm", "iot_class": "local_polling", "loggers": ["synology_dsm"], - "requirements": ["py-synologydsm-api==2.4.4"], + "requirements": ["py-synologydsm-api==2.4.5"], "ssdp": [ { "manufacturer": "Synology", diff --git a/requirements_all.txt b/requirements_all.txt index 0f42af47476..654f75903ea 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1665,7 +1665,7 @@ py-schluter==0.1.7 py-sucks==0.9.10 # homeassistant.components.synology_dsm -py-synologydsm-api==2.4.4 +py-synologydsm-api==2.4.5 # homeassistant.components.zabbix py-zabbix==1.1.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index feee17b9452..d7bf5e27bf0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1354,7 +1354,7 @@ py-nightscout==1.2.2 py-sucks==0.9.10 # homeassistant.components.synology_dsm -py-synologydsm-api==2.4.4 +py-synologydsm-api==2.4.5 # homeassistant.components.hdmi_cec pyCEC==0.5.2 From 8539591307498fa999389bbddaf077b8c702e1a1 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 14 Aug 2024 15:55:59 +0200 Subject: [PATCH 052/271] Fix blocking I/O of SSLContext.load_default_certs in Ecovacs (#123856) --- .../components/ecovacs/config_flow.py | 14 +++++--- .../components/ecovacs/controller.py | 36 +++++++++++-------- 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/ecovacs/config_flow.py b/homeassistant/components/ecovacs/config_flow.py index a254731a946..fa078bb02ef 100644 --- a/homeassistant/components/ecovacs/config_flow.py +++ b/homeassistant/components/ecovacs/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations +from functools import partial import logging import ssl from typing import Any, cast @@ -105,11 +106,14 @@ async def _validate_input( if not user_input.get(CONF_VERIFY_MQTT_CERTIFICATE, True) and mqtt_url: ssl_context = get_default_no_verify_context() - mqtt_config = create_mqtt_config( - device_id=device_id, - country=country, - override_mqtt_url=mqtt_url, - ssl_context=ssl_context, + mqtt_config = await hass.async_add_executor_job( + partial( + create_mqtt_config, + device_id=device_id, + country=country, + override_mqtt_url=mqtt_url, + ssl_context=ssl_context, + ) ) client = MqttClient(mqtt_config, authenticator) diff --git a/homeassistant/components/ecovacs/controller.py b/homeassistant/components/ecovacs/controller.py index c22fb240536..ec67845cf9f 100644 --- a/homeassistant/components/ecovacs/controller.py +++ b/homeassistant/components/ecovacs/controller.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Mapping +from functools import partial import logging import ssl from typing import Any @@ -64,32 +65,28 @@ class EcovacsController: if not config.get(CONF_VERIFY_MQTT_CERTIFICATE, True) and mqtt_url: ssl_context = get_default_no_verify_context() - self._mqtt = MqttClient( - create_mqtt_config( - device_id=self._device_id, - country=country, - override_mqtt_url=mqtt_url, - ssl_context=ssl_context, - ), - self._authenticator, + self._mqtt_config_fn = partial( + create_mqtt_config, + device_id=self._device_id, + country=country, + override_mqtt_url=mqtt_url, + ssl_context=ssl_context, ) + self._mqtt_client: MqttClient | None = None self._added_legacy_entities: set[str] = set() async def initialize(self) -> None: """Init controller.""" - mqtt_config_verfied = False try: devices = await self._api_client.get_devices() credentials = await self._authenticator.authenticate() for device_config in devices: if isinstance(device_config, DeviceInfo): # MQTT device - if not mqtt_config_verfied: - await self._mqtt.verify_config() - mqtt_config_verfied = True device = Device(device_config, self._authenticator) - await device.initialize(self._mqtt) + mqtt = await self._get_mqtt_client() + await device.initialize(mqtt) self._devices.append(device) else: # Legacy device @@ -116,7 +113,8 @@ class EcovacsController: await device.teardown() for legacy_device in self._legacy_devices: await self._hass.async_add_executor_job(legacy_device.disconnect) - await self._mqtt.disconnect() + if self._mqtt_client is not None: + await self._mqtt_client.disconnect() await self._authenticator.teardown() def add_legacy_entity(self, device: VacBot, component: str) -> None: @@ -127,6 +125,16 @@ class EcovacsController: """Check if legacy entity is added.""" return f"{device.vacuum['did']}_{component}" in self._added_legacy_entities + async def _get_mqtt_client(self) -> MqttClient: + """Return validated MQTT client.""" + if self._mqtt_client is None: + config = await self._hass.async_add_executor_job(self._mqtt_config_fn) + mqtt = MqttClient(config, self._authenticator) + await mqtt.verify_config() + self._mqtt_client = mqtt + + return self._mqtt_client + @property def devices(self) -> list[Device]: """Return devices.""" From 80abf90c87d0266556b4b7d4f8dee7a97d226abd Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 14 Aug 2024 12:09:46 +0200 Subject: [PATCH 053/271] Fix translation for integration not found repair issue (#123868) * correct setp id in strings * add issue_ignored string --- homeassistant/components/homeassistant/strings.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homeassistant/strings.json b/homeassistant/components/homeassistant/strings.json index e3e1464077a..69a3e26ad79 100644 --- a/homeassistant/components/homeassistant/strings.json +++ b/homeassistant/components/homeassistant/strings.json @@ -60,8 +60,11 @@ "integration_not_found": { "title": "Integration {domain} not found", "fix_flow": { + "abort": { + "issue_ignored": "Not existing integration {domain} ignored." + }, "step": { - "remove_entries": { + "init": { "title": "[%key:component::homeassistant::issues::integration_not_found::title%]", "description": "The integration `{domain}` could not be found. This happens when a (custom) integration was removed from Home Assistant, but there are still configurations for this `integration`. Please use the buttons below to either remove the previous configurations for `{domain}` or ignore this.", "menu_options": { From 55a911120cb41780834d8be463bb5a773783d5b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Wed, 14 Aug 2024 13:06:52 +0200 Subject: [PATCH 054/271] Handle timeouts on Airzone DHCP config flow (#123869) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit airzone: config_flow: dhcp: catch timeout exception Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/airzone/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/airzone/config_flow.py b/homeassistant/components/airzone/config_flow.py index 24ee37bbcb4..406fd72a6db 100644 --- a/homeassistant/components/airzone/config_flow.py +++ b/homeassistant/components/airzone/config_flow.py @@ -114,7 +114,7 @@ class AirZoneConfigFlow(ConfigFlow, domain=DOMAIN): ) try: await airzone.get_version() - except AirzoneError as err: + except (AirzoneError, TimeoutError) as err: raise AbortFlow("cannot_connect") from err return await self.async_step_discovered_connection() From 7d00ccbbbc2e33d0e15ff22a5284977b0d905bf5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 14 Aug 2024 10:02:44 -0500 Subject: [PATCH 055/271] Bump pylutron_caseta to 0.21.1 (#123924) --- homeassistant/components/lutron_caseta/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/licenses.py | 1 - tests/components/lutron_caseta/test_device_trigger.py | 5 +++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/lutron_caseta/manifest.json b/homeassistant/components/lutron_caseta/manifest.json index 48445f645aa..3c6348ed4da 100644 --- a/homeassistant/components/lutron_caseta/manifest.json +++ b/homeassistant/components/lutron_caseta/manifest.json @@ -9,7 +9,7 @@ }, "iot_class": "local_push", "loggers": ["pylutron_caseta"], - "requirements": ["pylutron-caseta==0.20.0"], + "requirements": ["pylutron-caseta==0.21.1"], "zeroconf": [ { "type": "_lutron._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 654f75903ea..962fe1035eb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1993,7 +1993,7 @@ pylitejet==0.6.2 pylitterbot==2023.5.0 # homeassistant.components.lutron_caseta -pylutron-caseta==0.20.0 +pylutron-caseta==0.21.1 # homeassistant.components.lutron pylutron==0.2.15 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d7bf5e27bf0..83d7553d73e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1592,7 +1592,7 @@ pylitejet==0.6.2 pylitterbot==2023.5.0 # homeassistant.components.lutron_caseta -pylutron-caseta==0.20.0 +pylutron-caseta==0.21.1 # homeassistant.components.lutron pylutron==0.2.15 diff --git a/script/licenses.py b/script/licenses.py index dc89cdad9a9..9c584e7f4fc 100644 --- a/script/licenses.py +++ b/script/licenses.py @@ -159,7 +159,6 @@ EXCEPTIONS = { "pyTibber", # https://github.com/Danielhiversen/pyTibber/pull/294 "pybbox", # https://github.com/HydrelioxGitHub/pybbox/pull/5 "pyeconet", # https://github.com/w1ll1am23/pyeconet/pull/41 - "pylutron-caseta", # https://github.com/gurumitts/pylutron-caseta/pull/168 "pysabnzbd", # https://github.com/jeradM/pysabnzbd/pull/6 "pyvera", # https://github.com/maximvelichko/pyvera/pull/164 "pyxeoma", # https://github.com/jeradM/pyxeoma/pull/11 diff --git a/tests/components/lutron_caseta/test_device_trigger.py b/tests/components/lutron_caseta/test_device_trigger.py index 405c504dee1..9353b897602 100644 --- a/tests/components/lutron_caseta/test_device_trigger.py +++ b/tests/components/lutron_caseta/test_device_trigger.py @@ -487,8 +487,9 @@ async def test_if_fires_on_button_event_late_setup( }, ) - await hass.config_entries.async_setup(config_entry_id) - await hass.async_block_till_done() + with patch("homeassistant.components.lutron_caseta.Smartbridge.create_tls"): + await hass.config_entries.async_setup(config_entry_id) + await hass.async_block_till_done() message = { ATTR_SERIAL: device.get("serial"), From 59aecda8cfa6d7087f0fcf515efba142e6d65752 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 14 Aug 2024 20:39:15 +0200 Subject: [PATCH 056/271] Fix PI-Hole update entity when no update available (#123930) show installed version when no update available --- homeassistant/components/pi_hole/update.py | 8 +++- tests/components/pi_hole/__init__.py | 23 +++++++++-- tests/components/pi_hole/test_config_flow.py | 2 +- tests/components/pi_hole/test_update.py | 43 +++++++++++++++++++- 4 files changed, 70 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/pi_hole/update.py b/homeassistant/components/pi_hole/update.py index db78d3ab0a5..c1a435f628c 100644 --- a/homeassistant/components/pi_hole/update.py +++ b/homeassistant/components/pi_hole/update.py @@ -22,6 +22,7 @@ class PiHoleUpdateEntityDescription(UpdateEntityDescription): installed_version: Callable[[dict], str | None] = lambda api: None latest_version: Callable[[dict], str | None] = lambda api: None + has_update: Callable[[dict], bool | None] = lambda api: None release_base_url: str | None = None title: str | None = None @@ -34,6 +35,7 @@ UPDATE_ENTITY_TYPES: tuple[PiHoleUpdateEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, installed_version=lambda versions: versions.get("core_current"), latest_version=lambda versions: versions.get("core_latest"), + has_update=lambda versions: versions.get("core_update"), release_base_url="https://github.com/pi-hole/pi-hole/releases/tag", ), PiHoleUpdateEntityDescription( @@ -43,6 +45,7 @@ UPDATE_ENTITY_TYPES: tuple[PiHoleUpdateEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, installed_version=lambda versions: versions.get("web_current"), latest_version=lambda versions: versions.get("web_latest"), + has_update=lambda versions: versions.get("web_update"), release_base_url="https://github.com/pi-hole/AdminLTE/releases/tag", ), PiHoleUpdateEntityDescription( @@ -52,6 +55,7 @@ UPDATE_ENTITY_TYPES: tuple[PiHoleUpdateEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, installed_version=lambda versions: versions.get("FTL_current"), latest_version=lambda versions: versions.get("FTL_latest"), + has_update=lambda versions: versions.get("FTL_update"), release_base_url="https://github.com/pi-hole/FTL/releases/tag", ), ) @@ -110,7 +114,9 @@ class PiHoleUpdateEntity(PiHoleEntity, UpdateEntity): def latest_version(self) -> str | None: """Latest version available for install.""" if isinstance(self.api.versions, dict): - return self.entity_description.latest_version(self.api.versions) + if self.entity_description.has_update(self.api.versions): + return self.entity_description.latest_version(self.api.versions) + return self.installed_version return None @property diff --git a/tests/components/pi_hole/__init__.py b/tests/components/pi_hole/__init__.py index 38231778624..993f6a2571c 100644 --- a/tests/components/pi_hole/__init__.py +++ b/tests/components/pi_hole/__init__.py @@ -33,7 +33,7 @@ ZERO_DATA = { "unique_domains": 0, } -SAMPLE_VERSIONS = { +SAMPLE_VERSIONS_WITH_UPDATES = { "core_current": "v5.5", "core_latest": "v5.6", "core_update": True, @@ -45,6 +45,18 @@ SAMPLE_VERSIONS = { "FTL_update": True, } +SAMPLE_VERSIONS_NO_UPDATES = { + "core_current": "v5.5", + "core_latest": "v5.5", + "core_update": False, + "web_current": "v5.7", + "web_latest": "v5.7", + "web_update": False, + "FTL_current": "v5.10", + "FTL_latest": "v5.10", + "FTL_update": False, +} + HOST = "1.2.3.4" PORT = 80 LOCATION = "location" @@ -103,7 +115,9 @@ CONFIG_ENTRY_WITHOUT_API_KEY = { SWITCH_ENTITY_ID = "switch.pi_hole" -def _create_mocked_hole(raise_exception=False, has_versions=True, has_data=True): +def _create_mocked_hole( + raise_exception=False, has_versions=True, has_update=True, has_data=True +): mocked_hole = MagicMock() type(mocked_hole).get_data = AsyncMock( side_effect=HoleError("") if raise_exception else None @@ -118,7 +132,10 @@ def _create_mocked_hole(raise_exception=False, has_versions=True, has_data=True) else: mocked_hole.data = [] if has_versions: - mocked_hole.versions = SAMPLE_VERSIONS + if has_update: + mocked_hole.versions = SAMPLE_VERSIONS_WITH_UPDATES + else: + mocked_hole.versions = SAMPLE_VERSIONS_NO_UPDATES else: mocked_hole.versions = None return mocked_hole diff --git a/tests/components/pi_hole/test_config_flow.py b/tests/components/pi_hole/test_config_flow.py index 326b01b9a7a..d13712d6f76 100644 --- a/tests/components/pi_hole/test_config_flow.py +++ b/tests/components/pi_hole/test_config_flow.py @@ -96,7 +96,7 @@ async def test_flow_user_without_api_key(hass: HomeAssistant) -> None: async def test_flow_user_invalid(hass: HomeAssistant) -> None: """Test user initialized flow with invalid server.""" - mocked_hole = _create_mocked_hole(True) + mocked_hole = _create_mocked_hole(raise_exception=True) with _patch_config_flow_hole(mocked_hole): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONFIG_FLOW_USER diff --git a/tests/components/pi_hole/test_update.py b/tests/components/pi_hole/test_update.py index 091b553c475..705e9f9c08d 100644 --- a/tests/components/pi_hole/test_update.py +++ b/tests/components/pi_hole/test_update.py @@ -1,7 +1,7 @@ """Test pi_hole component.""" from homeassistant.components import pi_hole -from homeassistant.const import STATE_ON, STATE_UNKNOWN +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN from homeassistant.core import HomeAssistant from . import CONFIG_DATA_DEFAULTS, _create_mocked_hole, _patch_init_hole @@ -80,3 +80,44 @@ async def test_update_no_versions(hass: HomeAssistant) -> None: assert state.attributes["installed_version"] is None assert state.attributes["latest_version"] is None assert state.attributes["release_url"] is None + + +async def test_update_no_updates(hass: HomeAssistant) -> None: + """Tests update entity when no latest data available.""" + mocked_hole = _create_mocked_hole(has_versions=True, has_update=False) + entry = MockConfigEntry(domain=pi_hole.DOMAIN, data=CONFIG_DATA_DEFAULTS) + entry.add_to_hass(hass) + with _patch_init_hole(mocked_hole): + assert await hass.config_entries.async_setup(entry.entry_id) + + await hass.async_block_till_done() + + state = hass.states.get("update.pi_hole_core_update_available") + assert state.name == "Pi-Hole Core update available" + assert state.state == STATE_OFF + assert state.attributes["installed_version"] == "v5.5" + assert state.attributes["latest_version"] == "v5.5" + assert ( + state.attributes["release_url"] + == "https://github.com/pi-hole/pi-hole/releases/tag/v5.5" + ) + + state = hass.states.get("update.pi_hole_ftl_update_available") + assert state.name == "Pi-Hole FTL update available" + assert state.state == STATE_OFF + assert state.attributes["installed_version"] == "v5.10" + assert state.attributes["latest_version"] == "v5.10" + assert ( + state.attributes["release_url"] + == "https://github.com/pi-hole/FTL/releases/tag/v5.10" + ) + + state = hass.states.get("update.pi_hole_web_update_available") + assert state.name == "Pi-Hole Web update available" + assert state.state == STATE_OFF + assert state.attributes["installed_version"] == "v5.7" + assert state.attributes["latest_version"] == "v5.7" + assert ( + state.attributes["release_url"] + == "https://github.com/pi-hole/AdminLTE/releases/tag/v5.7" + ) From e9915463a9d44497f01609e4b923726f3cd8a49e Mon Sep 17 00:00:00 2001 From: IceBotYT <34712694+IceBotYT@users.noreply.github.com> Date: Wed, 14 Aug 2024 11:39:23 -0400 Subject: [PATCH 057/271] Bump LaCrosse View to 1.0.2, fixes blocking call (#123935) --- homeassistant/components/lacrosse_view/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lacrosse_view/manifest.json b/homeassistant/components/lacrosse_view/manifest.json index 1236f63ddad..1cf8794237d 100644 --- a/homeassistant/components/lacrosse_view/manifest.json +++ b/homeassistant/components/lacrosse_view/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/lacrosse_view", "iot_class": "cloud_polling", "loggers": ["lacrosse_view"], - "requirements": ["lacrosse-view==1.0.1"] + "requirements": ["lacrosse-view==1.0.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 962fe1035eb..1a9da3cbd70 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1228,7 +1228,7 @@ konnected==1.2.0 krakenex==2.1.0 # homeassistant.components.lacrosse_view -lacrosse-view==1.0.1 +lacrosse-view==1.0.2 # homeassistant.components.eufy lakeside==0.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 83d7553d73e..576c1b2393c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1024,7 +1024,7 @@ konnected==1.2.0 krakenex==2.1.0 # homeassistant.components.lacrosse_view -lacrosse-view==1.0.1 +lacrosse-view==1.0.2 # homeassistant.components.laundrify laundrify-aio==1.2.2 From 796ad47dd0ba7e188852a68cf0727da9bdabe95e Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Wed, 14 Aug 2024 21:36:11 +0200 Subject: [PATCH 058/271] Bump pypck to 0.7.20 (#123948) --- homeassistant/components/lcn/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lcn/manifest.json b/homeassistant/components/lcn/manifest.json index 6153ecf4540..44a4d683c81 100644 --- a/homeassistant/components/lcn/manifest.json +++ b/homeassistant/components/lcn/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/lcn", "iot_class": "local_push", "loggers": ["pypck"], - "requirements": ["pypck==0.7.17"] + "requirements": ["pypck==0.7.20"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1a9da3cbd70..aea1dd79968 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2100,7 +2100,7 @@ pyownet==0.10.0.post1 pypca==0.0.7 # homeassistant.components.lcn -pypck==0.7.17 +pypck==0.7.20 # homeassistant.components.pjlink pypjlink2==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 576c1b2393c..9ad157927b6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1678,7 +1678,7 @@ pyoverkiz==1.13.14 pyownet==0.10.0.post1 # homeassistant.components.lcn -pypck==0.7.17 +pypck==0.7.20 # homeassistant.components.pjlink pypjlink2==1.2.1 From bfd302109e8d37723831c1eda190edb7124bcd82 Mon Sep 17 00:00:00 2001 From: Glenn Waters Date: Thu, 15 Aug 2024 10:14:01 -0400 Subject: [PATCH 059/271] Environment Canada weather format fix (#123960) * Add missing isoformat. * Move fixture loading to common conftest.py * Add deepcopy. --- .../components/environment_canada/weather.py | 8 ++++-- .../components/environment_canada/conftest.py | 27 +++++++++++++++++++ .../snapshots/test_weather.ambr | 22 +++++++-------- .../environment_canada/test_diagnostics.py | 2 ++ .../environment_canada/test_weather.py | 24 +++++------------ 5 files changed, 53 insertions(+), 30 deletions(-) create mode 100644 tests/components/environment_canada/conftest.py diff --git a/homeassistant/components/environment_canada/weather.py b/homeassistant/components/environment_canada/weather.py index 2d54a313dde..1871062c2e9 100644 --- a/homeassistant/components/environment_canada/weather.py +++ b/homeassistant/components/environment_canada/weather.py @@ -2,6 +2,8 @@ from __future__ import annotations +from typing import Any + from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_CLOUDY, @@ -190,10 +192,12 @@ def get_forecast(ec_data, hourly) -> list[Forecast] | None: if not (half_days := ec_data.daily_forecasts): return None - def get_day_forecast(fcst: list[dict[str, str]]) -> Forecast: + def get_day_forecast( + fcst: list[dict[str, Any]], + ) -> Forecast: high_temp = int(fcst[0]["temperature"]) if len(fcst) == 2 else None return { - ATTR_FORECAST_TIME: fcst[0]["timestamp"], + ATTR_FORECAST_TIME: fcst[0]["timestamp"].isoformat(), ATTR_FORECAST_NATIVE_TEMP: high_temp, ATTR_FORECAST_NATIVE_TEMP_LOW: int(fcst[-1]["temperature"]), ATTR_FORECAST_PRECIPITATION_PROBABILITY: int( diff --git a/tests/components/environment_canada/conftest.py b/tests/components/environment_canada/conftest.py new file mode 100644 index 00000000000..69cec187d11 --- /dev/null +++ b/tests/components/environment_canada/conftest.py @@ -0,0 +1,27 @@ +"""Common fixture for Environment Canada tests.""" + +import contextlib +from datetime import datetime +import json + +import pytest + +from tests.common import load_fixture + + +@pytest.fixture +def ec_data(): + """Load Environment Canada data.""" + + def date_hook(weather): + """Convert timestamp string to datetime.""" + + if t := weather.get("timestamp"): + with contextlib.suppress(ValueError): + weather["timestamp"] = datetime.fromisoformat(t) + return weather + + return json.loads( + load_fixture("environment_canada/current_conditions_data.json"), + object_hook=date_hook, + ) diff --git a/tests/components/environment_canada/snapshots/test_weather.ambr b/tests/components/environment_canada/snapshots/test_weather.ambr index 7ba37110c2a..cfa0ad912a4 100644 --- a/tests/components/environment_canada/snapshots/test_weather.ambr +++ b/tests/components/environment_canada/snapshots/test_weather.ambr @@ -5,35 +5,35 @@ 'forecast': list([ dict({ 'condition': 'sunny', - 'datetime': '2022-10-04 15:00:00+00:00', + 'datetime': '2022-10-04T15:00:00+00:00', 'precipitation_probability': 0, 'temperature': 18.0, 'templow': 3.0, }), dict({ 'condition': 'sunny', - 'datetime': '2022-10-05 15:00:00+00:00', + 'datetime': '2022-10-05T15:00:00+00:00', 'precipitation_probability': 0, 'temperature': 20.0, 'templow': 9.0, }), dict({ 'condition': 'partlycloudy', - 'datetime': '2022-10-06 15:00:00+00:00', + 'datetime': '2022-10-06T15:00:00+00:00', 'precipitation_probability': 0, 'temperature': 20.0, 'templow': 7.0, }), dict({ 'condition': 'rainy', - 'datetime': '2022-10-07 15:00:00+00:00', + 'datetime': '2022-10-07T15:00:00+00:00', 'precipitation_probability': 40, 'temperature': 13.0, 'templow': 1.0, }), dict({ 'condition': 'partlycloudy', - 'datetime': '2022-10-08 15:00:00+00:00', + 'datetime': '2022-10-08T15:00:00+00:00', 'precipitation_probability': 0, 'temperature': 10.0, 'templow': 3.0, @@ -48,42 +48,42 @@ 'forecast': list([ dict({ 'condition': 'clear-night', - 'datetime': '2022-10-03 15:00:00+00:00', + 'datetime': '2022-10-03T15:00:00+00:00', 'precipitation_probability': 0, 'temperature': None, 'templow': -1.0, }), dict({ 'condition': 'sunny', - 'datetime': '2022-10-04 15:00:00+00:00', + 'datetime': '2022-10-04T15:00:00+00:00', 'precipitation_probability': 0, 'temperature': 18.0, 'templow': 3.0, }), dict({ 'condition': 'sunny', - 'datetime': '2022-10-05 15:00:00+00:00', + 'datetime': '2022-10-05T15:00:00+00:00', 'precipitation_probability': 0, 'temperature': 20.0, 'templow': 9.0, }), dict({ 'condition': 'partlycloudy', - 'datetime': '2022-10-06 15:00:00+00:00', + 'datetime': '2022-10-06T15:00:00+00:00', 'precipitation_probability': 0, 'temperature': 20.0, 'templow': 7.0, }), dict({ 'condition': 'rainy', - 'datetime': '2022-10-07 15:00:00+00:00', + 'datetime': '2022-10-07T15:00:00+00:00', 'precipitation_probability': 40, 'temperature': 13.0, 'templow': 1.0, }), dict({ 'condition': 'partlycloudy', - 'datetime': '2022-10-08 15:00:00+00:00', + 'datetime': '2022-10-08T15:00:00+00:00', 'precipitation_probability': 0, 'temperature': 10.0, 'templow': 3.0, diff --git a/tests/components/environment_canada/test_diagnostics.py b/tests/components/environment_canada/test_diagnostics.py index 7e9c8691f90..79b72961124 100644 --- a/tests/components/environment_canada/test_diagnostics.py +++ b/tests/components/environment_canada/test_diagnostics.py @@ -1,6 +1,7 @@ """Test Environment Canada diagnostics.""" import json +from typing import Any from syrupy import SnapshotAssertion @@ -26,6 +27,7 @@ async def test_entry_diagnostics( hass: HomeAssistant, hass_client: ClientSessionGenerator, snapshot: SnapshotAssertion, + ec_data: dict[str, Any], ) -> None: """Test config entry diagnostics.""" diff --git a/tests/components/environment_canada/test_weather.py b/tests/components/environment_canada/test_weather.py index e8c21e2dc06..8e22f68462f 100644 --- a/tests/components/environment_canada/test_weather.py +++ b/tests/components/environment_canada/test_weather.py @@ -1,6 +1,7 @@ """Test weather.""" -import json +import copy +from typing import Any from syrupy.assertion import SnapshotAssertion @@ -12,23 +13,17 @@ from homeassistant.core import HomeAssistant from . import init_integration -from tests.common import load_fixture - async def test_forecast_daily( - hass: HomeAssistant, - snapshot: SnapshotAssertion, + hass: HomeAssistant, snapshot: SnapshotAssertion, ec_data: dict[str, Any] ) -> None: """Test basic forecast.""" - ec_data = json.loads( - load_fixture("environment_canada/current_conditions_data.json") - ) - # First entry in test data is a half day; we don't want that for this test - del ec_data["daily_forecasts"][0] + local_ec_data = copy.deepcopy(ec_data) + del local_ec_data["daily_forecasts"][0] - await init_integration(hass, ec_data) + await init_integration(hass, local_ec_data) response = await hass.services.async_call( WEATHER_DOMAIN, @@ -44,15 +39,10 @@ async def test_forecast_daily( async def test_forecast_daily_with_some_previous_days_data( - hass: HomeAssistant, - snapshot: SnapshotAssertion, + hass: HomeAssistant, snapshot: SnapshotAssertion, ec_data: dict[str, Any] ) -> None: """Test forecast with half day at start.""" - ec_data = json.loads( - load_fixture("environment_canada/current_conditions_data.json") - ) - await init_integration(hass, ec_data) response = await hass.services.async_call( From e8914552b11cd0f75082fb8f9ca33e1fcc094543 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 15 Aug 2024 12:39:01 +0200 Subject: [PATCH 060/271] Bump pyhomeworks to 1.1.1 (#123981) --- homeassistant/components/homeworks/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homeworks/manifest.json b/homeassistant/components/homeworks/manifest.json index 1ba0672c9f1..a399e0a98e7 100644 --- a/homeassistant/components/homeworks/manifest.json +++ b/homeassistant/components/homeworks/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/homeworks", "iot_class": "local_push", "loggers": ["pyhomeworks"], - "requirements": ["pyhomeworks==1.1.0"] + "requirements": ["pyhomeworks==1.1.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index aea1dd79968..a936ce45b5e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1909,7 +1909,7 @@ pyhiveapi==0.5.16 pyhomematic==0.1.77 # homeassistant.components.homeworks -pyhomeworks==1.1.0 +pyhomeworks==1.1.1 # homeassistant.components.ialarm pyialarm==2.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9ad157927b6..4d8a33cef35 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1523,7 +1523,7 @@ pyhiveapi==0.5.16 pyhomematic==0.1.77 # homeassistant.components.homeworks -pyhomeworks==1.1.0 +pyhomeworks==1.1.1 # homeassistant.components.ialarm pyialarm==2.2.0 From 0de89b42aa26e783d58c3c6727d4487ff0574e63 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 16 Aug 2024 09:48:59 -0500 Subject: [PATCH 061/271] Ensure event entities are allowed for linked homekit config via YAML (#123994) --- homeassistant/components/homekit/util.py | 7 +++- tests/components/homekit/test_util.py | 50 ++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index a4566efaa35..4d4620477cb 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -22,6 +22,7 @@ from homeassistant.components import ( sensor, ) from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN +from homeassistant.components.event import DOMAIN as EVENT_DOMAIN from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN from homeassistant.components.media_player import ( DOMAIN as MEDIA_PLAYER_DOMAIN, @@ -167,9 +168,11 @@ CAMERA_SCHEMA = BASIC_INFO_SCHEMA.extend( vol.Optional( CONF_VIDEO_PACKET_SIZE, default=DEFAULT_VIDEO_PACKET_SIZE ): cv.positive_int, - vol.Optional(CONF_LINKED_MOTION_SENSOR): cv.entity_domain(binary_sensor.DOMAIN), + vol.Optional(CONF_LINKED_MOTION_SENSOR): cv.entity_domain( + [binary_sensor.DOMAIN, EVENT_DOMAIN] + ), vol.Optional(CONF_LINKED_DOORBELL_SENSOR): cv.entity_domain( - binary_sensor.DOMAIN + [binary_sensor.DOMAIN, EVENT_DOMAIN] ), } ) diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index 4939511166f..7f7e3ee0ce0 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -7,13 +7,38 @@ import voluptuous as vol from homeassistant.components.homekit.const import ( BRIDGE_NAME, + CONF_AUDIO_CODEC, + CONF_AUDIO_MAP, + CONF_AUDIO_PACKET_SIZE, CONF_FEATURE, CONF_FEATURE_LIST, CONF_LINKED_BATTERY_SENSOR, + CONF_LINKED_DOORBELL_SENSOR, + CONF_LINKED_MOTION_SENSOR, CONF_LOW_BATTERY_THRESHOLD, + CONF_MAX_FPS, + CONF_MAX_HEIGHT, + CONF_MAX_WIDTH, + CONF_STREAM_COUNT, + CONF_SUPPORT_AUDIO, CONF_THRESHOLD_CO, CONF_THRESHOLD_CO2, + CONF_VIDEO_CODEC, + CONF_VIDEO_MAP, + CONF_VIDEO_PACKET_SIZE, + DEFAULT_AUDIO_CODEC, + DEFAULT_AUDIO_MAP, + DEFAULT_AUDIO_PACKET_SIZE, DEFAULT_CONFIG_FLOW_PORT, + DEFAULT_LOW_BATTERY_THRESHOLD, + DEFAULT_MAX_FPS, + DEFAULT_MAX_HEIGHT, + DEFAULT_MAX_WIDTH, + DEFAULT_STREAM_COUNT, + DEFAULT_SUPPORT_AUDIO, + DEFAULT_VIDEO_CODEC, + DEFAULT_VIDEO_MAP, + DEFAULT_VIDEO_PACKET_SIZE, DOMAIN, FEATURE_ON_OFF, FEATURE_PLAY_PAUSE, @@ -178,6 +203,31 @@ def test_validate_entity_config() -> None: assert vec({"sensor.co2": {CONF_THRESHOLD_CO2: 500}}) == { "sensor.co2": {CONF_THRESHOLD_CO2: 500, CONF_LOW_BATTERY_THRESHOLD: 20} } + assert vec( + { + "camera.demo": { + CONF_LINKED_DOORBELL_SENSOR: "event.doorbell", + CONF_LINKED_MOTION_SENSOR: "event.motion", + } + } + ) == { + "camera.demo": { + CONF_LINKED_DOORBELL_SENSOR: "event.doorbell", + CONF_LINKED_MOTION_SENSOR: "event.motion", + CONF_AUDIO_CODEC: DEFAULT_AUDIO_CODEC, + CONF_SUPPORT_AUDIO: DEFAULT_SUPPORT_AUDIO, + CONF_MAX_WIDTH: DEFAULT_MAX_WIDTH, + CONF_MAX_HEIGHT: DEFAULT_MAX_HEIGHT, + CONF_MAX_FPS: DEFAULT_MAX_FPS, + CONF_AUDIO_MAP: DEFAULT_AUDIO_MAP, + CONF_VIDEO_MAP: DEFAULT_VIDEO_MAP, + CONF_STREAM_COUNT: DEFAULT_STREAM_COUNT, + CONF_VIDEO_CODEC: DEFAULT_VIDEO_CODEC, + CONF_AUDIO_PACKET_SIZE: DEFAULT_AUDIO_PACKET_SIZE, + CONF_VIDEO_PACKET_SIZE: DEFAULT_VIDEO_PACKET_SIZE, + CONF_LOW_BATTERY_THRESHOLD: DEFAULT_LOW_BATTERY_THRESHOLD, + } + } def test_validate_media_player_features() -> None: From f5fd5e045749cc5769c84e4c05685961d2c6c9e5 Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Fri, 16 Aug 2024 13:53:11 +0200 Subject: [PATCH 062/271] Bump openwebifpy to 4.2.7 (#123995) * Bump openwebifpy to 4.2.6 * Bump openwebifpy to 4.2.7 --------- Co-authored-by: J. Nick Koston --- homeassistant/components/enigma2/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/enigma2/manifest.json b/homeassistant/components/enigma2/manifest.json index 538cfb56388..1a0875b04c0 100644 --- a/homeassistant/components/enigma2/manifest.json +++ b/homeassistant/components/enigma2/manifest.json @@ -7,5 +7,5 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["openwebif"], - "requirements": ["openwebifpy==4.2.5"] + "requirements": ["openwebifpy==4.2.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index a936ce45b5e..1fdaa072b3d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1505,7 +1505,7 @@ openhomedevice==2.2.0 opensensemap-api==0.2.0 # homeassistant.components.enigma2 -openwebifpy==4.2.5 +openwebifpy==4.2.7 # homeassistant.components.luci openwrt-luci-rpc==1.1.17 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4d8a33cef35..1dadb4821ec 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1238,7 +1238,7 @@ openerz-api==0.3.0 openhomedevice==2.2.0 # homeassistant.components.enigma2 -openwebifpy==4.2.5 +openwebifpy==4.2.7 # homeassistant.components.opower opower==0.6.0 From 04bf8482b232450d0ef407b7278fd4b44de6f3b4 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 16 Aug 2024 16:46:58 +0200 Subject: [PATCH 063/271] Re-enable concord232 (#124000) --- homeassistant/components/concord232/alarm_control_panel.py | 3 +-- homeassistant/components/concord232/binary_sensor.py | 3 +-- homeassistant/components/concord232/manifest.json | 3 +-- homeassistant/components/concord232/ruff.toml | 5 ----- requirements_all.txt | 3 +++ 5 files changed, 6 insertions(+), 11 deletions(-) delete mode 100644 homeassistant/components/concord232/ruff.toml diff --git a/homeassistant/components/concord232/alarm_control_panel.py b/homeassistant/components/concord232/alarm_control_panel.py index d3bafdeba4a..661a2beacc0 100644 --- a/homeassistant/components/concord232/alarm_control_panel.py +++ b/homeassistant/components/concord232/alarm_control_panel.py @@ -1,12 +1,11 @@ """Support for Concord232 alarm control panels.""" -# mypy: ignore-errors from __future__ import annotations import datetime import logging -# from concord232 import client as concord232_client +from concord232 import client as concord232_client import requests import voluptuous as vol diff --git a/homeassistant/components/concord232/binary_sensor.py b/homeassistant/components/concord232/binary_sensor.py index 588e7681746..a1dcbc222f7 100644 --- a/homeassistant/components/concord232/binary_sensor.py +++ b/homeassistant/components/concord232/binary_sensor.py @@ -1,12 +1,11 @@ """Support for exposing Concord232 elements as sensors.""" -# mypy: ignore-errors from __future__ import annotations import datetime import logging -# from concord232 import client as concord232_client +from concord232 import client as concord232_client import requests import voluptuous as vol diff --git a/homeassistant/components/concord232/manifest.json b/homeassistant/components/concord232/manifest.json index ef075ba5f96..e0aea5d64d9 100644 --- a/homeassistant/components/concord232/manifest.json +++ b/homeassistant/components/concord232/manifest.json @@ -2,9 +2,8 @@ "domain": "concord232", "name": "Concord232", "codeowners": [], - "disabled": "This integration is disabled because it uses non-open source code to operate.", "documentation": "https://www.home-assistant.io/integrations/concord232", "iot_class": "local_polling", "loggers": ["concord232", "stevedore"], - "requirements": ["concord232==0.15"] + "requirements": ["concord232==0.15.1"] } diff --git a/homeassistant/components/concord232/ruff.toml b/homeassistant/components/concord232/ruff.toml deleted file mode 100644 index 38f6f586aef..00000000000 --- a/homeassistant/components/concord232/ruff.toml +++ /dev/null @@ -1,5 +0,0 @@ -extend = "../../../pyproject.toml" - -lint.extend-ignore = [ - "F821" -] diff --git a/requirements_all.txt b/requirements_all.txt index 1fdaa072b3d..5397993f7d0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -672,6 +672,9 @@ colorlog==6.8.2 # homeassistant.components.color_extractor colorthief==0.2.1 +# homeassistant.components.concord232 +concord232==0.15.1 + # homeassistant.components.upc_connect connect-box==0.3.1 From fd904c65a7a2177cba3a9774f53fffa7c42021bc Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 16 Aug 2024 04:36:06 +0200 Subject: [PATCH 064/271] Bump aiounifi to v80 (#124004) --- homeassistant/components/unifi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index aa9b553cb67..6f92dec5361 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_push", "loggers": ["aiounifi"], "quality_scale": "platinum", - "requirements": ["aiounifi==79"], + "requirements": ["aiounifi==80"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 5397993f7d0..71a915a55e4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -386,7 +386,7 @@ aiotankerkoenig==0.4.1 aiotractive==0.6.0 # homeassistant.components.unifi -aiounifi==79 +aiounifi==80 # homeassistant.components.vlc_telnet aiovlc==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1dadb4821ec..c787c89f670 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -368,7 +368,7 @@ aiotankerkoenig==0.4.1 aiotractive==0.6.0 # homeassistant.components.unifi -aiounifi==79 +aiounifi==80 # homeassistant.components.vlc_telnet aiovlc==0.3.2 From 6103811de86e6f6e25a22b5d133390c3449d7da9 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Fri, 16 Aug 2024 07:52:18 +1000 Subject: [PATCH 065/271] Fix rear trunk logic in Tessie (#124011) Allow open to be anything not zero --- homeassistant/components/tessie/cover.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tessie/cover.py b/homeassistant/components/tessie/cover.py index 93ce25993d9..e739f8c074d 100644 --- a/homeassistant/components/tessie/cover.py +++ b/homeassistant/components/tessie/cover.py @@ -168,13 +168,13 @@ class TessieRearTrunkEntity(TessieEntity, CoverEntity): async def async_open_cover(self, **kwargs: Any) -> None: """Open rear trunk.""" - if self._value == TessieCoverStates.CLOSED: + if self.is_closed: await self.run(open_close_rear_trunk) self.set((self.key, TessieCoverStates.OPEN)) async def async_close_cover(self, **kwargs: Any) -> None: """Close rear trunk.""" - if self._value == TessieCoverStates.OPEN: + if not self.is_closed: await self.run(open_close_rear_trunk) self.set((self.key, TessieCoverStates.CLOSED)) From 4f0261d7393dbc19e6b9003afd4cff7d53417e77 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 16 Aug 2024 07:12:17 -0500 Subject: [PATCH 066/271] Bump bluetooth-adapters to 0.19.4 (#124018) Fixes a call to enumerate USB devices that did blocking I/O --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 95d2b171c9f..657209cdba0 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -16,7 +16,7 @@ "requirements": [ "bleak==0.22.2", "bleak-retry-connector==3.5.0", - "bluetooth-adapters==0.19.3", + "bluetooth-adapters==0.19.4", "bluetooth-auto-recovery==1.4.2", "bluetooth-data-tools==1.19.4", "dbus-fast==2.22.1", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 05b2bebceea..e87307e13d2 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ awesomeversion==24.6.0 bcrypt==4.1.3 bleak-retry-connector==3.5.0 bleak==0.22.2 -bluetooth-adapters==0.19.3 +bluetooth-adapters==0.19.4 bluetooth-auto-recovery==1.4.2 bluetooth-data-tools==1.19.4 cached_ipaddress==0.3.0 diff --git a/requirements_all.txt b/requirements_all.txt index 71a915a55e4..b450bba1767 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -591,7 +591,7 @@ bluemaestro-ble==0.2.3 # bluepy==1.3.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.19.3 +bluetooth-adapters==0.19.4 # homeassistant.components.bluetooth bluetooth-auto-recovery==1.4.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c787c89f670..63e7a430b61 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -515,7 +515,7 @@ bluecurrent-api==1.2.3 bluemaestro-ble==0.2.3 # homeassistant.components.bluetooth -bluetooth-adapters==0.19.3 +bluetooth-adapters==0.19.4 # homeassistant.components.bluetooth bluetooth-auto-recovery==1.4.2 From def2ace4ecfc764d73d69e7f31348ff174418ca6 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Fri, 16 Aug 2024 13:43:02 +0200 Subject: [PATCH 067/271] Fix loading KNX integration actions when not using YAML (#124027) * Fix loading KNX integration services when not using YAML * remove unnecessary comment * Remove unreachable test --- homeassistant/components/knx/__init__.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index fd46cad8489..a401ee2ccac 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -147,18 +147,10 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Start the KNX integration.""" hass.data[DATA_HASS_CONFIG] = config - conf: ConfigType | None = config.get(DOMAIN) - - if conf is None: - # If we have a config entry, setup is done by that config entry. - # If there is no config entry, this should fail. - return bool(hass.config_entries.async_entries(DOMAIN)) - - conf = dict(conf) - hass.data[DATA_KNX_CONFIG] = conf + if (conf := config.get(DOMAIN)) is not None: + hass.data[DATA_KNX_CONFIG] = dict(conf) register_knx_services(hass) - return True From 93dc08a05fe47e2ad6324543e1b3a9ab84a983ae Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 16 Aug 2024 16:48:33 +0200 Subject: [PATCH 068/271] Bump aiomealie to 0.8.1 (#124047) --- homeassistant/components/mealie/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mealie/manifest.json b/homeassistant/components/mealie/manifest.json index acfe30aecaa..75093577b0f 100644 --- a/homeassistant/components/mealie/manifest.json +++ b/homeassistant/components/mealie/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/mealie", "integration_type": "service", "iot_class": "local_polling", - "requirements": ["aiomealie==0.8.0"] + "requirements": ["aiomealie==0.8.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index b450bba1767..735997d3208 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -288,7 +288,7 @@ aiolookin==1.0.0 aiolyric==1.1.0 # homeassistant.components.mealie -aiomealie==0.8.0 +aiomealie==0.8.1 # homeassistant.components.modern_forms aiomodernforms==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 63e7a430b61..757f8ccf405 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -270,7 +270,7 @@ aiolookin==1.0.0 aiolyric==1.1.0 # homeassistant.components.mealie -aiomealie==0.8.0 +aiomealie==0.8.1 # homeassistant.components.modern_forms aiomodernforms==0.1.8 From be5577c2f9486167696aa7920cdb9e342cda2bf7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 16 Aug 2024 18:02:52 +0200 Subject: [PATCH 069/271] Bump version to 2024.8.2 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index c76bcdaf4b8..39df0486e06 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "1" +PATCH_VERSION: Final = "2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 39e1cb4d221..9bc294b2d0f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.8.1" +version = "2024.8.2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From a2027fc78c3624c88ad9ba74ffb7c00f39b1453c Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 16 Aug 2024 13:50:02 +0200 Subject: [PATCH 070/271] Exclude aiohappyeyeballs from license check (#124041) --- script/licenses.py | 1 + 1 file changed, 1 insertion(+) diff --git a/script/licenses.py b/script/licenses.py index 9c584e7f4fc..659f8cb8dcc 100644 --- a/script/licenses.py +++ b/script/licenses.py @@ -124,6 +124,7 @@ EXCEPTIONS = { "PyXiaomiGateway", # https://github.com/Danielhiversen/PyXiaomiGateway/pull/201 "aiocomelit", # https://github.com/chemelli74/aiocomelit/pull/138 "aioecowitt", # https://github.com/home-assistant-libs/aioecowitt/pull/180 + "aiohappyeyeballs", # Python-2.0.1 "aioopenexchangerates", # https://github.com/MartinHjelmare/aioopenexchangerates/pull/94 "aiooui", # https://github.com/Bluetooth-Devices/aiooui/pull/8 "aioruuvigateway", # https://github.com/akx/aioruuvigateway/pull/6 From e2c1a38d87a729e1933ab9acfe5881f17145f280 Mon Sep 17 00:00:00 2001 From: Daniel Rozycki Date: Sun, 18 Aug 2024 05:24:44 -0700 Subject: [PATCH 071/271] Skip NextBus update if integration is still loading (#123564) * Skip NextBus update if integration is still loading Fixes a race between the loading thread and update thread leading to an unrecoverable error * Use async_at_started * Use local copy of _route_stops to avoid NextBus race condition * Update homeassistant/components/nextbus/coordinator.py --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/nextbus/coordinator.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nextbus/coordinator.py b/homeassistant/components/nextbus/coordinator.py index 6c438f6f808..781742e4c08 100644 --- a/homeassistant/components/nextbus/coordinator.py +++ b/homeassistant/components/nextbus/coordinator.py @@ -50,13 +50,15 @@ class NextBusDataUpdateCoordinator(DataUpdateCoordinator): async def _async_update_data(self) -> dict[str, Any]: """Fetch data from NextBus.""" - self.logger.debug("Updating data from API. Routes: %s", str(self._route_stops)) + + _route_stops = set(self._route_stops) + self.logger.debug("Updating data from API. Routes: %s", str(_route_stops)) def _update_data() -> dict: """Fetch data from NextBus.""" self.logger.debug("Updating data from API (executor)") predictions: dict[RouteStop, dict[str, Any]] = {} - for route_stop in self._route_stops: + for route_stop in _route_stops: prediction_results: list[dict[str, Any]] = [] try: prediction_results = self.client.predictions_for_stop( From dc967e2ef2a3283fc5da80e92ae92bc635ce35cc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 12 Aug 2024 15:40:35 -0500 Subject: [PATCH 072/271] Bump yalexs to 6.5.0 (#123739) --- homeassistant/components/august/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index 293c94c9629..5a911eee5e5 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -28,5 +28,5 @@ "documentation": "https://www.home-assistant.io/integrations/august", "iot_class": "cloud_push", "loggers": ["pubnub", "yalexs"], - "requirements": ["yalexs==6.4.3", "yalexs-ble==2.4.3"] + "requirements": ["yalexs==6.5.0", "yalexs-ble==2.4.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 735997d3208..43ca454e28e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2959,7 +2959,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.4.3 # homeassistant.components.august -yalexs==6.4.3 +yalexs==6.5.0 # homeassistant.components.yeelight yeelight==0.7.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 757f8ccf405..897c1ddd1fe 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2336,7 +2336,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.4.3 # homeassistant.components.august -yalexs==6.4.3 +yalexs==6.5.0 # homeassistant.components.yeelight yeelight==0.7.14 From 80df582ebdd40ad4a67461c9dded654a5290dfa3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 13 Aug 2024 14:06:38 -0500 Subject: [PATCH 073/271] Bump yalexs to 8.0.2 (#123817) --- homeassistant/components/august/config_flow.py | 2 +- homeassistant/components/august/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/august/mocks.py | 2 +- tests/components/august/test_config_flow.py | 2 +- tests/components/august/test_gateway.py | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/august/config_flow.py b/homeassistant/components/august/config_flow.py index 18c15ad61a1..3523a4f7c39 100644 --- a/homeassistant/components/august/config_flow.py +++ b/homeassistant/components/august/config_flow.py @@ -8,7 +8,7 @@ from typing import Any import aiohttp import voluptuous as vol -from yalexs.authenticator import ValidationResult +from yalexs.authenticator_common import ValidationResult from yalexs.const import BRANDS, DEFAULT_BRAND from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index 5a911eee5e5..13035d68dfe 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -28,5 +28,5 @@ "documentation": "https://www.home-assistant.io/integrations/august", "iot_class": "cloud_push", "loggers": ["pubnub", "yalexs"], - "requirements": ["yalexs==6.5.0", "yalexs-ble==2.4.3"] + "requirements": ["yalexs==8.0.2", "yalexs-ble==2.4.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 43ca454e28e..367170f8706 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2959,7 +2959,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.4.3 # homeassistant.components.august -yalexs==6.5.0 +yalexs==8.0.2 # homeassistant.components.yeelight yeelight==0.7.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 897c1ddd1fe..837ec134197 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2336,7 +2336,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.4.3 # homeassistant.components.august -yalexs==6.5.0 +yalexs==8.0.2 # homeassistant.components.yeelight yeelight==0.7.14 diff --git a/tests/components/august/mocks.py b/tests/components/august/mocks.py index 30be50e75c9..a0f5b55a607 100644 --- a/tests/components/august/mocks.py +++ b/tests/components/august/mocks.py @@ -25,7 +25,7 @@ from yalexs.activity import ( DoorOperationActivity, LockOperationActivity, ) -from yalexs.authenticator import AuthenticationState +from yalexs.authenticator_common import AuthenticationState from yalexs.const import Brand from yalexs.doorbell import Doorbell, DoorbellDetail from yalexs.lock import Lock, LockDetail diff --git a/tests/components/august/test_config_flow.py b/tests/components/august/test_config_flow.py index aec08864c65..fdebb8d5c46 100644 --- a/tests/components/august/test_config_flow.py +++ b/tests/components/august/test_config_flow.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from yalexs.authenticator import ValidationResult +from yalexs.authenticator_common import ValidationResult from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation from homeassistant import config_entries diff --git a/tests/components/august/test_gateway.py b/tests/components/august/test_gateway.py index e605fd74f0a..74266397ed5 100644 --- a/tests/components/august/test_gateway.py +++ b/tests/components/august/test_gateway.py @@ -50,5 +50,5 @@ async def _patched_refresh_access_token( ) await august_gateway.async_refresh_access_token_if_needed() refresh_access_token_mock.assert_called() - assert august_gateway.access_token == new_token + assert await august_gateway.async_get_access_token() == new_token assert august_gateway.authentication.access_token_expires == new_token_expire_time From 3484ab3c0cbb510816cf797807ce6031204e8edb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 19 Aug 2024 19:13:35 -0500 Subject: [PATCH 074/271] Bump aioshelly to 11.2.4 (#124080) --- homeassistant/components/shelly/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index c742b45632c..da3bbc4bb6e 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -9,7 +9,7 @@ "iot_class": "local_push", "loggers": ["aioshelly"], "quality_scale": "platinum", - "requirements": ["aioshelly==11.2.0"], + "requirements": ["aioshelly==11.2.4"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 367170f8706..664e56695c4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -359,7 +359,7 @@ aioruuvigateway==0.1.0 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==11.2.0 +aioshelly==11.2.4 # homeassistant.components.skybell aioskybell==22.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 837ec134197..f2e82dc9bae 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -341,7 +341,7 @@ aioruuvigateway==0.1.0 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==11.2.0 +aioshelly==11.2.4 # homeassistant.components.skybell aioskybell==22.7.0 From d1f09ecd0c0214e4292b964cd50ae30015351984 Mon Sep 17 00:00:00 2001 From: cdnninja Date: Sun, 18 Aug 2024 07:36:03 -0600 Subject: [PATCH 075/271] Add Alt Core300s model to vesync integration (#124091) --- homeassistant/components/vesync/const.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/vesync/const.py b/homeassistant/components/vesync/const.py index 483ab89b02e..54fc21d2659 100644 --- a/homeassistant/components/vesync/const.py +++ b/homeassistant/components/vesync/const.py @@ -40,6 +40,7 @@ SKU_TO_BASE_DEVICE = { "LAP-C202S-WUSR": "Core200S", # Alt ID Model Core200S "Core300S": "Core300S", "LAP-C301S-WJP": "Core300S", # Alt ID Model Core300S + "LAP-C301S-WAAA": "Core300S", # Alt ID Model Core300S "Core400S": "Core400S", "LAP-C401S-WJP": "Core400S", # Alt ID Model Core400S "LAP-C401S-WUSR": "Core400S", # Alt ID Model Core400S From 0fcdc3c200c8231fb24ecd0f5cf9e097f93d6ceb Mon Sep 17 00:00:00 2001 From: Artem Draft Date: Sat, 17 Aug 2024 17:30:26 +0300 Subject: [PATCH 076/271] Bump pybravia to 0.3.4 (#124113) --- homeassistant/components/braviatv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/braviatv/manifest.json b/homeassistant/components/braviatv/manifest.json index 5a0a9def0ae..a445a34cfcd 100644 --- a/homeassistant/components/braviatv/manifest.json +++ b/homeassistant/components/braviatv/manifest.json @@ -7,7 +7,7 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["pybravia"], - "requirements": ["pybravia==0.3.3"], + "requirements": ["pybravia==0.3.4"], "ssdp": [ { "st": "urn:schemas-sony-com:service:ScalarWebAPI:1", diff --git a/requirements_all.txt b/requirements_all.txt index 664e56695c4..64a531b3156 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1759,7 +1759,7 @@ pyblu==0.4.0 pybotvac==0.0.25 # homeassistant.components.braviatv -pybravia==0.3.3 +pybravia==0.3.4 # homeassistant.components.nissan_leaf pycarwings2==2.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f2e82dc9bae..85c211978f9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1421,7 +1421,7 @@ pyblu==0.4.0 pybotvac==0.0.25 # homeassistant.components.braviatv -pybravia==0.3.3 +pybravia==0.3.4 # homeassistant.components.cloudflare pycfdns==3.0.0 From 157a61845b70793a8278818a8e5a7e1bebe66308 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 17 Aug 2024 10:32:58 -0500 Subject: [PATCH 077/271] Bump aiohomekit to 3.2.3 (#124115) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 007153aceaf..b2b215a98b9 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -14,6 +14,6 @@ "documentation": "https://www.home-assistant.io/integrations/homekit_controller", "iot_class": "local_push", "loggers": ["aiohomekit", "commentjson"], - "requirements": ["aiohomekit==3.2.2"], + "requirements": ["aiohomekit==3.2.3"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 64a531b3156..a88075818c1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -255,7 +255,7 @@ aioguardian==2022.07.0 aioharmony==0.2.10 # homeassistant.components.homekit_controller -aiohomekit==3.2.2 +aiohomekit==3.2.3 # homeassistant.components.hue aiohue==4.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 85c211978f9..fadcabbb82c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -240,7 +240,7 @@ aioguardian==2022.07.0 aioharmony==0.2.10 # homeassistant.components.homekit_controller -aiohomekit==3.2.2 +aiohomekit==3.2.3 # homeassistant.components.hue aiohue==4.7.2 From f89e8e6ceb3c92e3acb0ebeb2fe86c7a5c0d82d1 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 17 Aug 2024 12:11:19 -0700 Subject: [PATCH 078/271] Bump nest to 4.0.7 to increase subscriber deadline (#124131) Bump nest to 4.0.7 --- homeassistant/components/nest/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index fbe5ddb6534..3472fa64e8f 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -20,5 +20,5 @@ "iot_class": "cloud_push", "loggers": ["google_nest_sdm"], "quality_scale": "platinum", - "requirements": ["google-nest-sdm==4.0.6"] + "requirements": ["google-nest-sdm==4.0.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index a88075818c1..84e2baa39ae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -989,7 +989,7 @@ google-cloud-texttospeech==2.16.3 google-generativeai==0.6.0 # homeassistant.components.nest -google-nest-sdm==4.0.6 +google-nest-sdm==4.0.7 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fadcabbb82c..ed59051cf3f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -833,7 +833,7 @@ google-cloud-pubsub==2.13.11 google-generativeai==0.6.0 # homeassistant.components.nest -google-nest-sdm==4.0.6 +google-nest-sdm==4.0.7 # homeassistant.components.google_travel_time googlemaps==2.5.1 From 22bb3e5477ff49ef2f95491325308dcbc23237ae Mon Sep 17 00:00:00 2001 From: MarkGodwin <10632972+MarkGodwin@users.noreply.github.com> Date: Sun, 18 Aug 2024 13:23:47 +0100 Subject: [PATCH 079/271] Bump tplink-omada-api to 1.4.2 (#124136) Fix for bad pre-registered clients --- homeassistant/components/tplink_omada/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tplink_omada/manifest.json b/homeassistant/components/tplink_omada/manifest.json index 9544470d7a9..6bde656dc30 100644 --- a/homeassistant/components/tplink_omada/manifest.json +++ b/homeassistant/components/tplink_omada/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/tplink_omada", "integration_type": "hub", "iot_class": "local_polling", - "requirements": ["tplink-omada-client==1.3.12"] + "requirements": ["tplink-omada-client==1.4.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 84e2baa39ae..1d7c14235e2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2792,7 +2792,7 @@ total-connect-client==2024.5 tp-connected==0.0.4 # homeassistant.components.tplink_omada -tplink-omada-client==1.3.12 +tplink-omada-client==1.4.2 # homeassistant.components.transmission transmission-rpc==7.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ed59051cf3f..5fd713da9cb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2190,7 +2190,7 @@ toonapi==0.3.0 total-connect-client==2024.5 # homeassistant.components.tplink_omada -tplink-omada-client==1.3.12 +tplink-omada-client==1.4.2 # homeassistant.components.transmission transmission-rpc==7.0.3 From e80dc521759ae0ad74956ad30443309d241885e8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 18 Aug 2024 08:39:56 -0500 Subject: [PATCH 080/271] Bump aiohttp to 3.10.4 (#124137) changelog: https://github.com/aio-libs/aiohttp/compare/v3.10.3...v3.10.4 --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e87307e13d2..1d909111657 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodhcpwatcher==1.0.2 aiodiscover==2.1.0 aiodns==3.2.0 aiohttp-fast-zlib==0.1.1 -aiohttp==3.10.3 +aiohttp==3.10.4 aiohttp_cors==0.7.0 aiozoneinfo==0.2.1 astral==2.2 diff --git a/pyproject.toml b/pyproject.toml index 9bc294b2d0f..4df72b9b47f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ classifiers = [ requires-python = ">=3.12.0" dependencies = [ "aiodns==3.2.0", - "aiohttp==3.10.3", + "aiohttp==3.10.4", "aiohttp_cors==0.7.0", "aiohttp-fast-zlib==0.1.1", "aiozoneinfo==0.2.1", diff --git a/requirements.txt b/requirements.txt index 7a4b0bd6d09..39801bdeb91 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ # Home Assistant Core aiodns==3.2.0 -aiohttp==3.10.3 +aiohttp==3.10.4 aiohttp_cors==0.7.0 aiohttp-fast-zlib==0.1.1 aiozoneinfo==0.2.1 From 45b44f8a592f70db87a45c6abeac137b6836e2e9 Mon Sep 17 00:00:00 2001 From: Christopher Maio Date: Sun, 25 Aug 2024 09:05:13 -0400 Subject: [PATCH 081/271] Update Matter light transition blocklist to include GE Cync Undercabinet Lights (#124138) --- homeassistant/components/matter/light.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/matter/light.py b/homeassistant/components/matter/light.py index 6e9019c46fa..58ef8081fa9 100644 --- a/homeassistant/components/matter/light.py +++ b/homeassistant/components/matter/light.py @@ -60,6 +60,8 @@ TRANSITION_BLOCKLIST = ( (4456, 1011, "1.0.0", "2.00.00"), (4488, 260, "1.0", "1.0.0"), (4488, 514, "1.0", "1.0.0"), + (4921, 42, "1.0", "1.01.060"), + (4921, 43, "1.0", "1.01.060"), (4999, 24875, "1.0", "27.0"), (4999, 25057, "1.0", "27.0"), (5009, 514, "1.0", "1.0.0"), From 129035967b9563089690d542799edb26c1d87e3a Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sun, 18 Aug 2024 18:35:02 +0300 Subject: [PATCH 082/271] Shelly RPC - do not stop BLE scanner if a sleeping device (#124147) --- .../components/shelly/coordinator.py | 3 ++- tests/components/shelly/test_coordinator.py | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index 50140e1890d..2710565f960 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -711,7 +711,8 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): """Shutdown the coordinator.""" if self.device.connected: try: - await async_stop_scanner(self.device) + if not self.sleep_period: + await async_stop_scanner(self.device) await super().shutdown() except InvalidAuthError: self.entry.async_start_reauth(self.hass) diff --git a/tests/components/shelly/test_coordinator.py b/tests/components/shelly/test_coordinator.py index d3494c094f9..1140c93775b 100644 --- a/tests/components/shelly/test_coordinator.py +++ b/tests/components/shelly/test_coordinator.py @@ -854,6 +854,27 @@ async def test_rpc_runs_connected_events_when_initialized( assert call.script_list() in mock_rpc_device.mock_calls +async def test_rpc_sleeping_device_unload_ignore_ble_scanner( + hass: HomeAssistant, + mock_rpc_device: Mock, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test RPC sleeping device does not stop ble scanner on unload.""" + monkeypatch.setattr(mock_rpc_device, "connected", True) + entry = await init_integration(hass, 2, sleep_period=1000) + + # Make device online + mock_rpc_device.mock_online() + await hass.async_block_till_done(wait_background_tasks=True) + + # Unload + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + # BLE script list is called during stop ble scanner + assert call.script_list() not in mock_rpc_device.mock_calls + + async def test_block_sleeping_device_connection_error( hass: HomeAssistant, device_registry: dr.DeviceRegistry, From b7d8f3d005db033bb1f03aeebd7a24f0c0b5088d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 22 Aug 2024 08:14:45 -0500 Subject: [PATCH 083/271] Fix shelly available check when device is not initialized (#124182) * Fix shelly available check when device is not initialized available needs to check for device.initialized or if the device is sleepy as calls to status will raise NotInitialized which results in many unretrieved exceptions while writing state fixes ``` 2024-08-18 09:33:03.757 ERROR (MainThread) [homeassistant] Error doing job: Task exception was never retrieved (None) Traceback (most recent call last): File "/usr/src/homeassistant/homeassistant/helpers/update_coordinator.py", line 258, in _handle_refresh_interval await self._async_refresh(log_failures=True, scheduled=True) File "/usr/src/homeassistant/homeassistant/helpers/update_coordinator.py", line 453, in _async_refresh self.async_update_listeners() File "/usr/src/homeassistant/homeassistant/helpers/update_coordinator.py", line 168, in async_update_listeners update_callback() File "/config/custom_components/shelly/entity.py", line 374, in _update_callback self.async_write_ha_state() File "/usr/src/homeassistant/homeassistant/helpers/entity.py", line 1005, in async_write_ha_state self._async_write_ha_state() File "/usr/src/homeassistant/homeassistant/helpers/entity.py", line 1130, in _async_write_ha_state self.__async_calculate_state() File "/usr/src/homeassistant/homeassistant/helpers/entity.py", line 1067, in __async_calculate_state state = self._stringify_state(available) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/usr/src/homeassistant/homeassistant/helpers/entity.py", line 1011, in _stringify_state if (state := self.state) is None: ^^^^^^^^^^ File "/usr/src/homeassistant/homeassistant/components/binary_sensor/__init__.py", line 293, in state if (is_on := self.is_on) is None: ^^^^^^^^^^ File "/config/custom_components/shelly/binary_sensor.py", line 331, in is_on return bool(self.attribute_value) ^^^^^^^^^^^^^^^^^^^^ File "/config/custom_components/shelly/entity.py", line 545, in attribute_value self._last_value = self.sub_status ^^^^^^^^^^^^^^^ File "/config/custom_components/shelly/entity.py", line 534, in sub_status return self.status[self.entity_description.sub_key] ^^^^^^^^^^^ File "/config/custom_components/shelly/entity.py", line 364, in status return cast(dict, self.coordinator.device.status[self.key]) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/usr/local/lib/python3.12/site-packages/aioshelly/rpc_device/device.py", line 390, in status raise NotInitialized aioshelly.exceptions.NotInitialized ``` * tweak * cover * fix * cover * fixes --- .../components/shelly/coordinator.py | 3 ++ homeassistant/components/shelly/entity.py | 8 +++++ tests/components/shelly/test_sensor.py | 34 ++++++++++++++++++- 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index 2710565f960..6286e515727 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -682,6 +682,7 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): self.entry.async_create_background_task( self.hass, self._async_connected(), "rpc device init", eager_start=True ) + # Make sure entities are marked available self.async_set_updated_data(None) elif update_type is RpcUpdateType.DISCONNECTED: self.entry.async_create_background_task( @@ -690,6 +691,8 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): "rpc device disconnected", eager_start=True, ) + # Make sure entities are marked as unavailable + self.async_set_updated_data(None) elif update_type is RpcUpdateType.STATUS: self.async_set_updated_data(None) if self.sleep_period: diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index 5bf8a411377..980a39feaba 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -358,6 +358,14 @@ class ShellyRpcEntity(CoordinatorEntity[ShellyRpcCoordinator]): self._attr_unique_id = f"{coordinator.mac}-{key}" self._attr_name = get_rpc_entity_name(coordinator.device, key) + @property + def available(self) -> bool: + """Check if device is available and initialized or sleepy.""" + coordinator = self.coordinator + return super().available and ( + coordinator.device.initialized or bool(coordinator.sleep_period) + ) + @property def status(self) -> dict: """Device status by entity key.""" diff --git a/tests/components/shelly/test_sensor.py b/tests/components/shelly/test_sensor.py index a39123a6722..2da82a5da87 100644 --- a/tests/components/shelly/test_sensor.py +++ b/tests/components/shelly/test_sensor.py @@ -43,7 +43,7 @@ from . import ( register_entity, ) -from tests.common import mock_restore_cache_with_extra_data +from tests.common import async_fire_time_changed, mock_restore_cache_with_extra_data RELAY_BLOCK_ID = 0 SENSOR_BLOCK_ID = 3 @@ -1189,3 +1189,35 @@ async def test_rpc_remove_enum_virtual_sensor_when_orphaned( entry = entity_registry.async_get(entity_id) assert not entry + + +async def test_rpc_device_sensor_goes_unavailable_on_disconnect( + hass: HomeAssistant, + mock_rpc_device: Mock, + monkeypatch: pytest.MonkeyPatch, + freezer: FrozenDateTimeFactory, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test RPC device with sensor goes unavailable on disconnect.""" + await init_integration(hass, 2) + temp_sensor_state = hass.states.get("sensor.test_name_temperature") + assert temp_sensor_state is not None + assert temp_sensor_state.state != STATE_UNAVAILABLE + monkeypatch.setattr(mock_rpc_device, "connected", False) + monkeypatch.setattr(mock_rpc_device, "initialized", False) + mock_rpc_device.mock_disconnected() + await hass.async_block_till_done() + temp_sensor_state = hass.states.get("sensor.test_name_temperature") + assert temp_sensor_state.state == STATE_UNAVAILABLE + + freezer.tick(60) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert "NotInitialized" not in caplog.text + + monkeypatch.setattr(mock_rpc_device, "connected", True) + monkeypatch.setattr(mock_rpc_device, "initialized", True) + mock_rpc_device.mock_initialized() + await hass.async_block_till_done() + temp_sensor_state = hass.states.get("sensor.test_name_temperature") + assert temp_sensor_state.state != STATE_UNAVAILABLE From a857f603c82b21d745aa3de654d1a4da61085bc0 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 19 Aug 2024 10:20:58 +0200 Subject: [PATCH 084/271] Bump pyhomeworks to 1.1.2 (#124199) --- homeassistant/components/homeworks/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homeworks/manifest.json b/homeassistant/components/homeworks/manifest.json index a399e0a98e7..011c301d00d 100644 --- a/homeassistant/components/homeworks/manifest.json +++ b/homeassistant/components/homeworks/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/homeworks", "iot_class": "local_push", "loggers": ["pyhomeworks"], - "requirements": ["pyhomeworks==1.1.1"] + "requirements": ["pyhomeworks==1.1.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1d7c14235e2..e65c4d4c039 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1912,7 +1912,7 @@ pyhiveapi==0.5.16 pyhomematic==0.1.77 # homeassistant.components.homeworks -pyhomeworks==1.1.1 +pyhomeworks==1.1.2 # homeassistant.components.ialarm pyialarm==2.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5fd713da9cb..7cc3aabdf4f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1523,7 +1523,7 @@ pyhiveapi==0.5.16 pyhomematic==0.1.77 # homeassistant.components.homeworks -pyhomeworks==1.1.1 +pyhomeworks==1.1.2 # homeassistant.components.ialarm pyialarm==2.2.0 From 1f466702662e0b2bd541aaa06aa5464f28dab2b8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 19 Aug 2024 15:40:32 -0500 Subject: [PATCH 085/271] Bump aiohttp to 3.10.5 (#124254) --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1d909111657..432e213d267 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodhcpwatcher==1.0.2 aiodiscover==2.1.0 aiodns==3.2.0 aiohttp-fast-zlib==0.1.1 -aiohttp==3.10.4 +aiohttp==3.10.5 aiohttp_cors==0.7.0 aiozoneinfo==0.2.1 astral==2.2 diff --git a/pyproject.toml b/pyproject.toml index 4df72b9b47f..c2b167bbbd7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ classifiers = [ requires-python = ">=3.12.0" dependencies = [ "aiodns==3.2.0", - "aiohttp==3.10.4", + "aiohttp==3.10.5", "aiohttp_cors==0.7.0", "aiohttp-fast-zlib==0.1.1", "aiozoneinfo==0.2.1", diff --git a/requirements.txt b/requirements.txt index 39801bdeb91..9af81e775ee 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ # Home Assistant Core aiodns==3.2.0 -aiohttp==3.10.4 +aiohttp==3.10.5 aiohttp_cors==0.7.0 aiohttp-fast-zlib==0.1.1 aiozoneinfo==0.2.1 From 524e09b45eb1f63f5e9b9282724645eff04fd864 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sat, 24 Aug 2024 06:48:02 +0200 Subject: [PATCH 086/271] Update xknx to 3.1.1 (#124257) --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 9ecf687d6b9..b7efd14fa2a 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -11,7 +11,7 @@ "loggers": ["xknx", "xknxproject"], "quality_scale": "platinum", "requirements": [ - "xknx==3.1.0", + "xknx==3.1.1", "xknxproject==3.7.1", "knx-frontend==2024.8.9.225351" ], diff --git a/requirements_all.txt b/requirements_all.txt index e65c4d4c039..f260f2eed36 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2936,7 +2936,7 @@ xbox-webapi==2.0.11 xiaomi-ble==0.30.2 # homeassistant.components.knx -xknx==3.1.0 +xknx==3.1.1 # homeassistant.components.knx xknxproject==3.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7cc3aabdf4f..94d224d5e62 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2316,7 +2316,7 @@ xbox-webapi==2.0.11 xiaomi-ble==0.30.2 # homeassistant.components.knx -xknx==3.1.0 +xknx==3.1.1 # homeassistant.components.knx xknxproject==3.7.1 From 5a73b636e35e551fcfef089b96818c8a9040c344 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 20 Aug 2024 00:51:44 -0700 Subject: [PATCH 087/271] Bump python-roborock to 2.6.0 (#124268) --- homeassistant/components/roborock/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/roborock/manifest.json b/homeassistant/components/roborock/manifest.json index 7a80a9083e9..3bb3b9b2046 100644 --- a/homeassistant/components/roborock/manifest.json +++ b/homeassistant/components/roborock/manifest.json @@ -7,7 +7,7 @@ "iot_class": "local_polling", "loggers": ["roborock"], "requirements": [ - "python-roborock==2.5.0", + "python-roborock==2.6.0", "vacuum-map-parser-roborock==0.1.2" ] } diff --git a/requirements_all.txt b/requirements_all.txt index f260f2eed36..2cf52a82ac5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2341,7 +2341,7 @@ python-rabbitair==0.0.8 python-ripple-api==0.0.3 # homeassistant.components.roborock -python-roborock==2.5.0 +python-roborock==2.6.0 # homeassistant.components.smarttub python-smarttub==0.0.36 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 94d224d5e62..eebb7d7c133 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1850,7 +1850,7 @@ python-picnic-api==1.1.0 python-rabbitair==0.0.8 # homeassistant.components.roborock -python-roborock==2.5.0 +python-roborock==2.6.0 # homeassistant.components.smarttub python-smarttub==0.0.36 From 5a8045d1fbad645e26992b19d4018b9b2eaee48f Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Sun, 25 Aug 2024 15:06:16 +0200 Subject: [PATCH 088/271] Prevent KeyError when Matter device sends invalid value for StartUpOnOff (#124280) --- homeassistant/components/matter/select.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/matter/select.py b/homeassistant/components/matter/select.py index 4a9ef3780d1..b46cad53123 100644 --- a/homeassistant/components/matter/select.py +++ b/homeassistant/components/matter/select.py @@ -229,12 +229,12 @@ DISCOVERY_SCHEMAS = [ entity_category=EntityCategory.CONFIG, translation_key="startup_on_off", options=["On", "Off", "Toggle", "Previous"], - measurement_to_ha=lambda x: { + measurement_to_ha=lambda x: { # pylint: disable=unnecessary-lambda 0: "Off", 1: "On", 2: "Toggle", None: "Previous", - }[x], + }.get(x), ha_to_native_value=lambda x: { "Off": 0, "On": 1, From 769c7f1ea35ed14944f1f1974c9efbfdfc0fc522 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 24 Aug 2024 07:20:00 +0200 Subject: [PATCH 089/271] Don't abort airgradient user flow if flow in progress (#124300) --- .../components/airgradient/config_flow.py | 4 ++- .../airgradient/test_config_flow.py | 29 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/airgradient/config_flow.py b/homeassistant/components/airgradient/config_flow.py index 93cd0be61c4..70fa8a1755b 100644 --- a/homeassistant/components/airgradient/config_flow.py +++ b/homeassistant/components/airgradient/config_flow.py @@ -92,7 +92,9 @@ class AirGradientConfigFlow(ConfigFlow, domain=DOMAIN): except AirGradientError: errors["base"] = "cannot_connect" else: - await self.async_set_unique_id(current_measures.serial_number) + await self.async_set_unique_id( + current_measures.serial_number, raise_on_progress=False + ) self._abort_if_unique_id_configured() await self.set_configuration_source() return self.async_create_entry( diff --git a/tests/components/airgradient/test_config_flow.py b/tests/components/airgradient/test_config_flow.py index 222ac5d04af..8730b18676f 100644 --- a/tests/components/airgradient/test_config_flow.py +++ b/tests/components/airgradient/test_config_flow.py @@ -253,3 +253,32 @@ async def test_zeroconf_flow_abort_old_firmware(hass: HomeAssistant) -> None: ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_version" + + +async def test_user_flow_works_discovery( + hass: HomeAssistant, + mock_new_airgradient_client: AsyncMock, + mock_setup_entry: AsyncMock, +) -> None: + """Test user flow can continue after discovery happened.""" + await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=ZEROCONF_DISCOVERY, + ) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + assert len(hass.config_entries.flow.async_progress(DOMAIN)) == 2 + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "10.0.0.131"}, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + + # Verify the discovery flow was aborted + assert not hass.config_entries.flow.async_progress(DOMAIN) From 70a58a0bb00ee0fa61f28dcf3c3a7b3e3fe7a7e4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 20 Aug 2024 11:38:29 -0500 Subject: [PATCH 090/271] Bump yalexs to 8.1.2 (#124303) --- homeassistant/components/august/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index 13035d68dfe..5d7b253e952 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -28,5 +28,5 @@ "documentation": "https://www.home-assistant.io/integrations/august", "iot_class": "cloud_push", "loggers": ["pubnub", "yalexs"], - "requirements": ["yalexs==8.0.2", "yalexs-ble==2.4.3"] + "requirements": ["yalexs==8.1.2", "yalexs-ble==2.4.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2cf52a82ac5..a09e7820a83 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2959,7 +2959,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.4.3 # homeassistant.components.august -yalexs==8.0.2 +yalexs==8.1.2 # homeassistant.components.yeelight yeelight==0.7.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index eebb7d7c133..943a69901f7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2336,7 +2336,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.4.3 # homeassistant.components.august -yalexs==8.0.2 +yalexs==8.1.2 # homeassistant.components.yeelight yeelight==0.7.14 From 236fa8e2384031cb24ce20991632484597426a8b Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 18 Aug 2024 20:05:41 +0200 Subject: [PATCH 091/271] Bump python-holidays to 0.54 (#124170) --- homeassistant/components/holiday/manifest.json | 2 +- homeassistant/components/workday/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/holiday/manifest.json b/homeassistant/components/holiday/manifest.json index ebe472d7f0e..0a714815ae3 100644 --- a/homeassistant/components/holiday/manifest.json +++ b/homeassistant/components/holiday/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/holiday", "iot_class": "local_polling", - "requirements": ["holidays==0.53", "babel==2.15.0"] + "requirements": ["holidays==0.54", "babel==2.15.0"] } diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index 69df8080fa5..133c82454bc 100644 --- a/homeassistant/components/workday/manifest.json +++ b/homeassistant/components/workday/manifest.json @@ -7,5 +7,5 @@ "iot_class": "local_polling", "loggers": ["holidays"], "quality_scale": "internal", - "requirements": ["holidays==0.53"] + "requirements": ["holidays==0.54"] } diff --git a/requirements_all.txt b/requirements_all.txt index a09e7820a83..db41a5ed5a0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1096,7 +1096,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.53 +holidays==0.54 # homeassistant.components.frontend home-assistant-frontend==20240809.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 943a69901f7..b62084a586d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -916,7 +916,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.53 +holidays==0.54 # homeassistant.components.frontend home-assistant-frontend==20240809.0 From e5a64a1e0a1d107dcf1da2b3bdf0142c29dfae44 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 22 Aug 2024 07:59:21 +0200 Subject: [PATCH 092/271] Bump python-holidays to 0.55 (#124314) --- homeassistant/components/holiday/manifest.json | 2 +- homeassistant/components/workday/binary_sensor.py | 4 ++-- homeassistant/components/workday/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/holiday/manifest.json b/homeassistant/components/holiday/manifest.json index 0a714815ae3..0a3064450d4 100644 --- a/homeassistant/components/holiday/manifest.json +++ b/homeassistant/components/holiday/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/holiday", "iot_class": "local_polling", - "requirements": ["holidays==0.54", "babel==2.15.0"] + "requirements": ["holidays==0.55", "babel==2.15.0"] } diff --git a/homeassistant/components/workday/binary_sensor.py b/homeassistant/components/workday/binary_sensor.py index 4635b2209a6..33c2e249024 100644 --- a/homeassistant/components/workday/binary_sensor.py +++ b/homeassistant/components/workday/binary_sensor.py @@ -92,7 +92,7 @@ def _get_obj_holidays( subdiv=province, years=year, language=language, - categories=set_categories, # type: ignore[arg-type] + categories=set_categories, ) if (supported_languages := obj_holidays.supported_languages) and language == "en": for lang in supported_languages: @@ -102,7 +102,7 @@ def _get_obj_holidays( subdiv=province, years=year, language=lang, - categories=set_categories, # type: ignore[arg-type] + categories=set_categories, ) LOGGER.debug("Changing language from %s to %s", language, lang) return obj_holidays diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index 133c82454bc..fafa870d00a 100644 --- a/homeassistant/components/workday/manifest.json +++ b/homeassistant/components/workday/manifest.json @@ -7,5 +7,5 @@ "iot_class": "local_polling", "loggers": ["holidays"], "quality_scale": "internal", - "requirements": ["holidays==0.54"] + "requirements": ["holidays==0.55"] } diff --git a/requirements_all.txt b/requirements_all.txt index db41a5ed5a0..cf9bff55916 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1096,7 +1096,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.54 +holidays==0.55 # homeassistant.components.frontend home-assistant-frontend==20240809.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b62084a586d..992a19e41e4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -916,7 +916,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.54 +holidays==0.55 # homeassistant.components.frontend home-assistant-frontend==20240809.0 From 667af10017caaf033727df6ee865a106f0b5ef48 Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Tue, 20 Aug 2024 09:45:16 -0700 Subject: [PATCH 093/271] Add missing strings for riemann options flow (#124317) --- homeassistant/components/integration/strings.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/integration/strings.json b/homeassistant/components/integration/strings.json index 55d4df1b45e..6186521aa1b 100644 --- a/homeassistant/components/integration/strings.json +++ b/homeassistant/components/integration/strings.json @@ -31,12 +31,14 @@ "round": "[%key:component::integration::config::step::user::data::round%]", "source": "[%key:component::integration::config::step::user::data::source%]", "unit_prefix": "[%key:component::integration::config::step::user::data::unit_prefix%]", - "unit_time": "[%key:component::integration::config::step::user::data::unit_time%]" + "unit_time": "[%key:component::integration::config::step::user::data::unit_time%]", + "max_sub_interval": "[%key:component::integration::config::step::user::data::max_sub_interval%]" }, "data_description": { "round": "[%key:component::integration::config::step::user::data_description::round%]", "unit_prefix": "[%key:component::integration::config::step::user::data_description::unit_prefix%]", - "unit_time": "[%key:component::integration::config::step::user::data_description::unit_time%]" + "unit_time": "[%key:component::integration::config::step::user::data_description::unit_time%]", + "max_sub_interval": "[%key:component::integration::config::step::user::data_description::max_sub_interval%]" } } } From 8f4af4f7c2e513e7468e5d465aa3f3c41f5fc65f Mon Sep 17 00:00:00 2001 From: Pete Sage <76050312+PeteRager@users.noreply.github.com> Date: Wed, 21 Aug 2024 15:05:09 -0400 Subject: [PATCH 094/271] Fix Spotify Media Browsing fails for new config entries (#124368) * initial commit * tests * tests * update tests * update tests * update tests --- .../components/spotify/browse_media.py | 11 +- tests/components/spotify/conftest.py | 128 ++++++++++ .../spotify/snapshots/test_media_browser.ambr | 236 ++++++++++++++++++ .../components/spotify/test_media_browser.py | 61 +++++ 4 files changed, 434 insertions(+), 2 deletions(-) create mode 100644 tests/components/spotify/conftest.py create mode 100644 tests/components/spotify/snapshots/test_media_browser.ambr create mode 100644 tests/components/spotify/test_media_browser.py diff --git a/homeassistant/components/spotify/browse_media.py b/homeassistant/components/spotify/browse_media.py index cff7cae5ebd..abcb6df6205 100644 --- a/homeassistant/components/spotify/browse_media.py +++ b/homeassistant/components/spotify/browse_media.py @@ -172,10 +172,17 @@ async def async_browse_media( # Check for config entry specifier, and extract Spotify URI parsed_url = yarl.URL(media_content_id) + host = parsed_url.host if ( - parsed_url.host is None - or (entry := hass.config_entries.async_get_entry(parsed_url.host)) is None + host is None + # config entry ids can be upper or lower case. Yarl always returns host + # names in lower case, so we need to look for the config entry in both + or ( + entry := hass.config_entries.async_get_entry(host) + or hass.config_entries.async_get_entry(host.upper()) + ) + is None or not isinstance(entry.runtime_data, HomeAssistantSpotifyData) ): raise BrowseError("Invalid Spotify account specified") diff --git a/tests/components/spotify/conftest.py b/tests/components/spotify/conftest.py new file mode 100644 index 00000000000..3f248b54529 --- /dev/null +++ b/tests/components/spotify/conftest.py @@ -0,0 +1,128 @@ +"""Common test fixtures.""" + +from collections.abc import Generator +from typing import Any +from unittest.mock import MagicMock, patch + +import pytest + +from homeassistant.components.application_credentials import ( + ClientCredential, + async_import_client_credential, +) +from homeassistant.components.spotify import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_config_entry_1() -> MockConfigEntry: + """Mock a config entry with an upper case entry id.""" + return MockConfigEntry( + domain=DOMAIN, + title="spotify_1", + data={ + "auth_implementation": "spotify_c95e4090d4d3438b922331e7428f8171", + "token": { + "access_token": "AccessToken", + "token_type": "Bearer", + "expires_in": 3600, + "refresh_token": "RefreshToken", + "scope": "playlist-read-private ...", + "expires_at": 1724198975.8829377, + }, + "id": "32oesphrnacjcf7vw5bf6odx3oiu", + "name": "spotify_account_1", + }, + unique_id="84fce612f5b8", + entry_id="01J5TX5A0FF6G5V0QJX6HBC94T", + ) + + +@pytest.fixture +def mock_config_entry_2() -> MockConfigEntry: + """Mock a config entry with a lower case entry id.""" + return MockConfigEntry( + domain=DOMAIN, + title="spotify_2", + data={ + "auth_implementation": "spotify_c95e4090d4d3438b922331e7428f8171", + "token": { + "access_token": "AccessToken", + "token_type": "Bearer", + "expires_in": 3600, + "refresh_token": "RefreshToken", + "scope": "playlist-read-private ...", + "expires_at": 1724198975.8829377, + }, + "id": "55oesphrnacjcf7vw5bf6odx3oiu", + "name": "spotify_account_2", + }, + unique_id="99fce612f5b8", + entry_id="32oesphrnacjcf7vw5bf6odx3", + ) + + +@pytest.fixture +def spotify_playlists() -> dict[str, Any]: + """Mock the return from getting a list of playlists.""" + return { + "href": "https://api.spotify.com/v1/users/31oesphrnacjcf7vw5bf6odx3oiu/playlists?offset=0&limit=48", + "limit": 48, + "next": None, + "offset": 0, + "previous": None, + "total": 1, + "items": [ + { + "collaborative": False, + "description": "", + "id": "unique_identifier_00", + "name": "Playlist1", + "type": "playlist", + "uri": "spotify:playlist:unique_identifier_00", + } + ], + } + + +@pytest.fixture +def spotify_mock(spotify_playlists: dict[str, Any]) -> Generator[MagicMock]: + """Mock the Spotify API.""" + with patch("homeassistant.components.spotify.Spotify") as spotify_mock: + mock = MagicMock() + mock.current_user_playlists.return_value = spotify_playlists + spotify_mock.return_value = mock + yield spotify_mock + + +@pytest.fixture +async def spotify_setup( + hass: HomeAssistant, + spotify_mock: MagicMock, + mock_config_entry_1: MockConfigEntry, + mock_config_entry_2: MockConfigEntry, +): + """Set up the spotify integration.""" + with patch( + "homeassistant.components.spotify.OAuth2Session.async_ensure_token_valid" + ): + await async_setup_component(hass, "application_credentials", {}) + await hass.async_block_till_done() + await async_import_client_credential( + hass, + DOMAIN, + ClientCredential("CLIENT_ID", "CLIENT_SECRET"), + "spotify_c95e4090d4d3438b922331e7428f8171", + ) + await hass.async_block_till_done() + mock_config_entry_1.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry_1.entry_id) + mock_config_entry_2.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry_2.entry_id) + await hass.async_block_till_done(wait_background_tasks=True) + await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done(wait_background_tasks=True) + yield diff --git a/tests/components/spotify/snapshots/test_media_browser.ambr b/tests/components/spotify/snapshots/test_media_browser.ambr new file mode 100644 index 00000000000..4236fcb2e79 --- /dev/null +++ b/tests/components/spotify/snapshots/test_media_browser.ambr @@ -0,0 +1,236 @@ +# serializer version: 1 +# name: test_browse_media_categories + dict({ + 'can_expand': True, + 'can_play': False, + 'children': list([ + dict({ + 'can_expand': True, + 'can_play': False, + 'children_media_class': , + 'media_class': , + 'media_content_id': 'spotify://01j5tx5a0ff6g5v0qjx6hbc94t/current_user_playlists', + 'media_content_type': 'spotify://current_user_playlists', + 'thumbnail': None, + 'title': 'Playlists', + }), + dict({ + 'can_expand': True, + 'can_play': False, + 'children_media_class': , + 'media_class': , + 'media_content_id': 'spotify://01j5tx5a0ff6g5v0qjx6hbc94t/current_user_followed_artists', + 'media_content_type': 'spotify://current_user_followed_artists', + 'thumbnail': None, + 'title': 'Artists', + }), + dict({ + 'can_expand': True, + 'can_play': False, + 'children_media_class': , + 'media_class': , + 'media_content_id': 'spotify://01j5tx5a0ff6g5v0qjx6hbc94t/current_user_saved_albums', + 'media_content_type': 'spotify://current_user_saved_albums', + 'thumbnail': None, + 'title': 'Albums', + }), + dict({ + 'can_expand': True, + 'can_play': False, + 'children_media_class': , + 'media_class': , + 'media_content_id': 'spotify://01j5tx5a0ff6g5v0qjx6hbc94t/current_user_saved_tracks', + 'media_content_type': 'spotify://current_user_saved_tracks', + 'thumbnail': None, + 'title': 'Tracks', + }), + dict({ + 'can_expand': True, + 'can_play': False, + 'children_media_class': , + 'media_class': , + 'media_content_id': 'spotify://01j5tx5a0ff6g5v0qjx6hbc94t/current_user_saved_shows', + 'media_content_type': 'spotify://current_user_saved_shows', + 'thumbnail': None, + 'title': 'Podcasts', + }), + dict({ + 'can_expand': True, + 'can_play': False, + 'children_media_class': , + 'media_class': , + 'media_content_id': 'spotify://01j5tx5a0ff6g5v0qjx6hbc94t/current_user_recently_played', + 'media_content_type': 'spotify://current_user_recently_played', + 'thumbnail': None, + 'title': 'Recently played', + }), + dict({ + 'can_expand': True, + 'can_play': False, + 'children_media_class': , + 'media_class': , + 'media_content_id': 'spotify://01j5tx5a0ff6g5v0qjx6hbc94t/current_user_top_artists', + 'media_content_type': 'spotify://current_user_top_artists', + 'thumbnail': None, + 'title': 'Top Artists', + }), + dict({ + 'can_expand': True, + 'can_play': False, + 'children_media_class': , + 'media_class': , + 'media_content_id': 'spotify://01j5tx5a0ff6g5v0qjx6hbc94t/current_user_top_tracks', + 'media_content_type': 'spotify://current_user_top_tracks', + 'thumbnail': None, + 'title': 'Top Tracks', + }), + dict({ + 'can_expand': True, + 'can_play': False, + 'children_media_class': , + 'media_class': , + 'media_content_id': 'spotify://01j5tx5a0ff6g5v0qjx6hbc94t/categories', + 'media_content_type': 'spotify://categories', + 'thumbnail': None, + 'title': 'Categories', + }), + dict({ + 'can_expand': True, + 'can_play': False, + 'children_media_class': , + 'media_class': , + 'media_content_id': 'spotify://01j5tx5a0ff6g5v0qjx6hbc94t/featured_playlists', + 'media_content_type': 'spotify://featured_playlists', + 'thumbnail': None, + 'title': 'Featured Playlists', + }), + dict({ + 'can_expand': True, + 'can_play': False, + 'children_media_class': , + 'media_class': , + 'media_content_id': 'spotify://01j5tx5a0ff6g5v0qjx6hbc94t/new_releases', + 'media_content_type': 'spotify://new_releases', + 'thumbnail': None, + 'title': 'New Releases', + }), + ]), + 'children_media_class': , + 'media_class': , + 'media_content_id': 'spotify://01j5tx5a0ff6g5v0qjx6hbc94t/library', + 'media_content_type': 'spotify://library', + 'not_shown': 0, + 'thumbnail': None, + 'title': 'Media Library', + }) +# --- +# name: test_browse_media_playlists + dict({ + 'can_expand': True, + 'can_play': False, + 'children': list([ + dict({ + 'can_expand': True, + 'can_play': True, + 'children_media_class': , + 'media_class': , + 'media_content_id': 'spotify://01j5tx5a0ff6g5v0qjx6hbc94t/spotify:playlist:unique_identifier_00', + 'media_content_type': 'spotify://playlist', + 'thumbnail': None, + 'title': 'Playlist1', + }), + ]), + 'children_media_class': , + 'media_class': , + 'media_content_id': 'spotify://01j5tx5a0ff6g5v0qjx6hbc94t/current_user_playlists', + 'media_content_type': 'spotify://current_user_playlists', + 'not_shown': 0, + 'thumbnail': None, + 'title': 'Playlists', + }) +# --- +# name: test_browse_media_playlists[01J5TX5A0FF6G5V0QJX6HBC94T] + dict({ + 'can_expand': True, + 'can_play': False, + 'children': list([ + dict({ + 'can_expand': True, + 'can_play': True, + 'children_media_class': , + 'media_class': , + 'media_content_id': 'spotify://01j5tx5a0ff6g5v0qjx6hbc94t/spotify:playlist:unique_identifier_00', + 'media_content_type': 'spotify://playlist', + 'thumbnail': None, + 'title': 'Playlist1', + }), + ]), + 'children_media_class': , + 'media_class': , + 'media_content_id': 'spotify://01j5tx5a0ff6g5v0qjx6hbc94t/current_user_playlists', + 'media_content_type': 'spotify://current_user_playlists', + 'not_shown': 0, + 'thumbnail': None, + 'title': 'Playlists', + }) +# --- +# name: test_browse_media_playlists[32oesphrnacjcf7vw5bf6odx3] + dict({ + 'can_expand': True, + 'can_play': False, + 'children': list([ + dict({ + 'can_expand': True, + 'can_play': True, + 'children_media_class': , + 'media_class': , + 'media_content_id': 'spotify://32oesphrnacjcf7vw5bf6odx3/spotify:playlist:unique_identifier_00', + 'media_content_type': 'spotify://playlist', + 'thumbnail': None, + 'title': 'Playlist1', + }), + ]), + 'children_media_class': , + 'media_class': , + 'media_content_id': 'spotify://32oesphrnacjcf7vw5bf6odx3/current_user_playlists', + 'media_content_type': 'spotify://current_user_playlists', + 'not_shown': 0, + 'thumbnail': None, + 'title': 'Playlists', + }) +# --- +# name: test_browse_media_root + dict({ + 'can_expand': True, + 'can_play': False, + 'children': list([ + dict({ + 'can_expand': True, + 'can_play': False, + 'children_media_class': None, + 'media_class': , + 'media_content_id': 'spotify://01J5TX5A0FF6G5V0QJX6HBC94T', + 'media_content_type': 'spotify://library', + 'thumbnail': 'https://brands.home-assistant.io/_/spotify/logo.png', + 'title': 'spotify_1', + }), + dict({ + 'can_expand': True, + 'can_play': False, + 'children_media_class': None, + 'media_class': , + 'media_content_id': 'spotify://32oesphrnacjcf7vw5bf6odx3', + 'media_content_type': 'spotify://library', + 'thumbnail': 'https://brands.home-assistant.io/_/spotify/logo.png', + 'title': 'spotify_2', + }), + ]), + 'children_media_class': , + 'media_class': , + 'media_content_id': 'spotify://', + 'media_content_type': 'spotify', + 'not_shown': 0, + 'thumbnail': 'https://brands.home-assistant.io/_/spotify/logo.png', + 'title': 'Spotify', + }) +# --- diff --git a/tests/components/spotify/test_media_browser.py b/tests/components/spotify/test_media_browser.py new file mode 100644 index 00000000000..2b47aed9ee3 --- /dev/null +++ b/tests/components/spotify/test_media_browser.py @@ -0,0 +1,61 @@ +"""Test the media browser interface.""" + +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.components.spotify import DOMAIN +from homeassistant.components.spotify.browse_media import async_browse_media +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + + +async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: + """Fixture for setting up the component.""" + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done(wait_background_tasks=True) + await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done(wait_background_tasks=True) + + +async def test_browse_media_root( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + spotify_setup, +) -> None: + """Test browsing the root.""" + response = await async_browse_media(hass, None, None) + assert response.as_dict() == snapshot + + +async def test_browse_media_categories( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + spotify_setup, +) -> None: + """Test browsing categories.""" + response = await async_browse_media( + hass, "spotify://library", "spotify://01J5TX5A0FF6G5V0QJX6HBC94T" + ) + assert response.as_dict() == snapshot + + +@pytest.mark.parametrize( + ("config_entry_id"), [("01J5TX5A0FF6G5V0QJX6HBC94T"), ("32oesphrnacjcf7vw5bf6odx3")] +) +async def test_browse_media_playlists( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + config_entry_id: str, + spotify_setup, +) -> None: + """Test browsing playlists for the two config entries.""" + response = await async_browse_media( + hass, + "spotify://current_user_playlists", + f"spotify://{config_entry_id}/current_user_playlists", + ) + assert response.as_dict() == snapshot From 102528e5d3ccd057db27296f1c6e9671f9e69cfb Mon Sep 17 00:00:00 2001 From: Angel Nunez Mencias Date: Wed, 21 Aug 2024 21:14:03 +0200 Subject: [PATCH 095/271] update ttn_client - fix crash with SenseCAP devices (#124370) update ttn_client --- homeassistant/components/thethingsnetwork/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/thethingsnetwork/manifest.json b/homeassistant/components/thethingsnetwork/manifest.json index c39b2b7c421..8d826750e39 100644 --- a/homeassistant/components/thethingsnetwork/manifest.json +++ b/homeassistant/components/thethingsnetwork/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/thethingsnetwork", "integration_type": "hub", "iot_class": "cloud_polling", - "requirements": ["ttn_client==1.1.0"] + "requirements": ["ttn_client==1.2.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index cf9bff55916..74a0ad2d881 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2801,7 +2801,7 @@ transmission-rpc==7.0.3 ttls==1.8.3 # homeassistant.components.thethingsnetwork -ttn_client==1.1.0 +ttn_client==1.2.0 # homeassistant.components.tuya tuya-device-sharing-sdk==0.1.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 992a19e41e4..da6b9d92a78 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2199,7 +2199,7 @@ transmission-rpc==7.0.3 ttls==1.8.3 # homeassistant.components.thethingsnetwork -ttn_client==1.1.0 +ttn_client==1.2.0 # homeassistant.components.tuya tuya-device-sharing-sdk==0.1.9 From 03c7f2cf5b4005103666e0cb2de0e9024a01fef9 Mon Sep 17 00:00:00 2001 From: Penny Wood Date: Thu, 22 Aug 2024 21:39:09 +0800 Subject: [PATCH 096/271] Add supported features for iZone (#124416) * Fix for #123462 * Set outside of constructor --- homeassistant/components/izone/climate.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/izone/climate.py b/homeassistant/components/izone/climate.py index 3a1279a9bd4..617cdc730cc 100644 --- a/homeassistant/components/izone/climate.py +++ b/homeassistant/components/izone/climate.py @@ -441,6 +441,9 @@ class ZoneDevice(ClimateEntity): _attr_name = None _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_target_temperature_step = 0.5 + _attr_supported_features = ( + ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON + ) def __init__(self, controller: ControllerDevice, zone: Zone) -> None: """Initialise ZoneDevice.""" From a128e2e4fce43db67e26f432dbb159ec6fd2378f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 22 Aug 2024 14:46:54 -0500 Subject: [PATCH 097/271] Bump yalexs to 8.1.4 (#124425) changelog: https://github.com/bdraco/yalexs/compare/v8.1.2...v8.1.4 --- homeassistant/components/august/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index 5d7b253e952..7e73e55e6ba 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -28,5 +28,5 @@ "documentation": "https://www.home-assistant.io/integrations/august", "iot_class": "cloud_push", "loggers": ["pubnub", "yalexs"], - "requirements": ["yalexs==8.1.2", "yalexs-ble==2.4.3"] + "requirements": ["yalexs==8.1.4", "yalexs-ble==2.4.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 74a0ad2d881..504f81e032b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2959,7 +2959,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.4.3 # homeassistant.components.august -yalexs==8.1.2 +yalexs==8.1.4 # homeassistant.components.yeelight yeelight==0.7.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index da6b9d92a78..39db339241e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2336,7 +2336,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.4.3 # homeassistant.components.august -yalexs==8.1.2 +yalexs==8.1.4 # homeassistant.components.yeelight yeelight==0.7.14 From fa914b2811f91c841c17d2d4e4742d280528ac97 Mon Sep 17 00:00:00 2001 From: Ino Dekker Date: Fri, 23 Aug 2024 13:43:17 +0200 Subject: [PATCH 098/271] Bump aiohue to version 4.7.3 (#124436) --- homeassistant/components/hue/manifest.json | 2 +- homeassistant/components/hue/v2/hue_event.py | 6 +++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/hue/fixtures/v2_resources.json | 12 +++++++++--- 5 files changed, 15 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index 71aabd4c204..dbd9b511977 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -11,6 +11,6 @@ "iot_class": "local_push", "loggers": ["aiohue"], "quality_scale": "platinum", - "requirements": ["aiohue==4.7.2"], + "requirements": ["aiohue==4.7.3"], "zeroconf": ["_hue._tcp.local."] } diff --git a/homeassistant/components/hue/v2/hue_event.py b/homeassistant/components/hue/v2/hue_event.py index b286a11aade..2eace5139af 100644 --- a/homeassistant/components/hue/v2/hue_event.py +++ b/homeassistant/components/hue/v2/hue_event.py @@ -80,9 +80,9 @@ async def async_setup_hue_events(bridge: HueBridge): CONF_DEVICE_ID: device.id, # type: ignore[union-attr] CONF_UNIQUE_ID: hue_resource.id, CONF_TYPE: hue_resource.relative_rotary.rotary_report.action.value, - CONF_SUBTYPE: hue_resource.relative_rotary.last_event.rotation.direction.value, - CONF_DURATION: hue_resource.relative_rotary.last_event.rotation.duration, - CONF_STEPS: hue_resource.relative_rotary.last_event.rotation.steps, + CONF_SUBTYPE: hue_resource.relative_rotary.rotary_report.rotation.direction.value, + CONF_DURATION: hue_resource.relative_rotary.rotary_report.rotation.duration, + CONF_STEPS: hue_resource.relative_rotary.rotary_report.rotation.steps, } hass.bus.async_fire(ATTR_HUE_EVENT, data) diff --git a/requirements_all.txt b/requirements_all.txt index 504f81e032b..011601ba3e6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -258,7 +258,7 @@ aioharmony==0.2.10 aiohomekit==3.2.3 # homeassistant.components.hue -aiohue==4.7.2 +aiohue==4.7.3 # homeassistant.components.imap aioimaplib==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 39db339241e..6c272c5e9e0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -243,7 +243,7 @@ aioharmony==0.2.10 aiohomekit==3.2.3 # homeassistant.components.hue -aiohue==4.7.2 +aiohue==4.7.3 # homeassistant.components.imap aioimaplib==1.1.0 diff --git a/tests/components/hue/fixtures/v2_resources.json b/tests/components/hue/fixtures/v2_resources.json index 980086d0988..3d718f24c50 100644 --- a/tests/components/hue/fixtures/v2_resources.json +++ b/tests/components/hue/fixtures/v2_resources.json @@ -1288,7 +1288,9 @@ }, { "button": { - "last_event": "short_release" + "button_report": { + "event": "short_release" + } }, "id": "c658d3d8-a013-4b81-8ac6-78b248537e70", "id_v1": "/sensors/50", @@ -1327,7 +1329,9 @@ }, { "button": { - "last_event": "short_release" + "button_report": { + "event": "short_release" + } }, "id": "7f1ab9f6-cc2b-4b40-9011-65e2af153f75", "id_v1": "/sensors/10", @@ -1366,7 +1370,9 @@ }, { "button": { - "last_event": "short_release" + "button_report": { + "event": "short_release" + } }, "id": "31cffcda-efc2-401f-a152-e10db3eed232", "id_v1": "/sensors/5", From 5f275a6b9c9adf1d2916483870e63f06a98d9292 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 24 Aug 2024 07:04:50 +0200 Subject: [PATCH 099/271] Don't raise WLED user flow unique_id check (#124481) --- homeassistant/components/wled/config_flow.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wled/config_flow.py b/homeassistant/components/wled/config_flow.py index 7853ad2101e..2798e0d46d1 100644 --- a/homeassistant/components/wled/config_flow.py +++ b/homeassistant/components/wled/config_flow.py @@ -46,7 +46,9 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN): except WLEDConnectionError: errors["base"] = "cannot_connect" else: - await self.async_set_unique_id(device.info.mac_address) + await self.async_set_unique_id( + device.info.mac_address, raise_on_progress=False + ) self._abort_if_unique_id_configured( updates={CONF_HOST: user_input[CONF_HOST]} ) @@ -56,8 +58,6 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN): CONF_HOST: user_input[CONF_HOST], }, ) - else: - user_input = {} return self.async_show_form( step_id="user", From 2db362ab3df3144337c744d7772ba2ae8ba59af1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 23 Aug 2024 12:23:05 -0500 Subject: [PATCH 100/271] Bump yalexs to 8.3.3 (#124492) * Bump yalexs to 8.2.0 changelog: https://github.com/bdraco/yalexs/compare/v8.1.4...v8.2.0 * bump to 8.3.1 * bump * one more bump to ensure we do not hit the ratelimit/shutdown cleanly * empty commit to restart ci since close/open did not work in flight --- homeassistant/components/august/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/august/conftest.py | 8 ++++++++ 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index 7e73e55e6ba..49c23dac660 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -28,5 +28,5 @@ "documentation": "https://www.home-assistant.io/integrations/august", "iot_class": "cloud_push", "loggers": ["pubnub", "yalexs"], - "requirements": ["yalexs==8.1.4", "yalexs-ble==2.4.3"] + "requirements": ["yalexs==8.3.3", "yalexs-ble==2.4.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 011601ba3e6..b1a919a0c9b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2959,7 +2959,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.4.3 # homeassistant.components.august -yalexs==8.1.4 +yalexs==8.3.3 # homeassistant.components.yeelight yeelight==0.7.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6c272c5e9e0..383bc52ae1e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2336,7 +2336,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.4.3 # homeassistant.components.august -yalexs==8.1.4 +yalexs==8.3.3 # homeassistant.components.yeelight yeelight==0.7.14 diff --git a/tests/components/august/conftest.py b/tests/components/august/conftest.py index 052cde7d2a2..78cb2cdad89 100644 --- a/tests/components/august/conftest.py +++ b/tests/components/august/conftest.py @@ -3,6 +3,7 @@ from unittest.mock import patch import pytest +from yalexs.manager.ratelimit import _RateLimitChecker @pytest.fixture(name="mock_discovery", autouse=True) @@ -12,3 +13,10 @@ def mock_discovery_fixture(): "homeassistant.components.august.data.discovery_flow.async_create_flow" ) as mock_discovery: yield mock_discovery + + +@pytest.fixture(name="disable_ratelimit_checks", autouse=True) +def disable_ratelimit_checks_fixture(): + """Disable rate limit checks.""" + with patch.object(_RateLimitChecker, "register_wakeup"): + yield From b294a92ad2d463e003a7df06c796575b953242ec Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 24 Aug 2024 01:44:12 -0500 Subject: [PATCH 101/271] Bump yalexs to 8.4.0 (#124520) --- homeassistant/components/august/config_flow.py | 6 +++--- homeassistant/components/august/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/august/config_flow.py b/homeassistant/components/august/config_flow.py index 3523a4f7c39..2a1a20a9dc4 100644 --- a/homeassistant/components/august/config_flow.py +++ b/homeassistant/components/august/config_flow.py @@ -9,7 +9,7 @@ from typing import Any import aiohttp import voluptuous as vol from yalexs.authenticator_common import ValidationResult -from yalexs.const import BRANDS, DEFAULT_BRAND +from yalexs.const import BRANDS_WITHOUT_OAUTH, DEFAULT_BRAND from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation from homeassistant.config_entries import ConfigFlow, ConfigFlowResult @@ -118,7 +118,7 @@ class AugustConfigFlow(ConfigFlow, domain=DOMAIN): vol.Required( CONF_BRAND, default=self._user_auth_details.get(CONF_BRAND, DEFAULT_BRAND), - ): vol.In(BRANDS), + ): vol.In(BRANDS_WITHOUT_OAUTH), vol.Required( CONF_LOGIN_METHOD, default=self._user_auth_details.get( @@ -208,7 +208,7 @@ class AugustConfigFlow(ConfigFlow, domain=DOMAIN): vol.Required( CONF_BRAND, default=self._user_auth_details.get(CONF_BRAND, DEFAULT_BRAND), - ): vol.In(BRANDS), + ): vol.In(BRANDS_WITHOUT_OAUTH), vol.Required(CONF_PASSWORD): str, } ), diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index 49c23dac660..abe9dc707c3 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -28,5 +28,5 @@ "documentation": "https://www.home-assistant.io/integrations/august", "iot_class": "cloud_push", "loggers": ["pubnub", "yalexs"], - "requirements": ["yalexs==8.3.3", "yalexs-ble==2.4.3"] + "requirements": ["yalexs==8.4.0", "yalexs-ble==2.4.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index b1a919a0c9b..d6f7c866360 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2959,7 +2959,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.4.3 # homeassistant.components.august -yalexs==8.3.3 +yalexs==8.4.0 # homeassistant.components.yeelight yeelight==0.7.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 383bc52ae1e..f035cebac78 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2336,7 +2336,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.4.3 # homeassistant.components.august -yalexs==8.3.3 +yalexs==8.4.0 # homeassistant.components.yeelight yeelight==0.7.14 From 1bdf9d657e1183a67be4ade412f5164bd3cfd353 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 25 Aug 2024 01:16:20 -1000 Subject: [PATCH 102/271] Bump yalexs to 8.4.1 (#124553) changelog: https://github.com/bdraco/yalexs/compare/v8.4.0...v8.4.1 --- homeassistant/components/august/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index abe9dc707c3..e0739aadff0 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -28,5 +28,5 @@ "documentation": "https://www.home-assistant.io/integrations/august", "iot_class": "cloud_push", "loggers": ["pubnub", "yalexs"], - "requirements": ["yalexs==8.4.0", "yalexs-ble==2.4.3"] + "requirements": ["yalexs==8.4.1", "yalexs-ble==2.4.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index d6f7c866360..48fed02cd53 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2959,7 +2959,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.4.3 # homeassistant.components.august -yalexs==8.4.0 +yalexs==8.4.1 # homeassistant.components.yeelight yeelight==0.7.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f035cebac78..5df5a0836c7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2336,7 +2336,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.4.3 # homeassistant.components.august -yalexs==8.4.0 +yalexs==8.4.1 # homeassistant.components.yeelight yeelight==0.7.14 From a45c1a39148c4f5e949bf8de5026c32b27db60ab Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Sun, 25 Aug 2024 15:15:47 +0200 Subject: [PATCH 103/271] Fix missing id in Habitica completed todos API response (#124565) * Fix missing id in completed todos API response * Copy id only if none * Update homeassistant/components/habitica/coordinator.py Co-authored-by: Paulus Schoutsen --------- Co-authored-by: Paulus Schoutsen --- .../components/habitica/coordinator.py | 9 +++++- tests/components/habitica/test_init.py | 28 +++++++++---------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/habitica/coordinator.py b/homeassistant/components/habitica/coordinator.py index 9d0ebe651e3..1b17eee6352 100644 --- a/homeassistant/components/habitica/coordinator.py +++ b/homeassistant/components/habitica/coordinator.py @@ -49,7 +49,14 @@ class HabiticaDataUpdateCoordinator(DataUpdateCoordinator[HabiticaData]): try: user_response = await self.api.user.get() tasks_response = await self.api.tasks.user.get() - tasks_response.extend(await self.api.tasks.user.get(type="completedTodos")) + tasks_response.extend( + [ + {"id": task["_id"], **task} + for task in await self.api.tasks.user.get(type="completedTodos") + if task.get("_id") + ] + ) + except ClientResponseError as error: raise UpdateFailed(f"Error communicating with API: {error}") from error diff --git a/tests/components/habitica/test_init.py b/tests/components/habitica/test_init.py index 31c3a1fae39..4c2b1e2aae6 100644 --- a/tests/components/habitica/test_init.py +++ b/tests/components/habitica/test_init.py @@ -73,7 +73,20 @@ def common_requests(aioclient_mock: AiohttpClientMocker) -> AiohttpClientMocker: } }, ) - + aioclient_mock.get( + "https://habitica.com/api/v3/tasks/user?type=completedTodos", + json={ + "data": [ + { + "text": "this is a mock todo #5", + "id": 5, + "_id": 5, + "type": "todo", + "completed": True, + } + ] + }, + ) aioclient_mock.get( "https://habitica.com/api/v3/tasks/user", json={ @@ -88,19 +101,6 @@ def common_requests(aioclient_mock: AiohttpClientMocker) -> AiohttpClientMocker: ] }, ) - aioclient_mock.get( - "https://habitica.com/api/v3/tasks/user?type=completedTodos", - json={ - "data": [ - { - "text": "this is a mock todo #5", - "id": 5, - "type": "todo", - "completed": True, - } - ] - }, - ) aioclient_mock.post( "https://habitica.com/api/v3/tasks/user", From b34c90b1893a582b950c58ead23cfa96eeac01a0 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sun, 25 Aug 2024 15:09:08 +0200 Subject: [PATCH 104/271] Only support remote activity on Alexa if feature is set and at least one feature is in the activity_list (#124567) Only support remote activity on Alexa if feaure is set and at least one feature is in the activity_list --- homeassistant/components/alexa/entities.py | 9 +++++--- tests/components/alexa/test_capabilities.py | 24 +++++++++++++++++---- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 8bba4ed2468..ca7b389a0f1 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -661,9 +661,12 @@ class RemoteCapabilities(AlexaEntity): def interfaces(self) -> Generator[AlexaCapability]: """Yield the supported interfaces.""" yield AlexaPowerController(self.entity) - yield AlexaModeController( - self.entity, instance=f"{remote.DOMAIN}.{remote.ATTR_ACTIVITY}" - ) + supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + activities = self.entity.attributes.get(remote.ATTR_ACTIVITY_LIST) or [] + if activities and supported & remote.RemoteEntityFeature.ACTIVITY: + yield AlexaModeController( + self.entity, instance=f"{remote.DOMAIN}.{remote.ATTR_ACTIVITY}" + ) yield AlexaEndpointHealth(self.hass, self.entity) yield Alexa(self.entity) diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index 162149f095b..b56d8054d7b 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -70,6 +70,7 @@ async def test_discovery_remote( { "current_activity": current_activity, "activity_list": activity_list, + "supported_features": 4, }, ) msg = await smart_home.async_handle_message(hass, get_default_config(hass), request) @@ -790,22 +791,37 @@ async def test_report_remote_activity(hass: HomeAssistant) -> None: hass.states.async_set( "remote.unknown", "on", - {"current_activity": "UNKNOWN"}, + { + "current_activity": "UNKNOWN", + "supported_features": 4, + }, ) hass.states.async_set( "remote.tv", "on", - {"current_activity": "TV", "activity_list": ["TV", "MUSIC", "DVD"]}, + { + "current_activity": "TV", + "activity_list": ["TV", "MUSIC", "DVD"], + "supported_features": 4, + }, ) hass.states.async_set( "remote.music", "on", - {"current_activity": "MUSIC", "activity_list": ["TV", "MUSIC", "DVD"]}, + { + "current_activity": "MUSIC", + "activity_list": ["TV", "MUSIC", "DVD"], + "supported_features": 4, + }, ) hass.states.async_set( "remote.dvd", "on", - {"current_activity": "DVD", "activity_list": ["TV", "MUSIC", "DVD"]}, + { + "current_activity": "DVD", + "activity_list": ["TV", "MUSIC", "DVD"], + "supported_features": 4, + }, ) properties = await reported_properties(hass, "remote#unknown") From 18efd84a357aa806f4f82e710ac0d89d6df08bb3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 25 Aug 2024 13:26:00 +0000 Subject: [PATCH 105/271] Bump version to 2024.8.3 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 39df0486e06..2a06c24843a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "2" +PATCH_VERSION: Final = "3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index c2b167bbbd7..437aea9f097 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.8.2" +version = "2024.8.3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 2d5289e7dd2c1cde5c4af6052a0880983d7cacc4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 17 Aug 2024 10:10:45 -0500 Subject: [PATCH 106/271] Revert "Exclude aiohappyeyeballs from license check" (#124116) --- script/licenses.py | 1 - 1 file changed, 1 deletion(-) diff --git a/script/licenses.py b/script/licenses.py index 659f8cb8dcc..9c584e7f4fc 100644 --- a/script/licenses.py +++ b/script/licenses.py @@ -124,7 +124,6 @@ EXCEPTIONS = { "PyXiaomiGateway", # https://github.com/Danielhiversen/PyXiaomiGateway/pull/201 "aiocomelit", # https://github.com/chemelli74/aiocomelit/pull/138 "aioecowitt", # https://github.com/home-assistant-libs/aioecowitt/pull/180 - "aiohappyeyeballs", # Python-2.0.1 "aioopenexchangerates", # https://github.com/MartinHjelmare/aioopenexchangerates/pull/94 "aiooui", # https://github.com/Bluetooth-Devices/aiooui/pull/8 "aioruuvigateway", # https://github.com/akx/aioruuvigateway/pull/6 From 2856525c12a3c396ea3da3617886efcbd3155b5a Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 28 Aug 2024 16:40:52 +0000 Subject: [PATCH 107/271] Bump version to 2024.9.0b0 --- homeassistant/const.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 8384a6d44bd..a74ea6f7ebe 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "0.dev0" +PATCH_VERSION: Final = "0b0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index b4d3bf46916..d50cf2f9cd4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.9.0.dev0" +version = "2024.9.0b0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" @@ -801,7 +801,7 @@ ignore = [ "SIM103", # Return the condition {condition} directly "SIM108", # Use ternary operator {contents} instead of if-else-block "SIM115", # Use context handler for opening files - + # Moving imports into type-checking blocks can mess with pytest.patch() "TCH001", # Move application import {} into a type-checking block "TCH002", # Move third-party import {} into a type-checking block From 3b214f6610431ee37059175d1c87bb26890a4396 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 29 Aug 2024 07:59:07 +0200 Subject: [PATCH 108/271] Bump pyatmo to 8.1.0 (#124340) --- homeassistant/components/netatmo/manifest.json | 2 +- homeassistant/components/netatmo/select.py | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../netatmo/snapshots/test_sensor.ambr | 18 +++++++++--------- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index 98734bcb742..0a32777b527 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -12,5 +12,5 @@ "integration_type": "hub", "iot_class": "cloud_polling", "loggers": ["pyatmo"], - "requirements": ["pyatmo==8.0.3"] + "requirements": ["pyatmo==8.1.0"] } diff --git a/homeassistant/components/netatmo/select.py b/homeassistant/components/netatmo/select.py index 3fe098a75a9..92568b73e80 100644 --- a/homeassistant/components/netatmo/select.py +++ b/homeassistant/components/netatmo/select.py @@ -72,7 +72,7 @@ class NetatmoScheduleSelect(NetatmoBaseEntity, SelectEntity): self._attr_current_option = getattr(self.home.get_selected_schedule(), "name") self._attr_options = [ - schedule.name for schedule in self.home.schedules.values() + schedule.name for schedule in self.home.schedules.values() if schedule.name ] async def async_added_to_hass(self) -> None: @@ -128,5 +128,5 @@ class NetatmoScheduleSelect(NetatmoBaseEntity, SelectEntity): self.home.schedules ) self._attr_options = [ - schedule.name for schedule in self.home.schedules.values() + schedule.name for schedule in self.home.schedules.values() if schedule.name ] diff --git a/requirements_all.txt b/requirements_all.txt index 1660c94ac11..e833c8d3fe7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1741,7 +1741,7 @@ pyasuswrt==0.1.21 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==8.0.3 +pyatmo==8.1.0 # homeassistant.components.apple_tv pyatv==0.15.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e4cda97ea71..1aaebb735ee 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1409,7 +1409,7 @@ pyasuswrt==0.1.21 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==8.0.3 +pyatmo==8.1.0 # homeassistant.components.apple_tv pyatv==0.15.0 diff --git a/tests/components/netatmo/snapshots/test_sensor.ambr b/tests/components/netatmo/snapshots/test_sensor.ambr index bc2a18d918d..0d13a88cd67 100644 --- a/tests/components/netatmo/snapshots/test_sensor.ambr +++ b/tests/components/netatmo/snapshots/test_sensor.ambr @@ -1159,7 +1159,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'True', }) # --- # name: test_entity[sensor.cold_water_power-entry] @@ -1508,7 +1508,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'True', }) # --- # name: test_entity[sensor.gas_power-entry] @@ -3257,7 +3257,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'True', }) # --- # name: test_entity[sensor.hot_water_power-entry] @@ -3896,7 +3896,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'True', }) # --- # name: test_entity[sensor.line_1_power-entry] @@ -3995,7 +3995,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'True', }) # --- # name: test_entity[sensor.line_2_power-entry] @@ -4094,7 +4094,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'True', }) # --- # name: test_entity[sensor.line_3_power-entry] @@ -4193,7 +4193,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'True', }) # --- # name: test_entity[sensor.line_4_power-entry] @@ -4292,7 +4292,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'True', }) # --- # name: test_entity[sensor.line_5_power-entry] @@ -5622,7 +5622,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'True', }) # --- # name: test_entity[sensor.total_power-entry] From 66480da21889a7f7bfbb2f4860ae470eab6af1c2 Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Wed, 28 Aug 2024 19:19:04 +0200 Subject: [PATCH 109/271] Bump pydaikin to 2.13.5 (#124802) bump pydaikin version --- homeassistant/components/daikin/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/daikin/manifest.json b/homeassistant/components/daikin/manifest.json index 0d93c0e25ad..c395ee35cad 100644 --- a/homeassistant/components/daikin/manifest.json +++ b/homeassistant/components/daikin/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/daikin", "iot_class": "local_polling", "loggers": ["pydaikin"], - "requirements": ["pydaikin==2.13.4"], + "requirements": ["pydaikin==2.13.5"], "zeroconf": ["_dkapi._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index e833c8d3fe7..cf438e721bd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1798,7 +1798,7 @@ pycsspeechtts==1.0.8 # pycups==1.9.73 # homeassistant.components.daikin -pydaikin==2.13.4 +pydaikin==2.13.5 # homeassistant.components.danfoss_air pydanfossair==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1aaebb735ee..bc6416948d3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1445,7 +1445,7 @@ pycoolmasternet-async==0.2.2 pycsspeechtts==1.0.8 # homeassistant.components.daikin -pydaikin==2.13.4 +pydaikin==2.13.5 # homeassistant.components.deconz pydeconz==116 From aa72b08c16adb62c9668bde4e1f58bbe207a8e75 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 28 Aug 2024 09:00:52 -1000 Subject: [PATCH 110/271] Address yale review comments (#124810) Co-authored-by: Joost Lekkerkerker --- homeassistant/components/yale/__init__.py | 2 +- .../components/yale/binary_sensor.py | 11 +- homeassistant/components/yale/button.py | 4 +- homeassistant/components/yale/camera.py | 4 +- homeassistant/components/yale/config_flow.py | 5 +- homeassistant/components/yale/const.py | 4 - homeassistant/components/yale/diagnostics.py | 5 +- homeassistant/components/yale/entity.py | 4 +- homeassistant/components/yale/event.py | 28 +- homeassistant/components/yale/lock.py | 4 +- homeassistant/components/yale/manifest.json | 2 +- homeassistant/components/yale/sensor.py | 21 +- homeassistant/components/yale/util.py | 7 +- tests/components/yale/__init__.py | 11 - .../yale/snapshots/test_binary_sensor.ambr | 33 +++ .../yale/snapshots/test_diagnostics.ambr | 2 +- tests/components/yale/test_binary_sensor.py | 252 ++++++------------ tests/components/yale/test_config_flow.py | 76 +++++- tests/components/yale/test_event.py | 44 ++- tests/components/yale/test_init.py | 31 ++- 20 files changed, 267 insertions(+), 283 deletions(-) create mode 100644 tests/components/yale/snapshots/test_binary_sensor.ambr diff --git a/homeassistant/components/yale/__init__.py b/homeassistant/components/yale/__init__.py index f7a4a6e0f4d..1cbd9c87b57 100644 --- a/homeassistant/components/yale/__init__.py +++ b/homeassistant/components/yale/__init__.py @@ -26,7 +26,7 @@ from .util import async_create_yale_clientsession type YaleConfigEntry = ConfigEntry[YaleData] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: YaleConfigEntry) -> bool: """Set up yale from a config entry.""" session = async_create_yale_clientsession(hass) implementation = ( diff --git a/homeassistant/components/yale/binary_sensor.py b/homeassistant/components/yale/binary_sensor.py index cbc0b48b177..dbb00ad7d42 100644 --- a/homeassistant/components/yale/binary_sensor.py +++ b/homeassistant/components/yale/binary_sensor.py @@ -109,12 +109,11 @@ async def async_setup_entry( for description in SENSOR_TYPES_DOORBELL ) - for doorbell in data.doorbells: - entities.extend( - YaleDoorbellBinarySensor(data, doorbell, description) - for description in SENSOR_TYPES_DOORBELL + SENSOR_TYPES_VIDEO_DOORBELL - ) - + entities.extend( + YaleDoorbellBinarySensor(data, doorbell, description) + for description in SENSOR_TYPES_DOORBELL + SENSOR_TYPES_VIDEO_DOORBELL + for doorbell in data.doorbells + ) async_add_entities(entities) diff --git a/homeassistant/components/yale/button.py b/homeassistant/components/yale/button.py index de0cff4f0c8..b04ad638f0c 100644 --- a/homeassistant/components/yale/button.py +++ b/homeassistant/components/yale/button.py @@ -5,7 +5,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import YaleConfigEntry -from .entity import YaleEntityMixin +from .entity import YaleEntity async def async_setup_entry( @@ -18,7 +18,7 @@ async def async_setup_entry( async_add_entities(YaleWakeLockButton(data, lock, "wake") for lock in data.locks) -class YaleWakeLockButton(YaleEntityMixin, ButtonEntity): +class YaleWakeLockButton(YaleEntity, ButtonEntity): """Representation of an Yale lock wake button.""" _attr_translation_key = "wake" diff --git a/homeassistant/components/yale/camera.py b/homeassistant/components/yale/camera.py index 500239d7f3a..217e8f5f6fd 100644 --- a/homeassistant/components/yale/camera.py +++ b/homeassistant/components/yale/camera.py @@ -16,7 +16,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import YaleConfigEntry, YaleData from .const import DEFAULT_NAME, DEFAULT_TIMEOUT -from .entity import YaleEntityMixin +from .entity import YaleEntity _LOGGER = logging.getLogger(__name__) @@ -38,7 +38,7 @@ async def async_setup_entry( ) -class YaleCamera(YaleEntityMixin, Camera): +class YaleCamera(YaleEntity, Camera): """An implementation of an Yale security camera.""" _attr_translation_key = "camera" diff --git a/homeassistant/components/yale/config_flow.py b/homeassistant/components/yale/config_flow.py index cdd44754103..6cbc9543ea4 100644 --- a/homeassistant/components/yale/config_flow.py +++ b/homeassistant/components/yale/config_flow.py @@ -26,7 +26,9 @@ class YaleConfigFlow(config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain= """Return logger.""" return _LOGGER - async def async_step_reauth(self, data: Mapping[str, Any]) -> ConfigFlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle configuration by re-auth.""" self.reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -54,4 +56,5 @@ class YaleConfigFlow(config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain= return self.async_abort(reason="reauth_invalid_user") return self.async_update_reload_and_abort(entry, data=data) await self.async_set_unique_id(user_id) + self._abort_if_unique_id_configured() return await super().async_oauth_create_entry(data) diff --git a/homeassistant/components/yale/const.py b/homeassistant/components/yale/const.py index 630d15f7230..3da4fb1dfb4 100644 --- a/homeassistant/components/yale/const.py +++ b/homeassistant/components/yale/const.py @@ -1,7 +1,5 @@ """Constants for Yale devices.""" -from yalexs.const import Brand - from homeassistant.const import Platform DEFAULT_TIMEOUT = 25 @@ -13,8 +11,6 @@ CONF_INSTALL_ID = "install_id" VERIFICATION_CODE_KEY = "verification_code" -DEFAULT_BRAND = Brand.YALE_HOME - MANUFACTURER = "Yale Home Inc." DEFAULT_NAME = "Yale" diff --git a/homeassistant/components/yale/diagnostics.py b/homeassistant/components/yale/diagnostics.py index ef8d837b82e..7e7f6179e7a 100644 --- a/homeassistant/components/yale/diagnostics.py +++ b/homeassistant/components/yale/diagnostics.py @@ -4,11 +4,12 @@ from __future__ import annotations from typing import Any +from yalexs.const import Brand + from homeassistant.components.diagnostics import async_redact_data from homeassistant.core import HomeAssistant from . import YaleConfigEntry -from .const import CONF_BRAND, DEFAULT_BRAND TO_REDACT = { "HouseID", @@ -45,5 +46,5 @@ async def async_get_config_entry_diagnostics( ) for doorbell in data.doorbells }, - "brand": entry.data.get(CONF_BRAND, DEFAULT_BRAND), + "brand": Brand.YALE_GLOBAL.value, } diff --git a/homeassistant/components/yale/entity.py b/homeassistant/components/yale/entity.py index 7105fda861c..152070c0be3 100644 --- a/homeassistant/components/yale/entity.py +++ b/homeassistant/components/yale/entity.py @@ -20,7 +20,7 @@ from .const import MANUFACTURER DEVICE_TYPES = ["keypad", "lock", "camera", "doorbell", "door", "bell"] -class YaleEntityMixin(Entity): +class YaleEntity(Entity): """Base implementation for Yale device.""" _attr_should_poll = False @@ -87,7 +87,7 @@ class YaleEntityMixin(Entity): self._update_from_data() -class YaleDescriptionEntity(YaleEntityMixin): +class YaleDescriptionEntity(YaleEntity): """An Yale entity with a description.""" def __init__( diff --git a/homeassistant/components/yale/event.py b/homeassistant/components/yale/event.py index 7014c5dafbf..935ba7376f8 100644 --- a/homeassistant/components/yale/event.py +++ b/homeassistant/components/yale/event.py @@ -63,22 +63,17 @@ async def async_setup_entry( ) -> None: """Set up the yale event platform.""" data = config_entry.runtime_data - entities: list[YaleEventEntity] = [] - - for lock in data.locks: - detail = data.get_device_detail(lock.device_id) - if detail.doorbell: - entities.extend( - YaleEventEntity(data, lock, description) - for description in TYPES_DOORBELL - ) - - for doorbell in data.doorbells: - entities.extend( - YaleEventEntity(data, doorbell, description) - for description in TYPES_DOORBELL + TYPES_VIDEO_DOORBELL - ) - + entities: list[YaleEventEntity] = [ + YaleEventEntity(data, lock, description) + for description in TYPES_DOORBELL + for lock in data.locks + if (detail := data.get_device_detail(lock.device_id)) and detail.doorbell + ] + entities.extend( + YaleEventEntity(data, doorbell, description) + for description in TYPES_DOORBELL + TYPES_VIDEO_DOORBELL + for doorbell in data.doorbells + ) async_add_entities(entities) @@ -86,7 +81,6 @@ class YaleEventEntity(YaleDescriptionEntity, EventEntity): """An yale event entity.""" entity_description: YaleEventEntityDescription - _attr_has_entity_name = True _last_activity: Activity | None = None @callback diff --git a/homeassistant/components/yale/lock.py b/homeassistant/components/yale/lock.py index 36d865bf527..b911c92ba0f 100644 --- a/homeassistant/components/yale/lock.py +++ b/homeassistant/components/yale/lock.py @@ -19,7 +19,7 @@ from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.util.dt as dt_util from . import YaleConfigEntry, YaleData -from .entity import YaleEntityMixin +from .entity import YaleEntity _LOGGER = logging.getLogger(__name__) @@ -36,7 +36,7 @@ async def async_setup_entry( async_add_entities(YaleLock(data, lock) for lock in data.locks) -class YaleLock(YaleEntityMixin, RestoreEntity, LockEntity): +class YaleLock(YaleEntity, RestoreEntity, LockEntity): """Representation of an Yale lock.""" _attr_name = None diff --git a/homeassistant/components/yale/manifest.json b/homeassistant/components/yale/manifest.json index 2dc84758610..d6da9ba3993 100644 --- a/homeassistant/components/yale/manifest.json +++ b/homeassistant/components/yale/manifest.json @@ -11,6 +11,6 @@ ], "documentation": "https://www.home-assistant.io/integrations/yale", "iot_class": "cloud_push", - "loggers": ["pubnub", "yalexs"], + "loggers": ["socketio", "engineio", "yalexs"], "requirements": ["yalexs==8.5.4", "yalexs-ble==2.4.3"] } diff --git a/homeassistant/components/yale/sensor.py b/homeassistant/components/yale/sensor.py index f1931c112cb..bb3d4317277 100644 --- a/homeassistant/components/yale/sensor.py +++ b/homeassistant/components/yale/sensor.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from typing import Any, Generic, TypeVar, cast +from typing import Any, cast from yalexs.activity import ActivityType, LockOperationActivity from yalexs.doorbell import Doorbell @@ -42,7 +42,7 @@ from .const import ( OPERATION_METHOD_REMOTE, OPERATION_METHOD_TAG, ) -from .entity import YaleDescriptionEntity, YaleEntityMixin +from .entity import YaleDescriptionEntity, YaleEntity def _retrieve_device_battery_state(detail: LockDetail) -> int: @@ -55,14 +55,13 @@ def _retrieve_linked_keypad_battery_state(detail: KeypadDetail) -> int | None: return detail.battery_percentage -_T = TypeVar("_T", LockDetail, KeypadDetail) - - @dataclass(frozen=True, kw_only=True) -class YaleSensorEntityDescription(SensorEntityDescription, Generic[_T]): +class YaleSensorEntityDescription[T: LockDetail | KeypadDetail]( + SensorEntityDescription +): """Mixin for required keys.""" - value_fn: Callable[[_T], int | None] + value_fn: Callable[[T], int | None] SENSOR_TYPE_DEVICE_BATTERY = YaleSensorEntityDescription[LockDetail]( @@ -112,7 +111,7 @@ async def async_setup_entry( async_add_entities(entities) -class YaleOperatorSensor(YaleEntityMixin, RestoreSensor): +class YaleOperatorSensor(YaleEntity, RestoreSensor): """Representation of an Yale lock operation sensor.""" _attr_translation_key = "operator" @@ -196,10 +195,12 @@ class YaleOperatorSensor(YaleEntityMixin, RestoreSensor): self._operated_autorelock = last_attrs[ATTR_OPERATION_AUTORELOCK] -class YaleBatterySensor(YaleDescriptionEntity, SensorEntity, Generic[_T]): +class YaleBatterySensor[T: LockDetail | KeypadDetail]( + YaleDescriptionEntity, SensorEntity +): """Representation of an Yale sensor.""" - entity_description: YaleSensorEntityDescription[_T] + entity_description: YaleSensorEntityDescription[T] _attr_device_class = SensorDeviceClass.BATTERY _attr_native_unit_of_measurement = PERCENTAGE diff --git a/homeassistant/components/yale/util.py b/homeassistant/components/yale/util.py index d8bdaab4a66..3462c576fd9 100644 --- a/homeassistant/components/yale/util.py +++ b/homeassistant/components/yale/util.py @@ -63,16 +63,11 @@ def _activity_time_based(latest: Activity) -> Activity | None: """Get the latest state of the sensor.""" start = latest.activity_start_time end = latest.activity_end_time + TIME_TO_DECLARE_DETECTION - if start <= _native_datetime() <= end: + if start <= datetime.now() <= end: return latest return None -def _native_datetime() -> datetime: - """Return time in the format yale uses without timezone.""" - return datetime.now() - - def retrieve_online_state(data: YaleData, detail: DoorbellDetail | LockDetail) -> bool: """Get the latest state of the sensor.""" # The doorbell will go into standby mode when there is no motion diff --git a/tests/components/yale/__init__.py b/tests/components/yale/__init__.py index f0604940686..7f72d348042 100644 --- a/tests/components/yale/__init__.py +++ b/tests/components/yale/__init__.py @@ -1,12 +1 @@ """Tests for the yale component.""" - -MOCK_CONFIG_ENTRY_DATA = { - "auth_implementation": "cloud", - "token": { - "access_token": "access_token", - "expires_in": 1, - "refresh_token": "refresh_token", - "expires_at": 2, - "service": "yale", - }, -} diff --git a/tests/components/yale/snapshots/test_binary_sensor.ambr b/tests/components/yale/snapshots/test_binary_sensor.ambr new file mode 100644 index 00000000000..e294cb7c76c --- /dev/null +++ b/tests/components/yale/snapshots/test_binary_sensor.ambr @@ -0,0 +1,33 @@ +# serializer version: 1 +# name: test_doorbell_device_registry + DeviceRegistryEntrySnapshot({ + 'area_id': 'tmt100_name', + 'config_entries': , + 'configuration_url': 'https://account.aaecosystem.com', + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'yale', + 'tmt100', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Yale Home Inc.', + 'model': 'hydra1', + 'model_id': None, + 'name': 'tmt100 Name', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': 'tmt100 Name', + 'sw_version': '3.1.0-HYDRC75+201909251139', + 'via_device_id': None, + }) +# --- diff --git a/tests/components/yale/snapshots/test_diagnostics.ambr b/tests/components/yale/snapshots/test_diagnostics.ambr index fd31bc0ec91..c3d8d8e2aaa 100644 --- a/tests/components/yale/snapshots/test_diagnostics.ambr +++ b/tests/components/yale/snapshots/test_diagnostics.ambr @@ -1,7 +1,7 @@ # serializer version: 1 # name: test_diagnostics dict({ - 'brand': 'yale_home', + 'brand': 'yale_global', 'doorbells': dict({ 'K98GiDT45GUL': dict({ 'HouseID': '**REDACTED**', diff --git a/tests/components/yale/test_binary_sensor.py b/tests/components/yale/test_binary_sensor.py index ad4d4155e5b..811c845e359 100644 --- a/tests/components/yale/test_binary_sensor.py +++ b/tests/components/yale/test_binary_sensor.py @@ -1,7 +1,9 @@ """The binary_sensor tests for the yale platform.""" import datetime -from unittest.mock import patch + +from freezegun.api import FrozenDateTimeFactory +from syrupy import SnapshotAssertion from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN from homeassistant.const import ( @@ -33,28 +35,19 @@ async def test_doorsense(hass: HomeAssistant) -> None: hass, "get_lock.online_with_doorsense.json" ) await _create_yale_with_devices(hass, [lock_one]) - - binary_sensor_online_with_doorsense_name = hass.states.get( - "binary_sensor.online_with_doorsense_name_door" - ) - assert binary_sensor_online_with_doorsense_name.state == STATE_ON + states = hass.states + assert states.get("binary_sensor.online_with_doorsense_name_door").state == STATE_ON data = {ATTR_ENTITY_ID: "lock.online_with_doorsense_name"} await hass.services.async_call(LOCK_DOMAIN, SERVICE_UNLOCK, data, blocking=True) - await hass.async_block_till_done() - binary_sensor_online_with_doorsense_name = hass.states.get( - "binary_sensor.online_with_doorsense_name_door" - ) - assert binary_sensor_online_with_doorsense_name.state == STATE_ON + assert states.get("binary_sensor.online_with_doorsense_name_door").state == STATE_ON await hass.services.async_call(LOCK_DOMAIN, SERVICE_LOCK, data, blocking=True) - await hass.async_block_till_done() - binary_sensor_online_with_doorsense_name = hass.states.get( - "binary_sensor.online_with_doorsense_name_door" + assert ( + states.get("binary_sensor.online_with_doorsense_name_door").state == STATE_OFF ) - assert binary_sensor_online_with_doorsense_name.state == STATE_OFF async def test_lock_bridge_offline(hass: HomeAssistant) -> None: @@ -66,112 +59,78 @@ async def test_lock_bridge_offline(hass: HomeAssistant) -> None: hass, "get_activity.bridge_offline.json" ) await _create_yale_with_devices(hass, [lock_one], activities=activities) - - binary_sensor_online_with_doorsense_name = hass.states.get( - "binary_sensor.online_with_doorsense_name_door" + states = hass.states + assert ( + states.get("binary_sensor.online_with_doorsense_name_door").state + == STATE_UNAVAILABLE ) - assert binary_sensor_online_with_doorsense_name.state == STATE_UNAVAILABLE async def test_create_doorbell(hass: HomeAssistant) -> None: """Test creation of a doorbell.""" doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.json") await _create_yale_with_devices(hass, [doorbell_one]) - - binary_sensor_k98gidt45gul_name_motion = hass.states.get( - "binary_sensor.k98gidt45gul_name_motion" + states = hass.states + assert states.get("binary_sensor.k98gidt45gul_name_motion").state == STATE_OFF + assert ( + states.get("binary_sensor.k98gidt45gul_name_image_capture").state == STATE_OFF ) - assert binary_sensor_k98gidt45gul_name_motion.state == STATE_OFF - binary_sensor_k98gidt45gul_name_image_capture = hass.states.get( - "binary_sensor.k98gidt45gul_name_image_capture" + assert states.get("binary_sensor.k98gidt45gul_name_connectivity").state == STATE_ON + assert ( + states.get("binary_sensor.k98gidt45gul_name_doorbell_ding").state == STATE_OFF ) - assert binary_sensor_k98gidt45gul_name_image_capture.state == STATE_OFF - binary_sensor_k98gidt45gul_name_online = hass.states.get( - "binary_sensor.k98gidt45gul_name_connectivity" + assert states.get("binary_sensor.k98gidt45gul_name_motion").state == STATE_OFF + assert ( + states.get("binary_sensor.k98gidt45gul_name_image_capture").state == STATE_OFF ) - assert binary_sensor_k98gidt45gul_name_online.state == STATE_ON - binary_sensor_k98gidt45gul_name_ding = hass.states.get( - "binary_sensor.k98gidt45gul_name_doorbell_ding" - ) - assert binary_sensor_k98gidt45gul_name_ding.state == STATE_OFF - binary_sensor_k98gidt45gul_name_motion = hass.states.get( - "binary_sensor.k98gidt45gul_name_motion" - ) - assert binary_sensor_k98gidt45gul_name_motion.state == STATE_OFF - binary_sensor_k98gidt45gul_name_image_capture = hass.states.get( - "binary_sensor.k98gidt45gul_name_image_capture" - ) - assert binary_sensor_k98gidt45gul_name_image_capture.state == STATE_OFF async def test_create_doorbell_offline(hass: HomeAssistant) -> None: """Test creation of a doorbell that is offline.""" doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.offline.json") await _create_yale_with_devices(hass, [doorbell_one]) - - binary_sensor_tmt100_name_motion = hass.states.get( - "binary_sensor.tmt100_name_motion" + states = hass.states + assert states.get("binary_sensor.tmt100_name_motion").state == STATE_UNAVAILABLE + assert states.get("binary_sensor.tmt100_name_connectivity").state == STATE_OFF + assert ( + states.get("binary_sensor.tmt100_name_doorbell_ding").state == STATE_UNAVAILABLE ) - assert binary_sensor_tmt100_name_motion.state == STATE_UNAVAILABLE - binary_sensor_tmt100_name_online = hass.states.get( - "binary_sensor.tmt100_name_connectivity" - ) - assert binary_sensor_tmt100_name_online.state == STATE_OFF - binary_sensor_tmt100_name_ding = hass.states.get( - "binary_sensor.tmt100_name_doorbell_ding" - ) - assert binary_sensor_tmt100_name_ding.state == STATE_UNAVAILABLE -async def test_create_doorbell_with_motion(hass: HomeAssistant) -> None: +async def test_create_doorbell_with_motion( + hass: HomeAssistant, freezer: FrozenDateTimeFactory +) -> None: """Test creation of a doorbell.""" doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.json") activities = await _mock_activities_from_fixture( hass, "get_activity.doorbell_motion.json" ) await _create_yale_with_devices(hass, [doorbell_one], activities=activities) - - binary_sensor_k98gidt45gul_name_motion = hass.states.get( - "binary_sensor.k98gidt45gul_name_motion" + states = hass.states + assert states.get("binary_sensor.k98gidt45gul_name_motion").state == STATE_ON + assert states.get("binary_sensor.k98gidt45gul_name_connectivity").state == STATE_ON + assert ( + states.get("binary_sensor.k98gidt45gul_name_doorbell_ding").state == STATE_OFF ) - assert binary_sensor_k98gidt45gul_name_motion.state == STATE_ON - binary_sensor_k98gidt45gul_name_online = hass.states.get( - "binary_sensor.k98gidt45gul_name_connectivity" - ) - assert binary_sensor_k98gidt45gul_name_online.state == STATE_ON - binary_sensor_k98gidt45gul_name_ding = hass.states.get( - "binary_sensor.k98gidt45gul_name_doorbell_ding" - ) - assert binary_sensor_k98gidt45gul_name_ding.state == STATE_OFF - new_time = dt_util.utcnow() + datetime.timedelta(seconds=40) - native_time = datetime.datetime.now() + datetime.timedelta(seconds=40) - with patch( - "homeassistant.components.yale.util._native_datetime", - return_value=native_time, - ): - async_fire_time_changed(hass, new_time) - await hass.async_block_till_done() - binary_sensor_k98gidt45gul_name_motion = hass.states.get( - "binary_sensor.k98gidt45gul_name_motion" - ) - assert binary_sensor_k98gidt45gul_name_motion.state == STATE_OFF + freezer.tick(40) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert states.get("binary_sensor.k98gidt45gul_name_motion").state == STATE_OFF -async def test_doorbell_update_via_socketio(hass: HomeAssistant) -> None: +async def test_doorbell_update_via_socketio( + hass: HomeAssistant, freezer: FrozenDateTimeFactory +) -> None: """Test creation of a doorbell that can be updated via socketio.""" doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.json") _, socketio = await _create_yale_with_devices(hass, [doorbell_one]) assert doorbell_one.pubsub_channel == "7c7a6672-59c8-3333-ffff-dcd98705cccc" - - binary_sensor_k98gidt45gul_name_motion = hass.states.get( - "binary_sensor.k98gidt45gul_name_motion" + states = hass.states + assert states.get("binary_sensor.k98gidt45gul_name_motion").state == STATE_OFF + assert ( + states.get("binary_sensor.k98gidt45gul_name_doorbell_ding").state == STATE_OFF ) - assert binary_sensor_k98gidt45gul_name_motion.state == STATE_OFF - binary_sensor_k98gidt45gul_name_ding = hass.states.get( - "binary_sensor.k98gidt45gul_name_doorbell_ding" - ) - assert binary_sensor_k98gidt45gul_name_ding.state == STATE_OFF listener = list(socketio._listeners)[0] listener( @@ -192,10 +151,7 @@ async def test_doorbell_update_via_socketio(hass: HomeAssistant) -> None: await hass.async_block_till_done() - binary_sensor_k98gidt45gul_name_image_capture = hass.states.get( - "binary_sensor.k98gidt45gul_name_image_capture" - ) - assert binary_sensor_k98gidt45gul_name_image_capture.state == STATE_ON + assert states.get("binary_sensor.k98gidt45gul_name_image_capture").state == STATE_ON listener( doorbell_one.device_id, @@ -226,29 +182,18 @@ async def test_doorbell_update_via_socketio(hass: HomeAssistant) -> None: await hass.async_block_till_done() - binary_sensor_k98gidt45gul_name_motion = hass.states.get( - "binary_sensor.k98gidt45gul_name_motion" + assert states.get("binary_sensor.k98gidt45gul_name_motion").state == STATE_ON + assert ( + states.get("binary_sensor.k98gidt45gul_name_doorbell_ding").state == STATE_OFF ) - assert binary_sensor_k98gidt45gul_name_motion.state == STATE_ON - binary_sensor_k98gidt45gul_name_ding = hass.states.get( - "binary_sensor.k98gidt45gul_name_doorbell_ding" + freezer.tick(40) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert ( + states.get("binary_sensor.k98gidt45gul_name_image_capture").state == STATE_OFF ) - assert binary_sensor_k98gidt45gul_name_ding.state == STATE_OFF - - new_time = dt_util.utcnow() + datetime.timedelta(seconds=40) - native_time = datetime.datetime.now() + datetime.timedelta(seconds=40) - with patch( - "homeassistant.components.yale.util._native_datetime", - return_value=native_time, - ): - async_fire_time_changed(hass, new_time) - await hass.async_block_till_done() - - binary_sensor_k98gidt45gul_name_image_capture = hass.states.get( - "binary_sensor.k98gidt45gul_name_image_capture" - ) - assert binary_sensor_k98gidt45gul_name_image_capture.state == STATE_OFF listener( doorbell_one.device_id, @@ -260,37 +205,28 @@ async def test_doorbell_update_via_socketio(hass: HomeAssistant) -> None: await hass.async_block_till_done() - binary_sensor_k98gidt45gul_name_ding = hass.states.get( - "binary_sensor.k98gidt45gul_name_doorbell_ding" - ) - assert binary_sensor_k98gidt45gul_name_ding.state == STATE_ON - new_time = dt_util.utcnow() + datetime.timedelta(seconds=40) - native_time = datetime.datetime.now() + datetime.timedelta(seconds=40) - with patch( - "homeassistant.components.yale.util._native_datetime", - return_value=native_time, - ): - async_fire_time_changed(hass, new_time) - await hass.async_block_till_done() + assert states.get("binary_sensor.k98gidt45gul_name_doorbell_ding").state == STATE_ON - binary_sensor_k98gidt45gul_name_ding = hass.states.get( - "binary_sensor.k98gidt45gul_name_doorbell_ding" + freezer.tick(40) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert ( + states.get("binary_sensor.k98gidt45gul_name_doorbell_ding").state == STATE_OFF ) - assert binary_sensor_k98gidt45gul_name_ding.state == STATE_OFF async def test_doorbell_device_registry( - hass: HomeAssistant, device_registry: dr.DeviceRegistry + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + snapshot: SnapshotAssertion, ) -> None: """Test creation of a lock with doorsense and bridge ands up in the registry.""" doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.offline.json") await _create_yale_with_devices(hass, [doorbell_one]) reg_device = device_registry.async_get_device(identifiers={("yale", "tmt100")}) - assert reg_device.model == "hydra1" - assert reg_device.name == "tmt100 Name" - assert reg_device.manufacturer == "Yale Home Inc." - assert reg_device.sw_version == "3.1.0-HYDRC75+201909251139" + assert reg_device == snapshot async def test_door_sense_update_via_socketio(hass: HomeAssistant) -> None: @@ -302,11 +238,8 @@ async def test_door_sense_update_via_socketio(hass: HomeAssistant) -> None: config_entry, socketio = await _create_yale_with_devices( hass, [lock_one], activities=activities ) - - binary_sensor_online_with_doorsense_name = hass.states.get( - "binary_sensor.online_with_doorsense_name_door" - ) - assert binary_sensor_online_with_doorsense_name.state == STATE_ON + states = hass.states + assert states.get("binary_sensor.online_with_doorsense_name_door").state == STATE_ON listener = list(socketio._listeners)[0] listener( @@ -316,10 +249,10 @@ async def test_door_sense_update_via_socketio(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - binary_sensor_online_with_doorsense_name = hass.states.get( - "binary_sensor.online_with_doorsense_name_door" + + assert ( + states.get("binary_sensor.online_with_doorsense_name_door").state == STATE_OFF ) - assert binary_sensor_online_with_doorsense_name.state == STATE_OFF listener( lock_one.device_id, @@ -328,33 +261,22 @@ async def test_door_sense_update_via_socketio(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - binary_sensor_online_with_doorsense_name = hass.states.get( - "binary_sensor.online_with_doorsense_name_door" - ) - assert binary_sensor_online_with_doorsense_name.state == STATE_ON + + assert states.get("binary_sensor.online_with_doorsense_name_door").state == STATE_ON async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(seconds=30)) await hass.async_block_till_done() - binary_sensor_online_with_doorsense_name = hass.states.get( - "binary_sensor.online_with_doorsense_name_door" - ) - assert binary_sensor_online_with_doorsense_name.state == STATE_ON + assert states.get("binary_sensor.online_with_doorsense_name_door").state == STATE_ON socketio.connected = True async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(seconds=30)) await hass.async_block_till_done() - binary_sensor_online_with_doorsense_name = hass.states.get( - "binary_sensor.online_with_doorsense_name_door" - ) - assert binary_sensor_online_with_doorsense_name.state == STATE_ON + assert states.get("binary_sensor.online_with_doorsense_name_door").state == STATE_ON # Ensure socketio status is always preserved async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(hours=2)) await hass.async_block_till_done() - binary_sensor_online_with_doorsense_name = hass.states.get( - "binary_sensor.online_with_doorsense_name_door" - ) - assert binary_sensor_online_with_doorsense_name.state == STATE_ON + assert states.get("binary_sensor.online_with_doorsense_name_door").state == STATE_ON listener( lock_one.device_id, @@ -363,17 +285,11 @@ async def test_door_sense_update_via_socketio(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - binary_sensor_online_with_doorsense_name = hass.states.get( - "binary_sensor.online_with_doorsense_name_door" - ) - assert binary_sensor_online_with_doorsense_name.state == STATE_ON + assert states.get("binary_sensor.online_with_doorsense_name_door").state == STATE_ON async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(hours=4)) await hass.async_block_till_done() - binary_sensor_online_with_doorsense_name = hass.states.get( - "binary_sensor.online_with_doorsense_name_door" - ) - assert binary_sensor_online_with_doorsense_name.state == STATE_ON + assert states.get("binary_sensor.online_with_doorsense_name_door").state == STATE_ON await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() @@ -383,8 +299,10 @@ async def test_create_lock_with_doorbell(hass: HomeAssistant) -> None: """Test creation of a lock with a doorbell.""" lock_one = await _mock_lock_from_fixture(hass, "lock_with_doorbell.online.json") await _create_yale_with_devices(hass, [lock_one]) - - ding_sensor = hass.states.get( - "binary_sensor.a6697750d607098bae8d6baa11ef8063_name_doorbell_ding" + states = hass.states + assert ( + states.get( + "binary_sensor.a6697750d607098bae8d6baa11ef8063_name_doorbell_ding" + ).state + == STATE_OFF ) - assert ding_sensor.state == STATE_OFF diff --git a/tests/components/yale/test_config_flow.py b/tests/components/yale/test_config_flow.py index a62aa2d38f9..163f8240553 100644 --- a/tests/components/yale/test_config_flow.py +++ b/tests/components/yale/test_config_flow.py @@ -1,16 +1,16 @@ """Test the yale config flow.""" from collections.abc import Generator -from unittest.mock import Mock, patch +from unittest.mock import ANY, Mock, patch import pytest -from homeassistant import config_entries from homeassistant.components.yale.application_credentials import ( OAUTH2_AUTHORIZE, OAUTH2_TOKEN, ) from homeassistant.components.yale.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow @@ -44,7 +44,7 @@ async def test_full_flow( ) -> None: """Check full flow.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) state = config_entry_oauth2_flow._encode_jwt( hass, @@ -78,13 +78,81 @@ async def test_full_flow( }, ) - await hass.config_entries.flow.async_configure(result["flow_id"]) + result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert len(mock_setup_entry.mock_calls) == 1 entry = hass.config_entries.async_entries(DOMAIN)[0] assert entry.unique_id == USER_ID + assert result2["type"] is FlowResultType.CREATE_ENTRY + assert result2["result"].unique_id == USER_ID + assert entry.data == { + "auth_implementation": "yale", + "token": { + "access_token": jwt, + "expires_at": ANY, + "expires_in": ANY, + "refresh_token": "mock-refresh-token", + "scope": "any", + "user_id": "mock-user-id", + }, + } + + +@pytest.mark.usefixtures("client_credentials") +@pytest.mark.usefixtures("current_request_with_host") +async def test_full_flow_already_exists( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, + jwt: str, + mock_setup_entry: Mock, + mock_config_entry: MockConfigEntry, +) -> None: + """Check full flow for a user that already exists.""" + + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + + assert result["url"] == ( + f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}" + ) + + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.clear_requests() + aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "access_token": jwt, + "scope": "any", + "expires_in": 86399, + "refresh_token": "mock-refresh-token", + "user_id": "mock-user-id", + "expires_at": 1697753347, + }, + ) + + result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result2["type"] is FlowResultType.ABORT + assert result2["reason"] == "already_configured" + @pytest.mark.usefixtures("client_credentials") @pytest.mark.usefixtures("current_request_with_host") diff --git a/tests/components/yale/test_event.py b/tests/components/yale/test_event.py index f2f205289ff..7aeb9d8f12b 100644 --- a/tests/components/yale/test_event.py +++ b/tests/components/yale/test_event.py @@ -1,7 +1,6 @@ """The event tests for the yale.""" -import datetime -from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import HomeAssistant @@ -42,7 +41,9 @@ async def test_create_doorbell_offline(hass: HomeAssistant) -> None: assert doorbell_state.state == STATE_UNAVAILABLE -async def test_create_doorbell_with_motion(hass: HomeAssistant) -> None: +async def test_create_doorbell_with_motion( + hass: HomeAssistant, freezer: FrozenDateTimeFactory +) -> None: """Test creation of a doorbell.""" doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.json") activities = await _mock_activities_from_fixture( @@ -58,19 +59,16 @@ async def test_create_doorbell_with_motion(hass: HomeAssistant) -> None: assert doorbell_state is not None assert doorbell_state.state == STATE_UNKNOWN - new_time = dt_util.utcnow() + datetime.timedelta(seconds=40) - native_time = datetime.datetime.now() + datetime.timedelta(seconds=40) - with patch( - "homeassistant.components.yale.util._native_datetime", - return_value=native_time, - ): - async_fire_time_changed(hass, new_time) - await hass.async_block_till_done() + freezer.tick(40) + async_fire_time_changed(hass) + await hass.async_block_till_done() motion_state = hass.states.get("event.k98gidt45gul_name_motion") assert motion_state.state == isotime -async def test_doorbell_update_via_socketio(hass: HomeAssistant) -> None: +async def test_doorbell_update_via_socketio( + hass: HomeAssistant, freezer: FrozenDateTimeFactory +) -> None: """Test creation of a doorbell that can be updated via socketio.""" doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.json") @@ -119,14 +117,9 @@ async def test_doorbell_update_via_socketio(hass: HomeAssistant) -> None: assert motion_state.state != STATE_UNKNOWN isotime = motion_state.state - new_time = dt_util.utcnow() + datetime.timedelta(seconds=40) - native_time = datetime.datetime.now() + datetime.timedelta(seconds=40) - with patch( - "homeassistant.components.yale.util._native_datetime", - return_value=native_time, - ): - async_fire_time_changed(hass, new_time) - await hass.async_block_till_done() + freezer.tick(40) + async_fire_time_changed(hass) + await hass.async_block_till_done() motion_state = hass.states.get("event.k98gidt45gul_name_motion") assert motion_state is not None @@ -147,14 +140,9 @@ async def test_doorbell_update_via_socketio(hass: HomeAssistant) -> None: assert doorbell_state.state != STATE_UNKNOWN isotime = motion_state.state - new_time = dt_util.utcnow() + datetime.timedelta(seconds=40) - native_time = datetime.datetime.now() + datetime.timedelta(seconds=40) - with patch( - "homeassistant.components.yale.util._native_datetime", - return_value=native_time, - ): - async_fire_time_changed(hass, new_time) - await hass.async_block_till_done() + freezer.tick(40) + async_fire_time_changed(hass) + await hass.async_block_till_done() doorbell_state = hass.states.get("event.k98gidt45gul_name_doorbell") assert doorbell_state is not None diff --git a/tests/components/yale/test_init.py b/tests/components/yale/test_init.py index c9cb4be5882..4f0a853710c 100644 --- a/tests/components/yale/test_init.py +++ b/tests/components/yale/test_init.py @@ -89,16 +89,15 @@ async def test_unlock_throws_yale_api_http_error(hass: HomeAssistant) -> None: "unlock_return_activities": _unlock_return_activities_side_effect }, ) - last_err = None data = {ATTR_ENTITY_ID: "lock.a6697750d607098bae8d6baa11ef8063_name"} - try: + with pytest.raises( + HomeAssistantError, + match=( + "A6697750D607098BAE8D6BAA11EF8063 Name: This should bubble up as its user" + " consumable" + ), + ): await hass.services.async_call(LOCK_DOMAIN, SERVICE_UNLOCK, data, blocking=True) - except HomeAssistantError as err: - last_err = err - assert str(last_err) == ( - "A6697750D607098BAE8D6BAA11EF8063 Name: This should bubble up as its user" - " consumable" - ) async def test_lock_throws_yale_api_http_error(hass: HomeAssistant) -> None: @@ -119,16 +118,15 @@ async def test_lock_throws_yale_api_http_error(hass: HomeAssistant) -> None: "lock_return_activities": _lock_return_activities_side_effect }, ) - last_err = None data = {ATTR_ENTITY_ID: "lock.a6697750d607098bae8d6baa11ef8063_name"} - try: + with pytest.raises( + HomeAssistantError, + match=( + "A6697750D607098BAE8D6BAA11EF8063 Name: This should bubble up as its user" + " consumable" + ), + ): await hass.services.async_call(LOCK_DOMAIN, SERVICE_LOCK, data, blocking=True) - except HomeAssistantError as err: - last_err = err - assert str(last_err) == ( - "A6697750D607098BAE8D6BAA11EF8063 Name: This should bubble up as its user" - " consumable" - ) async def test_open_throws_hass_service_not_supported_error( @@ -185,6 +183,7 @@ async def test_load_unload(hass: HomeAssistant) -> None: await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.NOT_LOADED async def test_load_triggers_ble_discovery( From a2053d073f8628bfb6d1ec313d2e32c822995e80 Mon Sep 17 00:00:00 2001 From: Pete Sage <76050312+PeteRager@users.noreply.github.com> Date: Thu, 29 Aug 2024 05:29:54 -0400 Subject: [PATCH 111/271] Fix sonos get_queue service call to restrict to sonos media_player entities (#124815) add sonos to filter --- homeassistant/components/sonos/services.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/sonos/services.yaml b/homeassistant/components/sonos/services.yaml index 6d6e7ef83f9..89706428899 100644 --- a/homeassistant/components/sonos/services.yaml +++ b/homeassistant/components/sonos/services.yaml @@ -66,6 +66,7 @@ remove_from_queue: get_queue: target: entity: + integration: sonos domain: media_player update_alarm: From e8b722f7b278697e416a63b6442ffc6eae2c06f2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 28 Aug 2024 09:01:17 -1000 Subject: [PATCH 112/271] Redirect virtual integration yale_home to point to yale (#124817) --- homeassistant/components/yale_home/manifest.json | 2 +- homeassistant/generated/integrations.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/yale_home/manifest.json b/homeassistant/components/yale_home/manifest.json index 0e45b0da7d0..c497fa3fe34 100644 --- a/homeassistant/components/yale_home/manifest.json +++ b/homeassistant/components/yale_home/manifest.json @@ -2,5 +2,5 @@ "domain": "yale_home", "name": "Yale Home", "integration_type": "virtual", - "supported_by": "august" + "supported_by": "yale" } diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index e204170a06f..5f155c2926f 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -7005,7 +7005,7 @@ "yale_home": { "integration_type": "virtual", "config_flow": false, - "supported_by": "august", + "supported_by": "yale", "name": "Yale Home" }, "yale": { From 71de50dae89a2a1816d29cd417168047e1f61184 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 28 Aug 2024 12:28:41 -1000 Subject: [PATCH 113/271] Add missing dependencies to yale (#124821) * Add missing dependencies to yale * try another way * Revert "try another way" This reverts commit fbb731a33491bf51290fd98acde7b532ea39fb88. * patch out cloud setup --- homeassistant/components/yale/manifest.json | 1 + tests/components/yale/conftest.py | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/homeassistant/components/yale/manifest.json b/homeassistant/components/yale/manifest.json index d6da9ba3993..115036b96d5 100644 --- a/homeassistant/components/yale/manifest.json +++ b/homeassistant/components/yale/manifest.json @@ -3,6 +3,7 @@ "name": "Yale", "codeowners": ["@bdraco"], "config_flow": true, + "dependencies": ["application_credentials", "cloud"], "dhcp": [ { "hostname": "yale-connect-plus", diff --git a/tests/components/yale/conftest.py b/tests/components/yale/conftest.py index c890087ad12..3e633430846 100644 --- a/tests/components/yale/conftest.py +++ b/tests/components/yale/conftest.py @@ -57,3 +57,16 @@ def load_reauth_jwt_wrong_account_fixture() -> str: async def mock_client_credentials_fixture(hass: HomeAssistant) -> None: """Mock client credentials.""" await mock_client_credentials(hass) + + +@pytest.fixture(name="skip_cloud", autouse=True) +def skip_cloud_fixture(): + """Skip setting up cloud. + + Cloud already has its own tests for account link. + + We do not need to test it here as we only need to test our + usage of the oauth2 helpers. + """ + with patch("homeassistant.components.cloud.async_setup", return_value=True): + yield From 3078b47d0649b81cf5d290d24ebfc2dafbe1a9b9 Mon Sep 17 00:00:00 2001 From: AutonomousOwl <116417295+AutonomousOwl@users.noreply.github.com> Date: Thu, 29 Aug 2024 02:34:13 -0400 Subject: [PATCH 114/271] Update utility_account_id in Opower to be lowercase in statistic id (#124837) Update utility_account_id to be lowercase in statistic id --- homeassistant/components/opower/coordinator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/opower/coordinator.py b/homeassistant/components/opower/coordinator.py index d0795ae4e15..9cef4e4a252 100644 --- a/homeassistant/components/opower/coordinator.py +++ b/homeassistant/components/opower/coordinator.py @@ -98,7 +98,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]): account.meter_type.name.lower(), # Some utilities like AEP have "-" in their account id. # Replace it with "_" to avoid "Invalid statistic_id" - account.utility_account_id.replace("-", "_"), + account.utility_account_id.replace("-", "_").lower(), ) ) cost_statistic_id = f"{DOMAIN}:{id_prefix}_energy_cost" From ff39f09c4e4de9854868b9686bae2e252671d10d Mon Sep 17 00:00:00 2001 From: Andrew Jackson Date: Thu, 29 Aug 2024 11:23:04 +0100 Subject: [PATCH 115/271] Fix Mastodon migrate config entry log warning (#124848) Fix migrate config entry --- homeassistant/components/mastodon/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/mastodon/__init__.py b/homeassistant/components/mastodon/__init__.py index 0d680170f3d..e8d23434248 100644 --- a/homeassistant/components/mastodon/__init__.py +++ b/homeassistant/components/mastodon/__init__.py @@ -97,10 +97,9 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: LOGGER.error("Migration failed with error %s", ex) return False - entry.minor_version = 2 - hass.config_entries.async_update_entry( entry, + minor_version=2, unique_id=slugify(construct_mastodon_username(instance, account)), ) From 754e4255b62b96eb8579ceb4a07b1a074170bb63 Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Thu, 29 Aug 2024 13:20:57 +0200 Subject: [PATCH 116/271] Bump pydaikin to 2.13.6 (#124852) --- homeassistant/components/daikin/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/daikin/manifest.json b/homeassistant/components/daikin/manifest.json index c395ee35cad..88c29a20435 100644 --- a/homeassistant/components/daikin/manifest.json +++ b/homeassistant/components/daikin/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/daikin", "iot_class": "local_polling", "loggers": ["pydaikin"], - "requirements": ["pydaikin==2.13.5"], + "requirements": ["pydaikin==2.13.6"], "zeroconf": ["_dkapi._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index cf438e721bd..903ab7e8b2d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1798,7 +1798,7 @@ pycsspeechtts==1.0.8 # pycups==1.9.73 # homeassistant.components.daikin -pydaikin==2.13.5 +pydaikin==2.13.6 # homeassistant.components.danfoss_air pydanfossair==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bc6416948d3..4bfe2ff7160 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1445,7 +1445,7 @@ pycoolmasternet-async==0.2.2 pycsspeechtts==1.0.8 # homeassistant.components.daikin -pydaikin==2.13.5 +pydaikin==2.13.6 # homeassistant.components.deconz pydeconz==116 From b906a1b52124bcceab4f201f75bbfa847d3de39c Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 29 Aug 2024 17:25:04 +0200 Subject: [PATCH 117/271] Add missing translation key in Knocki (#124862) --- homeassistant/components/knocki/strings.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/knocki/strings.json b/homeassistant/components/knocki/strings.json index b7a7daad1fc..8f5d0161166 100644 --- a/homeassistant/components/knocki/strings.json +++ b/homeassistant/components/knocki/strings.json @@ -11,6 +11,9 @@ "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" } }, "entity": { From b2f27a45193ec18477770d34fdd53fe2240a9855 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 29 Aug 2024 17:24:25 +0200 Subject: [PATCH 118/271] Update frontend to 20240829.0 (#124864) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 0e1d443553d..7e934c887fa 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20240828.0"] + "requirements": ["home-assistant-frontend==20240829.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3aa1b7a298a..e6a5d6746f5 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -31,7 +31,7 @@ habluetooth==3.3.2 hass-nabucasa==0.81.1 hassil==1.7.4 home-assistant-bluetooth==1.12.2 -home-assistant-frontend==20240828.0 +home-assistant-frontend==20240829.0 home-assistant-intents==2024.8.7 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 903ab7e8b2d..4fb99787ab0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1102,7 +1102,7 @@ hole==0.8.0 holidays==0.55 # homeassistant.components.frontend -home-assistant-frontend==20240828.0 +home-assistant-frontend==20240829.0 # homeassistant.components.conversation home-assistant-intents==2024.8.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4bfe2ff7160..e0eb220a980 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -925,7 +925,7 @@ hole==0.8.0 holidays==0.55 # homeassistant.components.frontend -home-assistant-frontend==20240828.0 +home-assistant-frontend==20240829.0 # homeassistant.components.conversation home-assistant-intents==2024.8.7 From 03a02fa5657b9f34b2a8a776e69ea912e6907af8 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 29 Aug 2024 17:31:37 +0200 Subject: [PATCH 119/271] Bump version to 2024.9.0b1 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index a74ea6f7ebe..9e2a0ba9a2c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "0b0" +PATCH_VERSION: Final = "0b1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index d50cf2f9cd4..b960d559746 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.9.0b0" +version = "2024.9.0b1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 2d041a1fa922642b7dc6f5134e86a1280d71ce88 Mon Sep 17 00:00:00 2001 From: Tony <29752086+ms264556@users.noreply.github.com> Date: Fri, 30 Aug 2024 14:03:51 +1200 Subject: [PATCH 120/271] Bump aioruckus to v0.41 removing blocking call to load_default_certs from ruckus_unleashed integration (#123974) * fix ruckusd_unleashed blocking call to load_default_certs * remove extra loggers, bump aioruckus ver for debian packagers --- homeassistant/components/ruckus_unleashed/manifest.json | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/ruckus_unleashed/manifest.json b/homeassistant/components/ruckus_unleashed/manifest.json index edaf0aa95d2..039840efc14 100644 --- a/homeassistant/components/ruckus_unleashed/manifest.json +++ b/homeassistant/components/ruckus_unleashed/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/ruckus_unleashed", "integration_type": "hub", "iot_class": "local_polling", - "loggers": ["aioruckus", "xmltodict"], - "requirements": ["aioruckus==0.34"] + "loggers": ["aioruckus"], + "requirements": ["aioruckus==0.41"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4fb99787ab0..2bc559157a1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -347,7 +347,7 @@ aiorecollect==2023.09.0 aioridwell==2024.01.0 # homeassistant.components.ruckus_unleashed -aioruckus==0.34 +aioruckus==0.41 # homeassistant.components.russound_rio aiorussound==2.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e0eb220a980..f5400956d05 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -329,7 +329,7 @@ aiorecollect==2023.09.0 aioridwell==2024.01.0 # homeassistant.components.ruckus_unleashed -aioruckus==0.34 +aioruckus==0.41 # homeassistant.components.russound_rio aiorussound==2.3.2 From 81d2231e6fadcab1ba83720d3a5863deed787db1 Mon Sep 17 00:00:00 2001 From: Jeef Date: Fri, 30 Aug 2024 04:45:08 -0600 Subject: [PATCH 121/271] Bump weatherflow4py to 0.2.23 (#124072) patch weatherflow for new data --- homeassistant/components/weatherflow_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/weatherflow_cloud/manifest.json b/homeassistant/components/weatherflow_cloud/manifest.json index 354b9642c06..aaa5bce2e16 100644 --- a/homeassistant/components/weatherflow_cloud/manifest.json +++ b/homeassistant/components/weatherflow_cloud/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/weatherflow_cloud", "iot_class": "cloud_polling", "loggers": ["weatherflow4py"], - "requirements": ["weatherflow4py==0.2.21"] + "requirements": ["weatherflow4py==0.2.23"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2bc559157a1..fb205ac95b4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2915,7 +2915,7 @@ watchdog==2.3.1 waterfurnace==1.1.0 # homeassistant.components.weatherflow_cloud -weatherflow4py==0.2.21 +weatherflow4py==0.2.23 # homeassistant.components.webmin webmin-xmlrpc==0.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f5400956d05..2825fa1d824 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2304,7 +2304,7 @@ wallbox==0.7.0 watchdog==2.3.1 # homeassistant.components.weatherflow_cloud -weatherflow4py==0.2.21 +weatherflow4py==0.2.23 # homeassistant.components.webmin webmin-xmlrpc==0.0.2 From 3c0480596d067be0235a2c1754a9252a2aa84c6c Mon Sep 17 00:00:00 2001 From: tronikos Date: Fri, 30 Aug 2024 08:34:27 -0700 Subject: [PATCH 122/271] Attempt to fix IndexError in Opower (#124478) * Change the order of async_add_external_statistics in Opower * Use consumption_statistic_id instead of cost_statistic_id --- homeassistant/components/opower/coordinator.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/opower/coordinator.py b/homeassistant/components/opower/coordinator.py index 9cef4e4a252..3249cf1a375 100644 --- a/homeassistant/components/opower/coordinator.py +++ b/homeassistant/components/opower/coordinator.py @@ -110,7 +110,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]): ) last_stat = await get_instance(self.hass).async_add_executor_job( - get_last_statistics, self.hass, 1, cost_statistic_id, True, set() + get_last_statistics, self.hass, 1, consumption_statistic_id, True, set() ) if not last_stat: _LOGGER.debug("Updating statistic for the first time") @@ -124,7 +124,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]): cost_reads = await self._async_get_cost_reads( account, self.api.utility.timezone(), - last_stat[cost_statistic_id][0]["start"], + last_stat[consumption_statistic_id][0]["start"], ) if not cost_reads: _LOGGER.debug("No recent usage/cost data. Skipping update") @@ -141,7 +141,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]): ) cost_sum = cast(float, stats[cost_statistic_id][0]["sum"]) consumption_sum = cast(float, stats[consumption_statistic_id][0]["sum"]) - last_stats_time = stats[cost_statistic_id][0]["start"] + last_stats_time = stats[consumption_statistic_id][0]["start"] cost_statistics = [] consumption_statistics = [] @@ -187,7 +187,17 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]): else UnitOfVolume.CENTUM_CUBIC_FEET, ) + _LOGGER.debug( + "Adding %s statistics for %s", + len(cost_statistics), + cost_statistic_id, + ) async_add_external_statistics(self.hass, cost_metadata, cost_statistics) + _LOGGER.debug( + "Adding %s statistics for %s", + len(consumption_statistics), + consumption_statistic_id, + ) async_add_external_statistics( self.hass, consumption_metadata, consumption_statistics ) From 26f33057431b6515a72b4651c1c2606ea1785e53 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 30 Aug 2024 08:48:09 -0400 Subject: [PATCH 123/271] Bump ZHA to 0.0.32 (#124804) * Always prefer XY color mode in ZHA Remove a few more HS remnants * Use new ZHA OTA format * Bump ZHA to 0.0.32 * Fix existing OTA unit tests * Fix schema conversion test to account for new command parameters * Update snapshot with new `zcl_type` kwarg * Migrate existing entities to icon translations * Remove "no longer compatible" test * Test that the library release summary is correctly exposed to ZHA * Revert "Always prefer XY color mode in ZHA" This reverts commit 8fb7789ea8ddb6ed2a287aed5010374c0452f6c9. * Test `release_notes`, not `release_summary` --- homeassistant/components/zha/icons.json | 39 ++++++ homeassistant/components/zha/manifest.json | 2 +- homeassistant/components/zha/update.py | 11 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../zha/snapshots/test_diagnostics.ambr | 18 +-- tests/components/zha/test_helpers.py | 10 +- tests/components/zha/test_update.py | 121 ++++++++---------- 8 files changed, 117 insertions(+), 88 deletions(-) diff --git a/homeassistant/components/zha/icons.json b/homeassistant/components/zha/icons.json index 65ad029a66d..9d5254fe237 100644 --- a/homeassistant/components/zha/icons.json +++ b/homeassistant/components/zha/icons.json @@ -86,6 +86,18 @@ }, "presence_detection_timeout": { "default": "mdi:timer-edit" + }, + "exercise_trigger_time": { + "default": "mdi:clock" + }, + "external_temperature_sensor": { + "default": "mdi:thermometer" + }, + "load_room_mean": { + "default": "mdi:scale-balance" + }, + "regulation_setpoint_offset": { + "default": "mdi:thermostat" } }, "select": { @@ -94,6 +106,9 @@ }, "keypad_lockout": { "default": "mdi:lock" + }, + "exercise_day_of_week": { + "default": "mdi:wrench-clock" } }, "sensor": { @@ -132,6 +147,15 @@ }, "hooks_state": { "default": "mdi:hook" + }, + "open_window_detected": { + "default": "mdi:window-open" + }, + "load_estimate": { + "default": "mdi:scale-balance" + }, + "preheat_time": { + "default": "mdi:radiator" } }, "switch": { @@ -158,6 +182,21 @@ }, "hooks_locked": { "default": "mdi:lock" + }, + "external_window_sensor": { + "default": "mdi:window-open" + }, + "use_internal_window_detection": { + "default": "mdi:window-open" + }, + "prioritize_external_temperature_sensor": { + "default": "mdi:thermometer" + }, + "heat_available": { + "default": "mdi:water-boiler" + }, + "use_load_balancing": { + "default": "mdi:scale-balance" } } }, diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index a5e57fcb1ec..df60829a1e2 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -21,7 +21,7 @@ "zha", "universal_silabs_flasher" ], - "requirements": ["universal-silabs-flasher==0.0.22", "zha==0.0.31"], + "requirements": ["universal-silabs-flasher==0.0.22", "zha==0.0.32"], "usb": [ { "vid": "10C4", diff --git a/homeassistant/components/zha/update.py b/homeassistant/components/zha/update.py index e12d048b190..3a857f9d89b 100644 --- a/homeassistant/components/zha/update.py +++ b/homeassistant/components/zha/update.py @@ -95,6 +95,7 @@ class ZHAFirmwareUpdateEntity( UpdateEntityFeature.INSTALL | UpdateEntityFeature.PROGRESS | UpdateEntityFeature.SPECIFIC_VERSION + | UpdateEntityFeature.RELEASE_NOTES ) def __init__(self, entity_data: EntityData, **kwargs: Any) -> None: @@ -143,6 +144,14 @@ class ZHAFirmwareUpdateEntity( """ return self.entity_data.entity.release_summary + async def async_release_notes(self) -> str | None: + """Return full release notes. + + This is suitable for a long changelog that does not fit in the release_summary + property. The returned string can contain markdown. + """ + return self.entity_data.entity.release_notes + @property def release_url(self) -> str | None: """URL to the full release notes of the latest version available.""" @@ -155,7 +164,7 @@ class ZHAFirmwareUpdateEntity( ) -> None: """Install an update.""" try: - await self.entity_data.entity.async_install(version=version, backup=backup) + await self.entity_data.entity.async_install(version=version) except ZHAException as exc: raise HomeAssistantError(exc) from exc finally: diff --git a/requirements_all.txt b/requirements_all.txt index fb205ac95b4..7648cfce56e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3006,7 +3006,7 @@ zeroconf==0.133.0 zeversolar==0.3.1 # homeassistant.components.zha -zha==0.0.31 +zha==0.0.32 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2825fa1d824..aa1fda4ad20 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2383,7 +2383,7 @@ zeroconf==0.133.0 zeversolar==0.3.1 # homeassistant.components.zha -zha==0.0.31 +zha==0.0.32 # homeassistant.components.zwave_js zwave-js-server-python==0.57.0 diff --git a/tests/components/zha/snapshots/test_diagnostics.ambr b/tests/components/zha/snapshots/test_diagnostics.ambr index 67655aebc8c..e0da54e2492 100644 --- a/tests/components/zha/snapshots/test_diagnostics.ambr +++ b/tests/components/zha/snapshots/test_diagnostics.ambr @@ -162,19 +162,19 @@ '0x0500': dict({ 'attributes': dict({ '0x0000': dict({ - 'attribute': "ZCLAttributeDef(id=0x0000, name='zone_state', type=, access=, mandatory=True, is_manufacturer_specific=False)", + 'attribute': "ZCLAttributeDef(id=0x0000, name='zone_state', type=, zcl_type=, access=, mandatory=True, is_manufacturer_specific=False)", 'value': None, }), '0x0001': dict({ - 'attribute': "ZCLAttributeDef(id=0x0001, name='zone_type', type=, access=, mandatory=True, is_manufacturer_specific=False)", + 'attribute': "ZCLAttributeDef(id=0x0001, name='zone_type', type=, zcl_type=, access=, mandatory=True, is_manufacturer_specific=False)", 'value': None, }), '0x0002': dict({ - 'attribute': "ZCLAttributeDef(id=0x0002, name='zone_status', type=, access=, mandatory=True, is_manufacturer_specific=False)", + 'attribute': "ZCLAttributeDef(id=0x0002, name='zone_status', type=, zcl_type=, access=, mandatory=True, is_manufacturer_specific=False)", 'value': None, }), '0x0010': dict({ - 'attribute': "ZCLAttributeDef(id=0x0010, name='cie_addr', type=, access=, mandatory=True, is_manufacturer_specific=False)", + 'attribute': "ZCLAttributeDef(id=0x0010, name='cie_addr', type=, zcl_type=, access=, mandatory=True, is_manufacturer_specific=False)", 'value': list([ 50, 79, @@ -187,15 +187,15 @@ ]), }), '0x0011': dict({ - 'attribute': "ZCLAttributeDef(id=0x0011, name='zone_id', type=, access=, mandatory=True, is_manufacturer_specific=False)", + 'attribute': "ZCLAttributeDef(id=0x0011, name='zone_id', type=, zcl_type=, access=, mandatory=True, is_manufacturer_specific=False)", 'value': None, }), '0x0012': dict({ - 'attribute': "ZCLAttributeDef(id=0x0012, name='num_zone_sensitivity_levels_supported', type=, access=, mandatory=False, is_manufacturer_specific=False)", + 'attribute': "ZCLAttributeDef(id=0x0012, name='num_zone_sensitivity_levels_supported', type=, zcl_type=, access=, mandatory=False, is_manufacturer_specific=False)", 'value': None, }), '0x0013': dict({ - 'attribute': "ZCLAttributeDef(id=0x0013, name='current_zone_sensitivity_level', type=, access=, mandatory=False, is_manufacturer_specific=False)", + 'attribute': "ZCLAttributeDef(id=0x0013, name='current_zone_sensitivity_level', type=, zcl_type=, access=, mandatory=False, is_manufacturer_specific=False)", 'value': None, }), }), @@ -208,11 +208,11 @@ '0x0501': dict({ 'attributes': dict({ '0xfffd': dict({ - 'attribute': "ZCLAttributeDef(id=0xFFFD, name='cluster_revision', type=, access=, mandatory=True, is_manufacturer_specific=False)", + 'attribute': "ZCLAttributeDef(id=0xFFFD, name='cluster_revision', type=, zcl_type=, access=, mandatory=True, is_manufacturer_specific=False)", 'value': None, }), '0xfffe': dict({ - 'attribute': "ZCLAttributeDef(id=0xFFFE, name='reporting_status', type=, access=, mandatory=False, is_manufacturer_specific=False)", + 'attribute': "ZCLAttributeDef(id=0xFFFE, name='reporting_status', type=, zcl_type=, access=, mandatory=False, is_manufacturer_specific=False)", 'value': None, }), }), diff --git a/tests/components/zha/test_helpers.py b/tests/components/zha/test_helpers.py index 13c03c17cf7..d3392685437 100644 --- a/tests/components/zha/test_helpers.py +++ b/tests/components/zha/test_helpers.py @@ -60,16 +60,14 @@ async def test_zcl_schema_conversions(hass: HomeAssistant) -> None: "required": True, }, { - "type": "integer", - "valueMin": 0, - "valueMax": 255, + "type": "multi_select", + "options": ["Execute if off present"], "name": "options_mask", "optional": True, }, { - "type": "integer", - "valueMin": 0, - "valueMax": 255, + "type": "multi_select", + "options": ["Execute if off"], "name": "options_override", "optional": True, }, diff --git a/tests/components/zha/test_update.py b/tests/components/zha/test_update.py index 6a1a19b407f..e2a614915f9 100644 --- a/tests/components/zha/test_update.py +++ b/tests/components/zha/test_update.py @@ -3,8 +3,11 @@ from unittest.mock import AsyncMock, call, patch import pytest +from zha.application.platforms.update import ( + FirmwareUpdateEntity as ZhaFirmwareUpdateEntity, +) from zigpy.exceptions import DeliveryError -from zigpy.ota import OtaImageWithMetadata +from zigpy.ota import OtaImagesResult, OtaImageWithMetadata import zigpy.ota.image as firmware from zigpy.ota.providers import BaseOtaImageMetadata from zigpy.profiles import zha @@ -43,6 +46,8 @@ from homeassistant.setup import async_setup_component from .common import find_entity_id, update_attribute_cache from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE +from tests.typing import WebSocketGenerator + @pytest.fixture(autouse=True) def update_platform_only(): @@ -119,8 +124,11 @@ async def setup_test_data( ), ) - cluster.endpoint.device.application.ota.get_ota_image = AsyncMock( - return_value=None if file_not_found else fw_image + cluster.endpoint.device.application.ota.get_ota_images = AsyncMock( + return_value=OtaImagesResult( + upgrades=() if file_not_found else (fw_image,), + downgrades=(), + ) ) zha_device_proxy: ZHADeviceProxy = gateway_proxy.get_device_proxy(zigpy_device.ieee) zha_device_proxy.device.async_update_sw_build_id(installed_fw_version) @@ -544,81 +552,56 @@ async def test_firmware_update_raises( ) -async def test_firmware_update_no_longer_compatible( +async def test_update_release_notes( hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, setup_zha, zigpy_device_mock, ) -> None: - """Test ZHA update platform - firmware update is no longer valid.""" + """Test ZHA update platform release notes.""" await setup_zha() - zha_device, cluster, fw_image, installed_fw_version = await setup_test_data( - hass, zigpy_device_mock + + gateway = get_zha_gateway(hass) + gateway_proxy: ZHAGatewayProxy = get_zha_gateway_proxy(hass) + + zigpy_device = zigpy_device_mock( + { + 1: { + SIG_EP_INPUT: [general.Basic.cluster_id, general.OnOff.cluster_id], + SIG_EP_OUTPUT: [general.Ota.cluster_id], + SIG_EP_TYPE: zha.DeviceType.ON_OFF_SWITCH, + SIG_EP_PROFILE: zha.PROFILE_ID, + } + }, + node_descriptor=b"\x02@\x84_\x11\x7fd\x00\x00,d\x00\x00", ) + gateway.get_or_create_device(zigpy_device) + await gateway.async_device_initialized(zigpy_device) + await hass.async_block_till_done(wait_background_tasks=True) + + zha_device: ZHADeviceProxy = gateway_proxy.get_device_proxy(zigpy_device.ieee) + zha_lib_entity = next( + e + for e in zha_device.device.platform_entities.values() + if isinstance(e, ZhaFirmwareUpdateEntity) + ) + zha_lib_entity._attr_release_notes = "Some lengthy release notes" + zha_lib_entity.maybe_emit_state_changed_event() + await hass.async_block_till_done() + entity_id = find_entity_id(Platform.UPDATE, zha_device, hass) assert entity_id is not None - assert hass.states.get(entity_id).state == STATE_UNKNOWN - - # simulate an image available notification - await cluster._handle_query_next_image( - foundation.ZCLHeader.cluster( - tsn=0x12, command_id=general.Ota.ServerCommandDefs.query_next_image.id - ), - general.QueryNextImageCommand( - fw_image.firmware.header.field_control, - zha_device.device.manufacturer_code, - fw_image.firmware.header.image_type, - installed_fw_version, - fw_image.firmware.header.header_version, - ), + ws_client = await hass_ws_client(hass) + await ws_client.send_json( + { + "id": 1, + "type": "update/release_notes", + "entity_id": entity_id, + } ) - await hass.async_block_till_done() - state = hass.states.get(entity_id) - assert state.state == STATE_ON - attrs = state.attributes - assert attrs[ATTR_INSTALLED_VERSION] == f"0x{installed_fw_version:08x}" - assert not attrs[ATTR_IN_PROGRESS] - assert ( - attrs[ATTR_LATEST_VERSION] == f"0x{fw_image.firmware.header.file_version:08x}" - ) - - new_version = 0x99999999 - - async def endpoint_reply(cluster_id, tsn, data, command_id): - if cluster_id == general.Ota.cluster_id: - hdr, cmd = cluster.deserialize(data) - if isinstance(cmd, general.Ota.ImageNotifyCommand): - zha_device.device.device.packet_received( - make_packet( - zha_device.device.device, - cluster, - general.Ota.ServerCommandDefs.query_next_image.name, - field_control=general.Ota.QueryNextImageCommand.FieldControl.HardwareVersion, - manufacturer_code=fw_image.firmware.header.manufacturer_id, - image_type=fw_image.firmware.header.image_type, - # The device reports that it is no longer compatible! - current_file_version=new_version, - hardware_version=1, - ) - ) - - cluster.endpoint.reply = AsyncMock(side_effect=endpoint_reply) - with pytest.raises(HomeAssistantError): - await hass.services.async_call( - UPDATE_DOMAIN, - SERVICE_INSTALL, - { - ATTR_ENTITY_ID: entity_id, - }, - blocking=True, - ) - - # We updated the currently installed firmware version, as it is no longer valid - state = hass.states.get(entity_id) - assert state.state == STATE_OFF - attrs = state.attributes - assert attrs[ATTR_INSTALLED_VERSION] == f"0x{new_version:08x}" - assert not attrs[ATTR_IN_PROGRESS] - assert attrs[ATTR_LATEST_VERSION] == f"0x{new_version:08x}" + result = await ws_client.receive_json() + assert result["success"] is True + assert result["result"] == "Some lengthy release notes" From 98cbd7d8da84add5cdb2769d6b6cac78e61c38b0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 29 Aug 2024 07:32:13 -1000 Subject: [PATCH 124/271] Address august review comments (#124819) * Address august review comments Followup to https://github.com/home-assistant/core/pull/124677 * cleanup loop * drop mixin name * event entity add cleanup * remove duplicate prop * pep0695 type * remove some not needed block till done * cleanup august tests * switch to freezegun * snapshots for dev reg * SOURCE_USER nit * snapshots * pytest.raises * not loaded check --- homeassistant/components/august/__init__.py | 2 +- .../components/august/binary_sensor.py | 11 +- homeassistant/components/august/button.py | 4 +- homeassistant/components/august/camera.py | 4 +- homeassistant/components/august/entity.py | 4 +- homeassistant/components/august/event.py | 28 +- homeassistant/components/august/lock.py | 4 +- homeassistant/components/august/sensor.py | 21 +- homeassistant/components/august/util.py | 7 +- .../august/snapshots/test_binary_sensor.ambr | 33 +++ .../august/snapshots/test_lock.ambr | 37 +++ tests/components/august/test_binary_sensor.py | 239 ++++++------------ tests/components/august/test_button.py | 1 - tests/components/august/test_camera.py | 10 +- tests/components/august/test_config_flow.py | 14 +- tests/components/august/test_event.py | 46 ++-- tests/components/august/test_init.py | 32 +-- tests/components/august/test_lock.py | 166 ++++-------- tests/components/august/test_sensor.py | 71 ++---- 19 files changed, 312 insertions(+), 422 deletions(-) create mode 100644 tests/components/august/snapshots/test_binary_sensor.ambr create mode 100644 tests/components/august/snapshots/test_lock.ambr diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index 53aa3cdffd8..47a7f75611a 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -24,7 +24,7 @@ from .util import async_create_august_clientsession type AugustConfigEntry = ConfigEntry[AugustData] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: AugustConfigEntry) -> bool: """Set up August from a config entry.""" session = async_create_august_clientsession(hass) august_gateway = AugustGateway(Path(hass.config.config_dir), session) diff --git a/homeassistant/components/august/binary_sensor.py b/homeassistant/components/august/binary_sensor.py index 6a56692bcd6..fb877252010 100644 --- a/homeassistant/components/august/binary_sensor.py +++ b/homeassistant/components/august/binary_sensor.py @@ -109,12 +109,11 @@ async def async_setup_entry( for description in SENSOR_TYPES_DOORBELL ) - for doorbell in data.doorbells: - entities.extend( - AugustDoorbellBinarySensor(data, doorbell, description) - for description in SENSOR_TYPES_DOORBELL + SENSOR_TYPES_VIDEO_DOORBELL - ) - + entities.extend( + AugustDoorbellBinarySensor(data, doorbell, description) + for description in SENSOR_TYPES_DOORBELL + SENSOR_TYPES_VIDEO_DOORBELL + for doorbell in data.doorbells + ) async_add_entities(entities) diff --git a/homeassistant/components/august/button.py b/homeassistant/components/august/button.py index 406475db601..79f2b67888a 100644 --- a/homeassistant/components/august/button.py +++ b/homeassistant/components/august/button.py @@ -5,7 +5,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import AugustConfigEntry -from .entity import AugustEntityMixin +from .entity import AugustEntity async def async_setup_entry( @@ -18,7 +18,7 @@ async def async_setup_entry( async_add_entities(AugustWakeLockButton(data, lock, "wake") for lock in data.locks) -class AugustWakeLockButton(AugustEntityMixin, ButtonEntity): +class AugustWakeLockButton(AugustEntity, ButtonEntity): """Representation of an August lock wake button.""" _attr_translation_key = "wake" diff --git a/homeassistant/components/august/camera.py b/homeassistant/components/august/camera.py index 4e569e2a91e..f4398455256 100644 --- a/homeassistant/components/august/camera.py +++ b/homeassistant/components/august/camera.py @@ -16,7 +16,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import AugustConfigEntry, AugustData from .const import DEFAULT_NAME, DEFAULT_TIMEOUT -from .entity import AugustEntityMixin +from .entity import AugustEntity _LOGGER = logging.getLogger(__name__) @@ -38,7 +38,7 @@ async def async_setup_entry( ) -class AugustCamera(AugustEntityMixin, Camera): +class AugustCamera(AugustEntity, Camera): """An implementation of an August security camera.""" _attr_translation_key = "camera" diff --git a/homeassistant/components/august/entity.py b/homeassistant/components/august/entity.py index babf5c587fb..28c722354ba 100644 --- a/homeassistant/components/august/entity.py +++ b/homeassistant/components/august/entity.py @@ -20,7 +20,7 @@ from .const import MANUFACTURER DEVICE_TYPES = ["keypad", "lock", "camera", "doorbell", "door", "bell"] -class AugustEntityMixin(Entity): +class AugustEntity(Entity): """Base implementation for August device.""" _attr_should_poll = False @@ -87,7 +87,7 @@ class AugustEntityMixin(Entity): self._update_from_data() -class AugustDescriptionEntity(AugustEntityMixin): +class AugustDescriptionEntity(AugustEntity): """An August entity with a description.""" def __init__( diff --git a/homeassistant/components/august/event.py b/homeassistant/components/august/event.py index b65f72272a3..49b14630337 100644 --- a/homeassistant/components/august/event.py +++ b/homeassistant/components/august/event.py @@ -63,22 +63,17 @@ async def async_setup_entry( ) -> None: """Set up the august event platform.""" data = config_entry.runtime_data - entities: list[AugustEventEntity] = [] - - for lock in data.locks: - detail = data.get_device_detail(lock.device_id) - if detail.doorbell: - entities.extend( - AugustEventEntity(data, lock, description) - for description in TYPES_DOORBELL - ) - - for doorbell in data.doorbells: - entities.extend( - AugustEventEntity(data, doorbell, description) - for description in TYPES_DOORBELL + TYPES_VIDEO_DOORBELL - ) - + entities: list[AugustEventEntity] = [ + AugustEventEntity(data, lock, description) + for description in TYPES_DOORBELL + for lock in data.locks + if (detail := data.get_device_detail(lock.device_id)) and detail.doorbell + ] + entities.extend( + AugustEventEntity(data, doorbell, description) + for description in TYPES_DOORBELL + TYPES_VIDEO_DOORBELL + for doorbell in data.doorbells + ) async_add_entities(entities) @@ -86,7 +81,6 @@ class AugustEventEntity(AugustDescriptionEntity, EventEntity): """An august event entity.""" entity_description: AugustEventEntityDescription - _attr_has_entity_name = True _last_activity: Activity | None = None @callback diff --git a/homeassistant/components/august/lock.py b/homeassistant/components/august/lock.py index 5382c710229..fe5d90371ad 100644 --- a/homeassistant/components/august/lock.py +++ b/homeassistant/components/august/lock.py @@ -19,7 +19,7 @@ from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.util.dt as dt_util from . import AugustConfigEntry, AugustData -from .entity import AugustEntityMixin +from .entity import AugustEntity _LOGGER = logging.getLogger(__name__) @@ -36,7 +36,7 @@ async def async_setup_entry( async_add_entities(AugustLock(data, lock) for lock in data.locks) -class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity): +class AugustLock(AugustEntity, RestoreEntity, LockEntity): """Representation of an August lock.""" _attr_name = None diff --git a/homeassistant/components/august/sensor.py b/homeassistant/components/august/sensor.py index 7a4c1a92358..b7c0d618492 100644 --- a/homeassistant/components/august/sensor.py +++ b/homeassistant/components/august/sensor.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from typing import Any, Generic, TypeVar, cast +from typing import Any, cast from yalexs.activity import ActivityType, LockOperationActivity from yalexs.doorbell import Doorbell @@ -42,7 +42,7 @@ from .const import ( OPERATION_METHOD_REMOTE, OPERATION_METHOD_TAG, ) -from .entity import AugustDescriptionEntity, AugustEntityMixin +from .entity import AugustDescriptionEntity, AugustEntity def _retrieve_device_battery_state(detail: LockDetail) -> int: @@ -55,14 +55,13 @@ def _retrieve_linked_keypad_battery_state(detail: KeypadDetail) -> int | None: return detail.battery_percentage -_T = TypeVar("_T", LockDetail, KeypadDetail) - - @dataclass(frozen=True, kw_only=True) -class AugustSensorEntityDescription(SensorEntityDescription, Generic[_T]): +class AugustSensorEntityDescription[T: LockDetail | KeypadDetail]( + SensorEntityDescription +): """Mixin for required keys.""" - value_fn: Callable[[_T], int | None] + value_fn: Callable[[T], int | None] SENSOR_TYPE_DEVICE_BATTERY = AugustSensorEntityDescription[LockDetail]( @@ -114,7 +113,7 @@ async def async_setup_entry( async_add_entities(entities) -class AugustOperatorSensor(AugustEntityMixin, RestoreSensor): +class AugustOperatorSensor(AugustEntity, RestoreSensor): """Representation of an August lock operation sensor.""" _attr_translation_key = "operator" @@ -198,10 +197,12 @@ class AugustOperatorSensor(AugustEntityMixin, RestoreSensor): self._operated_autorelock = last_attrs[ATTR_OPERATION_AUTORELOCK] -class AugustBatterySensor(AugustDescriptionEntity, SensorEntity, Generic[_T]): +class AugustBatterySensor[T: LockDetail | KeypadDetail]( + AugustDescriptionEntity, SensorEntity +): """Representation of an August sensor.""" - entity_description: AugustSensorEntityDescription[_T] + entity_description: AugustSensorEntityDescription[T] _attr_device_class = SensorDeviceClass.BATTERY _attr_native_unit_of_measurement = PERCENTAGE diff --git a/homeassistant/components/august/util.py b/homeassistant/components/august/util.py index 6972913ba22..5449d048613 100644 --- a/homeassistant/components/august/util.py +++ b/homeassistant/components/august/util.py @@ -63,16 +63,11 @@ def _activity_time_based(latest: Activity) -> Activity | None: """Get the latest state of the sensor.""" start = latest.activity_start_time end = latest.activity_end_time + TIME_TO_DECLARE_DETECTION - if start <= _native_datetime() <= end: + if start <= datetime.now() <= end: return latest return None -def _native_datetime() -> datetime: - """Return time in the format august uses without timezone.""" - return datetime.now() - - def retrieve_online_state( data: AugustData, detail: DoorbellDetail | LockDetail ) -> bool: diff --git a/tests/components/august/snapshots/test_binary_sensor.ambr b/tests/components/august/snapshots/test_binary_sensor.ambr new file mode 100644 index 00000000000..6e95b0ce552 --- /dev/null +++ b/tests/components/august/snapshots/test_binary_sensor.ambr @@ -0,0 +1,33 @@ +# serializer version: 1 +# name: test_doorbell_device_registry + DeviceRegistryEntrySnapshot({ + 'area_id': 'tmt100_name', + 'config_entries': , + 'configuration_url': 'https://account.august.com', + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'august', + 'tmt100', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'August Home Inc.', + 'model': 'hydra1', + 'model_id': None, + 'name': 'tmt100 Name', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': 'tmt100 Name', + 'sw_version': '3.1.0-HYDRC75+201909251139', + 'via_device_id': None, + }) +# --- diff --git a/tests/components/august/snapshots/test_lock.ambr b/tests/components/august/snapshots/test_lock.ambr new file mode 100644 index 00000000000..6aad3a140ca --- /dev/null +++ b/tests/components/august/snapshots/test_lock.ambr @@ -0,0 +1,37 @@ +# serializer version: 1 +# name: test_lock_device_registry + DeviceRegistryEntrySnapshot({ + 'area_id': 'online_with_doorsense_name', + 'config_entries': , + 'configuration_url': 'https://account.august.com', + 'connections': set({ + tuple( + 'bluetooth', + '12:22', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'august', + 'online_with_doorsense', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'August Home Inc.', + 'model': 'AUG-MD01', + 'model_id': None, + 'name': 'online_with_doorsense Name', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': 'online_with_doorsense Name', + 'sw_version': 'undefined-4.3.0-1.8.14', + 'via_device_id': None, + }) +# --- diff --git a/tests/components/august/test_binary_sensor.py b/tests/components/august/test_binary_sensor.py index 33d582de8d8..4ae300ae56b 100644 --- a/tests/components/august/test_binary_sensor.py +++ b/tests/components/august/test_binary_sensor.py @@ -1,8 +1,10 @@ """The binary_sensor tests for the august platform.""" import datetime -from unittest.mock import Mock, patch +from unittest.mock import Mock +from freezegun.api import FrozenDateTimeFactory +from syrupy import SnapshotAssertion from yalexs.pubnub_async import AugustPubNub from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN @@ -36,28 +38,20 @@ async def test_doorsense(hass: HomeAssistant) -> None: hass, "get_lock.online_with_doorsense.json" ) await _create_august_with_devices(hass, [lock_one]) + states = hass.states - binary_sensor_online_with_doorsense_name = hass.states.get( - "binary_sensor.online_with_doorsense_name_door" - ) - assert binary_sensor_online_with_doorsense_name.state == STATE_ON + assert states.get("binary_sensor.online_with_doorsense_name_door").state == STATE_ON data = {ATTR_ENTITY_ID: "lock.online_with_doorsense_name"} await hass.services.async_call(LOCK_DOMAIN, SERVICE_UNLOCK, data, blocking=True) - await hass.async_block_till_done() - binary_sensor_online_with_doorsense_name = hass.states.get( - "binary_sensor.online_with_doorsense_name_door" - ) - assert binary_sensor_online_with_doorsense_name.state == STATE_ON + assert states.get("binary_sensor.online_with_doorsense_name_door").state == STATE_ON await hass.services.async_call(LOCK_DOMAIN, SERVICE_LOCK, data, blocking=True) - await hass.async_block_till_done() - binary_sensor_online_with_doorsense_name = hass.states.get( - "binary_sensor.online_with_doorsense_name_door" + assert ( + states.get("binary_sensor.online_with_doorsense_name_door").state == STATE_OFF ) - assert binary_sensor_online_with_doorsense_name.state == STATE_OFF async def test_lock_bridge_offline(hass: HomeAssistant) -> None: @@ -69,113 +63,82 @@ async def test_lock_bridge_offline(hass: HomeAssistant) -> None: hass, "get_activity.bridge_offline.json" ) await _create_august_with_devices(hass, [lock_one], activities=activities) - - binary_sensor_online_with_doorsense_name = hass.states.get( - "binary_sensor.online_with_doorsense_name_door" + states = hass.states + assert ( + states.get("binary_sensor.online_with_doorsense_name_door").state + == STATE_UNAVAILABLE ) - assert binary_sensor_online_with_doorsense_name.state == STATE_UNAVAILABLE async def test_create_doorbell(hass: HomeAssistant) -> None: """Test creation of a doorbell.""" doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.json") await _create_august_with_devices(hass, [doorbell_one]) + states = hass.states - binary_sensor_k98gidt45gul_name_motion = hass.states.get( - "binary_sensor.k98gidt45gul_name_motion" + assert states.get("binary_sensor.k98gidt45gul_name_motion").state == STATE_OFF + assert ( + states.get("binary_sensor.k98gidt45gul_name_image_capture").state == STATE_OFF ) - assert binary_sensor_k98gidt45gul_name_motion.state == STATE_OFF - binary_sensor_k98gidt45gul_name_image_capture = hass.states.get( - "binary_sensor.k98gidt45gul_name_image_capture" + assert states.get("binary_sensor.k98gidt45gul_name_connectivity").state == STATE_ON + assert ( + states.get("binary_sensor.k98gidt45gul_name_doorbell_ding").state == STATE_OFF ) - assert binary_sensor_k98gidt45gul_name_image_capture.state == STATE_OFF - binary_sensor_k98gidt45gul_name_online = hass.states.get( - "binary_sensor.k98gidt45gul_name_connectivity" + assert states.get("binary_sensor.k98gidt45gul_name_motion").state == STATE_OFF + assert ( + states.get("binary_sensor.k98gidt45gul_name_image_capture").state == STATE_OFF ) - assert binary_sensor_k98gidt45gul_name_online.state == STATE_ON - binary_sensor_k98gidt45gul_name_ding = hass.states.get( - "binary_sensor.k98gidt45gul_name_doorbell_ding" - ) - assert binary_sensor_k98gidt45gul_name_ding.state == STATE_OFF - binary_sensor_k98gidt45gul_name_motion = hass.states.get( - "binary_sensor.k98gidt45gul_name_motion" - ) - assert binary_sensor_k98gidt45gul_name_motion.state == STATE_OFF - binary_sensor_k98gidt45gul_name_image_capture = hass.states.get( - "binary_sensor.k98gidt45gul_name_image_capture" - ) - assert binary_sensor_k98gidt45gul_name_image_capture.state == STATE_OFF async def test_create_doorbell_offline(hass: HomeAssistant) -> None: """Test creation of a doorbell that is offline.""" doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.offline.json") await _create_august_with_devices(hass, [doorbell_one]) + states = hass.states - binary_sensor_tmt100_name_motion = hass.states.get( - "binary_sensor.tmt100_name_motion" + assert states.get("binary_sensor.tmt100_name_motion").state == STATE_UNAVAILABLE + assert states.get("binary_sensor.tmt100_name_connectivity").state == STATE_OFF + assert ( + states.get("binary_sensor.tmt100_name_doorbell_ding").state == STATE_UNAVAILABLE ) - assert binary_sensor_tmt100_name_motion.state == STATE_UNAVAILABLE - binary_sensor_tmt100_name_online = hass.states.get( - "binary_sensor.tmt100_name_connectivity" - ) - assert binary_sensor_tmt100_name_online.state == STATE_OFF - binary_sensor_tmt100_name_ding = hass.states.get( - "binary_sensor.tmt100_name_doorbell_ding" - ) - assert binary_sensor_tmt100_name_ding.state == STATE_UNAVAILABLE -async def test_create_doorbell_with_motion(hass: HomeAssistant) -> None: +async def test_create_doorbell_with_motion( + hass: HomeAssistant, freezer: FrozenDateTimeFactory +) -> None: """Test creation of a doorbell.""" doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.json") activities = await _mock_activities_from_fixture( hass, "get_activity.doorbell_motion.json" ) await _create_august_with_devices(hass, [doorbell_one], activities=activities) + states = hass.states - binary_sensor_k98gidt45gul_name_motion = hass.states.get( - "binary_sensor.k98gidt45gul_name_motion" + assert states.get("binary_sensor.k98gidt45gul_name_motion").state == STATE_ON + assert states.get("binary_sensor.k98gidt45gul_name_connectivity").state == STATE_ON + assert ( + states.get("binary_sensor.k98gidt45gul_name_doorbell_ding").state == STATE_OFF ) - assert binary_sensor_k98gidt45gul_name_motion.state == STATE_ON - binary_sensor_k98gidt45gul_name_online = hass.states.get( - "binary_sensor.k98gidt45gul_name_connectivity" - ) - assert binary_sensor_k98gidt45gul_name_online.state == STATE_ON - binary_sensor_k98gidt45gul_name_ding = hass.states.get( - "binary_sensor.k98gidt45gul_name_doorbell_ding" - ) - assert binary_sensor_k98gidt45gul_name_ding.state == STATE_OFF - new_time = dt_util.utcnow() + datetime.timedelta(seconds=40) - native_time = datetime.datetime.now() + datetime.timedelta(seconds=40) - with patch( - "homeassistant.components.august.util._native_datetime", - return_value=native_time, - ): - async_fire_time_changed(hass, new_time) - await hass.async_block_till_done() - binary_sensor_k98gidt45gul_name_motion = hass.states.get( - "binary_sensor.k98gidt45gul_name_motion" - ) - assert binary_sensor_k98gidt45gul_name_motion.state == STATE_OFF + freezer.tick(40) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert states.get("binary_sensor.k98gidt45gul_name_motion").state == STATE_OFF -async def test_doorbell_update_via_pubnub(hass: HomeAssistant) -> None: +async def test_doorbell_update_via_pubnub( + hass: HomeAssistant, freezer: FrozenDateTimeFactory +) -> None: """Test creation of a doorbell that can be updated via pubnub.""" doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.json") pubnub = AugustPubNub() await _create_august_with_devices(hass, [doorbell_one], pubnub=pubnub) assert doorbell_one.pubsub_channel == "7c7a6672-59c8-3333-ffff-dcd98705cccc" - - binary_sensor_k98gidt45gul_name_motion = hass.states.get( - "binary_sensor.k98gidt45gul_name_motion" + states = hass.states + assert states.get("binary_sensor.k98gidt45gul_name_motion").state == STATE_OFF + assert ( + states.get("binary_sensor.k98gidt45gul_name_doorbell_ding").state == STATE_OFF ) - assert binary_sensor_k98gidt45gul_name_motion.state == STATE_OFF - binary_sensor_k98gidt45gul_name_ding = hass.states.get( - "binary_sensor.k98gidt45gul_name_doorbell_ding" - ) - assert binary_sensor_k98gidt45gul_name_ding.state == STATE_OFF pubnub.message( pubnub, @@ -198,10 +161,7 @@ async def test_doorbell_update_via_pubnub(hass: HomeAssistant) -> None: await hass.async_block_till_done() - binary_sensor_k98gidt45gul_name_image_capture = hass.states.get( - "binary_sensor.k98gidt45gul_name_image_capture" - ) - assert binary_sensor_k98gidt45gul_name_image_capture.state == STATE_ON + assert states.get("binary_sensor.k98gidt45gul_name_image_capture").state == STATE_ON pubnub.message( pubnub, @@ -235,29 +195,19 @@ async def test_doorbell_update_via_pubnub(hass: HomeAssistant) -> None: await hass.async_block_till_done() - binary_sensor_k98gidt45gul_name_motion = hass.states.get( - "binary_sensor.k98gidt45gul_name_motion" - ) - assert binary_sensor_k98gidt45gul_name_motion.state == STATE_ON + assert states.get("binary_sensor.k98gidt45gul_name_motion").state == STATE_ON - binary_sensor_k98gidt45gul_name_ding = hass.states.get( - "binary_sensor.k98gidt45gul_name_doorbell_ding" + assert ( + states.get("binary_sensor.k98gidt45gul_name_doorbell_ding").state == STATE_OFF ) - assert binary_sensor_k98gidt45gul_name_ding.state == STATE_OFF - new_time = dt_util.utcnow() + datetime.timedelta(seconds=40) - native_time = datetime.datetime.now() + datetime.timedelta(seconds=40) - with patch( - "homeassistant.components.august.util._native_datetime", - return_value=native_time, - ): - async_fire_time_changed(hass, new_time) - await hass.async_block_till_done() + freezer.tick(40) + async_fire_time_changed(hass) + await hass.async_block_till_done() - binary_sensor_k98gidt45gul_name_image_capture = hass.states.get( - "binary_sensor.k98gidt45gul_name_image_capture" + assert ( + states.get("binary_sensor.k98gidt45gul_name_image_capture").state == STATE_OFF ) - assert binary_sensor_k98gidt45gul_name_image_capture.state == STATE_OFF pubnub.message( pubnub, @@ -271,37 +221,25 @@ async def test_doorbell_update_via_pubnub(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - binary_sensor_k98gidt45gul_name_ding = hass.states.get( - "binary_sensor.k98gidt45gul_name_doorbell_ding" - ) - assert binary_sensor_k98gidt45gul_name_ding.state == STATE_ON - new_time = dt_util.utcnow() + datetime.timedelta(seconds=40) - native_time = datetime.datetime.now() + datetime.timedelta(seconds=40) - with patch( - "homeassistant.components.august.util._native_datetime", - return_value=native_time, - ): - async_fire_time_changed(hass, new_time) - await hass.async_block_till_done() + assert states.get("binary_sensor.k98gidt45gul_name_doorbell_ding").state == STATE_ON + freezer.tick(40) + async_fire_time_changed(hass) + await hass.async_block_till_done() - binary_sensor_k98gidt45gul_name_ding = hass.states.get( - "binary_sensor.k98gidt45gul_name_doorbell_ding" + assert ( + states.get("binary_sensor.k98gidt45gul_name_doorbell_ding").state == STATE_OFF ) - assert binary_sensor_k98gidt45gul_name_ding.state == STATE_OFF async def test_doorbell_device_registry( - hass: HomeAssistant, device_registry: dr.DeviceRegistry + hass: HomeAssistant, device_registry: dr.DeviceRegistry, snapshot: SnapshotAssertion ) -> None: """Test creation of a lock with doorsense and bridge ands up in the registry.""" doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.offline.json") await _create_august_with_devices(hass, [doorbell_one]) reg_device = device_registry.async_get_device(identifiers={("august", "tmt100")}) - assert reg_device.model == "hydra1" - assert reg_device.name == "tmt100 Name" - assert reg_device.manufacturer == "August Home Inc." - assert reg_device.sw_version == "3.1.0-HYDRC75+201909251139" + assert reg_device == snapshot async def test_door_sense_update_via_pubnub(hass: HomeAssistant) -> None: @@ -314,11 +252,9 @@ async def test_door_sense_update_via_pubnub(hass: HomeAssistant) -> None: config_entry = await _create_august_with_devices( hass, [lock_one], activities=activities, pubnub=pubnub ) + states = hass.states - binary_sensor_online_with_doorsense_name = hass.states.get( - "binary_sensor.online_with_doorsense_name_door" - ) - assert binary_sensor_online_with_doorsense_name.state == STATE_ON + assert states.get("binary_sensor.online_with_doorsense_name_door").state == STATE_ON pubnub.message( pubnub, @@ -330,10 +266,9 @@ async def test_door_sense_update_via_pubnub(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - binary_sensor_online_with_doorsense_name = hass.states.get( - "binary_sensor.online_with_doorsense_name_door" + assert ( + states.get("binary_sensor.online_with_doorsense_name_door").state == STATE_OFF ) - assert binary_sensor_online_with_doorsense_name.state == STATE_OFF pubnub.message( pubnub, @@ -344,33 +279,22 @@ async def test_door_sense_update_via_pubnub(hass: HomeAssistant) -> None: ), ) await hass.async_block_till_done() - binary_sensor_online_with_doorsense_name = hass.states.get( - "binary_sensor.online_with_doorsense_name_door" - ) - assert binary_sensor_online_with_doorsense_name.state == STATE_ON + assert states.get("binary_sensor.online_with_doorsense_name_door").state == STATE_ON async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(seconds=30)) await hass.async_block_till_done() - binary_sensor_online_with_doorsense_name = hass.states.get( - "binary_sensor.online_with_doorsense_name_door" - ) - assert binary_sensor_online_with_doorsense_name.state == STATE_ON + assert states.get("binary_sensor.online_with_doorsense_name_door").state == STATE_ON pubnub.connected = True async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(seconds=30)) await hass.async_block_till_done() - binary_sensor_online_with_doorsense_name = hass.states.get( - "binary_sensor.online_with_doorsense_name_door" - ) - assert binary_sensor_online_with_doorsense_name.state == STATE_ON + assert states.get("binary_sensor.online_with_doorsense_name_door").state == STATE_ON # Ensure pubnub status is always preserved async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(hours=2)) await hass.async_block_till_done() - binary_sensor_online_with_doorsense_name = hass.states.get( - "binary_sensor.online_with_doorsense_name_door" - ) - assert binary_sensor_online_with_doorsense_name.state == STATE_ON + + assert states.get("binary_sensor.online_with_doorsense_name_door").state == STATE_ON pubnub.message( pubnub, @@ -381,17 +305,11 @@ async def test_door_sense_update_via_pubnub(hass: HomeAssistant) -> None: ), ) await hass.async_block_till_done() - binary_sensor_online_with_doorsense_name = hass.states.get( - "binary_sensor.online_with_doorsense_name_door" - ) - assert binary_sensor_online_with_doorsense_name.state == STATE_ON + assert states.get("binary_sensor.online_with_doorsense_name_door").state == STATE_ON async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(hours=4)) await hass.async_block_till_done() - binary_sensor_online_with_doorsense_name = hass.states.get( - "binary_sensor.online_with_doorsense_name_door" - ) - assert binary_sensor_online_with_doorsense_name.state == STATE_ON + assert states.get("binary_sensor.online_with_doorsense_name_door").state == STATE_ON await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() @@ -402,7 +320,10 @@ async def test_create_lock_with_doorbell(hass: HomeAssistant) -> None: lock_one = await _mock_lock_from_fixture(hass, "lock_with_doorbell.online.json") await _create_august_with_devices(hass, [lock_one]) - ding_sensor = hass.states.get( - "binary_sensor.a6697750d607098bae8d6baa11ef8063_name_doorbell_ding" + states = hass.states + assert ( + states.get( + "binary_sensor.a6697750d607098bae8d6baa11ef8063_name_doorbell_ding" + ).state + == STATE_OFF ) - assert ding_sensor.state == STATE_OFF diff --git a/tests/components/august/test_button.py b/tests/components/august/test_button.py index 8ae2bc8a70d..948b59b2286 100644 --- a/tests/components/august/test_button.py +++ b/tests/components/august/test_button.py @@ -20,5 +20,4 @@ async def test_wake_lock(hass: HomeAssistant) -> None: await hass.services.async_call( BUTTON_DOMAIN, SERVICE_PRESS, {ATTR_ENTITY_ID: entity_id}, blocking=True ) - await hass.async_block_till_done() api_instance.async_status_async.assert_called_once() diff --git a/tests/components/august/test_camera.py b/tests/components/august/test_camera.py index 539a26cc30f..5ab7d49c3b8 100644 --- a/tests/components/august/test_camera.py +++ b/tests/components/august/test_camera.py @@ -25,14 +25,10 @@ async def test_create_doorbell( ): await _create_august_with_devices(hass, [doorbell_one], brand=Brand.AUGUST) - camera_k98gidt45gul_name_camera = hass.states.get( - "camera.k98gidt45gul_name_camera" - ) - assert camera_k98gidt45gul_name_camera.state == STATE_IDLE + camera_state = hass.states.get("camera.k98gidt45gul_name_camera") + assert camera_state.state == STATE_IDLE - url = hass.states.get("camera.k98gidt45gul_name_camera").attributes[ - "entity_picture" - ] + url = camera_state.attributes["entity_picture"] client = await hass_client_no_auth() resp = await client.get(url) diff --git a/tests/components/august/test_config_flow.py b/tests/components/august/test_config_flow.py index e0ccee55f10..9902901d29f 100644 --- a/tests/components/august/test_config_flow.py +++ b/tests/components/august/test_config_flow.py @@ -5,7 +5,6 @@ from unittest.mock import patch from yalexs.authenticator_common import ValidationResult from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation -from homeassistant import config_entries from homeassistant.components.august.const import ( CONF_ACCESS_TOKEN_CACHE_FILE, CONF_BRAND, @@ -14,6 +13,7 @@ from homeassistant.components.august.const import ( DOMAIN, VERIFICATION_CODE_KEY, ) +from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -25,7 +25,7 @@ async def test_form(hass: HomeAssistant) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] is FlowResultType.FORM assert result["errors"] == {} @@ -66,7 +66,7 @@ async def test_form(hass: HomeAssistant) -> None: async def test_form_invalid_auth(hass: HomeAssistant) -> None: """Test we handle invalid auth.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) with patch( @@ -90,7 +90,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: async def test_user_unexpected_exception(hass: HomeAssistant) -> None: """Test we handle an unexpected exception.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) with patch( @@ -115,7 +115,7 @@ async def test_user_unexpected_exception(hass: HomeAssistant) -> None: async def test_form_cannot_connect(hass: HomeAssistant) -> None: """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) with patch( @@ -138,7 +138,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: async def test_form_needs_validate(hass: HomeAssistant) -> None: """Test we present validation when we need to validate.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) with ( @@ -367,7 +367,7 @@ async def test_switching_brands(hass: HomeAssistant) -> None: ) entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] is FlowResultType.FORM assert result["errors"] == {} diff --git a/tests/components/august/test_event.py b/tests/components/august/test_event.py index 61b7560f462..0bb482c5b89 100644 --- a/tests/components/august/test_event.py +++ b/tests/components/august/test_event.py @@ -1,13 +1,12 @@ """The event tests for the august.""" -import datetime -from unittest.mock import Mock, patch +from unittest.mock import Mock +from freezegun.api import FrozenDateTimeFactory from yalexs.pubnub_async import AugustPubNub from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import HomeAssistant -import homeassistant.util.dt as dt_util from .mocks import ( _create_august_with_devices, @@ -45,7 +44,9 @@ async def test_create_doorbell_offline(hass: HomeAssistant) -> None: assert doorbell_state.state == STATE_UNAVAILABLE -async def test_create_doorbell_with_motion(hass: HomeAssistant) -> None: +async def test_create_doorbell_with_motion( + hass: HomeAssistant, freezer: FrozenDateTimeFactory +) -> None: """Test creation of a doorbell.""" doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.json") activities = await _mock_activities_from_fixture( @@ -61,19 +62,16 @@ async def test_create_doorbell_with_motion(hass: HomeAssistant) -> None: assert doorbell_state is not None assert doorbell_state.state == STATE_UNKNOWN - new_time = dt_util.utcnow() + datetime.timedelta(seconds=40) - native_time = datetime.datetime.now() + datetime.timedelta(seconds=40) - with patch( - "homeassistant.components.august.util._native_datetime", - return_value=native_time, - ): - async_fire_time_changed(hass, new_time) - await hass.async_block_till_done() + freezer.tick(40) + async_fire_time_changed(hass) + await hass.async_block_till_done() motion_state = hass.states.get("event.k98gidt45gul_name_motion") assert motion_state.state == isotime -async def test_doorbell_update_via_pubnub(hass: HomeAssistant) -> None: +async def test_doorbell_update_via_pubnub( + hass: HomeAssistant, freezer: FrozenDateTimeFactory +) -> None: """Test creation of a doorbell that can be updated via pubnub.""" doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.json") pubnub = AugustPubNub() @@ -125,14 +123,9 @@ async def test_doorbell_update_via_pubnub(hass: HomeAssistant) -> None: assert motion_state.state != STATE_UNKNOWN isotime = motion_state.state - new_time = dt_util.utcnow() + datetime.timedelta(seconds=40) - native_time = datetime.datetime.now() + datetime.timedelta(seconds=40) - with patch( - "homeassistant.components.august.util._native_datetime", - return_value=native_time, - ): - async_fire_time_changed(hass, new_time) - await hass.async_block_till_done() + freezer.tick(40) + async_fire_time_changed(hass) + await hass.async_block_till_done() motion_state = hass.states.get("event.k98gidt45gul_name_motion") assert motion_state is not None @@ -155,14 +148,9 @@ async def test_doorbell_update_via_pubnub(hass: HomeAssistant) -> None: assert doorbell_state.state != STATE_UNKNOWN isotime = motion_state.state - new_time = dt_util.utcnow() + datetime.timedelta(seconds=40) - native_time = datetime.datetime.now() + datetime.timedelta(seconds=40) - with patch( - "homeassistant.components.august.util._native_datetime", - return_value=native_time, - ): - async_fire_time_changed(hass, new_time) - await hass.async_block_till_done() + freezer.tick(40) + async_fire_time_changed(hass) + await hass.async_block_till_done() doorbell_state = hass.states.get("event.k98gidt45gul_name_doorbell") assert doorbell_state is not None diff --git a/tests/components/august/test_init.py b/tests/components/august/test_init.py index 8261e32d668..954436f209a 100644 --- a/tests/components/august/test_init.py +++ b/tests/components/august/test_init.py @@ -122,16 +122,16 @@ async def test_unlock_throws_august_api_http_error(hass: HomeAssistant) -> None: "unlock_return_activities": _unlock_return_activities_side_effect }, ) - last_err = None data = {ATTR_ENTITY_ID: "lock.a6697750d607098bae8d6baa11ef8063_name"} - try: + + with pytest.raises( + HomeAssistantError, + match=( + "A6697750D607098BAE8D6BAA11EF8063 Name: This should bubble up as its user" + " consumable" + ), + ): await hass.services.async_call(LOCK_DOMAIN, SERVICE_UNLOCK, data, blocking=True) - except HomeAssistantError as err: - last_err = err - assert str(last_err) == ( - "A6697750D607098BAE8D6BAA11EF8063 Name: This should bubble up as its user" - " consumable" - ) async def test_lock_throws_august_api_http_error(hass: HomeAssistant) -> None: @@ -152,16 +152,15 @@ async def test_lock_throws_august_api_http_error(hass: HomeAssistant) -> None: "lock_return_activities": _lock_return_activities_side_effect }, ) - last_err = None data = {ATTR_ENTITY_ID: "lock.a6697750d607098bae8d6baa11ef8063_name"} - try: + with pytest.raises( + HomeAssistantError, + match=( + "A6697750D607098BAE8D6BAA11EF8063 Name: This should bubble up as its user" + " consumable" + ), + ): await hass.services.async_call(LOCK_DOMAIN, SERVICE_LOCK, data, blocking=True) - except HomeAssistantError as err: - last_err = err - assert str(last_err) == ( - "A6697750D607098BAE8D6BAA11EF8063 Name: This should bubble up as its user" - " consumable" - ) async def test_open_throws_hass_service_not_supported_error( @@ -371,6 +370,7 @@ async def test_load_unload(hass: HomeAssistant) -> None: await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.NOT_LOADED async def test_load_triggers_ble_discovery( diff --git a/tests/components/august/test_lock.py b/tests/components/august/test_lock.py index 8bb71826d24..e786cebf3e1 100644 --- a/tests/components/august/test_lock.py +++ b/tests/components/august/test_lock.py @@ -6,6 +6,7 @@ from unittest.mock import Mock from aiohttp import ClientResponseError from freezegun.api import FrozenDateTimeFactory import pytest +from syrupy import SnapshotAssertion from yalexs.manager.activity import INITIAL_LOCK_RESYNC_TIME from yalexs.pubnub_async import AugustPubNub @@ -43,7 +44,7 @@ from tests.common import async_fire_time_changed async def test_lock_device_registry( - hass: HomeAssistant, device_registry: dr.DeviceRegistry + hass: HomeAssistant, device_registry: dr.DeviceRegistry, snapshot: SnapshotAssertion ) -> None: """Test creation of a lock with doorsense and bridge ands up in the registry.""" lock_one = await _mock_doorsense_enabled_august_lock_detail(hass) @@ -52,10 +53,7 @@ async def test_lock_device_registry( reg_device = device_registry.async_get_device( identifiers={("august", "online_with_doorsense")} ) - assert reg_device.model == "AUG-MD01" - assert reg_device.sw_version == "undefined-4.3.0-1.8.14" - assert reg_device.name == "online_with_doorsense Name" - assert reg_device.manufacturer == "August Home Inc." + assert reg_device == snapshot async def test_lock_changed_by(hass: HomeAssistant) -> None: @@ -65,14 +63,10 @@ async def test_lock_changed_by(hass: HomeAssistant) -> None: activities = await _mock_activities_from_fixture(hass, "get_activity.lock.json") await _create_august_with_devices(hass, [lock_one], activities=activities) - lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") + lock_state = hass.states.get("lock.online_with_doorsense_name") - assert lock_online_with_doorsense_name.state == STATE_LOCKED - - assert ( - lock_online_with_doorsense_name.attributes.get("changed_by") - == "Your favorite elven princess" - ) + assert lock_state.state == STATE_LOCKED + assert lock_state.attributes["changed_by"] == "Your favorite elven princess" async def test_state_locking(hass: HomeAssistant) -> None: @@ -82,9 +76,7 @@ async def test_state_locking(hass: HomeAssistant) -> None: activities = await _mock_activities_from_fixture(hass, "get_activity.locking.json") await _create_august_with_devices(hass, [lock_one], activities=activities) - lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") - - assert lock_online_with_doorsense_name.state == STATE_LOCKING + assert hass.states.get("lock.online_with_doorsense_name").state == STATE_LOCKING async def test_state_unlocking(hass: HomeAssistant) -> None: @@ -96,9 +88,7 @@ async def test_state_unlocking(hass: HomeAssistant) -> None: ) await _create_august_with_devices(hass, [lock_one], activities=activities) - lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") - - assert lock_online_with_doorsense_name.state == STATE_UNLOCKING + assert hass.states.get("lock.online_with_doorsense_name").state == STATE_UNLOCKING async def test_state_jammed(hass: HomeAssistant) -> None: @@ -108,9 +98,7 @@ async def test_state_jammed(hass: HomeAssistant) -> None: activities = await _mock_activities_from_fixture(hass, "get_activity.jammed.json") await _create_august_with_devices(hass, [lock_one], activities=activities) - lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") - - assert lock_online_with_doorsense_name.state == STATE_JAMMED + assert hass.states.get("lock.online_with_doorsense_name").state == STATE_JAMMED async def test_one_lock_operation( @@ -119,35 +107,27 @@ async def test_one_lock_operation( """Test creation of a lock with doorsense and bridge.""" lock_one = await _mock_doorsense_enabled_august_lock_detail(hass) await _create_august_with_devices(hass, [lock_one]) + states = hass.states - lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") + lock_state = states.get("lock.online_with_doorsense_name") - assert lock_online_with_doorsense_name.state == STATE_LOCKED + assert lock_state.state == STATE_LOCKED - assert lock_online_with_doorsense_name.attributes.get("battery_level") == 92 - assert ( - lock_online_with_doorsense_name.attributes.get("friendly_name") - == "online_with_doorsense Name" - ) + assert lock_state.attributes["battery_level"] == 92 + assert lock_state.attributes["friendly_name"] == "online_with_doorsense Name" data = {ATTR_ENTITY_ID: "lock.online_with_doorsense_name"} await hass.services.async_call(LOCK_DOMAIN, SERVICE_UNLOCK, data, blocking=True) - await hass.async_block_till_done() - lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") - assert lock_online_with_doorsense_name.state == STATE_UNLOCKED + lock_state = states.get("lock.online_with_doorsense_name") + assert lock_state.state == STATE_UNLOCKED - assert lock_online_with_doorsense_name.attributes.get("battery_level") == 92 - assert ( - lock_online_with_doorsense_name.attributes.get("friendly_name") - == "online_with_doorsense Name" - ) + assert lock_state.attributes["battery_level"] == 92 + assert lock_state.attributes["friendly_name"] == "online_with_doorsense Name" await hass.services.async_call(LOCK_DOMAIN, SERVICE_LOCK, data, blocking=True) - await hass.async_block_till_done() - lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") - assert lock_online_with_doorsense_name.state == STATE_LOCKED + assert states.get("lock.online_with_doorsense_name").state == STATE_LOCKED # No activity means it will be unavailable until the activity feed has data lock_operator_sensor = entity_registry.async_get( @@ -155,8 +135,7 @@ async def test_one_lock_operation( ) assert lock_operator_sensor assert ( - hass.states.get("sensor.online_with_doorsense_name_operator").state - == STATE_UNKNOWN + states.get("sensor.online_with_doorsense_name_operator").state == STATE_UNKNOWN ) @@ -170,7 +149,6 @@ async def test_open_lock_operation(hass: HomeAssistant) -> None: data = {ATTR_ENTITY_ID: "lock.online_with_unlatch_name"} await hass.services.async_call(LOCK_DOMAIN, SERVICE_OPEN, data, blocking=True) - await hass.async_block_till_done() lock_online_with_unlatch_name = hass.states.get("lock.online_with_unlatch_name") assert lock_online_with_unlatch_name.state == STATE_UNLOCKED @@ -189,12 +167,10 @@ async def test_open_lock_operation_pubnub_connected( await _create_august_with_devices(hass, [lock_with_unlatch], pubnub=pubnub) pubnub.connected = True - lock_online_with_unlatch_name = hass.states.get("lock.online_with_unlatch_name") - assert lock_online_with_unlatch_name.state == STATE_LOCKED + assert hass.states.get("lock.online_with_unlatch_name").state == STATE_LOCKED data = {ATTR_ENTITY_ID: "lock.online_with_unlatch_name"} await hass.services.async_call(LOCK_DOMAIN, SERVICE_OPEN, data, blocking=True) - await hass.async_block_till_done() pubnub.message( pubnub, @@ -209,8 +185,7 @@ async def test_open_lock_operation_pubnub_connected( await hass.async_block_till_done() await hass.async_block_till_done() - lock_online_with_unlatch_name = hass.states.get("lock.online_with_unlatch_name") - assert lock_online_with_unlatch_name.state == STATE_UNLOCKED + assert hass.states.get("lock.online_with_unlatch_name").state == STATE_UNLOCKED await hass.async_block_till_done() @@ -227,19 +202,15 @@ async def test_one_lock_operation_pubnub_connected( await _create_august_with_devices(hass, [lock_one], pubnub=pubnub) pubnub.connected = True - lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") + lock_state = hass.states.get("lock.online_with_doorsense_name") - assert lock_online_with_doorsense_name.state == STATE_LOCKED + assert lock_state.state == STATE_LOCKED - assert lock_online_with_doorsense_name.attributes.get("battery_level") == 92 - assert ( - lock_online_with_doorsense_name.attributes.get("friendly_name") - == "online_with_doorsense Name" - ) + assert lock_state.attributes["battery_level"] == 92 + assert lock_state.attributes["friendly_name"] == "online_with_doorsense Name" data = {ATTR_ENTITY_ID: "lock.online_with_doorsense_name"} await hass.services.async_call(LOCK_DOMAIN, SERVICE_UNLOCK, data, blocking=True) - await hass.async_block_till_done() pubnub.message( pubnub, @@ -254,17 +225,13 @@ async def test_one_lock_operation_pubnub_connected( await hass.async_block_till_done() await hass.async_block_till_done() - lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") - assert lock_online_with_doorsense_name.state == STATE_UNLOCKED + lock_state = hass.states.get("lock.online_with_doorsense_name") + assert lock_state.state == STATE_UNLOCKED - assert lock_online_with_doorsense_name.attributes.get("battery_level") == 92 - assert ( - lock_online_with_doorsense_name.attributes.get("friendly_name") - == "online_with_doorsense Name" - ) + assert lock_state.attributes["battery_level"] == 92 + assert lock_state.attributes["friendly_name"] == "online_with_doorsense Name" await hass.services.async_call(LOCK_DOMAIN, SERVICE_LOCK, data, blocking=True) - await hass.async_block_till_done() pubnub.message( pubnub, @@ -279,8 +246,8 @@ async def test_one_lock_operation_pubnub_connected( await hass.async_block_till_done() await hass.async_block_till_done() - lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") - assert lock_online_with_doorsense_name.state == STATE_LOCKED + lock_state = hass.states.get("lock.online_with_doorsense_name") + assert lock_state.state == STATE_LOCKED # No activity means it will be unavailable until the activity feed has data lock_operator_sensor = entity_registry.async_get( @@ -306,8 +273,8 @@ async def test_one_lock_operation_pubnub_connected( ) await hass.async_block_till_done() - lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") - assert lock_online_with_doorsense_name.state == STATE_UNLOCKED + lock_state = hass.states.get("lock.online_with_doorsense_name") + assert lock_state.state == STATE_UNLOCKED async def test_lock_jammed(hass: HomeAssistant) -> None: @@ -325,22 +292,18 @@ async def test_lock_jammed(hass: HomeAssistant) -> None: }, ) - lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") + lock_state = hass.states.get("lock.online_with_doorsense_name") - assert lock_online_with_doorsense_name.state == STATE_LOCKED + assert lock_state.state == STATE_LOCKED - assert lock_online_with_doorsense_name.attributes.get("battery_level") == 92 - assert ( - lock_online_with_doorsense_name.attributes.get("friendly_name") - == "online_with_doorsense Name" - ) + assert lock_state.attributes["battery_level"] == 92 + assert lock_state.attributes["friendly_name"] == "online_with_doorsense Name" data = {ATTR_ENTITY_ID: "lock.online_with_doorsense_name"} await hass.services.async_call(LOCK_DOMAIN, SERVICE_UNLOCK, data, blocking=True) - await hass.async_block_till_done() - lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") - assert lock_online_with_doorsense_name.state == STATE_JAMMED + lock_state = hass.states.get("lock.online_with_doorsense_name") + assert lock_state.state == STATE_JAMMED async def test_lock_throws_exception_on_unknown_status_code( @@ -360,15 +323,12 @@ async def test_lock_throws_exception_on_unknown_status_code( }, ) - lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") + lock_state = hass.states.get("lock.online_with_doorsense_name") - assert lock_online_with_doorsense_name.state == STATE_LOCKED + assert lock_state.state == STATE_LOCKED - assert lock_online_with_doorsense_name.attributes.get("battery_level") == 92 - assert ( - lock_online_with_doorsense_name.attributes.get("friendly_name") - == "online_with_doorsense Name" - ) + assert lock_state.attributes["battery_level"] == 92 + assert lock_state.attributes["friendly_name"] == "online_with_doorsense Name" data = {ATTR_ENTITY_ID: "lock.online_with_doorsense_name"} with pytest.raises(ClientResponseError): @@ -383,9 +343,7 @@ async def test_one_lock_unknown_state(hass: HomeAssistant) -> None: ) await _create_august_with_devices(hass, [lock_one]) - lock_brokenid_name = hass.states.get("lock.brokenid_name") - - assert lock_brokenid_name.state == STATE_UNKNOWN + assert hass.states.get("lock.brokenid_name").state == STATE_UNKNOWN async def test_lock_bridge_offline(hass: HomeAssistant) -> None: @@ -397,9 +355,7 @@ async def test_lock_bridge_offline(hass: HomeAssistant) -> None: ) await _create_august_with_devices(hass, [lock_one], activities=activities) - lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") - - assert lock_online_with_doorsense_name.state == STATE_UNAVAILABLE + assert hass.states.get("lock.online_with_doorsense_name").state == STATE_UNAVAILABLE async def test_lock_bridge_online(hass: HomeAssistant) -> None: @@ -411,14 +367,13 @@ async def test_lock_bridge_online(hass: HomeAssistant) -> None: ) await _create_august_with_devices(hass, [lock_one], activities=activities) - lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") - - assert lock_online_with_doorsense_name.state == STATE_LOCKED + assert hass.states.get("lock.online_with_doorsense_name").state == STATE_LOCKED async def test_lock_update_via_pubnub(hass: HomeAssistant) -> None: """Test creation of a lock with doorsense and bridge.""" lock_one = await _mock_doorsense_enabled_august_lock_detail(hass) + states = hass.states assert lock_one.pubsub_channel == "pubsub" pubnub = AugustPubNub() @@ -428,9 +383,7 @@ async def test_lock_update_via_pubnub(hass: HomeAssistant) -> None: ) pubnub.connected = True - lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") - - assert lock_online_with_doorsense_name.state == STATE_LOCKED + assert states.get("lock.online_with_doorsense_name").state == STATE_LOCKED pubnub.message( pubnub, @@ -446,8 +399,7 @@ async def test_lock_update_via_pubnub(hass: HomeAssistant) -> None: await hass.async_block_till_done() await hass.async_block_till_done() - lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") - assert lock_online_with_doorsense_name.state == STATE_UNLOCKING + assert states.get("lock.online_with_doorsense_name").state == STATE_UNLOCKING pubnub.message( pubnub, @@ -463,25 +415,21 @@ async def test_lock_update_via_pubnub(hass: HomeAssistant) -> None: await hass.async_block_till_done() await hass.async_block_till_done() - lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") - assert lock_online_with_doorsense_name.state == STATE_LOCKING + assert states.get("lock.online_with_doorsense_name").state == STATE_LOCKING async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(seconds=30)) await hass.async_block_till_done() - lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") - assert lock_online_with_doorsense_name.state == STATE_LOCKING + assert hass.states.get("lock.online_with_doorsense_name").state == STATE_LOCKING pubnub.connected = True async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(seconds=30)) await hass.async_block_till_done() - lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") - assert lock_online_with_doorsense_name.state == STATE_LOCKING + assert states.get("lock.online_with_doorsense_name").state == STATE_LOCKING # Ensure pubnub status is always preserved async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(hours=2)) await hass.async_block_till_done() - lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") - assert lock_online_with_doorsense_name.state == STATE_LOCKING + assert states.get("lock.online_with_doorsense_name").state == STATE_LOCKING pubnub.message( pubnub, @@ -496,13 +444,11 @@ async def test_lock_update_via_pubnub(hass: HomeAssistant) -> None: await hass.async_block_till_done() await hass.async_block_till_done() - lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") - assert lock_online_with_doorsense_name.state == STATE_UNLOCKING + assert states.get("lock.online_with_doorsense_name").state == STATE_UNLOCKING async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(hours=4)) await hass.async_block_till_done() - lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") - assert lock_online_with_doorsense_name.state == STATE_UNLOCKING + assert states.get("lock.online_with_doorsense_name").state == STATE_UNLOCKING await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/august/test_sensor.py b/tests/components/august/test_sensor.py index 67223e9dff0..2d72d287ce3 100644 --- a/tests/components/august/test_sensor.py +++ b/tests/components/august/test_sensor.py @@ -28,13 +28,9 @@ async def test_create_doorbell(hass: HomeAssistant) -> None: doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.json") await _create_august_with_devices(hass, [doorbell_one]) - sensor_k98gidt45gul_name_battery = hass.states.get( - "sensor.k98gidt45gul_name_battery" - ) - assert sensor_k98gidt45gul_name_battery.state == "96" - assert ( - sensor_k98gidt45gul_name_battery.attributes["unit_of_measurement"] == PERCENTAGE - ) + battery_state = hass.states.get("sensor.k98gidt45gul_name_battery") + assert battery_state.state == "96" + assert battery_state.attributes["unit_of_measurement"] == PERCENTAGE async def test_create_doorbell_offline( @@ -44,9 +40,9 @@ async def test_create_doorbell_offline( doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.offline.json") await _create_august_with_devices(hass, [doorbell_one]) - sensor_tmt100_name_battery = hass.states.get("sensor.tmt100_name_battery") - assert sensor_tmt100_name_battery.state == "81" - assert sensor_tmt100_name_battery.attributes["unit_of_measurement"] == PERCENTAGE + battery_state = hass.states.get("sensor.tmt100_name_battery") + assert battery_state.state == "81" + assert battery_state.attributes["unit_of_measurement"] == PERCENTAGE entry = entity_registry.async_get("sensor.tmt100_name_battery") assert entry @@ -60,8 +56,7 @@ async def test_create_doorbell_hardwired(hass: HomeAssistant) -> None: ) await _create_august_with_devices(hass, [doorbell_one]) - sensor_tmt100_name_battery = hass.states.get("sensor.tmt100_name_battery") - assert sensor_tmt100_name_battery is None + assert hass.states.get("sensor.tmt100_name_battery") is None async def test_create_lock_with_linked_keypad( @@ -71,25 +66,21 @@ async def test_create_lock_with_linked_keypad( lock_one = await _mock_lock_from_fixture(hass, "get_lock.doorsense_init.json") await _create_august_with_devices(hass, [lock_one]) - sensor_a6697750d607098bae8d6baa11ef8063_name_battery = hass.states.get( + battery_state = hass.states.get( "sensor.a6697750d607098bae8d6baa11ef8063_name_battery" ) - assert sensor_a6697750d607098bae8d6baa11ef8063_name_battery.state == "88" - assert ( - sensor_a6697750d607098bae8d6baa11ef8063_name_battery.attributes[ - "unit_of_measurement" - ] - == PERCENTAGE - ) + assert battery_state.state == "88" + assert battery_state.attributes["unit_of_measurement"] == PERCENTAGE + entry = entity_registry.async_get( "sensor.a6697750d607098bae8d6baa11ef8063_name_battery" ) assert entry assert entry.unique_id == "A6697750D607098BAE8D6BAA11EF8063_device_battery" - state = hass.states.get("sensor.front_door_lock_keypad_battery") - assert state.state == "62" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE + keypad_battery_state = hass.states.get("sensor.front_door_lock_keypad_battery") + assert keypad_battery_state.state == "62" + assert keypad_battery_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE entry = entity_registry.async_get("sensor.front_door_lock_keypad_battery") assert entry assert entry.unique_id == "5bc65c24e6ef2a263e1450a8_linked_keypad_battery" @@ -101,42 +92,32 @@ async def test_create_lock_with_low_battery_linked_keypad( """Test creation of a lock with a linked keypad that both have a battery.""" lock_one = await _mock_lock_from_fixture(hass, "get_lock.low_keypad_battery.json") await _create_august_with_devices(hass, [lock_one]) + states = hass.states - sensor_a6697750d607098bae8d6baa11ef8063_name_battery = hass.states.get( - "sensor.a6697750d607098bae8d6baa11ef8063_name_battery" - ) - assert sensor_a6697750d607098bae8d6baa11ef8063_name_battery.state == "88" - assert ( - sensor_a6697750d607098bae8d6baa11ef8063_name_battery.attributes[ - "unit_of_measurement" - ] - == PERCENTAGE - ) + battery_state = states.get("sensor.a6697750d607098bae8d6baa11ef8063_name_battery") + assert battery_state.state == "88" + assert battery_state.attributes["unit_of_measurement"] == PERCENTAGE entry = entity_registry.async_get( "sensor.a6697750d607098bae8d6baa11ef8063_name_battery" ) assert entry assert entry.unique_id == "A6697750D607098BAE8D6BAA11EF8063_device_battery" - state = hass.states.get("sensor.front_door_lock_keypad_battery") - assert state.state == "10" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE + keypad_battery_state = states.get("sensor.front_door_lock_keypad_battery") + assert keypad_battery_state.state == "10" + assert keypad_battery_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE entry = entity_registry.async_get("sensor.front_door_lock_keypad_battery") assert entry assert entry.unique_id == "5bc65c24e6ef2a263e1450a8_linked_keypad_battery" # No activity means it will be unavailable until someone unlocks/locks it - lock_operator_sensor = entity_registry.async_get( + operator_entry = entity_registry.async_get( "sensor.a6697750d607098bae8d6baa11ef8063_name_operator" ) - assert ( - lock_operator_sensor.unique_id - == "A6697750D607098BAE8D6BAA11EF8063_lock_operator" - ) - assert ( - hass.states.get("sensor.a6697750d607098bae8d6baa11ef8063_name_operator").state - == STATE_UNKNOWN - ) + assert operator_entry.unique_id == "A6697750D607098BAE8D6BAA11EF8063_lock_operator" + + operator_state = states.get("sensor.a6697750d607098bae8d6baa11ef8063_name_operator") + assert operator_state.state == STATE_UNKNOWN async def test_lock_operator_bluetooth( From bd2be0a763791a196833c656f64390ccbe59f244 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Fri, 30 Aug 2024 13:09:10 +0200 Subject: [PATCH 125/271] Optimize hassfest image (#124855) * Optimize hassfest docker image * Adjust CI * Use dynamic uv version * Remove workaround --- .github/workflows/builder.yml | 10 +- script/hassfest/docker.py | 134 +++++++++++++++--- script/hassfest/docker/Dockerfile | 35 +++-- .../hassfest/docker/Dockerfile.dockerignore | 8 ++ script/hassfest/docker/entrypoint.sh | 22 +-- 5 files changed, 163 insertions(+), 46 deletions(-) create mode 100644 script/hassfest/docker/Dockerfile.dockerignore diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index d206f8fe8c8..65ad0e240bc 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -491,7 +491,7 @@ jobs: packages: write attestations: write id-token: write - needs: ["init", "build_base"] + needs: ["init"] if: github.repository_owner == 'home-assistant' env: HASSFEST_IMAGE_NAME: ghcr.io/home-assistant/hassfest @@ -510,8 +510,8 @@ jobs: - name: Build Docker image uses: docker/build-push-action@5cd11c3a4ced054e52742c5fd54dca954e0edd85 # v6.7.0 with: - context: ./script/hassfest/docker - build-args: BASE_IMAGE=ghcr.io/home-assistant/amd64-homeassistant:${{ needs.init.outputs.version }} + context: . # So action will not pull the repository again + file: ./script/hassfest/docker/Dockerfile load: true tags: ${{ env.HASSFEST_IMAGE_TAG }} @@ -523,8 +523,8 @@ jobs: id: push uses: docker/build-push-action@5cd11c3a4ced054e52742c5fd54dca954e0edd85 # v6.7.0 with: - context: ./script/hassfest/docker - build-args: BASE_IMAGE=ghcr.io/home-assistant/amd64-homeassistant:${{ needs.init.outputs.version }} + context: . # So action will not pull the repository again + file: ./script/hassfest/docker/Dockerfile push: true tags: ${{ env.HASSFEST_IMAGE_TAG }},${{ env.HASSFEST_IMAGE_NAME }}:latest diff --git a/script/hassfest/docker.py b/script/hassfest/docker.py index e38a238be7d..6e39a5c350b 100644 --- a/script/hassfest/docker.py +++ b/script/hassfest/docker.py @@ -1,7 +1,12 @@ """Generate and validate the dockerfile.""" +from dataclasses import dataclass +from pathlib import Path + from homeassistant import core +from homeassistant.const import Platform from homeassistant.util import executor, thread +from script.gen_requirements_all import gather_recursive_requirements from .model import Config, Integration from .requirements import PACKAGE_REGEX, PIP_VERSION_RANGE_SEPARATOR @@ -20,7 +25,7 @@ ENV \ ARG QEMU_CPU # Install uv -RUN pip3 install uv=={uv_version} +RUN pip3 install uv=={uv} WORKDIR /usr/src @@ -61,30 +66,105 @@ COPY rootfs / WORKDIR /config """ +_HASSFEST_TEMPLATE = r"""# Automatically generated by hassfest. +# +# To update, run python3 -m script.hassfest -p docker +FROM python:alpine3.20 -def _get_uv_version() -> str: - with open("requirements_test.txt") as fp: +ENV \ + UV_SYSTEM_PYTHON=true \ + UV_EXTRA_INDEX_URL="https://wheels.home-assistant.io/musllinux-index/" + +SHELL ["/bin/sh", "-o", "pipefail", "-c"] +ENTRYPOINT ["/usr/src/homeassistant/script/hassfest/docker/entrypoint.sh"] +WORKDIR "/github/workspace" + +# Install uv +COPY --from=ghcr.io/astral-sh/uv:{uv} /uv /bin/uv + +COPY . /usr/src/homeassistant + +RUN \ + # Required for PyTurboJPEG + apk add --no-cache libturbojpeg \ + && cd /usr/src/homeassistant \ + && uv pip install \ + --no-build \ + --no-cache \ + -c homeassistant/package_constraints.txt \ + -r requirements.txt \ + stdlib-list==0.10.0 pipdeptree=={pipdeptree} tqdm=={tqdm} ruff=={ruff} \ + {required_components_packages} + +LABEL "name"="hassfest" +LABEL "maintainer"="Home Assistant " + +LABEL "com.github.actions.name"="hassfest" +LABEL "com.github.actions.description"="Run hassfest to validate standalone integration repositories" +LABEL "com.github.actions.icon"="terminal" +LABEL "com.github.actions.color"="gray-dark" +""" + + +def _get_package_versions(file: str, packages: set[str]) -> dict[str, str]: + package_versions: dict[str, str] = {} + with open(file, encoding="UTF-8") as fp: for _, line in enumerate(fp): + if package_versions.keys() == packages: + return package_versions + if match := PACKAGE_REGEX.match(line): pkg, sep, version = match.groups() - if pkg != "uv": + if pkg not in packages: continue if sep != "==" or not version: raise RuntimeError( - 'Requirement uv need to be pinned "uv==".' + f'Requirement {pkg} need to be pinned "{pkg}==".' ) for part in version.split(";", 1)[0].split(","): version_part = PIP_VERSION_RANGE_SEPARATOR.match(part) if version_part: - return version_part.group(2) + package_versions[pkg] = version_part.group(2) + break - raise RuntimeError("Invalid uv requirement in requirements_test.txt") + if package_versions.keys() == packages: + return package_versions + + raise RuntimeError("At least one package was not found in the requirements file.") -def _generate_dockerfile() -> str: +@dataclass +class File: + """File.""" + + content: str + path: Path + + +def _generate_hassfest_dockerimage( + config: Config, timeout: int, package_versions: dict[str, str] +) -> File: + packages = set() + already_checked_domains = set() + for platform in Platform: + packages.update( + gather_recursive_requirements(platform.value, already_checked_domains) + ) + + return File( + _HASSFEST_TEMPLATE.format( + timeout=timeout, + required_components_packages=" ".join(sorted(packages)), + **package_versions, + ), + config.root / "script/hassfest/docker/Dockerfile", + ) + + +def _generate_files(config: Config) -> list[File]: timeout = ( core.STOPPING_STAGE_SHUTDOWN_TIMEOUT + core.STOP_STAGE_SHUTDOWN_TIMEOUT @@ -93,27 +173,39 @@ def _generate_dockerfile() -> str: + executor.EXECUTOR_SHUTDOWN_TIMEOUT + thread.THREADING_SHUTDOWN_TIMEOUT + 10 + ) * 1000 + + package_versions = _get_package_versions( + "requirements_test.txt", {"pipdeptree", "tqdm", "uv"} ) - return DOCKERFILE_TEMPLATE.format( - timeout=timeout * 1000, uv_version=_get_uv_version() + package_versions |= _get_package_versions( + "requirements_test_pre_commit.txt", {"ruff"} ) + return [ + File( + DOCKERFILE_TEMPLATE.format(timeout=timeout, **package_versions), + config.root / "Dockerfile", + ), + _generate_hassfest_dockerimage(config, timeout, package_versions), + ] + def validate(integrations: dict[str, Integration], config: Config) -> None: """Validate dockerfile.""" - dockerfile_content = _generate_dockerfile() - config.cache["dockerfile"] = dockerfile_content + docker_files = _generate_files(config) + config.cache["docker"] = docker_files - dockerfile_path = config.root / "Dockerfile" - if dockerfile_path.read_text() != dockerfile_content: - config.add_error( - "docker", - "File Dockerfile is not up to date. Run python3 -m script.hassfest", - fixable=True, - ) + for file in docker_files: + if file.content != file.path.read_text(): + config.add_error( + "docker", + f"File {file.path} is not up to date. Run python3 -m script.hassfest", + fixable=True, + ) def generate(integrations: dict[str, Integration], config: Config) -> None: """Generate dockerfile.""" - dockerfile_path = config.root / "Dockerfile" - dockerfile_path.write_text(config.cache["dockerfile"]) + for file in _generate_files(config): + file.path.write_text(file.content) diff --git a/script/hassfest/docker/Dockerfile b/script/hassfest/docker/Dockerfile index 8921d92307e..4fc60c0c621 100644 --- a/script/hassfest/docker/Dockerfile +++ b/script/hassfest/docker/Dockerfile @@ -1,17 +1,32 @@ -ARG BASE_IMAGE=ghcr.io/home-assistant/home-assistant:beta -FROM $BASE_IMAGE +# Automatically generated by hassfest. +# +# To update, run python3 -m script.hassfest -p docker +FROM python:alpine3.20 -SHELL ["/bin/bash", "-o", "pipefail", "-c"] +ENV \ + UV_SYSTEM_PYTHON=true \ + UV_EXTRA_INDEX_URL="https://wheels.home-assistant.io/musllinux-index/" -COPY entrypoint.sh /entrypoint.sh +SHELL ["/bin/sh", "-o", "pipefail", "-c"] +ENTRYPOINT ["/usr/src/homeassistant/script/hassfest/docker/entrypoint.sh"] +WORKDIR "/github/workspace" + +# Install uv +COPY --from=ghcr.io/astral-sh/uv:0.2.27 /uv /bin/uv + +COPY . /usr/src/homeassistant RUN \ - uv pip install stdlib-list==0.10.0 \ - $(grep -e "^pipdeptree" -e "^tqdm" /usr/src/homeassistant/requirements_test.txt) \ - $(grep -e "^ruff" /usr/src/homeassistant/requirements_test_pre_commit.txt) - -WORKDIR "/github/workspace" -ENTRYPOINT ["/entrypoint.sh"] + # Required for PyTurboJPEG + apk add --no-cache libturbojpeg \ + && cd /usr/src/homeassistant \ + && uv pip install \ + --no-build \ + --no-cache \ + -c homeassistant/package_constraints.txt \ + -r requirements.txt \ + stdlib-list==0.10.0 pipdeptree==2.23.1 tqdm==4.66.4 ruff==0.6.2 \ + PyTurboJPEG==1.7.5 ha-ffmpeg==3.2.0 hassil==1.7.4 home-assistant-intents==2024.8.29 mutagen==1.47.0 LABEL "name"="hassfest" LABEL "maintainer"="Home Assistant " diff --git a/script/hassfest/docker/Dockerfile.dockerignore b/script/hassfest/docker/Dockerfile.dockerignore new file mode 100644 index 00000000000..75ed4f0e5d3 --- /dev/null +++ b/script/hassfest/docker/Dockerfile.dockerignore @@ -0,0 +1,8 @@ +# Ignore everything except the specified files +* + +!homeassistant/ +!requirements.txt +!script/ +script/hassfest/docker/ +!script/hassfest/docker/entrypoint.sh diff --git a/script/hassfest/docker/entrypoint.sh b/script/hassfest/docker/entrypoint.sh index 33330f63161..7b75eb186d2 100755 --- a/script/hassfest/docker/entrypoint.sh +++ b/script/hassfest/docker/entrypoint.sh @@ -1,16 +1,18 @@ -#!/usr/bin/env bashio -declare -a integrations -declare integration_path +#!/bin/sh -shopt -s globstar nullglob -for manifest in **/manifest.json; do +integrations="" +integration_path="" + +# Enable recursive globbing using find +for manifest in $(find . -name "manifest.json"); do manifest_path=$(realpath "${manifest}") - integrations+=(--integration-path "${manifest_path%/*}") + integrations="$integrations --integration-path ${manifest_path%/*}" done -if [[ ${#integrations[@]} -eq 0 ]]; then - bashio::exit.nok "No integrations found!" +if [ -z "$integrations" ]; then + echo "Error: No integrations found!" + exit 1 fi -cd /usr/src/homeassistant -exec python3 -m script.hassfest --action validate "${integrations[@]}" "$@" \ No newline at end of file +cd /usr/src/homeassistant || exit 1 +exec python3 -m script.hassfest --action validate $integrations "$@" From 0d5dc01048886e4d89ed1601e642de4cc74f9aac Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Thu, 29 Aug 2024 19:34:19 +0200 Subject: [PATCH 126/271] Bump PyTurboJPEG to 1.7.5 (#124865) --- homeassistant/components/camera/manifest.json | 2 +- homeassistant/components/stream/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/camera/manifest.json b/homeassistant/components/camera/manifest.json index b1df158a260..9c56d97f910 100644 --- a/homeassistant/components/camera/manifest.json +++ b/homeassistant/components/camera/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/camera", "integration_type": "entity", "quality_scale": "internal", - "requirements": ["PyTurboJPEG==1.7.1"] + "requirements": ["PyTurboJPEG==1.7.5"] } diff --git a/homeassistant/components/stream/manifest.json b/homeassistant/components/stream/manifest.json index 37158aa5fe3..dffd6d65a6e 100644 --- a/homeassistant/components/stream/manifest.json +++ b/homeassistant/components/stream/manifest.json @@ -7,5 +7,5 @@ "integration_type": "system", "iot_class": "local_push", "quality_scale": "internal", - "requirements": ["PyTurboJPEG==1.7.1", "ha-av==10.1.1", "numpy==1.26.0"] + "requirements": ["PyTurboJPEG==1.7.5", "ha-av==10.1.1", "numpy==1.26.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e6a5d6746f5..329b2535855 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -51,7 +51,7 @@ pyOpenSSL==24.2.1 pyserial==3.5 pyspeex-noise==1.0.2 python-slugify==8.0.4 -PyTurboJPEG==1.7.1 +PyTurboJPEG==1.7.5 pyudev==0.24.1 PyYAML==6.0.2 requests==2.32.3 diff --git a/requirements_all.txt b/requirements_all.txt index 7648cfce56e..078d31cad54 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -97,7 +97,7 @@ PyTransportNSW==0.1.1 # homeassistant.components.camera # homeassistant.components.stream -PyTurboJPEG==1.7.1 +PyTurboJPEG==1.7.5 # homeassistant.components.vicare PyViCare-neo==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index aa1fda4ad20..5259ae785c2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -91,7 +91,7 @@ PyTransportNSW==0.1.1 # homeassistant.components.camera # homeassistant.components.stream -PyTurboJPEG==1.7.1 +PyTurboJPEG==1.7.5 # homeassistant.components.vicare PyViCare-neo==0.2.1 From 37af180edc3be6188a113f3dfbe449e99d8666ac Mon Sep 17 00:00:00 2001 From: IceBotYT <34712694+IceBotYT@users.noreply.github.com> Date: Fri, 30 Aug 2024 11:08:58 -0400 Subject: [PATCH 127/271] Bump `nice-go` to 0.3.8 (#124872) * Bump nice-go to 0.3.6 * Bump to 0.3.7 * Bump to 0.3.8 --- homeassistant/components/nice_go/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nice_go/manifest.json b/homeassistant/components/nice_go/manifest.json index 45dd3c8b5b4..884f2eb7b18 100644 --- a/homeassistant/components/nice_go/manifest.json +++ b/homeassistant/components/nice_go/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/nice_go", "iot_class": "cloud_push", "loggers": ["nice-go"], - "requirements": ["nice-go==0.3.5"] + "requirements": ["nice-go==0.3.8"] } diff --git a/requirements_all.txt b/requirements_all.txt index 078d31cad54..a4afcd46fa7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1429,7 +1429,7 @@ nextdns==3.2.0 nibe==2.11.0 # homeassistant.components.nice_go -nice-go==0.3.5 +nice-go==0.3.8 # homeassistant.components.niko_home_control niko-home-control==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5259ae785c2..1c8506df52e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1186,7 +1186,7 @@ nextdns==3.2.0 nibe==2.11.0 # homeassistant.components.nice_go -nice-go==0.3.5 +nice-go==0.3.8 # homeassistant.components.nfandroidtv notifications-android-tv==0.1.5 From 5b866e071c376a69b782dca3fe70d70a821016fa Mon Sep 17 00:00:00 2001 From: Louis Christ Date: Fri, 30 Aug 2024 11:38:07 +0200 Subject: [PATCH 128/271] Handle CancelledError in bluesound integration (#124873) Catch CancledError in async_will_remove_from_hass --- homeassistant/components/bluesound/media_player.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index 92f47977ee5..1ed53d7bfc5 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -309,7 +309,7 @@ class BluesoundPlayer(MediaPlayerEntity): return True - async def _start_poll_command(self): + async def _poll_loop(self): """Loop which polls the status of the player.""" while True: try: @@ -335,7 +335,7 @@ class BluesoundPlayer(MediaPlayerEntity): await super().async_added_to_hass() self._polling_task = self.hass.async_create_background_task( - self._start_poll_command(), + self._poll_loop(), name=f"bluesound.polling_{self.host}:{self.port}", ) @@ -345,7 +345,9 @@ class BluesoundPlayer(MediaPlayerEntity): assert self._polling_task is not None if self._polling_task.cancel(): - await self._polling_task + # the sleeps in _poll_loop will raise CancelledError + with suppress(CancelledError): + await self._polling_task self.hass.data[DATA_BLUESOUND].remove(self) From 8668af17f69baff15d5a686c0d82d62337830cc1 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Thu, 29 Aug 2024 13:29:11 -0500 Subject: [PATCH 129/271] Bump intents to 2024.8.29 (#124874) --- homeassistant/components/conversation/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index d7a308b8b2b..5a689485b29 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/conversation", "integration_type": "system", "quality_scale": "internal", - "requirements": ["hassil==1.7.4", "home-assistant-intents==2024.8.7"] + "requirements": ["hassil==1.7.4", "home-assistant-intents==2024.8.29"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 329b2535855..c01b23ab4e4 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ hass-nabucasa==0.81.1 hassil==1.7.4 home-assistant-bluetooth==1.12.2 home-assistant-frontend==20240829.0 -home-assistant-intents==2024.8.7 +home-assistant-intents==2024.8.29 httpx==0.27.0 ifaddr==0.2.0 Jinja2==3.1.4 diff --git a/requirements_all.txt b/requirements_all.txt index a4afcd46fa7..d41eaeff9e8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1105,7 +1105,7 @@ holidays==0.55 home-assistant-frontend==20240829.0 # homeassistant.components.conversation -home-assistant-intents==2024.8.7 +home-assistant-intents==2024.8.29 # homeassistant.components.home_connect homeconnect==0.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1c8506df52e..a7d25ab41ff 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -928,7 +928,7 @@ holidays==0.55 home-assistant-frontend==20240829.0 # homeassistant.components.conversation -home-assistant-intents==2024.8.7 +home-assistant-intents==2024.8.29 # homeassistant.components.home_connect homeconnect==0.8.0 From 533c8ca31ce8de60d607ae1142420aceec07e5be Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 29 Aug 2024 21:37:19 -1000 Subject: [PATCH 130/271] Address yale review comments part 2 (#124887) * Remove some unneeded block till done * Additional state check cleanups and snapshots * Use more snapshots in yale tests --- .../components/yale/snapshots/test_lock.ambr | 37 ++++ .../yale/snapshots/test_sensor.ambr | 95 +++++++++ tests/components/yale/test_button.py | 1 - tests/components/yale/test_lock.py | 193 ++++++------------ tests/components/yale/test_sensor.py | 106 +++------- 5 files changed, 226 insertions(+), 206 deletions(-) create mode 100644 tests/components/yale/snapshots/test_lock.ambr create mode 100644 tests/components/yale/snapshots/test_sensor.ambr diff --git a/tests/components/yale/snapshots/test_lock.ambr b/tests/components/yale/snapshots/test_lock.ambr new file mode 100644 index 00000000000..b1a9f6a4d86 --- /dev/null +++ b/tests/components/yale/snapshots/test_lock.ambr @@ -0,0 +1,37 @@ +# serializer version: 1 +# name: test_lock_device_registry + DeviceRegistryEntrySnapshot({ + 'area_id': 'online_with_doorsense_name', + 'config_entries': , + 'configuration_url': 'https://account.aaecosystem.com', + 'connections': set({ + tuple( + 'bluetooth', + '12:22', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'yale', + 'online_with_doorsense', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Yale Home Inc.', + 'model': 'AUG-MD01', + 'model_id': None, + 'name': 'online_with_doorsense Name', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': 'online_with_doorsense Name', + 'sw_version': 'undefined-4.3.0-1.8.14', + 'via_device_id': None, + }) +# --- diff --git a/tests/components/yale/snapshots/test_sensor.ambr b/tests/components/yale/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..a425cfa90de --- /dev/null +++ b/tests/components/yale/snapshots/test_sensor.ambr @@ -0,0 +1,95 @@ +# serializer version: 1 +# name: test_lock_operator_autorelock + ReadOnlyDict({ + 'autorelock': True, + 'friendly_name': 'online_with_doorsense Name Operator', + 'keypad': False, + 'manual': False, + 'method': 'autorelock', + 'remote': False, + 'tag': False, + }) +# --- +# name: test_lock_operator_keypad + ReadOnlyDict({ + 'autorelock': False, + 'friendly_name': 'online_with_doorsense Name Operator', + 'keypad': True, + 'manual': False, + 'method': 'keypad', + 'remote': False, + 'tag': False, + }) +# --- +# name: test_lock_operator_manual + ReadOnlyDict({ + 'autorelock': False, + 'friendly_name': 'online_with_doorsense Name Operator', + 'keypad': False, + 'manual': True, + 'method': 'manual', + 'remote': False, + 'tag': False, + }) +# --- +# name: test_lock_operator_remote + ReadOnlyDict({ + 'autorelock': False, + 'friendly_name': 'online_with_doorsense Name Operator', + 'keypad': False, + 'manual': False, + 'method': 'remote', + 'remote': True, + 'tag': False, + }) +# --- +# name: test_restored_state + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'autorelock': False, + 'entity_picture': 'image.png', + 'friendly_name': 'online_with_doorsense Name Operator', + 'keypad': False, + 'manual': False, + 'method': 'tag', + 'remote': False, + 'tag': True, + }), + 'context': , + 'entity_id': 'sensor.online_with_doorsense_name_operator', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Tag Unlock', + }) +# --- +# name: test_unlock_operator_manual + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'autorelock': False, + 'friendly_name': 'online_with_doorsense Name Operator', + 'keypad': False, + 'manual': True, + 'method': 'manual', + 'remote': False, + 'tag': False, + }), + 'context': , + 'entity_id': 'sensor.online_with_doorsense_name_operator', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Your favorite elven princess', + }) +# --- +# name: test_unlock_operator_tag + ReadOnlyDict({ + 'autorelock': False, + 'friendly_name': 'online_with_doorsense Name Operator', + 'keypad': False, + 'manual': False, + 'method': 'tag', + 'remote': False, + 'tag': True, + }) +# --- diff --git a/tests/components/yale/test_button.py b/tests/components/yale/test_button.py index ebd22f1da59..92d3ecef859 100644 --- a/tests/components/yale/test_button.py +++ b/tests/components/yale/test_button.py @@ -20,5 +20,4 @@ async def test_wake_lock(hass: HomeAssistant) -> None: await hass.services.async_call( BUTTON_DOMAIN, SERVICE_PRESS, {ATTR_ENTITY_ID: entity_id}, blocking=True ) - await hass.async_block_till_done() api_instance.async_status_async.assert_called_once() diff --git a/tests/components/yale/test_lock.py b/tests/components/yale/test_lock.py index b449be9153d..2bbb7408953 100644 --- a/tests/components/yale/test_lock.py +++ b/tests/components/yale/test_lock.py @@ -5,6 +5,7 @@ import datetime from aiohttp import ClientResponseError from freezegun.api import FrozenDateTimeFactory import pytest +from syrupy import SnapshotAssertion from yalexs.manager.activity import INITIAL_LOCK_RESYNC_TIME from homeassistant.components.lock import ( @@ -41,7 +42,7 @@ from tests.common import async_fire_time_changed async def test_lock_device_registry( - hass: HomeAssistant, device_registry: dr.DeviceRegistry + hass: HomeAssistant, device_registry: dr.DeviceRegistry, snapshot: SnapshotAssertion ) -> None: """Test creation of a lock with doorsense and bridge ands up in the registry.""" lock_one = await _mock_doorsense_enabled_yale_lock_detail(hass) @@ -50,10 +51,7 @@ async def test_lock_device_registry( reg_device = device_registry.async_get_device( identifiers={("yale", "online_with_doorsense")} ) - assert reg_device.model == "AUG-MD01" - assert reg_device.sw_version == "undefined-4.3.0-1.8.14" - assert reg_device.name == "online_with_doorsense Name" - assert reg_device.manufacturer == "Yale Home Inc." + assert reg_device == snapshot async def test_lock_changed_by(hass: HomeAssistant) -> None: @@ -63,14 +61,9 @@ async def test_lock_changed_by(hass: HomeAssistant) -> None: activities = await _mock_activities_from_fixture(hass, "get_activity.lock.json") await _create_yale_with_devices(hass, [lock_one], activities=activities) - lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") - - assert lock_online_with_doorsense_name.state == STATE_LOCKED - - assert ( - lock_online_with_doorsense_name.attributes.get("changed_by") - == "Your favorite elven princess" - ) + lock_state = hass.states.get("lock.online_with_doorsense_name") + assert lock_state.state == STATE_LOCKED + assert lock_state.attributes["changed_by"] == "Your favorite elven princess" async def test_state_locking(hass: HomeAssistant) -> None: @@ -80,9 +73,7 @@ async def test_state_locking(hass: HomeAssistant) -> None: activities = await _mock_activities_from_fixture(hass, "get_activity.locking.json") await _create_yale_with_devices(hass, [lock_one], activities=activities) - lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") - - assert lock_online_with_doorsense_name.state == STATE_LOCKING + assert hass.states.get("lock.online_with_doorsense_name").state == STATE_LOCKING async def test_state_unlocking(hass: HomeAssistant) -> None: @@ -106,9 +97,7 @@ async def test_state_jammed(hass: HomeAssistant) -> None: activities = await _mock_activities_from_fixture(hass, "get_activity.jammed.json") await _create_yale_with_devices(hass, [lock_one], activities=activities) - lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") - - assert lock_online_with_doorsense_name.state == STATE_JAMMED + assert hass.states.get("lock.online_with_doorsense_name").state == STATE_JAMMED async def test_one_lock_operation( @@ -118,44 +107,31 @@ async def test_one_lock_operation( lock_one = await _mock_doorsense_enabled_yale_lock_detail(hass) await _create_yale_with_devices(hass, [lock_one]) - lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") + lock_state = hass.states.get("lock.online_with_doorsense_name") - assert lock_online_with_doorsense_name.state == STATE_LOCKED + assert lock_state.state == STATE_LOCKED - assert lock_online_with_doorsense_name.attributes.get("battery_level") == 92 - assert ( - lock_online_with_doorsense_name.attributes.get("friendly_name") - == "online_with_doorsense Name" - ) + assert lock_state.attributes["battery_level"] == 92 + assert lock_state.attributes["friendly_name"] == "online_with_doorsense Name" data = {ATTR_ENTITY_ID: "lock.online_with_doorsense_name"} await hass.services.async_call(LOCK_DOMAIN, SERVICE_UNLOCK, data, blocking=True) - await hass.async_block_till_done() - lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") - assert lock_online_with_doorsense_name.state == STATE_UNLOCKED + lock_state = hass.states.get("lock.online_with_doorsense_name") + assert lock_state.state == STATE_UNLOCKED - assert lock_online_with_doorsense_name.attributes.get("battery_level") == 92 - assert ( - lock_online_with_doorsense_name.attributes.get("friendly_name") - == "online_with_doorsense Name" - ) + assert lock_state.attributes["battery_level"] == 92 + assert lock_state.attributes["friendly_name"] == "online_with_doorsense Name" await hass.services.async_call(LOCK_DOMAIN, SERVICE_LOCK, data, blocking=True) - await hass.async_block_till_done() - lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") - assert lock_online_with_doorsense_name.state == STATE_LOCKED + lock_state = hass.states.get("lock.online_with_doorsense_name") + assert lock_state.state == STATE_LOCKED # No activity means it will be unavailable until the activity feed has data - lock_operator_sensor = entity_registry.async_get( - "sensor.online_with_doorsense_name_operator" - ) - assert lock_operator_sensor - assert ( - hass.states.get("sensor.online_with_doorsense_name_operator").state - == STATE_UNKNOWN - ) + assert entity_registry.async_get("sensor.online_with_doorsense_name_operator") + operator_state = hass.states.get("sensor.online_with_doorsense_name_operator") + assert operator_state.state == STATE_UNKNOWN async def test_open_lock_operation(hass: HomeAssistant) -> None: @@ -163,15 +139,12 @@ async def test_open_lock_operation(hass: HomeAssistant) -> None: lock_with_unlatch = await _mock_lock_with_unlatch(hass) await _create_yale_with_devices(hass, [lock_with_unlatch]) - lock_online_with_unlatch_name = hass.states.get("lock.online_with_unlatch_name") - assert lock_online_with_unlatch_name.state == STATE_LOCKED + assert hass.states.get("lock.online_with_unlatch_name").state == STATE_LOCKED data = {ATTR_ENTITY_ID: "lock.online_with_unlatch_name"} await hass.services.async_call(LOCK_DOMAIN, SERVICE_OPEN, data, blocking=True) - await hass.async_block_till_done() - lock_online_with_unlatch_name = hass.states.get("lock.online_with_unlatch_name") - assert lock_online_with_unlatch_name.state == STATE_UNLOCKED + assert hass.states.get("lock.online_with_unlatch_name").state == STATE_UNLOCKED async def test_open_lock_operation_socketio_connected( @@ -186,12 +159,10 @@ async def test_open_lock_operation_socketio_connected( _, socketio = await _create_yale_with_devices(hass, [lock_with_unlatch]) socketio.connected = True - lock_online_with_unlatch_name = hass.states.get("lock.online_with_unlatch_name") - assert lock_online_with_unlatch_name.state == STATE_LOCKED + assert hass.states.get("lock.online_with_unlatch_name").state == STATE_LOCKED data = {ATTR_ENTITY_ID: "lock.online_with_unlatch_name"} await hass.services.async_call(LOCK_DOMAIN, SERVICE_OPEN, data, blocking=True) - await hass.async_block_till_done() listener = list(socketio._listeners)[0] listener( @@ -205,8 +176,7 @@ async def test_open_lock_operation_socketio_connected( await hass.async_block_till_done() await hass.async_block_till_done() - lock_online_with_unlatch_name = hass.states.get("lock.online_with_unlatch_name") - assert lock_online_with_unlatch_name.state == STATE_UNLOCKED + assert hass.states.get("lock.online_with_unlatch_name").state == STATE_UNLOCKED await hass.async_block_till_done() @@ -218,23 +188,18 @@ async def test_one_lock_operation_socketio_connected( """Test lock and unlock operations are async when socketio is connected.""" lock_one = await _mock_doorsense_enabled_yale_lock_detail(hass) assert lock_one.pubsub_channel == "pubsub" + states = hass.states _, socketio = await _create_yale_with_devices(hass, [lock_one]) socketio.connected = True - lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") - - assert lock_online_with_doorsense_name.state == STATE_LOCKED - - assert lock_online_with_doorsense_name.attributes.get("battery_level") == 92 - assert ( - lock_online_with_doorsense_name.attributes.get("friendly_name") - == "online_with_doorsense Name" - ) + lock_state = hass.states.get("lock.online_with_doorsense_name") + assert lock_state.state == STATE_LOCKED + assert lock_state.attributes["battery_level"] == 92 + assert lock_state.attributes["friendly_name"] == "online_with_doorsense Name" data = {ATTR_ENTITY_ID: "lock.online_with_doorsense_name"} await hass.services.async_call(LOCK_DOMAIN, SERVICE_UNLOCK, data, blocking=True) - await hass.async_block_till_done() listener = list(socketio._listeners)[0] listener( @@ -248,17 +213,12 @@ async def test_one_lock_operation_socketio_connected( await hass.async_block_till_done() await hass.async_block_till_done() - lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") - assert lock_online_with_doorsense_name.state == STATE_UNLOCKED - - assert lock_online_with_doorsense_name.attributes.get("battery_level") == 92 - assert ( - lock_online_with_doorsense_name.attributes.get("friendly_name") - == "online_with_doorsense Name" - ) + lock_state = states.get("lock.online_with_doorsense_name") + assert lock_state.state == STATE_UNLOCKED + assert lock_state.attributes["battery_level"] == 92 + assert lock_state.attributes["friendly_name"] == "online_with_doorsense Name" await hass.services.async_call(LOCK_DOMAIN, SERVICE_LOCK, data, blocking=True) - await hass.async_block_till_done() listener( lock_one.device_id, @@ -271,17 +231,12 @@ async def test_one_lock_operation_socketio_connected( await hass.async_block_till_done() await hass.async_block_till_done() - lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") - assert lock_online_with_doorsense_name.state == STATE_LOCKED + assert states.get("lock.online_with_doorsense_name").state == STATE_LOCKED # No activity means it will be unavailable until the activity feed has data - lock_operator_sensor = entity_registry.async_get( - "sensor.online_with_doorsense_name_operator" - ) - assert lock_operator_sensor + assert entity_registry.async_get("sensor.online_with_doorsense_name_operator") assert ( - hass.states.get("sensor.online_with_doorsense_name_operator").state - == STATE_UNKNOWN + states.get("sensor.online_with_doorsense_name_operator").state == STATE_UNKNOWN ) freezer.tick(INITIAL_LOCK_RESYNC_TIME) @@ -296,8 +251,7 @@ async def test_one_lock_operation_socketio_connected( await hass.async_block_till_done() - lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") - assert lock_online_with_doorsense_name.state == STATE_UNLOCKED + assert states.get("lock.online_with_doorsense_name").state == STATE_UNLOCKED async def test_lock_jammed(hass: HomeAssistant) -> None: @@ -315,22 +269,16 @@ async def test_lock_jammed(hass: HomeAssistant) -> None: }, ) - lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") - - assert lock_online_with_doorsense_name.state == STATE_LOCKED - - assert lock_online_with_doorsense_name.attributes.get("battery_level") == 92 - assert ( - lock_online_with_doorsense_name.attributes.get("friendly_name") - == "online_with_doorsense Name" - ) + states = hass.states + lock_state = states.get("lock.online_with_doorsense_name") + assert lock_state.state == STATE_LOCKED + assert lock_state.attributes["battery_level"] == 92 + assert lock_state.attributes["friendly_name"] == "online_with_doorsense Name" data = {ATTR_ENTITY_ID: "lock.online_with_doorsense_name"} await hass.services.async_call(LOCK_DOMAIN, SERVICE_UNLOCK, data, blocking=True) - await hass.async_block_till_done() - lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") - assert lock_online_with_doorsense_name.state == STATE_JAMMED + assert states.get("lock.online_with_doorsense_name").state == STATE_JAMMED async def test_lock_throws_exception_on_unknown_status_code( @@ -350,15 +298,10 @@ async def test_lock_throws_exception_on_unknown_status_code( }, ) - lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") - - assert lock_online_with_doorsense_name.state == STATE_LOCKED - - assert lock_online_with_doorsense_name.attributes.get("battery_level") == 92 - assert ( - lock_online_with_doorsense_name.attributes.get("friendly_name") - == "online_with_doorsense Name" - ) + lock_state = hass.states.get("lock.online_with_doorsense_name") + assert lock_state.state == STATE_LOCKED + assert lock_state.attributes["battery_level"] == 92 + assert lock_state.attributes["friendly_name"] == "online_with_doorsense Name" data = {ATTR_ENTITY_ID: "lock.online_with_doorsense_name"} with pytest.raises(ClientResponseError): @@ -373,9 +316,7 @@ async def test_one_lock_unknown_state(hass: HomeAssistant) -> None: ) await _create_yale_with_devices(hass, [lock_one]) - lock_brokenid_name = hass.states.get("lock.brokenid_name") - - assert lock_brokenid_name.state == STATE_UNKNOWN + assert hass.states.get("lock.brokenid_name").state == STATE_UNKNOWN async def test_lock_bridge_offline(hass: HomeAssistant) -> None: @@ -387,9 +328,8 @@ async def test_lock_bridge_offline(hass: HomeAssistant) -> None: ) await _create_yale_with_devices(hass, [lock_one], activities=activities) - lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") - - assert lock_online_with_doorsense_name.state == STATE_UNAVAILABLE + states = hass.states + assert states.get("lock.online_with_doorsense_name").state == STATE_UNAVAILABLE async def test_lock_bridge_online(hass: HomeAssistant) -> None: @@ -401,9 +341,8 @@ async def test_lock_bridge_online(hass: HomeAssistant) -> None: ) await _create_yale_with_devices(hass, [lock_one], activities=activities) - lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") - - assert lock_online_with_doorsense_name.state == STATE_LOCKED + states = hass.states + assert states.get("lock.online_with_doorsense_name").state == STATE_LOCKED async def test_lock_update_via_socketio(hass: HomeAssistant) -> None: @@ -416,10 +355,9 @@ async def test_lock_update_via_socketio(hass: HomeAssistant) -> None: hass, [lock_one], activities=activities ) socketio.connected = True + states = hass.states - lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") - - assert lock_online_with_doorsense_name.state == STATE_LOCKED + assert states.get("lock.online_with_doorsense_name").state == STATE_LOCKED listener = list(socketio._listeners)[0] listener( @@ -433,8 +371,7 @@ async def test_lock_update_via_socketio(hass: HomeAssistant) -> None: await hass.async_block_till_done() await hass.async_block_till_done() - lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") - assert lock_online_with_doorsense_name.state == STATE_UNLOCKING + assert states.get("lock.online_with_doorsense_name").state == STATE_UNLOCKING listener( lock_one.device_id, @@ -447,25 +384,21 @@ async def test_lock_update_via_socketio(hass: HomeAssistant) -> None: await hass.async_block_till_done() await hass.async_block_till_done() - lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") - assert lock_online_with_doorsense_name.state == STATE_LOCKING + assert states.get("lock.online_with_doorsense_name").state == STATE_LOCKING async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(seconds=30)) await hass.async_block_till_done() - lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") - assert lock_online_with_doorsense_name.state == STATE_LOCKING + assert states.get("lock.online_with_doorsense_name").state == STATE_LOCKING socketio.connected = True async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(seconds=30)) await hass.async_block_till_done() - lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") - assert lock_online_with_doorsense_name.state == STATE_LOCKING + assert states.get("lock.online_with_doorsense_name").state == STATE_LOCKING # Ensure socketio status is always preserved async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(hours=2)) await hass.async_block_till_done() - lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") - assert lock_online_with_doorsense_name.state == STATE_LOCKING + assert states.get("lock.online_with_doorsense_name").state == STATE_LOCKING listener( lock_one.device_id, @@ -478,13 +411,11 @@ async def test_lock_update_via_socketio(hass: HomeAssistant) -> None: await hass.async_block_till_done() await hass.async_block_till_done() - lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") - assert lock_online_with_doorsense_name.state == STATE_UNLOCKING + assert states.get("lock.online_with_doorsense_name").state == STATE_UNLOCKING async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(hours=4)) await hass.async_block_till_done() - lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") - assert lock_online_with_doorsense_name.state == STATE_UNLOCKING + assert states.get("lock.online_with_doorsense_name").state == STATE_UNLOCKING await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/yale/test_sensor.py b/tests/components/yale/test_sensor.py index caf8781b4ad..5d724b4bb9d 100644 --- a/tests/components/yale/test_sensor.py +++ b/tests/components/yale/test_sensor.py @@ -2,6 +2,8 @@ from typing import Any +from syrupy import SnapshotAssertion + from homeassistant import core as ha from homeassistant.const import ( ATTR_ENTITY_PICTURE, @@ -28,13 +30,9 @@ async def test_create_doorbell(hass: HomeAssistant) -> None: doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.json") await _create_yale_with_devices(hass, [doorbell_one]) - sensor_k98gidt45gul_name_battery = hass.states.get( - "sensor.k98gidt45gul_name_battery" - ) - assert sensor_k98gidt45gul_name_battery.state == "96" - assert ( - sensor_k98gidt45gul_name_battery.attributes["unit_of_measurement"] == PERCENTAGE - ) + battery_state = hass.states.get("sensor.k98gidt45gul_name_battery") + assert battery_state.state == "96" + assert battery_state.attributes["unit_of_measurement"] == PERCENTAGE async def test_create_doorbell_offline( @@ -44,9 +42,9 @@ async def test_create_doorbell_offline( doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.offline.json") await _create_yale_with_devices(hass, [doorbell_one]) - sensor_tmt100_name_battery = hass.states.get("sensor.tmt100_name_battery") - assert sensor_tmt100_name_battery.state == "81" - assert sensor_tmt100_name_battery.attributes["unit_of_measurement"] == PERCENTAGE + battery_state = hass.states.get("sensor.tmt100_name_battery") + assert battery_state.state == "81" + assert battery_state.attributes["unit_of_measurement"] == PERCENTAGE entry = entity_registry.async_get("sensor.tmt100_name_battery") assert entry @@ -71,25 +69,21 @@ async def test_create_lock_with_linked_keypad( lock_one = await _mock_lock_from_fixture(hass, "get_lock.doorsense_init.json") await _create_yale_with_devices(hass, [lock_one]) - sensor_a6697750d607098bae8d6baa11ef8063_name_battery = hass.states.get( + battery_state = hass.states.get( "sensor.a6697750d607098bae8d6baa11ef8063_name_battery" ) - assert sensor_a6697750d607098bae8d6baa11ef8063_name_battery.state == "88" - assert ( - sensor_a6697750d607098bae8d6baa11ef8063_name_battery.attributes[ - "unit_of_measurement" - ] - == PERCENTAGE - ) + assert battery_state.state == "88" + assert battery_state.attributes["unit_of_measurement"] == PERCENTAGE + entry = entity_registry.async_get( "sensor.a6697750d607098bae8d6baa11ef8063_name_battery" ) assert entry assert entry.unique_id == "A6697750D607098BAE8D6BAA11EF8063_device_battery" - state = hass.states.get("sensor.front_door_lock_keypad_battery") - assert state.state == "62" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE + keypad_battery_state = hass.states.get("sensor.front_door_lock_keypad_battery") + assert keypad_battery_state.state == "62" + assert keypad_battery_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE entry = entity_registry.async_get("sensor.front_door_lock_keypad_battery") assert entry assert entry.unique_id == "5bc65c24e6ef2a263e1450a8_linked_keypad_battery" @@ -102,16 +96,11 @@ async def test_create_lock_with_low_battery_linked_keypad( lock_one = await _mock_lock_from_fixture(hass, "get_lock.low_keypad_battery.json") await _create_yale_with_devices(hass, [lock_one]) - sensor_a6697750d607098bae8d6baa11ef8063_name_battery = hass.states.get( + battery_state = hass.states.get( "sensor.a6697750d607098bae8d6baa11ef8063_name_battery" ) - assert sensor_a6697750d607098bae8d6baa11ef8063_name_battery.state == "88" - assert ( - sensor_a6697750d607098bae8d6baa11ef8063_name_battery.attributes[ - "unit_of_measurement" - ] - == PERCENTAGE - ) + assert battery_state.state == "88" + assert battery_state.attributes["unit_of_measurement"] == PERCENTAGE entry = entity_registry.async_get( "sensor.a6697750d607098bae8d6baa11ef8063_name_battery" ) @@ -166,7 +155,7 @@ async def test_lock_operator_bluetooth( async def test_lock_operator_keypad( - hass: HomeAssistant, entity_registry: er.EntityRegistry + hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion ) -> None: """Test operation of a lock with doorsense and bridge.""" lock_one = await _mock_doorsense_enabled_yale_lock_detail(hass) @@ -183,16 +172,11 @@ async def test_lock_operator_keypad( state = hass.states.get("sensor.online_with_doorsense_name_operator") assert state.state == "Your favorite elven princess" - assert state.attributes["manual"] is False - assert state.attributes["tag"] is False - assert state.attributes["remote"] is False - assert state.attributes["keypad"] is True - assert state.attributes["autorelock"] is False - assert state.attributes["method"] == "keypad" + assert state.attributes == snapshot async def test_lock_operator_remote( - hass: HomeAssistant, entity_registry: er.EntityRegistry + hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion ) -> None: """Test operation of a lock with doorsense and bridge.""" lock_one = await _mock_doorsense_enabled_yale_lock_detail(hass) @@ -207,16 +191,11 @@ async def test_lock_operator_remote( state = hass.states.get("sensor.online_with_doorsense_name_operator") assert state.state == "Your favorite elven princess" - assert state.attributes["manual"] is False - assert state.attributes["tag"] is False - assert state.attributes["remote"] is True - assert state.attributes["keypad"] is False - assert state.attributes["autorelock"] is False - assert state.attributes["method"] == "remote" + assert state.attributes == snapshot async def test_lock_operator_manual( - hass: HomeAssistant, entity_registry: er.EntityRegistry + hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion ) -> None: """Test operation of a lock with doorsense and bridge.""" lock_one = await _mock_doorsense_enabled_yale_lock_detail(hass) @@ -232,16 +211,11 @@ async def test_lock_operator_manual( assert lock_operator_sensor state = hass.states.get("sensor.online_with_doorsense_name_operator") assert state.state == "Your favorite elven princess" - assert state.attributes["manual"] is True - assert state.attributes["tag"] is False - assert state.attributes["remote"] is False - assert state.attributes["keypad"] is False - assert state.attributes["autorelock"] is False - assert state.attributes["method"] == "manual" + assert state.attributes == snapshot async def test_lock_operator_autorelock( - hass: HomeAssistant, entity_registry: er.EntityRegistry + hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion ) -> None: """Test operation of a lock with doorsense and bridge.""" lock_one = await _mock_doorsense_enabled_yale_lock_detail(hass) @@ -258,16 +232,11 @@ async def test_lock_operator_autorelock( state = hass.states.get("sensor.online_with_doorsense_name_operator") assert state.state == "Auto Relock" - assert state.attributes["manual"] is False - assert state.attributes["tag"] is False - assert state.attributes["remote"] is False - assert state.attributes["keypad"] is False - assert state.attributes["autorelock"] is True - assert state.attributes["method"] == "autorelock" + assert state.attributes == snapshot async def test_unlock_operator_manual( - hass: HomeAssistant, entity_registry: er.EntityRegistry + hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion ) -> None: """Test operation of a lock manually.""" lock_one = await _mock_doorsense_enabled_yale_lock_detail(hass) @@ -284,16 +253,11 @@ async def test_unlock_operator_manual( state = hass.states.get("sensor.online_with_doorsense_name_operator") assert state.state == "Your favorite elven princess" - assert state.attributes["manual"] is True - assert state.attributes["tag"] is False - assert state.attributes["remote"] is False - assert state.attributes["keypad"] is False - assert state.attributes["autorelock"] is False - assert state.attributes["method"] == "manual" + assert state == snapshot async def test_unlock_operator_tag( - hass: HomeAssistant, entity_registry: er.EntityRegistry + hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion ) -> None: """Test operation of a lock with a tag.""" lock_one = await _mock_doorsense_enabled_yale_lock_detail(hass) @@ -310,16 +274,11 @@ async def test_unlock_operator_tag( state = hass.states.get("sensor.online_with_doorsense_name_operator") assert state.state == "Your favorite elven princess" - assert state.attributes["manual"] is False - assert state.attributes["tag"] is True - assert state.attributes["remote"] is False - assert state.attributes["keypad"] is False - assert state.attributes["autorelock"] is False - assert state.attributes["method"] == "tag" + assert state.attributes == snapshot async def test_restored_state( - hass: HomeAssistant, hass_storage: dict[str, Any] + hass: HomeAssistant, hass_storage: dict[str, Any], snapshot: SnapshotAssertion ) -> None: """Test restored state.""" @@ -358,5 +317,4 @@ async def test_restored_state( state = hass.states.get(entity_id) assert state.state == "Tag Unlock" - assert state.attributes["method"] == "tag" - assert state.attributes[ATTR_ENTITY_PICTURE] == "image.png" + assert state == snapshot From 3b4e3b1370d98eca17fc581ac2f87334b54b0197 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Fri, 30 Aug 2024 02:13:47 +0200 Subject: [PATCH 131/271] Fix ZHA group removal entity registry cleanup (#124889) * Fix ZHA cleanup entity registry parameter * Fix missing `gateway` when accessing coordinator device * Get `ZHADeviceProxy` for coordinator device --- homeassistant/components/zha/helpers.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zha/helpers.py b/homeassistant/components/zha/helpers.py index a5446af7e76..f70c8a9cb3e 100644 --- a/homeassistant/components/zha/helpers.py +++ b/homeassistant/components/zha/helpers.py @@ -802,21 +802,24 @@ class ZHAGatewayProxy(EventBase): ) def _cleanup_group_entity_registry_entries( - self, zigpy_group: zigpy.group.Group + self, zha_group_proxy: ZHAGroupProxy ) -> None: """Remove entity registry entries for group entities when the groups are removed from HA.""" # first we collect the potential unique ids for entities that could be created from this group possible_entity_unique_ids = [ - f"{domain}_zha_group_0x{zigpy_group.group_id:04x}" + f"{domain}_zha_group_0x{zha_group_proxy.group.group_id:04x}" for domain in GROUP_ENTITY_DOMAINS ] # then we get all group entity entries tied to the coordinator entity_registry = er.async_get(self.hass) - assert self.coordinator_zha_device + assert self.gateway.coordinator_zha_device + coordinator_proxy = self.device_proxies[ + self.gateway.coordinator_zha_device.ieee + ] all_group_entity_entries = er.async_entries_for_device( entity_registry, - self.coordinator_zha_device.device_id, + coordinator_proxy.device_id, include_disabled_entities=True, ) From d4830caac06a58a97190370b15ef9ce9f10eb31b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 29 Aug 2024 16:19:12 -1000 Subject: [PATCH 132/271] Bump aioesphomeapi to 25.3.1 (#124890) changelog: https://github.com/esphome/aioesphomeapi/compare/v25.2.1...v25.3.1 --- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 454b547cdf4..9d42b7206e3 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -17,7 +17,7 @@ "mqtt": ["esphome/discover/#"], "quality_scale": "platinum", "requirements": [ - "aioesphomeapi==25.2.1", + "aioesphomeapi==25.3.1", "esphome-dashboard-api==1.2.3", "bleak-esphome==1.0.0" ], diff --git a/requirements_all.txt b/requirements_all.txt index d41eaeff9e8..c7ada7c4490 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -237,7 +237,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==25.2.1 +aioesphomeapi==25.3.1 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a7d25ab41ff..719f6463627 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -225,7 +225,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==25.2.1 +aioesphomeapi==25.3.1 # homeassistant.components.flo aioflo==2021.11.0 From ee9e3fe27bc1ca7286b9047d5c337a63489ad4b4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 29 Aug 2024 13:07:21 -1000 Subject: [PATCH 133/271] Bump yalexs to 8.5.5 (#124891) changelog: https://github.com/bdraco/yalexs/compare/v8.5.4...v8.5.5 --- homeassistant/components/august/manifest.json | 2 +- homeassistant/components/yale/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index fe630638cf2..5f317a20834 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -24,5 +24,5 @@ "documentation": "https://www.home-assistant.io/integrations/august", "iot_class": "cloud_push", "loggers": ["pubnub", "yalexs"], - "requirements": ["yalexs==8.5.4", "yalexs-ble==2.4.3"] + "requirements": ["yalexs==8.5.5", "yalexs-ble==2.4.3"] } diff --git a/homeassistant/components/yale/manifest.json b/homeassistant/components/yale/manifest.json index 115036b96d5..9bee7df2e00 100644 --- a/homeassistant/components/yale/manifest.json +++ b/homeassistant/components/yale/manifest.json @@ -13,5 +13,5 @@ "documentation": "https://www.home-assistant.io/integrations/yale", "iot_class": "cloud_push", "loggers": ["socketio", "engineio", "yalexs"], - "requirements": ["yalexs==8.5.4", "yalexs-ble==2.4.3"] + "requirements": ["yalexs==8.5.5", "yalexs-ble==2.4.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index c7ada7c4490..d719955cf93 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2973,7 +2973,7 @@ yalexs-ble==2.4.3 # homeassistant.components.august # homeassistant.components.yale -yalexs==8.5.4 +yalexs==8.5.5 # homeassistant.components.yeelight yeelight==0.7.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 719f6463627..41af213af51 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2356,7 +2356,7 @@ yalexs-ble==2.4.3 # homeassistant.components.august # homeassistant.components.yale -yalexs==8.5.4 +yalexs==8.5.5 # homeassistant.components.yeelight yeelight==0.7.14 From 8ab8f7a7400ed97f5b19d90183181ff47b09c077 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 29 Aug 2024 21:35:19 -1000 Subject: [PATCH 134/271] Add a repair issue for Yale Home users using the August integration (#124895) The Yale Home brand will stop working with the August integration very soon. Users must migrate to the Yale integration to avoid an interruption in service. --- homeassistant/components/august/__init__.py | 32 +++++++++++++++++-- .../components/august/config_flow.py | 10 ++++-- homeassistant/components/august/strings.json | 6 ++++ tests/components/august/test_config_flow.py | 4 +-- tests/components/august/test_init.py | 28 +++++++++++++++- 5 files changed, 73 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index 47a7f75611a..434db46384b 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -6,15 +6,16 @@ from pathlib import Path from typing import cast from aiohttp import ClientResponseError +from yalexs.const import Brand from yalexs.exceptions import AugustApiAIOHTTPError from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation from yalexs.manager.gateway import Config as YaleXSConfig from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import device_registry as dr, issue_registry as ir from .const import DOMAIN, PLATFORMS from .data import AugustData @@ -24,6 +25,26 @@ from .util import async_create_august_clientsession type AugustConfigEntry = ConfigEntry[AugustData] +@callback +def _async_create_yale_brand_migration_issue( + hass: HomeAssistant, entry: AugustConfigEntry +) -> None: + """Create an issue for a brand migration.""" + ir.async_create_issue( + hass, + DOMAIN, + "yale_brand_migration", + breaks_in_ha_version="2024.9", + learn_more_url="https://www.home-assistant.io/integrations/yale", + translation_key="yale_brand_migration", + is_fixable=False, + severity=ir.IssueSeverity.CRITICAL, + translation_placeholders={ + "migrate_url": "https://my.home-assistant.io/redirect/config_flow_start?domain=yale" + }, + ) + + async def async_setup_entry(hass: HomeAssistant, entry: AugustConfigEntry) -> bool: """Set up August from a config entry.""" session = async_create_august_clientsession(hass) @@ -40,6 +61,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: AugustConfigEntry) -> bo return True +async def async_remove_entry(hass: HomeAssistant, entry: AugustConfigEntry) -> None: + """Remove an August config entry.""" + ir.async_delete_issue(hass, DOMAIN, "yale_brand_migration") + + async def async_unload_entry(hass: HomeAssistant, entry: AugustConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) @@ -51,6 +77,8 @@ async def async_setup_august( """Set up the August component.""" config = cast(YaleXSConfig, entry.data) await august_gateway.async_setup(config) + if august_gateway.api.brand == Brand.YALE_HOME: + _async_create_yale_brand_migration_issue(hass, entry) await august_gateway.async_authenticate() await august_gateway.async_refresh_access_token_if_needed() data = entry.runtime_data = AugustData(hass, august_gateway) diff --git a/homeassistant/components/august/config_flow.py b/homeassistant/components/august/config_flow.py index 2a1a20a9dc4..58c3549fe4d 100644 --- a/homeassistant/components/august/config_flow.py +++ b/homeassistant/components/august/config_flow.py @@ -9,7 +9,7 @@ from typing import Any import aiohttp import voluptuous as vol from yalexs.authenticator_common import ValidationResult -from yalexs.const import BRANDS_WITHOUT_OAUTH, DEFAULT_BRAND +from yalexs.const import BRANDS_WITHOUT_OAUTH, DEFAULT_BRAND, Brand from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation from homeassistant.config_entries import ConfigFlow, ConfigFlowResult @@ -28,6 +28,12 @@ from .const import ( from .gateway import AugustGateway from .util import async_create_august_clientsession +# The Yale Home Brand is not supported by the August integration +# anymore and should migrate to the Yale integration +AVAILABLE_BRANDS = BRANDS_WITHOUT_OAUTH.copy() +del AVAILABLE_BRANDS[Brand.YALE_HOME] + + _LOGGER = logging.getLogger(__name__) @@ -118,7 +124,7 @@ class AugustConfigFlow(ConfigFlow, domain=DOMAIN): vol.Required( CONF_BRAND, default=self._user_auth_details.get(CONF_BRAND, DEFAULT_BRAND), - ): vol.In(BRANDS_WITHOUT_OAUTH), + ): vol.In(AVAILABLE_BRANDS), vol.Required( CONF_LOGIN_METHOD, default=self._user_auth_details.get( diff --git a/homeassistant/components/august/strings.json b/homeassistant/components/august/strings.json index 772a8dca479..589a494590b 100644 --- a/homeassistant/components/august/strings.json +++ b/homeassistant/components/august/strings.json @@ -1,4 +1,10 @@ { + "issues": { + "yale_brand_migration": { + "title": "Yale Home has a new integration", + "description": "Add the [Yale integration]({migrate_url}), and remove the August integration as soon as possible to avoid an interruption in service. The Yale Home brand will stop working with the August integration soon and will be removed in a future release." + } + }, "config": { "error": { "unhandled": "Unhandled error: {error}", diff --git a/tests/components/august/test_config_flow.py b/tests/components/august/test_config_flow.py index 9902901d29f..b3138342b8c 100644 --- a/tests/components/august/test_config_flow.py +++ b/tests/components/august/test_config_flow.py @@ -385,7 +385,7 @@ async def test_switching_brands(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - CONF_BRAND: "yale_home", + CONF_BRAND: "yale_access", CONF_LOGIN_METHOD: "email", CONF_USERNAME: "my@email.tld", CONF_PASSWORD: "test-password", @@ -396,4 +396,4 @@ async def test_switching_brands(hass: HomeAssistant) -> None: assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 - assert entry.data[CONF_BRAND] == "yale_home" + assert entry.data[CONF_BRAND] == "yale_access" diff --git a/tests/components/august/test_init.py b/tests/components/august/test_init.py index 954436f209a..1bbe8033ec8 100644 --- a/tests/components/august/test_init.py +++ b/tests/components/august/test_init.py @@ -5,6 +5,7 @@ from unittest.mock import Mock, patch from aiohttp import ClientResponseError import pytest from yalexs.authenticator_common import AuthenticationState +from yalexs.const import Brand from yalexs.exceptions import AugustApiAIOHTTPError from homeassistant.components.august.const import DOMAIN @@ -20,7 +21,11 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import ( + device_registry as dr, + entity_registry as er, + issue_registry as ir, +) from homeassistant.setup import async_setup_component from .mocks import ( @@ -420,3 +425,24 @@ async def test_device_remove_devices( ) response = await client.remove_device(dead_device_entry.id, config_entry.entry_id) assert response["success"] + + +async def test_brand_migration_issue(hass: HomeAssistant) -> None: + """Test creating and removing the brand migration issue.""" + august_operative_lock = await _mock_operative_august_lock_detail(hass) + config_entry = await _create_august_with_devices( + hass, [august_operative_lock], brand=Brand.YALE_HOME + ) + + assert config_entry.state is ConfigEntryState.LOADED + + issue_reg = ir.async_get(hass) + issue_entry = issue_reg.async_get_issue(DOMAIN, "yale_brand_migration") + assert issue_entry + assert issue_entry.severity == ir.IssueSeverity.CRITICAL + assert issue_entry.translation_placeholders == { + "migrate_url": "https://my.home-assistant.io/redirect/config_flow_start?domain=yale" + } + + await hass.config_entries.async_remove(config_entry.entry_id) + assert not issue_reg.async_get_issue(DOMAIN, "yale_brand_migration") From f33b4b0dc045f59cc616cb34c52832ac4bb56ac9 Mon Sep 17 00:00:00 2001 From: Josef Zweck <24647999+zweckj@users.noreply.github.com> Date: Fri, 30 Aug 2024 10:05:28 +0200 Subject: [PATCH 135/271] Bump lmcloud to 1.2.1 (#124908) --- homeassistant/components/lamarzocco/__init__.py | 1 + homeassistant/components/lamarzocco/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lamarzocco/__init__.py b/homeassistant/components/lamarzocco/__init__.py index dfcaa54047d..02e47ecd78e 100644 --- a/homeassistant/components/lamarzocco/__init__.py +++ b/homeassistant/components/lamarzocco/__init__.py @@ -53,6 +53,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) - cloud_client = LaMarzoccoCloudClient( username=entry.data[CONF_USERNAME], password=entry.data[CONF_PASSWORD], + client=get_async_client(hass), ) # initialize local API diff --git a/homeassistant/components/lamarzocco/manifest.json b/homeassistant/components/lamarzocco/manifest.json index 73d14250525..37a4e1d0c99 100644 --- a/homeassistant/components/lamarzocco/manifest.json +++ b/homeassistant/components/lamarzocco/manifest.json @@ -22,5 +22,5 @@ "integration_type": "device", "iot_class": "cloud_polling", "loggers": ["lmcloud"], - "requirements": ["lmcloud==1.1.13"] + "requirements": ["lmcloud==1.2.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index d719955cf93..e871a857cf8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1282,7 +1282,7 @@ linear-garage-door==0.2.9 linode-api==4.1.9b1 # homeassistant.components.lamarzocco -lmcloud==1.1.13 +lmcloud==1.2.1 # homeassistant.components.google_maps locationsharinglib==5.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 41af213af51..d2a8c25e197 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1060,7 +1060,7 @@ libsoundtouch==0.8 linear-garage-door==0.2.9 # homeassistant.components.lamarzocco -lmcloud==1.1.13 +lmcloud==1.2.1 # homeassistant.components.london_underground london-tube-status==0.5 From dd8471e7868e2a42ceabccffdad1abc114ee9551 Mon Sep 17 00:00:00 2001 From: Josef Zweck <24647999+zweckj@users.noreply.github.com> Date: Fri, 30 Aug 2024 11:02:29 +0200 Subject: [PATCH 136/271] Bump lmcloud 1.2.2 (#124911) bump lmcloud 1.2.2 --- homeassistant/components/lamarzocco/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lamarzocco/manifest.json b/homeassistant/components/lamarzocco/manifest.json index 37a4e1d0c99..181a2b9ab9b 100644 --- a/homeassistant/components/lamarzocco/manifest.json +++ b/homeassistant/components/lamarzocco/manifest.json @@ -22,5 +22,5 @@ "integration_type": "device", "iot_class": "cloud_polling", "loggers": ["lmcloud"], - "requirements": ["lmcloud==1.2.1"] + "requirements": ["lmcloud==1.2.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index e871a857cf8..5418e29c0f7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1282,7 +1282,7 @@ linear-garage-door==0.2.9 linode-api==4.1.9b1 # homeassistant.components.lamarzocco -lmcloud==1.2.1 +lmcloud==1.2.2 # homeassistant.components.google_maps locationsharinglib==5.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d2a8c25e197..e5a0b4521ad 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1060,7 +1060,7 @@ libsoundtouch==0.8 linear-garage-door==0.2.9 # homeassistant.components.lamarzocco -lmcloud==1.2.1 +lmcloud==1.2.2 # homeassistant.components.london_underground london-tube-status==0.5 From 3a8aa4200dfdc304b2937b80f1b38f5585634890 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 30 Aug 2024 16:41:48 +0200 Subject: [PATCH 137/271] Bump aiomealie to 0.9.0 (#124924) * Bump aiomealie to 0.9.0 * Bump aiomealie to 0.9.0 --- homeassistant/components/mealie/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../mealie/snapshots/test_diagnostics.ambr | 29 +++++++++++++++ .../mealie/snapshots/test_services.ambr | 37 +++++++++++++++++++ 5 files changed, 69 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mealie/manifest.json b/homeassistant/components/mealie/manifest.json index 75093577b0f..4a277cbd09b 100644 --- a/homeassistant/components/mealie/manifest.json +++ b/homeassistant/components/mealie/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/mealie", "integration_type": "service", "iot_class": "local_polling", - "requirements": ["aiomealie==0.8.1"] + "requirements": ["aiomealie==0.9.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5418e29c0f7..3b73569373c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -288,7 +288,7 @@ aiolookin==1.0.0 aiolyric==2.0.1 # homeassistant.components.mealie -aiomealie==0.8.1 +aiomealie==0.9.0 # homeassistant.components.modern_forms aiomodernforms==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e5a0b4521ad..e63e4be3e99 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -270,7 +270,7 @@ aiolookin==1.0.0 aiolyric==2.0.1 # homeassistant.components.mealie -aiomealie==0.8.1 +aiomealie==0.9.0 # homeassistant.components.modern_forms aiomodernforms==0.1.8 diff --git a/tests/components/mealie/snapshots/test_diagnostics.ambr b/tests/components/mealie/snapshots/test_diagnostics.ambr index e6c72c950cc..a694c72fcf6 100644 --- a/tests/components/mealie/snapshots/test_diagnostics.ambr +++ b/tests/components/mealie/snapshots/test_diagnostics.ambr @@ -10,6 +10,7 @@ 'description': None, 'entry_type': 'breakfast', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'mealplan_date': dict({ '__type': "", 'isoformat': '2024-01-23', @@ -18,6 +19,7 @@ 'recipe': dict({ 'description': 'The BEST Roast Chicken recipe is simple, budget friendly, and gives you a tender, mouth-watering chicken full of flavor! Served with roasted vegetables, this recipe is simple enough for any cook!', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'image': 'JeQ2', 'name': 'Roast Chicken', 'original_url': 'https://tastesbetterfromscratch.com/roast-chicken/', @@ -35,6 +37,7 @@ 'description': None, 'entry_type': 'dinner', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'mealplan_date': dict({ '__type': "", 'isoformat': '2024-01-22', @@ -43,6 +46,7 @@ 'recipe': dict({ 'description': "Een traybake is eigenlijk altijd een goed idee. Deze zoete aardappel curry traybake dus ook. Waarom? Omdat je alleen maar wat groenten - en in dit geval kip - op een bakplaat (traybake dus) legt, hier wat kruiden aan toevoegt en deze in de oven schuift. Ideaal dus als je geen zin hebt om lang in de keuken te staan. Maar gewoon lekker op de bank wil ploffen om te wachten tot de oven klaar is. Joe! That\\'s what we like. Deze zoete aardappel curry traybake bevat behalve zoete aardappel en curry ook kikkererwten, kippendijfilet en bloemkoolroosjes. Je gebruikt yoghurt en limoen als een soort dressing. En je serveert deze heerlijke traybake met naanbrood. Je kunt natuurljk ook voor deze traybake met chipolataworstjes gaan. Wil je graag meer ovengerechten? Dan moet je eigenlijk even kijken naar onze Ovenbijbel. Onmisbaar in je keuken! We willen je deze zoete aardappelstamppot met prei ook niet onthouden. Megalekker bordje comfortfood als je \\'t ons vraagt.", 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'image': 'AiIo', 'name': 'Zoete aardappel curry traybake', 'original_url': 'https://chickslovefood.com/recept/zoete-aardappel-curry-traybake/', @@ -58,6 +62,7 @@ 'description': None, 'entry_type': 'dinner', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'mealplan_date': dict({ '__type': "", 'isoformat': '2024-01-23', @@ -66,6 +71,7 @@ 'recipe': dict({ 'description': 'Εύκολη μακαρονάδα με κεφτεδάκια στον φούρνο από τον Άκη Πετρετζίκη. Φτιάξτε την πιο εύκολη μακαρονάδα με κεφτεδάκια σε μόνο ένα σκεύος.', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'image': 'En9o', 'name': 'Εύκολη μακαρονάδα με κεφτεδάκια στον φούρνο (1)', 'original_url': 'https://akispetretzikis.com/recipe/7959/efkolh-makaronada-me-keftedakia-ston-fourno', @@ -81,6 +87,7 @@ 'description': None, 'entry_type': 'dinner', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'mealplan_date': dict({ '__type': "", 'isoformat': '2024-01-23', @@ -89,6 +96,7 @@ 'recipe': dict({ 'description': 'Delicious Greek turkey meatballs with lemon orzo, tender veggies, and a creamy feta yogurt sauce. These healthy baked Greek turkey meatballs are filled with tons of wonderful herbs and make the perfect protein-packed weeknight meal!', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'image': 'Kn62', 'name': 'Greek Turkey Meatballs with Lemon Orzo & Creamy Feta Yogurt Sauce', 'original_url': 'https://www.ambitiouskitchen.com/greek-turkey-meatballs/', @@ -104,6 +112,7 @@ 'description': None, 'entry_type': 'dinner', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'mealplan_date': dict({ '__type': "", 'isoformat': '2024-01-23', @@ -112,6 +121,7 @@ 'recipe': dict({ 'description': 'This is a modified Pampered Chef recipe. You can use a trifle bowl or large glass punch/salad bowl to show it off. It is really easy to make and I never have any leftovers. Cook time includes chill time.', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'image': 'ibL6', 'name': 'Pampered Chef Double Chocolate Mocha Trifle', 'original_url': 'https://www.food.com/recipe/pampered-chef-double-chocolate-mocha-trifle-74963', @@ -127,6 +137,7 @@ 'description': None, 'entry_type': 'dinner', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'mealplan_date': dict({ '__type': "", 'isoformat': '2024-01-22', @@ -135,6 +146,7 @@ 'recipe': dict({ 'description': 'Cheeseburger Sliders are juicy, cheesy and beefy - everything we love about classic burgers! These sliders are quick and easy plus they are make-ahead and reheat really well.', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'image': 'beGq', 'name': 'Cheeseburger Sliders (Easy, 30-min Recipe)', 'original_url': 'https://natashaskitchen.com/cheeseburger-sliders/', @@ -150,6 +162,7 @@ 'description': None, 'entry_type': 'dinner', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'mealplan_date': dict({ '__type': "", 'isoformat': '2024-01-23', @@ -158,6 +171,7 @@ 'recipe': dict({ 'description': 'This All-American beef stew recipe includes tender beef coated in a rich, intense sauce and vegetables that bring complementary texture and flavor.', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'image': '356X', 'name': 'All-American Beef Stew Recipe', 'original_url': 'https://www.seriouseats.com/all-american-beef-stew-recipe', @@ -173,6 +187,7 @@ 'description': None, 'entry_type': 'dinner', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'mealplan_date': dict({ '__type': "", 'isoformat': '2024-01-22', @@ -181,6 +196,7 @@ 'recipe': dict({ 'description': 'Einfacher Nudelauflauf mit Brokkoli, Sahnesauce und extra Käse. Dieses vegetarische 5 Zutaten Rezept ist super schnell gemacht und SO gut!', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'image': 'nOPT', 'name': 'Einfacher Nudelauflauf mit Brokkoli', 'original_url': 'https://kochkarussell.com/einfacher-nudelauflauf-brokkoli/', @@ -196,6 +212,7 @@ 'description': None, 'entry_type': 'dinner', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'mealplan_date': dict({ '__type': "", 'isoformat': '2024-01-23', @@ -204,6 +221,7 @@ 'recipe': dict({ 'description': 'Simple to prepare and ready in 25 minutes, this vegetarian miso noodle recipe can be eaten on its own or served as a side.', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'image': '5G1v', 'name': 'Miso Udon Noodles with Spinach and Tofu', 'original_url': 'https://www.allrecipes.com/recipe/284039/miso-udon-noodles-with-spinach-and-tofu/', @@ -219,6 +237,7 @@ 'description': None, 'entry_type': 'dinner', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'mealplan_date': dict({ '__type': "", 'isoformat': '2024-01-22', @@ -227,6 +246,7 @@ 'recipe': dict({ 'description': 'Avis aux nostalgiques des années 1980, la mousse de saumon est de retour dans une présentation adaptée au goût du jour. On utilise une technique sans faille : un saumon frais cuit au micro-ondes et mélangé au robot avec du fromage à la crème et de la crème sure. On obtient ainsi une texture onctueuse à tartiner, qui n’a rien à envier aux préparations gélatineuses d’antan !', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'image': 'rrNL', 'name': 'Mousse de saumon', 'original_url': 'https://www.ricardocuisine.com/recettes/8919-mousse-de-saumon', @@ -242,6 +262,7 @@ 'description': 'Dineren met de boys', 'entry_type': 'dinner', 'group_id': '3931df86-0679-4579-8c63-4bedc9ca9a85', + 'household_id': None, 'mealplan_date': dict({ '__type': "", 'isoformat': '2024-01-21', @@ -257,6 +278,7 @@ 'description': None, 'entry_type': 'lunch', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'mealplan_date': dict({ '__type': "", 'isoformat': '2024-01-23', @@ -265,6 +287,7 @@ 'recipe': dict({ 'description': 'Te explicamos paso a paso, de manera sencilla, la elaboración de la receta de pollo al curry con leche de coco en 10 minutos. Ingredientes, tiempo de...', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'image': 'INQz', 'name': 'Receta de pollo al curry en 10 minutos (con vídeo incluido)', 'original_url': 'https://www.directoalpaladar.com/recetas-de-carnes-y-aves/receta-de-pollo-al-curry-en-10-minutos', @@ -280,6 +303,7 @@ 'description': None, 'entry_type': 'lunch', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'mealplan_date': dict({ '__type': "", 'isoformat': '2024-01-23', @@ -288,6 +312,7 @@ 'recipe': dict({ 'description': 'bourguignon, oignon, carotte, bouquet garni, vin rouge, beurre, sel, poivre', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'image': 'nj5M', 'name': 'Boeuf bourguignon : la vraie recette (2)', 'original_url': 'https://www.marmiton.org/recettes/recette_boeuf-bourguignon_18889.aspx', @@ -303,6 +328,7 @@ 'description': None, 'entry_type': 'lunch', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'mealplan_date': dict({ '__type': "", 'isoformat': '2024-01-22', @@ -311,6 +337,7 @@ 'recipe': dict({ 'description': 'This All-American beef stew recipe includes tender beef coated in a rich, intense sauce and vegetables that bring complementary texture and flavor.', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'image': '356X', 'name': 'All-American Beef Stew Recipe', 'original_url': 'https://www.seriouseats.com/all-american-beef-stew-recipe', @@ -328,6 +355,7 @@ 'description': None, 'entry_type': 'side', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'mealplan_date': dict({ '__type': "", 'isoformat': '2024-01-23', @@ -336,6 +364,7 @@ 'recipe': dict({ 'description': 'Einfacher Nudelauflauf mit Brokkoli, Sahnesauce und extra Käse. Dieses vegetarische 5 Zutaten Rezept ist super schnell gemacht und SO gut!', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'image': 'nOPT', 'name': 'Einfacher Nudelauflauf mit Brokkoli', 'original_url': 'https://kochkarussell.com/einfacher-nudelauflauf-brokkoli/', diff --git a/tests/components/mealie/snapshots/test_services.ambr b/tests/components/mealie/snapshots/test_services.ambr index 3ae158f1d2d..4f9ee6a5c09 100644 --- a/tests/components/mealie/snapshots/test_services.ambr +++ b/tests/components/mealie/snapshots/test_services.ambr @@ -5,6 +5,7 @@ 'date_added': datetime.date(2024, 6, 29), 'description': 'The world’s most famous cake, the Original Sacher-Torte, is the consequence of several lucky twists of fate. The first was in 1832, when the Austrian State Chancellor, Prince Klemens Wenzel von Metternich, tasked his kitchen staff with concocting an extraordinary dessert to impress his special guests. As fortune had it, the chef had fallen ill that evening, leaving the apprentice chef, the then-16-year-old Franz Sacher, to perform this culinary magic trick. Metternich’s parting words to the talented teenager: “I hope you won’t disgrace me tonight.”', 'group_id': '24477569-f6af-4b53-9e3f-6d04b0ca6916', + 'household_id': None, 'image': 'SuPW', 'ingredients': list([ dict({ @@ -196,11 +197,13 @@ 'description': None, 'entry_type': , 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'mealplan_date': FakeDate(2024, 1, 22), 'mealplan_id': 230, 'recipe': dict({ 'description': "Een traybake is eigenlijk altijd een goed idee. Deze zoete aardappel curry traybake dus ook. Waarom? Omdat je alleen maar wat groenten - en in dit geval kip - op een bakplaat (traybake dus) legt, hier wat kruiden aan toevoegt en deze in de oven schuift. Ideaal dus als je geen zin hebt om lang in de keuken te staan. Maar gewoon lekker op de bank wil ploffen om te wachten tot de oven klaar is. Joe! That\\'s what we like. Deze zoete aardappel curry traybake bevat behalve zoete aardappel en curry ook kikkererwten, kippendijfilet en bloemkoolroosjes. Je gebruikt yoghurt en limoen als een soort dressing. En je serveert deze heerlijke traybake met naanbrood. Je kunt natuurljk ook voor deze traybake met chipolataworstjes gaan. Wil je graag meer ovengerechten? Dan moet je eigenlijk even kijken naar onze Ovenbijbel. Onmisbaar in je keuken! We willen je deze zoete aardappelstamppot met prei ook niet onthouden. Megalekker bordje comfortfood als je \\'t ons vraagt.", 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'image': 'AiIo', 'name': 'Zoete aardappel curry traybake', 'original_url': 'https://chickslovefood.com/recept/zoete-aardappel-curry-traybake/', @@ -216,11 +219,13 @@ 'description': None, 'entry_type': , 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'mealplan_date': FakeDate(2024, 1, 23), 'mealplan_id': 229, 'recipe': dict({ 'description': 'The BEST Roast Chicken recipe is simple, budget friendly, and gives you a tender, mouth-watering chicken full of flavor! Served with roasted vegetables, this recipe is simple enough for any cook!', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'image': 'JeQ2', 'name': 'Roast Chicken', 'original_url': 'https://tastesbetterfromscratch.com/roast-chicken/', @@ -236,11 +241,13 @@ 'description': None, 'entry_type': , 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'mealplan_date': FakeDate(2024, 1, 23), 'mealplan_id': 226, 'recipe': dict({ 'description': 'Te explicamos paso a paso, de manera sencilla, la elaboración de la receta de pollo al curry con leche de coco en 10 minutos. Ingredientes, tiempo de...', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'image': 'INQz', 'name': 'Receta de pollo al curry en 10 minutos (con vídeo incluido)', 'original_url': 'https://www.directoalpaladar.com/recetas-de-carnes-y-aves/receta-de-pollo-al-curry-en-10-minutos', @@ -256,11 +263,13 @@ 'description': None, 'entry_type': , 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'mealplan_date': FakeDate(2024, 1, 23), 'mealplan_id': 224, 'recipe': dict({ 'description': 'bourguignon, oignon, carotte, bouquet garni, vin rouge, beurre, sel, poivre', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'image': 'nj5M', 'name': 'Boeuf bourguignon : la vraie recette (2)', 'original_url': 'https://www.marmiton.org/recettes/recette_boeuf-bourguignon_18889.aspx', @@ -276,11 +285,13 @@ 'description': None, 'entry_type': , 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'mealplan_date': FakeDate(2024, 1, 23), 'mealplan_id': 222, 'recipe': dict({ 'description': 'Εύκολη μακαρονάδα με κεφτεδάκια στον φούρνο από τον Άκη Πετρετζίκη. Φτιάξτε την πιο εύκολη μακαρονάδα με κεφτεδάκια σε μόνο ένα σκεύος.', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'image': 'En9o', 'name': 'Εύκολη μακαρονάδα με κεφτεδάκια στον φούρνο (1)', 'original_url': 'https://akispetretzikis.com/recipe/7959/efkolh-makaronada-me-keftedakia-ston-fourno', @@ -296,11 +307,13 @@ 'description': None, 'entry_type': , 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'mealplan_date': FakeDate(2024, 1, 23), 'mealplan_id': 221, 'recipe': dict({ 'description': 'Delicious Greek turkey meatballs with lemon orzo, tender veggies, and a creamy feta yogurt sauce. These healthy baked Greek turkey meatballs are filled with tons of wonderful herbs and make the perfect protein-packed weeknight meal!', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'image': 'Kn62', 'name': 'Greek Turkey Meatballs with Lemon Orzo & Creamy Feta Yogurt Sauce', 'original_url': 'https://www.ambitiouskitchen.com/greek-turkey-meatballs/', @@ -316,11 +329,13 @@ 'description': None, 'entry_type': , 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'mealplan_date': FakeDate(2024, 1, 23), 'mealplan_id': 220, 'recipe': dict({ 'description': 'Einfacher Nudelauflauf mit Brokkoli, Sahnesauce und extra Käse. Dieses vegetarische 5 Zutaten Rezept ist super schnell gemacht und SO gut!', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'image': 'nOPT', 'name': 'Einfacher Nudelauflauf mit Brokkoli', 'original_url': 'https://kochkarussell.com/einfacher-nudelauflauf-brokkoli/', @@ -336,11 +351,13 @@ 'description': None, 'entry_type': , 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'mealplan_date': FakeDate(2024, 1, 23), 'mealplan_id': 219, 'recipe': dict({ 'description': 'This is a modified Pampered Chef recipe. You can use a trifle bowl or large glass punch/salad bowl to show it off. It is really easy to make and I never have any leftovers. Cook time includes chill time.', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'image': 'ibL6', 'name': 'Pampered Chef Double Chocolate Mocha Trifle', 'original_url': 'https://www.food.com/recipe/pampered-chef-double-chocolate-mocha-trifle-74963', @@ -356,11 +373,13 @@ 'description': None, 'entry_type': , 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'mealplan_date': FakeDate(2024, 1, 22), 'mealplan_id': 217, 'recipe': dict({ 'description': 'Cheeseburger Sliders are juicy, cheesy and beefy - everything we love about classic burgers! These sliders are quick and easy plus they are make-ahead and reheat really well.', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'image': 'beGq', 'name': 'Cheeseburger Sliders (Easy, 30-min Recipe)', 'original_url': 'https://natashaskitchen.com/cheeseburger-sliders/', @@ -376,11 +395,13 @@ 'description': None, 'entry_type': , 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'mealplan_date': FakeDate(2024, 1, 22), 'mealplan_id': 216, 'recipe': dict({ 'description': 'This All-American beef stew recipe includes tender beef coated in a rich, intense sauce and vegetables that bring complementary texture and flavor.', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'image': '356X', 'name': 'All-American Beef Stew Recipe', 'original_url': 'https://www.seriouseats.com/all-american-beef-stew-recipe', @@ -396,11 +417,13 @@ 'description': None, 'entry_type': , 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'mealplan_date': FakeDate(2024, 1, 23), 'mealplan_id': 212, 'recipe': dict({ 'description': 'This All-American beef stew recipe includes tender beef coated in a rich, intense sauce and vegetables that bring complementary texture and flavor.', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'image': '356X', 'name': 'All-American Beef Stew Recipe', 'original_url': 'https://www.seriouseats.com/all-american-beef-stew-recipe', @@ -416,11 +439,13 @@ 'description': None, 'entry_type': , 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'mealplan_date': FakeDate(2024, 1, 22), 'mealplan_id': 211, 'recipe': dict({ 'description': 'Einfacher Nudelauflauf mit Brokkoli, Sahnesauce und extra Käse. Dieses vegetarische 5 Zutaten Rezept ist super schnell gemacht und SO gut!', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'image': 'nOPT', 'name': 'Einfacher Nudelauflauf mit Brokkoli', 'original_url': 'https://kochkarussell.com/einfacher-nudelauflauf-brokkoli/', @@ -436,11 +461,13 @@ 'description': None, 'entry_type': , 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'mealplan_date': FakeDate(2024, 1, 23), 'mealplan_id': 196, 'recipe': dict({ 'description': 'Simple to prepare and ready in 25 minutes, this vegetarian miso noodle recipe can be eaten on its own or served as a side.', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'image': '5G1v', 'name': 'Miso Udon Noodles with Spinach and Tofu', 'original_url': 'https://www.allrecipes.com/recipe/284039/miso-udon-noodles-with-spinach-and-tofu/', @@ -456,11 +483,13 @@ 'description': None, 'entry_type': , 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'mealplan_date': FakeDate(2024, 1, 22), 'mealplan_id': 195, 'recipe': dict({ 'description': 'Avis aux nostalgiques des années 1980, la mousse de saumon est de retour dans une présentation adaptée au goût du jour. On utilise une technique sans faille : un saumon frais cuit au micro-ondes et mélangé au robot avec du fromage à la crème et de la crème sure. On obtient ainsi une texture onctueuse à tartiner, qui n’a rien à envier aux préparations gélatineuses d’antan !', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'image': 'rrNL', 'name': 'Mousse de saumon', 'original_url': 'https://www.ricardocuisine.com/recettes/8919-mousse-de-saumon', @@ -476,6 +505,7 @@ 'description': 'Dineren met de boys', 'entry_type': , 'group_id': '3931df86-0679-4579-8c63-4bedc9ca9a85', + 'household_id': None, 'mealplan_date': FakeDate(2024, 1, 21), 'mealplan_id': 1, 'recipe': None, @@ -491,6 +521,7 @@ 'date_added': datetime.date(2024, 6, 29), 'description': 'The world’s most famous cake, the Original Sacher-Torte, is the consequence of several lucky twists of fate. The first was in 1832, when the Austrian State Chancellor, Prince Klemens Wenzel von Metternich, tasked his kitchen staff with concocting an extraordinary dessert to impress his special guests. As fortune had it, the chef had fallen ill that evening, leaving the apprentice chef, the then-16-year-old Franz Sacher, to perform this culinary magic trick. Metternich’s parting words to the talented teenager: “I hope you won’t disgrace me tonight.”', 'group_id': '24477569-f6af-4b53-9e3f-6d04b0ca6916', + 'household_id': None, 'image': 'SuPW', 'ingredients': list([ dict({ @@ -681,11 +712,13 @@ 'description': None, 'entry_type': , 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'mealplan_date': datetime.date(2024, 1, 22), 'mealplan_id': 230, 'recipe': dict({ 'description': "Een traybake is eigenlijk altijd een goed idee. Deze zoete aardappel curry traybake dus ook. Waarom? Omdat je alleen maar wat groenten - en in dit geval kip - op een bakplaat (traybake dus) legt, hier wat kruiden aan toevoegt en deze in de oven schuift. Ideaal dus als je geen zin hebt om lang in de keuken te staan. Maar gewoon lekker op de bank wil ploffen om te wachten tot de oven klaar is. Joe! That\\'s what we like. Deze zoete aardappel curry traybake bevat behalve zoete aardappel en curry ook kikkererwten, kippendijfilet en bloemkoolroosjes. Je gebruikt yoghurt en limoen als een soort dressing. En je serveert deze heerlijke traybake met naanbrood. Je kunt natuurljk ook voor deze traybake met chipolataworstjes gaan. Wil je graag meer ovengerechten? Dan moet je eigenlijk even kijken naar onze Ovenbijbel. Onmisbaar in je keuken! We willen je deze zoete aardappelstamppot met prei ook niet onthouden. Megalekker bordje comfortfood als je \\'t ons vraagt.", 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'image': 'AiIo', 'name': 'Zoete aardappel curry traybake', 'original_url': 'https://chickslovefood.com/recept/zoete-aardappel-curry-traybake/', @@ -705,11 +738,13 @@ 'description': None, 'entry_type': , 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'mealplan_date': datetime.date(2024, 1, 22), 'mealplan_id': 230, 'recipe': dict({ 'description': "Een traybake is eigenlijk altijd een goed idee. Deze zoete aardappel curry traybake dus ook. Waarom? Omdat je alleen maar wat groenten - en in dit geval kip - op een bakplaat (traybake dus) legt, hier wat kruiden aan toevoegt en deze in de oven schuift. Ideaal dus als je geen zin hebt om lang in de keuken te staan. Maar gewoon lekker op de bank wil ploffen om te wachten tot de oven klaar is. Joe! That\\'s what we like. Deze zoete aardappel curry traybake bevat behalve zoete aardappel en curry ook kikkererwten, kippendijfilet en bloemkoolroosjes. Je gebruikt yoghurt en limoen als een soort dressing. En je serveert deze heerlijke traybake met naanbrood. Je kunt natuurljk ook voor deze traybake met chipolataworstjes gaan. Wil je graag meer ovengerechten? Dan moet je eigenlijk even kijken naar onze Ovenbijbel. Onmisbaar in je keuken! We willen je deze zoete aardappelstamppot met prei ook niet onthouden. Megalekker bordje comfortfood als je \\'t ons vraagt.", 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'image': 'AiIo', 'name': 'Zoete aardappel curry traybake', 'original_url': 'https://chickslovefood.com/recept/zoete-aardappel-curry-traybake/', @@ -729,11 +764,13 @@ 'description': None, 'entry_type': , 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'mealplan_date': datetime.date(2024, 1, 22), 'mealplan_id': 230, 'recipe': dict({ 'description': "Een traybake is eigenlijk altijd een goed idee. Deze zoete aardappel curry traybake dus ook. Waarom? Omdat je alleen maar wat groenten - en in dit geval kip - op een bakplaat (traybake dus) legt, hier wat kruiden aan toevoegt en deze in de oven schuift. Ideaal dus als je geen zin hebt om lang in de keuken te staan. Maar gewoon lekker op de bank wil ploffen om te wachten tot de oven klaar is. Joe! That\\'s what we like. Deze zoete aardappel curry traybake bevat behalve zoete aardappel en curry ook kikkererwten, kippendijfilet en bloemkoolroosjes. Je gebruikt yoghurt en limoen als een soort dressing. En je serveert deze heerlijke traybake met naanbrood. Je kunt natuurljk ook voor deze traybake met chipolataworstjes gaan. Wil je graag meer ovengerechten? Dan moet je eigenlijk even kijken naar onze Ovenbijbel. Onmisbaar in je keuken! We willen je deze zoete aardappelstamppot met prei ook niet onthouden. Megalekker bordje comfortfood als je \\'t ons vraagt.", 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, 'image': 'AiIo', 'name': 'Zoete aardappel curry traybake', 'original_url': 'https://chickslovefood.com/recept/zoete-aardappel-curry-traybake/', From 411b014da2237df1a77426e7a80893d4d7ab636c Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 30 Aug 2024 20:08:46 +0200 Subject: [PATCH 138/271] Bump version to 2024.9.0b2 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 9e2a0ba9a2c..e2026800727 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "0b1" +PATCH_VERSION: Final = "0b2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index b960d559746..5b78a55a831 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.9.0b1" +version = "2024.9.0b2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 234f32265ea3e209b453be509203b04350106f74 Mon Sep 17 00:00:00 2001 From: Jeef Date: Sun, 1 Sep 2024 04:48:38 -0600 Subject: [PATCH 139/271] Bump Intellifire to 4.1.9 (#121091) * rebase * Minor patch to fix duplicate DeviceInfo beign created - if data hasnt updated yet * rebase * Minor patch to fix duplicate DeviceInfo beign created - if data hasnt updated yet * fixing formatting * Update homeassistant/components/intellifire/__init__.py Co-authored-by: Erik Montnemery * Update homeassistant/components/intellifire/__init__.py Co-authored-by: Erik Montnemery * Removing cloud connectivity sensor - leaving local one in * Renaming class to something more useful * addressing pr * Update homeassistant/components/intellifire/__init__.py Co-authored-by: Erik Montnemery * add ruff exception * Fix test annotations * remove access to private variable * Bumping to 4.1.9 instead of 4.1.5 * A renaming * rename * Updated testing * Update __init__.py Co-authored-by: Joost Lekkerkerker * updateing styrings * Update tests/components/intellifire/conftest.py Co-authored-by: Joost Lekkerkerker * Testing refactor - WIP * everything is passing - cleanup still needed * cleaning up comments * update pr * unrename * Update homeassistant/components/intellifire/coordinator.py Co-authored-by: Joost Lekkerkerker * fixing sentence * fixed fixture and removed error codes * reverted a bad change * fixing strings.json * revert renaming * fix * typing inother pr * adding extra tests - one has a really dumb name * using a real value * added a migration in * Update homeassistant/components/intellifire/config_flow.py Co-authored-by: Joost Lekkerkerker * Update tests/components/intellifire/test_init.py Co-authored-by: Joost Lekkerkerker * cleanup continues * addressing pr * switch back to debug * Update tests/components/intellifire/conftest.py Co-authored-by: Joost Lekkerkerker * some changes * restore property mock cuase didnt work otherwise * cleanup has begun * removed extra text * addressing pr stuff * fixed reauth --------- Co-authored-by: Erik Montnemery Co-authored-by: Joost Lekkerkerker --- .../components/intellifire/__init__.py | 168 ++-- .../components/intellifire/binary_sensor.py | 4 +- .../components/intellifire/climate.py | 2 +- .../components/intellifire/config_flow.py | 399 +++++----- homeassistant/components/intellifire/const.py | 19 +- .../components/intellifire/coordinator.py | 50 +- .../components/intellifire/entity.py | 6 +- homeassistant/components/intellifire/fan.py | 10 +- homeassistant/components/intellifire/light.py | 9 +- .../components/intellifire/manifest.json | 2 +- .../components/intellifire/sensor.py | 53 +- .../components/intellifire/strings.json | 35 +- .../components/intellifire/switch.py | 29 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/intellifire/__init__.py | 12 + tests/components/intellifire/conftest.py | 242 +++++- .../intellifire/fixtures/local_poll.json | 29 + .../intellifire/fixtures/user_data_1.json | 17 + .../intellifire/fixtures/user_data_3.json | 33 + .../snapshots/test_binary_sensor.ambr | 717 ++++++++++++++++++ .../intellifire/snapshots/test_climate.ambr | 66 ++ .../intellifire/snapshots/test_sensor.ambr | 587 ++++++++++++++ .../intellifire/test_binary_sensor.py | 35 + tests/components/intellifire/test_climate.py | 34 + .../intellifire/test_config_flow.py | 415 ++++------ tests/components/intellifire/test_init.py | 111 +++ tests/components/intellifire/test_sensor.py | 35 + 28 files changed, 2445 insertions(+), 678 deletions(-) create mode 100644 tests/components/intellifire/fixtures/local_poll.json create mode 100644 tests/components/intellifire/fixtures/user_data_1.json create mode 100644 tests/components/intellifire/fixtures/user_data_3.json create mode 100644 tests/components/intellifire/snapshots/test_binary_sensor.ambr create mode 100644 tests/components/intellifire/snapshots/test_climate.ambr create mode 100644 tests/components/intellifire/snapshots/test_sensor.ambr create mode 100644 tests/components/intellifire/test_binary_sensor.py create mode 100644 tests/components/intellifire/test_climate.py create mode 100644 tests/components/intellifire/test_init.py create mode 100644 tests/components/intellifire/test_sensor.py diff --git a/homeassistant/components/intellifire/__init__.py b/homeassistant/components/intellifire/__init__.py index 7af472c8745..7609398673b 100644 --- a/homeassistant/components/intellifire/__init__.py +++ b/homeassistant/components/intellifire/__init__.py @@ -2,15 +2,17 @@ from __future__ import annotations -from aiohttp import ClientConnectionError -from intellifire4py import IntellifireControlAsync -from intellifire4py.exceptions import LoginException -from intellifire4py.intellifire import IntellifireAPICloud, IntellifireAPILocal +import asyncio + +from intellifire4py import UnifiedFireplace +from intellifire4py.cloud_interface import IntelliFireCloudInterface +from intellifire4py.model import IntelliFireCommonFireplaceData from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_API_KEY, CONF_HOST, + CONF_IP_ADDRESS, CONF_PASSWORD, CONF_USERNAME, Platform, @@ -18,7 +20,18 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from .const import CONF_USER_ID, DOMAIN, LOGGER +from .const import ( + CONF_AUTH_COOKIE, + CONF_CONTROL_MODE, + CONF_READ_MODE, + CONF_SERIAL, + CONF_USER_ID, + CONF_WEB_CLIENT_ID, + DOMAIN, + INIT_WAIT_TIME_SECONDS, + LOGGER, + STARTUP_TIMEOUT, +) from .coordinator import IntellifireDataUpdateCoordinator PLATFORMS = [ @@ -32,79 +45,114 @@ PLATFORMS = [ ] +def _construct_common_data(entry: ConfigEntry) -> IntelliFireCommonFireplaceData: + """Convert config entry data into IntelliFireCommonFireplaceData.""" + + return IntelliFireCommonFireplaceData( + auth_cookie=entry.data[CONF_AUTH_COOKIE], + user_id=entry.data[CONF_USER_ID], + web_client_id=entry.data[CONF_WEB_CLIENT_ID], + serial=entry.data[CONF_SERIAL], + api_key=entry.data[CONF_API_KEY], + ip_address=entry.data[CONF_IP_ADDRESS], + read_mode=entry.options[CONF_READ_MODE], + control_mode=entry.options[CONF_CONTROL_MODE], + ) + + +async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Migrate entries.""" + LOGGER.debug( + "Migrating configuration from version %s.%s", + config_entry.version, + config_entry.minor_version, + ) + + if config_entry.version == 1: + new = {**config_entry.data} + + if config_entry.minor_version < 2: + username = config_entry.data[CONF_USERNAME] + password = config_entry.data[CONF_PASSWORD] + + # Create a Cloud Interface + async with IntelliFireCloudInterface() as cloud_interface: + await cloud_interface.login_with_credentials( + username=username, password=password + ) + + new_data = cloud_interface.user_data.get_data_for_ip(new[CONF_HOST]) + + if not new_data: + raise ConfigEntryAuthFailed + new[CONF_API_KEY] = new_data.api_key + new[CONF_WEB_CLIENT_ID] = new_data.web_client_id + new[CONF_AUTH_COOKIE] = new_data.auth_cookie + + new[CONF_IP_ADDRESS] = new_data.ip_address + new[CONF_SERIAL] = new_data.serial + + hass.config_entries.async_update_entry( + config_entry, + data=new, + options={CONF_READ_MODE: "local", CONF_CONTROL_MODE: "local"}, + unique_id=new[CONF_SERIAL], + version=1, + minor_version=2, + ) + LOGGER.debug("Pseudo Migration %s successful", config_entry.version) + + return True + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up IntelliFire from a config entry.""" - LOGGER.debug("Setting up config entry: %s", entry.unique_id) if CONF_USERNAME not in entry.data: - LOGGER.debug("Old config entry format detected: %s", entry.unique_id) + LOGGER.debug("Config entry without username detected: %s", entry.unique_id) raise ConfigEntryAuthFailed - ift_control = IntellifireControlAsync( - fireplace_ip=entry.data[CONF_HOST], - ) try: - await ift_control.login( - username=entry.data[CONF_USERNAME], - password=entry.data[CONF_PASSWORD], + fireplace: UnifiedFireplace = ( + await UnifiedFireplace.build_fireplace_from_common( + _construct_common_data(entry) + ) ) - except (ConnectionError, ClientConnectionError) as err: - raise ConfigEntryNotReady from err - except LoginException as err: - raise ConfigEntryAuthFailed(err) from err - - finally: - await ift_control.close() - - # Extract API Key and User_ID from ift_control - # Eventually this will migrate to using IntellifireAPICloud - - if CONF_USER_ID not in entry.data or CONF_API_KEY not in entry.data: - LOGGER.info( - "Updating intellifire config entry for %s with api information", - entry.unique_id, - ) - cloud_api = IntellifireAPICloud() - await cloud_api.login( - username=entry.data[CONF_USERNAME], - password=entry.data[CONF_PASSWORD], - ) - api_key = cloud_api.get_fireplace_api_key() - user_id = cloud_api.get_user_id() - # Update data entry - hass.config_entries.async_update_entry( - entry, - data={ - **entry.data, - CONF_API_KEY: api_key, - CONF_USER_ID: user_id, - }, + LOGGER.debug("Waiting for Fireplace to Initialize") + await asyncio.wait_for( + _async_wait_for_initialization(fireplace), timeout=STARTUP_TIMEOUT ) + except TimeoutError as err: + raise ConfigEntryNotReady( + "Initialization of fireplace timed out after 10 minutes" + ) from err - else: - api_key = entry.data[CONF_API_KEY] - user_id = entry.data[CONF_USER_ID] - - # Instantiate local control - api = IntellifireAPILocal( - fireplace_ip=entry.data[CONF_HOST], - api_key=api_key, - user_id=user_id, + # Construct coordinator + data_update_coordinator = IntellifireDataUpdateCoordinator( + hass=hass, fireplace=fireplace ) - # Define the update coordinator - coordinator = IntellifireDataUpdateCoordinator( - hass=hass, - api=api, - ) + LOGGER.debug("Fireplace to Initialized - Awaiting first refresh") + await data_update_coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = data_update_coordinator - await coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True +async def _async_wait_for_initialization( + fireplace: UnifiedFireplace, timeout=STARTUP_TIMEOUT +): + """Wait for a fireplace to be initialized.""" + while ( + fireplace.data.ipv4_address == "127.0.0.1" and fireplace.data.serial == "unset" + ): + LOGGER.debug(f"Waiting for fireplace to initialize [{fireplace.read_mode}]") + await asyncio.sleep(INIT_WAIT_TIME_SECONDS) + + async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): diff --git a/homeassistant/components/intellifire/binary_sensor.py b/homeassistant/components/intellifire/binary_sensor.py index a1b8865c876..f0a5d84fa62 100644 --- a/homeassistant/components/intellifire/binary_sensor.py +++ b/homeassistant/components/intellifire/binary_sensor.py @@ -5,7 +5,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from intellifire4py import IntellifirePollData +from intellifire4py.model import IntelliFirePollData from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, @@ -26,7 +26,7 @@ from .entity import IntellifireEntity class IntellifireBinarySensorRequiredKeysMixin: """Mixin for required keys.""" - value_fn: Callable[[IntellifirePollData], bool] + value_fn: Callable[[IntelliFirePollData], bool] @dataclass(frozen=True) diff --git a/homeassistant/components/intellifire/climate.py b/homeassistant/components/intellifire/climate.py index ed4facffc67..4eddde5ff10 100644 --- a/homeassistant/components/intellifire/climate.py +++ b/homeassistant/components/intellifire/climate.py @@ -69,7 +69,7 @@ class IntellifireClimate(IntellifireEntity, ClimateEntity): super().__init__(coordinator, description) if coordinator.data.thermostat_on: - self.last_temp = coordinator.data.thermostat_setpoint_c + self.last_temp = int(coordinator.data.thermostat_setpoint_c) @property def hvac_mode(self) -> HVACMode: diff --git a/homeassistant/components/intellifire/config_flow.py b/homeassistant/components/intellifire/config_flow.py index 268fc6623d3..56f0d5ca6a5 100644 --- a/homeassistant/components/intellifire/config_flow.py +++ b/homeassistant/components/intellifire/config_flow.py @@ -7,16 +7,33 @@ from dataclasses import dataclass from typing import Any from aiohttp import ClientConnectionError -from intellifire4py import AsyncUDPFireplaceFinder -from intellifire4py.exceptions import LoginException -from intellifire4py.intellifire import IntellifireAPICloud, IntellifireAPILocal +from intellifire4py.cloud_interface import IntelliFireCloudInterface +from intellifire4py.exceptions import LoginError +from intellifire4py.local_api import IntelliFireAPILocal +from intellifire4py.model import IntelliFireCommonFireplaceData import voluptuous as vol from homeassistant.components.dhcp import DhcpServiceInfo -from homeassistant.config_entries import ConfigFlow, ConfigFlowResult -from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult +from homeassistant.const import ( + CONF_API_KEY, + CONF_HOST, + CONF_IP_ADDRESS, + CONF_PASSWORD, + CONF_USERNAME, +) -from .const import CONF_USER_ID, DOMAIN, LOGGER +from .const import ( + API_MODE_LOCAL, + CONF_AUTH_COOKIE, + CONF_CONTROL_MODE, + CONF_READ_MODE, + CONF_SERIAL, + CONF_USER_ID, + CONF_WEB_CLIENT_ID, + DOMAIN, + LOGGER, +) STEP_USER_DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str}) @@ -31,17 +48,20 @@ class DiscoveredHostInfo: serial: str | None -async def validate_host_input(host: str, dhcp_mode: bool = False) -> str: +async def _async_poll_local_fireplace_for_serial( + host: str, dhcp_mode: bool = False +) -> str: """Validate the user input allows us to connect. Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. """ LOGGER.debug("Instantiating IntellifireAPI with host: [%s]", host) - api = IntellifireAPILocal(fireplace_ip=host) + api = IntelliFireAPILocal(fireplace_ip=host) await api.poll(suppress_warnings=dhcp_mode) serial = api.data.serial LOGGER.debug("Found a fireplace: %s", serial) + # Return the serial number which will be used to calculate a unique ID for the device/sensors return serial @@ -50,239 +70,206 @@ class IntelliFireConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for IntelliFire.""" VERSION = 1 + MINOR_VERSION = 2 def __init__(self) -> None: """Initialize the Config Flow Handler.""" - self._host: str = "" - self._serial: str = "" - self._not_configured_hosts: list[DiscoveredHostInfo] = [] + + # DHCP Variables + self._dhcp_discovered_serial: str = "" # used only in discovery mode self._discovered_host: DiscoveredHostInfo + self._dhcp_mode = False + self._is_reauth = False + + self._not_configured_hosts: list[DiscoveredHostInfo] = [] self._reauth_needed: DiscoveredHostInfo - async def _find_fireplaces(self): - """Perform UDP discovery.""" - fireplace_finder = AsyncUDPFireplaceFinder() - discovered_hosts = await fireplace_finder.search_fireplace(timeout=12) - configured_hosts = { - entry.data[CONF_HOST] - for entry in self._async_current_entries(include_ignore=False) - if CONF_HOST in entry.data # CONF_HOST will be missing for ignored entries - } + self._configured_serials: list[str] = [] - self._not_configured_hosts = [ - DiscoveredHostInfo(ip, None) - for ip in discovered_hosts - if ip not in configured_hosts - ] - LOGGER.debug("Discovered Hosts: %s", discovered_hosts) - LOGGER.debug("Configured Hosts: %s", configured_hosts) - LOGGER.debug("Not Configured Hosts: %s", self._not_configured_hosts) - - async def validate_api_access_and_create_or_update( - self, *, host: str, username: str, password: str, serial: str - ): - """Validate username/password against api.""" - LOGGER.debug("Attempting login to iftapi with: %s", username) - - ift_cloud = IntellifireAPICloud() - await ift_cloud.login(username=username, password=password) - api_key = ift_cloud.get_fireplace_api_key() - user_id = ift_cloud.get_user_id() - - data = { - CONF_HOST: host, - CONF_PASSWORD: password, - CONF_USERNAME: username, - CONF_API_KEY: api_key, - CONF_USER_ID: user_id, - } - - # Update or Create - existing_entry = await self.async_set_unique_id(serial) - if existing_entry: - self.hass.config_entries.async_update_entry(existing_entry, data=data) - await self.hass.config_entries.async_reload(existing_entry.entry_id) - return self.async_abort(reason="reauth_successful") - return self.async_create_entry(title=f"Fireplace {serial}", data=data) - - async def async_step_api_config( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Configure API access.""" - - errors = {} - control_schema = vol.Schema( - { - vol.Required(CONF_USERNAME): str, - vol.Required(CONF_PASSWORD): str, - } - ) - - if user_input is not None: - control_schema = vol.Schema( - { - vol.Required( - CONF_USERNAME, default=user_input.get(CONF_USERNAME, "") - ): str, - vol.Required( - CONF_PASSWORD, default=user_input.get(CONF_PASSWORD, "") - ): str, - } - ) - - try: - return await self.validate_api_access_and_create_or_update( - host=self._host, - username=user_input[CONF_USERNAME], - password=user_input[CONF_PASSWORD], - serial=self._serial, - ) - - except (ConnectionError, ClientConnectionError): - errors["base"] = "iftapi_connect" - LOGGER.error( - "Could not connect to iftapi.net over https - verify connectivity" - ) - except LoginException: - errors["base"] = "api_error" - LOGGER.error("Invalid credentials for iftapi.net") - - return self.async_show_form( - step_id="api_config", errors=errors, data_schema=control_schema - ) - - async def _async_validate_ip_and_continue(self, host: str) -> ConfigFlowResult: - """Validate local config and continue.""" - self._async_abort_entries_match({CONF_HOST: host}) - self._serial = await validate_host_input(host) - await self.async_set_unique_id(self._serial, raise_on_progress=False) - self._abort_if_unique_id_configured(updates={CONF_HOST: host}) - # Store current data and jump to next stage - self._host = host - - return await self.async_step_api_config() - - async def async_step_manual_device_entry(self, user_input=None): - """Handle manual input of local IP configuration.""" - LOGGER.debug("STEP: manual_device_entry") - errors = {} - self._host = user_input.get(CONF_HOST) if user_input else None - if user_input is not None: - try: - return await self._async_validate_ip_and_continue(self._host) - except (ConnectionError, ClientConnectionError): - errors["base"] = "cannot_connect" - - return self.async_show_form( - step_id="manual_device_entry", - errors=errors, - data_schema=vol.Schema({vol.Required(CONF_HOST, default=self._host): str}), - ) - - async def async_step_pick_device( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Pick which device to configure.""" - errors = {} - LOGGER.debug("STEP: pick_device") - - if user_input is not None: - if user_input[CONF_HOST] == MANUAL_ENTRY_STRING: - return await self.async_step_manual_device_entry() - - try: - return await self._async_validate_ip_and_continue(user_input[CONF_HOST]) - except (ConnectionError, ClientConnectionError): - errors["base"] = "cannot_connect" - - return self.async_show_form( - step_id="pick_device", - errors=errors, - data_schema=vol.Schema( - { - vol.Required(CONF_HOST): vol.In( - [host.ip for host in self._not_configured_hosts] - + [MANUAL_ENTRY_STRING] - ) - } - ), - ) + # Define a cloud api interface we can use + self.cloud_api_interface = IntelliFireCloudInterface() async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Start the user flow.""" - # Launch fireplaces discovery - await self._find_fireplaces() - LOGGER.debug("STEP: user") - if self._not_configured_hosts: - LOGGER.debug("Running Step: pick_device") - return await self.async_step_pick_device() - LOGGER.debug("Running Step: manual_device_entry") - return await self.async_step_manual_device_entry() + current_entries = self._async_current_entries(include_ignore=False) + self._configured_serials = [ + entry.data[CONF_SERIAL] for entry in current_entries + ] + + return await self.async_step_cloud_api() + + async def async_step_cloud_api( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Authenticate against IFTAPI Cloud in order to see configured devices. + + Local control of IntelliFire devices requires that the user download the correct API KEY which is only available on the cloud. Cloud control of the devices requires the user has at least once authenticated against the cloud and a set of cookie variables have been stored locally. + + """ + errors: dict[str, str] = {} + LOGGER.debug("STEP: cloud_api") + + if user_input is not None: + try: + async with self.cloud_api_interface as cloud_interface: + await cloud_interface.login_with_credentials( + username=user_input[CONF_USERNAME], + password=user_input[CONF_PASSWORD], + ) + + # If login was successful pass username/password to next step + return await self.async_step_pick_cloud_device() + except LoginError: + errors["base"] = "api_error" + + return self.async_show_form( + step_id="cloud_api", + errors=errors, + data_schema=vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } + ), + ) + + async def async_step_pick_cloud_device( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Step to select a device from the cloud. + + We can only get here if we have logged in. If there is only one device available it will be auto-configured, + else the user will be given a choice to pick a device. + """ + errors: dict[str, str] = {} + LOGGER.debug( + f"STEP: pick_cloud_device: {user_input} - DHCP_MODE[{self._dhcp_mode}" + ) + + if self._dhcp_mode or user_input is not None: + if self._dhcp_mode: + serial = self._dhcp_discovered_serial + LOGGER.debug(f"DHCP Mode detected for serial [{serial}]") + if user_input is not None: + serial = user_input[CONF_SERIAL] + + # Run a unique ID Check prior to anything else + await self.async_set_unique_id(serial) + self._abort_if_unique_id_configured(updates={CONF_SERIAL: serial}) + + # If Serial is Good obtain fireplace and configure + fireplace = self.cloud_api_interface.user_data.get_data_for_serial(serial) + if fireplace: + return await self._async_create_config_entry_from_common_data( + fireplace=fireplace + ) + + # Parse User Data to see if we auto-configure or prompt for selection: + user_data = self.cloud_api_interface.user_data + + available_fireplaces: list[IntelliFireCommonFireplaceData] = [ + fp + for fp in user_data.fireplaces + if fp.serial not in self._configured_serials + ] + + # Abort if all devices have been configured + if not available_fireplaces: + return self.async_abort(reason="no_available_devices") + + # If there is a single fireplace configure it + if len(available_fireplaces) == 1: + if self._is_reauth: + reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + return await self._async_create_config_entry_from_common_data( + fireplace=available_fireplaces[0], existing_entry=reauth_entry + ) + + return await self._async_create_config_entry_from_common_data( + fireplace=available_fireplaces[0] + ) + + return self.async_show_form( + step_id="pick_cloud_device", + errors=errors, + data_schema=vol.Schema( + { + vol.Required(CONF_SERIAL): vol.In( + [fp.serial for fp in available_fireplaces] + ) + } + ), + ) + + async def _async_create_config_entry_from_common_data( + self, + fireplace: IntelliFireCommonFireplaceData, + existing_entry: ConfigEntry | None = None, + ) -> ConfigFlowResult: + """Construct a config entry based on an object of IntelliFireCommonFireplaceData.""" + + data = { + CONF_IP_ADDRESS: fireplace.ip_address, + CONF_API_KEY: fireplace.api_key, + CONF_SERIAL: fireplace.serial, + CONF_AUTH_COOKIE: fireplace.auth_cookie, + CONF_WEB_CLIENT_ID: fireplace.web_client_id, + CONF_USER_ID: fireplace.user_id, + CONF_USERNAME: self.cloud_api_interface.user_data.username, + CONF_PASSWORD: self.cloud_api_interface.user_data.password, + } + + options = {CONF_READ_MODE: API_MODE_LOCAL, CONF_CONTROL_MODE: API_MODE_LOCAL} + + if existing_entry: + return self.async_update_reload_and_abort( + existing_entry, data=data, options=options + ) + return self.async_create_entry( + title=f"Fireplace {fireplace.serial}", data=data, options=options + ) async def async_step_reauth( self, entry_data: Mapping[str, Any] ) -> ConfigFlowResult: """Perform reauth upon an API authentication error.""" LOGGER.debug("STEP: reauth") + self._is_reauth = True entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) - assert entry - assert entry.unique_id # populate the expected vars - self._serial = entry.unique_id - self._host = entry.data[CONF_HOST] + self._dhcp_discovered_serial = entry.data[CONF_SERIAL] # type: ignore[union-attr] - placeholders = {CONF_HOST: self._host, "serial": self._serial} + placeholders = {"serial": self._dhcp_discovered_serial} self.context["title_placeholders"] = placeholders - return await self.async_step_api_config() + + return await self.async_step_cloud_api() async def async_step_dhcp( self, discovery_info: DhcpServiceInfo ) -> ConfigFlowResult: """Handle DHCP Discovery.""" + self._dhcp_mode = True # Run validation logic on ip - host = discovery_info.ip - LOGGER.debug("STEP: dhcp for host %s", host) + ip_address = discovery_info.ip + LOGGER.debug("STEP: dhcp for ip_address %s", ip_address) - self._async_abort_entries_match({CONF_HOST: host}) + self._async_abort_entries_match({CONF_IP_ADDRESS: ip_address}) try: - self._serial = await validate_host_input(host, dhcp_mode=True) + self._dhcp_discovered_serial = await _async_poll_local_fireplace_for_serial( + ip_address, dhcp_mode=True + ) except (ConnectionError, ClientConnectionError): LOGGER.debug( - "DHCP Discovery has determined %s is not an IntelliFire device", host + "DHCP Discovery has determined %s is not an IntelliFire device", + ip_address, ) return self.async_abort(reason="not_intellifire_device") - await self.async_set_unique_id(self._serial) - self._abort_if_unique_id_configured(updates={CONF_HOST: host}) - self._discovered_host = DiscoveredHostInfo(ip=host, serial=self._serial) - - placeholders = {CONF_HOST: host, "serial": self._serial} - self.context["title_placeholders"] = placeholders - self._set_confirm_only() - - return await self.async_step_dhcp_confirm() - - async def async_step_dhcp_confirm(self, user_input=None): - """Attempt to confirm.""" - - LOGGER.debug("STEP: dhcp_confirm") - # Add the hosts one by one - host = self._discovered_host.ip - serial = self._discovered_host.serial - - if user_input is None: - # Show the confirmation dialog - return self.async_show_form( - step_id="dhcp_confirm", - description_placeholders={CONF_HOST: host, "serial": serial}, - ) - - return self.async_create_entry( - title=f"Fireplace {serial}", - data={CONF_HOST: host}, - ) + return await self.async_step_cloud_api() diff --git a/homeassistant/components/intellifire/const.py b/homeassistant/components/intellifire/const.py index 5c8af1eefe9..f194eeaf4e2 100644 --- a/homeassistant/components/intellifire/const.py +++ b/homeassistant/components/intellifire/const.py @@ -5,11 +5,22 @@ from __future__ import annotations import logging DOMAIN = "intellifire" - -CONF_USER_ID = "user_id" - LOGGER = logging.getLogger(__package__) +DEFAULT_THERMOSTAT_TEMP = 21 + +CONF_USER_ID = "user_id" # part of the cloud cookie +CONF_WEB_CLIENT_ID = "web_client_id" # part of the cloud cookie +CONF_AUTH_COOKIE = "auth_cookie" # part of the cloud cookie CONF_SERIAL = "serial" +CONF_READ_MODE = "cloud_read" +CONF_CONTROL_MODE = "cloud_control" -DEFAULT_THERMOSTAT_TEMP = 21 + +API_MODE_LOCAL = "local" +API_MODE_CLOUD = "cloud" + + +STARTUP_TIMEOUT = 600 + +INIT_WAIT_TIME_SECONDS = 10 diff --git a/homeassistant/components/intellifire/coordinator.py b/homeassistant/components/intellifire/coordinator.py index 0a46ff61435..b4f03f4b5c8 100644 --- a/homeassistant/components/intellifire/coordinator.py +++ b/homeassistant/components/intellifire/coordinator.py @@ -2,27 +2,27 @@ from __future__ import annotations -import asyncio from datetime import timedelta -from aiohttp import ClientConnectionError -from intellifire4py import IntellifirePollData -from intellifire4py.intellifire import IntellifireAPILocal +from intellifire4py import UnifiedFireplace +from intellifire4py.control import IntelliFireController +from intellifire4py.model import IntelliFirePollData +from intellifire4py.read import IntelliFireDataProvider from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN, LOGGER -class IntellifireDataUpdateCoordinator(DataUpdateCoordinator[IntellifirePollData]): +class IntellifireDataUpdateCoordinator(DataUpdateCoordinator[IntelliFirePollData]): """Class to manage the polling of the fireplace API.""" def __init__( self, hass: HomeAssistant, - api: IntellifireAPILocal, + fireplace: UnifiedFireplace, ) -> None: """Initialize the Coordinator.""" super().__init__( @@ -31,36 +31,21 @@ class IntellifireDataUpdateCoordinator(DataUpdateCoordinator[IntellifirePollData name=DOMAIN, update_interval=timedelta(seconds=15), ) - self._api = api - async def _async_update_data(self) -> IntellifirePollData: - if not self._api.is_polling_in_background: - LOGGER.info("Starting Intellifire Background Polling Loop") - await self._api.start_background_polling() - - # Don't return uninitialized poll data - async with asyncio.timeout(15): - try: - await self._api.poll() - except (ConnectionError, ClientConnectionError) as exception: - raise UpdateFailed from exception - - LOGGER.debug("Failure Count %d", self._api.failed_poll_attempts) - if self._api.failed_poll_attempts > 10: - LOGGER.debug("Too many polling errors - raising exception") - raise UpdateFailed - - return self._api.data + self.fireplace = fireplace @property - def read_api(self) -> IntellifireAPILocal: + def read_api(self) -> IntelliFireDataProvider: """Return the Status API pointer.""" - return self._api + return self.fireplace.read_api @property - def control_api(self) -> IntellifireAPILocal: + def control_api(self) -> IntelliFireController: """Return the control API.""" - return self._api + return self.fireplace.control_api + + async def _async_update_data(self) -> IntelliFirePollData: + return self.fireplace.data @property def device_info(self) -> DeviceInfo: @@ -69,7 +54,6 @@ class IntellifireDataUpdateCoordinator(DataUpdateCoordinator[IntellifirePollData manufacturer="Hearth and Home", model="IFT-WFM", name="IntelliFire", - identifiers={("IntelliFire", f"{self.read_api.data.serial}]")}, - sw_version=self.read_api.data.fw_ver_str, - configuration_url=f"http://{self._api.fireplace_ip}/poll", + identifiers={("IntelliFire", str(self.fireplace.serial))}, + configuration_url=f"http://{self.fireplace.ip_address}/poll", ) diff --git a/homeassistant/components/intellifire/entity.py b/homeassistant/components/intellifire/entity.py index 3b35c9dabd8..571c4717ac2 100644 --- a/homeassistant/components/intellifire/entity.py +++ b/homeassistant/components/intellifire/entity.py @@ -9,7 +9,7 @@ from . import IntellifireDataUpdateCoordinator class IntellifireEntity(CoordinatorEntity[IntellifireDataUpdateCoordinator]): - """Define a generic class for Intellifire entities.""" + """Define a generic class for IntelliFire entities.""" _attr_attribution = "Data provided by unpublished Intellifire API" _attr_has_entity_name = True @@ -22,6 +22,8 @@ class IntellifireEntity(CoordinatorEntity[IntellifireDataUpdateCoordinator]): """Class initializer.""" super().__init__(coordinator=coordinator) self.entity_description = description - self._attr_unique_id = f"{description.key}_{coordinator.read_api.data.serial}" + self._attr_unique_id = f"{description.key}_{coordinator.fireplace.serial}" + self.identifiers = ({("IntelliFire", f"{coordinator.fireplace.serial}]")},) + # Configure the Device Info self._attr_device_info = self.coordinator.device_info diff --git a/homeassistant/components/intellifire/fan.py b/homeassistant/components/intellifire/fan.py index f68827b0a56..dc2fc279a5d 100644 --- a/homeassistant/components/intellifire/fan.py +++ b/homeassistant/components/intellifire/fan.py @@ -7,7 +7,8 @@ from dataclasses import dataclass import math from typing import Any -from intellifire4py import IntellifireControlAsync, IntellifirePollData +from intellifire4py.control import IntelliFireController +from intellifire4py.model import IntelliFirePollData from homeassistant.components.fan import ( FanEntity, @@ -31,8 +32,8 @@ from .entity import IntellifireEntity class IntellifireFanRequiredKeysMixin: """Required keys for fan entity.""" - set_fn: Callable[[IntellifireControlAsync, int], Awaitable] - value_fn: Callable[[IntellifirePollData], bool] + set_fn: Callable[[IntelliFireController, int], Awaitable] + value_fn: Callable[[IntelliFirePollData], int] speed_range: tuple[int, int] @@ -91,7 +92,8 @@ class IntellifireFan(IntellifireEntity, FanEntity): def percentage(self) -> int | None: """Return fan percentage.""" return ranged_value_to_percentage( - self.entity_description.speed_range, self.coordinator.read_api.data.fanspeed + self.entity_description.speed_range, + self.coordinator.read_api.data.fanspeed, ) @property diff --git a/homeassistant/components/intellifire/light.py b/homeassistant/components/intellifire/light.py index a7f2befaf33..5f25b5de823 100644 --- a/homeassistant/components/intellifire/light.py +++ b/homeassistant/components/intellifire/light.py @@ -6,7 +6,8 @@ from collections.abc import Awaitable, Callable from dataclasses import dataclass from typing import Any -from intellifire4py import IntellifireControlAsync, IntellifirePollData +from intellifire4py.control import IntelliFireController +from intellifire4py.model import IntelliFirePollData from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -27,8 +28,8 @@ from .entity import IntellifireEntity class IntellifireLightRequiredKeysMixin: """Required keys for fan entity.""" - set_fn: Callable[[IntellifireControlAsync, int], Awaitable] - value_fn: Callable[[IntellifirePollData], bool] + set_fn: Callable[[IntelliFireController, int], Awaitable] + value_fn: Callable[[IntelliFirePollData], int] @dataclass(frozen=True) @@ -56,7 +57,7 @@ class IntellifireLight(IntellifireEntity, LightEntity): _attr_supported_color_modes = {ColorMode.BRIGHTNESS} @property - def brightness(self): + def brightness(self) -> int: """Return the current brightness 0-255.""" return 85 * self.entity_description.value_fn(self.coordinator.read_api.data) diff --git a/homeassistant/components/intellifire/manifest.json b/homeassistant/components/intellifire/manifest.json index 90d41fcffe7..e3ee663e8fe 100644 --- a/homeassistant/components/intellifire/manifest.json +++ b/homeassistant/components/intellifire/manifest.json @@ -11,5 +11,5 @@ "documentation": "https://www.home-assistant.io/integrations/intellifire", "iot_class": "local_polling", "loggers": ["intellifire4py"], - "requirements": ["intellifire4py==2.2.2"] + "requirements": ["intellifire4py==4.1.9"] } diff --git a/homeassistant/components/intellifire/sensor.py b/homeassistant/components/intellifire/sensor.py index dd3eef9c9b4..eaff89d08e7 100644 --- a/homeassistant/components/intellifire/sensor.py +++ b/homeassistant/components/intellifire/sensor.py @@ -6,8 +6,6 @@ from collections.abc import Callable from dataclasses import dataclass from datetime import datetime, timedelta -from intellifire4py import IntellifirePollData - from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, @@ -29,7 +27,9 @@ from .entity import IntellifireEntity class IntellifireSensorRequiredKeysMixin: """Mixin for required keys.""" - value_fn: Callable[[IntellifirePollData], int | str | datetime | None] + value_fn: Callable[ + [IntellifireDataUpdateCoordinator], int | str | datetime | float | None + ] @dataclass(frozen=True) @@ -40,16 +40,29 @@ class IntellifireSensorEntityDescription( """Describes a sensor entity.""" -def _time_remaining_to_timestamp(data: IntellifirePollData) -> datetime | None: +def _time_remaining_to_timestamp( + coordinator: IntellifireDataUpdateCoordinator, +) -> datetime | None: """Define a sensor that takes into account timezone.""" - if not (seconds_offset := data.timeremaining_s): + if not (seconds_offset := coordinator.data.timeremaining_s): return None return utcnow() + timedelta(seconds=seconds_offset) -def _downtime_to_timestamp(data: IntellifirePollData) -> datetime | None: +def _downtime_to_timestamp( + coordinator: IntellifireDataUpdateCoordinator, +) -> datetime | None: """Define a sensor that takes into account a timezone.""" - if not (seconds_offset := data.downtime): + if not (seconds_offset := coordinator.data.downtime): + return None + return utcnow() - timedelta(seconds=seconds_offset) + + +def _uptime_to_timestamp( + coordinator: IntellifireDataUpdateCoordinator, +) -> datetime | None: + """Return a timestamp of how long the sensor has been up.""" + if not (seconds_offset := coordinator.data.uptime): return None return utcnow() - timedelta(seconds=seconds_offset) @@ -60,14 +73,14 @@ INTELLIFIRE_SENSORS: tuple[IntellifireSensorEntityDescription, ...] = ( translation_key="flame_height", state_class=SensorStateClass.MEASUREMENT, # UI uses 1-5 for flame height, backing lib uses 0-4 - value_fn=lambda data: (data.flameheight + 1), + value_fn=lambda coordinator: (coordinator.data.flameheight + 1), ), IntellifireSensorEntityDescription( key="temperature", state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, - value_fn=lambda data: data.temperature_c, + value_fn=lambda coordinator: coordinator.data.temperature_c, ), IntellifireSensorEntityDescription( key="target_temp", @@ -75,13 +88,13 @@ INTELLIFIRE_SENSORS: tuple[IntellifireSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, - value_fn=lambda data: data.thermostat_setpoint_c, + value_fn=lambda coordinator: coordinator.data.thermostat_setpoint_c, ), IntellifireSensorEntityDescription( key="fan_speed", translation_key="fan_speed", state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda data: data.fanspeed, + value_fn=lambda coordinator: coordinator.data.fanspeed, ), IntellifireSensorEntityDescription( key="timer_end_timestamp", @@ -102,27 +115,27 @@ INTELLIFIRE_SENSORS: tuple[IntellifireSensorEntityDescription, ...] = ( translation_key="uptime", entity_category=EntityCategory.DIAGNOSTIC, device_class=SensorDeviceClass.TIMESTAMP, - value_fn=lambda data: utcnow() - timedelta(seconds=data.uptime), + value_fn=_uptime_to_timestamp, ), IntellifireSensorEntityDescription( key="connection_quality", translation_key="connection_quality", entity_category=EntityCategory.DIAGNOSTIC, - value_fn=lambda data: data.connection_quality, + value_fn=lambda coordinator: coordinator.data.connection_quality, entity_registry_enabled_default=False, ), IntellifireSensorEntityDescription( key="ecm_latency", translation_key="ecm_latency", entity_category=EntityCategory.DIAGNOSTIC, - value_fn=lambda data: data.ecm_latency, + value_fn=lambda coordinator: coordinator.data.ecm_latency, entity_registry_enabled_default=False, ), IntellifireSensorEntityDescription( key="ipv4_address", translation_key="ipv4_address", entity_category=EntityCategory.DIAGNOSTIC, - value_fn=lambda data: data.ipv4_address, + value_fn=lambda coordinator: coordinator.data.ipv4_address, ), ) @@ -134,17 +147,17 @@ async def async_setup_entry( coordinator: IntellifireDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities( - IntellifireSensor(coordinator=coordinator, description=description) + IntelliFireSensor(coordinator=coordinator, description=description) for description in INTELLIFIRE_SENSORS ) -class IntellifireSensor(IntellifireEntity, SensorEntity): - """Extends IntellifireEntity with Sensor specific logic.""" +class IntelliFireSensor(IntellifireEntity, SensorEntity): + """Extends IntelliFireEntity with Sensor specific logic.""" entity_description: IntellifireSensorEntityDescription @property - def native_value(self) -> int | str | datetime | None: + def native_value(self) -> int | str | datetime | float | None: """Return the state.""" - return self.entity_description.value_fn(self.coordinator.read_api.data) + return self.entity_description.value_fn(self.coordinator) diff --git a/homeassistant/components/intellifire/strings.json b/homeassistant/components/intellifire/strings.json index 6393a4e070d..2eeb2b50b93 100644 --- a/homeassistant/components/intellifire/strings.json +++ b/homeassistant/components/intellifire/strings.json @@ -1,39 +1,30 @@ { "config": { - "flow_title": "{serial} ({host})", + "flow_title": "{serial}", "step": { - "manual_device_entry": { - "description": "Local Configuration", - "data": { - "host": "Host (IP Address)" - } + "pick_cloud_device": { + "title": "Configure fireplace", + "description": "Select fireplace by serial number:" }, - "api_config": { + "cloud_api": { + "description": "Authenticate against IntelliFire Cloud", + "data_description": { + "username": "Your IntelliFire app username", + "password": "Your IntelliFire app password" + }, "data": { "username": "[%key:common::config_flow::data::email%]", "password": "[%key:common::config_flow::data::password%]" } - }, - "dhcp_confirm": { - "description": "Do you want to set up {host}\nSerial: {serial}?" - }, - "pick_device": { - "title": "Device Selection", - "description": "The following IntelliFire devices were discovered. Please select which you wish to configure.", - "data": { - "host": "[%key:common::config_flow::data::host%]" - } } }, "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "api_error": "Login failed", - "iftapi_connect": "Error conecting to iftapi.net" + "api_error": "Login failed" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", - "not_intellifire_device": "Not an IntelliFire Device." + "not_intellifire_device": "Not an IntelliFire device.", + "no_available_devices": "All available devices have already been configured." } }, "entity": { diff --git a/homeassistant/components/intellifire/switch.py b/homeassistant/components/intellifire/switch.py index 00de6d74a9c..ac6096497b6 100644 --- a/homeassistant/components/intellifire/switch.py +++ b/homeassistant/components/intellifire/switch.py @@ -6,16 +6,13 @@ from collections.abc import Awaitable, Callable from dataclasses import dataclass from typing import Any -from intellifire4py import IntellifirePollData -from intellifire4py.intellifire import IntellifireAPILocal - from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +from . import IntellifireDataUpdateCoordinator from .const import DOMAIN -from .coordinator import IntellifireDataUpdateCoordinator from .entity import IntellifireEntity @@ -23,9 +20,9 @@ from .entity import IntellifireEntity class IntellifireSwitchRequiredKeysMixin: """Mixin for required keys.""" - on_fn: Callable[[IntellifireAPILocal], Awaitable] - off_fn: Callable[[IntellifireAPILocal], Awaitable] - value_fn: Callable[[IntellifirePollData], bool] + on_fn: Callable[[IntellifireDataUpdateCoordinator], Awaitable] + off_fn: Callable[[IntellifireDataUpdateCoordinator], Awaitable] + value_fn: Callable[[IntellifireDataUpdateCoordinator], bool] @dataclass(frozen=True) @@ -39,16 +36,16 @@ INTELLIFIRE_SWITCHES: tuple[IntellifireSwitchEntityDescription, ...] = ( IntellifireSwitchEntityDescription( key="on_off", translation_key="flame", - on_fn=lambda control_api: control_api.flame_on(), - off_fn=lambda control_api: control_api.flame_off(), - value_fn=lambda data: data.is_on, + on_fn=lambda coordinator: coordinator.control_api.flame_on(), + off_fn=lambda coordinator: coordinator.control_api.flame_off(), + value_fn=lambda coordinator: coordinator.read_api.data.is_on, ), IntellifireSwitchEntityDescription( key="pilot", translation_key="pilot_light", - on_fn=lambda control_api: control_api.pilot_on(), - off_fn=lambda control_api: control_api.pilot_off(), - value_fn=lambda data: data.pilot_on, + on_fn=lambda coordinator: coordinator.control_api.pilot_on(), + off_fn=lambda coordinator: coordinator.control_api.pilot_off(), + value_fn=lambda coordinator: coordinator.read_api.data.pilot_on, ), ) @@ -74,15 +71,15 @@ class IntellifireSwitch(IntellifireEntity, SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the switch.""" - await self.entity_description.on_fn(self.coordinator.control_api) + await self.entity_description.on_fn(self.coordinator) await self.async_update_ha_state(force_refresh=True) async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the switch.""" - await self.entity_description.off_fn(self.coordinator.control_api) + await self.entity_description.off_fn(self.coordinator) await self.async_update_ha_state(force_refresh=True) @property def is_on(self) -> bool | None: """Return the on state.""" - return self.entity_description.value_fn(self.coordinator.read_api.data) + return self.entity_description.value_fn(self.coordinator) diff --git a/requirements_all.txt b/requirements_all.txt index 3b73569373c..6037623d90f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1179,7 +1179,7 @@ inkbird-ble==0.5.8 insteon-frontend-home-assistant==0.5.0 # homeassistant.components.intellifire -intellifire4py==2.2.2 +intellifire4py==4.1.9 # homeassistant.components.iotty iottycloud==0.1.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e63e4be3e99..deed5fb713d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -987,7 +987,7 @@ inkbird-ble==0.5.8 insteon-frontend-home-assistant==0.5.0 # homeassistant.components.intellifire -intellifire4py==2.2.2 +intellifire4py==4.1.9 # homeassistant.components.iotty iottycloud==0.1.3 diff --git a/tests/components/intellifire/__init__.py b/tests/components/intellifire/__init__.py index f655ccc2fa4..50497939f7f 100644 --- a/tests/components/intellifire/__init__.py +++ b/tests/components/intellifire/__init__.py @@ -1 +1,13 @@ """Tests for the IntelliFire integration.""" + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: + """Fixture for setting up the component.""" + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/intellifire/conftest.py b/tests/components/intellifire/conftest.py index cf1e085c10f..251d5bdde48 100644 --- a/tests/components/intellifire/conftest.py +++ b/tests/components/intellifire/conftest.py @@ -1,14 +1,40 @@ """Fixtures for IntelliFire integration tests.""" from collections.abc import Generator -from unittest.mock import AsyncMock, MagicMock, Mock, patch +from unittest.mock import AsyncMock, MagicMock, Mock, PropertyMock, patch -from aiohttp.client_reqrep import ConnectionKey +from intellifire4py.const import IntelliFireApiMode +from intellifire4py.model import ( + IntelliFireCommonFireplaceData, + IntelliFirePollData, + IntelliFireUserData, +) import pytest +from homeassistant.components.intellifire.const import ( + API_MODE_CLOUD, + API_MODE_LOCAL, + CONF_AUTH_COOKIE, + CONF_CONTROL_MODE, + CONF_READ_MODE, + CONF_SERIAL, + CONF_USER_ID, + CONF_WEB_CLIENT_ID, + DOMAIN, +) +from homeassistant.const import ( + CONF_API_KEY, + CONF_HOST, + CONF_IP_ADDRESS, + CONF_PASSWORD, + CONF_USERNAME, +) + +from tests.common import MockConfigEntry, load_json_object_fixture + @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock]: +def mock_setup_entry() -> Generator[AsyncMock, None, None]: """Mock setting up a config entry.""" with patch( "homeassistant.components.intellifire.async_setup_entry", return_value=True @@ -17,44 +43,206 @@ def mock_setup_entry() -> Generator[AsyncMock]: @pytest.fixture -def mock_fireplace_finder_none() -> Generator[MagicMock]: +def mock_fireplace_finder_none() -> Generator[None, MagicMock, None]: """Mock fireplace finder.""" mock_found_fireplaces = Mock() mock_found_fireplaces.ips = [] with patch( - "homeassistant.components.intellifire.config_flow.AsyncUDPFireplaceFinder.search_fireplace" + "homeassistant.components.intellifire.config_flow.UDPFireplaceFinder.search_fireplace" ): yield mock_found_fireplaces @pytest.fixture -def mock_fireplace_finder_single() -> Generator[MagicMock]: - """Mock fireplace finder.""" - mock_found_fireplaces = Mock() - mock_found_fireplaces.ips = ["192.168.1.69"] - with patch( - "homeassistant.components.intellifire.config_flow.AsyncUDPFireplaceFinder.search_fireplace" - ): - yield mock_found_fireplaces +def mock_config_entry_current() -> MockConfigEntry: + """Return a mock config entry.""" + return MockConfigEntry( + domain=DOMAIN, + version=1, + minor_version=2, + data={ + CONF_IP_ADDRESS: "192.168.2.108", + CONF_USERNAME: "grumpypanda@china.cn", + CONF_PASSWORD: "you-stole-my-pandas", + CONF_SERIAL: "3FB284769E4736F30C8973A7ED358123", + CONF_WEB_CLIENT_ID: "FA2B1C3045601234D0AE17D72F8E975", + CONF_API_KEY: "B5C4DA27AAEF31D1FB21AFF9BFA6BCD2", + CONF_AUTH_COOKIE: "B984F21A6378560019F8A1CDE41B6782", + CONF_USER_ID: "52C3F9E8B9D3AC99F8E4D12345678901FE9A2BC7D85F7654E28BF98BCD123456", + }, + options={CONF_READ_MODE: API_MODE_LOCAL, CONF_CONTROL_MODE: API_MODE_CLOUD}, + unique_id="3FB284769E4736F30C8973A7ED358123", + ) @pytest.fixture -def mock_intellifire_config_flow() -> Generator[MagicMock]: - """Return a mocked IntelliFire client.""" - data_mock = Mock() - data_mock.serial = "12345" +def mock_config_entry_old() -> MockConfigEntry: + """For migration testing.""" + return MockConfigEntry( + domain=DOMAIN, + version=1, + minor_version=1, + title="Fireplace 3FB284769E4736F30C8973A7ED358123", + data={ + CONF_HOST: "192.168.2.108", + CONF_USERNAME: "grumpypanda@china.cn", + CONF_PASSWORD: "you-stole-my-pandas", + CONF_USER_ID: "52C3F9E8B9D3AC99F8E4D12345678901FE9A2BC7D85F7654E28BF98BCD123456", + }, + ) + +@pytest.fixture +def mock_common_data_local() -> IntelliFireCommonFireplaceData: + """Fixture for mock common data.""" + return IntelliFireCommonFireplaceData( + auth_cookie="B984F21A6378560019F8A1CDE41B6782", + user_id="52C3F9E8B9D3AC99F8E4D12345678901FE9A2BC7D85F7654E28BF98BCD123456", + web_client_id="FA2B1C3045601234D0AE17D72F8E975", + serial="3FB284769E4736F30C8973A7ED358123", + api_key="B5C4DA27AAEF31D1FB21AFF9BFA6BCD2", + ip_address="192.168.2.108", + read_mode=IntelliFireApiMode.LOCAL, + control_mode=IntelliFireApiMode.LOCAL, + ) + + +@pytest.fixture +def mock_apis_multifp( + mock_cloud_interface, mock_local_interface, mock_fp +) -> Generator[tuple[AsyncMock, AsyncMock, MagicMock], None, None]: + """Multi fireplace version of mocks.""" + return mock_local_interface, mock_cloud_interface, mock_fp + + +@pytest.fixture +def mock_apis_single_fp( + mock_cloud_interface, mock_local_interface, mock_fp +) -> Generator[tuple[AsyncMock, AsyncMock, MagicMock], None, None]: + """Single fire place version of the mocks.""" + data_v1 = IntelliFireUserData( + **load_json_object_fixture("user_data_1.json", DOMAIN) + ) + with patch.object( + type(mock_cloud_interface), "user_data", new_callable=PropertyMock + ) as mock_user_data: + mock_user_data.return_value = data_v1 + yield mock_local_interface, mock_cloud_interface, mock_fp + + +@pytest.fixture +def mock_cloud_interface() -> Generator[AsyncMock, None, None]: + """Mock cloud interface to use for testing.""" + user_data = IntelliFireUserData( + **load_json_object_fixture("user_data_3.json", DOMAIN) + ) + + with ( + patch( + "homeassistant.components.intellifire.IntelliFireCloudInterface", + autospec=True, + ) as mock_client, + patch( + "homeassistant.components.intellifire.config_flow.IntelliFireCloudInterface", + new=mock_client, + ), + patch( + "intellifire4py.cloud_interface.IntelliFireCloudInterface", + new=mock_client, + ), + ): + # Mock async context manager + mock_client = mock_client.return_value + mock_client.__aenter__ = AsyncMock(return_value=mock_client) + mock_client.__aexit__ = AsyncMock(return_value=None) + + # Mock other async methods if needed + mock_client.login_with_credentials = AsyncMock() + mock_client.poll = AsyncMock() + type(mock_client).user_data = PropertyMock(return_value=user_data) + + yield mock_client # Yielding to the test + + +@pytest.fixture +def mock_local_interface() -> Generator[AsyncMock, None, None]: + """Mock version of IntelliFireAPILocal.""" + poll_data = IntelliFirePollData( + **load_json_object_fixture("intellifire/local_poll.json") + ) with patch( - "homeassistant.components.intellifire.config_flow.IntellifireAPILocal", + "homeassistant.components.intellifire.config_flow.IntelliFireAPILocal", autospec=True, - ) as intellifire_mock: - intellifire = intellifire_mock.return_value - intellifire.data = data_mock - yield intellifire + ) as mock_client: + mock_client = mock_client.return_value + # Mock all instances of the class + type(mock_client).data = PropertyMock(return_value=poll_data) + yield mock_client -def mock_api_connection_error() -> ConnectionError: - """Return a fake a ConnectionError for iftapi.net.""" - ret = ConnectionError() - ret.args = [ConnectionKey("iftapi.net", 443, False, None, None, None, None)] - return ret +@pytest.fixture +def mock_fp(mock_common_data_local) -> Generator[AsyncMock, None, None]: + """Mock fireplace.""" + + local_poll_data = IntelliFirePollData( + **load_json_object_fixture("local_poll.json", DOMAIN) + ) + + assert local_poll_data.connection_quality == 988451 + + with patch( + "homeassistant.components.intellifire.UnifiedFireplace" + ) as mock_unified_fireplace: + # Create an instance of the mock + mock_instance = mock_unified_fireplace.return_value + + # Mock methods and properties of the instance + mock_instance.perform_cloud_poll = AsyncMock() + mock_instance.perform_local_poll = AsyncMock() + + mock_instance.async_validate_connectivity = AsyncMock(return_value=(True, True)) + + type(mock_instance).is_cloud_polling = PropertyMock(return_value=False) + type(mock_instance).is_local_polling = PropertyMock(return_value=True) + + mock_instance.get_user_data_as_json.return_value = '{"mock": "data"}' + + mock_instance.ip_address = "192.168.1.100" + mock_instance.api_key = "mock_api_key" + mock_instance.serial = "mock_serial" + mock_instance.user_id = "mock_user_id" + mock_instance.auth_cookie = "mock_auth_cookie" + mock_instance.web_client_id = "mock_web_client_id" + + # Configure the READ Api + mock_instance.read_api = MagicMock() + mock_instance.read_api.poll = MagicMock(return_value=local_poll_data) + mock_instance.read_api.data = local_poll_data + + mock_instance.control_api = MagicMock() + + mock_instance.local_connectivity = True + mock_instance.cloud_connectivity = False + + mock_instance._read_mode = IntelliFireApiMode.LOCAL + mock_instance.read_mode = IntelliFireApiMode.LOCAL + + mock_instance.control_mode = IntelliFireApiMode.LOCAL + mock_instance._control_mode = IntelliFireApiMode.LOCAL + + mock_instance.data = local_poll_data + + mock_instance.set_read_mode = AsyncMock() + mock_instance.set_control_mode = AsyncMock() + + mock_instance.async_validate_connectivity = AsyncMock( + return_value=(True, False) + ) + + # Patch class methods + with patch( + "homeassistant.components.intellifire.UnifiedFireplace.build_fireplace_from_common", + new_callable=AsyncMock, + return_value=mock_instance, + ): + yield mock_instance diff --git a/tests/components/intellifire/fixtures/local_poll.json b/tests/components/intellifire/fixtures/local_poll.json new file mode 100644 index 00000000000..9dac47c698d --- /dev/null +++ b/tests/components/intellifire/fixtures/local_poll.json @@ -0,0 +1,29 @@ +{ + "name": "", + "serial": "4GC295860E5837G40D9974B7FD459234", + "temperature": 17, + "battery": 0, + "pilot": 1, + "light": 0, + "height": 1, + "fanspeed": 1, + "hot": 0, + "power": 1, + "thermostat": 0, + "setpoint": 0, + "timer": 0, + "timeremaining": 0, + "prepurge": 0, + "feature_light": 0, + "feature_thermostat": 1, + "power_vent": 0, + "feature_fan": 1, + "errors": [], + "fw_version": "0x00030200", + "fw_ver_str": "0.3.2+hw2", + "downtime": 0, + "uptime": 117, + "connection_quality": 988451, + "ecm_latency": 0, + "ipv4_address": "192.168.2.108" +} diff --git a/tests/components/intellifire/fixtures/user_data_1.json b/tests/components/intellifire/fixtures/user_data_1.json new file mode 100644 index 00000000000..501d240662b --- /dev/null +++ b/tests/components/intellifire/fixtures/user_data_1.json @@ -0,0 +1,17 @@ +{ + "auth_cookie": "B984F21A6378560019F8A1CDE41B6782", + "user_id": "52C3F9E8B9D3AC99F8E4D12345678901FE9A2BC7D85F7654E28BF98BCD123456", + "web_client_id": "FA2B1C3045601234D0AE17D72F8E975", + "fireplaces": [ + { + "auth_cookie": "B984F21A6378560019F8A1CDE41B6782", + "user_id": "52C3F9E8B9D3AC99F8E4D12345678901FE9A2BC7D85F7654E28BF98BCD123456", + "web_client_id": "FA2B1C3045601234D0AE17D72F8E975", + "ip_address": "192.168.2.108", + "api_key": "B5C4DA27AAEF31D1FB21AFF9BFA6BCD2", + "serial": "3FB284769E4736F30C8973A7ED358123" + } + ], + "username": "grumpypanda@china.cn", + "password": "you-stole-my-pandas" +} diff --git a/tests/components/intellifire/fixtures/user_data_3.json b/tests/components/intellifire/fixtures/user_data_3.json new file mode 100644 index 00000000000..39e9c95abbd --- /dev/null +++ b/tests/components/intellifire/fixtures/user_data_3.json @@ -0,0 +1,33 @@ +{ + "auth_cookie": "B984F21A6378560019F8A1CDE41B6782", + "user_id": "52C3F9E8B9D3AC99F8E4D12345678901FE9A2BC7D85F7654E28BF98BCD123456", + "web_client_id": "FA2B1C3045601234D0AE17D72F8E975", + "fireplaces": [ + { + "auth_cookie": "B984F21A6378560019F8A1CDE41B6782", + "user_id": "52C3F9E8B9D3AC99F8E4D12345678901FE9A2BC7D85F7654E28BF98BCD123456", + "web_client_id": "FA2B1C3045601234D0AE17D72F8E975", + "ip_address": "192.168.2.108", + "api_key": "B5C4DA27AAEF31D1FB21AFF9BFA6BCD2", + "serial": "3FB284769E4736F30C8973A7ED358123" + }, + { + "auth_cookie": "B984F21A6378560019F8A1CDE41B6782", + "user_id": "52C3F9E8B9D3AC99F8E4D12345678901FE9A2BC7D85F7654E28BF98BCD123456", + "web_client_id": "FA2B1C3045601234D0AE17D72F8E975", + "ip_address": "192.168.2.109", + "api_key": "D4C5EB28BBFF41E1FB21AFF9BFA6CD34", + "serial": "4GC295860E5837G40D9974B7FD459234" + }, + { + "auth_cookie": "B984F21A6378560019F8A1CDE41B6782", + "user_id": "52C3F9E8B9D3AC99F8E4D12345678901FE9A2BC7D85F7654E28BF98BCD123456", + "web_client_id": "FA2B1C3045601234D0AE17D72F8E975", + "ip_address": "192.168.2.110", + "api_key": "E5D6FC39CCED52F1FB21AFF9BFA6DE56", + "serial": "5HD306971F5938H51EAA85C8GE561345" + } + ], + "username": "grumpypanda@china.cn", + "password": "you-stole-my-pandas" +} diff --git a/tests/components/intellifire/snapshots/test_binary_sensor.ambr b/tests/components/intellifire/snapshots/test_binary_sensor.ambr new file mode 100644 index 00000000000..34d5836a025 --- /dev/null +++ b/tests/components/intellifire/snapshots/test_binary_sensor.ambr @@ -0,0 +1,717 @@ +# serializer version: 1 +# name: test_all_binary_sensor_entities[binary_sensor.intellifire_accessory_error-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.intellifire_accessory_error', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Accessory error', + 'platform': 'intellifire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'accessory_error', + 'unique_id': 'error_accessory_mock_serial', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_binary_sensor_entities[binary_sensor.intellifire_accessory_error-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by unpublished Intellifire API', + 'device_class': 'problem', + 'friendly_name': 'IntelliFire Accessory error', + }), + 'context': , + 'entity_id': 'binary_sensor.intellifire_accessory_error', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_binary_sensor_entities[binary_sensor.intellifire_disabled_error-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.intellifire_disabled_error', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Disabled error', + 'platform': 'intellifire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'disabled_error', + 'unique_id': 'error_disabled_mock_serial', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_binary_sensor_entities[binary_sensor.intellifire_disabled_error-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by unpublished Intellifire API', + 'device_class': 'problem', + 'friendly_name': 'IntelliFire Disabled error', + }), + 'context': , + 'entity_id': 'binary_sensor.intellifire_disabled_error', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_binary_sensor_entities[binary_sensor.intellifire_ecm_offline_error-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.intellifire_ecm_offline_error', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ECM offline error', + 'platform': 'intellifire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ecm_offline_error', + 'unique_id': 'error_ecm_offline_mock_serial', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_binary_sensor_entities[binary_sensor.intellifire_ecm_offline_error-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by unpublished Intellifire API', + 'device_class': 'problem', + 'friendly_name': 'IntelliFire ECM offline error', + }), + 'context': , + 'entity_id': 'binary_sensor.intellifire_ecm_offline_error', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_binary_sensor_entities[binary_sensor.intellifire_fan_delay_error-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.intellifire_fan_delay_error', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Fan delay error', + 'platform': 'intellifire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'fan_delay_error', + 'unique_id': 'error_fan_delay_mock_serial', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_binary_sensor_entities[binary_sensor.intellifire_fan_delay_error-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by unpublished Intellifire API', + 'device_class': 'problem', + 'friendly_name': 'IntelliFire Fan delay error', + }), + 'context': , + 'entity_id': 'binary_sensor.intellifire_fan_delay_error', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_binary_sensor_entities[binary_sensor.intellifire_fan_error-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.intellifire_fan_error', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Fan error', + 'platform': 'intellifire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'fan_error', + 'unique_id': 'error_fan_mock_serial', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_binary_sensor_entities[binary_sensor.intellifire_fan_error-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by unpublished Intellifire API', + 'device_class': 'problem', + 'friendly_name': 'IntelliFire Fan error', + }), + 'context': , + 'entity_id': 'binary_sensor.intellifire_fan_error', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_binary_sensor_entities[binary_sensor.intellifire_flame-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.intellifire_flame', + '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': 'Flame', + 'platform': 'intellifire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'flame', + 'unique_id': 'on_off_mock_serial', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_binary_sensor_entities[binary_sensor.intellifire_flame-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by unpublished Intellifire API', + 'friendly_name': 'IntelliFire Flame', + }), + 'context': , + 'entity_id': 'binary_sensor.intellifire_flame', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_all_binary_sensor_entities[binary_sensor.intellifire_flame_error-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.intellifire_flame_error', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Flame Error', + 'platform': 'intellifire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'flame_error', + 'unique_id': 'error_flame_mock_serial', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_binary_sensor_entities[binary_sensor.intellifire_flame_error-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by unpublished Intellifire API', + 'device_class': 'problem', + 'friendly_name': 'IntelliFire Flame Error', + }), + 'context': , + 'entity_id': 'binary_sensor.intellifire_flame_error', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_binary_sensor_entities[binary_sensor.intellifire_lights_error-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.intellifire_lights_error', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Lights error', + 'platform': 'intellifire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lights_error', + 'unique_id': 'error_lights_mock_serial', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_binary_sensor_entities[binary_sensor.intellifire_lights_error-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by unpublished Intellifire API', + 'device_class': 'problem', + 'friendly_name': 'IntelliFire Lights error', + }), + 'context': , + 'entity_id': 'binary_sensor.intellifire_lights_error', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_binary_sensor_entities[binary_sensor.intellifire_maintenance_error-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.intellifire_maintenance_error', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Maintenance error', + 'platform': 'intellifire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'maintenance_error', + 'unique_id': 'error_maintenance_mock_serial', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_binary_sensor_entities[binary_sensor.intellifire_maintenance_error-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by unpublished Intellifire API', + 'device_class': 'problem', + 'friendly_name': 'IntelliFire Maintenance error', + }), + 'context': , + 'entity_id': 'binary_sensor.intellifire_maintenance_error', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_binary_sensor_entities[binary_sensor.intellifire_offline_error-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.intellifire_offline_error', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Offline error', + 'platform': 'intellifire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'offline_error', + 'unique_id': 'error_offline_mock_serial', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_binary_sensor_entities[binary_sensor.intellifire_offline_error-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by unpublished Intellifire API', + 'device_class': 'problem', + 'friendly_name': 'IntelliFire Offline error', + }), + 'context': , + 'entity_id': 'binary_sensor.intellifire_offline_error', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_binary_sensor_entities[binary_sensor.intellifire_pilot_flame_error-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.intellifire_pilot_flame_error', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Pilot flame error', + 'platform': 'intellifire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pilot_flame_error', + 'unique_id': 'error_pilot_flame_mock_serial', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_binary_sensor_entities[binary_sensor.intellifire_pilot_flame_error-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by unpublished Intellifire API', + 'device_class': 'problem', + 'friendly_name': 'IntelliFire Pilot flame error', + }), + 'context': , + 'entity_id': 'binary_sensor.intellifire_pilot_flame_error', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_binary_sensor_entities[binary_sensor.intellifire_pilot_light_on-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.intellifire_pilot_light_on', + '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': 'Pilot light on', + 'platform': 'intellifire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pilot_light_on', + 'unique_id': 'pilot_light_on_mock_serial', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_binary_sensor_entities[binary_sensor.intellifire_pilot_light_on-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by unpublished Intellifire API', + 'friendly_name': 'IntelliFire Pilot light on', + }), + 'context': , + 'entity_id': 'binary_sensor.intellifire_pilot_light_on', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_all_binary_sensor_entities[binary_sensor.intellifire_soft_lock_out_error-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.intellifire_soft_lock_out_error', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Soft lock out error', + 'platform': 'intellifire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'soft_lock_out_error', + 'unique_id': 'error_soft_lock_out_mock_serial', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_binary_sensor_entities[binary_sensor.intellifire_soft_lock_out_error-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by unpublished Intellifire API', + 'device_class': 'problem', + 'friendly_name': 'IntelliFire Soft lock out error', + }), + 'context': , + 'entity_id': 'binary_sensor.intellifire_soft_lock_out_error', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_binary_sensor_entities[binary_sensor.intellifire_thermostat_on-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.intellifire_thermostat_on', + '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': 'Thermostat on', + 'platform': 'intellifire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'thermostat_on', + 'unique_id': 'thermostat_on_mock_serial', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_binary_sensor_entities[binary_sensor.intellifire_thermostat_on-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by unpublished Intellifire API', + 'friendly_name': 'IntelliFire Thermostat on', + }), + 'context': , + 'entity_id': 'binary_sensor.intellifire_thermostat_on', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_binary_sensor_entities[binary_sensor.intellifire_timer_on-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.intellifire_timer_on', + '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': 'Timer on', + 'platform': 'intellifire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'timer_on', + 'unique_id': 'timer_on_mock_serial', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_binary_sensor_entities[binary_sensor.intellifire_timer_on-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by unpublished Intellifire API', + 'friendly_name': 'IntelliFire Timer on', + }), + 'context': , + 'entity_id': 'binary_sensor.intellifire_timer_on', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/intellifire/snapshots/test_climate.ambr b/tests/components/intellifire/snapshots/test_climate.ambr new file mode 100644 index 00000000000..36f719d2264 --- /dev/null +++ b/tests/components/intellifire/snapshots/test_climate.ambr @@ -0,0 +1,66 @@ +# serializer version: 1 +# name: test_all_sensor_entities[climate.intellifire_thermostat-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + , + ]), + 'max_temp': 37, + 'min_temp': 0, + 'target_temp_step': 1.0, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.intellifire_thermostat', + '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': 'Thermostat', + 'platform': 'intellifire', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'climate_mock_serial', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_sensor_entities[climate.intellifire_thermostat-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by unpublished Intellifire API', + 'current_temperature': 17.0, + 'friendly_name': 'IntelliFire Thermostat', + 'hvac_modes': list([ + , + , + ]), + 'max_temp': 37, + 'min_temp': 0, + 'supported_features': , + 'target_temp_step': 1.0, + 'temperature': 0.0, + }), + 'context': , + 'entity_id': 'climate.intellifire_thermostat', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/intellifire/snapshots/test_sensor.ambr b/tests/components/intellifire/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..d5e59e3f00f --- /dev/null +++ b/tests/components/intellifire/snapshots/test_sensor.ambr @@ -0,0 +1,587 @@ +# serializer version: 1 +# name: test_all_sensor_entities[sensor.intellifire_connection_quality-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.intellifire_connection_quality', + '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': 'Connection quality', + 'platform': 'intellifire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'connection_quality', + 'unique_id': 'connection_quality_mock_serial', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_sensor_entities[sensor.intellifire_connection_quality-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by unpublished Intellifire API', + 'friendly_name': 'IntelliFire Connection quality', + }), + 'context': , + 'entity_id': 'sensor.intellifire_connection_quality', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '988451', + }) +# --- +# name: test_all_sensor_entities[sensor.intellifire_downtime-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.intellifire_downtime', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Downtime', + 'platform': 'intellifire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'downtime', + 'unique_id': 'downtime_mock_serial', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_sensor_entities[sensor.intellifire_downtime-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by unpublished Intellifire API', + 'device_class': 'timestamp', + 'friendly_name': 'IntelliFire Downtime', + }), + 'context': , + 'entity_id': 'sensor.intellifire_downtime', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_sensor_entities[sensor.intellifire_ecm_latency-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.intellifire_ecm_latency', + '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': 'ECM latency', + 'platform': 'intellifire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ecm_latency', + 'unique_id': 'ecm_latency_mock_serial', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_sensor_entities[sensor.intellifire_ecm_latency-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by unpublished Intellifire API', + 'friendly_name': 'IntelliFire ECM latency', + }), + 'context': , + 'entity_id': 'sensor.intellifire_ecm_latency', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_all_sensor_entities[sensor.intellifire_fan_speed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.intellifire_fan_speed', + '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': 'Fan Speed', + 'platform': 'intellifire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'fan_speed', + 'unique_id': 'fan_speed_mock_serial', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_sensor_entities[sensor.intellifire_fan_speed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by unpublished Intellifire API', + 'friendly_name': 'IntelliFire Fan Speed', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.intellifire_fan_speed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1', + }) +# --- +# name: test_all_sensor_entities[sensor.intellifire_flame_height-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.intellifire_flame_height', + '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': 'Flame height', + 'platform': 'intellifire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'flame_height', + 'unique_id': 'flame_height_mock_serial', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_sensor_entities[sensor.intellifire_flame_height-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by unpublished Intellifire API', + 'friendly_name': 'IntelliFire Flame height', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.intellifire_flame_height', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2', + }) +# --- +# name: test_all_sensor_entities[sensor.intellifire_ip_address-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.intellifire_ip_address', + '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': 'IP address', + 'platform': 'intellifire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ipv4_address', + 'unique_id': 'ipv4_address_mock_serial', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_sensor_entities[sensor.intellifire_ip_address-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by unpublished Intellifire API', + 'friendly_name': 'IntelliFire IP address', + }), + 'context': , + 'entity_id': 'sensor.intellifire_ip_address', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '192.168.2.108', + }) +# --- +# name: test_all_sensor_entities[sensor.intellifire_local_connectivity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.intellifire_local_connectivity', + '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': 'Local connectivity', + 'platform': 'intellifire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'local_connectivity', + 'unique_id': 'local_connectivity_mock_serial', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_sensor_entities[sensor.intellifire_local_connectivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by unpublished Intellifire API', + 'friendly_name': 'IntelliFire Local connectivity', + }), + 'context': , + 'entity_id': 'sensor.intellifire_local_connectivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'True', + }) +# --- +# name: test_all_sensor_entities[sensor.intellifire_none-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.intellifire_none', + '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': None, + 'platform': 'intellifire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'local_connectivity', + 'unique_id': 'local_connectivity_mock_serial', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_sensor_entities[sensor.intellifire_none-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by unpublished Intellifire API', + 'friendly_name': 'IntelliFire None', + }), + 'context': , + 'entity_id': 'sensor.intellifire_none', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'True', + }) +# --- +# name: test_all_sensor_entities[sensor.intellifire_target_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.intellifire_target_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Target temperature', + 'platform': 'intellifire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'target_temp', + 'unique_id': 'target_temp_mock_serial', + 'unit_of_measurement': , + }) +# --- +# name: test_all_sensor_entities[sensor.intellifire_target_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by unpublished Intellifire API', + 'device_class': 'temperature', + 'friendly_name': 'IntelliFire Target temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.intellifire_target_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_all_sensor_entities[sensor.intellifire_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.intellifire_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'intellifire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'temperature_mock_serial', + 'unit_of_measurement': , + }) +# --- +# name: test_all_sensor_entities[sensor.intellifire_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by unpublished Intellifire API', + 'device_class': 'temperature', + 'friendly_name': 'IntelliFire Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.intellifire_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '17', + }) +# --- +# name: test_all_sensor_entities[sensor.intellifire_timer_end-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.intellifire_timer_end', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Timer end', + 'platform': 'intellifire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'timer_end_timestamp', + 'unique_id': 'timer_end_timestamp_mock_serial', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_sensor_entities[sensor.intellifire_timer_end-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by unpublished Intellifire API', + 'device_class': 'timestamp', + 'friendly_name': 'IntelliFire Timer end', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.intellifire_timer_end', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_sensor_entities[sensor.intellifire_uptime-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.intellifire_uptime', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Uptime', + 'platform': 'intellifire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'uptime', + 'unique_id': 'uptime_mock_serial', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_sensor_entities[sensor.intellifire_uptime-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by unpublished Intellifire API', + 'device_class': 'timestamp', + 'friendly_name': 'IntelliFire Uptime', + }), + 'context': , + 'entity_id': 'sensor.intellifire_uptime', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2021-01-01T11:58:03+00:00', + }) +# --- diff --git a/tests/components/intellifire/test_binary_sensor.py b/tests/components/intellifire/test_binary_sensor.py new file mode 100644 index 00000000000..a40f92b84d5 --- /dev/null +++ b/tests/components/intellifire/test_binary_sensor.py @@ -0,0 +1,35 @@ +"""Test IntelliFire Binary Sensors.""" + +from unittest.mock import AsyncMock, patch + +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_all_binary_sensor_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_config_entry_current: MockConfigEntry, + entity_registry: er.EntityRegistry, + mock_apis_single_fp: tuple[AsyncMock, AsyncMock, AsyncMock], +) -> None: + """Test all entities.""" + + with ( + patch( + "homeassistant.components.intellifire.PLATFORMS", [Platform.BINARY_SENSOR] + ), + ): + await setup_integration(hass, mock_config_entry_current) + await snapshot_platform( + hass, entity_registry, snapshot, mock_config_entry_current.entry_id + ) diff --git a/tests/components/intellifire/test_climate.py b/tests/components/intellifire/test_climate.py new file mode 100644 index 00000000000..da1b2864791 --- /dev/null +++ b/tests/components/intellifire/test_climate.py @@ -0,0 +1,34 @@ +"""Test climate.""" + +from unittest.mock import patch + +from freezegun import freeze_time +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +@freeze_time("2021-01-01T12:00:00Z") +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_all_sensor_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_config_entry_current: MockConfigEntry, + entity_registry: er.EntityRegistry, + mock_fp, +) -> None: + """Test all entities.""" + with ( + patch("homeassistant.components.intellifire.PLATFORMS", [Platform.CLIMATE]), + ): + await setup_integration(hass, mock_config_entry_current) + await snapshot_platform( + hass, entity_registry, snapshot, mock_config_entry_current.entry_id + ) diff --git a/tests/components/intellifire/test_config_flow.py b/tests/components/intellifire/test_config_flow.py index ba4e2f039a3..f1465c4dcd4 100644 --- a/tests/components/intellifire/test_config_flow.py +++ b/tests/components/intellifire/test_config_flow.py @@ -1,323 +1,168 @@ """Test the IntelliFire config flow.""" -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import AsyncMock -from intellifire4py.exceptions import LoginException +from intellifire4py.exceptions import LoginError from homeassistant import config_entries from homeassistant.components import dhcp -from homeassistant.components.intellifire.config_flow import MANUAL_ENTRY_STRING -from homeassistant.components.intellifire.const import CONF_USER_ID, DOMAIN -from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.components.intellifire.const import CONF_SERIAL, DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from .conftest import mock_api_connection_error - from tests.common import MockConfigEntry -@patch.multiple( - "homeassistant.components.intellifire.config_flow.IntellifireAPICloud", - login=AsyncMock(), - get_user_id=MagicMock(return_value="intellifire"), - get_fireplace_api_key=MagicMock(return_value="key"), -) -async def test_no_discovery( +async def test_standard_config_with_single_fireplace( hass: HomeAssistant, mock_setup_entry: AsyncMock, - mock_intellifire_config_flow: MagicMock, + mock_apis_single_fp, ) -> None: - """Test we should get the manual discovery form - because no discovered fireplaces.""" - with patch( - "homeassistant.components.intellifire.config_flow.AsyncUDPFireplaceFinder.search_fireplace", - return_value=[], - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] is FlowResultType.FORM + """Test standard flow with a user who has only a single fireplace.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} - assert result["step_id"] == "manual_device_entry" + assert result["step_id"] == "cloud_api" - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], - { - CONF_HOST: "1.1.1.1", - }, + {CONF_USERNAME: "donJulio", CONF_PASSWORD: "Tequila0FD00m"}, ) - await hass.async_block_till_done() - - assert result2["type"] is FlowResultType.FORM - assert result2["step_id"] == "api_config" - - result3 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_USERNAME: "test", CONF_PASSWORD: "AROONIE"}, - ) - await hass.async_block_till_done() - - assert result3["type"] is FlowResultType.CREATE_ENTRY - assert result3["title"] == "Fireplace 12345" - assert result3["data"] == { - CONF_HOST: "1.1.1.1", - CONF_USERNAME: "test", - CONF_PASSWORD: "AROONIE", - CONF_API_KEY: "key", - CONF_USER_ID: "intellifire", + # For a single fireplace we just create it + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == { + "ip_address": "192.168.2.108", + "api_key": "B5C4DA27AAEF31D1FB21AFF9BFA6BCD2", + "serial": "3FB284769E4736F30C8973A7ED358123", + "auth_cookie": "B984F21A6378560019F8A1CDE41B6782", + "web_client_id": "FA2B1C3045601234D0AE17D72F8E975", + "user_id": "52C3F9E8B9D3AC99F8E4D12345678901FE9A2BC7D85F7654E28BF98BCD123456", + "username": "grumpypanda@china.cn", + "password": "you-stole-my-pandas", } - assert len(mock_setup_entry.mock_calls) == 1 -@patch.multiple( - "homeassistant.components.intellifire.config_flow.IntellifireAPICloud", - login=AsyncMock(side_effect=mock_api_connection_error()), - get_user_id=MagicMock(return_value="intellifire"), - get_fireplace_api_key=MagicMock(return_value="key"), -) -async def test_single_discovery( +async def test_standard_config_with_pre_configured_fireplace( hass: HomeAssistant, mock_setup_entry: AsyncMock, - mock_intellifire_config_flow: MagicMock, + mock_config_entry_current, + mock_apis_single_fp, ) -> None: - """Test single fireplace UDP discovery.""" - with patch( - "homeassistant.components.intellifire.config_flow.AsyncUDPFireplaceFinder.search_fireplace", - return_value=["192.168.1.69"], - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) + """What if we try to configure an already configured fireplace.""" + # Configure an existing entry + mock_config_entry_current.add_to_hass(hass) - await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_HOST: "192.168.1.69"} + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} ) - await hass.async_block_till_done() - result3 = await hass.config_entries.flow.async_configure( + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {} + assert result["step_id"] == "cloud_api" + + result = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_USERNAME: "test", CONF_PASSWORD: "AROONIE"}, + {CONF_USERNAME: "donJulio", CONF_PASSWORD: "Tequila0FD00m"}, ) - await hass.async_block_till_done() - assert result3["type"] is FlowResultType.FORM - assert result3["errors"] == {"base": "iftapi_connect"} + + # For a single fireplace we just create it + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_available_devices" -@patch.multiple( - "homeassistant.components.intellifire.config_flow.IntellifireAPICloud", - login=AsyncMock(side_effect=LoginException), - get_user_id=MagicMock(return_value="intellifire"), - get_fireplace_api_key=MagicMock(return_value="key"), -) -async def test_single_discovery_loign_error( +async def test_standard_config_with_single_fireplace_and_bad_credentials( hass: HomeAssistant, mock_setup_entry: AsyncMock, - mock_intellifire_config_flow: MagicMock, + mock_apis_single_fp, ) -> None: - """Test single fireplace UDP discovery.""" - with patch( - "homeassistant.components.intellifire.config_flow.AsyncUDPFireplaceFinder.search_fireplace", - return_value=["192.168.1.69"], - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_HOST: "192.168.1.69"} - ) - await hass.async_block_till_done() - result3 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_USERNAME: "test", CONF_PASSWORD: "AROONIE"}, - ) - await hass.async_block_till_done() - assert result3["type"] is FlowResultType.FORM - assert result3["errors"] == {"base": "api_error"} - - -async def test_manual_entry( - hass: HomeAssistant, - mock_setup_entry: AsyncMock, - mock_intellifire_config_flow: MagicMock, -) -> None: - """Test for multiple Fireplace discovery - involving a pick_device step.""" - with patch( - "homeassistant.components.intellifire.config_flow.AsyncUDPFireplaceFinder.search_fireplace", - return_value=["192.168.1.69", "192.168.1.33", "192.168.169"], - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - assert result["step_id"] == "pick_device" - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_HOST: MANUAL_ENTRY_STRING} - ) - - await hass.async_block_till_done() - assert result2["step_id"] == "manual_device_entry" - - -async def test_multi_discovery( - hass: HomeAssistant, - mock_setup_entry: AsyncMock, - mock_intellifire_config_flow: MagicMock, -) -> None: - """Test for multiple fireplace discovery - involving a pick_device step.""" - with patch( - "homeassistant.components.intellifire.config_flow.AsyncUDPFireplaceFinder.search_fireplace", - return_value=["192.168.1.69", "192.168.1.33", "192.168.169"], - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - assert result["step_id"] == "pick_device" - await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_HOST: "192.168.1.33"} - ) - await hass.async_block_till_done() - assert result["step_id"] == "pick_device" - - -async def test_multi_discovery_cannot_connect( - hass: HomeAssistant, - mock_setup_entry: AsyncMock, - mock_intellifire_config_flow: MagicMock, -) -> None: - """Test for multiple fireplace discovery - involving a pick_device step.""" - with patch( - "homeassistant.components.intellifire.config_flow.AsyncUDPFireplaceFinder.search_fireplace", - return_value=["192.168.1.69", "192.168.1.33", "192.168.169"], - ): - mock_intellifire_config_flow.poll.side_effect = ConnectionError - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "pick_device" - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_HOST: "192.168.1.33"} - ) - await hass.async_block_till_done() - assert result2["type"] is FlowResultType.FORM - assert result2["errors"] == {"base": "cannot_connect"} - - -async def test_form_cannot_connect_manual_entry( - hass: HomeAssistant, - mock_intellifire_config_flow: MagicMock, - mock_fireplace_finder_single: AsyncMock, -) -> None: - """Test we handle cannot connect error.""" - mock_intellifire_config_flow.poll.side_effect = ConnectionError + """Test bad credentials on a login.""" + mock_local_interface, mock_cloud_interface, mock_fp = mock_apis_single_fp + # Set login error + mock_cloud_interface.login_with_credentials.side_effect = LoginError result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "manual_device_entry" + assert result["errors"] == {} + assert result["step_id"] == "cloud_api" - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], - { - CONF_HOST: "1.1.1.1", - }, + {CONF_USERNAME: "donJulio", CONF_PASSWORD: "Tequila0FD00m"}, ) - assert result2["type"] is FlowResultType.FORM - assert result2["errors"] == {"base": "cannot_connect"} + # Erase the error + mock_cloud_interface.login_with_credentials.side_effect = None + + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "api_error"} + assert result["step_id"] == "cloud_api" + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_USERNAME: "donJulio", CONF_PASSWORD: "Tequila0FD00m"}, + ) + # For a single fireplace we just create it + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == { + "ip_address": "192.168.2.108", + "api_key": "B5C4DA27AAEF31D1FB21AFF9BFA6BCD2", + "serial": "3FB284769E4736F30C8973A7ED358123", + "auth_cookie": "B984F21A6378560019F8A1CDE41B6782", + "web_client_id": "FA2B1C3045601234D0AE17D72F8E975", + "user_id": "52C3F9E8B9D3AC99F8E4D12345678901FE9A2BC7D85F7654E28BF98BCD123456", + "username": "grumpypanda@china.cn", + "password": "you-stole-my-pandas", + } -async def test_picker_already_discovered( +async def test_standard_config_with_multiple_fireplace( hass: HomeAssistant, mock_setup_entry: AsyncMock, - mock_intellifire_config_flow: MagicMock, + mock_apis_multifp, ) -> None: - """Test single fireplace UDP discovery.""" - - entry = MockConfigEntry( - domain=DOMAIN, - data={ - "host": "192.168.1.3", - }, - title="Fireplace", - unique_id=44444, - ) - entry.add_to_hass(hass) - with patch( - "homeassistant.components.intellifire.config_flow.AsyncUDPFireplaceFinder.search_fireplace", - return_value=["192.168.1.3"], - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - await hass.async_block_till_done() - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_HOST: "192.168.1.4", - }, - ) - assert result2["type"] is FlowResultType.FORM - assert len(mock_setup_entry.mock_calls) == 0 - - -@patch.multiple( - "homeassistant.components.intellifire.config_flow.IntellifireAPICloud", - login=AsyncMock(), - get_user_id=MagicMock(return_value="intellifire"), - get_fireplace_api_key=MagicMock(return_value="key"), -) -async def test_reauth_flow( - hass: HomeAssistant, - mock_setup_entry: AsyncMock, - mock_intellifire_config_flow: MagicMock, -) -> None: - """Test the reauth flow.""" - - entry = MockConfigEntry( - domain=DOMAIN, - data={ - "host": "192.168.1.3", - }, - title="Fireplace 1234", - version=1, - unique_id="4444", - ) - entry.add_to_hass(hass) - + """Test multi-fireplace user who must be very rich.""" result = await hass.config_entries.flow.async_init( - DOMAIN, - context={ - "source": "reauth", - "unique_id": entry.unique_id, - "entry_id": entry.entry_id, - }, + DOMAIN, context={"source": config_entries.SOURCE_USER} ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {} + assert result["step_id"] == "cloud_api" - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "api_config" - - result3 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_USERNAME: "test", CONF_PASSWORD: "AROONIE"}, + {CONF_USERNAME: "donJulio", CONF_PASSWORD: "Tequila0FD00m"}, ) - await hass.async_block_till_done() - assert result3["type"] is FlowResultType.ABORT - assert entry.data[CONF_PASSWORD] == "AROONIE" - assert entry.data[CONF_USERNAME] == "test" + # When we have multiple fireplaces we get to pick a serial + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "pick_cloud_device" + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_SERIAL: "4GC295860E5837G40D9974B7FD459234"}, + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == { + "ip_address": "192.168.2.109", + "api_key": "D4C5EB28BBFF41E1FB21AFF9BFA6CD34", + "serial": "4GC295860E5837G40D9974B7FD459234", + "auth_cookie": "B984F21A6378560019F8A1CDE41B6782", + "web_client_id": "FA2B1C3045601234D0AE17D72F8E975", + "user_id": "52C3F9E8B9D3AC99F8E4D12345678901FE9A2BC7D85F7654E28BF98BCD123456", + "username": "grumpypanda@china.cn", + "password": "you-stole-my-pandas", + } async def test_dhcp_discovery_intellifire_device( hass: HomeAssistant, mock_setup_entry: AsyncMock, - mock_intellifire_config_flow: MagicMock, + mock_apis_multifp, ) -> None: """Test successful DHCP Discovery.""" + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, @@ -327,26 +172,26 @@ async def test_dhcp_discovery_intellifire_device( hostname="zentrios-Test", ), ) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "dhcp_confirm" - result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result2["type"] is FlowResultType.FORM - assert result2["step_id"] == "dhcp_confirm" - result3 = await hass.config_entries.flow.async_configure( - result2["flow_id"], user_input={} + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "cloud_api" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_USERNAME: "donJulio", CONF_PASSWORD: "Tequila0FD00m"}, ) - assert result3["title"] == "Fireplace 12345" - assert result3["data"] == {"host": "1.1.1.1"} + assert result["type"] == FlowResultType.CREATE_ENTRY async def test_dhcp_discovery_non_intellifire_device( hass: HomeAssistant, - mock_intellifire_config_flow: MagicMock, mock_setup_entry: AsyncMock, + mock_apis_multifp, ) -> None: - """Test failed DHCP Discovery.""" + """Test successful DHCP Discovery of a non intellifire device..""" - mock_intellifire_config_flow.poll.side_effect = ConnectionError + # Patch poll with an exception + mock_local_interface, mock_cloud_interface, mock_fp = mock_apis_multifp + mock_local_interface.poll.side_effect = ConnectionError result = await hass.config_entries.flow.async_init( DOMAIN, @@ -357,6 +202,28 @@ async def test_dhcp_discovery_non_intellifire_device( hostname="zentrios-Evil", ), ) - - assert result["type"] is FlowResultType.ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "not_intellifire_device" + # Test is finished - the DHCP scanner detected a hostname that "might" be an IntelliFire device, but it was not. + + +async def test_reauth_flow( + hass: HomeAssistant, + mock_config_entry_current: MockConfigEntry, + mock_apis_single_fp, + mock_setup_entry: AsyncMock, +) -> None: + """Test reauth.""" + + mock_config_entry_current.add_to_hass(hass) + result = await mock_config_entry_current.start_reauth_flow(hass) + assert result["type"] == FlowResultType.FORM + result["step_id"] = "cloud_api" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_USERNAME: "donJulio", CONF_PASSWORD: "Tequila0FD00m"}, + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "reauth_successful" diff --git a/tests/components/intellifire/test_init.py b/tests/components/intellifire/test_init.py new file mode 100644 index 00000000000..6d08fda26c3 --- /dev/null +++ b/tests/components/intellifire/test_init.py @@ -0,0 +1,111 @@ +"""Test the IntelliFire config flow.""" + +from unittest.mock import AsyncMock, patch + +from homeassistant.components.intellifire import CONF_USER_ID +from homeassistant.components.intellifire.const import ( + API_MODE_CLOUD, + API_MODE_LOCAL, + CONF_AUTH_COOKIE, + CONF_CONTROL_MODE, + CONF_READ_MODE, + CONF_SERIAL, + CONF_WEB_CLIENT_ID, + DOMAIN, +) +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ( + CONF_API_KEY, + CONF_HOST, + CONF_IP_ADDRESS, + CONF_PASSWORD, + CONF_USERNAME, +) +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def test_minor_migration( + hass: HomeAssistant, mock_config_entry_old, mock_apis_single_fp +) -> None: + """With the new library we are going to end up rewriting the config entries.""" + mock_config_entry_old.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry_old.entry_id) + + assert mock_config_entry_old.data == { + "ip_address": "192.168.2.108", + "host": "192.168.2.108", + "api_key": "B5C4DA27AAEF31D1FB21AFF9BFA6BCD2", + "serial": "3FB284769E4736F30C8973A7ED358123", + "auth_cookie": "B984F21A6378560019F8A1CDE41B6782", + "web_client_id": "FA2B1C3045601234D0AE17D72F8E975", + "user_id": "52C3F9E8B9D3AC99F8E4D12345678901FE9A2BC7D85F7654E28BF98BCD123456", + "username": "grumpypanda@china.cn", + "password": "you-stole-my-pandas", + } + + +async def test_minor_migration_error(hass: HomeAssistant, mock_apis_single_fp) -> None: + """Test the case where we completely fail to initialize.""" + mock_config_entry = MockConfigEntry( + domain=DOMAIN, + version=1, + minor_version=1, + title="Fireplace of testing", + data={ + CONF_HOST: "11.168.2.218", + CONF_USERNAME: "grumpypanda@china.cn", + CONF_PASSWORD: "you-stole-my-pandas", + CONF_USER_ID: "52C3F9E8B9D3AC99F8E4D12345678901FE9A2BC7D85F7654E28BF98BCD123456", + }, + ) + + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + assert mock_config_entry.state is ConfigEntryState.MIGRATION_ERROR + + +async def test_init_with_no_username(hass: HomeAssistant, mock_apis_single_fp) -> None: + """Test the case where we completely fail to initialize.""" + mock_config_entry = MockConfigEntry( + domain=DOMAIN, + version=1, + minor_version=2, + data={ + CONF_IP_ADDRESS: "192.168.2.108", + CONF_PASSWORD: "you-stole-my-pandas", + CONF_SERIAL: "3FB284769E4736F30C8973A7ED358123", + CONF_WEB_CLIENT_ID: "FA2B1C3045601234D0AE17D72F8E975", + CONF_API_KEY: "B5C4DA27AAEF31D1FB21AFF9BFA6BCD2", + CONF_AUTH_COOKIE: "B984F21A6378560019F8A1CDE41B6782", + CONF_USER_ID: "52C3F9E8B9D3AC99F8E4D12345678901FE9A2BC7D85F7654E28BF98BCD123456", + }, + options={CONF_READ_MODE: API_MODE_LOCAL, CONF_CONTROL_MODE: API_MODE_CLOUD}, + unique_id="3FB284769E4736F30C8973A7ED358123", + ) + + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR + + +async def test_connectivity_bad( + hass: HomeAssistant, + mock_config_entry_current, + mock_apis_single_fp, +) -> None: + """Test a timeout error on the setup flow.""" + + with patch( + "homeassistant.components.intellifire.UnifiedFireplace.build_fireplace_from_common", + new_callable=AsyncMock, + side_effect=TimeoutError, + ): + mock_config_entry_current.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry_current.entry_id) + + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 0 diff --git a/tests/components/intellifire/test_sensor.py b/tests/components/intellifire/test_sensor.py new file mode 100644 index 00000000000..96e344d77fc --- /dev/null +++ b/tests/components/intellifire/test_sensor.py @@ -0,0 +1,35 @@ +"""Test IntelliFire Binary Sensors.""" + +from unittest.mock import AsyncMock, patch + +from freezegun import freeze_time +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +@freeze_time("2021-01-01T12:00:00Z") +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_all_sensor_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_config_entry_current: MockConfigEntry, + entity_registry: er.EntityRegistry, + mock_apis_single_fp: tuple[AsyncMock, AsyncMock, AsyncMock], +) -> None: + """Test all entities.""" + + with ( + patch("homeassistant.components.intellifire.PLATFORMS", [Platform.SENSOR]), + ): + await setup_integration(hass, mock_config_entry_current) + await snapshot_platform( + hass, entity_registry, snapshot, mock_config_entry_current.entry_id + ) From c4e484539d2ed2276d0e22eeab16ea5fb8eee1b5 Mon Sep 17 00:00:00 2001 From: Etienne Soufflet Date: Sun, 1 Sep 2024 18:33:45 +0200 Subject: [PATCH 140/271] Fix Tado fan speed for AC (#122415) * change capabilities * fix tests 2 * improve usability with capabilities * fix swings management * Update homeassistant/components/tado/climate.py Co-authored-by: Erwin Douna * fix after Erwin's review * fix after joostlek's review * use constant * use in instead of get --------- Co-authored-by: Erwin Douna --- homeassistant/components/tado/climate.py | 165 +++++++++++++++-------- homeassistant/components/tado/const.py | 7 + 2 files changed, 115 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 314a2315d0a..60096c25301 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -16,6 +16,7 @@ from homeassistant.components.climate import ( SWING_BOTH, SWING_HORIZONTAL, SWING_OFF, + SWING_ON, SWING_VERTICAL, ClimateEntity, ClimateEntityFeature, @@ -47,7 +48,6 @@ from .const import ( HA_TO_TADO_FAN_MODE_MAP, HA_TO_TADO_FAN_MODE_MAP_LEGACY, HA_TO_TADO_HVAC_MODE_MAP, - HA_TO_TADO_SWING_MODE_MAP, ORDERED_KNOWN_TADO_MODES, PRESET_AUTO, SIGNAL_TADO_UPDATE_RECEIVED, @@ -55,17 +55,20 @@ from .const import ( SUPPORT_PRESET_MANUAL, TADO_DEFAULT_MAX_TEMP, TADO_DEFAULT_MIN_TEMP, - TADO_FAN_LEVELS, - TADO_FAN_SPEEDS, + TADO_FANLEVEL_SETTING, + TADO_FANSPEED_SETTING, + TADO_HORIZONTAL_SWING_SETTING, TADO_HVAC_ACTION_TO_HA_HVAC_ACTION, TADO_MODES_WITH_NO_TEMP_SETTING, TADO_SWING_OFF, TADO_SWING_ON, + TADO_SWING_SETTING, TADO_TO_HA_FAN_MODE_MAP, TADO_TO_HA_FAN_MODE_MAP_LEGACY, TADO_TO_HA_HVAC_MODE_MAP, TADO_TO_HA_OFFSET_MAP, TADO_TO_HA_SWING_MODE_MAP, + TADO_VERTICAL_SWING_SETTING, TEMP_OFFSET, TYPE_AIR_CONDITIONING, TYPE_HEATING, @@ -166,29 +169,30 @@ def create_climate_entity( supported_hvac_modes.append(TADO_TO_HA_HVAC_MODE_MAP[mode]) if ( - capabilities[mode].get("swings") - or capabilities[mode].get("verticalSwing") - or capabilities[mode].get("horizontalSwing") + TADO_SWING_SETTING in capabilities[mode] + or TADO_VERTICAL_SWING_SETTING in capabilities[mode] + or TADO_VERTICAL_SWING_SETTING in capabilities[mode] ): support_flags |= ClimateEntityFeature.SWING_MODE supported_swing_modes = [] - if capabilities[mode].get("swings"): + if TADO_SWING_SETTING in capabilities[mode]: supported_swing_modes.append( TADO_TO_HA_SWING_MODE_MAP[TADO_SWING_ON] ) - if capabilities[mode].get("verticalSwing"): + if TADO_VERTICAL_SWING_SETTING in capabilities[mode]: supported_swing_modes.append(SWING_VERTICAL) - if capabilities[mode].get("horizontalSwing"): + if TADO_HORIZONTAL_SWING_SETTING in capabilities[mode]: supported_swing_modes.append(SWING_HORIZONTAL) if ( SWING_HORIZONTAL in supported_swing_modes - and SWING_HORIZONTAL in supported_swing_modes + and SWING_VERTICAL in supported_swing_modes ): supported_swing_modes.append(SWING_BOTH) supported_swing_modes.append(TADO_TO_HA_SWING_MODE_MAP[TADO_SWING_OFF]) - if not capabilities[mode].get("fanSpeeds") and not capabilities[mode].get( - "fanLevel" + if ( + TADO_FANSPEED_SETTING not in capabilities[mode] + and TADO_FANLEVEL_SETTING not in capabilities[mode] ): continue @@ -197,14 +201,15 @@ def create_climate_entity( if supported_fan_modes: continue - if capabilities[mode].get("fanSpeeds"): + if TADO_FANSPEED_SETTING in capabilities[mode]: supported_fan_modes = generate_supported_fanmodes( - TADO_TO_HA_FAN_MODE_MAP_LEGACY, capabilities[mode]["fanSpeeds"] + TADO_TO_HA_FAN_MODE_MAP_LEGACY, + capabilities[mode][TADO_FANSPEED_SETTING], ) else: supported_fan_modes = generate_supported_fanmodes( - TADO_TO_HA_FAN_MODE_MAP, capabilities[mode]["fanLevel"] + TADO_TO_HA_FAN_MODE_MAP, capabilities[mode][TADO_FANLEVEL_SETTING] ) cool_temperatures = capabilities[CONST_MODE_COOL]["temperatures"] @@ -316,12 +321,16 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): self._target_temp: float | None = None self._current_tado_fan_speed = CONST_FAN_OFF + self._current_tado_fan_level = CONST_FAN_OFF self._current_tado_hvac_mode = CONST_MODE_OFF self._current_tado_hvac_action = HVACAction.OFF self._current_tado_swing_mode = TADO_SWING_OFF self._current_tado_vertical_swing = TADO_SWING_OFF self._current_tado_horizontal_swing = TADO_SWING_OFF + capabilities = tado.get_capabilities(zone_id) + self._current_tado_capabilities = capabilities + self._tado_zone_data: PyTado.TadoZone = {} self._tado_geofence_data: dict[str, str] | None = None @@ -382,20 +391,23 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): def fan_mode(self) -> str | None: """Return the fan setting.""" if self._ac_device: - return TADO_TO_HA_FAN_MODE_MAP.get( - self._current_tado_fan_speed, - TADO_TO_HA_FAN_MODE_MAP_LEGACY.get( + if self._is_valid_setting_for_hvac_mode(TADO_FANSPEED_SETTING): + return TADO_TO_HA_FAN_MODE_MAP_LEGACY.get( self._current_tado_fan_speed, FAN_AUTO - ), - ) + ) + if self._is_valid_setting_for_hvac_mode(TADO_FANLEVEL_SETTING): + return TADO_TO_HA_FAN_MODE_MAP.get( + self._current_tado_fan_level, FAN_AUTO + ) + return FAN_AUTO return None def set_fan_mode(self, fan_mode: str) -> None: """Turn fan on/off.""" - if self._current_tado_fan_speed in TADO_FAN_LEVELS: - self._control_hvac(fan_mode=HA_TO_TADO_FAN_MODE_MAP[fan_mode]) - else: + if self._is_valid_setting_for_hvac_mode(TADO_FANSPEED_SETTING): self._control_hvac(fan_mode=HA_TO_TADO_FAN_MODE_MAP_LEGACY[fan_mode]) + elif self._is_valid_setting_for_hvac_mode(TADO_FANLEVEL_SETTING): + self._control_hvac(fan_mode=HA_TO_TADO_FAN_MODE_MAP[fan_mode]) @property def preset_mode(self) -> str: @@ -555,24 +567,30 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): swing = None if self._attr_swing_modes is None: return - if ( - SWING_VERTICAL in self._attr_swing_modes - or SWING_HORIZONTAL in self._attr_swing_modes - ): - if swing_mode == SWING_VERTICAL: + if swing_mode == SWING_OFF: + if self._is_valid_setting_for_hvac_mode(TADO_SWING_SETTING): + swing = TADO_SWING_OFF + if self._is_valid_setting_for_hvac_mode(TADO_HORIZONTAL_SWING_SETTING): + horizontal_swing = TADO_SWING_OFF + if self._is_valid_setting_for_hvac_mode(TADO_VERTICAL_SWING_SETTING): + vertical_swing = TADO_SWING_OFF + if swing_mode == SWING_ON: + swing = TADO_SWING_ON + if swing_mode == SWING_VERTICAL: + if self._is_valid_setting_for_hvac_mode(TADO_VERTICAL_SWING_SETTING): vertical_swing = TADO_SWING_ON - elif swing_mode == SWING_HORIZONTAL: + if self._is_valid_setting_for_hvac_mode(TADO_HORIZONTAL_SWING_SETTING): + horizontal_swing = TADO_SWING_OFF + if swing_mode == SWING_HORIZONTAL: + if self._is_valid_setting_for_hvac_mode(TADO_VERTICAL_SWING_SETTING): + vertical_swing = TADO_SWING_OFF + if self._is_valid_setting_for_hvac_mode(TADO_HORIZONTAL_SWING_SETTING): horizontal_swing = TADO_SWING_ON - elif swing_mode == SWING_BOTH: + if swing_mode == SWING_BOTH: + if self._is_valid_setting_for_hvac_mode(TADO_VERTICAL_SWING_SETTING): vertical_swing = TADO_SWING_ON + if self._is_valid_setting_for_hvac_mode(TADO_HORIZONTAL_SWING_SETTING): horizontal_swing = TADO_SWING_ON - elif swing_mode == SWING_OFF: - if SWING_VERTICAL in self._attr_swing_modes: - vertical_swing = TADO_SWING_OFF - if SWING_HORIZONTAL in self._attr_swing_modes: - horizontal_swing = TADO_SWING_OFF - else: - swing = HA_TO_TADO_SWING_MODE_MAP[swing_mode] self._control_hvac( swing_mode=swing, @@ -596,21 +614,23 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): self._device_id ][TEMP_OFFSET][offset_key] - self._current_tado_fan_speed = ( - self._tado_zone_data.current_fan_level - if self._tado_zone_data.current_fan_level is not None - else self._tado_zone_data.current_fan_speed - ) - self._current_tado_hvac_mode = self._tado_zone_data.current_hvac_mode self._current_tado_hvac_action = self._tado_zone_data.current_hvac_action - self._current_tado_swing_mode = self._tado_zone_data.current_swing_mode - self._current_tado_vertical_swing = ( - self._tado_zone_data.current_vertical_swing_mode - ) - self._current_tado_horizontal_swing = ( - self._tado_zone_data.current_horizontal_swing_mode - ) + + if self._is_valid_setting_for_hvac_mode(TADO_FANLEVEL_SETTING): + self._current_tado_fan_level = self._tado_zone_data.current_fan_level + if self._is_valid_setting_for_hvac_mode(TADO_FANSPEED_SETTING): + self._current_tado_fan_speed = self._tado_zone_data.current_fan_speed + if self._is_valid_setting_for_hvac_mode(TADO_SWING_SETTING): + self._current_tado_swing_mode = self._tado_zone_data.current_swing_mode + if self._is_valid_setting_for_hvac_mode(TADO_VERTICAL_SWING_SETTING): + self._current_tado_vertical_swing = ( + self._tado_zone_data.current_vertical_swing_mode + ) + if self._is_valid_setting_for_hvac_mode(TADO_HORIZONTAL_SWING_SETTING): + self._current_tado_horizontal_swing = ( + self._tado_zone_data.current_horizontal_swing_mode + ) @callback def _async_update_zone_callback(self) -> None: @@ -665,7 +685,10 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): self._target_temp = target_temp if fan_mode: - self._current_tado_fan_speed = fan_mode + if self._is_valid_setting_for_hvac_mode(TADO_FANSPEED_SETTING): + self._current_tado_fan_speed = fan_mode + if self._is_valid_setting_for_hvac_mode(TADO_FANLEVEL_SETTING): + self._current_tado_fan_level = fan_mode if swing_mode: self._current_tado_swing_mode = swing_mode @@ -735,21 +758,32 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): fan_speed = None fan_level = None if self.supported_features & ClimateEntityFeature.FAN_MODE: - if self._current_tado_fan_speed in TADO_FAN_LEVELS: - fan_level = self._current_tado_fan_speed - elif self._current_tado_fan_speed in TADO_FAN_SPEEDS: + if self._is_current_setting_supported_by_current_hvac_mode( + TADO_FANSPEED_SETTING, self._current_tado_fan_speed + ): fan_speed = self._current_tado_fan_speed + if self._is_current_setting_supported_by_current_hvac_mode( + TADO_FANLEVEL_SETTING, self._current_tado_fan_level + ): + fan_level = self._current_tado_fan_level + swing = None vertical_swing = None horizontal_swing = None if ( self.supported_features & ClimateEntityFeature.SWING_MODE ) and self._attr_swing_modes is not None: - if SWING_VERTICAL in self._attr_swing_modes: + if self._is_current_setting_supported_by_current_hvac_mode( + TADO_VERTICAL_SWING_SETTING, self._current_tado_vertical_swing + ): vertical_swing = self._current_tado_vertical_swing - if SWING_HORIZONTAL in self._attr_swing_modes: + if self._is_current_setting_supported_by_current_hvac_mode( + TADO_HORIZONTAL_SWING_SETTING, self._current_tado_horizontal_swing + ): horizontal_swing = self._current_tado_horizontal_swing - if vertical_swing is None and horizontal_swing is None: + if self._is_current_setting_supported_by_current_hvac_mode( + TADO_SWING_SETTING, self._current_tado_swing_mode + ): swing = self._current_tado_swing_mode self._tado.set_zone_overlay( @@ -765,3 +799,20 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): vertical_swing=vertical_swing, # api defaults to not sending verticalSwing if swing not None horizontal_swing=horizontal_swing, # api defaults to not sending horizontalSwing if swing not None ) + + def _is_valid_setting_for_hvac_mode(self, setting: str) -> bool: + return ( + self._current_tado_capabilities.get(self._current_tado_hvac_mode, {}).get( + setting + ) + is not None + ) + + def _is_current_setting_supported_by_current_hvac_mode( + self, setting: str, current_state: str | None + ) -> bool: + if self._is_valid_setting_for_hvac_mode(setting): + return current_state in self._current_tado_capabilities[ + self._current_tado_hvac_mode + ].get(setting, []) + return False diff --git a/homeassistant/components/tado/const.py b/homeassistant/components/tado/const.py index 5c6a80c5beb..8033a653325 100644 --- a/homeassistant/components/tado/const.py +++ b/homeassistant/components/tado/const.py @@ -234,3 +234,10 @@ CONF_READING = "reading" ATTR_MESSAGE = "message" WATER_HEATER_FALLBACK_REPAIR = "water_heater_fallback" + +TADO_SWING_SETTING = "swings" +TADO_FANSPEED_SETTING = "fanSpeeds" + +TADO_FANLEVEL_SETTING = "fanLevel" +TADO_VERTICAL_SWING_SETTING = "verticalSwing" +TADO_HORIZONTAL_SWING_SETTING = "horizontalSwing" From 9be20d61301bfc3ddddf66f052c87ffe1bf92b94 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 30 Aug 2024 22:07:36 -1000 Subject: [PATCH 141/271] Restore sisyphus integration (#124749) * Revert "Disable sisyphus integration (#124742)" This reverts commit 1b304e60d926ceffbe79e25c5065af233fc4c059. * Restore sisyphus integration reverts #124742 and updates the lib instead changelog: https://github.com/jkeljo/sisyphus-control/compare/v3.1.3...v3.1.4 release is pending: https://github.com/jkeljo/sisyphus-control/pull/8#issuecomment-2313893689 --- homeassistant/components/sisyphus/__init__.py | 3 +-- homeassistant/components/sisyphus/manifest.json | 3 +-- homeassistant/components/sisyphus/media_player.py | 3 +-- homeassistant/components/sisyphus/ruff.toml | 5 ----- requirements_all.txt | 3 +++ 5 files changed, 6 insertions(+), 11 deletions(-) delete mode 100644 homeassistant/components/sisyphus/ruff.toml diff --git a/homeassistant/components/sisyphus/__init__.py b/homeassistant/components/sisyphus/__init__.py index 1fc440f260d..da8d670d412 100644 --- a/homeassistant/components/sisyphus/__init__.py +++ b/homeassistant/components/sisyphus/__init__.py @@ -1,10 +1,9 @@ """Support for controlling Sisyphus Kinetic Art Tables.""" -# mypy: ignore-errors import asyncio import logging -# from sisyphus_control import Table +from sisyphus_control import Table import voluptuous as vol from homeassistant.const import CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STOP, Platform diff --git a/homeassistant/components/sisyphus/manifest.json b/homeassistant/components/sisyphus/manifest.json index f1d90cebbd3..4e344c0b25e 100644 --- a/homeassistant/components/sisyphus/manifest.json +++ b/homeassistant/components/sisyphus/manifest.json @@ -2,9 +2,8 @@ "domain": "sisyphus", "name": "Sisyphus", "codeowners": ["@jkeljo"], - "disabled": "This integration is disabled because it uses an old version of socketio.", "documentation": "https://www.home-assistant.io/integrations/sisyphus", "iot_class": "local_push", "loggers": ["sisyphus_control"], - "requirements": ["sisyphus-control==3.1.3"] + "requirements": ["sisyphus-control==3.1.4"] } diff --git a/homeassistant/components/sisyphus/media_player.py b/homeassistant/components/sisyphus/media_player.py index 0248bbeac32..3884a83928a 100644 --- a/homeassistant/components/sisyphus/media_player.py +++ b/homeassistant/components/sisyphus/media_player.py @@ -1,11 +1,10 @@ """Support for track controls on the Sisyphus Kinetic Art Table.""" -# mypy: ignore-errors from __future__ import annotations import aiohttp +from sisyphus_control import Track -# from sisyphus_control import Track from homeassistant.components.media_player import ( MediaPlayerEntity, MediaPlayerEntityFeature, diff --git a/homeassistant/components/sisyphus/ruff.toml b/homeassistant/components/sisyphus/ruff.toml deleted file mode 100644 index 38f6f586aef..00000000000 --- a/homeassistant/components/sisyphus/ruff.toml +++ /dev/null @@ -1,5 +0,0 @@ -extend = "../../../pyproject.toml" - -lint.extend-ignore = [ - "F821" -] diff --git a/requirements_all.txt b/requirements_all.txt index 6037623d90f..4e9d761fb49 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2618,6 +2618,9 @@ simplepush==2.2.3 # homeassistant.components.simplisafe simplisafe-python==2024.01.0 +# homeassistant.components.sisyphus +sisyphus-control==3.1.4 + # homeassistant.components.slack slackclient==2.5.0 From 0948a944092534888ae18ea599beffc15f25379a Mon Sep 17 00:00:00 2001 From: vhkristof Date: Sat, 31 Aug 2024 10:56:23 +0200 Subject: [PATCH 142/271] Bump renault-api to v0.2.7 (#124858) * Bump renault-api to v0.2.7 * Updated requirements_all and requirements_test_all --- homeassistant/components/renault/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/renault/manifest.json b/homeassistant/components/renault/manifest.json index 6691921e850..716f2086bf1 100644 --- a/homeassistant/components/renault/manifest.json +++ b/homeassistant/components/renault/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["renault_api"], "quality_scale": "platinum", - "requirements": ["renault-api==0.2.5"] + "requirements": ["renault-api==0.2.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4e9d761fb49..dbb003f6735 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2498,7 +2498,7 @@ refoss-ha==1.2.4 regenmaschine==2024.03.0 # homeassistant.components.renault -renault-api==0.2.5 +renault-api==0.2.7 # homeassistant.components.renson renson-endura-delta==1.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index deed5fb713d..d464872f7fe 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1980,7 +1980,7 @@ refoss-ha==1.2.4 regenmaschine==2024.03.0 # homeassistant.components.renault -renault-api==0.2.5 +renault-api==0.2.7 # homeassistant.components.renson renson-endura-delta==1.7.1 From c6ff445dd418011006073fbedf2ef84dcfb7e4cf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 30 Aug 2024 09:05:16 -1000 Subject: [PATCH 143/271] Bump aioshelly to 11.4.1 to accomodate shelly GetStatus calls that take a few seconds to respond (#124893) Co-authored-by: Shay Levy --- homeassistant/components/shelly/coordinator.py | 3 +-- homeassistant/components/shelly/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/shelly/test_coordinator.py | 4 ++-- 5 files changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index 012f6b43dc7..c8e6cc03a06 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -793,8 +793,7 @@ class ShellyRpcPollingCoordinator(ShellyCoordinatorBase[RpcDevice]): LOGGER.debug("Polling Shelly RPC Device - %s", self.name) try: - await self.device.update_status() - await self.device.get_dynamic_components() + await self.device.poll() except (DeviceConnectionError, RpcCallError) as err: raise UpdateFailed(f"Device disconnected: {err!r}") from err except InvalidAuthError: diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index a384255705c..f9fa2d571d1 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -9,7 +9,7 @@ "iot_class": "local_push", "loggers": ["aioshelly"], "quality_scale": "platinum", - "requirements": ["aioshelly==11.3.0"], + "requirements": ["aioshelly==11.4.1"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index dbb003f6735..e6dc220e46c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -359,7 +359,7 @@ aioruuvigateway==0.1.0 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==11.3.0 +aioshelly==11.4.1 # homeassistant.components.skybell aioskybell==22.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d464872f7fe..8a6aa9cd879 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -341,7 +341,7 @@ aioruuvigateway==0.1.0 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==11.3.0 +aioshelly==11.4.1 # homeassistant.components.skybell aioskybell==22.7.0 diff --git a/tests/components/shelly/test_coordinator.py b/tests/components/shelly/test_coordinator.py index bb9694cf9b4..47c338e3fad 100644 --- a/tests/components/shelly/test_coordinator.py +++ b/tests/components/shelly/test_coordinator.py @@ -678,7 +678,7 @@ async def test_rpc_polling_auth_error( monkeypatch.setattr( mock_rpc_device, - "update_status", + "poll", AsyncMock( side_effect=InvalidAuthError, ), @@ -768,7 +768,7 @@ async def test_rpc_polling_connection_error( monkeypatch.setattr( mock_rpc_device, - "update_status", + "poll", AsyncMock( side_effect=DeviceConnectionError, ), From b2b69e40fd727f9d530f526c3d663aeb5d40988c Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 30 Aug 2024 22:02:10 +0200 Subject: [PATCH 144/271] Make set_value required in number template (#124917) * Make set_value required in number template * Make set_value required in number template * Fix tests --- homeassistant/components/template/number.py | 9 ++++----- tests/components/template/test_config_flow.py | 20 +++++++++++++++++++ tests/components/template/test_init.py | 10 ++++++++++ tests/components/template/test_number.py | 10 ++++++++++ 4 files changed, 44 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/template/number.py b/homeassistant/components/template/number.py index 955600a9b9e..499ddc192cc 100644 --- a/homeassistant/components/template/number.py +++ b/homeassistant/components/template/number.py @@ -70,7 +70,7 @@ NUMBER_CONFIG_SCHEMA = vol.Schema( vol.Required(CONF_NAME): cv.template, vol.Required(CONF_STATE): cv.template, vol.Required(CONF_STEP): cv.template, - vol.Optional(CONF_SET_VALUE): cv.SCRIPT_SCHEMA, + vol.Required(CONF_SET_VALUE): cv.SCRIPT_SCHEMA, vol.Optional(CONF_MIN): cv.template, vol.Optional(CONF_MAX): cv.template, vol.Optional(CONF_DEVICE_ID): selector.DeviceSelector(), @@ -154,11 +154,10 @@ class TemplateNumber(TemplateEntity, NumberEntity): super().__init__(hass, config=config, unique_id=unique_id) assert self._attr_name is not None self._value_template = config[CONF_STATE] - self._command_set_value = ( - Script(hass, config[CONF_SET_VALUE], self._attr_name, DOMAIN) - if config.get(CONF_SET_VALUE, None) is not None - else None + self._command_set_value = Script( + hass, config[CONF_SET_VALUE], self._attr_name, DOMAIN ) + self._step_template = config[CONF_STEP] self._min_value_template = config[CONF_MIN] self._max_value_template = config[CONF_MAX] diff --git a/tests/components/template/test_config_flow.py b/tests/components/template/test_config_flow.py index a62370f4261..f8ab190e664 100644 --- a/tests/components/template/test_config_flow.py +++ b/tests/components/template/test_config_flow.py @@ -101,11 +101,21 @@ from tests.typing import WebSocketGenerator "min": "{{ 0 }}", "max": "{{ 100 }}", "step": "{{ 0.1 }}", + "set_value": { + "action": "input_number.set_value", + "target": {"entity_id": "input_number.test"}, + "data": {"value": "{{ value }}"}, + }, }, { "min": "{{ 0 }}", "max": "{{ 100 }}", "step": "{{ 0.1 }}", + "set_value": { + "action": "input_number.set_value", + "target": {"entity_id": "input_number.test"}, + "data": {"value": "{{ value }}"}, + }, }, {}, ), @@ -444,11 +454,21 @@ def get_suggested(schema, key): "min": "{{ 0 }}", "max": "{{ 100 }}", "step": "{{ 0.1 }}", + "set_value": { + "action": "input_number.set_value", + "target": {"entity_id": "input_number.test"}, + "data": {"value": "{{ value }}"}, + }, }, { "min": "{{ 0 }}", "max": "{{ 100 }}", "step": "{{ 0.1 }}", + "set_value": { + "action": "input_number.set_value", + "target": {"entity_id": "input_number.test"}, + "data": {"value": "{{ value }}"}, + }, }, "state", ), diff --git a/tests/components/template/test_init.py b/tests/components/template/test_init.py index 06d59d4d176..3b4db4bf668 100644 --- a/tests/components/template/test_init.py +++ b/tests/components/template/test_init.py @@ -322,12 +322,22 @@ async def async_yaml_patch_helper(hass: HomeAssistant, filename: str) -> None: "min": "{{ 0 }}", "max": "{{ 100 }}", "step": "{{ 0.1 }}", + "set_value": { + "action": "input_number.set_value", + "target": {"entity_id": "input_number.test"}, + "data": {"value": "{{ value }}"}, + }, }, { "state": "{{ 11 }}", "min": "{{ 0 }}", "max": "{{ 100 }}", "step": "{{ 0.1 }}", + "set_value": { + "action": "input_number.set_value", + "target": {"entity_id": "input_number.test"}, + "data": {"value": "{{ value }}"}, + }, }, ), ( diff --git a/tests/components/template/test_number.py b/tests/components/template/test_number.py index c8befc2b8f8..fdca94d9fa4 100644 --- a/tests/components/template/test_number.py +++ b/tests/components/template/test_number.py @@ -61,6 +61,11 @@ async def test_setup_config_entry( "min": "{{ 0 }}", "max": "{{ 100 }}", "step": "{{ 0.1 }}", + "set_value": { + "action": "input_number.set_value", + "target": {"entity_id": "input_number.test"}, + "data": {"value": "{{ value }}"}, + }, }, title="My template", ) @@ -522,6 +527,11 @@ async def test_device_id( "min": "{{ 0 }}", "max": "{{ 100 }}", "step": "{{ 0.1 }}", + "set_value": { + "action": "input_number.set_value", + "target": {"entity_id": "input_number.test"}, + "data": {"value": "{{ value }}"}, + }, "device_id": device_entry.id, }, title="My template", From 03ab471d2341e8383efb5fb630fc832fb5a40da4 Mon Sep 17 00:00:00 2001 From: "Steven B." <51370195+sdb9696@users.noreply.github.com> Date: Fri, 30 Aug 2024 19:43:28 +0100 Subject: [PATCH 145/271] Bump python-kasa to 0.7.2 (#124930) --- homeassistant/components/tplink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index 10b0ef61153..0d9761ec8ce 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -301,5 +301,5 @@ "iot_class": "local_polling", "loggers": ["kasa"], "quality_scale": "platinum", - "requirements": ["python-kasa[speedups]==0.7.1"] + "requirements": ["python-kasa[speedups]==0.7.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index e6dc220e46c..9a9371497a9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2313,7 +2313,7 @@ python-join-api==0.0.9 python-juicenet==1.1.0 # homeassistant.components.tplink -python-kasa[speedups]==0.7.1 +python-kasa[speedups]==0.7.2 # homeassistant.components.linkplay python-linkplay==0.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8a6aa9cd879..0218230605d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1831,7 +1831,7 @@ python-izone==1.2.9 python-juicenet==1.1.0 # homeassistant.components.tplink -python-kasa[speedups]==0.7.1 +python-kasa[speedups]==0.7.2 # homeassistant.components.linkplay python-linkplay==0.0.8 From 9cfad057932540540dea408ec5e8ebe9913ce9c7 Mon Sep 17 00:00:00 2001 From: "Steven B." <51370195+sdb9696@users.noreply.github.com> Date: Fri, 30 Aug 2024 19:56:30 +0100 Subject: [PATCH 146/271] Exclude tplink firmware entities (#124935) Co-authored-by: J. Nick Koston --- homeassistant/components/tplink/entity.py | 2 ++ tests/components/tplink/fixtures/features.json | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/homeassistant/components/tplink/entity.py b/homeassistant/components/tplink/entity.py index 4ec0480cf82..beb71d4e5ce 100644 --- a/homeassistant/components/tplink/entity.py +++ b/homeassistant/components/tplink/entity.py @@ -68,6 +68,8 @@ EXCLUDED_FEATURES = { # update "current_firmware_version", "available_firmware_version", + "update_available", + "check_latest_firmware", } diff --git a/tests/components/tplink/fixtures/features.json b/tests/components/tplink/fixtures/features.json index 7cfe979ea25..6d4afd98d15 100644 --- a/tests/components/tplink/fixtures/features.json +++ b/tests/components/tplink/fixtures/features.json @@ -150,6 +150,11 @@ "type": "Sensor", "category": "Debug" }, + "check_latest_firmware": { + "value": "", + "type": "Action", + "category": "Info" + }, "thermostat_mode": { "value": "off", "type": "Sensor", From d54c1935f8bdbecd483a57e41e3f3536f1475616 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 31 Aug 2024 12:00:12 +0200 Subject: [PATCH 147/271] Define household support in Mealie (#124950) --- homeassistant/components/mealie/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/mealie/__init__.py b/homeassistant/components/mealie/__init__.py index 5c9c91729c0..bf0fbcac406 100644 --- a/homeassistant/components/mealie/__init__.py +++ b/homeassistant/components/mealie/__init__.py @@ -48,6 +48,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: MealieConfigEntry) -> bo ), ) try: + await client.define_household_support() about = await client.get_about() version = create_version(about.version) except MealieAuthenticationError as error: From 1b9aa727f8c4b2eeaf76f47e0803a21679453a21 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 30 Aug 2024 23:03:08 -1000 Subject: [PATCH 148/271] Bump yarl to 1.9.6 (#124955) * Bump yarl to 1.9.5 changelog: https://github.com/aio-libs/yarl/compare/v1.9.4...v1.9.5 * remove default port since mocker does exact matching and yarl now normalizes this * 1.9.6 --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- tests/components/dremel_3d_printer/conftest.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c01b23ab4e4..30edee058bb 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -62,7 +62,7 @@ urllib3>=1.26.5,<2 voluptuous-openapi==0.0.5 voluptuous-serialize==2.6.0 voluptuous==0.15.2 -yarl==1.9.4 +yarl==1.9.6 zeroconf==0.133.0 # Constrain pycryptodome to avoid vulnerability diff --git a/pyproject.toml b/pyproject.toml index 5b78a55a831..c0658bf903a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,7 +69,7 @@ dependencies = [ "voluptuous==0.15.2", "voluptuous-serialize==2.6.0", "voluptuous-openapi==0.0.5", - "yarl==1.9.4", + "yarl==1.9.6", ] [project.urls] diff --git a/requirements.txt b/requirements.txt index ad6a39ddb54..a9e01545b83 100644 --- a/requirements.txt +++ b/requirements.txt @@ -41,4 +41,4 @@ urllib3>=1.26.5,<2 voluptuous==0.15.2 voluptuous-serialize==2.6.0 voluptuous-openapi==0.0.5 -yarl==1.9.4 +yarl==1.9.6 diff --git a/tests/components/dremel_3d_printer/conftest.py b/tests/components/dremel_3d_printer/conftest.py index 6490b844dc0..cc70537db3d 100644 --- a/tests/components/dremel_3d_printer/conftest.py +++ b/tests/components/dremel_3d_printer/conftest.py @@ -34,7 +34,7 @@ def connection() -> None: """Mock Dremel 3D Printer connection.""" with requests_mock.Mocker() as mock: mock.post( - f"http://{HOST}:80/command", + f"http://{HOST}/command", response_list=[ {"text": load_fixture("dremel_3d_printer/command_1.json")}, {"text": load_fixture("dremel_3d_printer/command_2.json")}, From f9bca7619ca6113c42dc5cb75ed1671cfbf0aa83 Mon Sep 17 00:00:00 2001 From: Alan Murray Date: Sat, 31 Aug 2024 19:33:58 +1000 Subject: [PATCH 149/271] Bump aiopulse to 0.4.6 (#124964) Non-breaking changes to fix isses: * eliminating hub exceptions raised due use of unicode strings. * eliminating hub exceptions raised due to Timers being configured on hub. --- homeassistant/components/acmeda/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/acmeda/manifest.json b/homeassistant/components/acmeda/manifest.json index a8b3c7c829f..0c35904cac6 100644 --- a/homeassistant/components/acmeda/manifest.json +++ b/homeassistant/components/acmeda/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/acmeda", "iot_class": "local_push", "loggers": ["aiopulse"], - "requirements": ["aiopulse==0.4.4"] + "requirements": ["aiopulse==0.4.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index 9a9371497a9..6ddcac3ea4b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -318,7 +318,7 @@ aiooui==0.1.6 aiopegelonline==0.0.10 # homeassistant.components.acmeda -aiopulse==0.4.4 +aiopulse==0.4.6 # homeassistant.components.purpleair aiopurpleair==2022.12.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0218230605d..d9a2170d690 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -300,7 +300,7 @@ aiooui==0.1.6 aiopegelonline==0.0.10 # homeassistant.components.acmeda -aiopulse==0.4.4 +aiopulse==0.4.6 # homeassistant.components.purpleair aiopurpleair==2022.12.1 From e04fc74fcfe04e0492b086ce6ffb9d0cf3da0297 Mon Sep 17 00:00:00 2001 From: Richard Kroegel <42204099+rikroe@users.noreply.github.com> Date: Sun, 1 Sep 2024 17:35:55 +0200 Subject: [PATCH 150/271] Fix ollama blocking on load_default_certs (#125012) * Fix ollama blocking on load_default_certs * Use get_default_context instead of client_context --- homeassistant/components/ollama/__init__.py | 3 ++- homeassistant/components/ollama/config_flow.py | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ollama/__init__.py b/homeassistant/components/ollama/__init__.py index 2ad389c55c3..3bcba567803 100644 --- a/homeassistant/components/ollama/__init__.py +++ b/homeassistant/components/ollama/__init__.py @@ -13,6 +13,7 @@ from homeassistant.const import CONF_URL, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv +from homeassistant.util.ssl import get_default_context from .const import ( CONF_KEEP_ALIVE, @@ -43,7 +44,7 @@ PLATFORMS = (Platform.CONVERSATION,) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Ollama from a config entry.""" settings = {**entry.data, **entry.options} - client = ollama.AsyncClient(host=settings[CONF_URL]) + client = ollama.AsyncClient(host=settings[CONF_URL], verify=get_default_context()) try: async with asyncio.timeout(DEFAULT_TIMEOUT): await client.list() diff --git a/homeassistant/components/ollama/config_flow.py b/homeassistant/components/ollama/config_flow.py index 6b516d67138..65b8efaf525 100644 --- a/homeassistant/components/ollama/config_flow.py +++ b/homeassistant/components/ollama/config_flow.py @@ -33,6 +33,7 @@ from homeassistant.helpers.selector import ( TextSelectorConfig, TextSelectorType, ) +from homeassistant.util.ssl import get_default_context from .const import ( CONF_KEEP_ALIVE, @@ -91,7 +92,9 @@ class OllamaConfigFlow(ConfigFlow, domain=DOMAIN): errors = {} try: - self.client = ollama.AsyncClient(host=self.url) + self.client = ollama.AsyncClient( + host=self.url, verify=get_default_context() + ) async with asyncio.timeout(DEFAULT_TIMEOUT): response = await self.client.list() From 7662ca8a96b1da623a1727fcb2d984eaedf77304 Mon Sep 17 00:00:00 2001 From: Richard Kroegel <42204099+rikroe@users.noreply.github.com> Date: Sun, 1 Sep 2024 17:13:04 +0200 Subject: [PATCH 151/271] Fix telegram_bot blocking on load_default_certs (#125014) * Fix telegram_bot blocking on load_default_certs * Use sync variant of create_issue --- homeassistant/components/telegram_bot/__init__.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index 9d1a5398055..2d53c744c22 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -41,6 +41,7 @@ from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv, issue_registry as ir from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_loaded_integration +from homeassistant.util.ssl import get_default_context, get_default_no_verify_context _LOGGER = logging.getLogger(__name__) @@ -378,7 +379,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: for p_config in domain_config: # Each platform config gets its own bot - bot = initialize_bot(hass, p_config) + bot = await hass.async_add_executor_job(initialize_bot, hass, p_config) p_type: str = p_config[CONF_PLATFORM] platform = platforms[p_type] @@ -486,7 +487,7 @@ def initialize_bot(hass: HomeAssistant, p_config: dict) -> Bot: # Auth can actually be stuffed into the URL, but the docs have previously # indicated to put them here. auth = proxy_params.pop("username"), proxy_params.pop("password") - ir.async_create_issue( + ir.create_issue( hass, DOMAIN, "proxy_params_auth_deprecation", @@ -503,7 +504,7 @@ def initialize_bot(hass: HomeAssistant, p_config: dict) -> Bot: learn_more_url="https://github.com/home-assistant/core/pull/112778", ) else: - ir.async_create_issue( + ir.create_issue( hass, DOMAIN, "proxy_params_deprecation", @@ -852,7 +853,11 @@ class TelegramNotificationService: username=kwargs.get(ATTR_USERNAME), password=kwargs.get(ATTR_PASSWORD), authentication=kwargs.get(ATTR_AUTHENTICATION), - verify_ssl=kwargs.get(ATTR_VERIFY_SSL), + verify_ssl=( + get_default_context() + if kwargs.get(ATTR_VERIFY_SSL, False) + else get_default_no_verify_context() + ), ) if file_content: From 06660f9170552e250fc29d78a75bb19465f77f4c Mon Sep 17 00:00:00 2001 From: Richard Kroegel <42204099+rikroe@users.noreply.github.com> Date: Sun, 1 Sep 2024 16:26:14 +0200 Subject: [PATCH 152/271] Fix BMW client blocking on load_default_certs (#125015) * Fix BMW client blocking load_default_certs * Use get_default_context --- homeassistant/components/bmw_connected_drive/coordinator.py | 2 ++ homeassistant/components/bmw_connected_drive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/coordinator.py b/homeassistant/components/bmw_connected_drive/coordinator.py index 6e0ed2ab670..992e7dea6b2 100644 --- a/homeassistant/components/bmw_connected_drive/coordinator.py +++ b/homeassistant/components/bmw_connected_drive/coordinator.py @@ -15,6 +15,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.util.ssl import get_default_context from .const import CONF_GCID, CONF_READ_ONLY, CONF_REFRESH_TOKEN, DOMAIN, SCAN_INTERVALS @@ -33,6 +34,7 @@ class BMWDataUpdateCoordinator(DataUpdateCoordinator[None]): entry.data[CONF_PASSWORD], get_region_from_name(entry.data[CONF_REGION]), observer_position=GPSPosition(hass.config.latitude, hass.config.longitude), + verify=get_default_context(), ) self.read_only = entry.options[CONF_READ_ONLY] self._entry = entry diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index 7ee91388d29..6bc9027ac19 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -7,5 +7,5 @@ "iot_class": "cloud_polling", "loggers": ["bimmer_connected"], "quality_scale": "platinum", - "requirements": ["bimmer-connected[china]==0.16.2"] + "requirements": ["bimmer-connected[china]==0.16.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 6ddcac3ea4b..377c15f7eac 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -559,7 +559,7 @@ beautifulsoup4==4.12.3 # beewi-smartclim==0.0.10 # homeassistant.components.bmw_connected_drive -bimmer-connected[china]==0.16.2 +bimmer-connected[china]==0.16.3 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d9a2170d690..73cb59248ad 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -493,7 +493,7 @@ base36==0.1.1 beautifulsoup4==4.12.3 # homeassistant.components.bmw_connected_drive -bimmer-connected[china]==0.16.2 +bimmer-connected[china]==0.16.3 # homeassistant.components.eq3btsmart # homeassistant.components.esphome From 62ef951ace91d2aee31cc03d5a14f32bfa89694e Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 1 Sep 2024 17:37:06 +0200 Subject: [PATCH 153/271] Bump aiomealie to 0.9.1 (#125017) --- homeassistant/components/mealie/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mealie/manifest.json b/homeassistant/components/mealie/manifest.json index 4a277cbd09b..d8fe26d97b3 100644 --- a/homeassistant/components/mealie/manifest.json +++ b/homeassistant/components/mealie/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/mealie", "integration_type": "service", "iot_class": "local_polling", - "requirements": ["aiomealie==0.9.0"] + "requirements": ["aiomealie==0.9.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 377c15f7eac..19b9c2b0a56 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -288,7 +288,7 @@ aiolookin==1.0.0 aiolyric==2.0.1 # homeassistant.components.mealie -aiomealie==0.9.0 +aiomealie==0.9.1 # homeassistant.components.modern_forms aiomodernforms==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 73cb59248ad..5e781ed842d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -270,7 +270,7 @@ aiolookin==1.0.0 aiolyric==2.0.1 # homeassistant.components.mealie -aiomealie==0.9.0 +aiomealie==0.9.1 # homeassistant.components.modern_forms aiomodernforms==0.1.8 From b1ef1be9a372eb8063848df13ac86204d9f2120f Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sun, 1 Sep 2024 17:51:31 +0200 Subject: [PATCH 154/271] Bump python-telegram-bot to 21.5 (#125025) --- homeassistant/components/telegram_bot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/telegram_bot/manifest.json b/homeassistant/components/telegram_bot/manifest.json index c176e6c2cdf..b432c88762f 100644 --- a/homeassistant/components/telegram_bot/manifest.json +++ b/homeassistant/components/telegram_bot/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/telegram_bot", "iot_class": "cloud_push", "loggers": ["telegram"], - "requirements": ["python-telegram-bot[socks]==21.0.1"] + "requirements": ["python-telegram-bot[socks]==21.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index 19b9c2b0a56..92f8fdab44a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2368,7 +2368,7 @@ python-tado==0.17.6 python-technove==1.3.1 # homeassistant.components.telegram_bot -python-telegram-bot[socks]==21.0.1 +python-telegram-bot[socks]==21.5 # homeassistant.components.vlc python-vlc==3.0.18122 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5e781ed842d..b4229ed4d23 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1880,7 +1880,7 @@ python-tado==0.17.6 python-technove==1.3.1 # homeassistant.components.telegram_bot -python-telegram-bot[socks]==21.0.1 +python-telegram-bot[socks]==21.5 # homeassistant.components.tile pytile==2023.12.0 From fa3a301e975582774db6a7bef763a3674f06685c Mon Sep 17 00:00:00 2001 From: dontinelli <73341522+dontinelli@users.noreply.github.com> Date: Sun, 1 Sep 2024 21:02:32 +0200 Subject: [PATCH 155/271] Add ConductivityConverter in websocket_api.py (#125029) --- homeassistant/components/recorder/websocket_api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index 5e0eef37721..f08f7bdcb97 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -15,6 +15,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.json import json_bytes from homeassistant.util import dt as dt_util from homeassistant.util.unit_conversion import ( + ConductivityConverter, DataRateConverter, DistanceConverter, DurationConverter, @@ -48,7 +49,7 @@ from .util import PERIOD_SCHEMA, get_instance, resolve_period UNIT_SCHEMA = vol.Schema( { - vol.Optional("conductivity"): vol.In(DataRateConverter.VALID_UNITS), + vol.Optional("conductivity"): vol.In(ConductivityConverter.VALID_UNITS), vol.Optional("data_rate"): vol.In(DataRateConverter.VALID_UNITS), vol.Optional("distance"): vol.In(DistanceConverter.VALID_UNITS), vol.Optional("duration"): vol.In(DurationConverter.VALID_UNITS), From a8f472f44ec34d1ef6dd025bebdcefaca5940943 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 1 Sep 2024 22:04:29 +0200 Subject: [PATCH 156/271] Add diagnostics platform to modern forms (#125032) --- .../components/modern_forms/diagnostics.py | 36 +++++++++++++ .../snapshots/test_diagnostics.ambr | 50 +++++++++++++++++++ .../modern_forms/test_diagnostics.py | 26 ++++++++++ 3 files changed, 112 insertions(+) create mode 100644 homeassistant/components/modern_forms/diagnostics.py create mode 100644 tests/components/modern_forms/snapshots/test_diagnostics.ambr create mode 100644 tests/components/modern_forms/test_diagnostics.py diff --git a/homeassistant/components/modern_forms/diagnostics.py b/homeassistant/components/modern_forms/diagnostics.py new file mode 100644 index 00000000000..0011a7c3bab --- /dev/null +++ b/homeassistant/components/modern_forms/diagnostics.py @@ -0,0 +1,36 @@ +"""Diagnostics support for Modern Forms.""" + +from __future__ import annotations + +from dataclasses import asdict +from typing import TYPE_CHECKING, Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_MAC +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +from .coordinator import ModernFormsDataUpdateCoordinator + +REDACT_CONFIG = {CONF_MAC} +REDACT_DEVICE_INFO = {"mac_address", "owner"} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + coordinator: ModernFormsDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + if TYPE_CHECKING: + assert coordinator is not None + + return { + "config_entry": async_redact_data(entry.as_dict(), REDACT_CONFIG), + "device": { + "info": async_redact_data( + asdict(coordinator.modern_forms.info), REDACT_DEVICE_INFO + ), + "status": asdict(coordinator.modern_forms.status), + }, + } diff --git a/tests/components/modern_forms/snapshots/test_diagnostics.ambr b/tests/components/modern_forms/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..56e299aa12a --- /dev/null +++ b/tests/components/modern_forms/snapshots/test_diagnostics.ambr @@ -0,0 +1,50 @@ +# serializer version: 1 +# name: test_entry_diagnostics + dict({ + 'config_entry': dict({ + 'data': dict({ + 'host': '192.168.1.123', + 'mac': '**REDACTED**', + }), + 'disabled_by': None, + 'domain': 'modern_forms', + 'minor_version': 1, + 'options': dict({ + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'title': 'Mock Title', + 'unique_id': None, + 'version': 1, + }), + 'device': dict({ + 'info': dict({ + 'client_id': 'MF_000000000000', + 'device_name': 'ModernFormsFan', + 'fan_motor_type': 'DC125X25', + 'fan_type': '1818-56', + 'federated_identity': 'us-east-1:f3da237b-c19c-4f61-b387-0e6dde2e470b', + 'firmware_url': '', + 'firmware_version': '01.03.0025', + 'light_type': 'F6IN-120V-R1-30', + 'mac_address': '**REDACTED**', + 'main_mcu_firmware_version': '01.03.3008', + 'owner': '**REDACTED**', + 'product_sku': '', + 'production_lot_number': '', + }), + 'status': dict({ + 'adaptive_learning_enabled': False, + 'away_mode_enabled': False, + 'fan_direction': 'forward', + 'fan_on': True, + 'fan_sleep_timer': 0, + 'fan_speed': 3, + 'light_brightness': 50, + 'light_on': True, + 'light_sleep_timer': 0, + }), + }), + }) +# --- diff --git a/tests/components/modern_forms/test_diagnostics.py b/tests/components/modern_forms/test_diagnostics.py new file mode 100644 index 00000000000..9eb2e4efa94 --- /dev/null +++ b/tests/components/modern_forms/test_diagnostics.py @@ -0,0 +1,26 @@ +"""Tests for the Modern Forms diagnostics platform.""" + +from syrupy import SnapshotAssertion +from syrupy.filters import props + +from homeassistant.core import HomeAssistant + +from . import init_integration + +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.test_util.aiohttp import AiohttpClientMocker +from tests.typing import ClientSessionGenerator + + +async def test_entry_diagnostics( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + hass_client: ClientSessionGenerator, + snapshot: SnapshotAssertion, +) -> None: + """Test the creation and values of the Modern Forms fans.""" + entry = await init_integration(hass, aioclient_mock) + + result = await get_diagnostics_for_config_entry(hass, hass_client, entry) + + assert result == snapshot(exclude=props("created_at", "modified_at", "entry_id")) From 450c63ad28372242cc73e93af5dede2bd90404a1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 1 Sep 2024 10:47:24 -1000 Subject: [PATCH 157/271] Bump yarl to 1.9.7 (#125035) --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 30edee058bb..414bff657a0 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -62,7 +62,7 @@ urllib3>=1.26.5,<2 voluptuous-openapi==0.0.5 voluptuous-serialize==2.6.0 voluptuous==0.15.2 -yarl==1.9.6 +yarl==1.9.7 zeroconf==0.133.0 # Constrain pycryptodome to avoid vulnerability diff --git a/pyproject.toml b/pyproject.toml index c0658bf903a..1ebdef36e83 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,7 +69,7 @@ dependencies = [ "voluptuous==0.15.2", "voluptuous-serialize==2.6.0", "voluptuous-openapi==0.0.5", - "yarl==1.9.6", + "yarl==1.9.7", ] [project.urls] diff --git a/requirements.txt b/requirements.txt index a9e01545b83..fd6e8815e90 100644 --- a/requirements.txt +++ b/requirements.txt @@ -41,4 +41,4 @@ urllib3>=1.26.5,<2 voluptuous==0.15.2 voluptuous-serialize==2.6.0 voluptuous-openapi==0.0.5 -yarl==1.9.6 +yarl==1.9.7 From 3b5c08ecf88eb2479d74e70e7ef47797634a108e Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 2 Sep 2024 00:08:19 +0300 Subject: [PATCH 158/271] Bump aioshelly to 11.4.2 (#125036) --- homeassistant/components/shelly/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index f9fa2d571d1..5e2522ea456 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -9,7 +9,7 @@ "iot_class": "local_push", "loggers": ["aioshelly"], "quality_scale": "platinum", - "requirements": ["aioshelly==11.4.1"], + "requirements": ["aioshelly==11.4.2"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 92f8fdab44a..f8716fa7b26 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -359,7 +359,7 @@ aioruuvigateway==0.1.0 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==11.4.1 +aioshelly==11.4.2 # homeassistant.components.skybell aioskybell==22.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b4229ed4d23..ecddcfa8a42 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -341,7 +341,7 @@ aioruuvigateway==0.1.0 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==11.4.1 +aioshelly==11.4.2 # homeassistant.components.skybell aioskybell==22.7.0 From f85a802ebdc62544a2c0815cb08c4e5471f6fe8f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 2 Sep 2024 14:20:50 +0200 Subject: [PATCH 159/271] Don't raise when registering entity service with invalid schema (#125057) * Don't raise when registering entity service with invalid schema * Update homeassistant/helpers/service.py Co-authored-by: Robert Resch --------- Co-authored-by: Robert Resch --- homeassistant/helpers/service.py | 11 ++++++++++- tests/helpers/test_entity_component.py | 22 ++++++++++++---------- tests/helpers/test_entity_platform.py | 22 ++++++++++++---------- 3 files changed, 34 insertions(+), 21 deletions(-) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 573073f3809..bb9490b9edd 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -1268,7 +1268,16 @@ def async_register_entity_service( # the check could be extended to require All/Any to have sub schema(s) # with all entity service fields elif not cv.is_entity_service_schema(schema): - raise HomeAssistantError("The schema is not an entity service schema") + # pylint: disable-next=import-outside-toplevel + from .frame import report + + report( + ( + "registers an entity service with a non entity service schema " + "which will stop working in HA Core 2025.9" + ), + error_if_core=False, + ) service_func: str | HassJob[..., Any] service_func = func if isinstance(func, str) else HassJob(func) diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 8f4ece09a17..9723b91eb9a 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -557,21 +557,22 @@ async def test_register_entity_service( async def test_register_entity_service_non_entity_service_schema( - hass: HomeAssistant, + hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test attempting to register a service with a non entity service schema.""" component = EntityComponent(_LOGGER, DOMAIN, hass) + expected_message = "registers an entity service with a non entity service schema" - for schema in ( - vol.Schema({"some": str}), - vol.All(vol.Schema({"some": str})), - vol.Any(vol.Schema({"some": str})), + for idx, schema in enumerate( + ( + vol.Schema({"some": str}), + vol.All(vol.Schema({"some": str})), + vol.Any(vol.Schema({"some": str})), + ) ): - with pytest.raises( - HomeAssistantError, - match=("The schema is not an entity service schema"), - ): - component.async_register_entity_service("hello", schema, Mock()) + component.async_register_entity_service(f"hello_{idx}", schema, Mock()) + assert expected_message in caplog.text + caplog.clear() for idx, schema in enumerate( ( @@ -581,6 +582,7 @@ async def test_register_entity_service_non_entity_service_schema( ) ): component.async_register_entity_service(f"test_service_{idx}", schema, Mock()) + assert expected_message not in caplog.text async def test_register_entity_service_response_data(hass: HomeAssistant) -> None: diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 2b0598cfe9d..db83819085b 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -1811,23 +1811,24 @@ async def test_register_entity_service_none_schema( async def test_register_entity_service_non_entity_service_schema( - hass: HomeAssistant, + hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test attempting to register a service with a non entity service schema.""" entity_platform = MockEntityPlatform( hass, domain="mock_integration", platform_name="mock_platform", platform=None ) + expected_message = "registers an entity service with a non entity service schema" - for schema in ( - vol.Schema({"some": str}), - vol.All(vol.Schema({"some": str})), - vol.Any(vol.Schema({"some": str})), + for idx, schema in enumerate( + ( + vol.Schema({"some": str}), + vol.All(vol.Schema({"some": str})), + vol.Any(vol.Schema({"some": str})), + ) ): - with pytest.raises( - HomeAssistantError, - match="The schema is not an entity service schema", - ): - entity_platform.async_register_entity_service("hello", schema, Mock()) + entity_platform.async_register_entity_service(f"hello_{idx}", schema, Mock()) + assert expected_message in caplog.text + caplog.clear() for idx, schema in enumerate( ( @@ -1839,6 +1840,7 @@ async def test_register_entity_service_non_entity_service_schema( entity_platform.async_register_entity_service( f"test_service_{idx}", schema, Mock() ) + assert expected_message not in caplog.text @pytest.mark.parametrize("update_before_add", [True, False]) From 1a67052cbd7160bde6f003d3c80dc6a60283dc2a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 1 Sep 2024 23:28:42 -1000 Subject: [PATCH 160/271] Bump habluetooth to 3.4.0 (#125058) changelog: https://github.com/Bluetooth-Devices/habluetooth/compare/v3.3.2...v3.4.0 --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 027e2450bb4..0d17be70e0b 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -20,6 +20,6 @@ "bluetooth-auto-recovery==1.4.2", "bluetooth-data-tools==1.20.0", "dbus-fast==2.24.0", - "habluetooth==3.3.2" + "habluetooth==3.4.0" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 414bff657a0..0b91d1e792c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -27,7 +27,7 @@ dbus-fast==2.24.0 fnv-hash-fast==1.0.2 ha-av==10.1.1 ha-ffmpeg==3.2.0 -habluetooth==3.3.2 +habluetooth==3.4.0 hass-nabucasa==0.81.1 hassil==1.7.4 home-assistant-bluetooth==1.12.2 diff --git a/requirements_all.txt b/requirements_all.txt index f8716fa7b26..5b198b1384a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1059,7 +1059,7 @@ ha-philipsjs==3.2.2 habitipy==0.3.1 # homeassistant.components.bluetooth -habluetooth==3.3.2 +habluetooth==3.4.0 # homeassistant.components.cloud hass-nabucasa==0.81.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ecddcfa8a42..79cc4b05bce 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -894,7 +894,7 @@ ha-philipsjs==3.2.2 habitipy==0.3.1 # homeassistant.components.bluetooth -habluetooth==3.3.2 +habluetooth==3.4.0 # homeassistant.components.cloud hass-nabucasa==0.81.1 From 16ab57c9a655080b3f2f10e25b20fd9383dbded8 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 2 Sep 2024 09:51:05 +0200 Subject: [PATCH 161/271] Fix motionblinds_ble tests (#125060) --- tests/components/motionblinds_ble/test_entity.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/components/motionblinds_ble/test_entity.py b/tests/components/motionblinds_ble/test_entity.py index 1bfd3b185e5..00369ba1e22 100644 --- a/tests/components/motionblinds_ble/test_entity.py +++ b/tests/components/motionblinds_ble/test_entity.py @@ -23,6 +23,7 @@ from . import setup_integration from tests.common import MockConfigEntry +@pytest.mark.usefixtures("motionblinds_ble_connect") @pytest.mark.parametrize( ("platform", "entity"), [ From e7f957def2fe825dc9918f7db4fb4ef5cd6bb8f8 Mon Sep 17 00:00:00 2001 From: tronikos Date: Mon, 2 Sep 2024 01:41:29 -0700 Subject: [PATCH 162/271] Bump androidtvremote2 to 0.1.2 to fix blocking event loop when loading ssl certificate chain (#125061) Bump androidtvremote2 to 0.1.2 --- homeassistant/components/androidtv_remote/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/androidtv_remote/manifest.json b/homeassistant/components/androidtv_remote/manifest.json index e24fcc5d653..a06152fa570 100644 --- a/homeassistant/components/androidtv_remote/manifest.json +++ b/homeassistant/components/androidtv_remote/manifest.json @@ -8,6 +8,6 @@ "iot_class": "local_push", "loggers": ["androidtvremote2"], "quality_scale": "platinum", - "requirements": ["androidtvremote2==0.1.1"], + "requirements": ["androidtvremote2==0.1.2"], "zeroconf": ["_androidtvremote2._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 5b198b1384a..4c7d440730e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -440,7 +440,7 @@ amcrest==1.9.8 androidtv[async]==0.0.73 # homeassistant.components.androidtv_remote -androidtvremote2==0.1.1 +androidtvremote2==0.1.2 # homeassistant.components.anel_pwrctrl anel-pwrctrl-homeassistant==0.0.1.dev2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 79cc4b05bce..d484bb70fdb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -416,7 +416,7 @@ amberelectric==1.1.1 androidtv[async]==0.0.73 # homeassistant.components.androidtv_remote -androidtvremote2==0.1.1 +androidtvremote2==0.1.2 # homeassistant.components.anova anova-wifi==0.17.0 From d07e62b2f151fc47290d62964a0e2d63a8d0bde4 Mon Sep 17 00:00:00 2001 From: dontinelli <73341522+dontinelli@users.noreply.github.com> Date: Mon, 2 Sep 2024 11:27:31 +0200 Subject: [PATCH 163/271] Bump fyta_cli to 0.6.6 (#125065) --- homeassistant/components/fyta/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/fyta/manifest.json b/homeassistant/components/fyta/manifest.json index c07a19a3db0..dbd44ed34dc 100644 --- a/homeassistant/components/fyta/manifest.json +++ b/homeassistant/components/fyta/manifest.json @@ -7,5 +7,5 @@ "integration_type": "hub", "iot_class": "cloud_polling", "quality_scale": "platinum", - "requirements": ["fyta_cli==0.6.3"] + "requirements": ["fyta_cli==0.6.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4c7d440730e..8f20ce9b9f8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -924,7 +924,7 @@ freesms==0.2.0 fritzconnection[qr]==1.13.2 # homeassistant.components.fyta -fyta_cli==0.6.3 +fyta_cli==0.6.6 # homeassistant.components.google_translate gTTS==2.2.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d484bb70fdb..0e6e8901bba 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -777,7 +777,7 @@ freebox-api==1.1.0 fritzconnection[qr]==1.13.2 # homeassistant.components.fyta -fyta_cli==0.6.3 +fyta_cli==0.6.6 # homeassistant.components.google_translate gTTS==2.2.4 From a0f2e2ebdd40625987d3eb7b1eb32202fe9bf068 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 2 Sep 2024 20:04:33 +0200 Subject: [PATCH 164/271] Update frontend to 20240902.0 (#125093) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 7e934c887fa..50bcb3b3d97 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20240829.0"] + "requirements": ["home-assistant-frontend==20240902.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 0b91d1e792c..1729e6e8131 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -31,7 +31,7 @@ habluetooth==3.4.0 hass-nabucasa==0.81.1 hassil==1.7.4 home-assistant-bluetooth==1.12.2 -home-assistant-frontend==20240829.0 +home-assistant-frontend==20240902.0 home-assistant-intents==2024.8.29 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 8f20ce9b9f8..b42d34a761e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1102,7 +1102,7 @@ hole==0.8.0 holidays==0.55 # homeassistant.components.frontend -home-assistant-frontend==20240829.0 +home-assistant-frontend==20240902.0 # homeassistant.components.conversation home-assistant-intents==2024.8.29 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0e6e8901bba..de793acb135 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -925,7 +925,7 @@ hole==0.8.0 holidays==0.55 # homeassistant.components.frontend -home-assistant-frontend==20240829.0 +home-assistant-frontend==20240902.0 # homeassistant.components.conversation home-assistant-intents==2024.8.29 From c839cc1f152930039340e1417dbe5fc5af92fecc Mon Sep 17 00:00:00 2001 From: "Steven B." <51370195+sdb9696@users.noreply.github.com> Date: Mon, 2 Sep 2024 17:03:58 +0100 Subject: [PATCH 165/271] Call async_write_ha_state after ring update (#125096) Use async_write_ha_state after ring update --- homeassistant/components/ring/camera.py | 4 +++- homeassistant/components/ring/light.py | 2 +- homeassistant/components/ring/switch.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ring/camera.py b/homeassistant/components/ring/camera.py index b45803f3618..df71de29089 100644 --- a/homeassistant/components/ring/camera.py +++ b/homeassistant/components/ring/camera.py @@ -81,6 +81,8 @@ class RingCam(RingEntity[RingDoorBell], Camera): history_data = self._device.last_history if history_data: self._last_event = history_data[0] + # will call async_update to update the attributes and get the + # video url from the api self.async_schedule_update_ha_state(True) else: self._last_event = None @@ -183,7 +185,7 @@ class RingCam(RingEntity[RingDoorBell], Camera): await self._device.async_set_motion_detection(new_state) self._attr_motion_detection_enabled = new_state - self.async_schedule_update_ha_state(False) + self.async_write_ha_state() async def async_enable_motion_detection(self) -> None: """Enable motion detection in the camera.""" diff --git a/homeassistant/components/ring/light.py b/homeassistant/components/ring/light.py index f7f7f9b44ae..99c4105f4e9 100644 --- a/homeassistant/components/ring/light.py +++ b/homeassistant/components/ring/light.py @@ -86,7 +86,7 @@ class RingLight(RingEntity[RingStickUpCam], LightEntity): self._attr_is_on = new_state == OnOffState.ON self._no_updates_until = dt_util.utcnow() + SKIP_UPDATES_DELAY - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on for 30 seconds.""" diff --git a/homeassistant/components/ring/switch.py b/homeassistant/components/ring/switch.py index 810011d68c8..effb43cedbe 100644 --- a/homeassistant/components/ring/switch.py +++ b/homeassistant/components/ring/switch.py @@ -87,7 +87,7 @@ class SirenSwitch(BaseRingSwitch): self._attr_is_on = new_state > 0 self._no_updates_until = dt_util.utcnow() + SKIP_UPDATES_DELAY - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_turn_on(self, **kwargs: Any) -> None: """Turn the siren on for 30 seconds.""" From 3af11fb2b192b785f5071e7d28dd0c9b3aacdfce Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 2 Sep 2024 20:06:41 +0200 Subject: [PATCH 166/271] Bump version to 2024.9.0b3 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index e2026800727..5789c9becb8 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "0b2" +PATCH_VERSION: Final = "0b3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 1ebdef36e83..72f64391411 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.9.0b2" +version = "2024.9.0b3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From c7d1ad27f0591f8ab24e4bd363b07db2724f855b Mon Sep 17 00:00:00 2001 From: UltimateGG Date: Tue, 3 Sep 2024 09:31:48 -0500 Subject: [PATCH 167/271] Fix updating insteon modem configuration while disconnected (#121918) #121917 Fix updating insteon modem configuration while disconnected --- homeassistant/components/insteon/api/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/insteon/api/config.py b/homeassistant/components/insteon/api/config.py index 8a617911d1e..88c062c3271 100644 --- a/homeassistant/components/insteon/api/config.py +++ b/homeassistant/components/insteon/api/config.py @@ -211,7 +211,7 @@ async def websocket_update_modem_config( """Get the schema for the modem configuration.""" config = msg["config"] config_entry = get_insteon_config_entry(hass) - is_connected = devices.modem.connected + is_connected = devices.modem is not None and devices.modem.connected if not await _async_connect(**config): connection.send_error( From 009989d7ae1b9efda191858d980261e043e836af Mon Sep 17 00:00:00 2001 From: Philip Vanloo <26272906+dukeofphilberg@users.noreply.github.com> Date: Tue, 3 Sep 2024 13:34:47 +0200 Subject: [PATCH 168/271] Add Linkplay mTLS/HTTPS and improve logging (#124307) * Work * Implement 0.0.8 changes, fixup tests * Cleanup * Implement new playmodes, close clientsession upon ha close * Implement new playmodes, close clientsession upon ha close * Add test for zeroconf bridge failure * Bump 0.0.9 Address old comments in 113940 * Exact _async_register_default_clientsession_shutdown --- homeassistant/components/linkplay/__init__.py | 24 ++++++---- .../components/linkplay/config_flow.py | 40 ++++++++++++---- homeassistant/components/linkplay/const.py | 1 + .../components/linkplay/manifest.json | 2 +- .../components/linkplay/media_player.py | 11 +++++ homeassistant/components/linkplay/utils.py | 27 +++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/linkplay/conftest.py | 9 +++- tests/components/linkplay/test_config_flow.py | 48 +++++++++++++------ 10 files changed, 128 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/linkplay/__init__.py b/homeassistant/components/linkplay/__init__.py index c0fe711a61b..808f2f93ce2 100644 --- a/homeassistant/components/linkplay/__init__.py +++ b/homeassistant/components/linkplay/__init__.py @@ -1,17 +1,22 @@ """Support for LinkPlay devices.""" +from dataclasses import dataclass + +from aiohttp import ClientSession from linkplay.bridge import LinkPlayBridge -from linkplay.discovery import linkplay_factory_bridge +from linkplay.discovery import linkplay_factory_httpapi_bridge +from linkplay.exceptions import LinkPlayRequestException from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import PLATFORMS +from .utils import async_get_client_session +@dataclass class LinkPlayData: """Data for LinkPlay.""" @@ -24,16 +29,17 @@ type LinkPlayConfigEntry = ConfigEntry[LinkPlayData] async def async_setup_entry(hass: HomeAssistant, entry: LinkPlayConfigEntry) -> bool: """Async setup hass config entry. Called when an entry has been setup.""" - session = async_get_clientsession(hass) - if ( - bridge := await linkplay_factory_bridge(entry.data[CONF_HOST], session) - ) is None: + session: ClientSession = await async_get_client_session(hass) + bridge: LinkPlayBridge | None = None + + try: + bridge = await linkplay_factory_httpapi_bridge(entry.data[CONF_HOST], session) + except LinkPlayRequestException as exception: raise ConfigEntryNotReady( f"Failed to connect to LinkPlay device at {entry.data[CONF_HOST]}" - ) + ) from exception - entry.runtime_data = LinkPlayData() - entry.runtime_data.bridge = bridge + entry.runtime_data = LinkPlayData(bridge=bridge) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/linkplay/config_flow.py b/homeassistant/components/linkplay/config_flow.py index 0f9c40d0fd4..7dfdce238ff 100644 --- a/homeassistant/components/linkplay/config_flow.py +++ b/homeassistant/components/linkplay/config_flow.py @@ -1,16 +1,22 @@ """Config flow to configure LinkPlay component.""" +import logging from typing import Any -from linkplay.discovery import linkplay_factory_bridge +from aiohttp import ClientSession +from linkplay.bridge import LinkPlayBridge +from linkplay.discovery import linkplay_factory_httpapi_bridge +from linkplay.exceptions import LinkPlayRequestException import voluptuous as vol from homeassistant.components import zeroconf from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_MODEL -from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN +from .utils import async_get_client_session + +_LOGGER = logging.getLogger(__name__) class LinkPlayConfigFlow(ConfigFlow, domain=DOMAIN): @@ -25,10 +31,15 @@ class LinkPlayConfigFlow(ConfigFlow, domain=DOMAIN): ) -> ConfigFlowResult: """Handle Zeroconf discovery.""" - session = async_get_clientsession(self.hass) - bridge = await linkplay_factory_bridge(discovery_info.host, session) + session: ClientSession = await async_get_client_session(self.hass) + bridge: LinkPlayBridge | None = None - if bridge is None: + try: + bridge = await linkplay_factory_httpapi_bridge(discovery_info.host, session) + except LinkPlayRequestException: + _LOGGER.exception( + "Failed to connect to LinkPlay device at %s", discovery_info.host + ) return self.async_abort(reason="cannot_connect") self.data[CONF_HOST] = discovery_info.host @@ -66,14 +77,26 @@ class LinkPlayConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a flow initialized by the user.""" errors: dict[str, str] = {} if user_input: - session = async_get_clientsession(self.hass) - bridge = await linkplay_factory_bridge(user_input[CONF_HOST], session) + session: ClientSession = await async_get_client_session(self.hass) + bridge: LinkPlayBridge | None = None + + try: + bridge = await linkplay_factory_httpapi_bridge( + user_input[CONF_HOST], session + ) + except LinkPlayRequestException: + _LOGGER.exception( + "Failed to connect to LinkPlay device at %s", user_input[CONF_HOST] + ) + errors["base"] = "cannot_connect" if bridge is not None: self.data[CONF_HOST] = user_input[CONF_HOST] self.data[CONF_MODEL] = bridge.device.name - await self.async_set_unique_id(bridge.device.uuid) + await self.async_set_unique_id( + bridge.device.uuid, raise_on_progress=False + ) self._abort_if_unique_id_configured( updates={CONF_HOST: self.data[CONF_HOST]} ) @@ -83,7 +106,6 @@ class LinkPlayConfigFlow(ConfigFlow, domain=DOMAIN): data={CONF_HOST: self.data[CONF_HOST]}, ) - errors["base"] = "cannot_connect" return self.async_show_form( step_id="user", data_schema=vol.Schema({vol.Required(CONF_HOST): str}), diff --git a/homeassistant/components/linkplay/const.py b/homeassistant/components/linkplay/const.py index 48ae225dd98..91a427d5eb8 100644 --- a/homeassistant/components/linkplay/const.py +++ b/homeassistant/components/linkplay/const.py @@ -4,3 +4,4 @@ from homeassistant.const import Platform DOMAIN = "linkplay" PLATFORMS = [Platform.MEDIA_PLAYER] +CONF_SESSION = "session" diff --git a/homeassistant/components/linkplay/manifest.json b/homeassistant/components/linkplay/manifest.json index 5212f3f99b8..66a719c640e 100644 --- a/homeassistant/components/linkplay/manifest.json +++ b/homeassistant/components/linkplay/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/linkplay", "integration_type": "hub", "iot_class": "local_polling", - "requirements": ["python-linkplay==0.0.8"], + "requirements": ["python-linkplay==0.0.9"], "zeroconf": ["_linkplay._tcp.local."] } diff --git a/homeassistant/components/linkplay/media_player.py b/homeassistant/components/linkplay/media_player.py index 398add235bd..0b62b4dbcee 100644 --- a/homeassistant/components/linkplay/media_player.py +++ b/homeassistant/components/linkplay/media_player.py @@ -48,6 +48,17 @@ SOURCE_MAP: dict[PlayingMode, str] = { PlayingMode.XLR: "XLR", PlayingMode.HDMI: "HDMI", PlayingMode.OPTICAL_2: "Optical 2", + PlayingMode.EXTERN_BLUETOOTH: "External Bluetooth", + PlayingMode.PHONO: "Phono", + PlayingMode.ARC: "ARC", + PlayingMode.COAXIAL_2: "Coaxial 2", + PlayingMode.TF_CARD_1: "SD Card 1", + PlayingMode.TF_CARD_2: "SD Card 2", + PlayingMode.CD: "CD", + PlayingMode.DAB: "DAB Radio", + PlayingMode.FM: "FM Radio", + PlayingMode.RCA: "RCA", + PlayingMode.UDISK: "USB", } SOURCE_MAP_INV: dict[str, PlayingMode] = {v: k for k, v in SOURCE_MAP.items()} diff --git a/homeassistant/components/linkplay/utils.py b/homeassistant/components/linkplay/utils.py index 7532c9b354a..7f15e297145 100644 --- a/homeassistant/components/linkplay/utils.py +++ b/homeassistant/components/linkplay/utils.py @@ -2,6 +2,14 @@ from typing import Final +from aiohttp import ClientSession +from linkplay.utils import async_create_unverified_client_session + +from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE +from homeassistant.core import Event, HomeAssistant, callback + +from .const import CONF_SESSION, DOMAIN + MANUFACTURER_ARTSOUND: Final[str] = "ArtSound" MANUFACTURER_ARYLIC: Final[str] = "Arylic" MANUFACTURER_IEAST: Final[str] = "iEAST" @@ -44,3 +52,22 @@ def get_info_from_project(project: str) -> tuple[str, str]: return MANUFACTURER_IEAST, MODELS_IEAST_AUDIOCAST_M5 case _: return MANUFACTURER_GENERIC, MODELS_GENERIC + + +async def async_get_client_session(hass: HomeAssistant) -> ClientSession: + """Get a ClientSession that can be used with LinkPlay devices.""" + hass.data.setdefault(DOMAIN, {}) + if CONF_SESSION not in hass.data[DOMAIN]: + clientsession: ClientSession = await async_create_unverified_client_session() + + @callback + def _async_close_websession(event: Event) -> None: + """Close websession.""" + clientsession.detach() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, _async_close_websession) + hass.data[DOMAIN][CONF_SESSION] = clientsession + return clientsession + + session: ClientSession = hass.data[DOMAIN][CONF_SESSION] + return session diff --git a/requirements_all.txt b/requirements_all.txt index b42d34a761e..179e0512d01 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2316,7 +2316,7 @@ python-juicenet==1.1.0 python-kasa[speedups]==0.7.2 # homeassistant.components.linkplay -python-linkplay==0.0.8 +python-linkplay==0.0.9 # homeassistant.components.lirc # python-lirc==1.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index de793acb135..e5bc24881dc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1834,7 +1834,7 @@ python-juicenet==1.1.0 python-kasa[speedups]==0.7.2 # homeassistant.components.linkplay -python-linkplay==0.0.8 +python-linkplay==0.0.9 # homeassistant.components.matter python-matter-server==6.3.0 diff --git a/tests/components/linkplay/conftest.py b/tests/components/linkplay/conftest.py index b3d65422e08..be83dd2412d 100644 --- a/tests/components/linkplay/conftest.py +++ b/tests/components/linkplay/conftest.py @@ -3,6 +3,7 @@ from collections.abc import Generator from unittest.mock import AsyncMock, patch +from aiohttp import ClientSession from linkplay.bridge import LinkPlayBridge, LinkPlayDevice import pytest @@ -14,11 +15,15 @@ NAME = "Smart Zone 1_54B9" @pytest.fixture def mock_linkplay_factory_bridge() -> Generator[AsyncMock]: - """Mock for linkplay_factory_bridge.""" + """Mock for linkplay_factory_httpapi_bridge.""" with ( patch( - "homeassistant.components.linkplay.config_flow.linkplay_factory_bridge" + "homeassistant.components.linkplay.config_flow.async_get_client_session", + return_value=AsyncMock(spec=ClientSession), + ), + patch( + "homeassistant.components.linkplay.config_flow.linkplay_factory_httpapi_bridge", ) as factory, ): bridge = AsyncMock(spec=LinkPlayBridge) diff --git a/tests/components/linkplay/test_config_flow.py b/tests/components/linkplay/test_config_flow.py index 641f09893c2..3fd1fbea95e 100644 --- a/tests/components/linkplay/test_config_flow.py +++ b/tests/components/linkplay/test_config_flow.py @@ -3,6 +3,9 @@ from ipaddress import ip_address from unittest.mock import AsyncMock +from linkplay.exceptions import LinkPlayRequestException +import pytest + from homeassistant.components.linkplay.const import DOMAIN from homeassistant.components.zeroconf import ZeroconfServiceInfo from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF @@ -47,10 +50,9 @@ ZEROCONF_DISCOVERY_RE_ENTRY = ZeroconfServiceInfo( ) +@pytest.mark.usefixtures("mock_linkplay_factory_bridge", "mock_setup_entry") async def test_user_flow( hass: HomeAssistant, - mock_linkplay_factory_bridge: AsyncMock, - mock_setup_entry: AsyncMock, ) -> None: """Test user setup config flow.""" result = await hass.config_entries.flow.async_init( @@ -74,10 +76,9 @@ async def test_user_flow( assert result["result"].unique_id == UUID +@pytest.mark.usefixtures("mock_linkplay_factory_bridge") async def test_user_flow_re_entry( hass: HomeAssistant, - mock_linkplay_factory_bridge: AsyncMock, - mock_setup_entry: AsyncMock, ) -> None: """Test user setup config flow when an entry with the same unique id already exists.""" @@ -105,10 +106,9 @@ async def test_user_flow_re_entry( assert result["reason"] == "already_configured" +@pytest.mark.usefixtures("mock_linkplay_factory_bridge", "mock_setup_entry") async def test_zeroconf_flow( hass: HomeAssistant, - mock_linkplay_factory_bridge: AsyncMock, - mock_setup_entry: AsyncMock, ) -> None: """Test Zeroconf flow.""" result = await hass.config_entries.flow.async_init( @@ -133,10 +133,9 @@ async def test_zeroconf_flow( assert result["result"].unique_id == UUID +@pytest.mark.usefixtures("mock_linkplay_factory_bridge") async def test_zeroconf_flow_re_entry( hass: HomeAssistant, - mock_linkplay_factory_bridge: AsyncMock, - mock_setup_entry: AsyncMock, ) -> None: """Test Zeroconf flow when an entry with the same unique id already exists.""" @@ -160,16 +159,35 @@ async def test_zeroconf_flow_re_entry( assert result["reason"] == "already_configured" -async def test_flow_errors( +@pytest.mark.usefixtures("mock_setup_entry") +async def test_zeroconf_flow_errors( + hass: HomeAssistant, + mock_linkplay_factory_bridge: AsyncMock, +) -> None: + """Test flow when the device discovered through Zeroconf cannot be reached.""" + + # Temporarily make the mock_linkplay_factory_bridge throw an exception + mock_linkplay_factory_bridge.side_effect = (LinkPlayRequestException("Error"),) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=ZEROCONF_DISCOVERY, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "cannot_connect" + + +@pytest.mark.usefixtures("mock_setup_entry") +async def test_user_flow_errors( hass: HomeAssistant, mock_linkplay_factory_bridge: AsyncMock, - mock_setup_entry: AsyncMock, ) -> None: """Test flow when the device cannot be reached.""" - # Temporarily store bridge in a separate variable and set factory to return None - bridge = mock_linkplay_factory_bridge.return_value - mock_linkplay_factory_bridge.return_value = None + # Temporarily make the mock_linkplay_factory_bridge throw an exception + mock_linkplay_factory_bridge.side_effect = (LinkPlayRequestException("Error"),) result = await hass.config_entries.flow.async_init( DOMAIN, @@ -188,8 +206,8 @@ async def test_flow_errors( assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} - # Make linkplay_factory_bridge return a mock bridge again - mock_linkplay_factory_bridge.return_value = bridge + # Make mock_linkplay_factory_bridge_exception no longer throw an exception + mock_linkplay_factory_bridge.side_effect = None result = await hass.config_entries.flow.async_configure( result["flow_id"], From 9a690ed421969e2e3515c1ea9c6e8396f7569ec7 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 2 Sep 2024 21:23:24 +0200 Subject: [PATCH 169/271] Handle telegram polling errors (#124327) --- .../components/telegram_bot/polling.py | 16 ++- .../telegram_bot/test_telegram_bot.py | 103 +++++++++++++++++- 2 files changed, 114 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/telegram_bot/polling.py b/homeassistant/components/telegram_bot/polling.py index 45d2ee65b45..bee7f752f6c 100644 --- a/homeassistant/components/telegram_bot/polling.py +++ b/homeassistant/components/telegram_bot/polling.py @@ -25,14 +25,22 @@ async def async_setup_platform(hass, bot, config): async def process_error(update: Update, context: CallbackContext) -> None: """Telegram bot error handler.""" + if context.error: + error_callback(context.error, update) + + +def error_callback(error: Exception, update: Update | None = None) -> None: + """Log the error.""" try: - if context.error: - raise context.error + raise error except (TimedOut, NetworkError, RetryAfter): # Long polling timeout or connection problem. Nothing serious. pass except TelegramError: - _LOGGER.error('Update "%s" caused error: "%s"', update, context.error) + if update is not None: + _LOGGER.error('Update "%s" caused error: "%s"', update, error) + else: + _LOGGER.error("%s: %s", error.__class__.__name__, error) class PollBot(BaseTelegramBotEntity): @@ -53,7 +61,7 @@ class PollBot(BaseTelegramBotEntity): """Start the polling task.""" _LOGGER.debug("Starting polling") await self.application.initialize() - await self.application.updater.start_polling() + await self.application.updater.start_polling(error_callback=error_callback) await self.application.start() async def stop_polling(self, event=None): diff --git a/tests/components/telegram_bot/test_telegram_bot.py b/tests/components/telegram_bot/test_telegram_bot.py index aad758827ca..bdf6ba72fcc 100644 --- a/tests/components/telegram_bot/test_telegram_bot.py +++ b/tests/components/telegram_bot/test_telegram_bot.py @@ -1,8 +1,11 @@ """Tests for the telegram_bot component.""" -from unittest.mock import AsyncMock, patch +from typing import Any +from unittest.mock import AsyncMock, MagicMock, patch +import pytest from telegram import Update +from telegram.error import NetworkError, RetryAfter, TelegramError, TimedOut from homeassistant.components.telegram_bot import ( ATTR_MESSAGE, @@ -11,6 +14,7 @@ from homeassistant.components.telegram_bot import ( SERVICE_SEND_MESSAGE, ) from homeassistant.components.telegram_bot.webhooks import TELEGRAM_WEBHOOK_URL +from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.core import Context, HomeAssistant from homeassistant.setup import async_setup_component @@ -188,6 +192,103 @@ async def test_polling_platform_message_text_update( assert isinstance(events[0].context, Context) +@pytest.mark.parametrize( + ("error", "log_message"), + [ + ( + TelegramError("Telegram error"), + 'caused error: "Telegram error"', + ), + (NetworkError("Network error"), ""), + (RetryAfter(42), ""), + (TimedOut("TimedOut error"), ""), + ], +) +async def test_polling_platform_add_error_handler( + hass: HomeAssistant, + config_polling: dict[str, Any], + update_message_text: dict[str, Any], + caplog: pytest.LogCaptureFixture, + error: Exception, + log_message: str, +) -> None: + """Test polling add error handler.""" + with patch( + "homeassistant.components.telegram_bot.polling.ApplicationBuilder" + ) as application_builder_class: + await async_setup_component( + hass, + DOMAIN, + config_polling, + ) + await hass.async_block_till_done() + + application = ( + application_builder_class.return_value.bot.return_value.build.return_value + ) + application.updater.stop = AsyncMock() + application.stop = AsyncMock() + application.shutdown = AsyncMock() + process_error = application.add_error_handler.call_args[0][0] + application.bot.defaults.tzinfo = None + update = Update.de_json(update_message_text, application.bot) + + await process_error(update, MagicMock(error=error)) + + assert log_message in caplog.text + + +@pytest.mark.parametrize( + ("error", "log_message"), + [ + ( + TelegramError("Telegram error"), + "TelegramError: Telegram error", + ), + (NetworkError("Network error"), ""), + (RetryAfter(42), ""), + (TimedOut("TimedOut error"), ""), + ], +) +async def test_polling_platform_start_polling_error_callback( + hass: HomeAssistant, + config_polling: dict[str, Any], + caplog: pytest.LogCaptureFixture, + error: Exception, + log_message: str, +) -> None: + """Test polling add error handler.""" + with patch( + "homeassistant.components.telegram_bot.polling.ApplicationBuilder" + ) as application_builder_class: + await async_setup_component( + hass, + DOMAIN, + config_polling, + ) + await hass.async_block_till_done() + + application = ( + application_builder_class.return_value.bot.return_value.build.return_value + ) + application.initialize = AsyncMock() + application.updater.start_polling = AsyncMock() + application.start = AsyncMock() + application.updater.stop = AsyncMock() + application.stop = AsyncMock() + application.shutdown = AsyncMock() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + error_callback = application.updater.start_polling.call_args.kwargs[ + "error_callback" + ] + + error_callback(error) + + assert log_message in caplog.text + + async def test_webhook_endpoint_unauthorized_update_doesnt_generate_telegram_text_event( hass: HomeAssistant, webhook_platform, From b81d7a0ed896f1133d3cdfa1f093b45719e342ac Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 31 Aug 2024 11:38:45 -0700 Subject: [PATCH 170/271] Update nest to only include the image attachment payload for cameras that support fetching media (#124590) Only include the image attachment payload for cameras that support fetching media --- homeassistant/components/nest/__init__.py | 31 +++++++++------- tests/components/nest/test_events.py | 43 ++++++++++++++++++++--- 2 files changed, 57 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index da72fdfd53b..8a1719a9bd5 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -166,38 +166,43 @@ class SignalUpdateCallback: ) if not device_entry: return + supported_traits = self._supported_traits(device_id) for api_event_type, image_event in events.items(): if not (event_type := EVENT_NAME_MAP.get(api_event_type)): continue nest_event_id = image_event.event_token - attachment = { - "image": EVENT_THUMBNAIL_URL_FORMAT.format( - device_id=device_entry.id, event_token=image_event.event_token - ), - } - if self._supports_clip(device_id): - attachment["video"] = EVENT_MEDIA_API_URL_FORMAT.format( - device_id=device_entry.id, event_token=image_event.event_token - ) message = { "device_id": device_entry.id, "type": event_type, "timestamp": event_message.timestamp, "nest_event_id": nest_event_id, - "attachment": attachment, } + if ( + TraitType.CAMERA_EVENT_IMAGE in supported_traits + or TraitType.CAMERA_CLIP_PREVIEW in supported_traits + ): + attachment = { + "image": EVENT_THUMBNAIL_URL_FORMAT.format( + device_id=device_entry.id, event_token=image_event.event_token + ) + } + if TraitType.CAMERA_CLIP_PREVIEW in supported_traits: + attachment["video"] = EVENT_MEDIA_API_URL_FORMAT.format( + device_id=device_entry.id, event_token=image_event.event_token + ) + message["attachment"] = attachment if image_event.zones: message["zones"] = image_event.zones self._hass.bus.async_fire(NEST_EVENT, message) - def _supports_clip(self, device_id: str) -> bool: + def _supported_traits(self, device_id: str) -> list[TraitType]: if not ( device_manager := self._hass.data[DOMAIN] .get(self._config_entry_id, {}) .get(DATA_DEVICE_MANAGER) ) or not (device := device_manager.devices.get(device_id)): - return False - return TraitType.CAMERA_CLIP_PREVIEW in device.traits + return [] + return list(device.traits) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/tests/components/nest/test_events.py b/tests/components/nest/test_events.py index 643a2614bbc..e746e5f263f 100644 --- a/tests/components/nest/test_events.py +++ b/tests/components/nest/test_events.py @@ -122,28 +122,28 @@ def create_events(events, device_id=DEVICE_ID, timestamp=None): [ ( "sdm.devices.types.DOORBELL", - ["sdm.devices.traits.DoorbellChime"], + ["sdm.devices.traits.DoorbellChime", "sdm.devices.traits.CameraEventImage"], "sdm.devices.events.DoorbellChime.Chime", "Doorbell", "doorbell_chime", ), ( "sdm.devices.types.CAMERA", - ["sdm.devices.traits.CameraMotion"], + ["sdm.devices.traits.CameraMotion", "sdm.devices.traits.CameraEventImage"], "sdm.devices.events.CameraMotion.Motion", "Camera", "camera_motion", ), ( "sdm.devices.types.CAMERA", - ["sdm.devices.traits.CameraPerson"], + ["sdm.devices.traits.CameraPerson", "sdm.devices.traits.CameraEventImage"], "sdm.devices.events.CameraPerson.Person", "Camera", "camera_person", ), ( "sdm.devices.types.CAMERA", - ["sdm.devices.traits.CameraSound"], + ["sdm.devices.traits.CameraSound", "sdm.devices.traits.CameraEventImage"], "sdm.devices.events.CameraSound.Sound", "Camera", "camera_sound", @@ -234,6 +234,41 @@ async def test_camera_multiple_event( } +@pytest.mark.parametrize( + "device_traits", + [(["sdm.devices.traits.CameraMotion"])], +) +async def test_media_not_supported( + hass: HomeAssistant, entity_registry: er.EntityRegistry, subscriber, setup_platform +) -> None: + """Test a pubsub message for a camera person event.""" + events = async_capture_events(hass, NEST_EVENT) + await setup_platform() + entry = entity_registry.async_get("camera.front") + assert entry is not None + + event_map = { + "sdm.devices.events.CameraMotion.Motion": { + "eventSessionId": EVENT_SESSION_ID, + "eventId": EVENT_ID, + }, + } + + timestamp = utcnow() + await subscriber.async_receive_event(create_events(event_map, timestamp=timestamp)) + await hass.async_block_till_done() + + event_time = timestamp.replace(microsecond=0) + assert len(events) == 1 + assert event_view(events[0].data) == { + "device_id": entry.device_id, + "type": "camera_motion", + "timestamp": event_time, + } + # Media fetching not supported by this device + assert "attachment" not in events[0].data + + async def test_unknown_event(hass: HomeAssistant, subscriber, setup_platform) -> None: """Test a pubsub message for an unknown event type.""" events = async_capture_events(hass, NEST_EVENT) From 005be4e8baa338b7ba2b30402685b5967e2356b3 Mon Sep 17 00:00:00 2001 From: MJJ Date: Tue, 3 Sep 2024 16:50:30 +0200 Subject: [PATCH 171/271] Increase timeout for fetching buienradar weather data (#124597) Increase timeout for fetching weather data --- homeassistant/components/buienradar/const.py | 1 + homeassistant/components/buienradar/util.py | 16 ++++++++-------- homeassistant/components/buienradar/weather.py | 2 +- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/buienradar/const.py b/homeassistant/components/buienradar/const.py index c82970ed318..fd92afd59b0 100644 --- a/homeassistant/components/buienradar/const.py +++ b/homeassistant/components/buienradar/const.py @@ -2,6 +2,7 @@ DOMAIN = "buienradar" +DEFAULT_TIMEOUT = 60 DEFAULT_TIMEFRAME = 60 DEFAULT_DIMENSION = 700 diff --git a/homeassistant/components/buienradar/util.py b/homeassistant/components/buienradar/util.py index b641644cebe..f089fce89b7 100644 --- a/homeassistant/components/buienradar/util.py +++ b/homeassistant/components/buienradar/util.py @@ -1,9 +1,9 @@ """Shared utilities for different supported platforms.""" -from asyncio import timeout from datetime import datetime, timedelta from http import HTTPStatus import logging +from typing import Any import aiohttp from buienradar.buienradar import parse_data @@ -27,12 +27,12 @@ from buienradar.constants import ( from buienradar.urls import JSON_FEED_URL, json_precipitation_forecast_url from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE -from homeassistant.core import CALLBACK_TYPE, callback +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util import dt as dt_util -from .const import SCHEDULE_NOK, SCHEDULE_OK +from .const import DEFAULT_TIMEOUT, SCHEDULE_NOK, SCHEDULE_OK __all__ = ["BrData"] _LOGGER = logging.getLogger(__name__) @@ -59,10 +59,10 @@ class BrData: load_error_count: int = WARN_THRESHOLD rain_error_count: int = WARN_THRESHOLD - def __init__(self, hass, coordinates, timeframe, devices): + def __init__(self, hass: HomeAssistant, coordinates, timeframe, devices) -> None: """Initialize the data object.""" self.devices = devices - self.data = {} + self.data: dict[str, Any] | None = {} self.hass = hass self.coordinates = coordinates self.timeframe = timeframe @@ -93,9 +93,9 @@ class BrData: resp = None try: websession = async_get_clientsession(self.hass) - async with timeout(10): - resp = await websession.get(url) - + async with websession.get( + url, timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT) + ) as resp: result[STATUS_CODE] = resp.status result[CONTENT] = await resp.text() if resp.status == HTTPStatus.OK: diff --git a/homeassistant/components/buienradar/weather.py b/homeassistant/components/buienradar/weather.py index 02e1f444c9c..2af66982fab 100644 --- a/homeassistant/components/buienradar/weather.py +++ b/homeassistant/components/buienradar/weather.py @@ -130,7 +130,7 @@ class BrWeather(WeatherEntity): _attr_should_poll = False _attr_supported_features = WeatherEntityFeature.FORECAST_DAILY - def __init__(self, config, coordinates): + def __init__(self, config, coordinates) -> None: """Initialize the platform with a data instance and station name.""" self._stationname = config.get(CONF_NAME, "Buienradar") self._attr_name = self._stationname or f"BR {'(unknown station)'}" From a58bf149fcdaf9eceea8c6fc58e75f4ac5cb5c27 Mon Sep 17 00:00:00 2001 From: Richard Kroegel <42204099+rikroe@users.noreply.github.com> Date: Mon, 2 Sep 2024 21:23:38 +0200 Subject: [PATCH 172/271] Fix blocking calls for OpenAI conversation (#125010) --- .../components/openai_conversation/__init__.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/openai_conversation/__init__.py b/homeassistant/components/openai_conversation/__init__.py index 75b5db23094..0fbda9b7f4a 100644 --- a/homeassistant/components/openai_conversation/__init__.py +++ b/homeassistant/components/openai_conversation/__init__.py @@ -19,6 +19,7 @@ from homeassistant.exceptions import ( ServiceValidationError, ) from homeassistant.helpers import config_validation as cv, selector +from homeassistant.helpers.httpx_client import get_async_client from homeassistant.helpers.typing import ConfigType from .const import DOMAIN, LOGGER @@ -88,7 +89,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: OpenAIConfigEntry) -> bool: """Set up OpenAI Conversation from a config entry.""" - client = openai.AsyncOpenAI(api_key=entry.data[CONF_API_KEY]) + client = openai.AsyncOpenAI( + api_key=entry.data[CONF_API_KEY], + http_client=get_async_client(hass), + ) + + # Cache current platform data which gets added to each request (caching done by library) + _ = await hass.async_add_executor_job(client.platform_headers) + try: await hass.async_add_executor_job(client.with_options(timeout=10.0).models.list) except openai.AuthenticationError as err: From 4c5ba0617ae55366f293778ec726e4cc1258205d Mon Sep 17 00:00:00 2001 From: ilan <31193909+iloveicedgreentea@users.noreply.github.com> Date: Tue, 3 Sep 2024 07:56:59 -0400 Subject: [PATCH 173/271] Bump py-madvr2 to 1.6.32 (#125049) feat: update lib --- homeassistant/components/madvr/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/madvr/manifest.json b/homeassistant/components/madvr/manifest.json index ce6336acabc..0ac906fdbef 100644 --- a/homeassistant/components/madvr/manifest.json +++ b/homeassistant/components/madvr/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/madvr", "integration_type": "device", "iot_class": "local_push", - "requirements": ["py-madvr2==1.6.29"] + "requirements": ["py-madvr2==1.6.32"] } diff --git a/requirements_all.txt b/requirements_all.txt index 179e0512d01..3c908247fa7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1656,7 +1656,7 @@ py-dormakaba-dkey==1.0.5 py-improv-ble-client==1.0.3 # homeassistant.components.madvr -py-madvr2==1.6.29 +py-madvr2==1.6.32 # homeassistant.components.melissa py-melissa-climate==2.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e5bc24881dc..892fcab93a3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1348,7 +1348,7 @@ py-dormakaba-dkey==1.0.5 py-improv-ble-client==1.0.3 # homeassistant.components.madvr -py-madvr2==1.6.29 +py-madvr2==1.6.32 # homeassistant.components.melissa py-melissa-climate==2.1.4 From 94d2da1685b8bf5493adb7da24086cb364608a4a Mon Sep 17 00:00:00 2001 From: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com> Date: Mon, 2 Sep 2024 21:23:07 +0200 Subject: [PATCH 174/271] Fix area registry indexing when there is a name collision (#125050) --- homeassistant/helpers/area_registry.py | 5 +++-- tests/helpers/test_area_registry.py | 7 ++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/area_registry.py b/homeassistant/helpers/area_registry.py index 3e101f185ed..5009ec654cf 100644 --- a/homeassistant/helpers/area_registry.py +++ b/homeassistant/helpers/area_registry.py @@ -153,22 +153,23 @@ class AreaRegistryItems(NormalizedNameBaseRegistryItems[AreaEntry]): def _index_entry(self, key: str, entry: AreaEntry) -> None: """Index an entry.""" + super()._index_entry(key, entry) if entry.floor_id is not None: self._floors_index[entry.floor_id][key] = True for label in entry.labels: self._labels_index[label][key] = True - super()._index_entry(key, entry) def _unindex_entry( self, key: str, replacement_entry: AreaEntry | None = None ) -> None: + # always call base class before other indices + super()._unindex_entry(key, replacement_entry) entry = self.data[key] if labels := entry.labels: for label in labels: self._unindex_entry_value(key, label, self._labels_index) if floor_id := entry.floor_id: self._unindex_entry_value(key, floor_id, self._floors_index) - return super()._unindex_entry(key, replacement_entry) def get_areas_for_label(self, label: str) -> list[AreaEntry]: """Get areas for label.""" diff --git a/tests/helpers/test_area_registry.py b/tests/helpers/test_area_registry.py index ad571ac50cc..da1947adbc8 100644 --- a/tests/helpers/test_area_registry.py +++ b/tests/helpers/test_area_registry.py @@ -242,9 +242,12 @@ async def test_update_area_with_same_name_change_case( async def test_update_area_with_name_already_in_use( area_registry: ar.AreaRegistry, + floor_registry: fr.FloorRegistry, ) -> None: """Make sure that we can't update an area with a name already in use.""" - area1 = area_registry.async_create("mock1") + floor = floor_registry.async_create("mock") + floor_id = floor.floor_id + area1 = area_registry.async_create("mock1", floor_id=floor_id) area2 = area_registry.async_create("mock2") with pytest.raises(ValueError) as e_info: @@ -255,6 +258,8 @@ async def test_update_area_with_name_already_in_use( assert area2.name == "mock2" assert len(area_registry.areas) == 2 + assert area_registry.areas.get_areas_for_floor(floor_id) == [area1] + async def test_update_area_with_normalized_name_already_in_use( area_registry: ar.AreaRegistry, From d0054405440fe4deab7167f26e2b2619f9b3119c Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Tue, 3 Sep 2024 05:22:39 +1000 Subject: [PATCH 175/271] Bump aiolifx to 1.0.9 and remove unused HomeKit model prefixes (#125055) Co-authored-by: J. Nick Koston --- homeassistant/components/lifx/manifest.json | 4 +--- homeassistant/generated/zeroconf.py | 8 -------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 3 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/lifx/manifest.json b/homeassistant/components/lifx/manifest.json index 08540702736..3ef70f16467 100644 --- a/homeassistant/components/lifx/manifest.json +++ b/homeassistant/components/lifx/manifest.json @@ -17,7 +17,6 @@ "models": [ "LIFX A19", "LIFX A21", - "LIFX B10", "LIFX Beam", "LIFX BR30", "LIFX Candle", @@ -41,7 +40,6 @@ "LIFX Round", "LIFX Square", "LIFX String", - "LIFX T10", "LIFX Tile", "LIFX White", "LIFX Z" @@ -50,7 +48,7 @@ "iot_class": "local_polling", "loggers": ["aiolifx", "aiolifx_effects", "bitstring"], "requirements": [ - "aiolifx==1.0.8", + "aiolifx==1.0.9", "aiolifx-effects==0.3.2", "aiolifx-themes==0.5.0" ] diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 389a4435910..3e5e34090d1 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -68,10 +68,6 @@ HOMEKIT = { "always_discover": True, "domain": "lifx", }, - "LIFX B10": { - "always_discover": True, - "domain": "lifx", - }, "LIFX BR30": { "always_discover": True, "domain": "lifx", @@ -164,10 +160,6 @@ HOMEKIT = { "always_discover": True, "domain": "lifx", }, - "LIFX T10": { - "always_discover": True, - "domain": "lifx", - }, "LIFX Tile": { "always_discover": True, "domain": "lifx", diff --git a/requirements_all.txt b/requirements_all.txt index 3c908247fa7..f536b985686 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -276,7 +276,7 @@ aiolifx-effects==0.3.2 aiolifx-themes==0.5.0 # homeassistant.components.lifx -aiolifx==1.0.8 +aiolifx==1.0.9 # homeassistant.components.livisi aiolivisi==0.0.19 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 892fcab93a3..5e89bbed182 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -258,7 +258,7 @@ aiolifx-effects==0.3.2 aiolifx-themes==0.5.0 # homeassistant.components.lifx -aiolifx==1.0.8 +aiolifx==1.0.9 # homeassistant.components.livisi aiolivisi==0.0.19 From 3f65bc78e8c86912b4b91620a97e23f7beeb962b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Sep 2024 09:43:34 -1000 Subject: [PATCH 176/271] Bump yalexs to 8.6.0 (#125102) --- homeassistant/components/august/manifest.json | 2 +- homeassistant/components/yale/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index 5f317a20834..a40c6920136 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -24,5 +24,5 @@ "documentation": "https://www.home-assistant.io/integrations/august", "iot_class": "cloud_push", "loggers": ["pubnub", "yalexs"], - "requirements": ["yalexs==8.5.5", "yalexs-ble==2.4.3"] + "requirements": ["yalexs==8.6.0", "yalexs-ble==2.4.3"] } diff --git a/homeassistant/components/yale/manifest.json b/homeassistant/components/yale/manifest.json index 9bee7df2e00..030df50a482 100644 --- a/homeassistant/components/yale/manifest.json +++ b/homeassistant/components/yale/manifest.json @@ -13,5 +13,5 @@ "documentation": "https://www.home-assistant.io/integrations/yale", "iot_class": "cloud_push", "loggers": ["socketio", "engineio", "yalexs"], - "requirements": ["yalexs==8.5.5", "yalexs-ble==2.4.3"] + "requirements": ["yalexs==8.6.0", "yalexs-ble==2.4.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index f536b985686..1efa8c40c04 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2976,7 +2976,7 @@ yalexs-ble==2.4.3 # homeassistant.components.august # homeassistant.components.yale -yalexs==8.5.5 +yalexs==8.6.0 # homeassistant.components.yeelight yeelight==0.7.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5e89bbed182..7b1c0b670fd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2356,7 +2356,7 @@ yalexs-ble==2.4.3 # homeassistant.components.august # homeassistant.components.yale -yalexs==8.5.5 +yalexs==8.6.0 # homeassistant.components.yeelight yeelight==0.7.14 From a0bbcb0401261e2014abadd18f8e27cb74695003 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20J=C3=A1l?= Date: Tue, 3 Sep 2024 14:22:39 +0200 Subject: [PATCH 177/271] Bump PySwitchbot to 0.48.2 (#125113) --- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 0cbbd70a805..f97162184c6 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -39,5 +39,5 @@ "documentation": "https://www.home-assistant.io/integrations/switchbot", "iot_class": "local_push", "loggers": ["switchbot"], - "requirements": ["PySwitchbot==0.48.1"] + "requirements": ["PySwitchbot==0.48.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1efa8c40c04..993dbbe2c9a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -84,7 +84,7 @@ PyQRCode==1.2.1 PyRMVtransport==0.3.3 # homeassistant.components.switchbot -PySwitchbot==0.48.1 +PySwitchbot==0.48.2 # homeassistant.components.switchmate PySwitchmate==0.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7b1c0b670fd..b04a0f20ceb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ PyQRCode==1.2.1 PyRMVtransport==0.3.3 # homeassistant.components.switchbot -PySwitchbot==0.48.1 +PySwitchbot==0.48.2 # homeassistant.components.syncthru PySyncThru==0.7.10 From 393a0ac0df92e546fd5d3648a99423aa61227bf3 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 3 Sep 2024 06:21:52 -0600 Subject: [PATCH 178/271] Fix unhandled exception with missing IQVIA data (#125114) --- homeassistant/components/iqvia/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/iqvia/sensor.py b/homeassistant/components/iqvia/sensor.py index ba3c288b702..af351e0d543 100644 --- a/homeassistant/components/iqvia/sensor.py +++ b/homeassistant/components/iqvia/sensor.py @@ -244,8 +244,8 @@ class IndexSensor(IQVIAEntity, SensorEntity): key = self.entity_description.key.split("_")[-1].title() try: - [period] = [p for p in data["periods"] if p["Type"] == key] # type: ignore[index] - except TypeError: + period = next(p for p in data["periods"] if p["Type"] == key) # type: ignore[index] + except StopIteration: return data = cast(dict[str, Any], data) From be3b16b7fa10beb5af178d24452336d3fa488a29 Mon Sep 17 00:00:00 2001 From: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com> Date: Tue, 3 Sep 2024 12:43:31 +0200 Subject: [PATCH 179/271] Fix Onkyo action select_hdmi_output (#125115) * Fix Onkyo service select_hdmi_output * Move Hasskey directly under Onkyo domain --- .../components/onkyo/media_player.py | 72 +++++++++++-------- 1 file changed, 43 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/onkyo/media_player.py b/homeassistant/components/onkyo/media_player.py index acc0459e258..8d8f4d3bfd5 100644 --- a/homeassistant/components/onkyo/media_player.py +++ b/homeassistant/components/onkyo/media_player.py @@ -11,7 +11,7 @@ import pyeiscp import voluptuous as vol from homeassistant.components.media_player import ( - DOMAIN, + DOMAIN as MEDIA_PLAYER_DOMAIN, PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, @@ -28,9 +28,14 @@ from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.util.hass_dict import HassKey _LOGGER = logging.getLogger(__name__) +DOMAIN = "onkyo" + +DATA_MP_ENTITIES: HassKey[list[dict[str, OnkyoMediaPlayer]]] = HassKey(DOMAIN) + CONF_SOURCES = "sources" CONF_MAX_VOLUME = "max_volume" CONF_RECEIVER_MAX_VOLUME = "receiver_max_volume" @@ -148,6 +153,33 @@ class ReceiverInfo: identifier: str +async def async_register_services(hass: HomeAssistant) -> None: + """Register Onkyo services.""" + + async def async_service_handle(service: ServiceCall) -> None: + """Handle for services.""" + entity_ids = service.data[ATTR_ENTITY_ID] + + targets: list[OnkyoMediaPlayer] = [] + for receiver_entities in hass.data[DATA_MP_ENTITIES]: + targets.extend( + entity + for entity in receiver_entities.values() + if entity.entity_id in entity_ids + ) + + for target in targets: + if service.service == SERVICE_SELECT_HDMI_OUTPUT: + await target.async_select_output(service.data[ATTR_HDMI_OUTPUT]) + + hass.services.async_register( + MEDIA_PLAYER_DOMAIN, + SERVICE_SELECT_HDMI_OUTPUT, + async_service_handle, + schema=ONKYO_SELECT_OUTPUT_SCHEMA, + ) + + async def async_setup_platform( hass: HomeAssistant, config: ConfigType, @@ -155,29 +187,10 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Onkyo platform.""" + await async_register_services(hass) + receivers: dict[str, pyeiscp.Connection] = {} # indexed by host - entities: dict[str, dict[str, OnkyoMediaPlayer]] = {} # indexed by host and zone - - async def async_service_handle(service: ServiceCall) -> None: - """Handle for services.""" - entity_ids = service.data[ATTR_ENTITY_ID] - targets = [ - entity - for h in entities.values() - for entity in h.values() - if entity.entity_id in entity_ids - ] - - for target in targets: - if service.service == SERVICE_SELECT_HDMI_OUTPUT: - await target.async_select_output(service.data[ATTR_HDMI_OUTPUT]) - - hass.services.async_register( - DOMAIN, - SERVICE_SELECT_HDMI_OUTPUT, - async_service_handle, - schema=ONKYO_SELECT_OUTPUT_SCHEMA, - ) + all_entities = hass.data.setdefault(DATA_MP_ENTITIES, []) host = config.get(CONF_HOST) name = config.get(CONF_NAME) @@ -188,6 +201,9 @@ async def async_setup_platform( async def async_setup_receiver( info: ReceiverInfo, discovered: bool, name: str | None ) -> None: + entities: dict[str, OnkyoMediaPlayer] = {} + all_entities.append(entities) + @callback def async_onkyo_update_callback( message: tuple[str, str, Any], origin: str @@ -199,7 +215,7 @@ async def async_setup_platform( ) zone, _, value = message - entity = entities[origin].get(zone) + entity = entities.get(zone) if entity is not None: if entity.enabled: entity.process_update(message) @@ -210,7 +226,7 @@ async def async_setup_platform( zone_entity = OnkyoMediaPlayer( receiver, sources, zone, max_volume, receiver_max_volume ) - entities[origin][zone] = zone_entity + entities[zone] = zone_entity async_add_entities([zone_entity]) @callback @@ -221,7 +237,7 @@ async def async_setup_platform( "Receiver (re)connected: %s (%s)", receiver.name, receiver.host ) - for entity in entities[origin].values(): + for entity in entities.values(): entity.backfill_state() _LOGGER.debug("Creating receiver: %s (%s)", info.model_name, info.host) @@ -237,9 +253,7 @@ async def async_setup_platform( receiver.name = name or info.model_name receiver.discovered = discovered - # Store the receiver object and create a dictionary to store its entities. receivers[receiver.host] = receiver - entities[receiver.host] = {} # Discover what zones are available for the receiver by querying the power. # If we get a response for the specific zone, it means it is available. @@ -251,7 +265,7 @@ async def async_setup_platform( main_entity = OnkyoMediaPlayer( receiver, sources, "main", max_volume, receiver_max_volume ) - entities[receiver.host]["main"] = main_entity + entities["main"] = main_entity async_add_entities([main_entity]) if host is not None: From 4982e1cbcfdf1c93b0d577e9722110f9c7c69df9 Mon Sep 17 00:00:00 2001 From: "Steven B." <51370195+sdb9696@users.noreply.github.com> Date: Tue, 3 Sep 2024 13:00:30 +0100 Subject: [PATCH 180/271] Pass hass clientsession to ring config flow (#125119) Pass hass clientsession to ring config flow --- homeassistant/components/ring/config_flow.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ring/config_flow.py b/homeassistant/components/ring/config_flow.py index ee78541dec7..b82b4f22223 100644 --- a/homeassistant/components/ring/config_flow.py +++ b/homeassistant/components/ring/config_flow.py @@ -17,6 +17,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import CONF_2FA, DOMAIN @@ -31,7 +32,10 @@ STEP_REAUTH_DATA_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str}) async def validate_input(hass: HomeAssistant, data: dict[str, str]) -> dict[str, Any]: """Validate the user input allows us to connect.""" - auth = Auth(f"{APPLICATION_NAME}/{ha_version}") + auth = Auth( + f"{APPLICATION_NAME}/{ha_version}", + http_client_session=async_get_clientsession(hass), + ) try: token = await auth.async_fetch_token( From 31267b40958c36ea97aee5d76f3e412a9fa44297 Mon Sep 17 00:00:00 2001 From: Christopher Fenner <9592452+CFenner@users.noreply.github.com> Date: Tue, 3 Sep 2024 12:50:05 +0200 Subject: [PATCH 181/271] Correct device serial for ViCare integration (#125125) * expose correct serial * adapt inits * adjust _build_entities * adapt inits * add serial data point * update snapshot * apply suggestions * apply suggestions --- .../components/vicare/binary_sensor.py | 78 +++++++----------- homeassistant/components/vicare/button.py | 6 +- homeassistant/components/vicare/climate.py | 2 +- homeassistant/components/vicare/entity.py | 16 ++-- homeassistant/components/vicare/fan.py | 2 +- homeassistant/components/vicare/number.py | 43 +++++----- homeassistant/components/vicare/sensor.py | 79 +++++++------------ .../components/vicare/water_heater.py | 2 +- .../vicare/fixtures/Vitodens300W.json | 17 ++++ .../vicare/snapshots/test_diagnostics.ambr | 18 +++++ 10 files changed, 128 insertions(+), 135 deletions(-) diff --git a/homeassistant/components/vicare/binary_sensor.py b/homeassistant/components/vicare/binary_sensor.py index 2c114d15b85..7fe248fa266 100644 --- a/homeassistant/components/vicare/binary_sensor.py +++ b/homeassistant/components/vicare/binary_sensor.py @@ -112,61 +112,36 @@ def _build_entities( entities: list[ViCareBinarySensor] = [] for device in device_list: - entities.extend(_build_entities_for_device(device.api, device.config)) + # add device entities entities.extend( - _build_entities_for_component( - get_circuits(device.api), device.config, CIRCUIT_SENSORS + ViCareBinarySensor( + description, + device.config, + device.api, ) + for description in GLOBAL_SENSORS + if is_supported(description.key, description, device.api) ) - entities.extend( - _build_entities_for_component( - get_burners(device.api), device.config, BURNER_SENSORS + # add component entities + for component_list, entity_description_list in ( + (get_circuits(device.api), CIRCUIT_SENSORS), + (get_burners(device.api), BURNER_SENSORS), + (get_compressors(device.api), COMPRESSOR_SENSORS), + ): + entities.extend( + ViCareBinarySensor( + description, + device.config, + device.api, + component, + ) + for component in component_list + for description in entity_description_list + if is_supported(description.key, description, component) ) - ) - entities.extend( - _build_entities_for_component( - get_compressors(device.api), device.config, COMPRESSOR_SENSORS - ) - ) return entities -def _build_entities_for_device( - device: PyViCareDevice, - device_config: PyViCareDeviceConfig, -) -> list[ViCareBinarySensor]: - """Create device specific ViCare binary sensor entities.""" - - return [ - ViCareBinarySensor( - device_config, - device, - description, - ) - for description in GLOBAL_SENSORS - if is_supported(description.key, description, device) - ] - - -def _build_entities_for_component( - components: list[PyViCareHeatingDeviceComponent], - device_config: PyViCareDeviceConfig, - entity_descriptions: tuple[ViCareBinarySensorEntityDescription, ...], -) -> list[ViCareBinarySensor]: - """Create component specific ViCare binary sensor entities.""" - - return [ - ViCareBinarySensor( - device_config, - component, - description, - ) - for component in components - for description in entity_descriptions - if is_supported(description.key, description, component) - ] - - async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -190,12 +165,13 @@ class ViCareBinarySensor(ViCareEntity, BinarySensorEntity): def __init__( self, - device_config: PyViCareDeviceConfig, - api: PyViCareDevice | PyViCareHeatingDeviceComponent, description: ViCareBinarySensorEntityDescription, + device_config: PyViCareDeviceConfig, + device: PyViCareDevice, + component: PyViCareHeatingDeviceComponent | None = None, ) -> None: """Initialize the sensor.""" - super().__init__(device_config, api, description.key) + super().__init__(description.key, device_config, device, component) self.entity_description = description @property diff --git a/homeassistant/components/vicare/button.py b/homeassistant/components/vicare/button.py index f880c39ddea..51a763c1fcc 100644 --- a/homeassistant/components/vicare/button.py +++ b/homeassistant/components/vicare/button.py @@ -54,9 +54,9 @@ def _build_entities( return [ ViCareButton( + description, device.config, device.api, - description, ) for device in device_list for description in BUTTON_DESCRIPTIONS @@ -87,12 +87,12 @@ class ViCareButton(ViCareEntity, ButtonEntity): def __init__( self, + description: ViCareButtonEntityDescription, device_config: PyViCareDeviceConfig, device: PyViCareDevice, - description: ViCareButtonEntityDescription, ) -> None: """Initialize the button.""" - super().__init__(device_config, device, description.key) + super().__init__(description.key, device_config, device) self.entity_description = description def press(self) -> None: diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py index df1cde2abca..4968e565d0b 100644 --- a/homeassistant/components/vicare/climate.py +++ b/homeassistant/components/vicare/climate.py @@ -148,7 +148,7 @@ class ViCareClimate(ViCareEntity, ClimateEntity): circuit: PyViCareHeatingCircuit, ) -> None: """Initialize the climate device.""" - super().__init__(device_config, device, circuit.id) + super().__init__(circuit.id, device_config, device) self._circuit = circuit self._attributes: dict[str, Any] = {} self._attributes["vicare_programs"] = self._circuit.getPrograms() diff --git a/homeassistant/components/vicare/entity.py b/homeassistant/components/vicare/entity.py index 1bb2993cd3a..eef114b4039 100644 --- a/homeassistant/components/vicare/entity.py +++ b/homeassistant/components/vicare/entity.py @@ -2,6 +2,9 @@ from PyViCare.PyViCareDevice import Device as PyViCareDevice from PyViCare.PyViCareDeviceConfig import PyViCareDeviceConfig +from PyViCare.PyViCareHeatingDevice import ( + HeatingDeviceWithComponent as PyViCareHeatingDeviceComponent, +) from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import Entity @@ -16,21 +19,24 @@ class ViCareEntity(Entity): def __init__( self, + unique_id_suffix: str, device_config: PyViCareDeviceConfig, device: PyViCareDevice, - unique_id_suffix: str, + component: PyViCareHeatingDeviceComponent | None = None, ) -> None: """Initialize the entity.""" - self._api = device + self._api: PyViCareDevice | PyViCareHeatingDeviceComponent = ( + component if component else device + ) self._attr_unique_id = f"{device_config.getConfig().serial}-{unique_id_suffix}" # valid for compressors, circuits, burners (HeatingDeviceWithComponent) - if hasattr(device, "id"): - self._attr_unique_id += f"-{device.id}" + if component: + self._attr_unique_id += f"-{component.id}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, device_config.getConfig().serial)}, - serial_number=device_config.getConfig().serial, + serial_number=device.getSerial(), name=device_config.getModel(), manufacturer="Viessmann", model=device_config.getModel(), diff --git a/homeassistant/components/vicare/fan.py b/homeassistant/components/vicare/fan.py index 5b9dd2787e8..d7dbd037b56 100644 --- a/homeassistant/components/vicare/fan.py +++ b/homeassistant/components/vicare/fan.py @@ -129,7 +129,7 @@ class ViCareFan(ViCareEntity, FanEntity): device: PyViCareDevice, ) -> None: """Initialize the fan entity.""" - super().__init__(device_config, device, self._attr_translation_key) + super().__init__(self._attr_translation_key, device_config, device) def update(self) -> None: """Update state of fan.""" diff --git a/homeassistant/components/vicare/number.py b/homeassistant/components/vicare/number.py index d53b7183327..3a0cd8dd2cb 100644 --- a/homeassistant/components/vicare/number.py +++ b/homeassistant/components/vicare/number.py @@ -233,30 +233,30 @@ def _build_entities( ) -> list[ViCareNumber]: """Create ViCare number entities for a device.""" - entities: list[ViCareNumber] = [ - ViCareNumber( - device.config, - device.api, - description, - ) - for device in device_list - for description in DEVICE_ENTITY_DESCRIPTIONS - if is_supported(description.key, description, device.api) - ] - - entities.extend( - [ + entities: list[ViCareNumber] = [] + for device in device_list: + # add device entities + entities.extend( ViCareNumber( - device.config, - circuit, description, + device.config, + device.api, + ) + for description in DEVICE_ENTITY_DESCRIPTIONS + if is_supported(description.key, description, device.api) + ) + # add component entities + entities.extend( + ViCareNumber( + description, + device.config, + device.api, + circuit, ) - for device in device_list for circuit in get_circuits(device.api) for description in CIRCUIT_ENTITY_DESCRIPTIONS if is_supported(description.key, description, circuit) - ] - ) + ) return entities @@ -283,12 +283,13 @@ class ViCareNumber(ViCareEntity, NumberEntity): def __init__( self, - device_config: PyViCareDeviceConfig, - api: PyViCareDevice | PyViCareHeatingDeviceComponent, description: ViCareNumberEntityDescription, + device_config: PyViCareDeviceConfig, + device: PyViCareDevice, + component: PyViCareHeatingDeviceComponent | None = None, ) -> None: """Initialize the number.""" - super().__init__(device_config, api, description.key) + super().__init__(description.key, device_config, device, component) self.entity_description = description @property diff --git a/homeassistant/components/vicare/sensor.py b/homeassistant/components/vicare/sensor.py index 5d51abfbbf6..3a16d77249e 100644 --- a/homeassistant/components/vicare/sensor.py +++ b/homeassistant/components/vicare/sensor.py @@ -747,7 +747,6 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( ), ) - CIRCUIT_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( ViCareSensorEntityDescription( key="supply_temperature", @@ -865,61 +864,36 @@ def _build_entities( entities: list[ViCareSensor] = [] for device in device_list: - entities.extend(_build_entities_for_device(device.api, device.config)) + # add device entities entities.extend( - _build_entities_for_component( - get_circuits(device.api), device.config, CIRCUIT_SENSORS + ViCareSensor( + description, + device.config, + device.api, ) + for description in GLOBAL_SENSORS + if is_supported(description.key, description, device.api) ) - entities.extend( - _build_entities_for_component( - get_burners(device.api), device.config, BURNER_SENSORS + # add component entities + for component_list, entity_description_list in ( + (get_circuits(device.api), CIRCUIT_SENSORS), + (get_burners(device.api), BURNER_SENSORS), + (get_compressors(device.api), COMPRESSOR_SENSORS), + ): + entities.extend( + ViCareSensor( + description, + device.config, + device.api, + component, + ) + for component in component_list + for description in entity_description_list + if is_supported(description.key, description, component) ) - ) - entities.extend( - _build_entities_for_component( - get_compressors(device.api), device.config, COMPRESSOR_SENSORS - ) - ) return entities -def _build_entities_for_device( - device: PyViCareDevice, - device_config: PyViCareDeviceConfig, -) -> list[ViCareSensor]: - """Create device specific ViCare sensor entities.""" - - return [ - ViCareSensor( - device_config, - device, - description, - ) - for description in GLOBAL_SENSORS - if is_supported(description.key, description, device) - ] - - -def _build_entities_for_component( - components: list[PyViCareHeatingDeviceComponent], - device_config: PyViCareDeviceConfig, - entity_descriptions: tuple[ViCareSensorEntityDescription, ...], -) -> list[ViCareSensor]: - """Create component specific ViCare sensor entities.""" - - return [ - ViCareSensor( - device_config, - component, - description, - ) - for component in components - for description in entity_descriptions - if is_supported(description.key, description, component) - ] - - async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -943,12 +917,13 @@ class ViCareSensor(ViCareEntity, SensorEntity): def __init__( self, - device_config: PyViCareDeviceConfig, - api: PyViCareDevice | PyViCareHeatingDeviceComponent, description: ViCareSensorEntityDescription, + device_config: PyViCareDeviceConfig, + device: PyViCareDevice, + component: PyViCareHeatingDeviceComponent | None = None, ) -> None: """Initialize the sensor.""" - super().__init__(device_config, api, description.key) + super().__init__(description.key, device_config, device, component) self.entity_description = description # run update to have device_class set depending on unit_of_measurement self.update() diff --git a/homeassistant/components/vicare/water_heater.py b/homeassistant/components/vicare/water_heater.py index c76c6ea81aa..621d2f2a09b 100644 --- a/homeassistant/components/vicare/water_heater.py +++ b/homeassistant/components/vicare/water_heater.py @@ -113,7 +113,7 @@ class ViCareWater(ViCareEntity, WaterHeaterEntity): circuit: PyViCareHeatingCircuit, ) -> None: """Initialize the DHW water_heater device.""" - super().__init__(device_config, device, circuit.id) + super().__init__(circuit.id, device_config, device) self._circuit = circuit self._attributes: dict[str, Any] = {} diff --git a/tests/components/vicare/fixtures/Vitodens300W.json b/tests/components/vicare/fixtures/Vitodens300W.json index 4cf67ebe0f7..bb86bda981b 100644 --- a/tests/components/vicare/fixtures/Vitodens300W.json +++ b/tests/components/vicare/fixtures/Vitodens300W.json @@ -1,5 +1,22 @@ { "data": [ + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "device.serial", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "value": { + "type": "string", + "value": "################" + } + }, + "timestamp": "2024-03-20T01:29:35.549Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/device.serial" + }, { "properties": {}, "commands": {}, diff --git a/tests/components/vicare/snapshots/test_diagnostics.ambr b/tests/components/vicare/snapshots/test_diagnostics.ambr index dfc29d46cc2..430b2de35ad 100644 --- a/tests/components/vicare/snapshots/test_diagnostics.ambr +++ b/tests/components/vicare/snapshots/test_diagnostics.ambr @@ -4,6 +4,24 @@ 'data': list([ dict({ 'data': list([ + dict({ + 'apiVersion': 1, + 'commands': dict({ + }), + 'deviceId': '0', + 'feature': 'device.serial', + 'gatewayId': '################', + 'isEnabled': True, + 'isReady': True, + 'properties': dict({ + 'value': dict({ + 'type': 'string', + 'value': '################', + }), + }), + 'timestamp': '2024-03-20T01:29:35.549Z', + 'uri': 'https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/device.serial', + }), dict({ 'apiVersion': 1, 'commands': dict({ From 1efd267ee67a6f1aaead38da6e8dd2a8caef9a37 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Tue, 3 Sep 2024 15:24:49 +0200 Subject: [PATCH 182/271] Fix energy sensor for ThirdReality Matter powerplug (#125140) --- homeassistant/components/matter/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/matter/sensor.py b/homeassistant/components/matter/sensor.py index c3ab18072f0..5d4ad900d8e 100644 --- a/homeassistant/components/matter/sensor.py +++ b/homeassistant/components/matter/sensor.py @@ -384,7 +384,7 @@ DISCOVERY_SCHEMAS = [ key="ThirdRealityEnergySensorWattAccumulated", device_class=SensorDeviceClass.ENERGY, entity_category=EntityCategory.DIAGNOSTIC, - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, suggested_display_precision=3, state_class=SensorStateClass.TOTAL_INCREASING, measurement_to_ha=lambda x: x / 1000, From 70b811096c26688d312469f594fb74a6bfad496b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 3 Sep 2024 14:37:21 +0200 Subject: [PATCH 183/271] Log deprecation warning when `cv.template` is called from wrong thread (#125141) Log deprecation warning when cv.template is called from wrong thread --- homeassistant/helpers/config_validation.py | 26 ++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 3d3de40a2c6..d88c388f9c7 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -715,8 +715,19 @@ def template(value: Any | None) -> template_helper.Template: raise vol.Invalid("template value is None") if isinstance(value, (list, dict, template_helper.Template)): raise vol.Invalid("template value should be a string") + if not (hass := _async_get_hass_or_none()): + # pylint: disable-next=import-outside-toplevel + from .frame import report - template_value = template_helper.Template(str(value), _async_get_hass_or_none()) + report( + ( + "validates schema outside the event loop, " + "which will stop working in HA Core 2025.10" + ), + error_if_core=False, + ) + + template_value = template_helper.Template(str(value), hass) try: template_value.ensure_valid() @@ -733,8 +744,19 @@ def dynamic_template(value: Any | None) -> template_helper.Template: raise vol.Invalid("template value should be a string") if not template_helper.is_template_string(str(value)): raise vol.Invalid("template value does not contain a dynamic template") + if not (hass := _async_get_hass_or_none()): + # pylint: disable-next=import-outside-toplevel + from .frame import report - template_value = template_helper.Template(str(value), _async_get_hass_or_none()) + report( + ( + "validates schema outside the event loop, " + "which will stop working in HA Core 2025.10" + ), + error_if_core=False, + ) + + template_value = template_helper.Template(str(value), hass) try: template_value.ensure_valid() From 54cf52069e46ac41c728ca5645ad6a01c84124fd Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 3 Sep 2024 21:09:12 +0200 Subject: [PATCH 184/271] Log deprecation warning when `template.Template` is created without `hass` (#125142) * Log deprecation warning when template.Template is created without hass * Improve docstring --- homeassistant/helpers/template.py | 18 +++++++++++++++++- tests/helpers/test_template.py | 17 +++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index e090e0de2d1..12a005cc7d6 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -495,10 +495,26 @@ class Template: ) def __init__(self, template: str, hass: HomeAssistant | None = None) -> None: - """Instantiate a template.""" + """Instantiate a template. + + Note: A valid hass instance should always be passed in. The hass parameter + will be non optional in Home Assistant Core 2025.10. + """ + # pylint: disable-next=import-outside-toplevel + from .frame import report + if not isinstance(template, str): raise TypeError("Expected template to be a string") + if not hass: + report( + ( + "creates a template object without passing hass, " + "which will stop working in HA Core 2025.10" + ), + error_if_core=False, + ) + self.template: str = template.strip() self._compiled_code: CodeType | None = None self._compiled: jinja2.Template | None = None diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 0676ae21ab7..370e752e950 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -6281,3 +6281,20 @@ def test_unzip(hass: HomeAssistant, col, expected) -> None: ).async_render({"col": col}) == expected ) + + +def test_warn_no_hass(hass: HomeAssistant, caplog: pytest.LogCaptureFixture) -> None: + """Test deprecation warning when instantiating Template without hass.""" + + message = "Detected code that creates a template object without passing hass" + template.Template("blah") + assert message in caplog.text + caplog.clear() + + template.Template("blah", None) + assert message in caplog.text + caplog.clear() + + template.Template("blah", hass) + assert message not in caplog.text + caplog.clear() From 4e1a77326ed13e454708aea20475dab3633e757f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 3 Sep 2024 15:25:35 +0200 Subject: [PATCH 185/271] Restore unnecessary assignment of Template.hass in event helper (#125143) --- homeassistant/helpers/event.py | 16 ++++++++++++++ tests/helpers/test_event.py | 40 ++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 38f461d8d7a..97a85fdde89 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -981,6 +981,22 @@ class TrackTemplateResultInfo: self._last_result: dict[Template, bool | str | TemplateError] = {} + for track_template_ in track_templates: + if track_template_.template.hass: + continue + + # pylint: disable-next=import-outside-toplevel + from .frame import report + + report( + ( + "calls async_track_template_result with template without hass, " + "which will stop working in HA Core 2025.10" + ), + error_if_core=False, + ) + track_template_.template.hass = hass + self._rate_limit = KeyedRateLimit(hass) self._info: dict[Template, RenderInfo] = {} self._track_state_changes: _TrackStateChangeFiltered | None = None diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 6c71f1d8a7c..19f1ef5bb76 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -4938,3 +4938,43 @@ async def test_async_track_state_report_event(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(tracker_called) == 2 unsub() + + +async def test_async_track_template_no_hass_deprecated( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test async_track_template with a template without hass is deprecated.""" + message = ( + "Detected code that calls async_track_template_result with template without " + "hass, which will stop working in HA Core 2025.10. Please report this issue." + ) + + async_track_template(hass, Template("blah"), lambda x, y, z: None) + assert message in caplog.text + caplog.clear() + + async_track_template(hass, Template("blah", hass), lambda x, y, z: None) + assert message not in caplog.text + caplog.clear() + + +async def test_async_track_template_result_no_hass_deprecated( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test async_track_template_result with a template without hass is deprecated.""" + message = ( + "Detected code that calls async_track_template_result with template without " + "hass, which will stop working in HA Core 2025.10. Please report this issue." + ) + + async_track_template_result( + hass, [TrackTemplate(Template("blah"), None)], lambda x, y, z: None + ) + assert message in caplog.text + caplog.clear() + + async_track_template_result( + hass, [TrackTemplate(Template("blah", hass), None)], lambda x, y, z: None + ) + assert message not in caplog.text + caplog.clear() From 82cffcbc23b1542d883d4b22a0978f6df1ce202a Mon Sep 17 00:00:00 2001 From: Andrew Jackson Date: Tue, 3 Sep 2024 16:09:26 +0100 Subject: [PATCH 186/271] Bump aiomealie to 0.9.2 (#125153) Bump mealie version --- homeassistant/components/mealie/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mealie/manifest.json b/homeassistant/components/mealie/manifest.json index d8fe26d97b3..4fabdffadc4 100644 --- a/homeassistant/components/mealie/manifest.json +++ b/homeassistant/components/mealie/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/mealie", "integration_type": "service", "iot_class": "local_polling", - "requirements": ["aiomealie==0.9.1"] + "requirements": ["aiomealie==0.9.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 993dbbe2c9a..58c356c4a88 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -288,7 +288,7 @@ aiolookin==1.0.0 aiolyric==2.0.1 # homeassistant.components.mealie -aiomealie==0.9.1 +aiomealie==0.9.2 # homeassistant.components.modern_forms aiomodernforms==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b04a0f20ceb..3796fa22213 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -270,7 +270,7 @@ aiolookin==1.0.0 aiolyric==2.0.1 # homeassistant.components.mealie -aiomealie==0.9.1 +aiomealie==0.9.2 # homeassistant.components.modern_forms aiomodernforms==0.1.8 From 74fd16b953c50ef34f95ccf7735a524927c2b588 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 3 Sep 2024 19:52:38 +0200 Subject: [PATCH 187/271] Update frontend to 20240903.1 (#125160) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 50bcb3b3d97..7b904cba999 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20240902.0"] + "requirements": ["home-assistant-frontend==20240903.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1729e6e8131..ddb96da6bff 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -31,7 +31,7 @@ habluetooth==3.4.0 hass-nabucasa==0.81.1 hassil==1.7.4 home-assistant-bluetooth==1.12.2 -home-assistant-frontend==20240902.0 +home-assistant-frontend==20240903.1 home-assistant-intents==2024.8.29 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 58c356c4a88..98b3d9ff696 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1102,7 +1102,7 @@ hole==0.8.0 holidays==0.55 # homeassistant.components.frontend -home-assistant-frontend==20240902.0 +home-assistant-frontend==20240903.1 # homeassistant.components.conversation home-assistant-intents==2024.8.29 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3796fa22213..8db0fd8a7fa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -925,7 +925,7 @@ hole==0.8.0 holidays==0.55 # homeassistant.components.frontend -home-assistant-frontend==20240902.0 +home-assistant-frontend==20240903.1 # homeassistant.components.conversation home-assistant-intents==2024.8.29 From 6082220f7f886039a27749b3c0c97e10dab4fd2d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Sep 2024 07:53:10 -1000 Subject: [PATCH 188/271] Bump yalexs to 8.6.2 (#125162) changelog: https://github.com/bdraco/yalexs/compare/v8.6.0...v8.6.2 --- homeassistant/components/august/manifest.json | 2 +- homeassistant/components/yale/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index a40c6920136..42f97e56fd2 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -24,5 +24,5 @@ "documentation": "https://www.home-assistant.io/integrations/august", "iot_class": "cloud_push", "loggers": ["pubnub", "yalexs"], - "requirements": ["yalexs==8.6.0", "yalexs-ble==2.4.3"] + "requirements": ["yalexs==8.6.2", "yalexs-ble==2.4.3"] } diff --git a/homeassistant/components/yale/manifest.json b/homeassistant/components/yale/manifest.json index 030df50a482..0942dcb5dcb 100644 --- a/homeassistant/components/yale/manifest.json +++ b/homeassistant/components/yale/manifest.json @@ -13,5 +13,5 @@ "documentation": "https://www.home-assistant.io/integrations/yale", "iot_class": "cloud_push", "loggers": ["socketio", "engineio", "yalexs"], - "requirements": ["yalexs==8.6.0", "yalexs-ble==2.4.3"] + "requirements": ["yalexs==8.6.2", "yalexs-ble==2.4.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 98b3d9ff696..ecad196b4d2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2976,7 +2976,7 @@ yalexs-ble==2.4.3 # homeassistant.components.august # homeassistant.components.yale -yalexs==8.6.0 +yalexs==8.6.2 # homeassistant.components.yeelight yeelight==0.7.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8db0fd8a7fa..63b0bdba07b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2356,7 +2356,7 @@ yalexs-ble==2.4.3 # homeassistant.components.august # homeassistant.components.yale -yalexs==8.6.0 +yalexs==8.6.2 # homeassistant.components.yeelight yeelight==0.7.14 From 116090bff177ff8e168a185efc1d05a6a5134dbc Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 3 Sep 2024 21:12:20 +0200 Subject: [PATCH 189/271] Bump version to 2024.9.0b4 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5789c9becb8..f25a7a98ef5 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "0b3" +PATCH_VERSION: Final = "0b4" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 72f64391411..c19df06fed0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.9.0b3" +version = "2024.9.0b4" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 8293f270df62aa2535b0ead77370155e5093f240 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Tue, 3 Sep 2024 21:45:43 +0200 Subject: [PATCH 190/271] Update gardena_bluetooth dependency to 1.4.3 (#125175) --- homeassistant/components/gardena_bluetooth/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/gardena_bluetooth/manifest.json b/homeassistant/components/gardena_bluetooth/manifest.json index 4812def7dde..6d7566b3edf 100644 --- a/homeassistant/components/gardena_bluetooth/manifest.json +++ b/homeassistant/components/gardena_bluetooth/manifest.json @@ -14,5 +14,5 @@ "documentation": "https://www.home-assistant.io/integrations/gardena_bluetooth", "iot_class": "local_polling", "loggers": ["bleak", "bleak_esphome", "gardena_bluetooth"], - "requirements": ["gardena-bluetooth==1.4.2"] + "requirements": ["gardena-bluetooth==1.4.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index ecad196b4d2..e4fdad50a3f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -930,7 +930,7 @@ fyta_cli==0.6.6 gTTS==2.2.4 # homeassistant.components.gardena_bluetooth -gardena-bluetooth==1.4.2 +gardena-bluetooth==1.4.3 # homeassistant.components.google_assistant_sdk gassist-text==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 63b0bdba07b..f90fe997895 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -783,7 +783,7 @@ fyta_cli==0.6.6 gTTS==2.2.4 # homeassistant.components.gardena_bluetooth -gardena-bluetooth==1.4.2 +gardena-bluetooth==1.4.3 # homeassistant.components.google_assistant_sdk gassist-text==0.0.11 From a0d97644437668b17212f9fa412de4dcd56ae2a6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Sep 2024 10:18:19 -1000 Subject: [PATCH 191/271] Bump yalexs to 8.6.3 (#125176) Fixes the battery state not refreshing due to a refactoring error in the library. changelog: https://github.com/bdraco/yalexs/compare/v8.6.2...v8.6.3 --- homeassistant/components/august/manifest.json | 2 +- homeassistant/components/yale/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index 42f97e56fd2..6635a95f1cf 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -24,5 +24,5 @@ "documentation": "https://www.home-assistant.io/integrations/august", "iot_class": "cloud_push", "loggers": ["pubnub", "yalexs"], - "requirements": ["yalexs==8.6.2", "yalexs-ble==2.4.3"] + "requirements": ["yalexs==8.6.3", "yalexs-ble==2.4.3"] } diff --git a/homeassistant/components/yale/manifest.json b/homeassistant/components/yale/manifest.json index 0942dcb5dcb..fc93d259891 100644 --- a/homeassistant/components/yale/manifest.json +++ b/homeassistant/components/yale/manifest.json @@ -13,5 +13,5 @@ "documentation": "https://www.home-assistant.io/integrations/yale", "iot_class": "cloud_push", "loggers": ["socketio", "engineio", "yalexs"], - "requirements": ["yalexs==8.6.2", "yalexs-ble==2.4.3"] + "requirements": ["yalexs==8.6.3", "yalexs-ble==2.4.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index e4fdad50a3f..9f2e158f545 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2976,7 +2976,7 @@ yalexs-ble==2.4.3 # homeassistant.components.august # homeassistant.components.yale -yalexs==8.6.2 +yalexs==8.6.3 # homeassistant.components.yeelight yeelight==0.7.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f90fe997895..1287952a767 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2356,7 +2356,7 @@ yalexs-ble==2.4.3 # homeassistant.components.august # homeassistant.components.yale -yalexs==8.6.2 +yalexs==8.6.3 # homeassistant.components.yeelight yeelight==0.7.14 From 65e98eab9ce7d2bc4770849a4af7700343c20115 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 3 Sep 2024 22:43:03 +0200 Subject: [PATCH 192/271] Bump python-holidays to 0.56 (#125182) --- homeassistant/components/holiday/manifest.json | 2 +- homeassistant/components/workday/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/holiday/manifest.json b/homeassistant/components/holiday/manifest.json index 0a3064450d4..0a2d98e71c5 100644 --- a/homeassistant/components/holiday/manifest.json +++ b/homeassistant/components/holiday/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/holiday", "iot_class": "local_polling", - "requirements": ["holidays==0.55", "babel==2.15.0"] + "requirements": ["holidays==0.56", "babel==2.15.0"] } diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index fafa870d00a..297b20b8c0e 100644 --- a/homeassistant/components/workday/manifest.json +++ b/homeassistant/components/workday/manifest.json @@ -7,5 +7,5 @@ "iot_class": "local_polling", "loggers": ["holidays"], "quality_scale": "internal", - "requirements": ["holidays==0.55"] + "requirements": ["holidays==0.56"] } diff --git a/requirements_all.txt b/requirements_all.txt index 9f2e158f545..fa304d3d97c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1099,7 +1099,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.55 +holidays==0.56 # homeassistant.components.frontend home-assistant-frontend==20240903.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1287952a767..fcabd4fe736 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -922,7 +922,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.55 +holidays==0.56 # homeassistant.components.frontend home-assistant-frontend==20240903.1 From d0629d4e66cf8806eddce6ec8d1c776b60f9ea69 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Wed, 4 Sep 2024 11:00:02 +0200 Subject: [PATCH 193/271] Update knx-frontend to 2024.9.4.64538 (#125196) --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index b7efd14fa2a..181dca6f4b8 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -13,7 +13,7 @@ "requirements": [ "xknx==3.1.1", "xknxproject==3.7.1", - "knx-frontend==2024.8.9.225351" + "knx-frontend==2024.9.4.64538" ], "single_config_entry": true } diff --git a/requirements_all.txt b/requirements_all.txt index fa304d3d97c..f6d74270701 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1225,7 +1225,7 @@ kiwiki-client==0.1.1 knocki==0.3.1 # homeassistant.components.knx -knx-frontend==2024.8.9.225351 +knx-frontend==2024.9.4.64538 # homeassistant.components.konnected konnected==1.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fcabd4fe736..6fba8e8785b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1021,7 +1021,7 @@ kegtron-ble==0.4.0 knocki==0.3.1 # homeassistant.components.knx -knx-frontend==2024.8.9.225351 +knx-frontend==2024.9.4.64538 # homeassistant.components.konnected konnected==1.2.0 From 9ef0a1f0a279d9fd8011b115aa33defbc4aefb22 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 4 Sep 2024 11:35:14 +0200 Subject: [PATCH 194/271] Update frontend to 20240904.0 (#125206) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 7b904cba999..fbdafe6025d 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20240903.1"] + "requirements": ["home-assistant-frontend==20240904.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index ddb96da6bff..73f3452b259 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -31,7 +31,7 @@ habluetooth==3.4.0 hass-nabucasa==0.81.1 hassil==1.7.4 home-assistant-bluetooth==1.12.2 -home-assistant-frontend==20240903.1 +home-assistant-frontend==20240904.0 home-assistant-intents==2024.8.29 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index f6d74270701..f74d11bd5ad 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1102,7 +1102,7 @@ hole==0.8.0 holidays==0.56 # homeassistant.components.frontend -home-assistant-frontend==20240903.1 +home-assistant-frontend==20240904.0 # homeassistant.components.conversation home-assistant-intents==2024.8.29 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6fba8e8785b..f32455183bb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -925,7 +925,7 @@ hole==0.8.0 holidays==0.56 # homeassistant.components.frontend -home-assistant-frontend==20240903.1 +home-assistant-frontend==20240904.0 # homeassistant.components.conversation home-assistant-intents==2024.8.29 From bcdc3563a58ae47ccfbc72a44a0cf1932bf3e875 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 4 Sep 2024 11:46:41 +0200 Subject: [PATCH 195/271] Bump deebot-client to 8.4.0 (#125207) --- homeassistant/components/ecovacs/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ecovacs/manifest.json b/homeassistant/components/ecovacs/manifest.json index 560ee4d599c..33977b3b0de 100644 --- a/homeassistant/components/ecovacs/manifest.json +++ b/homeassistant/components/ecovacs/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/ecovacs", "iot_class": "cloud_push", "loggers": ["sleekxmppfs", "sucks", "deebot_client"], - "requirements": ["py-sucks==0.9.10", "deebot-client==8.3.0"] + "requirements": ["py-sucks==0.9.10", "deebot-client==8.4.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index f74d11bd5ad..0075ed4a4e1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -715,7 +715,7 @@ debugpy==1.8.1 # decora==0.6 # homeassistant.components.ecovacs -deebot-client==8.3.0 +deebot-client==8.4.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f32455183bb..6205260a9a4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -605,7 +605,7 @@ dbus-fast==2.24.0 debugpy==1.8.1 # homeassistant.components.ecovacs -deebot-client==8.3.0 +deebot-client==8.4.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns From de99dfef4e794c323764fa92b5efecaa618b552b Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 4 Sep 2024 11:48:24 +0200 Subject: [PATCH 196/271] Bump version to 2024.9.0b5 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index f25a7a98ef5..54d76829e4d 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "0b4" +PATCH_VERSION: Final = "0b5" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index c19df06fed0..2bb167622a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.9.0b4" +version = "2024.9.0b5" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 122f11c7901116eb212f5bebc14ff718467a7a11 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 4 Sep 2024 15:05:51 +0200 Subject: [PATCH 197/271] Update modified_at datetime on storage collection changes (#125218) --- homeassistant/helpers/collection.py | 37 +++++++++++-- tests/helpers/test_collection.py | 81 +++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/collection.py b/homeassistant/helpers/collection.py index 9151a9dfc6b..86d3450c3a0 100644 --- a/homeassistant/helpers/collection.py +++ b/homeassistant/helpers/collection.py @@ -7,6 +7,7 @@ import asyncio from collections.abc import Awaitable, Callable, Coroutine, Iterable from dataclasses import dataclass from functools import partial +from hashlib import md5 from itertools import groupby import logging from operator import attrgetter @@ -25,6 +26,7 @@ from homeassistant.util import slugify from . import entity_registry from .entity import Entity from .entity_component import EntityComponent +from .json import json_bytes from .storage import Store from .typing import ConfigType, VolDictType @@ -50,6 +52,7 @@ class CollectionChange: change_type: str item_id: str item: Any + item_hash: str | None = None type ChangeListener = Callable[ @@ -273,7 +276,9 @@ class StorageCollection[_ItemT, _StoreT: SerializedStorageCollection]( await self.notify_changes( [ - CollectionChange(CHANGE_ADDED, item[CONF_ID], item) + CollectionChange( + CHANGE_ADDED, item[CONF_ID], item, self._hash_item(item) + ) for item in raw_storage["items"] ] ) @@ -313,7 +318,16 @@ class StorageCollection[_ItemT, _StoreT: SerializedStorageCollection]( item = self._create_item(item_id, validated_data) self.data[item_id] = item self._async_schedule_save() - await self.notify_changes([CollectionChange(CHANGE_ADDED, item_id, item)]) + await self.notify_changes( + [ + CollectionChange( + CHANGE_ADDED, + item_id, + item, + self._hash_item(self._serialize_item(item_id, item)), + ) + ] + ) return item async def async_update_item(self, item_id: str, updates: dict) -> _ItemT: @@ -331,7 +345,16 @@ class StorageCollection[_ItemT, _StoreT: SerializedStorageCollection]( self.data[item_id] = updated self._async_schedule_save() - await self.notify_changes([CollectionChange(CHANGE_UPDATED, item_id, updated)]) + await self.notify_changes( + [ + CollectionChange( + CHANGE_UPDATED, + item_id, + updated, + self._hash_item(self._serialize_item(item_id, updated)), + ) + ] + ) return self.data[item_id] @@ -365,6 +388,10 @@ class StorageCollection[_ItemT, _StoreT: SerializedStorageCollection]( def _data_to_save(self) -> _StoreT: """Return JSON-compatible date for storing to file.""" + def _hash_item(self, item: dict) -> str: + """Return a hash of the item.""" + return md5(json_bytes(item)).hexdigest() + class DictStorageCollection(StorageCollection[dict, SerializedStorageCollection]): """A specialized StorageCollection where the items are untyped dicts.""" @@ -464,6 +491,10 @@ class _CollectionLifeCycle(Generic[_EntityT]): async def _update_entity(self, change_set: CollectionChange) -> None: if entity := self.entities.get(change_set.item_id): + if change_set.item_hash: + self.ent_reg.async_update_entity_options( + entity.entity_id, "collection", {"hash": change_set.item_hash} + ) await entity.async_update_config(change_set.item) async def _collection_changed(self, change_set: Iterable[CollectionChange]) -> None: diff --git a/tests/helpers/test_collection.py b/tests/helpers/test_collection.py index f0287218d7f..f564f85ec3b 100644 --- a/tests/helpers/test_collection.py +++ b/tests/helpers/test_collection.py @@ -2,8 +2,10 @@ from __future__ import annotations +from datetime import timedelta import logging +from freezegun.api import FrozenDateTimeFactory import pytest import voluptuous as vol @@ -15,6 +17,7 @@ from homeassistant.helpers import ( storage, ) from homeassistant.helpers.typing import ConfigType +from homeassistant.util.dt import utcnow from tests.common import flush_store from tests.typing import WebSocketGenerator @@ -254,6 +257,84 @@ async def test_storage_collection(hass: HomeAssistant) -> None: } +async def test_storage_collection_update_modifiet_at( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test that updating a storage collection will update the modified_at datetime in the entity registry.""" + + entities: dict[str, TestEntity] = {} + + class TestEntity(MockEntity): + """Entity that is config based.""" + + def __init__(self, config: ConfigType) -> None: + """Initialize entity.""" + super().__init__(config) + self._state = "initial" + + @classmethod + def from_storage(cls, config: ConfigType) -> TestEntity: + """Create instance from storage.""" + obj = super().from_storage(config) + entities[obj.unique_id] = obj + return obj + + @property + def state(self) -> str: + """Return state of entity.""" + return self._state + + def set_state(self, value: str) -> None: + """Set value.""" + self._state = value + self.async_write_ha_state() + + store = storage.Store(hass, 1, "test-data") + data = {"id": "mock-1", "name": "Mock 1", "data": 1} + await store.async_save( + { + "items": [ + data, + ] + } + ) + id_manager = collection.IDManager() + ent_comp = entity_component.EntityComponent(_LOGGER, "test", hass) + await ent_comp.async_setup({}) + coll = MockStorageCollection(store, id_manager) + collection.sync_entity_lifecycle(hass, "test", "test", ent_comp, coll, TestEntity) + changes = track_changes(coll) + + await coll.async_load() + assert id_manager.has_id("mock-1") + assert len(changes) == 1 + assert changes[0] == (collection.CHANGE_ADDED, "mock-1", data) + + modified_1 = entity_registry.async_get("test.mock_1").modified_at + assert modified_1 == utcnow() + + freezer.tick(timedelta(minutes=1)) + + updated_item = await coll.async_update_item("mock-1", {"data": 2}) + assert id_manager.has_id("mock-1") + assert updated_item == {"id": "mock-1", "name": "Mock 1", "data": 2} + assert len(changes) == 2 + assert changes[1] == (collection.CHANGE_UPDATED, "mock-1", updated_item) + + modified_2 = entity_registry.async_get("test.mock_1").modified_at + assert modified_2 > modified_1 + assert modified_2 == utcnow() + + freezer.tick(timedelta(minutes=1)) + + entities["mock-1"].set_state("second") + + modified_3 = entity_registry.async_get("test.mock_1").modified_at + assert modified_3 == modified_2 + + async def test_attach_entity_component_collection(hass: HomeAssistant) -> None: """Test attaching collection to entity component.""" ent_comp = entity_component.EntityComponent(_LOGGER, "test", hass) From 438af042edabdd88cf3788f28d91410fb6910f55 Mon Sep 17 00:00:00 2001 From: Denis Shulyaka Date: Wed, 4 Sep 2024 16:30:28 +0300 Subject: [PATCH 198/271] Update Anthropic default model to Haiku (#125225) --- homeassistant/components/anthropic/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/anthropic/const.py b/homeassistant/components/anthropic/const.py index 4ccf2c88faa..0dbf9c51ac1 100644 --- a/homeassistant/components/anthropic/const.py +++ b/homeassistant/components/anthropic/const.py @@ -8,7 +8,7 @@ LOGGER = logging.getLogger(__package__) CONF_RECOMMENDED = "recommended" CONF_PROMPT = "prompt" CONF_CHAT_MODEL = "chat_model" -RECOMMENDED_CHAT_MODEL = "claude-3-5-sonnet-20240620" +RECOMMENDED_CHAT_MODEL = "claude-3-haiku-20240307" CONF_MAX_TOKENS = "max_tokens" RECOMMENDED_MAX_TOKENS = 1024 CONF_TEMPERATURE = "temperature" From ac19ee3e2e9cc8fbacfa8755dfe5c12c6ff936bb Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 4 Sep 2024 09:36:25 -0500 Subject: [PATCH 199/271] Bump intents to 2024.9.4 (#125232) --- homeassistant/components/conversation/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/hassfest/docker/Dockerfile | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index 5a689485b29..837ac9f9b1f 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/conversation", "integration_type": "system", "quality_scale": "internal", - "requirements": ["hassil==1.7.4", "home-assistant-intents==2024.8.29"] + "requirements": ["hassil==1.7.4", "home-assistant-intents==2024.9.4"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 73f3452b259..fd878c1ffcf 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ hass-nabucasa==0.81.1 hassil==1.7.4 home-assistant-bluetooth==1.12.2 home-assistant-frontend==20240904.0 -home-assistant-intents==2024.8.29 +home-assistant-intents==2024.9.4 httpx==0.27.0 ifaddr==0.2.0 Jinja2==3.1.4 diff --git a/requirements_all.txt b/requirements_all.txt index 0075ed4a4e1..59e9f95e93e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1105,7 +1105,7 @@ holidays==0.56 home-assistant-frontend==20240904.0 # homeassistant.components.conversation -home-assistant-intents==2024.8.29 +home-assistant-intents==2024.9.4 # homeassistant.components.home_connect homeconnect==0.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6205260a9a4..ace1c743fe0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -928,7 +928,7 @@ holidays==0.56 home-assistant-frontend==20240904.0 # homeassistant.components.conversation -home-assistant-intents==2024.8.29 +home-assistant-intents==2024.9.4 # homeassistant.components.home_connect homeconnect==0.8.0 diff --git a/script/hassfest/docker/Dockerfile b/script/hassfest/docker/Dockerfile index 4fc60c0c621..fc96653604e 100644 --- a/script/hassfest/docker/Dockerfile +++ b/script/hassfest/docker/Dockerfile @@ -26,7 +26,7 @@ RUN \ -c homeassistant/package_constraints.txt \ -r requirements.txt \ stdlib-list==0.10.0 pipdeptree==2.23.1 tqdm==4.66.4 ruff==0.6.2 \ - PyTurboJPEG==1.7.5 ha-ffmpeg==3.2.0 hassil==1.7.4 home-assistant-intents==2024.8.29 mutagen==1.47.0 + PyTurboJPEG==1.7.5 ha-ffmpeg==3.2.0 hassil==1.7.4 home-assistant-intents==2024.9.4 mutagen==1.47.0 LABEL "name"="hassfest" LABEL "maintainer"="Home Assistant " From 84a0a28be2b5318a1111df896d2ca923f0f826b1 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 4 Sep 2024 17:08:18 +0200 Subject: [PATCH 200/271] Bump version to 2024.9.0 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 54d76829e4d..5c61650ec32 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "0b5" +PATCH_VERSION: Final = "0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 2bb167622a2..9a935b3a5fe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.9.0b5" +version = "2024.9.0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From a14826d75e4f6aac7579710c7bcce721a28f5807 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Wed, 4 Sep 2024 18:34:11 +0300 Subject: [PATCH 201/271] Fix BTHome validate triggers for device with multiple buttons (#125183) * Fix BTHome validate triggers for device with multiple buttons * Remove None default --- .../components/bthome/device_trigger.py | 56 +++++--- .../components/bthome/test_device_trigger.py | 124 +++++++++++++++++- 2 files changed, 158 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/bthome/device_trigger.py b/homeassistant/components/bthome/device_trigger.py index c49664b1146..c50ffc05900 100644 --- a/homeassistant/components/bthome/device_trigger.py +++ b/homeassistant/components/bthome/device_trigger.py @@ -7,6 +7,9 @@ from typing import Any import voluptuous as vol from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA +from homeassistant.components.device_automation.exceptions import ( + InvalidDeviceAutomationConfig, +) from homeassistant.components.homeassistant.triggers import event as event_trigger from homeassistant.const import ( CONF_DEVICE_ID, @@ -43,33 +46,46 @@ TRIGGERS_BY_EVENT_CLASS = { EVENT_CLASS_DIMMER: {"rotate_left", "rotate_right"}, } -SCHEMA_BY_EVENT_CLASS = { - EVENT_CLASS_BUTTON: DEVICE_TRIGGER_BASE_SCHEMA.extend( - { - vol.Required(CONF_TYPE): vol.In([EVENT_CLASS_BUTTON]), - vol.Required(CONF_SUBTYPE): vol.In( - TRIGGERS_BY_EVENT_CLASS[EVENT_CLASS_BUTTON] - ), - } - ), - EVENT_CLASS_DIMMER: DEVICE_TRIGGER_BASE_SCHEMA.extend( - { - vol.Required(CONF_TYPE): vol.In([EVENT_CLASS_DIMMER]), - vol.Required(CONF_SUBTYPE): vol.In( - TRIGGERS_BY_EVENT_CLASS[EVENT_CLASS_DIMMER] - ), - } - ), -} +TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( + {vol.Required(CONF_TYPE): str, vol.Required(CONF_SUBTYPE): str} +) async def async_validate_trigger_config( hass: HomeAssistant, config: ConfigType ) -> ConfigType: """Validate trigger config.""" - return SCHEMA_BY_EVENT_CLASS.get(config[CONF_TYPE], DEVICE_TRIGGER_BASE_SCHEMA)( # type: ignore[no-any-return] - config + config = TRIGGER_SCHEMA(config) + event_class = config[CONF_TYPE] + event_type = config[CONF_SUBTYPE] + + device_registry = dr.async_get(hass) + device = device_registry.async_get(config[CONF_DEVICE_ID]) + assert device is not None + config_entries = [ + hass.config_entries.async_get_entry(entry_id) + for entry_id in device.config_entries + ] + bthome_config_entry = next( + iter(entry for entry in config_entries if entry and entry.domain == DOMAIN) ) + event_classes: list[str] = bthome_config_entry.data.get( + CONF_DISCOVERED_EVENT_CLASSES, [] + ) + + if event_class not in event_classes: + raise InvalidDeviceAutomationConfig( + f"BTHome trigger {event_class} is not valid for device " + f"{device} ({config[CONF_DEVICE_ID]})" + ) + + if event_type not in TRIGGERS_BY_EVENT_CLASS.get(event_class.split("_")[0], ()): + raise InvalidDeviceAutomationConfig( + f"BTHome trigger {event_type} is not valid for device " + f"{device} ({config[CONF_DEVICE_ID]})" + ) + + return config async def async_get_triggers( diff --git a/tests/components/bthome/test_device_trigger.py b/tests/components/bthome/test_device_trigger.py index 459654826f9..c4c900ef6e1 100644 --- a/tests/components/bthome/test_device_trigger.py +++ b/tests/components/bthome/test_device_trigger.py @@ -1,10 +1,19 @@ """Test BTHome BLE events.""" +import pytest + from homeassistant.components import automation from homeassistant.components.bluetooth import DOMAIN as BLUETOOTH_DOMAIN from homeassistant.components.bthome.const import CONF_SUBTYPE, DOMAIN from homeassistant.components.device_automation import DeviceAutomationType -from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE +from homeassistant.const import ( + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_PLATFORM, + CONF_TYPE, + STATE_ON, + STATE_UNAVAILABLE, +) from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component @@ -121,6 +130,117 @@ async def test_get_triggers_button( await hass.async_block_till_done() +async def test_get_triggers_multiple_buttons( + hass: HomeAssistant, device_registry: dr.DeviceRegistry +) -> None: + """Test that we get the expected triggers for multiple buttons device.""" + mac = "A4:C1:38:8D:18:B2" + entry = await _async_setup_bthome_device(hass, mac) + events = async_capture_events(hass, "bthome_ble_event") + + # Emit button_1 long press and button_2 press events + # so it creates the device in the registry + inject_bluetooth_service_info_bleak( + hass, + make_bthome_v2_adv(mac, b"\x40\x3a\x04\x3a\x01"), + ) + + # wait for the event + await hass.async_block_till_done() + assert len(events) == 2 + + device = device_registry.async_get_device(identifiers={get_device_id(mac)}) + assert device + expected_trigger1 = { + CONF_PLATFORM: "device", + CONF_DOMAIN: DOMAIN, + CONF_DEVICE_ID: device.id, + CONF_TYPE: "button_1", + CONF_SUBTYPE: "long_press", + "metadata": {}, + } + expected_trigger2 = { + CONF_PLATFORM: "device", + CONF_DOMAIN: DOMAIN, + CONF_DEVICE_ID: device.id, + CONF_TYPE: "button_2", + CONF_SUBTYPE: "press", + "metadata": {}, + } + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device.id + ) + assert expected_trigger1 in triggers + assert expected_trigger2 in triggers + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +@pytest.mark.parametrize( + ("event_class", "event_type", "expected"), + [ + ("button_1", "long_press", STATE_ON), + ("button_2", "press", STATE_ON), + ("button_3", "long_press", STATE_UNAVAILABLE), + ("button", "long_press", STATE_UNAVAILABLE), + ("button_1", "invalid_press", STATE_UNAVAILABLE), + ], +) +async def test_validate_trigger_config( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + event_class: str, + event_type: str, + expected: str, +) -> None: + """Test unsupported trigger does not return a trigger config.""" + mac = "A4:C1:38:8D:18:B2" + entry = await _async_setup_bthome_device(hass, mac) + + # Emit button_1 long press and button_2 press events + # so it creates the device in the registry + inject_bluetooth_service_info_bleak( + hass, + make_bthome_v2_adv(mac, b"\x40\x3a\x04\x3a\x01"), + ) + + # wait for the event + await hass.async_block_till_done() + + device = device_registry.async_get_device(identifiers={get_device_id(mac)}) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + CONF_PLATFORM: "device", + CONF_DOMAIN: DOMAIN, + CONF_DEVICE_ID: device.id, + CONF_TYPE: event_class, + CONF_SUBTYPE: event_type, + }, + "action": { + "service": "test.automation", + "data_template": {"some": "test_trigger_button_long_press"}, + }, + }, + ] + }, + ) + await hass.async_block_till_done() + + automations = hass.states.async_entity_ids(automation.DOMAIN) + assert len(automations) == 1 + assert hass.states.get(automations[0]).state == expected + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + async def test_get_triggers_dimmer( hass: HomeAssistant, device_registry: dr.DeviceRegistry ) -> None: @@ -235,7 +355,7 @@ async def test_if_fires_on_motion_detected( make_bthome_v2_adv(mac, b"\x40\x3a\x03"), ) - # # wait for the event + # wait for the event await hass.async_block_till_done() device = device_registry.async_get_device(identifiers={get_device_id(mac)}) From 3c13f4b4ccf64104e97b2d25ec395efb1947a205 Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Fri, 6 Sep 2024 15:02:38 +0200 Subject: [PATCH 202/271] Improve play media support in LinkPlay (#125205) Improve play media support in linkplay --- homeassistant/components/linkplay/media_player.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/linkplay/media_player.py b/homeassistant/components/linkplay/media_player.py index 0b62b4dbcee..9eed51241cb 100644 --- a/homeassistant/components/linkplay/media_player.py +++ b/homeassistant/components/linkplay/media_player.py @@ -20,6 +20,9 @@ from homeassistant.components.media_player import ( MediaType, RepeatMode, ) +from homeassistant.components.media_player.browse_media import ( + async_process_play_media_url, +) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr @@ -233,10 +236,14 @@ class LinkPlayMediaPlayerEntity(MediaPlayerEntity): self, media_type: MediaType | str, media_id: str, **kwargs: Any ) -> None: """Play a piece of media.""" - media = await media_source.async_resolve_media( - self.hass, media_id, self.entity_id - ) - await self._bridge.player.play(media.url) + if media_source.is_media_source_id(media_id): + play_item = await media_source.async_resolve_media( + self.hass, media_id, self.entity_id + ) + media_id = play_item.url + + url = async_process_play_media_url(self.hass, media_id) + await self._bridge.player.play(url) def _update_properties(self) -> None: """Update the properties of the media player.""" From 48fcf58eb9e2e0a596d941a904b95a0b2acf198d Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 6 Sep 2024 15:42:56 +0200 Subject: [PATCH 203/271] Revert #122676 Yamaha discovery (#125216) Revert Yamaha discovery --- .../components/yamaha/media_player.py | 30 +------------------ 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/homeassistant/components/yamaha/media_player.py b/homeassistant/components/yamaha/media_player.py index 58f501b99be..bccb7b437f8 100644 --- a/homeassistant/components/yamaha/media_player.py +++ b/homeassistant/components/yamaha/media_player.py @@ -2,7 +2,6 @@ from __future__ import annotations -import contextlib import logging from typing import Any @@ -130,34 +129,7 @@ def _discovery(config_info): zones.extend(recv.zone_controllers()) else: _LOGGER.debug("Config Zones") - zones = None - - # Fix for upstream issues in rxv.find() with some hardware. - with contextlib.suppress(AttributeError, ValueError): - for recv in rxv.find(DISCOVER_TIMEOUT): - _LOGGER.debug( - "Found Serial %s %s %s", - recv.serial_number, - recv.ctrl_url, - recv.zone, - ) - if recv.ctrl_url == config_info.ctrl_url: - _LOGGER.debug( - "Config Zones Matched Serial %s: %s", - recv.ctrl_url, - recv.serial_number, - ) - zones = rxv.RXV( - config_info.ctrl_url, - friendly_name=config_info.name, - serial_number=recv.serial_number, - model_name=recv.model_name, - ).zone_controllers() - break - - if not zones: - _LOGGER.debug("Config Zones Fallback") - zones = rxv.RXV(config_info.ctrl_url, config_info.name).zone_controllers() + zones = rxv.RXV(config_info.ctrl_url, config_info.name).zone_controllers() _LOGGER.debug("Returned _discover zones: %s", zones) return zones From 6c15f251c67c28c122b6e7813bc8efe697195aa0 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 4 Sep 2024 21:14:54 +0200 Subject: [PATCH 204/271] Fix blocking call in yale_smart_alarm (#125255) --- homeassistant/components/yale_smart_alarm/coordinator.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/yale_smart_alarm/coordinator.py b/homeassistant/components/yale_smart_alarm/coordinator.py index 1067b9279a4..b47545ea88b 100644 --- a/homeassistant/components/yale_smart_alarm/coordinator.py +++ b/homeassistant/components/yale_smart_alarm/coordinator.py @@ -36,8 +36,10 @@ class YaleDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): async def _async_setup(self) -> None: """Set up connection to Yale.""" try: - self.yale = YaleSmartAlarmClient( - self.entry.data[CONF_USERNAME], self.entry.data[CONF_PASSWORD] + self.yale = await self.hass.async_add_executor_job( + YaleSmartAlarmClient, + self.entry.data[CONF_USERNAME], + self.entry.data[CONF_PASSWORD], ) except AuthenticationError as error: raise ConfigEntryAuthFailed from error From 84c204a7b3a5e0adf35ffb71f748893b9141146f Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 4 Sep 2024 21:49:28 +0200 Subject: [PATCH 205/271] Don't show input panel if default code provided in envisalink (#125256) --- homeassistant/components/envisalink/alarm_control_panel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/envisalink/alarm_control_panel.py b/homeassistant/components/envisalink/alarm_control_panel.py index d4bbe174f20..ea8b6390178 100644 --- a/homeassistant/components/envisalink/alarm_control_panel.py +++ b/homeassistant/components/envisalink/alarm_control_panel.py @@ -119,7 +119,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, AlarmControlPanelEntity): self._partition_number = partition_number self._panic_type = panic_type self._alarm_control_panel_option_default_code = code - self._attr_code_format = CodeFormat.NUMBER + self._attr_code_format = CodeFormat.NUMBER if not code else None _LOGGER.debug("Setting up alarm: %s", alarm_name) super().__init__(alarm_name, info, controller) From 5c2073481d6e0a8652cfada896a7e2c6fbf5d8fd Mon Sep 17 00:00:00 2001 From: Jordi Date: Wed, 4 Sep 2024 23:22:31 +0200 Subject: [PATCH 206/271] Increase AquaCell timeout and handle timeout exception properly (#125263) * Increase timeout and add handling of timeout exception * Raise update failed instead of config entry error --- homeassistant/components/aquacell/config_flow.py | 2 +- homeassistant/components/aquacell/coordinator.py | 4 ++-- tests/components/aquacell/test_config_flow.py | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aquacell/config_flow.py b/homeassistant/components/aquacell/config_flow.py index 332cd16e749..1ee89035d93 100644 --- a/homeassistant/components/aquacell/config_flow.py +++ b/homeassistant/components/aquacell/config_flow.py @@ -56,7 +56,7 @@ class AquaCellConfigFlow(ConfigFlow, domain=DOMAIN): refresh_token = await api.authenticate( user_input[CONF_EMAIL], user_input[CONF_PASSWORD] ) - except ApiException: + except (ApiException, TimeoutError): errors["base"] = "cannot_connect" except AuthenticationFailed: errors["base"] = "invalid_auth" diff --git a/homeassistant/components/aquacell/coordinator.py b/homeassistant/components/aquacell/coordinator.py index dd5dfcd2d0d..ee4afb451b9 100644 --- a/homeassistant/components/aquacell/coordinator.py +++ b/homeassistant/components/aquacell/coordinator.py @@ -56,7 +56,7 @@ class AquacellCoordinator(DataUpdateCoordinator[dict[str, Softener]]): so entities can quickly look up their data. """ - async with asyncio.timeout(10): + async with asyncio.timeout(30): # Check if the refresh token is expired expiry_time = ( self.refresh_token_creation_time @@ -72,7 +72,7 @@ class AquacellCoordinator(DataUpdateCoordinator[dict[str, Softener]]): softeners = await self.aquacell_api.get_all_softeners() except AuthenticationFailed as err: raise ConfigEntryError from err - except AquacellApiException as err: + except (AquacellApiException, TimeoutError) as err: raise UpdateFailed(f"Error communicating with API: {err}") from err return {softener.dsn: softener for softener in softeners} diff --git a/tests/components/aquacell/test_config_flow.py b/tests/components/aquacell/test_config_flow.py index b73852d513f..f677b3f8348 100644 --- a/tests/components/aquacell/test_config_flow.py +++ b/tests/components/aquacell/test_config_flow.py @@ -79,6 +79,7 @@ async def test_full_flow( ("exception", "error"), [ (ApiException, "cannot_connect"), + (TimeoutError, "cannot_connect"), (AuthenticationFailed, "invalid_auth"), (Exception, "unknown"), ], From 5c8b2cde925cb491b000fdc41b8d0a59a5048ce7 Mon Sep 17 00:00:00 2001 From: Noah Husby <32528627+noahhusby@users.noreply.github.com> Date: Fri, 6 Sep 2024 09:22:39 -0400 Subject: [PATCH 207/271] Bump aiorussound to 3.0.4 (#125285) feat: bump aiorussound to 3.0.4 --- .../components/russound_rio/__init__.py | 10 ++++----- .../components/russound_rio/config_flow.py | 9 ++++---- .../components/russound_rio/const.py | 4 ++-- .../components/russound_rio/entity.py | 15 +++++++++---- .../components/russound_rio/manifest.json | 2 +- .../components/russound_rio/media_player.py | 22 ++++++++++--------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/russound_rio/conftest.py | 4 ++-- 9 files changed, 39 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/russound_rio/__init__.py b/homeassistant/components/russound_rio/__init__.py index 8627c636ef2..823d0736037 100644 --- a/homeassistant/components/russound_rio/__init__.py +++ b/homeassistant/components/russound_rio/__init__.py @@ -3,7 +3,7 @@ import asyncio import logging -from aiorussound import Russound +from aiorussound import RussoundClient, RussoundTcpConnectionHandler from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, Platform @@ -16,7 +16,7 @@ PLATFORMS = [Platform.MEDIA_PLAYER] _LOGGER = logging.getLogger(__name__) -type RussoundConfigEntry = ConfigEntry[Russound] +type RussoundConfigEntry = ConfigEntry[RussoundClient] async def async_setup_entry(hass: HomeAssistant, entry: RussoundConfigEntry) -> bool: @@ -24,7 +24,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: RussoundConfigEntry) -> host = entry.data[CONF_HOST] port = entry.data[CONF_PORT] - russ = Russound(hass.loop, host, port) + russ = RussoundClient(RussoundTcpConnectionHandler(hass.loop, host, port)) @callback def is_connected_updated(connected: bool) -> None: @@ -37,14 +37,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: RussoundConfigEntry) -> port, ) - russ.add_connection_callback(is_connected_updated) - + russ.connection_handler.add_connection_callback(is_connected_updated) try: async with asyncio.timeout(CONNECT_TIMEOUT): await russ.connect() except RUSSOUND_RIO_EXCEPTIONS as err: raise ConfigEntryNotReady(f"Error while connecting to {host}:{port}") from err - entry.runtime_data = russ await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/russound_rio/config_flow.py b/homeassistant/components/russound_rio/config_flow.py index df173d29f61..03e32f39c08 100644 --- a/homeassistant/components/russound_rio/config_flow.py +++ b/homeassistant/components/russound_rio/config_flow.py @@ -6,7 +6,7 @@ import asyncio import logging from typing import Any -from aiorussound import Controller, Russound +from aiorussound import Controller, RussoundClient, RussoundTcpConnectionHandler import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult @@ -54,8 +54,9 @@ class FlowHandler(ConfigFlow, domain=DOMAIN): host = user_input[CONF_HOST] port = user_input[CONF_PORT] - controllers = None - russ = Russound(self.hass.loop, host, port) + russ = RussoundClient( + RussoundTcpConnectionHandler(self.hass.loop, host, port) + ) try: async with asyncio.timeout(CONNECT_TIMEOUT): await russ.connect() @@ -87,7 +88,7 @@ class FlowHandler(ConfigFlow, domain=DOMAIN): port = import_data.get(CONF_PORT, 9621) # Connection logic is repeated here since this method will be removed in future releases - russ = Russound(self.hass.loop, host, port) + russ = RussoundClient(RussoundTcpConnectionHandler(self.hass.loop, host, port)) try: async with asyncio.timeout(CONNECT_TIMEOUT): await russ.connect() diff --git a/homeassistant/components/russound_rio/const.py b/homeassistant/components/russound_rio/const.py index d1f4e1c4c0e..42a1db5f2ad 100644 --- a/homeassistant/components/russound_rio/const.py +++ b/homeassistant/components/russound_rio/const.py @@ -2,7 +2,7 @@ import asyncio -from aiorussound import CommandException +from aiorussound import CommandError from aiorussound.const import FeatureFlag from homeassistant.components.media_player import MediaPlayerEntityFeature @@ -10,7 +10,7 @@ from homeassistant.components.media_player import MediaPlayerEntityFeature DOMAIN = "russound_rio" RUSSOUND_RIO_EXCEPTIONS = ( - CommandException, + CommandError, ConnectionRefusedError, TimeoutError, asyncio.CancelledError, diff --git a/homeassistant/components/russound_rio/entity.py b/homeassistant/components/russound_rio/entity.py index 0e4d5cf7dde..4d458118939 100644 --- a/homeassistant/components/russound_rio/entity.py +++ b/homeassistant/components/russound_rio/entity.py @@ -4,7 +4,7 @@ from collections.abc import Awaitable, Callable, Coroutine from functools import wraps from typing import Any, Concatenate -from aiorussound import Controller +from aiorussound import Controller, RussoundTcpConnectionHandler from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError @@ -53,7 +53,6 @@ class RussoundBaseEntity(Entity): or f"{self._primary_mac_address}-{self._controller.controller_id}" ) self._attr_device_info = DeviceInfo( - configuration_url=f"http://{self._instance.host}", # Use MAC address of Russound device as identifier identifiers={(DOMAIN, self._device_identifier)}, manufacturer="Russound", @@ -61,6 +60,10 @@ class RussoundBaseEntity(Entity): model=controller.controller_type, sw_version=controller.firmware_version, ) + if isinstance(self._instance.connection_handler, RussoundTcpConnectionHandler): + self._attr_device_info["configuration_url"] = ( + f"http://{self._instance.connection_handler.host}" + ) if controller.parent_controller: self._attr_device_info["via_device"] = ( DOMAIN, @@ -79,8 +82,12 @@ class RussoundBaseEntity(Entity): async def async_added_to_hass(self) -> None: """Register callbacks.""" - self._instance.add_connection_callback(self._is_connected_updated) + self._instance.connection_handler.add_connection_callback( + self._is_connected_updated + ) async def async_will_remove_from_hass(self) -> None: """Remove callbacks.""" - self._instance.remove_connection_callback(self._is_connected_updated) + self._instance.connection_handler.remove_connection_callback( + self._is_connected_updated + ) diff --git a/homeassistant/components/russound_rio/manifest.json b/homeassistant/components/russound_rio/manifest.json index 6c473d94874..19273de92ee 100644 --- a/homeassistant/components/russound_rio/manifest.json +++ b/homeassistant/components/russound_rio/manifest.json @@ -7,5 +7,5 @@ "iot_class": "local_push", "loggers": ["aiorussound"], "quality_scale": "silver", - "requirements": ["aiorussound==2.3.2"] + "requirements": ["aiorussound==3.0.4"] } diff --git a/homeassistant/components/russound_rio/media_player.py b/homeassistant/components/russound_rio/media_player.py index 20aaf0f3c08..a5bb392a028 100644 --- a/homeassistant/components/russound_rio/media_player.py +++ b/homeassistant/components/russound_rio/media_player.py @@ -84,14 +84,16 @@ async def async_setup_entry( """Set up the Russound RIO platform.""" russ = entry.runtime_data + await russ.init_sources() + sources = russ.sources + for source in sources.values(): + await source.watch() + # Discover controllers controllers = await russ.enumerate_controllers() entities = [] for controller in controllers.values(): - sources = controller.sources - for source in sources.values(): - await source.watch() for zone in controller.zones.values(): await zone.watch() mp = RussoundZoneDevice(zone, sources) @@ -154,7 +156,7 @@ class RussoundZoneDevice(RussoundBaseEntity, MediaPlayerEntity): @property def state(self) -> MediaPlayerState | None: """Return the state of the device.""" - status = self._zone.status + status = self._zone.properties.status if status == "ON": return MediaPlayerState.ON if status == "OFF": @@ -174,22 +176,22 @@ class RussoundZoneDevice(RussoundBaseEntity, MediaPlayerEntity): @property def media_title(self): """Title of current playing media.""" - return self._current_source().song_name + return self._current_source().properties.song_name @property def media_artist(self): """Artist of current playing media, music track only.""" - return self._current_source().artist_name + return self._current_source().properties.artist_name @property def media_album_name(self): """Album name of current playing media, music track only.""" - return self._current_source().album_name + return self._current_source().properties.album_name @property def media_image_url(self): """Image url of current playing media.""" - return self._current_source().cover_art_url + return self._current_source().properties.cover_art_url @property def volume_level(self): @@ -198,7 +200,7 @@ class RussoundZoneDevice(RussoundBaseEntity, MediaPlayerEntity): Value is returned based on a range (0..50). Therefore float divide by 50 to get to the required range. """ - return float(self._zone.volume or "0") / 50.0 + return float(self._zone.properties.volume or "0") / 50.0 @command async def async_turn_off(self) -> None: @@ -214,7 +216,7 @@ class RussoundZoneDevice(RussoundBaseEntity, MediaPlayerEntity): async def async_set_volume_level(self, volume: float) -> None: """Set the volume level.""" rvol = int(volume * 50.0) - await self._zone.set_volume(rvol) + await self._zone.set_volume(str(rvol)) @command async def async_select_source(self, source: str) -> None: diff --git a/requirements_all.txt b/requirements_all.txt index 59e9f95e93e..b4599b7cde9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -350,7 +350,7 @@ aioridwell==2024.01.0 aioruckus==0.41 # homeassistant.components.russound_rio -aiorussound==2.3.2 +aiorussound==3.0.4 # homeassistant.components.ruuvi_gateway aioruuvigateway==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ace1c743fe0..511974f85f7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -332,7 +332,7 @@ aioridwell==2024.01.0 aioruckus==0.41 # homeassistant.components.russound_rio -aiorussound==2.3.2 +aiorussound==3.0.4 # homeassistant.components.ruuvi_gateway aioruuvigateway==0.1.0 diff --git a/tests/components/russound_rio/conftest.py b/tests/components/russound_rio/conftest.py index a87d0a74fa8..344c743d0b3 100644 --- a/tests/components/russound_rio/conftest.py +++ b/tests/components/russound_rio/conftest.py @@ -37,10 +37,10 @@ def mock_russound() -> Generator[AsyncMock]: """Mock the Russound RIO client.""" with ( patch( - "homeassistant.components.russound_rio.Russound", autospec=True + "homeassistant.components.russound_rio.RussoundClient", autospec=True ) as mock_client, patch( - "homeassistant.components.russound_rio.config_flow.Russound", + "homeassistant.components.russound_rio.config_flow.RussoundClient", return_value=mock_client, ), ): From 4ed18495f36e5ee9e31b24261c66c0ee65cbc646 Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Thu, 5 Sep 2024 08:50:49 +0200 Subject: [PATCH 208/271] Add follower to the PlayingMode enum (#125294) Update media_player.py --- homeassistant/components/linkplay/media_player.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/linkplay/media_player.py b/homeassistant/components/linkplay/media_player.py index 9eed51241cb..c538c9c3219 100644 --- a/homeassistant/components/linkplay/media_player.py +++ b/homeassistant/components/linkplay/media_player.py @@ -62,6 +62,7 @@ SOURCE_MAP: dict[PlayingMode, str] = { PlayingMode.FM: "FM Radio", PlayingMode.RCA: "RCA", PlayingMode.UDISK: "USB", + PlayingMode.FOLLOWER: "Follower", } SOURCE_MAP_INV: dict[str, PlayingMode] = {v: k for k, v in SOURCE_MAP.items()} From 61ee3a9412aab2e4a84dbabc58b9c5c6a4509769 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 6 Sep 2024 07:59:22 +0200 Subject: [PATCH 209/271] Don't allow templating min, max, step in config entry template number (#125342) --- homeassistant/components/template/__init__.py | 20 ++++++-- .../components/template/config_flow.py | 18 +++---- homeassistant/components/template/const.py | 9 ++-- homeassistant/components/template/number.py | 5 +- tests/components/template/test_config_flow.py | 48 +++++++++--------- tests/components/template/test_init.py | 49 ++++++++++++++++--- tests/components/template/test_number.py | 12 ++--- 7 files changed, 106 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/template/__init__.py b/homeassistant/components/template/__init__.py index efa99342699..d3cfda2d4eb 100644 --- a/homeassistant/components/template/__init__.py +++ b/homeassistant/components/template/__init__.py @@ -7,9 +7,14 @@ import logging from homeassistant import config as conf_util from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_DEVICE_ID, CONF_UNIQUE_ID, SERVICE_RELOAD +from homeassistant.const import ( + CONF_DEVICE_ID, + CONF_NAME, + CONF_UNIQUE_ID, + SERVICE_RELOAD, +) from homeassistant.core import Event, HomeAssistant, ServiceCall -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import ConfigEntryError, HomeAssistantError from homeassistant.helpers import discovery from homeassistant.helpers.device import ( async_remove_stale_devices_links_keep_current_device, @@ -19,7 +24,7 @@ from homeassistant.helpers.service import async_register_admin_service from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_integration -from .const import CONF_TRIGGER, DOMAIN, PLATFORMS +from .const import CONF_MAX, CONF_MIN, CONF_STEP, CONF_TRIGGER, DOMAIN, PLATFORMS from .coordinator import TriggerUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -67,6 +72,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.options.get(CONF_DEVICE_ID), ) + for key in (CONF_MAX, CONF_MIN, CONF_STEP): + if key not in entry.options: + continue + if isinstance(entry.options[key], str): + raise ConfigEntryError( + f"The '{entry.options.get(CONF_NAME) or ""}' number template needs to " + f"be reconfigured, {key} must be a number, got '{entry.options[key]}'" + ) + await hass.config_entries.async_forward_entry_setups( entry, (entry.options["template_type"],) ) diff --git a/homeassistant/components/template/config_flow.py b/homeassistant/components/template/config_flow.py index 2c12a0d03e9..ba4f4a78f53 100644 --- a/homeassistant/components/template/config_flow.py +++ b/homeassistant/components/template/config_flow.py @@ -107,15 +107,15 @@ def generate_schema(domain: str, flow_type: str) -> vol.Schema: if domain == Platform.NUMBER: schema |= { vol.Required(CONF_STATE): selector.TemplateSelector(), - vol.Required( - CONF_MIN, default=f"{{{{{DEFAULT_MIN_VALUE}}}}}" - ): selector.TemplateSelector(), - vol.Required( - CONF_MAX, default=f"{{{{{DEFAULT_MAX_VALUE}}}}}" - ): selector.TemplateSelector(), - vol.Required( - CONF_STEP, default=f"{{{{{DEFAULT_STEP}}}}}" - ): selector.TemplateSelector(), + vol.Required(CONF_MIN, default=DEFAULT_MIN_VALUE): selector.NumberSelector( + selector.NumberSelectorConfig(mode=selector.NumberSelectorMode.BOX), + ), + vol.Required(CONF_MAX, default=DEFAULT_MAX_VALUE): selector.NumberSelector( + selector.NumberSelectorConfig(mode=selector.NumberSelectorMode.BOX), + ), + vol.Required(CONF_STEP, default=DEFAULT_STEP): selector.NumberSelector( + selector.NumberSelectorConfig(mode=selector.NumberSelectorMode.BOX), + ), vol.Optional(CONF_SET_VALUE): selector.ActionSelector(), } diff --git a/homeassistant/components/template/const.py b/homeassistant/components/template/const.py index 8b4e46ba383..89df87b4031 100644 --- a/homeassistant/components/template/const.py +++ b/homeassistant/components/template/const.py @@ -28,11 +28,14 @@ PLATFORMS = [ Platform.WEATHER, ] -CONF_AVAILABILITY = "availability" -CONF_ATTRIBUTES = "attributes" CONF_ATTRIBUTE_TEMPLATES = "attribute_templates" +CONF_ATTRIBUTES = "attributes" +CONF_AVAILABILITY = "availability" +CONF_MAX = "max" +CONF_MIN = "min" +CONF_OBJECT_ID = "object_id" CONF_PICTURE = "picture" CONF_PRESS = "press" -CONF_OBJECT_ID = "object_id" +CONF_STEP = "step" CONF_TURN_OFF = "turn_off" CONF_TURN_ON = "turn_on" diff --git a/homeassistant/components/template/number.py b/homeassistant/components/template/number.py index 499ddc192cc..e051f124149 100644 --- a/homeassistant/components/template/number.py +++ b/homeassistant/components/template/number.py @@ -31,7 +31,7 @@ from homeassistant.helpers.script import Script from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import TriggerUpdateCoordinator -from .const import DOMAIN +from .const import CONF_MAX, CONF_MIN, CONF_STEP, DOMAIN from .template_entity import ( TEMPLATE_ENTITY_AVAILABILITY_SCHEMA, TEMPLATE_ENTITY_ICON_SCHEMA, @@ -42,9 +42,6 @@ from .trigger_entity import TriggerEntity _LOGGER = logging.getLogger(__name__) CONF_SET_VALUE = "set_value" -CONF_MIN = "min" -CONF_MAX = "max" -CONF_STEP = "step" DEFAULT_NAME = "Template Number" DEFAULT_OPTIMISTIC = False diff --git a/tests/components/template/test_config_flow.py b/tests/components/template/test_config_flow.py index f8ab190e664..ee748ce41f5 100644 --- a/tests/components/template/test_config_flow.py +++ b/tests/components/template/test_config_flow.py @@ -98,9 +98,9 @@ from tests.typing import WebSocketGenerator {"one": "30.0", "two": "20.0"}, {}, { - "min": "{{ 0 }}", - "max": "{{ 100 }}", - "step": "{{ 0.1 }}", + "min": "0", + "max": "100", + "step": "0.1", "set_value": { "action": "input_number.set_value", "target": {"entity_id": "input_number.test"}, @@ -108,9 +108,9 @@ from tests.typing import WebSocketGenerator }, }, { - "min": "{{ 0 }}", - "max": "{{ 100 }}", - "step": "{{ 0.1 }}", + "min": 0, + "max": 100, + "step": 0.1, "set_value": { "action": "input_number.set_value", "target": {"entity_id": "input_number.test"}, @@ -258,14 +258,14 @@ async def test_config_flow( "number", {"state": "{{ states('number.one') }}"}, { - "min": "{{ 0 }}", - "max": "{{ 100 }}", - "step": "{{ 0.1 }}", + "min": "0", + "max": "100", + "step": "0.1", }, { - "min": "{{ 0 }}", - "max": "{{ 100 }}", - "step": "{{ 0.1 }}", + "min": 0, + "max": 100, + "step": 0.1, }, ), ( @@ -451,9 +451,9 @@ def get_suggested(schema, key): ["30.0", "20.0"], {"one": "30.0", "two": "20.0"}, { - "min": "{{ 0 }}", - "max": "{{ 100 }}", - "step": "{{ 0.1 }}", + "min": 0, + "max": 100, + "step": 0.1, "set_value": { "action": "input_number.set_value", "target": {"entity_id": "input_number.test"}, @@ -461,9 +461,9 @@ def get_suggested(schema, key): }, }, { - "min": "{{ 0 }}", - "max": "{{ 100 }}", - "step": "{{ 0.1 }}", + "min": 0, + "max": 100, + "step": 0.1, "set_value": { "action": "input_number.set_value", "target": {"entity_id": "input_number.test"}, @@ -1230,14 +1230,14 @@ async def test_option_flow_sensor_preview_config_entry_removed( "number", {"state": "{{ states('number.one') }}"}, { - "min": "{{ 0 }}", - "max": "{{ 100 }}", - "step": "{{ 0.1 }}", + "min": 0, + "max": 100, + "step": 0.1, }, { - "min": "{{ 0 }}", - "max": "{{ 100 }}", - "step": "{{ 0.1 }}", + "min": 0, + "max": 100, + "step": 0.1, }, ), ( diff --git a/tests/components/template/test_init.py b/tests/components/template/test_init.py index 3b4db4bf668..0de57062984 100644 --- a/tests/components/template/test_init.py +++ b/tests/components/template/test_init.py @@ -319,9 +319,9 @@ async def async_yaml_patch_helper(hass: HomeAssistant, filename: str) -> None: "template_type": "number", "name": "My template", "state": "{{ 10 }}", - "min": "{{ 0 }}", - "max": "{{ 100 }}", - "step": "{{ 0.1 }}", + "min": 0, + "max": 100, + "step": 0.1, "set_value": { "action": "input_number.set_value", "target": {"entity_id": "input_number.test"}, @@ -330,9 +330,9 @@ async def async_yaml_patch_helper(hass: HomeAssistant, filename: str) -> None: }, { "state": "{{ 11 }}", - "min": "{{ 0 }}", - "max": "{{ 100 }}", - "step": "{{ 0.1 }}", + "min": 0, + "max": 100, + "step": 0.1, "set_value": { "action": "input_number.set_value", "target": {"entity_id": "input_number.test"}, @@ -454,3 +454,40 @@ async def test_change_device( ) == [] ) + + +async def test_fail_non_numerical_number_settings( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test that non numerical number options causes config entry setup to fail. + + Support for non numerical max, min and step was added in HA Core 2024.9.0 and + removed in HA Core 2024.9.1. + """ + + options = { + "template_type": "number", + "name": "My template", + "state": "{{ 10 }}", + "min": "{{ 0 }}", + "max": "{{ 100 }}", + "step": "{{ 0.1 }}", + "set_value": { + "action": "input_number.set_value", + "target": {"entity_id": "input_number.test"}, + "data": {"value": "{{ value }}"}, + }, + } + # Setup the config entry + template_config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options=options, + title="Template", + ) + template_config_entry.add_to_hass(hass) + assert not await hass.config_entries.async_setup(template_config_entry.entry_id) + assert ( + "The 'My template' number template needs to be reconfigured, " + "max must be a number, got '{{ 100 }}'" in caplog.text + ) diff --git a/tests/components/template/test_number.py b/tests/components/template/test_number.py index fdca94d9fa4..43decf848ff 100644 --- a/tests/components/template/test_number.py +++ b/tests/components/template/test_number.py @@ -58,9 +58,9 @@ async def test_setup_config_entry( "name": "My template", "template_type": "number", "state": "{{ 10 }}", - "min": "{{ 0 }}", - "max": "{{ 100 }}", - "step": "{{ 0.1 }}", + "min": 0, + "max": 100, + "step": 0.1, "set_value": { "action": "input_number.set_value", "target": {"entity_id": "input_number.test"}, @@ -524,9 +524,9 @@ async def test_device_id( "name": "My template", "template_type": "number", "state": "{{ 10 }}", - "min": "{{ 0 }}", - "max": "{{ 100 }}", - "step": "{{ 0.1 }}", + "min": 0, + "max": 100, + "step": 0.1, "set_value": { "action": "input_number.set_value", "target": {"entity_id": "input_number.test"}, From 6c640d2abef30e827249897616caf09c1a673678 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Fri, 6 Sep 2024 14:06:46 +0200 Subject: [PATCH 210/271] Fix for Hue sending effect None at turn_on command while no effect is active (#125377) * Fix for Hue sending effect None at turn_on command while no effect is active * typo * update tests --- homeassistant/components/hue/v2/light.py | 6 ++- tests/components/hue/test_light_v2.py | 54 +++++++++++++++++++----- 2 files changed, 49 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/hue/v2/light.py b/homeassistant/components/hue/v2/light.py index b908ec83877..6fd0eea7a0b 100644 --- a/homeassistant/components/hue/v2/light.py +++ b/homeassistant/components/hue/v2/light.py @@ -226,7 +226,11 @@ class HueLight(HueBaseEntity, LightEntity): flash = kwargs.get(ATTR_FLASH) effect = effect_str = kwargs.get(ATTR_EFFECT) if effect_str in (EFFECT_NONE, EFFECT_NONE.lower()): - effect = EffectStatus.NO_EFFECT + # ignore effect if set to "None" and we have no effect active + # the special effect "None" is only used to stop an active effect + # but sending it while no effect is active can actually result in issues + # https://github.com/home-assistant/core/issues/122165 + effect = None if self.effect == EFFECT_NONE else EffectStatus.NO_EFFECT elif effect_str is not None: # work out if we got a regular effect or timed effect effect = EffectStatus(effect_str) diff --git a/tests/components/hue/test_light_v2.py b/tests/components/hue/test_light_v2.py index 417670a3769..2b978ffc33f 100644 --- a/tests/components/hue/test_light_v2.py +++ b/tests/components/hue/test_light_v2.py @@ -175,7 +175,7 @@ async def test_light_turn_on_service( assert len(mock_bridge_v2.mock_requests) == 6 assert mock_bridge_v2.mock_requests[5]["json"]["color_temperature"]["mirek"] == 500 - # test enable effect + # test enable an effect await hass.services.async_call( "light", "turn_on", @@ -184,8 +184,20 @@ async def test_light_turn_on_service( ) assert len(mock_bridge_v2.mock_requests) == 7 assert mock_bridge_v2.mock_requests[6]["json"]["effects"]["effect"] == "candle" + # fire event to update effect in HA state + event = { + "id": "3a6710fa-4474-4eba-b533-5e6e72968feb", + "type": "light", + "effects": {"status": "candle"}, + } + mock_bridge_v2.api.emit_event("update", event) + await hass.async_block_till_done() + test_light = hass.states.get(test_light_id) + assert test_light is not None + assert test_light.attributes["effect"] == "candle" # test disable effect + # it should send a request with effect set to "no_effect" await hass.services.async_call( "light", "turn_on", @@ -194,6 +206,28 @@ async def test_light_turn_on_service( ) assert len(mock_bridge_v2.mock_requests) == 8 assert mock_bridge_v2.mock_requests[7]["json"]["effects"]["effect"] == "no_effect" + # fire event to update effect in HA state + event = { + "id": "3a6710fa-4474-4eba-b533-5e6e72968feb", + "type": "light", + "effects": {"status": "no_effect"}, + } + mock_bridge_v2.api.emit_event("update", event) + await hass.async_block_till_done() + test_light = hass.states.get(test_light_id) + assert test_light is not None + assert test_light.attributes["effect"] == "None" + + # test turn on with useless effect + # it should send a effect in the request if the device has no effect active + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": test_light_id, "effect": "None"}, + blocking=True, + ) + assert len(mock_bridge_v2.mock_requests) == 9 + assert "effects" not in mock_bridge_v2.mock_requests[8]["json"] # test timed effect await hass.services.async_call( @@ -202,11 +236,11 @@ async def test_light_turn_on_service( {"entity_id": test_light_id, "effect": "sunrise", "transition": 6}, blocking=True, ) - assert len(mock_bridge_v2.mock_requests) == 9 + assert len(mock_bridge_v2.mock_requests) == 10 assert ( - mock_bridge_v2.mock_requests[8]["json"]["timed_effects"]["effect"] == "sunrise" + mock_bridge_v2.mock_requests[9]["json"]["timed_effects"]["effect"] == "sunrise" ) - assert mock_bridge_v2.mock_requests[8]["json"]["timed_effects"]["duration"] == 6000 + assert mock_bridge_v2.mock_requests[9]["json"]["timed_effects"]["duration"] == 6000 # test enabling effect should ignore color temperature await hass.services.async_call( @@ -215,9 +249,9 @@ async def test_light_turn_on_service( {"entity_id": test_light_id, "effect": "candle", "color_temp": 500}, blocking=True, ) - assert len(mock_bridge_v2.mock_requests) == 10 - assert mock_bridge_v2.mock_requests[9]["json"]["effects"]["effect"] == "candle" - assert "color_temperature" not in mock_bridge_v2.mock_requests[9]["json"] + assert len(mock_bridge_v2.mock_requests) == 11 + assert mock_bridge_v2.mock_requests[10]["json"]["effects"]["effect"] == "candle" + assert "color_temperature" not in mock_bridge_v2.mock_requests[10]["json"] # test enabling effect should ignore xy color await hass.services.async_call( @@ -226,9 +260,9 @@ async def test_light_turn_on_service( {"entity_id": test_light_id, "effect": "candle", "xy_color": [0.123, 0.123]}, blocking=True, ) - assert len(mock_bridge_v2.mock_requests) == 11 - assert mock_bridge_v2.mock_requests[10]["json"]["effects"]["effect"] == "candle" - assert "xy_color" not in mock_bridge_v2.mock_requests[9]["json"] + assert len(mock_bridge_v2.mock_requests) == 12 + assert mock_bridge_v2.mock_requests[11]["json"]["effects"]["effect"] == "candle" + assert "xy_color" not in mock_bridge_v2.mock_requests[11]["json"] async def test_light_turn_off_service( From 7859d31ca0e5bf90e77d203992bdbf981860144f Mon Sep 17 00:00:00 2001 From: Ryan Mattson Date: Fri, 6 Sep 2024 04:45:39 -0500 Subject: [PATCH 211/271] Lyric: fixed missed snake case conversions (#125382) fixed missed snake case conversions --- homeassistant/components/lyric/climate.py | 26 +++++++++++------------ 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/lyric/climate.py b/homeassistant/components/lyric/climate.py index 1c459c2c66a..bd9cf4997eb 100644 --- a/homeassistant/components/lyric/climate.py +++ b/homeassistant/components/lyric/climate.py @@ -358,8 +358,8 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity): await self._update_thermostat( self.location, device, - coolSetpoint=target_temp_high, - heatSetpoint=target_temp_low, + cool_setpoint=target_temp_high, + heat_setpoint=target_temp_low, mode=mode, ) except LYRIC_EXCEPTIONS as exception: @@ -371,11 +371,11 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity): try: if self.hvac_mode == HVACMode.COOL: await self._update_thermostat( - self.location, device, coolSetpoint=temp + self.location, device, cool_setpoint=temp ) else: await self._update_thermostat( - self.location, device, heatSetpoint=temp + self.location, device, heat_setpoint=temp ) except LYRIC_EXCEPTIONS as exception: _LOGGER.error(exception) @@ -410,7 +410,7 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity): self.location, self.device, mode=HVAC_MODES[LYRIC_HVAC_MODE_HEAT], - autoChangeoverActive=False, + auto_changeover_active=False, ) # Sleep 3 seconds before proceeding await asyncio.sleep(3) @@ -422,7 +422,7 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity): self.location, self.device, mode=HVAC_MODES[LYRIC_HVAC_MODE_HEAT], - autoChangeoverActive=True, + auto_changeover_active=True, ) else: _LOGGER.debug( @@ -430,7 +430,7 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity): HVAC_MODES[self.device.changeable_values.mode], ) await self._update_thermostat( - self.location, self.device, autoChangeoverActive=True + self.location, self.device, auto_changeover_active=True ) else: _LOGGER.debug("HVAC mode passed to lyric: %s", LYRIC_HVAC_MODES[hvac_mode]) @@ -438,13 +438,13 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity): self.location, self.device, mode=LYRIC_HVAC_MODES[hvac_mode], - autoChangeoverActive=False, + auto_changeover_active=False, ) async def _async_set_hvac_mode_lcc(self, hvac_mode: HVACMode) -> None: """Set hvac mode for LCC devices (e.g., T5,6).""" _LOGGER.debug("HVAC mode passed to lyric: %s", LYRIC_HVAC_MODES[hvac_mode]) - # Set autoChangeoverActive to True if the mode being passed is Auto + # Set auto_changeover_active to True if the mode being passed is Auto # otherwise leave unchanged. if ( LYRIC_HVAC_MODES[hvac_mode] == LYRIC_HVAC_MODE_HEAT_COOL @@ -458,7 +458,7 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity): self.location, self.device, mode=LYRIC_HVAC_MODES[hvac_mode], - autoChangeoverActive=auto_changeover, + auto_changeover_active=auto_changeover, ) async def async_set_preset_mode(self, preset_mode: str) -> None: @@ -466,7 +466,7 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity): _LOGGER.debug("Set preset mode: %s", preset_mode) try: await self._update_thermostat( - self.location, self.device, thermostatSetpointStatus=preset_mode + self.location, self.device, thermostat_setpoint_status=preset_mode ) except LYRIC_EXCEPTIONS as exception: _LOGGER.error(exception) @@ -479,8 +479,8 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity): await self._update_thermostat( self.location, self.device, - thermostatSetpointStatus=PRESET_HOLD_UNTIL, - nextPeriodTime=time_period, + thermostat_setpoint_status=PRESET_HOLD_UNTIL, + next_period_time=time_period, ) except LYRIC_EXCEPTIONS as exception: _LOGGER.error(exception) From edb7c76caa1ad358fa35c3a9a7a621cc1d10d60b Mon Sep 17 00:00:00 2001 From: TimL Date: Fri, 6 Sep 2024 18:20:12 +1000 Subject: [PATCH 212/271] Bump pysmlight to 0.0.14 (#125387) Bump pysmlight 0.0.14 for smlight --- homeassistant/components/smlight/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/smlight/manifest.json b/homeassistant/components/smlight/manifest.json index 72d915666e5..1a91b29234c 100644 --- a/homeassistant/components/smlight/manifest.json +++ b/homeassistant/components/smlight/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/smlight", "integration_type": "device", "iot_class": "local_polling", - "requirements": ["pysmlight==0.0.13"], + "requirements": ["pysmlight==0.0.14"], "zeroconf": [ { "type": "_slzb-06._tcp.local." diff --git a/requirements_all.txt b/requirements_all.txt index b4599b7cde9..6528e09aa1d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2214,7 +2214,7 @@ pysmartthings==0.7.8 pysml==0.0.12 # homeassistant.components.smlight -pysmlight==0.0.13 +pysmlight==0.0.14 # homeassistant.components.snmp pysnmp==6.2.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 511974f85f7..0696f65449f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1768,7 +1768,7 @@ pysmartthings==0.7.8 pysml==0.0.12 # homeassistant.components.smlight -pysmlight==0.0.13 +pysmlight==0.0.14 # homeassistant.components.snmp pysnmp==6.2.5 From 27dc2e1b9de9699e1146e53c63054949b52cee60 Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Fri, 6 Sep 2024 08:29:49 +0200 Subject: [PATCH 213/271] Bump pypck to 0.7.22 (#125389) --- homeassistant/components/lcn/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lcn/manifest.json b/homeassistant/components/lcn/manifest.json index f8b7d02b103..9023941277f 100644 --- a/homeassistant/components/lcn/manifest.json +++ b/homeassistant/components/lcn/manifest.json @@ -8,5 +8,5 @@ "documentation": "https://www.home-assistant.io/integrations/lcn", "iot_class": "local_push", "loggers": ["pypck"], - "requirements": ["pypck==0.7.21", "lcn-frontend==0.1.6"] + "requirements": ["pypck==0.7.22", "lcn-frontend==0.1.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index 6528e09aa1d..e86c21d0124 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2109,7 +2109,7 @@ pyownet==0.10.0.post1 pypca==0.0.7 # homeassistant.components.lcn -pypck==0.7.21 +pypck==0.7.22 # homeassistant.components.pjlink pypjlink2==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0696f65449f..8bb92e7d2e8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1687,7 +1687,7 @@ pyoverkiz==1.13.14 pyownet==0.10.0.post1 # homeassistant.components.lcn -pypck==0.7.21 +pypck==0.7.22 # homeassistant.components.pjlink pypjlink2==1.2.1 From e80e189e6bbb8679c08d42aa87b06fe3a8e021db Mon Sep 17 00:00:00 2001 From: dontinelli <73341522+dontinelli@users.noreply.github.com> Date: Fri, 6 Sep 2024 10:23:30 +0200 Subject: [PATCH 214/271] Increase coordinator update_interval for fyta (#125393) * Increase update_interval * Update homeassistant/components/fyta/coordinator.py --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/fyta/coordinator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/fyta/coordinator.py b/homeassistant/components/fyta/coordinator.py index c92a96eed63..df607de76b0 100644 --- a/homeassistant/components/fyta/coordinator.py +++ b/homeassistant/components/fyta/coordinator.py @@ -39,7 +39,7 @@ class FytaCoordinator(DataUpdateCoordinator[dict[int, Plant]]): hass, _LOGGER, name="FYTA Coordinator", - update_interval=timedelta(seconds=60), + update_interval=timedelta(minutes=4), ) self.fyta = fyta From 973e43ae6abde5a606db7a8824a451169df9ca7e Mon Sep 17 00:00:00 2001 From: Dave Leaver Date: Fri, 6 Sep 2024 19:06:33 +1200 Subject: [PATCH 215/271] Fix controlling AC temperature in airtouch5 (#125394) Fix controlling AC temperature --- homeassistant/components/airtouch5/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/airtouch5/climate.py b/homeassistant/components/airtouch5/climate.py index 2d5740b1837..dfc34c1beaf 100644 --- a/homeassistant/components/airtouch5/climate.py +++ b/homeassistant/components/airtouch5/climate.py @@ -262,7 +262,7 @@ class Airtouch5AC(Airtouch5ClimateEntity): _LOGGER.debug("Argument `temperature` is missing in set_temperature") return - await self._control(temp=temp) + await self._control(setpoint=SetpointControl.CHANGE_SETPOINT, temp=temp) class Airtouch5Zone(Airtouch5ClimateEntity): From a3f42e36ac74869eed819e7b476be1c5fa9e53b5 Mon Sep 17 00:00:00 2001 From: Alexandre TRUPIN <72858385+AlexT59@users.noreply.github.com> Date: Fri, 6 Sep 2024 16:18:47 +0200 Subject: [PATCH 216/271] Bump sfrbox-api to 0.0.10 (#125405) * bump sfr_box requirement to 0.0.10 * upate manifest file * Handle None values --------- Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- homeassistant/components/sfr_box/__init__.py | 3 ++ .../components/sfr_box/binary_sensor.py | 14 +++++--- homeassistant/components/sfr_box/button.py | 8 +++-- .../components/sfr_box/config_flow.py | 4 ++- .../components/sfr_box/coordinator.py | 6 ++-- .../components/sfr_box/diagnostics.py | 31 ++++++++---------- .../components/sfr_box/manifest.json | 2 +- homeassistant/components/sfr_box/sensor.py | 32 +++++++++++++++---- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../sfr_box/snapshots/test_diagnostics.ambr | 4 +-- 11 files changed, 68 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/sfr_box/__init__.py b/homeassistant/components/sfr_box/__init__.py index dade1af0e52..d386c670365 100644 --- a/homeassistant/components/sfr_box/__init__.py +++ b/homeassistant/components/sfr_box/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio +from typing import TYPE_CHECKING from sfrbox_api.bridge import SFRBox from sfrbox_api.exceptions import SFRBoxAuthenticationError, SFRBoxError @@ -46,6 +47,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Preload system information await data.system.async_config_entry_first_refresh() system_info = data.system.data + if TYPE_CHECKING: + assert system_info is not None # Preload other coordinators (based on net infrastructure) tasks = [data.wan.async_config_entry_first_refresh()] diff --git a/homeassistant/components/sfr_box/binary_sensor.py b/homeassistant/components/sfr_box/binary_sensor.py index b299af33513..4ef5e87761d 100644 --- a/homeassistant/components/sfr_box/binary_sensor.py +++ b/homeassistant/components/sfr_box/binary_sensor.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass +from typing import TYPE_CHECKING from sfrbox_api.models import DslInfo, FtthInfo, SystemInfo, WanInfo @@ -65,19 +66,22 @@ async def async_setup_entry( ) -> None: """Set up the sensors.""" data: DomainData = hass.data[DOMAIN][entry.entry_id] + system_info = data.system.data + if TYPE_CHECKING: + assert system_info is not None entities: list[SFRBoxBinarySensor] = [ - SFRBoxBinarySensor(data.wan, description, data.system.data) + SFRBoxBinarySensor(data.wan, description, system_info) for description in WAN_SENSOR_TYPES ] - if (net_infra := data.system.data.net_infra) == "adsl": + if (net_infra := system_info.net_infra) == "adsl": entities.extend( - SFRBoxBinarySensor(data.dsl, description, data.system.data) + SFRBoxBinarySensor(data.dsl, description, system_info) for description in DSL_SENSOR_TYPES ) elif net_infra == "ftth": entities.extend( - SFRBoxBinarySensor(data.ftth, description, data.system.data) + SFRBoxBinarySensor(data.ftth, description, system_info) for description in FTTH_SENSOR_TYPES ) @@ -111,4 +115,6 @@ class SFRBoxBinarySensor[_T]( @property def is_on(self) -> bool | None: """Return the native value of the device.""" + if self.coordinator.data is None: + return None return self.entity_description.value_fn(self.coordinator.data) diff --git a/homeassistant/components/sfr_box/button.py b/homeassistant/components/sfr_box/button.py index f6d3100d692..bddb1e8f926 100644 --- a/homeassistant/components/sfr_box/button.py +++ b/homeassistant/components/sfr_box/button.py @@ -5,7 +5,7 @@ from __future__ import annotations from collections.abc import Awaitable, Callable, Coroutine from dataclasses import dataclass from functools import wraps -from typing import Any, Concatenate +from typing import TYPE_CHECKING, Any, Concatenate from sfrbox_api.bridge import SFRBox from sfrbox_api.exceptions import SFRBoxError @@ -69,10 +69,12 @@ async def async_setup_entry( ) -> None: """Set up the buttons.""" data: DomainData = hass.data[DOMAIN][entry.entry_id] + system_info = data.system.data + if TYPE_CHECKING: + assert system_info is not None entities = [ - SFRBoxButton(data.box, description, data.system.data) - for description in BUTTON_TYPES + SFRBoxButton(data.box, description, system_info) for description in BUTTON_TYPES ] async_add_entities(entities) diff --git a/homeassistant/components/sfr_box/config_flow.py b/homeassistant/components/sfr_box/config_flow.py index f7d72c01ccd..a4f14e59069 100644 --- a/homeassistant/components/sfr_box/config_flow.py +++ b/homeassistant/components/sfr_box/config_flow.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Mapping -from typing import Any +from typing import TYPE_CHECKING, Any from sfrbox_api.bridge import SFRBox from sfrbox_api.exceptions import SFRBoxAuthenticationError, SFRBoxError @@ -51,6 +51,8 @@ class SFRBoxFlowHandler(ConfigFlow, domain=DOMAIN): except SFRBoxError: errors["base"] = "cannot_connect" else: + if TYPE_CHECKING: + assert system_info is not None await self.async_set_unique_id(system_info.mac_addr) self._abort_if_unique_id_configured() self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]}) diff --git a/homeassistant/components/sfr_box/coordinator.py b/homeassistant/components/sfr_box/coordinator.py index af3195723f4..5877d5a454a 100644 --- a/homeassistant/components/sfr_box/coordinator.py +++ b/homeassistant/components/sfr_box/coordinator.py @@ -15,7 +15,7 @@ _LOGGER = logging.getLogger(__name__) _SCAN_INTERVAL = timedelta(minutes=1) -class SFRDataUpdateCoordinator[_DataT](DataUpdateCoordinator[_DataT]): +class SFRDataUpdateCoordinator[_DataT](DataUpdateCoordinator[_DataT | None]): """Coordinator to manage data updates.""" def __init__( @@ -23,14 +23,14 @@ class SFRDataUpdateCoordinator[_DataT](DataUpdateCoordinator[_DataT]): hass: HomeAssistant, box: SFRBox, name: str, - method: Callable[[SFRBox], Coroutine[Any, Any, _DataT]], + method: Callable[[SFRBox], Coroutine[Any, Any, _DataT | None]], ) -> None: """Initialize coordinator.""" self.box = box self._method = method super().__init__(hass, _LOGGER, name=name, update_interval=_SCAN_INTERVAL) - async def _async_update_data(self) -> _DataT: + async def _async_update_data(self) -> _DataT | None: """Update data.""" try: return await self._method(self.box) diff --git a/homeassistant/components/sfr_box/diagnostics.py b/homeassistant/components/sfr_box/diagnostics.py index b5aca834af5..0553bfe4233 100644 --- a/homeassistant/components/sfr_box/diagnostics.py +++ b/homeassistant/components/sfr_box/diagnostics.py @@ -3,7 +3,7 @@ from __future__ import annotations import dataclasses -from typing import Any +from typing import TYPE_CHECKING, Any from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry @@ -12,9 +12,18 @@ from homeassistant.core import HomeAssistant from .const import DOMAIN from .models import DomainData +if TYPE_CHECKING: + from _typeshed import DataclassInstance + TO_REDACT = {"mac_addr", "serial_number", "ip_addr", "ipv6_addr"} +def _async_redact_data(obj: DataclassInstance | None) -> dict[str, Any] | None: + if obj is None: + return None + return async_redact_data(dataclasses.asdict(obj), TO_REDACT) + + async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry ) -> dict[str, Any]: @@ -27,21 +36,9 @@ async def async_get_config_entry_diagnostics( "data": dict(entry.data), }, "data": { - "dsl": async_redact_data( - dataclasses.asdict(await data.system.box.dsl_get_info()), - TO_REDACT, - ), - "ftth": async_redact_data( - dataclasses.asdict(await data.system.box.ftth_get_info()), - TO_REDACT, - ), - "system": async_redact_data( - dataclasses.asdict(await data.system.box.system_get_info()), - TO_REDACT, - ), - "wan": async_redact_data( - dataclasses.asdict(await data.system.box.wan_get_info()), - TO_REDACT, - ), + "dsl": _async_redact_data(await data.system.box.dsl_get_info()), + "ftth": _async_redact_data(await data.system.box.ftth_get_info()), + "system": _async_redact_data(await data.system.box.system_get_info()), + "wan": _async_redact_data(await data.system.box.wan_get_info()), }, } diff --git a/homeassistant/components/sfr_box/manifest.json b/homeassistant/components/sfr_box/manifest.json index bf4d91a50f1..cd42997cec5 100644 --- a/homeassistant/components/sfr_box/manifest.json +++ b/homeassistant/components/sfr_box/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/sfr_box", "integration_type": "device", "iot_class": "local_polling", - "requirements": ["sfrbox-api==0.0.8"] + "requirements": ["sfrbox-api==0.0.10"] } diff --git a/homeassistant/components/sfr_box/sensor.py b/homeassistant/components/sfr_box/sensor.py index d19ff82b393..ee3285a8f38 100644 --- a/homeassistant/components/sfr_box/sensor.py +++ b/homeassistant/components/sfr_box/sensor.py @@ -2,6 +2,7 @@ from collections.abc import Callable from dataclasses import dataclass +from typing import TYPE_CHECKING from sfrbox_api.models import DslInfo, SystemInfo, WanInfo @@ -129,7 +130,7 @@ DSL_SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription[DslInfo], ...] = ( "unknown", ], translation_key="dsl_line_status", - value_fn=lambda x: x.line_status.lower().replace(" ", "_"), + value_fn=lambda x: _value_to_option(x.line_status), ), SFRBoxSensorEntityDescription[DslInfo]( key="training", @@ -149,7 +150,7 @@ DSL_SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription[DslInfo], ...] = ( "unknown", ], translation_key="dsl_training", - value_fn=lambda x: x.training.lower().replace(" ", "_").replace(".", "_"), + value_fn=lambda x: _value_to_option(x.training), ), ) SYSTEM_SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription[SystemInfo], ...] = ( @@ -181,7 +182,7 @@ SYSTEM_SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription[SystemInfo], ...] = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, native_unit_of_measurement=UnitOfTemperature.CELSIUS, - value_fn=lambda x: None if x.temperature is None else x.temperature / 1000, + value_fn=lambda x: _get_temperature(x.temperature), ), ) WAN_SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription[WanInfo], ...] = ( @@ -203,23 +204,38 @@ WAN_SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription[WanInfo], ...] = ( ) +def _value_to_option(value: str | None) -> str | None: + if value is None: + return value + return value.lower().replace(" ", "_").replace(".", "_") + + +def _get_temperature(value: float | None) -> float | None: + if value is None or value < 1000: + return value + return value / 1000 + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the sensors.""" data: DomainData = hass.data[DOMAIN][entry.entry_id] + system_info = data.system.data + if TYPE_CHECKING: + assert system_info is not None entities: list[SFRBoxSensor] = [ - SFRBoxSensor(data.system, description, data.system.data) + SFRBoxSensor(data.system, description, system_info) for description in SYSTEM_SENSOR_TYPES ] entities.extend( - SFRBoxSensor(data.wan, description, data.system.data) + SFRBoxSensor(data.wan, description, system_info) for description in WAN_SENSOR_TYPES ) - if data.system.data.net_infra == "adsl": + if system_info.net_infra == "adsl": entities.extend( - SFRBoxSensor(data.dsl, description, data.system.data) + SFRBoxSensor(data.dsl, description, system_info) for description in DSL_SENSOR_TYPES ) @@ -251,4 +267,6 @@ class SFRBoxSensor[_T](CoordinatorEntity[SFRDataUpdateCoordinator[_T]], SensorEn @property def native_value(self) -> StateType: """Return the native value of the device.""" + if self.coordinator.data is None: + return None return self.entity_description.value_fn(self.coordinator.data) diff --git a/requirements_all.txt b/requirements_all.txt index e86c21d0124..d2f60f1a0bf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2595,7 +2595,7 @@ sensorpush-ble==1.6.2 sentry-sdk==1.40.3 # homeassistant.components.sfr_box -sfrbox-api==0.0.8 +sfrbox-api==0.0.10 # homeassistant.components.sharkiq sharkiq==1.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8bb92e7d2e8..95919b5d9c9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2053,7 +2053,7 @@ sensorpush-ble==1.6.2 sentry-sdk==1.40.3 # homeassistant.components.sfr_box -sfrbox-api==0.0.8 +sfrbox-api==0.0.10 # homeassistant.components.sharkiq sharkiq==1.0.2 diff --git a/tests/components/sfr_box/snapshots/test_diagnostics.ambr b/tests/components/sfr_box/snapshots/test_diagnostics.ambr index 22a914f8a79..69139c2c374 100644 --- a/tests/components/sfr_box/snapshots/test_diagnostics.ambr +++ b/tests/components/sfr_box/snapshots/test_diagnostics.ambr @@ -31,7 +31,7 @@ 'product_id': 'NB6VAC-FXC-r0', 'refclient': '', 'serial_number': '**REDACTED**', - 'temperature': 27560, + 'temperature': 27560.0, 'uptime': 2353575, 'version_bootloader': 'NB6VAC-BOOTLOADER-R4.0.8', 'version_dsldriver': 'NB6VAC-XDSL-A2pv6F039p', @@ -90,7 +90,7 @@ 'product_id': 'NB6VAC-FXC-r0', 'refclient': '', 'serial_number': '**REDACTED**', - 'temperature': 27560, + 'temperature': 27560.0, 'uptime': 2353575, 'version_bootloader': 'NB6VAC-BOOTLOADER-R4.0.8', 'version_dsldriver': 'NB6VAC-XDSL-A2pv6F039p', From 0b95cf125137edb32c7770da3ec2c5afacdf9d69 Mon Sep 17 00:00:00 2001 From: TimL Date: Fri, 6 Sep 2024 23:46:08 +1000 Subject: [PATCH 217/271] Improve handling of old firmware versions (#125406) * Update Info fixture with new fields from pysmlight 0.0.14 * Create repair if device is running unsupported firmware * Add test for legacy firmware info * Add strings for repair issue --- .../components/smlight/coordinator.py | 22 +++++++++++- homeassistant/components/smlight/strings.json | 6 ++++ tests/components/smlight/fixtures/info.json | 4 ++- .../smlight/snapshots/test_init.ambr | 2 +- tests/components/smlight/test_init.py | 36 +++++++++++++++++-- 5 files changed, 65 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/smlight/coordinator.py b/homeassistant/components/smlight/coordinator.py index 6a29f14fafd..2c8f09766e7 100644 --- a/homeassistant/components/smlight/coordinator.py +++ b/homeassistant/components/smlight/coordinator.py @@ -9,8 +9,10 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryError +from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import format_mac +from homeassistant.helpers.issue_registry import IssueSeverity from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN, LOGGER, SCAN_INTERVAL @@ -40,6 +42,7 @@ class SmDataUpdateCoordinator(DataUpdateCoordinator[SmData]): self.unique_id: str | None = None self.client = Api2(host=host, session=async_get_clientsession(hass)) + self.legacy_api: int = 0 async def _async_setup(self) -> None: """Authenticate if needed during initial setup.""" @@ -60,11 +63,28 @@ class SmDataUpdateCoordinator(DataUpdateCoordinator[SmData]): info = await self.client.get_info() self.unique_id = format_mac(info.MAC) + if info.legacy_api: + self.legacy_api = info.legacy_api + ir.async_create_issue( + self.hass, + DOMAIN, + "unsupported_firmware", + is_fixable=False, + is_persistent=False, + learn_more_url="https://smlight.tech/flasher/#SLZB-06", + severity=IssueSeverity.ERROR, + translation_key="unsupported_firmware", + ) + async def _async_update_data(self) -> SmData: """Fetch data from the SMLIGHT device.""" try: + sensors = Sensors() + if not self.legacy_api: + sensors = await self.client.get_sensors() + return SmData( - sensors=await self.client.get_sensors(), + sensors=sensors, info=await self.client.get_info(), ) except SmlightConnectionError as err: diff --git a/homeassistant/components/smlight/strings.json b/homeassistant/components/smlight/strings.json index 02b9ebcc4e8..abe88caff85 100644 --- a/homeassistant/components/smlight/strings.json +++ b/homeassistant/components/smlight/strings.json @@ -45,5 +45,11 @@ "name": "RAM usage" } } + }, + "issues": { + "unsupported_firmware": { + "title": "SLZB core firmware update required", + "description": "Your SMLIGHT SLZB-06x device is running an unsupported core firmware version. Please update it to the latest version to enjoy all the features of this integration." + } } } diff --git a/tests/components/smlight/fixtures/info.json b/tests/components/smlight/fixtures/info.json index 72bb7c1ed9b..070232512f3 100644 --- a/tests/components/smlight/fixtures/info.json +++ b/tests/components/smlight/fixtures/info.json @@ -3,10 +3,12 @@ "device_ip": "192.168.1.161", "fs_total": 3456, "fw_channel": "dev", + "legacy_api": 0, + "hostname": "SLZB-06p7", "MAC": "AA:BB:CC:DD:EE:FF", "model": "SLZB-06p7", "ram_total": 296, - "sw_version": "v2.3.1.dev", + "sw_version": "v2.3.6", "wifi_mode": 0, "zb_flash_size": 704, "zb_hw": "CC2652P7", diff --git a/tests/components/smlight/snapshots/test_init.ambr b/tests/components/smlight/snapshots/test_init.ambr index 528a7b7b340..bb6a6c50f9b 100644 --- a/tests/components/smlight/snapshots/test_init.ambr +++ b/tests/components/smlight/snapshots/test_init.ambr @@ -27,7 +27,7 @@ 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, - 'sw_version': 'core: v2.3.1.dev / zigbee: -1', + 'sw_version': 'core: v2.3.6 / zigbee: -1', 'via_device_id': None, }) # --- diff --git a/tests/components/smlight/test_init.py b/tests/components/smlight/test_init.py index 682993cb943..d4b4b30d465 100644 --- a/tests/components/smlight/test_init.py +++ b/tests/components/smlight/test_init.py @@ -3,15 +3,17 @@ from unittest.mock import MagicMock from freezegun.api import FrozenDateTimeFactory -from pysmlight.exceptions import SmlightAuthError, SmlightConnectionError +from pysmlight import Info +from pysmlight.exceptions import SmlightAuthError, SmlightConnectionError, SmlightError import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components.smlight.const import SCAN_INTERVAL +from homeassistant.components.smlight.const import DOMAIN, SCAN_INTERVAL from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.issue_registry import IssueRegistry from .conftest import setup_integration @@ -92,3 +94,33 @@ async def test_device_info( ) assert device_entry is not None assert device_entry == snapshot + + +async def test_device_legacy_firmware( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_smlight_client: MagicMock, + device_registry: dr.DeviceRegistry, + issue_registry: IssueRegistry, +) -> None: + """Test device setup for old firmware version that dont support required API.""" + LEGACY_VERSION = "v2.3.1" + mock_smlight_client.get_sensors.side_effect = SmlightError + mock_smlight_client.get_info.return_value = Info( + legacy_api=1, sw_version=LEGACY_VERSION, MAC="AA:BB:CC:DD:EE:FF" + ) + entry = await setup_integration(hass, mock_config_entry) + + assert entry.unique_id == "aa:bb:cc:dd:ee:ff" + + device_entry = device_registry.async_get_device( + connections={(dr.CONNECTION_NETWORK_MAC, entry.unique_id)} + ) + assert LEGACY_VERSION in device_entry.sw_version + + issue = issue_registry.async_get_issue( + domain=DOMAIN, issue_id="unsupported_firmware" + ) + assert issue is not None + assert issue.domain == DOMAIN + assert issue.issue_id == "unsupported_firmware" From 5cf89bf2bbbae1790d710dabb30b6acff2a97e38 Mon Sep 17 00:00:00 2001 From: Marlon Date: Fri, 6 Sep 2024 16:52:32 +0200 Subject: [PATCH 218/271] Set min_power similar to max_power to support all inverters from apsystems (#124247) Set min_power similar to max_power to support all inverters from apsystems ez1 series --- homeassistant/components/apsystems/coordinator.py | 5 +++-- homeassistant/components/apsystems/number.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/apsystems/coordinator.py b/homeassistant/components/apsystems/coordinator.py index 6ba4f01dbc8..b6e951343f7 100644 --- a/homeassistant/components/apsystems/coordinator.py +++ b/homeassistant/components/apsystems/coordinator.py @@ -36,10 +36,11 @@ class ApSystemsDataCoordinator(DataUpdateCoordinator[ApSystemsSensorData]): async def _async_setup(self) -> None: try: - max_power = (await self.api.get_device_info()).maxPower + device_info = await self.api.get_device_info() except (ConnectionError, TimeoutError): raise UpdateFailed from None - self.api.max_power = max_power + self.api.max_power = device_info.maxPower + self.api.min_power = device_info.minPower async def _async_update_data(self) -> ApSystemsSensorData: output_data = await self.api.get_output_data() diff --git a/homeassistant/components/apsystems/number.py b/homeassistant/components/apsystems/number.py index 51e7130587f..01e991f5188 100644 --- a/homeassistant/components/apsystems/number.py +++ b/homeassistant/components/apsystems/number.py @@ -26,7 +26,6 @@ async def async_setup_entry( class ApSystemsMaxOutputNumber(ApSystemsEntity, NumberEntity): """Base sensor to be used with description.""" - _attr_native_min_value = 30 _attr_native_step = 1 _attr_device_class = NumberDeviceClass.POWER _attr_mode = NumberMode.BOX @@ -42,6 +41,7 @@ class ApSystemsMaxOutputNumber(ApSystemsEntity, NumberEntity): self._api = data.coordinator.api self._attr_unique_id = f"{data.device_id}_output_limit" self._attr_native_max_value = data.coordinator.api.max_power + self._attr_native_min_value = data.coordinator.api.min_power async def async_update(self) -> None: """Set the state with the value fetched from the inverter.""" From 1c7c6d6592d62280f08f20ba386372b8a0f96178 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Fri, 6 Sep 2024 14:49:58 +0200 Subject: [PATCH 219/271] Update frontend to 20240906.0 (#125409) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index fbdafe6025d..e40832e4733 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20240904.0"] + "requirements": ["home-assistant-frontend==20240906.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index fd878c1ffcf..1b9b4fa9ebf 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -31,7 +31,7 @@ habluetooth==3.4.0 hass-nabucasa==0.81.1 hassil==1.7.4 home-assistant-bluetooth==1.12.2 -home-assistant-frontend==20240904.0 +home-assistant-frontend==20240906.0 home-assistant-intents==2024.9.4 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index d2f60f1a0bf..a8977d706de 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1102,7 +1102,7 @@ hole==0.8.0 holidays==0.56 # homeassistant.components.frontend -home-assistant-frontend==20240904.0 +home-assistant-frontend==20240906.0 # homeassistant.components.conversation home-assistant-intents==2024.9.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 95919b5d9c9..5bee8d3c0e5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -925,7 +925,7 @@ hole==0.8.0 holidays==0.56 # homeassistant.components.frontend -home-assistant-frontend==20240904.0 +home-assistant-frontend==20240906.0 # homeassistant.components.conversation home-assistant-intents==2024.9.4 From b50d8fca16d4dae8d017d5b1a11c8af360dcc621 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Fri, 6 Sep 2024 15:43:16 +0200 Subject: [PATCH 220/271] Bump pyatv to 0.15.1 (#125412) --- homeassistant/components/apple_tv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/apple_tv/manifest.json b/homeassistant/components/apple_tv/manifest.json index 9a053829516..b4e1b354878 100644 --- a/homeassistant/components/apple_tv/manifest.json +++ b/homeassistant/components/apple_tv/manifest.json @@ -7,7 +7,7 @@ "documentation": "https://www.home-assistant.io/integrations/apple_tv", "iot_class": "local_push", "loggers": ["pyatv", "srptools"], - "requirements": ["pyatv==0.15.0"], + "requirements": ["pyatv==0.15.1"], "zeroconf": [ "_mediaremotetv._tcp.local.", "_companion-link._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index a8977d706de..ca4610d1ec2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1744,7 +1744,7 @@ pyatag==0.3.5.3 pyatmo==8.1.0 # homeassistant.components.apple_tv -pyatv==0.15.0 +pyatv==0.15.1 # homeassistant.components.aussie_broadband pyaussiebb==0.0.15 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5bee8d3c0e5..b80096cda54 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1412,7 +1412,7 @@ pyatag==0.3.5.3 pyatmo==8.1.0 # homeassistant.components.apple_tv -pyatv==0.15.0 +pyatv==0.15.1 # homeassistant.components.aussie_broadband pyaussiebb==0.0.15 From ed2d321746eeb80ad3e3c03a5dba933e41020f07 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 6 Sep 2024 14:57:08 +0000 Subject: [PATCH 221/271] Bump version to 2024.9.1 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5c61650ec32..49f4914e4b9 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "0" +PATCH_VERSION: Final = "1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 9a935b3a5fe..0af28ce0fe8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.9.0" +version = "2024.9.1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From dc189e1d586115bdacd1d0e5146768ef9083d0d3 Mon Sep 17 00:00:00 2001 From: Kristof Mattei <864376+kristof-mattei@users.noreply.github.com> Date: Tue, 10 Sep 2024 08:06:25 -0700 Subject: [PATCH 222/271] Fix Lyric climate Auto mode (#123490) fix: Lyric has an actual "Auto" mode that is exposed if the device has an Auto mode. --- homeassistant/components/lyric/climate.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/homeassistant/components/lyric/climate.py b/homeassistant/components/lyric/climate.py index bd9cf4997eb..22ab8ba57d4 100644 --- a/homeassistant/components/lyric/climate.py +++ b/homeassistant/components/lyric/climate.py @@ -208,10 +208,7 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity): if LYRIC_HVAC_MODE_COOL in device.allowed_modes: self._attr_hvac_modes.append(HVACMode.COOL) - if ( - LYRIC_HVAC_MODE_HEAT in device.allowed_modes - and LYRIC_HVAC_MODE_COOL in device.allowed_modes - ): + if LYRIC_HVAC_MODE_HEAT_COOL in device.allowed_modes: self._attr_hvac_modes.append(HVACMode.HEAT_COOL) # Setup supported features From b1d691178e98121c47070d140f4cb90400c2f7ad Mon Sep 17 00:00:00 2001 From: Simon <80467011+sorgfresser@users.noreply.github.com> Date: Tue, 10 Sep 2024 17:42:17 +0200 Subject: [PATCH 223/271] Use default voice id as fallback in get_tts_audio (#123624) --- homeassistant/components/elevenlabs/tts.py | 2 +- tests/components/elevenlabs/test_tts.py | 46 ++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/elevenlabs/tts.py b/homeassistant/components/elevenlabs/tts.py index 35ba6053cd8..40c35d07c06 100644 --- a/homeassistant/components/elevenlabs/tts.py +++ b/homeassistant/components/elevenlabs/tts.py @@ -100,7 +100,7 @@ class ElevenLabsTTSEntity(TextToSpeechEntity): """Load tts audio file from the engine.""" _LOGGER.debug("Getting TTS audio for %s", message) _LOGGER.debug("Options: %s", options) - voice_id = options[ATTR_VOICE] + voice_id = options.get(ATTR_VOICE, self._default_voice_id) try: audio = await self._client.generate( text=message, diff --git a/tests/components/elevenlabs/test_tts.py b/tests/components/elevenlabs/test_tts.py index 8b14ab26487..381993626d9 100644 --- a/tests/components/elevenlabs/test_tts.py +++ b/tests/components/elevenlabs/test_tts.py @@ -268,3 +268,49 @@ async def test_tts_service_speak_error( tts_entity._client.generate.assert_called_once_with( text="There is a person at the front door.", voice="voice1", model="model1" ) + + +@pytest.mark.parametrize( + ("setup", "tts_service", "service_data"), + [ + ( + "mock_config_entry_setup", + "speak", + { + ATTR_ENTITY_ID: "tts.mock_title", + tts.ATTR_MEDIA_PLAYER_ENTITY_ID: "media_player.something", + tts.ATTR_MESSAGE: "There is a person at the front door.", + tts.ATTR_OPTIONS: {}, + }, + ), + ], + indirect=["setup"], +) +async def test_tts_service_speak_without_options( + setup: AsyncMock, + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + calls: list[ServiceCall], + tts_service: str, + service_data: dict[str, Any], +) -> None: + """Test service call say with http response 200.""" + tts_entity = hass.data[tts.DOMAIN].get_entity(service_data[ATTR_ENTITY_ID]) + tts_entity._client.generate.reset_mock() + + await hass.services.async_call( + tts.DOMAIN, + tts_service, + service_data, + blocking=True, + ) + + assert len(calls) == 1 + assert ( + await retrieve_media(hass, hass_client, calls[0].data[ATTR_MEDIA_CONTENT_ID]) + == HTTPStatus.OK + ) + + tts_entity._client.generate.assert_called_once_with( + text="There is a person at the front door.", voice="voice1", model="model1" + ) From 73b26407f64b79d64b943cda77fca79cf71c27d1 Mon Sep 17 00:00:00 2001 From: David Knowles Date: Sun, 8 Sep 2024 11:39:23 -0400 Subject: [PATCH 224/271] Fix Schlage removed locks (#123627) * Fix bugs when a lock is no longer returned by the API * Changes requested during review * Only mark unavailable if lock is not present * Remove stale comment * Remove over-judicious nullability checks * Remove another unnecessary null check --- homeassistant/components/schlage/entity.py | 3 +- homeassistant/components/schlage/lock.py | 5 +- homeassistant/components/schlage/sensor.py | 5 +- .../components/schlage/test_binary_sensor.py | 34 +++++++++--- tests/components/schlage/test_lock.py | 54 +++++++++++++++++-- 5 files changed, 84 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/schlage/entity.py b/homeassistant/components/schlage/entity.py index 61bdbcb7730..cc4745e51cc 100644 --- a/homeassistant/components/schlage/entity.py +++ b/homeassistant/components/schlage/entity.py @@ -42,5 +42,4 @@ class SchlageEntity(CoordinatorEntity[SchlageDataUpdateCoordinator]): @property def available(self) -> bool: """Return if entity is available.""" - # When is_locked is None the lock is unavailable. - return super().available and self._lock.is_locked is not None + return super().available and self.device_id in self.coordinator.data.locks diff --git a/homeassistant/components/schlage/lock.py b/homeassistant/components/schlage/lock.py index 7e6f60211b0..59ce00e809a 100644 --- a/homeassistant/components/schlage/lock.py +++ b/homeassistant/components/schlage/lock.py @@ -42,8 +42,9 @@ class SchlageLockEntity(SchlageEntity, LockEntity): @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" - self._update_attrs() - return super()._handle_coordinator_update() + if self.device_id in self.coordinator.data.locks: + self._update_attrs() + super()._handle_coordinator_update() def _update_attrs(self) -> None: """Update our internal state attributes.""" diff --git a/homeassistant/components/schlage/sensor.py b/homeassistant/components/schlage/sensor.py index 2cf1694e111..8de09fa4cbb 100644 --- a/homeassistant/components/schlage/sensor.py +++ b/homeassistant/components/schlage/sensor.py @@ -64,5 +64,6 @@ class SchlageBatterySensor(SchlageEntity, SensorEntity): @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" - self._attr_native_value = getattr(self._lock, self.entity_description.key) - return super()._handle_coordinator_update() + if self.device_id in self.coordinator.data.locks: + self._attr_native_value = getattr(self._lock, self.entity_description.key) + super()._handle_coordinator_update() diff --git a/tests/components/schlage/test_binary_sensor.py b/tests/components/schlage/test_binary_sensor.py index 97f11577b86..dbbc5b07b87 100644 --- a/tests/components/schlage/test_binary_sensor.py +++ b/tests/components/schlage/test_binary_sensor.py @@ -3,37 +3,56 @@ from datetime import timedelta from unittest.mock import Mock +from freezegun.api import FrozenDateTimeFactory from pyschlage.exceptions import UnknownError from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_ON, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant -from homeassistant.util.dt import utcnow from tests.common import async_fire_time_changed async def test_keypad_disabled_binary_sensor( - hass: HomeAssistant, mock_lock: Mock, mock_added_config_entry: ConfigEntry + hass: HomeAssistant, + mock_schlage: Mock, + mock_lock: Mock, + mock_added_config_entry: ConfigEntry, + freezer: FrozenDateTimeFactory, ) -> None: """Test the keypad_disabled binary_sensor.""" mock_lock.keypad_disabled.reset_mock() mock_lock.keypad_disabled.return_value = True # Make the coordinator refresh data. - async_fire_time_changed(hass, utcnow() + timedelta(seconds=31)) + freezer.tick(timedelta(seconds=30)) + async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) keypad = hass.states.get("binary_sensor.vault_door_keypad_disabled") assert keypad is not None - assert keypad.state == "on" + assert keypad.state == STATE_ON assert keypad.attributes["device_class"] == BinarySensorDeviceClass.PROBLEM mock_lock.keypad_disabled.assert_called_once_with([]) + mock_schlage.locks.return_value = [] + # Make the coordinator refresh data. + freezer.tick(timedelta(seconds=30)) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + keypad = hass.states.get("binary_sensor.vault_door_keypad_disabled") + assert keypad is not None + assert keypad.state == STATE_UNAVAILABLE + async def test_keypad_disabled_binary_sensor_use_previous_logs_on_failure( - hass: HomeAssistant, mock_lock: Mock, mock_added_config_entry: ConfigEntry + hass: HomeAssistant, + mock_schlage: Mock, + mock_lock: Mock, + mock_added_config_entry: ConfigEntry, + freezer: FrozenDateTimeFactory, ) -> None: """Test the keypad_disabled binary_sensor.""" mock_lock.keypad_disabled.reset_mock() @@ -42,12 +61,13 @@ async def test_keypad_disabled_binary_sensor_use_previous_logs_on_failure( mock_lock.logs.side_effect = UnknownError("Cannot load logs") # Make the coordinator refresh data. - async_fire_time_changed(hass, utcnow() + timedelta(seconds=31)) + freezer.tick(timedelta(seconds=30)) + async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) keypad = hass.states.get("binary_sensor.vault_door_keypad_disabled") assert keypad is not None - assert keypad.state == "on" + assert keypad.state == STATE_ON assert keypad.attributes["device_class"] == BinarySensorDeviceClass.PROBLEM mock_lock.keypad_disabled.assert_called_once_with([]) diff --git a/tests/components/schlage/test_lock.py b/tests/components/schlage/test_lock.py index 6c06f124693..ab0f4f5d863 100644 --- a/tests/components/schlage/test_lock.py +++ b/tests/components/schlage/test_lock.py @@ -3,12 +3,20 @@ from datetime import timedelta from unittest.mock import Mock +from freezegun.api import FrozenDateTimeFactory + from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ENTITY_ID, SERVICE_LOCK, SERVICE_UNLOCK +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_LOCK, + SERVICE_UNLOCK, + STATE_JAMMED, + STATE_UNAVAILABLE, + STATE_UNLOCKED, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr -from homeassistant.util.dt import utcnow from tests.common import async_fire_time_changed @@ -26,6 +34,40 @@ async def test_lock_device_registry( assert device.manufacturer == "Schlage" +async def test_lock_attributes( + hass: HomeAssistant, + mock_added_config_entry: ConfigEntry, + mock_schlage: Mock, + mock_lock: Mock, + freezer: FrozenDateTimeFactory, +) -> None: + """Test lock attributes.""" + lock = hass.states.get("lock.vault_door") + assert lock is not None + assert lock.state == STATE_UNLOCKED + assert lock.attributes["changed_by"] == "thumbturn" + + mock_lock.is_locked = False + mock_lock.is_jammed = True + # Make the coordinator refresh data. + freezer.tick(timedelta(seconds=30)) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + lock = hass.states.get("lock.vault_door") + assert lock is not None + assert lock.state == STATE_JAMMED + + mock_schlage.locks.return_value = [] + # Make the coordinator refresh data. + freezer.tick(timedelta(seconds=30)) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + lock = hass.states.get("lock.vault_door") + assert lock is not None + assert lock.state == STATE_UNAVAILABLE + assert "changed_by" not in lock.attributes + + async def test_lock_services( hass: HomeAssistant, mock_lock: Mock, mock_added_config_entry: ConfigEntry ) -> None: @@ -52,14 +94,18 @@ async def test_lock_services( async def test_changed_by( - hass: HomeAssistant, mock_lock: Mock, mock_added_config_entry: ConfigEntry + hass: HomeAssistant, + mock_lock: Mock, + mock_added_config_entry: ConfigEntry, + freezer: FrozenDateTimeFactory, ) -> None: """Test population of the changed_by attribute.""" mock_lock.last_changed_by.reset_mock() mock_lock.last_changed_by.return_value = "access code - foo" # Make the coordinator refresh data. - async_fire_time_changed(hass, utcnow() + timedelta(seconds=31)) + freezer.tick(timedelta(seconds=30)) + async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) mock_lock.last_changed_by.assert_called_once_with() From 781342be406b70dec8e629b865525ee4b6927201 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Jaworski?= Date: Fri, 6 Sep 2024 16:59:14 +0200 Subject: [PATCH 225/271] Fix mired range in blebox color temp mode lights (#124258) * fix: use default mired range in belbox lights running in color temp mode * fix: ruff --- homeassistant/components/blebox/light.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/blebox/light.py b/homeassistant/components/blebox/light.py index 1f994db7243..34f9b24b17b 100644 --- a/homeassistant/components/blebox/light.py +++ b/homeassistant/components/blebox/light.py @@ -60,6 +60,9 @@ COLOR_MODE_MAP = { class BleBoxLightEntity(BleBoxEntity[blebox_uniapi.light.Light], LightEntity): """Representation of BleBox lights.""" + _attr_max_mireds = 370 # 1,000,000 divided by 2700 Kelvin = 370 Mireds + _attr_min_mireds = 154 # 1,000,000 divided by 6500 Kelvin = 154 Mireds + def __init__(self, feature: blebox_uniapi.light.Light) -> None: """Initialize a BleBox light.""" super().__init__(feature) @@ -87,12 +90,7 @@ class BleBoxLightEntity(BleBoxEntity[blebox_uniapi.light.Light], LightEntity): Set values to _attr_ibutes if needed. """ - color_mode_tmp = COLOR_MODE_MAP.get(self._feature.color_mode, ColorMode.ONOFF) - if color_mode_tmp == ColorMode.COLOR_TEMP: - self._attr_min_mireds = 1 - self._attr_max_mireds = 255 - - return color_mode_tmp + return COLOR_MODE_MAP.get(self._feature.color_mode, ColorMode.ONOFF) @property def supported_color_modes(self): From e7c48d58706bd4dcc6ad79b3e80668d011d8b756 Mon Sep 17 00:00:00 2001 From: Willem-Jan van Rootselaar Date: Fri, 30 Aug 2024 10:41:07 +0200 Subject: [PATCH 226/271] Update diagnostics for BSBLan (#124508) * update diagnostics to include static and make room for multiple coordinator data objects * fix mac address is not stored in config_entry but on device --- .../components/bsblan/diagnostics.py | 5 +- homeassistant/components/bsblan/entity.py | 6 +- .../bsblan/snapshots/test_diagnostics.ambr | 88 +++++++++++-------- 3 files changed, 60 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/bsblan/diagnostics.py b/homeassistant/components/bsblan/diagnostics.py index 3b42d47e1d3..b4ff67f4fbf 100644 --- a/homeassistant/components/bsblan/diagnostics.py +++ b/homeassistant/components/bsblan/diagnostics.py @@ -20,5 +20,8 @@ async def async_get_config_entry_diagnostics( return { "info": data.info.to_dict(), "device": data.device.to_dict(), - "state": data.coordinator.data.state.to_dict(), + "coordinator_data": { + "state": data.coordinator.data.state.to_dict(), + }, + "static": data.static.to_dict(), } diff --git a/homeassistant/components/bsblan/entity.py b/homeassistant/components/bsblan/entity.py index 0c507938794..252c397f4f2 100644 --- a/homeassistant/components/bsblan/entity.py +++ b/homeassistant/components/bsblan/entity.py @@ -22,10 +22,10 @@ class BSBLanEntity(CoordinatorEntity[BSBLanUpdateCoordinator]): def __init__(self, coordinator: BSBLanUpdateCoordinator, data: BSBLanData) -> None: """Initialize BSBLan entity.""" super().__init__(coordinator, data) - host = self.coordinator.config_entry.data["host"] - mac = self.coordinator.config_entry.data["mac"] + host = coordinator.config_entry.data["host"] + mac = data.device.MAC self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, data.device.MAC)}, + identifiers={(DOMAIN, mac)}, connections={(CONNECTION_NETWORK_MAC, format_mac(mac))}, name=data.device.name, manufacturer="BSBLAN Inc.", diff --git a/tests/components/bsblan/snapshots/test_diagnostics.ambr b/tests/components/bsblan/snapshots/test_diagnostics.ambr index b172d26c249..c9a82edf4e2 100644 --- a/tests/components/bsblan/snapshots/test_diagnostics.ambr +++ b/tests/components/bsblan/snapshots/test_diagnostics.ambr @@ -1,6 +1,52 @@ # serializer version: 1 # name: test_diagnostics dict({ + 'coordinator_data': dict({ + 'state': dict({ + 'current_temperature': dict({ + 'data_type': 0, + 'desc': '', + 'name': 'Room temp 1 actual value', + 'unit': '°C', + 'value': '18.6', + }), + 'hvac_action': dict({ + 'data_type': 1, + 'desc': 'Raumtemp’begrenzung', + 'name': 'Status heating circuit 1', + 'unit': '', + 'value': '122', + }), + 'hvac_mode': dict({ + 'data_type': 1, + 'desc': 'Komfort', + 'name': 'Operating mode', + 'unit': '', + 'value': 'heat', + }), + 'hvac_mode2': dict({ + 'data_type': 1, + 'desc': 'Reduziert', + 'name': 'Operating mode', + 'unit': '', + 'value': '2', + }), + 'room1_thermostat_mode': dict({ + 'data_type': 1, + 'desc': 'Kein Bedarf', + 'name': 'Raumthermostat 1', + 'unit': '', + 'value': '0', + }), + 'target_temperature': dict({ + 'data_type': 0, + 'desc': '', + 'name': 'Room temperature Comfort setpoint', + 'unit': '°C', + 'value': '18.5', + }), + }), + }), 'device': dict({ 'MAC': '00:80:41:19:69:90', 'name': 'BSB-LAN', @@ -30,48 +76,20 @@ 'value': 'RVS21.831F/127', }), }), - 'state': dict({ - 'current_temperature': dict({ + 'static': dict({ + 'max_temp': dict({ 'data_type': 0, 'desc': '', - 'name': 'Room temp 1 actual value', + 'name': 'Summer/winter changeover temp heat circuit 1', 'unit': '°C', - 'value': '18.6', + 'value': '20.0', }), - 'hvac_action': dict({ - 'data_type': 1, - 'desc': 'Raumtemp’begrenzung', - 'name': 'Status heating circuit 1', - 'unit': '', - 'value': '122', - }), - 'hvac_mode': dict({ - 'data_type': 1, - 'desc': 'Komfort', - 'name': 'Operating mode', - 'unit': '', - 'value': 'heat', - }), - 'hvac_mode2': dict({ - 'data_type': 1, - 'desc': 'Reduziert', - 'name': 'Operating mode', - 'unit': '', - 'value': '2', - }), - 'room1_thermostat_mode': dict({ - 'data_type': 1, - 'desc': 'Kein Bedarf', - 'name': 'Raumthermostat 1', - 'unit': '', - 'value': '0', - }), - 'target_temperature': dict({ + 'min_temp': dict({ 'data_type': 0, 'desc': '', - 'name': 'Room temperature Comfort setpoint', + 'name': 'Room temp frost protection setpoint', 'unit': '°C', - 'value': '18.5', + 'value': '8.0', }), }), }) From e6b4c2e70093caf3b136b385a90ee9a827bb6130 Mon Sep 17 00:00:00 2001 From: tmenguy Date: Sat, 7 Sep 2024 12:38:59 +0200 Subject: [PATCH 227/271] Fix renault plug state (#125421) * Added PlugState 3, that is coming with renault-api 0.2.7, it fixes #124682 HA ticket * Added PlugState 3, that is coming with renault-api 0.2.7, it fixes #124682 HA ticket --- .../components/renault/binary_sensor.py | 16 +++++++++---- homeassistant/components/renault/sensor.py | 8 ++++++- homeassistant/components/renault/strings.json | 1 + tests/components/renault/const.py | 24 ++++++++++++++++--- .../renault/snapshots/test_sensor.ambr | 12 ++++++++++ 5 files changed, 52 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/renault/binary_sensor.py b/homeassistant/components/renault/binary_sensor.py index 2041499b711..98c298761ce 100644 --- a/homeassistant/components/renault/binary_sensor.py +++ b/homeassistant/components/renault/binary_sensor.py @@ -28,7 +28,7 @@ class RenaultBinarySensorEntityDescription( """Class describing Renault binary sensor entities.""" on_key: str - on_value: StateType + on_value: StateType | list[StateType] async def async_setup_entry( @@ -58,6 +58,9 @@ class RenaultBinarySensor( """Return true if the binary sensor is on.""" if (data := self._get_data_attr(self.entity_description.on_key)) is None: return None + + if isinstance(self.entity_description.on_value, list): + return data in self.entity_description.on_value return data == self.entity_description.on_value @@ -68,7 +71,10 @@ BINARY_SENSOR_TYPES: tuple[RenaultBinarySensorEntityDescription, ...] = tuple( coordinator="battery", device_class=BinarySensorDeviceClass.PLUG, on_key="plugStatus", - on_value=PlugState.PLUGGED.value, + on_value=[ + PlugState.PLUGGED.value, + PlugState.PLUGGED_WAITING_FOR_CHARGE.value, + ], ), RenaultBinarySensorEntityDescription( key="charging", @@ -104,13 +110,13 @@ BINARY_SENSOR_TYPES: tuple[RenaultBinarySensorEntityDescription, ...] = tuple( ] + [ RenaultBinarySensorEntityDescription( - key=f"{door.replace(' ','_').lower()}_door_status", + key=f"{door.replace(' ', '_').lower()}_door_status", coordinator="lock_status", # On means open, Off means closed device_class=BinarySensorDeviceClass.DOOR, - on_key=f"doorStatus{door.replace(' ','')}", + on_key=f"doorStatus{door.replace(' ', '')}", on_value="open", - translation_key=f"{door.lower().replace(' ','_')}_door_status", + translation_key=f"{door.lower().replace(' ', '_')}_door_status", ) for door in ("Rear Left", "Rear Right", "Driver", "Passenger") ], diff --git a/homeassistant/components/renault/sensor.py b/homeassistant/components/renault/sensor.py index 5cb4ee333cc..78e64ae9acc 100644 --- a/homeassistant/components/renault/sensor.py +++ b/homeassistant/components/renault/sensor.py @@ -197,7 +197,13 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( translation_key="plug_state", device_class=SensorDeviceClass.ENUM, entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], - options=["unplugged", "plugged", "plug_error", "plug_unknown"], + options=[ + "unplugged", + "plugged", + "plugged_waiting_for_charge", + "plug_error", + "plug_unknown", + ], value_lambda=_get_plug_state_formatted, ), RenaultSensorEntityDescription( diff --git a/homeassistant/components/renault/strings.json b/homeassistant/components/renault/strings.json index 5217b4ff65a..54864387869 100644 --- a/homeassistant/components/renault/strings.json +++ b/homeassistant/components/renault/strings.json @@ -141,6 +141,7 @@ "state": { "unplugged": "Unplugged", "plugged": "Plugged in", + "plugged_waiting_for_charge": "Plugged in, waiting for charge", "plug_error": "Plug error", "plug_unknown": "Plug unknown" } diff --git a/tests/components/renault/const.py b/tests/components/renault/const.py index 19c40f6ec20..4e4fd23f311 100644 --- a/tests/components/renault/const.py +++ b/tests/components/renault/const.py @@ -246,7 +246,13 @@ MOCK_VEHICLES = { ATTR_DEVICE_CLASS: SensorDeviceClass.ENUM, ATTR_ENTITY_ID: "sensor.reg_number_plug_state", ATTR_ICON: "mdi:power-plug", - ATTR_OPTIONS: ["unplugged", "plugged", "plug_error", "plug_unknown"], + ATTR_OPTIONS: [ + "unplugged", + "plugged", + "plugged_waiting_for_charge", + "plug_error", + "plug_unknown", + ], ATTR_STATE: "plugged", ATTR_UNIQUE_ID: "vf1aaaaa555777999_plug_state", }, @@ -487,7 +493,13 @@ MOCK_VEHICLES = { ATTR_DEVICE_CLASS: SensorDeviceClass.ENUM, ATTR_ENTITY_ID: "sensor.reg_number_plug_state", ATTR_ICON: "mdi:power-plug-off", - ATTR_OPTIONS: ["unplugged", "plugged", "plug_error", "plug_unknown"], + ATTR_OPTIONS: [ + "unplugged", + "plugged", + "plugged_waiting_for_charge", + "plug_error", + "plug_unknown", + ], ATTR_STATE: "unplugged", ATTR_UNIQUE_ID: "vf1aaaaa555777999_plug_state", }, @@ -725,7 +737,13 @@ MOCK_VEHICLES = { ATTR_DEVICE_CLASS: SensorDeviceClass.ENUM, ATTR_ENTITY_ID: "sensor.reg_number_plug_state", ATTR_ICON: "mdi:power-plug", - ATTR_OPTIONS: ["unplugged", "plugged", "plug_error", "plug_unknown"], + ATTR_OPTIONS: [ + "unplugged", + "plugged", + "plugged_waiting_for_charge", + "plug_error", + "plug_unknown", + ], ATTR_STATE: "plugged", ATTR_UNIQUE_ID: "vf1aaaaa555777123_plug_state", }, diff --git a/tests/components/renault/snapshots/test_sensor.ambr b/tests/components/renault/snapshots/test_sensor.ambr index e4bb2d74297..39a52260c76 100644 --- a/tests/components/renault/snapshots/test_sensor.ambr +++ b/tests/components/renault/snapshots/test_sensor.ambr @@ -494,6 +494,7 @@ 'options': list([ 'unplugged', 'plugged', + 'plugged_waiting_for_charge', 'plug_error', 'plug_unknown', ]), @@ -921,6 +922,7 @@ 'options': list([ 'unplugged', 'plugged', + 'plugged_waiting_for_charge', 'plug_error', 'plug_unknown', ]), @@ -1249,6 +1251,7 @@ 'options': list([ 'unplugged', 'plugged', + 'plugged_waiting_for_charge', 'plug_error', 'plug_unknown', ]), @@ -1674,6 +1677,7 @@ 'options': list([ 'unplugged', 'plugged', + 'plugged_waiting_for_charge', 'plug_error', 'plug_unknown', ]), @@ -2000,6 +2004,7 @@ 'options': list([ 'unplugged', 'plugged', + 'plugged_waiting_for_charge', 'plug_error', 'plug_unknown', ]), @@ -2456,6 +2461,7 @@ 'options': list([ 'unplugged', 'plugged', + 'plugged_waiting_for_charge', 'plug_error', 'plug_unknown', ]), @@ -3104,6 +3110,7 @@ 'options': list([ 'unplugged', 'plugged', + 'plugged_waiting_for_charge', 'plug_error', 'plug_unknown', ]), @@ -3531,6 +3538,7 @@ 'options': list([ 'unplugged', 'plugged', + 'plugged_waiting_for_charge', 'plug_error', 'plug_unknown', ]), @@ -3859,6 +3867,7 @@ 'options': list([ 'unplugged', 'plugged', + 'plugged_waiting_for_charge', 'plug_error', 'plug_unknown', ]), @@ -4284,6 +4293,7 @@ 'options': list([ 'unplugged', 'plugged', + 'plugged_waiting_for_charge', 'plug_error', 'plug_unknown', ]), @@ -4610,6 +4620,7 @@ 'options': list([ 'unplugged', 'plugged', + 'plugged_waiting_for_charge', 'plug_error', 'plug_unknown', ]), @@ -5066,6 +5077,7 @@ 'options': list([ 'unplugged', 'plugged', + 'plugged_waiting_for_charge', 'plug_error', 'plug_unknown', ]), From 17402848f27d77c27e6f2e1e998a2275912b5e5f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 6 Sep 2024 19:35:57 -0500 Subject: [PATCH 228/271] Bump yalexs to 8.6.4 (#125442) adds a debounce to the updates to ensure we do not request the activities api too often if the websocket sends rapid updates fixes #125277 --- homeassistant/components/august/manifest.json | 2 +- homeassistant/components/yale/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index 6635a95f1cf..e2c35fc155f 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -24,5 +24,5 @@ "documentation": "https://www.home-assistant.io/integrations/august", "iot_class": "cloud_push", "loggers": ["pubnub", "yalexs"], - "requirements": ["yalexs==8.6.3", "yalexs-ble==2.4.3"] + "requirements": ["yalexs==8.6.4", "yalexs-ble==2.4.3"] } diff --git a/homeassistant/components/yale/manifest.json b/homeassistant/components/yale/manifest.json index fc93d259891..8b8095a0863 100644 --- a/homeassistant/components/yale/manifest.json +++ b/homeassistant/components/yale/manifest.json @@ -13,5 +13,5 @@ "documentation": "https://www.home-assistant.io/integrations/yale", "iot_class": "cloud_push", "loggers": ["socketio", "engineio", "yalexs"], - "requirements": ["yalexs==8.6.3", "yalexs-ble==2.4.3"] + "requirements": ["yalexs==8.6.4", "yalexs-ble==2.4.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index ca4610d1ec2..541036d3081 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2976,7 +2976,7 @@ yalexs-ble==2.4.3 # homeassistant.components.august # homeassistant.components.yale -yalexs==8.6.3 +yalexs==8.6.4 # homeassistant.components.yeelight yeelight==0.7.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b80096cda54..cdcb120315f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2356,7 +2356,7 @@ yalexs-ble==2.4.3 # homeassistant.components.august # homeassistant.components.yale -yalexs==8.6.3 +yalexs==8.6.4 # homeassistant.components.yeelight yeelight==0.7.14 From fe247a60ef10c4a5181b207f477f445818dcbe5b Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Sun, 8 Sep 2024 17:53:32 +1000 Subject: [PATCH 229/271] Bump aiolifx and aiolifx-themes to support more than 82 zones (#125487) Signed-off-by: Avi Miller --- homeassistant/components/lifx/manifest.json | 4 +- requirements_all.txt | 4 +- requirements_test_all.txt | 4 +- tests/components/lifx/__init__.py | 20 +++-- tests/components/lifx/test_diagnostics.py | 33 ++++++++ tests/components/lifx/test_light.py | 85 +++++++++++++-------- 6 files changed, 109 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/lifx/manifest.json b/homeassistant/components/lifx/manifest.json index 3ef70f16467..c7d8a27a1c7 100644 --- a/homeassistant/components/lifx/manifest.json +++ b/homeassistant/components/lifx/manifest.json @@ -48,8 +48,8 @@ "iot_class": "local_polling", "loggers": ["aiolifx", "aiolifx_effects", "bitstring"], "requirements": [ - "aiolifx==1.0.9", + "aiolifx==1.1.1", "aiolifx-effects==0.3.2", - "aiolifx-themes==0.5.0" + "aiolifx-themes==0.5.5" ] } diff --git a/requirements_all.txt b/requirements_all.txt index 541036d3081..c83f17b8d83 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -273,10 +273,10 @@ aiokef==0.2.16 aiolifx-effects==0.3.2 # homeassistant.components.lifx -aiolifx-themes==0.5.0 +aiolifx-themes==0.5.5 # homeassistant.components.lifx -aiolifx==1.0.9 +aiolifx==1.1.1 # homeassistant.components.livisi aiolivisi==0.0.19 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cdcb120315f..e426f7c3e4d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -255,10 +255,10 @@ aiokafka==0.10.0 aiolifx-effects==0.3.2 # homeassistant.components.lifx -aiolifx-themes==0.5.0 +aiolifx-themes==0.5.5 # homeassistant.components.lifx -aiolifx==1.0.9 +aiolifx==1.1.1 # homeassistant.components.livisi aiolivisi==0.0.19 diff --git a/tests/components/lifx/__init__.py b/tests/components/lifx/__init__.py index 432e7673db6..81b913da6ce 100644 --- a/tests/components/lifx/__init__.py +++ b/tests/components/lifx/__init__.py @@ -65,10 +65,13 @@ class MockLifxCommand: """Init command.""" self.bulb = bulb self.calls = [] - self.msg_kwargs = kwargs + self.msg_kwargs = { + k.removeprefix("msg_"): v for k, v in kwargs.items() if k.startswith("msg_") + } for k, v in kwargs.items(): - if k != "callb": - setattr(self.bulb, k, v) + if k.startswith("msg_") or k == "callb": + continue + setattr(self.bulb, k, v) def __call__(self, *args, **kwargs): """Call command.""" @@ -156,9 +159,16 @@ def _mocked_infrared_bulb() -> Light: def _mocked_light_strip() -> Light: bulb = _mocked_bulb() bulb.product = 31 # LIFX Z - bulb.color_zones = [MagicMock(), MagicMock()] + bulb.zones_count = 3 + bulb.color_zones = [MagicMock()] * 3 bulb.effect = {"effect": "MOVE", "speed": 3, "duration": 0, "direction": "RIGHT"} - bulb.get_color_zones = MockLifxCommand(bulb) + bulb.get_color_zones = MockLifxCommand( + bulb, + msg_seq_num=bulb.seq_next(), + msg_count=bulb.zones_count, + msg_index=0, + msg_color=bulb.color_zones, + ) bulb.set_color_zones = MockLifxCommand(bulb) bulb.get_multizone_effect = MockLifxCommand(bulb) bulb.set_multizone_effect = MockLifxCommand(bulb) diff --git a/tests/components/lifx/test_diagnostics.py b/tests/components/lifx/test_diagnostics.py index e3588dd3ed1..22e335612f8 100644 --- a/tests/components/lifx/test_diagnostics.py +++ b/tests/components/lifx/test_diagnostics.py @@ -9,6 +9,7 @@ from . import ( DEFAULT_ENTRY_TITLE, IP_ADDRESS, SERIAL, + MockLifxCommand, _mocked_bulb, _mocked_clean_bulb, _mocked_infrared_bulb, @@ -188,6 +189,22 @@ async def test_legacy_multizone_bulb_diagnostics( ) config_entry.add_to_hass(hass) bulb = _mocked_light_strip() + bulb.get_color_zones = MockLifxCommand( + bulb, + msg_seq_num=0, + msg_count=8, + msg_color=[ + (54612, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + ], + msg_index=0, + ) bulb.zones_count = 8 bulb.color_zones = [ (54612, 65535, 65535, 3500), @@ -302,6 +319,22 @@ async def test_multizone_bulb_diagnostics( config_entry.add_to_hass(hass) bulb = _mocked_light_strip() bulb.product = 38 + bulb.get_color_zones = MockLifxCommand( + bulb, + msg_seq_num=0, + msg_count=8, + msg_color=[ + (54612, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + ], + msg_index=0, + ) bulb.zones_count = 8 bulb.color_zones = [ (54612, 65535, 65535, 3500), diff --git a/tests/components/lifx/test_light.py b/tests/components/lifx/test_light.py index a642347b4e6..1ce7c69d7fa 100644 --- a/tests/components/lifx/test_light.py +++ b/tests/components/lifx/test_light.py @@ -192,15 +192,7 @@ async def test_light_strip(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, blocking=True, ) - call_dict = bulb.set_color_zones.calls[0][1] - call_dict.pop("callb") - assert call_dict == { - "apply": 0, - "color": [], - "duration": 0, - "end_index": 0, - "start_index": 0, - } + assert len(bulb.set_color_zones.calls) == 0 bulb.set_color_zones.reset_mock() await hass.services.async_call( @@ -209,15 +201,7 @@ async def test_light_strip(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (10, 30)}, blocking=True, ) - call_dict = bulb.set_color_zones.calls[0][1] - call_dict.pop("callb") - assert call_dict == { - "apply": 0, - "color": [], - "duration": 0, - "end_index": 0, - "start_index": 0, - } + assert len(bulb.set_color_zones.calls) == 0 bulb.set_color_zones.reset_mock() bulb.color_zones = [ @@ -238,7 +222,7 @@ async def test_light_strip(hass: HomeAssistant) -> None: blocking=True, ) # Single color uses the fast path - assert bulb.set_color.calls[0][0][0] == [1820, 19660, 65535, 3500] + assert bulb.set_color.calls[1][0][0] == [1820, 19660, 65535, 3500] bulb.set_color.reset_mock() assert len(bulb.set_color_zones.calls) == 0 @@ -422,7 +406,9 @@ async def test_light_strip(hass: HomeAssistant) -> None: blocking=True, ) - bulb.get_color_zones = MockLifxCommand(bulb) + bulb.get_color_zones = MockLifxCommand( + bulb, msg_seq_num=0, msg_color=[0, 0, 65535, 3500] * 3, msg_index=0, msg_count=3 + ) bulb.get_color = MockFailingLifxCommand(bulb) with pytest.raises(HomeAssistantError): @@ -587,14 +573,14 @@ async def test_extended_multizone_messages(hass: HomeAssistant) -> None: bulb.set_extended_color_zones.reset_mock() bulb.color_zones = [ - (0, 65535, 65535, 3500), - (54612, 65535, 65535, 3500), - (54612, 65535, 65535, 3500), - (54612, 65535, 65535, 3500), - (46420, 65535, 65535, 3500), - (46420, 65535, 65535, 3500), - (46420, 65535, 65535, 3500), - (46420, 65535, 65535, 3500), + [0, 65535, 65535, 3500], + [54612, 65535, 65535, 3500], + [54612, 65535, 65535, 3500], + [54612, 65535, 65535, 3500], + [46420, 65535, 65535, 3500], + [46420, 65535, 65535, 3500], + [46420, 65535, 65535, 3500], + [46420, 65535, 65535, 3500], ] await hass.services.async_call( @@ -1308,7 +1294,11 @@ async def test_config_zoned_light_strip_fails( def __call__(self, callb=None, *args, **kwargs): """Call command.""" self.call_count += 1 - response = None if self.call_count >= 2 else MockMessage() + response = ( + None + if self.call_count >= 2 + else MockMessage(seq_num=0, color=[], index=0, count=0) + ) if callb: callb(self.bulb, response) @@ -1349,7 +1339,15 @@ async def test_legacy_zoned_light_strip( self.call_count += 1 self.bulb.color_zones = [None] * 12 if callb: - callb(self.bulb, MockMessage()) + callb( + self.bulb, + MockMessage( + seq_num=0, + index=0, + count=self.bulb.zones_count, + color=self.bulb.color_zones, + ), + ) get_color_zones_mock = MockPopulateLifxZonesCommand(light_strip) light_strip.get_color_zones = get_color_zones_mock @@ -1946,6 +1944,33 @@ async def test_light_strip_zones_not_populated_yet(hass: HomeAssistant) -> None: bulb.power_level = 65535 bulb.color_zones = None bulb.color = [65535, 65535, 65535, 65535] + bulb.get_color_zones = next( + iter( + [ + MockLifxCommand( + bulb, + msg_seq_num=0, + msg_color=[0, 0, 65535, 3500] * 8, + msg_index=0, + msg_count=16, + ), + MockLifxCommand( + bulb, + msg_seq_num=1, + msg_color=[0, 0, 65535, 3500] * 8, + msg_index=0, + msg_count=16, + ), + MockLifxCommand( + bulb, + msg_seq_num=2, + msg_color=[0, 0, 65535, 3500] * 8, + msg_index=8, + msg_count=16, + ), + ] + ) + ) assert bulb.get_color_zones.calls == [] with ( From 0b1a898c7c4cb23bfdd0228529e2ef2ecd0b0b6b Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 8 Sep 2024 14:06:40 +0200 Subject: [PATCH 230/271] Fix yale_smart_alarm on missing key (#125508) --- .../yale_smart_alarm/coordinator.py | 13 +- tests/components/yale_smart_alarm/conftest.py | 24 +- .../snapshots/test_diagnostics.ambr | 1644 +++++++++-------- .../components/yale_smart_alarm/test_lock.py | 6 +- 4 files changed, 854 insertions(+), 833 deletions(-) diff --git a/homeassistant/components/yale_smart_alarm/coordinator.py b/homeassistant/components/yale_smart_alarm/coordinator.py index b47545ea88b..3bfd13b2152 100644 --- a/homeassistant/components/yale_smart_alarm/coordinator.py +++ b/homeassistant/components/yale_smart_alarm/coordinator.py @@ -154,10 +154,15 @@ class YaleDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): except YALE_BASE_ERRORS as error: raise UpdateFailed from error + cycle = data.cycle["data"] if data.cycle else None + status = data.status["data"] if data.status else None + online = data.online["data"] if data.online else None + panel_info = data.panel_info["data"] if data.panel_info else None + return { "arm_status": arm_status, - "cycle": data.cycle, - "status": data.status, - "online": data.online, - "panel_info": data.panel_info, + "cycle": cycle, + "status": status, + "online": online, + "panel_info": panel_info, } diff --git a/tests/components/yale_smart_alarm/conftest.py b/tests/components/yale_smart_alarm/conftest.py index 6ac6dfc6871..0499b6212d6 100644 --- a/tests/components/yale_smart_alarm/conftest.py +++ b/tests/components/yale_smart_alarm/conftest.py @@ -82,10 +82,10 @@ def get_fixture_data() -> dict[str, Any]: def get_update_data(loaded_fixture: dict[str, Any]) -> YaleSmartAlarmData: """Load update data and return.""" - status = loaded_fixture["STATUS"] - cycle = loaded_fixture["CYCLE"] - online = loaded_fixture["ONLINE"] - panel_info = loaded_fixture["PANEL INFO"] + status = {"data": loaded_fixture["STATUS"]} + cycle = {"data": loaded_fixture["CYCLE"]} + online = {"data": loaded_fixture["ONLINE"]} + panel_info = {"data": loaded_fixture["PANEL INFO"]} return YaleSmartAlarmData( status=status, cycle=cycle, @@ -98,14 +98,14 @@ def get_update_data(loaded_fixture: dict[str, Any]) -> YaleSmartAlarmData: def get_diag_data(loaded_fixture: dict[str, Any]) -> YaleSmartAlarmData: """Load all data and return.""" - devices = loaded_fixture["DEVICES"] - mode = loaded_fixture["MODE"] - status = loaded_fixture["STATUS"] - cycle = loaded_fixture["CYCLE"] - online = loaded_fixture["ONLINE"] - history = loaded_fixture["HISTORY"] - panel_info = loaded_fixture["PANEL INFO"] - auth_check = loaded_fixture["AUTH CHECK"] + devices = {"data": loaded_fixture["DEVICES"]} + mode = {"data": loaded_fixture["MODE"]} + status = {"data": loaded_fixture["STATUS"]} + cycle = {"data": loaded_fixture["CYCLE"]} + online = {"data": loaded_fixture["ONLINE"]} + history = {"data": loaded_fixture["HISTORY"]} + panel_info = {"data": loaded_fixture["PANEL INFO"]} + auth_check = {"data": loaded_fixture["AUTH CHECK"]} return YaleSmartAlarmData( devices=devices, mode=mode, diff --git a/tests/components/yale_smart_alarm/snapshots/test_diagnostics.ambr b/tests/components/yale_smart_alarm/snapshots/test_diagnostics.ambr index d4bbd42aaeb..750430b529a 100644 --- a/tests/components/yale_smart_alarm/snapshots/test_diagnostics.ambr +++ b/tests/components/yale_smart_alarm/snapshots/test_diagnostics.ambr @@ -2,27 +2,661 @@ # name: test_diagnostics dict({ 'auth_check': dict({ - 'agent': False, - 'dealer_group': 'yale', - 'dealer_id': '605', - 'first_login': '1', - 'id': '**REDACTED**', - 'is_auth': '1', - 'mac': '**REDACTED**', - 'mail_address': '**REDACTED**', - 'master': '1', - 'name': '**REDACTED**', - 'token_time': '2023-08-17 16:19:20', - 'user_id': '**REDACTED**', - 'xml_version': '2', + 'data': dict({ + 'agent': False, + 'dealer_group': 'yale', + 'dealer_id': '605', + 'first_login': '1', + 'id': '**REDACTED**', + 'is_auth': '1', + 'mac': '**REDACTED**', + 'mail_address': '**REDACTED**', + 'master': '1', + 'name': '**REDACTED**', + 'token_time': '2023-08-17 16:19:20', + 'user_id': '**REDACTED**', + 'xml_version': '2', + }), }), 'cycle': dict({ - 'alarm_event_latest': None, - 'capture_latest': None, - 'device_status': list([ + 'data': dict({ + 'alarm_event_latest': None, + 'capture_latest': None, + 'device_status': list([ + dict({ + '_state': 'locked', + '_state2': 'closed', + 'address': '**REDACTED**', + 'area': '1', + 'bypass': '0', + 'device_group': '002', + 'device_id': '**REDACTED**', + 'device_id2': '', + 'extension': None, + 'group_id': None, + 'group_name': None, + 'ipcam_trigger_by_zone1': None, + 'ipcam_trigger_by_zone2': None, + 'ipcam_trigger_by_zone3': None, + 'ipcam_trigger_by_zone4': None, + 'mac': '**REDACTED**', + 'minigw_configuration_data': '02FF000001000000000000000000001E000100', + 'minigw_lock_status': '35', + 'minigw_number_of_credentials_supported': '10', + 'minigw_product_data': '21020120', + 'minigw_protocol': 'DM', + 'minigw_syncing': '0', + 'name': '**REDACTED**', + 'no': '1', + 'rf': None, + 'rssi': '9', + 'scene_restore': None, + 'scene_trigger': '0', + 'sresp_button_1': None, + 'sresp_button_2': None, + 'sresp_button_3': None, + 'sresp_button_4': None, + 'status1': 'device_status.lock', + 'status2': None, + 'status_dim_level': None, + 'status_fault': list([ + ]), + 'status_hue': None, + 'status_humi': None, + 'status_lux': '', + 'status_open': list([ + 'device_status.lock', + ]), + 'status_power': None, + 'status_saturation': None, + 'status_switch': None, + 'status_temp': None, + 'status_temp_format': 'C', + 'status_total_energy': None, + 'thermo_c_setpoint': None, + 'thermo_c_setpoint_away': None, + 'thermo_fan_mode': None, + 'thermo_mode': None, + 'thermo_schd_setting': None, + 'thermo_setpoint': None, + 'thermo_setpoint_away': None, + 'trigger_by_zone': list([ + ]), + 'type': 'device_type.door_lock', + 'type_no': '72', + }), + dict({ + '_state': 'unlocked', + '_state2': 'unknown', + 'address': '**REDACTED**', + 'area': '1', + 'bypass': '0', + 'device_group': '002', + 'device_id': '**REDACTED**', + 'device_id2': '', + 'extension': None, + 'group_id': None, + 'group_name': None, + 'ipcam_trigger_by_zone1': None, + 'ipcam_trigger_by_zone2': None, + 'ipcam_trigger_by_zone3': None, + 'ipcam_trigger_by_zone4': None, + 'mac': '**REDACTED**', + 'minigw_configuration_data': '02FF000001000000000000000000001E000100', + 'minigw_lock_status': None, + 'minigw_number_of_credentials_supported': '10', + 'minigw_product_data': '21020120', + 'minigw_protocol': 'DM', + 'minigw_syncing': '0', + 'name': '**REDACTED**', + 'no': '2', + 'rf': None, + 'rssi': '9', + 'scene_restore': None, + 'scene_trigger': '0', + 'sresp_button_1': None, + 'sresp_button_2': None, + 'sresp_button_3': None, + 'sresp_button_4': None, + 'status1': 'device_status.unlock', + 'status2': None, + 'status_dim_level': None, + 'status_fault': list([ + ]), + 'status_hue': None, + 'status_humi': None, + 'status_lux': '', + 'status_open': list([ + ]), + 'status_power': None, + 'status_saturation': None, + 'status_switch': None, + 'status_temp': None, + 'status_temp_format': 'C', + 'status_total_energy': None, + 'thermo_c_setpoint': None, + 'thermo_c_setpoint_away': None, + 'thermo_fan_mode': None, + 'thermo_mode': None, + 'thermo_schd_setting': None, + 'thermo_setpoint': None, + 'thermo_setpoint_away': None, + 'trigger_by_zone': list([ + ]), + 'type': 'device_type.door_lock', + 'type_no': '72', + }), + dict({ + '_state': 'locked', + '_state2': 'unknown', + 'address': '**REDACTED**', + 'area': '1', + 'bypass': '0', + 'device_group': '002', + 'device_id': '**REDACTED**', + 'device_id2': '', + 'extension': None, + 'group_id': None, + 'group_name': None, + 'ipcam_trigger_by_zone1': None, + 'ipcam_trigger_by_zone2': None, + 'ipcam_trigger_by_zone3': None, + 'ipcam_trigger_by_zone4': None, + 'mac': '**REDACTED**', + 'minigw_configuration_data': '02FF000001000000000000000000001E000100', + 'minigw_lock_status': None, + 'minigw_number_of_credentials_supported': '10', + 'minigw_product_data': '21020120', + 'minigw_protocol': 'DM', + 'minigw_syncing': '0', + 'name': '**REDACTED**', + 'no': '3', + 'rf': None, + 'rssi': '9', + 'scene_restore': None, + 'scene_trigger': '0', + 'sresp_button_1': None, + 'sresp_button_2': None, + 'sresp_button_3': None, + 'sresp_button_4': None, + 'status1': 'device_status.lock', + 'status2': None, + 'status_dim_level': None, + 'status_fault': list([ + ]), + 'status_hue': None, + 'status_humi': None, + 'status_lux': '', + 'status_open': list([ + 'device_status.lock', + ]), + 'status_power': None, + 'status_saturation': None, + 'status_switch': None, + 'status_temp': None, + 'status_temp_format': 'C', + 'status_total_energy': None, + 'thermo_c_setpoint': None, + 'thermo_c_setpoint_away': None, + 'thermo_fan_mode': None, + 'thermo_mode': None, + 'thermo_schd_setting': None, + 'thermo_setpoint': None, + 'thermo_setpoint_away': None, + 'trigger_by_zone': list([ + ]), + 'type': 'device_type.door_lock', + 'type_no': '72', + }), + dict({ + '_state': 'closed', + 'address': '**REDACTED**', + 'area': '1', + 'bypass': '0', + 'device_group': '000', + 'device_id': '**REDACTED**', + 'device_id2': '', + 'extension': None, + 'group_id': None, + 'group_name': None, + 'ipcam_trigger_by_zone1': None, + 'ipcam_trigger_by_zone2': None, + 'ipcam_trigger_by_zone3': None, + 'ipcam_trigger_by_zone4': None, + 'mac': '**REDACTED**', + 'minigw_configuration_data': '', + 'minigw_lock_status': '', + 'minigw_number_of_credentials_supported': '', + 'minigw_product_data': '', + 'minigw_protocol': '', + 'minigw_syncing': '', + 'name': '**REDACTED**', + 'no': '4', + 'rf': None, + 'rssi': '0', + 'scene_restore': None, + 'scene_trigger': '0', + 'sresp_button_1': None, + 'sresp_button_2': None, + 'sresp_button_3': None, + 'sresp_button_4': None, + 'status1': 'device_status.dc_close', + 'status2': None, + 'status_dim_level': None, + 'status_fault': list([ + ]), + 'status_hue': None, + 'status_humi': None, + 'status_lux': '', + 'status_open': list([ + 'device_status.dc_close', + ]), + 'status_power': None, + 'status_saturation': None, + 'status_switch': None, + 'status_temp': None, + 'status_temp_format': 'C', + 'status_total_energy': None, + 'thermo_c_setpoint': None, + 'thermo_c_setpoint_away': None, + 'thermo_fan_mode': None, + 'thermo_mode': None, + 'thermo_schd_setting': None, + 'thermo_setpoint': None, + 'thermo_setpoint_away': None, + 'trigger_by_zone': list([ + ]), + 'type': 'device_type.door_contact', + 'type_no': '4', + }), + dict({ + '_state': 'open', + 'address': '**REDACTED**', + 'area': '1', + 'bypass': '0', + 'device_group': '000', + 'device_id': '**REDACTED**', + 'device_id2': '', + 'extension': None, + 'group_id': None, + 'group_name': None, + 'ipcam_trigger_by_zone1': None, + 'ipcam_trigger_by_zone2': None, + 'ipcam_trigger_by_zone3': None, + 'ipcam_trigger_by_zone4': None, + 'mac': '**REDACTED**', + 'minigw_configuration_data': '', + 'minigw_lock_status': '', + 'minigw_number_of_credentials_supported': '', + 'minigw_product_data': '', + 'minigw_protocol': '', + 'minigw_syncing': '', + 'name': '**REDACTED**', + 'no': '5', + 'rf': None, + 'rssi': '0', + 'scene_restore': None, + 'scene_trigger': '0', + 'sresp_button_1': None, + 'sresp_button_2': None, + 'sresp_button_3': None, + 'sresp_button_4': None, + 'status1': 'device_status.dc_open', + 'status2': None, + 'status_dim_level': None, + 'status_fault': list([ + ]), + 'status_hue': None, + 'status_humi': None, + 'status_lux': '', + 'status_open': list([ + 'device_status.dc_open', + ]), + 'status_power': None, + 'status_saturation': None, + 'status_switch': None, + 'status_temp': None, + 'status_temp_format': 'C', + 'status_total_energy': None, + 'thermo_c_setpoint': None, + 'thermo_c_setpoint_away': None, + 'thermo_fan_mode': None, + 'thermo_mode': None, + 'thermo_schd_setting': None, + 'thermo_setpoint': None, + 'thermo_setpoint_away': None, + 'trigger_by_zone': list([ + ]), + 'type': 'device_type.door_contact', + 'type_no': '4', + }), + dict({ + '_state': 'unavailable', + 'address': '**REDACTED**', + 'area': '1', + 'bypass': '0', + 'device_group': '000', + 'device_id': '**REDACTED**', + 'device_id2': '', + 'extension': None, + 'group_id': None, + 'group_name': None, + 'ipcam_trigger_by_zone1': None, + 'ipcam_trigger_by_zone2': None, + 'ipcam_trigger_by_zone3': None, + 'ipcam_trigger_by_zone4': None, + 'mac': '**REDACTED**', + 'minigw_configuration_data': '', + 'minigw_lock_status': '', + 'minigw_number_of_credentials_supported': '', + 'minigw_product_data': '', + 'minigw_protocol': '', + 'minigw_syncing': '', + 'name': '**REDACTED**', + 'no': '6', + 'rf': None, + 'rssi': '0', + 'scene_restore': None, + 'scene_trigger': '0', + 'sresp_button_1': None, + 'sresp_button_2': None, + 'sresp_button_3': None, + 'sresp_button_4': None, + 'status1': 'unknwon', + 'status2': None, + 'status_dim_level': None, + 'status_fault': list([ + ]), + 'status_hue': None, + 'status_humi': None, + 'status_lux': '', + 'status_open': list([ + ]), + 'status_power': None, + 'status_saturation': None, + 'status_switch': None, + 'status_temp': None, + 'status_temp_format': 'C', + 'status_total_energy': None, + 'thermo_c_setpoint': None, + 'thermo_c_setpoint_away': None, + 'thermo_fan_mode': None, + 'thermo_mode': None, + 'thermo_schd_setting': None, + 'thermo_setpoint': None, + 'thermo_setpoint_away': None, + 'trigger_by_zone': list([ + ]), + 'type': 'device_type.door_contact', + 'type_no': '4', + }), + dict({ + '_state': 'unlocked', + '_state2': 'closed', + 'address': '**REDACTED**', + 'area': '1', + 'bypass': '0', + 'device_group': '002', + 'device_id': '**REDACTED**', + 'device_id2': '', + 'extension': None, + 'group_id': None, + 'group_name': None, + 'ipcam_trigger_by_zone1': None, + 'ipcam_trigger_by_zone2': None, + 'ipcam_trigger_by_zone3': None, + 'ipcam_trigger_by_zone4': None, + 'mac': '**REDACTED**', + 'minigw_configuration_data': '02FF000001000000000000000000001E000100', + 'minigw_lock_status': '36', + 'minigw_number_of_credentials_supported': '10', + 'minigw_product_data': '21020120', + 'minigw_protocol': 'DM', + 'minigw_syncing': '0', + 'name': '**REDACTED**', + 'no': '7', + 'rf': None, + 'rssi': '9', + 'scene_restore': None, + 'scene_trigger': '0', + 'sresp_button_1': None, + 'sresp_button_2': None, + 'sresp_button_3': None, + 'sresp_button_4': None, + 'status1': 'device_status.lock', + 'status2': None, + 'status_dim_level': None, + 'status_fault': list([ + ]), + 'status_hue': None, + 'status_humi': None, + 'status_lux': '', + 'status_open': list([ + 'device_status.lock', + ]), + 'status_power': None, + 'status_saturation': None, + 'status_switch': None, + 'status_temp': None, + 'status_temp_format': 'C', + 'status_total_energy': None, + 'thermo_c_setpoint': None, + 'thermo_c_setpoint_away': None, + 'thermo_fan_mode': None, + 'thermo_mode': None, + 'thermo_schd_setting': None, + 'thermo_setpoint': None, + 'thermo_setpoint_away': None, + 'trigger_by_zone': list([ + ]), + 'type': 'device_type.door_lock', + 'type_no': '72', + }), + dict({ + '_state': 'unlocked', + '_state2': 'open', + 'address': '**REDACTED**', + 'area': '1', + 'bypass': '0', + 'device_group': '002', + 'device_id': '**REDACTED**', + 'device_id2': '', + 'extension': None, + 'group_id': None, + 'group_name': None, + 'ipcam_trigger_by_zone1': None, + 'ipcam_trigger_by_zone2': None, + 'ipcam_trigger_by_zone3': None, + 'ipcam_trigger_by_zone4': None, + 'mac': '**REDACTED**', + 'minigw_configuration_data': '02FF000001000000000000000000001E000100', + 'minigw_lock_status': '4', + 'minigw_number_of_credentials_supported': '10', + 'minigw_product_data': '21020120', + 'minigw_protocol': 'DM', + 'minigw_syncing': '0', + 'name': '**REDACTED**', + 'no': '8', + 'rf': None, + 'rssi': '9', + 'scene_restore': None, + 'scene_trigger': '0', + 'sresp_button_1': None, + 'sresp_button_2': None, + 'sresp_button_3': None, + 'sresp_button_4': None, + 'status1': 'device_status.unlock', + 'status2': None, + 'status_dim_level': None, + 'status_fault': list([ + ]), + 'status_hue': None, + 'status_humi': None, + 'status_lux': '', + 'status_open': list([ + 'device_status.unlock', + ]), + 'status_power': None, + 'status_saturation': None, + 'status_switch': None, + 'status_temp': None, + 'status_temp_format': 'C', + 'status_total_energy': None, + 'thermo_c_setpoint': None, + 'thermo_c_setpoint_away': None, + 'thermo_fan_mode': None, + 'thermo_mode': None, + 'thermo_schd_setting': None, + 'thermo_setpoint': None, + 'thermo_setpoint_away': None, + 'trigger_by_zone': list([ + ]), + 'type': 'device_type.door_lock', + 'type_no': '72', + }), + dict({ + '_state': 'unavailable', + 'address': '**REDACTED**', + 'area': '1', + 'bypass': '0', + 'device_group': '002', + 'device_id': '**REDACTED**', + 'device_id2': '', + 'extension': None, + 'group_id': None, + 'group_name': None, + 'ipcam_trigger_by_zone1': None, + 'ipcam_trigger_by_zone2': None, + 'ipcam_trigger_by_zone3': None, + 'ipcam_trigger_by_zone4': None, + 'mac': '**REDACTED**', + 'minigw_configuration_data': '02FF000001000000000000000000001E000100', + 'minigw_lock_status': '10', + 'minigw_number_of_credentials_supported': '10', + 'minigw_product_data': '21020120', + 'minigw_protocol': 'DM', + 'minigw_syncing': '0', + 'name': '**REDACTED**', + 'no': '9', + 'rf': None, + 'rssi': '9', + 'scene_restore': None, + 'scene_trigger': '0', + 'sresp_button_1': None, + 'sresp_button_2': None, + 'sresp_button_3': None, + 'sresp_button_4': None, + 'status1': 'device_status.error', + 'status2': None, + 'status_dim_level': None, + 'status_fault': list([ + ]), + 'status_hue': None, + 'status_humi': None, + 'status_lux': '', + 'status_open': list([ + 'device_status.error', + ]), + 'status_power': None, + 'status_saturation': None, + 'status_switch': None, + 'status_temp': None, + 'status_temp_format': 'C', + 'status_total_energy': None, + 'thermo_c_setpoint': None, + 'thermo_c_setpoint_away': None, + 'thermo_fan_mode': None, + 'thermo_mode': None, + 'thermo_schd_setting': None, + 'thermo_setpoint': None, + 'thermo_setpoint_away': None, + 'trigger_by_zone': list([ + ]), + 'type': 'device_type.door_lock', + 'type_no': '72', + }), + dict({ + 'address': '**REDACTED**', + 'area': '1', + 'bypass': '0', + 'device_group': '001', + 'device_id': '**REDACTED**', + 'device_id2': '', + 'extension': None, + 'group_id': None, + 'group_name': None, + 'ipcam_trigger_by_zone1': None, + 'ipcam_trigger_by_zone2': None, + 'ipcam_trigger_by_zone3': None, + 'ipcam_trigger_by_zone4': None, + 'mac': '**REDACTED**', + 'minigw_configuration_data': '', + 'minigw_lock_status': '', + 'minigw_number_of_credentials_supported': '', + 'minigw_product_data': '', + 'minigw_protocol': '', + 'minigw_syncing': '', + 'name': '**REDACTED**', + 'no': '8', + 'rf': None, + 'rssi': '9', + 'scene_restore': None, + 'scene_trigger': '0', + 'sresp_button_1': None, + 'sresp_button_2': None, + 'sresp_button_3': None, + 'sresp_button_4': None, + 'status1': '', + 'status2': None, + 'status_dim_level': None, + 'status_fault': list([ + ]), + 'status_hue': None, + 'status_humi': None, + 'status_lux': '', + 'status_open': list([ + ]), + 'status_power': None, + 'status_saturation': None, + 'status_switch': None, + 'status_temp': 21, + 'status_temp_format': 'C', + 'status_total_energy': None, + 'thermo_c_setpoint': None, + 'thermo_c_setpoint_away': None, + 'thermo_fan_mode': None, + 'thermo_mode': None, + 'thermo_schd_setting': None, + 'thermo_setpoint': None, + 'thermo_setpoint_away': None, + 'trigger_by_zone': list([ + ]), + 'type': 'device_type.temperature_sensor', + 'type_no': '40', + }), + ]), + 'model': list([ + dict({ + 'area': '1', + 'mode': 'disarm', + }), + ]), + 'panel_status': dict({ + 'warning_snd_mute': '0', + }), + 'report_event_latest': dict({ + 'cid_code': '1807', + 'event_time': None, + 'id': '**REDACTED**', + 'report_id': '1027299996', + 'time': '1692271914', + 'utc_event_time': None, + }), + }), + }), + 'devices': dict({ + 'data': list([ dict({ - '_state': 'locked', - '_state2': 'closed', 'address': '**REDACTED**', 'area': '1', 'bypass': '0', @@ -83,8 +717,6 @@ 'type_no': '72', }), dict({ - '_state': 'unlocked', - '_state2': 'unknown', 'address': '**REDACTED**', 'area': '1', 'bypass': '0', @@ -144,8 +776,6 @@ 'type_no': '72', }), dict({ - '_state': 'locked', - '_state2': 'unknown', 'address': '**REDACTED**', 'area': '1', 'bypass': '0', @@ -206,7 +836,6 @@ 'type_no': '72', }), dict({ - '_state': 'closed', 'address': '**REDACTED**', 'area': '1', 'bypass': '0', @@ -267,7 +896,6 @@ 'type_no': '4', }), dict({ - '_state': 'open', 'address': '**REDACTED**', 'area': '1', 'bypass': '0', @@ -328,7 +956,6 @@ 'type_no': '4', }), dict({ - '_state': 'unavailable', 'address': '**REDACTED**', 'area': '1', 'bypass': '0', @@ -388,8 +1015,6 @@ 'type_no': '4', }), dict({ - '_state': 'unlocked', - '_state2': 'closed', 'address': '**REDACTED**', 'area': '1', 'bypass': '0', @@ -450,8 +1075,6 @@ 'type_no': '72', }), dict({ - '_state': 'unlocked', - '_state2': 'open', 'address': '**REDACTED**', 'area': '1', 'bypass': '0', @@ -512,7 +1135,6 @@ 'type_no': '72', }), dict({ - '_state': 'unavailable', 'address': '**REDACTED**', 'area': '1', 'bypass': '0', @@ -632,799 +1254,193 @@ 'type_no': '40', }), ]), - 'model': list([ + }), + 'history': dict({ + 'data': list([ + dict({ + 'area': 1, + 'cid': '18180701000', + 'cid_source': 'DEVICE', + 'event_time': None, + 'event_type': '1807', + 'name': '**REDACTED**', + 'report_id': '1027299996', + 'status_temp_format': 'C', + 'time': '2023/08/17 11:31:54', + 'type': 'device_type.door_lock', + 'user': 0, + 'zone': 1, + }), + dict({ + 'area': 1, + 'cid': '18180201101', + 'cid_source': 'DEVICE', + 'event_time': None, + 'event_type': '1802', + 'name': '**REDACTED**', + 'report_id': '1027299889', + 'status_temp_format': 'C', + 'time': '2023/08/17 11:31:43', + 'type': 'device_type.door_lock', + 'user': 101, + 'zone': 1, + }), + dict({ + 'area': 1, + 'cid': '18180701000', + 'cid_source': 'DEVICE', + 'event_time': None, + 'event_type': '1807', + 'name': '**REDACTED**', + 'report_id': '1027299587', + 'status_temp_format': 'C', + 'time': '2023/08/17 11:31:11', + 'type': 'device_type.door_lock', + 'user': 0, + 'zone': 1, + }), + dict({ + 'area': 1, + 'cid': '18180101001', + 'cid_source': 'DEVICE', + 'event_time': None, + 'event_type': '1801', + 'name': '**REDACTED**', + 'report_id': '1027296099', + 'status_temp_format': 'C', + 'time': '2023/08/17 11:24:52', + 'type': 'device_type.door_lock', + 'user': 1, + 'zone': 1, + }), + dict({ + 'area': 1, + 'cid': '18180701000', + 'cid_source': 'DEVICE', + 'event_time': None, + 'event_type': '1807', + 'name': '**REDACTED**', + 'report_id': '1027273782', + 'status_temp_format': 'C', + 'time': '2023/08/17 10:43:21', + 'type': 'device_type.door_lock', + 'user': 0, + 'zone': 1, + }), + dict({ + 'area': 1, + 'cid': '18180201101', + 'cid_source': 'DEVICE', + 'event_time': None, + 'event_type': '1802', + 'name': '**REDACTED**', + 'report_id': '1027273230', + 'status_temp_format': 'C', + 'time': '2023/08/17 10:42:09', + 'type': 'device_type.door_lock', + 'user': 101, + 'zone': 1, + }), + dict({ + 'area': 1, + 'cid': '18180701000', + 'cid_source': 'DEVICE', + 'event_time': None, + 'event_type': '1807', + 'name': '**REDACTED**', + 'report_id': '1027100172', + 'status_temp_format': 'C', + 'time': '2023/08/17 05:28:57', + 'type': 'device_type.door_lock', + 'user': 0, + 'zone': 1, + }), + dict({ + 'area': 1, + 'cid': '18180101001', + 'cid_source': 'DEVICE', + 'event_time': None, + 'event_type': '1801', + 'name': '**REDACTED**', + 'report_id': '1027099978', + 'status_temp_format': 'C', + 'time': '2023/08/17 05:28:39', + 'type': 'device_type.door_lock', + 'user': 1, + 'zone': 1, + }), + dict({ + 'area': 0, + 'cid': '18160200000', + 'cid_source': 'SYSTEM', + 'event_time': None, + 'event_type': '1602', + 'name': '', + 'report_id': '1027093266', + 'status_temp_format': 'C', + 'time': '2023/08/17 05:17:12', + 'type': '', + 'user': '', + 'zone': 0, + }), + dict({ + 'area': 1, + 'cid': '18180701000', + 'cid_source': 'DEVICE', + 'event_time': None, + 'event_type': '1807', + 'name': '**REDACTED**', + 'report_id': '1026912623', + 'status_temp_format': 'C', + 'time': '2023/08/16 20:29:36', + 'type': 'device_type.door_lock', + 'user': 0, + 'zone': 1, + }), + ]), + }), + 'mode': dict({ + 'data': list([ dict({ 'area': '1', 'mode': 'disarm', }), ]), - 'panel_status': dict({ - 'warning_snd_mute': '0', - }), - 'report_event_latest': dict({ - 'cid_code': '1807', - 'event_time': None, - 'id': '**REDACTED**', - 'report_id': '1027299996', - 'time': '1692271914', - 'utc_event_time': None, - }), }), - 'devices': list([ - dict({ - 'address': '**REDACTED**', - 'area': '1', - 'bypass': '0', - 'device_group': '002', - 'device_id': '**REDACTED**', - 'device_id2': '', - 'extension': None, - 'group_id': None, - 'group_name': None, - 'ipcam_trigger_by_zone1': None, - 'ipcam_trigger_by_zone2': None, - 'ipcam_trigger_by_zone3': None, - 'ipcam_trigger_by_zone4': None, - 'mac': '**REDACTED**', - 'minigw_configuration_data': '02FF000001000000000000000000001E000100', - 'minigw_lock_status': '35', - 'minigw_number_of_credentials_supported': '10', - 'minigw_product_data': '21020120', - 'minigw_protocol': 'DM', - 'minigw_syncing': '0', - 'name': '**REDACTED**', - 'no': '1', - 'rf': None, - 'rssi': '9', - 'scene_restore': None, - 'scene_trigger': '0', - 'sresp_button_1': None, - 'sresp_button_2': None, - 'sresp_button_3': None, - 'sresp_button_4': None, - 'status1': 'device_status.lock', - 'status2': None, - 'status_dim_level': None, - 'status_fault': list([ - ]), - 'status_hue': None, - 'status_humi': None, - 'status_lux': '', - 'status_open': list([ - 'device_status.lock', - ]), - 'status_power': None, - 'status_saturation': None, - 'status_switch': None, - 'status_temp': None, - 'status_temp_format': 'C', - 'status_total_energy': None, - 'thermo_c_setpoint': None, - 'thermo_c_setpoint_away': None, - 'thermo_fan_mode': None, - 'thermo_mode': None, - 'thermo_schd_setting': None, - 'thermo_setpoint': None, - 'thermo_setpoint_away': None, - 'trigger_by_zone': list([ - ]), - 'type': 'device_type.door_lock', - 'type_no': '72', - }), - dict({ - 'address': '**REDACTED**', - 'area': '1', - 'bypass': '0', - 'device_group': '002', - 'device_id': '**REDACTED**', - 'device_id2': '', - 'extension': None, - 'group_id': None, - 'group_name': None, - 'ipcam_trigger_by_zone1': None, - 'ipcam_trigger_by_zone2': None, - 'ipcam_trigger_by_zone3': None, - 'ipcam_trigger_by_zone4': None, - 'mac': '**REDACTED**', - 'minigw_configuration_data': '02FF000001000000000000000000001E000100', - 'minigw_lock_status': None, - 'minigw_number_of_credentials_supported': '10', - 'minigw_product_data': '21020120', - 'minigw_protocol': 'DM', - 'minigw_syncing': '0', - 'name': '**REDACTED**', - 'no': '2', - 'rf': None, - 'rssi': '9', - 'scene_restore': None, - 'scene_trigger': '0', - 'sresp_button_1': None, - 'sresp_button_2': None, - 'sresp_button_3': None, - 'sresp_button_4': None, - 'status1': 'device_status.unlock', - 'status2': None, - 'status_dim_level': None, - 'status_fault': list([ - ]), - 'status_hue': None, - 'status_humi': None, - 'status_lux': '', - 'status_open': list([ - ]), - 'status_power': None, - 'status_saturation': None, - 'status_switch': None, - 'status_temp': None, - 'status_temp_format': 'C', - 'status_total_energy': None, - 'thermo_c_setpoint': None, - 'thermo_c_setpoint_away': None, - 'thermo_fan_mode': None, - 'thermo_mode': None, - 'thermo_schd_setting': None, - 'thermo_setpoint': None, - 'thermo_setpoint_away': None, - 'trigger_by_zone': list([ - ]), - 'type': 'device_type.door_lock', - 'type_no': '72', - }), - dict({ - 'address': '**REDACTED**', - 'area': '1', - 'bypass': '0', - 'device_group': '002', - 'device_id': '**REDACTED**', - 'device_id2': '', - 'extension': None, - 'group_id': None, - 'group_name': None, - 'ipcam_trigger_by_zone1': None, - 'ipcam_trigger_by_zone2': None, - 'ipcam_trigger_by_zone3': None, - 'ipcam_trigger_by_zone4': None, - 'mac': '**REDACTED**', - 'minigw_configuration_data': '02FF000001000000000000000000001E000100', - 'minigw_lock_status': None, - 'minigw_number_of_credentials_supported': '10', - 'minigw_product_data': '21020120', - 'minigw_protocol': 'DM', - 'minigw_syncing': '0', - 'name': '**REDACTED**', - 'no': '3', - 'rf': None, - 'rssi': '9', - 'scene_restore': None, - 'scene_trigger': '0', - 'sresp_button_1': None, - 'sresp_button_2': None, - 'sresp_button_3': None, - 'sresp_button_4': None, - 'status1': 'device_status.lock', - 'status2': None, - 'status_dim_level': None, - 'status_fault': list([ - ]), - 'status_hue': None, - 'status_humi': None, - 'status_lux': '', - 'status_open': list([ - 'device_status.lock', - ]), - 'status_power': None, - 'status_saturation': None, - 'status_switch': None, - 'status_temp': None, - 'status_temp_format': 'C', - 'status_total_energy': None, - 'thermo_c_setpoint': None, - 'thermo_c_setpoint_away': None, - 'thermo_fan_mode': None, - 'thermo_mode': None, - 'thermo_schd_setting': None, - 'thermo_setpoint': None, - 'thermo_setpoint_away': None, - 'trigger_by_zone': list([ - ]), - 'type': 'device_type.door_lock', - 'type_no': '72', - }), - dict({ - 'address': '**REDACTED**', - 'area': '1', - 'bypass': '0', - 'device_group': '000', - 'device_id': '**REDACTED**', - 'device_id2': '', - 'extension': None, - 'group_id': None, - 'group_name': None, - 'ipcam_trigger_by_zone1': None, - 'ipcam_trigger_by_zone2': None, - 'ipcam_trigger_by_zone3': None, - 'ipcam_trigger_by_zone4': None, - 'mac': '**REDACTED**', - 'minigw_configuration_data': '', - 'minigw_lock_status': '', - 'minigw_number_of_credentials_supported': '', - 'minigw_product_data': '', - 'minigw_protocol': '', - 'minigw_syncing': '', - 'name': '**REDACTED**', - 'no': '4', - 'rf': None, - 'rssi': '0', - 'scene_restore': None, - 'scene_trigger': '0', - 'sresp_button_1': None, - 'sresp_button_2': None, - 'sresp_button_3': None, - 'sresp_button_4': None, - 'status1': 'device_status.dc_close', - 'status2': None, - 'status_dim_level': None, - 'status_fault': list([ - ]), - 'status_hue': None, - 'status_humi': None, - 'status_lux': '', - 'status_open': list([ - 'device_status.dc_close', - ]), - 'status_power': None, - 'status_saturation': None, - 'status_switch': None, - 'status_temp': None, - 'status_temp_format': 'C', - 'status_total_energy': None, - 'thermo_c_setpoint': None, - 'thermo_c_setpoint_away': None, - 'thermo_fan_mode': None, - 'thermo_mode': None, - 'thermo_schd_setting': None, - 'thermo_setpoint': None, - 'thermo_setpoint_away': None, - 'trigger_by_zone': list([ - ]), - 'type': 'device_type.door_contact', - 'type_no': '4', - }), - dict({ - 'address': '**REDACTED**', - 'area': '1', - 'bypass': '0', - 'device_group': '000', - 'device_id': '**REDACTED**', - 'device_id2': '', - 'extension': None, - 'group_id': None, - 'group_name': None, - 'ipcam_trigger_by_zone1': None, - 'ipcam_trigger_by_zone2': None, - 'ipcam_trigger_by_zone3': None, - 'ipcam_trigger_by_zone4': None, - 'mac': '**REDACTED**', - 'minigw_configuration_data': '', - 'minigw_lock_status': '', - 'minigw_number_of_credentials_supported': '', - 'minigw_product_data': '', - 'minigw_protocol': '', - 'minigw_syncing': '', - 'name': '**REDACTED**', - 'no': '5', - 'rf': None, - 'rssi': '0', - 'scene_restore': None, - 'scene_trigger': '0', - 'sresp_button_1': None, - 'sresp_button_2': None, - 'sresp_button_3': None, - 'sresp_button_4': None, - 'status1': 'device_status.dc_open', - 'status2': None, - 'status_dim_level': None, - 'status_fault': list([ - ]), - 'status_hue': None, - 'status_humi': None, - 'status_lux': '', - 'status_open': list([ - 'device_status.dc_open', - ]), - 'status_power': None, - 'status_saturation': None, - 'status_switch': None, - 'status_temp': None, - 'status_temp_format': 'C', - 'status_total_energy': None, - 'thermo_c_setpoint': None, - 'thermo_c_setpoint_away': None, - 'thermo_fan_mode': None, - 'thermo_mode': None, - 'thermo_schd_setting': None, - 'thermo_setpoint': None, - 'thermo_setpoint_away': None, - 'trigger_by_zone': list([ - ]), - 'type': 'device_type.door_contact', - 'type_no': '4', - }), - dict({ - 'address': '**REDACTED**', - 'area': '1', - 'bypass': '0', - 'device_group': '000', - 'device_id': '**REDACTED**', - 'device_id2': '', - 'extension': None, - 'group_id': None, - 'group_name': None, - 'ipcam_trigger_by_zone1': None, - 'ipcam_trigger_by_zone2': None, - 'ipcam_trigger_by_zone3': None, - 'ipcam_trigger_by_zone4': None, - 'mac': '**REDACTED**', - 'minigw_configuration_data': '', - 'minigw_lock_status': '', - 'minigw_number_of_credentials_supported': '', - 'minigw_product_data': '', - 'minigw_protocol': '', - 'minigw_syncing': '', - 'name': '**REDACTED**', - 'no': '6', - 'rf': None, - 'rssi': '0', - 'scene_restore': None, - 'scene_trigger': '0', - 'sresp_button_1': None, - 'sresp_button_2': None, - 'sresp_button_3': None, - 'sresp_button_4': None, - 'status1': 'unknwon', - 'status2': None, - 'status_dim_level': None, - 'status_fault': list([ - ]), - 'status_hue': None, - 'status_humi': None, - 'status_lux': '', - 'status_open': list([ - ]), - 'status_power': None, - 'status_saturation': None, - 'status_switch': None, - 'status_temp': None, - 'status_temp_format': 'C', - 'status_total_energy': None, - 'thermo_c_setpoint': None, - 'thermo_c_setpoint_away': None, - 'thermo_fan_mode': None, - 'thermo_mode': None, - 'thermo_schd_setting': None, - 'thermo_setpoint': None, - 'thermo_setpoint_away': None, - 'trigger_by_zone': list([ - ]), - 'type': 'device_type.door_contact', - 'type_no': '4', - }), - dict({ - 'address': '**REDACTED**', - 'area': '1', - 'bypass': '0', - 'device_group': '002', - 'device_id': '**REDACTED**', - 'device_id2': '', - 'extension': None, - 'group_id': None, - 'group_name': None, - 'ipcam_trigger_by_zone1': None, - 'ipcam_trigger_by_zone2': None, - 'ipcam_trigger_by_zone3': None, - 'ipcam_trigger_by_zone4': None, - 'mac': '**REDACTED**', - 'minigw_configuration_data': '02FF000001000000000000000000001E000100', - 'minigw_lock_status': '36', - 'minigw_number_of_credentials_supported': '10', - 'minigw_product_data': '21020120', - 'minigw_protocol': 'DM', - 'minigw_syncing': '0', - 'name': '**REDACTED**', - 'no': '7', - 'rf': None, - 'rssi': '9', - 'scene_restore': None, - 'scene_trigger': '0', - 'sresp_button_1': None, - 'sresp_button_2': None, - 'sresp_button_3': None, - 'sresp_button_4': None, - 'status1': 'device_status.lock', - 'status2': None, - 'status_dim_level': None, - 'status_fault': list([ - ]), - 'status_hue': None, - 'status_humi': None, - 'status_lux': '', - 'status_open': list([ - 'device_status.lock', - ]), - 'status_power': None, - 'status_saturation': None, - 'status_switch': None, - 'status_temp': None, - 'status_temp_format': 'C', - 'status_total_energy': None, - 'thermo_c_setpoint': None, - 'thermo_c_setpoint_away': None, - 'thermo_fan_mode': None, - 'thermo_mode': None, - 'thermo_schd_setting': None, - 'thermo_setpoint': None, - 'thermo_setpoint_away': None, - 'trigger_by_zone': list([ - ]), - 'type': 'device_type.door_lock', - 'type_no': '72', - }), - dict({ - 'address': '**REDACTED**', - 'area': '1', - 'bypass': '0', - 'device_group': '002', - 'device_id': '**REDACTED**', - 'device_id2': '', - 'extension': None, - 'group_id': None, - 'group_name': None, - 'ipcam_trigger_by_zone1': None, - 'ipcam_trigger_by_zone2': None, - 'ipcam_trigger_by_zone3': None, - 'ipcam_trigger_by_zone4': None, - 'mac': '**REDACTED**', - 'minigw_configuration_data': '02FF000001000000000000000000001E000100', - 'minigw_lock_status': '4', - 'minigw_number_of_credentials_supported': '10', - 'minigw_product_data': '21020120', - 'minigw_protocol': 'DM', - 'minigw_syncing': '0', - 'name': '**REDACTED**', - 'no': '8', - 'rf': None, - 'rssi': '9', - 'scene_restore': None, - 'scene_trigger': '0', - 'sresp_button_1': None, - 'sresp_button_2': None, - 'sresp_button_3': None, - 'sresp_button_4': None, - 'status1': 'device_status.unlock', - 'status2': None, - 'status_dim_level': None, - 'status_fault': list([ - ]), - 'status_hue': None, - 'status_humi': None, - 'status_lux': '', - 'status_open': list([ - 'device_status.unlock', - ]), - 'status_power': None, - 'status_saturation': None, - 'status_switch': None, - 'status_temp': None, - 'status_temp_format': 'C', - 'status_total_energy': None, - 'thermo_c_setpoint': None, - 'thermo_c_setpoint_away': None, - 'thermo_fan_mode': None, - 'thermo_mode': None, - 'thermo_schd_setting': None, - 'thermo_setpoint': None, - 'thermo_setpoint_away': None, - 'trigger_by_zone': list([ - ]), - 'type': 'device_type.door_lock', - 'type_no': '72', - }), - dict({ - 'address': '**REDACTED**', - 'area': '1', - 'bypass': '0', - 'device_group': '002', - 'device_id': '**REDACTED**', - 'device_id2': '', - 'extension': None, - 'group_id': None, - 'group_name': None, - 'ipcam_trigger_by_zone1': None, - 'ipcam_trigger_by_zone2': None, - 'ipcam_trigger_by_zone3': None, - 'ipcam_trigger_by_zone4': None, - 'mac': '**REDACTED**', - 'minigw_configuration_data': '02FF000001000000000000000000001E000100', - 'minigw_lock_status': '10', - 'minigw_number_of_credentials_supported': '10', - 'minigw_product_data': '21020120', - 'minigw_protocol': 'DM', - 'minigw_syncing': '0', - 'name': '**REDACTED**', - 'no': '9', - 'rf': None, - 'rssi': '9', - 'scene_restore': None, - 'scene_trigger': '0', - 'sresp_button_1': None, - 'sresp_button_2': None, - 'sresp_button_3': None, - 'sresp_button_4': None, - 'status1': 'device_status.error', - 'status2': None, - 'status_dim_level': None, - 'status_fault': list([ - ]), - 'status_hue': None, - 'status_humi': None, - 'status_lux': '', - 'status_open': list([ - 'device_status.error', - ]), - 'status_power': None, - 'status_saturation': None, - 'status_switch': None, - 'status_temp': None, - 'status_temp_format': 'C', - 'status_total_energy': None, - 'thermo_c_setpoint': None, - 'thermo_c_setpoint_away': None, - 'thermo_fan_mode': None, - 'thermo_mode': None, - 'thermo_schd_setting': None, - 'thermo_setpoint': None, - 'thermo_setpoint_away': None, - 'trigger_by_zone': list([ - ]), - 'type': 'device_type.door_lock', - 'type_no': '72', - }), - dict({ - 'address': '**REDACTED**', - 'area': '1', - 'bypass': '0', - 'device_group': '001', - 'device_id': '**REDACTED**', - 'device_id2': '', - 'extension': None, - 'group_id': None, - 'group_name': None, - 'ipcam_trigger_by_zone1': None, - 'ipcam_trigger_by_zone2': None, - 'ipcam_trigger_by_zone3': None, - 'ipcam_trigger_by_zone4': None, - 'mac': '**REDACTED**', - 'minigw_configuration_data': '', - 'minigw_lock_status': '', - 'minigw_number_of_credentials_supported': '', - 'minigw_product_data': '', - 'minigw_protocol': '', - 'minigw_syncing': '', - 'name': '**REDACTED**', - 'no': '8', - 'rf': None, - 'rssi': '9', - 'scene_restore': None, - 'scene_trigger': '0', - 'sresp_button_1': None, - 'sresp_button_2': None, - 'sresp_button_3': None, - 'sresp_button_4': None, - 'status1': '', - 'status2': None, - 'status_dim_level': None, - 'status_fault': list([ - ]), - 'status_hue': None, - 'status_humi': None, - 'status_lux': '', - 'status_open': list([ - ]), - 'status_power': None, - 'status_saturation': None, - 'status_switch': None, - 'status_temp': 21, - 'status_temp_format': 'C', - 'status_total_energy': None, - 'thermo_c_setpoint': None, - 'thermo_c_setpoint_away': None, - 'thermo_fan_mode': None, - 'thermo_mode': None, - 'thermo_schd_setting': None, - 'thermo_setpoint': None, - 'thermo_setpoint_away': None, - 'trigger_by_zone': list([ - ]), - 'type': 'device_type.temperature_sensor', - 'type_no': '40', - }), - ]), - 'history': list([ - dict({ - 'area': 1, - 'cid': '18180701000', - 'cid_source': 'DEVICE', - 'event_time': None, - 'event_type': '1807', - 'name': '**REDACTED**', - 'report_id': '1027299996', - 'status_temp_format': 'C', - 'time': '2023/08/17 11:31:54', - 'type': 'device_type.door_lock', - 'user': 0, - 'zone': 1, - }), - dict({ - 'area': 1, - 'cid': '18180201101', - 'cid_source': 'DEVICE', - 'event_time': None, - 'event_type': '1802', - 'name': '**REDACTED**', - 'report_id': '1027299889', - 'status_temp_format': 'C', - 'time': '2023/08/17 11:31:43', - 'type': 'device_type.door_lock', - 'user': 101, - 'zone': 1, - }), - dict({ - 'area': 1, - 'cid': '18180701000', - 'cid_source': 'DEVICE', - 'event_time': None, - 'event_type': '1807', - 'name': '**REDACTED**', - 'report_id': '1027299587', - 'status_temp_format': 'C', - 'time': '2023/08/17 11:31:11', - 'type': 'device_type.door_lock', - 'user': 0, - 'zone': 1, - }), - dict({ - 'area': 1, - 'cid': '18180101001', - 'cid_source': 'DEVICE', - 'event_time': None, - 'event_type': '1801', - 'name': '**REDACTED**', - 'report_id': '1027296099', - 'status_temp_format': 'C', - 'time': '2023/08/17 11:24:52', - 'type': 'device_type.door_lock', - 'user': 1, - 'zone': 1, - }), - dict({ - 'area': 1, - 'cid': '18180701000', - 'cid_source': 'DEVICE', - 'event_time': None, - 'event_type': '1807', - 'name': '**REDACTED**', - 'report_id': '1027273782', - 'status_temp_format': 'C', - 'time': '2023/08/17 10:43:21', - 'type': 'device_type.door_lock', - 'user': 0, - 'zone': 1, - }), - dict({ - 'area': 1, - 'cid': '18180201101', - 'cid_source': 'DEVICE', - 'event_time': None, - 'event_type': '1802', - 'name': '**REDACTED**', - 'report_id': '1027273230', - 'status_temp_format': 'C', - 'time': '2023/08/17 10:42:09', - 'type': 'device_type.door_lock', - 'user': 101, - 'zone': 1, - }), - dict({ - 'area': 1, - 'cid': '18180701000', - 'cid_source': 'DEVICE', - 'event_time': None, - 'event_type': '1807', - 'name': '**REDACTED**', - 'report_id': '1027100172', - 'status_temp_format': 'C', - 'time': '2023/08/17 05:28:57', - 'type': 'device_type.door_lock', - 'user': 0, - 'zone': 1, - }), - dict({ - 'area': 1, - 'cid': '18180101001', - 'cid_source': 'DEVICE', - 'event_time': None, - 'event_type': '1801', - 'name': '**REDACTED**', - 'report_id': '1027099978', - 'status_temp_format': 'C', - 'time': '2023/08/17 05:28:39', - 'type': 'device_type.door_lock', - 'user': 1, - 'zone': 1, - }), - dict({ - 'area': 0, - 'cid': '18160200000', - 'cid_source': 'SYSTEM', - 'event_time': None, - 'event_type': '1602', - 'name': '', - 'report_id': '1027093266', - 'status_temp_format': 'C', - 'time': '2023/08/17 05:17:12', - 'type': '', - 'user': '', - 'zone': 0, - }), - dict({ - 'area': 1, - 'cid': '18180701000', - 'cid_source': 'DEVICE', - 'event_time': None, - 'event_type': '1807', - 'name': '**REDACTED**', - 'report_id': '1026912623', - 'status_temp_format': 'C', - 'time': '2023/08/16 20:29:36', - 'type': 'device_type.door_lock', - 'user': 0, - 'zone': 1, - }), - ]), - 'mode': list([ - dict({ - 'area': '1', - 'mode': 'disarm', - }), - ]), - 'online': 'online', + 'online': dict({ + 'data': 'online', + }), 'panel_info': dict({ - 'SMS_Balance': '50', - 'contact': '', - 'dealer_name': 'Poland', - 'mac': '**REDACTED**', - 'mail_address': '**REDACTED**', - 'name': '', - 'net_version': 'MINIGW-MZ-1_G 1.0.1.29A', - 'phone': 'UK-01902364606 / Sweden-0770373710 / Demark-89887818 / Norway-81569036', - 'report_account': '**REDACTED**', - 'rf51_version': '', - 'service_time': 'UK - Mon to Fri 8:30 til 17:30 / Scandinavia - Mon to Fri 8:00 til 20:00, Sat to Sun 10:00 til 15:00', - 'version': 'MINIGW-MZ-1_G 1.0.1.29A,,4.1.2.6.2,00:1D:94:0B:5E:A7,10111112,ML_yamga', - 'voice_balance': '0', - 'xml_version': '2', - 'zb_version': '4.1.2.6.2', - 'zw_version': '', + 'data': dict({ + 'SMS_Balance': '50', + 'contact': '', + 'dealer_name': 'Poland', + 'mac': '**REDACTED**', + 'mail_address': '**REDACTED**', + 'name': '', + 'net_version': 'MINIGW-MZ-1_G 1.0.1.29A', + 'phone': 'UK-01902364606 / Sweden-0770373710 / Demark-89887818 / Norway-81569036', + 'report_account': '**REDACTED**', + 'rf51_version': '', + 'service_time': 'UK - Mon to Fri 8:30 til 17:30 / Scandinavia - Mon to Fri 8:00 til 20:00, Sat to Sun 10:00 til 15:00', + 'version': 'MINIGW-MZ-1_G 1.0.1.29A,,4.1.2.6.2,00:1D:94:0B:5E:A7,10111112,ML_yamga', + 'voice_balance': '0', + 'xml_version': '2', + 'zb_version': '4.1.2.6.2', + 'zw_version': '', + }), }), 'status': dict({ - 'acfail': 'main.normal', - 'battery': 'main.normal', - 'gsm_rssi': '0', - 'imei': '', - 'imsi': '', - 'jam': 'main.normal', - 'rssi': '1', - 'tamper': 'main.normal', + 'data': dict({ + 'acfail': 'main.normal', + 'battery': 'main.normal', + 'gsm_rssi': '0', + 'imei': '', + 'imsi': '', + 'jam': 'main.normal', + 'rssi': '1', + 'tamper': 'main.normal', + }), }), }) # --- diff --git a/tests/components/yale_smart_alarm/test_lock.py b/tests/components/yale_smart_alarm/test_lock.py index 7c67703924b..b1bbbaabc57 100644 --- a/tests/components/yale_smart_alarm/test_lock.py +++ b/tests/components/yale_smart_alarm/test_lock.py @@ -55,7 +55,7 @@ async def test_lock_service_calls( client = load_config_entry[1] data = deepcopy(get_data.cycle) - data["data"] = data.pop("device_status") + data["data"] = data["data"].pop("device_status") client.auth.get_authenticated = Mock(return_value=data) client.auth.post_authenticated = Mock(return_value={"code": "000"}) @@ -109,7 +109,7 @@ async def test_lock_service_call_fails( client = load_config_entry[1] data = deepcopy(get_data.cycle) - data["data"] = data.pop("device_status") + data["data"] = data["data"].pop("device_status") client.auth.get_authenticated = Mock(return_value=data) client.auth.post_authenticated = Mock(side_effect=UnknownError("test_side_effect")) @@ -161,7 +161,7 @@ async def test_lock_service_call_fails_with_incorrect_status( client = load_config_entry[1] data = deepcopy(get_data.cycle) - data["data"] = data.pop("device_status") + data["data"] = data["data"].pop("device_status") client.auth.get_authenticated = Mock(return_value=data) client.auth.post_authenticated = Mock(return_value={"code": "FFF"}) From 6b2526ddbd1719688bc2d6ad244a662962c5a55e Mon Sep 17 00:00:00 2001 From: Pete Sage <76050312+PeteRager@users.noreply.github.com> Date: Sun, 8 Sep 2024 11:34:27 -0400 Subject: [PATCH 231/271] FIx Sonos announce regression issue (#125515) * initial commit * initial commit --- .../components/sonos/media_player.py | 24 +++++++++++++++---- tests/components/sonos/test_media_player.py | 21 ++++++++++++++++ 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 75527bdcb72..bf7dda96cc8 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -84,6 +84,7 @@ REPEAT_TO_SONOS = { SONOS_TO_REPEAT = {meaning: mode for mode, meaning in REPEAT_TO_SONOS.items()} UPNP_ERRORS_TO_IGNORE = ["701", "711", "712"] +ANNOUNCE_NOT_SUPPORTED_ERRORS: list[str] = ["globalError"] SERVICE_SNAPSHOT = "snapshot" SERVICE_RESTORE = "restore" @@ -556,11 +557,24 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): ) from exc if response.get("success"): return - raise HomeAssistantError( - translation_domain=SONOS_DOMAIN, - translation_key="announce_media_error", - translation_placeholders={"media_id": media_id, "response": response}, - ) + if response.get("type") in ANNOUNCE_NOT_SUPPORTED_ERRORS: + # If the speaker does not support announce do not raise and + # fall through to_play_media to play the clip directly. + _LOGGER.debug( + "Speaker %s does not support announce, media_id %s response %s", + self.speaker.zone_name, + media_id, + response, + ) + else: + raise HomeAssistantError( + translation_domain=SONOS_DOMAIN, + translation_key="announce_media_error", + translation_placeholders={ + "media_id": media_id, + "response": response, + }, + ) if spotify.is_spotify_media_type(media_type): media_type = spotify.resolve_spotify_media_type(media_type) diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index ac877f47904..4a49e36e677 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -1123,6 +1123,27 @@ async def test_play_media_announce( ) assert sonos_websocket.play_clip.call_count == 1 + # Test speakers that do not support announce. This + # will result in playing the clip directly via play_uri + sonos_websocket.play_clip.reset_mock() + sonos_websocket.play_clip.side_effect = None + retval = {"success": 0, "type": "globalError"} + sonos_websocket.play_clip.return_value = [retval, {}] + + await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: "media_player.zone_a", + ATTR_MEDIA_CONTENT_TYPE: "music", + ATTR_MEDIA_CONTENT_ID: content_id, + ATTR_MEDIA_ANNOUNCE: True, + }, + blocking=True, + ) + assert sonos_websocket.play_clip.call_count == 1 + soco.play_uri.assert_called_with(content_id, force_radio=False) + async def test_media_get_queue( hass: HomeAssistant, From 7eb9036cbb255f296f06e4222a8806e861f29784 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 9 Sep 2024 22:33:08 +0200 Subject: [PATCH 232/271] Update frontend to 20240909.1 (#125610) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index e40832e4733..7f394611375 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20240906.0"] + "requirements": ["home-assistant-frontend==20240909.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1b9b4fa9ebf..d6f4dfcf0ab 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -31,7 +31,7 @@ habluetooth==3.4.0 hass-nabucasa==0.81.1 hassil==1.7.4 home-assistant-bluetooth==1.12.2 -home-assistant-frontend==20240906.0 +home-assistant-frontend==20240909.1 home-assistant-intents==2024.9.4 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index c83f17b8d83..2a68f1a0749 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1102,7 +1102,7 @@ hole==0.8.0 holidays==0.56 # homeassistant.components.frontend -home-assistant-frontend==20240906.0 +home-assistant-frontend==20240909.1 # homeassistant.components.conversation home-assistant-intents==2024.9.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e426f7c3e4d..60a76dee4e4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -925,7 +925,7 @@ hole==0.8.0 holidays==0.56 # homeassistant.components.frontend -home-assistant-frontend==20240906.0 +home-assistant-frontend==20240909.1 # homeassistant.components.conversation home-assistant-intents==2024.9.4 From 7734bdfdab999aa194a14647ae11df6e208c4cf8 Mon Sep 17 00:00:00 2001 From: "Steven B." <51370195+sdb9696@users.noreply.github.com> Date: Tue, 10 Sep 2024 19:52:10 +0100 Subject: [PATCH 233/271] Update tplink config to include aes keys (#125685) --- homeassistant/components/tplink/__init__.py | 116 ++-- .../components/tplink/config_flow.py | 77 +-- homeassistant/components/tplink/const.py | 6 +- tests/components/tplink/__init__.py | 46 +- tests/components/tplink/conftest.py | 10 +- tests/components/tplink/test_config_flow.py | 548 +++++++++++------- tests/components/tplink/test_init.py | 117 +++- 7 files changed, 598 insertions(+), 322 deletions(-) diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index 83cfc733716..ceeb1120ed8 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -26,6 +26,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_ALIAS, CONF_AUTHENTICATION, + CONF_DEVICE, CONF_HOST, CONF_MAC, CONF_MODEL, @@ -44,8 +45,12 @@ from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType from .const import ( + CONF_AES_KEYS, + CONF_CONFIG_ENTRY_MINOR_VERSION, + CONF_CONNECTION_PARAMETERS, CONF_CREDENTIALS_HASH, CONF_DEVICE_CONFIG, + CONF_USES_HTTP, CONNECT_TIMEOUT, DISCOVERY_TIMEOUT, DOMAIN, @@ -85,9 +90,7 @@ def async_trigger_discovery( CONF_ALIAS: device.alias or mac_alias(device.mac), CONF_HOST: device.host, CONF_MAC: formatted_mac, - CONF_DEVICE_CONFIG: device.config.to_dict( - exclude_credentials=True, - ), + CONF_DEVICE: device, }, ) @@ -136,25 +139,27 @@ async def async_setup_entry(hass: HomeAssistant, entry: TPLinkConfigEntry) -> bo host: str = entry.data[CONF_HOST] credentials = await get_credentials(hass) entry_credentials_hash = entry.data.get(CONF_CREDENTIALS_HASH) + entry_use_http = entry.data.get(CONF_USES_HTTP, False) + entry_aes_keys = entry.data.get(CONF_AES_KEYS) - config: DeviceConfig | None = None - if config_dict := entry.data.get(CONF_DEVICE_CONFIG): + conn_params: Device.ConnectionParameters | None = None + if conn_params_dict := entry.data.get(CONF_CONNECTION_PARAMETERS): try: - config = DeviceConfig.from_dict(config_dict) + conn_params = Device.ConnectionParameters.from_dict(conn_params_dict) except KasaException: _LOGGER.warning( - "Invalid connection type dict for %s: %s", host, config_dict + "Invalid connection parameters dict for %s: %s", host, conn_params_dict ) - if not config: - config = DeviceConfig(host) - else: - config.host = host - - config.timeout = CONNECT_TIMEOUT - if config.uses_http is True: - config.http_client = create_async_tplink_clientsession(hass) - + client = create_async_tplink_clientsession(hass) if entry_use_http else None + config = DeviceConfig( + host, + timeout=CONNECT_TIMEOUT, + http_client=client, + aes_keys=entry_aes_keys, + ) + if conn_params: + config.connection_type = conn_params # If we have in memory credentials use them otherwise check for credentials_hash if credentials: config.credentials = credentials @@ -173,14 +178,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: TPLinkConfigEntry) -> bo raise ConfigEntryNotReady from ex device_credentials_hash = device.credentials_hash - device_config_dict = device.config.to_dict(exclude_credentials=True) - # Do not store the credentials hash inside the device_config - device_config_dict.pop(CONF_CREDENTIALS_HASH, None) + + # We not need to update the connection parameters or the use_http here + # because if they were wrong we would have failed to connect. + # Discovery will update those if necessary. updates: dict[str, Any] = {} if device_credentials_hash and device_credentials_hash != entry_credentials_hash: updates[CONF_CREDENTIALS_HASH] = device_credentials_hash - if device_config_dict != config_dict: - updates[CONF_DEVICE_CONFIG] = device_config_dict + if entry_aes_keys != device.config.aes_keys: + updates[CONF_AES_KEYS] = device.config.aes_keys if entry.data.get(CONF_ALIAS) != device.alias: updates[CONF_ALIAS] = device.alias if entry.data.get(CONF_MODEL) != device.model: @@ -307,12 +313,20 @@ def _device_id_is_mac_or_none(mac: str, device_ids: Iterable[str]) -> str | None async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Migrate old entry.""" - version = config_entry.version - minor_version = config_entry.minor_version + entry_version = config_entry.version + entry_minor_version = config_entry.minor_version + # having a condition to check for the current version allows + # tests to be written per migration step. + config_flow_minor_version = CONF_CONFIG_ENTRY_MINOR_VERSION - _LOGGER.debug("Migrating from version %s.%s", version, minor_version) - - if version == 1 and minor_version < 3: + new_minor_version = 3 + if ( + entry_version == 1 + and entry_minor_version < new_minor_version <= config_flow_minor_version + ): + _LOGGER.debug( + "Migrating from version %s.%s", entry_version, entry_minor_version + ) # Previously entities on child devices added themselves to the parent # device and set their device id as identifiers along with mac # as a connection which creates a single device entry linked by all @@ -359,12 +373,19 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> new_identifiers, ) - minor_version = 3 - hass.config_entries.async_update_entry(config_entry, minor_version=3) + hass.config_entries.async_update_entry( + config_entry, minor_version=new_minor_version + ) - _LOGGER.debug("Migration to version %s.%s complete", version, minor_version) + _LOGGER.debug( + "Migration to version %s.%s complete", entry_version, new_minor_version + ) - if version == 1 and minor_version == 3: + new_minor_version = 4 + if ( + entry_version == 1 + and entry_minor_version < new_minor_version <= config_flow_minor_version + ): # credentials_hash stored in the device_config should be moved to data. updates: dict[str, Any] = {} if config_dict := config_entry.data.get(CONF_DEVICE_CONFIG): @@ -372,15 +393,44 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> if credentials_hash := config_dict.pop(CONF_CREDENTIALS_HASH, None): updates[CONF_CREDENTIALS_HASH] = credentials_hash updates[CONF_DEVICE_CONFIG] = config_dict - minor_version = 4 hass.config_entries.async_update_entry( config_entry, data={ **config_entry.data, **updates, }, - minor_version=minor_version, + minor_version=new_minor_version, + ) + _LOGGER.debug( + "Migration to version %s.%s complete", entry_version, new_minor_version ) - _LOGGER.debug("Migration to version %s.%s complete", version, minor_version) + new_minor_version = 5 + if ( + entry_version == 1 + and entry_minor_version < new_minor_version <= config_flow_minor_version + ): + # complete device config no longer to be stored, only required + # attributes like connection parameters and aes_keys + updates = {} + entry_data = { + k: v for k, v in config_entry.data.items() if k != CONF_DEVICE_CONFIG + } + if config_dict := config_entry.data.get(CONF_DEVICE_CONFIG): + assert isinstance(config_dict, dict) + if connection_parameters := config_dict.get("connection_type"): + updates[CONF_CONNECTION_PARAMETERS] = connection_parameters + if (use_http := config_dict.get(CONF_USES_HTTP)) is not None: + updates[CONF_USES_HTTP] = use_http + hass.config_entries.async_update_entry( + config_entry, + data={ + **entry_data, + **updates, + }, + minor_version=new_minor_version, + ) + _LOGGER.debug( + "Migration to version %s.%s complete", entry_version, new_minor_version + ) return True diff --git a/homeassistant/components/tplink/config_flow.py b/homeassistant/components/tplink/config_flow.py index 1c02466aef1..03234d545b5 100644 --- a/homeassistant/components/tplink/config_flow.py +++ b/homeassistant/components/tplink/config_flow.py @@ -46,9 +46,11 @@ from . import ( set_credentials, ) from .const import ( - CONF_CONNECTION_TYPE, + CONF_AES_KEYS, + CONF_CONFIG_ENTRY_MINOR_VERSION, + CONF_CONNECTION_PARAMETERS, CONF_CREDENTIALS_HASH, - CONF_DEVICE_CONFIG, + CONF_USES_HTTP, CONNECT_TIMEOUT, DOMAIN, ) @@ -64,7 +66,7 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for tplink.""" VERSION = 1 - MINOR_VERSION = 4 + MINOR_VERSION = CONF_CONFIG_ENTRY_MINOR_VERSION reauth_entry: ConfigEntry | None = None def __init__(self) -> None: @@ -87,38 +89,43 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): return await self._async_handle_discovery( discovery_info[CONF_HOST], discovery_info[CONF_MAC], - discovery_info[CONF_DEVICE_CONFIG], + discovery_info[CONF_DEVICE], ) @callback def _get_config_updates( - self, entry: ConfigEntry, host: str, config: dict + self, entry: ConfigEntry, host: str, device: Device | None ) -> dict | None: """Return updates if the host or device config has changed.""" entry_data = entry.data - entry_config_dict = entry_data.get(CONF_DEVICE_CONFIG) - if entry_config_dict == config and entry_data[CONF_HOST] == host: + updates: dict[str, Any] = {} + new_connection_params = False + if entry_data[CONF_HOST] != host: + updates[CONF_HOST] = host + if device: + device_conn_params_dict = device.config.connection_type.to_dict() + entry_conn_params_dict = entry_data.get(CONF_CONNECTION_PARAMETERS) + if device_conn_params_dict != entry_conn_params_dict: + new_connection_params = True + updates[CONF_CONNECTION_PARAMETERS] = device_conn_params_dict + updates[CONF_USES_HTTP] = device.config.uses_http + if not updates: return None - updates = {**entry.data, CONF_DEVICE_CONFIG: config, CONF_HOST: host} + updates = {**entry.data, **updates} # If the connection parameters have changed the credentials_hash will be invalid. - if ( - entry_config_dict - and isinstance(entry_config_dict, dict) - and entry_config_dict.get(CONF_CONNECTION_TYPE) - != config.get(CONF_CONNECTION_TYPE) - ): + if new_connection_params: updates.pop(CONF_CREDENTIALS_HASH, None) _LOGGER.debug( "Connection type changed for %s from %s to: %s", host, - entry_config_dict.get(CONF_CONNECTION_TYPE), - config.get(CONF_CONNECTION_TYPE), + entry_conn_params_dict, + device_conn_params_dict, ) return updates @callback def _update_config_if_entry_in_setup_error( - self, entry: ConfigEntry, host: str, config: dict + self, entry: ConfigEntry, host: str, device: Device | None ) -> ConfigFlowResult | None: """If discovery encounters a device that is in SETUP_ERROR or SETUP_RETRY update the device config.""" if entry.state not in ( @@ -126,7 +133,7 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): ConfigEntryState.SETUP_RETRY, ): return None - if updates := self._get_config_updates(entry, host, config): + if updates := self._get_config_updates(entry, host, device): return self.async_update_reload_and_abort( entry, data=updates, @@ -135,19 +142,15 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): return None async def _async_handle_discovery( - self, host: str, formatted_mac: str, config: dict | None = None + self, host: str, formatted_mac: str, device: Device | None = None ) -> ConfigFlowResult: """Handle any discovery.""" current_entry = await self.async_set_unique_id( formatted_mac, raise_on_progress=False ) - if ( - config - and current_entry - and ( - result := self._update_config_if_entry_in_setup_error( - current_entry, host, config - ) + if current_entry and ( + result := self._update_config_if_entry_in_setup_error( + current_entry, host, device ) ): return result @@ -159,9 +162,13 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_abort(reason="already_in_progress") credentials = await get_credentials(self.hass) try: - await self._async_try_discover_and_update( - host, credentials, raise_on_progress=True - ) + if device: + self._discovered_device = device + await self._async_try_connect(device, credentials) + else: + await self._async_try_discover_and_update( + host, credentials, raise_on_progress=True + ) except AuthenticationError: return await self.async_step_discovery_auth_confirm() except KasaException: @@ -381,14 +388,15 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): # This is only ever called after a successful device update so we know that # the credential_hash is correct and should be saved. self._abort_if_unique_id_configured(updates={CONF_HOST: device.host}) - data = { + data: dict[str, Any] = { CONF_HOST: device.host, CONF_ALIAS: device.alias, CONF_MODEL: device.model, - CONF_DEVICE_CONFIG: device.config.to_dict( - exclude_credentials=True, - ), + CONF_CONNECTION_PARAMETERS: device.config.connection_type.to_dict(), + CONF_USES_HTTP: device.config.uses_http, } + if device.config.aes_keys: + data[CONF_AES_KEYS] = device.config.aes_keys if device.credentials_hash: data[CONF_CREDENTIALS_HASH] = device.credentials_hash return self.async_create_entry( @@ -494,8 +502,7 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): placeholders["error"] = str(ex) else: await set_credentials(self.hass, username, password) - config = device.config.to_dict(exclude_credentials=True) - if updates := self._get_config_updates(reauth_entry, host, config): + if updates := self._get_config_updates(reauth_entry, host, device): self.hass.config_entries.async_update_entry( reauth_entry, data=updates ) diff --git a/homeassistant/components/tplink/const.py b/homeassistant/components/tplink/const.py index babd92e2c34..91085edb5a2 100644 --- a/homeassistant/components/tplink/const.py +++ b/homeassistant/components/tplink/const.py @@ -21,7 +21,11 @@ ATTR_TOTAL_ENERGY_KWH: Final = "total_energy_kwh" CONF_DEVICE_CONFIG: Final = "device_config" CONF_CREDENTIALS_HASH: Final = "credentials_hash" -CONF_CONNECTION_TYPE: Final = "connection_type" +CONF_CONNECTION_PARAMETERS: Final = "connection_parameters" +CONF_USES_HTTP: Final = "uses_http" +CONF_AES_KEYS: Final = "aes_keys" + +CONF_CONFIG_ENTRY_MINOR_VERSION: Final = 5 PLATFORMS: Final = [ Platform.BINARY_SENSOR, diff --git a/tests/components/tplink/__init__.py b/tests/components/tplink/__init__.py index c63ca9139f1..93c3a35a2e9 100644 --- a/tests/components/tplink/__init__.py +++ b/tests/components/tplink/__init__.py @@ -21,11 +21,13 @@ from kasa.protocol import BaseProtocol from syrupy import SnapshotAssertion from homeassistant.components.tplink import ( + CONF_AES_KEYS, CONF_ALIAS, + CONF_CONNECTION_PARAMETERS, CONF_CREDENTIALS_HASH, - CONF_DEVICE_CONFIG, CONF_HOST, CONF_MODEL, + CONF_USES_HTTP, Credentials, ) from homeassistant.components.tplink.const import DOMAIN @@ -54,35 +56,42 @@ DHCP_FORMATTED_MAC_ADDRESS = MAC_ADDRESS.replace(":", "") MAC_ADDRESS2 = "11:22:33:44:55:66" DEFAULT_ENTRY_TITLE = f"{ALIAS} {MODEL}" CREDENTIALS_HASH_LEGACY = "" +CONN_PARAMS_LEGACY = DeviceConnectionParameters( + DeviceFamily.IotSmartPlugSwitch, DeviceEncryptionType.Xor +) DEVICE_CONFIG_LEGACY = DeviceConfig(IP_ADDRESS) DEVICE_CONFIG_DICT_LEGACY = DEVICE_CONFIG_LEGACY.to_dict(exclude_credentials=True) CREDENTIALS = Credentials("foo", "bar") CREDENTIALS_HASH_AES = "AES/abcdefghijklmnopqrstuvabcdefghijklmnopqrstuv==" CREDENTIALS_HASH_KLAP = "KLAP/abcdefghijklmnopqrstuv==" +CONN_PARAMS_KLAP = DeviceConnectionParameters( + DeviceFamily.SmartTapoPlug, DeviceEncryptionType.Klap +) DEVICE_CONFIG_KLAP = DeviceConfig( IP_ADDRESS, credentials=CREDENTIALS, - connection_type=DeviceConnectionParameters( - DeviceFamily.SmartTapoPlug, DeviceEncryptionType.Klap - ), + connection_type=CONN_PARAMS_KLAP, uses_http=True, ) +CONN_PARAMS_AES = DeviceConnectionParameters( + DeviceFamily.SmartTapoPlug, DeviceEncryptionType.Aes +) +AES_KEYS = {"private": "foo", "public": "bar"} DEVICE_CONFIG_AES = DeviceConfig( IP_ADDRESS2, credentials=CREDENTIALS, - connection_type=DeviceConnectionParameters( - DeviceFamily.SmartTapoPlug, DeviceEncryptionType.Aes - ), + connection_type=CONN_PARAMS_AES, uses_http=True, + aes_keys=AES_KEYS, ) DEVICE_CONFIG_DICT_KLAP = DEVICE_CONFIG_KLAP.to_dict(exclude_credentials=True) DEVICE_CONFIG_DICT_AES = DEVICE_CONFIG_AES.to_dict(exclude_credentials=True) - CREATE_ENTRY_DATA_LEGACY = { CONF_HOST: IP_ADDRESS, CONF_ALIAS: ALIAS, CONF_MODEL: MODEL, - CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_LEGACY, + CONF_CONNECTION_PARAMETERS: CONN_PARAMS_LEGACY.to_dict(), + CONF_USES_HTTP: False, } CREATE_ENTRY_DATA_KLAP = { @@ -90,23 +99,18 @@ CREATE_ENTRY_DATA_KLAP = { CONF_ALIAS: ALIAS, CONF_MODEL: MODEL, CONF_CREDENTIALS_HASH: CREDENTIALS_HASH_KLAP, - CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_KLAP, + CONF_CONNECTION_PARAMETERS: CONN_PARAMS_KLAP.to_dict(), + CONF_USES_HTTP: True, } CREATE_ENTRY_DATA_AES = { CONF_HOST: IP_ADDRESS2, CONF_ALIAS: ALIAS, CONF_MODEL: MODEL, CONF_CREDENTIALS_HASH: CREDENTIALS_HASH_AES, - CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_AES, + CONF_CONNECTION_PARAMETERS: CONN_PARAMS_AES.to_dict(), + CONF_USES_HTTP: True, + CONF_AES_KEYS: AES_KEYS, } -CONNECTION_TYPE_KLAP = DeviceConnectionParameters( - DeviceFamily.SmartTapoPlug, DeviceEncryptionType.Klap -) -CONNECTION_TYPE_KLAP_DICT = CONNECTION_TYPE_KLAP.to_dict() -CONNECTION_TYPE_AES = DeviceConnectionParameters( - DeviceFamily.SmartTapoPlug, DeviceEncryptionType.Aes -) -CONNECTION_TYPE_AES_DICT = CONNECTION_TYPE_AES.to_dict() def _load_feature_fixtures(): @@ -452,11 +456,11 @@ MODULE_TO_MOCK_GEN = { } -def _patch_discovery(device=None, no_device=False): +def _patch_discovery(device=None, no_device=False, ip_address=IP_ADDRESS): async def _discovery(*args, **kwargs): if no_device: return {} - return {IP_ADDRESS: _mocked_device()} + return {ip_address: device if device else _mocked_device()} return patch("homeassistant.components.tplink.Discover.discover", new=_discovery) diff --git a/tests/components/tplink/conftest.py b/tests/components/tplink/conftest.py index ee4530575ce..f1586ee4a0a 100644 --- a/tests/components/tplink/conftest.py +++ b/tests/components/tplink/conftest.py @@ -1,9 +1,9 @@ """tplink conftest.""" from collections.abc import Generator -import copy from unittest.mock import DEFAULT, AsyncMock, patch +from kasa import DeviceConfig import pytest from homeassistant.components.tplink import DOMAIN @@ -34,13 +34,13 @@ def mock_discovery(): discover_single=DEFAULT, ) as mock_discovery: device = _mocked_device( - device_config=copy.deepcopy(DEVICE_CONFIG_KLAP), + device_config=DeviceConfig.from_dict(DEVICE_CONFIG_KLAP.to_dict()), credentials_hash=CREDENTIALS_HASH_KLAP, alias=None, ) devices = { "127.0.0.1": _mocked_device( - device_config=copy.deepcopy(DEVICE_CONFIG_KLAP), + device_config=DeviceConfig.from_dict(DEVICE_CONFIG_KLAP.to_dict()), credentials_hash=CREDENTIALS_HASH_KLAP, alias=None, ) @@ -57,12 +57,12 @@ def mock_connect(): with patch("homeassistant.components.tplink.Device.connect") as mock_connect: devices = { IP_ADDRESS: _mocked_device( - device_config=DEVICE_CONFIG_KLAP, + device_config=DeviceConfig.from_dict(DEVICE_CONFIG_KLAP.to_dict()), credentials_hash=CREDENTIALS_HASH_KLAP, ip_address=IP_ADDRESS, ), IP_ADDRESS2: _mocked_device( - device_config=DEVICE_CONFIG_AES, + device_config=DeviceConfig.from_dict(DEVICE_CONFIG_AES.to_dict()), credentials_hash=CREDENTIALS_HASH_AES, mac=MAC_ADDRESS2, ip_address=IP_ADDRESS2, diff --git a/tests/components/tplink/test_config_flow.py b/tests/components/tplink/test_config_flow.py index f90eb985d38..7b24769c858 100644 --- a/tests/components/tplink/test_config_flow.py +++ b/tests/components/tplink/test_config_flow.py @@ -1,5 +1,6 @@ """Test the tplink config flow.""" +from contextlib import contextmanager import logging from unittest.mock import AsyncMock, patch @@ -17,7 +18,7 @@ from homeassistant.components.tplink import ( KasaException, ) from homeassistant.components.tplink.const import ( - CONF_CONNECTION_TYPE, + CONF_CONNECTION_PARAMETERS, CONF_CREDENTIALS_HASH, CONF_DEVICE_CONFIG, ) @@ -34,17 +35,21 @@ from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from . import ( + AES_KEYS, ALIAS, - CONNECTION_TYPE_KLAP_DICT, + CONN_PARAMS_AES, + CONN_PARAMS_KLAP, + CONN_PARAMS_LEGACY, CREATE_ENTRY_DATA_AES, CREATE_ENTRY_DATA_KLAP, CREATE_ENTRY_DATA_LEGACY, CREDENTIALS_HASH_AES, CREDENTIALS_HASH_KLAP, DEFAULT_ENTRY_TITLE, - DEVICE_CONFIG_DICT_AES, + DEVICE_CONFIG_AES, DEVICE_CONFIG_DICT_KLAP, - DEVICE_CONFIG_DICT_LEGACY, + DEVICE_CONFIG_KLAP, + DEVICE_CONFIG_LEGACY, DHCP_FORMATTED_MAC_ADDRESS, IP_ADDRESS, MAC_ADDRESS, @@ -59,9 +64,44 @@ from . import ( from tests.common import MockConfigEntry -async def test_discovery(hass: HomeAssistant) -> None: +@contextmanager +def override_side_effect(mock: AsyncMock, effect): + """Temporarily override a mock side effect and replace afterwards.""" + try: + default_side_effect = mock.side_effect + mock.side_effect = effect + yield mock + finally: + mock.side_effect = default_side_effect + + +@pytest.mark.parametrize( + ("device_config", "expected_entry_data", "credentials_hash"), + [ + pytest.param( + DEVICE_CONFIG_KLAP, CREATE_ENTRY_DATA_KLAP, CREDENTIALS_HASH_KLAP, id="KLAP" + ), + pytest.param( + DEVICE_CONFIG_AES, CREATE_ENTRY_DATA_AES, CREDENTIALS_HASH_AES, id="AES" + ), + pytest.param(DEVICE_CONFIG_LEGACY, CREATE_ENTRY_DATA_LEGACY, None, id="Legacy"), + ], +) +async def test_discovery( + hass: HomeAssistant, device_config, expected_entry_data, credentials_hash +) -> None: """Test setting up discovery.""" - with _patch_discovery(), _patch_single_discovery(), _patch_connect(): + ip_address = device_config.host + device = _mocked_device( + device_config=device_config, + credentials_hash=credentials_hash, + ip_address=ip_address, + ) + with ( + _patch_discovery(device, ip_address=ip_address), + _patch_single_discovery(device), + _patch_connect(device), + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -91,9 +131,9 @@ async def test_discovery(hass: HomeAssistant) -> None: assert not result2["errors"] with ( - _patch_discovery(), - _patch_single_discovery(), - _patch_connect(), + _patch_discovery(device, ip_address=ip_address), + _patch_single_discovery(device), + _patch_connect(device), patch(f"{MODULE}.async_setup", return_value=True) as mock_setup, patch(f"{MODULE}.async_setup_entry", return_value=True) as mock_setup_entry, ): @@ -105,7 +145,7 @@ async def test_discovery(hass: HomeAssistant) -> None: assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == DEFAULT_ENTRY_TITLE - assert result3["data"] == CREATE_ENTRY_DATA_LEGACY + assert result3["data"] == expected_entry_data mock_setup.assert_called_once() mock_setup_entry.assert_called_once() @@ -130,24 +170,25 @@ async def test_discovery_auth( ) -> None: """Test authenticated discovery.""" - mock_discovery["mock_device"].update.side_effect = AuthenticationError + mock_device = mock_connect["mock_devices"][IP_ADDRESS] + assert mock_device.config == DEVICE_CONFIG_KLAP - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, - data={ - CONF_HOST: IP_ADDRESS, - CONF_MAC: MAC_ADDRESS, - CONF_ALIAS: ALIAS, - CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_KLAP, - }, - ) + with override_side_effect(mock_connect["connect"], AuthenticationError): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={ + CONF_HOST: IP_ADDRESS, + CONF_MAC: MAC_ADDRESS, + CONF_ALIAS: ALIAS, + CONF_DEVICE: mock_device, + }, + ) await hass.async_block_till_done() assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_auth_confirm" assert not result["errors"] - mock_discovery["mock_device"].update.reset_mock(side_effect=True) result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ @@ -172,40 +213,43 @@ async def test_discovery_auth( ) async def test_discovery_auth_errors( hass: HomeAssistant, - mock_discovery: AsyncMock, mock_connect: AsyncMock, mock_init, error_type, errors_msg, error_placement, ) -> None: - """Test handling of discovery authentication errors.""" - mock_discovery["mock_device"].update.side_effect = AuthenticationError - default_connect_side_effect = mock_connect["connect"].side_effect - mock_connect["connect"].side_effect = error_type + """Test handling of discovery authentication errors. - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, - data={ - CONF_HOST: IP_ADDRESS, - CONF_MAC: MAC_ADDRESS, - CONF_ALIAS: ALIAS, - CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_KLAP, - }, - ) - await hass.async_block_till_done() + Tests for errors received during credential + entry during discovery_auth_confirm. + """ + mock_device = mock_connect["mock_devices"][IP_ADDRESS] + + with override_side_effect(mock_connect["connect"], AuthenticationError): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={ + CONF_HOST: IP_ADDRESS, + CONF_MAC: MAC_ADDRESS, + CONF_ALIAS: ALIAS, + CONF_DEVICE: mock_device, + }, + ) + await hass.async_block_till_done() assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_auth_confirm" assert not result["errors"] - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_USERNAME: "fake_username", - CONF_PASSWORD: "fake_password", - }, - ) + with override_side_effect(mock_connect["connect"], error_type): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_USERNAME: "fake_username", + CONF_PASSWORD: "fake_password", + }, + ) assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {error_placement: errors_msg} @@ -213,7 +257,6 @@ async def test_discovery_auth_errors( await hass.async_block_till_done() - mock_connect["connect"].side_effect = default_connect_side_effect result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], { @@ -228,29 +271,29 @@ async def test_discovery_auth_errors( async def test_discovery_new_credentials( hass: HomeAssistant, - mock_discovery: AsyncMock, mock_connect: AsyncMock, mock_init, ) -> None: """Test setting up discovery with new credentials.""" - mock_discovery["mock_device"].update.side_effect = AuthenticationError + mock_device = mock_connect["mock_devices"][IP_ADDRESS] - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, - data={ - CONF_HOST: IP_ADDRESS, - CONF_MAC: MAC_ADDRESS, - CONF_ALIAS: ALIAS, - CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_KLAP, - }, - ) - await hass.async_block_till_done() + with override_side_effect(mock_connect["connect"], AuthenticationError): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={ + CONF_HOST: IP_ADDRESS, + CONF_MAC: MAC_ADDRESS, + CONF_ALIAS: ALIAS, + CONF_DEVICE: mock_device, + }, + ) + await hass.async_block_till_done() assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_auth_confirm" assert not result["errors"] - assert mock_connect["connect"].call_count == 0 + assert mock_connect["connect"].call_count == 1 with patch( "homeassistant.components.tplink.config_flow.get_credentials", @@ -260,7 +303,7 @@ async def test_discovery_new_credentials( result["flow_id"], ) - assert mock_connect["connect"].call_count == 1 + assert mock_connect["connect"].call_count == 2 assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "discovery_confirm" @@ -277,48 +320,54 @@ async def test_discovery_new_credentials( async def test_discovery_new_credentials_invalid( hass: HomeAssistant, - mock_discovery: AsyncMock, mock_connect: AsyncMock, mock_init, ) -> None: """Test setting up discovery with new invalid credentials.""" - mock_discovery["mock_device"].update.side_effect = AuthenticationError - default_connect_side_effect = mock_connect["connect"].side_effect + mock_device = mock_connect["mock_devices"][IP_ADDRESS] - mock_connect["connect"].side_effect = AuthenticationError - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, - data={ - CONF_HOST: IP_ADDRESS, - CONF_MAC: MAC_ADDRESS, - CONF_ALIAS: ALIAS, - CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_KLAP, - }, - ) - await hass.async_block_till_done() + with ( + patch("homeassistant.components.tplink.Discover.discover", return_value={}), + patch( + "homeassistant.components.tplink.config_flow.get_credentials", + return_value=None, + ), + override_side_effect(mock_connect["connect"], AuthenticationError), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={ + CONF_HOST: IP_ADDRESS, + CONF_MAC: MAC_ADDRESS, + CONF_ALIAS: ALIAS, + CONF_DEVICE: mock_device, + }, + ) + await hass.async_block_till_done() assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_auth_confirm" assert not result["errors"] - assert mock_connect["connect"].call_count == 0 + assert mock_connect["connect"].call_count == 1 - with patch( - "homeassistant.components.tplink.config_flow.get_credentials", - return_value=Credentials("fake_user", "fake_pass"), + with ( + patch( + "homeassistant.components.tplink.config_flow.get_credentials", + return_value=Credentials("fake_user", "fake_pass"), + ), + override_side_effect(mock_connect["connect"], AuthenticationError), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], ) - assert mock_connect["connect"].call_count == 1 + assert mock_connect["connect"].call_count == 2 assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "discovery_auth_confirm" await hass.async_block_till_done() - mock_connect["connect"].side_effect = default_connect_side_effect result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], { @@ -577,32 +626,30 @@ async def test_manual_auth_errors( assert not result["errors"] mock_discovery["mock_device"].update.side_effect = AuthenticationError - default_connect_side_effect = mock_connect["connect"].side_effect - mock_connect["connect"].side_effect = error_type - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_HOST: IP_ADDRESS} - ) + with override_side_effect(mock_connect["connect"], error_type): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_HOST: IP_ADDRESS} + ) assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user_auth_confirm" assert not result2["errors"] await hass.async_block_till_done() - - result3 = await hass.config_entries.flow.async_configure( - result2["flow_id"], - user_input={ - CONF_USERNAME: "fake_username", - CONF_PASSWORD: "fake_password", - }, - ) - await hass.async_block_till_done() + with override_side_effect(mock_connect["connect"], error_type): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + user_input={ + CONF_USERNAME: "fake_username", + CONF_PASSWORD: "fake_password", + }, + ) + await hass.async_block_till_done() assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "user_auth_confirm" assert result3["errors"] == {error_placement: errors_msg} assert result3["description_placeholders"]["error"] == str(error_type) - mock_connect["connect"].side_effect = default_connect_side_effect result4 = await hass.config_entries.flow.async_configure( result3["flow_id"], { @@ -628,7 +675,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: CONF_HOST: IP_ADDRESS, CONF_MAC: MAC_ADDRESS, CONF_ALIAS: ALIAS, - CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_LEGACY, + CONF_DEVICE: _mocked_device(device_config=DEVICE_CONFIG_LEGACY), }, ) await hass.async_block_till_done() @@ -691,7 +738,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: CONF_HOST: IP_ADDRESS, CONF_MAC: MAC_ADDRESS, CONF_ALIAS: ALIAS, - CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_LEGACY, + CONF_DEVICE: _mocked_device(device_config=DEVICE_CONFIG_LEGACY), }, ), ], @@ -745,7 +792,7 @@ async def test_discovered_by_dhcp_or_discovery( CONF_HOST: IP_ADDRESS, CONF_MAC: MAC_ADDRESS, CONF_ALIAS: ALIAS, - CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_LEGACY, + CONF_DEVICE: _mocked_device(device_config=DEVICE_CONFIG_LEGACY), }, ), ], @@ -775,9 +822,11 @@ async def test_integration_discovery_with_ip_change( mock_connect: AsyncMock, ) -> None: """Test reauth flow.""" - mock_connect["connect"].side_effect = KasaException() mock_config_entry.add_to_hass(hass) - with patch("homeassistant.components.tplink.Discover.discover", return_value={}): + with ( + patch("homeassistant.components.tplink.Discover.discover", return_value={}), + override_side_effect(mock_connect["connect"], KasaException()), + ): await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() @@ -785,39 +834,57 @@ async def test_integration_discovery_with_ip_change( flows = hass.config_entries.flow.async_progress() assert len(flows) == 0 - assert mock_config_entry.data[CONF_DEVICE_CONFIG] == DEVICE_CONFIG_DICT_LEGACY - assert mock_config_entry.data[CONF_DEVICE_CONFIG].get(CONF_HOST) == "127.0.0.1" - - discovery_result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, - data={ - CONF_HOST: "127.0.0.2", - CONF_MAC: MAC_ADDRESS, - CONF_ALIAS: ALIAS, - CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_KLAP, - }, + assert ( + mock_config_entry.data[CONF_CONNECTION_PARAMETERS] + == CONN_PARAMS_LEGACY.to_dict() ) + assert mock_config_entry.data[CONF_HOST] == "127.0.0.1" + + mocked_device = _mocked_device(device_config=DEVICE_CONFIG_KLAP) + with override_side_effect(mock_connect["connect"], lambda *_, **__: mocked_device): + discovery_result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={ + CONF_HOST: "127.0.0.2", + CONF_MAC: MAC_ADDRESS, + CONF_ALIAS: ALIAS, + CONF_DEVICE: mocked_device, + }, + ) await hass.async_block_till_done() assert discovery_result["type"] is FlowResultType.ABORT assert discovery_result["reason"] == "already_configured" - assert mock_config_entry.data[CONF_DEVICE_CONFIG] == DEVICE_CONFIG_DICT_KLAP + assert ( + mock_config_entry.data[CONF_CONNECTION_PARAMETERS] == CONN_PARAMS_KLAP.to_dict() + ) assert mock_config_entry.data[CONF_HOST] == "127.0.0.2" config = DeviceConfig.from_dict(DEVICE_CONFIG_DICT_KLAP) + # Do a reload here and check that the + # new config is picked up in setup_entry mock_connect["connect"].reset_mock(side_effect=True) bulb = _mocked_device( device_config=config, mac=mock_config_entry.unique_id, ) - mock_connect["connect"].return_value = bulb - await hass.config_entries.async_reload(mock_config_entry.entry_id) - await hass.async_block_till_done() + + with ( + patch( + "homeassistant.components.tplink.async_create_clientsession", + return_value="Foo", + ), + override_side_effect(mock_connect["connect"], lambda *_, **__: bulb), + ): + await hass.config_entries.async_reload(mock_config_entry.entry_id) + await hass.async_block_till_done() assert mock_config_entry.state is ConfigEntryState.LOADED # Check that init set the new host correctly before calling connect assert config.host == "127.0.0.1" config.host = "127.0.0.2" + config.uses_http = False # Not passed in to new config class + config.http_client = "Foo" mock_connect["connect"].assert_awaited_once_with(config=config) @@ -831,8 +898,6 @@ async def test_integration_discovery_with_connection_change( And that connection_hash is removed as it will be invalid. """ - mock_connect["connect"].side_effect = KasaException() - mock_config_entry = MockConfigEntry( title="TPLink", domain=DOMAIN, @@ -840,7 +905,10 @@ async def test_integration_discovery_with_connection_change( unique_id=MAC_ADDRESS2, ) mock_config_entry.add_to_hass(hass) - with patch("homeassistant.components.tplink.Discover.discover", return_value={}): + with ( + patch("homeassistant.components.tplink.Discover.discover", return_value={}), + override_side_effect(mock_connect["connect"], KasaException()), + ): await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done(wait_background_tasks=True) @@ -854,43 +922,57 @@ async def test_integration_discovery_with_connection_change( == 0 ) assert mock_config_entry.data[CONF_HOST] == "127.0.0.2" - assert mock_config_entry.data[CONF_DEVICE_CONFIG] == DEVICE_CONFIG_DICT_AES - assert mock_config_entry.data[CONF_DEVICE_CONFIG].get(CONF_HOST) == "127.0.0.2" + assert ( + mock_config_entry.data[CONF_CONNECTION_PARAMETERS] == CONN_PARAMS_AES.to_dict() + ) assert mock_config_entry.data[CONF_CREDENTIALS_HASH] == CREDENTIALS_HASH_AES + mock_connect["connect"].reset_mock() NEW_DEVICE_CONFIG = { **DEVICE_CONFIG_DICT_KLAP, - CONF_CONNECTION_TYPE: CONNECTION_TYPE_KLAP_DICT, + "connection_type": CONN_PARAMS_KLAP.to_dict(), CONF_HOST: "127.0.0.2", } config = DeviceConfig.from_dict(NEW_DEVICE_CONFIG) # Reset the connect mock so when the config flow reloads the entry it succeeds - mock_connect["connect"].reset_mock(side_effect=True) + bulb = _mocked_device( device_config=config, mac=mock_config_entry.unique_id, ) - mock_connect["connect"].return_value = bulb - discovery_result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, - data={ - CONF_HOST: "127.0.0.2", - CONF_MAC: MAC_ADDRESS2, - CONF_ALIAS: ALIAS, - CONF_DEVICE_CONFIG: NEW_DEVICE_CONFIG, - }, - ) + with ( + patch( + "homeassistant.components.tplink.async_create_clientsession", + return_value="Foo", + ), + override_side_effect(mock_connect["connect"], lambda *_, **__: bulb), + ): + discovery_result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={ + CONF_HOST: "127.0.0.2", + CONF_MAC: MAC_ADDRESS2, + CONF_ALIAS: ALIAS, + CONF_DEVICE: bulb, + }, + ) await hass.async_block_till_done(wait_background_tasks=True) assert discovery_result["type"] is FlowResultType.ABORT assert discovery_result["reason"] == "already_configured" - assert mock_config_entry.data[CONF_DEVICE_CONFIG] == NEW_DEVICE_CONFIG + assert ( + mock_config_entry.data[CONF_CONNECTION_PARAMETERS] == CONN_PARAMS_KLAP.to_dict() + ) assert mock_config_entry.data[CONF_HOST] == "127.0.0.2" assert CREDENTIALS_HASH_AES not in mock_config_entry.data assert mock_config_entry.state is ConfigEntryState.LOADED + config.host = "127.0.0.2" + config.uses_http = False # Not passed in to new config class + config.http_client = "Foo" + config.aes_keys = AES_KEYS mock_connect["connect"].assert_awaited_once_with(config=config) @@ -901,17 +983,18 @@ async def test_dhcp_discovery_with_ip_change( mock_connect: AsyncMock, ) -> None: """Test dhcp discovery with an IP change.""" - mock_connect["connect"].side_effect = KasaException() mock_config_entry.add_to_hass(hass) - with patch("homeassistant.components.tplink.Discover.discover", return_value={}): + with ( + patch("homeassistant.components.tplink.Discover.discover", return_value={}), + override_side_effect(mock_connect["connect"], KasaException()), + ): await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY flows = hass.config_entries.flow.async_progress() assert len(flows) == 0 - assert mock_config_entry.data[CONF_DEVICE_CONFIG] == DEVICE_CONFIG_DICT_LEGACY - assert mock_config_entry.data[CONF_DEVICE_CONFIG].get(CONF_HOST) == "127.0.0.1" + assert mock_config_entry.data[CONF_HOST] == "127.0.0.1" discovery_result = await hass.config_entries.flow.async_init( DOMAIN, @@ -966,8 +1049,7 @@ async def test_reauth_update_with_encryption_change( caplog: pytest.LogCaptureFixture, ) -> None: """Test reauth flow.""" - orig_side_effect = mock_connect["connect"].side_effect - mock_connect["connect"].side_effect = AuthenticationError() + mock_config_entry = MockConfigEntry( title="TPLink", domain=DOMAIN, @@ -975,10 +1057,15 @@ async def test_reauth_update_with_encryption_change( unique_id=MAC_ADDRESS2, ) mock_config_entry.add_to_hass(hass) - assert mock_config_entry.data[CONF_DEVICE_CONFIG] == DEVICE_CONFIG_DICT_AES + assert ( + mock_config_entry.data[CONF_CONNECTION_PARAMETERS] == CONN_PARAMS_AES.to_dict() + ) assert mock_config_entry.data[CONF_CREDENTIALS_HASH] == CREDENTIALS_HASH_AES - with patch("homeassistant.components.tplink.Discover.discover", return_value={}): + with ( + patch("homeassistant.components.tplink.Discover.discover", return_value={}), + override_side_effect(mock_connect["connect"], AuthenticationError()), + ): await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR @@ -988,7 +1075,9 @@ async def test_reauth_update_with_encryption_change( assert len(flows) == 1 [result] = flows assert result["step_id"] == "reauth_confirm" - assert mock_config_entry.data[CONF_DEVICE_CONFIG] == DEVICE_CONFIG_DICT_AES + assert ( + mock_config_entry.data[CONF_CONNECTION_PARAMETERS] == CONN_PARAMS_AES.to_dict() + ) assert CONF_CREDENTIALS_HASH not in mock_config_entry.data new_config = DeviceConfig( @@ -1005,7 +1094,6 @@ async def test_reauth_update_with_encryption_change( mock_connect["mock_devices"]["127.0.0.2"].config = new_config mock_connect["mock_devices"]["127.0.0.2"].credentials_hash = CREDENTIALS_HASH_KLAP - mock_connect["connect"].side_effect = orig_side_effect result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ @@ -1023,10 +1111,10 @@ async def test_reauth_update_with_encryption_change( assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert mock_config_entry.state is ConfigEntryState.LOADED - assert mock_config_entry.data[CONF_DEVICE_CONFIG] == { - **DEVICE_CONFIG_DICT_KLAP, - CONF_HOST: "127.0.0.2", - } + assert ( + mock_config_entry.data[CONF_CONNECTION_PARAMETERS] == CONN_PARAMS_KLAP.to_dict() + ) + assert mock_config_entry.data[CONF_HOST] == "127.0.0.2" assert mock_config_entry.data[CONF_CREDENTIALS_HASH] == CREDENTIALS_HASH_KLAP @@ -1037,9 +1125,11 @@ async def test_reauth_update_from_discovery( mock_connect: AsyncMock, ) -> None: """Test reauth flow.""" - mock_connect["connect"].side_effect = AuthenticationError mock_config_entry.add_to_hass(hass) - with patch("homeassistant.components.tplink.Discover.discover", return_value={}): + with ( + patch("homeassistant.components.tplink.Discover.discover", return_value={}), + override_side_effect(mock_connect["connect"], AuthenticationError()), + ): await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() @@ -1049,22 +1139,32 @@ async def test_reauth_update_from_discovery( assert len(flows) == 1 [result] = flows assert result["step_id"] == "reauth_confirm" - assert mock_config_entry.data[CONF_DEVICE_CONFIG] == DEVICE_CONFIG_DICT_LEGACY - - discovery_result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, - data={ - CONF_HOST: IP_ADDRESS, - CONF_MAC: MAC_ADDRESS, - CONF_ALIAS: ALIAS, - CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_KLAP, - }, + assert ( + mock_config_entry.data[CONF_CONNECTION_PARAMETERS] + == CONN_PARAMS_LEGACY.to_dict() ) + + device = _mocked_device( + device_config=DEVICE_CONFIG_KLAP, + mac=mock_config_entry.unique_id, + ) + with override_side_effect(mock_connect["connect"], lambda *_, **__: device): + discovery_result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={ + CONF_HOST: IP_ADDRESS, + CONF_MAC: MAC_ADDRESS, + CONF_ALIAS: ALIAS, + CONF_DEVICE: device, + }, + ) await hass.async_block_till_done() assert discovery_result["type"] is FlowResultType.ABORT assert discovery_result["reason"] == "already_configured" - assert mock_config_entry.data[CONF_DEVICE_CONFIG] == DEVICE_CONFIG_DICT_KLAP + assert ( + mock_config_entry.data[CONF_CONNECTION_PARAMETERS] == CONN_PARAMS_KLAP.to_dict() + ) async def test_reauth_update_from_discovery_with_ip_change( @@ -1074,9 +1174,11 @@ async def test_reauth_update_from_discovery_with_ip_change( mock_connect: AsyncMock, ) -> None: """Test reauth flow.""" - mock_connect["connect"].side_effect = AuthenticationError() mock_config_entry.add_to_hass(hass) - with patch("homeassistant.components.tplink.Discover.discover", return_value={}): + with ( + patch("homeassistant.components.tplink.Discover.discover", return_value={}), + override_side_effect(mock_connect["connect"], AuthenticationError()), + ): await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR @@ -1085,22 +1187,32 @@ async def test_reauth_update_from_discovery_with_ip_change( assert len(flows) == 1 [result] = flows assert result["step_id"] == "reauth_confirm" - assert mock_config_entry.data[CONF_DEVICE_CONFIG] == DEVICE_CONFIG_DICT_LEGACY - - discovery_result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, - data={ - CONF_HOST: "127.0.0.2", - CONF_MAC: MAC_ADDRESS, - CONF_ALIAS: ALIAS, - CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_KLAP, - }, + assert ( + mock_config_entry.data[CONF_CONNECTION_PARAMETERS] + == CONN_PARAMS_LEGACY.to_dict() ) + + device = _mocked_device( + device_config=DEVICE_CONFIG_KLAP, + mac=mock_config_entry.unique_id, + ) + with override_side_effect(mock_connect["connect"], lambda *_, **__: device): + discovery_result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={ + CONF_HOST: "127.0.0.2", + CONF_MAC: MAC_ADDRESS, + CONF_ALIAS: ALIAS, + CONF_DEVICE: device, + }, + ) await hass.async_block_till_done() assert discovery_result["type"] is FlowResultType.ABORT assert discovery_result["reason"] == "already_configured" - assert mock_config_entry.data[CONF_DEVICE_CONFIG] == DEVICE_CONFIG_DICT_KLAP + assert ( + mock_config_entry.data[CONF_CONNECTION_PARAMETERS] == CONN_PARAMS_KLAP.to_dict() + ) assert mock_config_entry.data[CONF_HOST] == "127.0.0.2" @@ -1111,8 +1223,8 @@ async def test_reauth_no_update_if_config_and_ip_the_same( mock_connect: AsyncMock, ) -> None: """Test reauth discovery does not update when the host and config are the same.""" - mock_connect["connect"].side_effect = AuthenticationError() mock_config_entry.add_to_hass(hass) + hass.config_entries.async_update_entry( mock_config_entry, data={ @@ -1120,30 +1232,40 @@ async def test_reauth_no_update_if_config_and_ip_the_same( CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_KLAP, }, ) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() + with override_side_effect(mock_connect["connect"], AuthenticationError()): + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 [result] = flows assert result["step_id"] == "reauth_confirm" - assert mock_config_entry.data[CONF_DEVICE_CONFIG] == DEVICE_CONFIG_DICT_KLAP - - discovery_result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, - data={ - CONF_HOST: IP_ADDRESS, - CONF_MAC: MAC_ADDRESS, - CONF_ALIAS: ALIAS, - CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_KLAP, - }, + assert ( + mock_config_entry.data[CONF_CONNECTION_PARAMETERS] == CONN_PARAMS_KLAP.to_dict() ) + + device = _mocked_device( + device_config=DEVICE_CONFIG_KLAP, + mac=mock_config_entry.unique_id, + ) + with override_side_effect(mock_connect["connect"], lambda *_, **__: device): + discovery_result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={ + CONF_HOST: IP_ADDRESS, + CONF_MAC: MAC_ADDRESS, + CONF_ALIAS: ALIAS, + CONF_DEVICE: device, + }, + ) await hass.async_block_till_done() assert discovery_result["type"] is FlowResultType.ABORT assert discovery_result["reason"] == "already_configured" - assert mock_config_entry.data[CONF_DEVICE_CONFIG] == DEVICE_CONFIG_DICT_KLAP + assert ( + mock_config_entry.data[CONF_CONNECTION_PARAMETERS] == CONN_PARAMS_KLAP.to_dict() + ) assert mock_config_entry.data[CONF_HOST] == IP_ADDRESS @@ -1241,17 +1363,15 @@ async def test_pick_device_errors( assert result2["step_id"] == "pick_device" assert not result2["errors"] - default_connect_side_effect = mock_connect["connect"].side_effect - mock_connect["connect"].side_effect = error_type - result3 = await hass.config_entries.flow.async_configure( - result2["flow_id"], - {CONF_DEVICE: MAC_ADDRESS}, - ) - await hass.async_block_till_done() + with override_side_effect(mock_connect["connect"], error_type): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {CONF_DEVICE: MAC_ADDRESS}, + ) + await hass.async_block_till_done() assert result3["type"] == expected_flow if expected_flow != FlowResultType.ABORT: - mock_connect["connect"].side_effect = default_connect_side_effect result4 = await hass.config_entries.flow.async_configure( result3["flow_id"], user_input={ @@ -1300,17 +1420,17 @@ async def test_discovery_timeout_connect_legacy_error( DOMAIN, context={"source": config_entries.SOURCE_USER} ) mock_discovery["discover_single"].side_effect = TimeoutError - mock_connect["connect"].side_effect = KasaException await hass.async_block_till_done() assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] assert mock_connect["connect"].call_count == 0 - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_HOST: IP_ADDRESS} - ) - await hass.async_block_till_done() + with override_side_effect(mock_connect["connect"], KasaException): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_HOST: IP_ADDRESS} + ) + await hass.async_block_till_done() assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} assert mock_connect["connect"].call_count == 1 @@ -1334,17 +1454,17 @@ async def test_reauth_update_other_flows( data={**CREATE_ENTRY_DATA_AES}, unique_id=MAC_ADDRESS2, ) - default_side_effect = mock_connect["connect"].side_effect - mock_connect["connect"].side_effect = AuthenticationError() mock_config_entry.add_to_hass(hass) mock_config_entry2.add_to_hass(hass) - with patch("homeassistant.components.tplink.Discover.discover", return_value={}): + with ( + patch("homeassistant.components.tplink.Discover.discover", return_value={}), + override_side_effect(mock_connect["connect"], AuthenticationError()), + ): await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() assert mock_config_entry2.state is ConfigEntryState.SETUP_ERROR assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR - mock_connect["connect"].side_effect = default_side_effect await hass.async_block_till_done() @@ -1353,7 +1473,9 @@ async def test_reauth_update_other_flows( flows_by_entry_id = {flow["context"]["entry_id"]: flow for flow in flows} result = flows_by_entry_id[mock_config_entry.entry_id] assert result["step_id"] == "reauth_confirm" - assert mock_config_entry.data[CONF_DEVICE_CONFIG] == DEVICE_CONFIG_DICT_KLAP + assert ( + mock_config_entry.data[CONF_CONNECTION_PARAMETERS] == CONN_PARAMS_KLAP.to_dict() + ) result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ diff --git a/tests/components/tplink/test_init.py b/tests/components/tplink/test_init.py index 986aaebd170..dd01c381adf 100644 --- a/tests/components/tplink/test_init.py +++ b/tests/components/tplink/test_init.py @@ -4,6 +4,7 @@ from __future__ import annotations import copy from datetime import timedelta +from typing import Any from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch from freezegun.api import FrozenDateTimeFactory @@ -13,14 +14,18 @@ import pytest from homeassistant import setup from homeassistant.components import tplink from homeassistant.components.tplink.const import ( + CONF_AES_KEYS, + CONF_CONNECTION_PARAMETERS, CONF_CREDENTIALS_HASH, CONF_DEVICE_CONFIG, DOMAIN, ) from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.const import ( + CONF_ALIAS, CONF_AUTHENTICATION, CONF_HOST, + CONF_MODEL, CONF_PASSWORD, CONF_USERNAME, STATE_ON, @@ -33,13 +38,20 @@ from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util from . import ( + ALIAS, + CREATE_ENTRY_DATA_AES, CREATE_ENTRY_DATA_KLAP, CREATE_ENTRY_DATA_LEGACY, + CREDENTIALS_HASH_AES, + CREDENTIALS_HASH_KLAP, + DEVICE_CONFIG_AES, DEVICE_CONFIG_KLAP, + DEVICE_CONFIG_LEGACY, DEVICE_ID, DEVICE_ID_MAC, IP_ADDRESS, MAC_ADDRESS, + MODEL, _mocked_device, _patch_connect, _patch_discovery, @@ -207,16 +219,21 @@ async def test_config_entry_with_stored_credentials( hass.data.setdefault(DOMAIN, {})[CONF_AUTHENTICATION] = auth mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) + with patch( + "homeassistant.components.tplink.async_create_clientsession", return_value="Foo" + ): + await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() assert mock_config_entry.state is ConfigEntryState.LOADED - config = DEVICE_CONFIG_KLAP + config = DeviceConfig.from_dict(DEVICE_CONFIG_KLAP.to_dict()) + config.uses_http = False + config.http_client = "Foo" assert config.credentials != stored_credentials config.credentials = stored_credentials mock_connect["connect"].assert_called_once_with(config=config) -async def test_config_entry_device_config_invalid( +async def test_config_entry_conn_params_invalid( hass: HomeAssistant, mock_discovery: AsyncMock, mock_connect: AsyncMock, @@ -224,7 +241,7 @@ async def test_config_entry_device_config_invalid( ) -> None: """Test that an invalid device config logs an error and loads the config entry.""" entry_data = copy.deepcopy(CREATE_ENTRY_DATA_KLAP) - entry_data[CONF_DEVICE_CONFIG] = {"foo": "bar"} + entry_data[CONF_CONNECTION_PARAMETERS] = {"foo": "bar"} mock_config_entry = MockConfigEntry( title="TPLink", domain=DOMAIN, @@ -237,7 +254,7 @@ async def test_config_entry_device_config_invalid( assert mock_config_entry.state is ConfigEntryState.LOADED assert ( - f"Invalid connection type dict for {IP_ADDRESS}: {entry_data.get(CONF_DEVICE_CONFIG)}" + f"Invalid connection parameters dict for {IP_ADDRESS}: {entry_data.get(CONF_CONNECTION_PARAMETERS)}" in caplog.text ) @@ -495,8 +512,9 @@ async def test_unlink_devices( } assert device_entries[0].identifiers == set(test_identifiers) - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + with patch("homeassistant.components.tplink.CONF_CONFIG_ENTRY_MINOR_VERSION", 3): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() device_entries = dr.async_entries_for_config_entry(device_registry, entry.entry_id) @@ -504,7 +522,7 @@ async def test_unlink_devices( assert device_entries[0].identifiers == set(expected_identifiers) assert entry.version == 1 - assert entry.minor_version == 4 + assert entry.minor_version == 3 assert update_msg in caplog.text assert "Migration to version 1.3 complete" in caplog.text @@ -545,6 +563,7 @@ async def test_move_credentials_hash( with ( patch("homeassistant.components.tplink.Device.connect", new=_connect), patch("homeassistant.components.tplink.PLATFORMS", []), + patch("homeassistant.components.tplink.CONF_CONFIG_ENTRY_MINOR_VERSION", 4), ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -589,6 +608,7 @@ async def test_move_credentials_hash_auth_error( side_effect=AuthenticationError, ), patch("homeassistant.components.tplink.PLATFORMS", []), + patch("homeassistant.components.tplink.CONF_CONFIG_ENTRY_MINOR_VERSION", 4), ): entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) @@ -631,6 +651,7 @@ async def test_move_credentials_hash_other_error( "homeassistant.components.tplink.Device.connect", side_effect=KasaException ), patch("homeassistant.components.tplink.PLATFORMS", []), + patch("homeassistant.components.tplink.CONF_CONFIG_ENTRY_MINOR_VERSION", 4), ): entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) @@ -647,10 +668,8 @@ async def test_credentials_hash( hass: HomeAssistant, ) -> None: """Test credentials_hash used to call connect.""" - device_config = {**DEVICE_CONFIG_KLAP.to_dict(exclude_credentials=True)} entry_data = { **CREATE_ENTRY_DATA_KLAP, - CONF_DEVICE_CONFIG: device_config, CONF_CREDENTIALS_HASH: "theHash", } @@ -674,9 +693,7 @@ async def test_credentials_hash( await hass.async_block_till_done() assert entry.state is ConfigEntryState.LOADED - assert CONF_CREDENTIALS_HASH not in entry.data[CONF_DEVICE_CONFIG] assert CONF_CREDENTIALS_HASH in entry.data - assert entry.data[CONF_DEVICE_CONFIG] == device_config assert entry.data[CONF_CREDENTIALS_HASH] == "theHash" @@ -684,10 +701,8 @@ async def test_credentials_hash_auth_error( hass: HomeAssistant, ) -> None: """Test credentials_hash is deleted after an auth failure.""" - device_config = {**DEVICE_CONFIG_KLAP.to_dict(exclude_credentials=True)} entry_data = { **CREATE_ENTRY_DATA_KLAP, - CONF_DEVICE_CONFIG: device_config, CONF_CREDENTIALS_HASH: "theHash", } @@ -700,6 +715,10 @@ async def test_credentials_hash_auth_error( with ( patch("homeassistant.components.tplink.PLATFORMS", []), + patch( + "homeassistant.components.tplink.async_create_clientsession", + return_value="Foo", + ), patch( "homeassistant.components.tplink.Device.connect", side_effect=AuthenticationError, @@ -712,6 +731,76 @@ async def test_credentials_hash_auth_error( expected_config = DeviceConfig.from_dict( DEVICE_CONFIG_KLAP.to_dict(exclude_credentials=True, credentials_hash="theHash") ) + expected_config.uses_http = False + expected_config.http_client = "Foo" connect_mock.assert_called_with(config=expected_config) assert entry.state is ConfigEntryState.SETUP_ERROR assert CONF_CREDENTIALS_HASH not in entry.data + + +@pytest.mark.parametrize( + ("device_config", "expected_entry_data", "credentials_hash"), + [ + pytest.param( + DEVICE_CONFIG_KLAP, CREATE_ENTRY_DATA_KLAP, CREDENTIALS_HASH_KLAP, id="KLAP" + ), + pytest.param( + DEVICE_CONFIG_AES, CREATE_ENTRY_DATA_AES, CREDENTIALS_HASH_AES, id="AES" + ), + pytest.param(DEVICE_CONFIG_LEGACY, CREATE_ENTRY_DATA_LEGACY, None, id="Legacy"), + ], +) +async def test_migrate_remove_device_config( + hass: HomeAssistant, + mock_connect: AsyncMock, + caplog: pytest.LogCaptureFixture, + device_config: DeviceConfig, + expected_entry_data: dict[str, Any], + credentials_hash: str, +) -> None: + """Test credentials hash moved to parent. + + As async_setup_entry will succeed the hash on the parent is updated + from the device. + """ + OLD_CREATE_ENTRY_DATA = { + CONF_HOST: expected_entry_data[CONF_HOST], + CONF_ALIAS: ALIAS, + CONF_MODEL: MODEL, + CONF_DEVICE_CONFIG: device_config.to_dict(exclude_credentials=True), + } + + entry = MockConfigEntry( + title="TPLink", + domain=DOMAIN, + data=OLD_CREATE_ENTRY_DATA, + entry_id="123456", + unique_id=MAC_ADDRESS, + version=1, + minor_version=4, + ) + entry.add_to_hass(hass) + + async def _connect(config): + config.credentials_hash = credentials_hash + config.aes_keys = expected_entry_data.get(CONF_AES_KEYS) + return _mocked_device(device_config=config, credentials_hash=credentials_hash) + + with ( + patch("homeassistant.components.tplink.Device.connect", new=_connect), + patch("homeassistant.components.tplink.PLATFORMS", []), + patch( + "homeassistant.components.tplink.async_create_clientsession", + return_value="Foo", + ), + patch("homeassistant.components.tplink.CONF_CONFIG_ENTRY_MINOR_VERSION", 5), + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.minor_version == 5 + assert entry.state is ConfigEntryState.LOADED + assert CONF_DEVICE_CONFIG not in entry.data + assert entry.data == expected_entry_data + + assert "Migration to version 1.5 complete" in caplog.text From 1e63b956f5862da488afd3ffea54f480b59eb463 Mon Sep 17 00:00:00 2001 From: "Steven B." <51370195+sdb9696@users.noreply.github.com> Date: Tue, 10 Sep 2024 18:35:18 +0100 Subject: [PATCH 234/271] Bump tplink python-kasa lib to 0.7.3 (#125686) --- homeassistant/components/tplink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index 0d9761ec8ce..b655f2e646a 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -301,5 +301,5 @@ "iot_class": "local_polling", "loggers": ["kasa"], "quality_scale": "platinum", - "requirements": ["python-kasa[speedups]==0.7.2"] + "requirements": ["python-kasa[speedups]==0.7.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2a68f1a0749..72d5e2a4c07 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2313,7 +2313,7 @@ python-join-api==0.0.9 python-juicenet==1.1.0 # homeassistant.components.tplink -python-kasa[speedups]==0.7.2 +python-kasa[speedups]==0.7.3 # homeassistant.components.linkplay python-linkplay==0.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 60a76dee4e4..62366c6c503 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1831,7 +1831,7 @@ python-izone==1.2.9 python-juicenet==1.1.0 # homeassistant.components.tplink -python-kasa[speedups]==0.7.2 +python-kasa[speedups]==0.7.3 # homeassistant.components.linkplay python-linkplay==0.0.9 From d0b6ef877e11b7d229f74eb80ac6ad0bef5fc65d Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 10 Sep 2024 22:04:53 +0200 Subject: [PATCH 235/271] Fix incomfort invalid setpoint if override is reported as 0.0 (#125694) --- homeassistant/components/incomfort/climate.py | 4 +- .../incomfort/snapshots/test_climate.ambr | 70 ++++++++++++++++++- tests/components/incomfort/test_climate.py | 15 +++- 3 files changed, 85 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/incomfort/climate.py b/homeassistant/components/incomfort/climate.py index dc08ce8a6c0..eccf03588dc 100644 --- a/homeassistant/components/incomfort/climate.py +++ b/homeassistant/components/incomfort/climate.py @@ -90,8 +90,10 @@ class InComfortClimate(IncomfortEntity, ClimateEntity): As we set the override, we report back the override. The actual set point is is returned at a later time. + Some older thermostats return 0.0 as override, in that case we fallback to + the actual setpoint. """ - return self._room.override + return self._room.override or self._room.setpoint async def async_set_temperature(self, **kwargs: Any) -> None: """Set a new target temperature for this zone.""" diff --git a/tests/components/incomfort/snapshots/test_climate.ambr b/tests/components/incomfort/snapshots/test_climate.ambr index 05b2d4878d0..17adcbb3bab 100644 --- a/tests/components/incomfort/snapshots/test_climate.ambr +++ b/tests/components/incomfort/snapshots/test_climate.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_setup_platform[climate.thermostat_1-entry] +# name: test_setup_platform[legacy_thermostat][climate.thermostat_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -38,7 +38,73 @@ 'unit_of_measurement': None, }) # --- -# name: test_setup_platform[climate.thermostat_1-state] +# name: test_setup_platform[legacy_thermostat][climate.thermostat_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 21.4, + 'friendly_name': 'Thermostat 1', + 'hvac_action': , + 'hvac_modes': list([ + , + ]), + 'max_temp': 30.0, + 'min_temp': 5.0, + 'status': dict({ + 'override': 0.0, + 'room_temp': 21.42, + 'setpoint': 18.0, + }), + 'supported_features': , + 'temperature': 18.0, + }), + 'context': , + 'entity_id': 'climate.thermostat_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'heat', + }) +# --- +# name: test_setup_platform[new_thermostat][climate.thermostat_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + ]), + 'max_temp': 30.0, + 'min_temp': 5.0, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.thermostat_1', + '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': None, + 'platform': 'incomfort', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'c0ffeec0ffee_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup_platform[new_thermostat][climate.thermostat_1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_temperature': 21.4, diff --git a/tests/components/incomfort/test_climate.py b/tests/components/incomfort/test_climate.py index d5f7397aaaf..ae4c1cf31f7 100644 --- a/tests/components/incomfort/test_climate.py +++ b/tests/components/incomfort/test_climate.py @@ -2,6 +2,7 @@ from unittest.mock import MagicMock, patch +import pytest from syrupy import SnapshotAssertion from homeassistant.config_entries import ConfigEntry @@ -13,6 +14,14 @@ from tests.common import snapshot_platform @patch("homeassistant.components.incomfort.PLATFORMS", [Platform.CLIMATE]) +@pytest.mark.parametrize( + "mock_room_status", + [ + {"room_temp": 21.42, "setpoint": 18.0, "override": 18.0}, + {"room_temp": 21.42, "setpoint": 18.0, "override": 0.0}, + ], + ids=["new_thermostat", "legacy_thermostat"], +) async def test_setup_platform( hass: HomeAssistant, mock_incomfort: MagicMock, @@ -20,6 +29,10 @@ async def test_setup_platform( snapshot: SnapshotAssertion, mock_config_entry: ConfigEntry, ) -> None: - """Test the incomfort entities are set up correctly.""" + """Test the incomfort entities are set up correctly. + + Legacy thermostats report 0.0 as override if no override is set, + but new thermostat sync the override with the actual setpoint instead. + """ await hass.config_entries.async_setup(mock_config_entry.entry_id) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) From 1dcd5471a0863dbf4f38e702b9d765bc87024542 Mon Sep 17 00:00:00 2001 From: jonnynch Date: Thu, 12 Sep 2024 00:33:26 +1000 Subject: [PATCH 236/271] Bump to python-nest-sdm to 5.0.1 (#125706) --- homeassistant/components/nest/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 1b0697f7602..8453c51518d 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -20,5 +20,5 @@ "iot_class": "cloud_push", "loggers": ["google_nest_sdm"], "quality_scale": "platinum", - "requirements": ["google-nest-sdm==5.0.0"] + "requirements": ["google-nest-sdm==5.0.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 72d5e2a4c07..279ddf172f0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -992,7 +992,7 @@ google-cloud-texttospeech==2.16.3 google-generativeai==0.6.0 # homeassistant.components.nest -google-nest-sdm==5.0.0 +google-nest-sdm==5.0.1 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 62366c6c503..dcf9dfc64af 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -839,7 +839,7 @@ google-cloud-pubsub==2.13.11 google-generativeai==0.6.0 # homeassistant.components.nest -google-nest-sdm==5.0.0 +google-nest-sdm==5.0.1 # homeassistant.components.google_travel_time googlemaps==2.5.1 From 06d4b3281b78c718315118365020932527a68bf5 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 13 Sep 2024 11:34:06 -0400 Subject: [PATCH 237/271] Remove unused keys from the ZHA config schema (#125710) --- homeassistant/components/zha/helpers.py | 3 +- tests/components/zha/test_helpers.py | 39 +++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zha/helpers.py b/homeassistant/components/zha/helpers.py index f70c8a9cb3e..2030ffcdb3d 100644 --- a/homeassistant/components/zha/helpers.py +++ b/homeassistant/components/zha/helpers.py @@ -1166,7 +1166,8 @@ CONF_ZHA_OPTIONS_SCHEMA = vol.Schema( CONF_CONSIDER_UNAVAILABLE_BATTERY, default=CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY, ): cv.positive_int, - } + }, + extra=vol.REMOVE_EXTRA, ) CONF_ZHA_ALARM_SCHEMA = vol.Schema( diff --git a/tests/components/zha/test_helpers.py b/tests/components/zha/test_helpers.py index d3392685437..f6dc8291d9f 100644 --- a/tests/components/zha/test_helpers.py +++ b/tests/components/zha/test_helpers.py @@ -5,16 +5,23 @@ from typing import Any import pytest import voluptuous_serialize +from zigpy.application import ControllerApplication from zigpy.types.basic import uint16_t from zigpy.zcl.clusters import lighting +import homeassistant.components.zha.const as zha_const from homeassistant.components.zha.helpers import ( cluster_command_schema_to_vol_schema, convert_to_zcl_values, + create_zha_config, exclude_none_values, + get_zha_data, ) from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry _LOGGER = logging.getLogger(__name__) @@ -177,3 +184,35 @@ def test_exclude_none_values( for key in expected_output: assert expected_output[key] == obj[key] + + +async def test_create_zha_config_remove_unused( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_zigpy_connect: ControllerApplication, +) -> None: + """Test creating ZHA config data with unused keys.""" + config_entry.add_to_hass(hass) + + options = config_entry.options.copy() + options["custom_configuration"]["zha_options"]["some_random_key"] = "a value" + + hass.config_entries.async_update_entry(config_entry, options=options) + + assert ( + config_entry.options["custom_configuration"]["zha_options"]["some_random_key"] + == "a value" + ) + + status = await async_setup_component( + hass, + zha_const.DOMAIN, + {zha_const.DOMAIN: {zha_const.CONF_ENABLE_QUIRKS: False}}, + ) + assert status is True + await hass.async_block_till_done() + + ha_zha_data = get_zha_data(hass) + + # Does not error out + create_zha_config(hass, ha_zha_data) From d4be1f3666fea4d34e2e095db2d1d8b1ac499a63 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 11 Sep 2024 12:51:39 +0200 Subject: [PATCH 238/271] Bump sfrbox-api to 0.0.11 (#125732) * Bump sfrbox-api to 0.0.11 * Re-enable tests --- homeassistant/components/sfr_box/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/sfr_box/snapshots/test_diagnostics.ambr | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/sfr_box/manifest.json b/homeassistant/components/sfr_box/manifest.json index cd42997cec5..a2d65e9819d 100644 --- a/homeassistant/components/sfr_box/manifest.json +++ b/homeassistant/components/sfr_box/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/sfr_box", "integration_type": "device", "iot_class": "local_polling", - "requirements": ["sfrbox-api==0.0.10"] + "requirements": ["sfrbox-api==0.0.11"] } diff --git a/requirements_all.txt b/requirements_all.txt index 279ddf172f0..c1c26e992ef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2595,7 +2595,7 @@ sensorpush-ble==1.6.2 sentry-sdk==1.40.3 # homeassistant.components.sfr_box -sfrbox-api==0.0.10 +sfrbox-api==0.0.11 # homeassistant.components.sharkiq sharkiq==1.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dcf9dfc64af..044cc11f2ec 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2053,7 +2053,7 @@ sensorpush-ble==1.6.2 sentry-sdk==1.40.3 # homeassistant.components.sfr_box -sfrbox-api==0.0.10 +sfrbox-api==0.0.11 # homeassistant.components.sharkiq sharkiq==1.0.2 diff --git a/tests/components/sfr_box/snapshots/test_diagnostics.ambr b/tests/components/sfr_box/snapshots/test_diagnostics.ambr index 69139c2c374..22a914f8a79 100644 --- a/tests/components/sfr_box/snapshots/test_diagnostics.ambr +++ b/tests/components/sfr_box/snapshots/test_diagnostics.ambr @@ -31,7 +31,7 @@ 'product_id': 'NB6VAC-FXC-r0', 'refclient': '', 'serial_number': '**REDACTED**', - 'temperature': 27560.0, + 'temperature': 27560, 'uptime': 2353575, 'version_bootloader': 'NB6VAC-BOOTLOADER-R4.0.8', 'version_dsldriver': 'NB6VAC-XDSL-A2pv6F039p', @@ -90,7 +90,7 @@ 'product_id': 'NB6VAC-FXC-r0', 'refclient': '', 'serial_number': '**REDACTED**', - 'temperature': 27560.0, + 'temperature': 27560, 'uptime': 2353575, 'version_bootloader': 'NB6VAC-BOOTLOADER-R4.0.8', 'version_dsldriver': 'NB6VAC-XDSL-A2pv6F039p', From 4583e070df9573d46641fa447013c5607eaae94d Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Wed, 11 Sep 2024 12:23:23 +0200 Subject: [PATCH 239/271] Update knx-frontend to 2024.9.10.221729 (#125734) --- homeassistant/components/knx/manifest.json | 2 +- .../components/knx/storage/entity_store_validation.py | 5 ++++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 181dca6f4b8..76212496dec 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -13,7 +13,7 @@ "requirements": [ "xknx==3.1.1", "xknxproject==3.7.1", - "knx-frontend==2024.9.4.64538" + "knx-frontend==2024.9.10.221729" ], "single_config_entry": true } diff --git a/homeassistant/components/knx/storage/entity_store_validation.py b/homeassistant/components/knx/storage/entity_store_validation.py index e9997bd9f1a..9bad5297853 100644 --- a/homeassistant/components/knx/storage/entity_store_validation.py +++ b/homeassistant/components/knx/storage/entity_store_validation.py @@ -38,7 +38,10 @@ def parse_invalid(exc: vol.Invalid) -> _ErrorDescription: def validate_entity_data(entity_data: dict) -> dict: - """Validate entity data. Return validated data or raise EntityStoreValidationException.""" + """Validate entity data. + + Return validated data or raise EntityStoreValidationException. + """ try: # return so defaults are applied return ENTITY_STORE_DATA_SCHEMA(entity_data) # type: ignore[no-any-return] diff --git a/requirements_all.txt b/requirements_all.txt index c1c26e992ef..9f2582bfd95 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1225,7 +1225,7 @@ kiwiki-client==0.1.1 knocki==0.3.1 # homeassistant.components.knx -knx-frontend==2024.9.4.64538 +knx-frontend==2024.9.10.221729 # homeassistant.components.konnected konnected==1.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 044cc11f2ec..6e6df238769 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1021,7 +1021,7 @@ kegtron-ble==0.4.0 knocki==0.3.1 # homeassistant.components.knx -knx-frontend==2024.9.4.64538 +knx-frontend==2024.9.10.221729 # homeassistant.components.konnected konnected==1.2.0 From 20ded56c99196c3d047140c777d5d28cfa2fe105 Mon Sep 17 00:00:00 2001 From: Assaf Akrabi Date: Wed, 11 Sep 2024 16:48:20 +0300 Subject: [PATCH 240/271] Bump russound to 0.2.0 (#125743) * Update russound library to fix BrokenPipeError * Remove library from license expection list --- homeassistant/components/russound_rnet/manifest.json | 2 +- homeassistant/components/russound_rnet/media_player.py | 8 +++++++- requirements_all.txt | 2 +- script/licenses.py | 1 - 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/russound_rnet/manifest.json b/homeassistant/components/russound_rnet/manifest.json index a93e3fe5a87..90bf5d5a7f3 100644 --- a/homeassistant/components/russound_rnet/manifest.json +++ b/homeassistant/components/russound_rnet/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/russound_rnet", "iot_class": "local_polling", "loggers": ["russound"], - "requirements": ["russound==0.1.9"] + "requirements": ["russound==0.2.0"] } diff --git a/homeassistant/components/russound_rnet/media_player.py b/homeassistant/components/russound_rnet/media_player.py index a08cfbe7747..f8369ed64ca 100644 --- a/homeassistant/components/russound_rnet/media_player.py +++ b/homeassistant/components/russound_rnet/media_player.py @@ -96,7 +96,13 @@ class RussoundRNETDevice(MediaPlayerEntity): # Updated this function to make a single call to get_zone_info, so that # with a single call we can get On/Off, Volume and Source, reducing the # amount of traffic and speeding up the update process. - ret = self._russ.get_zone_info(self._controller_id, self._zone_id, 4) + try: + ret = self._russ.get_zone_info(self._controller_id, self._zone_id, 4) + except BrokenPipeError: + _LOGGER.error("Broken Pipe Error, trying to reconnect to Russound RNET") + self._russ.connect() + ret = self._russ.get_zone_info(self._controller_id, self._zone_id, 4) + _LOGGER.debug("ret= %s", ret) if ret is not None: _LOGGER.debug( diff --git a/requirements_all.txt b/requirements_all.txt index 9f2582bfd95..0ff67e3f696 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2546,7 +2546,7 @@ rpi-bad-power==0.1.0 rtsp-to-webrtc==0.5.1 # homeassistant.components.russound_rnet -russound==0.1.9 +russound==0.2.0 # homeassistant.components.ruuvitag_ble ruuvitag-ble==0.1.2 diff --git a/script/licenses.py b/script/licenses.py index ac9a836396c..347362dec16 100644 --- a/script/licenses.py +++ b/script/licenses.py @@ -160,7 +160,6 @@ EXCEPTIONS = { "pyvera", # https://github.com/maximvelichko/pyvera/pull/164 "pyxeoma", # https://github.com/jeradM/pyxeoma/pull/11 "repoze.lru", - "russound", # https://github.com/laf/russound/pull/14 # codespell:ignore laf "ruuvitag-ble", # https://github.com/Bluetooth-Devices/ruuvitag-ble/pull/10 "sensirion-ble", # https://github.com/akx/sensirion-ble/pull/9 "sharp_aquos_rc", # https://github.com/jmoore987/sharp_aquos_rc/pull/14 From f365995c8addd8307337fecb4fe08fc9b6f91db2 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 11 Sep 2024 15:58:23 +0200 Subject: [PATCH 241/271] Fix favorite position missing for Motion Blinds TDBU devices (#125750) * Add favorite position for TDBU * fix styling --- homeassistant/components/motion_blinds/button.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/motion_blinds/button.py b/homeassistant/components/motion_blinds/button.py index 30f1cd53e6f..89841bf8fd4 100644 --- a/homeassistant/components/motion_blinds/button.py +++ b/homeassistant/components/motion_blinds/button.py @@ -26,7 +26,13 @@ async def async_setup_entry( coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] for blind in motion_gateway.device_list.values(): - if blind.limit_status == LimitStatus.Limit3Detected.name: + if blind.limit_status in ( + LimitStatus.Limit3Detected.name, + { + "T": LimitStatus.Limit3Detected.name, + "B": LimitStatus.Limit3Detected.name, + }, + ): entities.append(MotionGoFavoriteButton(coordinator, blind)) entities.append(MotionSetFavoriteButton(coordinator, blind)) From 8a6eec925f278f82c0f47b45074903579a54a155 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 16 Sep 2024 09:37:46 -0400 Subject: [PATCH 242/271] Add missing Zigbee/Thread firmware config flow translations (#125782) --- .../components/homeassistant_hardware/strings.json | 3 ++- .../components/homeassistant_sky_connect/strings.json | 8 ++++++-- .../components/homeassistant_yellow/strings.json | 3 ++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/homeassistant_hardware/strings.json b/homeassistant/components/homeassistant_hardware/strings.json index dbbb2057323..b483df75d75 100644 --- a/homeassistant/components/homeassistant_hardware/strings.json +++ b/homeassistant/components/homeassistant_hardware/strings.json @@ -51,7 +51,8 @@ "not_hassio_thread": "The OpenThread Border Router addon can only be installed with Home Assistant OS. If you would like to use the {model} as an Thread border router, please flash the firmware manually using the [web flasher]({docs_web_flasher_url}) and set up OpenThread Border Router to communicate with it.", "otbr_addon_already_running": "The OpenThread Border Router add-on is already running, it cannot be installed again.", "zha_still_using_stick": "This {model} is in use by the Zigbee Home Automation integration. Please migrate your Zigbee network to another adapter or delete the integration and try again.", - "otbr_still_using_stick": "This {model} is in use by the OpenThread Border Router add-on. If you use the Thread network, make sure you have alternative border routers. Uninstall the add-on and try again." + "otbr_still_using_stick": "This {model} is in use by the OpenThread Border Router add-on. If you use the Thread network, make sure you have alternative border routers. Uninstall the add-on and try again.", + "unsupported_firmware": "The radio firmware on your {model} could not be determined. Make sure that no other integration or addon is currently trying to communicate with the device. If you are running Home Assistant OS in a virtual machine or in Docker, please make sure that permissions are set correctly for the device." }, "progress": { "install_zigbee_flasher_addon": "The Silicon Labs Flasher addon is installed, this may take a few minutes.", diff --git a/homeassistant/components/homeassistant_sky_connect/strings.json b/homeassistant/components/homeassistant_sky_connect/strings.json index 20f587c2dbb..a596b9846ce 100644 --- a/homeassistant/components/homeassistant_sky_connect/strings.json +++ b/homeassistant/components/homeassistant_sky_connect/strings.json @@ -113,7 +113,8 @@ "not_hassio_thread": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::not_hassio_thread%]", "otbr_addon_already_running": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_addon_already_running%]", "zha_still_using_stick": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::zha_still_using_stick%]", - "otbr_still_using_stick": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_still_using_stick%]" + "otbr_still_using_stick": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_still_using_stick%]", + "unsupported_firmware": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::unsupported_firmware%]" }, "progress": { "install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]", @@ -181,7 +182,10 @@ "zha_migration_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::zha_migration_failed%]", "not_hassio": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::not_hassio%]", "not_hassio_thread": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::not_hassio_thread%]", - "otbr_addon_already_running": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_addon_already_running%]" + "otbr_addon_already_running": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_addon_already_running%]", + "zha_still_using_stick": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::zha_still_using_stick%]", + "otbr_still_using_stick": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_still_using_stick%]", + "unsupported_firmware": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::unsupported_firmware%]" }, "progress": { "install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]", diff --git a/homeassistant/components/homeassistant_yellow/strings.json b/homeassistant/components/homeassistant_yellow/strings.json index fd3be3586b1..b089e483899 100644 --- a/homeassistant/components/homeassistant_yellow/strings.json +++ b/homeassistant/components/homeassistant_yellow/strings.json @@ -138,7 +138,8 @@ "not_hassio_thread": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::not_hassio_thread%]", "otbr_addon_already_running": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_addon_already_running%]", "zha_still_using_stick": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::zha_still_using_stick%]", - "otbr_still_using_stick": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_still_using_stick%]" + "otbr_still_using_stick": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_still_using_stick%]", + "unsupported_firmware": "The radio firmware on your {model} could not be determined. Make sure that no other integration or addon is currently trying to communicate with the device." }, "progress": { "install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]", From 16e049b7fa78d2e07ad146b70494fdba8642e007 Mon Sep 17 00:00:00 2001 From: Josef Zweck <24647999+zweckj@users.noreply.github.com> Date: Fri, 13 Sep 2024 14:08:29 +0200 Subject: [PATCH 243/271] Bump lmcloud to 1.2.3 (#125801) --- homeassistant/components/lamarzocco/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lamarzocco/manifest.json b/homeassistant/components/lamarzocco/manifest.json index 181a2b9ab9b..a1da8982cd8 100644 --- a/homeassistant/components/lamarzocco/manifest.json +++ b/homeassistant/components/lamarzocco/manifest.json @@ -22,5 +22,5 @@ "integration_type": "device", "iot_class": "cloud_polling", "loggers": ["lmcloud"], - "requirements": ["lmcloud==1.2.2"] + "requirements": ["lmcloud==1.2.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 0ff67e3f696..ab9ff5f80d3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1282,7 +1282,7 @@ linear-garage-door==0.2.9 linode-api==4.1.9b1 # homeassistant.components.lamarzocco -lmcloud==1.2.2 +lmcloud==1.2.3 # homeassistant.components.google_maps locationsharinglib==5.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6e6df238769..2abc4e030e6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1060,7 +1060,7 @@ libsoundtouch==0.8 linear-garage-door==0.2.9 # homeassistant.components.lamarzocco -lmcloud==1.2.2 +lmcloud==1.2.3 # homeassistant.components.london_underground london-tube-status==0.5 From 359f61e55ac97a8f916fc6599b16f6a5f00d54d1 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 13 Sep 2024 14:15:58 -0400 Subject: [PATCH 244/271] Bump ZHA to 0.0.33 (#125914) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index df60829a1e2..7046642160c 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -21,7 +21,7 @@ "zha", "universal_silabs_flasher" ], - "requirements": ["universal-silabs-flasher==0.0.22", "zha==0.0.32"], + "requirements": ["universal-silabs-flasher==0.0.22", "zha==0.0.33"], "usb": [ { "vid": "10C4", diff --git a/requirements_all.txt b/requirements_all.txt index ab9ff5f80d3..d93b416a1a4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3009,7 +3009,7 @@ zeroconf==0.133.0 zeversolar==0.3.1 # homeassistant.components.zha -zha==0.0.32 +zha==0.0.33 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2abc4e030e6..67eb8f0b58c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2383,7 +2383,7 @@ zeroconf==0.133.0 zeversolar==0.3.1 # homeassistant.components.zha -zha==0.0.32 +zha==0.0.33 # homeassistant.components.zwave_js zwave-js-server-python==0.57.0 From 0b226c1868050810f822ee663062893e1d30809c Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 14 Sep 2024 16:36:32 +0200 Subject: [PATCH 245/271] Bump motionblinds to 0.6.25 (#125957) --- homeassistant/components/motion_blinds/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/motion_blinds/manifest.json b/homeassistant/components/motion_blinds/manifest.json index e1e12cf6729..b327c146300 100644 --- a/homeassistant/components/motion_blinds/manifest.json +++ b/homeassistant/components/motion_blinds/manifest.json @@ -21,5 +21,5 @@ "documentation": "https://www.home-assistant.io/integrations/motion_blinds", "iot_class": "local_push", "loggers": ["motionblinds"], - "requirements": ["motionblinds==0.6.24"] + "requirements": ["motionblinds==0.6.25"] } diff --git a/requirements_all.txt b/requirements_all.txt index d93b416a1a4..b4a08b6da18 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1366,7 +1366,7 @@ monzopy==1.3.2 mopeka-iot-ble==0.8.0 # homeassistant.components.motion_blinds -motionblinds==0.6.24 +motionblinds==0.6.25 # homeassistant.components.motionblinds_ble motionblindsble==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 67eb8f0b58c..18ca788d3f6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1132,7 +1132,7 @@ monzopy==1.3.2 mopeka-iot-ble==0.8.0 # homeassistant.components.motion_blinds -motionblinds==0.6.24 +motionblinds==0.6.25 # homeassistant.components.motionblinds_ble motionblindsble==0.1.1 From d91cc96cd2dcb92c854678418920326bd0e74ef2 Mon Sep 17 00:00:00 2001 From: Galorhallen <12990764+Galorhallen@users.noreply.github.com> Date: Sat, 14 Sep 2024 23:42:38 +0200 Subject: [PATCH 246/271] Bump govee light local to 1.5.2 (#125968) Update govee light local library --- homeassistant/components/govee_light_local/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/govee_light_local/manifest.json b/homeassistant/components/govee_light_local/manifest.json index 168a13e2477..b6b25f5aa09 100644 --- a/homeassistant/components/govee_light_local/manifest.json +++ b/homeassistant/components/govee_light_local/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["network"], "documentation": "https://www.home-assistant.io/integrations/govee_light_local", "iot_class": "local_push", - "requirements": ["govee-local-api==1.5.1"] + "requirements": ["govee-local-api==1.5.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index b4a08b6da18..ca9ba2423e9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1007,7 +1007,7 @@ gotailwind==0.2.3 govee-ble==0.40.0 # homeassistant.components.govee_light_local -govee-local-api==1.5.1 +govee-local-api==1.5.2 # homeassistant.components.remote_rpi_gpio gpiozero==1.6.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 18ca788d3f6..bc69ea266c7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -851,7 +851,7 @@ gotailwind==0.2.3 govee-ble==0.40.0 # homeassistant.components.govee_light_local -govee-local-api==1.5.1 +govee-local-api==1.5.2 # homeassistant.components.gpsd gps3==0.33.3 From c4eca4469f96fd527ca366d8c7e7870d65f1fb77 Mon Sep 17 00:00:00 2001 From: Noah Husby <32528627+noahhusby@users.noreply.github.com> Date: Sat, 14 Sep 2024 17:00:59 -0400 Subject: [PATCH 247/271] Bump aiorussound to 3.0.5 (#125975) --- homeassistant/components/russound_rio/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/russound_rio/manifest.json b/homeassistant/components/russound_rio/manifest.json index 19273de92ee..0a18bdb3b8a 100644 --- a/homeassistant/components/russound_rio/manifest.json +++ b/homeassistant/components/russound_rio/manifest.json @@ -7,5 +7,5 @@ "iot_class": "local_push", "loggers": ["aiorussound"], "quality_scale": "silver", - "requirements": ["aiorussound==3.0.4"] + "requirements": ["aiorussound==3.0.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index ca9ba2423e9..71a87379f21 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -350,7 +350,7 @@ aioridwell==2024.01.0 aioruckus==0.41 # homeassistant.components.russound_rio -aiorussound==3.0.4 +aiorussound==3.0.5 # homeassistant.components.ruuvi_gateway aioruuvigateway==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bc69ea266c7..f1f4adafbb6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -332,7 +332,7 @@ aioridwell==2024.01.0 aioruckus==0.41 # homeassistant.components.russound_rio -aiorussound==3.0.4 +aiorussound==3.0.5 # homeassistant.components.ruuvi_gateway aioruuvigateway==0.1.0 From fae26ee5da4822657c48dc301c828b5b80b331e7 Mon Sep 17 00:00:00 2001 From: TimL Date: Mon, 16 Sep 2024 21:45:39 +1000 Subject: [PATCH 248/271] Abort zeroconf flow on connect error during discovery (#125980) Abort zereconf flow on connect error during discovery --- homeassistant/components/smlight/config_flow.py | 7 ++++++- tests/components/smlight/test_config_flow.py | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/smlight/config_flow.py b/homeassistant/components/smlight/config_flow.py index 1b8cc4efeb1..8b502856e4c 100644 --- a/homeassistant/components/smlight/config_flow.py +++ b/homeassistant/components/smlight/config_flow.py @@ -94,8 +94,13 @@ class SmlightConfigFlow(ConfigFlow, domain=DOMAIN): mac = discovery_info.properties.get("mac") # fallback for legacy firmware if mac is None: - info = await self.client.get_info() + try: + info = await self.client.get_info() + except SmlightConnectionError: + # User is likely running unsupported ESPHome firmware + return self.async_abort(reason="cannot_connect") mac = info.MAC + await self.async_set_unique_id(format_mac(mac)) self._abort_if_unique_id_configured() diff --git a/tests/components/smlight/test_config_flow.py b/tests/components/smlight/test_config_flow.py index 9a23a8de753..328ae78c47b 100644 --- a/tests/components/smlight/test_config_flow.py +++ b/tests/components/smlight/test_config_flow.py @@ -336,6 +336,22 @@ async def test_zeroconf_cannot_connect( assert result2["reason"] == "cannot_connect" +async def test_zeroconf_legacy_cannot_connect( + hass: HomeAssistant, mock_smlight_client: MagicMock +) -> None: + """Test we abort flow on zeroconf discovery unsupported firmware.""" + mock_smlight_client.get_info.side_effect = SmlightConnectionError + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=DISCOVERY_INFO_LEGACY, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "cannot_connect" + + @pytest.mark.usefixtures("mock_smlight_client") async def test_zeroconf_legacy_mac( hass: HomeAssistant, mock_smlight_client: MagicMock, mock_setup_entry: AsyncMock From d259055af08d85f2f9d07a4c574bd85975b96d1d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 16 Sep 2024 16:55:02 +0200 Subject: [PATCH 249/271] Bump version to 2024.9.2 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 49f4914e4b9..eec4530576d 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "1" +PATCH_VERSION: Final = "2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 0af28ce0fe8..ee32a56651e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.9.1" +version = "2024.9.2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 861fcbe598335336abb7e76c4129a2844a33c225 Mon Sep 17 00:00:00 2001 From: Jason Hunter Date: Wed, 11 Sep 2024 03:35:05 -0400 Subject: [PATCH 250/271] Pin pyasn1 until fixed (#125712) * pin pyasn1 until fixed * add to gen requirements --- homeassistant/package_constraints.txt | 6 ++++++ script/gen_requirements_all.py | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d6f4dfcf0ab..b6a36544f8e 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -185,3 +185,9 @@ tuf>=4.0.0 # https://github.com/jd/tenacity/issues/471 tenacity!=8.4.0 + +# pyasn1.compat.octets was removed in pyasn1 0.6.1 and breaks some integrations +# and tests that import it directly +# https://github.com/pyasn1/pyasn1/pull/60 +# https://github.com/lextudio/pysnmp/issues/114 +pyasn1==0.6.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index b2165289ad8..8d4a9154a0f 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -206,6 +206,12 @@ tuf>=4.0.0 # https://github.com/jd/tenacity/issues/471 tenacity!=8.4.0 + +# pyasn1.compat.octets was removed in pyasn1 0.6.1 and breaks some integrations +# and tests that import it directly +# https://github.com/pyasn1/pyasn1/pull/60 +# https://github.com/lextudio/pysnmp/issues/114 +pyasn1==0.6.0 """ GENERATED_MESSAGE = ( From c64222de4f3ca7726b0ee8b10a6d248b43a15b5a Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Thu, 19 Sep 2024 19:04:27 +1000 Subject: [PATCH 251/271] Fix wall connector state in Teslemetry (#124149) * Fix wall connector state * review feedback * Rename None to Disconnected * Translate disconnected --- homeassistant/components/teslemetry/entity.py | 7 +++++++ homeassistant/components/teslemetry/sensor.py | 19 ++++++++++--------- .../components/teslemetry/strings.json | 5 ++++- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/teslemetry/entity.py b/homeassistant/components/teslemetry/entity.py index 74c1fdd52b1..bba678f754b 100644 --- a/homeassistant/components/teslemetry/entity.py +++ b/homeassistant/components/teslemetry/entity.py @@ -192,3 +192,10 @@ class TeslemetryWallConnectorEntity( .get(self.din, {}) .get(self.key) ) + + @property + def exists(self) -> bool: + """Return True if it exists in the wall connector coordinator data.""" + return self.key in self.coordinator.data.get("wall_connectors", {}).get( + self.din, {} + ) diff --git a/homeassistant/components/teslemetry/sensor.py b/homeassistant/components/teslemetry/sensor.py index 90b37cc1dac..b63f6b905b4 100644 --- a/homeassistant/components/teslemetry/sensor.py +++ b/homeassistant/components/teslemetry/sensor.py @@ -379,18 +379,18 @@ ENERGY_LIVE_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription(key="island_status", device_class=SensorDeviceClass.ENUM), ) -WALL_CONNECTOR_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( +WALL_CONNECTOR_DESCRIPTIONS: tuple[TeslemetrySensorEntityDescription, ...] = ( + TeslemetrySensorEntityDescription( key="wall_connector_state", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + TeslemetrySensorEntityDescription( key="wall_connector_fault_state", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + TeslemetrySensorEntityDescription( key="wall_connector_power", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.WATT, @@ -398,8 +398,9 @@ WALL_CONNECTOR_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = ( suggested_display_precision=2, device_class=SensorDeviceClass.POWER, ), - SensorEntityDescription( + TeslemetrySensorEntityDescription( key="vin", + value_fn=lambda vin: vin or "disconnected", ), ) @@ -525,13 +526,13 @@ class TeslemetryEnergyLiveSensorEntity(TeslemetryEnergyLiveEntity, SensorEntity) class TeslemetryWallConnectorSensorEntity(TeslemetryWallConnectorEntity, SensorEntity): """Base class for Teslemetry energy site metric sensors.""" - entity_description: SensorEntityDescription + entity_description: TeslemetrySensorEntityDescription def __init__( self, data: TeslemetryEnergyData, din: str, - description: SensorEntityDescription, + description: TeslemetrySensorEntityDescription, ) -> None: """Initialize the sensor.""" self.entity_description = description @@ -543,8 +544,8 @@ class TeslemetryWallConnectorSensorEntity(TeslemetryWallConnectorEntity, SensorE def _async_update_attrs(self) -> None: """Update the attributes of the sensor.""" - self._attr_available = not self.is_none - self._attr_native_value = self._value + if self.exists: + self._attr_native_value = self.entity_description.value_fn(self._value) class TeslemetryEnergyInfoSensorEntity(TeslemetryEnergyInfoEntity, SensorEntity): diff --git a/homeassistant/components/teslemetry/strings.json b/homeassistant/components/teslemetry/strings.json index 48eb4aae8bc..29c9ef3bbb7 100644 --- a/homeassistant/components/teslemetry/strings.json +++ b/homeassistant/components/teslemetry/strings.json @@ -420,7 +420,10 @@ "name": "version" }, "vin": { - "name": "Vehicle" + "name": "Vehicle", + "state": { + "disconnected": "Disconnected" + } }, "vpp_backup_reserve_percent": { "name": "VPP backup reserve" From d924fc5967c4964ea08b468e5e1e26cb227ac3a3 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Tue, 17 Sep 2024 16:34:26 +0200 Subject: [PATCH 252/271] Fix set brightness for Netatmo lights (#126075) * fix set brightness for Netatmo lights * round returns int by default * Update homeassistant/components/netatmo/light.py --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/netatmo/light.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/netatmo/light.py b/homeassistant/components/netatmo/light.py index b1871e9dabb..fe30dc0eaa4 100644 --- a/homeassistant/components/netatmo/light.py +++ b/homeassistant/components/netatmo/light.py @@ -173,7 +173,9 @@ class NetatmoLight(NetatmoModuleEntity, LightEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn light on.""" if ATTR_BRIGHTNESS in kwargs: - await self.device.async_set_brightness(kwargs[ATTR_BRIGHTNESS]) + await self.device.async_set_brightness( + round(kwargs[ATTR_BRIGHTNESS] / 2.55) + ) else: await self.device.async_on() @@ -194,6 +196,6 @@ class NetatmoLight(NetatmoModuleEntity, LightEntity): if (brightness := self.device.brightness) is not None: # Netatmo uses a range of [0, 100] to control brightness - self._attr_brightness = round((brightness / 100) * 255) + self._attr_brightness = round(brightness * 2.55) else: self._attr_brightness = None From 991114eb7f11488f8f43d993cbb86abae960578e Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Wed, 18 Sep 2024 16:26:09 +0200 Subject: [PATCH 253/271] Update Aseko to support new API (#126133) * Update Aseko to support new API * Apply suggestions from code review Co-authored-by: Joost Lekkerkerker * Use self.unit instead of self._unit * Refactor sensor setup entry * Keep same unique id and identifier * Revert rename free_chlorine translation key * Remove new heating entity to keep PR small * Fix keep same unique id --------- Co-authored-by: Joost Lekkerkerker --- .../components/aseko_pool_live/__init__.py | 29 ++--- .../aseko_pool_live/binary_sensor.py | 52 +++----- .../components/aseko_pool_live/config_flow.py | 20 ++- .../components/aseko_pool_live/coordinator.py | 23 ++-- .../components/aseko_pool_live/entity.py | 49 ++++++-- .../components/aseko_pool_live/icons.json | 15 ++- .../components/aseko_pool_live/manifest.json | 2 +- .../components/aseko_pool_live/sensor.py | 117 +++++++++++------- .../components/aseko_pool_live/strings.json | 13 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/aseko_pool_live/conftest.py | 20 +++ .../aseko_pool_live/test_config_flow.py | 54 ++++---- 13 files changed, 222 insertions(+), 176 deletions(-) create mode 100644 tests/components/aseko_pool_live/conftest.py diff --git a/homeassistant/components/aseko_pool_live/__init__.py b/homeassistant/components/aseko_pool_live/__init__.py index 5773b3eb5b9..5985af4d023 100644 --- a/homeassistant/components/aseko_pool_live/__init__.py +++ b/homeassistant/components/aseko_pool_live/__init__.py @@ -4,13 +4,12 @@ from __future__ import annotations import logging -from aioaseko import APIUnavailable, InvalidAuthCredentials, MobileAccount +from aioaseko import Aseko, AsekoNotLoggedIn from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.exceptions import ConfigEntryAuthFailed from .const import DOMAIN from .coordinator import AsekoDataUpdateCoordinator @@ -22,28 +21,17 @@ PLATFORMS: list[str] = [Platform.BINARY_SENSOR, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Aseko Pool Live from a config entry.""" - account = MobileAccount( - async_get_clientsession(hass), - username=entry.data[CONF_EMAIL], - password=entry.data[CONF_PASSWORD], - ) + aseko = Aseko(entry.data[CONF_EMAIL], entry.data[CONF_PASSWORD]) try: - units = await account.get_units() - except InvalidAuthCredentials as err: + await aseko.login() + except AsekoNotLoggedIn as err: raise ConfigEntryAuthFailed from err - except APIUnavailable as err: - raise ConfigEntryNotReady from err - - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = [] - - for unit in units: - coordinator = AsekoDataUpdateCoordinator(hass, unit) - await coordinator.async_config_entry_first_refresh() - hass.data[DOMAIN][entry.entry_id].append((unit, coordinator)) + coordinator = AsekoDataUpdateCoordinator(hass, aseko) + await coordinator.async_config_entry_first_refresh() + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - return True @@ -51,7 +39,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok diff --git a/homeassistant/components/aseko_pool_live/binary_sensor.py b/homeassistant/components/aseko_pool_live/binary_sensor.py index 79953565769..90be61b230d 100644 --- a/homeassistant/components/aseko_pool_live/binary_sensor.py +++ b/homeassistant/components/aseko_pool_live/binary_sensor.py @@ -8,7 +8,6 @@ from dataclasses import dataclass from aioaseko import Unit from homeassistant.components.binary_sensor import ( - BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) @@ -25,26 +24,14 @@ from .entity import AsekoEntity class AsekoBinarySensorEntityDescription(BinarySensorEntityDescription): """Describes an Aseko binary sensor entity.""" - value_fn: Callable[[Unit], bool] + value_fn: Callable[[Unit], bool | None] -UNIT_BINARY_SENSORS: tuple[AsekoBinarySensorEntityDescription, ...] = ( +BINARY_SENSORS: tuple[AsekoBinarySensorEntityDescription, ...] = ( AsekoBinarySensorEntityDescription( key="water_flow", - translation_key="water_flow", - value_fn=lambda unit: unit.water_flow, - ), - AsekoBinarySensorEntityDescription( - key="has_alarm", - translation_key="alarm", - value_fn=lambda unit: unit.has_alarm, - device_class=BinarySensorDeviceClass.SAFETY, - ), - AsekoBinarySensorEntityDescription( - key="has_error", - translation_key="error", - value_fn=lambda unit: unit.has_error, - device_class=BinarySensorDeviceClass.PROBLEM, + translation_key="water_flow_to_probes", + value_fn=lambda unit: unit.water_flow_to_probes, ), ) @@ -55,33 +42,22 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Aseko Pool Live binary sensors.""" - data: list[tuple[Unit, AsekoDataUpdateCoordinator]] = hass.data[DOMAIN][ - config_entry.entry_id - ] + coordinator: AsekoDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + units = coordinator.data.values() async_add_entities( - AsekoUnitBinarySensorEntity(unit, coordinator, description) - for unit, coordinator in data - for description in UNIT_BINARY_SENSORS + AsekoBinarySensorEntity(unit, coordinator, description) + for description in BINARY_SENSORS + for unit in units + if description.value_fn(unit) is not None ) -class AsekoUnitBinarySensorEntity(AsekoEntity, BinarySensorEntity): - """Representation of a unit water flow binary sensor entity.""" +class AsekoBinarySensorEntity(AsekoEntity, BinarySensorEntity): + """Representation of an Aseko binary sensor entity.""" entity_description: AsekoBinarySensorEntityDescription - def __init__( - self, - unit: Unit, - coordinator: AsekoDataUpdateCoordinator, - entity_description: AsekoBinarySensorEntityDescription, - ) -> None: - """Initialize the unit binary sensor.""" - super().__init__(unit, coordinator) - self.entity_description = entity_description - self._attr_unique_id = f"{self._unit.serial_number}_{entity_description.key}" - @property - def is_on(self) -> bool: + def is_on(self) -> bool | None: """Return the state of the sensor.""" - return self.entity_description.value_fn(self._unit) + return self.entity_description.value_fn(self.unit) diff --git a/homeassistant/components/aseko_pool_live/config_flow.py b/homeassistant/components/aseko_pool_live/config_flow.py index ce6de3683d5..c0edee694be 100644 --- a/homeassistant/components/aseko_pool_live/config_flow.py +++ b/homeassistant/components/aseko_pool_live/config_flow.py @@ -6,12 +6,11 @@ from collections.abc import Mapping import logging from typing import Any -from aioaseko import APIUnavailable, InvalidAuthCredentials, WebAccount +from aioaseko import Aseko, AsekoAPIError, AsekoInvalidCredentials import voluptuous as vol from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_UNIQUE_ID -from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN @@ -34,15 +33,12 @@ class AsekoConfigFlow(ConfigFlow, domain=DOMAIN): async def get_account_info(self, email: str, password: str) -> dict: """Get account info from the mobile API and the web API.""" - session = async_get_clientsession(self.hass) - - web_account = WebAccount(session, email, password) - web_account_info = await web_account.login() - + aseko = Aseko(email, password) + user = await aseko.login() return { CONF_EMAIL: email, CONF_PASSWORD: password, - CONF_UNIQUE_ID: web_account_info.user_id, + CONF_UNIQUE_ID: user.user_id, } async def async_step_user( @@ -58,9 +54,9 @@ class AsekoConfigFlow(ConfigFlow, domain=DOMAIN): info = await self.get_account_info( user_input[CONF_EMAIL], user_input[CONF_PASSWORD] ) - except APIUnavailable: + except AsekoAPIError: errors["base"] = "cannot_connect" - except InvalidAuthCredentials: + except AsekoInvalidCredentials: errors["base"] = "invalid_auth" except Exception: _LOGGER.exception("Unexpected exception") @@ -122,9 +118,9 @@ class AsekoConfigFlow(ConfigFlow, domain=DOMAIN): info = await self.get_account_info( user_input[CONF_EMAIL], user_input[CONF_PASSWORD] ) - except APIUnavailable: + except AsekoAPIError: errors["base"] = "cannot_connect" - except InvalidAuthCredentials: + except AsekoInvalidCredentials: errors["base"] = "invalid_auth" except Exception: _LOGGER.exception("Unexpected exception") diff --git a/homeassistant/components/aseko_pool_live/coordinator.py b/homeassistant/components/aseko_pool_live/coordinator.py index a7f2d5ad5ac..eb7ccf9ec42 100644 --- a/homeassistant/components/aseko_pool_live/coordinator.py +++ b/homeassistant/components/aseko_pool_live/coordinator.py @@ -5,34 +5,31 @@ from __future__ import annotations from datetime import timedelta import logging -from aioaseko import Unit, Variable +from aioaseko import Aseko, Unit from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from .const import DOMAIN + _LOGGER = logging.getLogger(__name__) -class AsekoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Variable]]): +class AsekoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Unit]]): """Class to manage fetching Aseko unit data from single endpoint.""" - def __init__(self, hass: HomeAssistant, unit: Unit) -> None: + def __init__(self, hass: HomeAssistant, aseko: Aseko) -> None: """Initialize global Aseko unit data updater.""" - self._unit = unit - - if self._unit.name: - name = self._unit.name - else: - name = f"{self._unit.type}-{self._unit.serial_number}" + self._aseko = aseko super().__init__( hass, _LOGGER, - name=name, + name=DOMAIN, update_interval=timedelta(minutes=2), ) - async def _async_update_data(self) -> dict[str, Variable]: + async def _async_update_data(self) -> dict[str, Unit]: """Fetch unit data.""" - await self._unit.get_state() - return {variable.type: variable for variable in self._unit.variables} + units = await self._aseko.get_units() + return {unit.serial_number: unit for unit in units} diff --git a/homeassistant/components/aseko_pool_live/entity.py b/homeassistant/components/aseko_pool_live/entity.py index 6f0979da2e7..038e0a175d3 100644 --- a/homeassistant/components/aseko_pool_live/entity.py +++ b/homeassistant/components/aseko_pool_live/entity.py @@ -3,6 +3,7 @@ from aioaseko import Unit from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN @@ -14,20 +15,44 @@ class AsekoEntity(CoordinatorEntity[AsekoDataUpdateCoordinator]): _attr_has_entity_name = True - def __init__(self, unit: Unit, coordinator: AsekoDataUpdateCoordinator) -> None: + def __init__( + self, + unit: Unit, + coordinator: AsekoDataUpdateCoordinator, + description: EntityDescription, + ) -> None: """Initialize the aseko entity.""" super().__init__(coordinator) + self.entity_description = description self._unit = unit - - if self._unit.type == "Remote": - self._device_model = "ASIN Pool" - else: - self._device_model = f"ASIN AQUA {self._unit.type}" - self._device_name = self._unit.name if self._unit.name else self._device_model - + self._attr_unique_id = f"{self.unit.serial_number}{self.entity_description.key}" self._attr_device_info = DeviceInfo( - name=self._device_name, - identifiers={(DOMAIN, str(self._unit.serial_number))}, - manufacturer="Aseko", - model=self._device_model, + identifiers={(DOMAIN, self.unit.serial_number)}, + serial_number=self.unit.serial_number, + name=unit.name or unit.serial_number, + manufacturer=( + self.unit.brand_name.primary + if self.unit.brand_name is not None + else None + ), + model=( + self.unit.brand_name.secondary + if self.unit.brand_name is not None + else None + ), + configuration_url=f"https://aseko.cloud/unit/{self.unit.serial_number}", + ) + + @property + def unit(self) -> Unit: + """Return the aseko unit.""" + return self.coordinator.data[self._unit.serial_number] + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return ( + super().available + and self.unit.serial_number in self.coordinator.data + and self.unit.online ) diff --git a/homeassistant/components/aseko_pool_live/icons.json b/homeassistant/components/aseko_pool_live/icons.json index 2f8a77fc417..23a8459d857 100644 --- a/homeassistant/components/aseko_pool_live/icons.json +++ b/homeassistant/components/aseko_pool_live/icons.json @@ -1,16 +1,25 @@ { "entity": { "binary_sensor": { - "water_flow": { + "water_flow_to_probes": { "default": "mdi:waves-arrow-right" } }, "sensor": { + "air_temperature": { + "default": "mdi:thermometer-lines" + }, "free_chlorine": { - "default": "mdi:flask" + "default": "mdi:pool" + }, + "redox": { + "default": "mdi:pool" + }, + "salinity": { + "default": "mdi:pool" }, "water_temperature": { - "default": "mdi:coolant-temperature" + "default": "mdi:pool-thermometer" } } } diff --git a/homeassistant/components/aseko_pool_live/manifest.json b/homeassistant/components/aseko_pool_live/manifest.json index a340408ad71..628a9732188 100644 --- a/homeassistant/components/aseko_pool_live/manifest.json +++ b/homeassistant/components/aseko_pool_live/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/aseko_pool_live", "iot_class": "cloud_polling", "loggers": ["aioaseko"], - "requirements": ["aioaseko==0.2.0"] + "requirements": ["aioaseko==1.0.0"] } diff --git a/homeassistant/components/aseko_pool_live/sensor.py b/homeassistant/components/aseko_pool_live/sensor.py index a4ddea9ad89..d140d2a474f 100644 --- a/homeassistant/components/aseko_pool_live/sensor.py +++ b/homeassistant/components/aseko_pool_live/sensor.py @@ -2,77 +2,104 @@ from __future__ import annotations -from aioaseko import Unit, Variable +from collections.abc import Callable +from dataclasses import dataclass + +from aioaseko import Unit from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, + SensorEntityDescription, SensorStateClass, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import UnitOfElectricPotential, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType from .const import DOMAIN from .coordinator import AsekoDataUpdateCoordinator from .entity import AsekoEntity +@dataclass(frozen=True, kw_only=True) +class AsekoSensorEntityDescription(SensorEntityDescription): + """Describes an Aseko sensor entity.""" + + value_fn: Callable[[Unit], StateType] + + +SENSORS: list[AsekoSensorEntityDescription] = [ + AsekoSensorEntityDescription( + key="airTemp", + translation_key="air_temperature", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda unit: unit.air_temperature, + ), + AsekoSensorEntityDescription( + key="free_chlorine", + translation_key="free_chlorine", + native_unit_of_measurement="mg/l", + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda unit: unit.cl_free, + ), + AsekoSensorEntityDescription( + key="ph", + device_class=SensorDeviceClass.PH, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda unit: unit.ph, + ), + AsekoSensorEntityDescription( + key="rx", + translation_key="redox", + native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda unit: unit.redox, + ), + AsekoSensorEntityDescription( + key="salinity", + translation_key="salinity", + native_unit_of_measurement="kg/m³", + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda unit: unit.salinity, + ), + AsekoSensorEntityDescription( + key="waterTemp", + translation_key="water_temperature", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda unit: unit.water_temperature, + ), +] + + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Aseko Pool Live sensors.""" - data: list[tuple[Unit, AsekoDataUpdateCoordinator]] = hass.data[DOMAIN][ - config_entry.entry_id - ] - + coordinator: AsekoDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + units = coordinator.data.values() async_add_entities( - VariableSensorEntity(unit, variable, coordinator) - for unit, coordinator in data - for variable in unit.variables + AsekoSensorEntity(unit, coordinator, description) + for description in SENSORS + for unit in units + if description.value_fn(unit) is not None ) -class VariableSensorEntity(AsekoEntity, SensorEntity): - """Representation of a unit variable sensor entity.""" +class AsekoSensorEntity(AsekoEntity, SensorEntity): + """Representation of an Aseko unit sensor entity.""" - _attr_state_class = SensorStateClass.MEASUREMENT - - def __init__( - self, unit: Unit, variable: Variable, coordinator: AsekoDataUpdateCoordinator - ) -> None: - """Initialize the variable sensor.""" - super().__init__(unit, coordinator) - self._variable = variable - - translation_key = { - "Air temp.": "air_temperature", - "Cl free": "free_chlorine", - "Water temp.": "water_temperature", - }.get(self._variable.name) - if translation_key is not None: - self._attr_translation_key = translation_key - else: - self._attr_name = self._variable.name - - self._attr_unique_id = f"{self._unit.serial_number}{self._variable.type}" - self._attr_native_unit_of_measurement = self._variable.unit - - self._attr_icon = { - "rx": "mdi:test-tube", - "waterLevel": "mdi:waves", - }.get(self._variable.type) - - self._attr_device_class = { - "airTemp": SensorDeviceClass.TEMPERATURE, - "waterTemp": SensorDeviceClass.TEMPERATURE, - "ph": SensorDeviceClass.PH, - }.get(self._variable.type) + entity_description: AsekoSensorEntityDescription @property - def native_value(self) -> int | None: + def native_value(self) -> StateType: """Return the state of the sensor.""" - variable = self.coordinator.data[self._variable.type] - return variable.current_value + return self.entity_description.value_fn(self.unit) diff --git a/homeassistant/components/aseko_pool_live/strings.json b/homeassistant/components/aseko_pool_live/strings.json index 7f77b9ec69b..9ac341a7989 100644 --- a/homeassistant/components/aseko_pool_live/strings.json +++ b/homeassistant/components/aseko_pool_live/strings.json @@ -26,11 +26,8 @@ }, "entity": { "binary_sensor": { - "water_flow": { - "name": "Water flow" - }, - "alarm": { - "name": "Alarm" + "water_flow_to_probes": { + "name": "Water flow to probes" } }, "sensor": { @@ -40,6 +37,12 @@ "free_chlorine": { "name": "Free chlorine" }, + "redox": { + "name": "Redox potential" + }, + "salinity": { + "name": "Salinity" + }, "water_temperature": { "name": "Water temperature" } diff --git a/requirements_all.txt b/requirements_all.txt index 71a87379f21..0b82a3b71fc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -192,7 +192,7 @@ aioapcaccess==0.4.2 aioaquacell==0.2.0 # homeassistant.components.aseko_pool_live -aioaseko==0.2.0 +aioaseko==1.0.0 # homeassistant.components.asuswrt aioasuswrt==1.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f1f4adafbb6..c21de496fa1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -180,7 +180,7 @@ aioapcaccess==0.4.2 aioaquacell==0.2.0 # homeassistant.components.aseko_pool_live -aioaseko==0.2.0 +aioaseko==1.0.0 # homeassistant.components.asuswrt aioasuswrt==1.4.0 diff --git a/tests/components/aseko_pool_live/conftest.py b/tests/components/aseko_pool_live/conftest.py new file mode 100644 index 00000000000..f3bbddb2cab --- /dev/null +++ b/tests/components/aseko_pool_live/conftest.py @@ -0,0 +1,20 @@ +"""Aseko Pool Live conftest.""" + +from datetime import datetime + +from aioaseko import User +import pytest + + +@pytest.fixture +def user() -> User: + """Aseko User fixture.""" + return User( + user_id="a_user_id", + created_at=datetime.now(), + updated_at=datetime.now(), + name="John", + surname="Doe", + language="any_language", + is_active=True, + ) diff --git a/tests/components/aseko_pool_live/test_config_flow.py b/tests/components/aseko_pool_live/test_config_flow.py index e4dedf36da4..de1bf0912f8 100644 --- a/tests/components/aseko_pool_live/test_config_flow.py +++ b/tests/components/aseko_pool_live/test_config_flow.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from aioaseko import AccountInfo, APIUnavailable, InvalidAuthCredentials +from aioaseko import AsekoAPIError, AsekoInvalidCredentials, User import pytest from homeassistant import config_entries @@ -23,7 +23,7 @@ async def test_async_step_user_form(hass: HomeAssistant) -> None: assert result["errors"] == {} -async def test_async_step_user_success(hass: HomeAssistant) -> None: +async def test_async_step_user_success(hass: HomeAssistant, user: User) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -31,8 +31,8 @@ async def test_async_step_user_success(hass: HomeAssistant) -> None: with ( patch( - "homeassistant.components.aseko_pool_live.config_flow.WebAccount.login", - return_value=AccountInfo("aseko@example.com", "a_user_id", "any_language"), + "homeassistant.components.aseko_pool_live.config_flow.Aseko.login", + return_value=user, ), patch( "homeassistant.components.aseko_pool_live.async_setup_entry", @@ -60,13 +60,13 @@ async def test_async_step_user_success(hass: HomeAssistant) -> None: @pytest.mark.parametrize( ("error_web", "reason"), [ - (APIUnavailable, "cannot_connect"), - (InvalidAuthCredentials, "invalid_auth"), + (AsekoAPIError, "cannot_connect"), + (AsekoInvalidCredentials, "invalid_auth"), (Exception, "unknown"), ], ) async def test_async_step_user_exception( - hass: HomeAssistant, error_web: Exception, reason: str + hass: HomeAssistant, user: User, error_web: Exception, reason: str ) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( @@ -74,8 +74,8 @@ async def test_async_step_user_exception( ) with patch( - "homeassistant.components.aseko_pool_live.config_flow.WebAccount.login", - return_value=AccountInfo("aseko@example.com", "a_user_id", "any_language"), + "homeassistant.components.aseko_pool_live.config_flow.Aseko.login", + return_value=user, side_effect=error_web, ): result2 = await hass.config_entries.flow.async_configure( @@ -93,13 +93,13 @@ async def test_async_step_user_exception( @pytest.mark.parametrize( ("error_web", "reason"), [ - (APIUnavailable, "cannot_connect"), - (InvalidAuthCredentials, "invalid_auth"), + (AsekoAPIError, "cannot_connect"), + (AsekoInvalidCredentials, "invalid_auth"), (Exception, "unknown"), ], ) async def test_get_account_info_exceptions( - hass: HomeAssistant, error_web: Exception, reason: str + hass: HomeAssistant, user: User, error_web: Exception, reason: str ) -> None: """Test we handle config flow exceptions.""" result = await hass.config_entries.flow.async_init( @@ -107,8 +107,8 @@ async def test_get_account_info_exceptions( ) with patch( - "homeassistant.components.aseko_pool_live.config_flow.WebAccount.login", - return_value=AccountInfo("aseko@example.com", "a_user_id", "any_language"), + "homeassistant.components.aseko_pool_live.config_flow.Aseko.login", + return_value=user, side_effect=error_web, ): result2 = await hass.config_entries.flow.async_configure( @@ -123,7 +123,7 @@ async def test_get_account_info_exceptions( assert result2["errors"] == {"base": reason} -async def test_async_step_reauth_success(hass: HomeAssistant) -> None: +async def test_async_step_reauth_success(hass: HomeAssistant, user: User) -> None: """Test successful reauthentication.""" mock_entry = MockConfigEntry( @@ -139,10 +139,16 @@ async def test_async_step_reauth_success(hass: HomeAssistant) -> None: assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} - with patch( - "homeassistant.components.aseko_pool_live.config_flow.WebAccount.login", - return_value=AccountInfo("aseko@example.com", "a_user_id", "any_language"), - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.aseko_pool_live.config_flow.Aseko.login", + return_value=user, + ), + patch( + "homeassistant.components.aseko_pool_live.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_EMAIL: "aseko@example.com", CONF_PASSWORD: "passw0rd"}, @@ -156,13 +162,13 @@ async def test_async_step_reauth_success(hass: HomeAssistant) -> None: @pytest.mark.parametrize( ("error_web", "reason"), [ - (APIUnavailable, "cannot_connect"), - (InvalidAuthCredentials, "invalid_auth"), + (AsekoAPIError, "cannot_connect"), + (AsekoInvalidCredentials, "invalid_auth"), (Exception, "unknown"), ], ) async def test_async_step_reauth_exception( - hass: HomeAssistant, error_web: Exception, reason: str + hass: HomeAssistant, user: User, error_web: Exception, reason: str ) -> None: """Test we get the form.""" @@ -176,8 +182,8 @@ async def test_async_step_reauth_exception( result = await mock_entry.start_reauth_flow(hass) with patch( - "homeassistant.components.aseko_pool_live.config_flow.WebAccount.login", - return_value=AccountInfo("aseko@example.com", "a_user_id", "any_language"), + "homeassistant.components.aseko_pool_live.config_flow.Aseko.login", + return_value=user, side_effect=error_web, ): result2 = await hass.config_entries.flow.async_configure( From b336cae118f40be56e19f5e040f351e665580490 Mon Sep 17 00:00:00 2001 From: Arun Philip Date: Thu, 19 Sep 2024 04:34:27 -0400 Subject: [PATCH 254/271] Fix qbittorrent error when torrent count is 0 (#126146) Fix handling of `NoneType` for torrents in `count_torrents_in_states` function Added a check to handle cases where the 'torrents' data is None, avoiding a `TypeError` when attempting to get the length of a `NoneType` object. The function now returns 0 if 'torrents' is None, ensuring robust behavior when no torrent data is available. --- homeassistant/components/qbittorrent/sensor.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/qbittorrent/sensor.py b/homeassistant/components/qbittorrent/sensor.py index cd65fb766e4..68de7e1d5e5 100644 --- a/homeassistant/components/qbittorrent/sensor.py +++ b/homeassistant/components/qbittorrent/sensor.py @@ -177,8 +177,12 @@ def count_torrents_in_states( # When torrents are not in the returned data, there are none, return 0. try: torrents = cast(Mapping[str, Mapping], coordinator.data.get("torrents")) + if torrents is None: + return 0 + if not states: return len(torrents) + return len( [torrent for torrent in torrents.values() if torrent.get("state") in states] ) From b38c193fe481775222813ff48d43174f1184119a Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Thu, 19 Sep 2024 10:45:26 +0200 Subject: [PATCH 255/271] Prevent blocking event loop in ps4 (#126151) * Prevent blocking event loop in ps4 * Process code review comment --- homeassistant/components/ps4/media_player.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ps4/media_player.py b/homeassistant/components/ps4/media_player.py index 77477ba7901..be0eed7ea25 100644 --- a/homeassistant/components/ps4/media_player.py +++ b/homeassistant/components/ps4/media_player.py @@ -96,11 +96,10 @@ class PS4Device(MediaPlayerEntity): self._retry = 0 self._disconnected = False - @callback def status_callback(self) -> None: """Handle status callback. Parse status.""" self._parse_status() - self.async_write_ha_state() + self.schedule_update_ha_state() @callback def subscribe_to_protocol(self) -> None: @@ -157,7 +156,7 @@ class PS4Device(MediaPlayerEntity): self._ps4.ddp_protocol = self.hass.data[PS4_DATA].protocol self.subscribe_to_protocol() - self._parse_status() + await self.hass.async_add_executor_job(self._parse_status) def _parse_status(self) -> None: """Parse status.""" From 6e36febd3732af562cbdadcba48494f180832854 Mon Sep 17 00:00:00 2001 From: Andrew Jackson Date: Wed, 18 Sep 2024 11:29:50 +0100 Subject: [PATCH 256/271] Broaden scope of ConfigEntryNotReady in Mealie (#126208) Broaden scope of ConfigEntryNotReady --- homeassistant/components/mealie/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mealie/__init__.py b/homeassistant/components/mealie/__init__.py index bf0fbcac406..443c8fdd991 100644 --- a/homeassistant/components/mealie/__init__.py +++ b/homeassistant/components/mealie/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations -from aiomealie import MealieAuthenticationError, MealieClient, MealieConnectionError +from aiomealie import MealieAuthenticationError, MealieClient, MealieError from homeassistant.const import CONF_API_TOKEN, CONF_HOST, CONF_VERIFY_SSL, Platform from homeassistant.core import HomeAssistant @@ -53,7 +53,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: MealieConfigEntry) -> bo version = create_version(about.version) except MealieAuthenticationError as error: raise ConfigEntryAuthFailed from error - except MealieConnectionError as error: + except MealieError as error: raise ConfigEntryNotReady(error) from error if not version.valid: From c81f280bc1f0201b3a365baf2a9b4ecc71a7a3be Mon Sep 17 00:00:00 2001 From: Sebastian Nohn Date: Thu, 19 Sep 2024 09:11:57 +0200 Subject: [PATCH 257/271] Fix tibber fails if power production is enabled but no power is produced (#126209) * fix #125312 - tibber integration fails if power production is enabled but no power is produced * fix requirements_all.txt --- homeassistant/components/tibber/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json index 527364b6866..eb59d2456fb 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["tibber"], "quality_scale": "silver", - "requirements": ["pyTibber==0.30.1"] + "requirements": ["pyTibber==0.30.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 0b82a3b71fc..66e2929ac58 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1707,7 +1707,7 @@ pyRFXtrx==0.31.1 pySDCP==1 # homeassistant.components.tibber -pyTibber==0.30.1 +pyTibber==0.30.2 # homeassistant.components.dlink pyW215==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c21de496fa1..a37ab9fdcac 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1381,7 +1381,7 @@ pyElectra==1.2.4 pyRFXtrx==0.31.1 # homeassistant.components.tibber -pyTibber==0.30.1 +pyTibber==0.30.2 # homeassistant.components.dlink pyW215==0.7.0 From 7658ed8eaa317af00c06cf427e775d87358d713c Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Wed, 18 Sep 2024 20:37:01 +0200 Subject: [PATCH 258/271] Bump pydaikin to 2.13.7 (#126219) --- homeassistant/components/daikin/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/daikin/manifest.json b/homeassistant/components/daikin/manifest.json index 88c29a20435..f6e9cb78efb 100644 --- a/homeassistant/components/daikin/manifest.json +++ b/homeassistant/components/daikin/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/daikin", "iot_class": "local_polling", "loggers": ["pydaikin"], - "requirements": ["pydaikin==2.13.6"], + "requirements": ["pydaikin==2.13.7"], "zeroconf": ["_dkapi._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 66e2929ac58..3b265cb9d86 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1798,7 +1798,7 @@ pycsspeechtts==1.0.8 # pycups==1.9.73 # homeassistant.components.daikin -pydaikin==2.13.6 +pydaikin==2.13.7 # homeassistant.components.danfoss_air pydanfossair==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a37ab9fdcac..e76484e016a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1445,7 +1445,7 @@ pycoolmasternet-async==0.2.2 pycsspeechtts==1.0.8 # homeassistant.components.daikin -pydaikin==2.13.6 +pydaikin==2.13.7 # homeassistant.components.deconz pydeconz==116 From 2322d071e45d3bab3bfa72d23a0122a73385bb76 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Mon, 23 Sep 2024 09:27:11 +0200 Subject: [PATCH 259/271] Fix Matter climate platform attributes when dedicated OnOff attribute is off (#126286) --- homeassistant/components/matter/climate.py | 90 ++++++++++++---------- 1 file changed, 49 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/matter/climate.py b/homeassistant/components/matter/climate.py index ff00e4ee495..4eec539c0db 100644 --- a/homeassistant/components/matter/climate.py +++ b/homeassistant/components/matter/climate.py @@ -190,48 +190,56 @@ class MatterClimate(MatterEntity, ClimateEntity): # if the mains power is off - treat it as if the HVAC mode is off self._attr_hvac_mode = HVACMode.OFF self._attr_hvac_action = None - return - - # update hvac_mode from SystemMode - system_mode_value = int( - self.get_matter_attribute_value(clusters.Thermostat.Attributes.SystemMode) - ) - match system_mode_value: - case SystemModeEnum.kAuto: - self._attr_hvac_mode = HVACMode.HEAT_COOL - case SystemModeEnum.kDry: - self._attr_hvac_mode = HVACMode.DRY - case SystemModeEnum.kFanOnly: - self._attr_hvac_mode = HVACMode.FAN_ONLY - case SystemModeEnum.kCool | SystemModeEnum.kPrecooling: - self._attr_hvac_mode = HVACMode.COOL - case SystemModeEnum.kHeat | SystemModeEnum.kEmergencyHeat: - self._attr_hvac_mode = HVACMode.HEAT - case SystemModeEnum.kFanOnly: - self._attr_hvac_mode = HVACMode.FAN_ONLY - case SystemModeEnum.kDry: - self._attr_hvac_mode = HVACMode.DRY - case _: - self._attr_hvac_mode = HVACMode.OFF - # running state is an optional attribute - # which we map to hvac_action if it exists (its value is not None) - self._attr_hvac_action = None - if running_state_value := self.get_matter_attribute_value( - clusters.Thermostat.Attributes.ThermostatRunningState - ): - match running_state_value: - case ThermostatRunningState.Heat | ThermostatRunningState.HeatStage2: - self._attr_hvac_action = HVACAction.HEATING - case ThermostatRunningState.Cool | ThermostatRunningState.CoolStage2: - self._attr_hvac_action = HVACAction.COOLING - case ( - ThermostatRunningState.Fan - | ThermostatRunningState.FanStage2 - | ThermostatRunningState.FanStage3 - ): - self._attr_hvac_action = HVACAction.FAN + else: + # update hvac_mode from SystemMode + system_mode_value = int( + self.get_matter_attribute_value( + clusters.Thermostat.Attributes.SystemMode + ) + ) + match system_mode_value: + case SystemModeEnum.kAuto: + self._attr_hvac_mode = HVACMode.HEAT_COOL + case SystemModeEnum.kDry: + self._attr_hvac_mode = HVACMode.DRY + case SystemModeEnum.kFanOnly: + self._attr_hvac_mode = HVACMode.FAN_ONLY + case SystemModeEnum.kCool | SystemModeEnum.kPrecooling: + self._attr_hvac_mode = HVACMode.COOL + case SystemModeEnum.kHeat | SystemModeEnum.kEmergencyHeat: + self._attr_hvac_mode = HVACMode.HEAT + case SystemModeEnum.kFanOnly: + self._attr_hvac_mode = HVACMode.FAN_ONLY + case SystemModeEnum.kDry: + self._attr_hvac_mode = HVACMode.DRY case _: - self._attr_hvac_action = HVACAction.OFF + self._attr_hvac_mode = HVACMode.OFF + # running state is an optional attribute + # which we map to hvac_action if it exists (its value is not None) + self._attr_hvac_action = None + if running_state_value := self.get_matter_attribute_value( + clusters.Thermostat.Attributes.ThermostatRunningState + ): + match running_state_value: + case ( + ThermostatRunningState.Heat + | ThermostatRunningState.HeatStage2 + ): + self._attr_hvac_action = HVACAction.HEATING + case ( + ThermostatRunningState.Cool + | ThermostatRunningState.CoolStage2 + ): + self._attr_hvac_action = HVACAction.COOLING + case ( + ThermostatRunningState.Fan + | ThermostatRunningState.FanStage2 + | ThermostatRunningState.FanStage3 + ): + self._attr_hvac_action = HVACAction.FAN + case _: + self._attr_hvac_action = HVACAction.OFF + # update target temperature high/low supports_range = ( self._attr_supported_features From edfb9f3f6bf6e6d7e2727efe7f6bcdf11bdce2f8 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Fri, 20 Sep 2024 11:16:58 +0200 Subject: [PATCH 260/271] Fix loading KNX UI entities with entity category set (#126290) * Fix loading KNX UI entities with entity category set * add test * docstring fixes * telegram order * Optionally ignore telegram sending order in tests because we can't know which platform initialises first --- homeassistant/components/knx/knx_entity.py | 21 +++-- homeassistant/components/knx/light.py | 14 ++- homeassistant/components/knx/switch.py | 13 ++- tests/components/knx/README.md | 16 ++-- tests/components/knx/conftest.py | 85 ++++++++++++------- .../components/knx/fixtures/config_store.json | 21 ++++- tests/components/knx/test_device.py | 3 +- tests/components/knx/test_light.py | 27 +++++- 8 files changed, 134 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/knx/knx_entity.py b/homeassistant/components/knx/knx_entity.py index c81a6ee06db..6574e5d5860 100644 --- a/homeassistant/components/knx/knx_entity.py +++ b/homeassistant/components/knx/knx_entity.py @@ -2,20 +2,23 @@ from __future__ import annotations -from abc import ABC, abstractmethod from typing import TYPE_CHECKING, Any from xknx.devices import Device as XknxDevice +from homeassistant.const import CONF_ENTITY_CATEGORY, EntityCategory +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import EntityPlatform from homeassistant.helpers.entity_registry import RegistryEntry +from .const import DOMAIN +from .storage.config_store import PlatformControllerBase +from .storage.const import CONF_DEVICE_INFO + if TYPE_CHECKING: from . import KNXModule -from .storage.config_store import PlatformControllerBase - class KnxUiEntityPlatformController(PlatformControllerBase): """Class to manage dynamic adding and reloading of UI entities.""" @@ -93,13 +96,19 @@ class KnxYamlEntity(_KnxEntityBase): self._device = device -class KnxUiEntity(_KnxEntityBase, ABC): +class KnxUiEntity(_KnxEntityBase): """Representation of a KNX UI entity.""" _attr_unique_id: str + _attr_has_entity_name = True - @abstractmethod def __init__( - self, knx_module: KNXModule, unique_id: str, config: dict[str, Any] + self, knx_module: KNXModule, unique_id: str, entity_config: dict[str, Any] ) -> None: """Initialize the UI entity.""" + self._knx_module = knx_module + self._attr_unique_id = unique_id + if entity_category := entity_config.get(CONF_ENTITY_CATEGORY): + self._attr_entity_category = EntityCategory(entity_category) + if device_info := entity_config.get(CONF_DEVICE_INFO): + self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, device_info)}) diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index 0caa3f0a799..6abfd3fa9c8 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -20,7 +20,6 @@ from homeassistant.components.light import ( ) from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME, Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import ( AddEntitiesCallback, async_get_current_platform, @@ -35,7 +34,6 @@ from .schema import LightSchema from .storage.const import ( CONF_COLOR_TEMP_MAX, CONF_COLOR_TEMP_MIN, - CONF_DEVICE_INFO, CONF_DPT, CONF_ENTITY, CONF_GA_BLUE_BRIGHTNESS, @@ -554,21 +552,19 @@ class KnxYamlLight(_KnxLight, KnxYamlEntity): class KnxUiLight(_KnxLight, KnxUiEntity): """Representation of a KNX light.""" - _attr_has_entity_name = True _device: XknxLight def __init__( self, knx_module: KNXModule, unique_id: str, config: ConfigType ) -> None: """Initialize of KNX light.""" - self._knx_module = knx_module + super().__init__( + knx_module=knx_module, + unique_id=unique_id, + entity_config=config[CONF_ENTITY], + ) self._device = _create_ui_light( knx_module.xknx, config[DOMAIN], config[CONF_ENTITY][CONF_NAME] ) self._attr_max_color_temp_kelvin: int = config[DOMAIN][CONF_COLOR_TEMP_MAX] self._attr_min_color_temp_kelvin: int = config[DOMAIN][CONF_COLOR_TEMP_MIN] - - self._attr_entity_category = config[CONF_ENTITY][CONF_ENTITY_CATEGORY] - self._attr_unique_id = unique_id - if device_info := config[CONF_ENTITY].get(CONF_DEVICE_INFO): - self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, device_info)}) diff --git a/homeassistant/components/knx/switch.py b/homeassistant/components/knx/switch.py index ebe930957d6..64e21a4d2b3 100644 --- a/homeassistant/components/knx/switch.py +++ b/homeassistant/components/knx/switch.py @@ -18,7 +18,6 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import ( AddEntitiesCallback, async_get_current_platform, @@ -38,7 +37,6 @@ from .const import ( from .knx_entity import KnxUiEntity, KnxUiEntityPlatformController, KnxYamlEntity from .schema import SwitchSchema from .storage.const import ( - CONF_DEVICE_INFO, CONF_ENTITY, CONF_GA_PASSIVE, CONF_GA_STATE, @@ -133,14 +131,17 @@ class KnxYamlSwitch(_KnxSwitch, KnxYamlEntity): class KnxUiSwitch(_KnxSwitch, KnxUiEntity): """Representation of a KNX switch configured from UI.""" - _attr_has_entity_name = True _device: XknxSwitch def __init__( self, knx_module: KNXModule, unique_id: str, config: dict[str, Any] ) -> None: """Initialize KNX switch.""" - self._knx_module = knx_module + super().__init__( + knx_module=knx_module, + unique_id=unique_id, + entity_config=config[CONF_ENTITY], + ) self._device = XknxSwitch( knx_module.xknx, name=config[CONF_ENTITY][CONF_NAME], @@ -153,7 +154,3 @@ class KnxUiSwitch(_KnxSwitch, KnxUiEntity): sync_state=config[DOMAIN][CONF_SYNC_STATE], invert=config[DOMAIN][CONF_INVERT], ) - self._attr_entity_category = config[CONF_ENTITY][CONF_ENTITY_CATEGORY] - self._attr_unique_id = unique_id - if device_info := config[CONF_ENTITY].get(CONF_DEVICE_INFO): - self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, device_info)}) diff --git a/tests/components/knx/README.md b/tests/components/knx/README.md index 8778feb2251..ef8398b3d17 100644 --- a/tests/components/knx/README.md +++ b/tests/components/knx/README.md @@ -18,22 +18,22 @@ async def test_something(hass, knx): ## Asserting outgoing telegrams -All outgoing telegrams are pushed to an assertion queue. Assert them in order they were sent. +All outgoing telegrams are appended to an assertion list. Assert them in order they were sent or pass `ignore_order=True` to the assertion method. - `knx.assert_no_telegram` - Asserts that no telegram was sent (assertion queue is empty). + Asserts that no telegram was sent (assertion list is empty). - `knx.assert_telegram_count(count: int)` Asserts that `count` telegrams were sent. -- `knx.assert_read(group_address: str, response: int | tuple[int, ...] | None = None)` +- `knx.assert_read(group_address: str, response: int | tuple[int, ...] | None = None, ignore_order: bool = False)` Asserts that a GroupValueRead telegram was sent to `group_address`. - The telegram will be removed from the assertion queue. + The telegram will be removed from the assertion list. Optionally inject incoming GroupValueResponse telegram after reception to clear the value reader waiting task. This can also be done manually with `knx.receive_response`. -- `knx.assert_response(group_address: str, payload: int | tuple[int, ...])` +- `knx.assert_response(group_address: str, payload: int | tuple[int, ...], ignore_order: bool = False)` Asserts that a GroupValueResponse telegram with `payload` was sent to `group_address`. - The telegram will be removed from the assertion queue. -- `knx.assert_write(group_address: str, payload: int | tuple[int, ...])` + The telegram will be removed from the assertion list. +- `knx.assert_write(group_address: str, payload: int | tuple[int, ...], ignore_order: bool = False)` Asserts that a GroupValueWrite telegram with `payload` was sent to `group_address`. - The telegram will be removed from the assertion queue. + The telegram will be removed from the assertion list. Change some states or call some services and assert outgoing telegrams. diff --git a/tests/components/knx/conftest.py b/tests/components/knx/conftest.py index 19f2bc4d845..c0ec1dd9b9a 100644 --- a/tests/components/knx/conftest.py +++ b/tests/components/knx/conftest.py @@ -57,9 +57,9 @@ class KNXTestKit: self.hass: HomeAssistant = hass self.mock_config_entry: MockConfigEntry = mock_config_entry self.xknx: XKNX - # outgoing telegrams will be put in the Queue instead of sent to the interface + # outgoing telegrams will be put in the List instead of sent to the interface # telegrams to an InternalGroupAddress won't be queued here - self._outgoing_telegrams: asyncio.Queue = asyncio.Queue() + self._outgoing_telegrams: list[Telegram] = [] def assert_state(self, entity_id: str, state: str, **attributes) -> None: """Assert the state of an entity.""" @@ -76,7 +76,7 @@ class KNXTestKit: async def patch_xknx_start(): """Patch `xknx.start` for unittests.""" self.xknx.cemi_handler.send_telegram = AsyncMock( - side_effect=self._outgoing_telegrams.put + side_effect=self._outgoing_telegrams.append ) # after XKNX.__init__() to not overwrite it by the config entry again # before StateUpdater starts to avoid slow down of tests @@ -117,24 +117,22 @@ class KNXTestKit: ######################## def _list_remaining_telegrams(self) -> str: - """Return a string containing remaining outgoing telegrams in test Queue. One per line.""" - remaining_telegrams = [] - while not self._outgoing_telegrams.empty(): - remaining_telegrams.append(self._outgoing_telegrams.get_nowait()) - return "\n".join(map(str, remaining_telegrams)) + """Return a string containing remaining outgoing telegrams in test List.""" + return "\n".join(map(str, self._outgoing_telegrams)) async def assert_no_telegram(self) -> None: - """Assert if every telegram in test Queue was checked.""" + """Assert if every telegram in test List was checked.""" await self.hass.async_block_till_done() - assert self._outgoing_telegrams.empty(), ( - f"Found remaining unasserted Telegrams: {self._outgoing_telegrams.qsize()}\n" + remaining_telegram_count = len(self._outgoing_telegrams) + assert not remaining_telegram_count, ( + f"Found remaining unasserted Telegrams: {remaining_telegram_count}\n" f"{self._list_remaining_telegrams()}" ) async def assert_telegram_count(self, count: int) -> None: - """Assert outgoing telegram count in test Queue.""" + """Assert outgoing telegram count in test List.""" await self.hass.async_block_till_done() - actual_count = self._outgoing_telegrams.qsize() + actual_count = len(self._outgoing_telegrams) assert actual_count == count, ( f"Outgoing telegrams: {actual_count} - Expected: {count}\n" f"{self._list_remaining_telegrams()}" @@ -149,52 +147,79 @@ class KNXTestKit: group_address: str, payload: int | tuple[int, ...] | None, apci_type: type[APCI], + ignore_order: bool = False, ) -> None: - """Assert outgoing telegram. One by one in timely order.""" + """Assert outgoing telegram. Optionally in timely order.""" await self.xknx.telegrams.join() - try: - telegram = self._outgoing_telegrams.get_nowait() - except asyncio.QueueEmpty as err: + if not self._outgoing_telegrams: raise AssertionError( f"No Telegram found. Expected: {apci_type.__name__} -" f" {group_address} - {payload}" - ) from err + ) + _expected_ga = GroupAddress(group_address) + if ignore_order: + for telegram in self._outgoing_telegrams: + if ( + telegram.destination_address == _expected_ga + and isinstance(telegram.payload, apci_type) + and (payload is None or telegram.payload.value.value == payload) + ): + self._outgoing_telegrams.remove(telegram) + return + raise AssertionError( + f"Telegram not found. Expected: {apci_type.__name__} -" + f" {group_address} - {payload}" + f"\nUnasserted telegrams:\n{self._list_remaining_telegrams()}" + ) + + telegram = self._outgoing_telegrams.pop(0) assert isinstance( telegram.payload, apci_type ), f"APCI type mismatch in {telegram} - Expected: {apci_type.__name__}" - assert ( - str(telegram.destination_address) == group_address + telegram.destination_address == _expected_ga ), f"Group address mismatch in {telegram} - Expected: {group_address}" - if payload is not None: assert ( telegram.payload.value.value == payload # type: ignore[attr-defined] ), f"Payload mismatch in {telegram} - Expected: {payload}" async def assert_read( - self, group_address: str, response: int | tuple[int, ...] | None = None + self, + group_address: str, + response: int | tuple[int, ...] | None = None, + ignore_order: bool = False, ) -> None: - """Assert outgoing GroupValueRead telegram. One by one in timely order. + """Assert outgoing GroupValueRead telegram. Optionally in timely order. Optionally inject incoming GroupValueResponse telegram after reception. """ - await self.assert_telegram(group_address, None, GroupValueRead) + await self.assert_telegram(group_address, None, GroupValueRead, ignore_order) if response is not None: await self.receive_response(group_address, response) async def assert_response( - self, group_address: str, payload: int | tuple[int, ...] + self, + group_address: str, + payload: int | tuple[int, ...], + ignore_order: bool = False, ) -> None: - """Assert outgoing GroupValueResponse telegram. One by one in timely order.""" - await self.assert_telegram(group_address, payload, GroupValueResponse) + """Assert outgoing GroupValueResponse telegram. Optionally in timely order.""" + await self.assert_telegram( + group_address, payload, GroupValueResponse, ignore_order + ) async def assert_write( - self, group_address: str, payload: int | tuple[int, ...] + self, + group_address: str, + payload: int | tuple[int, ...], + ignore_order: bool = False, ) -> None: - """Assert outgoing GroupValueWrite telegram. One by one in timely order.""" - await self.assert_telegram(group_address, payload, GroupValueWrite) + """Assert outgoing GroupValueWrite telegram. Optionally in timely order.""" + await self.assert_telegram( + group_address, payload, GroupValueWrite, ignore_order + ) #################### # Incoming telegrams diff --git a/tests/components/knx/fixtures/config_store.json b/tests/components/knx/fixtures/config_store.json index 971b692ade1..5eabcfa87f9 100644 --- a/tests/components/knx/fixtures/config_store.json +++ b/tests/components/knx/fixtures/config_store.json @@ -23,7 +23,26 @@ } } }, - "light": {} + "light": { + "knx_es_01J85ZKTFHSZNG4X9DYBE592TF": { + "entity": { + "name": "test", + "device_info": null, + "entity_category": "config" + }, + "knx": { + "color_temp_min": 2700, + "color_temp_max": 6000, + "_light_color_mode_schema": "default", + "ga_switch": { + "write": "1/1/21", + "state": "1/0/21", + "passive": [] + }, + "sync_state": true + } + } + } } } } diff --git a/tests/components/knx/test_device.py b/tests/components/knx/test_device.py index 330fd854a50..04ff02f0611 100644 --- a/tests/components/knx/test_device.py +++ b/tests/components/knx/test_device.py @@ -58,7 +58,8 @@ async def test_remove_device( await knx.setup_integration({}) client = await hass_ws_client(hass) - await knx.assert_read("1/0/45", response=True) + await knx.assert_read("1/0/21", response=True, ignore_order=True) # test light + await knx.assert_read("1/0/45", response=True, ignore_order=True) # test switch assert hass_storage[KNX_CONFIG_STORAGE_KEY]["data"]["entities"].get("switch") test_device = device_registry.async_get_device( diff --git a/tests/components/knx/test_light.py b/tests/components/knx/test_light.py index e2e4a673a0d..88f76a163d5 100644 --- a/tests/components/knx/test_light.py +++ b/tests/components/knx/test_light.py @@ -19,8 +19,9 @@ from homeassistant.components.light import ( ATTR_RGBW_COLOR, ColorMode, ) -from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON +from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON, EntityCategory from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from . import KnxEntityGenerator from .conftest import KNXTestKit @@ -1159,7 +1160,7 @@ async def test_light_ui_create( knx: KNXTestKit, create_ui_entity: KnxEntityGenerator, ) -> None: - """Test creating a switch.""" + """Test creating a light.""" await knx.setup_integration({}) await create_ui_entity( platform=Platform.LIGHT, @@ -1192,7 +1193,7 @@ async def test_light_ui_color_temp( color_temp_mode: str, raw_ct: tuple[int, ...], ) -> None: - """Test creating a switch.""" + """Test creating a color-temp light.""" await knx.setup_integration({}) await create_ui_entity( platform=Platform.LIGHT, @@ -1218,3 +1219,23 @@ async def test_light_ui_color_temp( state = hass.states.get("light.test") assert state.state is STATE_ON assert state.attributes[ATTR_COLOR_TEMP_KELVIN] == pytest.approx(4200, abs=1) + + +async def test_light_ui_load( + hass: HomeAssistant, + knx: KNXTestKit, + load_config_store: None, + entity_registry: er.EntityRegistry, +) -> None: + """Test loading a light from storage.""" + await knx.setup_integration({}) + + await knx.assert_read("1/0/21", response=True, ignore_order=True) + # unrelated switch in config store + await knx.assert_read("1/0/45", response=True, ignore_order=True) + + state = hass.states.get("light.test") + assert state.state is STATE_ON + + entity = entity_registry.async_get("light.test") + assert entity.entity_category is EntityCategory.CONFIG From fba24b8eade93c79e27ac937d415886e0e6420af Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 21 Sep 2024 13:11:27 +0200 Subject: [PATCH 261/271] Bump airgradient to 0.9.0 (#126319) * Bump airgradient to 0.9.0 * Bump airgradient to 0.9.0 --- homeassistant/components/airgradient/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/airgradient/snapshots/test_sensor.ambr | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/airgradient/manifest.json b/homeassistant/components/airgradient/manifest.json index fed4fafdc74..c0472131357 100644 --- a/homeassistant/components/airgradient/manifest.json +++ b/homeassistant/components/airgradient/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/airgradient", "integration_type": "device", "iot_class": "local_polling", - "requirements": ["airgradient==0.8.0"], + "requirements": ["airgradient==0.9.0"], "zeroconf": ["_airgradient._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 3b265cb9d86..6db7772f11c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -410,7 +410,7 @@ aiowithings==3.0.3 aioymaps==1.2.5 # homeassistant.components.airgradient -airgradient==0.8.0 +airgradient==0.9.0 # homeassistant.components.airly airly==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e76484e016a..fb11103d917 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -392,7 +392,7 @@ aiowithings==3.0.3 aioymaps==1.2.5 # homeassistant.components.airgradient -airgradient==0.8.0 +airgradient==0.9.0 # homeassistant.components.airly airly==1.1.0 diff --git a/tests/components/airgradient/snapshots/test_sensor.ambr b/tests/components/airgradient/snapshots/test_sensor.ambr index ff83fdcc111..941369ff266 100644 --- a/tests/components/airgradient/snapshots/test_sensor.ambr +++ b/tests/components/airgradient/snapshots/test_sensor.ambr @@ -305,7 +305,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '48.0', + 'state': '47.0', }) # --- # name: test_all_entities[indoor][sensor.airgradient_led_bar_brightness-entry] @@ -912,7 +912,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '27.96', + 'state': '22.17', }) # --- # name: test_all_entities[indoor][sensor.airgradient_voc_index-entry] From 4eb1fca68e3cdaf5cba30001ce4254b5fe33a962 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 23 Sep 2024 10:11:27 +0200 Subject: [PATCH 262/271] Fix next change (scheduler) sensors in AVM FRITZ!SmartHome (#126363) --- homeassistant/components/fritzbox/sensor.py | 26 +++++++-- tests/components/fritzbox/__init__.py | 5 +- tests/components/fritzbox/test_climate.py | 2 +- tests/components/fritzbox/test_sensor.py | 62 ++++++++++++++++++++- 4 files changed, 85 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/fritzbox/sensor.py b/homeassistant/components/fritzbox/sensor.py index d28727c01f5..dbfdc2f9c95 100644 --- a/homeassistant/components/fritzbox/sensor.py +++ b/homeassistant/components/fritzbox/sensor.py @@ -83,20 +83,38 @@ def entity_category_temperature(device: FritzhomeDevice) -> EntityCategory | Non return None -def value_nextchange_preset(device: FritzhomeDevice) -> str: +def value_nextchange_preset(device: FritzhomeDevice) -> str | None: """Return native value for next scheduled preset sensor.""" + if not device.nextchange_endperiod: + return None if device.nextchange_temperature == device.eco_temperature: return PRESET_ECO return PRESET_COMFORT -def value_scheduled_preset(device: FritzhomeDevice) -> str: +def value_scheduled_preset(device: FritzhomeDevice) -> str | None: """Return native value for current scheduled preset sensor.""" + if not device.nextchange_endperiod: + return None if device.nextchange_temperature == device.eco_temperature: return PRESET_COMFORT return PRESET_ECO +def value_nextchange_temperature(device: FritzhomeDevice) -> float | None: + """Return native value for next scheduled temperature time sensor.""" + if device.nextchange_endperiod and isinstance(device.nextchange_temperature, float): + return device.nextchange_temperature + return None + + +def value_nextchange_time(device: FritzhomeDevice) -> datetime | None: + """Return native value for next scheduled changed time sensor.""" + if device.nextchange_endperiod: + return utc_from_timestamp(device.nextchange_endperiod) + return None + + SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = ( FritzSensorEntityDescription( key="temperature", @@ -181,7 +199,7 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = ( device_class=SensorDeviceClass.TEMPERATURE, entity_category=EntityCategory.DIAGNOSTIC, suitable=suitable_nextchange_temperature, - native_value=lambda device: device.nextchange_temperature, + native_value=value_nextchange_temperature, ), FritzSensorEntityDescription( key="nextchange_time", @@ -189,7 +207,7 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = ( device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, suitable=suitable_nextchange_time, - native_value=lambda device: utc_from_timestamp(device.nextchange_endperiod), + native_value=value_nextchange_time, ), FritzSensorEntityDescription( key="nextchange_preset", diff --git a/tests/components/fritzbox/__init__.py b/tests/components/fritzbox/__init__.py index bd68615212d..034b86497db 100644 --- a/tests/components/fritzbox/__init__.py +++ b/tests/components/fritzbox/__init__.py @@ -5,7 +5,6 @@ from __future__ import annotations from typing import Any from unittest.mock import Mock -from homeassistant.components.climate import PRESET_COMFORT, PRESET_ECO from homeassistant.components.fritzbox.const import DOMAIN from homeassistant.core import HomeAssistant @@ -110,9 +109,7 @@ class FritzDeviceClimateMock(FritzEntityBaseMock): target_temperature = 19.5 window_open = "fake_window" nextchange_temperature = 22.0 - nextchange_endperiod = 0 - nextchange_preset = PRESET_COMFORT - scheduled_preset = PRESET_ECO + nextchange_endperiod = 1726855200 class FritzDeviceClimateWithoutTempSensorMock(FritzDeviceClimateMock): diff --git a/tests/components/fritzbox/test_climate.py b/tests/components/fritzbox/test_climate.py index 358eeaa714e..2f1e0d37001 100644 --- a/tests/components/fritzbox/test_climate.py +++ b/tests/components/fritzbox/test_climate.py @@ -123,7 +123,7 @@ async def test_setup(hass: HomeAssistant, fritz: Mock) -> None: f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_next_scheduled_change_time" ) assert state - assert state.state == "1970-01-01T00:00:00+00:00" + assert state.state == "2024-09-20T18:00:00+00:00" assert ( state.attributes[ATTR_FRIENDLY_NAME] == f"{CONF_FAKE_NAME} Next scheduled change time" diff --git a/tests/components/fritzbox/test_sensor.py b/tests/components/fritzbox/test_sensor.py index 63d0b67d7f4..753e4d6d3a3 100644 --- a/tests/components/fritzbox/test_sensor.py +++ b/tests/components/fritzbox/test_sensor.py @@ -3,8 +3,10 @@ from datetime import timedelta from unittest.mock import Mock +import pytest from requests.exceptions import HTTPError +from homeassistant.components.climate import PRESET_COMFORT, PRESET_ECO from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN from homeassistant.components.sensor import ATTR_STATE_CLASS, DOMAIN, SensorStateClass from homeassistant.const import ( @@ -12,6 +14,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_DEVICES, PERCENTAGE, + STATE_UNKNOWN, EntityCategory, UnitOfTemperature, ) @@ -19,7 +22,12 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er import homeassistant.util.dt as dt_util -from . import FritzDeviceSensorMock, set_devices, setup_config_entry +from . import ( + FritzDeviceClimateMock, + FritzDeviceSensorMock, + set_devices, + setup_config_entry, +) from .const import CONF_FAKE_NAME, MOCK_CONFIG from tests.common import async_fire_time_changed @@ -132,3 +140,55 @@ async def test_discover_new_device(hass: HomeAssistant, fritz: Mock) -> None: state = hass.states.get(f"{DOMAIN}.new_device_temperature") assert state + + +@pytest.mark.parametrize( + ("next_changes", "expected_states"), + [ + ( + [0, 16], + [STATE_UNKNOWN, STATE_UNKNOWN, STATE_UNKNOWN, STATE_UNKNOWN], + ), + ( + [0, 22], + [STATE_UNKNOWN, STATE_UNKNOWN, STATE_UNKNOWN, STATE_UNKNOWN], + ), + ( + [1726855200, 16.0], + ["2024-09-20T18:00:00+00:00", "16.0", PRESET_ECO, PRESET_COMFORT], + ), + ( + [1726855200, 22.0], + ["2024-09-20T18:00:00+00:00", "22.0", PRESET_COMFORT, PRESET_ECO], + ), + ], +) +async def test_next_change_sensors( + hass: HomeAssistant, fritz: Mock, next_changes: list, expected_states: list +) -> None: + """Test next change sensors.""" + device = FritzDeviceClimateMock() + device.nextchange_endperiod = next_changes[0] + device.nextchange_temperature = next_changes[1] + + assert await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) + + base_name = f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}" + + state = hass.states.get(f"{base_name}_next_scheduled_change_time") + assert state + assert state.state == expected_states[0] + + state = hass.states.get(f"{base_name}_next_scheduled_temperature") + assert state + assert state.state == expected_states[1] + + state = hass.states.get(f"{base_name}_next_scheduled_preset") + assert state + assert state.state == expected_states[2] + + state = hass.states.get(f"{base_name}_current_scheduled_preset") + assert state + assert state.state == expected_states[3] From e8a5a75e96ca5fd52d133b56d4bb40575dba7a8a Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 22 Sep 2024 03:05:52 +0200 Subject: [PATCH 263/271] Bump python-holidays to 0.57 (#126367) --- homeassistant/components/holiday/manifest.json | 2 +- homeassistant/components/workday/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/holiday/manifest.json b/homeassistant/components/holiday/manifest.json index 0a2d98e71c5..30cfd34e0fb 100644 --- a/homeassistant/components/holiday/manifest.json +++ b/homeassistant/components/holiday/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/holiday", "iot_class": "local_polling", - "requirements": ["holidays==0.56", "babel==2.15.0"] + "requirements": ["holidays==0.57", "babel==2.15.0"] } diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index 297b20b8c0e..1201354bab2 100644 --- a/homeassistant/components/workday/manifest.json +++ b/homeassistant/components/workday/manifest.json @@ -7,5 +7,5 @@ "iot_class": "local_polling", "loggers": ["holidays"], "quality_scale": "internal", - "requirements": ["holidays==0.56"] + "requirements": ["holidays==0.57"] } diff --git a/requirements_all.txt b/requirements_all.txt index 6db7772f11c..d4398da66c1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1099,7 +1099,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.56 +holidays==0.57 # homeassistant.components.frontend home-assistant-frontend==20240909.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fb11103d917..7eece637a16 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -922,7 +922,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.56 +holidays==0.57 # homeassistant.components.frontend home-assistant-frontend==20240909.1 From ccec85f047894adb6b70c81b16e86cf5ade264c9 Mon Sep 17 00:00:00 2001 From: Manuel Frei Date: Mon, 23 Sep 2024 10:09:58 +0200 Subject: [PATCH 264/271] Fix surepetcare token update (#126385) Co-authored-by: Joostlek --- .../components/surepetcare/config_flow.py | 98 +++++++++---------- .../surepetcare/test_config_flow.py | 35 ++++--- 2 files changed, 71 insertions(+), 62 deletions(-) diff --git a/homeassistant/components/surepetcare/config_flow.py b/homeassistant/components/surepetcare/config_flow.py index 6626b1d6dee..a993e9a47f1 100644 --- a/homeassistant/components/surepetcare/config_flow.py +++ b/homeassistant/components/surepetcare/config_flow.py @@ -10,9 +10,8 @@ import surepy from surepy.exceptions import SurePetcareAuthenticationError, SurePetcareError import voluptuous as vol -from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME -from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN, SURE_API_TIMEOUT @@ -27,57 +26,43 @@ USER_DATA_SCHEMA = vol.Schema( ) -async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: - """Validate the user input allows us to connect.""" - surepy_client = surepy.Surepy( - data[CONF_USERNAME], - data[CONF_PASSWORD], - auth_token=None, - api_timeout=SURE_API_TIMEOUT, - session=async_get_clientsession(hass), - ) - - token = await surepy_client.sac.get_token() - - return {CONF_TOKEN: token} - - class SurePetCareConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Sure Petcare.""" VERSION = 1 - def __init__(self) -> None: - """Initialize.""" - self._username: str | None = None + reauth_entry: ConfigEntry | None = None async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle the initial step.""" - if user_input is None: - return self.async_show_form(step_id="user", data_schema=USER_DATA_SCHEMA) - errors = {} - - try: - info = await validate_input(self.hass, user_input) - except SurePetcareAuthenticationError: - errors["base"] = "invalid_auth" - except SurePetcareError: - errors["base"] = "cannot_connect" - except Exception: - _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" - else: - await self.async_set_unique_id(user_input[CONF_USERNAME].lower()) - self._abort_if_unique_id_configured() - - user_input[CONF_TOKEN] = info[CONF_TOKEN] - return self.async_create_entry( - title="Sure Petcare", - data=user_input, + if user_input is not None: + client = surepy.Surepy( + user_input[CONF_USERNAME], + user_input[CONF_PASSWORD], + auth_token=None, + api_timeout=SURE_API_TIMEOUT, + session=async_get_clientsession(self.hass), ) + try: + token = await client.sac.get_token() + except SurePetcareAuthenticationError: + errors["base"] = "invalid_auth" + except SurePetcareError: + errors["base"] = "cannot_connect" + except Exception: + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + await self.async_set_unique_id(user_input[CONF_USERNAME].lower()) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title="Sure Petcare", + data={**user_input, CONF_TOKEN: token}, + ) return self.async_show_form( step_id="user", data_schema=USER_DATA_SCHEMA, errors=errors @@ -87,18 +72,27 @@ class SurePetCareConfigFlow(ConfigFlow, domain=DOMAIN): self, entry_data: Mapping[str, Any] ) -> ConfigFlowResult: """Handle configuration by re-auth.""" - self._username = entry_data[CONF_USERNAME] + self.reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Dialog that informs the user that reauth is required.""" + assert self.reauth_entry errors = {} if user_input is not None: - user_input[CONF_USERNAME] = self._username + client = surepy.Surepy( + self.reauth_entry.data[CONF_USERNAME], + user_input[CONF_PASSWORD], + auth_token=None, + api_timeout=SURE_API_TIMEOUT, + session=async_get_clientsession(self.hass), + ) try: - await validate_input(self.hass, user_input) + token = await client.sac.get_token() except SurePetcareAuthenticationError: errors["base"] = "invalid_auth" except SurePetcareError: @@ -107,16 +101,20 @@ class SurePetCareConfigFlow(ConfigFlow, domain=DOMAIN): _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: - existing_entry = await self.async_set_unique_id( - user_input[CONF_USERNAME].lower() + return self.async_update_reload_and_abort( + self.reauth_entry, + data={ + **self.reauth_entry.data, + CONF_PASSWORD: user_input[CONF_PASSWORD], + CONF_TOKEN: token, + }, ) - if existing_entry: - await self.hass.config_entries.async_reload(existing_entry.entry_id) - return self.async_abort(reason="reauth_successful") return self.async_show_form( step_id="reauth_confirm", - description_placeholders={"username": self._username}, + description_placeholders={ + "username": self.reauth_entry.data[CONF_USERNAME] + }, data_schema=vol.Schema({vol.Required(CONF_PASSWORD): str}), errors=errors, ) diff --git a/tests/components/surepetcare/test_config_flow.py b/tests/components/surepetcare/test_config_flow.py index c4055ebe658..1140a2c54ef 100644 --- a/tests/components/surepetcare/test_config_flow.py +++ b/tests/components/surepetcare/test_config_flow.py @@ -6,6 +6,7 @@ from surepy.exceptions import SurePetcareAuthenticationError, SurePetcareError from homeassistant import config_entries from homeassistant.components.surepetcare.const import DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -24,7 +25,7 @@ async def test_form(hass: HomeAssistant, surepetcare: NonCallableMagicMock) -> N DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM - assert result["errors"] is None + assert not result["errors"] with patch( "homeassistant.components.surepetcare.async_setup_entry", @@ -146,11 +147,17 @@ async def test_flow_entry_already_exists( assert result["reason"] == "already_configured" -async def test_reauthentication(hass: HomeAssistant) -> None: +async def test_reauthentication( + hass: HomeAssistant, surepetcare: NonCallableMagicMock +) -> None: """Test surepetcare reauthentication.""" old_entry = MockConfigEntry( domain="surepetcare", - data=INPUT_DATA, + data={ + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_TOKEN: "token", + }, unique_id="test-username", ) old_entry.add_to_hass(hass) @@ -161,19 +168,23 @@ async def test_reauthentication(hass: HomeAssistant) -> None: assert result["errors"] == {} assert result["step_id"] == "reauth_confirm" - with patch( - "homeassistant.components.surepetcare.config_flow.surepy.client.SureAPIClient.get_token", - return_value={"token": "token"}, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"password": "test-password"}, - ) - await hass.async_block_till_done() + surepetcare.get_token.return_value = "token2" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"password": "test-password2"}, + ) + await hass.async_block_till_done() assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" + assert old_entry.data == { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password2", + CONF_TOKEN: "token2", + } + async def test_reauthentication_failure(hass: HomeAssistant) -> None: """Test surepetcare reauthentication failure.""" From 36e6ab4af88b5db8997719c4f0b91483e6dfdadc Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Sun, 22 Sep 2024 12:08:50 +0200 Subject: [PATCH 265/271] Fix due date calculation for future dailies in Habitica integration (#126403) Calculate next due date for dailies with startdate in the future --- homeassistant/components/habitica/util.py | 41 +++++++++++++++++------ 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/habitica/util.py b/homeassistant/components/habitica/util.py index b3241aa5787..0ac3ea2a4e2 100644 --- a/homeassistant/components/habitica/util.py +++ b/homeassistant/components/habitica/util.py @@ -3,7 +3,7 @@ from __future__ import annotations import datetime -from typing import Any +from typing import TYPE_CHECKING, Any from homeassistant.components.automation import automations_with_entity from homeassistant.components.script import scripts_with_entity @@ -14,25 +14,44 @@ from homeassistant.util import dt as dt_util def next_due_date(task: dict[str, Any], last_cron: str) -> datetime.date | None: """Calculate due date for dailies and yesterdailies.""" + today = to_date(last_cron) + startdate = to_date(task["startDate"]) + if TYPE_CHECKING: + assert today + assert startdate + if task["isDue"] and not task["completed"]: - return dt_util.as_local(datetime.datetime.fromisoformat(last_cron)).date() + return to_date(last_cron) + + if startdate > today: + if task["frequency"] == "daily" or ( + task["frequency"] in ("monthly", "yearly") and task["daysOfMonth"] + ): + return startdate + + if ( + task["frequency"] in ("weekly", "monthly") + and (nextdue := to_date(task["nextDue"][0])) + and startdate > nextdue + ): + return to_date(task["nextDue"][1]) + + return to_date(task["nextDue"][0]) + + +def to_date(date: str) -> datetime.date | None: + """Convert an iso date to a datetime.date object.""" try: - return dt_util.as_local( - datetime.datetime.fromisoformat(task["nextDue"][0]) - ).date() + return dt_util.as_local(datetime.datetime.fromisoformat(date)).date() except ValueError: - # sometimes nextDue dates are in this format instead of iso: + # sometimes nextDue dates are JavaScript datetime strings instead of iso: # "Mon May 06 2024 00:00:00 GMT+0200" try: return dt_util.as_local( - datetime.datetime.strptime( - task["nextDue"][0], "%a %b %d %Y %H:%M:%S %Z%z" - ) + datetime.datetime.strptime(date, "%a %b %d %Y %H:%M:%S %Z%z") ).date() except ValueError: return None - except IndexError: - return None def entity_used_in(hass: HomeAssistant, entity_id: str) -> list[str]: From 06d825d6c8d2b85884742a4ba89fd12994b133c9 Mon Sep 17 00:00:00 2001 From: David Knowles Date: Sun, 22 Sep 2024 12:04:19 -0400 Subject: [PATCH 266/271] Bump pydrawise to 2024.9.0 (#126431) --- homeassistant/components/hydrawise/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hydrawise/manifest.json b/homeassistant/components/hydrawise/manifest.json index 9b733cb73d0..9678dc83e5f 100644 --- a/homeassistant/components/hydrawise/manifest.json +++ b/homeassistant/components/hydrawise/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/hydrawise", "iot_class": "cloud_polling", "loggers": ["pydrawise"], - "requirements": ["pydrawise==2024.8.0"] + "requirements": ["pydrawise==2024.9.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index d4398da66c1..8487cf5fb15 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1819,7 +1819,7 @@ pydiscovergy==3.0.1 pydoods==1.0.2 # homeassistant.components.hydrawise -pydrawise==2024.8.0 +pydrawise==2024.9.0 # homeassistant.components.android_ip_webcam pydroid-ipcam==2.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7eece637a16..b3bfc4a25a2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1457,7 +1457,7 @@ pydexcom==0.2.3 pydiscovergy==3.0.1 # homeassistant.components.hydrawise -pydrawise==2024.8.0 +pydrawise==2024.9.0 # homeassistant.components.android_ip_webcam pydroid-ipcam==2.0.0 From c9571126a36180af03f642abaaa5d8e4e79de52e Mon Sep 17 00:00:00 2001 From: Steve Easley Date: Mon, 23 Sep 2024 04:14:01 -0400 Subject: [PATCH 267/271] Add support for new JVC Projector auth method (#126453) --- homeassistant/components/jvc_projector/coordinator.py | 3 ++- homeassistant/components/jvc_projector/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/jvc_projector/coordinator.py b/homeassistant/components/jvc_projector/coordinator.py index 874253b3324..a2ecfa8eb52 100644 --- a/homeassistant/components/jvc_projector/coordinator.py +++ b/homeassistant/components/jvc_projector/coordinator.py @@ -4,6 +4,7 @@ from __future__ import annotations from datetime import timedelta import logging +from typing import Any from jvcprojector import ( JvcProjector, @@ -40,7 +41,7 @@ class JvcProjectorDataUpdateCoordinator(DataUpdateCoordinator[dict[str, str]]): self.device = device self.unique_id = format_mac(device.mac) - async def _async_update_data(self) -> dict[str, str]: + async def _async_update_data(self) -> dict[str, Any]: """Get the latest state data.""" try: state = await self.device.get_state() diff --git a/homeassistant/components/jvc_projector/manifest.json b/homeassistant/components/jvc_projector/manifest.json index 5d83e937494..f24ec4df51c 100644 --- a/homeassistant/components/jvc_projector/manifest.json +++ b/homeassistant/components/jvc_projector/manifest.json @@ -7,5 +7,5 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["jvcprojector"], - "requirements": ["pyjvcprojector==1.0.12"] + "requirements": ["pyjvcprojector==1.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 8487cf5fb15..b3ecfe563d0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1954,7 +1954,7 @@ pyisy==3.1.14 pyitachip2ir==0.0.7 # homeassistant.components.jvc_projector -pyjvcprojector==1.0.12 +pyjvcprojector==1.1.0 # homeassistant.components.kaleidescape pykaleidescape==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b3bfc4a25a2..3ee440ca5fb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1559,7 +1559,7 @@ pyiss==1.0.1 pyisy==3.1.14 # homeassistant.components.jvc_projector -pyjvcprojector==1.0.12 +pyjvcprojector==1.1.0 # homeassistant.components.kaleidescape pykaleidescape==1.0.1 From 08b0064ce7fb3fe0b194e793e36fbac64e88f888 Mon Sep 17 00:00:00 2001 From: Markus Jacobsen Date: Sun, 22 Sep 2024 23:08:27 +0200 Subject: [PATCH 268/271] Fix blocking call in Bang & Olufsen API client initialization (#126456) * Update API * Add fix for blocking call to load_default_certs --- homeassistant/components/bang_olufsen/__init__.py | 3 ++- homeassistant/components/bang_olufsen/config_flow.py | 7 +++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bang_olufsen/__init__.py b/homeassistant/components/bang_olufsen/__init__.py index 07b9d0befe1..e11df6ad5ed 100644 --- a/homeassistant/components/bang_olufsen/__init__.py +++ b/homeassistant/components/bang_olufsen/__init__.py @@ -17,6 +17,7 @@ from homeassistant.const import CONF_HOST, CONF_MODEL, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.device_registry as dr +from homeassistant.util.ssl import get_default_context from .const import DOMAIN from .websocket import BangOlufsenWebsocket @@ -48,7 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: model=entry.data[CONF_MODEL], ) - client = MozartClient(host=entry.data[CONF_HOST]) + client = MozartClient(host=entry.data[CONF_HOST], ssl_context=get_default_context()) # Check API and WebSocket connection try: diff --git a/homeassistant/components/bang_olufsen/config_flow.py b/homeassistant/components/bang_olufsen/config_flow.py index 76e4656129e..a051e9ba3ad 100644 --- a/homeassistant/components/bang_olufsen/config_flow.py +++ b/homeassistant/components/bang_olufsen/config_flow.py @@ -14,6 +14,7 @@ from homeassistant.components.zeroconf import ZeroconfServiceInfo from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_MODEL from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig +from homeassistant.util.ssl import get_default_context from .const import ( ATTR_FRIENDLY_NAME, @@ -87,7 +88,9 @@ class BangOlufsenConfigFlowHandler(ConfigFlow, domain=DOMAIN): errors={"base": _exception_map[type(error)]}, ) - self._client = MozartClient(self._host) + self._client = MozartClient( + host=self._host, ssl_context=get_default_context() + ) # Try to get information from Beolink self method. async with self._client: @@ -136,7 +139,7 @@ class BangOlufsenConfigFlowHandler(ConfigFlow, domain=DOMAIN): return self.async_abort(reason="ipv6_address") # Check connection to ensure valid address is received - self._client = MozartClient(self._host) + self._client = MozartClient(self._host, ssl_context=get_default_context()) async with self._client: try: From 4949727cd54ca612ac2fe71e932bba15a758bf6b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 23 Sep 2024 19:47:41 +0200 Subject: [PATCH 269/271] Bump version to 2024.9.3 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index eec4530576d..2b67710aab2 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "2" +PATCH_VERSION: Final = "3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index ee32a56651e..d1ebd402370 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.9.2" +version = "2024.9.3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 95053f7114514d6d71badb97bf771d427b28b267 Mon Sep 17 00:00:00 2001 From: Markus Jacobsen Date: Sun, 22 Sep 2024 16:36:36 +0200 Subject: [PATCH 270/271] Bump mozart_api to 3.4.1.8.8 (#126334) Update API --- homeassistant/components/bang_olufsen/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bang_olufsen/manifest.json b/homeassistant/components/bang_olufsen/manifest.json index 3cc9fdb5cd1..a93a6e7a624 100644 --- a/homeassistant/components/bang_olufsen/manifest.json +++ b/homeassistant/components/bang_olufsen/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/bang_olufsen", "integration_type": "device", "iot_class": "local_push", - "requirements": ["mozart-api==3.4.1.8.6"], + "requirements": ["mozart-api==3.4.1.8.8"], "zeroconf": ["_bangolufsen._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index b3ecfe563d0..17857d4fada 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1375,7 +1375,7 @@ motionblindsble==0.1.1 motioneye-client==0.3.14 # homeassistant.components.bang_olufsen -mozart-api==3.4.1.8.6 +mozart-api==3.4.1.8.8 # homeassistant.components.mullvad mullvad-api==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3ee440ca5fb..b13ca6df805 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1141,7 +1141,7 @@ motionblindsble==0.1.1 motioneye-client==0.3.14 # homeassistant.components.bang_olufsen -mozart-api==3.4.1.8.6 +mozart-api==3.4.1.8.8 # homeassistant.components.mullvad mullvad-api==1.0.0 From 59ecd47374b79840176529b0b8df756deb06f9a4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 23 Sep 2024 20:09:09 +0200 Subject: [PATCH 271/271] Hotfix test for patch release in fritzbox --- tests/components/fritzbox/test_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/fritzbox/test_sensor.py b/tests/components/fritzbox/test_sensor.py index 753e4d6d3a3..094bec30b64 100644 --- a/tests/components/fritzbox/test_sensor.py +++ b/tests/components/fritzbox/test_sensor.py @@ -175,7 +175,7 @@ async def test_next_change_sensors( hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz ) - base_name = f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}" + base_name = f"{DOMAIN}.{CONF_FAKE_NAME}" state = hass.states.get(f"{base_name}_next_scheduled_change_time") assert state