1 """Allow users to set and activate scenes."""
3 from __future__
import annotations
5 from collections.abc
import Mapping, ValuesView
7 from typing
import Any, NamedTuple, cast
9 import voluptuous
as vol
11 from homeassistant
import config
as conf_util
31 async_extract_entity_ids,
32 async_register_admin_service,
38 from .const
import DOMAIN
42 """Convert state definitions to State objects."""
43 result: dict[str, State] = {}
45 for entity_id, info
in states.items():
46 entity_id = cv.entity_id(entity_id)
48 if isinstance(info, dict):
49 entity_attrs = info.copy()
50 state = entity_attrs.pop(ATTR_STATE,
None)
51 attributes = entity_attrs
58 if isinstance(state, bool):
59 state = STATE_ON
if state
else STATE_OFF
60 elif not isinstance(state, str):
61 raise vol.Invalid(f
"State for {entity_id} should be a string")
63 result[entity_id] =
State(entity_id, state, attributes)
69 """Validate that entities and snapshot_entities do not overlap."""
71 CONF_SNAPSHOT
not in value
72 or CONF_ENTITIES
not in value
74 entity_id
not in value[CONF_SNAPSHOT]
for entity_id
in value[CONF_ENTITIES]
79 raise vol.Invalid(
"entities and snapshot_entities must not overlap")
82 CONF_SCENE_ID =
"scene_id"
83 CONF_SNAPSHOT =
"snapshot_entities"
84 DATA_PLATFORM =
"homeassistant_scene"
85 EVENT_SCENE_RELOADED =
"scene_reloaded"
86 STATES_SCHEMA = vol.All(dict, _convert_states)
89 PLATFORM_SCHEMA = vol.Schema(
91 vol.Required(CONF_PLATFORM): DOMAIN,
92 vol.Required(STATES): vol.All(
97 vol.Optional(CONF_ID): cv.string,
98 vol.Required(CONF_NAME): cv.string,
99 vol.Optional(CONF_ICON): cv.icon,
100 vol.Required(CONF_ENTITIES): STATES_SCHEMA,
101 vol.Optional(
"metadata"): dict,
107 extra=vol.ALLOW_EXTRA,
110 CREATE_SCENE_SCHEMA = vol.All(
111 cv.has_at_least_one_key(CONF_ENTITIES, CONF_SNAPSHOT),
112 _ensure_no_intersection,
115 vol.Required(CONF_SCENE_ID): cv.slug,
116 vol.Optional(CONF_ENTITIES, default={}): STATES_SCHEMA,
117 vol.Optional(CONF_SNAPSHOT, default=[]): cv.entity_ids,
122 SERVICE_APPLY =
"apply"
123 SERVICE_CREATE =
"create"
124 SERVICE_DELETE =
"delete"
126 _LOGGER = logging.getLogger(__name__)
130 """Object for storing scene config."""
135 states: dict[str, State]
140 """Return all scenes that reference the entity."""
141 if DATA_PLATFORM
not in hass.data:
144 platform: EntityPlatform = hass.data[DATA_PLATFORM]
146 scene_entities = cast(ValuesView[HomeAssistantScene], platform.entities.values())
148 scene_entity.entity_id
149 for scene_entity
in scene_entities
150 if entity_id
in scene_entity.scene_config.states
156 """Return all entities in a scene."""
157 if DATA_PLATFORM
not in hass.data:
160 platform: EntityPlatform = hass.data[DATA_PLATFORM]
162 if (entity := platform.entities.get(entity_id))
is None:
165 return list(cast(HomeAssistantScene, entity).scene_config.states)
171 async_add_entities: AddEntitiesCallback,
172 discovery_info: DiscoveryInfoType |
None =
None,
174 """Set up Home Assistant scene entries."""
178 if hass.services.has_service(SCENE_DOMAIN, SERVICE_RELOAD):
182 platform = hass.data[DATA_PLATFORM] = entity_platform.async_get_current_platform()
184 async
def reload_config(call: ServiceCall) ->
None:
185 """Reload the scene config."""
187 config = await conf_util.async_hass_config_yaml(hass)
188 except HomeAssistantError
as err:
194 conf = await conf_util.async_process_component_and_handle_errors(
195 hass, config, integration
198 if not (conf
and platform):
201 await platform.async_reset()
204 for p_type, p_config
in conf_util.config_per_platform(conf, SCENE_DOMAIN):
210 hass.bus.async_fire(EVENT_SCENE_RELOADED, context=call.context)
214 async
def apply_service(call: ServiceCall) ->
None:
216 reproduce_options = {}
218 if ATTR_TRANSITION
in call.data:
219 reproduce_options[ATTR_TRANSITION] = call.data.get(ATTR_TRANSITION)
223 call.data[CONF_ENTITIES].values(),
224 context=call.context,
225 reproduce_options=reproduce_options,
228 hass.services.async_register(
234 vol.Optional(ATTR_TRANSITION): vol.All(
235 vol.Coerce(float), vol.Clamp(min=0, max=6553)
237 vol.Required(CONF_ENTITIES): STATES_SCHEMA,
242 async
def create_service(call: ServiceCall) ->
None:
243 """Create a scene."""
244 snapshot = call.data[CONF_SNAPSHOT]
245 entities = call.data[CONF_ENTITIES]
247 for entity_id
in snapshot:
248 if (state := hass.states.get(entity_id))
is None:
250 "Entity %s does not exist and therefore cannot be snapshotted",
254 entities[entity_id] =
State(entity_id, state.state, state.attributes)
257 _LOGGER.warning(
"Empty scenes are not allowed")
260 scene_config =
SceneConfig(
None, call.data[CONF_SCENE_ID],
None, entities)
261 entity_id = f
"{SCENE_DOMAIN}.{scene_config.name}"
262 if (old := platform.entities.get(entity_id))
is not None:
263 if not isinstance(old, HomeAssistantScene)
or not old.from_service:
264 _LOGGER.warning(
"The scene %s already exists", entity_id)
266 await platform.async_remove_entity(entity_id)
269 hass.services.async_register(
270 SCENE_DOMAIN, SERVICE_CREATE, create_service, CREATE_SCENE_SCHEMA
273 async
def delete_service(call: ServiceCall) ->
None:
274 """Delete a dynamically created scene."""
277 for entity_id
in entity_ids:
278 scene = platform.entities.get(entity_id)
281 translation_domain=SCENE_DOMAIN,
282 translation_key=
"entity_not_scene",
283 translation_placeholders={
284 "entity_id": entity_id,
287 assert isinstance(scene, HomeAssistantScene)
288 if not scene.from_service:
290 translation_domain=SCENE_DOMAIN,
291 translation_key=
"entity_not_dynamically_created",
292 translation_placeholders={
293 "entity_id": entity_id,
297 await platform.async_remove_entity(entity_id)
299 hass.services.async_register(
303 cv.make_entity_service_schema({}),
308 hass: HomeAssistant, async_add_entities: AddEntitiesCallback, config: dict[str, Any]
310 """Process multiple scenes and add them."""
312 scene_config: list[dict[str, Any]]
313 if not (scene_config := config[STATES]):
322 scene.get(CONF_ICON),
323 scene[CONF_ENTITIES],
326 for scene
in scene_config
331 """A scene is a group of entities and the states we want them to be."""
334 self, hass: HomeAssistant, scene_config: SceneConfig, from_service: bool =
False
336 """Initialize the scene."""
343 """Return the name of the scene."""
348 """Return the icon of the scene."""
353 """Return unique ID."""
358 """Return the scene state attributes."""
359 attributes: dict[str, Any] = {ATTR_ENTITY_ID:
list(self.
scene_configscene_config.states)}
360 if (unique_id := self.
unique_idunique_id)
is not None:
361 attributes[CONF_ID] = unique_id
365 """Activate scene. Try to get entities into requested state."""
369 context=self._context,
370 reproduce_options=kwargs,
None __init__(self, HomeAssistant hass, SceneConfig scene_config, bool from_service=False)
Mapping[str, Any] extra_state_attributes(self)
None async_activate(self, **Any kwargs)
None async_setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback async_add_entities, DiscoveryInfoType|None discovery_info=None)
list[str] scenes_with_entity(HomeAssistant hass, str entity_id)
dict[str, Any] _ensure_no_intersection(dict[str, Any] value)
dict[str, State] _convert_states(dict[str, Any] states)
list[str] entities_in_scene(HomeAssistant hass, str entity_id)
None _process_scenes_config(HomeAssistant hass, AddEntitiesCallback async_add_entities, dict[str, Any] config)
None async_register_admin_service(HomeAssistant hass, str domain, str service, Callable[[ServiceCall], Awaitable[None]|None] service_func, VolSchemaType schema=vol.Schema({}, extra=vol.PREVENT_EXTRA))
set[str] async_extract_entity_ids(HomeAssistant hass, ServiceCall service_call, bool expand_group=True)
None async_reproduce_state(HomeAssistant hass, State|Iterable[State] states, *Context|None context=None, dict[str, Any]|None reproduce_options=None)
Integration async_get_integration(HomeAssistant hass, str domain)