diff --git a/config/home-assistant.conf.example b/config/home-assistant.conf.example index 83a92140e3d..0dc87732864 100644 --- a/config/home-assistant.conf.example +++ b/config/home-assistant.conf.example @@ -11,6 +11,10 @@ api_password=mypass [light] platform=hue +[wink] +# Get your token at https://winkbearertoken.appspot.com +token=YOUR_TOKEN + [device_tracker] # The following types are available: netgear, tomato, luci, nmap_tracker platform=netgear diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 99c13c63794..efa3aa47cbf 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -15,15 +15,13 @@ from homeassistant.external.netdisco.netdisco import DiscoveryService import homeassistant.external.netdisco.netdisco.const as services from homeassistant import bootstrap -from homeassistant.const import EVENT_HOMEASSISTANT_START, ATTR_SERVICE +from homeassistant.const import ( + EVENT_HOMEASSISTANT_START, EVENT_SERVICE_DISCOVERED, + ATTR_SERVICE, ATTR_DISCOVERED) DOMAIN = "discovery" DEPENDENCIES = [] -EVENT_SERVICE_DISCOVERED = "service_discovered" - -ATTR_DISCOVERED = "discovered" - SCAN_INTERVAL = 300 # seconds SERVICE_HANDLERS = { @@ -39,8 +37,10 @@ def listen(hass, service, callback): Service can be a string or a list/tuple. """ - if not isinstance(service, str): + if isinstance(service, str): service = (service,) + else: + service = tuple(service) def discovery_event_listener(event): """ Listens for discovery events. """ diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 6a8b97f363e..b169c0d44d5 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -52,12 +52,13 @@ import logging import os import csv +from homeassistant.loader import get_component import homeassistant.util as util from homeassistant.const import ( STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID) from homeassistant.helpers import ( extract_entity_ids, platform_devices_from_config) -from homeassistant.components import group +from homeassistant.components import group, discovery, wink DOMAIN = "light" @@ -87,9 +88,13 @@ ATTR_FLASH = "flash" FLASH_SHORT = "short" FLASH_LONG = "long" - LIGHT_PROFILES_FILE = "light_profiles.csv" +# Maps discovered services to their platforms +DISCOVERY_PLATFORMS = { + wink.DISCOVER_LIGHTS: 'wink', +} + _LOGGER = logging.getLogger(__name__) @@ -166,19 +171,41 @@ def setup(hass, config): lights = platform_devices_from_config( config, DOMAIN, hass, ENTITY_ID_FORMAT, _LOGGER) - if not lights: - return False - # pylint: disable=unused-argument def update_lights_state(now): """ Update the states of all the lights. """ - for light in lights.values(): - light.update_ha_state(hass) + if lights: + _LOGGER.info("Updating light states") + + for light in lights.values(): + light.update_ha_state(hass) update_lights_state(None) # Track all lights in a group - group.Group(hass, GROUP_NAME_ALL_LIGHTS, lights.keys(), False) + light_group = group.Group( + hass, GROUP_NAME_ALL_LIGHTS, lights.keys(), False) + + def light_discovered(service, info): + """ Called when a light is discovered. """ + platform = get_component( + "{}.{}".format(DOMAIN, DISCOVERY_PLATFORMS[service])) + + discovered = platform.devices_discovered(hass, config, info) + + for light in discovered: + if light is not None and light not in lights.values(): + light.entity_id = util.ensure_unique_string( + ENTITY_ID_FORMAT.format(util.slugify(light.get_name())), + lights.keys()) + + lights[light.entity_id] = light + + light.update_ha_state(hass) + + light_group.update_tracked_entity_ids(lights.keys()) + + discovery.listen(hass, DISCOVERY_PLATFORMS.keys(), light_discovered) def handle_light_service(service): """ Hande a turn light on or off service call. """ diff --git a/homeassistant/components/light/wink.py b/homeassistant/components/light/wink.py new file mode 100644 index 00000000000..061ff06b7f8 --- /dev/null +++ b/homeassistant/components/light/wink.py @@ -0,0 +1,63 @@ +""" Support for Hue lights. """ +import logging + +# pylint: disable=no-name-in-module, import-error +import homeassistant.external.wink.pywink as pywink + +from homeassistant.helpers import ToggleDevice +from homeassistant.const import ATTR_FRIENDLY_NAME, CONF_ACCESS_TOKEN + + +# pylint: disable=unused-argument +def get_devices(hass, config): + """ Find and return Wink lights. """ + token = config.get(CONF_ACCESS_TOKEN) + + if token is None: + logging.getLogger(__name__).error( + "Missing wink access_token - " + "get one at https://winkbearertoken.appspot.com/") + return [] + + pywink.set_bearer_token(token) + + return get_lights() + + +# pylint: disable=unused-argument +def devices_discovered(hass, config, info): + """ Called when a device is discovered. """ + return get_lights() + + +def get_lights(): + """ Returns the Wink switches. """ + return [WinkLight(light) for light in pywink.get_bulbs()] + + +class WinkLight(ToggleDevice): + """ Represents a Wink light """ + + def __init__(self, wink): + self.wink = wink + self.state_attr = {ATTR_FRIENDLY_NAME: wink.name()} + + def get_name(self): + """ Returns the name of the light if any. """ + return self.wink.name() + + def turn_on(self, **kwargs): + """ Turns the light on. """ + self.wink.setState(True) + + def turn_off(self): + """ Turns the light off. """ + self.wink.setState(False) + + def is_on(self): + """ True if light is on. """ + return self.wink.state() + + def get_state_attributes(self): + """ Returns optional state attributes. """ + return self.state_attr diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index c601fad92f7..13bfa075932 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -12,7 +12,7 @@ from homeassistant.const import ( STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID) from homeassistant.helpers import ( extract_entity_ids, platform_devices_from_config) -from homeassistant.components import group, discovery +from homeassistant.components import group, discovery, wink DOMAIN = 'switch' DEPENDENCIES = [] @@ -29,8 +29,9 @@ ATTR_CURRENT_POWER_MWH = "current_power_mwh" MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) # Maps discovered services to their platforms -DISCOVERY = { - discovery.services.BELKIN_WEMO: 'wemo' +DISCOVERY_PLATFORMS = { + discovery.services.BELKIN_WEMO: 'wemo', + wink.DISCOVER_SWITCHES: 'wink', } _LOGGER = logging.getLogger(__name__) @@ -82,22 +83,24 @@ def setup(hass, config): def switch_discovered(service, info): """ Called when a switch is discovered. """ - platform = get_component("{}.{}".format(DOMAIN, DISCOVERY[service])) + platform = get_component("{}.{}".format( + DOMAIN, DISCOVERY_PLATFORMS[service])) - switch = platform.device_discovered(hass, config, info) + discovered = platform.devices_discovered(hass, config, info) - if switch is not None and switch not in switches.values(): - switch.entity_id = util.ensure_unique_string( - ENTITY_ID_FORMAT.format(util.slugify(switch.name)), - switches.keys()) + for switch in discovered: + if switch is not None and switch not in switches.values(): + switch.entity_id = util.ensure_unique_string( + ENTITY_ID_FORMAT.format(util.slugify(switch.name)), + switches.keys()) - switches[switch.entity_id] = switch + switches[switch.entity_id] = switch - switch.update_ha_state(hass) + switch.update_ha_state(hass) - switch_group.update_tracked_entity_ids(switches.keys()) + switch_group.update_tracked_entity_ids(switches.keys()) - discovery.listen(hass, discovery.services.BELKIN_WEMO, switch_discovered) + discovery.listen(hass, DISCOVERY_PLATFORMS.keys(), switch_discovered) def handle_switch_service(service): """ Handles calls to the switch services. """ diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py index 3c3db895713..bb6290526f7 100644 --- a/homeassistant/components/switch/wemo.py +++ b/homeassistant/components/switch/wemo.py @@ -24,16 +24,16 @@ def get_devices(hass, config): if isinstance(switch, pywemo.Switch)] -def device_discovered(hass, config, info): +def devices_discovered(hass, config, info): """ Called when a device is discovered. """ _, discovery = get_pywemo() if discovery is None: - return + return [] device = discovery.device_from_description(info) - return None if device is None else WemoSwitch(device) + return [] if device is None else [WemoSwitch(device)] def get_pywemo(): diff --git a/homeassistant/components/switch/wink.py b/homeassistant/components/switch/wink.py new file mode 100644 index 00000000000..edc404781fe --- /dev/null +++ b/homeassistant/components/switch/wink.py @@ -0,0 +1,63 @@ +""" Support for WeMo switchces. """ +import logging + +# pylint: disable=no-name-in-module, import-error +import homeassistant.external.wink.pywink as pywink + +from homeassistant.helpers import ToggleDevice +from homeassistant.const import ATTR_FRIENDLY_NAME, CONF_ACCESS_TOKEN + + +# pylint: disable=unused-argument +def get_devices(hass, config): + """ Find and return Wink switches. """ + token = config.get(CONF_ACCESS_TOKEN) + + if token is None: + logging.getLogger(__name__).error( + "Missing wink access_token - " + "get one at https://winkbearertoken.appspot.com/") + return [] + + pywink.set_bearer_token(token) + + return get_switches() + + +# pylint: disable=unused-argument +def devices_discovered(hass, config, info): + """ Called when a device is discovered. """ + return get_switches() + + +def get_switches(): + """ Returns the Wink switches. """ + return [WinkSwitch(switch) for switch in pywink.get_switches()] + + +class WinkSwitch(ToggleDevice): + """ represents a WeMo switch within home assistant. """ + + def __init__(self, wink): + self.wink = wink + self.state_attr = {ATTR_FRIENDLY_NAME: wink.name()} + + def get_name(self): + """ Returns the name of the switch if any. """ + return self.wink.name() + + def turn_on(self, **kwargs): + """ Turns the switch on. """ + self.wink.setState(True) + + def turn_off(self): + """ Turns the switch off. """ + self.wink.setState(False) + + def is_on(self): + """ True if switch is on. """ + return self.wink.state() + + def get_state_attributes(self): + """ Returns optional state attributes. """ + return self.state_attr diff --git a/homeassistant/components/wink.py b/homeassistant/components/wink.py new file mode 100644 index 00000000000..dd7f5e35942 --- /dev/null +++ b/homeassistant/components/wink.py @@ -0,0 +1,51 @@ +""" +Connects to a Wink hub and loads relevant components to control its devices. +""" +import logging + +# pylint: disable=no-name-in-module, import-error +import homeassistant.external.wink.pywink as pywink + +from homeassistant import bootstrap +from homeassistant.loader import get_component +from homeassistant.helpers import validate_config +from homeassistant.const import ( + EVENT_SERVICE_DISCOVERED, ATTR_SERVICE, ATTR_DISCOVERED, CONF_ACCESS_TOKEN) + +DOMAIN = "wink" +DEPENDENCIES = [] + +DISCOVER_LIGHTS = "wink.lights" +DISCOVER_SWITCHES = "wink.switches" + + +def setup(hass, config): + """ Sets up the Wink component. """ + logger = logging.getLogger(__name__) + + if not validate_config(config, {DOMAIN: [CONF_ACCESS_TOKEN]}, logger): + return False + + pywink.set_bearer_token(config[DOMAIN][CONF_ACCESS_TOKEN]) + + # Load components for the devices in the Wink that we support + for component_name, func_exists, discovery_type in ( + ('light', pywink.get_bulbs, DISCOVER_LIGHTS), + ('switch', pywink.get_switches, DISCOVER_SWITCHES)): + + if func_exists(): + component = get_component(component_name) + + # Ensure component is loaded + if component.DOMAIN not in hass.components: + # Add a worker on succesfull setup + if bootstrap.setup_component(hass, component.DOMAIN, config): + hass.pool.add_worker() + + # Fire discovery event + hass.bus.fire(EVENT_SERVICE_DISCOVERED, { + ATTR_SERVICE: discovery_type, + ATTR_DISCOVERED: {} + }) + + return True diff --git a/homeassistant/const.py b/homeassistant/const.py index f1a14b35de7..06b5ce8428e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -18,6 +18,7 @@ CONF_HOSTS = "hosts" CONF_USERNAME = "username" CONF_PASSWORD = "password" CONF_API_KEY = "api_key" +CONF_ACCESS_TOKEN = "access_token" # #### EVENTS #### EVENT_HOMEASSISTANT_START = "homeassistant_start" @@ -26,6 +27,7 @@ EVENT_STATE_CHANGED = "state_changed" EVENT_TIME_CHANGED = "time_changed" EVENT_CALL_SERVICE = "call_service" EVENT_SERVICE_EXECUTED = "service_executed" +EVENT_SERVICE_DISCOVERED = "service_discovered" # #### STATES #### STATE_ON = 'on' @@ -64,6 +66,9 @@ ATTR_TEMPERATURE = "temperature" TEMP_CELCIUS = "°C" TEMP_FAHRENHEIT = "°F" +# Contains the information that is discovered +ATTR_DISCOVERED = "discovered" + # #### SERVICES #### SERVICE_HOMEASSISTANT_STOP = "stop" diff --git a/homeassistant/external/wink/pywink.py b/homeassistant/external/wink/pywink.py new file mode 100644 index 00000000000..3131873a9e2 --- /dev/null +++ b/homeassistant/external/wink/pywink.py @@ -0,0 +1,274 @@ +__author__ = 'JOHNMCL' + +import json + +import requests + + +baseUrl = "https://winkapi.quirky.com" + +object_type = "light_bulb" +object_type_plural = "light_bulbs" + +bearer_token="" + +headers = {} + + +class wink_binary_switch(): + """ represents a wink.py switch + json_obj holds the json stat at init (and if there is a refresh it's updated + it's the native format for this objects methods + and looks like so: + +{ + "data": { + "binary_switch_id": "4153", + "name": "Garage door indicator", + "locale": "en_us", + "units": {}, + "created_at": 1411614982, + "hidden_at": null, + "capabilities": {}, + "subscription": {}, + "triggers": [], + "desired_state": { + "powered": false + }, + "manufacturer_device_model": "leviton_dzs15", + "manufacturer_device_id": null, + "device_manufacturer": "leviton", + "model_name": "Switch", + "upc_id": "94", + "gang_id": null, + "hub_id": "11780", + "local_id": "9", + "radio_type": "zwave", + "last_reading": { + "powered": false, + "powered_updated_at": 1411614983.6153464, + "powering_mode": null, + "powering_mode_updated_at": null, + "consumption": null, + "consumption_updated_at": null, + "cost": null, + "cost_updated_at": null, + "budget_percentage": null, + "budget_percentage_updated_at": null, + "budget_velocity": null, + "budget_velocity_updated_at": null, + "summation_delivered": null, + "summation_delivered_updated_at": null, + "sum_delivered_multiplier": null, + "sum_delivered_multiplier_updated_at": null, + "sum_delivered_divisor": null, + "sum_delivered_divisor_updated_at": null, + "sum_delivered_formatting": null, + "sum_delivered_formatting_updated_at": null, + "sum_unit_of_measure": null, + "sum_unit_of_measure_updated_at": null, + "desired_powered": false, + "desired_powered_updated_at": 1417893563.7567682, + "desired_powering_mode": null, + "desired_powering_mode_updated_at": null + }, + "current_budget": null, + "lat_lng": [ + 38.429996, + -122.653721 + ], + "location": "", + "order": 0 + }, + "errors": [], + "pagination": {} +} + + """ + jsonState = {} + + + + def __init__(self, aJSonObj): + self.jsonState = aJSonObj + self.objectprefix = "binary_switches" + + + def __str__(self): + return "%s %s %s" % (self.name(), self.deviceId(), self.state()) + + def __repr__(self): + return "" % (self.name(), self.deviceId(), self.state()) + + def name(self): + name = self.jsonState.get('name') + return name or "Unknown Name" + + def state(self): + state = self.jsonState.get('desired_state').get('powered') + return state + + def setState(self, state): + """ + :param state: a boolean of true (on) or false ('off') + :return: nothing + """ + urlString = baseUrl + "/%s/%s" % (self.objectprefix, self.deviceId()) + values = {"desired_state": {"powered": state}} + urlString = baseUrl + "/%s/%s" % (self.objectprefix, self.deviceId()) + arequest = requests.put(urlString, data=json.dumps(values), headers=headers) + self._updateStateFromResponse(arequest.json()) + + + def deviceId(self): + deviceId = self.jsonState.get('binary_switch_id') + return deviceId or "Unknown Device ID" + + def updateState(self): + urlString = baseUrl + "/%s/%s" % (self.objectprefix, self.deviceId()) + arequest = requests.get(urlString, headers=headers) + self._updateStateFromResponse(arequest.json()) + + def _updateStateFromResponse(self, response_json): + """ + :param response_json: the json obj returned from query + :return: + """ + self.jsonState = response_json.get('data') + +class wink_bulb(wink_binary_switch): + """ represents a wink.py bulb + json_obj holds the json stat at init (and if there is a refresh it's updated + it's the native format for this objects methods + and looks like so: + + "light_bulb_id": "33990", + "name": "downstaurs lamp", + "locale": "en_us", + "units":{}, + "created_at": 1410925804, + "hidden_at": null, + "capabilities":{}, + "subscription":{}, + "triggers":[], + "desired_state":{"powered": true, "brightness": 1}, + "manufacturer_device_model": "lutron_p_pkg1_w_wh_d", + "manufacturer_device_id": null, + "device_manufacturer": "lutron", + "model_name": "Caseta Wireless Dimmer & Pico", + "upc_id": "3", + "hub_id": "11780", + "local_id": "8", + "radio_type": "lutron", + "linked_service_id": null, + "last_reading":{ + "brightness": 1, + "brightness_updated_at": 1417823487.490747, + "connection": true, + "connection_updated_at": 1417823487.4907365, + "powered": true, + "powered_updated_at": 1417823487.4907532, + "desired_powered": true, + "desired_powered_updated_at": 1417823485.054675, + "desired_brightness": 1, + "desired_brightness_updated_at": 1417409293.2591703 + }, + "lat_lng":[38.429962, -122.653715], + "location": "", + "order": 0 + + """ + jsonState = {} + + def __init__(self, ajsonobj): + self.jsonState = ajsonobj + self.objectprefix = "light_bulbs" + + def __str__(self): + return "%s %s %s" % (self.name(), self.deviceId(), self.state()) + + def __repr__(self): + return "" % (self.name(), self.deviceId(), self.state()) + + def name(self): + name = self.jsonState.get('name') + return name or "Unknown Name" + + def state(self): + state = self.jsonState.get('desired_state').get('powered') + return state + + def setState(self, state): + """ + :param state: a boolean of true (on) or false ('off') + :return: nothing + """ + urlString = baseUrl + "/light_bulbs/%s" % self.deviceId() + values = {"desired_state": {"desired_powered": state, "powered": state}} + urlString = baseUrl + "/light_bulbs/%s" % self.deviceId() + arequest = requests.put(urlString, data=json.dumps(values), headers=headers) + + self.updateState() + + + def deviceId(self): + deviceId = self.jsonState.get('light_bulb_id') + return deviceId or "Unknown Device ID" + + +def get_bulbs_and_switches(): + arequestUrl = baseUrl + "/users/me/wink_devices" + j = requests.get(arequestUrl, headers=headers).json() + + items = j.get('data') + + switches = [] + for item in items: + id = item.get('light_bulb_id') + if id != None: + switches.append(wink_bulb(item)) + id = item.get('binary_switch_id') + if id != None: + switches.append(wink_binary_switch(item)) + + return switches + + +def get_bulbs(): + arequestUrl = baseUrl + "/users/me/wink_devices" + j = requests.get(arequestUrl, headers=headers).json() + + items = j.get('data') + + switches = [] + for item in items: + id = item.get('light_bulb_id') + if id != None: + switches.append(wink_bulb(item)) + + return switches + + +def get_switches(): + arequestUrl = baseUrl + "/users/me/wink_devices" + j = requests.get(arequestUrl, headers=headers).json() + + items = j.get('data') + + switches = [] + for item in items: + id = item.get('binary_switch_id') + if id != None: + switches.append(wink_binary_switch(item)) + + return switches + +def set_bearer_token(token): + global headers + bearer_token=token + headers={"Content-Type": "application/json", "Authorization": "Bearer {}".format(token)} + +if __name__ == "__main__": + sw = get_bulbs() + lamp = sw[3] + lamp.setState(False) diff --git a/tests/test_component_light.py b/tests/test_component_light.py index 84fb07d6427..c5db2f37299 100644 --- a/tests/test_component_light.py +++ b/tests/test_component_light.py @@ -214,28 +214,6 @@ class TestLight(unittest.TestCase): light.ATTR_XY_COLOR: [prof_x, prof_y]}, data) - def test_setup(self): - """ Test the setup method. """ - # Bogus config - self.assertFalse(light.setup(self.hass, {})) - - self.assertFalse(light.setup(self.hass, {light.DOMAIN: {}})) - - # Test with non-existing component - self.assertFalse(light.setup( - self.hass, {light.DOMAIN: {CONF_TYPE: 'nonexisting'}} - )) - - # Test if light component returns 0 lightes - platform = loader.get_component('light.test') - platform.init(True) - - self.assertEqual([], platform.get_lights(None, None)) - - self.assertFalse(light.setup( - self.hass, {light.DOMAIN: {CONF_TYPE: 'test'}} - )) - def test_light_profiles(self): """ Test light profiles. """ platform = loader.get_component('light.test')