From 762f15a0d3e301e4829b438dcf5ade5490e03442 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 26 May 2021 19:44:48 +0200 Subject: [PATCH 001/123] Bumped version to 2021.6.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 00e06f1e8d0..581c08c5f94 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 6 -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, 8, 0) From 255577436e614425906dee46deae051cd2a098fa Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Thu, 27 May 2021 10:55:47 +0200 Subject: [PATCH 002/123] Followup PR for SIA integration (#51108) * Updates based on Martin's review * fix strings and cleaned up constants --- .../components/sia/alarm_control_panel.py | 62 +++++-------- homeassistant/components/sia/config_flow.py | 30 +++---- homeassistant/components/sia/const.py | 34 +++----- homeassistant/components/sia/hub.py | 24 +++-- homeassistant/components/sia/manifest.json | 2 +- homeassistant/components/sia/strings.json | 2 +- homeassistant/components/sia/utils.py | 87 +++++++++++-------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 9 files changed, 114 insertions(+), 131 deletions(-) diff --git a/homeassistant/components/sia/alarm_control_panel.py b/homeassistant/components/sia/alarm_control_panel.py index 9d5f62b02de..fe5b95b639e 100644 --- a/homeassistant/components/sia/alarm_control_panel.py +++ b/homeassistant/components/sia/alarm_control_panel.py @@ -2,18 +2,14 @@ from __future__ import annotations import logging -from typing import Any, Callable +from typing import Any from pysiaalarm import SIAEvent -from homeassistant.components.alarm_control_panel import ( - ENTITY_ID_FORMAT as ALARM_ENTITY_ID_FORMAT, - AlarmControlPanelEntity, -) +from homeassistant.components.alarm_control_panel import AlarmControlPanelEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_PORT, - CONF_ZONE, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMED_NIGHT, @@ -21,8 +17,10 @@ from homeassistant.const import ( STATE_ALARM_TRIGGERED, STATE_UNAVAILABLE, ) -from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_call_later from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import StateType @@ -33,7 +31,6 @@ from .const import ( CONF_PING_INTERVAL, CONF_ZONES, DOMAIN, - SIA_ENTITY_ID_FORMAT, SIA_EVENT, SIA_NAME_FORMAT, SIA_UNIQUE_ID_FORMAT_ALARM, @@ -76,21 +73,17 @@ CODE_CONSEQUENCES: dict[str, StateType] = { async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[..., None], -) -> bool: + async_add_entities: AddEntitiesCallback, +) -> None: """Set up SIA alarm_control_panel(s) from a config entry.""" async_add_entities( - [ - SIAAlarmControlPanel(entry, account_data, zone) - for account_data in entry.data[CONF_ACCOUNTS] - for zone in range( - 1, - entry.options[CONF_ACCOUNTS][account_data[CONF_ACCOUNT]][CONF_ZONES] - + 1, - ) - ] + SIAAlarmControlPanel(entry, account_data, zone) + for account_data in entry.data[CONF_ACCOUNTS] + for zone in range( + 1, + entry.options[CONF_ACCOUNTS][account_data[CONF_ACCOUNT]][CONF_ZONES] + 1, + ) ) - return True class SIAAlarmControlPanel(AlarmControlPanelEntity, RestoreEntity): @@ -111,18 +104,7 @@ class SIAAlarmControlPanel(AlarmControlPanelEntity, RestoreEntity): self._account: str = self._account_data[CONF_ACCOUNT] self._ping_interval: int = self._account_data[CONF_PING_INTERVAL] - self.entity_id: str = ALARM_ENTITY_ID_FORMAT.format( - SIA_ENTITY_ID_FORMAT.format( - self._port, self._account, self._zone, DEVICE_CLASS_ALARM - ) - ) - - self._attr: dict[str, Any] = { - CONF_PORT: self._port, - CONF_ACCOUNT: self._account, - CONF_ZONE: self._zone, - CONF_PING_INTERVAL: f"{self._ping_interval} minute(s)", - } + self._attr: dict[str, Any] = {} self._available: bool = True self._state: StateType = None @@ -134,16 +116,17 @@ class SIAAlarmControlPanel(AlarmControlPanelEntity, RestoreEntity): Overridden from Entity. - 1. start the event listener and add the callback to on_remove + 1. register the dispatcher and add the callback to on_remove 2. get previous state from storage 3. if previous state: restore 4. if previous state is unavailable: set _available to False and return 5. if available: create availability cb """ self.async_on_remove( - self.hass.bus.async_listen( - event_type=SIA_EVENT.format(self._port, self._account), - listener=self.async_handle_event, + async_dispatcher_connect( + self.hass, + SIA_EVENT.format(self._port, self._account), + self.async_handle_event, ) ) last_state = await self.async_get_last_state() @@ -162,14 +145,11 @@ class SIAAlarmControlPanel(AlarmControlPanelEntity, RestoreEntity): if self._cancel_availability_cb: self._cancel_availability_cb() - async def async_handle_event(self, event: Event) -> None: - """Listen to events for this port and account and update state and attributes. + async def async_handle_event(self, sia_event: SIAEvent) -> None: + """Listen to dispatcher events for this port and account and update state and attributes. If the port and account combo receives any message it means it is online and can therefore be set to available. """ - sia_event: SIAEvent = SIAEvent.from_dict( # pylint: disable=no-member - event.data - ) _LOGGER.debug("Received event: %s", sia_event) if int(sia_event.ri) == self._zone: self._attr.update(get_attr_from_sia_event(sia_event)) diff --git a/homeassistant/components/sia/config_flow.py b/homeassistant/components/sia/config_flow.py index fe49ec65777..a9b49765c19 100644 --- a/homeassistant/components/sia/config_flow.py +++ b/homeassistant/components/sia/config_flow.py @@ -18,6 +18,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PORT, CONF_PROTOCOL from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.typing import ConfigType from .const import ( @@ -104,7 +105,7 @@ class SIAConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._data: ConfigType = {} self._options: Mapping[str, Any] = {CONF_ACCOUNTS: {}} - async def async_step_user(self, user_input: ConfigType = None): + async def async_step_user(self, user_input: ConfigType = None) -> FlowResult: """Handle the initial user step.""" errors: dict[str, str] | None = None if user_input is not None: @@ -115,7 +116,7 @@ class SIAConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) return await self.async_handle_data_and_route(user_input) - async def async_step_add_account(self, user_input: ConfigType = None): + async def async_step_add_account(self, user_input: ConfigType = None) -> FlowResult: """Handle the additional accounts steps.""" errors: dict[str, str] | None = None if user_input is not None: @@ -126,11 +127,11 @@ class SIAConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) return await self.async_handle_data_and_route(user_input) - async def async_handle_data_and_route(self, user_input: ConfigType): + async def async_handle_data_and_route(self, user_input: ConfigType) -> FlowResult: """Handle the user_input, check if configured and route to the right next step or create entry.""" self._update_data(user_input) - if self._data and self._port_already_configured(): - return self.async_abort(reason="already_configured") + + self._async_abort_entries_match({CONF_PORT: self._data[CONF_PORT]}) if user_input[CONF_ADDITIONAL_ACCOUNTS]: return await self.async_step_add_account() @@ -163,13 +164,6 @@ class SIAConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._options[CONF_ACCOUNTS].setdefault(account, deepcopy(DEFAULT_OPTIONS)) self._options[CONF_ACCOUNTS][account][CONF_ZONES] = user_input[CONF_ZONES] - def _port_already_configured(self): - """See if we already have a SIA entry matching the port.""" - for entry in self._async_current_entries(include_ignore=False): - if entry.data[CONF_PORT] == self._data[CONF_PORT]: - return True - return False - class SIAOptionsFlowHandler(config_entries.OptionsFlow): """Handle SIA options.""" @@ -181,14 +175,15 @@ class SIAOptionsFlowHandler(config_entries.OptionsFlow): self.hub: SIAHub | None = None self.accounts_todo: list = [] - async def async_step_init(self, user_input: ConfigType = None): + async def async_step_init(self, user_input: ConfigType = None) -> FlowResult: """Manage the SIA options.""" self.hub = self.hass.data[DOMAIN][self.config_entry.entry_id] - if self.hub is not None and self.hub.sia_accounts is not None: - self.accounts_todo = [a.account_id for a in self.hub.sia_accounts] - return await self.async_step_options() + assert self.hub is not None + assert self.hub.sia_accounts is not None + self.accounts_todo = [a.account_id for a in self.hub.sia_accounts] + return await self.async_step_options() - async def async_step_options(self, user_input: ConfigType = None): + async def async_step_options(self, user_input: ConfigType = None) -> FlowResult: """Create the options step for a account.""" errors: dict[str, str] | None = None if user_input is not None: @@ -223,7 +218,6 @@ class SIAOptionsFlowHandler(config_entries.OptionsFlow): self.options[CONF_ACCOUNTS][account][CONF_ZONES] = user_input[CONF_ZONES] if self.accounts_todo: return await self.async_step_options() - _LOGGER.warning("Updating SIA Options with %s", self.options) return self.async_create_entry(title="", data=self.options) @property diff --git a/homeassistant/components/sia/const.py b/homeassistant/components/sia/const.py index ceeaac75923..916cdb9621c 100644 --- a/homeassistant/components/sia/const.py +++ b/homeassistant/components/sia/const.py @@ -5,34 +5,24 @@ from homeassistant.components.alarm_control_panel import ( PLATFORMS = [ALARM_CONTROL_PANEL_DOMAIN] +DOMAIN = "sia" + +ATTR_CODE = "last_code" +ATTR_ZONE = "zone" +ATTR_MESSAGE = "last_message" +ATTR_ID = "last_id" +ATTR_TIMESTAMP = "last_timestamp" + +TITLE = "SIA Alarm on port {}" CONF_ACCOUNT = "account" CONF_ACCOUNTS = "accounts" CONF_ADDITIONAL_ACCOUNTS = "additional_account" -CONF_PING_INTERVAL = "ping_interval" CONF_ENCRYPTION_KEY = "encryption_key" -CONF_ZONES = "zones" CONF_IGNORE_TIMESTAMPS = "ignore_timestamps" +CONF_PING_INTERVAL = "ping_interval" +CONF_ZONES = "zones" -DOMAIN = "sia" -TITLE = "SIA Alarm on port {}" -SIA_EVENT = "sia_event_{}_{}" SIA_NAME_FORMAT = "{} - {} - zone {} - {}" -SIA_NAME_FORMAT_HUB = "{} - {} - {}" -SIA_ENTITY_ID_FORMAT = "{}_{}_{}_{}" -SIA_ENTITY_ID_FORMAT_HUB = "{}_{}_{}" SIA_UNIQUE_ID_FORMAT_ALARM = "{}_{}_{}" -SIA_UNIQUE_ID_FORMAT = "{}_{}_{}_{}" -HUB_SENSOR_NAME = "last_heartbeat" -HUB_ZONE = 0 -PING_INTERVAL_MARGIN = 30 -DEFAULT_TIMEBAND = (80, 40) -IGNORED_TIMEBAND = (3600, 1800) - -EVENT_CODE = "last_code" -EVENT_ACCOUNT = "account" -EVENT_ZONE = "zone" -EVENT_PORT = "port" -EVENT_MESSAGE = "last_message" -EVENT_ID = "last_id" -EVENT_TIMESTAMP = "last_timestamp" +SIA_EVENT = "sia_event_{}_{}" diff --git a/homeassistant/components/sia/hub.py b/homeassistant/components/sia/hub.py index e5dc7b85ed8..387c2273606 100644 --- a/homeassistant/components/sia/hub.py +++ b/homeassistant/components/sia/hub.py @@ -9,8 +9,9 @@ from pysiaalarm.aio import CommunicationsProtocol, SIAAccount, SIAClient, SIAEve from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PORT, CONF_PROTOCOL, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import Event, EventOrigin, HomeAssistant +from homeassistant.core import Event, HomeAssistant from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import ( CONF_ACCOUNT, @@ -18,16 +19,19 @@ from .const import ( CONF_ENCRYPTION_KEY, CONF_IGNORE_TIMESTAMPS, CONF_ZONES, - DEFAULT_TIMEBAND, DOMAIN, - IGNORED_TIMEBAND, PLATFORMS, SIA_EVENT, ) +from .utils import get_event_data_from_sia_event _LOGGER = logging.getLogger(__name__) +DEFAULT_TIMEBAND = (80, 40) +IGNORED_TIMEBAND = (3600, 1800) + + class SIAHub: """Class for SIA Hubs.""" @@ -39,7 +43,7 @@ class SIAHub: """Create the SIAHub.""" self._hass: HomeAssistant = hass self._entry: ConfigEntry = entry - self._port: int = int(entry.data[CONF_PORT]) + self._port: int = entry.data[CONF_PORT] self._title: str = entry.title self._accounts: list[dict[str, Any]] = deepcopy(entry.data[CONF_ACCOUNTS]) self._protocol: str = entry.data[CONF_PROTOCOL] @@ -69,21 +73,23 @@ class SIAHub: await self.sia_client.stop() async def async_create_and_fire_event(self, event: SIAEvent) -> None: - """Create a event on HA's bus, with the data from the SIAEvent. + """Create a event on HA dispatcher and then on HA's bus, with the data from the SIAEvent. The created event is handled by default for only a small subset for each platform (there are about 320 SIA Codes defined, only 22 of those are used in the alarm_control_panel), a user can choose to build other automation or even entities on the same event for SIA codes not handled by the built-in platforms. """ _LOGGER.debug( - "Adding event to bus for code %s for port %s and account %s", + "Adding event to dispatch and bus for code %s for port %s and account %s", event.code, self._port, event.account, ) + async_dispatcher_send( + self._hass, SIA_EVENT.format(self._port, event.account), event + ) self._hass.bus.async_fire( event_type=SIA_EVENT.format(self._port, event.account), - event_data=event.to_dict(encode_json=True), - origin=EventOrigin.remote, + event_data=get_event_data_from_sia_event(event), ) def update_accounts(self): @@ -115,7 +121,7 @@ class SIAHub: options = dict(self._entry.options) for acc in self._accounts: acc_id = acc[CONF_ACCOUNT] - if acc_id in options[CONF_ACCOUNTS].keys(): + if acc_id in options[CONF_ACCOUNTS]: acc[CONF_IGNORE_TIMESTAMPS] = options[CONF_ACCOUNTS][acc_id][ CONF_IGNORE_TIMESTAMPS ] diff --git a/homeassistant/components/sia/manifest.json b/homeassistant/components/sia/manifest.json index 67c2a0e91a1..eaeb4547167 100644 --- a/homeassistant/components/sia/manifest.json +++ b/homeassistant/components/sia/manifest.json @@ -3,7 +3,7 @@ "name": "SIA Alarm Systems", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sia", - "requirements": ["pysiaalarm==3.0.0b12"], + "requirements": ["pysiaalarm==3.0.0"], "codeowners": ["@eavanvalkenburg"], "iot_class": "local_push" } diff --git a/homeassistant/components/sia/strings.json b/homeassistant/components/sia/strings.json index b091fdd341d..f837d41056a 100644 --- a/homeassistant/components/sia/strings.json +++ b/homeassistant/components/sia/strings.json @@ -27,7 +27,7 @@ }, "error": { "invalid_key_format": "The key is not a hex value, please use only 0-9 and A-F.", - "invalid_key_length": "The key is not the right length, it has to be 16, 24 or 32 characters hex characters.", + "invalid_key_length": "The key is not the right length, it has to be 16, 24 or 32 hex characters.", "invalid_account_format": "The account is not a hex value, please use only 0-9 and A-F.", "invalid_account_length": "The account is not the right length, it has to be between 3 and 16 characters.", "invalid_ping": "The ping interval needs to be between 1 and 1440 minutes.", diff --git a/homeassistant/components/sia/utils.py b/homeassistant/components/sia/utils.py index 9b02025aa8d..08e0fce8ab2 100644 --- a/homeassistant/components/sia/utils.py +++ b/homeassistant/components/sia/utils.py @@ -6,19 +6,9 @@ from typing import Any from pysiaalarm import SIAEvent -from homeassistant.const import DEVICE_CLASS_TIMESTAMP +from .const import ATTR_CODE, ATTR_ID, ATTR_MESSAGE, ATTR_TIMESTAMP, ATTR_ZONE -from .const import ( - EVENT_ACCOUNT, - EVENT_CODE, - EVENT_ID, - EVENT_MESSAGE, - EVENT_TIMESTAMP, - EVENT_ZONE, - HUB_SENSOR_NAME, - HUB_ZONE, - PING_INTERVAL_MARGIN, -) +PING_INTERVAL_MARGIN = 30 def get_unavailability_interval(ping: int) -> float: @@ -26,32 +16,55 @@ def get_unavailability_interval(ping: int) -> float: return timedelta(minutes=ping, seconds=PING_INTERVAL_MARGIN).total_seconds() -def get_name(port: int, account: str, zone: int, entity_type: str) -> str: - """Give back a entity_id and name according to the variables.""" - if zone == HUB_ZONE: - return f"{port} - {account} - {'Last Heartbeat' if entity_type == DEVICE_CLASS_TIMESTAMP else 'Power'}" - return f"{port} - {account} - zone {zone} - {entity_type}" - - -def get_entity_id(port: int, account: str, zone: int, entity_type: str) -> str: - """Give back a entity_id according to the variables.""" - if zone == HUB_ZONE: - return f"{port}_{account}_{HUB_SENSOR_NAME if entity_type == DEVICE_CLASS_TIMESTAMP else entity_type}" - return f"{port}_{account}_{zone}_{entity_type}" - - -def get_unique_id(entry_id: str, account: str, zone: int, domain: str) -> str: - """Return the unique id.""" - return f"{entry_id}_{account}_{zone}_{domain}" - - def get_attr_from_sia_event(event: SIAEvent) -> dict[str, Any]: """Create the attributes dict from a SIAEvent.""" return { - EVENT_ACCOUNT: event.account, - EVENT_ZONE: event.ri, - EVENT_CODE: event.code, - EVENT_MESSAGE: event.message, - EVENT_ID: event.id, - EVENT_TIMESTAMP: event.timestamp, + ATTR_ZONE: event.ri, + ATTR_CODE: event.code, + ATTR_MESSAGE: event.message, + ATTR_ID: event.id, + ATTR_TIMESTAMP: event.timestamp.isoformat(), + } + + +def get_event_data_from_sia_event(event: SIAEvent) -> dict[str, Any]: + """Create a dict from the SIA Event for the HA Event.""" + return { + "message_type": event.message_type, + "receiver": event.receiver, + "line": event.line, + "account": event.account, + "sequence": event.sequence, + "content": event.content, + "ti": event.ti, + "id": event.id, + "ri": event.ri, + "code": event.code, + "message": event.message, + "x_data": event.x_data, + "timestamp": event.timestamp.isoformat(), + "event_qualifier": event.qualifier, + "event_type": event.event_type, + "partition": event.partition, + "extended_data": [ + { + "identifier": xd.identifier, + "name": xd.name, + "description": xd.description, + "length": xd.length, + "characters": xd.characters, + "value": xd.value, + } + for xd in event.extended_data + ] + if event.extended_data is not None + else None, + "sia_code": { + "code": event.sia_code.code, + "type": event.sia_code.type, + "description": event.sia_code.description, + "concerns": event.sia_code.concerns, + } + if event.sia_code is not None + else None, } diff --git a/requirements_all.txt b/requirements_all.txt index 98774e9a9ef..cb3b05fed6e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1723,7 +1723,7 @@ pysesame2==1.0.1 pysher==1.0.1 # homeassistant.components.sia -pysiaalarm==3.0.0b12 +pysiaalarm==3.0.0 # homeassistant.components.signal_messenger pysignalclirestapi==0.3.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d2aa89f3d53..148936e126e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -962,7 +962,7 @@ pyserial-asyncio==0.5 pyserial==3.5 # homeassistant.components.sia -pysiaalarm==3.0.0b12 +pysiaalarm==3.0.0 # homeassistant.components.signal_messenger pysignalclirestapi==0.3.4 From c2c760eb8b89fd6cfabed8e15d77da0ed90036a7 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 27 May 2021 00:27:35 -0400 Subject: [PATCH 003/123] Fix zwave_js.set_value schema (#51114) * fix zwave_js.set_value schema * wrap all schemas in vol.Schema * readd removed assertions --- homeassistant/components/zwave_js/services.py | 106 ++++++++++-------- tests/components/zwave_js/test_services.py | 15 +++ 2 files changed, 75 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/zwave_js/services.py b/homeassistant/components/zwave_js/services.py index 48719063376..c2ebe965fdd 100644 --- a/homeassistant/components/zwave_js/services.py +++ b/homeassistant/components/zwave_js/services.py @@ -67,22 +67,26 @@ class ZWaveServices: const.DOMAIN, const.SERVICE_SET_CONFIG_PARAMETER, self.async_set_config_parameter, - schema=vol.All( - { - vol.Optional(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(const.ATTR_CONFIG_PARAMETER): vol.Any( - vol.Coerce(int), cv.string - ), - vol.Optional(const.ATTR_CONFIG_PARAMETER_BITMASK): vol.Any( - vol.Coerce(int), BITMASK_SCHEMA - ), - vol.Required(const.ATTR_CONFIG_VALUE): vol.Any( - vol.Coerce(int), cv.string - ), - }, - cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), - parameter_name_does_not_need_bitmask, + schema=vol.Schema( + vol.All( + { + vol.Optional(ATTR_DEVICE_ID): vol.All( + cv.ensure_list, [cv.string] + ), + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(const.ATTR_CONFIG_PARAMETER): vol.Any( + vol.Coerce(int), cv.string + ), + vol.Optional(const.ATTR_CONFIG_PARAMETER_BITMASK): vol.Any( + vol.Coerce(int), BITMASK_SCHEMA + ), + vol.Required(const.ATTR_CONFIG_VALUE): vol.Any( + vol.Coerce(int), cv.string + ), + }, + cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), + parameter_name_does_not_need_bitmask, + ), ), ) @@ -90,21 +94,25 @@ class ZWaveServices: const.DOMAIN, const.SERVICE_BULK_SET_PARTIAL_CONFIG_PARAMETERS, self.async_bulk_set_partial_config_parameters, - schema=vol.All( - { - vol.Optional(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(const.ATTR_CONFIG_PARAMETER): vol.Coerce(int), - vol.Required(const.ATTR_CONFIG_VALUE): vol.Any( - vol.Coerce(int), - { - vol.Any( - vol.Coerce(int), BITMASK_SCHEMA, cv.string - ): vol.Any(vol.Coerce(int), cv.string) - }, - ), - }, - cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), + schema=vol.Schema( + vol.All( + { + vol.Optional(ATTR_DEVICE_ID): vol.All( + cv.ensure_list, [cv.string] + ), + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(const.ATTR_CONFIG_PARAMETER): vol.Coerce(int), + vol.Required(const.ATTR_CONFIG_VALUE): vol.Any( + vol.Coerce(int), + { + vol.Any( + vol.Coerce(int), BITMASK_SCHEMA, cv.string + ): vol.Any(vol.Coerce(int), cv.string) + }, + ), + }, + cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), + ), ), ) @@ -125,21 +133,27 @@ class ZWaveServices: const.SERVICE_SET_VALUE, self.async_set_value, schema=vol.Schema( - { - vol.Optional(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(const.ATTR_COMMAND_CLASS): vol.Coerce(int), - vol.Required(const.ATTR_PROPERTY): vol.Any(vol.Coerce(int), str), - vol.Optional(const.ATTR_PROPERTY_KEY): vol.Any( - vol.Coerce(int), str - ), - vol.Optional(const.ATTR_ENDPOINT): vol.Coerce(int), - vol.Required(const.ATTR_VALUE): vol.Any( - bool, vol.Coerce(int), vol.Coerce(float), cv.string - ), - vol.Optional(const.ATTR_WAIT_FOR_RESULT): vol.Coerce(bool), - }, - cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), + vol.All( + { + vol.Optional(ATTR_DEVICE_ID): vol.All( + cv.ensure_list, [cv.string] + ), + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(const.ATTR_COMMAND_CLASS): vol.Coerce(int), + vol.Required(const.ATTR_PROPERTY): vol.Any( + vol.Coerce(int), str + ), + vol.Optional(const.ATTR_PROPERTY_KEY): vol.Any( + vol.Coerce(int), str + ), + vol.Optional(const.ATTR_ENDPOINT): vol.Coerce(int), + vol.Required(const.ATTR_VALUE): vol.Any( + bool, vol.Coerce(int), vol.Coerce(float), cv.string + ), + vol.Optional(const.ATTR_WAIT_FOR_RESULT): vol.Coerce(bool), + }, + cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), + ), ), ) diff --git a/tests/components/zwave_js/test_services.py b/tests/components/zwave_js/test_services.py index 956361d3953..3c08c49a36f 100644 --- a/tests/components/zwave_js/test_services.py +++ b/tests/components/zwave_js/test_services.py @@ -599,6 +599,7 @@ async def test_set_value(hass, client, climate_danfoss_lc_13, integration): ) assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] assert args["command"] == "node.set_value" assert args["nodeId"] == 5 @@ -619,3 +620,17 @@ async def test_set_value(hass, client, climate_danfoss_lc_13, integration): "value": 0, } assert args["value"] == 2 + + # Test missing device and entities keys + with pytest.raises(vol.MultipleInvalid): + await hass.services.async_call( + DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_COMMAND_CLASS: 117, + ATTR_PROPERTY: "local", + ATTR_VALUE: 2, + ATTR_WAIT_FOR_RESULT: True, + }, + blocking=True, + ) From 4ebc0d97bcec65caa4cee85bd9ca04b2c69a2dd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 27 May 2021 06:04:05 +0200 Subject: [PATCH 004/123] Handle blank string in location name for mobile app (#51130) --- homeassistant/components/mobile_app/device_tracker.py | 4 +++- tests/components/mobile_app/test_device_tracker.py | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mobile_app/device_tracker.py b/homeassistant/components/mobile_app/device_tracker.py index 1b006f69827..1deebf6b531 100644 --- a/homeassistant/components/mobile_app/device_tracker.py +++ b/homeassistant/components/mobile_app/device_tracker.py @@ -94,7 +94,9 @@ class MobileAppEntity(TrackerEntity, RestoreEntity): @property def location_name(self): """Return a location name for the current location of the device.""" - return self._data.get(ATTR_LOCATION_NAME) + if location_name := self._data.get(ATTR_LOCATION_NAME): + return location_name + return None @property def name(self): diff --git a/tests/components/mobile_app/test_device_tracker.py b/tests/components/mobile_app/test_device_tracker.py index 164b90a5290..b755a0a8d09 100644 --- a/tests/components/mobile_app/test_device_tracker.py +++ b/tests/components/mobile_app/test_device_tracker.py @@ -48,6 +48,7 @@ async def test_sending_location(hass, create_registrations, webhook_client): "course": 6, "speed": 7, "vertical_accuracy": 8, + "location_name": "", }, }, ) @@ -82,7 +83,6 @@ async def test_restoring_location(hass, create_registrations, webhook_client): "course": 60, "speed": 70, "vertical_accuracy": 80, - "location_name": "bar", }, }, ) @@ -104,6 +104,7 @@ async def test_restoring_location(hass, create_registrations, webhook_client): assert state_1 is not state_2 assert state_2.name == "Test 1" + assert state_2.state == "not_home" assert state_2.attributes["source_type"] == "gps" assert state_2.attributes["latitude"] == 10 assert state_2.attributes["longitude"] == 20 From 74e397dc73a287651a68f82be655673ca5272e02 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 27 May 2021 00:12:43 -0500 Subject: [PATCH 005/123] Fix Sonos TV source attribute (#51131) --- homeassistant/components/sonos/speaker.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 7ce51176a88..e827b35b16d 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -819,7 +819,9 @@ class SonosSpeaker: if variables and "transport_state" in variables: self.media.play_mode = variables["current_play_mode"] - track_uri = variables["enqueued_transport_uri"] + track_uri = ( + variables["enqueued_transport_uri"] or variables["current_track_uri"] + ) music_source = self.soco.music_source_from_uri(track_uri) else: self.media.play_mode = self.soco.play_mode From f3639c60e202e1a94490125391bdd8785dd33012 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 27 May 2021 03:53:51 -0500 Subject: [PATCH 006/123] Fix Sonos media position with radio sources (#51137) --- homeassistant/components/sonos/speaker.py | 25 +++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index e827b35b16d..c1f1dbb9104 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -844,7 +844,8 @@ class SonosSpeaker: if music_source == MUSIC_SRC_RADIO: self.update_media_radio(variables) else: - self.update_media_music(update_position, track_info) + self.update_media_music(track_info) + self.update_media_position(update_position, track_info) self.write_entity_states() @@ -907,11 +908,25 @@ class SonosSpeaker: if fav.reference.get_uri() == media_info["uri"]: self.media.source_name = fav.title - def update_media_music(self, update_media_position: bool, track_info: dict) -> None: + def update_media_music(self, track_info: dict) -> None: + """Update state when playing music tracks.""" + self.media.image_url = track_info.get("album_art") + + playlist_position = int(track_info.get("playlist_position")) # type: ignore + if playlist_position > 0: + self.media.queue_position = playlist_position - 1 + + def update_media_position( + self, update_media_position: bool, track_info: dict + ) -> None: """Update state when playing music tracks.""" self.media.duration = _timespan_secs(track_info.get("duration")) current_position = _timespan_secs(track_info.get("position")) + if self.media.duration == 0: + self.media.clear_position() + return + # player started reporting position? if current_position is not None and self.media.position is None: update_media_position = True @@ -935,9 +950,3 @@ class SonosSpeaker: elif update_media_position: self.media.position = current_position self.media.position_updated_at = dt_util.utcnow() - - self.media.image_url = track_info.get("album_art") - - playlist_position = int(track_info.get("playlist_position")) # type: ignore - if playlist_position > 0: - self.media.queue_position = playlist_position - 1 From b92db104dcefa5549fc0761a654fcb93adbbdfe9 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 28 May 2021 11:01:28 +0200 Subject: [PATCH 007/123] Add deprecated backwards compatible history.LazyState (#51144) --- homeassistant/components/history/__init__.py | 20 +++--- homeassistant/helpers/deprecation.py | 72 +++++++++++++------- 2 files changed, 60 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index c92718a87e4..ac8a13e69ad 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -13,8 +13,7 @@ import voluptuous as vol from homeassistant.components import websocket_api from homeassistant.components.http import HomeAssistantView -from homeassistant.components.recorder import history -from homeassistant.components.recorder.models import States +from homeassistant.components.recorder import history, models as history_models from homeassistant.components.recorder.statistics import statistics_during_period from homeassistant.components.recorder.util import session_scope from homeassistant.const import ( @@ -26,7 +25,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.deprecation import deprecated_function +from homeassistant.helpers.deprecation import deprecated_class, deprecated_function from homeassistant.helpers.entityfilter import ( CONF_ENTITY_GLOBS, INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA, @@ -110,6 +109,11 @@ async def async_setup(hass, config): return True +@deprecated_class("homeassistant.components.recorder.models.LazyState") +class LazyState(history_models.LazyState): + """A lazy version of core State.""" + + @websocket_api.websocket_command( { vol.Required("type"): "history/statistics_during_period", @@ -345,17 +349,17 @@ class Filters: """Generate the entity filter query.""" includes = [] if self.included_domains: - includes.append(States.domain.in_(self.included_domains)) + includes.append(history_models.States.domain.in_(self.included_domains)) if self.included_entities: - includes.append(States.entity_id.in_(self.included_entities)) + includes.append(history_models.States.entity_id.in_(self.included_entities)) for glob in self.included_entity_globs: includes.append(_glob_to_like(glob)) excludes = [] if self.excluded_domains: - excludes.append(States.domain.in_(self.excluded_domains)) + excludes.append(history_models.States.domain.in_(self.excluded_domains)) if self.excluded_entities: - excludes.append(States.entity_id.in_(self.excluded_entities)) + excludes.append(history_models.States.entity_id.in_(self.excluded_entities)) for glob in self.excluded_entity_globs: excludes.append(_glob_to_like(glob)) @@ -373,7 +377,7 @@ class Filters: def _glob_to_like(glob_str): """Translate glob to sql.""" - return States.entity_id.like(glob_str.translate(GLOB_TO_SQL_CHARS)) + return history_models.States.entity_id.like(glob_str.translate(GLOB_TO_SQL_CHARS)) def _entities_may_have_state_changes_after( diff --git a/homeassistant/helpers/deprecation.py b/homeassistant/helpers/deprecation.py index 06f09327dc9..adf3d8a5d88 100644 --- a/homeassistant/helpers/deprecation.py +++ b/homeassistant/helpers/deprecation.py @@ -80,6 +80,23 @@ def get_deprecated( return config.get(new_name, default) +def deprecated_class(replacement: str) -> Any: + """Mark class as deprecated and provide a replacement class to be used instead.""" + + def deprecated_decorator(cls: Any) -> Any: + """Decorate class as deprecated.""" + + @functools.wraps(cls) + def deprecated_cls(*args: tuple, **kwargs: dict[str, Any]) -> Any: + """Wrap for the original class.""" + _print_deprecation_warning(cls, replacement, "class") + return cls(*args, **kwargs) + + return deprecated_cls + + return deprecated_decorator + + def deprecated_function(replacement: str) -> Callable[..., Callable]: """Mark function as deprecated and provide a replacement function to be used instead.""" @@ -89,32 +106,39 @@ def deprecated_function(replacement: str) -> Callable[..., Callable]: @functools.wraps(func) def deprecated_func(*args: tuple, **kwargs: dict[str, Any]) -> Any: """Wrap for the original function.""" - logger = logging.getLogger(func.__module__) - try: - _, integration, path = get_integration_frame() - if path == "custom_components/": - logger.warning( - "%s was called from %s, this is a deprecated function. Use %s instead, please report this to the maintainer of %s", - func.__name__, - integration, - replacement, - integration, - ) - else: - logger.warning( - "%s was called from %s, this is a deprecated function. Use %s instead", - func.__name__, - integration, - replacement, - ) - except MissingIntegrationFrame: - logger.warning( - "%s is a deprecated function. Use %s instead", - func.__name__, - replacement, - ) + _print_deprecation_warning(func, replacement, "function") return func(*args, **kwargs) return deprecated_func return deprecated_decorator + + +def _print_deprecation_warning(obj: Any, replacement: str, description: str) -> None: + logger = logging.getLogger(obj.__module__) + try: + _, integration, path = get_integration_frame() + if path == "custom_components/": + logger.warning( + "%s was called from %s, this is a deprecated %s. Use %s instead, please report this to the maintainer of %s", + obj.__name__, + integration, + description, + replacement, + integration, + ) + else: + logger.warning( + "%s was called from %s, this is a deprecated %s. Use %s instead", + obj.__name__, + integration, + description, + replacement, + ) + except MissingIntegrationFrame: + logger.warning( + "%s is a deprecated %s. Use %s instead", + obj.__name__, + description, + replacement, + ) From 27e32bbb19d3151b90f1e5506fccfe32e990c094 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 28 May 2021 13:16:52 +0200 Subject: [PATCH 008/123] Weight sensor average statistics by state durations (#51150) * Weight sensor average statistics by state durations * Fix test --- homeassistant/components/sensor/recorder.py | 45 +++++++++++++++++--- tests/components/recorder/test_statistics.py | 2 +- tests/components/sensor/test_recorder.py | 24 +++++------ 3 files changed, 53 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index a75aa6298bc..fb6c8d2fba3 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -3,7 +3,6 @@ from __future__ import annotations import datetime import itertools -from statistics import fmean from homeassistant.components.recorder import history, statistics from homeassistant.components.sensor import ( @@ -16,7 +15,7 @@ from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, ) from homeassistant.const import ATTR_DEVICE_CLASS -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, State import homeassistant.util.dt as dt_util from . import DOMAIN @@ -53,6 +52,44 @@ def _is_number(s: str) -> bool: # pylint: disable=invalid-name return s.replace(".", "", 1).isdigit() +def _time_weighted_average( + fstates: list[tuple[float, State]], start: datetime.datetime, end: datetime.datetime +) -> float: + """Calculate a time weighted average. + + The average is calculated by, weighting the states by duration in seconds between + state changes. + Note: there's no interpolation of values between state changes. + """ + old_fstate: float | None = None + old_start_time: datetime.datetime | None = None + accumulated = 0.0 + + for fstate, state in fstates: + # The recorder will give us the last known state, which may be well + # before the requested start time for the statistics + start_time = start if state.last_updated < start else state.last_updated + if old_start_time is None: + # Adjust start time, if there was no last known state + start = start_time + else: + duration = start_time - old_start_time + # Accumulate the value, weighted by duration until next state change + assert old_fstate is not None + accumulated += old_fstate * duration.total_seconds() + + old_fstate = fstate + old_start_time = start_time + + if old_fstate is not None: + # Accumulate the value, weighted by duration until end of the period + assert old_start_time is not None + duration = end - old_start_time + accumulated += old_fstate * duration.total_seconds() + + return accumulated / (end - start).total_seconds() + + def compile_statistics( hass: HomeAssistant, start: datetime.datetime, end: datetime.datetime ) -> dict: @@ -91,10 +128,8 @@ def compile_statistics( if "min" in wanted_statistics: result[entity_id]["min"] = min(*itertools.islice(zip(*fstates), 1)) - # Note: The average calculation will be incorrect for unevenly spaced readings, - # this needs to be improved by weighting with time between measurements if "mean" in wanted_statistics: - result[entity_id]["mean"] = fmean(*itertools.islice(zip(*fstates), 1)) + result[entity_id]["mean"] = _time_weighted_average(fstates, start, end) if "sum" in wanted_statistics: last_reset = old_last_reset = None diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index 74be1075626..cffb67937fe 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -30,7 +30,7 @@ def test_compile_hourly_statistics(hass_recorder): { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(zero), - "mean": 15.0, + "mean": 14.915254237288135, "min": 10.0, "max": 20.0, "last_reset": None, diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 5d86ac520a5..37cc7387f25 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -31,9 +31,9 @@ def test_compile_hourly_statistics(hass_recorder): { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(zero), - "mean": 15.0, + "mean": 16.440677966101696, "min": 10.0, - "max": 20.0, + "max": 30.0, "last_reset": None, "state": None, "sum": None, @@ -243,9 +243,9 @@ def test_compile_hourly_statistics_unchanged(hass_recorder): { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(four), - "mean": 20.0, - "min": 20.0, - "max": 20.0, + "mean": 30.0, + "min": 30.0, + "max": 30.0, "last_reset": None, "state": None, "sum": None, @@ -271,7 +271,7 @@ def test_compile_hourly_statistics_partially_unavailable(hass_recorder): { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(zero), - "mean": 17.5, + "mean": 21.1864406779661, "min": 10.0, "max": 25.0, "last_reset": None, @@ -318,9 +318,9 @@ def record_states(hass): zero = dt_util.utcnow() one = zero + timedelta(minutes=1) - two = one + timedelta(minutes=15) - three = two + timedelta(minutes=30) - four = three + timedelta(minutes=15) + two = one + timedelta(minutes=10) + three = two + timedelta(minutes=40) + four = three + timedelta(minutes=10) states = {mp: [], sns1: [], sns2: [], sns3: []} with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=one): @@ -340,9 +340,9 @@ def record_states(hass): states[sns3].append(set_state(sns3, "15", attributes=sns3_attr)) with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=three): - states[sns1].append(set_state(sns1, "20", attributes=sns1_attr)) - states[sns2].append(set_state(sns2, "20", attributes=sns2_attr)) - states[sns3].append(set_state(sns3, "20", attributes=sns3_attr)) + states[sns1].append(set_state(sns1, "30", attributes=sns1_attr)) + states[sns2].append(set_state(sns2, "30", attributes=sns2_attr)) + states[sns3].append(set_state(sns3, "30", attributes=sns3_attr)) return zero, four, states From e86e70f327666d8380fed07b14d8dc2072595123 Mon Sep 17 00:00:00 2001 From: Greg Dowling Date: Thu, 27 May 2021 20:01:04 +0100 Subject: [PATCH 009/123] Bump pyroon to 0.0.37 (#51164) --- homeassistant/components/roon/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/roon/manifest.json b/homeassistant/components/roon/manifest.json index 09fcaad5f1f..354117e8fe4 100644 --- a/homeassistant/components/roon/manifest.json +++ b/homeassistant/components/roon/manifest.json @@ -3,7 +3,7 @@ "name": "RoonLabs music player", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/roon", - "requirements": ["roonapi==0.0.36"], + "requirements": ["roonapi==0.0.37"], "codeowners": ["@pavoni"], "iot_class": "local_push" } diff --git a/requirements_all.txt b/requirements_all.txt index cb3b05fed6e..6e288fcd333 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2015,7 +2015,7 @@ rokuecp==0.8.1 roombapy==1.6.3 # homeassistant.components.roon -roonapi==0.0.36 +roonapi==0.0.37 # homeassistant.components.rova rova==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 148936e126e..4e6f5a0ef1d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1091,7 +1091,7 @@ rokuecp==0.8.1 roombapy==1.6.3 # homeassistant.components.roon -roonapi==0.0.36 +roonapi==0.0.37 # homeassistant.components.rpi_power rpi-bad-power==0.1.0 From a6a18effee4228490aa82727d19234ccdee9595a Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 28 May 2021 05:07:58 -0500 Subject: [PATCH 010/123] Improve Sonos polling (#51170) * Improve Sonos polling Warn user if polling is being used Provide callback IP:port to help user fix networking Fix radio handling when polling (no event payload) Clarify dispatch target to reflect polling action * Lint * Revert method removal --- .../components/sonos/binary_sensor.py | 3 +-- homeassistant/components/sonos/const.py | 2 +- homeassistant/components/sonos/entity.py | 18 +++++++++++++++--- homeassistant/components/sonos/media_player.py | 7 +++---- homeassistant/components/sonos/sensor.py | 3 +-- homeassistant/components/sonos/speaker.py | 17 +++++++++++++---- homeassistant/components/sonos/switch.py | 2 +- 7 files changed, 35 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/sonos/binary_sensor.py b/homeassistant/components/sonos/binary_sensor.py index 21e0c077136..8583132521c 100644 --- a/homeassistant/components/sonos/binary_sensor.py +++ b/homeassistant/components/sonos/binary_sensor.py @@ -1,7 +1,6 @@ """Entity representing a Sonos power sensor.""" from __future__ import annotations -import datetime import logging from typing import Any @@ -50,7 +49,7 @@ class SonosPowerEntity(SonosEntity, BinarySensorEntity): """Return the entity's device class.""" return DEVICE_CLASS_BATTERY_CHARGING - async def async_update(self, now: datetime.datetime | None = None) -> None: + async def async_update(self) -> None: """Poll the device for the current state.""" await self.speaker.async_poll_battery() diff --git a/homeassistant/components/sonos/const.py b/homeassistant/components/sonos/const.py index c32f981e345..0a70844e6b5 100644 --- a/homeassistant/components/sonos/const.py +++ b/homeassistant/components/sonos/const.py @@ -136,7 +136,7 @@ SONOS_CREATE_ALARM = "sonos_create_alarm" SONOS_CREATE_BATTERY = "sonos_create_battery" SONOS_CREATE_MEDIA_PLAYER = "sonos_create_media_player" SONOS_ENTITY_CREATED = "sonos_entity_created" -SONOS_ENTITY_UPDATE = "sonos_entity_update" +SONOS_POLL_UPDATE = "sonos_poll_update" SONOS_GROUP_UPDATE = "sonos_group_update" SONOS_HOUSEHOLD_UPDATED = "sonos_household_updated" SONOS_ALARM_UPDATE = "sonos_alarm_update" diff --git a/homeassistant/components/sonos/entity.py b/homeassistant/components/sonos/entity.py index 8632357d618..8c47c69b2d7 100644 --- a/homeassistant/components/sonos/entity.py +++ b/homeassistant/components/sonos/entity.py @@ -1,6 +1,7 @@ """Entity representing a Sonos player.""" from __future__ import annotations +import datetime import logging from pysonos.core import SoCo @@ -15,8 +16,8 @@ from homeassistant.helpers.entity import DeviceInfo, Entity from .const import ( DOMAIN, SONOS_ENTITY_CREATED, - SONOS_ENTITY_UPDATE, SONOS_HOUSEHOLD_UPDATED, + SONOS_POLL_UPDATE, SONOS_STATE_UPDATED, ) from .speaker import SonosSpeaker @@ -38,8 +39,8 @@ class SonosEntity(Entity): self.async_on_remove( async_dispatcher_connect( self.hass, - f"{SONOS_ENTITY_UPDATE}-{self.soco.uid}", - self.async_update, # pylint: disable=no-member + f"{SONOS_POLL_UPDATE}-{self.soco.uid}", + self.async_poll, ) ) self.async_on_remove( @@ -60,6 +61,17 @@ class SonosEntity(Entity): self.hass, f"{SONOS_ENTITY_CREATED}-{self.soco.uid}", self.platform.domain ) + async def async_poll(self, now: datetime.datetime) -> None: + """Poll the entity if subscriptions fail.""" + if self.speaker.is_first_poll: + _LOGGER.warning( + "%s cannot reach [%s], falling back to polling, functionality may be limited", + self.speaker.zone_name, + self.speaker.subscription_address, + ) + self.speaker.is_first_poll = False + await self.async_update() # pylint: disable=no-member + @property def soco(self) -> SoCo: """Return the speaker SoCo instance.""" diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 9ca4b21425b..e75400b06ab 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -292,13 +292,12 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): return STATE_PLAYING return STATE_IDLE - async def async_update(self, now: datetime.datetime | None = None) -> None: + async def async_update(self) -> None: """Retrieve latest state.""" - await self.hass.async_add_executor_job(self._update, now) + await self.hass.async_add_executor_job(self._update) - def _update(self, now: datetime.datetime | None = None) -> None: + def _update(self) -> None: """Retrieve latest state.""" - _LOGGER.debug("Polling speaker %s", self.speaker.zone_name) try: self.speaker.update_groups() self.speaker.update_volume() diff --git a/homeassistant/components/sonos/sensor.py b/homeassistant/components/sonos/sensor.py index d9ff19af581..9e5277819a7 100644 --- a/homeassistant/components/sonos/sensor.py +++ b/homeassistant/components/sonos/sensor.py @@ -1,7 +1,6 @@ """Entity representing a Sonos battery level.""" from __future__ import annotations -import datetime import logging from homeassistant.components.sensor import SensorEntity @@ -50,7 +49,7 @@ class SonosBatteryEntity(SonosEntity, SensorEntity): """Get the unit of measurement.""" return PERCENTAGE - async def async_update(self, now: datetime.datetime | None = None) -> None: + async def async_update(self) -> None: """Poll the device for the current state.""" await self.speaker.async_poll_battery() diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index c1f1dbb9104..b4b403e0445 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -44,8 +44,8 @@ from .const import ( SONOS_CREATE_BATTERY, SONOS_CREATE_MEDIA_PLAYER, SONOS_ENTITY_CREATED, - SONOS_ENTITY_UPDATE, SONOS_GROUP_UPDATE, + SONOS_POLL_UPDATE, SONOS_SEEN, SONOS_STATE_PLAYING, SONOS_STATE_TRANSITIONING, @@ -138,6 +138,7 @@ class SonosSpeaker: self.household_id: str = soco.household_id self.media = SonosMedia(soco) + self.is_first_poll: bool = True self._is_ready: bool = False self._subscriptions: list[SubscriptionBase] = [] self._resubscription_lock: asyncio.Lock | None = None @@ -322,7 +323,7 @@ class SonosSpeaker: partial( async_dispatcher_send, self.hass, - f"{SONOS_ENTITY_UPDATE}-{self.soco.uid}", + f"{SONOS_POLL_UPDATE}-{self.soco.uid}", ), SCAN_INTERVAL, ) @@ -418,7 +419,7 @@ class SonosSpeaker: ): async_dispatcher_send(self.hass, SONOS_CREATE_ALARM, self, new_alarms) - async_dispatcher_send(self.hass, SONOS_ALARM_UPDATE, self) + async_dispatcher_send(self.hass, SONOS_ALARM_UPDATE) self.async_write_entity_states() @@ -877,7 +878,7 @@ class SonosSpeaker: if not self.media.artist: try: self.media.artist = variables["current_track_meta_data"].creator - except (KeyError, AttributeError): + except (TypeError, KeyError, AttributeError): pass # Radios without tagging can have part of the radio URI as title. @@ -950,3 +951,11 @@ class SonosSpeaker: elif update_media_position: self.media.position = current_position self.media.position_updated_at = dt_util.utcnow() + + @property + def subscription_address(self) -> str | None: + """Return the current subscription callback address if any.""" + if self._subscriptions: + addr, port = self._subscriptions[0].event_listener.address + return ":".join([addr, str(port)]) + return None diff --git a/homeassistant/components/sonos/switch.py b/homeassistant/components/sonos/switch.py index 967bc21da59..8ea30bfe7a4 100644 --- a/homeassistant/components/sonos/switch.py +++ b/homeassistant/components/sonos/switch.py @@ -106,7 +106,7 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): return False - async def async_update(self, now: datetime.datetime | None = None) -> None: + async def async_update(self) -> None: """Poll the device for the current state.""" if await self.async_check_if_available(): await self.hass.async_add_executor_job(self.update_alarm) From 6388203f73ef8d6da55677e3e9d841af5e170b92 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 28 May 2021 08:00:11 +0200 Subject: [PATCH 011/123] Fix Netatmo data class update (#51177) --- homeassistant/components/netatmo/data_handler.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/netatmo/data_handler.py b/homeassistant/components/netatmo/data_handler.py index 83215bd3af5..11415417ee1 100644 --- a/homeassistant/components/netatmo/data_handler.py +++ b/homeassistant/components/netatmo/data_handler.py @@ -99,7 +99,8 @@ class NetatmoDataHandler: time() + data_class["interval"] ) - await self.async_fetch_data(data_class["name"]) + if data_class_name := data_class["name"]: + await self.async_fetch_data(data_class_name) self._queue.rotate(BATCH_SIZE) From 3e57b6178de395bd6ad78482ae7b8575a797917f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 28 May 2021 12:02:35 +0200 Subject: [PATCH 012/123] Use get with default for consider home (#51194) --- homeassistant/components/fritz/common.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index ad906e8956b..ec7e402f760 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -16,7 +16,10 @@ from fritzconnection.core.exceptions import ( from fritzconnection.lib.fritzhosts import FritzHosts from fritzconnection.lib.fritzstatus import FritzStatus -from homeassistant.components.device_tracker.const import CONF_CONSIDER_HOME +from homeassistant.components.device_tracker.const import ( + CONF_CONSIDER_HOME, + DEFAULT_CONSIDER_HOME, +) from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC @@ -143,7 +146,9 @@ class FritzBoxTools: """Scan for new devices and return a list of found device ids.""" _LOGGER.debug("Checking devices for FRITZ!Box router %s", self.host) - consider_home = self._options[CONF_CONSIDER_HOME] + consider_home = self._options.get( + CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME.total_seconds() + ) new_device = False for known_host in self._update_info(): From fe84d060d69b3fcf99f0d2c37ac9d388c974db30 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 28 May 2021 13:36:22 +0200 Subject: [PATCH 013/123] Fix Netatmo sensor initialization (#51195) --- homeassistant/components/netatmo/__init__.py | 7 +++++++ homeassistant/components/netatmo/data_handler.py | 10 ++++++---- .../components/netatmo/netatmo_entity_base.py | 4 ---- homeassistant/components/netatmo/sensor.py | 14 +++++--------- 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index 354ce2cf942..f6805099211 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -204,9 +204,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.services.async_register(DOMAIN, "register_webhook", register_webhook) hass.services.async_register(DOMAIN, "unregister_webhook", unregister_webhook) + entry.add_update_listener(async_config_entry_updated) + return True +async def async_config_entry_updated(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle signals of config entry being updated.""" + async_dispatcher_send(hass, f"signal-{DOMAIN}-public-update-{entry.entry_id}") + + async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" if CONF_WEBHOOK_ID in entry.data: diff --git a/homeassistant/components/netatmo/data_handler.py b/homeassistant/components/netatmo/data_handler.py index 11415417ee1..12376e5ac78 100644 --- a/homeassistant/components/netatmo/data_handler.py +++ b/homeassistant/components/netatmo/data_handler.py @@ -95,12 +95,14 @@ class NetatmoDataHandler: for data_class in islice(self._queue, 0, BATCH_SIZE): if data_class[NEXT_SCAN] > time(): continue - self.data_classes[data_class["name"]][NEXT_SCAN] = ( - time() + data_class["interval"] - ) if data_class_name := data_class["name"]: - await self.async_fetch_data(data_class_name) + self.data_classes[data_class_name][NEXT_SCAN] = ( + time() + data_class["interval"] + ) + + if self.data_classes[data_class_name]["subscriptions"]: + await self.async_fetch_data(data_class_name) self._queue.rotate(BATCH_SIZE) diff --git a/homeassistant/components/netatmo/netatmo_entity_base.py b/homeassistant/components/netatmo/netatmo_entity_base.py index 1fcd4a121d8..5d43a46e89b 100644 --- a/homeassistant/components/netatmo/netatmo_entity_base.py +++ b/homeassistant/components/netatmo/netatmo_entity_base.py @@ -1,16 +1,12 @@ """Base class for Netatmo entities.""" from __future__ import annotations -import logging - from homeassistant.core import CALLBACK_TYPE, callback from homeassistant.helpers.entity import Entity from .const import DATA_DEVICE_IDS, DOMAIN, MANUFACTURER, MODELS, SIGNAL_NAME from .data_handler import PUBLICDATA_DATA_CLASS_NAME, NetatmoDataHandler -_LOGGER = logging.getLogger(__name__) - class NetatmoBase(Entity): """Netatmo entity base class.""" diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index e56847386a3..eeaab52a21a 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -2,7 +2,6 @@ import logging from homeassistant.components.sensor import SensorEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_LATITUDE, ATTR_LONGITUDE, @@ -21,7 +20,7 @@ from homeassistant.const import ( SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import callback from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.device_registry import async_entries_for_config_entry from homeassistant.helpers.dispatcher import ( @@ -131,6 +130,7 @@ PUBLIC = "public" async def async_setup_entry(hass, entry, async_add_entities): """Set up the Netatmo weather and homecoach platform.""" data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] + platform_not_ready = False async def find_entities(data_class_name): """Find all entities.""" @@ -184,7 +184,7 @@ async def async_setup_entry(hass, entry, async_add_entities): data_class = data_handler.data.get(data_class_name) if not data_class or not data_class.raw_data: - raise PlatformNotReady + platform_not_ready = True async_add_entities(await find_entities(data_class_name), True) @@ -241,14 +241,10 @@ async def async_setup_entry(hass, entry, async_add_entities): hass, f"signal-{DOMAIN}-public-update-{entry.entry_id}", add_public_entities ) - entry.add_update_listener(async_config_entry_updated) - await add_public_entities(False) - -async def async_config_entry_updated(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Handle signals of config entry being updated.""" - async_dispatcher_send(hass, f"signal-{DOMAIN}-public-update-{entry.entry_id}") + if platform_not_ready: + raise PlatformNotReady class NetatmoSensor(NetatmoBase, SensorEntity): From ae914be44f8cb1992518db12f94ffaefa6bfab6d Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Fri, 28 May 2021 13:32:26 +0200 Subject: [PATCH 014/123] Only run philips_js notify service while TV is turned on (#51196) Co-authored-by: Martin Hjelmare Co-authored-by: Franck Nijhof --- .../components/philips_js/__init__.py | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/philips_js/__init__.py b/homeassistant/components/philips_js/__init__.py index f21337f512e..a2e5dd4cbc2 100644 --- a/homeassistant/components/philips_js/__init__.py +++ b/homeassistant/components/philips_js/__init__.py @@ -116,8 +116,21 @@ class PhilipsTVDataUpdateCoordinator(DataUpdateCoordinator[None]): ), ) + @property + def _notify_wanted(self): + """Return if the notify feature should be active. + + We only run it when TV is considered fully on. When powerstate is in standby, the TV + will go in low power states and seemingly break the http server in odd ways. + """ + return ( + self.api.on + and self.api.powerstate == "On" + and self.api.notify_change_supported + ) + async def _notify_task(self): - while self.api.on and self.api.notify_change_supported: + while self._notify_wanted: res = await self.api.notifyChange(130) if res: self.async_set_updated_data(None) @@ -133,11 +146,10 @@ class PhilipsTVDataUpdateCoordinator(DataUpdateCoordinator[None]): @callback def _async_notify_schedule(self): - if ( - (self._notify_future is None or self._notify_future.done()) - and self.api.on - and self.api.notify_change_supported - ): + if self._notify_future and not self._notify_future.done(): + return + + if self._notify_wanted: self._notify_future = asyncio.create_task(self._notify_task()) @callback From de575fdb7be15af5d35cb3fac5c02cd969c96fcc Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 28 May 2021 13:22:58 +0200 Subject: [PATCH 015/123] Update base image to 2021.05.0 (#51198) --- build.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.json b/build.json index de5f895af2a..c3e5d83dc78 100644 --- a/build.json +++ b/build.json @@ -2,11 +2,11 @@ "image": "homeassistant/{arch}-homeassistant", "shadow_repository": "ghcr.io/home-assistant", "build_from": { - "aarch64": "ghcr.io/home-assistant/aarch64-homeassistant-base:2021.04.3", - "armhf": "ghcr.io/home-assistant/armhf-homeassistant-base:2021.04.3", - "armv7": "ghcr.io/home-assistant/armv7-homeassistant-base:2021.04.3", - "amd64": "ghcr.io/home-assistant/amd64-homeassistant-base:2021.04.3", - "i386": "ghcr.io/home-assistant/i386-homeassistant-base:2021.04.3" + "aarch64": "ghcr.io/home-assistant/aarch64-homeassistant-base:2021.05.0", + "armhf": "ghcr.io/home-assistant/armhf-homeassistant-base:2021.05.0", + "armv7": "ghcr.io/home-assistant/armv7-homeassistant-base:2021.05.0", + "amd64": "ghcr.io/home-assistant/amd64-homeassistant-base:2021.05.0", + "i386": "ghcr.io/home-assistant/i386-homeassistant-base:2021.05.0" }, "labels": { "io.hass.type": "core", From 0c9c113528b4df2e0810549665c29ce912f2bc21 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 28 May 2021 14:38:01 +0200 Subject: [PATCH 016/123] Update frontend to 20210528.0 (#51199) --- 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 57380b53be8..9ee97c851e9 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210526.0" + "home-assistant-frontend==20210528.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b61e0d24785..7e6be2f0016 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ defusedxml==0.7.1 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.43.0 -home-assistant-frontend==20210526.0 +home-assistant-frontend==20210528.0 httpx==0.18.0 ifaddr==0.1.7 jinja2>=3.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index 6e288fcd333..18d6df72c70 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -765,7 +765,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210526.0 +home-assistant-frontend==20210528.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4e6f5a0ef1d..4aa6218067e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -429,7 +429,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210526.0 +home-assistant-frontend==20210528.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 0de8604631d15d8f1604842db66555c89077c0f6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 28 May 2021 14:51:21 +0200 Subject: [PATCH 017/123] Bumped version to 2021.6.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 581c08c5f94..a38655119bc 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 6 -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, 8, 0) From 215869b3df92810e4e5a1a0abc5713f0c15bee38 Mon Sep 17 00:00:00 2001 From: Vilppu Vuorinen Date: Fri, 28 May 2021 17:48:30 +0300 Subject: [PATCH 018/123] Update to pymelcloud 2.5.3 (#51043) Previous version of pymelcloud performs requests that are not permitted for guest users. Bypassing these requests results only in less detailed device info. --- homeassistant/components/melcloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/melcloud/manifest.json b/homeassistant/components/melcloud/manifest.json index 641a4df583e..4aff46a22b6 100644 --- a/homeassistant/components/melcloud/manifest.json +++ b/homeassistant/components/melcloud/manifest.json @@ -3,7 +3,7 @@ "name": "MELCloud", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/melcloud", - "requirements": ["pymelcloud==2.5.2"], + "requirements": ["pymelcloud==2.5.3"], "codeowners": ["@vilppuvuorinen"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 18d6df72c70..55d07ae773d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1557,7 +1557,7 @@ pymazda==0.1.6 pymediaroom==0.6.4.1 # homeassistant.components.melcloud -pymelcloud==2.5.2 +pymelcloud==2.5.3 # homeassistant.components.meteoclimatic pymeteoclimatic==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4aa6218067e..9f95763e54f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -868,7 +868,7 @@ pymata-express==1.19 pymazda==0.1.6 # homeassistant.components.melcloud -pymelcloud==2.5.2 +pymelcloud==2.5.3 # homeassistant.components.meteoclimatic pymeteoclimatic==0.0.6 From e980365a9c39080ccbf23b9ac820125c86a6e436 Mon Sep 17 00:00:00 2001 From: Aaron David Schneider Date: Thu, 27 May 2021 19:56:59 +0200 Subject: [PATCH 019/123] Add tests for sonos switch platform (#51142) * add tests * refactor async_added_to_hass * fix tests and race condition * use async_get * typo --- homeassistant/components/sonos/speaker.py | 2 - homeassistant/components/sonos/switch.py | 18 ++++++--- tests/components/sonos/conftest.py | 48 +++++++++++++++++++---- tests/components/sonos/test_switch.py | 35 +++++++++++++++-- 4 files changed, 84 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index b4b403e0445..81c95f6e33f 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -421,8 +421,6 @@ class SonosSpeaker: async_dispatcher_send(self.hass, SONOS_ALARM_UPDATE) - self.async_write_entity_states() - async def async_update_battery_info(self, battery_dict: dict[str, Any]) -> None: """Update battery info using the decoded SonosEvent.""" self._last_battery_event = dt_util.utcnow() diff --git a/homeassistant/components/sonos/switch.py b/homeassistant/components/sonos/switch.py index 8ea30bfe7a4..83449c846c6 100644 --- a/homeassistant/components/sonos/switch.py +++ b/homeassistant/components/sonos/switch.py @@ -43,11 +43,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entity = SonosAlarmEntity(alarm_id, speaker) async_add_entities([entity]) configured_alarms.add(alarm_id) - config_entry.async_on_unload( - async_dispatcher_connect( - hass, SONOS_ALARM_UPDATE, entity.async_update - ) - ) config_entry.async_on_unload( async_dispatcher_connect(hass, SONOS_CREATE_ALARM, _async_create_entity) @@ -64,9 +59,20 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): self._alarm_id = alarm_id self.entity_id = ENTITY_ID_FORMAT.format(f"sonos_alarm_{self.alarm_id}") + async def async_added_to_hass(self) -> None: + """Handle switch setup when added to hass.""" + await super().async_added_to_hass() + self.async_on_remove( + async_dispatcher_connect( + self.hass, + SONOS_ALARM_UPDATE, + self.async_update, + ) + ) + @property def alarm(self): - """Return the ID of the alarm.""" + """Return the alarm instance.""" return self.hass.data[DATA_SONOS].alarms[self.alarm_id] @property diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index 2feb2b54896..aa14dcaa5cf 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -30,7 +30,7 @@ def config_entry_fixture(): @pytest.fixture(name="soco") def soco_fixture( - music_library, speaker_info, battery_info, dummy_soco_service, alarmClock + music_library, speaker_info, battery_info, dummy_soco_service, alarm_clock ): """Create a mock pysonos SoCo fixture.""" with patch("pysonos.SoCo", autospec=True) as mock, patch( @@ -46,7 +46,7 @@ def soco_fixture( mock_soco.zoneGroupTopology = dummy_soco_service mock_soco.contentDirectory = dummy_soco_service mock_soco.deviceProperties = dummy_soco_service - mock_soco.alarmClock = alarmClock + mock_soco.alarmClock = alarm_clock mock_soco.mute = False mock_soco.night_mode = True mock_soco.dialog_mode = True @@ -90,12 +90,28 @@ def music_library_fixture(): return music_library -@pytest.fixture(name="alarmClock") -def alarmClock_fixture(): +@pytest.fixture(name="alarm_clock") +def alarm_clock_fixture(): """Create alarmClock fixture.""" - alarmClock = Mock() - alarmClock.subscribe = AsyncMock() - alarmClock.ListAlarms.return_value = { + alarm_clock = Mock() + alarm_clock.subscribe = AsyncMock() + alarm_clock.ListAlarms.return_value = { + "CurrentAlarmList": "" + '' + " " + } + return alarm_clock + + +@pytest.fixture(name="alarm_clock_extended") +def alarm_clock_fixture_extended(): + """Create alarmClock fixture.""" + alarm_clock = Mock() + alarm_clock.subscribe = AsyncMock() + alarm_clock.ListAlarms.return_value = { "CurrentAlarmList": "" '' " " } - return alarmClock + return alarm_clock @pytest.fixture(name="speaker_info") @@ -141,3 +157,19 @@ def battery_event_fixture(soco): "more_info": "BattChg:NOT_CHARGING,RawBattPct:100,BattPct:100,BattTmp:25", } return SonosMockEvent(soco, variables) + + +@pytest.fixture(name="alarm_event") +def alarm_event_fixture(soco): + """Create alarm_event fixture.""" + variables = { + "time_zone": "ffc40a000503000003000502ffc4", + "time_server": "0.sonostime.pool.ntp.org,1.sonostime.pool.ntp.org,2.sonostime.pool.ntp.org,3.sonostime.pool.ntp.org", + "time_generation": "20000001", + "alarm_list_version": "RINCON_test", + "time_format": "INV", + "date_format": "INV", + "daily_index_refresh_time": None, + } + + return SonosMockEvent(soco, variables) diff --git a/tests/components/sonos/test_switch.py b/tests/components/sonos/test_switch.py index c33c472ee27..d4448d22b32 100644 --- a/tests/components/sonos/test_switch.py +++ b/tests/components/sonos/test_switch.py @@ -9,17 +9,18 @@ from homeassistant.components.sonos.switch import ( ATTR_VOLUME, ) from homeassistant.const import ATTR_TIME, STATE_ON +from homeassistant.helpers.entity_registry import async_get as async_get_entity_registry from homeassistant.setup import async_setup_component async def setup_platform(hass, config_entry, config): - """Set up the media player platform for testing.""" + """Set up the switch platform for testing.""" config_entry.add_to_hass(hass) assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() -async def test_entity_registry(hass, config_entry, config, soco): +async def test_entity_registry(hass, config_entry, config): """Test sonos device with alarm registered in the device registry.""" await setup_platform(hass, config_entry, config) @@ -29,7 +30,7 @@ async def test_entity_registry(hass, config_entry, config, soco): assert "switch.sonos_alarm_14" in entity_registry.entities -async def test_alarm_attributes(hass, config_entry, config, soco): +async def test_alarm_attributes(hass, config_entry, config): """Test for correct sonos alarm state.""" await setup_platform(hass, config_entry, config) @@ -45,3 +46,31 @@ async def test_alarm_attributes(hass, config_entry, config, soco): assert alarm_state.attributes.get(ATTR_VOLUME) == 0.25 assert alarm_state.attributes.get(ATTR_PLAY_MODE) == "SHUFFLE_NOREPEAT" assert not alarm_state.attributes.get(ATTR_INCLUDE_LINKED_ZONES) + + +async def test_alarm_create_delete( + hass, config_entry, config, soco, alarm_clock, alarm_clock_extended, alarm_event +): + """Test for correct creation and deletion of alarms during runtime.""" + soco.alarmClock = alarm_clock_extended + + await setup_platform(hass, config_entry, config) + + subscription = alarm_clock_extended.subscribe.return_value + sub_callback = subscription.callback + + sub_callback(event=alarm_event) + await hass.async_block_till_done() + + entity_registry = async_get_entity_registry(hass) + + assert "switch.sonos_alarm_14" in entity_registry.entities + assert "switch.sonos_alarm_15" in entity_registry.entities + + alarm_clock_extended.ListAlarms.return_value = alarm_clock.ListAlarms.return_value + + sub_callback(event=alarm_event) + await hass.async_block_till_done() + + assert "switch.sonos_alarm_14" in entity_registry.entities + assert "switch.sonos_alarm_15" not in entity_registry.entities From a0696fe9230bba9bd57c6f765ef1ab043c075ab4 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 28 May 2021 21:32:50 -0500 Subject: [PATCH 020/123] Centralize Sonos subscription logic (#51172) * Centralize Sonos subscription logic * Clean up mocked Sonos Service instances, use subscription callback * Use existing mocked attributes * Use event dispatcher dict, move methods together, make update_alarms sync * Create dispatcher dict once --- homeassistant/components/sonos/favorites.py | 4 +- homeassistant/components/sonos/speaker.py | 158 ++++++++++---------- tests/components/sonos/conftest.py | 47 +++--- tests/components/sonos/test_sensor.py | 8 +- 4 files changed, 111 insertions(+), 106 deletions(-) diff --git a/homeassistant/components/sonos/favorites.py b/homeassistant/components/sonos/favorites.py index 19dcb5184e5..2f5cab23be2 100644 --- a/homeassistant/components/sonos/favorites.py +++ b/homeassistant/components/sonos/favorites.py @@ -41,7 +41,9 @@ class SonosFavorites: Updated favorites are not always immediately available. """ - event_id = event.variables["favorites_update_id"] + if not (event_id := event.variables.get("favorites_update_id")): + return + if not self._event_version: self._event_version = event_id return diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 81c95f6e33f..079b916a4bc 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -61,6 +61,14 @@ EVENT_CHARGING = { "CHARGING": True, "NOT_CHARGING": False, } +SUBSCRIPTION_SERVICES = [ + "alarmClock", + "avTransport", + "contentDirectory", + "deviceProperties", + "renderingControl", + "zoneGroupTopology", +] UNAVAILABLE_VALUES = {"", "NOT_IMPLEMENTED", None} @@ -140,8 +148,12 @@ class SonosSpeaker: self.is_first_poll: bool = True self._is_ready: bool = False + + # Subscriptions and events self._subscriptions: list[SubscriptionBase] = [] self._resubscription_lock: asyncio.Lock | None = None + self._event_dispatchers: dict[str, Callable] = {} + self._poll_timer: Callable | None = None self._seen_timer: Callable | None = None self._platforms_ready: set[str] = set() @@ -209,6 +221,15 @@ class SonosSpeaker: else: self._platforms_ready.add(SWITCH_DOMAIN) + self._event_dispatchers = { + "AlarmClock": self.async_dispatch_alarms, + "AVTransport": self.async_dispatch_media_update, + "ContentDirectory": self.favorites.async_delayed_update, + "DeviceProperties": self.async_dispatch_device_properties, + "RenderingControl": self.async_update_volume, + "ZoneGroupTopology": self.async_update_groups, + } + dispatcher_send(self.hass, SONOS_CREATE_MEDIA_PLAYER, self) async def async_handle_new_entity(self, entity_type: str) -> None: @@ -238,6 +259,9 @@ class SonosSpeaker: """Return whether this speaker is available.""" return self._seen_timer is not None + # + # Subscription handling and event dispatchers + # async def async_subscribe(self) -> bool: """Initiate event subscriptions.""" _LOGGER.debug("Creating subscriptions for %s", self.zone_name) @@ -250,18 +274,11 @@ class SonosSpeaker: f"when existing subscriptions exist: {self._subscriptions}" ) - await asyncio.gather( - self._subscribe(self.soco.avTransport, self.async_update_media), - self._subscribe(self.soco.renderingControl, self.async_update_volume), - self._subscribe(self.soco.contentDirectory, self.async_update_content), - self._subscribe( - self.soco.zoneGroupTopology, self.async_dispatch_groups - ), - self._subscribe( - self.soco.deviceProperties, self.async_dispatch_properties - ), - self._subscribe(self.soco.alarmClock, self.async_dispatch_alarms), - ) + subscriptions = [ + self._subscribe(getattr(self.soco, service), self.async_dispatch_event) + for service in SUBSCRIPTION_SERVICES + ] + await asyncio.gather(*subscriptions) return True except SoCoException as ex: _LOGGER.warning("Could not connect %s: %s", self.zone_name, ex) @@ -279,26 +296,58 @@ class SonosSpeaker: self._subscriptions.append(subscription) @callback - def async_dispatch_properties(self, event: SonosEvent | None = None) -> None: - """Update properties from event.""" - self.hass.async_create_task(self.async_update_device_properties(event)) - - @callback - def async_dispatch_alarms(self, event: SonosEvent | None = None) -> None: - """Update alarms from event.""" - self.hass.async_create_task(self.async_update_alarms(event)) - - @callback - def async_dispatch_groups(self, event: SonosEvent | None = None) -> None: - """Update groups from event.""" - if event and self._poll_timer: + def async_dispatch_event(self, event: SonosEvent) -> None: + """Handle callback event and route as needed.""" + if self._poll_timer: _LOGGER.debug( "Received event, cancelling poll timer for %s", self.zone_name ) self._poll_timer() self._poll_timer = None - self.async_update_groups(event) + dispatcher = self._event_dispatchers[event.service.service_type] + dispatcher(event) + + @callback + def async_dispatch_alarms(self, event: SonosEvent) -> None: + """Create a task to update alarms from an event.""" + self.hass.async_add_executor_job(self.update_alarms) + + @callback + def async_dispatch_device_properties(self, event: SonosEvent) -> None: + """Update device properties from an event.""" + self.hass.async_create_task(self.async_update_device_properties(event)) + + async def async_update_device_properties(self, event: SonosEvent) -> None: + """Update device properties from an event.""" + if (more_info := event.variables.get("more_info")) is not None: + battery_dict = dict(x.split(":") for x in more_info.split(",")) + await self.async_update_battery_info(battery_dict) + self.async_write_entity_states() + + @callback + def async_dispatch_media_update(self, event: SonosEvent) -> None: + """Update information about currently playing media from an event.""" + self.hass.async_add_executor_job(self.update_media, event) + + @callback + def async_update_volume(self, event: SonosEvent) -> None: + """Update information about currently volume settings.""" + variables = event.variables + + if "volume" in variables: + self.volume = int(variables["volume"]["Master"]) + + if "mute" in variables: + self.muted = variables["mute"]["Master"] == "1" + + if "night_mode" in variables: + self.night_mode = variables["night_mode"] == "1" + + if "dialog_level" in variables: + self.dialog_mode = variables["dialog_level"] == "1" + + self.async_write_entity_states() async def async_seen(self, soco: SoCo | None = None) -> None: """Record that this speaker was seen right now.""" @@ -376,17 +425,6 @@ class SonosSpeaker: self._subscriptions = [] - async def async_update_device_properties(self, event: SonosEvent = None) -> None: - """Update device properties using the provided SonosEvent.""" - if event is None: - return - - if (more_info := event.variables.get("more_info")) is not None: - battery_dict = dict(x.split(":") for x in more_info.split(",")) - await self.async_update_battery_info(battery_dict) - - self.async_write_entity_states() - def update_alarms_for_speaker(self) -> set[str]: """Update current alarm instances. @@ -409,17 +447,11 @@ class SonosSpeaker: return new_alarms - async def async_update_alarms(self, event: SonosEvent | None = None) -> None: - """Update device properties using the provided SonosEvent.""" - if event is None: - return - - if new_alarms := await self.hass.async_add_executor_job( - self.update_alarms_for_speaker - ): - async_dispatcher_send(self.hass, SONOS_CREATE_ALARM, self, new_alarms) - - async_dispatcher_send(self.hass, SONOS_ALARM_UPDATE) + def update_alarms(self) -> None: + """Update alarms from an event.""" + if new_alarms := self.update_alarms_for_speaker(): + dispatcher_send(self.hass, SONOS_CREATE_ALARM, self, new_alarms) + dispatcher_send(self.hass, SONOS_ALARM_UPDATE) async def async_update_battery_info(self, battery_dict: dict[str, Any]) -> None: """Update battery info using the decoded SonosEvent.""" @@ -759,12 +791,6 @@ class SonosSpeaker: """Return the SonosFavorites instance for this household.""" return self.hass.data[DATA_SONOS].favorites[self.household_id] - @callback - def async_update_content(self, event: SonosEvent | None = None) -> None: - """Update information about available content.""" - if event and "favorites_update_id" in event.variables: - self.favorites.async_delayed_update(event) - def update_volume(self) -> None: """Update information about current volume settings.""" self.volume = self.soco.volume @@ -772,30 +798,6 @@ class SonosSpeaker: self.night_mode = self.soco.night_mode self.dialog_mode = self.soco.dialog_mode - @callback - def async_update_volume(self, event: SonosEvent) -> None: - """Update information about currently volume settings.""" - variables = event.variables - - if "volume" in variables: - self.volume = int(variables["volume"]["Master"]) - - if "mute" in variables: - self.muted = variables["mute"]["Master"] == "1" - - if "night_mode" in variables: - self.night_mode = variables["night_mode"] == "1" - - if "dialog_level" in variables: - self.dialog_mode = variables["dialog_level"] == "1" - - self.async_write_entity_states() - - @callback - def async_update_media(self, event: SonosEvent | None = None) -> None: - """Update information about currently playing media.""" - self.hass.async_add_executor_job(self.update_media, event) - def update_media(self, event: SonosEvent | None = None) -> None: """Update information about currently playing media.""" variables = event and event.variables diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index aa14dcaa5cf..fc5ef84c2d6 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -10,15 +10,24 @@ from homeassistant.const import CONF_HOSTS from tests.common import MockConfigEntry +class SonosMockService: + """Mock a Sonos Service used in callbacks.""" + + def __init__(self, service_type): + """Initialize the instance.""" + self.service_type = service_type + self.subscribe = AsyncMock() + + class SonosMockEvent: """Mock a sonos Event used in callbacks.""" - def __init__(self, soco, variables): + def __init__(self, soco, service, variables): """Initialize the instance.""" self.sid = f"{soco.uid}_sub0000000001" self.seq = "0" self.timestamp = 1621000000.0 - self.service = dummy_soco_service_fixture + self.service = service self.variables = variables @@ -29,9 +38,7 @@ def config_entry_fixture(): @pytest.fixture(name="soco") -def soco_fixture( - music_library, speaker_info, battery_info, dummy_soco_service, alarm_clock -): +def soco_fixture(music_library, speaker_info, battery_info, alarm_clock): """Create a mock pysonos SoCo fixture.""" with patch("pysonos.SoCo", autospec=True) as mock, patch( "socket.gethostbyname", return_value="192.168.42.2" @@ -41,11 +48,11 @@ def soco_fixture( mock_soco.play_mode = "NORMAL" mock_soco.music_library = music_library mock_soco.get_speaker_info.return_value = speaker_info - mock_soco.avTransport = dummy_soco_service - mock_soco.renderingControl = dummy_soco_service - mock_soco.zoneGroupTopology = dummy_soco_service - mock_soco.contentDirectory = dummy_soco_service - mock_soco.deviceProperties = dummy_soco_service + mock_soco.avTransport = SonosMockService("AVTransport") + mock_soco.renderingControl = SonosMockService("RenderingControl") + mock_soco.zoneGroupTopology = SonosMockService("ZoneGroupTopology") + mock_soco.contentDirectory = SonosMockService("ContentDirectory") + mock_soco.deviceProperties = SonosMockService("DeviceProperties") mock_soco.alarmClock = alarm_clock mock_soco.mute = False mock_soco.night_mode = True @@ -74,14 +81,6 @@ def config_fixture(): return {DOMAIN: {MP_DOMAIN: {CONF_HOSTS: ["192.168.42.1"]}}} -@pytest.fixture(name="dummy_soco_service") -def dummy_soco_service_fixture(): - """Create dummy_soco_service fixture.""" - service = Mock() - service.subscribe = AsyncMock() - return service - - @pytest.fixture(name="music_library") def music_library_fixture(): """Create music_library fixture.""" @@ -93,8 +92,8 @@ def music_library_fixture(): @pytest.fixture(name="alarm_clock") def alarm_clock_fixture(): """Create alarmClock fixture.""" - alarm_clock = Mock() - alarm_clock.subscribe = AsyncMock() + alarm_clock = SonosMockService("AlarmClock") + alarm_clock.ListAlarms = Mock() alarm_clock.ListAlarms.return_value = { "CurrentAlarmList": "" '" ' Date: Fri, 28 May 2021 09:06:17 -0500 Subject: [PATCH 021/123] Fix samsungtv yaml import without configured name (#51204) --- .../components/samsungtv/config_flow.py | 2 +- .../components/samsungtv/test_config_flow.py | 23 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index b45f6c5670b..46800e1653b 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -173,7 +173,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) except socket.gaierror as err: raise data_entry_flow.AbortFlow(RESULT_UNKNOWN_HOST) from err - self._name = user_input[CONF_NAME] + self._name = user_input.get(CONF_NAME, self._host) self._title = self._name async def async_step_user(self, user_input=None): diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index 04dffd31801..5b85ecf7048 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -54,6 +54,9 @@ MOCK_IMPORT_DATA = { CONF_NAME: "fake", CONF_PORT: 55000, } +MOCK_IMPORT_DATA_WITHOUT_NAME = { + CONF_HOST: "fake_host", +} MOCK_IMPORT_WSDATA = { CONF_HOST: "fake_host", CONF_NAME: "fake", @@ -509,6 +512,26 @@ async def test_import_legacy(hass: HomeAssistant): assert result["result"].unique_id is None +async def test_import_legacy_without_name(hass: HomeAssistant): + """Test importing from yaml without a name.""" + with patch( + "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", + return_value="fake_host", + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=MOCK_IMPORT_DATA_WITHOUT_NAME, + ) + await hass.async_block_till_done() + assert result["type"] == "create_entry" + assert result["title"] == "fake_host" + assert result["data"][CONF_METHOD] == METHOD_LEGACY + assert result["data"][CONF_HOST] == "fake_host" + assert result["data"][CONF_MANUFACTURER] == "Samsung" + assert result["result"].unique_id is None + + async def test_import_websocket(hass: HomeAssistant): """Test importing from yaml with hostname.""" with patch( From f32309273b3eb495aa08caccda3b1aea77361866 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 28 May 2021 23:28:07 -0500 Subject: [PATCH 022/123] Fix use of async in Sonos switch (#51210) * Fix use of async in Sonos switch * Simplify * Convert to callback --- homeassistant/components/sonos/switch.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/sonos/switch.py b/homeassistant/components/sonos/switch.py index 83449c846c6..4783795a343 100644 --- a/homeassistant/components/sonos/switch.py +++ b/homeassistant/components/sonos/switch.py @@ -8,6 +8,7 @@ from pysonos.exceptions import SoCoUPnPException from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchEntity from homeassistant.const import ATTR_TIME +from homeassistant.core import callback from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -99,7 +100,8 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): str(self.alarm.start_time)[0:5], ) - async def async_check_if_available(self): + @callback + def async_check_if_available(self): """Check if alarm exists and remove alarm entity if not available.""" if self.alarm_id in self.hass.data[DATA_SONOS].alarms: return True @@ -114,12 +116,10 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): async def async_update(self) -> None: """Poll the device for the current state.""" - if await self.async_check_if_available(): - await self.hass.async_add_executor_job(self.update_alarm) + if not self.async_check_if_available(): + return - def update_alarm(self): - """Update the state of the alarm.""" - _LOGGER.debug("Updating the state of the alarm") + _LOGGER.debug("Updating alarm: %s", self.entity_id) if self.speaker.soco.uid != self.alarm.zone.uid: self.speaker = self.hass.data[DATA_SONOS].discovered.get( self.alarm.zone.uid @@ -129,11 +129,12 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): "No configured Sonos speaker has been found to match the alarm." ) - self._update_device() + self._async_update_device() - self.schedule_update_ha_state() + self.async_write_ha_state() - def _update_device(self): + @callback + def _async_update_device(self): """Update the device, since this alarm moved to a different player.""" device_registry = dr.async_get(self.hass) entity_registry = er.async_get(self.hass) From fa7837bb127c9622b4096abd189e13dc3384817f Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 28 May 2021 17:45:43 -0500 Subject: [PATCH 023/123] Improve Sonos alarm logging (#51212) --- homeassistant/components/sonos/switch.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/sonos/switch.py b/homeassistant/components/sonos/switch.py index 4783795a343..4b24224f6a0 100644 --- a/homeassistant/components/sonos/switch.py +++ b/homeassistant/components/sonos/switch.py @@ -106,7 +106,7 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): if self.alarm_id in self.hass.data[DATA_SONOS].alarms: return True - _LOGGER.debug("The alarm is removed from hass because it has been deleted") + _LOGGER.debug("%s has been deleted", self.entity_id) entity_registry = er.async_get(self.hass) if entity_registry.async_get(self.entity_id): @@ -151,7 +151,7 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): connections={(dr.CONNECTION_NETWORK_MAC, self.speaker.mac_address)}, ) if not entity_registry.async_get(self.entity_id).device_id == new_device.id: - _LOGGER.debug("The alarm is switching the sonos player") + _LOGGER.debug("%s is moving to %s", self.entity_id, new_device.name) # pylint: disable=protected-access entity_registry._async_update_entity( self.entity_id, device_id=new_device.id @@ -200,10 +200,8 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): async def async_handle_switch_on_off(self, turn_on: bool) -> None: """Handle turn on/off of alarm switch.""" try: - _LOGGER.debug("Switching the state of the alarm") + _LOGGER.debug("Toggling the state of %s", self.entity_id) self.alarm.enabled = turn_on await self.hass.async_add_executor_job(self.alarm.save) except SoCoUPnPException as exc: - _LOGGER.warning( - "Home Assistant couldn't switch the alarm %s", exc, exc_info=True - ) + _LOGGER.error("Could not update %s: %s", self.entity_id, exc, exc_info=True) From b75f4b1f4da2c16b01785e582829109c7b979c1d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 28 May 2021 22:37:17 +0200 Subject: [PATCH 024/123] Fix flaky statistics tests (#51214) * Fix flaky statistics tests * Tweak --- tests/components/recorder/test_statistics.py | 8 ++- tests/components/sensor/test_recorder.py | 68 ++++++++++---------- 2 files changed, 40 insertions(+), 36 deletions(-) diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index cffb67937fe..1ec0f2284b4 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -3,6 +3,8 @@ from datetime import timedelta from unittest.mock import patch, sentinel +from pytest import approx + from homeassistant.components.recorder import history from homeassistant.components.recorder.const import DATA_INSTANCE from homeassistant.components.recorder.models import process_timestamp_to_utc_isoformat @@ -30,9 +32,9 @@ def test_compile_hourly_statistics(hass_recorder): { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(zero), - "mean": 14.915254237288135, - "min": 10.0, - "max": 20.0, + "mean": approx(14.915254237288135), + "min": approx(10.0), + "max": approx(20.0), "last_reset": None, "state": None, "sum": None, diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 37cc7387f25..47a950f9eaa 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -3,6 +3,8 @@ from datetime import timedelta from unittest.mock import patch, sentinel +from pytest import approx + from homeassistant.components.recorder import history from homeassistant.components.recorder.const import DATA_INSTANCE from homeassistant.components.recorder.models import process_timestamp_to_utc_isoformat @@ -31,9 +33,9 @@ def test_compile_hourly_statistics(hass_recorder): { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(zero), - "mean": 16.440677966101696, - "min": 10.0, - "max": 30.0, + "mean": approx(16.440677966101696), + "min": approx(10.0), + "max": approx(30.0), "last_reset": None, "state": None, "sum": None, @@ -75,8 +77,8 @@ def test_compile_hourly_energy_statistics(hass_recorder): "mean": None, "min": None, "last_reset": process_timestamp_to_utc_isoformat(zero), - "state": 20.0, - "sum": 10.0, + "state": approx(20.0), + "sum": approx(10.0), }, { "statistic_id": "sensor.test1", @@ -85,8 +87,8 @@ def test_compile_hourly_energy_statistics(hass_recorder): "mean": None, "min": None, "last_reset": process_timestamp_to_utc_isoformat(four), - "state": 40.0, - "sum": 10.0, + "state": approx(40.0), + "sum": approx(10.0), }, { "statistic_id": "sensor.test1", @@ -95,8 +97,8 @@ def test_compile_hourly_energy_statistics(hass_recorder): "mean": None, "min": None, "last_reset": process_timestamp_to_utc_isoformat(four), - "state": 70.0, - "sum": 40.0, + "state": approx(70.0), + "sum": approx(40.0), }, ] } @@ -135,8 +137,8 @@ def test_compile_hourly_energy_statistics2(hass_recorder): "mean": None, "min": None, "last_reset": process_timestamp_to_utc_isoformat(zero), - "state": 20.0, - "sum": 10.0, + "state": approx(20.0), + "sum": approx(10.0), }, { "statistic_id": "sensor.test1", @@ -145,8 +147,8 @@ def test_compile_hourly_energy_statistics2(hass_recorder): "mean": None, "min": None, "last_reset": process_timestamp_to_utc_isoformat(four), - "state": 40.0, - "sum": 10.0, + "state": approx(40.0), + "sum": approx(10.0), }, { "statistic_id": "sensor.test1", @@ -155,8 +157,8 @@ def test_compile_hourly_energy_statistics2(hass_recorder): "mean": None, "min": None, "last_reset": process_timestamp_to_utc_isoformat(four), - "state": 70.0, - "sum": 40.0, + "state": approx(70.0), + "sum": approx(40.0), }, ], "sensor.test2": [ @@ -167,8 +169,8 @@ def test_compile_hourly_energy_statistics2(hass_recorder): "mean": None, "min": None, "last_reset": process_timestamp_to_utc_isoformat(zero), - "state": 130.0, - "sum": 20.0, + "state": approx(130.0), + "sum": approx(20.0), }, { "statistic_id": "sensor.test2", @@ -177,8 +179,8 @@ def test_compile_hourly_energy_statistics2(hass_recorder): "mean": None, "min": None, "last_reset": process_timestamp_to_utc_isoformat(four), - "state": 45.0, - "sum": -95.0, + "state": approx(45.0), + "sum": approx(-95.0), }, { "statistic_id": "sensor.test2", @@ -187,8 +189,8 @@ def test_compile_hourly_energy_statistics2(hass_recorder): "mean": None, "min": None, "last_reset": process_timestamp_to_utc_isoformat(four), - "state": 75.0, - "sum": -65.0, + "state": approx(75.0), + "sum": approx(-65.0), }, ], "sensor.test3": [ @@ -199,8 +201,8 @@ def test_compile_hourly_energy_statistics2(hass_recorder): "mean": None, "min": None, "last_reset": process_timestamp_to_utc_isoformat(zero), - "state": 5.0, - "sum": 5.0, + "state": approx(5.0), + "sum": approx(5.0), }, { "statistic_id": "sensor.test3", @@ -209,8 +211,8 @@ def test_compile_hourly_energy_statistics2(hass_recorder): "mean": None, "min": None, "last_reset": process_timestamp_to_utc_isoformat(four), - "state": 50.0, - "sum": 30.0, + "state": approx(50.0), + "sum": approx(30.0), }, { "statistic_id": "sensor.test3", @@ -219,8 +221,8 @@ def test_compile_hourly_energy_statistics2(hass_recorder): "mean": None, "min": None, "last_reset": process_timestamp_to_utc_isoformat(four), - "state": 90.0, - "sum": 70.0, + "state": approx(90.0), + "sum": approx(70.0), }, ], } @@ -243,9 +245,9 @@ def test_compile_hourly_statistics_unchanged(hass_recorder): { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(four), - "mean": 30.0, - "min": 30.0, - "max": 30.0, + "mean": approx(30.0), + "min": approx(30.0), + "max": approx(30.0), "last_reset": None, "state": None, "sum": None, @@ -271,9 +273,9 @@ def test_compile_hourly_statistics_partially_unavailable(hass_recorder): { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(zero), - "mean": 21.1864406779661, - "min": 10.0, - "max": 25.0, + "mean": approx(21.1864406779661), + "min": approx(10.0), + "max": approx(25.0), "last_reset": None, "state": None, "sum": None, From 51d98bb9c8d5dc8876736be079c9ef80078af2ef Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Sat, 29 May 2021 14:10:45 +0200 Subject: [PATCH 025/123] Fix Netatmo data class update (#51215) * Catch if data class entry is None * Guard --- homeassistant/components/netatmo/data_handler.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/netatmo/data_handler.py b/homeassistant/components/netatmo/data_handler.py index 12376e5ac78..1c092c40930 100644 --- a/homeassistant/components/netatmo/data_handler.py +++ b/homeassistant/components/netatmo/data_handler.py @@ -101,8 +101,7 @@ class NetatmoDataHandler: time() + data_class["interval"] ) - if self.data_classes[data_class_name]["subscriptions"]: - await self.async_fetch_data(data_class_name) + await self.async_fetch_data(data_class_name) self._queue.rotate(BATCH_SIZE) @@ -133,6 +132,9 @@ class NetatmoDataHandler: async def async_fetch_data(self, data_class_entry): """Fetch data and notify.""" + if self.data[data_class_entry] is None: + return + try: await self.data[data_class_entry].async_update() From 835a9efc6453fe99c9c4d615882a607ce8ab2907 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sat, 29 May 2021 09:08:46 -0500 Subject: [PATCH 026/123] Reorganize SonosSpeaker class for readability (#51222) --- homeassistant/components/sonos/speaker.py | 112 ++++++++++++++-------- 1 file changed, 71 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 079b916a4bc..701fe5aa8c4 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -146,36 +146,43 @@ class SonosSpeaker: self.household_id: str = soco.household_id self.media = SonosMedia(soco) + # Synchronization helpers self.is_first_poll: bool = True self._is_ready: bool = False + self._platforms_ready: set[str] = set() # Subscriptions and events self._subscriptions: list[SubscriptionBase] = [] self._resubscription_lock: asyncio.Lock | None = None self._event_dispatchers: dict[str, Callable] = {} + # Scheduled callback handles self._poll_timer: Callable | None = None self._seen_timer: Callable | None = None - self._platforms_ready: set[str] = set() + # Dispatcher handles self._entity_creation_dispatcher: Callable | None = None self._group_dispatcher: Callable | None = None self._seen_dispatcher: Callable | None = None + # Device information self.mac_address = speaker_info["mac_address"] self.model_name = speaker_info["model_name"] self.version = speaker_info["display_version"] self.zone_name = speaker_info["zone_name"] + # Battery self.battery_info: dict[str, Any] | None = None self._last_battery_event: datetime.datetime | None = None self._battery_poll_timer: Callable | None = None + # Volume / Sound self.volume: int | None = None self.muted: bool | None = None self.night_mode: bool | None = None self.dialog_mode: bool | None = None + # Grouping self.coordinator: SonosSpeaker | None = None self.sonos_group: list[SonosSpeaker] = [self] self.sonos_group_entities: list[str] = [] @@ -232,6 +239,9 @@ class SonosSpeaker: dispatcher_send(self.hass, SONOS_CREATE_MEDIA_PLAYER, self) + # + # Entity management + # async def async_handle_new_entity(self, entity_type: str) -> None: """Listen to new entities to trigger first subscription.""" self._platforms_ready.add(entity_type) @@ -254,11 +264,32 @@ class SonosSpeaker: self.media.play_mode = self.soco.play_mode self.update_volume() + # + # Properties + # @property def available(self) -> bool: """Return whether this speaker is available.""" return self._seen_timer is not None + @property + def favorites(self) -> SonosFavorites: + """Return the SonosFavorites instance for this household.""" + return self.hass.data[DATA_SONOS].favorites[self.household_id] + + @property + def is_coordinator(self) -> bool: + """Return true if player is a coordinator.""" + return self.coordinator is None + + @property + def subscription_address(self) -> str | None: + """Return the current subscription callback address if any.""" + if self._subscriptions: + addr, port = self._subscriptions[0].event_listener.address + return ":".join([addr, str(port)]) + return None + # # Subscription handling and event dispatchers # @@ -295,6 +326,30 @@ class SonosSpeaker: subscription.auto_renew_fail = self.async_renew_failed self._subscriptions.append(subscription) + @callback + def async_renew_failed(self, exception: Exception) -> None: + """Handle a failed subscription renewal.""" + self.hass.async_create_task(self.async_resubscribe(exception)) + + async def async_resubscribe(self, exception: Exception) -> None: + """Attempt to resubscribe when a renewal failure is detected.""" + async with self._resubscription_lock: + if not self.available: + return + + if getattr(exception, "status", None) == 412: + _LOGGER.warning( + "Subscriptions for %s failed, speaker may have lost power", + self.zone_name, + ) + else: + _LOGGER.error( + "Subscription renewals for %s failed", + self.zone_name, + exc_info=exception, + ) + await self.async_unseen() + @callback def async_dispatch_event(self, event: SonosEvent) -> None: """Handle callback event and route as needed.""" @@ -349,6 +404,9 @@ class SonosSpeaker: self.async_write_entity_states() + # + # Speaker availability methods + # async def async_seen(self, soco: SoCo | None = None) -> None: """Record that this speaker was seen right now.""" if soco is not None: @@ -386,28 +444,6 @@ class SonosSpeaker: self.async_write_entity_states() - async def async_resubscribe(self, exception: Exception) -> None: - """Attempt to resubscribe when a renewal failure is detected.""" - async with self._resubscription_lock: - if self.available: - if getattr(exception, "status", None) == 412: - _LOGGER.warning( - "Subscriptions for %s failed, speaker may have lost power", - self.zone_name, - ) - else: - _LOGGER.error( - "Subscription renewals for %s failed", - self.zone_name, - exc_info=exception, - ) - await self.async_unseen() - - @callback - def async_renew_failed(self, exception: Exception) -> None: - """Handle a failed subscription renewal.""" - self.hass.async_create_task(self.async_resubscribe(exception)) - async def async_unseen(self, now: datetime.datetime | None = None) -> None: """Make this player unavailable when it was not seen recently.""" self.async_write_entity_states() @@ -425,6 +461,9 @@ class SonosSpeaker: self._subscriptions = [] + # + # Alarm management + # def update_alarms_for_speaker(self) -> set[str]: """Update current alarm instances. @@ -453,6 +492,9 @@ class SonosSpeaker: dispatcher_send(self.hass, SONOS_CREATE_ALARM, self, new_alarms) dispatcher_send(self.hass, SONOS_ALARM_UPDATE) + # + # Battery management + # async def async_update_battery_info(self, battery_dict: dict[str, Any]) -> None: """Update battery info using the decoded SonosEvent.""" self._last_battery_event = dt_util.utcnow() @@ -477,11 +519,6 @@ class SonosSpeaker: ): self.battery_info = battery_info - @property - def is_coordinator(self) -> bool: - """Return true if player is a coordinator.""" - return self.coordinator is None - @property def power_source(self) -> str | None: """Return the name of the current power source. @@ -516,6 +553,9 @@ class SonosSpeaker: self.battery_info = battery_info self.async_write_entity_states() + # + # Group management + # def update_groups(self, event: SonosEvent | None = None) -> None: """Handle callback for topology change event.""" coro = self.create_update_groups_coro(event) @@ -786,11 +826,9 @@ class SonosSpeaker: for speaker in hass.data[DATA_SONOS].discovered.values(): speaker.soco._zgs_cache.clear() # pylint: disable=protected-access - @property - def favorites(self) -> SonosFavorites: - """Return the SonosFavorites instance for this household.""" - return self.hass.data[DATA_SONOS].favorites[self.household_id] - + # + # Media and playback state handlers + # def update_volume(self) -> None: """Update information about current volume settings.""" self.volume = self.soco.volume @@ -951,11 +989,3 @@ class SonosSpeaker: elif update_media_position: self.media.position = current_position self.media.position_updated_at = dt_util.utcnow() - - @property - def subscription_address(self) -> str | None: - """Return the current subscription callback address if any.""" - if self._subscriptions: - addr, port = self._subscriptions[0].event_listener.address - return ":".join([addr, str(port)]) - return None From d236e07046442c1b7630cc87b1067c6eca4ca6f2 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sun, 30 May 2021 23:03:53 -0500 Subject: [PATCH 027/123] Skip processed Sonos alarm updates (#51217) * Skip processed Sonos alarm updates * Fix bad conflict merge --- homeassistant/components/sonos/__init__.py | 3 ++- homeassistant/components/sonos/speaker.py | 10 ++++++++++ tests/components/sonos/conftest.py | 11 ++++++++++- tests/components/sonos/test_switch.py | 1 + 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index a904ae58db6..7f772aec6af 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from collections import OrderedDict +from collections import OrderedDict, deque import datetime import logging import socket @@ -73,6 +73,7 @@ class SonosData: self.discovered: OrderedDict[str, SonosSpeaker] = OrderedDict() self.favorites: dict[str, SonosFavorites] = {} self.alarms: dict[str, Alarm] = {} + self.processed_alarm_events = deque(maxlen=5) self.topology_condition = asyncio.Condition() self.discovery_thread = None self.hosts_heartbeat = None diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 701fe5aa8c4..acd53e1f877 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from collections import deque from collections.abc import Coroutine import contextlib import datetime @@ -282,6 +283,11 @@ class SonosSpeaker: """Return true if player is a coordinator.""" return self.coordinator is None + @property + def processed_alarm_events(self) -> deque[str]: + """Return the container of processed alarm events.""" + return self.hass.data[DATA_SONOS].processed_alarm_events + @property def subscription_address(self) -> str | None: """Return the current subscription callback address if any.""" @@ -366,6 +372,10 @@ class SonosSpeaker: @callback def async_dispatch_alarms(self, event: SonosEvent) -> None: """Create a task to update alarms from an event.""" + update_id = event.variables["alarm_list_version"] + if update_id in self.processed_alarm_events: + return + self.processed_alarm_events.append(update_id) self.hass.async_add_executor_job(self.update_alarms) @callback diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index fc5ef84c2d6..62fd3254d60 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -30,6 +30,15 @@ class SonosMockEvent: self.service = service self.variables = variables + def increment_variable(self, var_name): + """Increment the value of the var_name key in variables dict attribute. + + Assumes value has a format of :. + """ + base, count = self.variables[var_name].split(":") + newcount = int(count) + 1 + self.variables[var_name] = ":".join([base, str(newcount)]) + @pytest.fixture(name="config_entry") def config_entry_fixture(): @@ -165,7 +174,7 @@ def alarm_event_fixture(soco): "time_zone": "ffc40a000503000003000502ffc4", "time_server": "0.sonostime.pool.ntp.org,1.sonostime.pool.ntp.org,2.sonostime.pool.ntp.org,3.sonostime.pool.ntp.org", "time_generation": "20000001", - "alarm_list_version": "RINCON_test", + "alarm_list_version": "RINCON_test:1", "time_format": "INV", "date_format": "INV", "daily_index_refresh_time": None, diff --git a/tests/components/sonos/test_switch.py b/tests/components/sonos/test_switch.py index d4448d22b32..41cb241d377 100644 --- a/tests/components/sonos/test_switch.py +++ b/tests/components/sonos/test_switch.py @@ -68,6 +68,7 @@ async def test_alarm_create_delete( assert "switch.sonos_alarm_15" in entity_registry.entities alarm_clock_extended.ListAlarms.return_value = alarm_clock.ListAlarms.return_value + alarm_event.increment_variable("alarm_list_version") sub_callback(event=alarm_event) await hass.async_block_till_done() From 99fd5be36983ef152d3eb2020bc7f48f553ff7bb Mon Sep 17 00:00:00 2001 From: Ludovico de Nittis Date: Sat, 29 May 2021 18:50:45 +0200 Subject: [PATCH 028/123] Bump pyialarm to 1.7 (#51233) --- homeassistant/components/ialarm/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ialarm/manifest.json b/homeassistant/components/ialarm/manifest.json index 5cdc0ead3ea..e112a26003e 100644 --- a/homeassistant/components/ialarm/manifest.json +++ b/homeassistant/components/ialarm/manifest.json @@ -2,7 +2,7 @@ "domain": "ialarm", "name": "Antifurto365 iAlarm", "documentation": "https://www.home-assistant.io/integrations/ialarm", - "requirements": ["pyialarm==1.5"], + "requirements": ["pyialarm==1.7"], "codeowners": ["@RyuzakiKK"], "config_flow": true, "iot_class": "local_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 55d07ae773d..4c74f67ad19 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1464,7 +1464,7 @@ pyhomematic==0.1.72 pyhomeworks==0.0.6 # homeassistant.components.ialarm -pyialarm==1.5 +pyialarm==1.7 # homeassistant.components.icloud pyicloud==0.10.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9f95763e54f..44b7c1bc799 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -808,7 +808,7 @@ pyhiveapi==0.4.2 pyhomematic==0.1.72 # homeassistant.components.ialarm -pyialarm==1.5 +pyialarm==1.7 # homeassistant.components.icloud pyicloud==0.10.2 From 8fd3761893014069858e09d42f64d37f1dccd024 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 29 May 2021 16:00:36 +0200 Subject: [PATCH 029/123] Fix flaky statistics tests (#51242) --- tests/components/history/test_init.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index 36dd3f30156..bf8d34e6ffe 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -5,6 +5,7 @@ import json from unittest.mock import patch, sentinel import pytest +from pytest import approx from homeassistant.components import history, recorder from homeassistant.components.recorder.history import get_significant_states @@ -884,9 +885,9 @@ async def test_statistics_during_period(hass, hass_ws_client): { "statistic_id": "sensor.test", "start": now.isoformat(), - "mean": 10.0, - "min": 10.0, - "max": 10.0, + "mean": approx(10.0), + "min": approx(10.0), + "max": approx(10.0), "last_reset": None, "state": None, "sum": None, From 9dfd578b65ae85a698272cf2f88d04261e4b7edc Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Mon, 31 May 2021 05:55:45 +0200 Subject: [PATCH 030/123] Fix unnecessary API calls in Netatmo (#51260) --- homeassistant/components/netatmo/data_handler.py | 8 +++++++- homeassistant/components/netatmo/sensor.py | 12 +++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/netatmo/data_handler.py b/homeassistant/components/netatmo/data_handler.py index 1c092c40930..e93e602d6a7 100644 --- a/homeassistant/components/netatmo/data_handler.py +++ b/homeassistant/components/netatmo/data_handler.py @@ -158,7 +158,13 @@ class NetatmoDataHandler: ): """Register data class.""" if data_class_entry in self.data_classes: - self.data_classes[data_class_entry]["subscriptions"].append(update_callback) + if ( + update_callback + not in self.data_classes[data_class_entry]["subscriptions"] + ): + self.data_classes[data_class_entry]["subscriptions"].append( + update_callback + ) return self.data_classes[data_class_entry] = { diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index eeaab52a21a..ed75ddf2f7f 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -130,7 +130,7 @@ PUBLIC = "public" async def async_setup_entry(hass, entry, async_add_entities): """Set up the Netatmo weather and homecoach platform.""" data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] - platform_not_ready = False + platform_not_ready = True async def find_entities(data_class_name): """Find all entities.""" @@ -183,8 +183,8 @@ async def async_setup_entry(hass, entry, async_add_entities): await data_handler.register_data_class(data_class_name, data_class_name, None) data_class = data_handler.data.get(data_class_name) - if not data_class or not data_class.raw_data: - platform_not_ready = True + if not (data_class and data_class.raw_data): + platform_not_ready = False async_add_entities(await find_entities(data_class_name), True) @@ -226,6 +226,12 @@ async def async_setup_entry(hass, entry, async_add_entities): lat_sw=area.lat_sw, lon_sw=area.lon_sw, ) + data_class = data_handler.data.get(signal_name) + + if not (data_class and data_class.raw_data): + nonlocal platform_not_ready + platform_not_ready = False + for sensor_type in SUPPORTED_PUBLIC_SENSOR_TYPES: new_entities.append( NetatmoPublicSensor(data_handler, area, sensor_type) From c20ac0efb2f5ca906ea15597c78ef5f687f09fab Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 30 May 2021 21:04:13 -0700 Subject: [PATCH 031/123] Updated frontend to 20210531.0 (#51281) --- 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 9ee97c851e9..61da986b06b 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210528.0" + "home-assistant-frontend==20210531.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7e6be2f0016..81bbaf1ff5d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ defusedxml==0.7.1 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.43.0 -home-assistant-frontend==20210528.0 +home-assistant-frontend==20210531.0 httpx==0.18.0 ifaddr==0.1.7 jinja2>=3.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index 4c74f67ad19..c72b147bf38 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -765,7 +765,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210528.0 +home-assistant-frontend==20210531.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 44b7c1bc799..5ff64645bcc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -429,7 +429,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210528.0 +home-assistant-frontend==20210531.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 792d7bb3f522b25a46b6e102c238b80edbdccd63 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 30 May 2021 21:13:41 -0700 Subject: [PATCH 032/123] Bumped version to 2021.6.0b2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index a38655119bc..76015e87360 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 6 -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, 8, 0) From a08fffea1783cbcb6d0db331bc3bc89a47a28cec Mon Sep 17 00:00:00 2001 From: Ron Klinkien Date: Mon, 31 May 2021 23:38:33 +0200 Subject: [PATCH 033/123] Fix Garmin Connect integration with python-garminconnect-aio (#50865) --- .../components/garmin_connect/__init__.py | 51 ++++++++----------- .../components/garmin_connect/config_flow.py | 19 ++++--- .../components/garmin_connect/const.py | 5 +- .../components/garmin_connect/manifest.json | 2 +- .../components/garmin_connect/sensor.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../garmin_connect/test_config_flow.py | 40 ++++++++++----- 8 files changed, 67 insertions(+), 56 deletions(-) diff --git a/homeassistant/components/garmin_connect/__init__.py b/homeassistant/components/garmin_connect/__init__.py index 4ac157707fc..45c71bf1f07 100644 --- a/homeassistant/components/garmin_connect/__init__.py +++ b/homeassistant/components/garmin_connect/__init__.py @@ -1,8 +1,8 @@ """The Garmin Connect integration.""" -from datetime import date, timedelta +from datetime import date import logging -from garminconnect import ( +from garminconnect_aio import ( Garmin, GarminConnectAuthenticationError, GarminConnectConnectionError, @@ -13,25 +13,27 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import Throttle -from .const import DOMAIN +from .const import DEFAULT_UPDATE_INTERVAL, DOMAIN _LOGGER = logging.getLogger(__name__) PLATFORMS = ["sensor"] -MIN_SCAN_INTERVAL = timedelta(minutes=10) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Garmin Connect from a config entry.""" - username = entry.data[CONF_USERNAME] - password = entry.data[CONF_PASSWORD] - garmin_client = Garmin(username, password) + websession = async_get_clientsession(hass) + username: str = entry.data[CONF_USERNAME] + password: str = entry.data[CONF_PASSWORD] + + garmin_client = Garmin(websession, username, password) try: - await hass.async_add_executor_job(garmin_client.login) + await garmin_client.login() except ( GarminConnectAuthenticationError, GarminConnectTooManyRequestsError, @@ -73,38 +75,29 @@ class GarminConnectData: self.client = client self.data = None - async def _get_combined_alarms_of_all_devices(self): - """Combine the list of active alarms from all garmin devices.""" - alarms = [] - devices = await self.hass.async_add_executor_job(self.client.get_devices) - for device in devices: - device_settings = await self.hass.async_add_executor_job( - self.client.get_device_settings, device["deviceId"] - ) - alarms += device_settings["alarms"] - return alarms - - @Throttle(MIN_SCAN_INTERVAL) + @Throttle(DEFAULT_UPDATE_INTERVAL) async def async_update(self): - """Update data via library.""" + """Update data via API wrapper.""" today = date.today() try: - self.data = await self.hass.async_add_executor_job( - self.client.get_stats_and_body, today.isoformat() - ) - self.data["nextAlarm"] = await self._get_combined_alarms_of_all_devices() + summary = await self.client.get_user_summary(today.isoformat()) + body = await self.client.get_body_composition(today.isoformat()) + + self.data = { + **summary, + **body["totalAverage"], + } + self.data["nextAlarm"] = await self.client.get_device_alarms() except ( GarminConnectAuthenticationError, GarminConnectTooManyRequestsError, GarminConnectConnectionError, ) as err: _LOGGER.error( - "Error occurred during Garmin Connect get activity request: %s", err + "Error occurred during Garmin Connect update requests: %s", err ) - return except Exception: # pylint: disable=broad-except _LOGGER.exception( - "Unknown error occurred during Garmin Connect get activity request" + "Unknown error occurred during Garmin Connect update requests" ) - return diff --git a/homeassistant/components/garmin_connect/config_flow.py b/homeassistant/components/garmin_connect/config_flow.py index 218a98ba9a4..8e26e2bf608 100644 --- a/homeassistant/components/garmin_connect/config_flow.py +++ b/homeassistant/components/garmin_connect/config_flow.py @@ -1,7 +1,7 @@ """Config flow for Garmin Connect integration.""" import logging -from garminconnect import ( +from garminconnect_aio import ( Garmin, GarminConnectAuthenticationError, GarminConnectConnectionError, @@ -11,6 +11,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN @@ -37,11 +38,15 @@ class GarminConnectConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if user_input is None: return await self._show_setup_form() - garmin_client = Garmin(user_input[CONF_USERNAME], user_input[CONF_PASSWORD]) + websession = async_get_clientsession(self.hass) + + garmin_client = Garmin( + websession, user_input[CONF_USERNAME], user_input[CONF_PASSWORD] + ) errors = {} try: - await self.hass.async_add_executor_job(garmin_client.login) + username = await garmin_client.login() except GarminConnectConnectionError: errors["base"] = "cannot_connect" return await self._show_setup_form(errors) @@ -56,15 +61,13 @@ class GarminConnectConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "unknown" return await self._show_setup_form(errors) - unique_id = garmin_client.get_full_name() - - await self.async_set_unique_id(unique_id) + await self.async_set_unique_id(username) self._abort_if_unique_id_configured() return self.async_create_entry( - title=unique_id, + title=username, data={ - CONF_ID: unique_id, + CONF_ID: username, CONF_USERNAME: user_input[CONF_USERNAME], CONF_PASSWORD: user_input[CONF_PASSWORD], }, diff --git a/homeassistant/components/garmin_connect/const.py b/homeassistant/components/garmin_connect/const.py index 991ac90526a..19ed4ca4d94 100644 --- a/homeassistant/components/garmin_connect/const.py +++ b/homeassistant/components/garmin_connect/const.py @@ -1,4 +1,6 @@ """Constants for the Garmin Connect integration.""" +from datetime import timedelta + from homeassistant.const import ( DEVICE_CLASS_TIMESTAMP, LENGTH_METERS, @@ -8,7 +10,8 @@ from homeassistant.const import ( ) DOMAIN = "garmin_connect" -ATTRIBUTION = "Data provided by garmin.com" +ATTRIBUTION = "connect.garmin.com" +DEFAULT_UPDATE_INTERVAL = timedelta(minutes=10) GARMIN_ENTITY_LIST = { "totalSteps": ["Total Steps", "steps", "mdi:walk", None, True], diff --git a/homeassistant/components/garmin_connect/manifest.json b/homeassistant/components/garmin_connect/manifest.json index 913e85de954..2495249e4a4 100644 --- a/homeassistant/components/garmin_connect/manifest.json +++ b/homeassistant/components/garmin_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "garmin_connect", "name": "Garmin Connect", "documentation": "https://www.home-assistant.io/integrations/garmin_connect", - "requirements": ["garminconnect==0.1.19"], + "requirements": ["garminconnect_aio==0.1.1"], "codeowners": ["@cyberjunky"], "config_flow": true, "iot_class": "cloud_polling" diff --git a/homeassistant/components/garmin_connect/sensor.py b/homeassistant/components/garmin_connect/sensor.py index 0d946d5e88e..eb1690c9765 100644 --- a/homeassistant/components/garmin_connect/sensor.py +++ b/homeassistant/components/garmin_connect/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from garminconnect import ( +from garminconnect_aio import ( GarminConnectAuthenticationError, GarminConnectConnectionError, GarminConnectTooManyRequestsError, diff --git a/requirements_all.txt b/requirements_all.txt index c72b147bf38..67c8adb70ac 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -635,7 +635,7 @@ gTTS==2.2.2 garages-amsterdam==2.1.1 # homeassistant.components.garmin_connect -garminconnect==0.1.19 +garminconnect_aio==0.1.1 # homeassistant.components.geniushub geniushub-client==0.6.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5ff64645bcc..e4aa37d8634 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -341,7 +341,7 @@ gTTS==2.2.2 garages-amsterdam==2.1.1 # homeassistant.components.garmin_connect -garminconnect==0.1.19 +garminconnect_aio==0.1.1 # homeassistant.components.geo_json_events # homeassistant.components.usgs_earthquakes_feed diff --git a/tests/components/garmin_connect/test_config_flow.py b/tests/components/garmin_connect/test_config_flow.py index f3784d5e2e2..2ad36ffa29c 100644 --- a/tests/components/garmin_connect/test_config_flow.py +++ b/tests/components/garmin_connect/test_config_flow.py @@ -1,7 +1,7 @@ """Test the Garmin Connect config flow.""" from unittest.mock import patch -from garminconnect import ( +from garminconnect_aio import ( GarminConnectAuthenticationError, GarminConnectConnectionError, GarminConnectTooManyRequestsError, @@ -15,7 +15,7 @@ from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME from tests.common import MockConfigEntry MOCK_CONF = { - CONF_ID: "First Lastname", + CONF_ID: "my@email.address", CONF_USERNAME: "my@email.address", CONF_PASSWORD: "mypassw0rd", } @@ -23,27 +23,33 @@ MOCK_CONF = { @pytest.fixture(name="mock_garmin_connect") def mock_garmin(): - """Mock Garmin.""" + """Mock Garmin Connect.""" with patch( "homeassistant.components.garmin_connect.config_flow.Garmin", ) as garmin: - garmin.return_value.get_full_name.return_value = MOCK_CONF[CONF_ID] + garmin.return_value.login.return_value = MOCK_CONF[CONF_ID] yield garmin.return_value async def test_show_form(hass): """Test that the form is served with no input.""" + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "user" + assert result["errors"] == {} + assert result["step_id"] == config_entries.SOURCE_USER -async def test_step_user(hass, mock_garmin_connect): +async def test_step_user(hass): """Test registering an integration and finishing flow works.""" with patch( + "homeassistant.components.garmin_connect.Garmin.login", + return_value="my@email.address", + ), patch( "homeassistant.components.garmin_connect.async_setup_entry", return_value=True ): result = await hass.config_entries.flow.async_init( @@ -95,12 +101,18 @@ async def test_unknown_error(hass, mock_garmin_connect): assert result["errors"] == {"base": "unknown"} -async def test_abort_if_already_setup(hass, mock_garmin_connect): +async def test_abort_if_already_setup(hass): """Test abort if already setup.""" - entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONF, unique_id=MOCK_CONF[CONF_ID]) - entry.add_to_hass(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_CONF - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" + MockConfigEntry( + domain=DOMAIN, data=MOCK_CONF, unique_id=MOCK_CONF[CONF_ID] + ).add_to_hass(hass) + with patch( + "homeassistant.components.garmin_connect.config_flow.Garmin", autospec=True + ) as garmin: + garmin.return_value.login.return_value = MOCK_CONF[CONF_ID] + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=MOCK_CONF + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" From 273f57261c24ebc5e378b7b8a688a058b72e6e6b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 May 2021 13:47:12 -0500 Subject: [PATCH 034/123] Upgrade HAP-python to 3.5.0 (#51261) * Upgrade HAP-python to 3.4.2 - Fixes for malformed event sending - Performance improvements * Bump * update tests to point to async --- homeassistant/components/homekit/__init__.py | 8 ++++---- homeassistant/components/homekit/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/homekit/conftest.py | 2 +- tests/components/homekit/test_homekit.py | 17 ++++++++++------- tests/components/homekit/test_type_cameras.py | 2 +- 7 files changed, 19 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 87742104b86..ef228f3ae60 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -456,7 +456,7 @@ class HomeKit: self.bridge = None self.driver = None - def setup(self, zeroconf_instance): + def setup(self, async_zeroconf_instance): """Set up bridge and accessory driver.""" ip_addr = self._ip_address or get_local_ip() persist_file = get_persist_fullpath_for_entry_id(self.hass, self._entry_id) @@ -471,7 +471,7 @@ class HomeKit: port=self._port, persist_file=persist_file, advertised_address=self._advertise_ip, - zeroconf_instance=zeroconf_instance, + async_zeroconf_instance=async_zeroconf_instance, ) # If we do not load the mac address will be wrong @@ -595,8 +595,8 @@ class HomeKit: if self.status != STATUS_READY: return self.status = STATUS_WAIT - zc_instance = await zeroconf.async_get_instance(self.hass) - await self.hass.async_add_executor_job(self.setup, zc_instance) + async_zc_instance = await zeroconf.async_get_async_instance(self.hass) + await self.hass.async_add_executor_job(self.setup, async_zc_instance) self.aid_storage = AccessoryAidStorage(self.hass, self._entry_id) await self.aid_storage.async_initialize() await self._async_create_accessories() diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index 483279d55f3..d2c2f094a0f 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit", "documentation": "https://www.home-assistant.io/integrations/homekit", "requirements": [ - "HAP-python==3.4.1", + "HAP-python==3.5.0", "fnvhash==0.1.0", "PyQRCode==1.2.1", "base36==0.1.1", diff --git a/requirements_all.txt b/requirements_all.txt index 67c8adb70ac..3ca655541aa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -14,7 +14,7 @@ Adafruit-SHT31==1.0.2 # Adafruit_BBIO==1.1.1 # homeassistant.components.homekit -HAP-python==3.4.1 +HAP-python==3.5.0 # homeassistant.components.mastodon Mastodon.py==1.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e4aa37d8634..a42813bf174 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.homekit -HAP-python==3.4.1 +HAP-python==3.5.0 # homeassistant.components.flick_electric PyFlick==0.0.2 diff --git a/tests/components/homekit/conftest.py b/tests/components/homekit/conftest.py index 469a0a7deb7..5441bcc195c 100644 --- a/tests/components/homekit/conftest.py +++ b/tests/components/homekit/conftest.py @@ -12,7 +12,7 @@ from tests.common import async_capture_events @pytest.fixture def hk_driver(loop): """Return a custom AccessoryDriver instance for HomeKit accessory init.""" - with patch("pyhap.accessory_driver.Zeroconf"), patch( + with patch("pyhap.accessory_driver.AsyncZeroconf"), patch( "pyhap.accessory_driver.AccessoryEncoder" ), patch("pyhap.accessory_driver.HAPServer.async_stop"), patch( "pyhap.accessory_driver.HAPServer.async_start" diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index fd7d74aeaba..bd7af3b3596 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -250,7 +250,7 @@ async def test_homekit_setup(hass, hk_driver, mock_zeroconf): port=DEFAULT_PORT, persist_file=path, advertised_address=None, - zeroconf_instance=zeroconf_mock, + async_zeroconf_instance=zeroconf_mock, ) assert homekit.driver.safe_mode is False @@ -290,7 +290,7 @@ async def test_homekit_setup_ip_address(hass, hk_driver, mock_zeroconf): port=DEFAULT_PORT, persist_file=path, advertised_address=None, - zeroconf_instance=mock_zeroconf, + async_zeroconf_instance=mock_zeroconf, ) @@ -315,10 +315,10 @@ async def test_homekit_setup_advertise_ip(hass, hk_driver, mock_zeroconf): entry_title=entry.title, ) - zeroconf_instance = MagicMock() + async_zeroconf_instance = MagicMock() path = get_persist_fullpath_for_entry_id(hass, entry.entry_id) with patch(f"{PATH_HOMEKIT}.HomeDriver", return_value=hk_driver) as mock_driver: - await hass.async_add_executor_job(homekit.setup, zeroconf_instance) + await hass.async_add_executor_job(homekit.setup, async_zeroconf_instance) mock_driver.assert_called_with( hass, entry.entry_id, @@ -329,7 +329,7 @@ async def test_homekit_setup_advertise_ip(hass, hk_driver, mock_zeroconf): port=DEFAULT_PORT, persist_file=path, advertised_address="192.168.1.100", - zeroconf_instance=zeroconf_instance, + async_zeroconf_instance=async_zeroconf_instance, ) @@ -851,7 +851,7 @@ async def test_homekit_uses_system_zeroconf(hass, hk_driver, mock_zeroconf): options={}, ) assert await async_setup_component(hass, "zeroconf", {"zeroconf": {}}) - system_zc = await zeroconf.async_get_instance(hass) + system_async_zc = await zeroconf.async_get_async_instance(hass) with patch("pyhap.accessory_driver.AccessoryDriver.async_start"), patch( f"{PATH_HOMEKIT}.HomeKit.async_stop" @@ -859,7 +859,10 @@ async def test_homekit_uses_system_zeroconf(hass, hk_driver, mock_zeroconf): entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert hass.data[DOMAIN][entry.entry_id][HOMEKIT].driver.advertiser == system_zc + assert ( + hass.data[DOMAIN][entry.entry_id][HOMEKIT].driver.advertiser + == system_async_zc + ) assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/homekit/test_type_cameras.py b/tests/components/homekit/test_type_cameras.py index ba08ea3caaf..354db900470 100644 --- a/tests/components/homekit/test_type_cameras.py +++ b/tests/components/homekit/test_type_cameras.py @@ -80,7 +80,7 @@ async def _async_stop_stream(hass, acc, session_info): @pytest.fixture() def run_driver(hass): """Return a custom AccessoryDriver instance for HomeKit accessory init.""" - with patch("pyhap.accessory_driver.Zeroconf"), patch( + with patch("pyhap.accessory_driver.AsyncZeroconf"), patch( "pyhap.accessory_driver.AccessoryEncoder" ), patch("pyhap.accessory_driver.HAPServer"), patch( "pyhap.accessory_driver.AccessoryDriver.publish" From c4a98755a38cf2e56fc7d01437335af5e3982fbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 31 May 2021 14:06:11 +0200 Subject: [PATCH 035/123] Resolve addon repository slug for device registry (#51287) * Resolve addon repository slug for device registry * typo * Adjust onboarding test * Use /store --- homeassistant/components/hassio/__init__.py | 28 +++++++++++++++++++-- homeassistant/components/hassio/handler.py | 8 ++++++ tests/components/hassio/test_init.py | 23 +++++++++++------ tests/components/onboarding/test_views.py | 3 +++ 4 files changed, 53 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index e33c689c59e..d391817f964 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -71,6 +71,7 @@ CONFIG_SCHEMA = vol.Schema( DATA_CORE_INFO = "hassio_core_info" DATA_HOST_INFO = "hassio_host_info" +DATA_STORE = "hassio_store" DATA_INFO = "hassio_info" DATA_OS_INFO = "hassio_os_info" DATA_SUPERVISOR_INFO = "hassio_supervisor_info" @@ -291,6 +292,16 @@ def get_host_info(hass): return hass.data.get(DATA_HOST_INFO) +@callback +@bind_hass +def get_store(hass): + """Return store information. + + Async friendly. + """ + return hass.data.get(DATA_STORE) + + @callback @bind_hass def get_supervisor_info(hass): @@ -456,6 +467,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: try: hass.data[DATA_INFO] = await hassio.get_info() hass.data[DATA_HOST_INFO] = await hassio.get_host_info() + hass.data[DATA_STORE] = await hassio.get_store() hass.data[DATA_CORE_INFO] = await hassio.get_core_info() hass.data[DATA_SUPERVISOR_INFO] = await hassio.get_supervisor_info() hass.data[DATA_OS_INFO] = await hassio.get_os_info() @@ -627,10 +639,22 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): async def _async_update_data(self) -> dict[str, Any]: """Update data via library.""" new_data = {} - addon_data = get_supervisor_info(self.hass) + supervisor_info = get_supervisor_info(self.hass) + store_data = get_store(self.hass) + + repositories = { + repo[ATTR_SLUG]: repo[ATTR_NAME] + for repo in store_data.get("repositories", []) + } new_data["addons"] = { - addon[ATTR_SLUG]: addon for addon in addon_data.get("addons", []) + addon[ATTR_SLUG]: { + **addon, + ATTR_REPOSITORY: repositories.get( + addon.get(ATTR_REPOSITORY), addon.get(ATTR_REPOSITORY, "") + ), + } + for addon in supervisor_info.get("addons", []) } if self.is_hass_os: new_data["os"] = get_os_info(self.hass) diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index 301d353faf0..37b645eb7d3 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -118,6 +118,14 @@ class HassIO: """ return self.send_command(f"/addons/{addon}/info", method="get") + @api_data + def get_store(self): + """Return data from the store. + + This method return a coroutine. + """ + return self.send_command("/store", method="get") + @api_data def get_ingress_panels(self): """Return data for Add-on ingress panels. diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 5bf8a45ab52..7e9d7cd91c8 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -32,6 +32,13 @@ def mock_all(aioclient_mock, request): "data": {"supervisor": "222", "homeassistant": "0.110.0", "hassos": None}, }, ) + aioclient_mock.get( + "http://127.0.0.1/store", + json={ + "result": "ok", + "data": {"addons": [], "repositories": []}, + }, + ) aioclient_mock.get( "http://127.0.0.1/host/info", json={ @@ -67,6 +74,7 @@ def mock_all(aioclient_mock, request): "update_available": False, "version": "1.0.0", "version_latest": "1.0.0", + "repository": "core", "url": "https://github.com/home-assistant/addons/test", }, { @@ -76,6 +84,7 @@ def mock_all(aioclient_mock, request): "update_available": False, "version": "1.0.0", "version_latest": "1.0.0", + "repository": "core", "url": "https://github.com", }, ], @@ -92,7 +101,7 @@ async def test_setup_api_ping(hass, aioclient_mock): result = await async_setup_component(hass, "hassio", {}) assert result - assert aioclient_mock.call_count == 9 + assert aioclient_mock.call_count == 10 assert hass.components.hassio.get_core_info()["version_latest"] == "1.0.0" assert hass.components.hassio.is_hassio() @@ -131,7 +140,7 @@ async def test_setup_api_push_api_data(hass, aioclient_mock): ) assert result - assert aioclient_mock.call_count == 9 + assert aioclient_mock.call_count == 10 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 9999 assert aioclient_mock.mock_calls[1][2]["watchdog"] @@ -147,7 +156,7 @@ async def test_setup_api_push_api_data_server_host(hass, aioclient_mock): ) assert result - assert aioclient_mock.call_count == 9 + assert aioclient_mock.call_count == 10 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 9999 assert not aioclient_mock.mock_calls[1][2]["watchdog"] @@ -159,7 +168,7 @@ async def test_setup_api_push_api_data_default(hass, aioclient_mock, hass_storag result = await async_setup_component(hass, "hassio", {"http": {}, "hassio": {}}) assert result - assert aioclient_mock.call_count == 9 + assert aioclient_mock.call_count == 10 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 8123 refresh_token = aioclient_mock.mock_calls[1][2]["refresh_token"] @@ -206,7 +215,7 @@ async def test_setup_api_existing_hassio_user(hass, aioclient_mock, hass_storage result = await async_setup_component(hass, "hassio", {"http": {}, "hassio": {}}) assert result - assert aioclient_mock.call_count == 9 + assert aioclient_mock.call_count == 10 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 8123 assert aioclient_mock.mock_calls[1][2]["refresh_token"] == token.token @@ -220,7 +229,7 @@ async def test_setup_core_push_timezone(hass, aioclient_mock): result = await async_setup_component(hass, "hassio", {"hassio": {}}) assert result - assert aioclient_mock.call_count == 9 + assert aioclient_mock.call_count == 10 assert aioclient_mock.mock_calls[2][2]["timezone"] == "testzone" with patch("homeassistant.util.dt.set_default_time_zone"): @@ -237,7 +246,7 @@ async def test_setup_hassio_no_additional_data(hass, aioclient_mock): result = await async_setup_component(hass, "hassio", {"hassio": {}}) assert result - assert aioclient_mock.call_count == 9 + assert aioclient_mock.call_count == 10 assert aioclient_mock.mock_calls[-1][3]["X-Hassio-Key"] == "123456" diff --git a/tests/components/onboarding/test_views.py b/tests/components/onboarding/test_views.py index d8ae50b851f..a921dfe39d4 100644 --- a/tests/components/onboarding/test_views.py +++ b/tests/components/onboarding/test_views.py @@ -73,6 +73,9 @@ async def mock_supervisor_fixture(hass, aioclient_mock): ), patch( "homeassistant.components.hassio.HassIO.get_host_info", return_value={}, + ), patch( + "homeassistant.components.hassio.HassIO.get_store", + return_value={}, ), patch( "homeassistant.components.hassio.HassIO.get_supervisor_info", return_value={"diagnostics": True}, From b3ccc44ee9749351023401cb8bf3dc232a176c1f Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 31 May 2021 14:03:26 +0200 Subject: [PATCH 036/123] Revert "GRPC is fixed, don't need a workaround" (#51289) This reverts commit 9d174e8a0504a83831530ef9c9178bbbe5a58872. --- homeassistant/package_constraints.txt | 4 ++++ script/gen_requirements_all.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 81bbaf1ff5d..e7e458efd70 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -48,6 +48,10 @@ h11>=0.12.0 # https://github.com/advisories/GHSA-93xj-8mrv-444m httplib2>=0.19.0 +# gRPC 1.32+ currently causes issues on ARMv7, see: +# https://github.com/home-assistant/core/issues/40148 +grpcio==1.31.0 + # This is a old unmaintained library and is replaced with pycryptodome pycrypto==1000000000.0.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 79d4c05b0b6..4fd96cb1b04 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -68,6 +68,10 @@ h11>=0.12.0 # https://github.com/advisories/GHSA-93xj-8mrv-444m httplib2>=0.19.0 +# gRPC 1.32+ currently causes issues on ARMv7, see: +# https://github.com/home-assistant/core/issues/40148 +grpcio==1.31.0 + # This is a old unmaintained library and is replaced with pycryptodome pycrypto==1000000000.0.0 From 127a230703bdcdeb0bd0c00ec0a3302e11f3d5cf Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 31 May 2021 15:36:40 -0700 Subject: [PATCH 037/123] Add system option to disable polling (#51299) --- .../components/config/config_entries.py | 36 +++++---- homeassistant/config_entries.py | 27 +++++-- homeassistant/helpers/entity_platform.py | 7 +- homeassistant/helpers/update_coordinator.py | 9 ++- .../components/config/test_config_entries.py | 73 +++++++++++++------ tests/helpers/test_entity_platform.py | 13 ++++ tests/helpers/test_update_coordinator.py | 12 ++- 7 files changed, 123 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index efc60288439..9d88a9b5311 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -31,7 +31,6 @@ async def async_setup(hass): hass.components.websocket_api.async_register_command(config_entry_disable) hass.components.websocket_api.async_register_command(config_entry_update) hass.components.websocket_api.async_register_command(config_entries_progress) - hass.components.websocket_api.async_register_command(system_options_list) hass.components.websocket_api.async_register_command(system_options_update) hass.components.websocket_api.async_register_command(ignore_config_flow) @@ -231,20 +230,6 @@ def config_entries_progress(hass, connection, msg): ) -@websocket_api.require_admin -@websocket_api.async_response -@websocket_api.websocket_command( - {"type": "config_entries/system_options/list", "entry_id": str} -) -async def system_options_list(hass, connection, msg): - """List all system options for a config entry.""" - entry_id = msg["entry_id"] - entry = hass.config_entries.async_get_entry(entry_id) - - if entry: - connection.send_result(msg["id"], entry.system_options.as_dict()) - - def send_entry_not_found(connection, msg_id): """Send Config entry not found error.""" connection.send_error( @@ -267,6 +252,7 @@ def get_entry(hass, connection, entry_id, msg_id): "type": "config_entries/system_options/update", "entry_id": str, vol.Optional("disable_new_entities"): bool, + vol.Optional("disable_polling"): bool, } ) async def system_options_update(hass, connection, msg): @@ -280,8 +266,25 @@ async def system_options_update(hass, connection, msg): if entry is None: return + old_disable_polling = entry.system_options.disable_polling + hass.config_entries.async_update_entry(entry, system_options=changes) - connection.send_result(msg["id"], entry.system_options.as_dict()) + + result = { + "system_options": entry.system_options.as_dict(), + "require_restart": False, + } + + if ( + old_disable_polling != entry.system_options.disable_polling + and entry.state is config_entries.ConfigEntryState.LOADED + ): + if not await hass.config_entries.async_reload(entry.entry_id): + result["require_restart"] = ( + entry.state is config_entries.ConfigEntryState.FAILED_UNLOAD + ) + + connection.send_result(msg["id"], result) @websocket_api.require_admin @@ -388,6 +391,7 @@ def entry_json(entry: config_entries.ConfigEntry) -> dict: "state": entry.state.value, "supports_options": supports_options, "supports_unload": entry.supports_unload, + "system_options": entry.system_options.as_dict(), "disabled_by": entry.disabled_by, "reason": entry.reason, } diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index b3589d03b92..33c18fc0d7c 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -994,12 +994,10 @@ class ConfigEntries: changed = True entry.options = MappingProxyType(options) - if ( - system_options is not UNDEFINED - and entry.system_options.as_dict() != system_options - ): - changed = True + if system_options is not UNDEFINED: + old_system_options = entry.system_options.as_dict() entry.system_options.update(**system_options) + changed = entry.system_options.as_dict() != old_system_options if not changed: return False @@ -1408,14 +1406,27 @@ class SystemOptions: """Config entry system options.""" disable_new_entities: bool = attr.ib(default=False) + disable_polling: bool = attr.ib(default=False) - def update(self, *, disable_new_entities: bool) -> None: + def update( + self, + *, + disable_new_entities: bool | UndefinedType = UNDEFINED, + disable_polling: bool | UndefinedType = UNDEFINED, + ) -> None: """Update properties.""" - self.disable_new_entities = disable_new_entities + if disable_new_entities is not UNDEFINED: + self.disable_new_entities = disable_new_entities + + if disable_polling is not UNDEFINED: + self.disable_polling = disable_polling def as_dict(self) -> dict[str, Any]: """Return dictionary version of this config entries system options.""" - return {"disable_new_entities": self.disable_new_entities} + return { + "disable_new_entities": self.disable_new_entities, + "disable_polling": self.disable_polling, + } class EntityRegistryDisabledHandler: diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 26bfdb43e66..f0d691a1c8d 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -214,6 +214,7 @@ class EntityPlatform: @callback def async_create_setup_task() -> Coroutine: """Get task to set up platform.""" + config_entries.current_entry.set(config_entry) return platform.async_setup_entry( # type: ignore[no-any-return,union-attr] self.hass, config_entry, self._async_schedule_add_entities ) @@ -395,8 +396,10 @@ class EntityPlatform: ) raise - if self._async_unsub_polling is not None or not any( - entity.should_poll for entity in self.entities.values() + if ( + (self.config_entry and self.config_entry.system_options.disable_polling) + or self._async_unsub_polling is not None + or not any(entity.should_poll for entity in self.entities.values()) ): return diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index c15d6534626..e91acfaf82f 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -49,6 +49,7 @@ class DataUpdateCoordinator(Generic[T]): self.name = name self.update_method = update_method self.update_interval = update_interval + self.config_entry = config_entries.current_entry.get() # It's None before the first successful update. # Components should call async_config_entry_first_refresh @@ -110,6 +111,9 @@ class DataUpdateCoordinator(Generic[T]): if self.update_interval is None: return + if self.config_entry and self.config_entry.system_options.disable_polling: + return + if self._unsub_refresh: self._unsub_refresh() self._unsub_refresh = None @@ -229,9 +233,8 @@ class DataUpdateCoordinator(Generic[T]): if raise_on_auth_failed: raise - config_entry = config_entries.current_entry.get() - if config_entry: - config_entry.async_start_reauth(self.hass) + if self.config_entry: + self.config_entry.async_start_reauth(self.hass) except NotImplementedError as err: self.last_exception = err raise err diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 10fc3aadba0..570d847e86e 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -87,6 +87,10 @@ async def test_get_entries(hass, client): "state": core_ce.ConfigEntryState.NOT_LOADED.value, "supports_options": True, "supports_unload": True, + "system_options": { + "disable_new_entities": False, + "disable_polling": False, + }, "disabled_by": None, "reason": None, }, @@ -97,6 +101,10 @@ async def test_get_entries(hass, client): "state": core_ce.ConfigEntryState.SETUP_ERROR.value, "supports_options": False, "supports_unload": False, + "system_options": { + "disable_new_entities": False, + "disable_polling": False, + }, "disabled_by": None, "reason": "Unsupported API", }, @@ -107,6 +115,10 @@ async def test_get_entries(hass, client): "state": core_ce.ConfigEntryState.NOT_LOADED.value, "supports_options": False, "supports_unload": False, + "system_options": { + "disable_new_entities": False, + "disable_polling": False, + }, "disabled_by": core_ce.DISABLED_USER, "reason": None, }, @@ -328,6 +340,10 @@ async def test_create_account(hass, client, enable_custom_integrations): "state": core_ce.ConfigEntryState.LOADED.value, "supports_options": False, "supports_unload": False, + "system_options": { + "disable_new_entities": False, + "disable_polling": False, + }, "title": "Test Entry", "reason": None, }, @@ -399,6 +415,10 @@ async def test_two_step_flow(hass, client, enable_custom_integrations): "state": core_ce.ConfigEntryState.LOADED.value, "supports_options": False, "supports_unload": False, + "system_options": { + "disable_new_entities": False, + "disable_polling": False, + }, "title": "user-title", "reason": None, }, @@ -678,35 +698,17 @@ async def test_two_step_options_flow(hass, client): } -async def test_list_system_options(hass, hass_ws_client): - """Test that we can list an entries system options.""" - assert await async_setup_component(hass, "config", {}) - ws_client = await hass_ws_client(hass) - - entry = MockConfigEntry(domain="demo") - entry.add_to_hass(hass) - - await ws_client.send_json( - { - "id": 5, - "type": "config_entries/system_options/list", - "entry_id": entry.entry_id, - } - ) - response = await ws_client.receive_json() - - assert response["success"] - assert response["result"] == {"disable_new_entities": False} - - async def test_update_system_options(hass, hass_ws_client): """Test that we can update system options.""" assert await async_setup_component(hass, "config", {}) ws_client = await hass_ws_client(hass) - entry = MockConfigEntry(domain="demo") + entry = MockConfigEntry(domain="demo", state=core_ce.ConfigEntryState.LOADED) entry.add_to_hass(hass) + assert entry.system_options.disable_new_entities is False + assert entry.system_options.disable_polling is False + await ws_client.send_json( { "id": 5, @@ -718,8 +720,31 @@ async def test_update_system_options(hass, hass_ws_client): response = await ws_client.receive_json() assert response["success"] - assert response["result"]["disable_new_entities"] - assert entry.system_options.disable_new_entities + assert response["result"] == { + "require_restart": False, + "system_options": {"disable_new_entities": True, "disable_polling": False}, + } + assert entry.system_options.disable_new_entities is True + assert entry.system_options.disable_polling is False + + await ws_client.send_json( + { + "id": 6, + "type": "config_entries/system_options/update", + "entry_id": entry.entry_id, + "disable_new_entities": False, + "disable_polling": True, + } + ) + response = await ws_client.receive_json() + + assert response["success"] + assert response["result"] == { + "require_restart": True, + "system_options": {"disable_new_entities": False, "disable_polling": True}, + } + assert entry.system_options.disable_new_entities is False + assert entry.system_options.disable_polling is True async def test_update_system_options_nonexisting(hass, hass_ws_client): diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 6675e441adf..944f02d46c0 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -57,6 +57,19 @@ async def test_polling_only_updates_entities_it_should_poll(hass): assert poll_ent.async_update.called +async def test_polling_disabled_by_config_entry(hass): + """Test the polling of only updated entities.""" + entity_platform = MockEntityPlatform(hass) + entity_platform.config_entry = MockConfigEntry( + system_options={"disable_polling": True} + ) + + poll_ent = MockEntity(should_poll=True) + + await entity_platform.async_add_entities([poll_ent]) + assert entity_platform._async_unsub_polling is None + + async def test_polling_updates_entities_with_exception(hass): """Test the updated entities that not break with an exception.""" component = EntityComponent(_LOGGER, DOMAIN, hass, timedelta(seconds=20)) diff --git a/tests/helpers/test_update_coordinator.py b/tests/helpers/test_update_coordinator.py index 244e221f53a..a0ce751aed8 100644 --- a/tests/helpers/test_update_coordinator.py +++ b/tests/helpers/test_update_coordinator.py @@ -9,13 +9,14 @@ import aiohttp import pytest import requests +from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import CoreState from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import update_coordinator from homeassistant.util.dt import utcnow -from tests.common import async_fire_time_changed +from tests.common import MockConfigEntry, async_fire_time_changed _LOGGER = logging.getLogger(__name__) @@ -371,3 +372,12 @@ async def test_async_config_entry_first_refresh_success(crd, caplog): await crd.async_config_entry_first_refresh() assert crd.last_update_success is True + + +async def test_not_schedule_refresh_if_system_option_disable_polling(hass): + """Test we do not schedule a refresh if disable polling in config entry.""" + entry = MockConfigEntry(system_options={"disable_polling": True}) + config_entries.current_entry.set(entry) + crd = get_crd(hass, DEFAULT_UPDATE_INTERVAL) + crd.async_add_listener(lambda: None) + assert crd._unsub_refresh is None From 1e2913ad4cd738f665493acb3dc608f0a00b70e9 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 31 May 2021 23:35:33 +0200 Subject: [PATCH 038/123] Fix stream profiles not available as expected (#51305) --- homeassistant/components/axis/config_flow.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py index 8753114d86e..d313e4b2745 100644 --- a/homeassistant/components/axis/config_flow.py +++ b/homeassistant/components/axis/config_flow.py @@ -243,8 +243,7 @@ class AxisOptionsFlowHandler(config_entries.OptionsFlow): # Stream profiles - if vapix.params.stream_profiles_max_groups > 0: - + if vapix.stream_profiles or vapix.params.stream_profiles_max_groups > 0: stream_profiles = [DEFAULT_STREAM_PROFILE] for profile in vapix.streaming_profiles: stream_profiles.append(profile.name) From cbc75ffe8a01fb1c57e2d0b3167033f345bbd50b Mon Sep 17 00:00:00 2001 From: Jc2k Date: Mon, 31 May 2021 23:28:14 +0100 Subject: [PATCH 039/123] Bump aiohomekit to 0.2.66 (#51310) --- 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 46fe126ebf0..c18ee9e574f 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==0.2.65"], + "requirements": ["aiohomekit==0.2.66"], "zeroconf": ["_hap._tcp.local."], "after_dependencies": ["zeroconf"], "codeowners": ["@Jc2k", "@bdraco"], diff --git a/requirements_all.txt b/requirements_all.txt index 3ca655541aa..3d27fa83bb1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -175,7 +175,7 @@ aioguardian==1.0.4 aioharmony==0.2.7 # homeassistant.components.homekit_controller -aiohomekit==0.2.65 +aiohomekit==0.2.66 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a42813bf174..04eaa89b32a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -112,7 +112,7 @@ aioguardian==1.0.4 aioharmony==0.2.7 # homeassistant.components.homekit_controller -aiohomekit==0.2.65 +aiohomekit==0.2.66 # homeassistant.components.emulated_hue # homeassistant.components.http From 14db5a0999ce106a7855f4d95e5a664fa30d3f73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 1 Jun 2021 00:32:03 +0200 Subject: [PATCH 040/123] Move version validation to resolver (#51311) --- homeassistant/loader.py | 60 +++++++++---------- tests/test_loader.py | 57 +++++++----------- .../test_bad_version/__init__.py | 1 + .../test_bad_version/manifest.json | 4 ++ .../test_no_version/__init__.py | 1 + .../test_no_version/manifest.json | 3 + 6 files changed, 58 insertions(+), 68 deletions(-) create mode 100644 tests/testing_config/custom_components/test_bad_version/__init__.py create mode 100644 tests/testing_config/custom_components/test_bad_version/manifest.json create mode 100644 tests/testing_config/custom_components/test_no_version/__init__.py create mode 100644 tests/testing_config/custom_components/test_no_version/manifest.json diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 444e35add33..06bf5045c9f 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -47,7 +47,7 @@ DATA_CUSTOM_COMPONENTS = "custom_components" PACKAGE_CUSTOM_COMPONENTS = "custom_components" PACKAGE_BUILTIN = "homeassistant.components" CUSTOM_WARNING = ( - "You are using a custom integration %s which has not " + "We found a custom integration %s which has not " "been tested by Home Assistant. This component might " "cause stability problems, be sure to disable it if you " "experience issues with Home Assistant" @@ -290,13 +290,39 @@ class Integration: ) continue - return cls( + integration = cls( hass, f"{root_module.__name__}.{domain}", manifest_path.parent, manifest, ) + if integration.is_built_in: + return integration + + _LOGGER.warning(CUSTOM_WARNING, integration.domain) + try: + AwesomeVersion( + integration.version, + [ + AwesomeVersionStrategy.CALVER, + AwesomeVersionStrategy.SEMVER, + AwesomeVersionStrategy.SIMPLEVER, + AwesomeVersionStrategy.BUILDVER, + AwesomeVersionStrategy.PEP440, + ], + ) + except AwesomeVersionException: + _LOGGER.error( + "The custom integration '%s' does not have a " + "valid version key (%s) in the manifest file and was blocked from loading. " + "See https://developers.home-assistant.io/blog/2021/01/29/custom-integration-changes#versions for more details", + integration.domain, + integration.version, + ) + return None + return integration + return None def __init__( @@ -523,8 +549,6 @@ async def _async_get_integration(hass: HomeAssistant, domain: str) -> Integratio # Instead of using resolve_from_root we use the cache of custom # components to find the integration. if integration := (await async_get_custom_components(hass)).get(domain): - validate_custom_integration_version(integration) - _LOGGER.warning(CUSTOM_WARNING, integration.domain) return integration from homeassistant import components # pylint: disable=import-outside-toplevel @@ -744,31 +768,3 @@ def _lookup_path(hass: HomeAssistant) -> list[str]: if hass.config.safe_mode: return [PACKAGE_BUILTIN] return [PACKAGE_CUSTOM_COMPONENTS, PACKAGE_BUILTIN] - - -def validate_custom_integration_version(integration: Integration) -> None: - """ - Validate the version of custom integrations. - - Raises IntegrationNotFound when version is missing or not valid - """ - try: - AwesomeVersion( - integration.version, - [ - AwesomeVersionStrategy.CALVER, - AwesomeVersionStrategy.SEMVER, - AwesomeVersionStrategy.SIMPLEVER, - AwesomeVersionStrategy.BUILDVER, - AwesomeVersionStrategy.PEP440, - ], - ) - except AwesomeVersionException: - _LOGGER.error( - "The custom integration '%s' does not have a " - "valid version key (%s) in the manifest file and was blocked from loading. " - "See https://developers.home-assistant.io/blog/2021/01/29/custom-integration-changes#versions for more details", - integration.domain, - integration.version, - ) - raise IntegrationNotFound(integration.domain) from None diff --git a/tests/test_loader.py b/tests/test_loader.py index e696f27351d..20dcf90d90e 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -127,37 +127,30 @@ async def test_log_warning_custom_component(hass, caplog, enable_custom_integrat """Test that we log a warning when loading a custom component.""" await loader.async_get_integration(hass, "test_package") - assert "You are using a custom integration test_package" in caplog.text + assert "We found a custom integration test_package" in caplog.text await loader.async_get_integration(hass, "test") - assert "You are using a custom integration test " in caplog.text + assert "We found a custom integration test " in caplog.text -async def test_custom_integration_version_not_valid(hass, caplog): +async def test_custom_integration_version_not_valid( + hass, caplog, enable_custom_integrations +): """Test that we log a warning when custom integrations have a invalid version.""" - test_integration1 = loader.Integration( - hass, "custom_components.test", None, {"domain": "test1", "version": "test"} - ) - test_integration2 = loader.Integration( - hass, "custom_components.test", None, {"domain": "test2"} + with pytest.raises(loader.IntegrationNotFound): + await loader.async_get_integration(hass, "test_no_version") + + assert ( + "The custom integration 'test_no_version' does not have a valid version key (None) in the manifest file and was blocked from loading." + in caplog.text ) - with patch("homeassistant.loader.async_get_custom_components") as mock_get: - mock_get.return_value = {"test1": test_integration1, "test2": test_integration2} - - with pytest.raises(loader.IntegrationNotFound): - await loader.async_get_integration(hass, "test1") - assert ( - "The custom integration 'test1' does not have a valid version key (test) in the manifest file and was blocked from loading." - in caplog.text - ) - - with pytest.raises(loader.IntegrationNotFound): - await loader.async_get_integration(hass, "test2") - assert ( - "The custom integration 'test2' does not have a valid version key (None) in the manifest file and was blocked from loading." - in caplog.text - ) + with pytest.raises(loader.IntegrationNotFound): + await loader.async_get_integration(hass, "test2") + assert ( + "The custom integration 'test_bad_version' does not have a valid version key (bad) in the manifest file and was blocked from loading." + in caplog.text + ) async def test_get_integration(hass): @@ -471,19 +464,11 @@ async def test_get_custom_components_safe_mode(hass): async def test_custom_integration_missing_version(hass, caplog): """Test trying to load a custom integration without a version twice does not deadlock.""" - test_integration_1 = loader.Integration( - hass, "custom_components.test1", None, {"domain": "test1"} - ) - with patch("homeassistant.loader.async_get_custom_components") as mock_get: - mock_get.return_value = { - "test1": test_integration_1, - } + with pytest.raises(loader.IntegrationNotFound): + await loader.async_get_integration(hass, "test_no_version") - with pytest.raises(loader.IntegrationNotFound): - await loader.async_get_integration(hass, "test1") - - with pytest.raises(loader.IntegrationNotFound): - await loader.async_get_integration(hass, "test1") + with pytest.raises(loader.IntegrationNotFound): + await loader.async_get_integration(hass, "test_no_version") async def test_custom_integration_missing(hass, caplog): diff --git a/tests/testing_config/custom_components/test_bad_version/__init__.py b/tests/testing_config/custom_components/test_bad_version/__init__.py new file mode 100644 index 00000000000..e39053682e3 --- /dev/null +++ b/tests/testing_config/custom_components/test_bad_version/__init__.py @@ -0,0 +1 @@ +"""Provide a mock integration.""" diff --git a/tests/testing_config/custom_components/test_bad_version/manifest.json b/tests/testing_config/custom_components/test_bad_version/manifest.json new file mode 100644 index 00000000000..69d322a33ad --- /dev/null +++ b/tests/testing_config/custom_components/test_bad_version/manifest.json @@ -0,0 +1,4 @@ +{ + "domain": "test_bad_version", + "version": "bad" +} \ No newline at end of file diff --git a/tests/testing_config/custom_components/test_no_version/__init__.py b/tests/testing_config/custom_components/test_no_version/__init__.py new file mode 100644 index 00000000000..e39053682e3 --- /dev/null +++ b/tests/testing_config/custom_components/test_no_version/__init__.py @@ -0,0 +1 @@ +"""Provide a mock integration.""" diff --git a/tests/testing_config/custom_components/test_no_version/manifest.json b/tests/testing_config/custom_components/test_no_version/manifest.json new file mode 100644 index 00000000000..9054cf4f5e3 --- /dev/null +++ b/tests/testing_config/custom_components/test_no_version/manifest.json @@ -0,0 +1,3 @@ +{ + "domain": "test_no_version" +} \ No newline at end of file From a904b1e37fade819eb0020336785900baa2b4b1b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 31 May 2021 16:35:31 -0700 Subject: [PATCH 041/123] Set up cloud semi-dependencies at start (#51313) --- .../components/cloud/alexa_config.py | 10 +++-- .../components/cloud/google_config.py | 9 +++-- homeassistant/core.py | 4 +- homeassistant/helpers/start.py | 25 ++++++++++++ tests/components/cloud/test_google_config.py | 1 + tests/helpers/test_start.py | 39 +++++++++++++++++++ 6 files changed, 80 insertions(+), 8 deletions(-) create mode 100644 homeassistant/helpers/start.py create mode 100644 tests/helpers/test_start.py diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py index c7568d7ae25..7394936f355 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -17,7 +17,7 @@ from homeassistant.components.alexa import ( ) from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES, HTTP_BAD_REQUEST from homeassistant.core import HomeAssistant, callback, split_entity_id -from homeassistant.helpers import entity_registry +from homeassistant.helpers import entity_registry, start from homeassistant.helpers.event import async_call_later from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow @@ -107,8 +107,12 @@ class AlexaConfig(alexa_config.AbstractConfig): async def async_initialize(self): """Initialize the Alexa config.""" - if self.enabled and ALEXA_DOMAIN not in self.hass.config.components: - await async_setup_component(self.hass, ALEXA_DOMAIN, {}) + + async def hass_started(hass): + if self.enabled and ALEXA_DOMAIN not in self.hass.config.components: + await async_setup_component(self.hass, ALEXA_DOMAIN, {}) + + start.async_at_start(self.hass, hass_started) def should_expose(self, entity_id): """If an entity should be exposed.""" diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index 41f62c32c39..65cbe8bb342 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -9,7 +9,7 @@ from homeassistant.components.google_assistant.const import DOMAIN as GOOGLE_DOM from homeassistant.components.google_assistant.helpers import AbstractConfig from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES, HTTP_OK from homeassistant.core import CoreState, split_entity_id -from homeassistant.helpers import entity_registry +from homeassistant.helpers import entity_registry, start from homeassistant.setup import async_setup_component from .const import ( @@ -86,8 +86,11 @@ class CloudGoogleConfig(AbstractConfig): """Perform async initialization of config.""" await super().async_initialize() - if self.enabled and GOOGLE_DOMAIN not in self.hass.config.components: - await async_setup_component(self.hass, GOOGLE_DOMAIN, {}) + async def hass_started(hass): + if self.enabled and GOOGLE_DOMAIN not in self.hass.config.components: + await async_setup_component(self.hass, GOOGLE_DOMAIN, {}) + + start.async_at_start(self.hass, hass_started) # Remove old/wrong user agent ids remove_agent_user_ids = [] diff --git a/homeassistant/core.py b/homeassistant/core.py index 7b5c93b15bb..b9bf97e7e6c 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -374,7 +374,7 @@ class HomeAssistant: return task - def create_task(self, target: Coroutine) -> None: + def create_task(self, target: Awaitable) -> None: """Add task to the executor pool. target: target to call. @@ -382,7 +382,7 @@ class HomeAssistant: self.loop.call_soon_threadsafe(self.async_create_task, target) @callback - def async_create_task(self, target: Coroutine) -> asyncio.tasks.Task: + def async_create_task(self, target: Awaitable) -> asyncio.tasks.Task: """Create a task from within the eventloop. This method must be run in the event loop. diff --git a/homeassistant/helpers/start.py b/homeassistant/helpers/start.py new file mode 100644 index 00000000000..e7e827ec5c3 --- /dev/null +++ b/homeassistant/helpers/start.py @@ -0,0 +1,25 @@ +"""Helpers to help during startup.""" +from collections.abc import Awaitable +from typing import Callable + +from homeassistant.const import EVENT_HOMEASSISTANT_START +from homeassistant.core import Event, HomeAssistant, callback + + +@callback +def async_at_start( + hass: HomeAssistant, at_start_cb: Callable[[HomeAssistant], Awaitable] +) -> None: + """Execute something when Home Assistant is started. + + Will execute it now if Home Assistant is already started. + """ + if hass.is_running: + hass.async_create_task(at_start_cb(hass)) + return + + async def _matched_event(event: Event) -> None: + """Call the callback when Home Assistant started.""" + await at_start_cb(hass) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _matched_event) diff --git a/tests/components/cloud/test_google_config.py b/tests/components/cloud/test_google_config.py index bc430347e08..64d50250259 100644 --- a/tests/components/cloud/test_google_config.py +++ b/tests/components/cloud/test_google_config.py @@ -234,6 +234,7 @@ async def test_setup_integration(hass, mock_conf, cloud_prefs): assert "google_assistant" not in hass.config.components await mock_conf.async_initialize() + await hass.async_block_till_done() assert "google_assistant" in hass.config.components hass.config.components.remove("google_assistant") diff --git a/tests/helpers/test_start.py b/tests/helpers/test_start.py new file mode 100644 index 00000000000..35838f1ceaa --- /dev/null +++ b/tests/helpers/test_start.py @@ -0,0 +1,39 @@ +"""Test starting HA helpers.""" +from homeassistant import core +from homeassistant.const import EVENT_HOMEASSISTANT_START +from homeassistant.helpers import start + + +async def test_at_start_when_running(hass): + """Test at start when already running.""" + assert hass.is_running + + calls = [] + + async def cb_at_start(hass): + """Home Assistant is started.""" + calls.append(1) + + start.async_at_start(hass, cb_at_start) + await hass.async_block_till_done() + assert len(calls) == 1 + + +async def test_at_start_when_starting(hass): + """Test at start when yet to start.""" + hass.state = core.CoreState.not_running + assert not hass.is_running + + calls = [] + + async def cb_at_start(hass): + """Home Assistant is started.""" + calls.append(1) + + start.async_at_start(hass, cb_at_start) + await hass.async_block_till_done() + assert len(calls) == 0 + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + assert len(calls) == 1 From 837efaf29b2860249cd30e30aac4c46295149aa3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 31 May 2021 16:35:08 -0700 Subject: [PATCH 042/123] Updated frontend to 20210531.1 (#51314) --- 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 61da986b06b..49b51a7864c 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210531.0" + "home-assistant-frontend==20210531.1" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e7e458efd70..80203972831 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ defusedxml==0.7.1 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.43.0 -home-assistant-frontend==20210531.0 +home-assistant-frontend==20210531.1 httpx==0.18.0 ifaddr==0.1.7 jinja2>=3.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index 3d27fa83bb1..3f895847641 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -765,7 +765,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210531.0 +home-assistant-frontend==20210531.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 04eaa89b32a..009e7313c57 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -429,7 +429,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210531.0 +home-assistant-frontend==20210531.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From bd279786bb36cfe6e633be61335f05f00c041957 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 31 May 2021 16:43:14 -0700 Subject: [PATCH 043/123] Bumped version to 2021.6.0b3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 76015e87360..6900c6c0b15 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 6 -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, 8, 0) From 413fd1b255061aff12277f7d07ca279fccc4ae5d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 1 Jun 2021 03:51:44 -0700 Subject: [PATCH 044/123] Trusted networks auth provider warns if detects a requests with x-forwarded-for header while the http integration is not configured for reverse proxies (#51319) * Trusted networks auth provider to require http integration configured for proxies to allow logging in with requests with x-forwarded-for header * Make it a warning --- .../auth/providers/trusted_networks.py | 111 +++++++++++++----- homeassistant/components/http/__init__.py | 1 + tests/auth/providers/test_trusted_networks.py | 74 ++++++++++-- 3 files changed, 146 insertions(+), 40 deletions(-) diff --git a/homeassistant/auth/providers/trusted_networks.py b/homeassistant/auth/providers/trusted_networks.py index 2f120e56652..e93587e91ca 100644 --- a/homeassistant/auth/providers/trusted_networks.py +++ b/homeassistant/auth/providers/trusted_networks.py @@ -14,10 +14,13 @@ from ipaddress import ( ip_address, ip_network, ) +import logging from typing import Any, Dict, List, Union, cast +from aiohttp import hdrs import voluptuous as vol +from homeassistant.components import http from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError @@ -86,40 +89,60 @@ class TrustedNetworksAuthProvider(AuthProvider): """Trusted Networks auth provider does not support MFA.""" return False + @callback + def is_allowed_request(self) -> bool: + """Return if it is an allowed request.""" + request = http.current_request.get() + if request is not None and ( + self.hass.http.use_x_forwarded_for + or hdrs.X_FORWARDED_FOR not in request.headers + ): + return True + + logging.getLogger(__name__).warning( + "A request contained an x-forwarded-for header but your HTTP integration is not set-up " + "for reverse proxies. This usually means that you have not configured your reverse proxy " + "correctly. This request will be blocked in Home Assistant 2021.7 unless you configure " + "your HTTP integration to allow this header." + ) + return True + async def async_login_flow(self, context: dict | None) -> LoginFlow: """Return a flow to login.""" assert context is not None + + if not self.is_allowed_request(): + return MisconfiguredTrustedNetworksLoginFlow(self) + ip_addr = cast(IPAddress, context.get("ip_address")) users = await self.store.async_get_users() available_users = [ user for user in users if not user.system_generated and user.is_active ] for ip_net, user_or_group_list in self.trusted_users.items(): - if ip_addr in ip_net: - user_list = [ - user_id - for user_id in user_or_group_list - if isinstance(user_id, str) - ] - group_list = [ - group[CONF_GROUP] - for group in user_or_group_list - if isinstance(group, dict) - ] - flattened_group_list = [ - group for sublist in group_list for group in sublist - ] - available_users = [ - user - for user in available_users - if ( - user.id in user_list - or any( - group.id in flattened_group_list for group in user.groups - ) - ) - ] - break + if ip_addr not in ip_net: + continue + + user_list = [ + user_id for user_id in user_or_group_list if isinstance(user_id, str) + ] + group_list = [ + group[CONF_GROUP] + for group in user_or_group_list + if isinstance(group, dict) + ] + flattened_group_list = [ + group for sublist in group_list for group in sublist + ] + available_users = [ + user + for user in available_users + if ( + user.id in user_list + or any(group.id in flattened_group_list for group in user.groups) + ) + ] + break return TrustedNetworksLoginFlow( self, @@ -136,13 +159,22 @@ class TrustedNetworksAuthProvider(AuthProvider): users = await self.store.async_get_users() for user in users: - if not user.system_generated and user.is_active and user.id == user_id: - for credential in await self.async_credentials(): - if credential.data["user_id"] == user_id: - return credential - cred = self.async_create_credentials({"user_id": user_id}) - await self.store.async_link_user(user, cred) - return cred + if user.id != user_id: + continue + + if user.system_generated: + continue + + if not user.is_active: + continue + + for credential in await self.async_credentials(): + if credential.data["user_id"] == user_id: + return credential + + cred = self.async_create_credentials({"user_id": user_id}) + await self.store.async_link_user(user, cred) + return cred # We only allow login as exist user raise InvalidUserError @@ -163,6 +195,11 @@ class TrustedNetworksAuthProvider(AuthProvider): Raise InvalidAuthError if not. Raise InvalidAuthError if trusted_networks is not configured. """ + if not self.is_allowed_request(): + raise InvalidAuthError( + "No request or it contains x-forwarded-for header and that's not allowed by configuration" + ) + if not self.trusted_networks: raise InvalidAuthError("trusted_networks is not configured") @@ -183,6 +220,16 @@ class TrustedNetworksAuthProvider(AuthProvider): self.async_validate_access(ip_address(remote_ip)) +class MisconfiguredTrustedNetworksLoginFlow(LoginFlow): + """Login handler for misconfigured trusted networks.""" + + async def async_step_init( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Handle the step of the form.""" + return self.async_abort(reason="forwared_for_header_not_allowed") + + class TrustedNetworksLoginFlow(LoginFlow): """Handler for the login flow.""" diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 8bd20e31628..db198cb334a 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -252,6 +252,7 @@ class HomeAssistantHTTP: self.ssl_key = ssl_key self.server_host = server_host self.server_port = server_port + self.use_x_forwarded_for = use_x_forwarded_for self.trusted_proxies = trusted_proxies self.is_ban_enabled = is_ban_enabled self.ssl_profile = ssl_profile diff --git a/tests/auth/providers/test_trusted_networks.py b/tests/auth/providers/test_trusted_networks.py index 39764fa4206..c68d6651e3c 100644 --- a/tests/auth/providers/test_trusted_networks.py +++ b/tests/auth/providers/test_trusted_networks.py @@ -5,9 +5,12 @@ from unittest.mock import Mock, patch import pytest import voluptuous as vol -from homeassistant import auth +from homeassistant import auth, const from homeassistant.auth import auth_store from homeassistant.auth.providers import trusted_networks as tn_auth +from homeassistant.setup import async_setup_component + +FORWARD_FOR_IS_WARNING = (const.MAJOR_VERSION, const.MINOR_VERSION) < (2021, 8) @pytest.fixture @@ -111,7 +114,17 @@ def manager_bypass_login(hass, store, provider_bypass_login): ) -async def test_trusted_networks_credentials(manager, provider): +@pytest.fixture +def mock_allowed_request(): + """Mock that the request is allowed.""" + with patch( + "homeassistant.auth.providers.trusted_networks.TrustedNetworksAuthProvider.is_allowed_request", + return_value=True, + ): + yield + + +async def test_trusted_networks_credentials(manager, provider, mock_allowed_request): """Test trusted_networks credentials related functions.""" owner = await manager.async_create_user("test-owner") tn_owner_cred = await provider.async_get_or_create_credentials({"user": owner.id}) @@ -128,7 +141,7 @@ async def test_trusted_networks_credentials(manager, provider): await provider.async_get_or_create_credentials({"user": "invalid-user"}) -async def test_validate_access(provider): +async def test_validate_access(provider, mock_allowed_request): """Test validate access from trusted networks.""" provider.async_validate_access(ip_address("192.168.0.1")) provider.async_validate_access(ip_address("192.168.128.10")) @@ -143,7 +156,7 @@ async def test_validate_access(provider): provider.async_validate_access(ip_address("2001:db8::ff00:42:8329")) -async def test_validate_refresh_token(provider): +async def test_validate_refresh_token(provider, mock_allowed_request): """Verify re-validation of refresh token.""" with patch.object(provider, "async_validate_access") as mock: with pytest.raises(tn_auth.InvalidAuthError): @@ -153,7 +166,7 @@ async def test_validate_refresh_token(provider): mock.assert_called_once_with(ip_address("127.0.0.1")) -async def test_login_flow(manager, provider): +async def test_login_flow(manager, provider, mock_allowed_request): """Test login flow.""" owner = await manager.async_create_user("test-owner") user = await manager.async_create_user("test-user") @@ -180,7 +193,9 @@ async def test_login_flow(manager, provider): assert step["data"]["user"] == user.id -async def test_trusted_users_login(manager_with_user, provider_with_user): +async def test_trusted_users_login( + manager_with_user, provider_with_user, mock_allowed_request +): """Test available user list changed per different IP.""" owner = await manager_with_user.async_create_user("test-owner") sys_user = await manager_with_user.async_create_system_user( @@ -260,7 +275,9 @@ async def test_trusted_users_login(manager_with_user, provider_with_user): assert schema({"user": sys_user.id}) -async def test_trusted_group_login(manager_with_user, provider_with_user): +async def test_trusted_group_login( + manager_with_user, provider_with_user, mock_allowed_request +): """Test config trusted_user with group_id.""" owner = await manager_with_user.async_create_user("test-owner") # create a user in user group @@ -313,7 +330,9 @@ async def test_trusted_group_login(manager_with_user, provider_with_user): assert schema({"user": user.id}) -async def test_bypass_login_flow(manager_bypass_login, provider_bypass_login): +async def test_bypass_login_flow( + manager_bypass_login, provider_bypass_login, mock_allowed_request +): """Test login flow can be bypass if only one user available.""" owner = await manager_bypass_login.async_create_user("test-owner") @@ -344,3 +363,42 @@ async def test_bypass_login_flow(manager_bypass_login, provider_bypass_login): # both owner and user listed assert schema({"user": owner.id}) assert schema({"user": user.id}) + + +async def test_allowed_request(hass, provider, current_request, caplog): + """Test allowing requests.""" + assert await async_setup_component(hass, "http", {}) + + provider.async_validate_access(ip_address("192.168.0.1")) + + current_request.get.return_value = current_request.get.return_value.clone( + headers={ + **current_request.get.return_value.headers, + "x-forwarded-for": "1.2.3.4", + } + ) + + if FORWARD_FOR_IS_WARNING: + caplog.clear() + provider.async_validate_access(ip_address("192.168.0.1")) + assert "This request will be blocked" in caplog.text + else: + with pytest.raises(tn_auth.InvalidAuthError): + provider.async_validate_access(ip_address("192.168.0.1")) + + hass.http.use_x_forwarded_for = True + + provider.async_validate_access(ip_address("192.168.0.1")) + + +@pytest.mark.skipif(FORWARD_FOR_IS_WARNING, reason="Currently a warning") +async def test_login_flow_no_request(provider): + """Test getting a login flow.""" + login_flow = await provider.async_login_flow({"ip_address": ip_address("1.1.1.1")}) + assert await login_flow.async_step_init() == { + "description_placeholders": None, + "flow_id": None, + "handler": None, + "reason": "forwared_for_header_not_allowed", + "type": "abort", + } From 0856232ea629a2d1670f2229766905d450d450cd Mon Sep 17 00:00:00 2001 From: Eugenio Panadero Date: Tue, 1 Jun 2021 09:45:37 +0200 Subject: [PATCH 045/123] Bump aiopvpc to apply quickfix for new electricity price tariff (#51320) Since 2021-06-01, the three PVPC price tariffs become one and only: '2.0 TD', and the JSON schema from the official API (data source of this integration) is slightly different. This patch allows a no-pain jump between the old tariffs and the new one. --- homeassistant/components/pvpc_hourly_pricing/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/pvpc_hourly_pricing/manifest.json b/homeassistant/components/pvpc_hourly_pricing/manifest.json index c39d66163e0..bbbe18350c8 100644 --- a/homeassistant/components/pvpc_hourly_pricing/manifest.json +++ b/homeassistant/components/pvpc_hourly_pricing/manifest.json @@ -3,7 +3,7 @@ "name": "Spain electricity hourly pricing (PVPC)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/pvpc_hourly_pricing", - "requirements": ["aiopvpc==2.1.1"], + "requirements": ["aiopvpc==2.1.2"], "codeowners": ["@azogue"], "quality_scale": "platinum", "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 3f895847641..9ca111d71ff 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -218,7 +218,7 @@ aiopulse==0.4.2 aiopvapi==1.6.14 # homeassistant.components.pvpc_hourly_pricing -aiopvpc==2.1.1 +aiopvpc==2.1.2 # homeassistant.components.webostv aiopylgtv==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 009e7313c57..1ee977e31f1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -140,7 +140,7 @@ aiopulse==0.4.2 aiopvapi==1.6.14 # homeassistant.components.pvpc_hourly_pricing -aiopvpc==2.1.1 +aiopvpc==2.1.2 # homeassistant.components.webostv aiopylgtv==0.4.0 From 96191c07c9e5204c18b21b4b07aaaaaf514ffef3 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 1 Jun 2021 10:41:34 +0200 Subject: [PATCH 046/123] Fix exception after removing Shelly config entry and stopping HA (#51321) * Fix device shutdown twice * Change if logic --- homeassistant/components/shelly/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index d2c56217afe..8fc7cf6be23 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -288,8 +288,10 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator): def shutdown(self): """Shutdown the wrapper.""" - self.device.shutdown() - self._async_remove_device_updates_handler() + if self.device: + self.device.shutdown() + self._async_remove_device_updates_handler() + self.device = None @callback def _handle_ha_stop(self, _): From 42bf29856e5da45996984f1bd0ff60991f8f230e Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 1 Jun 2021 12:38:49 +0200 Subject: [PATCH 047/123] Update frontend to 20210601.0 (#51329) --- 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 49b51a7864c..0e0685cd772 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210531.1" + "home-assistant-frontend==20210601.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 80203972831..4aa67a73e14 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ defusedxml==0.7.1 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.43.0 -home-assistant-frontend==20210531.1 +home-assistant-frontend==20210601.0 httpx==0.18.0 ifaddr==0.1.7 jinja2>=3.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index 9ca111d71ff..1b280e1a9a7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -765,7 +765,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210531.1 +home-assistant-frontend==20210601.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1ee977e31f1..2ba64795d0b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -429,7 +429,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210531.1 +home-assistant-frontend==20210601.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 76527ab79a54c910e7852f081dc1577c83b6fe44 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 1 Jun 2021 13:13:34 +0200 Subject: [PATCH 048/123] Bumped version to 2021.6.0b4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 6900c6c0b15..c1364fc5596 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 6 -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, 8, 0) From fac5b23b86325b7cf1567b9c23e40239b851e968 Mon Sep 17 00:00:00 2001 From: AJ Schmidt Date: Tue, 1 Jun 2021 02:44:56 -0400 Subject: [PATCH 049/123] update adext dependency (#51315) --- homeassistant/components/alarmdecoder/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/alarmdecoder/manifest.json b/homeassistant/components/alarmdecoder/manifest.json index fa2bcca389f..a762d698545 100644 --- a/homeassistant/components/alarmdecoder/manifest.json +++ b/homeassistant/components/alarmdecoder/manifest.json @@ -2,7 +2,7 @@ "domain": "alarmdecoder", "name": "AlarmDecoder", "documentation": "https://www.home-assistant.io/integrations/alarmdecoder", - "requirements": ["adext==0.4.1"], + "requirements": ["adext==0.4.2"], "codeowners": ["@ajschmidt8"], "config_flow": true, "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 1b280e1a9a7..19afc85cdc9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -108,7 +108,7 @@ adafruit-circuitpython-mcp230xx==2.2.2 adb-shell[async]==0.3.1 # homeassistant.components.alarmdecoder -adext==0.4.1 +adext==0.4.2 # homeassistant.components.adguard adguardhome==0.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2ba64795d0b..243d061224d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -51,7 +51,7 @@ accuweather==0.2.0 adb-shell[async]==0.3.1 # homeassistant.components.alarmdecoder -adext==0.4.1 +adext==0.4.2 # homeassistant.components.adguard adguardhome==0.5.0 From 6031f7ce992397338510f1173e2b7c36f9c53212 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 1 Jun 2021 15:09:23 +0200 Subject: [PATCH 050/123] Add arch to payload (#51330) --- homeassistant/components/analytics/analytics.py | 2 ++ homeassistant/components/analytics/const.py | 1 + tests/components/analytics/test_analytics.py | 11 +++++++++-- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/analytics/analytics.py b/homeassistant/components/analytics/analytics.py index e6e7ffac337..571ffd90f22 100644 --- a/homeassistant/components/analytics/analytics.py +++ b/homeassistant/components/analytics/analytics.py @@ -21,6 +21,7 @@ from .const import ( ANALYTICS_ENDPOINT_URL_DEV, ATTR_ADDON_COUNT, ATTR_ADDONS, + ATTR_ARCH, ATTR_AUTO_UPDATE, ATTR_AUTOMATION_COUNT, ATTR_BASE, @@ -157,6 +158,7 @@ class Analytics: payload[ATTR_SUPERVISOR] = { ATTR_HEALTHY: supervisor_info[ATTR_HEALTHY], ATTR_SUPPORTED: supervisor_info[ATTR_SUPPORTED], + ATTR_ARCH: supervisor_info[ATTR_ARCH], } if operating_system_info.get(ATTR_BOARD) is not None: diff --git a/homeassistant/components/analytics/const.py b/homeassistant/components/analytics/const.py index 16929a7131d..4688c578a00 100644 --- a/homeassistant/components/analytics/const.py +++ b/homeassistant/components/analytics/const.py @@ -16,6 +16,7 @@ LOGGER: logging.Logger = logging.getLogger(__package__) ATTR_ADDON_COUNT = "addon_count" ATTR_ADDONS = "addons" +ATTR_ARCH = "arch" ATTR_AUTO_UPDATE = "auto_update" ATTR_AUTOMATION_COUNT = "automation_count" ATTR_BASE = "base" diff --git a/tests/components/analytics/test_analytics.py b/tests/components/analytics/test_analytics.py index 09f82e37fba..ee67a7e3935 100644 --- a/tests/components/analytics/test_analytics.py +++ b/tests/components/analytics/test_analytics.py @@ -132,7 +132,9 @@ async def test_send_base_with_supervisor(hass, caplog, aioclient_mock): with patch( "homeassistant.components.hassio.get_supervisor_info", - side_effect=Mock(return_value={"supported": True, "healthy": True}), + side_effect=Mock( + return_value={"supported": True, "healthy": True, "arch": "amd64"} + ), ), patch( "homeassistant.components.hassio.get_os_info", side_effect=Mock(return_value={"board": "blue", "version": "123"}), @@ -157,7 +159,10 @@ async def test_send_base_with_supervisor(hass, caplog, aioclient_mock): assert f"'uuid': '{MOCK_UUID}'" in caplog.text assert f"'version': '{MOCK_VERSION}'" in caplog.text - assert "'supervisor': {'healthy': True, 'supported': True}" in caplog.text + assert ( + "'supervisor': {'healthy': True, 'supported': True, 'arch': 'amd64'}" + in caplog.text + ) assert "'operating_system': {'board': 'blue', 'version': '123'}" in caplog.text assert "'installation_type':" in caplog.text assert "'integration_count':" not in caplog.text @@ -197,6 +202,7 @@ async def test_send_usage_with_supervisor(hass, caplog, aioclient_mock): return_value={ "healthy": True, "supported": True, + "arch": "amd64", "addons": [{"slug": "test_addon"}], } ), @@ -303,6 +309,7 @@ async def test_send_statistics_with_supervisor(hass, caplog, aioclient_mock): return_value={ "healthy": True, "supported": True, + "arch": "amd64", "addons": [{"slug": "test_addon"}], } ), From f54cbff223e55f2186a0b04246260ceb086779b2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 1 Jun 2021 18:38:55 +0200 Subject: [PATCH 051/123] Always load middle to handle forwarded proxy data (#51332) --- .../auth/providers/trusted_networks.py | 40 ---------- homeassistant/components/http/__init__.py | 6 +- homeassistant/components/http/forwarded.py | 36 +++++++-- tests/auth/providers/test_trusted_networks.py | 74 ++----------------- tests/components/http/test_auth.py | 2 +- tests/components/http/test_forwarded.py | 63 +++++++++++----- 6 files changed, 85 insertions(+), 136 deletions(-) diff --git a/homeassistant/auth/providers/trusted_networks.py b/homeassistant/auth/providers/trusted_networks.py index e93587e91ca..fd2014667f8 100644 --- a/homeassistant/auth/providers/trusted_networks.py +++ b/homeassistant/auth/providers/trusted_networks.py @@ -14,13 +14,10 @@ from ipaddress import ( ip_address, ip_network, ) -import logging from typing import Any, Dict, List, Union, cast -from aiohttp import hdrs import voluptuous as vol -from homeassistant.components import http from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError @@ -89,31 +86,9 @@ class TrustedNetworksAuthProvider(AuthProvider): """Trusted Networks auth provider does not support MFA.""" return False - @callback - def is_allowed_request(self) -> bool: - """Return if it is an allowed request.""" - request = http.current_request.get() - if request is not None and ( - self.hass.http.use_x_forwarded_for - or hdrs.X_FORWARDED_FOR not in request.headers - ): - return True - - logging.getLogger(__name__).warning( - "A request contained an x-forwarded-for header but your HTTP integration is not set-up " - "for reverse proxies. This usually means that you have not configured your reverse proxy " - "correctly. This request will be blocked in Home Assistant 2021.7 unless you configure " - "your HTTP integration to allow this header." - ) - return True - async def async_login_flow(self, context: dict | None) -> LoginFlow: """Return a flow to login.""" assert context is not None - - if not self.is_allowed_request(): - return MisconfiguredTrustedNetworksLoginFlow(self) - ip_addr = cast(IPAddress, context.get("ip_address")) users = await self.store.async_get_users() available_users = [ @@ -195,11 +170,6 @@ class TrustedNetworksAuthProvider(AuthProvider): Raise InvalidAuthError if not. Raise InvalidAuthError if trusted_networks is not configured. """ - if not self.is_allowed_request(): - raise InvalidAuthError( - "No request or it contains x-forwarded-for header and that's not allowed by configuration" - ) - if not self.trusted_networks: raise InvalidAuthError("trusted_networks is not configured") @@ -220,16 +190,6 @@ class TrustedNetworksAuthProvider(AuthProvider): self.async_validate_access(ip_address(remote_ip)) -class MisconfiguredTrustedNetworksLoginFlow(LoginFlow): - """Login handler for misconfigured trusted networks.""" - - async def async_step_init( - self, user_input: dict[str, str] | None = None - ) -> FlowResult: - """Handle the step of the form.""" - return self.async_abort(reason="forwared_for_header_not_allowed") - - class TrustedNetworksLoginFlow(LoginFlow): """Handler for the login flow.""" diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index db198cb334a..19e3437b79c 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -232,10 +232,7 @@ class HomeAssistantHTTP: # forwarded middleware needs to go second. setup_security_filter(app) - # Only register middleware if `use_x_forwarded_for` is enabled - # and trusted proxies are provided - if use_x_forwarded_for and trusted_proxies: - async_setup_forwarded(app, trusted_proxies) + async_setup_forwarded(app, use_x_forwarded_for, trusted_proxies) setup_request_context(app, current_request) @@ -252,7 +249,6 @@ class HomeAssistantHTTP: self.ssl_key = ssl_key self.server_host = server_host self.server_port = server_port - self.use_x_forwarded_for = use_x_forwarded_for self.trusted_proxies = trusted_proxies self.is_ban_enabled = is_ban_enabled self.ssl_profile = ssl_profile diff --git a/homeassistant/components/http/forwarded.py b/homeassistant/components/http/forwarded.py index 5c5726a2597..5c62a469924 100644 --- a/homeassistant/components/http/forwarded.py +++ b/homeassistant/components/http/forwarded.py @@ -14,7 +14,9 @@ _LOGGER = logging.getLogger(__name__) @callback -def async_setup_forwarded(app: Application, trusted_proxies: list[str]) -> None: +def async_setup_forwarded( + app: Application, use_x_forwarded_for: bool | None, trusted_proxies: list[str] +) -> None: """Create forwarded middleware for the app. Process IP addresses, proto and host information in the forwarded for headers. @@ -73,15 +75,37 @@ def async_setup_forwarded(app: Application, trusted_proxies: list[str]) -> None: # No forwarding headers, continue as normal return await handler(request) - # Ensure the IP of the connected peer is trusted - assert request.transport is not None + # Get connected IP + if ( + request.transport is None + or request.transport.get_extra_info("peername") is None + ): + # Connected IP isn't retrieveable from the request transport, continue + return await handler(request) + connected_ip = ip_address(request.transport.get_extra_info("peername")[0]) - if not any(connected_ip in trusted_proxy for trusted_proxy in trusted_proxies): + + # We have X-Forwarded-For, but config does not agree + if not use_x_forwarded_for: _LOGGER.warning( - "Received X-Forwarded-For header from untrusted proxy %s, headers not processed", + "A request from a reverse proxy was received from %s, but your " + "HTTP integration is not set-up for reverse proxies; " + "This request will be blocked in Home Assistant 2021.7 unless " + "you configure your HTTP integration to allow this header", connected_ip, ) - # Not trusted, continue as normal + # Block this request in the future, for now we pass. + return await handler(request) + + # Ensure the IP of the connected peer is trusted + if not any(connected_ip in trusted_proxy for trusted_proxy in trusted_proxies): + _LOGGER.warning( + "Received X-Forwarded-For header from untrusted proxy %s, headers not processed; " + "This request will be blocked in Home Assistant 2021.7 unless you configure " + "your HTTP integration to allow this proxy to reverse your Home Assistant instance", + connected_ip, + ) + # Not trusted, Block this request in the future, continue as normal return await handler(request) # Multiple X-Forwarded-For headers diff --git a/tests/auth/providers/test_trusted_networks.py b/tests/auth/providers/test_trusted_networks.py index c68d6651e3c..39764fa4206 100644 --- a/tests/auth/providers/test_trusted_networks.py +++ b/tests/auth/providers/test_trusted_networks.py @@ -5,12 +5,9 @@ from unittest.mock import Mock, patch import pytest import voluptuous as vol -from homeassistant import auth, const +from homeassistant import auth from homeassistant.auth import auth_store from homeassistant.auth.providers import trusted_networks as tn_auth -from homeassistant.setup import async_setup_component - -FORWARD_FOR_IS_WARNING = (const.MAJOR_VERSION, const.MINOR_VERSION) < (2021, 8) @pytest.fixture @@ -114,17 +111,7 @@ def manager_bypass_login(hass, store, provider_bypass_login): ) -@pytest.fixture -def mock_allowed_request(): - """Mock that the request is allowed.""" - with patch( - "homeassistant.auth.providers.trusted_networks.TrustedNetworksAuthProvider.is_allowed_request", - return_value=True, - ): - yield - - -async def test_trusted_networks_credentials(manager, provider, mock_allowed_request): +async def test_trusted_networks_credentials(manager, provider): """Test trusted_networks credentials related functions.""" owner = await manager.async_create_user("test-owner") tn_owner_cred = await provider.async_get_or_create_credentials({"user": owner.id}) @@ -141,7 +128,7 @@ async def test_trusted_networks_credentials(manager, provider, mock_allowed_requ await provider.async_get_or_create_credentials({"user": "invalid-user"}) -async def test_validate_access(provider, mock_allowed_request): +async def test_validate_access(provider): """Test validate access from trusted networks.""" provider.async_validate_access(ip_address("192.168.0.1")) provider.async_validate_access(ip_address("192.168.128.10")) @@ -156,7 +143,7 @@ async def test_validate_access(provider, mock_allowed_request): provider.async_validate_access(ip_address("2001:db8::ff00:42:8329")) -async def test_validate_refresh_token(provider, mock_allowed_request): +async def test_validate_refresh_token(provider): """Verify re-validation of refresh token.""" with patch.object(provider, "async_validate_access") as mock: with pytest.raises(tn_auth.InvalidAuthError): @@ -166,7 +153,7 @@ async def test_validate_refresh_token(provider, mock_allowed_request): mock.assert_called_once_with(ip_address("127.0.0.1")) -async def test_login_flow(manager, provider, mock_allowed_request): +async def test_login_flow(manager, provider): """Test login flow.""" owner = await manager.async_create_user("test-owner") user = await manager.async_create_user("test-user") @@ -193,9 +180,7 @@ async def test_login_flow(manager, provider, mock_allowed_request): assert step["data"]["user"] == user.id -async def test_trusted_users_login( - manager_with_user, provider_with_user, mock_allowed_request -): +async def test_trusted_users_login(manager_with_user, provider_with_user): """Test available user list changed per different IP.""" owner = await manager_with_user.async_create_user("test-owner") sys_user = await manager_with_user.async_create_system_user( @@ -275,9 +260,7 @@ async def test_trusted_users_login( assert schema({"user": sys_user.id}) -async def test_trusted_group_login( - manager_with_user, provider_with_user, mock_allowed_request -): +async def test_trusted_group_login(manager_with_user, provider_with_user): """Test config trusted_user with group_id.""" owner = await manager_with_user.async_create_user("test-owner") # create a user in user group @@ -330,9 +313,7 @@ async def test_trusted_group_login( assert schema({"user": user.id}) -async def test_bypass_login_flow( - manager_bypass_login, provider_bypass_login, mock_allowed_request -): +async def test_bypass_login_flow(manager_bypass_login, provider_bypass_login): """Test login flow can be bypass if only one user available.""" owner = await manager_bypass_login.async_create_user("test-owner") @@ -363,42 +344,3 @@ async def test_bypass_login_flow( # both owner and user listed assert schema({"user": owner.id}) assert schema({"user": user.id}) - - -async def test_allowed_request(hass, provider, current_request, caplog): - """Test allowing requests.""" - assert await async_setup_component(hass, "http", {}) - - provider.async_validate_access(ip_address("192.168.0.1")) - - current_request.get.return_value = current_request.get.return_value.clone( - headers={ - **current_request.get.return_value.headers, - "x-forwarded-for": "1.2.3.4", - } - ) - - if FORWARD_FOR_IS_WARNING: - caplog.clear() - provider.async_validate_access(ip_address("192.168.0.1")) - assert "This request will be blocked" in caplog.text - else: - with pytest.raises(tn_auth.InvalidAuthError): - provider.async_validate_access(ip_address("192.168.0.1")) - - hass.http.use_x_forwarded_for = True - - provider.async_validate_access(ip_address("192.168.0.1")) - - -@pytest.mark.skipif(FORWARD_FOR_IS_WARNING, reason="Currently a warning") -async def test_login_flow_no_request(provider): - """Test getting a login flow.""" - login_flow = await provider.async_login_flow({"ip_address": ip_address("1.1.1.1")}) - assert await login_flow.async_step_init() == { - "description_placeholders": None, - "flow_id": None, - "handler": None, - "reason": "forwared_for_header_not_allowed", - "type": "abort", - } diff --git a/tests/components/http/test_auth.py b/tests/components/http/test_auth.py index 71c01630a67..6bd1d622b12 100644 --- a/tests/components/http/test_auth.py +++ b/tests/components/http/test_auth.py @@ -53,7 +53,7 @@ def app(hass): app = web.Application() app["hass"] = hass app.router.add_get("/", mock_handler) - async_setup_forwarded(app, []) + async_setup_forwarded(app, True, []) return app diff --git a/tests/components/http/test_forwarded.py b/tests/components/http/test_forwarded.py index 2946c0b383c..4b7a3421b0a 100644 --- a/tests/components/http/test_forwarded.py +++ b/tests/components/http/test_forwarded.py @@ -28,7 +28,7 @@ async def test_x_forwarded_for_without_trusted_proxy(aiohttp_client, caplog): app = web.Application() app.router.add_get("/", handler) - async_setup_forwarded(app, []) + async_setup_forwarded(app, True, []) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get("/", headers={X_FORWARDED_FOR: "255.255.255.255"}) @@ -74,7 +74,7 @@ async def test_x_forwarded_for_with_trusted_proxy( app = web.Application() app.router.add_get("/", handler) async_setup_forwarded( - app, [ip_network(trusted_proxy) for trusted_proxy in trusted_proxies] + app, True, [ip_network(trusted_proxy) for trusted_proxy in trusted_proxies] ) mock_api_client = await aiohttp_client(app) @@ -83,6 +83,33 @@ async def test_x_forwarded_for_with_trusted_proxy( assert resp.status == 200 +async def test_x_forwarded_for_disabled_with_proxy(aiohttp_client, caplog): + """Test that we warn when processing is disabled, but proxy has been detected.""" + + async def handler(request): + url = mock_api_client.make_url("/") + assert request.host == f"{url.host}:{url.port}" + assert request.scheme == "http" + assert not request.secure + assert request.remote == "127.0.0.1" + + return web.Response() + + app = web.Application() + app.router.add_get("/", handler) + + async_setup_forwarded(app, False, []) + + mock_api_client = await aiohttp_client(app) + resp = await mock_api_client.get("/", headers={X_FORWARDED_FOR: "255.255.255.255"}) + + assert resp.status == 200 + assert ( + "A request from a reverse proxy was received from 127.0.0.1, but your HTTP " + "integration is not set-up for reverse proxies" in caplog.text + ) + + async def test_x_forwarded_for_with_untrusted_proxy(aiohttp_client): """Test that we get the IP from transport with untrusted proxy.""" @@ -97,7 +124,7 @@ async def test_x_forwarded_for_with_untrusted_proxy(aiohttp_client): app = web.Application() app.router.add_get("/", handler) - async_setup_forwarded(app, [ip_network("1.1.1.1")]) + async_setup_forwarded(app, True, [ip_network("1.1.1.1")]) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get("/", headers={X_FORWARDED_FOR: "255.255.255.255"}) @@ -119,7 +146,7 @@ async def test_x_forwarded_for_with_spoofed_header(aiohttp_client): app = web.Application() app.router.add_get("/", handler) - async_setup_forwarded(app, [ip_network("127.0.0.1")]) + async_setup_forwarded(app, True, [ip_network("127.0.0.1")]) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get( @@ -148,7 +175,7 @@ async def test_x_forwarded_for_with_malformed_header( """Test that we get a HTTP 400 bad request with a malformed header.""" app = web.Application() app.router.add_get("/", mock_handler) - async_setup_forwarded(app, [ip_network("127.0.0.1")]) + async_setup_forwarded(app, True, [ip_network("127.0.0.1")]) mock_api_client = await aiohttp_client(app) @@ -162,7 +189,7 @@ async def test_x_forwarded_for_with_multiple_headers(aiohttp_client, caplog): """Test that we get a HTTP 400 bad request with multiple headers.""" app = web.Application() app.router.add_get("/", mock_handler) - async_setup_forwarded(app, [ip_network("127.0.0.1")]) + async_setup_forwarded(app, True, [ip_network("127.0.0.1")]) mock_api_client = await aiohttp_client(app) @@ -193,7 +220,7 @@ async def test_x_forwarded_proto_without_trusted_proxy(aiohttp_client): app = web.Application() app.router.add_get("/", handler) - async_setup_forwarded(app, []) + async_setup_forwarded(app, True, []) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get( @@ -245,7 +272,7 @@ async def test_x_forwarded_proto_with_trusted_proxy( app = web.Application() app.router.add_get("/", handler) - async_setup_forwarded(app, [ip_network("127.0.0.0/24")]) + async_setup_forwarded(app, True, [ip_network("127.0.0.0/24")]) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get( @@ -273,7 +300,7 @@ async def test_x_forwarded_proto_with_trusted_proxy_multiple_for(aiohttp_client) app = web.Application() app.router.add_get("/", handler) - async_setup_forwarded(app, [ip_network("127.0.0.0/24")]) + async_setup_forwarded(app, True, [ip_network("127.0.0.0/24")]) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get( @@ -301,7 +328,7 @@ async def test_x_forwarded_proto_not_processed_without_for(aiohttp_client): app = web.Application() app.router.add_get("/", handler) - async_setup_forwarded(app, [ip_network("127.0.0.1")]) + async_setup_forwarded(app, True, [ip_network("127.0.0.1")]) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get("/", headers={X_FORWARDED_PROTO: "https"}) @@ -313,7 +340,7 @@ async def test_x_forwarded_proto_with_multiple_headers(aiohttp_client, caplog): """Test that we get a HTTP 400 bad request with multiple headers.""" app = web.Application() app.router.add_get("/", mock_handler) - async_setup_forwarded(app, [ip_network("127.0.0.1")]) + async_setup_forwarded(app, True, [ip_network("127.0.0.1")]) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get( @@ -339,7 +366,7 @@ async def test_x_forwarded_proto_empty_element( """Test that we get a HTTP 400 bad request with empty proto.""" app = web.Application() app.router.add_get("/", mock_handler) - async_setup_forwarded(app, [ip_network("127.0.0.1")]) + async_setup_forwarded(app, True, [ip_network("127.0.0.1")]) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get( @@ -364,7 +391,7 @@ async def test_x_forwarded_proto_incorrect_number_of_elements( """Test that we get a HTTP 400 bad request with incorrect number of elements.""" app = web.Application() app.router.add_get("/", mock_handler) - async_setup_forwarded(app, [ip_network("127.0.0.1")]) + async_setup_forwarded(app, True, [ip_network("127.0.0.1")]) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get( @@ -397,7 +424,7 @@ async def test_x_forwarded_host_without_trusted_proxy(aiohttp_client): app = web.Application() app.router.add_get("/", handler) - async_setup_forwarded(app, []) + async_setup_forwarded(app, True, []) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get( @@ -421,7 +448,7 @@ async def test_x_forwarded_host_with_trusted_proxy(aiohttp_client): app = web.Application() app.router.add_get("/", handler) - async_setup_forwarded(app, [ip_network("127.0.0.1")]) + async_setup_forwarded(app, True, [ip_network("127.0.0.1")]) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get( @@ -446,7 +473,7 @@ async def test_x_forwarded_host_not_processed_without_for(aiohttp_client): app = web.Application() app.router.add_get("/", handler) - async_setup_forwarded(app, [ip_network("127.0.0.1")]) + async_setup_forwarded(app, True, [ip_network("127.0.0.1")]) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get("/", headers={X_FORWARDED_HOST: "example.com"}) @@ -458,7 +485,7 @@ async def test_x_forwarded_host_with_multiple_headers(aiohttp_client, caplog): """Test that we get a HTTP 400 bad request with multiple headers.""" app = web.Application() app.router.add_get("/", mock_handler) - async_setup_forwarded(app, [ip_network("127.0.0.1")]) + async_setup_forwarded(app, True, [ip_network("127.0.0.1")]) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get( @@ -478,7 +505,7 @@ async def test_x_forwarded_host_with_empty_header(aiohttp_client, caplog): """Test that we get a HTTP 400 bad request with empty host value.""" app = web.Application() app.router.add_get("/", mock_handler) - async_setup_forwarded(app, [ip_network("127.0.0.1")]) + async_setup_forwarded(app, True, [ip_network("127.0.0.1")]) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get( From bbd743368678ad3ad0230999c522e78fd2642366 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 1 Jun 2021 17:07:45 +0200 Subject: [PATCH 052/123] Improve time condition trace (#51335) --- homeassistant/helpers/condition.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index a59ad459874..23816b94a65 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -756,15 +756,18 @@ def time( ) if after < before: + condition_trace_update_result(after=after, now_time=now_time, before=before) if not after <= now_time < before: return False else: + condition_trace_update_result(after=after, now_time=now_time, before=before) if before <= now_time < after: return False if weekday is not None: now_weekday = WEEKDAYS[now.weekday()] + condition_trace_update_result(weekday=weekday, now_weekday=now_weekday) if ( isinstance(weekday, str) and weekday != now_weekday From d78694c9b8a3c0659b0bc85667c659797fe9e613 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 1 Jun 2021 17:57:23 +0200 Subject: [PATCH 053/123] Fix time condition microsecond offset when using input helpers (#51337) --- homeassistant/helpers/condition.py | 1 - tests/helpers/test_condition.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index 23816b94a65..a467d952683 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -752,7 +752,6 @@ def time( before_entity.attributes.get("hour", 23), before_entity.attributes.get("minute", 59), before_entity.attributes.get("second", 59), - 999999, ) if after < before: diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index 9347d0bc025..2290ce9f679 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -791,6 +791,34 @@ async def test_time_using_input_datetime(hass): hass, after="input_datetime.pm", before="input_datetime.am" ) + # Trigger on PM time + with patch( + "homeassistant.helpers.condition.dt_util.now", + return_value=dt_util.now().replace(hour=18, minute=0, second=0), + ): + assert condition.time( + hass, after="input_datetime.pm", before="input_datetime.am" + ) + assert not condition.time( + hass, after="input_datetime.am", before="input_datetime.pm" + ) + assert condition.time(hass, after="input_datetime.pm") + assert not condition.time(hass, before="input_datetime.pm") + + # Trigger on AM time + with patch( + "homeassistant.helpers.condition.dt_util.now", + return_value=dt_util.now().replace(hour=6, minute=0, second=0), + ): + assert not condition.time( + hass, after="input_datetime.pm", before="input_datetime.am" + ) + assert condition.time( + hass, after="input_datetime.am", before="input_datetime.pm" + ) + assert condition.time(hass, after="input_datetime.am") + assert not condition.time(hass, before="input_datetime.am") + with pytest.raises(ConditionError): condition.time(hass, after="input_datetime.not_existing") From 941b02b73ee0f99b546b588b449b88b37a652292 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Tue, 1 Jun 2021 17:58:25 +0200 Subject: [PATCH 054/123] Fix Netatmo sensor logic (#51338) --- homeassistant/components/netatmo/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index ed75ddf2f7f..2dbbeb56c76 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -183,7 +183,7 @@ async def async_setup_entry(hass, entry, async_add_entities): await data_handler.register_data_class(data_class_name, data_class_name, None) data_class = data_handler.data.get(data_class_name) - if not (data_class and data_class.raw_data): + if data_class and data_class.raw_data: platform_not_ready = False async_add_entities(await find_entities(data_class_name), True) @@ -228,7 +228,7 @@ async def async_setup_entry(hass, entry, async_add_entities): ) data_class = data_handler.data.get(signal_name) - if not (data_class and data_class.raw_data): + if data_class and data_class.raw_data: nonlocal platform_not_ready platform_not_ready = False From 464c66f97f25f99a29caea9e96857cb2a4474032 Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Tue, 1 Jun 2021 20:32:17 +0200 Subject: [PATCH 055/123] Fix SIA event data func (#51339) --- homeassistant/components/sia/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sia/utils.py b/homeassistant/components/sia/utils.py index 08e0fce8ab2..66fdd7d95be 100644 --- a/homeassistant/components/sia/utils.py +++ b/homeassistant/components/sia/utils.py @@ -30,7 +30,7 @@ def get_attr_from_sia_event(event: SIAEvent) -> dict[str, Any]: def get_event_data_from_sia_event(event: SIAEvent) -> dict[str, Any]: """Create a dict from the SIA Event for the HA Event.""" return { - "message_type": event.message_type, + "message_type": event.message_type.value, "receiver": event.receiver, "line": event.line, "account": event.account, @@ -43,7 +43,7 @@ def get_event_data_from_sia_event(event: SIAEvent) -> dict[str, Any]: "message": event.message, "x_data": event.x_data, "timestamp": event.timestamp.isoformat(), - "event_qualifier": event.qualifier, + "event_qualifier": event.event_qualifier, "event_type": event.event_type, "partition": event.partition, "extended_data": [ From 5ea798462f9a3c935e10aae4783495e26f776133 Mon Sep 17 00:00:00 2001 From: definitio <37266727+definitio@users.noreply.github.com> Date: Tue, 1 Jun 2021 20:07:51 +0300 Subject: [PATCH 056/123] Fix Snapcast state after restoring snapshot (#51340) --- homeassistant/components/snapcast/media_player.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/snapcast/media_player.py b/homeassistant/components/snapcast/media_player.py index dcb4b62b35a..26bf4c903a6 100644 --- a/homeassistant/components/snapcast/media_player.py +++ b/homeassistant/components/snapcast/media_player.py @@ -198,6 +198,7 @@ class SnapcastGroupDevice(MediaPlayerEntity): async def async_restore(self): """Restore the group state.""" await self._group.restore() + self.async_write_ha_state() class SnapcastClientDevice(MediaPlayerEntity): @@ -326,6 +327,7 @@ class SnapcastClientDevice(MediaPlayerEntity): async def async_restore(self): """Restore the client state.""" await self._client.restore() + self.async_write_ha_state() async def async_set_latency(self, latency): """Set the latency of the client.""" From 89a374057dd1a48abe431cc4630e8030d91fec27 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 1 Jun 2021 19:26:54 +0200 Subject: [PATCH 057/123] Bump zwave-js-server-python to 0.26.0 (#51341) --- homeassistant/components/zwave_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index c68206373ba..5ce65fcbb35 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave JS", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", - "requirements": ["zwave-js-server-python==0.25.1"], + "requirements": ["zwave-js-server-python==0.26.0"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["http", "websocket_api"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 19afc85cdc9..f33792126ad 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2442,4 +2442,4 @@ zigpy==0.33.0 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.25.1 +zwave-js-server-python==0.26.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 243d061224d..cfffb2c5dfc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1324,4 +1324,4 @@ zigpy-znp==0.5.1 zigpy==0.33.0 # homeassistant.components.zwave_js -zwave-js-server-python==0.25.1 +zwave-js-server-python==0.26.0 From f93acfc4c0aea37a72bbdf682a887b18342d5f6e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 1 Jun 2021 13:34:31 -0700 Subject: [PATCH 058/123] Merge system options into pref properties (#51347) * Make system options future proof * Update tests * Add types --- .../components/config/config_entries.py | 82 ++++++------- homeassistant/config_entries.py | 99 ++++++++-------- homeassistant/helpers/entity_platform.py | 2 +- homeassistant/helpers/entity_registry.py | 2 +- homeassistant/helpers/update_coordinator.py | 2 +- tests/common.py | 6 +- .../bmw_connected_drive/test_config_flow.py | 1 - .../components/config/test_config_entries.py | 95 ++++++--------- .../forked_daapd/test_config_flow.py | 1 - .../forked_daapd/test_media_player.py | 1 - .../components/home_plus_control/conftest.py | 1 - .../homekit_controller/test_storage.py | 1 - .../components/homematicip_cloud/conftest.py | 1 - tests/components/hue/conftest.py | 1 - tests/components/hue/test_bridge.py | 4 - tests/components/hue/test_light.py | 1 - tests/components/huisbaasje/test_init.py | 3 - tests/components/huisbaasje/test_sensor.py | 2 - .../hvv_departures/test_config_flow.py | 3 - tests/components/smartthings/conftest.py | 1 - tests/components/unifi/test_device_tracker.py | 1 - tests/components/unifi/test_switch.py | 2 - tests/components/zwave/test_lock.py | 1 - tests/helpers/test_entity_platform.py | 4 +- tests/helpers/test_entity_registry.py | 6 +- tests/helpers/test_update_coordinator.py | 2 +- tests/test_config_entries.py | 108 ++++++++++-------- 27 files changed, 188 insertions(+), 245 deletions(-) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 9d88a9b5311..7fe5cb0d190 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -1,4 +1,6 @@ """Http views to control the config manager.""" +from __future__ import annotations + import aiohttp.web_exceptions import voluptuous as vol @@ -7,7 +9,7 @@ from homeassistant.auth.permissions.const import CAT_CONFIG_ENTRIES, POLICY_EDIT from homeassistant.components import websocket_api from homeassistant.components.http import HomeAssistantView from homeassistant.const import HTTP_FORBIDDEN, HTTP_NOT_FOUND -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import Unauthorized from homeassistant.helpers.data_entry_flow import ( FlowManagerIndexView, @@ -31,7 +33,6 @@ async def async_setup(hass): hass.components.websocket_api.async_register_command(config_entry_disable) hass.components.websocket_api.async_register_command(config_entry_update) hass.components.websocket_api.async_register_command(config_entries_progress) - hass.components.websocket_api.async_register_command(system_options_update) hass.components.websocket_api.async_register_command(ignore_config_flow) return True @@ -230,14 +231,21 @@ def config_entries_progress(hass, connection, msg): ) -def send_entry_not_found(connection, msg_id): +def send_entry_not_found( + connection: websocket_api.ActiveConnection, msg_id: int +) -> None: """Send Config entry not found error.""" connection.send_error( msg_id, websocket_api.const.ERR_NOT_FOUND, "Config entry not found" ) -def get_entry(hass, connection, entry_id, msg_id): +def get_entry( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + entry_id: str, + msg_id: int, +) -> config_entries.ConfigEntry | None: """Get entry, send error message if it doesn't exist.""" entry = hass.config_entries.async_get_entry(entry_id) if entry is None: @@ -249,49 +257,13 @@ def get_entry(hass, connection, entry_id, msg_id): @websocket_api.async_response @websocket_api.websocket_command( { - "type": "config_entries/system_options/update", + "type": "config_entries/update", "entry_id": str, - vol.Optional("disable_new_entities"): bool, - vol.Optional("disable_polling"): bool, + vol.Optional("title"): str, + vol.Optional("pref_disable_new_entities"): bool, + vol.Optional("pref_disable_polling"): bool, } ) -async def system_options_update(hass, connection, msg): - """Update config entry system options.""" - changes = dict(msg) - changes.pop("id") - changes.pop("type") - changes.pop("entry_id") - - entry = get_entry(hass, connection, msg["entry_id"], msg["id"]) - if entry is None: - return - - old_disable_polling = entry.system_options.disable_polling - - hass.config_entries.async_update_entry(entry, system_options=changes) - - result = { - "system_options": entry.system_options.as_dict(), - "require_restart": False, - } - - if ( - old_disable_polling != entry.system_options.disable_polling - and entry.state is config_entries.ConfigEntryState.LOADED - ): - if not await hass.config_entries.async_reload(entry.entry_id): - result["require_restart"] = ( - entry.state is config_entries.ConfigEntryState.FAILED_UNLOAD - ) - - connection.send_result(msg["id"], result) - - -@websocket_api.require_admin -@websocket_api.async_response -@websocket_api.websocket_command( - {"type": "config_entries/update", "entry_id": str, vol.Optional("title"): str} -) async def config_entry_update(hass, connection, msg): """Update config entry.""" changes = dict(msg) @@ -303,8 +275,25 @@ async def config_entry_update(hass, connection, msg): if entry is None: return + old_disable_polling = entry.pref_disable_polling + hass.config_entries.async_update_entry(entry, **changes) - connection.send_result(msg["id"], entry_json(entry)) + + result = { + "config_entry": entry_json(entry), + "require_restart": False, + } + + if ( + old_disable_polling != entry.pref_disable_polling + and entry.state is config_entries.ConfigEntryState.LOADED + ): + if not await hass.config_entries.async_reload(entry.entry_id): + result["require_restart"] = ( + entry.state is config_entries.ConfigEntryState.FAILED_UNLOAD + ) + + connection.send_result(msg["id"], result) @websocket_api.require_admin @@ -391,7 +380,8 @@ def entry_json(entry: config_entries.ConfigEntry) -> dict: "state": entry.state.value, "supports_options": supports_options, "supports_unload": entry.supports_unload, - "system_options": entry.system_options.as_dict(), + "pref_disable_new_entities": entry.pref_disable_new_entities, + "pref_disable_polling": entry.pref_disable_polling, "disabled_by": entry.disabled_by, "reason": entry.reason, } diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 33c18fc0d7c..eeaf0149cc2 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -11,8 +11,6 @@ from types import MappingProxyType, MethodType from typing import Any, Callable, Optional, cast import weakref -import attr - from homeassistant import data_entry_flow, loader from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP from homeassistant.core import CALLBACK_TYPE, CoreState, HomeAssistant, callback @@ -152,7 +150,8 @@ class ConfigEntry: "options", "unique_id", "supports_unload", - "system_options", + "pref_disable_new_entities", + "pref_disable_polling", "source", "state", "disabled_by", @@ -170,7 +169,8 @@ class ConfigEntry: title: str, data: Mapping[str, Any], source: str, - system_options: dict, + pref_disable_new_entities: bool | None = None, + pref_disable_polling: bool | None = None, options: Mapping[str, Any] | None = None, unique_id: str | None = None, entry_id: str | None = None, @@ -197,7 +197,15 @@ class ConfigEntry: self.options = MappingProxyType(options or {}) # Entry system options - self.system_options = SystemOptions(**system_options) + if pref_disable_new_entities is None: + pref_disable_new_entities = False + + self.pref_disable_new_entities = pref_disable_new_entities + + if pref_disable_polling is None: + pref_disable_polling = False + + self.pref_disable_polling = pref_disable_polling # Source of the configuration (user, discovery, cloud) self.source = source @@ -535,7 +543,8 @@ class ConfigEntry: "title": self.title, "data": dict(self.data), "options": dict(self.options), - "system_options": self.system_options.as_dict(), + "pref_disable_new_entities": self.pref_disable_new_entities, + "pref_disable_polling": self.pref_disable_polling, "source": self.source, "unique_id": self.unique_id, "disabled_by": self.disabled_by, @@ -652,7 +661,6 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager): title=result["title"], data=result["data"], options=result["options"], - system_options={}, source=flow.context["source"], unique_id=flow.unique_id, ) @@ -845,8 +853,18 @@ class ConfigEntries: self._entries = {} return - self._entries = { - entry["entry_id"]: ConfigEntry( + entries = {} + + for entry in config["entries"]: + pref_disable_new_entities = entry.get("pref_disable_new_entities") + + # Between 0.98 and 2021.6 we stored 'disable_new_entities' in a system options dictionary + if pref_disable_new_entities is None and "system_options" in entry: + pref_disable_new_entities = entry.get("system_options", {}).get( + "disable_new_entities" + ) + + entries[entry["entry_id"]] = ConfigEntry( version=entry["version"], domain=entry["domain"], entry_id=entry["entry_id"], @@ -855,15 +873,16 @@ class ConfigEntries: title=entry["title"], # New in 0.89 options=entry.get("options"), - # New in 0.98 - system_options=entry.get("system_options", {}), # New in 0.104 unique_id=entry.get("unique_id"), # New in 2021.3 disabled_by=entry.get("disabled_by"), + # New in 2021.6 + pref_disable_new_entities=pref_disable_new_entities, + pref_disable_polling=entry.get("pref_disable_polling"), ) - for entry in config["entries"] - } + + self._entries = entries async def async_setup(self, entry_id: str) -> bool: """Set up a config entry. @@ -962,11 +981,12 @@ class ConfigEntries: self, entry: ConfigEntry, *, - unique_id: str | dict | None | UndefinedType = UNDEFINED, - title: str | dict | UndefinedType = UNDEFINED, + unique_id: str | None | UndefinedType = UNDEFINED, + title: str | UndefinedType = UNDEFINED, data: dict | UndefinedType = UNDEFINED, options: Mapping[str, Any] | UndefinedType = UNDEFINED, - system_options: dict | UndefinedType = UNDEFINED, + pref_disable_new_entities: bool | UndefinedType = UNDEFINED, + pref_disable_polling: bool | UndefinedType = UNDEFINED, ) -> bool: """Update a config entry. @@ -978,13 +998,17 @@ class ConfigEntries: """ changed = False - if unique_id is not UNDEFINED and entry.unique_id != unique_id: - changed = True - entry.unique_id = cast(Optional[str], unique_id) + for attr, value in ( + ("unique_id", unique_id), + ("title", title), + ("pref_disable_new_entities", pref_disable_new_entities), + ("pref_disable_polling", pref_disable_polling), + ): + if value == UNDEFINED or getattr(entry, attr) == value: + continue - if title is not UNDEFINED and entry.title != title: + setattr(entry, attr, value) changed = True - entry.title = cast(str, title) if data is not UNDEFINED and entry.data != data: # type: ignore changed = True @@ -994,11 +1018,6 @@ class ConfigEntries: changed = True entry.options = MappingProxyType(options) - if system_options is not UNDEFINED: - old_system_options = entry.system_options.as_dict() - entry.system_options.update(**system_options) - changed = entry.system_options.as_dict() != old_system_options - if not changed: return False @@ -1401,34 +1420,6 @@ class OptionsFlow(data_entry_flow.FlowHandler): handler: str -@attr.s(slots=True) -class SystemOptions: - """Config entry system options.""" - - disable_new_entities: bool = attr.ib(default=False) - disable_polling: bool = attr.ib(default=False) - - def update( - self, - *, - disable_new_entities: bool | UndefinedType = UNDEFINED, - disable_polling: bool | UndefinedType = UNDEFINED, - ) -> None: - """Update properties.""" - if disable_new_entities is not UNDEFINED: - self.disable_new_entities = disable_new_entities - - if disable_polling is not UNDEFINED: - self.disable_polling = disable_polling - - def as_dict(self) -> dict[str, Any]: - """Return dictionary version of this config entries system options.""" - return { - "disable_new_entities": self.disable_new_entities, - "disable_polling": self.disable_polling, - } - - class EntityRegistryDisabledHandler: """Handler to handle when entities related to config entries updating disabled_by.""" diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index f0d691a1c8d..b22fb9ec2d2 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -397,7 +397,7 @@ class EntityPlatform: raise if ( - (self.config_entry and self.config_entry.system_options.disable_polling) + (self.config_entry and self.config_entry.pref_disable_polling) or self._async_unsub_polling is not None or not any(entity.should_poll for entity in self.entities.values()) ): diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index dbb3fae0e53..fc9ef575c7d 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -286,7 +286,7 @@ class EntityRegistry: if ( disabled_by is None and config_entry - and config_entry.system_options.disable_new_entities + and config_entry.pref_disable_new_entities ): disabled_by = DISABLED_INTEGRATION diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index e91acfaf82f..e83a2d0edc3 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -111,7 +111,7 @@ class DataUpdateCoordinator(Generic[T]): if self.update_interval is None: return - if self.config_entry and self.config_entry.system_options.disable_polling: + if self.config_entry and self.config_entry.pref_disable_polling: return if self._unsub_refresh: diff --git a/tests/common.py b/tests/common.py index 952350fe68c..03b53294db0 100644 --- a/tests/common.py +++ b/tests/common.py @@ -732,7 +732,8 @@ class MockConfigEntry(config_entries.ConfigEntry): title="Mock Title", state=None, options={}, - system_options={}, + pref_disable_new_entities=None, + pref_disable_polling=None, unique_id=None, disabled_by=None, reason=None, @@ -742,7 +743,8 @@ class MockConfigEntry(config_entries.ConfigEntry): "entry_id": entry_id or uuid_util.random_uuid_hex(), "domain": domain, "data": data or {}, - "system_options": system_options, + "pref_disable_new_entities": pref_disable_new_entities, + "pref_disable_polling": pref_disable_polling, "options": options, "version": version, "title": title, diff --git a/tests/components/bmw_connected_drive/test_config_flow.py b/tests/components/bmw_connected_drive/test_config_flow.py index 75ca9b4aa1c..6a0bd210387 100644 --- a/tests/components/bmw_connected_drive/test_config_flow.py +++ b/tests/components/bmw_connected_drive/test_config_flow.py @@ -29,7 +29,6 @@ FIXTURE_CONFIG_ENTRY = { CONF_REGION: FIXTURE_USER_INPUT[CONF_REGION], }, "options": {CONF_READ_ONLY: False, CONF_USE_LOCATION: False}, - "system_options": {"disable_new_entities": False}, "source": config_entries.SOURCE_USER, "unique_id": f"{FIXTURE_USER_INPUT[CONF_REGION]}-{FIXTURE_USER_INPUT[CONF_REGION]}", } diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 570d847e86e..0e1b471cbd5 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -87,10 +87,8 @@ async def test_get_entries(hass, client): "state": core_ce.ConfigEntryState.NOT_LOADED.value, "supports_options": True, "supports_unload": True, - "system_options": { - "disable_new_entities": False, - "disable_polling": False, - }, + "pref_disable_new_entities": False, + "pref_disable_polling": False, "disabled_by": None, "reason": None, }, @@ -101,10 +99,8 @@ async def test_get_entries(hass, client): "state": core_ce.ConfigEntryState.SETUP_ERROR.value, "supports_options": False, "supports_unload": False, - "system_options": { - "disable_new_entities": False, - "disable_polling": False, - }, + "pref_disable_new_entities": False, + "pref_disable_polling": False, "disabled_by": None, "reason": "Unsupported API", }, @@ -115,10 +111,8 @@ async def test_get_entries(hass, client): "state": core_ce.ConfigEntryState.NOT_LOADED.value, "supports_options": False, "supports_unload": False, - "system_options": { - "disable_new_entities": False, - "disable_polling": False, - }, + "pref_disable_new_entities": False, + "pref_disable_polling": False, "disabled_by": core_ce.DISABLED_USER, "reason": None, }, @@ -340,10 +334,8 @@ async def test_create_account(hass, client, enable_custom_integrations): "state": core_ce.ConfigEntryState.LOADED.value, "supports_options": False, "supports_unload": False, - "system_options": { - "disable_new_entities": False, - "disable_polling": False, - }, + "pref_disable_new_entities": False, + "pref_disable_polling": False, "title": "Test Entry", "reason": None, }, @@ -415,10 +407,8 @@ async def test_two_step_flow(hass, client, enable_custom_integrations): "state": core_ce.ConfigEntryState.LOADED.value, "supports_options": False, "supports_unload": False, - "system_options": { - "disable_new_entities": False, - "disable_polling": False, - }, + "pref_disable_new_entities": False, + "pref_disable_polling": False, "title": "user-title", "reason": None, }, @@ -698,7 +688,7 @@ async def test_two_step_options_flow(hass, client): } -async def test_update_system_options(hass, hass_ws_client): +async def test_update_prefrences(hass, hass_ws_client): """Test that we can update system options.""" assert await async_setup_component(hass, "config", {}) ws_client = await hass_ws_client(hass) @@ -706,64 +696,45 @@ async def test_update_system_options(hass, hass_ws_client): entry = MockConfigEntry(domain="demo", state=core_ce.ConfigEntryState.LOADED) entry.add_to_hass(hass) - assert entry.system_options.disable_new_entities is False - assert entry.system_options.disable_polling is False - - await ws_client.send_json( - { - "id": 5, - "type": "config_entries/system_options/update", - "entry_id": entry.entry_id, - "disable_new_entities": True, - } - ) - response = await ws_client.receive_json() - - assert response["success"] - assert response["result"] == { - "require_restart": False, - "system_options": {"disable_new_entities": True, "disable_polling": False}, - } - assert entry.system_options.disable_new_entities is True - assert entry.system_options.disable_polling is False + assert entry.pref_disable_new_entities is False + assert entry.pref_disable_polling is False await ws_client.send_json( { "id": 6, - "type": "config_entries/system_options/update", + "type": "config_entries/update", "entry_id": entry.entry_id, - "disable_new_entities": False, - "disable_polling": True, + "pref_disable_new_entities": True, } ) response = await ws_client.receive_json() assert response["success"] - assert response["result"] == { - "require_restart": True, - "system_options": {"disable_new_entities": False, "disable_polling": True}, - } - assert entry.system_options.disable_new_entities is False - assert entry.system_options.disable_polling is True + assert response["result"]["require_restart"] is False + assert response["result"]["config_entry"]["pref_disable_new_entities"] is True + assert response["result"]["config_entry"]["pref_disable_polling"] is False - -async def test_update_system_options_nonexisting(hass, hass_ws_client): - """Test that we can update entry.""" - assert await async_setup_component(hass, "config", {}) - ws_client = await hass_ws_client(hass) + assert entry.pref_disable_new_entities is True + assert entry.pref_disable_polling is False await ws_client.send_json( { - "id": 5, - "type": "config_entries/system_options/update", - "entry_id": "non_existing", - "disable_new_entities": True, + "id": 7, + "type": "config_entries/update", + "entry_id": entry.entry_id, + "pref_disable_new_entities": False, + "pref_disable_polling": True, } ) response = await ws_client.receive_json() - assert not response["success"] - assert response["error"]["code"] == "not_found" + assert response["success"] + assert response["result"]["require_restart"] is True + assert response["result"]["config_entry"]["pref_disable_new_entities"] is False + assert response["result"]["config_entry"]["pref_disable_polling"] is True + + assert entry.pref_disable_new_entities is False + assert entry.pref_disable_polling is True async def test_update_entry(hass, hass_ws_client): @@ -785,7 +756,7 @@ async def test_update_entry(hass, hass_ws_client): response = await ws_client.receive_json() assert response["success"] - assert response["result"]["title"] == "Updated Title" + assert response["result"]["config_entry"]["title"] == "Updated Title" assert entry.title == "Updated Title" diff --git a/tests/components/forked_daapd/test_config_flow.py b/tests/components/forked_daapd/test_config_flow.py index a99f91d3f91..668f1be0a4f 100644 --- a/tests/components/forked_daapd/test_config_flow.py +++ b/tests/components/forked_daapd/test_config_flow.py @@ -46,7 +46,6 @@ def config_entry_fixture(): title="", data=data, options={}, - system_options={}, source=SOURCE_USER, entry_id=1, ) diff --git a/tests/components/forked_daapd/test_media_player.py b/tests/components/forked_daapd/test_media_player.py index 032e3dde22c..a2e0050c3d9 100644 --- a/tests/components/forked_daapd/test_media_player.py +++ b/tests/components/forked_daapd/test_media_player.py @@ -282,7 +282,6 @@ def config_entry_fixture(): title="", data=data, options={CONF_TTS_PAUSE_TIME: 0}, - system_options={}, source=SOURCE_USER, entry_id=1, ) diff --git a/tests/components/home_plus_control/conftest.py b/tests/components/home_plus_control/conftest.py index 4b60f2623c4..78a0da41fb8 100644 --- a/tests/components/home_plus_control/conftest.py +++ b/tests/components/home_plus_control/conftest.py @@ -35,7 +35,6 @@ def mock_config_entry(): }, source="test", options={}, - system_options={"disable_new_entities": False}, unique_id=DOMAIN, entry_id="home_plus_control_entry_id", ) diff --git a/tests/components/homekit_controller/test_storage.py b/tests/components/homekit_controller/test_storage.py index b1c3ee9ff4c..aa0a5e55057 100644 --- a/tests/components/homekit_controller/test_storage.py +++ b/tests/components/homekit_controller/test_storage.py @@ -95,7 +95,6 @@ async def test_storage_is_removed_on_config_entry_removal(hass, utcnow): "TestData", pairing_data, "test", - system_options={}, ) assert hkid in hass.data[ENTITY_MAP].storage_data diff --git a/tests/components/homematicip_cloud/conftest.py b/tests/components/homematicip_cloud/conftest.py index b5dd6105e0f..c720df4a1bb 100644 --- a/tests/components/homematicip_cloud/conftest.py +++ b/tests/components/homematicip_cloud/conftest.py @@ -62,7 +62,6 @@ def hmip_config_entry_fixture() -> config_entries.ConfigEntry: unique_id=HAPID, data=entry_data, source=SOURCE_IMPORT, - system_options={"disable_new_entities": False}, ) return config_entry diff --git a/tests/components/hue/conftest.py b/tests/components/hue/conftest.py index b5c2aec3042..648337d7539 100644 --- a/tests/components/hue/conftest.py +++ b/tests/components/hue/conftest.py @@ -127,7 +127,6 @@ async def setup_bridge_for_sensors(hass, mock_bridge, hostname=None): domain=hue.DOMAIN, title="Mock Title", data={"host": hostname}, - system_options={}, ) mock_bridge.config_entry = config_entry hass.data[hue.DOMAIN] = {config_entry.entry_id: mock_bridge} diff --git a/tests/components/hue/test_bridge.py b/tests/components/hue/test_bridge.py index eb5c93862fe..034acf88efa 100644 --- a/tests/components/hue/test_bridge.py +++ b/tests/components/hue/test_bridge.py @@ -181,7 +181,6 @@ async def test_hue_activate_scene(hass, mock_api): "Mock Title", {"host": "mock-host", "username": "mock-username"}, "test", - system_options={}, options={CONF_ALLOW_HUE_GROUPS: True, CONF_ALLOW_UNREACHABLE: False}, ) hue_bridge = bridge.HueBridge(hass, config_entry) @@ -215,7 +214,6 @@ async def test_hue_activate_scene_transition(hass, mock_api): "Mock Title", {"host": "mock-host", "username": "mock-username"}, "test", - system_options={}, options={CONF_ALLOW_HUE_GROUPS: True, CONF_ALLOW_UNREACHABLE: False}, ) hue_bridge = bridge.HueBridge(hass, config_entry) @@ -249,7 +247,6 @@ async def test_hue_activate_scene_group_not_found(hass, mock_api): "Mock Title", {"host": "mock-host", "username": "mock-username"}, "test", - system_options={}, options={CONF_ALLOW_HUE_GROUPS: True, CONF_ALLOW_UNREACHABLE: False}, ) hue_bridge = bridge.HueBridge(hass, config_entry) @@ -278,7 +275,6 @@ async def test_hue_activate_scene_scene_not_found(hass, mock_api): "Mock Title", {"host": "mock-host", "username": "mock-username"}, "test", - system_options={}, options={CONF_ALLOW_HUE_GROUPS: True, CONF_ALLOW_UNREACHABLE: False}, ) hue_bridge = bridge.HueBridge(hass, config_entry) diff --git a/tests/components/hue/test_light.py b/tests/components/hue/test_light.py index 5efb74d015f..f4f663c23ae 100644 --- a/tests/components/hue/test_light.py +++ b/tests/components/hue/test_light.py @@ -179,7 +179,6 @@ async def setup_bridge(hass, mock_bridge): "Mock Title", {"host": "mock-host"}, "test", - system_options={}, ) mock_bridge.config_entry = config_entry hass.data[hue.DOMAIN] = {config_entry.entry_id: mock_bridge} diff --git a/tests/components/huisbaasje/test_init.py b/tests/components/huisbaasje/test_init.py index dde62a9c78b..390dc6c304d 100644 --- a/tests/components/huisbaasje/test_init.py +++ b/tests/components/huisbaasje/test_init.py @@ -41,7 +41,6 @@ async def test_setup_entry(hass: HomeAssistant): CONF_PASSWORD: "password", }, source="test", - system_options={}, ) config_entry.add_to_hass(hass) @@ -81,7 +80,6 @@ async def test_setup_entry_error(hass: HomeAssistant): CONF_PASSWORD: "password", }, source="test", - system_options={}, ) config_entry.add_to_hass(hass) @@ -122,7 +120,6 @@ async def test_unload_entry(hass: HomeAssistant): CONF_PASSWORD: "password", }, source="test", - system_options={}, ) config_entry.add_to_hass(hass) diff --git a/tests/components/huisbaasje/test_sensor.py b/tests/components/huisbaasje/test_sensor.py index c753c89627d..45ce20af628 100644 --- a/tests/components/huisbaasje/test_sensor.py +++ b/tests/components/huisbaasje/test_sensor.py @@ -34,7 +34,6 @@ async def test_setup_entry(hass: HomeAssistant): CONF_PASSWORD: "password", }, source="test", - system_options={}, ) config_entry.add_to_hass(hass) @@ -90,7 +89,6 @@ async def test_setup_entry_absent_measurement(hass: HomeAssistant): CONF_PASSWORD: "password", }, source="test", - system_options={}, ) config_entry.add_to_hass(hass) diff --git a/tests/components/hvv_departures/test_config_flow.py b/tests/components/hvv_departures/test_config_flow.py index 4a18e639315..9c510bb3db0 100644 --- a/tests/components/hvv_departures/test_config_flow.py +++ b/tests/components/hvv_departures/test_config_flow.py @@ -256,7 +256,6 @@ async def test_options_flow(hass): title="Wartenau", data=FIXTURE_CONFIG_ENTRY, source=SOURCE_USER, - system_options={"disable_new_entities": False}, options=FIXTURE_OPTIONS, unique_id="1234", ) @@ -306,7 +305,6 @@ async def test_options_flow_invalid_auth(hass): title="Wartenau", data=FIXTURE_CONFIG_ENTRY, source=SOURCE_USER, - system_options={"disable_new_entities": False}, options=FIXTURE_OPTIONS, unique_id="1234", ) @@ -346,7 +344,6 @@ async def test_options_flow_cannot_connect(hass): title="Wartenau", data=FIXTURE_CONFIG_ENTRY, source=SOURCE_USER, - system_options={"disable_new_entities": False}, options=FIXTURE_OPTIONS, unique_id="1234", ) diff --git a/tests/components/smartthings/conftest.py b/tests/components/smartthings/conftest.py index be822371030..2a7b5ed7084 100644 --- a/tests/components/smartthings/conftest.py +++ b/tests/components/smartthings/conftest.py @@ -61,7 +61,6 @@ async def setup_platform(hass, platform: str, *, devices=None, scenes=None): "Test", {CONF_INSTALLED_APP_ID: str(uuid4())}, SOURCE_USER, - system_options={}, ) broker = DeviceBroker( hass, config_entry, Mock(), Mock(), devices or [], scenes or [] diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 62a52b500f9..d583cad86c3 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -943,7 +943,6 @@ async def test_restoring_client(hass, aioclient_mock): title="Mock Title", data=ENTRY_CONFIG, source="test", - system_options={}, options={}, entry_id=1, ) diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index 5c4a65e0a78..ad277f18a8d 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -793,7 +793,6 @@ async def test_restore_client_succeed(hass, aioclient_mock): title="Mock Title", data=ENTRY_CONFIG, source="test", - system_options={}, options={}, entry_id=1, ) @@ -884,7 +883,6 @@ async def test_restore_client_no_old_state(hass, aioclient_mock): title="Mock Title", data=ENTRY_CONFIG, source="test", - system_options={}, options={}, entry_id=1, ) diff --git a/tests/components/zwave/test_lock.py b/tests/components/zwave/test_lock.py index f265b36dcb6..04d46620013 100644 --- a/tests/components/zwave/test_lock.py +++ b/tests/components/zwave/test_lock.py @@ -286,7 +286,6 @@ async def setup_ozw(hass, mock_openzwave): "Mock Title", {"usb_path": "mock-path", "network_key": "mock-key"}, "test", - system_options={}, ) await hass.config_entries.async_forward_entry_setup(config_entry, "lock") await hass.async_block_till_done() diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 944f02d46c0..65a46f33cd8 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -60,9 +60,7 @@ async def test_polling_only_updates_entities_it_should_poll(hass): async def test_polling_disabled_by_config_entry(hass): """Test the polling of only updated entities.""" entity_platform = MockEntityPlatform(hass) - entity_platform.config_entry = MockConfigEntry( - system_options={"disable_polling": True} - ) + entity_platform.config_entry = MockConfigEntry(pref_disable_polling=True) poll_ent = MockEntity(should_poll=True) diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index a124e1e6da1..fe445e32c96 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -513,12 +513,12 @@ async def test_disabled_by(registry): assert entry2.disabled_by is None -async def test_disabled_by_system_options(registry): - """Test system options setting disabled_by.""" +async def test_disabled_by_config_entry_pref(registry): + """Test config entry preference setting disabled_by.""" mock_config = MockConfigEntry( domain="light", entry_id="mock-id-1", - system_options={"disable_new_entities": True}, + pref_disable_new_entities=True, ) entry = registry.async_get_or_create( "light", "hue", "AAAA", config_entry=mock_config diff --git a/tests/helpers/test_update_coordinator.py b/tests/helpers/test_update_coordinator.py index a0ce751aed8..7023798f2b4 100644 --- a/tests/helpers/test_update_coordinator.py +++ b/tests/helpers/test_update_coordinator.py @@ -376,7 +376,7 @@ async def test_async_config_entry_first_refresh_success(crd, caplog): async def test_not_schedule_refresh_if_system_option_disable_polling(hass): """Test we do not schedule a refresh if disable polling in config entry.""" - entry = MockConfigEntry(system_options={"disable_polling": True}) + entry = MockConfigEntry(pref_disable_polling=True) config_entries.current_entry.set(entry) crd = get_crd(hass, DEFAULT_UPDATE_INTERVAL) crd.async_add_listener(lambda: None) diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index e9e864c4491..556f06fce54 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -574,6 +574,13 @@ async def test_saving_and_loading(hass): ) assert len(hass.config_entries.async_entries()) == 2 + entry_1 = hass.config_entries.async_entries()[0] + + hass.config_entries.async_update_entry( + entry_1, + pref_disable_polling=True, + pref_disable_new_entities=True, + ) # To trigger the call_later async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=1)) @@ -596,6 +603,8 @@ async def test_saving_and_loading(hass): assert orig.data == loaded.data assert orig.source == loaded.source assert orig.unique_id == loaded.unique_id + assert orig.pref_disable_new_entities == loaded.pref_disable_new_entities + assert orig.pref_disable_polling == loaded.pref_disable_polling async def test_forward_entry_sets_up_component(hass): @@ -813,14 +822,19 @@ async def test_updating_entry_system_options(manager): domain="test", data={"first": True}, state=config_entries.ConfigEntryState.SETUP_ERROR, - system_options={"disable_new_entities": True}, + pref_disable_new_entities=True, ) entry.add_to_manager(manager) - assert entry.system_options.disable_new_entities + assert entry.pref_disable_new_entities is True + assert entry.pref_disable_polling is False - entry.system_options.update(disable_new_entities=False) - assert not entry.system_options.disable_new_entities + manager.async_update_entry( + entry, pref_disable_new_entities=False, pref_disable_polling=True + ) + + assert entry.pref_disable_new_entities is False + assert entry.pref_disable_polling is True async def test_update_entry_options_and_trigger_listener(hass, manager): @@ -2557,48 +2571,18 @@ async def test_updating_entry_with_and_without_changes(manager): entry.add_to_manager(manager) assert manager.async_update_entry(entry) is False - assert manager.async_update_entry(entry, data={"second": True}) is True - assert manager.async_update_entry(entry, data={"second": True}) is False - assert ( - manager.async_update_entry(entry, data={"second": True, "third": 456}) is True - ) - assert ( - manager.async_update_entry(entry, data={"second": True, "third": 456}) is False - ) - assert manager.async_update_entry(entry, options={"second": True}) is True - assert manager.async_update_entry(entry, options={"second": True}) is False - assert ( - manager.async_update_entry(entry, options={"second": True, "third": "123"}) - is True - ) - assert ( - manager.async_update_entry(entry, options={"second": True, "third": "123"}) - is False - ) - assert ( - manager.async_update_entry(entry, system_options={"disable_new_entities": True}) - is True - ) - assert ( - manager.async_update_entry(entry, system_options={"disable_new_entities": True}) - is False - ) - assert ( - manager.async_update_entry( - entry, system_options={"disable_new_entities": False} - ) - is True - ) - assert ( - manager.async_update_entry( - entry, system_options={"disable_new_entities": False} - ) - is False - ) - assert manager.async_update_entry(entry, title="thetitle") is False - assert manager.async_update_entry(entry, title="newtitle") is True - assert manager.async_update_entry(entry, unique_id="abc123") is False - assert manager.async_update_entry(entry, unique_id="abc1234") is True + + for change in ( + {"data": {"second": True, "third": 456}}, + {"data": {"second": True}}, + {"options": {"hello": True}}, + {"pref_disable_new_entities": True}, + {"pref_disable_polling": True}, + {"title": "sometitle"}, + {"unique_id": "abcd1234"}, + ): + assert manager.async_update_entry(entry, **change) is True + assert manager.async_update_entry(entry, **change) is False async def test_entry_reload_calls_on_unload_listeners(hass, manager): @@ -2863,3 +2847,35 @@ async def test__async_abort_entries_match(hass, manager, matchers, reason): assert result["type"] == "abort" assert result["reason"] == reason + + +async def test_loading_old_data(hass, hass_storage): + """Test automatically migrating old data.""" + hass_storage[config_entries.STORAGE_KEY] = { + "version": 1, + "data": { + "entries": [ + { + "version": 5, + "domain": "my_domain", + "entry_id": "mock-id", + "data": {"my": "data"}, + "source": "user", + "title": "Mock title", + "system_options": {"disable_new_entities": True}, + } + ] + }, + } + manager = config_entries.ConfigEntries(hass, {}) + await manager.async_initialize() + + entries = manager.async_entries() + assert len(entries) == 1 + entry = entries[0] + assert entry.version == 5 + assert entry.domain == "my_domain" + assert entry.entry_id == "mock-id" + assert entry.title == "Mock title" + assert entry.data == {"my": "data"} + assert entry.pref_disable_new_entities is True From fc24b34408ff332a546ec6ec2c56d8d60cfbf152 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Tue, 1 Jun 2021 15:28:56 -0500 Subject: [PATCH 059/123] Handle incomplete Sonos alarm event payloads (#51353) --- homeassistant/components/sonos/speaker.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index acd53e1f877..957851dfbee 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -372,7 +372,8 @@ class SonosSpeaker: @callback def async_dispatch_alarms(self, event: SonosEvent) -> None: """Create a task to update alarms from an event.""" - update_id = event.variables["alarm_list_version"] + if not (update_id := event.variables.get("alarm_list_version")): + return if update_id in self.processed_alarm_events: return self.processed_alarm_events.append(update_id) From e4e3d5f81480d74d9a24312fc8cbfa828ec016fa Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 1 Jun 2021 22:35:13 +0200 Subject: [PATCH 060/123] Update frontend to 20210601.1 (#51354) --- 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 0e0685cd772..42f29f36976 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210601.0" + "home-assistant-frontend==20210601.1" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 4aa67a73e14..6dc7bedaab8 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ defusedxml==0.7.1 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.43.0 -home-assistant-frontend==20210601.0 +home-assistant-frontend==20210601.1 httpx==0.18.0 ifaddr==0.1.7 jinja2>=3.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index f33792126ad..a057d319362 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -765,7 +765,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210601.0 +home-assistant-frontend==20210601.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cfffb2c5dfc..8ad75872263 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -429,7 +429,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210601.0 +home-assistant-frontend==20210601.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From dc17d664eb6204846f0259935d0dc58c3c39c27b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 1 Jun 2021 13:39:48 -0700 Subject: [PATCH 061/123] Bumped version to 2021.6.0b5 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index c1364fc5596..8b6e95e005a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 6 -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, 8, 0) From f8d68e47b8aa4b1f0b829035e0c1f5195d5a0385 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 2 Jun 2021 10:00:24 +0200 Subject: [PATCH 062/123] Do not attempt to unload non loaded config entries (#51356) --- homeassistant/config_entries.py | 3 +++ tests/components/smartthings/test_binary_sensor.py | 2 ++ tests/components/smartthings/test_cover.py | 2 ++ tests/components/smartthings/test_fan.py | 2 ++ tests/components/smartthings/test_light.py | 2 ++ tests/components/smartthings/test_lock.py | 2 ++ tests/components/smartthings/test_scene.py | 2 ++ tests/components/smartthings/test_sensor.py | 2 ++ tests/components/smartthings/test_switch.py | 2 ++ 9 files changed, 19 insertions(+) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index eeaf0149cc2..49892937217 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -392,6 +392,9 @@ class ConfigEntry: self.reason = None return True + if self.state == ConfigEntryState.NOT_LOADED: + return True + if integration is None: try: integration = await loader.async_get_integration(hass, self.domain) diff --git a/tests/components/smartthings/test_binary_sensor.py b/tests/components/smartthings/test_binary_sensor.py index 7e8ab7d2c9b..f3d548c1e39 100644 --- a/tests/components/smartthings/test_binary_sensor.py +++ b/tests/components/smartthings/test_binary_sensor.py @@ -12,6 +12,7 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.components.smartthings import binary_sensor from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_UNAVAILABLE from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -91,6 +92,7 @@ async def test_unload_config_entry(hass, device_factory): "Motion Sensor 1", [Capability.motion_sensor], {Attribute.motion: "inactive"} ) config_entry = await setup_platform(hass, BINARY_SENSOR_DOMAIN, devices=[device]) + config_entry.state = ConfigEntryState.LOADED # Act await hass.config_entries.async_forward_entry_unload(config_entry, "binary_sensor") # Assert diff --git a/tests/components/smartthings/test_cover.py b/tests/components/smartthings/test_cover.py index 44c2b2f9285..aad7a4b037e 100644 --- a/tests/components/smartthings/test_cover.py +++ b/tests/components/smartthings/test_cover.py @@ -19,6 +19,7 @@ from homeassistant.components.cover import ( STATE_OPENING, ) from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_ENTITY_ID, STATE_UNAVAILABLE from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -191,6 +192,7 @@ async def test_unload_config_entry(hass, device_factory): "Garage", [Capability.garage_door_control], {Attribute.door: "open"} ) config_entry = await setup_platform(hass, COVER_DOMAIN, devices=[device]) + config_entry.state = ConfigEntryState.LOADED # Act await hass.config_entries.async_forward_entry_unload(config_entry, COVER_DOMAIN) # Assert diff --git a/tests/components/smartthings/test_fan.py b/tests/components/smartthings/test_fan.py index 6cdfa5b8917..2a66fc646c7 100644 --- a/tests/components/smartthings/test_fan.py +++ b/tests/components/smartthings/test_fan.py @@ -17,6 +17,7 @@ from homeassistant.components.fan import ( SUPPORT_SET_SPEED, ) from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, @@ -186,6 +187,7 @@ async def test_unload_config_entry(hass, device_factory): status={Attribute.switch: "off", Attribute.fan_speed: 0}, ) config_entry = await setup_platform(hass, FAN_DOMAIN, devices=[device]) + config_entry.state = ConfigEntryState.LOADED # Act await hass.config_entries.async_forward_entry_unload(config_entry, "fan") # Assert diff --git a/tests/components/smartthings/test_light.py b/tests/components/smartthings/test_light.py index c9dbb094161..81062adf934 100644 --- a/tests/components/smartthings/test_light.py +++ b/tests/components/smartthings/test_light.py @@ -19,6 +19,7 @@ from homeassistant.components.light import ( SUPPORT_TRANSITION, ) from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, @@ -306,6 +307,7 @@ async def test_unload_config_entry(hass, device_factory): }, ) config_entry = await setup_platform(hass, LIGHT_DOMAIN, devices=[device]) + config_entry.state = ConfigEntryState.LOADED # Act await hass.config_entries.async_forward_entry_unload(config_entry, "light") # Assert diff --git a/tests/components/smartthings/test_lock.py b/tests/components/smartthings/test_lock.py index 1168108656e..86c8d534a71 100644 --- a/tests/components/smartthings/test_lock.py +++ b/tests/components/smartthings/test_lock.py @@ -9,6 +9,7 @@ from pysmartthings.device import Status from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -103,6 +104,7 @@ async def test_unload_config_entry(hass, device_factory): # Arrange device = device_factory("Lock_1", [Capability.lock], {Attribute.lock: "locked"}) config_entry = await setup_platform(hass, LOCK_DOMAIN, devices=[device]) + config_entry.state = ConfigEntryState.LOADED # Act await hass.config_entries.async_forward_entry_unload(config_entry, "lock") # Assert diff --git a/tests/components/smartthings/test_scene.py b/tests/components/smartthings/test_scene.py index 647389eeb42..288fae046f5 100644 --- a/tests/components/smartthings/test_scene.py +++ b/tests/components/smartthings/test_scene.py @@ -5,6 +5,7 @@ The only mocking required is of the underlying SmartThings API object so real HTTP calls are not initiated during testing. """ from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON, STATE_UNAVAILABLE from homeassistant.helpers import entity_registry as er @@ -44,6 +45,7 @@ async def test_unload_config_entry(hass, scene): """Test the scene is removed when the config entry is unloaded.""" # Arrange config_entry = await setup_platform(hass, SCENE_DOMAIN, scenes=[scene]) + config_entry.state = ConfigEntryState.LOADED # Act await hass.config_entries.async_forward_entry_unload(config_entry, SCENE_DOMAIN) # Assert diff --git a/tests/components/smartthings/test_sensor.py b/tests/components/smartthings/test_sensor.py index 0f148b8931f..4af88e27fe4 100644 --- a/tests/components/smartthings/test_sensor.py +++ b/tests/components/smartthings/test_sensor.py @@ -9,6 +9,7 @@ from pysmartthings import ATTRIBUTES, CAPABILITIES, Attribute, Capability from homeassistant.components.sensor import DEVICE_CLASSES, DOMAIN as SENSOR_DOMAIN from homeassistant.components.smartthings import sensor from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, @@ -116,6 +117,7 @@ async def test_unload_config_entry(hass, device_factory): # Arrange device = device_factory("Sensor 1", [Capability.battery], {Attribute.battery: 100}) config_entry = await setup_platform(hass, SENSOR_DOMAIN, devices=[device]) + config_entry.state = ConfigEntryState.LOADED # Act await hass.config_entries.async_forward_entry_unload(config_entry, "sensor") # Assert diff --git a/tests/components/smartthings/test_switch.py b/tests/components/smartthings/test_switch.py index 21d508bcbc2..7c202fad12e 100644 --- a/tests/components/smartthings/test_switch.py +++ b/tests/components/smartthings/test_switch.py @@ -12,6 +12,7 @@ from homeassistant.components.switch import ( ATTR_TODAY_ENERGY_KWH, DOMAIN as SWITCH_DOMAIN, ) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -95,6 +96,7 @@ async def test_unload_config_entry(hass, device_factory): # Arrange device = device_factory("Switch 1", [Capability.switch], {Attribute.switch: "on"}) config_entry = await setup_platform(hass, SWITCH_DOMAIN, devices=[device]) + config_entry.state = ConfigEntryState.LOADED # Act await hass.config_entries.async_forward_entry_unload(config_entry, "switch") # Assert From b7153fe25fc6f8f3ced0b50c4fa8872c3faa4e44 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 1 Jun 2021 21:35:12 -0600 Subject: [PATCH 063/123] Bump pyiqvia to 1.0.0 (#51357) --- homeassistant/components/iqvia/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index 779e62de4fb..75249ded6a1 100644 --- a/homeassistant/components/iqvia/manifest.json +++ b/homeassistant/components/iqvia/manifest.json @@ -3,7 +3,7 @@ "name": "IQVIA", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iqvia", - "requirements": ["numpy==1.20.3", "pyiqvia==0.3.1"], + "requirements": ["numpy==1.20.3", "pyiqvia==1.0.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index a057d319362..759e4580a92 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1482,7 +1482,7 @@ pyipma==2.0.5 pyipp==0.11.0 # homeassistant.components.iqvia -pyiqvia==0.3.1 +pyiqvia==1.0.0 # homeassistant.components.irish_rail_transport pyirishrail==0.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8ad75872263..8b4244b4517 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -823,7 +823,7 @@ pyipma==2.0.5 pyipp==0.11.0 # homeassistant.components.iqvia -pyiqvia==0.3.1 +pyiqvia==1.0.0 # homeassistant.components.isy994 pyisy==3.0.0 From 089374b7e27cf38a398338367a97fcb8a24b6ab9 Mon Sep 17 00:00:00 2001 From: gadgetmobile <57815233+gadgetmobile@users.noreply.github.com> Date: Wed, 2 Jun 2021 14:02:37 +0200 Subject: [PATCH 064/123] Fix BleBox wLightBoxS and gateBox support (#51367) Co-authored-by: bbx-jp <83213200+bbx-jp@users.noreply.github.com> --- CODEOWNERS | 2 +- homeassistant/components/blebox/manifest.json | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index faa623456f1..2bee90dcf99 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -64,7 +64,7 @@ homeassistant/components/azure_service_bus/* @hfurubotten homeassistant/components/beewi_smartclim/* @alemuro homeassistant/components/bitcoin/* @fabaff homeassistant/components/bizkaibus/* @UgaitzEtxebarria -homeassistant/components/blebox/* @gadgetmobile +homeassistant/components/blebox/* @bbx-a @bbx-jp homeassistant/components/blink/* @fronzbot homeassistant/components/blueprint/* @home-assistant/core homeassistant/components/bmp280/* @belidzs diff --git a/homeassistant/components/blebox/manifest.json b/homeassistant/components/blebox/manifest.json index 00b4b61c507..39c0d37e2e3 100644 --- a/homeassistant/components/blebox/manifest.json +++ b/homeassistant/components/blebox/manifest.json @@ -3,7 +3,7 @@ "name": "BleBox devices", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/blebox", - "requirements": ["blebox_uniapi==1.3.2"], - "codeowners": ["@gadgetmobile"], + "requirements": ["blebox_uniapi==1.3.3"], + "codeowners": ["@bbx-a", "@bbx-jp"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 759e4580a92..5e976904b36 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -358,7 +358,7 @@ bimmer_connected==0.7.15 bizkaibus==0.1.1 # homeassistant.components.blebox -blebox_uniapi==1.3.2 +blebox_uniapi==1.3.3 # homeassistant.components.blink blinkpy==0.17.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8b4244b4517..0050f48917f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -211,7 +211,7 @@ bellows==0.24.0 bimmer_connected==0.7.15 # homeassistant.components.blebox -blebox_uniapi==1.3.2 +blebox_uniapi==1.3.3 # homeassistant.components.blink blinkpy==0.17.0 From 7938f69dc5f9e0a90918825cb90caca75ee4fba0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 2 Jun 2021 17:16:04 +0200 Subject: [PATCH 065/123] Fix Tibber timestamps parsing (#51368) --- homeassistant/components/tibber/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index 70dfa54c70a..f2ff23dfe5d 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -385,7 +385,7 @@ class TibberRtDataHandler: if live_measurement is None: return - timestamp = datetime.fromisoformat(live_measurement.pop("timestamp")) + timestamp = dt_util.parse_datetime(live_measurement.pop("timestamp")) new_entities = [] for sensor_type, state in live_measurement.items(): if state is None or sensor_type not in RT_SENSOR_MAP: From e3994e8029cc51faceac61b14fae409150a33405 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 2 Jun 2021 17:29:50 +0200 Subject: [PATCH 066/123] Bumped version to 2021.6.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 8b6e95e005a..229646d74d1 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 6 -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, 8, 0) From 768d49d7a6450b2159f7f49fca9cd99f8cd89d8c Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 4 Jun 2021 00:42:59 +0200 Subject: [PATCH 067/123] Fix last activity consideration for AVM Fritz!Tools device tracker (#51375) --- homeassistant/components/fritz/common.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index ec7e402f760..84288fe7fb3 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -217,21 +217,22 @@ class FritzDevice: """Update device info.""" utc_point_in_time = dt_util.utcnow() - if not self._name: - self._name = dev_info.name or self._mac.replace(":", "_") - - if not dev_home and self._last_activity: - self._connected = ( + if self._last_activity: + consider_home_evaluated = ( utc_point_in_time - self._last_activity ).total_seconds() < consider_home else: - self._connected = dev_home + consider_home_evaluated = dev_home - if self._connected: - self._ip_address = dev_info.ip_address + if not self._name: + self._name = dev_info.name or self._mac.replace(":", "_") + + self._connected = dev_home or consider_home_evaluated + + if dev_home: self._last_activity = utc_point_in_time - else: - self._ip_address = None + + self._ip_address = dev_info.ip_address if self._connected else None @property def is_connected(self): From 10a64f17cec188fdb04419505e95d7e91ab8d4f1 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 2 Jun 2021 23:10:27 -0500 Subject: [PATCH 068/123] Handle Sonos connection issues better when polling (#51376) --- homeassistant/components/sonos/entity.py | 6 +++++- homeassistant/components/sonos/media_player.py | 17 +++++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/sonos/entity.py b/homeassistant/components/sonos/entity.py index 8c47c69b2d7..7d4e168c960 100644 --- a/homeassistant/components/sonos/entity.py +++ b/homeassistant/components/sonos/entity.py @@ -5,6 +5,7 @@ import datetime import logging from pysonos.core import SoCo +from pysonos.exceptions import SoCoException import homeassistant.helpers.device_registry as dr from homeassistant.helpers.dispatcher import ( @@ -70,7 +71,10 @@ class SonosEntity(Entity): self.speaker.subscription_address, ) self.speaker.is_first_poll = False - await self.async_update() # pylint: disable=no-member + try: + await self.async_update() # pylint: disable=no-member + except (OSError, SoCoException) as ex: + _LOGGER.debug("Error connecting to %s: %s", self.entity_id, ex) @property def soco(self) -> SoCo: diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index e75400b06ab..1e083b69b61 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -13,7 +13,7 @@ from pysonos.core import ( PLAY_MODE_BY_MEANING, PLAY_MODES, ) -from pysonos.exceptions import SoCoException, SoCoUPnPException +from pysonos.exceptions import SoCoUPnPException import voluptuous as vol from homeassistant.components.media_player import MediaPlayerEntity @@ -293,18 +293,15 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): return STATE_IDLE async def async_update(self) -> None: - """Retrieve latest state.""" + """Retrieve latest state by polling.""" await self.hass.async_add_executor_job(self._update) def _update(self) -> None: - """Retrieve latest state.""" - try: - self.speaker.update_groups() - self.speaker.update_volume() - if self.speaker.is_coordinator: - self.speaker.update_media() - except SoCoException: - pass + """Retrieve latest state by polling.""" + self.speaker.update_groups() + self.speaker.update_volume() + if self.speaker.is_coordinator: + self.speaker.update_media() @property def volume_level(self) -> float | None: From 4cf2f49d7ee553a7dcf135bf291c83107e1bd747 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 3 Jun 2021 00:07:47 -0400 Subject: [PATCH 069/123] Fix no value error for heatit climate entities (#51392) --- .../zwave_js/discovery_data_template.py | 2 +- tests/components/zwave_js/conftest.py | 18 + tests/components/zwave_js/test_climate.py | 10 + .../climate_heatit_z_trm3_no_value_state.json | 1250 +++++++++++++++++ 4 files changed, 1279 insertions(+), 1 deletion(-) create mode 100644 tests/fixtures/zwave_js/climate_heatit_z_trm3_no_value_state.json diff --git a/homeassistant/components/zwave_js/discovery_data_template.py b/homeassistant/components/zwave_js/discovery_data_template.py index 4a2a8d2da94..7962b6b1c05 100644 --- a/homeassistant/components/zwave_js/discovery_data_template.py +++ b/homeassistant/components/zwave_js/discovery_data_template.py @@ -100,7 +100,7 @@ class DynamicCurrentTempClimateDataTemplate(BaseDiscoverySchemaDataTemplate): lookup_table: dict[str | int, ZwaveValue | None] = resolved_data["lookup_table"] dependent_value: ZwaveValue | None = resolved_data["dependent_value"] - if dependent_value: + if dependent_value and dependent_value.value is not None: lookup_key = dependent_value.metadata.states[ str(dependent_value.value) ].split("-")[0] diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 12db8bafb77..caddbb050a5 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -261,6 +261,14 @@ def climate_heatit_z_trm2fx_state_fixture(): return json.loads(load_fixture("zwave_js/climate_heatit_z_trm2fx_state.json")) +@pytest.fixture(name="climate_heatit_z_trm3_no_value_state", scope="session") +def climate_heatit_z_trm3_no_value_state_fixture(): + """Load the climate HEATIT Z-TRM3 thermostat node w/no value state fixture data.""" + return json.loads( + load_fixture("zwave_js/climate_heatit_z_trm3_no_value_state.json") + ) + + @pytest.fixture(name="nortek_thermostat_state", scope="session") def nortek_thermostat_state_fixture(): """Load the nortek thermostat node state fixture data.""" @@ -517,6 +525,16 @@ def climate_eurotronic_spirit_z_fixture(client, climate_eurotronic_spirit_z_stat return node +@pytest.fixture(name="climate_heatit_z_trm3_no_value") +def climate_heatit_z_trm3_no_value_fixture( + client, climate_heatit_z_trm3_no_value_state +): + """Mock a climate radio HEATIT Z-TRM3 node.""" + node = Node(client, copy.deepcopy(climate_heatit_z_trm3_no_value_state)) + client.driver.controller.nodes[node.node_id] = node + return node + + @pytest.fixture(name="climate_heatit_z_trm3") def climate_heatit_z_trm3_fixture(client, climate_heatit_z_trm3_state): """Mock a climate radio HEATIT Z-TRM3 node.""" diff --git a/tests/components/zwave_js/test_climate.py b/tests/components/zwave_js/test_climate.py index a1b86b14ebc..f86052b3692 100644 --- a/tests/components/zwave_js/test_climate.py +++ b/tests/components/zwave_js/test_climate.py @@ -437,6 +437,16 @@ async def test_setpoint_thermostat(hass, client, climate_danfoss_lc_13, integrat client.async_send_command_no_wait.reset_mock() +async def test_thermostat_heatit_z_trm3_no_value( + hass, client, climate_heatit_z_trm3_no_value, integration +): + """Test a heatit Z-TRM3 entity that is missing a value.""" + # When the config parameter that specifies what sensor to use has no value, we fall + # back to the first temperature sensor found on the device + state = hass.states.get(CLIMATE_FLOOR_THERMOSTAT_ENTITY) + assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 22.5 + + async def test_thermostat_heatit_z_trm3( hass, client, climate_heatit_z_trm3, integration ): diff --git a/tests/fixtures/zwave_js/climate_heatit_z_trm3_no_value_state.json b/tests/fixtures/zwave_js/climate_heatit_z_trm3_no_value_state.json new file mode 100644 index 00000000000..50886b504a7 --- /dev/null +++ b/tests/fixtures/zwave_js/climate_heatit_z_trm3_no_value_state.json @@ -0,0 +1,1250 @@ +{ + "nodeId": 74, + "index": 0, + "installerIcon": 4608, + "userIcon": 4609, + "status": 4, + "ready": true, + "isListening": true, + "isRouting": true, + "isSecure": false, + "manufacturerId": 411, + "productId": 515, + "productType": 3, + "firmwareVersion": "4.0", + "zwavePlusVersion": 1, + "deviceConfig": { + "filename": "/usr/src/node_modules/@zwave-js/config/config/devices/0x019b/z-trm3.json", + "manufacturer": "ThermoFloor", + "manufacturerId": 411, + "label": "Heatit Z-TRM3", + "description": "Floor thermostat", + "devices": [ + { + "productType": 3, + "productId": 515 + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "paramInformation": { + "_map": {} + }, + "compat": { + "valueIdRegex": {}, + "overrideFloatEncoding": { + "size": 2 + }, + "addCCs": {} + }, + "isEmbedded": true + }, + "label": "Heatit Z-TRM3", + "endpointCountIsDynamic": false, + "endpointsHaveIdenticalCapabilities": false, + "individualEndpointCount": 4, + "aggregatedEndpointCount": 0, + "interviewAttempts": 0, + "endpoints": [ + { + "nodeId": 74, + "index": 0, + "installerIcon": 4608, + "userIcon": 4609, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 8, + "label": "Thermostat" + }, + "specific": { + "key": 6, + "label": "General Thermostat V2" + }, + "mandatorySupportedCCs": [32, 114, 64, 67, 134], + "mandatoryControlledCCs": [] + } + }, + { + "nodeId": 74, + "index": 1, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 8, + "label": "Thermostat" + }, + "specific": { + "key": 6, + "label": "General Thermostat V2" + }, + "mandatorySupportedCCs": [32, 114, 64, 67, 134], + "mandatoryControlledCCs": [] + } + }, + { + "nodeId": 74, + "index": 2, + "installerIcon": 3328, + "userIcon": 3329, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 33, + "label": "Multilevel Sensor" + }, + "specific": { + "key": 1, + "label": "Routing Multilevel Sensor" + }, + "mandatorySupportedCCs": [32, 49], + "mandatoryControlledCCs": [] + } + }, + { + "nodeId": 74, + "index": 3, + "installerIcon": 3328, + "userIcon": 3329, + "deviceClass": null + }, + { + "nodeId": 74, + "index": 4, + "installerIcon": 3328, + "userIcon": 3329, + "deviceClass": null + } + ], + "values": [ + { + "endpoint": 0, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyKey": 66049, + "propertyName": "value", + "propertyKeyName": "Electric_W_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumed [W]", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 2 + }, + "unit": "W" + }, + "value": 0.17 + }, + { + "endpoint": 0, + "commandClass": 96, + "commandClassName": "Multi Channel", + "property": "endpointIndizes", + "propertyName": "endpointIndizes", + "ccVersion": 4, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": [1, 2, 3, 4] + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 2, + "propertyName": "Sensor mode", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Sensor mode", + "default": 1, + "min": 0, + "max": 4, + "states": { + "0": "F-mode, floor sensor mode", + "1": "A-mode, internal room sensor mode", + "2": "AF-mode, internal sensor and floor sensor mode", + "3": "A2-mode, external room sensor mode", + "4": "A2F-mode, external sensor with floor limitation" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 3, + "propertyName": "Floor sensor type", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Floor sensor type", + "default": 0, + "min": 0, + "max": 5, + "states": { + "0": "10K-NTC", + "1": "12K-NTC", + "2": "15K-NTC", + "3": "22K-NTC", + "4": "33K-NTC", + "5": "47K-NTC" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 4, + "propertyName": "Temperature control hysteresis (DIFF I)", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Temperature control hysteresis (DIFF I)", + "default": 5, + "min": 3, + "max": 30, + "unit": ".1\u00b0C", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 5, + "propertyName": "Floor minimum temperature limit (FLo)", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Floor minimum temperature limit (FLo)", + "default": 50, + "min": 50, + "max": 400, + "unit": ".1\u00b0C", + "valueSize": 2, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 6, + "propertyName": "Floor maximum temperature (FHi)", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Floor maximum temperature (FHi)", + "default": 400, + "min": 50, + "max": 400, + "unit": "0.1\u00b0C", + "valueSize": 2, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 7, + "propertyName": "Air minimum temperature limit (ALo)", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Air minimum temperature limit (ALo)", + "default": 50, + "min": 50, + "max": 400, + "unit": ".1\u00b0C", + "valueSize": 2, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 8, + "propertyName": "Air maximum temperature limit (AHi)", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Air maximum temperature limit (AHi)", + "default": 400, + "min": 50, + "max": 400, + "unit": ".1\u00b0C", + "valueSize": 2, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 10, + "propertyName": "Room sensor calibration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Room sensor calibration", + "default": 0, + "min": -60, + "max": 60, + "unit": ".1\u00b0C", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 11, + "propertyName": "Floor sensor calibration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Floor sensor calibration", + "default": 0, + "min": -60, + "max": 60, + "unit": ".1\u00b0C", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 12, + "propertyName": "External sensor calibration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "External sensor calibration", + "default": 0, + "min": -60, + "max": 60, + "unit": ".1\u00b0C", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 13, + "propertyName": "Temperature display", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Selects which temperature is shown on the display.", + "label": "Temperature display", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "Display setpoint temperature", + "1": "Display calculated temperature" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 14, + "propertyName": "Button brightness - dimmed state", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Button brightness - dimmed state", + "default": 50, + "min": 0, + "max": 100, + "unit": "%", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 15, + "propertyName": "Button brightness - active state", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Button brightness - active state", + "default": 100, + "min": 0, + "max": 100, + "unit": "%", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 16, + "propertyName": "Display brightness - dimmed state", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Display brightness - dimmed state", + "default": 50, + "min": 0, + "max": 100, + "unit": "%", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 17, + "propertyName": "Display brightness - active state", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Display brightness - active state", + "default": 100, + "min": 0, + "max": 100, + "unit": "%", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 18, + "propertyName": "Temperature report interval", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Temperature report interval", + "default": 60, + "min": 0, + "max": 32767, + "unit": "seconds", + "valueSize": 2, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 19, + "propertyName": "Temperature report hysteresis", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Temperature report hysteresis", + "default": 10, + "min": 1, + "max": 100, + "unit": "\u00b0C/10", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 20, + "propertyName": "Meter report interval", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Meter report interval", + "default": 90, + "min": 0, + "max": 32767, + "unit": "seconds", + "valueSize": 2, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 21, + "propertyName": "Meter report delta value", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Meter report delta value", + "default": 10, + "min": 0, + "max": 255, + "unit": "kWh/10", + "valueSize": 1, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Manufacturer ID", + "min": 0, + "max": 65535 + }, + "value": 411 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product type", + "min": 0, + "max": 65535 + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product ID", + "min": 0, + "max": 65535 + }, + "value": 515 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Library type" + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "6.7" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": ["4.0", "3.2"] + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version" + }, + "value": 2 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "sdkVersion", + "propertyName": "sdkVersion", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": "6.81.6" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationFrameworkAPIVersion", + "propertyName": "applicationFrameworkAPIVersion", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": "4.3.0" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationFrameworkBuildNumber", + "propertyName": "applicationFrameworkBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": 52445 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hostInterfaceVersion", + "propertyName": "hostInterfaceVersion", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": "unused" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hostInterfaceBuildNumber", + "propertyName": "hostInterfaceBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "zWaveProtocolVersion", + "propertyName": "zWaveProtocolVersion", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": "6.7.0" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "zWaveProtocolBuildNumber", + "propertyName": "zWaveProtocolBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": 97 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationVersion", + "propertyName": "applicationVersion", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": "4.0.10" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationBuildNumber", + "propertyName": "applicationBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": 52445 + }, + { + "endpoint": 1, + "commandClass": 64, + "commandClassName": "Thermostat Mode", + "property": "mode", + "propertyName": "mode", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Thermostat mode", + "min": 0, + "max": 255, + "states": { + "0": "Off", + "1": "Heat" + } + }, + "value": 1 + }, + { + "endpoint": 1, + "commandClass": 64, + "commandClassName": "Thermostat Mode", + "property": "manufacturerData", + "propertyName": "manufacturerData", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + } + }, + { + "endpoint": 1, + "commandClass": 67, + "commandClassName": "Thermostat Setpoint", + "property": "setpoint", + "propertyKey": 1, + "propertyName": "setpoint", + "propertyKeyName": "Heating", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "ccSpecific": { + "setpointType": 1 + }, + "min": 5, + "max": 35, + "unit": "\u00b0C" + }, + "value": 8 + }, + { + "endpoint": 1, + "commandClass": 66, + "commandClassName": "Thermostat Operating State", + "property": "state", + "propertyName": "state", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Operating state", + "min": 0, + "max": 255, + "states": { + "0": "Idle", + "1": "Heating", + "2": "Cooling", + "3": "Fan Only", + "4": "Pending Heat", + "5": "Pending Cool", + "6": "Vent/Economizer", + "7": "Aux Heating", + "8": "2nd Stage Heating", + "9": "2nd Stage Cooling", + "10": "2nd Stage Aux Heat", + "11": "3rd Stage Aux Heat" + } + }, + "value": 0 + }, + { + "endpoint": 1, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyKey": 65537, + "propertyName": "value", + "propertyKeyName": "Electric_kWh_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumed [kWh]", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 0 + }, + "unit": "kWh" + }, + "value": 2422.8 + }, + { + "endpoint": 1, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyKey": 66561, + "propertyName": "value", + "propertyKeyName": "Electric_V_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumed [V]", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 4 + }, + "unit": "V" + }, + "value": 242.1 + }, + { + "endpoint": 2, + "commandClass": 32, + "commandClassName": "Basic", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current value", + "min": 0, + "max": 99 + }, + "value": 99 + }, + { + "endpoint": 2, + "commandClass": 32, + "commandClassName": "Basic", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Target value", + "min": 0, + "max": 99 + } + }, + { + "endpoint": 2, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "Air temperature", + "propertyName": "Air temperature", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Air temperature", + "ccSpecific": { + "sensorType": 1, + "scale": 0 + }, + "unit": "\u00b0C" + }, + "value": 22.5 + }, + { + "endpoint": 2, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "UNKNOWN (0x00)", + "propertyName": "UNKNOWN (0x00)", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "UNKNOWN (0x00)", + "ccSpecific": { + "sensorType": 0, + "scale": 0 + } + }, + "value": 23 + }, + { + "endpoint": 3, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "Air temperature", + "propertyName": "Air temperature", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Air temperature", + "ccSpecific": { + "sensorType": 1, + "scale": 0 + }, + "unit": "\u00b0C" + }, + "value": 0 + }, + { + "endpoint": 3, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "UNKNOWN (0x00)", + "propertyName": "UNKNOWN (0x00)", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "UNKNOWN (0x00)", + "ccSpecific": { + "sensorType": 0, + "scale": 0 + } + }, + "value": 0 + }, + { + "endpoint": 3, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "Time", + "propertyName": "Time", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Time", + "ccSpecific": { + "sensorType": 33, + "scale": 0 + }, + "unit": "s" + }, + "value": 3.2 + }, + { + "endpoint": 4, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "Air temperature", + "propertyName": "Air temperature", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Air temperature", + "ccSpecific": { + "sensorType": 1, + "scale": 0 + }, + "unit": "\u00b0C" + }, + "value": 15.3 + } + ], + "neighbors": [1, 24, 25, 87, 88], + "isFrequentListening": false, + "maxDataRate": 100000, + "supportedDataRates": [40000, 100000], + "protocolVersion": 3, + "supportsBeaming": true, + "supportsSecurity": false, + "nodeType": 1, + "zwavePlusNodeType": 0, + "zwavePlusRoleType": 5, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 8, + "label": "Thermostat" + }, + "specific": { + "key": 6, + "label": "General Thermostat V2" + }, + "mandatorySupportedCCs": [32, 114, 64, 67, 134], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 50, + "name": "Meter", + "version": 3, + "isSecure": false + }, + { + "id": 64, + "name": "Thermostat Mode", + "version": 3, + "isSecure": false + }, + { + "id": 66, + "name": "Thermostat Operating State", + "version": 1, + "isSecure": false + }, + { + "id": 67, + "name": "Thermostat Setpoint", + "version": 3, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 96, + "name": "Multi Channel", + "version": 4, + "isSecure": false + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 3, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 1, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 4, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 3, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + } + ], + "interviewStage": "Complete" +} From 3bbf1e7c834b8bdc77e034209a626a012e62cff2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Thu, 3 Jun 2021 11:59:22 +0200 Subject: [PATCH 070/123] Fix Tibber Pulse device name and sensor update (#51402) --- homeassistant/components/tibber/sensor.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index f2ff23dfe5d..b60d88e9814 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -186,6 +186,7 @@ class TibberSensor(SensorEntity): """Initialize the sensor.""" self._tibber_home = tibber_home self._home_name = tibber_home.info["viewer"]["home"]["appNickname"] + self._device_name = None if self._home_name is None: self._home_name = tibber_home.info["viewer"]["home"]["address"].get( "address1", "" @@ -202,7 +203,7 @@ class TibberSensor(SensorEntity): """Return the device_info of the device.""" device_info = { "identifiers": {(TIBBER_DOMAIN, self.device_id)}, - "name": self.name, + "name": self._device_name, "manufacturer": MANUFACTURER, } if self._model is not None: @@ -237,6 +238,8 @@ class TibberSensorElPrice(TibberSensor): self._attr_unique_id = f"{self._tibber_home.home_id}" self._model = "Price Sensor" + self._device_name = self._attr_name + async def async_update(self): """Get the latest data and updates the states.""" now = dt_util.now() @@ -295,6 +298,7 @@ class TibberSensorRT(TibberSensor): super().__init__(tibber_home) self._sensor_name = sensor_name self._model = "Tibber Pulse" + self._device_name = f"{self._model} {self._home_name}" self._attr_device_class = device_class self._attr_name = f"{self._sensor_name} {self._home_name}" @@ -330,7 +334,7 @@ class TibberSensorRT(TibberSensor): self.async_on_remove( async_dispatcher_connect( self.hass, - SIGNAL_UPDATE_ENTITY.format(self._sensor_name), + SIGNAL_UPDATE_ENTITY.format(self.unique_id), self._set_state, ) ) @@ -370,7 +374,7 @@ class TibberRtDataHandler: self._async_add_entities = async_add_entities self._tibber_home = tibber_home self.hass = hass - self._entities = set() + self._entities = {} async def async_callback(self, payload): """Handle received data.""" @@ -393,7 +397,7 @@ class TibberRtDataHandler: if sensor_type in self._entities: async_dispatcher_send( self.hass, - SIGNAL_UPDATE_ENTITY.format(RT_SENSOR_MAP[sensor_type][0]), + SIGNAL_UPDATE_ENTITY.format(self._entities[sensor_type]), state, timestamp, ) @@ -412,6 +416,6 @@ class TibberRtDataHandler: state_class, ) new_entities.append(entity) - self._entities.add(sensor_type) + self._entities[sensor_type] = entity.unique_id if new_entities: self._async_add_entities(new_entities) From 46ad5eeb63c73d81fff16ad011da0aca2a627e33 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Thu, 3 Jun 2021 12:40:00 +0200 Subject: [PATCH 071/123] Fix shopping list "complete all" service name (#51406) --- homeassistant/components/shopping_list/services.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/shopping_list/services.yaml b/homeassistant/components/shopping_list/services.yaml index 9f5437701ed..7bf209550d7 100644 --- a/homeassistant/components/shopping_list/services.yaml +++ b/homeassistant/components/shopping_list/services.yaml @@ -34,7 +34,7 @@ incomplete_item: text: complete_all: - name: Complete call + name: Complete all description: Marks all items as completed in the shopping list. It does not remove the items. incomplete_all: From 08e58e9f7dea887087679d4dc1ef2d164f85215e Mon Sep 17 00:00:00 2001 From: Jc2k Date: Thu, 3 Jun 2021 21:51:09 +0100 Subject: [PATCH 072/123] Bump aiohomekit to 0.2.67 (fixes #51391) (#51418) --- 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 c18ee9e574f..7ff32e402fe 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==0.2.66"], + "requirements": ["aiohomekit==0.2.67"], "zeroconf": ["_hap._tcp.local."], "after_dependencies": ["zeroconf"], "codeowners": ["@Jc2k", "@bdraco"], diff --git a/requirements_all.txt b/requirements_all.txt index 5e976904b36..39d3fecf3c0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -175,7 +175,7 @@ aioguardian==1.0.4 aioharmony==0.2.7 # homeassistant.components.homekit_controller -aiohomekit==0.2.66 +aiohomekit==0.2.67 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0050f48917f..abb534bec65 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -112,7 +112,7 @@ aioguardian==1.0.4 aioharmony==0.2.7 # homeassistant.components.homekit_controller -aiohomekit==0.2.66 +aiohomekit==0.2.67 # homeassistant.components.emulated_hue # homeassistant.components.http From e8aa578acc6a175b0ca81aaf01851b0a81e07209 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 3 Jun 2021 15:11:45 -0400 Subject: [PATCH 073/123] Bump zwave-js-server-python to 0.26.1 (#51425) --- homeassistant/components/zwave_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 5ce65fcbb35..fd342b8d498 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave JS", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", - "requirements": ["zwave-js-server-python==0.26.0"], + "requirements": ["zwave-js-server-python==0.26.1"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["http", "websocket_api"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 39d3fecf3c0..3fe7373588a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2442,4 +2442,4 @@ zigpy==0.33.0 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.26.0 +zwave-js-server-python==0.26.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index abb534bec65..1d53662bbea 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1324,4 +1324,4 @@ zigpy-znp==0.5.1 zigpy==0.33.0 # homeassistant.components.zwave_js -zwave-js-server-python==0.26.0 +zwave-js-server-python==0.26.1 From eb45714c1644064e160306415b3f4681e1492ecd Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 4 Jun 2021 00:41:59 +0200 Subject: [PATCH 074/123] Update frontend to 20210603.0 (#51442) --- 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 42f29f36976..8f5f5f091d9 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210601.1" + "home-assistant-frontend==20210603.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 6dc7bedaab8..b4d8e99e54a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ defusedxml==0.7.1 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.43.0 -home-assistant-frontend==20210601.1 +home-assistant-frontend==20210603.0 httpx==0.18.0 ifaddr==0.1.7 jinja2>=3.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index 3fe7373588a..96fee9fe3a2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -765,7 +765,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210601.1 +home-assistant-frontend==20210603.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1d53662bbea..d970fb55180 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -429,7 +429,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210601.1 +home-assistant-frontend==20210603.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From a3c50229569de43b1bec21977fda415f0a6f3650 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 3 Jun 2021 15:45:09 -0700 Subject: [PATCH 075/123] Bumped version to 2021.6.1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 229646d74d1..690ea282734 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 6 -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, 8, 0) From 6f4302ff7026fec143c2fd6c2cf05dcfcc73896c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 4 Jun 2021 10:02:42 -0700 Subject: [PATCH 076/123] Hot fix version of Apply modbus interval patch (#51487) --- homeassistant/components/modbus/__init__.py | 20 +++++++++++--------- homeassistant/components/modbus/const.py | 1 - 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index d1c3c1a0c8a..e27549df169 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -99,7 +99,6 @@ from .const import ( DEFAULT_SCAN_INTERVAL, DEFAULT_STRUCTURE_PREFIX, DEFAULT_TEMP_UNIT, - MINIMUM_SCAN_INTERVAL, MODBUS_DOMAIN as DOMAIN, PLATFORMS, ) @@ -139,27 +138,30 @@ def control_scan_interval(config: dict) -> dict: for entry in hub[conf_key]: scan_interval = entry.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) - if scan_interval < MINIMUM_SCAN_INTERVAL: - if scan_interval == 0: - continue + if scan_interval == 0: + continue + if scan_interval < 5: _LOGGER.warning( - "%s %s scan_interval(%d) is adjusted to minimum(%d)", + "%s %s scan_interval(%d) is lower than 5 seconds, " + "which may cause Home Assistant stability issues", component, entry.get(CONF_NAME), scan_interval, - MINIMUM_SCAN_INTERVAL, ) - scan_interval = MINIMUM_SCAN_INTERVAL entry[CONF_SCAN_INTERVAL] = scan_interval minimum_scan_interval = min(scan_interval, minimum_scan_interval) - if CONF_TIMEOUT in hub and hub[CONF_TIMEOUT] > minimum_scan_interval - 1: + if ( + CONF_TIMEOUT in hub + and hub[CONF_TIMEOUT] > minimum_scan_interval - 1 + and minimum_scan_interval > 1 + ): _LOGGER.warning( "Modbus %s timeout(%d) is adjusted(%d) due to scan_interval", hub.get(CONF_NAME, ""), hub[CONF_TIMEOUT], minimum_scan_interval - 1, ) - hub[CONF_TIMEOUT] = minimum_scan_interval - 1 + hub[CONF_TIMEOUT] = minimum_scan_interval - 1 return config diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py index dfec0dbb50a..cfda4a3863a 100644 --- a/homeassistant/components/modbus/const.py +++ b/homeassistant/components/modbus/const.py @@ -90,7 +90,6 @@ SERVICE_WRITE_REGISTER = "write_register" # integration names DEFAULT_HUB = "modbus_hub" -MINIMUM_SCAN_INTERVAL = 5 # seconds DEFAULT_SCAN_INTERVAL = 15 # seconds DEFAULT_SLAVE = 1 DEFAULT_STRUCTURE_PREFIX = ">f" From 756a4c2ea6d034525a2285c6ee8720e572ccd3db Mon Sep 17 00:00:00 2001 From: Florent Thoumie Date: Thu, 3 Jun 2021 23:32:01 -0700 Subject: [PATCH 077/123] Update to iaqualink 0.3.90 (#51452) --- homeassistant/components/iaqualink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/iaqualink/manifest.json b/homeassistant/components/iaqualink/manifest.json index b3aa257a9b2..26c6e0b4bfd 100644 --- a/homeassistant/components/iaqualink/manifest.json +++ b/homeassistant/components/iaqualink/manifest.json @@ -4,6 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iaqualink/", "codeowners": ["@flz"], - "requirements": ["iaqualink==0.3.4"], + "requirements": ["iaqualink==0.3.90"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 96fee9fe3a2..01df5ffd38a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -807,7 +807,7 @@ hyperion-py==0.7.4 iammeter==0.1.7 # homeassistant.components.iaqualink -iaqualink==0.3.4 +iaqualink==0.3.90 # homeassistant.components.watson_tts ibm-watson==5.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d970fb55180..dab75a4f205 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -457,7 +457,7 @@ huisbaasje-client==0.1.0 hyperion-py==0.7.4 # homeassistant.components.iaqualink -iaqualink==0.3.4 +iaqualink==0.3.90 # homeassistant.components.ping icmplib==2.1.1 From 42c74c1e14e21ae4f8ad153495a3ef2a364aac6a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 3 Jun 2021 20:54:45 -1000 Subject: [PATCH 078/123] Retry isy994 setup later if isy.initialize times out (#51453) Maybe fixes https://forum.universal-devices.com/topic/26633-home-assistant-isy-component/?do=findComment&comment=312147 --- homeassistant/components/isy994/__init__.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index 27d81a671c8..99905e4d946 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -1,6 +1,7 @@ """Support the ISY-994 controllers.""" from __future__ import annotations +import asyncio from urllib.parse import urlparse from aiohttp import CookieJar @@ -171,8 +172,14 @@ async def async_setup_entry( ) try: - with async_timeout.timeout(30): + async with async_timeout.timeout(30): await isy.initialize() + except asyncio.TimeoutError as err: + _LOGGER.error( + "Timed out initializing the ISY; device may be busy, trying again later: %s", + err, + ) + raise ConfigEntryNotReady from err except ISYInvalidAuthError as err: _LOGGER.error( "Invalid credentials for the ISY, please adjust settings and try again: %s", From c8655c8e374f7b63dcdfe2cd31e76e9261be0228 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Mon, 31 May 2021 09:58:48 +0200 Subject: [PATCH 079/123] xknx 0.18.3 (#51277) --- 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 0a722d46162..b1ed504f7ad 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -2,7 +2,7 @@ "domain": "knx", "name": "KNX", "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==0.18.2"], + "requirements": ["xknx==0.18.3"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "silver", "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 01df5ffd38a..fbd09d8977b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2375,7 +2375,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.knx -xknx==0.18.2 +xknx==0.18.3 # homeassistant.components.bluesound # homeassistant.components.rest diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dab75a4f205..83c02a03269 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1281,7 +1281,7 @@ wolf_smartset==0.1.8 xbox-webapi==2.0.11 # homeassistant.components.knx -xknx==0.18.2 +xknx==0.18.3 # homeassistant.components.bluesound # homeassistant.components.rest From e10a4c2a91211dfbd81e1c39262aa365a1e94b7e Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Fri, 4 Jun 2021 08:34:16 +0200 Subject: [PATCH 080/123] Update xknx to version 0.18.4 (#51459) --- 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 b1ed504f7ad..6b1d4d328ac 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -2,7 +2,7 @@ "domain": "knx", "name": "KNX", "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==0.18.3"], + "requirements": ["xknx==0.18.4"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "silver", "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index fbd09d8977b..a3889f79c14 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2375,7 +2375,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.knx -xknx==0.18.3 +xknx==0.18.4 # homeassistant.components.bluesound # homeassistant.components.rest diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 83c02a03269..3e390cddde6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1281,7 +1281,7 @@ wolf_smartset==0.1.8 xbox-webapi==2.0.11 # homeassistant.components.knx -xknx==0.18.3 +xknx==0.18.4 # homeassistant.components.bluesound # homeassistant.components.rest From d9c6c3719ce8550455e6459a580f45204eee78a5 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Fri, 4 Jun 2021 16:26:44 +0100 Subject: [PATCH 081/123] Bump aiolyric to 1.0.7 (#51473) --- homeassistant/components/lyric/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lyric/manifest.json b/homeassistant/components/lyric/manifest.json index 6317c6c3357..fbcc9567c3a 100644 --- a/homeassistant/components/lyric/manifest.json +++ b/homeassistant/components/lyric/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/lyric", "dependencies": ["http"], - "requirements": ["aiolyric==1.0.6"], + "requirements": ["aiolyric==1.0.7"], "codeowners": ["@timmo001"], "quality_scale": "silver", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index a3889f79c14..decf8ff2967 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -203,7 +203,7 @@ aiolifx_effects==0.2.2 aiolip==1.1.4 # homeassistant.components.lyric -aiolyric==1.0.6 +aiolyric==1.0.7 # homeassistant.components.keyboard_remote aionotify==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3e390cddde6..009cfd2c790 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -128,7 +128,7 @@ aiokafka==0.6.0 aiolip==1.1.4 # homeassistant.components.lyric -aiolyric==1.0.6 +aiolyric==1.0.7 # homeassistant.components.notion aionotion==1.1.0 From 6dcde1b2a67dd1d81a27f369e59bf5826dd7487b Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Fri, 4 Jun 2021 18:02:39 +0200 Subject: [PATCH 082/123] Improve logging for SamsungTV (#51477) --- homeassistant/components/samsungtv/bridge.py | 10 ++++++---- homeassistant/components/samsungtv/config_flow.py | 3 +++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index 84b518a4633..7e1c24c6d2f 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -184,7 +184,9 @@ class SamsungTVLegacyBridge(SamsungTVBridge): if self._remote is None: # We need to create a new instance to reconnect. try: - LOGGER.debug("Create SamsungRemote") + LOGGER.debug( + "Create SamsungTVLegacyBridge for %s (%s)", CONF_NAME, self.host + ) self._remote = Remote(self.config.copy()) # This is only happening when the auth was switched to DENY # A removed auth will lead to socket timeout because waiting for auth popup is just an open socket @@ -199,7 +201,7 @@ class SamsungTVLegacyBridge(SamsungTVBridge): def stop(self): """Stop Bridge.""" - LOGGER.debug("Stopping SamsungRemote") + LOGGER.debug("Stopping SamsungTVLegacyBridge") self.close_remote() @@ -272,7 +274,7 @@ class SamsungTVWSBridge(SamsungTVBridge): # We need to create a new instance to reconnect. try: LOGGER.debug( - "Create SamsungTVWS for %s (%s)", VALUE_CONF_NAME, self.host + "Create SamsungTVWSBridge for %s (%s)", CONF_NAME, self.host ) self._remote = SamsungTVWS( host=self.host, @@ -293,5 +295,5 @@ class SamsungTVWSBridge(SamsungTVBridge): def stop(self): """Stop Bridge.""" - LOGGER.debug("Stopping SamsungTVWS") + LOGGER.debug("Stopping SamsungTVWSBridge") self.close_remote() diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index 46800e1653b..7c6dea56b96 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -224,6 +224,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_ssdp(self, discovery_info: DiscoveryInfoType): """Handle a flow initialized by ssdp discovery.""" + LOGGER.debug("Samsung device found via SSDP: %s", discovery_info) self._udn = _strip_uuid(discovery_info[ATTR_UPNP_UDN]) await self._async_set_unique_id_from_udn() await self._async_start_discovery_for_host( @@ -242,6 +243,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_dhcp(self, discovery_info: DiscoveryInfoType): """Handle a flow initialized by dhcp discovery.""" + LOGGER.debug("Samsung device found via DHCP: %s", discovery_info) self._mac = discovery_info[MAC_ADDRESS] await self._async_start_discovery_for_host(discovery_info[IP_ADDRESS]) await self._async_set_device_unique_id() @@ -250,6 +252,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf(self, discovery_info: DiscoveryInfoType): """Handle a flow initialized by zeroconf discovery.""" + LOGGER.debug("Samsung device found via ZEROCONF: %s", discovery_info) self._mac = format_mac(discovery_info[ATTR_PROPERTIES]["deviceid"]) await self._async_start_discovery_for_host(discovery_info[CONF_HOST]) await self._async_set_device_unique_id() From 8f741e0c6f4d036a8b501b13ed1d140ddf2af6ac Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 4 Jun 2021 18:02:59 +0200 Subject: [PATCH 083/123] Upgrade elgato to 2.1.1 (#51483) --- homeassistant/components/elgato/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/elgato/manifest.json b/homeassistant/components/elgato/manifest.json index dbb83f18995..7a095cb5917 100644 --- a/homeassistant/components/elgato/manifest.json +++ b/homeassistant/components/elgato/manifest.json @@ -3,7 +3,7 @@ "name": "Elgato Light", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/elgato", - "requirements": ["elgato==2.1.0"], + "requirements": ["elgato==2.1.1"], "zeroconf": ["_elg._tcp.local."], "codeowners": ["@frenck"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index decf8ff2967..43f477f9682 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -533,7 +533,7 @@ ebusdpy==0.0.16 ecoaliface==0.4.0 # homeassistant.components.elgato -elgato==2.1.0 +elgato==2.1.1 # homeassistant.components.eliqonline eliqonline==1.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 009cfd2c790..183124f95f0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -291,7 +291,7 @@ dsmr_parser==0.29 dynalite_devices==0.1.46 # homeassistant.components.elgato -elgato==2.1.0 +elgato==2.1.1 # homeassistant.components.elkm1 elkm1-lib==0.8.10 From f5dd8384099d5524abf1629804b0f365793ba849 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 4 Jun 2021 09:14:18 -0700 Subject: [PATCH 084/123] Protect our user agent (#51486) * Protect our user agent * Fix expected error --- homeassistant/helpers/aiohttp_client.py | 8 +++++++- tests/helpers/test_aiohttp_client.py | 11 +++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index 0bb9a815c84..696f2d40cb8 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -6,6 +6,7 @@ from collections.abc import Awaitable from contextlib import suppress from ssl import SSLContext import sys +from types import MappingProxyType from typing import Any, Callable, cast import aiohttp @@ -95,9 +96,14 @@ def _async_create_clientsession( """Create a new ClientSession with kwargs, i.e. for cookies.""" clientsession = aiohttp.ClientSession( connector=_async_get_connector(hass, verify_ssl), - headers={USER_AGENT: SERVER_SOFTWARE}, **kwargs, ) + # Prevent packages accidentally overriding our default headers + # It's important that we identify as Home Assistant + # If a package requires a different user agent, override it by passing a headers + # dictionary to the request method. + # pylint: disable=protected-access + clientsession._default_headers = MappingProxyType({USER_AGENT: SERVER_SOFTWARE}) # type: ignore clientsession.close = warn_use(clientsession.close, WARN_CLOSE_MSG) # type: ignore diff --git a/tests/helpers/test_aiohttp_client.py b/tests/helpers/test_aiohttp_client.py index e6f113c7699..f68c7ba2181 100644 --- a/tests/helpers/test_aiohttp_client.py +++ b/tests/helpers/test_aiohttp_client.py @@ -195,3 +195,14 @@ async def test_async_aiohttp_proxy_stream_client_err(aioclient_mock, camera_clie resp = await camera_client.get("/api/camera_proxy_stream/camera.config_test") assert resp.status == 502 + + +async def test_client_session_immutable_headers(hass): + """Test we can't mutate headers.""" + session = client.async_get_clientsession(hass) + + with pytest.raises(TypeError): + session.headers["user-agent"] = "bla" + + with pytest.raises(AttributeError): + session.headers.update({"user-agent": "bla"}) From eb2f5c28a9a59197de579b232eafef38a9744a9a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 4 Jun 2021 10:07:09 -0700 Subject: [PATCH 085/123] Bumped version to 2021.6.2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 690ea282734..87e53643240 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 6 -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, 8, 0) From 0f3b73e75fc5d4afd6787abaca63b47d0a06df98 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Fri, 4 Jun 2021 18:03:13 -0300 Subject: [PATCH 086/123] Use a single job to ping all devices in the Broadlink integration (#51466) --- homeassistant/components/broadlink/heartbeat.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/broadlink/heartbeat.py b/homeassistant/components/broadlink/heartbeat.py index 282df3ae6a8..b4deffa5b81 100644 --- a/homeassistant/components/broadlink/heartbeat.py +++ b/homeassistant/components/broadlink/heartbeat.py @@ -44,11 +44,15 @@ class BroadlinkHeartbeat: """Send packets to feed watchdog timers.""" hass = self._hass config_entries = hass.config_entries.async_entries(DOMAIN) + hosts = {entry.data[CONF_HOST] for entry in config_entries} + await hass.async_add_executor_job(self.heartbeat, hosts) - for entry in config_entries: - host = entry.data[CONF_HOST] + @staticmethod + def heartbeat(hosts): + """Send packets to feed watchdog timers.""" + for host in hosts: try: - await hass.async_add_executor_job(blk.ping, host) + blk.ping(host) except OSError as err: _LOGGER.debug("Failed to send heartbeat to %s: %s", host, err) else: From 703b088f861581bd3013005f975964e6565c454d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Jun 2021 08:21:10 -1000 Subject: [PATCH 087/123] Fix loop in tod binary sensor (#51491) Co-authored-by: Paulus Schoutsen --- homeassistant/components/tod/binary_sensor.py | 22 +- tests/components/tod/test_binary_sensor.py | 219 +++++++++++++++++- 2 files changed, 237 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tod/binary_sensor.py b/homeassistant/components/tod/binary_sensor.py index 4fd9a3b8bf9..8264468e2e7 100644 --- a/homeassistant/components/tod/binary_sensor.py +++ b/homeassistant/components/tod/binary_sensor.py @@ -156,6 +156,26 @@ class TodSensor(BinarySensorEntity): self._time_after += self._after_offset self._time_before += self._before_offset + def _turn_to_next_day(self): + """Turn to to the next day.""" + if is_sun_event(self._after): + self._time_after = get_astral_event_next( + self.hass, self._after, self._time_after - self._after_offset + ) + self._time_after += self._after_offset + else: + # Offset is already there + self._time_after += timedelta(days=1) + + if is_sun_event(self._before): + self._time_before = get_astral_event_next( + self.hass, self._before, self._time_before - self._before_offset + ) + self._time_before += self._before_offset + else: + # Offset is already there + self._time_before += timedelta(days=1) + async def async_added_to_hass(self): """Call when entity about to be added to Home Assistant.""" self._calculate_boudary_time() @@ -182,7 +202,7 @@ class TodSensor(BinarySensorEntity): if now < self._time_before: self._next_update = self._time_before return - self._calculate_boudary_time() + self._turn_to_next_day() self._next_update = self._time_after @callback diff --git a/tests/components/tod/test_binary_sensor.py b/tests/components/tod/test_binary_sensor.py index 8b63082c36c..ef8088d6aab 100644 --- a/tests/components/tod/test_binary_sensor.py +++ b/tests/components/tod/test_binary_sensor.py @@ -12,6 +12,8 @@ import homeassistant.util.dt as dt_util from tests.common import assert_setup_component +ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE + @pytest.fixture(autouse=True) def mock_legacy_time(legacy_patchable_time): @@ -26,6 +28,13 @@ def setup_fixture(hass): hass.config.longitude = 18.98583 +@pytest.fixture(autouse=True) +def restore_timezone(hass): + """Make sure we change timezone.""" + yield + dt_util.set_default_time_zone(ORIG_TIMEZONE) + + async def test_setup(hass): """Test the setup.""" config = { @@ -863,6 +872,7 @@ async def test_sun_offset(hass): async def test_dst(hass): """Test sun event with offset.""" hass.config.time_zone = "CET" + dt_util.set_default_time_zone(dt_util.get_time_zone("CET")) test_time = datetime(2019, 3, 30, 3, 0, 0, tzinfo=dt_util.UTC) config = { "binary_sensor": [ @@ -882,7 +892,210 @@ async def test_dst(hass): await hass.async_block_till_done() state = hass.states.get(entity_id) - assert state.attributes["after"] == "2019-03-30T03:30:00+01:00" - assert state.attributes["before"] == "2019-03-30T03:40:00+01:00" - assert state.attributes["next_update"] == "2019-03-30T03:30:00+01:00" + assert state.attributes["after"] == "2019-03-31T03:30:00+02:00" + assert state.attributes["before"] == "2019-03-31T03:40:00+02:00" + assert state.attributes["next_update"] == "2019-03-31T03:30:00+02:00" assert state.state == STATE_OFF + + +async def test_simple_before_after_does_not_loop_utc_not_in_range(hass): + """Test simple before after.""" + hass.config.time_zone = "UTC" + dt_util.set_default_time_zone(dt_util.UTC) + test_time = datetime(2019, 1, 10, 18, 43, 0, tzinfo=dt_util.UTC) + config = { + "binary_sensor": [ + { + "platform": "tod", + "name": "Night", + "before": "06:00", + "after": "22:00", + } + ] + } + with patch( + "homeassistant.components.tod.binary_sensor.dt_util.utcnow", + return_value=test_time, + ): + await async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.night") + assert state.state == STATE_OFF + assert state.attributes["after"] == "2019-01-10T22:00:00+00:00" + assert state.attributes["before"] == "2019-01-11T06:00:00+00:00" + assert state.attributes["next_update"] == "2019-01-10T22:00:00+00:00" + + +async def test_simple_before_after_does_not_loop_utc_in_range(hass): + """Test simple before after.""" + hass.config.time_zone = "UTC" + dt_util.set_default_time_zone(dt_util.UTC) + test_time = datetime(2019, 1, 10, 22, 43, 0, tzinfo=dt_util.UTC) + config = { + "binary_sensor": [ + { + "platform": "tod", + "name": "Night", + "before": "06:00", + "after": "22:00", + } + ] + } + with patch( + "homeassistant.components.tod.binary_sensor.dt_util.utcnow", + return_value=test_time, + ): + await async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.night") + assert state.state == STATE_ON + assert state.attributes["after"] == "2019-01-10T22:00:00+00:00" + assert state.attributes["before"] == "2019-01-11T06:00:00+00:00" + assert state.attributes["next_update"] == "2019-01-11T06:00:00+00:00" + + +async def test_simple_before_after_does_not_loop_utc_fire_at_before(hass): + """Test simple before after.""" + hass.config.time_zone = "UTC" + dt_util.set_default_time_zone(dt_util.UTC) + test_time = datetime(2019, 1, 11, 6, 0, 0, tzinfo=dt_util.UTC) + config = { + "binary_sensor": [ + { + "platform": "tod", + "name": "Night", + "before": "06:00", + "after": "22:00", + } + ] + } + with patch( + "homeassistant.components.tod.binary_sensor.dt_util.utcnow", + return_value=test_time, + ): + await async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.night") + assert state.state == STATE_OFF + assert state.attributes["after"] == "2019-01-11T22:00:00+00:00" + assert state.attributes["before"] == "2019-01-12T06:00:00+00:00" + assert state.attributes["next_update"] == "2019-01-11T22:00:00+00:00" + + +async def test_simple_before_after_does_not_loop_utc_fire_at_after(hass): + """Test simple before after.""" + hass.config.time_zone = "UTC" + dt_util.set_default_time_zone(dt_util.UTC) + test_time = datetime(2019, 1, 10, 22, 0, 0, tzinfo=dt_util.UTC) + config = { + "binary_sensor": [ + { + "platform": "tod", + "name": "Night", + "before": "06:00", + "after": "22:00", + } + ] + } + with patch( + "homeassistant.components.tod.binary_sensor.dt_util.utcnow", + return_value=test_time, + ): + await async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.night") + assert state.state == STATE_ON + assert state.attributes["after"] == "2019-01-10T22:00:00+00:00" + assert state.attributes["before"] == "2019-01-11T06:00:00+00:00" + assert state.attributes["next_update"] == "2019-01-11T06:00:00+00:00" + + +async def test_simple_before_after_does_not_loop_utc_both_before_now(hass): + """Test simple before after.""" + hass.config.time_zone = "UTC" + dt_util.set_default_time_zone(dt_util.UTC) + test_time = datetime(2019, 1, 10, 22, 0, 0, tzinfo=dt_util.UTC) + config = { + "binary_sensor": [ + { + "platform": "tod", + "name": "Morning", + "before": "08:00", + "after": "00:00", + } + ] + } + with patch( + "homeassistant.components.tod.binary_sensor.dt_util.utcnow", + return_value=test_time, + ): + await async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.morning") + assert state.state == STATE_OFF + assert state.attributes["after"] == "2019-01-11T00:00:00+00:00" + assert state.attributes["before"] == "2019-01-11T08:00:00+00:00" + assert state.attributes["next_update"] == "2019-01-11T00:00:00+00:00" + + +async def test_simple_before_after_does_not_loop_berlin_not_in_range(hass): + """Test simple before after.""" + hass.config.time_zone = "Europe/Berlin" + dt_util.set_default_time_zone(dt_util.get_time_zone("Europe/Berlin")) + test_time = datetime(2019, 1, 10, 18, 43, 0, tzinfo=dt_util.UTC) + config = { + "binary_sensor": [ + { + "platform": "tod", + "name": "Dark", + "before": "06:00", + "after": "00:00", + } + ] + } + with patch( + "homeassistant.components.tod.binary_sensor.dt_util.utcnow", + return_value=test_time, + ): + await async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.dark") + assert state.state == STATE_OFF + assert state.attributes["after"] == "2019-01-11T00:00:00+01:00" + assert state.attributes["before"] == "2019-01-11T06:00:00+01:00" + assert state.attributes["next_update"] == "2019-01-11T00:00:00+01:00" + + +async def test_simple_before_after_does_not_loop_berlin_in_range(hass): + """Test simple before after.""" + hass.config.time_zone = "Europe/Berlin" + dt_util.set_default_time_zone(dt_util.get_time_zone("Europe/Berlin")) + test_time = datetime(2019, 1, 10, 23, 43, 0, tzinfo=dt_util.UTC) + config = { + "binary_sensor": [ + { + "platform": "tod", + "name": "Dark", + "before": "06:00", + "after": "00:00", + } + ] + } + with patch( + "homeassistant.components.tod.binary_sensor.dt_util.utcnow", + return_value=test_time, + ): + await async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.dark") + assert state.state == STATE_ON + assert state.attributes["after"] == "2019-01-11T00:00:00+01:00" + assert state.attributes["before"] == "2019-01-11T06:00:00+01:00" + assert state.attributes["next_update"] == "2019-01-11T06:00:00+01:00" From 96ade688d5b62fdf709a647aedf24245f193bb5a Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Mon, 7 Jun 2021 19:21:24 +0100 Subject: [PATCH 088/123] AsusWRT fix keyerror when firmver is missing from info (#51499) Co-authored-by: Franck Nijhof Co-authored-by: Paulus Schoutsen --- homeassistant/components/asuswrt/router.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/asuswrt/router.py b/homeassistant/components/asuswrt/router.py index 0912869abb7..134cf960aae 100644 --- a/homeassistant/components/asuswrt/router.py +++ b/homeassistant/components/asuswrt/router.py @@ -228,10 +228,10 @@ class AsusWrtRouter: # System model = await _get_nvram_info(self._api, "MODEL") - if model: + if model and "model" in model: self._model = model["model"] firmware = await _get_nvram_info(self._api, "FIRMWARE") - if firmware: + if firmware and "firmver" in firmware and "buildno" in firmware: self._sw_v = f"{firmware['firmver']} (build {firmware['buildno']})" # Load tracked entities from registry From 89db8d45199cdbf7ce23f22939ff6e44433af88b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 5 Jun 2021 00:13:12 -1000 Subject: [PATCH 089/123] Handle missing options in foreign_key for MSSQL (#51503) --- homeassistant/components/recorder/migration.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 8e6c4861739..02c74635f03 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -280,10 +280,10 @@ def _update_states_table_with_foreign_key_options(connection, engine): for foreign_key in inspector.get_foreign_keys(TABLE_STATES): if foreign_key["name"] and ( # MySQL/MariaDB will have empty options - not foreign_key["options"] + not foreign_key.get("options") or # Postgres will have ondelete set to None - foreign_key["options"].get("ondelete") is None + foreign_key.get("options", {}).get("ondelete") is None ): alters.append( { @@ -319,7 +319,7 @@ def _drop_foreign_key_constraints(connection, engine, table, columns): for foreign_key in inspector.get_foreign_keys(table): if ( foreign_key["name"] - and foreign_key["options"].get("ondelete") + and foreign_key.get("options", {}).get("ondelete") and foreign_key["constrained_columns"] == columns ): drops.append(ForeignKeyConstraint((), (), name=foreign_key["name"])) From ed68a268ad4d1ce194896a291354d54228a89c56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Sat, 5 Jun 2021 11:50:56 +0200 Subject: [PATCH 090/123] Fix missing Tibber power production (#51505) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/tibber/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index b60d88e9814..660bbb741b0 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -45,6 +45,7 @@ SIGNAL_UPDATE_ENTITY = "tibber_rt_update_{}" RT_SENSOR_MAP = { "averagePower": ["average power", DEVICE_CLASS_POWER, POWER_WATT, None], "power": ["power", DEVICE_CLASS_POWER, POWER_WATT, None], + "powerProduction": ["power production", DEVICE_CLASS_POWER, POWER_WATT, None], "minPower": ["min power", DEVICE_CLASS_POWER, POWER_WATT, None], "maxPower": ["max power", DEVICE_CLASS_POWER, POWER_WATT, None], "accumulatedConsumption": [ From b21076c599dca8f2987f53c52c3a73ef1f5e88f7 Mon Sep 17 00:00:00 2001 From: Ron Klinkien Date: Sat, 5 Jun 2021 12:07:52 +0200 Subject: [PATCH 091/123] Bump garminconnect_aio to 0.1.4 (#51507) --- homeassistant/components/garmin_connect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/garmin_connect/manifest.json b/homeassistant/components/garmin_connect/manifest.json index 2495249e4a4..22e115d0e06 100644 --- a/homeassistant/components/garmin_connect/manifest.json +++ b/homeassistant/components/garmin_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "garmin_connect", "name": "Garmin Connect", "documentation": "https://www.home-assistant.io/integrations/garmin_connect", - "requirements": ["garminconnect_aio==0.1.1"], + "requirements": ["garminconnect_aio==0.1.4"], "codeowners": ["@cyberjunky"], "config_flow": true, "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 43f477f9682..72f851f2215 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -635,7 +635,7 @@ gTTS==2.2.2 garages-amsterdam==2.1.1 # homeassistant.components.garmin_connect -garminconnect_aio==0.1.1 +garminconnect_aio==0.1.4 # homeassistant.components.geniushub geniushub-client==0.6.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 183124f95f0..cd7cde6e118 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -341,7 +341,7 @@ gTTS==2.2.2 garages-amsterdam==2.1.1 # homeassistant.components.garmin_connect -garminconnect_aio==0.1.1 +garminconnect_aio==0.1.4 # homeassistant.components.geo_json_events # homeassistant.components.usgs_earthquakes_feed From b06558fa0a4d6b87bb0faa1a1569d4fbbd84e069 Mon Sep 17 00:00:00 2001 From: Ludovico de Nittis Date: Sat, 5 Jun 2021 14:11:39 +0200 Subject: [PATCH 092/123] Bump pyialarm to 1.8.1 (#51519) --- homeassistant/components/ialarm/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ialarm/manifest.json b/homeassistant/components/ialarm/manifest.json index e112a26003e..08666129fd9 100644 --- a/homeassistant/components/ialarm/manifest.json +++ b/homeassistant/components/ialarm/manifest.json @@ -2,7 +2,7 @@ "domain": "ialarm", "name": "Antifurto365 iAlarm", "documentation": "https://www.home-assistant.io/integrations/ialarm", - "requirements": ["pyialarm==1.7"], + "requirements": ["pyialarm==1.8.1"], "codeowners": ["@RyuzakiKK"], "config_flow": true, "iot_class": "local_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 72f851f2215..95589423c7f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1464,7 +1464,7 @@ pyhomematic==0.1.72 pyhomeworks==0.0.6 # homeassistant.components.ialarm -pyialarm==1.7 +pyialarm==1.8.1 # homeassistant.components.icloud pyicloud==0.10.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cd7cde6e118..5f3171899e5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -808,7 +808,7 @@ pyhiveapi==0.4.2 pyhomematic==0.1.72 # homeassistant.components.ialarm -pyialarm==1.7 +pyialarm==1.8.1 # homeassistant.components.icloud pyicloud==0.10.2 From 8818df06636f80ae9d19a740f77608517fc06a37 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Mon, 7 Jun 2021 04:03:56 +1000 Subject: [PATCH 093/123] Improve log message when zone missing in geolocation trigger (#51522) * log warning message if zone cannot be found * improve log message * add test case --- .../components/geo_location/trigger.py | 11 +++++ tests/components/geo_location/test_trigger.py | 45 +++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/homeassistant/components/geo_location/trigger.py b/homeassistant/components/geo_location/trigger.py index 3cdefccfeab..4410d39c0a6 100644 --- a/homeassistant/components/geo_location/trigger.py +++ b/homeassistant/components/geo_location/trigger.py @@ -1,4 +1,6 @@ """Offer geolocation automation rules.""" +import logging + import voluptuous as vol from homeassistant.components.geo_location import DOMAIN @@ -10,6 +12,8 @@ from homeassistant.helpers.event import TrackStates, async_track_state_change_fi # mypy: allow-untyped-defs, no-check-untyped-defs +_LOGGER = logging.getLogger(__name__) + EVENT_ENTER = "enter" EVENT_LEAVE = "leave" DEFAULT_EVENT = EVENT_ENTER @@ -49,6 +53,13 @@ async def async_attach_trigger(hass, config, action, automation_info): return zone_state = hass.states.get(zone_entity_id) + if zone_state is None: + _LOGGER.warning( + "Unable to execute automation %s: Zone %s not found", + automation_info["name"], + zone_entity_id, + ) + return from_match = ( condition.zone(hass, zone_state, from_state) if from_state else False diff --git a/tests/components/geo_location/test_trigger.py b/tests/components/geo_location/test_trigger.py index e40b134e657..bc74f01f6f1 100644 --- a/tests/components/geo_location/test_trigger.py +++ b/tests/components/geo_location/test_trigger.py @@ -1,4 +1,6 @@ """The tests for the geolocation trigger.""" +import logging + import pytest from homeassistant.components import automation, zone @@ -318,3 +320,46 @@ async def test_if_fires_on_zone_disappear(hass, calls): assert ( calls[0].data["some"] == "geo_location - geo_location.entity - hello - - test" ) + + +async def test_zone_undefined(hass, calls, caplog): + """Test for undefined zone.""" + hass.states.async_set( + "geo_location.entity", + "hello", + {"latitude": 32.880586, "longitude": -117.237564, "source": "test_source"}, + ) + await hass.async_block_till_done() + + caplog.set_level(logging.WARNING) + + zone_does_not_exist = "zone.does_not_exist" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "geo_location", + "source": "test_source", + "zone": zone_does_not_exist, + "event": "leave", + }, + "action": {"service": "test.automation"}, + } + }, + ) + + hass.states.async_set( + "geo_location.entity", + "hello", + {"latitude": 32.881011, "longitude": -117.234758, "source": "test_source"}, + ) + await hass.async_block_till_done() + + assert len(calls) == 0 + + assert ( + f"Unable to execute automation automation 0: Zone {zone_does_not_exist} not found" + in caplog.text + ) From b807b8754b50255cae5fbe7bb8eb19322581e36c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 5 Jun 2021 12:02:36 -1000 Subject: [PATCH 094/123] Ensure host is always set with samsungtv SSDP discovery (#51527) There was a case where self._host could have been None before _async_set_unique_id_from_udn was called Fixes #51186 --- .../components/samsungtv/config_flow.py | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index 7c6dea56b96..a69a456df40 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -112,6 +112,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _async_set_unique_id_from_udn(self, raise_on_progress=True): """Set the unique id from the udn.""" + assert self._host is not None await self.async_set_unique_id(self._udn, raise_on_progress=raise_on_progress) self._async_update_existing_host_entry(self._host) updates = {CONF_HOST: self._host} @@ -206,30 +207,28 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return entry return None - async def _async_start_discovery_for_host(self, host): - """Start discovery for a host.""" - if entry := self._async_update_existing_host_entry(host): + async def _async_start_discovery(self): + """Start discovery.""" + assert self._host is not None + if entry := self._async_update_existing_host_entry(self._host): if entry.unique_id: # Let the flow continue to fill the missing # unique id as we may be able to obtain it # in the next step raise data_entry_flow.AbortFlow("already_configured") - self.context[CONF_HOST] = host + self.context[CONF_HOST] = self._host for progress in self._async_in_progress(): - if progress.get("context", {}).get(CONF_HOST) == host: + if progress.get("context", {}).get(CONF_HOST) == self._host: raise data_entry_flow.AbortFlow("already_in_progress") - self._host = host - async def async_step_ssdp(self, discovery_info: DiscoveryInfoType): """Handle a flow initialized by ssdp discovery.""" LOGGER.debug("Samsung device found via SSDP: %s", discovery_info) self._udn = _strip_uuid(discovery_info[ATTR_UPNP_UDN]) + self._host = urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname await self._async_set_unique_id_from_udn() - await self._async_start_discovery_for_host( - urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname - ) + await self._async_start_discovery() self._manufacturer = discovery_info[ATTR_UPNP_MANUFACTURER] if not self._manufacturer or not self._manufacturer.lower().startswith( "samsung" @@ -245,7 +244,8 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a flow initialized by dhcp discovery.""" LOGGER.debug("Samsung device found via DHCP: %s", discovery_info) self._mac = discovery_info[MAC_ADDRESS] - await self._async_start_discovery_for_host(discovery_info[IP_ADDRESS]) + self._host = discovery_info[IP_ADDRESS] + await self._async_start_discovery() await self._async_set_device_unique_id() self.context["title_placeholders"] = {"device": self._title} return await self.async_step_confirm() @@ -254,7 +254,8 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a flow initialized by zeroconf discovery.""" LOGGER.debug("Samsung device found via ZEROCONF: %s", discovery_info) self._mac = format_mac(discovery_info[ATTR_PROPERTIES]["deviceid"]) - await self._async_start_discovery_for_host(discovery_info[CONF_HOST]) + self._host = discovery_info[CONF_HOST] + await self._async_start_discovery() await self._async_set_device_unique_id() self.context["title_placeholders"] = {"device": self._title} return await self.async_step_confirm() From 45d94c7fc8006204efe0073107b6c15697e9b3b7 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 6 Jun 2021 00:31:11 -0600 Subject: [PATCH 095/123] Bump aiorecollect to 1.0.5 (#51538) --- homeassistant/components/recollect_waste/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/recollect_waste/manifest.json b/homeassistant/components/recollect_waste/manifest.json index e33edcc2ab5..550612fbea2 100644 --- a/homeassistant/components/recollect_waste/manifest.json +++ b/homeassistant/components/recollect_waste/manifest.json @@ -3,7 +3,7 @@ "name": "ReCollect Waste", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/recollect_waste", - "requirements": ["aiorecollect==1.0.4"], + "requirements": ["aiorecollect==1.0.5"], "codeowners": ["@bachya"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 95589423c7f..9902a6ab31f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -224,7 +224,7 @@ aiopvpc==2.1.2 aiopylgtv==0.4.0 # homeassistant.components.recollect_waste -aiorecollect==1.0.4 +aiorecollect==1.0.5 # homeassistant.components.shelly aioshelly==0.6.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5f3171899e5..b2d64664395 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -146,7 +146,7 @@ aiopvpc==2.1.2 aiopylgtv==0.4.0 # homeassistant.components.recollect_waste -aiorecollect==1.0.4 +aiorecollect==1.0.5 # homeassistant.components.shelly aioshelly==0.6.4 From 5016fd9fa8ec3487177add55d3298f070c2d71f6 Mon Sep 17 00:00:00 2001 From: Ron Klinkien Date: Mon, 7 Jun 2021 10:09:08 +0200 Subject: [PATCH 096/123] Fix garmin_connect config flow multiple account creation (#51542) --- .../components/garmin_connect/config_flow.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/garmin_connect/config_flow.py b/homeassistant/components/garmin_connect/config_flow.py index 8e26e2bf608..8f83a9e1071 100644 --- a/homeassistant/components/garmin_connect/config_flow.py +++ b/homeassistant/components/garmin_connect/config_flow.py @@ -39,14 +39,14 @@ class GarminConnectConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self._show_setup_form() websession = async_get_clientsession(self.hass) + username = user_input[CONF_USERNAME] + password = user_input[CONF_PASSWORD] - garmin_client = Garmin( - websession, user_input[CONF_USERNAME], user_input[CONF_PASSWORD] - ) + garmin_client = Garmin(websession, username, password) errors = {} try: - username = await garmin_client.login() + await garmin_client.login() except GarminConnectConnectionError: errors["base"] = "cannot_connect" return await self._show_setup_form(errors) @@ -68,7 +68,7 @@ class GarminConnectConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): title=username, data={ CONF_ID: username, - CONF_USERNAME: user_input[CONF_USERNAME], - CONF_PASSWORD: user_input[CONF_PASSWORD], + CONF_USERNAME: username, + CONF_PASSWORD: password, }, ) From 619e37b6003fc8b3318108b35a3edc60fb9c1cf5 Mon Sep 17 00:00:00 2001 From: stephan192 Date: Mon, 7 Jun 2021 10:53:36 +0200 Subject: [PATCH 097/123] Bump dwdwfsapi to 1.0.4 (#51556) --- homeassistant/components/dwd_weather_warnings/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/dwd_weather_warnings/manifest.json b/homeassistant/components/dwd_weather_warnings/manifest.json index 1550d9262a4..55c848ea219 100644 --- a/homeassistant/components/dwd_weather_warnings/manifest.json +++ b/homeassistant/components/dwd_weather_warnings/manifest.json @@ -3,6 +3,6 @@ "name": "Deutscher Wetterdienst (DWD) Weather Warnings", "documentation": "https://www.home-assistant.io/integrations/dwd_weather_warnings", "codeowners": ["@runningman84", "@stephan192", "@Hummel95"], - "requirements": ["dwdwfsapi==1.0.3"], + "requirements": ["dwdwfsapi==1.0.4"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 9902a6ab31f..48e5632b730 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -515,7 +515,7 @@ dovado==0.4.1 dsmr_parser==0.29 # homeassistant.components.dwd_weather_warnings -dwdwfsapi==1.0.3 +dwdwfsapi==1.0.4 # homeassistant.components.dweet dweepy==0.3.0 From 03f10333c44531e01cc35d32c54d670f74244a9f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 6 Jun 2021 23:49:37 -1000 Subject: [PATCH 098/123] Increase isy setup timeout to 60s (#51559) - Ensure errors are displayed in the UI --- homeassistant/components/isy994/__init__.py | 25 +++++++++------------ 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index 99905e4d946..51c34aeb0a7 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -172,14 +172,12 @@ async def async_setup_entry( ) try: - async with async_timeout.timeout(30): + async with async_timeout.timeout(60): await isy.initialize() except asyncio.TimeoutError as err: - _LOGGER.error( - "Timed out initializing the ISY; device may be busy, trying again later: %s", - err, - ) - raise ConfigEntryNotReady from err + raise ConfigEntryNotReady( + f"Timed out initializing the ISY; device may be busy, trying again later: {err}" + ) from err except ISYInvalidAuthError as err: _LOGGER.error( "Invalid credentials for the ISY, please adjust settings and try again: %s", @@ -187,16 +185,13 @@ async def async_setup_entry( ) return False except ISYConnectionError as err: - _LOGGER.error( - "Failed to connect to the ISY, please adjust settings and try again: %s", - err, - ) - raise ConfigEntryNotReady from err + raise ConfigEntryNotReady( + f"Failed to connect to the ISY, please adjust settings and try again: {err}" + ) from err except ISYResponseParseError as err: - _LOGGER.warning( - "Error processing responses from the ISY; device may be busy, trying again later" - ) - raise ConfigEntryNotReady from err + raise ConfigEntryNotReady( + f"Invalid XML response from ISY; Ensure the ISY is running the latest firmware: {err}" + ) from err _categorize_nodes(hass_isy_data, isy.nodes, ignore_identifier, sensor_identifier) _categorize_programs(hass_isy_data, isy.programs) From bd24431930ebfe39d31a61897c3c581fb527bce7 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 7 Jun 2021 04:46:56 -0500 Subject: [PATCH 099/123] Fix Sonos restore calls (#51565) --- homeassistant/components/sonos/speaker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 957851dfbee..58598648992 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -764,7 +764,7 @@ class SonosSpeaker: """Pause all current coordinators and restore groups.""" for speaker in (s for s in speakers if s.is_coordinator): if speaker.media.playback_status == SONOS_STATE_PLAYING: - hass.async_create_task(speaker.soco.pause()) + speaker.soco.pause() groups = [] From 70f4907414bd8104040a95eb446257d5cccb4b00 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 7 Jun 2021 19:16:47 +0200 Subject: [PATCH 100/123] Update builder to 2021.06.2 (#51582) --- .github/workflows/builder.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 190c449cf3c..607af99fb51 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -115,7 +115,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2021.05.0 + uses: home-assistant/builder@2021.06.2 with: args: | $BUILD_ARGS \ @@ -167,7 +167,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2021.05.0 + uses: home-assistant/builder@2021.06.2 with: args: | $BUILD_ARGS \ From 4dd875199f98c2009030aeebdd9913b0f12c9bab Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 7 Jun 2021 23:14:42 +0200 Subject: [PATCH 101/123] Fix deprecated value_template for MQTT light (#51587) --- homeassistant/components/mqtt/light/schema_basic.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 3e347363428..684dcf337aa 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -186,9 +186,6 @@ async def async_setup_entity_basic( hass, config, async_add_entities, config_entry, discovery_data=None ): """Set up a MQTT Light.""" - if CONF_STATE_VALUE_TEMPLATE not in config and CONF_VALUE_TEMPLATE in config: - config[CONF_STATE_VALUE_TEMPLATE] = config[CONF_VALUE_TEMPLATE] - async_add_entities([MqttLight(hass, config, config_entry, discovery_data)]) @@ -236,6 +233,9 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): def _setup_from_config(self, config): """(Re)Setup the entity.""" + if CONF_STATE_VALUE_TEMPLATE not in config and CONF_VALUE_TEMPLATE in config: + config[CONF_STATE_VALUE_TEMPLATE] = config[CONF_VALUE_TEMPLATE] + topic = { key: config.get(key) for key in ( From 6b38480caffc41bf1ba7c9b8a4a2422e8789f150 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 7 Jun 2021 14:15:39 -0700 Subject: [PATCH 102/123] Bumped version to 2021.6.3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 87e53643240..3b1fc4705a8 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 6 -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, 8, 0) From cca1b426bb2064d9bd01811b42a8bdc26f04b839 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 7 Jun 2021 17:29:17 -0500 Subject: [PATCH 103/123] Fix Sonos battery sensors on S1 firmware (#51585) --- homeassistant/components/sonos/speaker.py | 31 ++++++++++++----------- tests/components/sonos/test_sensor.py | 13 ++++------ 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 58598648992..055b4ce6845 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -173,7 +173,7 @@ class SonosSpeaker: self.zone_name = speaker_info["zone_name"] # Battery - self.battery_info: dict[str, Any] | None = None + self.battery_info: dict[str, Any] = {} self._last_battery_event: datetime.datetime | None = None self._battery_poll_timer: Callable | None = None @@ -208,21 +208,15 @@ class SonosSpeaker: self.hass, f"{SONOS_SEEN}-{self.soco.uid}", self.async_seen ) - if (battery_info := fetch_battery_info_or_none(self.soco)) is None: - self._platforms_ready.update({BINARY_SENSOR_DOMAIN, SENSOR_DOMAIN}) - else: + if battery_info := fetch_battery_info_or_none(self.soco): self.battery_info = battery_info - # Only create a polling task if successful, may fail on S1 firmware - if battery_info: - # Battery events can be infrequent, polling is still necessary - self._battery_poll_timer = self.hass.helpers.event.track_time_interval( - self.async_poll_battery, BATTERY_SCAN_INTERVAL - ) - else: - _LOGGER.warning( - "S1 firmware detected, battery sensor may update infrequently" - ) + # Battery events can be infrequent, polling is still necessary + self._battery_poll_timer = self.hass.helpers.event.track_time_interval( + self.async_poll_battery, BATTERY_SCAN_INTERVAL + ) dispatcher_send(self.hass, SONOS_CREATE_BATTERY, self) + else: + self._platforms_ready.update({BINARY_SENSOR_DOMAIN, SENSOR_DOMAIN}) if new_alarms := self.update_alarms_for_speaker(): dispatcher_send(self.hass, SONOS_CREATE_ALARM, self, new_alarms) @@ -386,7 +380,7 @@ class SonosSpeaker: async def async_update_device_properties(self, event: SonosEvent) -> None: """Update device properties from an event.""" - if (more_info := event.variables.get("more_info")) is not None: + if more_info := event.variables.get("more_info"): battery_dict = dict(x.split(":") for x in more_info.split(",")) await self.async_update_battery_info(battery_dict) self.async_write_entity_states() @@ -514,12 +508,19 @@ class SonosSpeaker: if not self._battery_poll_timer: # Battery info received for an S1 speaker + new_battery = not self.battery_info self.battery_info.update( { "Level": int(battery_dict["BattPct"]), "PowerSource": "EXTERNAL" if is_charging else "BATTERY", } ) + if new_battery: + _LOGGER.warning( + "S1 firmware detected on %s, battery info may update infrequently", + self.zone_name, + ) + async_dispatcher_send(self.hass, SONOS_CREATE_BATTERY, self) return if is_charging == self.charging: diff --git a/tests/components/sonos/test_sensor.py b/tests/components/sonos/test_sensor.py index c8910b481f3..12c12821a0d 100644 --- a/tests/components/sonos/test_sensor.py +++ b/tests/components/sonos/test_sensor.py @@ -3,7 +3,7 @@ from pysonos.exceptions import NotSupportedException from homeassistant.components.sonos import DOMAIN from homeassistant.components.sonos.binary_sensor import ATTR_BATTERY_POWER_SOURCE -from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE +from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.setup import async_setup_component @@ -68,21 +68,18 @@ async def test_battery_on_S1(hass, config_entry, config, soco, battery_event): entity_registry = await hass.helpers.entity_registry.async_get_registry() - battery = entity_registry.entities["sensor.zone_a_battery"] - battery_state = hass.states.get(battery.entity_id) - assert battery_state.state == STATE_UNAVAILABLE - - power = entity_registry.entities["binary_sensor.zone_a_power"] - power_state = hass.states.get(power.entity_id) - assert power_state.state == STATE_UNAVAILABLE + assert "sensor.zone_a_battery" not in entity_registry.entities + assert "binary_sensor.zone_a_power" not in entity_registry.entities # Update the speaker with a callback event sub_callback(battery_event) await hass.async_block_till_done() + battery = entity_registry.entities["sensor.zone_a_battery"] battery_state = hass.states.get(battery.entity_id) assert battery_state.state == "100" + power = entity_registry.entities["binary_sensor.zone_a_power"] power_state = hass.states.get(power.entity_id) assert power_state.state == STATE_OFF assert power_state.attributes.get(ATTR_BATTERY_POWER_SOURCE) == "BATTERY" From 3a5f51ed7d6eb798c6eb0aa97e221c6aad3ef71a Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 7 Jun 2021 20:17:14 -0500 Subject: [PATCH 104/123] Handle missing section ID for Plex clips (#51598) --- homeassistant/components/plex/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/plex/models.py b/homeassistant/components/plex/models.py index 2dc7b83b439..406c263dbff 100644 --- a/homeassistant/components/plex/models.py +++ b/homeassistant/components/plex/models.py @@ -78,7 +78,7 @@ class PlexSession: if media.librarySectionID in SPECIAL_SECTIONS: self.media_library_title = SPECIAL_SECTIONS[media.librarySectionID] - elif media.librarySectionID < 1: + elif media.librarySectionID and media.librarySectionID < 1: self.media_library_title = UNKNOWN_SECTION _LOGGER.warning( "Unknown library section ID (%s) for title '%s', please create an issue", From 880fe82337e8599011b17dcfd646b62ebcd61ca7 Mon Sep 17 00:00:00 2001 From: blastoise186 <40033667+blastoise186@users.noreply.github.com> Date: Tue, 8 Jun 2021 13:20:15 +0100 Subject: [PATCH 105/123] Reduce ovo_energy polling rate to be less aggressive (#51613) * Reduce polling rate to be less aggressive The current polling rate is too aggressive for the purpose, this commit reduces it to 12 hours to play nice with OVO. * tweak polling to hourly --- homeassistant/components/ovo_energy/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ovo_energy/__init__.py b/homeassistant/components/ovo_energy/__init__.py index 18414db7292..79a8e6138eb 100644 --- a/homeassistant/components/ovo_energy/__init__.py +++ b/homeassistant/components/ovo_energy/__init__.py @@ -64,7 +64,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: name="sensor", update_method=async_update_data, # Polling interval. Will only be polled if there are subscribers. - update_interval=timedelta(seconds=300), + update_interval=timedelta(seconds=3600), ) hass.data.setdefault(DOMAIN, {}) From cfea8a9ad1c10f6bc870a078da0852cb0ec2a7d9 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 8 Jun 2021 13:23:25 +0200 Subject: [PATCH 106/123] Do not configure Shelly config entry created by custom component (#51616) --- homeassistant/components/shelly/__init__.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 8fc7cf6be23..3f75e290362 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -68,6 +68,18 @@ async def async_setup(hass: HomeAssistant, config: dict): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Shelly from a config entry.""" + # The custom component for Shelly devices uses shelly domain as well as core + # integration. If the user removes the custom component but doesn't remove the + # config entry, core integration will try to configure that config entry with an + # error. The config entry data for this custom component doesn't contain host + # value, so if host isn't present, config entry will not be configured. + if not entry.data.get(CONF_HOST): + _LOGGER.warning( + "The config entry %s probably comes from a custom integration, please remove it if you want to use core Shelly integration", + entry.title, + ) + return False + hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id] = {} hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][DEVICE] = None From 90d28e911c95558d11fd93c3c7f3034bfa7f1cba Mon Sep 17 00:00:00 2001 From: Pawel Date: Tue, 8 Jun 2021 20:01:36 +0200 Subject: [PATCH 107/123] Fix Onvif get_time_zone from device (#51620) Co-authored-by: Martin Hjelmare --- homeassistant/components/onvif/device.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/onvif/device.py b/homeassistant/components/onvif/device.py index 35dc436d201..87b68508fa1 100644 --- a/homeassistant/components/onvif/device.py +++ b/homeassistant/components/onvif/device.py @@ -169,7 +169,9 @@ class ONVIFDevice: cdate = device_time.UTCDateTime else: tzone = ( - dt_util.get_time_zone(device_time.TimeZone) + dt_util.get_time_zone( + device_time.TimeZone or str(dt_util.DEFAULT_TIME_ZONE) + ) or dt_util.DEFAULT_TIME_ZONE ) cdate = device_time.LocalDateTime From 60b89101e5f359f6ee8c7fe42c68f664c47f2035 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 10 Jun 2021 07:23:00 -1000 Subject: [PATCH 108/123] Ensure samsungtv reloads after reauth (#51714) * Ensure samsungtv reloads after reauth - Fixes a case of I/O in the event loop * Ensure config entry is reloaded --- homeassistant/components/samsungtv/config_flow.py | 3 ++- tests/components/samsungtv/test_config_flow.py | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index a69a456df40..e29298da2eb 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -291,13 +291,14 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): bridge = SamsungTVBridge.get_bridge( self._reauth_entry.data[CONF_METHOD], self._reauth_entry.data[CONF_HOST] ) - result = bridge.try_connect() + result = await self.hass.async_add_executor_job(bridge.try_connect) if result == RESULT_SUCCESS: new_data = dict(self._reauth_entry.data) new_data[CONF_TOKEN] = bridge.token self.hass.config_entries.async_update_entry( self._reauth_entry, data=new_data ) + await self.hass.config_entries.async_reload(self._reauth_entry.entry_id) return self.async_abort(reason="reauth_successful") if result not in (RESULT_AUTH_MISSING, RESULT_CANNOT_CONNECT): return self.async_abort(reason=result) diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index 5b85ecf7048..1dd11fa5ad9 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -905,6 +905,8 @@ async def test_form_reauth_websocket(hass, remotews: Mock): """Test reauthenticate websocket.""" entry = MockConfigEntry(domain=DOMAIN, data=MOCK_WS_ENTRY) entry.add_to_hass(hass) + assert entry.state == config_entries.ConfigEntryState.NOT_LOADED + result = await hass.config_entries.flow.async_init( DOMAIN, context={"entry_id": entry.entry_id, "source": config_entries.SOURCE_REAUTH}, @@ -920,6 +922,7 @@ async def test_form_reauth_websocket(hass, remotews: Mock): await hass.async_block_till_done() assert result2["type"] == "abort" assert result2["reason"] == "reauth_successful" + assert entry.state == config_entries.ConfigEntryState.LOADED async def test_form_reauth_websocket_cannot_connect(hass, remotews: Mock): From 548e847453edfbbf66659177ab1573b6670800a0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 10 Jun 2021 07:24:30 -1000 Subject: [PATCH 109/123] Fix race condition in samsungtv turn off (#51716) - The state would flip flop if the update happened before the TV had fully shutdown --- .../components/samsungtv/media_player.py | 17 +++++++++++++++-- tests/components/samsungtv/test_media_player.py | 12 ++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index 5822bafcc55..7efdcdcd439 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -21,6 +21,7 @@ from homeassistant.components.media_player.const import ( ) from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, STATE_OFF, STATE_ON +from homeassistant.helpers import entity_component import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.script import Script @@ -50,6 +51,13 @@ SUPPORT_SAMSUNGTV = ( | SUPPORT_PLAY_MEDIA ) +# Since the TV will take a few seconds to go to sleep +# and actually be seen as off, we need to wait just a bit +# more than the next scan interval +SCAN_INTERVAL_PLUS_OFF_TIME = entity_component.DEFAULT_SCAN_INTERVAL + timedelta( + seconds=5 +) + async def async_setup_entry(hass, entry, async_add_entities): """Set up the Samsung TV from a config entry.""" @@ -148,7 +156,12 @@ class SamsungTVDevice(MediaPlayerEntity): """Return the availability of the device.""" if self._auth_failed: return False - return self._state == STATE_ON or self._on_script or self._mac + return ( + self._state == STATE_ON + or self._on_script + or self._mac + or self._power_off_in_progress() + ) @property def device_info(self): @@ -187,7 +200,7 @@ class SamsungTVDevice(MediaPlayerEntity): def turn_off(self): """Turn off media player.""" - self._end_of_power_off = dt_util.utcnow() + timedelta(seconds=15) + self._end_of_power_off = dt_util.utcnow() + SCAN_INTERVAL_PLUS_OFF_TIME self.send_key("KEY_POWEROFF") # Force closing of remote session to provide instant UI feedback diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index 02eceeaacb7..2cdd5cf56df 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -419,6 +419,18 @@ async def test_state_without_turnon(hass, remote): assert await hass.services.async_call( DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID_NOTURNON}, True ) + state = hass.states.get(ENTITY_ID_NOTURNON) + # Should be STATE_UNAVAILABLE after the timer expires + assert state.state == STATE_OFF + + next_update = dt_util.utcnow() + timedelta(seconds=20) + with patch( + "homeassistant.components.samsungtv.bridge.Remote", + side_effect=OSError, + ), patch("homeassistant.util.dt.utcnow", return_value=next_update): + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + state = hass.states.get(ENTITY_ID_NOTURNON) # Should be STATE_UNAVAILABLE since there is no way to turn it back on assert state.state == STATE_UNAVAILABLE From 97e36cd3c4c95e872cfc68988e56291ae5bc195f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 11 Jun 2021 21:42:27 -0700 Subject: [PATCH 110/123] Bumped version to 2021.6.4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 3b1fc4705a8..8eed701d74d 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 6 -PATCH_VERSION: Final = "3" +PATCH_VERSION: Final = "4" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From fcc66139f843a65ebf2f1b593ae9381bdefcd99f Mon Sep 17 00:00:00 2001 From: Ron Klinkien Date: Sat, 12 Jun 2021 10:05:27 +0200 Subject: [PATCH 111/123] Replace garminconnect_aio with garminconnect_ha (#51730) * Fixed config_flow for multiple account creation * Replaced python package to fix multiple accounts * Replaced python package to fix multiple accounts * Implemented config entries user * Config entries user * Fixed test code config flow * Fixed patch --- .../components/garmin_connect/__init__.py | 22 +++-- .../components/garmin_connect/config_flow.py | 8 +- .../components/garmin_connect/manifest.json | 4 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../garmin_connect/test_config_flow.py | 96 +++++++++---------- 6 files changed, 65 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/garmin_connect/__init__.py b/homeassistant/components/garmin_connect/__init__.py index 45c71bf1f07..bd8920e43a8 100644 --- a/homeassistant/components/garmin_connect/__init__.py +++ b/homeassistant/components/garmin_connect/__init__.py @@ -2,7 +2,7 @@ from datetime import date import logging -from garminconnect_aio import ( +from garminconnect_ha import ( Garmin, GarminConnectAuthenticationError, GarminConnectConnectionError, @@ -13,7 +13,6 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import Throttle from .const import DEFAULT_UPDATE_INTERVAL, DOMAIN @@ -26,14 +25,13 @@ PLATFORMS = ["sensor"] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Garmin Connect from a config entry.""" - websession = async_get_clientsession(hass) username: str = entry.data[CONF_USERNAME] password: str = entry.data[CONF_PASSWORD] - garmin_client = Garmin(websession, username, password) + api = Garmin(username, password) try: - await garmin_client.login() + await hass.async_add_executor_job(api.login) except ( GarminConnectAuthenticationError, GarminConnectTooManyRequestsError, @@ -49,7 +47,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): _LOGGER.exception("Unknown error occurred during Garmin Connect login request") return False - garmin_data = GarminConnectData(hass, garmin_client) + garmin_data = GarminConnectData(hass, api) hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = garmin_data @@ -81,14 +79,20 @@ class GarminConnectData: today = date.today() try: - summary = await self.client.get_user_summary(today.isoformat()) - body = await self.client.get_body_composition(today.isoformat()) + summary = await self.hass.async_add_executor_job( + self.client.get_user_summary, today.isoformat() + ) + body = await self.hass.async_add_executor_job( + self.client.get_body_composition, today.isoformat() + ) self.data = { **summary, **body["totalAverage"], } - self.data["nextAlarm"] = await self.client.get_device_alarms() + self.data["nextAlarm"] = await self.hass.async_add_executor_job( + self.client.get_device_alarms + ) except ( GarminConnectAuthenticationError, GarminConnectTooManyRequestsError, diff --git a/homeassistant/components/garmin_connect/config_flow.py b/homeassistant/components/garmin_connect/config_flow.py index 8f83a9e1071..e9966859f99 100644 --- a/homeassistant/components/garmin_connect/config_flow.py +++ b/homeassistant/components/garmin_connect/config_flow.py @@ -1,7 +1,7 @@ """Config flow for Garmin Connect integration.""" import logging -from garminconnect_aio import ( +from garminconnect_ha import ( Garmin, GarminConnectAuthenticationError, GarminConnectConnectionError, @@ -11,7 +11,6 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME -from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN @@ -38,15 +37,14 @@ class GarminConnectConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if user_input is None: return await self._show_setup_form() - websession = async_get_clientsession(self.hass) username = user_input[CONF_USERNAME] password = user_input[CONF_PASSWORD] - garmin_client = Garmin(websession, username, password) + api = Garmin(username, password) errors = {} try: - await garmin_client.login() + await self.hass.async_add_executor_job(api.login) except GarminConnectConnectionError: errors["base"] = "cannot_connect" return await self._show_setup_form(errors) diff --git a/homeassistant/components/garmin_connect/manifest.json b/homeassistant/components/garmin_connect/manifest.json index 22e115d0e06..43b4a028290 100644 --- a/homeassistant/components/garmin_connect/manifest.json +++ b/homeassistant/components/garmin_connect/manifest.json @@ -2,8 +2,8 @@ "domain": "garmin_connect", "name": "Garmin Connect", "documentation": "https://www.home-assistant.io/integrations/garmin_connect", - "requirements": ["garminconnect_aio==0.1.4"], + "requirements": ["garminconnect_ha==0.1.6"], "codeowners": ["@cyberjunky"], "config_flow": true, "iot_class": "cloud_polling" -} +} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index 48e5632b730..6804280928e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -635,7 +635,7 @@ gTTS==2.2.2 garages-amsterdam==2.1.1 # homeassistant.components.garmin_connect -garminconnect_aio==0.1.4 +garminconnect_ha==0.1.6 # homeassistant.components.geniushub geniushub-client==0.6.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b2d64664395..f0e5b5f0abf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -341,7 +341,7 @@ gTTS==2.2.2 garages-amsterdam==2.1.1 # homeassistant.components.garmin_connect -garminconnect_aio==0.1.4 +garminconnect_ha==0.1.6 # homeassistant.components.geo_json_events # homeassistant.components.usgs_earthquakes_feed diff --git a/tests/components/garmin_connect/test_config_flow.py b/tests/components/garmin_connect/test_config_flow.py index 2ad36ffa29c..dd56fba9c1c 100644 --- a/tests/components/garmin_connect/test_config_flow.py +++ b/tests/components/garmin_connect/test_config_flow.py @@ -1,12 +1,11 @@ """Test the Garmin Connect config flow.""" from unittest.mock import patch -from garminconnect_aio import ( +from garminconnect_ha import ( GarminConnectAuthenticationError, GarminConnectConnectionError, GarminConnectTooManyRequestsError, ) -import pytest from homeassistant import config_entries, data_entry_flow from homeassistant.components.garmin_connect.const import DOMAIN @@ -21,37 +20,23 @@ MOCK_CONF = { } -@pytest.fixture(name="mock_garmin_connect") -def mock_garmin(): - """Mock Garmin Connect.""" - with patch( - "homeassistant.components.garmin_connect.config_flow.Garmin", - ) as garmin: - garmin.return_value.login.return_value = MOCK_CONF[CONF_ID] - yield garmin.return_value - - async def test_show_form(hass): """Test that the form is served with no input.""" - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {} assert result["step_id"] == config_entries.SOURCE_USER async def test_step_user(hass): """Test registering an integration and finishing flow works.""" - with patch( - "homeassistant.components.garmin_connect.Garmin.login", - return_value="my@email.address", - ), patch( "homeassistant.components.garmin_connect.async_setup_entry", return_value=True - ): + ), patch( + "homeassistant.components.garmin_connect.config_flow.Garmin", + ) as garmin: + garmin.return_value.login.return_value = MOCK_CONF[CONF_ID] result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_CONF ) @@ -59,60 +44,69 @@ async def test_step_user(hass): assert result["data"] == MOCK_CONF -async def test_connection_error(hass, mock_garmin_connect): +async def test_connection_error(hass): """Test for connection error.""" - mock_garmin_connect.login.side_effect = GarminConnectConnectionError("errormsg") - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_CONF - ) + with patch( + "homeassistant.components.garmin_connect.Garmin.login", + side_effect=GarminConnectConnectionError("errormsg"), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=MOCK_CONF + ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {"base": "cannot_connect"} -async def test_authentication_error(hass, mock_garmin_connect): +async def test_authentication_error(hass): """Test for authentication error.""" - mock_garmin_connect.login.side_effect = GarminConnectAuthenticationError("errormsg") - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_CONF - ) + with patch( + "homeassistant.components.garmin_connect.Garmin.login", + side_effect=GarminConnectAuthenticationError("errormsg"), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=MOCK_CONF + ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {"base": "invalid_auth"} -async def test_toomanyrequest_error(hass, mock_garmin_connect): +async def test_toomanyrequest_error(hass): """Test for toomanyrequests error.""" - mock_garmin_connect.login.side_effect = GarminConnectTooManyRequestsError( - "errormsg" - ) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_CONF - ) + with patch( + "homeassistant.components.garmin_connect.Garmin.login", + side_effect=GarminConnectTooManyRequestsError("errormsg"), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=MOCK_CONF + ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {"base": "too_many_requests"} -async def test_unknown_error(hass, mock_garmin_connect): +async def test_unknown_error(hass): """Test for unknown error.""" - mock_garmin_connect.login.side_effect = Exception - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_CONF - ) + with patch( + "homeassistant.components.garmin_connect.Garmin.login", + side_effect=Exception, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=MOCK_CONF + ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {"base": "unknown"} async def test_abort_if_already_setup(hass): """Test abort if already setup.""" - MockConfigEntry( - domain=DOMAIN, data=MOCK_CONF, unique_id=MOCK_CONF[CONF_ID] - ).add_to_hass(hass) with patch( - "homeassistant.components.garmin_connect.config_flow.Garmin", autospec=True - ) as garmin: - garmin.return_value.login.return_value = MOCK_CONF[CONF_ID] - + "homeassistant.components.garmin_connect.config_flow.Garmin", + ): + entry = MockConfigEntry( + domain=DOMAIN, data=MOCK_CONF, unique_id=MOCK_CONF[CONF_ID] + ) + entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "user"}, data=MOCK_CONF ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" From 8f34d1cf3243fd30fac3c7de1c6420d2dc561116 Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Sun, 13 Jun 2021 11:38:55 +0200 Subject: [PATCH 112/123] Bump pydaikin, fix airbase issues (#51797) --- 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 2db81e8f167..ec0b2716053 100644 --- a/homeassistant/components/daikin/manifest.json +++ b/homeassistant/components/daikin/manifest.json @@ -3,7 +3,7 @@ "name": "Daikin AC", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/daikin", - "requirements": ["pydaikin==2.4.1"], + "requirements": ["pydaikin==2.4.2"], "codeowners": ["@fredrike"], "zeroconf": ["_dkapi._tcp.local."], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 6804280928e..418a0561148 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1352,7 +1352,7 @@ pycsspeechtts==1.0.4 # pycups==1.9.73 # homeassistant.components.daikin -pydaikin==2.4.1 +pydaikin==2.4.2 # homeassistant.components.danfoss_air pydanfossair==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f0e5b5f0abf..10ff054154e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -747,7 +747,7 @@ pycomfoconnect==0.4 pycoolmasternet-async==0.1.2 # homeassistant.components.daikin -pydaikin==2.4.1 +pydaikin==2.4.2 # homeassistant.components.deconz pydeconz==79 From dcc7bc10e8cd42f8c958e41ca42807c892d5479c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sun, 13 Jun 2021 10:21:26 +0200 Subject: [PATCH 113/123] Add httpcore with version 0.13.3 (#51799) --- homeassistant/package_constraints.txt | 4 ++++ script/gen_requirements_all.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b4d8e99e54a..29af3f52a07 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -63,3 +63,7 @@ enum34==1000000000.0.0 typing==1000000000.0.0 uuid==1000000000.0.0 +# httpcore 0.13.4 breaks several integrations +# https://github.com/home-assistant/core/issues/51778 +httpcore==0.13.3 + diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 4fd96cb1b04..dc0c5fa2c10 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -83,6 +83,10 @@ enum34==1000000000.0.0 typing==1000000000.0.0 uuid==1000000000.0.0 +# httpcore 0.13.4 breaks several integrations +# https://github.com/home-assistant/core/issues/51778 +httpcore==0.13.3 + """ IGNORE_PRE_COMMIT_HOOK_ID = ( From ee7a2b29ad90ce63a7d77e7fb004b61cffd5276c Mon Sep 17 00:00:00 2001 From: Ludovico de Nittis Date: Mon, 14 Jun 2021 03:47:56 +0200 Subject: [PATCH 114/123] Bump pyialarm to 1.9.0 (#51804) --- homeassistant/components/ialarm/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ialarm/manifest.json b/homeassistant/components/ialarm/manifest.json index 08666129fd9..751faec56c7 100644 --- a/homeassistant/components/ialarm/manifest.json +++ b/homeassistant/components/ialarm/manifest.json @@ -2,7 +2,7 @@ "domain": "ialarm", "name": "Antifurto365 iAlarm", "documentation": "https://www.home-assistant.io/integrations/ialarm", - "requirements": ["pyialarm==1.8.1"], + "requirements": ["pyialarm==1.9.0"], "codeowners": ["@RyuzakiKK"], "config_flow": true, "iot_class": "local_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 418a0561148..9b1cb90850d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1464,7 +1464,7 @@ pyhomematic==0.1.72 pyhomeworks==0.0.6 # homeassistant.components.ialarm -pyialarm==1.8.1 +pyialarm==1.9.0 # homeassistant.components.icloud pyicloud==0.10.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 10ff054154e..6ba61f4642e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -808,7 +808,7 @@ pyhiveapi==0.4.2 pyhomematic==0.1.72 # homeassistant.components.ialarm -pyialarm==1.8.1 +pyialarm==1.9.0 # homeassistant.components.icloud pyicloud==0.10.2 From fcb2c26a24d2eee9a0b8a276b44b3560b9ed23a6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 15 Jun 2021 19:45:33 +0200 Subject: [PATCH 115/123] Bumped version to 2021.6.5 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 8eed701d74d..9c2c34c52f4 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 6 -PATCH_VERSION: Final = "4" +PATCH_VERSION: Final = "5" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From 398fca3b9d13af42f3dfe2294b2a9c429923d91a Mon Sep 17 00:00:00 2001 From: Konstantin Antselovich Date: Wed, 16 Jun 2021 20:57:46 -0700 Subject: [PATCH 116/123] Fix whois expiration date (#51868) --- homeassistant/components/whois/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/whois/sensor.py b/homeassistant/components/whois/sensor.py index 6d97037a4ee..4219c80193d 100644 --- a/homeassistant/components/whois/sensor.py +++ b/homeassistant/components/whois/sensor.py @@ -118,6 +118,7 @@ class WhoisSensor(SensorEntity): expiration_date = response["expiration_date"] if isinstance(expiration_date, list): attrs[ATTR_EXPIRES] = expiration_date[0].isoformat() + expiration_date = expiration_date[0] else: attrs[ATTR_EXPIRES] = expiration_date.isoformat() From c8e678a2c62b0a21d2effc252c2537ea2080e233 Mon Sep 17 00:00:00 2001 From: djtimca <60706061+djtimca@users.noreply.github.com> Date: Wed, 16 Jun 2021 08:39:09 -0400 Subject: [PATCH 117/123] Add Omnilogic switch defaults for max_speed and min_speed (#51889) --- homeassistant/components/omnilogic/switch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/omnilogic/switch.py b/homeassistant/components/omnilogic/switch.py index 771b02a24c1..8fffa384916 100644 --- a/homeassistant/components/omnilogic/switch.py +++ b/homeassistant/components/omnilogic/switch.py @@ -153,8 +153,8 @@ class OmniLogicPumpControl(OmniLogicSwitch): state_key=state_key, ) - self._max_speed = int(coordinator.data[item_id]["Max-Pump-Speed"]) - self._min_speed = int(coordinator.data[item_id]["Min-Pump-Speed"]) + self._max_speed = int(coordinator.data[item_id].get("Max-Pump-Speed", 100)) + self._min_speed = int(coordinator.data[item_id].get("Min-Pump-Speed", 0)) if "Filter-Type" in coordinator.data[item_id]: self._pump_type = PUMP_TYPES[coordinator.data[item_id]["Filter-Type"]] From d6fd41bd035a7043c3923d3575ef7603e07200b1 Mon Sep 17 00:00:00 2001 From: Rob Bierbooms Date: Wed, 16 Jun 2021 03:34:05 +0200 Subject: [PATCH 118/123] Bump pyRFXtrx to 0.27.0 (#51911) * Bump version * Fix test --- homeassistant/components/rfxtrx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/rfxtrx/test_init.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/rfxtrx/manifest.json b/homeassistant/components/rfxtrx/manifest.json index 34c31c72a0d..0fc12e79d49 100644 --- a/homeassistant/components/rfxtrx/manifest.json +++ b/homeassistant/components/rfxtrx/manifest.json @@ -2,7 +2,7 @@ "domain": "rfxtrx", "name": "RFXCOM RFXtrx", "documentation": "https://www.home-assistant.io/integrations/rfxtrx", - "requirements": ["pyRFXtrx==0.26.1"], + "requirements": ["pyRFXtrx==0.27.0"], "codeowners": ["@danielhiversen", "@elupus", "@RobBie1221"], "config_flow": true, "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 9b1cb90850d..950b8b90da8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1253,7 +1253,7 @@ pyMetEireann==0.2 pyMetno==0.8.3 # homeassistant.components.rfxtrx -pyRFXtrx==0.26.1 +pyRFXtrx==0.27.0 # homeassistant.components.switchmate # pySwitchmate==0.4.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6ba61f4642e..041bccbb28d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -693,7 +693,7 @@ pyMetEireann==0.2 pyMetno==0.8.3 # homeassistant.components.rfxtrx -pyRFXtrx==0.26.1 +pyRFXtrx==0.27.0 # homeassistant.components.tibber pyTibber==0.17.0 diff --git a/tests/components/rfxtrx/test_init.py b/tests/components/rfxtrx/test_init.py index b3829e2b5cc..3625c23ebb8 100644 --- a/tests/components/rfxtrx/test_init.py +++ b/tests/components/rfxtrx/test_init.py @@ -117,7 +117,7 @@ async def test_fire_event(hass, rfxtrx): "type_string": "Byron SX", "id_string": "00:90", "data": "0716000100900970", - "values": {"Command": "Chime", "Rssi numeric": 7, "Sound": 9}, + "values": {"Command": "Sound 9", "Rssi numeric": 7, "Sound": 9}, "device_id": device_id_2.id, }, ] From 47560006b8b7041f36c773419f9146a9d0385f20 Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Thu, 17 Jun 2021 10:58:28 +0200 Subject: [PATCH 119/123] Bump pydaikin to 2.4.3 (#51926) --- 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 ec0b2716053..704cfcf739c 100644 --- a/homeassistant/components/daikin/manifest.json +++ b/homeassistant/components/daikin/manifest.json @@ -3,7 +3,7 @@ "name": "Daikin AC", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/daikin", - "requirements": ["pydaikin==2.4.2"], + "requirements": ["pydaikin==2.4.3"], "codeowners": ["@fredrike"], "zeroconf": ["_dkapi._tcp.local."], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 950b8b90da8..65a981fa039 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1352,7 +1352,7 @@ pycsspeechtts==1.0.4 # pycups==1.9.73 # homeassistant.components.daikin -pydaikin==2.4.2 +pydaikin==2.4.3 # homeassistant.components.danfoss_air pydanfossair==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 041bccbb28d..df1f7e6dcf2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -747,7 +747,7 @@ pycomfoconnect==0.4 pycoolmasternet-async==0.1.2 # homeassistant.components.daikin -pydaikin==2.4.2 +pydaikin==2.4.3 # homeassistant.components.deconz pydeconz==79 From 67699e3c1f73d2dfad042982deabd00baae46ff8 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 20 Jun 2021 23:53:08 +0200 Subject: [PATCH 120/123] Fix AccuWeather sensors updates (#52031) Co-authored-by: Paulus Schoutsen --- .../components/accuweather/sensor.py | 30 +++++++++++++---- tests/components/accuweather/test_sensor.py | 33 +++++++++++++++++++ 2 files changed, 56 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/accuweather/sensor.py b/homeassistant/components/accuweather/sensor.py index 09e9cda30ad..d6f9339409f 100644 --- a/homeassistant/components/accuweather/sensor.py +++ b/homeassistant/components/accuweather/sensor.py @@ -12,7 +12,7 @@ from homeassistant.const import ( CONF_NAME, DEVICE_CLASS_TEMPERATURE, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType @@ -81,16 +81,11 @@ class AccuWeatherSensor(CoordinatorEntity, SensorEntity): ) -> None: """Initialize.""" super().__init__(coordinator) + self._sensor_data = _get_sensor_data(coordinator.data, forecast_day, kind) if forecast_day is None: self._description = SENSOR_TYPES[kind] - self._sensor_data: dict[str, Any] - if kind == "Precipitation": - self._sensor_data = coordinator.data["PrecipitationSummary"][kind] - else: - self._sensor_data = coordinator.data[kind] else: self._description = FORECAST_SENSOR_TYPES[kind] - self._sensor_data = coordinator.data[ATTR_FORECAST][forecast_day][kind] self._unit_system = API_METRIC if coordinator.is_metric else API_IMPERIAL self._name = name self.kind = kind @@ -182,3 +177,24 @@ class AccuWeatherSensor(CoordinatorEntity, SensorEntity): def entity_registry_enabled_default(self) -> bool: """Return if the entity should be enabled when first added to the entity registry.""" return self._description[ATTR_ENABLED] + + @callback + def _handle_coordinator_update(self) -> None: + """Handle data update.""" + self._sensor_data = _get_sensor_data( + self.coordinator.data, self.forecast_day, self.kind + ) + self.async_write_ha_state() + + +def _get_sensor_data( + sensors: dict[str, Any], forecast_day: int | None, kind: str +) -> Any: + """Get sensor data.""" + if forecast_day is not None: + return sensors[ATTR_FORECAST][forecast_day][kind] + + if kind == "Precipitation": + return sensors["PrecipitationSummary"][kind] + + return sensors[kind] diff --git a/tests/components/accuweather/test_sensor.py b/tests/components/accuweather/test_sensor.py index 482fae696c0..64c49c61fe7 100644 --- a/tests/components/accuweather/test_sensor.py +++ b/tests/components/accuweather/test_sensor.py @@ -673,3 +673,36 @@ async def test_sensor_imperial_units(hass): assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_ICON) == "mdi:weather-fog" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_FEET + + +async def test_state_update(hass): + """Ensure the sensor state changes after updating the data.""" + await init_integration(hass) + + state = hass.states.get("sensor.home_cloud_ceiling") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "3200" + + future = utcnow() + timedelta(minutes=60) + + current_condition = json.loads( + load_fixture("accuweather/current_conditions_data.json") + ) + current_condition["Ceiling"]["Metric"]["Value"] = 3300 + + with patch( + "homeassistant.components.accuweather.AccuWeather.async_get_current_conditions", + return_value=current_condition, + ), patch( + "homeassistant.components.accuweather.AccuWeather.requests_remaining", + new_callable=PropertyMock, + return_value=10, + ): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("sensor.home_cloud_ceiling") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "3300" From 79cfd444d9a85d776ece209d74e02bcb83335aff Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 20 Jun 2021 14:53:21 -0700 Subject: [PATCH 121/123] Fix double subscriptions for local push notifications (#52039) --- .../components/mobile_app/__init__.py | 2 +- tests/components/mobile_app/test_notify.py | 21 +++++++++++++++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py index 951c6f3beaf..9633ec6556d 100644 --- a/homeassistant/components/mobile_app/__init__.py +++ b/homeassistant/components/mobile_app/__init__.py @@ -160,7 +160,7 @@ def handle_push_notification_channel(hass, connection, msg): registered_channels = hass.data[DOMAIN][DATA_PUSH_CHANNEL] if webhook_id in registered_channels: - registered_channels.pop(webhook_id)() + registered_channels.pop(webhook_id) @callback def forward_push_notification(data): diff --git a/tests/components/mobile_app/test_notify.py b/tests/components/mobile_app/test_notify.py index 9c4ca146898..1e3b999d5f5 100644 --- a/tests/components/mobile_app/test_notify.py +++ b/tests/components/mobile_app/test_notify.py @@ -136,6 +136,18 @@ async def test_notify_ws_works( sub_result = await client.receive_json() assert sub_result["success"] + # Subscribe twice, it should forward all messages to 2nd subscription + await client.send_json( + { + "id": 6, + "type": "mobile_app/push_notification_channel", + "webhook_id": "mock-webhook_id", + } + ) + + sub_result = await client.receive_json() + assert sub_result["success"] + assert await hass.services.async_call( "notify", "mobile_app_test", {"message": "Hello world"}, blocking=True ) @@ -144,13 +156,14 @@ async def test_notify_ws_works( msg_result = await client.receive_json() assert msg_result["event"] == {"message": "Hello world"} + assert msg_result["id"] == 6 # This is the new subscription # Unsubscribe, now it should go over http await client.send_json( { - "id": 6, + "id": 7, "type": "unsubscribe_events", - "subscription": 5, + "subscription": 6, } ) sub_result = await client.receive_json() @@ -165,7 +178,7 @@ async def test_notify_ws_works( # Test non-existing webhook ID await client.send_json( { - "id": 7, + "id": 8, "type": "mobile_app/push_notification_channel", "webhook_id": "non-existing", } @@ -180,7 +193,7 @@ async def test_notify_ws_works( # Test webhook ID linked to other user await client.send_json( { - "id": 8, + "id": 9, "type": "mobile_app/push_notification_channel", "webhook_id": "webhook_id_2", } From 0d351e4a0ec5af53062239493cc2c56bb67abf4c Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sun, 20 Jun 2021 23:38:07 -0500 Subject: [PATCH 122/123] Catch unexpected battery update payloads on Sonos (#52040) --- homeassistant/components/sonos/speaker.py | 8 ++++++++ tests/components/sonos/test_sensor.py | 20 ++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 055b4ce6845..dc610cee38a 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -382,6 +382,14 @@ class SonosSpeaker: """Update device properties from an event.""" if more_info := event.variables.get("more_info"): battery_dict = dict(x.split(":") for x in more_info.split(",")) + if "BattChg" not in battery_dict: + _LOGGER.debug( + "Unknown device properties update for %s (%s), please report an issue: '%s'", + self.zone_name, + self.model_name, + more_info, + ) + return await self.async_update_battery_info(battery_dict) self.async_write_entity_states() diff --git a/tests/components/sonos/test_sensor.py b/tests/components/sonos/test_sensor.py index 12c12821a0d..8d402b589b0 100644 --- a/tests/components/sonos/test_sensor.py +++ b/tests/components/sonos/test_sensor.py @@ -83,3 +83,23 @@ async def test_battery_on_S1(hass, config_entry, config, soco, battery_event): power_state = hass.states.get(power.entity_id) assert power_state.state == STATE_OFF assert power_state.attributes.get(ATTR_BATTERY_POWER_SOURCE) == "BATTERY" + + +async def test_device_payload_without_battery( + hass, config_entry, config, soco, battery_event, caplog +): + """Test device properties event update without battery info.""" + soco.get_battery_info.return_value = None + + await setup_platform(hass, config_entry, config) + + subscription = soco.deviceProperties.subscribe.return_value + sub_callback = subscription.callback + + bad_payload = "BadKey:BadValue" + battery_event.variables["more_info"] = bad_payload + + sub_callback(battery_event) + await hass.async_block_till_done() + + assert bad_payload in caplog.text From 34f266bfa61345f317b1ffe3575f77d3c4c63825 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 20 Jun 2021 21:49:17 -0700 Subject: [PATCH 123/123] Bumped version to 2021.6.6 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 9c2c34c52f4..0bc06252960 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 6 -PATCH_VERSION: Final = "5" +PATCH_VERSION: Final = "6" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0)