diff --git a/demo.py b/demo.py new file mode 100644 index 0000000..75b389a --- /dev/null +++ b/demo.py @@ -0,0 +1,6 @@ +import pywink +pywink.set_bearer_token('d7cbda4bf96789e3270c332ebf6a6d5c') + +for switch in pywink.get_bulbs(): + print("Toggling {} to {}".format(switch.name(), switch.state())) + # switch.set_state(not switch.state()) diff --git a/pywink/__init__.py b/pywink/__init__.py index 28d575c..cce7d2a 100644 --- a/pywink/__init__.py +++ b/pywink/__init__.py @@ -1,654 +1,10 @@ """ Objects for interfacing with the Wink API """ -import logging -import json -import time -import requests - from pywink import device_types +from pywink.sensor import WinkSensorPod BASE_URL = "https://winkapi.quirky.com" HEADERS = {} -DEVICE_ID_KEYS = { - device_types.BINARY_SWITCH: 'binary_switch_id', - device_types.EGG_TRAY: 'eggtray_id', - device_types.GARAGE_DOOR: 'garage_door_id', - device_types.LIGHT_BULB: 'light_bulb_id', - device_types.LOCK: 'lock_id', - device_types.POWER_STRIP: 'powerstrip_id', - device_types.SENSOR_POD: 'sensor_pod_id', - device_types.SIREN: 'siren_id' -} - - -class WinkDevice(object): - @staticmethod - def factory(device_state_as_json): - - new_object = None - - # pylint: disable=redefined-variable-type - # These objects all share the same base class: WinkDevice - - if "light_bulb_id" in device_state_as_json: - new_object = WinkBulb(device_state_as_json) - elif "sensor_pod_id" in device_state_as_json: - new_object = WinkSensorPod(device_state_as_json) - elif "binary_switch_id" in device_state_as_json: - new_object = WinkBinarySwitch(device_state_as_json) - elif "outlet_id" in device_state_as_json: - new_object = WinkPowerStripOutlet(device_state_as_json) - elif "lock_id" in device_state_as_json: - new_object = WinkLock(device_state_as_json) - elif "eggtray_id" in device_state_as_json: - new_object = WinkEggTray(device_state_as_json) - elif "garage_door_id" in device_state_as_json: - new_object = WinkGarageDoor(device_state_as_json) - elif "siren_id" in device_state_as_json: - new_object = WinkSiren(device_state_as_json) - - return new_object or WinkDevice(device_state_as_json) - - def __init__(self, device_state_as_json, objectprefix=None): - self.objectprefix = objectprefix - self.json_state = device_state_as_json - - def __str__(self): - return "%s %s %s" % (self.name(), self.device_id(), self.state()) - - def __repr__(self): - return "".format(name=self.name(), - device=self.device_id(), - state=self.state()) - - def name(self): - return self.json_state.get('name', "Unknown Name") - - def state(self): - raise NotImplementedError("Must implement state") - - def device_id(self): - raise NotImplementedError("Must implement device_id") - - @property - def _last_reading(self): - return self.json_state.get('last_reading') or {} - - def _update_state_from_response(self, response_json): - """ - :param response_json: the json obj returned from query - :return: - """ - self.json_state = response_json.get('data') - - def update_state(self): - """ Update state with latest info from Wink API. """ - url_string = "{}/{}/{}".format(BASE_URL, - self.objectprefix, self.device_id()) - arequest = requests.get(url_string, headers=HEADERS) - self._update_state_from_response(arequest.json()) - - def refresh_state_at_hub(self): - """ - Tell hub to query latest status from device and upload to Wink. - PS: Not sure if this even works.. - """ - url_string = "{}/{}/{}/refresh".format(BASE_URL, - self.objectprefix, - self.device_id()) - requests.get(url_string, headers=HEADERS) - - -class WinkEggTray(WinkDevice): - """ represents a wink.py egg tray - json_obj holds the json stat at init (if there is a refresh it's updated) - it's the native format for this objects methods - """ - - def __init__(self, device_state_as_json, objectprefix="eggtrays"): - super(WinkEggTray, self).__init__(device_state_as_json, - objectprefix=objectprefix) - - def __repr__(self): - return "".format(name=self.name(), - device=self.device_id(), - state=self.state()) - - def state(self): - if 'inventory' in self._last_reading: - return self._last_reading['inventory'] - return False - - def device_id(self): - return self.json_state.get('eggtray_id', self.name()) - - -class WinkSensorPod(WinkDevice): - """ represents a wink.py sensor - json_obj holds the json stat at init (if there is a refresh it's updated) - it's the native format for this objects methods - and looks like so: - """ - - def __init__(self, device_state_as_json, objectprefix="sensor_pods"): - super(WinkSensorPod, self).__init__(device_state_as_json, - objectprefix=objectprefix) - - def __repr__(self): - return "" % (self.name(), - self.device_id(), self.state()) - - def state(self): - if 'opened' in self._last_reading: - return self._last_reading['opened'] - elif 'motion' in self._last_reading: - return self._last_reading['motion'] - return False - - def device_id(self): - return self.json_state.get('sensor_pod_id', self.name()) - - -class WinkBinarySwitch(WinkDevice): - """ represents a wink.py switch - json_obj holds the json stat at init (if there is a refresh it's updated) - it's the native format for this objects methods - """ - - def __init__(self, device_state_as_json, objectprefix="binary_switches"): - super(WinkBinarySwitch, self).__init__(device_state_as_json, - objectprefix=objectprefix) - # Tuple (desired state, time) - self._last_call = (0, None) - - def __repr__(self): - return "" % (self.name(), - self.device_id(), self.state()) - - def state(self): - if not self._last_reading.get('connection', False): - return False - # Optimistic approach to setState: - # Within 15 seconds of a call to setState we assume it worked. - if self._recent_state_set(): - return self._last_call[1] - - return self._last_reading.get('powered', False) - - def device_id(self): - return self.json_state.get('binary_switch_id', self.name()) - - # pylint: disable=unused-argument - # kwargs is unused here but is used by child implementations - def set_state(self, state, **kwargs): - """ - :param state: a boolean of true (on) or false ('off') - :return: nothing - """ - url_string = "{}/{}/{}".format(BASE_URL, - self.objectprefix, self.device_id()) - values = {"desired_state": {"powered": state}} - arequest = requests.put(url_string, - data=json.dumps(values), headers=HEADERS) - self._update_state_from_response(arequest.json()) - - self._last_call = (time.time(), state) - - def wait_till_desired_reached(self): - """ Wait till desired state reached. Max 10s. """ - if self._recent_state_set(): - return - - # self.refresh_state_at_hub() - tries = 1 - - while True: - self.update_state() - last_read = self._last_reading - - if last_read.get('desired_powered') == last_read.get('powered') or tries == 5: - break - - time.sleep(2) - - tries += 1 - self.update_state() - last_read = self._last_reading - - def _recent_state_set(self): - return time.time() - self._last_call[0] < 15 - - -class WinkBulb(WinkBinarySwitch): - """ - Represents a Wink light bulb - json_obj holds the json stat at init (if there is a refresh it's updated) - it's the native format for this objects methods - - For example API responses, see unit tests. - """ - json_state = {} - - def __init__(self, device_state_as_json): - super().__init__(device_state_as_json, - objectprefix="light_bulbs") - - def device_id(self): - return self.json_state.get('light_bulb_id', self.name()) - - def brightness(self): - return self._last_reading.get('brightness') - - def color_xy(self): - """ - XY colour value: [float, float] or None - :rtype: list float - """ - color_x = self._last_reading.get('color_x') - color_y = self._last_reading.get('color_y') - - if color_x and color_y: - return [float(color_x), float(color_y)] - - return None - - def color_temperature_kelvin(self): - """ - Color temperature, in degrees Kelvin. - Eg: "Daylight" light bulbs are 4600K - :rtype: int - """ - return self._last_reading.get('color_temperature') - - def set_state(self, state, brightness=None, - color_kelvin=None, color_xy=None, **kwargs): - """ - :param state: a boolean of true (on) or false ('off') - :param brightness: a float from 0 to 1 to set the brightness of - this bulb - :param color_kelvin: an integer greater than 0 which is a color in - degrees Kelvin - :param color_xy: a pair of floats in a list which specify the desired - CIE 1931 x,y color coordinates - :return: nothing - """ - url_string = "{}/light_bulbs/{}".format(BASE_URL, self.device_id()) - values = { - "desired_state": { - "powered": state - } - } - - if brightness is not None: - values["desired_state"]["brightness"] = brightness - - if color_kelvin and color_xy: - logging.warning("Both color temperature and CIE 1931 x,y" - " color coordinates we provided to setState." - "Using color temperature and ignoring" - " CIE 1931 values.") - - if color_kelvin: - values["desired_state"]["color_model"] = "color_temperature" - values["desired_state"]["color_temperature"] = color_kelvin - elif color_xy: - values["desired_state"]["color_model"] = "xy" - color_xy_iter = iter(color_xy) - values["desired_state"]["color_x"] = next(color_xy_iter) - values["desired_state"]["color_y"] = next(color_xy_iter) - - url_string = "{}/light_bulbs/{}".format(BASE_URL, self.device_id()) - arequest = requests.put(url_string, - data=json.dumps(values), headers=HEADERS) - self._update_state_from_response(arequest.json()) - - self._last_call = (time.time(), state) - - def __repr__(self): - return "" % ( - self.name(), self.device_id(), self.state()) - - -class WinkLock(WinkDevice): - """ - represents a wink.py lock - json_obj holds the json stat at init (if there is a refresh it's updated) - it's the native format for this objects methods - """ - - def __init__(self, device_state_as_json, objectprefix="locks"): - super(WinkLock, self).__init__(device_state_as_json, - objectprefix=objectprefix) - # Tuple (desired state, time) - self._last_call = (0, None) - - def __repr__(self): - return "" % (self.name(), - self.device_id(), self.state()) - - def state(self): - # Optimistic approach to setState: - # Within 15 seconds of a call to setState we assume it worked. - if self._recent_state_set(): - return self._last_call[1] - - return self._last_reading.get('locked', False) - - def device_id(self): - return self.json_state.get('lock_id', self.name()) - - def set_state(self, state): - """ - :param state: a boolean of true (on) or false ('off') - :return: nothing - """ - url_string = "{}/{}/{}".format(BASE_URL, - self.objectprefix, self.device_id()) - values = {"desired_state": {"locked": state}} - arequest = requests.put(url_string, - data=json.dumps(values), headers=HEADERS) - self._update_state_from_response(arequest.json()) - - self._last_call = (time.time(), state) - - def wait_till_desired_reached(self): - """ Wait till desired state reached. Max 10s. """ - if self._recent_state_set(): - return - - # self.refresh_state_at_hub() - tries = 1 - - while True: - self.update_state() - last_read = self._last_reading - - if last_read.get('desired_locked') == last_read.get('locked') or tries == 5: - break - - time.sleep(2) - - tries += 1 - self.update_state() - last_read = self._last_reading - - def _recent_state_set(self): - return time.time() - self._last_call[0] < 15 - - -class WinkPowerStripOutlet(WinkBinarySwitch): - """ represents a wink.py switch - json_obj holds the json stat at init (if there is a refresh it's updated) - it's the native format for this objects methods - and looks like so: - """ - - def __init__(self, device_state_as_json, objectprefix="powerstrips"): - super(WinkPowerStripOutlet, self).__init__(device_state_as_json, - objectprefix=objectprefix) - # Tuple (desired state, time) - self._last_call = (0, None) - - def __repr__(self): - return "".format(name=self.name(), - device=self.device_id(), - parent_id=self.parent_id(), - state=self.state()) - - @property - def _last_reading(self): - return self.json_state.get('last_reading') or {} - - def _update_state_from_response(self, response_json): - """ - :param response_json: the json obj returned from query - :return: - """ - power_strip = response_json.get('data') - power_strip_reading = power_strip.get('last_reading') - outlets = power_strip.get('outlets', power_strip) - for outlet in outlets: - if outlet.get('outlet_id') == str(self.device_id()): - outlet['last_reading']['connection'] = power_strip_reading.get('connection') - self.json_state = outlet - - def update_state(self): - """ Update state with latest info from Wink API. """ - url_string = "{}/{}/{}".format(BASE_URL, - self.objectprefix, self.parent_id()) - arequest = requests.get(url_string, headers=HEADERS) - self._update_state_from_response(arequest.json()) - - def index(self): - return self.json_state.get('outlet_index', None) - - def device_id(self): - return self.json_state.get('outlet_id', self.name()) - - def parent_id(self): - return self.json_state.get('parent_object_id', - self.json_state.get('powerstrip_id')) - - # pylint: disable=unused-argument - # kwargs is unused here but is used by child implementations - def set_state(self, state, **kwargs): - """ - :param state: a boolean of true (on) or false ('off') - :return: nothing - """ - url_string = "{}/{}/{}".format(BASE_URL, - self.objectprefix, self.parent_id()) - if self.index() == 0: - values = {"outlets": [{"desired_state": {"powered": state}}, {}]} - else: - values = {"outlets": [{}, {"desired_state": {"powered": state}}]} - - arequest = requests.put(url_string, - data=json.dumps(values), headers=HEADERS) - self._update_state_from_response(arequest.json()) - - self._last_call = (time.time(), state) - - def wait_till_desired_reached(self): - """ Wait till desired state reached. Max 10s. """ - if self._recent_state_set(): - return - - # self.refresh_state_at_hub() - tries = 1 - - while True: - self.update_state() - last_read = self._last_reading - - if last_read.get('desired_powered') == last_read.get('powered') or tries == 5: - break - - time.sleep(2) - - tries += 1 - self.update_state() - last_read = self._last_reading - - def _recent_state_set(self): - return time.time() - self._last_call[0] < 15 - - -class WinkGarageDoor(WinkDevice): - r""" represents a wink.py garage door - 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: - """ - - def __init__(self, device_state_as_json, objectprefix="garage_doors"): - super(WinkGarageDoor, self).__init__(device_state_as_json, - objectprefix=objectprefix) - # Tuple (desired state, time) - self._last_call = (0, None) - - def __repr__(self): - return "" % (self.name(), self.device_id(), self.state()) - - def state(self): - # Optimistic approach to setState: - # Within 15 seconds of a call to setState we assume it worked. - if self._recent_state_set(): - return self._last_call[1] - - return self._last_reading.get('position', 0) - - def device_id(self): - return self.json_state.get('garage_door_id', self.name()) - - def set_state(self, state): - """ - :param state: a number of 1 ('open') or 0 ('close') - :return: nothing - """ - url_string = "{}/{}/{}".format(BASE_URL, self.objectprefix, self.device_id()) - values = {"desired_state": {"position": state}} - arequest = requests.put(url_string, data=json.dumps(values), headers=HEADERS) - self._update_state_from_response(arequest.json()) - - self._last_call = (time.time(), state) - - def wait_till_desired_reached(self): - """ Wait till desired state reached. Max 10s. """ - if self._recent_state_set(): - return - - # self.refresh_state_at_hub() - tries = 1 - - while True: - self.update_state() - last_read = self._last_reading - - if last_read.get('desired_position') == last_read.get('0.0') \ - or tries == 5: - break - - time.sleep(2) - - tries += 1 - self.update_state() - last_read = self._last_reading - - def _recent_state_set(self): - return time.time() - self._last_call[0] < 15 - - -class WinkSiren(WinkBinarySwitch): - """ represents a wink.py siren - json_obj holds the json stat at init (if there is a refresh it's updated) - it's the native format for this objects methods - """ - - def __init__(self, device_state_as_json, objectprefix="sirens"): - super(WinkSiren, self).__init__(device_state_as_json, - objectprefix=objectprefix) - # Tuple (desired state, time) - self._last_call = (0, None) - - def __repr__(self): - return "" % (self.name(), - self.device_id(), self.state()) - - def device_id(self): - return self.json_state.get('siren_id', self.name()) - - -def get_devices(device_type): - arequest_url = "{}/users/me/wink_devices".format(BASE_URL) - response = requests.get(arequest_url, headers=HEADERS) - if response.status_code == 200: - response_dict = response.json() - filter_key = DEVICE_ID_KEYS.get(device_type) - return get_devices_from_response_dict(response_dict, - filter_key=filter_key) - - if response.status_code == 401: - raise WinkAPIException("401 Response from Wink API. Maybe Bearer token is expired?") - else: - raise WinkAPIException("Unexpected") - - -def get_devices_from_response_dict(response_dict, filter_key=None): - items = response_dict.get('data') - - devices = [] - - keys = DEVICE_ID_KEYS.values() - if filter_key: - keys = [filter_key] - - for item in items: - for key in keys: - value_at_key = item.get(key) - if value_at_key is not None and item.get("hidden_at") is None: - if key == "powerstrip_id": - outlets = item['outlets'] - for outlet in outlets: - value_at_key = outlet.get('outlet_id') - if (value_at_key is not None and - outlet.get("hidden_at") is None): - devices.append(WinkDevice.factory(outlet)) - else: - devices.append(WinkDevice.factory(item)) - - return devices - - -def get_bulbs(): - return get_devices(device_types.LIGHT_BULB) - - -def get_switches(): - return get_devices(device_types.BINARY_SWITCH) - - -def get_sensors(): - return get_devices(device_types.SENSOR_POD) - - -def get_locks(): - return get_devices(device_types.LOCK) - - -def get_eggtrays(): - return get_devices(device_types.EGG_TRAY) - - -def get_garage_doors(): - return get_devices(device_types.GARAGE_DOOR) - - -def get_powerstrip_outlets(): - return get_devices(device_types.POWER_STRIP) - - -def get_sirens(): - return get_devices(device_types.SIREN) - - -def is_token_set(): - """ Returns if an auth token has been set. """ - return bool(HEADERS) - - -def set_bearer_token(token): - global HEADERS - - HEADERS = { - "Content-Type": "application/json", - "Authorization": "Bearer {}".format(token) - } - - -class WinkAPIException(Exception): - pass diff --git a/pywink/base.py b/pywink/base.py new file mode 100644 index 0000000..ccf6d62 --- /dev/null +++ b/pywink/base.py @@ -0,0 +1,52 @@ +class WinkDevice(object): + + + def __init__(self, device_state_as_json, objectprefix=None): + self.objectprefix = objectprefix + self.json_state = device_state_as_json + + def __str__(self): + return "%s %s %s" % (self.name(), self.device_id(), self.state()) + + def __repr__(self): + return "".format(name=self.name(), + device=self.device_id(), + state=self.state()) + + def name(self): + return self.json_state.get('name', "Unknown Name") + + def state(self): + raise NotImplementedError("Must implement state") + + def device_id(self): + raise NotImplementedError("Must implement device_id") + + @property + def _last_reading(self): + return self.json_state.get('last_reading') or {} + + def _update_state_from_response(self, response_json): + """ + :param response_json: the json obj returned from query + :return: + """ + self.json_state = response_json.get('data') + + def update_state(self): + """ Update state with latest info from Wink API. """ + url_string = "{}/{}/{}".format(BASE_URL, + self.objectprefix, self.device_id()) + arequest = requests.get(url_string, headers=HEADERS) + self._update_state_from_response(arequest.json()) + + def refresh_state_at_hub(self): + """ + Tell hub to query latest status from device and upload to Wink. + PS: Not sure if this even works.. + """ + url_string = "{}/{}/{}/refresh".format(BASE_URL, + self.objectprefix, + self.device_id()) + requests.get(url_string, headers=HEADERS) + diff --git a/pywink/devices.py b/pywink/devices.py new file mode 100644 index 0000000..273aeef --- /dev/null +++ b/pywink/devices.py @@ -0,0 +1,570 @@ +from pywink import device_types +from pywink.base import WinkDevice +from pywink.sensor import WinkSensorPod + + +def load_from_json(device_state_as_json): + + new_object = None + + # pylint: disable=redefined-variable-type + # These objects all share the same base class: WinkDevice + + if "light_bulb_id" in device_state_as_json: + new_object = WinkBulb(device_state_as_json) + elif "sensor_pod_id" in device_state_as_json: + new_object = WinkSensorPod(device_state_as_json) + elif "binary_switch_id" in device_state_as_json: + new_object = WinkBinarySwitch(device_state_as_json) + elif "outlet_id" in device_state_as_json: + new_object = WinkPowerStripOutlet(device_state_as_json) + elif "lock_id" in device_state_as_json: + new_object = WinkLock(device_state_as_json) + elif "eggtray_id" in device_state_as_json: + new_object = WinkEggTray(device_state_as_json) + elif "garage_door_id" in device_state_as_json: + new_object = WinkGarageDoor(device_state_as_json) + elif "siren_id" in device_state_as_json: + new_object = WinkSiren(device_state_as_json) + + return new_object or WinkDevice(device_state_as_json) + +DEVICE_ID_KEYS = { + device_types.BINARY_SWITCH: 'binary_switch_id', + device_types.EGG_TRAY: 'eggtray_id', + device_types.GARAGE_DOOR: 'garage_door_id', + device_types.LIGHT_BULB: 'light_bulb_id', + device_types.LOCK: 'lock_id', + device_types.POWER_STRIP: 'powerstrip_id', + device_types.SENSOR_POD: 'sensor_pod_id', + device_types.SIREN: 'siren_id' +} + + + + +def get_devices(device_type): + arequest_url = "{}/users/me/wink_devices".format(BASE_URL) + response = requests.get(arequest_url, headers=HEADERS) + if response.status_code == 200: + response_dict = response.json() + filter_key = DEVICE_ID_KEYS.get(device_type) + return get_devices_from_response_dict(response_dict, + filter_key=filter_key) + + if response.status_code == 401: + raise WinkAPIException("401 Response from Wink API. Maybe Bearer token is expired?") + else: + raise WinkAPIException("Unexpected") + + +def get_devices_from_response_dict(response_dict, filter_key=None): + items = response_dict.get('data') + + devices = [] + + keys = DEVICE_ID_KEYS.values() + if filter_key: + keys = [DEVICE_ID_KEYS.get(filter_key)] + + for item in items: + for key in keys: + value_at_key = item.get(key) + if value_at_key is not None and item.get("hidden_at") is None: + if key == "powerstrip_id": + outlets = item['outlets'] + for outlet in outlets: + value_at_key = outlet.get('outlet_id') + if (value_at_key is not None and + outlet.get("hidden_at") is None): + devices.append(load_from_json(outlet)) + else: + devices.append(load_from_json(item)) + + return devices + + +def get_bulbs(): + return get_devices(device_types.LIGHT_BULB) + + +def get_switches(): + return get_devices(device_types.BINARY_SWITCH) + + +def get_sensors(): + return get_devices(device_types.SENSOR_POD) + + +def get_locks(): + return get_devices(device_types.LOCK) + + +def get_eggtrays(): + return get_devices(device_types.EGG_TRAY) + + +def get_garage_doors(): + return get_devices(device_types.GARAGE_DOOR) + + +def get_powerstrip_outlets(): + return get_devices(device_types.POWER_STRIP) + + +def get_sirens(): + return get_devices(device_types.SIREN) + + +def is_token_set(): + """ Returns if an auth token has been set. """ + return bool(HEADERS) + + +def set_bearer_token(token): + global HEADERS + + HEADERS = { + "Content-Type": "application/json", + "Authorization": "Bearer {}".format(token) + } + + +class WinkAPIException(Exception): + pass + + + +class WinkEggTray(WinkDevice): + """ represents a wink.py egg tray + json_obj holds the json stat at init (if there is a refresh it's updated) + it's the native format for this objects methods + """ + + def __init__(self, device_state_as_json, objectprefix="eggtrays"): + super(WinkEggTray, self).__init__(device_state_as_json, + objectprefix=objectprefix) + + def __repr__(self): + return "".format(name=self.name(), + device=self.device_id(), + state=self.state()) + + def state(self): + if 'inventory' in self._last_reading: + return self._last_reading['inventory'] + return False + + def device_id(self): + return self.json_state.get('eggtray_id', self.name()) + + +class WinkBinarySwitch(WinkDevice): + """ represents a wink.py switch + json_obj holds the json stat at init (if there is a refresh it's updated) + it's the native format for this objects methods + """ + + def __init__(self, device_state_as_json, objectprefix="binary_switches"): + super(WinkBinarySwitch, self).__init__(device_state_as_json, + objectprefix=objectprefix) + # Tuple (desired state, time) + self._last_call = (0, None) + + def __repr__(self): + return "" % (self.name(), + self.device_id(), self.state()) + + def state(self): + if not self._last_reading.get('connection', False): + return False + # Optimistic approach to setState: + # Within 15 seconds of a call to setState we assume it worked. + if self._recent_state_set(): + return self._last_call[1] + + return self._last_reading.get('powered', False) + + def device_id(self): + return self.json_state.get('binary_switch_id', self.name()) + + # pylint: disable=unused-argument + # kwargs is unused here but is used by child implementations + def set_state(self, state, **kwargs): + """ + :param state: a boolean of true (on) or false ('off') + :return: nothing + """ + url_string = "{}/{}/{}".format(BASE_URL, + self.objectprefix, self.device_id()) + values = {"desired_state": {"powered": state}} + arequest = requests.put(url_string, + data=json.dumps(values), headers=HEADERS) + self._update_state_from_response(arequest.json()) + + self._last_call = (time.time(), state) + + def wait_till_desired_reached(self): + """ Wait till desired state reached. Max 10s. """ + if self._recent_state_set(): + return + + # self.refresh_state_at_hub() + tries = 1 + + while True: + self.update_state() + last_read = self._last_reading + + if last_read.get('desired_powered') == last_read.get('powered') or tries == 5: + break + + time.sleep(2) + + tries += 1 + self.update_state() + last_read = self._last_reading + + def _recent_state_set(self): + return time.time() - self._last_call[0] < 15 + + +class WinkBulb(WinkBinarySwitch): + """ + Represents a Wink light bulb + json_obj holds the json stat at init (if there is a refresh it's updated) + it's the native format for this objects methods + + For example API responses, see unit tests. + """ + json_state = {} + + def __init__(self, device_state_as_json): + super().__init__(device_state_as_json, + objectprefix="light_bulbs") + + def device_id(self): + return self.json_state.get('light_bulb_id', self.name()) + + def brightness(self): + return self._last_reading.get('brightness') + + def color_xy(self): + """ + XY colour value: [float, float] or None + :rtype: list float + """ + color_x = self._last_reading.get('color_x') + color_y = self._last_reading.get('color_y') + + if color_x and color_y: + return [float(color_x), float(color_y)] + + return None + + def color_temperature_kelvin(self): + """ + Color temperature, in degrees Kelvin. + Eg: "Daylight" light bulbs are 4600K + :rtype: int + """ + return self._last_reading.get('color_temperature') + + def set_state(self, state, brightness=None, + color_kelvin=None, color_xy=None, **kwargs): + """ + :param state: a boolean of true (on) or false ('off') + :param brightness: a float from 0 to 1 to set the brightness of + this bulb + :param color_kelvin: an integer greater than 0 which is a color in + degrees Kelvin + :param color_xy: a pair of floats in a list which specify the desired + CIE 1931 x,y color coordinates + :return: nothing + """ + url_string = "{}/light_bulbs/{}".format(BASE_URL, self.device_id()) + values = { + "desired_state": { + "powered": state + } + } + + if brightness is not None: + values["desired_state"]["brightness"] = brightness + + if color_kelvin and color_xy: + logging.warning("Both color temperature and CIE 1931 x,y" + " color coordinates we provided to setState." + "Using color temperature and ignoring" + " CIE 1931 values.") + + if color_kelvin: + values["desired_state"]["color_model"] = "color_temperature" + values["desired_state"]["color_temperature"] = color_kelvin + elif color_xy: + values["desired_state"]["color_model"] = "xy" + color_xy_iter = iter(color_xy) + values["desired_state"]["color_x"] = next(color_xy_iter) + values["desired_state"]["color_y"] = next(color_xy_iter) + + url_string = "{}/light_bulbs/{}".format(BASE_URL, self.device_id()) + arequest = requests.put(url_string, + data=json.dumps(values), headers=HEADERS) + self._update_state_from_response(arequest.json()) + + self._last_call = (time.time(), state) + + def __repr__(self): + return "" % ( + self.name(), self.device_id(), self.state()) + + +class WinkLock(WinkDevice): + """ + represents a wink.py lock + json_obj holds the json stat at init (if there is a refresh it's updated) + it's the native format for this objects methods + """ + + def __init__(self, device_state_as_json, objectprefix="locks"): + super(WinkLock, self).__init__(device_state_as_json, + objectprefix=objectprefix) + # Tuple (desired state, time) + self._last_call = (0, None) + + def __repr__(self): + return "" % (self.name(), + self.device_id(), self.state()) + + def state(self): + # Optimistic approach to setState: + # Within 15 seconds of a call to setState we assume it worked. + if self._recent_state_set(): + return self._last_call[1] + + return self._last_reading.get('locked', False) + + def device_id(self): + return self.json_state.get('lock_id', self.name()) + + def set_state(self, state): + """ + :param state: a boolean of true (on) or false ('off') + :return: nothing + """ + url_string = "{}/{}/{}".format(BASE_URL, + self.objectprefix, self.device_id()) + values = {"desired_state": {"locked": state}} + arequest = requests.put(url_string, + data=json.dumps(values), headers=HEADERS) + self._update_state_from_response(arequest.json()) + + self._last_call = (time.time(), state) + + def wait_till_desired_reached(self): + """ Wait till desired state reached. Max 10s. """ + if self._recent_state_set(): + return + + # self.refresh_state_at_hub() + tries = 1 + + while True: + self.update_state() + last_read = self._last_reading + + if last_read.get('desired_locked') == last_read.get('locked') or tries == 5: + break + + time.sleep(2) + + tries += 1 + self.update_state() + last_read = self._last_reading + + def _recent_state_set(self): + return time.time() - self._last_call[0] < 15 + + +class WinkPowerStripOutlet(WinkBinarySwitch): + """ represents a wink.py switch + json_obj holds the json stat at init (if there is a refresh it's updated) + it's the native format for this objects methods + and looks like so: + """ + + def __init__(self, device_state_as_json, objectprefix="powerstrips"): + super(WinkPowerStripOutlet, self).__init__(device_state_as_json, + objectprefix=objectprefix) + # Tuple (desired state, time) + self._last_call = (0, None) + + def __repr__(self): + return "".format(name=self.name(), + device=self.device_id(), + parent_id=self.parent_id(), + state=self.state()) + + @property + def _last_reading(self): + return self.json_state.get('last_reading') or {} + + def _update_state_from_response(self, response_json): + """ + :param response_json: the json obj returned from query + :return: + """ + power_strip = response_json.get('data') + power_strip_reading = power_strip.get('last_reading') + outlets = power_strip.get('outlets', power_strip) + for outlet in outlets: + if outlet.get('outlet_id') == str(self.device_id()): + outlet['last_reading']['connection'] = power_strip_reading.get('connection') + self.json_state = outlet + + def update_state(self): + """ Update state with latest info from Wink API. """ + url_string = "{}/{}/{}".format(BASE_URL, + self.objectprefix, self.parent_id()) + arequest = requests.get(url_string, headers=HEADERS) + self._update_state_from_response(arequest.json()) + + def index(self): + return self.json_state.get('outlet_index', None) + + def device_id(self): + return self.json_state.get('outlet_id', self.name()) + + def parent_id(self): + return self.json_state.get('parent_object_id', + self.json_state.get('powerstrip_id')) + + # pylint: disable=unused-argument + # kwargs is unused here but is used by child implementations + def set_state(self, state, **kwargs): + """ + :param state: a boolean of true (on) or false ('off') + :return: nothing + """ + url_string = "{}/{}/{}".format(BASE_URL, + self.objectprefix, self.parent_id()) + if self.index() == 0: + values = {"outlets": [{"desired_state": {"powered": state}}, {}]} + else: + values = {"outlets": [{}, {"desired_state": {"powered": state}}]} + + arequest = requests.put(url_string, + data=json.dumps(values), headers=HEADERS) + self._update_state_from_response(arequest.json()) + + self._last_call = (time.time(), state) + + def wait_till_desired_reached(self): + """ Wait till desired state reached. Max 10s. """ + if self._recent_state_set(): + return + + # self.refresh_state_at_hub() + tries = 1 + + while True: + self.update_state() + last_read = self._last_reading + + if last_read.get('desired_powered') == last_read.get('powered') or tries == 5: + break + + time.sleep(2) + + tries += 1 + self.update_state() + last_read = self._last_reading + + def _recent_state_set(self): + return time.time() - self._last_call[0] < 15 + + +class WinkGarageDoor(WinkDevice): + r""" represents a wink.py garage door + 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: + """ + + def __init__(self, device_state_as_json, objectprefix="garage_doors"): + super(WinkGarageDoor, self).__init__(device_state_as_json, + objectprefix=objectprefix) + # Tuple (desired state, time) + self._last_call = (0, None) + + def __repr__(self): + return "" % (self.name(), self.device_id(), self.state()) + + def state(self): + # Optimistic approach to setState: + # Within 15 seconds of a call to setState we assume it worked. + if self._recent_state_set(): + return self._last_call[1] + + return self._last_reading.get('position', 0) + + def device_id(self): + return self.json_state.get('garage_door_id', self.name()) + + def set_state(self, state): + """ + :param state: a number of 1 ('open') or 0 ('close') + :return: nothing + """ + url_string = "{}/{}/{}".format(BASE_URL, self.objectprefix, self.device_id()) + values = {"desired_state": {"position": state}} + arequest = requests.put(url_string, data=json.dumps(values), headers=HEADERS) + self._update_state_from_response(arequest.json()) + + self._last_call = (time.time(), state) + + def wait_till_desired_reached(self): + """ Wait till desired state reached. Max 10s. """ + if self._recent_state_set(): + return + + # self.refresh_state_at_hub() + tries = 1 + + while True: + self.update_state() + last_read = self._last_reading + + if last_read.get('desired_position') == last_read.get('0.0') \ + or tries == 5: + break + + time.sleep(2) + + tries += 1 + self.update_state() + last_read = self._last_reading + + def _recent_state_set(self): + return time.time() - self._last_call[0] < 15 + + +class WinkSiren(WinkBinarySwitch): + """ represents a wink.py siren + json_obj holds the json stat at init (if there is a refresh it's updated) + it's the native format for this objects methods + """ + + def __init__(self, device_state_as_json, objectprefix="sirens"): + super(WinkSiren, self).__init__(device_state_as_json, + objectprefix=objectprefix) + # Tuple (desired state, time) + self._last_call = (0, None) + + def __repr__(self): + return "" % (self.name(), + self.device_id(), self.state()) + + def device_id(self): + return self.json_state.get('siren_id', self.name()) diff --git a/pywink/sensor.py b/pywink/sensor.py new file mode 100644 index 0000000..db1d57d --- /dev/null +++ b/pywink/sensor.py @@ -0,0 +1,29 @@ +from pywink.base import WinkDevice + + +class WinkSensorPod(WinkDevice): + """ + A Wink "Sensor Pod" represents a single sensor. Multi-sensor devices will be exposed as individual WinkSensorPods + with a common root_id. + """ + + def __init__(self, device_state_as_json, objectprefix="sensor_pods"): + super(WinkSensorPod, self).__init__(device_state_as_json, + objectprefix=objectprefix) + + def __repr__(self): + return "" % (self.name(), + self.device_id(), self.state()) + + def root_id(self): + return None + + def state(self): + if 'opened' in self._last_reading: + return self._last_reading['opened'] + elif 'motion' in self._last_reading: + return self._last_reading['motion'] + return False + + def device_id(self): + return self.json_state.get('sensor_pod_id', self.name()) diff --git a/setup.py b/setup.py index 35148ee..76a931c 100644 --- a/setup.py +++ b/setup.py @@ -1,12 +1,12 @@ from setuptools import setup setup(name='python-wink', - version='0.4.3', + version='0.5.0', description='Access Wink devices via the Wink API', url='http://github.com/bradsk88/python-wink', - author='John McLaughlin', + author='Brad Johnson', license='MIT', - install_requires=['requests>=2.0'], + install_requires=['requests>=2.0', 'ijson==2.3'], tests_require=['mock'], test_suite='tests', packages=['pywink'], diff --git a/tests/api_responses/quirky_spotter.json b/tests/api_responses/quirky_spotter.json new file mode 100644 index 0000000..6cac0e4 --- /dev/null +++ b/tests/api_responses/quirky_spotter.json @@ -0,0 +1,1410 @@ +{ + "data": [ + { + "desired_state": { + "pairing_mode": null, + "pairing_prefix": null, + "pairing_mode_duration": 5 + }, + "last_reading": { + "connection": true, + "connection_updated_at": 1453134317.1239395, + "agent_session_id": "1fcea86e7e855f50671194467aa8494f", + "agent_session_id_updated_at": 1453134316.6489544, + "pairing_mode": null, + "pairing_mode_updated_at": 1452663492.6237466, + "pairing_prefix": null, + "pairing_prefix_updated_at": null, + "kidde_radio_code_updated_at": 1430784269.8093991, + "pairing_mode_duration": 5, + "pairing_mode_duration_updated_at": 1452663492.6237466, + "updating_firmware": false, + "updating_firmware_updated_at": 1453134316.2249265, + "firmware_version": "2.19.0", + "firmware_version_updated_at": 1453134317.1239395, + "update_needed": false, + "update_needed_updated_at": 1453134317.1239395, + "mac_address": "34:23:BA:FD:58:90", + "mac_address_updated_at": 1453134317.1239395, + "ip_address": "192.168.1.140", + "ip_address_updated_at": 1453134317.1239395, + "hub_version": "00.01", + "hub_version_updated_at": 1453134317.1239395, + "remote_pairable": false, + "remote_pairable_updated_at": 1443227696.515682, + "local_control_public_key_hash": null, + "local_control_public_key_hash_updated_at": null, + "local_control_id": null, + "local_control_id_updated_at": null, + "desired_pairing_mode_updated_at": 1451085051.4455187, + "desired_pairing_prefix_updated_at": 1451085051.1444066, + "desired_kidde_radio_code_updated_at": 1451085051.1444066, + "desired_pairing_mode_duration_updated_at": 1451084923.4153354, + "connection_changed_at": 1453134316.2249265, + "agent_session_id_changed_at": 1453134316.6489544, + "desired_pairing_mode_duration_changed_at": 1451084923.4153354, + "desired_pairing_mode_changed_at": 1451084914.6288083, + "pairing_mode_changed_at": 1451084923.3720696, + "desired_pairing_prefix_changed_at": 1451085051.1444066, + "ip_address_changed_at": 1452984994.9847243 + }, + "hub_id": "96647", + "uuid": "986eab82-b213-45cf-bca1-a1b22f9bd8cc", + "name": "Hub", + "locale": "en_us", + "units": {}, + "created_at": 1422609930, + "hidden_at": null, + "capabilities": { + "oauth2_clients": [ + "wink_hub" + ], + "home_security_device": true + }, + "triggers": [], + "manufacturer_device_model": "wink_hub", + "manufacturer_device_id": null, + "device_manufacturer": "wink", + "model_name": "HUB", + "upc_id": "15", + "upc_code": "840410102358", + "lat_lng": [ + null, + null + ], + "location": "", + "update_needed": false, + "configuration": { + "kidde_radio_code": 0 + } + }, + { + "desired_state": { + "powered": false, + "brightness": 1 + }, + "last_reading": { + "connection": false, + "connection_updated_at": 1453134920.2468944, + "firmware_version": "0.1b03 / 0.4b00", + "firmware_version_updated_at": 1453134920.2468944, + "firmware_date_code": "20140812", + "firmware_date_code_updated_at": 1453134920.2468944, + "powered": false, + "powered_updated_at": 1453134920.2468944, + "brightness": 1, + "brightness_updated_at": 1453134920.2468944, + "desired_powered_updated_at": 1453047163.5901022, + "desired_brightness_updated_at": 1453047163.5901022, + "desired_powered_changed_at": 1453047163.5901022, + "desired_brightness_changed_at": 1452766723.7550044, + "connection_changed_at": 1451088047.393394 + }, + "light_bulb_id": "609771", + "name": "Boxed bulb", + "locale": "en_us", + "units": {}, + "created_at": 1431330307, + "hidden_at": null, + "capabilities": {}, + "triggers": [], + "manufacturer_device_model": "ge_zigbee_light", + "manufacturer_device_id": null, + "device_manufacturer": "ge", + "model_name": "GE Light Bulb", + "upc_id": "244", + "upc_code": "ge_zigbee3", + "gang_id": null, + "hub_id": "96647", + "local_id": "14", + "radio_type": "zigbee", + "linked_service_id": null, + "lat_lng": [ + null, + null + ], + "location": "", + "order": 0 + }, + { + "last_event": { + "brightness_occurred_at": 1445973676.198793, + "loudness_occurred_at": 1453186523.6125298, + "vibration_occurred_at": 1453186429.210991 + }, + "desired_state": {}, + "last_reading": { + "battery": 0.85, + "battery_updated_at": 1453187132.7789793, + "brightness": 1, + "brightness_updated_at": 1453187132.7789793, + "external_power": true, + "external_power_updated_at": 1453187132.7789793, + "humidity": 48, + "humidity_updated_at": 1453187132.7789793, + "loudness": false, + "loudness_updated_at": 1453187132.7789793, + "temperature": 5, + "temperature_updated_at": 1453187132.7789793, + "vibration": false, + "vibration_updated_at": 1453187132.7789793, + "brightness_true": "N/A", + "brightness_true_updated_at": 1445973676.198793, + "loudness_true": "N/A", + "loudness_true_updated_at": 1453186523.6125298, + "vibration_true": "N/A", + "vibration_true_updated_at": 1453186429.210991, + "connection": true, + "connection_updated_at": 1453187132.7789793, + "agent_session_id": null, + "agent_session_id_updated_at": null, + "desired_battery_updated_at": null, + "desired_brightness_updated_at": null, + "desired_external_power_updated_at": null, + "desired_humidity_updated_at": null, + "desired_loudness_updated_at": null, + "desired_temperature_updated_at": null, + "desired_vibration_updated_at": null, + "loudness_changed_at": 1453186586.7168324, + "loudness_true_changed_at": 1453186523.6125298, + "vibration_changed_at": 1453186528.978827, + "vibration_true_changed_at": 1453186429.210991, + "temperature_changed_at": 1453186523.6125298, + "humidity_changed_at": 1453187132.7789793, + "brightness_changed_at": 1445973676.198793, + "brightness_true_changed_at": 1445973676.198793, + "battery_changed_at": 1452267645.8792017 + }, + "sensor_pod_id": "72503", + "uuid": "0d889d64-e77b-48a3-a132-475626f8ab1f", + "name": "Well Spotter", + "locale": "en_us", + "units": {}, + "created_at": 1432962859, + "hidden_at": null, + "capabilities": { + "fields": [ + { + "field": "battery", + "type": "percentage", + "mutability": "read-only" + }, + { + "field": "brightness", + "type": "percentage", + "mutability": "read-only" + }, + { + "field": "external_power", + "type": "boolean", + "mutability": "read-only" + }, + { + "field": "humidity", + "type": "percentage", + "mutability": "read-only" + }, + { + "field": "loudness", + "type": "boolean", + "mutability": "read-only" + }, + { + "field": "temperature", + "type": "float", + "mutability": "read-only" + }, + { + "field": "vibration", + "type": "boolean", + "mutability": "read-only" + } + ] + }, + "triggers": [], + "manufacturer_device_model": "quirky_ge_spotter", + "manufacturer_device_id": null, + "device_manufacturer": "quirky_ge", + "model_name": "Spotter", + "upc_id": "25", + "upc_code": "814434018858", + "gang_id": null, + "hub_id": null, + "local_id": null, + "radio_type": null, + "linked_service_id": null, + "lat_lng": [ + null, + null + ], + "location": "", + "mac_address": "0c2a5905a5a2", + "serial": "ABAB00010864" + }, + { + "desired_state": { + "powered": false, + "brightness": 1 + }, + "last_reading": { + "connection": true, + "connection_updated_at": 1453134342.9120734, + "firmware_version": "0.1b03 / 0.4b00", + "firmware_version_updated_at": 1453134342.9120734, + "firmware_date_code": "20140812", + "firmware_date_code_updated_at": 1453134342.9120734, + "powered": false, + "powered_updated_at": 1453134342.9120734, + "brightness": 1, + "brightness_updated_at": 1453134342.9120734, + "desired_powered_updated_at": 1453047163.4562936, + "desired_brightness_updated_at": 1453047163.4562936, + "desired_powered_changed_at": 1453047163.4562936, + "desired_brightness_changed_at": 1453020418.8140907, + "powered_changed_at": 1452910664.16325, + "connection_changed_at": 1453020955.6564333, + "brightness_changed_at": 1449954381.1657941, + "firmware_version_changed_at": 1450787564.8940828 + }, + "light_bulb_id": "694630", + "name": "Light", + "locale": "en_us", + "units": {}, + "created_at": 1434131699, + "hidden_at": null, + "capabilities": {}, + "triggers": [], + "manufacturer_device_model": "ge_bulb", + "manufacturer_device_id": null, + "device_manufacturer": "ge", + "model_name": "GE Light Bulb", + "upc_id": "244", + "upc_code": "ge_zigbee3", + "gang_id": null, + "hub_id": "96647", + "local_id": "15", + "radio_type": "zigbee", + "linked_service_id": null, + "lat_lng": [ + 43.29879, + -70.87014 + ], + "location": "", + "order": 0 + }, + { + "last_event": { + "brightness_occurred_at": 1450698768.6228566, + "loudness_occurred_at": 1453188090.419577, + "vibration_occurred_at": 1453187050.929243 + }, + "desired_state": {}, + "last_reading": { + "battery": 0.86, + "battery_updated_at": 1453188090.419577, + "brightness": 0, + "brightness_updated_at": 1453188090.419577, + "external_power": true, + "external_power_updated_at": 1453188090.419577, + "humidity": 27, + "humidity_updated_at": 1453188090.419577, + "loudness": 1, + "loudness_updated_at": 1453188090.419577, + "temperature": 16, + "temperature_updated_at": 1453188090.419577, + "vibration": false, + "vibration_updated_at": 1453188090.419577, + "brightness_true": "N/A", + "brightness_true_updated_at": 1450698768.6228566, + "loudness_true": "N/A", + "loudness_true_updated_at": 1453188090.419577, + "vibration_true": "N/A", + "vibration_true_updated_at": 1453187050.929243, + "connection": true, + "connection_updated_at": 1453188090.419577, + "agent_session_id": null, + "agent_session_id_updated_at": null, + "humidity_changed_at": 1453187780.769285, + "battery_changed_at": 1453014608.7718346, + "temperature_changed_at": 1453169998.9940693, + "vibration_changed_at": 1453187056.2631874, + "loudness_changed_at": 1453188090.419577, + "loudness_true_changed_at": 1453188090.419577, + "vibration_true_changed_at": 1453187050.929243, + "brightness_changed_at": 1450698773.854434, + "brightness_true_changed_at": 1450698768.6228566 + }, + "sensor_pod_id": "84197", + "uuid": "c34335fe-208a-491d-b4d6-685e609e0088", + "name": "Spotter", + "locale": "en_us", + "units": {}, + "created_at": 1436338918, + "hidden_at": null, + "capabilities": { + "fields": [ + { + "field": "battery", + "type": "percentage", + "mutability": "read-only" + }, + { + "field": "brightness", + "type": "percentage", + "mutability": "read-only" + }, + { + "field": "external_power", + "type": "boolean", + "mutability": "read-only" + }, + { + "field": "humidity", + "type": "percentage", + "mutability": "read-only" + }, + { + "field": "loudness", + "type": "boolean", + "mutability": "read-only" + }, + { + "field": "temperature", + "type": "float", + "mutability": "read-only" + }, + { + "field": "vibration", + "type": "boolean", + "mutability": "read-only" + } + ] + }, + "triggers": [], + "manufacturer_device_model": "quirky_ge_spotter", + "manufacturer_device_id": null, + "device_manufacturer": "quirky_ge", + "model_name": "Spotter", + "upc_id": "25", + "upc_code": "814434018858", + "gang_id": null, + "hub_id": null, + "local_id": null, + "radio_type": null, + "linked_service_id": null, + "lat_lng": [ + null, + null + ], + "location": "", + "mac_address": "0v2a6905b738", + "serial": "ABAB00006476" + }, + { + "last_reading": { + "connection": true, + "battery": 0.37, + "remaining": 0.51 + }, + "propane_tank_id": "11382", + "name": "Refuel", + "locale": "en_us", + "units": {}, + "created_at": 1441608692, + "hidden_at": null, + "capabilities": {}, + "triggers": [ + { + "trigger_id": "216968", + "name": "Refuel remaining", + "enabled": true, + "trigger_configuration": { + "reading_type": "remaining", + "edge": "falling", + "threshold": 0.125, + "object_id": "11382", + "object_type": "propane_tank" + }, + "channel_configuration": { + "recipient_user_ids": [], + "channel_id": "15", + "object_type": null, + "object_id": null + }, + "robot_id": "1612585", + "triggered_at": null + } + ], + "device_manufacturer": "quirky_ge", + "model_name": "Refuel", + "upc_id": "17", + "upc_code": "840410100866", + "lat_lng": [ + null, + null + ], + "location": "", + "mac_address": "0v2a69073a9d", + "serial": "ACAB00031540", + "tare": 20, + "tank_changed_at": 1451433641 + }, + { + "desired_state": {}, + "last_reading": { + "connection": true, + "connection_updated_at": 1452984987.3455675, + "connection_changed_at": 1452984987.3455675 + }, + "powerstrip_id": "21095", + "name": "smart power", + "locale": "en_us", + "units": {}, + "created_at": 1441695359, + "hidden_at": null, + "capabilities": {}, + "triggers": [], + "device_manufacturer": "quirky_ge", + "model_name": "Pivot Power Genius", + "upc_id": "24", + "upc_code": "814434017226", + "lat_lng": [ + null, + null + ], + "location": "", + "mac_address": "0v2a6904e718", + "serial": "AAAA00025499", + "outlets": [ + { + "powered": false, + "scheduled_outlet_states": [], + "name": "Outlet #1", + "outlet_index": 0, + "outlet_id": "42192", + "icon_id": "15", + "parent_object_type": "powerstrip", + "parent_object_id": "21095", + "desired_state": { + "powered": false + }, + "last_reading": { + "powered": false, + "powered_updated_at": 1452984987.2208877, + "powered_changed_at": 1452760923.3140078, + "desired_powered_updated_at": 1452760923.3252928 + } + }, + { + "powered": false, + "scheduled_outlet_states": [], + "name": "Outlet #2", + "outlet_index": 1, + "outlet_id": "42193", + "icon_id": "4", + "parent_object_type": "powerstrip", + "parent_object_id": "21095", + "desired_state": { + "powered": false + }, + "last_reading": { + "powered": false, + "powered_updated_at": 1452984987.3360405, + "desired_powered_updated_at": 1451570483.5086787 + } + } + ] + }, + { + "desired_state": { + "powered": false + }, + "last_reading": { + "connection": true, + "connection_updated_at": 1453134333.8795211, + "powered": false, + "powered_updated_at": 1453134333.8795211, + "desired_powered_updated_at": 1453047162.8793688, + "desired_powered_changed_at": 1453047162.8793688, + "connection_changed_at": 1451086582.1729136, + "powered_changed_at": 1448624883.3522606 + }, + "binary_switch_id": "95584", + "name": "HA outlet", + "locale": "en_us", + "units": {}, + "created_at": 1442828209, + "hidden_at": null, + "capabilities": { + "fields": [ + { + "field": "connection", + "type": "boolean", + "mutability": "read-only" + }, + { + "field": "powered", + "type": "boolean", + "mutability": "read-write" + } + ] + }, + "triggers": [], + "manufacturer_device_model": null, + "manufacturer_device_id": null, + "device_manufacturer": null, + "model_name": "Binary Switch", + "upc_id": "78", + "upc_code": "GENERIC_ZWAVE_BINARY", + "gang_id": null, + "hub_id": "96647", + "local_id": "21", + "radio_type": "zwave", + "linked_service_id": null, + "current_budget": null, + "lat_lng": [ + null, + null + ], + "location": "", + "order": 0 + }, + { + "desired_state": { + "powered": false + }, + "last_reading": { + "connection": true, + "connection_updated_at": 1453186727.7322688, + "powered": false, + "powered_updated_at": 1453187076.237218, + "desired_powered_updated_at": 1453187076.2940626, + "powered_changed_at": 1453186727.7322688, + "desired_powered_changed_at": 1453187076.2940626, + "connection_changed_at": 1451086576.7848463 + }, + "binary_switch_id": "95585", + "name": "Front room", + "locale": "en_us", + "units": {}, + "created_at": 1442828654, + "hidden_at": null, + "capabilities": { + "zwave_association_command_class": false, + "fields": [ + { + "field": "connection", + "type": "boolean", + "mutability": "read-only" + }, + { + "field": "powered", + "type": "boolean", + "mutability": "read-write" + } + ] + }, + "triggers": [], + "manufacturer_device_model": "ge_jasco_binary", + "manufacturer_device_id": null, + "device_manufacturer": "ge", + "model_name": "Binary Switch", + "upc_id": "200", + "upc_code": "JASCO_ZWAVE_BINARY_POWER", + "gang_id": null, + "hub_id": "96647", + "local_id": "24", + "radio_type": "zwave", + "linked_service_id": null, + "current_budget": null, + "lat_lng": [ + null, + null + ], + "location": "", + "order": 0 + }, + { + "desired_state": { + "powered": true + }, + "last_reading": { + "connection": true, + "connection_updated_at": 1453186644.045074, + "powered": true, + "powered_updated_at": 1453186644.045074, + "desired_powered_updated_at": 1453168245.179851, + "desired_powered_changed_at": 1453168102.6182692, + "connection_changed_at": 1451086500.5488758, + "powered_changed_at": 1453186644.045074 + }, + "binary_switch_id": "95586", + "name": "Kitchen", + "locale": "en_us", + "units": {}, + "created_at": 1442828786, + "hidden_at": null, + "capabilities": { + "zwave_association_command_class": false, + "fields": [ + { + "field": "connection", + "type": "boolean", + "mutability": "read-only" + }, + { + "field": "powered", + "type": "boolean", + "mutability": "read-write" + } + ] + }, + "triggers": [], + "manufacturer_device_model": "ge_jasco_binary", + "manufacturer_device_id": null, + "device_manufacturer": "ge", + "model_name": "Binary Switch", + "upc_id": "200", + "upc_code": "JASCO_ZWAVE_BINARY_POWER", + "gang_id": null, + "hub_id": "96647", + "local_id": "25", + "radio_type": "zwave", + "linked_service_id": null, + "current_budget": null, + "lat_lng": [ + null, + null + ], + "location": "", + "order": 0 + }, + { + "desired_state": { + "powered": false + }, + "last_reading": { + "connection": true, + "connection_updated_at": 1453186721.836749, + "powered": false, + "powered_updated_at": 1453187078.2694647, + "desired_powered_updated_at": 1453187078.3215687, + "desired_powered_changed_at": 1453187078.3215687, + "connection_changed_at": 1451086457.9053743, + "powered_changed_at": 1453186721.836749 + }, + "binary_switch_id": "95587", + "name": "Deck Light", + "locale": "en_us", + "units": {}, + "created_at": 1442828870, + "hidden_at": null, + "capabilities": { + "zwave_association_command_class": false, + "fields": [ + { + "field": "connection", + "type": "boolean", + "mutability": "read-only" + }, + { + "field": "powered", + "type": "boolean", + "mutability": "read-write" + } + ] + }, + "triggers": [], + "manufacturer_device_model": "ge_jasco_binary", + "manufacturer_device_id": null, + "device_manufacturer": "ge", + "model_name": "Binary Switch", + "upc_id": "200", + "upc_code": "JASCO_ZWAVE_BINARY_POWER", + "gang_id": null, + "hub_id": "96647", + "local_id": "26", + "radio_type": "zwave", + "linked_service_id": null, + "current_budget": null, + "lat_lng": [ + null, + null + ], + "location": "", + "order": 0 + }, + { + "desired_state": { + "powered": false, + "brightness": 1 + }, + "last_reading": { + "connection": true, + "connection_updated_at": 1453181414.8290415, + "powered": false, + "powered_updated_at": 1453181414.8290415, + "brightness": 1, + "brightness_updated_at": 1453181414.8290415, + "desired_powered_updated_at": 1453092459.7353978, + "desired_brightness_updated_at": 1453092459.7353978, + "powered_changed_at": 1453181414.8290415, + "desired_powered_changed_at": 1453092459.7353978, + "desired_brightness_changed_at": 1453092459.7353978, + "brightness_changed_at": 1453046456.5470738 + }, + "light_bulb_id": "951959", + "name": "Bedroom", + "locale": "en_us", + "units": {}, + "created_at": 1443227645, + "hidden_at": null, + "capabilities": {}, + "triggers": [], + "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", + "upc_code": "784276072076", + "gang_id": null, + "hub_id": "96647", + "local_id": "28", + "radio_type": "lutron", + "linked_service_id": null, + "lat_lng": [ + null, + null + ], + "location": "", + "order": 0 + }, + { + "remote_id": "43849", + "name": "Bedroom Remote", + "members": [ + { + "object_type": "light_bulb", + "object_id": "951959", + "desired_state": { + "powered_updated_at": 1453092459.7353978, + "brightness_updated_at": 1453092459.7353978, + "actor_id_updated_at": 1453092459.7353978, + "actor_type_updated_at": 1453092459.7353978, + "triggering_object_id_updated_at": 1453092459.7353978, + "triggering_object_type_updated_at": 1453092459.7353978, + "powered_changed_at": 1453092459.7353978, + "actor_id_changed_at": 1453092459.7353978, + "actor_type_changed_at": 1453092459.7353978, + "brightness_changed_at": 1453092459.7353978, + "triggering_object_type_changed_at": 1452766532.86446, + "triggering_object_id_changed_at": 1452766532.86446 + } + } + ], + "desired_state": {}, + "last_reading": { + "connection": true, + "connection_updated_at": 1443227691.5931447, + "remote_pairable": false, + "remote_pairable_updated_at": 1443227696.5397356 + }, + "locale": "en_us", + "units": {}, + "created_at": 1443227691, + "hidden_at": null, + "capabilities": {}, + "device_manufacturer": "lutron", + "model_name": "Pico", + "upc_id": "99", + "upc_code": "784276067782", + "hub_id": "96647", + "local_id": "29", + "radio_type": "lutron", + "lat_lng": [ + null, + null + ], + "location": "" + }, + { + "desired_state": { + "powered": true, + "brightness": 1 + }, + "last_reading": { + "connection": true, + "connection_updated_at": 1453149475.4529073, + "powered": true, + "powered_updated_at": 1453149475.4529073, + "brightness": 1, + "brightness_updated_at": 1453149475.4529073, + "desired_powered_updated_at": 1453047163.5160596, + "desired_brightness_updated_at": 1453047163.5160596, + "desired_powered_changed_at": 1453047163.5160596, + "desired_brightness_changed_at": 1453020127.224208, + "powered_changed_at": 1453149475.4529073, + "brightness_changed_at": 1453149475.4529073 + }, + "light_bulb_id": "952023", + "name": "Living room ", + "locale": "en_us", + "units": {}, + "created_at": 1443229228, + "hidden_at": null, + "capabilities": {}, + "triggers": [], + "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", + "upc_code": "784276072076", + "gang_id": null, + "hub_id": "96647", + "local_id": "30", + "radio_type": "lutron", + "linked_service_id": null, + "lat_lng": [ + null, + null + ], + "location": "", + "order": 0 + }, + { + "remote_id": "43853", + "name": "Livingroom Remote", + "members": [ + { + "object_type": "light_bulb", + "object_id": "952023", + "desired_state": { + "powered_updated_at": 1453047163.5160596, + "brightness_updated_at": 1453047163.5160596, + "actor_id_updated_at": 1453047163.5160596, + "actor_type_updated_at": 1453047163.5160596, + "triggering_object_id_updated_at": 1453047163.5160596, + "triggering_object_type_updated_at": 1453047163.5160596, + "powered_changed_at": 1453047163.5160596, + "brightness_changed_at": 1453020127.224208, + "actor_id_changed_at": 1453047163.5160596, + "actor_type_changed_at": 1453047163.5160596, + "triggering_object_type_changed_at": 1452766538.1142526, + "triggering_object_id_changed_at": 1452766538.1142526 + } + } + ], + "desired_state": {}, + "last_reading": { + "connection": true, + "connection_updated_at": 1443229558.9623888, + "remote_pairable": null, + "remote_pairable_updated_at": null + }, + "locale": "en_us", + "units": {}, + "created_at": 1443229558, + "hidden_at": null, + "capabilities": {}, + "device_manufacturer": "lutron", + "model_name": "Pico", + "upc_id": "99", + "upc_code": "784276067782", + "hub_id": "96647", + "local_id": "31", + "radio_type": "lutron", + "lat_lng": [ + null, + null + ], + "location": "" + }, + { + "desired_state": { + "powered": false + }, + "last_reading": { + "connection": true, + "connection_updated_at": 1453134330.2385209, + "powered": false, + "powered_updated_at": 1453134330.2385209, + "desired_powered_updated_at": 1453047163.2106736, + "connection_changed_at": 1451086581.1163218, + "powered_changed_at": 1452869923.705073, + "desired_powered_changed_at": 1453047163.2106736 + }, + "binary_switch_id": "110349", + "name": "Well heat", + "locale": "en_us", + "units": {}, + "created_at": 1445887221, + "hidden_at": null, + "capabilities": { + "fields": [ + { + "field": "connection", + "type": "boolean", + "mutability": "read-only" + }, + { + "field": "powered", + "type": "boolean", + "mutability": "read-write" + } + ] + }, + "triggers": [], + "manufacturer_device_model": null, + "manufacturer_device_id": null, + "device_manufacturer": null, + "model_name": "Binary Switch", + "upc_id": "78", + "upc_code": "GENERIC_ZWAVE_BINARY", + "gang_id": null, + "hub_id": "96647", + "local_id": "34", + "radio_type": "zwave", + "linked_service_id": null, + "current_budget": null, + "lat_lng": [ + null, + null + ], + "location": "", + "order": 0 + }, + { + "desired_state": { + "locked": true + }, + "last_reading": { + "connection": true, + "connection_updated_at": 1453186721.4189656, + "locked": true, + "locked_updated_at": 1453186721.4189656, + "battery": 0.7, + "battery_updated_at": 1453186721.4189656, + "last_error": "error_reported_by_hub", + "last_error_updated_at": 1448273779.911335, + "desired_locked_updated_at": 1453098412.962081, + "connection_changed_at": 1448064828.026504, + "locked_changed_at": 1453186721.4189656, + "battery_changed_at": 1452519585.7874553, + "desired_locked_changed_at": 1453098412.962081, + "last_error_changed_at": 1448273779.911335 + }, + "lock_id": "58044", + "name": "Primrose access panel", + "locale": "en_us", + "units": {}, + "created_at": 1447842046, + "hidden_at": null, + "capabilities": { + "home_security_device": true, + "fields": [ + { + "field": "connection", + "type": "boolean", + "mutability": "read-only" + }, + { + "field": "locked", + "type": "boolean", + "mutability": "read-write" + }, + { + "field": "battery", + "type": "percentage", + "mutability": "read-only" + } + ] + }, + "triggers": [], + "manufacturer_device_model": "kwikset_zwave_lock", + "manufacturer_device_id": null, + "device_manufacturer": "kwikset", + "model_name": "Z-Wave Deadbolt", + "upc_id": "14", + "upc_code": "883351278591", + "hub_id": "96647", + "local_id": "35", + "radio_type": "zwave", + "lat_lng": [ + null, + null + ], + "location": "" + }, + { + "desired_state": { + "powered": false, + "brightness": 1 + }, + "last_reading": { + "connection": true, + "connection_updated_at": 1453187087.5703778, + "powered": false, + "powered_updated_at": 1453187087.5703778, + "brightness": 1, + "brightness_updated_at": 1453187087.5703778, + "desired_powered_updated_at": 1453187081.6530936, + "desired_brightness_updated_at": 1453187081.6530936, + "connection_changed_at": 1451272120.5724835, + "brightness_changed_at": 1453112298.9536548, + "powered_changed_at": 1453187081.3301075, + "desired_powered_changed_at": 1453187081.6530936, + "desired_brightness_changed_at": 1453187081.6530936 + }, + "light_bulb_id": "1235824", + "name": "Driveway Light", + "locale": "en_us", + "units": {}, + "created_at": 1451084929, + "hidden_at": null, + "capabilities": { + "zwave_association_command_class": false + }, + "triggers": [], + "manufacturer_device_model": "ge_jasco_dimmer", + "manufacturer_device_id": null, + "device_manufacturer": "ge", + "model_name": "Dimmer", + "upc_id": "203", + "upc_code": "JASCO_ZWAVE_DIMMER", + "gang_id": null, + "hub_id": "96647", + "local_id": "36", + "radio_type": "zwave", + "linked_service_id": null, + "lat_lng": [ + null, + null + ], + "location": "", + "order": 0 + }, + { + "desired_state": { + "pairing_mode": null, + "pairing_prefix": null, + "pairing_mode_duration": null + }, + "last_reading": { + "connection": true, + "connection_updated_at": 1453045548.3366618, + "agent_session_id": "0c88723e3f37e5b70894c2dded5d2a52", + "agent_session_id_updated_at": 1453045548.1992471, + "pairing_mode": null, + "pairing_mode_updated_at": null, + "pairing_prefix": null, + "pairing_prefix_updated_at": null, + "kidde_radio_code_updated_at": null, + "pairing_mode_duration": null, + "pairing_mode_duration_updated_at": null, + "updating_firmware": false, + "updating_firmware_updated_at": 1453045547.8480704, + "firmware_version": "1.0.426", + "firmware_version_updated_at": 1453045548.3366618, + "update_needed": false, + "update_needed_updated_at": 1453045548.3366618, + "mac_address": "B4:79:A7:0F:F8:13", + "mac_address_updated_at": 1453045548.3366618, + "ip_address": "192.168.1.143", + "ip_address_updated_at": 1453045548.3366618, + "hub_version": "user", + "hub_version_updated_at": 1453045548.3366618, + "local_control_public_key_hash": "09:48:E5:7D:36:C7:EF:B6:4D:09:FD:DC:8E:37:C5:12:B5:26:40:8B:42:B2:61:F4:9A:8B:B1:C3:2F:3E:FC:FE", + "local_control_public_key_hash_updated_at": 1452759017.2491362, + "local_control_id": "fc0efae8-1327-4310-bfbf-271afb1a85ac", + "local_control_id_updated_at": 1452759017.2491362, + "desired_pairing_mode": "idle", + "desired_pairing_mode_updated_at": null, + "desired_pairing_prefix": null, + "desired_pairing_prefix_updated_at": null, + "desired_kidde_radio_code": null, + "desired_kidde_radio_code_updated_at": null, + "desired_pairing_mode_duration": null, + "desired_pairing_mode_duration_updated_at": null, + "update_needed_changed_at": 1452758995.61097, + "connection_changed_at": 1452984973.6248682, + "updating_firmware_changed_at": 1452758996.611777, + "agent_session_id_changed_at": 1453045548.1992471, + "hub_version_changed_at": 1452758997.4550383, + "firmware_version_changed_at": 1452758997.4550383, + "mac_address_changed_at": 1452758997.4550383, + "ip_address_changed_at": 1452854078.174485, + "local_control_public_key_hash_changed_at": 1452759017.2491362, + "local_control_id_changed_at": 1452759017.2491362 + }, + "hub_id": "335211", + "uuid": "f61e3ebb-aad7-450a-9bb2-461d04ebcd9b", + "name": "Hal Relay", + "locale": "en_us", + "units": {}, + "created_at": 1452758995, + "hidden_at": null, + "capabilities": { + "oauth2_clients": [ + "wink_project_one" + ], + "home_security_device": true + }, + "triggers": [], + "manufacturer_device_model": "wink_project_one", + "manufacturer_device_id": null, + "device_manufacturer": "wink", + "model_name": "Wink Relay", + "upc_id": "186", + "upc_code": "wink_p1", + "lat_lng": [ + null, + null + ], + "location": null, + "update_needed": false, + "configuration": { + "kidde_radio_code": null + } + }, + { + "desired_state": {}, + "last_reading": { + "connection": true, + "connection_updated_at": 1452758995.7523732 + }, + "gang_id": "27211", + "name": "Gang", + "locale": "en_us", + "units": {}, + "created_at": 1452758995, + "hidden_at": null, + "capabilities": {}, + "manufacturer_device_model": "wink_project_one", + "manufacturer_device_id": null, + "device_manufacturer": "wink", + "model_name": "Wink Relay Gang", + "upc_id": "317", + "upc_code": "wink_project_one_gang", + "hub_id": "335211", + "local_id": null, + "radio_type": null, + "lat_lng": [ + null, + null + ], + "location": "" + }, + { + "last_reading": { + "connection": true, + "connection_updated_at": 1453045548.4129484, + "pressed": false, + "pressed_updated_at": 1453045548.4129484, + "long_pressed": null, + "long_pressed_updated_at": null, + "connection_changed_at": 1452758997.82191, + "pressed_changed_at": 1452846065.0979395 + }, + "button_id": "54870", + "name": "Smart Button", + "locale": "en_us", + "units": {}, + "created_at": 1452758997, + "hidden_at": null, + "capabilities": {}, + "manufacturer_device_model": "wink_relay_button", + "manufacturer_device_id": null, + "device_manufacturer": "wink", + "model_name": "Wink Relay Button", + "upc_id": "316", + "upc_code": "wink_p1_button", + "gang_id": "27211", + "hub_id": "335211", + "local_id": "4", + "radio_type": "project_one", + "lat_lng": [ + null, + null + ], + "location": "" + }, + { + "last_reading": { + "connection": true, + "connection_updated_at": 1453045548.3851051, + "pressed": false, + "pressed_updated_at": 1453045548.3851051, + "long_pressed": null, + "long_pressed_updated_at": null, + "connection_changed_at": 1452758997.8721824, + "pressed_changed_at": 1452846070.666879 + }, + "button_id": "54871", + "name": "Smart Button", + "locale": "en_us", + "units": {}, + "created_at": 1452758997, + "hidden_at": null, + "capabilities": {}, + "manufacturer_device_model": "wink_relay_button", + "manufacturer_device_id": null, + "device_manufacturer": "wink", + "model_name": "Wink Relay Button", + "upc_id": "316", + "upc_code": "wink_p1_button", + "gang_id": "27211", + "hub_id": "335211", + "local_id": "5", + "radio_type": "project_one", + "lat_lng": [ + null, + null + ], + "location": "" + }, + { + "desired_state": { + "powered": false, + "powering_mode": null + }, + "last_reading": { + "connection": true, + "connection_updated_at": 1453150819.4565022, + "powered": false, + "powered_updated_at": 1453150819.4565022, + "powering_mode": null, + "powering_mode_updated_at": null, + "desired_powered_updated_at": 1453150819.5289104, + "desired_powering_mode_updated_at": 1453150946.3513427, + "connection_changed_at": 1452758997.930097, + "powered_changed_at": 1453150819.4565022, + "desired_powered_changed_at": 1453150819.5289104, + "desired_powering_mode_changed_at": 1452837974.2831588 + }, + "binary_switch_id": "158504", + "name": "Street Light", + "locale": "en_us", + "units": {}, + "created_at": 1452758997, + "hidden_at": null, + "capabilities": { + "fields": [ + { + "field": "connection", + "type": "boolean", + "mutability": "read-only" + }, + { + "field": "powered", + "type": "boolean", + "mutability": "read-write" + }, + { + "field": "powering_mode", + "type": "selection", + "mutability": "read-write", + "choices": [ + "smart", + "dumb", + "none" + ] + } + ] + }, + "triggers": [], + "manufacturer_device_model": "wink_relay_switch", + "manufacturer_device_id": null, + "device_manufacturer": "wink", + "model_name": "Wink Relay Switch", + "upc_id": "315", + "upc_code": "wink_p1_binary_switch", + "gang_id": "27211", + "hub_id": "335211", + "local_id": "2", + "radio_type": "project_one", + "linked_service_id": null, + "current_budget": null, + "lat_lng": [ + null, + null + ], + "location": "", + "order": 0 + }, + { + "desired_state": { + "powered": false, + "powering_mode": "dumb" + }, + "last_reading": { + "connection": true, + "connection_updated_at": 1453150820.9220295, + "powered": false, + "powered_updated_at": 1453150820.9220295, + "powering_mode": "dumb", + "powering_mode_updated_at": 1452843917.4219997, + "desired_powered_updated_at": 1453150820.9329145, + "desired_powering_mode_updated_at": 1453150963.7616146, + "connection_changed_at": 1452758999.4788241, + "powered_changed_at": 1452856365.3283637, + "desired_powered_changed_at": 1453150820.9329145, + "desired_powering_mode_changed_at": 1452843917.445659, + "powering_mode_changed_at": 1452843890.5603461 + }, + "binary_switch_id": "158505", + "name": "Front Deck", + "locale": "en_us", + "units": {}, + "created_at": 1452758997, + "hidden_at": null, + "capabilities": { + "fields": [ + { + "field": "connection", + "type": "boolean", + "mutability": "read-only" + }, + { + "field": "powered", + "type": "boolean", + "mutability": "read-write" + }, + { + "field": "powering_mode", + "type": "selection", + "mutability": "read-write", + "choices": [ + "smart", + "dumb", + "none" + ] + } + ] + }, + "triggers": [], + "manufacturer_device_model": "wink_relay_switch", + "manufacturer_device_id": null, + "device_manufacturer": "wink", + "model_name": "Wink Relay Switch", + "upc_id": "315", + "upc_code": "wink_p1_binary_switch", + "gang_id": "27211", + "hub_id": "335211", + "local_id": "1", + "radio_type": "project_one", + "linked_service_id": null, + "current_budget": null, + "lat_lng": [ + null, + null + ], + "location": "", + "order": 0 + } + ], + "errors": [], + "pagination": { + "count": 24 + } +} diff --git a/tests/sensor_test.py b/tests/sensor_test.py new file mode 100644 index 0000000..d5c3dc3 --- /dev/null +++ b/tests/sensor_test.py @@ -0,0 +1,14 @@ +import json +import unittest + +from pywink.devices import get_devices_from_response_dict, device_types + + +class SensorTests(unittest.TestCase): + + def test_quirky_spotter_api_response_should_create_unique_five_sensors(self): + with open('api_responses/quirky_spotter.json') as spotter_file: + response_dict = json.load(spotter_file) + + sensors = get_devices_from_response_dict(response_dict, device_types.SENSOR_POD) + self.assertEquals(5, len(sensors))