From 78fe1fb102bc5b514406e0fc9a361ac6959e517d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 29 Jun 2022 19:29:36 +0200 Subject: [PATCH 01/71] Bumped version to 2022.7.0b0 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 698f6bee240..df160e1cc6b 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 7 -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, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 59df3967b6e..2cf2db3f240 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.7.0.dev0" +version = "2022.7.0b0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 71c2f99ee4d1562546cbf3144ec6c0af48b56a13 Mon Sep 17 00:00:00 2001 From: MasonCrawford Date: Fri, 1 Jul 2022 01:00:39 +0800 Subject: [PATCH 02/71] Add config flow to lg_soundbar (#71153) Co-authored-by: Paulus Schoutsen --- .../components/discovery/__init__.py | 2 +- .../components/lg_soundbar/__init__.py | 37 ++++++++ .../components/lg_soundbar/config_flow.py | 78 +++++++++++++++ homeassistant/components/lg_soundbar/const.py | 4 + .../components/lg_soundbar/manifest.json | 3 +- .../components/lg_soundbar/media_player.py | 51 +++++----- .../components/lg_soundbar/strings.json | 18 ++++ .../lg_soundbar/translations/en.json | 18 ++++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 3 + tests/components/lg_soundbar/__init__.py | 1 + .../lg_soundbar/test_config_flow.py | 95 +++++++++++++++++++ 13 files changed, 284 insertions(+), 29 deletions(-) create mode 100644 homeassistant/components/lg_soundbar/config_flow.py create mode 100644 homeassistant/components/lg_soundbar/const.py create mode 100644 homeassistant/components/lg_soundbar/strings.json create mode 100644 homeassistant/components/lg_soundbar/translations/en.json create mode 100644 tests/components/lg_soundbar/__init__.py create mode 100644 tests/components/lg_soundbar/test_config_flow.py diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py index a0ffbf235ab..3c3538c1ca0 100644 --- a/homeassistant/components/discovery/__init__.py +++ b/homeassistant/components/discovery/__init__.py @@ -63,7 +63,6 @@ SERVICE_HANDLERS = { "openhome": ServiceDetails("media_player", "openhome"), "bose_soundtouch": ServiceDetails("media_player", "soundtouch"), "bluesound": ServiceDetails("media_player", "bluesound"), - "lg_smart_device": ServiceDetails("media_player", "lg_soundbar"), } OPTIONAL_SERVICE_HANDLERS: dict[str, tuple[str, str | None]] = {} @@ -98,6 +97,7 @@ MIGRATED_SERVICE_HANDLERS = [ SERVICE_YEELIGHT, SERVICE_SABNZBD, "nanoleaf_aurora", + "lg_smart_device", ] DEFAULT_ENABLED = ( diff --git a/homeassistant/components/lg_soundbar/__init__.py b/homeassistant/components/lg_soundbar/__init__.py index 175153556f9..75b2109b22a 100644 --- a/homeassistant/components/lg_soundbar/__init__.py +++ b/homeassistant/components/lg_soundbar/__init__.py @@ -1 +1,38 @@ """The lg_soundbar component.""" +import logging + +from homeassistant import config_entries, core +from homeassistant.const import CONF_HOST, CONF_PORT, Platform +from homeassistant.exceptions import ConfigEntryNotReady + +from .config_flow import test_connect +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = [Platform.MEDIA_PLAYER] + + +async def async_setup_entry( + hass: core.HomeAssistant, entry: config_entries.ConfigEntry +) -> bool: + """Set up platform from a ConfigEntry.""" + hass.data.setdefault(DOMAIN, {}) + # Verify the device is reachable with the given config before setting up the platform + try: + await hass.async_add_executor_job( + test_connect, entry.data[CONF_HOST], entry.data[CONF_PORT] + ) + except ConnectionError as err: + raise ConfigEntryNotReady from err + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True + + +async def async_unload_entry( + hass: core.HomeAssistant, entry: config_entries.ConfigEntry +) -> bool: + """Unload a config entry.""" + result = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + return result diff --git a/homeassistant/components/lg_soundbar/config_flow.py b/homeassistant/components/lg_soundbar/config_flow.py new file mode 100644 index 00000000000..bd9a727d1f4 --- /dev/null +++ b/homeassistant/components/lg_soundbar/config_flow.py @@ -0,0 +1,78 @@ +"""Config flow to configure the LG Soundbar integration.""" +from queue import Queue +import socket + +import temescal +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST, CONF_PORT + +from .const import DEFAULT_PORT, DOMAIN + +DATA_SCHEMA = { + vol.Required(CONF_HOST): str, +} + + +def test_connect(host, port): + """LG Soundbar config flow test_connect.""" + uuid_q = Queue(maxsize=1) + name_q = Queue(maxsize=1) + + def msg_callback(response): + if response["msg"] == "MAC_INFO_DEV" and "s_uuid" in response["data"]: + uuid_q.put_nowait(response["data"]["s_uuid"]) + if ( + response["msg"] == "SPK_LIST_VIEW_INFO" + and "s_user_name" in response["data"] + ): + name_q.put_nowait(response["data"]["s_user_name"]) + + try: + connection = temescal.temescal(host, port=port, callback=msg_callback) + connection.get_mac_info() + connection.get_info() + details = {"name": name_q.get(timeout=10), "uuid": uuid_q.get(timeout=10)} + return details + except socket.timeout as err: + raise ConnectionError(f"Connection timeout with server: {host}:{port}") from err + except OSError as err: + raise ConnectionError(f"Cannot resolve hostname: {host}") from err + + +class LGSoundbarConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """LG Soundbar config flow.""" + + VERSION = 1 + + async def async_step_user(self, user_input=None): + """Handle a flow initiated by the user.""" + if user_input is None: + return self._show_form() + + errors = {} + try: + details = await self.hass.async_add_executor_job( + test_connect, user_input[CONF_HOST], DEFAULT_PORT + ) + except ConnectionError: + errors["base"] = "cannot_connect" + else: + await self.async_set_unique_id(details["uuid"]) + self._abort_if_unique_id_configured() + info = { + CONF_HOST: user_input[CONF_HOST], + CONF_PORT: DEFAULT_PORT, + } + return self.async_create_entry(title=details["name"], data=info) + + return self._show_form(errors) + + def _show_form(self, errors=None): + """Show the form to the user.""" + return self.async_show_form( + step_id="user", + data_schema=vol.Schema(DATA_SCHEMA), + errors=errors if errors else {}, + ) diff --git a/homeassistant/components/lg_soundbar/const.py b/homeassistant/components/lg_soundbar/const.py new file mode 100644 index 00000000000..c71e43c0d60 --- /dev/null +++ b/homeassistant/components/lg_soundbar/const.py @@ -0,0 +1,4 @@ +"""Constants for the LG Soundbar integration.""" +DOMAIN = "lg_soundbar" + +DEFAULT_PORT = 9741 diff --git a/homeassistant/components/lg_soundbar/manifest.json b/homeassistant/components/lg_soundbar/manifest.json index f40ad1d194c..c05174a8938 100644 --- a/homeassistant/components/lg_soundbar/manifest.json +++ b/homeassistant/components/lg_soundbar/manifest.json @@ -1,8 +1,9 @@ { "domain": "lg_soundbar", + "config_flow": true, "name": "LG Soundbars", "documentation": "https://www.home-assistant.io/integrations/lg_soundbar", - "requirements": ["temescal==0.3"], + "requirements": ["temescal==0.5"], "codeowners": [], "iot_class": "local_polling", "loggers": ["temescal"] diff --git a/homeassistant/components/lg_soundbar/media_player.py b/homeassistant/components/lg_soundbar/media_player.py index 569678c8c15..f8f6fcf26fd 100644 --- a/homeassistant/components/lg_soundbar/media_player.py +++ b/homeassistant/components/lg_soundbar/media_player.py @@ -7,26 +7,33 @@ from homeassistant.components.media_player import ( MediaPlayerEntity, MediaPlayerEntityFeature, ) -from homeassistant.const import STATE_ON +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PORT, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -def setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: - """Set up the LG platform.""" - if discovery_info is not None: - add_entities([LGDevice(discovery_info)]) + """Set up media_player from a config entry created in the integrations UI.""" + async_add_entities( + [ + LGDevice( + config_entry.data[CONF_HOST], + config_entry.data[CONF_PORT], + config_entry.unique_id, + ) + ] + ) class LGDevice(MediaPlayerEntity): """Representation of an LG soundbar device.""" + _attr_should_poll = False _attr_supported_features = ( MediaPlayerEntityFeature.VOLUME_SET | MediaPlayerEntityFeature.VOLUME_MUTE @@ -34,13 +41,13 @@ class LGDevice(MediaPlayerEntity): | MediaPlayerEntityFeature.SELECT_SOUND_MODE ) - def __init__(self, discovery_info): + def __init__(self, host, port, unique_id): """Initialize the LG speakers.""" - self._host = discovery_info["host"] - self._port = discovery_info["port"] - self._hostname = discovery_info["hostname"] + self._host = host + self._port = port + self._attr_unique_id = unique_id - self._name = self._hostname.split(".")[0] + self._name = None self._volume = 0 self._volume_min = 0 self._volume_max = 0 @@ -68,6 +75,8 @@ class LGDevice(MediaPlayerEntity): self._device = temescal.temescal( self._host, port=self._port, callback=self.handle_event ) + self._device.get_product_info() + self._device.get_mac_info() self.update() def handle_event(self, response): @@ -116,7 +125,8 @@ class LGDevice(MediaPlayerEntity): if "i_curr_eq" in data: self._equaliser = data["i_curr_eq"] if "s_user_name" in data: - self._name = data["s_user_name"] + self._attr_name = data["s_user_name"] + self.schedule_update_ha_state() def update(self): @@ -125,17 +135,6 @@ class LGDevice(MediaPlayerEntity): self._device.get_info() self._device.get_func() self._device.get_settings() - self._device.get_product_info() - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def name(self): - """Return the name of the device.""" - return self._name @property def volume_level(self): diff --git a/homeassistant/components/lg_soundbar/strings.json b/homeassistant/components/lg_soundbar/strings.json new file mode 100644 index 00000000000..ef7bf32a051 --- /dev/null +++ b/homeassistant/components/lg_soundbar/strings.json @@ -0,0 +1,18 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "abort": { + "existing_instance_updated": "Updated existing configuration.", + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + } + } +} diff --git a/homeassistant/components/lg_soundbar/translations/en.json b/homeassistant/components/lg_soundbar/translations/en.json new file mode 100644 index 00000000000..a646279203f --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/en.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Service is already configured", + "existing_instance_updated": "Updated existing configuration." + }, + "error": { + "cannot_connect": "Failed to connect" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 3c6ad94a21f..af4b8481873 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -189,6 +189,7 @@ FLOWS = { "kulersky", "launch_library", "laundrify", + "lg_soundbar", "life360", "lifx", "litejet", diff --git a/requirements_all.txt b/requirements_all.txt index 98a6c6f5a95..6f677d1d092 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2289,7 +2289,7 @@ tellcore-py==1.1.2 tellduslive==0.10.11 # homeassistant.components.lg_soundbar -temescal==0.3 +temescal==0.5 # homeassistant.components.temper temperusb==1.5.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 64ad3f769b9..d0c9bc54ddd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1521,6 +1521,9 @@ tailscale==0.2.0 # homeassistant.components.tellduslive tellduslive==0.10.11 +# homeassistant.components.lg_soundbar +temescal==0.5 + # homeassistant.components.powerwall tesla-powerwall==0.3.18 diff --git a/tests/components/lg_soundbar/__init__.py b/tests/components/lg_soundbar/__init__.py new file mode 100644 index 00000000000..8756d343130 --- /dev/null +++ b/tests/components/lg_soundbar/__init__.py @@ -0,0 +1 @@ +"""Tests for the lg_soundbar component.""" diff --git a/tests/components/lg_soundbar/test_config_flow.py b/tests/components/lg_soundbar/test_config_flow.py new file mode 100644 index 00000000000..3fafc2c7628 --- /dev/null +++ b/tests/components/lg_soundbar/test_config_flow.py @@ -0,0 +1,95 @@ +"""Test the lg_soundbar config flow.""" +from unittest.mock import MagicMock, patch + +from homeassistant import config_entries +from homeassistant.components.lg_soundbar.const import DEFAULT_PORT, DOMAIN +from homeassistant.const import CONF_HOST, CONF_PORT + +from tests.common import MockConfigEntry + + +async def test_form(hass): + """Test we get the form.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.lg_soundbar.config_flow.temescal", + return_value=MagicMock(), + ), patch( + "homeassistant.components.lg_soundbar.config_flow.test_connect", + return_value={"uuid": "uuid", "name": "name"}, + ), patch( + "homeassistant.components.lg_soundbar.async_setup_entry", return_value=True + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "name" + assert result2["data"] == { + CONF_HOST: "1.1.1.1", + CONF_PORT: DEFAULT_PORT, + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.lg_soundbar.config_flow.test_connect", + side_effect=ConnectionError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_already_configured(hass): + """Test we handle already configured error.""" + mock_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "1.1.1.1", + CONF_PORT: 0000, + }, + unique_id="uuid", + ) + mock_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.lg_soundbar.config_flow.test_connect", + return_value={"uuid": "uuid", "name": "name"}, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + }, + ) + + assert result2["type"] == "abort" + assert result2["reason"] == "already_configured" From 7690ecc4ff533ec333dd276c867c04fc9be9a9cb Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 30 Jun 2022 03:43:14 +0200 Subject: [PATCH 03/71] Fix clicksend request content type headers (#74189) --- homeassistant/components/clicksend/notify.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/clicksend/notify.py b/homeassistant/components/clicksend/notify.py index 74f1c2e1ae5..ec6bed3c55d 100644 --- a/homeassistant/components/clicksend/notify.py +++ b/homeassistant/components/clicksend/notify.py @@ -3,7 +3,6 @@ from http import HTTPStatus import json import logging -from aiohttp.hdrs import CONTENT_TYPE import requests import voluptuous as vol @@ -23,7 +22,7 @@ BASE_API_URL = "https://rest.clicksend.com/v3" DEFAULT_SENDER = "hass" TIMEOUT = 5 -HEADERS = {CONTENT_TYPE: CONTENT_TYPE_JSON} +HEADERS = {"Content-Type": CONTENT_TYPE_JSON} PLATFORM_SCHEMA = vol.Schema( From 9d727d2a710c76249b88ceae4a85f4cdde072c8f Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Thu, 30 Jun 2022 02:10:25 +0300 Subject: [PATCH 04/71] Fix Shelly Duo RGBW color mode attribute (#74193) --- homeassistant/components/shelly/light.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py index 79db9c509f4..b75e1ad2377 100644 --- a/homeassistant/components/shelly/light.py +++ b/homeassistant/components/shelly/light.py @@ -215,7 +215,7 @@ class BlockShellyLight(ShellyBlockEntity, LightEntity): def color_mode(self) -> ColorMode: """Return the color mode of the light.""" if self.mode == "color": - if hasattr(self.block, "white"): + if self.wrapper.model in RGBW_MODELS: return ColorMode.RGBW return ColorMode.RGB From d36643947d43683c53fb2e26a33340b4e9f6547f Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Wed, 29 Jun 2022 18:52:54 -0400 Subject: [PATCH 05/71] Fix duplicate key for motion sensor for UniFi Protect (#74202) --- homeassistant/components/unifiprotect/binary_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index d3bf71a4274..62a4893692b 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -150,7 +150,7 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ufp_perm=PermRequired.NO_WRITE, ), ProtectBinaryEntityDescription( - key="motion", + key="motion_enabled", name="Detections: Motion", icon="mdi:run-fast", ufp_value="recording_settings.enable_motion_detection", From dbe552b1a1ea29aed1add941ac05637330213da3 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 1 Jul 2022 05:07:03 +1200 Subject: [PATCH 06/71] ESPHome use dhcp responses to update connection host of known devices (#74206) * ESPHome use dhcp responses to update connection host of known devices * Add test for dhcp * Add another test to cover when there are no changes required --- .../components/esphome/config_flow.py | 45 +++++++++++++- .../components/esphome/manifest.json | 1 + homeassistant/generated/dhcp.py | 1 + tests/components/esphome/test_config_flow.py | 60 ++++++++++++++++++- 4 files changed, 105 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 76359cda4e7..ecfa381bc69 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -16,7 +16,7 @@ from aioesphomeapi import ( ) import voluptuous as vol -from homeassistant.components import zeroconf +from homeassistant.components import dhcp, zeroconf from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT from homeassistant.core import callback @@ -189,6 +189,49 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): return await self.async_step_discovery_confirm() + async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + """Handle DHCP discovery.""" + node_name = discovery_info.hostname + + await self.async_set_unique_id(node_name) + self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.ip}) + + for entry in self._async_current_entries(): + found = False + + if CONF_HOST in entry.data and entry.data[CONF_HOST] in ( + discovery_info.ip, + f"{node_name}.local", + ): + # Is this address or IP address already configured? + found = True + elif DomainData.get(self.hass).is_entry_loaded(entry): + # Does a config entry with this name already exist? + data = DomainData.get(self.hass).get_entry_data(entry) + + # Node names are unique in the network + if data.device_info is not None: + found = data.device_info.name == node_name + + if found: + # Backwards compat, we update old entries + if not entry.unique_id: + self.hass.config_entries.async_update_entry( + entry, + data={ + **entry.data, + CONF_HOST: discovery_info.ip, + }, + unique_id=node_name, + ) + self.hass.async_create_task( + self.hass.config_entries.async_reload(entry.entry_id) + ) + + break + + return self.async_abort(reason="already_configured") + @callback def _async_get_entry(self) -> FlowResult: config_data = { diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index b89671c6f90..a8a76c2b0c8 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -5,6 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/esphome", "requirements": ["aioesphomeapi==10.10.0"], "zeroconf": ["_esphomelib._tcp.local."], + "dhcp": [{ "registered_devices": true }], "codeowners": ["@OttoWinter", "@jesserockz"], "after_dependencies": ["zeroconf", "tag"], "iot_class": "local_push", diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 91398ed00ef..e9cf6ca4c06 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -28,6 +28,7 @@ DHCP: list[dict[str, str | bool]] = [ {'domain': 'elkm1', 'macaddress': '00409D*'}, {'domain': 'emonitor', 'hostname': 'emonitor*', 'macaddress': '0090C2*'}, {'domain': 'emonitor', 'registered_devices': True}, + {'domain': 'esphome', 'registered_devices': True}, {'domain': 'flume', 'hostname': 'flume-gw-*'}, {'domain': 'flux_led', 'registered_devices': True}, {'domain': 'flux_led', 'hostname': '[ba][lk]*', 'macaddress': '18B905*'}, diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index f7da5d66bd5..1d2cff051ae 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -12,7 +12,7 @@ from aioesphomeapi import ( import pytest from homeassistant import config_entries -from homeassistant.components import zeroconf +from homeassistant.components import dhcp, zeroconf from homeassistant.components.esphome import CONF_NOISE_PSK, DOMAIN, DomainData from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.data_entry_flow import ( @@ -532,3 +532,61 @@ async def test_reauth_confirm_invalid(hass, mock_client, mock_zeroconf): assert result["step_id"] == "reauth_confirm" assert result["errors"] assert result["errors"]["base"] == "invalid_psk" + + +async def test_discovery_dhcp_updates_host(hass, mock_client): + """Test dhcp discovery updates host and aborts.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "192.168.43.183", CONF_PORT: 6053, CONF_PASSWORD: ""}, + ) + entry.add_to_hass(hass) + + mock_entry_data = MagicMock() + mock_entry_data.device_info.name = "test8266" + domain_data = DomainData.get(hass) + domain_data.set_entry_data(entry, mock_entry_data) + + service_info = dhcp.DhcpServiceInfo( + ip="192.168.43.184", + hostname="test8266", + macaddress="00:00:00:00:00:00", + ) + result = await hass.config_entries.flow.async_init( + "esphome", context={"source": config_entries.SOURCE_DHCP}, data=service_info + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + assert entry.unique_id == "test8266" + assert entry.data[CONF_HOST] == "192.168.43.184" + + +async def test_discovery_dhcp_no_changes(hass, mock_client): + """Test dhcp discovery updates host and aborts.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "192.168.43.183", CONF_PORT: 6053, CONF_PASSWORD: ""}, + ) + entry.add_to_hass(hass) + + mock_entry_data = MagicMock() + mock_entry_data.device_info.name = "test8266" + domain_data = DomainData.get(hass) + domain_data.set_entry_data(entry, mock_entry_data) + + service_info = dhcp.DhcpServiceInfo( + ip="192.168.43.183", + hostname="test8266", + macaddress="00:00:00:00:00:00", + ) + result = await hass.config_entries.flow.async_init( + "esphome", context={"source": config_entries.SOURCE_DHCP}, data=service_info + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + assert entry.unique_id == "test8266" + assert entry.data[CONF_HOST] == "192.168.43.183" From b135560274ba666e1a896008fdda5718f02bd84a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Jun 2022 19:14:56 -0500 Subject: [PATCH 07/71] Allow tuple subclasses to be json serialized (#74207) --- homeassistant/helpers/json.py | 2 +- tests/helpers/test_json.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/json.py b/homeassistant/helpers/json.py index 8b91f5eb2b5..74a2f542910 100644 --- a/homeassistant/helpers/json.py +++ b/homeassistant/helpers/json.py @@ -33,7 +33,7 @@ def json_encoder_default(obj: Any) -> Any: Hand other objects to the original method. """ - if isinstance(obj, set): + if isinstance(obj, (set, tuple)): return list(obj) if isinstance(obj, float): return float(obj) diff --git a/tests/helpers/test_json.py b/tests/helpers/test_json.py index cfb403ca4a9..54c488690fa 100644 --- a/tests/helpers/test_json.py +++ b/tests/helpers/test_json.py @@ -1,6 +1,7 @@ """Test Home Assistant remote methods and classes.""" import datetime import json +import time import pytest @@ -87,3 +88,11 @@ def test_json_dumps_float_subclass(): """A float subclass.""" assert json_dumps({"c": FloatSubclass(1.2)}) == '{"c":1.2}' + + +def test_json_dumps_tuple_subclass(): + """Test the json dumps a tuple subclass.""" + + tt = time.struct_time((1999, 3, 17, 32, 44, 55, 2, 76, 0)) + + assert json_dumps(tt) == "[1999,3,17,32,44,55,2,76,0]" From 1e8c897702b2f642bdd09f3caf6dc445c9a3e160 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 30 Jun 2022 03:40:58 +0200 Subject: [PATCH 08/71] Update requests to 2.28.1 (#74210) --- homeassistant/package_constraints.txt | 6 +----- pyproject.toml | 2 +- requirements.txt | 2 +- requirements_test.txt | 2 +- script/gen_requirements_all.py | 4 ---- 5 files changed, 4 insertions(+), 12 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 11ffd9c4c0c..9ba945c3e2b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -28,7 +28,7 @@ pyserial==3.5 python-slugify==4.0.1 pyudev==0.22.0 pyyaml==6.0 -requests==2.28.0 +requests==2.28.1 scapy==2.4.5 sqlalchemy==1.4.38 typing-extensions>=3.10.0.2,<5.0 @@ -114,7 +114,3 @@ backoff<2.0 # Breaking change in version # https://github.com/samuelcolvin/pydantic/issues/4092 pydantic!=1.9.1 - -# Pin charset-normalizer to 2.0.12 due to version conflict. -# https://github.com/home-assistant/core/pull/74104 -charset-normalizer==2.0.12 diff --git a/pyproject.toml b/pyproject.toml index 2cf2db3f240..69c5a539f3f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,7 @@ dependencies = [ "pip>=21.0,<22.2", "python-slugify==4.0.1", "pyyaml==6.0", - "requests==2.28.0", + "requests==2.28.1", "typing-extensions>=3.10.0.2,<5.0", "voluptuous==0.13.1", "voluptuous-serialize==2.5.0", diff --git a/requirements.txt b/requirements.txt index 7506201eae1..98b148fa923 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,7 +19,7 @@ orjson==3.7.5 pip>=21.0,<22.2 python-slugify==4.0.1 pyyaml==6.0 -requests==2.28.0 +requests==2.28.1 typing-extensions>=3.10.0.2,<5.0 voluptuous==0.13.1 voluptuous-serialize==2.5.0 diff --git a/requirements_test.txt b/requirements_test.txt index 046d8bfb400..6072ce896ee 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -42,6 +42,6 @@ types-pkg-resources==0.1.3 types-python-slugify==0.1.2 types-pytz==2021.1.2 types-PyYAML==5.4.6 -types-requests==2.27.30 +types-requests==2.28.0 types-toml==0.1.5 types-ujson==0.1.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index c6b50c6bd32..a2a0eab897a 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -132,10 +132,6 @@ backoff<2.0 # Breaking change in version # https://github.com/samuelcolvin/pydantic/issues/4092 pydantic!=1.9.1 - -# Pin charset-normalizer to 2.0.12 due to version conflict. -# https://github.com/home-assistant/core/pull/74104 -charset-normalizer==2.0.12 """ IGNORE_PRE_COMMIT_HOOK_ID = ( From f4df584f1326b5f19bbe6d120c5aa64075057b96 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 30 Jun 2022 07:06:35 +0200 Subject: [PATCH 09/71] Fix input_number invalid state restore handling (#74213) Co-authored-by: J. Nick Koston --- .../components/input_number/__init__.py | 7 ++++-- tests/components/input_number/test_init.py | 23 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/input_number/__init__.py b/homeassistant/components/input_number/__init__.py index a6ee8dd0f7d..8e922687e59 100644 --- a/homeassistant/components/input_number/__init__.py +++ b/homeassistant/components/input_number/__init__.py @@ -1,6 +1,7 @@ """Support to set a numeric value from a slider or text box.""" from __future__ import annotations +from contextlib import suppress import logging import voluptuous as vol @@ -281,8 +282,10 @@ class InputNumber(RestoreEntity): if self._current_value is not None: return - state = await self.async_get_last_state() - value = state and float(state.state) + value: float | None = None + if state := await self.async_get_last_state(): + with suppress(ValueError): + value = float(state.state) # Check against None because value can be 0 if value is not None and self._minimum <= value <= self._maximum: diff --git a/tests/components/input_number/test_init.py b/tests/components/input_number/test_init.py index ca496723d99..4149627720b 100644 --- a/tests/components/input_number/test_init.py +++ b/tests/components/input_number/test_init.py @@ -255,6 +255,29 @@ async def test_restore_state(hass): assert float(state.state) == 10 +async def test_restore_invalid_state(hass): + """Ensure an invalid restore state is handled.""" + mock_restore_cache( + hass, (State("input_number.b1", "="), State("input_number.b2", "200")) + ) + + hass.state = CoreState.starting + + await async_setup_component( + hass, + DOMAIN, + {DOMAIN: {"b1": {"min": 2, "max": 100}, "b2": {"min": 10, "max": 100}}}, + ) + + state = hass.states.get("input_number.b1") + assert state + assert float(state.state) == 2 + + state = hass.states.get("input_number.b2") + assert state + assert float(state.state) == 10 + + async def test_initial_state_overrules_restore_state(hass): """Ensure states are restored on startup.""" mock_restore_cache( From e3b99fe62ab5e16611df7a3740f26b28ad74deb5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 30 Jun 2022 10:00:10 -0700 Subject: [PATCH 10/71] Treat thermostat unknown state like unavailable in alexa (#74220) --- homeassistant/components/alexa/capabilities.py | 2 ++ tests/components/alexa/test_capabilities.py | 17 ++++++++++------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 818b4b794cf..25ec43b689c 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -1046,6 +1046,8 @@ class AlexaThermostatController(AlexaCapability): if preset in API_THERMOSTAT_PRESETS: mode = API_THERMOSTAT_PRESETS[preset] + elif self.entity.state == STATE_UNKNOWN: + return None else: mode = API_THERMOSTAT_MODES.get(self.entity.state) if mode is None: diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index 3e176b0fb8c..ea6c96bbaef 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -658,13 +658,16 @@ async def test_report_climate_state(hass): "Alexa.TemperatureSensor", "temperature", {"value": 34.0, "scale": "CELSIUS"} ) - hass.states.async_set( - "climate.unavailable", - "unavailable", - {"friendly_name": "Climate Unavailable", "supported_features": 91}, - ) - properties = await reported_properties(hass, "climate.unavailable") - properties.assert_not_has_property("Alexa.ThermostatController", "thermostatMode") + for state in "unavailable", "unknown": + hass.states.async_set( + f"climate.{state}", + state, + {"friendly_name": f"Climate {state}", "supported_features": 91}, + ) + properties = await reported_properties(hass, f"climate.{state}") + properties.assert_not_has_property( + "Alexa.ThermostatController", "thermostatMode" + ) hass.states.async_set( "climate.unsupported", From b71205acd74588df57d4f8108ccd6cf7e1dd5f76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Thu, 30 Jun 2022 18:59:46 +0200 Subject: [PATCH 11/71] Make media_player.toggle turn on a standby device (#74221) --- homeassistant/components/media_player/__init__.py | 3 ++- .../components/media_player/test_async_helpers.py | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index dc2f3624a0e..14546a36ec8 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -52,6 +52,7 @@ from homeassistant.const import ( STATE_IDLE, STATE_OFF, STATE_PLAYING, + STATE_STANDBY, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -888,7 +889,7 @@ class MediaPlayerEntity(Entity): await self.hass.async_add_executor_job(self.toggle) return - if self.state in (STATE_OFF, STATE_IDLE): + if self.state in (STATE_OFF, STATE_IDLE, STATE_STANDBY): await self.async_turn_on() else: await self.async_turn_off() diff --git a/tests/components/media_player/test_async_helpers.py b/tests/components/media_player/test_async_helpers.py index 53c80bfc8de..8be263e7ee0 100644 --- a/tests/components/media_player/test_async_helpers.py +++ b/tests/components/media_player/test_async_helpers.py @@ -8,6 +8,7 @@ from homeassistant.const import ( STATE_ON, STATE_PAUSED, STATE_PLAYING, + STATE_STANDBY, ) @@ -79,9 +80,13 @@ class ExtendedMediaPlayer(mp.MediaPlayerEntity): """Turn off state.""" self._state = STATE_OFF + def standby(self): + """Put device in standby.""" + self._state = STATE_STANDBY + def toggle(self): """Toggle the power on the media player.""" - if self._state in [STATE_OFF, STATE_IDLE]: + if self._state in [STATE_OFF, STATE_IDLE, STATE_STANDBY]: self._state = STATE_ON else: self._state = STATE_OFF @@ -138,6 +143,10 @@ class SimpleMediaPlayer(mp.MediaPlayerEntity): """Turn off state.""" self._state = STATE_OFF + def standby(self): + """Put device in standby.""" + self._state = STATE_STANDBY + @pytest.fixture(params=[ExtendedMediaPlayer, SimpleMediaPlayer]) def player(hass, request): @@ -188,3 +197,7 @@ async def test_toggle(player): assert player.state == STATE_ON await player.async_toggle() assert player.state == STATE_OFF + player.standby() + assert player.state == STATE_STANDBY + await player.async_toggle() + assert player.state == STATE_ON From 518468a70bd2b423bab6590055affd9590210f73 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 29 Jun 2022 23:54:51 -0700 Subject: [PATCH 12/71] Allow legacy nest integration with no configuration.yaml (#74222) --- homeassistant/components/nest/__init__.py | 2 +- tests/components/nest/common.py | 12 +++++++++++- tests/components/nest/test_init_legacy.py | 5 ++++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 0e0128136ad..b31354b598c 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -177,7 +177,7 @@ class SignalUpdateCallback: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Nest from a config entry with dispatch between old/new flows.""" config_mode = config_flow.get_config_mode(hass) - if config_mode == config_flow.ConfigMode.LEGACY: + if DATA_SDM not in entry.data or config_mode == config_flow.ConfigMode.LEGACY: return await async_setup_legacy_entry(hass, entry) if config_mode == config_flow.ConfigMode.SDM: diff --git a/tests/components/nest/common.py b/tests/components/nest/common.py index 765a954b6de..f86112ada75 100644 --- a/tests/components/nest/common.py +++ b/tests/components/nest/common.py @@ -147,7 +147,17 @@ TEST_CONFIG_LEGACY = NestTestConfig( }, }, }, - credential=None, +) +TEST_CONFIG_ENTRY_LEGACY = NestTestConfig( + config_entry_data={ + "auth_implementation": "local", + "tokens": { + "expires_at": time.time() + 86400, + "access_token": { + "token": "some-token", + }, + }, + }, ) diff --git a/tests/components/nest/test_init_legacy.py b/tests/components/nest/test_init_legacy.py index cbf1bfe2d48..fc4e6070faf 100644 --- a/tests/components/nest/test_init_legacy.py +++ b/tests/components/nest/test_init_legacy.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock, PropertyMock, patch import pytest -from .common import TEST_CONFIG_LEGACY +from .common import TEST_CONFIG_ENTRY_LEGACY, TEST_CONFIG_LEGACY DOMAIN = "nest" @@ -33,6 +33,9 @@ def make_thermostat(): return device +@pytest.mark.parametrize( + "nest_test_config", [TEST_CONFIG_LEGACY, TEST_CONFIG_ENTRY_LEGACY] +) async def test_thermostat(hass, setup_base_platform): """Test simple initialization for thermostat entities.""" From a36a2d53ecf1be8e567400c6b4f42b0bf616b87b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 30 Jun 2022 09:42:15 +0200 Subject: [PATCH 13/71] Correct native_pressure_unit for zamg weather (#74225) --- homeassistant/components/zamg/weather.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zamg/weather.py b/homeassistant/components/zamg/weather.py index 2bf4a5b39f6..6910955fcf7 100644 --- a/homeassistant/components/zamg/weather.py +++ b/homeassistant/components/zamg/weather.py @@ -18,7 +18,7 @@ from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, - LENGTH_MILLIMETERS, + PRESSURE_HPA, SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS, ) @@ -87,9 +87,7 @@ def setup_platform( class ZamgWeather(WeatherEntity): """Representation of a weather condition.""" - _attr_native_pressure_unit = ( - LENGTH_MILLIMETERS # API reports l/m², equivalent to mm - ) + _attr_native_pressure_unit = PRESSURE_HPA _attr_native_temperature_unit = TEMP_CELSIUS _attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR From e1fc2ed0466173daf165cfd869734f99deaef534 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 30 Jun 2022 19:15:25 +0200 Subject: [PATCH 14/71] Fire event_mqtt_reloaded only after reload is completed (#74226) --- homeassistant/components/mqtt/__init__.py | 70 +++++++++++-------- .../components/mqtt/alarm_control_panel.py | 6 +- .../components/mqtt/binary_sensor.py | 6 +- homeassistant/components/mqtt/button.py | 6 +- homeassistant/components/mqtt/camera.py | 6 +- homeassistant/components/mqtt/climate.py | 6 +- homeassistant/components/mqtt/const.py | 3 +- homeassistant/components/mqtt/cover.py | 6 +- homeassistant/components/mqtt/fan.py | 4 +- homeassistant/components/mqtt/humidifier.py | 6 +- .../components/mqtt/light/__init__.py | 6 +- homeassistant/components/mqtt/lock.py | 6 +- homeassistant/components/mqtt/mixins.py | 46 +++++------- homeassistant/components/mqtt/number.py | 6 +- homeassistant/components/mqtt/scene.py | 6 +- homeassistant/components/mqtt/select.py | 6 +- homeassistant/components/mqtt/sensor.py | 6 +- homeassistant/components/mqtt/siren.py | 6 +- homeassistant/components/mqtt/switch.py | 6 +- .../components/mqtt/vacuum/__init__.py | 6 +- 20 files changed, 96 insertions(+), 123 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 6fd288a86cf..a099e7b580c 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -28,14 +28,12 @@ from homeassistant.data_entry_flow import BaseServiceInfo from homeassistant.exceptions import TemplateError, Unauthorized from homeassistant.helpers import config_validation as cv, event, template from homeassistant.helpers.device_registry import DeviceEntry -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.reload import ( async_integration_yaml_config, - async_setup_reload_service, + async_reload_integration_platforms, ) +from homeassistant.helpers.service import async_register_admin_service from homeassistant.helpers.typing import ConfigType # Loading the config flow file will register the flow @@ -78,10 +76,10 @@ from .const import ( # noqa: F401 DOMAIN, MQTT_CONNECTED, MQTT_DISCONNECTED, - MQTT_RELOADED, PLATFORMS, RELOADABLE_PLATFORMS, ) +from .mixins import async_discover_yaml_entities from .models import ( # noqa: F401 MqttCommandTemplate, MqttValueTemplate, @@ -241,7 +239,9 @@ async def _async_config_entry_updated(hass: HomeAssistant, entry: ConfigEntry) - await _async_setup_discovery(hass, mqtt_client.conf, entry) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry( # noqa: C901 + hass: HomeAssistant, entry: ConfigEntry +) -> bool: """Load a config entry.""" # Merge basic configuration, and add missing defaults for basic options _merge_basic_config(hass, entry, hass.data.get(DATA_MQTT_CONFIG, {})) @@ -378,16 +378,32 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DATA_CONFIG_ENTRY_LOCK] = asyncio.Lock() hass.data[CONFIG_ENTRY_IS_SETUP] = set() - # Setup reload service. Once support for legacy config is removed in 2022.9, we - # should no longer call async_setup_reload_service but instead implement a custom - # service - await async_setup_reload_service(hass, DOMAIN, RELOADABLE_PLATFORMS) + async def async_setup_reload_service() -> None: + """Create the reload service for the MQTT domain.""" + if hass.services.has_service(DOMAIN, SERVICE_RELOAD): + return - async def _async_reload_platforms(_: Event | None) -> None: - """Discover entities for a platform.""" - config_yaml = await async_integration_yaml_config(hass, DOMAIN) or {} - hass.data[DATA_MQTT_UPDATED_CONFIG] = config_yaml.get(DOMAIN, {}) - async_dispatcher_send(hass, MQTT_RELOADED) + async def _reload_config(call: ServiceCall) -> None: + """Reload the platforms.""" + # Reload the legacy yaml platform + await async_reload_integration_platforms(hass, DOMAIN, RELOADABLE_PLATFORMS) + + # Reload the modern yaml platforms + config_yaml = await async_integration_yaml_config(hass, DOMAIN) or {} + hass.data[DATA_MQTT_UPDATED_CONFIG] = config_yaml.get(DOMAIN, {}) + await asyncio.gather( + *( + [ + async_discover_yaml_entities(hass, component) + for component in RELOADABLE_PLATFORMS + ] + ) + ) + + # Fire event + hass.bus.async_fire(f"event_{DOMAIN}_reloaded", context=call.context) + + async_register_admin_service(hass, DOMAIN, SERVICE_RELOAD, _reload_config) async def async_forward_entry_setup_and_setup_discovery(config_entry): """Forward the config entry setup to the platforms and set up discovery.""" @@ -411,21 +427,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if conf.get(CONF_DISCOVERY): await _async_setup_discovery(hass, conf, entry) # Setup reload service after all platforms have loaded - entry.async_on_unload( - hass.bus.async_listen("event_mqtt_reloaded", _async_reload_platforms) - ) + await async_setup_reload_service() + + if DATA_MQTT_RELOAD_NEEDED in hass.data: + hass.data.pop(DATA_MQTT_RELOAD_NEEDED) + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + {}, + blocking=False, + ) hass.async_create_task(async_forward_entry_setup_and_setup_discovery(entry)) - if DATA_MQTT_RELOAD_NEEDED in hass.data: - hass.data.pop(DATA_MQTT_RELOAD_NEEDED) - await hass.services.async_call( - DOMAIN, - SERVICE_RELOAD, - {}, - blocking=False, - ) - return True diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index 8e5ee54d688..b6f2f8f236e 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -44,8 +44,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -147,9 +147,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT alarm control panel through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, alarm.DOMAIN) - ) + await async_discover_yaml_entities(hass, alarm.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index 39fd87c8b02..9e0a049b15e 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -41,8 +41,8 @@ from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttAvailability, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -102,9 +102,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT binary sensor through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, binary_sensor.DOMAIN) - ) + await async_discover_yaml_entities(hass, binary_sensor.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/button.py b/homeassistant/components/mqtt/button.py index 0374727bf7d..b75fbe4b97f 100644 --- a/homeassistant/components/mqtt/button.py +++ b/homeassistant/components/mqtt/button.py @@ -25,8 +25,8 @@ from .const import ( from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -82,9 +82,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT button through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, button.DOMAIN) - ) + await async_discover_yaml_entities(hass, button.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index 5c8d3bc48b2..69af7992229 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -22,8 +22,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -80,9 +80,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT camera through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, camera.DOMAIN) - ) + await async_discover_yaml_entities(hass, camera.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index a26e9cba8df..6b09891483c 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -50,8 +50,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -391,9 +391,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT climate device through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, climate.DOMAIN) - ) + await async_discover_yaml_entities(hass, climate.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 67a9208faba..6ac77021337 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -64,7 +64,6 @@ DOMAIN = "mqtt" MQTT_CONNECTED = "mqtt_connected" MQTT_DISCONNECTED = "mqtt_disconnected" -MQTT_RELOADED = "mqtt_reloaded" PAYLOAD_EMPTY_JSON = "{}" PAYLOAD_NONE = "None" @@ -105,8 +104,8 @@ RELOADABLE_PLATFORMS = [ Platform.LIGHT, Platform.LOCK, Platform.NUMBER, - Platform.SELECT, Platform.SCENE, + Platform.SELECT, Platform.SENSOR, Platform.SIREN, Platform.SWITCH, diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 54ed4f2b0a0..14746329250 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -46,8 +46,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -242,9 +242,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT cover through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, cover.DOMAIN) - ) + await async_discover_yaml_entities(hass, cover.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 721fa93f244..15e4a80f3e7 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -50,8 +50,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -232,7 +232,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT fan through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload(await async_setup_platform_discovery(hass, fan.DOMAIN)) + await async_discover_yaml_entities(hass, fan.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/humidifier.py b/homeassistant/components/mqtt/humidifier.py index d2856767cf0..5f09fc0d513 100644 --- a/homeassistant/components/mqtt/humidifier.py +++ b/homeassistant/components/mqtt/humidifier.py @@ -45,8 +45,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -187,9 +187,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT humidifier through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, humidifier.DOMAIN) - ) + await async_discover_yaml_entities(hass, humidifier.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/light/__init__.py b/homeassistant/components/mqtt/light/__init__.py index d4914cb9506..c7f3395ba4e 100644 --- a/homeassistant/components/mqtt/light/__init__.py +++ b/homeassistant/components/mqtt/light/__init__.py @@ -13,8 +13,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from ..mixins import ( + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -111,9 +111,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT lights configured under the light platform key (deprecated).""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, light.DOMAIN) - ) + await async_discover_yaml_entities(hass, light.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index 1d6a40c2331..b4788f1db0c 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -28,8 +28,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -103,9 +103,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT lock through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, lock.DOMAIN) - ) + await async_discover_yaml_entities(hass, lock.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 10fe6cb6cc5..8e59d09dfce 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -26,7 +26,7 @@ from homeassistant.const import ( CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, ) -from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import ( config_validation as cv, device_registry as dr, @@ -69,7 +69,6 @@ from .const import ( DOMAIN, MQTT_CONNECTED, MQTT_DISCONNECTED, - MQTT_RELOADED, ) from .debug_info import log_message, log_messages from .discovery import ( @@ -261,34 +260,27 @@ class SetupEntity(Protocol): """Define setup_entities type.""" -async def async_setup_platform_discovery( +async def async_discover_yaml_entities( hass: HomeAssistant, platform_domain: str -) -> CALLBACK_TYPE: - """Set up platform discovery for manual config.""" - - async def _async_discover_entities() -> None: - """Discover entities for a platform.""" - if DATA_MQTT_UPDATED_CONFIG in hass.data: - # The platform has been reloaded - config_yaml = hass.data[DATA_MQTT_UPDATED_CONFIG] - else: - config_yaml = hass.data.get(DATA_MQTT_CONFIG, {}) - if not config_yaml: - return - if platform_domain not in config_yaml: - return - await asyncio.gather( - *( - discovery.async_load_platform(hass, platform_domain, DOMAIN, config, {}) - for config in await async_get_platform_config_from_yaml( - hass, platform_domain, config_yaml - ) +) -> None: + """Discover entities for a platform.""" + if DATA_MQTT_UPDATED_CONFIG in hass.data: + # The platform has been reloaded + config_yaml = hass.data[DATA_MQTT_UPDATED_CONFIG] + else: + config_yaml = hass.data.get(DATA_MQTT_CONFIG, {}) + if not config_yaml: + return + if platform_domain not in config_yaml: + return + await asyncio.gather( + *( + discovery.async_load_platform(hass, platform_domain, DOMAIN, config, {}) + for config in await async_get_platform_config_from_yaml( + hass, platform_domain, config_yaml ) ) - - unsub = async_dispatcher_connect(hass, MQTT_RELOADED, _async_discover_entities) - await _async_discover_entities() - return unsub + ) async def async_get_platform_config_from_yaml( diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index 660ffe987f0..dc27a740720 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -41,8 +41,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -135,9 +135,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT number through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, number.DOMAIN) - ) + await async_discover_yaml_entities(hass, number.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/scene.py b/homeassistant/components/mqtt/scene.py index cc911cc3431..8b654f7cca0 100644 --- a/homeassistant/components/mqtt/scene.py +++ b/homeassistant/components/mqtt/scene.py @@ -22,8 +22,8 @@ from .mixins import ( CONF_OBJECT_ID, MQTT_AVAILABILITY_SCHEMA, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -79,9 +79,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT scene through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, scene.DOMAIN) - ) + await async_discover_yaml_entities(hass, scene.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py index 0d9f1411fd1..4c302446b19 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -30,8 +30,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -94,9 +94,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT select through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, select.DOMAIN) - ) + await async_discover_yaml_entities(hass, select.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 672e22f632f..6948e173039 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -41,8 +41,8 @@ from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttAvailability, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -147,9 +147,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT sensor through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, sensor.DOMAIN) - ) + await async_discover_yaml_entities(hass, sensor.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/siren.py b/homeassistant/components/mqtt/siren.py index a6cff4cf91d..dfb89d2ee79 100644 --- a/homeassistant/components/mqtt/siren.py +++ b/homeassistant/components/mqtt/siren.py @@ -51,8 +51,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -143,9 +143,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT siren through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, siren.DOMAIN) - ) + await async_discover_yaml_entities(hass, siren.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index dadd5f86f20..b04f2433659 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -37,8 +37,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -97,9 +97,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT switch through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, switch.DOMAIN) - ) + await async_discover_yaml_entities(hass, switch.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/vacuum/__init__.py b/homeassistant/components/mqtt/vacuum/__init__.py index 694e9530939..c49b8cfa012 100644 --- a/homeassistant/components/mqtt/vacuum/__init__.py +++ b/homeassistant/components/mqtt/vacuum/__init__.py @@ -12,8 +12,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from ..mixins import ( + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, ) from .schema import CONF_SCHEMA, LEGACY, MQTT_VACUUM_SCHEMA, STATE @@ -91,9 +91,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT vacuum through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, vacuum.DOMAIN) - ) + await async_discover_yaml_entities(hass, vacuum.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry From 15149f4aa1c9667deb796e0575bf7fe41f4a3f23 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Thu, 30 Jun 2022 13:03:39 -0400 Subject: [PATCH 15/71] Fix ZHA events for logbook (#74245) --- homeassistant/components/zha/logbook.py | 10 +++++++--- tests/components/zha/test_logbook.py | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/logbook.py b/homeassistant/components/zha/logbook.py index e2d238ddbe8..8140a5244f1 100644 --- a/homeassistant/components/zha/logbook.py +++ b/homeassistant/components/zha/logbook.py @@ -62,14 +62,18 @@ def async_describe_events( break if event_type is None: - event_type = event_data[ATTR_COMMAND] + event_type = event_data.get(ATTR_COMMAND, ZHA_EVENT) if event_subtype is not None and event_subtype != event_type: event_type = f"{event_type} - {event_subtype}" - event_type = event_type.replace("_", " ").title() + if event_type is not None: + event_type = event_type.replace("_", " ").title() + if "event" in event_type.lower(): + message = f"{event_type} was fired" + else: + message = f"{event_type} event was fired" - message = f"{event_type} event was fired" if event_data["params"]: message = f"{message} with parameters: {event_data['params']}" diff --git a/tests/components/zha/test_logbook.py b/tests/components/zha/test_logbook.py index 33b758fd0a7..6c28284b1e6 100644 --- a/tests/components/zha/test_logbook.py +++ b/tests/components/zha/test_logbook.py @@ -172,6 +172,19 @@ async def test_zha_logbook_event_device_no_triggers(hass, mock_devices): }, }, ), + MockRow( + ZHA_EVENT, + { + CONF_DEVICE_ID: reg_device.id, + "device_ieee": str(ieee_address), + CONF_UNIQUE_ID: f"{str(ieee_address)}:1:0x0006", + "endpoint_id": 1, + "cluster_id": 6, + "params": { + "test": "test", + }, + }, + ), ], ) @@ -182,6 +195,12 @@ async def test_zha_logbook_event_device_no_triggers(hass, mock_devices): == "Shake event was fired with parameters: {'test': 'test'}" ) + assert events[1]["name"] == "FakeManufacturer FakeModel" + assert events[1]["domain"] == "zha" + assert ( + events[1]["message"] == "Zha Event was fired with parameters: {'test': 'test'}" + ) + async def test_zha_logbook_event_device_no_device(hass, mock_devices): """Test zha logbook events without device and without triggers.""" From 00468db5afbfdf9b41b1c57a750f9302ed82603b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 30 Jun 2022 19:04:59 +0200 Subject: [PATCH 16/71] Update numpy to 1.23.0 (#74250) --- homeassistant/components/compensation/manifest.json | 2 +- homeassistant/components/iqvia/manifest.json | 2 +- homeassistant/components/opencv/manifest.json | 2 +- homeassistant/components/tensorflow/manifest.json | 2 +- homeassistant/components/trend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/gen_requirements_all.py | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/compensation/manifest.json b/homeassistant/components/compensation/manifest.json index eb3135954b0..509d5740b22 100644 --- a/homeassistant/components/compensation/manifest.json +++ b/homeassistant/components/compensation/manifest.json @@ -2,7 +2,7 @@ "domain": "compensation", "name": "Compensation", "documentation": "https://www.home-assistant.io/integrations/compensation", - "requirements": ["numpy==1.22.4"], + "requirements": ["numpy==1.23.0"], "codeowners": ["@Petro31"], "iot_class": "calculated" } diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index 8b0dacd3575..7485ff9d608 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.22.4", "pyiqvia==2022.04.0"], + "requirements": ["numpy==1.23.0", "pyiqvia==2022.04.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "loggers": ["pyiqvia"] diff --git a/homeassistant/components/opencv/manifest.json b/homeassistant/components/opencv/manifest.json index 8cd1604f106..0272feb0f9e 100644 --- a/homeassistant/components/opencv/manifest.json +++ b/homeassistant/components/opencv/manifest.json @@ -2,7 +2,7 @@ "domain": "opencv", "name": "OpenCV", "documentation": "https://www.home-assistant.io/integrations/opencv", - "requirements": ["numpy==1.22.4", "opencv-python-headless==4.6.0.66"], + "requirements": ["numpy==1.23.0", "opencv-python-headless==4.6.0.66"], "codeowners": [], "iot_class": "local_push" } diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index 4168d820fb6..42d0eae1ecd 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -6,7 +6,7 @@ "tensorflow==2.5.0", "tf-models-official==2.5.0", "pycocotools==2.0.1", - "numpy==1.22.4", + "numpy==1.23.0", "pillow==9.1.1" ], "codeowners": [], diff --git a/homeassistant/components/trend/manifest.json b/homeassistant/components/trend/manifest.json index 578aea3bbc6..b579cc036bb 100644 --- a/homeassistant/components/trend/manifest.json +++ b/homeassistant/components/trend/manifest.json @@ -2,7 +2,7 @@ "domain": "trend", "name": "Trend", "documentation": "https://www.home-assistant.io/integrations/trend", - "requirements": ["numpy==1.22.4"], + "requirements": ["numpy==1.23.0"], "codeowners": [], "quality_scale": "internal", "iot_class": "local_push" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 9ba945c3e2b..6b7b689b93d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -88,7 +88,7 @@ httpcore==0.15.0 hyperframe>=5.2.0 # Ensure we run compatible with musllinux build env -numpy>=1.22.0 +numpy==1.23.0 # pytest_asyncio breaks our test suite. We rely on pytest-aiohttp instead pytest_asyncio==1000000000.0.0 diff --git a/requirements_all.txt b/requirements_all.txt index 6f677d1d092..2ebabd89a5a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1126,7 +1126,7 @@ numato-gpio==0.10.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.22.4 +numpy==1.23.0 # homeassistant.components.oasa_telematics oasatelematics==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d0c9bc54ddd..a19338e5715 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -779,7 +779,7 @@ numato-gpio==0.10.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.22.4 +numpy==1.23.0 # homeassistant.components.google oauth2client==4.1.3 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index a2a0eab897a..11e88976e83 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -106,7 +106,7 @@ httpcore==0.15.0 hyperframe>=5.2.0 # Ensure we run compatible with musllinux build env -numpy>=1.22.0 +numpy==1.23.0 # pytest_asyncio breaks our test suite. We rely on pytest-aiohttp instead pytest_asyncio==1000000000.0.0 From d47e1d28dea6f91b5f50470b015263b189b57a55 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Jun 2022 12:39:36 -0500 Subject: [PATCH 17/71] Filter out CONF_SCAN_INTERVAL from scrape import (#74254) --- homeassistant/components/scrape/sensor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index 1c447439820..a73dbc17c1c 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -24,6 +24,7 @@ from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, CONF_RESOURCE, + CONF_SCAN_INTERVAL, CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME, CONF_VALUE_TEMPLATE, @@ -90,7 +91,7 @@ async def async_setup_platform( hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, - data=config, + data={k: v for k, v in config.items() if k != CONF_SCAN_INTERVAL}, ) ) From 249af3a78dc7a6201333df6b4b3626bf39ff2189 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 30 Jun 2022 19:47:29 +0200 Subject: [PATCH 18/71] Met.no use native_* (#74259) --- homeassistant/components/met/const.py | 16 +++---- homeassistant/components/met/weather.py | 57 ++++++------------------- 2 files changed, 20 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/met/const.py b/homeassistant/components/met/const.py index 93f9e3414dd..5b2a756847e 100644 --- a/homeassistant/components/met/const.py +++ b/homeassistant/components/met/const.py @@ -11,13 +11,13 @@ from homeassistant.components.weather import ( ATTR_CONDITION_SNOWY_RAINY, ATTR_CONDITION_SUNNY, ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_PRESSURE, ATTR_WEATHER_TEMPERATURE, @@ -173,13 +173,13 @@ CONDITIONS_MAP = { FORECAST_MAP = { ATTR_FORECAST_CONDITION: "condition", - ATTR_FORECAST_PRECIPITATION: "precipitation", + ATTR_FORECAST_NATIVE_PRECIPITATION: "precipitation", ATTR_FORECAST_PRECIPITATION_PROBABILITY: "precipitation_probability", - ATTR_FORECAST_TEMP: "temperature", - ATTR_FORECAST_TEMP_LOW: "templow", + ATTR_FORECAST_NATIVE_TEMP: "temperature", + ATTR_FORECAST_NATIVE_TEMP_LOW: "templow", ATTR_FORECAST_TIME: "datetime", ATTR_FORECAST_WIND_BEARING: "wind_bearing", - ATTR_FORECAST_WIND_SPEED: "wind_speed", + ATTR_FORECAST_NATIVE_WIND_SPEED: "wind_speed", } ATTR_MAP = { diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py index 251d99ad295..0ff0a60bfa1 100644 --- a/homeassistant/components/met/weather.py +++ b/homeassistant/components/met/weather.py @@ -6,7 +6,6 @@ from typing import Any from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, - ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME, ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_PRESSURE, @@ -21,12 +20,9 @@ from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, - LENGTH_INCHES, LENGTH_MILLIMETERS, PRESSURE_HPA, - PRESSURE_INHG, SPEED_KILOMETERS_PER_HOUR, - SPEED_MILES_PER_HOUR, TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant @@ -34,19 +30,9 @@ from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from homeassistant.util.distance import convert as convert_distance -from homeassistant.util.pressure import convert as convert_pressure -from homeassistant.util.speed import convert as convert_speed from . import MetDataUpdateCoordinator -from .const import ( - ATTR_FORECAST_PRECIPITATION, - ATTR_MAP, - CONDITIONS_MAP, - CONF_TRACK_HOME, - DOMAIN, - FORECAST_MAP, -) +from .const import ATTR_MAP, CONDITIONS_MAP, CONF_TRACK_HOME, DOMAIN, FORECAST_MAP ATTRIBUTION = ( "Weather forecast from met.no, delivered by the Norwegian " @@ -85,6 +71,11 @@ def format_condition(condition: str) -> str: class MetWeather(CoordinatorEntity[MetDataUpdateCoordinator], WeatherEntity): """Implementation of a Met.no weather condition.""" + _attr_native_temperature_unit = TEMP_CELSIUS + _attr_native_precipitation_unit = LENGTH_MILLIMETERS + _attr_native_pressure_unit = PRESSURE_HPA + _attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR + def __init__( self, coordinator: MetDataUpdateCoordinator, @@ -144,27 +135,18 @@ class MetWeather(CoordinatorEntity[MetDataUpdateCoordinator], WeatherEntity): return format_condition(condition) @property - def temperature(self) -> float | None: + def native_temperature(self) -> float | None: """Return the temperature.""" return self.coordinator.data.current_weather_data.get( ATTR_MAP[ATTR_WEATHER_TEMPERATURE] ) @property - def temperature_unit(self) -> str: - """Return the unit of measurement.""" - return TEMP_CELSIUS - - @property - def pressure(self) -> float | None: + def native_pressure(self) -> float | None: """Return the pressure.""" - pressure_hpa = self.coordinator.data.current_weather_data.get( + return self.coordinator.data.current_weather_data.get( ATTR_MAP[ATTR_WEATHER_PRESSURE] ) - if self._is_metric or pressure_hpa is None: - return pressure_hpa - - return round(convert_pressure(pressure_hpa, PRESSURE_HPA, PRESSURE_INHG), 2) @property def humidity(self) -> float | None: @@ -174,18 +156,11 @@ class MetWeather(CoordinatorEntity[MetDataUpdateCoordinator], WeatherEntity): ) @property - def wind_speed(self) -> float | None: + def native_wind_speed(self) -> float | None: """Return the wind speed.""" - speed_km_h = self.coordinator.data.current_weather_data.get( + return self.coordinator.data.current_weather_data.get( ATTR_MAP[ATTR_WEATHER_WIND_SPEED] ) - if self._is_metric or speed_km_h is None: - return speed_km_h - - speed_mi_h = convert_speed( - speed_km_h, SPEED_KILOMETERS_PER_HOUR, SPEED_MILES_PER_HOUR - ) - return int(round(speed_mi_h)) @property def wind_bearing(self) -> float | str | None: @@ -206,7 +181,7 @@ class MetWeather(CoordinatorEntity[MetDataUpdateCoordinator], WeatherEntity): met_forecast = self.coordinator.data.hourly_forecast else: met_forecast = self.coordinator.data.daily_forecast - required_keys = {ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME} + required_keys = {"temperature", ATTR_FORECAST_TIME} ha_forecast: list[Forecast] = [] for met_item in met_forecast: if not set(met_item).issuperset(required_keys): @@ -216,14 +191,6 @@ class MetWeather(CoordinatorEntity[MetDataUpdateCoordinator], WeatherEntity): for k, v in FORECAST_MAP.items() if met_item.get(v) is not None } - if not self._is_metric and ATTR_FORECAST_PRECIPITATION in ha_item: - if ha_item[ATTR_FORECAST_PRECIPITATION] is not None: - precip_inches = convert_distance( - ha_item[ATTR_FORECAST_PRECIPITATION], - LENGTH_MILLIMETERS, - LENGTH_INCHES, - ) - ha_item[ATTR_FORECAST_PRECIPITATION] = round(precip_inches, 2) if ha_item.get(ATTR_FORECAST_CONDITION): ha_item[ATTR_FORECAST_CONDITION] = format_condition( ha_item[ATTR_FORECAST_CONDITION] From 6f63cd731bc0a761f4257e60a0dd1e0400c9eda0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Jun 2022 12:05:29 -0500 Subject: [PATCH 19/71] Add debug logging to esphome state updates (#74260) --- homeassistant/components/esphome/entry_data.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index 7980d1a6a17..d4bcc67db4a 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio from collections.abc import Callable from dataclasses import dataclass, field +import logging from typing import Any, cast from aioesphomeapi import ( @@ -36,6 +37,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.storage import Store SAVE_DELAY = 120 +_LOGGER = logging.getLogger(__name__) # Mapping from ESPHome info type to HA platform INFO_TYPE_TO_PLATFORM: dict[type[EntityInfo], str] = { @@ -128,6 +130,12 @@ class RuntimeEntryData: component_key = self.key_to_component[state.key] self.state[component_key][state.key] = state signal = f"esphome_{self.entry_id}_update_{component_key}_{state.key}" + _LOGGER.debug( + "Dispatching update for component %s with state key %s: %s", + component_key, + state.key, + state, + ) async_dispatcher_send(hass, signal) @callback From 3db1552f26c1e7d7ebfd66be02e14c69546d412f Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Thu, 30 Jun 2022 12:10:05 -0500 Subject: [PATCH 20/71] Fix Life360 unload (#74263) * Fix life360 async_unload_entry * Update tracked_members when unloading config entry --- homeassistant/components/life360/__init__.py | 14 ++++++++++---- homeassistant/components/life360/device_tracker.py | 10 +++++----- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/life360/__init__.py b/homeassistant/components/life360/__init__.py index 66c9416a1c1..4527f6ac298 100644 --- a/homeassistant/components/life360/__init__.py +++ b/homeassistant/components/life360/__init__.py @@ -124,11 +124,11 @@ class IntegData: """Integration data.""" cfg_options: dict[str, Any] | None = None - # ConfigEntry.unique_id: Life360DataUpdateCoordinator + # ConfigEntry.entry_id: Life360DataUpdateCoordinator coordinators: dict[str, Life360DataUpdateCoordinator] = field( init=False, default_factory=dict ) - # member_id: ConfigEntry.unique_id + # member_id: ConfigEntry.entry_id tracked_members: dict[str, str] = field(init=False, default_factory=dict) logged_circles: list[str] = field(init=False, default_factory=list) logged_places: list[str] = field(init=False, default_factory=list) @@ -171,7 +171,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload config entry.""" - del hass.data[DOMAIN].coordinators[entry.entry_id] # Unload components for our platforms. - return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + del hass.data[DOMAIN].coordinators[entry.entry_id] + # Remove any members that were tracked by this entry. + for member_id, entry_id in hass.data[DOMAIN].tracked_members.copy().items(): + if entry_id == entry.entry_id: + del hass.data[DOMAIN].tracked_members[member_id] + + return unload_ok diff --git a/homeassistant/components/life360/device_tracker.py b/homeassistant/components/life360/device_tracker.py index a38181a6830..5a18422487e 100644 --- a/homeassistant/components/life360/device_tracker.py +++ b/homeassistant/components/life360/device_tracker.py @@ -78,13 +78,13 @@ async def async_setup_entry( new_entities = [] for member_id, member in coordinator.data.members.items(): - tracked_by_account = tracked_members.get(member_id) - if new_member := not tracked_by_account: - tracked_members[member_id] = entry.unique_id - LOGGER.debug("Member: %s", member.name) + tracked_by_entry = tracked_members.get(member_id) + if new_member := not tracked_by_entry: + tracked_members[member_id] = entry.entry_id + LOGGER.debug("Member: %s (%s)", member.name, entry.unique_id) if ( new_member - or tracked_by_account == entry.unique_id + or tracked_by_entry == entry.entry_id and not new_members_only ): new_entities.append(Life360DeviceTracker(coordinator, member_id)) From 4755f0054936cbb948e2e2065c56c6f4308c69fd Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Thu, 30 Jun 2022 13:01:23 -0500 Subject: [PATCH 21/71] Bump frontend to 20220630.0 (#74266) --- 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 23f056ba0da..27ff0a73f20 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220629.0"], + "requirements": ["home-assistant-frontend==20220630.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 6b7b689b93d..7ee5a9fe8d3 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220629.0 +home-assistant-frontend==20220630.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 2ebabd89a5a..84fb3e6894d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -828,7 +828,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220629.0 +home-assistant-frontend==20220630.0 # homeassistant.components.home_connect homeconnect==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a19338e5715..b15b9446de3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -595,7 +595,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220629.0 +home-assistant-frontend==20220630.0 # homeassistant.components.home_connect homeconnect==0.7.1 From 762fe17f48cd01a324192696f4ab2127aa15d30f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 30 Jun 2022 11:02:38 -0700 Subject: [PATCH 22/71] Bumped version to 2022.7.0b1 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index df160e1cc6b..60a3d6817b5 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 7 -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, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 69c5a539f3f..2b1e14a40f6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.7.0b0" +version = "2022.7.0b1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 135d104430f7dc4c30ef420120b80c2a7b366a57 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 30 Jun 2022 23:48:50 +0200 Subject: [PATCH 23/71] Bump pyRFXtrx to 0.30.0 (#74146) --- homeassistant/components/rfxtrx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/rfxtrx/test_config_flow.py | 12 ++++-- tests/components/rfxtrx/test_cover.py | 37 ++++++++++--------- tests/components/rfxtrx/test_switch.py | 4 +- 6 files changed, 34 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/rfxtrx/manifest.json b/homeassistant/components/rfxtrx/manifest.json index cfe1049c888..3439fbba70c 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.29.0"], + "requirements": ["pyRFXtrx==0.30.0"], "codeowners": ["@danielhiversen", "@elupus", "@RobBie1221"], "config_flow": true, "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 84fb3e6894d..ca304ff6764 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1336,7 +1336,7 @@ pyMetEireann==2021.8.0 pyMetno==0.9.0 # homeassistant.components.rfxtrx -pyRFXtrx==0.29.0 +pyRFXtrx==0.30.0 # homeassistant.components.switchmate # pySwitchmate==0.4.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b15b9446de3..cd67a1a72d7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -914,7 +914,7 @@ pyMetEireann==2021.8.0 pyMetno==0.9.0 # homeassistant.components.rfxtrx -pyRFXtrx==0.29.0 +pyRFXtrx==0.30.0 # homeassistant.components.tibber pyTibber==0.22.3 diff --git a/tests/components/rfxtrx/test_config_flow.py b/tests/components/rfxtrx/test_config_flow.py index a756bf26b9f..2c695d71d2e 100644 --- a/tests/components/rfxtrx/test_config_flow.py +++ b/tests/components/rfxtrx/test_config_flow.py @@ -847,7 +847,7 @@ async def test_options_configure_rfy_cover_device(hass): result["flow_id"], user_input={ "automatic_add": True, - "event_code": "071a000001020301", + "event_code": "0C1a0000010203010000000000", }, ) @@ -863,7 +863,10 @@ async def test_options_configure_rfy_cover_device(hass): await hass.async_block_till_done() - assert entry.data["devices"]["071a000001020301"]["venetian_blind_mode"] == "EU" + assert ( + entry.data["devices"]["0C1a0000010203010000000000"]["venetian_blind_mode"] + == "EU" + ) device_registry = dr.async_get(hass) device_entries = dr.async_entries_for_config_entry(device_registry, entry.entry_id) @@ -897,7 +900,10 @@ async def test_options_configure_rfy_cover_device(hass): await hass.async_block_till_done() - assert entry.data["devices"]["071a000001020301"]["venetian_blind_mode"] == "EU" + assert ( + entry.data["devices"]["0C1a0000010203010000000000"]["venetian_blind_mode"] + == "EU" + ) def test_get_serial_by_id_no_dir(): diff --git a/tests/components/rfxtrx/test_cover.py b/tests/components/rfxtrx/test_cover.py index e3d44edda82..3be41d9233e 100644 --- a/tests/components/rfxtrx/test_cover.py +++ b/tests/components/rfxtrx/test_cover.py @@ -146,8 +146,11 @@ async def test_rfy_cover(hass, rfxtrx): "071a000001020301": { "venetian_blind_mode": "Unknown", }, - "071a000001020302": {"venetian_blind_mode": "US"}, - "071a000001020303": {"venetian_blind_mode": "EU"}, + "0c1a0000010203010000000000": { + "venetian_blind_mode": "Unknown", + }, + "0c1a0000010203020000000000": {"venetian_blind_mode": "US"}, + "0c1a0000010203030000000000": {"venetian_blind_mode": "EU"}, } ) mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) @@ -199,9 +202,9 @@ async def test_rfy_cover(hass, rfxtrx): ) assert rfxtrx.transport.send.mock_calls == [ - call(bytearray(b"\x08\x1a\x00\x00\x01\x02\x03\x01\x00")), - call(bytearray(b"\x08\x1a\x00\x01\x01\x02\x03\x01\x01")), - call(bytearray(b"\x08\x1a\x00\x02\x01\x02\x03\x01\x03")), + call(bytearray(b"\x0C\x1a\x00\x00\x01\x02\x03\x01\x00\x00\x00\x00\x00")), + call(bytearray(b"\x0C\x1a\x00\x01\x01\x02\x03\x01\x01\x00\x00\x00\x00")), + call(bytearray(b"\x0C\x1a\x00\x02\x01\x02\x03\x01\x03\x00\x00\x00\x00")), ] # Test a blind with venetian mode set to US @@ -252,12 +255,12 @@ async def test_rfy_cover(hass, rfxtrx): ) assert rfxtrx.transport.send.mock_calls == [ - call(bytearray(b"\x08\x1a\x00\x00\x01\x02\x03\x02\x00")), - call(bytearray(b"\x08\x1a\x00\x01\x01\x02\x03\x02\x0F")), - call(bytearray(b"\x08\x1a\x00\x02\x01\x02\x03\x02\x10")), - call(bytearray(b"\x08\x1a\x00\x03\x01\x02\x03\x02\x11")), - call(bytearray(b"\x08\x1a\x00\x04\x01\x02\x03\x02\x12")), - call(bytearray(b"\x08\x1a\x00\x00\x01\x02\x03\x02\x00")), + call(bytearray(b"\x0C\x1a\x00\x00\x01\x02\x03\x02\x00\x00\x00\x00\x00")), + call(bytearray(b"\x0C\x1a\x00\x01\x01\x02\x03\x02\x0F\x00\x00\x00\x00")), + call(bytearray(b"\x0C\x1a\x00\x02\x01\x02\x03\x02\x10\x00\x00\x00\x00")), + call(bytearray(b"\x0C\x1a\x00\x03\x01\x02\x03\x02\x11\x00\x00\x00\x00")), + call(bytearray(b"\x0C\x1a\x00\x04\x01\x02\x03\x02\x12\x00\x00\x00\x00")), + call(bytearray(b"\x0C\x1a\x00\x00\x01\x02\x03\x02\x00\x00\x00\x00\x00")), ] # Test a blind with venetian mode set to EU @@ -308,10 +311,10 @@ async def test_rfy_cover(hass, rfxtrx): ) assert rfxtrx.transport.send.mock_calls == [ - call(bytearray(b"\x08\x1a\x00\x00\x01\x02\x03\x03\x00")), - call(bytearray(b"\x08\x1a\x00\x01\x01\x02\x03\x03\x11")), - call(bytearray(b"\x08\x1a\x00\x02\x01\x02\x03\x03\x12")), - call(bytearray(b"\x08\x1a\x00\x03\x01\x02\x03\x03\x0F")), - call(bytearray(b"\x08\x1a\x00\x04\x01\x02\x03\x03\x10")), - call(bytearray(b"\x08\x1a\x00\x00\x01\x02\x03\x03\x00")), + call(bytearray(b"\x0C\x1a\x00\x00\x01\x02\x03\x03\x00\x00\x00\x00\x00")), + call(bytearray(b"\x0C\x1a\x00\x01\x01\x02\x03\x03\x11\x00\x00\x00\x00")), + call(bytearray(b"\x0C\x1a\x00\x02\x01\x02\x03\x03\x12\x00\x00\x00\x00")), + call(bytearray(b"\x0C\x1a\x00\x03\x01\x02\x03\x03\x0F\x00\x00\x00\x00")), + call(bytearray(b"\x0C\x1a\x00\x04\x01\x02\x03\x03\x10\x00\x00\x00\x00")), + call(bytearray(b"\x0C\x1a\x00\x00\x01\x02\x03\x03\x00\x00\x00\x00\x00")), ] diff --git a/tests/components/rfxtrx/test_switch.py b/tests/components/rfxtrx/test_switch.py index 4da7f1d9881..4d92c6fa332 100644 --- a/tests/components/rfxtrx/test_switch.py +++ b/tests/components/rfxtrx/test_switch.py @@ -11,8 +11,8 @@ from homeassistant.core import State from tests.common import MockConfigEntry, mock_restore_cache from tests.components.rfxtrx.conftest import create_rfx_test_cfg -EVENT_RFY_ENABLE_SUN_AUTO = "081a00000301010113" -EVENT_RFY_DISABLE_SUN_AUTO = "081a00000301010114" +EVENT_RFY_ENABLE_SUN_AUTO = "0C1a0000030101011300000003" +EVENT_RFY_DISABLE_SUN_AUTO = "0C1a0000030101011400000003" async def test_one_switch(hass, rfxtrx): From 1351b83731aa8b19231b0f053f60b2d408147d6e Mon Sep 17 00:00:00 2001 From: Christopher Hoage Date: Thu, 30 Jun 2022 13:06:22 -0700 Subject: [PATCH 24/71] Bump venstarcolortouch to 0.17 (#74271) --- homeassistant/components/venstar/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/venstar/manifest.json b/homeassistant/components/venstar/manifest.json index e63c75792bf..2f3331af6e2 100644 --- a/homeassistant/components/venstar/manifest.json +++ b/homeassistant/components/venstar/manifest.json @@ -3,7 +3,7 @@ "name": "Venstar", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/venstar", - "requirements": ["venstarcolortouch==0.16"], + "requirements": ["venstarcolortouch==0.17"], "codeowners": ["@garbled1"], "iot_class": "local_polling", "loggers": ["venstarcolortouch"] diff --git a/requirements_all.txt b/requirements_all.txt index ca304ff6764..84501dbce79 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2387,7 +2387,7 @@ vehicle==0.4.0 velbus-aio==2022.6.2 # homeassistant.components.venstar -venstarcolortouch==0.16 +venstarcolortouch==0.17 # homeassistant.components.vilfo vilfo-api-client==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cd67a1a72d7..de9ca72060d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1590,7 +1590,7 @@ vehicle==0.4.0 velbus-aio==2022.6.2 # homeassistant.components.venstar -venstarcolortouch==0.16 +venstarcolortouch==0.17 # homeassistant.components.vilfo vilfo-api-client==0.3.2 From b61530742ef2a887a979b14d6aaf0e1094d0b72a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 1 Jul 2022 00:19:40 -0500 Subject: [PATCH 25/71] Fix key collision between platforms in esphome state updates (#74273) --- homeassistant/components/esphome/__init__.py | 19 +--- .../components/esphome/entry_data.py | 92 ++++++++++++++----- 2 files changed, 73 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 2e88a883dc1..0c1eac3aa45 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -150,11 +150,6 @@ async def async_setup_entry( # noqa: C901 hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, on_stop) ) - @callback - def async_on_state(state: EntityState) -> None: - """Send dispatcher updates when a new state is received.""" - entry_data.async_update_state(hass, state) - @callback def async_on_service_call(service: HomeassistantServiceCall) -> None: """Call service when user automation in ESPHome config is triggered.""" @@ -288,7 +283,7 @@ async def async_setup_entry( # noqa: C901 entity_infos, services = await cli.list_entities_services() await entry_data.async_update_static_infos(hass, entry, entity_infos) await _setup_services(hass, entry_data, services) - await cli.subscribe_states(async_on_state) + await cli.subscribe_states(entry_data.async_update_state) await cli.subscribe_service_calls(async_on_service_call) await cli.subscribe_home_assistant_states(async_on_state_subscription) @@ -568,7 +563,6 @@ async def platform_async_setup_entry( @callback def async_list_entities(infos: list[EntityInfo]) -> None: """Update entities of this platform when entities are listed.""" - key_to_component = entry_data.key_to_component old_infos = entry_data.info[component_key] new_infos: dict[int, EntityInfo] = {} add_entities = [] @@ -587,12 +581,10 @@ async def platform_async_setup_entry( entity = entity_type(entry_data, component_key, info.key) add_entities.append(entity) new_infos[info.key] = info - key_to_component[info.key] = component_key # Remove old entities for info in old_infos.values(): entry_data.async_remove_entity(hass, component_key, info.key) - key_to_component.pop(info.key, None) # First copy the now-old info into the backup object entry_data.old_info[component_key] = entry_data.info[component_key] @@ -714,13 +706,8 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]): ) self.async_on_remove( - async_dispatcher_connect( - self.hass, - ( - f"esphome_{self._entry_id}" - f"_update_{self._component_key}_{self._key}" - ), - self._on_state_update, + self._entry_data.async_subscribe_state_update( + self._component_key, self._key, self._on_state_update ) ) diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index d4bcc67db4a..8eb56e6fdb6 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -12,26 +12,40 @@ from aioesphomeapi import ( APIClient, APIVersion, BinarySensorInfo, + BinarySensorState, CameraInfo, + CameraState, ClimateInfo, + ClimateState, CoverInfo, + CoverState, DeviceInfo, EntityInfo, EntityState, FanInfo, + FanState, LightInfo, + LightState, LockInfo, + LockState, MediaPlayerInfo, + MediaPlayerState, NumberInfo, + NumberState, SelectInfo, + SelectState, SensorInfo, + SensorState, SwitchInfo, + SwitchState, TextSensorInfo, + TextSensorState, UserService, ) from aioesphomeapi.model import ButtonInfo from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.storage import Store @@ -41,20 +55,37 @@ _LOGGER = logging.getLogger(__name__) # Mapping from ESPHome info type to HA platform INFO_TYPE_TO_PLATFORM: dict[type[EntityInfo], str] = { - BinarySensorInfo: "binary_sensor", - ButtonInfo: "button", - CameraInfo: "camera", - ClimateInfo: "climate", - CoverInfo: "cover", - FanInfo: "fan", - LightInfo: "light", - LockInfo: "lock", - MediaPlayerInfo: "media_player", - NumberInfo: "number", - SelectInfo: "select", - SensorInfo: "sensor", - SwitchInfo: "switch", - TextSensorInfo: "sensor", + BinarySensorInfo: Platform.BINARY_SENSOR, + ButtonInfo: Platform.BINARY_SENSOR, + CameraInfo: Platform.BINARY_SENSOR, + ClimateInfo: Platform.CLIMATE, + CoverInfo: Platform.COVER, + FanInfo: Platform.FAN, + LightInfo: Platform.LIGHT, + LockInfo: Platform.LOCK, + MediaPlayerInfo: Platform.MEDIA_PLAYER, + NumberInfo: Platform.NUMBER, + SelectInfo: Platform.SELECT, + SensorInfo: Platform.SENSOR, + SwitchInfo: Platform.SWITCH, + TextSensorInfo: Platform.SENSOR, +} + +STATE_TYPE_TO_COMPONENT_KEY = { + BinarySensorState: Platform.BINARY_SENSOR, + EntityState: Platform.BINARY_SENSOR, + CameraState: Platform.BINARY_SENSOR, + ClimateState: Platform.CLIMATE, + CoverState: Platform.COVER, + FanState: Platform.FAN, + LightState: Platform.LIGHT, + LockState: Platform.LOCK, + MediaPlayerState: Platform.MEDIA_PLAYER, + NumberState: Platform.NUMBER, + SelectState: Platform.SELECT, + SensorState: Platform.SENSOR, + SwitchState: Platform.SWITCH, + TextSensorState: Platform.SENSOR, } @@ -67,7 +98,6 @@ class RuntimeEntryData: store: Store state: dict[str, dict[int, EntityState]] = field(default_factory=dict) info: dict[str, dict[int, EntityInfo]] = field(default_factory=dict) - key_to_component: dict[int, str] = field(default_factory=dict) # A second list of EntityInfo objects # This is necessary for when an entity is being removed. HA requires @@ -81,6 +111,9 @@ class RuntimeEntryData: api_version: APIVersion = field(default_factory=APIVersion) cleanup_callbacks: list[Callable[[], None]] = field(default_factory=list) disconnect_callbacks: list[Callable[[], None]] = field(default_factory=list) + state_subscriptions: dict[tuple[str, int], Callable[[], None]] = field( + default_factory=dict + ) loaded_platforms: set[str] = field(default_factory=set) platform_load_lock: asyncio.Lock = field(default_factory=asyncio.Lock) _storage_contents: dict[str, Any] | None = None @@ -125,18 +158,33 @@ class RuntimeEntryData: async_dispatcher_send(hass, signal, infos) @callback - def async_update_state(self, hass: HomeAssistant, state: EntityState) -> None: + def async_subscribe_state_update( + self, + component_key: str, + state_key: int, + entity_callback: Callable[[], None], + ) -> Callable[[], None]: + """Subscribe to state updates.""" + + def _unsubscribe() -> None: + self.state_subscriptions.pop((component_key, state_key)) + + self.state_subscriptions[(component_key, state_key)] = entity_callback + return _unsubscribe + + @callback + def async_update_state(self, state: EntityState) -> None: """Distribute an update of state information to the target.""" - component_key = self.key_to_component[state.key] + component_key = STATE_TYPE_TO_COMPONENT_KEY[type(state)] + subscription_key = (component_key, state.key) self.state[component_key][state.key] = state - signal = f"esphome_{self.entry_id}_update_{component_key}_{state.key}" _LOGGER.debug( - "Dispatching update for component %s with state key %s: %s", - component_key, - state.key, + "Dispatching update with key %s: %s", + subscription_key, state, ) - async_dispatcher_send(hass, signal) + if subscription_key in self.state_subscriptions: + self.state_subscriptions[subscription_key]() @callback def async_update_device_state(self, hass: HomeAssistant) -> None: From 78e5296d07aa7f9428e95a3b372e86d07c784467 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Thu, 30 Jun 2022 16:59:35 -0400 Subject: [PATCH 26/71] Fix bad conditional in ZHA logbook (#74277) * Fix bad conditional in ZHA logbook * change syntax --- homeassistant/components/zha/logbook.py | 4 ++-- tests/components/zha/test_logbook.py | 29 +++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/logbook.py b/homeassistant/components/zha/logbook.py index 8140a5244f1..90d433be210 100644 --- a/homeassistant/components/zha/logbook.py +++ b/homeassistant/components/zha/logbook.py @@ -74,8 +74,8 @@ def async_describe_events( else: message = f"{event_type} event was fired" - if event_data["params"]: - message = f"{message} with parameters: {event_data['params']}" + if params := event_data.get("params"): + message = f"{message} with parameters: {params}" return { LOGBOOK_ENTRY_NAME: device_name, diff --git a/tests/components/zha/test_logbook.py b/tests/components/zha/test_logbook.py index 6c28284b1e6..373a48c2d47 100644 --- a/tests/components/zha/test_logbook.py +++ b/tests/components/zha/test_logbook.py @@ -185,6 +185,27 @@ async def test_zha_logbook_event_device_no_triggers(hass, mock_devices): }, }, ), + MockRow( + ZHA_EVENT, + { + CONF_DEVICE_ID: reg_device.id, + "device_ieee": str(ieee_address), + CONF_UNIQUE_ID: f"{str(ieee_address)}:1:0x0006", + "endpoint_id": 1, + "cluster_id": 6, + "params": {}, + }, + ), + MockRow( + ZHA_EVENT, + { + CONF_DEVICE_ID: reg_device.id, + "device_ieee": str(ieee_address), + CONF_UNIQUE_ID: f"{str(ieee_address)}:1:0x0006", + "endpoint_id": 1, + "cluster_id": 6, + }, + ), ], ) @@ -201,6 +222,14 @@ async def test_zha_logbook_event_device_no_triggers(hass, mock_devices): events[1]["message"] == "Zha Event was fired with parameters: {'test': 'test'}" ) + assert events[2]["name"] == "FakeManufacturer FakeModel" + assert events[2]["domain"] == "zha" + assert events[2]["message"] == "Zha Event was fired" + + assert events[3]["name"] == "FakeManufacturer FakeModel" + assert events[3]["domain"] == "zha" + assert events[3]["message"] == "Zha Event was fired" + async def test_zha_logbook_event_device_no_device(hass, mock_devices): """Test zha logbook events without device and without triggers.""" From 4817f9f905d9474b1b9810032ef7bd7c35482afe Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 1 Jul 2022 00:40:05 -0700 Subject: [PATCH 27/71] Add scan interval to scrape sensor (#74285) --- homeassistant/components/scrape/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index a73dbc17c1c..b6b8828ca73 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -1,6 +1,7 @@ """Support for getting data from websites with scraping.""" from __future__ import annotations +from datetime import timedelta import logging from typing import Any @@ -44,6 +45,7 @@ from .const import CONF_INDEX, CONF_SELECT, DEFAULT_NAME, DEFAULT_VERIFY_SSL, DO _LOGGER = logging.getLogger(__name__) +SCAN_INTERVAL = timedelta(minutes=10) ICON = "mdi:web" PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( From 877803169bc2d0c50b0eb2d5d75a1b7201f7df63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Fri, 1 Jul 2022 11:52:46 +0200 Subject: [PATCH 28/71] Fix QNAP QSW DHCP discover bugs (#74291) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit qnqp_qsw: fix DHCP discover bugs Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/qnap_qsw/config_flow.py | 3 ++- homeassistant/components/qnap_qsw/strings.json | 6 ++++++ homeassistant/components/qnap_qsw/translations/en.json | 6 ++++++ tests/components/qnap_qsw/test_config_flow.py | 5 +++-- 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/qnap_qsw/config_flow.py b/homeassistant/components/qnap_qsw/config_flow.py index e9d11433021..bb42c9ea294 100644 --- a/homeassistant/components/qnap_qsw/config_flow.py +++ b/homeassistant/components/qnap_qsw/config_flow.py @@ -113,9 +113,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except LoginError: errors[CONF_PASSWORD] = "invalid_auth" except QswError: - errors[CONF_URL] = "cannot_connect" + errors["base"] = "cannot_connect" else: title = f"QNAP {system_board.get_product()} {self._discovered_mac}" + user_input[CONF_URL] = self._discovered_url return self.async_create_entry(title=title, data=user_input) return self.async_show_form( diff --git a/homeassistant/components/qnap_qsw/strings.json b/homeassistant/components/qnap_qsw/strings.json index 351245a9591..ba0cb28ba77 100644 --- a/homeassistant/components/qnap_qsw/strings.json +++ b/homeassistant/components/qnap_qsw/strings.json @@ -9,6 +9,12 @@ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" }, "step": { + "discovered_connection": { + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + }, "user": { "data": { "url": "[%key:common::config_flow::data::url%]", diff --git a/homeassistant/components/qnap_qsw/translations/en.json b/homeassistant/components/qnap_qsw/translations/en.json index b6f68f2f062..c75c2d76ac8 100644 --- a/homeassistant/components/qnap_qsw/translations/en.json +++ b/homeassistant/components/qnap_qsw/translations/en.json @@ -9,6 +9,12 @@ "invalid_auth": "Invalid authentication" }, "step": { + "discovered_connection": { + "data": { + "password": "Password", + "username": "Username" + } + }, "user": { "data": { "password": "Password", diff --git a/tests/components/qnap_qsw/test_config_flow.py b/tests/components/qnap_qsw/test_config_flow.py index 0b7072dd602..02f873c6a4a 100644 --- a/tests/components/qnap_qsw/test_config_flow.py +++ b/tests/components/qnap_qsw/test_config_flow.py @@ -24,7 +24,7 @@ DHCP_SERVICE_INFO = dhcp.DhcpServiceInfo( ) TEST_PASSWORD = "test-password" -TEST_URL = "test-url" +TEST_URL = f"http://{DHCP_SERVICE_INFO.ip}" TEST_USERNAME = "test-username" @@ -187,6 +187,7 @@ async def test_dhcp_flow(hass: HomeAssistant) -> None: assert result2["data"] == { CONF_USERNAME: TEST_USERNAME, CONF_PASSWORD: TEST_PASSWORD, + CONF_URL: TEST_URL, } assert len(mock_setup_entry.mock_calls) == 1 @@ -237,7 +238,7 @@ async def test_dhcp_connection_error(hass: HomeAssistant): }, ) - assert result["errors"] == {CONF_URL: "cannot_connect"} + assert result["errors"] == {"base": "cannot_connect"} async def test_dhcp_login_error(hass: HomeAssistant): From 2305c625fb4c87b75e2389f6316649498e392cd3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 1 Jul 2022 12:10:40 -0500 Subject: [PATCH 29/71] Revert scrape changes to 2022.6.6 (#74305) --- CODEOWNERS | 4 +- homeassistant/components/scrape/__init__.py | 63 ------ homeassistant/components/scrape/manifest.json | 3 +- homeassistant/components/scrape/sensor.py | 104 ++++----- homeassistant/generated/config_flows.py | 1 - tests/components/scrape/__init__.py | 40 +--- tests/components/scrape/test_config_flow.py | 194 ---------------- tests/components/scrape/test_init.py | 89 -------- tests/components/scrape/test_sensor.py | 209 ++++++++---------- 9 files changed, 137 insertions(+), 570 deletions(-) delete mode 100644 tests/components/scrape/test_config_flow.py delete mode 100644 tests/components/scrape/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index 1b6d88b5464..5f5588c0b91 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -891,8 +891,8 @@ build.json @home-assistant/supervisor /homeassistant/components/scene/ @home-assistant/core /tests/components/scene/ @home-assistant/core /homeassistant/components/schluter/ @prairieapps -/homeassistant/components/scrape/ @fabaff @gjohansson-ST -/tests/components/scrape/ @fabaff @gjohansson-ST +/homeassistant/components/scrape/ @fabaff +/tests/components/scrape/ @fabaff /homeassistant/components/screenlogic/ @dieselrabbit @bdraco /tests/components/screenlogic/ @dieselrabbit @bdraco /homeassistant/components/script/ @home-assistant/core diff --git a/homeassistant/components/scrape/__init__.py b/homeassistant/components/scrape/__init__.py index 684be76b80d..f9222c126b5 100644 --- a/homeassistant/components/scrape/__init__.py +++ b/homeassistant/components/scrape/__init__.py @@ -1,64 +1 @@ """The scrape component.""" -from __future__ import annotations - -import httpx - -from homeassistant.components.rest.data import RestData -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_AUTHENTICATION, - CONF_HEADERS, - CONF_PASSWORD, - CONF_RESOURCE, - CONF_USERNAME, - CONF_VERIFY_SSL, - HTTP_DIGEST_AUTHENTICATION, -) -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady - -from .const import DOMAIN, PLATFORMS - - -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up Scrape from a config entry.""" - - resource: str = entry.options[CONF_RESOURCE] - method: str = "GET" - payload: str | None = None - headers: str | None = entry.options.get(CONF_HEADERS) - verify_ssl: bool = entry.options[CONF_VERIFY_SSL] - username: str | None = entry.options.get(CONF_USERNAME) - password: str | None = entry.options.get(CONF_PASSWORD) - - auth: httpx.DigestAuth | tuple[str, str] | None = None - if username and password: - if entry.options.get(CONF_AUTHENTICATION) == HTTP_DIGEST_AUTHENTICATION: - auth = httpx.DigestAuth(username, password) - else: - auth = (username, password) - - rest = RestData(hass, method, resource, auth, headers, None, payload, verify_ssl) - await rest.async_update() - - if rest.data is None: - raise ConfigEntryNotReady - - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = rest - - entry.async_on_unload(entry.add_update_listener(async_update_listener)) - - hass.config_entries.async_setup_platforms(entry, PLATFORMS) - - return True - - -async def async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Update listener for options.""" - await hass.config_entries.async_reload(entry.entry_id) - - -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Unload Scrape config entry.""" - - return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/scrape/manifest.json b/homeassistant/components/scrape/manifest.json index 631af2e6051..b1ccbb354a9 100644 --- a/homeassistant/components/scrape/manifest.json +++ b/homeassistant/components/scrape/manifest.json @@ -4,7 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/scrape", "requirements": ["beautifulsoup4==4.11.1", "lxml==4.8.0"], "after_dependencies": ["rest"], - "codeowners": ["@fabaff", "@gjohansson-ST"], - "config_flow": true, + "codeowners": ["@fabaff"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index b6b8828ca73..e15f7c5ba97 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -1,11 +1,11 @@ """Support for getting data from websites with scraping.""" from __future__ import annotations -from datetime import timedelta import logging from typing import Any from bs4 import BeautifulSoup +import httpx import voluptuous as vol from homeassistant.components.rest.data import RestData @@ -16,16 +16,13 @@ from homeassistant.components.sensor import ( STATE_CLASSES_SCHEMA, SensorEntity, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( - CONF_ATTRIBUTE, CONF_AUTHENTICATION, CONF_DEVICE_CLASS, CONF_HEADERS, CONF_NAME, CONF_PASSWORD, CONF_RESOURCE, - CONF_SCAN_INTERVAL, CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME, CONF_VALUE_TEMPLATE, @@ -34,25 +31,26 @@ from homeassistant.const import ( HTTP_DIGEST_AUTHENTICATION, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .const import CONF_INDEX, CONF_SELECT, DEFAULT_NAME, DEFAULT_VERIFY_SSL, DOMAIN - _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(minutes=10) -ICON = "mdi:web" +CONF_ATTR = "attribute" +CONF_SELECT = "select" +CONF_INDEX = "index" + +DEFAULT_NAME = "Web scrape" +DEFAULT_VERIFY_SSL = True PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_RESOURCE): cv.string, vol.Required(CONF_SELECT): cv.string, - vol.Optional(CONF_ATTRIBUTE): cv.string, + vol.Optional(CONF_ATTR): cv.string, vol.Optional(CONF_INDEX, default=0): cv.positive_int, vol.Optional(CONF_AUTHENTICATION): vol.In( [HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION] @@ -64,7 +62,7 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_STATE_CLASS): STATE_CLASSES_SCHEMA, vol.Optional(CONF_USERNAME): cv.string, - vol.Optional(CONF_VALUE_TEMPLATE): cv.string, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, } ) @@ -77,47 +75,37 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Web scrape sensor.""" - _LOGGER.warning( - # Config flow added in Home Assistant Core 2022.7, remove import flow in 2022.9 - "Loading Scrape via platform setup has been deprecated in Home Assistant 2022.7 " - "Your configuration has been automatically imported and you can " - "remove it from your configuration.yaml" - ) + name: str = config[CONF_NAME] + resource: str = config[CONF_RESOURCE] + method: str = "GET" + payload: str | None = None + headers: str | None = config.get(CONF_HEADERS) + verify_ssl: bool = config[CONF_VERIFY_SSL] + select: str | None = config.get(CONF_SELECT) + attr: str | None = config.get(CONF_ATTR) + index: int = config[CONF_INDEX] + unit: str | None = config.get(CONF_UNIT_OF_MEASUREMENT) + device_class: str | None = config.get(CONF_DEVICE_CLASS) + state_class: str | None = config.get(CONF_STATE_CLASS) + username: str | None = config.get(CONF_USERNAME) + password: str | None = config.get(CONF_PASSWORD) + value_template: Template | None = config.get(CONF_VALUE_TEMPLATE) - if config.get(CONF_VALUE_TEMPLATE): - template: Template = Template(config[CONF_VALUE_TEMPLATE]) - template.ensure_valid() - config[CONF_VALUE_TEMPLATE] = template.template - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={k: v for k, v in config.items() if k != CONF_SCAN_INTERVAL}, - ) - ) - - -async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback -) -> None: - """Set up the Scrape sensor entry.""" - name: str = entry.options[CONF_NAME] - resource: str = entry.options[CONF_RESOURCE] - select: str | None = entry.options.get(CONF_SELECT) - attr: str | None = entry.options.get(CONF_ATTRIBUTE) - index: int = int(entry.options[CONF_INDEX]) - unit: str | None = entry.options.get(CONF_UNIT_OF_MEASUREMENT) - device_class: str | None = entry.options.get(CONF_DEVICE_CLASS) - state_class: str | None = entry.options.get(CONF_STATE_CLASS) - value_template: str | None = entry.options.get(CONF_VALUE_TEMPLATE) - entry_id: str = entry.entry_id - - val_template: Template | None = None if value_template is not None: - val_template = Template(value_template, hass) + value_template.hass = hass - rest = hass.data[DOMAIN][entry.entry_id] + auth: httpx.DigestAuth | tuple[str, str] | None = None + if username and password: + if config.get(CONF_AUTHENTICATION) == HTTP_DIGEST_AUTHENTICATION: + auth = httpx.DigestAuth(username, password) + else: + auth = (username, password) + + rest = RestData(hass, method, resource, auth, headers, None, payload, verify_ssl) + await rest.async_update() + + if rest.data is None: + raise PlatformNotReady async_add_entities( [ @@ -127,12 +115,10 @@ async def async_setup_entry( select, attr, index, - val_template, + value_template, unit, device_class, state_class, - entry_id, - resource, ) ], True, @@ -142,8 +128,6 @@ async def async_setup_entry( class ScrapeSensor(SensorEntity): """Representation of a web scrape sensor.""" - _attr_icon = ICON - def __init__( self, rest: RestData, @@ -155,8 +139,6 @@ class ScrapeSensor(SensorEntity): unit: str | None, device_class: str | None, state_class: str | None, - entry_id: str, - resource: str, ) -> None: """Initialize a web scrape sensor.""" self.rest = rest @@ -169,14 +151,6 @@ class ScrapeSensor(SensorEntity): self._attr_native_unit_of_measurement = unit self._attr_device_class = device_class self._attr_state_class = state_class - self._attr_unique_id = entry_id - self._attr_device_info = DeviceInfo( - entry_type=DeviceEntryType.SERVICE, - identifiers={(DOMAIN, entry_id)}, - manufacturer="Scrape", - name=name, - configuration_url=resource, - ) def _extract_value(self) -> Any: """Parse the html extraction in the executor.""" diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index af4b8481873..d7ed9159d7f 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -300,7 +300,6 @@ FLOWS = { "ruckus_unleashed", "sabnzbd", "samsungtv", - "scrape", "screenlogic", "season", "sense", diff --git a/tests/components/scrape/__init__.py b/tests/components/scrape/__init__.py index 37abb061e75..0ba9266a79d 100644 --- a/tests/components/scrape/__init__.py +++ b/tests/components/scrape/__init__.py @@ -2,42 +2,6 @@ from __future__ import annotations from typing import Any -from unittest.mock import patch - -from homeassistant.components.scrape.const import DOMAIN -from homeassistant.config_entries import SOURCE_USER -from homeassistant.core import HomeAssistant - -from tests.common import MockConfigEntry - - -async def init_integration( - hass: HomeAssistant, - config: dict[str, Any], - data: str, - entry_id: str = "1", - source: str = SOURCE_USER, -) -> MockConfigEntry: - """Set up the Scrape integration in Home Assistant.""" - - config_entry = MockConfigEntry( - domain=DOMAIN, - source=source, - data={}, - options=config, - entry_id=entry_id, - ) - - config_entry.add_to_hass(hass) - mocker = MockRestData(data) - with patch( - "homeassistant.components.scrape.RestData", - return_value=mocker, - ): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - return config_entry def return_config( @@ -61,8 +25,6 @@ def return_config( "resource": "https://www.home-assistant.io", "select": select, "name": name, - "index": 0, - "verify_ssl": True, } if attribute: config["attribute"] = attribute @@ -76,7 +38,7 @@ def return_config( config["device_class"] = device_class if state_class: config["state_class"] = state_class - if username: + if authentication: config["authentication"] = authentication config["username"] = username config["password"] = password diff --git a/tests/components/scrape/test_config_flow.py b/tests/components/scrape/test_config_flow.py deleted file mode 100644 index 287004b1dd3..00000000000 --- a/tests/components/scrape/test_config_flow.py +++ /dev/null @@ -1,194 +0,0 @@ -"""Test the Scrape config flow.""" -from __future__ import annotations - -from unittest.mock import patch - -from homeassistant import config_entries -from homeassistant.components.scrape.const import CONF_INDEX, CONF_SELECT, DOMAIN -from homeassistant.const import ( - CONF_NAME, - CONF_RESOURCE, - CONF_VALUE_TEMPLATE, - CONF_VERIFY_SSL, -) -from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) - -from . import MockRestData - -from tests.common import MockConfigEntry - - -async def test_form(hass: HomeAssistant) -> None: - """Test we get the form.""" - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == RESULT_TYPE_FORM - - with patch( - "homeassistant.components.scrape.sensor.RestData", - return_value=MockRestData("test_scrape_sensor"), - ), patch( - "homeassistant.components.scrape.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_RESOURCE: "https://www.home-assistant.io", - CONF_NAME: "Release", - CONF_SELECT: ".current-version h1", - CONF_VALUE_TEMPLATE: "{{ value.split(':')[1] }}", - }, - ) - await hass.async_block_till_done() - - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY - assert result2["title"] == "Release" - assert result2["options"] == { - "resource": "https://www.home-assistant.io", - "name": "Release", - "select": ".current-version h1", - "value_template": "{{ value.split(':')[1] }}", - "index": 0.0, - "verify_ssl": True, - } - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_import_flow_success(hass: HomeAssistant) -> None: - """Test a successful import of yaml.""" - - with patch( - "homeassistant.components.scrape.sensor.RestData", - return_value=MockRestData("test_scrape_sensor"), - ), patch( - "homeassistant.components.scrape.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_RESOURCE: "https://www.home-assistant.io", - CONF_NAME: "Release", - CONF_SELECT: ".current-version h1", - CONF_VALUE_TEMPLATE: "{{ value.split(':')[1] }}", - CONF_INDEX: 0, - CONF_VERIFY_SSL: True, - }, - ) - await hass.async_block_till_done() - - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY - assert result2["title"] == "Release" - assert result2["options"] == { - "resource": "https://www.home-assistant.io", - "name": "Release", - "select": ".current-version h1", - "value_template": "{{ value.split(':')[1] }}", - "index": 0, - "verify_ssl": True, - } - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_import_flow_already_exist(hass: HomeAssistant) -> None: - """Test import of yaml already exist.""" - - MockConfigEntry( - domain=DOMAIN, - data={}, - options={ - "resource": "https://www.home-assistant.io", - "name": "Release", - "select": ".current-version h1", - "value_template": "{{ value.split(':')[1] }}", - "index": 0, - "verify_ssl": True, - }, - ).add_to_hass(hass) - - with patch( - "homeassistant.components.scrape.sensor.RestData", - return_value=MockRestData("test_scrape_sensor"), - ), patch( - "homeassistant.components.scrape.async_setup_entry", - return_value=True, - ): - result3 = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_RESOURCE: "https://www.home-assistant.io", - CONF_NAME: "Release", - CONF_SELECT: ".current-version h1", - CONF_VALUE_TEMPLATE: "{{ value.split(':')[1] }}", - CONF_INDEX: 0, - CONF_VERIFY_SSL: True, - }, - ) - await hass.async_block_till_done() - - assert result3["type"] == RESULT_TYPE_ABORT - assert result3["reason"] == "already_configured" - - -async def test_options_form(hass: HomeAssistant) -> None: - """Test we get the form in options.""" - - entry = MockConfigEntry( - domain=DOMAIN, - data={}, - options={ - "resource": "https://www.home-assistant.io", - "name": "Release", - "select": ".current-version h1", - "value_template": "{{ value.split(':')[1] }}", - "index": 0, - "verify_ssl": True, - }, - entry_id="1", - ) - entry.add_to_hass(hass) - - with patch( - "homeassistant.components.scrape.RestData", - return_value=MockRestData("test_scrape_sensor"), - ): - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - result = await hass.config_entries.options.async_init(entry.entry_id) - - with patch( - "homeassistant.components.scrape.RestData", - return_value=MockRestData("test_scrape_sensor"), - ): - result2 = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={ - "value_template": "{{ value.split(':')[1] }}", - "index": 1.0, - "verify_ssl": True, - }, - ) - - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY - assert result2["data"] == { - "resource": "https://www.home-assistant.io", - "name": "Release", - "select": ".current-version h1", - "value_template": "{{ value.split(':')[1] }}", - "index": 1.0, - "verify_ssl": True, - } - entry_check = hass.config_entries.async_get_entry("1") - assert entry_check.state == config_entries.ConfigEntryState.LOADED - assert entry_check.update_listeners is not None diff --git a/tests/components/scrape/test_init.py b/tests/components/scrape/test_init.py deleted file mode 100644 index 021790e65c3..00000000000 --- a/tests/components/scrape/test_init.py +++ /dev/null @@ -1,89 +0,0 @@ -"""Test Scrape component setup process.""" -from __future__ import annotations - -from unittest.mock import patch - -from homeassistant.components.scrape.const import DOMAIN -from homeassistant.config_entries import ConfigEntryState -from homeassistant.core import HomeAssistant - -from . import MockRestData - -from tests.common import MockConfigEntry - -TEST_CONFIG = { - "resource": "https://www.home-assistant.io", - "name": "Release", - "select": ".current-version h1", - "value_template": "{{ value.split(':')[1] }}", - "index": 0, - "verify_ssl": True, -} - - -async def test_setup_entry(hass: HomeAssistant) -> None: - """Test setup entry.""" - entry = MockConfigEntry( - domain=DOMAIN, - data={}, - options=TEST_CONFIG, - title="Release", - ) - entry.add_to_hass(hass) - - with patch( - "homeassistant.components.scrape.RestData", - return_value=MockRestData("test_scrape_sensor"), - ): - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - state = hass.states.get("sensor.release") - assert state - - -async def test_setup_entry_no_data_fails(hass: HomeAssistant) -> None: - """Test setup entry no data fails.""" - entry = MockConfigEntry( - domain=DOMAIN, data={}, options=TEST_CONFIG, title="Release", entry_id="1" - ) - entry.add_to_hass(hass) - - with patch( - "homeassistant.components.scrape.RestData", - return_value=MockRestData("test_scrape_sensor_no_data"), - ): - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - state = hass.states.get("sensor.ha_version") - assert state is None - entry = hass.config_entries.async_get_entry("1") - assert entry.state == ConfigEntryState.SETUP_RETRY - - -async def test_remove_entry(hass: HomeAssistant) -> None: - """Test remove entry.""" - entry = MockConfigEntry( - domain=DOMAIN, - data={}, - options=TEST_CONFIG, - title="Release", - ) - entry.add_to_hass(hass) - - with patch( - "homeassistant.components.scrape.RestData", - return_value=MockRestData("test_scrape_sensor"), - ): - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - state = hass.states.get("sensor.release") - assert state - - await hass.config_entries.async_remove(entry.entry_id) - await hass.async_block_till_done() - - state = hass.states.get("sensor.release") - assert not state diff --git a/tests/components/scrape/test_sensor.py b/tests/components/scrape/test_sensor.py index cd4e27e88a2..aaf156208ef 100644 --- a/tests/components/scrape/test_sensor.py +++ b/tests/components/scrape/test_sensor.py @@ -3,15 +3,10 @@ from __future__ import annotations from unittest.mock import patch -import pytest - from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass from homeassistant.components.sensor.const import CONF_STATE_CLASS -from homeassistant.config_entries import SOURCE_USER from homeassistant.const import ( CONF_DEVICE_CLASS, - CONF_NAME, - CONF_RESOURCE, CONF_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, TEMP_CELSIUS, @@ -20,20 +15,22 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_component import async_update_entity from homeassistant.setup import async_setup_component -from . import MockRestData, init_integration, return_config - -from tests.common import MockConfigEntry +from . import MockRestData, return_config DOMAIN = "scrape" async def test_scrape_sensor(hass: HomeAssistant) -> None: """Test Scrape sensor minimal.""" - await init_integration( - hass, - return_config(select=".current-version h1", name="HA version"), - "test_scrape_sensor", - ) + config = {"sensor": return_config(select=".current-version h1", name="HA version")} + + mocker = MockRestData("test_scrape_sensor") + with patch( + "homeassistant.components.scrape.sensor.RestData", + return_value=mocker, + ): + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() state = hass.states.get("sensor.ha_version") assert state.state == "Current Version: 2021.12.10" @@ -41,15 +38,21 @@ async def test_scrape_sensor(hass: HomeAssistant) -> None: async def test_scrape_sensor_value_template(hass: HomeAssistant) -> None: """Test Scrape sensor with value template.""" - await init_integration( - hass, - return_config( + config = { + "sensor": return_config( select=".current-version h1", name="HA version", template="{{ value.split(':')[1] }}", - ), - "test_scrape_sensor", - ) + ) + } + + mocker = MockRestData("test_scrape_sensor") + with patch( + "homeassistant.components.scrape.sensor.RestData", + return_value=mocker, + ): + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() state = hass.states.get("sensor.ha_version") assert state.state == "2021.12.10" @@ -57,18 +60,24 @@ async def test_scrape_sensor_value_template(hass: HomeAssistant) -> None: async def test_scrape_uom_and_classes(hass: HomeAssistant) -> None: """Test Scrape sensor for unit of measurement, device class and state class.""" - await init_integration( - hass, - return_config( + config = { + "sensor": return_config( select=".current-temp h3", name="Current Temp", template="{{ value.split(':')[1] }}", uom="°C", device_class="temperature", state_class="measurement", - ), - "test_scrape_uom_and_classes", - ) + ) + } + + mocker = MockRestData("test_scrape_uom_and_classes") + with patch( + "homeassistant.components.scrape.sensor.RestData", + return_value=mocker, + ): + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() state = hass.states.get("sensor.current_temp") assert state.state == "22.1" @@ -79,28 +88,31 @@ async def test_scrape_uom_and_classes(hass: HomeAssistant) -> None: async def test_scrape_sensor_authentication(hass: HomeAssistant) -> None: """Test Scrape sensor with authentication.""" - await init_integration( - hass, - return_config( - select=".return", - name="Auth page", - username="user@secret.com", - password="12345678", - authentication="digest", - ), - "test_scrape_sensor_authentication", - ) - await init_integration( - hass, - return_config( - select=".return", - name="Auth page2", - username="user@secret.com", - password="12345678", - ), - "test_scrape_sensor_authentication", - entry_id="2", - ) + config = { + "sensor": [ + return_config( + select=".return", + name="Auth page", + username="user@secret.com", + password="12345678", + authentication="digest", + ), + return_config( + select=".return", + name="Auth page2", + username="user@secret.com", + password="12345678", + ), + ] + } + + mocker = MockRestData("test_scrape_sensor_authentication") + with patch( + "homeassistant.components.scrape.sensor.RestData", + return_value=mocker, + ): + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() state = hass.states.get("sensor.auth_page") assert state.state == "secret text" @@ -110,11 +122,15 @@ async def test_scrape_sensor_authentication(hass: HomeAssistant) -> None: async def test_scrape_sensor_no_data(hass: HomeAssistant) -> None: """Test Scrape sensor fails on no data.""" - await init_integration( - hass, - return_config(select=".current-version h1", name="HA version"), - "test_scrape_sensor_no_data", - ) + config = {"sensor": return_config(select=".current-version h1", name="HA version")} + + mocker = MockRestData("test_scrape_sensor_no_data") + with patch( + "homeassistant.components.scrape.sensor.RestData", + return_value=mocker, + ): + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() state = hass.states.get("sensor.ha_version") assert state is None @@ -122,21 +138,14 @@ async def test_scrape_sensor_no_data(hass: HomeAssistant) -> None: async def test_scrape_sensor_no_data_refresh(hass: HomeAssistant) -> None: """Test Scrape sensor no data on refresh.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - source=SOURCE_USER, - data={}, - options=return_config(select=".current-version h1", name="HA version"), - entry_id="1", - ) + config = {"sensor": return_config(select=".current-version h1", name="HA version")} - config_entry.add_to_hass(hass) mocker = MockRestData("test_scrape_sensor") with patch( - "homeassistant.components.scrape.RestData", + "homeassistant.components.scrape.sensor.RestData", return_value=mocker, ): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await async_setup_component(hass, "sensor", config) await hass.async_block_till_done() state = hass.states.get("sensor.ha_version") @@ -153,17 +162,20 @@ async def test_scrape_sensor_no_data_refresh(hass: HomeAssistant) -> None: async def test_scrape_sensor_attribute_and_tag(hass: HomeAssistant) -> None: """Test Scrape sensor with attribute and tag.""" - await init_integration( - hass, - return_config(select="div", name="HA class", index=1, attribute="class"), - "test_scrape_sensor", - ) - await init_integration( - hass, - return_config(select="template", name="HA template"), - "test_scrape_sensor", - entry_id="2", - ) + config = { + "sensor": [ + return_config(select="div", name="HA class", index=1, attribute="class"), + return_config(select="template", name="HA template"), + ] + } + + mocker = MockRestData("test_scrape_sensor") + with patch( + "homeassistant.components.scrape.sensor.RestData", + return_value=mocker, + ): + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() state = hass.states.get("sensor.ha_class") assert state.state == "['links']" @@ -173,55 +185,22 @@ async def test_scrape_sensor_attribute_and_tag(hass: HomeAssistant) -> None: async def test_scrape_sensor_errors(hass: HomeAssistant) -> None: """Test Scrape sensor handle errors.""" - await init_integration( - hass, - return_config(select="div", name="HA class", index=5, attribute="class"), - "test_scrape_sensor", - ) - await init_integration( - hass, - return_config(select="div", name="HA class2", attribute="classes"), - "test_scrape_sensor", - entry_id="2", - ) - - state = hass.states.get("sensor.ha_class") - assert state.state == STATE_UNKNOWN - state2 = hass.states.get("sensor.ha_class2") - assert state2.state == STATE_UNKNOWN - - -async def test_import(hass: HomeAssistant, caplog: pytest.LogCaptureFixture) -> None: - """Test the Scrape sensor import.""" config = { - "sensor": { - "platform": "scrape", - "resource": "https://www.home-assistant.io", - "select": ".current-version h1", - "name": "HA Version", - "index": 0, - "verify_ssl": True, - "value_template": "{{ value.split(':')[1] }}", - } + "sensor": [ + return_config(select="div", name="HA class", index=5, attribute="class"), + return_config(select="div", name="HA class2", attribute="classes"), + ] } mocker = MockRestData("test_scrape_sensor") with patch( - "homeassistant.components.scrape.RestData", + "homeassistant.components.scrape.sensor.RestData", return_value=mocker, ): assert await async_setup_component(hass, "sensor", config) await hass.async_block_till_done() - assert ( - "Loading Scrape via platform setup has been deprecated in Home Assistant" - in caplog.text - ) - - assert hass.config_entries.async_entries(DOMAIN) - options = hass.config_entries.async_entries(DOMAIN)[0].options - assert options[CONF_NAME] == "HA Version" - assert options[CONF_RESOURCE] == "https://www.home-assistant.io" - - state = hass.states.get("sensor.ha_version") - assert state.state == "2021.12.10" + state = hass.states.get("sensor.ha_class") + assert state.state == STATE_UNKNOWN + state2 = hass.states.get("sensor.ha_class2") + assert state2.state == STATE_UNKNOWN From bc41832c71ee41df9cc2070e044697fbded1f5fa Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 1 Jul 2022 10:12:02 -0700 Subject: [PATCH 30/71] Bumped version to 2022.7.0b2 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 60a3d6817b5..de1148a3932 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 7 -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, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 2b1e14a40f6..92a7a962cbb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.7.0b1" +version = "2022.7.0b2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 4e793a51ba1da105846544bb3b73400cc6796be8 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Sun, 3 Jul 2022 21:26:00 +0100 Subject: [PATCH 31/71] Dont substitute user/pass for relative stream urls on generic camera (#74201) Co-authored-by: Dave T --- homeassistant/components/generic/camera.py | 8 +++- .../components/generic/config_flow.py | 13 +++++- homeassistant/components/generic/strings.json | 4 ++ .../components/generic/translations/en.json | 11 ++--- tests/components/generic/test_config_flow.py | 46 +++++++++++++++++-- 5 files changed, 68 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/generic/camera.py b/homeassistant/components/generic/camera.py index 8b03f0a8ed3..961d3cecfb7 100644 --- a/homeassistant/components/generic/camera.py +++ b/homeassistant/components/generic/camera.py @@ -228,7 +228,13 @@ class GenericCamera(Camera): try: stream_url = self._stream_source.async_render(parse_result=False) url = yarl.URL(stream_url) - if not url.user and not url.password and self._username and self._password: + if ( + not url.user + and not url.password + and self._username + and self._password + and url.is_absolute() + ): url = url.with_user(self._username).with_password(self._password) return str(url) except TemplateError as err: diff --git a/homeassistant/components/generic/config_flow.py b/homeassistant/components/generic/config_flow.py index 9096f2ce87e..514264f919e 100644 --- a/homeassistant/components/generic/config_flow.py +++ b/homeassistant/components/generic/config_flow.py @@ -150,6 +150,12 @@ async def async_test_still( except TemplateError as err: _LOGGER.warning("Problem rendering template %s: %s", url, err) return {CONF_STILL_IMAGE_URL: "template_error"}, None + try: + yarl_url = yarl.URL(url) + except ValueError: + return {CONF_STILL_IMAGE_URL: "malformed_url"}, None + if not yarl_url.is_absolute(): + return {CONF_STILL_IMAGE_URL: "relative_url"}, None verify_ssl = info[CONF_VERIFY_SSL] auth = generate_auth(info) try: @@ -222,7 +228,12 @@ async def async_test_stream( if info.get(CONF_USE_WALLCLOCK_AS_TIMESTAMPS): stream_options[CONF_USE_WALLCLOCK_AS_TIMESTAMPS] = True - url = yarl.URL(stream_source) + try: + url = yarl.URL(stream_source) + except ValueError: + return {CONF_STREAM_SOURCE: "malformed_url"} + if not url.is_absolute(): + return {CONF_STREAM_SOURCE: "relative_url"} if not url.user and not url.password: username = info.get(CONF_USERNAME) password = info.get(CONF_PASSWORD) diff --git a/homeassistant/components/generic/strings.json b/homeassistant/components/generic/strings.json index 7d3cab19aa5..608c85c1379 100644 --- a/homeassistant/components/generic/strings.json +++ b/homeassistant/components/generic/strings.json @@ -6,6 +6,8 @@ "unable_still_load": "Unable to load valid image from still image URL (e.g. invalid host, URL or authentication failure). Review log for more info.", "no_still_image_or_stream_url": "You must specify at least a still image or stream URL", "invalid_still_image": "URL did not return a valid still image", + "malformed_url": "Malformed URL", + "relative_url": "Relative URLs are not allowed", "stream_file_not_found": "File not found while trying to connect to stream (is ffmpeg installed?)", "stream_http_not_found": "HTTP 404 Not found while trying to connect to stream", "template_error": "Error rendering template. Review log for more info.", @@ -75,6 +77,8 @@ "unable_still_load": "[%key:component::generic::config::error::unable_still_load%]", "no_still_image_or_stream_url": "[%key:component::generic::config::error::no_still_image_or_stream_url%]", "invalid_still_image": "[%key:component::generic::config::error::invalid_still_image%]", + "malformed_url": "[%key:component::generic::config::error::malformed_url%]", + "relative_url": "[%key:component::generic::config::error::relative_url%]", "template_error": "[%key:component::generic::config::error::template_error%]", "timeout": "[%key:component::generic::config::error::timeout%]", "stream_no_route_to_host": "[%key:component::generic::config::error::stream_no_route_to_host%]", diff --git a/homeassistant/components/generic/translations/en.json b/homeassistant/components/generic/translations/en.json index d01e6e59a4b..cb2200f9755 100644 --- a/homeassistant/components/generic/translations/en.json +++ b/homeassistant/components/generic/translations/en.json @@ -1,20 +1,19 @@ { "config": { "abort": { - "no_devices_found": "No devices found on the network", "single_instance_allowed": "Already configured. Only a single configuration possible." }, "error": { "already_exists": "A camera with these URL settings already exists.", "invalid_still_image": "URL did not return a valid still image", + "malformed_url": "Malformed URL", "no_still_image_or_stream_url": "You must specify at least a still image or stream URL", + "relative_url": "Relative URLs are not allowed", "stream_file_not_found": "File not found while trying to connect to stream (is ffmpeg installed?)", "stream_http_not_found": "HTTP 404 Not found while trying to connect to stream", "stream_io_error": "Input/Output error while trying to connect to stream. Wrong RTSP transport protocol?", "stream_no_route_to_host": "Could not find host while trying to connect to stream", - "stream_no_video": "Stream has no video", "stream_not_permitted": "Operation not permitted while trying to connect to stream. Wrong RTSP transport protocol?", - "stream_unauthorised": "Authorisation failed while trying to connect to stream", "template_error": "Error rendering template. Review log for more info.", "timeout": "Timeout while loading URL", "unable_still_load": "Unable to load valid image from still image URL (e.g. invalid host, URL or authentication failure). Review log for more info.", @@ -50,14 +49,12 @@ "error": { "already_exists": "A camera with these URL settings already exists.", "invalid_still_image": "URL did not return a valid still image", + "malformed_url": "Malformed URL", "no_still_image_or_stream_url": "You must specify at least a still image or stream URL", - "stream_file_not_found": "File not found while trying to connect to stream (is ffmpeg installed?)", - "stream_http_not_found": "HTTP 404 Not found while trying to connect to stream", + "relative_url": "Relative URLs are not allowed", "stream_io_error": "Input/Output error while trying to connect to stream. Wrong RTSP transport protocol?", "stream_no_route_to_host": "Could not find host while trying to connect to stream", - "stream_no_video": "Stream has no video", "stream_not_permitted": "Operation not permitted while trying to connect to stream. Wrong RTSP transport protocol?", - "stream_unauthorised": "Authorisation failed while trying to connect to stream", "template_error": "Error rendering template. Review log for more info.", "timeout": "Timeout while loading URL", "unable_still_load": "Unable to load valid image from still image URL (e.g. invalid host, URL or authentication failure). Review log for more info.", diff --git a/tests/components/generic/test_config_flow.py b/tests/components/generic/test_config_flow.py index f0589301014..592d139f92e 100644 --- a/tests/components/generic/test_config_flow.py +++ b/tests/components/generic/test_config_flow.py @@ -180,33 +180,43 @@ async def test_form_only_still_sample(hass, user_flow, image_file): @respx.mock @pytest.mark.parametrize( - ("template", "url", "expected_result"), + ("template", "url", "expected_result", "expected_errors"), [ # Test we can handle templates in strange parts of the url, #70961. ( "http://localhost:812{{3}}/static/icons/favicon-apple-180x180.png", "http://localhost:8123/static/icons/favicon-apple-180x180.png", data_entry_flow.RESULT_TYPE_CREATE_ENTRY, + None, ), ( "{% if 1 %}https://bla{% else %}https://yo{% endif %}", "https://bla/", data_entry_flow.RESULT_TYPE_CREATE_ENTRY, + None, ), ( "http://{{example.org", "http://example.org", data_entry_flow.RESULT_TYPE_FORM, + {"still_image_url": "template_error"}, ), ( "invalid1://invalid:4\\1", "invalid1://invalid:4%5c1", - data_entry_flow.RESULT_TYPE_CREATE_ENTRY, + data_entry_flow.RESULT_TYPE_FORM, + {"still_image_url": "malformed_url"}, + ), + ( + "relative/urls/are/not/allowed.jpg", + "relative/urls/are/not/allowed.jpg", + data_entry_flow.RESULT_TYPE_FORM, + {"still_image_url": "relative_url"}, ), ], ) async def test_still_template( - hass, user_flow, fakeimgbytes_png, template, url, expected_result + hass, user_flow, fakeimgbytes_png, template, url, expected_result, expected_errors ) -> None: """Test we can handle various templates.""" respx.get(url).respond(stream=fakeimgbytes_png) @@ -220,6 +230,7 @@ async def test_still_template( ) await hass.async_block_till_done() assert result2["type"] == expected_result + assert result2.get("errors") == expected_errors @respx.mock @@ -514,8 +525,29 @@ async def test_options_template_error(hass, fakeimgbytes_png, mock_create_stream result4["flow_id"], user_input=data, ) - assert result5.get("type") == data_entry_flow.RESULT_TYPE_FORM - assert result5["errors"] == {"stream_source": "template_error"} + + assert result5.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result5["errors"] == {"stream_source": "template_error"} + + # verify that an relative stream url is rejected. + data[CONF_STILL_IMAGE_URL] = "http://127.0.0.1/testurl/1" + data[CONF_STREAM_SOURCE] = "relative/stream.mjpeg" + result6 = await hass.config_entries.options.async_configure( + result5["flow_id"], + user_input=data, + ) + assert result6.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result6["errors"] == {"stream_source": "relative_url"} + + # verify that an malformed stream url is rejected. + data[CONF_STILL_IMAGE_URL] = "http://127.0.0.1/testurl/1" + data[CONF_STREAM_SOURCE] = "http://example.com:45:56" + result7 = await hass.config_entries.options.async_configure( + result6["flow_id"], + user_input=data, + ) + assert result7.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result7["errors"] == {"stream_source": "malformed_url"} async def test_slug(hass, caplog): @@ -528,6 +560,10 @@ async def test_slug(hass, caplog): assert result is None assert "Syntax error in" in caplog.text + result = slug(hass, "http://example.com:999999999999/stream") + assert result is None + assert "Syntax error in" in caplog.text + @respx.mock async def test_options_only_stream(hass, fakeimgbytes_png, mock_create_stream): From 373347950cc963a7fe14b2f7a91a0b1210fb9f9f Mon Sep 17 00:00:00 2001 From: mbo18 Date: Sun, 3 Jul 2022 22:49:03 +0200 Subject: [PATCH 32/71] Migrate Meteo_france to native_* (#74297) --- .../components/meteo_france/weather.py | 49 +++++++++++-------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/meteo_france/weather.py b/homeassistant/components/meteo_france/weather.py index cca1f6fe684..a30a65304b0 100644 --- a/homeassistant/components/meteo_france/weather.py +++ b/homeassistant/components/meteo_france/weather.py @@ -4,16 +4,22 @@ import time from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, WeatherEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_MODE, TEMP_CELSIUS +from homeassistant.const import ( + CONF_MODE, + LENGTH_MILLIMETERS, + PRESSURE_HPA, + SPEED_METERS_PER_SECOND, + TEMP_CELSIUS, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo @@ -71,6 +77,11 @@ async def async_setup_entry( class MeteoFranceWeather(CoordinatorEntity, WeatherEntity): """Representation of a weather condition.""" + _attr_native_temperature_unit = TEMP_CELSIUS + _attr_native_precipitation_unit = LENGTH_MILLIMETERS + _attr_native_pressure_unit = PRESSURE_HPA + _attr_native_wind_speed_unit = SPEED_METERS_PER_SECOND + def __init__(self, coordinator: DataUpdateCoordinator, mode: str) -> None: """Initialise the platform with a data instance and station name.""" super().__init__(coordinator) @@ -107,17 +118,12 @@ class MeteoFranceWeather(CoordinatorEntity, WeatherEntity): ) @property - def temperature(self): + def native_temperature(self): """Return the temperature.""" return self.coordinator.data.current_forecast["T"]["value"] @property - def temperature_unit(self): - """Return the unit of measurement.""" - return TEMP_CELSIUS - - @property - def pressure(self): + def native_pressure(self): """Return the pressure.""" return self.coordinator.data.current_forecast["sea_level"] @@ -127,10 +133,9 @@ class MeteoFranceWeather(CoordinatorEntity, WeatherEntity): return self.coordinator.data.current_forecast["humidity"] @property - def wind_speed(self): + def native_wind_speed(self): """Return the wind speed.""" - # convert from API m/s to km/h - return round(self.coordinator.data.current_forecast["wind"]["speed"] * 3.6) + return self.coordinator.data.current_forecast["wind"]["speed"] @property def wind_bearing(self): @@ -158,9 +163,9 @@ class MeteoFranceWeather(CoordinatorEntity, WeatherEntity): ATTR_FORECAST_CONDITION: format_condition( forecast["weather"]["desc"] ), - ATTR_FORECAST_TEMP: forecast["T"]["value"], - ATTR_FORECAST_PRECIPITATION: forecast["rain"].get("1h"), - ATTR_FORECAST_WIND_SPEED: forecast["wind"]["speed"], + ATTR_FORECAST_NATIVE_TEMP: forecast["T"]["value"], + ATTR_FORECAST_NATIVE_PRECIPITATION: forecast["rain"].get("1h"), + ATTR_FORECAST_NATIVE_WIND_SPEED: forecast["wind"]["speed"], ATTR_FORECAST_WIND_BEARING: forecast["wind"]["direction"] if forecast["wind"]["direction"] != -1 else None, @@ -179,9 +184,11 @@ class MeteoFranceWeather(CoordinatorEntity, WeatherEntity): ATTR_FORECAST_CONDITION: format_condition( forecast["weather12H"]["desc"] ), - ATTR_FORECAST_TEMP: forecast["T"]["max"], - ATTR_FORECAST_TEMP_LOW: forecast["T"]["min"], - ATTR_FORECAST_PRECIPITATION: forecast["precipitation"]["24h"], + ATTR_FORECAST_NATIVE_TEMP: forecast["T"]["max"], + ATTR_FORECAST_NATIVE_TEMP_LOW: forecast["T"]["min"], + ATTR_FORECAST_NATIVE_PRECIPITATION: forecast["precipitation"][ + "24h" + ], } ) return forecast_data From 08ee73d671697cdfef86aaa4fea86395ef7fe6c0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 1 Jul 2022 11:01:07 -0700 Subject: [PATCH 33/71] Guard creating areas in onboarding (#74306) --- homeassistant/components/onboarding/views.py | 8 +++++--- tests/components/onboarding/test_views.py | 7 ++++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/onboarding/views.py b/homeassistant/components/onboarding/views.py index 7f40ad87e84..c29fb7edf3a 100644 --- a/homeassistant/components/onboarding/views.py +++ b/homeassistant/components/onboarding/views.py @@ -156,9 +156,11 @@ class UserOnboardingView(_BaseOnboardingView): area_registry = ar.async_get(hass) for area in DEFAULT_AREAS: - area_registry.async_create( - translations[f"component.onboarding.area.{area}"] - ) + name = translations[f"component.onboarding.area.{area}"] + # Guard because area might have been created by an automatically + # set up integration. + if not area_registry.async_get_area_by_name(name): + area_registry.async_create(name) await self._async_mark_done(hass) diff --git a/tests/components/onboarding/test_views.py b/tests/components/onboarding/test_views.py index 982f5b86e65..204eb6bf772 100644 --- a/tests/components/onboarding/test_views.py +++ b/tests/components/onboarding/test_views.py @@ -144,6 +144,12 @@ async def test_onboarding_user_already_done(hass, hass_storage, hass_client_no_a async def test_onboarding_user(hass, hass_storage, hass_client_no_auth): """Test creating a new user.""" + area_registry = ar.async_get(hass) + + # Create an existing area to mimic an integration creating an area + # before onboarding is done. + area_registry.async_create("Living Room") + assert await async_setup_component(hass, "person", {}) assert await async_setup_component(hass, "onboarding", {}) await hass.async_block_till_done() @@ -194,7 +200,6 @@ async def test_onboarding_user(hass, hass_storage, hass_client_no_auth): ) # Validate created areas - area_registry = ar.async_get(hass) assert len(area_registry.areas) == 3 assert sorted(area.name for area in area_registry.async_list_areas()) == [ "Bedroom", From 0ca4e81e13295d4ff72af033597a1cfe01722bd6 Mon Sep 17 00:00:00 2001 From: avee87 <6134677+avee87@users.noreply.github.com> Date: Sat, 2 Jul 2022 21:27:47 +0100 Subject: [PATCH 34/71] Migrate metoffice to native_* (#74312) --- homeassistant/components/metoffice/weather.py | 25 ++++++++-------- tests/components/metoffice/test_weather.py | 29 +++++++++++-------- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/metoffice/weather.py b/homeassistant/components/metoffice/weather.py index daf37bcf83f..2e9a55b4415 100644 --- a/homeassistant/components/metoffice/weather.py +++ b/homeassistant/components/metoffice/weather.py @@ -1,15 +1,15 @@ """Support for UK Met Office weather service.""" from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, WeatherEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import PRESSURE_HPA, SPEED_MILES_PER_HOUR, TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -55,11 +55,11 @@ def _build_forecast_data(timestep): if timestep.precipitation: data[ATTR_FORECAST_PRECIPITATION_PROBABILITY] = timestep.precipitation.value if timestep.temperature: - data[ATTR_FORECAST_TEMP] = timestep.temperature.value + data[ATTR_FORECAST_NATIVE_TEMP] = timestep.temperature.value if timestep.wind_direction: data[ATTR_FORECAST_WIND_BEARING] = timestep.wind_direction.value if timestep.wind_speed: - data[ATTR_FORECAST_WIND_SPEED] = timestep.wind_speed.value + data[ATTR_FORECAST_NATIVE_WIND_SPEED] = timestep.wind_speed.value return data @@ -73,6 +73,10 @@ def _get_weather_condition(metoffice_code): class MetOfficeWeather(CoordinatorEntity, WeatherEntity): """Implementation of a Met Office weather condition.""" + _attr_native_temperature_unit = TEMP_CELSIUS + _attr_native_pressure_unit = PRESSURE_HPA + _attr_native_wind_speed_unit = SPEED_MILES_PER_HOUR + def __init__(self, coordinator, hass_data, use_3hourly): """Initialise the platform with a data instance.""" super().__init__(coordinator) @@ -94,17 +98,12 @@ class MetOfficeWeather(CoordinatorEntity, WeatherEntity): return None @property - def temperature(self): + def native_temperature(self): """Return the platform temperature.""" if self.coordinator.data.now.temperature: return self.coordinator.data.now.temperature.value return None - @property - def temperature_unit(self): - """Return the unit of measurement.""" - return TEMP_CELSIUS - @property def visibility(self): """Return the platform visibility.""" @@ -119,7 +118,7 @@ class MetOfficeWeather(CoordinatorEntity, WeatherEntity): return _visibility @property - def pressure(self): + def native_pressure(self): """Return the mean sea-level pressure.""" weather_now = self.coordinator.data.now if weather_now and weather_now.pressure: @@ -135,7 +134,7 @@ class MetOfficeWeather(CoordinatorEntity, WeatherEntity): return None @property - def wind_speed(self): + def native_wind_speed(self): """Return the wind speed.""" weather_now = self.coordinator.data.now if weather_now and weather_now.wind_speed: diff --git a/tests/components/metoffice/test_weather.py b/tests/components/metoffice/test_weather.py index bf279ff3cf7..3b7ebd08678 100644 --- a/tests/components/metoffice/test_weather.py +++ b/tests/components/metoffice/test_weather.py @@ -133,7 +133,8 @@ async def test_one_weather_site_running(hass, requests_mock): assert weather.state == "sunny" assert weather.attributes.get("temperature") == 17 - assert weather.attributes.get("wind_speed") == 9 + assert weather.attributes.get("wind_speed") == 14.48 + assert weather.attributes.get("wind_speed_unit") == "km/h" assert weather.attributes.get("wind_bearing") == "SSE" assert weather.attributes.get("visibility") == "Good - 10-20" assert weather.attributes.get("humidity") == 50 @@ -148,7 +149,7 @@ async def test_one_weather_site_running(hass, requests_mock): assert weather.attributes.get("forecast")[26]["condition"] == "cloudy" assert weather.attributes.get("forecast")[26]["precipitation_probability"] == 9 assert weather.attributes.get("forecast")[26]["temperature"] == 10 - assert weather.attributes.get("forecast")[26]["wind_speed"] == 4 + assert weather.attributes.get("forecast")[26]["wind_speed"] == 6.44 assert weather.attributes.get("forecast")[26]["wind_bearing"] == "NNE" # Wavertree daily weather platform expected results @@ -157,7 +158,7 @@ async def test_one_weather_site_running(hass, requests_mock): assert weather.state == "sunny" assert weather.attributes.get("temperature") == 19 - assert weather.attributes.get("wind_speed") == 9 + assert weather.attributes.get("wind_speed") == 14.48 assert weather.attributes.get("wind_bearing") == "SSE" assert weather.attributes.get("visibility") == "Good - 10-20" assert weather.attributes.get("humidity") == 50 @@ -172,7 +173,7 @@ async def test_one_weather_site_running(hass, requests_mock): assert weather.attributes.get("forecast")[3]["condition"] == "rainy" assert weather.attributes.get("forecast")[3]["precipitation_probability"] == 59 assert weather.attributes.get("forecast")[3]["temperature"] == 13 - assert weather.attributes.get("forecast")[3]["wind_speed"] == 13 + assert weather.attributes.get("forecast")[3]["wind_speed"] == 20.92 assert weather.attributes.get("forecast")[3]["wind_bearing"] == "SE" @@ -229,7 +230,8 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.state == "sunny" assert weather.attributes.get("temperature") == 17 - assert weather.attributes.get("wind_speed") == 9 + assert weather.attributes.get("wind_speed") == 14.48 + assert weather.attributes.get("wind_speed_unit") == "km/h" assert weather.attributes.get("wind_bearing") == "SSE" assert weather.attributes.get("visibility") == "Good - 10-20" assert weather.attributes.get("humidity") == 50 @@ -244,7 +246,7 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.attributes.get("forecast")[18]["condition"] == "clear-night" assert weather.attributes.get("forecast")[18]["precipitation_probability"] == 1 assert weather.attributes.get("forecast")[18]["temperature"] == 9 - assert weather.attributes.get("forecast")[18]["wind_speed"] == 4 + assert weather.attributes.get("forecast")[18]["wind_speed"] == 6.44 assert weather.attributes.get("forecast")[18]["wind_bearing"] == "NW" # Wavertree daily weather platform expected results @@ -253,7 +255,8 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.state == "sunny" assert weather.attributes.get("temperature") == 19 - assert weather.attributes.get("wind_speed") == 9 + assert weather.attributes.get("wind_speed") == 14.48 + assert weather.attributes.get("wind_speed_unit") == "km/h" assert weather.attributes.get("wind_bearing") == "SSE" assert weather.attributes.get("visibility") == "Good - 10-20" assert weather.attributes.get("humidity") == 50 @@ -268,7 +271,7 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.attributes.get("forecast")[3]["condition"] == "rainy" assert weather.attributes.get("forecast")[3]["precipitation_probability"] == 59 assert weather.attributes.get("forecast")[3]["temperature"] == 13 - assert weather.attributes.get("forecast")[3]["wind_speed"] == 13 + assert weather.attributes.get("forecast")[3]["wind_speed"] == 20.92 assert weather.attributes.get("forecast")[3]["wind_bearing"] == "SE" # King's Lynn 3-hourly weather platform expected results @@ -277,7 +280,8 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.state == "sunny" assert weather.attributes.get("temperature") == 14 - assert weather.attributes.get("wind_speed") == 2 + assert weather.attributes.get("wind_speed") == 3.22 + assert weather.attributes.get("wind_speed_unit") == "km/h" assert weather.attributes.get("wind_bearing") == "E" assert weather.attributes.get("visibility") == "Very Good - 20-40" assert weather.attributes.get("humidity") == 60 @@ -292,7 +296,7 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.attributes.get("forecast")[18]["condition"] == "cloudy" assert weather.attributes.get("forecast")[18]["precipitation_probability"] == 9 assert weather.attributes.get("forecast")[18]["temperature"] == 10 - assert weather.attributes.get("forecast")[18]["wind_speed"] == 7 + assert weather.attributes.get("forecast")[18]["wind_speed"] == 11.27 assert weather.attributes.get("forecast")[18]["wind_bearing"] == "SE" # King's Lynn daily weather platform expected results @@ -301,7 +305,8 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.state == "cloudy" assert weather.attributes.get("temperature") == 9 - assert weather.attributes.get("wind_speed") == 4 + assert weather.attributes.get("wind_speed") == 6.44 + assert weather.attributes.get("wind_speed_unit") == "km/h" assert weather.attributes.get("wind_bearing") == "ESE" assert weather.attributes.get("visibility") == "Very Good - 20-40" assert weather.attributes.get("humidity") == 75 @@ -316,5 +321,5 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.attributes.get("forecast")[2]["condition"] == "cloudy" assert weather.attributes.get("forecast")[2]["precipitation_probability"] == 14 assert weather.attributes.get("forecast")[2]["temperature"] == 11 - assert weather.attributes.get("forecast")[2]["wind_speed"] == 7 + assert weather.attributes.get("forecast")[2]["wind_speed"] == 11.27 assert weather.attributes.get("forecast")[2]["wind_bearing"] == "ESE" From ec036313395090f3cff86d4aedbc8b425ab73eac Mon Sep 17 00:00:00 2001 From: avee87 <6134677+avee87@users.noreply.github.com> Date: Sat, 2 Jul 2022 21:42:58 +0100 Subject: [PATCH 35/71] Remove visibility from metoffice weather (#74314) Co-authored-by: Paulus Schoutsen --- homeassistant/components/metoffice/weather.py | 15 --------------- tests/components/metoffice/test_weather.py | 6 ------ 2 files changed, 21 deletions(-) diff --git a/homeassistant/components/metoffice/weather.py b/homeassistant/components/metoffice/weather.py index 2e9a55b4415..f4e0bf61d30 100644 --- a/homeassistant/components/metoffice/weather.py +++ b/homeassistant/components/metoffice/weather.py @@ -27,8 +27,6 @@ from .const import ( MODE_3HOURLY_LABEL, MODE_DAILY, MODE_DAILY_LABEL, - VISIBILITY_CLASSES, - VISIBILITY_DISTANCE_CLASSES, ) @@ -104,19 +102,6 @@ class MetOfficeWeather(CoordinatorEntity, WeatherEntity): return self.coordinator.data.now.temperature.value return None - @property - def visibility(self): - """Return the platform visibility.""" - _visibility = None - weather_now = self.coordinator.data.now - if hasattr(weather_now, "visibility"): - visibility_class = VISIBILITY_CLASSES.get(weather_now.visibility.value) - visibility_distance = VISIBILITY_DISTANCE_CLASSES.get( - weather_now.visibility.value - ) - _visibility = f"{visibility_class} - {visibility_distance}" - return _visibility - @property def native_pressure(self): """Return the mean sea-level pressure.""" diff --git a/tests/components/metoffice/test_weather.py b/tests/components/metoffice/test_weather.py index 3b7ebd08678..a93b1ea6b62 100644 --- a/tests/components/metoffice/test_weather.py +++ b/tests/components/metoffice/test_weather.py @@ -136,7 +136,6 @@ async def test_one_weather_site_running(hass, requests_mock): assert weather.attributes.get("wind_speed") == 14.48 assert weather.attributes.get("wind_speed_unit") == "km/h" assert weather.attributes.get("wind_bearing") == "SSE" - assert weather.attributes.get("visibility") == "Good - 10-20" assert weather.attributes.get("humidity") == 50 # Forecasts added - just pick out 1 entry to check @@ -160,7 +159,6 @@ async def test_one_weather_site_running(hass, requests_mock): assert weather.attributes.get("temperature") == 19 assert weather.attributes.get("wind_speed") == 14.48 assert weather.attributes.get("wind_bearing") == "SSE" - assert weather.attributes.get("visibility") == "Good - 10-20" assert weather.attributes.get("humidity") == 50 # Also has Forecasts added - again, just pick out 1 entry to check @@ -233,7 +231,6 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.attributes.get("wind_speed") == 14.48 assert weather.attributes.get("wind_speed_unit") == "km/h" assert weather.attributes.get("wind_bearing") == "SSE" - assert weather.attributes.get("visibility") == "Good - 10-20" assert weather.attributes.get("humidity") == 50 # Forecasts added - just pick out 1 entry to check @@ -258,7 +255,6 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.attributes.get("wind_speed") == 14.48 assert weather.attributes.get("wind_speed_unit") == "km/h" assert weather.attributes.get("wind_bearing") == "SSE" - assert weather.attributes.get("visibility") == "Good - 10-20" assert weather.attributes.get("humidity") == 50 # Also has Forecasts added - again, just pick out 1 entry to check @@ -283,7 +279,6 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.attributes.get("wind_speed") == 3.22 assert weather.attributes.get("wind_speed_unit") == "km/h" assert weather.attributes.get("wind_bearing") == "E" - assert weather.attributes.get("visibility") == "Very Good - 20-40" assert weather.attributes.get("humidity") == 60 # Also has Forecast added - just pick out 1 entry to check @@ -308,7 +303,6 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.attributes.get("wind_speed") == 6.44 assert weather.attributes.get("wind_speed_unit") == "km/h" assert weather.attributes.get("wind_bearing") == "ESE" - assert weather.attributes.get("visibility") == "Very Good - 20-40" assert weather.attributes.get("humidity") == 75 # All should have Forecast added - again, just picking out 1 entry to check From fe437cc9b7fe03db5b96f8451fb4272e996a4ea4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 2 Jul 2022 22:10:38 +0200 Subject: [PATCH 36/71] Add configuration directory to system health (#74318) --- homeassistant/components/homeassistant/strings.json | 3 ++- homeassistant/components/homeassistant/system_health.py | 1 + homeassistant/components/homeassistant/translations/en.json | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homeassistant/strings.json b/homeassistant/components/homeassistant/strings.json index 43180b237b9..317e9d1dfcd 100644 --- a/homeassistant/components/homeassistant/strings.json +++ b/homeassistant/components/homeassistant/strings.json @@ -2,15 +2,16 @@ "system_health": { "info": { "arch": "CPU Architecture", + "config_dir": "Configuration Directory", "dev": "Development", "docker": "Docker", - "user": "User", "hassio": "Supervisor", "installation_type": "Installation Type", "os_name": "Operating System Family", "os_version": "Operating System Version", "python_version": "Python Version", "timezone": "Timezone", + "user": "User", "version": "Version", "virtualenv": "Virtual Environment" } diff --git a/homeassistant/components/homeassistant/system_health.py b/homeassistant/components/homeassistant/system_health.py index f13278ddfeb..4006228de25 100644 --- a/homeassistant/components/homeassistant/system_health.py +++ b/homeassistant/components/homeassistant/system_health.py @@ -29,4 +29,5 @@ async def system_health_info(hass): "os_version": info.get("os_version"), "arch": info.get("arch"), "timezone": info.get("timezone"), + "config_dir": hass.config.config_dir, } diff --git a/homeassistant/components/homeassistant/translations/en.json b/homeassistant/components/homeassistant/translations/en.json index 977bc203fea..37c4498b32b 100644 --- a/homeassistant/components/homeassistant/translations/en.json +++ b/homeassistant/components/homeassistant/translations/en.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "CPU Architecture", + "config_dir": "Configuration Directory", "dev": "Development", "docker": "Docker", "hassio": "Supervisor", From 2a1a6301a9d5f060b6cfae2f64c3141d37e7ee90 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 3 Jul 2022 22:53:44 +0200 Subject: [PATCH 37/71] Fix unique id issue for OpenWeatherMap (#74335) --- .../components/openweathermap/const.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/openweathermap/const.py b/homeassistant/components/openweathermap/const.py index f180f2a9bbf..06f13daa9c2 100644 --- a/homeassistant/components/openweathermap/const.py +++ b/homeassistant/components/openweathermap/const.py @@ -22,10 +22,6 @@ from homeassistant.components.weather import ( ATTR_CONDITION_WINDY, ATTR_CONDITION_WINDY_VARIANT, ATTR_FORECAST_CONDITION, - ATTR_FORECAST_NATIVE_PRECIPITATION, - ATTR_FORECAST_NATIVE_PRESSURE, - ATTR_FORECAST_NATIVE_TEMP, - ATTR_FORECAST_NATIVE_TEMP_LOW, ATTR_FORECAST_PRECIPITATION_PROBABILITY, ATTR_FORECAST_TIME, ) @@ -72,6 +68,11 @@ ATTR_API_FORECAST = "forecast" UPDATE_LISTENER = "update_listener" PLATFORMS = [Platform.SENSOR, Platform.WEATHER] +ATTR_FORECAST_PRECIPITATION = "precipitation" +ATTR_FORECAST_PRESSURE = "pressure" +ATTR_FORECAST_TEMP = "temperature" +ATTR_FORECAST_TEMP_LOW = "templow" + FORECAST_MODE_HOURLY = "hourly" FORECAST_MODE_DAILY = "daily" FORECAST_MODE_FREE_DAILY = "freedaily" @@ -266,7 +267,7 @@ FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( name="Condition", ), SensorEntityDescription( - key=ATTR_FORECAST_NATIVE_PRECIPITATION, + key=ATTR_FORECAST_PRECIPITATION, name="Precipitation", native_unit_of_measurement=LENGTH_MILLIMETERS, ), @@ -276,19 +277,19 @@ FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( native_unit_of_measurement=PERCENTAGE, ), SensorEntityDescription( - key=ATTR_FORECAST_NATIVE_PRESSURE, + key=ATTR_FORECAST_PRESSURE, name="Pressure", native_unit_of_measurement=PRESSURE_HPA, device_class=SensorDeviceClass.PRESSURE, ), SensorEntityDescription( - key=ATTR_FORECAST_NATIVE_TEMP, + key=ATTR_FORECAST_TEMP, name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( - key=ATTR_FORECAST_NATIVE_TEMP_LOW, + key=ATTR_FORECAST_TEMP_LOW, name="Temperature Low", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, From c2072cc92b5da2eefe322bd93269b06c9fd90a4b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 3 Jul 2022 15:48:34 -0500 Subject: [PATCH 38/71] Fix esphome state mapping (#74337) --- homeassistant/components/esphome/__init__.py | 17 +++--- .../components/esphome/entry_data.py | 53 ++++--------------- 2 files changed, 22 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 0c1eac3aa45..ddedaf11ceb 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -558,7 +558,7 @@ async def platform_async_setup_entry( entry_data: RuntimeEntryData = DomainData.get(hass).get_entry_data(entry) entry_data.info[component_key] = {} entry_data.old_info[component_key] = {} - entry_data.state[component_key] = {} + entry_data.state.setdefault(state_type, {}) @callback def async_list_entities(infos: list[EntityInfo]) -> None: @@ -578,7 +578,7 @@ async def platform_async_setup_entry( old_infos.pop(info.key) else: # Create new entity - entity = entity_type(entry_data, component_key, info.key) + entity = entity_type(entry_data, component_key, info.key, state_type) add_entities.append(entity) new_infos[info.key] = info @@ -677,12 +677,17 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]): """Define a base esphome entity.""" def __init__( - self, entry_data: RuntimeEntryData, component_key: str, key: int + self, + entry_data: RuntimeEntryData, + component_key: str, + key: int, + state_type: type[_StateT], ) -> None: """Initialize.""" self._entry_data = entry_data self._component_key = component_key self._key = key + self._state_type = state_type async def async_added_to_hass(self) -> None: """Register callbacks.""" @@ -707,7 +712,7 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]): self.async_on_remove( self._entry_data.async_subscribe_state_update( - self._component_key, self._key, self._on_state_update + self._state_type, self._key, self._on_state_update ) ) @@ -755,11 +760,11 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]): @property def _state(self) -> _StateT: - return cast(_StateT, self._entry_data.state[self._component_key][self._key]) + return cast(_StateT, self._entry_data.state[self._state_type][self._key]) @property def _has_state(self) -> bool: - return self._key in self._entry_data.state[self._component_key] + return self._key in self._entry_data.state[self._state_type] @property def available(self) -> bool: diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index 8eb56e6fdb6..41a0e89245e 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -12,34 +12,21 @@ from aioesphomeapi import ( APIClient, APIVersion, BinarySensorInfo, - BinarySensorState, CameraInfo, - CameraState, ClimateInfo, - ClimateState, CoverInfo, - CoverState, DeviceInfo, EntityInfo, EntityState, FanInfo, - FanState, LightInfo, - LightState, LockInfo, - LockState, MediaPlayerInfo, - MediaPlayerState, NumberInfo, - NumberState, SelectInfo, - SelectState, SensorInfo, - SensorState, SwitchInfo, - SwitchState, TextSensorInfo, - TextSensorState, UserService, ) from aioesphomeapi.model import ButtonInfo @@ -56,8 +43,8 @@ _LOGGER = logging.getLogger(__name__) # Mapping from ESPHome info type to HA platform INFO_TYPE_TO_PLATFORM: dict[type[EntityInfo], str] = { BinarySensorInfo: Platform.BINARY_SENSOR, - ButtonInfo: Platform.BINARY_SENSOR, - CameraInfo: Platform.BINARY_SENSOR, + ButtonInfo: Platform.BUTTON, + CameraInfo: Platform.CAMERA, ClimateInfo: Platform.CLIMATE, CoverInfo: Platform.COVER, FanInfo: Platform.FAN, @@ -71,23 +58,6 @@ INFO_TYPE_TO_PLATFORM: dict[type[EntityInfo], str] = { TextSensorInfo: Platform.SENSOR, } -STATE_TYPE_TO_COMPONENT_KEY = { - BinarySensorState: Platform.BINARY_SENSOR, - EntityState: Platform.BINARY_SENSOR, - CameraState: Platform.BINARY_SENSOR, - ClimateState: Platform.CLIMATE, - CoverState: Platform.COVER, - FanState: Platform.FAN, - LightState: Platform.LIGHT, - LockState: Platform.LOCK, - MediaPlayerState: Platform.MEDIA_PLAYER, - NumberState: Platform.NUMBER, - SelectState: Platform.SELECT, - SensorState: Platform.SENSOR, - SwitchState: Platform.SWITCH, - TextSensorState: Platform.SENSOR, -} - @dataclass class RuntimeEntryData: @@ -96,7 +66,7 @@ class RuntimeEntryData: entry_id: str client: APIClient store: Store - state: dict[str, dict[int, EntityState]] = field(default_factory=dict) + state: dict[type[EntityState], dict[int, EntityState]] = field(default_factory=dict) info: dict[str, dict[int, EntityInfo]] = field(default_factory=dict) # A second list of EntityInfo objects @@ -111,9 +81,9 @@ class RuntimeEntryData: api_version: APIVersion = field(default_factory=APIVersion) cleanup_callbacks: list[Callable[[], None]] = field(default_factory=list) disconnect_callbacks: list[Callable[[], None]] = field(default_factory=list) - state_subscriptions: dict[tuple[str, int], Callable[[], None]] = field( - default_factory=dict - ) + state_subscriptions: dict[ + tuple[type[EntityState], int], Callable[[], None] + ] = field(default_factory=dict) loaded_platforms: set[str] = field(default_factory=set) platform_load_lock: asyncio.Lock = field(default_factory=asyncio.Lock) _storage_contents: dict[str, Any] | None = None @@ -160,24 +130,23 @@ class RuntimeEntryData: @callback def async_subscribe_state_update( self, - component_key: str, + state_type: type[EntityState], state_key: int, entity_callback: Callable[[], None], ) -> Callable[[], None]: """Subscribe to state updates.""" def _unsubscribe() -> None: - self.state_subscriptions.pop((component_key, state_key)) + self.state_subscriptions.pop((state_type, state_key)) - self.state_subscriptions[(component_key, state_key)] = entity_callback + self.state_subscriptions[(state_type, state_key)] = entity_callback return _unsubscribe @callback def async_update_state(self, state: EntityState) -> None: """Distribute an update of state information to the target.""" - component_key = STATE_TYPE_TO_COMPONENT_KEY[type(state)] - subscription_key = (component_key, state.key) - self.state[component_key][state.key] = state + subscription_key = (type(state), state.key) + self.state[type(state)][state.key] = state _LOGGER.debug( "Dispatching update with key %s: %s", subscription_key, From c530e965f83940412c9a9d92210521bb7c1aab63 Mon Sep 17 00:00:00 2001 From: shbatm Date: Sat, 2 Jul 2022 20:38:48 -0500 Subject: [PATCH 39/71] Onvif: bump onvif-zeep-async to 1.2.1 (#74341) * Update requirements_all.txt * Update requirements_test_all.txt * Update manifest.json --- homeassistant/components/onvif/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/onvif/manifest.json b/homeassistant/components/onvif/manifest.json index cd220500751..2df7c3004f0 100644 --- a/homeassistant/components/onvif/manifest.json +++ b/homeassistant/components/onvif/manifest.json @@ -2,7 +2,7 @@ "domain": "onvif", "name": "ONVIF", "documentation": "https://www.home-assistant.io/integrations/onvif", - "requirements": ["onvif-zeep-async==1.2.0", "WSDiscovery==2.0.0"], + "requirements": ["onvif-zeep-async==1.2.1", "WSDiscovery==2.0.0"], "dependencies": ["ffmpeg"], "codeowners": ["@hunterjm"], "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 84501dbce79..8812e1b8cfe 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1150,7 +1150,7 @@ ondilo==0.2.0 onkyo-eiscp==1.2.7 # homeassistant.components.onvif -onvif-zeep-async==1.2.0 +onvif-zeep-async==1.2.1 # homeassistant.components.opengarage open-garage==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index de9ca72060d..dd2f423a7cc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -794,7 +794,7 @@ omnilogic==0.4.5 ondilo==0.2.0 # homeassistant.components.onvif -onvif-zeep-async==1.2.0 +onvif-zeep-async==1.2.1 # homeassistant.components.opengarage open-garage==0.2.0 From b7a02d946589eaa63ff951ff46f81073fdb652a7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 3 Jul 2022 13:56:29 -0700 Subject: [PATCH 40/71] Bumped version to 2022.7.0b3 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index de1148a3932..c07650c8ac1 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 7 -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, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 92a7a962cbb..57707cf8af1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.7.0b2" +version = "2022.7.0b3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 3aafa0cf498849593f31ed18d2ff09952e600e2b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 4 Jul 2022 14:06:32 +0200 Subject: [PATCH 41/71] Migrate aemet to native_* (#74037) --- homeassistant/components/aemet/__init__.py | 39 ++++++++- homeassistant/components/aemet/const.py | 26 ++++-- homeassistant/components/aemet/sensor.py | 16 ++-- homeassistant/components/aemet/weather.py | 20 +++-- .../aemet/weather_update_coordinator.py | 20 ++--- tests/components/aemet/test_init.py | 84 +++++++++++++++++++ 6 files changed, 168 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/aemet/__init__.py b/homeassistant/components/aemet/__init__.py index a914a23a0da..7b86a5559e0 100644 --- a/homeassistant/components/aemet/__init__.py +++ b/homeassistant/components/aemet/__init__.py @@ -1,18 +1,30 @@ """The AEMET OpenData component.""" +from __future__ import annotations + import logging +from typing import Any from aemet_opendata.interface import AEMET from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME -from homeassistant.core import HomeAssistant +from homeassistant.const import ( + CONF_API_KEY, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_NAME, + Platform, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er from .const import ( CONF_STATION_UPDATES, DOMAIN, ENTRY_NAME, ENTRY_WEATHER_COORDINATOR, + FORECAST_MODES, PLATFORMS, + RENAMED_FORECAST_SENSOR_KEYS, ) from .weather_update_coordinator import WeatherUpdateCoordinator @@ -21,6 +33,8 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up AEMET OpenData as config entry.""" + await er.async_migrate_entries(hass, entry.entry_id, async_migrate_entity_entry) + name = entry.data[CONF_NAME] api_key = entry.data[CONF_API_KEY] latitude = entry.data[CONF_LATITUDE] @@ -60,3 +74,24 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok + + +@callback +def async_migrate_entity_entry(entry: er.RegistryEntry) -> dict[str, Any] | None: + """Migrate AEMET entity entries. + + - Migrates unique ID from old forecast sensors to the new unique ID + """ + if entry.domain != Platform.SENSOR: + return None + for old_key, new_key in RENAMED_FORECAST_SENSOR_KEYS.items(): + for forecast_mode in FORECAST_MODES: + old_suffix = f"-forecast-{forecast_mode}-{old_key}" + if entry.unique_id.endswith(old_suffix): + new_suffix = f"-forecast-{forecast_mode}-{new_key}" + return { + "new_unique_id": entry.unique_id.replace(old_suffix, new_suffix) + } + + # No migration needed + return None diff --git a/homeassistant/components/aemet/const.py b/homeassistant/components/aemet/const.py index 4be90011f5a..48e7335934f 100644 --- a/homeassistant/components/aemet/const.py +++ b/homeassistant/components/aemet/const.py @@ -18,6 +18,10 @@ from homeassistant.components.weather import ( ATTR_CONDITION_SNOWY, ATTR_CONDITION_SUNNY, ATTR_FORECAST_CONDITION, + ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_PRECIPITATION_PROBABILITY, ATTR_FORECAST_TEMP, @@ -159,13 +163,13 @@ CONDITIONS_MAP = { FORECAST_MONITORED_CONDITIONS = [ ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_NATIVE_PRECIPITATION, ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, + ATTR_FORECAST_NATIVE_WIND_SPEED, ] MONITORED_CONDITIONS = [ ATTR_API_CONDITION, @@ -206,7 +210,7 @@ FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( name="Condition", ), SensorEntityDescription( - key=ATTR_FORECAST_PRECIPITATION, + key=ATTR_FORECAST_NATIVE_PRECIPITATION, name="Precipitation", native_unit_of_measurement=PRECIPITATION_MILLIMETERS_PER_HOUR, ), @@ -216,13 +220,13 @@ FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( native_unit_of_measurement=PERCENTAGE, ), SensorEntityDescription( - key=ATTR_FORECAST_TEMP, + key=ATTR_FORECAST_NATIVE_TEMP, name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( - key=ATTR_FORECAST_TEMP_LOW, + key=ATTR_FORECAST_NATIVE_TEMP_LOW, name="Temperature Low", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, @@ -238,11 +242,17 @@ FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( native_unit_of_measurement=DEGREE, ), SensorEntityDescription( - key=ATTR_FORECAST_WIND_SPEED, + key=ATTR_FORECAST_NATIVE_WIND_SPEED, name="Wind speed", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, ), ) +RENAMED_FORECAST_SENSOR_KEYS = { + ATTR_FORECAST_PRECIPITATION: ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_FORECAST_TEMP: ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_TEMP_LOW: ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_WIND_SPEED: ATTR_FORECAST_NATIVE_WIND_SPEED, +} WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key=ATTR_API_CONDITION, diff --git a/homeassistant/components/aemet/sensor.py b/homeassistant/components/aemet/sensor.py index f98e3fff49e..8439b166a47 100644 --- a/homeassistant/components/aemet/sensor.py +++ b/homeassistant/components/aemet/sensor.py @@ -45,17 +45,13 @@ async def async_setup_entry( entities.extend( [ AemetForecastSensor( - name_prefix, - unique_id_prefix, + f"{domain_data[ENTRY_NAME]} {mode} Forecast", + f"{unique_id}-forecast-{mode}", weather_coordinator, mode, description, ) for mode in FORECAST_MODES - if ( - (name_prefix := f"{domain_data[ENTRY_NAME]} {mode} Forecast") - and (unique_id_prefix := f"{unique_id}-forecast-{mode}") - ) for description in FORECAST_SENSOR_TYPES if description.key in FORECAST_MONITORED_CONDITIONS ] @@ -89,14 +85,14 @@ class AemetSensor(AbstractAemetSensor): def __init__( self, name, - unique_id, + unique_id_prefix, weather_coordinator: WeatherUpdateCoordinator, description: SensorEntityDescription, ): """Initialize the sensor.""" super().__init__( name=name, - unique_id=f"{unique_id}-{description.key}", + unique_id=f"{unique_id_prefix}-{description.key}", coordinator=weather_coordinator, description=description, ) @@ -113,7 +109,7 @@ class AemetForecastSensor(AbstractAemetSensor): def __init__( self, name, - unique_id, + unique_id_prefix, weather_coordinator: WeatherUpdateCoordinator, forecast_mode, description: SensorEntityDescription, @@ -121,7 +117,7 @@ class AemetForecastSensor(AbstractAemetSensor): """Initialize the sensor.""" super().__init__( name=name, - unique_id=f"{unique_id}-{description.key}", + unique_id=f"{unique_id_prefix}-{description.key}", coordinator=weather_coordinator, description=description, ) diff --git a/homeassistant/components/aemet/weather.py b/homeassistant/components/aemet/weather.py index d05442b621e..a67726d1f51 100644 --- a/homeassistant/components/aemet/weather.py +++ b/homeassistant/components/aemet/weather.py @@ -1,7 +1,12 @@ """Support for the AEMET OpenData service.""" from homeassistant.components.weather import WeatherEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PRESSURE_HPA, SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS +from homeassistant.const import ( + LENGTH_MILLIMETERS, + PRESSURE_HPA, + SPEED_KILOMETERS_PER_HOUR, + TEMP_CELSIUS, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -47,9 +52,10 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity): """Implementation of an AEMET OpenData sensor.""" _attr_attribution = ATTRIBUTION - _attr_temperature_unit = TEMP_CELSIUS - _attr_pressure_unit = PRESSURE_HPA - _attr_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR + _attr_native_precipitation_unit = LENGTH_MILLIMETERS + _attr_native_pressure_unit = PRESSURE_HPA + _attr_native_temperature_unit = TEMP_CELSIUS + _attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR def __init__( self, @@ -83,12 +89,12 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity): return self.coordinator.data[ATTR_API_HUMIDITY] @property - def pressure(self): + def native_pressure(self): """Return the pressure.""" return self.coordinator.data[ATTR_API_PRESSURE] @property - def temperature(self): + def native_temperature(self): """Return the temperature.""" return self.coordinator.data[ATTR_API_TEMPERATURE] @@ -98,6 +104,6 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity): return self.coordinator.data[ATTR_API_WIND_BEARING] @property - def wind_speed(self): + def native_wind_speed(self): """Return the wind speed.""" return self.coordinator.data[ATTR_API_WIND_SPEED] diff --git a/homeassistant/components/aemet/weather_update_coordinator.py b/homeassistant/components/aemet/weather_update_coordinator.py index c86465ea8f1..4f0bf6ac5ea 100644 --- a/homeassistant/components/aemet/weather_update_coordinator.py +++ b/homeassistant/components/aemet/weather_update_coordinator.py @@ -44,13 +44,13 @@ import async_timeout from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, ) from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import dt as dt_util @@ -406,10 +406,10 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): ATTR_FORECAST_PRECIPITATION_PROBABILITY: self._get_precipitation_prob_day( day ), - ATTR_FORECAST_TEMP: self._get_temperature_day(day), - ATTR_FORECAST_TEMP_LOW: self._get_temperature_low_day(day), + ATTR_FORECAST_NATIVE_TEMP: self._get_temperature_day(day), + ATTR_FORECAST_NATIVE_TEMP_LOW: self._get_temperature_low_day(day), ATTR_FORECAST_TIME: dt_util.as_utc(date).isoformat(), - ATTR_FORECAST_WIND_SPEED: self._get_wind_speed_day(day), + ATTR_FORECAST_NATIVE_WIND_SPEED: self._get_wind_speed_day(day), ATTR_FORECAST_WIND_BEARING: self._get_wind_bearing_day(day), } @@ -421,13 +421,13 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): return { ATTR_FORECAST_CONDITION: condition, - ATTR_FORECAST_PRECIPITATION: self._calc_precipitation(day, hour), + ATTR_FORECAST_NATIVE_PRECIPITATION: self._calc_precipitation(day, hour), ATTR_FORECAST_PRECIPITATION_PROBABILITY: self._calc_precipitation_prob( day, hour ), - ATTR_FORECAST_TEMP: self._get_temperature(day, hour), + ATTR_FORECAST_NATIVE_TEMP: self._get_temperature(day, hour), ATTR_FORECAST_TIME: dt_util.as_utc(forecast_dt).isoformat(), - ATTR_FORECAST_WIND_SPEED: self._get_wind_speed(day, hour), + ATTR_FORECAST_NATIVE_WIND_SPEED: self._get_wind_speed(day, hour), ATTR_FORECAST_WIND_BEARING: self._get_wind_bearing(day, hour), } diff --git a/tests/components/aemet/test_init.py b/tests/components/aemet/test_init.py index b1f452c1b46..8dd177a145d 100644 --- a/tests/components/aemet/test_init.py +++ b/tests/components/aemet/test_init.py @@ -2,11 +2,15 @@ from unittest.mock import patch +import pytest import requests_mock from homeassistant.components.aemet.const import DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er import homeassistant.util.dt as dt_util from .util import aemet_requests_mock @@ -42,3 +46,83 @@ async def test_unload_entry(hass): await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.NOT_LOADED + + +@pytest.mark.parametrize( + "old_unique_id,new_unique_id", + [ + # Sensors which should be migrated + ( + "aemet_unique_id-forecast-daily-precipitation", + "aemet_unique_id-forecast-daily-native_precipitation", + ), + ( + "aemet_unique_id-forecast-daily-temperature", + "aemet_unique_id-forecast-daily-native_temperature", + ), + ( + "aemet_unique_id-forecast-daily-templow", + "aemet_unique_id-forecast-daily-native_templow", + ), + ( + "aemet_unique_id-forecast-daily-wind_speed", + "aemet_unique_id-forecast-daily-native_wind_speed", + ), + ( + "aemet_unique_id-forecast-hourly-precipitation", + "aemet_unique_id-forecast-hourly-native_precipitation", + ), + ( + "aemet_unique_id-forecast-hourly-temperature", + "aemet_unique_id-forecast-hourly-native_temperature", + ), + ( + "aemet_unique_id-forecast-hourly-templow", + "aemet_unique_id-forecast-hourly-native_templow", + ), + ( + "aemet_unique_id-forecast-hourly-wind_speed", + "aemet_unique_id-forecast-hourly-native_wind_speed", + ), + # Already migrated + ( + "aemet_unique_id-forecast-daily-native_templow", + "aemet_unique_id-forecast-daily-native_templow", + ), + # No migration needed + ( + "aemet_unique_id-forecast-daily-condition", + "aemet_unique_id-forecast-daily-condition", + ), + ], +) +async def test_migrate_unique_id_sensor( + hass: HomeAssistant, + old_unique_id: str, + new_unique_id: str, +) -> None: + """Test migration of unique_id.""" + now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00") + with patch("homeassistant.util.dt.now", return_value=now), patch( + "homeassistant.util.dt.utcnow", return_value=now + ), requests_mock.mock() as _m: + aemet_requests_mock(_m) + config_entry = MockConfigEntry( + domain=DOMAIN, unique_id="aemet_unique_id", data=CONFIG + ) + config_entry.add_to_hass(hass) + + entity_registry = er.async_get(hass) + entity: er.RegistryEntry = entity_registry.async_get_or_create( + domain=SENSOR_DOMAIN, + platform=DOMAIN, + unique_id=old_unique_id, + config_entry=config_entry, + ) + assert entity.unique_id == old_unique_id + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + entity_migrated = entity_registry.async_get(entity.entity_id) + assert entity_migrated + assert entity_migrated.unique_id == new_unique_id From a90654bd630b016705557dd33093d9bdce2156e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Br=C3=BCckmann?= Date: Tue, 5 Jul 2022 12:25:20 +0200 Subject: [PATCH 42/71] Fix unreachable DenonAVR reporting as available when polling fails (#74344) --- homeassistant/components/denonavr/media_player.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index 8d3102c441b..85e28c29d7c 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -204,12 +204,14 @@ class DenonDevice(MediaPlayerEntity): ) self._available = False except AvrCommandError as err: + available = False _LOGGER.error( "Command %s failed with error: %s", func.__name__, err, ) except DenonAvrError as err: + available = False _LOGGER.error( "Error %s occurred in method %s for Denon AVR receiver", err, From ce04480e60107ffbcb77141ea67f5aea2df1406a Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Mon, 4 Jul 2022 14:24:21 +0200 Subject: [PATCH 43/71] Support unload for multiple adguard entries (#74360) --- homeassistant/components/adguard/__init__.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/adguard/__init__.py b/homeassistant/components/adguard/__init__.py index 1f2645e227c..2a244a5fe80 100644 --- a/homeassistant/components/adguard/__init__.py +++ b/homeassistant/components/adguard/__init__.py @@ -115,14 +115,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload AdGuard Home config entry.""" - hass.services.async_remove(DOMAIN, SERVICE_ADD_URL) - hass.services.async_remove(DOMAIN, SERVICE_REMOVE_URL) - hass.services.async_remove(DOMAIN, SERVICE_ENABLE_URL) - hass.services.async_remove(DOMAIN, SERVICE_DISABLE_URL) - hass.services.async_remove(DOMAIN, SERVICE_REFRESH) - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + if not hass.data[DOMAIN]: + hass.services.async_remove(DOMAIN, SERVICE_ADD_URL) + hass.services.async_remove(DOMAIN, SERVICE_REMOVE_URL) + hass.services.async_remove(DOMAIN, SERVICE_ENABLE_URL) + hass.services.async_remove(DOMAIN, SERVICE_DISABLE_URL) + hass.services.async_remove(DOMAIN, SERVICE_REFRESH) del hass.data[DOMAIN] return unload_ok From bb844840965b8a866f324595f29486590db10003 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 3 Jul 2022 22:03:13 -0700 Subject: [PATCH 44/71] Guard invalid data sensor significant change (#74369) --- .../components/sensor/significant_change.py | 18 ++++++++++++++---- .../sensor/test_significant_change.py | 2 ++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/significant_change.py b/homeassistant/components/sensor/significant_change.py index 31b4f00c37f..6ff23b43508 100644 --- a/homeassistant/components/sensor/significant_change.py +++ b/homeassistant/components/sensor/significant_change.py @@ -63,13 +63,23 @@ def async_check_significant_change( absolute_change = 1.0 percentage_change = 2.0 + try: + # New state is invalid, don't report it + new_state_f = float(new_state) + except ValueError: + return False + + try: + # Old state was invalid, we should report again + old_state_f = float(old_state) + except ValueError: + return True + if absolute_change is not None and percentage_change is not None: return _absolute_and_relative_change( - float(old_state), float(new_state), absolute_change, percentage_change + old_state_f, new_state_f, absolute_change, percentage_change ) if absolute_change is not None: - return check_absolute_change( - float(old_state), float(new_state), absolute_change - ) + return check_absolute_change(old_state_f, new_state_f, absolute_change) return None diff --git a/tests/components/sensor/test_significant_change.py b/tests/components/sensor/test_significant_change.py index 051a92f3b07..bfa01d6eb08 100644 --- a/tests/components/sensor/test_significant_change.py +++ b/tests/components/sensor/test_significant_change.py @@ -52,6 +52,8 @@ TEMP_FREEDOM_ATTRS = { ("12.1", "12.2", TEMP_CELSIUS_ATTRS, False), ("70", "71", TEMP_FREEDOM_ATTRS, True), ("70", "70.5", TEMP_FREEDOM_ATTRS, False), + ("fail", "70", TEMP_FREEDOM_ATTRS, True), + ("70", "fail", TEMP_FREEDOM_ATTRS, False), ], ) async def test_significant_change_temperature(old_state, new_state, attrs, result): From f0993ca4a85a8cc27995d9cd6e17bd5bf3f91e85 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 4 Jul 2022 10:47:59 +0200 Subject: [PATCH 45/71] Migrate knx weather to native_* (#74386) --- homeassistant/components/knx/weather.py | 39 ++++++++++++------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/knx/weather.py b/homeassistant/components/knx/weather.py index 6e71c09501f..32f37ad2ac2 100644 --- a/homeassistant/components/knx/weather.py +++ b/homeassistant/components/knx/weather.py @@ -6,7 +6,14 @@ from xknx.devices import Weather as XknxWeather from homeassistant import config_entries from homeassistant.components.weather import WeatherEntity -from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME, TEMP_CELSIUS, Platform +from homeassistant.const import ( + CONF_ENTITY_CATEGORY, + CONF_NAME, + PRESSURE_PA, + SPEED_METERS_PER_SECOND, + TEMP_CELSIUS, + Platform, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType @@ -68,7 +75,9 @@ class KNXWeather(KnxEntity, WeatherEntity): """Representation of a KNX weather device.""" _device: XknxWeather - _attr_temperature_unit = TEMP_CELSIUS + _attr_native_pressure_unit = PRESSURE_PA + _attr_native_temperature_unit = TEMP_CELSIUS + _attr_native_wind_speed_unit = SPEED_METERS_PER_SECOND def __init__(self, xknx: XKNX, config: ConfigType) -> None: """Initialize of a KNX sensor.""" @@ -77,19 +86,14 @@ class KNXWeather(KnxEntity, WeatherEntity): self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) @property - def temperature(self) -> float | None: - """Return current temperature.""" + def native_temperature(self) -> float | None: + """Return current temperature in C.""" return self._device.temperature @property - def pressure(self) -> float | None: - """Return current air pressure.""" - # KNX returns pA - HA requires hPa - return ( - self._device.air_pressure / 100 - if self._device.air_pressure is not None - else None - ) + def native_pressure(self) -> float | None: + """Return current air pressure in Pa.""" + return self._device.air_pressure @property def condition(self) -> str: @@ -107,11 +111,6 @@ class KNXWeather(KnxEntity, WeatherEntity): return self._device.wind_bearing @property - def wind_speed(self) -> float | None: - """Return current wind speed in km/h.""" - # KNX only supports wind speed in m/s - return ( - self._device.wind_speed * 3.6 - if self._device.wind_speed is not None - else None - ) + def native_wind_speed(self) -> float | None: + """Return current wind speed in m/s.""" + return self._device.wind_speed From d0a86b3cd20e89cf83d44472d10df0e0e4752060 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 4 Jul 2022 15:18:57 +0200 Subject: [PATCH 46/71] Migrate ipma weather to native_* (#74387) --- homeassistant/components/ipma/weather.py | 33 ++++++++++++------------ 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/ipma/weather.py b/homeassistant/components/ipma/weather.py index 731c3d7fb60..dd585b88802 100644 --- a/homeassistant/components/ipma/weather.py +++ b/homeassistant/components/ipma/weather.py @@ -25,12 +25,12 @@ from homeassistant.components.weather import ( ATTR_CONDITION_WINDY, ATTR_CONDITION_WINDY_VARIANT, ATTR_FORECAST_CONDITION, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, PLATFORM_SCHEMA, WeatherEntity, ) @@ -40,6 +40,8 @@ from homeassistant.const import ( CONF_LONGITUDE, CONF_MODE, CONF_NAME, + PRESSURE_HPA, + SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant, callback @@ -174,6 +176,10 @@ async def async_get_location(hass, api, latitude, longitude): class IPMAWeather(WeatherEntity): """Representation of a weather condition.""" + _attr_native_pressure_unit = PRESSURE_HPA + _attr_native_temperature_unit = TEMP_CELSIUS + _attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR + def __init__(self, location: Location, api: IPMA_API, config): """Initialise the platform with a data instance and station name.""" self._api = api @@ -237,7 +243,7 @@ class IPMAWeather(WeatherEntity): ) @property - def temperature(self): + def native_temperature(self): """Return the current temperature.""" if not self._observation: return None @@ -245,7 +251,7 @@ class IPMAWeather(WeatherEntity): return self._observation.temperature @property - def pressure(self): + def native_pressure(self): """Return the current pressure.""" if not self._observation: return None @@ -261,7 +267,7 @@ class IPMAWeather(WeatherEntity): return self._observation.humidity @property - def wind_speed(self): + def native_wind_speed(self): """Return the current windspeed.""" if not self._observation: return None @@ -276,11 +282,6 @@ class IPMAWeather(WeatherEntity): return self._observation.wind_direction - @property - def temperature_unit(self): - """Return the unit of measurement.""" - return TEMP_CELSIUS - @property def forecast(self): """Return the forecast array.""" @@ -307,13 +308,13 @@ class IPMAWeather(WeatherEntity): ), None, ), - ATTR_FORECAST_TEMP: float(data_in.feels_like_temperature), + ATTR_FORECAST_NATIVE_TEMP: float(data_in.feels_like_temperature), ATTR_FORECAST_PRECIPITATION_PROBABILITY: ( int(float(data_in.precipitation_probability)) if int(float(data_in.precipitation_probability)) >= 0 else None ), - ATTR_FORECAST_WIND_SPEED: data_in.wind_strength, + ATTR_FORECAST_NATIVE_WIND_SPEED: data_in.wind_strength, ATTR_FORECAST_WIND_BEARING: data_in.wind_direction, } for data_in in forecast_filtered @@ -331,10 +332,10 @@ class IPMAWeather(WeatherEntity): ), None, ), - ATTR_FORECAST_TEMP_LOW: data_in.min_temperature, - ATTR_FORECAST_TEMP: data_in.max_temperature, + ATTR_FORECAST_NATIVE_TEMP_LOW: data_in.min_temperature, + ATTR_FORECAST_NATIVE_TEMP: data_in.max_temperature, ATTR_FORECAST_PRECIPITATION_PROBABILITY: data_in.precipitation_probability, - ATTR_FORECAST_WIND_SPEED: data_in.wind_strength, + ATTR_FORECAST_NATIVE_WIND_SPEED: data_in.wind_strength, ATTR_FORECAST_WIND_BEARING: data_in.wind_direction, } for data_in in forecast_filtered From 6ba06c0f5343c2421c998d3d99657a339a3928f6 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 4 Jul 2022 14:20:47 +0200 Subject: [PATCH 47/71] Migrate met_eireann weather to native_* (#74391) Co-authored-by: avee87 <6134677+avee87@users.noreply.github.com> Co-authored-by: Franck Nijhof --- homeassistant/components/met_eireann/const.py | 20 +++---- .../components/met_eireann/weather.py | 59 +++++-------------- 2 files changed, 22 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/met_eireann/const.py b/homeassistant/components/met_eireann/const.py index 98d862183c4..efe80cb9d17 100644 --- a/homeassistant/components/met_eireann/const.py +++ b/homeassistant/components/met_eireann/const.py @@ -1,6 +1,4 @@ """Constants for Met Éireann component.""" -import logging - from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_CLOUDY, @@ -12,13 +10,13 @@ from homeassistant.components.weather import ( ATTR_CONDITION_SNOWY_RAINY, ATTR_CONDITION_SUNNY, ATTR_FORECAST_CONDITION, + ATTR_FORECAST_NATIVE_PRESSURE, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_PRECIPITATION, - ATTR_FORECAST_PRESSURE, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, DOMAIN as WEATHER_DOMAIN, ) @@ -32,17 +30,15 @@ HOME_LOCATION_NAME = "Home" ENTITY_ID_SENSOR_FORMAT_HOME = f"{WEATHER_DOMAIN}.met_eireann_{HOME_LOCATION_NAME}" -_LOGGER = logging.getLogger(".") - FORECAST_MAP = { ATTR_FORECAST_CONDITION: "condition", - ATTR_FORECAST_PRESSURE: "pressure", + ATTR_FORECAST_NATIVE_PRESSURE: "pressure", ATTR_FORECAST_PRECIPITATION: "precipitation", - ATTR_FORECAST_TEMP: "temperature", - ATTR_FORECAST_TEMP_LOW: "templow", + ATTR_FORECAST_NATIVE_TEMP: "temperature", + ATTR_FORECAST_NATIVE_TEMP_LOW: "templow", ATTR_FORECAST_TIME: "datetime", ATTR_FORECAST_WIND_BEARING: "wind_bearing", - ATTR_FORECAST_WIND_SPEED: "wind_speed", + ATTR_FORECAST_NATIVE_WIND_SPEED: "wind_speed", } CONDITION_MAP = { diff --git a/homeassistant/components/met_eireann/weather.py b/homeassistant/components/met_eireann/weather.py index cbf5c99342a..f20f0e1254a 100644 --- a/homeassistant/components/met_eireann/weather.py +++ b/homeassistant/components/met_eireann/weather.py @@ -3,8 +3,6 @@ import logging from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, - ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME, WeatherEntity, ) @@ -13,12 +11,9 @@ from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, - LENGTH_INCHES, LENGTH_MILLIMETERS, PRESSURE_HPA, - PRESSURE_INHG, SPEED_METERS_PER_SECOND, - SPEED_MILES_PER_HOUR, TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant @@ -27,9 +22,6 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import dt as dt_util -from homeassistant.util.distance import convert as convert_distance -from homeassistant.util.pressure import convert as convert_pressure -from homeassistant.util.speed import convert as convert_speed from .const import ATTRIBUTION, CONDITION_MAP, DEFAULT_NAME, DOMAIN, FORECAST_MAP @@ -54,12 +46,8 @@ async def async_setup_entry( coordinator = hass.data[DOMAIN][config_entry.entry_id] async_add_entities( [ - MetEireannWeather( - coordinator, config_entry.data, hass.config.units.is_metric, False - ), - MetEireannWeather( - coordinator, config_entry.data, hass.config.units.is_metric, True - ), + MetEireannWeather(coordinator, config_entry.data, False), + MetEireannWeather(coordinator, config_entry.data, True), ] ) @@ -67,11 +55,15 @@ async def async_setup_entry( class MetEireannWeather(CoordinatorEntity, WeatherEntity): """Implementation of a Met Éireann weather condition.""" - def __init__(self, coordinator, config, is_metric, hourly): + _attr_native_precipitation_unit = LENGTH_MILLIMETERS + _attr_native_pressure_unit = PRESSURE_HPA + _attr_native_temperature_unit = TEMP_CELSIUS + _attr_native_wind_speed_unit = SPEED_METERS_PER_SECOND + + def __init__(self, coordinator, config, hourly): """Initialise the platform with a data instance and site.""" super().__init__(coordinator) self._config = config - self._is_metric = is_metric self._hourly = hourly @property @@ -109,23 +101,14 @@ class MetEireannWeather(CoordinatorEntity, WeatherEntity): ) @property - def temperature(self): + def native_temperature(self): """Return the temperature.""" return self.coordinator.data.current_weather_data.get("temperature") @property - def temperature_unit(self): - """Return the unit of measurement.""" - return TEMP_CELSIUS - - @property - def pressure(self): + def native_pressure(self): """Return the pressure.""" - pressure_hpa = self.coordinator.data.current_weather_data.get("pressure") - if self._is_metric or pressure_hpa is None: - return pressure_hpa - - return round(convert_pressure(pressure_hpa, PRESSURE_HPA, PRESSURE_INHG), 2) + return self.coordinator.data.current_weather_data.get("pressure") @property def humidity(self): @@ -133,16 +116,9 @@ class MetEireannWeather(CoordinatorEntity, WeatherEntity): return self.coordinator.data.current_weather_data.get("humidity") @property - def wind_speed(self): + def native_wind_speed(self): """Return the wind speed.""" - speed_m_s = self.coordinator.data.current_weather_data.get("wind_speed") - if self._is_metric or speed_m_s is None: - return speed_m_s - - speed_mi_h = convert_speed( - speed_m_s, SPEED_METERS_PER_SECOND, SPEED_MILES_PER_HOUR - ) - return int(round(speed_mi_h)) + return self.coordinator.data.current_weather_data.get("wind_speed") @property def wind_bearing(self): @@ -161,7 +137,7 @@ class MetEireannWeather(CoordinatorEntity, WeatherEntity): me_forecast = self.coordinator.data.hourly_forecast else: me_forecast = self.coordinator.data.daily_forecast - required_keys = {ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME} + required_keys = {"temperature", "datetime"} ha_forecast = [] @@ -171,13 +147,6 @@ class MetEireannWeather(CoordinatorEntity, WeatherEntity): ha_item = { k: item[v] for k, v in FORECAST_MAP.items() if item.get(v) is not None } - if not self._is_metric and ATTR_FORECAST_PRECIPITATION in ha_item: - precip_inches = convert_distance( - ha_item[ATTR_FORECAST_PRECIPITATION], - LENGTH_MILLIMETERS, - LENGTH_INCHES, - ) - ha_item[ATTR_FORECAST_PRECIPITATION] = round(precip_inches, 2) if ha_item.get(ATTR_FORECAST_CONDITION): ha_item[ATTR_FORECAST_CONDITION] = format_condition( ha_item[ATTR_FORECAST_CONDITION] From 25639ccf25b81099a3230133cc4498e65d1e2560 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 4 Jul 2022 13:38:53 +0200 Subject: [PATCH 48/71] Migrate meteoclimatic weather to native_* (#74392) --- .../components/meteoclimatic/weather.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/meteoclimatic/weather.py b/homeassistant/components/meteoclimatic/weather.py index 4faecdaa3ac..8044dd04aa8 100644 --- a/homeassistant/components/meteoclimatic/weather.py +++ b/homeassistant/components/meteoclimatic/weather.py @@ -3,7 +3,7 @@ from meteoclimatic import Condition from homeassistant.components.weather import WeatherEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import PRESSURE_HPA, SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo @@ -38,6 +38,10 @@ async def async_setup_entry( class MeteoclimaticWeather(CoordinatorEntity, WeatherEntity): """Representation of a weather condition.""" + _attr_native_pressure_unit = PRESSURE_HPA + _attr_native_temperature_unit = TEMP_CELSIUS + _attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR + def __init__(self, coordinator: DataUpdateCoordinator) -> None: """Initialise the weather platform.""" super().__init__(coordinator) @@ -71,27 +75,22 @@ class MeteoclimaticWeather(CoordinatorEntity, WeatherEntity): return format_condition(self.coordinator.data["weather"].condition) @property - def temperature(self): + def native_temperature(self): """Return the temperature.""" return self.coordinator.data["weather"].temp_current - @property - def temperature_unit(self): - """Return the unit of measurement.""" - return TEMP_CELSIUS - @property def humidity(self): """Return the humidity.""" return self.coordinator.data["weather"].humidity_current @property - def pressure(self): + def native_pressure(self): """Return the pressure.""" return self.coordinator.data["weather"].pressure_current @property - def wind_speed(self): + def native_wind_speed(self): """Return the wind speed.""" return self.coordinator.data["weather"].wind_current From db83c784786c4553f61002c25fc8eca50aca52b2 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 4 Jul 2022 14:14:27 +0300 Subject: [PATCH 49/71] Bump aioimaplib to 1.0.0 (#74393) --- homeassistant/components/imap/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/imap/manifest.json b/homeassistant/components/imap/manifest.json index 655590005bf..f4bbadfa6ac 100644 --- a/homeassistant/components/imap/manifest.json +++ b/homeassistant/components/imap/manifest.json @@ -2,7 +2,7 @@ "domain": "imap", "name": "IMAP", "documentation": "https://www.home-assistant.io/integrations/imap", - "requirements": ["aioimaplib==0.9.0"], + "requirements": ["aioimaplib==1.0.0"], "codeowners": [], "iot_class": "cloud_push", "loggers": ["aioimaplib"] diff --git a/requirements_all.txt b/requirements_all.txt index 8812e1b8cfe..8956b7ba238 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -178,7 +178,7 @@ aiohttp_cors==0.7.0 aiohue==4.4.2 # homeassistant.components.imap -aioimaplib==0.9.0 +aioimaplib==1.0.0 # homeassistant.components.apache_kafka aiokafka==0.6.0 From 3ff0218326105c16ed634091882013672326ca24 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 4 Jul 2022 15:12:45 +0200 Subject: [PATCH 50/71] Migrate accuweather weather to native_* (#74407) --- .../components/accuweather/weather.py | 57 ++++++++++++------- 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/accuweather/weather.py b/homeassistant/components/accuweather/weather.py index 536f66a3cb9..ae1824aef4a 100644 --- a/homeassistant/components/accuweather/weather.py +++ b/homeassistant/components/accuweather/weather.py @@ -6,19 +6,26 @@ from typing import Any, cast from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, Forecast, WeatherEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_NAME, + LENGTH_INCHES, + LENGTH_KILOMETERS, + LENGTH_MILES, + LENGTH_MILLIMETERS, + PRESSURE_HPA, + PRESSURE_INHG, + SPEED_KILOMETERS_PER_HOUR, SPEED_MILES_PER_HOUR, TEMP_CELSIUS, TEMP_FAHRENHEIT, @@ -66,19 +73,25 @@ class AccuWeatherEntity( ) -> None: """Initialize.""" super().__init__(coordinator) - self._unit_system = API_METRIC if coordinator.is_metric else API_IMPERIAL - wind_speed_unit = self.coordinator.data["Wind"]["Speed"][self._unit_system][ - "Unit" - ] - if wind_speed_unit == "mi/h": - self._attr_wind_speed_unit = SPEED_MILES_PER_HOUR + # Coordinator data is used also for sensors which don't have units automatically + # converted, hence the weather entity's native units follow the configured unit + # system + if coordinator.is_metric: + self._attr_native_precipitation_unit = LENGTH_MILLIMETERS + self._attr_native_pressure_unit = PRESSURE_HPA + self._attr_native_temperature_unit = TEMP_CELSIUS + self._attr_native_visibility_unit = LENGTH_KILOMETERS + self._attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR + self._unit_system = API_METRIC else: - self._attr_wind_speed_unit = wind_speed_unit + self._unit_system = API_IMPERIAL + self._attr_native_precipitation_unit = LENGTH_INCHES + self._attr_native_pressure_unit = PRESSURE_INHG + self._attr_native_temperature_unit = TEMP_FAHRENHEIT + self._attr_native_visibility_unit = LENGTH_MILES + self._attr_native_wind_speed_unit = SPEED_MILES_PER_HOUR self._attr_name = name self._attr_unique_id = coordinator.location_key - self._attr_temperature_unit = ( - TEMP_CELSIUS if coordinator.is_metric else TEMP_FAHRENHEIT - ) self._attr_attribution = ATTRIBUTION self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, @@ -106,14 +119,14 @@ class AccuWeatherEntity( return None @property - def temperature(self) -> float: + def native_temperature(self) -> float: """Return the temperature.""" return cast( float, self.coordinator.data["Temperature"][self._unit_system]["Value"] ) @property - def pressure(self) -> float: + def native_pressure(self) -> float: """Return the pressure.""" return cast( float, self.coordinator.data["Pressure"][self._unit_system]["Value"] @@ -125,7 +138,7 @@ class AccuWeatherEntity( return cast(int, self.coordinator.data["RelativeHumidity"]) @property - def wind_speed(self) -> float: + def native_wind_speed(self) -> float: """Return the wind speed.""" return cast( float, self.coordinator.data["Wind"]["Speed"][self._unit_system]["Value"] @@ -137,7 +150,7 @@ class AccuWeatherEntity( return cast(int, self.coordinator.data["Wind"]["Direction"]["Degrees"]) @property - def visibility(self) -> float: + def native_visibility(self) -> float: """Return the visibility.""" return cast( float, self.coordinator.data["Visibility"][self._unit_system]["Value"] @@ -162,9 +175,9 @@ class AccuWeatherEntity( return [ { ATTR_FORECAST_TIME: utc_from_timestamp(item["EpochDate"]).isoformat(), - ATTR_FORECAST_TEMP: item["TemperatureMax"]["Value"], - ATTR_FORECAST_TEMP_LOW: item["TemperatureMin"]["Value"], - ATTR_FORECAST_PRECIPITATION: self._calc_precipitation(item), + ATTR_FORECAST_NATIVE_TEMP: item["TemperatureMax"]["Value"], + ATTR_FORECAST_NATIVE_TEMP_LOW: item["TemperatureMin"]["Value"], + ATTR_FORECAST_NATIVE_PRECIPITATION: self._calc_precipitation(item), ATTR_FORECAST_PRECIPITATION_PROBABILITY: round( mean( [ @@ -173,7 +186,7 @@ class AccuWeatherEntity( ] ) ), - ATTR_FORECAST_WIND_SPEED: item["WindDay"]["Speed"]["Value"], + ATTR_FORECAST_NATIVE_WIND_SPEED: item["WindDay"]["Speed"]["Value"], ATTR_FORECAST_WIND_BEARING: item["WindDay"]["Direction"]["Degrees"], ATTR_FORECAST_CONDITION: [ k for k, v in CONDITION_CLASSES.items() if item["IconDay"] in v From edb5c7d2c88cb20e1fdd43f6fbbb1e77055f30f3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 4 Jul 2022 16:59:36 +0200 Subject: [PATCH 51/71] Correct climacell weather migration to native_* (#74409) --- homeassistant/components/climacell/weather.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/climacell/weather.py b/homeassistant/components/climacell/weather.py index 2b284114981..6aee9b54f6c 100644 --- a/homeassistant/components/climacell/weather.py +++ b/homeassistant/components/climacell/weather.py @@ -10,13 +10,13 @@ from pyclimacell.const import CURRENT, DAILY, FORECASTS, HOURLY, NOWCAST from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, WeatherEntity, ) from homeassistant.config_entries import ConfigEntry @@ -135,12 +135,12 @@ class BaseClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity): data = { ATTR_FORECAST_TIME: forecast_dt.isoformat(), ATTR_FORECAST_CONDITION: translated_condition, - ATTR_FORECAST_PRECIPITATION: precipitation, + ATTR_FORECAST_NATIVE_PRECIPITATION: precipitation, ATTR_FORECAST_PRECIPITATION_PROBABILITY: precipitation_probability, - ATTR_FORECAST_TEMP: temp, - ATTR_FORECAST_TEMP_LOW: temp_low, + ATTR_FORECAST_NATIVE_TEMP: temp, + ATTR_FORECAST_NATIVE_TEMP_LOW: temp_low, ATTR_FORECAST_WIND_BEARING: wind_direction, - ATTR_FORECAST_WIND_SPEED: wind_speed, + ATTR_FORECAST_NATIVE_WIND_SPEED: wind_speed, } return {k: v for k, v in data.items() if v is not None} @@ -224,7 +224,7 @@ class ClimaCellV3WeatherEntity(BaseClimaCellWeatherEntity): return CONDITIONS_V3[condition] @property - def temperature(self): + def native_temperature(self): """Return the platform temperature.""" return self._get_cc_value( self.coordinator.data[CURRENT], CC_V3_ATTR_TEMPERATURE From 82b9eae88218bb98756bc85acdb188ffeef9d885 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 4 Jul 2022 10:36:56 -0500 Subject: [PATCH 52/71] Bump rflink to 0.0.63 (#74417) --- homeassistant/components/rflink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rflink/manifest.json b/homeassistant/components/rflink/manifest.json index debc12ae4e0..6cef409a736 100644 --- a/homeassistant/components/rflink/manifest.json +++ b/homeassistant/components/rflink/manifest.json @@ -2,7 +2,7 @@ "domain": "rflink", "name": "RFLink", "documentation": "https://www.home-assistant.io/integrations/rflink", - "requirements": ["rflink==0.0.62"], + "requirements": ["rflink==0.0.63"], "codeowners": ["@javicalle"], "iot_class": "assumed_state", "loggers": ["rflink"] diff --git a/requirements_all.txt b/requirements_all.txt index 8956b7ba238..bcf326c4817 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2077,7 +2077,7 @@ restrictedpython==5.2 rfk101py==0.0.1 # homeassistant.components.rflink -rflink==0.0.62 +rflink==0.0.63 # homeassistant.components.ring ring_doorbell==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dd2f423a7cc..3ab1015f872 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1385,7 +1385,7 @@ renault-api==0.1.11 restrictedpython==5.2 # homeassistant.components.rflink -rflink==0.0.62 +rflink==0.0.63 # homeassistant.components.ring ring_doorbell==0.7.2 From 54516ee9392f1ba5e74f027f08d5d332f2bbf82e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 4 Jul 2022 13:53:25 -0500 Subject: [PATCH 53/71] Bump pyunifiprotect to 4.0.9 (#74424) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index da82871d313..9aeb8b48050 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==4.0.8", "unifi-discovery==1.1.4"], + "requirements": ["pyunifiprotect==4.0.9", "unifi-discovery==1.1.4"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index bcf326c4817..0fbbfa8a154 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1993,7 +1993,7 @@ pytrafikverket==0.2.0.1 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==4.0.8 +pyunifiprotect==4.0.9 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3ab1015f872..72c7ccb569c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1331,7 +1331,7 @@ pytrafikverket==0.2.0.1 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==4.0.8 +pyunifiprotect==4.0.9 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From 18b3ffbf9932473a8a58625fd28a1dae5aa5b17b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 4 Jul 2022 21:10:26 +0200 Subject: [PATCH 54/71] Remove lutron_caseta from mypy ignore list (#74427) --- homeassistant/components/lutron_caseta/__init__.py | 13 +++++++------ homeassistant/components/lutron_caseta/cover.py | 4 ++-- .../components/lutron_caseta/device_trigger.py | 6 ++++-- homeassistant/components/lutron_caseta/models.py | 4 +--- mypy.ini | 9 --------- script/hassfest/mypy_config.py | 3 --- 6 files changed, 14 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index b4ce82a36c6..c6ad8781478 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -215,10 +215,10 @@ def _async_register_button_devices( config_entry_id: str, bridge_device, button_devices_by_id: dict[int, dict], -) -> dict[str, dr.DeviceEntry]: +) -> dict[str, dict]: """Register button devices (Pico Remotes) in the device registry.""" device_registry = dr.async_get(hass) - button_devices_by_dr_id = {} + button_devices_by_dr_id: dict[str, dict] = {} seen = set() for device in button_devices_by_id.values(): @@ -226,7 +226,7 @@ def _async_register_button_devices( continue seen.add(device["serial"]) area, name = _area_and_name_from_name(device["name"]) - device_args = { + device_args: dict[str, Any] = { "name": f"{area} {name}", "manufacturer": MANUFACTURER, "config_entry_id": config_entry_id, @@ -246,7 +246,8 @@ def _async_register_button_devices( def _area_and_name_from_name(device_name: str) -> tuple[str, str]: """Return the area and name from the devices internal name.""" if "_" in device_name: - return device_name.split("_", 1) + area_device_name = device_name.split("_", 1) + return area_device_name[0], area_device_name[1] return UNASSIGNED_AREA, device_name @@ -382,13 +383,13 @@ class LutronCasetaDevice(Entity): class LutronCasetaDeviceUpdatableEntity(LutronCasetaDevice): """A lutron_caseta entity that can update by syncing data from the bridge.""" - async def async_update(self): + async def async_update(self) -> None: """Update when forcing a refresh of the device.""" self._device = self._smartbridge.get_device_by_id(self.device_id) _LOGGER.debug(self._device) -def _id_to_identifier(lutron_id: str) -> None: +def _id_to_identifier(lutron_id: str) -> tuple[str, str]: """Convert a lutron caseta identifier to a device identifier.""" return (DOMAIN, lutron_id) diff --git a/homeassistant/components/lutron_caseta/cover.py b/homeassistant/components/lutron_caseta/cover.py index b74642a8589..d63c1191d57 100644 --- a/homeassistant/components/lutron_caseta/cover.py +++ b/homeassistant/components/lutron_caseta/cover.py @@ -66,13 +66,13 @@ class LutronCasetaCover(LutronCasetaDeviceUpdatableEntity, CoverEntity): async def async_close_cover(self, **kwargs: Any) -> None: """Close the cover.""" await self._smartbridge.lower_cover(self.device_id) - self.async_update() + await self.async_update() self.async_write_ha_state() async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" await self._smartbridge.raise_cover(self.device_id) - self.async_update() + await self.async_update() self.async_write_ha_state() async def async_set_cover_position(self, **kwargs: Any) -> None: diff --git a/homeassistant/components/lutron_caseta/device_trigger.py b/homeassistant/components/lutron_caseta/device_trigger.py index e762e79a8d7..ed809e0994a 100644 --- a/homeassistant/components/lutron_caseta/device_trigger.py +++ b/homeassistant/components/lutron_caseta/device_trigger.py @@ -386,10 +386,12 @@ async def async_attach_trigger( """Attach a trigger.""" device_registry = dr.async_get(hass) device = device_registry.async_get(config[CONF_DEVICE_ID]) + assert device + assert device.model device_type = _device_model_to_type(device.model) _, serial = list(device.identifiers)[0] - schema = DEVICE_TYPE_SCHEMA_MAP.get(device_type) - valid_buttons = DEVICE_TYPE_SUBTYPE_MAP_TO_LIP.get(device_type) + schema = DEVICE_TYPE_SCHEMA_MAP[device_type] + valid_buttons = DEVICE_TYPE_SUBTYPE_MAP_TO_LIP[device_type] config = schema(config) event_config = { event_trigger.CONF_PLATFORM: CONF_EVENT, diff --git a/homeassistant/components/lutron_caseta/models.py b/homeassistant/components/lutron_caseta/models.py index 5845c888a2e..362760b0caf 100644 --- a/homeassistant/components/lutron_caseta/models.py +++ b/homeassistant/components/lutron_caseta/models.py @@ -6,8 +6,6 @@ from typing import Any from pylutron_caseta.smartbridge import Smartbridge -from homeassistant.helpers.device_registry import DeviceEntry - @dataclass class LutronCasetaData: @@ -15,4 +13,4 @@ class LutronCasetaData: bridge: Smartbridge bridge_device: dict[str, Any] - button_devices: dict[str, DeviceEntry] + button_devices: dict[str, dict] diff --git a/mypy.ini b/mypy.ini index fb67983a31c..2b657041865 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2756,15 +2756,6 @@ ignore_errors = true [mypy-homeassistant.components.lovelace.websocket] ignore_errors = true -[mypy-homeassistant.components.lutron_caseta] -ignore_errors = true - -[mypy-homeassistant.components.lutron_caseta.device_trigger] -ignore_errors = true - -[mypy-homeassistant.components.lutron_caseta.switch] -ignore_errors = true - [mypy-homeassistant.components.lyric.climate] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index bbb628a76bb..b4df9e00495 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -64,9 +64,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.lovelace.dashboard", "homeassistant.components.lovelace.resources", "homeassistant.components.lovelace.websocket", - "homeassistant.components.lutron_caseta", - "homeassistant.components.lutron_caseta.device_trigger", - "homeassistant.components.lutron_caseta.switch", "homeassistant.components.lyric.climate", "homeassistant.components.lyric.config_flow", "homeassistant.components.lyric.sensor", From c933a49c719b8952ed3e3e9a5c01779015c6477b Mon Sep 17 00:00:00 2001 From: Arne Mauer Date: Tue, 5 Jul 2022 10:35:05 +0200 Subject: [PATCH 55/71] Fix multi_match to match with the IKEA airpurifier channel (#74432) Fix multi_match for FilterLifeTime, device_run_time, filter_run_time sensors for ikea starkvind --- homeassistant/components/zha/number.py | 6 +----- homeassistant/components/zha/sensor.py | 16 ++-------------- 2 files changed, 3 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/zha/number.py b/homeassistant/components/zha/number.py index c3d7f352318..e1268e29190 100644 --- a/homeassistant/components/zha/number.py +++ b/homeassistant/components/zha/number.py @@ -526,11 +526,7 @@ class TimerDurationMinutes(ZHANumberConfigurationEntity, id_suffix="timer_durati @CONFIG_DIAGNOSTIC_MATCH( - channel_names="ikea_manufacturer", - manufacturers={ - "IKEA of Sweden", - }, - models={"STARKVIND Air purifier"}, + channel_names="ikea_airpurifier", models={"STARKVIND Air purifier"} ) class FilterLifeTime(ZHANumberConfigurationEntity, id_suffix="filter_life_time"): """Representation of a ZHA timer duration configuration entity.""" diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 2fe38193ecb..4a4700b3c4c 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -810,13 +810,7 @@ class TimeLeft(Sensor, id_suffix="time_left"): _unit = TIME_MINUTES -@MULTI_MATCH( - channel_names="ikea_manufacturer", - manufacturers={ - "IKEA of Sweden", - }, - models={"STARKVIND Air purifier"}, -) +@MULTI_MATCH(channel_names="ikea_airpurifier", models={"STARKVIND Air purifier"}) class IkeaDeviceRunTime(Sensor, id_suffix="device_run_time"): """Sensor that displays device run time (in minutes).""" @@ -826,13 +820,7 @@ class IkeaDeviceRunTime(Sensor, id_suffix="device_run_time"): _unit = TIME_MINUTES -@MULTI_MATCH( - channel_names="ikea_manufacturer", - manufacturers={ - "IKEA of Sweden", - }, - models={"STARKVIND Air purifier"}, -) +@MULTI_MATCH(channel_names="ikea_airpurifier", models={"STARKVIND Air purifier"}) class IkeaFilterRunTime(Sensor, id_suffix="filter_run_time"): """Sensor that displays run time of the current filter (in minutes).""" From c79b741971cf8633d4e6b6e58f5d36f773db312f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 5 Jul 2022 13:41:33 +0200 Subject: [PATCH 56/71] Re-introduce default scan interval in Scrape sensor (#74455) --- homeassistant/components/scrape/sensor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index e15f7c5ba97..88c9b564b29 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -1,6 +1,7 @@ """Support for getting data from websites with scraping.""" from __future__ import annotations +from datetime import timedelta import logging from typing import Any @@ -39,6 +40,8 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) +SCAN_INTERVAL = timedelta(minutes=10) + CONF_ATTR = "attribute" CONF_SELECT = "select" CONF_INDEX = "index" From 98d5c415b3d5aeff696b5f193e8744a8c1718448 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 5 Jul 2022 08:50:00 -0700 Subject: [PATCH 57/71] Bumped version to 2022.7.0b4 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index c07650c8ac1..5a8d761e83b 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 7 -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, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 57707cf8af1..27cd670e809 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.7.0b3" +version = "2022.7.0b4" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 59aba0bc75d92b4ba332a76ce4409cc811ed8003 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 5 Jul 2022 12:43:38 -0500 Subject: [PATCH 58/71] Bump aiohomekit to 0.7.19 (#74463) --- 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 3b3c5e51cf8..a15c576c313 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.7.18"], + "requirements": ["aiohomekit==0.7.19"], "zeroconf": ["_hap._tcp.local."], "after_dependencies": ["zeroconf"], "codeowners": ["@Jc2k", "@bdraco"], diff --git a/requirements_all.txt b/requirements_all.txt index 0fbbfa8a154..d280f382e74 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==0.7.18 +aiohomekit==0.7.19 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 72c7ccb569c..98e516d4336 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==0.7.18 +aiohomekit==0.7.19 # homeassistant.components.emulated_hue # homeassistant.components.http From 43fe351f1bac2a821296986b85bde1d35a63f096 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 5 Jul 2022 12:27:27 -0500 Subject: [PATCH 59/71] Avoid loading mqtt for type checking (#74464) --- homeassistant/helpers/config_entry_flow.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index 1190e947eba..3617c0b1f29 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -6,7 +6,7 @@ import logging from typing import TYPE_CHECKING, Any, Generic, TypeVar, Union, cast from homeassistant import config_entries -from homeassistant.components import dhcp, mqtt, onboarding, ssdp, zeroconf +from homeassistant.components import dhcp, onboarding, ssdp, zeroconf from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult @@ -15,6 +15,9 @@ from .typing import UNDEFINED, DiscoveryInfoType, UndefinedType if TYPE_CHECKING: import asyncio + from homeassistant.components import mqtt + + _R = TypeVar("_R", bound="Awaitable[bool] | bool") DiscoveryFunctionType = Callable[[HomeAssistant], _R] From 9cbb684d50664332cff6c41b6605f47f459eec86 Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Tue, 5 Jul 2022 12:43:10 -0500 Subject: [PATCH 60/71] Bump Frontend to 20220705.0 (#74467) --- 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 27ff0a73f20..6b378fe1098 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220630.0"], + "requirements": ["home-assistant-frontend==20220705.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7ee5a9fe8d3..00f5c776712 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220630.0 +home-assistant-frontend==20220705.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index d280f382e74..726c499b88c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -828,7 +828,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220630.0 +home-assistant-frontend==20220705.0 # homeassistant.components.home_connect homeconnect==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 98e516d4336..0a7d822a5d9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -595,7 +595,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220630.0 +home-assistant-frontend==20220705.0 # homeassistant.components.home_connect homeconnect==0.7.1 From 89360516d74fdb18f43b212e700f24d74f364ff9 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 5 Jul 2022 22:24:08 +0200 Subject: [PATCH 61/71] Revert "Migrate aemet to native_*" (#74471) --- homeassistant/components/aemet/__init__.py | 39 +-------- homeassistant/components/aemet/const.py | 26 ++---- homeassistant/components/aemet/sensor.py | 16 ++-- homeassistant/components/aemet/weather.py | 20 ++--- .../aemet/weather_update_coordinator.py | 20 ++--- tests/components/aemet/test_init.py | 84 ------------------- 6 files changed, 37 insertions(+), 168 deletions(-) diff --git a/homeassistant/components/aemet/__init__.py b/homeassistant/components/aemet/__init__.py index 7b86a5559e0..a914a23a0da 100644 --- a/homeassistant/components/aemet/__init__.py +++ b/homeassistant/components/aemet/__init__.py @@ -1,30 +1,18 @@ """The AEMET OpenData component.""" -from __future__ import annotations - import logging -from typing import Any from aemet_opendata.interface import AEMET from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_API_KEY, - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_NAME, - Platform, -) -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import entity_registry as er +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.core import HomeAssistant from .const import ( CONF_STATION_UPDATES, DOMAIN, ENTRY_NAME, ENTRY_WEATHER_COORDINATOR, - FORECAST_MODES, PLATFORMS, - RENAMED_FORECAST_SENSOR_KEYS, ) from .weather_update_coordinator import WeatherUpdateCoordinator @@ -33,8 +21,6 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up AEMET OpenData as config entry.""" - await er.async_migrate_entries(hass, entry.entry_id, async_migrate_entity_entry) - name = entry.data[CONF_NAME] api_key = entry.data[CONF_API_KEY] latitude = entry.data[CONF_LATITUDE] @@ -74,24 +60,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok - - -@callback -def async_migrate_entity_entry(entry: er.RegistryEntry) -> dict[str, Any] | None: - """Migrate AEMET entity entries. - - - Migrates unique ID from old forecast sensors to the new unique ID - """ - if entry.domain != Platform.SENSOR: - return None - for old_key, new_key in RENAMED_FORECAST_SENSOR_KEYS.items(): - for forecast_mode in FORECAST_MODES: - old_suffix = f"-forecast-{forecast_mode}-{old_key}" - if entry.unique_id.endswith(old_suffix): - new_suffix = f"-forecast-{forecast_mode}-{new_key}" - return { - "new_unique_id": entry.unique_id.replace(old_suffix, new_suffix) - } - - # No migration needed - return None diff --git a/homeassistant/components/aemet/const.py b/homeassistant/components/aemet/const.py index 48e7335934f..4be90011f5a 100644 --- a/homeassistant/components/aemet/const.py +++ b/homeassistant/components/aemet/const.py @@ -18,10 +18,6 @@ from homeassistant.components.weather import ( ATTR_CONDITION_SNOWY, ATTR_CONDITION_SUNNY, ATTR_FORECAST_CONDITION, - ATTR_FORECAST_NATIVE_PRECIPITATION, - ATTR_FORECAST_NATIVE_TEMP, - ATTR_FORECAST_NATIVE_TEMP_LOW, - ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_PRECIPITATION_PROBABILITY, ATTR_FORECAST_TEMP, @@ -163,13 +159,13 @@ CONDITIONS_MAP = { FORECAST_MONITORED_CONDITIONS = [ ATTR_FORECAST_CONDITION, - ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_NATIVE_TEMP, - ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_NATIVE_WIND_SPEED, + ATTR_FORECAST_WIND_SPEED, ] MONITORED_CONDITIONS = [ ATTR_API_CONDITION, @@ -210,7 +206,7 @@ FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( name="Condition", ), SensorEntityDescription( - key=ATTR_FORECAST_NATIVE_PRECIPITATION, + key=ATTR_FORECAST_PRECIPITATION, name="Precipitation", native_unit_of_measurement=PRECIPITATION_MILLIMETERS_PER_HOUR, ), @@ -220,13 +216,13 @@ FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( native_unit_of_measurement=PERCENTAGE, ), SensorEntityDescription( - key=ATTR_FORECAST_NATIVE_TEMP, + key=ATTR_FORECAST_TEMP, name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( - key=ATTR_FORECAST_NATIVE_TEMP_LOW, + key=ATTR_FORECAST_TEMP_LOW, name="Temperature Low", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, @@ -242,17 +238,11 @@ FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( native_unit_of_measurement=DEGREE, ), SensorEntityDescription( - key=ATTR_FORECAST_NATIVE_WIND_SPEED, + key=ATTR_FORECAST_WIND_SPEED, name="Wind speed", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, ), ) -RENAMED_FORECAST_SENSOR_KEYS = { - ATTR_FORECAST_PRECIPITATION: ATTR_FORECAST_NATIVE_PRECIPITATION, - ATTR_FORECAST_TEMP: ATTR_FORECAST_NATIVE_TEMP, - ATTR_FORECAST_TEMP_LOW: ATTR_FORECAST_NATIVE_TEMP_LOW, - ATTR_FORECAST_WIND_SPEED: ATTR_FORECAST_NATIVE_WIND_SPEED, -} WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key=ATTR_API_CONDITION, diff --git a/homeassistant/components/aemet/sensor.py b/homeassistant/components/aemet/sensor.py index 8439b166a47..f98e3fff49e 100644 --- a/homeassistant/components/aemet/sensor.py +++ b/homeassistant/components/aemet/sensor.py @@ -45,13 +45,17 @@ async def async_setup_entry( entities.extend( [ AemetForecastSensor( - f"{domain_data[ENTRY_NAME]} {mode} Forecast", - f"{unique_id}-forecast-{mode}", + name_prefix, + unique_id_prefix, weather_coordinator, mode, description, ) for mode in FORECAST_MODES + if ( + (name_prefix := f"{domain_data[ENTRY_NAME]} {mode} Forecast") + and (unique_id_prefix := f"{unique_id}-forecast-{mode}") + ) for description in FORECAST_SENSOR_TYPES if description.key in FORECAST_MONITORED_CONDITIONS ] @@ -85,14 +89,14 @@ class AemetSensor(AbstractAemetSensor): def __init__( self, name, - unique_id_prefix, + unique_id, weather_coordinator: WeatherUpdateCoordinator, description: SensorEntityDescription, ): """Initialize the sensor.""" super().__init__( name=name, - unique_id=f"{unique_id_prefix}-{description.key}", + unique_id=f"{unique_id}-{description.key}", coordinator=weather_coordinator, description=description, ) @@ -109,7 +113,7 @@ class AemetForecastSensor(AbstractAemetSensor): def __init__( self, name, - unique_id_prefix, + unique_id, weather_coordinator: WeatherUpdateCoordinator, forecast_mode, description: SensorEntityDescription, @@ -117,7 +121,7 @@ class AemetForecastSensor(AbstractAemetSensor): """Initialize the sensor.""" super().__init__( name=name, - unique_id=f"{unique_id_prefix}-{description.key}", + unique_id=f"{unique_id}-{description.key}", coordinator=weather_coordinator, description=description, ) diff --git a/homeassistant/components/aemet/weather.py b/homeassistant/components/aemet/weather.py index a67726d1f51..d05442b621e 100644 --- a/homeassistant/components/aemet/weather.py +++ b/homeassistant/components/aemet/weather.py @@ -1,12 +1,7 @@ """Support for the AEMET OpenData service.""" from homeassistant.components.weather import WeatherEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - LENGTH_MILLIMETERS, - PRESSURE_HPA, - SPEED_KILOMETERS_PER_HOUR, - TEMP_CELSIUS, -) +from homeassistant.const import PRESSURE_HPA, SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -52,10 +47,9 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity): """Implementation of an AEMET OpenData sensor.""" _attr_attribution = ATTRIBUTION - _attr_native_precipitation_unit = LENGTH_MILLIMETERS - _attr_native_pressure_unit = PRESSURE_HPA - _attr_native_temperature_unit = TEMP_CELSIUS - _attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR + _attr_temperature_unit = TEMP_CELSIUS + _attr_pressure_unit = PRESSURE_HPA + _attr_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR def __init__( self, @@ -89,12 +83,12 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity): return self.coordinator.data[ATTR_API_HUMIDITY] @property - def native_pressure(self): + def pressure(self): """Return the pressure.""" return self.coordinator.data[ATTR_API_PRESSURE] @property - def native_temperature(self): + def temperature(self): """Return the temperature.""" return self.coordinator.data[ATTR_API_TEMPERATURE] @@ -104,6 +98,6 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity): return self.coordinator.data[ATTR_API_WIND_BEARING] @property - def native_wind_speed(self): + def wind_speed(self): """Return the wind speed.""" return self.coordinator.data[ATTR_API_WIND_SPEED] diff --git a/homeassistant/components/aemet/weather_update_coordinator.py b/homeassistant/components/aemet/weather_update_coordinator.py index 4f0bf6ac5ea..c86465ea8f1 100644 --- a/homeassistant/components/aemet/weather_update_coordinator.py +++ b/homeassistant/components/aemet/weather_update_coordinator.py @@ -44,13 +44,13 @@ import async_timeout from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, - ATTR_FORECAST_NATIVE_PRECIPITATION, - ATTR_FORECAST_NATIVE_TEMP, - ATTR_FORECAST_NATIVE_TEMP_LOW, - ATTR_FORECAST_NATIVE_WIND_SPEED, + ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, + ATTR_FORECAST_WIND_SPEED, ) from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import dt as dt_util @@ -406,10 +406,10 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): ATTR_FORECAST_PRECIPITATION_PROBABILITY: self._get_precipitation_prob_day( day ), - ATTR_FORECAST_NATIVE_TEMP: self._get_temperature_day(day), - ATTR_FORECAST_NATIVE_TEMP_LOW: self._get_temperature_low_day(day), + ATTR_FORECAST_TEMP: self._get_temperature_day(day), + ATTR_FORECAST_TEMP_LOW: self._get_temperature_low_day(day), ATTR_FORECAST_TIME: dt_util.as_utc(date).isoformat(), - ATTR_FORECAST_NATIVE_WIND_SPEED: self._get_wind_speed_day(day), + ATTR_FORECAST_WIND_SPEED: self._get_wind_speed_day(day), ATTR_FORECAST_WIND_BEARING: self._get_wind_bearing_day(day), } @@ -421,13 +421,13 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): return { ATTR_FORECAST_CONDITION: condition, - ATTR_FORECAST_NATIVE_PRECIPITATION: self._calc_precipitation(day, hour), + ATTR_FORECAST_PRECIPITATION: self._calc_precipitation(day, hour), ATTR_FORECAST_PRECIPITATION_PROBABILITY: self._calc_precipitation_prob( day, hour ), - ATTR_FORECAST_NATIVE_TEMP: self._get_temperature(day, hour), + ATTR_FORECAST_TEMP: self._get_temperature(day, hour), ATTR_FORECAST_TIME: dt_util.as_utc(forecast_dt).isoformat(), - ATTR_FORECAST_NATIVE_WIND_SPEED: self._get_wind_speed(day, hour), + ATTR_FORECAST_WIND_SPEED: self._get_wind_speed(day, hour), ATTR_FORECAST_WIND_BEARING: self._get_wind_bearing(day, hour), } diff --git a/tests/components/aemet/test_init.py b/tests/components/aemet/test_init.py index 8dd177a145d..b1f452c1b46 100644 --- a/tests/components/aemet/test_init.py +++ b/tests/components/aemet/test_init.py @@ -2,15 +2,11 @@ from unittest.mock import patch -import pytest import requests_mock from homeassistant.components.aemet.const import DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME -from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er import homeassistant.util.dt as dt_util from .util import aemet_requests_mock @@ -46,83 +42,3 @@ async def test_unload_entry(hass): await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.NOT_LOADED - - -@pytest.mark.parametrize( - "old_unique_id,new_unique_id", - [ - # Sensors which should be migrated - ( - "aemet_unique_id-forecast-daily-precipitation", - "aemet_unique_id-forecast-daily-native_precipitation", - ), - ( - "aemet_unique_id-forecast-daily-temperature", - "aemet_unique_id-forecast-daily-native_temperature", - ), - ( - "aemet_unique_id-forecast-daily-templow", - "aemet_unique_id-forecast-daily-native_templow", - ), - ( - "aemet_unique_id-forecast-daily-wind_speed", - "aemet_unique_id-forecast-daily-native_wind_speed", - ), - ( - "aemet_unique_id-forecast-hourly-precipitation", - "aemet_unique_id-forecast-hourly-native_precipitation", - ), - ( - "aemet_unique_id-forecast-hourly-temperature", - "aemet_unique_id-forecast-hourly-native_temperature", - ), - ( - "aemet_unique_id-forecast-hourly-templow", - "aemet_unique_id-forecast-hourly-native_templow", - ), - ( - "aemet_unique_id-forecast-hourly-wind_speed", - "aemet_unique_id-forecast-hourly-native_wind_speed", - ), - # Already migrated - ( - "aemet_unique_id-forecast-daily-native_templow", - "aemet_unique_id-forecast-daily-native_templow", - ), - # No migration needed - ( - "aemet_unique_id-forecast-daily-condition", - "aemet_unique_id-forecast-daily-condition", - ), - ], -) -async def test_migrate_unique_id_sensor( - hass: HomeAssistant, - old_unique_id: str, - new_unique_id: str, -) -> None: - """Test migration of unique_id.""" - now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00") - with patch("homeassistant.util.dt.now", return_value=now), patch( - "homeassistant.util.dt.utcnow", return_value=now - ), requests_mock.mock() as _m: - aemet_requests_mock(_m) - config_entry = MockConfigEntry( - domain=DOMAIN, unique_id="aemet_unique_id", data=CONFIG - ) - config_entry.add_to_hass(hass) - - entity_registry = er.async_get(hass) - entity: er.RegistryEntry = entity_registry.async_get_or_create( - domain=SENSOR_DOMAIN, - platform=DOMAIN, - unique_id=old_unique_id, - config_entry=config_entry, - ) - assert entity.unique_id == old_unique_id - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - entity_migrated = entity_registry.async_get(entity.entity_id) - assert entity_migrated - assert entity_migrated.unique_id == new_unique_id From 56e90dd30b3c4d9883cf6b951839abc9ddd9b184 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 5 Jul 2022 13:56:38 -0700 Subject: [PATCH 62/71] Bumped version to 2022.7.0b5 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5a8d761e83b..69ebbacb0e2 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 7 -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, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 27cd670e809..61ff9ba39aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.7.0b4" +version = "2022.7.0b5" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From cd4255523881fc7b8a7b221f5f9b9d9b74b020ae Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 5 Jul 2022 23:00:40 -0500 Subject: [PATCH 63/71] Fix apple tv not coming online if connected before entity created (#74488) --- homeassistant/components/apple_tv/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/apple_tv/__init__.py b/homeassistant/components/apple_tv/__init__.py index 45250451f37..5177c6f3486 100644 --- a/homeassistant/components/apple_tv/__init__.py +++ b/homeassistant/components/apple_tv/__init__.py @@ -123,6 +123,10 @@ class AppleTVEntity(Entity): self.atv = None self.async_write_ha_state() + if self.manager.atv: + # ATV is already connected + _async_connected(self.manager.atv) + self.async_on_remove( async_dispatcher_connect( self.hass, f"{SIGNAL_CONNECTED}_{self.unique_id}", _async_connected From 06aa92b0b6409ced10521ca2b1e298f764675816 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 6 Jul 2022 00:52:41 -0500 Subject: [PATCH 64/71] Bump aiohomekit to 0.7.20 (#74489) --- 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 a15c576c313..955f5e37177 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.7.19"], + "requirements": ["aiohomekit==0.7.20"], "zeroconf": ["_hap._tcp.local."], "after_dependencies": ["zeroconf"], "codeowners": ["@Jc2k", "@bdraco"], diff --git a/requirements_all.txt b/requirements_all.txt index 726c499b88c..b0171ae3ee2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==0.7.19 +aiohomekit==0.7.20 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0a7d822a5d9..bd0800310a0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==0.7.19 +aiohomekit==0.7.20 # homeassistant.components.emulated_hue # homeassistant.components.http From c7c88877191d774af76ef0028cf5b309de8bdff9 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 6 Jul 2022 16:22:45 +0200 Subject: [PATCH 65/71] Migrate aemet weather to native_* (#74494) --- homeassistant/components/aemet/const.py | 48 ++++++------- homeassistant/components/aemet/sensor.py | 20 +++--- homeassistant/components/aemet/weather.py | 69 ++++++++++++++++--- .../aemet/weather_update_coordinator.py | 46 ++++++------- 4 files changed, 114 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/aemet/const.py b/homeassistant/components/aemet/const.py index 4be90011f5a..645c1ad0ea2 100644 --- a/homeassistant/components/aemet/const.py +++ b/homeassistant/components/aemet/const.py @@ -17,14 +17,6 @@ from homeassistant.components.weather import ( ATTR_CONDITION_RAINY, ATTR_CONDITION_SNOWY, ATTR_CONDITION_SUNNY, - ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, - ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, - ATTR_FORECAST_TIME, - ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, ) from homeassistant.const import ( DEGREE, @@ -45,8 +37,16 @@ ENTRY_NAME = "name" ENTRY_WEATHER_COORDINATOR = "weather_coordinator" ATTR_API_CONDITION = "condition" +ATTR_API_FORECAST_CONDITION = "condition" ATTR_API_FORECAST_DAILY = "forecast-daily" ATTR_API_FORECAST_HOURLY = "forecast-hourly" +ATTR_API_FORECAST_PRECIPITATION = "precipitation" +ATTR_API_FORECAST_PRECIPITATION_PROBABILITY = "precipitation_probability" +ATTR_API_FORECAST_TEMP = "temperature" +ATTR_API_FORECAST_TEMP_LOW = "templow" +ATTR_API_FORECAST_TIME = "datetime" +ATTR_API_FORECAST_WIND_BEARING = "wind_bearing" +ATTR_API_FORECAST_WIND_SPEED = "wind_speed" ATTR_API_HUMIDITY = "humidity" ATTR_API_PRESSURE = "pressure" ATTR_API_RAIN = "rain" @@ -158,14 +158,14 @@ CONDITIONS_MAP = { } FORECAST_MONITORED_CONDITIONS = [ - ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, - ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, - ATTR_FORECAST_TIME, - ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, + ATTR_API_FORECAST_CONDITION, + ATTR_API_FORECAST_PRECIPITATION, + ATTR_API_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_API_FORECAST_TEMP, + ATTR_API_FORECAST_TEMP_LOW, + ATTR_API_FORECAST_TIME, + ATTR_API_FORECAST_WIND_BEARING, + ATTR_API_FORECAST_WIND_SPEED, ] MONITORED_CONDITIONS = [ ATTR_API_CONDITION, @@ -202,43 +202,43 @@ FORECAST_MODE_ATTR_API = { FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( - key=ATTR_FORECAST_CONDITION, + key=ATTR_API_FORECAST_CONDITION, name="Condition", ), SensorEntityDescription( - key=ATTR_FORECAST_PRECIPITATION, + key=ATTR_API_FORECAST_PRECIPITATION, name="Precipitation", native_unit_of_measurement=PRECIPITATION_MILLIMETERS_PER_HOUR, ), SensorEntityDescription( - key=ATTR_FORECAST_PRECIPITATION_PROBABILITY, + key=ATTR_API_FORECAST_PRECIPITATION_PROBABILITY, name="Precipitation probability", native_unit_of_measurement=PERCENTAGE, ), SensorEntityDescription( - key=ATTR_FORECAST_TEMP, + key=ATTR_API_FORECAST_TEMP, name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( - key=ATTR_FORECAST_TEMP_LOW, + key=ATTR_API_FORECAST_TEMP_LOW, name="Temperature Low", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( - key=ATTR_FORECAST_TIME, + key=ATTR_API_FORECAST_TIME, name="Time", device_class=SensorDeviceClass.TIMESTAMP, ), SensorEntityDescription( - key=ATTR_FORECAST_WIND_BEARING, + key=ATTR_API_FORECAST_WIND_BEARING, name="Wind bearing", native_unit_of_measurement=DEGREE, ), SensorEntityDescription( - key=ATTR_FORECAST_WIND_SPEED, + key=ATTR_API_FORECAST_WIND_SPEED, name="Wind speed", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, ), diff --git a/homeassistant/components/aemet/sensor.py b/homeassistant/components/aemet/sensor.py index f98e3fff49e..e34583148e1 100644 --- a/homeassistant/components/aemet/sensor.py +++ b/homeassistant/components/aemet/sensor.py @@ -10,7 +10,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import dt as dt_util from .const import ( - ATTR_FORECAST_TIME, + ATTR_API_FORECAST_TIME, ATTRIBUTION, DOMAIN, ENTRY_NAME, @@ -45,17 +45,13 @@ async def async_setup_entry( entities.extend( [ AemetForecastSensor( - name_prefix, - unique_id_prefix, + f"{domain_data[ENTRY_NAME]} {mode} Forecast", + f"{unique_id}-forecast-{mode}", weather_coordinator, mode, description, ) for mode in FORECAST_MODES - if ( - (name_prefix := f"{domain_data[ENTRY_NAME]} {mode} Forecast") - and (unique_id_prefix := f"{unique_id}-forecast-{mode}") - ) for description in FORECAST_SENSOR_TYPES if description.key in FORECAST_MONITORED_CONDITIONS ] @@ -89,14 +85,14 @@ class AemetSensor(AbstractAemetSensor): def __init__( self, name, - unique_id, + unique_id_prefix, weather_coordinator: WeatherUpdateCoordinator, description: SensorEntityDescription, ): """Initialize the sensor.""" super().__init__( name=name, - unique_id=f"{unique_id}-{description.key}", + unique_id=f"{unique_id_prefix}-{description.key}", coordinator=weather_coordinator, description=description, ) @@ -113,7 +109,7 @@ class AemetForecastSensor(AbstractAemetSensor): def __init__( self, name, - unique_id, + unique_id_prefix, weather_coordinator: WeatherUpdateCoordinator, forecast_mode, description: SensorEntityDescription, @@ -121,7 +117,7 @@ class AemetForecastSensor(AbstractAemetSensor): """Initialize the sensor.""" super().__init__( name=name, - unique_id=f"{unique_id}-{description.key}", + unique_id=f"{unique_id_prefix}-{description.key}", coordinator=weather_coordinator, description=description, ) @@ -139,6 +135,6 @@ class AemetForecastSensor(AbstractAemetSensor): ) if forecasts: forecast = forecasts[0].get(self.entity_description.key) - if self.entity_description.key == ATTR_FORECAST_TIME: + if self.entity_description.key == ATTR_API_FORECAST_TIME: forecast = dt_util.parse_datetime(forecast) return forecast diff --git a/homeassistant/components/aemet/weather.py b/homeassistant/components/aemet/weather.py index d05442b621e..a7ff3630e78 100644 --- a/homeassistant/components/aemet/weather.py +++ b/homeassistant/components/aemet/weather.py @@ -1,13 +1,36 @@ """Support for the AEMET OpenData service.""" -from homeassistant.components.weather import WeatherEntity +from homeassistant.components.weather import ( + ATTR_FORECAST_CONDITION, + ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_SPEED, + ATTR_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_FORECAST_TIME, + ATTR_FORECAST_WIND_BEARING, + WeatherEntity, +) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PRESSURE_HPA, SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS +from homeassistant.const import ( + LENGTH_MILLIMETERS, + PRESSURE_HPA, + SPEED_KILOMETERS_PER_HOUR, + TEMP_CELSIUS, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( ATTR_API_CONDITION, + ATTR_API_FORECAST_CONDITION, + ATTR_API_FORECAST_PRECIPITATION, + ATTR_API_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_API_FORECAST_TEMP, + ATTR_API_FORECAST_TEMP_LOW, + ATTR_API_FORECAST_TIME, + ATTR_API_FORECAST_WIND_BEARING, + ATTR_API_FORECAST_WIND_SPEED, ATTR_API_HUMIDITY, ATTR_API_PRESSURE, ATTR_API_TEMPERATURE, @@ -19,10 +42,32 @@ from .const import ( ENTRY_WEATHER_COORDINATOR, FORECAST_MODE_ATTR_API, FORECAST_MODE_DAILY, + FORECAST_MODE_HOURLY, FORECAST_MODES, ) from .weather_update_coordinator import WeatherUpdateCoordinator +FORECAST_MAP = { + FORECAST_MODE_DAILY: { + ATTR_API_FORECAST_CONDITION: ATTR_FORECAST_CONDITION, + ATTR_API_FORECAST_PRECIPITATION_PROBABILITY: ATTR_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_API_FORECAST_TEMP_LOW: ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_API_FORECAST_TEMP: ATTR_FORECAST_NATIVE_TEMP, + ATTR_API_FORECAST_TIME: ATTR_FORECAST_TIME, + ATTR_API_FORECAST_WIND_BEARING: ATTR_FORECAST_WIND_BEARING, + ATTR_API_FORECAST_WIND_SPEED: ATTR_FORECAST_NATIVE_WIND_SPEED, + }, + FORECAST_MODE_HOURLY: { + ATTR_API_FORECAST_CONDITION: ATTR_FORECAST_CONDITION, + ATTR_API_FORECAST_PRECIPITATION_PROBABILITY: ATTR_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_API_FORECAST_PRECIPITATION: ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_API_FORECAST_TEMP: ATTR_FORECAST_NATIVE_TEMP, + ATTR_API_FORECAST_TIME: ATTR_FORECAST_TIME, + ATTR_API_FORECAST_WIND_BEARING: ATTR_FORECAST_WIND_BEARING, + ATTR_API_FORECAST_WIND_SPEED: ATTR_FORECAST_NATIVE_WIND_SPEED, + }, +} + async def async_setup_entry( hass: HomeAssistant, @@ -47,9 +92,10 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity): """Implementation of an AEMET OpenData sensor.""" _attr_attribution = ATTRIBUTION - _attr_temperature_unit = TEMP_CELSIUS - _attr_pressure_unit = PRESSURE_HPA - _attr_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR + _attr_native_precipitation_unit = LENGTH_MILLIMETERS + _attr_native_pressure_unit = PRESSURE_HPA + _attr_native_temperature_unit = TEMP_CELSIUS + _attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR def __init__( self, @@ -75,7 +121,12 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity): @property def forecast(self): """Return the forecast array.""" - return self.coordinator.data[FORECAST_MODE_ATTR_API[self._forecast_mode]] + forecasts = self.coordinator.data[FORECAST_MODE_ATTR_API[self._forecast_mode]] + forecast_map = FORECAST_MAP[self._forecast_mode] + return [ + {ha_key: forecast[api_key] for api_key, ha_key in forecast_map.items()} + for forecast in forecasts + ] @property def humidity(self): @@ -83,12 +134,12 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity): return self.coordinator.data[ATTR_API_HUMIDITY] @property - def pressure(self): + def native_pressure(self): """Return the pressure.""" return self.coordinator.data[ATTR_API_PRESSURE] @property - def temperature(self): + def native_temperature(self): """Return the temperature.""" return self.coordinator.data[ATTR_API_TEMPERATURE] @@ -98,6 +149,6 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity): return self.coordinator.data[ATTR_API_WIND_BEARING] @property - def wind_speed(self): + def native_wind_speed(self): """Return the wind speed.""" return self.coordinator.data[ATTR_API_WIND_SPEED] diff --git a/homeassistant/components/aemet/weather_update_coordinator.py b/homeassistant/components/aemet/weather_update_coordinator.py index c86465ea8f1..1c64206891c 100644 --- a/homeassistant/components/aemet/weather_update_coordinator.py +++ b/homeassistant/components/aemet/weather_update_coordinator.py @@ -42,23 +42,21 @@ from aemet_opendata.helpers import ( ) import async_timeout -from homeassistant.components.weather import ( - ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, - ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, - ATTR_FORECAST_TIME, - ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, -) from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import dt as dt_util from .const import ( ATTR_API_CONDITION, + ATTR_API_FORECAST_CONDITION, ATTR_API_FORECAST_DAILY, ATTR_API_FORECAST_HOURLY, + ATTR_API_FORECAST_PRECIPITATION, + ATTR_API_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_API_FORECAST_TEMP, + ATTR_API_FORECAST_TEMP_LOW, + ATTR_API_FORECAST_TIME, + ATTR_API_FORECAST_WIND_BEARING, + ATTR_API_FORECAST_WIND_SPEED, ATTR_API_HUMIDITY, ATTR_API_PRESSURE, ATTR_API_RAIN, @@ -402,15 +400,15 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): return None return { - ATTR_FORECAST_CONDITION: condition, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: self._get_precipitation_prob_day( + ATTR_API_FORECAST_CONDITION: condition, + ATTR_API_FORECAST_PRECIPITATION_PROBABILITY: self._get_precipitation_prob_day( day ), - ATTR_FORECAST_TEMP: self._get_temperature_day(day), - ATTR_FORECAST_TEMP_LOW: self._get_temperature_low_day(day), - ATTR_FORECAST_TIME: dt_util.as_utc(date).isoformat(), - ATTR_FORECAST_WIND_SPEED: self._get_wind_speed_day(day), - ATTR_FORECAST_WIND_BEARING: self._get_wind_bearing_day(day), + ATTR_API_FORECAST_TEMP: self._get_temperature_day(day), + ATTR_API_FORECAST_TEMP_LOW: self._get_temperature_low_day(day), + ATTR_API_FORECAST_TIME: dt_util.as_utc(date).isoformat(), + ATTR_API_FORECAST_WIND_SPEED: self._get_wind_speed_day(day), + ATTR_API_FORECAST_WIND_BEARING: self._get_wind_bearing_day(day), } def _convert_forecast_hour(self, date, day, hour): @@ -420,15 +418,15 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): forecast_dt = date.replace(hour=hour, minute=0, second=0) return { - ATTR_FORECAST_CONDITION: condition, - ATTR_FORECAST_PRECIPITATION: self._calc_precipitation(day, hour), - ATTR_FORECAST_PRECIPITATION_PROBABILITY: self._calc_precipitation_prob( + ATTR_API_FORECAST_CONDITION: condition, + ATTR_API_FORECAST_PRECIPITATION: self._calc_precipitation(day, hour), + ATTR_API_FORECAST_PRECIPITATION_PROBABILITY: self._calc_precipitation_prob( day, hour ), - ATTR_FORECAST_TEMP: self._get_temperature(day, hour), - ATTR_FORECAST_TIME: dt_util.as_utc(forecast_dt).isoformat(), - ATTR_FORECAST_WIND_SPEED: self._get_wind_speed(day, hour), - ATTR_FORECAST_WIND_BEARING: self._get_wind_bearing(day, hour), + ATTR_API_FORECAST_TEMP: self._get_temperature(day, hour), + ATTR_API_FORECAST_TIME: dt_util.as_utc(forecast_dt).isoformat(), + ATTR_API_FORECAST_WIND_SPEED: self._get_wind_speed(day, hour), + ATTR_API_FORECAST_WIND_BEARING: self._get_wind_bearing(day, hour), } def _calc_precipitation(self, day, hour): From b277c28ed7227ce8981534e44882de4a48a1f005 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 6 Jul 2022 13:35:25 +0200 Subject: [PATCH 66/71] Bump aioslimproto to 2.1.1 (#74499) --- homeassistant/components/slimproto/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/slimproto/manifest.json b/homeassistant/components/slimproto/manifest.json index 23b3198d7e4..1e076046b44 100644 --- a/homeassistant/components/slimproto/manifest.json +++ b/homeassistant/components/slimproto/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "iot_class": "local_push", "documentation": "https://www.home-assistant.io/integrations/slimproto", - "requirements": ["aioslimproto==2.0.1"], + "requirements": ["aioslimproto==2.1.1"], "codeowners": ["@marcelveldt"], "after_dependencies": ["media_source"] } diff --git a/requirements_all.txt b/requirements_all.txt index b0171ae3ee2..9e6a6aa2b0a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -250,7 +250,7 @@ aioshelly==2.0.0 aioskybell==22.6.1 # homeassistant.components.slimproto -aioslimproto==2.0.1 +aioslimproto==2.1.1 # homeassistant.components.steamist aiosteamist==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bd0800310a0..030ca8ab7ec 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -219,7 +219,7 @@ aioshelly==2.0.0 aioskybell==22.6.1 # homeassistant.components.slimproto -aioslimproto==2.0.1 +aioslimproto==2.1.1 # homeassistant.components.steamist aiosteamist==0.3.2 From 519d15428c488f9ac81d282b2216e0f24dc3e778 Mon Sep 17 00:00:00 2001 From: Gyosa3 <51777889+Gyosa3@users.noreply.github.com> Date: Wed, 6 Jul 2022 17:48:12 +0200 Subject: [PATCH 67/71] Add new alias for valid Celcius temperature units in Tuya (#74511) --- homeassistant/components/tuya/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 8a3e59b1ac9..727e505200b 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -579,7 +579,7 @@ UNITS = ( ), UnitOfMeasurement( unit=TEMP_CELSIUS, - aliases={"°c", "c", "celsius"}, + aliases={"°c", "c", "celsius", "℃"}, device_classes={SensorDeviceClass.TEMPERATURE}, ), UnitOfMeasurement( From 9d3dde60ff1dd6eb4e67798de49f66099ebf95d5 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 6 Jul 2022 17:49:06 +0200 Subject: [PATCH 68/71] Fix openweathermap forecast sensors (#74513) --- .../components/openweathermap/const.py | 30 ++++++------- .../components/openweathermap/weather.py | 41 +++++++++++++++++- .../weather_update_coordinator.py | 42 +++++++++---------- 3 files changed, 74 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/openweathermap/const.py b/homeassistant/components/openweathermap/const.py index 06f13daa9c2..836a56c70b2 100644 --- a/homeassistant/components/openweathermap/const.py +++ b/homeassistant/components/openweathermap/const.py @@ -21,9 +21,6 @@ from homeassistant.components.weather import ( ATTR_CONDITION_SUNNY, ATTR_CONDITION_WINDY, ATTR_CONDITION_WINDY_VARIANT, - ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TIME, ) from homeassistant.const import ( DEGREE, @@ -68,10 +65,15 @@ ATTR_API_FORECAST = "forecast" UPDATE_LISTENER = "update_listener" PLATFORMS = [Platform.SENSOR, Platform.WEATHER] -ATTR_FORECAST_PRECIPITATION = "precipitation" -ATTR_FORECAST_PRESSURE = "pressure" -ATTR_FORECAST_TEMP = "temperature" -ATTR_FORECAST_TEMP_LOW = "templow" +ATTR_API_FORECAST_CONDITION = "condition" +ATTR_API_FORECAST_PRECIPITATION = "precipitation" +ATTR_API_FORECAST_PRECIPITATION_PROBABILITY = "precipitation_probability" +ATTR_API_FORECAST_PRESSURE = "pressure" +ATTR_API_FORECAST_TEMP = "temperature" +ATTR_API_FORECAST_TEMP_LOW = "templow" +ATTR_API_FORECAST_TIME = "datetime" +ATTR_API_FORECAST_WIND_BEARING = "wind_bearing" +ATTR_API_FORECAST_WIND_SPEED = "wind_speed" FORECAST_MODE_HOURLY = "hourly" FORECAST_MODE_DAILY = "daily" @@ -263,39 +265,39 @@ WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ) FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( - key=ATTR_FORECAST_CONDITION, + key=ATTR_API_FORECAST_CONDITION, name="Condition", ), SensorEntityDescription( - key=ATTR_FORECAST_PRECIPITATION, + key=ATTR_API_FORECAST_PRECIPITATION, name="Precipitation", native_unit_of_measurement=LENGTH_MILLIMETERS, ), SensorEntityDescription( - key=ATTR_FORECAST_PRECIPITATION_PROBABILITY, + key=ATTR_API_FORECAST_PRECIPITATION_PROBABILITY, name="Precipitation probability", native_unit_of_measurement=PERCENTAGE, ), SensorEntityDescription( - key=ATTR_FORECAST_PRESSURE, + key=ATTR_API_FORECAST_PRESSURE, name="Pressure", native_unit_of_measurement=PRESSURE_HPA, device_class=SensorDeviceClass.PRESSURE, ), SensorEntityDescription( - key=ATTR_FORECAST_TEMP, + key=ATTR_API_FORECAST_TEMP, name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( - key=ATTR_FORECAST_TEMP_LOW, + key=ATTR_API_FORECAST_TEMP_LOW, name="Temperature Low", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( - key=ATTR_FORECAST_TIME, + key=ATTR_API_FORECAST_TIME, name="Time", device_class=SensorDeviceClass.TIMESTAMP, ), diff --git a/homeassistant/components/openweathermap/weather.py b/homeassistant/components/openweathermap/weather.py index fce6efdf3c5..ea439a35586 100644 --- a/homeassistant/components/openweathermap/weather.py +++ b/homeassistant/components/openweathermap/weather.py @@ -1,7 +1,20 @@ """Support for the OpenWeatherMap (OWM) service.""" from __future__ import annotations -from homeassistant.components.weather import Forecast, WeatherEntity +from typing import cast + +from homeassistant.components.weather import ( + ATTR_FORECAST_CONDITION, + ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_SPEED, + ATTR_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_FORECAST_TIME, + ATTR_FORECAST_WIND_BEARING, + Forecast, + WeatherEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( LENGTH_MILLIMETERS, @@ -17,6 +30,14 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( ATTR_API_CONDITION, ATTR_API_FORECAST, + ATTR_API_FORECAST_CONDITION, + ATTR_API_FORECAST_PRECIPITATION, + ATTR_API_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_API_FORECAST_TEMP, + ATTR_API_FORECAST_TEMP_LOW, + ATTR_API_FORECAST_TIME, + ATTR_API_FORECAST_WIND_BEARING, + ATTR_API_FORECAST_WIND_SPEED, ATTR_API_HUMIDITY, ATTR_API_PRESSURE, ATTR_API_TEMPERATURE, @@ -31,6 +52,17 @@ from .const import ( ) from .weather_update_coordinator import WeatherUpdateCoordinator +FORECAST_MAP = { + ATTR_API_FORECAST_CONDITION: ATTR_FORECAST_CONDITION, + ATTR_API_FORECAST_PRECIPITATION: ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_API_FORECAST_PRECIPITATION_PROBABILITY: ATTR_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_API_FORECAST_TEMP_LOW: ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_API_FORECAST_TEMP: ATTR_FORECAST_NATIVE_TEMP, + ATTR_API_FORECAST_TIME: ATTR_FORECAST_TIME, + ATTR_API_FORECAST_WIND_BEARING: ATTR_FORECAST_WIND_BEARING, + ATTR_API_FORECAST_WIND_SPEED: ATTR_FORECAST_NATIVE_WIND_SPEED, +} + async def async_setup_entry( hass: HomeAssistant, @@ -109,7 +141,12 @@ class OpenWeatherMapWeather(WeatherEntity): @property def forecast(self) -> list[Forecast] | None: """Return the forecast array.""" - return self._weather_coordinator.data[ATTR_API_FORECAST] + api_forecasts = self._weather_coordinator.data[ATTR_API_FORECAST] + forecasts = [ + {ha_key: forecast[api_key] for api_key, ha_key in FORECAST_MAP.items()} + for forecast in api_forecasts + ] + return cast(list[Forecast], forecasts) @property def available(self) -> bool: diff --git a/homeassistant/components/openweathermap/weather_update_coordinator.py b/homeassistant/components/openweathermap/weather_update_coordinator.py index 36511424737..98c3b56fb5e 100644 --- a/homeassistant/components/openweathermap/weather_update_coordinator.py +++ b/homeassistant/components/openweathermap/weather_update_coordinator.py @@ -8,15 +8,6 @@ from pyowm.commons.exceptions import APIRequestError, UnauthorizedError from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_SUNNY, - ATTR_FORECAST_CONDITION, - ATTR_FORECAST_NATIVE_PRECIPITATION, - ATTR_FORECAST_NATIVE_PRESSURE, - ATTR_FORECAST_NATIVE_TEMP, - ATTR_FORECAST_NATIVE_TEMP_LOW, - ATTR_FORECAST_NATIVE_WIND_SPEED, - ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TIME, - ATTR_FORECAST_WIND_BEARING, ) from homeassistant.helpers import sun from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -29,6 +20,15 @@ from .const import ( ATTR_API_DEW_POINT, ATTR_API_FEELS_LIKE_TEMPERATURE, ATTR_API_FORECAST, + ATTR_API_FORECAST_CONDITION, + ATTR_API_FORECAST_PRECIPITATION, + ATTR_API_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_API_FORECAST_PRESSURE, + ATTR_API_FORECAST_TEMP, + ATTR_API_FORECAST_TEMP_LOW, + ATTR_API_FORECAST_TIME, + ATTR_API_FORECAST_WIND_BEARING, + ATTR_API_FORECAST_WIND_SPEED, ATTR_API_HUMIDITY, ATTR_API_PRECIPITATION_KIND, ATTR_API_PRESSURE, @@ -158,19 +158,19 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): def _convert_forecast(self, entry): """Convert the forecast data.""" forecast = { - ATTR_FORECAST_TIME: dt.utc_from_timestamp( + ATTR_API_FORECAST_TIME: dt.utc_from_timestamp( entry.reference_time("unix") ).isoformat(), - ATTR_FORECAST_NATIVE_PRECIPITATION: self._calc_precipitation( + ATTR_API_FORECAST_PRECIPITATION: self._calc_precipitation( entry.rain, entry.snow ), - ATTR_FORECAST_PRECIPITATION_PROBABILITY: ( + ATTR_API_FORECAST_PRECIPITATION_PROBABILITY: ( round(entry.precipitation_probability * 100) ), - ATTR_FORECAST_NATIVE_PRESSURE: entry.pressure.get("press"), - ATTR_FORECAST_NATIVE_WIND_SPEED: entry.wind().get("speed"), - ATTR_FORECAST_WIND_BEARING: entry.wind().get("deg"), - ATTR_FORECAST_CONDITION: self._get_condition( + ATTR_API_FORECAST_PRESSURE: entry.pressure.get("press"), + ATTR_API_FORECAST_WIND_SPEED: entry.wind().get("speed"), + ATTR_API_FORECAST_WIND_BEARING: entry.wind().get("deg"), + ATTR_API_FORECAST_CONDITION: self._get_condition( entry.weather_code, entry.reference_time("unix") ), ATTR_API_CLOUDS: entry.clouds, @@ -178,16 +178,12 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): temperature_dict = entry.temperature("celsius") if "max" in temperature_dict and "min" in temperature_dict: - forecast[ATTR_FORECAST_NATIVE_TEMP] = entry.temperature("celsius").get( - "max" - ) - forecast[ATTR_FORECAST_NATIVE_TEMP_LOW] = entry.temperature("celsius").get( + forecast[ATTR_API_FORECAST_TEMP] = entry.temperature("celsius").get("max") + forecast[ATTR_API_FORECAST_TEMP_LOW] = entry.temperature("celsius").get( "min" ) else: - forecast[ATTR_FORECAST_NATIVE_TEMP] = entry.temperature("celsius").get( - "temp" - ) + forecast[ATTR_API_FORECAST_TEMP] = entry.temperature("celsius").get("temp") return forecast From 380244fa7b67e8cccdaa5ca2cc18012d424e851c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 6 Jul 2022 18:34:51 +0200 Subject: [PATCH 69/71] Update homematicip to 1.0.3 (#74516) --- homeassistant/components/homematicip_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index b13c8ca19b2..40f7e67fd07 100644 --- a/homeassistant/components/homematicip_cloud/manifest.json +++ b/homeassistant/components/homematicip_cloud/manifest.json @@ -3,7 +3,7 @@ "name": "HomematicIP Cloud", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homematicip_cloud", - "requirements": ["homematicip==1.0.2"], + "requirements": ["homematicip==1.0.3"], "codeowners": [], "quality_scale": "platinum", "iot_class": "cloud_push", diff --git a/requirements_all.txt b/requirements_all.txt index 9e6a6aa2b0a..769235b30a1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -834,7 +834,7 @@ home-assistant-frontend==20220705.0 homeconnect==0.7.1 # homeassistant.components.homematicip_cloud -homematicip==1.0.2 +homematicip==1.0.3 # homeassistant.components.home_plus_control homepluscontrol==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 030ca8ab7ec..44daeef21fd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -601,7 +601,7 @@ home-assistant-frontend==20220705.0 homeconnect==0.7.1 # homeassistant.components.homematicip_cloud -homematicip==1.0.2 +homematicip==1.0.3 # homeassistant.components.home_plus_control homepluscontrol==0.0.5 From 8e5b6ff185872484767f25247163847406e0c244 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 6 Jul 2022 19:31:57 +0200 Subject: [PATCH 70/71] Update Home Assistant Frontend to 20220706.0 (#74520) Bump Home Assistant Frontend to 20220706.0 --- 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 6b378fe1098..85ead380485 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220705.0"], + "requirements": ["home-assistant-frontend==20220706.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 00f5c776712..c291d969219 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220705.0 +home-assistant-frontend==20220706.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 769235b30a1..99be85cc418 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -828,7 +828,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220705.0 +home-assistant-frontend==20220706.0 # homeassistant.components.home_connect homeconnect==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 44daeef21fd..b6a493d37f9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -595,7 +595,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220705.0 +home-assistant-frontend==20220706.0 # homeassistant.components.home_connect homeconnect==0.7.1 From 06c6ddb2d63cb41ff075e09919a699e9adf472ec Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 6 Jul 2022 19:33:46 +0200 Subject: [PATCH 71/71] Bumped version to 2022.7.0 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 69ebbacb0e2..c2ee7de691f 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 7 -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, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 61ff9ba39aa..b4994d55edb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.7.0b5" +version = "2022.7.0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst"