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/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.""" 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 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/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 diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index a0b70af2db5..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 @@ -12,9 +11,11 @@ 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, + UpnpXmlContentError, ) from async_upnp_client.profiles.dlna import DmrDevice from async_upnp_client.utils import async_get_local_ip @@ -270,11 +271,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 @@ -307,8 +309,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/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 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/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}" 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/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" 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/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/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/requirements_all.txt b/requirements_all.txt index ef48c8117f5..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 @@ -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 @@ -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..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 @@ -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 @@ -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 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 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" diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index e8407a86a3e..e548822f1d0 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, ) @@ -1368,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( @@ -1385,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( @@ -1505,3 +1509,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 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")] ) 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 )