diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index f21c1b851ca..8b21a10a39f 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -46,6 +46,17 @@ class BMWSensorEntityDescription(SensorEntityDescription): value: Callable = lambda x, y: x +def convert_and_round( + state: tuple, + converter: Callable[[float | None, str], float], + precision: int, +) -> float | None: + """Safely convert and round a value from a Tuple[value, unit].""" + if state[0] is None: + return None + return round(converter(state[0], UNIT_MAP.get(state[1], state[1])), precision) + + SENSOR_TYPES: dict[str, BMWSensorEntityDescription] = { # --- Generic --- "charging_start_time": BMWSensorEntityDescription( @@ -78,45 +89,35 @@ SENSOR_TYPES: dict[str, BMWSensorEntityDescription] = { icon="mdi:speedometer", unit_metric=LENGTH_KILOMETERS, unit_imperial=LENGTH_MILES, - value=lambda x, hass: round( - hass.config.units.length(x[0], UNIT_MAP.get(x[1], x[1])), 2 - ), + value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2), ), "remaining_range_total": BMWSensorEntityDescription( key="remaining_range_total", icon="mdi:map-marker-distance", unit_metric=LENGTH_KILOMETERS, unit_imperial=LENGTH_MILES, - value=lambda x, hass: round( - hass.config.units.length(x[0], UNIT_MAP.get(x[1], x[1])), 2 - ), + value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2), ), "remaining_range_electric": BMWSensorEntityDescription( key="remaining_range_electric", icon="mdi:map-marker-distance", unit_metric=LENGTH_KILOMETERS, unit_imperial=LENGTH_MILES, - value=lambda x, hass: round( - hass.config.units.length(x[0], UNIT_MAP.get(x[1], x[1])), 2 - ), + value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2), ), "remaining_range_fuel": BMWSensorEntityDescription( key="remaining_range_fuel", icon="mdi:map-marker-distance", unit_metric=LENGTH_KILOMETERS, unit_imperial=LENGTH_MILES, - value=lambda x, hass: round( - hass.config.units.length(x[0], UNIT_MAP.get(x[1], x[1])), 2 - ), + value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2), ), "remaining_fuel": BMWSensorEntityDescription( key="remaining_fuel", icon="mdi:gas-station", unit_metric=VOLUME_LITERS, unit_imperial=VOLUME_GALLONS, - value=lambda x, hass: round( - hass.config.units.volume(x[0], UNIT_MAP.get(x[1], x[1])), 2 - ), + value=lambda x, hass: convert_and_round(x, hass.config.units.volume, 2), ), "fuel_percent": BMWSensorEntityDescription( key="fuel_percent", diff --git a/homeassistant/components/gdacs/manifest.json b/homeassistant/components/gdacs/manifest.json index 65407e85848..57c275f2beb 100644 --- a/homeassistant/components/gdacs/manifest.json +++ b/homeassistant/components/gdacs/manifest.json @@ -3,7 +3,7 @@ "name": "Global Disaster Alert and Coordination System (GDACS)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/gdacs", - "requirements": ["aio_georss_gdacs==0.5"], + "requirements": ["aio_georss_gdacs==0.7"], "codeowners": ["@exxamalte"], "quality_scale": "platinum", "iot_class": "cloud_polling" diff --git a/homeassistant/components/generic/config_flow.py b/homeassistant/components/generic/config_flow.py index df8946ccbad..c6310d22dce 100644 --- a/homeassistant/components/generic/config_flow.py +++ b/homeassistant/components/generic/config_flow.py @@ -58,7 +58,7 @@ DEFAULT_DATA = { CONF_VERIFY_SSL: True, } -SUPPORTED_IMAGE_TYPES = {"png", "jpeg", "gif", "svg+xml"} +SUPPORTED_IMAGE_TYPES = {"png", "jpeg", "gif", "svg+xml", "webp"} def build_schema( @@ -324,16 +324,7 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN): # abort if we've already got this one. if self.check_for_existing(import_config): return self.async_abort(reason="already_exists") - errors, still_format = await async_test_still(self.hass, import_config) - if errors.get(CONF_STILL_IMAGE_URL) == "template_error": - _LOGGER.warning( - "Could not render template, but it could be that " - "referenced entities are still initialising. " - "Continuing assuming that imported YAML template is valid" - ) - errors.pop(CONF_STILL_IMAGE_URL) - still_format = import_config.get(CONF_CONTENT_TYPE, "image/jpeg") - errors = errors | await async_test_stream(self.hass, import_config) + # Don't bother testing the still or stream details on yaml import. still_url = import_config.get(CONF_STILL_IMAGE_URL) stream_url = import_config.get(CONF_STREAM_SOURCE) name = import_config.get( @@ -341,15 +332,10 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN): ) if CONF_LIMIT_REFETCH_TO_URL_CHANGE not in import_config: import_config[CONF_LIMIT_REFETCH_TO_URL_CHANGE] = False - if not errors: - import_config[CONF_CONTENT_TYPE] = still_format - await self.async_set_unique_id(self.flow_id) - return self.async_create_entry(title=name, data={}, options=import_config) - _LOGGER.error( - "Error importing generic IP camera platform config: unexpected error '%s'", - list(errors.values()), - ) - return self.async_abort(reason="unknown") + still_format = import_config.get(CONF_CONTENT_TYPE, "image/jpeg") + import_config[CONF_CONTENT_TYPE] = still_format + await self.async_set_unique_id(self.flow_id) + return self.async_create_entry(title=name, data={}, options=import_config) class GenericOptionsFlowHandler(OptionsFlow): diff --git a/homeassistant/components/generic/manifest.json b/homeassistant/components/generic/manifest.json index 66efa0925c5..7d3841dcf47 100644 --- a/homeassistant/components/generic/manifest.json +++ b/homeassistant/components/generic/manifest.json @@ -2,7 +2,7 @@ "domain": "generic", "name": "Generic Camera", "config_flow": true, - "requirements": ["av==9.0.0", "pillow==9.0.1"], + "requirements": ["av==8.1.0", "pillow==9.0.1"], "documentation": "https://www.home-assistant.io/integrations/generic", "codeowners": ["@davet2001"], "iot_class": "local_push" diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index f158db884dc..f6629cb3938 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -7,6 +7,7 @@ from datetime import datetime, timedelta import logging from typing import Any +import aiohttp from httplib2.error import ServerNotFoundError from oauth2client.file import Storage import voluptuous as vol @@ -24,7 +25,11 @@ from homeassistant.const import ( CONF_OFFSET, ) from homeassistant.core import HomeAssistant, ServiceCall -from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError +from homeassistant.exceptions import ( + ConfigEntryAuthFailed, + ConfigEntryNotReady, + HomeAssistantError, +) from homeassistant.helpers import config_entry_oauth2_flow import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -185,8 +190,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, entry ) ) - assert isinstance(implementation, DeviceAuth) session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation) + # Force a token refresh to fix a bug where tokens were persisted with + # expires_in (relative time delta) and expires_at (absolute time) swapped. + if session.token["expires_at"] >= datetime(2070, 1, 1).timestamp(): + session.token["expires_in"] = 0 + session.token["expires_at"] = datetime.now().timestamp() + try: + await session.async_ensure_token_valid() + except aiohttp.ClientResponseError as err: + if 400 <= err.status < 500: + raise ConfigEntryAuthFailed from err + raise ConfigEntryNotReady from err + except aiohttp.ClientError as err: + raise ConfigEntryNotReady from err + required_scope = hass.data[DOMAIN][DATA_CONFIG][CONF_CALENDAR_ACCESS].scope if required_scope not in session.token.get("scope", []): raise ConfigEntryAuthFailed( diff --git a/homeassistant/components/google/config_flow.py b/homeassistant/components/google/config_flow.py index c70dd83fcae..8bbd2a6c2b1 100644 --- a/homeassistant/components/google/config_flow.py +++ b/homeassistant/components/google/config_flow.py @@ -34,7 +34,7 @@ class OAuth2FlowHandler( return logging.getLogger(__name__) async def async_step_import(self, info: dict[str, Any]) -> FlowResult: - """Import existing auth from Nest.""" + """Import existing auth into a new config entry.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") implementations = await config_entry_oauth2_flow.async_get_implementations( diff --git a/homeassistant/components/google/strings.json b/homeassistant/components/google/strings.json index 14f020f08fd..e8ec7091030 100644 --- a/homeassistant/components/google/strings.json +++ b/homeassistant/components/google/strings.json @@ -6,7 +6,7 @@ }, "reauth_confirm": { "title": "[%key:common::config_flow::title::reauth%]", - "description": "The Nest integration needs to re-authenticate your account" + "description": "The Google Calendar integration needs to re-authenticate your account" }, "auth": { "title": "Link Google Account" diff --git a/homeassistant/components/google/translations/en.json b/homeassistant/components/google/translations/en.json index 51d1ad9aab8..02c8e6d7029 100644 --- a/homeassistant/components/google/translations/en.json +++ b/homeassistant/components/google/translations/en.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Account is already configured", "already_in_progress": "Configuration flow is already in progress", - "code_expired": "Authentication code expired, please try again.", + "code_expired": "Authentication code expired or credential setup is invalid, please try again.", "invalid_access_token": "Invalid access token", "missing_configuration": "The component is not configured. Please follow the documentation.", "oauth_error": "Received invalid token data.", @@ -23,7 +23,7 @@ "title": "Pick Authentication Method" }, "reauth_confirm": { - "description": "The Nest integration needs to re-authenticate your account", + "description": "The Google Calendar integration needs to re-authenticate your account", "title": "Reauthenticate Integration" } } diff --git a/homeassistant/components/mpd/manifest.json b/homeassistant/components/mpd/manifest.json index 880d32b5877..9bfeb8dad03 100644 --- a/homeassistant/components/mpd/manifest.json +++ b/homeassistant/components/mpd/manifest.json @@ -2,7 +2,7 @@ "domain": "mpd", "name": "Music Player Daemon (MPD)", "documentation": "https://www.home-assistant.io/integrations/mpd", - "requirements": ["python-mpd2==3.0.4"], + "requirements": ["python-mpd2==3.0.5"], "codeowners": ["@fabaff"], "iot_class": "local_polling", "loggers": ["mpd"] diff --git a/homeassistant/components/netgear/switch.py b/homeassistant/components/netgear/switch.py index cf8cd835f89..b38179ccb2a 100644 --- a/homeassistant/components/netgear/switch.py +++ b/homeassistant/components/netgear/switch.py @@ -90,10 +90,12 @@ class NetgearAllowBlock(NetgearDeviceEntity, SwitchEntity): async def async_turn_on(self, **kwargs): """Turn the switch on.""" await self._router.async_allow_block_device(self._mac, ALLOW) + await self.coordinator.async_request_refresh() async def async_turn_off(self, **kwargs): """Turn the switch off.""" await self._router.async_allow_block_device(self._mac, BLOCK) + await self.coordinator.async_request_refresh() @callback def async_update_device(self) -> None: diff --git a/homeassistant/components/nina/manifest.json b/homeassistant/components/nina/manifest.json index 0797076917d..24a8eb7163a 100644 --- a/homeassistant/components/nina/manifest.json +++ b/homeassistant/components/nina/manifest.json @@ -3,7 +3,7 @@ "name": "NINA", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/nina", - "requirements": ["pynina==0.1.7"], + "requirements": ["pynina==0.1.8"], "dependencies": [], "codeowners": ["@DeerMaximum"], "iot_class": "cloud_polling", diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index e0a84ced16f..4ca83d98242 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -159,7 +159,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.data[CONF_SERVER], error, ) - return False + # Retry as setups behind a proxy can return transient 404 or 502 errors + raise ConfigEntryNotReady from error _LOGGER.debug( "Connected to: %s (%s)", plex_server.friendly_name, plex_server.url_in_use diff --git a/homeassistant/components/plex/services.py b/homeassistant/components/plex/services.py index 8cf714b8823..06ecc081c9b 100644 --- a/homeassistant/components/plex/services.py +++ b/homeassistant/components/plex/services.py @@ -173,7 +173,9 @@ def process_plex_payload( media = plex_server.lookup_media(content_type, **search_query) if supports_playqueues and (isinstance(media, list) or shuffle): - playqueue = plex_server.create_playqueue(media, shuffle=shuffle) + playqueue = plex_server.create_playqueue( + media, includeRelated=0, shuffle=shuffle + ) return PlexMediaSearchResult(playqueue, content) return PlexMediaSearchResult(media, content) diff --git a/homeassistant/components/rtsp_to_webrtc/manifest.json b/homeassistant/components/rtsp_to_webrtc/manifest.json index bf11e7c00bc..4e077c74a83 100644 --- a/homeassistant/components/rtsp_to_webrtc/manifest.json +++ b/homeassistant/components/rtsp_to_webrtc/manifest.json @@ -3,7 +3,7 @@ "name": "RTSPtoWebRTC", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rtsp_to_webrtc", - "requirements": ["rtsp-to-webrtc==0.5.0"], + "requirements": ["rtsp-to-webrtc==0.5.1"], "dependencies": ["camera"], "codeowners": ["@allenporter"], "iot_class": "local_push", diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index 47dae90d3e6..556294409d6 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -214,13 +214,19 @@ class SamsungTVDevice(MediaPlayerEntity): ) if self._attr_state != STATE_ON: + if self._dmr_device and self._dmr_device.is_subscribed: + await self._dmr_device.async_unsubscribe_services() return - startup_tasks: list[Coroutine[Any, Any, None]] = [] + startup_tasks: list[Coroutine[Any, Any, Any]] = [] if not self._app_list_event.is_set(): startup_tasks.append(self._async_startup_app_list()) + if self._dmr_device and not self._dmr_device.is_subscribed: + startup_tasks.append( + self._dmr_device.async_subscribe_services(auto_resubscribe=True) + ) if not self._dmr_device and self._ssdp_rendering_control_location: startup_tasks.append(self._async_startup_dmr()) @@ -273,7 +279,10 @@ class SamsungTVDevice(MediaPlayerEntity): if self._dmr_device is None: session = async_get_clientsession(self.hass) upnp_requester = AiohttpSessionRequester(session) - upnp_factory = UpnpFactory(upnp_requester) + # Set non_strict to avoid invalid data sent by Samsung TV: + # Got invalid value for : + # NETWORK,NONE + upnp_factory = UpnpFactory(upnp_requester, non_strict=True) upnp_device: UpnpDevice | None = None with contextlib.suppress(UpnpConnectionError): upnp_device = await upnp_factory.async_create_device( diff --git a/homeassistant/components/shelly/cover.py b/homeassistant/components/shelly/cover.py index 4885a2a0d2e..79ee15ee5ce 100644 --- a/homeassistant/components/shelly/cover.py +++ b/homeassistant/components/shelly/cover.py @@ -157,9 +157,6 @@ class RpcShellyCover(ShellyRpcEntity, CoverEntity): @property def is_closed(self) -> bool | None: """If cover is closed.""" - if not self.status["pos_control"]: - return None - return cast(bool, self.status["state"] == "closed") @property diff --git a/homeassistant/components/sleepiq/number.py b/homeassistant/components/sleepiq/number.py index fb17336ccb3..b9aca69b3f4 100644 --- a/homeassistant/components/sleepiq/number.py +++ b/homeassistant/components/sleepiq/number.py @@ -130,6 +130,7 @@ async def async_setup_entry( class SleepIQNumberEntity(SleepIQBedEntity, NumberEntity): """Representation of a SleepIQ number entity.""" + entity_description: SleepIQNumberEntityDescription _attr_icon = "mdi:bed" def __init__( @@ -140,7 +141,7 @@ class SleepIQNumberEntity(SleepIQBedEntity, NumberEntity): description: SleepIQNumberEntityDescription, ) -> None: """Initialize the number.""" - self.description = description + self.entity_description = description self.device = device self._attr_name = description.get_name_fn(bed, device) @@ -151,10 +152,10 @@ class SleepIQNumberEntity(SleepIQBedEntity, NumberEntity): @callback def _async_update_attrs(self) -> None: """Update number attributes.""" - self._attr_value = float(self.description.value_fn(self.device)) + self._attr_value = float(self.entity_description.value_fn(self.device)) async def async_set_value(self, value: float) -> None: """Set the number value.""" - await self.description.set_value_fn(self.device, int(value)) + await self.entity_description.set_value_fn(self.device, int(value)) self._attr_value = value self.async_write_ha_state() diff --git a/homeassistant/components/soundtouch/services.yaml b/homeassistant/components/soundtouch/services.yaml index 0de37c6daa2..8d255e5f069 100644 --- a/homeassistant/components/soundtouch/services.yaml +++ b/homeassistant/components/soundtouch/services.yaml @@ -28,10 +28,10 @@ create_zone: description: Name of slaves entities to add to the new zone. required: true selector: - target: - entity: - integration: soundtouch - domain: media_player + entity: + multiple: true + integration: soundtouch + domain: media_player add_zone_slave: name: Add zone slave @@ -50,10 +50,10 @@ add_zone_slave: description: Name of slaves entities to add to the existing zone. required: true selector: - target: - entity: - integration: soundtouch - domain: media_player + entity: + multiple: true + integration: soundtouch + domain: media_player remove_zone_slave: name: Remove zone slave @@ -72,7 +72,7 @@ remove_zone_slave: description: Name of slaves entities to remove from the existing zone. required: true selector: - target: - entity: - integration: soundtouch - domain: media_player + entity: + multiple: true + integration: soundtouch + domain: media_player diff --git a/homeassistant/components/stream/manifest.json b/homeassistant/components/stream/manifest.json index f2c56d0af80..d8fd035a5ab 100644 --- a/homeassistant/components/stream/manifest.json +++ b/homeassistant/components/stream/manifest.json @@ -2,7 +2,7 @@ "domain": "stream", "name": "Stream", "documentation": "https://www.home-assistant.io/integrations/stream", - "requirements": ["PyTurboJPEG==1.6.6", "av==9.0.0"], + "requirements": ["PyTurboJPEG==1.6.6", "av==8.1.0"], "dependencies": ["http"], "codeowners": ["@hunterjm", "@uvjustin", "@allenporter"], "quality_scale": "internal", diff --git a/homeassistant/components/tomorrowio/const.py b/homeassistant/components/tomorrowio/const.py index 5f49700e511..b09cbf8adc0 100644 --- a/homeassistant/components/tomorrowio/const.py +++ b/homeassistant/components/tomorrowio/const.py @@ -29,7 +29,7 @@ INTEGRATION_NAME = "Tomorrow.io" DEFAULT_NAME = INTEGRATION_NAME ATTRIBUTION = "Powered by Tomorrow.io" -MAX_REQUESTS_PER_DAY = 500 +MAX_REQUESTS_PER_DAY = 100 CLEAR_CONDITIONS = {"night": ATTR_CONDITION_CLEAR_NIGHT, "day": ATTR_CONDITION_SUNNY} diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index 32af7cf47be..ecbcb84d772 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -101,7 +101,7 @@ RANDOM_EFFECT_DICT: Final = { cv.ensure_list_csv, [vol.Coerce(int)], HSV_SEQUENCE ), vol.Optional("random_seed", default=100): vol.All( - vol.Coerce(int), vol.Range(min=1, max=100) + vol.Coerce(int), vol.Range(min=1, max=600) ), vol.Optional("backgrounds"): vol.All( cv.ensure_list, diff --git a/homeassistant/components/tplink/services.yaml b/homeassistant/components/tplink/services.yaml index 6f2d9054b1e..9b98018e771 100644 --- a/homeassistant/components/tplink/services.yaml +++ b/homeassistant/components/tplink/services.yaml @@ -180,4 +180,4 @@ random_effect: number: min: 1 step: 1 - max: 100 + max: 600 diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index a70d295fd74..e120e4ada4e 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.2.0", "unifi-discovery==1.1.2"], + "requirements": ["pyunifiprotect==3.3.0", "unifi-discovery==1.1.2"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/homeassistant/components/unifiprotect/switch.py b/homeassistant/components/unifiprotect/switch.py index 9271e87db50..a3626c3082e 100644 --- a/homeassistant/components/unifiprotect/switch.py +++ b/homeassistant/components/unifiprotect/switch.py @@ -137,7 +137,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( name="Detections: Person", icon="mdi:walk", entity_category=EntityCategory.CONFIG, - ufp_required_field="feature_flags.has_smart_detect", + ufp_required_field="can_detect_person", ufp_value="is_person_detection_on", ufp_set_method="set_person_detection", ), @@ -146,10 +146,19 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( name="Detections: Vehicle", icon="mdi:car", entity_category=EntityCategory.CONFIG, - ufp_required_field="feature_flags.has_smart_detect", + ufp_required_field="can_detect_vehicle", ufp_value="is_vehicle_detection_on", ufp_set_method="set_vehicle_detection", ), + ProtectSwitchEntityDescription( + key="smart_face", + name="Detections: Face", + icon="mdi:human-greeting", + entity_category=EntityCategory.CONFIG, + ufp_required_field="can_detect_face", + ufp_value="is_face_detection_on", + ufp_set_method="set_face_detection", + ), ) SENSE_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( diff --git a/homeassistant/components/xmpp/manifest.json b/homeassistant/components/xmpp/manifest.json index d5b08b6bc23..5fc8d6e50a2 100644 --- a/homeassistant/components/xmpp/manifest.json +++ b/homeassistant/components/xmpp/manifest.json @@ -2,7 +2,7 @@ "domain": "xmpp", "name": "Jabber (XMPP)", "documentation": "https://www.home-assistant.io/integrations/xmpp", - "requirements": ["slixmpp==1.8.0.1"], + "requirements": ["slixmpp==1.8.2"], "codeowners": ["@fabaff", "@flowolf"], "iot_class": "cloud_push", "loggers": ["pyasn1", "slixmpp"] diff --git a/homeassistant/components/zwave_js/humidifier.py b/homeassistant/components/zwave_js/humidifier.py index b94c7a8e2a3..44d7bc19bbb 100644 --- a/homeassistant/components/zwave_js/humidifier.py +++ b/homeassistant/components/zwave_js/humidifier.py @@ -150,10 +150,9 @@ class ZWaveHumidifier(ZWaveBaseEntity, HumidifierEntity): @property def is_on(self) -> bool | None: """Return True if entity is on.""" - return int(self._current_mode.value) in [ - self.entity_description.on_mode, - HumidityControlMode.AUTO, - ] + if (value := self._current_mode.value) is None: + return None + return int(value) in [self.entity_description.on_mode, HumidityControlMode.AUTO] def _supports_inverse_mode(self) -> bool: return ( @@ -163,7 +162,9 @@ class ZWaveHumidifier(ZWaveBaseEntity, HumidifierEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn on device.""" - mode = int(self._current_mode.value) + if (value := self._current_mode.value) is None: + return + mode = int(value) if mode == HumidityControlMode.OFF: new_mode = self.entity_description.on_mode elif mode == self.entity_description.inverse_mode: @@ -175,7 +176,9 @@ class ZWaveHumidifier(ZWaveBaseEntity, HumidifierEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Turn off device.""" - mode = int(self._current_mode.value) + if (value := self._current_mode.value) is None: + return + mode = int(value) if mode == HumidityControlMode.AUTO: if self._supports_inverse_mode(): new_mode = self.entity_description.inverse_mode @@ -191,7 +194,7 @@ class ZWaveHumidifier(ZWaveBaseEntity, HumidifierEntity): @property def target_humidity(self) -> int | None: """Return the humidity we try to reach.""" - if not self._setpoint: + if not self._setpoint or self._setpoint.value is None: return None return int(self._setpoint.value) diff --git a/homeassistant/const.py b/homeassistant/const.py index b84dee7274c..9007b681b7b 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 = 4 -PATCH_VERSION: Final = "1" +PATCH_VERSION: Final = "2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/requirements_all.txt b/requirements_all.txt index 425573c4c61..44ede223ea9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -107,7 +107,7 @@ aio_geojson_geonetnz_volcano==0.6 aio_geojson_nsw_rfs_incidents==0.4 # homeassistant.components.gdacs -aio_georss_gdacs==0.5 +aio_georss_gdacs==0.7 # homeassistant.components.airzone aioairzone==0.3.3 @@ -349,7 +349,7 @@ aurorapy==0.2.6 # homeassistant.components.generic # homeassistant.components.stream -av==9.0.0 +av==8.1.0 # homeassistant.components.avea # avea==1.5.1 @@ -1661,7 +1661,7 @@ pynetgear==0.9.4 pynetio==0.1.9.1 # homeassistant.components.nina -pynina==0.1.7 +pynina==0.1.8 # homeassistant.components.nuki pynuki==1.5.2 @@ -1907,7 +1907,7 @@ python-kasa==0.4.3 python-miio==0.5.11 # homeassistant.components.mpd -python-mpd2==3.0.4 +python-mpd2==3.0.5 # homeassistant.components.mystrom python-mystrom==1.1.2 @@ -1971,7 +1971,7 @@ pytrafikverket==0.1.6.2 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==3.2.0 +pyunifiprotect==3.3.0 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 @@ -2082,7 +2082,7 @@ rova==0.3.0 rpi-bad-power==0.1.0 # homeassistant.components.rtsp_to_webrtc -rtsp-to-webrtc==0.5.0 +rtsp-to-webrtc==0.5.1 # homeassistant.components.russound_rnet russound==0.1.9 @@ -2155,7 +2155,7 @@ skybellpy==0.6.3 slackclient==2.5.0 # homeassistant.components.xmpp -slixmpp==1.8.0.1 +slixmpp==1.8.2 # homeassistant.components.smart_meter_texas smart-meter-texas==0.4.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e3baebd2e9d..9e2f826ae1e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -91,7 +91,7 @@ aio_geojson_geonetnz_volcano==0.6 aio_geojson_nsw_rfs_incidents==0.4 # homeassistant.components.gdacs -aio_georss_gdacs==0.5 +aio_georss_gdacs==0.7 # homeassistant.components.airzone aioairzone==0.3.3 @@ -279,7 +279,7 @@ aurorapy==0.2.6 # homeassistant.components.generic # homeassistant.components.stream -av==9.0.0 +av==8.1.0 # homeassistant.components.axis axis==44 @@ -1107,7 +1107,7 @@ pymysensors==0.22.1 pynetgear==0.9.4 # homeassistant.components.nina -pynina==0.1.7 +pynina==0.1.8 # homeassistant.components.nuki pynuki==1.5.2 @@ -1285,7 +1285,7 @@ pytrafikverket==0.1.6.2 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==3.2.0 +pyunifiprotect==3.3.0 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 @@ -1351,7 +1351,7 @@ roonapi==0.0.38 rpi-bad-power==0.1.0 # homeassistant.components.rtsp_to_webrtc -rtsp-to-webrtc==0.5.0 +rtsp-to-webrtc==0.5.1 # homeassistant.components.yamaha rxv==0.7.0 diff --git a/setup.cfg b/setup.cfg index 2420a7607d0..2959c5e4dee 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.4.1 +version = 2022.4.2 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 diff --git a/tests/components/generic/sample5_webp.webp b/tests/components/generic/sample5_webp.webp new file mode 100644 index 00000000000..0cf62db1844 Binary files /dev/null and b/tests/components/generic/sample5_webp.webp differ diff --git a/tests/components/generic/test_camera.py b/tests/components/generic/test_camera.py index 5a96391e10e..6e8b804f848 100644 --- a/tests/components/generic/test_camera.py +++ b/tests/components/generic/test_camera.py @@ -43,12 +43,12 @@ async def test_fetching_url(hass, hass_client, fakeimgbytes_png, mock_av_open): resp = await client.get("/api/camera_proxy/camera.config_test") assert resp.status == HTTPStatus.OK - assert respx.calls.call_count == 2 + assert respx.calls.call_count == 1 body = await resp.read() assert body == fakeimgbytes_png resp = await client.get("/api/camera_proxy/camera.config_test") - assert respx.calls.call_count == 3 + assert respx.calls.call_count == 2 @respx.mock @@ -143,19 +143,19 @@ async def test_limit_refetch(hass, hass_client, fakeimgbytes_png, fakeimgbytes_j ): resp = await client.get("/api/camera_proxy/camera.config_test") - assert respx.calls.call_count == 2 + assert respx.calls.call_count == 1 assert resp.status == HTTPStatus.OK hass.states.async_set("sensor.temp", "10") resp = await client.get("/api/camera_proxy/camera.config_test") - assert respx.calls.call_count == 3 + assert respx.calls.call_count == 2 assert resp.status == HTTPStatus.OK body = await resp.read() assert body == fakeimgbytes_png resp = await client.get("/api/camera_proxy/camera.config_test") - assert respx.calls.call_count == 3 + assert respx.calls.call_count == 2 assert resp.status == HTTPStatus.OK body = await resp.read() assert body == fakeimgbytes_png @@ -164,7 +164,7 @@ async def test_limit_refetch(hass, hass_client, fakeimgbytes_png, fakeimgbytes_j # Url change = fetch new image resp = await client.get("/api/camera_proxy/camera.config_test") - assert respx.calls.call_count == 4 + assert respx.calls.call_count == 3 assert resp.status == HTTPStatus.OK body = await resp.read() assert body == fakeimgbytes_jpg @@ -172,7 +172,7 @@ async def test_limit_refetch(hass, hass_client, fakeimgbytes_png, fakeimgbytes_j # Cause a template render error hass.states.async_remove("sensor.temp") resp = await client.get("/api/camera_proxy/camera.config_test") - assert respx.calls.call_count == 4 + assert respx.calls.call_count == 3 assert resp.status == HTTPStatus.OK body = await resp.read() assert body == fakeimgbytes_jpg @@ -392,14 +392,14 @@ async def test_camera_content_type( client = await hass_client() resp_1 = await client.get("/api/camera_proxy/camera.config_test_svg") - assert respx.calls.call_count == 3 + assert respx.calls.call_count == 1 assert resp_1.status == HTTPStatus.OK assert resp_1.content_type == "image/svg+xml" body = await resp_1.read() assert body == fakeimgbytes_svg resp_2 = await client.get("/api/camera_proxy/camera.config_test_jpg") - assert respx.calls.call_count == 4 + assert respx.calls.call_count == 2 assert resp_2.status == HTTPStatus.OK assert resp_2.content_type == "image/jpeg" body = await resp_2.read() @@ -432,7 +432,7 @@ async def test_timeout_cancelled(hass, hass_client, fakeimgbytes_png, fakeimgbyt resp = await client.get("/api/camera_proxy/camera.config_test") assert resp.status == HTTPStatus.OK - assert respx.calls.call_count == 2 + assert respx.calls.call_count == 1 assert await resp.read() == fakeimgbytes_png respx.get("http://example.com").respond(stream=fakeimgbytes_jpg) @@ -442,7 +442,7 @@ async def test_timeout_cancelled(hass, hass_client, fakeimgbytes_png, fakeimgbyt side_effect=asyncio.CancelledError(), ): resp = await client.get("/api/camera_proxy/camera.config_test") - assert respx.calls.call_count == 2 + assert respx.calls.call_count == 1 assert resp.status == HTTPStatus.INTERNAL_SERVER_ERROR respx.get("http://example.com").side_effect = [ @@ -450,7 +450,7 @@ async def test_timeout_cancelled(hass, hass_client, fakeimgbytes_png, fakeimgbyt httpx.TimeoutException, ] - for total_calls in range(3, 5): + for total_calls in range(2, 4): resp = await client.get("/api/camera_proxy/camera.config_test") assert respx.calls.call_count == total_calls assert resp.status == HTTPStatus.OK diff --git a/tests/components/generic/test_config_flow.py b/tests/components/generic/test_config_flow.py index 548b09cf0bb..aab04dae203 100644 --- a/tests/components/generic/test_config_flow.py +++ b/tests/components/generic/test_config_flow.py @@ -147,6 +147,7 @@ async def test_form_only_svg_whitespace(hass, fakeimgbytes_svg, user_flow): ("sample2_jpeg_odd_header.jpg"), ("sample3_jpeg_odd_header.jpg"), ("sample4_K5-60mileAnim-320x240.gif"), + ("sample5_webp.webp"), ], ) async def test_form_only_still_sample(hass, user_flow, image_file): @@ -167,8 +168,9 @@ async def test_form_only_still_sample(hass, user_flow, image_file): async def test_form_rtsp_mode(hass, fakeimg_png, mock_av_open, user_flow): """Test we complete ok if the user enters a stream url.""" with mock_av_open as mock_setup: - data = TESTDATA + data = TESTDATA.copy() data[CONF_RTSP_TRANSPORT] = "tcp" + data[CONF_STREAM_SOURCE] = "rtsp://127.0.0.1/testurl/2" result2 = await hass.config_entries.flow.async_configure( user_flow["flow_id"], data ) @@ -178,7 +180,7 @@ async def test_form_rtsp_mode(hass, fakeimg_png, mock_av_open, user_flow): assert result2["options"] == { CONF_STILL_IMAGE_URL: "http://127.0.0.1/testurl/1", CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, - CONF_STREAM_SOURCE: "http://127.0.0.1/testurl/2", + CONF_STREAM_SOURCE: "rtsp://127.0.0.1/testurl/2", CONF_RTSP_TRANSPORT: "tcp", CONF_USERNAME: "fred_flintstone", CONF_PASSWORD: "bambam", @@ -216,7 +218,6 @@ async def test_form_only_stream(hass, mock_av_open, fakeimgbytes_jpg): assert result3["options"] == { CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, CONF_STREAM_SOURCE: "http://127.0.0.1/testurl/2", - CONF_RTSP_TRANSPORT: "tcp", CONF_USERNAME: "fred_flintstone", CONF_PASSWORD: "bambam", CONF_LIMIT_REFETCH_TO_URL_CHANGE: False, @@ -551,31 +552,6 @@ async def test_import(hass, fakeimg_png, mock_av_open): assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT -@respx.mock -async def test_import_invalid_still_image(hass, mock_av_open): - """Test configuration.yaml import used during migration.""" - respx.get("http://127.0.0.1/testurl/1").respond(stream=b"invalid") - with mock_av_open: - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TESTDATA_YAML - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "unknown" - - -@respx.mock -async def test_import_other_error(hass, fakeimgbytes_png): - """Test that non-specific import errors are raised.""" - respx.get("http://127.0.0.1/testurl/1").respond(stream=fakeimgbytes_png) - with patch( - "homeassistant.components.generic.config_flow.av.open", - side_effect=OSError("other error"), - ), pytest.raises(OSError): - await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TESTDATA_YAML - ) - - # These above can be deleted after deprecation period is finished. diff --git a/tests/components/google/conftest.py b/tests/components/google/conftest.py index 102b8a1ccc6..617590cb856 100644 --- a/tests/components/google/conftest.py +++ b/tests/components/google/conftest.py @@ -132,9 +132,16 @@ async def token_scopes() -> list[str]: @pytest.fixture -async def creds(token_scopes: list[str]) -> OAuth2Credentials: +def token_expiry() -> datetime.datetime: + """Expiration time for credentials used in the test.""" + return utcnow() + datetime.timedelta(days=7) + + +@pytest.fixture +def creds( + token_scopes: list[str], token_expiry: datetime.datetime +) -> OAuth2Credentials: """Fixture that defines creds used in the test.""" - token_expiry = utcnow() + datetime.timedelta(days=7) return OAuth2Credentials( access_token="ACCESS_TOKEN", client_id="client-id", @@ -156,9 +163,16 @@ async def storage() -> YieldFixture[FakeStorage]: @pytest.fixture -async def config_entry(token_scopes: list[str]) -> MockConfigEntry: +def config_entry_token_expiry(token_expiry: datetime.datetime) -> float: + """Fixture for token expiration value stored in the config entry.""" + return token_expiry.timestamp() + + +@pytest.fixture +async def config_entry( + token_scopes: list[str], config_entry_token_expiry: float +) -> MockConfigEntry: """Fixture to create a config entry for the integration.""" - token_expiry = utcnow() + datetime.timedelta(days=7) return MockConfigEntry( domain=DOMAIN, data={ @@ -168,7 +182,7 @@ async def config_entry(token_scopes: list[str]) -> MockConfigEntry: "refresh_token": "REFRESH_TOKEN", "scope": " ".join(token_scopes), "token_type": "Bearer", - "expires_at": token_expiry.timestamp(), + "expires_at": config_entry_token_expiry, }, }, ) diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py index 49e10d137b6..85803c3958e 100644 --- a/tests/components/google/test_init.py +++ b/tests/components/google/test_init.py @@ -3,6 +3,8 @@ from __future__ import annotations from collections.abc import Awaitable, Callable import datetime +import http +import time from typing import Any from unittest.mock import Mock, call, patch @@ -29,6 +31,9 @@ from .conftest import ( ) from tests.common import MockConfigEntry +from tests.test_util.aiohttp import AiohttpClientMocker + +EXPIRED_TOKEN_TIMESTAMP = datetime.datetime(2022, 4, 8).timestamp() # Typing helpers HassApi = Callable[[], Awaitable[dict[str, Any]]] @@ -469,3 +474,86 @@ async def test_scan_calendars( assert state assert state.name == "Calendar 2" assert state.state == STATE_OFF + + +@pytest.mark.parametrize( + "config_entry_token_expiry", [datetime.datetime.max.timestamp() + 1] +) +async def test_invalid_token_expiry_in_config_entry( + hass: HomeAssistant, + component_setup: ComponentSetup, + setup_config_entry: MockConfigEntry, + aioclient_mock: AiohttpClientMocker, +) -> None: + """Exercise case in issue #69623 with invalid token expiration persisted.""" + + # The token is refreshed and new expiration values are returned + expires_in = 86400 + expires_at = time.time() + expires_in + aioclient_mock.post( + "https://oauth2.googleapis.com/token", + json={ + "refresh_token": "some-refresh-token", + "access_token": "some-updated-token", + "expires_at": expires_at, + "expires_in": expires_in, + }, + ) + + assert await component_setup() + + # Verify token expiration values are updated + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state is ConfigEntryState.LOADED + assert entries[0].data["token"]["access_token"] == "some-updated-token" + assert entries[0].data["token"]["expires_in"] == expires_in + + +@pytest.mark.parametrize("config_entry_token_expiry", [EXPIRED_TOKEN_TIMESTAMP]) +async def test_expired_token_refresh_internal_error( + hass: HomeAssistant, + component_setup: ComponentSetup, + setup_config_entry: MockConfigEntry, + aioclient_mock: AiohttpClientMocker, +) -> None: + """Generic errors on reauth are treated as a retryable setup error.""" + + aioclient_mock.post( + "https://oauth2.googleapis.com/token", + status=http.HTTPStatus.INTERNAL_SERVER_ERROR, + ) + + assert await component_setup() + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state is ConfigEntryState.SETUP_RETRY + + +@pytest.mark.parametrize( + "config_entry_token_expiry", + [EXPIRED_TOKEN_TIMESTAMP], +) +async def test_expired_token_requires_reauth( + hass: HomeAssistant, + component_setup: ComponentSetup, + setup_config_entry: MockConfigEntry, + aioclient_mock: AiohttpClientMocker, +) -> None: + """Test case where reauth is required for token that cannot be refreshed.""" + + aioclient_mock.post( + "https://oauth2.googleapis.com/token", + status=http.HTTPStatus.BAD_REQUEST, + ) + + assert await component_setup() + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state is ConfigEntryState.SETUP_ERROR + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert flows[0]["step_id"] == "reauth_confirm" diff --git a/tests/components/plex/test_init.py b/tests/components/plex/test_init.py index 8e09f702386..bbab50a7bbb 100644 --- a/tests/components/plex/test_init.py +++ b/tests/components/plex/test_init.py @@ -63,7 +63,7 @@ async def test_setup_config_entry_with_error(hass, entry): await hass.async_block_till_done() assert len(hass.config_entries.async_entries(const.DOMAIN)) == 1 - assert entry.state is ConfigEntryState.SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_setup_with_insecure_config_entry(hass, entry, setup_plex_server): diff --git a/tests/components/samsungtv/conftest.py b/tests/components/samsungtv/conftest.py index 0f3bc53905a..d7f8ed0d1a1 100644 --- a/tests/components/samsungtv/conftest.py +++ b/tests/components/samsungtv/conftest.py @@ -101,12 +101,27 @@ async def dmr_device_fixture(upnp_device: Mock) -> Mock: dmr_device.volume_level = 0.44 dmr_device.is_volume_muted = False dmr_device.on_event = None + dmr_device.is_subscribed = False def _raise_event(service, state_variables): if dmr_device.on_event: dmr_device.on_event(service, state_variables) dmr_device.raise_event = _raise_event + + def _async_subscribe_services(auto_resubscribe: bool = False): + dmr_device.is_subscribed = True + + dmr_device.async_subscribe_services = AsyncMock( + side_effect=_async_subscribe_services + ) + + def _async_unsubscribe_services(): + dmr_device.is_subscribed = False + + dmr_device.async_unsubscribe_services = AsyncMock( + side_effect=_async_unsubscribe_services + ) yield dmr_device diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index 5d0ea5e6be0..e8407a86a3e 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -1469,3 +1469,39 @@ async def test_upnp_subscribe_events_upnpresponseerror( upnp_notify_server.async_stop_server.assert_not_called() assert "Device rejected subscription" in caplog.text + + +@pytest.mark.usefixtures("rest_api", "upnp_notify_server") +async def test_upnp_re_subscribe_events( + hass: HomeAssistant, remotews: Mock, dmr_device: Mock, mock_now: datetime +) -> 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): + 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 dmr_device.async_subscribe_services.call_count == 2 + assert dmr_device.async_unsubscribe_services.call_count == 1 diff --git a/tests/components/shelly/test_cover.py b/tests/components/shelly/test_cover.py index 8813de200da..ab8c9a9a876 100644 --- a/tests/components/shelly/test_cover.py +++ b/tests/components/shelly/test_cover.py @@ -12,7 +12,7 @@ from homeassistant.components.cover import ( STATE_OPEN, STATE_OPENING, ) -from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN +from homeassistant.const import ATTR_ENTITY_ID from homeassistant.helpers.entity_component import async_update_entity ROLLER_BLOCK_ID = 1 @@ -189,4 +189,4 @@ async def test_rpc_device_no_position_control(hass, rpc_wrapper, monkeypatch): await async_update_entity(hass, "cover.test_cover_0") await hass.async_block_till_done() - assert hass.states.get("cover.test_cover_0").state == STATE_UNKNOWN + assert hass.states.get("cover.test_cover_0").state == STATE_OPEN diff --git a/tests/components/sleepiq/test_number.py b/tests/components/sleepiq/test_number.py index be9221f3b12..bf554b69499 100644 --- a/tests/components/sleepiq/test_number.py +++ b/tests/components/sleepiq/test_number.py @@ -1,6 +1,12 @@ """The tests for SleepIQ number platform.""" from homeassistant.components.number import DOMAIN -from homeassistant.components.number.const import ATTR_VALUE, SERVICE_SET_VALUE +from homeassistant.components.number.const import ( + ATTR_MAX, + ATTR_MIN, + ATTR_STEP, + ATTR_VALUE, + SERVICE_SET_VALUE, +) from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_ICON from homeassistant.helpers import entity_registry as er @@ -28,6 +34,9 @@ async def test_firmness(hass, mock_asyncsleepiq): ) assert state.state == "40.0" assert state.attributes.get(ATTR_ICON) == "mdi:bed" + assert state.attributes.get(ATTR_MIN) == 5 + assert state.attributes.get(ATTR_MAX) == 100 + assert state.attributes.get(ATTR_STEP) == 5 assert ( state.attributes.get(ATTR_FRIENDLY_NAME) == f"SleepNumber {BED_NAME} {SLEEPER_L_NAME} Firmness" @@ -44,6 +53,9 @@ async def test_firmness(hass, mock_asyncsleepiq): ) assert state.state == "80.0" assert state.attributes.get(ATTR_ICON) == "mdi:bed" + assert state.attributes.get(ATTR_MIN) == 5 + assert state.attributes.get(ATTR_MAX) == 100 + assert state.attributes.get(ATTR_STEP) == 5 assert ( state.attributes.get(ATTR_FRIENDLY_NAME) == f"SleepNumber {BED_NAME} {SLEEPER_R_NAME} Firmness" @@ -78,6 +90,9 @@ async def test_actuators(hass, mock_asyncsleepiq): state = hass.states.get(f"number.sleepnumber_{BED_NAME_LOWER}_right_head_position") assert state.state == "60.0" assert state.attributes.get(ATTR_ICON) == "mdi:bed" + assert state.attributes.get(ATTR_MIN) == 0 + assert state.attributes.get(ATTR_MAX) == 100 + assert state.attributes.get(ATTR_STEP) == 1 assert ( state.attributes.get(ATTR_FRIENDLY_NAME) == f"SleepNumber {BED_NAME} Right Head Position" @@ -92,6 +107,9 @@ async def test_actuators(hass, mock_asyncsleepiq): state = hass.states.get(f"number.sleepnumber_{BED_NAME_LOWER}_left_head_position") assert state.state == "50.0" assert state.attributes.get(ATTR_ICON) == "mdi:bed" + assert state.attributes.get(ATTR_MIN) == 0 + assert state.attributes.get(ATTR_MAX) == 100 + assert state.attributes.get(ATTR_STEP) == 1 assert ( state.attributes.get(ATTR_FRIENDLY_NAME) == f"SleepNumber {BED_NAME} Left Head Position" @@ -106,6 +124,9 @@ async def test_actuators(hass, mock_asyncsleepiq): state = hass.states.get(f"number.sleepnumber_{BED_NAME_LOWER}_foot_position") assert state.state == "10.0" assert state.attributes.get(ATTR_ICON) == "mdi:bed" + assert state.attributes.get(ATTR_MIN) == 0 + assert state.attributes.get(ATTR_MAX) == 100 + assert state.attributes.get(ATTR_STEP) == 1 assert ( state.attributes.get(ATTR_FRIENDLY_NAME) == f"SleepNumber {BED_NAME} Foot Position" diff --git a/tests/components/tplink/test_light.py b/tests/components/tplink/test_light.py index c56bb6399fb..e5d20276490 100644 --- a/tests/components/tplink/test_light.py +++ b/tests/components/tplink/test_light.py @@ -523,6 +523,7 @@ async def test_smart_strip_custom_random_effect(hass: HomeAssistant) -> None: { ATTR_ENTITY_ID: entity_id, "init_states": [340, 20, 50], + "random_seed": 600, }, blocking=True, ) @@ -539,7 +540,7 @@ async def test_smart_strip_custom_random_effect(hass: HomeAssistant) -> None: "transition": 0, "type": "random", "init_states": [[340, 20, 50]], - "random_seed": 100, + "random_seed": 600, } ) strip.set_custom_effect.reset_mock() diff --git a/tests/components/unifiprotect/test_switch.py b/tests/components/unifiprotect/test_switch.py index 87ffc5683c0..0a3ac92076e 100644 --- a/tests/components/unifiprotect/test_switch.py +++ b/tests/components/unifiprotect/test_switch.py @@ -6,7 +6,7 @@ from unittest.mock import AsyncMock, Mock import pytest from pyunifiprotect.data import Camera, Light -from pyunifiprotect.data.types import RecordingMode, VideoMode +from pyunifiprotect.data.types import RecordingMode, SmartDetectObjectType, VideoMode from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION from homeassistant.components.unifiprotect.switch import ( @@ -26,6 +26,11 @@ from .conftest import ( ids_from_device_description, ) +CAMERA_SWITCHES_NO_FACE = [d for d in CAMERA_SWITCHES if d.name != "Detections: Face"] +CAMERA_SWITCHES_NO_EXTRA = [ + d for d in CAMERA_SWITCHES_NO_FACE if d.name not in ("High FPS", "Privacy Mode") +] + @pytest.fixture(name="light") async def light_fixture( @@ -79,6 +84,10 @@ async def camera_fixture( camera_obj.feature_flags.has_privacy_mask = True camera_obj.feature_flags.has_speaker = True camera_obj.feature_flags.has_smart_detect = True + camera_obj.feature_flags.smart_detect_types = [ + SmartDetectObjectType.PERSON, + SmartDetectObjectType.VEHICLE, + ] camera_obj.is_ssh_enabled = False camera_obj.led_settings.is_enabled = False camera_obj.hdr_mode = False @@ -244,7 +253,7 @@ async def test_switch_setup_camera_all( entity_registry = er.async_get(hass) - for description in CAMERA_SWITCHES: + for description in CAMERA_SWITCHES_NO_FACE: unique_id, entity_id = ids_from_device_description( Platform.SWITCH, camera, description ) @@ -375,15 +384,12 @@ async def test_switch_camera_ssh( camera.set_ssh.assert_called_with(False) -@pytest.mark.parametrize("description", CAMERA_SWITCHES) +@pytest.mark.parametrize("description", CAMERA_SWITCHES_NO_EXTRA) async def test_switch_camera_simple( hass: HomeAssistant, camera: Camera, description: ProtectSwitchEntityDescription ): """Tests all simple switches for cameras.""" - if description.name in ("High FPS", "Privacy Mode"): - return - assert description.ufp_set_method is not None camera.__fields__[description.ufp_set_method] = Mock()