Files
core/homeassistant/components/timer/__init__.py
T
Julio Guerra c20324793e timer: include the remaining time in the state attributes (#11510)
Add the amount of remaining time before a timer is finished in its state attributes, so that it is received when fetching a timer state.

In my particular case, it allows me to correctly implement a UI for a timer when it is paused and when the UI didn't received the pause state change (which happens when the UI is started after the pause). In this case, it is impossible to show the remaining amount of time before the timer ends.
2018-01-07 13:37:19 -08:00

323 lines
9.3 KiB
Python

"""
Timer component.
For more details about this component, please refer to the documentation
at https://home-assistant.io/components/timer/
"""
import asyncio
import logging
import os
from datetime import timedelta
import voluptuous as vol
import homeassistant.util.dt as dt_util
import homeassistant.helpers.config_validation as cv
from homeassistant.config import load_yaml_config_file
from homeassistant.const import (ATTR_ENTITY_ID, CONF_ICON, CONF_NAME)
from homeassistant.core import callback
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.loader import bind_hass
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'timer'
ENTITY_ID_FORMAT = DOMAIN + '.{}'
DEFAULT_DURATION = 0
ATTR_DURATION = 'duration'
ATTR_REMAINING = 'remaining'
CONF_DURATION = 'duration'
STATUS_IDLE = 'idle'
STATUS_ACTIVE = 'active'
STATUS_PAUSED = 'paused'
EVENT_TIMER_FINISHED = 'timer.finished'
EVENT_TIMER_CANCELLED = 'timer.cancelled'
SERVICE_START = 'start'
SERVICE_PAUSE = 'pause'
SERVICE_CANCEL = 'cancel'
SERVICE_FINISH = 'finish'
SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
})
SERVICE_SCHEMA_DURATION = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Optional(ATTR_DURATION,
default=timedelta(DEFAULT_DURATION)): cv.time_period,
})
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
cv.slug: vol.Any({
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_ICON): cv.icon,
vol.Optional(CONF_DURATION, timedelta(DEFAULT_DURATION)):
cv.time_period,
}, None)
})
}, extra=vol.ALLOW_EXTRA)
@bind_hass
def start(hass, entity_id, duration):
"""Start a timer."""
hass.add_job(async_start, hass, entity_id, {ATTR_ENTITY_ID: entity_id,
ATTR_DURATION: duration})
@callback
@bind_hass
def async_start(hass, entity_id, duration):
"""Start a timer."""
hass.async_add_job(hass.services.async_call(
DOMAIN, SERVICE_START, {ATTR_ENTITY_ID: entity_id,
ATTR_DURATION: duration}))
@bind_hass
def pause(hass, entity_id):
"""Pause a timer."""
hass.add_job(async_pause, hass, entity_id)
@callback
@bind_hass
def async_pause(hass, entity_id):
"""Pause a timer."""
hass.async_add_job(hass.services.async_call(
DOMAIN, SERVICE_PAUSE, {ATTR_ENTITY_ID: entity_id}))
@bind_hass
def cancel(hass, entity_id):
"""Cancel a timer."""
hass.add_job(async_cancel, hass, entity_id)
@callback
@bind_hass
def async_cancel(hass, entity_id):
"""Cancel a timer."""
hass.async_add_job(hass.services.async_call(
DOMAIN, SERVICE_CANCEL, {ATTR_ENTITY_ID: entity_id}))
@bind_hass
def finish(hass, entity_id):
"""Finish a timer."""
hass.add_job(async_cancel, hass, entity_id)
@callback
@bind_hass
def async_finish(hass, entity_id):
"""Finish a timer."""
hass.async_add_job(hass.services.async_call(
DOMAIN, SERVICE_FINISH, {ATTR_ENTITY_ID: entity_id}))
@asyncio.coroutine
def async_setup(hass, config):
"""Set up a timer."""
component = EntityComponent(_LOGGER, DOMAIN, hass)
entities = []
for object_id, cfg in config[DOMAIN].items():
if not cfg:
cfg = {}
name = cfg.get(CONF_NAME)
icon = cfg.get(CONF_ICON)
duration = cfg.get(CONF_DURATION)
entities.append(Timer(hass, object_id, name, icon, duration))
if not entities:
return False
@asyncio.coroutine
def async_handler_service(service):
"""Handle a call to the timer services."""
target_timers = component.async_extract_from_service(service)
attr = None
if service.service == SERVICE_PAUSE:
attr = 'async_pause'
elif service.service == SERVICE_CANCEL:
attr = 'async_cancel'
elif service.service == SERVICE_FINISH:
attr = 'async_finish'
tasks = [getattr(timer, attr)() for timer in target_timers if attr]
if service.service == SERVICE_START:
for timer in target_timers:
tasks.append(
timer.async_start(service.data.get(ATTR_DURATION))
)
if tasks:
yield from asyncio.wait(tasks, loop=hass.loop)
descriptions = yield from hass.async_add_job(
load_yaml_config_file,
os.path.join(os.path.dirname(__file__), 'services.yaml')
)
hass.services.async_register(
DOMAIN, SERVICE_START, async_handler_service,
descriptions[SERVICE_START], SERVICE_SCHEMA_DURATION)
hass.services.async_register(
DOMAIN, SERVICE_PAUSE, async_handler_service,
descriptions[SERVICE_PAUSE], SERVICE_SCHEMA)
hass.services.async_register(
DOMAIN, SERVICE_CANCEL, async_handler_service,
descriptions[SERVICE_CANCEL], SERVICE_SCHEMA)
hass.services.async_register(
DOMAIN, SERVICE_FINISH, async_handler_service,
descriptions[SERVICE_FINISH], SERVICE_SCHEMA)
yield from component.async_add_entities(entities)
return True
class Timer(Entity):
"""Representation of a timer."""
def __init__(self, hass, object_id, name, icon, duration):
"""Initialize a timer."""
self.entity_id = ENTITY_ID_FORMAT.format(object_id)
self._name = name
self._state = STATUS_IDLE
self._duration = duration
self._remaining = self._duration
self._icon = icon
self._hass = hass
self._end = None
self._listener = None
@property
def should_poll(self):
"""If entity should be polled."""
return False
@property
def name(self):
"""Return name of the timer."""
return self._name
@property
def icon(self):
"""Return the icon to be used for this entity."""
return self._icon
@property
def state(self):
"""Return the current value of the timer."""
return self._state
@property
def state_attributes(self):
"""Return the state attributes."""
return {
ATTR_DURATION: str(self._duration),
ATTR_REMAINING: str(self._remaining)
}
@asyncio.coroutine
def async_added_to_hass(self):
"""Call when entity is about to be added to Home Assistant."""
# If not None, we got an initial value.
if self._state is not None:
return
restore_state = self._hass.helpers.restore_state
state = yield from restore_state.async_get_last_state(self.entity_id)
self._state = state and state.state == state
@asyncio.coroutine
def async_start(self, duration):
"""Start a timer."""
if self._listener:
self._listener()
self._listener = None
newduration = None
if duration:
newduration = duration
self._state = STATUS_ACTIVE
# pylint: disable=redefined-outer-name
start = dt_util.utcnow()
if self._remaining and newduration is None:
self._end = start + self._remaining
else:
if newduration:
self._duration = newduration
self._remaining = newduration
else:
self._remaining = self._duration
self._end = start + self._duration
self._listener = async_track_point_in_utc_time(self._hass,
self.async_finished,
self._end)
yield from self.async_update_ha_state()
@asyncio.coroutine
def async_pause(self):
"""Pause a timer."""
if self._listener is None:
return
self._listener()
self._listener = None
self._remaining = self._end - dt_util.utcnow()
self._state = STATUS_PAUSED
self._end = None
yield from self.async_update_ha_state()
@asyncio.coroutine
def async_cancel(self):
"""Cancel a timer."""
if self._listener:
self._listener()
self._listener = None
self._state = STATUS_IDLE
self._end = None
self._remaining = timedelta()
self._hass.bus.async_fire(EVENT_TIMER_CANCELLED,
{"entity_id": self.entity_id})
yield from self.async_update_ha_state()
@asyncio.coroutine
def async_finish(self):
"""Reset and updates the states, fire finished event."""
if self._state != STATUS_ACTIVE:
return
self._listener = None
self._state = STATUS_IDLE
self._remaining = timedelta()
self._hass.bus.async_fire(EVENT_TIMER_FINISHED,
{"entity_id": self.entity_id})
yield from self.async_update_ha_state()
@asyncio.coroutine
def async_finished(self, time):
"""Reset and updates the states, fire finished event."""
if self._state != STATUS_ACTIVE:
return
self._listener = None
self._state = STATUS_IDLE
self._remaining = timedelta()
self._hass.bus.async_fire(EVENT_TIMER_FINISHED,
{"entity_id": self.entity_id})
yield from self.async_update_ha_state()