Merge pull request #69835 from home-assistant/rc

This commit is contained in:
Franck Nijhof 2022-04-11 11:02:51 +02:00 committed by GitHub
commit a1fddc3c4d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 339 additions and 153 deletions

View File

@ -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",

View File

@ -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"

View File

@ -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):

View File

@ -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"

View File

@ -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(

View File

@ -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(

View File

@ -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"

View File

@ -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"
}
}

View File

@ -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"]

View File

@ -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:

View File

@ -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",

View File

@ -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

View File

@ -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)

View File

@ -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",

View File

@ -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 <UpnpStateVariable(PlaybackStorageMedium, string)>:
# 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(

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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",

View File

@ -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}

View File

@ -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,

View File

@ -180,4 +180,4 @@ random_effect:
number:
min: 1
step: 1
max: 100
max: 600

View File

@ -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",

View File

@ -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, ...] = (

View File

@ -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"]

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -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

View File

@ -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.

View File

@ -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,
},
},
)

View File

@ -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"

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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()

View File

@ -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()