diff --git a/homeassistant/actors.py b/homeassistant/actors.py index d58c10f892d..93048b780c2 100644 --- a/homeassistant/actors.py +++ b/homeassistant/actors.py @@ -18,8 +18,13 @@ import homeassistant as ha import homeassistant.util as util from homeassistant.observers import ( STATE_CATEGORY_SUN, SUN_STATE_BELOW_HORIZON, SUN_STATE_ABOVE_HORIZON, + STATE_ATTRIBUTE_NEXT_SUN_SETTING, STATE_ATTRIBUTE_NEXT_SUN_RISING, + STATE_CATEGORY_ALL_DEVICES, DEVICE_STATE_HOME, DEVICE_STATE_NOT_HOME, - STATE_ATTRIBUTE_NEXT_SUN_SETTING) + STATE_CATEGORY_DEVICE_FORMAT, + + DOMAIN_LIGHT_CONTROL, SERVICE_TURN_LIGHT_ON, SERVICE_TURN_LIGHT_OFF, + STATE_CATEGORY_ALL_LIGHTS, STATE_CATEGORY_LIGHT_FORMAT, LIGHT_STATE_ON) LIGHT_TRANSITION_TIME = timedelta(minutes=15) @@ -37,83 +42,149 @@ SERVICE_KEYBOARD_MEDIA_NEXT_TRACK = "media_next_track" SERVICE_KEYBOARD_MEDIA_PREV_TRACK = "media_prev_track" -def _hue_process_transition_time(transition_seconds): - """ Transition time is in 1/10th seconds - and cannot exceed MAX_TRANSITION_TIME. """ - - # Max transition time for Hue is 900 seconds/15 minutes - return min(9000, transition_seconds * 10) +def is_sun_up(statemachine): + """ Returns if the sun is currently up based on the statemachine. """ + return statemachine.is_state(STATE_CATEGORY_SUN, SUN_STATE_ABOVE_HORIZON) -# pylint: disable=too-few-public-methods -class LightTrigger(object): - """ Class to turn on lights based on state of devices and the sun - or triggered by light events. """ +def next_sun_setting(statemachine): + """ Returns the datetime object representing the next sun setting. """ + state = statemachine.get_state(STATE_CATEGORY_SUN) - def __init__(self, bus, statemachine, device_tracker, light_control): - self.bus = bus - self.statemachine = statemachine - self.light_control = light_control + return None if not state else ha.str_to_datetime( + state['attributes'][STATE_ATTRIBUTE_NEXT_SUN_SETTING]) - self.logger = logging.getLogger(__name__) - # Track home coming of each seperate device - for category in device_tracker.device_state_categories: - ha.track_state_change(bus, category, - DEVICE_STATE_NOT_HOME, DEVICE_STATE_HOME, - self._handle_device_state_change) +def next_sun_rising(statemachine): + """ Returns the datetime object representing the next sun setting. """ + state = statemachine.get_state(STATE_CATEGORY_SUN) - # Track when all devices are gone to shut down lights - ha.track_state_change(bus, STATE_CATEGORY_ALL_DEVICES, - DEVICE_STATE_HOME, DEVICE_STATE_NOT_HOME, - self._handle_device_state_change) + return None if not state else ha.str_to_datetime( + state['attributes'][STATE_ATTRIBUTE_NEXT_SUN_RISING]) - # Track every time sun rises so we can schedule a time-based - # pre-sun set event - ha.track_state_change(bus, STATE_CATEGORY_SUN, - SUN_STATE_BELOW_HORIZON, SUN_STATE_ABOVE_HORIZON, - self._handle_sun_rising) - # If the sun is already above horizon - # schedule the time-based pre-sun set event - if statemachine.is_state(STATE_CATEGORY_SUN, SUN_STATE_ABOVE_HORIZON): - self._handle_sun_rising(None, None, None) +def is_device_home(statemachine, device_id=None): + """ Returns if any or specified device is home. """ + category = STATE_CATEGORY_DEVICE_FORMAT.format(device_id) if device_id \ + else STATE_CATEGORY_ALL_DEVICES + + return statemachine.is_state(category, DEVICE_STATE_HOME) + + +def is_light_on(statemachine, light_id=None): + """ Returns if the lights are on based on the statemachine. """ + category = STATE_CATEGORY_LIGHT_FORMAT.format(light_id) if light_id \ + else STATE_CATEGORY_ALL_LIGHTS + + return statemachine.is_state(category, LIGHT_STATE_ON) + + +def turn_light_on(bus, light_id=None, transition_seconds=None): + """ Turns all or specified light on. """ + data = {} + + if light_id: + data["light_id"] = light_id + + if transition_seconds: + data["transition_seconds"] = transition_seconds + + bus.call_service(DOMAIN_LIGHT_CONTROL, SERVICE_TURN_LIGHT_ON, data) + + +def turn_light_off(bus, light_id=None, transition_seconds=None): + """ Turns all or specified light off. """ + data = {} + + if light_id: + data["light_id"] = light_id + + if transition_seconds: + data["transition_seconds"] = transition_seconds + + bus.call_service(DOMAIN_LIGHT_CONTROL, SERVICE_TURN_LIGHT_OFF, data) + + +def get_light_count(statemachine): + """ Get the number of lights being tracked in the statemachine. """ + return len(get_light_ids(statemachine)) + + +def get_light_ids(statemachine): + """ Get the light IDs that are being tracked in the statemachine. """ + lights_prefix = STATE_CATEGORY_LIGHT_FORMAT.format("") + + light_id_part = slice(len(lights_prefix), None) + + return [cat[light_id_part] for cat in statemachine.categories + if cat.startswith(lights_prefix)] + + +# pylint: disable=too-many-branches +def setup_device_light_triggers(bus, statemachine, device_state_categories): + """ Triggers to turn lights on or off based on device precense. """ + + logger = logging.getLogger(__name__) + + if len(device_state_categories) == 0: + logger.error("LightTrigger:No devices given to track") + + return False + + light_ids = get_light_ids(statemachine) + + if len(light_ids) == 0: + logger.error("LightTrigger:No lights found to turn on") + + return False + + # Calculates the time when to start fading lights in when sun sets + time_for_light_before_sun_set = lambda: \ + (next_sun_setting(statemachine) - LIGHT_TRANSITION_TIME * + get_light_count(statemachine)) # pylint: disable=unused-argument - def _handle_sun_rising(self, category, old_state, new_state): + def handle_sun_rising(category, old_state, new_state): """The moment sun sets we want to have all the lights on. We will schedule to have each light start after one another and slowly transition in.""" - start_point = self._time_for_light_before_sun_set() + def turn_light_on_before_sunset(light_id): + """ Helper function to turn on lights slowly if there + are devices home and the light is not on yet. """ + if (is_device_home(statemachine) and + not is_light_on(statemachine, light_id)): - def turn_on(light): + turn_light_on(bus, light_id, LIGHT_TRANSITION_TIME.seconds) + + def turn_on(light_id): """ Lambda can keep track of function parameters but not local parameters. If we put the lambda directly in the below statement - only the last light would be turned on.. """ - return lambda now: self._turn_light_on_before_sunset(light) + only the last light will be turned on.. """ + return lambda now: turn_light_on_before_sunset(light_id) - for index, light_id in enumerate(self.light_control.light_ids): - ha.track_time_change(self.bus, turn_on(light_id), + start_point = time_for_light_before_sun_set() + + for index, light_id in enumerate(light_ids): + ha.track_time_change(bus, turn_on(light_id), point_in_time=(start_point + index * LIGHT_TRANSITION_TIME)) - def _turn_light_on_before_sunset(self, light_id=None): - """ Helper function to turn on lights slowly if there - are devices home and the light is not on yet. """ - if self.statemachine.is_state(STATE_CATEGORY_ALL_DEVICES, - DEVICE_STATE_HOME) and not self.light_control.is_light_on(light_id): + # Track every time sun rises so we can schedule a time-based + # pre-sun set event + ha.track_state_change(bus, STATE_CATEGORY_SUN, SUN_STATE_BELOW_HORIZON, + SUN_STATE_ABOVE_HORIZON, handle_sun_rising) - self.light_control.turn_light_on(light_id, - LIGHT_TRANSITION_TIME.seconds) + # If the sun is already above horizon + # schedule the time-based pre-sun set event + if is_sun_up(statemachine): + handle_sun_rising(None, None, None) - def _handle_device_state_change(self, category, old_state, new_state): + def handle_device_state_change(category, old_state, new_state): """ Function to handle tracked device state changes. """ - lights_are_on = self.light_control.is_light_on() + lights_are_on = is_light_on(statemachine) - light_needed = (not lights_are_on and - self.statemachine.is_state(STATE_CATEGORY_SUN, - SUN_STATE_BELOW_HORIZON)) + light_needed = not (lights_are_on or is_sun_up(statemachine)) # Specific device came home ? if (category != STATE_CATEGORY_ALL_DEVICES and @@ -121,29 +192,29 @@ class LightTrigger(object): # These variables are needed for the elif check now = datetime.now() - start_point = self._time_for_light_before_sun_set() + start_point = time_for_light_before_sun_set() # Do we need lights? if light_needed: - self.logger.info( + logger.info( "Home coming event for {}. Turning lights on". format(category)) - self.light_control.turn_light_on() + turn_light_on(bus) # Are we in the time span were we would turn on the lights # if someone would be home? # Check this by seeing if current time is later then the point # in time when we would start putting the lights on. - elif start_point < now < self._next_sun_setting(): + elif start_point < now < next_sun_setting(statemachine): # Check for every light if it would be on if someone was home # when the fading in started and turn it on if so - for index, light_id in enumerate(self.light_control.light_ids): + for index, light_id in enumerate(light_ids): if now > start_point + index * LIGHT_TRANSITION_TIME: - self.light_control.turn_light_on(light_id) + turn_light_on(bus, light_id) else: # If this light didn't happen to be turned on yet so @@ -154,25 +225,23 @@ class LightTrigger(object): elif (category == STATE_CATEGORY_ALL_DEVICES and new_state['state'] == DEVICE_STATE_NOT_HOME and lights_are_on): - self.logger.info(("Everyone has left but lights are on. " - "Turning lights off")) - self.light_control.turn_light_off() + logger.info( + "Everyone has left but lights are on. Turning lights off") - def _next_sun_setting(self): - """ Returns the datetime object representing the next sun setting. """ - state = self.statemachine.get_state(STATE_CATEGORY_SUN) + turn_light_off(bus) - return ha.str_to_datetime( - state['attributes'][STATE_ATTRIBUTE_NEXT_SUN_SETTING]) + # Track home coming of each seperate device + for category in device_state_categories: + ha.track_state_change(bus, category, + DEVICE_STATE_NOT_HOME, DEVICE_STATE_HOME, + handle_device_state_change) - def _time_for_light_before_sun_set(self): - """ Helper method to calculate the point in time we have to start - fading in lights so that all the lights are on the moment the sun - sets. - """ + # Track when all devices are gone to shut down lights + ha.track_state_change(bus, STATE_CATEGORY_ALL_DEVICES, + DEVICE_STATE_HOME, DEVICE_STATE_NOT_HOME, + handle_device_state_change) - return (self._next_sun_setting() - - LIGHT_TRANSITION_TIME * len(self.light_control.light_ids)) + return True class HueLightControl(object): @@ -191,8 +260,8 @@ class HueLightControl(object): self._bridge = phue.Bridge(host) - self._light_map = {light.light_id: light - for light in self._bridge.get_light_objects()} + self._light_map = {util.slugify(light.name): light for light + in self._bridge.get_light_objects()} self.success_init = True @@ -201,10 +270,6 @@ class HueLightControl(object): """ Return a list of light ids. """ return self._light_map.keys() - def get_light_name(self, light_id): - """ Return the name of the specified light. """ - return self._light_map[light_id].name - def is_light_on(self, light_id=None): """ Returns if specified or all light are on. """ if not light_id: @@ -212,34 +277,37 @@ class HueLightControl(object): [1 for light in self._light_map.values() if light.on]) > 0 else: - return self._bridge.get_light(light_id, 'on') + return self._bridge.get_light(self._convert_id(light_id), 'on') def turn_light_on(self, light_id=None, transition_seconds=None): """ Turn the specified or all lights on. """ - if not light_id: - light_id = self.light_ids - - command = {'on': True, 'xy': [0.5119, 0.4147], 'bri': 164} - - if transition_seconds: - command['transitiontime'] = \ - _hue_process_transition_time(transition_seconds) - - self._bridge.set_light(light_id, command) + self._turn_light(True, light_id, transition_seconds) def turn_light_off(self, light_id=None, transition_seconds=None): """ Turn the specified or all lights off. """ - if not light_id: - light_id = self.light_ids + self._turn_light(False, light_id, transition_seconds) - command = {'on': False} + def _turn_light(self, turn_on, light_id=None, transition_seconds=None): + """ Helper method to turn lights on or off. """ + if light_id: + light_id = self._convert_id(light_id) + else: + light_id = [light.light_id for light in self._light_map.values()] + + command = {'on': True, 'xy': [0.5119, 0.4147], 'bri': 164} if turn_on \ + else {'on': False} if transition_seconds: - command['transitiontime'] = \ - _hue_process_transition_time(transition_seconds) + # Transition time is in 1/10th seconds and cannot exceed + # MAX_TRANSITION_TIME which is 900 seconds for Hue. + command['transitiontime'] = min(9000, transition_seconds * 10) self._bridge.set_light(light_id, command) + def _convert_id(self, light_id): + """ Returns internal light id to be used with phue. """ + return self._light_map[light_id].light_id + def setup_file_downloader(bus, download_path): """ Listens for download events to download files. """ diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 6dc3c44e328..01ee8e34fce 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -92,12 +92,10 @@ def from_config_file(config_path): # Light trigger if light_control: - observers.setup_light_control_services(bus, statemachine, light_control) + observers.setup_light_control(bus, statemachine, light_control) - actors.LightTrigger(bus, statemachine, - device_tracker, light_control) - - statusses.append(("Light Trigger", True)) + statusses.append(("Light Trigger", actors.setup_device_light_triggers( + bus, statemachine, device_tracker.device_state_categories))) if config.has_option("downloader", "download_dir"): result = actors.setup_file_downloader( diff --git a/homeassistant/observers.py b/homeassistant/observers.py index 4b3a811e6aa..5970e8d896b 100644 --- a/homeassistant/observers.py +++ b/homeassistant/observers.py @@ -33,13 +33,13 @@ STATE_CATEGORY_SUN = "weather.sun" STATE_ATTRIBUTE_NEXT_SUN_RISING = "next_rising" STATE_ATTRIBUTE_NEXT_SUN_SETTING = "next_setting" -STATE_CATEGORY_ALL_DEVICES = 'device.ALL' -STATE_CATEGORY_DEVICE_FORMAT = 'device.{}' +STATE_CATEGORY_ALL_DEVICES = 'devices' +STATE_CATEGORY_DEVICE_FORMAT = 'devices.{}' STATE_CATEGORY_CHROMECAST = 'chromecast' -STATE_CATEGORY_ALL_LIGHTS = 'light.ALL' -STATE_CATEGORY_LIGHT_FORMAT = "light.{}" +STATE_CATEGORY_ALL_LIGHTS = 'lights' +STATE_CATEGORY_LIGHT_FORMAT = "lights.{}" SUN_STATE_ABOVE_HORIZON = "above_horizon" SUN_STATE_BELOW_HORIZON = "below_horizon" @@ -57,6 +57,8 @@ TIME_SPAN_FOR_ERROR_IN_SCANNING = timedelta(minutes=1) # Return cached results if last scan was less then this time ago TOMATO_MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) +LIGHTS_MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) + # Filename to save known devices to KNOWN_DEVICES_FILE = "known_devices.csv" @@ -141,26 +143,35 @@ def setup_chromecast(bus, statemachine, host): return True -def setup_light_control_services(bus, statemachine, light_control): +def setup_light_control(bus, statemachine, light_control): """ Exposes light control via statemachine and services. """ def update_light_state(time): # pylint: disable=unused-argument """ Track the state of the lights. """ - status = {light_id: light_control.is_light_on(light_id) - for light_id in light_control.light_ids} + try: + should_update = datetime.now() - update_light_state.last_updated \ + > LIGHTS_MIN_TIME_BETWEEN_SCANS - for light_id, state in status.items(): - state_category = STATE_CATEGORY_LIGHT_FORMAT.format( - util.slugify(light_control.get_light_name(light_id))) + except AttributeError: # if last_updated does not exist + should_update = True - statemachine.set_state(state_category, - LIGHT_STATE_ON if state + if should_update: + update_light_state.last_updated = datetime.now() + + status = {light_id: light_control.is_light_on(light_id) + for light_id in light_control.light_ids} + + for light_id, state in status.items(): + state_category = STATE_CATEGORY_LIGHT_FORMAT.format(light_id) + + statemachine.set_state(state_category, + LIGHT_STATE_ON if state + else LIGHT_STATE_OFF) + + statemachine.set_state(STATE_CATEGORY_ALL_LIGHTS, + LIGHT_STATE_ON if True in status.values() else LIGHT_STATE_OFF) - statemachine.set_state(STATE_CATEGORY_ALL_LIGHTS, - LIGHT_STATE_ON if True in status.values() - else LIGHT_STATE_OFF) - ha.track_time_change(bus, update_light_state, second=[0, 30]) def handle_light_event(service): @@ -173,6 +184,8 @@ def setup_light_control_services(bus, statemachine, light_control): else: light_control.turn_light_off(light_id, transition_seconds) + update_light_state(None) + # Listen for light on and light off events bus.register_service(DOMAIN_LIGHT_CONTROL, SERVICE_TURN_LIGHT_ON, handle_light_event) @@ -180,6 +193,8 @@ def setup_light_control_services(bus, statemachine, light_control): bus.register_service(DOMAIN_LIGHT_CONTROL, SERVICE_TURN_LIGHT_OFF, handle_light_event) + update_light_state(None) + return True