diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 994ac596527..4380440408f 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -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" -} +} \ No newline at end of file diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index 0c889d9aee4..b9153ef1372 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -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): diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index c789755c9a3..ee337cd3d71 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -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", diff --git a/homeassistant/components/hue/scene.py b/homeassistant/components/hue/scene.py index 7335d2a048e..d67a3b097c7 100644 --- a/homeassistant/components/hue/scene.py +++ b/homeassistant/components/hue/scene.py @@ -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, diff --git a/homeassistant/components/hue/v2/entity.py b/homeassistant/components/hue/v2/entity.py index 6dbc959fd9c..68c427fd3a5 100644 --- a/homeassistant/components/hue/v2/entity.py +++ b/homeassistant/components/hue/v2/entity.py @@ -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 diff --git a/homeassistant/components/hue/v2/group.py b/homeassistant/components/hue/v2/group.py index 312fef6629f..08f1dc72325 100644 --- a/homeassistant/components/hue/v2/group.py +++ b/homeassistant/components/hue/v2/group.py @@ -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 diff --git a/homeassistant/components/hue/v2/light.py b/homeassistant/components/hue/v2/light.py index 42972f2242c..de5388e1220 100644 --- a/homeassistant/components/hue/v2/light.py +++ b/homeassistant/components/hue/v2/light.py @@ -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, diff --git a/homeassistant/components/hunterdouglas_powerview/manifest.json b/homeassistant/components/hunterdouglas_powerview/manifest.json index ad763a33bc8..ade3b25f31c 100644 --- a/homeassistant/components/hunterdouglas_powerview/manifest.json +++ b/homeassistant/components/hunterdouglas_powerview/manifest.json @@ -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": { diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 11a464dbaf1..507711c73ff 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -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": [ diff --git a/homeassistant/components/nest/media_source.py b/homeassistant/components/nest/media_source.py index b02b9b1870e..7c33e004b2b 100644 --- a/homeassistant/components/nest/media_source.py +++ b/homeassistant/components/nest/media_source.py @@ -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=[], diff --git a/homeassistant/components/solarlog/const.py b/homeassistant/components/solarlog/const.py index 0e9e5e8e5e0..3159bc46218 100644 --- a/homeassistant/components/solarlog/const.py +++ b/homeassistant/components/solarlog/const.py @@ -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, ), ) diff --git a/homeassistant/components/solarlog/sensor.py b/homeassistant/components/solarlog/sensor.py index 5d79efb94c9..5c4c2bfad28 100644 --- a/homeassistant/components/solarlog/sensor.py +++ b/homeassistant/components/solarlog/sensor.py @@ -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 diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index e12166d7795..30b240c9fd7 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -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 diff --git a/homeassistant/const.py b/homeassistant/const.py index f0808c28aaf..00e2b23df4e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -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) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 18c7c1befda..6c86e5804d8 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -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 diff --git a/requirements_all.txt b/requirements_all.txt index 42e3237f874..8e079958d13 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9c4def0e712..e1f9e1f7351 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py index c357598a3df..44c9365fc04 100644 --- a/tests/components/homekit/test_type_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -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.""" diff --git a/tests/components/hue/test_light_v2.py b/tests/components/hue/test_light_v2.py index 7843cab1574..362b7076a92 100644 --- a/tests/components/hue/test_light_v2.py +++ b/tests/components/hue/test_light_v2.py @@ -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): diff --git a/tests/components/hue/test_scene.py b/tests/components/hue/test_scene.py index 0f3d6255e86..1d270706c99 100644 --- a/tests/components/hue/test_scene.py +++ b/tests/components/hue/test_scene.py @@ -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, } diff --git a/tests/components/nest/common.py b/tests/components/nest/common.py index 35183a441a5..0f1d47c687e 100644 --- a/tests/components/nest/common.py +++ b/tests/components/nest/common.py @@ -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( diff --git a/tests/components/nest/test_events.py b/tests/components/nest/test_events.py index a2f5c21fdac..4767fd815d2 100644 --- a/tests/components/nest/test_events.py +++ b/tests/components/nest/test_events.py @@ -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, + } diff --git a/tests/components/nest/test_media_source.py b/tests/components/nest/test_media_source.py index 22ed0721eb2..f52b89c4f4d 100644 --- a/tests/components/nest/test_media_source.py +++ b/tests/components/nest/test_media_source.py @@ -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(