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 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] = { SENSOR_TYPES: dict[str, BMWSensorEntityDescription] = {
# --- Generic --- # --- Generic ---
"charging_start_time": BMWSensorEntityDescription( "charging_start_time": BMWSensorEntityDescription(
@ -78,45 +89,35 @@ SENSOR_TYPES: dict[str, BMWSensorEntityDescription] = {
icon="mdi:speedometer", icon="mdi:speedometer",
unit_metric=LENGTH_KILOMETERS, unit_metric=LENGTH_KILOMETERS,
unit_imperial=LENGTH_MILES, unit_imperial=LENGTH_MILES,
value=lambda x, hass: round( value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2),
hass.config.units.length(x[0], UNIT_MAP.get(x[1], x[1])), 2
),
), ),
"remaining_range_total": BMWSensorEntityDescription( "remaining_range_total": BMWSensorEntityDescription(
key="remaining_range_total", key="remaining_range_total",
icon="mdi:map-marker-distance", icon="mdi:map-marker-distance",
unit_metric=LENGTH_KILOMETERS, unit_metric=LENGTH_KILOMETERS,
unit_imperial=LENGTH_MILES, unit_imperial=LENGTH_MILES,
value=lambda x, hass: round( value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2),
hass.config.units.length(x[0], UNIT_MAP.get(x[1], x[1])), 2
),
), ),
"remaining_range_electric": BMWSensorEntityDescription( "remaining_range_electric": BMWSensorEntityDescription(
key="remaining_range_electric", key="remaining_range_electric",
icon="mdi:map-marker-distance", icon="mdi:map-marker-distance",
unit_metric=LENGTH_KILOMETERS, unit_metric=LENGTH_KILOMETERS,
unit_imperial=LENGTH_MILES, unit_imperial=LENGTH_MILES,
value=lambda x, hass: round( value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2),
hass.config.units.length(x[0], UNIT_MAP.get(x[1], x[1])), 2
),
), ),
"remaining_range_fuel": BMWSensorEntityDescription( "remaining_range_fuel": BMWSensorEntityDescription(
key="remaining_range_fuel", key="remaining_range_fuel",
icon="mdi:map-marker-distance", icon="mdi:map-marker-distance",
unit_metric=LENGTH_KILOMETERS, unit_metric=LENGTH_KILOMETERS,
unit_imperial=LENGTH_MILES, unit_imperial=LENGTH_MILES,
value=lambda x, hass: round( value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2),
hass.config.units.length(x[0], UNIT_MAP.get(x[1], x[1])), 2
),
), ),
"remaining_fuel": BMWSensorEntityDescription( "remaining_fuel": BMWSensorEntityDescription(
key="remaining_fuel", key="remaining_fuel",
icon="mdi:gas-station", icon="mdi:gas-station",
unit_metric=VOLUME_LITERS, unit_metric=VOLUME_LITERS,
unit_imperial=VOLUME_GALLONS, unit_imperial=VOLUME_GALLONS,
value=lambda x, hass: round( value=lambda x, hass: convert_and_round(x, hass.config.units.volume, 2),
hass.config.units.volume(x[0], UNIT_MAP.get(x[1], x[1])), 2
),
), ),
"fuel_percent": BMWSensorEntityDescription( "fuel_percent": BMWSensorEntityDescription(
key="fuel_percent", key="fuel_percent",

View File

@ -3,7 +3,7 @@
"name": "Global Disaster Alert and Coordination System (GDACS)", "name": "Global Disaster Alert and Coordination System (GDACS)",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/gdacs", "documentation": "https://www.home-assistant.io/integrations/gdacs",
"requirements": ["aio_georss_gdacs==0.5"], "requirements": ["aio_georss_gdacs==0.7"],
"codeowners": ["@exxamalte"], "codeowners": ["@exxamalte"],
"quality_scale": "platinum", "quality_scale": "platinum",
"iot_class": "cloud_polling" "iot_class": "cloud_polling"

View File

@ -58,7 +58,7 @@ DEFAULT_DATA = {
CONF_VERIFY_SSL: True, CONF_VERIFY_SSL: True,
} }
SUPPORTED_IMAGE_TYPES = {"png", "jpeg", "gif", "svg+xml"} SUPPORTED_IMAGE_TYPES = {"png", "jpeg", "gif", "svg+xml", "webp"}
def build_schema( def build_schema(
@ -324,16 +324,7 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN):
# abort if we've already got this one. # abort if we've already got this one.
if self.check_for_existing(import_config): if self.check_for_existing(import_config):
return self.async_abort(reason="already_exists") return self.async_abort(reason="already_exists")
errors, still_format = await async_test_still(self.hass, import_config) # Don't bother testing the still or stream details on yaml import.
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)
still_url = import_config.get(CONF_STILL_IMAGE_URL) still_url = import_config.get(CONF_STILL_IMAGE_URL)
stream_url = import_config.get(CONF_STREAM_SOURCE) stream_url = import_config.get(CONF_STREAM_SOURCE)
name = import_config.get( name = import_config.get(
@ -341,15 +332,10 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN):
) )
if CONF_LIMIT_REFETCH_TO_URL_CHANGE not in import_config: if CONF_LIMIT_REFETCH_TO_URL_CHANGE not in import_config:
import_config[CONF_LIMIT_REFETCH_TO_URL_CHANGE] = False import_config[CONF_LIMIT_REFETCH_TO_URL_CHANGE] = False
if not errors: still_format = import_config.get(CONF_CONTENT_TYPE, "image/jpeg")
import_config[CONF_CONTENT_TYPE] = still_format import_config[CONF_CONTENT_TYPE] = still_format
await self.async_set_unique_id(self.flow_id) await self.async_set_unique_id(self.flow_id)
return self.async_create_entry(title=name, data={}, options=import_config) 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")
class GenericOptionsFlowHandler(OptionsFlow): class GenericOptionsFlowHandler(OptionsFlow):

View File

@ -2,7 +2,7 @@
"domain": "generic", "domain": "generic",
"name": "Generic Camera", "name": "Generic Camera",
"config_flow": true, "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", "documentation": "https://www.home-assistant.io/integrations/generic",
"codeowners": ["@davet2001"], "codeowners": ["@davet2001"],
"iot_class": "local_push" "iot_class": "local_push"

View File

@ -7,6 +7,7 @@ from datetime import datetime, timedelta
import logging import logging
from typing import Any from typing import Any
import aiohttp
from httplib2.error import ServerNotFoundError from httplib2.error import ServerNotFoundError
from oauth2client.file import Storage from oauth2client.file import Storage
import voluptuous as vol import voluptuous as vol
@ -24,7 +25,11 @@ from homeassistant.const import (
CONF_OFFSET, CONF_OFFSET,
) )
from homeassistant.core import HomeAssistant, ServiceCall 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 from homeassistant.helpers import config_entry_oauth2_flow
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.dispatcher import async_dispatcher_send
@ -185,8 +190,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass, entry hass, entry
) )
) )
assert isinstance(implementation, DeviceAuth)
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation) 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 required_scope = hass.data[DOMAIN][DATA_CONFIG][CONF_CALENDAR_ACCESS].scope
if required_scope not in session.token.get("scope", []): if required_scope not in session.token.get("scope", []):
raise ConfigEntryAuthFailed( raise ConfigEntryAuthFailed(

View File

@ -34,7 +34,7 @@ class OAuth2FlowHandler(
return logging.getLogger(__name__) return logging.getLogger(__name__)
async def async_step_import(self, info: dict[str, Any]) -> FlowResult: 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(): if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed") return self.async_abort(reason="single_instance_allowed")
implementations = await config_entry_oauth2_flow.async_get_implementations( implementations = await config_entry_oauth2_flow.async_get_implementations(

View File

@ -6,7 +6,7 @@
}, },
"reauth_confirm": { "reauth_confirm": {
"title": "[%key:common::config_flow::title::reauth%]", "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": { "auth": {
"title": "Link Google Account" "title": "Link Google Account"

View File

@ -3,7 +3,7 @@
"abort": { "abort": {
"already_configured": "Account is already configured", "already_configured": "Account is already configured",
"already_in_progress": "Configuration flow is already in progress", "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", "invalid_access_token": "Invalid access token",
"missing_configuration": "The component is not configured. Please follow the documentation.", "missing_configuration": "The component is not configured. Please follow the documentation.",
"oauth_error": "Received invalid token data.", "oauth_error": "Received invalid token data.",
@ -23,7 +23,7 @@
"title": "Pick Authentication Method" "title": "Pick Authentication Method"
}, },
"reauth_confirm": { "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" "title": "Reauthenticate Integration"
} }
} }

View File

@ -2,7 +2,7 @@
"domain": "mpd", "domain": "mpd",
"name": "Music Player Daemon (MPD)", "name": "Music Player Daemon (MPD)",
"documentation": "https://www.home-assistant.io/integrations/mpd", "documentation": "https://www.home-assistant.io/integrations/mpd",
"requirements": ["python-mpd2==3.0.4"], "requirements": ["python-mpd2==3.0.5"],
"codeowners": ["@fabaff"], "codeowners": ["@fabaff"],
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["mpd"] "loggers": ["mpd"]

View File

@ -90,10 +90,12 @@ class NetgearAllowBlock(NetgearDeviceEntity, SwitchEntity):
async def async_turn_on(self, **kwargs): async def async_turn_on(self, **kwargs):
"""Turn the switch on.""" """Turn the switch on."""
await self._router.async_allow_block_device(self._mac, ALLOW) await self._router.async_allow_block_device(self._mac, ALLOW)
await self.coordinator.async_request_refresh()
async def async_turn_off(self, **kwargs): async def async_turn_off(self, **kwargs):
"""Turn the switch off.""" """Turn the switch off."""
await self._router.async_allow_block_device(self._mac, BLOCK) await self._router.async_allow_block_device(self._mac, BLOCK)
await self.coordinator.async_request_refresh()
@callback @callback
def async_update_device(self) -> None: def async_update_device(self) -> None:

View File

@ -3,7 +3,7 @@
"name": "NINA", "name": "NINA",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/nina", "documentation": "https://www.home-assistant.io/integrations/nina",
"requirements": ["pynina==0.1.7"], "requirements": ["pynina==0.1.8"],
"dependencies": [], "dependencies": [],
"codeowners": ["@DeerMaximum"], "codeowners": ["@DeerMaximum"],
"iot_class": "cloud_polling", "iot_class": "cloud_polling",

View File

@ -159,7 +159,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
entry.data[CONF_SERVER], entry.data[CONF_SERVER],
error, error,
) )
return False # Retry as setups behind a proxy can return transient 404 or 502 errors
raise ConfigEntryNotReady from error
_LOGGER.debug( _LOGGER.debug(
"Connected to: %s (%s)", plex_server.friendly_name, plex_server.url_in_use "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) media = plex_server.lookup_media(content_type, **search_query)
if supports_playqueues and (isinstance(media, list) or shuffle): 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(playqueue, content)
return PlexMediaSearchResult(media, content) return PlexMediaSearchResult(media, content)

View File

@ -3,7 +3,7 @@
"name": "RTSPtoWebRTC", "name": "RTSPtoWebRTC",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/rtsp_to_webrtc", "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"], "dependencies": ["camera"],
"codeowners": ["@allenporter"], "codeowners": ["@allenporter"],
"iot_class": "local_push", "iot_class": "local_push",

View File

@ -214,13 +214,19 @@ class SamsungTVDevice(MediaPlayerEntity):
) )
if self._attr_state != STATE_ON: if self._attr_state != STATE_ON:
if self._dmr_device and self._dmr_device.is_subscribed:
await self._dmr_device.async_unsubscribe_services()
return return
startup_tasks: list[Coroutine[Any, Any, None]] = [] startup_tasks: list[Coroutine[Any, Any, Any]] = []
if not self._app_list_event.is_set(): if not self._app_list_event.is_set():
startup_tasks.append(self._async_startup_app_list()) 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: if not self._dmr_device and self._ssdp_rendering_control_location:
startup_tasks.append(self._async_startup_dmr()) startup_tasks.append(self._async_startup_dmr())
@ -273,7 +279,10 @@ class SamsungTVDevice(MediaPlayerEntity):
if self._dmr_device is None: if self._dmr_device is None:
session = async_get_clientsession(self.hass) session = async_get_clientsession(self.hass)
upnp_requester = AiohttpSessionRequester(session) 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 upnp_device: UpnpDevice | None = None
with contextlib.suppress(UpnpConnectionError): with contextlib.suppress(UpnpConnectionError):
upnp_device = await upnp_factory.async_create_device( upnp_device = await upnp_factory.async_create_device(

View File

@ -157,9 +157,6 @@ class RpcShellyCover(ShellyRpcEntity, CoverEntity):
@property @property
def is_closed(self) -> bool | None: def is_closed(self) -> bool | None:
"""If cover is closed.""" """If cover is closed."""
if not self.status["pos_control"]:
return None
return cast(bool, self.status["state"] == "closed") return cast(bool, self.status["state"] == "closed")
@property @property

View File

@ -130,6 +130,7 @@ async def async_setup_entry(
class SleepIQNumberEntity(SleepIQBedEntity, NumberEntity): class SleepIQNumberEntity(SleepIQBedEntity, NumberEntity):
"""Representation of a SleepIQ number entity.""" """Representation of a SleepIQ number entity."""
entity_description: SleepIQNumberEntityDescription
_attr_icon = "mdi:bed" _attr_icon = "mdi:bed"
def __init__( def __init__(
@ -140,7 +141,7 @@ class SleepIQNumberEntity(SleepIQBedEntity, NumberEntity):
description: SleepIQNumberEntityDescription, description: SleepIQNumberEntityDescription,
) -> None: ) -> None:
"""Initialize the number.""" """Initialize the number."""
self.description = description self.entity_description = description
self.device = device self.device = device
self._attr_name = description.get_name_fn(bed, device) self._attr_name = description.get_name_fn(bed, device)
@ -151,10 +152,10 @@ class SleepIQNumberEntity(SleepIQBedEntity, NumberEntity):
@callback @callback
def _async_update_attrs(self) -> None: def _async_update_attrs(self) -> None:
"""Update number attributes.""" """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: async def async_set_value(self, value: float) -> None:
"""Set the number value.""" """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._attr_value = value
self.async_write_ha_state() self.async_write_ha_state()

View File

@ -28,10 +28,10 @@ create_zone:
description: Name of slaves entities to add to the new zone. description: Name of slaves entities to add to the new zone.
required: true required: true
selector: selector:
target: entity:
entity: multiple: true
integration: soundtouch integration: soundtouch
domain: media_player domain: media_player
add_zone_slave: add_zone_slave:
name: 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. description: Name of slaves entities to add to the existing zone.
required: true required: true
selector: selector:
target: entity:
entity: multiple: true
integration: soundtouch integration: soundtouch
domain: media_player domain: media_player
remove_zone_slave: remove_zone_slave:
name: 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. description: Name of slaves entities to remove from the existing zone.
required: true required: true
selector: selector:
target: entity:
entity: multiple: true
integration: soundtouch integration: soundtouch
domain: media_player domain: media_player

View File

@ -2,7 +2,7 @@
"domain": "stream", "domain": "stream",
"name": "Stream", "name": "Stream",
"documentation": "https://www.home-assistant.io/integrations/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"], "dependencies": ["http"],
"codeowners": ["@hunterjm", "@uvjustin", "@allenporter"], "codeowners": ["@hunterjm", "@uvjustin", "@allenporter"],
"quality_scale": "internal", "quality_scale": "internal",

View File

@ -29,7 +29,7 @@ INTEGRATION_NAME = "Tomorrow.io"
DEFAULT_NAME = INTEGRATION_NAME DEFAULT_NAME = INTEGRATION_NAME
ATTRIBUTION = "Powered by Tomorrow.io" 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} 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 cv.ensure_list_csv, [vol.Coerce(int)], HSV_SEQUENCE
), ),
vol.Optional("random_seed", default=100): vol.All( 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( vol.Optional("backgrounds"): vol.All(
cv.ensure_list, cv.ensure_list,

View File

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

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.2.0", "unifi-discovery==1.1.2"], "requirements": ["pyunifiprotect==3.3.0", "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

@ -137,7 +137,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
name="Detections: Person", name="Detections: Person",
icon="mdi:walk", icon="mdi:walk",
entity_category=EntityCategory.CONFIG, 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_value="is_person_detection_on",
ufp_set_method="set_person_detection", ufp_set_method="set_person_detection",
), ),
@ -146,10 +146,19 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
name="Detections: Vehicle", name="Detections: Vehicle",
icon="mdi:car", icon="mdi:car",
entity_category=EntityCategory.CONFIG, 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_value="is_vehicle_detection_on",
ufp_set_method="set_vehicle_detection", 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, ...] = ( SENSE_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (

View File

@ -2,7 +2,7 @@
"domain": "xmpp", "domain": "xmpp",
"name": "Jabber (XMPP)", "name": "Jabber (XMPP)",
"documentation": "https://www.home-assistant.io/integrations/xmpp", "documentation": "https://www.home-assistant.io/integrations/xmpp",
"requirements": ["slixmpp==1.8.0.1"], "requirements": ["slixmpp==1.8.2"],
"codeowners": ["@fabaff", "@flowolf"], "codeowners": ["@fabaff", "@flowolf"],
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["pyasn1", "slixmpp"] "loggers": ["pyasn1", "slixmpp"]

View File

@ -150,10 +150,9 @@ class ZWaveHumidifier(ZWaveBaseEntity, HumidifierEntity):
@property @property
def is_on(self) -> bool | None: def is_on(self) -> bool | None:
"""Return True if entity is on.""" """Return True if entity is on."""
return int(self._current_mode.value) in [ if (value := self._current_mode.value) is None:
self.entity_description.on_mode, return None
HumidityControlMode.AUTO, return int(value) in [self.entity_description.on_mode, HumidityControlMode.AUTO]
]
def _supports_inverse_mode(self) -> bool: def _supports_inverse_mode(self) -> bool:
return ( return (
@ -163,7 +162,9 @@ class ZWaveHumidifier(ZWaveBaseEntity, HumidifierEntity):
async def async_turn_on(self, **kwargs: Any) -> None: async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on device.""" """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: if mode == HumidityControlMode.OFF:
new_mode = self.entity_description.on_mode new_mode = self.entity_description.on_mode
elif mode == self.entity_description.inverse_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: async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off device.""" """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 mode == HumidityControlMode.AUTO:
if self._supports_inverse_mode(): if self._supports_inverse_mode():
new_mode = self.entity_description.inverse_mode new_mode = self.entity_description.inverse_mode
@ -191,7 +194,7 @@ class ZWaveHumidifier(ZWaveBaseEntity, HumidifierEntity):
@property @property
def target_humidity(self) -> int | None: def target_humidity(self) -> int | None:
"""Return the humidity we try to reach.""" """Return the humidity we try to reach."""
if not self._setpoint: if not self._setpoint or self._setpoint.value is None:
return None return None
return int(self._setpoint.value) return int(self._setpoint.value)

View File

@ -7,7 +7,7 @@ from .backports.enum import StrEnum
MAJOR_VERSION: Final = 2022 MAJOR_VERSION: Final = 2022
MINOR_VERSION: Final = 4 MINOR_VERSION: Final = 4
PATCH_VERSION: Final = "1" PATCH_VERSION: Final = "2"
__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

@ -107,7 +107,7 @@ aio_geojson_geonetnz_volcano==0.6
aio_geojson_nsw_rfs_incidents==0.4 aio_geojson_nsw_rfs_incidents==0.4
# homeassistant.components.gdacs # homeassistant.components.gdacs
aio_georss_gdacs==0.5 aio_georss_gdacs==0.7
# homeassistant.components.airzone # homeassistant.components.airzone
aioairzone==0.3.3 aioairzone==0.3.3
@ -349,7 +349,7 @@ aurorapy==0.2.6
# homeassistant.components.generic # homeassistant.components.generic
# homeassistant.components.stream # homeassistant.components.stream
av==9.0.0 av==8.1.0
# homeassistant.components.avea # homeassistant.components.avea
# avea==1.5.1 # avea==1.5.1
@ -1661,7 +1661,7 @@ pynetgear==0.9.4
pynetio==0.1.9.1 pynetio==0.1.9.1
# homeassistant.components.nina # homeassistant.components.nina
pynina==0.1.7 pynina==0.1.8
# homeassistant.components.nuki # homeassistant.components.nuki
pynuki==1.5.2 pynuki==1.5.2
@ -1907,7 +1907,7 @@ python-kasa==0.4.3
python-miio==0.5.11 python-miio==0.5.11
# homeassistant.components.mpd # homeassistant.components.mpd
python-mpd2==3.0.4 python-mpd2==3.0.5
# homeassistant.components.mystrom # homeassistant.components.mystrom
python-mystrom==1.1.2 python-mystrom==1.1.2
@ -1971,7 +1971,7 @@ pytrafikverket==0.1.6.2
pyudev==0.22.0 pyudev==0.22.0
# homeassistant.components.unifiprotect # homeassistant.components.unifiprotect
pyunifiprotect==3.2.0 pyunifiprotect==3.3.0
# homeassistant.components.uptimerobot # homeassistant.components.uptimerobot
pyuptimerobot==22.2.0 pyuptimerobot==22.2.0
@ -2082,7 +2082,7 @@ rova==0.3.0
rpi-bad-power==0.1.0 rpi-bad-power==0.1.0
# homeassistant.components.rtsp_to_webrtc # homeassistant.components.rtsp_to_webrtc
rtsp-to-webrtc==0.5.0 rtsp-to-webrtc==0.5.1
# homeassistant.components.russound_rnet # homeassistant.components.russound_rnet
russound==0.1.9 russound==0.1.9
@ -2155,7 +2155,7 @@ skybellpy==0.6.3
slackclient==2.5.0 slackclient==2.5.0
# homeassistant.components.xmpp # homeassistant.components.xmpp
slixmpp==1.8.0.1 slixmpp==1.8.2
# homeassistant.components.smart_meter_texas # homeassistant.components.smart_meter_texas
smart-meter-texas==0.4.7 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 aio_geojson_nsw_rfs_incidents==0.4
# homeassistant.components.gdacs # homeassistant.components.gdacs
aio_georss_gdacs==0.5 aio_georss_gdacs==0.7
# homeassistant.components.airzone # homeassistant.components.airzone
aioairzone==0.3.3 aioairzone==0.3.3
@ -279,7 +279,7 @@ aurorapy==0.2.6
# homeassistant.components.generic # homeassistant.components.generic
# homeassistant.components.stream # homeassistant.components.stream
av==9.0.0 av==8.1.0
# homeassistant.components.axis # homeassistant.components.axis
axis==44 axis==44
@ -1107,7 +1107,7 @@ pymysensors==0.22.1
pynetgear==0.9.4 pynetgear==0.9.4
# homeassistant.components.nina # homeassistant.components.nina
pynina==0.1.7 pynina==0.1.8
# homeassistant.components.nuki # homeassistant.components.nuki
pynuki==1.5.2 pynuki==1.5.2
@ -1285,7 +1285,7 @@ pytrafikverket==0.1.6.2
pyudev==0.22.0 pyudev==0.22.0
# homeassistant.components.unifiprotect # homeassistant.components.unifiprotect
pyunifiprotect==3.2.0 pyunifiprotect==3.3.0
# homeassistant.components.uptimerobot # homeassistant.components.uptimerobot
pyuptimerobot==22.2.0 pyuptimerobot==22.2.0
@ -1351,7 +1351,7 @@ roonapi==0.0.38
rpi-bad-power==0.1.0 rpi-bad-power==0.1.0
# homeassistant.components.rtsp_to_webrtc # homeassistant.components.rtsp_to_webrtc
rtsp-to-webrtc==0.5.0 rtsp-to-webrtc==0.5.1
# homeassistant.components.yamaha # homeassistant.components.yamaha
rxv==0.7.0 rxv==0.7.0

View File

@ -1,6 +1,6 @@
[metadata] [metadata]
name = homeassistant name = homeassistant
version = 2022.4.1 version = 2022.4.2
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

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") resp = await client.get("/api/camera_proxy/camera.config_test")
assert resp.status == HTTPStatus.OK assert resp.status == HTTPStatus.OK
assert respx.calls.call_count == 2 assert respx.calls.call_count == 1
body = await resp.read() body = await resp.read()
assert body == fakeimgbytes_png assert body == fakeimgbytes_png
resp = await client.get("/api/camera_proxy/camera.config_test") resp = await client.get("/api/camera_proxy/camera.config_test")
assert respx.calls.call_count == 3 assert respx.calls.call_count == 2
@respx.mock @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") 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 assert resp.status == HTTPStatus.OK
hass.states.async_set("sensor.temp", "10") hass.states.async_set("sensor.temp", "10")
resp = await client.get("/api/camera_proxy/camera.config_test") 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 assert resp.status == HTTPStatus.OK
body = await resp.read() body = await resp.read()
assert body == fakeimgbytes_png assert body == fakeimgbytes_png
resp = await client.get("/api/camera_proxy/camera.config_test") 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 assert resp.status == HTTPStatus.OK
body = await resp.read() body = await resp.read()
assert body == fakeimgbytes_png 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 # Url change = fetch new image
resp = await client.get("/api/camera_proxy/camera.config_test") 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 assert resp.status == HTTPStatus.OK
body = await resp.read() body = await resp.read()
assert body == fakeimgbytes_jpg 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 # Cause a template render error
hass.states.async_remove("sensor.temp") hass.states.async_remove("sensor.temp")
resp = await client.get("/api/camera_proxy/camera.config_test") 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 assert resp.status == HTTPStatus.OK
body = await resp.read() body = await resp.read()
assert body == fakeimgbytes_jpg assert body == fakeimgbytes_jpg
@ -392,14 +392,14 @@ async def test_camera_content_type(
client = await hass_client() client = await hass_client()
resp_1 = await client.get("/api/camera_proxy/camera.config_test_svg") 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.status == HTTPStatus.OK
assert resp_1.content_type == "image/svg+xml" assert resp_1.content_type == "image/svg+xml"
body = await resp_1.read() body = await resp_1.read()
assert body == fakeimgbytes_svg assert body == fakeimgbytes_svg
resp_2 = await client.get("/api/camera_proxy/camera.config_test_jpg") 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.status == HTTPStatus.OK
assert resp_2.content_type == "image/jpeg" assert resp_2.content_type == "image/jpeg"
body = await resp_2.read() 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") resp = await client.get("/api/camera_proxy/camera.config_test")
assert resp.status == HTTPStatus.OK assert resp.status == HTTPStatus.OK
assert respx.calls.call_count == 2 assert respx.calls.call_count == 1
assert await resp.read() == fakeimgbytes_png assert await resp.read() == fakeimgbytes_png
respx.get("http://example.com").respond(stream=fakeimgbytes_jpg) 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(), side_effect=asyncio.CancelledError(),
): ):
resp = await client.get("/api/camera_proxy/camera.config_test") 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 assert resp.status == HTTPStatus.INTERNAL_SERVER_ERROR
respx.get("http://example.com").side_effect = [ respx.get("http://example.com").side_effect = [
@ -450,7 +450,7 @@ async def test_timeout_cancelled(hass, hass_client, fakeimgbytes_png, fakeimgbyt
httpx.TimeoutException, 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") resp = await client.get("/api/camera_proxy/camera.config_test")
assert respx.calls.call_count == total_calls assert respx.calls.call_count == total_calls
assert resp.status == HTTPStatus.OK 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"), ("sample2_jpeg_odd_header.jpg"),
("sample3_jpeg_odd_header.jpg"), ("sample3_jpeg_odd_header.jpg"),
("sample4_K5-60mileAnim-320x240.gif"), ("sample4_K5-60mileAnim-320x240.gif"),
("sample5_webp.webp"),
], ],
) )
async def test_form_only_still_sample(hass, user_flow, image_file): 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): 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.""" """Test we complete ok if the user enters a stream url."""
with mock_av_open as mock_setup: with mock_av_open as mock_setup:
data = TESTDATA data = TESTDATA.copy()
data[CONF_RTSP_TRANSPORT] = "tcp" data[CONF_RTSP_TRANSPORT] = "tcp"
data[CONF_STREAM_SOURCE] = "rtsp://127.0.0.1/testurl/2"
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
user_flow["flow_id"], data 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"] == { assert result2["options"] == {
CONF_STILL_IMAGE_URL: "http://127.0.0.1/testurl/1", CONF_STILL_IMAGE_URL: "http://127.0.0.1/testurl/1",
CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, 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_RTSP_TRANSPORT: "tcp",
CONF_USERNAME: "fred_flintstone", CONF_USERNAME: "fred_flintstone",
CONF_PASSWORD: "bambam", CONF_PASSWORD: "bambam",
@ -216,7 +218,6 @@ async def test_form_only_stream(hass, mock_av_open, fakeimgbytes_jpg):
assert result3["options"] == { assert result3["options"] == {
CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION,
CONF_STREAM_SOURCE: "http://127.0.0.1/testurl/2", CONF_STREAM_SOURCE: "http://127.0.0.1/testurl/2",
CONF_RTSP_TRANSPORT: "tcp",
CONF_USERNAME: "fred_flintstone", CONF_USERNAME: "fred_flintstone",
CONF_PASSWORD: "bambam", CONF_PASSWORD: "bambam",
CONF_LIMIT_REFETCH_TO_URL_CHANGE: False, 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 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. # These above can be deleted after deprecation period is finished.

View File

@ -132,9 +132,16 @@ async def token_scopes() -> list[str]:
@pytest.fixture @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.""" """Fixture that defines creds used in the test."""
token_expiry = utcnow() + datetime.timedelta(days=7)
return OAuth2Credentials( return OAuth2Credentials(
access_token="ACCESS_TOKEN", access_token="ACCESS_TOKEN",
client_id="client-id", client_id="client-id",
@ -156,9 +163,16 @@ async def storage() -> YieldFixture[FakeStorage]:
@pytest.fixture @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.""" """Fixture to create a config entry for the integration."""
token_expiry = utcnow() + datetime.timedelta(days=7)
return MockConfigEntry( return MockConfigEntry(
domain=DOMAIN, domain=DOMAIN,
data={ data={
@ -168,7 +182,7 @@ async def config_entry(token_scopes: list[str]) -> MockConfigEntry:
"refresh_token": "REFRESH_TOKEN", "refresh_token": "REFRESH_TOKEN",
"scope": " ".join(token_scopes), "scope": " ".join(token_scopes),
"token_type": "Bearer", "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 from collections.abc import Awaitable, Callable
import datetime import datetime
import http
import time
from typing import Any from typing import Any
from unittest.mock import Mock, call, patch from unittest.mock import Mock, call, patch
@ -29,6 +31,9 @@ from .conftest import (
) )
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
from tests.test_util.aiohttp import AiohttpClientMocker
EXPIRED_TOKEN_TIMESTAMP = datetime.datetime(2022, 4, 8).timestamp()
# Typing helpers # Typing helpers
HassApi = Callable[[], Awaitable[dict[str, Any]]] HassApi = Callable[[], Awaitable[dict[str, Any]]]
@ -469,3 +474,86 @@ async def test_scan_calendars(
assert state assert state
assert state.name == "Calendar 2" assert state.name == "Calendar 2"
assert state.state == STATE_OFF 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() await hass.async_block_till_done()
assert len(hass.config_entries.async_entries(const.DOMAIN)) == 1 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): 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.volume_level = 0.44
dmr_device.is_volume_muted = False dmr_device.is_volume_muted = False
dmr_device.on_event = None dmr_device.on_event = None
dmr_device.is_subscribed = False
def _raise_event(service, state_variables): def _raise_event(service, state_variables):
if dmr_device.on_event: if dmr_device.on_event:
dmr_device.on_event(service, state_variables) dmr_device.on_event(service, state_variables)
dmr_device.raise_event = _raise_event 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 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() upnp_notify_server.async_stop_server.assert_not_called()
assert "Device rejected subscription" in caplog.text 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_OPEN,
STATE_OPENING, 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 from homeassistant.helpers.entity_component import async_update_entity
ROLLER_BLOCK_ID = 1 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 async_update_entity(hass, "cover.test_cover_0")
await hass.async_block_till_done() 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.""" """The tests for SleepIQ number platform."""
from homeassistant.components.number import DOMAIN 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.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_ICON
from homeassistant.helpers import entity_registry as er 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.state == "40.0"
assert state.attributes.get(ATTR_ICON) == "mdi:bed" 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 ( assert (
state.attributes.get(ATTR_FRIENDLY_NAME) state.attributes.get(ATTR_FRIENDLY_NAME)
== f"SleepNumber {BED_NAME} {SLEEPER_L_NAME} Firmness" == 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.state == "80.0"
assert state.attributes.get(ATTR_ICON) == "mdi:bed" 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 ( assert (
state.attributes.get(ATTR_FRIENDLY_NAME) state.attributes.get(ATTR_FRIENDLY_NAME)
== f"SleepNumber {BED_NAME} {SLEEPER_R_NAME} Firmness" == 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") state = hass.states.get(f"number.sleepnumber_{BED_NAME_LOWER}_right_head_position")
assert state.state == "60.0" assert state.state == "60.0"
assert state.attributes.get(ATTR_ICON) == "mdi:bed" 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 ( assert (
state.attributes.get(ATTR_FRIENDLY_NAME) state.attributes.get(ATTR_FRIENDLY_NAME)
== f"SleepNumber {BED_NAME} Right Head Position" == 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") state = hass.states.get(f"number.sleepnumber_{BED_NAME_LOWER}_left_head_position")
assert state.state == "50.0" assert state.state == "50.0"
assert state.attributes.get(ATTR_ICON) == "mdi:bed" 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 ( assert (
state.attributes.get(ATTR_FRIENDLY_NAME) state.attributes.get(ATTR_FRIENDLY_NAME)
== f"SleepNumber {BED_NAME} Left Head Position" == 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") state = hass.states.get(f"number.sleepnumber_{BED_NAME_LOWER}_foot_position")
assert state.state == "10.0" assert state.state == "10.0"
assert state.attributes.get(ATTR_ICON) == "mdi:bed" 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 ( assert (
state.attributes.get(ATTR_FRIENDLY_NAME) state.attributes.get(ATTR_FRIENDLY_NAME)
== f"SleepNumber {BED_NAME} Foot Position" == 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, ATTR_ENTITY_ID: entity_id,
"init_states": [340, 20, 50], "init_states": [340, 20, 50],
"random_seed": 600,
}, },
blocking=True, blocking=True,
) )
@ -539,7 +540,7 @@ async def test_smart_strip_custom_random_effect(hass: HomeAssistant) -> None:
"transition": 0, "transition": 0,
"type": "random", "type": "random",
"init_states": [[340, 20, 50]], "init_states": [[340, 20, 50]],
"random_seed": 100, "random_seed": 600,
} }
) )
strip.set_custom_effect.reset_mock() strip.set_custom_effect.reset_mock()

View File

@ -6,7 +6,7 @@ from unittest.mock import AsyncMock, Mock
import pytest import pytest
from pyunifiprotect.data import Camera, Light 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.const import DEFAULT_ATTRIBUTION
from homeassistant.components.unifiprotect.switch import ( from homeassistant.components.unifiprotect.switch import (
@ -26,6 +26,11 @@ 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_NO_EXTRA = [
d for d in CAMERA_SWITCHES_NO_FACE if d.name not in ("High FPS", "Privacy Mode")
]
@pytest.fixture(name="light") @pytest.fixture(name="light")
async def light_fixture( 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_privacy_mask = True
camera_obj.feature_flags.has_speaker = True camera_obj.feature_flags.has_speaker = True
camera_obj.feature_flags.has_smart_detect = 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.is_ssh_enabled = False
camera_obj.led_settings.is_enabled = False camera_obj.led_settings.is_enabled = False
camera_obj.hdr_mode = False camera_obj.hdr_mode = False
@ -244,7 +253,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: for description in CAMERA_SWITCHES_NO_FACE:
unique_id, entity_id = ids_from_device_description( unique_id, entity_id = ids_from_device_description(
Platform.SWITCH, camera, description Platform.SWITCH, camera, description
) )
@ -375,15 +384,12 @@ async def test_switch_camera_ssh(
camera.set_ssh.assert_called_with(False) 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( async def test_switch_camera_simple(
hass: HomeAssistant, camera: Camera, description: ProtectSwitchEntityDescription hass: HomeAssistant, camera: Camera, description: ProtectSwitchEntityDescription
): ):
"""Tests all simple switches for cameras.""" """Tests all simple switches for cameras."""
if description.name in ("High FPS", "Privacy Mode"):
return
assert description.ufp_set_method is not None assert description.ufp_set_method is not None
camera.__fields__[description.ufp_set_method] = Mock() camera.__fields__[description.ufp_set_method] = Mock()