Merge pull request #61625 from home-assistant/rc

This commit is contained in:
Paulus Schoutsen 2021-12-12 15:39:42 -08:00 committed by GitHub
commit 5df747276f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 215 additions and 96 deletions

View File

@ -2,7 +2,9 @@
"domain": "frontend",
"name": "Home Assistant Frontend",
"documentation": "https://www.home-assistant.io/integrations/frontend",
"requirements": ["home-assistant-frontend==20211211.0"],
"requirements": [
"home-assistant-frontend==20211212.0"
],
"dependencies": [
"api",
"auth",
@ -15,6 +17,8 @@
"system_log",
"websocket_api"
],
"codeowners": ["@home-assistant/frontend"],
"codeowners": [
"@home-assistant/frontend"
],
"quality_scale": "internal"
}

View File

@ -249,14 +249,17 @@ class OpeningDeviceBase(HomeAccessory):
def async_update_state(self, new_state):
"""Update cover position and tilt after state changed."""
# update tilt
if not self._supports_tilt:
return
current_tilt = new_state.attributes.get(ATTR_CURRENT_TILT_POSITION)
if isinstance(current_tilt, (float, int)):
# HomeKit sends values between -90 and 90.
# We'll have to normalize to [0,100]
current_tilt = (current_tilt / 100.0 * 180.0) - 90.0
current_tilt = int(current_tilt)
self.char_current_tilt.set_value(current_tilt)
self.char_target_tilt.set_value(current_tilt)
if not isinstance(current_tilt, (float, int)):
return
# HomeKit sends values between -90 and 90.
# We'll have to normalize to [0,100]
current_tilt = (current_tilt / 100.0 * 180.0) - 90.0
current_tilt = int(current_tilt)
self.char_current_tilt.set_value(current_tilt)
self.char_target_tilt.set_value(current_tilt)
class OpeningDevice(OpeningDeviceBase, HomeAccessory):

View File

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

View File

@ -96,8 +96,8 @@ class HueSceneEntity(HueBaseEntity, SceneEntity):
"""Activate Hue scene."""
transition = kwargs.get("transition")
if transition is not None:
# hue transition duration is in steps of 100 ms
transition = int(transition * 100)
# hue transition duration is in milliseconds
transition = int(transition * 1000)
dynamic = kwargs.get("dynamic", self.is_dynamic)
await self.bridge.async_request_call(
self.controller.recall,

View File

@ -103,6 +103,9 @@ class HueBaseEntity(Entity):
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
return True
if zigbee := self.bridge.api.devices.get_zigbee_connectivity(self.device.id):
# all device-attached entities get availability from the zigbee connectivity
return zigbee.status == ConnectivityServiceStatus.CONNECTED

View File

@ -24,7 +24,7 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from ..bridge import HueBridge
from ..const import DOMAIN
from ..const import CONF_ALLOW_HUE_GROUPS, DOMAIN
from .entity import HueBaseEntity
ALLOWED_ERRORS = [
@ -76,8 +76,6 @@ async def async_setup_entry(
class GroupedHueLight(HueBaseEntity, LightEntity):
"""Representation of a Grouped Hue light."""
# Entities for Hue groups are disabled by default
_attr_entity_registry_enabled_default = False
_attr_icon = "mdi:lightbulb-group"
def __init__(
@ -92,6 +90,12 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
self.api: HueBridgeV2 = bridge.api
self._attr_supported_features |= SUPPORT_TRANSITION
# Entities for Hue groups are disabled by default
# unless they were enabled in old version (legacy option)
self._attr_entity_registry_enabled_default = bridge.config_entry.data.get(
CONF_ALLOW_HUE_GROUPS, False
)
self._update_values()
async def async_added_to_hass(self) -> None:
@ -146,8 +150,8 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
# Hue uses a range of [0, 100] to control brightness.
brightness = float((brightness / 255) * 100)
if transition is not None:
# hue transition duration is in steps of 100 ms
transition = int(transition * 100)
# 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

View File

@ -158,8 +158,8 @@ class HueLight(HueBaseEntity, LightEntity):
# Hue uses a range of [0, 100] to control brightness.
brightness = float((brightness / 255) * 100)
if transition is not None:
# hue transition duration is in steps of 100 ms
transition = int(transition * 100)
# hue transition duration is in milliseconds
transition = int(transition * 1000)
await self.bridge.async_request_call(
self.controller.set_state,
@ -176,8 +176,8 @@ class HueLight(HueBaseEntity, LightEntity):
"""Turn the light off."""
transition = kwargs.get(ATTR_TRANSITION)
if transition is not None:
# hue transition duration is in steps of 100 ms
transition = int(transition * 100)
# hue transition duration is in milliseconds
transition = int(transition * 1000)
await self.bridge.async_request_call(
self.controller.set_state,
id=self.resource.id,

View File

@ -2,7 +2,7 @@
"domain": "hunterdouglas_powerview",
"name": "Hunter Douglas PowerView",
"documentation": "https://www.home-assistant.io/integrations/hunterdouglas_powerview",
"requirements": ["aiopvapi==1.6.14"],
"requirements": ["aiopvapi==1.6.19"],
"codeowners": ["@bdraco"],
"config_flow": true,
"homekit": {

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.5"],
"requirements": ["python-nest==4.1.0", "google-nest-sdm==0.4.6"],
"codeowners": ["@allenporter"],
"quality_scale": "platinum",
"dhcp": [

View File

@ -24,7 +24,7 @@ import logging
from google_nest_sdm.camera_traits import CameraClipPreviewTrait, CameraEventImageTrait
from google_nest_sdm.device import Device
from google_nest_sdm.event import ImageEventBase
from google_nest_sdm.event import EventImageType, ImageEventBase
from homeassistant.components.media_player.const import (
MEDIA_CLASS_DIRECTORY,
@ -253,7 +253,7 @@ def _browse_event(
event_name=MEDIA_SOURCE_EVENT_TITLE_MAP.get(event.event_type, "Event"),
event_time=dt_util.as_local(event.timestamp).strftime(DATE_STR_FORMAT),
),
can_play=True,
can_play=(event.event_image_type == EventImageType.CLIP_PREVIEW),
can_expand=False,
thumbnail=None,
children=[],

View File

@ -4,16 +4,11 @@ from __future__ import annotations
from dataclasses import dataclass
from homeassistant.components.sensor import (
STATE_CLASS_MEASUREMENT,
STATE_CLASS_TOTAL,
SensorDeviceClass,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import (
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_POWER,
DEVICE_CLASS_POWER_FACTOR,
DEVICE_CLASS_TIMESTAMP,
DEVICE_CLASS_VOLTAGE,
ELECTRIC_POTENTIAL_VOLT,
ENERGY_KILO_WATT_HOUR,
PERCENTAGE,
@ -38,35 +33,35 @@ SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = (
SolarLogSensorEntityDescription(
key="time",
name="last update",
device_class=DEVICE_CLASS_TIMESTAMP,
device_class=SensorDeviceClass.TIMESTAMP,
),
SolarLogSensorEntityDescription(
key="power_ac",
name="power AC",
icon="mdi:solar-power",
native_unit_of_measurement=POWER_WATT,
state_class=STATE_CLASS_MEASUREMENT,
state_class=SensorStateClass.MEASUREMENT,
),
SolarLogSensorEntityDescription(
key="power_dc",
name="power DC",
icon="mdi:solar-power",
native_unit_of_measurement=POWER_WATT,
state_class=STATE_CLASS_MEASUREMENT,
state_class=SensorStateClass.MEASUREMENT,
),
SolarLogSensorEntityDescription(
key="voltage_ac",
name="voltage AC",
native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
device_class=DEVICE_CLASS_VOLTAGE,
state_class=STATE_CLASS_MEASUREMENT,
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
SolarLogSensorEntityDescription(
key="voltage_dc",
name="voltage DC",
native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
device_class=DEVICE_CLASS_VOLTAGE,
state_class=STATE_CLASS_MEASUREMENT,
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
SolarLogSensorEntityDescription(
key="yield_day",
@ -101,50 +96,50 @@ SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = (
name="yield total",
icon="mdi:solar-power",
native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
state_class=STATE_CLASS_TOTAL,
state_class=SensorStateClass.TOTAL,
factor=0.001,
),
SolarLogSensorEntityDescription(
key="consumption_ac",
name="consumption AC",
native_unit_of_measurement=POWER_WATT,
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
SolarLogSensorEntityDescription(
key="consumption_day",
name="consumption day",
native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
device_class=DEVICE_CLASS_ENERGY,
device_class=SensorDeviceClass.ENERGY,
factor=0.001,
),
SolarLogSensorEntityDescription(
key="consumption_yesterday",
name="consumption yesterday",
native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
device_class=DEVICE_CLASS_ENERGY,
device_class=SensorDeviceClass.ENERGY,
factor=0.001,
),
SolarLogSensorEntityDescription(
key="consumption_month",
name="consumption month",
native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
device_class=DEVICE_CLASS_ENERGY,
device_class=SensorDeviceClass.ENERGY,
factor=0.001,
),
SolarLogSensorEntityDescription(
key="consumption_year",
name="consumption year",
native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
device_class=DEVICE_CLASS_ENERGY,
device_class=SensorDeviceClass.ENERGY,
factor=0.001,
),
SolarLogSensorEntityDescription(
key="consumption_total",
name="consumption total",
native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL,
factor=0.001,
),
SolarLogSensorEntityDescription(
@ -152,31 +147,31 @@ SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = (
name="installed peak power",
icon="mdi:solar-power",
native_unit_of_measurement=POWER_WATT,
device_class=DEVICE_CLASS_POWER,
device_class=SensorDeviceClass.POWER,
),
SolarLogSensorEntityDescription(
key="alternator_loss",
name="alternator loss",
icon="mdi:solar-power",
native_unit_of_measurement=POWER_WATT,
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
SolarLogSensorEntityDescription(
key="capacity",
name="capacity",
icon="mdi:solar-power",
native_unit_of_measurement=PERCENTAGE,
device_class=DEVICE_CLASS_POWER_FACTOR,
state_class=STATE_CLASS_MEASUREMENT,
device_class=SensorDeviceClass.POWER_FACTOR,
state_class=SensorStateClass.MEASUREMENT,
factor=100,
),
SolarLogSensorEntityDescription(
key="efficiency",
name="efficiency",
native_unit_of_measurement=PERCENTAGE,
device_class=DEVICE_CLASS_POWER_FACTOR,
state_class=STATE_CLASS_MEASUREMENT,
device_class=SensorDeviceClass.POWER_FACTOR,
state_class=SensorStateClass.MEASUREMENT,
factor=100,
),
SolarLogSensorEntityDescription(
@ -184,15 +179,15 @@ SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = (
name="power available",
icon="mdi:solar-power",
native_unit_of_measurement=POWER_WATT,
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
SolarLogSensorEntityDescription(
key="usage",
name="usage",
native_unit_of_measurement=PERCENTAGE,
device_class=DEVICE_CLASS_POWER_FACTOR,
state_class=STATE_CLASS_MEASUREMENT,
device_class=SensorDeviceClass.POWER_FACTOR,
state_class=SensorStateClass.MEASUREMENT,
factor=100,
),
)

View File

@ -1,7 +1,8 @@
"""Platform for solarlog sensors."""
from homeassistant.components.sensor import SensorEntity
from homeassistant.helpers import update_coordinator
from homeassistant.helpers.entity import DeviceInfo, StateType
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.util.dt import as_local
from . import SolarlogData
from .const import DOMAIN, SENSOR_TYPES, SolarLogSensorEntityDescription
@ -38,11 +39,16 @@ class SolarlogSensor(update_coordinator.CoordinatorEntity, SensorEntity):
)
@property
def native_value(self) -> StateType:
def native_value(self):
"""Return the native sensor value."""
result = getattr(self.coordinator.data, self.entity_description.key)
if self.entity_description.factor:
state = round(result * self.entity_description.factor, 3)
if self.entity_description.key == "time":
state = as_local(
getattr(self.coordinator.data, self.entity_description.key)
)
else:
state = result
result = getattr(self.coordinator.data, self.entity_description.key)
if self.entity_description.factor:
state = round(result * self.entity_description.factor, 3)
else:
state = result
return state

View File

@ -193,7 +193,7 @@ class SonosSpeaker:
self.volume: int | None = None
self.muted: bool | None = None
self.night_mode: bool | None = None
self.dialog_mode: bool | None = None
self.dialog_level: bool | None = None
self.cross_fade: bool | None = None
self.bass: int | None = None
self.treble: int | None = None
@ -498,17 +498,18 @@ class SonosSpeaker:
if "mute" in variables:
self.muted = variables["mute"]["Master"] == "1"
if "night_mode" in variables:
self.night_mode = variables["night_mode"] == "1"
for bool_var in (
"dialog_level",
"night_mode",
"sub_enabled",
"surround_enabled",
):
if bool_var in variables:
setattr(self, bool_var, variables[bool_var] == "1")
if "dialog_level" in variables:
self.dialog_mode = variables["dialog_level"] == "1"
if "bass" in variables:
self.bass = variables["bass"]
if "treble" in variables:
self.treble = variables["treble"]
for int_var in ("bass", "treble"):
if int_var in variables:
setattr(self, int_var, variables[int_var])
self.async_write_entity_states()
@ -982,7 +983,7 @@ class SonosSpeaker:
self.volume = self.soco.volume
self.muted = self.soco.mute
self.night_mode = self.soco.night_mode
self.dialog_mode = self.soco.dialog_mode
self.dialog_level = self.soco.dialog_mode
self.bass = self.soco.bass
self.treble = self.soco.treble

View File

@ -7,7 +7,7 @@ from homeassistant.backports.enum import StrEnum
MAJOR_VERSION: Final = 2021
MINOR_VERSION: Final = 12
PATCH_VERSION: Final = "0"
PATCH_VERSION: Final = "1"
__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==20211211.0
home-assistant-frontend==20211212.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.2
aiohue==3.0.3
# homeassistant.components.imap
aioimaplib==0.9.0
@ -231,7 +231,7 @@ aionotion==3.0.2
aiopulse==0.4.3
# homeassistant.components.hunterdouglas_powerview
aiopvapi==1.6.14
aiopvapi==1.6.19
# homeassistant.components.pvpc_hourly_pricing
aiopvpc==2.2.4
@ -738,7 +738,7 @@ google-cloud-pubsub==2.1.0
google-cloud-texttospeech==0.4.0
# homeassistant.components.nest
google-nest-sdm==0.4.5
google-nest-sdm==0.4.6
# 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==20211211.0
home-assistant-frontend==20211212.0
# homeassistant.components.zwave
homeassistant-pyozw==0.1.10

View File

@ -131,7 +131,7 @@ aiohomekit==0.6.4
aiohttp_cors==0.7.0
# homeassistant.components.hue
aiohue==3.0.2
aiohue==3.0.3
# homeassistant.components.apache_kafka
aiokafka==0.6.0
@ -161,7 +161,7 @@ aionotion==3.0.2
aiopulse==0.4.3
# homeassistant.components.hunterdouglas_powerview
aiopvapi==1.6.14
aiopvapi==1.6.19
# homeassistant.components.pvpc_hourly_pricing
aiopvpc==2.2.4
@ -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.5
google-nest-sdm==0.4.6
# 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==20211211.0
home-assistant-frontend==20211212.0
# homeassistant.components.zwave
homeassistant-pyozw==0.1.10

View File

@ -223,11 +223,15 @@ async def test_windowcovering_set_cover_position(hass, hk_driver, events):
assert events[-1].data[ATTR_VALUE] == 75
async def test_window_instantiate(hass, hk_driver, events):
"""Test if Window accessory is instantiated correctly."""
async def test_window_instantiate_set_position(hass, hk_driver, events):
"""Test if Window accessory is instantiated correctly and can set position."""
entity_id = "cover.window"
hass.states.async_set(entity_id, None)
hass.states.async_set(
entity_id,
STATE_OPEN,
{ATTR_SUPPORTED_FEATURES: SUPPORT_SET_POSITION, ATTR_CURRENT_POSITION: 0},
)
await hass.async_block_till_done()
acc = Window(hass, hk_driver, "Window", entity_id, 2, None)
await acc.run()
@ -239,6 +243,29 @@ async def test_window_instantiate(hass, hk_driver, events):
assert acc.char_current_position.value == 0
assert acc.char_target_position.value == 0
hass.states.async_set(
entity_id,
STATE_OPEN,
{ATTR_SUPPORTED_FEATURES: SUPPORT_SET_POSITION, ATTR_CURRENT_POSITION: 50},
)
await hass.async_block_till_done()
assert acc.char_current_position.value == 50
assert acc.char_target_position.value == 50
assert acc.char_position_state.value == 2
hass.states.async_set(
entity_id,
STATE_OPEN,
{
ATTR_SUPPORTED_FEATURES: SUPPORT_SET_POSITION,
ATTR_CURRENT_POSITION: "GARBAGE",
},
)
await hass.async_block_till_done()
assert acc.char_current_position.value == 50
assert acc.char_target_position.value == 50
assert acc.char_position_state.value == 2
async def test_windowcovering_cover_set_tilt(hass, hk_driver, events):
"""Test if accessory and HA update slat tilt accordingly."""

View File

@ -119,7 +119,7 @@ async def test_light_turn_on_service(hass, mock_bridge_v2, v2_resources_test_dat
)
assert len(mock_bridge_v2.mock_requests) == 2
assert mock_bridge_v2.mock_requests[1]["json"]["on"]["on"] is True
assert mock_bridge_v2.mock_requests[1]["json"]["dynamics"]["duration"] == 600
assert mock_bridge_v2.mock_requests[1]["json"]["dynamics"]["duration"] == 6000
async def test_light_turn_off_service(hass, mock_bridge_v2, v2_resources_test_data):
@ -164,7 +164,7 @@ async def test_light_turn_off_service(hass, mock_bridge_v2, v2_resources_test_da
)
assert len(mock_bridge_v2.mock_requests) == 2
assert mock_bridge_v2.mock_requests[1]["json"]["on"]["on"] is False
assert mock_bridge_v2.mock_requests[1]["json"]["dynamics"]["duration"] == 600
assert mock_bridge_v2.mock_requests[1]["json"]["dynamics"]["duration"] == 6000
async def test_light_added(hass, mock_bridge_v2):

View File

@ -83,7 +83,7 @@ async def test_scene_turn_on_service(hass, mock_bridge_v2, v2_resources_test_dat
assert len(mock_bridge_v2.mock_requests) == 2
assert mock_bridge_v2.mock_requests[1]["json"]["recall"] == {
"action": "active",
"duration": 600,
"duration": 6000,
}

View File

@ -62,7 +62,7 @@ class FakeSubscriber(GoogleNestSubscriber):
def set_update_callback(self, callback: Callable[[EventMessage], Awaitable[None]]):
"""Capture the callback set by Home Assistant."""
self._callback = callback
self._device_manager.set_update_callback(callback)
async def create_subscription(self):
"""Create the subscription."""
@ -93,7 +93,6 @@ class FakeSubscriber(GoogleNestSubscriber):
"""Simulate a received pubsub message, invoked by tests."""
# Update device state, then invoke HomeAssistant to refresh
await self._device_manager.async_handle_event(event_message)
await self._callback(event_message)
async def async_setup_sdm_platform(

View File

@ -4,6 +4,8 @@ These tests fake out the subscriber/devicemanager, and are not using a real
pubsub subscriber.
"""
import datetime
from google_nest_sdm.device import Device
from google_nest_sdm.event import EventMessage
@ -298,3 +300,74 @@ async def test_event_message_without_device_event(hass):
await hass.async_block_till_done()
assert len(events) == 0
async def test_doorbell_event_thread(hass):
"""Test a series of pubsub messages in the same thread."""
events = async_capture_events(hass, NEST_EVENT)
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": {},
},
)
registry = er.async_get(hass)
entry = registry.async_get("camera.front")
assert entry is not None
event_message_data = {
"eventId": "some-event-id-ignored",
"resourceUpdate": {
"name": DEVICE_ID,
"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",
},
},
},
"eventThreadId": "CjY5Y3VKaTZwR3o4Y19YbTVfMF...",
"resourcegroup": [DEVICE_ID],
}
# Publish message #1 that starts the event thread
timestamp1 = utcnow()
message_data_1 = event_message_data.copy()
message_data_1.update(
{
"timestamp": timestamp1.isoformat(timespec="seconds"),
"eventThreadState": "STARTED",
}
)
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
timestamp2 = timestamp1 + datetime.timedelta(seconds=1)
message_data_2 = event_message_data.copy()
message_data_2.update(
{
"timestamp": timestamp2.isoformat(timespec="seconds"),
"eventThreadState": "ENDED",
}
)
await subscriber.async_receive_event(EventMessage(message_data_2, auth=None))
await hass.async_block_till_done()
# The event is only published once
assert len(events) == 1
assert events[0].data == {
"device_id": entry.device_id,
"type": "camera_motion",
"timestamp": timestamp1.replace(microsecond=0),
"nest_event_id": EVENT_SESSION_ID,
}

View File

@ -231,6 +231,7 @@ async def test_camera_event(hass, auth, hass_client):
assert "Person" in browse.title
assert not browse.can_expand
assert not browse.children
assert not browse.can_play
# Resolving the event links to the media
media = await media_source.async_resolve_media(
@ -302,6 +303,7 @@ async def test_event_order(hass, auth):
event_timestamp_string = event_timestamp2.strftime(DATE_STR_FORMAT)
assert browse.children[0].title == f"Motion @ {event_timestamp_string}"
assert not browse.children[0].can_expand
assert not browse.can_play
# Person event is next
assert browse.children[1].domain == DOMAIN
@ -310,6 +312,7 @@ async def test_event_order(hass, auth):
event_timestamp_string = event_timestamp1.strftime(DATE_STR_FORMAT)
assert browse.children[1].title == f"Person @ {event_timestamp_string}"
assert not browse.children[1].can_expand
assert not browse.can_play
async def test_browse_invalid_device_id(hass, auth):
@ -449,6 +452,7 @@ async def test_camera_event_clip_preview(hass, auth, hass_client):
assert browse.children[0].title == f"Motion @ {event_timestamp_string}"
assert not browse.children[0].can_expand
assert len(browse.children[0].children) == 0
assert browse.children[0].can_play
# Resolving the event links to the media
media = await media_source.async_resolve_media(