Merge pull request #72107 from home-assistant/rc

This commit is contained in:
Paulus Schoutsen 2022-05-18 13:23:10 -07:00 committed by GitHub
commit 204762d23a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 222 additions and 67 deletions

View File

@ -3,7 +3,7 @@
"name": "Brother Printer", "name": "Brother Printer",
"documentation": "https://www.home-assistant.io/integrations/brother", "documentation": "https://www.home-assistant.io/integrations/brother",
"codeowners": ["@bieniu"], "codeowners": ["@bieniu"],
"requirements": ["brother==1.2.0"], "requirements": ["brother==1.1.0"],
"zeroconf": [ "zeroconf": [
{ {
"type": "_printer._tcp.local.", "type": "_printer._tcp.local.",

View File

@ -23,6 +23,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import FIBARO_DEVICES, FibaroDevice from . import FIBARO_DEVICES, FibaroDevice
from .const import DOMAIN from .const import DOMAIN
PARALLEL_UPDATES = 2
def scaleto255(value: int | None) -> int: def scaleto255(value: int | None) -> int:
"""Scale the input value from 0-100 to 0-255.""" """Scale the input value from 0-100 to 0-255."""

View File

@ -132,7 +132,7 @@ class FileSizeCoordinator(DataUpdateCoordinator):
async def _async_update_data(self) -> dict[str, float | int | datetime]: async def _async_update_data(self) -> dict[str, float | int | datetime]:
"""Fetch file information.""" """Fetch file information."""
try: try:
statinfo = os.stat(self._path) statinfo = await self.hass.async_add_executor_job(os.stat, self._path)
except OSError as error: except OSError as error:
raise UpdateFailed(f"Can not retrieve file statistics {error}") from error raise UpdateFailed(f"Can not retrieve file statistics {error}") from error

View File

@ -19,7 +19,7 @@ class HistoryStatsState:
"""The current stats of the history stats.""" """The current stats of the history stats."""
hours_matched: float | None hours_matched: float | None
changes_to_match_state: int | None match_count: int | None
period: tuple[datetime.datetime, datetime.datetime] period: tuple[datetime.datetime, datetime.datetime]
@ -121,14 +121,12 @@ class HistoryStats:
self._state = HistoryStatsState(None, None, self._period) self._state = HistoryStatsState(None, None, self._period)
return self._state 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, now_timestamp,
current_period_start_timestamp, current_period_start_timestamp,
current_period_end_timestamp, current_period_end_timestamp,
) )
self._state = HistoryStatsState( self._state = HistoryStatsState(hours_matched, match_count, self._period)
hours_matched, changes_to_match_state, self._period
)
return self._state return self._state
def _update_from_database( def _update_from_database(
@ -156,7 +154,7 @@ class HistoryStats:
) )
last_state_change_timestamp = start_timestamp last_state_change_timestamp = start_timestamp
elapsed = 0.0 elapsed = 0.0
changes_to_match_state = 0 match_count = 1 if previous_state_matches else 0
# Make calculations # Make calculations
for item in self._history_current_period: for item in self._history_current_period:
@ -166,7 +164,7 @@ class HistoryStats:
if previous_state_matches: if previous_state_matches:
elapsed += state_change_timestamp - last_state_change_timestamp elapsed += state_change_timestamp - last_state_change_timestamp
elif current_state_matches: elif current_state_matches:
changes_to_match_state += 1 match_count += 1
previous_state_matches = current_state_matches previous_state_matches = current_state_matches
last_state_change_timestamp = state_change_timestamp last_state_change_timestamp = state_change_timestamp
@ -178,4 +176,4 @@ class HistoryStats:
# Save value in hours # Save value in hours
hours_matched = elapsed / 3600 hours_matched = elapsed / 3600
return hours_matched, changes_to_match_state return hours_matched, match_count

View File

@ -166,4 +166,4 @@ class HistoryStatsSensor(HistoryStatsSensorBase):
elif self._type == CONF_TYPE_RATIO: elif self._type == CONF_TYPE_RATIO:
self._attr_native_value = pretty_ratio(state.hours_matched, state.period) self._attr_native_value = pretty_ratio(state.hours_matched, state.period)
elif self._type == CONF_TYPE_COUNT: elif self._type == CONF_TYPE_COUNT:
self._attr_native_value = state.changes_to_match_state self._attr_native_value = state.match_count

View File

@ -419,6 +419,8 @@ class LIFXManager:
if color_resp is None or version_resp is None: if color_resp is None or version_resp is None:
_LOGGER.error("Failed to connect to %s", bulb.ip_addr) _LOGGER.error("Failed to connect to %s", bulb.ip_addr)
bulb.registered = False bulb.registered = False
if bulb.mac_addr in self.discoveries_inflight:
self.discoveries_inflight.pop(bulb.mac_addr)
else: else:
bulb.timeout = MESSAGE_TIMEOUT bulb.timeout = MESSAGE_TIMEOUT
bulb.retry_count = MESSAGE_RETRIES bulb.retry_count = MESSAGE_RETRIES

View File

@ -3,7 +3,6 @@ from __future__ import annotations
import asyncio import asyncio
from collections.abc import Coroutine, Sequence from collections.abc import Coroutine, Sequence
import contextlib
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Any 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.client_factory import UpnpFactory
from async_upnp_client.exceptions import ( from async_upnp_client.exceptions import (
UpnpActionResponseError, UpnpActionResponseError,
UpnpCommunicationError,
UpnpConnectionError, UpnpConnectionError,
UpnpError, UpnpError,
UpnpResponseError, UpnpResponseError,
UpnpXmlContentError,
) )
from async_upnp_client.profiles.dlna import DmrDevice from async_upnp_client.profiles.dlna import DmrDevice
from async_upnp_client.utils import async_get_local_ip from async_upnp_client.utils import async_get_local_ip
@ -270,11 +271,12 @@ class SamsungTVDevice(MediaPlayerEntity):
# NETWORK,NONE # NETWORK,NONE
upnp_factory = UpnpFactory(upnp_requester, non_strict=True) upnp_factory = UpnpFactory(upnp_requester, non_strict=True)
upnp_device: UpnpDevice | None = None upnp_device: UpnpDevice | None = None
with contextlib.suppress(UpnpConnectionError, UpnpResponseError): try:
upnp_device = await upnp_factory.async_create_device( upnp_device = await upnp_factory.async_create_device(
self._ssdp_rendering_control_location 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 return
_, event_ip = await async_get_local_ip( _, event_ip = await async_get_local_ip(
self._ssdp_rendering_control_location, self.hass.loop self._ssdp_rendering_control_location, self.hass.loop
@ -307,8 +309,10 @@ class SamsungTVDevice(MediaPlayerEntity):
async def _async_resubscribe_dmr(self) -> None: async def _async_resubscribe_dmr(self) -> None:
assert self._dmr_device assert self._dmr_device
with contextlib.suppress(UpnpConnectionError): try:
await self._dmr_device.async_subscribe_services(auto_resubscribe=True) 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: async def _async_shutdown_dmr(self) -> None:
"""Handle removal.""" """Handle removal."""

View File

@ -812,7 +812,7 @@ class RpcPollingWrapper(update_coordinator.DataUpdateCoordinator):
LOGGER.debug("Polling Shelly RPC Device - %s", self.name) LOGGER.debug("Polling Shelly RPC Device - %s", self.name)
async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC):
await self.device.update_status() await self.device.update_status()
except OSError as err: except (OSError, aioshelly.exceptions.RPCTimeout) as err:
raise update_coordinator.UpdateFailed("Device disconnected") from err raise update_coordinator.UpdateFailed("Device disconnected") from err
@property @property

View File

@ -119,6 +119,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
) )
except HTTP_CONNECT_ERRORS: except HTTP_CONNECT_ERRORS:
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
except KeyError:
errors["base"] = "firmware_not_fully_provisioned"
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
LOGGER.exception("Unexpected exception") LOGGER.exception("Unexpected exception")
errors["base"] = "unknown" errors["base"] = "unknown"
@ -160,6 +162,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
except aioshelly.exceptions.JSONRPCError: except aioshelly.exceptions.JSONRPCError:
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
except KeyError:
errors["base"] = "firmware_not_fully_provisioned"
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
LOGGER.exception("Unexpected exception") LOGGER.exception("Unexpected exception")
errors["base"] = "unknown" errors["base"] = "unknown"
@ -219,6 +223,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
try: try:
self.device_info = await validate_input(self.hass, self.host, self.info, {}) 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: except HTTP_CONNECT_ERRORS:
return self.async_abort(reason="cannot_connect") return self.async_abort(reason="cannot_connect")
@ -229,18 +235,21 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
) -> FlowResult: ) -> FlowResult:
"""Handle discovery confirm.""" """Handle discovery confirm."""
errors: dict[str, str] = {} errors: dict[str, str] = {}
if user_input is not None: try:
return self.async_create_entry( if user_input is not None:
title=self.device_info["title"], return self.async_create_entry(
data={ title=self.device_info["title"],
"host": self.host, data={
CONF_SLEEP_PERIOD: self.device_info[CONF_SLEEP_PERIOD], "host": self.host,
"model": self.device_info["model"], CONF_SLEEP_PERIOD: self.device_info[CONF_SLEEP_PERIOD],
"gen": self.device_info["gen"], "model": self.device_info["model"],
}, "gen": self.device_info["gen"],
) },
)
self._set_confirm_only() except KeyError:
errors["base"] = "firmware_not_fully_provisioned"
else:
self._set_confirm_only()
return self.async_show_form( return self.async_show_form(
step_id="confirm_discovery", step_id="confirm_discovery",

View File

@ -21,7 +21,8 @@
"error": { "error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "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": { "abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",

View File

@ -6,6 +6,7 @@
}, },
"error": { "error": {
"cannot_connect": "Failed to connect", "cannot_connect": "Failed to connect",
"firmware_not_fully_provisioned": "Device not fully provisioned. Please contact Shelly support",
"invalid_auth": "Invalid authentication", "invalid_auth": "Invalid authentication",
"unknown": "Unexpected error" "unknown": "Unexpected error"
}, },

View File

@ -297,6 +297,9 @@ def get_rpc_key_instances(keys_dict: dict[str, Any], key: str) -> list[str]:
if key in keys_dict: if key in keys_dict:
return [key] return [key]
if key == "switch" and "cover:0" in keys_dict:
key = "cover"
keys_list: list[str] = [] keys_list: list[str] = []
for i in range(MAX_RPC_KEY_INSTANCES): for i in range(MAX_RPC_KEY_INSTANCES):
key_inst = f"{key}:{i}" key_inst = f"{key}:{i}"

View File

@ -2,7 +2,7 @@
"domain": "snmp", "domain": "snmp",
"name": "SNMP", "name": "SNMP",
"documentation": "https://www.home-assistant.io/integrations/snmp", "documentation": "https://www.home-assistant.io/integrations/snmp",
"requirements": ["pysnmplib==5.0.10"], "requirements": ["pysnmp==4.4.12"],
"codeowners": [], "codeowners": [],
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["pyasn1", "pysmi", "pysnmp"] "loggers": ["pyasn1", "pysmi", "pysnmp"]

View File

@ -28,6 +28,7 @@ from homeassistant.helpers.device_registry import (
DeviceEntry, DeviceEntry,
async_get_registry as get_dev_reg, async_get_registry as get_dev_reg,
) )
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .common import SynoApi from .common import SynoApi
@ -41,6 +42,7 @@ from .const import (
EXCEPTION_DETAILS, EXCEPTION_DETAILS,
EXCEPTION_UNKNOWN, EXCEPTION_UNKNOWN,
PLATFORMS, PLATFORMS,
SIGNAL_CAMERA_SOURCE_CHANGED,
SYNO_API, SYNO_API,
SYSTEM_LOADED, SYSTEM_LOADED,
UNDO_UPDATE_LISTENER, UNDO_UPDATE_LISTENER,
@ -128,6 +130,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return None return None
surveillance_station = api.surveillance_station surveillance_station = api.surveillance_station
current_data: dict[str, SynoCamera] = {
camera.id: camera for camera in surveillance_station.get_all_cameras()
}
try: try:
async with async_timeout.timeout(30): async with async_timeout.timeout(30):
@ -135,12 +140,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
except SynologyDSMAPIErrorException as err: except SynologyDSMAPIErrorException as err:
raise UpdateFailed(f"Error communicating with API: {err}") from err raise UpdateFailed(f"Error communicating with API: {err}") from err
return { new_data: dict[str, SynoCamera] = {
"cameras": { camera.id: camera for camera in surveillance_station.get_all_cameras()
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: async def async_coordinator_update_data_central() -> None:
"""Fetch all device and sensor data from api.""" """Fetch all device and sensor data from api."""
try: try:

View File

@ -16,7 +16,8 @@ from homeassistant.components.camera import (
CameraEntityFeature, CameraEntityFeature,
) )
from homeassistant.config_entries import ConfigEntry 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 import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
@ -27,6 +28,7 @@ from .const import (
COORDINATOR_CAMERAS, COORDINATOR_CAMERAS,
DEFAULT_SNAPSHOT_QUALITY, DEFAULT_SNAPSHOT_QUALITY,
DOMAIN, DOMAIN,
SIGNAL_CAMERA_SOURCE_CHANGED,
SYNO_API, SYNO_API,
) )
from .entity import SynologyDSMBaseEntity, SynologyDSMEntityDescription from .entity import SynologyDSMBaseEntity, SynologyDSMEntityDescription
@ -130,6 +132,29 @@ class SynoDSMCamera(SynologyDSMBaseEntity, Camera):
"""Return the camera motion detection status.""" """Return the camera motion detection status."""
return self.camera_data.is_motion_detection_enabled # type: ignore[no-any-return] 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( def camera_image(
self, width: int | None = None, height: int | None = None self, width: int | None = None, height: int | None = None
) -> bytes | None: ) -> bytes | None:
@ -162,6 +187,7 @@ class SynoDSMCamera(SynologyDSMBaseEntity, Camera):
) )
if not self.available: if not self.available:
return None return None
return self.camera_data.live_view.rtsp # type: ignore[no-any-return] return self.camera_data.live_view.rtsp # type: ignore[no-any-return]
def enable_motion_detection(self) -> None: def enable_motion_detection(self) -> None:

View File

@ -43,6 +43,9 @@ DEFAULT_SNAPSHOT_QUALITY = SNAPSHOT_PROFILE_BALANCED
ENTITY_UNIT_LOAD = "load" ENTITY_UNIT_LOAD = "load"
# Signals
SIGNAL_CAMERA_SOURCE_CHANGED = "synology_dsm.camera_stream_source_changed"
# Services # Services
SERVICE_REBOOT = "reboot" SERVICE_REBOOT = "reboot"
SERVICE_SHUTDOWN = "shutdown" SERVICE_SHUTDOWN = "shutdown"

View File

@ -143,7 +143,9 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
user_input[CONF_VERIFY_SSL] = False user_input[CONF_VERIFY_SSL] = False
nvr_data, errors = await self._async_get_nvr_data(user_input) nvr_data, errors = await self._async_get_nvr_data(user_input)
if nvr_data and not errors: 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 = { placeholders = {
"name": discovery_info["hostname"] "name": discovery_info["hostname"]
@ -289,7 +291,9 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
await self.async_set_unique_id(nvr_data.mac) await self.async_set_unique_id(nvr_data.mac)
self._abort_if_unique_id_configured() 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 {} user_input = user_input or {}
return self.async_show_form( return self.async_show_form(

View File

@ -3,7 +3,7 @@
"name": "UniFi Protect", "name": "UniFi Protect",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/unifiprotect", "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"], "dependencies": ["http"],
"codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"],
"quality_scale": "platinum", "quality_scale": "platinum",

View File

@ -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]]: def _get_paired_camera_options(api: ProtectApiClient) -> list[dict[str, Any]]:
options = [{"id": TYPE_EMPTY_VALUE, "name": "Not Paired"}] options = [{"id": TYPE_EMPTY_VALUE, "name": "Not Paired"}]
for camera in api.bootstrap.cameras.values(): 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 return options

View File

@ -159,6 +159,15 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
ufp_value="is_face_detection_on", ufp_value="is_face_detection_on",
ufp_set_method="set_face_detection", 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, ...] = ( SENSE_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (

View File

@ -171,8 +171,8 @@ class VeSyncFanHA(VeSyncDevice, FanEntity):
if hasattr(self.smartfan, "night_light"): if hasattr(self.smartfan, "night_light"):
attr["night_light"] = self.smartfan.night_light attr["night_light"] = self.smartfan.night_light
if hasattr(self.smartfan, "air_quality"): if self.smartfan.details.get("air_quality_value") is not None:
attr["air_quality"] = self.smartfan.air_quality attr["air_quality"] = self.smartfan.details["air_quality_value"]
if hasattr(self.smartfan, "mode"): if hasattr(self.smartfan, "mode"):
attr["mode"] = self.smartfan.mode attr["mode"] = self.smartfan.mode

View File

@ -3,7 +3,7 @@
"name": "VeSync", "name": "VeSync",
"documentation": "https://www.home-assistant.io/integrations/vesync", "documentation": "https://www.home-assistant.io/integrations/vesync",
"codeowners": ["@markperdue", "@webdjoe", "@thegardenmonkey"], "codeowners": ["@markperdue", "@webdjoe", "@thegardenmonkey"],
"requirements": ["pyvesync==2.0.2"], "requirements": ["pyvesync==2.0.3"],
"config_flow": true, "config_flow": true,
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["pyvesync"] "loggers": ["pyvesync"]

View File

@ -7,7 +7,7 @@ from .backports.enum import StrEnum
MAJOR_VERSION: Final = 2022 MAJOR_VERSION: Final = 2022
MINOR_VERSION: Final = 5 MINOR_VERSION: Final = 5
PATCH_VERSION: Final = "4" PATCH_VERSION: Final = "5"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0)

View File

@ -436,7 +436,7 @@ bravia-tv==1.0.11
broadlink==0.18.1 broadlink==0.18.1
# homeassistant.components.brother # homeassistant.components.brother
brother==1.2.0 brother==1.1.0
# homeassistant.components.brottsplatskartan # homeassistant.components.brottsplatskartan
brottsplatskartan==0.0.1 brottsplatskartan==0.0.1
@ -1829,7 +1829,7 @@ pysmarty==0.8
pysml==0.0.7 pysml==0.0.7
# homeassistant.components.snmp # homeassistant.components.snmp
pysnmplib==5.0.10 pysnmp==4.4.12
# homeassistant.components.soma # homeassistant.components.soma
pysoma==0.0.10 pysoma==0.0.10
@ -1981,7 +1981,7 @@ pytrafikverket==0.1.6.2
pyudev==0.22.0 pyudev==0.22.0
# homeassistant.components.unifiprotect # homeassistant.components.unifiprotect
pyunifiprotect==3.4.1 pyunifiprotect==3.5.1
# homeassistant.components.uptimerobot # homeassistant.components.uptimerobot
pyuptimerobot==22.2.0 pyuptimerobot==22.2.0
@ -1996,7 +1996,7 @@ pyvera==0.3.13
pyversasense==0.0.6 pyversasense==0.0.6
# homeassistant.components.vesync # homeassistant.components.vesync
pyvesync==2.0.2 pyvesync==2.0.3
# homeassistant.components.vizio # homeassistant.components.vizio
pyvizio==0.1.57 pyvizio==0.1.57

View File

@ -327,7 +327,7 @@ bravia-tv==1.0.11
broadlink==0.18.1 broadlink==0.18.1
# homeassistant.components.brother # homeassistant.components.brother
brother==1.2.0 brother==1.1.0
# homeassistant.components.brunt # homeassistant.components.brunt
brunt==1.2.0 brunt==1.2.0
@ -1304,7 +1304,7 @@ pytrafikverket==0.1.6.2
pyudev==0.22.0 pyudev==0.22.0
# homeassistant.components.unifiprotect # homeassistant.components.unifiprotect
pyunifiprotect==3.4.1 pyunifiprotect==3.5.1
# homeassistant.components.uptimerobot # homeassistant.components.uptimerobot
pyuptimerobot==22.2.0 pyuptimerobot==22.2.0
@ -1313,7 +1313,7 @@ pyuptimerobot==22.2.0
pyvera==0.3.13 pyvera==0.3.13
# homeassistant.components.vesync # homeassistant.components.vesync
pyvesync==2.0.2 pyvesync==2.0.3
# homeassistant.components.vizio # homeassistant.components.vizio
pyvizio==0.1.57 pyvizio==0.1.57

View File

@ -1,6 +1,6 @@
[metadata] [metadata]
name = homeassistant name = homeassistant
version = 2022.5.4 version = 2022.5.5
author = The Home Assistant Authors author = The Home Assistant Authors
author_email = hello@home-assistant.io author_email = hello@home-assistant.io
license = Apache-2.0 license = Apache-2.0

View File

@ -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.sensor1").state == "0.83"
assert hass.states.get("sensor.sensor2").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" 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_sensor1").state == "1.0"
assert hass.states.get("sensor.on_sensor2").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" 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.sensor1").state == "0.0"
assert hass.states.get("sensor.sensor2").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" assert hass.states.get("sensor.sensor4").state == "0.0"
one_hour_in = start_time + timedelta(minutes=60) 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.sensor1").state == "1.0"
assert hass.states.get("sensor.sensor2").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" assert hass.states.get("sensor.sensor4").state == "50.0"
turn_off_time = start_time + timedelta(minutes=90) 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.sensor1").state == "1.5"
assert hass.states.get("sensor.sensor2").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" assert hass.states.get("sensor.sensor4").state == "75.0"
turn_back_on_time = start_time + timedelta(minutes=105) 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.sensor1").state == "1.5"
assert hass.states.get("sensor.sensor2").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" assert hass.states.get("sensor.sensor4").state == "75.0"
with freeze_time(turn_back_on_time): 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.sensor1").state == "1.5"
assert hass.states.get("sensor.sensor2").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" assert hass.states.get("sensor.sensor4").state == "75.0"
end_time = start_time + timedelta(minutes=120) 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.sensor1").state == "1.75"
assert hass.states.get("sensor.sensor2").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" 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.sensor1").state == "0.83"
assert hass.states.get("sensor.sensor2").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" assert hass.states.get("sensor.sensor4").state == "41.7"
past_next_update = start_time + timedelta(minutes=30) 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.sensor1").state == "0.83"
assert hass.states.get("sensor.sensor2").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" 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.sensor1").state == "0.83"
assert hass.states.get("sensor.sensor2").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" assert hass.states.get("sensor.sensor4").state == "83.3"
past_next_update = start_time + timedelta(minutes=30) 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.sensor1").state == "0.83"
assert hass.states.get("sensor.sensor2").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" 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.sensor1").state == "0.83"
assert hass.states.get("sensor.sensor2").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" assert hass.states.get("sensor.sensor4").state == "83.3"

View File

@ -6,6 +6,8 @@ from unittest.mock import DEFAULT as DEFAULT_MOCK, AsyncMock, Mock, call, patch
from async_upnp_client.exceptions import ( from async_upnp_client.exceptions import (
UpnpActionResponseError, UpnpActionResponseError,
UpnpCommunicationError,
UpnpConnectionError,
UpnpError, UpnpError,
UpnpResponseError, UpnpResponseError,
) )
@ -1368,6 +1370,7 @@ async def test_upnp_not_available(
) -> None: ) -> None:
"""Test for volume control when Upnp is not available.""" """Test for volume control when Upnp is not available."""
await setup_samsungtv_entry(hass, MOCK_ENTRY_WS) await setup_samsungtv_entry(hass, MOCK_ENTRY_WS)
assert "Unable to create Upnp DMR device" in caplog.text
# Upnp action fails # Upnp action fails
assert await hass.services.async_call( assert await hass.services.async_call(
@ -1385,6 +1388,7 @@ async def test_upnp_missing_service(
) -> None: ) -> None:
"""Test for volume control when Upnp is not available.""" """Test for volume control when Upnp is not available."""
await setup_samsungtv_entry(hass, MOCK_ENTRY_WS) await setup_samsungtv_entry(hass, MOCK_ENTRY_WS)
assert "Unable to create Upnp DMR device" in caplog.text
# Upnp action fails # Upnp action fails
assert await hass.services.async_call( assert await hass.services.async_call(
@ -1505,3 +1509,49 @@ async def test_upnp_re_subscribe_events(
assert state.state == STATE_ON assert state.state == STATE_ON
assert dmr_device.async_subscribe_services.call_count == 2 assert dmr_device.async_subscribe_services.call_count == 2
assert dmr_device.async_unsubscribe_services.call_count == 1 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

View File

@ -225,6 +225,29 @@ async def test_form_errors_get_info(hass, error):
assert result2["errors"] == {"base": base_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( @pytest.mark.parametrize(
"error", [(asyncio.TimeoutError, "cannot_connect"), (ValueError, "unknown")] "error", [(asyncio.TimeoutError, "cannot_connect"), (ValueError, "unknown")]
) )

View File

@ -26,9 +26,13 @@ from .conftest import (
ids_from_device_description, 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 = [ 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) 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( unique_id, entity_id = ids_from_device_description(
Platform.SWITCH, camera, description Platform.SWITCH, camera, description
) )