diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py index 063f5e01939..7d7ccca3f8b 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -116,6 +116,54 @@ def extract_entity_ids(hass, service): return entity_ids +class ToggleDevice(object): + """ ABC for devices that can be turned on and off. """ + # pylint: disable=no-self-use + + entity_id = None + + def get_name(self): + """ Returns the name of the device if any. """ + return None + + def turn_on(self, **kwargs): + """ Turn the device on. """ + pass + + def turn_off(self, **kwargs): + """ Turn the device off. """ + pass + + def is_on(self): + """ True if device is on. """ + return False + + def get_state_attributes(self): + """ Returns optional state attributes. """ + return None + + def update(self): + """ Retrieve latest state from the real device. """ + pass + + def update_ha_state(self, hass, force_refresh=False): + """ + Updates Home Assistant with current state of device. + If force_refresh == True will update device before setting state. + """ + if self.entity_id is None: + raise ha.NoEntitySpecifiedError( + "No entity specified for device {}".format(self.get_name())) + + if force_refresh: + self.update() + + state = STATE_ON if self.is_on() else STATE_OFF + + return hass.states.set(self.entity_id, state, + self.get_state_attributes()) + + # pylint: disable=unused-argument def setup(hass, config): """ Setup general services related to homeassistant. """ diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 54518f822d8..2fafdf39cef 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -57,10 +57,9 @@ import csv import homeassistant as ha import homeassistant.util as util -from homeassistant.components import (group, extract_entity_ids, - STATE_ON, STATE_OFF, - SERVICE_TURN_ON, SERVICE_TURN_OFF, - ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME) +from homeassistant.components import ( + ToggleDevice, group, extract_entity_ids, STATE_ON, + SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME) DOMAIN = "light" @@ -90,6 +89,8 @@ ATTR_PROFILE = "profile" PHUE_CONFIG_FILE = "phue.conf" LIGHT_PROFILES_FILE = "light_profiles.csv" +_LOGGER = logging.getLogger(__name__) + def is_on(hass, entity_id=None): """ Returns if the lights are on based on the statemachine. """ @@ -142,90 +143,42 @@ def turn_off(hass, entity_id=None, transition=None): def setup(hass, config): """ Exposes light control via statemachine and services. """ - logger = logging.getLogger(__name__) - - if not util.validate_config(config, {DOMAIN: [ha.CONF_TYPE]}, logger): + if not util.validate_config(config, {DOMAIN: [ha.CONF_TYPE]}, _LOGGER): return False light_type = config[DOMAIN][ha.CONF_TYPE] if light_type == 'hue': - light_init = HueLightControl + light_init = get_hue_lights else: - logger.error("Unknown light type specified: %s", light_type) + _LOGGER.error("Unknown light type specified: %s", light_type) return False - light_control = light_init(hass, config[DOMAIN]) + lights = light_init(hass, config[DOMAIN]) + + if len(lights) == 0: + _LOGGER.error("No lights found") + return False ent_to_light = {} - light_to_ent = {} - def _update_light_state(light_id, light_state): - """ Update statemachine based on the LightState passed in. """ - name = light_control.get_name(light_id) or "Unknown Light" + no_name_count = 1 - try: - entity_id = light_to_ent[light_id] - except KeyError: - # We have not seen this light before, set it up + for light in lights: + name = light.get_name() - # Create entity id - logger.info("Found new light %s", name) + if name is None: + name = "Light #{}".format(no_name_count) + no_name_count += 1 - entity_id = util.ensure_unique_string( - ENTITY_ID_FORMAT.format(util.slugify(name)), - list(ent_to_light.keys())) + entity_id = util.ensure_unique_string( + ENTITY_ID_FORMAT.format(util.slugify(name)), + list(ent_to_light.keys())) - ent_to_light[entity_id] = light_id - light_to_ent[light_id] = entity_id - - state_attr = {ATTR_FRIENDLY_NAME: name} - - if light_state.on: - state = STATE_ON - - if light_state.brightness: - state_attr[ATTR_BRIGHTNESS] = light_state.brightness - - if light_state.color: - state_attr[ATTR_XY_COLOR] = light_state.color - - else: - state = STATE_OFF - - hass.states.set(entity_id, state, state_attr) - - def update_light_state(light_id): - """ Update the state of specified light. """ - _update_light_state(light_id, light_control.get(light_id)) - - # pylint: disable=unused-argument - def update_lights_state(time, force_reload=False): - """ Update the state of all the lights. """ - - # First time this method gets called, force_reload should be True - if force_reload or \ - datetime.now() - update_lights_state.last_updated > \ - MIN_TIME_BETWEEN_SCANS: - - logger.info("Updating light status") - update_lights_state.last_updated = datetime.now() - - for light_id, light_state in light_control.gets().items(): - _update_light_state(light_id, light_state) - - # Update light state and discover lights for tracking the group - update_lights_state(None, True) - - if len(ent_to_light) == 0: - logger.error("No lights found") - return False - - # Track all lights in a group - group.setup_group( - hass, GROUP_NAME_ALL_LIGHTS, light_to_ent.values(), False) + light.entity_id = entity_id + ent_to_light[entity_id] = light # Load built-in profiles and custom profiles profile_paths = [os.path.join(os.path.dirname(__file__), @@ -250,28 +203,41 @@ def setup(hass, config): except ValueError: # ValueError if not 4 values per row # ValueError if convert to float/int failed - logger.error( + _LOGGER.error( "Error parsing light profiles from %s", profile_path) return False + # pylint: disable=unused-argument + def update_lights_state(now): + """ Update the states of all the lights. """ + for light in lights: + light.update_ha_state(hass) + + update_lights_state(None) + + # Track all lights in a group + group.setup_group( + hass, GROUP_NAME_ALL_LIGHTS, ent_to_light.keys(), False) + def handle_light_service(service): """ Hande a turn light on or off service call. """ # Get and validate data dat = service.data # Convert the entity ids to valid light ids - light_ids = [ent_to_light[entity_id] for entity_id - in extract_entity_ids(hass, service) - if entity_id in ent_to_light] + lights = [ent_to_light[entity_id] for entity_id + in extract_entity_ids(hass, service) + if entity_id in ent_to_light] - if not light_ids: - light_ids = list(ent_to_light.values()) + if not lights: + lights = list(ent_to_light.values()) transition = util.convert(dat.get(ATTR_TRANSITION), int) if service.service == SERVICE_TURN_OFF: - light_control.turn_light_off(light_ids, transition) + for light in lights: + light.turn_off(transition=transition) else: # Processing extra data for turn light on request @@ -317,14 +283,12 @@ def setup(hass, config): # ValueError if not all values can be converted to int pass - light_control.turn_light_on(light_ids, transition, bright, color) + for light in lights: + light.turn_on(transition=transition, brightness=bright, + xy_color=color) - # Update state of lights touched. If there was only 1 light selected - # then just update that light else update all - if len(light_ids) == 1: - update_light_state(light_ids[0]) - else: - update_lights_state(None, True) + for light in lights: + light.update_ha_state(hass, True) # Update light state every 30 seconds hass.track_time_change(update_lights_state, second=[0, 30]) @@ -339,140 +303,134 @@ def setup(hass, config): return True -LightState = namedtuple("LightState", ['on', 'brightness', 'color']) +def get_hue_lights(hass, config): + """ Gets the Hue lights. """ + host = config.get(ha.CONF_HOST, None) - -def _hue_to_light_state(info): - """ Helper method to convert a Hue state to a LightState. """ try: - return LightState(info['state']['reachable'] and info['state']['on'], - info['state']['bri'], info['state']['xy']) - except KeyError: - # KeyError if one of the keys didn't exist - return None + # Pylint does not play nice if not every folders has an __init__.py + # pylint: disable=no-name-in-module, import-error + import homeassistant.external.phue.phue as phue + except ImportError: + _LOGGER.exception("Hue:Error while importing dependency phue.") + return [] -class HueLightControl(object): - """ Class to interface with the Hue light system. """ + try: + bridge = phue.Bridge( + host, config_file_path=hass.get_config_path(PHUE_CONFIG_FILE)) + except socket.error: # Error connecting using Phue + _LOGGER.exception(( + "Hue:Error while connecting to the bridge. " + "Did you follow the instructions to set it up?")) - def __init__(self, hass, config): - logger = logging.getLogger("{}.{}".format(__name__, "HueLightControl")) + return [] - host = config.get(ha.CONF_HOST, None) + lights = {} + + def update_lights(force_reload=False): + """ Updates the light states. """ + now = datetime.now() try: - # Pylint does not play nice if not every folders has an __init__.py - # pylint: disable=no-name-in-module, import-error - import homeassistant.external.phue.phue as phue - except ImportError: - logger.exception("Error while importing dependency phue.") + time_scans = now - update_lights.last_updated - self.success_init = False - - return - - try: - self._bridge = phue.Bridge(host, - config_file_path=hass.get_config_path( - PHUE_CONFIG_FILE)) - except socket.error: # Error connecting using Phue - logger.exception(( - "Error while connecting to the bridge. " - "Did you follow the instructions to set it up?")) - - self.success_init = False - - return - - # Dict mapping light_id to name - self._lights = {} - self._update_lights() - - if len(self._lights) == 0: - logger.error("Could not find any lights. ") - - self.success_init = False - else: - self.success_init = True - - def _update_lights(self): - """ Helper method to update the known names from Hue. """ - try: - self._lights = {int(item[0]): item[1]['name'] for item - in self._bridge.get_light().items()} - - except (socket.error, KeyError): - # socket.error because sometimes we cannot reach Hue - # KeyError if we got unexpected data - # We don't do anything, keep old values + # force_reload == True, return if updated in last second + # force_reload == False, return if last update was less then + # MIN_TIME_BETWEEN_SCANS ago + if force_reload and time_scans.seconds < 1 or \ + not force_reload and time_scans < MIN_TIME_BETWEEN_SCANS: + return + except AttributeError: + # First time we run last_updated is not set, continue as usual pass - def get_name(self, light_id): - """ Return name for specified light_id or None if no name known. """ - if light_id not in self._lights: - self._update_lights() - - return self._lights.get(light_id) - - def get(self, light_id): - """ Return a LightState representing light light_id. """ - try: - info = self._bridge.get_light(light_id) - - return _hue_to_light_state(info) - - except socket.error: - # socket.error when we cannot reach Hue - return None - - def gets(self): - """ Return a dict with id mapped to LightState objects. """ - states = {} + update_lights.last_updated = now try: - api = self._bridge.get_api() - + api = bridge.get_api() except socket.error: # socket.error when we cannot reach Hue - return states + _LOGGER.exception("Hue:Cannot reach the bridge") + return api_states = api.get('lights') if not isinstance(api_states, dict): - return states + _LOGGER.error("Hue:Got unexpected result from Hue API") + return for light_id, info in api_states.items(): - state = _hue_to_light_state(info) + if light_id not in lights: + lights[light_id] = HueLight(int(light_id), info, + bridge, update_lights) + else: + lights[light_id].info = info - if state: - states[int(light_id)] = state + update_lights() - return states + return list(lights.values()) - def turn_light_on(self, light_ids, transition, brightness, xy_color): + +class HueLight(ToggleDevice): + """ Represents a Hue light """ + + def __init__(self, light_id, info, bridge, update_lights): + self.light_id = light_id + self.info = info + self.bridge = bridge + self.update_lights = update_lights + + def get_name(self): + """ Get the mame of the Hue light. """ + return self.info['name'] + + def turn_on(self, **kwargs): """ Turn the specified or all lights on. """ command = {'on': True} - if transition is not None: + if kwargs.get('transition') is not None: # Transition time is in 1/10th seconds and cannot exceed # 900 seconds. - command['transitiontime'] = min(9000, transition * 10) + command['transitiontime'] = min(9000, kwargs['transition'] * 10) - if brightness is not None: - command['bri'] = brightness + if kwargs.get('brightness') is not None: + command['bri'] = kwargs['brightness'] - if xy_color: - command['xy'] = xy_color + if kwargs.get('xy_color') is not None: + command['xy'] = kwargs['xy_color'] - self._bridge.set_light(light_ids, command) + self.bridge.set_light(self.light_id, command) - def turn_light_off(self, light_ids, transition): + def turn_off(self, **kwargs): """ Turn the specified or all lights off. """ command = {'on': False} - if transition is not None: + if kwargs.get('transition') is not None: # Transition time is in 1/10th seconds and cannot exceed # 900 seconds. - command['transitiontime'] = min(9000, transition * 10) + command['transitiontime'] = min(9000, kwargs['transition'] * 10) - self._bridge.set_light(light_ids, command) + self.bridge.set_light(self.light_id, command) + + def is_on(self): + """ True if device is on. """ + self.update_lights() + + return self.info['state']['reachable'] and self.info['state']['on'] + + def get_state_attributes(self): + """ Returns optional state attributes. """ + attr = { + ATTR_FRIENDLY_NAME: self.get_name() + } + + if self.is_on(): + attr[ATTR_BRIGHTNESS] = self.info['state']['bri'] + attr[ATTR_XY_COLOR] = self.info['state']['xy'] + + return attr + + def update(self): + """ Synchronize state with bridge. """ + self.update_lights(True) diff --git a/homeassistant/components/switch.py b/homeassistant/components/switch.py index ca1bec9ff67..e6c0e46f1ca 100644 --- a/homeassistant/components/switch.py +++ b/homeassistant/components/switch.py @@ -8,10 +8,10 @@ from datetime import datetime, timedelta import homeassistant as ha import homeassistant.util as util -from homeassistant.components import (group, extract_entity_ids, - STATE_ON, STATE_OFF, - SERVICE_TURN_ON, SERVICE_TURN_OFF, - ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME) +from homeassistant.components import ( + ToggleDevice, group, extract_entity_ids, STATE_ON, + SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME) + DOMAIN = 'switch' DEPENDENCIES = [] @@ -143,48 +143,6 @@ def setup(hass, config): return True -class Switch(object): - """ ABC for Switches within Home Assistant. """ - # pylint: disable=no-self-use - - entity_id = None - - def get_name(self): - """ Returns the name of the switch if any. """ - return None - - def turn_on(self, dimming=100): - """ - Turns the switch on. - Dimming is a number between 0-100 and specifies how much switch has - to be dimmed. There is no guarantee that the switch supports dimming. - """ - pass - - def turn_off(self): - """ Turns the switch off. """ - pass - - def is_on(self): - """ True if switch is on. """ - return False - - def get_state_attributes(self): - """ Returns optional state attributes. """ - return None - - def update_ha_state(self, hass): - """ Updates Home Assistant with its current state. """ - if self.entity_id is None: - raise ha.NoEntitySpecifiedError( - "No entity specified for switch {}".format(self.get_name())) - - state = STATE_ON if self.is_on() else STATE_OFF - - return hass.states.set(self.entity_id, state, - self.get_state_attributes()) - - def get_wemo_switches(config): """ Find and return WeMo switches. """ @@ -213,7 +171,7 @@ def get_wemo_switches(config): if isinstance(switch, pywemo.Switch)] -class WemoSwitch(Switch): +class WemoSwitch(ToggleDevice): """ represents a WeMo switch within home assistant. """ def __init__(self, wemo): self.wemo = wemo @@ -223,7 +181,7 @@ class WemoSwitch(Switch): """ Returns the name of the switch if any. """ return self.wemo.name - def turn_on(self, dimming=100): + def turn_on(self, **kwargs): """ Turns the switch on. """ self.wemo.on()