Give scenes last activated state (#62673)

This commit is contained in:
Franck Nijhof 2022-01-07 19:02:32 +01:00 committed by GitHub
parent e03283292b
commit 3f7275a9c7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 93 additions and 27 deletions

View file

@ -4,7 +4,7 @@ from __future__ import annotations
import functools as ft import functools as ft
import importlib import importlib
import logging import logging
from typing import Any from typing import Any, final
import voluptuous as vol import voluptuous as vol
@ -12,14 +12,14 @@ from homeassistant.components.light import ATTR_TRANSITION
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PLATFORM, SERVICE_TURN_ON from homeassistant.const import CONF_PLATFORM, SERVICE_TURN_ON
from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from homeassistant.util import dt as dt_util
# mypy: allow-untyped-defs, no-check-untyped-defs # mypy: allow-untyped-defs, no-check-untyped-defs
DOMAIN = "scene" DOMAIN = "scene"
STATE = "scening"
STATES = "states" STATES = "states"
@ -71,7 +71,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
component.async_register_entity_service( component.async_register_entity_service(
SERVICE_TURN_ON, SERVICE_TURN_ON,
{ATTR_TRANSITION: vol.All(vol.Coerce(float), vol.Clamp(min=0, max=6553))}, {ATTR_TRANSITION: vol.All(vol.Coerce(float), vol.Clamp(min=0, max=6553))},
"async_activate", "_async_activate",
) )
return True return True
@ -89,18 +89,35 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return await component.async_unload_entry(entry) return await component.async_unload_entry(entry)
class Scene(Entity): class Scene(RestoreEntity):
"""A scene is a group of entities and the states we want them to be.""" """A scene is a group of entities and the states we want them to be."""
@property _attr_should_poll = False
def should_poll(self) -> bool: __last_activated: str | None = None
"""No polling needed."""
return False
@property @property
@final
def state(self) -> str | None: def state(self) -> str | None:
"""Return the state of the scene.""" """Return the state of the scene."""
return STATE if self.__last_activated is None:
return None
return self.__last_activated
@final
async def _async_activate(self, **kwargs: Any) -> None:
"""Activate scene.
Should not be overridden, handle setting last press timestamp.
"""
self.__last_activated = dt_util.utcnow().isoformat()
self.async_write_ha_state()
await self.async_activate(**kwargs)
async def async_added_to_hass(self) -> None:
"""Call when the button is added to hass."""
state = await self.async_get_last_state()
if state is not None and state.state is not None:
self.__last_activated = state.state
def activate(self, **kwargs: Any) -> None: def activate(self, **kwargs: Any) -> None:
"""Activate scene. Try to get entities into requested state.""" """Activate scene. Try to get entities into requested state."""

View file

@ -800,7 +800,7 @@ async def test_scene_scene(hass):
assert helpers.get_google_type(scene.DOMAIN, None) is not None assert helpers.get_google_type(scene.DOMAIN, None) is not None
assert trait.SceneTrait.supported(scene.DOMAIN, 0, None, None) assert trait.SceneTrait.supported(scene.DOMAIN, 0, None, None)
trt = trait.SceneTrait(hass, State("scene.bla", scene.STATE), BASIC_CONFIG) trt = trait.SceneTrait(hass, State("scene.bla", STATE_UNKNOWN), BASIC_CONFIG)
assert trt.sync_attributes() == {} assert trt.sync_attributes() == {}
assert trt.query_attributes() == {} assert trt.query_attributes() == {}
assert trt.can_execute(trait.COMMAND_ACTIVATE_SCENE, {}) assert trt.can_execute(trait.COMMAND_ACTIVATE_SCENE, {})

View file

@ -6,6 +6,7 @@ import voluptuous as vol
from homeassistant.components.homeassistant import scene as ha_scene from homeassistant.components.homeassistant import scene as ha_scene
from homeassistant.components.homeassistant.scene import EVENT_SCENE_RELOADED from homeassistant.components.homeassistant.scene import EVENT_SCENE_RELOADED
from homeassistant.const import STATE_UNKNOWN
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from tests.common import async_capture_events, async_mock_service from tests.common import async_capture_events, async_mock_service
@ -119,7 +120,7 @@ async def test_create_service(hass, caplog):
assert scene is not None assert scene is not None
assert scene.domain == "scene" assert scene.domain == "scene"
assert scene.name == "hallo" assert scene.name == "hallo"
assert scene.state == "scening" assert scene.state == STATE_UNKNOWN
assert scene.attributes.get("entity_id") == ["light.bed_light"] assert scene.attributes.get("entity_id") == ["light.bed_light"]
assert await hass.services.async_call( assert await hass.services.async_call(
@ -137,7 +138,7 @@ async def test_create_service(hass, caplog):
assert scene is not None assert scene is not None
assert scene.domain == "scene" assert scene.domain == "scene"
assert scene.name == "hallo" assert scene.name == "hallo"
assert scene.state == "scening" assert scene.state == STATE_UNKNOWN
assert scene.attributes.get("entity_id") == ["light.kitchen_light"] assert scene.attributes.get("entity_id") == ["light.kitchen_light"]
assert await hass.services.async_call( assert await hass.services.async_call(
@ -156,7 +157,7 @@ async def test_create_service(hass, caplog):
assert scene is not None assert scene is not None
assert scene.domain == "scene" assert scene.domain == "scene"
assert scene.name == "hallo_2" assert scene.name == "hallo_2"
assert scene.state == "scening" assert scene.state == STATE_UNKNOWN
assert scene.attributes.get("entity_id") == ["light.kitchen"] assert scene.attributes.get("entity_id") == ["light.kitchen"]

View file

@ -1,6 +1,7 @@
"""Philips Hue scene platform tests for V2 bridge/api.""" """Philips Hue scene platform tests for V2 bridge/api."""
from homeassistant.const import STATE_UNKNOWN
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from .conftest import setup_platform from .conftest import setup_platform
@ -21,7 +22,7 @@ async def test_scene(hass, mock_bridge_v2, v2_resources_test_data):
test_entity = hass.states.get("scene.test_zone_dynamic_test_scene") test_entity = hass.states.get("scene.test_zone_dynamic_test_scene")
assert test_entity is not None assert test_entity is not None
assert test_entity.name == "Test Zone - Dynamic Test Scene" assert test_entity.name == "Test Zone - Dynamic Test Scene"
assert test_entity.state == "scening" assert test_entity.state == STATE_UNKNOWN
assert test_entity.attributes["group_name"] == "Test Zone" assert test_entity.attributes["group_name"] == "Test Zone"
assert test_entity.attributes["group_type"] == "zone" assert test_entity.attributes["group_type"] == "zone"
assert test_entity.attributes["name"] == "Dynamic Test Scene" assert test_entity.attributes["name"] == "Dynamic Test Scene"
@ -33,7 +34,7 @@ async def test_scene(hass, mock_bridge_v2, v2_resources_test_data):
test_entity = hass.states.get("scene.test_room_regular_test_scene") test_entity = hass.states.get("scene.test_room_regular_test_scene")
assert test_entity is not None assert test_entity is not None
assert test_entity.name == "Test Room - Regular Test Scene" assert test_entity.name == "Test Room - Regular Test Scene"
assert test_entity.state == "scening" assert test_entity.state == STATE_UNKNOWN
assert test_entity.attributes["group_name"] == "Test Room" assert test_entity.attributes["group_name"] == "Test Room"
assert test_entity.attributes["group_type"] == "room" assert test_entity.attributes["group_type"] == "room"
assert test_entity.attributes["name"] == "Regular Test Scene" assert test_entity.attributes["name"] == "Regular Test Scene"
@ -142,7 +143,7 @@ async def test_scene_updates(hass, mock_bridge_v2, v2_resources_test_data):
# the entity should now be available # the entity should now be available
test_entity = hass.states.get(test_entity_id) test_entity = hass.states.get(test_entity_id)
assert test_entity is not None assert test_entity is not None
assert test_entity.state == "scening" assert test_entity.state == STATE_UNKNOWN
assert test_entity.name == "Test Room - Mocked Scene" assert test_entity.name == "Test Room - Mocked Scene"
assert test_entity.attributes["brightness"] == 65.0 assert test_entity.attributes["brightness"] == 65.0

View file

@ -5,7 +5,7 @@ from unittest.mock import patch
import pytest import pytest
from homeassistant.components import scene from homeassistant.components import scene
from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON, STATE_UNKNOWN
import homeassistant.core as ha import homeassistant.core as ha
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
@ -34,7 +34,7 @@ DEFAULT_CONFIG = {
async def test_sending_mqtt_commands(hass, mqtt_mock): async def test_sending_mqtt_commands(hass, mqtt_mock):
"""Test the sending MQTT commands.""" """Test the sending MQTT commands."""
fake_state = ha.State("scene.test", scene.STATE) fake_state = ha.State("scene.test", STATE_UNKNOWN)
with patch( with patch(
"homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state",
@ -55,7 +55,7 @@ async def test_sending_mqtt_commands(hass, mqtt_mock):
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get("scene.test") state = hass.states.get("scene.test")
assert state.state == scene.STATE assert state.state == STATE_UNKNOWN
data = {ATTR_ENTITY_ID: "scene.test"} data = {ATTR_ENTITY_ID: "scene.test"}
await hass.services.async_call(scene.DOMAIN, SERVICE_TURN_ON, data, blocking=True) await hass.services.async_call(scene.DOMAIN, SERVICE_TURN_ON, data, blocking=True)

View file

@ -1,14 +1,22 @@
"""The tests for the Scene component.""" """The tests for the Scene component."""
import io import io
from unittest.mock import patch
import pytest import pytest
from homeassistant.components import light, scene from homeassistant.components import light, scene
from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, SERVICE_TURN_ON from homeassistant.const import (
ATTR_ENTITY_ID,
ENTITY_MATCH_ALL,
SERVICE_TURN_ON,
STATE_UNKNOWN,
)
from homeassistant.core import State
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util
from homeassistant.util.yaml import loader as yaml_loader from homeassistant.util.yaml import loader as yaml_loader
from tests.common import async_mock_service from tests.common import async_mock_service, mock_restore_cache
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
@ -111,7 +119,14 @@ async def test_activate_scene(hass, entities, enable_custom_integrations):
}, },
) )
await hass.async_block_till_done() await hass.async_block_till_done()
await activate(hass, "scene.test")
assert hass.states.get("scene.test").state == STATE_UNKNOWN
now = dt_util.utcnow()
with patch("homeassistant.core.dt_util.utcnow", return_value=now):
await activate(hass, "scene.test")
assert hass.states.get("scene.test").state == now.isoformat()
assert light.is_on(hass, light_1.entity_id) assert light.is_on(hass, light_1.entity_id)
assert light.is_on(hass, light_2.entity_id) assert light.is_on(hass, light_2.entity_id)
@ -121,10 +136,14 @@ async def test_activate_scene(hass, entities, enable_custom_integrations):
calls = async_mock_service(hass, "light", "turn_on") calls = async_mock_service(hass, "light", "turn_on")
await hass.services.async_call( now = dt_util.utcnow()
scene.DOMAIN, "turn_on", {"transition": 42, "entity_id": "scene.test"} with patch("homeassistant.core.dt_util.utcnow", return_value=now):
) await hass.services.async_call(
await hass.async_block_till_done() scene.DOMAIN, "turn_on", {"transition": 42, "entity_id": "scene.test"}
)
await hass.async_block_till_done()
assert hass.states.get("scene.test").state == now.isoformat()
assert len(calls) == 1 assert len(calls) == 1
assert calls[0].domain == "light" assert calls[0].domain == "light"
@ -132,6 +151,32 @@ async def test_activate_scene(hass, entities, enable_custom_integrations):
assert calls[0].data.get("transition") == 42 assert calls[0].data.get("transition") == 42
async def test_restore_state(hass, entities, enable_custom_integrations):
"""Test we restore state integration."""
mock_restore_cache(hass, (State("scene.test", "2021-01-01T23:59:59+00:00"),))
light_1, light_2 = await setup_lights(hass, entities)
assert await async_setup_component(
hass,
scene.DOMAIN,
{
"scene": [
{
"name": "test",
"entities": {
light_1.entity_id: "on",
light_2.entity_id: "on",
},
}
]
},
)
await hass.async_block_till_done()
assert hass.states.get("scene.test").state == "2021-01-01T23:59:59+00:00"
async def activate(hass, entity_id=ENTITY_MATCH_ALL): async def activate(hass, entity_id=ENTITY_MATCH_ALL):
"""Activate a scene.""" """Activate a scene."""
data = {} data = {}

View file

@ -289,6 +289,8 @@ def scene_factory_fixture(location):
scene = Mock(SceneEntity) scene = Mock(SceneEntity)
scene.scene_id = str(uuid4()) scene.scene_id = str(uuid4())
scene.name = name scene.name = name
scene.icon = None
scene.color = None
scene.location_id = location.location_id scene.location_id = location.location_id
return scene return scene