Merge pull request #61902 from home-assistant/rc

This commit is contained in:
Franck Nijhof 2021-12-15 17:02:35 +01:00 committed by GitHub
commit 6d8d472f0f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 533 additions and 100 deletions

View File

@ -131,7 +131,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build base image
uses: home-assistant/builder@2021.11.4
uses: home-assistant/builder@2021.12.0
with:
args: |
$BUILD_ARGS \
@ -170,6 +170,17 @@ jobs:
- name: Checkout the repository
uses: actions/checkout@v2.4.0
- name: Set build additional args
run: |
# Create general tags
if [[ "${{ needs.init.outputs.version }}" =~ d ]]; then
echo "BUILD_ARGS=--additional-tag dev" >> $GITHUB_ENV
elif [[ "${{ needs.init.outputs.version }}" =~ b ]]; then
echo "BUILD_ARGS=--additional-tag beta" >> $GITHUB_ENV
else
echo "BUILD_ARGS=--additional-tag stable" >> $GITHUB_ENV
fi
- name: Login to DockerHub
uses: docker/login-action@v1.10.0
with:
@ -184,7 +195,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build base image
uses: home-assistant/builder@2021.11.4
uses: home-assistant/builder@2021.12.0
with:
args: |
$BUILD_ARGS \

View File

@ -3,7 +3,7 @@
"name": "Brunt Blind Engine",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/brunt",
"requirements": ["brunt==1.0.0"],
"requirements": ["brunt==1.0.1"],
"codeowners": ["@eavanvalkenburg"],
"iot_class": "cloud_polling"
}

View File

@ -3,7 +3,7 @@
"name": "Google Cast",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/cast",
"requirements": ["pychromecast==10.1.1"],
"requirements": ["pychromecast==10.2.1"],
"after_dependencies": [
"cloud",
"http",

View File

@ -47,7 +47,6 @@ from homeassistant.components.plex.const import PLEX_URI_SCHEME
from homeassistant.components.plex.services import lookup_plex_media
from homeassistant.const import (
CAST_APP_ID_HOMEASSISTANT_LOVELACE,
CAST_APP_ID_HOMEASSISTANT_MEDIA,
EVENT_HOMEASSISTANT_STOP,
STATE_IDLE,
STATE_OFF,
@ -230,7 +229,6 @@ class CastDevice(MediaPlayerEntity):
self._cast_info.cast_info,
ChromeCastZeroconf.get_zeroconf(),
)
chromecast.media_controller.app_id = CAST_APP_ID_HOMEASSISTANT_MEDIA
self._chromecast = chromecast
if CAST_MULTIZONE_MANAGER_KEY not in self.hass.data:
@ -527,9 +525,8 @@ class CastDevice(MediaPlayerEntity):
self._chromecast.register_handler(controller)
controller.play_media(media)
else:
self._chromecast.media_controller.play_media(
media_id, media_type, **kwargs.get(ATTR_MEDIA_EXTRA, {})
)
app_data = {"media_id": media_id, "media_type": media_type, **extra}
quick_play(self._chromecast, "homeassistant_media", app_data)
def _media_status(self):
"""
@ -820,7 +817,6 @@ class DynamicCastGroup:
self._cast_info.cast_info,
ChromeCastZeroconf.get_zeroconf(),
)
chromecast.media_controller.app_id = CAST_APP_ID_HOMEASSISTANT_MEDIA
self._chromecast = chromecast
if CAST_MULTIZONE_MANAGER_KEY not in self.hass.data:

View File

@ -2,7 +2,7 @@
"domain": "environment_canada",
"name": "Environment Canada",
"documentation": "https://www.home-assistant.io/integrations/environment_canada",
"requirements": ["env_canada==0.5.18"],
"requirements": ["env_canada==0.5.20"],
"codeowners": ["@gwww", "@michaeldavie"],
"config_flow": true,
"iot_class": "cloud_polling"

View File

@ -3,7 +3,7 @@
"name": "Home Assistant Frontend",
"documentation": "https://www.home-assistant.io/integrations/frontend",
"requirements": [
"home-assistant-frontend==20211212.0"
"home-assistant-frontend==20211215.0"
],
"dependencies": [
"api",

View File

@ -3,7 +3,7 @@
"name": "Philips Hue",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/hue",
"requirements": ["aiohue==3.0.3"],
"requirements": ["aiohue==3.0.5"],
"ssdp": [
{
"manufacturer": "Royal Philips Electronics",

View File

@ -7,6 +7,7 @@ from aiohue.v2.models.button import ButtonEvent
from aiohue.v2.models.resource import ResourceTypes
import voluptuous as vol
from homeassistant.components import persistent_notification
from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
from homeassistant.components.homeassistant.triggers import event as event_trigger
from homeassistant.const import (
@ -35,7 +36,7 @@ if TYPE_CHECKING:
TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_TYPE): str,
vol.Required(CONF_SUBTYPE): int,
vol.Required(CONF_SUBTYPE): vol.Union(int, str),
vol.Optional(CONF_UNIQUE_ID): str,
}
)
@ -54,6 +55,33 @@ DEVICE_SPECIFIC_EVENT_TYPES = {
}
def check_invalid_device_trigger(
bridge: HueBridge,
config: ConfigType,
device_entry: DeviceEntry,
automation_info: AutomationTriggerInfo | None = None,
):
"""Check automation config for deprecated format."""
# NOTE: Remove this check after 2022.6
if isinstance(config["subtype"], int):
return
# found deprecated V1 style trigger, notify the user that it should be adjusted
msg = (
f"Incompatible device trigger detected for "
f"[{device_entry.name}](/config/devices/device/{device_entry.id}) "
"Please manually fix the outdated automation(s) once to fix this issue."
)
if automation_info:
automation_id = automation_info["variables"]["this"]["attributes"]["id"] # type: ignore
msg += f"\n\n[Check it out](/config/automation/edit/{automation_id})."
persistent_notification.async_create(
bridge.hass,
msg,
title="Outdated device trigger found",
notification_id=f"hue_trigger_{device_entry.id}",
)
async def async_validate_trigger_config(
bridge: "HueBridge",
device_entry: DeviceEntry,
@ -61,6 +89,7 @@ async def async_validate_trigger_config(
):
"""Validate config."""
config = TRIGGER_SCHEMA(config)
check_invalid_device_trigger(bridge, config, device_entry)
return config
@ -84,6 +113,7 @@ async def async_attach_trigger(
},
}
)
check_invalid_device_trigger(bridge, config, device_entry, automation_info)
return await event_trigger.async_attach_trigger(
hass, event_config, action, automation_info, platform_type="device"
)

View File

@ -47,6 +47,20 @@ class HueBaseEntity(Entity):
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self.device.id)},
)
# some (3th party) Hue lights report their connection status incorrectly
# causing the zigbee availability to report as disconnected while in fact
# it can be controlled. Although this is in fact something the device manufacturer
# should fix, we work around it here. If the light is reported unavailable at
# startup, we ignore the availability status of the zigbee connection
self._ignore_availability = False
if self.device is None:
return
if zigbee := self.bridge.api.devices.get_zigbee_connectivity(self.device.id):
self._ignore_availability = (
# Official Hue lights are reliable
self.device.product_data.manufacturer_name != "Signify Netherlands B.V."
and zigbee.status != ConnectivityServiceStatus.CONNECTED
)
@property
def name(self) -> str:
@ -98,13 +112,12 @@ class HueBaseEntity(Entity):
def available(self) -> bool:
"""Return entity availability."""
if self.device is None:
# devices without a device attached should be always available
# entities without a device attached should be always available
return True
if self.resource.type == ResourceTypes.ZIGBEE_CONNECTIVITY:
# the zigbee connectivity sensor itself should be always available
return True
if self.device.product_data.manufacturer_name != "Signify Netherlands B.V.":
# availability status for non-philips brand lights is unreliable
if self._ignore_availability:
return True
if zigbee := self.bridge.api.devices.get_zigbee_connectivity(self.device.id):
# all device-attached entities get availability from the zigbee connectivity

View File

@ -6,16 +6,19 @@ from typing import Any
from aiohue.v2 import HueBridgeV2
from aiohue.v2.controllers.events import EventType
from aiohue.v2.controllers.groups import GroupedLight, Room, Zone
from aiohue.v2.models.feature import AlertEffectType
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP,
ATTR_FLASH,
ATTR_TRANSITION,
ATTR_XY_COLOR,
COLOR_MODE_BRIGHTNESS,
COLOR_MODE_COLOR_TEMP,
COLOR_MODE_ONOFF,
COLOR_MODE_XY,
SUPPORT_FLASH,
SUPPORT_TRANSITION,
LightEntity,
)
@ -32,6 +35,7 @@ ALLOWED_ERRORS = [
'device (groupedLight) is "soft off", command (on) may not have effect',
"device (light) has communication issues, command (on) may not have effect",
'device (light) is "soft off", command (on) may not have effect',
"attribute (supportedAlertActions) cannot be written",
]
@ -88,6 +92,7 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
self.group = group
self.controller = controller
self.api: HueBridgeV2 = bridge.api
self._attr_supported_features |= SUPPORT_FLASH
self._attr_supported_features |= SUPPORT_TRANSITION
# Entities for Hue groups are disabled by default
@ -146,6 +151,7 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
xy_color = kwargs.get(ATTR_XY_COLOR)
color_temp = kwargs.get(ATTR_COLOR_TEMP)
brightness = kwargs.get(ATTR_BRIGHTNESS)
flash = kwargs.get(ATTR_FLASH)
if brightness is not None:
# Hue uses a range of [0, 100] to control brightness.
brightness = float((brightness / 255) * 100)
@ -160,6 +166,7 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
and xy_color is None
and color_temp is None
and transition is None
and flash is None
):
await self.bridge.async_request_call(
self.controller.set_state,
@ -180,17 +187,37 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
color_xy=xy_color if light.supports_color else None,
color_temp=color_temp if light.supports_color_temperature else None,
transition_time=transition,
alert=AlertEffectType.BREATHE if flash is not None else None,
allowed_errors=ALLOWED_ERRORS,
)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the light off."""
await self.bridge.async_request_call(
self.controller.set_state,
id=self.resource.id,
on=False,
allowed_errors=ALLOWED_ERRORS,
)
transition = kwargs.get(ATTR_TRANSITION)
if transition is not None:
# hue transition duration is in milliseconds
transition = int(transition * 1000)
# NOTE: a grouped_light can only handle turn on/off
# To set other features, you'll have to control the attached lights
if transition is None:
await self.bridge.async_request_call(
self.controller.set_state,
id=self.resource.id,
on=False,
allowed_errors=ALLOWED_ERRORS,
)
return
# redirect all other feature commands to underlying lights
for light in self.controller.get_lights(self.resource.id):
await self.bridge.async_request_call(
self.api.lights.set_state,
light.id,
on=False,
transition_time=transition,
allowed_errors=ALLOWED_ERRORS,
)
@callback
def on_update(self) -> None:

View File

@ -6,17 +6,20 @@ from typing import Any
from aiohue import HueBridgeV2
from aiohue.v2.controllers.events import EventType
from aiohue.v2.controllers.lights import LightsController
from aiohue.v2.models.feature import AlertEffectType
from aiohue.v2.models.light import Light
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP,
ATTR_FLASH,
ATTR_TRANSITION,
ATTR_XY_COLOR,
COLOR_MODE_BRIGHTNESS,
COLOR_MODE_COLOR_TEMP,
COLOR_MODE_ONOFF,
COLOR_MODE_XY,
SUPPORT_FLASH,
SUPPORT_TRANSITION,
LightEntity,
)
@ -31,6 +34,7 @@ from .entity import HueBaseEntity
ALLOWED_ERRORS = [
"device (light) has communication issues, command (on) may not have effect",
'device (light) is "soft off", command (on) may not have effect',
"attribute (supportedAlertActions) cannot be written",
]
@ -68,6 +72,7 @@ class HueLight(HueBaseEntity, LightEntity):
) -> None:
"""Initialize the light."""
super().__init__(bridge, controller, resource)
self._attr_supported_features |= SUPPORT_FLASH
self.resource = resource
self.controller = controller
self._supported_color_modes = set()
@ -154,6 +159,7 @@ class HueLight(HueBaseEntity, LightEntity):
xy_color = kwargs.get(ATTR_XY_COLOR)
color_temp = kwargs.get(ATTR_COLOR_TEMP)
brightness = kwargs.get(ATTR_BRIGHTNESS)
flash = kwargs.get(ATTR_FLASH)
if brightness is not None:
# Hue uses a range of [0, 100] to control brightness.
brightness = float((brightness / 255) * 100)
@ -169,12 +175,14 @@ class HueLight(HueBaseEntity, LightEntity):
color_xy=xy_color,
color_temp=color_temp,
transition_time=transition,
alert=AlertEffectType.BREATHE if flash is not None else None,
allowed_errors=ALLOWED_ERRORS,
)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the light off."""
transition = kwargs.get(ATTR_TRANSITION)
flash = kwargs.get(ATTR_FLASH)
if transition is not None:
# hue transition duration is in milliseconds
transition = int(transition * 1000)
@ -183,5 +191,6 @@ class HueLight(HueBaseEntity, LightEntity):
id=self.resource.id,
on=False,
transition_time=transition,
alert=AlertEffectType.BREATHE if flash is not None else None,
allowed_errors=ALLOWED_ERRORS,
)

View File

@ -383,6 +383,7 @@ class KNXModule:
if _conn_type == CONF_KNX_ROUTING:
return ConnectionConfig(
connection_type=ConnectionType.ROUTING,
local_ip=self.config.get(ConnectionSchema.CONF_KNX_LOCAL_IP),
auto_reconnect=True,
)
if _conn_type == CONF_KNX_TUNNELING:

View File

@ -137,9 +137,11 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
vol.Required(
ConnectionSchema.CONF_KNX_ROUTE_BACK, default=False
): vol.Coerce(bool),
vol.Optional(ConnectionSchema.CONF_KNX_LOCAL_IP): str,
}
if self.show_advanced_options:
fields[vol.Optional(ConnectionSchema.CONF_KNX_LOCAL_IP)] = str
return self.async_show_form(
step_id="manual_tunnel", data_schema=vol.Schema(fields), errors=errors
)
@ -195,6 +197,9 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
CONF_KNX_INDIVIDUAL_ADDRESS: user_input[
CONF_KNX_INDIVIDUAL_ADDRESS
],
ConnectionSchema.CONF_KNX_LOCAL_IP: user_input.get(
ConnectionSchema.CONF_KNX_LOCAL_IP
),
CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING,
},
)
@ -211,6 +216,9 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
): cv.port,
}
if self.show_advanced_options:
fields[vol.Optional(ConnectionSchema.CONF_KNX_LOCAL_IP)] = str
return self.async_show_form(
step_id="routing", data_schema=vol.Schema(fields), errors=errors
)
@ -306,7 +314,6 @@ class KNXOptionsFlowHandler(OptionsFlow):
vol.Required(
CONF_PORT, default=self.current_config.get(CONF_PORT, 3671)
): cv.port,
vol.Optional(ConnectionSchema.CONF_KNX_LOCAL_IP): str,
vol.Required(
ConnectionSchema.CONF_KNX_ROUTE_BACK,
default=self.current_config.get(
@ -381,6 +388,14 @@ class KNXOptionsFlowHandler(OptionsFlow):
}
if self.show_advanced_options:
data_schema[
vol.Optional(
ConnectionSchema.CONF_KNX_LOCAL_IP,
default=self.current_config.get(
ConnectionSchema.CONF_KNX_LOCAL_IP,
),
)
] = str
data_schema[
vol.Required(
ConnectionSchema.CONF_KNX_STATE_UPDATER,

View File

@ -28,7 +28,8 @@
"data": {
"individual_address": "Individual address for the routing connection",
"multicast_group": "The multicast group used for routing",
"multicast_port": "The multicast port used for routing"
"multicast_port": "The multicast port used for routing",
"local_ip": "Local IP (leave empty if unsure)"
}
}
},
@ -48,6 +49,7 @@
"individual_address": "Default individual address",
"multicast_group": "Multicast group used for routing and discovery",
"multicast_port": "Multicast port used for routing and discovery",
"local_ip": "Local IP (leave empty if unsure)",
"state_updater": "Globally enable reading states from the KNX Bus",
"rate_limit": "Maximum outgoing telegrams per second"
}
@ -56,8 +58,7 @@
"data": {
"port": "[%key:common::config_flow::data::port%]",
"host": "[%key:common::config_flow::data::host%]",
"route_back": "Route Back / NAT Mode",
"local_ip": "Local IP (leave empty if unsure)"
"route_back": "Route Back / NAT Mode"
}
}
}

View File

@ -22,7 +22,8 @@
"data": {
"individual_address": "Individual address for the routing connection",
"multicast_group": "The multicast group used for routing",
"multicast_port": "The multicast port used for routing"
"multicast_port": "The multicast port used for routing",
"local_ip": "Local IP (leave empty if unsure)"
},
"description": "Please configure the routing options."
},
@ -48,6 +49,7 @@
"individual_address": "Default individual address",
"multicast_group": "Multicast group used for routing and discovery",
"multicast_port": "Multicast port used for routing and discovery",
"local_ip": "Local IP (leave empty if unsure)",
"rate_limit": "Maximum outgoing telegrams per second",
"state_updater": "Globally enable reading states from the KNX Bus"
}
@ -55,7 +57,6 @@
"tunnel": {
"data": {
"host": "Host",
"local_ip": "Local IP (leave empty if unsure)",
"port": "Port",
"route_back": "Route Back / NAT Mode"
}

View File

@ -1,9 +1,8 @@
{
"disabled": "Library has incompatible requirements.",
"domain": "lupusec",
"name": "Lupus Electronics LUPUSEC",
"documentation": "https://www.home-assistant.io/integrations/lupusec",
"requirements": ["lupupy==0.0.21"],
"requirements": ["lupupy==0.0.24"],
"codeowners": ["@majuss"],
"iot_class": "local_polling"
}

View File

@ -3,7 +3,7 @@
"name": "MELCloud",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/melcloud",
"requirements": ["pymelcloud==2.5.5"],
"requirements": ["pymelcloud==2.5.6"],
"codeowners": ["@vilppuvuorinen"],
"iot_class": "cloud_polling"
}

View File

@ -4,7 +4,7 @@
"config_flow": true,
"dependencies": ["ffmpeg", "http", "media_source"],
"documentation": "https://www.home-assistant.io/integrations/nest",
"requirements": ["python-nest==4.1.0", "google-nest-sdm==0.4.6"],
"requirements": ["python-nest==4.1.0", "google-nest-sdm==0.4.8"],
"codeowners": ["@allenporter"],
"quality_scale": "platinum",
"dhcp": [

View File

@ -63,6 +63,9 @@ async def async_get_media_source(hass: HomeAssistant) -> MediaSource:
async def get_media_source_devices(hass: HomeAssistant) -> Mapping[str, Device]:
"""Return a mapping of device id to eligible Nest event media devices."""
if DATA_SUBSCRIBER not in hass.data[DOMAIN]:
# Integration unloaded, or is legacy nest integration
return {}
subscriber = hass.data[DOMAIN][DATA_SUBSCRIBER]
device_manager = await subscriber.async_get_device_manager()
device_registry = await hass.helpers.device_registry.async_get_registry()

View File

@ -612,10 +612,24 @@ class SimpliSafe:
data={**self.entry.data, CONF_TOKEN: token},
)
@callback
def async_handle_refresh_token(token: str) -> None:
"""Handle a new refresh token."""
async_save_refresh_token(token)
if TYPE_CHECKING:
assert self._api.websocket
if self._api.websocket.connected:
# If a websocket connection is open, reconnect it to use the
# new access token:
asyncio.create_task(self._api.websocket.async_reconnect())
self.entry.async_on_unload(
self._api.add_refresh_token_callback(async_save_refresh_token)
self._api.add_refresh_token_callback(async_handle_refresh_token)
)
# Save the refresh token we got on entry setup:
async_save_refresh_token(self._api.refresh_token)
async def async_update(self) -> None:

View File

@ -5,7 +5,7 @@
"documentation": "https://www.home-assistant.io/integrations/smappee",
"dependencies": ["http"],
"requirements": [
"pysmappee==0.2.27"
"pysmappee==0.2.29"
],
"codeowners": [
"@bsmappee"

View File

@ -3,7 +3,7 @@
"name": "Tailscale",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/tailscale",
"requirements": ["tailscale==0.1.4"],
"requirements": ["tailscale==0.1.5"],
"codeowners": ["@frenck"],
"quality_scale": "platinum",
"iot_class": "cloud_polling"

View File

@ -2,7 +2,7 @@
"domain": "tibber",
"name": "Tibber",
"documentation": "https://www.home-assistant.io/integrations/tibber",
"requirements": ["pyTibber==0.21.0"],
"requirements": ["pyTibber==0.21.1"],
"codeowners": ["@danielhiversen"],
"quality_scale": "silver",
"config_flow": true,

View File

@ -2,7 +2,7 @@
"domain": "totalconnect",
"name": "Total Connect",
"documentation": "https://www.home-assistant.io/integrations/totalconnect",
"requirements": ["total_connect_client==2021.11.4"],
"requirements": ["total_connect_client==2021.12"],
"dependencies": [],
"codeowners": ["@austinmroczek"],
"config_flow": true,

View File

@ -3,7 +3,7 @@
"name": "Xiaomi Miio",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/xiaomi_miio",
"requirements": ["construct==2.10.56", "micloud==0.4", "python-miio==0.5.9.1"],
"requirements": ["construct==2.10.56", "micloud==0.4", "python-miio==0.5.9.2"],
"codeowners": ["@rytilahti", "@syssi", "@starkillerOG", "@bieniu"],
"zeroconf": ["_miio._udp.local."],
"iot_class": "local_polling"

View File

@ -206,11 +206,11 @@ class Thermostat(ZhaEntity, ClimateEntity):
unoccupied_cooling_setpoint = self._thrm.unoccupied_cooling_setpoint
if unoccupied_cooling_setpoint is not None:
data[ATTR_UNOCCP_HEAT_SETPT] = unoccupied_cooling_setpoint
data[ATTR_UNOCCP_COOL_SETPT] = unoccupied_cooling_setpoint
unoccupied_heating_setpoint = self._thrm.unoccupied_heating_setpoint
if unoccupied_heating_setpoint is not None:
data[ATTR_UNOCCP_COOL_SETPT] = unoccupied_heating_setpoint
data[ATTR_UNOCCP_HEAT_SETPT] = unoccupied_heating_setpoint
return data
@property

View File

@ -7,7 +7,7 @@ from homeassistant.backports.enum import StrEnum
MAJOR_VERSION: Final = 2021
MINOR_VERSION: Final = 12
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, 8, 0)

View File

@ -16,7 +16,7 @@ ciso8601==2.2.0
cryptography==35.0.0
emoji==1.5.0
hass-nabucasa==0.50.0
home-assistant-frontend==20211212.0
home-assistant-frontend==20211215.0
httpx==0.21.0
ifaddr==0.1.7
jinja2==3.0.3

View File

@ -186,7 +186,7 @@ aiohomekit==0.6.4
aiohttp_cors==0.7.0
# homeassistant.components.hue
aiohue==3.0.3
aiohue==3.0.5
# homeassistant.components.imap
aioimaplib==0.9.0
@ -440,7 +440,7 @@ brother==1.1.0
brottsplatskartan==0.0.1
# homeassistant.components.brunt
brunt==1.0.0
brunt==1.0.1
# homeassistant.components.bsblan
bsblan==0.4.0
@ -600,7 +600,7 @@ enocean==0.50
enturclient==0.2.2
# homeassistant.components.environment_canada
env_canada==0.5.18
env_canada==0.5.20
# homeassistant.components.envirophat
# envirophat==0.0.6
@ -738,7 +738,7 @@ google-cloud-pubsub==2.1.0
google-cloud-texttospeech==0.4.0
# homeassistant.components.nest
google-nest-sdm==0.4.6
google-nest-sdm==0.4.8
# homeassistant.components.google_travel_time
googlemaps==2.5.1
@ -819,7 +819,7 @@ hole==0.7.0
holidays==0.11.3.1
# homeassistant.components.frontend
home-assistant-frontend==20211212.0
home-assistant-frontend==20211215.0
# homeassistant.components.zwave
homeassistant-pyozw==0.1.10
@ -968,6 +968,9 @@ london-tube-status==0.2
# homeassistant.components.luftdaten
luftdaten==0.7.1
# homeassistant.components.lupusec
lupupy==0.0.24
# homeassistant.components.lw12wifi
lw12==0.9.2
@ -1324,7 +1327,7 @@ pyRFXtrx==0.27.0
# pySwitchmate==0.4.6
# homeassistant.components.tibber
pyTibber==0.21.0
pyTibber==0.21.1
# homeassistant.components.dlink
pyW215==0.7.0
@ -1393,7 +1396,7 @@ pycfdns==1.2.2
pychannels==1.0.0
# homeassistant.components.cast
pychromecast==10.1.1
pychromecast==10.2.1
# homeassistant.components.pocketcasts
pycketcasts==1.0.0
@ -1628,7 +1631,7 @@ pymazda==0.2.2
pymediaroom==0.6.4.1
# homeassistant.components.melcloud
pymelcloud==2.5.5
pymelcloud==2.5.6
# homeassistant.components.meteoclimatic
pymeteoclimatic==0.0.6
@ -1802,7 +1805,7 @@ pyskyqhub==0.1.3
pysma==0.6.9
# homeassistant.components.smappee
pysmappee==0.2.27
pysmappee==0.2.29
# homeassistant.components.smartthings
pysmartapp==0.3.3
@ -1901,7 +1904,7 @@ python-kasa==0.4.0
# python-lirc==1.2.3
# homeassistant.components.xiaomi_miio
python-miio==0.5.9.1
python-miio==0.5.9.2
# homeassistant.components.mpd
python-mpd2==3.0.4
@ -2269,7 +2272,7 @@ systembridge==2.2.3
tahoma-api==0.0.16
# homeassistant.components.tailscale
tailscale==0.1.4
tailscale==0.1.5
# homeassistant.components.tank_utility
tank_utility==1.4.0
@ -2326,7 +2329,7 @@ tololib==0.1.0b3
toonapi==0.2.1
# homeassistant.components.totalconnect
total_connect_client==2021.11.4
total_connect_client==2021.12
# homeassistant.components.tplink_lte
tp-connected==0.0.4

View File

@ -131,7 +131,7 @@ aiohomekit==0.6.4
aiohttp_cors==0.7.0
# homeassistant.components.hue
aiohue==3.0.3
aiohue==3.0.5
# homeassistant.components.apache_kafka
aiokafka==0.6.0
@ -281,7 +281,7 @@ broadlink==0.18.0
brother==1.1.0
# homeassistant.components.brunt
brunt==1.0.0
brunt==1.0.1
# homeassistant.components.bsblan
bsblan==0.4.0
@ -375,7 +375,7 @@ emulated_roku==0.2.1
enocean==0.50
# homeassistant.components.environment_canada
env_canada==0.5.18
env_canada==0.5.20
# homeassistant.components.enphase_envoy
envoy_reader==0.20.1
@ -461,7 +461,7 @@ google-api-python-client==1.6.4
google-cloud-pubsub==2.1.0
# homeassistant.components.nest
google-nest-sdm==0.4.6
google-nest-sdm==0.4.8
# homeassistant.components.google_travel_time
googlemaps==2.5.1
@ -515,7 +515,7 @@ hole==0.7.0
holidays==0.11.3.1
# homeassistant.components.frontend
home-assistant-frontend==20211212.0
home-assistant-frontend==20211215.0
# homeassistant.components.zwave
homeassistant-pyozw==0.1.10
@ -808,7 +808,7 @@ pyMetno==0.9.0
pyRFXtrx==0.27.0
# homeassistant.components.tibber
pyTibber==0.21.0
pyTibber==0.21.1
# homeassistant.components.nextbus
py_nextbusnext==0.1.5
@ -850,7 +850,7 @@ pybotvac==0.0.22
pycfdns==1.2.2
# homeassistant.components.cast
pychromecast==10.1.1
pychromecast==10.2.1
# homeassistant.components.climacell
pyclimacell==0.18.2
@ -995,7 +995,7 @@ pymata-express==1.19
pymazda==0.2.2
# homeassistant.components.melcloud
pymelcloud==2.5.5
pymelcloud==2.5.6
# homeassistant.components.meteoclimatic
pymeteoclimatic==0.0.6
@ -1109,7 +1109,7 @@ pysignalclirestapi==0.3.4
pysma==0.6.9
# homeassistant.components.smappee
pysmappee==0.2.27
pysmappee==0.2.29
# homeassistant.components.smartthings
pysmartapp==0.3.3
@ -1145,7 +1145,7 @@ python-juicenet==1.0.2
python-kasa==0.4.0
# homeassistant.components.xiaomi_miio
python-miio==0.5.9.1
python-miio==0.5.9.2
# homeassistant.components.nest
python-nest==4.1.0
@ -1352,7 +1352,7 @@ surepy==0.7.2
systembridge==2.2.3
# homeassistant.components.tailscale
tailscale==0.1.4
tailscale==0.1.5
# homeassistant.components.tellduslive
tellduslive==0.10.11
@ -1370,7 +1370,7 @@ tololib==0.1.0b3
toonapi==0.2.1
# homeassistant.components.totalconnect
total_connect_client==2021.11.4
total_connect_client==2021.12
# homeassistant.components.transmission
transmissionrpc==0.11

View File

@ -754,7 +754,7 @@ async def test_supported_features(
assert state.attributes.get("supported_features") == supported_features
async def test_entity_play_media(hass: HomeAssistant):
async def test_entity_play_media(hass: HomeAssistant, quick_play_mock):
"""Test playing media."""
entity_id = "media_player.speaker"
reg = er.async_get(hass)
@ -776,8 +776,28 @@ async def test_entity_play_media(hass: HomeAssistant):
assert entity_id == reg.async_get_entity_id("media_player", "cast", str(info.uuid))
# Play_media
await common.async_play_media(hass, "audio", "best.mp3", entity_id)
chromecast.media_controller.play_media.assert_called_once_with("best.mp3", "audio")
await hass.services.async_call(
media_player.DOMAIN,
media_player.SERVICE_PLAY_MEDIA,
{
ATTR_ENTITY_ID: entity_id,
media_player.ATTR_MEDIA_CONTENT_TYPE: "audio",
media_player.ATTR_MEDIA_CONTENT_ID: "best.mp3",
media_player.ATTR_MEDIA_EXTRA: {"metadata": {"metadatatype": 3}},
},
blocking=True,
)
chromecast.media_controller.play_media.assert_not_called()
quick_play_mock.assert_called_once_with(
chromecast,
"homeassistant_media",
{
"media_id": "best.mp3",
"media_type": "audio",
"metadata": {"metadatatype": 3},
},
)
async def test_entity_play_media_cast(hass: HomeAssistant, quick_play_mock):
@ -865,7 +885,7 @@ async def test_entity_play_media_cast_invalid(hass, caplog, quick_play_mock):
assert "App unknown not supported" in caplog.text
async def test_entity_play_media_sign_URL(hass: HomeAssistant):
async def test_entity_play_media_sign_URL(hass: HomeAssistant, quick_play_mock):
"""Test playing media."""
entity_id = "media_player.speaker"
@ -886,8 +906,10 @@ async def test_entity_play_media_sign_URL(hass: HomeAssistant):
# Play_media
await common.async_play_media(hass, "audio", "/best.mp3", entity_id)
chromecast.media_controller.play_media.assert_called_once_with(ANY, "audio")
assert chromecast.media_controller.play_media.call_args[0][0].startswith(
quick_play_mock.assert_called_once_with(
chromecast, "homeassistant_media", {"media_id": ANY, "media_type": "audio"}
)
assert quick_play_mock.call_args[0][2]["media_id"].startswith(
"http://example.com:8123/best.mp3?authSig="
)
@ -1231,7 +1253,7 @@ async def test_group_media_states(hass, mz_mock):
assert state.state == "playing"
async def test_group_media_control(hass, mz_mock):
async def test_group_media_control(hass, mz_mock, quick_play_mock):
"""Test media controls are handled by group if entity has no state."""
entity_id = "media_player.speaker"
reg = er.async_get(hass)
@ -1286,7 +1308,12 @@ async def test_group_media_control(hass, mz_mock):
# Verify play_media is not forwarded
await common.async_play_media(hass, "music", "best.mp3", entity_id)
assert not grp_media.play_media.called
assert chromecast.media_controller.play_media.called
assert not chromecast.media_controller.play_media.called
quick_play_mock.assert_called_once_with(
chromecast,
"homeassistant_media",
{"media_id": "best.mp3", "media_type": "music"},
)
async def test_failed_cast_on_idle(hass, caplog):

View File

@ -121,6 +121,17 @@ async def test_light_turn_on_service(hass, mock_bridge_v2, v2_resources_test_dat
assert mock_bridge_v2.mock_requests[1]["json"]["on"]["on"] is True
assert mock_bridge_v2.mock_requests[1]["json"]["dynamics"]["duration"] == 6000
# test again with sending flash/alert
await hass.services.async_call(
"light",
"turn_on",
{"entity_id": test_light_id, "flash": "long"},
blocking=True,
)
assert len(mock_bridge_v2.mock_requests) == 3
assert mock_bridge_v2.mock_requests[2]["json"]["on"]["on"] is True
assert mock_bridge_v2.mock_requests[2]["json"]["alert"]["action"] == "breathe"
async def test_light_turn_off_service(hass, mock_bridge_v2, v2_resources_test_data):
"""Test calling the turn off service on a light."""
@ -295,7 +306,12 @@ async def test_grouped_lights(hass, mock_bridge_v2, v2_resources_test_data):
await hass.services.async_call(
"light",
"turn_on",
{"entity_id": test_light_id, "brightness_pct": 100, "xy_color": (0.123, 0.123)},
{
"entity_id": test_light_id,
"brightness_pct": 100,
"xy_color": (0.123, 0.123),
"transition": 6,
},
blocking=True,
)
@ -308,6 +324,9 @@ async def test_grouped_lights(hass, mock_bridge_v2, v2_resources_test_data):
)
assert mock_bridge_v2.mock_requests[index]["json"]["color"]["xy"]["x"] == 0.123
assert mock_bridge_v2.mock_requests[index]["json"]["color"]["xy"]["y"] == 0.123
assert (
mock_bridge_v2.mock_requests[index]["json"]["dynamics"]["duration"] == 6000
)
# Now generate update events by emitting the json we've sent as incoming events
for index in range(0, 3):
@ -346,3 +365,24 @@ async def test_grouped_lights(hass, mock_bridge_v2, v2_resources_test_data):
test_light = hass.states.get(test_light_id)
assert test_light is not None
assert test_light.state == "off"
# Test calling the turn off service on a grouped light with transition
mock_bridge_v2.mock_requests.clear()
test_light_id = "light.test_zone"
await hass.services.async_call(
"light",
"turn_off",
{
"entity_id": test_light_id,
"transition": 6,
},
blocking=True,
)
# PUT request should have been sent to ALL group lights with correct params
assert len(mock_bridge_v2.mock_requests) == 3
for index in range(0, 3):
assert mock_bridge_v2.mock_requests[index]["json"]["on"]["on"] is False
assert (
mock_bridge_v2.mock_requests[index]["json"]["dynamics"]["duration"] == 6000
)

View File

@ -83,6 +83,60 @@ async def test_routing_setup(hass: HomeAssistant) -> None:
CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING,
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
ConnectionSchema.CONF_KNX_MCAST_PORT: 3675,
ConnectionSchema.CONF_KNX_LOCAL_IP: None,
CONF_KNX_INDIVIDUAL_ADDRESS: "1.1.110",
}
assert len(mock_setup_entry.mock_calls) == 1
async def test_routing_setup_advanced(hass: HomeAssistant) -> None:
"""Test routing setup with advanced options."""
with patch("xknx.io.gateway_scanner.GatewayScanner.scan") as gateways:
gateways.return_value = []
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={
"source": config_entries.SOURCE_USER,
"show_advanced_options": True,
},
)
assert result["type"] == RESULT_TYPE_FORM
assert not result["errors"]
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING,
},
)
await hass.async_block_till_done()
assert result2["type"] == RESULT_TYPE_FORM
assert result2["step_id"] == "routing"
assert not result2["errors"]
with patch(
"homeassistant.components.knx.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
{
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
ConnectionSchema.CONF_KNX_MCAST_PORT: 3675,
CONF_KNX_INDIVIDUAL_ADDRESS: "1.1.110",
ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112",
},
)
await hass.async_block_till_done()
assert result3["type"] == RESULT_TYPE_CREATE_ENTRY
assert result3["title"] == CONF_KNX_ROUTING.capitalize()
assert result3["data"] == {
**DEFAULT_ENTRY_DATA,
CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING,
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
ConnectionSchema.CONF_KNX_MCAST_PORT: 3675,
ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112",
CONF_KNX_INDIVIDUAL_ADDRESS: "1.1.110",
}
@ -144,7 +198,11 @@ async def test_tunneling_setup_for_local_ip(hass: HomeAssistant) -> None:
with patch("xknx.io.gateway_scanner.GatewayScanner.scan") as gateways:
gateways.return_value = [gateway]
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
DOMAIN,
context={
"source": config_entries.SOURCE_USER,
"show_advanced_options": True,
},
)
assert result["type"] == RESULT_TYPE_FORM
assert not result["errors"]
@ -563,7 +621,6 @@ async def test_tunneling_options_flow(
CONF_HOST: "192.168.1.1",
CONF_PORT: 3675,
ConnectionSchema.CONF_KNX_ROUTE_BACK: True,
ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112",
},
)
@ -581,7 +638,6 @@ async def test_tunneling_options_flow(
CONF_HOST: "192.168.1.1",
CONF_PORT: 3675,
ConnectionSchema.CONF_KNX_ROUTE_BACK: True,
ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112",
}
@ -611,6 +667,7 @@ async def test_advanced_options(
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
ConnectionSchema.CONF_KNX_RATE_LIMIT: 25,
ConnectionSchema.CONF_KNX_STATE_UPDATER: False,
ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112",
},
)
@ -626,4 +683,5 @@ async def test_advanced_options(
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
ConnectionSchema.CONF_KNX_RATE_LIMIT: 25,
ConnectionSchema.CONF_KNX_STATE_UPDATER: False,
ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112",
}

View File

@ -0,0 +1,78 @@
"""Test KNX init."""
import pytest
from xknx import XKNX
from xknx.io import ConnectionConfig, ConnectionType
from homeassistant.components.knx.const import (
CONF_KNX_AUTOMATIC,
CONF_KNX_CONNECTION_TYPE,
CONF_KNX_INDIVIDUAL_ADDRESS,
CONF_KNX_ROUTING,
CONF_KNX_TUNNELING,
DOMAIN as KNX_DOMAIN,
)
from homeassistant.components.knx.schema import ConnectionSchema
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.core import HomeAssistant
from .conftest import KNXTestKit
from tests.common import MockConfigEntry
@pytest.mark.parametrize(
"config_entry_data,connection_config",
[
(
{
CONF_KNX_INDIVIDUAL_ADDRESS: XKNX.DEFAULT_ADDRESS,
CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC,
},
ConnectionConfig(),
),
(
{
CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING,
ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.1",
},
ConnectionConfig(
connection_type=ConnectionType.ROUTING, local_ip="192.168.1.1"
),
),
(
{
CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING,
CONF_HOST: "192.168.0.2",
CONF_PORT: 3675,
ConnectionSchema.CONF_KNX_ROUTE_BACK: False,
ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112",
},
ConnectionConfig(
connection_type=ConnectionType.TUNNELING,
route_back=False,
gateway_ip="192.168.0.2",
gateway_port=3675,
local_ip="192.168.1.112",
auto_reconnect=True,
),
),
],
)
async def test_init_connection_handling(
hass: HomeAssistant, knx: KNXTestKit, config_entry_data, connection_config
):
"""Test correctly generating connection config."""
config_entry = MockConfigEntry(
title="KNX",
domain=KNX_DOMAIN,
data=config_entry_data,
)
knx.mock_config_entry = config_entry
await knx.setup_integration({})
assert hass.data.get(KNX_DOMAIN) is not None
assert (
hass.data[KNX_DOMAIN].connection_config().__dict__ == connection_config.__dict__
)

View File

@ -39,13 +39,12 @@ async def async_setup_devices(hass, device_type, traits={}):
return await async_setup_sdm_platform(hass, PLATFORM, devices=devices)
def create_device_traits(event_trait):
def create_device_traits(event_traits=[]):
"""Create fake traits for a device."""
return {
result = {
"sdm.devices.traits.Info": {
"customName": "Front",
},
event_trait: {},
"sdm.devices.traits.CameraLiveStream": {
"maxVideoResolution": {
"width": 640,
@ -55,6 +54,8 @@ def create_device_traits(event_trait):
"audioCodecs": ["AAC"],
},
}
result.update({t: {} for t in event_traits})
return result
def create_event(event_type, device_id=DEVICE_ID, timestamp=None):
@ -91,7 +92,7 @@ async def test_doorbell_chime_event(hass):
subscriber = await async_setup_devices(
hass,
"sdm.devices.types.DOORBELL",
create_device_traits("sdm.devices.traits.DoorbellChime"),
create_device_traits(["sdm.devices.traits.DoorbellChime"]),
)
registry = er.async_get(hass)
@ -129,7 +130,7 @@ async def test_camera_motion_event(hass):
subscriber = await async_setup_devices(
hass,
"sdm.devices.types.CAMERA",
create_device_traits("sdm.devices.traits.CameraMotion"),
create_device_traits(["sdm.devices.traits.CameraMotion"]),
)
registry = er.async_get(hass)
entry = registry.async_get("camera.front")
@ -157,7 +158,7 @@ async def test_camera_sound_event(hass):
subscriber = await async_setup_devices(
hass,
"sdm.devices.types.CAMERA",
create_device_traits("sdm.devices.traits.CameraSound"),
create_device_traits(["sdm.devices.traits.CameraSound"]),
)
registry = er.async_get(hass)
entry = registry.async_get("camera.front")
@ -185,7 +186,7 @@ async def test_camera_person_event(hass):
subscriber = await async_setup_devices(
hass,
"sdm.devices.types.DOORBELL",
create_device_traits("sdm.devices.traits.CameraEventImage"),
create_device_traits(["sdm.devices.traits.CameraPerson"]),
)
registry = er.async_get(hass)
entry = registry.async_get("camera.front")
@ -213,7 +214,9 @@ async def test_camera_multiple_event(hass):
subscriber = await async_setup_devices(
hass,
"sdm.devices.types.DOORBELL",
create_device_traits("sdm.devices.traits.CameraEventImage"),
create_device_traits(
["sdm.devices.traits.CameraMotion", "sdm.devices.traits.CameraPerson"]
),
)
registry = er.async_get(hass)
entry = registry.async_get("camera.front")
@ -256,7 +259,7 @@ async def test_unknown_event(hass):
subscriber = await async_setup_devices(
hass,
"sdm.devices.types.DOORBELL",
create_device_traits("sdm.devices.traits.DoorbellChime"),
create_device_traits(["sdm.devices.traits.DoorbellChime"]),
)
await subscriber.async_receive_event(create_event("some-event-id"))
await hass.async_block_till_done()
@ -270,7 +273,7 @@ async def test_unknown_device_id(hass):
subscriber = await async_setup_devices(
hass,
"sdm.devices.types.DOORBELL",
create_device_traits("sdm.devices.traits.DoorbellChime"),
create_device_traits(["sdm.devices.traits.DoorbellChime"]),
)
await subscriber.async_receive_event(
create_event("sdm.devices.events.DoorbellChime.Chime", "invalid-device-id")
@ -286,7 +289,7 @@ async def test_event_message_without_device_event(hass):
subscriber = await async_setup_devices(
hass,
"sdm.devices.types.DOORBELL",
create_device_traits("sdm.devices.traits.DoorbellChime"),
create_device_traits(["sdm.devices.traits.DoorbellChime"]),
)
timestamp = utcnow()
event = EventMessage(
@ -308,14 +311,12 @@ async def test_doorbell_event_thread(hass):
subscriber = await async_setup_devices(
hass,
"sdm.devices.types.DOORBELL",
traits={
"sdm.devices.traits.Info": {
"customName": "Front",
},
"sdm.devices.traits.CameraLiveStream": {},
"sdm.devices.traits.CameraClipPreview": {},
"sdm.devices.traits.CameraPerson": {},
},
create_device_traits(
[
"sdm.devices.traits.CameraClipPreview",
"sdm.devices.traits.CameraPerson",
]
),
)
registry = er.async_get(hass)
entry = registry.async_get("camera.front")
@ -351,7 +352,7 @@ async def test_doorbell_event_thread(hass):
)
await subscriber.async_receive_event(EventMessage(message_data_1, auth=None))
# Publish message #1 that sends a no-op update to end the event thread
# Publish message #2 that sends a no-op update to end the event thread
timestamp2 = timestamp1 + datetime.timedelta(seconds=1)
message_data_2 = event_message_data.copy()
message_data_2.update(
@ -371,3 +372,77 @@ async def test_doorbell_event_thread(hass):
"timestamp": timestamp1.replace(microsecond=0),
"nest_event_id": EVENT_SESSION_ID,
}
async def test_doorbell_event_session_update(hass):
"""Test a pubsub message with updates to an existing session."""
events = async_capture_events(hass, NEST_EVENT)
subscriber = await async_setup_devices(
hass,
"sdm.devices.types.DOORBELL",
create_device_traits(
[
"sdm.devices.traits.CameraClipPreview",
"sdm.devices.traits.CameraPerson",
"sdm.devices.traits.CameraMotion",
]
),
)
registry = er.async_get(hass)
entry = registry.async_get("camera.front")
assert entry is not None
# Message #1 has a motion event
timestamp1 = utcnow()
await subscriber.async_receive_event(
create_events(
{
"sdm.devices.events.CameraMotion.Motion": {
"eventSessionId": EVENT_SESSION_ID,
"eventId": "n:1",
},
"sdm.devices.events.CameraClipPreview.ClipPreview": {
"eventSessionId": EVENT_SESSION_ID,
"previewUrl": "image-url-1",
},
},
timestamp=timestamp1,
)
)
# Message #2 has an extra person event
timestamp2 = utcnow()
await subscriber.async_receive_event(
create_events(
{
"sdm.devices.events.CameraMotion.Motion": {
"eventSessionId": EVENT_SESSION_ID,
"eventId": "n:1",
},
"sdm.devices.events.CameraPerson.Person": {
"eventSessionId": EVENT_SESSION_ID,
"eventId": "n:2",
},
"sdm.devices.events.CameraClipPreview.ClipPreview": {
"eventSessionId": EVENT_SESSION_ID,
"previewUrl": "image-url-1",
},
},
timestamp=timestamp2,
)
)
await hass.async_block_till_done()
assert len(events) == 2
assert events[0].data == {
"device_id": entry.device_id,
"type": "camera_motion",
"timestamp": timestamp1.replace(microsecond=0),
"nest_event_id": EVENT_SESSION_ID,
}
assert events[1].data == {
"device_id": entry.device_id,
"type": "camera_person",
"timestamp": timestamp2.replace(microsecond=0),
"nest_event_id": EVENT_SESSION_ID,
}

View File

@ -16,6 +16,7 @@ from homeassistant.components import media_source
from homeassistant.components.media_player.errors import BrowseError
from homeassistant.components.media_source import const
from homeassistant.components.media_source.error import Unresolvable
from homeassistant.config_entries import ConfigEntryState
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.template import DATE_STR_FORMAT
import homeassistant.util.dt as dt_util
@ -164,6 +165,37 @@ async def test_supported_device(hass, auth):
assert len(browse.children) == 0
async def test_integration_unloaded(hass, auth):
"""Test the media player loads, but has no devices, when config unloaded."""
await async_setup_devices(
hass,
auth,
CAMERA_DEVICE_TYPE,
CAMERA_TRAITS,
)
browse = await media_source.async_browse_media(hass, f"{const.URI_SCHEME}{DOMAIN}")
assert browse.domain == DOMAIN
assert browse.identifier == ""
assert browse.title == "Nest"
assert len(browse.children) == 1
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1
entry = entries[0]
assert entry.state is ConfigEntryState.LOADED
assert await hass.config_entries.async_unload(entry.entry_id)
assert entry.state == ConfigEntryState.NOT_LOADED
# No devices returned
browse = await media_source.async_browse_media(hass, f"{const.URI_SCHEME}{DOMAIN}")
assert browse.domain == DOMAIN
assert browse.identifier == ""
assert browse.title == "Nest"
assert len(browse.children) == 0
async def test_camera_event(hass, auth, hass_client):
"""Test a media source and image created for an event."""
event_timestamp = dt_util.now()