mirror of
https://github.com/home-assistant/core.git
synced 2025-07-25 14:17:45 +00:00
Merge pull request #69835 from home-assistant/rc
This commit is contained in:
commit
a1fddc3c4d
@ -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",
|
||||
|
@ -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"
|
||||
|
@ -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):
|
||||
|
@ -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"
|
||||
|
@ -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(
|
||||
|
@ -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(
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"]
|
||||
|
@ -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:
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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",
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -180,4 +180,4 @@ random_effect:
|
||||
number:
|
||||
min: 1
|
||||
step: 1
|
||||
max: 100
|
||||
max: 600
|
||||
|
@ -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",
|
||||
|
@ -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, ...] = (
|
||||
|
@ -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"]
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
BIN
tests/components/generic/sample5_webp.webp
Normal file
BIN
tests/components/generic/sample5_webp.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.1 KiB |
@ -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
|
||||
|
@ -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.
|
||||
|
||||
|
||||
|
@ -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,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
@ -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"
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
Loading…
x
Reference in New Issue
Block a user