Give scenes last activated state (#62673)
This commit is contained in:
parent
e03283292b
commit
3f7275a9c7
7 changed files with 93 additions and 27 deletions
|
@ -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."""
|
||||||
|
|
|
@ -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, {})
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 = {}
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue