diff --git a/homeassistant/components/scheduler/__init__.py b/homeassistant/components/scheduler/__init__.py new file mode 100644 index 00000000000..c06f6acd7f5 --- /dev/null +++ b/homeassistant/components/scheduler/__init__.py @@ -0,0 +1,126 @@ +""" +homeassistant.components.scheduler +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +A component that will act as a scheduler and performe actions based +on the events in the schedule. + +It will read a json object from schedule.json in the config dir +and create a schedule based on it. +Each schedule is a JSON with the keys id, name, description, +entity_ids, and events. +- days is an array with the weekday number (monday=0) that the schdule + is active +- entity_ids an array with entity ids that the events in the schedule should + effect (can also be groups) +- events is an array of objects that describe the different events that is + supported. Read in the events descriptions for more information +""" +import logging +import json + +from homeassistant.loader import get_component + +from homeassistant.const import ATTR_ENTITY_ID + +# The domain of your component. Should be equal to the name of your component +DOMAIN = 'scheduler' + +# List of component names (string) your component depends upon +# If you are setting up a group but not using a group for anything, +# don't depend on group +DEPENDENCIES = ['sun'] + +_LOGGER = logging.getLogger(__name__) + +_SCHEDULE_FILE = 'schedule.json' + + +# pylint: disable=unused-argument +def setup(hass, config): + """ Create the schedules """ + + def setup_schedule(schedule_data): + """ setup a schedule based on the description """ + + schedule = Schedule(schedule_data['id'], + name=schedule_data['name'], + description=schedule_data['description'], + entity_ids=schedule_data['entity_ids'], + days=schedule_data['days']) + + for event_data in schedule_data['events']: + event_listener_type = \ + get_component('scheduler.{}'.format(event_data['type'])) + event_listener = event_listener_type.create(schedule, event_data) + schedule.add_event_listener(event_listener) + + schedule.schedule(hass) + return True + + with open(hass.get_config_path(_SCHEDULE_FILE)) as schedule_file: + schedule_descriptions = json.load(schedule_file) + + for schedule_description in schedule_descriptions: + if not setup_schedule(schedule_description): + return False + + return True + + +class Schedule(object): + """ A Schedule """ + + # pylint: disable=too-many-arguments + def __init__(self, schedule_id, name=None, description=None, + entity_ids=None, days=None): + + self.schedule_id = schedule_id + self.name = name + self.description = description + + self.entity_ids = entity_ids or [] + + self.days = days or [0, 1, 2, 3, 4, 5, 6] + + self.__event_listeners = [] + + def add_event_listener(self, event_listener): + """ Add a event to the schedule """ + self.__event_listeners.append(event_listener) + + def schedule(self, hass): + """ Schedule all the events in the schdule """ + for event in self.__event_listeners: + event.schedule(hass) + + +class EventListener(object): + """ The base EventListner class that the schedule uses """ + def __init__(self, schedule): + self.my_schedule = schedule + + def schedule(self, hass): + """ Schedule the event """ + pass + + def execute(self, hass): + """ execute the event """ + pass + + +# pylint: disable=too-few-public-methods +class ServiceEventListener(EventListener): + """ A EventListner that calls a service when executed """ + + def __init__(self, schdule, service): + EventListener.__init__(self, schdule) + + (self.domain, self.service) = service.split('.') + + def execute(self, hass): + """ Call the service """ + data = {ATTR_ENTITY_ID: self.my_schedule.entity_ids} + hass.call_service(self.domain, self.service, data) + + # Reschedule for next day + self.schedule(hass) diff --git a/homeassistant/components/scheduler/sun.py b/homeassistant/components/scheduler/sun.py new file mode 100644 index 00000000000..5af965a1764 --- /dev/null +++ b/homeassistant/components/scheduler/sun.py @@ -0,0 +1,114 @@ +""" +An event in the scheduler component that will call the service +when the sun rises or sets with an offset. +The sun evnt need to have the type 'sun', which service to call, +which event (sunset or sunrise) and the offset. + +{ + "type": "sun", + "service": "switch.turn_on", + "event": "sunset", + "offset": "-01:00:00" +} + +""" + +from datetime import datetime, timedelta +import logging + +from homeassistant.components.scheduler import ServiceEventListener +import homeassistant.components.sun as sun + +_LOGGER = logging.getLogger(__name__) + + +def create(schedule, event_listener_data): + """ Create a sun event listener based on the description. """ + + negative_offset = False + service = event_listener_data['service'] + offset_str = event_listener_data['offset'] + event = event_listener_data['event'] + + if offset_str.startswith('-'): + negative_offset = True + offset_str = offset_str[1:] + + (hour, minute, second) = [int(x) for x in offset_str.split(':')] + + offset = timedelta(hours=hour, minutes=minute, seconds=second) + + if event == 'sunset': + return SunsetEventListener(schedule, service, offset, negative_offset) + + return SunriseEventListener(schedule, service, offset, negative_offset) + + +# pylint: disable=too-few-public-methods +class SunEventListener(ServiceEventListener): + """ This is the base class for sun event listeners. """ + + def __init__(self, schedule, service, offset, negative_offset): + ServiceEventListener.__init__(self, schedule, service) + + self.offset = offset + self.negative_offset = negative_offset + + def __get_next_time(self, next_event): + """ + Returns when the next time the service should be called. + Taking into account the offset and which days the event should execute. + """ + + if self.negative_offset: + next_time = next_event - self.offset + else: + next_time = next_event + self.offset + + while next_time < datetime.now() or \ + next_time.weekday() not in self.my_schedule.days: + next_time = next_time + timedelta(days=1) + + return next_time + + def schedule_next_event(self, hass, next_event): + """ Schedule the event """ + next_time = self.__get_next_time(next_event) + + # pylint: disable=unused-argument + def execute(now): + """ Call the execute method """ + self.execute(hass) + + hass.track_point_in_time(execute, next_time) + + return next_time + + +# pylint: disable=too-few-public-methods +class SunsetEventListener(SunEventListener): + """ This class is used the call a service when the sun sets. """ + def schedule(self, hass): + """ Schedule the event """ + next_setting = sun.next_setting(hass) + + next_time = self.schedule_next_event(hass, next_setting) + + _LOGGER.info( + 'SunsetEventListener scheduled for %s, will call service %s.%s', + next_time, self.domain, self.service) + + +# pylint: disable=too-few-public-methods +class SunriseEventListener(SunEventListener): + """ This class is used the call a service when the sun rises. """ + + def schedule(self, hass): + """ Schedule the event """ + next_rising = sun.next_rising(hass) + + next_time = self.schedule_next_event(hass, next_rising) + + _LOGGER.info( + 'SunriseEventListener scheduled for %s, will call service %s.%s', + next_time, self.domain, self.service) diff --git a/homeassistant/components/scheduler/time.py b/homeassistant/components/scheduler/time.py new file mode 100644 index 00000000000..c1d8ebb7845 --- /dev/null +++ b/homeassistant/components/scheduler/time.py @@ -0,0 +1,69 @@ +""" +An event in the scheduler component that will call the service +every specified day at the time specified. +A time event need to have the type 'time', which service to call and at +which time. + +{ + "type": "time", + "service": "switch.turn_off", + "time": "22:00:00" +} + +""" + +from datetime import datetime, timedelta +import logging + +from homeassistant.components.scheduler import ServiceEventListener + +_LOGGER = logging.getLogger(__name__) + + +def create(schedule, event_listener_data): + """ Create a TimeEvent based on the description """ + + service = event_listener_data['service'] + (hour, minute, second) = [int(x) for x in + event_listener_data['time'].split(':')] + + return TimeEventListener(schedule, service, hour, minute, second) + + +# pylint: disable=too-few-public-methods +class TimeEventListener(ServiceEventListener): + """ The time event that the scheduler uses """ + + # pylint: disable=too-many-arguments + def __init__(self, schedule, service, hour, minute, second): + ServiceEventListener.__init__(self, schedule, service) + + self.hour = hour + self.minute = minute + self.second = second + + def schedule(self, hass): + """ Schedule this event so that it will be called """ + + next_time = datetime.now().replace(hour=self.hour, + minute=self.minute, + second=self.second, + microsecond=0) + + # Calculate the next time the event should be executed. + # That is the next day that the schedule is configured to run + while next_time < datetime.now() or \ + next_time.weekday() not in self.my_schedule.days: + + next_time = next_time + timedelta(days=1) + + # pylint: disable=unused-argument + def execute(now): + """ Call the execute method """ + self.execute(hass) + + hass.track_point_in_time(execute, next_time) + + _LOGGER.info( + 'TimeEventListener scheduled for %s, will call service %s.%s', + next_time, self.domain, self.service)