-
-
Notifications
You must be signed in to change notification settings - Fork 37.8k
Template light #7657
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Template light #7657
Changes from 22 commits
9116b5a
01b675a
7693083
c2fa266
876f270
43645d2
852c87f
a4cfcdf
2e5af74
c47a10a
287ed13
e6cb570
1da1dff
044e2a5
4f26997
9136f38
9aa2c53
f902649
2b0d4b2
df577a7
67d0655
62ffa4b
d63b87a
b004f8d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,236 @@ | ||
| """ | ||
| Support for Template lights. | ||
|
|
||
| For more details about this platform, please refer to the documentation at | ||
| https://home-assistant.io/components/light.template/ | ||
| """ | ||
| import logging | ||
| import asyncio | ||
|
|
||
| import voluptuous as vol | ||
|
|
||
| from homeassistant.core import callback | ||
| from homeassistant.components.light import ( | ||
| ATTR_BRIGHTNESS, ENTITY_ID_FORMAT, Light, SUPPORT_BRIGHTNESS) | ||
| from homeassistant.const import ( | ||
| CONF_VALUE_TEMPLATE, CONF_ENTITY_ID, CONF_FRIENDLY_NAME, STATE_ON, | ||
| STATE_OFF, EVENT_HOMEASSISTANT_START, MATCH_ALL | ||
| ) | ||
| from homeassistant.helpers.config_validation import PLATFORM_SCHEMA | ||
| from homeassistant.exceptions import TemplateError | ||
| import homeassistant.helpers.config_validation as cv | ||
| from homeassistant.helpers.entity import async_generate_entity_id | ||
| from homeassistant.helpers.event import async_track_state_change | ||
| from homeassistant.helpers.restore_state import async_get_last_state | ||
| from homeassistant.helpers.script import Script | ||
|
|
||
| _LOGGER = logging.getLogger(__name__) | ||
| _VALID_STATES = [STATE_ON, STATE_OFF, 'true', 'false'] | ||
|
|
||
| CONF_LIGHTS = 'lights' | ||
| CONF_ON_ACTION = 'turn_on' | ||
| CONF_OFF_ACTION = 'turn_off' | ||
| CONF_LEVEL_ACTION = 'set_level' | ||
| CONF_LEVEL_TEMPLATE = 'level_template' | ||
|
|
||
|
|
||
| LIGHT_SCHEMA = vol.Schema({ | ||
| vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, | ||
| vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA, | ||
| vol.Optional(CONF_VALUE_TEMPLATE, default=None): cv.template, | ||
| vol.Optional(CONF_LEVEL_ACTION, default=None): cv.SCRIPT_SCHEMA, | ||
| vol.Optional(CONF_LEVEL_TEMPLATE, default=None): cv.template, | ||
| vol.Optional(CONF_FRIENDLY_NAME, default=None): cv.string, | ||
| vol.Optional(CONF_ENTITY_ID): cv.entity_ids | ||
| }) | ||
|
|
||
| PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ | ||
| vol.Required(CONF_LIGHTS): vol.Schema({cv.slug: LIGHT_SCHEMA}), | ||
| }) | ||
|
|
||
|
|
||
| @asyncio.coroutine | ||
| def async_setup_platform(hass, config, async_add_devices, discovery_info=None): | ||
| """Set up Template Lights.""" | ||
| lights = [] | ||
|
|
||
| for device, device_config in config[CONF_LIGHTS].items(): | ||
| friendly_name = device_config.get(CONF_FRIENDLY_NAME, device) | ||
| state_template = device_config[CONF_VALUE_TEMPLATE] | ||
| on_action = device_config[CONF_ON_ACTION] | ||
| off_action = device_config[CONF_OFF_ACTION] | ||
| level_action = device_config[CONF_LEVEL_ACTION] | ||
| level_template = device_config[CONF_LEVEL_TEMPLATE] | ||
|
|
||
| template_entity_ids = [] | ||
|
|
||
| if state_template is not None: | ||
| temp_ids = state_template.extract_entities() | ||
| if str(temp_ids) != MATCH_ALL: | ||
| template_entity_ids = list(set(template_entity_ids) | | ||
| set(temp_ids)) | ||
|
|
||
| if level_template is not None: | ||
| temp_ids = level_template.extract_entities() | ||
| if str(temp_ids) != MATCH_ALL: | ||
| template_entity_ids = list(set(template_entity_ids) | | ||
| set(temp_ids)) | ||
|
|
||
| entity_ids = device_config.get(CONF_ENTITY_ID, template_entity_ids) | ||
|
|
||
| lights.append( | ||
| LightTemplate( | ||
| hass, device, friendly_name, state_template, | ||
| on_action, off_action, level_action, level_template, | ||
| entity_ids) | ||
| ) | ||
|
|
||
| if not lights: | ||
| _LOGGER.error("No lights added") | ||
| return False | ||
|
|
||
| async_add_devices(lights, True) | ||
| return True | ||
|
|
||
|
|
||
| class LightTemplate(Light): | ||
| """Representation of a templated Light, including dimmable.""" | ||
|
|
||
| def __init__(self, hass, device_id, friendly_name, state_template, | ||
| on_action, off_action, level_action, level_template, | ||
| entity_ids): | ||
| """Initialize the light.""" | ||
| self.hass = hass | ||
| self.entity_id = async_generate_entity_id( | ||
| ENTITY_ID_FORMAT, device_id, hass=hass) | ||
| self._name = friendly_name | ||
| self._template = state_template | ||
| self._on_script = Script(hass, on_action) | ||
| self._off_script = Script(hass, off_action) | ||
| self._level_script = Script(hass, level_action) | ||
| self._level_template = level_template | ||
|
|
||
| self._state = False | ||
| self._brightness = None | ||
| self._entities = entity_ids | ||
|
|
||
| if self._template is not None: | ||
| self._template.hass = self.hass | ||
| if self._level_template is not None: | ||
| self._level_template.hass = self.hass | ||
|
|
||
| @property | ||
| def brightness(self): | ||
| """Return the brightness of the light.""" | ||
| return self._brightness | ||
|
|
||
| @property | ||
| def supported_features(self): | ||
| """Flag supported features.""" | ||
| if self._level_script is not None: | ||
| return SUPPORT_BRIGHTNESS | ||
|
|
||
| return 0 | ||
|
|
||
| @property | ||
| def is_on(self): | ||
| """Return true if device is on.""" | ||
| return self._state | ||
|
|
||
| @property | ||
| def should_poll(self): | ||
| """Return the polling state.""" | ||
| return False | ||
|
|
||
| @asyncio.coroutine | ||
| def async_added_to_hass(self): | ||
| """Register callbacks.""" | ||
| state = yield from async_get_last_state(self.hass, self.entity_id) | ||
| if state: | ||
| self._state = state.state == STATE_ON | ||
|
|
||
| @callback | ||
| def template_light_state_listener(entity, old_state, new_state): | ||
| """Handle target device state changes.""" | ||
| if ( | ||
| self._template is not None or | ||
| self._level_template is not None): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This condition should be above line 163 |
||
| self.hass.async_add_job(self.async_update_ha_state(True)) | ||
|
|
||
| @callback | ||
| def template_light_startup(event): | ||
| """Update template on startup.""" | ||
| async_track_state_change( | ||
| self.hass, self._entities, template_light_state_listener) | ||
|
|
||
| self.hass.async_add_job(self.async_update_ha_state(True)) | ||
|
|
||
| self.hass.bus.async_listen_once( | ||
| EVENT_HOMEASSISTANT_START, template_light_startup) | ||
|
|
||
| @asyncio.coroutine | ||
| def async_turn_on(self, **kwargs): | ||
| """Turn the light on.""" | ||
| optimistic_set = False | ||
| # set optimistic states | ||
| if self._template is None: | ||
| self._state = True | ||
| optimistic_set = True | ||
|
|
||
| if self._level_template is None and ATTR_BRIGHTNESS in kwargs: | ||
| _LOGGER.info("Optimistically setting brightness to %s", | ||
| kwargs[ATTR_BRIGHTNESS]) | ||
| self._brightness = kwargs[ATTR_BRIGHTNESS] | ||
| optimistic_set = True | ||
|
|
||
| if ATTR_BRIGHTNESS in kwargs and self._level_script: | ||
| self.hass.async_add_job(self._level_script.async_run( | ||
| {"brightness": kwargs[ATTR_BRIGHTNESS]})) | ||
| else: | ||
| self.hass.async_add_job(self._on_script.async_run()) | ||
|
|
||
| if optimistic_set: | ||
| self.hass.async_add_job(self.async_update_ha_state()) | ||
|
|
||
| @asyncio.coroutine | ||
| def async_turn_off(self, **kwargs): | ||
| """Turn the light off.""" | ||
| self.hass.async_add_job(self._off_script.async_run()) | ||
| if self._template is None: | ||
| self._state = False | ||
| self.hass.async_add_job(self.async_update_ha_state()) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Indent this so it only runs if there is no template. |
||
|
|
||
| @asyncio.coroutine | ||
| def async_update(self): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Include should_poll False, as done here. |
||
| """Update the state from the template.""" | ||
| if self._template is not None: | ||
| try: | ||
| state = self._template.async_render().lower() | ||
| except TemplateError as ex: | ||
| _LOGGER.error(ex) | ||
| self._state = None | ||
|
|
||
| if state in _VALID_STATES: | ||
| self._state = state in ('true', STATE_ON) | ||
| else: | ||
| _LOGGER.error( | ||
| 'Received invalid light is_on state: %s. ' + | ||
| 'Expected: %s', | ||
| state, ', '.join(_VALID_STATES)) | ||
| self._state = None | ||
|
|
||
| if self._level_template is not None: | ||
| try: | ||
| brightness = self._level_template.async_render() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need error checking here similar to above. If it doesn't match an integer 0-255, we should log a similar error and set |
||
| except TemplateError as ex: | ||
| _LOGGER.error(ex) | ||
| self._state = None | ||
|
|
||
| if 0 <= int(brightness) <= 255: | ||
| self._brightness = brightness | ||
| else: | ||
| _LOGGER.error( | ||
| 'Received invalid brightness : %s' + | ||
| 'Expected: 0-255', | ||
| brightness) | ||
| self._brightness = None | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at this, now, we can actually make this even simpler. Let's initialize
template_entity_ids, as an empty set, then here we can just writetemplate_entity_ids |= set(temp_ids). We can actually pass the set right toasync_track_state_changewithout casting it back. Also though, since we're now filtering outMATCH_ALL, we need to catch the scenario where none of the templates have matches, so at the end you'll need to check