diff --git a/README.md b/README.md index 32071c08ed5..5b83edb2745 100644 --- a/README.md +++ b/README.md @@ -182,13 +182,13 @@ Optional service data: - `rgb_color` - three comma seperated integers that represent the color in RGB - `brightness` - integer between 0 and 255 for how bright the color should be -**wemo** -Keeps track which WeMo switches are in the network, their state and allows you to control them. +**switch** +Keeps track which switches are in the network, their state and allows you to control them. -Registers services `wemo/turn_on` and `wemo/turn_off` to turn a or all wemo switches on or off. +Registers services `switch/turn_on` and `switch/turn_off` to turn a or all switches on or off. Optional service data: - - `entity_id` - only act on specific WeMo switch. Else targets all. + - `entity_id` - only act on specific switch. Else targets all. **device_sun_light_trigger** Turns lights on or off using a light control component based on state of the sun and devices that are home. diff --git a/config/home-assistant.conf.example b/config/home-assistant.conf.example index 38dd4d53f70..1c6dbb2d276 100644 --- a/config/home-assistant.conf.example +++ b/config/home-assistant.conf.example @@ -23,9 +23,9 @@ password=PASSWORD # instead of scanning the network # hosts=192.168.1.9,192.168.1.12 -[wemo] -# Optional: hard code the hosts (comma seperated) to find WeMos -# instead of scanning the network +[switch] +type=wemo +# Optional: hard code the hosts (comma seperated) to avoid scanning the network # hosts=192.168.1.9,192.168.1.12 [downloader] diff --git a/homeassistant/__init__.py b/homeassistant/__init__.py index 3d6d8536912..5e50380b7d6 100644 --- a/homeassistant/__init__.py +++ b/homeassistant/__init__.py @@ -640,7 +640,14 @@ class Timer(threading.Thread): class HomeAssistantError(Exception): """ General Home Assistant exception occured. """ + pass class InvalidEntityFormatError(HomeAssistantError): """ When an invalid formatted entity is encountered. """ + pass + + +class NoEntitySpecifiedError(HomeAssistantError): + """ When no entity is specified. """ + pass diff --git a/homeassistant/components/demo.py b/homeassistant/components/demo.py index a42ae280768..379c89077e2 100644 --- a/homeassistant/components/demo.py +++ b/homeassistant/components/demo.py @@ -74,20 +74,20 @@ def setup(hass, config): group.setup_group(hass, GROUP_NAME_ALL_LIGHTS, lights, False) - # Setup Wemo - wemos = ['wemo.AC', 'wemo.Christmas_Lights'] + # Setup switch + switches = ['switch.AC', 'switch.Christmas_Lights'] - hass.services.register('wemo', SERVICE_TURN_ON, mock_turn_on) - hass.services.register('wemo', SERVICE_TURN_OFF, mock_turn_off) + hass.services.register('switch', SERVICE_TURN_ON, mock_turn_on) + hass.services.register('switch', SERVICE_TURN_OFF, mock_turn_off) - mock_turn_on(ha.ServiceCall('wemo', SERVICE_TURN_ON, - {'entity_id': wemos[0:1]})) - mock_turn_off(ha.ServiceCall('wemo', SERVICE_TURN_OFF, - {'entity_id': wemos[1:]})) + mock_turn_on(ha.ServiceCall('switch', SERVICE_TURN_ON, + {'entity_id': switches[0:1]})) + mock_turn_off(ha.ServiceCall('switch', SERVICE_TURN_OFF, + {'entity_id': switches[1:]})) # Setup room groups - group.setup_group(hass, 'living_room', lights[0:3] + wemos[0:1]) - group.setup_group(hass, 'bedroom', [lights[3]] + wemos[1:]) + group.setup_group(hass, 'living_room', lights[0:3] + switches[0:1]) + group.setup_group(hass, 'bedroom', [lights[3]] + switches[1:]) # Setup process hass.states.set("process.XBMC", STATE_ON) diff --git a/homeassistant/components/http/frontend.py b/homeassistant/components/http/frontend.py index c6093c2354a..d18a0bc1915 100644 --- a/homeassistant/components/http/frontend.py +++ b/homeassistant/components/http/frontend.py @@ -1,2 +1,2 @@ """ DO NOT MODIFY. Auto-generated by build_frontend script """ -VERSION = "eabfdd5cb0e712c8d6d5d837fb9bfeb9" +VERSION = "6d353f9599942124690691fb22c115ee" diff --git a/homeassistant/components/http/www_static/frontend.html b/homeassistant/components/http/www_static/frontend.html index afb1cbd11a2..c06ced766e1 100644 --- a/homeassistant/components/http/www_static/frontend.html +++ b/homeassistant/components/http/www_static/frontend.html @@ -17747,8 +17747,8 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN case "device_tracker": return "social:person"; - case "wemo": - return "settings-input-svideo"; + case "switch": + return "image:flash-on"; case "chromecast": if(state && state != "idle") { diff --git a/homeassistant/components/http/www_static/polymer/domain-icon.html b/homeassistant/components/http/www_static/polymer/domain-icon.html index b6d867d5e49..3210ae45564 100644 --- a/homeassistant/components/http/www_static/polymer/domain-icon.html +++ b/homeassistant/components/http/www_static/polymer/domain-icon.html @@ -24,8 +24,8 @@ case "device_tracker": return "social:person"; - case "wemo": - return "settings-input-svideo"; + case "switch": + return "image:flash-on"; case "chromecast": if(state && state != "idle") { diff --git a/homeassistant/components/switch.py b/homeassistant/components/switch.py new file mode 100644 index 00000000000..ca1bec9ff67 --- /dev/null +++ b/homeassistant/components/switch.py @@ -0,0 +1,240 @@ +""" +homeassistant.components.switch +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Component to interface with various switches that can be controlled remotely. +""" +import logging +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) +DOMAIN = 'switch' +DEPENDENCIES = [] + +GROUP_NAME_ALL_SWITCHES = 'all_switches' +ENTITY_ID_ALL_SWITCHES = group.ENTITY_ID_FORMAT.format( + GROUP_NAME_ALL_SWITCHES) + +ENTITY_ID_FORMAT = DOMAIN + '.{}' + +ATTR_TODAY_KWH = "today_kwh" +ATTR_CURRENT_POWER = "current_power" +ATTR_TODAY_ON_TIME = "today_on_time" +ATTR_TODAY_STANDBY_TIME = "today_standby_time" + +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) + +_LOGGER = logging.getLogger(__name__) + + +def is_on(hass, entity_id=None): + """ Returns if the switch is on based on the statemachine. """ + entity_id = entity_id or ENTITY_ID_ALL_SWITCHES + + return hass.states.is_state(entity_id, STATE_ON) + + +def turn_on(hass, entity_id=None): + """ Turns all or specified switch on. """ + data = {ATTR_ENTITY_ID: entity_id} if entity_id else None + + hass.call_service(DOMAIN, SERVICE_TURN_ON, data) + + +def turn_off(hass, entity_id=None): + """ Turns all or specified switch off. """ + data = {ATTR_ENTITY_ID: entity_id} if entity_id else None + + hass.call_service(DOMAIN, SERVICE_TURN_OFF, data) + + +# pylint: disable=too-many-branches +def setup(hass, config): + """ Track states and offer events for switches. """ + logger = logging.getLogger(__name__) + + if not util.validate_config(config, {DOMAIN: [ha.CONF_TYPE]}, logger): + return False + + switch_type = config[DOMAIN][ha.CONF_TYPE] + + if switch_type == 'wemo': + switch_init = get_wemo_switches + + else: + logger.error("Unknown switch type specified: %s", switch_type) + + return False + + switches = switch_init(config[DOMAIN]) + + if len(switches) == 0: + logger.error("No switches found") + return False + + # Setup a dict mapping entity IDs to devices + ent_to_switch = {} + + no_name_count = 1 + + for switch in switches: + name = switch.get_name() + + if name is None: + name = "Switch #{}".format(no_name_count) + no_name_count += 1 + + entity_id = util.ensure_unique_string( + ENTITY_ID_FORMAT.format(util.slugify(name)), + list(ent_to_switch.keys())) + + switch.entity_id = entity_id + ent_to_switch[entity_id] = switch + + # pylint: disable=unused-argument + def update_states(time, force_reload=False): + """ Update states of all switches. """ + + # First time this method gets called, force_reload should be True + if force_reload or \ + datetime.now() - update_states.last_updated > \ + MIN_TIME_BETWEEN_SCANS: + + logger.info("Updating switch states") + update_states.last_updated = datetime.now() + + for switch in switches: + switch.update_ha_state(hass) + + update_states(None, True) + + def handle_switch_service(service): + """ Handles calls to the switch services. """ + devices = [ent_to_switch[entity_id] for entity_id + in extract_entity_ids(hass, service) + if entity_id in ent_to_switch] + + if not devices: + devices = switches + + for switch in devices: + if service.service == SERVICE_TURN_ON: + switch.turn_on() + else: + switch.turn_off() + + switch.update_ha_state(hass) + + # Track all wemos in a group + group.setup_group(hass, GROUP_NAME_ALL_SWITCHES, + ent_to_switch.keys(), False) + + # Update state every 30 seconds + hass.track_time_change(update_states, second=[0, 30]) + + hass.services.register(DOMAIN, SERVICE_TURN_OFF, handle_switch_service) + + hass.services.register(DOMAIN, SERVICE_TURN_ON, handle_switch_service) + + 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. """ + + 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.pywemo.pywemo as pywemo + except ImportError: + _LOGGER.exception(( + "Wemo:Failed to import pywemo. " + "Did you maybe not run `git submodule init` " + "and `git submodule update`?")) + + return [] + + if ha.CONF_HOSTS in config: + switches = (pywemo.device_from_host(host) for host + in config[ha.CONF_HOSTS].split(",")) + + else: + _LOGGER.info("Scanning for WeMo devices") + switches = pywemo.discover_devices() + + # Filter out the switches and wrap in WemoSwitch object + return [WemoSwitch(switch) for switch in switches + if isinstance(switch, pywemo.Switch)] + + +class WemoSwitch(Switch): + """ represents a WeMo switch within home assistant. """ + def __init__(self, wemo): + self.wemo = wemo + self.state_attr = {ATTR_FRIENDLY_NAME: wemo.name} + + def get_name(self): + """ Returns the name of the switch if any. """ + return self.wemo.name + + def turn_on(self, dimming=100): + """ Turns the switch on. """ + self.wemo.on() + + def turn_off(self): + """ Turns the switch off. """ + self.wemo.off() + + def is_on(self): + """ True if switch is on. """ + return self.wemo.get_state(True) + + def get_state_attributes(self): + """ Returns optional state attributes. """ + return self.state_attr diff --git a/homeassistant/components/wemo.py b/homeassistant/components/wemo.py deleted file mode 100644 index 66b625402f1..00000000000 --- a/homeassistant/components/wemo.py +++ /dev/null @@ -1,171 +0,0 @@ -""" -Component to interface with WeMo devices on the network. -""" -import logging -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) -DOMAIN = 'wemo' -DEPENDENCIES = [] - -GROUP_NAME_ALL_WEMOS = 'all_wemos' -ENTITY_ID_ALL_WEMOS = group.ENTITY_ID_FORMAT.format( - GROUP_NAME_ALL_WEMOS) - -ENTITY_ID_FORMAT = DOMAIN + '.{}' - -ATTR_TODAY_KWH = "today_kwh" -ATTR_CURRENT_POWER = "current_power" -ATTR_TODAY_ON_TIME = "today_on_time" -ATTR_TODAY_STANDBY_TIME = "today_standby_time" - -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) - - -def is_on(hass, entity_id=None): - """ Returns if the wemo is on based on the statemachine. """ - entity_id = entity_id or ENTITY_ID_ALL_WEMOS - - return hass.states.is_state(entity_id, STATE_ON) - - -def turn_on(hass, entity_id=None): - """ Turns all or specified wemo on. """ - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - - hass.call_service(DOMAIN, SERVICE_TURN_ON, data) - - -def turn_off(hass, entity_id=None): - """ Turns all or specified wemo off. """ - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - - hass.call_service(DOMAIN, SERVICE_TURN_OFF, data) - - -# pylint: disable=too-many-branches -def setup(hass, config): - """ Track states and offer events for WeMo switches. """ - logger = logging.getLogger(__name__) - - 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.pywemo.pywemo as pywemo - except ImportError: - logger.exception(( - "Failed to import pywemo. " - "Did you maybe not run `git submodule init` " - "and `git submodule update`?")) - - return False - - if ha.CONF_HOSTS in config[DOMAIN]: - devices = [] - - for host in config[DOMAIN][ha.CONF_HOSTS].split(","): - device = pywemo.device_from_host(host) - - if device: - devices.append(device) - - else: - logger.info("Scanning for WeMo devices") - devices = pywemo.discover_devices() - - is_switch = lambda switch: isinstance(switch, pywemo.Switch) - - switches = [device for device in devices if is_switch(device)] - - if len(switches) == 0: - logger.error("No WeMo switches found") - return False - - # Dict mapping serial no to entity IDs - sno_to_ent = {} - # Dict mapping entity IDs to devices - ent_to_dev = {} - - def update_wemo_state(device): - """ Update the state of specified WeMo device. """ - - # We currently only support switches - if not is_switch(device): - return - - try: - entity_id = sno_to_ent[device.serialnumber] - - except KeyError: - # New device, set it up - entity_id = util.ensure_unique_string( - ENTITY_ID_FORMAT.format(util.slugify(device.name)), - list(ent_to_dev.keys())) - - sno_to_ent[device.serialnumber] = entity_id - ent_to_dev[entity_id] = device - - state = STATE_ON if device.get_state(True) else STATE_OFF - - state_attr = {ATTR_FRIENDLY_NAME: device.name} - - if isinstance(device, pywemo.Insight): - pass - # Should work but doesn't.. - #state_attr[ATTR_TODAY_KWH] = device.today_kwh - #state_attr[ATTR_CURRENT_POWER] = device.current_power - #state_attr[ATTR_TODAY_ON_TIME] = device.today_on_time - #state_attr[ATTR_TODAY_STANDBY_TIME] = device.today_standby_time - - hass.states.set(entity_id, state, state_attr) - - # pylint: disable=unused-argument - def update_wemos_state(time, force_reload=False): - """ Update states of all WeMo devices. """ - - # First time this method gets called, force_reload should be True - if force_reload or \ - datetime.now() - update_wemos_state.last_updated > \ - MIN_TIME_BETWEEN_SCANS: - - logger.info("Updating WeMo status") - update_wemos_state.last_updated = datetime.now() - - for device in switches: - update_wemo_state(device) - - update_wemos_state(None, True) - - # Track all wemos in a group - group.setup_group(hass, GROUP_NAME_ALL_WEMOS, sno_to_ent.values(), False) - - def handle_wemo_service(service): - """ Handles calls to the WeMo service. """ - devices = [ent_to_dev[entity_id] for entity_id - in extract_entity_ids(hass, service) - if entity_id in ent_to_dev] - - if not devices: - devices = ent_to_dev.values() - - for device in devices: - if service.service == SERVICE_TURN_ON: - device.on() - else: - device.off() - - update_wemo_state(device) - - # Update WeMo state every 30 seconds - hass.track_time_change(update_wemos_state, second=[0, 30]) - - hass.services.register(DOMAIN, SERVICE_TURN_OFF, handle_wemo_service) - - hass.services.register(DOMAIN, SERVICE_TURN_ON, handle_wemo_service) - - return True