From 5deb78a0dd26e663f54723c320c7452ba4d3ada0 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 18 May 2022 19:59:35 +0200 Subject: [PATCH 01/15] Refresh camera stream source of Synology DSM connected cameras (#70938) Co-authored-by: Paulus Schoutsen --- .../components/synology_dsm/__init__.py | 24 +++++++++++++--- .../components/synology_dsm/camera.py | 28 ++++++++++++++++++- .../components/synology_dsm/const.py | 3 ++ 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index 1151bf128cc..0881d5a85e9 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -28,6 +28,7 @@ from homeassistant.helpers.device_registry import ( DeviceEntry, async_get_registry as get_dev_reg, ) +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .common import SynoApi @@ -41,6 +42,7 @@ from .const import ( EXCEPTION_DETAILS, EXCEPTION_UNKNOWN, PLATFORMS, + SIGNAL_CAMERA_SOURCE_CHANGED, SYNO_API, SYSTEM_LOADED, UNDO_UPDATE_LISTENER, @@ -128,6 +130,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return None surveillance_station = api.surveillance_station + current_data: dict[str, SynoCamera] = { + camera.id: camera for camera in surveillance_station.get_all_cameras() + } try: async with async_timeout.timeout(30): @@ -135,12 +140,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except SynologyDSMAPIErrorException as err: raise UpdateFailed(f"Error communicating with API: {err}") from err - return { - "cameras": { - camera.id: camera for camera in surveillance_station.get_all_cameras() - } + new_data: dict[str, SynoCamera] = { + camera.id: camera for camera in surveillance_station.get_all_cameras() } + for cam_id, cam_data_new in new_data.items(): + if ( + (cam_data_current := current_data.get(cam_id)) is not None + and cam_data_current.live_view.rtsp != cam_data_new.live_view.rtsp + ): + async_dispatcher_send( + hass, + f"{SIGNAL_CAMERA_SOURCE_CHANGED}_{entry.entry_id}_{cam_id}", + cam_data_new.live_view.rtsp, + ) + + return {"cameras": new_data} + async def async_coordinator_update_data_central() -> None: """Fetch all device and sensor data from api.""" try: diff --git a/homeassistant/components/synology_dsm/camera.py b/homeassistant/components/synology_dsm/camera.py index c6d44d8883d..cab2536187c 100644 --- a/homeassistant/components/synology_dsm/camera.py +++ b/homeassistant/components/synology_dsm/camera.py @@ -16,7 +16,8 @@ from homeassistant.components.camera import ( CameraEntityFeature, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -27,6 +28,7 @@ from .const import ( COORDINATOR_CAMERAS, DEFAULT_SNAPSHOT_QUALITY, DOMAIN, + SIGNAL_CAMERA_SOURCE_CHANGED, SYNO_API, ) from .entity import SynologyDSMBaseEntity, SynologyDSMEntityDescription @@ -130,6 +132,29 @@ class SynoDSMCamera(SynologyDSMBaseEntity, Camera): """Return the camera motion detection status.""" return self.camera_data.is_motion_detection_enabled # type: ignore[no-any-return] + def _listen_source_updates(self) -> None: + """Listen for camera source changed events.""" + + @callback + def _handle_signal(url: str) -> None: + if self.stream: + _LOGGER.debug("Update stream URL for camera %s", self.camera_data.name) + self.stream.update_source(url) + + assert self.platform + assert self.platform.config_entry + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{SIGNAL_CAMERA_SOURCE_CHANGED}_{self.platform.config_entry.entry_id}_{self.camera_data.id}", + _handle_signal, + ) + ) + + async def async_added_to_hass(self) -> None: + """Subscribe to signal.""" + self._listen_source_updates() + def camera_image( self, width: int | None = None, height: int | None = None ) -> bytes | None: @@ -162,6 +187,7 @@ class SynoDSMCamera(SynologyDSMBaseEntity, Camera): ) if not self.available: return None + return self.camera_data.live_view.rtsp # type: ignore[no-any-return] def enable_motion_detection(self) -> None: diff --git a/homeassistant/components/synology_dsm/const.py b/homeassistant/components/synology_dsm/const.py index 1b4e5f0bb36..f716130a5e4 100644 --- a/homeassistant/components/synology_dsm/const.py +++ b/homeassistant/components/synology_dsm/const.py @@ -43,6 +43,9 @@ DEFAULT_SNAPSHOT_QUALITY = SNAPSHOT_PROFILE_BALANCED ENTITY_UNIT_LOAD = "load" +# Signals +SIGNAL_CAMERA_SOURCE_CHANGED = "synology_dsm.camera_stream_source_changed" + # Services SERVICE_REBOOT = "reboot" SERVICE_SHUTDOWN = "shutdown" From a3bd911ce396956e2cee038b2ea8d39011e12db0 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Wed, 18 May 2022 19:29:02 +0200 Subject: [PATCH 02/15] Warn user if "model" key is missing from Shelly firmware (#71612) Co-authored-by: Paulus Schoutsen --- .../components/shelly/config_flow.py | 33 ++++++++++++------- homeassistant/components/shelly/strings.json | 3 +- .../components/shelly/translations/en.json | 1 + tests/components/shelly/test_config_flow.py | 23 +++++++++++++ 4 files changed, 47 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index 9311be1a49e..abcfe689e93 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -119,6 +119,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) except HTTP_CONNECT_ERRORS: errors["base"] = "cannot_connect" + except KeyError: + errors["base"] = "firmware_not_fully_provisioned" except Exception: # pylint: disable=broad-except LOGGER.exception("Unexpected exception") errors["base"] = "unknown" @@ -160,6 +162,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "cannot_connect" except aioshelly.exceptions.JSONRPCError: errors["base"] = "cannot_connect" + except KeyError: + errors["base"] = "firmware_not_fully_provisioned" except Exception: # pylint: disable=broad-except LOGGER.exception("Unexpected exception") errors["base"] = "unknown" @@ -219,6 +223,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: self.device_info = await validate_input(self.hass, self.host, self.info, {}) + except KeyError: + LOGGER.debug("Shelly host %s firmware not fully provisioned", self.host) except HTTP_CONNECT_ERRORS: return self.async_abort(reason="cannot_connect") @@ -229,18 +235,21 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Handle discovery confirm.""" errors: dict[str, str] = {} - if user_input is not None: - return self.async_create_entry( - title=self.device_info["title"], - data={ - "host": self.host, - CONF_SLEEP_PERIOD: self.device_info[CONF_SLEEP_PERIOD], - "model": self.device_info["model"], - "gen": self.device_info["gen"], - }, - ) - - self._set_confirm_only() + try: + if user_input is not None: + return self.async_create_entry( + title=self.device_info["title"], + data={ + "host": self.host, + CONF_SLEEP_PERIOD: self.device_info[CONF_SLEEP_PERIOD], + "model": self.device_info["model"], + "gen": self.device_info["gen"], + }, + ) + except KeyError: + errors["base"] = "firmware_not_fully_provisioned" + else: + self._set_confirm_only() return self.async_show_form( step_id="confirm_discovery", diff --git a/homeassistant/components/shelly/strings.json b/homeassistant/components/shelly/strings.json index 209ae6682b8..db1c6043187 100644 --- a/homeassistant/components/shelly/strings.json +++ b/homeassistant/components/shelly/strings.json @@ -21,7 +21,8 @@ "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "unknown": "[%key:common::config_flow::error::unknown%]" + "unknown": "[%key:common::config_flow::error::unknown%]", + "firmware_not_fully_provisioned": "Device not fully provisioned. Please contact Shelly support" }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", diff --git a/homeassistant/components/shelly/translations/en.json b/homeassistant/components/shelly/translations/en.json index c23eb13840c..f3e882c3016 100644 --- a/homeassistant/components/shelly/translations/en.json +++ b/homeassistant/components/shelly/translations/en.json @@ -6,6 +6,7 @@ }, "error": { "cannot_connect": "Failed to connect", + "firmware_not_fully_provisioned": "Device not fully provisioned. Please contact Shelly support", "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index 1293fe92760..713999de36f 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -225,6 +225,29 @@ async def test_form_errors_get_info(hass, error): assert result2["errors"] == {"base": base_error} +@pytest.mark.parametrize("error", [(KeyError, "firmware_not_fully_provisioned")]) +async def test_form_missing_key_get_info(hass, error): + """Test we handle missing key.""" + exc, base_error = error + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + with patch( + "aioshelly.common.get_info", + return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False, "gen": "2"}, + ), patch( + "homeassistant.components.shelly.config_flow.validate_input", + side_effect=KeyError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"host": "1.1.1.1"}, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": base_error} + + @pytest.mark.parametrize( "error", [(asyncio.TimeoutError, "cannot_connect"), (ValueError, "unknown")] ) From 5fbc4b8dbaab4f41e8fd030f8286e1912a0f4586 Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Fri, 13 May 2022 21:30:44 +1000 Subject: [PATCH 03/15] Remove LIFX bulb discovery from the inflight list if it fails to connect (#71673) Remove the bulb discovery from the inflight list if it fails to connect Signed-off-by: Avi Miller --- homeassistant/components/lifx/light.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index 428ae8745e0..c4df24a0cb0 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -419,6 +419,8 @@ class LIFXManager: if color_resp is None or version_resp is None: _LOGGER.error("Failed to connect to %s", bulb.ip_addr) bulb.registered = False + if bulb.mac_addr in self.discoveries_inflight: + self.discoveries_inflight.pop(bulb.mac_addr) else: bulb.timeout = MESSAGE_TIMEOUT bulb.retry_count = MESSAGE_RETRIES From 107615ebefdb0b4db4985c560077276da7a1dd8c Mon Sep 17 00:00:00 2001 From: rappenze Date: Sun, 15 May 2022 15:02:05 +0200 Subject: [PATCH 04/15] Limit parallel requests in fibaro light (#71762) --- homeassistant/components/fibaro/light.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/fibaro/light.py b/homeassistant/components/fibaro/light.py index 08a9e651668..0115e0301c3 100644 --- a/homeassistant/components/fibaro/light.py +++ b/homeassistant/components/fibaro/light.py @@ -23,6 +23,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import FIBARO_DEVICES, FibaroDevice from .const import DOMAIN +PARALLEL_UPDATES = 2 + def scaleto255(value: int | None) -> int: """Scale the input value from 0-100 to 0-255.""" From 2448661371246bb0192fda88c6b11376686c91c2 Mon Sep 17 00:00:00 2001 From: Ethan Madden Date: Mon, 16 May 2022 01:30:49 -0700 Subject: [PATCH 05/15] Fix VeSync air_quality fan attribute (#71771) * Refactor attribute inclusion for VeSync fans. A recent change to pyvesync (introduced in 2.2) changed `air_quality` to refer to air quality as an integer representation of perceived air quality rather than a direct reading of the PM2.5 sensor. With 2.3 the PM2.5 sensor access was restored as `air_quality_value`. Unfortunately, `air_quality_value` was not added as an attribute on the fan object, and rather only exists in the `details` dictionary on the fan object. * Update homeassistant/components/vesync/fan.py Co-authored-by: Martin Hjelmare * Rename `air_quality_value` attribute to `pm25` This should make it more clear what the attribute actually represents * `air_quality` attribute reports `air_quality_value` This restores previous behavior for this integration to what it was before the `pyvesync==2.02` upgrade, using the `air_quality` attribute to report pm2.5 concentrations (formerly `air_quality`) rather the vague measurement now reported by `air_quality`. Co-authored-by: Martin Hjelmare --- homeassistant/components/vesync/fan.py | 4 ++-- homeassistant/components/vesync/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/vesync/fan.py b/homeassistant/components/vesync/fan.py index e37a6c8893e..f16a785ee1e 100644 --- a/homeassistant/components/vesync/fan.py +++ b/homeassistant/components/vesync/fan.py @@ -171,8 +171,8 @@ class VeSyncFanHA(VeSyncDevice, FanEntity): if hasattr(self.smartfan, "night_light"): attr["night_light"] = self.smartfan.night_light - if hasattr(self.smartfan, "air_quality"): - attr["air_quality"] = self.smartfan.air_quality + if self.smartfan.details.get("air_quality_value") is not None: + attr["air_quality"] = self.smartfan.details["air_quality_value"] if hasattr(self.smartfan, "mode"): attr["mode"] = self.smartfan.mode diff --git a/homeassistant/components/vesync/manifest.json b/homeassistant/components/vesync/manifest.json index c93e070a484..49be473b748 100644 --- a/homeassistant/components/vesync/manifest.json +++ b/homeassistant/components/vesync/manifest.json @@ -3,7 +3,7 @@ "name": "VeSync", "documentation": "https://www.home-assistant.io/integrations/vesync", "codeowners": ["@markperdue", "@webdjoe", "@thegardenmonkey"], - "requirements": ["pyvesync==2.0.2"], + "requirements": ["pyvesync==2.0.3"], "config_flow": true, "iot_class": "cloud_polling", "loggers": ["pyvesync"] diff --git a/requirements_all.txt b/requirements_all.txt index ef48c8117f5..011e759b481 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1996,7 +1996,7 @@ pyvera==0.3.13 pyversasense==0.0.6 # homeassistant.components.vesync -pyvesync==2.0.2 +pyvesync==2.0.3 # homeassistant.components.vizio pyvizio==0.1.57 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cb683ae0bab..49bbec71c35 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1313,7 +1313,7 @@ pyuptimerobot==22.2.0 pyvera==0.3.13 # homeassistant.components.vesync -pyvesync==2.0.2 +pyvesync==2.0.3 # homeassistant.components.vizio pyvizio==0.1.57 From 5f3c7f11d8bd572d7e1525c72353c59bd472b0f2 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Fri, 13 May 2022 18:42:33 -0400 Subject: [PATCH 06/15] Fix handling package detection for latest UniFi Protect beta (#71821) Co-authored-by: J. Nick Koston --- homeassistant/components/unifiprotect/config_flow.py | 8 ++++++-- homeassistant/components/unifiprotect/manifest.json | 2 +- homeassistant/components/unifiprotect/select.py | 2 +- homeassistant/components/unifiprotect/switch.py | 9 +++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/unifiprotect/test_switch.py | 10 +++++++--- 7 files changed, 26 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/unifiprotect/config_flow.py b/homeassistant/components/unifiprotect/config_flow.py index 0cfce44a6ca..27108d24eaa 100644 --- a/homeassistant/components/unifiprotect/config_flow.py +++ b/homeassistant/components/unifiprotect/config_flow.py @@ -143,7 +143,9 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): user_input[CONF_VERIFY_SSL] = False nvr_data, errors = await self._async_get_nvr_data(user_input) if nvr_data and not errors: - return self._async_create_entry(nvr_data.name, user_input) + return self._async_create_entry( + nvr_data.name or nvr_data.type, user_input + ) placeholders = { "name": discovery_info["hostname"] @@ -289,7 +291,9 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(nvr_data.mac) self._abort_if_unique_id_configured() - return self._async_create_entry(nvr_data.name, user_input) + return self._async_create_entry( + nvr_data.name or nvr_data.type, user_input + ) user_input = user_input or {} return self.async_show_form( diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 3c3b461ed4f..3efad51db31 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==3.4.1", "unifi-discovery==1.1.2"], + "requirements": ["pyunifiprotect==3.5.1", "unifi-discovery==1.1.2"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/homeassistant/components/unifiprotect/select.py b/homeassistant/components/unifiprotect/select.py index b7b53ff81c8..f0500ea54e5 100644 --- a/homeassistant/components/unifiprotect/select.py +++ b/homeassistant/components/unifiprotect/select.py @@ -140,7 +140,7 @@ def _get_doorbell_options(api: ProtectApiClient) -> list[dict[str, Any]]: def _get_paired_camera_options(api: ProtectApiClient) -> list[dict[str, Any]]: options = [{"id": TYPE_EMPTY_VALUE, "name": "Not Paired"}] for camera in api.bootstrap.cameras.values(): - options.append({"id": camera.id, "name": camera.name}) + options.append({"id": camera.id, "name": camera.name or camera.type}) return options diff --git a/homeassistant/components/unifiprotect/switch.py b/homeassistant/components/unifiprotect/switch.py index 2257f399f75..85a089994f8 100644 --- a/homeassistant/components/unifiprotect/switch.py +++ b/homeassistant/components/unifiprotect/switch.py @@ -159,6 +159,15 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ufp_value="is_face_detection_on", ufp_set_method="set_face_detection", ), + ProtectSwitchEntityDescription( + key="smart_package", + name="Detections: Package", + icon="mdi:package-variant-closed", + entity_category=EntityCategory.CONFIG, + ufp_required_field="can_detect_package", + ufp_value="is_package_detection_on", + ufp_set_method="set_package_detection", + ), ) SENSE_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( diff --git a/requirements_all.txt b/requirements_all.txt index 011e759b481..a7cef9f1b43 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1981,7 +1981,7 @@ pytrafikverket==0.1.6.2 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==3.4.1 +pyunifiprotect==3.5.1 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 49bbec71c35..eb2be965ee7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1304,7 +1304,7 @@ pytrafikverket==0.1.6.2 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==3.4.1 +pyunifiprotect==3.5.1 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/tests/components/unifiprotect/test_switch.py b/tests/components/unifiprotect/test_switch.py index 0a3ac92076e..c54d04a8cb7 100644 --- a/tests/components/unifiprotect/test_switch.py +++ b/tests/components/unifiprotect/test_switch.py @@ -26,9 +26,13 @@ from .conftest import ( ids_from_device_description, ) -CAMERA_SWITCHES_NO_FACE = [d for d in CAMERA_SWITCHES if d.name != "Detections: Face"] +CAMERA_SWITCHES_BASIC = [ + d + for d in CAMERA_SWITCHES + if d.name != "Detections: Face" and d.name != "Detections: Package" +] CAMERA_SWITCHES_NO_EXTRA = [ - d for d in CAMERA_SWITCHES_NO_FACE if d.name not in ("High FPS", "Privacy Mode") + d for d in CAMERA_SWITCHES_BASIC if d.name not in ("High FPS", "Privacy Mode") ] @@ -253,7 +257,7 @@ async def test_switch_setup_camera_all( entity_registry = er.async_get(hass) - for description in CAMERA_SWITCHES_NO_FACE: + for description in CAMERA_SWITCHES_BASIC: unique_id, entity_id = ids_from_device_description( Platform.SWITCH, camera, description ) From 81f9cc40dd8eaad557a58829fc488c1624b20083 Mon Sep 17 00:00:00 2001 From: RadekHvizdos <10856567+RadekHvizdos@users.noreply.github.com> Date: Sun, 15 May 2022 11:29:35 +0200 Subject: [PATCH 07/15] Add missing Shelly Cover sensors bugfix (#71831) Switching Shelly Plus 2PM from switch to cover mode results in missing sensors for Power, Voltage, Energy and Temperature. These parameters are still available in the API, but need to be accessed via "cover" key instead of "switch" key. This change adds the missing sensors. --- homeassistant/components/shelly/utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index df5a75a7ed9..77c09283fbf 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -297,6 +297,9 @@ def get_rpc_key_instances(keys_dict: dict[str, Any], key: str) -> list[str]: if key in keys_dict: return [key] + if key == "switch" and "cover:0" in keys_dict: + key = "cover" + keys_list: list[str] = [] for i in range(MAX_RPC_KEY_INSTANCES): key_inst = f"{key}:{i}" From ce39461810194d8f2f4b2fb1e351c0fa24efc8ae Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 15 May 2022 20:36:57 +0200 Subject: [PATCH 08/15] Revert changing `pysnmp` to `pysnmplib` (#71901) --- homeassistant/components/brother/manifest.json | 2 +- homeassistant/components/snmp/manifest.json | 2 +- requirements_all.txt | 4 ++-- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/brother/manifest.json b/homeassistant/components/brother/manifest.json index c230373ea36..aaf1af72db9 100644 --- a/homeassistant/components/brother/manifest.json +++ b/homeassistant/components/brother/manifest.json @@ -3,7 +3,7 @@ "name": "Brother Printer", "documentation": "https://www.home-assistant.io/integrations/brother", "codeowners": ["@bieniu"], - "requirements": ["brother==1.2.0"], + "requirements": ["brother==1.1.0"], "zeroconf": [ { "type": "_printer._tcp.local.", diff --git a/homeassistant/components/snmp/manifest.json b/homeassistant/components/snmp/manifest.json index ef0213e82dc..76df9e18606 100644 --- a/homeassistant/components/snmp/manifest.json +++ b/homeassistant/components/snmp/manifest.json @@ -2,7 +2,7 @@ "domain": "snmp", "name": "SNMP", "documentation": "https://www.home-assistant.io/integrations/snmp", - "requirements": ["pysnmplib==5.0.10"], + "requirements": ["pysnmp==4.4.12"], "codeowners": [], "iot_class": "local_polling", "loggers": ["pyasn1", "pysmi", "pysnmp"] diff --git a/requirements_all.txt b/requirements_all.txt index a7cef9f1b43..b851ba6b9d2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -436,7 +436,7 @@ bravia-tv==1.0.11 broadlink==0.18.1 # homeassistant.components.brother -brother==1.2.0 +brother==1.1.0 # homeassistant.components.brottsplatskartan brottsplatskartan==0.0.1 @@ -1829,7 +1829,7 @@ pysmarty==0.8 pysml==0.0.7 # homeassistant.components.snmp -pysnmplib==5.0.10 +pysnmp==4.4.12 # homeassistant.components.soma pysoma==0.0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index eb2be965ee7..192a5db2d40 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -327,7 +327,7 @@ bravia-tv==1.0.11 broadlink==0.18.1 # homeassistant.components.brother -brother==1.2.0 +brother==1.1.0 # homeassistant.components.brunt brunt==1.2.0 From a0d1c5d1e6eb8a2f27add6bcfac7483a976b7909 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 18 May 2022 19:01:56 +0200 Subject: [PATCH 09/15] Suppress Upnp error in SamsungTV resubscribe (#71925) * Suppress Upnp error in SamsungTV resubscribe * Supress UpnpCommunicationError instead * Log resubscribe errors * Add tests * Add exc_info --- .../components/samsungtv/media_player.py | 5 +- .../components/samsungtv/test_media_player.py | 48 +++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index a0b70af2db5..e064b016dc6 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -12,6 +12,7 @@ from async_upnp_client.client import UpnpDevice, UpnpService, UpnpStateVariable from async_upnp_client.client_factory import UpnpFactory from async_upnp_client.exceptions import ( UpnpActionResponseError, + UpnpCommunicationError, UpnpConnectionError, UpnpError, UpnpResponseError, @@ -307,8 +308,10 @@ class SamsungTVDevice(MediaPlayerEntity): async def _async_resubscribe_dmr(self) -> None: assert self._dmr_device - with contextlib.suppress(UpnpConnectionError): + try: await self._dmr_device.async_subscribe_services(auto_resubscribe=True) + except UpnpCommunicationError as err: + LOGGER.debug("Device rejected re-subscription: %r", err, exc_info=True) async def _async_shutdown_dmr(self) -> None: """Handle removal.""" diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index e8407a86a3e..1686b8d6a95 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -6,6 +6,8 @@ from unittest.mock import DEFAULT as DEFAULT_MOCK, AsyncMock, Mock, call, patch from async_upnp_client.exceptions import ( UpnpActionResponseError, + UpnpCommunicationError, + UpnpConnectionError, UpnpError, UpnpResponseError, ) @@ -1505,3 +1507,49 @@ async def test_upnp_re_subscribe_events( assert state.state == STATE_ON assert dmr_device.async_subscribe_services.call_count == 2 assert dmr_device.async_unsubscribe_services.call_count == 1 + + +@pytest.mark.usefixtures("rest_api", "upnp_notify_server") +@pytest.mark.parametrize( + "error", + {UpnpConnectionError(), UpnpCommunicationError(), UpnpResponseError(status=400)}, +) +async def test_upnp_failed_re_subscribe_events( + hass: HomeAssistant, + remotews: Mock, + dmr_device: Mock, + mock_now: datetime, + caplog: pytest.LogCaptureFixture, + error: Exception, +) -> None: + """Test for Upnp event feedback.""" + await setup_samsungtv_entry(hass, MOCK_ENTRY_WS) + + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_ON + assert dmr_device.async_subscribe_services.call_count == 1 + assert dmr_device.async_unsubscribe_services.call_count == 0 + + with patch.object( + remotews, "start_listening", side_effect=WebSocketException("Boom") + ), patch.object(remotews, "is_alive", return_value=False): + next_update = mock_now + timedelta(minutes=5) + with patch("homeassistant.util.dt.utcnow", return_value=next_update): + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_OFF + assert dmr_device.async_subscribe_services.call_count == 1 + assert dmr_device.async_unsubscribe_services.call_count == 1 + + next_update = mock_now + timedelta(minutes=10) + with patch("homeassistant.util.dt.utcnow", return_value=next_update), patch.object( + dmr_device, "async_subscribe_services", side_effect=error + ): + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_ON + assert "Device rejected re-subscription" in caplog.text From 6155a642226d5e40f79b59d00de199678af1ca02 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 16 May 2022 12:50:03 +0200 Subject: [PATCH 10/15] Properly handle Shelly gen2 device disconnect (#71937) --- homeassistant/components/shelly/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index d29584c4e83..41a9e68fbdd 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -812,7 +812,7 @@ class RpcPollingWrapper(update_coordinator.DataUpdateCoordinator): LOGGER.debug("Polling Shelly RPC Device - %s", self.name) async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): await self.device.update_status() - except OSError as err: + except (OSError, aioshelly.exceptions.RPCTimeout) as err: raise update_coordinator.UpdateFailed("Device disconnected") from err @property From d34d3baa07ab7a17a5644e049a3075e87f50872e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 May 2022 12:28:04 -0500 Subject: [PATCH 11/15] Include initial state in history_stats count (#71952) --- .../components/history_stats/data.py | 14 +++++----- .../components/history_stats/sensor.py | 2 +- tests/components/history_stats/test_sensor.py | 26 +++++++++---------- 3 files changed, 20 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/history_stats/data.py b/homeassistant/components/history_stats/data.py index 3d21cca6b6d..8153557422d 100644 --- a/homeassistant/components/history_stats/data.py +++ b/homeassistant/components/history_stats/data.py @@ -19,7 +19,7 @@ class HistoryStatsState: """The current stats of the history stats.""" hours_matched: float | None - changes_to_match_state: int | None + match_count: int | None period: tuple[datetime.datetime, datetime.datetime] @@ -121,14 +121,12 @@ class HistoryStats: self._state = HistoryStatsState(None, None, self._period) return self._state - hours_matched, changes_to_match_state = self._async_compute_hours_and_changes( + hours_matched, match_count = self._async_compute_hours_and_changes( now_timestamp, current_period_start_timestamp, current_period_end_timestamp, ) - self._state = HistoryStatsState( - hours_matched, changes_to_match_state, self._period - ) + self._state = HistoryStatsState(hours_matched, match_count, self._period) return self._state def _update_from_database( @@ -156,7 +154,7 @@ class HistoryStats: ) last_state_change_timestamp = start_timestamp elapsed = 0.0 - changes_to_match_state = 0 + match_count = 1 if previous_state_matches else 0 # Make calculations for item in self._history_current_period: @@ -166,7 +164,7 @@ class HistoryStats: if previous_state_matches: elapsed += state_change_timestamp - last_state_change_timestamp elif current_state_matches: - changes_to_match_state += 1 + match_count += 1 previous_state_matches = current_state_matches last_state_change_timestamp = state_change_timestamp @@ -178,4 +176,4 @@ class HistoryStats: # Save value in hours hours_matched = elapsed / 3600 - return hours_matched, changes_to_match_state + return hours_matched, match_count diff --git a/homeassistant/components/history_stats/sensor.py b/homeassistant/components/history_stats/sensor.py index b3e64106d9f..b0ce1a8fca5 100644 --- a/homeassistant/components/history_stats/sensor.py +++ b/homeassistant/components/history_stats/sensor.py @@ -166,4 +166,4 @@ class HistoryStatsSensor(HistoryStatsSensorBase): elif self._type == CONF_TYPE_RATIO: self._attr_native_value = pretty_ratio(state.hours_matched, state.period) elif self._type == CONF_TYPE_COUNT: - self._attr_native_value = state.changes_to_match_state + self._attr_native_value = state.match_count diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py index 4f56edaa291..b375a8f63c4 100644 --- a/tests/components/history_stats/test_sensor.py +++ b/tests/components/history_stats/test_sensor.py @@ -438,7 +438,7 @@ async def test_measure(hass, recorder_mock): assert hass.states.get("sensor.sensor1").state == "0.83" assert hass.states.get("sensor.sensor2").state == "0.83" - assert hass.states.get("sensor.sensor3").state == "1" + assert hass.states.get("sensor.sensor3").state == "2" assert hass.states.get("sensor.sensor4").state == "83.3" @@ -519,7 +519,7 @@ async def test_async_on_entire_period(hass, recorder_mock): assert hass.states.get("sensor.on_sensor1").state == "1.0" assert hass.states.get("sensor.on_sensor2").state == "1.0" - assert hass.states.get("sensor.on_sensor3").state == "0" + assert hass.states.get("sensor.on_sensor3").state == "1" assert hass.states.get("sensor.on_sensor4").state == "100.0" @@ -886,7 +886,7 @@ async def test_async_start_from_history_and_switch_to_watching_state_changes_mul assert hass.states.get("sensor.sensor1").state == "0.0" assert hass.states.get("sensor.sensor2").state == "0.0" - assert hass.states.get("sensor.sensor3").state == "0" + assert hass.states.get("sensor.sensor3").state == "1" assert hass.states.get("sensor.sensor4").state == "0.0" one_hour_in = start_time + timedelta(minutes=60) @@ -896,7 +896,7 @@ async def test_async_start_from_history_and_switch_to_watching_state_changes_mul assert hass.states.get("sensor.sensor1").state == "1.0" assert hass.states.get("sensor.sensor2").state == "1.0" - assert hass.states.get("sensor.sensor3").state == "0" + assert hass.states.get("sensor.sensor3").state == "1" assert hass.states.get("sensor.sensor4").state == "50.0" turn_off_time = start_time + timedelta(minutes=90) @@ -908,7 +908,7 @@ async def test_async_start_from_history_and_switch_to_watching_state_changes_mul assert hass.states.get("sensor.sensor1").state == "1.5" assert hass.states.get("sensor.sensor2").state == "1.5" - assert hass.states.get("sensor.sensor3").state == "0" + assert hass.states.get("sensor.sensor3").state == "1" assert hass.states.get("sensor.sensor4").state == "75.0" turn_back_on_time = start_time + timedelta(minutes=105) @@ -918,7 +918,7 @@ async def test_async_start_from_history_and_switch_to_watching_state_changes_mul assert hass.states.get("sensor.sensor1").state == "1.5" assert hass.states.get("sensor.sensor2").state == "1.5" - assert hass.states.get("sensor.sensor3").state == "0" + assert hass.states.get("sensor.sensor3").state == "1" assert hass.states.get("sensor.sensor4").state == "75.0" with freeze_time(turn_back_on_time): @@ -927,7 +927,7 @@ async def test_async_start_from_history_and_switch_to_watching_state_changes_mul assert hass.states.get("sensor.sensor1").state == "1.5" assert hass.states.get("sensor.sensor2").state == "1.5" - assert hass.states.get("sensor.sensor3").state == "1" + assert hass.states.get("sensor.sensor3").state == "2" assert hass.states.get("sensor.sensor4").state == "75.0" end_time = start_time + timedelta(minutes=120) @@ -937,7 +937,7 @@ async def test_async_start_from_history_and_switch_to_watching_state_changes_mul assert hass.states.get("sensor.sensor1").state == "1.75" assert hass.states.get("sensor.sensor2").state == "1.75" - assert hass.states.get("sensor.sensor3").state == "1" + assert hass.states.get("sensor.sensor3").state == "2" assert hass.states.get("sensor.sensor4").state == "87.5" @@ -1198,7 +1198,7 @@ async def test_measure_sliding_window(hass, recorder_mock): assert hass.states.get("sensor.sensor1").state == "0.83" assert hass.states.get("sensor.sensor2").state == "0.83" - assert hass.states.get("sensor.sensor3").state == "1" + assert hass.states.get("sensor.sensor3").state == "2" assert hass.states.get("sensor.sensor4").state == "41.7" past_next_update = start_time + timedelta(minutes=30) @@ -1211,7 +1211,7 @@ async def test_measure_sliding_window(hass, recorder_mock): assert hass.states.get("sensor.sensor1").state == "0.83" assert hass.states.get("sensor.sensor2").state == "0.83" - assert hass.states.get("sensor.sensor3").state == "1" + assert hass.states.get("sensor.sensor3").state == "2" assert hass.states.get("sensor.sensor4").state == "41.7" @@ -1291,7 +1291,7 @@ async def test_measure_from_end_going_backwards(hass, recorder_mock): assert hass.states.get("sensor.sensor1").state == "0.83" assert hass.states.get("sensor.sensor2").state == "0.83" - assert hass.states.get("sensor.sensor3").state == "1" + assert hass.states.get("sensor.sensor3").state == "2" assert hass.states.get("sensor.sensor4").state == "83.3" past_next_update = start_time + timedelta(minutes=30) @@ -1304,7 +1304,7 @@ async def test_measure_from_end_going_backwards(hass, recorder_mock): assert hass.states.get("sensor.sensor1").state == "0.83" assert hass.states.get("sensor.sensor2").state == "0.83" - assert hass.states.get("sensor.sensor3").state == "1" + assert hass.states.get("sensor.sensor3").state == "2" assert hass.states.get("sensor.sensor4").state == "83.3" @@ -1385,7 +1385,7 @@ async def test_measure_cet(hass, recorder_mock): assert hass.states.get("sensor.sensor1").state == "0.83" assert hass.states.get("sensor.sensor2").state == "0.83" - assert hass.states.get("sensor.sensor3").state == "1" + assert hass.states.get("sensor.sensor3").state == "2" assert hass.states.get("sensor.sensor4").state == "83.3" From 6b0c7a2dd4cd52837cdf27f5b7aa80e673850589 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Wed, 18 May 2022 10:00:46 +0300 Subject: [PATCH 12/15] Fix filesize doing IO in event loop (#72038) --- homeassistant/components/filesize/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/filesize/sensor.py b/homeassistant/components/filesize/sensor.py index 22b8cd60d79..52bb869827a 100644 --- a/homeassistant/components/filesize/sensor.py +++ b/homeassistant/components/filesize/sensor.py @@ -132,7 +132,7 @@ class FileSizeCoordinator(DataUpdateCoordinator): async def _async_update_data(self) -> dict[str, float | int | datetime]: """Fetch file information.""" try: - statinfo = os.stat(self._path) + statinfo = await self.hass.async_add_executor_job(os.stat, self._path) except OSError as error: raise UpdateFailed(f"Can not retrieve file statistics {error}") from error From a1df9c33aabc37e280b8241baf0db2b1939035e0 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 18 May 2022 09:36:20 +0200 Subject: [PATCH 13/15] Ignore UpnpXmlContentError in SamsungTV (#72056) --- homeassistant/components/samsungtv/media_player.py | 6 ++++-- tests/components/samsungtv/test_media_player.py | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index e064b016dc6..423a778011d 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -16,6 +16,7 @@ from async_upnp_client.exceptions import ( UpnpConnectionError, UpnpError, UpnpResponseError, + UpnpXmlContentError, ) from async_upnp_client.profiles.dlna import DmrDevice from async_upnp_client.utils import async_get_local_ip @@ -271,11 +272,12 @@ class SamsungTVDevice(MediaPlayerEntity): # NETWORK,NONE upnp_factory = UpnpFactory(upnp_requester, non_strict=True) upnp_device: UpnpDevice | None = None - with contextlib.suppress(UpnpConnectionError, UpnpResponseError): + try: upnp_device = await upnp_factory.async_create_device( self._ssdp_rendering_control_location ) - if not upnp_device: + except (UpnpConnectionError, UpnpResponseError, UpnpXmlContentError) as err: + LOGGER.debug("Unable to create Upnp DMR device: %r", err, exc_info=True) return _, event_ip = await async_get_local_ip( self._ssdp_rendering_control_location, self.hass.loop diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index 1686b8d6a95..e548822f1d0 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -1370,6 +1370,7 @@ async def test_upnp_not_available( ) -> None: """Test for volume control when Upnp is not available.""" await setup_samsungtv_entry(hass, MOCK_ENTRY_WS) + assert "Unable to create Upnp DMR device" in caplog.text # Upnp action fails assert await hass.services.async_call( @@ -1387,6 +1388,7 @@ async def test_upnp_missing_service( ) -> None: """Test for volume control when Upnp is not available.""" await setup_samsungtv_entry(hass, MOCK_ENTRY_WS) + assert "Unable to create Upnp DMR device" in caplog.text # Upnp action fails assert await hass.services.async_call( From 996633553be1b472c73dcb4de0c27d989718fbf7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 18 May 2022 20:04:42 +0200 Subject: [PATCH 14/15] Cleanup unused import in SamsungTV (#72102) --- homeassistant/components/samsungtv/media_player.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index 423a778011d..6a884c59a87 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -3,7 +3,6 @@ from __future__ import annotations import asyncio from collections.abc import Coroutine, Sequence -import contextlib from datetime import datetime, timedelta from typing import Any From 1b107f6845833592d9b1214ec27e8eb4997828ee Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 18 May 2022 12:14:42 -0700 Subject: [PATCH 15/15] Bumped version to 2022.5.5 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5eb59e819da..e41189fda07 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 5 -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, 9, 0) diff --git a/setup.cfg b/setup.cfg index b492bd0a240..ab8af3d941b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.5.4 +version = 2022.5.5 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0