From 0e275014a9eb0dd783b47e85ef894cd6c47889ff Mon Sep 17 00:00:00 2001 From: lufton <lufton@gmail.com> Date: Sat, 18 May 2019 22:24:23 +0300 Subject: [PATCH] Added ToggleEntity save and restore state mechanism --- homeassistant/components/fan/__init__.py | 24 +++++- homeassistant/components/fan/services.yaml | 27 +++++++ homeassistant/components/light/__init__.py | 22 +++++- homeassistant/components/light/services.yaml | 27 +++++++ homeassistant/components/switch/__init__.py | 23 +++++- homeassistant/components/switch/services.yaml | 27 +++++++ homeassistant/const.py | 3 + homeassistant/helpers/entity.py | 75 ++++++++++++++++++- 8 files changed, 220 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index 23015769f28..fef362f76d4 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -6,10 +6,13 @@ import logging import voluptuous as vol from homeassistant.components import group -from homeassistant.const import (SERVICE_TURN_ON, SERVICE_TOGGLE, - SERVICE_TURN_OFF, ATTR_ENTITY_ID) +from homeassistant.const import ( + SERVICE_CANCEL_RESTORE_STATE, SERVICE_RESTORE_STATE, SERVICE_SAVE_STATE, + SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, ATTR_ENTITY_ID) from homeassistant.loader import bind_hass -from homeassistant.helpers.entity import ToggleEntity +from homeassistant.helpers.entity import ( + ToggleEntity, ENTITY_SAVE_STATE_SCHEMA, + ENTITY_RESTORE_STATE_SCHEMA, ENTITY_CANCEL_RESTORE_STATE_SCHEMA) from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.config_validation import ( # noqa PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE) @@ -123,6 +126,21 @@ async def async_setup(hass, config: dict): 'async_set_direction' ) + component.async_register_entity_service( + SERVICE_SAVE_STATE, ENTITY_SAVE_STATE_SCHEMA, + 'async_save_state' + ) + + component.async_register_entity_service( + SERVICE_RESTORE_STATE, ENTITY_RESTORE_STATE_SCHEMA, + 'async_restore_state' + ) + + component.async_register_entity_service( + SERVICE_CANCEL_RESTORE_STATE, ENTITY_CANCEL_RESTORE_STATE_SCHEMA, + 'async_cancel_restore_state' + ) + return True diff --git a/homeassistant/components/fan/services.yaml b/homeassistant/components/fan/services.yaml index 16d3742d9ab..6d2beea8b35 100644 --- a/homeassistant/components/fan/services.yaml +++ b/homeassistant/components/fan/services.yaml @@ -54,6 +54,33 @@ set_direction: description: The direction to rotate. Either 'forward' or 'reverse' example: 'forward' +save_state: + description: Saves a state of the entity. + fields: + entity_id: + description: Name(s) of entities to save state of. + example: 'fan.attic' + rewrite: + description: Should it rewrite already saved state fo the entity (default False). + example: True + +restore_state: + description: Restores a state of the entity. + fields: + entity_id: + description: Name(s) of entities to restore state of. + example: 'fan.attic' + delay: + description: Time period before restore. + example: "5, '0:05', {'minutes': 5}" + +cancel_restore_state: + description: Cancels scheduled state restore of the entity. + fields: + entity_id: + description: Name(s) of entities to cancel restoring state of. + example: 'fan.attic' + xiaomi_miio_set_buzzer_on: description: Turn the buzzer on. fields: diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index f9ce6eb05d4..7c98058972d 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -11,13 +11,16 @@ from homeassistant.auth.permissions.const import POLICY_CONTROL from homeassistant.components.group import \ ENTITY_ID_FORMAT as GROUP_ENTITY_ID_FORMAT from homeassistant.const import ( - ATTR_ENTITY_ID, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, + ATTR_ENTITY_ID, SERVICE_CANCEL_RESTORE_STATE, SERVICE_RESTORE_STATE, + SERVICE_SAVE_STATE, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_ON) from homeassistant.exceptions import UnknownUser, Unauthorized import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE) -from homeassistant.helpers.entity import ToggleEntity +from homeassistant.helpers.entity import ( + ToggleEntity, ENTITY_SAVE_STATE_SCHEMA, ENTITY_RESTORE_STATE_SCHEMA, + ENTITY_CANCEL_RESTORE_STATE_SCHEMA) from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers import intent from homeassistant.loader import bind_hass @@ -320,6 +323,21 @@ async def async_setup(hass, config): 'async_toggle' ) + component.async_register_entity_service( + SERVICE_SAVE_STATE, ENTITY_SAVE_STATE_SCHEMA, + 'async_save_state' + ) + + component.async_register_entity_service( + SERVICE_RESTORE_STATE, ENTITY_RESTORE_STATE_SCHEMA, + 'async_restore_state' + ) + + component.async_register_entity_service( + SERVICE_CANCEL_RESTORE_STATE, ENTITY_CANCEL_RESTORE_STATE_SCHEMA, + 'async_cancel_restore_state' + ) + hass.helpers.intent.async_register(SetIntentHandler()) return True diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index ef944d75efc..c471d1e48cc 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -71,6 +71,33 @@ toggle: '...': description: All turn_on parameters can be used. +save_state: + description: Saves a state of the entity. + fields: + entity_id: + description: Name(s) of entities to save state of. + example: 'light.attic' + rewrite: + description: Should it rewrite already saved state fo the entity (default False). + example: True + +restore_state: + description: Restores a state of the entity. + fields: + entity_id: + description: Name(s) of entities to restore state of. + example: 'light.attic' + delay: + description: Time period before restore. + example: "5, '0:05', {'minutes': 5}" + +cancel_restore_state: + description: Cancels scheduled state restore of the entity. + fields: + entity_id: + description: Name(s) of entities to cancel restoring state of. + example: 'light.attic' + lifx_set_state: description: Set a color/brightness and possibliy turn the light on/off. fields: diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index e3f756abf53..6791e2052cb 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -6,12 +6,15 @@ import voluptuous as vol from homeassistant.loader import bind_hass from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.entity import ToggleEntity +from homeassistant.helpers.entity import ( + ToggleEntity, ENTITY_SAVE_STATE_SCHEMA, + ENTITY_RESTORE_STATE_SCHEMA, ENTITY_CANCEL_RESTORE_STATE_SCHEMA) from homeassistant.helpers.config_validation import ( # noqa PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE) import homeassistant.helpers.config_validation as cv from homeassistant.const import ( - STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, + STATE_ON, SERVICE_CANCEL_RESTORE_STATE, SERVICE_RESTORE_STATE, + SERVICE_SAVE_STATE, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, ATTR_ENTITY_ID) from homeassistant.components import group @@ -81,6 +84,22 @@ async def async_setup(hass, config): 'async_toggle' ) + component.async_register_entity_service( + SERVICE_SAVE_STATE, ENTITY_SAVE_STATE_SCHEMA, + 'async_save_state' + ) + + component.async_register_entity_service( + SERVICE_RESTORE_STATE, ENTITY_RESTORE_STATE_SCHEMA, + 'async_restore_state' + ) + + component.async_register_entity_service( + SERVICE_CANCEL_RESTORE_STATE, ENTITY_CANCEL_RESTORE_STATE_SCHEMA, + 'async_cancel_restore_state' + ) + + return True diff --git a/homeassistant/components/switch/services.yaml b/homeassistant/components/switch/services.yaml index 46b1237f57c..5eed4d3a351 100644 --- a/homeassistant/components/switch/services.yaml +++ b/homeassistant/components/switch/services.yaml @@ -21,6 +21,33 @@ toggle: description: Name(s) of entities to toggle. example: 'switch.living_room' +save_state: + description: Saves a state of the entity. + fields: + entity_id: + description: Name(s) of entities to save state of. + example: 'switch.attic' + rewrite: + description: Should it rewrite already saved state fo the entity (default False). + example: True + +restore_state: + description: Restores a state of the entity. + fields: + entity_id: + description: Name(s) of entities to restore state of. + example: 'switch.attic' + delay: + description: Time period before restore. + example: "5, '0:05', {'minutes': 5}" + +cancel_restore_state: + description: Cancels scheduled state restore of the entity. + fields: + entity_id: + description: Name(s) of entities to cancel restoring state of. + example: 'switch.attic' + mysensors_send_ir_code: description: Set an IR code as a state attribute for a MySensors IR device switch and turn the switch on. fields: diff --git a/homeassistant/const.py b/homeassistant/const.py index 9176c1b8939..af6ad3887a3 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -371,6 +371,9 @@ SERVICE_HOMEASSISTANT_RESTART = 'restart' SERVICE_TURN_ON = 'turn_on' SERVICE_TURN_OFF = 'turn_off' SERVICE_TOGGLE = 'toggle' +SERVICE_SAVE_STATE = 'save_state' +SERVICE_RESTORE_STATE = 'restore_state' +SERVICE_CANCEL_RESTORE_STATE = 'cancel_restore_state' SERVICE_RELOAD = 'reload' SERVICE_VOLUME_UP = 'volume_up' diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index d69cdd3d997..a09c9cde0d4 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -5,21 +5,45 @@ import functools as ft from timeit import default_timer as timer from typing import Optional, List, Iterable +import voluptuous as vol + from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_FRIENDLY_NAME, ATTR_HIDDEN, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, DEVICE_DEFAULT_NAME, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT, - ATTR_ENTITY_PICTURE, ATTR_SUPPORTED_FEATURES, ATTR_DEVICE_CLASS) + ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE, ATTR_SUPPORTED_FEATURES, + ATTR_DEVICE_CLASS) from homeassistant.core import HomeAssistant, callback from homeassistant.config import DATA_CUSTOMIZE from homeassistant.exceptions import NoEntitySpecifiedError from homeassistant.util import ensure_unique_string, slugify from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.util import dt as dt_util +import homeassistant.helpers.config_validation as cv +import homeassistant.helpers.event as evt _LOGGER = logging.getLogger(__name__) SLOW_UPDATE_WARNING = 10 +ATTR_REWRITE = "rewrite" +ATTR_DELAY = "delay" + +ENTITY_SAVE_STATE_SCHEMA = vol.Schema({ + ATTR_ENTITY_ID: cv.comp_entity_ids, + ATTR_REWRITE: cv.boolean, +}) + +ENTITY_RESTORE_STATE_SCHEMA = vol.Schema({ + ATTR_ENTITY_ID: cv.comp_entity_ids, + ATTR_DELAY: vol.All(cv.time_period, cv.positive_timedelta), +}) + +ENTITY_CANCEL_RESTORE_STATE_SCHEMA = vol.Schema({ + ATTR_ENTITY_ID: cv.comp_entity_ids, +}) + +SAVED_STATE_ID_FORMAT = "saved_{}" + def generate_entity_id(entity_id_format: str, name: Optional[str], current_ids: Optional[List[str]] = None, @@ -448,6 +472,8 @@ class Entity: class ToggleEntity(Entity): """An abstract class for entities that can be turned on and off.""" + _restore_state_listener = None + @property def state(self) -> str: """Return the state.""" @@ -458,6 +484,11 @@ class ToggleEntity(Entity): """Return True if entity is on.""" raise NotImplementedError() + @property + def saved_state_id(self): + """Return the id of state to save""" + return SAVED_STATE_ID_FORMAT.format(self.entity_id) + def turn_on(self, **kwargs) -> None: """Turn the entity on.""" raise NotImplementedError() @@ -497,3 +528,45 @@ class ToggleEntity(Entity): if self.is_on: return self.async_turn_off(**kwargs) return self.async_turn_on(**kwargs) + + async def async_cancel_restore_state(self): + """Cancel scheduled entity state restore.""" + if self._restore_state_listener: + self._restore_state_listener() + self._restore_state_listener = None + + async def async_save_state(self, rewrite=False): + """Save entity state.""" + if rewrite: + await self.async_cancel_restore_state() + + if rewrite or not self.hass.states.get(self.saved_state_id): + self.hass.states.async_set( + self.saved_state_id, + self.state, + self.hass.states.get(self.entity_id).attributes + ) + + @callback + async def async_restore_state_listener(self, *args): + """Restore entity state after a delay.""" + await self.async_cancel_restore_state() + saved_state = self.hass.states.get(self.saved_state_id) + if saved_state: + if saved_state.state == STATE_ON: + await self.async_turn_on(**saved_state.attributes) + else: + await self.async_turn_off() + self.hass.states.async_remove(self.saved_state_id) + + async def async_restore_state(self, delay=None): + """Restore previously saved entity state.""" + if delay: + await self.async_cancel_restore_state() + self._restore_state_listener = evt.async_call_later( + self.hass, + delay.total_seconds(), + self.async_restore_state_listener() + ) + else: + await self.async_restore_state_listener()