From 4fffeb1155fffc592fc8af04145bcb412d6f7961 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 27 Oct 2021 22:01:47 +0200 Subject: [PATCH 001/174] Bumped version to 2021.11.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 85f7ad1bd6e..17ea68898a0 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 11 -PATCH_VERSION: Final = "0.dev0" +PATCH_VERSION: Final = "0b0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From 3650a5e2a47b7e3019bb60529a038e3d819511ef Mon Sep 17 00:00:00 2001 From: Dave Lowper Date: Thu, 28 Oct 2021 09:30:06 +0200 Subject: [PATCH 002/174] Fix ZeroDivisionError on freebox/sensor (#57077) Co-authored-by: Martin Hjelmare --- homeassistant/components/freebox/sensor.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index 814c2ea402f..016434ac89f 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -177,6 +177,9 @@ class FreeboxDiskSensor(FreeboxSensor): @callback def async_update_state(self) -> None: """Update the Freebox disk sensor.""" - self._attr_native_value = round( - self._partition["free_bytes"] * 100 / self._partition["total_bytes"], 2 - ) + value = None + if self._partition.get("total_bytes"): + value = round( + self._partition["free_bytes"] * 100 / self._partition["total_bytes"], 2 + ) + self._attr_native_value = value From 59bee8e830734badf5d1ef9857b4f4c2fa1fd9ac Mon Sep 17 00:00:00 2001 From: Pieter Mulder Date: Thu, 28 Oct 2021 22:14:50 +0200 Subject: [PATCH 003/174] Allow initialized callback to have arguments (#58129) --- homeassistant/components/hdmi_cec/__init__.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/hdmi_cec/__init__.py b/homeassistant/components/hdmi_cec/__init__.py index 47bd8d160c6..9dfd68d4a4f 100644 --- a/homeassistant/components/hdmi_cec/__init__.py +++ b/homeassistant/components/hdmi_cec/__init__.py @@ -1,9 +1,10 @@ """Support for HDMI CEC.""" from __future__ import annotations -from functools import partial, reduce +from functools import reduce import logging import multiprocessing +from typing import Any from pycec.cec import CecAdapter from pycec.commands import CecCommand, KeyPressCommand, KeyReleaseCommand @@ -41,7 +42,7 @@ from homeassistant.const import ( STATE_PLAYING, STATE_UNAVAILABLE, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import discovery, event import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -222,9 +223,12 @@ def setup(hass: HomeAssistant, base_config: ConfigType) -> bool: # noqa: C901 hass.bus.fire(EVENT_HDMI_CEC_UNAVAILABLE) adapter.init() - hdmi_network.set_initialized_callback( - partial(event.async_call_later, hass, WATCHDOG_INTERVAL, _adapter_watchdog) - ) + @callback + def _async_initialized_callback(*_: Any): + """Add watchdog on initialization.""" + return event.async_call_later(hass, WATCHDOG_INTERVAL, _adapter_watchdog) + + hdmi_network.set_initialized_callback(_async_initialized_callback) def _volume(call): """Increase/decrease volume and mute/unmute system.""" From 695bc3f23cc7464c175bc0c1535c10b4d67319ef Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 28 Oct 2021 21:07:29 -0700 Subject: [PATCH 004/174] Add an image placeholder for Nest WebRTC cameras (#58250) --- homeassistant/components/nest/camera_sdm.py | 44 ++++++++++++++++++++- tests/components/nest/test_camera_sdm.py | 14 ++++--- 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py index 99234c3de8a..abebc8db3ef 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -3,9 +3,11 @@ from __future__ import annotations from collections.abc import Callable import datetime +import io import logging from typing import Any +from PIL import Image, ImageDraw, ImageFilter from google_nest_sdm.camera_traits import ( CameraEventImageTrait, CameraImageTrait, @@ -38,6 +40,15 @@ _LOGGER = logging.getLogger(__name__) # Used to schedule an alarm to refresh the stream before expiration STREAM_EXPIRATION_BUFFER = datetime.timedelta(seconds=30) +# The Google Home app dispays a placeholder image that appears as a faint +# light source (dim, blurred sphere) giving the user an indication the camera +# is available, not just a blank screen. These constants define a blurred +# ellipse at the top left of the thumbnail. +PLACEHOLDER_ELLIPSE_BLUR = 0.1 +PLACEHOLDER_ELLIPSE_XY = [-0.4, 0.3, 0.3, 0.4] +PLACEHOLDER_OVERLAY_COLOR = "#ffffff" +PLACEHOLDER_ELLIPSE_OPACITY = 255 + async def async_setup_sdm_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -62,6 +73,30 @@ async def async_setup_sdm_entry( async_add_entities(entities) +def placeholder_image(width: int | None = None, height: int | None = None) -> Image: + """Return a camera image preview for cameras without live thumbnails.""" + if not width or not height: + return Image.new("RGB", (1, 1)) + # Draw a dark scene with a fake light source + blank = Image.new("RGB", (width, height)) + overlay = Image.new("RGB", blank.size, color=PLACEHOLDER_OVERLAY_COLOR) + ellipse = Image.new("L", blank.size, color=0) + draw = ImageDraw.Draw(ellipse) + draw.ellipse( + ( + width * PLACEHOLDER_ELLIPSE_XY[0], + height * PLACEHOLDER_ELLIPSE_XY[1], + width * PLACEHOLDER_ELLIPSE_XY[2], + height * PLACEHOLDER_ELLIPSE_XY[3], + ), + fill=PLACEHOLDER_ELLIPSE_OPACITY, + ) + mask = ellipse.filter( + ImageFilter.GaussianBlur(radius=width * PLACEHOLDER_ELLIPSE_BLUR) + ) + return Image.composite(overlay, blank, mask) + + class NestCamera(Camera): """Devices that support cameras.""" @@ -212,7 +247,14 @@ class NestCamera(Camera): # Fetch still image from the live stream stream_url = await self.stream_source() if not stream_url: - return None + if self.frontend_stream_type != STREAM_TYPE_WEB_RTC: + return None + # Nest Web RTC cams only have image previews for events, and not + # for "now" by design to save batter, and need a placeholder. + image = placeholder_image(width=width, height=height) + with io.BytesIO() as content: + image.save(content, format="JPEG", optimize=True) + return content.getvalue() return await async_get_image(self.hass, stream_url, output_format=IMAGE_JPEG) async def _async_active_event_image(self) -> bytes | None: diff --git a/tests/components/nest/test_camera_sdm.py b/tests/components/nest/test_camera_sdm.py index b7637bf3e2e..1ac1b4ca6f9 100644 --- a/tests/components/nest/test_camera_sdm.py +++ b/tests/components/nest/test_camera_sdm.py @@ -135,7 +135,7 @@ async def fire_alarm(hass, point_in_time): await hass.async_block_till_done() -async def async_get_image(hass): +async def async_get_image(hass, width=None, height=None): """Get image from the camera, a wrapper around camera.async_get_image.""" # Note: this patches ImageFrame to simulate decoding an image from a live # stream, however the test may not use it. Tests assert on the image @@ -145,7 +145,9 @@ async def async_get_image(hass): autopatch=True, return_value=IMAGE_BYTES_FROM_STREAM, ): - return await camera.async_get_image(hass, "camera.my_camera") + return await camera.async_get_image( + hass, "camera.my_camera", width=width, height=height + ) async def test_no_devices(hass): @@ -721,9 +723,11 @@ async def test_camera_web_rtc(hass, auth, hass_ws_client): assert msg["success"] assert msg["result"]["answer"] == "v=0\r\ns=-\r\n" - # Nest WebRTC cameras do not support a still image - with pytest.raises(HomeAssistantError): - await async_get_image(hass) + # Nest WebRTC cameras return a placeholder + content = await async_get_image(hass) + assert content.content_type == "image/jpeg" + content = await async_get_image(hass, width=1024, height=768) + assert content.content_type == "image/jpeg" async def test_camera_web_rtc_unsupported(hass, auth, hass_ws_client): From 41007c286428b1d815262ac969cf73b556cc8872 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Oct 2021 15:44:28 -0500 Subject: [PATCH 005/174] Reduce rainmachine intervals to avoid device overload (#58319) --- homeassistant/components/rainmachine/__init__.py | 10 +++++++++- homeassistant/components/rainmachine/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index d8eea8b3df5..db9177591ca 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -55,6 +55,14 @@ CONFIG_SCHEMA = cv.deprecated(DOMAIN) PLATFORMS = ["binary_sensor", "sensor", "switch"] +UPDATE_INTERVALS = { + DATA_PROVISION_SETTINGS: timedelta(minutes=1), + DATA_PROGRAMS: timedelta(seconds=30), + DATA_RESTRICTIONS_CURRENT: timedelta(minutes=1), + DATA_RESTRICTIONS_UNIVERSAL: timedelta(minutes=1), + DATA_ZONES: timedelta(seconds=15), +} + # Constants expected by the RainMachine API for Service Data CONF_CONDITION = "condition" CONF_DEWPOINT = "dewpoint" @@ -228,7 +236,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, LOGGER, name=f'{controller.name} ("{api_category}")', - update_interval=DEFAULT_UPDATE_INTERVAL, + update_interval=UPDATE_INTERVALS[api_category], update_method=partial(async_update, api_category), ) controller_init_tasks.append(coordinator.async_refresh()) diff --git a/homeassistant/components/rainmachine/manifest.json b/homeassistant/components/rainmachine/manifest.json index 307cd7681f5..b6dc4e5c1db 100644 --- a/homeassistant/components/rainmachine/manifest.json +++ b/homeassistant/components/rainmachine/manifest.json @@ -3,7 +3,7 @@ "name": "RainMachine", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rainmachine", - "requirements": ["regenmaschine==3.2.0"], + "requirements": ["regenmaschine==2021.10.0"], "codeowners": ["@bachya"], "iot_class": "local_polling", "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index 1a2a3b7d493..8acd1f74681 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2037,7 +2037,7 @@ raincloudy==0.0.7 raspyrfm-client==1.2.8 # homeassistant.components.rainmachine -regenmaschine==3.2.0 +regenmaschine==2021.10.0 # homeassistant.components.renault renault-api==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 82199eadaf1..1acf3981f01 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1187,7 +1187,7 @@ pyzerproc==0.4.8 rachiopy==1.0.3 # homeassistant.components.rainmachine -regenmaschine==3.2.0 +regenmaschine==2021.10.0 # homeassistant.components.renault renault-api==0.1.4 From 2cae44c98a40d191a7f45194d3f4c00e8f3ea8b7 Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Fri, 29 Oct 2021 03:27:40 +1100 Subject: [PATCH 006/174] Return the real MAC address for LIFX bulbs with newer firmware (#58511) --- homeassistant/components/lifx/light.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index bdb0337c1a1..106c66c8900 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -7,6 +7,7 @@ import math import aiolifx as aiolifx_module import aiolifx_effects as aiolifx_effects_module +from awesomeversion import AwesomeVersion import voluptuous as vol from homeassistant import util @@ -66,6 +67,8 @@ MESSAGE_TIMEOUT = 1.0 MESSAGE_RETRIES = 8 UNAVAILABLE_GRACE = 90 +FIX_MAC_FW = AwesomeVersion("3.70") + SERVICE_LIFX_SET_STATE = "set_state" ATTR_INFRARED = "infrared" @@ -455,20 +458,34 @@ class LIFXLight(LightEntity): self.postponed_update = None self.lock = asyncio.Lock() + def get_mac_addr(self): + """Increment the last byte of the mac address by one for FW>3.70.""" + if ( + self.bulb.host_firmware_version + and AwesomeVersion(self.bulb.host_firmware_version) >= FIX_MAC_FW + ): + octets = [int(octet, 16) for octet in self.bulb.mac_addr.split(":")] + octets[5] = (octets[5] + 1) % 256 + return ":".join(f"{octet:02x}" for octet in octets) + return self.bulb.mac_addr + @property def device_info(self) -> DeviceInfo: """Return information about the device.""" _map = aiolifx().products.product_map + info = DeviceInfo( identifiers={(LIFX_DOMAIN, self.unique_id)}, - connections={(dr.CONNECTION_NETWORK_MAC, self.bulb.mac_addr)}, + connections={(dr.CONNECTION_NETWORK_MAC, self.get_mac_addr())}, manufacturer="LIFX", name=self.name, ) - if model := (_map.get(self.bulb.product) or self.bulb.product) is not None: + + if (model := (_map.get(self.bulb.product) or self.bulb.product)) is not None: info[ATTR_MODEL] = str(model) if (version := self.bulb.host_firmware_version) is not None: info[ATTR_SW_VERSION] = version + return info @property From 49e0bbe3dfc6a0134dfcfd43325022b815385241 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Oct 2021 11:13:21 -0500 Subject: [PATCH 007/174] Add tplink KP303 to discovery (#58548) --- homeassistant/components/tplink/manifest.json | 4 ++++ homeassistant/generated/dhcp.py | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index c82eafb96d8..4db98d680d3 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -9,6 +9,10 @@ "quality_scale": "platinum", "iot_class": "local_polling", "dhcp": [ + { + "hostname": "k[lp]*", + "macaddress": "005F67*" + }, { "hostname": "k[lp]*", "macaddress": "1027F5*" diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 1bb37e6c736..ada826de8a8 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -351,6 +351,11 @@ DHCP = [ "hostname": "eneco-*", "macaddress": "74C63B*" }, + { + "domain": "tplink", + "hostname": "k[lp]*", + "macaddress": "005F67*" + }, { "domain": "tplink", "hostname": "k[lp]*", From c9f4972b5956d18df31d3408b5cbaf577a5f51b6 Mon Sep 17 00:00:00 2001 From: Tom Matheussen Date: Thu, 28 Oct 2021 14:32:53 +0200 Subject: [PATCH 008/174] Add service configuration URL to Doorbird (#58549) --- homeassistant/components/doorbird/entity.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/doorbird/entity.py b/homeassistant/components/doorbird/entity.py index 73404e7c199..2cf97aa4b57 100644 --- a/homeassistant/components/doorbird/entity.py +++ b/homeassistant/components/doorbird/entity.py @@ -28,6 +28,7 @@ class DoorBirdEntity(Entity): firmware = self._doorstation_info[DOORBIRD_INFO_KEY_FIRMWARE] firmware_build = self._doorstation_info[DOORBIRD_INFO_KEY_BUILD_NUMBER] return DeviceInfo( + configuration_url="https://webadmin.doorbird.com/", connections={(dr.CONNECTION_NETWORK_MAC, self._mac_addr)}, manufacturer=MANUFACTURER, model=self._doorstation_info[DOORBIRD_INFO_KEY_DEVICE_TYPE], From e1f9ae4b967f1e188c014bba5d9f3feae728d35e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 27 Oct 2021 16:28:10 -0700 Subject: [PATCH 009/174] Add entity category to ZHA battery (#58553) --- homeassistant/components/zha/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 8e8a92c099a..d7c3e0797b8 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -35,6 +35,7 @@ from homeassistant.const import ( ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, + ENTITY_CATEGORY_DIAGNOSTIC, LIGHT_LUX, PERCENTAGE, POWER_WATT, @@ -223,6 +224,7 @@ class Battery(Sensor): _device_class = DEVICE_CLASS_BATTERY _state_class = STATE_CLASS_MEASUREMENT _unit = PERCENTAGE + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC @staticmethod def formatter(value: int) -> int: From 211041eb3a52b98ba284b271c7094475cb95a900 Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 28 Oct 2021 08:56:38 +0200 Subject: [PATCH 010/174] Add `configuration_url` to Freebox integration (#58555) --- homeassistant/components/freebox/router.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py index 2eef38d7d1e..e352146915e 100644 --- a/homeassistant/components/freebox/router.py +++ b/homeassistant/components/freebox/router.py @@ -184,6 +184,7 @@ class FreeboxRouter: def device_info(self) -> DeviceInfo: """Return the device information.""" return DeviceInfo( + configuration_url=f"https://{self._host}:{self._port}/", connections={(CONNECTION_NETWORK_MAC, self.mac)}, identifiers={(DOMAIN, self.mac)}, manufacturer="Freebox SAS", From 928b8bcdbdcd867ad5b07484a691f8f7018c7e88 Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 28 Oct 2021 09:03:53 +0200 Subject: [PATCH 011/174] Add `configuration_url` to iCloud integration (#58557) --- homeassistant/components/icloud/device_tracker.py | 1 + homeassistant/components/icloud/sensor.py | 1 + 2 files changed, 2 insertions(+) diff --git a/homeassistant/components/icloud/device_tracker.py b/homeassistant/components/icloud/device_tracker.py index d255d29b6ee..4c6dcf37297 100644 --- a/homeassistant/components/icloud/device_tracker.py +++ b/homeassistant/components/icloud/device_tracker.py @@ -116,6 +116,7 @@ class IcloudTrackerEntity(TrackerEntity): def device_info(self) -> DeviceInfo: """Return the device information.""" return DeviceInfo( + configuration_url="https://icloud.com/", identifiers={(DOMAIN, self._device.unique_id)}, manufacturer="Apple", model=self._device.device_model, diff --git a/homeassistant/components/icloud/sensor.py b/homeassistant/components/icloud/sensor.py index 6de8a49daf1..699f3e9baa3 100644 --- a/homeassistant/components/icloud/sensor.py +++ b/homeassistant/components/icloud/sensor.py @@ -94,6 +94,7 @@ class IcloudDeviceBatterySensor(SensorEntity): def device_info(self) -> DeviceInfo: """Return the device information.""" return DeviceInfo( + configuration_url="https://icloud.com/", identifiers={(DOMAIN, self._device.unique_id)}, manufacturer="Apple", model=self._device.device_model, From 07c9d7741471ca1dbf33ad1a85351d66630d580a Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 28 Oct 2021 23:11:54 +1300 Subject: [PATCH 012/174] Allow configuration_url to be removed/nullified from device registry (#58564) * Allow configuration_url to be removed from device registry * Add test * Check for None before stringifying and url parsing * Add type to dict to remove mypy error on assigning None --- homeassistant/helpers/entity_platform.py | 29 ++++++++------- tests/helpers/test_entity_platform.py | 45 ++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 12 deletions(-) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 1864111c0ed..c44eb96026d 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -458,7 +458,9 @@ class EntityPlatform: device_id = None if config_entry_id is not None and device_info is not None: - processed_dev_info = {"config_entry_id": config_entry_id} + processed_dev_info: dict[str, str | None] = { + "config_entry_id": config_entry_id + } for key in ( "connections", "default_manufacturer", @@ -477,18 +479,21 @@ class EntityPlatform: processed_dev_info[key] = device_info[key] # type: ignore[misc] if "configuration_url" in device_info: - configuration_url = str(device_info["configuration_url"]) - if urlparse(configuration_url).scheme in [ - "http", - "https", - "homeassistant", - ]: - processed_dev_info["configuration_url"] = configuration_url + if device_info["configuration_url"] is None: + processed_dev_info["configuration_url"] = None else: - _LOGGER.warning( - "Ignoring invalid device configuration_url '%s'", - configuration_url, - ) + configuration_url = str(device_info["configuration_url"]) + if urlparse(configuration_url).scheme in [ + "http", + "https", + "homeassistant", + ]: + processed_dev_info["configuration_url"] = configuration_url + else: + _LOGGER.warning( + "Ignoring invalid device configuration_url '%s'", + configuration_url, + ) try: device = device_registry.async_get_or_create(**processed_dev_info) # type: ignore[arg-type] diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 636ce7a764b..a151a3b7ef3 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -1011,6 +1011,51 @@ async def test_device_info_homeassistant_url(hass, caplog): assert device.configuration_url == "homeassistant://config/mqtt" +async def test_device_info_change_to_no_url(hass, caplog): + """Test device info changes to no URL.""" + registry = dr.async_get(hass) + registry.async_get_or_create( + config_entry_id="123", + connections=set(), + identifiers={("mqtt", "via-id")}, + manufacturer="manufacturer", + model="via", + configuration_url="homeassistant://config/mqtt", + ) + + async def async_setup_entry(hass, config_entry, async_add_entities): + """Mock setup entry method.""" + async_add_entities( + [ + # Valid device info, with homeassistant url + MockEntity( + unique_id="qwer", + device_info={ + "identifiers": {("mqtt", "1234")}, + "configuration_url": None, + }, + ), + ] + ) + return True + + platform = MockPlatform(async_setup_entry=async_setup_entry) + config_entry = MockConfigEntry(entry_id="super-mock-id") + entity_platform = MockEntityPlatform( + hass, platform_name=config_entry.domain, platform=platform + ) + + assert await entity_platform.async_setup_entry(config_entry) + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids()) == 1 + + device = registry.async_get_device({("mqtt", "1234")}) + assert device is not None + assert device.identifiers == {("mqtt", "1234")} + assert device.configuration_url is None + + async def test_entity_disabled_by_integration(hass): """Test entity disabled by integration.""" component = EntityComponent(_LOGGER, DOMAIN, hass, timedelta(seconds=20)) From d36df7270101397f9b2286363626e88c8467866e Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 29 Oct 2021 17:04:00 +1300 Subject: [PATCH 013/174] Add configuration_url to ESPHome (#58565) --- homeassistant/components/esphome/__init__.py | 6 +++++- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 97b459ae0cd..2825e036884 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -327,9 +327,13 @@ async def _async_setup_device_registry( sw_version = device_info.esphome_version if device_info.compilation_time: sw_version += f" ({device_info.compilation_time})" - device_registry = await dr.async_get_registry(hass) + configuration_url = None + if device_info.webserver_port > 0: + configuration_url = f"http://{entry.data['host']}:{device_info.webserver_port}" + device_registry = dr.async_get(hass) device_entry = device_registry.async_get_or_create( config_entry_id=entry.entry_id, + configuration_url=configuration_url, connections={(dr.CONNECTION_NETWORK_MAC, device_info.mac_address)}, name=device_info.name, manufacturer="espressif", diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 4b54a4d7883..247c78abb92 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -3,7 +3,7 @@ "name": "ESPHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", - "requirements": ["aioesphomeapi==10.1.0"], + "requirements": ["aioesphomeapi==10.2.0"], "zeroconf": ["_esphomelib._tcp.local."], "codeowners": ["@OttoWinter", "@jesserockz"], "after_dependencies": ["zeroconf", "tag"], diff --git a/requirements_all.txt b/requirements_all.txt index 8acd1f74681..392170e5df2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -161,7 +161,7 @@ aioeagle==1.1.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==10.1.0 +aioesphomeapi==10.2.0 # homeassistant.components.flo aioflo==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1acf3981f01..e37a2f69499 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -109,7 +109,7 @@ aioeagle==1.1.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==10.1.0 +aioesphomeapi==10.2.0 # homeassistant.components.flo aioflo==0.4.1 From a23c1624fbebdef060724dad4bc83d0dc3c49217 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 28 Oct 2021 02:20:28 -0700 Subject: [PATCH 014/174] Fix default value for host in octoprint config flow (#58568) --- homeassistant/components/octoprint/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/octoprint/config_flow.py b/homeassistant/components/octoprint/config_flow.py index 5962aedc89f..9674806d49f 100644 --- a/homeassistant/components/octoprint/config_flow.py +++ b/homeassistant/components/octoprint/config_flow.py @@ -22,7 +22,7 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -def _schema_with_defaults(username="", host=None, port=80, path="/", ssl=False): +def _schema_with_defaults(username="", host="", port=80, path="/", ssl=False): return vol.Schema( { vol.Required(CONF_USERNAME, default=username): cv.string, From 15c2d5c278ef62267419bc6e7e5dccb613642809 Mon Sep 17 00:00:00 2001 From: Robert Chmielowiec Date: Thu, 28 Oct 2021 14:32:20 +0200 Subject: [PATCH 015/174] Add `configuration_url` to Huawei LTE integration (#58584) --- homeassistant/components/huawei_lte/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index 1314344f48f..4b33f3e5a71 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -431,6 +431,7 @@ async def async_setup_entry( # noqa: C901 # Set up device registry if router.device_identifiers or router.device_connections: device_info = DeviceInfo( + configuration_url=router.url, connections=router.device_connections, identifiers=router.device_identifiers, name=router.device_name, From e729ee24f86d1aa03b3b01a2c1af236a5ad874af Mon Sep 17 00:00:00 2001 From: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com> Date: Thu, 28 Oct 2021 21:06:04 +0200 Subject: [PATCH 016/174] Add ROCKROBO_S5_MAX to xiaomi_miio vacuum models (#58591) * Add ROCKROBO_S5_MAX to xiaomi_miio vacuum models. https://github.com/home-assistant/core/issues/58550 Signed-off-by: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com> * disable pylint for todo Signed-off-by: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com> * Minor refactor Signed-off-by: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com> --- homeassistant/components/xiaomi_miio/const.py | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index e27fb4d2110..a1f2414b713 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -195,8 +195,25 @@ MODELS_LIGHT = ( + MODELS_LIGHT_BULB + MODELS_LIGHT_MONO ) -MODELS_VACUUM = [ROCKROBO_V1, ROCKROBO_S5, ROCKROBO_S6, ROCKROBO_S6_MAXV, ROCKROBO_S7] -MODELS_VACUUM_WITH_MOP = [ROCKROBO_S5, ROCKROBO_S6, ROCKROBO_S6_MAXV, ROCKROBO_S7] + +# TODO: use const from pythonmiio once new release with the constant has been published. # pylint: disable=fixme +ROCKROBO_S5_MAX = "roborock.vacuum.s5e" +MODELS_VACUUM = [ + ROCKROBO_V1, + ROCKROBO_S5, + ROCKROBO_S5_MAX, + ROCKROBO_S6, + ROCKROBO_S6_MAXV, + ROCKROBO_S7, +] +MODELS_VACUUM_WITH_MOP = [ + ROCKROBO_S5, + ROCKROBO_S5_MAX, + ROCKROBO_S6, + ROCKROBO_S6_MAXV, + ROCKROBO_S7, +] + MODELS_AIR_MONITOR = [ MODEL_AIRQUALITYMONITOR_V1, MODEL_AIRQUALITYMONITOR_B1, From c513516237cb9451896597795f66da2be34b04bd Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Thu, 28 Oct 2021 21:33:06 +0200 Subject: [PATCH 017/174] Add configuration_url to devolo Home Control (#58594) --- homeassistant/components/devolo_home_control/devolo_device.py | 2 ++ tests/components/devolo_home_control/mocks.py | 1 + 2 files changed, 3 insertions(+) diff --git a/homeassistant/components/devolo_home_control/devolo_device.py b/homeassistant/components/devolo_home_control/devolo_device.py index 7fdd53d0d87..f4f2432aa6e 100644 --- a/homeassistant/components/devolo_home_control/devolo_device.py +++ b/homeassistant/components/devolo_home_control/devolo_device.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from urllib.parse import urlparse from devolo_home_control_api.devices.zwave import Zwave from devolo_home_control_api.homecontrol import HomeControl @@ -33,6 +34,7 @@ class DevoloDeviceEntity(Entity): self._attr_should_poll = False self._attr_unique_id = element_uid self._attr_device_info = DeviceInfo( + configuration_url=f"https://{urlparse(device_instance.href).netloc}", identifiers={(DOMAIN, self._device_instance.uid)}, manufacturer=device_instance.brand, model=device_instance.name, diff --git a/tests/components/devolo_home_control/mocks.py b/tests/components/devolo_home_control/mocks.py index 7700d30b1dd..6651215251a 100644 --- a/tests/components/devolo_home_control/mocks.py +++ b/tests/components/devolo_home_control/mocks.py @@ -45,6 +45,7 @@ class DeviceMock(Zwave): self.name = "Test Device" self.uid = "Test" self.settings_property = {"general_device_settings": SettingsMock()} + self.href = "https://www.mydevolo.com" class BinarySensorMock(DeviceMock): From f942131d6a9cce67e1464487cf322ab24cdb7546 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 28 Oct 2021 08:27:01 -0400 Subject: [PATCH 018/174] Fix missing config string in sense (#58597) --- homeassistant/components/sense/strings.json | 3 ++- homeassistant/components/sense/translations/en.json | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sense/strings.json b/homeassistant/components/sense/strings.json index 44a45f07ce9..29e85c98fc2 100644 --- a/homeassistant/components/sense/strings.json +++ b/homeassistant/components/sense/strings.json @@ -5,7 +5,8 @@ "title": "Connect to your Sense Energy Monitor", "data": { "email": "[%key:common::config_flow::data::email%]", - "password": "[%key:common::config_flow::data::password%]" + "password": "[%key:common::config_flow::data::password%]", + "timeout": "Timeout" } } }, diff --git a/homeassistant/components/sense/translations/en.json b/homeassistant/components/sense/translations/en.json index 5582a8424a6..24cde7411a8 100644 --- a/homeassistant/components/sense/translations/en.json +++ b/homeassistant/components/sense/translations/en.json @@ -12,7 +12,8 @@ "user": { "data": { "email": "Email", - "password": "Password" + "password": "Password", + "timeout": "Timeout" }, "title": "Connect to your Sense Energy Monitor" } From cc9b9a8f9500b4884443af6ebd9fd7a43cdc3418 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Oct 2021 14:27:19 -0500 Subject: [PATCH 019/174] Fix uncaught exception in sense and retry later (#58623) --- homeassistant/components/sense/__init__.py | 25 ++++++++++++++-------- homeassistant/components/sense/const.py | 3 +++ 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/sense/__init__.py b/homeassistant/components/sense/__init__.py index 5c8028f2525..92a4e29108c 100644 --- a/homeassistant/components/sense/__init__.py +++ b/homeassistant/components/sense/__init__.py @@ -3,11 +3,7 @@ import asyncio from datetime import timedelta import logging -from sense_energy import ( - ASyncSenseable, - SenseAPITimeoutException, - SenseAuthenticationException, -) +from sense_energy import ASyncSenseable, SenseAuthenticationException from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -30,6 +26,7 @@ from .const import ( SENSE_DEVICE_UPDATE, SENSE_DEVICES_DATA, SENSE_DISCOVERED_DEVICES_DATA, + SENSE_EXCEPTIONS, SENSE_TIMEOUT_EXCEPTIONS, SENSE_TRENDS_COORDINATOR, ) @@ -76,14 +73,22 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.error("Could not authenticate with sense server") return False except SENSE_TIMEOUT_EXCEPTIONS as err: - raise ConfigEntryNotReady from err + raise ConfigEntryNotReady( + str(err) or "Timed out during authentication" + ) from err + except SENSE_EXCEPTIONS as err: + raise ConfigEntryNotReady(str(err) or "Error during authentication") from err sense_devices_data = SenseDevicesData() try: sense_discovered_devices = await gateway.get_discovered_device_data() await gateway.update_realtime() except SENSE_TIMEOUT_EXCEPTIONS as err: - raise ConfigEntryNotReady from err + raise ConfigEntryNotReady( + str(err) or "Timed out during realtime update" + ) from err + except SENSE_EXCEPTIONS as err: + raise ConfigEntryNotReady(str(err) or "Error during realtime update") from err trends_coordinator = DataUpdateCoordinator( hass, @@ -114,8 +119,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Retrieve latest state.""" try: await gateway.update_realtime() - except SenseAPITimeoutException: - _LOGGER.error("Timeout retrieving data") + except SENSE_TIMEOUT_EXCEPTIONS as ex: + _LOGGER.error("Timeout retrieving data: %s", ex) + except SENSE_EXCEPTIONS as ex: + _LOGGER.error("Failed to update data: %s", ex) data = gateway.get_realtime() if "devices" in data: diff --git a/homeassistant/components/sense/const.py b/homeassistant/components/sense/const.py index af8454bbeab..bb323151950 100644 --- a/homeassistant/components/sense/const.py +++ b/homeassistant/components/sense/const.py @@ -1,8 +1,10 @@ """Constants for monitoring a Sense energy sensor.""" import asyncio +import socket from sense_energy import SenseAPITimeoutException +from sense_energy.sense_exceptions import SenseWebsocketException DOMAIN = "sense" DEFAULT_TIMEOUT = 10 @@ -37,6 +39,7 @@ SOLAR_POWERED_ID = "solar_powered" ICON = "mdi:flash" SENSE_TIMEOUT_EXCEPTIONS = (asyncio.TimeoutError, SenseAPITimeoutException) +SENSE_EXCEPTIONS = (socket.gaierror, SenseWebsocketException) MDI_ICONS = { "ac": "air-conditioner", From 23841c4b747f6b7a861d92c99f4c01418929a289 Mon Sep 17 00:00:00 2001 From: Chen-IL <18098431+Chen-IL@users.noreply.github.com> Date: Thu, 28 Oct 2021 22:34:26 +0300 Subject: [PATCH 020/174] Add entity category for load sensors to AsusWRT (#58625) --- homeassistant/components/asuswrt/sensor.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/asuswrt/sensor.py b/homeassistant/components/asuswrt/sensor.py index 4b865bfb0e3..8beb5d3d9ee 100644 --- a/homeassistant/components/asuswrt/sensor.py +++ b/homeassistant/components/asuswrt/sensor.py @@ -11,7 +11,11 @@ from homeassistant.components.sensor import ( SensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import DATA_GIGABYTES, DATA_RATE_MEGABITS_PER_SECOND +from homeassistant.const import ( + DATA_GIGABYTES, + DATA_RATE_MEGABITS_PER_SECOND, + ENTITY_CATEGORY_DIAGNOSTIC, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -89,6 +93,7 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = ( name="Load Avg (1m)", icon="mdi:cpu-32-bit", state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_registry_enabled_default=False, factor=1, precision=1, @@ -98,6 +103,7 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = ( name="Load Avg (5m)", icon="mdi:cpu-32-bit", state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_registry_enabled_default=False, factor=1, precision=1, @@ -107,6 +113,7 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = ( name="Load Avg (15m)", icon="mdi:cpu-32-bit", state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_registry_enabled_default=False, factor=1, precision=1, From 22773d050314b0e56f3158f8e0b037cb7a2f1a34 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Oct 2021 14:32:22 -0500 Subject: [PATCH 021/174] Add package constraint to websockets (#58626) --- homeassistant/package_constraints.txt | 4 ++++ script/gen_requirements_all.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d2699d02ed0..2e2b9657800 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -79,3 +79,7 @@ regex==2021.8.28 # anyio has a bug that was fixed in 3.3.1 # can remove after httpx/httpcore updates its anyio version pin anyio>=3.3.1 + +# websockets 10.0 is broken with AWS +# https://github.com/aaugustin/websockets/issues/1065 +websockets==9.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 939806d379e..3d2ace4c240 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -106,6 +106,10 @@ regex==2021.8.28 # anyio has a bug that was fixed in 3.3.1 # can remove after httpx/httpcore updates its anyio version pin anyio>=3.3.1 + +# websockets 10.0 is broken with AWS +# https://github.com/aaugustin/websockets/issues/1065 +websockets==9.1 """ IGNORE_PRE_COMMIT_HOOK_ID = ( From 92af39cf8921289be43e973f0645f1b1cad13896 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 28 Oct 2021 13:52:06 -0600 Subject: [PATCH 022/174] Fix missing triggered state in SimpliSafe alarm control panel (#58628) --- .../simplisafe/alarm_control_panel.py | 34 +++++++------------ 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py index 6643bd3a1a1..bc2e2a8ac74 100644 --- a/homeassistant/components/simplisafe/alarm_control_panel.py +++ b/homeassistant/components/simplisafe/alarm_control_panel.py @@ -151,22 +151,7 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): self._attr_supported_features = SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY self._last_event = None - if system.alarm_going_off: - self._attr_state = STATE_ALARM_TRIGGERED - elif system.state == SystemStates.away: - self._attr_state = STATE_ALARM_ARMED_AWAY - elif system.state in ( - SystemStates.away_count, - SystemStates.exit_delay, - SystemStates.home_count, - ): - self._attr_state = STATE_ALARM_ARMING - elif system.state == SystemStates.home: - self._attr_state = STATE_ALARM_ARMED_HOME - elif system.state == SystemStates.off: - self._attr_state = STATE_ALARM_DISARMED - else: - self._attr_state = None + self._set_state_from_system_data() @callback def _is_code_valid(self, code: str | None, state: str) -> bool: @@ -182,6 +167,17 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): return True + @callback + def _set_state_from_system_data(self) -> None: + """Set the state based on the latest REST API data.""" + if self._system.alarm_going_off: + self._attr_state = STATE_ALARM_TRIGGERED + elif state := STATE_MAP_FROM_REST_API.get(self._system.state): + self._attr_state = state + else: + LOGGER.error("Unknown system state (REST API): %s", self._system.state) + self._attr_state = None + async def async_alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" if not self._is_code_valid(code, STATE_ALARM_DISARMED): @@ -266,11 +262,7 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): self._errors = 0 - if state := STATE_MAP_FROM_REST_API.get(self._system.state): - self._attr_state = state - else: - LOGGER.error("Unknown system state (REST API): %s", self._system.state) - self._attr_state = None + self._set_state_from_system_data() @callback def async_update_from_websocket_event(self, event: WebsocketEvent) -> None: From 2546efe026f9d5e0cbcca9e91c0d4dbb81e387d0 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 28 Oct 2021 21:32:38 +0200 Subject: [PATCH 023/174] Update frontend to 20211028.0 (#58629) --- 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 d66d9138e24..9f15dc4fc5a 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20211027.0" + "home-assistant-frontend==20211028.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 2e2b9657800..c4f1a31f5c6 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==3.4.8 emoji==1.5.0 hass-nabucasa==0.50.0 -home-assistant-frontend==20211027.0 +home-assistant-frontend==20211028.0 httpx==0.19.0 ifaddr==0.1.7 jinja2==3.0.2 diff --git a/requirements_all.txt b/requirements_all.txt index 392170e5df2..e6952a51d27 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -813,7 +813,7 @@ hole==0.5.1 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211027.0 +home-assistant-frontend==20211028.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e37a2f69499..9496060e590 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -500,7 +500,7 @@ hole==0.5.1 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211027.0 +home-assistant-frontend==20211028.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 6858191ca35357e1d74b0f35458c5a67ce7c9ecd Mon Sep 17 00:00:00 2001 From: Hans Oischinger Date: Thu, 28 Oct 2021 22:58:28 +0200 Subject: [PATCH 024/174] Improve ViCare energy units (#58630) --- homeassistant/components/vicare/sensor.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/vicare/sensor.py b/homeassistant/components/vicare/sensor.py index 89c30f8b570..044632b6244 100644 --- a/homeassistant/components/vicare/sensor.py +++ b/homeassistant/components/vicare/sensor.py @@ -22,6 +22,7 @@ from homeassistant.components.sensor import ( ) from homeassistant.const import ( DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, ENERGY_KILO_WATT_HOUR, PERCENTAGE, @@ -83,7 +84,7 @@ SENSOR_POWER_PRODUCTION_THIS_YEAR = "power_production_this_year" class ViCareSensorEntityDescription(SensorEntityDescription, ViCareRequiredKeysMixin): """Describes ViCare sensor entity.""" - unit_getter: Callable[[Device], bool | None] | None = None + unit_getter: Callable[[Device], str | None] | None = None GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( @@ -185,7 +186,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( name="Power production current", native_unit_of_measurement=POWER_WATT, value_getter=lambda api: api.getPowerProductionCurrent(), - device_class=DEVICE_CLASS_ENERGY, + device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), ViCareSensorEntityDescription( @@ -312,7 +313,7 @@ def _build_entity(name, vicare_api, device_config, sensor): try: sensor.value_getter(vicare_api) - if callable(sensor.unit_getter): + if sensor.unit_getter: with suppress(PyViCareNotSupportedFeatureError): vicare_unit = sensor.unit_getter(vicare_api) if vicare_unit is not None: From ffe008cbefab0497ce396df4bf0315c730470fe5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 29 Oct 2021 06:08:59 +0200 Subject: [PATCH 025/174] Migrate Tuya unique IDs for switches & lights (#58631) --- homeassistant/components/tuya/__init__.py | 85 ++++++++++++++++++++++- 1 file changed, 84 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index bcc5c9dc79c..4f34d3c31bf 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -14,9 +14,11 @@ from tuya_iot import ( TuyaOpenMQ, ) +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import dispatcher_send from .const import ( @@ -33,6 +35,7 @@ from .const import ( PLATFORMS, TUYA_DISCOVERY_NEW, TUYA_HA_SIGNAL_UPDATE_ENTITY, + DPCode, ) _LOGGER = logging.getLogger(__name__) @@ -115,6 +118,9 @@ async def _init_tuya_sdk(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.async_add_executor_job(home_manager.update_device_cache) await cleanup_device_registry(hass, device_manager) + # Migrate old unique_ids to the new format + async_migrate_entities_unique_ids(hass, entry, device_manager) + # Register known device IDs device_registry = dr.async_get(hass) for device in device_manager.device_map.values(): @@ -143,6 +149,83 @@ async def cleanup_device_registry( break +@callback +def async_migrate_entities_unique_ids( + hass: HomeAssistant, config_entry: ConfigEntry, device_manager: TuyaDeviceManager +) -> None: + """Migrate unique_ids in the entity registry to the new format.""" + entity_registry = er.async_get(hass) + registry_entries = er.async_entries_for_config_entry( + entity_registry, config_entry.entry_id + ) + light_entries = { + entry.unique_id: entry + for entry in registry_entries + if entry.domain == LIGHT_DOMAIN + } + switch_entries = { + entry.unique_id: entry + for entry in registry_entries + if entry.domain == SWITCH_DOMAIN + } + + for device in device_manager.device_map.values(): + # Old lights where in `tuya.{device_id}` format, now the DPCode is added. + # + # If the device is a previously supported light category and still has + # the old format for the unique ID, migrate it to the new format. + # + # Previously only devices providing the SWITCH_LED DPCode were supported, + # thus this can be added to those existing IDs. + # + # `tuya.{device_id}` -> `tuya.{device_id}{SWITCH_LED}` + if ( + device.category in ("dc", "dd", "dj", "fs", "fwl", "jsq", "xdd", "xxj") + and (entry := light_entries.get(f"tuya.{device.id}")) + and f"tuya.{device.id}{DPCode.SWITCH_LED}" not in light_entries + ): + entity_registry.async_update_entity( + entry.entity_id, new_unique_id=f"tuya.{device.id}{DPCode.SWITCH_LED}" + ) + + # Old switches has different formats for the unique ID, but is mappable. + # + # If the device is a previously supported switch category and still has + # the old format for the unique ID, migrate it to the new format. + # + # `tuya.{device_id}` -> `tuya.{device_id}{SWITCH}` + # `tuya.{device_id}_1` -> `tuya.{device_id}{SWITCH_1}` + # ... + # `tuya.{device_id}_6` -> `tuya.{device_id}{SWITCH_6}` + # `tuya.{device_id}_usb1` -> `tuya.{device_id}{SWITCH_USB1}` + # ... + # `tuya.{device_id}_usb6` -> `tuya.{device_id}{SWITCH_USB6}` + # + # In all other cases, the unique ID is not changed. + if device.category in ("bh", "cwysj", "cz", "dlq", "kg", "kj", "pc", "xxj"): + for postfix, dpcode in ( + ("", DPCode.SWITCH), + ("_1", DPCode.SWITCH_1), + ("_2", DPCode.SWITCH_2), + ("_3", DPCode.SWITCH_3), + ("_4", DPCode.SWITCH_4), + ("_5", DPCode.SWITCH_5), + ("_6", DPCode.SWITCH_6), + ("_usb1", DPCode.SWITCH_USB1), + ("_usb2", DPCode.SWITCH_USB2), + ("_usb3", DPCode.SWITCH_USB3), + ("_usb4", DPCode.SWITCH_USB4), + ("_usb5", DPCode.SWITCH_USB5), + ("_usb6", DPCode.SWITCH_USB6), + ): + if ( + entry := switch_entries.get(f"tuya.{device.id}{postfix}") + ) and f"tuya.{device.id}{dpcode}" not in switch_entries: + entity_registry.async_update_entity( + entry.entity_id, new_unique_id=f"tuya.{device.id}{dpcode}" + ) + + async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unloading the Tuya platforms.""" unload = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) From da2addf15a74884c43b34870ff745e5025073d75 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 28 Oct 2021 14:29:25 -0600 Subject: [PATCH 026/174] Fix incorrect RainMachine service helper (#58633) --- homeassistant/components/rainmachine/__init__.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index db9177591ca..b72fe0fb25d 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -135,9 +135,11 @@ def async_get_controller_for_service_call( device_registry = dr.async_get(hass) if device_entry := device_registry.async_get(device_id): - for entry_id in device_entry.config_entries: - if controller := hass.data[DOMAIN][entry_id][DATA_CONTROLLER]: - return cast(Controller, controller) + for entry in hass.config_entries.async_entries(DOMAIN): + if entry.entry_id in device_entry.config_entries: + return cast( + Controller, hass.data[DOMAIN][entry.entry_id][DATA_CONTROLLER] + ) raise ValueError(f"No controller for device ID: {device_id}") From 26f3c1adea261811f015fd7c144380b5ae6a1e00 Mon Sep 17 00:00:00 2001 From: Clifford Roche Date: Thu, 28 Oct 2021 19:58:59 -0400 Subject: [PATCH 027/174] Bump greeclimate to 0.12.3 (#58635) --- homeassistant/components/gree/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/gree/manifest.json b/homeassistant/components/gree/manifest.json index accd02dbe79..62d5bec6bb8 100644 --- a/homeassistant/components/gree/manifest.json +++ b/homeassistant/components/gree/manifest.json @@ -3,7 +3,7 @@ "name": "Gree Climate", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/gree", - "requirements": ["greeclimate==0.12.2"], + "requirements": ["greeclimate==0.12.3"], "codeowners": ["@cmroche"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index e6952a51d27..688dd88a729 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -747,7 +747,7 @@ gpiozero==1.5.1 gps3==0.33.3 # homeassistant.components.gree -greeclimate==0.12.2 +greeclimate==0.12.3 # homeassistant.components.greeneye_monitor greeneye_monitor==2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9496060e590..be242f88af9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -455,7 +455,7 @@ google-nest-sdm==0.3.8 googlemaps==2.5.1 # homeassistant.components.gree -greeclimate==0.12.2 +greeclimate==0.12.3 # homeassistant.components.growatt_server growattServer==1.1.0 From 6ca92812aedda9631b4ed6f1f5b1c84de2e2e74c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 29 Oct 2021 00:27:22 +0200 Subject: [PATCH 028/174] Fix missing temperature level on Tuya Heater (qn) devices (#58643) --- homeassistant/components/tuya/const.py | 1 + homeassistant/components/tuya/select.py | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index db43ec7a8eb..a9f7afb0ec5 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -210,6 +210,7 @@ class DPCode(str, Enum): LED_TYPE_1 = "led_type_1" LED_TYPE_2 = "led_type_2" LED_TYPE_3 = "led_type_3" + LEVEL = "level" LIGHT = "light" # Light LIGHT_MODE = "light_mode" LOCK = "lock" # Lock / Child lock diff --git a/homeassistant/components/tuya/select.py b/homeassistant/components/tuya/select.py index f56d2929a84..6df5b4e84dd 100644 --- a/homeassistant/components/tuya/select.py +++ b/homeassistant/components/tuya/select.py @@ -75,6 +75,15 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { entity_category=ENTITY_CATEGORY_CONFIG, ), ), + # Heater + # https://developer.tuya.com/en/docs/iot/categoryqn?id=Kaiuz18kih0sm + "qn": ( + SelectEntityDescription( + key=DPCode.LEVEL, + name="Temperature Level", + icon="mdi:thermometer-lines", + ), + ), # Siren Alarm # https://developer.tuya.com/en/docs/iot/categorysgbj?id=Kaiuz37tlpbnu "sgbj": ( From 96d145f2f09d703d41457f12816a902d90086acc Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 28 Oct 2021 21:18:17 -0700 Subject: [PATCH 029/174] Bumped version to 2021.11.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 17ea68898a0..c8ae44d9fbd 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 11 -PATCH_VERSION: Final = "0b0" +PATCH_VERSION: Final = "0b1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From 78082afa94d78dfda389f08ddc4fbb565e505e49 Mon Sep 17 00:00:00 2001 From: mezz64 <2854333+mezz64@users.noreply.github.com> Date: Fri, 29 Oct 2021 09:59:32 -0400 Subject: [PATCH 030/174] Bump pyhik to 0.3.0 (#58659) --- homeassistant/components/hikvision/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hikvision/manifest.json b/homeassistant/components/hikvision/manifest.json index 9676870ecc4..a8f89401148 100644 --- a/homeassistant/components/hikvision/manifest.json +++ b/homeassistant/components/hikvision/manifest.json @@ -2,7 +2,7 @@ "domain": "hikvision", "name": "Hikvision", "documentation": "https://www.home-assistant.io/integrations/hikvision", - "requirements": ["pyhik==0.2.8"], + "requirements": ["pyhik==0.3.0"], "codeowners": ["@mezz64"], "iot_class": "local_push" } diff --git a/requirements_all.txt b/requirements_all.txt index 688dd88a729..1a1033ec974 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1517,7 +1517,7 @@ pyhaversion==21.10.0 pyheos==0.7.2 # homeassistant.components.hikvision -pyhik==0.2.8 +pyhik==0.3.0 # homeassistant.components.hive pyhiveapi==0.4.2 From e9b67b3590aaca5cff116a0aded03964309bce35 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 29 Oct 2021 15:51:14 +0200 Subject: [PATCH 031/174] Update light turn_on schema to coerce colors to tuple before asserting sequence type (#58670) * Make color_name_to_rgb return a tuple * Tweak * Tweak * Update test * Tweak test --- homeassistant/components/group/light.py | 5 + homeassistant/components/light/__init__.py | 10 +- tests/components/group/test_light.py | 189 ++++++++++++++++++++- tests/components/light/test_init.py | 78 +++++++++ 4 files changed, 273 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index a3a02ee6b9c..4a14bc5dcf3 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections import Counter import itertools +import logging from typing import Any, Set, cast import voluptuous as vol @@ -66,6 +67,8 @@ SUPPORT_GROUP_LIGHT = ( SUPPORT_EFFECT | SUPPORT_FLASH | SUPPORT_TRANSITION | SUPPORT_WHITE_VALUE ) +_LOGGER = logging.getLogger(__name__) + async def async_setup_platform( hass: HomeAssistant, @@ -152,6 +155,8 @@ class LightGroup(GroupEntity, light.LightEntity): } data[ATTR_ENTITY_ID] = self._entity_ids + _LOGGER.debug("Forwarded turn_on command: %s", data) + await self.hass.services.async_call( light.DOMAIN, light.SERVICE_TURN_ON, diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index a09a2fcd58e..a6222c27284 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -202,25 +202,25 @@ LIGHT_TURN_ON_SCHEMA = { ), vol.Exclusive(ATTR_KELVIN, COLOR_GROUP): cv.positive_int, vol.Exclusive(ATTR_HS_COLOR, COLOR_GROUP): vol.All( + vol.Coerce(tuple), vol.ExactSequence( ( vol.All(vol.Coerce(float), vol.Range(min=0, max=360)), vol.All(vol.Coerce(float), vol.Range(min=0, max=100)), ) ), - vol.Coerce(tuple), ), vol.Exclusive(ATTR_RGB_COLOR, COLOR_GROUP): vol.All( - vol.ExactSequence((cv.byte,) * 3), vol.Coerce(tuple) + vol.Coerce(tuple), vol.ExactSequence((cv.byte,) * 3) ), vol.Exclusive(ATTR_RGBW_COLOR, COLOR_GROUP): vol.All( - vol.ExactSequence((cv.byte,) * 4), vol.Coerce(tuple) + vol.Coerce(tuple), vol.ExactSequence((cv.byte,) * 4) ), vol.Exclusive(ATTR_RGBWW_COLOR, COLOR_GROUP): vol.All( - vol.ExactSequence((cv.byte,) * 5), vol.Coerce(tuple) + vol.Coerce(tuple), vol.ExactSequence((cv.byte,) * 5) ), vol.Exclusive(ATTR_XY_COLOR, COLOR_GROUP): vol.All( - vol.ExactSequence((cv.small_float, cv.small_float)), vol.Coerce(tuple) + vol.Coerce(tuple), vol.ExactSequence((cv.small_float, cv.small_float)) ), vol.Exclusive(ATTR_WHITE, COLOR_GROUP): VALID_BRIGHTNESS, ATTR_WHITE_VALUE: vol.All(vol.Coerce(int), vol.Range(min=0, max=255)), diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index e769bf33f8a..e1a45d6fe53 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -3,12 +3,15 @@ from os import path import unittest.mock from unittest.mock import MagicMock, patch +import pytest + from homeassistant import config as hass_config from homeassistant.components.group import DOMAIN, SERVICE_RELOAD import homeassistant.components.group.light as group from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_MODE, + ATTR_COLOR_NAME, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_EFFECT_LIST, @@ -28,6 +31,7 @@ from homeassistant.components.light import ( COLOR_MODE_COLOR_TEMP, COLOR_MODE_HS, COLOR_MODE_ONOFF, + COLOR_MODE_RGB, COLOR_MODE_RGBW, COLOR_MODE_RGBWW, COLOR_MODE_WHITE, @@ -261,6 +265,77 @@ async def test_color_hs(hass, enable_custom_integrations): assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 +async def test_color_rgb(hass, enable_custom_integrations): + """Test rgbw color reporting.""" + platform = getattr(hass.components, "test.light") + platform.init(empty=True) + + platform.ENTITIES.append(platform.MockLight("test1", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("test2", STATE_OFF)) + + entity0 = platform.ENTITIES[0] + entity0.supported_color_modes = {COLOR_MODE_RGB} + entity0.color_mode = COLOR_MODE_RGB + entity0.brightness = 255 + entity0.rgb_color = (0, 64, 128) + + entity1 = platform.ENTITIES[1] + entity1.supported_color_modes = {COLOR_MODE_RGB} + entity1.color_mode = COLOR_MODE_RGB + entity1.brightness = 255 + entity1.rgb_color = (255, 128, 64) + + assert await async_setup_component( + hass, + LIGHT_DOMAIN, + { + LIGHT_DOMAIN: [ + {"platform": "test"}, + { + "platform": DOMAIN, + "entities": ["light.test1", "light.test2"], + }, + ] + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("light.light_group") + assert state.state == STATE_ON + assert state.attributes[ATTR_COLOR_MODE] == "rgb" + assert state.attributes[ATTR_RGB_COLOR] == (0, 64, 128) + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["rgb"] + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": [entity1.entity_id]}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("light.light_group") + assert state.attributes[ATTR_COLOR_MODE] == "rgb" + assert state.attributes[ATTR_RGB_COLOR] == (127, 96, 96) + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["rgb"] + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + + await hass.services.async_call( + "light", + "turn_off", + {"entity_id": [entity0.entity_id]}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("light.light_group") + assert state.attributes[ATTR_COLOR_MODE] == "rgb" + assert state.attributes[ATTR_RGB_COLOR] == (255, 128, 64) + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["rgb"] + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + + async def test_color_rgbw(hass, enable_custom_integrations): """Test rgbw color reporting.""" platform = getattr(hass.components, "test.light") @@ -1039,14 +1114,40 @@ async def test_supported_features(hass): assert state.attributes[ATTR_SUPPORTED_FEATURES] == 40 -async def test_service_calls(hass): +@pytest.mark.parametrize("supported_color_modes", [COLOR_MODE_HS, COLOR_MODE_RGB]) +async def test_service_calls(hass, enable_custom_integrations, supported_color_modes): """Test service calls.""" + platform = getattr(hass.components, "test.light") + platform.init(empty=True) + + platform.ENTITIES.append(platform.MockLight("bed_light", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("ceiling_lights", STATE_OFF)) + platform.ENTITIES.append(platform.MockLight("kitchen_lights", STATE_OFF)) + + entity0 = platform.ENTITIES[0] + entity0.supported_color_modes = {supported_color_modes} + entity0.color_mode = supported_color_modes + entity0.brightness = 255 + entity0.rgb_color = (0, 64, 128) + + entity1 = platform.ENTITIES[1] + entity1.supported_color_modes = {supported_color_modes} + entity1.color_mode = supported_color_modes + entity1.brightness = 255 + entity1.rgb_color = (255, 128, 64) + + entity2 = platform.ENTITIES[2] + entity2.supported_color_modes = {supported_color_modes} + entity2.color_mode = supported_color_modes + entity2.brightness = 255 + entity2.rgb_color = (255, 128, 64) + await async_setup_component( hass, LIGHT_DOMAIN, { LIGHT_DOMAIN: [ - {"platform": "demo"}, + {"platform": "test"}, { "platform": DOMAIN, "entities": [ @@ -1062,14 +1163,16 @@ async def test_service_calls(hass): await hass.async_start() await hass.async_block_till_done() - assert hass.states.get("light.light_group").state == STATE_ON + group_state = hass.states.get("light.light_group") + assert group_state.state == STATE_ON + assert group_state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [supported_color_modes] + await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: "light.light_group"}, blocking=True, ) - assert hass.states.get("light.bed_light").state == STATE_OFF assert hass.states.get("light.ceiling_lights").state == STATE_OFF assert hass.states.get("light.kitchen_lights").state == STATE_OFF @@ -1096,6 +1199,84 @@ async def test_service_calls(hass): assert hass.states.get("light.ceiling_lights").state == STATE_OFF assert hass.states.get("light.kitchen_lights").state == STATE_OFF + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "light.light_group", + ATTR_BRIGHTNESS: 128, + ATTR_RGB_COLOR: (42, 255, 255), + }, + blocking=True, + ) + + state = hass.states.get("light.bed_light") + assert state.state == STATE_ON + assert state.attributes[ATTR_BRIGHTNESS] == 128 + assert state.attributes[ATTR_RGB_COLOR] == (42, 255, 255) + + state = hass.states.get("light.ceiling_lights") + assert state.state == STATE_ON + assert state.attributes[ATTR_BRIGHTNESS] == 128 + assert state.attributes[ATTR_RGB_COLOR] == (42, 255, 255) + + state = hass.states.get("light.kitchen_lights") + assert state.state == STATE_ON + assert state.attributes[ATTR_BRIGHTNESS] == 128 + assert state.attributes[ATTR_RGB_COLOR] == (42, 255, 255) + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "light.light_group", + ATTR_BRIGHTNESS: 128, + ATTR_COLOR_NAME: "red", + }, + blocking=True, + ) + + state = hass.states.get("light.bed_light") + assert state.state == STATE_ON + assert state.attributes[ATTR_BRIGHTNESS] == 128 + assert state.attributes[ATTR_RGB_COLOR] == (255, 0, 0) + + state = hass.states.get("light.ceiling_lights") + assert state.state == STATE_ON + assert state.attributes[ATTR_BRIGHTNESS] == 128 + assert state.attributes[ATTR_RGB_COLOR] == (255, 0, 0) + + state = hass.states.get("light.kitchen_lights") + assert state.state == STATE_ON + assert state.attributes[ATTR_BRIGHTNESS] == 128 + assert state.attributes[ATTR_RGB_COLOR] == (255, 0, 0) + + +async def test_service_call_effect(hass): + """Test service calls.""" + await async_setup_component( + hass, + LIGHT_DOMAIN, + { + LIGHT_DOMAIN: [ + {"platform": "demo"}, + { + "platform": DOMAIN, + "entities": [ + "light.bed_light", + "light.ceiling_lights", + "light.kitchen_lights", + ], + }, + ] + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + assert hass.states.get("light.light_group").state == STATE_ON + await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index f3bd4583676..b1dd4afa18e 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -18,6 +18,7 @@ from homeassistant.const import ( ) from homeassistant.exceptions import Unauthorized from homeassistant.setup import async_setup_component +import homeassistant.util.color as color_util from tests.common import async_mock_service @@ -1589,6 +1590,83 @@ async def test_light_service_call_color_conversion(hass, enable_custom_integrati assert data == {"brightness": 128, "rgbww_color": (0, 75, 140, 255, 255)} +async def test_light_service_call_color_conversion_named_tuple( + hass, enable_custom_integrations +): + """Test a named tuple (RGBColor) is handled correctly.""" + platform = getattr(hass.components, "test.light") + platform.init(empty=True) + + platform.ENTITIES.append(platform.MockLight("Test_hs", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("Test_rgb", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("Test_xy", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("Test_all", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("Test_legacy", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("Test_rgbw", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("Test_rgbww", STATE_ON)) + + entity0 = platform.ENTITIES[0] + entity0.supported_color_modes = {light.COLOR_MODE_HS} + + entity1 = platform.ENTITIES[1] + entity1.supported_color_modes = {light.COLOR_MODE_RGB} + + entity2 = platform.ENTITIES[2] + entity2.supported_color_modes = {light.COLOR_MODE_XY} + + entity3 = platform.ENTITIES[3] + entity3.supported_color_modes = { + light.COLOR_MODE_HS, + light.COLOR_MODE_RGB, + light.COLOR_MODE_XY, + } + + entity4 = platform.ENTITIES[4] + entity4.supported_features = light.SUPPORT_COLOR + + entity5 = platform.ENTITIES[5] + entity5.supported_color_modes = {light.COLOR_MODE_RGBW} + + entity6 = platform.ENTITIES[6] + entity6.supported_color_modes = {light.COLOR_MODE_RGBWW} + + assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) + await hass.async_block_till_done() + + await hass.services.async_call( + "light", + "turn_on", + { + "entity_id": [ + entity0.entity_id, + entity1.entity_id, + entity2.entity_id, + entity3.entity_id, + entity4.entity_id, + entity5.entity_id, + entity6.entity_id, + ], + "brightness_pct": 25, + "rgb_color": color_util.RGBColor(128, 0, 0), + }, + blocking=True, + ) + _, data = entity0.last_call("turn_on") + assert data == {"brightness": 64, "hs_color": (0.0, 100.0)} + _, data = entity1.last_call("turn_on") + assert data == {"brightness": 64, "rgb_color": (128, 0, 0)} + _, data = entity2.last_call("turn_on") + assert data == {"brightness": 64, "xy_color": (0.701, 0.299)} + _, data = entity3.last_call("turn_on") + assert data == {"brightness": 64, "rgb_color": (128, 0, 0)} + _, data = entity4.last_call("turn_on") + assert data == {"brightness": 64, "hs_color": (0.0, 100.0)} + _, data = entity5.last_call("turn_on") + assert data == {"brightness": 64, "rgbw_color": (128, 0, 0, 0)} + _, data = entity6.last_call("turn_on") + assert data == {"brightness": 64, "rgbww_color": (128, 0, 0, 0, 0)} + + async def test_light_service_call_color_temp_emulation( hass, enable_custom_integrations ): From ec3f730b5ed3daff3a064cff27300f6c764cd01e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 29 Oct 2021 15:48:11 +0200 Subject: [PATCH 032/174] Convert RGBW and RGBWW colors in light turn_on calls (#58680) --- homeassistant/components/light/__init__.py | 36 ++++++ tests/components/light/test_init.py | 135 +++++++++++++++++++++ tests/components/tasmota/test_light.py | 16 +-- 3 files changed, 179 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index a6222c27284..c5ae88eaaa0 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -401,6 +401,14 @@ async def async_setup(hass, config): # noqa: C901 params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color) elif (xy_color := params.pop(ATTR_XY_COLOR, None)) is not None: params[ATTR_HS_COLOR] = color_util.color_xy_to_hs(*xy_color) + elif (rgbw_color := params.pop(ATTR_RGBW_COLOR, None)) is not None: + rgb_color = color_util.color_rgbw_to_rgb(*rgbw_color) + params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color) + elif (rgbww_color := params.pop(ATTR_RGBWW_COLOR, None)) is not None: + rgb_color = color_util.color_rgbww_to_rgb( + *rgbww_color, light.min_mireds, light.max_mireds + ) + params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color) elif ATTR_HS_COLOR in params and COLOR_MODE_HS not in supported_color_modes: hs_color = params.pop(ATTR_HS_COLOR) if COLOR_MODE_RGB in supported_color_modes: @@ -441,6 +449,34 @@ async def async_setup(hass, config): # noqa: C901 params[ATTR_RGBWW_COLOR] = color_util.color_rgb_to_rgbww( *rgb_color, light.min_mireds, light.max_mireds ) + elif ATTR_RGBW_COLOR in params and COLOR_MODE_RGBW not in supported_color_modes: + rgbw_color = params.pop(ATTR_RGBW_COLOR) + rgb_color = color_util.color_rgbw_to_rgb(*rgbw_color) + if COLOR_MODE_RGB in supported_color_modes: + params[ATTR_RGB_COLOR] = rgb_color + elif COLOR_MODE_RGBWW in supported_color_modes: + params[ATTR_RGBWW_COLOR] = color_util.color_rgb_to_rgbww( + *rgb_color, light.min_mireds, light.max_mireds + ) + elif COLOR_MODE_HS in supported_color_modes: + params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color) + elif COLOR_MODE_XY in supported_color_modes: + params[ATTR_XY_COLOR] = color_util.color_RGB_to_xy(*rgb_color) + elif ( + ATTR_RGBWW_COLOR in params and COLOR_MODE_RGBWW not in supported_color_modes + ): + rgbww_color = params.pop(ATTR_RGBWW_COLOR) + rgb_color = color_util.color_rgbww_to_rgb( + *rgbww_color, light.min_mireds, light.max_mireds + ) + if COLOR_MODE_RGB in supported_color_modes: + params[ATTR_RGB_COLOR] = rgb_color + elif COLOR_MODE_RGBW in supported_color_modes: + params[ATTR_RGBW_COLOR] = color_util.color_rgb_to_rgbw(*rgb_color) + elif COLOR_MODE_HS in supported_color_modes: + params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color) + elif COLOR_MODE_XY in supported_color_modes: + params[ATTR_XY_COLOR] = color_util.color_RGB_to_xy(*rgb_color) # If both white and brightness are specified, override white if ( diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index b1dd4afa18e..d51a5b64861 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -1589,6 +1589,141 @@ async def test_light_service_call_color_conversion(hass, enable_custom_integrati # The midpoint the the white channels is warm, compensated by adding green + blue assert data == {"brightness": 128, "rgbww_color": (0, 75, 140, 255, 255)} + await hass.services.async_call( + "light", + "turn_on", + { + "entity_id": [ + entity0.entity_id, + entity1.entity_id, + entity2.entity_id, + entity3.entity_id, + entity4.entity_id, + entity5.entity_id, + entity6.entity_id, + ], + "brightness_pct": 50, + "rgbw_color": (128, 0, 0, 64), + }, + blocking=True, + ) + _, data = entity0.last_call("turn_on") + assert data == {"brightness": 128, "hs_color": (0.0, 66.406)} + _, data = entity1.last_call("turn_on") + assert data == {"brightness": 128, "rgb_color": (128, 43, 43)} + _, data = entity2.last_call("turn_on") + assert data == {"brightness": 128, "xy_color": (0.592, 0.308)} + _, data = entity3.last_call("turn_on") + assert data == {"brightness": 128, "rgb_color": (128, 43, 43)} + _, data = entity4.last_call("turn_on") + assert data == {"brightness": 128, "hs_color": (0.0, 66.406)} + _, data = entity5.last_call("turn_on") + assert data == {"brightness": 128, "rgbw_color": (128, 0, 0, 64)} + _, data = entity6.last_call("turn_on") + # The midpoint the the white channels is warm, compensated by adding green + blue + assert data == {"brightness": 128, "rgbww_color": (128, 0, 30, 117, 117)} + + await hass.services.async_call( + "light", + "turn_on", + { + "entity_id": [ + entity0.entity_id, + entity1.entity_id, + entity2.entity_id, + entity3.entity_id, + entity4.entity_id, + entity5.entity_id, + entity6.entity_id, + ], + "brightness_pct": 50, + "rgbw_color": (255, 255, 255, 255), + }, + blocking=True, + ) + _, data = entity0.last_call("turn_on") + assert data == {"brightness": 128, "hs_color": (0.0, 0.0)} + _, data = entity1.last_call("turn_on") + assert data == {"brightness": 128, "rgb_color": (255, 255, 255)} + _, data = entity2.last_call("turn_on") + assert data == {"brightness": 128, "xy_color": (0.323, 0.329)} + _, data = entity3.last_call("turn_on") + assert data == {"brightness": 128, "rgb_color": (255, 255, 255)} + _, data = entity4.last_call("turn_on") + assert data == {"brightness": 128, "hs_color": (0.0, 0.0)} + _, data = entity5.last_call("turn_on") + assert data == {"brightness": 128, "rgbw_color": (255, 255, 255, 255)} + _, data = entity6.last_call("turn_on") + # The midpoint the the white channels is warm, compensated by adding green + blue + assert data == {"brightness": 128, "rgbww_color": (0, 76, 141, 255, 255)} + + await hass.services.async_call( + "light", + "turn_on", + { + "entity_id": [ + entity0.entity_id, + entity1.entity_id, + entity2.entity_id, + entity3.entity_id, + entity4.entity_id, + entity5.entity_id, + entity6.entity_id, + ], + "brightness_pct": 50, + "rgbww_color": (128, 0, 0, 64, 32), + }, + blocking=True, + ) + _, data = entity0.last_call("turn_on") + assert data == {"brightness": 128, "hs_color": (4.118, 79.688)} + _, data = entity1.last_call("turn_on") + assert data == {"brightness": 128, "rgb_color": (128, 33, 26)} + _, data = entity2.last_call("turn_on") + assert data == {"brightness": 128, "xy_color": (0.639, 0.312)} + _, data = entity3.last_call("turn_on") + assert data == {"brightness": 128, "rgb_color": (128, 33, 26)} + _, data = entity4.last_call("turn_on") + assert data == {"brightness": 128, "hs_color": (4.118, 79.688)} + _, data = entity5.last_call("turn_on") + assert data == {"brightness": 128, "rgbw_color": (128, 9, 0, 33)} + _, data = entity6.last_call("turn_on") + assert data == {"brightness": 128, "rgbww_color": (128, 0, 0, 64, 32)} + + await hass.services.async_call( + "light", + "turn_on", + { + "entity_id": [ + entity0.entity_id, + entity1.entity_id, + entity2.entity_id, + entity3.entity_id, + entity4.entity_id, + entity5.entity_id, + entity6.entity_id, + ], + "brightness_pct": 50, + "rgbww_color": (255, 255, 255, 255, 255), + }, + blocking=True, + ) + _, data = entity0.last_call("turn_on") + assert data == {"brightness": 128, "hs_color": (27.429, 27.451)} + _, data = entity1.last_call("turn_on") + assert data == {"brightness": 128, "rgb_color": (255, 217, 185)} + _, data = entity2.last_call("turn_on") + assert data == {"brightness": 128, "xy_color": (0.396, 0.359)} + _, data = entity3.last_call("turn_on") + assert data == {"brightness": 128, "rgb_color": (255, 217, 185)} + _, data = entity4.last_call("turn_on") + assert data == {"brightness": 128, "hs_color": (27.429, 27.451)} + _, data = entity5.last_call("turn_on") + # The midpoint the the white channels is warm, compensated by decreasing green + blue + assert data == {"brightness": 128, "rgbw_color": (96, 44, 0, 255)} + _, data = entity6.last_call("turn_on") + assert data == {"brightness": 128, "rgbww_color": (255, 255, 255, 255, 255)} + async def test_light_service_call_color_conversion_named_tuple( hass, enable_custom_integrations diff --git a/tests/components/tasmota/test_light.py b/tests/components/tasmota/test_light.py index 411567208db..a46a1f6851b 100644 --- a/tests/components/tasmota/test_light.py +++ b/tests/components/tasmota/test_light.py @@ -868,21 +868,21 @@ async def test_sending_mqtt_commands_rgbw_legacy(hass, mqtt_mock, setup_tasmota) ) mqtt_mock.async_publish.reset_mock() - # rgbw_color should be ignored + # rgbw_color should be converted await common.async_turn_on(hass, "light.test", rgbw_color=[128, 64, 32, 0]) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - "NoDelay;Power1 ON", + "NoDelay;Power1 ON;NoDelay;HsbColor1 20;NoDelay;HsbColor2 75", 0, False, ) mqtt_mock.async_publish.reset_mock() - # rgbw_color should be ignored + # rgbw_color should be converted await common.async_turn_on(hass, "light.test", rgbw_color=[16, 64, 32, 128]) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - "NoDelay;Power1 ON", + "NoDelay;Power1 ON;NoDelay;HsbColor1 141;NoDelay;HsbColor2 25", 0, False, ) @@ -974,21 +974,21 @@ async def test_sending_mqtt_commands_rgbw(hass, mqtt_mock, setup_tasmota): ) mqtt_mock.async_publish.reset_mock() - # rgbw_color should be ignored + # rgbw_color should be converted await common.async_turn_on(hass, "light.test", rgbw_color=[128, 64, 32, 0]) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - "NoDelay;Power1 ON", + "NoDelay;Power1 ON;NoDelay;HsbColor1 20;NoDelay;HsbColor2 75", 0, False, ) mqtt_mock.async_publish.reset_mock() - # rgbw_color should be ignored + # rgbw_color should be converted await common.async_turn_on(hass, "light.test", rgbw_color=[16, 64, 32, 128]) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - "NoDelay;Power1 ON", + "NoDelay;Power1 ON;NoDelay;HsbColor1 141;NoDelay;HsbColor2 25", 0, False, ) From 6d000c7d1a592ff1b0b62cdb0ef7966ed11c16a1 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 29 Oct 2021 15:59:16 +0200 Subject: [PATCH 033/174] Fix regression in MQTT discovery (#58684) * Fix regression in MQTT discovery * Update test --- homeassistant/components/mqtt/discovery.py | 5 +- tests/components/mqtt/test_discovery.py | 70 ++++++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index a237ea7aea1..ffc0c1de435 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -10,6 +10,7 @@ import time from homeassistant.const import CONF_DEVICE, CONF_PLATFORM from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import RESULT_TYPE_ABORT +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -141,7 +142,9 @@ async def async_start( # noqa: C901 if value[-1] == TOPIC_BASE and key.endswith("topic"): payload[key] = f"{value[:-1]}{base}" if payload.get(CONF_AVAILABILITY): - for availability_conf in payload[CONF_AVAILABILITY]: + for availability_conf in cv.ensure_list(payload[CONF_AVAILABILITY]): + if not isinstance(availability_conf, dict): + continue if topic := availability_conf.get(CONF_TOPIC): if topic[0] == TOPIC_BASE: availability_conf[CONF_TOPIC] = f"{base}{topic[1:]}" diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 8d2106c90fa..60c3961477b 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -504,6 +504,76 @@ async def test_discovery_expansion(hass, mqtt_mock, caplog): assert state.state == STATE_UNAVAILABLE +async def test_discovery_expansion_2(hass, mqtt_mock, caplog): + """Test expansion of abbreviated discovery payload.""" + data = ( + '{ "~": "some/base/topic",' + ' "name": "DiscoveryExpansionTest1",' + ' "stat_t": "test_topic/~",' + ' "cmd_t": "~/test_topic",' + ' "availability": {' + ' "topic":"~/avail_item1",' + ' "payload_available": "available",' + ' "payload_not_available": "not_available"' + " }," + ' "dev":{' + ' "ids":["5706DF"],' + ' "name":"DiscoveryExpansionTest1 Device",' + ' "mdl":"Generic",' + ' "sw":"1.2.3.4",' + ' "mf":"None",' + ' "sa":"default_area"' + " }" + "}" + ) + + async_fire_mqtt_message(hass, "homeassistant/switch/bla/config", data) + await hass.async_block_till_done() + + state = hass.states.get("switch.DiscoveryExpansionTest1") + assert state.state == STATE_UNAVAILABLE + + async_fire_mqtt_message(hass, "some/base/topic/avail_item1", "available") + await hass.async_block_till_done() + + state = hass.states.get("switch.DiscoveryExpansionTest1") + assert state is not None + assert state.name == "DiscoveryExpansionTest1" + assert ("switch", "bla") in hass.data[ALREADY_DISCOVERED] + assert state.state == STATE_OFF + + +@pytest.mark.no_fail_on_log_exception +async def test_discovery_expansion_3(hass, mqtt_mock, caplog): + """Test expansion of broken discovery payload.""" + data = ( + '{ "~": "some/base/topic",' + ' "name": "DiscoveryExpansionTest1",' + ' "stat_t": "test_topic/~",' + ' "cmd_t": "~/test_topic",' + ' "availability": "incorrect",' + ' "dev":{' + ' "ids":["5706DF"],' + ' "name":"DiscoveryExpansionTest1 Device",' + ' "mdl":"Generic",' + ' "sw":"1.2.3.4",' + ' "mf":"None",' + ' "sa":"default_area"' + " }" + "}" + ) + + async_fire_mqtt_message(hass, "homeassistant/switch/bla/config", data) + await hass.async_block_till_done() + assert hass.states.get("switch.DiscoveryExpansionTest1") is None + # Make sure the malformed availability data does not trip up discovery by asserting + # there are schema valdiation errors in the log + assert ( + "voluptuous.error.MultipleInvalid: expected a dictionary @ data['availability'][0]" + in caplog.text + ) + + ABBREVIATIONS_WHITE_LIST = [ # MQTT client/server/trigger settings "CONF_BIRTH_MESSAGE", From 76593d6473af910b16b2ed6f774a665a41299782 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 29 Oct 2021 13:21:57 +0200 Subject: [PATCH 034/174] Fix spelling of OctoPrint (#58686) --- homeassistant/components/octoprint/__init__.py | 2 +- .../components/octoprint/binary_sensor.py | 6 +++--- homeassistant/components/octoprint/sensor.py | 6 +++--- tests/components/octoprint/__init__.py | 4 ++-- tests/components/octoprint/test_binary_sensor.py | 8 ++++---- tests/components/octoprint/test_sensor.py | 14 +++++++------- 6 files changed, 20 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/octoprint/__init__.py b/homeassistant/components/octoprint/__init__.py index 7ee6a3169da..eee1ccd2814 100644 --- a/homeassistant/components/octoprint/__init__.py +++ b/homeassistant/components/octoprint/__init__.py @@ -47,7 +47,7 @@ def ensure_valid_path(value): PLATFORMS = ["binary_sensor", "sensor"] -DEFAULT_NAME = "Octoprint" +DEFAULT_NAME = "OctoPrint" CONF_NUMBER_OF_TOOLS = "number_of_tools" CONF_BED = "bed" diff --git a/homeassistant/components/octoprint/binary_sensor.py b/homeassistant/components/octoprint/binary_sensor.py index fe18af4f808..1adb04d3417 100644 --- a/homeassistant/components/octoprint/binary_sensor.py +++ b/homeassistant/components/octoprint/binary_sensor.py @@ -53,7 +53,7 @@ class OctoPrintBinarySensorBase(CoordinatorEntity, BinarySensorEntity): """Initialize a new OctoPrint sensor.""" super().__init__(coordinator) self._device_id = device_id - self._attr_name = f"Octoprint {sensor_type}" + self._attr_name = f"OctoPrint {sensor_type}" self._attr_unique_id = f"{sensor_type}-{device_id}" @property @@ -61,8 +61,8 @@ class OctoPrintBinarySensorBase(CoordinatorEntity, BinarySensorEntity): """Device info.""" return { "identifiers": {(COMPONENT_DOMAIN, self._device_id)}, - "manufacturer": "Octoprint", - "name": "Octoprint", + "manufacturer": "OctoPrint", + "name": "OctoPrint", } @property diff --git a/homeassistant/components/octoprint/sensor.py b/homeassistant/components/octoprint/sensor.py index 3feff099297..5a9614c69b4 100644 --- a/homeassistant/components/octoprint/sensor.py +++ b/homeassistant/components/octoprint/sensor.py @@ -76,7 +76,7 @@ class OctoPrintSensorBase(CoordinatorEntity, SensorEntity): """Initialize a new OctoPrint sensor.""" super().__init__(coordinator) self._device_id = device_id - self._attr_name = f"Octoprint {sensor_type}" + self._attr_name = f"OctoPrint {sensor_type}" self._attr_unique_id = f"{sensor_type}-{device_id}" @property @@ -84,8 +84,8 @@ class OctoPrintSensorBase(CoordinatorEntity, SensorEntity): """Device info.""" return { "identifiers": {(COMPONENT_DOMAIN, self._device_id)}, - "manufacturer": "Octoprint", - "name": "Octoprint", + "manufacturer": "OctoPrint", + "name": "OctoPrint", } diff --git a/tests/components/octoprint/__init__.py b/tests/components/octoprint/__init__.py index 4af4a2ea131..5176d2209b1 100644 --- a/tests/components/octoprint/__init__.py +++ b/tests/components/octoprint/__init__.py @@ -67,12 +67,12 @@ async def init_integration( data={ "host": "1.1.1.1", "api_key": "test-key", - "name": "Octoprint", + "name": "OctoPrint", "port": 81, "ssl": True, "path": "/", }, - title="Octoprint", + title="OctoPrint", ) config_entry.add_to_hass(hass) diff --git a/tests/components/octoprint/test_binary_sensor.py b/tests/components/octoprint/test_binary_sensor.py index 139ed0dc139..55e240eb282 100644 --- a/tests/components/octoprint/test_binary_sensor.py +++ b/tests/components/octoprint/test_binary_sensor.py @@ -21,14 +21,14 @@ async def test_sensors(hass): state = hass.states.get("binary_sensor.octoprint_printing") assert state is not None assert state.state == STATE_ON - assert state.name == "Octoprint Printing" + assert state.name == "OctoPrint Printing" entry = entity_registry.async_get("binary_sensor.octoprint_printing") assert entry.unique_id == "Printing-uuid" state = hass.states.get("binary_sensor.octoprint_printing_error") assert state is not None assert state.state == STATE_OFF - assert state.name == "Octoprint Printing Error" + assert state.name == "OctoPrint Printing Error" entry = entity_registry.async_get("binary_sensor.octoprint_printing_error") assert entry.unique_id == "Printing Error-uuid" @@ -42,13 +42,13 @@ async def test_sensors_printer_offline(hass): state = hass.states.get("binary_sensor.octoprint_printing") assert state is not None assert state.state == STATE_UNAVAILABLE - assert state.name == "Octoprint Printing" + assert state.name == "OctoPrint Printing" entry = entity_registry.async_get("binary_sensor.octoprint_printing") assert entry.unique_id == "Printing-uuid" state = hass.states.get("binary_sensor.octoprint_printing_error") assert state is not None assert state.state == STATE_UNAVAILABLE - assert state.name == "Octoprint Printing Error" + assert state.name == "OctoPrint Printing Error" entry = entity_registry.async_get("binary_sensor.octoprint_printing_error") assert entry.unique_id == "Printing Error-uuid" diff --git a/tests/components/octoprint/test_sensor.py b/tests/components/octoprint/test_sensor.py index 3954e9ffbca..c3a02c1bab5 100644 --- a/tests/components/octoprint/test_sensor.py +++ b/tests/components/octoprint/test_sensor.py @@ -29,48 +29,48 @@ async def test_sensors(hass): state = hass.states.get("sensor.octoprint_job_percentage") assert state is not None assert state.state == "50" - assert state.name == "Octoprint Job Percentage" + assert state.name == "OctoPrint Job Percentage" entry = entity_registry.async_get("sensor.octoprint_job_percentage") assert entry.unique_id == "Job Percentage-uuid" state = hass.states.get("sensor.octoprint_current_state") assert state is not None assert state.state == "Operational" - assert state.name == "Octoprint Current State" + assert state.name == "OctoPrint Current State" entry = entity_registry.async_get("sensor.octoprint_current_state") assert entry.unique_id == "Current State-uuid" state = hass.states.get("sensor.octoprint_actual_tool1_temp") assert state is not None assert state.state == "18.83" - assert state.name == "Octoprint actual tool1 temp" + assert state.name == "OctoPrint actual tool1 temp" entry = entity_registry.async_get("sensor.octoprint_actual_tool1_temp") assert entry.unique_id == "actual tool1 temp-uuid" state = hass.states.get("sensor.octoprint_target_tool1_temp") assert state is not None assert state.state == "37.83" - assert state.name == "Octoprint target tool1 temp" + assert state.name == "OctoPrint target tool1 temp" entry = entity_registry.async_get("sensor.octoprint_target_tool1_temp") assert entry.unique_id == "target tool1 temp-uuid" state = hass.states.get("sensor.octoprint_target_tool1_temp") assert state is not None assert state.state == "37.83" - assert state.name == "Octoprint target tool1 temp" + assert state.name == "OctoPrint target tool1 temp" entry = entity_registry.async_get("sensor.octoprint_target_tool1_temp") assert entry.unique_id == "target tool1 temp-uuid" state = hass.states.get("sensor.octoprint_start_time") assert state is not None assert state.state == "2020-02-20T09:00:00" - assert state.name == "Octoprint Start Time" + assert state.name == "OctoPrint Start Time" entry = entity_registry.async_get("sensor.octoprint_start_time") assert entry.unique_id == "Start Time-uuid" state = hass.states.get("sensor.octoprint_estimated_finish_time") assert state is not None assert state.state == "2020-02-20T10:50:00" - assert state.name == "Octoprint Estimated Finish Time" + assert state.name == "OctoPrint Estimated Finish Time" entry = entity_registry.async_get("sensor.octoprint_estimated_finish_time") assert entry.unique_id == "Estimated Finish Time-uuid" From 9821169cea402ac07c00811aaa0bc5b8a2fdc1fc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 29 Oct 2021 14:07:25 +0200 Subject: [PATCH 035/174] Fix OctoPrint config flow schema (#58688) --- homeassistant/components/octoprint/config_flow.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/octoprint/config_flow.py b/homeassistant/components/octoprint/config_flow.py index 9674806d49f..ea2013f29b9 100644 --- a/homeassistant/components/octoprint/config_flow.py +++ b/homeassistant/components/octoprint/config_flow.py @@ -25,11 +25,11 @@ _LOGGER = logging.getLogger(__name__) def _schema_with_defaults(username="", host="", port=80, path="/", ssl=False): return vol.Schema( { - vol.Required(CONF_USERNAME, default=username): cv.string, - vol.Required(CONF_HOST, default=host): cv.string, - vol.Optional(CONF_PORT, default=port): cv.port, - vol.Optional(CONF_PATH, default=path): cv.string, - vol.Optional(CONF_SSL, default=ssl): cv.boolean, + vol.Required(CONF_USERNAME, default=username): str, + vol.Required(CONF_HOST, default=host): str, + vol.Required(CONF_PORT, default=port): cv.port, + vol.Required(CONF_PATH, default=path): str, + vol.Required(CONF_SSL, default=ssl): bool, }, extra=vol.ALLOW_EXTRA, ) From 45dc01b8f41c72ac4aeb64e0adfc3f363fd21b29 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 30 Oct 2021 02:52:23 +0200 Subject: [PATCH 036/174] Fix round - wallbox (#58689) * Fix wallbox round * Add test case --- homeassistant/components/wallbox/__init__.py | 2 +- tests/components/wallbox/__init__.py | 2 +- tests/components/wallbox/const.py | 1 + tests/components/wallbox/test_sensor.py | 5 +++++ 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/wallbox/__init__.py b/homeassistant/components/wallbox/__init__.py index e5c8b7719a3..410a3115f9f 100644 --- a/homeassistant/components/wallbox/__init__.py +++ b/homeassistant/components/wallbox/__init__.py @@ -75,7 +75,7 @@ class WallboxCoordinator(DataUpdateCoordinator): filtered_data = {k: data[k] for k in CONF_SENSOR_TYPES if k in data} for key, value in filtered_data.items(): - if sensor_round := CONF_SENSOR_TYPES[key][CONF_ROUND]: + if (sensor_round := CONF_SENSOR_TYPES[key][CONF_ROUND]) is not None: try: filtered_data[key] = round(value, sensor_round) except TypeError: diff --git a/tests/components/wallbox/__init__.py b/tests/components/wallbox/__init__.py index 4a403d0afc8..f8031bd86a4 100644 --- a/tests/components/wallbox/__init__.py +++ b/tests/components/wallbox/__init__.py @@ -31,7 +31,7 @@ test_response = json.loads( json.dumps( { CONF_CHARGING_POWER_KEY: 0, - CONF_MAX_AVAILABLE_POWER_KEY: 25, + CONF_MAX_AVAILABLE_POWER_KEY: 25.2, CONF_CHARGING_SPEED_KEY: 0, CONF_ADDED_RANGE_KEY: "xx", CONF_ADDED_ENERGY_KEY: "44.697", diff --git a/tests/components/wallbox/const.py b/tests/components/wallbox/const.py index 3aa2dde38f0..9777602f6c9 100644 --- a/tests/components/wallbox/const.py +++ b/tests/components/wallbox/const.py @@ -8,3 +8,4 @@ CONF_STATUS = "status" CONF_MOCK_NUMBER_ENTITY_ID = "number.mock_title_max_charging_current" CONF_MOCK_SENSOR_CHARGING_SPEED_ID = "sensor.mock_title_charging_speed" CONF_MOCK_SENSOR_CHARGING_POWER_ID = "sensor.mock_title_charging_power" +CONF_MOCK_SENSOR_MAX_AVAILABLE_POWER = "sensor.mock_title_max_available_power" diff --git a/tests/components/wallbox/test_sensor.py b/tests/components/wallbox/test_sensor.py index 41dcd0e6ee0..2551eed6a2e 100644 --- a/tests/components/wallbox/test_sensor.py +++ b/tests/components/wallbox/test_sensor.py @@ -5,6 +5,7 @@ from tests.components.wallbox import entry, setup_integration from tests.components.wallbox.const import ( CONF_MOCK_SENSOR_CHARGING_POWER_ID, CONF_MOCK_SENSOR_CHARGING_SPEED_ID, + CONF_MOCK_SENSOR_MAX_AVAILABLE_POWER, ) @@ -21,4 +22,8 @@ async def test_wallbox_sensor_class(hass): assert state.attributes[CONF_ICON] == "mdi:speedometer" assert state.name == "Mock Title Charging Speed" + # Test round with precision '0' works + state = hass.states.get(CONF_MOCK_SENSOR_MAX_AVAILABLE_POWER) + assert state.state == "25.0" + await hass.config_entries.async_unload(entry.entry_id) From 9af8c608381e417665253b2db4afdd93a88df2fb Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 29 Oct 2021 16:34:27 +0200 Subject: [PATCH 037/174] Fix OctoPrint SSDP URL parsing and discovered values (#58698) --- homeassistant/components/octoprint/config_flow.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/octoprint/config_flow.py b/homeassistant/components/octoprint/config_flow.py index ea2013f29b9..acc1449bd96 100644 --- a/homeassistant/components/octoprint/config_flow.py +++ b/homeassistant/components/octoprint/config_flow.py @@ -1,9 +1,9 @@ """Config flow for OctoPrint integration.""" import logging -from urllib.parse import urlsplit from pyoctoprintapi import ApiError, OctoprintClient, OctoprintException import voluptuous as vol +from yarl import URL from homeassistant import config_entries, data_entry_flow, exceptions from homeassistant.const import ( @@ -162,14 +162,16 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(uuid) self._abort_if_unique_id_configured() - url = urlsplit(discovery_info["presentationURL"]) + url = URL(discovery_info["presentationURL"]) self.context["title_placeholders"] = { - CONF_HOST: url.hostname, + CONF_HOST: url.host, } self.discovery_schema = _schema_with_defaults( - host=url.hostname, + host=url.host, + path=url.path, port=url.port, + ssl=url.scheme == "https", ) return await self.async_step_user() From 23c407969f556348c89bab4fa3706969d84744b1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 29 Oct 2021 12:29:02 -0500 Subject: [PATCH 038/174] Avoid doorbird device probe during discovery for known devices (#58701) --- homeassistant/components/doorbird/config_flow.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/doorbird/config_flow.py b/homeassistant/components/doorbird/config_flow.py index 4c62fab17ef..01fcc2b2c22 100644 --- a/homeassistant/components/doorbird/config_flow.py +++ b/homeassistant/components/doorbird/config_flow.py @@ -99,13 +99,15 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="not_doorbird_device") if is_link_local(ip_address(host)): return self.async_abort(reason="link_local_address") - if not await async_verify_supported_device(self.hass, host): - return self.async_abort(reason="not_doorbird_device") await self.async_set_unique_id(macaddress) - self._abort_if_unique_id_configured(updates={CONF_HOST: host}) + self._async_abort_entries_match({CONF_HOST: host}) + + if not await async_verify_supported_device(self.hass, host): + return self.async_abort(reason="not_doorbird_device") + chop_ending = "._axis-video._tcp.local." friendly_hostname = discovery_info["name"] if friendly_hostname.endswith(chop_ending): From 9dda5c5f8ab1f60502c57554e50df1b15d059f60 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 29 Oct 2021 14:43:59 -0500 Subject: [PATCH 039/174] Disable polling Sonos switches by default (#58705) --- homeassistant/components/sonos/switch.py | 1 + tests/components/sonos/test_switch.py | 38 ++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/homeassistant/components/sonos/switch.py b/homeassistant/components/sonos/switch.py index 9af6c1eebec..830f3b09481 100644 --- a/homeassistant/components/sonos/switch.py +++ b/homeassistant/components/sonos/switch.py @@ -136,6 +136,7 @@ class SonosSwitchEntity(SonosEntity, SwitchEntity): self._attr_icon = FEATURE_ICONS.get(feature_type) if feature_type in POLL_REQUIRED: + self._attr_entity_registry_enabled_default = False self._attr_should_poll = True async def _async_poll(self) -> None: diff --git a/tests/components/sonos/test_switch.py b/tests/components/sonos/test_switch.py index 906695bdbaf..c2f997dbcb6 100644 --- a/tests/components/sonos/test_switch.py +++ b/tests/components/sonos/test_switch.py @@ -1,7 +1,10 @@ """Tests for the Sonos Alarm switch platform.""" from copy import copy +from datetime import timedelta +from unittest.mock import patch from homeassistant.components.sonos import DOMAIN +from homeassistant.components.sonos.const import DATA_SONOS_DISCOVERY_MANAGER from homeassistant.components.sonos.switch import ( ATTR_DURATION, ATTR_ID, @@ -10,9 +13,15 @@ from homeassistant.components.sonos.switch import ( ATTR_RECURRENCE, ATTR_VOLUME, ) +from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY from homeassistant.const import ATTR_TIME, STATE_ON from homeassistant.helpers.entity_registry import async_get as async_get_entity_registry from homeassistant.setup import async_setup_component +from homeassistant.util import dt + +from .conftest import SonosMockEvent + +from tests.common import async_fire_time_changed async def setup_platform(hass, config_entry, config): @@ -67,7 +76,36 @@ async def test_switch_attributes(hass, config_entry, config, soco): crossfade_state = hass.states.get(crossfade.entity_id) assert crossfade_state.state == STATE_ON + # Ensure switches are disabled status_light = entity_registry.entities["switch.sonos_zone_a_status_light"] + assert hass.states.get(status_light.entity_id) is None + + touch_controls = entity_registry.entities["switch.sonos_zone_a_touch_controls"] + assert hass.states.get(touch_controls.entity_id) is None + + # Enable disabled switches + for entity in (status_light, touch_controls): + entity_registry.async_update_entity( + entity_id=entity.entity_id, disabled_by=None + ) + await hass.async_block_till_done() + + # Fire event to cancel poll timer and avoid triggering errors during time jump + service = soco.contentDirectory + empty_event = SonosMockEvent(soco, service, {}) + subscription = service.subscribe.return_value + subscription.callback(event=empty_event) + await hass.async_block_till_done() + + # Mock shutdown calls during config entry reload + with patch.object(hass.data[DATA_SONOS_DISCOVERY_MANAGER], "async_shutdown") as m: + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), + ) + await hass.async_block_till_done() + assert m.called + status_light_state = hass.states.get(status_light.entity_id) assert status_light_state.state == STATE_ON From 8cfdd49f36659b0467833cf568641a54feba5256 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 29 Oct 2021 13:43:39 -0600 Subject: [PATCH 040/174] Bump aioambient to 2021.10.1 (#58708) --- homeassistant/components/ambient_station/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ambient_station/manifest.json b/homeassistant/components/ambient_station/manifest.json index 116a52f58c5..857ce6de585 100644 --- a/homeassistant/components/ambient_station/manifest.json +++ b/homeassistant/components/ambient_station/manifest.json @@ -3,7 +3,7 @@ "name": "Ambient Weather Station", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ambient_station", - "requirements": ["aioambient==2021.10.0"], + "requirements": ["aioambient==2021.10.1"], "codeowners": ["@bachya"], "iot_class": "cloud_push" } diff --git a/requirements_all.txt b/requirements_all.txt index 1a1033ec974..70fd65845e7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -133,7 +133,7 @@ aio_geojson_nsw_rfs_incidents==0.4 aio_georss_gdacs==0.5 # homeassistant.components.ambient_station -aioambient==2021.10.0 +aioambient==2021.10.1 # homeassistant.components.asuswrt aioasuswrt==1.3.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index be242f88af9..042528056ab 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ aio_geojson_nsw_rfs_incidents==0.4 aio_georss_gdacs==0.5 # homeassistant.components.ambient_station -aioambient==2021.10.0 +aioambient==2021.10.1 # homeassistant.components.asuswrt aioasuswrt==1.3.4 From d7531096ef387019b164c9fbd1528b079b1594e4 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sat, 30 Oct 2021 00:04:57 +0200 Subject: [PATCH 041/174] reload service: remove entities before disconnection (#58712) --- homeassistant/components/knx/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 54d4b4b7237..57c88b84cc7 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -239,15 +239,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # First check for config file. If for some reason it is no longer there # or knx is no longer mentioned, stop the reload. config = await async_integration_yaml_config(hass, DOMAIN) - if not config or DOMAIN not in config: return - await knx_module.xknx.stop() - await asyncio.gather( *(platform.async_reset() for platform in async_get_platforms(hass, DOMAIN)) ) + await knx_module.xknx.stop() await async_setup(hass, config) From f930a1fb0694586782f3977020539e80be3ecd73 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 29 Oct 2021 23:17:15 -0600 Subject: [PATCH 042/174] Fix bug with volumes in SimpliSafe set_system_properties service (#58721) Co-authored-by: Paulus Schoutsen --- homeassistant/components/simplisafe/__init__.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index ae5c3cd9527..b3bb850244e 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -118,7 +118,12 @@ PLATFORMS = ( "sensor", ) -VOLUMES = [VOLUME_OFF, VOLUME_LOW, VOLUME_MEDIUM, VOLUME_HIGH] +VOLUME_MAP = { + "high": VOLUME_HIGH, + "low": VOLUME_LOW, + "medium": VOLUME_MEDIUM, + "off": VOLUME_OFF, +} SERVICE_BASE_SCHEMA = vol.Schema({vol.Required(ATTR_SYSTEM_ID): cv.positive_int}) @@ -137,8 +142,8 @@ SERVICE_SET_SYSTEM_PROPERTIES_SCHEMA = SERVICE_BASE_SCHEMA.extend( lambda value: value.total_seconds(), vol.Range(min=30, max=480), ), - vol.Optional(ATTR_ALARM_VOLUME): vol.All(vol.Coerce(int), vol.In(VOLUMES)), - vol.Optional(ATTR_CHIME_VOLUME): vol.All(vol.Coerce(int), vol.In(VOLUMES)), + vol.Optional(ATTR_ALARM_VOLUME): vol.All(vol.In(VOLUME_MAP), VOLUME_MAP.get), + vol.Optional(ATTR_CHIME_VOLUME): vol.All(vol.In(VOLUME_MAP), VOLUME_MAP.get), vol.Optional(ATTR_ENTRY_DELAY_AWAY): vol.All( cv.time_period, lambda value: value.total_seconds(), @@ -157,7 +162,7 @@ SERVICE_SET_SYSTEM_PROPERTIES_SCHEMA = SERVICE_BASE_SCHEMA.extend( ), vol.Optional(ATTR_LIGHT): cv.boolean, vol.Optional(ATTR_VOICE_PROMPT_VOLUME): vol.All( - vol.Coerce(int), vol.In(VOLUMES) + vol.In(VOLUME_MAP), VOLUME_MAP.get ), } ) From 030f19c1ed64e72e7c8431885e22abde7cb0f304 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 29 Oct 2021 19:57:01 -0500 Subject: [PATCH 043/174] Improve handling of invalid serial numbers in HomeKit Controller (#58723) Fixes #58719 --- .../components/homekit_controller/__init__.py | 6 +++--- .../components/homekit_controller/connection.py | 12 +++++++++++- .../specific_devices/test_ryse_smart_bridge.py | 2 +- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index bab989ba9bc..f91355906dc 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -19,7 +19,7 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.entity import DeviceInfo, Entity from .config_flow import normalize_hkid -from .connection import HKDevice +from .connection import HKDevice, valid_serial_number from .const import ( CONTROLLER, DOMAIN, @@ -141,7 +141,7 @@ class HomeKitEntity(Entity): """Return the ID of this device.""" info = self.accessory_info serial = info.value(CharacteristicsTypes.SERIAL_NUMBER) - if serial: + if valid_serial_number(serial): return f"homekit-{serial}-{self._iid}" # Some accessories do not have a serial number return f"homekit-{self._accessory.unique_id}-{self._aid}-{self._iid}" @@ -161,7 +161,7 @@ class HomeKitEntity(Entity): """Return the device info.""" info = self.accessory_info accessory_serial = info.value(CharacteristicsTypes.SERIAL_NUMBER) - if accessory_serial: + if valid_serial_number(accessory_serial): # Some accessories do not have a serial number identifier = (DOMAIN, IDENTIFIER_SERIAL_NUMBER, accessory_serial) else: diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index cf8381a9a28..8523fec7b8f 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -36,6 +36,16 @@ MAX_POLL_FAILURES_TO_DECLARE_UNAVAILABLE = 3 _LOGGER = logging.getLogger(__name__) +def valid_serial_number(serial): + """Return if the serial number appears to be valid.""" + if not serial: + return False + try: + return float("".join(serial.rsplit(".", 1))) > 1 + except ValueError: + return True + + def get_accessory_information(accessory): """Obtain the accessory information service of a HomeKit device.""" result = {} @@ -211,7 +221,7 @@ class HKDevice: serial_number = info.value(CharacteristicsTypes.SERIAL_NUMBER) - if serial_number: + if valid_serial_number(serial_number): identifiers = {(DOMAIN, IDENTIFIER_SERIAL_NUMBER, serial_number)} else: # Some accessories do not have a serial number diff --git a/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py b/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py index 8430919297c..ad5180658ad 100644 --- a/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py +++ b/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py @@ -19,7 +19,7 @@ async def test_ryse_smart_bridge_setup(hass): # Check that the cover.master_bath_south is correctly found and set up cover_id = "cover.master_bath_south" cover = entity_registry.async_get(cover_id) - assert cover.unique_id == "homekit-1.0.0-48" + assert cover.unique_id == "homekit-00:00:00:00:00:00-2-48" cover_helper = Helper( hass, From 94fb8adbdcf57f7a976d5a86026b31556e1fa213 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 29 Oct 2021 22:18:17 -0700 Subject: [PATCH 044/174] Bumped version to 2021.11.0b2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index c8ae44d9fbd..b5edbd66a86 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 11 -PATCH_VERSION: Final = "0b1" +PATCH_VERSION: Final = "0b2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From 2ea90b803c4f01c52ad98f3e095c45b5dbcc026d Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 31 Oct 2021 20:12:25 +0100 Subject: [PATCH 045/174] Add configuration url to AVM Fritz!Smarthome (#57711) * add configuration url * extend data update coordinator * improve exception handling during data update * store coordinator after first refresh * fix light init --- homeassistant/components/fritzbox/__init__.py | 62 +++-------------- .../components/fritzbox/binary_sensor.py | 4 +- .../components/fritzbox/coordinator.py | 68 +++++++++++++++++++ homeassistant/components/fritzbox/light.py | 6 +- tests/components/fritzbox/test_init.py | 18 ++++- 5 files changed, 98 insertions(+), 60 deletions(-) create mode 100644 homeassistant/components/fritzbox/coordinator.py diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index 0ddd0b8d417..e72e1d86fc1 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -1,10 +1,7 @@ """Support for AVM FRITZ!SmartHome devices.""" from __future__ import annotations -from datetime import timedelta - from pyfritzhome import Fritzhome, FritzhomeDevice, LoginError -import requests from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -18,10 +15,7 @@ from homeassistant.core import Event, HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( ATTR_STATE_DEVICE_LOCKED, @@ -32,6 +26,7 @@ from .const import ( LOGGER, PLATFORMS, ) +from .coordinator import FritzboxDataUpdateCoordinator from .model import FritzExtraAttributes @@ -53,52 +48,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: CONF_CONNECTIONS: fritz, } - def _update_fritz_devices() -> dict[str, FritzhomeDevice]: - """Update all fritzbox device data.""" - try: - devices = fritz.get_devices() - except requests.exceptions.HTTPError: - # If the device rebooted, login again - try: - fritz.login() - except requests.exceptions.HTTPError as ex: - raise ConfigEntryAuthFailed from ex - devices = fritz.get_devices() - - data = {} - fritz.update_devices() - for device in devices: - # assume device as unavailable, see #55799 - if ( - device.has_powermeter - and device.present - and hasattr(device, "voltage") - and device.voltage <= 0 - and device.power <= 0 - and device.energy <= 0 - ): - LOGGER.debug("Assume device %s as unavailable", device.name) - device.present = False - - data[device.ain] = device - return data - - async def async_update_coordinator() -> dict[str, FritzhomeDevice]: - """Fetch all device data.""" - return await hass.async_add_executor_job(_update_fritz_devices) - - hass.data[DOMAIN][entry.entry_id][ - CONF_COORDINATOR - ] = coordinator = DataUpdateCoordinator( - hass, - LOGGER, - name=f"{entry.entry_id}", - update_method=async_update_coordinator, - update_interval=timedelta(seconds=30), - ) + coordinator = FritzboxDataUpdateCoordinator(hass, entry) await coordinator.async_config_entry_first_refresh() + hass.data[DOMAIN][entry.entry_id][CONF_COORDINATOR] = coordinator + def _update_unique_id(entry: RegistryEntry) -> dict[str, str] | None: """Update unique ID of entity entry.""" if ( @@ -142,9 +97,11 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class FritzBoxEntity(CoordinatorEntity): """Basis FritzBox entity.""" + coordinator: FritzboxDataUpdateCoordinator + def __init__( self, - coordinator: DataUpdateCoordinator[dict[str, FritzhomeDevice]], + coordinator: FritzboxDataUpdateCoordinator, ain: str, entity_description: EntityDescription | None = None, ) -> None: @@ -174,11 +131,12 @@ class FritzBoxEntity(CoordinatorEntity): def device_info(self) -> DeviceInfo: """Return device specific attributes.""" return DeviceInfo( + name=self.device.name, identifiers={(DOMAIN, self.ain)}, manufacturer=self.device.manufacturer, model=self.device.productname, - name=self.device.name, sw_version=self.device.fw_version, + configuration_url=self.coordinator.configuration_url, ) @property diff --git a/homeassistant/components/fritzbox/binary_sensor.py b/homeassistant/components/fritzbox/binary_sensor.py index 1317710c570..b0f5e63d424 100644 --- a/homeassistant/components/fritzbox/binary_sensor.py +++ b/homeassistant/components/fritzbox/binary_sensor.py @@ -15,10 +15,10 @@ from homeassistant.components.binary_sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import FritzBoxEntity from .const import CONF_COORDINATOR, DOMAIN as FRITZBOX_DOMAIN +from .coordinator import FritzboxDataUpdateCoordinator from .model import FritzEntityDescriptionMixinBase @@ -70,7 +70,7 @@ class FritzboxBinarySensor(FritzBoxEntity, BinarySensorEntity): def __init__( self, - coordinator: DataUpdateCoordinator[dict[str, FritzhomeDevice]], + coordinator: FritzboxDataUpdateCoordinator, ain: str, entity_description: FritzBinarySensorEntityDescription, ) -> None: diff --git a/homeassistant/components/fritzbox/coordinator.py b/homeassistant/components/fritzbox/coordinator.py new file mode 100644 index 00000000000..69ab0b4c274 --- /dev/null +++ b/homeassistant/components/fritzbox/coordinator.py @@ -0,0 +1,68 @@ +"""Data update coordinator for AVM FRITZ!SmartHome devices.""" +from __future__ import annotations + +from datetime import timedelta + +from pyfritzhome import Fritzhome, FritzhomeDevice, LoginError +import requests + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import CONF_CONNECTIONS, DOMAIN, LOGGER + + +class FritzboxDataUpdateCoordinator(DataUpdateCoordinator): + """Fritzbox Smarthome device data update coordinator.""" + + configuration_url: str + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Initialize the Fritzbox Smarthome device coordinator.""" + self.entry = entry + self.fritz: Fritzhome = hass.data[DOMAIN][self.entry.entry_id][CONF_CONNECTIONS] + self.configuration_url = self.fritz.get_prefixed_host() + super().__init__( + hass, + LOGGER, + name=entry.entry_id, + update_interval=timedelta(seconds=30), + ) + + def _update_fritz_devices(self) -> dict[str, FritzhomeDevice]: + """Update all fritzbox device data.""" + try: + devices = self.fritz.get_devices() + except requests.exceptions.ConnectionError as ex: + raise ConfigEntryNotReady from ex + except requests.exceptions.HTTPError: + # If the device rebooted, login again + try: + self.fritz.login() + except LoginError as ex: + raise ConfigEntryAuthFailed from ex + devices = self.fritz.get_devices() + + data = {} + self.fritz.update_devices() + for device in devices: + # assume device as unavailable, see #55799 + if ( + device.has_powermeter + and device.present + and hasattr(device, "voltage") + and device.voltage <= 0 + and device.power <= 0 + and device.energy <= 0 + ): + LOGGER.debug("Assume device %s as unavailable", device.name) + device.present = False + + data[device.ain] = device + return data + + async def _async_update_data(self) -> dict[str, FritzhomeDevice]: + """Fetch all device data.""" + return await self.hass.async_add_executor_job(self._update_fritz_devices) diff --git a/homeassistant/components/fritzbox/light.py b/homeassistant/components/fritzbox/light.py index 3f9e3cabfa2..272d170e13d 100644 --- a/homeassistant/components/fritzbox/light.py +++ b/homeassistant/components/fritzbox/light.py @@ -3,8 +3,6 @@ from __future__ import annotations from typing import Any -from pyfritzhome.fritzhomedevice import FritzhomeDevice - from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, @@ -16,7 +14,6 @@ from homeassistant.components.light import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.util import color from . import FritzBoxEntity @@ -26,6 +23,7 @@ from .const import ( CONF_COORDINATOR, DOMAIN as FRITZBOX_DOMAIN, ) +from .coordinator import FritzboxDataUpdateCoordinator SUPPORTED_COLOR_MODES = {COLOR_MODE_COLOR_TEMP, COLOR_MODE_HS} @@ -64,7 +62,7 @@ class FritzboxLight(FritzBoxEntity, LightEntity): def __init__( self, - coordinator: DataUpdateCoordinator[dict[str, FritzhomeDevice]], + coordinator: FritzboxDataUpdateCoordinator, ain: str, supported_colors: dict, supported_color_temps: list[str], diff --git a/tests/components/fritzbox/test_init.py b/tests/components/fritzbox/test_init.py index ea0356c6af1..60828e83801 100644 --- a/tests/components/fritzbox/test_init.py +++ b/tests/components/fritzbox/test_init.py @@ -4,7 +4,7 @@ from __future__ import annotations from unittest.mock import Mock, call, patch from pyfritzhome import LoginError -from requests.exceptions import HTTPError +from requests.exceptions import ConnectionError, HTTPError from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN @@ -120,13 +120,27 @@ async def test_coordinator_update_after_password_change( ) entry.add_to_hass(hass) fritz().get_devices.side_effect = HTTPError() - fritz().login.side_effect = ["", HTTPError()] + fritz().login.side_effect = ["", LoginError("some_user")] assert not await hass.config_entries.async_setup(entry.entry_id) assert fritz().get_devices.call_count == 1 assert fritz().login.call_count == 2 +async def test_coordinator_update_when_unreachable(hass: HomeAssistant, fritz: Mock): + """Test coordinator after reboot.""" + entry = MockConfigEntry( + domain=FB_DOMAIN, + data=MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], + unique_id="any", + ) + entry.add_to_hass(hass) + fritz().get_devices.side_effect = [ConnectionError(), ""] + + assert not await hass.config_entries.async_setup(entry.entry_id) + assert entry.state is ConfigEntryState.SETUP_RETRY + + async def test_unload_remove(hass: HomeAssistant, fritz: Mock): """Test unload and remove of integration.""" fritz().get_devices.return_value = [FritzDeviceSwitchMock()] From 4086a40c05892cd26574f05fbaec4ad3bfd959fe Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 31 Oct 2021 20:21:46 -0700 Subject: [PATCH 046/174] Mobile app to update entity registry on re-register sensors (#58378) Co-authored-by: J. Nick Koston --- .../components/mobile_app/binary_sensor.py | 2 + homeassistant/components/mobile_app/sensor.py | 2 + .../components/mobile_app/webhook.py | 20 +++++ homeassistant/helpers/entity.py | 5 +- homeassistant/helpers/entity_registry.py | 82 +++++++++++-------- tests/components/mobile_app/test_webhook.py | 56 +++++++++++++ 6 files changed, 128 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/mobile_app/binary_sensor.py b/homeassistant/components/mobile_app/binary_sensor.py index 616cd97a775..4e2822a4c3c 100644 --- a/homeassistant/components/mobile_app/binary_sensor.py +++ b/homeassistant/components/mobile_app/binary_sensor.py @@ -9,6 +9,7 @@ from .const import ( ATTR_DEVICE_NAME, ATTR_SENSOR_ATTRIBUTES, ATTR_SENSOR_DEVICE_CLASS, + ATTR_SENSOR_ENTITY_CATEGORY, ATTR_SENSOR_ICON, ATTR_SENSOR_NAME, ATTR_SENSOR_STATE, @@ -40,6 +41,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ATTR_SENSOR_STATE: None, ATTR_SENSOR_TYPE: entry.domain, ATTR_SENSOR_UNIQUE_ID: entry.unique_id, + ATTR_SENSOR_ENTITY_CATEGORY: entry.entity_category, } entities.append(MobileAppBinarySensor(config, entry.device_id, config_entry)) diff --git a/homeassistant/components/mobile_app/sensor.py b/homeassistant/components/mobile_app/sensor.py index 9d56e55a106..89c8d437628 100644 --- a/homeassistant/components/mobile_app/sensor.py +++ b/homeassistant/components/mobile_app/sensor.py @@ -11,6 +11,7 @@ from .const import ( ATTR_DEVICE_NAME, ATTR_SENSOR_ATTRIBUTES, ATTR_SENSOR_DEVICE_CLASS, + ATTR_SENSOR_ENTITY_CATEGORY, ATTR_SENSOR_ICON, ATTR_SENSOR_NAME, ATTR_SENSOR_STATE, @@ -45,6 +46,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ATTR_SENSOR_TYPE: entry.domain, ATTR_SENSOR_UNIQUE_ID: entry.unique_id, ATTR_SENSOR_UOM: entry.unit_of_measurement, + ATTR_SENSOR_ENTITY_CATEGORY: entry.entity_category, } entities.append(MobileAppSensor(config, entry.device_id, config_entry)) diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 7d8b6ad4b53..fd6bd81fb6c 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -446,6 +446,26 @@ async def webhook_register_sensor(hass, config_entry, data): "Re-register for %s of existing sensor %s", device_name, unique_id ) + entry = entity_registry.async_get(existing_sensor) + changes = {} + + if ( + new_name := f"{device_name} {data[ATTR_SENSOR_NAME]}" + ) != entry.original_name: + changes["original_name"] = new_name + + for ent_reg_key, data_key in ( + ("device_class", ATTR_SENSOR_DEVICE_CLASS), + ("unit_of_measurement", ATTR_SENSOR_UOM), + ("entity_category", ATTR_SENSOR_ENTITY_CATEGORY), + ("original_icon", ATTR_SENSOR_ICON), + ): + if data_key in data and getattr(entry, ent_reg_key) != data[data_key]: + changes[ent_reg_key] = data[data_key] + + if changes: + entity_registry.async_update_entity(existing_sensor, **changes) + async_dispatcher_send(hass, SIGNAL_SENSOR_UPDATE, data) else: register_signal = f"{DOMAIN}_{data[ATTR_SENSOR_TYPE]}_register" diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index a05d2c7c2fa..c76aefc3fa9 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -39,7 +39,6 @@ from homeassistant.core import CALLBACK_TYPE, Context, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError, NoEntitySpecifiedError from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import EntityPlatform -from homeassistant.helpers.entity_registry import RegistryEntry from homeassistant.helpers.event import Event, async_track_entity_registry_updated_event from homeassistant.helpers.typing import StateType from homeassistant.loader import bind_hass @@ -233,7 +232,7 @@ class Entity(ABC): parallel_updates: asyncio.Semaphore | None = None # Entry in the entity registry - registry_entry: RegistryEntry | None = None + registry_entry: er.RegistryEntry | None = None # Hold list for functions to call on remove. _on_remove: list[CALLBACK_TYPE] | None = None @@ -812,7 +811,7 @@ class Entity(ABC): if data["action"] != "update": return - ent_reg = await self.hass.helpers.entity_registry.async_get_registry() + ent_reg = er.async_get(self.hass) old = self.registry_entry self.registry_entry = ent_reg.async_get(data["entity_id"]) assert self.registry_entry is not None diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index bedbdc51785..b7b0eed2f32 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -243,21 +243,21 @@ class EntityRegistry: unique_id: str, *, # To influence entity ID generation - suggested_object_id: str | None = None, known_object_ids: Iterable[str] | None = None, + suggested_object_id: str | None = None, # To disable an entity if it gets created disabled_by: str | None = None, # Data that we want entry to have - config_entry: ConfigEntry | None = None, - device_id: str | None = None, area_id: str | None = None, capabilities: Mapping[str, Any] | None = None, - supported_features: int | None = None, + config_entry: ConfigEntry | None = None, device_class: str | None = None, - unit_of_measurement: str | None = None, - original_name: str | None = None, - original_icon: str | None = None, + device_id: str | None = None, entity_category: str | None = None, + original_icon: str | None = None, + original_name: str | None = None, + supported_features: int | None = None, + unit_of_measurement: str | None = None, ) -> RegistryEntry: """Get entity. Create if it doesn't exist.""" config_entry_id = None @@ -300,20 +300,20 @@ class EntityRegistry: disabled_by = DISABLED_INTEGRATION entity = RegistryEntry( - entity_id=entity_id, - config_entry_id=config_entry_id, - device_id=device_id, area_id=area_id, - unique_id=unique_id, - platform=platform, - disabled_by=disabled_by, capabilities=capabilities, - supported_features=supported_features or 0, + config_entry_id=config_entry_id, device_class=device_class, - unit_of_measurement=unit_of_measurement, - original_name=original_name, - original_icon=original_icon, + device_id=device_id, + disabled_by=disabled_by, entity_category=entity_category, + entity_id=entity_id, + original_icon=original_icon, + original_name=original_name, + platform=platform, + supported_features=supported_features or 0, + unique_id=unique_id, + unit_of_measurement=unit_of_measurement, ) self._register_entry(entity) _LOGGER.info("Registered new %s.%s entity: %s", domain, platform, entity_id) @@ -383,24 +383,34 @@ class EntityRegistry: self, entity_id: str, *, - name: str | None | UndefinedType = UNDEFINED, - icon: str | None | UndefinedType = UNDEFINED, - config_entry_id: str | None | UndefinedType = UNDEFINED, area_id: str | None | UndefinedType = UNDEFINED, + config_entry_id: str | None | UndefinedType = UNDEFINED, + device_class: str | None | UndefinedType = UNDEFINED, + disabled_by: str | None | UndefinedType = UNDEFINED, + entity_category: str | None | UndefinedType = UNDEFINED, + icon: str | None | UndefinedType = UNDEFINED, + name: str | None | UndefinedType = UNDEFINED, new_entity_id: str | UndefinedType = UNDEFINED, new_unique_id: str | UndefinedType = UNDEFINED, - disabled_by: str | None | UndefinedType = UNDEFINED, + original_icon: str | None | UndefinedType = UNDEFINED, + original_name: str | None | UndefinedType = UNDEFINED, + unit_of_measurement: str | None | UndefinedType = UNDEFINED, ) -> RegistryEntry: """Update properties of an entity.""" return self._async_update_entity( entity_id, - name=name, - icon=icon, - config_entry_id=config_entry_id, area_id=area_id, + config_entry_id=config_entry_id, + device_class=device_class, + disabled_by=disabled_by, + entity_category=entity_category, + icon=icon, + name=name, new_entity_id=new_entity_id, new_unique_id=new_unique_id, - disabled_by=disabled_by, + original_icon=original_icon, + original_name=original_name, + unit_of_measurement=unit_of_measurement, ) @callback @@ -408,21 +418,21 @@ class EntityRegistry: self, entity_id: str, *, - name: str | None | UndefinedType = UNDEFINED, - icon: str | None | UndefinedType = UNDEFINED, - config_entry_id: str | None | UndefinedType = UNDEFINED, - new_entity_id: str | UndefinedType = UNDEFINED, - device_id: str | None | UndefinedType = UNDEFINED, area_id: str | None | UndefinedType = UNDEFINED, - new_unique_id: str | UndefinedType = UNDEFINED, - disabled_by: str | None | UndefinedType = UNDEFINED, capabilities: Mapping[str, Any] | None | UndefinedType = UNDEFINED, - supported_features: int | UndefinedType = UNDEFINED, + config_entry_id: str | None | UndefinedType = UNDEFINED, device_class: str | None | UndefinedType = UNDEFINED, - unit_of_measurement: str | None | UndefinedType = UNDEFINED, - original_name: str | None | UndefinedType = UNDEFINED, - original_icon: str | None | UndefinedType = UNDEFINED, + device_id: str | None | UndefinedType = UNDEFINED, + disabled_by: str | None | UndefinedType = UNDEFINED, entity_category: str | None | UndefinedType = UNDEFINED, + icon: str | None | UndefinedType = UNDEFINED, + name: str | None | UndefinedType = UNDEFINED, + new_entity_id: str | UndefinedType = UNDEFINED, + new_unique_id: str | UndefinedType = UNDEFINED, + original_icon: str | None | UndefinedType = UNDEFINED, + original_name: str | None | UndefinedType = UNDEFINED, + supported_features: int | UndefinedType = UNDEFINED, + unit_of_measurement: str | None | UndefinedType = UNDEFINED, ) -> RegistryEntry: """Private facing update properties method.""" old = self.entities[entity_id] diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index 623abf30e9e..41b939c7113 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -10,6 +10,7 @@ from homeassistant.components.zone import DOMAIN as ZONE_DOMAIN from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er from .const import CALL_SERVICE, FIRE_EVENT, REGISTER_CLEARTEXT, RENDER_TEMPLATE, UPDATE @@ -515,3 +516,58 @@ async def test_register_sensor_limits_state_class( # This means it was ignored. assert reg_resp.status == HTTPStatus.OK + + +async def test_reregister_sensor(hass, create_registrations, webhook_client): + """Test that we can add more info in re-registration.""" + webhook_id = create_registrations[1]["webhook_id"] + webhook_url = f"/api/webhook/{webhook_id}" + + reg_resp = await webhook_client.post( + webhook_url, + json={ + "type": "register_sensor", + "data": { + "name": "Battery State", + "state": 100, + "type": "sensor", + "unique_id": "abcd", + }, + }, + ) + + assert reg_resp.status == HTTPStatus.CREATED + + ent_reg = er.async_get(hass) + entry = ent_reg.async_get("sensor.test_1_battery_state") + assert entry.original_name == "Test 1 Battery State" + assert entry.device_class is None + assert entry.unit_of_measurement is None + assert entry.entity_category is None + assert entry.original_icon == "mdi:cellphone" + + reg_resp = await webhook_client.post( + webhook_url, + json={ + "type": "register_sensor", + "data": { + "name": "New Name", + "state": 100, + "type": "sensor", + "unique_id": "abcd", + "state_class": "total", + "device_class": "battery", + "entity_category": "diagnostic", + "icon": "mdi:new-icon", + "unit_of_measurement": "%", + }, + }, + ) + + assert reg_resp.status == HTTPStatus.CREATED + entry = ent_reg.async_get("sensor.test_1_battery_state") + assert entry.original_name == "Test 1 New Name" + assert entry.device_class == "battery" + assert entry.unit_of_measurement == "%" + assert entry.entity_category == "diagnostic" + assert entry.original_icon == "mdi:new-icon" From 8c2af76a51429c0463d68aea2bd6f3a4fa7f08b1 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 30 Oct 2021 16:50:24 +0200 Subject: [PATCH 047/174] Coerce to tuple before asserting the sequence (#58672) --- homeassistant/components/flux_led/light.py | 2 +- homeassistant/components/lifx/light.py | 6 +++--- homeassistant/components/opencv/image_processing.py | 2 +- homeassistant/components/yeelight/light.py | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index 6f1db96a7aa..f1fa4ed7dbb 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -165,7 +165,7 @@ CUSTOM_EFFECT_DICT: Final = { vol.Required(CONF_COLORS): vol.All( cv.ensure_list, vol.Length(min=1, max=16), - [vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)), vol.Coerce(tuple))], + [vol.All(vol.Coerce(tuple), vol.ExactSequence((cv.byte, cv.byte, cv.byte)))], ), vol.Optional(CONF_SPEED_PCT, default=50): vol.All( vol.Range(min=0, max=100), vol.Coerce(int) diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index 106c66c8900..998b99ef88f 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -119,19 +119,19 @@ LIFX_EFFECT_PULSE_SCHEMA = cv.make_entity_service_schema( ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT, vol.Exclusive(ATTR_COLOR_NAME, COLOR_GROUP): cv.string, vol.Exclusive(ATTR_RGB_COLOR, COLOR_GROUP): vol.All( - vol.ExactSequence((cv.byte, cv.byte, cv.byte)), vol.Coerce(tuple) + vol.Coerce(tuple), vol.ExactSequence((cv.byte, cv.byte, cv.byte)) ), vol.Exclusive(ATTR_XY_COLOR, COLOR_GROUP): vol.All( - vol.ExactSequence((cv.small_float, cv.small_float)), vol.Coerce(tuple) + vol.Coerce(tuple), vol.ExactSequence((cv.small_float, cv.small_float)) ), vol.Exclusive(ATTR_HS_COLOR, COLOR_GROUP): vol.All( + vol.Coerce(tuple), vol.ExactSequence( ( vol.All(vol.Coerce(float), vol.Range(min=0, max=360)), vol.All(vol.Coerce(float), vol.Range(min=0, max=100)), ) ), - vol.Coerce(tuple), ), vol.Exclusive(ATTR_COLOR_TEMP, COLOR_GROUP): vol.All( vol.Coerce(int), vol.Range(min=1) diff --git a/homeassistant/components/opencv/image_processing.py b/homeassistant/components/opencv/image_processing.py index bf63ec0bfff..9228ab26ec5 100644 --- a/homeassistant/components/opencv/image_processing.py +++ b/homeassistant/components/opencv/image_processing.py @@ -62,7 +62,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( CONF_NEIGHBORS, DEFAULT_NEIGHBORS ): cv.positive_int, vol.Optional(CONF_MIN_SIZE, DEFAULT_MIN_SIZE): vol.Schema( - vol.All(vol.ExactSequence([int, int]), vol.Coerce(tuple)) + vol.All(vol.Coerce(tuple), vol.ExactSequence([int, int])) ), } ), diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index d70845ae86d..a6b51046fc6 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -180,20 +180,20 @@ SERVICE_SCHEMA_START_FLOW = YEELIGHT_FLOW_TRANSITION_SCHEMA SERVICE_SCHEMA_SET_COLOR_SCENE = { vol.Required(ATTR_RGB_COLOR): vol.All( - vol.ExactSequence((cv.byte, cv.byte, cv.byte)), vol.Coerce(tuple) + vol.Coerce(tuple), vol.ExactSequence((cv.byte, cv.byte, cv.byte)) ), vol.Required(ATTR_BRIGHTNESS): VALID_BRIGHTNESS, } SERVICE_SCHEMA_SET_HSV_SCENE = { vol.Required(ATTR_HS_COLOR): vol.All( + vol.Coerce(tuple), vol.ExactSequence( ( vol.All(vol.Coerce(float), vol.Range(min=0, max=359)), vol.All(vol.Coerce(float), vol.Range(min=0, max=100)), ) ), - vol.Coerce(tuple), ), vol.Required(ATTR_BRIGHTNESS): VALID_BRIGHTNESS, } From b6d2a7a56209254e00eca01323892ab4d48be472 Mon Sep 17 00:00:00 2001 From: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com> Date: Sun, 31 Oct 2021 15:36:37 +0100 Subject: [PATCH 048/174] Add ROCKROBO_S4 to xiaomi_miio vaccum models (#58682) --- homeassistant/components/xiaomi_miio/const.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index a1f2414b713..6630def38ef 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -197,9 +197,11 @@ MODELS_LIGHT = ( ) # TODO: use const from pythonmiio once new release with the constant has been published. # pylint: disable=fixme +ROCKROBO_S4 = "roborock.vacuum.s4" ROCKROBO_S5_MAX = "roborock.vacuum.s5e" MODELS_VACUUM = [ ROCKROBO_V1, + ROCKROBO_S4, ROCKROBO_S5, ROCKROBO_S5_MAX, ROCKROBO_S6, From 73dfa2d2058cb8fc54702231bd87b3503a31f79b Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Sun, 31 Oct 2021 15:38:01 +0100 Subject: [PATCH 049/174] Set Netatmo max default temperature (#58718) --- homeassistant/components/netatmo/climate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 145735f4c95..db324cd1722 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -232,6 +232,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): if self._model == NA_THERM: self._operation_list.append(HVAC_MODE_OFF) + self._attr_max_temp = DEFAULT_MAX_TEMP self._attr_unique_id = f"{self._id}-{self._model}" async def async_added_to_hass(self) -> None: @@ -446,7 +447,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): if (temp := kwargs.get(ATTR_TEMPERATURE)) is None: return await self._home_status.async_set_room_thermpoint( - self._id, STATE_NETATMO_MANUAL, temp + self._id, STATE_NETATMO_MANUAL, min(temp, DEFAULT_MAX_TEMP) ) self.async_write_ha_state() From aae8c2f5ddb1579c33e11b8f11869756b7ed446e Mon Sep 17 00:00:00 2001 From: Anders Liljekvist Date: Sat, 30 Oct 2021 14:57:45 +0200 Subject: [PATCH 050/174] Fix bluesound player internally used id (#58732) --- .../components/bluesound/media_player.py | 51 ++++++++++++------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index ddc67bed6ab..6c90a511a05 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -106,8 +106,6 @@ SERVICE_TO_METHOD = { def _add_player(hass, async_add_entities, host, port=None, name=None): """Add Bluesound players.""" - if host in [x.host for x in hass.data[DATA_BLUESOUND]]: - return @callback def _init_player(event=None): @@ -127,6 +125,11 @@ def _add_player(hass, async_add_entities, host, port=None, name=None): @callback def _add_player_cb(): """Add player after first sync fetch.""" + if player.id in [x.id for x in hass.data[DATA_BLUESOUND]]: + _LOGGER.warning("Player already added %s", player.id) + return + + hass.data[DATA_BLUESOUND].append(player) async_add_entities([player]) _LOGGER.info("Added device with name: %s", player.name) @@ -138,7 +141,6 @@ def _add_player(hass, async_add_entities, host, port=None, name=None): hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop_polling) player = BluesoundPlayer(hass, host, port, name, _add_player_cb) - hass.data[DATA_BLUESOUND].append(player) if hass.is_running: _init_player() @@ -208,6 +210,7 @@ class BluesoundPlayer(MediaPlayerEntity): self._polling_session = async_get_clientsession(hass) self._polling_task = None # The actual polling task. self._name = name + self._id = None self._icon = None self._capture_items = [] self._services_items = [] @@ -225,6 +228,7 @@ class BluesoundPlayer(MediaPlayerEntity): self._bluesound_device_name = None self._init_callback = init_callback + if self.port is None: self.port = DEFAULT_PORT @@ -251,6 +255,8 @@ class BluesoundPlayer(MediaPlayerEntity): if not self._name: self._name = self._sync_status.get("@name", self.host) + if not self._id: + self._id = self._sync_status.get("@id", None) if not self._bluesound_device_name: self._bluesound_device_name = self._sync_status.get("@name", self.host) if not self._icon: @@ -259,17 +265,19 @@ class BluesoundPlayer(MediaPlayerEntity): if (master := self._sync_status.get("master")) is not None: self._is_master = False master_host = master.get("#text") + master_port = master.get("@port", "11000") + master_id = f"{master_host}:{master_port}" master_device = [ device for device in self._hass.data[DATA_BLUESOUND] - if device.host == master_host + if device.id == master_id ] - if master_device and master_host != self.host: + if master_device and master_id != self.id: self._master = master_device[0] else: self._master = None - _LOGGER.error("Master not found %s", master_host) + _LOGGER.error("Master not found %s", master_id) else: if self._master is not None: self._master = None @@ -287,14 +295,14 @@ class BluesoundPlayer(MediaPlayerEntity): await self.async_update_status() except (asyncio.TimeoutError, ClientError, BluesoundPlayer._TimeoutException): - _LOGGER.info("Node %s is offline, retrying later", self._name) + _LOGGER.info("Node %s:%s is offline, retrying later", self.name, self.port) await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT) self.start_polling() except CancelledError: - _LOGGER.debug("Stopping the polling of node %s", self._name) + _LOGGER.debug("Stopping the polling of node %s:%s", self.name, self.port) except Exception: - _LOGGER.exception("Unexpected error in %s", self._name) + _LOGGER.exception("Unexpected error in %s:%s", self.name, self.port) raise def start_polling(self): @@ -314,12 +322,14 @@ class BluesoundPlayer(MediaPlayerEntity): await self.force_update_sync_status(self._init_callback, True) except (asyncio.TimeoutError, ClientError): - _LOGGER.info("Node %s is offline, retrying later", self.host) + _LOGGER.info("Node %s:%s is offline, retrying later", self.host, self.port) self._retry_remove = async_track_time_interval( self._hass, self.async_init, NODE_RETRY_INITIATION ) except Exception: - _LOGGER.exception("Unexpected when initiating error in %s", self.host) + _LOGGER.exception( + "Unexpected when initiating error in %s:%s", self.host, self.port + ) raise async def async_update(self): @@ -366,9 +376,9 @@ class BluesoundPlayer(MediaPlayerEntity): except (asyncio.TimeoutError, aiohttp.ClientError): if raise_timeout: - _LOGGER.info("Timeout: %s", self.host) + _LOGGER.info("Timeout: %s:%s", self.host, self.port) raise - _LOGGER.debug("Failed communicating: %s", self.host) + _LOGGER.debug("Failed communicating: %s:%s", self.host, self.port) return None return data @@ -403,7 +413,7 @@ class BluesoundPlayer(MediaPlayerEntity): group_name = self._status.get("groupName") if group_name != self._group_name: - _LOGGER.debug("Group name change detected on device: %s", self.host) + _LOGGER.debug("Group name change detected on device: %s", self.id) self._group_name = group_name # rebuild ordered list of entity_ids that are in the group, master is first @@ -659,6 +669,11 @@ class BluesoundPlayer(MediaPlayerEntity): mute = bool(int(mute)) return mute + @property + def id(self): + """Get id of device.""" + return self._id + @property def name(self): """Return the name of the device.""" @@ -831,8 +846,8 @@ class BluesoundPlayer(MediaPlayerEntity): if master_device: _LOGGER.debug( "Trying to join player: %s to master: %s", - self.host, - master_device[0].host, + self.id, + master_device[0].id, ) await master_device[0].async_add_slave(self) @@ -877,7 +892,7 @@ class BluesoundPlayer(MediaPlayerEntity): if self._master is None: return - _LOGGER.debug("Trying to unjoin player: %s", self.host) + _LOGGER.debug("Trying to unjoin player: %s", self.id) await self._master.async_remove_slave(self) async def async_add_slave(self, slave_device): @@ -896,7 +911,7 @@ class BluesoundPlayer(MediaPlayerEntity): """Increase sleep time on player.""" sleep_time = await self.send_bluesound_command("/Sleep") if sleep_time is None: - _LOGGER.error("Error while increasing sleep time on player: %s", self.host) + _LOGGER.error("Error while increasing sleep time on player: %s", self.id) return 0 return int(sleep_time.get("sleep", "0")) From 2c509bfc06321fc6e8ce0dbeed02f9e37caa683c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 31 Oct 2021 09:19:37 -0500 Subject: [PATCH 051/174] Add additional test coverage for RYSE smartbridges with HK (#58746) --- .../test_ryse_smart_bridge.py | 68 ++ .../ryse_smart_bridge_four_shades.json | 1066 +++++++++++++++++ 2 files changed, 1134 insertions(+) create mode 100644 tests/fixtures/homekit_controller/ryse_smart_bridge_four_shades.json diff --git a/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py b/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py index ad5180658ad..e10e0ccd62a 100644 --- a/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py +++ b/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py @@ -71,3 +71,71 @@ async def test_ryse_smart_bridge_setup(hass): assert device.name == "RYSE SmartShade" assert device.model == "RYSE Shade" assert device.sw_version == "" + + +async def test_ryse_smart_bridge_four_shades_setup(hass): + """Test that a Ryse smart bridge with four shades can be correctly setup in HA.""" + accessories = await setup_accessories_from_file( + hass, "ryse_smart_bridge_four_shades.json" + ) + config_entry, pairing = await setup_test_accessories(hass, accessories) + + entity_registry = er.async_get(hass) + + cover_id = "cover.lr_left" + cover = entity_registry.async_get(cover_id) + assert cover.unique_id == "homekit-00:00:00:00:00:00-2-48" + + cover_id = "cover.lr_right" + cover = entity_registry.async_get(cover_id) + assert cover.unique_id == "homekit-00:00:00:00:00:00-3-48" + + cover_id = "cover.br_left" + cover = entity_registry.async_get(cover_id) + assert cover.unique_id == "homekit-00:00:00:00:00:00-4-48" + + cover_id = "cover.rzss" + cover = entity_registry.async_get(cover_id) + assert cover.unique_id == "homekit-00:00:00:00:00:00-5-48" + + sensor_id = "sensor.lr_left_battery" + sensor = entity_registry.async_get(sensor_id) + assert sensor.unique_id == "homekit-00:00:00:00:00:00-2-64" + + sensor_id = "sensor.lr_right_battery" + sensor = entity_registry.async_get(sensor_id) + assert sensor.unique_id == "homekit-00:00:00:00:00:00-3-64" + + sensor_id = "sensor.br_left_battery" + sensor = entity_registry.async_get(sensor_id) + assert sensor.unique_id == "homekit-00:00:00:00:00:00-4-64" + + sensor_id = "sensor.rzss_battery" + sensor = entity_registry.async_get(sensor_id) + assert sensor.unique_id == "homekit-00:00:00:00:00:00-5-64" + + cover_helper = Helper( + hass, + cover_id, + pairing, + accessories[0], + config_entry, + ) + + cover_state = await cover_helper.poll_and_get_state() + assert cover_state.attributes["friendly_name"] == "RZSS" + assert cover_state.state == "open" + + device_registry = dr.async_get(hass) + + device = device_registry.async_get(cover.device_id) + assert device.manufacturer == "RYSE Inc." + assert device.name == "RZSS" + assert device.model == "RYSE Shade" + assert device.sw_version == "3.0.8" + + bridge = device_registry.async_get(device.via_device_id) + assert bridge.manufacturer == "RYSE Inc." + assert bridge.name == "RYSE SmartBridge" + assert bridge.model == "RYSE SmartBridge" + assert bridge.sw_version == "1.3.0" diff --git a/tests/fixtures/homekit_controller/ryse_smart_bridge_four_shades.json b/tests/fixtures/homekit_controller/ryse_smart_bridge_four_shades.json new file mode 100644 index 00000000000..b2e7aabd95d --- /dev/null +++ b/tests/fixtures/homekit_controller/ryse_smart_bridge_four_shades.json @@ -0,0 +1,1066 @@ +[ + { + "aid": 1, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 2, + "type": "00000014-0000-1000-8000-0026BB765291", + "format": "bool", + "perms": [ + "pw" + ] + }, + { + "iid": 3, + "type": "00000020-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Inc.", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 4, + "type": "00000021-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE SmartBridge", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 5, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE SmartBridge", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 6, + "type": "00000030-0000-1000-8000-0026BB765291", + "format": "string", + "value": "0401.3521.0679", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 7, + "type": "00000052-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.3.0", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 8, + "type": "00000053-0000-1000-8000-0026BB765291", + "format": "string", + "value": "0401.3521.0679", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 9, + "type": "34AB8811-AC7F-4340-BAC3-FD6A85F9943B", + "format": "string", + "value": "4.1;3fac0fb4", + "perms": [ + "pr", + "hd" + ], + "ev": false + }, + { + "iid": 10, + "type": "220", + "format": "data", + "value": "Yhl9CmseEb8=", + "perms": [ + "pr", + "hd" + ], + "ev": false, + "maxDataLen": 8 + } + ] + }, + { + "iid": 16, + "type": "000000A2-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 18, + "type": "00000037-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.1.0", + "perms": [ + "pr" + ], + "ev": false + } + ] + } + ] + }, + { + "aid": 2, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 2, + "type": "00000014-0000-1000-8000-0026BB765291", + "format": "bool", + "perms": [ + "pw" + ] + }, + { + "iid": 3, + "type": "00000020-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Inc.", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 4, + "type": "00000021-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Shade", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 5, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "LR Left", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 6, + "type": "00000030-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.0.0", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 7, + "type": "00000052-0000-1000-8000-0026BB765291", + "format": "string", + "value": "3.0.8", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 8, + "type": "00000053-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.0.0", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 11, + "type": "000000A6-0000-1000-8000-0026BB765291", + "format": "uint32", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + } + ] + }, + { + "iid": 48, + "type": "0000008C-0000-1000-8000-0026BB765291", + "primary": true, + "hidden": false, + "linked": [ + 64 + ], + "characteristics": [ + { + "iid": 52, + "type": "0000007C-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": true, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "iid": 53, + "type": "0000006D-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": true, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "iid": 54, + "type": "00000072-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 2, + "perms": [ + "pr", + "ev" + ], + "ev": true, + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "iid": 50, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Shade", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 55, + "type": "00000024-0000-1000-8000-0026BB765291", + "format": "bool", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": true + } + ] + }, + { + "iid": 64, + "type": "00000096-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 67, + "type": "00000068-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 89, + "perms": [ + "pr", + "ev" + ], + "ev": true, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "iid": 68, + "type": "0000008F-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": true, + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "iid": 70, + "type": "00000079-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": true, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 66, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Shade", + "perms": [ + "pr" + ], + "ev": false + } + ] + } + ] + }, + { + "aid": 3, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 2, + "type": "00000014-0000-1000-8000-0026BB765291", + "format": "bool", + "perms": [ + "pw" + ] + }, + { + "iid": 3, + "type": "00000020-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Inc.", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 4, + "type": "00000021-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Shade", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 5, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "LR Right", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 6, + "type": "00000030-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.0.0", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 7, + "type": "00000052-0000-1000-8000-0026BB765291", + "format": "string", + "value": "3.0.8", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 8, + "type": "00000053-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.0.0", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 11, + "type": "000000A6-0000-1000-8000-0026BB765291", + "format": "uint32", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + } + ] + }, + { + "iid": 48, + "type": "0000008C-0000-1000-8000-0026BB765291", + "primary": true, + "hidden": false, + "linked": [ + 64 + ], + "characteristics": [ + { + "iid": 52, + "type": "0000007C-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "iid": 53, + "type": "0000006D-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "iid": 54, + "type": "00000072-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 2, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "iid": 50, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Shade", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 55, + "type": "00000024-0000-1000-8000-0026BB765291", + "format": "bool", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": false + } + ] + }, + { + "iid": 64, + "type": "00000096-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 67, + "type": "00000068-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 100, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "iid": 68, + "type": "0000008F-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 2, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "iid": 70, + "type": "00000079-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 66, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Shade", + "perms": [ + "pr" + ], + "ev": false + } + ] + } + ] + }, + { + "aid": 4, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 2, + "type": "00000014-0000-1000-8000-0026BB765291", + "format": "bool", + "perms": [ + "pw" + ] + }, + { + "iid": 3, + "type": "00000020-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Inc.", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 4, + "type": "00000021-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Shade", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 5, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "BR Left", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 6, + "type": "00000030-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.0.0", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 7, + "type": "00000052-0000-1000-8000-0026BB765291", + "format": "string", + "value": "3.0.8", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 8, + "type": "00000053-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.0.0", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 11, + "type": "000000A6-0000-1000-8000-0026BB765291", + "format": "uint32", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + } + ] + }, + { + "iid": 48, + "type": "0000008C-0000-1000-8000-0026BB765291", + "primary": true, + "hidden": false, + "linked": [ + 64 + ], + "characteristics": [ + { + "iid": 52, + "type": "0000007C-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 100, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "iid": 53, + "type": "0000006D-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 100, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "iid": 54, + "type": "00000072-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 2, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "iid": 50, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Shade", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 55, + "type": "00000024-0000-1000-8000-0026BB765291", + "format": "bool", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": false + } + ] + }, + { + "iid": 64, + "type": "00000096-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 67, + "type": "00000068-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 100, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "iid": 68, + "type": "0000008F-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 2, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "iid": 70, + "type": "00000079-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 66, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Shade", + "perms": [ + "pr" + ], + "ev": false + } + ] + } + ] + }, + { + "aid": 5, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 2, + "type": "00000014-0000-1000-8000-0026BB765291", + "format": "bool", + "perms": [ + "pw" + ] + }, + { + "iid": 3, + "type": "00000020-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Inc.", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 4, + "type": "00000021-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Shade", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 5, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RZSS", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 6, + "type": "00000030-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.0.0", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 7, + "type": "00000052-0000-1000-8000-0026BB765291", + "format": "string", + "value": "3.0.8", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 8, + "type": "00000053-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.0.0", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 11, + "type": "000000A6-0000-1000-8000-0026BB765291", + "format": "uint32", + "value": 1, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + } + ] + }, + { + "iid": 48, + "type": "0000008C-0000-1000-8000-0026BB765291", + "primary": true, + "hidden": false, + "linked": [ + 64 + ], + "characteristics": [ + { + "iid": 52, + "type": "0000007C-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 100, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "iid": 53, + "type": "0000006D-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 100, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "iid": 54, + "type": "00000072-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 2, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "iid": 50, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Shade", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 55, + "type": "00000024-0000-1000-8000-0026BB765291", + "format": "bool", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": false + } + ] + }, + { + "iid": 64, + "type": "00000096-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 67, + "type": "00000068-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "iid": 68, + "type": "0000008F-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "iid": 70, + "type": "00000079-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 1, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 66, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RYSE Shade", + "perms": [ + "pr" + ], + "ev": false + } + ] + } + ] + } +] From 8800ceba4d2bfc48acb91a629cf184e4e836708d Mon Sep 17 00:00:00 2001 From: Kapernicus Date: Sat, 30 Oct 2021 11:11:37 -0500 Subject: [PATCH 052/174] Bump nad_receiver to version 0.3.0 (#58751) --- homeassistant/components/nad/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nad/manifest.json b/homeassistant/components/nad/manifest.json index 59d82acddf2..12c1f84aa37 100644 --- a/homeassistant/components/nad/manifest.json +++ b/homeassistant/components/nad/manifest.json @@ -2,7 +2,7 @@ "domain": "nad", "name": "NAD", "documentation": "https://www.home-assistant.io/integrations/nad", - "requirements": ["nad_receiver==0.2.0"], + "requirements": ["nad_receiver==0.3.0"], "codeowners": [], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 70fd65845e7..36ceca0ea7e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1035,7 +1035,7 @@ mychevy==2.1.1 mycroftapi==2.0 # homeassistant.components.nad -nad_receiver==0.2.0 +nad_receiver==0.3.0 # homeassistant.components.keenetic_ndms2 ndms2_client==0.1.1 From 9b715383c30e0ef364aaab554fd8b6a88e2e872d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 31 Oct 2021 08:00:31 +0100 Subject: [PATCH 053/174] Add configuration_url to OctoPrint (#58753) * Add configuration_url to Octoprint * fix device_info() return Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com> --- .../components/octoprint/__init__.py | 30 +++++++++++++-- .../components/octoprint/binary_sensor.py | 31 +++++++-------- homeassistant/components/octoprint/sensor.py | 38 ++++++++++--------- 3 files changed, 62 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/octoprint/__init__.py b/homeassistant/components/octoprint/__init__.py index eee1ccd2814..706f54ac708 100644 --- a/homeassistant/components/octoprint/__init__.py +++ b/homeassistant/components/octoprint/__init__.py @@ -1,9 +1,11 @@ """Support for monitoring OctoPrint 3D printers.""" from datetime import timedelta import logging +from typing import cast from pyoctoprintapi import ApiError, OctoprintClient, PrinterOffline import voluptuous as vol +from yarl import URL from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( @@ -20,6 +22,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import slugify as util_slugify import homeassistant.util.dt as dt_util @@ -160,7 +163,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): client.set_api_key(entry.data[CONF_API_KEY]) - coordinator = OctoprintDataUpdateCoordinator(hass, client, entry.entry_id, 30) + coordinator = OctoprintDataUpdateCoordinator(hass, client, entry, 30) await coordinator.async_config_entry_first_refresh() @@ -184,20 +187,23 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): class OctoprintDataUpdateCoordinator(DataUpdateCoordinator): """Class to manage fetching Octoprint data.""" + config_entry: ConfigEntry + def __init__( self, hass: HomeAssistant, octoprint: OctoprintClient, - config_entry_id: str, + config_entry: ConfigEntry, interval: int, ) -> None: """Initialize.""" super().__init__( hass, _LOGGER, - name=f"octoprint-{config_entry_id}", + name=f"octoprint-{config_entry.entry_id}", update_interval=timedelta(seconds=interval), ) + self.config_entry = config_entry self._octoprint = octoprint self._printer_offline = False self.data = {"printer": None, "job": None, "last_read_time": None} @@ -225,3 +231,21 @@ class OctoprintDataUpdateCoordinator(DataUpdateCoordinator): self._printer_offline = False return {"job": job, "printer": printer, "last_read_time": dt_util.utcnow()} + + @property + def device_info(self) -> DeviceInfo: + """Device info.""" + unique_id = cast(str, self.config_entry.unique_id) + configuration_url = URL.build( + scheme=self.config_entry.data[CONF_SSL] and "https" or "http", + host=self.config_entry.data[CONF_HOST], + port=self.config_entry.data[CONF_PORT], + path=self.config_entry.data[CONF_PATH], + ) + + return DeviceInfo( + identifiers={(DOMAIN, unique_id)}, + manufacturer="OctoPrint", + name="OctoPrint", + configuration_url=str(configuration_url), + ) diff --git a/homeassistant/components/octoprint/binary_sensor.py b/homeassistant/components/octoprint/binary_sensor.py index 1adb04d3417..3f9e4417c6e 100644 --- a/homeassistant/components/octoprint/binary_sensor.py +++ b/homeassistant/components/octoprint/binary_sensor.py @@ -2,7 +2,6 @@ from __future__ import annotations from abc import abstractmethod -import logging from pyoctoprintapi import OctoprintPrinterInfo @@ -10,14 +9,10 @@ from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN as COMPONENT_DOMAIN - -_LOGGER = logging.getLogger(__name__) +from . import OctoprintDataUpdateCoordinator +from .const import DOMAIN async def async_setup_entry( @@ -26,7 +21,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the available OctoPrint binary sensors.""" - coordinator: DataUpdateCoordinator = hass.data[COMPONENT_DOMAIN][ + coordinator: OctoprintDataUpdateCoordinator = hass.data[DOMAIN][ config_entry.entry_id ]["coordinator"] device_id = config_entry.unique_id @@ -44,9 +39,11 @@ async def async_setup_entry( class OctoPrintBinarySensorBase(CoordinatorEntity, BinarySensorEntity): """Representation an OctoPrint binary sensor.""" + coordinator: OctoprintDataUpdateCoordinator + def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: OctoprintDataUpdateCoordinator, sensor_type: str, device_id: str, ) -> None: @@ -59,11 +56,7 @@ class OctoPrintBinarySensorBase(CoordinatorEntity, BinarySensorEntity): @property def device_info(self): """Device info.""" - return { - "identifiers": {(COMPONENT_DOMAIN, self._device_id)}, - "manufacturer": "OctoPrint", - "name": "OctoPrint", - } + return self.coordinator.device_info @property def is_on(self): @@ -87,7 +80,9 @@ class OctoPrintBinarySensorBase(CoordinatorEntity, BinarySensorEntity): class OctoPrintPrintingBinarySensor(OctoPrintBinarySensorBase): """Representation an OctoPrint binary sensor.""" - def __init__(self, coordinator: DataUpdateCoordinator, device_id: str) -> None: + def __init__( + self, coordinator: OctoprintDataUpdateCoordinator, device_id: str + ) -> None: """Initialize a new OctoPrint sensor.""" super().__init__(coordinator, "Printing", device_id) @@ -98,7 +93,9 @@ class OctoPrintPrintingBinarySensor(OctoPrintBinarySensorBase): class OctoPrintPrintingErrorBinarySensor(OctoPrintBinarySensorBase): """Representation an OctoPrint binary sensor.""" - def __init__(self, coordinator: DataUpdateCoordinator, device_id: str) -> None: + def __init__( + self, coordinator: OctoprintDataUpdateCoordinator, device_id: str + ) -> None: """Initialize a new OctoPrint sensor.""" super().__init__(coordinator, "Printing Error", device_id) diff --git a/homeassistant/components/octoprint/sensor.py b/homeassistant/components/octoprint/sensor.py index 5a9614c69b4..d46dd06b798 100644 --- a/homeassistant/components/octoprint/sensor.py +++ b/homeassistant/components/octoprint/sensor.py @@ -16,12 +16,10 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import DOMAIN as COMPONENT_DOMAIN +from . import OctoprintDataUpdateCoordinator +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -32,7 +30,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the available OctoPrint binary sensors.""" - coordinator: DataUpdateCoordinator = hass.data[COMPONENT_DOMAIN][ + coordinator: OctoprintDataUpdateCoordinator = hass.data[DOMAIN][ config_entry.entry_id ]["coordinator"] device_id = config_entry.unique_id @@ -67,9 +65,11 @@ async def async_setup_entry( class OctoPrintSensorBase(CoordinatorEntity, SensorEntity): """Representation of an OctoPrint sensor.""" + coordinator: OctoprintDataUpdateCoordinator + def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: OctoprintDataUpdateCoordinator, sensor_type: str, device_id: str, ) -> None: @@ -82,11 +82,7 @@ class OctoPrintSensorBase(CoordinatorEntity, SensorEntity): @property def device_info(self): """Device info.""" - return { - "identifiers": {(COMPONENT_DOMAIN, self._device_id)}, - "manufacturer": "OctoPrint", - "name": "OctoPrint", - } + return self.coordinator.device_info class OctoPrintStatusSensor(OctoPrintSensorBase): @@ -94,7 +90,9 @@ class OctoPrintStatusSensor(OctoPrintSensorBase): _attr_icon = "mdi:printer-3d" - def __init__(self, coordinator: DataUpdateCoordinator, device_id: str) -> None: + def __init__( + self, coordinator: OctoprintDataUpdateCoordinator, device_id: str + ) -> None: """Initialize a new OctoPrint sensor.""" super().__init__(coordinator, "Current State", device_id) @@ -119,7 +117,9 @@ class OctoPrintJobPercentageSensor(OctoPrintSensorBase): _attr_native_unit_of_measurement = PERCENTAGE _attr_icon = "mdi:file-percent" - def __init__(self, coordinator: DataUpdateCoordinator, device_id: str) -> None: + def __init__( + self, coordinator: OctoprintDataUpdateCoordinator, device_id: str + ) -> None: """Initialize a new OctoPrint sensor.""" super().__init__(coordinator, "Job Percentage", device_id) @@ -142,7 +142,9 @@ class OctoPrintEstimatedFinishTimeSensor(OctoPrintSensorBase): _attr_device_class = DEVICE_CLASS_TIMESTAMP - def __init__(self, coordinator: DataUpdateCoordinator, device_id: str) -> None: + def __init__( + self, coordinator: OctoprintDataUpdateCoordinator, device_id: str + ) -> None: """Initialize a new OctoPrint sensor.""" super().__init__(coordinator, "Estimated Finish Time", device_id) @@ -163,7 +165,9 @@ class OctoPrintStartTimeSensor(OctoPrintSensorBase): _attr_device_class = DEVICE_CLASS_TIMESTAMP - def __init__(self, coordinator: DataUpdateCoordinator, device_id: str) -> None: + def __init__( + self, coordinator: OctoprintDataUpdateCoordinator, device_id: str + ) -> None: """Initialize a new OctoPrint sensor.""" super().__init__(coordinator, "Start Time", device_id) @@ -189,7 +193,7 @@ class OctoPrintTemperatureSensor(OctoPrintSensorBase): def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: OctoprintDataUpdateCoordinator, tool: str, temp_type: str, device_id: str, From 0f367722edd2992ee373dd455d70149cb4c7f7b7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 30 Oct 2021 12:18:39 -0500 Subject: [PATCH 054/174] Bump zeroconf 0.36.11 (#58755) --- homeassistant/components/zeroconf/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/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 9870258027b..3f4dfb4929e 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -2,7 +2,7 @@ "domain": "zeroconf", "name": "Zero-configuration networking (zeroconf)", "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "requirements": ["zeroconf==0.36.9"], + "requirements": ["zeroconf==0.36.11"], "dependencies": ["network", "api"], "codeowners": ["@bdraco"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c4f1a31f5c6..5ff0536e395 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ sqlalchemy==1.4.23 voluptuous-serialize==2.4.0 voluptuous==0.12.2 yarl==1.6.3 -zeroconf==0.36.9 +zeroconf==0.36.11 pycryptodome>=3.6.6 diff --git a/requirements_all.txt b/requirements_all.txt index 36ceca0ea7e..b472b9eb009 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2465,7 +2465,7 @@ youtube_dl==2021.06.06 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.36.9 +zeroconf==0.36.11 # homeassistant.components.zha zha-quirks==0.0.63 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 042528056ab..93d64a9ade2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1430,7 +1430,7 @@ yeelight==0.7.8 youless-api==0.15 # homeassistant.components.zeroconf -zeroconf==0.36.9 +zeroconf==0.36.11 # homeassistant.components.zha zha-quirks==0.0.63 From 2cc3290794e21210aaed861f51a3b701c44072cb Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 31 Oct 2021 13:32:49 +0100 Subject: [PATCH 055/174] Fix channel.send in Discord (#58756) --- homeassistant/components/discord/notify.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/discord/notify.py b/homeassistant/components/discord/notify.py index 10ad1e8e018..16f30fbf051 100644 --- a/homeassistant/components/discord/notify.py +++ b/homeassistant/components/discord/notify.py @@ -94,9 +94,9 @@ class DiscordNotificationService(BaseNotificationService): for channelid in kwargs[ATTR_TARGET]: channelid = int(channelid) try: - channel = discord_bot.fetch_channel( + channel = await discord_bot.fetch_channel( channelid - ) or discord_bot.fetch_user(channelid) + ) or await discord_bot.fetch_user(channelid) except discord.NotFound: _LOGGER.warning("Channel not found for ID: %s", channelid) continue From 184342804e337c9669f05fbf6d8038f03856b4e5 Mon Sep 17 00:00:00 2001 From: purcell-lab <79175134+purcell-lab@users.noreply.github.com> Date: Mon, 1 Nov 2021 02:11:48 +1100 Subject: [PATCH 056/174] Fix solaredge energy sensor names (#58773) --- homeassistant/components/solaredge/const.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/solaredge/const.py b/homeassistant/components/solaredge/const.py index d97c191a36b..6e353ffe339 100644 --- a/homeassistant/components/solaredge/const.py +++ b/homeassistant/components/solaredge/const.py @@ -147,45 +147,45 @@ SENSOR_TYPES = [ icon="mdi:car-battery", ), SolarEdgeSensorEntityDescription( - key="purchased_power", + key="purchased_energy", json_key="Purchased", - name="Imported Power", + name="Imported Energy", entity_registry_enabled_default=False, state_class=STATE_CLASS_TOTAL_INCREASING, native_unit_of_measurement=ENERGY_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, ), SolarEdgeSensorEntityDescription( - key="production_power", + key="production_energy", json_key="Production", - name="Production Power", + name="Production Energy", entity_registry_enabled_default=False, state_class=STATE_CLASS_TOTAL_INCREASING, native_unit_of_measurement=ENERGY_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, ), SolarEdgeSensorEntityDescription( - key="consumption_power", + key="consumption_energy", json_key="Consumption", - name="Consumption Power", + name="Consumption Energy", entity_registry_enabled_default=False, state_class=STATE_CLASS_TOTAL_INCREASING, native_unit_of_measurement=ENERGY_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, ), SolarEdgeSensorEntityDescription( - key="selfconsumption_power", + key="selfconsumption_energy", json_key="SelfConsumption", - name="SelfConsumption Power", + name="SelfConsumption Energy", entity_registry_enabled_default=False, state_class=STATE_CLASS_TOTAL_INCREASING, native_unit_of_measurement=ENERGY_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, ), SolarEdgeSensorEntityDescription( - key="feedin_power", + key="feedin_energy", json_key="FeedIn", - name="Exported Power", + name="Exported Energy", entity_registry_enabled_default=False, state_class=STATE_CLASS_TOTAL_INCREASING, native_unit_of_measurement=ENERGY_WATT_HOUR, From e031917a30357df062388fb206dcb0880abef8aa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 31 Oct 2021 10:11:07 -0500 Subject: [PATCH 057/174] Workaround brightness transition delay from off in older yeelight models (#58774) --- homeassistant/components/yeelight/__init__.py | 14 +++++++++++++ homeassistant/components/yeelight/light.py | 6 +++++- tests/components/yeelight/test_light.py | 20 +++++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index e57979a7ea2..1408cb56709 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -35,6 +35,20 @@ _LOGGER = logging.getLogger(__name__) STATE_CHANGE_TIME = 0.40 # seconds POWER_STATE_CHANGE_TIME = 1 # seconds +# +# These models do not transition correctly when turning on, and +# yeelight is no longer updating the firmware on older devices +# +# https://github.com/home-assistant/core/issues/58315 +# +# The problem can be worked around by always setting the brightness +# even when the bulb is reporting the brightness is already at the +# desired level. +# +MODELS_WITH_DELAYED_ON_TRANSITION = { + "color", # YLDP02YL +} + DOMAIN = "yeelight" DATA_YEELIGHT = DOMAIN DATA_UPDATED = "yeelight_{}_data_updated" diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index a6b51046fc6..3d84a30f44e 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -63,6 +63,7 @@ from . import ( DATA_DEVICE, DATA_UPDATED, DOMAIN, + MODELS_WITH_DELAYED_ON_TRANSITION, POWER_STATE_CHANGE_TIME, YEELIGHT_FLOW_TRANSITION_SCHEMA, YeelightEntity, @@ -614,7 +615,10 @@ class YeelightGenericLight(YeelightEntity, LightEntity): """Set bulb brightness.""" if not brightness: return - if math.floor(self.brightness) == math.floor(brightness): + if ( + math.floor(self.brightness) == math.floor(brightness) + and self._bulb.model not in MODELS_WITH_DELAYED_ON_TRANSITION + ): _LOGGER.debug("brightness already set to: %s", brightness) # Already set, and since we get pushed updates # we avoid setting it again to ensure we do not diff --git a/tests/components/yeelight/test_light.py b/tests/components/yeelight/test_light.py index a4e9e2d9746..4377efe129f 100644 --- a/tests/components/yeelight/test_light.py +++ b/tests/components/yeelight/test_light.py @@ -641,6 +641,25 @@ async def test_state_already_set_avoid_ratelimit(hass: HomeAssistant): mocked_bulb.async_set_rgb.reset_mock() mocked_bulb.last_properties["flowing"] = "0" + mocked_bulb.model = "color" # color model needs a workaround (see MODELS_WITH_DELAYED_ON_TRANSITION) + await hass.services.async_call( + "light", + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: ENTITY_LIGHT, + ATTR_BRIGHTNESS_PCT: PROPERTIES["bright"], + }, + blocking=True, + ) + assert mocked_bulb.async_set_hsv.mock_calls == [] + assert mocked_bulb.async_set_rgb.mock_calls == [] + assert mocked_bulb.async_set_color_temp.mock_calls == [] + assert mocked_bulb.async_set_brightness.mock_calls == [ + call(pytest.approx(50.1, 0.1), duration=350, light_type=ANY) + ] + mocked_bulb.async_set_brightness.reset_mock() + + mocked_bulb.model = "colora" # colora does not need a workaround await hass.services.async_call( "light", SERVICE_TURN_ON, @@ -683,6 +702,7 @@ async def test_state_already_set_avoid_ratelimit(hass: HomeAssistant): assert mocked_bulb.async_set_brightness.mock_calls == [] mocked_bulb.last_properties["flowing"] = "1" + await hass.services.async_call( "light", SERVICE_TURN_ON, From 7fae711e0c46039aab92cf510cb42aff58c7ae85 Mon Sep 17 00:00:00 2001 From: Michael Chisholm Date: Mon, 1 Nov 2021 02:11:20 +1100 Subject: [PATCH 058/174] dlna_dmr: less eager discovery (#58780) --- homeassistant/components/dlna_dmr/config_flow.py | 16 ++++++++++++++++ homeassistant/components/dlna_dmr/manifest.json | 12 ------------ homeassistant/generated/ssdp.py | 12 ------------ tests/components/dlna_dmr/test_config_flow.py | 16 ++++++++++++++++ 4 files changed, 32 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/dlna_dmr/config_flow.py b/homeassistant/components/dlna_dmr/config_flow.py index 454a28c9f7d..8cd4f706087 100644 --- a/homeassistant/components/dlna_dmr/config_flow.py +++ b/homeassistant/components/dlna_dmr/config_flow.py @@ -471,4 +471,20 @@ def _is_ignored_device(discovery_info: Mapping[str, Any]) -> bool: if discovery_info.get(ssdp.ATTR_UPNP_DEVICE_TYPE) not in DmrDevice.DEVICE_TYPES: return True + # Special cases for devices with other discovery methods (e.g. mDNS), or + # that advertise multiple unrelated (sent in separate discovery packets) + # UPnP devices. + manufacturer = discovery_info.get(ssdp.ATTR_UPNP_MANUFACTURER, "").lower() + model = discovery_info.get(ssdp.ATTR_UPNP_MODEL_NAME, "").lower() + + if manufacturer.startswith("xbmc") or model == "kodi": + # kodi + return True + if manufacturer.startswith("samsung") and "tv" in model: + # samsungtv + return True + if manufacturer.startswith("lg") and "tv" in model: + # webostv + return True + return False diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index 2c87260834f..962b2e167be 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -17,18 +17,6 @@ { "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:3", "st": "urn:schemas-upnp-org:device:MediaRenderer:3" - }, - { - "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1", - "nt": "urn:schemas-upnp-org:device:MediaRenderer:1" - }, - { - "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:2", - "nt": "urn:schemas-upnp-org:device:MediaRenderer:2" - }, - { - "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:3", - "nt": "urn:schemas-upnp-org:device:MediaRenderer:3" } ], "codeowners": ["@StevenLooman", "@chishm"], diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index 925ba5b82fe..9434bc11f61 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -95,18 +95,6 @@ SSDP = { { "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:3", "st": "urn:schemas-upnp-org:device:MediaRenderer:3" - }, - { - "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1", - "nt": "urn:schemas-upnp-org:device:MediaRenderer:1" - }, - { - "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:2", - "nt": "urn:schemas-upnp-org:device:MediaRenderer:2" - }, - { - "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:3", - "nt": "urn:schemas-upnp-org:device:MediaRenderer:3" } ], "fritz": [ diff --git a/tests/components/dlna_dmr/test_config_flow.py b/tests/components/dlna_dmr/test_config_flow.py index 5a2327ecce9..e2d82d5b559 100644 --- a/tests/components/dlna_dmr/test_config_flow.py +++ b/tests/components/dlna_dmr/test_config_flow.py @@ -631,6 +631,22 @@ async def test_ssdp_ignore_device(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "alternative_integration" + for manufacturer, model in [ + ("XBMC Foundation", "Kodi"), + ("Samsung", "Smart TV"), + ("LG Electronics.", "LG TV"), + ]: + discovery = dict(MOCK_DISCOVERY) + discovery[ssdp.ATTR_UPNP_MANUFACTURER] = manufacturer + discovery[ssdp.ATTR_UPNP_MODEL_NAME] = model + result = await hass.config_entries.flow.async_init( + DLNA_DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=discovery, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "alternative_integration" + async def test_unignore_flow(hass: HomeAssistant, ssdp_scanner_mock: Mock) -> None: """Test a config flow started by unignoring a device.""" From a0fba152677504e9edc8d7cf245025b36e5bdca8 Mon Sep 17 00:00:00 2001 From: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com> Date: Mon, 1 Nov 2021 00:29:57 +0100 Subject: [PATCH 059/174] Add ROCKROBO_E2 to supported vacuums for xiaomi_miio (#58817) https://github.com/rytilahti/python-miio/blob/e1adea55f3be237f6e6904210b6f7b52162bf154/miio/vacuum.py#L129 --- homeassistant/components/xiaomi_miio/const.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index 6630def38ef..578a6d3ffab 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -199,8 +199,10 @@ MODELS_LIGHT = ( # TODO: use const from pythonmiio once new release with the constant has been published. # pylint: disable=fixme ROCKROBO_S4 = "roborock.vacuum.s4" ROCKROBO_S5_MAX = "roborock.vacuum.s5e" +ROCKROBO_E2 = "roborock.vacuum.e2" MODELS_VACUUM = [ ROCKROBO_V1, + ROCKROBO_E2, ROCKROBO_S4, ROCKROBO_S5, ROCKROBO_S5_MAX, @@ -209,6 +211,7 @@ MODELS_VACUUM = [ ROCKROBO_S7, ] MODELS_VACUUM_WITH_MOP = [ + ROCKROBO_E2, ROCKROBO_S5, ROCKROBO_S5_MAX, ROCKROBO_S6, From 6908fa612770bb52db9a2c57b0b0a6271f5cce70 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 31 Oct 2021 20:19:51 +0100 Subject: [PATCH 060/174] Fix Plugwise not updating config entry with discovery information (#58819) --- .../components/plugwise/config_flow.py | 2 +- tests/components/plugwise/test_config_flow.py | 23 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/plugwise/config_flow.py b/homeassistant/components/plugwise/config_flow.py index 450388b6f42..1dbf4324590 100644 --- a/homeassistant/components/plugwise/config_flow.py +++ b/homeassistant/components/plugwise/config_flow.py @@ -109,7 +109,7 @@ class PlugwiseConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): # unique_id is needed here, to be able to determine whether the discovered device is known, or not. unique_id = self.discovery_info.get("hostname").split(".")[0] await self.async_set_unique_id(unique_id) - self._abort_if_unique_id_configured() + self._abort_if_unique_id_configured({CONF_HOST: self.discovery_info[CONF_HOST]}) if DEFAULT_USERNAME not in unique_id: self.discovery_info[CONF_USERNAME] = STRETCH_USERNAME diff --git a/tests/components/plugwise/test_config_flow.py b/tests/components/plugwise/test_config_flow.py index 75851f5c15a..7f270e23cc1 100644 --- a/tests/components/plugwise/test_config_flow.py +++ b/tests/components/plugwise/test_config_flow.py @@ -203,6 +203,29 @@ async def test_zeroconf_stretch_form(hass): assert len(mock_setup_entry.mock_calls) == 1 +async def test_zercoconf_discovery_update_configuration(hass): + """Test if a discovered device is configured and updated with new host.""" + entry = MockConfigEntry( + domain=DOMAIN, + title=CONF_NAME, + data={CONF_HOST: "0.0.0.0", CONF_PASSWORD: TEST_PASSWORD}, + unique_id=TEST_HOSTNAME, + ) + entry.add_to_hass(hass) + + assert entry.data[CONF_HOST] == "0.0.0.0" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_ZEROCONF}, + data=TEST_DISCOVERY, + ) + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + assert entry.data[CONF_HOST] == "1.1.1.1" + + async def test_form_username(hass): """Test we get the username data back.""" From 68b0413c982d436ebb008475b4ef19ead52990d5 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 31 Oct 2021 13:41:55 -0400 Subject: [PATCH 061/174] Bump pyefergy to 0.1.3 (#58821) --- homeassistant/components/efergy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/efergy/manifest.json b/homeassistant/components/efergy/manifest.json index d95c0b69415..17f104c561f 100644 --- a/homeassistant/components/efergy/manifest.json +++ b/homeassistant/components/efergy/manifest.json @@ -3,7 +3,7 @@ "name": "Efergy", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/efergy", - "requirements": ["pyefergy==0.1.2"], + "requirements": ["pyefergy==0.1.3"], "codeowners": ["@tkdrob"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index b472b9eb009..e23c6a5b7d2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1447,7 +1447,7 @@ pyeconet==0.1.14 pyedimax==0.2.1 # homeassistant.components.efergy -pyefergy==0.1.2 +pyefergy==0.1.3 # homeassistant.components.eight_sleep pyeight==0.1.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 93d64a9ade2..f5743da2af7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -856,7 +856,7 @@ pydispatcher==2.0.5 pyeconet==0.1.14 # homeassistant.components.efergy -pyefergy==0.1.2 +pyefergy==0.1.3 # homeassistant.components.everlights pyeverlights==0.1.0 From 868fbc063d5fd680659f6672d5c01816ee773b01 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Mon, 1 Nov 2021 11:23:01 +0800 Subject: [PATCH 062/174] Improve part metadata in stream (#58822) --- homeassistant/components/stream/worker.py | 117 +++++++++++++--------- tests/components/stream/test_worker.py | 52 ++++++---- 2 files changed, 106 insertions(+), 63 deletions(-) diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index 64a43f68aa0..881614b04a3 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -66,9 +66,15 @@ class SegmentBuffer: memory_file: BytesIO, sequence: int, input_vstream: av.video.VideoStream, - ) -> av.container.OutputContainer: - """Make a new av OutputContainer.""" - return av.open( + input_astream: av.audio.stream.AudioStream, + ) -> tuple[ + av.container.OutputContainer, + av.video.VideoStream, + av.audio.stream.AudioStream | None, + ]: + """Make a new av OutputContainer and add output streams.""" + add_audio = input_astream and input_astream.name in AUDIO_CODECS + container = av.open( memory_file, mode="w", format=SEGMENT_CONTAINER_FORMAT, @@ -93,19 +99,21 @@ class SegmentBuffer: # Create a fragment every TARGET_PART_DURATION. The data from each fragment is stored in # a "Part" that can be combined with the data from all the other "Part"s, plus an init # section, to reconstitute the data in a "Segment". - # frag_duration is the threshold for determining part boundaries, and the dts of the last - # packet in the part should correspond to a duration that is smaller than this value. - # However, as the part duration includes the duration of the last frame, the part duration - # will be equal to or greater than this value. - # We previously scaled this number down by .85 to account for this while keeping within - # the 15% variance allowed in part duration. However, this did not work when inputs had - # an audio stream - sometimes the fragment would get cut on the audio packet, causing - # the durations to actually be to short. - # The current approach is to use this frag_duration for creating the media while - # adjusting the metadata duration to keep the durations in the metadata below the - # part_target_duration threshold. + # The LL-HLS spec allows for a fragment's duration to be within the range [0.85x,1.0x] + # of the part target duration. We use the frag_duration option to tell ffmpeg to try to + # cut the fragments when they reach frag_duration. However, the resulting fragments can + # have variability in their durations and can end up being too short or too long. If + # there are two tracks, as in the case of a video feed with audio, the fragment cut seems + # to be done on the first track that crosses the desired threshold, and cutting on the + # audio track may result in a shorter video fragment than desired. Conversely, with a + # video track with no audio, the discrete nature of frames means that the frame at the + # end of a fragment will sometimes extend slightly beyond the desired frag_duration. + # Given this, our approach is to use a frag_duration near the upper end of the range for + # outputs with audio using a frag_duration at the lower end of the range for outputs with + # only video. "frag_duration": str( - self._stream_settings.part_target_duration * 1e6 + self._stream_settings.part_target_duration + * (98e4 if add_audio else 9e5) ), } if self._stream_settings.ll_hls @@ -113,6 +121,12 @@ class SegmentBuffer: ), }, ) + output_vstream = container.add_stream(template=input_vstream) + # Check if audio is requested + output_astream = None + if add_audio: + output_astream = container.add_stream(template=input_astream) + return container, output_vstream, output_astream def set_streams( self, @@ -128,26 +142,22 @@ class SegmentBuffer: """Initialize a new stream segment.""" # Keep track of the number of segments we've processed self._sequence += 1 - self._segment_start_dts = video_dts + self._part_start_dts = self._segment_start_dts = video_dts self._segment = None self._memory_file = BytesIO() self._memory_file_pos = 0 - self._av_output = self.make_new_av( + ( + self._av_output, + self._output_video_stream, + self._output_audio_stream, + ) = self.make_new_av( memory_file=self._memory_file, sequence=self._sequence, input_vstream=self._input_video_stream, - ) - self._output_video_stream = self._av_output.add_stream( - template=self._input_video_stream + input_astream=self._input_audio_stream, ) if self._output_video_stream.name == "hevc": self._output_video_stream.codec_tag = "hvc1" - # Check if audio is requested - self._output_audio_stream = None - if self._input_audio_stream and self._input_audio_stream.name in AUDIO_CODECS: - self._output_audio_stream = self._av_output.add_stream( - template=self._input_audio_stream - ) def mux_packet(self, packet: av.Packet) -> None: """Mux a packet to the appropriate output stream.""" @@ -186,13 +196,9 @@ class SegmentBuffer: # Fetch the latest StreamOutputs, which may have changed since the # worker started. stream_outputs=self._outputs_callback().values(), - start_time=self._start_time - + datetime.timedelta( - seconds=float(self._segment_start_dts * packet.time_base) - ), + start_time=self._start_time, ) self._memory_file_pos = self._memory_file.tell() - self._part_start_dts = self._segment_start_dts else: # These are the ends of the part segments self.flush(packet, last_part=False) @@ -201,17 +207,23 @@ class SegmentBuffer: If last_part is True, also close the segment, give it a duration, and clean up the av_output and memory_file. + There are two different ways to enter this function, and when + last_part is True, packet has not yet been muxed, while when + last_part is False, the packet has already been muxed. However, + in both cases, packet is the next packet and is not included in + the Part. + This function writes the duration metadata for the Part and + for the Segment. However, as the fragmentation done by ffmpeg + may result in fragment durations which fall outside the + [0.85x,1.0x] tolerance band allowed by LL-HLS, we need to fudge + some durations a bit by reporting them as being within that + range. + Note that repeated adjustments may cause drift between the part + durations in the metadata and those in the media and result in + playback issues in some clients. """ - # In some cases using the current packet's dts (which is the start - # dts of the next part) to calculate the part duration will result in a - # value which exceeds the part_target_duration. This can muck up the - # duration of both this part and the next part. An easy fix is to just - # use the current packet dts and cap it by the part target duration. - # The adjustment may cause a drift between this adjusted duration - # (used in the metadata) and the media duration, but the drift should be - # automatically corrected when the part duration cleanly divides the - # framerate. - current_dts = min( + # Part durations should not exceed the part target duration + adjusted_dts = min( packet.dts, self._part_start_dts + self._stream_settings.part_target_duration / packet.time_base, @@ -220,29 +232,44 @@ class SegmentBuffer: # Closing the av_output will write the remaining buffered data to the # memory_file as a new moof/mdat. self._av_output.close() + elif not self._part_has_keyframe: + # Parts which are not the last part or an independent part should + # not have durations below 0.85 of the part target duration. + adjusted_dts = max( + adjusted_dts, + self._part_start_dts + + 0.85 * self._stream_settings.part_target_duration / packet.time_base, + ) assert self._segment self._memory_file.seek(self._memory_file_pos) self._hass.loop.call_soon_threadsafe( self._segment.async_add_part, Part( - duration=float((current_dts - self._part_start_dts) * packet.time_base), + duration=float( + (adjusted_dts - self._part_start_dts) * packet.time_base + ), has_keyframe=self._part_has_keyframe, data=self._memory_file.read(), ), - float((current_dts - self._segment_start_dts) * packet.time_base) + ( + segment_duration := float( + (adjusted_dts - self._segment_start_dts) * packet.time_base + ) + ) if last_part else 0, ) if last_part: # If we've written the last part, we can close the memory_file. self._memory_file.close() # We don't need the BytesIO object anymore + self._start_time += datetime.timedelta(seconds=segment_duration) # Reinitialize - self.reset(current_dts) + self.reset(packet.dts) else: # For the last part, these will get set again elsewhere so we can skip # setting them here. self._memory_file_pos = self._memory_file.tell() - self._part_start_dts = current_dts + self._part_start_dts = adjusted_dts self._part_has_keyframe = False def discontinuity(self) -> None: diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index 7c9ad91f543..97fe4bd0d37 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -677,6 +677,10 @@ async def test_worker_log(hass, caplog): async def test_durations(hass, record_worker_sync): """Test that the duration metadata matches the media.""" + + # Use a target part duration which has a slight mismatch + # with the incoming frame rate to better expose problems. + target_part_duration = TEST_PART_DURATION - 0.01 await async_setup_component( hass, "stream", @@ -684,12 +688,12 @@ async def test_durations(hass, record_worker_sync): "stream": { CONF_LL_HLS: True, CONF_SEGMENT_DURATION: SEGMENT_DURATION, - CONF_PART_DURATION: TEST_PART_DURATION, + CONF_PART_DURATION: target_part_duration, } }, ) - source = generate_h264_video() + source = generate_h264_video(duration=SEGMENT_DURATION + 1) stream = create_stream(hass, source, {}) # use record_worker_sync to grab output segments @@ -702,25 +706,37 @@ async def test_durations(hass, record_worker_sync): # check that the Part duration metadata matches the durations in the media running_metadata_duration = 0 for segment in complete_segments: - for part in segment.parts: + av_segment = av.open(io.BytesIO(segment.init + segment.get_data())) + av_segment.close() + for part_num, part in enumerate(segment.parts): av_part = av.open(io.BytesIO(segment.init + part.data)) running_metadata_duration += part.duration - # av_part.duration actually returns the dts of the first packet of - # the next av_part. When we normalize this by av.time_base we get - # the running duration of the media. - # The metadata duration is slightly different. The worker has - # some flexibility of where to set each metadata boundary, and - # when the media's duration is slightly too long, the metadata - # duration is adjusted down. This means that the running metadata - # duration may be up to one video frame duration smaller than the - # part duration. - assert running_metadata_duration < av_part.duration / av.time_base + 1e-6 - assert ( - running_metadata_duration - > av_part.duration / av.time_base - - 1 / av_part.streams.video[0].rate - - 1e-6 + # av_part.duration actually returns the dts of the first packet of the next + # av_part. When we normalize this by av.time_base we get the running + # duration of the media. + # The metadata duration may differ slightly from the media duration. + # The worker has some flexibility of where to set each metadata boundary, + # and when the media's duration is slightly too long or too short, the + # metadata duration may be adjusted up or down. + # We check here that the divergence between the metadata duration and the + # media duration is not too large (2 frames seems reasonable here). + assert math.isclose( + (av_part.duration - av_part.start_time) / av.time_base, + part.duration, + abs_tol=2 / av_part.streams.video[0].rate + 1e-6, ) + # Also check that the sum of the durations so far matches the last dts + # in the media. + assert math.isclose( + running_metadata_duration, + av_part.duration / av.time_base, + abs_tol=1e-6, + ) + # And check that the metadata duration is between 0.85x and 1.0x of + # the part target duration + if not (part.has_keyframe or part_num == len(segment.parts) - 1): + assert part.duration > 0.85 * target_part_duration - 1e-6 + assert part.duration < target_part_duration + 1e-6 av_part.close() # check that the Part durations are consistent with the Segment durations for segment in complete_segments: From 375e9fffd166fa65c60522f4756c16ebd800e0eb Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 1 Nov 2021 04:22:13 +0100 Subject: [PATCH 063/174] Add `configuration_url` to GIOS integration (#58840) --- homeassistant/components/gios/const.py | 2 ++ homeassistant/components/gios/sensor.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/homeassistant/components/gios/const.py b/homeassistant/components/gios/const.py index 4f19b0d8a68..9b98b0bda26 100644 --- a/homeassistant/components/gios/const.py +++ b/homeassistant/components/gios/const.py @@ -27,6 +27,8 @@ SCAN_INTERVAL: Final = timedelta(minutes=30) DOMAIN: Final = "gios" MANUFACTURER: Final = "Główny Inspektorat Ochrony Środowiska" +URL = "http://powietrze.gios.gov.pl/pjp/current/station_details/info/{station_id}" + API_TIMEOUT: Final = 30 ATTR_INDEX: Final = "index" diff --git a/homeassistant/components/gios/sensor.py b/homeassistant/components/gios/sensor.py index 5dd48656d12..f60a8e99d5a 100644 --- a/homeassistant/components/gios/sensor.py +++ b/homeassistant/components/gios/sensor.py @@ -25,6 +25,7 @@ from .const import ( DOMAIN, MANUFACTURER, SENSOR_TYPES, + URL, ) from .model import GiosSensorEntityDescription @@ -86,6 +87,7 @@ class GiosSensor(CoordinatorEntity, SensorEntity): identifiers={(DOMAIN, str(coordinator.gios.station_id))}, manufacturer=MANUFACTURER, name=DEFAULT_NAME, + configuration_url=URL.format(station_id=coordinator.gios.station_id), ) self._attr_name = f"{name} {description.name}" self._attr_unique_id = f"{coordinator.gios.station_id}-{description.key}" From 5ad1ec611d47fe8317ca59dd7ead7771d0bcc0e1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 31 Oct 2021 20:24:09 -0700 Subject: [PATCH 064/174] Bumped version to 2021.11.0b3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index b5edbd66a86..7edfdedd1f1 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 11 -PATCH_VERSION: Final = "0b2" +PATCH_VERSION: Final = "0b3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From 2de74c86e30afec4402141131e385cbf1db1a375 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sun, 17 Oct 2021 10:50:48 +0200 Subject: [PATCH 065/174] Fix Tuya documentation URL (#57889) --- homeassistant/components/tuya/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tuya/manifest.json b/homeassistant/components/tuya/manifest.json index 20df33f4573..672e5eeb0a0 100644 --- a/homeassistant/components/tuya/manifest.json +++ b/homeassistant/components/tuya/manifest.json @@ -1,7 +1,7 @@ { "domain": "tuya", "name": "Tuya", - "documentation": "https://github.com/tuya/tuya-home-assistant", + "documentation": "https://www.home-assistant.io/integrations/tuya", "requirements": ["tuya-iot-py-sdk==0.5.0"], "codeowners": ["@Tuya", "@zlinoliver", "@METISU"], "config_flow": true, From 387413b5f572be1036ca69d56247c81a529f1e34 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 18 Oct 2021 12:01:58 +0200 Subject: [PATCH 066/174] Fix netgear NoneType and discovery (#57904) --- homeassistant/components/netgear/config_flow.py | 4 +++- homeassistant/components/netgear/router.py | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/netgear/config_flow.py b/homeassistant/components/netgear/config_flow.py index 871cba5a95d..6ce97fdbe60 100644 --- a/homeassistant/components/netgear/config_flow.py +++ b/homeassistant/components/netgear/config_flow.py @@ -142,7 +142,9 @@ class NetgearFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): updated_data[CONF_PORT] = DEFAULT_PORT for model in MODELS_V2: - if discovery_info.get(ssdp.ATTR_UPNP_MODEL_NUMBER, "").startswith(model): + if discovery_info.get(ssdp.ATTR_UPNP_MODEL_NUMBER, "").startswith( + model + ) or discovery_info.get(ssdp.ATTR_UPNP_MODEL_NAME, "").startswith(model): updated_data[CONF_PORT] = ORBI_PORT self.placeholders.update(updated_data) diff --git a/homeassistant/components/netgear/router.py b/homeassistant/components/netgear/router.py index cc508f043ff..e4538d3df29 100644 --- a/homeassistant/components/netgear/router.py +++ b/homeassistant/components/netgear/router.py @@ -199,6 +199,9 @@ class NetgearRouter: ntg_devices = await self.async_get_attached_devices() now = dt_util.utcnow() + if ntg_devices is None: + return + if _LOGGER.isEnabledFor(logging.DEBUG): _LOGGER.debug("Netgear scan result: \n%s", ntg_devices) From ae463cb21079ffeaf8261dfd369962211a5065fd Mon Sep 17 00:00:00 2001 From: Andrey Kupreychik Date: Thu, 21 Oct 2021 04:53:23 +0700 Subject: [PATCH 067/174] Abort keenetic SSDP discovery if the unique id is already setup or ignored (#58009) --- .../components/keenetic_ndms2/config_flow.py | 1 + .../keenetic_ndms2/test_config_flow.py | 50 +++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/homeassistant/components/keenetic_ndms2/config_flow.py b/homeassistant/components/keenetic_ndms2/config_flow.py index fdb7dafc516..96caea06304 100644 --- a/homeassistant/components/keenetic_ndms2/config_flow.py +++ b/homeassistant/components/keenetic_ndms2/config_flow.py @@ -118,6 +118,7 @@ class KeeneticFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): host = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]).hostname await self.async_set_unique_id(discovery_info[ssdp.ATTR_UPNP_UDN]) + self._abort_if_unique_id_configured(updates={CONF_HOST: host}) self._async_abort_entries_match({CONF_HOST: host}) diff --git a/tests/components/keenetic_ndms2/test_config_flow.py b/tests/components/keenetic_ndms2/test_config_flow.py index 7e7d4882544..23c1bead25e 100644 --- a/tests/components/keenetic_ndms2/test_config_flow.py +++ b/tests/components/keenetic_ndms2/test_config_flow.py @@ -211,6 +211,56 @@ async def test_ssdp_already_configured(hass: HomeAssistant) -> None: assert result["reason"] == "already_configured" +async def test_ssdp_ignored(hass: HomeAssistant) -> None: + """Test unique ID ignored and discovered.""" + + entry = MockConfigEntry( + domain=keenetic.DOMAIN, + source=config_entries.SOURCE_IGNORE, + unique_id=MOCK_SSDP_DISCOVERY_INFO[ssdp.ATTR_UPNP_UDN], + ) + entry.add_to_hass(hass) + + discovery_info = MOCK_SSDP_DISCOVERY_INFO.copy() + result = await hass.config_entries.flow.async_init( + keenetic.DOMAIN, + context={CONF_SOURCE: config_entries.SOURCE_SSDP}, + data=discovery_info, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_ssdp_update_host(hass: HomeAssistant) -> None: + """Test unique ID configured and discovered with the new host.""" + + entry = MockConfigEntry( + domain=keenetic.DOMAIN, + data=MOCK_DATA, + options=MOCK_OPTIONS, + unique_id=MOCK_SSDP_DISCOVERY_INFO[ssdp.ATTR_UPNP_UDN], + ) + entry.add_to_hass(hass) + + new_ip = "10.10.10.10" + + discovery_info = { + **MOCK_SSDP_DISCOVERY_INFO, + ssdp.ATTR_SSDP_LOCATION: f"http://{new_ip}/", + } + + result = await hass.config_entries.flow.async_init( + keenetic.DOMAIN, + context={CONF_SOURCE: config_entries.SOURCE_SSDP}, + data=discovery_info, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + assert entry.data[CONF_HOST] == new_ip + + async def test_ssdp_reject_no_udn(hass: HomeAssistant) -> None: """Discovered device has no UDN.""" From a3c0f7b167e312b1fda2a23d8f35586050b081de Mon Sep 17 00:00:00 2001 From: micha91 Date: Wed, 20 Oct 2021 00:18:08 +0200 Subject: [PATCH 068/174] Fix Yamaha MusicCast media_stop (#58024) --- homeassistant/components/yamaha_musiccast/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/yamaha_musiccast/media_player.py b/homeassistant/components/yamaha_musiccast/media_player.py index 5081a716357..eb48aa1b410 100644 --- a/homeassistant/components/yamaha_musiccast/media_player.py +++ b/homeassistant/components/yamaha_musiccast/media_player.py @@ -309,7 +309,7 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): async def async_media_stop(self): """Send stop command.""" if self._is_netusb: - await self.coordinator.musiccast.netusb_pause() + await self.coordinator.musiccast.netusb_stop() else: raise HomeAssistantError( "Service stop is not supported for non NetUSB sources." From 698ceda7c5ce7e4d1d8a72066bb0fb4511c36aed Mon Sep 17 00:00:00 2001 From: Michael Chisholm Date: Fri, 22 Oct 2021 23:22:10 +1100 Subject: [PATCH 069/174] Sleep between device requests to detect socket closes (#58087) --- homeassistant/components/dlna_dmr/data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/dlna_dmr/data.py b/homeassistant/components/dlna_dmr/data.py index 8d4693dd435..8a43fc23763 100644 --- a/homeassistant/components/dlna_dmr/data.py +++ b/homeassistant/components/dlna_dmr/data.py @@ -39,7 +39,7 @@ class DlnaDmrData: """Initialize global data.""" self.lock = asyncio.Lock() session = aiohttp_client.async_get_clientsession(hass, verify_ssl=False) - self.requester = AiohttpSessionRequester(session, with_sleep=False) + self.requester = AiohttpSessionRequester(session, with_sleep=True) self.upnp_factory = UpnpFactory(self.requester, non_strict=True) self.event_notifiers = {} self.event_notifier_refs = defaultdict(int) From 97ba3689500b1d1a6358d9fe3d5e3b1751b59150 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 20 Oct 2021 23:53:06 +0200 Subject: [PATCH 070/174] Fix template sensor when name template doesn't render (#58088) --- homeassistant/components/template/sensor.py | 1 + tests/components/template/test_sensor.py | 26 +++++++++++++++++---- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index 4214323c8ee..ea203bdd879 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -245,6 +245,7 @@ class SensorTemplate(TemplateEntity, SensorEntity): self._friendly_name_template = friendly_name_template + self._attr_name = None # Try to render the name as it can influence the entity ID if friendly_name_template: friendly_name_template.hass = hass diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index 242ac09d3d0..269cb6d4350 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -115,7 +115,7 @@ async def test_entity_picture_template(hass, start_ha): @pytest.mark.parametrize("count,domain", [(1, sensor.DOMAIN)]) @pytest.mark.parametrize( - "attribute,config", + "attribute,config,expected", [ ( "friendly_name", @@ -130,6 +130,22 @@ async def test_entity_picture_template(hass, start_ha): }, }, }, + ("It .", "It Works."), + ), + ( + "friendly_name", + { + "sensor": { + "platform": "template", + "sensors": { + "test_template_sensor": { + "value_template": "{{ states.sensor.test_state.state }}", + "friendly_name_template": "{{ 'It ' + states.sensor.test_state.state + '.'}}", + } + }, + }, + }, + (None, "It Works."), ), ( "friendly_name", @@ -144,6 +160,7 @@ async def test_entity_picture_template(hass, start_ha): }, }, }, + ("It .", "It Works."), ), ( "test_attribute", @@ -160,16 +177,17 @@ async def test_entity_picture_template(hass, start_ha): }, }, }, + ("It .", "It Works."), ), ], ) -async def test_friendly_name_template(hass, attribute, start_ha): +async def test_friendly_name_template(hass, attribute, expected, start_ha): """Test friendly_name template with an unknown value_template.""" - assert hass.states.get(TEST_NAME).attributes.get(attribute) == "It ." + assert hass.states.get(TEST_NAME).attributes.get(attribute) == expected[0] hass.states.async_set("sensor.test_state", "Works") await hass.async_block_till_done() - assert hass.states.get(TEST_NAME).attributes[attribute] == "It Works." + assert hass.states.get(TEST_NAME).attributes[attribute] == expected[1] @pytest.mark.parametrize("count,domain", [(0, sensor.DOMAIN)]) From fe5b9c75b3da8a59b04421600f1bb96686ca8dc2 Mon Sep 17 00:00:00 2001 From: Brandon Rothweiler Date: Wed, 20 Oct 2021 18:58:40 -0400 Subject: [PATCH 071/174] Bump pymazda to 0.2.2 (#58113) --- homeassistant/components/mazda/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mazda/manifest.json b/homeassistant/components/mazda/manifest.json index 7eb85f722ae..27c6f0f5097 100644 --- a/homeassistant/components/mazda/manifest.json +++ b/homeassistant/components/mazda/manifest.json @@ -3,7 +3,7 @@ "name": "Mazda Connected Services", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/mazda", - "requirements": ["pymazda==0.2.1"], + "requirements": ["pymazda==0.2.2"], "codeowners": ["@bdr99"], "quality_scale": "platinum", "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 32793e053b7..7ef53b10627 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1619,7 +1619,7 @@ pymailgunner==1.4 pymata-express==1.19 # homeassistant.components.mazda -pymazda==0.2.1 +pymazda==0.2.2 # homeassistant.components.mediaroom pymediaroom==0.6.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 13bb6f707ba..9a7df475793 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -950,7 +950,7 @@ pymailgunner==1.4 pymata-express==1.19 # homeassistant.components.mazda -pymazda==0.2.1 +pymazda==0.2.2 # homeassistant.components.melcloud pymelcloud==2.5.4 From 96d1810019e401615450a863f24b1bfc7d77dec5 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 25 Oct 2021 22:44:23 +0200 Subject: [PATCH 072/174] Abort Fritz config flow for configured hostnames (#58140) * Abort Fritz config flow for configured hostnames * Fix tests + consider all combinations * Fix async context --- homeassistant/components/fritz/config_flow.py | 11 +++++- tests/components/fritz/test_config_flow.py | 35 ++++++++++++++----- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/fritz/config_flow.py b/homeassistant/components/fritz/config_flow.py index 5ca351cdec1..55c60cc41a8 100644 --- a/homeassistant/components/fritz/config_flow.py +++ b/homeassistant/components/fritz/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +import socket from typing import Any from urllib.parse import ParseResult, urlparse @@ -85,8 +86,16 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): async def async_check_configured_entry(self) -> ConfigEntry | None: """Check if entry is configured.""" + + current_host = await self.hass.async_add_executor_job( + socket.gethostbyname, self._host + ) + for entry in self._async_current_entries(include_ignore=False): - if entry.data[CONF_HOST] == self._host: + entry_host = await self.hass.async_add_executor_job( + socket.gethostbyname, entry.data[CONF_HOST] + ) + if entry_host == current_host: return entry return None diff --git a/tests/components/fritz/test_config_flow.py b/tests/components/fritz/test_config_flow.py index 0aecefedf0d..2d276293baa 100644 --- a/tests/components/fritz/test_config_flow.py +++ b/tests/components/fritz/test_config_flow.py @@ -41,6 +41,7 @@ ATTR_HOST = "host" ATTR_NEW_SERIAL_NUMBER = "NewSerialNumber" MOCK_HOST = "fake_host" +MOCK_IP = "192.168.178.1" MOCK_SERIAL_NUMBER = "fake_serial_number" MOCK_FIRMWARE_INFO = [True, "1.1.1"] @@ -51,7 +52,7 @@ MOCK_DEVICE_INFO = { } MOCK_IMPORT_CONFIG = {CONF_HOST: MOCK_HOST, CONF_USERNAME: "username"} MOCK_SSDP_DATA = { - ATTR_SSDP_LOCATION: "https://fake_host:12345/test", + ATTR_SSDP_LOCATION: f"https://{MOCK_IP}:12345/test", ATTR_UPNP_FRIENDLY_NAME: "fake_name", ATTR_UPNP_UDN: "uuid:only-a-test", } @@ -81,7 +82,10 @@ async def test_user(hass: HomeAssistant, fc_class_mock, mock_get_source_ip): "requests.get" ) as mock_request_get, patch( "requests.post" - ) as mock_request_post: + ) as mock_request_post, patch( + "homeassistant.components.fritz.config_flow.socket.gethostbyname", + return_value=MOCK_IP, + ): mock_request_get.return_value.status_code = 200 mock_request_get.return_value.content = MOCK_REQUEST @@ -129,7 +133,10 @@ async def test_user_already_configured( "requests.get" ) as mock_request_get, patch( "requests.post" - ) as mock_request_post: + ) as mock_request_post, patch( + "homeassistant.components.fritz.config_flow.socket.gethostbyname", + return_value=MOCK_IP, + ): mock_request_get.return_value.status_code = 200 mock_request_get.return_value.content = MOCK_REQUEST @@ -319,7 +326,10 @@ async def test_ssdp_already_configured( with patch( "homeassistant.components.fritz.common.FritzConnection", side_effect=fc_class_mock, - ), patch("homeassistant.components.fritz.common.FritzStatus"): + ), patch("homeassistant.components.fritz.common.FritzStatus"), patch( + "homeassistant.components.fritz.config_flow.socket.gethostbyname", + return_value=MOCK_IP, + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA @@ -343,7 +353,10 @@ async def test_ssdp_already_configured_host( with patch( "homeassistant.components.fritz.common.FritzConnection", side_effect=fc_class_mock, - ), patch("homeassistant.components.fritz.common.FritzStatus"): + ), patch("homeassistant.components.fritz.common.FritzStatus"), patch( + "homeassistant.components.fritz.config_flow.socket.gethostbyname", + return_value=MOCK_IP, + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA @@ -367,7 +380,10 @@ async def test_ssdp_already_configured_host_uuid( with patch( "homeassistant.components.fritz.common.FritzConnection", side_effect=fc_class_mock, - ), patch("homeassistant.components.fritz.common.FritzStatus"): + ), patch("homeassistant.components.fritz.common.FritzStatus"), patch( + "homeassistant.components.fritz.config_flow.socket.gethostbyname", + return_value=MOCK_IP, + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA @@ -436,7 +452,7 @@ async def test_ssdp(hass: HomeAssistant, fc_class_mock, mock_get_source_ip): ) assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["data"][CONF_HOST] == "fake_host" + assert result["data"][CONF_HOST] == MOCK_IP assert result["data"][CONF_PASSWORD] == "fake_pass" assert result["data"][CONF_USERNAME] == "fake_user" @@ -482,7 +498,10 @@ async def test_import(hass: HomeAssistant, fc_class_mock, mock_get_source_ip): "requests.get" ) as mock_request_get, patch( "requests.post" - ) as mock_request_post: + ) as mock_request_post, patch( + "homeassistant.components.fritz.config_flow.socket.gethostbyname", + return_value=MOCK_IP, + ): mock_request_get.return_value.status_code = 200 mock_request_get.return_value.content = MOCK_REQUEST From 5295ffd6f1a86becd4e641be88853e5536c01cbc Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 1 Nov 2021 17:45:13 +0100 Subject: [PATCH 073/174] Fix find_next_time_expression_time (#58894) * Better tests * Fix find_next_time_expression_time * Add tests for Nov 7th 2021, Chicago transtion * Update event tests * Update test_event.py * small performance improvement Co-authored-by: J. Nick Koston Co-authored-by: Erik Montnemery --- homeassistant/util/dt.py | 67 ++++--- tests/helpers/test_event.py | 141 +++++++++++++-- tests/util/test_dt.py | 345 +++++++++++++++++++++++++++++++++--- 3 files changed, 489 insertions(+), 64 deletions(-) diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index e2dd92a8b95..592b47ab6b1 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -272,7 +272,8 @@ def find_next_time_expression_time( return None return arr[left] - result = now.replace(microsecond=0) + # Reset microseconds and fold; fold (for ambiguous DST times) will be handled later + result = now.replace(microsecond=0, fold=0) # Match next second if (next_second := _lower_bound(seconds, result.second)) is None: @@ -309,40 +310,58 @@ def find_next_time_expression_time( result = result.replace(hour=next_hour) if result.tzinfo in (None, UTC): + # Using UTC, no DST checking needed return result - if _datetime_ambiguous(result): - # This happens when we're leaving daylight saving time and local - # clocks are rolled back. In this case, we want to trigger - # on both the DST and non-DST time. So when "now" is in the DST - # use the DST-on time, and if not, use the DST-off time. - fold = 1 if now.dst() else 0 - if result.fold != fold: - result = result.replace(fold=fold) - if not _datetime_exists(result): - # This happens when we're entering daylight saving time and local - # clocks are rolled forward, thus there are local times that do - # not exist. In this case, we want to trigger on the next time - # that *does* exist. - # In the worst case, this will run through all the seconds in the - # time shift, but that's max 3600 operations for once per year + # When entering DST and clocks are turned forward. + # There are wall clock times that don't "exist" (an hour is skipped). + + # -> trigger on the next time that 1. matches the pattern and 2. does exist + # for example: + # on 2021.03.28 02:00:00 in CET timezone clocks are turned forward an hour + # with pattern "02:30", don't run on 28 mar (such a wall time does not exist on this day) + # instead run at 02:30 the next day + + # We solve this edge case by just iterating one second until the result exists + # (max. 3600 operations, which should be fine for an edge case that happens once a year) return find_next_time_expression_time( result + dt.timedelta(seconds=1), seconds, minutes, hours ) - # Another edge-case when leaving DST: - # When now is in DST and ambiguous *and* the next trigger time we *should* - # trigger is ambiguous and outside DST, the excepts above won't catch it. - # For example: if triggering on 2:30 and now is 28.10.2018 2:30 (in DST) - # we should trigger next on 28.10.2018 2:30 (out of DST), but our - # algorithm above would produce 29.10.2018 2:30 (out of DST) - if _datetime_ambiguous(now): + now_is_ambiguous = _datetime_ambiguous(now) + result_is_ambiguous = _datetime_ambiguous(result) + + # When leaving DST and clocks are turned backward. + # Then there are wall clock times that are ambiguous i.e. exist with DST and without DST + # The logic above does not take into account if a given pattern matches _twice_ + # in a day. + # Example: on 2021.10.31 02:00:00 in CET timezone clocks are turned backward an hour + + if now_is_ambiguous and result_is_ambiguous: + # `now` and `result` are both ambiguous, so the next match happens + # _within_ the current fold. + + # Examples: + # 1. 2021.10.31 02:00:00+02:00 with pattern 02:30 -> 2021.10.31 02:30:00+02:00 + # 2. 2021.10.31 02:00:00+01:00 with pattern 02:30 -> 2021.10.31 02:30:00+01:00 + return result.replace(fold=now.fold) + + if now_is_ambiguous and now.fold == 0 and not result_is_ambiguous: + # `now` is in the first fold, but result is not ambiguous (meaning it no longer matches + # within the fold). + # -> Check if result matches in the next fold. If so, emit that match + + # Turn back the time by the DST offset, effectively run the algorithm on the first fold + # If it matches on the first fold, that means it will also match on the second one. + + # Example: 2021.10.31 02:45:00+02:00 with pattern 02:30 -> 2021.10.31 02:30:00+01:00 + check_result = find_next_time_expression_time( now + _dst_offset_diff(now), seconds, minutes, hours ) if _datetime_ambiguous(check_result): - return check_result + return check_result.replace(fold=1) return result diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index fb7464d405f..7d3dec08ab0 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -2907,9 +2907,19 @@ async def test_periodic_task_entering_dst(hass): dt_util.set_default_time_zone(timezone) specific_runs = [] - now = dt_util.utcnow() + # DST starts early morning March 27th 2022 + yy = 2022 + mm = 3 + dd = 27 + + # There's no 2022-03-27 02:30, the event should not fire until 2022-03-28 02:30 time_that_will_not_match_right_away = datetime( - now.year + 1, 3, 25, 2, 31, 0, tzinfo=timezone + yy, mm, dd, 1, 28, 0, tzinfo=timezone, fold=0 + ) + # Make sure we enter DST during the test + assert ( + time_that_will_not_match_right_away.utcoffset() + != (time_that_will_not_match_right_away + timedelta(hours=2)).utcoffset() ) with patch( @@ -2924,25 +2934,25 @@ async def test_periodic_task_entering_dst(hass): ) async_fire_time_changed( - hass, datetime(now.year + 1, 3, 25, 1, 50, 0, 999999, tzinfo=timezone) + hass, datetime(yy, mm, dd, 1, 50, 0, 999999, tzinfo=timezone) ) await hass.async_block_till_done() assert len(specific_runs) == 0 async_fire_time_changed( - hass, datetime(now.year + 1, 3, 25, 3, 50, 0, 999999, tzinfo=timezone) + hass, datetime(yy, mm, dd, 3, 50, 0, 999999, tzinfo=timezone) ) await hass.async_block_till_done() assert len(specific_runs) == 0 async_fire_time_changed( - hass, datetime(now.year + 1, 3, 26, 1, 50, 0, 999999, tzinfo=timezone) + hass, datetime(yy, mm, dd + 1, 1, 50, 0, 999999, tzinfo=timezone) ) await hass.async_block_till_done() assert len(specific_runs) == 0 async_fire_time_changed( - hass, datetime(now.year + 1, 3, 26, 2, 50, 0, 999999, tzinfo=timezone) + hass, datetime(yy, mm, dd + 1, 2, 50, 0, 999999, tzinfo=timezone) ) await hass.async_block_till_done() assert len(specific_runs) == 1 @@ -2956,10 +2966,19 @@ async def test_periodic_task_leaving_dst(hass): dt_util.set_default_time_zone(timezone) specific_runs = [] - now = dt_util.utcnow() + # DST ends early morning Ocotber 30th 2022 + yy = 2022 + mm = 10 + dd = 30 time_that_will_not_match_right_away = datetime( - now.year + 1, 10, 28, 2, 28, 0, tzinfo=timezone, fold=1 + yy, mm, dd, 2, 28, 0, tzinfo=timezone, fold=0 + ) + + # Make sure we leave DST during the test + assert ( + time_that_will_not_match_right_away.utcoffset() + != time_that_will_not_match_right_away.replace(fold=1).utcoffset() ) with patch( @@ -2973,38 +2992,134 @@ async def test_periodic_task_leaving_dst(hass): second=0, ) + # The task should not fire yet async_fire_time_changed( - hass, datetime(now.year + 1, 10, 28, 2, 5, 0, 999999, tzinfo=timezone, fold=0) + hass, datetime(yy, mm, dd, 2, 28, 0, 999999, tzinfo=timezone, fold=0) ) await hass.async_block_till_done() assert len(specific_runs) == 0 + # The task should fire async_fire_time_changed( - hass, datetime(now.year + 1, 10, 28, 2, 55, 0, 999999, tzinfo=timezone, fold=0) + hass, datetime(yy, mm, dd, 2, 30, 0, 999999, tzinfo=timezone, fold=0) ) await hass.async_block_till_done() assert len(specific_runs) == 1 + # The task should not fire again + async_fire_time_changed( + hass, datetime(yy, mm, dd, 2, 55, 0, 999999, tzinfo=timezone, fold=0) + ) + await hass.async_block_till_done() + assert len(specific_runs) == 1 + + # DST has ended, the task should not fire yet async_fire_time_changed( hass, - datetime(now.year + 2, 10, 28, 2, 45, 0, 999999, tzinfo=timezone, fold=1), + datetime(yy, mm, dd, 2, 15, 0, 999999, tzinfo=timezone, fold=1), + ) + await hass.async_block_till_done() + assert len(specific_runs) == 1 + + # The task should fire + async_fire_time_changed( + hass, + datetime(yy, mm, dd, 2, 45, 0, 999999, tzinfo=timezone, fold=1), ) await hass.async_block_till_done() assert len(specific_runs) == 2 + # The task should not fire again async_fire_time_changed( hass, - datetime(now.year + 2, 10, 28, 2, 55, 0, 999999, tzinfo=timezone, fold=1), + datetime(yy, mm, dd, 2, 55, 0, 999999, tzinfo=timezone, fold=1), ) await hass.async_block_till_done() assert len(specific_runs) == 2 + # The task should fire again the next day async_fire_time_changed( - hass, datetime(now.year + 2, 10, 28, 2, 55, 0, 999999, tzinfo=timezone, fold=1) + hass, datetime(yy, mm, dd + 1, 2, 55, 0, 999999, tzinfo=timezone, fold=1) + ) + await hass.async_block_till_done() + assert len(specific_runs) == 3 + + unsub() + + +async def test_periodic_task_leaving_dst_2(hass): + """Test periodic task behavior when leaving dst.""" + timezone = dt_util.get_time_zone("Europe/Vienna") + dt_util.set_default_time_zone(timezone) + specific_runs = [] + + # DST ends early morning Ocotber 30th 2022 + yy = 2022 + mm = 10 + dd = 30 + + time_that_will_not_match_right_away = datetime( + yy, mm, dd, 2, 28, 0, tzinfo=timezone, fold=0 + ) + # Make sure we leave DST during the test + assert ( + time_that_will_not_match_right_away.utcoffset() + != time_that_will_not_match_right_away.replace(fold=1).utcoffset() + ) + + with patch( + "homeassistant.util.dt.utcnow", return_value=time_that_will_not_match_right_away + ): + unsub = async_track_time_change( + hass, + callback(lambda x: specific_runs.append(x)), + minute=30, + second=0, + ) + + # The task should not fire yet + async_fire_time_changed( + hass, datetime(yy, mm, dd, 2, 28, 0, 999999, tzinfo=timezone, fold=0) + ) + await hass.async_block_till_done() + assert len(specific_runs) == 0 + + # The task should fire + async_fire_time_changed( + hass, datetime(yy, mm, dd, 2, 55, 0, 999999, tzinfo=timezone, fold=0) + ) + await hass.async_block_till_done() + assert len(specific_runs) == 1 + + # DST has ended, the task should not fire yet + async_fire_time_changed( + hass, datetime(yy, mm, dd, 2, 15, 0, 999999, tzinfo=timezone, fold=1) + ) + await hass.async_block_till_done() + assert len(specific_runs) == 1 + + # The task should fire + async_fire_time_changed( + hass, datetime(yy, mm, dd, 2, 45, 0, 999999, tzinfo=timezone, fold=1) ) await hass.async_block_till_done() assert len(specific_runs) == 2 + # The task should not fire again + async_fire_time_changed( + hass, + datetime(yy, mm, dd, 2, 55, 0, 999999, tzinfo=timezone, fold=1), + ) + await hass.async_block_till_done() + assert len(specific_runs) == 2 + + # The task should fire again the next hour + async_fire_time_changed( + hass, datetime(yy, mm, dd, 3, 55, 0, 999999, tzinfo=timezone, fold=0) + ) + await hass.async_block_till_done() + assert len(specific_runs) == 3 + unsub() diff --git a/tests/util/test_dt.py b/tests/util/test_dt.py index 628cb533681..63513c90360 100644 --- a/tests/util/test_dt.py +++ b/tests/util/test_dt.py @@ -224,120 +224,411 @@ def test_find_next_time_expression_time_dst(): tz = dt_util.get_time_zone("Europe/Vienna") dt_util.set_default_time_zone(tz) - def find(dt, hour, minute, second): + def find(dt, hour, minute, second) -> datetime: """Call test_find_next_time_expression_time.""" seconds = dt_util.parse_time_expression(second, 0, 59) minutes = dt_util.parse_time_expression(minute, 0, 59) hours = dt_util.parse_time_expression(hour, 0, 23) - return dt_util.find_next_time_expression_time(dt, seconds, minutes, hours) + local = dt_util.find_next_time_expression_time(dt, seconds, minutes, hours) + return dt_util.as_utc(local) # Entering DST, clocks are rolled forward - assert datetime(2018, 3, 26, 2, 30, 0, tzinfo=tz) == find( + assert dt_util.as_utc(datetime(2018, 3, 26, 2, 30, 0, tzinfo=tz)) == find( datetime(2018, 3, 25, 1, 50, 0, tzinfo=tz), 2, 30, 0 ) - assert datetime(2018, 3, 26, 2, 30, 0, tzinfo=tz) == find( + assert dt_util.as_utc(datetime(2018, 3, 26, 2, 30, 0, tzinfo=tz)) == find( datetime(2018, 3, 25, 3, 50, 0, tzinfo=tz), 2, 30, 0 ) - assert datetime(2018, 3, 26, 2, 30, 0, tzinfo=tz) == find( + assert dt_util.as_utc(datetime(2018, 3, 26, 2, 30, 0, tzinfo=tz)) == find( datetime(2018, 3, 26, 1, 50, 0, tzinfo=tz), 2, 30, 0 ) # Leaving DST, clocks are rolled back - assert datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=0) == find( + assert dt_util.as_utc(datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=0)) == find( datetime(2018, 10, 28, 2, 5, 0, tzinfo=tz, fold=0), 2, 30, 0 ) - assert datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=0) == find( + assert dt_util.as_utc(datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=0)) == find( datetime(2018, 10, 28, 2, 5, 0, tzinfo=tz), 2, 30, 0 ) - assert datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=1) == find( + assert dt_util.as_utc(datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=1)) == find( datetime(2018, 10, 28, 2, 55, 0, tzinfo=tz), 2, 30, 0 ) - assert datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=1) == find( + assert dt_util.as_utc(datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=1)) == find( datetime(2018, 10, 28, 2, 55, 0, tzinfo=tz, fold=0), 2, 30, 0 ) - assert datetime(2018, 10, 28, 4, 30, 0, tzinfo=tz, fold=0) == find( + assert dt_util.as_utc(datetime(2018, 10, 28, 4, 30, 0, tzinfo=tz, fold=0)) == find( datetime(2018, 10, 28, 2, 55, 0, tzinfo=tz, fold=1), 4, 30, 0 ) - assert datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=1) == find( + assert dt_util.as_utc(datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=1)) == find( datetime(2018, 10, 28, 2, 5, 0, tzinfo=tz, fold=1), 2, 30, 0 ) - assert datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=1) == find( + assert dt_util.as_utc(datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=1)) == find( datetime(2018, 10, 28, 2, 55, 0, tzinfo=tz, fold=0), 2, 30, 0 ) +# DST begins on 2021.03.28 2:00, clocks were turned forward 1h; 2:00-3:00 time does not exist +@pytest.mark.parametrize( + "now_dt, expected_dt", + [ + # 00:00 -> 2:30 + ( + datetime(2021, 3, 28, 0, 0, 0), + datetime(2021, 3, 29, 2, 30, 0), + ), + ], +) +def test_find_next_time_expression_entering_dst(now_dt, expected_dt): + """Test entering daylight saving time for find_next_time_expression_time.""" + tz = dt_util.get_time_zone("Europe/Vienna") + dt_util.set_default_time_zone(tz) + # match on 02:30:00 every day + pattern_seconds = dt_util.parse_time_expression(0, 0, 59) + pattern_minutes = dt_util.parse_time_expression(30, 0, 59) + pattern_hours = dt_util.parse_time_expression(2, 0, 59) + + now_dt = now_dt.replace(tzinfo=tz) + expected_dt = expected_dt.replace(tzinfo=tz) + + res_dt = dt_util.find_next_time_expression_time( + now_dt, pattern_seconds, pattern_minutes, pattern_hours + ) + assert dt_util.as_utc(res_dt) == dt_util.as_utc(expected_dt) + + +# DST ends on 2021.10.31 2:00, clocks were turned backward 1h; 2:00-3:00 time is ambiguous +@pytest.mark.parametrize( + "now_dt, expected_dt", + [ + # 00:00 -> 2:30 + ( + datetime(2021, 10, 31, 0, 0, 0), + datetime(2021, 10, 31, 2, 30, 0, fold=0), + ), + # 02:00(0) -> 2:30(0) + ( + datetime(2021, 10, 31, 2, 0, 0, fold=0), + datetime(2021, 10, 31, 2, 30, 0, fold=0), + ), + # 02:15(0) -> 2:30(0) + ( + datetime(2021, 10, 31, 2, 15, 0, fold=0), + datetime(2021, 10, 31, 2, 30, 0, fold=0), + ), + # 02:30:00(0) -> 2:30(1) + ( + datetime(2021, 10, 31, 2, 30, 0, fold=0), + datetime(2021, 10, 31, 2, 30, 0, fold=0), + ), + # 02:30:01(0) -> 2:30(1) + ( + datetime(2021, 10, 31, 2, 30, 1, fold=0), + datetime(2021, 10, 31, 2, 30, 0, fold=1), + ), + # 02:45(0) -> 2:30(1) + ( + datetime(2021, 10, 31, 2, 45, 0, fold=0), + datetime(2021, 10, 31, 2, 30, 0, fold=1), + ), + # 02:00(1) -> 2:30(1) + ( + datetime(2021, 10, 31, 2, 0, 0, fold=1), + datetime(2021, 10, 31, 2, 30, 0, fold=1), + ), + # 02:15(1) -> 2:30(1) + ( + datetime(2021, 10, 31, 2, 15, 0, fold=1), + datetime(2021, 10, 31, 2, 30, 0, fold=1), + ), + # 02:30:00(1) -> 2:30(1) + ( + datetime(2021, 10, 31, 2, 30, 0, fold=1), + datetime(2021, 10, 31, 2, 30, 0, fold=1), + ), + # 02:30:01(1) -> 2:30 next day + ( + datetime(2021, 10, 31, 2, 30, 1, fold=1), + datetime(2021, 11, 1, 2, 30, 0), + ), + # 02:45(1) -> 2:30 next day + ( + datetime(2021, 10, 31, 2, 45, 0, fold=1), + datetime(2021, 11, 1, 2, 30, 0), + ), + # 08:00(1) -> 2:30 next day + ( + datetime(2021, 10, 31, 8, 0, 1), + datetime(2021, 11, 1, 2, 30, 0), + ), + ], +) +def test_find_next_time_expression_exiting_dst(now_dt, expected_dt): + """Test exiting daylight saving time for find_next_time_expression_time.""" + tz = dt_util.get_time_zone("Europe/Vienna") + dt_util.set_default_time_zone(tz) + # match on 02:30:00 every day + pattern_seconds = dt_util.parse_time_expression(0, 0, 59) + pattern_minutes = dt_util.parse_time_expression(30, 0, 59) + pattern_hours = dt_util.parse_time_expression(2, 0, 59) + + now_dt = now_dt.replace(tzinfo=tz) + expected_dt = expected_dt.replace(tzinfo=tz) + + res_dt = dt_util.find_next_time_expression_time( + now_dt, pattern_seconds, pattern_minutes, pattern_hours + ) + assert dt_util.as_utc(res_dt) == dt_util.as_utc(expected_dt) + + def test_find_next_time_expression_time_dst_chicago(): """Test daylight saving time for find_next_time_expression_time.""" tz = dt_util.get_time_zone("America/Chicago") dt_util.set_default_time_zone(tz) - def find(dt, hour, minute, second): + def find(dt, hour, minute, second) -> datetime: """Call test_find_next_time_expression_time.""" seconds = dt_util.parse_time_expression(second, 0, 59) minutes = dt_util.parse_time_expression(minute, 0, 59) hours = dt_util.parse_time_expression(hour, 0, 23) - return dt_util.find_next_time_expression_time(dt, seconds, minutes, hours) + local = dt_util.find_next_time_expression_time(dt, seconds, minutes, hours) + return dt_util.as_utc(local) # Entering DST, clocks are rolled forward - assert datetime(2021, 3, 15, 2, 30, 0, tzinfo=tz) == find( + assert dt_util.as_utc(datetime(2021, 3, 15, 2, 30, 0, tzinfo=tz)) == find( datetime(2021, 3, 14, 1, 50, 0, tzinfo=tz), 2, 30, 0 ) - assert datetime(2021, 3, 15, 2, 30, 0, tzinfo=tz) == find( + assert dt_util.as_utc(datetime(2021, 3, 15, 2, 30, 0, tzinfo=tz)) == find( datetime(2021, 3, 14, 3, 50, 0, tzinfo=tz), 2, 30, 0 ) - assert datetime(2021, 3, 15, 2, 30, 0, tzinfo=tz) == find( + assert dt_util.as_utc(datetime(2021, 3, 15, 2, 30, 0, tzinfo=tz)) == find( datetime(2021, 3, 14, 1, 50, 0, tzinfo=tz), 2, 30, 0 ) - assert datetime(2021, 3, 14, 3, 30, 0, tzinfo=tz) == find( + assert dt_util.as_utc(datetime(2021, 3, 14, 3, 30, 0, tzinfo=tz)) == find( datetime(2021, 3, 14, 1, 50, 0, tzinfo=tz), 3, 30, 0 ) # Leaving DST, clocks are rolled back - assert datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=0) == find( + assert dt_util.as_utc(datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=0)) == find( datetime(2021, 11, 7, 2, 5, 0, tzinfo=tz, fold=0), 2, 30, 0 ) - assert datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz) == find( + assert dt_util.as_utc(datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz)) == find( datetime(2021, 11, 7, 2, 5, 0, tzinfo=tz), 2, 30, 0 ) - assert datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=0) == find( + assert dt_util.as_utc(datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=0)) == find( datetime(2021, 11, 7, 2, 5, 0, tzinfo=tz), 2, 30, 0 ) - assert datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=1) == find( + assert dt_util.as_utc(datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=1)) == find( datetime(2021, 11, 7, 2, 10, 0, tzinfo=tz), 2, 30, 0 ) - assert datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=1) == find( + assert dt_util.as_utc(datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=1)) == find( datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=0), 2, 30, 0 ) - assert datetime(2021, 11, 8, 2, 30, 0, tzinfo=tz, fold=1) == find( + assert dt_util.as_utc(datetime(2021, 11, 8, 2, 30, 0, tzinfo=tz, fold=1)) == find( datetime(2021, 11, 7, 2, 55, 0, tzinfo=tz, fold=0), 2, 30, 0 ) - assert datetime(2021, 11, 7, 4, 30, 0, tzinfo=tz, fold=0) == find( + assert dt_util.as_utc(datetime(2021, 11, 7, 4, 30, 0, tzinfo=tz, fold=0)) == find( datetime(2021, 11, 7, 2, 55, 0, tzinfo=tz, fold=1), 4, 30, 0 ) - assert datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=1) == find( + assert dt_util.as_utc(datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=1)) == find( datetime(2021, 11, 7, 2, 5, 0, tzinfo=tz, fold=1), 2, 30, 0 ) - assert datetime(2021, 11, 8, 2, 30, 0, tzinfo=tz) == find( + assert dt_util.as_utc(datetime(2021, 11, 8, 2, 30, 0, tzinfo=tz)) == find( datetime(2021, 11, 7, 2, 55, 0, tzinfo=tz, fold=0), 2, 30, 0 ) + + +def _get_matches(hours, minutes, seconds): + matching_hours = dt_util.parse_time_expression(hours, 0, 23) + matching_minutes = dt_util.parse_time_expression(minutes, 0, 59) + matching_seconds = dt_util.parse_time_expression(seconds, 0, 59) + return matching_hours, matching_minutes, matching_seconds + + +def test_find_next_time_expression_day_before_dst_change_the_same_time(): + """Test the day before DST to establish behavior without DST.""" + tz = dt_util.get_time_zone("America/Chicago") + dt_util.set_default_time_zone(tz) + + # Not in DST yet + hour_minute_second = (12, 30, 1) + test_time = datetime(2021, 10, 7, *hour_minute_second, tzinfo=tz, fold=0) + matching_hours, matching_minutes, matching_seconds = _get_matches( + *hour_minute_second + ) + next_time = dt_util.find_next_time_expression_time( + test_time, matching_seconds, matching_minutes, matching_hours + ) + assert next_time == datetime(2021, 10, 7, *hour_minute_second, tzinfo=tz, fold=0) + assert next_time.fold == 0 + assert dt_util.as_utc(next_time) == datetime( + 2021, 10, 7, 17, 30, 1, tzinfo=dt_util.UTC + ) + + +def test_find_next_time_expression_time_leave_dst_chicago_before_the_fold_30_s(): + """Test leaving daylight saving time for find_next_time_expression_time 30s into the future.""" + tz = dt_util.get_time_zone("America/Chicago") + dt_util.set_default_time_zone(tz) + + # Leaving DST, clocks are rolled back + + # Move ahead 30 seconds not folded yet + hour_minute_second = (1, 30, 31) + test_time = datetime(2021, 11, 7, 1, 30, 1, tzinfo=tz, fold=0) + matching_hours, matching_minutes, matching_seconds = _get_matches( + *hour_minute_second + ) + next_time = dt_util.find_next_time_expression_time( + test_time, matching_seconds, matching_minutes, matching_hours + ) + assert next_time == datetime(2021, 11, 7, 1, 30, 31, tzinfo=tz, fold=0) + assert dt_util.as_utc(next_time) == datetime( + 2021, 11, 7, 6, 30, 31, tzinfo=dt_util.UTC + ) + assert next_time.fold == 0 + + +def test_find_next_time_expression_time_leave_dst_chicago_before_the_fold_same_time(): + """Test leaving daylight saving time for find_next_time_expression_time with the same time.""" + tz = dt_util.get_time_zone("America/Chicago") + dt_util.set_default_time_zone(tz) + + # Leaving DST, clocks are rolled back + + # Move to the same time not folded yet + hour_minute_second = (0, 30, 1) + test_time = datetime(2021, 11, 7, *hour_minute_second, tzinfo=tz, fold=0) + matching_hours, matching_minutes, matching_seconds = _get_matches( + *hour_minute_second + ) + next_time = dt_util.find_next_time_expression_time( + test_time, matching_seconds, matching_minutes, matching_hours + ) + assert next_time == datetime(2021, 11, 7, *hour_minute_second, tzinfo=tz, fold=0) + assert dt_util.as_utc(next_time) == datetime( + 2021, 11, 7, 5, 30, 1, tzinfo=dt_util.UTC + ) + assert next_time.fold == 0 + + +def test_find_next_time_expression_time_leave_dst_chicago_into_the_fold_same_time(): + """Test leaving daylight saving time for find_next_time_expression_time.""" + tz = dt_util.get_time_zone("America/Chicago") + dt_util.set_default_time_zone(tz) + + # Leaving DST, clocks are rolled back + + # Find the same time inside the fold + hour_minute_second = (1, 30, 1) + test_time = datetime(2021, 11, 7, *hour_minute_second, tzinfo=tz, fold=0) + matching_hours, matching_minutes, matching_seconds = _get_matches( + *hour_minute_second + ) + + next_time = dt_util.find_next_time_expression_time( + test_time, matching_seconds, matching_minutes, matching_hours + ) + assert next_time == datetime(2021, 11, 7, *hour_minute_second, tzinfo=tz, fold=1) + assert next_time.fold == 0 + assert dt_util.as_utc(next_time) == datetime( + 2021, 11, 7, 6, 30, 1, tzinfo=dt_util.UTC + ) + + +def test_find_next_time_expression_time_leave_dst_chicago_into_the_fold_ahead_1_hour_10_min(): + """Test leaving daylight saving time for find_next_time_expression_time.""" + tz = dt_util.get_time_zone("America/Chicago") + dt_util.set_default_time_zone(tz) + + # Leaving DST, clocks are rolled back + + # Find 1h 10m after into the fold + # Start at 01:30:01 fold=0 + # Reach to 01:20:01 fold=1 + hour_minute_second = (1, 20, 1) + test_time = datetime(2021, 11, 7, 1, 30, 1, tzinfo=tz, fold=0) + matching_hours, matching_minutes, matching_seconds = _get_matches( + *hour_minute_second + ) + + next_time = dt_util.find_next_time_expression_time( + test_time, matching_seconds, matching_minutes, matching_hours + ) + assert next_time == datetime(2021, 11, 7, *hour_minute_second, tzinfo=tz, fold=1) + assert next_time.fold == 1 # time is ambiguous + assert dt_util.as_utc(next_time) == datetime( + 2021, 11, 7, 7, 20, 1, tzinfo=dt_util.UTC + ) + + +def test_find_next_time_expression_time_leave_dst_chicago_inside_the_fold_ahead_10_min(): + """Test leaving daylight saving time for find_next_time_expression_time.""" + tz = dt_util.get_time_zone("America/Chicago") + dt_util.set_default_time_zone(tz) + + # Leaving DST, clocks are rolled back + + # Find 10m later while we are in the fold + # Start at 01:30:01 fold=0 + # Reach to 01:40:01 fold=1 + hour_minute_second = (1, 40, 1) + test_time = datetime(2021, 11, 7, 1, 30, 1, tzinfo=tz, fold=1) + matching_hours, matching_minutes, matching_seconds = _get_matches( + *hour_minute_second + ) + + next_time = dt_util.find_next_time_expression_time( + test_time, matching_seconds, matching_minutes, matching_hours + ) + assert next_time == datetime(2021, 11, 7, *hour_minute_second, tzinfo=tz, fold=1) + assert next_time.fold == 1 # time is ambiguous + assert dt_util.as_utc(next_time) == datetime( + 2021, 11, 7, 7, 40, 1, tzinfo=dt_util.UTC + ) + + +def test_find_next_time_expression_time_leave_dst_chicago_past_the_fold_ahead_2_hour_10_min(): + """Test leaving daylight saving time for find_next_time_expression_time.""" + tz = dt_util.get_time_zone("America/Chicago") + dt_util.set_default_time_zone(tz) + + # Leaving DST, clocks are rolled back + + # Find 1h 10m after into the fold + # Start at 01:30:01 fold=0 + # Reach to 02:20:01 past the fold + hour_minute_second = (2, 20, 1) + test_time = datetime(2021, 11, 7, 1, 30, 1, tzinfo=tz, fold=0) + matching_hours, matching_minutes, matching_seconds = _get_matches( + *hour_minute_second + ) + + next_time = dt_util.find_next_time_expression_time( + test_time, matching_seconds, matching_minutes, matching_hours + ) + assert next_time == datetime(2021, 11, 7, *hour_minute_second, tzinfo=tz, fold=1) + assert next_time.fold == 0 # Time is no longer ambiguous + assert dt_util.as_utc(next_time) == datetime( + 2021, 11, 7, 8, 20, 1, tzinfo=dt_util.UTC + ) From ad55af4f67c9a1e3a87b8cc56edc19e954b86a9a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 1 Nov 2021 10:01:08 -0700 Subject: [PATCH 074/174] Bumped version to 2021.10.7 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 6895f18472a..9715023c46f 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 10 -PATCH_VERSION: Final = "6" +PATCH_VERSION: Final = "7" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From 6e9d759798e9b93a7c3e485b1210ab6c14389194 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20H=C3=B6rsken?= Date: Mon, 1 Nov 2021 13:27:58 +0100 Subject: [PATCH 075/174] Fix OpenWeatherMap options not being initialized the first time (#58736) --- homeassistant/components/openweathermap/config_flow.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/openweathermap/config_flow.py b/homeassistant/components/openweathermap/config_flow.py index 507f1b6f721..0b7a3a1a25f 100644 --- a/homeassistant/components/openweathermap/config_flow.py +++ b/homeassistant/components/openweathermap/config_flow.py @@ -109,13 +109,15 @@ class OpenWeatherMapOptionsFlow(config_entries.OptionsFlow): vol.Optional( CONF_MODE, default=self.config_entry.options.get( - CONF_MODE, DEFAULT_FORECAST_MODE + CONF_MODE, + self.config_entry.data.get(CONF_MODE, DEFAULT_FORECAST_MODE), ), ): vol.In(FORECAST_MODES), vol.Optional( CONF_LANGUAGE, default=self.config_entry.options.get( - CONF_LANGUAGE, DEFAULT_LANGUAGE + CONF_LANGUAGE, + self.config_entry.data.get(CONF_LANGUAGE, DEFAULT_LANGUAGE), ), ): vol.In(LANGUAGES), } From 7a0443e2a61b2a53d04dd27c60a877686082fea7 Mon Sep 17 00:00:00 2001 From: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com> Date: Mon, 1 Nov 2021 13:29:00 +0100 Subject: [PATCH 076/174] Add ROCKROBO_S4_MAX to supported xiaomi vacuums (#58826) --- homeassistant/components/xiaomi_miio/const.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index 578a6d3ffab..69c279df493 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -198,12 +198,14 @@ MODELS_LIGHT = ( # TODO: use const from pythonmiio once new release with the constant has been published. # pylint: disable=fixme ROCKROBO_S4 = "roborock.vacuum.s4" +ROCKROBO_S4_MAX = "roborock.vacuum.a19" ROCKROBO_S5_MAX = "roborock.vacuum.s5e" ROCKROBO_E2 = "roborock.vacuum.e2" MODELS_VACUUM = [ ROCKROBO_V1, ROCKROBO_E2, ROCKROBO_S4, + ROCKROBO_S4_MAX, ROCKROBO_S5, ROCKROBO_S5_MAX, ROCKROBO_S6, From 77c25aa141d43348902ad9a1eb89c561ae0c4e1d Mon Sep 17 00:00:00 2001 From: purcell-lab <79175134+purcell-lab@users.noreply.github.com> Date: Tue, 2 Nov 2021 01:37:48 +1100 Subject: [PATCH 077/174] Fix renamed solaredge sensor keys (#58875) --- homeassistant/components/solaredge/sensor.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/solaredge/sensor.py b/homeassistant/components/solaredge/sensor.py index e6c3fc3571a..a151a50a9c8 100644 --- a/homeassistant/components/solaredge/sensor.py +++ b/homeassistant/components/solaredge/sensor.py @@ -92,11 +92,11 @@ class SolarEdgeSensorFactory: self.services[key] = (SolarEdgeStorageLevelSensor, flow) for key in ( - "purchased_power", - "production_power", - "feedin_power", - "consumption_power", - "selfconsumption_power", + "purchased_energy", + "production_energy", + "feedin_energy", + "consumption_energy", + "selfconsumption_energy", ): self.services[key] = (SolarEdgeEnergyDetailsSensor, energy) From cfa4f24395d5922c877628d313828df92cdd6f19 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 1 Nov 2021 17:40:15 +0100 Subject: [PATCH 078/174] Handle `None` values in Xiaomi Miio integration (#58880) * Initial commit * Improve _handle_coordinator_update() * Fix entity_description define * Improve sensor & binary_sensor platforms * Log None value * Use coordinator variable * Improve log strings * Filter attributes with None values * Add hasattr condition * Update homeassistant/components/xiaomi_miio/sensor.py Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- .../components/xiaomi_miio/binary_sensor.py | 32 +++++++-- .../components/xiaomi_miio/device.py | 13 +--- homeassistant/components/xiaomi_miio/fan.py | 9 --- .../components/xiaomi_miio/humidifier.py | 9 --- .../components/xiaomi_miio/number.py | 9 --- .../components/xiaomi_miio/select.py | 11 +--- .../components/xiaomi_miio/sensor.py | 66 ++++++++++++------- .../components/xiaomi_miio/switch.py | 9 --- 8 files changed, 72 insertions(+), 86 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/binary_sensor.py b/homeassistant/components/xiaomi_miio/binary_sensor.py index d33059c20ef..1bdd647da79 100644 --- a/homeassistant/components/xiaomi_miio/binary_sensor.py +++ b/homeassistant/components/xiaomi_miio/binary_sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations from dataclasses import dataclass +import logging from typing import Callable from homeassistant.components.binary_sensor import ( @@ -12,6 +13,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntityDescription, ) from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC +from homeassistant.core import callback from . import VacuumCoordinatorDataAttributes from .const import ( @@ -30,6 +32,8 @@ from .const import ( ) from .device import XiaomiCoordinatedMiioEntity +_LOGGER = logging.getLogger(__name__) + ATTR_NO_WATER = "no_water" ATTR_POWERSUPPLY_ATTACHED = "powersupply_attached" ATTR_WATER_TANK_DETACHED = "water_tank_detached" @@ -108,21 +112,29 @@ HUMIDIFIER_MJJSQ_BINARY_SENSORS = (ATTR_NO_WATER, ATTR_WATER_TANK_DETACHED) def _setup_vacuum_sensors(hass, config_entry, async_add_entities): """Only vacuums with mop should have binary sensor registered.""" - if config_entry.data[CONF_MODEL] not in MODELS_VACUUM_WITH_MOP: return device = hass.data[DOMAIN][config_entry.entry_id].get(KEY_DEVICE) + coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] entities = [] for sensor, description in VACUUM_SENSORS.items(): + parent_key_data = getattr(coordinator.data, description.parent_key) + if getattr(parent_key_data, description.key, None) is None: + _LOGGER.debug( + "It seems the %s does not support the %s as the initial value is None", + config_entry.data[CONF_MODEL], + description.key, + ) + continue entities.append( XiaomiGenericBinarySensor( f"{config_entry.title} {description.name}", device, config_entry, f"{sensor}_{config_entry.unique_id}", - hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR], + coordinator, description, ) ) @@ -168,18 +180,26 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class XiaomiGenericBinarySensor(XiaomiCoordinatedMiioEntity, BinarySensorEntity): """Representation of a Xiaomi Humidifier binary sensor.""" + entity_description: XiaomiMiioBinarySensorDescription + def __init__(self, name, device, entry, unique_id, coordinator, description): """Initialize the entity.""" super().__init__(name, device, entry, unique_id, coordinator) - self.entity_description: XiaomiMiioBinarySensorDescription = description + self.entity_description = description self._attr_entity_registry_enabled_default = ( description.entity_registry_enabled_default ) + self._attr_is_on = self._determine_native_value() - @property - def is_on(self): - """Return true if the binary sensor is on.""" + @callback + def _handle_coordinator_update(self) -> None: + self._attr_is_on = self._determine_native_value() + + super()._handle_coordinator_update() + + def _determine_native_value(self): + """Determine native value.""" if self.entity_description.parent_key is not None: return self._extract_value_from_attribute( getattr(self.coordinator.data, self.entity_description.parent_key), diff --git a/homeassistant/components/xiaomi_miio/device.py b/homeassistant/components/xiaomi_miio/device.py index be9c1151aa5..6d56c7c44bd 100644 --- a/homeassistant/components/xiaomi_miio/device.py +++ b/homeassistant/components/xiaomi_miio/device.py @@ -168,17 +168,8 @@ class XiaomiCoordinatedMiioEntity(CoordinatorEntity): return cls._parse_datetime_datetime(value) if isinstance(value, datetime.timedelta): return cls._parse_time_delta(value) - if isinstance(value, float): - return value - if isinstance(value, int): - return value - - _LOGGER.warning( - "Could not determine how to parse state value of type %s for state %s and attribute %s", - type(value), - type(state), - attribute, - ) + if value is None: + _LOGGER.debug("Attribute %s is None, this is unexpected", attribute) return value diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index 01304008b76..07ec4613270 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -1,7 +1,6 @@ """Support for Xiaomi Mi Air Purifier and Xiaomi Mi Air Humidifier.""" from abc import abstractmethod import asyncio -from enum import Enum import logging import math @@ -363,14 +362,6 @@ class XiaomiGenericAirPurifier(XiaomiGenericDevice): return None - @staticmethod - def _extract_value_from_attribute(state, attribute): - value = getattr(state, attribute) - if isinstance(value, Enum): - return value.value - - return value - @callback def _handle_coordinator_update(self): """Fetch state from the device.""" diff --git a/homeassistant/components/xiaomi_miio/humidifier.py b/homeassistant/components/xiaomi_miio/humidifier.py index 411d1428c70..9896bf8f0ea 100644 --- a/homeassistant/components/xiaomi_miio/humidifier.py +++ b/homeassistant/components/xiaomi_miio/humidifier.py @@ -1,5 +1,4 @@ """Support for Xiaomi Mi Air Purifier and Xiaomi Mi Air Humidifier with humidifier entity.""" -from enum import Enum import logging import math @@ -124,14 +123,6 @@ class XiaomiGenericHumidifier(XiaomiCoordinatedMiioEntity, HumidifierEntity): """Return true if device is on.""" return self._state - @staticmethod - def _extract_value_from_attribute(state, attribute): - value = getattr(state, attribute) - if isinstance(value, Enum): - return value.value - - return value - @property def mode(self): """Get the current mode.""" diff --git a/homeassistant/components/xiaomi_miio/number.py b/homeassistant/components/xiaomi_miio/number.py index 1461f33add6..161a690a0df 100644 --- a/homeassistant/components/xiaomi_miio/number.py +++ b/homeassistant/components/xiaomi_miio/number.py @@ -2,7 +2,6 @@ from __future__ import annotations from dataclasses import dataclass -from enum import Enum from homeassistant.components.number import NumberEntity, NumberEntityDescription from homeassistant.const import DEGREE, ENTITY_CATEGORY_CONFIG, TIME_MINUTES @@ -285,14 +284,6 @@ class XiaomiNumberEntity(XiaomiCoordinatedMiioEntity, NumberEntity): return False return super().available - @staticmethod - def _extract_value_from_attribute(state, attribute): - value = getattr(state, attribute) - if isinstance(value, Enum): - return value.value - - return value - async def async_set_value(self, value): """Set an option of the miio device.""" method = getattr(self, self.entity_description.method) diff --git a/homeassistant/components/xiaomi_miio/select.py b/homeassistant/components/xiaomi_miio/select.py index 2753fb09786..ec1be6f3219 100644 --- a/homeassistant/components/xiaomi_miio/select.py +++ b/homeassistant/components/xiaomi_miio/select.py @@ -2,7 +2,6 @@ from __future__ import annotations from dataclasses import dataclass -from enum import Enum from miio.airfresh import LedBrightness as AirfreshLedBrightness from miio.airhumidifier import LedBrightness as AirhumidifierLedBrightness @@ -126,14 +125,6 @@ class XiaomiSelector(XiaomiCoordinatedMiioEntity, SelectEntity): self._attr_options = list(description.options) self.entity_description = description - @staticmethod - def _extract_value_from_attribute(state, attribute): - value = getattr(state, attribute) - if isinstance(value, Enum): - return value.value - - return value - class XiaomiAirHumidifierSelector(XiaomiSelector): """Representation of a Xiaomi Air Humidifier selector.""" @@ -153,7 +144,7 @@ class XiaomiAirHumidifierSelector(XiaomiSelector): ) # Sometimes (quite rarely) the device returns None as the LED brightness so we # check that the value is not None before updating the state. - if led_brightness: + if led_brightness is not None: self._current_led_brightness = led_brightness self.async_write_ha_state() diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index e1e2d91ad1a..f818a809a5c 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -48,6 +48,7 @@ from homeassistant.const import ( TIME_SECONDS, VOLUME_CUBIC_METERS, ) +from homeassistant.core import callback from . import VacuumCoordinatorDataAttributes from .const import ( @@ -529,17 +530,27 @@ VACUUM_SENSORS = { def _setup_vacuum_sensors(hass, config_entry, async_add_entities): + """Set up the Xiaomi vacuum sensors.""" device = hass.data[DOMAIN][config_entry.entry_id].get(KEY_DEVICE) + coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] entities = [] for sensor, description in VACUUM_SENSORS.items(): + parent_key_data = getattr(coordinator.data, description.parent_key) + if getattr(parent_key_data, description.key, None) is None: + _LOGGER.debug( + "It seems the %s does not support the %s as the initial value is None", + config_entry.data[CONF_MODEL], + description.key, + ) + continue entities.append( XiaomiGenericSensor( f"{config_entry.title} {description.name}", device, config_entry, f"{sensor}_{config_entry.unique_id}", - hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR], + coordinator, description, ) ) @@ -637,23 +648,41 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class XiaomiGenericSensor(XiaomiCoordinatedMiioEntity, SensorEntity): """Representation of a Xiaomi generic sensor.""" - def __init__( - self, - name, - device, - entry, - unique_id, - coordinator, - description: XiaomiMiioSensorDescription, - ): + entity_description: XiaomiMiioSensorDescription + + def __init__(self, name, device, entry, unique_id, coordinator, description): """Initialize the entity.""" super().__init__(name, device, entry, unique_id, coordinator) + self.entity_description = description self._attr_unique_id = unique_id - self.entity_description: XiaomiMiioSensorDescription = description + self._attr_native_value = self._determine_native_value() + self._attr_extra_state_attributes = self._extract_attributes(coordinator.data) - @property - def native_value(self): - """Return the state of the device.""" + @callback + def _extract_attributes(self, data): + """Return state attributes with valid values.""" + return { + attr: value + for attr in self.entity_description.attributes + if hasattr(data, attr) + and (value := self._extract_value_from_attribute(data, attr)) is not None + } + + @callback + def _handle_coordinator_update(self): + """Fetch state from the device.""" + native_value = self._determine_native_value() + # Sometimes (quite rarely) the device returns None as the sensor value so we + # check that the value is not None before updating the state. + if native_value is not None: + self._attr_native_value = native_value + self._attr_extra_state_attributes = self._extract_attributes( + self.coordinator.data + ) + self.async_write_ha_state() + + def _determine_native_value(self): + """Determine native value.""" if self.entity_description.parent_key is not None: return self._extract_value_from_attribute( getattr(self.coordinator.data, self.entity_description.parent_key), @@ -664,15 +693,6 @@ class XiaomiGenericSensor(XiaomiCoordinatedMiioEntity, SensorEntity): self.coordinator.data, self.entity_description.key ) - @property - def extra_state_attributes(self): - """Return the state attributes.""" - return { - attr: self._extract_value_from_attribute(self.coordinator.data, attr) - for attr in self.entity_description.attributes - if hasattr(self.coordinator.data, attr) - } - class XiaomiAirQualityMonitor(XiaomiMiioEntity, SensorEntity): """Representation of a Xiaomi Air Quality Monitor.""" diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index 5c29253ae73..ab825e2485d 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -3,7 +3,6 @@ from __future__ import annotations import asyncio from dataclasses import dataclass -from enum import Enum from functools import partial import logging @@ -474,14 +473,6 @@ class XiaomiGenericCoordinatedSwitch(XiaomiCoordinatedMiioEntity, SwitchEntity): return False return super().available - @staticmethod - def _extract_value_from_attribute(state, attribute): - value = getattr(state, attribute) - if isinstance(value, Enum): - return value.value - - return value - async def async_turn_on(self, **kwargs) -> None: """Turn on an option of the miio device.""" method = getattr(self, self.entity_description.method_on) From b4021de2b05b9270adb71683473f17760b6b0b8d Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 1 Nov 2021 17:45:13 +0100 Subject: [PATCH 079/174] Fix find_next_time_expression_time (#58894) * Better tests * Fix find_next_time_expression_time * Add tests for Nov 7th 2021, Chicago transtion * Update event tests * Update test_event.py * small performance improvement Co-authored-by: J. Nick Koston Co-authored-by: Erik Montnemery --- homeassistant/util/dt.py | 67 ++++--- tests/helpers/test_event.py | 141 +++++++++++++-- tests/util/test_dt.py | 345 +++++++++++++++++++++++++++++++++--- 3 files changed, 489 insertions(+), 64 deletions(-) diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index e2dd92a8b95..592b47ab6b1 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -272,7 +272,8 @@ def find_next_time_expression_time( return None return arr[left] - result = now.replace(microsecond=0) + # Reset microseconds and fold; fold (for ambiguous DST times) will be handled later + result = now.replace(microsecond=0, fold=0) # Match next second if (next_second := _lower_bound(seconds, result.second)) is None: @@ -309,40 +310,58 @@ def find_next_time_expression_time( result = result.replace(hour=next_hour) if result.tzinfo in (None, UTC): + # Using UTC, no DST checking needed return result - if _datetime_ambiguous(result): - # This happens when we're leaving daylight saving time and local - # clocks are rolled back. In this case, we want to trigger - # on both the DST and non-DST time. So when "now" is in the DST - # use the DST-on time, and if not, use the DST-off time. - fold = 1 if now.dst() else 0 - if result.fold != fold: - result = result.replace(fold=fold) - if not _datetime_exists(result): - # This happens when we're entering daylight saving time and local - # clocks are rolled forward, thus there are local times that do - # not exist. In this case, we want to trigger on the next time - # that *does* exist. - # In the worst case, this will run through all the seconds in the - # time shift, but that's max 3600 operations for once per year + # When entering DST and clocks are turned forward. + # There are wall clock times that don't "exist" (an hour is skipped). + + # -> trigger on the next time that 1. matches the pattern and 2. does exist + # for example: + # on 2021.03.28 02:00:00 in CET timezone clocks are turned forward an hour + # with pattern "02:30", don't run on 28 mar (such a wall time does not exist on this day) + # instead run at 02:30 the next day + + # We solve this edge case by just iterating one second until the result exists + # (max. 3600 operations, which should be fine for an edge case that happens once a year) return find_next_time_expression_time( result + dt.timedelta(seconds=1), seconds, minutes, hours ) - # Another edge-case when leaving DST: - # When now is in DST and ambiguous *and* the next trigger time we *should* - # trigger is ambiguous and outside DST, the excepts above won't catch it. - # For example: if triggering on 2:30 and now is 28.10.2018 2:30 (in DST) - # we should trigger next on 28.10.2018 2:30 (out of DST), but our - # algorithm above would produce 29.10.2018 2:30 (out of DST) - if _datetime_ambiguous(now): + now_is_ambiguous = _datetime_ambiguous(now) + result_is_ambiguous = _datetime_ambiguous(result) + + # When leaving DST and clocks are turned backward. + # Then there are wall clock times that are ambiguous i.e. exist with DST and without DST + # The logic above does not take into account if a given pattern matches _twice_ + # in a day. + # Example: on 2021.10.31 02:00:00 in CET timezone clocks are turned backward an hour + + if now_is_ambiguous and result_is_ambiguous: + # `now` and `result` are both ambiguous, so the next match happens + # _within_ the current fold. + + # Examples: + # 1. 2021.10.31 02:00:00+02:00 with pattern 02:30 -> 2021.10.31 02:30:00+02:00 + # 2. 2021.10.31 02:00:00+01:00 with pattern 02:30 -> 2021.10.31 02:30:00+01:00 + return result.replace(fold=now.fold) + + if now_is_ambiguous and now.fold == 0 and not result_is_ambiguous: + # `now` is in the first fold, but result is not ambiguous (meaning it no longer matches + # within the fold). + # -> Check if result matches in the next fold. If so, emit that match + + # Turn back the time by the DST offset, effectively run the algorithm on the first fold + # If it matches on the first fold, that means it will also match on the second one. + + # Example: 2021.10.31 02:45:00+02:00 with pattern 02:30 -> 2021.10.31 02:30:00+01:00 + check_result = find_next_time_expression_time( now + _dst_offset_diff(now), seconds, minutes, hours ) if _datetime_ambiguous(check_result): - return check_result + return check_result.replace(fold=1) return result diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index cf2e5ac13b8..f0b7a2c5d2d 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -3399,9 +3399,19 @@ async def test_periodic_task_entering_dst(hass): dt_util.set_default_time_zone(timezone) specific_runs = [] - now = dt_util.utcnow() + # DST starts early morning March 27th 2022 + yy = 2022 + mm = 3 + dd = 27 + + # There's no 2022-03-27 02:30, the event should not fire until 2022-03-28 02:30 time_that_will_not_match_right_away = datetime( - now.year + 1, 3, 25, 2, 31, 0, tzinfo=timezone + yy, mm, dd, 1, 28, 0, tzinfo=timezone, fold=0 + ) + # Make sure we enter DST during the test + assert ( + time_that_will_not_match_right_away.utcoffset() + != (time_that_will_not_match_right_away + timedelta(hours=2)).utcoffset() ) with patch( @@ -3416,25 +3426,25 @@ async def test_periodic_task_entering_dst(hass): ) async_fire_time_changed( - hass, datetime(now.year + 1, 3, 25, 1, 50, 0, 999999, tzinfo=timezone) + hass, datetime(yy, mm, dd, 1, 50, 0, 999999, tzinfo=timezone) ) await hass.async_block_till_done() assert len(specific_runs) == 0 async_fire_time_changed( - hass, datetime(now.year + 1, 3, 25, 3, 50, 0, 999999, tzinfo=timezone) + hass, datetime(yy, mm, dd, 3, 50, 0, 999999, tzinfo=timezone) ) await hass.async_block_till_done() assert len(specific_runs) == 0 async_fire_time_changed( - hass, datetime(now.year + 1, 3, 26, 1, 50, 0, 999999, tzinfo=timezone) + hass, datetime(yy, mm, dd + 1, 1, 50, 0, 999999, tzinfo=timezone) ) await hass.async_block_till_done() assert len(specific_runs) == 0 async_fire_time_changed( - hass, datetime(now.year + 1, 3, 26, 2, 50, 0, 999999, tzinfo=timezone) + hass, datetime(yy, mm, dd + 1, 2, 50, 0, 999999, tzinfo=timezone) ) await hass.async_block_till_done() assert len(specific_runs) == 1 @@ -3448,10 +3458,19 @@ async def test_periodic_task_leaving_dst(hass): dt_util.set_default_time_zone(timezone) specific_runs = [] - now = dt_util.utcnow() + # DST ends early morning Ocotber 30th 2022 + yy = 2022 + mm = 10 + dd = 30 time_that_will_not_match_right_away = datetime( - now.year + 1, 10, 28, 2, 28, 0, tzinfo=timezone, fold=1 + yy, mm, dd, 2, 28, 0, tzinfo=timezone, fold=0 + ) + + # Make sure we leave DST during the test + assert ( + time_that_will_not_match_right_away.utcoffset() + != time_that_will_not_match_right_away.replace(fold=1).utcoffset() ) with patch( @@ -3465,38 +3484,134 @@ async def test_periodic_task_leaving_dst(hass): second=0, ) + # The task should not fire yet async_fire_time_changed( - hass, datetime(now.year + 1, 10, 28, 2, 5, 0, 999999, tzinfo=timezone, fold=0) + hass, datetime(yy, mm, dd, 2, 28, 0, 999999, tzinfo=timezone, fold=0) ) await hass.async_block_till_done() assert len(specific_runs) == 0 + # The task should fire async_fire_time_changed( - hass, datetime(now.year + 1, 10, 28, 2, 55, 0, 999999, tzinfo=timezone, fold=0) + hass, datetime(yy, mm, dd, 2, 30, 0, 999999, tzinfo=timezone, fold=0) ) await hass.async_block_till_done() assert len(specific_runs) == 1 + # The task should not fire again + async_fire_time_changed( + hass, datetime(yy, mm, dd, 2, 55, 0, 999999, tzinfo=timezone, fold=0) + ) + await hass.async_block_till_done() + assert len(specific_runs) == 1 + + # DST has ended, the task should not fire yet async_fire_time_changed( hass, - datetime(now.year + 2, 10, 28, 2, 45, 0, 999999, tzinfo=timezone, fold=1), + datetime(yy, mm, dd, 2, 15, 0, 999999, tzinfo=timezone, fold=1), + ) + await hass.async_block_till_done() + assert len(specific_runs) == 1 + + # The task should fire + async_fire_time_changed( + hass, + datetime(yy, mm, dd, 2, 45, 0, 999999, tzinfo=timezone, fold=1), ) await hass.async_block_till_done() assert len(specific_runs) == 2 + # The task should not fire again async_fire_time_changed( hass, - datetime(now.year + 2, 10, 28, 2, 55, 0, 999999, tzinfo=timezone, fold=1), + datetime(yy, mm, dd, 2, 55, 0, 999999, tzinfo=timezone, fold=1), ) await hass.async_block_till_done() assert len(specific_runs) == 2 + # The task should fire again the next day async_fire_time_changed( - hass, datetime(now.year + 2, 10, 28, 2, 55, 0, 999999, tzinfo=timezone, fold=1) + hass, datetime(yy, mm, dd + 1, 2, 55, 0, 999999, tzinfo=timezone, fold=1) + ) + await hass.async_block_till_done() + assert len(specific_runs) == 3 + + unsub() + + +async def test_periodic_task_leaving_dst_2(hass): + """Test periodic task behavior when leaving dst.""" + timezone = dt_util.get_time_zone("Europe/Vienna") + dt_util.set_default_time_zone(timezone) + specific_runs = [] + + # DST ends early morning Ocotber 30th 2022 + yy = 2022 + mm = 10 + dd = 30 + + time_that_will_not_match_right_away = datetime( + yy, mm, dd, 2, 28, 0, tzinfo=timezone, fold=0 + ) + # Make sure we leave DST during the test + assert ( + time_that_will_not_match_right_away.utcoffset() + != time_that_will_not_match_right_away.replace(fold=1).utcoffset() + ) + + with patch( + "homeassistant.util.dt.utcnow", return_value=time_that_will_not_match_right_away + ): + unsub = async_track_time_change( + hass, + callback(lambda x: specific_runs.append(x)), + minute=30, + second=0, + ) + + # The task should not fire yet + async_fire_time_changed( + hass, datetime(yy, mm, dd, 2, 28, 0, 999999, tzinfo=timezone, fold=0) + ) + await hass.async_block_till_done() + assert len(specific_runs) == 0 + + # The task should fire + async_fire_time_changed( + hass, datetime(yy, mm, dd, 2, 55, 0, 999999, tzinfo=timezone, fold=0) + ) + await hass.async_block_till_done() + assert len(specific_runs) == 1 + + # DST has ended, the task should not fire yet + async_fire_time_changed( + hass, datetime(yy, mm, dd, 2, 15, 0, 999999, tzinfo=timezone, fold=1) + ) + await hass.async_block_till_done() + assert len(specific_runs) == 1 + + # The task should fire + async_fire_time_changed( + hass, datetime(yy, mm, dd, 2, 45, 0, 999999, tzinfo=timezone, fold=1) ) await hass.async_block_till_done() assert len(specific_runs) == 2 + # The task should not fire again + async_fire_time_changed( + hass, + datetime(yy, mm, dd, 2, 55, 0, 999999, tzinfo=timezone, fold=1), + ) + await hass.async_block_till_done() + assert len(specific_runs) == 2 + + # The task should fire again the next hour + async_fire_time_changed( + hass, datetime(yy, mm, dd, 3, 55, 0, 999999, tzinfo=timezone, fold=0) + ) + await hass.async_block_till_done() + assert len(specific_runs) == 3 + unsub() diff --git a/tests/util/test_dt.py b/tests/util/test_dt.py index 628cb533681..63513c90360 100644 --- a/tests/util/test_dt.py +++ b/tests/util/test_dt.py @@ -224,120 +224,411 @@ def test_find_next_time_expression_time_dst(): tz = dt_util.get_time_zone("Europe/Vienna") dt_util.set_default_time_zone(tz) - def find(dt, hour, minute, second): + def find(dt, hour, minute, second) -> datetime: """Call test_find_next_time_expression_time.""" seconds = dt_util.parse_time_expression(second, 0, 59) minutes = dt_util.parse_time_expression(minute, 0, 59) hours = dt_util.parse_time_expression(hour, 0, 23) - return dt_util.find_next_time_expression_time(dt, seconds, minutes, hours) + local = dt_util.find_next_time_expression_time(dt, seconds, minutes, hours) + return dt_util.as_utc(local) # Entering DST, clocks are rolled forward - assert datetime(2018, 3, 26, 2, 30, 0, tzinfo=tz) == find( + assert dt_util.as_utc(datetime(2018, 3, 26, 2, 30, 0, tzinfo=tz)) == find( datetime(2018, 3, 25, 1, 50, 0, tzinfo=tz), 2, 30, 0 ) - assert datetime(2018, 3, 26, 2, 30, 0, tzinfo=tz) == find( + assert dt_util.as_utc(datetime(2018, 3, 26, 2, 30, 0, tzinfo=tz)) == find( datetime(2018, 3, 25, 3, 50, 0, tzinfo=tz), 2, 30, 0 ) - assert datetime(2018, 3, 26, 2, 30, 0, tzinfo=tz) == find( + assert dt_util.as_utc(datetime(2018, 3, 26, 2, 30, 0, tzinfo=tz)) == find( datetime(2018, 3, 26, 1, 50, 0, tzinfo=tz), 2, 30, 0 ) # Leaving DST, clocks are rolled back - assert datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=0) == find( + assert dt_util.as_utc(datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=0)) == find( datetime(2018, 10, 28, 2, 5, 0, tzinfo=tz, fold=0), 2, 30, 0 ) - assert datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=0) == find( + assert dt_util.as_utc(datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=0)) == find( datetime(2018, 10, 28, 2, 5, 0, tzinfo=tz), 2, 30, 0 ) - assert datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=1) == find( + assert dt_util.as_utc(datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=1)) == find( datetime(2018, 10, 28, 2, 55, 0, tzinfo=tz), 2, 30, 0 ) - assert datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=1) == find( + assert dt_util.as_utc(datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=1)) == find( datetime(2018, 10, 28, 2, 55, 0, tzinfo=tz, fold=0), 2, 30, 0 ) - assert datetime(2018, 10, 28, 4, 30, 0, tzinfo=tz, fold=0) == find( + assert dt_util.as_utc(datetime(2018, 10, 28, 4, 30, 0, tzinfo=tz, fold=0)) == find( datetime(2018, 10, 28, 2, 55, 0, tzinfo=tz, fold=1), 4, 30, 0 ) - assert datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=1) == find( + assert dt_util.as_utc(datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=1)) == find( datetime(2018, 10, 28, 2, 5, 0, tzinfo=tz, fold=1), 2, 30, 0 ) - assert datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=1) == find( + assert dt_util.as_utc(datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=1)) == find( datetime(2018, 10, 28, 2, 55, 0, tzinfo=tz, fold=0), 2, 30, 0 ) +# DST begins on 2021.03.28 2:00, clocks were turned forward 1h; 2:00-3:00 time does not exist +@pytest.mark.parametrize( + "now_dt, expected_dt", + [ + # 00:00 -> 2:30 + ( + datetime(2021, 3, 28, 0, 0, 0), + datetime(2021, 3, 29, 2, 30, 0), + ), + ], +) +def test_find_next_time_expression_entering_dst(now_dt, expected_dt): + """Test entering daylight saving time for find_next_time_expression_time.""" + tz = dt_util.get_time_zone("Europe/Vienna") + dt_util.set_default_time_zone(tz) + # match on 02:30:00 every day + pattern_seconds = dt_util.parse_time_expression(0, 0, 59) + pattern_minutes = dt_util.parse_time_expression(30, 0, 59) + pattern_hours = dt_util.parse_time_expression(2, 0, 59) + + now_dt = now_dt.replace(tzinfo=tz) + expected_dt = expected_dt.replace(tzinfo=tz) + + res_dt = dt_util.find_next_time_expression_time( + now_dt, pattern_seconds, pattern_minutes, pattern_hours + ) + assert dt_util.as_utc(res_dt) == dt_util.as_utc(expected_dt) + + +# DST ends on 2021.10.31 2:00, clocks were turned backward 1h; 2:00-3:00 time is ambiguous +@pytest.mark.parametrize( + "now_dt, expected_dt", + [ + # 00:00 -> 2:30 + ( + datetime(2021, 10, 31, 0, 0, 0), + datetime(2021, 10, 31, 2, 30, 0, fold=0), + ), + # 02:00(0) -> 2:30(0) + ( + datetime(2021, 10, 31, 2, 0, 0, fold=0), + datetime(2021, 10, 31, 2, 30, 0, fold=0), + ), + # 02:15(0) -> 2:30(0) + ( + datetime(2021, 10, 31, 2, 15, 0, fold=0), + datetime(2021, 10, 31, 2, 30, 0, fold=0), + ), + # 02:30:00(0) -> 2:30(1) + ( + datetime(2021, 10, 31, 2, 30, 0, fold=0), + datetime(2021, 10, 31, 2, 30, 0, fold=0), + ), + # 02:30:01(0) -> 2:30(1) + ( + datetime(2021, 10, 31, 2, 30, 1, fold=0), + datetime(2021, 10, 31, 2, 30, 0, fold=1), + ), + # 02:45(0) -> 2:30(1) + ( + datetime(2021, 10, 31, 2, 45, 0, fold=0), + datetime(2021, 10, 31, 2, 30, 0, fold=1), + ), + # 02:00(1) -> 2:30(1) + ( + datetime(2021, 10, 31, 2, 0, 0, fold=1), + datetime(2021, 10, 31, 2, 30, 0, fold=1), + ), + # 02:15(1) -> 2:30(1) + ( + datetime(2021, 10, 31, 2, 15, 0, fold=1), + datetime(2021, 10, 31, 2, 30, 0, fold=1), + ), + # 02:30:00(1) -> 2:30(1) + ( + datetime(2021, 10, 31, 2, 30, 0, fold=1), + datetime(2021, 10, 31, 2, 30, 0, fold=1), + ), + # 02:30:01(1) -> 2:30 next day + ( + datetime(2021, 10, 31, 2, 30, 1, fold=1), + datetime(2021, 11, 1, 2, 30, 0), + ), + # 02:45(1) -> 2:30 next day + ( + datetime(2021, 10, 31, 2, 45, 0, fold=1), + datetime(2021, 11, 1, 2, 30, 0), + ), + # 08:00(1) -> 2:30 next day + ( + datetime(2021, 10, 31, 8, 0, 1), + datetime(2021, 11, 1, 2, 30, 0), + ), + ], +) +def test_find_next_time_expression_exiting_dst(now_dt, expected_dt): + """Test exiting daylight saving time for find_next_time_expression_time.""" + tz = dt_util.get_time_zone("Europe/Vienna") + dt_util.set_default_time_zone(tz) + # match on 02:30:00 every day + pattern_seconds = dt_util.parse_time_expression(0, 0, 59) + pattern_minutes = dt_util.parse_time_expression(30, 0, 59) + pattern_hours = dt_util.parse_time_expression(2, 0, 59) + + now_dt = now_dt.replace(tzinfo=tz) + expected_dt = expected_dt.replace(tzinfo=tz) + + res_dt = dt_util.find_next_time_expression_time( + now_dt, pattern_seconds, pattern_minutes, pattern_hours + ) + assert dt_util.as_utc(res_dt) == dt_util.as_utc(expected_dt) + + def test_find_next_time_expression_time_dst_chicago(): """Test daylight saving time for find_next_time_expression_time.""" tz = dt_util.get_time_zone("America/Chicago") dt_util.set_default_time_zone(tz) - def find(dt, hour, minute, second): + def find(dt, hour, minute, second) -> datetime: """Call test_find_next_time_expression_time.""" seconds = dt_util.parse_time_expression(second, 0, 59) minutes = dt_util.parse_time_expression(minute, 0, 59) hours = dt_util.parse_time_expression(hour, 0, 23) - return dt_util.find_next_time_expression_time(dt, seconds, minutes, hours) + local = dt_util.find_next_time_expression_time(dt, seconds, minutes, hours) + return dt_util.as_utc(local) # Entering DST, clocks are rolled forward - assert datetime(2021, 3, 15, 2, 30, 0, tzinfo=tz) == find( + assert dt_util.as_utc(datetime(2021, 3, 15, 2, 30, 0, tzinfo=tz)) == find( datetime(2021, 3, 14, 1, 50, 0, tzinfo=tz), 2, 30, 0 ) - assert datetime(2021, 3, 15, 2, 30, 0, tzinfo=tz) == find( + assert dt_util.as_utc(datetime(2021, 3, 15, 2, 30, 0, tzinfo=tz)) == find( datetime(2021, 3, 14, 3, 50, 0, tzinfo=tz), 2, 30, 0 ) - assert datetime(2021, 3, 15, 2, 30, 0, tzinfo=tz) == find( + assert dt_util.as_utc(datetime(2021, 3, 15, 2, 30, 0, tzinfo=tz)) == find( datetime(2021, 3, 14, 1, 50, 0, tzinfo=tz), 2, 30, 0 ) - assert datetime(2021, 3, 14, 3, 30, 0, tzinfo=tz) == find( + assert dt_util.as_utc(datetime(2021, 3, 14, 3, 30, 0, tzinfo=tz)) == find( datetime(2021, 3, 14, 1, 50, 0, tzinfo=tz), 3, 30, 0 ) # Leaving DST, clocks are rolled back - assert datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=0) == find( + assert dt_util.as_utc(datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=0)) == find( datetime(2021, 11, 7, 2, 5, 0, tzinfo=tz, fold=0), 2, 30, 0 ) - assert datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz) == find( + assert dt_util.as_utc(datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz)) == find( datetime(2021, 11, 7, 2, 5, 0, tzinfo=tz), 2, 30, 0 ) - assert datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=0) == find( + assert dt_util.as_utc(datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=0)) == find( datetime(2021, 11, 7, 2, 5, 0, tzinfo=tz), 2, 30, 0 ) - assert datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=1) == find( + assert dt_util.as_utc(datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=1)) == find( datetime(2021, 11, 7, 2, 10, 0, tzinfo=tz), 2, 30, 0 ) - assert datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=1) == find( + assert dt_util.as_utc(datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=1)) == find( datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=0), 2, 30, 0 ) - assert datetime(2021, 11, 8, 2, 30, 0, tzinfo=tz, fold=1) == find( + assert dt_util.as_utc(datetime(2021, 11, 8, 2, 30, 0, tzinfo=tz, fold=1)) == find( datetime(2021, 11, 7, 2, 55, 0, tzinfo=tz, fold=0), 2, 30, 0 ) - assert datetime(2021, 11, 7, 4, 30, 0, tzinfo=tz, fold=0) == find( + assert dt_util.as_utc(datetime(2021, 11, 7, 4, 30, 0, tzinfo=tz, fold=0)) == find( datetime(2021, 11, 7, 2, 55, 0, tzinfo=tz, fold=1), 4, 30, 0 ) - assert datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=1) == find( + assert dt_util.as_utc(datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=1)) == find( datetime(2021, 11, 7, 2, 5, 0, tzinfo=tz, fold=1), 2, 30, 0 ) - assert datetime(2021, 11, 8, 2, 30, 0, tzinfo=tz) == find( + assert dt_util.as_utc(datetime(2021, 11, 8, 2, 30, 0, tzinfo=tz)) == find( datetime(2021, 11, 7, 2, 55, 0, tzinfo=tz, fold=0), 2, 30, 0 ) + + +def _get_matches(hours, minutes, seconds): + matching_hours = dt_util.parse_time_expression(hours, 0, 23) + matching_minutes = dt_util.parse_time_expression(minutes, 0, 59) + matching_seconds = dt_util.parse_time_expression(seconds, 0, 59) + return matching_hours, matching_minutes, matching_seconds + + +def test_find_next_time_expression_day_before_dst_change_the_same_time(): + """Test the day before DST to establish behavior without DST.""" + tz = dt_util.get_time_zone("America/Chicago") + dt_util.set_default_time_zone(tz) + + # Not in DST yet + hour_minute_second = (12, 30, 1) + test_time = datetime(2021, 10, 7, *hour_minute_second, tzinfo=tz, fold=0) + matching_hours, matching_minutes, matching_seconds = _get_matches( + *hour_minute_second + ) + next_time = dt_util.find_next_time_expression_time( + test_time, matching_seconds, matching_minutes, matching_hours + ) + assert next_time == datetime(2021, 10, 7, *hour_minute_second, tzinfo=tz, fold=0) + assert next_time.fold == 0 + assert dt_util.as_utc(next_time) == datetime( + 2021, 10, 7, 17, 30, 1, tzinfo=dt_util.UTC + ) + + +def test_find_next_time_expression_time_leave_dst_chicago_before_the_fold_30_s(): + """Test leaving daylight saving time for find_next_time_expression_time 30s into the future.""" + tz = dt_util.get_time_zone("America/Chicago") + dt_util.set_default_time_zone(tz) + + # Leaving DST, clocks are rolled back + + # Move ahead 30 seconds not folded yet + hour_minute_second = (1, 30, 31) + test_time = datetime(2021, 11, 7, 1, 30, 1, tzinfo=tz, fold=0) + matching_hours, matching_minutes, matching_seconds = _get_matches( + *hour_minute_second + ) + next_time = dt_util.find_next_time_expression_time( + test_time, matching_seconds, matching_minutes, matching_hours + ) + assert next_time == datetime(2021, 11, 7, 1, 30, 31, tzinfo=tz, fold=0) + assert dt_util.as_utc(next_time) == datetime( + 2021, 11, 7, 6, 30, 31, tzinfo=dt_util.UTC + ) + assert next_time.fold == 0 + + +def test_find_next_time_expression_time_leave_dst_chicago_before_the_fold_same_time(): + """Test leaving daylight saving time for find_next_time_expression_time with the same time.""" + tz = dt_util.get_time_zone("America/Chicago") + dt_util.set_default_time_zone(tz) + + # Leaving DST, clocks are rolled back + + # Move to the same time not folded yet + hour_minute_second = (0, 30, 1) + test_time = datetime(2021, 11, 7, *hour_minute_second, tzinfo=tz, fold=0) + matching_hours, matching_minutes, matching_seconds = _get_matches( + *hour_minute_second + ) + next_time = dt_util.find_next_time_expression_time( + test_time, matching_seconds, matching_minutes, matching_hours + ) + assert next_time == datetime(2021, 11, 7, *hour_minute_second, tzinfo=tz, fold=0) + assert dt_util.as_utc(next_time) == datetime( + 2021, 11, 7, 5, 30, 1, tzinfo=dt_util.UTC + ) + assert next_time.fold == 0 + + +def test_find_next_time_expression_time_leave_dst_chicago_into_the_fold_same_time(): + """Test leaving daylight saving time for find_next_time_expression_time.""" + tz = dt_util.get_time_zone("America/Chicago") + dt_util.set_default_time_zone(tz) + + # Leaving DST, clocks are rolled back + + # Find the same time inside the fold + hour_minute_second = (1, 30, 1) + test_time = datetime(2021, 11, 7, *hour_minute_second, tzinfo=tz, fold=0) + matching_hours, matching_minutes, matching_seconds = _get_matches( + *hour_minute_second + ) + + next_time = dt_util.find_next_time_expression_time( + test_time, matching_seconds, matching_minutes, matching_hours + ) + assert next_time == datetime(2021, 11, 7, *hour_minute_second, tzinfo=tz, fold=1) + assert next_time.fold == 0 + assert dt_util.as_utc(next_time) == datetime( + 2021, 11, 7, 6, 30, 1, tzinfo=dt_util.UTC + ) + + +def test_find_next_time_expression_time_leave_dst_chicago_into_the_fold_ahead_1_hour_10_min(): + """Test leaving daylight saving time for find_next_time_expression_time.""" + tz = dt_util.get_time_zone("America/Chicago") + dt_util.set_default_time_zone(tz) + + # Leaving DST, clocks are rolled back + + # Find 1h 10m after into the fold + # Start at 01:30:01 fold=0 + # Reach to 01:20:01 fold=1 + hour_minute_second = (1, 20, 1) + test_time = datetime(2021, 11, 7, 1, 30, 1, tzinfo=tz, fold=0) + matching_hours, matching_minutes, matching_seconds = _get_matches( + *hour_minute_second + ) + + next_time = dt_util.find_next_time_expression_time( + test_time, matching_seconds, matching_minutes, matching_hours + ) + assert next_time == datetime(2021, 11, 7, *hour_minute_second, tzinfo=tz, fold=1) + assert next_time.fold == 1 # time is ambiguous + assert dt_util.as_utc(next_time) == datetime( + 2021, 11, 7, 7, 20, 1, tzinfo=dt_util.UTC + ) + + +def test_find_next_time_expression_time_leave_dst_chicago_inside_the_fold_ahead_10_min(): + """Test leaving daylight saving time for find_next_time_expression_time.""" + tz = dt_util.get_time_zone("America/Chicago") + dt_util.set_default_time_zone(tz) + + # Leaving DST, clocks are rolled back + + # Find 10m later while we are in the fold + # Start at 01:30:01 fold=0 + # Reach to 01:40:01 fold=1 + hour_minute_second = (1, 40, 1) + test_time = datetime(2021, 11, 7, 1, 30, 1, tzinfo=tz, fold=1) + matching_hours, matching_minutes, matching_seconds = _get_matches( + *hour_minute_second + ) + + next_time = dt_util.find_next_time_expression_time( + test_time, matching_seconds, matching_minutes, matching_hours + ) + assert next_time == datetime(2021, 11, 7, *hour_minute_second, tzinfo=tz, fold=1) + assert next_time.fold == 1 # time is ambiguous + assert dt_util.as_utc(next_time) == datetime( + 2021, 11, 7, 7, 40, 1, tzinfo=dt_util.UTC + ) + + +def test_find_next_time_expression_time_leave_dst_chicago_past_the_fold_ahead_2_hour_10_min(): + """Test leaving daylight saving time for find_next_time_expression_time.""" + tz = dt_util.get_time_zone("America/Chicago") + dt_util.set_default_time_zone(tz) + + # Leaving DST, clocks are rolled back + + # Find 1h 10m after into the fold + # Start at 01:30:01 fold=0 + # Reach to 02:20:01 past the fold + hour_minute_second = (2, 20, 1) + test_time = datetime(2021, 11, 7, 1, 30, 1, tzinfo=tz, fold=0) + matching_hours, matching_minutes, matching_seconds = _get_matches( + *hour_minute_second + ) + + next_time = dt_util.find_next_time_expression_time( + test_time, matching_seconds, matching_minutes, matching_hours + ) + assert next_time == datetime(2021, 11, 7, *hour_minute_second, tzinfo=tz, fold=1) + assert next_time.fold == 0 # Time is no longer ambiguous + assert dt_util.as_utc(next_time) == datetime( + 2021, 11, 7, 8, 20, 1, tzinfo=dt_util.UTC + ) From 632164f28395af2d13c883559ce00c6f6abc5e6e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 1 Nov 2021 10:56:45 -0700 Subject: [PATCH 080/174] Bumped version to 2021.11.0b4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 7edfdedd1f1..03240bacaf7 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 11 -PATCH_VERSION: Final = "0b3" +PATCH_VERSION: Final = "0b4" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From 34953c4c083412dd4a0c709bbc6e69a6ec1b1680 Mon Sep 17 00:00:00 2001 From: "Peter A. Bigot" Date: Tue, 2 Nov 2021 12:48:29 -0500 Subject: [PATCH 081/174] Fix color temp selection when brightness changed in Tuya light (#58341) Co-authored-by: Franck Nijhof Co-authored-by: Franck Nijhof --- homeassistant/components/tuya/light.py | 47 +++++++++++++------------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index dbae8e2e57f..75442159dea 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -443,7 +443,29 @@ class TuyaLightEntity(TuyaEntity, LightEntity): """Turn on or control the light.""" commands = [{"code": self.entity_description.key, "value": True}] - if self._color_data_type and ( + if self._color_temp_type and ATTR_COLOR_TEMP in kwargs: + if color_mode_dpcode := self.entity_description.color_mode: + commands += [ + { + "code": color_mode_dpcode, + "value": WorkMode.WHITE, + }, + ] + + commands += [ + { + "code": self._color_temp_dpcode, + "value": round( + self._color_temp_type.remap_value_from( + kwargs[ATTR_COLOR_TEMP], + self.min_mireds, + self.max_mireds, + reverse=True, + ) + ), + }, + ] + elif self._color_data_type and ( ATTR_HS_COLOR in kwargs or (ATTR_BRIGHTNESS in kwargs and self.color_mode == COLOR_MODE_HS) ): @@ -486,29 +508,6 @@ class TuyaLightEntity(TuyaEntity, LightEntity): }, ] - elif ATTR_COLOR_TEMP in kwargs and self._color_temp_type: - if color_mode_dpcode := self.entity_description.color_mode: - commands += [ - { - "code": color_mode_dpcode, - "value": WorkMode.WHITE, - }, - ] - - commands += [ - { - "code": self._color_temp_dpcode, - "value": round( - self._color_temp_type.remap_value_from( - kwargs[ATTR_COLOR_TEMP], - self.min_mireds, - self.max_mireds, - reverse=True, - ) - ), - }, - ] - if ( ATTR_BRIGHTNESS in kwargs and self.color_mode != COLOR_MODE_HS From c97160bf971944b996bf1b97e108608a3c1123ce Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 2 Nov 2021 18:21:08 +0100 Subject: [PATCH 082/174] Fix incorrect entity category in Advantage Air (#58754) --- homeassistant/components/advantage_air/binary_sensor.py | 4 ++-- homeassistant/components/advantage_air/sensor.py | 9 ++------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/advantage_air/binary_sensor.py b/homeassistant/components/advantage_air/binary_sensor.py index a7d7308d78c..eec0ce7dfa5 100644 --- a/homeassistant/components/advantage_air/binary_sensor.py +++ b/homeassistant/components/advantage_air/binary_sensor.py @@ -5,7 +5,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_PROBLEM, BinarySensorEntity, ) -from homeassistant.const import ENTITY_CATEGORY_CONFIG, ENTITY_CATEGORY_DIAGNOSTIC +from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN from .entity import AdvantageAirEntity @@ -74,7 +74,7 @@ class AdvantageAirZoneMyZone(AdvantageAirEntity, BinarySensorEntity): """Advantage Air Zone MyZone.""" _attr_entity_registry_enabled_default = False - _attr_entity_category = ENTITY_CATEGORY_CONFIG + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC def __init__(self, instance, ac_key, zone_key): """Initialize an Advantage Air Zone MyZone.""" diff --git a/homeassistant/components/advantage_air/sensor.py b/homeassistant/components/advantage_air/sensor.py index ed2e3b78156..d879693fdb5 100644 --- a/homeassistant/components/advantage_air/sensor.py +++ b/homeassistant/components/advantage_air/sensor.py @@ -6,12 +6,7 @@ from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, SensorEntity, ) -from homeassistant.const import ( - ENTITY_CATEGORY_CONFIG, - ENTITY_CATEGORY_DIAGNOSTIC, - PERCENTAGE, - TEMP_CELSIUS, -) +from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE, TEMP_CELSIUS from homeassistant.helpers import config_validation as cv, entity_platform from .const import ADVANTAGE_AIR_STATE_OPEN, DOMAIN as ADVANTAGE_AIR_DOMAIN @@ -55,7 +50,7 @@ class AdvantageAirTimeTo(AdvantageAirEntity, SensorEntity): """Representation of Advantage Air timer control.""" _attr_native_unit_of_measurement = ADVANTAGE_AIR_SET_COUNTDOWN_UNIT - _attr_entity_category = ENTITY_CATEGORY_CONFIG + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC def __init__(self, instance, ac_key, action): """Initialize the Advantage Air timer control.""" From 5e09685700a2be4cc40e8ae32b4a5a78734e1fe2 Mon Sep 17 00:00:00 2001 From: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com> Date: Tue, 2 Nov 2021 09:54:28 +0100 Subject: [PATCH 083/174] Add ROCKROBO_S6_PURE to supported vacuums for xiaomi_miio (#58901) --- homeassistant/components/xiaomi_miio/const.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index 69c279df493..c140ad526e2 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -200,6 +200,7 @@ MODELS_LIGHT = ( ROCKROBO_S4 = "roborock.vacuum.s4" ROCKROBO_S4_MAX = "roborock.vacuum.a19" ROCKROBO_S5_MAX = "roborock.vacuum.s5e" +ROCKROBO_S6_PURE = "roborock.vacuum.a08" ROCKROBO_E2 = "roborock.vacuum.e2" MODELS_VACUUM = [ ROCKROBO_V1, @@ -210,6 +211,7 @@ MODELS_VACUUM = [ ROCKROBO_S5_MAX, ROCKROBO_S6, ROCKROBO_S6_MAXV, + ROCKROBO_S6_PURE, ROCKROBO_S7, ] MODELS_VACUUM_WITH_MOP = [ @@ -218,6 +220,7 @@ MODELS_VACUUM_WITH_MOP = [ ROCKROBO_S5_MAX, ROCKROBO_S6, ROCKROBO_S6_MAXV, + ROCKROBO_S6_PURE, ROCKROBO_S7, ] From 26e925d88524f2bb37ac59565fdaca59d77515d0 Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Tue, 2 Nov 2021 04:31:30 -0400 Subject: [PATCH 084/174] Bump pyinsteon to 1.0.13 (#58908) --- homeassistant/components/insteon/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json index f5f9d57d8a8..c17f441a159 100644 --- a/homeassistant/components/insteon/manifest.json +++ b/homeassistant/components/insteon/manifest.json @@ -3,7 +3,7 @@ "name": "Insteon", "documentation": "https://www.home-assistant.io/integrations/insteon", "requirements": [ - "pyinsteon==1.0.12" + "pyinsteon==1.0.13" ], "codeowners": [ "@teharris1" diff --git a/requirements_all.txt b/requirements_all.txt index e23c6a5b7d2..fef79aa5ccc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1535,7 +1535,7 @@ pyialarm==1.9.0 pyicloud==0.10.2 # homeassistant.components.insteon -pyinsteon==1.0.12 +pyinsteon==1.0.13 # homeassistant.components.intesishome pyintesishome==1.7.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f5743da2af7..129e50565ad 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -914,7 +914,7 @@ pyialarm==1.9.0 pyicloud==0.10.2 # homeassistant.components.insteon -pyinsteon==1.0.12 +pyinsteon==1.0.13 # homeassistant.components.ipma pyipma==2.0.5 From 53cc9f35b96b0544b8fb94055cf04949dce8c18f Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 2 Nov 2021 09:39:23 +0100 Subject: [PATCH 085/174] Add `configuration_url` to Airly integration (#58911) --- homeassistant/components/airly/const.py | 1 + homeassistant/components/airly/sensor.py | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/homeassistant/components/airly/const.py b/homeassistant/components/airly/const.py index c583a56c22b..801bca58412 100644 --- a/homeassistant/components/airly/const.py +++ b/homeassistant/components/airly/const.py @@ -32,3 +32,4 @@ MANUFACTURER: Final = "Airly sp. z o.o." MAX_UPDATE_INTERVAL: Final = 90 MIN_UPDATE_INTERVAL: Final = 5 NO_AIRLY_SENSORS: Final = "There are no Airly sensors in this area yet." +URL = "https://airly.org/map/#{latitude},{longitude}" diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py index a331e99497e..76b4e7d9d48 100644 --- a/homeassistant/components/airly/sensor.py +++ b/homeassistant/components/airly/sensor.py @@ -55,6 +55,7 @@ from .const import ( MANUFACTURER, SUFFIX_LIMIT, SUFFIX_PERCENT, + URL, ) PARALLEL_UPDATES = 1 @@ -157,6 +158,9 @@ class AirlySensor(CoordinatorEntity, SensorEntity): identifiers={(DOMAIN, f"{coordinator.latitude}-{coordinator.longitude}")}, manufacturer=MANUFACTURER, name=DEFAULT_NAME, + configuration_url=URL.format( + latitude=coordinator.latitude, longitude=coordinator.longitude + ), ) self._attr_name = f"{name} {description.name}" self._attr_unique_id = ( From 6cd256f26b195357f48e896868089c86b5901a8d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 1 Nov 2021 18:11:17 -0500 Subject: [PATCH 086/174] Fix recursive limit in find_next_time_expression_time (#58914) * Fix recursive limit in find_next_time_expression_time * Add test case * Update test_event.py Co-authored-by: Erik Montnemery --- homeassistant/util/dt.py | 179 ++++++++++++++++++------------------ tests/helpers/test_event.py | 66 +++++++++++++ 2 files changed, 156 insertions(+), 89 deletions(-) diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index 592b47ab6b1..39f8a63e53f 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -245,6 +245,16 @@ def _dst_offset_diff(dattim: dt.datetime) -> dt.timedelta: return (dattim + delta).utcoffset() - (dattim - delta).utcoffset() # type: ignore[operator] +def _lower_bound(arr: list[int], cmp: int) -> int | None: + """Return the first value in arr greater or equal to cmp. + + Return None if no such value exists. + """ + if (left := bisect.bisect_left(arr, cmp)) == len(arr): + return None + return arr[left] + + def find_next_time_expression_time( now: dt.datetime, # pylint: disable=redefined-outer-name seconds: list[int], @@ -263,108 +273,99 @@ def find_next_time_expression_time( if not seconds or not minutes or not hours: raise ValueError("Cannot find a next time: Time expression never matches!") - def _lower_bound(arr: list[int], cmp: int) -> int | None: - """Return the first value in arr greater or equal to cmp. + while True: + # Reset microseconds and fold; fold (for ambiguous DST times) will be handled later + result = now.replace(microsecond=0, fold=0) - Return None if no such value exists. - """ - if (left := bisect.bisect_left(arr, cmp)) == len(arr): - return None - return arr[left] + # Match next second + if (next_second := _lower_bound(seconds, result.second)) is None: + # No second to match in this minute. Roll-over to next minute. + next_second = seconds[0] + result += dt.timedelta(minutes=1) - # Reset microseconds and fold; fold (for ambiguous DST times) will be handled later - result = now.replace(microsecond=0, fold=0) + result = result.replace(second=next_second) - # Match next second - if (next_second := _lower_bound(seconds, result.second)) is None: - # No second to match in this minute. Roll-over to next minute. - next_second = seconds[0] - result += dt.timedelta(minutes=1) + # Match next minute + next_minute = _lower_bound(minutes, result.minute) + if next_minute != result.minute: + # We're in the next minute. Seconds needs to be reset. + result = result.replace(second=seconds[0]) - result = result.replace(second=next_second) + if next_minute is None: + # No minute to match in this hour. Roll-over to next hour. + next_minute = minutes[0] + result += dt.timedelta(hours=1) - # Match next minute - next_minute = _lower_bound(minutes, result.minute) - if next_minute != result.minute: - # We're in the next minute. Seconds needs to be reset. - result = result.replace(second=seconds[0]) + result = result.replace(minute=next_minute) - if next_minute is None: - # No minute to match in this hour. Roll-over to next hour. - next_minute = minutes[0] - result += dt.timedelta(hours=1) + # Match next hour + next_hour = _lower_bound(hours, result.hour) + if next_hour != result.hour: + # We're in the next hour. Seconds+minutes needs to be reset. + result = result.replace(second=seconds[0], minute=minutes[0]) - result = result.replace(minute=next_minute) + if next_hour is None: + # No minute to match in this day. Roll-over to next day. + next_hour = hours[0] + result += dt.timedelta(days=1) - # Match next hour - next_hour = _lower_bound(hours, result.hour) - if next_hour != result.hour: - # We're in the next hour. Seconds+minutes needs to be reset. - result = result.replace(second=seconds[0], minute=minutes[0]) + result = result.replace(hour=next_hour) - if next_hour is None: - # No minute to match in this day. Roll-over to next day. - next_hour = hours[0] - result += dt.timedelta(days=1) + if result.tzinfo in (None, UTC): + # Using UTC, no DST checking needed + return result - result = result.replace(hour=next_hour) + if not _datetime_exists(result): + # When entering DST and clocks are turned forward. + # There are wall clock times that don't "exist" (an hour is skipped). + + # -> trigger on the next time that 1. matches the pattern and 2. does exist + # for example: + # on 2021.03.28 02:00:00 in CET timezone clocks are turned forward an hour + # with pattern "02:30", don't run on 28 mar (such a wall time does not exist on this day) + # instead run at 02:30 the next day + + # We solve this edge case by just iterating one second until the result exists + # (max. 3600 operations, which should be fine for an edge case that happens once a year) + now += dt.timedelta(seconds=1) + continue + + now_is_ambiguous = _datetime_ambiguous(now) + result_is_ambiguous = _datetime_ambiguous(result) + + # When leaving DST and clocks are turned backward. + # Then there are wall clock times that are ambiguous i.e. exist with DST and without DST + # The logic above does not take into account if a given pattern matches _twice_ + # in a day. + # Example: on 2021.10.31 02:00:00 in CET timezone clocks are turned backward an hour + + if now_is_ambiguous and result_is_ambiguous: + # `now` and `result` are both ambiguous, so the next match happens + # _within_ the current fold. + + # Examples: + # 1. 2021.10.31 02:00:00+02:00 with pattern 02:30 -> 2021.10.31 02:30:00+02:00 + # 2. 2021.10.31 02:00:00+01:00 with pattern 02:30 -> 2021.10.31 02:30:00+01:00 + return result.replace(fold=now.fold) + + if now_is_ambiguous and now.fold == 0 and not result_is_ambiguous: + # `now` is in the first fold, but result is not ambiguous (meaning it no longer matches + # within the fold). + # -> Check if result matches in the next fold. If so, emit that match + + # Turn back the time by the DST offset, effectively run the algorithm on the first fold + # If it matches on the first fold, that means it will also match on the second one. + + # Example: 2021.10.31 02:45:00+02:00 with pattern 02:30 -> 2021.10.31 02:30:00+01:00 + + check_result = find_next_time_expression_time( + now + _dst_offset_diff(now), seconds, minutes, hours + ) + if _datetime_ambiguous(check_result): + return check_result.replace(fold=1) - if result.tzinfo in (None, UTC): - # Using UTC, no DST checking needed return result - if not _datetime_exists(result): - # When entering DST and clocks are turned forward. - # There are wall clock times that don't "exist" (an hour is skipped). - - # -> trigger on the next time that 1. matches the pattern and 2. does exist - # for example: - # on 2021.03.28 02:00:00 in CET timezone clocks are turned forward an hour - # with pattern "02:30", don't run on 28 mar (such a wall time does not exist on this day) - # instead run at 02:30 the next day - - # We solve this edge case by just iterating one second until the result exists - # (max. 3600 operations, which should be fine for an edge case that happens once a year) - return find_next_time_expression_time( - result + dt.timedelta(seconds=1), seconds, minutes, hours - ) - - now_is_ambiguous = _datetime_ambiguous(now) - result_is_ambiguous = _datetime_ambiguous(result) - - # When leaving DST and clocks are turned backward. - # Then there are wall clock times that are ambiguous i.e. exist with DST and without DST - # The logic above does not take into account if a given pattern matches _twice_ - # in a day. - # Example: on 2021.10.31 02:00:00 in CET timezone clocks are turned backward an hour - - if now_is_ambiguous and result_is_ambiguous: - # `now` and `result` are both ambiguous, so the next match happens - # _within_ the current fold. - - # Examples: - # 1. 2021.10.31 02:00:00+02:00 with pattern 02:30 -> 2021.10.31 02:30:00+02:00 - # 2. 2021.10.31 02:00:00+01:00 with pattern 02:30 -> 2021.10.31 02:30:00+01:00 - return result.replace(fold=now.fold) - - if now_is_ambiguous and now.fold == 0 and not result_is_ambiguous: - # `now` is in the first fold, but result is not ambiguous (meaning it no longer matches - # within the fold). - # -> Check if result matches in the next fold. If so, emit that match - - # Turn back the time by the DST offset, effectively run the algorithm on the first fold - # If it matches on the first fold, that means it will also match on the second one. - - # Example: 2021.10.31 02:45:00+02:00 with pattern 02:30 -> 2021.10.31 02:30:00+01:00 - - check_result = find_next_time_expression_time( - now + _dst_offset_diff(now), seconds, minutes, hours - ) - if _datetime_ambiguous(check_result): - return check_result.replace(fold=1) - - return result - def _datetime_exists(dattim: dt.datetime) -> bool: """Check if a datetime exists.""" diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index f0b7a2c5d2d..9d48b0c0ada 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -3452,6 +3452,72 @@ async def test_periodic_task_entering_dst(hass): unsub() +async def test_periodic_task_entering_dst_2(hass): + """Test periodic task behavior when entering dst. + + This tests a task firing every second in the range 0..58 (not *:*:59) + """ + timezone = dt_util.get_time_zone("Europe/Vienna") + dt_util.set_default_time_zone(timezone) + specific_runs = [] + + # DST starts early morning March 27th 2022 + yy = 2022 + mm = 3 + dd = 27 + + # There's no 2022-03-27 02:00:00, the event should not fire until 2022-03-28 03:00:00 + time_that_will_not_match_right_away = datetime( + yy, mm, dd, 1, 59, 59, tzinfo=timezone, fold=0 + ) + # Make sure we enter DST during the test + assert ( + time_that_will_not_match_right_away.utcoffset() + != (time_that_will_not_match_right_away + timedelta(hours=2)).utcoffset() + ) + + with patch( + "homeassistant.util.dt.utcnow", return_value=time_that_will_not_match_right_away + ): + unsub = async_track_time_change( + hass, + callback(lambda x: specific_runs.append(x)), + second=list(range(59)), + ) + + async_fire_time_changed( + hass, datetime(yy, mm, dd, 1, 59, 59, 999999, tzinfo=timezone) + ) + await hass.async_block_till_done() + assert len(specific_runs) == 0 + + async_fire_time_changed( + hass, datetime(yy, mm, dd, 3, 0, 0, 999999, tzinfo=timezone) + ) + await hass.async_block_till_done() + assert len(specific_runs) == 1 + + async_fire_time_changed( + hass, datetime(yy, mm, dd, 3, 0, 1, 999999, tzinfo=timezone) + ) + await hass.async_block_till_done() + assert len(specific_runs) == 2 + + async_fire_time_changed( + hass, datetime(yy, mm, dd + 1, 1, 59, 59, 999999, tzinfo=timezone) + ) + await hass.async_block_till_done() + assert len(specific_runs) == 3 + + async_fire_time_changed( + hass, datetime(yy, mm, dd + 1, 2, 0, 0, 999999, tzinfo=timezone) + ) + await hass.async_block_till_done() + assert len(specific_runs) == 4 + + unsub() + + async def test_periodic_task_leaving_dst(hass): """Test periodic task behavior when leaving dst.""" timezone = dt_util.get_time_zone("Europe/Vienna") From d4ba9a137cd45954c1a6e318f379a60722437927 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Tue, 2 Nov 2021 21:32:02 +0800 Subject: [PATCH 087/174] Add libav.mpegts to logging filter (#58937) --- homeassistant/components/stream/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index cef70a2e809..77e946770e6 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -120,6 +120,7 @@ def filter_libav_logging() -> None: "libav.rtsp", "libav.tcp", "libav.tls", + "libav.mpegts", "libav.NULL", ): logging.getLogger(logging_namespace).addFilter(libav_filter) From e4143142bf6aff26467b3d522b4e58423e90d050 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 2 Nov 2021 16:56:38 +0100 Subject: [PATCH 088/174] Revert "Add offset support to time trigger" (#58947) --- .../components/homeassistant/triggers/time.py | 51 ++--- .../homeassistant/triggers/test_time.py | 184 ------------------ 2 files changed, 11 insertions(+), 224 deletions(-) diff --git a/homeassistant/components/homeassistant/triggers/time.py b/homeassistant/components/homeassistant/triggers/time.py index be7ded37dc2..90780489d7b 100644 --- a/homeassistant/components/homeassistant/triggers/time.py +++ b/homeassistant/components/homeassistant/triggers/time.py @@ -1,5 +1,5 @@ """Offer time listening automation rules.""" -from datetime import datetime, timedelta +from datetime import datetime from functools import partial import voluptuous as vol @@ -8,8 +8,6 @@ from homeassistant.components import sensor from homeassistant.const import ( ATTR_DEVICE_CLASS, CONF_AT, - CONF_ENTITY_ID, - CONF_OFFSET, CONF_PLATFORM, STATE_UNAVAILABLE, STATE_UNKNOWN, @@ -25,21 +23,9 @@ import homeassistant.util.dt as dt_util # mypy: allow-untyped-defs, no-check-untyped-defs -_TIME_TRIGGER_ENTITY_REFERENCE = vol.All( - str, cv.entity_domain(["input_datetime", "sensor"]) -) - -_TIME_TRIGGER_WITH_OFFSET_SCHEMA = vol.Schema( - { - vol.Required(CONF_ENTITY_ID): _TIME_TRIGGER_ENTITY_REFERENCE, - vol.Required(CONF_OFFSET): cv.time_period, - } -) - _TIME_TRIGGER_SCHEMA = vol.Any( cv.time, - _TIME_TRIGGER_ENTITY_REFERENCE, - _TIME_TRIGGER_WITH_OFFSET_SCHEMA, + vol.All(str, cv.entity_domain(["input_datetime", "sensor"])), msg="Expected HH:MM, HH:MM:SS or Entity ID with domain 'input_datetime' or 'sensor'", ) @@ -57,7 +43,6 @@ async def async_attach_trigger(hass, config, action, automation_info): entities = {} removes = [] job = HassJob(action) - offsets = {} @callback def time_automation_listener(description, now, *, entity_id=None): @@ -92,8 +77,6 @@ async def async_attach_trigger(hass, config, action, automation_info): if not new_state: return - offset = offsets[entity_id] if entity_id in offsets else timedelta(0) - # Check state of entity. If valid, set up a listener. if new_state.domain == "input_datetime": if has_date := new_state.attributes["has_date"]: @@ -110,17 +93,14 @@ async def async_attach_trigger(hass, config, action, automation_info): if has_date: # If input_datetime has date, then track point in time. - trigger_dt = ( - datetime( - year, - month, - day, - hour, - minute, - second, - tzinfo=dt_util.DEFAULT_TIME_ZONE, - ) - + offset + trigger_dt = datetime( + year, + month, + day, + hour, + minute, + second, + tzinfo=dt_util.DEFAULT_TIME_ZONE, ) # Only set up listener if time is now or in the future. if trigger_dt >= dt_util.now(): @@ -152,7 +132,7 @@ async def async_attach_trigger(hass, config, action, automation_info): == sensor.DEVICE_CLASS_TIMESTAMP and new_state.state not in (STATE_UNAVAILABLE, STATE_UNKNOWN) ): - trigger_dt = dt_util.parse_datetime(new_state.state) + offset + trigger_dt = dt_util.parse_datetime(new_state.state) if trigger_dt is not None and trigger_dt > dt_util.utcnow(): remove = async_track_point_in_time( @@ -176,15 +156,6 @@ async def async_attach_trigger(hass, config, action, automation_info): # entity to_track.append(at_time) update_entity_trigger(at_time, new_state=hass.states.get(at_time)) - elif isinstance(at_time, dict) and CONF_OFFSET in at_time: - # entity with offset - entity_id = at_time.get(CONF_ENTITY_ID) - to_track.append(entity_id) - offsets[entity_id] = at_time.get(CONF_OFFSET) - update_entity_trigger( - entity_id, - new_state=hass.states.get(entity_id), - ) else: # datetime.time removes.append( diff --git a/tests/components/homeassistant/triggers/test_time.py b/tests/components/homeassistant/triggers/test_time.py index 7961ce25026..499fcf8611e 100644 --- a/tests/components/homeassistant/triggers/test_time.py +++ b/tests/components/homeassistant/triggers/test_time.py @@ -150,96 +150,6 @@ async def test_if_fires_using_at_input_datetime(hass, calls, has_date, has_time) ) -@pytest.mark.parametrize( - "offset,delta", - [ - ("00:00:10", timedelta(seconds=10)), - ("-00:00:10", timedelta(seconds=-10)), - ({"minutes": 5}, timedelta(minutes=5)), - ], -) -async def test_if_fires_using_at_input_datetime_with_offset(hass, calls, offset, delta): - """Test for firing at input_datetime.""" - await async_setup_component( - hass, - "input_datetime", - {"input_datetime": {"trigger": {"has_date": True, "has_time": True}}}, - ) - now = dt_util.now() - - set_dt = now.replace(hour=5, minute=0, second=0, microsecond=0) + timedelta(2) - trigger_dt = set_dt + delta - - await hass.services.async_call( - "input_datetime", - "set_datetime", - { - ATTR_ENTITY_ID: "input_datetime.trigger", - "datetime": str(set_dt.replace(tzinfo=None)), - }, - blocking=True, - ) - - time_that_will_not_match_right_away = trigger_dt - timedelta(minutes=1) - - some_data = "{{ trigger.platform }}-{{ trigger.now.day }}-{{ trigger.now.hour }}-{{ trigger.now.minute }}-{{ trigger.now.second }}-{{trigger.entity_id}}" - with patch( - "homeassistant.util.dt.utcnow", - return_value=dt_util.as_utc(time_that_will_not_match_right_away), - ): - assert await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: { - "trigger": { - "platform": "time", - "at": { - "entity_id": "input_datetime.trigger", - "offset": offset, - }, - }, - "action": { - "service": "test.automation", - "data_template": {"some": some_data}, - }, - } - }, - ) - await hass.async_block_till_done() - - async_fire_time_changed(hass, trigger_dt + timedelta(seconds=1)) - await hass.async_block_till_done() - - assert len(calls) == 1 - assert ( - calls[0].data["some"] - == f"time-{trigger_dt.day}-{trigger_dt.hour}-{trigger_dt.minute}-{trigger_dt.second}-input_datetime.trigger" - ) - - set_dt += timedelta(days=1, hours=1) - trigger_dt += timedelta(days=1, hours=1) - - await hass.services.async_call( - "input_datetime", - "set_datetime", - { - ATTR_ENTITY_ID: "input_datetime.trigger", - "datetime": str(set_dt.replace(tzinfo=None)), - }, - blocking=True, - ) - - async_fire_time_changed(hass, trigger_dt + timedelta(seconds=1)) - await hass.async_block_till_done() - - assert len(calls) == 2 - assert ( - calls[1].data["some"] - == f"time-{trigger_dt.day}-{trigger_dt.hour}-{trigger_dt.minute}-{trigger_dt.second}-input_datetime.trigger" - ) - - async def test_if_fires_using_multiple_at(hass, calls): """Test for firing at.""" @@ -588,103 +498,12 @@ async def test_if_fires_using_at_sensor(hass, calls): assert len(calls) == 2 -@pytest.mark.parametrize( - "offset,delta", - [ - ("00:00:10", timedelta(seconds=10)), - ("-00:00:10", timedelta(seconds=-10)), - ({"minutes": 5}, timedelta(minutes=5)), - ], -) -async def test_if_fires_using_at_sensor_with_offset(hass, calls, offset, delta): - """Test for firing at sensor time.""" - now = dt_util.now() - - start_dt = now.replace(hour=5, minute=0, second=0, microsecond=0) + timedelta(2) - trigger_dt = start_dt + delta - - hass.states.async_set( - "sensor.next_alarm", - start_dt.isoformat(), - {ATTR_DEVICE_CLASS: sensor.DEVICE_CLASS_TIMESTAMP}, - ) - - time_that_will_not_match_right_away = trigger_dt - timedelta(minutes=1) - - some_data = "{{ trigger.platform }}-{{ trigger.now.day }}-{{ trigger.now.hour }}-{{ trigger.now.minute }}-{{ trigger.now.second }}-{{trigger.entity_id}}" - with patch( - "homeassistant.util.dt.utcnow", - return_value=dt_util.as_utc(time_that_will_not_match_right_away), - ): - assert await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: { - "trigger": { - "platform": "time", - "at": { - "entity_id": "sensor.next_alarm", - "offset": offset, - }, - }, - "action": { - "service": "test.automation", - "data_template": {"some": some_data}, - }, - } - }, - ) - await hass.async_block_till_done() - - async_fire_time_changed(hass, trigger_dt + timedelta(seconds=1)) - await hass.async_block_till_done() - - assert len(calls) == 1 - assert ( - calls[0].data["some"] - == f"time-{trigger_dt.day}-{trigger_dt.hour}-{trigger_dt.minute}-{trigger_dt.second}-sensor.next_alarm" - ) - - start_dt += timedelta(days=1, hours=1) - trigger_dt += timedelta(days=1, hours=1) - - hass.states.async_set( - "sensor.next_alarm", - start_dt.isoformat(), - {ATTR_DEVICE_CLASS: sensor.DEVICE_CLASS_TIMESTAMP}, - ) - await hass.async_block_till_done() - - async_fire_time_changed(hass, trigger_dt + timedelta(seconds=1)) - await hass.async_block_till_done() - - assert len(calls) == 2 - assert ( - calls[1].data["some"] - == f"time-{trigger_dt.day}-{trigger_dt.hour}-{trigger_dt.minute}-{trigger_dt.second}-sensor.next_alarm" - ) - - @pytest.mark.parametrize( "conf", [ {"platform": "time", "at": "input_datetime.bla"}, {"platform": "time", "at": "sensor.bla"}, {"platform": "time", "at": "12:34"}, - { - "platform": "time", - "at": {"entity_id": "input_datetime.bla", "offset": "00:01"}, - }, - {"platform": "time", "at": {"entity_id": "sensor.bla", "offset": "-00:01"}}, - { - "platform": "time", - "at": [{"entity_id": "input_datetime.bla", "offset": "01:00:00"}], - }, - { - "platform": "time", - "at": [{"entity_id": "sensor.bla", "offset": "-01:00:00"}], - }, ], ) def test_schema_valid(conf): @@ -698,9 +517,6 @@ def test_schema_valid(conf): {"platform": "time", "at": "binary_sensor.bla"}, {"platform": "time", "at": 745}, {"platform": "time", "at": "25:00"}, - {"platform": "time", "at": {"entity_id": "input_datetime.bla", "offset": "0:"}}, - {"platform": "time", "at": {"entity_id": "input_datetime.bla", "offset": "a"}}, - {"platform": "time", "at": {"entity_id": "13:00:00", "offset": "0:10"}}, ], ) def test_schema_invalid(conf): From 44334ea4da9278947ecb70c482bcc94deac84bd3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 2 Nov 2021 19:10:42 +0100 Subject: [PATCH 089/174] Extend Tuya Dimmer (tgq) support (#58951) --- homeassistant/components/tuya/light.py | 7 +++++++ homeassistant/components/tuya/select.py | 16 ++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index 75442159dea..581cd85647b 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -177,6 +177,13 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { # Dimmer # https://developer.tuya.com/en/docs/iot/tgq?id=Kaof8ke9il4k4 "tgq": ( + TuyaLightEntityDescription( + key=DPCode.SWITCH_LED, + name="Light", + brightness=(DPCode.BRIGHT_VALUE_V2, DPCode.BRIGHT_VALUE), + brightness_max=DPCode.BRIGHTNESS_MAX_1, + brightness_min=DPCode.BRIGHTNESS_MIN_1, + ), TuyaLightEntityDescription( key=DPCode.SWITCH_LED_1, name="Light", diff --git a/homeassistant/components/tuya/select.py b/homeassistant/components/tuya/select.py index 6df5b4e84dd..b4da7b8dfae 100644 --- a/homeassistant/components/tuya/select.py +++ b/homeassistant/components/tuya/select.py @@ -177,6 +177,22 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { entity_category=ENTITY_CATEGORY_CONFIG, ), ), + # Dimmer + # https://developer.tuya.com/en/docs/iot/tgq?id=Kaof8ke9il4k4 + "tgq": ( + SelectEntityDescription( + key=DPCode.LED_TYPE_1, + name="Light Source Type", + device_class=DEVICE_CLASS_TUYA_LED_TYPE, + entity_category=ENTITY_CATEGORY_CONFIG, + ), + SelectEntityDescription( + key=DPCode.LED_TYPE_2, + name="Light 2 Source Type", + device_class=DEVICE_CLASS_TUYA_LED_TYPE, + entity_category=ENTITY_CATEGORY_CONFIG, + ), + ), } From f8290ed026d523d2a7f0da172ff8b92b178e2455 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 2 Nov 2021 19:27:46 +0100 Subject: [PATCH 090/174] Add support for IoT Switches (tdq) in Tuya (#58952) --- homeassistant/components/tuya/select.py | 16 ++++++++++++++++ homeassistant/components/tuya/switch.py | 25 +++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/homeassistant/components/tuya/select.py b/homeassistant/components/tuya/select.py index b4da7b8dfae..921dc21eaaa 100644 --- a/homeassistant/components/tuya/select.py +++ b/homeassistant/components/tuya/select.py @@ -143,6 +143,22 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { entity_category=ENTITY_CATEGORY_CONFIG, ), ), + # IoT Switch? + # Note: Undocumented + "tdq": ( + SelectEntityDescription( + key=DPCode.RELAY_STATUS, + name="Power on Behavior", + device_class=DEVICE_CLASS_TUYA_RELAY_STATUS, + entity_category=ENTITY_CATEGORY_CONFIG, + ), + SelectEntityDescription( + key=DPCode.LIGHT_MODE, + name="Indicator Light Mode", + device_class=DEVICE_CLASS_TUYA_LIGHT_MODE, + entity_category=ENTITY_CATEGORY_CONFIG, + ), + ), # Dimmer Switch # https://developer.tuya.com/en/docs/iot/categorytgkg?id=Kaiuz0ktx7m0o "tgkg": ( diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index 6f0f7b85b17..8395c02cf39 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -369,6 +369,31 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { entity_category=ENTITY_CATEGORY_CONFIG, ), ), + # IoT Switch? + # Note: Undocumented + "tdq": ( + SwitchEntityDescription( + key=DPCode.SWITCH_1, + name="Switch 1", + device_class=DEVICE_CLASS_OUTLET, + ), + SwitchEntityDescription( + key=DPCode.SWITCH_2, + name="Switch 2", + device_class=DEVICE_CLASS_OUTLET, + ), + SwitchEntityDescription( + key=DPCode.SWITCH_3, + name="Switch 3", + device_class=DEVICE_CLASS_OUTLET, + ), + SwitchEntityDescription( + key=DPCode.CHILD_LOCK, + name="Child Lock", + icon="mdi:account-lock", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + ), # Solar Light # https://developer.tuya.com/en/docs/iot/tynd?id=Kaof8j02e1t98 "tyndj": ( From a897dfa5b700e0865d6390de2cb5e54ab9a3d207 Mon Sep 17 00:00:00 2001 From: Ernst Klamer Date: Tue, 2 Nov 2021 19:27:19 +0100 Subject: [PATCH 091/174] Add device configuration URL to Solar-Log (#58954) --- homeassistant/components/solarlog/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/solarlog/sensor.py b/homeassistant/components/solarlog/sensor.py index d54845f8027..5d79efb94c9 100644 --- a/homeassistant/components/solarlog/sensor.py +++ b/homeassistant/components/solarlog/sensor.py @@ -34,6 +34,7 @@ class SolarlogSensor(update_coordinator.CoordinatorEntity, SensorEntity): identifiers={(DOMAIN, coordinator.unique_id)}, manufacturer="Solar-Log", name=coordinator.name, + configuration_url=coordinator.host, ) @property From 608b89a6ada93a832e2565ab4863c7d487a62d0d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 2 Nov 2021 11:28:43 -0700 Subject: [PATCH 092/174] Bumped version to 2021.11.0b5 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 03240bacaf7..acc8ff2f6e0 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 11 -PATCH_VERSION: Final = "0b4" +PATCH_VERSION: Final = "0b5" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From ae99b678dd999d8ae5d268ed99f21f515b854205 Mon Sep 17 00:00:00 2001 From: kodsnutten Date: Wed, 3 Nov 2021 10:21:54 +0100 Subject: [PATCH 093/174] Fix unique_id of derived sent-sensors (#58298) --- homeassistant/components/upnp/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py index f7cc242f6f1..54176715b84 100644 --- a/homeassistant/components/upnp/sensor.py +++ b/homeassistant/components/upnp/sensor.py @@ -84,7 +84,7 @@ DERIVED_SENSORS: tuple[UpnpSensorEntityDescription, ...] = ( ), UpnpSensorEntityDescription( key=BYTES_SENT, - unique_id="KiB/sent", + unique_id="KiB/sec_sent", name=f"{DATA_RATE_KIBIBYTES_PER_SECOND} sent", icon="mdi:server-network", native_unit_of_measurement=DATA_RATE_KIBIBYTES_PER_SECOND, @@ -100,7 +100,7 @@ DERIVED_SENSORS: tuple[UpnpSensorEntityDescription, ...] = ( ), UpnpSensorEntityDescription( key=PACKETS_SENT, - unique_id="packets/sent", + unique_id="packets/sec_sent", name=f"{DATA_RATE_PACKETS_PER_SECOND} sent", icon="mdi:server-network", native_unit_of_measurement=DATA_RATE_PACKETS_PER_SECOND, From 0a27b0f3534bcf1f4b54ae16e634fc332f693af4 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Thu, 28 Oct 2021 22:47:49 +0100 Subject: [PATCH 094/174] Aurora abb energy metering (#58454) Co-authored-by: J. Nick Koston --- .../aurora_abb_powerone/aurora_device.py | 2 +- .../components/aurora_abb_powerone/sensor.py | 67 ++++++++++++------- .../aurora_abb_powerone/test_sensor.py | 35 ++++------ 3 files changed, 58 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/aurora_abb_powerone/aurora_device.py b/homeassistant/components/aurora_abb_powerone/aurora_device.py index 0a7aab4a921..3913515a9b9 100644 --- a/homeassistant/components/aurora_abb_powerone/aurora_device.py +++ b/homeassistant/components/aurora_abb_powerone/aurora_device.py @@ -32,7 +32,7 @@ class AuroraDevice(Entity): def unique_id(self) -> str: """Return the unique id for this device.""" serial = self._data[ATTR_SERIAL_NUMBER] - return f"{serial}_{self.type}" + return f"{serial}_{self.entity_description.key}" @property def available(self) -> bool: diff --git a/homeassistant/components/aurora_abb_powerone/sensor.py b/homeassistant/components/aurora_abb_powerone/sensor.py index 946f5645bdc..4f196c39630 100644 --- a/homeassistant/components/aurora_abb_powerone/sensor.py +++ b/homeassistant/components/aurora_abb_powerone/sensor.py @@ -1,6 +1,9 @@ """Support for Aurora ABB PowerOne Solar Photvoltaic (PV) inverter.""" +from __future__ import annotations +from collections.abc import Mapping import logging +from typing import Any from aurorapy.client import AuroraError, AuroraSerialClient import voluptuous as vol @@ -8,19 +11,22 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, + SensorEntityDescription, ) from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( CONF_ADDRESS, CONF_DEVICE, CONF_NAME, + DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, + ENERGY_KILO_WATT_HOUR, POWER_WATT, TEMP_CELSIUS, ) -from homeassistant.exceptions import InvalidStateError import homeassistant.helpers.config_validation as cv from .aurora_device import AuroraDevice @@ -28,6 +34,29 @@ from .const import DEFAULT_ADDRESS, DOMAIN _LOGGER = logging.getLogger(__name__) +SENSOR_TYPES = [ + SensorEntityDescription( + key="instantaneouspower", + device_class=DEVICE_CLASS_POWER, + native_unit_of_measurement=POWER_WATT, + state_class=STATE_CLASS_MEASUREMENT, + name="Power Output", + ), + SensorEntityDescription( + key="temp", + device_class=DEVICE_CLASS_TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + state_class=STATE_CLASS_MEASUREMENT, + name="Temperature", + ), + SensorEntityDescription( + key="totalenergy", + device_class=DEVICE_CLASS_ENERGY, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_TOTAL_INCREASING, + name="Total Energy", + ), +] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -55,15 +84,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities) -> None: """Set up aurora_abb_powerone sensor based on a config entry.""" entities = [] - sensortypes = [ - {"parameter": "instantaneouspower", "name": "Power Output"}, - {"parameter": "temperature", "name": "Temperature"}, - ] client = hass.data[DOMAIN][config_entry.unique_id] data = config_entry.data - for sens in sensortypes: - entities.append(AuroraSensor(client, data, sens["name"], sens["parameter"])) + for sens in SENSOR_TYPES: + entities.append(AuroraSensor(client, data, sens)) _LOGGER.debug("async_setup_entry adding %d entities", len(entities)) async_add_entities(entities, True) @@ -72,22 +97,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities) -> None: class AuroraSensor(AuroraDevice, SensorEntity): """Representation of a Sensor on a Aurora ABB PowerOne Solar inverter.""" - _attr_state_class = STATE_CLASS_MEASUREMENT - - def __init__(self, client: AuroraSerialClient, data, name, typename): + def __init__( + self, + client: AuroraSerialClient, + data: Mapping[str, Any], + entity_description: SensorEntityDescription, + ) -> None: """Initialize the sensor.""" super().__init__(client, data) - if typename == "instantaneouspower": - self.type = typename - self._attr_native_unit_of_measurement = POWER_WATT - self._attr_device_class = DEVICE_CLASS_POWER - elif typename == "temperature": - self.type = typename - self._attr_native_unit_of_measurement = TEMP_CELSIUS - self._attr_device_class = DEVICE_CLASS_TEMPERATURE - else: - raise InvalidStateError(f"Unrecognised typename '{typename}'") - self._attr_name = f"{name}" + self.entity_description = entity_description self.availableprev = True def update(self): @@ -98,13 +116,16 @@ class AuroraSensor(AuroraDevice, SensorEntity): try: self.availableprev = self._attr_available self.client.connect() - if self.type == "instantaneouspower": + if self.entity_description.key == "instantaneouspower": # read ADC channel 3 (grid power output) power_watts = self.client.measure(3, True) self._attr_native_value = round(power_watts, 1) - elif self.type == "temperature": + elif self.entity_description.key == "temp": temperature_c = self.client.measure(21) self._attr_native_value = round(temperature_c, 1) + elif self.entity_description.key == "totalenergy": + energy_wh = self.client.cumulated_energy(5) + self._attr_native_value = round(energy_wh / 1000, 2) self._attr_available = True except AuroraError as error: diff --git a/tests/components/aurora_abb_powerone/test_sensor.py b/tests/components/aurora_abb_powerone/test_sensor.py index 26486c6a116..ae9360498c7 100644 --- a/tests/components/aurora_abb_powerone/test_sensor.py +++ b/tests/components/aurora_abb_powerone/test_sensor.py @@ -3,7 +3,6 @@ from datetime import timedelta from unittest.mock import patch from aurorapy.client import AuroraError -import pytest from homeassistant.components.aurora_abb_powerone.const import ( ATTR_DEVICE_NAME, @@ -13,10 +12,8 @@ from homeassistant.components.aurora_abb_powerone.const import ( DEFAULT_INTEGRATION_TITLE, DOMAIN, ) -from homeassistant.components.aurora_abb_powerone.sensor import AuroraSensor from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_ADDRESS, CONF_PORT -from homeassistant.exceptions import InvalidStateError from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -39,6 +36,7 @@ def _simulated_returns(index, global_measure=None): returns = { 3: 45.678, # power 21: 9.876, # temperature + 5: 12345, # energy } return returns[index] @@ -66,7 +64,12 @@ async def test_setup_platform_valid_config(hass): with patch("aurorapy.client.AuroraSerialClient.connect", return_value=None), patch( "aurorapy.client.AuroraSerialClient.measure", side_effect=_simulated_returns, - ), assert_setup_component(1, "sensor"): + ), patch( + "aurorapy.client.AuroraSerialClient.cumulated_energy", + side_effect=_simulated_returns, + ), assert_setup_component( + 1, "sensor" + ): assert await async_setup_component(hass, "sensor", TEST_CONFIG) await hass.async_block_till_done() power = hass.states.get("sensor.power_output") @@ -91,6 +94,9 @@ async def test_sensors(hass): with patch("aurorapy.client.AuroraSerialClient.connect", return_value=None), patch( "aurorapy.client.AuroraSerialClient.measure", side_effect=_simulated_returns, + ), patch( + "aurorapy.client.AuroraSerialClient.cumulated_energy", + side_effect=_simulated_returns, ): mock_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_entry.entry_id) @@ -104,24 +110,9 @@ async def test_sensors(hass): assert temperature assert temperature.state == "9.9" - -async def test_sensor_invalid_type(hass): - """Test invalid sensor type during setup.""" - entities = [] - mock_entry = _mock_config_entry() - - with patch("aurorapy.client.AuroraSerialClient.connect", return_value=None), patch( - "aurorapy.client.AuroraSerialClient.measure", - side_effect=_simulated_returns, - ): - mock_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_entry.entry_id) - await hass.async_block_till_done() - - client = hass.data[DOMAIN][mock_entry.unique_id] - data = mock_entry.data - with pytest.raises(InvalidStateError): - entities.append(AuroraSensor(client, data, "WrongSensor", "wrongparameter")) + energy = hass.states.get("sensor.total_energy") + assert energy + assert energy.state == "12.35" async def test_sensor_dark(hass): From dff98b024cfd43e8be1150f9d88fba4bb466541f Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Wed, 3 Nov 2021 00:30:29 +0000 Subject: [PATCH 095/174] Aurora abb defer unique_id assignment during yaml import (#58887) * Defer unique_id assignment during yaml import if dark * Back out variable name change to simplify. * Allow config flow yaml setup deferral. * Fix deferred yaml import * Code review: only wrap necessary lines in try blk * Code review: catch possible duplicate unique_id * Simplify assignment. * Code review: use timedelta to retry yaml import * Code review: if a different error occurs, raise it * Remove current config entry if duplicate unique_id * Code review: remove unnecessary line. * Code review: revert change, leave to other PR. * Code review: remove unnecessary patch & min->sec * Remove unnecessary else after raise. * Increase test coverage. * Check the number of config entries at each stage * Raise ConfigEntryNotReady when connection fails. * Log & return false for error on yaml import --- .../aurora_abb_powerone/__init__.py | 42 ++++- .../aurora_abb_powerone/aurora_device.py | 8 +- .../aurora_abb_powerone/config_flow.py | 6 - .../aurora_abb_powerone/test_config_flow.py | 175 +++++++++++++++++- .../aurora_abb_powerone/test_init.py | 12 ++ .../aurora_abb_powerone/test_sensor.py | 36 ++++ 6 files changed, 260 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/aurora_abb_powerone/__init__.py b/homeassistant/components/aurora_abb_powerone/__init__.py index ff26c3770f0..2c3d0c546cd 100644 --- a/homeassistant/components/aurora_abb_powerone/__init__.py +++ b/homeassistant/components/aurora_abb_powerone/__init__.py @@ -10,13 +10,15 @@ import logging -from aurorapy.client import AuroraSerialClient +from aurorapy.client import AuroraError, AuroraSerialClient from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ADDRESS, CONF_PORT from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady -from .const import DOMAIN +from .config_flow import validate_and_connect +from .const import ATTR_SERIAL_NUMBER, DOMAIN PLATFORMS = ["sensor"] @@ -29,9 +31,43 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): comport = entry.data[CONF_PORT] address = entry.data[CONF_ADDRESS] serclient = AuroraSerialClient(address, comport, parity="N", timeout=1) + # To handle yaml import attempts in darkeness, (re)try connecting only if + # unique_id not yet assigned. + if entry.unique_id is None: + try: + res = await hass.async_add_executor_job( + validate_and_connect, hass, entry.data + ) + except AuroraError as error: + if "No response after" in str(error): + raise ConfigEntryNotReady("No response (could be dark)") from error + _LOGGER.error("Failed to connect to inverter: %s", error) + return False + except OSError as error: + if error.errno == 19: # No such device. + _LOGGER.error("Failed to connect to inverter: no such COM port") + return False + _LOGGER.error("Failed to connect to inverter: %s", error) + return False + else: + # If we got here, the device is now communicating (maybe after + # being in darkness). But there's a small risk that the user has + # configured via the UI since we last attempted the yaml setup, + # which means we'd get a duplicate unique ID. + new_id = res[ATTR_SERIAL_NUMBER] + # Check if this unique_id has already been used + for existing_entry in hass.config_entries.async_entries(DOMAIN): + if existing_entry.unique_id == new_id: + _LOGGER.debug( + "Remove already configured config entry for id %s", new_id + ) + hass.async_create_task( + hass.config_entries.async_remove(entry.entry_id) + ) + return False + hass.config_entries.async_update_entry(entry, unique_id=new_id) hass.data.setdefault(DOMAIN, {})[entry.unique_id] = serclient - hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True diff --git a/homeassistant/components/aurora_abb_powerone/aurora_device.py b/homeassistant/components/aurora_abb_powerone/aurora_device.py index 3913515a9b9..d2aed5ec7a8 100644 --- a/homeassistant/components/aurora_abb_powerone/aurora_device.py +++ b/homeassistant/components/aurora_abb_powerone/aurora_device.py @@ -1,4 +1,6 @@ """Top level class for AuroraABBPowerOneSolarPV inverters and sensors.""" +from __future__ import annotations + import logging from aurorapy.client import AuroraSerialClient @@ -29,9 +31,11 @@ class AuroraDevice(Entity): self._available = True @property - def unique_id(self) -> str: + def unique_id(self) -> str | None: """Return the unique id for this device.""" - serial = self._data[ATTR_SERIAL_NUMBER] + serial = self._data.get(ATTR_SERIAL_NUMBER) + if serial is None: + return None return f"{serial}_{self.entity_description.key}" @property diff --git a/homeassistant/components/aurora_abb_powerone/config_flow.py b/homeassistant/components/aurora_abb_powerone/config_flow.py index c0c87e9e103..012fe7b14bb 100644 --- a/homeassistant/components/aurora_abb_powerone/config_flow.py +++ b/homeassistant/components/aurora_abb_powerone/config_flow.py @@ -81,16 +81,10 @@ class AuroraABBConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="already_setup") conf = {} - conf[ATTR_SERIAL_NUMBER] = "sn_unknown_yaml" - conf[ATTR_MODEL] = "model_unknown_yaml" - conf[ATTR_FIRMWARE] = "fw_unknown_yaml" conf[CONF_PORT] = config["device"] conf[CONF_ADDRESS] = config["address"] # config["name"] from yaml is ignored. - await self.async_set_unique_id(self.flow_id) - self._abort_if_unique_id_configured() - return self.async_create_entry(title=DEFAULT_INTEGRATION_TITLE, data=conf) async def async_step_user(self, user_input=None): diff --git a/tests/components/aurora_abb_powerone/test_config_flow.py b/tests/components/aurora_abb_powerone/test_config_flow.py index d385d33ddd9..620d7e10e53 100644 --- a/tests/components/aurora_abb_powerone/test_config_flow.py +++ b/tests/components/aurora_abb_powerone/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Aurora ABB PowerOne Solar PV config flow.""" +from datetime import timedelta from logging import INFO from unittest.mock import patch @@ -12,7 +13,20 @@ from homeassistant.components.aurora_abb_powerone.const import ( ATTR_SERIAL_NUMBER, DOMAIN, ) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_ADDRESS, CONF_PORT +from homeassistant.util.dt import utcnow + +from tests.common import async_fire_time_changed + + +def _simulated_returns(index, global_measure=None): + returns = { + 3: 45.678, # power + 21: 9.876, # temperature + 5: 12345, # energy + } + return returns[index] async def test_form(hass): @@ -150,16 +164,161 @@ async def test_form_invalid_com_ports(hass): # Tests below can be deleted after deprecation period is finished. -async def test_import(hass): - """Test configuration.yaml import used during migration.""" - TESTDATA = {"device": "/dev/ttyUSB7", "address": 3, "name": "MyAuroraPV"} - with patch( - "homeassistant.components.generic.camera.GenericCamera.async_camera_image", - return_value=None, - ): +async def test_import_day(hass): + """Test .yaml import when the inverter is able to communicate.""" + TEST_DATA = {"device": "/dev/ttyUSB7", "address": 3, "name": "MyAuroraPV"} + + with patch("aurorapy.client.AuroraSerialClient.connect", return_value=None,), patch( + "aurorapy.client.AuroraSerialClient.serial_number", + return_value="9876543", + ), patch( + "aurorapy.client.AuroraSerialClient.version", + return_value="9.8.7.6", + ), patch( + "aurorapy.client.AuroraSerialClient.pn", + return_value="A.B.C", + ), patch( + "aurorapy.client.AuroraSerialClient.firmware", + return_value="1.234", + ) as mock_setup_entry: result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TESTDATA + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TEST_DATA ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["data"][CONF_PORT] == "/dev/ttyUSB7" assert result["data"][CONF_ADDRESS] == 3 + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import_night(hass): + """Test .yaml import when the inverter is inaccessible (e.g. darkness).""" + TEST_DATA = {"device": "/dev/ttyUSB7", "address": 3, "name": "MyAuroraPV"} + + # First time round, no response. + with patch( + "aurorapy.client.AuroraSerialClient.connect", + side_effect=AuroraError("No response after"), + ) as mock_connect: + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TEST_DATA + ) + + configs = hass.config_entries.async_entries(DOMAIN) + assert len(configs) == 1 + entry = configs[0] + assert not entry.unique_id + assert entry.state == ConfigEntryState.SETUP_RETRY + + assert len(mock_connect.mock_calls) == 1 + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"][CONF_PORT] == "/dev/ttyUSB7" + assert result["data"][CONF_ADDRESS] == 3 + + # Second time round, talking this time. + with patch("aurorapy.client.AuroraSerialClient.connect", return_value=None,), patch( + "aurorapy.client.AuroraSerialClient.serial_number", + return_value="9876543", + ), patch( + "aurorapy.client.AuroraSerialClient.version", + return_value="9.8.7.6", + ), patch( + "aurorapy.client.AuroraSerialClient.pn", + return_value="A.B.C", + ), patch( + "aurorapy.client.AuroraSerialClient.firmware", + return_value="1.234", + ), patch( + "aurorapy.client.AuroraSerialClient.measure", + side_effect=_simulated_returns, + ): + # Wait >5seconds for the config to auto retry. + async_fire_time_changed(hass, utcnow() + timedelta(seconds=6)) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.LOADED + assert entry.unique_id + + assert len(mock_connect.mock_calls) == 1 + assert hass.states.get("sensor.power_output").state == "45.7" + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + +async def test_import_night_then_user(hass): + """Attempt yaml import and fail (dark), but user sets up manually before auto retry.""" + TEST_DATA = {"device": "/dev/ttyUSB7", "address": 3, "name": "MyAuroraPV"} + + # First time round, no response. + with patch( + "aurorapy.client.AuroraSerialClient.connect", + side_effect=AuroraError("No response after"), + ) as mock_connect: + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TEST_DATA + ) + + configs = hass.config_entries.async_entries(DOMAIN) + assert len(configs) == 1 + entry = configs[0] + assert not entry.unique_id + assert entry.state == ConfigEntryState.SETUP_RETRY + + assert len(mock_connect.mock_calls) == 1 + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"][CONF_PORT] == "/dev/ttyUSB7" + assert result["data"][CONF_ADDRESS] == 3 + + # Failed once, now simulate the user initiating config flow with valid settings. + fakecomports = [] + fakecomports.append(list_ports_common.ListPortInfo("/dev/ttyUSB7")) + with patch( + "serial.tools.list_ports.comports", + return_value=fakecomports, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch("aurorapy.client.AuroraSerialClient.connect", return_value=None,), patch( + "aurorapy.client.AuroraSerialClient.serial_number", + return_value="9876543", + ), patch( + "aurorapy.client.AuroraSerialClient.version", + return_value="9.8.7.6", + ), patch( + "aurorapy.client.AuroraSerialClient.pn", + return_value="A.B.C", + ), patch( + "aurorapy.client.AuroraSerialClient.firmware", + return_value="1.234", + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PORT: "/dev/ttyUSB7", CONF_ADDRESS: 7}, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert len(hass.config_entries.async_entries(DOMAIN)) == 2 + + # Now retry yaml - it should fail with duplicate + with patch("aurorapy.client.AuroraSerialClient.connect", return_value=None,), patch( + "aurorapy.client.AuroraSerialClient.serial_number", + return_value="9876543", + ), patch( + "aurorapy.client.AuroraSerialClient.version", + return_value="9.8.7.6", + ), patch( + "aurorapy.client.AuroraSerialClient.pn", + return_value="A.B.C", + ), patch( + "aurorapy.client.AuroraSerialClient.firmware", + return_value="1.234", + ): + # Wait >5seconds for the config to auto retry. + async_fire_time_changed(hass, utcnow() + timedelta(seconds=6)) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.NOT_LOADED + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 diff --git a/tests/components/aurora_abb_powerone/test_init.py b/tests/components/aurora_abb_powerone/test_init.py index bd0f1c727cd..3bef40a14d3 100644 --- a/tests/components/aurora_abb_powerone/test_init.py +++ b/tests/components/aurora_abb_powerone/test_init.py @@ -19,6 +19,18 @@ async def test_unload_entry(hass): with patch("aurorapy.client.AuroraSerialClient.connect", return_value=None), patch( "homeassistant.components.aurora_abb_powerone.sensor.AuroraSensor.update", return_value=None, + ), patch( + "aurorapy.client.AuroraSerialClient.serial_number", + return_value="9876543", + ), patch( + "aurorapy.client.AuroraSerialClient.version", + return_value="9.8.7.6", + ), patch( + "aurorapy.client.AuroraSerialClient.pn", + return_value="A.B.C", + ), patch( + "aurorapy.client.AuroraSerialClient.firmware", + return_value="1.234", ): mock_entry = MockConfigEntry( domain=DOMAIN, diff --git a/tests/components/aurora_abb_powerone/test_sensor.py b/tests/components/aurora_abb_powerone/test_sensor.py index ae9360498c7..c1d1d4ad5c8 100644 --- a/tests/components/aurora_abb_powerone/test_sensor.py +++ b/tests/components/aurora_abb_powerone/test_sensor.py @@ -64,6 +64,18 @@ async def test_setup_platform_valid_config(hass): with patch("aurorapy.client.AuroraSerialClient.connect", return_value=None), patch( "aurorapy.client.AuroraSerialClient.measure", side_effect=_simulated_returns, + ), patch( + "aurorapy.client.AuroraSerialClient.serial_number", + return_value="9876543", + ), patch( + "aurorapy.client.AuroraSerialClient.version", + return_value="9.8.7.6", + ), patch( + "aurorapy.client.AuroraSerialClient.pn", + return_value="A.B.C", + ), patch( + "aurorapy.client.AuroraSerialClient.firmware", + return_value="1.234", ), patch( "aurorapy.client.AuroraSerialClient.cumulated_energy", side_effect=_simulated_returns, @@ -94,6 +106,18 @@ async def test_sensors(hass): with patch("aurorapy.client.AuroraSerialClient.connect", return_value=None), patch( "aurorapy.client.AuroraSerialClient.measure", side_effect=_simulated_returns, + ), patch( + "aurorapy.client.AuroraSerialClient.serial_number", + return_value="9876543", + ), patch( + "aurorapy.client.AuroraSerialClient.version", + return_value="9.8.7.6", + ), patch( + "aurorapy.client.AuroraSerialClient.pn", + return_value="A.B.C", + ), patch( + "aurorapy.client.AuroraSerialClient.firmware", + return_value="1.234", ), patch( "aurorapy.client.AuroraSerialClient.cumulated_energy", side_effect=_simulated_returns, @@ -123,6 +147,18 @@ async def test_sensor_dark(hass): # sun is up with patch("aurorapy.client.AuroraSerialClient.connect", return_value=None), patch( "aurorapy.client.AuroraSerialClient.measure", side_effect=_simulated_returns + ), patch( + "aurorapy.client.AuroraSerialClient.serial_number", + return_value="9876543", + ), patch( + "aurorapy.client.AuroraSerialClient.version", + return_value="9.8.7.6", + ), patch( + "aurorapy.client.AuroraSerialClient.pn", + return_value="A.B.C", + ), patch( + "aurorapy.client.AuroraSerialClient.firmware", + return_value="1.234", ): mock_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_entry.entry_id) From 4163ba5dbf9b049f36d1160632190d607b9e21f1 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Tue, 2 Nov 2021 23:21:56 -0400 Subject: [PATCH 096/174] Add missing ZMW currency (#58959) --- homeassistant/helpers/config_validation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index e6c2792304b..f2ac86239f8 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -1444,6 +1444,7 @@ currency = vol.In( "YER", "ZAR", "ZMK", + "ZMW", "ZWL", }, msg="invalid ISO 4217 formatted currency", From ded07857005e7406ded4a7a1fc815f4dd46362ec Mon Sep 17 00:00:00 2001 From: Hans Oischinger Date: Wed, 3 Nov 2021 08:45:22 +0100 Subject: [PATCH 097/174] Fix broken ViCare burner & compressor sensors (#58962) --- homeassistant/components/vicare/binary_sensor.py | 4 ++-- homeassistant/components/vicare/sensor.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/vicare/binary_sensor.py b/homeassistant/components/vicare/binary_sensor.py index d025d2b1ba6..4484d5f5040 100644 --- a/homeassistant/components/vicare/binary_sensor.py +++ b/homeassistant/components/vicare/binary_sensor.py @@ -129,14 +129,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= all_devices.append(entity) try: - _entities_from_descriptions( + await _entities_from_descriptions( hass, name, all_devices, BURNER_SENSORS, api.burners ) except PyViCareNotSupportedFeatureError: _LOGGER.info("No burners found") try: - _entities_from_descriptions( + await _entities_from_descriptions( hass, name, all_devices, COMPRESSOR_SENSORS, api.compressors ) except PyViCareNotSupportedFeatureError: diff --git a/homeassistant/components/vicare/sensor.py b/homeassistant/components/vicare/sensor.py index 044632b6244..2ff8ce4bf7d 100644 --- a/homeassistant/components/vicare/sensor.py +++ b/homeassistant/components/vicare/sensor.py @@ -393,14 +393,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= all_devices.append(entity) try: - _entities_from_descriptions( + await _entities_from_descriptions( hass, name, all_devices, BURNER_SENSORS, api.burners ) except PyViCareNotSupportedFeatureError: _LOGGER.info("No burners found") try: - _entities_from_descriptions( + await _entities_from_descriptions( hass, name, all_devices, COMPRESSOR_SENSORS, api.compressors ) except PyViCareNotSupportedFeatureError: From 1a08da7856d8c990722f09f64ad2f0130fc26b0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 3 Nov 2021 12:31:22 +0100 Subject: [PATCH 098/174] Bump pyMill to 0.7.4 (#58977) --- homeassistant/components/mill/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mill/manifest.json b/homeassistant/components/mill/manifest.json index 342b54d2483..75b6464c9c1 100644 --- a/homeassistant/components/mill/manifest.json +++ b/homeassistant/components/mill/manifest.json @@ -2,7 +2,7 @@ "domain": "mill", "name": "Mill", "documentation": "https://www.home-assistant.io/integrations/mill", - "requirements": ["millheater==0.7.3"], + "requirements": ["millheater==0.7.4"], "codeowners": ["@danielhiversen"], "config_flow": true, "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index fef79aa5ccc..510cc7584c8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1005,7 +1005,7 @@ micloud==0.4 miflora==0.7.0 # homeassistant.components.mill -millheater==0.7.3 +millheater==0.7.4 # homeassistant.components.minio minio==4.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 129e50565ad..0f3b9d36a11 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -600,7 +600,7 @@ mficlient==0.3.0 micloud==0.4 # homeassistant.components.mill -millheater==0.7.3 +millheater==0.7.4 # homeassistant.components.minio minio==4.0.9 From 7afb38ff96d7ba2aff0288361f095f6c554cf012 Mon Sep 17 00:00:00 2001 From: Sergio Gutierrez Alvarez Date: Wed, 3 Nov 2021 05:28:04 -0600 Subject: [PATCH 099/174] Fix battery_is_charging sensor on system bridge (#58980) --- homeassistant/components/system_bridge/binary_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/system_bridge/binary_sensor.py b/homeassistant/components/system_bridge/binary_sensor.py index a622a3a925a..7681a428cca 100644 --- a/homeassistant/components/system_bridge/binary_sensor.py +++ b/homeassistant/components/system_bridge/binary_sensor.py @@ -41,7 +41,7 @@ BATTERY_BINARY_SENSOR_TYPES: tuple[SystemBridgeBinarySensorEntityDescription, .. key="battery_is_charging", name="Battery Is Charging", device_class=DEVICE_CLASS_BATTERY_CHARGING, - value=lambda bridge: bridge.information.updates.available, + value=lambda bridge: bridge.battery.isCharging, ), ) From 55681212517a3d0c44616a49d1c4448444878e83 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 3 Nov 2021 11:51:17 +0100 Subject: [PATCH 100/174] Update frontend to 20211103.0 (#58988) --- 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 9f15dc4fc5a..c913de3367f 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20211028.0" + "home-assistant-frontend==20211103.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5ff0536e395..7d670da646c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==3.4.8 emoji==1.5.0 hass-nabucasa==0.50.0 -home-assistant-frontend==20211028.0 +home-assistant-frontend==20211103.0 httpx==0.19.0 ifaddr==0.1.7 jinja2==3.0.2 diff --git a/requirements_all.txt b/requirements_all.txt index 510cc7584c8..fdf88bf284b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -813,7 +813,7 @@ hole==0.5.1 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211028.0 +home-assistant-frontend==20211103.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0f3b9d36a11..148bd558d7e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -500,7 +500,7 @@ hole==0.5.1 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211028.0 +home-assistant-frontend==20211103.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From e3c021a910d36d8aadc73bf685d3fd2ab77b14dd Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 3 Nov 2021 15:03:43 +0100 Subject: [PATCH 101/174] Bumped version to 2021.11.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index acc8ff2f6e0..9eade8bd476 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 11 -PATCH_VERSION: Final = "0b5" +PATCH_VERSION: Final = "0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From 397f303d6d51bc6971d65bf76a69d385d9d9308b Mon Sep 17 00:00:00 2001 From: Thomas G <48775270+tomgie@users.noreply.github.com> Date: Thu, 4 Nov 2021 05:21:59 -0500 Subject: [PATCH 102/174] Swap sharkiq vacuum is_docked with is_charging (#58975) --- homeassistant/components/sharkiq/vacuum.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/homeassistant/components/sharkiq/vacuum.py b/homeassistant/components/sharkiq/vacuum.py index 3851dab7303..33412963cda 100644 --- a/homeassistant/components/sharkiq/vacuum.py +++ b/homeassistant/components/sharkiq/vacuum.py @@ -138,11 +138,6 @@ class SharkVacuumEntity(CoordinatorEntity, StateVacuumEntity): """Flag vacuum cleaner robot features that are supported.""" return SUPPORT_SHARKIQ - @property - def is_docked(self) -> bool | None: - """Is vacuum docked.""" - return self.sharkiq.get_property_value(Properties.DOCKED_STATUS) - @property def error_code(self) -> int | None: """Return the last observed error code (or None).""" @@ -175,7 +170,7 @@ class SharkVacuumEntity(CoordinatorEntity, StateVacuumEntity): In the app, these are (usually) handled by showing the robot as stopped and sending the user a notification. """ - if self.is_docked: + if self.sharkiq.get_property_value(Properties.CHARGING_STATUS): return STATE_DOCKED return self.operating_mode From 5e6cac3834773b2ee56eb15781e2dcddecaded83 Mon Sep 17 00:00:00 2001 From: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com> Date: Thu, 4 Nov 2021 17:00:25 +0100 Subject: [PATCH 103/174] Fix mop attribute for unified mop and water box in Xiaomi Miio (#58990) Co-authored-by: Teemu R. Co-authored-by: Martin Hjelmare --- .../components/xiaomi_miio/binary_sensor.py | 22 +++++++++++++++++-- homeassistant/components/xiaomi_miio/const.py | 3 +++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/binary_sensor.py b/homeassistant/components/xiaomi_miio/binary_sensor.py index 1bdd647da79..ef172bbbbc5 100644 --- a/homeassistant/components/xiaomi_miio/binary_sensor.py +++ b/homeassistant/components/xiaomi_miio/binary_sensor.py @@ -29,6 +29,7 @@ from .const import ( MODELS_HUMIDIFIER_MJJSQ, MODELS_VACUUM, MODELS_VACUUM_WITH_MOP, + MODELS_VACUUM_WITH_SEPARATE_MOP, ) from .device import XiaomiCoordinatedMiioEntity @@ -77,7 +78,7 @@ FAN_ZA5_BINARY_SENSORS = (ATTR_POWERSUPPLY_ATTACHED,) VACUUM_SENSORS = { ATTR_MOP_ATTACHED: XiaomiMiioBinarySensorDescription( - key=ATTR_MOP_ATTACHED, + key=ATTR_WATER_BOX_ATTACHED, name="Mop Attached", icon="mdi:square-rounded", parent_key=VacuumCoordinatorDataAttributes.status, @@ -105,6 +106,19 @@ VACUUM_SENSORS = { ), } +VACUUM_SENSORS_SEPARATE_MOP = { + **VACUUM_SENSORS, + ATTR_MOP_ATTACHED: XiaomiMiioBinarySensorDescription( + key=ATTR_MOP_ATTACHED, + name="Mop Attached", + icon="mdi:square-rounded", + parent_key=VacuumCoordinatorDataAttributes.status, + entity_registry_enabled_default=True, + device_class=DEVICE_CLASS_CONNECTIVITY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), +} + HUMIDIFIER_MIIO_BINARY_SENSORS = (ATTR_WATER_TANK_DETACHED,) HUMIDIFIER_MIOT_BINARY_SENSORS = (ATTR_WATER_TANK_DETACHED,) HUMIDIFIER_MJJSQ_BINARY_SENSORS = (ATTR_NO_WATER, ATTR_WATER_TANK_DETACHED) @@ -118,8 +132,12 @@ def _setup_vacuum_sensors(hass, config_entry, async_add_entities): device = hass.data[DOMAIN][config_entry.entry_id].get(KEY_DEVICE) coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] entities = [] + sensors = VACUUM_SENSORS - for sensor, description in VACUUM_SENSORS.items(): + if config_entry.data[CONF_MODEL] in MODELS_VACUUM_WITH_SEPARATE_MOP: + sensors = VACUUM_SENSORS_SEPARATE_MOP + + for sensor, description in sensors.items(): parent_key_data = getattr(coordinator.data, description.parent_key) if getattr(parent_key_data, description.key, None) is None: _LOGGER.debug( diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index c140ad526e2..0ed70c8aee5 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -223,6 +223,9 @@ MODELS_VACUUM_WITH_MOP = [ ROCKROBO_S6_PURE, ROCKROBO_S7, ] +MODELS_VACUUM_WITH_SEPARATE_MOP = [ + ROCKROBO_S7, +] MODELS_AIR_MONITOR = [ MODEL_AIRQUALITYMONITOR_V1, From af28d927b4c21caa6f6fcc18e165b1284af23459 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Wed, 3 Nov 2021 17:28:11 +0100 Subject: [PATCH 104/174] Fix timedelta-based sensors for xiaomi_miio (#58995) --- homeassistant/components/xiaomi_miio/device.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/device.py b/homeassistant/components/xiaomi_miio/device.py index 6d56c7c44bd..cb708fd4379 100644 --- a/homeassistant/components/xiaomi_miio/device.py +++ b/homeassistant/components/xiaomi_miio/device.py @@ -166,8 +166,7 @@ class XiaomiCoordinatedMiioEntity(CoordinatorEntity): return cls._parse_datetime_time(value) if isinstance(value, datetime.datetime): return cls._parse_datetime_datetime(value) - if isinstance(value, datetime.timedelta): - return cls._parse_time_delta(value) + if value is None: _LOGGER.debug("Attribute %s is None, this is unexpected", attribute) @@ -175,7 +174,7 @@ class XiaomiCoordinatedMiioEntity(CoordinatorEntity): @staticmethod def _parse_time_delta(timedelta: datetime.timedelta) -> int: - return timedelta.seconds + return int(timedelta.total_seconds()) @staticmethod def _parse_datetime_time(time: datetime.time) -> str: @@ -191,7 +190,3 @@ class XiaomiCoordinatedMiioEntity(CoordinatorEntity): @staticmethod def _parse_datetime_datetime(time: datetime.datetime) -> str: return time.isoformat() - - @staticmethod - def _parse_datetime_timedelta(time: datetime.timedelta) -> int: - return time.seconds From dcf6004166d79ce486e42a55f90c4a5cd2b79e72 Mon Sep 17 00:00:00 2001 From: Eugenio Panadero Date: Thu, 4 Nov 2021 05:32:16 +0100 Subject: [PATCH 105/174] Bump aiopvpc to 2.2.1 (#59008) happening because some config change in the ESIOS API server, solved with a version patch in aiopvpc (details in https://github.com/azogue/aiopvpc/pull/28) --- homeassistant/components/pvpc_hourly_pricing/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/pvpc_hourly_pricing/manifest.json b/homeassistant/components/pvpc_hourly_pricing/manifest.json index 612376a7931..a30ae1e2732 100644 --- a/homeassistant/components/pvpc_hourly_pricing/manifest.json +++ b/homeassistant/components/pvpc_hourly_pricing/manifest.json @@ -3,7 +3,7 @@ "name": "Spain electricity hourly pricing (PVPC)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/pvpc_hourly_pricing", - "requirements": ["aiopvpc==2.2.0"], + "requirements": ["aiopvpc==2.2.1"], "codeowners": ["@azogue"], "quality_scale": "platinum", "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index fdf88bf284b..2fcd6b2e324 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -234,7 +234,7 @@ aiopulse==0.4.2 aiopvapi==1.6.14 # homeassistant.components.pvpc_hourly_pricing -aiopvpc==2.2.0 +aiopvpc==2.2.1 # homeassistant.components.webostv aiopylgtv==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 148bd558d7e..ff1b98bff57 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -161,7 +161,7 @@ aiopulse==0.4.2 aiopvapi==1.6.14 # homeassistant.components.pvpc_hourly_pricing -aiopvpc==2.2.0 +aiopvpc==2.2.1 # homeassistant.components.webostv aiopylgtv==0.4.0 From c4aa6af953f64f1886f06c315dbe51a0d8539645 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Thu, 4 Nov 2021 14:14:31 +0100 Subject: [PATCH 106/174] Accept all roborock vacuum models for xiaomi_miio (#59018) --- homeassistant/components/xiaomi_miio/const.py | 2 + .../xiaomi_miio/test_config_flow.py | 46 +++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index 0ed70c8aee5..60c16a0717a 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -202,6 +202,7 @@ ROCKROBO_S4_MAX = "roborock.vacuum.a19" ROCKROBO_S5_MAX = "roborock.vacuum.s5e" ROCKROBO_S6_PURE = "roborock.vacuum.a08" ROCKROBO_E2 = "roborock.vacuum.e2" +ROCKROBO_GENERIC = "roborock.vacuum" MODELS_VACUUM = [ ROCKROBO_V1, ROCKROBO_E2, @@ -213,6 +214,7 @@ MODELS_VACUUM = [ ROCKROBO_S6_MAXV, ROCKROBO_S6_PURE, ROCKROBO_S7, + ROCKROBO_GENERIC, ] MODELS_VACUUM_WITH_MOP = [ ROCKROBO_E2, diff --git a/tests/components/xiaomi_miio/test_config_flow.py b/tests/components/xiaomi_miio/test_config_flow.py index 24aa5ac04e4..206da7ad4ae 100644 --- a/tests/components/xiaomi_miio/test_config_flow.py +++ b/tests/components/xiaomi_miio/test_config_flow.py @@ -695,6 +695,52 @@ async def config_flow_device_success(hass, model_to_test): } +async def config_flow_generic_roborock(hass): + """Test a successful config flow for a generic roborock vacuum.""" + DUMMY_MODEL = "roborock.vacuum.dummy" + + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "cloud" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {const.CONF_MANUAL: True}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "manual" + assert result["errors"] == {} + + mock_info = get_mock_info(model=DUMMY_MODEL) + + with patch( + "homeassistant.components.xiaomi_miio.device.Device.info", + return_value=mock_info, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN}, + ) + + assert result["type"] == "create_entry" + assert result["title"] == DUMMY_MODEL + assert result["data"] == { + const.CONF_FLOW_TYPE: const.CONF_DEVICE, + const.CONF_CLOUD_USERNAME: None, + const.CONF_CLOUD_PASSWORD: None, + const.CONF_CLOUD_COUNTRY: None, + CONF_HOST: TEST_HOST, + CONF_TOKEN: TEST_TOKEN, + const.CONF_MODEL: DUMMY_MODEL, + const.CONF_MAC: TEST_MAC, + } + + async def zeroconf_device_success(hass, zeroconf_name_to_test, model_to_test): """Test a successful zeroconf discovery of a device (base class).""" result = await hass.config_entries.flow.async_init( From b125e2c4251e6577caafb5c56cb7a903fca4eba8 Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Thu, 4 Nov 2021 14:26:17 +0100 Subject: [PATCH 107/174] Fix Nut resources option migration (#59020) Co-authored-by: Martin Hjelmare --- homeassistant/components/nut/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/nut/__init__.py b/homeassistant/components/nut/__init__.py index 81dd5f91f59..f0f2777373a 100644 --- a/homeassistant/components/nut/__init__.py +++ b/homeassistant/components/nut/__init__.py @@ -39,8 +39,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # strip out the stale options CONF_RESOURCES if CONF_RESOURCES in entry.options: + new_data = {**entry.data, CONF_RESOURCES: entry.options[CONF_RESOURCES]} new_options = {k: v for k, v in entry.options.items() if k != CONF_RESOURCES} - hass.config_entries.async_update_entry(entry, options=new_options) + hass.config_entries.async_update_entry( + entry, data=new_data, options=new_options + ) config = entry.data host = config[CONF_HOST] From 6e08cb815ba87a4172bdc485b2176bd9a3df760b Mon Sep 17 00:00:00 2001 From: Glenn Waters Date: Thu, 4 Nov 2021 23:11:22 -0400 Subject: [PATCH 108/174] Environment Canada config_flow fix (#59029) --- .../environment_canada/config_flow.py | 11 +++++----- .../environment_canada/test_config_flow.py | 20 ++++++++++++++++++- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/environment_canada/config_flow.py b/homeassistant/components/environment_canada/config_flow.py index e1eda36c345..683f312d9d5 100644 --- a/homeassistant/components/environment_canada/config_flow.py +++ b/homeassistant/components/environment_canada/config_flow.py @@ -20,13 +20,12 @@ async def validate_input(data): lat = data.get(CONF_LATITUDE) lon = data.get(CONF_LONGITUDE) station = data.get(CONF_STATION) - lang = data.get(CONF_LANGUAGE) + lang = data.get(CONF_LANGUAGE).lower() - weather_data = ECWeather( - station_id=station, - coordinates=(lat, lon), - language=lang.lower(), - ) + if station: + weather_data = ECWeather(station_id=station, language=lang) + else: + weather_data = ECWeather(coordinates=(lat, lon), language=lang) await weather_data.update() if lat is None or lon is None: diff --git a/tests/components/environment_canada/test_config_flow.py b/tests/components/environment_canada/test_config_flow.py index f2ebb48346c..2614778f9b4 100644 --- a/tests/components/environment_canada/test_config_flow.py +++ b/tests/components/environment_canada/test_config_flow.py @@ -123,7 +123,24 @@ async def test_exception_handling(hass, error): assert result["errors"] == {"base": base_error} -async def test_lat_or_lon_not_specified(hass): +async def test_import_station_not_specified(hass): + """Test that the import step works.""" + with mocked_ec(), patch( + "homeassistant.components.environment_canada.async_setup_entry", + return_value=True, + ): + fake_config = dict(FAKE_CONFIG) + del fake_config[CONF_STATION] + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=fake_config + ) + await hass.async_block_till_done() + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"] == FAKE_CONFIG + assert result["title"] == FAKE_TITLE + + +async def test_import_lat_lon_not_specified(hass): """Test that the import step works.""" with mocked_ec(), patch( "homeassistant.components.environment_canada.async_setup_entry", @@ -131,6 +148,7 @@ async def test_lat_or_lon_not_specified(hass): ): fake_config = dict(FAKE_CONFIG) del fake_config[CONF_LATITUDE] + del fake_config[CONF_LONGITUDE] result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=fake_config ) From 58d88c8371e9b251617e0ffa53c1641c760a5c83 Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Thu, 4 Nov 2021 12:27:41 +0100 Subject: [PATCH 109/174] Bump velbus-aio to 2021.11.0 (#59040) --- homeassistant/components/velbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json index 4541b428a4a..454b9d24d07 100644 --- a/homeassistant/components/velbus/manifest.json +++ b/homeassistant/components/velbus/manifest.json @@ -2,7 +2,7 @@ "domain": "velbus", "name": "Velbus", "documentation": "https://www.home-assistant.io/integrations/velbus", - "requirements": ["velbus-aio==2021.10.7"], + "requirements": ["velbus-aio==2021.11.0"], "config_flow": true, "codeowners": ["@Cereal2nd", "@brefra"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 2fcd6b2e324..70814af0f09 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2360,7 +2360,7 @@ uvcclient==0.11.0 vallox-websocket-api==2.8.1 # homeassistant.components.velbus -velbus-aio==2021.10.7 +velbus-aio==2021.11.0 # homeassistant.components.venstar venstarcolortouch==0.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ff1b98bff57..ff3e4092640 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1364,7 +1364,7 @@ url-normalize==1.4.1 uvcclient==0.11.0 # homeassistant.components.velbus -velbus-aio==2021.10.7 +velbus-aio==2021.11.0 # homeassistant.components.venstar venstarcolortouch==0.14 From 433743b0d1627a7c9934bedde4623057d1be5d56 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 4 Nov 2021 11:21:30 +0100 Subject: [PATCH 110/174] Constrain urllib3 to >=1.26.5 (#59043) --- homeassistant/package_constraints.txt | 4 ++-- script/gen_requirements_all.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7d670da646c..2dd084fc5fc 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -36,8 +36,8 @@ zeroconf==0.36.11 pycryptodome>=3.6.6 -# Constrain urllib3 to ensure we deal with CVE-2019-11236 & CVE-2019-11324 -urllib3>=1.24.3 +# Constrain urllib3 to ensure we deal with CVE-2020-26137 and CVE-2021-33503 +urllib3>=1.26.5 # Constrain H11 to ensure we get a new enough version to support non-rfc line endings h11>=0.12.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 3d2ace4c240..3deec512b4f 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -63,8 +63,8 @@ CONSTRAINT_PATH = os.path.join( CONSTRAINT_BASE = """ pycryptodome>=3.6.6 -# Constrain urllib3 to ensure we deal with CVE-2019-11236 & CVE-2019-11324 -urllib3>=1.24.3 +# Constrain urllib3 to ensure we deal with CVE-2020-26137 and CVE-2021-33503 +urllib3>=1.26.5 # Constrain H11 to ensure we get a new enough version to support non-rfc line endings h11>=0.12.0 From 543381b6f2dad365f551c83cae9c30ed596cfb7d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 4 Nov 2021 10:29:10 +0100 Subject: [PATCH 111/174] Correct migration to recorder schema 22 (#59048) --- .../components/recorder/migration.py | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 508476e0c2b..fe3e1aeb84d 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -12,6 +12,7 @@ from sqlalchemy.exc import ( SQLAlchemyError, ) from sqlalchemy.schema import AddConstraint, DropConstraint +from sqlalchemy.sql.expression import true from .models import ( SCHEMA_VERSION, @@ -24,7 +25,7 @@ from .models import ( StatisticsShortTerm, process_timestamp, ) -from .statistics import get_metadata_with_session, get_start_time +from .statistics import get_start_time from .util import session_scope _LOGGER = logging.getLogger(__name__) @@ -558,21 +559,25 @@ def _apply_update(instance, session, new_version, old_version): # noqa: C901 session.add(StatisticsRuns(start=fake_start_time)) fake_start_time += timedelta(minutes=5) - # Copy last hourly statistic to the newly created 5-minute statistics table - sum_statistics = get_metadata_with_session( - instance.hass, session, statistic_type="sum" - ) - for metadata_id, _ in sum_statistics.values(): + # When querying the database, be careful to only explicitly query for columns + # which were present in schema version 21. If querying the table, SQLAlchemy + # will refer to future columns. + for sum_statistic in session.query(StatisticsMeta.id).filter_by(has_sum=true()): last_statistic = ( - session.query(Statistics) - .filter_by(metadata_id=metadata_id) + session.query( + Statistics.start, + Statistics.last_reset, + Statistics.state, + Statistics.sum, + ) + .filter_by(metadata_id=sum_statistic.id) .order_by(Statistics.start.desc()) .first() ) if last_statistic: session.add( StatisticsShortTerm( - metadata_id=last_statistic.metadata_id, + metadata_id=sum_statistic.id, start=last_statistic.start, last_reset=last_statistic.last_reset, state=last_statistic.state, From c6d651e283d775ed52d3d50024a4926afa7b2fd0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 4 Nov 2021 11:06:17 +0100 Subject: [PATCH 112/174] Increase time to authorize OctoPrint (#59051) --- homeassistant/components/octoprint/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/octoprint/config_flow.py b/homeassistant/components/octoprint/config_flow.py index acc1449bd96..04efde16952 100644 --- a/homeassistant/components/octoprint/config_flow.py +++ b/homeassistant/components/octoprint/config_flow.py @@ -189,7 +189,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: user_input[CONF_API_KEY] = await octoprint.request_app_key( - "Home Assistant", user_input[CONF_USERNAME], 30 + "Home Assistant", user_input[CONF_USERNAME], 300 ) finally: # Continue the flow after show progress when the task is done. From c3882d07826c9405f71af4b040032c4c74426854 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Thu, 4 Nov 2021 14:25:07 +0100 Subject: [PATCH 113/174] Remove use_time sensor from mjjsq humidifers (#59066) --- homeassistant/components/xiaomi_miio/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index f818a809a5c..aefeea7c20a 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -299,7 +299,7 @@ HUMIDIFIER_MIOT_SENSORS = ( ATTR_USE_TIME, ATTR_WATER_LEVEL, ) -HUMIDIFIER_MJJSQ_SENSORS = (ATTR_HUMIDITY, ATTR_TEMPERATURE, ATTR_USE_TIME) +HUMIDIFIER_MJJSQ_SENSORS = (ATTR_HUMIDITY, ATTR_TEMPERATURE) PURIFIER_MIIO_SENSORS = ( ATTR_FILTER_LIFE_REMAINING, From d9d8b538b0c134dcaa1004bf93ef24ebf96e823d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 4 Nov 2021 16:34:35 +0100 Subject: [PATCH 114/174] Change minimum supported SQLite version to 3.31.0 (#59073) --- homeassistant/components/recorder/util.py | 4 ++-- tests/components/recorder/test_util.py | 14 +++++--------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index b4bbce27130..6c53347a536 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -49,7 +49,7 @@ MIN_VERSION_MARIA_DB_ROWNUM = AwesomeVersion("10.2.0", AwesomeVersionStrategy.SI MIN_VERSION_MYSQL = AwesomeVersion("8.0.0", AwesomeVersionStrategy.SIMPLEVER) MIN_VERSION_MYSQL_ROWNUM = AwesomeVersion("5.8.0", AwesomeVersionStrategy.SIMPLEVER) MIN_VERSION_PGSQL = AwesomeVersion("12.0", AwesomeVersionStrategy.SIMPLEVER) -MIN_VERSION_SQLITE = AwesomeVersion("3.32.1", AwesomeVersionStrategy.SIMPLEVER) +MIN_VERSION_SQLITE = AwesomeVersion("3.31.0", AwesomeVersionStrategy.SIMPLEVER) MIN_VERSION_SQLITE_ROWNUM = AwesomeVersion("3.25.0", AwesomeVersionStrategy.SIMPLEVER) # This is the maximum time after the recorder ends the session @@ -295,7 +295,7 @@ def _warn_unsupported_dialect(dialect): "Starting with Home Assistant 2022.2 this will prevent the recorder from " "starting. Please migrate your database to a supported software before then", dialect, - "MariaDB ≥ 10.3, MySQL ≥ 8.0, PostgreSQL ≥ 12, SQLite ≥ 3.32.1", + "MariaDB ≥ 10.3, MySQL ≥ 8.0, PostgreSQL ≥ 12, SQLite ≥ 3.31.0", ) diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index da88d17f02a..940925c48ca 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -364,20 +364,16 @@ def test_supported_pgsql(caplog, pgsql_version): "sqlite_version,message", [ ( - "3.32.0", - "Version 3.32.0 of SQLite is not supported; minimum supported version is 3.32.1.", - ), - ( - "3.31.0", - "Version 3.31.0 of SQLite is not supported; minimum supported version is 3.32.1.", + "3.30.0", + "Version 3.30.0 of SQLite is not supported; minimum supported version is 3.31.0.", ), ( "2.0.0", - "Version 2.0.0 of SQLite is not supported; minimum supported version is 3.32.1.", + "Version 2.0.0 of SQLite is not supported; minimum supported version is 3.31.0.", ), ( "dogs", - "Version dogs of SQLite is not supported; minimum supported version is 3.32.1.", + "Version dogs of SQLite is not supported; minimum supported version is 3.31.0.", ), ], ) @@ -410,7 +406,7 @@ def test_warn_outdated_sqlite(caplog, sqlite_version, message): @pytest.mark.parametrize( "sqlite_version", [ - ("3.32.1"), + ("3.31.0"), ("3.33.0"), ], ) From 61918e0e44c748e2eb0a802b710b4ef7666f56c7 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 4 Nov 2021 18:35:43 +0100 Subject: [PATCH 115/174] Correct rescheduling of ExternalStatisticsTask (#59076) --- homeassistant/components/recorder/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 465209c7ed7..9d19e0d876e 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -793,7 +793,7 @@ class Recorder(threading.Thread): if statistics.add_external_statistics(self, metadata, stats): return # Schedule a new statistics task if this one didn't finish - self.queue.put(StatisticsTask(metadata, stats)) + self.queue.put(ExternalStatisticsTask(metadata, stats)) def _process_one_event(self, event): """Process one event.""" From 039e361bffbd0df149dc43a393f2b7c155b86c51 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 Nov 2021 22:10:47 -0500 Subject: [PATCH 116/174] Bump flux_led to 0.24.14 (#59121) --- homeassistant/components/flux_led/__init__.py | 2 +- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/flux_led/__init__.py b/homeassistant/components/flux_led/__init__.py index 248ce7261e9..717a3c9e2b0 100644 --- a/homeassistant/components/flux_led/__init__.py +++ b/homeassistant/components/flux_led/__init__.py @@ -182,7 +182,7 @@ class FluxLedUpdateCoordinator(DataUpdateCoordinator): hass, _LOGGER, name=self.device.ipaddr, - update_interval=timedelta(seconds=5), + update_interval=timedelta(seconds=10), # We don't want an immediate refresh since the device # takes a moment to reflect the state change request_refresh_debouncer=Debouncer( diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 4b5a63542c9..d0134d07f79 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.24.13"], + "requirements": ["flux_led==0.24.14"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 70814af0f09..c5f93ad7251 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -652,7 +652,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.13 +flux_led==0.24.14 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ff3e4092640..fdacbc25dd2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -387,7 +387,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.13 +flux_led==0.24.14 # homeassistant.components.homekit fnvhash==0.1.0 From 189677c7136d42bdf1f3bf350616897fae5ac48c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 4 Nov 2021 20:14:07 -0700 Subject: [PATCH 117/174] Bumped version to 2021.11.1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 9eade8bd476..36b43ee25f1 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 11 -PATCH_VERSION: Final = "0" +PATCH_VERSION: Final = "1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From c150a296d264c9ede0975eeee9ba7ec4bfd9f306 Mon Sep 17 00:00:00 2001 From: Austin Mroczek Date: Sun, 7 Nov 2021 15:30:34 -0800 Subject: [PATCH 118/174] Bump total_connect_client to 2021.11.2 (#58818) * update total_connect_client to 2021.10 * update for total_connect_client changes * remove unused return value * bump total_connect_client to 2021.11.1 * bump total_connect_client to 2021.11.2 * Move to public ResultCode * load locations to prevent 'unknown error occurred' * add test for zero locations * Revert "load locations to prevent 'unknown error occurred'" This reverts commit 28b8984be5b1c8839fc8077d8d59bdba97eacc38. * Revert "add test for zero locations" This reverts commit 77bf7908d508d539d6165fc986930b041b13ca97. --- .../components/totalconnect/__init__.py | 4 +--- .../components/totalconnect/config_flow.py | 4 ++-- .../components/totalconnect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/totalconnect/common.py | 22 +++++++++---------- .../totalconnect/test_config_flow.py | 8 +++---- tests/components/totalconnect/test_init.py | 2 +- 8 files changed, 21 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/totalconnect/__init__.py b/homeassistant/components/totalconnect/__init__.py index bd1f693fd07..8acc7801de8 100644 --- a/homeassistant/components/totalconnect/__init__.py +++ b/homeassistant/components/totalconnect/__init__.py @@ -38,7 +38,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: TotalConnectClient, username, password, usercodes ) - if not client.is_valid_credentials(): + if not client.is_logged_in(): raise ConfigEntryAuthFailed("TotalConnect authentication failed") coordinator = TotalConnectDataUpdateCoordinator(hass, client) @@ -88,5 +88,3 @@ class TotalConnectDataUpdateCoordinator(DataUpdateCoordinator): raise UpdateFailed(exception) from exception except ValueError as exception: raise UpdateFailed("Unknown state from TotalConnect") from exception - - return True diff --git a/homeassistant/components/totalconnect/config_flow.py b/homeassistant/components/totalconnect/config_flow.py index 7ba96f11a1e..f3550722de5 100644 --- a/homeassistant/components/totalconnect/config_flow.py +++ b/homeassistant/components/totalconnect/config_flow.py @@ -40,7 +40,7 @@ class TotalConnectConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): TotalConnectClient, username, password, None ) - if client.is_valid_credentials(): + if client.is_logged_in(): # username/password valid so show user locations self.username = username self.password = password @@ -136,7 +136,7 @@ class TotalConnectConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.usercodes, ) - if not client.is_valid_credentials(): + if not client.is_logged_in(): errors["base"] = "invalid_auth" return self.async_show_form( step_id="reauth_confirm", diff --git a/homeassistant/components/totalconnect/manifest.json b/homeassistant/components/totalconnect/manifest.json index 25fb2fd2c75..c70eafd9c31 100644 --- a/homeassistant/components/totalconnect/manifest.json +++ b/homeassistant/components/totalconnect/manifest.json @@ -2,7 +2,7 @@ "domain": "totalconnect", "name": "Total Connect", "documentation": "https://www.home-assistant.io/integrations/totalconnect", - "requirements": ["total_connect_client==2021.8.3"], + "requirements": ["total_connect_client==2021.11.2"], "dependencies": [], "codeowners": ["@austinmroczek"], "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index c5f93ad7251..452d5482578 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2314,7 +2314,7 @@ todoist-python==8.0.0 toonapi==0.2.1 # homeassistant.components.totalconnect -total_connect_client==2021.8.3 +total_connect_client==2021.11.2 # homeassistant.components.tplink_lte tp-connected==0.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fdacbc25dd2..6a5231386b8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1330,7 +1330,7 @@ tesla-powerwall==0.3.12 toonapi==0.2.1 # homeassistant.components.totalconnect -total_connect_client==2021.8.3 +total_connect_client==2021.11.2 # homeassistant.components.transmission transmissionrpc==0.11 diff --git a/tests/components/totalconnect/common.py b/tests/components/totalconnect/common.py index 29278d33273..ec0c182895f 100644 --- a/tests/components/totalconnect/common.py +++ b/tests/components/totalconnect/common.py @@ -1,9 +1,7 @@ """Common methods used across tests for TotalConnect.""" from unittest.mock import patch -from total_connect_client.client import TotalConnectClient -from total_connect_client.const import ArmingState -from total_connect_client.zone import ZoneStatus, ZoneType +from total_connect_client import ArmingState, ResultCode, ZoneStatus, ZoneType from homeassistant.components.totalconnect.const import CONF_USERCODES, DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME @@ -44,7 +42,7 @@ USER = { } RESPONSE_AUTHENTICATE = { - "ResultCode": TotalConnectClient.SUCCESS, + "ResultCode": ResultCode.SUCCESS.value, "SessionID": 1, "Locations": LOCATIONS, "ModuleFlags": MODULE_FLAGS, @@ -52,7 +50,7 @@ RESPONSE_AUTHENTICATE = { } RESPONSE_AUTHENTICATE_FAILED = { - "ResultCode": TotalConnectClient.BAD_USER_OR_PASSWORD, + "ResultCode": ResultCode.BAD_USER_OR_PASSWORD.value, "ResultData": "test bad authentication", } @@ -255,18 +253,18 @@ RESPONSE_UNKNOWN = { "ArmingState": ArmingState.DISARMED, } -RESPONSE_ARM_SUCCESS = {"ResultCode": TotalConnectClient.ARM_SUCCESS} -RESPONSE_ARM_FAILURE = {"ResultCode": TotalConnectClient.COMMAND_FAILED} -RESPONSE_DISARM_SUCCESS = {"ResultCode": TotalConnectClient.DISARM_SUCCESS} +RESPONSE_ARM_SUCCESS = {"ResultCode": ResultCode.ARM_SUCCESS.value} +RESPONSE_ARM_FAILURE = {"ResultCode": ResultCode.COMMAND_FAILED.value} +RESPONSE_DISARM_SUCCESS = {"ResultCode": ResultCode.DISARM_SUCCESS.value} RESPONSE_DISARM_FAILURE = { - "ResultCode": TotalConnectClient.COMMAND_FAILED, + "ResultCode": ResultCode.COMMAND_FAILED.value, "ResultData": "Command Failed", } RESPONSE_USER_CODE_INVALID = { - "ResultCode": TotalConnectClient.USER_CODE_INVALID, + "ResultCode": ResultCode.USER_CODE_INVALID.value, "ResultData": "testing user code invalid", } -RESPONSE_SUCCESS = {"ResultCode": TotalConnectClient.SUCCESS} +RESPONSE_SUCCESS = {"ResultCode": ResultCode.SUCCESS.value} USERNAME = "username@me.com" PASSWORD = "password" @@ -292,7 +290,7 @@ PARTITION_DETAILS_2 = { PARTITION_DETAILS = {"PartitionDetails": [PARTITION_DETAILS_1, PARTITION_DETAILS_2]} RESPONSE_PARTITION_DETAILS = { - "ResultCode": TotalConnectClient.SUCCESS, + "ResultCode": ResultCode.SUCCESS.value, "ResultData": "testing partition details", "PartitionsInfoList": PARTITION_DETAILS, } diff --git a/tests/components/totalconnect/test_config_flow.py b/tests/components/totalconnect/test_config_flow.py index 20497102c6d..a9debb26dd4 100644 --- a/tests/components/totalconnect/test_config_flow.py +++ b/tests/components/totalconnect/test_config_flow.py @@ -95,7 +95,7 @@ async def test_abort_if_already_setup(hass): with patch( "homeassistant.components.totalconnect.config_flow.TotalConnectClient" ) as client_mock: - client_mock.return_value.is_valid_credentials.return_value = True + client_mock.return_value.is_logged_in.return_value = True result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, @@ -111,7 +111,7 @@ async def test_login_failed(hass): with patch( "homeassistant.components.totalconnect.config_flow.TotalConnectClient" ) as client_mock: - client_mock.return_value.is_valid_credentials.return_value = False + client_mock.return_value.is_logged_in.return_value = False result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, @@ -143,7 +143,7 @@ async def test_reauth(hass): "homeassistant.components.totalconnect.async_setup_entry", return_value=True ): # first test with an invalid password - client_mock.return_value.is_valid_credentials.return_value = False + client_mock.return_value.is_logged_in.return_value = False result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "password"} @@ -153,7 +153,7 @@ async def test_reauth(hass): assert result["errors"] == {"base": "invalid_auth"} # now test with the password valid - client_mock.return_value.is_valid_credentials.return_value = True + client_mock.return_value.is_logged_in.return_value = True result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "password"} diff --git a/tests/components/totalconnect/test_init.py b/tests/components/totalconnect/test_init.py index 41cd8bbae90..f1797f840ab 100644 --- a/tests/components/totalconnect/test_init.py +++ b/tests/components/totalconnect/test_init.py @@ -22,7 +22,7 @@ async def test_reauth_started(hass): "homeassistant.components.totalconnect.TotalConnectClient", autospec=True, ) as mock_client: - mock_client.return_value.is_valid_credentials.return_value = False + mock_client.return_value.is_logged_in.return_value = False assert await async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() From 96c08df88371b24eb05e57b492ffb18091408dbb Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Sat, 6 Nov 2021 09:54:51 +0800 Subject: [PATCH 119/174] Adjust frag_duration setting in stream (#59135) --- homeassistant/components/stream/worker.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index 881614b04a3..e4be3168393 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -102,18 +102,18 @@ class SegmentBuffer: # The LL-HLS spec allows for a fragment's duration to be within the range [0.85x,1.0x] # of the part target duration. We use the frag_duration option to tell ffmpeg to try to # cut the fragments when they reach frag_duration. However, the resulting fragments can - # have variability in their durations and can end up being too short or too long. If - # there are two tracks, as in the case of a video feed with audio, the fragment cut seems - # to be done on the first track that crosses the desired threshold, and cutting on the - # audio track may result in a shorter video fragment than desired. Conversely, with a + # have variability in their durations and can end up being too short or too long. With a # video track with no audio, the discrete nature of frames means that the frame at the # end of a fragment will sometimes extend slightly beyond the desired frag_duration. - # Given this, our approach is to use a frag_duration near the upper end of the range for - # outputs with audio using a frag_duration at the lower end of the range for outputs with - # only video. + # If there are two tracks, as in the case of a video feed with audio, there is an added + # wrinkle as the fragment cut seems to be done on the first track that crosses the desired + # threshold, and cutting on the audio track may also result in a shorter video fragment + # than desired. + # Given this, our approach is to give ffmpeg a frag_duration somewhere in the middle + # of the range, hoping that the parts stay pretty well bounded, and we adjust the part + # durations a bit in the hls metadata so that everything "looks" ok. "frag_duration": str( - self._stream_settings.part_target_duration - * (98e4 if add_audio else 9e5) + self._stream_settings.part_target_duration * 9e5 ), } if self._stream_settings.ll_hls From 2309dd48c9a8951b2bcd4f6b5744361badd87a47 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 5 Nov 2021 10:27:52 -0500 Subject: [PATCH 120/174] Bump flux_led to 0.24.15 (#59159) - Changes: https://github.com/Danielhiversen/flux_led/compare/0.24.14...0.24.15 - Fixes color reporting for addressable devices --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index d0134d07f79..3a3b7b7b572 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.24.14"], + "requirements": ["flux_led==0.24.15"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 452d5482578..b25b8ccd0e7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -652,7 +652,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.14 +flux_led==0.24.15 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6a5231386b8..019e3191cf7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -387,7 +387,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.14 +flux_led==0.24.15 # homeassistant.components.homekit fnvhash==0.1.0 From e2337304942576487b6862decf4c80ed7d64d04f Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 5 Nov 2021 09:27:35 -0600 Subject: [PATCH 121/174] Bump aioguardian to 2021.11.0 (#59161) --- homeassistant/components/guardian/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/guardian/manifest.json b/homeassistant/components/guardian/manifest.json index baa7eb50e7a..44da7d01cc9 100644 --- a/homeassistant/components/guardian/manifest.json +++ b/homeassistant/components/guardian/manifest.json @@ -3,7 +3,7 @@ "name": "Elexa Guardian", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/guardian", - "requirements": ["aioguardian==1.0.8"], + "requirements": ["aioguardian==2021.11.0"], "zeroconf": ["_api._udp.local."], "codeowners": ["@bachya"], "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index b25b8ccd0e7..4d7ee13c7cd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -173,7 +173,7 @@ aioftp==0.12.0 aiogithubapi==21.8.0 # homeassistant.components.guardian -aioguardian==1.0.8 +aioguardian==2021.11.0 # homeassistant.components.harmony aioharmony==0.2.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 019e3191cf7..73f038ab11d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -115,7 +115,7 @@ aioesphomeapi==10.2.0 aioflo==0.4.1 # homeassistant.components.guardian -aioguardian==1.0.8 +aioguardian==2021.11.0 # homeassistant.components.harmony aioharmony==0.2.8 From 3d8ca26c006b9bb0a564d317c0f2d23820a255f0 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 7 Nov 2021 16:34:25 -0700 Subject: [PATCH 122/174] Guard against flaky SimpliSafe API calls (#59175) --- .../components/simplisafe/__init__.py | 19 ++++++++++++++++++- .../simplisafe/alarm_control_panel.py | 17 ----------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index b3bb850244e..7db92252780 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -103,9 +103,11 @@ ATTR_TIMESTAMP = "timestamp" DEFAULT_ENTITY_MODEL = "alarm_control_panel" DEFAULT_ENTITY_NAME = "Alarm Control Panel" +DEFAULT_REST_API_ERROR_COUNT = 2 DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) DEFAULT_SOCKET_MIN_RETRY = 15 + DISPATCHER_TOPIC_WEBSOCKET_EVENT = "simplisafe_websocket_event_{0}" EVENT_SIMPLISAFE_EVENT = "SIMPLISAFE_EVENT" @@ -556,6 +558,8 @@ class SimpliSafeEntity(CoordinatorEntity): assert simplisafe.coordinator super().__init__(simplisafe.coordinator) + self._rest_api_errors = 0 + if device: model = device.type.name device_name = device.name @@ -618,11 +622,24 @@ class SimpliSafeEntity(CoordinatorEntity): else: system_offline = False - return super().available and self._online and not system_offline + return ( + self._rest_api_errors < DEFAULT_REST_API_ERROR_COUNT + and self._online + and not system_offline + ) @callback def _handle_coordinator_update(self) -> None: """Update the entity with new REST API data.""" + # SimpliSafe can incorrectly return an error state when there isn't any + # error. This can lead to the system having an unknown state frequently. + # To protect against that, we measure how many "error states" we receive + # and only alter the state if we detect a few in a row: + if self.coordinator.last_update_success: + self._rest_api_errors = 0 + else: + self._rest_api_errors += 1 + self.async_update_from_rest_api() self.async_write_ha_state() diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py index bc2e2a8ac74..5d2ffdcbd98 100644 --- a/homeassistant/components/simplisafe/alarm_control_panel.py +++ b/homeassistant/components/simplisafe/alarm_control_panel.py @@ -72,8 +72,6 @@ ATTR_RF_JAMMING = "rf_jamming" ATTR_WALL_POWER_LEVEL = "wall_power_level" ATTR_WIFI_STRENGTH = "wifi_strength" -DEFAULT_ERRORS_TO_ACCOMMODATE = 2 - VOLUME_STRING_MAP = { VOLUME_HIGH: "high", VOLUME_LOW: "low", @@ -141,8 +139,6 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): additional_websocket_events=WEBSOCKET_EVENTS_TO_LISTEN_FOR, ) - self._errors = 0 - if code := self._simplisafe.entry.options.get(CONF_CODE): if code.isdigit(): self._attr_code_format = FORMAT_NUMBER @@ -249,19 +245,6 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): } ) - # SimpliSafe can incorrectly return an error state when there isn't any - # error. This can lead to the system having an unknown state frequently. - # To protect against that, we measure how many "error states" we receive - # and only alter the state if we detect a few in a row: - if self._system.state == SystemStates.error: - if self._errors > DEFAULT_ERRORS_TO_ACCOMMODATE: - self._attr_state = None - else: - self._errors += 1 - return - - self._errors = 0 - self._set_state_from_system_data() @callback From f47e64e218566b0941d1b131d56d87944c8c55b2 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 6 Nov 2021 14:10:58 -0600 Subject: [PATCH 123/174] Guard against missing data in ReCollect Waste (#59177) --- homeassistant/components/recollect_waste/sensor.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/recollect_waste/sensor.py b/homeassistant/components/recollect_waste/sensor.py index 06a96a3bb74..9b509061957 100644 --- a/homeassistant/components/recollect_waste/sensor.py +++ b/homeassistant/components/recollect_waste/sensor.py @@ -91,8 +91,13 @@ class ReCollectWasteSensor(CoordinatorEntity, SensorEntity): @callback def update_from_latest_data(self) -> None: """Update the state.""" - pickup_event = self.coordinator.data[0] - next_pickup_event = self.coordinator.data[1] + try: + pickup_event = self.coordinator.data[0] + next_pickup_event = self.coordinator.data[1] + except IndexError: + self._attr_native_value = None + self._attr_extra_state_attributes = {} + return self._attr_extra_state_attributes.update( { From 1cc8e688c352ec8a6eceade768059d8d43a189b3 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 5 Nov 2021 13:29:12 -0600 Subject: [PATCH 124/174] Change ReCollect Waste device class to date (#59180) --- .../components/recollect_waste/sensor.py | 23 ++++--------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/recollect_waste/sensor.py b/homeassistant/components/recollect_waste/sensor.py index 9b509061957..619a12a42f7 100644 --- a/homeassistant/components/recollect_waste/sensor.py +++ b/homeassistant/components/recollect_waste/sensor.py @@ -1,17 +1,11 @@ """Support for ReCollect Waste sensors.""" from __future__ import annotations -from datetime import date, datetime, time - from aiorecollect.client import PickupType from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_ATTRIBUTION, - CONF_FRIENDLY_NAME, - DEVICE_CLASS_TIMESTAMP, -) +from homeassistant.const import ATTR_ATTRIBUTION, CONF_FRIENDLY_NAME, DEVICE_CLASS_DATE from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -19,7 +13,6 @@ from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, ) -from homeassistant.util.dt import as_utc from .const import CONF_PLACE_ID, CONF_SERVICE_ID, DATA_COORDINATOR, DOMAIN @@ -47,12 +40,6 @@ def async_get_pickup_type_names( ] -@callback -def async_get_utc_midnight(target_date: date) -> datetime: - """Get UTC midnight for a given date.""" - return as_utc(datetime.combine(target_date, time(0))) - - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: @@ -64,7 +51,7 @@ async def async_setup_entry( class ReCollectWasteSensor(CoordinatorEntity, SensorEntity): """ReCollect Waste Sensor.""" - _attr_device_class = DEVICE_CLASS_TIMESTAMP + _attr_device_class = DEVICE_CLASS_DATE def __init__(self, coordinator: DataUpdateCoordinator, entry: ConfigEntry) -> None: """Initialize the sensor.""" @@ -108,9 +95,7 @@ class ReCollectWasteSensor(CoordinatorEntity, SensorEntity): ATTR_NEXT_PICKUP_TYPES: async_get_pickup_type_names( self._entry, next_pickup_event.pickup_types ), - ATTR_NEXT_PICKUP_DATE: async_get_utc_midnight( - next_pickup_event.date - ).isoformat(), + ATTR_NEXT_PICKUP_DATE: next_pickup_event.date.isoformat(), } ) - self._attr_native_value = async_get_utc_midnight(pickup_event.date).isoformat() + self._attr_native_value = pickup_event.date.isoformat() From f5d04de523e87efd8bd130a8265fda1e628e92f2 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sat, 6 Nov 2021 12:50:53 +0100 Subject: [PATCH 125/174] bump aioshelly to 1.0.4 (#59209) --- homeassistant/components/shelly/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index 09a046ee78d..389d31ac195 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -3,7 +3,7 @@ "name": "Shelly", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/shelly", - "requirements": ["aioshelly==1.0.2"], + "requirements": ["aioshelly==1.0.4"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 4d7ee13c7cd..040cf629c96 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -243,7 +243,7 @@ aiopylgtv==0.4.0 aiorecollect==1.0.8 # homeassistant.components.shelly -aioshelly==1.0.2 +aioshelly==1.0.4 # homeassistant.components.switcher_kis aioswitcher==2.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 73f038ab11d..0cb7f85628a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -170,7 +170,7 @@ aiopylgtv==0.4.0 aiorecollect==1.0.8 # homeassistant.components.shelly -aioshelly==1.0.2 +aioshelly==1.0.4 # homeassistant.components.switcher_kis aioswitcher==2.0.6 From a6ff89c3e6ab1663d761474da75f5bf5b4353dec Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 6 Nov 2021 07:34:10 -0500 Subject: [PATCH 126/174] Bump flux_led to 0.24.17 (#59211) * Bump flux_led to 0.24.16 - Changes: https://github.com/Danielhiversen/flux_led/compare/0.24.15...0.24.16 - Fixes turning on/off when device is out of sync internally (seen on 0x33 firmware 8) - Fixes #59190 * Bump to .17 to fix typing --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 3a3b7b7b572..429465a0f3c 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.24.15"], + "requirements": ["flux_led==0.24.17"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 040cf629c96..225bcd04786 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -652,7 +652,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.15 +flux_led==0.24.17 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0cb7f85628a..24741cf2a44 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -387,7 +387,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.15 +flux_led==0.24.17 # homeassistant.components.homekit fnvhash==0.1.0 From dcada92cef9c26d74ad1ae7f6072633e9266a510 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sat, 6 Nov 2021 16:46:51 +0100 Subject: [PATCH 127/174] Fix tradfri group reachable access (#59217) --- .../components/tradfri/base_class.py | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tradfri/base_class.py b/homeassistant/components/tradfri/base_class.py index a54c9f7d500..8a7cc6a2f4a 100644 --- a/homeassistant/components/tradfri/base_class.py +++ b/homeassistant/components/tradfri/base_class.py @@ -60,7 +60,6 @@ class TradfriBaseClass(Entity): """Initialize a device.""" self._api = handle_error(api) self._attr_name = device.name - self._attr_available = device.reachable self._device: Device = device self._device_control: BlindControl | LightControl | SocketControl | SignalRepeaterControl | AirPurifierControl | None = ( None @@ -105,7 +104,6 @@ class TradfriBaseClass(Entity): """Refresh the device data.""" self._device = device self._attr_name = device.name - self._attr_available = device.reachable if write_ha: self.async_write_ha_state() @@ -116,6 +114,16 @@ class TradfriBaseDevice(TradfriBaseClass): All devices should inherit from this class. """ + def __init__( + self, + device: Device, + api: Callable[[Command | list[Command]], Any], + gateway_id: str, + ) -> None: + """Initialize a device.""" + self._attr_available = device.reachable + super().__init__(device, api, gateway_id) + @property def device_info(self) -> DeviceInfo: """Return the device info.""" @@ -128,3 +136,11 @@ class TradfriBaseDevice(TradfriBaseClass): sw_version=info.firmware_version, via_device=(DOMAIN, self._gateway_id), ) + + def _refresh(self, device: Device, write_ha: bool = True) -> None: + """Refresh the device data.""" + # The base class _refresh cannot be used, because + # there are devices (group) that do not have .reachable + # so set _attr_available here and let the base class do the rest. + self._attr_available = device.reachable + super()._refresh(device, write_ha) From a4253ff54ebff24b71f971319947146da3c20ecc Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Tue, 9 Nov 2021 05:42:20 +0100 Subject: [PATCH 128/174] Increase timeout for fetching camera data on Synology DSM (#59237) --- homeassistant/components/synology_dsm/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index a8e35178be4..1801e96d2bd 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -233,7 +233,7 @@ async def async_setup_entry( # noqa: C901 surveillance_station = api.surveillance_station try: - async with async_timeout.timeout(10): + async with async_timeout.timeout(30): await hass.async_add_executor_job(surveillance_station.update) except SynologyDSMAPIErrorException as err: raise UpdateFailed(f"Error communicating with API: {err}") from err From f9fc92c36bdbbe43d24455785caac2bb6d558d1c Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sun, 7 Nov 2021 12:53:28 -0500 Subject: [PATCH 129/174] Add Battery sensor regardless if the battery_percent_remaining attribute is supported or not (#59264) --- homeassistant/components/zha/sensor.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index d7c3e0797b8..2281d5295bc 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -226,6 +226,22 @@ class Battery(Sensor): _unit = PERCENTAGE _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + @classmethod + def create_entity( + cls, + unique_id: str, + zha_device: ZhaDeviceType, + channels: list[ChannelType], + **kwargs, + ) -> ZhaEntity | None: + """Entity Factory. + + Unlike any other entity, PowerConfiguration cluster may not support + battery_percent_remaining attribute, but zha-device-handlers takes care of it + so create the entity regardless + """ + return cls(unique_id, zha_device, channels, **kwargs) + @staticmethod def formatter(value: int) -> int: """Return the state of the entity.""" From 8b7686f4f255244ba4b6a43ee98b2c3d79e33340 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sun, 7 Nov 2021 15:17:50 +0100 Subject: [PATCH 130/174] Fix condition for fritz integration (#59281) --- homeassistant/components/fritz/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 78b3f2073a7..d6df32fa931 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -370,7 +370,7 @@ class FritzBoxTools: device_reg = async_get(self.hass) device_list = async_entries_for_config_entry(device_reg, config_entry.entry_id) for device_entry in device_list: - if async_entries_for_device( + if not async_entries_for_device( entity_reg, device_entry.id, include_disabled_entities=True, From e1b8e2ded3b3d599094e5ef51caa6409aea9bdee Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 7 Nov 2021 23:17:15 +0100 Subject: [PATCH 131/174] Remove illuminance sensor (#59305) --- homeassistant/components/xiaomi_miio/sensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index aefeea7c20a..ac26bc97fce 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -374,7 +374,6 @@ AIRFRESH_SENSORS = ( ATTR_FILTER_LIFE_REMAINING, ATTR_FILTER_USE, ATTR_HUMIDITY, - ATTR_ILLUMINANCE_LUX, ATTR_PM25, ATTR_TEMPERATURE, ATTR_USE_TIME, From 250160f0076e45e8fd2abc242eb760e3a47964cc Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 8 Nov 2021 01:29:29 +0200 Subject: [PATCH 132/174] Revert "Use DeviceInfo in shelly (#58520)" (#59315) This reverts commit df6351f86b50451c7ecd8e70cfb5bbc97829cc73. --- homeassistant/components/shelly/entity.py | 26 +++++++++++++++-------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index e33a2d87317..0a610545180 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -282,9 +282,6 @@ class ShellyBlockEntity(entity.Entity): self.wrapper = wrapper self.block = block self._name = get_block_entity_name(wrapper.device, block) - self._attr_device_info = DeviceInfo( - connections={(device_registry.CONNECTION_NETWORK_MAC, wrapper.mac)} - ) @property def name(self) -> str: @@ -296,6 +293,13 @@ class ShellyBlockEntity(entity.Entity): """If device should be polled.""" return False + @property + def device_info(self) -> DeviceInfo: + """Device info.""" + return { + "connections": {(device_registry.CONNECTION_NETWORK_MAC, self.wrapper.mac)} + } + @property def available(self) -> bool: """Available.""" @@ -344,9 +348,9 @@ class ShellyRpcEntity(entity.Entity): self.wrapper = wrapper self.key = key self._attr_should_poll = False - self._attr_device_info = DeviceInfo( - connections={(device_registry.CONNECTION_NETWORK_MAC, wrapper.mac)} - ) + self._attr_device_info = { + "connections": {(device_registry.CONNECTION_NETWORK_MAC, wrapper.mac)} + } self._attr_unique_id = f"{wrapper.mac}-{key}" self._attr_name = get_rpc_entity_name(wrapper.device, key) @@ -490,15 +494,19 @@ class ShellyRestAttributeEntity(update_coordinator.CoordinatorEntity): self.description = description self._name = get_block_entity_name(wrapper.device, None, self.description.name) self._last_value = None - self._attr_device_info = DeviceInfo( - connections={(device_registry.CONNECTION_NETWORK_MAC, wrapper.mac)} - ) @property def name(self) -> str: """Name of sensor.""" return self._name + @property + def device_info(self) -> DeviceInfo: + """Device info.""" + return { + "connections": {(device_registry.CONNECTION_NETWORK_MAC, self.wrapper.mac)} + } + @property def entity_registry_enabled_default(self) -> bool: """Return if it should be enabled by default.""" From 0873c3e92b42d133b7b29d5fe824e27567078174 Mon Sep 17 00:00:00 2001 From: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com> Date: Mon, 8 Nov 2021 23:13:00 +0100 Subject: [PATCH 133/174] Support generic xiaomi_miio vacuums (#59317) * Support generic xiaomi_miio vacuums Signed-off-by: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com> * Fix lint Signed-off-by: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com> * Remove warning log Signed-off-by: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com> --- homeassistant/components/xiaomi_miio/__init__.py | 12 ++++++++++-- homeassistant/components/xiaomi_miio/const.py | 4 +++- homeassistant/components/xiaomi_miio/sensor.py | 10 ++++++++-- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index de5baf69683..0f36d884207 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -66,6 +66,8 @@ from .const import ( MODELS_PURIFIER_MIOT, MODELS_SWITCH, MODELS_VACUUM, + ROBOROCK_GENERIC, + ROCKROBO_GENERIC, AuthException, SetupException, ) @@ -267,7 +269,7 @@ async def async_create_miio_device_and_coordinator( hass: core.HomeAssistant, entry: config_entries.ConfigEntry ): """Set up a data coordinator and one miio device to service multiple entities.""" - model = entry.data[CONF_MODEL] + model: str = entry.data[CONF_MODEL] host = entry.data[CONF_HOST] token = entry.data[CONF_TOKEN] name = entry.title @@ -280,6 +282,8 @@ async def async_create_miio_device_and_coordinator( model not in MODELS_HUMIDIFIER and model not in MODELS_FAN and model not in MODELS_VACUUM + and not model.startswith(ROBOROCK_GENERIC) + and not model.startswith(ROCKROBO_GENERIC) ): return @@ -304,7 +308,11 @@ async def async_create_miio_device_and_coordinator( device = AirPurifier(host, token) elif model.startswith("zhimi.airfresh."): device = AirFresh(host, token) - elif model in MODELS_VACUUM: + elif ( + model in MODELS_VACUUM + or model.startswith(ROBOROCK_GENERIC) + or model.startswith(ROCKROBO_GENERIC) + ): device = Vacuum(host, token) update_method = _async_update_data_vacuum coordinator_class = DataUpdateCoordinator[VacuumCoordinatorData] diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index 60c16a0717a..8c83c8015b2 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -202,7 +202,8 @@ ROCKROBO_S4_MAX = "roborock.vacuum.a19" ROCKROBO_S5_MAX = "roborock.vacuum.s5e" ROCKROBO_S6_PURE = "roborock.vacuum.a08" ROCKROBO_E2 = "roborock.vacuum.e2" -ROCKROBO_GENERIC = "roborock.vacuum" +ROBOROCK_GENERIC = "roborock.vacuum" +ROCKROBO_GENERIC = "rockrobo.vacuum" MODELS_VACUUM = [ ROCKROBO_V1, ROCKROBO_E2, @@ -214,6 +215,7 @@ MODELS_VACUUM = [ ROCKROBO_S6_MAXV, ROCKROBO_S6_PURE, ROCKROBO_S7, + ROBOROCK_GENERIC, ROCKROBO_GENERIC, ] MODELS_VACUUM_WITH_MOP = [ diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index ac26bc97fce..0d67014ced9 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -81,6 +81,8 @@ from .const import ( MODELS_PURIFIER_MIIO, MODELS_PURIFIER_MIOT, MODELS_VACUUM, + ROBOROCK_GENERIC, + ROCKROBO_GENERIC, ) from .device import XiaomiCoordinatedMiioEntity, XiaomiMiioEntity from .gateway import XiaomiGatewayDevice @@ -592,7 +594,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): elif config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE: host = config_entry.data[CONF_HOST] token = config_entry.data[CONF_TOKEN] - model = config_entry.data[CONF_MODEL] + model: str = config_entry.data[CONF_MODEL] if model in (MODEL_FAN_ZA1, MODEL_FAN_ZA3, MODEL_FAN_ZA4, MODEL_FAN_P5): return @@ -624,7 +626,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): sensors = PURIFIER_MIIO_SENSORS elif model in MODELS_PURIFIER_MIOT: sensors = PURIFIER_MIOT_SENSORS - elif model in MODELS_VACUUM: + elif ( + model in MODELS_VACUUM + or model.startswith(ROBOROCK_GENERIC) + or model.startswith(ROCKROBO_GENERIC) + ): return _setup_vacuum_sensors(hass, config_entry, async_add_entities) for sensor, description in SENSOR_TYPES.items(): From 6d3e380f642a2a2990e67a96e0363aa18905d948 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 8 Nov 2021 18:16:44 +0100 Subject: [PATCH 134/174] Bump paho-mqtt to 1.6.1 (#59339) --- homeassistant/components/mqtt/manifest.json | 2 +- homeassistant/components/shiftr/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mqtt/manifest.json b/homeassistant/components/mqtt/manifest.json index c5d9ad21ed6..6fb81deeb4d 100644 --- a/homeassistant/components/mqtt/manifest.json +++ b/homeassistant/components/mqtt/manifest.json @@ -3,7 +3,7 @@ "name": "MQTT", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/mqtt", - "requirements": ["paho-mqtt==1.5.1"], + "requirements": ["paho-mqtt==1.6.1"], "dependencies": ["http"], "codeowners": ["@emontnemery"], "iot_class": "local_push" diff --git a/homeassistant/components/shiftr/manifest.json b/homeassistant/components/shiftr/manifest.json index f7f04eb5a86..fc475c2f48e 100644 --- a/homeassistant/components/shiftr/manifest.json +++ b/homeassistant/components/shiftr/manifest.json @@ -2,7 +2,7 @@ "domain": "shiftr", "name": "shiftr.io", "documentation": "https://www.home-assistant.io/integrations/shiftr", - "requirements": ["paho-mqtt==1.5.1"], + "requirements": ["paho-mqtt==1.6.1"], "codeowners": ["@fabaff"], "iot_class": "cloud_push" } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 2dd084fc5fc..10bb0ffbd3d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -19,7 +19,7 @@ home-assistant-frontend==20211103.0 httpx==0.19.0 ifaddr==0.1.7 jinja2==3.0.2 -paho-mqtt==1.5.1 +paho-mqtt==1.6.1 pillow==8.2.0 pip>=8.0.3,<20.3 pyserial==3.5 diff --git a/requirements_all.txt b/requirements_all.txt index 225bcd04786..c1176f5a1c2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1163,7 +1163,7 @@ p1monitor==1.0.0 # homeassistant.components.mqtt # homeassistant.components.shiftr -paho-mqtt==1.5.1 +paho-mqtt==1.6.1 # homeassistant.components.panasonic_bluray panacotta==0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 24741cf2a44..0b264c7446e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -689,7 +689,7 @@ p1monitor==1.0.0 # homeassistant.components.mqtt # homeassistant.components.shiftr -paho-mqtt==1.5.1 +paho-mqtt==1.6.1 # homeassistant.components.panasonic_viera panasonic_viera==0.3.6 From 0f4a35dd28f35975fcf21636776177423ebc9417 Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Mon, 8 Nov 2021 21:56:17 +0100 Subject: [PATCH 135/174] Bump velbusaio to 2021.11.6 (#59353) --- homeassistant/components/velbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json index 454b9d24d07..5fb3c58c3c7 100644 --- a/homeassistant/components/velbus/manifest.json +++ b/homeassistant/components/velbus/manifest.json @@ -2,7 +2,7 @@ "domain": "velbus", "name": "Velbus", "documentation": "https://www.home-assistant.io/integrations/velbus", - "requirements": ["velbus-aio==2021.11.0"], + "requirements": ["velbus-aio==2021.11.6"], "config_flow": true, "codeowners": ["@Cereal2nd", "@brefra"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index c1176f5a1c2..7aa98ebcee3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2360,7 +2360,7 @@ uvcclient==0.11.0 vallox-websocket-api==2.8.1 # homeassistant.components.velbus -velbus-aio==2021.11.0 +velbus-aio==2021.11.6 # homeassistant.components.venstar venstarcolortouch==0.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0b264c7446e..ceb27439fd3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1364,7 +1364,7 @@ url-normalize==1.4.1 uvcclient==0.11.0 # homeassistant.components.velbus -velbus-aio==2021.11.0 +velbus-aio==2021.11.6 # homeassistant.components.venstar venstarcolortouch==0.14 From a6d795fce1a0c16087073e56a6893c2cfe39a07e Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 8 Nov 2021 21:45:40 +0100 Subject: [PATCH 136/174] Update frontend to 20211108.0 (#59364) --- 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 c913de3367f..43e0e7e86bc 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20211103.0" + "home-assistant-frontend==20211108.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 10bb0ffbd3d..a290aaadd39 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==3.4.8 emoji==1.5.0 hass-nabucasa==0.50.0 -home-assistant-frontend==20211103.0 +home-assistant-frontend==20211108.0 httpx==0.19.0 ifaddr==0.1.7 jinja2==3.0.2 diff --git a/requirements_all.txt b/requirements_all.txt index 7aa98ebcee3..3cb1b05e7e2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -813,7 +813,7 @@ hole==0.5.1 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211103.0 +home-assistant-frontend==20211108.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ceb27439fd3..401e872c6d3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -500,7 +500,7 @@ hole==0.5.1 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211103.0 +home-assistant-frontend==20211108.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 4d62d41cc13179a7b17268784f9222de15bdec7c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 8 Nov 2021 20:48:00 -0800 Subject: [PATCH 137/174] Bumped version to 2021.11.2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 36b43ee25f1..dcd68543068 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 11 -PATCH_VERSION: Final = "1" +PATCH_VERSION: Final = "2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From 533684545222420f7330d80dcfb836988e4806a6 Mon Sep 17 00:00:00 2001 From: enegaard Date: Wed, 10 Nov 2021 08:14:16 +0100 Subject: [PATCH 138/174] Fix rpi_camera setup hanging on initialization (#59316) --- homeassistant/components/rpi_camera/camera.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rpi_camera/camera.py b/homeassistant/components/rpi_camera/camera.py index 980586d4def..89fe7fe55d8 100644 --- a/homeassistant/components/rpi_camera/camera.py +++ b/homeassistant/components/rpi_camera/camera.py @@ -119,10 +119,13 @@ class RaspberryCamera(Camera): cmd_args.append("-a") cmd_args.append(str(device_info[CONF_OVERLAY_TIMESTAMP])) - with subprocess.Popen( + # The raspistill process started below must run "forever" in + # the background until killed when Home Assistant is stopped. + # Therefore it must not be wrapped with "with", since that + # waits for the subprocess to exit before continuing. + subprocess.Popen( # pylint: disable=consider-using-with cmd_args, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT - ): - pass + ) def camera_image( self, width: int | None = None, height: int | None = None From aacc0edde756e67feb614f6ee3cd551ce78a2d08 Mon Sep 17 00:00:00 2001 From: Keilin Bickar Date: Thu, 11 Nov 2021 02:49:07 -0500 Subject: [PATCH 139/174] Fix state of sense net_production sensor (#59391) --- homeassistant/components/sense/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sense/sensor.py b/homeassistant/components/sense/sensor.py index a8695e32b57..08677cda8d0 100644 --- a/homeassistant/components/sense/sensor.py +++ b/homeassistant/components/sense/sensor.py @@ -1,7 +1,7 @@ """Support for monitoring a Sense energy sensor.""" from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + STATE_CLASS_TOTAL, SensorEntity, ) from homeassistant.const import ( @@ -251,7 +251,7 @@ class SenseTrendsSensor(CoordinatorEntity, SensorEntity): """Implementation of a Sense energy sensor.""" _attr_device_class = DEVICE_CLASS_ENERGY - _attr_state_class = STATE_CLASS_TOTAL_INCREASING + _attr_state_class = STATE_CLASS_TOTAL _attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} _attr_icon = ICON From 66c5d75fbb9bf101817dc3554db8c32be91d15f0 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 9 Nov 2021 23:40:21 +0100 Subject: [PATCH 140/174] Update frontend to 20211109.0 (#59451) --- 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 43e0e7e86bc..d2db6138171 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20211108.0" + "home-assistant-frontend==20211109.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a290aaadd39..1f29737cdf3 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==3.4.8 emoji==1.5.0 hass-nabucasa==0.50.0 -home-assistant-frontend==20211108.0 +home-assistant-frontend==20211109.0 httpx==0.19.0 ifaddr==0.1.7 jinja2==3.0.2 diff --git a/requirements_all.txt b/requirements_all.txt index 3cb1b05e7e2..9aef5a12b5b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -813,7 +813,7 @@ hole==0.5.1 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211108.0 +home-assistant-frontend==20211109.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 401e872c6d3..26d9eba1d2d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -500,7 +500,7 @@ hole==0.5.1 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211108.0 +home-assistant-frontend==20211109.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From c2f227bf1687ceb55c38dc06b08c899e4974fda6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 11 Nov 2021 00:31:08 -0600 Subject: [PATCH 141/174] Fix zeroconf with sonos v1 firmware (#59460) --- homeassistant/components/sonos/config_flow.py | 2 +- homeassistant/components/sonos/helpers.py | 7 ++- tests/components/sonos/test_config_flow.py | 50 +++++++++++++++++++ tests/components/sonos/test_helpers.py | 6 +++ 4 files changed, 63 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sonos/config_flow.py b/homeassistant/components/sonos/config_flow.py index 745d3db3890..98e1194ebd0 100644 --- a/homeassistant/components/sonos/config_flow.py +++ b/homeassistant/components/sonos/config_flow.py @@ -30,7 +30,7 @@ class SonosDiscoveryFlowHandler(DiscoveryFlowHandler): ) -> FlowResult: """Handle a flow initialized by zeroconf.""" hostname = discovery_info["hostname"] - if hostname is None or not hostname.startswith("Sonos-"): + if hostname is None or not hostname.lower().startswith("sonos"): return self.async_abort(reason="not_sonos_device") await self.async_set_unique_id(self._domain, raise_on_progress=False) host = discovery_info[CONF_HOST] diff --git a/homeassistant/components/sonos/helpers.py b/homeassistant/components/sonos/helpers.py index 01a75eb7747..490bcdefba5 100644 --- a/homeassistant/components/sonos/helpers.py +++ b/homeassistant/components/sonos/helpers.py @@ -44,5 +44,10 @@ def soco_error(errorcodes: list[str] | None = None) -> Callable: def hostname_to_uid(hostname: str) -> str: """Convert a Sonos hostname to a uid.""" - baseuid = hostname.split("-")[1].replace(".local.", "") + if hostname.startswith("Sonos-"): + baseuid = hostname.split("-")[1].replace(".local.", "") + elif hostname.startswith("sonos"): + baseuid = hostname[5:].replace(".local.", "") + else: + raise ValueError(f"{hostname} is not a sonos device.") return f"{UID_PREFIX}{baseuid}{UID_POSTFIX}" diff --git a/tests/components/sonos/test_config_flow.py b/tests/components/sonos/test_config_flow.py index 39f3966e2ce..7d6fd02f51d 100644 --- a/tests/components/sonos/test_config_flow.py +++ b/tests/components/sonos/test_config_flow.py @@ -75,6 +75,56 @@ async def test_zeroconf_form(hass: core.HomeAssistant): assert len(mock_manager.mock_calls) == 2 +async def test_zeroconf_sonos_v1(hass: core.HomeAssistant): + """Test we pass sonos devices to the discovery manager with v1 firmware devices.""" + + mock_manager = hass.data[DATA_SONOS_DISCOVERY_MANAGER] = MagicMock() + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data={ + "host": "192.168.1.107", + "port": 1443, + "hostname": "sonos5CAAFDE47AC8.local.", + "type": "_sonos._tcp.local.", + "name": "Sonos-5CAAFDE47AC8._sonos._tcp.local.", + "properties": { + "_raw": { + "info": b"/api/v1/players/RINCON_5CAAFDE47AC801400/info", + "vers": b"1", + "protovers": b"1.18.9", + }, + "info": "/api/v1/players/RINCON_5CAAFDE47AC801400/info", + "vers": "1", + "protovers": "1.18.9", + }, + }, + ) + assert result["type"] == "form" + assert result["errors"] is None + + with patch( + "homeassistant.components.sonos.async_setup", + return_value=True, + ) as mock_setup, patch( + "homeassistant.components.sonos.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "Sonos" + assert result2["data"] == {} + + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_manager.mock_calls) == 2 + + async def test_zeroconf_form_not_sonos(hass: core.HomeAssistant): """Test we abort on non-sonos devices.""" mock_manager = hass.data[DATA_SONOS_DISCOVERY_MANAGER] = MagicMock() diff --git a/tests/components/sonos/test_helpers.py b/tests/components/sonos/test_helpers.py index a52337f9455..be32d3a190b 100644 --- a/tests/components/sonos/test_helpers.py +++ b/tests/components/sonos/test_helpers.py @@ -1,9 +1,15 @@ """Test the sonos config flow.""" from __future__ import annotations +import pytest + from homeassistant.components.sonos.helpers import hostname_to_uid async def test_uid_to_hostname(): """Test we can convert a hostname to a uid.""" assert hostname_to_uid("Sonos-347E5C0CF1E3.local.") == "RINCON_347E5C0CF1E301400" + assert hostname_to_uid("sonos5CAAFDE47AC8.local.") == "RINCON_5CAAFDE47AC801400" + + with pytest.raises(ValueError): + assert hostname_to_uid("notsonos5CAAFDE47AC8.local.") From 9cb4a5ca39f1f052650f0ec5dc0bae51414083df Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Thu, 11 Nov 2021 06:31:56 +0000 Subject: [PATCH 142/174] Ignore None state in state_change_event (#59485) --- homeassistant/components/integration/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index a2fd77fb4e1..6c0e09af0c5 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -156,6 +156,7 @@ class IntegrationSensor(RestoreEntity, SensorEntity): if ( old_state is None + or new_state is None or old_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE) or new_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE) ): From 04e1dc3a106f4d87c85b0b52e9e7e44abc5e3401 Mon Sep 17 00:00:00 2001 From: Sergiy Maysak Date: Thu, 11 Nov 2021 03:21:29 +0200 Subject: [PATCH 143/174] Fix wirelesstag switch arm/disarm (#59515) --- homeassistant/components/wirelesstag/__init__.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/wirelesstag/__init__.py b/homeassistant/components/wirelesstag/__init__.py index 24afb6b0465..b7de56d8eff 100644 --- a/homeassistant/components/wirelesstag/__init__.py +++ b/homeassistant/components/wirelesstag/__init__.py @@ -73,16 +73,14 @@ class WirelessTagPlatform: def arm(self, switch): """Arm entity sensor monitoring.""" - func_name = f"arm_{switch.sensor_type}" - arm_func = getattr(self.api, func_name) - if arm_func is not None: + func_name = f"arm_{switch.entity_description.key}" + if (arm_func := getattr(self.api, func_name)) is not None: arm_func(switch.tag_id, switch.tag_manager_mac) def disarm(self, switch): """Disarm entity sensor monitoring.""" - func_name = f"disarm_{switch.sensor_type}" - disarm_func = getattr(self.api, func_name) - if disarm_func is not None: + func_name = f"disarm_{switch.entity_description.key}" + if (disarm_func := getattr(self.api, func_name)) is not None: disarm_func(switch.tag_id, switch.tag_manager_mac) def start_monitoring(self): From 0f0ca36aa8b05dbfb409a6570230a0cf01fb3f4f Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Fri, 12 Nov 2021 00:59:13 +0800 Subject: [PATCH 144/174] Remove incomplete segment on stream restart (#59532) --- homeassistant/components/stream/hls.py | 10 ++++++++ homeassistant/components/stream/worker.py | 5 ++++ tests/components/stream/conftest.py | 10 ++++---- tests/components/stream/test_hls.py | 30 +++++++++++++++++++++++ 4 files changed, 50 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/stream/hls.py b/homeassistant/components/stream/hls.py index e1a0e6a8f67..44b19d2cc85 100644 --- a/homeassistant/components/stream/hls.py +++ b/homeassistant/components/stream/hls.py @@ -77,6 +77,16 @@ class HlsStreamOutput(StreamOutput): or self.stream_settings.min_segment_duration ) + def discontinuity(self) -> None: + """Remove incomplete segment from deque.""" + self._hass.loop.call_soon_threadsafe(self._async_discontinuity) + + @callback + def _async_discontinuity(self) -> None: + """Remove incomplete segment from deque in event loop.""" + if self._segments and not self._segments[-1].complete: + self._segments.pop() + class HlsMasterPlaylistView(StreamView): """Stream view used only for Chromecast compatibility.""" diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index e4be3168393..a0ab48290f5 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -18,6 +18,7 @@ from .const import ( ATTR_SETTINGS, AUDIO_CODECS, DOMAIN, + HLS_PROVIDER, MAX_MISSING_DTS, MAX_TIMESTAMP_GAP, PACKETS_TO_WAIT_FOR_AUDIO, @@ -25,6 +26,7 @@ from .const import ( SOURCE_TIMEOUT, ) from .core import Part, Segment, StreamOutput, StreamSettings +from .hls import HlsStreamOutput _LOGGER = logging.getLogger(__name__) @@ -279,6 +281,9 @@ class SegmentBuffer: # the discontinuity sequence number. self._stream_id += 1 self._start_time = datetime.datetime.utcnow() + # Call discontinuity to remove incomplete segment from the HLS output + if hls_output := self._outputs_callback().get(HLS_PROVIDER): + cast(HlsStreamOutput, hls_output).discontinuity() def close(self) -> None: """Close stream buffer.""" diff --git a/tests/components/stream/conftest.py b/tests/components/stream/conftest.py index f5f66258f70..b5d68ffaba5 100644 --- a/tests/components/stream/conftest.py +++ b/tests/components/stream/conftest.py @@ -22,8 +22,8 @@ from aiohttp import web import async_timeout import pytest -from homeassistant.components.stream import Stream from homeassistant.components.stream.core import Segment, StreamOutput +from homeassistant.components.stream.worker import SegmentBuffer TEST_TIMEOUT = 7.0 # Lower than 9s home assistant timeout @@ -34,7 +34,7 @@ class WorkerSync: def __init__(self): """Initialize WorkerSync.""" self._event = None - self._original = Stream._worker_finished + self._original = SegmentBuffer.discontinuity def pause(self): """Pause the worker before it finalizes the stream.""" @@ -45,7 +45,7 @@ class WorkerSync: logging.debug("waking blocked worker") self._event.set() - def blocking_finish(self, stream: Stream): + def blocking_discontinuity(self, stream: SegmentBuffer): """Intercept call to pause stream worker.""" # Worker is ending the stream, which clears all output buffers. # Block the worker thread until the test has a chance to verify @@ -63,8 +63,8 @@ def stream_worker_sync(hass): """Patch StreamOutput to allow test to synchronize worker stream end.""" sync = WorkerSync() with patch( - "homeassistant.components.stream.Stream._worker_finished", - side_effect=sync.blocking_finish, + "homeassistant.components.stream.worker.SegmentBuffer.discontinuity", + side_effect=sync.blocking_discontinuity, autospec=True, ): yield sync diff --git a/tests/components/stream/test_hls.py b/tests/components/stream/test_hls.py index 07c8cc88a65..3bff13a936b 100644 --- a/tests/components/stream/test_hls.py +++ b/tests/components/stream/test_hls.py @@ -448,3 +448,33 @@ async def test_hls_max_segments_discontinuity(hass, hls_stream, stream_worker_sy stream_worker_sync.resume() stream.stop() + + +async def test_remove_incomplete_segment_on_exit(hass, stream_worker_sync): + """Test that the incomplete segment gets removed when the worker thread quits.""" + await async_setup_component(hass, "stream", {"stream": {}}) + + stream = create_stream(hass, STREAM_SOURCE, {}) + stream_worker_sync.pause() + stream.start() + hls = stream.add_provider(HLS_PROVIDER) + + segment = Segment(sequence=0, stream_id=0, duration=SEGMENT_DURATION) + hls.put(segment) + segment = Segment(sequence=1, stream_id=0, duration=SEGMENT_DURATION) + hls.put(segment) + segment = Segment(sequence=2, stream_id=0, duration=0) + hls.put(segment) + await hass.async_block_till_done() + + segments = hls._segments + assert len(segments) == 3 + assert not segments[-1].complete + stream_worker_sync.resume() + stream._thread_quit.set() + stream._thread.join() + stream._thread = None + await hass.async_block_till_done() + assert segments[-1].complete + assert len(segments) == 2 + stream.stop() From 4cae92a5337d3ad4b4e78e5b2a3152ec34aad132 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 11 Nov 2021 10:33:14 -0800 Subject: [PATCH 145/174] Bumped version to 2021.11.3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index dcd68543068..7b9d8e4165d 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 11 -PATCH_VERSION: Final = "2" +PATCH_VERSION: Final = "3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From 89877a0685d020eee1b3e9a2cc89816c7ba987c0 Mon Sep 17 00:00:00 2001 From: jugla <59493499+jugla@users.noreply.github.com> Date: Sat, 13 Nov 2021 16:44:18 +0100 Subject: [PATCH 146/174] Air visual : robustness at startup when evaluate time interval (#59544) --- homeassistant/components/airvisual/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index 72b063c9394..dacc1bc1e38 100644 --- a/homeassistant/components/airvisual/__init__.py +++ b/homeassistant/components/airvisual/__init__.py @@ -105,9 +105,10 @@ def async_get_cloud_coordinators_by_api_key( ) -> list[DataUpdateCoordinator]: """Get all DataUpdateCoordinator objects related to a particular API key.""" return [ - attrs[DATA_COORDINATOR] + coordinator for entry_id, attrs in hass.data[DOMAIN].items() if (entry := hass.config_entries.async_get_entry(entry_id)) + and (coordinator := attrs.get(DATA_COORDINATOR)) and entry.data.get(CONF_API_KEY) == api_key ] From 3c4d5e6c91f8a7b1fffe59234eeeb39dba5b4565 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 12 Nov 2021 10:26:17 +0100 Subject: [PATCH 147/174] Override api url in norway_air (#59573) --- homeassistant/components/met/manifest.json | 2 +- homeassistant/components/norway_air/air_quality.py | 6 +++++- homeassistant/components/norway_air/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/met/manifest.json b/homeassistant/components/met/manifest.json index 97edf8eb67f..4ebbdd3b1e7 100644 --- a/homeassistant/components/met/manifest.json +++ b/homeassistant/components/met/manifest.json @@ -3,7 +3,7 @@ "name": "Meteorologisk institutt (Met.no)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/met", - "requirements": ["pyMetno==0.8.3"], + "requirements": ["pyMetno==0.8.4"], "codeowners": ["@danielhiversen", "@thimic"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/norway_air/air_quality.py b/homeassistant/components/norway_air/air_quality.py index 8e829355ea0..f38897d62c8 100644 --- a/homeassistant/components/norway_air/air_quality.py +++ b/homeassistant/components/norway_air/air_quality.py @@ -24,6 +24,8 @@ CONF_FORECAST = "forecast" DEFAULT_FORECAST = 0 DEFAULT_NAME = "Air quality Norway" +OVERRIDE_URL = "https://aa015h6buqvih86i1.api.met.no/weatherapi/airqualityforecast/0.1/" + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(CONF_FORECAST, default=DEFAULT_FORECAST): vol.Coerce(int), @@ -72,7 +74,9 @@ class AirSensor(AirQualityEntity): def __init__(self, name, coordinates, forecast, session): """Initialize the sensor.""" self._name = name - self._api = metno.AirQualityData(coordinates, forecast, session) + self._api = metno.AirQualityData( + coordinates, forecast, session, api_url=OVERRIDE_URL + ) @property def attribution(self) -> str: diff --git a/homeassistant/components/norway_air/manifest.json b/homeassistant/components/norway_air/manifest.json index 69b2e85808b..87981f085a6 100644 --- a/homeassistant/components/norway_air/manifest.json +++ b/homeassistant/components/norway_air/manifest.json @@ -2,7 +2,7 @@ "domain": "norway_air", "name": "Om Luftkvalitet i Norge (Norway Air)", "documentation": "https://www.home-assistant.io/integrations/norway_air", - "requirements": ["pyMetno==0.8.3"], + "requirements": ["pyMetno==0.8.4"], "codeowners": [], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 9aef5a12b5b..f98a68f8644 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1312,7 +1312,7 @@ pyMetEireann==2021.8.0 # homeassistant.components.met # homeassistant.components.norway_air -pyMetno==0.8.3 +pyMetno==0.8.4 # homeassistant.components.rfxtrx pyRFXtrx==0.27.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 26d9eba1d2d..e790322668d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -784,7 +784,7 @@ pyMetEireann==2021.8.0 # homeassistant.components.met # homeassistant.components.norway_air -pyMetno==0.8.3 +pyMetno==0.8.4 # homeassistant.components.rfxtrx pyRFXtrx==0.27.0 From e7aa90a5b178d619278ddfdd5d2940df7d9780f5 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Fri, 12 Nov 2021 19:09:03 +0100 Subject: [PATCH 148/174] Fix firmware status check for Fritz (#59578) --- homeassistant/components/fritz/common.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index d6df32fa931..09926e5d9ac 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -257,10 +257,10 @@ class FritzBoxTools: def _update_device_info(self) -> tuple[bool, str | None]: """Retrieve latest device information from the FRITZ!Box.""" - userinterface = self.connection.call_action("UserInterface1", "GetInfo") - return userinterface.get("NewUpgradeAvailable"), userinterface.get( + version = self.connection.call_action("UserInterface1", "GetInfo").get( "NewX_AVM-DE_Version" ) + return bool(version), version def scan_devices(self, now: datetime | None = None) -> None: """Scan for new devices and return a list of found device ids.""" From 7042fdb1457497a7d75e7728868d251009635de8 Mon Sep 17 00:00:00 2001 From: Michael Kowalchuk Date: Sat, 13 Nov 2021 04:00:36 -0800 Subject: [PATCH 149/174] Always use a step size of 1 for z-wave js fans (#59622) --- homeassistant/components/zwave_js/fan.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/zwave_js/fan.py b/homeassistant/components/zwave_js/fan.py index 6ee709893cb..4b4f23a85d2 100644 --- a/homeassistant/components/zwave_js/fan.py +++ b/homeassistant/components/zwave_js/fan.py @@ -103,6 +103,11 @@ class ZwaveFan(ZWaveBaseEntity, FanEntity): return None return ranged_value_to_percentage(SPEED_RANGE, self.info.primary_value.value) + @property + def percentage_step(self) -> float: + """Return the step size for percentage.""" + return 1 + @property def speed_count(self) -> int: """Return the number of speeds the fan supports.""" From 0153580defeb0add28e5385409175cbb930c871c Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 13 Nov 2021 12:59:48 +0100 Subject: [PATCH 150/174] Fix favorite RPM max value in Xiaomi Miio (#59631) --- homeassistant/components/xiaomi_miio/number.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/xiaomi_miio/number.py b/homeassistant/components/xiaomi_miio/number.py index 161a690a0df..87ccdd63d0f 100644 --- a/homeassistant/components/xiaomi_miio/number.py +++ b/homeassistant/components/xiaomi_miio/number.py @@ -177,7 +177,7 @@ NUMBER_TYPES = { icon="mdi:star-cog", unit_of_measurement="rpm", min_value=300, - max_value=2300, + max_value=2200, step=10, method="async_set_favorite_rpm", entity_category=ENTITY_CATEGORY_CONFIG, From b122774b129fd57c350c2ed002beb1c55b889a88 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 5 Nov 2021 05:22:59 -0500 Subject: [PATCH 151/174] Bump zeroconf to 0.36.12 (#59133) - Changelog: https://github.com/jstasiak/python-zeroconf/compare/0.36.11...0.36.12 Bugfix: Prevent service lookups from deadlocking if time abruptly moves backwards --- homeassistant/components/zeroconf/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/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 3f4dfb4929e..0c25a4c9860 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -2,7 +2,7 @@ "domain": "zeroconf", "name": "Zero-configuration networking (zeroconf)", "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "requirements": ["zeroconf==0.36.11"], + "requirements": ["zeroconf==0.36.12"], "dependencies": ["network", "api"], "codeowners": ["@bdraco"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1f29737cdf3..3a8f7c3ad6f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ sqlalchemy==1.4.23 voluptuous-serialize==2.4.0 voluptuous==0.12.2 yarl==1.6.3 -zeroconf==0.36.11 +zeroconf==0.36.12 pycryptodome>=3.6.6 diff --git a/requirements_all.txt b/requirements_all.txt index f98a68f8644..1999e21307b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2465,7 +2465,7 @@ youtube_dl==2021.06.06 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.36.11 +zeroconf==0.36.12 # homeassistant.components.zha zha-quirks==0.0.63 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e790322668d..3352b43b571 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1430,7 +1430,7 @@ yeelight==0.7.8 youless-api==0.15 # homeassistant.components.zeroconf -zeroconf==0.36.11 +zeroconf==0.36.12 # homeassistant.components.zha zha-quirks==0.0.63 From 84358fa7708e857c6a3afb5b4e015c41df346a96 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 13 Nov 2021 11:18:54 -0600 Subject: [PATCH 152/174] Bump zeroconf to 0.36.13 (#59644) - Closes #59415 - Fixes #58453 - Fixes #57678 - Changelog: https://github.com/jstasiak/python-zeroconf/compare/0.36.12...0.36.13 --- homeassistant/components/zeroconf/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/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 0c25a4c9860..95f9407661b 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -2,7 +2,7 @@ "domain": "zeroconf", "name": "Zero-configuration networking (zeroconf)", "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "requirements": ["zeroconf==0.36.12"], + "requirements": ["zeroconf==0.36.13"], "dependencies": ["network", "api"], "codeowners": ["@bdraco"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3a8f7c3ad6f..1b4edb91b19 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ sqlalchemy==1.4.23 voluptuous-serialize==2.4.0 voluptuous==0.12.2 yarl==1.6.3 -zeroconf==0.36.12 +zeroconf==0.36.13 pycryptodome>=3.6.6 diff --git a/requirements_all.txt b/requirements_all.txt index 1999e21307b..2817316fe88 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2465,7 +2465,7 @@ youtube_dl==2021.06.06 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.36.12 +zeroconf==0.36.13 # homeassistant.components.zha zha-quirks==0.0.63 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3352b43b571..8eeb4461162 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1430,7 +1430,7 @@ yeelight==0.7.8 youless-api==0.15 # homeassistant.components.zeroconf -zeroconf==0.36.12 +zeroconf==0.36.13 # homeassistant.components.zha zha-quirks==0.0.63 From f0fdd4388c8037976b74f7743c10bb4e15c7b55b Mon Sep 17 00:00:00 2001 From: Clifford Roche Date: Sat, 13 Nov 2021 12:18:12 -0500 Subject: [PATCH 153/174] Bump greecliamate to 0.12.4 (#59645) --- homeassistant/components/gree/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/gree/manifest.json b/homeassistant/components/gree/manifest.json index 62d5bec6bb8..e87a7c6a0bb 100644 --- a/homeassistant/components/gree/manifest.json +++ b/homeassistant/components/gree/manifest.json @@ -3,7 +3,7 @@ "name": "Gree Climate", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/gree", - "requirements": ["greeclimate==0.12.3"], + "requirements": ["greeclimate==0.12.4"], "codeowners": ["@cmroche"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 2817316fe88..69fc4585eee 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -747,7 +747,7 @@ gpiozero==1.5.1 gps3==0.33.3 # homeassistant.components.gree -greeclimate==0.12.3 +greeclimate==0.12.4 # homeassistant.components.greeneye_monitor greeneye_monitor==2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8eeb4461162..a916b511639 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -455,7 +455,7 @@ google-nest-sdm==0.3.8 googlemaps==2.5.1 # homeassistant.components.gree -greeclimate==0.12.3 +greeclimate==0.12.4 # homeassistant.components.growatt_server growattServer==1.1.0 From cb889281a661cf2d9ad1ae5e3c93e5b782f5eef1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 13 Nov 2021 23:55:23 -0600 Subject: [PATCH 154/174] Ensure flux_led bulbs turn on even if brightness is 0 (#59661) --- homeassistant/components/flux_led/light.py | 14 ++++++++--- tests/components/flux_led/test_light.py | 28 ++++++++++++++++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index f1fa4ed7dbb..c632492ea81 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -375,13 +375,21 @@ class FluxLight(FluxEntity, CoordinatorEntity, LightEntity): async def _async_turn_on(self, **kwargs: Any) -> None: """Turn the specified or all lights on.""" + if (brightness := kwargs.get(ATTR_BRIGHTNESS)) is None: + brightness = self.brightness + if not self.is_on: await self._device.async_turn_on() if not kwargs: return - - if (brightness := kwargs.get(ATTR_BRIGHTNESS)) is None: - brightness = self.brightness + # If the brightness was previously 0, the light + # will not turn on unless brightness is at least 1 + if not brightness: + brightness = 1 + elif not brightness: + # If the device was on and brightness was not + # set, it means it was masked by an effect + brightness = 255 # Handle switch to CCT Color Mode if ATTR_COLOR_TEMP in kwargs: diff --git a/tests/components/flux_led/test_light.py b/tests/components/flux_led/test_light.py index 6f0ad5aa253..01dfff85528 100644 --- a/tests/components/flux_led/test_light.py +++ b/tests/components/flux_led/test_light.py @@ -214,11 +214,26 @@ async def test_rgb_light(hass: HomeAssistant) -> None: await async_mock_device_turn_off(hass, bulb) assert hass.states.get(entity_id).state == STATE_OFF + bulb.brightness = 0 + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_RGB_COLOR: (10, 10, 30)}, + blocking=True, + ) + # If the bulb is off and we are using existing brightness + # it has to be at least 1 or the bulb won't turn on + bulb.async_set_levels.assert_called_with(10, 10, 30, brightness=1) + bulb.async_set_levels.reset_mock() + bulb.async_turn_on.reset_mock() + await hass.services.async_call( LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True ) bulb.async_turn_on.assert_called_once() bulb.async_turn_on.reset_mock() + await async_mock_device_turn_on(hass, bulb) + assert hass.states.get(entity_id).state == STATE_ON await hass.services.async_call( LIGHT_DOMAIN, @@ -229,6 +244,19 @@ async def test_rgb_light(hass: HomeAssistant) -> None: bulb.async_set_levels.assert_called_with(255, 0, 0, brightness=100) bulb.async_set_levels.reset_mock() + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_RGB_COLOR: (10, 10, 30)}, + blocking=True, + ) + # If the bulb is on and we are using existing brightness + # and brightness was 0 it means we could not read it because + # an effect is in progress so we use 255 + bulb.async_set_levels.assert_called_with(10, 10, 30, brightness=255) + bulb.async_set_levels.reset_mock() + + bulb.brightness = 128 await hass.services.async_call( LIGHT_DOMAIN, "turn_on", From c2aeeec12927b3bed7291f303f5bb84290fe5d19 Mon Sep 17 00:00:00 2001 From: Anton Malko Date: Sun, 14 Nov 2021 22:36:14 +0300 Subject: [PATCH 155/174] Update aiolookin to 0.0.4 version (#59684) --- homeassistant/components/lookin/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lookin/manifest.json b/homeassistant/components/lookin/manifest.json index 046b0e482a1..7260985654a 100644 --- a/homeassistant/components/lookin/manifest.json +++ b/homeassistant/components/lookin/manifest.json @@ -3,7 +3,7 @@ "name": "LOOKin", "documentation": "https://www.home-assistant.io/integrations/lookin/", "codeowners": ["@ANMalko"], - "requirements": ["aiolookin==0.0.3"], + "requirements": ["aiolookin==0.0.4"], "zeroconf": ["_lookin._tcp.local."], "config_flow": true, "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 69fc4585eee..614d85ee2a6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -207,7 +207,7 @@ aiolifx_effects==0.2.2 aiolip==1.1.6 # homeassistant.components.lookin -aiolookin==0.0.3 +aiolookin==0.0.4 # homeassistant.components.lyric aiolyric==1.0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a916b511639..0623f079b1b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -137,7 +137,7 @@ aiokafka==0.6.0 aiolip==1.1.6 # homeassistant.components.lookin -aiolookin==0.0.3 +aiolookin==0.0.4 # homeassistant.components.lyric aiolyric==1.0.7 From 6a4274b2805cc1074f906083a61a3fc6dff54d61 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 15 Nov 2021 15:30:26 -0700 Subject: [PATCH 156/174] Fix bug in AirVisual re-auth (#59685) --- homeassistant/components/airvisual/config_flow.py | 5 ++++- tests/components/airvisual/test_config_flow.py | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/airvisual/config_flow.py b/homeassistant/components/airvisual/config_flow.py index 636da54899f..e9ccb203eaa 100644 --- a/homeassistant/components/airvisual/config_flow.py +++ b/homeassistant/components/airvisual/config_flow.py @@ -141,6 +141,9 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): existing_entry = await self.async_set_unique_id(self._geo_id) if existing_entry: self.hass.config_entries.async_update_entry(existing_entry, data=user_input) + self.hass.async_create_task( + self.hass.config_entries.async_reload(existing_entry.entry_id) + ) return self.async_abort(reason="reauth_successful") return self.async_create_entry( @@ -237,7 +240,7 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): step_id="reauth_confirm", data_schema=API_KEY_DATA_SCHEMA ) - conf = {CONF_API_KEY: user_input[CONF_API_KEY], **self._entry_data_for_reauth} + conf = {**self._entry_data_for_reauth, CONF_API_KEY: user_input[CONF_API_KEY]} return await self._async_finish_geography( conf, self._entry_data_for_reauth[CONF_INTEGRATION_TYPE] diff --git a/tests/components/airvisual/test_config_flow.py b/tests/components/airvisual/test_config_flow.py index 6125b71e303..65534f1f16c 100644 --- a/tests/components/airvisual/test_config_flow.py +++ b/tests/components/airvisual/test_config_flow.py @@ -355,16 +355,19 @@ async def test_step_reauth(hass): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "reauth_confirm" + new_api_key = "defgh67890" + with patch( "homeassistant.components.airvisual.async_setup_entry", return_value=True ), patch("pyairvisual.air_quality.AirQuality.nearest_city", return_value=True): result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_API_KEY: "defgh67890"} + result["flow_id"], user_input={CONF_API_KEY: new_api_key} ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 + assert hass.config_entries.async_entries()[0].data[CONF_API_KEY] == new_api_key async def test_step_user(hass): From 5c0e34db6cabb2493bdc4dbb94e47b3ef369765f Mon Sep 17 00:00:00 2001 From: Clifford Roche Date: Mon, 15 Nov 2021 13:32:50 -0500 Subject: [PATCH 157/174] Bump greeclimate to 0.12.5 (#59730) --- homeassistant/components/gree/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/gree/manifest.json b/homeassistant/components/gree/manifest.json index e87a7c6a0bb..f4f8cf153a3 100644 --- a/homeassistant/components/gree/manifest.json +++ b/homeassistant/components/gree/manifest.json @@ -3,7 +3,7 @@ "name": "Gree Climate", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/gree", - "requirements": ["greeclimate==0.12.4"], + "requirements": ["greeclimate==0.12.5"], "codeowners": ["@cmroche"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 614d85ee2a6..519aec7f489 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -747,7 +747,7 @@ gpiozero==1.5.1 gps3==0.33.3 # homeassistant.components.gree -greeclimate==0.12.4 +greeclimate==0.12.5 # homeassistant.components.greeneye_monitor greeneye_monitor==2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0623f079b1b..62375d0725f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -455,7 +455,7 @@ google-nest-sdm==0.3.8 googlemaps==2.5.1 # homeassistant.components.gree -greeclimate==0.12.4 +greeclimate==0.12.5 # homeassistant.components.growatt_server growattServer==1.1.0 From 702c57f389859db33187c0f0ae91d646aeeddbc1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 14 Nov 2021 04:56:06 -0600 Subject: [PATCH 158/174] Bump flux_led to 0.24.21 (#59662) --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 429465a0f3c..203acb77385 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.24.17"], + "requirements": ["flux_led==0.24.21"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 519aec7f489..06ab8ba9bb5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -652,7 +652,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.17 +flux_led==0.24.21 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 62375d0725f..de2d98922d2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -387,7 +387,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.17 +flux_led==0.24.21 # homeassistant.components.homekit fnvhash==0.1.0 From 6596ebfe4327b4a60afe72ecb4fdf90857b543c7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 15 Nov 2021 16:13:37 -0600 Subject: [PATCH 159/174] Bump flux_led to 0.24.24 (#59740) --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 203acb77385..cd37897af67 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.24.21"], + "requirements": ["flux_led==0.24.24"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 06ab8ba9bb5..ab04da80a84 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -652,7 +652,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.21 +flux_led==0.24.24 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index de2d98922d2..0c693ec61df 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -387,7 +387,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.21 +flux_led==0.24.24 # homeassistant.components.homekit fnvhash==0.1.0 From ca3c0057d33c14cb22619c0d23cffe5c24ed8f23 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Mon, 15 Nov 2021 23:30:48 +0100 Subject: [PATCH 160/174] Fix invalid string syntax in French OwnTracks config flow (#59752) --- homeassistant/components/owntracks/translations/fr.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/owntracks/translations/fr.json b/homeassistant/components/owntracks/translations/fr.json index 0e753a455e0..9120cdb8637 100644 --- a/homeassistant/components/owntracks/translations/fr.json +++ b/homeassistant/components/owntracks/translations/fr.json @@ -4,7 +4,7 @@ "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, "create_entry": { - "default": "\n\n Sous Android, ouvrez [l'application OwnTracks] ( {android_url} ), acc\u00e9dez \u00e0 Pr\u00e9f\u00e9rences - > Connexion. Modifiez les param\u00e8tres suivants: \n - Mode: HTTP priv\u00e9 \n - H\u00f4te: {webhook_url} \n - Identification: \n - Nom d'utilisateur: ` ` \n - ID de p\u00e9riph\u00e9rique: ` ` \n\n Sur iOS, ouvrez [l'application OwnTracks] ( {ios_url} ), appuyez sur l'ic\u00f4ne (i) en haut \u00e0 gauche - > param\u00e8tres. Modifiez les param\u00e8tres suivants: \n - Mode: HTTP \n - URL: {webhook_url} \n - Activer l'authentification \n - ID utilisateur: ` ` \n\n {secret} \n \n Voir [la documentation] ( {docs_url} ) pour plus d'informations." + "default": "\n\n Sous Android, ouvrez [l'application OwnTracks]({android_url}), acc\u00e9dez \u00e0 Pr\u00e9f\u00e9rences - > Connexion. Modifiez les param\u00e8tres suivants: \n - Mode: HTTP priv\u00e9 \n - H\u00f4te: {webhook_url} \n - Identification: \n - Nom d'utilisateur: `''` \n - ID de p\u00e9riph\u00e9rique: `''` \n\n Sur iOS, ouvrez [l'application OwnTracks]({ios_url}), appuyez sur l'ic\u00f4ne (i) en haut \u00e0 gauche - > param\u00e8tres. Modifiez les param\u00e8tres suivants: \n - Mode: HTTP \n - URL: {webhook_url} \n - Activer l'authentification \n - ID utilisateur: `''` \n\n {secret} \n \n Voir [la documentation]({docs_url}) pour plus d'informations." }, "step": { "user": { From 0e12bce1741ccffc222e05362923bc383f4ab7bd Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 15 Nov 2021 14:37:30 -0800 Subject: [PATCH 161/174] Bumped version to 2021.11.4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 7b9d8e4165d..5005540dbf2 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 11 -PATCH_VERSION: Final = "3" +PATCH_VERSION: Final = "4" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From 711a00225fa7126be75b4571f9b42782568a55ce Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 16 Nov 2021 12:40:54 +0100 Subject: [PATCH 162/174] Use source list property instead of the attribute in Denon AVR integration (#59768) --- homeassistant/components/denonavr/media_player.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index 39186680e09..c3106a98c72 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -155,7 +155,6 @@ class DenonDevice(MediaPlayerEntity): name=config_entry.title, ) self._attr_sound_mode_list = receiver.sound_mode_list - self._attr_source_list = receiver.input_func_list self._receiver = receiver self._update_audyssey = update_audyssey @@ -246,6 +245,11 @@ class DenonDevice(MediaPlayerEntity): """Return the state of the device.""" return self._receiver.state + @property + def source_list(self): + """Return a list of available input sources.""" + return self._receiver.input_func_list + @property def is_volume_muted(self): """Return boolean if volume is currently muted.""" From 7316e0555b87d23d6288fd603415b7c0a8116297 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Tue, 16 Nov 2021 19:30:50 +0100 Subject: [PATCH 163/174] Fix typo in attribute for Fritz (#59791) --- homeassistant/components/fritz/binary_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/fritz/binary_sensor.py b/homeassistant/components/fritz/binary_sensor.py index 994c7ff656e..594c1721be4 100644 --- a/homeassistant/components/fritz/binary_sensor.py +++ b/homeassistant/components/fritz/binary_sensor.py @@ -92,5 +92,5 @@ class FritzBoxBinarySensor(FritzBoxBaseEntity, BinarySensorEntity): self._attr_is_on = self._fritzbox_tools.update_available self._attr_extra_state_attributes = { "installed_version": self._fritzbox_tools.current_firmware, - "latest_available_version:": self._fritzbox_tools.latest_firmware, + "latest_available_version": self._fritzbox_tools.latest_firmware, } From 85abc4034d328ddbabde32a5daa4d631087214b7 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 17 Nov 2021 11:49:12 +0100 Subject: [PATCH 164/174] Fix Netgear init error on orbi models (#59799) * fix Netgear init error on orbi models * Update sensor.py --- homeassistant/components/netgear/router.py | 2 ++ homeassistant/components/netgear/sensor.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/netgear/router.py b/homeassistant/components/netgear/router.py index 3c2497f2131..8a9a4b3ef85 100644 --- a/homeassistant/components/netgear/router.py +++ b/homeassistant/components/netgear/router.py @@ -173,6 +173,8 @@ class NetgearRouter: "link_rate": None, "signal": None, "ip": None, + "ssid": None, + "conn_ap_mac": None, } await self.async_update_device_trackers() diff --git a/homeassistant/components/netgear/sensor.py b/homeassistant/components/netgear/sensor.py index 57ffe6f98f2..4e9b55d3227 100644 --- a/homeassistant/components/netgear/sensor.py +++ b/homeassistant/components/netgear/sensor.py @@ -66,7 +66,7 @@ class NetgearSensorEntity(NetgearDeviceEntity, SensorEntity): self.entity_description = SENSOR_TYPES[self._attribute] self._name = f"{self.get_device_name()} {self.entity_description.name}" self._unique_id = f"{self._mac}-{self._attribute}" - self._state = self._device[self._attribute] + self._state = self._device.get(self._attribute) @property def native_value(self): @@ -78,7 +78,7 @@ class NetgearSensorEntity(NetgearDeviceEntity, SensorEntity): """Update the Netgear device.""" self._device = self._router.devices[self._mac] self._active = self._device["active"] - if self._device[self._attribute] is not None: + if self._device.get(self._attribute) is not None: self._state = self._device[self._attribute] self.async_write_ha_state() From 845f75868d9cdf9fe679670161760204e70bf3a1 Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Wed, 17 Nov 2021 10:15:19 +0100 Subject: [PATCH 165/174] Bump velbusaio to 2021.11.7 (#59817) --- homeassistant/components/velbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json index 5fb3c58c3c7..63d74536378 100644 --- a/homeassistant/components/velbus/manifest.json +++ b/homeassistant/components/velbus/manifest.json @@ -2,7 +2,7 @@ "domain": "velbus", "name": "Velbus", "documentation": "https://www.home-assistant.io/integrations/velbus", - "requirements": ["velbus-aio==2021.11.6"], + "requirements": ["velbus-aio==2021.11.7"], "config_flow": true, "codeowners": ["@Cereal2nd", "@brefra"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index ab04da80a84..89496ea700d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2360,7 +2360,7 @@ uvcclient==0.11.0 vallox-websocket-api==2.8.1 # homeassistant.components.velbus -velbus-aio==2021.11.6 +velbus-aio==2021.11.7 # homeassistant.components.venstar venstarcolortouch==0.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0c693ec61df..af0f2452ea9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1364,7 +1364,7 @@ url-normalize==1.4.1 uvcclient==0.11.0 # homeassistant.components.velbus -velbus-aio==2021.11.6 +velbus-aio==2021.11.7 # homeassistant.components.venstar venstarcolortouch==0.14 From 0d44328f42460ce9c269060beb19eb2a63609474 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Wed, 17 Nov 2021 16:05:50 +0100 Subject: [PATCH 166/174] Fix invalid string syntax in OwnTracks config flow translations (#59838) --- homeassistant/components/owntracks/translations/hu.json | 2 +- homeassistant/components/owntracks/translations/lb.json | 2 +- homeassistant/components/owntracks/translations/nl.json | 2 +- homeassistant/components/owntracks/translations/pl.json | 2 +- homeassistant/components/owntracks/translations/ru.json | 2 +- homeassistant/components/owntracks/translations/sv.json | 2 +- homeassistant/components/owntracks/translations/uk.json | 2 +- homeassistant/components/owntracks/translations/zh-Hans.json | 2 +- homeassistant/components/owntracks/translations/zh-Hant.json | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/owntracks/translations/hu.json b/homeassistant/components/owntracks/translations/hu.json index 84a40a1a593..e99b11a9e7e 100644 --- a/homeassistant/components/owntracks/translations/hu.json +++ b/homeassistant/components/owntracks/translations/hu.json @@ -4,7 +4,7 @@ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, "create_entry": { - "default": "\n\nAndroidon, nyissa meg [az OwnTracks appot]({android_url}), majd v\u00e1lassza ki a Preferences -> Connection men\u00fct. V\u00e1ltoztassa meg az al\u00e1bbi be\u00e1ll\u00edt\u00e1sokat:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: ``\n - Device ID: ``\n\niOS-en, nyissa meg [az OwnTracks appot]({ios_url}), kattintson az (i) ikonra bal oldalon fel\u00fcl -> settings. V\u00e1ltoztassa meg az al\u00e1bbi be\u00e1ll\u00edt\u00e1sokat:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: ``\n\n{secret}\n\nN\u00e9zze meg [a dokument\u00e1ci\u00f3t]({docs_url}) tov\u00e1bbi inform\u00e1ci\u00f3k\u00e9rt." + "default": "\n\nAndroidon, nyissa meg [az OwnTracks appot]({android_url}), majd v\u00e1lassza ki a Preferences -> Connection men\u00fct. V\u00e1ltoztassa meg az al\u00e1bbi be\u00e1ll\u00edt\u00e1sokat:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: `''`\n - Device ID: `''`\n\niOS-en, nyissa meg [az OwnTracks appot]({ios_url}), kattintson az (i) ikonra bal oldalon fel\u00fcl -> settings. V\u00e1ltoztassa meg az al\u00e1bbi be\u00e1ll\u00edt\u00e1sokat:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: `''`\n\n{secret}\n\nN\u00e9zze meg [a dokument\u00e1ci\u00f3t]({docs_url}) tov\u00e1bbi inform\u00e1ci\u00f3k\u00e9rt." }, "step": { "user": { diff --git a/homeassistant/components/owntracks/translations/lb.json b/homeassistant/components/owntracks/translations/lb.json index f2e9ea664d3..a5fa4ddc2f7 100644 --- a/homeassistant/components/owntracks/translations/lb.json +++ b/homeassistant/components/owntracks/translations/lb.json @@ -4,7 +4,7 @@ "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech." }, "create_entry": { - "default": "\n\nOp Android, an [der OwnTracks App]({android_url}), g\u00e9i an Preferences -> Connection. \u00c4nnert folgend Astellungen:\n- Mode: Private HTTP\n- Host {webhook_url}\n- Identification:\n - Username: ``\n - Device ID: ``\n\nOp IOS, an [der OwnTracks App]({ios_url}), klick op (i) Ikon uewen l\u00e9nks -> Settings. \u00c4nnert folgend Astellungen:\n- Mode: HTTP\n- URL: {webhook_url}\n- Turn on authentication:\n- UserID: ``\n\n{secret}\n\nKuck w.e.g. [Dokumentatioun]({docs_url}) fir m\u00e9i Informatiounen." + "default": "\n\nOp Android, an [der OwnTracks App]({android_url}), g\u00e9i an Preferences -> Connection. \u00c4nnert folgend Astellungen:\n- Mode: Private HTTP\n- Host {webhook_url}\n- Identification:\n - Username: `''`\n - Device ID: `''`\n\nOp IOS, an [der OwnTracks App]({ios_url}), klick op (i) Ikon uewen l\u00e9nks -> Settings. \u00c4nnert folgend Astellungen:\n- Mode: HTTP\n- URL: {webhook_url}\n- Turn on authentication:\n- UserID: `''`\n\n{secret}\n\nKuck w.e.g. [Dokumentatioun]({docs_url}) fir m\u00e9i Informatiounen." }, "step": { "user": { diff --git a/homeassistant/components/owntracks/translations/nl.json b/homeassistant/components/owntracks/translations/nl.json index 2d97724661c..ff6cadcbf25 100644 --- a/homeassistant/components/owntracks/translations/nl.json +++ b/homeassistant/components/owntracks/translations/nl.json @@ -4,7 +4,7 @@ "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "create_entry": { - "default": "\n\nOp Android, open [the OwnTracks app]({android_url}), ga naar 'preferences' -> 'connection'. Verander de volgende instellingen:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: ``\n - Device ID: ``\n\nOp iOS, open [the OwnTracks app]({ios_url}), tik op het (i) icoon links boven -> 'settings'. Verander de volgende instellingen:\n - Mode: HTTP\n - URL: {webhook_url}\n - zet 'authentication' aan\n - UserID: ``\n\n{secret}\n\nZie [the documentation]({docs_url}) voor meer informatie." + "default": "\n\nOp Android, open [the OwnTracks app]({android_url}), ga naar 'preferences' -> 'connection'. Verander de volgende instellingen:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: `''`\n - Device ID: `''`\n\nOp iOS, open [the OwnTracks app]({ios_url}), tik op het (i) icoon links boven -> 'settings'. Verander de volgende instellingen:\n - Mode: HTTP\n - URL: {webhook_url}\n - zet 'authentication' aan\n - UserID: `''`\n\n{secret}\n\nZie [the documentation]({docs_url}) voor meer informatie." }, "step": { "user": { diff --git a/homeassistant/components/owntracks/translations/pl.json b/homeassistant/components/owntracks/translations/pl.json index c4499800eda..98c8779fe1f 100644 --- a/homeassistant/components/owntracks/translations/pl.json +++ b/homeassistant/components/owntracks/translations/pl.json @@ -4,7 +4,7 @@ "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." }, "create_entry": { - "default": "\n\nNa Androidzie, otw\u00f3rz [aplikacj\u0119 OwnTracks]({android_url}), id\u017a do: ustawienia -> po\u0142\u0105czenia. Zmie\u0144 nast\u0119puj\u0105ce ustawienia:\n - Tryb: Private HTTP\n - Host: {webhook_url}\n - Identyfikacja:\n - Nazwa u\u017cytkownika: ``\n - ID urz\u0105dzenia: ``\n\nNa iOS, otw\u00f3rz [aplikacj\u0119 OwnTracks]({ios_url}), naci\u015bnij ikon\u0119 (i) w lewym g\u00f3rnym rogu -> ustawienia. Zmie\u0144 nast\u0119puj\u0105ce ustawienia:\n - Tryb: HTTP\n - URL: {webhook_url}\n - W\u0142\u0105cz uwierzytelnianie\n - ID u\u017cytkownika: ``\n\n{secret}\n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}), by pozna\u0107 szczeg\u00f3\u0142y." + "default": "\n\nNa Androidzie, otw\u00f3rz [aplikacj\u0119 OwnTracks]({android_url}), id\u017a do: ustawienia -> po\u0142\u0105czenia. Zmie\u0144 nast\u0119puj\u0105ce ustawienia:\n - Tryb: Private HTTP\n - Host: {webhook_url}\n - Identyfikacja:\n - Nazwa u\u017cytkownika: `''`\n - ID urz\u0105dzenia: `''`\n\nNa iOS, otw\u00f3rz [aplikacj\u0119 OwnTracks]({ios_url}), naci\u015bnij ikon\u0119 (i) w lewym g\u00f3rnym rogu -> ustawienia. Zmie\u0144 nast\u0119puj\u0105ce ustawienia:\n - Tryb: HTTP\n - URL: {webhook_url}\n - W\u0142\u0105cz uwierzytelnianie\n - ID u\u017cytkownika: `''`\n\n{secret}\n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}), by pozna\u0107 szczeg\u00f3\u0142y." }, "step": { "user": { diff --git a/homeassistant/components/owntracks/translations/ru.json b/homeassistant/components/owntracks/translations/ru.json index 09fdba77266..ed1e084090d 100644 --- a/homeassistant/components/owntracks/translations/ru.json +++ b/homeassistant/components/owntracks/translations/ru.json @@ -4,7 +4,7 @@ "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." }, "create_entry": { - "default": "\u0415\u0441\u043b\u0438 \u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435 Android, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 [OwnTracks]({android_url}), \u0437\u0430\u0442\u0435\u043c preferences -> connection. \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0442\u0430\u043a, \u043a\u0430\u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u043d\u0438\u0436\u0435:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: ``\n - Device ID: ``\n\n\u0415\u0441\u043b\u0438 \u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 iOS, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 [OwnTracks]({ios_url}), \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u043d\u0430 \u0437\u043d\u0430\u0447\u043e\u043a (i) \u0432 \u043b\u0435\u0432\u043e\u043c \u0432\u0435\u0440\u0445\u043d\u0435\u043c \u0443\u0433\u043b\u0443 -> settings. \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0442\u0430\u043a, \u043a\u0430\u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u043d\u0438\u0436\u0435:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: ``\n\n{secret}\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." + "default": "\u0415\u0441\u043b\u0438 \u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435 Android, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 [OwnTracks]({android_url}), \u0437\u0430\u0442\u0435\u043c preferences -> connection. \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0442\u0430\u043a, \u043a\u0430\u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u043d\u0438\u0436\u0435:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: `''`\n - Device ID: `''`\n\n\u0415\u0441\u043b\u0438 \u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 iOS, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 [OwnTracks]({ios_url}), \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u043d\u0430 \u0437\u043d\u0430\u0447\u043e\u043a (i) \u0432 \u043b\u0435\u0432\u043e\u043c \u0432\u0435\u0440\u0445\u043d\u0435\u043c \u0443\u0433\u043b\u0443 -> settings. \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0442\u0430\u043a, \u043a\u0430\u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u043d\u0438\u0436\u0435:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: `''`\n\n{secret}\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." }, "step": { "user": { diff --git a/homeassistant/components/owntracks/translations/sv.json b/homeassistant/components/owntracks/translations/sv.json index fd2162f153b..8642a32f889 100644 --- a/homeassistant/components/owntracks/translations/sv.json +++ b/homeassistant/components/owntracks/translations/sv.json @@ -1,7 +1,7 @@ { "config": { "create_entry": { - "default": "\n\n P\u00e5 Android, \u00f6ppna [OwnTracks-appen]({android_url}), g\u00e5 till inst\u00e4llningar -> anslutning. \u00c4ndra f\u00f6ljande inst\u00e4llningar: \n - L\u00e4ge: Privat HTTP \n - V\u00e4rden: {webhook_url}\n - Identifiering: \n - Anv\u00e4ndarnamn: ``\n - Enhets-ID: `` \n\n P\u00e5 IOS, \u00f6ppna [OwnTracks-appen]({ios_url}), tryck p\u00e5 (i) ikonen i \u00f6vre v\u00e4nstra h\u00f6rnet -> inst\u00e4llningarna. \u00c4ndra f\u00f6ljande inst\u00e4llningar: \n - L\u00e4ge: HTTP \n - URL: {webhook_url}\n - Sl\u00e5 p\u00e5 autentisering \n - UserID: `` \n\n {secret} \n \n Se [dokumentationen]({docs_url}) f\u00f6r mer information." + "default": "\n\n P\u00e5 Android, \u00f6ppna [OwnTracks-appen]({android_url}), g\u00e5 till inst\u00e4llningar -> anslutning. \u00c4ndra f\u00f6ljande inst\u00e4llningar: \n - L\u00e4ge: Privat HTTP \n - V\u00e4rden: {webhook_url}\n - Identifiering: \n - Anv\u00e4ndarnamn: `''`\n - Enhets-ID: `''` \n\n P\u00e5 IOS, \u00f6ppna [OwnTracks-appen]({ios_url}), tryck p\u00e5 (i) ikonen i \u00f6vre v\u00e4nstra h\u00f6rnet -> inst\u00e4llningarna. \u00c4ndra f\u00f6ljande inst\u00e4llningar: \n - L\u00e4ge: HTTP \n - URL: {webhook_url}\n - Sl\u00e5 p\u00e5 autentisering \n - UserID: `''` \n\n {secret} \n \n Se [dokumentationen]({docs_url}) f\u00f6r mer information." }, "step": { "user": { diff --git a/homeassistant/components/owntracks/translations/uk.json b/homeassistant/components/owntracks/translations/uk.json index e6a6fc26068..c355b745d1d 100644 --- a/homeassistant/components/owntracks/translations/uk.json +++ b/homeassistant/components/owntracks/translations/uk.json @@ -4,7 +4,7 @@ "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." }, "create_entry": { - "default": "\u042f\u043a\u0449\u043e \u0412\u0430\u0448 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0440\u0430\u0446\u044e\u0454 \u043d\u0430 \u043e\u043f\u0435\u0440\u0430\u0446\u0456\u0439\u043d\u0456\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0456 Android, \u0432\u0456\u0434\u043a\u0440\u0438\u0439\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u043e\u043a [OwnTracks]({android_url}), \u043f\u043e\u0442\u0456\u043c preferences - > connection. \u0417\u043c\u0456\u043d\u0456\u0442\u044c \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u0442\u0430\u043a, \u044f\u043a \u0437\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u043e \u043d\u0438\u0436\u0447\u0435:\n- Mode: Private HTTP\n- Host: {webhook_url}\n- Identification:\n- Username: ``\n- Device ID: `` \n\n\u042f\u043a\u0449\u043e \u0412\u0430\u0448 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0440\u0430\u0446\u044e\u0454 \u043d\u0430 iOS, \u0432\u0456\u0434\u043a\u0440\u0438\u0439\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u043e\u043a [OwnTracks]({ios_url}), \u043d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c \u043d\u0430 \u0437\u043d\u0430\u0447\u043e\u043a (i) \u0432 \u043b\u0456\u0432\u043e\u043c\u0443 \u0432\u0435\u0440\u0445\u043d\u044c\u043e\u043c\u0443 \u043a\u0443\u0442\u043a\u0443 - > settings. \u0417\u043c\u0456\u043d\u0456\u0442\u044c \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u0442\u0430\u043a, \u044f\u043a \u0437\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u043e \u043d\u0438\u0436\u0447\u0435:\n- Mode: HTTP\n- URL: {webhook_url}\n- Turn on authentication\n- UserID: ``\n\n{secret}\n\n\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0431\u0456\u043b\u044c\u0448 \u0434\u043e\u043a\u043b\u0430\u0434\u043d\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457." + "default": "\u042f\u043a\u0449\u043e \u0412\u0430\u0448 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0440\u0430\u0446\u044e\u0454 \u043d\u0430 \u043e\u043f\u0435\u0440\u0430\u0446\u0456\u0439\u043d\u0456\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0456 Android, \u0432\u0456\u0434\u043a\u0440\u0438\u0439\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u043e\u043a [OwnTracks]({android_url}), \u043f\u043e\u0442\u0456\u043c preferences - > connection. \u0417\u043c\u0456\u043d\u0456\u0442\u044c \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u0442\u0430\u043a, \u044f\u043a \u0437\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u043e \u043d\u0438\u0436\u0447\u0435:\n- Mode: Private HTTP\n- Host: {webhook_url}\n- Identification:\n- Username: `''`\n- Device ID: `''` \n\n\u042f\u043a\u0449\u043e \u0412\u0430\u0448 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0440\u0430\u0446\u044e\u0454 \u043d\u0430 iOS, \u0432\u0456\u0434\u043a\u0440\u0438\u0439\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u043e\u043a [OwnTracks]({ios_url}), \u043d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c \u043d\u0430 \u0437\u043d\u0430\u0447\u043e\u043a (i) \u0432 \u043b\u0456\u0432\u043e\u043c\u0443 \u0432\u0435\u0440\u0445\u043d\u044c\u043e\u043c\u0443 \u043a\u0443\u0442\u043a\u0443 - > settings. \u0417\u043c\u0456\u043d\u0456\u0442\u044c \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u0442\u0430\u043a, \u044f\u043a \u0437\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u043e \u043d\u0438\u0436\u0447\u0435:\n- Mode: HTTP\n- URL: {webhook_url}\n- Turn on authentication\n- UserID: `''`\n\n{secret}\n\n\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0431\u0456\u043b\u044c\u0448 \u0434\u043e\u043a\u043b\u0430\u0434\u043d\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457." }, "step": { "user": { diff --git a/homeassistant/components/owntracks/translations/zh-Hans.json b/homeassistant/components/owntracks/translations/zh-Hans.json index 6954b838d04..a6c01626c76 100644 --- a/homeassistant/components/owntracks/translations/zh-Hans.json +++ b/homeassistant/components/owntracks/translations/zh-Hans.json @@ -1,7 +1,7 @@ { "config": { "create_entry": { - "default": "\n\n\u5728 Android \u8bbe\u5907\u4e0a\uff0c\u6253\u5f00 [OwnTracks APP]({android_url})\uff0c\u524d\u5f80 Preferences -> Connection\u3002\u4fee\u6539\u4ee5\u4e0b\u8bbe\u5b9a\uff1a\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: ``\n - Device ID: ``\n\n\u5728 iOS \u8bbe\u5907\u4e0a\uff0c\u6253\u5f00 [OwnTracks APP]({ios_url})\uff0c\u70b9\u51fb\u5de6\u4e0a\u89d2\u7684 (i) \u56fe\u6807-> Settings\u3002\u4fee\u6539\u4ee5\u4e0b\u8bbe\u5b9a\uff1a\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: ``\n\n{secret}\n\n\u8bf7\u53c2\u9605[\u6587\u6863]({docs_url})\u4ee5\u4e86\u89e3\u66f4\u591a\u4fe1\u606f\u3002" + "default": "\n\n\u5728 Android \u8bbe\u5907\u4e0a\uff0c\u6253\u5f00 [OwnTracks APP]({android_url})\uff0c\u524d\u5f80 Preferences -> Connection\u3002\u4fee\u6539\u4ee5\u4e0b\u8bbe\u5b9a\uff1a\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: `''`\n - Device ID: `''`\n\n\u5728 iOS \u8bbe\u5907\u4e0a\uff0c\u6253\u5f00 [OwnTracks APP]({ios_url})\uff0c\u70b9\u51fb\u5de6\u4e0a\u89d2\u7684 (i) \u56fe\u6807-> Settings\u3002\u4fee\u6539\u4ee5\u4e0b\u8bbe\u5b9a\uff1a\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: `''`\n\n{secret}\n\n\u8bf7\u53c2\u9605[\u6587\u6863]({docs_url})\u4ee5\u4e86\u89e3\u66f4\u591a\u4fe1\u606f\u3002" }, "step": { "user": { diff --git a/homeassistant/components/owntracks/translations/zh-Hant.json b/homeassistant/components/owntracks/translations/zh-Hant.json index 6c92b557797..2803182629a 100644 --- a/homeassistant/components/owntracks/translations/zh-Hant.json +++ b/homeassistant/components/owntracks/translations/zh-Hant.json @@ -4,7 +4,7 @@ "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "create_entry": { - "default": "\n\n\u65bc Android \u8a2d\u5099\uff0c\u6253\u958b [OwnTracks app]({android_url})\u3001\u9ede\u9078\u8a2d\u5b9a\uff08preferences\uff09 -> \u9023\u7dda\uff08connection\uff09\u3002\u8b8a\u66f4\u4ee5\u4e0b\u8a2d\u5b9a\uff1a\n - \u6a21\u5f0f\uff08Mode\uff09\uff1aPrivate HTTP\n - \u4e3b\u6a5f\u7aef\uff08Host\uff09\uff1a{webhook_url}\n - Identification\uff1a\n - Username\uff1a ``\n - Device ID\uff1a``\n\n\u65bc iOS \u8a2d\u5099\uff0c\u6253\u958b [OwnTracks app]({ios_url})\u3001\u9ede\u9078\u5de6\u4e0a\u65b9\u7684 (i) \u5716\u793a -> \u8a2d\u5b9a\uff08settings\uff09\u3002\u8b8a\u66f4\u4ee5\u4e0b\u8a2d\u5b9a\uff1a\n - \u6a21\u5f0f\uff08Mode\uff09\uff1aHTTP\n - URL: {webhook_url}\n - \u958b\u555f authentication\n - UserID: ``\n\n{secret}\n\n\u8acb\u53c3\u95b1 [\u6587\u4ef6]({docs_url})\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u6599\u3002" + "default": "\n\n\u65bc Android \u8a2d\u5099\uff0c\u6253\u958b [OwnTracks app]({android_url})\u3001\u9ede\u9078\u8a2d\u5b9a\uff08preferences\uff09 -> \u9023\u7dda\uff08connection\uff09\u3002\u8b8a\u66f4\u4ee5\u4e0b\u8a2d\u5b9a\uff1a\n - \u6a21\u5f0f\uff08Mode\uff09\uff1aPrivate HTTP\n - \u4e3b\u6a5f\u7aef\uff08Host\uff09\uff1a{webhook_url}\n - Identification\uff1a\n - Username\uff1a `''`\n - Device ID\uff1a`''`\n\n\u65bc iOS \u8a2d\u5099\uff0c\u6253\u958b [OwnTracks app]({ios_url})\u3001\u9ede\u9078\u5de6\u4e0a\u65b9\u7684 (i) \u5716\u793a -> \u8a2d\u5b9a\uff08settings\uff09\u3002\u8b8a\u66f4\u4ee5\u4e0b\u8a2d\u5b9a\uff1a\n - \u6a21\u5f0f\uff08Mode\uff09\uff1aHTTP\n - URL: {webhook_url}\n - \u958b\u555f authentication\n - UserID: `''`\n\n{secret}\n\n\u8acb\u53c3\u95b1 [\u6587\u4ef6]({docs_url})\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u6599\u3002" }, "step": { "user": { From 684efd3fe5df64ad5de5addb6f35ce0fc0cc4d7e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 18 Nov 2021 17:21:51 -0600 Subject: [PATCH 167/174] Strip out deleted entities when configuring homekit (#59844) --- .../components/homekit/config_flow.py | 29 +++++--- tests/components/homekit/test_config_flow.py | 73 ++++++++++++++++++- 2 files changed, 91 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/homekit/config_flow.py b/homeassistant/components/homekit/config_flow.py index a79db949ab0..79a0e71f969 100644 --- a/homeassistant/components/homekit/config_flow.py +++ b/homeassistant/components/homekit/config_flow.py @@ -330,13 +330,19 @@ class OptionsFlowHandler(config_entries.OptionsFlow): return self.async_create_entry(title="", data=self.hk_options) all_supported_devices = await _async_get_supported_devices(self.hass) + # Strip out devices that no longer exist to prevent error in the UI + devices = [ + device_id + for device_id in self.hk_options.get(CONF_DEVICES, []) + if device_id in all_supported_devices + ] return self.async_show_form( step_id="advanced", data_schema=vol.Schema( { - vol.Optional( - CONF_DEVICES, default=self.hk_options.get(CONF_DEVICES, []) - ): cv.multi_select(all_supported_devices) + vol.Optional(CONF_DEVICES, default=devices): cv.multi_select( + all_supported_devices + ) } ), ) @@ -445,13 +451,16 @@ class OptionsFlowHandler(config_entries.OptionsFlow): ) data_schema = {} - entities = entity_filter.get(CONF_INCLUDE_ENTITIES, []) - if self.hk_options[CONF_HOMEKIT_MODE] == HOMEKIT_MODE_ACCESSORY: - entity_schema = vol.In - else: - if entities: - include_exclude_mode = MODE_INCLUDE - else: + entity_schema = vol.In + # Strip out entities that no longer exist to prevent error in the UI + entities = [ + entity_id + for entity_id in entity_filter.get(CONF_INCLUDE_ENTITIES, []) + if entity_id in all_supported_entities + ] + if self.hk_options[CONF_HOMEKIT_MODE] != HOMEKIT_MODE_ACCESSORY: + include_exclude_mode = MODE_INCLUDE + if not entities: include_exclude_mode = MODE_EXCLUDE entities = entity_filter.get(CONF_EXCLUDE_ENTITIES, []) data_schema[ diff --git a/tests/components/homekit/test_config_flow.py b/tests/components/homekit/test_config_flow.py index e5d395a1f29..f1564f9e3ae 100644 --- a/tests/components/homekit/test_config_flow.py +++ b/tests/components/homekit/test_config_flow.py @@ -365,7 +365,24 @@ async def test_options_flow_devices( mock_hap, hass, demo_cleanup, device_reg, entity_reg, mock_get_source_ip ): """Test devices can be bridged.""" - config_entry = _mock_config_entry_with_options_populated() + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_NAME: "mock_name", CONF_PORT: 12345}, + options={ + "devices": ["notexist"], + "filter": { + "include_domains": [ + "fan", + "humidifier", + "vacuum", + "media_player", + "climate", + "alarm_control_panel", + ], + "exclude_entities": ["climate.front_gate"], + }, + }, + ) config_entry.add_to_hass(hass) demo_config_entry = MockConfigEntry(domain="domain") @@ -491,6 +508,60 @@ async def test_options_flow_devices_preserved_when_advanced_off( } +async def test_options_flow_with_non_existant_entity(hass, mock_get_source_ip): + """Test config flow options in include mode.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_NAME: "mock_name", CONF_PORT: 12345}, + options={ + "filter": { + "include_entities": ["climate.not_exist", "climate.front_gate"], + }, + }, + ) + config_entry.add_to_hass(hass) + hass.states.async_set("climate.front_gate", "off") + hass.states.async_set("climate.new", "off") + + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init( + config_entry.entry_id, context={"show_advanced_options": False} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"domains": ["fan", "vacuum", "climate"]}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "include_exclude" + + entities = result["data_schema"]({})["entities"] + assert "climate.not_exist" not in entities + + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "entities": ["climate.new", "climate.front_gate"], + "include_exclude_mode": "include", + }, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert config_entry.options == { + "mode": "bridge", + "filter": { + "exclude_domains": [], + "exclude_entities": [], + "include_domains": ["fan", "vacuum"], + "include_entities": ["climate.new", "climate.front_gate"], + }, + } + + async def test_options_flow_include_mode_basic(hass, mock_get_source_ip): """Test config flow options in include mode.""" From 7b6d55bd8856e95897dd36973f737fe4698e2a0e Mon Sep 17 00:00:00 2001 From: PlusPlus-ua Date: Thu, 18 Nov 2021 17:53:34 +0200 Subject: [PATCH 168/174] Bugfix in Tuya Number value scaling (#59903) --- homeassistant/components/tuya/base.py | 4 ++++ homeassistant/components/tuya/number.py | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tuya/base.py b/homeassistant/components/tuya/base.py index d61c83b17ad..f4bc0dc561f 100644 --- a/homeassistant/components/tuya/base.py +++ b/homeassistant/components/tuya/base.py @@ -46,6 +46,10 @@ class IntegerTypeData: """Scale a value.""" return value * 1.0 / (10 ** self.scale) + def scale_value_back(self, value: float | int) -> int: + """Return raw value for scaled.""" + return int(value * (10 ** self.scale)) + def remap_value_to( self, value: float, diff --git a/homeassistant/components/tuya/number.py b/homeassistant/components/tuya/number.py index c724f6e79a3..8db2e4debd5 100644 --- a/homeassistant/components/tuya/number.py +++ b/homeassistant/components/tuya/number.py @@ -259,9 +259,9 @@ class TuyaNumberEntity(TuyaEntity, NumberEntity): # and determine unit of measurement if self._status_range.type == "Integer": self._type_data = IntegerTypeData.from_json(self._status_range.values) - self._attr_max_value = self._type_data.max - self._attr_min_value = self._type_data.min - self._attr_step = self._type_data.step + self._attr_max_value = self._type_data.max_scaled + self._attr_min_value = self._type_data.min_scaled + self._attr_step = self._type_data.step_scaled if description.unit_of_measurement is None: self._attr_unit_of_measurement = self._type_data.unit @@ -290,7 +290,7 @@ class TuyaNumberEntity(TuyaEntity, NumberEntity): [ { "code": self.entity_description.key, - "value": self._type_data.scale_value(value), + "value": self._type_data.scale_value_back(value), } ] ) From 6bed1a88002fe353f16a85119f33d25497bb9cd4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 19 Nov 2021 00:21:27 +0100 Subject: [PATCH 169/174] Fix Tuya back scaling in Climate and Humidifer entities (#59909) --- homeassistant/components/tuya/climate.py | 6 ++++-- homeassistant/components/tuya/humidifier.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tuya/climate.py b/homeassistant/components/tuya/climate.py index b26ff34bc6d..cb70fc5515a 100644 --- a/homeassistant/components/tuya/climate.py +++ b/homeassistant/components/tuya/climate.py @@ -322,7 +322,7 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): [ { "code": self._set_humidity_dpcode, - "value": self._set_humidity_type.scale_value(humidity), + "value": self._set_humidity_type.scale_value_back(humidity), } ] ) @@ -364,7 +364,9 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): { "code": self._set_temperature_dpcode, "value": round( - self._set_temperature_type.scale_value(kwargs["temperature"]) + self._set_temperature_type.scale_value_back( + kwargs["temperature"] + ) ), } ] diff --git a/homeassistant/components/tuya/humidifier.py b/homeassistant/components/tuya/humidifier.py index b9fc10790e3..3169000fcba 100644 --- a/homeassistant/components/tuya/humidifier.py +++ b/homeassistant/components/tuya/humidifier.py @@ -165,7 +165,7 @@ class TuyaHumidifierEntity(TuyaEntity, HumidifierEntity): [ { "code": self.entity_description.humidity, - "value": self._set_humidity_type.scale_value(humidity), + "value": self._set_humidity_type.scale_value_back(humidity), } ] ) From ca74d3c79edbfb201348fe04931d635436e7e579 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 18 Nov 2021 15:56:22 -0800 Subject: [PATCH 170/174] Store: copy pending data (#59934) --- homeassistant/helpers/storage.py | 5 ++++ tests/components/unifi/test_controller.py | 2 +- tests/components/unifi/test_device_tracker.py | 2 +- tests/components/unifi/test_init.py | 3 ++- tests/components/unifi/test_switch.py | 5 ++-- .../trace/automation_saved_traces.json | 1 + tests/fixtures/trace/script_saved_traces.json | 1 + tests/helpers/test_storage.py | 27 ++++++++++++++++--- 8 files changed, 38 insertions(+), 8 deletions(-) diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index 116c9186149..9de54682d67 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio from collections.abc import Callable from contextlib import suppress +from copy import deepcopy from json import JSONEncoder import logging import os @@ -130,6 +131,10 @@ class Store: # If we didn't generate data yet, do it now. if "data_func" in data: data["data"] = data.pop("data_func")() + + # We make a copy because code might assume it's safe to mutate loaded data + # and we don't want that to mess with what we're trying to store. + data = deepcopy(data) else: data = await self.hass.async_add_executor_job( json_util.load_json, self.path diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index 41864f5dc3e..0f3447b3dd9 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -49,7 +49,7 @@ import homeassistant.util.dt as dt_util from tests.common import MockConfigEntry, async_fire_time_changed -DEFAULT_CONFIG_ENTRY_ID = 1 +DEFAULT_CONFIG_ENTRY_ID = "1" DEFAULT_HOST = "1.2.3.4" DEFAULT_SITE = "site_id" diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 5ab61ce21a7..384db693f1c 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -1089,7 +1089,7 @@ async def test_restoring_client(hass, aioclient_mock): data=ENTRY_CONFIG, source="test", options={}, - entry_id=1, + entry_id="1", ) registry = er.async_get(hass) diff --git a/tests/components/unifi/test_init.py b/tests/components/unifi/test_init.py index 1d1fcad9cbe..85733d6d686 100644 --- a/tests/components/unifi/test_init.py +++ b/tests/components/unifi/test_init.py @@ -14,7 +14,7 @@ from .test_controller import ( setup_unifi_integration, ) -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, flush_store async def test_setup_with_no_config(hass): @@ -110,6 +110,7 @@ async def test_wireless_clients(hass, hass_storage, aioclient_mock): config_entry = await setup_unifi_integration( hass, aioclient_mock, clients_response=[client_1, client_2] ) + await flush_store(hass.data[unifi.UNIFI_WIRELESS_CLIENTS]._store) for mac in [ "00:00:00:00:00:00", diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index 85850062583..796434c5cd9 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -22,6 +22,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from .test_controller import ( CONTROLLER_HOST, + DEFAULT_CONFIG_ENTRY_ID, DESCRIPTION, ENTRY_CONFIG, setup_unifi_integration, @@ -857,7 +858,7 @@ async def test_restore_client_succeed(hass, aioclient_mock): data=ENTRY_CONFIG, source="test", options={}, - entry_id=1, + entry_id=DEFAULT_CONFIG_ENTRY_ID, ) registry = er.async_get(hass) @@ -947,7 +948,7 @@ async def test_restore_client_no_old_state(hass, aioclient_mock): data=ENTRY_CONFIG, source="test", options={}, - entry_id=1, + entry_id=DEFAULT_CONFIG_ENTRY_ID, ) registry = er.async_get(hass) diff --git a/tests/fixtures/trace/automation_saved_traces.json b/tests/fixtures/trace/automation_saved_traces.json index 45bcfffc157..7f6ed56a8bc 100644 --- a/tests/fixtures/trace/automation_saved_traces.json +++ b/tests/fixtures/trace/automation_saved_traces.json @@ -1,5 +1,6 @@ { "version": 1, + "minor_version": 1, "key": "trace.saved_traces", "data": { "automation.sun": [ diff --git a/tests/fixtures/trace/script_saved_traces.json b/tests/fixtures/trace/script_saved_traces.json index 91677b2a47e..ccd2902d726 100644 --- a/tests/fixtures/trace/script_saved_traces.json +++ b/tests/fixtures/trace/script_saved_traces.json @@ -1,5 +1,6 @@ { "version": 1, + "minor_version": 1, "key": "trace.saved_traces", "data": { "script.sun": [ diff --git a/tests/helpers/test_storage.py b/tests/helpers/test_storage.py index 61bf9fa8d0e..74c333f2448 100644 --- a/tests/helpers/test_storage.py +++ b/tests/helpers/test_storage.py @@ -25,7 +25,7 @@ MOCK_DATA2 = {"goodbye": "cruel world"} @pytest.fixture def store(hass): """Fixture of a store that prevents writing on Home Assistant stop.""" - yield storage.Store(hass, MOCK_VERSION, MOCK_KEY) + return storage.Store(hass, MOCK_VERSION, MOCK_KEY) async def test_loading(hass, store): @@ -64,8 +64,8 @@ async def test_loading_parallel(hass, store, hass_storage, caplog): results = await asyncio.gather(store.async_load(), store.async_load()) - assert results[0] is MOCK_DATA - assert results[1] is MOCK_DATA + assert results[0] == MOCK_DATA + assert results[0] is results[1] assert caplog.text.count(f"Loading data for {store.key}") @@ -279,3 +279,24 @@ async def test_migrator_transforming_config(hass, store, hass_storage): "version": MOCK_VERSION, "data": data, } + + +async def test_changing_delayed_written_data(hass, store, hass_storage): + """Test changing data that is written with delay.""" + data_to_store = {"hello": "world"} + store.async_delay_save(lambda: data_to_store, 1) + assert store.key not in hass_storage + + loaded_data = await store.async_load() + assert loaded_data == data_to_store + assert loaded_data is not data_to_store + + loaded_data["hello"] = "earth" + + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=1)) + await hass.async_block_till_done() + assert hass_storage[store.key] == { + "version": MOCK_VERSION, + "key": MOCK_KEY, + "data": {"hello": "world"}, + } From 090c65488da56d27812464329e5c8d2f054d5ad3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 18 Nov 2021 16:18:05 -0800 Subject: [PATCH 171/174] Bumped version to 2021.11.5 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5005540dbf2..910b9fe9ec8 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 11 -PATCH_VERSION: Final = "4" +PATCH_VERSION: Final = "5" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From 04a36e0679c1cff064d1615d6b3526c9e551662e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 16 Nov 2021 16:54:08 +0100 Subject: [PATCH 172/174] Remove test_check_package_version_does_not_match (#59785) --- tests/util/test_package.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tests/util/test_package.py b/tests/util/test_package.py index 3006cb17c37..d6b7402a5b6 100644 --- a/tests/util/test_package.py +++ b/tests/util/test_package.py @@ -251,13 +251,6 @@ def test_check_package_global(): assert not package.is_installed(f"{installed_package}<{installed_version}") -def test_check_package_version_does_not_match(): - """Test for version mismatch.""" - installed_package = list(pkg_resources.working_set)[0].project_name - assert not package.is_installed(f"{installed_package}==999.999.999") - assert not package.is_installed(f"{installed_package}>=999.999.999") - - def test_check_package_zip(): """Test for an installed zip package.""" assert not package.is_installed(TEST_ZIP_REQ) From 66d91544e839d01cbf48861d0cfdc86df2751ac5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 18 Nov 2021 22:33:46 -0800 Subject: [PATCH 173/174] Fix some tests --- tests/fixtures/trace/automation_saved_traces.json | 1 - tests/fixtures/trace/script_saved_traces.json | 1 - 2 files changed, 2 deletions(-) diff --git a/tests/fixtures/trace/automation_saved_traces.json b/tests/fixtures/trace/automation_saved_traces.json index 7f6ed56a8bc..45bcfffc157 100644 --- a/tests/fixtures/trace/automation_saved_traces.json +++ b/tests/fixtures/trace/automation_saved_traces.json @@ -1,6 +1,5 @@ { "version": 1, - "minor_version": 1, "key": "trace.saved_traces", "data": { "automation.sun": [ diff --git a/tests/fixtures/trace/script_saved_traces.json b/tests/fixtures/trace/script_saved_traces.json index ccd2902d726..91677b2a47e 100644 --- a/tests/fixtures/trace/script_saved_traces.json +++ b/tests/fixtures/trace/script_saved_traces.json @@ -1,6 +1,5 @@ { "version": 1, - "minor_version": 1, "key": "trace.saved_traces", "data": { "script.sun": [ From 754fba1fb7c472931205acff2f680820768a1d60 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 19 Nov 2021 10:14:12 -0600 Subject: [PATCH 174/174] Ignore non-Sonos SSDP devices with Sonos-like identifiers (#59809) --- homeassistant/components/sonos/__init__.py | 8 +++++--- tests/components/sonos/conftest.py | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index 5e02832b05a..72e5a33ca28 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -267,11 +267,13 @@ class SonosDiscoveryManager: if change == ssdp.SsdpChange.BYEBYE: return + uid = info.get(ssdp.ATTR_UPNP_UDN) + if not uid.startswith("uuid:RINCON_"): + return + + uid = uid[5:] discovered_ip = urlparse(info[ssdp.ATTR_SSDP_LOCATION]).hostname boot_seqnum = info.get("X-RINCON-BOOTSEQ") - uid = info.get(ssdp.ATTR_UPNP_UDN) - if uid.startswith("uuid:"): - uid = uid[5:] self.async_discovered_player( "SSDP", info, discovered_ip, uid, boot_seqnum, info.get("modelName"), None ) diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index f650c6e8fef..8a3a6571faa 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -93,7 +93,7 @@ def discover_fixture(soco): async def do_callback(hass, callback, *args, **kwargs): await callback( { - ssdp.ATTR_UPNP_UDN: soco.uid, + ssdp.ATTR_UPNP_UDN: f"uuid:{soco.uid}", ssdp.ATTR_SSDP_LOCATION: f"http://{soco.ip_address}/", }, ssdp.SsdpChange.ALIVE,