From 9eb39eaba0717922815e673ad1114c685839d890 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 8 Nov 2015 19:36:29 -0800 Subject: [PATCH 001/178] Version bump to 0.1.1 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b34d8e1..fe15125 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup setup(name='python-wink', - version='0.1', + version='0.1.1', description='Access Wink devices via the Wink API', url='http://github.com/balloob/python-wink', author='John McLaughlin', From b403efcab2560ddef63aa5f45d354f0066ce8eaa Mon Sep 17 00:00:00 2001 From: miniconfig Date: Mon, 16 Nov 2015 16:38:34 -0500 Subject: [PATCH 002/178] Added lock support --- pywink/__init__.py | 204 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 204 insertions(+) diff --git a/pywink/__init__.py b/pywink/__init__.py index 74fbde6..229298a 100644 --- a/pywink/__init__.py +++ b/pywink/__init__.py @@ -18,6 +18,8 @@ def factory(aJSonObj): return wink_sensor_pod(aJSonObj) elif "binary_switch_id" in aJSonObj: return wink_binary_switch(aJSonObj) + elif "lock_id" in aJSonObj: + return wink_lock(aJSonObj) #elif "thermostat_id" in aJSonObj: #elif "remote_id" in aJSonObj: return wink_device(aJSonObj) @@ -361,7 +363,206 @@ def __repr__(self): return "" % ( self.name(), self.deviceId(), self.state()) +class wink_lock(wink_device): + """ represents a wink.py lock + 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": [ + { + "desired_state": { + "locked": true, + "beeper_enabled": true, + "vacation_mode_enabled": false, + "auto_lock_enabled": false, + "key_code_length": 4, + "alarm_mode": null, + "alarm_sensitivity": 0.6, + "alarm_enabled": false + }, + "last_reading": { + "locked": true, + "locked_updated_at": 1417823487.490747, + "connection": true, + "connection_updated_at": 1417823487.490747, + "battery": 0.83, + "battery_updated_at": 1417823487.490747, + "alarm_activated": null, + "alarm_activated_updated_at": null, + "beeper_enabled": true, + "beeper_enabled_updated_at": 1417823487.490747, + "vacation_mode_enabled": false, + "vacation_mode_enabled_updated_at": 1417823487.490747, + "auto_lock_enabled": false, + "auto_lock_enabled_updated_at": 1417823487.490747, + "key_code_length": 4, + "key_code_length_updated_at": 1417823487.490747, + "alarm_mode": null, + "alarm_mode_updated_at": 1417823487.490747, + "alarm_sensitivity": 0.6, + "alarm_sensitivity_updated_at": 1417823487.490747, + "alarm_enabled": true, + "alarm_enabled_updated_at": 1417823487.490747, + "last_error": null, + "last_error_updated_at": 1417823487.490747, + "desired_locked_updated_at": 1417823487.490747, + "desired_beeper_enabled_updated_at": 1417823487.490747, + "desired_vacation_mode_enabled_updated_at": 1417823487.490747, + "desired_auto_lock_enabled_updated_at": 1417823487.490747, + "desired_key_code_length_updated_at": 1417823487.490747, + "desired_alarm_mode_updated_at": 1417823487.490747, + "desired_alarm_sensitivity_updated_at": 1417823487.490747, + "desired_alarm_enabled_updated_at": 1417823487.490747, + "locked_changed_at": 1417823487.490747, + "battery_changed_at": 1417823487.490747, + "desired_locked_changed_at": 1417823487.490747, + "desired_beeper_enabled_changed_at": 1417823487.490747, + "desired_vacation_mode_enabled_changed_at": 1417823487.490747, + "desired_auto_lock_enabled_changed_at": 1417823487.490747, + "desired_key_code_length_changed_at": 1417823487.490747, + "desired_alarm_mode_changed_at": 1417823487.490747, + "desired_alarm_sensitivity_changed_at": 1417823487.490747, + "desired_alarm_enabled_changed_at": 1417823487.490747, + "last_error_changed_at": 1417823487.490747 + }, + "lock_id": "5304", + "name": "Main", + "locale": "en_us", + "units": {}, + "created_at": 1417823382, + "hidden_at": null, + "capabilities": { + "fields": [ + { + "field": "locked", + "type": "boolean", + "mutability": "read-write" + }, + { + "field": "connection", + "mutability": "read-only", + "type": "boolean" + }, + { + "field": "battery", + "mutability": "read-only", + "type": "percentage" + }, + { + "field": "alarm_activated", + "mutability": "read-only", + "type": "boolean" + }, + { + "field": "beeper_enabled", + "type": "boolean" + }, + { + "field": "vacation_mode_enabled", + "type": "boolean" + }, + { + "field": "auto_lock_enabled", + "type": "boolean" + }, + { + "field": "key_code_length", + "type": "integer" + }, + { + "field": "alarm_mode", + "type": "string" + }, + { + "field": "alarm_sensitivity", + "type": "percentage" + }, + { + "field": "alarm_enabled", + "type": "boolean" + } + ], + "home_security_device": true + }, + "triggers": [], + "manufacturer_device_model": "schlage_zwave_lock", + "manufacturer_device_id": null, + "device_manufacturer": "schlage", + "model_name": "BE469", + "upc_id": "11", + "upc_code": "043156312214", + "hub_id": "11780", + "local_id": "1", + "radio_type": "zwave", + "lat_lng": [38.429962, -122.653715], + "location": "" + } + ], + "errors": [], + "pagination": { + "count": 1 + } +} + + """ + def __init__(self, aJSonObj, objectprefix="locks"): + self.jsonState = aJSonObj + self.objectprefix = objectprefix + # Tuple (desired state, time) + self._last_call = (0, None) + + def __repr__(self): + return "" % (self.name(), self.deviceId(), 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 deviceId(self): + return self.jsonState.get('lock_id', self.name()) + + 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": {"locked": state}} + arequest = requests.put(urlString, data=json.dumps(values), headers=headers) + self._updateStateFromResponse(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.updateState() + 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.updateState() + last_read = self._last_reading + + def _recent_state_set(self): + return time.time() - self._last_call[0] < 15 def get_devices(filter): arequestUrl = baseUrl + "/users/me/wink_devices" j = requests.get(arequestUrl, headers=headers).json() @@ -388,6 +589,9 @@ def get_switches(): def get_sensors(): return get_devices('sensor_pod_id') +def get_locks(): + return get_devices('lock_id') + def is_token_set(): """ Returns if an auth token has been set. """ From 42fdcfa721b1bc583688e3592d8427f4c13ba6d9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 19 Nov 2015 23:04:51 -0800 Subject: [PATCH 003/178] Version bump to 0.2 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index fe15125..d85d22d 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup setup(name='python-wink', - version='0.1.1', + version='0.2', description='Access Wink devices via the Wink API', url='http://github.com/balloob/python-wink', author='John McLaughlin', From 2235ff9f45e7618746421885b0b73f1a7c5ea917 Mon Sep 17 00:00:00 2001 From: bradsk88 <7kcwKg60tdcW> Date: Fri, 11 Dec 2015 17:34:30 -0600 Subject: [PATCH 004/178] Added color readout and control to light bulbs --- CHANGELOG.md | 8 ++++++ pywink/__init__.py | 70 ++++++++++++++++++++++++++++++++++++---------- script/__init__.py | 0 script/test | 16 +++++++++++ setup.py | 4 +-- tests/__init__.py | 0 tests/init_test.py | 37 ++++++++++++++++++++++++ 7 files changed, 119 insertions(+), 16 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 script/__init__.py create mode 100755 script/test create mode 100644 tests/__init__.py create mode 100644 tests/init_test.py diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..4c1dde9 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,8 @@ +# Change Log + +## 0.2.1 +- Added ability to change color via setState +- Added ability to request color from wink_bulb object + +## 0.2.0 +- Initial work by balloob, ryanturner, and miniconfig diff --git a/pywink/__init__.py b/pywink/__init__.py index 229298a..7cb87c8 100644 --- a/pywink/__init__.py +++ b/pywink/__init__.py @@ -1,3 +1,5 @@ +import logging + __author__ = 'JOHNMCL' import json @@ -10,7 +12,7 @@ headers = {} class wink_device(object): - + def factory(aJSonObj): if "light_bulb_id" in aJSonObj: return wink_bulb(aJSonObj) @@ -24,33 +26,33 @@ def factory(aJSonObj): #elif "remote_id" in aJSonObj: return wink_device(aJSonObj) factory = staticmethod(factory) - + 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): return self.jsonState.get('name', "Unknown Name") - + def state(self): raise NotImplementedError("Must implement state") - + def deviceId(self): raise NotImplementedError("Must implement state") - + @property def _last_reading(self): return self.jsonState.get('last_reading') or {} - + def _updateStateFromResponse(self, response_json): """ :param response_json: the json obj returned from query :return: """ self.jsonState = response_json.get('data') - + def updateState(self): """ Update state with latest info from Wink API. """ urlString = baseUrl + "/%s/%s" % (self.objectprefix, self.deviceId()) @@ -64,7 +66,7 @@ def refresh_state_at_hub(self): """ urlString = baseUrl + "/%s/%s/refresh" % (self.objectprefix, self.deviceId()) requests.get(urlString, headers=headers) - + class wink_sensor_pod(wink_device) : """ represents a wink.py sensor @@ -157,6 +159,7 @@ def state(self): def deviceId(self): return self.jsonState.get('sensor_pod_id', self.name()) + class wink_binary_switch(wink_device): """ represents a wink.py switch json_obj holds the json stat at init (and if there is a refresh it's updated @@ -338,12 +341,36 @@ def deviceId(self): def brightness(self): return self._last_reading.get('brightness') - def setState(self, state, brightness=None): + 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 setState(self, state, brightness=None, color_kelvin=None, color_xy=None): """ :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 """ - urlString = baseUrl + "/light_bulbs/%s" % self.deviceId() + url_string = baseUrl + "/light_bulbs/%s" % self.deviceId() values = { "desired_state": { "powered": state @@ -353,8 +380,21 @@ def setState(self, state, brightness=None): if brightness is not None: values["desired_state"]["brightness"] = brightness - urlString = baseUrl + "/light_bulbs/%s" % self.deviceId() - arequest = requests.put(urlString, data=json.dumps(values), headers=headers) + 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 = baseUrl + "/light_bulbs/%s" % self.deviceId() + arequest = requests.put(url_string, data=json.dumps(values), headers=headers) self._updateStateFromResponse(arequest.json()) self._last_call = (time.time(), state) @@ -363,6 +403,7 @@ def __repr__(self): return "" % ( self.name(), self.deviceId(), self.state()) + class wink_lock(wink_device): """ represents a wink.py lock json_obj holds the json stat at init (and if there is a refresh it's updated @@ -505,8 +546,8 @@ class wink_lock(wink_device): "count": 1 } } +""" - """ def __init__(self, aJSonObj, objectprefix="locks"): self.jsonState = aJSonObj self.objectprefix = objectprefix @@ -563,6 +604,7 @@ def wait_till_desired_reached(self): def _recent_state_set(self): return time.time() - self._last_call[0] < 15 + def get_devices(filter): arequestUrl = baseUrl + "/users/me/wink_devices" j = requests.get(arequestUrl, headers=headers).json() diff --git a/script/__init__.py b/script/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/script/test b/script/test new file mode 100755 index 0000000..e4314b3 --- /dev/null +++ b/script/test @@ -0,0 +1,16 @@ +#!/bin/sh + +# script/test: Run test suite for application. Optionallly pass in a path to an +# individual test file to run a single test. + +cd "$(dirname "$0")/.." + +echo "Running tests..." + +if [ "$1" = "coverage" ]; then + py.test --cov --cov-report= + TEST_STATUS=$? +else + py.test + TEST_STATUS=$? +fi diff --git a/setup.py b/setup.py index d85d22d..c454b04 100644 --- a/setup.py +++ b/setup.py @@ -1,11 +1,11 @@ from setuptools import setup setup(name='python-wink', - version='0.2', + version='0.2.1', description='Access Wink devices via the Wink API', url='http://github.com/balloob/python-wink', author='John McLaughlin', license='MIT', - install_requires=['requests>=2.0'], + install_requires=['requests>=2.0', 'mock'], packages=['pywink'], zip_safe=True) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/init_test.py b/tests/init_test.py new file mode 100644 index 0000000..b782662 --- /dev/null +++ b/tests/init_test.py @@ -0,0 +1,37 @@ +import json +import unittest +import mock +from pywink import wink_bulb + + +class LightSetStateTests(unittest.TestCase): + + @mock.patch('requests.put') + def test_should_send_correct_color_xy_values_to_wink_api(self, put_mock): + bulb = wink_bulb({}) + color_x = 0.75 + color_y = 0.25 + bulb.setState(True, color_xy=[color_x, color_y]) + sent_data = json.loads(put_mock.call_args[1].get('data')) + self.assertEquals(color_x, sent_data.get('desired_state', {}).get('color_x')) + self.assertEquals(color_y, sent_data.get('desired_state', {}).get('color_y')) + self.assertEquals('xy', sent_data['desired_state'].get('color_model')) + + @mock.patch('requests.put') + def test_should_send_correct_color_temperature_values_to_wink_api(self, put_mock): + bulb = wink_bulb({}) + arbitrary_kelvin_color = 4950 + bulb.setState(True, color_kelvin=arbitrary_kelvin_color) + sent_data = json.loads(put_mock.call_args[1].get('data')) + self.assertEquals('color_temperature', sent_data['desired_state'].get('color_model')) + self.assertEquals(arbitrary_kelvin_color, sent_data['desired_state'].get('color_temperature')) + + @mock.patch('requests.put') + def test_should_only_send_color_xy_if_both_color_xy_and_color_temperature_are_given(self, put_mock): + bulb = wink_bulb({}) + arbitrary_kelvin_color = 4950 + bulb.setState(True, color_kelvin=arbitrary_kelvin_color, color_xy=[0, 1]) + sent_data = json.loads(put_mock.call_args[1].get('data')) + self.assertEquals('color_temperature', sent_data['desired_state'].get('color_model')) + self.assertNotIn('color_x', sent_data['desired_state']) + self.assertNotIn('color_y', sent_data['desired_state']) From 4456b9a7d1692abdc0bc1bf529c0f10248163446 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Mon, 14 Dec 2015 13:16:55 -0500 Subject: [PATCH 005/178] Added Egg Minder support --- pywink/__init__.py | 106 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/pywink/__init__.py b/pywink/__init__.py index 7cb87c8..72183e0 100644 --- a/pywink/__init__.py +++ b/pywink/__init__.py @@ -22,6 +22,8 @@ def factory(aJSonObj): return wink_binary_switch(aJSonObj) elif "lock_id" in aJSonObj: return wink_lock(aJSonObj) + elif "eggtray_id" in aJSonObj: + return wink_eggtray(aJSonObj) #elif "thermostat_id" in aJSonObj: #elif "remote_id" in aJSonObj: return wink_device(aJSonObj) @@ -67,6 +69,108 @@ def refresh_state_at_hub(self): urlString = baseUrl + "/%s/%s/refresh" % (self.objectprefix, self.deviceId()) requests.get(urlString, headers=headers) +ass wink_eggtray(wink_device) : + """ represents a wink.py egg tray + 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": { + "last_reading": { + "connection": true, + "connection_updated_at": 1417823487.490747, + "battery": 0.83, + "battery_updated_at": 1417823487.490747, + "inventory": 3, + "inventory_updated_at": 1449705551.7313306, + "freshness_remaining": 2419191, + "freshness_remaining_updated_at": 1449705551.7313495, + "age_updated_at": 1449705551.7313418, + "age": 1449705542, + "connection_changed_at": 1449705443.6858568, + "next_trigger_at_updated_at": None, + "next_trigger_at": None, + "egg_1_timestamp_updated_at": 1449753143.8631344, + "egg_1_timestamp_changed_at": 1449705534.0782206, + "egg_1_timestamp": 1449705545.0, + "egg_2_timestamp_updated_at": 1449753143.8631344, + "egg_2_timestamp_changed_at": 1449705534.0782206, + "egg_2_timestamp": 1449705545.0, + "egg_3_timestamp_updated_at": 1449753143.8631344, + "egg_3_timestamp_changed_at": 1449705534.0782206, + "egg_3_timestamp": 1449705545.0, + "egg_4_timestamp_updated_at": 1449753143.8631344, + "egg_4_timestamp_changed_at": 1449705534.0782206, + "egg_4_timestamp": 1449705545.0, + "egg_5_timestamp_updated_at": 1449753143.8631344, + "egg_5_timestamp_changed_at": 1449705534.0782206, + "egg_5_timestamp": 1449705545.0, + "egg_6_timestamp_updated_at": 1449753143.8631344, + "egg_6_timestamp_changed_at": 1449705534.0782206, + "egg_6_timestamp": 1449705545.0, + "egg_7_timestamp_updated_at": 1449753143.8631344, + "egg_7_timestamp_changed_at": 1449705534.0782206, + "egg_7_timestamp": 1449705545.0, + "egg_8_timestamp_updated_at": 1449753143.8631344, + "egg_8_timestamp_changed_at": 1449705534.0782206, + "egg_8_timestamp": 1449705545.0, + "egg_9_timestamp_updated_at": 1449753143.8631344, + "egg_9_timestamp_changed_at": 1449705534.0782206, + "egg_9_timestamp": 1449705545.0, + "egg_10_timestamp_updated_at": 1449753143.8631344, + "egg_10_timestamp_changed_at": 1449705534.0782206, + "egg_10_timestamp": 1449705545.0, + "egg_11_timestamp_updated_at": 1449753143.8631344, + "egg_11_timestamp_changed_at": 1449705534.0782206, + "egg_11_timestamp": 1449705545.0, + "egg_12_timestamp_updated_at": 1449753143.8631344, + "egg_12_timestamp_changed_at": 1449705534.0782206, + "egg_12_timestamp": 1449705545.0, + "egg_13_timestamp_updated_at": 1449753143.8631344, + "egg_13_timestamp_changed_at": 1449705534.0782206, + "egg_13_timestamp": 1449705545.0, + "egg_14_timestamp_updated_at": 1449753143.8631344, + "egg_14_timestamp_changed_at": 1449705534.0782206, + "egg_14_timestamp": 1449705545.0, + }, + "eggtray_id": "153869", + "name": "Egg Minder", + "freshness_period": 2419200, + "locale": "en_us", + "units": {}, + "created_at": 1417823382, + "hidden_at": null, + "capabilities": {}, + "triggers": [], + "device_manufacturer": "quirky_ge", + "model_name": "Egg Minder", + "upc_id": "23", + "upc_code": "814434017233", + "lat_lng": [38.429962, -122.653715], + "location": "" + }, + "errors": [], + "pagination": { + "count": 1 + } +} + + """ + def __init__(self, aJSonObj, objectprefix="eggtrays"): + self.jsonState = aJSonObj + self.objectprefix = objectprefix + + def __repr__(self): + return "" % (self.name(), self.deviceId(), self.state()) + + def state(self): + if 'inventory' in self._last_reading: + return self._last_reading['inventory'] + return false + + def deviceId(self): + return self.jsonState.get('eggtray_id', self.name()) + class wink_sensor_pod(wink_device) : """ represents a wink.py sensor @@ -634,6 +738,8 @@ def get_sensors(): def get_locks(): return get_devices('lock_id') +def get_eggtrays() + return get_devices('eggtray_id') def is_token_set(): """ Returns if an auth token has been set. """ From bf0cebc0538e9447b9a0e109a82fe9952b475f43 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Mon, 14 Dec 2015 13:25:06 -0500 Subject: [PATCH 006/178] Fix typos --- pywink/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pywink/__init__.py b/pywink/__init__.py index 72183e0..438ca04 100644 --- a/pywink/__init__.py +++ b/pywink/__init__.py @@ -69,7 +69,7 @@ def refresh_state_at_hub(self): urlString = baseUrl + "/%s/%s/refresh" % (self.objectprefix, self.deviceId()) requests.get(urlString, headers=headers) -ass wink_eggtray(wink_device) : +class wink_eggtray(wink_device) : """ represents a wink.py egg tray 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 @@ -738,7 +738,7 @@ def get_sensors(): def get_locks(): return get_devices('lock_id') -def get_eggtrays() +def get_eggtrays(): return get_devices('eggtray_id') def is_token_set(): From 1156f666745d6db27a9440214224ec03c837c618 Mon Sep 17 00:00:00 2001 From: bradsk88 <7kcwKg60tdcW> Date: Mon, 14 Dec 2015 21:47:45 -0600 Subject: [PATCH 007/178] Added lint script Updated code to pass pylint and flake8 --- CHANGELOG.md | 3 + README.md | 2 +- pylintrc | 9 ++ pywink/__init__.py | 215 +++++++++++++++++++++++---------------------- script/lint | 19 ++++ script/test | 11 +++ setup.py | 2 +- tests/init_test.py | 16 ++-- tox.ini | 2 + 9 files changed, 163 insertions(+), 116 deletions(-) create mode 100644 pylintrc create mode 100755 script/lint create mode 100644 tox.ini diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c1dde9..336f33e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 0.3.0 +- Breaking change: Renamed classes to satisfy pylint + ## 0.2.1 - Added ability to change color via setState - Added ability to request color from wink_bulb object diff --git a/README.md b/README.md index 68dd088..7a2451e 100644 --- a/README.md +++ b/README.md @@ -25,5 +25,5 @@ pywink.set_bearer_token('YOUR_BEARER_TOKEN') for switch in pywink.get_switches(): print(switch.name(), switch.state()) - switch.setState(!switch.state()) + switch.set_state(!switch.state()) ``` diff --git a/pylintrc b/pylintrc new file mode 100644 index 0000000..2bc8970 --- /dev/null +++ b/pylintrc @@ -0,0 +1,9 @@ +[MASTER] +max-line-length=120 + +# Reasons disabled: +# missing-docstring - Document as you like. Good, descriptive method names and variables are preferred over docstrings. +# global-statement - used for the on-demand requirement installation +disable= + missing-docstring + , global-statement diff --git a/pywink/__init__.py b/pywink/__init__.py index 438ca04..da24757 100644 --- a/pywink/__init__.py +++ b/pywink/__init__.py @@ -1,75 +1,80 @@ +""" +Objects for interfacing with the Wink API +""" import logging - -__author__ = 'JOHNMCL' - import json import time - import requests -baseUrl = "https://winkapi.quirky.com" +BASE_URL = "https://winkapi.quirky.com" + +HEADERS = {} -headers = {} -class wink_device(object): +class WinkDevice(object): - def factory(aJSonObj): - if "light_bulb_id" in aJSonObj: - return wink_bulb(aJSonObj) - elif "sensor_pod_id" in aJSonObj: - return wink_sensor_pod(aJSonObj) - elif "binary_switch_id" in aJSonObj: - return wink_binary_switch(aJSonObj) - elif "lock_id" in aJSonObj: - return wink_lock(aJSonObj) - elif "eggtray_id" in aJSonObj: - return wink_eggtray(aJSonObj) - #elif "thermostat_id" in aJSonObj: - #elif "remote_id" in aJSonObj: - return wink_device(aJSonObj) - factory = staticmethod(factory) + @staticmethod + def factory(device_state_as_json): + if "light_bulb_id" in device_state_as_json: + return WinkBulb(device_state_as_json) + elif "sensor_pod_id" in device_state_as_json: + return WinkSensorPod(device_state_as_json) + elif "binary_switch_id" in device_state_as_json: + return WinkBinarySwitch(device_state_as_json) + elif "lock_id" in device_state_as_json: + return WinkLock(device_state_as_json) + elif "eggtray_id" in device_state_as_json: + return WinkEggTray(device_state_as_json) + # elif "thermostat_id" in aJSonObj: + # elif "remote_id" in aJSonObj: + return 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.deviceId(), self.state()) + return "%s %s %s" % (self.name(), self.device_id(), self.state()) def __repr__(self): - return "" % (self.name(), self.deviceId(), self.state()) + return "" % (self.name(), self.device_id(), self.state()) def name(self): - return self.jsonState.get('name', "Unknown Name") + return self.json_state.get('name', "Unknown Name") def state(self): raise NotImplementedError("Must implement state") - def deviceId(self): + def device_id(self): raise NotImplementedError("Must implement state") @property def _last_reading(self): - return self.jsonState.get('last_reading') or {} + return self.json_state.get('last_reading') or {} - def _updateStateFromResponse(self, response_json): + def _update_state_from_response(self, response_json): """ :param response_json: the json obj returned from query :return: """ - self.jsonState = response_json.get('data') + self.json_state = response_json.get('data') - def updateState(self): + def update_state(self): """ Update state with latest info from Wink API. """ - urlString = baseUrl + "/%s/%s" % (self.objectprefix, self.deviceId()) - arequest = requests.get(urlString, headers=headers) - self._updateStateFromResponse(arequest.json()) + url_string = BASE_URL + "/%s/%s" % (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.. """ - urlString = baseUrl + "/%s/%s/refresh" % (self.objectprefix, self.deviceId()) - requests.get(urlString, headers=headers) + url_string = BASE_URL + "/%s/%s/refresh" % (self.objectprefix, self.device_id()) + requests.get(url_string, headers=HEADERS) + -class wink_eggtray(wink_device) : +class WinkEggTray(WinkDevice): """ represents a wink.py egg tray 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 @@ -155,24 +160,20 @@ class wink_eggtray(wink_device) : } } - """ - def __init__(self, aJSonObj, objectprefix="eggtrays"): - self.jsonState = aJSonObj - self.objectprefix = objectprefix - +""" def __repr__(self): - return "" % (self.name(), self.deviceId(), self.state()) + return "" % (self.name(), self.device_id(), self.state()) def state(self): if 'inventory' in self._last_reading: return self._last_reading['inventory'] - return false + return False - def deviceId(self): - return self.jsonState.get('eggtray_id', self.name()) + def device_id(self): + return self.json_state.get('eggtray_id', self.name()) -class wink_sensor_pod(wink_device) : +class WinkSensorPod(WinkDevice): """ represents a wink.py sensor 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 @@ -246,25 +247,25 @@ class wink_sensor_pod(wink_device) : } """ - def __init__(self, aJSonObj, objectprefix="sensor_pods"): - self.jsonState = aJSonObj - self.objectprefix = objectprefix + 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.deviceId(), self.state()) + 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 + return False - def deviceId(self): - return self.jsonState.get('sensor_pod_id', self.name()) + def device_id(self): + return self.json_state.get('sensor_pod_id', self.name()) -class wink_binary_switch(wink_device): +class WinkBinarySwitch(WinkDevice): """ 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 @@ -334,14 +335,14 @@ class wink_binary_switch(wink_device): } """ - def __init__(self, aJSonObj, objectprefix="binary_switches"): - self.jsonState = aJSonObj - self.objectprefix = objectprefix + 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.deviceId(), self.state()) + return "" % (self.name(), self.device_id(), self.state()) def state(self): # Optimistic approach to setState: @@ -351,18 +352,20 @@ def state(self): return self._last_reading.get('powered', False) - def deviceId(self): - return self.jsonState.get('binary_switch_id', self.name()) + def device_id(self): + return self.json_state.get('binary_switch_id', self.name()) - def setState(self, state): + # 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 """ - urlString = baseUrl + "/%s/%s" % (self.objectprefix, self.deviceId()) + url_string = BASE_URL + "/%s/%s" % (self.objectprefix, self.device_id()) values = {"desired_state": {"powered": state}} - arequest = requests.put(urlString, data=json.dumps(values), headers=headers) - self._updateStateFromResponse(arequest.json()) + arequest = requests.put(url_string, data=json.dumps(values), headers=HEADERS) + self._update_state_from_response(arequest.json()) self._last_call = (time.time(), state) @@ -375,7 +378,7 @@ def wait_till_desired_reached(self): tries = 1 while True: - self.updateState() + self.update_state() last_read = self._last_reading if last_read.get('desired_powered') == last_read.get('powered') \ @@ -385,14 +388,14 @@ def wait_till_desired_reached(self): time.sleep(2) tries += 1 - self.updateState() + self.update_state() last_read = self._last_reading def _recent_state_set(self): return time.time() - self._last_call[0] < 15 -class wink_bulb(wink_binary_switch): +class WinkBulb(WinkBinarySwitch): """ 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 @@ -434,13 +437,14 @@ class wink_bulb(wink_binary_switch): "order": 0 """ - jsonState = {} + json_state = {} - def __init__(self, ajsonobj): - super().__init__(ajsonobj, "light_bulbs") + def __init__(self, device_state_as_json): + super().__init__(device_state_as_json, + objectprefix="light_bulbs") - def deviceId(self): - return self.jsonState.get('light_bulb_id', self.name()) + def device_id(self): + return self.json_state.get('light_bulb_id', self.name()) def brightness(self): return self._last_reading.get('brightness') @@ -466,7 +470,7 @@ def color_temperature_kelvin(self): """ return self._last_reading.get('color_temperature') - def setState(self, state, brightness=None, color_kelvin=None, color_xy=None): + 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 @@ -474,7 +478,7 @@ def setState(self, state, brightness=None, color_kelvin=None, color_xy=None): :param color_xy: a pair of floats in a list which specify the desired CIE 1931 x,y color coordinates :return: nothing """ - url_string = baseUrl + "/light_bulbs/%s" % self.deviceId() + url_string = BASE_URL + "/light_bulbs/%s" % self.device_id() values = { "desired_state": { "powered": state @@ -497,18 +501,18 @@ def setState(self, state, brightness=None, color_kelvin=None, color_xy=None): values["desired_state"]["color_x"] = next(color_xy_iter) values["desired_state"]["color_y"] = next(color_xy_iter) - url_string = baseUrl + "/light_bulbs/%s" % self.deviceId() - arequest = requests.put(url_string, data=json.dumps(values), headers=headers) - self._updateStateFromResponse(arequest.json()) + url_string = BASE_URL + "/light_bulbs/%s" % 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.deviceId(), self.state()) + self.name(), self.device_id(), self.state()) -class wink_lock(wink_device): +class WinkLock(WinkDevice): """ represents a wink.py lock 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 @@ -652,14 +656,14 @@ class wink_lock(wink_device): } """ - def __init__(self, aJSonObj, objectprefix="locks"): - self.jsonState = aJSonObj - self.objectprefix = objectprefix + 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.deviceId(), self.state()) + return "" % (self.name(), self.device_id(), self.state()) def state(self): # Optimistic approach to setState: @@ -669,18 +673,18 @@ def state(self): return self._last_reading.get('locked', False) - def deviceId(self): - return self.jsonState.get('lock_id', self.name()) + def device_id(self): + return self.json_state.get('lock_id', self.name()) - def setState(self, state): + def set_state(self, state): """ :param state: a boolean of true (on) or false ('off') :return: nothing """ - urlString = baseUrl + "/%s/%s" % (self.objectprefix, self.deviceId()) + url_string = BASE_URL + "/%s/%s" % (self.objectprefix, self.device_id()) values = {"desired_state": {"locked": state}} - arequest = requests.put(urlString, data=json.dumps(values), headers=headers) - self._updateStateFromResponse(arequest.json()) + arequest = requests.put(url_string, data=json.dumps(values), headers=HEADERS) + self._update_state_from_response(arequest.json()) self._last_call = (time.time(), state) @@ -693,7 +697,7 @@ def wait_till_desired_reached(self): tries = 1 while True: - self.updateState() + self.update_state() last_read = self._last_reading if last_read.get('desired_locked') == last_read.get('locked') \ @@ -703,23 +707,24 @@ def wait_till_desired_reached(self): time.sleep(2) tries += 1 - self.updateState() + self.update_state() last_read = self._last_reading def _recent_state_set(self): return time.time() - self._last_call[0] < 15 -def get_devices(filter): - arequestUrl = baseUrl + "/users/me/wink_devices" - j = requests.get(arequestUrl, headers=headers).json() + +def get_devices(filter_key): + arequest_url = BASE_URL + "/users/me/wink_devices" + j = requests.get(arequest_url, headers=HEADERS).json() items = j.get('data') devices = [] for item in items: - id = item.get(filter) - if (id is not None and item.get("hidden_at") is None): - devices.append(wink_device.factory(item)) + value_at_key = item.get(filter_key) + if value_at_key is not None and item.get("hidden_at") is None: + devices.append(WinkDevice.factory(item)) return devices @@ -735,26 +740,24 @@ def get_switches(): def get_sensors(): return get_devices('sensor_pod_id') + def get_locks(): return get_devices('lock_id') + def get_eggtrays(): - return get_devices('eggtray_id') + return get_devices('eggtray_id') + def is_token_set(): """ Returns if an auth token has been set. """ - return bool(headers) + return bool(HEADERS) def set_bearer_token(token): - global headers + global HEADERS - headers = { + HEADERS = { "Content-Type": "application/json", "Authorization": "Bearer {}".format(token) } - -if __name__ == "__main__": - sw = get_bulbs() - lamp = sw[3] - lamp.setState(False) diff --git a/script/lint b/script/lint new file mode 100755 index 0000000..b8ae40a --- /dev/null +++ b/script/lint @@ -0,0 +1,19 @@ +# Run style checks + +cd "$(dirname "$0")/.." + +echo "Checking style with flake8..." +flake8 pywink + +FLAKE8_STATUS=$? + +echo "Checking style with pylint..." +pylint pywink +PYLINT_STATUS=$? + +if [ $FLAKE8_STATUS -eq 0 ] +then + exit $PYLINT_STATUS +else + exit $FLAKE8_STATUS +fi diff --git a/script/test b/script/test index e4314b3..d407f57 100755 --- a/script/test +++ b/script/test @@ -5,6 +5,10 @@ cd "$(dirname "$0")/.." +script/lint + +LINT_STATUS=$? + echo "Running tests..." if [ "$1" = "coverage" ]; then @@ -14,3 +18,10 @@ else py.test TEST_STATUS=$? fi + +if [ $LINT_STATUS -eq 0 ] +then + exit $TEST_STATUS +else + exit $LINT_STATUS +fi diff --git a/setup.py b/setup.py index c454b04..4a94b47 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup setup(name='python-wink', - version='0.2.1', + version='0.3', description='Access Wink devices via the Wink API', url='http://github.com/balloob/python-wink', author='John McLaughlin', diff --git a/tests/init_test.py b/tests/init_test.py index b782662..ccaf557 100644 --- a/tests/init_test.py +++ b/tests/init_test.py @@ -1,17 +1,17 @@ import json -import unittest import mock -from pywink import wink_bulb +import unittest +from pywink import WinkBulb class LightSetStateTests(unittest.TestCase): @mock.patch('requests.put') def test_should_send_correct_color_xy_values_to_wink_api(self, put_mock): - bulb = wink_bulb({}) + bulb = WinkBulb({}) color_x = 0.75 color_y = 0.25 - bulb.setState(True, color_xy=[color_x, color_y]) + bulb.set_state(True, color_xy=[color_x, color_y]) sent_data = json.loads(put_mock.call_args[1].get('data')) self.assertEquals(color_x, sent_data.get('desired_state', {}).get('color_x')) self.assertEquals(color_y, sent_data.get('desired_state', {}).get('color_y')) @@ -19,18 +19,18 @@ def test_should_send_correct_color_xy_values_to_wink_api(self, put_mock): @mock.patch('requests.put') def test_should_send_correct_color_temperature_values_to_wink_api(self, put_mock): - bulb = wink_bulb({}) + bulb = WinkBulb({}) arbitrary_kelvin_color = 4950 - bulb.setState(True, color_kelvin=arbitrary_kelvin_color) + bulb.set_state(True, color_kelvin=arbitrary_kelvin_color) sent_data = json.loads(put_mock.call_args[1].get('data')) self.assertEquals('color_temperature', sent_data['desired_state'].get('color_model')) self.assertEquals(arbitrary_kelvin_color, sent_data['desired_state'].get('color_temperature')) @mock.patch('requests.put') def test_should_only_send_color_xy_if_both_color_xy_and_color_temperature_are_given(self, put_mock): - bulb = wink_bulb({}) + bulb = WinkBulb({}) arbitrary_kelvin_color = 4950 - bulb.setState(True, color_kelvin=arbitrary_kelvin_color, color_xy=[0, 1]) + bulb.set_state(True, color_kelvin=arbitrary_kelvin_color, color_xy=[0, 1]) sent_data = json.loads(put_mock.call_args[1].get('data')) self.assertEquals('color_temperature', sent_data['desired_state'].get('color_model')) self.assertNotIn('color_x', sent_data['desired_state']) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..6deafc2 --- /dev/null +++ b/tox.ini @@ -0,0 +1,2 @@ +[flake8] +max-line-length = 120 From 29cb1400ce75bcb8bf147b6b590d1e64fcd861e0 Mon Sep 17 00:00:00 2001 From: bradsk88 <7kcwKg60tdcW> Date: Mon, 14 Dec 2015 22:13:15 -0600 Subject: [PATCH 008/178] Update to new style string formatter --- pywink/__init__.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pywink/__init__.py b/pywink/__init__.py index da24757..ba3e880 100644 --- a/pywink/__init__.py +++ b/pywink/__init__.py @@ -46,7 +46,7 @@ def state(self): raise NotImplementedError("Must implement state") def device_id(self): - raise NotImplementedError("Must implement state") + raise NotImplementedError("Must implement device_id") @property def _last_reading(self): @@ -61,7 +61,7 @@ def _update_state_from_response(self, response_json): def update_state(self): """ Update state with latest info from Wink API. """ - url_string = BASE_URL + "/%s/%s" % (self.objectprefix, self.device_id()) + url_string = "{}/{}/{}".format(BASE_URL, self.objectprefix, self.device_id()) arequest = requests.get(url_string, headers=HEADERS) self._update_state_from_response(arequest.json()) @@ -70,7 +70,7 @@ 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 = BASE_URL + "/%s/%s/refresh" % (self.objectprefix, self.device_id()) + url_string = "{}/{}/{}/refresh".format(BASE_URL, self.objectprefix, self.device_id()) requests.get(url_string, headers=HEADERS) @@ -362,7 +362,7 @@ def set_state(self, state, **kwargs): :param state: a boolean of true (on) or false ('off') :return: nothing """ - url_string = BASE_URL + "/%s/%s" % (self.objectprefix, self.device_id()) + 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()) @@ -478,7 +478,7 @@ def set_state(self, state, brightness=None, color_kelvin=None, color_xy=None, ** :param color_xy: a pair of floats in a list which specify the desired CIE 1931 x,y color coordinates :return: nothing """ - url_string = BASE_URL + "/light_bulbs/%s" % self.device_id() + url_string = "{}/light_bulbs/{}".format(BASE_URL, self.device_id()) values = { "desired_state": { "powered": state @@ -501,7 +501,7 @@ def set_state(self, state, brightness=None, color_kelvin=None, color_xy=None, ** values["desired_state"]["color_x"] = next(color_xy_iter) values["desired_state"]["color_y"] = next(color_xy_iter) - url_string = BASE_URL + "/light_bulbs/%s" % self.device_id() + 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()) @@ -681,7 +681,7 @@ def set_state(self, state): :param state: a boolean of true (on) or false ('off') :return: nothing """ - url_string = BASE_URL + "/%s/%s" % (self.objectprefix, self.device_id()) + 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()) @@ -715,7 +715,7 @@ def _recent_state_set(self): def get_devices(filter_key): - arequest_url = BASE_URL + "/users/me/wink_devices" + arequest_url = "{}/users/me/wink_devices".format(BASE_URL) j = requests.get(arequest_url, headers=HEADERS).json() items = j.get('data') From e85c6ab4ad03d5969a4f42b5b176b7563b672cea Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Wed, 16 Dec 2015 13:29:23 -0500 Subject: [PATCH 009/178] Added init to WinkEggTray --- pywink/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pywink/__init__.py b/pywink/__init__.py index ba3e880..3034ac0 100644 --- a/pywink/__init__.py +++ b/pywink/__init__.py @@ -161,6 +161,9 @@ class WinkEggTray(WinkDevice): } """ + def __init__(self, device_state_as_json, objectprefix="eggtrays"): + super(WinkEggTray, self).__init__(device_state_as_json, + objectprefix=objectprefix) def __repr__(self): return "" % (self.name(), self.device_id(), self.state()) From 28fda03afc19947497d3023b3c57936ee0ddbc27 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Wed, 16 Dec 2015 14:33:18 -0500 Subject: [PATCH 010/178] Fix style issue --- pywink/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pywink/__init__.py b/pywink/__init__.py index 3034ac0..936e8e6 100644 --- a/pywink/__init__.py +++ b/pywink/__init__.py @@ -163,7 +163,7 @@ class WinkEggTray(WinkDevice): """ def __init__(self, device_state_as_json, objectprefix="eggtrays"): super(WinkEggTray, self).__init__(device_state_as_json, - objectprefix=objectprefix) + objectprefix=objectprefix) def __repr__(self): return "" % (self.name(), self.device_id(), self.state()) From 0651e28e1bc573d8046ccaf6ce8ebb331bc7f95d Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Wed, 16 Dec 2015 14:45:05 -0500 Subject: [PATCH 011/178] Fix style issue --- pywink/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pywink/__init__.py b/pywink/__init__.py index 936e8e6..474c689 100644 --- a/pywink/__init__.py +++ b/pywink/__init__.py @@ -164,6 +164,7 @@ class WinkEggTray(WinkDevice): def __init__(self, device_state_as_json, objectprefix="eggtrays"): super(WinkEggTray, self).__init__(device_state_as_json, objectprefix=objectprefix) + def __repr__(self): return "" % (self.name(), self.device_id(), self.state()) From bc916b193465adebd063d0d27dadd059735f9980 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Wed, 16 Dec 2015 14:51:57 -0500 Subject: [PATCH 012/178] Updated version and URL --- CHANGELOG.md | 3 +++ setup.py | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 336f33e..f31fe90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 0.3.1 +- Added init method for WinkEggTray + ## 0.3.0 - Breaking change: Renamed classes to satisfy pylint diff --git a/setup.py b/setup.py index 4a94b47..84b0639 100644 --- a/setup.py +++ b/setup.py @@ -1,9 +1,9 @@ from setuptools import setup setup(name='python-wink', - version='0.3', + version='0.3.1', description='Access Wink devices via the Wink API', - url='http://github.com/balloob/python-wink', + url='http://github.com/bradsk88/python-wink', author='John McLaughlin', license='MIT', install_requires=['requests>=2.0', 'mock'], From 07e7415953dae316b5697fdf15beab4cce773af3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 16 Dec 2015 12:39:01 -0800 Subject: [PATCH 013/178] Make test dependencies test only --- setup.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 84b0639..dbe95e2 100644 --- a/setup.py +++ b/setup.py @@ -6,6 +6,8 @@ url='http://github.com/bradsk88/python-wink', author='John McLaughlin', license='MIT', - install_requires=['requests>=2.0', 'mock'], + install_requires=['requests>=2.0'], + tests_require=['mock'], + test_suite='tests', packages=['pywink'], zip_safe=True) From a1c716aab9cad0515cd0164201c3583ed867d72d Mon Sep 17 00:00:00 2001 From: bradsk88 <7kcwKg60tdcW> Date: Wed, 16 Dec 2015 18:03:27 -0600 Subject: [PATCH 014/178] Changed mock to test-only dependency --- CHANGELOG.md | 3 +++ setup.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f31fe90..98b6353 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 0.3.1.1 +- Changed mock to test-only dependency + ## 0.3.1 - Added init method for WinkEggTray diff --git a/setup.py b/setup.py index dbe95e2..42b132b 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup setup(name='python-wink', - version='0.3.1', + version='0.3.1.1', description='Access Wink devices via the Wink API', url='http://github.com/bradsk88/python-wink', author='John McLaughlin', From 1f544f87f711cc776348e3f7d982e79dbfee16fc Mon Sep 17 00:00:00 2001 From: bradsk88 <7kcwKg60tdcW> Date: Thu, 17 Dec 2015 13:57:32 -0600 Subject: [PATCH 015/178] Updating readme --- .gitignore | 4 ++++ README.md | 6 ------ setup.cfg | 2 ++ 3 files changed, 6 insertions(+), 6 deletions(-) create mode 100644 setup.cfg diff --git a/.gitignore b/.gitignore index 9886e1e..45eff3f 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ +/python_wink.egg-info +/dist *.py[cod] +/test.py +/.cache diff --git a/README.md b/README.md index 7a2451e..97c4fb0 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,6 @@ Python Wink API --------------- -### Script works but no longer maintained. Looking for maintainers. - -Python implementation of the Wink API supporting switches, light bulbs and sensors. - -Script by [John McLaughlin](https://github.com/loghound). - _This script used to be part of Home Assistant. It has been extracted to fit the goal of Home Assistant to not contain any device specific API implementations but rely on open-source implementations of the API._ diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..b88034e --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[metadata] +description-file = README.md From d5eec37c3c1ed5617f4628fcf322da76e4e59de7 Mon Sep 17 00:00:00 2001 From: bradsk88 <7kcwKg60tdcW> Date: Thu, 17 Dec 2015 14:01:03 -0600 Subject: [PATCH 016/178] Basic Travis-CI config --- .travis.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..82b0434 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,10 @@ +sudo: false +language: python +cache: pip +python: + - 3.4 + - 3.5 +script: + - script/test +matrix: + fast_finish: true From 6529261591632e95a811ef0fb9b34d92690705ca Mon Sep 17 00:00:00 2001 From: bradsk88 <7kcwKg60tdcW> Date: Thu, 17 Dec 2015 14:07:14 -0600 Subject: [PATCH 017/178] Updating travis CI config --- .travis.yml | 2 ++ script/before_install | 6 ++++++ 2 files changed, 8 insertions(+) create mode 100644 script/before_install diff --git a/.travis.yml b/.travis.yml index 82b0434..449c433 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,8 @@ cache: pip python: - 3.4 - 3.5 +install: + - script/before_install script: - script/test matrix: diff --git a/script/before_install b/script/before_install new file mode 100644 index 0000000..75fb5dd --- /dev/null +++ b/script/before_install @@ -0,0 +1,6 @@ +install: + echo "Installing dependencies..." + python3 -m pip install --upgrade requests>=2,<3 + + echo "Installing development dependencies.." + python3 -m pip install --upgrade flake8 pylint coveralls pytest pytest-cov From 7642c291724a634ac7d8086765f1ae1c30dcb726 Mon Sep 17 00:00:00 2001 From: bradsk88 <7kcwKg60tdcW> Date: Thu, 17 Dec 2015 14:09:12 -0600 Subject: [PATCH 018/178] Making script runnable --- script/before_install | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 script/before_install diff --git a/script/before_install b/script/before_install old mode 100644 new mode 100755 From 3c5b242d9279fabb0c922672f507484908f8adcd Mon Sep 17 00:00:00 2001 From: Eric Rolf Date: Sat, 19 Dec 2015 20:34:16 -0500 Subject: [PATCH 019/178] Initial Wink Garage Door Component. --- pywink/__init__.py | 139 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/pywink/__init__.py b/pywink/__init__.py index 474c689..037cea2 100644 --- a/pywink/__init__.py +++ b/pywink/__init__.py @@ -25,6 +25,8 @@ def factory(device_state_as_json): return WinkLock(device_state_as_json) elif "eggtray_id" in device_state_as_json: return WinkEggTray(device_state_as_json) + elif "garage_door_id" in device_state_as_json: + return WinkGarageDoor(device_state_as_json) # elif "thermostat_id" in aJSonObj: # elif "remote_id" in aJSonObj: return WinkDevice(device_state_as_json) @@ -718,6 +720,140 @@ def _recent_state_set(self): return time.time() - self._last_call[0] < 15 +class WinkGarageDoor(WinkDevice): + """ 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: + +{ + "data": { + "desired_state": { + "position": 0 + }, + "last_reading": { + "position_opened": "N\/A", + "position_opened_updated_at": 1450357467.371, + "tamper_detected_true": null, + "tamper_detected_true_updated_at": null, + "connection": true, + "connection_updated_at": 1450357538.2715, + "position": 0, + "position_updated_at": 1450357537.836, + "battery": null, + "battery_updated_at": null, + "fault": false, + "fault_updated_at": 1447976866.0784, + "disabled": null, + "disabled_updated_at": null, + "control_enabled": true, + "control_enabled_updated_at": 1447976866.0784, + "desired_position_updated_at": 1447976846.8869, + "connection_changed_at": 1444775470.5484, + "position_changed_at": 1450357537.836, + "control_enabled_changed_at": 1444775472.2474, + "fault_changed_at": 1444775472.2474, + "position_opened_changed_at": 1450357467.371, + "desired_position_changed_at": 1447976846.8869 + }, + "garage_door_id": "30528", + "name": "Garage Door", + "locale": "en_us", + "units": { + + }, + "created_at": 1444775470, + "hidden_at": null, + "capabilities": { + "home_security_device": true + }, + "triggers": [ + + ], + "manufacturer_device_model": "chamberlain_garage_door_opener", + "manufacturer_device_id": "1133930", + "device_manufacturer": "chamberlain", + "model_name": "MyQ Garage Door Controller", + "upc_id": "26", + "upc_code": "012381109302", + "hub_id": null, + "local_id": null, + "radio_type": null, + "linked_service_id": "206203", + "lat_lng": [ + 0, + 0 + ], + "location": "", + "order": null + }, + "errors": [ + + ], + "pagination": { + + } +} +""" + + 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 + + def get_devices(filter_key): arequest_url = "{}/users/me/wink_devices".format(BASE_URL) j = requests.get(arequest_url, headers=HEADERS).json() @@ -753,6 +889,9 @@ def get_eggtrays(): return get_devices('eggtray_id') +def get_garage_doors(): + return get_devices('garage_door_id') + def is_token_set(): """ Returns if an auth token has been set. """ return bool(HEADERS) From 272de0cd8387579b87256c5e0a9eeca739299061 Mon Sep 17 00:00:00 2001 From: Eric Rolf Date: Sun, 20 Dec 2015 14:21:58 -0500 Subject: [PATCH 020/178] Added version number 0.3.2 --- CHANGELOG.md | 3 +++ setup.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98b6353..fd31b5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 0.3.2 +- Added init method for WinkGarageDoor + ## 0.3.1.1 - Changed mock to test-only dependency diff --git a/setup.py b/setup.py index 42b132b..2542d75 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup setup(name='python-wink', - version='0.3.1.1', + version='0.3.2', description='Access Wink devices via the Wink API', url='http://github.com/bradsk88/python-wink', author='John McLaughlin', From 13d2ec0f54318c14cf316342761a2cccfcd4dc62 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Tue, 22 Dec 2015 09:02:15 -0500 Subject: [PATCH 021/178] Fixed styling errors --- pywink/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pywink/__init__.py b/pywink/__init__.py index 037cea2..9a23161 100644 --- a/pywink/__init__.py +++ b/pywink/__init__.py @@ -721,7 +721,7 @@ def _recent_state_set(self): class WinkGarageDoor(WinkDevice): - """ represents a wink.py garage door + 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: @@ -798,7 +798,7 @@ class WinkGarageDoor(WinkDevice): def __init__(self, device_state_as_json, objectprefix="garage_doors"): super(WinkGarageDoor, self).__init__(device_state_as_json, - objectprefix=objectprefix) + objectprefix=objectprefix) # Tuple (desired state, time) self._last_call = (0, None) @@ -892,6 +892,7 @@ def get_eggtrays(): def get_garage_doors(): return get_devices('garage_door_id') + def is_token_set(): """ Returns if an auth token has been set. """ return bool(HEADERS) From 5db4ac7ffc40eb87723bf9877111ecb47b722486 Mon Sep 17 00:00:00 2001 From: Eric Rolf Date: Wed, 23 Dec 2015 12:21:55 -0500 Subject: [PATCH 022/178] Updated per bradsk88 comment. --- pywink/__init__.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/pywink/__init__.py b/pywink/__init__.py index 9a23161..004fb1e 100644 --- a/pywink/__init__.py +++ b/pywink/__init__.py @@ -15,21 +15,23 @@ class WinkDevice(object): @staticmethod def factory(device_state_as_json): + + new_object = None + if "light_bulb_id" in device_state_as_json: - return WinkBulb(device_state_as_json) + new_object = WinkBulb(device_state_as_json) elif "sensor_pod_id" in device_state_as_json: - return WinkSensorPod(device_state_as_json) + new_object = WinkSensorPod(device_state_as_json) elif "binary_switch_id" in device_state_as_json: - return WinkBinarySwitch(device_state_as_json) + new_object = WinkBinarySwitch(device_state_as_json) elif "lock_id" in device_state_as_json: - return WinkLock(device_state_as_json) + new_object = WinkLock(device_state_as_json) elif "eggtray_id" in device_state_as_json: - return WinkEggTray(device_state_as_json) + new_object = WinkEggTray(device_state_as_json) elif "garage_door_id" in device_state_as_json: - return WinkGarageDoor(device_state_as_json) - # elif "thermostat_id" in aJSonObj: - # elif "remote_id" in aJSonObj: - return WinkDevice(device_state_as_json) + new_object = WinkGarageDoor(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 From 439f961779cdaa6f897095117afc0f0d381b7053 Mon Sep 17 00:00:00 2001 From: bradsk88 <7kcwKg60tdcW> Date: Wed, 6 Jan 2016 17:56:24 -0600 Subject: [PATCH 023/178] Fixing pylint --- pywink/__init__.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pywink/__init__.py b/pywink/__init__.py index 004fb1e..0efc2b3 100644 --- a/pywink/__init__.py +++ b/pywink/__init__.py @@ -15,9 +15,12 @@ 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: @@ -30,7 +33,7 @@ def factory(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) - + return new_object or WinkDevice(device_state_as_json) def __init__(self, device_state_as_json, objectprefix=None): From 584e3676e16df505b3124717c92ceea6797f5470 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Wed, 6 Jan 2016 11:49:20 -0500 Subject: [PATCH 024/178] Added support for power strips --- pywink/__init__.py | 285 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 258 insertions(+), 27 deletions(-) diff --git a/pywink/__init__.py b/pywink/__init__.py index 0efc2b3..481c68a 100644 --- a/pywink/__init__.py +++ b/pywink/__init__.py @@ -12,7 +12,6 @@ class WinkDevice(object): - @staticmethod def factory(device_state_as_json): @@ -27,6 +26,8 @@ def factory(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: @@ -44,7 +45,8 @@ def __str__(self): return "%s %s %s" % (self.name(), self.device_id(), self.state()) def __repr__(self): - return "" % (self.name(), self.device_id(), self.state()) + return "" \ + % (self.name(), self.device_id(), self.state()) def name(self): return self.json_state.get('name', "Unknown Name") @@ -68,7 +70,8 @@ def _update_state_from_response(self, response_json): def update_state(self): """ Update state with latest info from Wink API. """ - url_string = "{}/{}/{}".format(BASE_URL, self.objectprefix, self.device_id()) + url_string = "{}/{}/{}".format(BASE_URL, + self.objectprefix, self.device_id()) arequest = requests.get(url_string, headers=HEADERS) self._update_state_from_response(arequest.json()) @@ -77,13 +80,15 @@ 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()) + 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 (and if there is a refresh it's updated + 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: { @@ -168,12 +173,15 @@ class WinkEggTray(WinkDevice): } """ + def __init__(self, device_state_as_json, objectprefix="eggtrays"): super(WinkEggTray, self).__init__(device_state_as_json, objectprefix=objectprefix) def __repr__(self): - return "" % (self.name(), self.device_id(), self.state()) + return "" % (self.name(), + self.device_id(), self.state()) def state(self): if 'inventory' in self._last_reading: @@ -186,7 +194,7 @@ def device_id(self): class WinkSensorPod(WinkDevice): """ represents a wink.py sensor - json_obj holds the json stat at init (and if there is a refresh it's updated + 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: { @@ -258,12 +266,14 @@ class WinkSensorPod(WinkDevice): } """ + 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()) + return "" % (self.name(), + self.device_id(), self.state()) def state(self): if 'opened' in self._last_reading: @@ -278,7 +288,7 @@ def device_id(self): class WinkBinarySwitch(WinkDevice): """ represents a wink.py switch - json_obj holds the json stat at init (and if there is a refresh it's updated + 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: @@ -346,6 +356,7 @@ class WinkBinarySwitch(WinkDevice): } """ + def __init__(self, device_state_as_json, objectprefix="binary_switches"): super(WinkBinarySwitch, self).__init__(device_state_as_json, objectprefix=objectprefix) @@ -353,7 +364,8 @@ def __init__(self, device_state_as_json, objectprefix="binary_switches"): self._last_call = (0, None) def __repr__(self): - return "" % (self.name(), self.device_id(), self.state()) + return "" % (self.name(), + self.device_id(), self.state()) def state(self): # Optimistic approach to setState: @@ -373,9 +385,11 @@ 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()) + 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) + arequest = requests.put(url_string, + data=json.dumps(values), headers=HEADERS) self._update_state_from_response(arequest.json()) self._last_call = (time.time(), state) @@ -393,7 +407,7 @@ def wait_till_desired_reached(self): last_read = self._last_reading if last_read.get('desired_powered') == last_read.get('powered') \ - or tries == 5: + or tries == 5: break time.sleep(2) @@ -408,7 +422,7 @@ def _recent_state_set(self): class WinkBulb(WinkBinarySwitch): """ represents a wink.py bulb - json_obj holds the json stat at init (and if there is a refresh it's updated + 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: @@ -481,12 +495,16 @@ def color_temperature_kelvin(self): """ return self._last_reading.get('color_temperature') - def set_state(self, state, brightness=None, color_kelvin=None, color_xy=None, **kwargs): + 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 + :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()) @@ -500,8 +518,10 @@ def set_state(self, state, brightness=None, color_kelvin=None, color_xy=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.") + 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" @@ -513,7 +533,8 @@ def set_state(self, state, brightness=None, color_kelvin=None, color_xy=None, ** 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) + arequest = requests.put(url_string, + data=json.dumps(values), headers=HEADERS) self._update_state_from_response(arequest.json()) self._last_call = (time.time(), state) @@ -525,7 +546,7 @@ def __repr__(self): class WinkLock(WinkDevice): """ represents a wink.py lock - json_obj holds the json stat at init (and if there is a refresh it's updated + 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: @@ -674,7 +695,8 @@ def __init__(self, device_state_as_json, objectprefix="locks"): self._last_call = (0, None) def __repr__(self): - return "" % (self.name(), self.device_id(), self.state()) + return "" % (self.name(), + self.device_id(), self.state()) def state(self): # Optimistic approach to setState: @@ -692,9 +714,11 @@ 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()) + 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) + arequest = requests.put(url_string, + data=json.dumps(values), headers=HEADERS) self._update_state_from_response(arequest.json()) self._last_call = (time.time(), state) @@ -712,7 +736,202 @@ def wait_till_desired_reached(self): last_read = self._last_reading if last_read.get('desired_locked') == last_read.get('locked') \ - or tries == 5: + 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(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 + and looks like so: + +{ + "errors":[ + + ], + "data":{ + "powerstrip_id":"12345", + "model_name":"Pivot Power Genius", + "created_at":1451578768, + "mac_address":"0c2a69000000", + "locale":"en_us", + "name":"Power strip", + "units":{ + + }, + "last_reading":{ + "connection":true, + "connection_changed_at":1451947138.418391, + "connection_updated_at":1452093346.488989 + }, + "triggers":[ + + ], + "location":"", + "capabilities":{ + + }, + "hidden_at":null, + "outlets":[ + { + "parent_object_type":"powerstrip", + "icon_id":"4", + "desired_state":{ + "powered":false + }, + "parent_object_id":"24313", + "scheduled_outlet_states":[ + + ], + "name":"Outlet #1", + "outlet_index":0, + "last_reading":{ + "desired_powered_updated_at":1452094688.1679382, + "powered_updated_at":1452094688.1461067, + "powered":false, + "powered_changed_at":1452094688.1461067 + }, + "powered":false, + "outlet_id":"48628" + }, + { + "parent_object_type":"powerstrip", + "icon_id":"4", + "desired_state":{ + "powered":false + }, + "parent_object_id":"24313", + "scheduled_outlet_states":[ + + ], + "name":"Outlet #2", + "outlet_index":1, + "last_reading":{ + "desired_powered_updated_at":1452094689.7589157, + "powered_updated_at":1452094689.443459, + "powered":false, + "powered_changed_at":1452094689.443459 + }, + "powered":false, + "outlet_id":"48629" + } + ], + "serial":"AAAA00012345", + "lat_lng":[ + 00.000000, + -00.000000 + ], + "desired_state":{ + + }, + "device_manufacturer":"quirky_ge", + "upc_id":"24", + "upc_code":"814434017226" + }, + "pagination":{ + + } +} + + """ + + 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 "" % (self.name(), + self.device_id(), + self.parent_id(), + 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') + outlets = power_strip.get('outlets', power_strip) + for outlet in outlets: + if outlet.get('outlet_id') == str(self.device_id()): + 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 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('powered', False) + + 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) @@ -869,7 +1088,15 @@ def get_devices(filter_key): for item in items: value_at_key = item.get(filter_key) if value_at_key is not None and item.get("hidden_at") is None: - devices.append(WinkDevice.factory(item)) + if filter_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 @@ -898,6 +1125,10 @@ def get_garage_doors(): return get_devices('garage_door_id') +def get_powerstrip_outlets(): + return get_devices('powerstrip_id') + + def is_token_set(): """ Returns if an auth token has been set. """ return bool(HEADERS) From fd8c4afa895ded7d27fe9519bec44c51095368df Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Wed, 6 Jan 2016 15:50:16 -0500 Subject: [PATCH 025/178] Style changes --- pywink/__init__.py | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/pywink/__init__.py b/pywink/__init__.py index 481c68a..f681a80 100644 --- a/pywink/__init__.py +++ b/pywink/__init__.py @@ -45,8 +45,9 @@ def __str__(self): return "%s %s %s" % (self.name(), self.device_id(), self.state()) def __repr__(self): - return "" \ - % (self.name(), self.device_id(), self.state()) + return "".format(name=self.name(), + device=self.device_id(), + state=self.state()) def name(self): return self.json_state.get('name', "Unknown Name") @@ -179,9 +180,9 @@ def __init__(self, device_state_as_json, objectprefix="eggtrays"): objectprefix=objectprefix) def __repr__(self): - return "" % (self.name(), - self.device_id(), self.state()) + return "".format(name=self.name(), + device=self.device_id(), + state=self.state()) def state(self): if 'inventory' in self._last_reading: @@ -406,8 +407,7 @@ def wait_till_desired_reached(self): self.update_state() last_read = self._last_reading - if last_read.get('desired_powered') == last_read.get('powered') \ - or tries == 5: + if last_read.get('desired_powered') == last_read.get('powered') or tries == 5: break time.sleep(2) @@ -735,8 +735,7 @@ def wait_till_desired_reached(self): self.update_state() last_read = self._last_reading - if last_read.get('desired_locked') == last_read.get('locked') \ - or tries == 5: + if last_read.get('desired_locked') == last_read.get('locked') or tries == 5: break time.sleep(2) @@ -852,11 +851,11 @@ def __init__(self, device_state_as_json, objectprefix="powerstrips"): self._last_call = (0, None) def __repr__(self): - return "" % (self.name(), - self.device_id(), - self.parent_id(), - self.state()) + return "".format(name=self.name(), + device=self.device_id(), + parent_id=self.parent_id(), + state=self.state()) @property def _last_reading(self): @@ -930,8 +929,7 @@ def wait_till_desired_reached(self): self.update_state() last_read = self._last_reading - if last_read.get('desired_powered') == last_read.get('powered') \ - or tries == 5: + if last_read.get('desired_powered') == last_read.get('powered') or tries == 5: break time.sleep(2) From e7315fd82db890da76b92e610f65581ec8706d4f Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Wed, 6 Jan 2016 19:42:09 -0500 Subject: [PATCH 026/178] Updated version and rebased to master --- CHANGELOG.md | 3 +++ setup.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd31b5b..260e823 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 0.3.3 +- Added init method for Wink Power strip + ## 0.3.2 - Added init method for WinkGarageDoor diff --git a/setup.py b/setup.py index 2542d75..04beb39 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup setup(name='python-wink', - version='0.3.2', + version='0.3.3', description='Access Wink devices via the Wink API', url='http://github.com/bradsk88/python-wink', author='John McLaughlin', From b544cbdbf2e6cd4f575d4cec489c75c0dc9077ce Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Thu, 7 Jan 2016 10:28:33 -0500 Subject: [PATCH 027/178] Diable pylint too-many-lines --- pywink/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pywink/__init__.py b/pywink/__init__.py index f681a80..72b4587 100644 --- a/pywink/__init__.py +++ b/pywink/__init__.py @@ -1,3 +1,4 @@ +# pylint: disable=too-many-lines """ Objects for interfacing with the Wink API """ From 396b9ac31343b45b81c32a57aa8789ce7cf97b37 Mon Sep 17 00:00:00 2001 From: bradsk88 <7kcwKg60tdcW> Date: Thu, 7 Jan 2016 23:42:47 -0600 Subject: [PATCH 028/178] Moved API responses out of comments and into unit tests. Some of them were not valid JSON so I'm a bit suspicious about whether or not they work but I don't have some of these devices to get real responses. Will investigate later. --- CHANGELOG.md | 4 + pywink/__init__.py | 632 ++++------------------------------------- pywink/device_types.py | 7 + setup.py | 2 +- tests/init_test.py | 607 ++++++++++++++++++++++++++++++++++++++- 5 files changed, 673 insertions(+), 579 deletions(-) create mode 100644 pywink/device_types.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 260e823..c151d4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Change Log +## 0.4.0 +- Removed API responses from docstring and moved into unit tests. +- Refactored __init__.py to support easier unit testing + ## 0.3.3 - Added init method for Wink Power strip diff --git a/pywink/__init__.py b/pywink/__init__.py index 72b4587..5df5aaf 100644 --- a/pywink/__init__.py +++ b/pywink/__init__.py @@ -1,4 +1,3 @@ -# pylint: disable=too-many-lines """ Objects for interfacing with the Wink API """ @@ -7,10 +6,22 @@ import time import requests +from pywink import device_types + 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' +} + class WinkDevice(object): @staticmethod @@ -92,89 +103,7 @@ 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 - and looks like so: -{ - "data": { - "last_reading": { - "connection": true, - "connection_updated_at": 1417823487.490747, - "battery": 0.83, - "battery_updated_at": 1417823487.490747, - "inventory": 3, - "inventory_updated_at": 1449705551.7313306, - "freshness_remaining": 2419191, - "freshness_remaining_updated_at": 1449705551.7313495, - "age_updated_at": 1449705551.7313418, - "age": 1449705542, - "connection_changed_at": 1449705443.6858568, - "next_trigger_at_updated_at": None, - "next_trigger_at": None, - "egg_1_timestamp_updated_at": 1449753143.8631344, - "egg_1_timestamp_changed_at": 1449705534.0782206, - "egg_1_timestamp": 1449705545.0, - "egg_2_timestamp_updated_at": 1449753143.8631344, - "egg_2_timestamp_changed_at": 1449705534.0782206, - "egg_2_timestamp": 1449705545.0, - "egg_3_timestamp_updated_at": 1449753143.8631344, - "egg_3_timestamp_changed_at": 1449705534.0782206, - "egg_3_timestamp": 1449705545.0, - "egg_4_timestamp_updated_at": 1449753143.8631344, - "egg_4_timestamp_changed_at": 1449705534.0782206, - "egg_4_timestamp": 1449705545.0, - "egg_5_timestamp_updated_at": 1449753143.8631344, - "egg_5_timestamp_changed_at": 1449705534.0782206, - "egg_5_timestamp": 1449705545.0, - "egg_6_timestamp_updated_at": 1449753143.8631344, - "egg_6_timestamp_changed_at": 1449705534.0782206, - "egg_6_timestamp": 1449705545.0, - "egg_7_timestamp_updated_at": 1449753143.8631344, - "egg_7_timestamp_changed_at": 1449705534.0782206, - "egg_7_timestamp": 1449705545.0, - "egg_8_timestamp_updated_at": 1449753143.8631344, - "egg_8_timestamp_changed_at": 1449705534.0782206, - "egg_8_timestamp": 1449705545.0, - "egg_9_timestamp_updated_at": 1449753143.8631344, - "egg_9_timestamp_changed_at": 1449705534.0782206, - "egg_9_timestamp": 1449705545.0, - "egg_10_timestamp_updated_at": 1449753143.8631344, - "egg_10_timestamp_changed_at": 1449705534.0782206, - "egg_10_timestamp": 1449705545.0, - "egg_11_timestamp_updated_at": 1449753143.8631344, - "egg_11_timestamp_changed_at": 1449705534.0782206, - "egg_11_timestamp": 1449705545.0, - "egg_12_timestamp_updated_at": 1449753143.8631344, - "egg_12_timestamp_changed_at": 1449705534.0782206, - "egg_12_timestamp": 1449705545.0, - "egg_13_timestamp_updated_at": 1449753143.8631344, - "egg_13_timestamp_changed_at": 1449705534.0782206, - "egg_13_timestamp": 1449705545.0, - "egg_14_timestamp_updated_at": 1449753143.8631344, - "egg_14_timestamp_changed_at": 1449705534.0782206, - "egg_14_timestamp": 1449705545.0, - }, - "eggtray_id": "153869", - "name": "Egg Minder", - "freshness_period": 2419200, - "locale": "en_us", - "units": {}, - "created_at": 1417823382, - "hidden_at": null, - "capabilities": {}, - "triggers": [], - "device_manufacturer": "quirky_ge", - "model_name": "Egg Minder", - "upc_id": "23", - "upc_code": "814434017233", - "lat_lng": [38.429962, -122.653715], - "location": "" - }, - "errors": [], - "pagination": { - "count": 1 - } -} - -""" + """ def __init__(self, device_state_as_json, objectprefix="eggtrays"): super(WinkEggTray, self).__init__(device_state_as_json, @@ -199,75 +128,7 @@ class WinkSensorPod(WinkDevice): 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: -{ - "data": { - "last_event": { - "brightness_occurred_at": None, - "loudness_occurred_at": None, - "vibration_occurred_at": None - }, - "model_name": "Tripper", - "capabilities": { - "sensor_types": [ - { - "field": "opened", - "type": "boolean" - }, - { - "field": "battery", - "type": "percentage" - } - ] - }, - "manufacturer_device_model": "quirky_ge_tripper", - "location": "", - "radio_type": "zigbee", - "manufacturer_device_id": None, - "gang_id": None, - "sensor_pod_id": "37614", - "subscription": { - }, - "units": { - }, - "upc_id": "184", - "hidden_at": None, - "last_reading": { - "battery_voltage_threshold_2": 0, - "opened": False, - "battery_alarm_mask": 0, - "opened_updated_at": 1421697092.7347496, - "battery_voltage_min_threshold_updated_at": 1421697092.7347229, - "battery_voltage_min_threshold": 0, - "connection": None, - "battery_voltage": 25, - "battery_voltage_threshold_1": 25, - "connection_updated_at": None, - "battery_voltage_threshold_3": 0, - "battery_voltage_updated_at": 1421697092.7347066, - "battery_voltage_threshold_1_updated_at": 1421697092.7347302, - "battery_voltage_threshold_3_updated_at": 1421697092.7347434, - "battery_voltage_threshold_2_updated_at": 1421697092.7347374, - "battery": 1.0, - "battery_updated_at": 1421697092.7347553, - "battery_alarm_mask_updated_at": 1421697092.734716 - }, - "triggers": [ - ], - "name": "MasterBathroom", - "lat_lng": [ - 37.550773, - -122.279182 - ], - "uuid": "a2cb868a-dda3-4211-ab73-fc08087aeed7", - "locale": "en_us", - "device_manufacturer": "quirky_ge", - "created_at": 1421523277, - "local_id": "2", - "hub_id": "88264" - }, -} - - """ + """ def __init__(self, device_state_as_json, objectprefix="sensor_pods"): super(WinkSensorPod, self).__init__(device_state_as_json, @@ -292,72 +153,7 @@ 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 - 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": {} -} - - """ + """ def __init__(self, device_state_as_json, objectprefix="binary_switches"): super(WinkBinarySwitch, self).__init__(device_state_as_json, @@ -422,46 +218,12 @@ def _recent_state_set(self): class WinkBulb(WinkBinarySwitch): - """ represents a wink.py bulb + """ + 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 - 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 + For example API responses, see unit tests. """ json_state = {} @@ -546,148 +308,11 @@ def __repr__(self): class WinkLock(WinkDevice): - """ represents a wink.py lock + """ + 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 - and looks like so: - -{ - "data": [ - { - "desired_state": { - "locked": true, - "beeper_enabled": true, - "vacation_mode_enabled": false, - "auto_lock_enabled": false, - "key_code_length": 4, - "alarm_mode": null, - "alarm_sensitivity": 0.6, - "alarm_enabled": false - }, - "last_reading": { - "locked": true, - "locked_updated_at": 1417823487.490747, - "connection": true, - "connection_updated_at": 1417823487.490747, - "battery": 0.83, - "battery_updated_at": 1417823487.490747, - "alarm_activated": null, - "alarm_activated_updated_at": null, - "beeper_enabled": true, - "beeper_enabled_updated_at": 1417823487.490747, - "vacation_mode_enabled": false, - "vacation_mode_enabled_updated_at": 1417823487.490747, - "auto_lock_enabled": false, - "auto_lock_enabled_updated_at": 1417823487.490747, - "key_code_length": 4, - "key_code_length_updated_at": 1417823487.490747, - "alarm_mode": null, - "alarm_mode_updated_at": 1417823487.490747, - "alarm_sensitivity": 0.6, - "alarm_sensitivity_updated_at": 1417823487.490747, - "alarm_enabled": true, - "alarm_enabled_updated_at": 1417823487.490747, - "last_error": null, - "last_error_updated_at": 1417823487.490747, - "desired_locked_updated_at": 1417823487.490747, - "desired_beeper_enabled_updated_at": 1417823487.490747, - "desired_vacation_mode_enabled_updated_at": 1417823487.490747, - "desired_auto_lock_enabled_updated_at": 1417823487.490747, - "desired_key_code_length_updated_at": 1417823487.490747, - "desired_alarm_mode_updated_at": 1417823487.490747, - "desired_alarm_sensitivity_updated_at": 1417823487.490747, - "desired_alarm_enabled_updated_at": 1417823487.490747, - "locked_changed_at": 1417823487.490747, - "battery_changed_at": 1417823487.490747, - "desired_locked_changed_at": 1417823487.490747, - "desired_beeper_enabled_changed_at": 1417823487.490747, - "desired_vacation_mode_enabled_changed_at": 1417823487.490747, - "desired_auto_lock_enabled_changed_at": 1417823487.490747, - "desired_key_code_length_changed_at": 1417823487.490747, - "desired_alarm_mode_changed_at": 1417823487.490747, - "desired_alarm_sensitivity_changed_at": 1417823487.490747, - "desired_alarm_enabled_changed_at": 1417823487.490747, - "last_error_changed_at": 1417823487.490747 - }, - "lock_id": "5304", - "name": "Main", - "locale": "en_us", - "units": {}, - "created_at": 1417823382, - "hidden_at": null, - "capabilities": { - "fields": [ - { - "field": "locked", - "type": "boolean", - "mutability": "read-write" - }, - { - "field": "connection", - "mutability": "read-only", - "type": "boolean" - }, - { - "field": "battery", - "mutability": "read-only", - "type": "percentage" - }, - { - "field": "alarm_activated", - "mutability": "read-only", - "type": "boolean" - }, - { - "field": "beeper_enabled", - "type": "boolean" - }, - { - "field": "vacation_mode_enabled", - "type": "boolean" - }, - { - "field": "auto_lock_enabled", - "type": "boolean" - }, - { - "field": "key_code_length", - "type": "integer" - }, - { - "field": "alarm_mode", - "type": "string" - }, - { - "field": "alarm_sensitivity", - "type": "percentage" - }, - { - "field": "alarm_enabled", - "type": "boolean" - } - ], - "home_security_device": true - }, - "triggers": [], - "manufacturer_device_model": "schlage_zwave_lock", - "manufacturer_device_id": null, - "device_manufacturer": "schlage", - "model_name": "BE469", - "upc_id": "11", - "upc_code": "043156312214", - "hub_id": "11780", - "local_id": "1", - "radio_type": "zwave", - "lat_lng": [38.429962, -122.653715], - "location": "" - } - ], - "errors": [], - "pagination": { - "count": 1 - } -} -""" + """ def __init__(self, device_state_as_json, objectprefix="locks"): super(WinkLock, self).__init__(device_state_as_json, @@ -754,96 +379,7 @@ class WinkPowerStripOutlet(WinkDevice): 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: - -{ - "errors":[ - - ], - "data":{ - "powerstrip_id":"12345", - "model_name":"Pivot Power Genius", - "created_at":1451578768, - "mac_address":"0c2a69000000", - "locale":"en_us", - "name":"Power strip", - "units":{ - - }, - "last_reading":{ - "connection":true, - "connection_changed_at":1451947138.418391, - "connection_updated_at":1452093346.488989 - }, - "triggers":[ - - ], - "location":"", - "capabilities":{ - - }, - "hidden_at":null, - "outlets":[ - { - "parent_object_type":"powerstrip", - "icon_id":"4", - "desired_state":{ - "powered":false - }, - "parent_object_id":"24313", - "scheduled_outlet_states":[ - - ], - "name":"Outlet #1", - "outlet_index":0, - "last_reading":{ - "desired_powered_updated_at":1452094688.1679382, - "powered_updated_at":1452094688.1461067, - "powered":false, - "powered_changed_at":1452094688.1461067 - }, - "powered":false, - "outlet_id":"48628" - }, - { - "parent_object_type":"powerstrip", - "icon_id":"4", - "desired_state":{ - "powered":false - }, - "parent_object_id":"24313", - "scheduled_outlet_states":[ - - ], - "name":"Outlet #2", - "outlet_index":1, - "last_reading":{ - "desired_powered_updated_at":1452094689.7589157, - "powered_updated_at":1452094689.443459, - "powered":false, - "powered_changed_at":1452094689.443459 - }, - "powered":false, - "outlet_id":"48629" - } - ], - "serial":"AAAA00012345", - "lat_lng":[ - 00.000000, - -00.000000 - ], - "desired_state":{ - - }, - "device_manufacturer":"quirky_ge", - "upc_id":"24", - "upc_code":"814434017226" - }, - "pagination":{ - - } -} - - """ + """ def __init__(self, device_state_as_json, objectprefix="powerstrips"): super(WinkPowerStripOutlet, self).__init__(device_state_as_json, @@ -948,76 +484,7 @@ class WinkGarageDoor(WinkDevice): 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": { - "desired_state": { - "position": 0 - }, - "last_reading": { - "position_opened": "N\/A", - "position_opened_updated_at": 1450357467.371, - "tamper_detected_true": null, - "tamper_detected_true_updated_at": null, - "connection": true, - "connection_updated_at": 1450357538.2715, - "position": 0, - "position_updated_at": 1450357537.836, - "battery": null, - "battery_updated_at": null, - "fault": false, - "fault_updated_at": 1447976866.0784, - "disabled": null, - "disabled_updated_at": null, - "control_enabled": true, - "control_enabled_updated_at": 1447976866.0784, - "desired_position_updated_at": 1447976846.8869, - "connection_changed_at": 1444775470.5484, - "position_changed_at": 1450357537.836, - "control_enabled_changed_at": 1444775472.2474, - "fault_changed_at": 1444775472.2474, - "position_opened_changed_at": 1450357467.371, - "desired_position_changed_at": 1447976846.8869 - }, - "garage_door_id": "30528", - "name": "Garage Door", - "locale": "en_us", - "units": { - - }, - "created_at": 1444775470, - "hidden_at": null, - "capabilities": { - "home_security_device": true - }, - "triggers": [ - - ], - "manufacturer_device_model": "chamberlain_garage_door_opener", - "manufacturer_device_id": "1133930", - "device_manufacturer": "chamberlain", - "model_name": "MyQ Garage Door Controller", - "upc_id": "26", - "upc_code": "012381109302", - "hub_id": null, - "local_id": null, - "radio_type": null, - "linked_service_id": "206203", - "lat_lng": [ - 0, - 0 - ], - "location": "", - "order": null - }, - "errors": [ - - ], - "pagination": { - - } -} -""" + """ def __init__(self, device_state_as_json, objectprefix="garage_doors"): super(WinkGarageDoor, self).__init__(device_state_as_json, @@ -1077,55 +544,66 @@ def _recent_state_set(self): return time.time() - self._last_call[0] < 15 -def get_devices(filter_key): +def get_devices(device_type): arequest_url = "{}/users/me/wink_devices".format(BASE_URL) - j = requests.get(arequest_url, headers=HEADERS).json() + response_dict = requests.get(arequest_url, headers=HEADERS).json() + filter_key = DEVICE_ID_KEYS.get(device_type) + return get_devices_from_response_dict(response_dict, + filter_key=filter_key) + - items = j.get('data') +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: - value_at_key = item.get(filter_key) - if value_at_key is not None and item.get("hidden_at") is None: - if filter_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)) + 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('light_bulb_id') + return get_devices(device_types.LIGHT_BULB) def get_switches(): - return get_devices('binary_switch_id') + return get_devices(device_types.BINARY_SWITCH) def get_sensors(): - return get_devices('sensor_pod_id') + return get_devices(device_types.SENSOR_POD) def get_locks(): - return get_devices('lock_id') + return get_devices(device_types.LOCK) def get_eggtrays(): - return get_devices('eggtray_id') + return get_devices(device_types.EGG_TRAY) def get_garage_doors(): - return get_devices('garage_door_id') + return get_devices(device_types.GARAGE_DOOR) def get_powerstrip_outlets(): - return get_devices('powerstrip_id') + return get_devices(device_types.POWER_STRIP) def is_token_set(): diff --git a/pywink/device_types.py b/pywink/device_types.py new file mode 100644 index 0000000..3815518 --- /dev/null +++ b/pywink/device_types.py @@ -0,0 +1,7 @@ +LIGHT_BULB = 'light_bulb' +BINARY_SWITCH = 'binary_switch' +SENSOR_POD = 'sensor_pod' +LOCK = 'lock' +EGG_TRAY = 'eggtray' +GARAGE_DOOR = 'garage_door' +POWER_STRIP = 'powerstrip' diff --git a/setup.py b/setup.py index 04beb39..5f33f13 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup setup(name='python-wink', - version='0.3.3', + version='0.4.0', description='Access Wink devices via the Wink API', url='http://github.com/bradsk88/python-wink', author='John McLaughlin', diff --git a/tests/init_test.py b/tests/init_test.py index ccaf557..ff98605 100644 --- a/tests/init_test.py +++ b/tests/init_test.py @@ -2,7 +2,9 @@ import mock import unittest -from pywink import WinkBulb +from pywink import WinkBulb, get_devices_from_response_dict, WinkGarageDoor, WinkPowerStripOutlet, WinkLock, \ + WinkBinarySwitch, WinkSensorPod, WinkEggTray + class LightSetStateTests(unittest.TestCase): @@ -35,3 +37,606 @@ def test_should_only_send_color_xy_if_both_color_xy_and_color_temperature_are_gi self.assertEquals('color_temperature', sent_data['desired_state'].get('color_model')) self.assertNotIn('color_x', sent_data['desired_state']) self.assertNotIn('color_y', sent_data['desired_state']) + + +class WinkAPIResponseHandlingTests(unittest.TestCase): + + def test_should_handle_light_bulb_response(self): + response = """ + { + "data": [{ + "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 + }] + } + """ + response_dict = json.loads(response) + devices = get_devices_from_response_dict(response_dict) + self.assertEqual(1, len(devices)) + self.assertIsInstance(devices[0], WinkBulb) + + def test_should_handle_garage_door_opener_response(self): + + response = """ + { + "data": [{ + "desired_state": { + "position": 0 + }, + "last_reading": { + "position_opened": "N\/A", + "position_opened_updated_at": 1450357467.371, + "tamper_detected_true": null, + "tamper_detected_true_updated_at": null, + "connection": true, + "connection_updated_at": 1450357538.2715, + "position": 0, + "position_updated_at": 1450357537.836, + "battery": null, + "battery_updated_at": null, + "fault": false, + "fault_updated_at": 1447976866.0784, + "disabled": null, + "disabled_updated_at": null, + "control_enabled": true, + "control_enabled_updated_at": 1447976866.0784, + "desired_position_updated_at": 1447976846.8869, + "connection_changed_at": 1444775470.5484, + "position_changed_at": 1450357537.836, + "control_enabled_changed_at": 1444775472.2474, + "fault_changed_at": 1444775472.2474, + "position_opened_changed_at": 1450357467.371, + "desired_position_changed_at": 1447976846.8869 + }, + "garage_door_id": "30528", + "name": "Garage Door", + "locale": "en_us", + "units": { + + }, + "created_at": 1444775470, + "hidden_at": null, + "capabilities": { + "home_security_device": true + }, + "triggers": [ + + ], + "manufacturer_device_model": "chamberlain_garage_door_opener", + "manufacturer_device_id": "1133930", + "device_manufacturer": "chamberlain", + "model_name": "MyQ Garage Door Controller", + "upc_id": "26", + "upc_code": "012381109302", + "hub_id": null, + "local_id": null, + "radio_type": null, + "linked_service_id": "206203", + "lat_lng": [ + 0, + 0 + ], + "location": "", + "order": null + }], + "errors": [], + "pagination": {} + } + """ + response_dict = json.loads(response) + devices = get_devices_from_response_dict(response_dict) + self.assertEqual(1, len(devices)) + self.assertIsInstance(devices[0], WinkGarageDoor) + + def test_should_handle_power_strip_response(self): + + response = """ + { + "errors": [ + + ], + "data": [{ + "powerstrip_id": "12345", + "model_name": "Pivot Power Genius", + "created_at": 1451578768, + "mac_address": "0c2a69000000", + "locale": "en_us", + "name": "Power strip", + "units": { + + }, + "last_reading": { + "connection": true, + "connection_changed_at": 1451947138.418391, + "connection_updated_at": 1452093346.488989 + }, + "triggers": [ + + ], + "location": "", + "capabilities": { + + }, + "hidden_at": null, + "outlets": [{ + "parent_object_type": "powerstrip", + "icon_id": "4", + "desired_state": { + "powered": false + }, + "parent_object_id": "24313", + "scheduled_outlet_states": [ + + ], + "name": "Outlet #1", + "outlet_index": 0, + "last_reading": { + "desired_powered_updated_at": 1452094688.1679382, + "powered_updated_at": 1452094688.1461067, + "powered": false, + "powered_changed_at": 1452094688.1461067 + }, + "powered": false, + "outlet_id": "48628" + }, { + "parent_object_type": "powerstrip", + "icon_id": "4", + "desired_state": { + "powered": false + }, + "parent_object_id": "24313", + "scheduled_outlet_states": [ + + ], + "name": "Outlet #2", + "outlet_index": 1, + "last_reading": { + "desired_powered_updated_at": 1452094689.7589157, + "powered_updated_at": 1452094689.443459, + "powered": false, + "powered_changed_at": 1452094689.443459 + }, + "powered": false, + "outlet_id": "48629" + }], + "serial": "AAAA00012345", + "lat_lng": [ + 0.000000, -0.000000 + ], + "desired_state": { + + }, + "device_manufacturer": "quirky_ge", + "upc_id": "24", + "upc_code": "814434017226" + }], + "pagination": { + + } + } + """ + response_dict = json.loads(response) + devices = get_devices_from_response_dict(response_dict) + self.assertEqual(2, len(devices)) + self.assertIsInstance(devices[0], WinkPowerStripOutlet) + self.assertIsInstance(devices[1], WinkPowerStripOutlet) + + def test_should_handle_lock_response(self): + + response = """ + { + "data": [ + { + "desired_state": { + "locked": true, + "beeper_enabled": true, + "vacation_mode_enabled": false, + "auto_lock_enabled": false, + "key_code_length": 4, + "alarm_mode": null, + "alarm_sensitivity": 0.6, + "alarm_enabled": false + }, + "last_reading": { + "locked": true, + "locked_updated_at": 1417823487.490747, + "connection": true, + "connection_updated_at": 1417823487.490747, + "battery": 0.83, + "battery_updated_at": 1417823487.490747, + "alarm_activated": null, + "alarm_activated_updated_at": null, + "beeper_enabled": true, + "beeper_enabled_updated_at": 1417823487.490747, + "vacation_mode_enabled": false, + "vacation_mode_enabled_updated_at": 1417823487.490747, + "auto_lock_enabled": false, + "auto_lock_enabled_updated_at": 1417823487.490747, + "key_code_length": 4, + "key_code_length_updated_at": 1417823487.490747, + "alarm_mode": null, + "alarm_mode_updated_at": 1417823487.490747, + "alarm_sensitivity": 0.6, + "alarm_sensitivity_updated_at": 1417823487.490747, + "alarm_enabled": true, + "alarm_enabled_updated_at": 1417823487.490747, + "last_error": null, + "last_error_updated_at": 1417823487.490747, + "desired_locked_updated_at": 1417823487.490747, + "desired_beeper_enabled_updated_at": 1417823487.490747, + "desired_vacation_mode_enabled_updated_at": 1417823487.490747, + "desired_auto_lock_enabled_updated_at": 1417823487.490747, + "desired_key_code_length_updated_at": 1417823487.490747, + "desired_alarm_mode_updated_at": 1417823487.490747, + "desired_alarm_sensitivity_updated_at": 1417823487.490747, + "desired_alarm_enabled_updated_at": 1417823487.490747, + "locked_changed_at": 1417823487.490747, + "battery_changed_at": 1417823487.490747, + "desired_locked_changed_at": 1417823487.490747, + "desired_beeper_enabled_changed_at": 1417823487.490747, + "desired_vacation_mode_enabled_changed_at": 1417823487.490747, + "desired_auto_lock_enabled_changed_at": 1417823487.490747, + "desired_key_code_length_changed_at": 1417823487.490747, + "desired_alarm_mode_changed_at": 1417823487.490747, + "desired_alarm_sensitivity_changed_at": 1417823487.490747, + "desired_alarm_enabled_changed_at": 1417823487.490747, + "last_error_changed_at": 1417823487.490747 + }, + "lock_id": "5304", + "name": "Main", + "locale": "en_us", + "units": {}, + "created_at": 1417823382, + "hidden_at": null, + "capabilities": { + "fields": [ + { + "field": "locked", + "type": "boolean", + "mutability": "read-write" + }, + { + "field": "connection", + "mutability": "read-only", + "type": "boolean" + }, + { + "field": "battery", + "mutability": "read-only", + "type": "percentage" + }, + { + "field": "alarm_activated", + "mutability": "read-only", + "type": "boolean" + }, + { + "field": "beeper_enabled", + "type": "boolean" + }, + { + "field": "vacation_mode_enabled", + "type": "boolean" + }, + { + "field": "auto_lock_enabled", + "type": "boolean" + }, + { + "field": "key_code_length", + "type": "integer" + }, + { + "field": "alarm_mode", + "type": "string" + }, + { + "field": "alarm_sensitivity", + "type": "percentage" + }, + { + "field": "alarm_enabled", + "type": "boolean" + } + ], + "home_security_device": true + }, + "triggers": [], + "manufacturer_device_model": "schlage_zwave_lock", + "manufacturer_device_id": null, + "device_manufacturer": "schlage", + "model_name": "BE469", + "upc_id": "11", + "upc_code": "043156312214", + "hub_id": "11780", + "local_id": "1", + "radio_type": "zwave", + "lat_lng": [38.429962, -122.653715], + "location": "" + } + ], + "errors": [], + "pagination": { + "count": 1 + } + } + """ + + response_dict = json.loads(response) + devices = get_devices_from_response_dict(response_dict) + self.assertEqual(1, len(devices)) + self.assertIsInstance(devices[0], WinkLock) + + def test_should_handle_binary_switch_response(self): + + response = """ + { + "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": {} + } + """ + + response_dict = json.loads(response) + devices = get_devices_from_response_dict(response_dict) + self.assertEqual(1, len(devices)) + self.assertIsInstance(devices[0], WinkBinarySwitch) + + def test_should_handle_sensor_pod_response(self): + + response = """ + { + "data": [{ + "last_event": { + "brightness_occurred_at": null, + "loudness_occurred_at": null, + "vibration_occurred_at": null + }, + "model_name": "Tripper", + "capabilities": { + "sensor_types": [ + { + "field": "opened", + "type": "boolean" + }, + { + "field": "battery", + "type": "percentage" + } + ] + }, + "manufacturer_device_model": "quirky_ge_tripper", + "location": "", + "radio_type": "zigbee", + "manufacturer_device_id": null, + "gang_id": null, + "sensor_pod_id": "37614", + "subscription": { + }, + "units": { + }, + "upc_id": "184", + "hidden_at": null, + "last_reading": { + "battery_voltage_threshold_2": 0, + "opened": false, + "battery_alarm_mask": 0, + "opened_updated_at": 1421697092.7347496, + "battery_voltage_min_threshold_updated_at": 1421697092.7347229, + "battery_voltage_min_threshold": 0, + "connection": null, + "battery_voltage": 25, + "battery_voltage_threshold_1": 25, + "connection_updated_at": null, + "battery_voltage_threshold_3": 0, + "battery_voltage_updated_at": 1421697092.7347066, + "battery_voltage_threshold_1_updated_at": 1421697092.7347302, + "battery_voltage_threshold_3_updated_at": 1421697092.7347434, + "battery_voltage_threshold_2_updated_at": 1421697092.7347374, + "battery": 1.0, + "battery_updated_at": 1421697092.7347553, + "battery_alarm_mask_updated_at": 1421697092.734716 + }, + "triggers": [ + ], + "name": "MasterBathroom", + "lat_lng": [ + 37.550773, + -122.279182 + ], + "uuid": "a2cb868a-dda3-4211-ab73-fc08087aeed7", + "locale": "en_us", + "device_manufacturer": "quirky_ge", + "created_at": 1421523277, + "local_id": "2", + "hub_id": "88264" + }] + } + """ + + response_dict = json.loads(response) + devices = get_devices_from_response_dict(response_dict) + self.assertEqual(1, len(devices)) + self.assertIsInstance(devices[0], WinkSensorPod) + + def test_should_handle_egg_tray_response(self): + + response = """ + { + "data": [{ + "last_reading": { + "connection": true, + "connection_updated_at": 1417823487.490747, + "battery": 0.83, + "battery_updated_at": 1417823487.490747, + "inventory": 3, + "inventory_updated_at": 1449705551.7313306, + "freshness_remaining": 2419191, + "freshness_remaining_updated_at": 1449705551.7313495, + "age_updated_at": 1449705551.7313418, + "age": 1449705542, + "connection_changed_at": 1449705443.6858568, + "next_trigger_at_updated_at": null, + "next_trigger_at": null, + "egg_1_timestamp_updated_at": 1449753143.8631344, + "egg_1_timestamp_changed_at": 1449705534.0782206, + "egg_1_timestamp": 1449705545.0, + "egg_2_timestamp_updated_at": 1449753143.8631344, + "egg_2_timestamp_changed_at": 1449705534.0782206, + "egg_2_timestamp": 1449705545.0, + "egg_3_timestamp_updated_at": 1449753143.8631344, + "egg_3_timestamp_changed_at": 1449705534.0782206, + "egg_3_timestamp": 1449705545.0, + "egg_4_timestamp_updated_at": 1449753143.8631344, + "egg_4_timestamp_changed_at": 1449705534.0782206, + "egg_4_timestamp": 1449705545.0, + "egg_5_timestamp_updated_at": 1449753143.8631344, + "egg_5_timestamp_changed_at": 1449705534.0782206, + "egg_5_timestamp": 1449705545.0, + "egg_6_timestamp_updated_at": 1449753143.8631344, + "egg_6_timestamp_changed_at": 1449705534.0782206, + "egg_6_timestamp": 1449705545.0, + "egg_7_timestamp_updated_at": 1449753143.8631344, + "egg_7_timestamp_changed_at": 1449705534.0782206, + "egg_7_timestamp": 1449705545.0, + "egg_8_timestamp_updated_at": 1449753143.8631344, + "egg_8_timestamp_changed_at": 1449705534.0782206, + "egg_8_timestamp": 1449705545.0, + "egg_9_timestamp_updated_at": 1449753143.8631344, + "egg_9_timestamp_changed_at": 1449705534.0782206, + "egg_9_timestamp": 1449705545.0, + "egg_10_timestamp_updated_at": 1449753143.8631344, + "egg_10_timestamp_changed_at": 1449705534.0782206, + "egg_10_timestamp": 1449705545.0, + "egg_11_timestamp_updated_at": 1449753143.8631344, + "egg_11_timestamp_changed_at": 1449705534.0782206, + "egg_11_timestamp": 1449705545.0, + "egg_12_timestamp_updated_at": 1449753143.8631344, + "egg_12_timestamp_changed_at": 1449705534.0782206, + "egg_12_timestamp": 1449705545.0, + "egg_13_timestamp_updated_at": 1449753143.8631344, + "egg_13_timestamp_changed_at": 1449705534.0782206, + "egg_13_timestamp": 1449705545.0, + "egg_14_timestamp_updated_at": 1449753143.8631344, + "egg_14_timestamp_changed_at": 1449705534.0782206, + "egg_14_timestamp": 1449705545.0 + }, + "eggtray_id": "153869", + "name": "Egg Minder", + "freshness_period": 2419200, + "locale": "en_us", + "units": {}, + "created_at": 1417823382, + "hidden_at": null, + "capabilities": {}, + "triggers": [], + "device_manufacturer": "quirky_ge", + "model_name": "Egg Minder", + "upc_id": "23", + "upc_code": "814434017233", + "lat_lng": [38.429962, -122.653715], + "location": "" + }], + "errors": [], + "pagination": { + "count": 1 + } + } + """ + + response_dict = json.loads(response) + devices = get_devices_from_response_dict(response_dict) + self.assertEqual(1, len(devices)) + self.assertIsInstance(devices[0], WinkEggTray) From e3b15f6bb918a1823c0bd4026ca0970557144311 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Fri, 8 Jan 2016 21:37:04 -0500 Subject: [PATCH 029/178] Treat offline binary switches as powered=false --- CHANGELOG.md | 3 +++ pywink/__init__.py | 14 +++++--------- setup.py | 2 +- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c151d4f..45662fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 0.4.1 +- Treating offline binary switches as if they have a powered state of false + ## 0.4.0 - Removed API responses from docstring and moved into unit tests. - Refactored __init__.py to support easier unit testing diff --git a/pywink/__init__.py b/pywink/__init__.py index 5df5aaf..03454b7 100644 --- a/pywink/__init__.py +++ b/pywink/__init__.py @@ -166,6 +166,8 @@ def __repr__(self): 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(): @@ -374,7 +376,7 @@ def _recent_state_set(self): return time.time() - self._last_call[0] < 15 -class WinkPowerStripOutlet(WinkDevice): +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 @@ -404,9 +406,11 @@ def _update_state_from_response(self, response_json): :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): @@ -416,14 +420,6 @@ def update_state(self): arequest = requests.get(url_string, headers=HEADERS) self._update_state_from_response(arequest.json()) - 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('powered', False) - def index(self): return self.json_state.get('outlet_index', None) diff --git a/setup.py b/setup.py index 5f33f13..5d32883 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup setup(name='python-wink', - version='0.4.0', + version='0.4.1', description='Access Wink devices via the Wink API', url='http://github.com/bradsk88/python-wink', author='John McLaughlin', From c0e8ba3d9fb4ab14ca81263b388d4b40cbbb4289 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Sun, 10 Jan 2016 14:20:57 -0500 Subject: [PATCH 030/178] Added unit test for treating offline devices as powered=false --- tests/init_test.py | 82 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/tests/init_test.py b/tests/init_test.py index ff98605..5b15229 100644 --- a/tests/init_test.py +++ b/tests/init_test.py @@ -39,6 +39,87 @@ def test_should_only_send_color_xy_if_both_color_xy_and_color_temperature_are_gi self.assertNotIn('color_y', sent_data['desired_state']) +class PowerStripStateTests(unittest.TestCase): + + def test_should_show_powered_state_as_false_if_device_is_disconnected(self): + respone = """ + { + "data": { + "desired_state": {}, + "last_reading": { + "connection": False, + "connection_updated_at": 1452306146.129263, + "connection_changed_at": 1452306144.425378 + }, + "powerstrip_id": "24123", + "name": "Power strip", + "locale": "en_us", + "units": {}, + "created_at": 1451578768, + "hidden_at": null, + "capabilities": {}, + "triggers": [], + "device_manufacturer": "quirky_ge", + "model_name": "Pivot Power Genius", + "upc_id": "24", + "upc_code": "814434017226", + "lat_lng": [ + 12.345678, + -98.765432 + ], + "location": "", + "mac_address": "0c2a69123456", + "serial": "AAAA00012345", + "outlets": [ + { + "powered": false, + "scheduled_outlet_states": [], + "name": "First", + "outlet_index": 0, + "outlet_id": "48123", + "icon_id": "4", + "parent_object_type": "powerstrip", + "parent_object_id": "24123", + "desired_state": { + "powered": false + }, + "last_reading": { + "powered": false, + "powered_updated_at": 1452306146.0882413, + "powered_changed_at": 1452306004.7519948, + "desired_powered_updated_at": 1452306008.2215497 + } + }, + { + "powered": false, + "scheduled_outlet_states": [], + "name": "Second", + "outlet_index": 1, + "outlet_id": "48124", + "icon_id": "4", + "parent_object_type": "powerstrip", + "parent_object_id": "24123", + "desired_state": { + "powered": true + }, + "last_reading": { + "powered": true, + "powered_updated_at": 1452311731.8861659, + "powered_changed_at": 1452311731.8861659, + "desired_powered_updated_at": 1452311885.3523679 + } + } + ] + }, + "errors": [], + "pagination": {} + } + """ + + devices = get_devices_from_respone_dict(json.loads(response)) + self.assertFalse(devices[0].state()) + + class WinkAPIResponseHandlingTests(unittest.TestCase): def test_should_handle_light_bulb_response(self): @@ -640,3 +721,4 @@ def test_should_handle_egg_tray_response(self): devices = get_devices_from_response_dict(response_dict) self.assertEqual(1, len(devices)) self.assertIsInstance(devices[0], WinkEggTray) + From eb2c35a8b1223f92312f594ffc3bd393875b7db0 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Sun, 10 Jan 2016 14:25:41 -0500 Subject: [PATCH 031/178] Fixed typos --- tests/init_test.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/init_test.py b/tests/init_test.py index 5b15229..01e8fd8 100644 --- a/tests/init_test.py +++ b/tests/init_test.py @@ -42,7 +42,7 @@ def test_should_only_send_color_xy_if_both_color_xy_and_color_temperature_are_gi class PowerStripStateTests(unittest.TestCase): def test_should_show_powered_state_as_false_if_device_is_disconnected(self): - respone = """ + response = """ { "data": { "desired_state": {}, @@ -115,8 +115,9 @@ def test_should_show_powered_state_as_false_if_device_is_disconnected(self): "pagination": {} } """ - - devices = get_devices_from_respone_dict(json.loads(response)) + + response_dict = json.loads(response) + devices = get_devices_from_response_dict(json.loads(response_dict)) self.assertFalse(devices[0].state()) @@ -721,4 +722,3 @@ def test_should_handle_egg_tray_response(self): devices = get_devices_from_response_dict(response_dict) self.assertEqual(1, len(devices)) self.assertIsInstance(devices[0], WinkEggTray) - From a4ff8453a045d10adfdbc3f03f20543d5d5a58c0 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Sun, 10 Jan 2016 14:33:24 -0500 Subject: [PATCH 032/178] Fixed json --- tests/init_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/init_test.py b/tests/init_test.py index 01e8fd8..f4cd911 100644 --- a/tests/init_test.py +++ b/tests/init_test.py @@ -47,7 +47,7 @@ def test_should_show_powered_state_as_false_if_device_is_disconnected(self): "data": { "desired_state": {}, "last_reading": { - "connection": False, + "connection": false, "connection_updated_at": 1452306146.129263, "connection_changed_at": 1452306144.425378 }, @@ -117,7 +117,7 @@ def test_should_show_powered_state_as_false_if_device_is_disconnected(self): """ response_dict = json.loads(response) - devices = get_devices_from_response_dict(json.loads(response_dict)) + devices = get_devices_from_response_dict(response_dict) self.assertFalse(devices[0].state()) From ae5ff20f36184909884c68520d01c01f168a6377 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Mon, 11 Jan 2016 13:27:37 -0500 Subject: [PATCH 033/178] Fixed JSON --- tests/init_test.py | 134 +++++++++++++++++++++++---------------------- 1 file changed, 69 insertions(+), 65 deletions(-) diff --git a/tests/init_test.py b/tests/init_test.py index f4cd911..894dc4f 100644 --- a/tests/init_test.py +++ b/tests/init_test.py @@ -44,75 +44,79 @@ class PowerStripStateTests(unittest.TestCase): def test_should_show_powered_state_as_false_if_device_is_disconnected(self): response = """ { - "data": { - "desired_state": {}, - "last_reading": { - "connection": false, - "connection_updated_at": 1452306146.129263, - "connection_changed_at": 1452306144.425378 - }, - "powerstrip_id": "24123", - "name": "Power strip", - "locale": "en_us", - "units": {}, - "created_at": 1451578768, - "hidden_at": null, - "capabilities": {}, - "triggers": [], - "device_manufacturer": "quirky_ge", - "model_name": "Pivot Power Genius", - "upc_id": "24", - "upc_code": "814434017226", - "lat_lng": [ - 12.345678, - -98.765432 - ], - "location": "", - "mac_address": "0c2a69123456", - "serial": "AAAA00012345", - "outlets": [ - { - "powered": false, - "scheduled_outlet_states": [], - "name": "First", - "outlet_index": 0, - "outlet_id": "48123", - "icon_id": "4", - "parent_object_type": "powerstrip", - "parent_object_id": "24123", - "desired_state": { - "powered": false - }, - "last_reading": { - "powered": false, - "powered_updated_at": 1452306146.0882413, - "powered_changed_at": 1452306004.7519948, - "desired_powered_updated_at": 1452306008.2215497 - } + "data": [ + { + "desired_state": {}, + "last_reading": { + "connection": false, + "connection_updated_at": 1452306146.129263, + "connection_changed_at": 1452306144.425378 }, - { - "powered": false, - "scheduled_outlet_states": [], - "name": "Second", - "outlet_index": 1, - "outlet_id": "48124", - "icon_id": "4", - "parent_object_type": "powerstrip", - "parent_object_id": "24123", - "desired_state": { - "powered": true + "powerstrip_id": "24123", + "name": "Power strip", + "locale": "en_us", + "units": {}, + "created_at": 1451578768, + "hidden_at": null, + "capabilities": {}, + "triggers": [], + "device_manufacturer": "quirky_ge", + "model_name": "Pivot Power Genius", + "upc_id": "24", + "upc_code": "814434017226", + "lat_lng": [ + 12.123456, + -98.765432 + ], + "location": "", + "mac_address": "0c2a69123456", + "serial": "AAAA00123456", + "outlets": [ + { + "powered": false, + "scheduled_outlet_states": [], + "name": "First", + "outlet_index": 0, + "outlet_id": "48123", + "icon_id": "4", + "parent_object_type": "powerstrip", + "parent_object_id": "24123", + "desired_state": { + "powered": false + }, + "last_reading": { + "powered": true, + "powered_updated_at": 1452306146.0882413, + "powered_changed_at": 1452306004.7519948, + "desired_powered_updated_at": 1452306008.2215497 + } }, - "last_reading": { - "powered": true, - "powered_updated_at": 1452311731.8861659, - "powered_changed_at": 1452311731.8861659, - "desired_powered_updated_at": 1452311885.3523679 + { + "powered": false, + "scheduled_outlet_states": [], + "name": "Second", + "outlet_index": 1, + "outlet_id": "48124", + "icon_id": "4", + "parent_object_type": "powerstrip", + "parent_object_id": "24123", + "desired_state": { + "powered": false + }, + "last_reading": { + "powered": true, + "powered_updated_at": 1452311731.8861659, + "powered_changed_at": 1452311731.8861659, + "desired_powered_updated_at": 1452311885.3523679 + } } - } - ] - }, + ] + } + ], "errors": [], - "pagination": {} + "pagination": { + "count": 10 + } } """ From 75041a0132fd015e958092841edb495c59cb4416 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Wed, 20 Jan 2016 19:15:39 -0500 Subject: [PATCH 034/178] Added support for Wink sirens --- pywink/__init__.py | 29 +++++++++++++++- pywink/device_types.py | 1 + tests/init_test.py | 75 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 104 insertions(+), 1 deletion(-) diff --git a/pywink/__init__.py b/pywink/__init__.py index 03454b7..2c38877 100644 --- a/pywink/__init__.py +++ b/pywink/__init__.py @@ -19,7 +19,8 @@ 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.SENSOR_POD: 'sensor_pod_id', + device_types.SIREN: 'siren_id' } @@ -46,6 +47,8 @@ def factory(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) @@ -540,6 +543,26 @@ 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_dict = requests.get(arequest_url, headers=HEADERS).json() @@ -602,6 +625,10 @@ 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) diff --git a/pywink/device_types.py b/pywink/device_types.py index 3815518..67c65c3 100644 --- a/pywink/device_types.py +++ b/pywink/device_types.py @@ -5,3 +5,4 @@ EGG_TRAY = 'eggtray' GARAGE_DOOR = 'garage_door' POWER_STRIP = 'powerstrip' +SIREN = 'siren' diff --git a/tests/init_test.py b/tests/init_test.py index 894dc4f..ee44c99 100644 --- a/tests/init_test.py +++ b/tests/init_test.py @@ -342,6 +342,81 @@ def test_should_handle_power_strip_response(self): self.assertIsInstance(devices[0], WinkPowerStripOutlet) self.assertIsInstance(devices[1], WinkPowerStripOutlet) + + def test_should_handle_siren_response(self): + + response = """ + { + "data":[ + { + "desired_state":{ + "auto_shutoff":30, + "mode":"siren_and_strobe", + "powered":false + }, + "last_reading":{ + "connection":true, + "connection_updated_at":1453249957.2466462, + "battery":1, + "battery_updated_at":1453249957.2466462, + "auto_shutoff":30, + "auto_shutoff_updated_at":1453249957.2466462, + "mode":"siren_and_strobe", + "mode_updated_at":1453249957.2466462, + "powered":false, + "powered_updated_at":1453249957.2466462, + "desired_auto_shutoff_updated_at":1452812848.5178623, + "desired_mode_updated_at":1452812848.5178623, + "desired_powered_updated_at":1452812668.1190264, + "connection_changed_at":1452812587.0312104, + "powered_changed_at":1452812668.0807295, + "battery_changed_at":1453032821.1796713, + "mode_changed_at":1452812589.8262901, + "auto_shutoff_changed_at":1452812589.8262901, + "desired_auto_shutoff_changed_at":1452812590.029748, + "desired_powered_changed_at":1452812668.1190264, + "desired_mode_changed_at":1452812848.5178623 + }, + "siren_id":"6123", + "name":"Alarm", + "locale":"en_us", + "units":{ + + }, + "created_at":1452812587, + "hidden_at":null, + "capabilities":{ + + }, + "device_manufacturer":"linear", + "model_name":"Wireless Siren & Strobe (Wireless)", + "upc_id":"243", + "upc_code":"wireless_linear_siren", + "hub_id":"30123", + "local_id":"8", + "radio_type":"zwave", + "lat_lng":[ + 12.1345678 + -98.765432 + ], + "location":"" + } + ], + "errors":[ + + ], + "pagination":{ + "count":17 + } + } + """ + + response_dict = json.loads(response) + devices = get_devices_from_response_dict(response_dict) + self.assertEqual(1, len(devices)) + self.assertIsInstance(devices[0], WinkSiren) + + def test_should_handle_lock_response(self): response = """ From 51520f996901ca2fbe1bc4ade1eb565268585dc6 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Wed, 20 Jan 2016 19:18:58 -0500 Subject: [PATCH 035/178] Fixed test json --- tests/init_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/init_test.py b/tests/init_test.py index ee44c99..f1f3544 100644 --- a/tests/init_test.py +++ b/tests/init_test.py @@ -396,7 +396,7 @@ def test_should_handle_siren_response(self): "local_id":"8", "radio_type":"zwave", "lat_lng":[ - 12.1345678 + 12.1345678, -98.765432 ], "location":"" From 44a1cceb8734dd48a887c0a13af1ef2944066b6a Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Wed, 20 Jan 2016 19:21:42 -0500 Subject: [PATCH 036/178] Fixed siren test and changed version --- CHANGELOG.md | 3 +++ setup.py | 2 +- tests/init_test.py | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45662fa..db30efc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 0.4.2 +- Added init method for WinkSiren + ## 0.4.1 - Treating offline binary switches as if they have a powered state of false diff --git a/setup.py b/setup.py index 5d32883..6b3f454 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup setup(name='python-wink', - version='0.4.1', + version='0.4.2', description='Access Wink devices via the Wink API', url='http://github.com/bradsk88/python-wink', author='John McLaughlin', diff --git a/tests/init_test.py b/tests/init_test.py index f1f3544..05e4ccf 100644 --- a/tests/init_test.py +++ b/tests/init_test.py @@ -3,7 +3,7 @@ import unittest from pywink import WinkBulb, get_devices_from_response_dict, WinkGarageDoor, WinkPowerStripOutlet, WinkLock, \ - WinkBinarySwitch, WinkSensorPod, WinkEggTray + WinkBinarySwitch, WinkSensorPod, WinkEggTray, WinkSiren class LightSetStateTests(unittest.TestCase): From e3aae194137b712b7bafa6ddd2e53d6c5df9b013 Mon Sep 17 00:00:00 2001 From: bradsk88 <7kcwKg60tdcW> Date: Fri, 5 Feb 2016 00:50:19 -0600 Subject: [PATCH 037/178] Moved API responses out of comments and into unit tests. Some of them were not valid JSON so I'm a bit suspicious about whether or not they work but I don't have some of these devices to get real responses. Will investigate later. --- CHANGELOG.md | 3 +++ pywink/__init__.py | 19 +++++++++++++++---- setup.py | 2 +- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db30efc..10e3671 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 0.4.3 +- Added better error handling for API authorization problems. + ## 0.4.2 - Added init method for WinkSiren diff --git a/pywink/__init__.py b/pywink/__init__.py index 2c38877..28d575c 100644 --- a/pywink/__init__.py +++ b/pywink/__init__.py @@ -565,10 +565,17 @@ def device_id(self): def get_devices(device_type): arequest_url = "{}/users/me/wink_devices".format(BASE_URL) - response_dict = requests.get(arequest_url, headers=HEADERS).json() - filter_key = DEVICE_ID_KEYS.get(device_type) - return get_devices_from_response_dict(response_dict, - filter_key=filter_key) + 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): @@ -641,3 +648,7 @@ def set_bearer_token(token): "Content-Type": "application/json", "Authorization": "Bearer {}".format(token) } + + +class WinkAPIException(Exception): + pass diff --git a/setup.py b/setup.py index 6b3f454..35148ee 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup setup(name='python-wink', - version='0.4.2', + version='0.4.3', description='Access Wink devices via the Wink API', url='http://github.com/bradsk88/python-wink', author='John McLaughlin', From b8f6b4103eb629d9c97fdbb62a5e38677710afb3 Mon Sep 17 00:00:00 2001 From: bradsk88 <7kcwKg60tdcW> Date: Sat, 6 Feb 2016 13:09:37 -0600 Subject: [PATCH 038/178] Major bug fix. get_bulbs and similar methods were returning empty lists. --- CHANGELOG.md | 3 +++ pywink/__init__.py | 4 ++-- setup.py | 2 +- tests/init_test.py | 22 +++++++++++----------- 4 files changed, 17 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10e3671..921d98f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 0.5.0 +- Major bug fix. Methods like `get_bulbs` were always returning empty lists. + ## 0.4.3 - Added better error handling for API authorization problems. diff --git a/pywink/__init__.py b/pywink/__init__.py index 28d575c..d0f67a2 100644 --- a/pywink/__init__.py +++ b/pywink/__init__.py @@ -578,14 +578,14 @@ def get_devices(device_type): raise WinkAPIException("Unexpected") -def get_devices_from_response_dict(response_dict, filter_key=None): +def get_devices_from_response_dict(response_dict, filter_key): items = response_dict.get('data') devices = [] keys = DEVICE_ID_KEYS.values() if filter_key: - keys = [filter_key] + keys = [DEVICE_ID_KEYS.get(filter_key)] for item in items: for key in keys: diff --git a/setup.py b/setup.py index 35148ee..1ef0416 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ 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', diff --git a/tests/init_test.py b/tests/init_test.py index 05e4ccf..3a83b39 100644 --- a/tests/init_test.py +++ b/tests/init_test.py @@ -2,8 +2,8 @@ import mock import unittest -from pywink import WinkBulb, get_devices_from_response_dict, WinkGarageDoor, WinkPowerStripOutlet, WinkLock, \ - WinkBinarySwitch, WinkSensorPod, WinkEggTray, WinkSiren +from pywink import WinkBulb, get_devices_from_response_dict, device_types, WinkGarageDoor, WinkPowerStripOutlet, \ + WinkLock, WinkBinarySwitch, WinkSensorPod, WinkEggTray, WinkSiren class LightSetStateTests(unittest.TestCase): @@ -121,7 +121,7 @@ def test_should_show_powered_state_as_false_if_device_is_disconnected(self): """ response_dict = json.loads(response) - devices = get_devices_from_response_dict(response_dict) + devices = get_devices_from_response_dict(response_dict, device_types.POWER_STRIP) self.assertFalse(devices[0].state()) @@ -172,7 +172,7 @@ def test_should_handle_light_bulb_response(self): } """ response_dict = json.loads(response) - devices = get_devices_from_response_dict(response_dict) + devices = get_devices_from_response_dict(response_dict, device_types.LIGHT_BULB) self.assertEqual(1, len(devices)) self.assertIsInstance(devices[0], WinkBulb) @@ -245,7 +245,7 @@ def test_should_handle_garage_door_opener_response(self): } """ response_dict = json.loads(response) - devices = get_devices_from_response_dict(response_dict) + devices = get_devices_from_response_dict(response_dict, device_types.GARAGE_DOOR) self.assertEqual(1, len(devices)) self.assertIsInstance(devices[0], WinkGarageDoor) @@ -337,7 +337,7 @@ def test_should_handle_power_strip_response(self): } """ response_dict = json.loads(response) - devices = get_devices_from_response_dict(response_dict) + devices = get_devices_from_response_dict(response_dict, device_types.POWER_STRIP) self.assertEqual(2, len(devices)) self.assertIsInstance(devices[0], WinkPowerStripOutlet) self.assertIsInstance(devices[1], WinkPowerStripOutlet) @@ -412,7 +412,7 @@ def test_should_handle_siren_response(self): """ response_dict = json.loads(response) - devices = get_devices_from_response_dict(response_dict) + devices = get_devices_from_response_dict(response_dict, device_types.SIREN) self.assertEqual(1, len(devices)) self.assertIsInstance(devices[0], WinkSiren) @@ -559,7 +559,7 @@ def test_should_handle_lock_response(self): """ response_dict = json.loads(response) - devices = get_devices_from_response_dict(response_dict) + devices = get_devices_from_response_dict(response_dict, device_types.LOCK) self.assertEqual(1, len(devices)) self.assertIsInstance(devices[0], WinkLock) @@ -631,7 +631,7 @@ def test_should_handle_binary_switch_response(self): """ response_dict = json.loads(response) - devices = get_devices_from_response_dict(response_dict) + devices = get_devices_from_response_dict(response_dict, device_types.BINARY_SWITCH) self.assertEqual(1, len(devices)) self.assertIsInstance(devices[0], WinkBinarySwitch) @@ -708,7 +708,7 @@ def test_should_handle_sensor_pod_response(self): """ response_dict = json.loads(response) - devices = get_devices_from_response_dict(response_dict) + devices = get_devices_from_response_dict(response_dict, device_types.SENSOR_POD) self.assertEqual(1, len(devices)) self.assertIsInstance(devices[0], WinkSensorPod) @@ -798,6 +798,6 @@ def test_should_handle_egg_tray_response(self): """ response_dict = json.loads(response) - devices = get_devices_from_response_dict(response_dict) + devices = get_devices_from_response_dict(response_dict, device_types.EGG_TRAY) self.assertEqual(1, len(devices)) self.assertIsInstance(devices[0], WinkEggTray) From e5b1fee0d95a873053ca1d552d2ad5009b9e26a3 Mon Sep 17 00:00:00 2001 From: bradsk88 <7kcwKg60tdcW> Date: Sun, 7 Feb 2016 15:38:59 -0600 Subject: [PATCH 039/178] Major refactoring to support multi-sensor devices without introducing circular dependencies (this restructure was overdue, anyway) GH-13 --- CHANGELOG.md | 4 + pylintrc | 3 + pywink/device_types.py | 8 - script/lint | 4 +- src/pywink/__init__.py | 7 + src/pywink/api.py | 191 ++++++++++++ {tests => src/pywink/devices}/__init__.py | 0 src/pywink/devices/base.py | 44 +++ src/pywink/devices/factory.py | 31 ++ src/pywink/devices/sensors.py | 144 +++++++++ .../pywink/devices/standard.py | 283 ++---------------- src/pywink/devices/types.py | 19 ++ src/pywink/test/__init__.py | 0 .../test/api_responses/quirky_spotter.json | 123 ++++++++ .../test/api_responses/quirky_spotter_2.json | 116 +++++++ {tests => src/pywink/test}/init_test.py | 40 ++- src/pywink/test/sensor_test.py | 76 +++++ setup.py => src/setup.py | 8 +- tox.ini | 1 + 19 files changed, 818 insertions(+), 284 deletions(-) delete mode 100644 pywink/device_types.py create mode 100644 src/pywink/__init__.py create mode 100644 src/pywink/api.py rename {tests => src/pywink/devices}/__init__.py (100%) create mode 100644 src/pywink/devices/base.py create mode 100644 src/pywink/devices/factory.py create mode 100644 src/pywink/devices/sensors.py rename pywink/__init__.py => src/pywink/devices/standard.py (57%) create mode 100644 src/pywink/devices/types.py create mode 100644 src/pywink/test/__init__.py create mode 100644 src/pywink/test/api_responses/quirky_spotter.json create mode 100644 src/pywink/test/api_responses/quirky_spotter_2.json rename {tests => src/pywink/test}/init_test.py (96%) create mode 100644 src/pywink/test/sensor_test.py rename setup.py => src/setup.py (64%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 921d98f..0975c97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Change Log +## 0.6.0 +- Major structural change. Using modules to avoid circular dependencies. +- Added support for devices that contain multiple onboard sensors. + ## 0.5.0 - Major bug fix. Methods like `get_bulbs` were always returning empty lists. diff --git a/pylintrc b/pylintrc index 2bc8970..dc9b501 100644 --- a/pylintrc +++ b/pylintrc @@ -1,9 +1,12 @@ [MASTER] max-line-length=120 +ignore=test # Reasons disabled: # missing-docstring - Document as you like. Good, descriptive method names and variables are preferred over docstrings. # global-statement - used for the on-demand requirement installation +# locally-disabled - Because that's the whole point! disable= missing-docstring , global-statement + , locally-disabled diff --git a/pywink/device_types.py b/pywink/device_types.py deleted file mode 100644 index 67c65c3..0000000 --- a/pywink/device_types.py +++ /dev/null @@ -1,8 +0,0 @@ -LIGHT_BULB = 'light_bulb' -BINARY_SWITCH = 'binary_switch' -SENSOR_POD = 'sensor_pod' -LOCK = 'lock' -EGG_TRAY = 'eggtray' -GARAGE_DOOR = 'garage_door' -POWER_STRIP = 'powerstrip' -SIREN = 'siren' diff --git a/script/lint b/script/lint index b8ae40a..32646ba 100755 --- a/script/lint +++ b/script/lint @@ -3,12 +3,12 @@ cd "$(dirname "$0")/.." echo "Checking style with flake8..." -flake8 pywink +flake8 src/pywink FLAKE8_STATUS=$? echo "Checking style with pylint..." -pylint pywink +pylint src/pywink PYLINT_STATUS=$? if [ $FLAKE8_STATUS -eq 0 ] diff --git a/src/pywink/__init__.py b/src/pywink/__init__.py new file mode 100644 index 0000000..555fe26 --- /dev/null +++ b/src/pywink/__init__.py @@ -0,0 +1,7 @@ +""" +Top level functions +""" +# noqa +from pywink.api import set_bearer_token, get_bulbs, get_eggtrays, get_garage_doors, get_locks, \ + get_powerstrip_outlets, get_sensors, get_sirens, get_switches, get_devices, is_token_set + diff --git a/src/pywink/api.py b/src/pywink/api.py new file mode 100644 index 0000000..517100f --- /dev/null +++ b/src/pywink/api.py @@ -0,0 +1,191 @@ +import json + +import requests + +from pywink.devices import types as device_types +from pywink.devices.factory import build_device +from pywink.devices.sensors import WinkSensorPod, WinkHumiditySensor, WinkBrightnessSensor, WinkSoundPresenceSensor, \ + WinkTemperatureSensor, WinkVibrationPresenceSensor +from pywink.devices.types import DEVICE_ID_KEYS + +API_HEADERS = {} + + +class WinkApiInterface(object): + + BASE_URL = "https://winkapi.quirky.com" + + def set_device_state(self, device, state, id_override=None): + """ + :type device: WinkDevice + :param state: a boolean of true (on) or false ('off') + :return: The JSON response from the API (new device state) + """ + _id = device.device_id() + if id_override: + _id = id_override + url_string = "{}/{}/{}".format(self.BASE_URL, + device.objectprefix, _id) + arequest = requests.put(url_string, + data=json.dumps(state), + headers=API_HEADERS) + return arequest.json() + + def get_device_state(self, device, id_override=None): + """ + :type device: WinkDevice + """ + device_id = id_override or device.device_id() + url_string = "{}/{}/{}".format(self.BASE_URL, + device.objectprefix, device_id) + arequest = requests.get(url_string, headers=API_HEADERS) + return arequest.json() + + +def set_bearer_token(token): + global API_HEADERS + + API_HEADERS = { + "Content-Type": "application/json", + "Authorization": "Bearer {}".format(token) + } + + +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 get_devices(device_type): + arequest_url = "{}/users/me/wink_devices".format(WinkApiInterface.BASE_URL) + response = requests.get(arequest_url, headers=API_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) + + 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): + """ + :rtype: list of WinkDevice + """ + items = response_dict.get('data') + + devices = [] + + keys = DEVICE_ID_KEYS.values() + if filter_key: + keys = [filter_key] + + api_interface = WinkApiInterface() + + for item in items: + for key in keys: + if not __device_is_visible(item, key): + continue + + if key == "powerstrip_id": + devices.extend(__get_outlets_from_powerstrip(item, api_interface)) + continue # Don't capture the powerstrip itself as a device, only the individual outlets + + if key == "sensor_pod_id": + subsensors = _get_subsensors_from_sensor_pod(item, api_interface) + if subsensors: + devices.extend(subsensors) + + devices.append(build_device(item, api_interface)) + + return devices + + +def _get_subsensors_from_sensor_pod(item, api_interface): + + capabilities = [cap['field'] for cap in item.get('capabilities', {}).get('fields', [])] + if not capabilities: + return + + subsensors = [] + + if WinkHumiditySensor.CAPABILITY in capabilities: + subsensors.append(WinkHumiditySensor(item, api_interface)) + + if WinkBrightnessSensor.CAPABILITY in capabilities: + subsensors.append(WinkBrightnessSensor(item, api_interface)) + + if WinkSoundPresenceSensor.CAPABILITY in capabilities: + subsensors.append(WinkSoundPresenceSensor(item, api_interface)) + + if WinkTemperatureSensor.CAPABILITY in capabilities: + subsensors.append(WinkTemperatureSensor(item, api_interface)) + + if WinkVibrationPresenceSensor.CAPABILITY in capabilities: + subsensors.append(WinkVibrationPresenceSensor(item, api_interface)) + + if WinkSensorPod.CAPABILITY in capabilities: + subsensors.append(WinkSensorPod(item, api_interface)) + + return subsensors + + +def __get_outlets_from_powerstrip(item, api_interface): + outlets = item['outlets'] + return [build_device(outlet, api_interface) for outlet in outlets if __device_is_visible(outlet, 'outlet_id')] + + +def __device_is_visible(item, key): + is_correctly_structured = bool(item.get(key)) + is_visible = not item.get('hidden_at') + return is_correctly_structured and is_visible + + +def refresh_state_at_hub(device): + """ + Tell hub to query latest status from device and upload to Wink. + PS: Not sure if this even works.. + :type device: WinkDevice + """ + url_string = "{}/{}/{}/refresh".format(WinkApiInterface.BASE_URL, + device.objectprefix, + device.device_id()) + requests.get(url_string, headers=API_HEADERS) + + +def is_token_set(): + """ Returns if an auth token has been set. """ + return bool(API_HEADERS) + + +class WinkAPIException(Exception): + pass diff --git a/tests/__init__.py b/src/pywink/devices/__init__.py similarity index 100% rename from tests/__init__.py rename to src/pywink/devices/__init__.py diff --git a/src/pywink/devices/base.py b/src/pywink/devices/base.py new file mode 100644 index 0000000..ae681a9 --- /dev/null +++ b/src/pywink/devices/base.py @@ -0,0 +1,44 @@ + +class WinkDevice(object): + + def __init__(self, device_state_as_json, api_interface, objectprefix=None): + """ + :type api_interface pywink.api.WinkApiInterface: + :return: + """ + self.api_interface = api_interface + 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. """ + response = self.api_interface.get_device_state(self) + self._update_state_from_response(response) diff --git a/src/pywink/devices/factory.py b/src/pywink/devices/factory.py new file mode 100644 index 0000000..1d38ccf --- /dev/null +++ b/src/pywink/devices/factory.py @@ -0,0 +1,31 @@ +from pywink.devices.base import WinkDevice +from pywink.devices.sensors import WinkSensorPod +from pywink.devices.standard import WinkBulb, WinkBinarySwitch, WinkPowerStripOutlet, WinkLock, \ + WinkEggTray, WinkGarageDoor, WinkSiren + + +def build_device(device_state_as_json, api_interface): + + 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, api_interface) + elif "sensor_pod_id" in device_state_as_json: + new_object = WinkSensorPod(device_state_as_json, api_interface) + elif "binary_switch_id" in device_state_as_json: + new_object = WinkBinarySwitch(device_state_as_json, api_interface) + elif "outlet_id" in device_state_as_json: + new_object = WinkPowerStripOutlet(device_state_as_json, api_interface) + elif "lock_id" in device_state_as_json: + new_object = WinkLock(device_state_as_json, api_interface) + elif "eggtray_id" in device_state_as_json: + new_object = WinkEggTray(device_state_as_json, api_interface) + elif "garage_door_id" in device_state_as_json: + new_object = WinkGarageDoor(device_state_as_json, api_interface) + elif "siren_id" in device_state_as_json: + new_object = WinkSiren(device_state_as_json, api_interface) + + return new_object or WinkDevice(device_state_as_json, api_interface) diff --git a/src/pywink/devices/sensors.py b/src/pywink/devices/sensors.py new file mode 100644 index 0000000..7ddafaf --- /dev/null +++ b/src/pywink/devices/sensors.py @@ -0,0 +1,144 @@ +# -*- coding: utf-8 -*- +from pywink.devices.base import WinkDevice + + +class _WinkCapabilitySensor(WinkDevice): + + def __init__(self, device_state_as_json, api_interface, capability, unit): + super(_WinkCapabilitySensor, self).__init__(device_state_as_json, api_interface, + objectprefix="sensor_pods") + self._capability = capability + self.unit = unit + + def __repr__(self): + return "".format(name=self.name(), + dev_id=self.device_id(), + reading=self._last_reading.get(self._capability), + unit='' if self.unit is None else self.unit) + + def state(self): + return self._last_reading.get('connection', False) + + def last_reading(self): + return self._last_reading.get(self._capability) + + def capability(self): + return self._capability + + def device_id(self): + root_name = self.json_state.get('sensor_pod_id', self.name()) + return '{}+{}'.format(root_name, self._capability) + + +class WinkSensorPod(_WinkCapabilitySensor): + """ 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: + """ + CAPABILITY = 'opened' + + def __init__(self, device_state_as_json, api_interface): + super(WinkSensorPod, self).__init__(device_state_as_json, api_interface, self.CAPABILITY, None) + + 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 WinkHumiditySensor(_WinkCapabilitySensor): + + CAPABILITY = 'humidity' + UNIT = '%' + + def __init__(self, device_state_as_json, api_interface): + super(WinkHumiditySensor, self).__init__(device_state_as_json, api_interface, + self.CAPABILITY, + self.UNIT) + + def humidity_percentage(self): + """ + :return: The relative humidity detected by the sensor (0% to 100%) + :rtype: int + """ + return self.last_reading() + + +class WinkBrightnessSensor(_WinkCapabilitySensor): + + CAPABILITY = 'brightness' + UNIT = '%' + + def __init__(self, device_state_as_json, api_interface): + super(WinkBrightnessSensor, self).__init__(device_state_as_json, api_interface, + self.CAPABILITY, + self.UNIT) + + def brightness_percentage(self): + """ + :return: The percentage of brightness as determined by the device. + :rtype: int + """ + return self.last_reading() + + +class WinkSoundPresenceSensor(_WinkCapabilitySensor): + + CAPABILITY = 'loudness' + + def __init__(self, device_state_as_json, api_interface): + super(WinkSoundPresenceSensor, self).__init__(device_state_as_json, api_interface, + self.CAPABILITY, + None) + + def loudness_boolean(self): + """ + :return: True if sound is detected. False if sound is below detection threshold (varies by device) + :rtype: bool + """ + return self.last_reading() + + +class WinkTemperatureSensor(_WinkCapabilitySensor): + + CAPABILITY = 'temperature' + UNIT = u'\N{DEGREE SIGN}' + + def __init__(self, device_state_as_json, api_interface): + super(WinkTemperatureSensor, self).__init__(device_state_as_json, api_interface, + self.CAPABILITY, + self.UNIT) + + def temperature_float(self): + """ + :return: A float indicating the temperature. Units are determined by the sensor. + :rtype: float + """ + return self.last_reading() + + +class WinkVibrationPresenceSensor(_WinkCapabilitySensor): + + CAPABILITY = 'vibration' + + def __init__(self, device_state_as_json, api_interface): + super(WinkVibrationPresenceSensor, self).__init__(device_state_as_json, api_interface, + self.CAPABILITY, + None) + + def vibration_boolean(self): + """ + :return: Returns True if vibration is detected. + :rtype: bool + """ + return self.last_reading() diff --git a/pywink/__init__.py b/src/pywink/devices/standard.py similarity index 57% rename from pywink/__init__.py rename to src/pywink/devices/standard.py index d0f67a2..71ce2f1 100644 --- a/pywink/__init__.py +++ b/src/pywink/devices/standard.py @@ -2,104 +2,9 @@ Objects for interfacing with the Wink API """ import logging -import json import time -import requests -from pywink import device_types - -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) +from pywink.devices.base import WinkDevice class WinkEggTray(WinkDevice): @@ -108,8 +13,8 @@ class WinkEggTray(WinkDevice): 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, + def __init__(self, device_state_as_json, api_interface, objectprefix="eggtrays"): + super(WinkEggTray, self).__init__(device_state_as_json, api_interface, objectprefix=objectprefix) def __repr__(self): @@ -126,40 +31,14 @@ 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, + def __init__(self, device_state_as_json, api_interface, objectprefix="binary_switches"): + super(WinkBinarySwitch, self).__init__(device_state_as_json, api_interface, objectprefix=objectprefix) # Tuple (desired state, time) self._last_call = (0, None) @@ -188,12 +67,8 @@ 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()) + response = self.api_interface.set_device_state(self, state) + self._update_state_from_response(response) self._last_call = (time.time(), state) @@ -232,8 +107,8 @@ class WinkBulb(WinkBinarySwitch): """ json_state = {} - def __init__(self, device_state_as_json): - super().__init__(device_state_as_json, + def __init__(self, device_state_as_json, api_interface): + super().__init__(device_state_as_json, api_interface, objectprefix="light_bulbs") def device_id(self): @@ -275,7 +150,6 @@ def set_state(self, state, brightness=None, CIE 1931 x,y color coordinates :return: nothing """ - url_string = "{}/light_bulbs/{}".format(BASE_URL, self.device_id()) values = { "desired_state": { "powered": state @@ -300,10 +174,8 @@ def set_state(self, state, brightness=None, 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()) + response = self.api_interface.set_device_state(self, values) + self._update_state_from_response(response) self._last_call = (time.time(), state) @@ -319,8 +191,8 @@ class WinkLock(WinkDevice): 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, + def __init__(self, device_state_as_json, api_interface, objectprefix="locks"): + super(WinkLock, self).__init__(device_state_as_json, api_interface, objectprefix=objectprefix) # Tuple (desired state, time) self._last_call = (0, None) @@ -345,13 +217,9 @@ 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()) - + response = self.api_interface.set_device_state(self, values) + self._update_state_from_response(response) self._last_call = (time.time(), state) def wait_till_desired_reached(self): @@ -386,8 +254,8 @@ class WinkPowerStripOutlet(WinkBinarySwitch): and looks like so: """ - def __init__(self, device_state_as_json, objectprefix="powerstrips"): - super(WinkPowerStripOutlet, self).__init__(device_state_as_json, + def __init__(self, device_state_as_json, api_interface, objectprefix="powerstrips"): + super(WinkPowerStripOutlet, self).__init__(device_state_as_json, api_interface, objectprefix=objectprefix) # Tuple (desired state, time) self._last_call = (0, None) @@ -418,10 +286,8 @@ def _update_state_from_response(self, response_json): 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()) + response = self.api_interface.get_device_state(self, id_override=self.parent_id()) + self._update_state_from_response(response) def index(self): return self.json_state.get('outlet_index', None) @@ -440,16 +306,13 @@ 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()) + response = self.api_interface.set_device_state(self, values, id_override=self.parent_id()) + self._update_state_from_response(response) self._last_call = (time.time(), state) @@ -485,8 +348,8 @@ class WinkGarageDoor(WinkDevice): and looks like so: """ - def __init__(self, device_state_as_json, objectprefix="garage_doors"): - super(WinkGarageDoor, self).__init__(device_state_as_json, + def __init__(self, device_state_as_json, api_interface, objectprefix="garage_doors"): + super(WinkGarageDoor, self).__init__(device_state_as_json, api_interface, objectprefix=objectprefix) # Tuple (desired state, time) self._last_call = (0, None) @@ -510,10 +373,9 @@ 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()) + response = self.api_interface.set_device_state(self, values) + self._update_state_from_response(response) self._last_call = (time.time(), state) @@ -549,8 +411,8 @@ class WinkSiren(WinkBinarySwitch): 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, + def __init__(self, device_state_as_json, api_interface, objectprefix="sirens"): + super(WinkSiren, self).__init__(device_state_as_json, api_interface, objectprefix=objectprefix) # Tuple (desired state, time) self._last_call = (0, None) @@ -561,94 +423,3 @@ def __repr__(self): 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): - 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(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/src/pywink/devices/types.py b/src/pywink/devices/types.py new file mode 100644 index 0000000..23904b4 --- /dev/null +++ b/src/pywink/devices/types.py @@ -0,0 +1,19 @@ +LIGHT_BULB = 'light_bulb' +BINARY_SWITCH = 'binary_switch' +SENSOR_POD = 'sensor_pod' +LOCK = 'lock' +EGG_TRAY = 'eggtray' +GARAGE_DOOR = 'garage_door' +POWER_STRIP = 'powerstrip' +SIREN = 'siren' + +DEVICE_ID_KEYS = { + BINARY_SWITCH: 'binary_switch_id', + EGG_TRAY: 'eggtray_id', + GARAGE_DOOR: 'garage_door_id', + LIGHT_BULB: 'light_bulb_id', + LOCK: 'lock_id', + POWER_STRIP: 'powerstrip_id', + SENSOR_POD: 'sensor_pod_id', + SIREN: 'siren_id' +} diff --git a/src/pywink/test/__init__.py b/src/pywink/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/pywink/test/api_responses/quirky_spotter.json b/src/pywink/test/api_responses/quirky_spotter.json new file mode 100644 index 0000000..e9d4352 --- /dev/null +++ b/src/pywink/test/api_responses/quirky_spotter.json @@ -0,0 +1,123 @@ +{ + "data": [ + { + "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" + } + ], + "errors": [], + "pagination": { + "count": 24 + } +} diff --git a/src/pywink/test/api_responses/quirky_spotter_2.json b/src/pywink/test/api_responses/quirky_spotter_2.json new file mode 100644 index 0000000..51295f9 --- /dev/null +++ b/src/pywink/test/api_responses/quirky_spotter_2.json @@ -0,0 +1,116 @@ +{ + "data": [ + { + "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" + } + ], + "errors": [], + "pagination": { + "count": 24 + } +} diff --git a/tests/init_test.py b/src/pywink/test/init_test.py similarity index 96% rename from tests/init_test.py rename to src/pywink/test/init_test.py index 3a83b39..496951c 100644 --- a/tests/init_test.py +++ b/src/pywink/test/init_test.py @@ -2,15 +2,23 @@ import mock import unittest -from pywink import WinkBulb, get_devices_from_response_dict, device_types, WinkGarageDoor, WinkPowerStripOutlet, \ - WinkLock, WinkBinarySwitch, WinkSensorPod, WinkEggTray, WinkSiren +from pywink.api import get_devices_from_response_dict, WinkApiInterface +from pywink.devices import types as device_types +from pywink.devices.sensors import WinkSensorPod +from pywink.devices.standard import WinkBulb, WinkGarageDoor, WinkPowerStripOutlet, WinkSiren, WinkLock, \ + WinkBinarySwitch, WinkEggTray +from pywink.devices.types import DEVICE_ID_KEYS class LightSetStateTests(unittest.TestCase): + def setUp(self): + super(LightSetStateTests, self).setUp() + self.api_interface = WinkApiInterface() + @mock.patch('requests.put') def test_should_send_correct_color_xy_values_to_wink_api(self, put_mock): - bulb = WinkBulb({}) + bulb = WinkBulb({}, self.api_interface) color_x = 0.75 color_y = 0.25 bulb.set_state(True, color_xy=[color_x, color_y]) @@ -21,7 +29,7 @@ def test_should_send_correct_color_xy_values_to_wink_api(self, put_mock): @mock.patch('requests.put') def test_should_send_correct_color_temperature_values_to_wink_api(self, put_mock): - bulb = WinkBulb({}) + bulb = WinkBulb({}, self.api_interface) arbitrary_kelvin_color = 4950 bulb.set_state(True, color_kelvin=arbitrary_kelvin_color) sent_data = json.loads(put_mock.call_args[1].get('data')) @@ -30,7 +38,7 @@ def test_should_send_correct_color_temperature_values_to_wink_api(self, put_mock @mock.patch('requests.put') def test_should_only_send_color_xy_if_both_color_xy_and_color_temperature_are_given(self, put_mock): - bulb = WinkBulb({}) + bulb = WinkBulb({}, self.api_interface) arbitrary_kelvin_color = 4950 bulb.set_state(True, color_kelvin=arbitrary_kelvin_color, color_xy=[0, 1]) sent_data = json.loads(put_mock.call_args[1].get('data')) @@ -121,12 +129,16 @@ def test_should_show_powered_state_as_false_if_device_is_disconnected(self): """ response_dict = json.loads(response) - devices = get_devices_from_response_dict(response_dict, device_types.POWER_STRIP) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.POWER_STRIP]) self.assertFalse(devices[0].state()) class WinkAPIResponseHandlingTests(unittest.TestCase): + def setUp(self): + super(WinkAPIResponseHandlingTests, self).setUp() + self.api_interface = mock.MagicMock() + def test_should_handle_light_bulb_response(self): response = """ { @@ -172,7 +184,7 @@ def test_should_handle_light_bulb_response(self): } """ response_dict = json.loads(response) - devices = get_devices_from_response_dict(response_dict, device_types.LIGHT_BULB) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.LIGHT_BULB]) self.assertEqual(1, len(devices)) self.assertIsInstance(devices[0], WinkBulb) @@ -245,7 +257,7 @@ def test_should_handle_garage_door_opener_response(self): } """ response_dict = json.loads(response) - devices = get_devices_from_response_dict(response_dict, device_types.GARAGE_DOOR) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.GARAGE_DOOR]) self.assertEqual(1, len(devices)) self.assertIsInstance(devices[0], WinkGarageDoor) @@ -337,7 +349,7 @@ def test_should_handle_power_strip_response(self): } """ response_dict = json.loads(response) - devices = get_devices_from_response_dict(response_dict, device_types.POWER_STRIP) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.POWER_STRIP]) self.assertEqual(2, len(devices)) self.assertIsInstance(devices[0], WinkPowerStripOutlet) self.assertIsInstance(devices[1], WinkPowerStripOutlet) @@ -412,7 +424,7 @@ def test_should_handle_siren_response(self): """ response_dict = json.loads(response) - devices = get_devices_from_response_dict(response_dict, device_types.SIREN) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SIREN]) self.assertEqual(1, len(devices)) self.assertIsInstance(devices[0], WinkSiren) @@ -559,7 +571,7 @@ def test_should_handle_lock_response(self): """ response_dict = json.loads(response) - devices = get_devices_from_response_dict(response_dict, device_types.LOCK) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.LOCK]) self.assertEqual(1, len(devices)) self.assertIsInstance(devices[0], WinkLock) @@ -631,7 +643,7 @@ def test_should_handle_binary_switch_response(self): """ response_dict = json.loads(response) - devices = get_devices_from_response_dict(response_dict, device_types.BINARY_SWITCH) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.BINARY_SWITCH]) self.assertEqual(1, len(devices)) self.assertIsInstance(devices[0], WinkBinarySwitch) @@ -708,7 +720,7 @@ def test_should_handle_sensor_pod_response(self): """ response_dict = json.loads(response) - devices = get_devices_from_response_dict(response_dict, device_types.SENSOR_POD) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SENSOR_POD]) self.assertEqual(1, len(devices)) self.assertIsInstance(devices[0], WinkSensorPod) @@ -798,6 +810,6 @@ def test_should_handle_egg_tray_response(self): """ response_dict = json.loads(response) - devices = get_devices_from_response_dict(response_dict, device_types.EGG_TRAY) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.EGG_TRAY]) self.assertEqual(1, len(devices)) self.assertIsInstance(devices[0], WinkEggTray) diff --git a/src/pywink/test/sensor_test.py b/src/pywink/test/sensor_test.py new file mode 100644 index 0000000..6b8b1f3 --- /dev/null +++ b/src/pywink/test/sensor_test.py @@ -0,0 +1,76 @@ +import json +import os +import unittest + +from pywink.devices import types as device_types +from pywink.api import get_devices_from_response_dict +from pywink.devices.sensors import WinkBrightnessSensor, WinkHumiditySensor, WinkSoundPresenceSensor, \ + WinkVibrationPresenceSensor, WinkTemperatureSensor +from pywink.devices.types import DEVICE_ID_KEYS + + +class SensorTests(unittest.TestCase): + + def test_quirky_spotter_api_response_should_create_unique_one_primary_sensor_and_five_subsensors(self): + with open('{}/api_responses/quirky_spotter.json'.format(os.path.dirname(__file__))) as spotter_file: + response_dict = json.load(spotter_file) + + sensors = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SENSOR_POD]) + self.assertEquals(1 + 5, len(sensors)) + + def test_alternative_quirky_spotter_api_response_should_create_one_primary_sensor_and_five_subsensors(self): + with open('{}/api_responses/quirky_spotter_2.json'.format(os.path.dirname(__file__))) as spotter_file: + response_dict = json.load(spotter_file) + + sensors = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SENSOR_POD]) + self.assertEquals(1 + 5, len(sensors)) + + def test_brightness_should_have_correct_value(self): + with open('{}/api_responses/quirky_spotter.json'.format(os.path.dirname(__file__))) as spotter_file: + response_dict = json.load(spotter_file) + + sensors = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SENSOR_POD]) + """:type : list of WinkBrightnessSensor""" + brightness_sensor = [sensor for sensor in sensors if sensor.capability() is WinkBrightnessSensor.CAPABILITY][0] + expected_brightness = 1 + self.assertEquals(expected_brightness, brightness_sensor.brightness_percentage()) + + def test_humidity_should_have_correct_value(self): + with open('{}/api_responses/quirky_spotter.json'.format(os.path.dirname(__file__))) as spotter_file: + response_dict = json.load(spotter_file) + + sensors = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SENSOR_POD]) + """:type : list of WinkHumiditySensor""" + humidity_sensor = [sensor for sensor in sensors if sensor.capability() is WinkHumiditySensor.CAPABILITY][0] + expected_humidity = 48 + self.assertEquals(expected_humidity, humidity_sensor.humidity_percentage()) + + def test_loudness_should_have_correct_value(self): + with open('{}/api_responses/quirky_spotter.json'.format(os.path.dirname(__file__))) as spotter_file: + response_dict = json.load(spotter_file) + + sensors = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SENSOR_POD]) + """:type : list of WinkSoundPresenceSensor""" + sound_sensor = [sensor for sensor in sensors if sensor.capability() is WinkSoundPresenceSensor.CAPABILITY][0] + expected_sound_presence = False + self.assertEquals(expected_sound_presence, sound_sensor.loudness_boolean()) + + def test_vibration_should_have_correct_value(self): + with open('{}/api_responses/quirky_spotter.json'.format(os.path.dirname(__file__))) as spotter_file: + response_dict = json.load(spotter_file) + + sensors = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SENSOR_POD]) + """:type : list of WinkVibrationPresenceSensor""" + sound_sensor = [sensor for sensor in sensors if sensor.capability() is WinkVibrationPresenceSensor.CAPABILITY][0] + expected_vibrartion_presence = False + self.assertEquals(expected_vibrartion_presence, sound_sensor.vibration_boolean()) + + def test_temperature_should_have_correct_value(self): + with open('{}/api_responses/quirky_spotter.json'.format(os.path.dirname(__file__))) as spotter_file: + response_dict = json.load(spotter_file) + + sensors = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SENSOR_POD]) + """:type : list of WinkTemperatureSensor""" + sound_sensor = [sensor for sensor in sensors if sensor.capability() is WinkTemperatureSensor.CAPABILITY][0] + expected_temperature = 5 + self.assertEquals(expected_temperature, sound_sensor.temperature_float()) diff --git a/setup.py b/src/setup.py similarity index 64% rename from setup.py rename to src/setup.py index 1ef0416..24efd45 100644 --- a/setup.py +++ b/src/setup.py @@ -1,13 +1,13 @@ -from setuptools import setup +from setuptools import setup, find_packages setup(name='python-wink', - version='0.5.0', + version='0.6.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'], tests_require=['mock'], test_suite='tests', - packages=['pywink'], + packages=find_packages(exclude=['dist', 'test*']), zip_safe=True) diff --git a/tox.ini b/tox.ini index 6deafc2..d20f9fb 100644 --- a/tox.ini +++ b/tox.ini @@ -1,2 +1,3 @@ [flake8] max-line-length = 120 +exclude = src/pywink/__init__.py,src/pywink/test From 65957b6a7e627f5d678582491c53228ef6523055 Mon Sep 17 00:00:00 2001 From: The Gitter Badger Date: Wed, 10 Feb 2016 18:56:49 +0000 Subject: [PATCH 040/178] Add Gitter badge --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 97c4fb0..a8363a2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ Python Wink API --------------- +[![Join the chat at https://gitter.im/bradsk88/python-wink](https://badges.gitter.im/bradsk88/python-wink.svg)](https://gitter.im/bradsk88/python-wink?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) + _This script used to be part of Home Assistant. It has been extracted to fit the goal of Home Assistant to not contain any device specific API implementations but rely on open-source implementations of the API._ From eb915fd36410fd865c3b35bc9b6eed3e6fcc2ab5 Mon Sep 17 00:00:00 2001 From: The Gitter Badger Date: Wed, 10 Feb 2016 18:56:49 +0000 Subject: [PATCH 041/178] Add Gitter badge --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 97c4fb0..a8363a2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ Python Wink API --------------- +[![Join the chat at https://gitter.im/bradsk88/python-wink](https://badges.gitter.im/bradsk88/python-wink.svg)](https://gitter.im/bradsk88/python-wink?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) + _This script used to be part of Home Assistant. It has been extracted to fit the goal of Home Assistant to not contain any device specific API implementations but rely on open-source implementations of the API._ From 9a269559ef88ac8a230599dc3dce0bf52ce74140 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Thu, 11 Feb 2016 20:51:43 -0500 Subject: [PATCH 042/178] Added sensor capability to sensor name --- CHANGELOG.md | 3 +++ src/pywink/devices/sensors.py | 6 ++++++ src/setup.py | 2 +- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0975c97..75db679 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 0.6.1 +- Return the capability of a sensor as part of the name. + ## 0.6.0 - Major structural change. Using modules to avoid circular dependencies. - Added support for devices that contain multiple onboard sensors. diff --git a/src/pywink/devices/sensors.py b/src/pywink/devices/sensors.py index 7ddafaf..bb3e9cb 100644 --- a/src/pywink/devices/sensors.py +++ b/src/pywink/devices/sensors.py @@ -25,6 +25,12 @@ def last_reading(self): def capability(self): return self._capability + def name(self): + name = self.json_state.get('name', "Unknown Name") + if self._capability != "opened": + name += " " + self._capability + return name + def device_id(self): root_name = self.json_state.get('sensor_pod_id', self.name()) return '{}+{}'.format(root_name, self._capability) diff --git a/src/setup.py b/src/setup.py index 24efd45..3ce487f 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='0.6.0', + version='0.6.1', description='Access Wink devices via the Wink API', url='http://github.com/bradsk88/python-wink', author='Brad Johnson', From ef54a0c0c5d51f1210e10f4e1b5cde827cbe9217 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Thu, 18 Feb 2016 08:21:34 -0500 Subject: [PATCH 043/178] Added values JSON to binary switch set_state --- src/pywink/devices/standard.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/pywink/devices/standard.py b/src/pywink/devices/standard.py index 71ce2f1..e29f5bf 100644 --- a/src/pywink/devices/standard.py +++ b/src/pywink/devices/standard.py @@ -67,7 +67,12 @@ def set_state(self, state, **kwargs): :param state: a boolean of true (on) or false ('off') :return: nothing """ - response = self.api_interface.set_device_state(self, state) + values = { + "desired_state": { + "powered": state + } + } + response = self.api_interface.set_device_state(self, values) self._update_state_from_response(response) self._last_call = (time.time(), state) From 672e40e3e899aae3920fac18d8b0791327103334 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Mon, 22 Feb 2016 17:19:43 -0500 Subject: [PATCH 044/178] Fixed sensor_pod brightness and added UNIT --- CHANGELOG.md | 4 ++++ src/pywink/devices/sensors.py | 14 +++++++++----- src/pywink/test/sensor_test.py | 2 +- src/setup.py | 2 +- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75db679..5483006 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Change Log +## 0.6.1 +- Changed sensor brightness to boolean. +- Added UNIT to all sensors. + ## 0.6.1 - Return the capability of a sensor as part of the name. diff --git a/src/pywink/devices/sensors.py b/src/pywink/devices/sensors.py index bb3e9cb..3560f62 100644 --- a/src/pywink/devices/sensors.py +++ b/src/pywink/devices/sensors.py @@ -43,9 +43,11 @@ class WinkSensorPod(_WinkCapabilitySensor): and looks like so: """ CAPABILITY = 'opened' + UNIT = None def __init__(self, device_state_as_json, api_interface): - super(WinkSensorPod, self).__init__(device_state_as_json, api_interface, self.CAPABILITY, None) + super(WinkSensorPod, self).__init__(device_state_as_json, api_interface, + self.CAPABILITY, self.UNIT) def __repr__(self): return "" % (self.name(), @@ -83,14 +85,14 @@ def humidity_percentage(self): class WinkBrightnessSensor(_WinkCapabilitySensor): CAPABILITY = 'brightness' - UNIT = '%' + UNIT = None def __init__(self, device_state_as_json, api_interface): super(WinkBrightnessSensor, self).__init__(device_state_as_json, api_interface, self.CAPABILITY, self.UNIT) - def brightness_percentage(self): + def brightness_boolean(self): """ :return: The percentage of brightness as determined by the device. :rtype: int @@ -101,11 +103,12 @@ def brightness_percentage(self): class WinkSoundPresenceSensor(_WinkCapabilitySensor): CAPABILITY = 'loudness' + UNIT = None def __init__(self, device_state_as_json, api_interface): super(WinkSoundPresenceSensor, self).__init__(device_state_as_json, api_interface, self.CAPABILITY, - None) + self.UNIT) def loudness_boolean(self): """ @@ -136,11 +139,12 @@ def temperature_float(self): class WinkVibrationPresenceSensor(_WinkCapabilitySensor): CAPABILITY = 'vibration' + UNIT = None def __init__(self, device_state_as_json, api_interface): super(WinkVibrationPresenceSensor, self).__init__(device_state_as_json, api_interface, self.CAPABILITY, - None) + self.UNIT) def vibration_boolean(self): """ diff --git a/src/pywink/test/sensor_test.py b/src/pywink/test/sensor_test.py index 6b8b1f3..30c938a 100644 --- a/src/pywink/test/sensor_test.py +++ b/src/pywink/test/sensor_test.py @@ -33,7 +33,7 @@ def test_brightness_should_have_correct_value(self): """:type : list of WinkBrightnessSensor""" brightness_sensor = [sensor for sensor in sensors if sensor.capability() is WinkBrightnessSensor.CAPABILITY][0] expected_brightness = 1 - self.assertEquals(expected_brightness, brightness_sensor.brightness_percentage()) + self.assertEquals(expected_brightness, brightness_sensor.brightness_boolean()) def test_humidity_should_have_correct_value(self): with open('{}/api_responses/quirky_spotter.json'.format(os.path.dirname(__file__))) as spotter_file: diff --git a/src/setup.py b/src/setup.py index 3ce487f..5e1b85f 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='0.6.1', + version='0.6.2', description='Access Wink devices via the Wink API', url='http://github.com/bradsk88/python-wink', author='Brad Johnson', From 5d7f122b5b7e17a9b6e2a99dbde4c65232a938f8 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Mon, 22 Feb 2016 17:21:11 -0500 Subject: [PATCH 045/178] Fixed indent --- src/pywink/devices/sensors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pywink/devices/sensors.py b/src/pywink/devices/sensors.py index 3560f62..e801176 100644 --- a/src/pywink/devices/sensors.py +++ b/src/pywink/devices/sensors.py @@ -47,7 +47,7 @@ class WinkSensorPod(_WinkCapabilitySensor): def __init__(self, device_state_as_json, api_interface): super(WinkSensorPod, self).__init__(device_state_as_json, api_interface, - self.CAPABILITY, self.UNIT) + self.CAPABILITY, self.UNIT) def __repr__(self): return "" % (self.name(), From 88d6d9174e4baceba19b5e3b82161eefe9564b36 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Mon, 22 Feb 2016 17:23:45 -0500 Subject: [PATCH 046/178] Updated version --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5483006..596e5d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Change Log -## 0.6.1 +## 0.6.2 - Changed sensor brightness to boolean. - Added UNIT to all sensors. From e7372d49e7901756f892d1b4fe236b6587215c51 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Sun, 13 Mar 2016 12:48:09 -0400 Subject: [PATCH 047/178] Override capability device_id in update request --- CHANGELOG.md | 3 + src/pywink/devices/sensors.py | 10 +- .../test/api_responses/binary_sensor.json | 67 + .../test/api_responses/binary_switch.json | 62 + src/pywink/test/api_responses/eggtray.json | 80 + .../test/api_responses/garage_door.json | 64 + src/pywink/test/api_responses/light_bulb.json | 41 + src/pywink/test/api_responses/lock.json | 136 ++ .../test/api_responses/power_strip.json | 75 + src/pywink/test/api_responses/siren.json | 63 + src/pywink/test/api_responses/winkapiv2.txt | 2044 +++++++++++++++++ src/pywink/test/init_test.py | 981 ++------ src/pywink/test/sensor_test.py | 76 - src/setup.py | 2 +- 14 files changed, 2892 insertions(+), 812 deletions(-) create mode 100644 src/pywink/test/api_responses/binary_sensor.json create mode 100644 src/pywink/test/api_responses/binary_switch.json create mode 100644 src/pywink/test/api_responses/eggtray.json create mode 100644 src/pywink/test/api_responses/garage_door.json create mode 100644 src/pywink/test/api_responses/light_bulb.json create mode 100644 src/pywink/test/api_responses/lock.json create mode 100644 src/pywink/test/api_responses/power_strip.json create mode 100644 src/pywink/test/api_responses/siren.json create mode 100644 src/pywink/test/api_responses/winkapiv2.txt delete mode 100644 src/pywink/test/sensor_test.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 596e5d2..734d995 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 0.6.3 +- Override capability sensor device_id during update. + ## 0.6.2 - Changed sensor brightness to boolean. - Added UNIT to all sensors. diff --git a/src/pywink/devices/sensors.py b/src/pywink/devices/sensors.py index e801176..a9392e7 100644 --- a/src/pywink/devices/sensors.py +++ b/src/pywink/devices/sensors.py @@ -35,6 +35,12 @@ def device_id(self): root_name = self.json_state.get('sensor_pod_id', self.name()) return '{}+{}'.format(root_name, self._capability) + def update_state(self): + """ Update state with latest info from Wink API. """ + root_name = self.json_state.get('sensor_pod_id', self.name()) + response = self.api_interface.get_device_state(self, root_name) + self._update_state_from_response(response) + class WinkSensorPod(_WinkCapabilitySensor): """ represents a wink.py sensor @@ -94,8 +100,8 @@ def __init__(self, device_state_as_json, api_interface): def brightness_boolean(self): """ - :return: The percentage of brightness as determined by the device. - :rtype: int + :return: True if light is detected. False if light is below detection threshold (varies by device) + :rtype: bool """ return self.last_reading() diff --git a/src/pywink/test/api_responses/binary_sensor.json b/src/pywink/test/api_responses/binary_sensor.json new file mode 100644 index 0000000..93404c9 --- /dev/null +++ b/src/pywink/test/api_responses/binary_sensor.json @@ -0,0 +1,67 @@ +{ + "data": [{ + "last_event": { + "brightness_occurred_at": null, + "loudness_occurred_at": null, + "vibration_occurred_at": null + }, + "model_name": "Tripper", + "capabilities": { + "sensor_types": [ + { + "field": "opened", + "type": "boolean" + }, + { + "field": "battery", + "type": "percentage" + } + ] + }, + "manufacturer_device_model": "quirky_ge_tripper", + "location": "", + "radio_type": "zigbee", + "manufacturer_device_id": null, + "gang_id": null, + "sensor_pod_id": "37614", + "subscription": { + }, + "units": { + }, + "upc_id": "184", + "hidden_at": null, + "last_reading": { + "battery_voltage_threshold_2": 0, + "opened": false, + "battery_alarm_mask": 0, + "opened_updated_at": 1421697092.7347496, + "battery_voltage_min_threshold_updated_at": 1421697092.7347229, + "battery_voltage_min_threshold": 0, + "connection": null, + "battery_voltage": 25, + "battery_voltage_threshold_1": 25, + "connection_updated_at": null, + "battery_voltage_threshold_3": 0, + "battery_voltage_updated_at": 1421697092.7347066, + "battery_voltage_threshold_1_updated_at": 1421697092.7347302, + "battery_voltage_threshold_3_updated_at": 1421697092.7347434, + "battery_voltage_threshold_2_updated_at": 1421697092.7347374, + "battery": 1.0, + "battery_updated_at": 1421697092.7347553, + "battery_alarm_mask_updated_at": 1421697092.734716 + }, + "triggers": [ + ], + "name": "MasterBathroom", + "lat_lng": [ + 37.550773, + -122.279182 + ], + "uuid": "a2cb868a-dda3-4211-ab73-fc08087aeed7", + "locale": "en_us", + "device_manufacturer": "quirky_ge", + "created_at": 1421523277, + "local_id": "2", + "hub_id": "88264" + }] +} \ No newline at end of file diff --git a/src/pywink/test/api_responses/binary_switch.json b/src/pywink/test/api_responses/binary_switch.json new file mode 100644 index 0000000..737678f --- /dev/null +++ b/src/pywink/test/api_responses/binary_switch.json @@ -0,0 +1,62 @@ +{ + "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": {} +} \ No newline at end of file diff --git a/src/pywink/test/api_responses/eggtray.json b/src/pywink/test/api_responses/eggtray.json new file mode 100644 index 0000000..0a1ebde --- /dev/null +++ b/src/pywink/test/api_responses/eggtray.json @@ -0,0 +1,80 @@ +{ + "data": [{ + "last_reading": { + "connection": true, + "connection_updated_at": 1417823487.490747, + "battery": 0.83, + "battery_updated_at": 1417823487.490747, + "inventory": 3, + "inventory_updated_at": 1449705551.7313306, + "freshness_remaining": 2419191, + "freshness_remaining_updated_at": 1449705551.7313495, + "age_updated_at": 1449705551.7313418, + "age": 1449705542, + "connection_changed_at": 1449705443.6858568, + "next_trigger_at_updated_at": null, + "next_trigger_at": null, + "egg_1_timestamp_updated_at": 1449753143.8631344, + "egg_1_timestamp_changed_at": 1449705534.0782206, + "egg_1_timestamp": 1449705545.0, + "egg_2_timestamp_updated_at": 1449753143.8631344, + "egg_2_timestamp_changed_at": 1449705534.0782206, + "egg_2_timestamp": 1449705545.0, + "egg_3_timestamp_updated_at": 1449753143.8631344, + "egg_3_timestamp_changed_at": 1449705534.0782206, + "egg_3_timestamp": 1449705545.0, + "egg_4_timestamp_updated_at": 1449753143.8631344, + "egg_4_timestamp_changed_at": 1449705534.0782206, + "egg_4_timestamp": 1449705545.0, + "egg_5_timestamp_updated_at": 1449753143.8631344, + "egg_5_timestamp_changed_at": 1449705534.0782206, + "egg_5_timestamp": 1449705545.0, + "egg_6_timestamp_updated_at": 1449753143.8631344, + "egg_6_timestamp_changed_at": 1449705534.0782206, + "egg_6_timestamp": 1449705545.0, + "egg_7_timestamp_updated_at": 1449753143.8631344, + "egg_7_timestamp_changed_at": 1449705534.0782206, + "egg_7_timestamp": 1449705545.0, + "egg_8_timestamp_updated_at": 1449753143.8631344, + "egg_8_timestamp_changed_at": 1449705534.0782206, + "egg_8_timestamp": 1449705545.0, + "egg_9_timestamp_updated_at": 1449753143.8631344, + "egg_9_timestamp_changed_at": 1449705534.0782206, + "egg_9_timestamp": 1449705545.0, + "egg_10_timestamp_updated_at": 1449753143.8631344, + "egg_10_timestamp_changed_at": 1449705534.0782206, + "egg_10_timestamp": 1449705545.0, + "egg_11_timestamp_updated_at": 1449753143.8631344, + "egg_11_timestamp_changed_at": 1449705534.0782206, + "egg_11_timestamp": 1449705545.0, + "egg_12_timestamp_updated_at": 1449753143.8631344, + "egg_12_timestamp_changed_at": 1449705534.0782206, + "egg_12_timestamp": 1449705545.0, + "egg_13_timestamp_updated_at": 1449753143.8631344, + "egg_13_timestamp_changed_at": 1449705534.0782206, + "egg_13_timestamp": 1449705545.0, + "egg_14_timestamp_updated_at": 1449753143.8631344, + "egg_14_timestamp_changed_at": 1449705534.0782206, + "egg_14_timestamp": 1449705545.0 + }, + "eggtray_id": "153869", + "name": "Egg Minder", + "freshness_period": 2419200, + "locale": "en_us", + "units": {}, + "created_at": 1417823382, + "hidden_at": null, + "capabilities": {}, + "triggers": [], + "device_manufacturer": "quirky_ge", + "model_name": "Egg Minder", + "upc_id": "23", + "upc_code": "814434017233", + "lat_lng": [38.429962, -122.653715], + "location": "" + }], + "errors": [], + "pagination": { + "count": 1 + } +} \ No newline at end of file diff --git a/src/pywink/test/api_responses/garage_door.json b/src/pywink/test/api_responses/garage_door.json new file mode 100644 index 0000000..b12f2a6 --- /dev/null +++ b/src/pywink/test/api_responses/garage_door.json @@ -0,0 +1,64 @@ +{ + "data": [{ + "desired_state": { + "position": 0 + }, + "last_reading": { + "position_opened": "N\/A", + "position_opened_updated_at": 1450357467.371, + "tamper_detected_true": null, + "tamper_detected_true_updated_at": null, + "connection": true, + "connection_updated_at": 1450357538.2715, + "position": 0, + "position_updated_at": 1450357537.836, + "battery": null, + "battery_updated_at": null, + "fault": false, + "fault_updated_at": 1447976866.0784, + "disabled": null, + "disabled_updated_at": null, + "control_enabled": true, + "control_enabled_updated_at": 1447976866.0784, + "desired_position_updated_at": 1447976846.8869, + "connection_changed_at": 1444775470.5484, + "position_changed_at": 1450357537.836, + "control_enabled_changed_at": 1444775472.2474, + "fault_changed_at": 1444775472.2474, + "position_opened_changed_at": 1450357467.371, + "desired_position_changed_at": 1447976846.8869 + }, + "garage_door_id": "30528", + "name": "Garage Door", + "locale": "en_us", + "units": { + + }, + "created_at": 1444775470, + "hidden_at": null, + "capabilities": { + "home_security_device": true + }, + "triggers": [ + + ], + "manufacturer_device_model": "chamberlain_garage_door_opener", + "manufacturer_device_id": "1133930", + "device_manufacturer": "chamberlain", + "model_name": "MyQ Garage Door Controller", + "upc_id": "26", + "upc_code": "012381109302", + "hub_id": null, + "local_id": null, + "radio_type": null, + "linked_service_id": "206203", + "lat_lng": [ + 0, + 0 + ], + "location": "", + "order": null + }], + "errors": [], + "pagination": {} +} \ No newline at end of file diff --git a/src/pywink/test/api_responses/light_bulb.json b/src/pywink/test/api_responses/light_bulb.json new file mode 100644 index 0000000..460c2a5 --- /dev/null +++ b/src/pywink/test/api_responses/light_bulb.json @@ -0,0 +1,41 @@ +{ + "data": [{ + "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 + }] +} \ No newline at end of file diff --git a/src/pywink/test/api_responses/lock.json b/src/pywink/test/api_responses/lock.json new file mode 100644 index 0000000..54b733d --- /dev/null +++ b/src/pywink/test/api_responses/lock.json @@ -0,0 +1,136 @@ +{ + "data": [ + { + "desired_state": { + "locked": true, + "beeper_enabled": true, + "vacation_mode_enabled": false, + "auto_lock_enabled": false, + "key_code_length": 4, + "alarm_mode": null, + "alarm_sensitivity": 0.6, + "alarm_enabled": false + }, + "last_reading": { + "locked": true, + "locked_updated_at": 1417823487.490747, + "connection": true, + "connection_updated_at": 1417823487.490747, + "battery": 0.83, + "battery_updated_at": 1417823487.490747, + "alarm_activated": null, + "alarm_activated_updated_at": null, + "beeper_enabled": true, + "beeper_enabled_updated_at": 1417823487.490747, + "vacation_mode_enabled": false, + "vacation_mode_enabled_updated_at": 1417823487.490747, + "auto_lock_enabled": false, + "auto_lock_enabled_updated_at": 1417823487.490747, + "key_code_length": 4, + "key_code_length_updated_at": 1417823487.490747, + "alarm_mode": null, + "alarm_mode_updated_at": 1417823487.490747, + "alarm_sensitivity": 0.6, + "alarm_sensitivity_updated_at": 1417823487.490747, + "alarm_enabled": true, + "alarm_enabled_updated_at": 1417823487.490747, + "last_error": null, + "last_error_updated_at": 1417823487.490747, + "desired_locked_updated_at": 1417823487.490747, + "desired_beeper_enabled_updated_at": 1417823487.490747, + "desired_vacation_mode_enabled_updated_at": 1417823487.490747, + "desired_auto_lock_enabled_updated_at": 1417823487.490747, + "desired_key_code_length_updated_at": 1417823487.490747, + "desired_alarm_mode_updated_at": 1417823487.490747, + "desired_alarm_sensitivity_updated_at": 1417823487.490747, + "desired_alarm_enabled_updated_at": 1417823487.490747, + "locked_changed_at": 1417823487.490747, + "battery_changed_at": 1417823487.490747, + "desired_locked_changed_at": 1417823487.490747, + "desired_beeper_enabled_changed_at": 1417823487.490747, + "desired_vacation_mode_enabled_changed_at": 1417823487.490747, + "desired_auto_lock_enabled_changed_at": 1417823487.490747, + "desired_key_code_length_changed_at": 1417823487.490747, + "desired_alarm_mode_changed_at": 1417823487.490747, + "desired_alarm_sensitivity_changed_at": 1417823487.490747, + "desired_alarm_enabled_changed_at": 1417823487.490747, + "last_error_changed_at": 1417823487.490747 + }, + "lock_id": "5304", + "name": "Main", + "locale": "en_us", + "units": {}, + "created_at": 1417823382, + "hidden_at": null, + "capabilities": { + "fields": [ + { + "field": "locked", + "type": "boolean", + "mutability": "read-write" + }, + { + "field": "connection", + "mutability": "read-only", + "type": "boolean" + }, + { + "field": "battery", + "mutability": "read-only", + "type": "percentage" + }, + { + "field": "alarm_activated", + "mutability": "read-only", + "type": "boolean" + }, + { + "field": "beeper_enabled", + "type": "boolean" + }, + { + "field": "vacation_mode_enabled", + "type": "boolean" + }, + { + "field": "auto_lock_enabled", + "type": "boolean" + }, + { + "field": "key_code_length", + "type": "integer" + }, + { + "field": "alarm_mode", + "type": "string" + }, + { + "field": "alarm_sensitivity", + "type": "percentage" + }, + { + "field": "alarm_enabled", + "type": "boolean" + } + ], + "home_security_device": true + }, + "triggers": [], + "manufacturer_device_model": "schlage_zwave_lock", + "manufacturer_device_id": null, + "device_manufacturer": "schlage", + "model_name": "BE469", + "upc_id": "11", + "upc_code": "043156312214", + "hub_id": "11780", + "local_id": "1", + "radio_type": "zwave", + "lat_lng": [38.429962, -122.653715], + "location": "" + } + ], + "errors": [], + "pagination": { + "count": 1 + } +} \ No newline at end of file diff --git a/src/pywink/test/api_responses/power_strip.json b/src/pywink/test/api_responses/power_strip.json new file mode 100644 index 0000000..463ba26 --- /dev/null +++ b/src/pywink/test/api_responses/power_strip.json @@ -0,0 +1,75 @@ +{ + "data": [ + { + "desired_state": {}, + "last_reading": { + "connection": false, + "connection_updated_at": 1452306146.129263, + "connection_changed_at": 1452306144.425378 + }, + "powerstrip_id": "24123", + "name": "Power strip", + "locale": "en_us", + "units": {}, + "created_at": 1451578768, + "hidden_at": null, + "capabilities": {}, + "triggers": [], + "device_manufacturer": "quirky_ge", + "model_name": "Pivot Power Genius", + "upc_id": "24", + "upc_code": "814434017226", + "lat_lng": [ + 12.123456, + -98.765432 + ], + "location": "", + "mac_address": "0c2a69123456", + "serial": "AAAA00123456", + "outlets": [ + { + "powered": false, + "scheduled_outlet_states": [], + "name": "First", + "outlet_index": 0, + "outlet_id": "48123", + "icon_id": "4", + "parent_object_type": "powerstrip", + "parent_object_id": "24123", + "desired_state": { + "powered": false + }, + "last_reading": { + "powered": true, + "powered_updated_at": 1452306146.0882413, + "powered_changed_at": 1452306004.7519948, + "desired_powered_updated_at": 1452306008.2215497 + } + }, + { + "powered": false, + "scheduled_outlet_states": [], + "name": "Second", + "outlet_index": 1, + "outlet_id": "48124", + "icon_id": "4", + "parent_object_type": "powerstrip", + "parent_object_id": "24123", + "desired_state": { + "powered": false + }, + "last_reading": { + "powered": true, + "powered_updated_at": 1452311731.8861659, + "powered_changed_at": 1452311731.8861659, + "desired_powered_updated_at": 1452311885.3523679 + } + } + ] + } + ], + "errors": [], + "pagination": { + "count": 10 + } +} \ No newline at end of file diff --git a/src/pywink/test/api_responses/siren.json b/src/pywink/test/api_responses/siren.json new file mode 100644 index 0000000..2786275 --- /dev/null +++ b/src/pywink/test/api_responses/siren.json @@ -0,0 +1,63 @@ +{ + "data":[ + { + "desired_state":{ + "auto_shutoff":30, + "mode":"siren_and_strobe", + "powered":false + }, + "last_reading":{ + "connection":true, + "connection_updated_at":1453249957.2466462, + "battery":1, + "battery_updated_at":1453249957.2466462, + "auto_shutoff":30, + "auto_shutoff_updated_at":1453249957.2466462, + "mode":"siren_and_strobe", + "mode_updated_at":1453249957.2466462, + "powered":false, + "powered_updated_at":1453249957.2466462, + "desired_auto_shutoff_updated_at":1452812848.5178623, + "desired_mode_updated_at":1452812848.5178623, + "desired_powered_updated_at":1452812668.1190264, + "connection_changed_at":1452812587.0312104, + "powered_changed_at":1452812668.0807295, + "battery_changed_at":1453032821.1796713, + "mode_changed_at":1452812589.8262901, + "auto_shutoff_changed_at":1452812589.8262901, + "desired_auto_shutoff_changed_at":1452812590.029748, + "desired_powered_changed_at":1452812668.1190264, + "desired_mode_changed_at":1452812848.5178623 + }, + "siren_id":"6123", + "name":"Alarm", + "locale":"en_us", + "units":{ + + }, + "created_at":1452812587, + "hidden_at":null, + "capabilities":{ + + }, + "device_manufacturer":"linear", + "model_name":"Wireless Siren & Strobe (Wireless)", + "upc_id":"243", + "upc_code":"wireless_linear_siren", + "hub_id":"30123", + "local_id":"8", + "radio_type":"zwave", + "lat_lng":[ + 12.1345678, + -98.765432 + ], + "location":"" + } + ], + "errors":[ + + ], + "pagination":{ + "count":17 + } +} \ No newline at end of file diff --git a/src/pywink/test/api_responses/winkapiv2.txt b/src/pywink/test/api_responses/winkapiv2.txt new file mode 100644 index 0000000..f031014 --- /dev/null +++ b/src/pywink/test/api_responses/winkapiv2.txt @@ -0,0 +1,2044 @@ +FORMAT: 1A + +HOST: https://api.wink.com + +# Wink API +The Wink API connects Wink devices to users, apps, each other, and the wider web. + +NOTE: This is the documentation for the v2 version of the Wink API. It has breaking changes on the [original API](http://docs.wink.apiary.io/) published in 2013. If you are a user of the original API but would like access to this version, please contact Wink Support to ask for a new set of credentials. + + +# Group A RESTful Service +The Wink API is a [RESTful](http://en.wikipedia.org/wiki/Representational_state_transfer) service. + +## Authentication +Nearly every request to the Wink API requires an OAuth bearer token. + +Exceptions to this rule will be documented. + +## Content types +Nearly every request to the Wink API should be expressed as JSON. + +Nearly every response from the Wink API will be expressed as JSON. + +Exceptions to this rule will be documented. + +## HTTP verbs +The Wink API uses HTTP verbs in pretty standard ways: + +- GET for retrieving information without side-effects +- PUT for updating existing resources, with partial-update semantics supported +- POST for creating new resources or blind upserts of existing resources +- DELETE for destructive operations on existing resurces + +## Identifiers +All objects in the Wink API can be identified by `object_type` and `object_id`. The `object_id` is a string and not globally unique, currently. That is there can be an `'object_type':'light_bulb'` and `'object_id':'abc'` and an `'object_type':'thermostat'` and `'object_id':'abc'` + +It is possible for the API to re-assign identifiers to resources to rebalance keys; in this case, your resource will still exist but it (and all references to it) will be updated to the new identifier. Your application should be able to handle this case. + +## Creatable vs. permanent +The term "creatable" will describe a resource which may be created and/or destroyed by the user. + +The term "permanent" will describe a resource which may not be directly created or deleted by a user. + +Note that permanent **does not imply** that the resource will always exist, just that the user may not create or destroy it. Under no circumstances should you assume that a resource will always exist. + +## Mutable vs. immutable +The term "mutable" will describe a resource or attribute which the user may modify at will, assuming the user has the necessary permissions to do so. + +The term "immutable" will describe a resouce or attribute which may not be modified directly by the user. + +Note that immutable **does not imply** that the resource or attribute will never change, just that the user may not directly change it. Under no circumstances should you assume that a resource or attribute will always remain the same. + +## Error states +The common [HTTP Response Status Codes](https://github.com/for-GET/know-your-http-well/blob/master/status-codes.md) are used. + +# Group OAuth +Authentication to the API + +## Obtain access token [/oauth2/token] +### Sign in as user, or refresh user's expired access token [POST] + +Note that unlike most other calls, this call does not require (and in fact should not use) an OAuth 2.0 bearer token. + ++ Request Sign in as user (application/json) + + { + "client_id": "consumer_key_goes_here", + "client_secret": "consumer_secret_goes_here", + "username": "user@example.com", + "password": "password_goes_here", + "grant_type": "password" + } + ++ Request Refresh expired access token (application/json) + + { + "client_id": "consumer_key_goes_here", + "client_secret": "consumer_secret_goes_here", + "grant_type": "refresh_token", + "refresh_token": "crazy_token_like_240qhn16hwrnga05euynaoeiyhw52_goes_here" + } + ++ Response 201 (application/json) + + { + "data": { + "access_token": "example_access_token_like_135fhn80w35hynainrsg0q824hyn", + "refresh_token": "crazy_token_like_240qhn16hwrnga05euynaoeiyhw52_goes_here", + "token_type": "bearer" + } + } + + + +# Group Subscriptions + +Real-time updates through the Wink API are managed by [PubNub](https://www.pubnub.com/) + +**Subscriptions are organized around "topics"** + +Subscriptions to topics fall into two categories: Lists and Objects. + +List subscriptions send updates when an object is added or removed from the list. For example, a subscription to `/users/me/wink_devices` would trigger an update when a new device is added to a user account. + +Object subscriptions send updates when an object is updated in any way. For example, a subscription to `/light_bulbs/abc` would trigger an update when a light bulb goes from powered off to powered on. +Several changes may be aggregated into a single broadcast, when the changes have happened in rapid succession. + +To subscribe to a topic, find the subscription object inside the response from a GET request to either a list or an object. + + "subscription": { + "pubnub": { + "subscribe_key": "worghwihr0aijyp5ejhapethnpaethn", + "channel": "w0y8hq03hy5naeorihnse05iyjse5yijsm" + } + } + +# Group Device + +**Common patterns and fields of Wink API Devices** + +Specific fields, particularly in desired_state and last_reading will be outlined in the object-specific sections + +Each Wink Device can have the following attributes, but not all attributes will be populated + +Prepare to receive null for any one of these. For specific implementations, refer to the device documentation of the given device type. + +|API field|Attributes|Description| +|----|----|----| +|object_type|(string, assigned)|type of object (NOTE: legacy apps expect a specific type id such as "light_bulb_id")| +|object_id|(string, assigned)|id of object (NOTE: legacy apps expect a specific type id such as "light_bulb_id")| +|name|(String, writable)|Name of the device, default given by server upon provisioning but can be updated by user| +|locale |(String, format LL_CC -- "en_us", "fr_fr")|Can be updated by user, but not exposed in the app, usually based of user's locale| +|units|(object, specific to device)|See [Units](/Wink_Devices/General/API/Units)| +|created_at|(long, timestamp, immutable)|Time device was added to account| +|subscription|(pubnub subscription object)|See [Subscription](/General/API/Subscriptions)| +|manufacturer_device_model|(String, assigned)|snaked case, unique device model| +|manufatcurer_device_id | (String, assigned | udid of third party device in third party system| +|hub_id|(String, assigned)|id of hub associated with device, only for devices with hub| +|local_id|(String, assigned)| id of device on hub, only for devices with hub| +|radio_type | (String, assigned)|currently only for devices with hub, available values "zigbee", "zwave", "lutron", "wink_project_one"| +|device_manufacturer|(String, assigned)|Human readable display string of manufacturer| +|lat_lng |(tuple of floats, writable)|location of device| +|location|(String, writable)|pretty printable location of device| +|desired_state|(object, values of requested state)| Depends on object type| +|last_reading|(object, values of last reading from device)| Depends on object type| +|capabilities|(object, specific capabilities of this object)| for instance for sensor the last_reading values available| + +## Capabilities + +The API sets capabilities for devices to indicate whether a field is present, whether it is mutable and what are the allowed values. + +The power of capabilities allow for devices with different attributes to have the same object type. An example is using capabilties to distinguish a light bulb that has only has dimming capabilites from a light bulb that has dimming and color changing capabilities. + +Capabilities currently contains one object `fields`, an array object of field capabilities. + +Note: note all devices have capabilities. Some legacy devices added in the first six months of the Wink API have yet to be converted. + +### Field attributes +|API field|Attributes|Description| +|----|----|----| +|field|(string)|name of field| +|type|(string)|one of "boolean", "percentage", "integer", "float", "string"| +|mutability|(string)|one of "read-only", "read-write"| +|choices|(array)|array of allowed choices for field. Nearly always a string array| +|range|(tuple)|tuple of upper and lower bounds for field. For integer and float fields| + + + { + "fields": [{ + "field": "connection", + "type": "boolean", + "mutability": "read-only" + }, { + "field": "powered", + "type": "boolean", + "mutability": "read-write" + }, { + "field": "brightness", + "type": "percentage", + "mutability": "read-write" + }, { + "field": "color_model", + "type": "string", + "choices": ["rgb", "xy", "hsb", "color_temperature"], + "mutability": "read-write" + }, { + "field": "color_temperature", + "type": "integer", + "range": [2000, 6500], + "mutability": "read-write" + }] + } +## Desired State and Last Reading [/{device_type}/{device_id}/desired_state] + +The desired_state/last_reading paradigm is available in almost all wink devices. Fields in desired_state are what the client requests the state of the device to be, whereas fields in last_reading are what the API believes to be the current state of things. To keep requested changes distinct and distinguishable from real changes, each desired_state field has the following counterparts in last_reading + +* `desired_foo` +* `desired_foo_updated_at` +* `foo` +* `foo_updated_at` +* `foo_changed_at` - this differs from update indicating when a reading was altered, in contrast to updated indicating the last time a reading was reported by a device + +When the device acknowledges that the state has been applied, the server will clear the field from desired_state. Then last_reading.x, last_reading.x_updated_at and last_reading.x_changed_at will update appropriately. + +If a device fails to change to the desired_state, likely due to a failure of some kind including the device being offline, the server will give up after 2 minutes and clear desired_state. Only last_reading.x_updated_at would update in that case as there was no change to the actual reading. + +The consequences of this change are two-fold + +- The clients can definitively know if a requested change has not yet been applied or failed to be applied. This means that UIs such as garage door and lock that depend on an applied state do not have to have an arbitrary time out. Or UIs such as the lights UI can display when a request is in progress +- When changes are requested, only those attributes that are actually changed will be applied rather than re-applying an entire desired_state when changing one attribute. + +Throughout the rest of the documentation, writable states will only be documented under desired_state and read only states will be documented under last_reading + +### Update desired state [PUT] + +While you can update a device's desired state with a PUT to the whole device, you can also do a put of just the desired state to this endpoint + ++ Request (application/json) + + + Body + + { + "desired_state": { + "powered": true + } + } + ++ Response 200 (application/json) + + [Wink Devices][] + +## Wink Devices [/users/me/wink_devices] + ++ Model (application/json) + + JSON representation of a device + + + Body + + { + "data":{ + "object_id":"27105", + "object_type":"light_bulb", + "locale":"Lighty Light", + "manufacturer_device_model":"ge_light_bulb", + "manufatcurer_device_id":"123456", + "hub_id":"rtyui", + "local_id":"5", + "radio_type":"zigbee", + "device_manufacturer":"GE", + "subscription": { + "pubnub": { + "subscribe_key": "worghwihr0aijyp5ejhapethnpaethn", + "channel": "w0y8hq03hy5naeorihnse05iyjse5yijsm" + } + } + "locale":"en_us", + "units":{ + }, + "lat_lng":[], + "desired_state":{}, + "last_reading":{}, + "capabilities":{}, + "created_at":1234567890, + }, + "errors":[ + ], + "pagination":{ + } + } + +### Retrieve All Devices of User [GET] + ++ Response 200 (application/json) + + [Wink Devices][] + +## Sharing device [/{device_type}/{device_id}/users] + +**Certain users have permissions to share the device with other users.** + +Only users with "manage_sharing" permissions can share a device, all other permissions are deprecated or underutilized at this time + ++ Model (application/json) + + JSON representation of a shared user + + + Body + + { + "device_id": "qs1ga9_1234deadbeef", //NOTE: the field name will vary depending on device + "user_id": "abc123def-an15nag", + "email": "user@example.com", + "permissions": ["read_data", "write_data", "read_triggers", "write_triggers", "manage_sharing"] + } + +### List shared device users [GET] + ++ Response 200 (application/json) + + [Sharing device][] + + +### Share a device [POST] + ++ Request + + { + "email": "user2@example2.com" + } + ++ Response 200 (application/json) + + [Sharing device][] + +## Unshare a device [/{device_type}/{device_id}/users/{email}] + +### Unshare a device [DELETE] + ++ Response 204 + +## Air Conditioner [/air_conditioners/{device_id}] + +### Get Air Conditioner [GET] + +#### Desired State Attributes + +|API field|Attributes|Description| +|----|----|----| +|fan_speed|float|0 - 1| +|mode|string|"cool_only", "fan_only", "auto_eco"| +|powered|boolean|whether or not the unit is powered on +|max_set_point|float|temperature above which the unit should be cooling| + +#### Last Reading Attributes + +|API field|Attributes|Description| +|----|----|----| +|connection|Boolean|whether or not the device is reachable remotely| +|temperature|float|maps to ambient temperature last read from device itself| +|consumption|float|total consumption in watts| + +## Binary Switch [/binary_switches/{device_id}] + +### Get Binary Switch [GET] + +#### Desired State Attributes + +|API field|Attributes|Description| +|----|----|----| +|powered|boolean|whether device is powered on| +#### Last Reading Attributes + +|API field|Attributes|Description| +|----|----|----| +|connection|Boolean|whether or not the device is reachable remotely| + +## Blind [/shades/{device_id}] + +### Get Blind [GET] + +#### Desired State Attributes + +|API field|Attributes|Description| +|----|----|----| +|position|float|0.0 is completely closed and 1.0 is completely open.| + +#### Last Reading Attributes + +|API field|Attributes|Description| +|----|----|----| +|connection|Boolean|whether or not the device is reachable remotely| + +## Camera [/cameras/{device_id}] + +### Get Camera [GET] + +#### Desired State Attributes + +|API field|Attributes|Description| +|----|----|----| +|capturing_video|boolean|Whether or not the camera is currently capturing video| +|capturing_audio|boolean|Whether or not the camera is currently capturing audio| +|mode|string|one of "armed", "disarmed", "privacy" + +#### Last Reading Attributes + +|API field|Attributes|Description| +|----|----|----| +|motion|Boolean|whether or not the dropcam currently detects movement| +|loudness|Boolean|whether or not the dropcam currently detects sound| +|connection|Boolean|whether or not the device is reachable remotely| + +## Doorbell [/doorbells/{device_id}] + +### Get Doorbell [GET] + +#### Desired State Attributes +n/a + +#### Last Reading Attributes + +|API field|Data Type|Description| +|---|---|---| +|button_pressed|boolean|doorbell button pressed event| +|motion|boolean|motion detected by doorbell event| +|battery|float|battery status| +|connection|boolean|online or offline| + +## Egg Minder [/eggtrays/{device_id}} + +### Get Egg Minder [GET] + +#### Additional API fields on eggtray object + +|API field|Attributes|Description| +|----|----|----| +|freshness_period|(integer, mutable with write_data permission on eggtray device)|[Period during which eggs are defined as fresh in seconds]| +|eggs|(array of 14 integers, assigned, immutable)|[Timestamp in seconds of when each egg was added]| + + { + "eggs": [ + 1377180085, + 1377180086, + 1377180087, + 1377180088, + 1377180089, + 1377180090, + 1377180091, + 1377180092, + 1377180093, + 1377180094, + 1377180095, + 1377180096, + 1377180097, + 1377180098 + ], + "freshness_period": 2419200 + } + +#### Desired State Attributes +n/a + +#### Last Reading Attributes + +|API field|Attributes|Description| +|----|----|----| +|connection|boolean|connection to server| +|battery|float|0.0 - 1.0 battery level| +|inventory|integer|# of eggs| +|freshness_remaining|integer|seconds until oldest egg goes bad| + +## Garage Door [/garage_doors/{device_id}] + +### Get Garage Door [GET] + +#### Desired State Attributes + +|API field|Attributes|Description| +|----|----|----| +|position|float|0 - 1, *while a float, the app should only send up 0 or 1, for security*| +|laser|boolean|turn on/off laser| +|calibration_enabled|boolean|turn on/off calibration mode| + +#### Last Reading Attributes + +|API field|Attributes|Description| +|----|----|----| +|connection|Boolean|whether or not the device is reachable remotely| +|buzzer|boolean|whether or not the buzzer is on| +|led|boolean|whether or not the LED is on| +|moving|boolean|whether or not the garage door is current moving| +|fault|boolean|whether or not there is an error with the garage door| +|disabled|boolean|whether remote control is disabled due to an error| +|error|array|string array of errors| +|control_enabled|boolean|whether or not the unit is capable of remote control| +|controller_error|array,string|errors from the controller unit,putting the garage door into state where remote control is disabled| +|tilt_sensor_error|array,string|whether the tilt sensor has a battery/in range or if that battery is low| + +## Hub [/hubs/{device_id}] + +### Get Hub [GET] + +### Desired State Attributes + +|API field|Attributes|Description| +|----|----|----| +|pairing_mode|(string of radio to enter)|[zigbee, zwave, lutron, zwave_exclusion]| +|kidde_radio_code|(int, assigned, mutable)|[0 - 255, represent 8-bit radio frequency of Wink Hub to look for kidde smoke detector}| + +### Last Reading Attributes + +|API field|Attributes|Description| +|----|----|----| +|connection|Boolean|whether or not the device is reachable remotely| +|remote_pairable|boolean|whether or not a connected Lutron remote is ready for association| +|updating_firmware|boolean|whether the hub is currently updating its firmware| +|firmware_version|string|current hub firmware_version| +|mac_address|string|hub mac address| +|ip_address|string|ip address of hub| +|update_needed|boolean|whether or not the hub needs a firmware update| + +## Light Bulb [/light_bulb/{device_id}] + +### Get Light Bulb [GET] + +#### Desired State Attributes + +|API field|Attributes|Description| +|----|----|----| +|powered|boolean|whether device is powered on| +|brightness|float|0.0 to 1.0, dimness level (binary_switch and light_bulb)| +|color_model|(string)|one of: "xy", "hsb", "color_temperature", or "rgb" | +|color_x|(float, precision 4)|the CIE 1931 coordinates of the bulb's color [0.0, 1.0]| +|color_y|(float, precision 6)|he CIE 1931 coordinates of the bulb's color [0.0, 1.0]| +|hue|(float, precision 6)|the 360-degree value of the bulb's color (normalized to 1.0)| +|saturation|(float, precision 6)|the percentage value of the bulb's saturation (normalized to 1.0) [0.0, 1.0]| +|color_temperature|(integer)|the Kelvin value of the bulb's color [2000 .. 6500]| +|color|(string)|the hexadecimal value of the bulb color (without a leading '#')| +|powering_mode|(string)|one of "dumb", "smart", "none" or null| + +#### Last Reading Attributes + +|API field|Attributes|Description| +|----|----|----| +|connection|Boolean|whether or not the device is reachable remotely| + +## Lock [/locks/{device_id}] + +### Get Lock [GET] + +#### Desired State Attributes + +|API field|Attributes|Description| +|----|----|----| +|locked|boolean|whether or not the lock is locked| +|alarm_mode|string|null, "activity", "tamper", "forced_entry"| +|alarm_sensitivity|float|ercentage 1.0 for Very sensitive, 0.2 for not sensitive, steps in values of 0.2| +|auto_lock_enabled|boolean|whether or not the auto lock feature is enabled| +|beeper_enabled|boolean|whether or not the beeper is enabled| +|vacation_mode|boolean|whether or not the vacation mode is enabled| +|key_code_length|integer| usually betweeen 4 and 8, check for capabilities for allowed units| + +#### Last Reading Attributes + +|API field|Attributes|Description| +|----|----|----| +|connection|Boolean|whether or not the device is reachable remotely| +|alarm_activated|boolean|becomes true when alarm is triggered on lock| + +## Nimbus [/cloud_clocks/{device_id}] + +The Nimbus is an example of a legacy device whose fields and conventions do not follow the standard wink device semantics. + +#### Device Model + +|API field|Attributes|Description| +|----|----|----| +|dials|array of Dials|Array of 4 dial objects representing the dials on the Nimbus face| +|alarms|array of Alarms|0 to many alarms, sent to and stored on Nimbus firmware| + +#### Dial Template + +Returns the available channel_configurations and dial_configurations for the dial resource + +Explanation of dial_configuration fields and values + +|API Field|Description| +|----|----| +|scale_type|log, linear [How the dial should move in response to higher values| +|rotation|cw, ccw [In which direction the dial should rotate]| +|min_value|any number [The minimum data value the dial should attempt to display at min_position]| +|max_value|any number greater than min_value [The maximum data value the dial should attempt to display at max_position]| +|min_position|degree rotation which corresponds to min_value. Generally [0, 360] but not required to be so. [The position of the needle at min_value]| +|max_postition|degree rotation which corresponds to max_value. Generally [0, 360] but not required to be so. [The position of the needle at max_value]| + +Read types available for each dial_template channel configuration: + +|Read Type|Values| +|----|----| +|Time|n/a| +|Weather|temperature, weather_conditions| +|Traffic|travel_time, travel_conditions| +|Calendar|time_until, time_of [refers to next appointment on calendar, currently only Google Calendar is supported]| +|Email|unread_message_count [currently only Gmail is supported]| +|Facebook|friend_request_count, latest_comment_count, latest_like_count, unread_message_count, unread_notification_count| +|Twitter|latest_retweet_count, recent_mention_count, recent_direct_message_count| +|Instagram|latest_like_count, latest_comment_count| +|Fitbit|calorie_out_count, heart_rate, sleep_duration, step_count| +|Eggminder|inventory| +|Porkfolio|balance| + +Other field values seen in dial_templates + +|API Field|Description| +|----|----| +|timezone|any IANA timezone +|locale:|A standard locale string of the ll_cc format, where ll is the two letter ISO language code and cc is the two letter ISO country code| +|lat_lng|tuple of (lat, lng) for the Weather channel| +|location|location string (New York, NY) for display for the Weather channel| +|start_lat_lng|tuple of (lat, lng) for the Traffic channel| +|start_location|location string (New York, NY) for display for the Traffic channel| +|stop_lat_lng|tuple of (lat, lng) for the Traffic channel| +|stop_location|location string (New York, NY) for display for the Traffic channel| +|transit_mode|one of ["car", "ped", "bike", "transit"] representing desired principal mode of transit for the Traffic channel| + + + { + "dial_template_id": "4", + "dial_configuration": { + "min_value": 0, + "max_value": 3600, + "min_position": 0, + "max_position": 360, + "scale_type": "linear", + "rotation": "cw" + }, + "channel_configuration": { + "channel_id": "4", + "reading_type": "time_until", + }, + "name": "Calendar" + } + +#### Dial model + +Each cloud_clock resources have 4 dial resources. Use dial_template to retrieve possible values for channel_configuration and dial_configuration + +**Atttributes** + +|API field|Attributes|Description| +|----|----|----| +|dial_id|(string, assigned, immutable)|API Id| +|dial_index|(integer, assigned, mutable)|Index of dial on Nimbus object| +|name|(string, mutable with write_data permissions)|Clients don't expose naming ability and name will normally map to channel configuration| +|label|(string, assigned, mutable)|deprecated, use labels +|labels|(array, assigned, immutable)|values determined by channel type and value, for display on clock LCD)| +|position|(float, assigned, immutable)|[0.0 - 359.0, position of needle on display]| +|brightness|(integer, assigned, mutable)|[0 - 100, display brightness of LCD, can also be updated on clock by pressing down]| +|channel_configuration|(object)|(see dial_templates for possible values) +|dial_configuration|(object)|(see dial_templates for possible values) + + { + "dial_id": "adsfljk_458", + "dial_index": 2, + "name": "Instagram", + "label": "INSTAGRAM", + "labels": ["INSTAGRAM", "1 LIKE"], + "position": 180.0, + "brightness": 25, + "channel_configuration": { + "channel_id": "4323", + "linked_service_ids": ["125"], + "linked_service_types": ["instagram.read_messages"] + "reading_type": "latest_comment_count", + "locale": "en_us", + }, + "dial_configuration": { + "scale_type": "linear", + "rotation": "cw", + "min_position": 0.0, + "min_value": 0.0, + "max_position": 0.0, + "max_value": 0.0, + "num_ticks": 0 + } + } + +Dials are be updated through the cloud_clock parent object + ++ Model (application/json) + + JSON representation of a device + + + Body + + { + "cloud_clock_id": "fasinfhs_12670s", + "name": "My Nimbus", + "dials": [ + { + "dial_id": "456", + "dial_index": 0, + "name": "Facebook", + "label": "FACEBOOK", + "labels": ["FACEBOOK", "1 REQUEST"], + "position": 90.0, + "brightness": 25, + "channel_configuration": { + "channel_id": "6", + "linked_service_ids": ["123"], + "linked_service_types": ["facebook.read_messages"], + "reading_type":"friend_request_count", + "locale": "en_us" + }, + "dial_configuration": {} + }, + { + "dial_id": "457", + "dial_index": 1, + "name": "Twitter", + "label": "TWITTER", + "labels": ["TWITTER", "1 TWEET"], + "position": 270.0, + "brightness": 25, + "channel_configuration": { + "channel_id": "4322", + "linked_service_ids": ["124"], + "linked_service_types": ["twitter.read_messages"], + "reading_type": "latest_retweet_count", + "locale": "en_us", + }, + "dial_configuration": {} + }, + { + "dial_id": "458", + "dial_index": 2, + "labels": ["638.2 HRS", "TO DEST"], + "name": "Instagram", + "label": "INSTAGRAM", + "position": 180.0, + "brightness": 25, + "channel_configuration": { + "channel_id": "4323", + "linked_service_ids": ["125"], + "linked_service_types": ["instagram.read_messages"] + "reading_type": "latest_comment_count", + "locale": "en_us", + }, + "dial_configuration": {} + }, + { + "dial_id": "459", + "dial_index": 3, + "name": "Weather", + "label": "WEATHER", + "labels": ["FLURRIES", "TEMP 32"], + "position": 0.0, + "brightness": 25, + "channel_configuration": { + "lat_lng": [40.7517836, -74.0050807], + "reading_type": "weather_conditions", + "locale": "en_us", + "channel_id": "4324" + }, + "dial_configuration": {} + } + ], + "alarms": [ + { + "alarm_id": "555", + "name": "Wakie wakie", + "recurrence": "DTSTART:20130821T140000ZnRRULE:FREQ=DAILY", + "media_id": "666", + "enabled": true, + "next_at": 123456789.0 + } + ] + } + +### List nimbi [GET] + ++ Response 200 (application/json) + + [Nimbus][] + +## Nimbus Alarm [/cloud_clocks/{cloud_clock_id}/alarms] + +The alarm resource has the following attributes: + +|API field|Attributes|Description| +|----|----|----| +|alarm_id|(string, assigned, immutable)|api id +|cloud_clock_id|(string, assigned, immutable)|[id of associated cloud_clock]| +|name|(string, mutable with write_data permissions)|user defined name| +|recurrence|(string, mutable with write_data permission)|[Recurrence string in iCalendar format]| +|enabled|(boolean, mutable with write_data)|if the alarm is currently enabled| +|next_at|(float, assigned, immutable)| [time stamp of next alarm]| + ++ Model + + { + "alarm_id": "fadlkfh_124_hasd", + "cloud_clock_id": "fasinfhs_12670s", + "name": "Wakie wakie", + "recurrence": "DTSTART;TZID=America/New_York:20130826T073000nRRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR", + "enabled": true, + "next_at": 123456789.0 + } + +### List alarms of nimbus [GET] + ++ Response 200 (application/json) + + [Nimbus Alarm][] + +### Create an alarm [POST] + ++ Response 200 (application/json) + + [Nimbus Alarm][] + +## Alarm [/alarms/{alarm_id}] + +### Edit an alarm [PUT] + ++ Response 200 (application/json) + + [Nimbus Alarm][] + +### Delete an alarm [DELETE] + ++ Response 204 + +## Power Strip [/power_strips/{device_id}] + +As a legacy device, the power strip is a bit unique. The device itself has no desired state or last reading, instead, it has an array of two outlet objects that have individual states. + +### Get Power Strip [GET] + +#### Power strip Model + +|API Fields|Attributes|Description| +|----|----|----| +|outlets|array of two outlets|the two outlets of the powerstrip| + +#### Power strip Last Reading + +|API Fields|Attributes|Description| +|----|----|----| +|connection|boolean|whether or not the powerstrip is connected| + +#### Outlet Model + +|API Fields|Attributes|Description| +|----|----|----| +|outlet_index|(numeric, assigned, immutable)|Order of outlet on power strip| + +#### Outlet Desired State + +|API Fields|Attributes|Description| +|----|----|----| +|powered|boolean|whether or not the outlet is on| + +## Piggy Bank [/piggy_bank/{device_id}] + +As a legacy device, the power strip is a bit unique. The deposits are a separate call outlined below. In addition, the color of the nose is not in the desired_state but on the object itself. + +### Get Piggy Bank [GET] + +#### Device Model + +|API field|Attributes|Description| +|----|----|----| +|color|String|hex string of porkfolio nose color| + +#### Last Reading Attributes + +|API field|Attributes|Description| +|----|----|----| +|connection|Boolean|whether or not the device is reachable remotely| +|battery|float|0 - 1 battery percentage| +|vibration|boolean|becomes true when accelerometer is triggered on movement| + +### Deposits [/piggy_banks/{piggy_bank_id}/deposits?since={timestamp}] + +#### Deposit Model + +|API field|Attributes|Description| +|----|----|----| +|deposit_id|string|API id| +|created_at|timestamp|when deposit was created| +|amount|integer|deposit amount in cents| + +Value can be negative for a withdrawal. + +### Get all deposits for Piggy Bank [GET] + +### Create a deposit or withdrawal [POST] + ++ Request + + { + "amount":10 + } + +## Refrigerators [/refrigerators/{device_id}] + +### Get Refrigerator [GET] + +#### Desired State Attributes + +|API field|Attributes|Description| +|----|----|----| +|refrigerator_set_point|float|set point for refrigerator in celsius| +|freezer_set_point|float|set point for freezer in celsius| +|refrigerator_ice_maker_enabled|boolean|whether or not the ice maker for the refrigerator is enabled| +|freezer_ice_maker_enabled|boolean|whether or not the ice maker for the freezer is enabled| +|sabbath_mode_enabled|boolean|whether or not sabbath mode is enabled| + +#### Last Reading Attributes + +|API field|Attributes|Description| +|----|----|----| +|min_refrigerator_set_point_allowed|float|minimum allowed set point in celsius| +|max_refrigerator_set_point_allowed|float|maximum allowed set point in celsius| +|min_freezer_set_point_allowed|float|minimum allowed set point in celsius| +|max_freezer_set_point_allowed|float|maximum allowed set point in celsius| +|refrigerator_left_door_ajar|boolean|whether the left refrigerator door is currently ajar| +|refrigerator_right_door_ajar|boolean|whether the right refrigerator door is currently ajar| +|refrigerator_door_ajar|boolean|whether either refrigerator door is currently ajar| +|freezer_door_ajar|boolean|whether the freezer door is currently ajar| +|water_filter_remaining|float|[0 - 1] percentage of water filter remaining| +|firmware_version|string|current firmware version of refrigerator unit| +|update_needed|boolean|whether refrigerator unit needs an update| +|updating_firmware|boolean|whether refrigerator unit is currently updating| +|symbiote_firmware_version|string|current firmware version of wifi module| +|symbiote_update_needed|boolean|whether wifi module needs an update| +|symbiote_updating_firmware|boolean|whether wifi module is currently updating| + +## Refuel [/propane_tanks/{device_id}] + +### Get Refuel [GET] + +#### Device Attributes + +|API field|Attributes|Description| +|----|----|----| +|tare|(float)|weight of tank, as printed on can| +|tank_changed_at|timestamp|time a new tank was added|| + +#### Desired State Attributes +n/a + +#### Last Reading Attributes + +|API field|Attributes|Description| +|----|----|----| +|connection|Boolean|whether or not the device is reachable remotely| +|battery|float|0 - 1, battery percentage| +|remaining|float|0 - 1, percent fuel remaining| + +## Remote [/remotes/{device_id}] + +### Get Remote [GET] + +#### Desired State Attributes +n/a + +#### Last Reading Attribute +|API field|Attributes|Description| +|----|----|----| +|remote_pairable|boolean|wehter or not the remote is pairing with a device| +|group_id|string|Reference to the Group object linked to the remote| +|button_up_pressed|boolean|up button is pressed| +|button_down_pressed|boolean|down button is pressed| +|button_on_pressed|boolean|on button is pressed| +|button_off_pressed|boolean|off button is pressed| + +## Sensor [/sensor_pods/{device_id}] + +### Get Sensor [GET] + +#### Desired State Attributes +n/a + +#### Last Reading Attribute + +|API Field|Attributes|Description| +|----|----|----| +|battery|float|[0 - 1] percentage of battery| +|connection|boolean|whether or not the sensor has connection| +|brightness|boolean|whether or not the sensor currently detects a large delta in light| +|external_power|boolean|whether the sensor is running on AC power or battery| +|humditity|float|[0 - 1] percentage of measured of humidity| +|loudness|boolean|whether the sensor is currently detects a large delta in sound| +|temperature|float|current reported temperature in celsius| +|vibration|boolean|whether the sensor currently detects a large delta in vibration| +|motion|boolean|whether the sensor currently detects a large delta in motion| +|opened|boolean|whether the sensor detects an opened state| +|locked|boolean|whether the sensor detects a locked state| +|liquid_detected|boolean|whether the sensor detects moisture| +|occupied|boolean|whether or not the sensor has detected occupancy in the last 30 minutes| + +## Sirens [/sirens/{device_id}] + +### Get Siren [GET] + +#### Desired State Fields + +|API Field|Attributes|Description| +|----|----|----| +|mode|String|one of [siren_only, strobe_only, siren_and_strobe]| +|powered|boolean|whether or not the siren is on| +|auto_shutoff|Integer|one of [null (never), 30, 60, 120]. Values are in seconds.| + +#### Last Reading Attribute + +|API Field|Attributes|Description| +|----|----|----| +|battery|float|[0 - 1] percentage of battery| +|connection|boolean|whether or not the sensor has connection| + +## Smoke Alarm [/smoke_detector/{device_id}] + +### Get Smoke Alarm [GET] + +#### Desired State Fields +n/a + +#### Last Reading Fields + +|API Field|Attributes|Description| +|----|----|----| +|smoke_detected|boolean|whether or not smoke is currently detected| +|co_detected|boolean|whether or not carbon monoxide is currently detected| +|test_activated|whether or not a test is currently activated| +|connection|boolean|current connection status| +|battery|float|[0 - 1] battery percentage| +|smoke_severity|float|[0 - 1] if present, severity of smoke detection| +|co_severity|float|[0 - 1] if present, severity of co detection| + +## Sprinklers [/sprinklers/{device_id}] + +### Get Sprinklers [GET] + +The sprinkler model has an array of zones. The zone objects themselves just display the state of the zones. Control of the zones is on the the desired_state of the sprinkler object with the run_zones and run_zones_durations. + +#### Sprinkler Model + +|API Field|Attribute|Description| +|----|----|----| +|zones|array of zones|Zones of sprinkler, which can display the state of the given nozzles| + +#### Desired State Fields + +|API Field|Attribute|Description| +|----|----|----| +|run_zones|array of integers|indices of zones to run| +|run_zones_durations|array of integer|duration in seconds of each of the run_zones| + + +#### Zone Model + +|API Field|Attribute|Description| +|----|----|----| +|zone_index|integer|index of zone on sprinkler system| +|enabled|boolean|whether or not the zone is hooked up| + +#### Zone Desired State + +|API Field|Attribute|Description| +|----|----|----| +|shade|String|Shade state of vegetation in zone, one of ["none", "moderate", "mostly"]| +|nozzle|String|Nozzle type of zone, one of ["fixed_spray_head","drip", "manual_sprinkler", "rotary_head", "rotor_head"]| +|slope|String|Slope of vegetation in zone, one of ["bottom", "flat", "slope", "top"]| +|soil|String|Soil of vegetation in zone, one of ["clay", "sand", "silt", "top_soil"]| +|vegetation|String|Type of vegetation in zone, one of ["annuals","garden", "grass","perennials", "shrubs", "trees", "xeric", "xeriscape" ]| + +#### Zone Last Reading + +|API Field|Attribute|Description| +|----|----|----| +|powered|boolean|whether or not the zone is currently on, note this cannot be controlled on the zone level. It can only be set as an object in run_zones| + +## Thermostats [/thermostats/{device_id}] + +### Get Thermostat [GET] + +#### Desired State Fields + +|API Field|Attribute|Description| +|----|----|----| +|mode|String|One of ["cool_only", "heat_only", "auto", "aux"], allowed value depends in `mode.choices` in capabilities| +|powered|boolean|whether or not the hvac is on| +|max_set_point|float, in celsius|max set point for cooling in celsius| +|min_set_point|float, in celsius|min set point for heating in celsius| +|override_temperature|float, in celsius|temperature sent to thermostat to override interally read temperature| +|setpoint_increment_value|integer, in tenths of celsius|value that the thermostat will change on tapping| +|accelerometer_enable|boolean|whether or not temperature change on tapping is enabled| +|temperature_override_enable|boolean|whether or not overriding the temperature is enabled| +|fan_duration|integer|Set fan on for duration in seconds| +|users_away|boolean|Set users away -- thermostat will manage temperature at a lower set point| +|cooling_system_stage|String|one of "cool_stage_1", "cool_stage_2"| +|heating_system_stage|String|one of "heat_stage_1", "heat_stage_2"| +|heating_system_type|String|one of "conventional", "heat_pump"| +|heating_fuel_source|String|one of "electric", "gas"| +|humidifier_mode|String|one of "on", "off", "auto"| +|humidifier_set_point|float|[0.0 - 1.0]| +|fan_mode|String|one of "[on,auto]", auto will turn the fan on when heating or cooling is active| +|dehumidifier_mode|string|one of “[on,off]”| +|dehumidifier_set_point|float|the humidity degree in which the thermostat will start to dehumidify| +|dehumidify_overcool_offset|float|cool in x F below cool setpoint in order to reach the dehumidification setpoint, capabilities will express an array of choices from 0 to the equivalent of 5 degrees F in steps of 0.5 degrees F, converted to C| +|profile|string| one of “[home,away,sleep,awake,null]” depends on capabilities] +|fan_run_time|int|minimum amount of time to circulate air per hour when fan is on AUTO mode, 0-3300 seconds, increments of 300 seconds| + +#### Last Reading Fields + +|API Field|Attribute|Description| +|----|----|----| +|connection|boolean|whether or not the device is reachable remotely| +|temperature|float in celsius|[maps to room temperature last read from device itself]| +|smart_temperature|ecobee only, mean temp of all remote sensors and thermostat| +|humidity|float|[0-1] from device readings| +|external_temperature|float in celsius|the outdoor temperature/weather| +|max_max_set_point|float in celsius|highest allowed max set point| +|min_max_set_point|float in celsius|lowest allowed max set point| +|max_min_set_point|float in celsius|highest allowed min set point| +|min_min_set_point|float in celsius|lowest allowed min set point| +|has_fan|boolean|whether or not the thermostat unit has a fan| +|fan_timer_active|boolean|whether or not the fan timer is active| +|eco_target|boolean|whether or not the thermostat is running in an energy efficient mode| +|override_temperature_group_id|string|group id of group used to calculate temperature override| +|deadband|float in celsius|minimum difference between max and min set points| +|technician_name|String|contractor contact data| +|technician_phone|String|contractor contact data| +|aux_active|boolean|Auxiliary heat is actively pumping| +|cool_active|boolean|Cool is actively pumping| +|heat_active|boolean|Heat is actively pumping| +|fan_active|boolean|Fan is actively running| +|last_error|string|the current alert/warning on the thermostat| +|occupied|boolean|Whether or not the thermostat has detected occupancy in the last 30 minutes| + +## Water Heaters [/water_heaters/{device_id}] + +### Get Water Heater [GET] + +#### Desired State Attributes + +|API Field|Attribute|Description| +|----|----|----| +|mode|String|one of "eco", "performance", "heat_pump", "high_demand", "electric_only", "gas"| +|powered|boolean|whether or not the water heater is on| +|set_point|float|set point in celsius| +|vacation_mode|boolean|whether vacation mode is ucrrently enabled| + +#### Last Reading Attributes + +|API Field|Attribute|Description| +|---|---|---| +|min_set_point_allowed|float|minimum set point allowed in celsius| +|max_set_point_allowed|float|maximum set point allowed in celsius| +|modes_allowed|String array|one or many of modes, depends on rheem type| +|scald_message|String|Populated if the set point is above 120F| +|rheem_type|(String)|one of "Electric Water Heater", "Heat Pump Water Heater", "Gas Water Heater"| + +# Group Member + +Members are used throughout the API for group hetereogenous devices in scenes, groups and robots. There are no specific endpoints for members and their creation and updating will happen within their respective parents + +## Member model + +Members have the following attributes + +|API field|Attributes|Description| +|----|----|----| +|object_id|(string, assignable)|API id| +|object_type|(string, assignable)|value will be singular types of wink devices and objects. i.e. air_conditioner, propane_tank, outlet, light_bulb, etc.| +|desired_state|(object)|current or requested desired_state of object| + +# Group Group + +**Resources for creating and controlling groups of devices** + +The group resource is a representation of a group of wink devices which may be controlled simultaneously. In addition, the group will have an aggregated reading to get the aggregate state of the grouped devices. + +The Wink API defines certain special groups which you cannot fully control. These include, but are not limited to: + + - System categories such as `.all` and `.sensors`, which will include respectively every product and every product which is contains environment sensors of any kind. You cannot create, delete, or rename system categories. You cannot add or remove objects from system categories. System categories have an `automation_mode` flag of `system_category`. + - User categories such as `@door_sensors` and `@power`. Some devices will appear in these categories by default, based on our best guess of how these devices will be used by most consumers. You cannot create, delete, or rename user categories. You can, however, add and remove objects, if our default classifications are not appropriate. User categories have an an `automation_mode` flag of `user_category`. + + +The group resource has the following attributes: + +|API field|Attributes|Description| +|---|---|---| +|name|(string, mutable)|User defined name. *NOTE* system_category and user_category names are immutable. In addition, an error will be thrown if a user tries to name their group the same name as a category group| +|members|(0 to many objects, assignable)|[Member object](/General/API/Member)| +|automation_mode|(string, assigned, immutable)|system_category or user_category, also indicated by a prefix of `.` or `@`| +|reading_aggregation|(object)|aggregated last_reading of group devices| +|desired_state|(object)|desired state PUT to change all members simultaneously| + + +Optional member attributes: +|blacklisted_readings|(array of strings, assignable)|Once set, this member will not contribute to the group aggregation of blacklisted readings| + +#### Reading Aggregation Model + +Reading aggregation differ from last_reading in that the fields identify a count of state or an average/max/min + +**Boolean Aggregation** + +A boolean aggregation has the following fields + +|API field|Attributes|Description| +|----|----|----| +|updated_at|(timestamp|Last time aggregation was updated| +|or|(boolean)|Whether the aggregation is driven by OR logic| +|and|(boolean)|Whether the aggregation is driven by AND logic| +|true_count|(integer)|How many members in the group are registering the given boolean field as true| +|false_count|(integer)|How many members in the group are registering the given boolean field as false| + +**Numeric Aggregation** + +A numeric aggregation has the following fields + +|API field|Attributes|Description| +|----|----|----| +|updated_at|(timestamp|Last time aggregation was updated| +|min|(float)|Lowest reading of members| +|max|(float)|Highest reading of members| +|average|(float)|Average reading of all members, currently unweighted| + +## Groups [/users/me/groups] + ++ Model (application/json) + + JSON representation of a shared group + + + Body + + { + "object_id": "agh1ity-876f00", + "object_type": "group", + "name": "Front windows", + "members": [ + { + "object_id": "adsjfhasdof", + "object_type": "light_bulb" + "desired_state": { + "powered":true + }, + "blacklisted_readings": ['brightness'] + }, + { + "object_id": "adsjfhasdof", + "object_type": "air_conditioner", + "desired_state": { + "powered":true + }, + "blacklisted_readings": [] + }], + "desired_state":{}, + "reading_aggregation": { + "powered": { + "updated_at":1234567890, + "or":false, + "and":true, + "true_count":2, + "false_count":0 + } + } + } + +### Get all groups [GET] + ++ Response 200 (application/json) + + [Groups][] + +### Create a group [POST] + ++ Request + + { + "name": "Front windows", + "members": [ + { + "object_id": "adsjfhasdof", + "object_type": "light_bulb" + "desired_state": { + "powered":true + }, + "blacklisted_readings": ['brightness'] + }, + { + "object_id": "adsjfhasdof", + "object_type": "air_conditioner", + "desired_state": { + "powered":true + }, + "blacklisted_readings": [] + }], + } + ++ Response 200 (application/json) + + [Groups][] + +## Group [/groups/{group_id}/] + +### Retrieve a group [GET] + ++ Response 200 (application/json) + + [Groups][] + +### Update group settings [PUT] + ++ Request + + { + "name": "Front windows", + } + ++ Response 200 (application/json) + + [Groups][] + +### Delete a group [DELETE] + ++ Response 204 + +## Set state of group [/groups/{group_id}/activate] + +When you post up a desired state object, the API will then change all the devices in the group to that desired state. Allowed values for desired_state are dependent on the devices in the group and you should refer to individual device documentation. + +If you have multiple types of devices in a group and a field in the desired_state object only applies to some of them, such as `color` for `light_bulb` types, the API will update the appropriate devices and ignore that state for devices that do not have a color state, such as air_conditioners + +### Set state [POST] + ++ Body + { + "desired_state": { + "powered":true + } + } + ++ Response 200 (application/json) + + [Groups][] + +# Group Scene + +A scene is a collection of desired states for any supported Wink device (such as an air conditioner or garage door) or any Wink object (such as the outlet on a powerstrip). The scene members can be heterogenous and the member objects are composed as defined above in member. +A scene member must have a valid desired state for the object type in order to be valid. + +## Scene Model + +|API field|Attributes|Description| +|----|----|----| +|name|(string, writable)|User defined name| +|members|array of members|See Member above| + +## Scenes [/users/me/scenes] + ++ Model (application/json) + + JSON representation of a shared group + + + Body + + { + "scene_id": "qs1ga9_1234deadbeef", + "name": "Coming home", + "members": [ + { + "object_id":"afdjlafd", + "object_type:"light_bulb", + "desired_state": { + "powered": true + } + }, + { + "object_id":"yasdfkha", + "object_type:"garage_door", + "desired_state": { + "position": 1.0 + } + } + ] + } +### Get all scenes [GET] + ++ Response 200 (application/json) + + [Scenes][] + +### Create a scene [POST] + ++ Request + + { + "name": "Coming home", + "members": [ + { + "object_id":"afdjlafd", + "object_type:"light_bulb", + "desired_state": { + "powered": true + } + }, + { + "object_id":"yasdfkha", + "object_type:"garage_door", + "desired_state": { + "position": 1.0 + } + } + ] + } + ++ Response 200 (application/json) + + [Scenes][] + +## Scene [/scenes/{scene_id}/] + +### Retrieve a scene [GET] + ++ Response 200 (application/json) + + [Scenes][] + +### Update scene settings [PUT] + ++ Request + + { + "name": "Coming home", + } + ++ Response 200 (application/json) + + [Scenes][] + +### Delete a scene [DELETE] + ++ Response 204 + +## Set state of scene [/scenes/{scene_id}/activate] + +In order to activate the scene, POST to the endpoint. No body is necessary. + +### Set state [POST] + ++ Response 200 (application/json) + + [Scenes][] + + +# Group Robot + +A Robot is the API object that allows for automations based on external triggers such as time or another device's state + +## Robot Model + +|API field|Attributes|Description| +|----|----|----| +|name|(string, writable)|User defined name| +|creating_actor_type|(string, assigned)|type of entity that created the robot, can be user or device in case of smart features| +|creating_actor_id|(string, assigned)|id of entity that created the robot| +|automation_mode|(string, writable)|mode of robot if generated for smart features, current possible values -- null (not smart), "smart_schedule", "smart_away_arriving", "smart_away_departing"; client writable values "notification" (fridge note), "tapt" (created implicitly through relay or tapt interface)| +|causes|(array, 1 to many Conditions)| Cause(s) that will trigger the robot| +|restrictions|(0 to many Conditions)| Restriction(s) that would prevent the robot from triggering on the given causes. **NOTE** while documented, currently this is not exposed on the clients| +|effects|(array, 1 to many|Effect of robot when triggered| + +### Desired State Fields +|API field|Attributes|Description| +|----|----|----| +|enabled|(boolean, writable)|Whether or not the robot is currently enabled| +|fired_limit|(integer, writable)|How many times the robot can fire. A value of null or 0 means the robot can fire as many times as possible. Currently used by Refrigerator note robots| + +### Last Reading Fields +|API field|Attributes|Description| +|----|----|----| +|fired|(boolean)|Whether the robot has fired| + +## Condition Model + +The causes array is an array of Conditions that can trigger a robot. The restrictions array is an array of Conditions that can prevent a robot from being fired, even if it is triggered by a cause. + +A condition has the following format: + +|API field|Attributs|Description| +|----|----|----| +|condition_id|(string, assigned, immutable)|id of condition| +|observed_field|(string, writable)|field in last reading| +|observed_object_id|(string, writable)|id of object being observed| +|observed_object_type|(string, writable)|type of object being observed, ex. "garage_door"| +|operator|(string, writable)|comparison operator| +|value|(string, writable)|desired value of observed_field| +|recurrence|(text, writeable)|iCal string| +|restriction_join|(string, writable)|"and" or "or"| +|robot_id|(integer, writable)|id of robot| +|restricted_object_id|(integer, writable)|if this condition is restricting something, the id of what is being restricted| +|restricted_object_type|(string, writable)|if this condition is restricting something, the type of what is being restricted, currently "robot" or "condition"| +|restrictions|(array of 0 to many)|embedded condition objects to restrict parent condition| + +For causes and restrictions, their truthiness is evaluated in the following order: + +- `recurrence && observed_field && restrictions.` + +### Condition recurrence + +Recurrence should be a string in iCal format and is to be used for a condition dependent on a time. If the recurrence is nil, it's evaluation is true. Thus, to omit a time restriction, set the recurrence string to null. If the recurrence is meant to restrict time for firing, it should have a start and end. See below for examples. + ++ It is 5 pm on a Tuesday + + { + "recurrence": "DTSTART;TZID=PDT:20140313T170000nRRULE:FREQ=DAILY" + } + ++ It is between 8pm and 10pm on Tuesdays + + { + "recurrence": "DTSTART;TZID=PDT:20140313T200000nDTEND;TZID=PDT:20140313T220000nRRULE:FREQ=DAILY" + } + ++ Every day at sunrise in my current location + + { + "recurrence": "DTSTART;TZID=PDT:20140313T080000nX-WINK-STSTART:sunrise;37.47;-122.25nDTEND;TZID=PDT:20140314T080000nRRULE:FREQ=DAILY" + } + ++ Every day at sunset in my current location + + { + "recurrence": "DTSTART;TZID=PDT:20140313T200000nX-WINK-STSTART:sunset;37.47;-122.25nDTEND;TZID=PDT:20140314T200000nRRULE:FREQ=DAILY" + } + ++ Every day between sunrise and sunset in my current location + + { + "recurrence": "DTSTART;TZID=PDT:20140313T200000nX-WINK-STSTART:sunrise;37.47;-122.25nDTEND;TZID=PDT:20140314T210000nRX-WINK-STEND:sunset;37.47;-122.25nRULE:FREQ=DAILY" + } + +### Condition observed fields + +The following fields are all required to properly evaluate observed_field + +- observed_field +- operator +- value + +observed_field can be any field in a device's last_reading object + +operator can be only of the following, as strings + +- == +- != +- > +- < +- >= +- <= + +value is the value that will be used in comparison with the operator. + +**NOTE:** although you can compare multiple types of values, such as boolean, string, int, or float, this value should be put and read as a string. See examples below. + +In order to evaluate to true, the observed field's value must be evaluated to true with the given operator + ++ Example of a condition: a garage door is opened by at least 50% + + { + "observed_object_id":"xyzasdfadsfhkj", + "observed_object_type":"garage_door", + "observed_field": "position", + "operator": ">=", + "value": "0.5" + } + ++ Example of a condition: geo fence is entered + + { + "observed_object_id":"defasdfkjha", + "observed_object_type":"geofence", + "observed_field": "within", + "operator": "==", + "value": "true" + } + ++ Example of a condition: geo fence is exited + + { + "observed_object_id":"defasdfkjha", + "observed_object_type":"geofence", + "observed_field": "within", + "operator": "==", + "value": "false" + } + + +#### Restriction Evaluation + +Each condition can have embedded restrictions to create complex logic. + +By default, each restriction in the array is joined by "and" so that all the restrictions in the array must evaluate to true in order for restrictions to evaluate to true. + +You can join restrictions by "or" by add a + +- restriction_join [allowed values "and" "or"] + +**NOTE ABOUT COMPLEXITY** + +Because each restriction is of the same class as the top level Condition, each embedded restriction goes through the same evaluation process for truthiness and can similarly have embedded restrictions of its own. + +The restrictions defined in the top level "restrictions" array of the robot object are joined by "and". From there, nested restrictions are joined based on the "restriction_join" field of the parent restriction. + +The allowed causes are either objects with an observed reading (such as a garage door opening or a geofence being triggered) or they are a time as defined by an iCal recurrence string. + +EXAMPLES: + +Note: each example could be in the causes or restrictions array on the robot object, causes are conditions that can trigger a robot and restrictions are conditions that can prevent a robot from being triggered. + + ++ The garage door is closed and I am within my home geofence + + { + "restrictions": [ + { + "observed_object_id":"xyzasdfadsfhkj", + "observed_object_type":"garage_door", + "observed_field": "position", + "operator": "==", + "value": "0.0" + }, + { + "observed_object_id":"defasdfkjha", + "observed_object_type":"geofence", + "observed_field": "within", + "operator": "==", + "value": "true" + } + ] + } + ++ (the garage door is closed and I am within my home geofence) or it is between 8pm and 10pm on Tuesdays + + { + "restriction_join": "or" + "restrictions": [ + { + "restrictions": [ + { + "observed_object_id":"xyzasdfadsfhkj", + "observed_object_type":"garage_door", + "observed_field": "position", + "operator": "==", + "value": "0.0" + }, + { + "observed_object_id":"defasdfkjha", + "observed_object_type":"geofence", + "observed_field": "within", + "operator": "==", + "value": "true" + } + ] + }, + { + "recurrence": "DTSTART;TZID=PDT:20140313T200000DTEND;TZID=PDT:20140313T220000nRRULE:FREQ=DAILY" + } + ] + } + ++ The garage door is closed and (I am within my home geofence or it is between 8pm and 10pm on Tuesdays) + + { + "restrictions": [ + { + "observed_object_id":"xyzasdfadsfhkj", + "observed_object_type":"garage_door", + "observed_field": "position", + "operator": "==", + "value": "0.0" + }, + { + "join_type": "or", + "restrictions": [ + + { + "observed_object_id":"defasdfkjha", + "observed_object_type":"geofence", + "observed_field": "within", + "operator": "==", + "value": "true" + }, + { + "recurrence": "DTSTART;TZID=PDT:20140313T200000DTEND;TZID=PDT:20140313T220000nRRULE:FREQ=DAILY" + } + ] + }, + + ] + } + ++ The garage door is closed and it is between 8pm and 10pm on Tuesdays + +Can be written as two embedded restrictions + + { + "restrictions": [ + { + "observed_object_id":"xyzasdfadsfhkj", + "observed_object_type":"garage_door", + "observed_field": "position", + "operator": "==", + "value": "0.0" + }, + { + "recurrence": "DTSTART;TZID=PDT:20140313T200000DTEND;TZID=PDT:20140313T220000nRRULE:FREQ=DAILY" + } + + ] + } + +## Effect Model + +An effect is what happens if any of the causes occur outside of the optional restriction. + +The effect has the following attributes: + +|API field|Attributs|Description| +|----|----|----| +|scene|(refers to scene that would be activated)|If the effect affects devices, a scene will be created| +|recipient_actor_id |(String)|Refers to a user that would get a notification of some type. Scenes and recipient actors are currently mutually exclusive| +|recipient_actor_type|(String)|Refers to the type of actor, currently just "user". Scenes and recipient actors are currently mutually exclusive| +|notification_type|(String)|notification type to send "email" or "push"| +|note|(custom text object)|used in refrigerator notes| + +An effect can have a scene OR a recipient_actor_id and notification_type, but should not have both. + +If an effect has a note, it should also have a notification_type and recipient_actor. + +**SCENE IN EFFECT** + +The scene object is saved within the system and has a scene_id, but will come down as a full object for ease of use + +**USER and NOTIFICATION_TYPE** + +recipient_actor_id is the user_id of the user who will be receiving the notification + +recipient_actor_type should be "user" + +Allowed values for notification_type are "email" and "push" + +## Custom Text Model +The custom text resource allows users to associate arbitrary text with another resource. + +Currently a custom text is only able to be associated with an effect. + +The custom text has the following attributes: + +|API field|Attributes|Description| +|----|----|----| +|custom_text_id|(string, assigned, immutable)|API id| +|body|(string, writable, mutable)|String used as the message of the custom text| +|subject_id|(string, assigned, immutable)|Assigned, referencing effect that has the custom text| +|subject_type|(string, assigned, immutable)|Assigned, referencing effect that has the custom text| + + ++ Model + + { + "custom_text_id": "1", + "body": "Some custom text", + "subject_id": "34", + "subject_type": "effect" + } + +## Robots [/users/me/robots] + ++ Model (application/json) + + JSON representation of a shared group + + + Body + + { + "name": "Data", + "creating_actor_type": "user", + "creating_actor_id": "asdfljafd", + "automation_mode": null, + "causes" : [ + { + "condition_id": "qweryoiu", + "observed_object_id":"xyzasdfadsfhkj", + "observed_object_type":"garage_door", + "observed_field": "position", + "operator": "==", + "value": "0.0" + } + ], + "restrictions" : [ + { + "condition_id": "sadfdsaafsd", + "recurrence": "DTSTART;TZID=PDT:20140313T200000DTEND;TZID=PDT:20140313T220000nRRULE:FREQ=DAILY" + } + ], + "effects": [ + { + "scene": { + "scene_id": "asdaioytf", + "name":"Data Scene", + "members": [ + "object_id":"asdfoaiye", + "object_type":"light_bulb", + "desired_state": { + "powered":true, + "brightness": 0.75 + } + ] + }, + "note": { + "custom_text_id", + "body": "Some text", + "subject_id": "abc", + "subject_type": "effect" + } + }, + { + "recipient_actor_id": "adsfhkjasdfy", + "recipient_actor_type": "user", + "notification_type": "email + } + ], + "last_reading": { + "enabled":true, + "fired_limit":2 + } + } + +### Get all robots [GET] + ++ Response 200 (application/json) + + [Robots][] + +### Create a robot [POST] + ++ Request + + { + "name": "Data", + "fired_limit": 2, + "automation_mode": null, + "causes" : [ + { + "observed_object_id":"xyzasdfadsfhkj", + "observed_object_type":"garage_door", + "observed_field": "position", + "operator": "==", + "value": "0.0" + } + ], + "restrictions" : [ + { + "recurrence": "DTSTART;TZID=PDT:20140313T200000DTEND;TZID=PDT:20140313T220000nRRULE:FREQ=DAILY" + } + ], + "effects": [ + { + "scene": { + "name":"Data Scene", + "members": [ + "object_id":"asdfoaiye", + "object_type":"light_bulb", + "desired_state": { + "powered":true, + "brightness": 0.75 + } + ] + }, + "note": { + "custom_text_id", + "body": "Some text", + "subject_id": "abc", + "subject_type": "effect" + } + }, + { + "recipient_actor_id": "adsfhkjasdfy", + "recipient_actor_type": "user", + "notification_type": "email + } + ], + "desired_state": { + "enabled":true, + "fired_limit":2 + } + } + ++ Response 200 (application/json) + + [Robots][] + +## Robot [/robots/{robot_id}/] + +### Retrieve a robot [GET] + ++ Response 200 (application/json) + + [Robots][] + +### Update robot settings [PUT] + ++ Request + + { + "name": "Data", + } + ++ Response 200 (application/json) + + [Robots][] + +### Delete a robot [DELETE] + ++ Response 204 + +# Group User +Resources for Users + +## User Model + +|API field|Attributes|Description| +|----|----|----| +|email|(string, writable, mutable)|user's email| +|first_name|(string, writable, mutable)|user's first name| +|last_name| (string, writable, mutable)| user's last name| +|oauth2|(object, assigned, mutable)| oauth 2 object for authentication| +|locale|(string, writable, mutable)|ISO locale| +|tos_accepted|(boolean, assigned, mutable)|whether or not the current TOS has been accepted| +|confirmed|(boolean, assigned, mutable)|whether or not the user has confirmed their email| + +## Desired State Attributes + +|API field|Attributes|Description| +|----|----|----| +|units|object|display units for user| + +## User [/users] + +### Create user [POST] ++ Request (application/json) + + + Body + + { + "client_id": "...", + "client_secret": "...", + "email": "user@example.com", + "first_name": "User", + "last_name": "McUserson", + "locale": "en_us", + "new_password": "********" + } + ++ Response 201 (application/json) + + { + "user_id": "27412", + "first_name": "User", + "last_name": "McUserson", + "email": "user@example.com", + "oauth2": { + "access_token": "example_access_token_like_135fhn80w35hynainrsg0q824hyn", + "refresh_token": "...", + "token_type": "bearer", + "token_endpoint": "https://winkapi.quirky.com/oauth2/token" + }, + "locale": "en_us", + "units": {}, + "tos_accepted": false, + "confirmed": false + } + + +## User [/users/{user_id}] + ++ Model (application/json) + + JSON representation of an user + + + Body + + { + "data":{ + "user_id":"27105", + "first_name":"User", + "last_name":"McUserson", + "email":"user@example.com", + "oauth2":{ + "access_token":"55bb2ce8488d7ff9313be76668a43ea0", + "refresh_token":"d30d2dcf5f33411b7a225e9e63952d84", + "token_type":"bearer", + "token_endpoint":"http://localhost:3000/oauth2/token" + }, + "locale":"en_us", + "units":{ + }, + "tos_accepted":false + }, + "errors":[ + ], + "pagination":{ + } + } + ++ Parameters + - user_id (required, string, `21212`) ... String `user_id` of the user to perform action on. Has example value. + +### Update current user's profile [PUT] ++ Request (application/json) + + + Headers + + Authorization : Bearer example_access_token_like_135fhn80w35hynainrsg0q824hyn + + + Body + + { + "email": "user@example.com", + } + ++ Response 200 (application/json) + + { + "data": { + "user_id": "abc123def-an15nag", + "email": "user@example.com" + } + } + +## User password [/users/{user_id}/update_password] + +### update password [POST] ++ Request (application/json) + + + Headers + + Authorization : Bearer example_access_token_like_135fhn80w35hynainrsg0q824hyn + + + Body + + { + "old_password" : '123456' + "new_password" : '654321' + } + ++ Response 200 (application/json) + + {} diff --git a/src/pywink/test/init_test.py b/src/pywink/test/init_test.py index 496951c..4436909 100644 --- a/src/pywink/test/init_test.py +++ b/src/pywink/test/init_test.py @@ -1,21 +1,32 @@ import json import mock import unittest +import sys +import os from pywink.api import get_devices_from_response_dict, WinkApiInterface from pywink.devices import types as device_types -from pywink.devices.sensors import WinkSensorPod +from pywink.devices.sensors import WinkSensorPod, WinkBrightnessSensor, WinkHumiditySensor, \ + WinkSoundPresenceSensor, WinkVibrationPresenceSensor, WinkTemperatureSensor, \ + _WinkCapabilitySensor from pywink.devices.standard import WinkBulb, WinkGarageDoor, WinkPowerStripOutlet, WinkSiren, WinkLock, \ - WinkBinarySwitch, WinkEggTray + WinkBinarySwitch, WinkEggTray from pywink.devices.types import DEVICE_ID_KEYS -class LightSetStateTests(unittest.TestCase): - +class LightTests(unittest.TestCase): + def setUp(self): - super(LightSetStateTests, self).setUp() + super(LightTests, self).setUp() self.api_interface = WinkApiInterface() - + + def test_should_handle_light_bulb_response(self): + with open('{}/api_responses/light_bulb.json'.format(os.path.dirname(__file__))) as light_file: + response_dict = json.load(light_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.LIGHT_BULB]) + self.assertEqual(1, len(devices)) + self.assertIsInstance(devices[0], WinkBulb) + @mock.patch('requests.put') def test_should_send_correct_color_xy_values_to_wink_api(self, put_mock): bulb = WinkBulb({}, self.api_interface) @@ -45,771 +56,275 @@ def test_should_only_send_color_xy_if_both_color_xy_and_color_temperature_are_gi self.assertEquals('color_temperature', sent_data['desired_state'].get('color_model')) self.assertNotIn('color_x', sent_data['desired_state']) self.assertNotIn('color_y', sent_data['desired_state']) + + def test_device_id_should_be_number(self): + with open('{}/api_responses/light_bulb.json'.format(os.path.dirname(__file__))) as light_file: + response_dict = json.load(light_file) + light = response_dict.get('data')[0] + wink_light = WinkBulb(light, self.api_interface) + device_id = wink_light.device_id() + self.assertRegex(device_id, "^[0-9]{4,6}$") -class PowerStripStateTests(unittest.TestCase): +class PowerStripTests(unittest.TestCase): + + def setUp(self): + super(PowerStripTests, self).setUp() + self.api_interface = mock.MagicMock() + + def test_should_handle_power_strip_response(self): + with open('{}/api_responses/power_strip.json'.format(os.path.dirname(__file__))) as powerstrip_file: + response_dict = json.load(powerstrip_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.POWER_STRIP]) + self.assertEqual(2, len(devices)) + self.assertIsInstance(devices[0], WinkPowerStripOutlet) + self.assertIsInstance(devices[1], WinkPowerStripOutlet) def test_should_show_powered_state_as_false_if_device_is_disconnected(self): - response = """ - { - "data": [ - { - "desired_state": {}, - "last_reading": { - "connection": false, - "connection_updated_at": 1452306146.129263, - "connection_changed_at": 1452306144.425378 - }, - "powerstrip_id": "24123", - "name": "Power strip", - "locale": "en_us", - "units": {}, - "created_at": 1451578768, - "hidden_at": null, - "capabilities": {}, - "triggers": [], - "device_manufacturer": "quirky_ge", - "model_name": "Pivot Power Genius", - "upc_id": "24", - "upc_code": "814434017226", - "lat_lng": [ - 12.123456, - -98.765432 - ], - "location": "", - "mac_address": "0c2a69123456", - "serial": "AAAA00123456", - "outlets": [ - { - "powered": false, - "scheduled_outlet_states": [], - "name": "First", - "outlet_index": 0, - "outlet_id": "48123", - "icon_id": "4", - "parent_object_type": "powerstrip", - "parent_object_id": "24123", - "desired_state": { - "powered": false - }, - "last_reading": { - "powered": true, - "powered_updated_at": 1452306146.0882413, - "powered_changed_at": 1452306004.7519948, - "desired_powered_updated_at": 1452306008.2215497 - } - }, - { - "powered": false, - "scheduled_outlet_states": [], - "name": "Second", - "outlet_index": 1, - "outlet_id": "48124", - "icon_id": "4", - "parent_object_type": "powerstrip", - "parent_object_id": "24123", - "desired_state": { - "powered": false - }, - "last_reading": { - "powered": true, - "powered_updated_at": 1452311731.8861659, - "powered_changed_at": 1452311731.8861659, - "desired_powered_updated_at": 1452311885.3523679 - } - } - ] - } - ], - "errors": [], - "pagination": { - "count": 10 - } - } - """ - - response_dict = json.loads(response) + with open('{}/api_responses/power_strip.json'.format(os.path.dirname(__file__))) as powerstrip_file: + response_dict = json.load(powerstrip_file) devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.POWER_STRIP]) self.assertFalse(devices[0].state()) + def test_device_id_should_be_number(self): + with open('{}/api_responses/power_strip.json'.format(os.path.dirname(__file__))) as powerstrip_file: + response_dict = json.load(powerstrip_file) + power_strip = response_dict.get('data') + outlets = power_strip[0].get('outlets') + + for outlet in outlets: + wink_outlet = WinkPowerStripOutlet(outlet, self.api_interface) + device_id = wink_outlet.device_id() + self.assertRegex(device_id, "^[0-9]{4,6}$") + -class WinkAPIResponseHandlingTests(unittest.TestCase): +class GarageDoorTests(unittest.TestCase): def setUp(self): - super(WinkAPIResponseHandlingTests, self).setUp() + super(GarageDoorTests, self).setUp() self.api_interface = mock.MagicMock() - def test_should_handle_light_bulb_response(self): - response = """ - { - "data": [{ - "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 - }] - } - """ - response_dict = json.loads(response) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.LIGHT_BULB]) - self.assertEqual(1, len(devices)) - self.assertIsInstance(devices[0], WinkBulb) - def test_should_handle_garage_door_opener_response(self): - - response = """ - { - "data": [{ - "desired_state": { - "position": 0 - }, - "last_reading": { - "position_opened": "N\/A", - "position_opened_updated_at": 1450357467.371, - "tamper_detected_true": null, - "tamper_detected_true_updated_at": null, - "connection": true, - "connection_updated_at": 1450357538.2715, - "position": 0, - "position_updated_at": 1450357537.836, - "battery": null, - "battery_updated_at": null, - "fault": false, - "fault_updated_at": 1447976866.0784, - "disabled": null, - "disabled_updated_at": null, - "control_enabled": true, - "control_enabled_updated_at": 1447976866.0784, - "desired_position_updated_at": 1447976846.8869, - "connection_changed_at": 1444775470.5484, - "position_changed_at": 1450357537.836, - "control_enabled_changed_at": 1444775472.2474, - "fault_changed_at": 1444775472.2474, - "position_opened_changed_at": 1450357467.371, - "desired_position_changed_at": 1447976846.8869 - }, - "garage_door_id": "30528", - "name": "Garage Door", - "locale": "en_us", - "units": { - - }, - "created_at": 1444775470, - "hidden_at": null, - "capabilities": { - "home_security_device": true - }, - "triggers": [ - - ], - "manufacturer_device_model": "chamberlain_garage_door_opener", - "manufacturer_device_id": "1133930", - "device_manufacturer": "chamberlain", - "model_name": "MyQ Garage Door Controller", - "upc_id": "26", - "upc_code": "012381109302", - "hub_id": null, - "local_id": null, - "radio_type": null, - "linked_service_id": "206203", - "lat_lng": [ - 0, - 0 - ], - "location": "", - "order": null - }], - "errors": [], - "pagination": {} - } - """ - response_dict = json.loads(response) + with open('{}/api_responses/garage_door.json'.format(os.path.dirname(__file__))) as garage_door_file: + response_dict = json.load(garage_door_file) devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.GARAGE_DOOR]) self.assertEqual(1, len(devices)) self.assertIsInstance(devices[0], WinkGarageDoor) + + def test_device_id_should_be_number(self): + with open('{}/api_responses/garage_door.json'.format(os.path.dirname(__file__))) as garage_door_file: + response_dict = json.load(garage_door_file) + garage_door = response_dict.get('data')[0] + wink_garage_door = WinkGarageDoor(garage_door, self.api_interface) + device_id = wink_garage_door.device_id() + self.assertRegex(device_id, "^[0-9]{4,6}$") + + +class SirenTests(unittest.TestCase): - def test_should_handle_power_strip_response(self): - - response = """ - { - "errors": [ - - ], - "data": [{ - "powerstrip_id": "12345", - "model_name": "Pivot Power Genius", - "created_at": 1451578768, - "mac_address": "0c2a69000000", - "locale": "en_us", - "name": "Power strip", - "units": { - - }, - "last_reading": { - "connection": true, - "connection_changed_at": 1451947138.418391, - "connection_updated_at": 1452093346.488989 - }, - "triggers": [ - - ], - "location": "", - "capabilities": { - - }, - "hidden_at": null, - "outlets": [{ - "parent_object_type": "powerstrip", - "icon_id": "4", - "desired_state": { - "powered": false - }, - "parent_object_id": "24313", - "scheduled_outlet_states": [ - - ], - "name": "Outlet #1", - "outlet_index": 0, - "last_reading": { - "desired_powered_updated_at": 1452094688.1679382, - "powered_updated_at": 1452094688.1461067, - "powered": false, - "powered_changed_at": 1452094688.1461067 - }, - "powered": false, - "outlet_id": "48628" - }, { - "parent_object_type": "powerstrip", - "icon_id": "4", - "desired_state": { - "powered": false - }, - "parent_object_id": "24313", - "scheduled_outlet_states": [ - - ], - "name": "Outlet #2", - "outlet_index": 1, - "last_reading": { - "desired_powered_updated_at": 1452094689.7589157, - "powered_updated_at": 1452094689.443459, - "powered": false, - "powered_changed_at": 1452094689.443459 - }, - "powered": false, - "outlet_id": "48629" - }], - "serial": "AAAA00012345", - "lat_lng": [ - 0.000000, -0.000000 - ], - "desired_state": { - - }, - "device_manufacturer": "quirky_ge", - "upc_id": "24", - "upc_code": "814434017226" - }], - "pagination": { - - } - } - """ - response_dict = json.loads(response) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.POWER_STRIP]) - self.assertEqual(2, len(devices)) - self.assertIsInstance(devices[0], WinkPowerStripOutlet) - self.assertIsInstance(devices[1], WinkPowerStripOutlet) - + def setUp(self): + super(SirenTests, self).setUp() + self.api_interface = mock.MagicMock() def test_should_handle_siren_response(self): - - response = """ - { - "data":[ - { - "desired_state":{ - "auto_shutoff":30, - "mode":"siren_and_strobe", - "powered":false - }, - "last_reading":{ - "connection":true, - "connection_updated_at":1453249957.2466462, - "battery":1, - "battery_updated_at":1453249957.2466462, - "auto_shutoff":30, - "auto_shutoff_updated_at":1453249957.2466462, - "mode":"siren_and_strobe", - "mode_updated_at":1453249957.2466462, - "powered":false, - "powered_updated_at":1453249957.2466462, - "desired_auto_shutoff_updated_at":1452812848.5178623, - "desired_mode_updated_at":1452812848.5178623, - "desired_powered_updated_at":1452812668.1190264, - "connection_changed_at":1452812587.0312104, - "powered_changed_at":1452812668.0807295, - "battery_changed_at":1453032821.1796713, - "mode_changed_at":1452812589.8262901, - "auto_shutoff_changed_at":1452812589.8262901, - "desired_auto_shutoff_changed_at":1452812590.029748, - "desired_powered_changed_at":1452812668.1190264, - "desired_mode_changed_at":1452812848.5178623 - }, - "siren_id":"6123", - "name":"Alarm", - "locale":"en_us", - "units":{ - - }, - "created_at":1452812587, - "hidden_at":null, - "capabilities":{ - - }, - "device_manufacturer":"linear", - "model_name":"Wireless Siren & Strobe (Wireless)", - "upc_id":"243", - "upc_code":"wireless_linear_siren", - "hub_id":"30123", - "local_id":"8", - "radio_type":"zwave", - "lat_lng":[ - 12.1345678, - -98.765432 - ], - "location":"" - } - ], - "errors":[ - - ], - "pagination":{ - "count":17 - } - } - """ - - response_dict = json.loads(response) + with open('{}/api_responses/siren.json'.format(os.path.dirname(__file__))) as siren_file: + response_dict = json.load(siren_file) devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SIREN]) self.assertEqual(1, len(devices)) self.assertIsInstance(devices[0], WinkSiren) - - + + def test_device_id_should_be_number(self): + with open('{}/api_responses/siren.json'.format(os.path.dirname(__file__))) as siren_file: + response_dict = json.load(siren_file) + siren = response_dict.get('data')[0] + wink_siren = WinkSiren(siren, self.api_interface) + device_id = wink_siren.device_id() + self.assertRegex(device_id, "^[0-9]{4,6}$") + + +class LockTests(unittest.TestCase): + + def setUp(self): + super(LockTests, self).setUp() + self.api_interface = mock.MagicMock() + def test_should_handle_lock_response(self): - - response = """ - { - "data": [ - { - "desired_state": { - "locked": true, - "beeper_enabled": true, - "vacation_mode_enabled": false, - "auto_lock_enabled": false, - "key_code_length": 4, - "alarm_mode": null, - "alarm_sensitivity": 0.6, - "alarm_enabled": false - }, - "last_reading": { - "locked": true, - "locked_updated_at": 1417823487.490747, - "connection": true, - "connection_updated_at": 1417823487.490747, - "battery": 0.83, - "battery_updated_at": 1417823487.490747, - "alarm_activated": null, - "alarm_activated_updated_at": null, - "beeper_enabled": true, - "beeper_enabled_updated_at": 1417823487.490747, - "vacation_mode_enabled": false, - "vacation_mode_enabled_updated_at": 1417823487.490747, - "auto_lock_enabled": false, - "auto_lock_enabled_updated_at": 1417823487.490747, - "key_code_length": 4, - "key_code_length_updated_at": 1417823487.490747, - "alarm_mode": null, - "alarm_mode_updated_at": 1417823487.490747, - "alarm_sensitivity": 0.6, - "alarm_sensitivity_updated_at": 1417823487.490747, - "alarm_enabled": true, - "alarm_enabled_updated_at": 1417823487.490747, - "last_error": null, - "last_error_updated_at": 1417823487.490747, - "desired_locked_updated_at": 1417823487.490747, - "desired_beeper_enabled_updated_at": 1417823487.490747, - "desired_vacation_mode_enabled_updated_at": 1417823487.490747, - "desired_auto_lock_enabled_updated_at": 1417823487.490747, - "desired_key_code_length_updated_at": 1417823487.490747, - "desired_alarm_mode_updated_at": 1417823487.490747, - "desired_alarm_sensitivity_updated_at": 1417823487.490747, - "desired_alarm_enabled_updated_at": 1417823487.490747, - "locked_changed_at": 1417823487.490747, - "battery_changed_at": 1417823487.490747, - "desired_locked_changed_at": 1417823487.490747, - "desired_beeper_enabled_changed_at": 1417823487.490747, - "desired_vacation_mode_enabled_changed_at": 1417823487.490747, - "desired_auto_lock_enabled_changed_at": 1417823487.490747, - "desired_key_code_length_changed_at": 1417823487.490747, - "desired_alarm_mode_changed_at": 1417823487.490747, - "desired_alarm_sensitivity_changed_at": 1417823487.490747, - "desired_alarm_enabled_changed_at": 1417823487.490747, - "last_error_changed_at": 1417823487.490747 - }, - "lock_id": "5304", - "name": "Main", - "locale": "en_us", - "units": {}, - "created_at": 1417823382, - "hidden_at": null, - "capabilities": { - "fields": [ - { - "field": "locked", - "type": "boolean", - "mutability": "read-write" - }, - { - "field": "connection", - "mutability": "read-only", - "type": "boolean" - }, - { - "field": "battery", - "mutability": "read-only", - "type": "percentage" - }, - { - "field": "alarm_activated", - "mutability": "read-only", - "type": "boolean" - }, - { - "field": "beeper_enabled", - "type": "boolean" - }, - { - "field": "vacation_mode_enabled", - "type": "boolean" - }, - { - "field": "auto_lock_enabled", - "type": "boolean" - }, - { - "field": "key_code_length", - "type": "integer" - }, - { - "field": "alarm_mode", - "type": "string" - }, - { - "field": "alarm_sensitivity", - "type": "percentage" - }, - { - "field": "alarm_enabled", - "type": "boolean" - } - ], - "home_security_device": true - }, - "triggers": [], - "manufacturer_device_model": "schlage_zwave_lock", - "manufacturer_device_id": null, - "device_manufacturer": "schlage", - "model_name": "BE469", - "upc_id": "11", - "upc_code": "043156312214", - "hub_id": "11780", - "local_id": "1", - "radio_type": "zwave", - "lat_lng": [38.429962, -122.653715], - "location": "" - } - ], - "errors": [], - "pagination": { - "count": 1 - } - } - """ - - response_dict = json.loads(response) + with open('{}/api_responses/lock.json'.format(os.path.dirname(__file__))) as lock_file: + response_dict = json.load(lock_file) devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.LOCK]) self.assertEqual(1, len(devices)) self.assertIsInstance(devices[0], WinkLock) + + def test_device_id_should_be_number(self): + with open('{}/api_responses/lock.json'.format(os.path.dirname(__file__))) as lock_file: + response_dict = json.load(lock_file) + lock = response_dict.get('data')[0] + wink_lock = WinkLock(lock, self.api_interface) + device_id = wink_lock.device_id() + self.assertRegex(device_id, "^[0-9]{4,6}$") + + +class BinarySwitchTests(unittest.TestCase): - def test_should_handle_binary_switch_response(self): + def setUp(self): + super(BinarySwitchTests, self).setUp() + self.api_interface = mock.MagicMock() - response = """ - { - "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": {} - } - """ - - response_dict = json.loads(response) + def test_should_handle_binary_switch_response(self): + with open('{}/api_responses/binary_switch.json'.format(os.path.dirname(__file__))) as binary_switch_file: + response_dict = json.load(binary_switch_file) devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.BINARY_SWITCH]) self.assertEqual(1, len(devices)) self.assertIsInstance(devices[0], WinkBinarySwitch) + + def test_device_id_should_be_number(self): + with open('{}/api_responses/binary_switch.json'.format(os.path.dirname(__file__))) as binary_switch_file: + response_dict = json.load(binary_switch_file) + switch = response_dict.get('data')[0] + wink_switch = WinkBinarySwitch(switch, self.api_interface) + device_id = wink_switch.device_id() + self.assertRegex(device_id, "^[0-9]{4,6}$") + + +class BinarySensorTests(unittest.TestCase): + def setUp(self): + super(BinarySensorTests, self).setUp() + self.api_interface = mock.MagicMock() + def test_should_handle_sensor_pod_response(self): - - response = """ - { - "data": [{ - "last_event": { - "brightness_occurred_at": null, - "loudness_occurred_at": null, - "vibration_occurred_at": null - }, - "model_name": "Tripper", - "capabilities": { - "sensor_types": [ - { - "field": "opened", - "type": "boolean" - }, - { - "field": "battery", - "type": "percentage" - } - ] - }, - "manufacturer_device_model": "quirky_ge_tripper", - "location": "", - "radio_type": "zigbee", - "manufacturer_device_id": null, - "gang_id": null, - "sensor_pod_id": "37614", - "subscription": { - }, - "units": { - }, - "upc_id": "184", - "hidden_at": null, - "last_reading": { - "battery_voltage_threshold_2": 0, - "opened": false, - "battery_alarm_mask": 0, - "opened_updated_at": 1421697092.7347496, - "battery_voltage_min_threshold_updated_at": 1421697092.7347229, - "battery_voltage_min_threshold": 0, - "connection": null, - "battery_voltage": 25, - "battery_voltage_threshold_1": 25, - "connection_updated_at": null, - "battery_voltage_threshold_3": 0, - "battery_voltage_updated_at": 1421697092.7347066, - "battery_voltage_threshold_1_updated_at": 1421697092.7347302, - "battery_voltage_threshold_3_updated_at": 1421697092.7347434, - "battery_voltage_threshold_2_updated_at": 1421697092.7347374, - "battery": 1.0, - "battery_updated_at": 1421697092.7347553, - "battery_alarm_mask_updated_at": 1421697092.734716 - }, - "triggers": [ - ], - "name": "MasterBathroom", - "lat_lng": [ - 37.550773, - -122.279182 - ], - "uuid": "a2cb868a-dda3-4211-ab73-fc08087aeed7", - "locale": "en_us", - "device_manufacturer": "quirky_ge", - "created_at": 1421523277, - "local_id": "2", - "hub_id": "88264" - }] - } - """ - - response_dict = json.loads(response) + with open('{}/api_responses/binary_sensor.json'.format(os.path.dirname(__file__))) as binary_sensor_file: + response_dict = json.load(binary_sensor_file) devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SENSOR_POD]) self.assertEqual(1, len(devices)) self.assertIsInstance(devices[0], WinkSensorPod) + + def test_device_id_should_be_number(self): + with open('{}/api_responses/binary_sensor.json'.format(os.path.dirname(__file__))) as binary_sensor_file: + response_dict = json.load(binary_sensor_file) + sensor = response_dict.get('data')[0] + wink_binary_sensor = WinkSensorPod(sensor, self.api_interface) + device_id = wink_binary_sensor.device_id() + self.assertRegex(device_id, "^[0-9]{4,6}$") + + +class EggtrayTests(unittest.TestCase): - def test_should_handle_egg_tray_response(self): + def setUp(self): + super(EggtrayTests, self).setUp() + self.api_interface = mock.MagicMock() - response = """ - { - "data": [{ - "last_reading": { - "connection": true, - "connection_updated_at": 1417823487.490747, - "battery": 0.83, - "battery_updated_at": 1417823487.490747, - "inventory": 3, - "inventory_updated_at": 1449705551.7313306, - "freshness_remaining": 2419191, - "freshness_remaining_updated_at": 1449705551.7313495, - "age_updated_at": 1449705551.7313418, - "age": 1449705542, - "connection_changed_at": 1449705443.6858568, - "next_trigger_at_updated_at": null, - "next_trigger_at": null, - "egg_1_timestamp_updated_at": 1449753143.8631344, - "egg_1_timestamp_changed_at": 1449705534.0782206, - "egg_1_timestamp": 1449705545.0, - "egg_2_timestamp_updated_at": 1449753143.8631344, - "egg_2_timestamp_changed_at": 1449705534.0782206, - "egg_2_timestamp": 1449705545.0, - "egg_3_timestamp_updated_at": 1449753143.8631344, - "egg_3_timestamp_changed_at": 1449705534.0782206, - "egg_3_timestamp": 1449705545.0, - "egg_4_timestamp_updated_at": 1449753143.8631344, - "egg_4_timestamp_changed_at": 1449705534.0782206, - "egg_4_timestamp": 1449705545.0, - "egg_5_timestamp_updated_at": 1449753143.8631344, - "egg_5_timestamp_changed_at": 1449705534.0782206, - "egg_5_timestamp": 1449705545.0, - "egg_6_timestamp_updated_at": 1449753143.8631344, - "egg_6_timestamp_changed_at": 1449705534.0782206, - "egg_6_timestamp": 1449705545.0, - "egg_7_timestamp_updated_at": 1449753143.8631344, - "egg_7_timestamp_changed_at": 1449705534.0782206, - "egg_7_timestamp": 1449705545.0, - "egg_8_timestamp_updated_at": 1449753143.8631344, - "egg_8_timestamp_changed_at": 1449705534.0782206, - "egg_8_timestamp": 1449705545.0, - "egg_9_timestamp_updated_at": 1449753143.8631344, - "egg_9_timestamp_changed_at": 1449705534.0782206, - "egg_9_timestamp": 1449705545.0, - "egg_10_timestamp_updated_at": 1449753143.8631344, - "egg_10_timestamp_changed_at": 1449705534.0782206, - "egg_10_timestamp": 1449705545.0, - "egg_11_timestamp_updated_at": 1449753143.8631344, - "egg_11_timestamp_changed_at": 1449705534.0782206, - "egg_11_timestamp": 1449705545.0, - "egg_12_timestamp_updated_at": 1449753143.8631344, - "egg_12_timestamp_changed_at": 1449705534.0782206, - "egg_12_timestamp": 1449705545.0, - "egg_13_timestamp_updated_at": 1449753143.8631344, - "egg_13_timestamp_changed_at": 1449705534.0782206, - "egg_13_timestamp": 1449705545.0, - "egg_14_timestamp_updated_at": 1449753143.8631344, - "egg_14_timestamp_changed_at": 1449705534.0782206, - "egg_14_timestamp": 1449705545.0 - }, - "eggtray_id": "153869", - "name": "Egg Minder", - "freshness_period": 2419200, - "locale": "en_us", - "units": {}, - "created_at": 1417823382, - "hidden_at": null, - "capabilities": {}, - "triggers": [], - "device_manufacturer": "quirky_ge", - "model_name": "Egg Minder", - "upc_id": "23", - "upc_code": "814434017233", - "lat_lng": [38.429962, -122.653715], - "location": "" - }], - "errors": [], - "pagination": { - "count": 1 - } - } - """ - - response_dict = json.loads(response) + def test_should_handle_egg_tray_response(self): + with open('{}/api_responses/eggtray.json'.format(os.path.dirname(__file__))) as eggtray_file: + response_dict = json.load(eggtray_file) devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.EGG_TRAY]) self.assertEqual(1, len(devices)) self.assertIsInstance(devices[0], WinkEggTray) + + def test_device_id_should_be_number(self): + with open('{}/api_responses/eggtray.json'.format(os.path.dirname(__file__))) as eggtray_file: + response_dict = json.load(eggtray_file) + eggtray = response_dict.get('data')[0] + wink_eggtray = WinkEggTray(eggtray, self.api_interface) + device_id = wink_eggtray.device_id() + self.assertRegex(device_id, "^[0-9]{4,6}$") + + +class SensorTests(unittest.TestCase): + + def setUp(self): + super(SensorTests, self).setUp() + self.api_interface = mock.MagicMock() + + def test_quirky_spotter_api_response_should_create_unique_one_primary_sensor_and_five_subsensors(self): + with open('{}/api_responses/quirky_spotter.json'.format(os.path.dirname(__file__))) as spotter_file: + response_dict = json.load(spotter_file) + + sensors = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SENSOR_POD]) + self.assertEquals(1 + 5, len(sensors)) + + def test_alternative_quirky_spotter_api_response_should_create_one_primary_sensor_and_five_subsensors(self): + with open('{}/api_responses/quirky_spotter_2.json'.format(os.path.dirname(__file__))) as spotter_file: + response_dict = json.load(spotter_file) + + sensors = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SENSOR_POD]) + self.assertEquals(1 + 5, len(sensors)) + + def test_brightness_should_have_correct_value(self): + with open('{}/api_responses/quirky_spotter.json'.format(os.path.dirname(__file__))) as spotter_file: + response_dict = json.load(spotter_file) + + sensors = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SENSOR_POD]) + """:type : list of WinkBrightnessSensor""" + brightness_sensor = [sensor for sensor in sensors if sensor.capability() is WinkBrightnessSensor.CAPABILITY][0] + expected_brightness = 1 + self.assertEquals(expected_brightness, brightness_sensor.brightness_boolean()) + + def test_humidity_should_have_correct_value(self): + with open('{}/api_responses/quirky_spotter.json'.format(os.path.dirname(__file__))) as spotter_file: + response_dict = json.load(spotter_file) + + sensors = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SENSOR_POD]) + """:type : list of WinkHumiditySensor""" + humidity_sensor = [sensor for sensor in sensors if sensor.capability() is WinkHumiditySensor.CAPABILITY][0] + expected_humidity = 48 + self.assertEquals(expected_humidity, humidity_sensor.humidity_percentage()) + + def test_loudness_should_have_correct_value(self): + with open('{}/api_responses/quirky_spotter.json'.format(os.path.dirname(__file__))) as spotter_file: + response_dict = json.load(spotter_file) + + sensors = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SENSOR_POD]) + """:type : list of WinkSoundPresenceSensor""" + sound_sensor = [sensor for sensor in sensors if sensor.capability() is WinkSoundPresenceSensor.CAPABILITY][0] + expected_sound_presence = False + self.assertEquals(expected_sound_presence, sound_sensor.loudness_boolean()) + + def test_vibration_should_have_correct_value(self): + with open('{}/api_responses/quirky_spotter.json'.format(os.path.dirname(__file__))) as spotter_file: + response_dict = json.load(spotter_file) + + sensors = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SENSOR_POD]) + """:type : list of WinkVibrationPresenceSensor""" + vibration_sensor = [sensor for sensor in sensors if sensor.capability() is WinkVibrationPresenceSensor.CAPABILITY][0] + expected_vibrartion_presence = False + self.assertEquals(expected_vibrartion_presence, vibration_sensor.vibration_boolean()) + + def test_temperature_should_have_correct_value(self): + with open('{}/api_responses/quirky_spotter.json'.format(os.path.dirname(__file__))) as spotter_file: + response_dict = json.load(spotter_file) + + sensors = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SENSOR_POD]) + """:type : list of WinkTemperatureSensor""" + temp_sensor = [sensor for sensor in sensors if sensor.capability() is WinkTemperatureSensor.CAPABILITY][0] + expected_temperature = 5 + self.assertEquals(expected_temperature, temp_sensor.temperature_float()) + + def test_device_id_should_start_with_a_number(self): + with open('{}/api_responses/quirky_spotter.json'.format(os.path.dirname(__file__))) as spotter_file: + response_dict = json.load(spotter_file) + + sensors = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SENSOR_POD]) + + for sensor in sensors: + device_id = sensor.device_id() + self.assertRegex(device_id, "^[0-9]{4,6}") + + +class WinkCapabilitySensorTests(unittest.TestCase): + + def setUp(self): + super(WinkCapabilitySensorTests, self).setUp() + self.api_interface = mock.MagicMock() + + def test_should_call_get_state_endpoint_with_capability_removed_from_id(self): + expected_id = '72503' + unit = 'DEG' # mock doesn't like unicode + capability = "Test" + sensor = _WinkCapabilitySensor({ + 'sensor_pod_id': expected_id + }, self.api_interface, unit, capability) + + sensor.update_state() + self.api_interface.get_device_state.assert_called_once_with(sensor, expected_id) + diff --git a/src/pywink/test/sensor_test.py b/src/pywink/test/sensor_test.py deleted file mode 100644 index 30c938a..0000000 --- a/src/pywink/test/sensor_test.py +++ /dev/null @@ -1,76 +0,0 @@ -import json -import os -import unittest - -from pywink.devices import types as device_types -from pywink.api import get_devices_from_response_dict -from pywink.devices.sensors import WinkBrightnessSensor, WinkHumiditySensor, WinkSoundPresenceSensor, \ - WinkVibrationPresenceSensor, WinkTemperatureSensor -from pywink.devices.types import DEVICE_ID_KEYS - - -class SensorTests(unittest.TestCase): - - def test_quirky_spotter_api_response_should_create_unique_one_primary_sensor_and_five_subsensors(self): - with open('{}/api_responses/quirky_spotter.json'.format(os.path.dirname(__file__))) as spotter_file: - response_dict = json.load(spotter_file) - - sensors = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SENSOR_POD]) - self.assertEquals(1 + 5, len(sensors)) - - def test_alternative_quirky_spotter_api_response_should_create_one_primary_sensor_and_five_subsensors(self): - with open('{}/api_responses/quirky_spotter_2.json'.format(os.path.dirname(__file__))) as spotter_file: - response_dict = json.load(spotter_file) - - sensors = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SENSOR_POD]) - self.assertEquals(1 + 5, len(sensors)) - - def test_brightness_should_have_correct_value(self): - with open('{}/api_responses/quirky_spotter.json'.format(os.path.dirname(__file__))) as spotter_file: - response_dict = json.load(spotter_file) - - sensors = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SENSOR_POD]) - """:type : list of WinkBrightnessSensor""" - brightness_sensor = [sensor for sensor in sensors if sensor.capability() is WinkBrightnessSensor.CAPABILITY][0] - expected_brightness = 1 - self.assertEquals(expected_brightness, brightness_sensor.brightness_boolean()) - - def test_humidity_should_have_correct_value(self): - with open('{}/api_responses/quirky_spotter.json'.format(os.path.dirname(__file__))) as spotter_file: - response_dict = json.load(spotter_file) - - sensors = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SENSOR_POD]) - """:type : list of WinkHumiditySensor""" - humidity_sensor = [sensor for sensor in sensors if sensor.capability() is WinkHumiditySensor.CAPABILITY][0] - expected_humidity = 48 - self.assertEquals(expected_humidity, humidity_sensor.humidity_percentage()) - - def test_loudness_should_have_correct_value(self): - with open('{}/api_responses/quirky_spotter.json'.format(os.path.dirname(__file__))) as spotter_file: - response_dict = json.load(spotter_file) - - sensors = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SENSOR_POD]) - """:type : list of WinkSoundPresenceSensor""" - sound_sensor = [sensor for sensor in sensors if sensor.capability() is WinkSoundPresenceSensor.CAPABILITY][0] - expected_sound_presence = False - self.assertEquals(expected_sound_presence, sound_sensor.loudness_boolean()) - - def test_vibration_should_have_correct_value(self): - with open('{}/api_responses/quirky_spotter.json'.format(os.path.dirname(__file__))) as spotter_file: - response_dict = json.load(spotter_file) - - sensors = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SENSOR_POD]) - """:type : list of WinkVibrationPresenceSensor""" - sound_sensor = [sensor for sensor in sensors if sensor.capability() is WinkVibrationPresenceSensor.CAPABILITY][0] - expected_vibrartion_presence = False - self.assertEquals(expected_vibrartion_presence, sound_sensor.vibration_boolean()) - - def test_temperature_should_have_correct_value(self): - with open('{}/api_responses/quirky_spotter.json'.format(os.path.dirname(__file__))) as spotter_file: - response_dict = json.load(spotter_file) - - sensors = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SENSOR_POD]) - """:type : list of WinkTemperatureSensor""" - sound_sensor = [sensor for sensor in sensors if sensor.capability() is WinkTemperatureSensor.CAPABILITY][0] - expected_temperature = 5 - self.assertEquals(expected_temperature, sound_sensor.temperature_float()) diff --git a/src/setup.py b/src/setup.py index 5e1b85f..a669ac3 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='0.6.2', + version='0.6.3', description='Access Wink devices via the Wink API', url='http://github.com/bradsk88/python-wink', author='Brad Johnson', From 39e438553d2fc77b63693426ab8c13370fbb04dd Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Fri, 11 Mar 2016 17:37:03 -0500 Subject: [PATCH 048/178] Added avalible method to base wink --- CHANGELOG.md | 3 +++ src/pywink/devices/base.py | 7 +++++++ src/setup.py | 2 +- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 734d995..322f539 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 0.6.4 +- Added available method to report "connection" status + ## 0.6.3 - Override capability sensor device_id during update. diff --git a/src/pywink/devices/base.py b/src/pywink/devices/base.py index ae681a9..1d09c20 100644 --- a/src/pywink/devices/base.py +++ b/src/pywink/devices/base.py @@ -31,6 +31,13 @@ def device_id(self): def _last_reading(self): return self.json_state.get('last_reading') or {} + @property + def available(self): + if not self._last_reading.get('connection', False): + return False + else: + return True + def _update_state_from_response(self, response_json): """ :param response_json: the json obj returned from query diff --git a/src/setup.py b/src/setup.py index a669ac3..c152ae1 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='0.6.3', + version='0.6.4', description='Access Wink devices via the Wink API', url='http://github.com/bradsk88/python-wink', author='Brad Johnson', From 6c4716e6790b4928b95acff84913bf93743f71de Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Tue, 15 Mar 2016 11:09:19 -0400 Subject: [PATCH 049/178] Simplifiy available return --- src/pywink/devices/base.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/pywink/devices/base.py b/src/pywink/devices/base.py index 1d09c20..e24e67b 100644 --- a/src/pywink/devices/base.py +++ b/src/pywink/devices/base.py @@ -33,10 +33,7 @@ def _last_reading(self): @property def available(self): - if not self._last_reading.get('connection', False): - return False - else: - return True + return self._last_reading.get('connection', False) def _update_state_from_response(self, response_json): """ From de82440451de39d7b4dcf9fe601482f367f144f0 Mon Sep 17 00:00:00 2001 From: bradsk88 <7kcwKg60tdcW> Date: Mon, 14 Mar 2016 22:12:51 -0600 Subject: [PATCH 050/178] Expanded upon color support for WinkBulbs Added support for supplying consumer_id, consumer_secret, and refresh_token to prevent tokens expiring. Refactored a bit because WinkBulb was ballooning in size. --- CHANGELOG.md | 5 + pylintrc | 7 + src/pywink/__init__.py | 5 +- src/pywink/api.py | 18 ++ src/pywink/color.py | 106 +++++++++ .../{standard.py => standard/__init__.py} | 173 ++------------ src/pywink/devices/standard/base.py | 75 ++++++ src/pywink/devices/standard/bulb.py | 213 ++++++++++++++++++ src/pywink/test/init_test.py | 120 +++------- src/pywink/test/standard/__init__.py | 0 .../hue_and_saturation_absent.json | 118 ++++++++++ .../hue_and_saturation_present.json | 129 +++++++++++ .../api_responses/light_bulb.json | 0 .../api_responses/temperature_absent.json | 119 ++++++++++ .../api_responses/temperature_present.json | 128 +++++++++++ src/pywink/test/standard/bulb_test.py | 146 ++++++++++++ src/setup.py | 2 +- 17 files changed, 1116 insertions(+), 248 deletions(-) create mode 100644 src/pywink/color.py rename src/pywink/devices/{standard.py => standard/__init__.py} (64%) create mode 100644 src/pywink/devices/standard/base.py create mode 100644 src/pywink/devices/standard/bulb.py create mode 100644 src/pywink/test/standard/__init__.py create mode 100644 src/pywink/test/standard/api_responses/hue_and_saturation_absent.json create mode 100644 src/pywink/test/standard/api_responses/hue_and_saturation_present.json rename src/pywink/test/{ => standard}/api_responses/light_bulb.json (100%) create mode 100644 src/pywink/test/standard/api_responses/temperature_absent.json create mode 100644 src/pywink/test/standard/api_responses/temperature_present.json create mode 100644 src/pywink/test/standard/bulb_test.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 322f539..20f80e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Change Log +## 0.7.0 +- Expanded color support for WinkBulbs +- Added ability to supply client_id, client_secret, and refresh_token +instead of access_token. This should get around tokens expiring. + ## 0.6.4 - Added available method to report "connection" status diff --git a/pylintrc b/pylintrc index dc9b501..20c728b 100644 --- a/pylintrc +++ b/pylintrc @@ -1,12 +1,19 @@ [MASTER] max-line-length=120 +max-args=10 ignore=test # Reasons disabled: # missing-docstring - Document as you like. Good, descriptive method names and variables are preferred over docstrings. # global-statement - used for the on-demand requirement installation +# invalid-name - this warning flags short names but I'm fine with short names when used correctly +# duplicate-code - I'd like to re-enabled this but 'wait_till_desired_reached' is duplicated. Fix this later. +# fixme - TODOs are allowed prior to v1.0 # locally-disabled - Because that's the whole point! disable= missing-docstring , global-statement + , invalid-name + , duplicate-code + , fixme , locally-disabled diff --git a/src/pywink/__init__.py b/src/pywink/__init__.py index 555fe26..12d2f70 100644 --- a/src/pywink/__init__.py +++ b/src/pywink/__init__.py @@ -2,6 +2,7 @@ Top level functions """ # noqa -from pywink.api import set_bearer_token, get_bulbs, get_eggtrays, get_garage_doors, get_locks, \ - get_powerstrip_outlets, get_sensors, get_sirens, get_switches, get_devices, is_token_set +from pywink.api import set_bearer_token, set_wink_credentials, get_bulbs, \ + get_eggtrays, get_garage_doors, get_locks, get_powerstrip_outlets, \ + get_sensors, get_sirens, get_switches, get_devices, is_token_set diff --git a/src/pywink/api.py b/src/pywink/api.py index 517100f..d0b0102 100644 --- a/src/pywink/api.py +++ b/src/pywink/api.py @@ -51,6 +51,24 @@ def set_bearer_token(token): } +def set_wink_credentials(client_id, client_secret, refresh_token): + data = { + "client_id": client_id, + "client_secret": client_secret, + "grant_type": "refresh_token", + "refresh_token": refresh_token + } + headers = { + 'Content-Type': 'application/json' + } + response = requests.post('{}/oauth2/token'.format(WinkApiInterface.BASE_URL), + data=json.dumps(data), + headers=headers) + response_json = response.json() + access_token = response_json.get('access_token') + set_bearer_token(access_token) + + def get_bulbs(): return get_devices(device_types.LIGHT_BULB) diff --git a/src/pywink/color.py b/src/pywink/color.py new file mode 100644 index 0000000..8b5c9fd --- /dev/null +++ b/src/pywink/color.py @@ -0,0 +1,106 @@ +import math + + +# pylint: disable=too-many-branches +def color_temperature_to_rgb(color_temperature_kelvin): + """ + Return an RGB color from a color temperature in Kelvin. + This is a rough approximation, based on the formula provided by Tanner Helland + http://www.tannerhelland.com/4435/convert-temperature-rgb-algorithm-code/ + """ + # range check + if color_temperature_kelvin < 1000: + color_temperature_kelvin = 1000 + elif color_temperature_kelvin > 40000: + color_temperature_kelvin = 40000 + + tmp_internal = color_temperature_kelvin / 100.0 + + # red + if tmp_internal <= 66: + red = 255 + else: + tmp_red = 329.698727446 * math.pow(tmp_internal - 60, -0.1332047592) + if tmp_red < 0: + red = 0 + elif tmp_red > 255: + red = 255 + else: + red = tmp_red + + # green + if tmp_internal <= 66: + tmp_green = 99.4708025861 * math.log(tmp_internal) - 161.1195681661 + if tmp_green < 0: + green = 0 + elif tmp_green > 255: + green = 255 + else: + green = tmp_green + else: + tmp_green = 288.1221695283 * math.pow(tmp_internal - 60, -0.0755148492) + if tmp_green < 0: + green = 0 + elif tmp_green > 255: + green = 255 + else: + green = tmp_green + + # blue + if tmp_internal >= 66: + blue = 255 + elif tmp_internal <= 19: + blue = 0 + else: + tmp_blue = 138.5177312231 * math.log(tmp_internal - 10) - 305.0447927307 + if tmp_blue < 0: + blue = 0 + elif tmp_blue > 255: + blue = 255 + else: + blue = tmp_blue + + return (red, green, blue) + + +# taken from +# https://github.com/benknight/hue-python-rgb-converter/blob/master/rgb_cie.py +# Copyright (c) 2014 Benjamin Knight / MIT License. +# pylint: disable=bad-builtin +def color_xy_brightness_to_rgb(vX, vY, brightness): + """Convert from XYZ to RGB.""" + brightness /= 255. + if brightness == 0: + return (0, 0, 0) + + Y = brightness + + if vY == 0: + vY += 0.00000000001 + + X = (Y / vY) * vX + Z = (Y / vY) * (1 - vX - vY) + + # Convert to RGB using Wide RGB D65 conversion. + r = X * 1.612 - Y * 0.203 - Z * 0.302 + g = -X * 0.509 + Y * 1.412 + Z * 0.066 + b = X * 0.026 - Y * 0.072 + Z * 0.962 + + # Apply reverse gamma correction. + r, g, b = map( + lambda x: (12.92 * x) if (x <= 0.0031308) else + ((1.0 + 0.055) * pow(x, (1.0 / 2.4)) - 0.055), + [r, g, b] + ) + + # Bring all negative components to zero. + r, g, b = map(lambda x: max(0, x), [r, g, b]) + + # If one component is greater than 1, weight components by that value. + max_component = max(r, g, b) + if max_component > 1: + r, g, b = map(lambda x: x / max_component, [r, g, b]) + + r, g, b = map(lambda x: int(x * 255), [r, g, b]) + + return (r, g, b) diff --git a/src/pywink/devices/standard.py b/src/pywink/devices/standard/__init__.py similarity index 64% rename from src/pywink/devices/standard.py rename to src/pywink/devices/standard/__init__.py index e29f5bf..0feb6d1 100644 --- a/src/pywink/devices/standard.py +++ b/src/pywink/devices/standard/__init__.py @@ -1,10 +1,11 @@ """ Objects for interfacing with the Wink API """ -import logging import time from pywink.devices.base import WinkDevice +from pywink.devices.standard.base import WinkBinarySwitch +from pywink.devices.standard.bulb import WinkBulb class WinkEggTray(WinkDevice): @@ -31,164 +32,6 @@ 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, api_interface, objectprefix="binary_switches"): - super(WinkBinarySwitch, self).__init__(device_state_as_json, api_interface, - 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 - """ - values = { - "desired_state": { - "powered": state - } - } - response = self.api_interface.set_device_state(self, values) - self._update_state_from_response(response) - - 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, api_interface): - super().__init__(device_state_as_json, api_interface, - 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 - """ - 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) - - response = self.api_interface.set_device_state(self, values) - self._update_state_from_response(response) - - 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 @@ -227,6 +70,7 @@ def set_state(self, state): self._update_state_from_response(response) self._last_call = (time.time(), state) + # pylint: disable=duplicate-code def wait_till_desired_reached(self): """ Wait till desired state reached. Max 10s. """ if self._recent_state_set(): @@ -384,6 +228,7 @@ def set_state(self, state): self._last_call = (time.time(), state) + # pylint: disable=duplicate-code def wait_till_desired_reached(self): """ Wait till desired state reached. Max 10s. """ if self._recent_state_set(): @@ -428,3 +273,13 @@ def __repr__(self): def device_id(self): return self.json_state.get('siren_id', self.name()) + + +# pylint-disable: undefined-all-variable +__all__ = [WinkEggTray.__name__, + WinkBinarySwitch.__name__, + WinkBulb.__name__, + WinkLock.__name__, + WinkPowerStripOutlet.__name__, + WinkGarageDoor.__name__, + WinkSiren.__name__] diff --git a/src/pywink/devices/standard/base.py b/src/pywink/devices/standard/base.py new file mode 100644 index 0000000..02b1598 --- /dev/null +++ b/src/pywink/devices/standard/base.py @@ -0,0 +1,75 @@ +import time + +from pywink.devices.base import WinkDevice + + +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, api_interface, objectprefix="binary_switches"): + super(WinkBinarySwitch, self).__init__(device_state_as_json, api_interface, + 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 + """ + values = { + "desired_state": { + "powered": state + } + } + response = self.api_interface.set_device_state(self, values) + self._update_state_from_response(response) + + self._last_call = (time.time(), state) + + def wait_till_desired_reached(self): + """ Wait till desired state reached. Max 10s. """ + # TODO: Get rid of this. Busy-wait loops can go in whatever project is making use of this library. + 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 diff --git a/src/pywink/devices/standard/bulb.py b/src/pywink/devices/standard/bulb.py new file mode 100644 index 0000000..0012c24 --- /dev/null +++ b/src/pywink/devices/standard/bulb.py @@ -0,0 +1,213 @@ +import colorsys +import time + +from pywink.devices.standard.base import WinkBinarySwitch +from pywink.color import color_temperature_to_rgb, color_xy_brightness_to_rgb + + +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, api_interface): + super().__init__(device_state_as_json, api_interface, + 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 is not None and color_y is not None: + 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 color_hue(self): + """ + Color hue from 0 to 1.0 + """ + return self._last_reading.get('hue') + + def color_saturation(self): + """ + Color saturation from 0 to 1.0 + :return: + """ + return self._last_reading.get('saturation') + + def set_state(self, state, brightness=None, + color_kelvin=None, color_xy=None, + color_hue_saturation=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 + :param color_hue_saturation: a pair of floats in a list which specify + the desired hue and saturation in that order. Brightness can be + supplied via the brightness param + :return: nothing + """ + desired_state = {"powered": state} + + color_state = self._format_color_data(color_hue_saturation, color_kelvin, color_xy, brightness or 1) + desired_state.update(color_state) + + response = self.api_interface.set_device_state(self, { + "desired_state": desired_state + }) + self._update_state_from_response(response) + + self._last_call = (time.time(), state) + + def _format_color_data(self, color_hue_saturation, color_kelvin, color_xy, brightness): + if brightness and color_hue_saturation is None and color_kelvin is None and color_xy is None: + return { + "brightness": brightness + } + + elif self._bulb_supports_rgb(): + rgb = _get_color_as_rgb(color_hue_saturation, color_kelvin, color_xy, brightness) + if rgb: + return { + "color_model": "rgb", + "color_r": rgb[0], + "color_g": rgb[1], + "color_b": rgb[2] + } + # TODO: Find out if this is the correct format + + if color_hue_saturation is None and color_kelvin is not None and self._bulb_supports_temperature(): + return _format_temperature(color_kelvin, brightness) + + if self._bulb_supports_hue_saturation(): + hsv = _get_color_as_hue_saturation_brightness(color_hue_saturation, brightness, color_kelvin, color_xy) + if hsv is not None: + return _format_hue_saturation_brightness(hsv) + + if self._bulb_supports_xy_color(): + if color_xy is not None: + return _format_xy(color_xy) + + return {} + + def _bulb_supports_rgb(self): + capabilities = self._last_reading.get('capabilities', {}) + if not capabilities.get('color_changeable', False): + return False + # cap_fields = capabilities.get('fields', []) + # TODO: Do any wink bulbs support RGB specification? + return False + + def _bulb_supports_hue_saturation(self): + capabilities = self.json_state.get('capabilities', {}) + if not capabilities.get('color_changeable', False): + return False + cap_fields = capabilities.get('fields', []) + supports_hue = False + supports_saturation = False + for field in cap_fields: + _field = field.get('field') + if _field == 'hue': + supports_hue = True + if _field == 'saturation': + supports_saturation = True + if supports_hue and supports_saturation: + return True + if supports_hue and supports_saturation: + return True + + def _bulb_supports_xy_color(self): + # TODO: Do any wink bulbs support XY color? + return self.json_state.get("todo", False) + + def _bulb_supports_temperature(self): + capabilities = self.json_state.get('capabilities', {}) + if not capabilities.get('color_changeable', False): + return False + cap_fields = capabilities.get('fields', []) + for field in cap_fields: + _field = field.get('field') + if _field == 'color_temperature': + return True + return False + + def __repr__(self): + return "" % ( + self.name(), self.device_id(), self.state()) + + +def _format_temperature(kelvin, brightness): + return { + "color_model": "color_temperature", + "color_temperature": kelvin, + "brightness": brightness + } + + +def _format_hue_saturation_brightness(hue_saturation_brightness): + hsv_iter = iter(hue_saturation_brightness) + return { + "color_model": "hsb", + "hue": next(hsv_iter), + "saturation": next(hsv_iter), + "brightness": next(hsv_iter) + } + + +def _format_xy(xy): + color_xy_iter = iter(xy) + return { + "color_model": "xy", + "color_x": next(color_xy_iter), + "color_y": next(color_xy_iter) + } + + +def _get_color_as_rgb(hue_sat, brightness, kelvin, xy): + if hue_sat is not None and brightness is not None: + h, s, v = colorsys.hsv_to_rgb(hue_sat[0], hue_sat[1], brightness) + return tuple(h, s, v) + if kelvin is not None: + return color_temperature_to_rgb(kelvin) + if xy is not None: + return color_xy_brightness_to_rgb(xy[0], xy[1], brightness) + return None + + +def _get_color_as_hue_saturation_brightness(hue_sat, brightness, kelvin, xy): + if hue_sat and brightness: + color_hs_iter = iter(hue_sat) + return (next(color_hs_iter), next(color_hs_iter), brightness) + rgb = _get_color_as_rgb(None, brightness, kelvin, xy) + if not rgb: + return None + h, s, v = colorsys.rgb_to_hsv(rgb[0], rgb[1], rgb[2]) + return (h, s, v) diff --git a/src/pywink/test/init_test.py b/src/pywink/test/init_test.py index 4436909..acda8c1 100644 --- a/src/pywink/test/init_test.py +++ b/src/pywink/test/init_test.py @@ -14,58 +14,6 @@ from pywink.devices.types import DEVICE_ID_KEYS -class LightTests(unittest.TestCase): - - def setUp(self): - super(LightTests, self).setUp() - self.api_interface = WinkApiInterface() - - def test_should_handle_light_bulb_response(self): - with open('{}/api_responses/light_bulb.json'.format(os.path.dirname(__file__))) as light_file: - response_dict = json.load(light_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.LIGHT_BULB]) - self.assertEqual(1, len(devices)) - self.assertIsInstance(devices[0], WinkBulb) - - @mock.patch('requests.put') - def test_should_send_correct_color_xy_values_to_wink_api(self, put_mock): - bulb = WinkBulb({}, self.api_interface) - color_x = 0.75 - color_y = 0.25 - bulb.set_state(True, color_xy=[color_x, color_y]) - sent_data = json.loads(put_mock.call_args[1].get('data')) - self.assertEquals(color_x, sent_data.get('desired_state', {}).get('color_x')) - self.assertEquals(color_y, sent_data.get('desired_state', {}).get('color_y')) - self.assertEquals('xy', sent_data['desired_state'].get('color_model')) - - @mock.patch('requests.put') - def test_should_send_correct_color_temperature_values_to_wink_api(self, put_mock): - bulb = WinkBulb({}, self.api_interface) - arbitrary_kelvin_color = 4950 - bulb.set_state(True, color_kelvin=arbitrary_kelvin_color) - sent_data = json.loads(put_mock.call_args[1].get('data')) - self.assertEquals('color_temperature', sent_data['desired_state'].get('color_model')) - self.assertEquals(arbitrary_kelvin_color, sent_data['desired_state'].get('color_temperature')) - - @mock.patch('requests.put') - def test_should_only_send_color_xy_if_both_color_xy_and_color_temperature_are_given(self, put_mock): - bulb = WinkBulb({}, self.api_interface) - arbitrary_kelvin_color = 4950 - bulb.set_state(True, color_kelvin=arbitrary_kelvin_color, color_xy=[0, 1]) - sent_data = json.loads(put_mock.call_args[1].get('data')) - self.assertEquals('color_temperature', sent_data['desired_state'].get('color_model')) - self.assertNotIn('color_x', sent_data['desired_state']) - self.assertNotIn('color_y', sent_data['desired_state']) - - def test_device_id_should_be_number(self): - with open('{}/api_responses/light_bulb.json'.format(os.path.dirname(__file__))) as light_file: - response_dict = json.load(light_file) - light = response_dict.get('data')[0] - wink_light = WinkBulb(light, self.api_interface) - device_id = wink_light.device_id() - self.assertRegex(device_id, "^[0-9]{4,6}$") - - class PowerStripTests(unittest.TestCase): def setUp(self): @@ -96,7 +44,7 @@ def test_device_id_should_be_number(self): wink_outlet = WinkPowerStripOutlet(outlet, self.api_interface) device_id = wink_outlet.device_id() self.assertRegex(device_id, "^[0-9]{4,6}$") - + class GarageDoorTests(unittest.TestCase): @@ -110,7 +58,7 @@ def test_should_handle_garage_door_opener_response(self): devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.GARAGE_DOOR]) self.assertEqual(1, len(devices)) self.assertIsInstance(devices[0], WinkGarageDoor) - + def test_device_id_should_be_number(self): with open('{}/api_responses/garage_door.json'.format(os.path.dirname(__file__))) as garage_door_file: response_dict = json.load(garage_door_file) @@ -119,12 +67,12 @@ def test_device_id_should_be_number(self): device_id = wink_garage_door.device_id() self.assertRegex(device_id, "^[0-9]{4,6}$") - -class SirenTests(unittest.TestCase): + +class SirenTests(unittest.TestCase): def setUp(self): super(SirenTests, self).setUp() - self.api_interface = mock.MagicMock() + self.api_interface = mock.MagicMock() def test_should_handle_siren_response(self): with open('{}/api_responses/siren.json'.format(os.path.dirname(__file__))) as siren_file: @@ -132,43 +80,43 @@ def test_should_handle_siren_response(self): devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SIREN]) self.assertEqual(1, len(devices)) self.assertIsInstance(devices[0], WinkSiren) - + def test_device_id_should_be_number(self): with open('{}/api_responses/siren.json'.format(os.path.dirname(__file__))) as siren_file: response_dict = json.load(siren_file) siren = response_dict.get('data')[0] wink_siren = WinkSiren(siren, self.api_interface) device_id = wink_siren.device_id() - self.assertRegex(device_id, "^[0-9]{4,6}$") + self.assertRegex(device_id, "^[0-9]{4,6}$") + + +class LockTests(unittest.TestCase): - -class LockTests(unittest.TestCase): - def setUp(self): super(LockTests, self).setUp() - self.api_interface = mock.MagicMock() - + self.api_interface = mock.MagicMock() + def test_should_handle_lock_response(self): with open('{}/api_responses/lock.json'.format(os.path.dirname(__file__))) as lock_file: response_dict = json.load(lock_file) devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.LOCK]) self.assertEqual(1, len(devices)) self.assertIsInstance(devices[0], WinkLock) - + def test_device_id_should_be_number(self): with open('{}/api_responses/lock.json'.format(os.path.dirname(__file__))) as lock_file: response_dict = json.load(lock_file) lock = response_dict.get('data')[0] wink_lock = WinkLock(lock, self.api_interface) device_id = wink_lock.device_id() - self.assertRegex(device_id, "^[0-9]{4,6}$") + self.assertRegex(device_id, "^[0-9]{4,6}$") + - -class BinarySwitchTests(unittest.TestCase): +class BinarySwitchTests(unittest.TestCase): def setUp(self): super(BinarySwitchTests, self).setUp() - self.api_interface = mock.MagicMock() + self.api_interface = mock.MagicMock() def test_should_handle_binary_switch_response(self): with open('{}/api_responses/binary_switch.json'.format(os.path.dirname(__file__))) as binary_switch_file: @@ -176,43 +124,43 @@ def test_should_handle_binary_switch_response(self): devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.BINARY_SWITCH]) self.assertEqual(1, len(devices)) self.assertIsInstance(devices[0], WinkBinarySwitch) - + def test_device_id_should_be_number(self): with open('{}/api_responses/binary_switch.json'.format(os.path.dirname(__file__))) as binary_switch_file: response_dict = json.load(binary_switch_file) switch = response_dict.get('data')[0] wink_switch = WinkBinarySwitch(switch, self.api_interface) device_id = wink_switch.device_id() - self.assertRegex(device_id, "^[0-9]{4,6}$") - - -class BinarySensorTests(unittest.TestCase): + self.assertRegex(device_id, "^[0-9]{4,6}$") + + +class BinarySensorTests(unittest.TestCase): def setUp(self): super(BinarySensorTests, self).setUp() - self.api_interface = mock.MagicMock() - + self.api_interface = mock.MagicMock() + def test_should_handle_sensor_pod_response(self): with open('{}/api_responses/binary_sensor.json'.format(os.path.dirname(__file__))) as binary_sensor_file: response_dict = json.load(binary_sensor_file) devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SENSOR_POD]) self.assertEqual(1, len(devices)) self.assertIsInstance(devices[0], WinkSensorPod) - + def test_device_id_should_be_number(self): with open('{}/api_responses/binary_sensor.json'.format(os.path.dirname(__file__))) as binary_sensor_file: response_dict = json.load(binary_sensor_file) sensor = response_dict.get('data')[0] wink_binary_sensor = WinkSensorPod(sensor, self.api_interface) device_id = wink_binary_sensor.device_id() - self.assertRegex(device_id, "^[0-9]{4,6}$") - - -class EggtrayTests(unittest.TestCase): + self.assertRegex(device_id, "^[0-9]{4,6}$") + + +class EggtrayTests(unittest.TestCase): def setUp(self): super(EggtrayTests, self).setUp() - self.api_interface = mock.MagicMock() + self.api_interface = mock.MagicMock() def test_should_handle_egg_tray_response(self): with open('{}/api_responses/eggtray.json'.format(os.path.dirname(__file__))) as eggtray_file: @@ -220,7 +168,7 @@ def test_should_handle_egg_tray_response(self): devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.EGG_TRAY]) self.assertEqual(1, len(devices)) self.assertIsInstance(devices[0], WinkEggTray) - + def test_device_id_should_be_number(self): with open('{}/api_responses/eggtray.json'.format(os.path.dirname(__file__))) as eggtray_file: response_dict = json.load(eggtray_file) @@ -229,12 +177,12 @@ def test_device_id_should_be_number(self): device_id = wink_eggtray.device_id() self.assertRegex(device_id, "^[0-9]{4,6}$") - + class SensorTests(unittest.TestCase): def setUp(self): super(SensorTests, self).setUp() - self.api_interface = mock.MagicMock() + self.api_interface = mock.MagicMock() def test_quirky_spotter_api_response_should_create_unique_one_primary_sensor_and_five_subsensors(self): with open('{}/api_responses/quirky_spotter.json'.format(os.path.dirname(__file__))) as spotter_file: @@ -308,7 +256,7 @@ def test_device_id_should_start_with_a_number(self): for sensor in sensors: device_id = sensor.device_id() - self.assertRegex(device_id, "^[0-9]{4,6}") + self.assertRegex(device_id, "^[0-9]{4,6}") class WinkCapabilitySensorTests(unittest.TestCase): diff --git a/src/pywink/test/standard/__init__.py b/src/pywink/test/standard/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/pywink/test/standard/api_responses/hue_and_saturation_absent.json b/src/pywink/test/standard/api_responses/hue_and_saturation_absent.json new file mode 100644 index 0000000..4efd714 --- /dev/null +++ b/src/pywink/test/standard/api_responses/hue_and_saturation_absent.json @@ -0,0 +1,118 @@ +{ + "data": [ + { + "uuid": "238539e2-1ad6-44ba-bc53-33c684c36e1d", + "desired_state": { + "powered": false, + "brightness": 1, + "color_model": "hsb", + "hue": 0.35, + "saturation": 1, + "color_temperature": 1901 + }, + "last_reading": { + "connection": true, + "connection_updated_at": 1458628651.862995, + "powered": false, + "powered_updated_at": 1458628651.862995, + "brightness": 1, + "brightness_updated_at": 1458628651.862995, + "color_model": "hsb", + "color_model_updated_at": 1458628651.862995, + "hue": 0.35, + "hue_updated_at": 1458628651.862995, + "saturation": 1, + "saturation_updated_at": 1458628651.862995, + "color_temperature": 1901, + "color_temperature_updated_at": 1458628651.862995, + "firmware_version": "0.1b02 / 0.3b22", + "firmware_version_updated_at": 1458628651.862995, + "firmware_date_code": "20150929N****", + "firmware_date_code_updated_at": 1458628651.862995, + "desired_powered_updated_at": 1458628650.8619466, + "desired_brightness_updated_at": 1458628820.0301423, + "desired_color_model_updated_at": 1458628820.0301423, + "desired_hue_updated_at": 1458628820.0301423, + "desired_saturation_updated_at": 1458628820.0301423, + "desired_color_temperature_updated_at": 1458628820.0301423, + "powered_changed_at": 1458628650.8134031, + "brightness_changed_at": 1458122238.7788615, + "connection_changed_at": 1457517588.4372394, + "desired_powered_changed_at": 1458628650.8619466, + "desired_brightness_changed_at": 1458628381.8566465, + "firmware_date_code_changed_at": 1457521561.0603704, + "color_model_changed_at": 1457521797.6389458, + "hue_changed_at": 1457595786.5472758, + "saturation_changed_at": 1457595782.71269, + "color_temperature_changed_at": 1457521786.911106, + "firmware_version_changed_at": 1457521561.0603704, + "desired_color_model_changed_at": 1458620834.569744, + "desired_hue_changed_at": 1457595786.605935, + "desired_saturation_changed_at": 1457595782.8273423, + "desired_color_temperature_changed_at": 1457521921.4747667 + }, + "light_bulb_id": "1515274", + "name": "Bat Signal", + "locale": "en_us", + "units": { + }, + "created_at": 1457517586, + "hidden_at": null, + "capabilities": { + "fields": [ + { + "field": "connection", + "type": "boolean", + "mutability": "read-only" + }, + { + "field": "powered", + "type": "boolean", + "mutability": "read-write" + }, + { + "field": "brightness", + "type": "percentage", + "mutability": "read-write" + }, + { + "field": "color_model", + "type": "string", + "choices": [ + "rgb", + "color_temperature" + ] + }, + { + "field": "color_temperature", + "range": [ + 1900, + 6500 + ], + "type": "integer", + "mutability": "read-write" + } + ], + "color_changeable": true + }, + "triggers": [], + "manufacturer_device_model": "sylvania_sylvania_rgbw", + "manufacturer_device_id": null, + "device_manufacturer": "sylvania", + "model_name": "Lightify RGBW Bulb", + "upc_id": "509", + "upc_code": "4613573703", + "gang_id": null, + "hub_id": "381678", + "local_id": "37", + "radio_type": "zigbee", + "linked_service_id": null, + "lat_lng": [ + null, + null + ], + "location": "", + "order": 0 + } + ] +} diff --git a/src/pywink/test/standard/api_responses/hue_and_saturation_present.json b/src/pywink/test/standard/api_responses/hue_and_saturation_present.json new file mode 100644 index 0000000..ae4e04b --- /dev/null +++ b/src/pywink/test/standard/api_responses/hue_and_saturation_present.json @@ -0,0 +1,129 @@ +{ + "data": [ + { + "uuid": "238539e2-1ad6-44ba-bc53-33c684c36e1d", + "desired_state": { + "powered": false, + "brightness": 1, + "color_model": "hsb", + "hue": 0.35, + "saturation": 1, + "color_temperature": 1901 + }, + "last_reading": { + "connection": true, + "connection_updated_at": 1458628651.862995, + "powered": false, + "powered_updated_at": 1458628651.862995, + "brightness": 1, + "brightness_updated_at": 1458628651.862995, + "color_model": "hsb", + "color_model_updated_at": 1458628651.862995, + "hue": 0.35, + "hue_updated_at": 1458628651.862995, + "saturation": 1, + "saturation_updated_at": 1458628651.862995, + "color_temperature": 1901, + "color_temperature_updated_at": 1458628651.862995, + "firmware_version": "0.1b02 / 0.3b22", + "firmware_version_updated_at": 1458628651.862995, + "firmware_date_code": "20150929N****", + "firmware_date_code_updated_at": 1458628651.862995, + "desired_powered_updated_at": 1458628650.8619466, + "desired_brightness_updated_at": 1458628820.0301423, + "desired_color_model_updated_at": 1458628820.0301423, + "desired_hue_updated_at": 1458628820.0301423, + "desired_saturation_updated_at": 1458628820.0301423, + "desired_color_temperature_updated_at": 1458628820.0301423, + "powered_changed_at": 1458628650.8134031, + "brightness_changed_at": 1458122238.7788615, + "connection_changed_at": 1457517588.4372394, + "desired_powered_changed_at": 1458628650.8619466, + "desired_brightness_changed_at": 1458628381.8566465, + "firmware_date_code_changed_at": 1457521561.0603704, + "color_model_changed_at": 1457521797.6389458, + "hue_changed_at": 1457595786.5472758, + "saturation_changed_at": 1457595782.71269, + "color_temperature_changed_at": 1457521786.911106, + "firmware_version_changed_at": 1457521561.0603704, + "desired_color_model_changed_at": 1458620834.569744, + "desired_hue_changed_at": 1457595786.605935, + "desired_saturation_changed_at": 1457595782.8273423, + "desired_color_temperature_changed_at": 1457521921.4747667 + }, + "light_bulb_id": "1515274", + "name": "Bat Signal", + "locale": "en_us", + "units": { + }, + "created_at": 1457517586, + "hidden_at": null, + "capabilities": { + "fields": [ + { + "field": "connection", + "type": "boolean", + "mutability": "read-only" + }, + { + "field": "powered", + "type": "boolean", + "mutability": "read-write" + }, + { + "field": "brightness", + "type": "percentage", + "mutability": "read-write" + }, + { + "field": "color_model", + "type": "string", + "choices": [ + "rgb", + "hsb", + "color_temperature" + ] + }, + { + "field": "hue", + "type": "percentage", + "mutability": "read-write" + }, + { + "field": "saturation", + "type": "percentage", + "mutability": "read-write" + }, + { + "field": "color_temperature", + "range": [ + 1900, + 6500 + ], + "type": "integer", + "mutability": "read-write" + } + ], + "color_changeable": true + }, + "triggers": [], + "manufacturer_device_model": "sylvania_sylvania_rgbw", + "manufacturer_device_id": null, + "device_manufacturer": "sylvania", + "model_name": "Lightify RGBW Bulb", + "upc_id": "509", + "upc_code": "4613573703", + "gang_id": null, + "hub_id": "381678", + "local_id": "37", + "radio_type": "zigbee", + "linked_service_id": null, + "lat_lng": [ + null, + null + ], + "location": "", + "order": 0 + } + ] +} diff --git a/src/pywink/test/api_responses/light_bulb.json b/src/pywink/test/standard/api_responses/light_bulb.json similarity index 100% rename from src/pywink/test/api_responses/light_bulb.json rename to src/pywink/test/standard/api_responses/light_bulb.json diff --git a/src/pywink/test/standard/api_responses/temperature_absent.json b/src/pywink/test/standard/api_responses/temperature_absent.json new file mode 100644 index 0000000..cc5f4e9 --- /dev/null +++ b/src/pywink/test/standard/api_responses/temperature_absent.json @@ -0,0 +1,119 @@ +{ + "data": [ + { + "uuid": "238539e2-1ad6-44ba-bc53-33c684c36e1d", + "desired_state": { + "powered": false, + "brightness": 1, + "color_model": "hsb", + "hue": 0.35, + "saturation": 1, + "color_temperature": 1901 + }, + "last_reading": { + "connection": true, + "connection_updated_at": 1458628651.862995, + "powered": false, + "powered_updated_at": 1458628651.862995, + "brightness": 1, + "brightness_updated_at": 1458628651.862995, + "color_model": "hsb", + "color_model_updated_at": 1458628651.862995, + "hue": 0.35, + "hue_updated_at": 1458628651.862995, + "saturation": 1, + "saturation_updated_at": 1458628651.862995, + "color_temperature": 1901, + "color_temperature_updated_at": 1458628651.862995, + "firmware_version": "0.1b02 / 0.3b22", + "firmware_version_updated_at": 1458628651.862995, + "firmware_date_code": "20150929N****", + "firmware_date_code_updated_at": 1458628651.862995, + "desired_powered_updated_at": 1458628650.8619466, + "desired_brightness_updated_at": 1458628820.0301423, + "desired_color_model_updated_at": 1458628820.0301423, + "desired_hue_updated_at": 1458628820.0301423, + "desired_saturation_updated_at": 1458628820.0301423, + "desired_color_temperature_updated_at": 1458628820.0301423, + "powered_changed_at": 1458628650.8134031, + "brightness_changed_at": 1458122238.7788615, + "connection_changed_at": 1457517588.4372394, + "desired_powered_changed_at": 1458628650.8619466, + "desired_brightness_changed_at": 1458628381.8566465, + "firmware_date_code_changed_at": 1457521561.0603704, + "color_model_changed_at": 1457521797.6389458, + "hue_changed_at": 1457595786.5472758, + "saturation_changed_at": 1457595782.71269, + "color_temperature_changed_at": 1457521786.911106, + "firmware_version_changed_at": 1457521561.0603704, + "desired_color_model_changed_at": 1458620834.569744, + "desired_hue_changed_at": 1457595786.605935, + "desired_saturation_changed_at": 1457595782.8273423, + "desired_color_temperature_changed_at": 1457521921.4747667 + }, + "light_bulb_id": "1515274", + "name": "Bat Signal", + "locale": "en_us", + "units": { + }, + "created_at": 1457517586, + "hidden_at": null, + "capabilities": { + "fields": [ + { + "field": "connection", + "type": "boolean", + "mutability": "read-only" + }, + { + "field": "powered", + "type": "boolean", + "mutability": "read-write" + }, + { + "field": "brightness", + "type": "percentage", + "mutability": "read-write" + }, + { + "field": "color_model", + "type": "string", + "choices": [ + "rgb", + "hsb" + ] + }, + { + "field": "hue", + "type": "percentage", + "mutability": "read-write" + }, + { + "field": "saturation", + "type": "percentage", + "mutability": "read-write" + } + ], + "color_changeable": true + }, + "triggers": [], + "manufacturer_device_model": "sylvania_sylvania_rgbw", + "manufacturer_device_id": null, + "device_manufacturer": "sylvania", + "model_name": "Lightify RGBW Bulb", + "upc_id": "509", + "upc_code": "4613573703", + "gang_id": null, + "hub_id": "381678", + "local_id": "37", + "radio_type": "zigbee", + "linked_service_id": null, + "lat_lng": [ + null, + null + ], + "location": "", + "order": 0 + } + ] +} diff --git a/src/pywink/test/standard/api_responses/temperature_present.json b/src/pywink/test/standard/api_responses/temperature_present.json new file mode 100644 index 0000000..ddc53a1 --- /dev/null +++ b/src/pywink/test/standard/api_responses/temperature_present.json @@ -0,0 +1,128 @@ +{ + "data": [ + { + "uuid": "238539e2-1ad6-44ba-bc53-33c684c36e1d", + "desired_state": { + "powered": false, + "brightness": 1, + "color_model": "hsb", + "hue": 0.35, + "saturation": 1, + "color_temperature": 1901 + }, + "last_reading": { + "connection": true, + "connection_updated_at": 1458628651.862995, + "powered": false, + "powered_updated_at": 1458628651.862995, + "brightness": 1, + "brightness_updated_at": 1458628651.862995, + "color_model": "hsb", + "color_model_updated_at": 1458628651.862995, + "hue": 0.35, + "hue_updated_at": 1458628651.862995, + "saturation": 1, + "saturation_updated_at": 1458628651.862995, + "color_temperature": 1901, + "color_temperature_updated_at": 1458628651.862995, + "firmware_version": "0.1b02 / 0.3b22", + "firmware_version_updated_at": 1458628651.862995, + "firmware_date_code": "20150929N****", + "firmware_date_code_updated_at": 1458628651.862995, + "desired_powered_updated_at": 1458628650.8619466, + "desired_brightness_updated_at": 1458628820.0301423, + "desired_color_model_updated_at": 1458628820.0301423, + "desired_hue_updated_at": 1458628820.0301423, + "desired_saturation_updated_at": 1458628820.0301423, + "desired_color_temperature_updated_at": 1458628820.0301423, + "powered_changed_at": 1458628650.8134031, + "brightness_changed_at": 1458122238.7788615, + "connection_changed_at": 1457517588.4372394, + "desired_powered_changed_at": 1458628650.8619466, + "desired_brightness_changed_at": 1458628381.8566465, + "firmware_date_code_changed_at": 1457521561.0603704, + "color_model_changed_at": 1457521797.6389458, + "hue_changed_at": 1457595786.5472758, + "saturation_changed_at": 1457595782.71269, + "color_temperature_changed_at": 1457521786.911106, + "firmware_version_changed_at": 1457521561.0603704, + "desired_color_model_changed_at": 1458620834.569744, + "desired_hue_changed_at": 1457595786.605935, + "desired_saturation_changed_at": 1457595782.8273423, + "desired_color_temperature_changed_at": 1457521921.4747667 + }, + "light_bulb_id": "1515274", + "name": "Bat Signal", + "locale": "en_us", + "units": { + }, + "created_at": 1457517586, + "hidden_at": null, + "capabilities": { + "fields": [ + { + "field": "connection", + "type": "boolean", + "mutability": "read-only" + }, + { + "field": "powered", + "type": "boolean", + "mutability": "read-write" + }, + { + "field": "brightness", + "type": "percentage", + "mutability": "read-write" + }, + { + "field": "color_model", + "type": "string", + "choices": [ + "rgb", + "hsb" + ] + }, + { + "field": "hue", + "type": "percentage", + "mutability": "read-write" + }, + { + "field": "saturation", + "type": "percentage", + "mutability": "read-write" + }, + { + "field": "color_temperature", + "range": [ + 1900, + 6500 + ], + "type": "integer", + "mutability": "read-write" + } + ], + "color_changeable": true + }, + "triggers": [], + "manufacturer_device_model": "sylvania_sylvania_rgbw", + "manufacturer_device_id": null, + "device_manufacturer": "sylvania", + "model_name": "Lightify RGBW Bulb", + "upc_id": "509", + "upc_code": "4613573703", + "gang_id": null, + "hub_id": "381678", + "local_id": "37", + "radio_type": "zigbee", + "linked_service_id": null, + "lat_lng": [ + null, + null + ], + "location": "", + "order": 0 + } + ] +} diff --git a/src/pywink/test/standard/bulb_test.py b/src/pywink/test/standard/bulb_test.py new file mode 100644 index 0000000..66687b4 --- /dev/null +++ b/src/pywink/test/standard/bulb_test.py @@ -0,0 +1,146 @@ +import json +import os +import unittest + +import mock + +from pywink.api import get_devices_from_response_dict, WinkApiInterface +from pywink.devices import types as device_types +from pywink.devices.standard import WinkBulb +from pywink.devices.types import DEVICE_ID_KEYS + + +class BulbSupportsHueSaturationTest(unittest.TestCase): + + def test_should_be_true_if_response_contains_hue_and_saturation_capabilities(self): + with open('{}/api_responses/hue_and_saturation_present.json'.format(os.path.dirname(__file__))) as light_file: + response_dict = json.load(light_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.LIGHT_BULB]) + + bulb = devices[0] + """ :type bulb: pywink.devices.standard.WinkBulb """ + supports_hs = bulb._bulb_supports_hue_saturation() + self.assertTrue(supports_hs, + msg="Expected hue and saturation to be supported") + + def test_should_be_false_if_response_does_not_contain_hue_and_saturation_capabilities(self): + with open('{}/api_responses/hue_and_saturation_absent.json'.format(os.path.dirname(__file__))) as light_file: + response_dict = json.load(light_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.LIGHT_BULB]) + + bulb = devices[0] + """ :type bulb: pywink.devices.standard.WinkBulb """ + supports_hs = bulb._bulb_supports_hue_saturation() + self.assertFalse(supports_hs, + msg="Expected hue and saturation to be supported") + + +class BulbSupportsTemperatureTest(unittest.TestCase): + + def test_should_be_true_if_response_contains_temperature_capabilities(self): + with open('{}/api_responses/temperature_present.json'.format(os.path.dirname(__file__))) as light_file: + response_dict = json.load(light_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.LIGHT_BULB]) + + bulb = devices[0] + """ :type bulb: pywink.devices.standard.WinkBulb """ + supports_temperature = bulb._bulb_supports_temperature() + self.assertTrue(supports_temperature, + msg="Expected temperature to be supported") + + + def test_should_be_false_if_response_does_not_contain_temperature_capabilities(self): + with open('{}/api_responses/temperature_absent.json'.format(os.path.dirname(__file__))) as light_file: + response_dict = json.load(light_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.LIGHT_BULB]) + + bulb = devices[0] + """ :type bulb: pywink.devices.standard.WinkBulb """ + supports_temperature = bulb._bulb_supports_temperature() + self.assertFalse(supports_temperature, + msg="Expected temperature to be un-supported") + + +class LightTests(unittest.TestCase): + + def setUp(self): + super(LightTests, self).setUp() + self.api_interface = WinkApiInterface() + + def test_should_handle_light_bulb_response(self): + with open('{}/api_responses/light_bulb.json'.format(os.path.dirname(__file__))) as light_file: + response_dict = json.load(light_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.LIGHT_BULB]) + self.assertEqual(1, len(devices)) + self.assertIsInstance(devices[0], WinkBulb) + + @mock.patch('requests.put') + def test_should_send_correct_color_hsb_values_to_wink_api(self, put_mock): + bulb = WinkBulb({ + "capabilities": { + "fields": [ + { + "field": "hue" + }, + { + "field": "saturation" + } + ], + "color_changeable": True + } + }, self.api_interface) + hue = 0.75 + saturation = 0.25 + bulb.set_state(True, color_hue_saturation=[hue, saturation]) + sent_data = json.loads(put_mock.call_args[1].get('data')) + self.assertEquals(hue, sent_data.get('desired_state', {}).get('hue')) + self.assertEquals(saturation, sent_data.get('desired_state', {}).get('saturation')) + self.assertEquals('hsb', sent_data['desired_state'].get('color_model')) + + @mock.patch('requests.put') + def test_should_send_correct_color_temperature_values_to_wink_api(self, put_mock): + bulb = WinkBulb({ + "capabilities": { + "fields": [ + { + "field": "color_temperature" + } + ], + "color_changeable": True + } + }, self.api_interface) + arbitrary_kelvin_color = 4950 + bulb.set_state(True, color_kelvin=arbitrary_kelvin_color) + sent_data = json.loads(put_mock.call_args[1].get('data')) + self.assertEquals('color_temperature', sent_data['desired_state'].get('color_model')) + self.assertEquals(arbitrary_kelvin_color, sent_data['desired_state'].get('color_temperature')) + + @mock.patch('requests.put') + def test_should_only_send_color_hsb_if_both_color_hsb_and_color_temperature_are_given(self, put_mock): + bulb = WinkBulb({ + "capabilities": { + "fields": [ + { + "field": "hue" + }, + { + "field": "saturation" + } + ], + "color_changeable": True + } + }, self.api_interface) + arbitrary_kelvin_color = 4950 + bulb.set_state(True, color_kelvin=arbitrary_kelvin_color, color_hue_saturation=[0, 1]) + sent_data = json.loads(put_mock.call_args[1].get('data')) + self.assertEquals('hsb', sent_data['desired_state'].get('color_model')) + self.assertNotIn('color_temperature', sent_data['desired_state']) + + def test_device_id_should_be_number(self): + with open('{}/api_responses/light_bulb.json'.format(os.path.dirname(__file__))) as light_file: + response_dict = json.load(light_file) + light = response_dict.get('data')[0] + wink_light = WinkBulb(light, self.api_interface) + device_id = wink_light.device_id() + self.assertRegex(device_id, "^[0-9]{4,6}$") + diff --git a/src/setup.py b/src/setup.py index c152ae1..3447a49 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='0.6.4', + version='0.7.0', description='Access Wink devices via the Wink API', url='http://github.com/bradsk88/python-wink', author='Brad Johnson', From 3eebae1caf4bb3adcc0797489177f3588acdc7f7 Mon Sep 17 00:00:00 2001 From: bradsk88 <7kcwKg60tdcW> Date: Sun, 27 Mar 2016 21:23:34 -0600 Subject: [PATCH 051/178] Exposed bulb color support methods E.g. supports_hue_saturation() --- CHANGELOG.md | 3 +++ src/pywink/devices/standard/bulb.py | 16 ++++++++-------- src/pywink/test/standard/bulb_test.py | 8 ++++---- src/setup.py | 2 +- 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20f80e0..ba84d8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 0.7.1 +- Exposed bulb color support methods (E.g. supports_hue_saturation()) + ## 0.7.0 - Expanded color support for WinkBulbs - Added ability to supply client_id, client_secret, and refresh_token diff --git a/src/pywink/devices/standard/bulb.py b/src/pywink/devices/standard/bulb.py index 0012c24..4f191f9 100644 --- a/src/pywink/devices/standard/bulb.py +++ b/src/pywink/devices/standard/bulb.py @@ -93,7 +93,7 @@ def _format_color_data(self, color_hue_saturation, color_kelvin, color_xy, brigh "brightness": brightness } - elif self._bulb_supports_rgb(): + elif self.supports_rgb(): rgb = _get_color_as_rgb(color_hue_saturation, color_kelvin, color_xy, brightness) if rgb: return { @@ -104,21 +104,21 @@ def _format_color_data(self, color_hue_saturation, color_kelvin, color_xy, brigh } # TODO: Find out if this is the correct format - if color_hue_saturation is None and color_kelvin is not None and self._bulb_supports_temperature(): + if color_hue_saturation is None and color_kelvin is not None and self.supports_temperature(): return _format_temperature(color_kelvin, brightness) - if self._bulb_supports_hue_saturation(): + if self.supports_hue_saturation(): hsv = _get_color_as_hue_saturation_brightness(color_hue_saturation, brightness, color_kelvin, color_xy) if hsv is not None: return _format_hue_saturation_brightness(hsv) - if self._bulb_supports_xy_color(): + if self.supports_xy_color(): if color_xy is not None: return _format_xy(color_xy) return {} - def _bulb_supports_rgb(self): + def supports_rgb(self): capabilities = self._last_reading.get('capabilities', {}) if not capabilities.get('color_changeable', False): return False @@ -126,7 +126,7 @@ def _bulb_supports_rgb(self): # TODO: Do any wink bulbs support RGB specification? return False - def _bulb_supports_hue_saturation(self): + def supports_hue_saturation(self): capabilities = self.json_state.get('capabilities', {}) if not capabilities.get('color_changeable', False): return False @@ -144,11 +144,11 @@ def _bulb_supports_hue_saturation(self): if supports_hue and supports_saturation: return True - def _bulb_supports_xy_color(self): + def supports_xy_color(self): # TODO: Do any wink bulbs support XY color? return self.json_state.get("todo", False) - def _bulb_supports_temperature(self): + def supports_temperature(self): capabilities = self.json_state.get('capabilities', {}) if not capabilities.get('color_changeable', False): return False diff --git a/src/pywink/test/standard/bulb_test.py b/src/pywink/test/standard/bulb_test.py index 66687b4..525b28a 100644 --- a/src/pywink/test/standard/bulb_test.py +++ b/src/pywink/test/standard/bulb_test.py @@ -19,7 +19,7 @@ def test_should_be_true_if_response_contains_hue_and_saturation_capabilities(sel bulb = devices[0] """ :type bulb: pywink.devices.standard.WinkBulb """ - supports_hs = bulb._bulb_supports_hue_saturation() + supports_hs = bulb.supports_hue_saturation() self.assertTrue(supports_hs, msg="Expected hue and saturation to be supported") @@ -30,7 +30,7 @@ def test_should_be_false_if_response_does_not_contain_hue_and_saturation_capabil bulb = devices[0] """ :type bulb: pywink.devices.standard.WinkBulb """ - supports_hs = bulb._bulb_supports_hue_saturation() + supports_hs = bulb.supports_hue_saturation() self.assertFalse(supports_hs, msg="Expected hue and saturation to be supported") @@ -44,7 +44,7 @@ def test_should_be_true_if_response_contains_temperature_capabilities(self): bulb = devices[0] """ :type bulb: pywink.devices.standard.WinkBulb """ - supports_temperature = bulb._bulb_supports_temperature() + supports_temperature = bulb.supports_temperature() self.assertTrue(supports_temperature, msg="Expected temperature to be supported") @@ -56,7 +56,7 @@ def test_should_be_false_if_response_does_not_contain_temperature_capabilities(s bulb = devices[0] """ :type bulb: pywink.devices.standard.WinkBulb """ - supports_temperature = bulb._bulb_supports_temperature() + supports_temperature = bulb.supports_temperature() self.assertFalse(supports_temperature, msg="Expected temperature to be un-supported") diff --git a/src/setup.py b/src/setup.py index 3447a49..055b875 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='0.7.0', + version='0.7.1', description='Access Wink devices via the Wink API', url='http://github.com/bradsk88/python-wink', author='Brad Johnson', From 770829d99363f53c89ddd9bbfc3e1ebf2b6a83cd Mon Sep 17 00:00:00 2001 From: bradsk88 <7kcwKg60tdcW> Date: Sun, 27 Mar 2016 22:06:21 -0600 Subject: [PATCH 052/178] Conserve Brighness on Color Temp Change Currently, if color_kelvin is Currently, if color temperature is supplied to ``` set_state ``` without also supplying a value for ``` brightness ```, the brightness will be set to 1.0 (full). This commit fixes that. --- .gitignore | 1 + CHANGELOG.md | 3 ++ src/pywink/color.py | 5 +- src/pywink/devices/standard/bulb.py | 49 ++++++++++--------- src/pywink/test/standard/bulb_test.py | 70 +++++++++++++++++++++++++++ src/setup.py | 2 +- 6 files changed, 104 insertions(+), 26 deletions(-) diff --git a/.gitignore b/.gitignore index 45eff3f..0a76e55 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ *.py[cod] /test.py /.cache +/src/python_wink.egg-info diff --git a/CHANGELOG.md b/CHANGELOG.md index ba84d8a..c0fe3f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 0.7.2 +- Conserving brightness when setting color (temperature, hue sat, etc.) + ## 0.7.1 - Exposed bulb color support methods (E.g. supports_hue_saturation()) diff --git a/src/pywink/color.py b/src/pywink/color.py index 8b5c9fd..3424690 100644 --- a/src/pywink/color.py +++ b/src/pywink/color.py @@ -68,7 +68,10 @@ def color_temperature_to_rgb(color_temperature_kelvin): # Copyright (c) 2014 Benjamin Knight / MIT License. # pylint: disable=bad-builtin def color_xy_brightness_to_rgb(vX, vY, brightness): - """Convert from XYZ to RGB.""" + """ + Convert from XYZ to RGB. + :rtype: tuple of int + """ brightness /= 255. if brightness == 0: return (0, 0, 0) diff --git a/src/pywink/devices/standard/bulb.py b/src/pywink/devices/standard/bulb.py index 4f191f9..905eb6c 100644 --- a/src/pywink/devices/standard/bulb.py +++ b/src/pywink/devices/standard/bulb.py @@ -77,9 +77,14 @@ def set_state(self, state, brightness=None, """ desired_state = {"powered": state} - color_state = self._format_color_data(color_hue_saturation, color_kelvin, color_xy, brightness or 1) + color_state = self._format_color_data(color_hue_saturation, color_kelvin, color_xy) desired_state.update(color_state) + brightness = brightness if brightness is not None else self.json_state.get('brightness', 1) + desired_state.update({ + 'brightness': brightness + }) + response = self.api_interface.set_device_state(self, { "desired_state": desired_state }) @@ -87,14 +92,12 @@ def set_state(self, state, brightness=None, self._last_call = (time.time(), state) - def _format_color_data(self, color_hue_saturation, color_kelvin, color_xy, brightness): - if brightness and color_hue_saturation is None and color_kelvin is None and color_xy is None: - return { - "brightness": brightness - } + def _format_color_data(self, color_hue_saturation, color_kelvin, color_xy): + if color_hue_saturation is None and color_kelvin is None and color_xy is None: + return {} - elif self.supports_rgb(): - rgb = _get_color_as_rgb(color_hue_saturation, color_kelvin, color_xy, brightness) + if self.supports_rgb(): + rgb = _get_color_as_rgb(color_hue_saturation, color_kelvin, color_xy) if rgb: return { "color_model": "rgb", @@ -105,12 +108,12 @@ def _format_color_data(self, color_hue_saturation, color_kelvin, color_xy, brigh # TODO: Find out if this is the correct format if color_hue_saturation is None and color_kelvin is not None and self.supports_temperature(): - return _format_temperature(color_kelvin, brightness) + return _format_temperature(color_kelvin) if self.supports_hue_saturation(): - hsv = _get_color_as_hue_saturation_brightness(color_hue_saturation, brightness, color_kelvin, color_xy) + hsv = _get_color_as_hue_saturation_brightness(color_hue_saturation, color_kelvin, color_xy) if hsv is not None: - return _format_hue_saturation_brightness(hsv) + return _format_hue_saturation(hsv) if self.supports_xy_color(): if color_xy is not None: @@ -164,21 +167,19 @@ def __repr__(self): self.name(), self.device_id(), self.state()) -def _format_temperature(kelvin, brightness): +def _format_temperature(kelvin): return { "color_model": "color_temperature", "color_temperature": kelvin, - "brightness": brightness } -def _format_hue_saturation_brightness(hue_saturation_brightness): - hsv_iter = iter(hue_saturation_brightness) +def _format_hue_saturation(hue_saturation): + hsv_iter = iter(hue_saturation) return { "color_model": "hsb", "hue": next(hsv_iter), "saturation": next(hsv_iter), - "brightness": next(hsv_iter) } @@ -191,22 +192,22 @@ def _format_xy(xy): } -def _get_color_as_rgb(hue_sat, brightness, kelvin, xy): - if hue_sat is not None and brightness is not None: - h, s, v = colorsys.hsv_to_rgb(hue_sat[0], hue_sat[1], brightness) +def _get_color_as_rgb(hue_sat, kelvin, xy): + if hue_sat is not None: + h, s, v = colorsys.hsv_to_rgb(hue_sat[0], hue_sat[1], 1) return tuple(h, s, v) if kelvin is not None: return color_temperature_to_rgb(kelvin) if xy is not None: - return color_xy_brightness_to_rgb(xy[0], xy[1], brightness) + return color_xy_brightness_to_rgb(xy[0], xy[1], 1) return None -def _get_color_as_hue_saturation_brightness(hue_sat, brightness, kelvin, xy): - if hue_sat and brightness: +def _get_color_as_hue_saturation_brightness(hue_sat, kelvin, xy): + if hue_sat: color_hs_iter = iter(hue_sat) - return (next(color_hs_iter), next(color_hs_iter), brightness) - rgb = _get_color_as_rgb(None, brightness, kelvin, xy) + return (next(color_hs_iter), next(color_hs_iter), 1) + rgb = _get_color_as_rgb(None, kelvin, xy) if not rgb: return None h, s, v = colorsys.rgb_to_hsv(rgb[0], rgb[1], rgb[2]) diff --git a/src/pywink/test/standard/bulb_test.py b/src/pywink/test/standard/bulb_test.py index 525b28a..53daef8 100644 --- a/src/pywink/test/standard/bulb_test.py +++ b/src/pywink/test/standard/bulb_test.py @@ -61,6 +61,76 @@ def test_should_be_false_if_response_does_not_contain_temperature_capabilities(s msg="Expected temperature to be un-supported") +class SetStateTests(unittest.TestCase): + + def setUp(self): + super(SetStateTests, self).setUp() + self.api_interface = mock.Mock() + + def test_should_send_current_brightness_to_api_if_only_color_temperature_is_provided_and_bulb_only_supports_temperature(self): + original_brightness = 0.5 + bulb = WinkBulb({ + 'brightness': original_brightness, + 'capabilities': { + 'color_changeable': True, + 'fields': [{ + 'field': 'color_temperature' + }] + } + }, self.api_interface) + bulb.set_state(True, color_kelvin=4000) + set_state_mock = self.api_interface.set_device_state + sent_desired_state = set_state_mock.call_args[0][1]['desired_state'] + self.assertEquals(original_brightness, sent_desired_state.get('brightness')) + + def test_should_send_color_temperature_to_api_if_color_temp_is_provided_and_bulb_only_supports_temperature(self): + bulb = WinkBulb({ + 'capabilities': { + 'color_changeable': True, + 'fields': [{ + 'field': 'color_temperature' + }] + } + }, self.api_interface) + color_kelvin = 4000 + bulb.set_state(True, color_kelvin=color_kelvin) + set_state_mock = self.api_interface.set_device_state + sent_desired_state = set_state_mock.call_args[0][1]['desired_state'] + self.assertEquals(color_kelvin, sent_desired_state.get('color_temperature')) + + def test_should_send_current_brightness_to_api_if_only_color_temperature_is_provided_and_bulb_only_supports_hue_sat( + self): + original_brightness = 0.5 + bulb = WinkBulb({ + 'brightness': original_brightness, + 'capabilities': { + 'color_changeable': True, + 'fields': [{'field': 'hue'}, + {'field': 'saturation'}] + } + }, self.api_interface) + bulb.set_state(True, color_kelvin=4000) + set_state_mock = self.api_interface.set_device_state + sent_desired_state = set_state_mock.call_args[0][1]['desired_state'] + self.assertEquals(original_brightness, sent_desired_state.get('brightness')) + + def test_should_send_current_hue_and_saturation_to_api_if_hue_and_sat_are_provided_and_bulb_only_supports_hue_sat(self): + bulb = WinkBulb({ + 'capabilities': { + 'color_changeable': True, + 'fields': [{'field': 'hue'}, + {'field': 'saturation'}] + } + }, self.api_interface) + hue = 0.2 + saturation = 0.3 + bulb.set_state(True, color_hue_saturation=[hue, saturation]) + set_state_mock = self.api_interface.set_device_state + sent_desired_state = set_state_mock.call_args[0][1]['desired_state'] + self.assertEquals(hue, sent_desired_state.get('hue')) + self.assertEquals(saturation, sent_desired_state.get('saturation')) + + class LightTests(unittest.TestCase): def setUp(self): diff --git a/src/setup.py b/src/setup.py index 055b875..4dc25b2 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='0.7.1', + version='0.7.2', description='Access Wink devices via the Wink API', url='http://github.com/bradsk88/python-wink', author='Brad Johnson', From ebf7f79a2ef4a362909581874226c4e0d91eaeda Mon Sep 17 00:00:00 2001 From: bradsk88 <7kcwKg60tdcW> Date: Tue, 29 Mar 2016 22:31:50 -0600 Subject: [PATCH 053/178] Can now require desired_state to have been reached before updating state --- CHANGELOG.md | 3 + src/pywink/devices/base.py | 14 +- src/pywink/devices/sensors.py | 5 +- src/pywink/devices/standard/__init__.py | 20 +- src/pywink/domain/__init__.py | 6 + src/pywink/domain/devices.py | 10 + src/pywink/test/api_responses/winkapiv2.txt | 2044 ----------------- .../test/{standard => devices}/__init__.py | 0 src/pywink/test/devices/standard/__init__.py | 0 .../api_responses/binary_sensor.json | 0 .../api_responses/binary_switch.json | 0 .../standard}/api_responses/eggtray.json | 0 .../standard}/api_responses/garage_door.json | 0 .../hue_and_saturation_absent.json | 0 .../hue_and_saturation_present.json | 0 .../standard/api_responses/light_bulb.json | 0 .../standard}/api_responses/lock.json | 0 .../standard}/api_responses/power_strip.json | 0 .../api_responses/quirky_spotter.json | 0 .../api_responses/quirky_spotter_2.json | 0 .../standard}/api_responses/siren.json | 0 .../api_responses/temperature_absent.json | 0 .../api_responses/temperature_present.json | 0 .../test/{ => devices}/standard/bulb_test.py | 15 + .../test/{ => devices/standard}/init_test.py | 0 src/pywink/test/domain/__init__.py | 0 src/pywink/test/domain/devices_test.py | 91 + src/setup.py | 4 +- 28 files changed, 149 insertions(+), 2063 deletions(-) create mode 100644 src/pywink/domain/__init__.py create mode 100644 src/pywink/domain/devices.py delete mode 100644 src/pywink/test/api_responses/winkapiv2.txt rename src/pywink/test/{standard => devices}/__init__.py (100%) create mode 100644 src/pywink/test/devices/standard/__init__.py rename src/pywink/test/{ => devices/standard}/api_responses/binary_sensor.json (100%) rename src/pywink/test/{ => devices/standard}/api_responses/binary_switch.json (100%) rename src/pywink/test/{ => devices/standard}/api_responses/eggtray.json (100%) rename src/pywink/test/{ => devices/standard}/api_responses/garage_door.json (100%) rename src/pywink/test/{ => devices}/standard/api_responses/hue_and_saturation_absent.json (100%) rename src/pywink/test/{ => devices}/standard/api_responses/hue_and_saturation_present.json (100%) rename src/pywink/test/{ => devices}/standard/api_responses/light_bulb.json (100%) rename src/pywink/test/{ => devices/standard}/api_responses/lock.json (100%) rename src/pywink/test/{ => devices/standard}/api_responses/power_strip.json (100%) rename src/pywink/test/{ => devices/standard}/api_responses/quirky_spotter.json (100%) rename src/pywink/test/{ => devices/standard}/api_responses/quirky_spotter_2.json (100%) rename src/pywink/test/{ => devices/standard}/api_responses/siren.json (100%) rename src/pywink/test/{ => devices}/standard/api_responses/temperature_absent.json (100%) rename src/pywink/test/{ => devices}/standard/api_responses/temperature_present.json (100%) rename src/pywink/test/{ => devices}/standard/bulb_test.py (92%) rename src/pywink/test/{ => devices/standard}/init_test.py (100%) create mode 100644 src/pywink/test/domain/__init__.py create mode 100644 src/pywink/test/domain/devices_test.py diff --git a/CHANGELOG.md b/CHANGELOG.md index c0fe3f5..61416b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 0.7.3 +- Can now require desired_state to have been reached before updating state + ## 0.7.2 - Conserving brightness when setting color (temperature, hue sat, etc.) diff --git a/src/pywink/devices/base.py b/src/pywink/devices/base.py index e24e67b..bf1642d 100644 --- a/src/pywink/devices/base.py +++ b/src/pywink/devices/base.py @@ -1,3 +1,5 @@ +from pywink.domain.devices import is_desired_state_reached + class WinkDevice(object): @@ -35,14 +37,18 @@ def _last_reading(self): def available(self): return self._last_reading.get('connection', False) - def _update_state_from_response(self, response_json): + def _update_state_from_response(self, response_json, require_desired_state_fulfilled=False): """ :param response_json: the json obj returned from query :return: """ - self.json_state = response_json.get('data') + response_json = response_json.get('data') + if response_json and require_desired_state_fulfilled: + if not is_desired_state_reached(response_json[0]): + return + self.json_state = response_json - def update_state(self): + def update_state(self, require_desired_state_fulfilled=False): """ Update state with latest info from Wink API. """ response = self.api_interface.get_device_state(self) - self._update_state_from_response(response) + self._update_state_from_response(response, require_desired_state_fulfilled) diff --git a/src/pywink/devices/sensors.py b/src/pywink/devices/sensors.py index a9392e7..cf0c08c 100644 --- a/src/pywink/devices/sensors.py +++ b/src/pywink/devices/sensors.py @@ -35,11 +35,12 @@ def device_id(self): root_name = self.json_state.get('sensor_pod_id', self.name()) return '{}+{}'.format(root_name, self._capability) - def update_state(self): + def update_state(self, require_desired_state_fulfilled=False): """ Update state with latest info from Wink API. """ root_name = self.json_state.get('sensor_pod_id', self.name()) response = self.api_interface.get_device_state(self, root_name) - self._update_state_from_response(response) + self._update_state_from_response(response, + require_desired_state_fulfilled=require_desired_state_fulfilled) class WinkSensorPod(_WinkCapabilitySensor): diff --git a/src/pywink/devices/standard/__init__.py b/src/pywink/devices/standard/__init__.py index 0feb6d1..eae197c 100644 --- a/src/pywink/devices/standard/__init__.py +++ b/src/pywink/devices/standard/__init__.py @@ -6,6 +6,7 @@ from pywink.devices.base import WinkDevice from pywink.devices.standard.base import WinkBinarySwitch from pywink.devices.standard.bulb import WinkBulb +from pywink.domain.devices import is_desired_state_reached class WinkEggTray(WinkDevice): @@ -120,12 +121,14 @@ def __repr__(self): 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') + def update_state(self, require_desired_state_fulfilled=False): + """ Update state with latest info from Wink API. """ + response = self.api_interface.get_device_state(self, id_override=self.parent_id()) + power_strip = response.get('data') + if require_desired_state_fulfilled: + if not is_desired_state_reached(power_strip[0]): + return + power_strip_reading = power_strip.get('last_reading') outlets = power_strip.get('outlets', power_strip) for outlet in outlets: @@ -133,11 +136,6 @@ def _update_state_from_response(self, response_json): 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. """ - response = self.api_interface.get_device_state(self, id_override=self.parent_id()) - self._update_state_from_response(response) - def index(self): return self.json_state.get('outlet_index', None) diff --git a/src/pywink/domain/__init__.py b/src/pywink/domain/__init__.py new file mode 100644 index 0000000..ece52c2 --- /dev/null +++ b/src/pywink/domain/__init__.py @@ -0,0 +1,6 @@ +""" +Helper classes and functions for python-wink entities. + +Any behaviour which is not directly related to storing device state or +interfacing with the Wink API should live in here. +""" diff --git a/src/pywink/domain/devices.py b/src/pywink/domain/devices.py new file mode 100644 index 0000000..336b369 --- /dev/null +++ b/src/pywink/domain/devices.py @@ -0,0 +1,10 @@ +def is_desired_state_reached(wink_device_state): + """ + :type wink_device: dict + """ + desired_state = wink_device_state.get('desired_state', {}) + for name, value in desired_state.items(): + if value != wink_device_state.get(name): + return False + + return True diff --git a/src/pywink/test/api_responses/winkapiv2.txt b/src/pywink/test/api_responses/winkapiv2.txt deleted file mode 100644 index f031014..0000000 --- a/src/pywink/test/api_responses/winkapiv2.txt +++ /dev/null @@ -1,2044 +0,0 @@ -FORMAT: 1A - -HOST: https://api.wink.com - -# Wink API -The Wink API connects Wink devices to users, apps, each other, and the wider web. - -NOTE: This is the documentation for the v2 version of the Wink API. It has breaking changes on the [original API](http://docs.wink.apiary.io/) published in 2013. If you are a user of the original API but would like access to this version, please contact Wink Support to ask for a new set of credentials. - - -# Group A RESTful Service -The Wink API is a [RESTful](http://en.wikipedia.org/wiki/Representational_state_transfer) service. - -## Authentication -Nearly every request to the Wink API requires an OAuth bearer token. - -Exceptions to this rule will be documented. - -## Content types -Nearly every request to the Wink API should be expressed as JSON. - -Nearly every response from the Wink API will be expressed as JSON. - -Exceptions to this rule will be documented. - -## HTTP verbs -The Wink API uses HTTP verbs in pretty standard ways: - -- GET for retrieving information without side-effects -- PUT for updating existing resources, with partial-update semantics supported -- POST for creating new resources or blind upserts of existing resources -- DELETE for destructive operations on existing resurces - -## Identifiers -All objects in the Wink API can be identified by `object_type` and `object_id`. The `object_id` is a string and not globally unique, currently. That is there can be an `'object_type':'light_bulb'` and `'object_id':'abc'` and an `'object_type':'thermostat'` and `'object_id':'abc'` - -It is possible for the API to re-assign identifiers to resources to rebalance keys; in this case, your resource will still exist but it (and all references to it) will be updated to the new identifier. Your application should be able to handle this case. - -## Creatable vs. permanent -The term "creatable" will describe a resource which may be created and/or destroyed by the user. - -The term "permanent" will describe a resource which may not be directly created or deleted by a user. - -Note that permanent **does not imply** that the resource will always exist, just that the user may not create or destroy it. Under no circumstances should you assume that a resource will always exist. - -## Mutable vs. immutable -The term "mutable" will describe a resource or attribute which the user may modify at will, assuming the user has the necessary permissions to do so. - -The term "immutable" will describe a resouce or attribute which may not be modified directly by the user. - -Note that immutable **does not imply** that the resource or attribute will never change, just that the user may not directly change it. Under no circumstances should you assume that a resource or attribute will always remain the same. - -## Error states -The common [HTTP Response Status Codes](https://github.com/for-GET/know-your-http-well/blob/master/status-codes.md) are used. - -# Group OAuth -Authentication to the API - -## Obtain access token [/oauth2/token] -### Sign in as user, or refresh user's expired access token [POST] - -Note that unlike most other calls, this call does not require (and in fact should not use) an OAuth 2.0 bearer token. - -+ Request Sign in as user (application/json) - - { - "client_id": "consumer_key_goes_here", - "client_secret": "consumer_secret_goes_here", - "username": "user@example.com", - "password": "password_goes_here", - "grant_type": "password" - } - -+ Request Refresh expired access token (application/json) - - { - "client_id": "consumer_key_goes_here", - "client_secret": "consumer_secret_goes_here", - "grant_type": "refresh_token", - "refresh_token": "crazy_token_like_240qhn16hwrnga05euynaoeiyhw52_goes_here" - } - -+ Response 201 (application/json) - - { - "data": { - "access_token": "example_access_token_like_135fhn80w35hynainrsg0q824hyn", - "refresh_token": "crazy_token_like_240qhn16hwrnga05euynaoeiyhw52_goes_here", - "token_type": "bearer" - } - } - - - -# Group Subscriptions - -Real-time updates through the Wink API are managed by [PubNub](https://www.pubnub.com/) - -**Subscriptions are organized around "topics"** - -Subscriptions to topics fall into two categories: Lists and Objects. - -List subscriptions send updates when an object is added or removed from the list. For example, a subscription to `/users/me/wink_devices` would trigger an update when a new device is added to a user account. - -Object subscriptions send updates when an object is updated in any way. For example, a subscription to `/light_bulbs/abc` would trigger an update when a light bulb goes from powered off to powered on. -Several changes may be aggregated into a single broadcast, when the changes have happened in rapid succession. - -To subscribe to a topic, find the subscription object inside the response from a GET request to either a list or an object. - - "subscription": { - "pubnub": { - "subscribe_key": "worghwihr0aijyp5ejhapethnpaethn", - "channel": "w0y8hq03hy5naeorihnse05iyjse5yijsm" - } - } - -# Group Device - -**Common patterns and fields of Wink API Devices** - -Specific fields, particularly in desired_state and last_reading will be outlined in the object-specific sections - -Each Wink Device can have the following attributes, but not all attributes will be populated - -Prepare to receive null for any one of these. For specific implementations, refer to the device documentation of the given device type. - -|API field|Attributes|Description| -|----|----|----| -|object_type|(string, assigned)|type of object (NOTE: legacy apps expect a specific type id such as "light_bulb_id")| -|object_id|(string, assigned)|id of object (NOTE: legacy apps expect a specific type id such as "light_bulb_id")| -|name|(String, writable)|Name of the device, default given by server upon provisioning but can be updated by user| -|locale |(String, format LL_CC -- "en_us", "fr_fr")|Can be updated by user, but not exposed in the app, usually based of user's locale| -|units|(object, specific to device)|See [Units](/Wink_Devices/General/API/Units)| -|created_at|(long, timestamp, immutable)|Time device was added to account| -|subscription|(pubnub subscription object)|See [Subscription](/General/API/Subscriptions)| -|manufacturer_device_model|(String, assigned)|snaked case, unique device model| -|manufatcurer_device_id | (String, assigned | udid of third party device in third party system| -|hub_id|(String, assigned)|id of hub associated with device, only for devices with hub| -|local_id|(String, assigned)| id of device on hub, only for devices with hub| -|radio_type | (String, assigned)|currently only for devices with hub, available values "zigbee", "zwave", "lutron", "wink_project_one"| -|device_manufacturer|(String, assigned)|Human readable display string of manufacturer| -|lat_lng |(tuple of floats, writable)|location of device| -|location|(String, writable)|pretty printable location of device| -|desired_state|(object, values of requested state)| Depends on object type| -|last_reading|(object, values of last reading from device)| Depends on object type| -|capabilities|(object, specific capabilities of this object)| for instance for sensor the last_reading values available| - -## Capabilities - -The API sets capabilities for devices to indicate whether a field is present, whether it is mutable and what are the allowed values. - -The power of capabilities allow for devices with different attributes to have the same object type. An example is using capabilties to distinguish a light bulb that has only has dimming capabilites from a light bulb that has dimming and color changing capabilities. - -Capabilities currently contains one object `fields`, an array object of field capabilities. - -Note: note all devices have capabilities. Some legacy devices added in the first six months of the Wink API have yet to be converted. - -### Field attributes -|API field|Attributes|Description| -|----|----|----| -|field|(string)|name of field| -|type|(string)|one of "boolean", "percentage", "integer", "float", "string"| -|mutability|(string)|one of "read-only", "read-write"| -|choices|(array)|array of allowed choices for field. Nearly always a string array| -|range|(tuple)|tuple of upper and lower bounds for field. For integer and float fields| - - - { - "fields": [{ - "field": "connection", - "type": "boolean", - "mutability": "read-only" - }, { - "field": "powered", - "type": "boolean", - "mutability": "read-write" - }, { - "field": "brightness", - "type": "percentage", - "mutability": "read-write" - }, { - "field": "color_model", - "type": "string", - "choices": ["rgb", "xy", "hsb", "color_temperature"], - "mutability": "read-write" - }, { - "field": "color_temperature", - "type": "integer", - "range": [2000, 6500], - "mutability": "read-write" - }] - } -## Desired State and Last Reading [/{device_type}/{device_id}/desired_state] - -The desired_state/last_reading paradigm is available in almost all wink devices. Fields in desired_state are what the client requests the state of the device to be, whereas fields in last_reading are what the API believes to be the current state of things. To keep requested changes distinct and distinguishable from real changes, each desired_state field has the following counterparts in last_reading - -* `desired_foo` -* `desired_foo_updated_at` -* `foo` -* `foo_updated_at` -* `foo_changed_at` - this differs from update indicating when a reading was altered, in contrast to updated indicating the last time a reading was reported by a device - -When the device acknowledges that the state has been applied, the server will clear the field from desired_state. Then last_reading.x, last_reading.x_updated_at and last_reading.x_changed_at will update appropriately. - -If a device fails to change to the desired_state, likely due to a failure of some kind including the device being offline, the server will give up after 2 minutes and clear desired_state. Only last_reading.x_updated_at would update in that case as there was no change to the actual reading. - -The consequences of this change are two-fold - -- The clients can definitively know if a requested change has not yet been applied or failed to be applied. This means that UIs such as garage door and lock that depend on an applied state do not have to have an arbitrary time out. Or UIs such as the lights UI can display when a request is in progress -- When changes are requested, only those attributes that are actually changed will be applied rather than re-applying an entire desired_state when changing one attribute. - -Throughout the rest of the documentation, writable states will only be documented under desired_state and read only states will be documented under last_reading - -### Update desired state [PUT] - -While you can update a device's desired state with a PUT to the whole device, you can also do a put of just the desired state to this endpoint - -+ Request (application/json) - - + Body - - { - "desired_state": { - "powered": true - } - } - -+ Response 200 (application/json) - - [Wink Devices][] - -## Wink Devices [/users/me/wink_devices] - -+ Model (application/json) - - JSON representation of a device - - + Body - - { - "data":{ - "object_id":"27105", - "object_type":"light_bulb", - "locale":"Lighty Light", - "manufacturer_device_model":"ge_light_bulb", - "manufatcurer_device_id":"123456", - "hub_id":"rtyui", - "local_id":"5", - "radio_type":"zigbee", - "device_manufacturer":"GE", - "subscription": { - "pubnub": { - "subscribe_key": "worghwihr0aijyp5ejhapethnpaethn", - "channel": "w0y8hq03hy5naeorihnse05iyjse5yijsm" - } - } - "locale":"en_us", - "units":{ - }, - "lat_lng":[], - "desired_state":{}, - "last_reading":{}, - "capabilities":{}, - "created_at":1234567890, - }, - "errors":[ - ], - "pagination":{ - } - } - -### Retrieve All Devices of User [GET] - -+ Response 200 (application/json) - - [Wink Devices][] - -## Sharing device [/{device_type}/{device_id}/users] - -**Certain users have permissions to share the device with other users.** - -Only users with "manage_sharing" permissions can share a device, all other permissions are deprecated or underutilized at this time - -+ Model (application/json) - - JSON representation of a shared user - - + Body - - { - "device_id": "qs1ga9_1234deadbeef", //NOTE: the field name will vary depending on device - "user_id": "abc123def-an15nag", - "email": "user@example.com", - "permissions": ["read_data", "write_data", "read_triggers", "write_triggers", "manage_sharing"] - } - -### List shared device users [GET] - -+ Response 200 (application/json) - - [Sharing device][] - - -### Share a device [POST] - -+ Request - - { - "email": "user2@example2.com" - } - -+ Response 200 (application/json) - - [Sharing device][] - -## Unshare a device [/{device_type}/{device_id}/users/{email}] - -### Unshare a device [DELETE] - -+ Response 204 - -## Air Conditioner [/air_conditioners/{device_id}] - -### Get Air Conditioner [GET] - -#### Desired State Attributes - -|API field|Attributes|Description| -|----|----|----| -|fan_speed|float|0 - 1| -|mode|string|"cool_only", "fan_only", "auto_eco"| -|powered|boolean|whether or not the unit is powered on -|max_set_point|float|temperature above which the unit should be cooling| - -#### Last Reading Attributes - -|API field|Attributes|Description| -|----|----|----| -|connection|Boolean|whether or not the device is reachable remotely| -|temperature|float|maps to ambient temperature last read from device itself| -|consumption|float|total consumption in watts| - -## Binary Switch [/binary_switches/{device_id}] - -### Get Binary Switch [GET] - -#### Desired State Attributes - -|API field|Attributes|Description| -|----|----|----| -|powered|boolean|whether device is powered on| -#### Last Reading Attributes - -|API field|Attributes|Description| -|----|----|----| -|connection|Boolean|whether or not the device is reachable remotely| - -## Blind [/shades/{device_id}] - -### Get Blind [GET] - -#### Desired State Attributes - -|API field|Attributes|Description| -|----|----|----| -|position|float|0.0 is completely closed and 1.0 is completely open.| - -#### Last Reading Attributes - -|API field|Attributes|Description| -|----|----|----| -|connection|Boolean|whether or not the device is reachable remotely| - -## Camera [/cameras/{device_id}] - -### Get Camera [GET] - -#### Desired State Attributes - -|API field|Attributes|Description| -|----|----|----| -|capturing_video|boolean|Whether or not the camera is currently capturing video| -|capturing_audio|boolean|Whether or not the camera is currently capturing audio| -|mode|string|one of "armed", "disarmed", "privacy" - -#### Last Reading Attributes - -|API field|Attributes|Description| -|----|----|----| -|motion|Boolean|whether or not the dropcam currently detects movement| -|loudness|Boolean|whether or not the dropcam currently detects sound| -|connection|Boolean|whether or not the device is reachable remotely| - -## Doorbell [/doorbells/{device_id}] - -### Get Doorbell [GET] - -#### Desired State Attributes -n/a - -#### Last Reading Attributes - -|API field|Data Type|Description| -|---|---|---| -|button_pressed|boolean|doorbell button pressed event| -|motion|boolean|motion detected by doorbell event| -|battery|float|battery status| -|connection|boolean|online or offline| - -## Egg Minder [/eggtrays/{device_id}} - -### Get Egg Minder [GET] - -#### Additional API fields on eggtray object - -|API field|Attributes|Description| -|----|----|----| -|freshness_period|(integer, mutable with write_data permission on eggtray device)|[Period during which eggs are defined as fresh in seconds]| -|eggs|(array of 14 integers, assigned, immutable)|[Timestamp in seconds of when each egg was added]| - - { - "eggs": [ - 1377180085, - 1377180086, - 1377180087, - 1377180088, - 1377180089, - 1377180090, - 1377180091, - 1377180092, - 1377180093, - 1377180094, - 1377180095, - 1377180096, - 1377180097, - 1377180098 - ], - "freshness_period": 2419200 - } - -#### Desired State Attributes -n/a - -#### Last Reading Attributes - -|API field|Attributes|Description| -|----|----|----| -|connection|boolean|connection to server| -|battery|float|0.0 - 1.0 battery level| -|inventory|integer|# of eggs| -|freshness_remaining|integer|seconds until oldest egg goes bad| - -## Garage Door [/garage_doors/{device_id}] - -### Get Garage Door [GET] - -#### Desired State Attributes - -|API field|Attributes|Description| -|----|----|----| -|position|float|0 - 1, *while a float, the app should only send up 0 or 1, for security*| -|laser|boolean|turn on/off laser| -|calibration_enabled|boolean|turn on/off calibration mode| - -#### Last Reading Attributes - -|API field|Attributes|Description| -|----|----|----| -|connection|Boolean|whether or not the device is reachable remotely| -|buzzer|boolean|whether or not the buzzer is on| -|led|boolean|whether or not the LED is on| -|moving|boolean|whether or not the garage door is current moving| -|fault|boolean|whether or not there is an error with the garage door| -|disabled|boolean|whether remote control is disabled due to an error| -|error|array|string array of errors| -|control_enabled|boolean|whether or not the unit is capable of remote control| -|controller_error|array,string|errors from the controller unit,putting the garage door into state where remote control is disabled| -|tilt_sensor_error|array,string|whether the tilt sensor has a battery/in range or if that battery is low| - -## Hub [/hubs/{device_id}] - -### Get Hub [GET] - -### Desired State Attributes - -|API field|Attributes|Description| -|----|----|----| -|pairing_mode|(string of radio to enter)|[zigbee, zwave, lutron, zwave_exclusion]| -|kidde_radio_code|(int, assigned, mutable)|[0 - 255, represent 8-bit radio frequency of Wink Hub to look for kidde smoke detector}| - -### Last Reading Attributes - -|API field|Attributes|Description| -|----|----|----| -|connection|Boolean|whether or not the device is reachable remotely| -|remote_pairable|boolean|whether or not a connected Lutron remote is ready for association| -|updating_firmware|boolean|whether the hub is currently updating its firmware| -|firmware_version|string|current hub firmware_version| -|mac_address|string|hub mac address| -|ip_address|string|ip address of hub| -|update_needed|boolean|whether or not the hub needs a firmware update| - -## Light Bulb [/light_bulb/{device_id}] - -### Get Light Bulb [GET] - -#### Desired State Attributes - -|API field|Attributes|Description| -|----|----|----| -|powered|boolean|whether device is powered on| -|brightness|float|0.0 to 1.0, dimness level (binary_switch and light_bulb)| -|color_model|(string)|one of: "xy", "hsb", "color_temperature", or "rgb" | -|color_x|(float, precision 4)|the CIE 1931 coordinates of the bulb's color [0.0, 1.0]| -|color_y|(float, precision 6)|he CIE 1931 coordinates of the bulb's color [0.0, 1.0]| -|hue|(float, precision 6)|the 360-degree value of the bulb's color (normalized to 1.0)| -|saturation|(float, precision 6)|the percentage value of the bulb's saturation (normalized to 1.0) [0.0, 1.0]| -|color_temperature|(integer)|the Kelvin value of the bulb's color [2000 .. 6500]| -|color|(string)|the hexadecimal value of the bulb color (without a leading '#')| -|powering_mode|(string)|one of "dumb", "smart", "none" or null| - -#### Last Reading Attributes - -|API field|Attributes|Description| -|----|----|----| -|connection|Boolean|whether or not the device is reachable remotely| - -## Lock [/locks/{device_id}] - -### Get Lock [GET] - -#### Desired State Attributes - -|API field|Attributes|Description| -|----|----|----| -|locked|boolean|whether or not the lock is locked| -|alarm_mode|string|null, "activity", "tamper", "forced_entry"| -|alarm_sensitivity|float|ercentage 1.0 for Very sensitive, 0.2 for not sensitive, steps in values of 0.2| -|auto_lock_enabled|boolean|whether or not the auto lock feature is enabled| -|beeper_enabled|boolean|whether or not the beeper is enabled| -|vacation_mode|boolean|whether or not the vacation mode is enabled| -|key_code_length|integer| usually betweeen 4 and 8, check for capabilities for allowed units| - -#### Last Reading Attributes - -|API field|Attributes|Description| -|----|----|----| -|connection|Boolean|whether or not the device is reachable remotely| -|alarm_activated|boolean|becomes true when alarm is triggered on lock| - -## Nimbus [/cloud_clocks/{device_id}] - -The Nimbus is an example of a legacy device whose fields and conventions do not follow the standard wink device semantics. - -#### Device Model - -|API field|Attributes|Description| -|----|----|----| -|dials|array of Dials|Array of 4 dial objects representing the dials on the Nimbus face| -|alarms|array of Alarms|0 to many alarms, sent to and stored on Nimbus firmware| - -#### Dial Template - -Returns the available channel_configurations and dial_configurations for the dial resource - -Explanation of dial_configuration fields and values - -|API Field|Description| -|----|----| -|scale_type|log, linear [How the dial should move in response to higher values| -|rotation|cw, ccw [In which direction the dial should rotate]| -|min_value|any number [The minimum data value the dial should attempt to display at min_position]| -|max_value|any number greater than min_value [The maximum data value the dial should attempt to display at max_position]| -|min_position|degree rotation which corresponds to min_value. Generally [0, 360] but not required to be so. [The position of the needle at min_value]| -|max_postition|degree rotation which corresponds to max_value. Generally [0, 360] but not required to be so. [The position of the needle at max_value]| - -Read types available for each dial_template channel configuration: - -|Read Type|Values| -|----|----| -|Time|n/a| -|Weather|temperature, weather_conditions| -|Traffic|travel_time, travel_conditions| -|Calendar|time_until, time_of [refers to next appointment on calendar, currently only Google Calendar is supported]| -|Email|unread_message_count [currently only Gmail is supported]| -|Facebook|friend_request_count, latest_comment_count, latest_like_count, unread_message_count, unread_notification_count| -|Twitter|latest_retweet_count, recent_mention_count, recent_direct_message_count| -|Instagram|latest_like_count, latest_comment_count| -|Fitbit|calorie_out_count, heart_rate, sleep_duration, step_count| -|Eggminder|inventory| -|Porkfolio|balance| - -Other field values seen in dial_templates - -|API Field|Description| -|----|----| -|timezone|any IANA timezone -|locale:|A standard locale string of the ll_cc format, where ll is the two letter ISO language code and cc is the two letter ISO country code| -|lat_lng|tuple of (lat, lng) for the Weather channel| -|location|location string (New York, NY) for display for the Weather channel| -|start_lat_lng|tuple of (lat, lng) for the Traffic channel| -|start_location|location string (New York, NY) for display for the Traffic channel| -|stop_lat_lng|tuple of (lat, lng) for the Traffic channel| -|stop_location|location string (New York, NY) for display for the Traffic channel| -|transit_mode|one of ["car", "ped", "bike", "transit"] representing desired principal mode of transit for the Traffic channel| - - - { - "dial_template_id": "4", - "dial_configuration": { - "min_value": 0, - "max_value": 3600, - "min_position": 0, - "max_position": 360, - "scale_type": "linear", - "rotation": "cw" - }, - "channel_configuration": { - "channel_id": "4", - "reading_type": "time_until", - }, - "name": "Calendar" - } - -#### Dial model - -Each cloud_clock resources have 4 dial resources. Use dial_template to retrieve possible values for channel_configuration and dial_configuration - -**Atttributes** - -|API field|Attributes|Description| -|----|----|----| -|dial_id|(string, assigned, immutable)|API Id| -|dial_index|(integer, assigned, mutable)|Index of dial on Nimbus object| -|name|(string, mutable with write_data permissions)|Clients don't expose naming ability and name will normally map to channel configuration| -|label|(string, assigned, mutable)|deprecated, use labels -|labels|(array, assigned, immutable)|values determined by channel type and value, for display on clock LCD)| -|position|(float, assigned, immutable)|[0.0 - 359.0, position of needle on display]| -|brightness|(integer, assigned, mutable)|[0 - 100, display brightness of LCD, can also be updated on clock by pressing down]| -|channel_configuration|(object)|(see dial_templates for possible values) -|dial_configuration|(object)|(see dial_templates for possible values) - - { - "dial_id": "adsfljk_458", - "dial_index": 2, - "name": "Instagram", - "label": "INSTAGRAM", - "labels": ["INSTAGRAM", "1 LIKE"], - "position": 180.0, - "brightness": 25, - "channel_configuration": { - "channel_id": "4323", - "linked_service_ids": ["125"], - "linked_service_types": ["instagram.read_messages"] - "reading_type": "latest_comment_count", - "locale": "en_us", - }, - "dial_configuration": { - "scale_type": "linear", - "rotation": "cw", - "min_position": 0.0, - "min_value": 0.0, - "max_position": 0.0, - "max_value": 0.0, - "num_ticks": 0 - } - } - -Dials are be updated through the cloud_clock parent object - -+ Model (application/json) - - JSON representation of a device - - + Body - - { - "cloud_clock_id": "fasinfhs_12670s", - "name": "My Nimbus", - "dials": [ - { - "dial_id": "456", - "dial_index": 0, - "name": "Facebook", - "label": "FACEBOOK", - "labels": ["FACEBOOK", "1 REQUEST"], - "position": 90.0, - "brightness": 25, - "channel_configuration": { - "channel_id": "6", - "linked_service_ids": ["123"], - "linked_service_types": ["facebook.read_messages"], - "reading_type":"friend_request_count", - "locale": "en_us" - }, - "dial_configuration": {} - }, - { - "dial_id": "457", - "dial_index": 1, - "name": "Twitter", - "label": "TWITTER", - "labels": ["TWITTER", "1 TWEET"], - "position": 270.0, - "brightness": 25, - "channel_configuration": { - "channel_id": "4322", - "linked_service_ids": ["124"], - "linked_service_types": ["twitter.read_messages"], - "reading_type": "latest_retweet_count", - "locale": "en_us", - }, - "dial_configuration": {} - }, - { - "dial_id": "458", - "dial_index": 2, - "labels": ["638.2 HRS", "TO DEST"], - "name": "Instagram", - "label": "INSTAGRAM", - "position": 180.0, - "brightness": 25, - "channel_configuration": { - "channel_id": "4323", - "linked_service_ids": ["125"], - "linked_service_types": ["instagram.read_messages"] - "reading_type": "latest_comment_count", - "locale": "en_us", - }, - "dial_configuration": {} - }, - { - "dial_id": "459", - "dial_index": 3, - "name": "Weather", - "label": "WEATHER", - "labels": ["FLURRIES", "TEMP 32"], - "position": 0.0, - "brightness": 25, - "channel_configuration": { - "lat_lng": [40.7517836, -74.0050807], - "reading_type": "weather_conditions", - "locale": "en_us", - "channel_id": "4324" - }, - "dial_configuration": {} - } - ], - "alarms": [ - { - "alarm_id": "555", - "name": "Wakie wakie", - "recurrence": "DTSTART:20130821T140000ZnRRULE:FREQ=DAILY", - "media_id": "666", - "enabled": true, - "next_at": 123456789.0 - } - ] - } - -### List nimbi [GET] - -+ Response 200 (application/json) - - [Nimbus][] - -## Nimbus Alarm [/cloud_clocks/{cloud_clock_id}/alarms] - -The alarm resource has the following attributes: - -|API field|Attributes|Description| -|----|----|----| -|alarm_id|(string, assigned, immutable)|api id -|cloud_clock_id|(string, assigned, immutable)|[id of associated cloud_clock]| -|name|(string, mutable with write_data permissions)|user defined name| -|recurrence|(string, mutable with write_data permission)|[Recurrence string in iCalendar format]| -|enabled|(boolean, mutable with write_data)|if the alarm is currently enabled| -|next_at|(float, assigned, immutable)| [time stamp of next alarm]| - -+ Model - - { - "alarm_id": "fadlkfh_124_hasd", - "cloud_clock_id": "fasinfhs_12670s", - "name": "Wakie wakie", - "recurrence": "DTSTART;TZID=America/New_York:20130826T073000nRRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR", - "enabled": true, - "next_at": 123456789.0 - } - -### List alarms of nimbus [GET] - -+ Response 200 (application/json) - - [Nimbus Alarm][] - -### Create an alarm [POST] - -+ Response 200 (application/json) - - [Nimbus Alarm][] - -## Alarm [/alarms/{alarm_id}] - -### Edit an alarm [PUT] - -+ Response 200 (application/json) - - [Nimbus Alarm][] - -### Delete an alarm [DELETE] - -+ Response 204 - -## Power Strip [/power_strips/{device_id}] - -As a legacy device, the power strip is a bit unique. The device itself has no desired state or last reading, instead, it has an array of two outlet objects that have individual states. - -### Get Power Strip [GET] - -#### Power strip Model - -|API Fields|Attributes|Description| -|----|----|----| -|outlets|array of two outlets|the two outlets of the powerstrip| - -#### Power strip Last Reading - -|API Fields|Attributes|Description| -|----|----|----| -|connection|boolean|whether or not the powerstrip is connected| - -#### Outlet Model - -|API Fields|Attributes|Description| -|----|----|----| -|outlet_index|(numeric, assigned, immutable)|Order of outlet on power strip| - -#### Outlet Desired State - -|API Fields|Attributes|Description| -|----|----|----| -|powered|boolean|whether or not the outlet is on| - -## Piggy Bank [/piggy_bank/{device_id}] - -As a legacy device, the power strip is a bit unique. The deposits are a separate call outlined below. In addition, the color of the nose is not in the desired_state but on the object itself. - -### Get Piggy Bank [GET] - -#### Device Model - -|API field|Attributes|Description| -|----|----|----| -|color|String|hex string of porkfolio nose color| - -#### Last Reading Attributes - -|API field|Attributes|Description| -|----|----|----| -|connection|Boolean|whether or not the device is reachable remotely| -|battery|float|0 - 1 battery percentage| -|vibration|boolean|becomes true when accelerometer is triggered on movement| - -### Deposits [/piggy_banks/{piggy_bank_id}/deposits?since={timestamp}] - -#### Deposit Model - -|API field|Attributes|Description| -|----|----|----| -|deposit_id|string|API id| -|created_at|timestamp|when deposit was created| -|amount|integer|deposit amount in cents| - -Value can be negative for a withdrawal. - -### Get all deposits for Piggy Bank [GET] - -### Create a deposit or withdrawal [POST] - -+ Request - - { - "amount":10 - } - -## Refrigerators [/refrigerators/{device_id}] - -### Get Refrigerator [GET] - -#### Desired State Attributes - -|API field|Attributes|Description| -|----|----|----| -|refrigerator_set_point|float|set point for refrigerator in celsius| -|freezer_set_point|float|set point for freezer in celsius| -|refrigerator_ice_maker_enabled|boolean|whether or not the ice maker for the refrigerator is enabled| -|freezer_ice_maker_enabled|boolean|whether or not the ice maker for the freezer is enabled| -|sabbath_mode_enabled|boolean|whether or not sabbath mode is enabled| - -#### Last Reading Attributes - -|API field|Attributes|Description| -|----|----|----| -|min_refrigerator_set_point_allowed|float|minimum allowed set point in celsius| -|max_refrigerator_set_point_allowed|float|maximum allowed set point in celsius| -|min_freezer_set_point_allowed|float|minimum allowed set point in celsius| -|max_freezer_set_point_allowed|float|maximum allowed set point in celsius| -|refrigerator_left_door_ajar|boolean|whether the left refrigerator door is currently ajar| -|refrigerator_right_door_ajar|boolean|whether the right refrigerator door is currently ajar| -|refrigerator_door_ajar|boolean|whether either refrigerator door is currently ajar| -|freezer_door_ajar|boolean|whether the freezer door is currently ajar| -|water_filter_remaining|float|[0 - 1] percentage of water filter remaining| -|firmware_version|string|current firmware version of refrigerator unit| -|update_needed|boolean|whether refrigerator unit needs an update| -|updating_firmware|boolean|whether refrigerator unit is currently updating| -|symbiote_firmware_version|string|current firmware version of wifi module| -|symbiote_update_needed|boolean|whether wifi module needs an update| -|symbiote_updating_firmware|boolean|whether wifi module is currently updating| - -## Refuel [/propane_tanks/{device_id}] - -### Get Refuel [GET] - -#### Device Attributes - -|API field|Attributes|Description| -|----|----|----| -|tare|(float)|weight of tank, as printed on can| -|tank_changed_at|timestamp|time a new tank was added|| - -#### Desired State Attributes -n/a - -#### Last Reading Attributes - -|API field|Attributes|Description| -|----|----|----| -|connection|Boolean|whether or not the device is reachable remotely| -|battery|float|0 - 1, battery percentage| -|remaining|float|0 - 1, percent fuel remaining| - -## Remote [/remotes/{device_id}] - -### Get Remote [GET] - -#### Desired State Attributes -n/a - -#### Last Reading Attribute -|API field|Attributes|Description| -|----|----|----| -|remote_pairable|boolean|wehter or not the remote is pairing with a device| -|group_id|string|Reference to the Group object linked to the remote| -|button_up_pressed|boolean|up button is pressed| -|button_down_pressed|boolean|down button is pressed| -|button_on_pressed|boolean|on button is pressed| -|button_off_pressed|boolean|off button is pressed| - -## Sensor [/sensor_pods/{device_id}] - -### Get Sensor [GET] - -#### Desired State Attributes -n/a - -#### Last Reading Attribute - -|API Field|Attributes|Description| -|----|----|----| -|battery|float|[0 - 1] percentage of battery| -|connection|boolean|whether or not the sensor has connection| -|brightness|boolean|whether or not the sensor currently detects a large delta in light| -|external_power|boolean|whether the sensor is running on AC power or battery| -|humditity|float|[0 - 1] percentage of measured of humidity| -|loudness|boolean|whether the sensor is currently detects a large delta in sound| -|temperature|float|current reported temperature in celsius| -|vibration|boolean|whether the sensor currently detects a large delta in vibration| -|motion|boolean|whether the sensor currently detects a large delta in motion| -|opened|boolean|whether the sensor detects an opened state| -|locked|boolean|whether the sensor detects a locked state| -|liquid_detected|boolean|whether the sensor detects moisture| -|occupied|boolean|whether or not the sensor has detected occupancy in the last 30 minutes| - -## Sirens [/sirens/{device_id}] - -### Get Siren [GET] - -#### Desired State Fields - -|API Field|Attributes|Description| -|----|----|----| -|mode|String|one of [siren_only, strobe_only, siren_and_strobe]| -|powered|boolean|whether or not the siren is on| -|auto_shutoff|Integer|one of [null (never), 30, 60, 120]. Values are in seconds.| - -#### Last Reading Attribute - -|API Field|Attributes|Description| -|----|----|----| -|battery|float|[0 - 1] percentage of battery| -|connection|boolean|whether or not the sensor has connection| - -## Smoke Alarm [/smoke_detector/{device_id}] - -### Get Smoke Alarm [GET] - -#### Desired State Fields -n/a - -#### Last Reading Fields - -|API Field|Attributes|Description| -|----|----|----| -|smoke_detected|boolean|whether or not smoke is currently detected| -|co_detected|boolean|whether or not carbon monoxide is currently detected| -|test_activated|whether or not a test is currently activated| -|connection|boolean|current connection status| -|battery|float|[0 - 1] battery percentage| -|smoke_severity|float|[0 - 1] if present, severity of smoke detection| -|co_severity|float|[0 - 1] if present, severity of co detection| - -## Sprinklers [/sprinklers/{device_id}] - -### Get Sprinklers [GET] - -The sprinkler model has an array of zones. The zone objects themselves just display the state of the zones. Control of the zones is on the the desired_state of the sprinkler object with the run_zones and run_zones_durations. - -#### Sprinkler Model - -|API Field|Attribute|Description| -|----|----|----| -|zones|array of zones|Zones of sprinkler, which can display the state of the given nozzles| - -#### Desired State Fields - -|API Field|Attribute|Description| -|----|----|----| -|run_zones|array of integers|indices of zones to run| -|run_zones_durations|array of integer|duration in seconds of each of the run_zones| - - -#### Zone Model - -|API Field|Attribute|Description| -|----|----|----| -|zone_index|integer|index of zone on sprinkler system| -|enabled|boolean|whether or not the zone is hooked up| - -#### Zone Desired State - -|API Field|Attribute|Description| -|----|----|----| -|shade|String|Shade state of vegetation in zone, one of ["none", "moderate", "mostly"]| -|nozzle|String|Nozzle type of zone, one of ["fixed_spray_head","drip", "manual_sprinkler", "rotary_head", "rotor_head"]| -|slope|String|Slope of vegetation in zone, one of ["bottom", "flat", "slope", "top"]| -|soil|String|Soil of vegetation in zone, one of ["clay", "sand", "silt", "top_soil"]| -|vegetation|String|Type of vegetation in zone, one of ["annuals","garden", "grass","perennials", "shrubs", "trees", "xeric", "xeriscape" ]| - -#### Zone Last Reading - -|API Field|Attribute|Description| -|----|----|----| -|powered|boolean|whether or not the zone is currently on, note this cannot be controlled on the zone level. It can only be set as an object in run_zones| - -## Thermostats [/thermostats/{device_id}] - -### Get Thermostat [GET] - -#### Desired State Fields - -|API Field|Attribute|Description| -|----|----|----| -|mode|String|One of ["cool_only", "heat_only", "auto", "aux"], allowed value depends in `mode.choices` in capabilities| -|powered|boolean|whether or not the hvac is on| -|max_set_point|float, in celsius|max set point for cooling in celsius| -|min_set_point|float, in celsius|min set point for heating in celsius| -|override_temperature|float, in celsius|temperature sent to thermostat to override interally read temperature| -|setpoint_increment_value|integer, in tenths of celsius|value that the thermostat will change on tapping| -|accelerometer_enable|boolean|whether or not temperature change on tapping is enabled| -|temperature_override_enable|boolean|whether or not overriding the temperature is enabled| -|fan_duration|integer|Set fan on for duration in seconds| -|users_away|boolean|Set users away -- thermostat will manage temperature at a lower set point| -|cooling_system_stage|String|one of "cool_stage_1", "cool_stage_2"| -|heating_system_stage|String|one of "heat_stage_1", "heat_stage_2"| -|heating_system_type|String|one of "conventional", "heat_pump"| -|heating_fuel_source|String|one of "electric", "gas"| -|humidifier_mode|String|one of "on", "off", "auto"| -|humidifier_set_point|float|[0.0 - 1.0]| -|fan_mode|String|one of "[on,auto]", auto will turn the fan on when heating or cooling is active| -|dehumidifier_mode|string|one of “[on,off]”| -|dehumidifier_set_point|float|the humidity degree in which the thermostat will start to dehumidify| -|dehumidify_overcool_offset|float|cool in x F below cool setpoint in order to reach the dehumidification setpoint, capabilities will express an array of choices from 0 to the equivalent of 5 degrees F in steps of 0.5 degrees F, converted to C| -|profile|string| one of “[home,away,sleep,awake,null]” depends on capabilities] -|fan_run_time|int|minimum amount of time to circulate air per hour when fan is on AUTO mode, 0-3300 seconds, increments of 300 seconds| - -#### Last Reading Fields - -|API Field|Attribute|Description| -|----|----|----| -|connection|boolean|whether or not the device is reachable remotely| -|temperature|float in celsius|[maps to room temperature last read from device itself]| -|smart_temperature|ecobee only, mean temp of all remote sensors and thermostat| -|humidity|float|[0-1] from device readings| -|external_temperature|float in celsius|the outdoor temperature/weather| -|max_max_set_point|float in celsius|highest allowed max set point| -|min_max_set_point|float in celsius|lowest allowed max set point| -|max_min_set_point|float in celsius|highest allowed min set point| -|min_min_set_point|float in celsius|lowest allowed min set point| -|has_fan|boolean|whether or not the thermostat unit has a fan| -|fan_timer_active|boolean|whether or not the fan timer is active| -|eco_target|boolean|whether or not the thermostat is running in an energy efficient mode| -|override_temperature_group_id|string|group id of group used to calculate temperature override| -|deadband|float in celsius|minimum difference between max and min set points| -|technician_name|String|contractor contact data| -|technician_phone|String|contractor contact data| -|aux_active|boolean|Auxiliary heat is actively pumping| -|cool_active|boolean|Cool is actively pumping| -|heat_active|boolean|Heat is actively pumping| -|fan_active|boolean|Fan is actively running| -|last_error|string|the current alert/warning on the thermostat| -|occupied|boolean|Whether or not the thermostat has detected occupancy in the last 30 minutes| - -## Water Heaters [/water_heaters/{device_id}] - -### Get Water Heater [GET] - -#### Desired State Attributes - -|API Field|Attribute|Description| -|----|----|----| -|mode|String|one of "eco", "performance", "heat_pump", "high_demand", "electric_only", "gas"| -|powered|boolean|whether or not the water heater is on| -|set_point|float|set point in celsius| -|vacation_mode|boolean|whether vacation mode is ucrrently enabled| - -#### Last Reading Attributes - -|API Field|Attribute|Description| -|---|---|---| -|min_set_point_allowed|float|minimum set point allowed in celsius| -|max_set_point_allowed|float|maximum set point allowed in celsius| -|modes_allowed|String array|one or many of modes, depends on rheem type| -|scald_message|String|Populated if the set point is above 120F| -|rheem_type|(String)|one of "Electric Water Heater", "Heat Pump Water Heater", "Gas Water Heater"| - -# Group Member - -Members are used throughout the API for group hetereogenous devices in scenes, groups and robots. There are no specific endpoints for members and their creation and updating will happen within their respective parents - -## Member model - -Members have the following attributes - -|API field|Attributes|Description| -|----|----|----| -|object_id|(string, assignable)|API id| -|object_type|(string, assignable)|value will be singular types of wink devices and objects. i.e. air_conditioner, propane_tank, outlet, light_bulb, etc.| -|desired_state|(object)|current or requested desired_state of object| - -# Group Group - -**Resources for creating and controlling groups of devices** - -The group resource is a representation of a group of wink devices which may be controlled simultaneously. In addition, the group will have an aggregated reading to get the aggregate state of the grouped devices. - -The Wink API defines certain special groups which you cannot fully control. These include, but are not limited to: - - - System categories such as `.all` and `.sensors`, which will include respectively every product and every product which is contains environment sensors of any kind. You cannot create, delete, or rename system categories. You cannot add or remove objects from system categories. System categories have an `automation_mode` flag of `system_category`. - - User categories such as `@door_sensors` and `@power`. Some devices will appear in these categories by default, based on our best guess of how these devices will be used by most consumers. You cannot create, delete, or rename user categories. You can, however, add and remove objects, if our default classifications are not appropriate. User categories have an an `automation_mode` flag of `user_category`. - - -The group resource has the following attributes: - -|API field|Attributes|Description| -|---|---|---| -|name|(string, mutable)|User defined name. *NOTE* system_category and user_category names are immutable. In addition, an error will be thrown if a user tries to name their group the same name as a category group| -|members|(0 to many objects, assignable)|[Member object](/General/API/Member)| -|automation_mode|(string, assigned, immutable)|system_category or user_category, also indicated by a prefix of `.` or `@`| -|reading_aggregation|(object)|aggregated last_reading of group devices| -|desired_state|(object)|desired state PUT to change all members simultaneously| - - -Optional member attributes: -|blacklisted_readings|(array of strings, assignable)|Once set, this member will not contribute to the group aggregation of blacklisted readings| - -#### Reading Aggregation Model - -Reading aggregation differ from last_reading in that the fields identify a count of state or an average/max/min - -**Boolean Aggregation** - -A boolean aggregation has the following fields - -|API field|Attributes|Description| -|----|----|----| -|updated_at|(timestamp|Last time aggregation was updated| -|or|(boolean)|Whether the aggregation is driven by OR logic| -|and|(boolean)|Whether the aggregation is driven by AND logic| -|true_count|(integer)|How many members in the group are registering the given boolean field as true| -|false_count|(integer)|How many members in the group are registering the given boolean field as false| - -**Numeric Aggregation** - -A numeric aggregation has the following fields - -|API field|Attributes|Description| -|----|----|----| -|updated_at|(timestamp|Last time aggregation was updated| -|min|(float)|Lowest reading of members| -|max|(float)|Highest reading of members| -|average|(float)|Average reading of all members, currently unweighted| - -## Groups [/users/me/groups] - -+ Model (application/json) - - JSON representation of a shared group - - + Body - - { - "object_id": "agh1ity-876f00", - "object_type": "group", - "name": "Front windows", - "members": [ - { - "object_id": "adsjfhasdof", - "object_type": "light_bulb" - "desired_state": { - "powered":true - }, - "blacklisted_readings": ['brightness'] - }, - { - "object_id": "adsjfhasdof", - "object_type": "air_conditioner", - "desired_state": { - "powered":true - }, - "blacklisted_readings": [] - }], - "desired_state":{}, - "reading_aggregation": { - "powered": { - "updated_at":1234567890, - "or":false, - "and":true, - "true_count":2, - "false_count":0 - } - } - } - -### Get all groups [GET] - -+ Response 200 (application/json) - - [Groups][] - -### Create a group [POST] - -+ Request - - { - "name": "Front windows", - "members": [ - { - "object_id": "adsjfhasdof", - "object_type": "light_bulb" - "desired_state": { - "powered":true - }, - "blacklisted_readings": ['brightness'] - }, - { - "object_id": "adsjfhasdof", - "object_type": "air_conditioner", - "desired_state": { - "powered":true - }, - "blacklisted_readings": [] - }], - } - -+ Response 200 (application/json) - - [Groups][] - -## Group [/groups/{group_id}/] - -### Retrieve a group [GET] - -+ Response 200 (application/json) - - [Groups][] - -### Update group settings [PUT] - -+ Request - - { - "name": "Front windows", - } - -+ Response 200 (application/json) - - [Groups][] - -### Delete a group [DELETE] - -+ Response 204 - -## Set state of group [/groups/{group_id}/activate] - -When you post up a desired state object, the API will then change all the devices in the group to that desired state. Allowed values for desired_state are dependent on the devices in the group and you should refer to individual device documentation. - -If you have multiple types of devices in a group and a field in the desired_state object only applies to some of them, such as `color` for `light_bulb` types, the API will update the appropriate devices and ignore that state for devices that do not have a color state, such as air_conditioners - -### Set state [POST] - -+ Body - { - "desired_state": { - "powered":true - } - } - -+ Response 200 (application/json) - - [Groups][] - -# Group Scene - -A scene is a collection of desired states for any supported Wink device (such as an air conditioner or garage door) or any Wink object (such as the outlet on a powerstrip). The scene members can be heterogenous and the member objects are composed as defined above in member. -A scene member must have a valid desired state for the object type in order to be valid. - -## Scene Model - -|API field|Attributes|Description| -|----|----|----| -|name|(string, writable)|User defined name| -|members|array of members|See Member above| - -## Scenes [/users/me/scenes] - -+ Model (application/json) - - JSON representation of a shared group - - + Body - - { - "scene_id": "qs1ga9_1234deadbeef", - "name": "Coming home", - "members": [ - { - "object_id":"afdjlafd", - "object_type:"light_bulb", - "desired_state": { - "powered": true - } - }, - { - "object_id":"yasdfkha", - "object_type:"garage_door", - "desired_state": { - "position": 1.0 - } - } - ] - } -### Get all scenes [GET] - -+ Response 200 (application/json) - - [Scenes][] - -### Create a scene [POST] - -+ Request - - { - "name": "Coming home", - "members": [ - { - "object_id":"afdjlafd", - "object_type:"light_bulb", - "desired_state": { - "powered": true - } - }, - { - "object_id":"yasdfkha", - "object_type:"garage_door", - "desired_state": { - "position": 1.0 - } - } - ] - } - -+ Response 200 (application/json) - - [Scenes][] - -## Scene [/scenes/{scene_id}/] - -### Retrieve a scene [GET] - -+ Response 200 (application/json) - - [Scenes][] - -### Update scene settings [PUT] - -+ Request - - { - "name": "Coming home", - } - -+ Response 200 (application/json) - - [Scenes][] - -### Delete a scene [DELETE] - -+ Response 204 - -## Set state of scene [/scenes/{scene_id}/activate] - -In order to activate the scene, POST to the endpoint. No body is necessary. - -### Set state [POST] - -+ Response 200 (application/json) - - [Scenes][] - - -# Group Robot - -A Robot is the API object that allows for automations based on external triggers such as time or another device's state - -## Robot Model - -|API field|Attributes|Description| -|----|----|----| -|name|(string, writable)|User defined name| -|creating_actor_type|(string, assigned)|type of entity that created the robot, can be user or device in case of smart features| -|creating_actor_id|(string, assigned)|id of entity that created the robot| -|automation_mode|(string, writable)|mode of robot if generated for smart features, current possible values -- null (not smart), "smart_schedule", "smart_away_arriving", "smart_away_departing"; client writable values "notification" (fridge note), "tapt" (created implicitly through relay or tapt interface)| -|causes|(array, 1 to many Conditions)| Cause(s) that will trigger the robot| -|restrictions|(0 to many Conditions)| Restriction(s) that would prevent the robot from triggering on the given causes. **NOTE** while documented, currently this is not exposed on the clients| -|effects|(array, 1 to many|Effect of robot when triggered| - -### Desired State Fields -|API field|Attributes|Description| -|----|----|----| -|enabled|(boolean, writable)|Whether or not the robot is currently enabled| -|fired_limit|(integer, writable)|How many times the robot can fire. A value of null or 0 means the robot can fire as many times as possible. Currently used by Refrigerator note robots| - -### Last Reading Fields -|API field|Attributes|Description| -|----|----|----| -|fired|(boolean)|Whether the robot has fired| - -## Condition Model - -The causes array is an array of Conditions that can trigger a robot. The restrictions array is an array of Conditions that can prevent a robot from being fired, even if it is triggered by a cause. - -A condition has the following format: - -|API field|Attributs|Description| -|----|----|----| -|condition_id|(string, assigned, immutable)|id of condition| -|observed_field|(string, writable)|field in last reading| -|observed_object_id|(string, writable)|id of object being observed| -|observed_object_type|(string, writable)|type of object being observed, ex. "garage_door"| -|operator|(string, writable)|comparison operator| -|value|(string, writable)|desired value of observed_field| -|recurrence|(text, writeable)|iCal string| -|restriction_join|(string, writable)|"and" or "or"| -|robot_id|(integer, writable)|id of robot| -|restricted_object_id|(integer, writable)|if this condition is restricting something, the id of what is being restricted| -|restricted_object_type|(string, writable)|if this condition is restricting something, the type of what is being restricted, currently "robot" or "condition"| -|restrictions|(array of 0 to many)|embedded condition objects to restrict parent condition| - -For causes and restrictions, their truthiness is evaluated in the following order: - -- `recurrence && observed_field && restrictions.` - -### Condition recurrence - -Recurrence should be a string in iCal format and is to be used for a condition dependent on a time. If the recurrence is nil, it's evaluation is true. Thus, to omit a time restriction, set the recurrence string to null. If the recurrence is meant to restrict time for firing, it should have a start and end. See below for examples. - -+ It is 5 pm on a Tuesday - - { - "recurrence": "DTSTART;TZID=PDT:20140313T170000nRRULE:FREQ=DAILY" - } - -+ It is between 8pm and 10pm on Tuesdays - - { - "recurrence": "DTSTART;TZID=PDT:20140313T200000nDTEND;TZID=PDT:20140313T220000nRRULE:FREQ=DAILY" - } - -+ Every day at sunrise in my current location - - { - "recurrence": "DTSTART;TZID=PDT:20140313T080000nX-WINK-STSTART:sunrise;37.47;-122.25nDTEND;TZID=PDT:20140314T080000nRRULE:FREQ=DAILY" - } - -+ Every day at sunset in my current location - - { - "recurrence": "DTSTART;TZID=PDT:20140313T200000nX-WINK-STSTART:sunset;37.47;-122.25nDTEND;TZID=PDT:20140314T200000nRRULE:FREQ=DAILY" - } - -+ Every day between sunrise and sunset in my current location - - { - "recurrence": "DTSTART;TZID=PDT:20140313T200000nX-WINK-STSTART:sunrise;37.47;-122.25nDTEND;TZID=PDT:20140314T210000nRX-WINK-STEND:sunset;37.47;-122.25nRULE:FREQ=DAILY" - } - -### Condition observed fields - -The following fields are all required to properly evaluate observed_field - -- observed_field -- operator -- value - -observed_field can be any field in a device's last_reading object - -operator can be only of the following, as strings - -- == -- != -- > -- < -- >= -- <= - -value is the value that will be used in comparison with the operator. - -**NOTE:** although you can compare multiple types of values, such as boolean, string, int, or float, this value should be put and read as a string. See examples below. - -In order to evaluate to true, the observed field's value must be evaluated to true with the given operator - -+ Example of a condition: a garage door is opened by at least 50% - - { - "observed_object_id":"xyzasdfadsfhkj", - "observed_object_type":"garage_door", - "observed_field": "position", - "operator": ">=", - "value": "0.5" - } - -+ Example of a condition: geo fence is entered - - { - "observed_object_id":"defasdfkjha", - "observed_object_type":"geofence", - "observed_field": "within", - "operator": "==", - "value": "true" - } - -+ Example of a condition: geo fence is exited - - { - "observed_object_id":"defasdfkjha", - "observed_object_type":"geofence", - "observed_field": "within", - "operator": "==", - "value": "false" - } - - -#### Restriction Evaluation - -Each condition can have embedded restrictions to create complex logic. - -By default, each restriction in the array is joined by "and" so that all the restrictions in the array must evaluate to true in order for restrictions to evaluate to true. - -You can join restrictions by "or" by add a - -- restriction_join [allowed values "and" "or"] - -**NOTE ABOUT COMPLEXITY** - -Because each restriction is of the same class as the top level Condition, each embedded restriction goes through the same evaluation process for truthiness and can similarly have embedded restrictions of its own. - -The restrictions defined in the top level "restrictions" array of the robot object are joined by "and". From there, nested restrictions are joined based on the "restriction_join" field of the parent restriction. - -The allowed causes are either objects with an observed reading (such as a garage door opening or a geofence being triggered) or they are a time as defined by an iCal recurrence string. - -EXAMPLES: - -Note: each example could be in the causes or restrictions array on the robot object, causes are conditions that can trigger a robot and restrictions are conditions that can prevent a robot from being triggered. - - -+ The garage door is closed and I am within my home geofence - - { - "restrictions": [ - { - "observed_object_id":"xyzasdfadsfhkj", - "observed_object_type":"garage_door", - "observed_field": "position", - "operator": "==", - "value": "0.0" - }, - { - "observed_object_id":"defasdfkjha", - "observed_object_type":"geofence", - "observed_field": "within", - "operator": "==", - "value": "true" - } - ] - } - -+ (the garage door is closed and I am within my home geofence) or it is between 8pm and 10pm on Tuesdays - - { - "restriction_join": "or" - "restrictions": [ - { - "restrictions": [ - { - "observed_object_id":"xyzasdfadsfhkj", - "observed_object_type":"garage_door", - "observed_field": "position", - "operator": "==", - "value": "0.0" - }, - { - "observed_object_id":"defasdfkjha", - "observed_object_type":"geofence", - "observed_field": "within", - "operator": "==", - "value": "true" - } - ] - }, - { - "recurrence": "DTSTART;TZID=PDT:20140313T200000DTEND;TZID=PDT:20140313T220000nRRULE:FREQ=DAILY" - } - ] - } - -+ The garage door is closed and (I am within my home geofence or it is between 8pm and 10pm on Tuesdays) - - { - "restrictions": [ - { - "observed_object_id":"xyzasdfadsfhkj", - "observed_object_type":"garage_door", - "observed_field": "position", - "operator": "==", - "value": "0.0" - }, - { - "join_type": "or", - "restrictions": [ - - { - "observed_object_id":"defasdfkjha", - "observed_object_type":"geofence", - "observed_field": "within", - "operator": "==", - "value": "true" - }, - { - "recurrence": "DTSTART;TZID=PDT:20140313T200000DTEND;TZID=PDT:20140313T220000nRRULE:FREQ=DAILY" - } - ] - }, - - ] - } - -+ The garage door is closed and it is between 8pm and 10pm on Tuesdays - -Can be written as two embedded restrictions - - { - "restrictions": [ - { - "observed_object_id":"xyzasdfadsfhkj", - "observed_object_type":"garage_door", - "observed_field": "position", - "operator": "==", - "value": "0.0" - }, - { - "recurrence": "DTSTART;TZID=PDT:20140313T200000DTEND;TZID=PDT:20140313T220000nRRULE:FREQ=DAILY" - } - - ] - } - -## Effect Model - -An effect is what happens if any of the causes occur outside of the optional restriction. - -The effect has the following attributes: - -|API field|Attributs|Description| -|----|----|----| -|scene|(refers to scene that would be activated)|If the effect affects devices, a scene will be created| -|recipient_actor_id |(String)|Refers to a user that would get a notification of some type. Scenes and recipient actors are currently mutually exclusive| -|recipient_actor_type|(String)|Refers to the type of actor, currently just "user". Scenes and recipient actors are currently mutually exclusive| -|notification_type|(String)|notification type to send "email" or "push"| -|note|(custom text object)|used in refrigerator notes| - -An effect can have a scene OR a recipient_actor_id and notification_type, but should not have both. - -If an effect has a note, it should also have a notification_type and recipient_actor. - -**SCENE IN EFFECT** - -The scene object is saved within the system and has a scene_id, but will come down as a full object for ease of use - -**USER and NOTIFICATION_TYPE** - -recipient_actor_id is the user_id of the user who will be receiving the notification - -recipient_actor_type should be "user" - -Allowed values for notification_type are "email" and "push" - -## Custom Text Model -The custom text resource allows users to associate arbitrary text with another resource. - -Currently a custom text is only able to be associated with an effect. - -The custom text has the following attributes: - -|API field|Attributes|Description| -|----|----|----| -|custom_text_id|(string, assigned, immutable)|API id| -|body|(string, writable, mutable)|String used as the message of the custom text| -|subject_id|(string, assigned, immutable)|Assigned, referencing effect that has the custom text| -|subject_type|(string, assigned, immutable)|Assigned, referencing effect that has the custom text| - - -+ Model - - { - "custom_text_id": "1", - "body": "Some custom text", - "subject_id": "34", - "subject_type": "effect" - } - -## Robots [/users/me/robots] - -+ Model (application/json) - - JSON representation of a shared group - - + Body - - { - "name": "Data", - "creating_actor_type": "user", - "creating_actor_id": "asdfljafd", - "automation_mode": null, - "causes" : [ - { - "condition_id": "qweryoiu", - "observed_object_id":"xyzasdfadsfhkj", - "observed_object_type":"garage_door", - "observed_field": "position", - "operator": "==", - "value": "0.0" - } - ], - "restrictions" : [ - { - "condition_id": "sadfdsaafsd", - "recurrence": "DTSTART;TZID=PDT:20140313T200000DTEND;TZID=PDT:20140313T220000nRRULE:FREQ=DAILY" - } - ], - "effects": [ - { - "scene": { - "scene_id": "asdaioytf", - "name":"Data Scene", - "members": [ - "object_id":"asdfoaiye", - "object_type":"light_bulb", - "desired_state": { - "powered":true, - "brightness": 0.75 - } - ] - }, - "note": { - "custom_text_id", - "body": "Some text", - "subject_id": "abc", - "subject_type": "effect" - } - }, - { - "recipient_actor_id": "adsfhkjasdfy", - "recipient_actor_type": "user", - "notification_type": "email - } - ], - "last_reading": { - "enabled":true, - "fired_limit":2 - } - } - -### Get all robots [GET] - -+ Response 200 (application/json) - - [Robots][] - -### Create a robot [POST] - -+ Request - - { - "name": "Data", - "fired_limit": 2, - "automation_mode": null, - "causes" : [ - { - "observed_object_id":"xyzasdfadsfhkj", - "observed_object_type":"garage_door", - "observed_field": "position", - "operator": "==", - "value": "0.0" - } - ], - "restrictions" : [ - { - "recurrence": "DTSTART;TZID=PDT:20140313T200000DTEND;TZID=PDT:20140313T220000nRRULE:FREQ=DAILY" - } - ], - "effects": [ - { - "scene": { - "name":"Data Scene", - "members": [ - "object_id":"asdfoaiye", - "object_type":"light_bulb", - "desired_state": { - "powered":true, - "brightness": 0.75 - } - ] - }, - "note": { - "custom_text_id", - "body": "Some text", - "subject_id": "abc", - "subject_type": "effect" - } - }, - { - "recipient_actor_id": "adsfhkjasdfy", - "recipient_actor_type": "user", - "notification_type": "email - } - ], - "desired_state": { - "enabled":true, - "fired_limit":2 - } - } - -+ Response 200 (application/json) - - [Robots][] - -## Robot [/robots/{robot_id}/] - -### Retrieve a robot [GET] - -+ Response 200 (application/json) - - [Robots][] - -### Update robot settings [PUT] - -+ Request - - { - "name": "Data", - } - -+ Response 200 (application/json) - - [Robots][] - -### Delete a robot [DELETE] - -+ Response 204 - -# Group User -Resources for Users - -## User Model - -|API field|Attributes|Description| -|----|----|----| -|email|(string, writable, mutable)|user's email| -|first_name|(string, writable, mutable)|user's first name| -|last_name| (string, writable, mutable)| user's last name| -|oauth2|(object, assigned, mutable)| oauth 2 object for authentication| -|locale|(string, writable, mutable)|ISO locale| -|tos_accepted|(boolean, assigned, mutable)|whether or not the current TOS has been accepted| -|confirmed|(boolean, assigned, mutable)|whether or not the user has confirmed their email| - -## Desired State Attributes - -|API field|Attributes|Description| -|----|----|----| -|units|object|display units for user| - -## User [/users] - -### Create user [POST] -+ Request (application/json) - - + Body - - { - "client_id": "...", - "client_secret": "...", - "email": "user@example.com", - "first_name": "User", - "last_name": "McUserson", - "locale": "en_us", - "new_password": "********" - } - -+ Response 201 (application/json) - - { - "user_id": "27412", - "first_name": "User", - "last_name": "McUserson", - "email": "user@example.com", - "oauth2": { - "access_token": "example_access_token_like_135fhn80w35hynainrsg0q824hyn", - "refresh_token": "...", - "token_type": "bearer", - "token_endpoint": "https://winkapi.quirky.com/oauth2/token" - }, - "locale": "en_us", - "units": {}, - "tos_accepted": false, - "confirmed": false - } - - -## User [/users/{user_id}] - -+ Model (application/json) - - JSON representation of an user - - + Body - - { - "data":{ - "user_id":"27105", - "first_name":"User", - "last_name":"McUserson", - "email":"user@example.com", - "oauth2":{ - "access_token":"55bb2ce8488d7ff9313be76668a43ea0", - "refresh_token":"d30d2dcf5f33411b7a225e9e63952d84", - "token_type":"bearer", - "token_endpoint":"http://localhost:3000/oauth2/token" - }, - "locale":"en_us", - "units":{ - }, - "tos_accepted":false - }, - "errors":[ - ], - "pagination":{ - } - } - -+ Parameters - - user_id (required, string, `21212`) ... String `user_id` of the user to perform action on. Has example value. - -### Update current user's profile [PUT] -+ Request (application/json) - - + Headers - - Authorization : Bearer example_access_token_like_135fhn80w35hynainrsg0q824hyn - - + Body - - { - "email": "user@example.com", - } - -+ Response 200 (application/json) - - { - "data": { - "user_id": "abc123def-an15nag", - "email": "user@example.com" - } - } - -## User password [/users/{user_id}/update_password] - -### update password [POST] -+ Request (application/json) - - + Headers - - Authorization : Bearer example_access_token_like_135fhn80w35hynainrsg0q824hyn - - + Body - - { - "old_password" : '123456' - "new_password" : '654321' - } - -+ Response 200 (application/json) - - {} diff --git a/src/pywink/test/standard/__init__.py b/src/pywink/test/devices/__init__.py similarity index 100% rename from src/pywink/test/standard/__init__.py rename to src/pywink/test/devices/__init__.py diff --git a/src/pywink/test/devices/standard/__init__.py b/src/pywink/test/devices/standard/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/pywink/test/api_responses/binary_sensor.json b/src/pywink/test/devices/standard/api_responses/binary_sensor.json similarity index 100% rename from src/pywink/test/api_responses/binary_sensor.json rename to src/pywink/test/devices/standard/api_responses/binary_sensor.json diff --git a/src/pywink/test/api_responses/binary_switch.json b/src/pywink/test/devices/standard/api_responses/binary_switch.json similarity index 100% rename from src/pywink/test/api_responses/binary_switch.json rename to src/pywink/test/devices/standard/api_responses/binary_switch.json diff --git a/src/pywink/test/api_responses/eggtray.json b/src/pywink/test/devices/standard/api_responses/eggtray.json similarity index 100% rename from src/pywink/test/api_responses/eggtray.json rename to src/pywink/test/devices/standard/api_responses/eggtray.json diff --git a/src/pywink/test/api_responses/garage_door.json b/src/pywink/test/devices/standard/api_responses/garage_door.json similarity index 100% rename from src/pywink/test/api_responses/garage_door.json rename to src/pywink/test/devices/standard/api_responses/garage_door.json diff --git a/src/pywink/test/standard/api_responses/hue_and_saturation_absent.json b/src/pywink/test/devices/standard/api_responses/hue_and_saturation_absent.json similarity index 100% rename from src/pywink/test/standard/api_responses/hue_and_saturation_absent.json rename to src/pywink/test/devices/standard/api_responses/hue_and_saturation_absent.json diff --git a/src/pywink/test/standard/api_responses/hue_and_saturation_present.json b/src/pywink/test/devices/standard/api_responses/hue_and_saturation_present.json similarity index 100% rename from src/pywink/test/standard/api_responses/hue_and_saturation_present.json rename to src/pywink/test/devices/standard/api_responses/hue_and_saturation_present.json diff --git a/src/pywink/test/standard/api_responses/light_bulb.json b/src/pywink/test/devices/standard/api_responses/light_bulb.json similarity index 100% rename from src/pywink/test/standard/api_responses/light_bulb.json rename to src/pywink/test/devices/standard/api_responses/light_bulb.json diff --git a/src/pywink/test/api_responses/lock.json b/src/pywink/test/devices/standard/api_responses/lock.json similarity index 100% rename from src/pywink/test/api_responses/lock.json rename to src/pywink/test/devices/standard/api_responses/lock.json diff --git a/src/pywink/test/api_responses/power_strip.json b/src/pywink/test/devices/standard/api_responses/power_strip.json similarity index 100% rename from src/pywink/test/api_responses/power_strip.json rename to src/pywink/test/devices/standard/api_responses/power_strip.json diff --git a/src/pywink/test/api_responses/quirky_spotter.json b/src/pywink/test/devices/standard/api_responses/quirky_spotter.json similarity index 100% rename from src/pywink/test/api_responses/quirky_spotter.json rename to src/pywink/test/devices/standard/api_responses/quirky_spotter.json diff --git a/src/pywink/test/api_responses/quirky_spotter_2.json b/src/pywink/test/devices/standard/api_responses/quirky_spotter_2.json similarity index 100% rename from src/pywink/test/api_responses/quirky_spotter_2.json rename to src/pywink/test/devices/standard/api_responses/quirky_spotter_2.json diff --git a/src/pywink/test/api_responses/siren.json b/src/pywink/test/devices/standard/api_responses/siren.json similarity index 100% rename from src/pywink/test/api_responses/siren.json rename to src/pywink/test/devices/standard/api_responses/siren.json diff --git a/src/pywink/test/standard/api_responses/temperature_absent.json b/src/pywink/test/devices/standard/api_responses/temperature_absent.json similarity index 100% rename from src/pywink/test/standard/api_responses/temperature_absent.json rename to src/pywink/test/devices/standard/api_responses/temperature_absent.json diff --git a/src/pywink/test/standard/api_responses/temperature_present.json b/src/pywink/test/devices/standard/api_responses/temperature_present.json similarity index 100% rename from src/pywink/test/standard/api_responses/temperature_present.json rename to src/pywink/test/devices/standard/api_responses/temperature_present.json diff --git a/src/pywink/test/standard/bulb_test.py b/src/pywink/test/devices/standard/bulb_test.py similarity index 92% rename from src/pywink/test/standard/bulb_test.py rename to src/pywink/test/devices/standard/bulb_test.py index 53daef8..a966c4b 100644 --- a/src/pywink/test/standard/bulb_test.py +++ b/src/pywink/test/devices/standard/bulb_test.py @@ -130,6 +130,21 @@ def test_should_send_current_hue_and_saturation_to_api_if_hue_and_sat_are_provid self.assertEquals(hue, sent_desired_state.get('hue')) self.assertEquals(saturation, sent_desired_state.get('saturation')) + def test_should_send_original_brightness_when_only_xy_color_given_and_only_hue_saturation_supported(self): + original_brightness = 0.5 + bulb = WinkBulb({ + 'brightness': original_brightness, + 'capabilities': { + 'color_changeable': True, + 'fields': [{'field': 'hue'}, + {'field': 'saturation'}] + } + }, self.api_interface) + bulb.set_state(True, color_xy=[0.5, 0.5]) + set_state_mock = self.api_interface.set_device_state + sent_desired_state = set_state_mock.call_args[0][1]['desired_state'] + self.assertEquals(original_brightness, sent_desired_state.get('brightness')) + class LightTests(unittest.TestCase): diff --git a/src/pywink/test/init_test.py b/src/pywink/test/devices/standard/init_test.py similarity index 100% rename from src/pywink/test/init_test.py rename to src/pywink/test/devices/standard/init_test.py diff --git a/src/pywink/test/domain/__init__.py b/src/pywink/test/domain/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/pywink/test/domain/devices_test.py b/src/pywink/test/domain/devices_test.py new file mode 100644 index 0000000..9f62181 --- /dev/null +++ b/src/pywink/test/domain/devices_test.py @@ -0,0 +1,91 @@ +import unittest + +from pywink.domain.devices import is_desired_state_reached + + +class IsDesiredStateReachedTests(unittest.TestCase): + + def test_should_return_true_for_bulb_if_desired_brightness_matches_actual_brightness(self): + brightness = 0.4 + bulb_state = { + 'desired_state': { + 'brightness': brightness + }, + 'brightness': brightness + } + self.assertTrue(is_desired_state_reached(bulb_state)) + + def test_should_return_false_for_bulb_if_desired_brightness_does_not_match_actual_brightness(self): + brightness_1 = 0.4 + brightness_2 = 0.5 + bulb_state = { + 'desired_state': { + 'brightness': brightness_1 + }, + 'brightness': brightness_2 + } + self.assertFalse(is_desired_state_reached(bulb_state)) + + def test_should_return_true_for_bulb_if_desired_hue_matches_actual_hue(self): + hue = 0.5 + bulb_state = { + 'desired_state': { + 'hue': hue + }, + 'hue': hue + } + self.assertTrue(is_desired_state_reached(bulb_state)) + + def test_should_return_false_for_bulb_if_desired_hue_does_not_match_actual_hue(self): + hue_1 = 0.4 + hue_2 = 0.5 + bulb_state = { + 'desired_state': { + 'hue': hue_1 + }, + 'hue': hue_2 + } + self.assertFalse(is_desired_state_reached(bulb_state)) + + def test_should_return_true_for_bulb_if_desired_saturation_matches_actual_saturation(self): + saturation = 0.5 + bulb_state = { + 'desired_state': { + 'saturation': saturation + }, + 'saturation': saturation + } + self.assertTrue(is_desired_state_reached(bulb_state)) + + def test_should_return_false_for_bulb_if_desired_saturation_does_not_match_actual_saturation(self): + saturation_1 = 0.4 + saturation_2 = 0.5 + bulb_state = { + 'desired_state': { + 'saturation': saturation_1 + }, + 'saturation': saturation_2 + } + self.assertFalse(is_desired_state_reached(bulb_state)) + + def test_should_return_true_for_bulb_if_desired_powered_matches_actual_powered(self): + powered = True + bulb_state = { + 'desired_state': { + 'powered': powered + }, + 'powered': powered + } + self.assertTrue(is_desired_state_reached(bulb_state)) + + def test_should_return_false_for_bulb_if_desired_powered_does_not_match_actual_powered(self): + powered_1 = True + powered_2 = False + bulb_state = { + 'desired_state': { + 'powered': powered_1 + }, + 'powered': powered_2 + } + self.assertFalse(is_desired_state_reached(bulb_state)) + diff --git a/src/setup.py b/src/setup.py index 4dc25b2..4ea9fd7 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='0.7.2', + version='0.7.3', description='Access Wink devices via the Wink API', url='http://github.com/bradsk88/python-wink', author='Brad Johnson', @@ -9,5 +9,5 @@ install_requires=['requests>=2.0'], tests_require=['mock'], test_suite='tests', - packages=find_packages(exclude=['dist', 'test*']), + packages=find_packages(exclude=["dist", "*.test", "*.test.*", "test.*", "test"]), zip_safe=True) From cdd6b8c246108593da5bb1a3eb43a6c2af932081 Mon Sep 17 00:00:00 2001 From: bradsk88 <7kcwKg60tdcW> Date: Tue, 5 Apr 2016 21:34:50 -0600 Subject: [PATCH 054/178] Can now require desired_state to have been reached before updating state --- src/pywink/devices/base.py | 2 +- src/setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pywink/devices/base.py b/src/pywink/devices/base.py index bf1642d..0d579f6 100644 --- a/src/pywink/devices/base.py +++ b/src/pywink/devices/base.py @@ -44,7 +44,7 @@ def _update_state_from_response(self, response_json, require_desired_state_fulfi """ response_json = response_json.get('data') if response_json and require_desired_state_fulfilled: - if not is_desired_state_reached(response_json[0]): + if not is_desired_state_reached(response_json): return self.json_state = response_json diff --git a/src/setup.py b/src/setup.py index 4ea9fd7..ce9ec9b 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='0.7.3', + version='0.7.3.2', description='Access Wink devices via the Wink API', url='http://github.com/bradsk88/python-wink', author='Brad Johnson', From 671b8e0fd50d63e39c4acdcf16c22f7eea065774 Mon Sep 17 00:00:00 2001 From: bradsk88 <7kcwKg60tdcW> Date: Tue, 5 Apr 2016 22:21:32 -0600 Subject: [PATCH 055/178] Use last reading of desired brightness as "current brightness" when changing color. --- src/pywink/devices/standard/bulb.py | 3 ++- src/pywink/test/devices/standard/bulb_test.py | 12 +++++++++--- src/setup.py | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/pywink/devices/standard/bulb.py b/src/pywink/devices/standard/bulb.py index 905eb6c..6c3d4b5 100644 --- a/src/pywink/devices/standard/bulb.py +++ b/src/pywink/devices/standard/bulb.py @@ -80,7 +80,8 @@ def set_state(self, state, brightness=None, color_state = self._format_color_data(color_hue_saturation, color_kelvin, color_xy) desired_state.update(color_state) - brightness = brightness if brightness is not None else self.json_state.get('brightness', 1) + brightness = brightness if brightness is not None \ + else self.json_state.get('last_reading', {}).get('desired_brightness', 1) desired_state.update({ 'brightness': brightness }) diff --git a/src/pywink/test/devices/standard/bulb_test.py b/src/pywink/test/devices/standard/bulb_test.py index a966c4b..bd526a8 100644 --- a/src/pywink/test/devices/standard/bulb_test.py +++ b/src/pywink/test/devices/standard/bulb_test.py @@ -70,7 +70,9 @@ def setUp(self): def test_should_send_current_brightness_to_api_if_only_color_temperature_is_provided_and_bulb_only_supports_temperature(self): original_brightness = 0.5 bulb = WinkBulb({ - 'brightness': original_brightness, + 'last_reading': { + 'desired_brightness': original_brightness + }, 'capabilities': { 'color_changeable': True, 'fields': [{ @@ -102,7 +104,9 @@ def test_should_send_current_brightness_to_api_if_only_color_temperature_is_prov self): original_brightness = 0.5 bulb = WinkBulb({ - 'brightness': original_brightness, + 'last_reading': { + 'desired_brightness': original_brightness + }, 'capabilities': { 'color_changeable': True, 'fields': [{'field': 'hue'}, @@ -133,7 +137,9 @@ def test_should_send_current_hue_and_saturation_to_api_if_hue_and_sat_are_provid def test_should_send_original_brightness_when_only_xy_color_given_and_only_hue_saturation_supported(self): original_brightness = 0.5 bulb = WinkBulb({ - 'brightness': original_brightness, + 'last_reading': { + 'desired_brightness': original_brightness + }, 'capabilities': { 'color_changeable': True, 'fields': [{'field': 'hue'}, diff --git a/src/setup.py b/src/setup.py index ce9ec9b..4ea9fd7 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='0.7.3.2', + version='0.7.3', description='Access Wink devices via the Wink API', url='http://github.com/bradsk88/python-wink', author='Brad Johnson', From 688b5ea8c59bae519bd4edd3b4b1654e4e5a0070 Mon Sep 17 00:00:00 2001 From: Brad Johnson Date: Tue, 5 Apr 2016 22:43:06 -0600 Subject: [PATCH 056/178] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a8363a2..41c0c7e 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ Python Wink API --------------- -[![Join the chat at https://gitter.im/bradsk88/python-wink](https://badges.gitter.im/bradsk88/python-wink.svg)](https://gitter.im/bradsk88/python-wink?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Join the chat at https://gitter.im/bradsk88/python-wink](https://badges.gitter.im/bradsk88/python-wink.svg)](https://gitter.im/bradsk88/python-wink?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Build Status](https://travis-ci.org/bradsk88/python-wink.svg?branch=master)](https://travis-ci.org/bradsk88/python-wink) _This script used to be part of Home Assistant. It has been extracted to fit the goal of Home Assistant to not contain any device specific API implementations From 7750a8d4cd9dec6ddf24e2d4326ead42617cf976 Mon Sep 17 00:00:00 2001 From: bradsk88 <7kcwKg60tdcW> Date: Tue, 5 Apr 2016 22:45:15 -0600 Subject: [PATCH 057/178] Upping version --- CHANGELOG.md | 3 +++ src/setup.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61416b4..8f6a812 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 0.7.4 +- Fixed bug where we shouldn't have been indexing into an object + ## 0.7.3 - Can now require desired_state to have been reached before updating state diff --git a/src/setup.py b/src/setup.py index 4ea9fd7..eff4cf6 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='0.7.3', + version='0.7.4', description='Access Wink devices via the Wink API', url='http://github.com/bradsk88/python-wink', author='Brad Johnson', From 853ee9ebab7344e1d38e21a0cfbfafac71240cc7 Mon Sep 17 00:00:00 2001 From: Brad Johnson Date: Sun, 17 Apr 2016 16:38:07 -0600 Subject: [PATCH 058/178] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 41c0c7e..803ff35 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ Python Wink API --------------- [![Join the chat at https://gitter.im/bradsk88/python-wink](https://badges.gitter.im/bradsk88/python-wink.svg)](https://gitter.im/bradsk88/python-wink?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Build Status](https://travis-ci.org/bradsk88/python-wink.svg?branch=master)](https://travis-ci.org/bradsk88/python-wink) +[![Coverage Status](https://coveralls.io/repos/github/bradsk88/python-wink/badge.svg?branch=master)](https://coveralls.io/github/bradsk88/python-wink?branch=master) _This script used to be part of Home Assistant. It has been extracted to fit the goal of Home Assistant to not contain any device specific API implementations From 87fd5b66acf40a007e8d70a940c1166718ea43e7 Mon Sep 17 00:00:00 2001 From: Brad Johnson Date: Sun, 17 Apr 2016 16:51:37 -0600 Subject: [PATCH 059/178] Update README.md --- README.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/README.md b/README.md index 803ff35..ed3d9f6 100644 --- a/README.md +++ b/README.md @@ -8,12 +8,6 @@ _This script used to be part of Home Assistant. It has been extracted to fit the goal of Home Assistant to not contain any device specific API implementations but rely on open-source implementations of the API._ -## Authentication - -You will need a Wink API bearer token to communicate with the Wink server. - -[Get yours using this web app.](https://winkbearertoken.appspot.com/) - ## Example usage ```python From 53e396f6a1242c5a57e87a1ef2af30247c9bf32f Mon Sep 17 00:00:00 2001 From: Brad Johnson Date: Sun, 17 Apr 2016 16:51:56 -0600 Subject: [PATCH 060/178] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ed3d9f6..e2c70bf 100644 --- a/README.md +++ b/README.md @@ -16,5 +16,5 @@ pywink.set_bearer_token('YOUR_BEARER_TOKEN') for switch in pywink.get_switches(): print(switch.name(), switch.state()) - switch.set_state(!switch.state()) + switch.set_state(not switch.state()) ``` From 757c1d3e8346459ab1e9158a92138ec4a9d32312 Mon Sep 17 00:00:00 2001 From: bradsk88 <7kcwKg60tdcW> Date: Sun, 17 Apr 2016 17:09:03 -0600 Subject: [PATCH 061/178] Adding coveralls for coverage checking --- .coveragerc | 2 ++ .gitignore | 1 + .travis.yml | 2 ++ script/before_install | 2 +- script/test | 9 ++------- 5 files changed, 8 insertions(+), 8 deletions(-) create mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..54fd6ad --- /dev/null +++ b/.coveragerc @@ -0,0 +1,2 @@ +[run] +omit = src/setup.py, src/*/__init__.py diff --git a/.gitignore b/.gitignore index 0a76e55..a051acf 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ /test.py /.cache /src/python_wink.egg-info +/.coverage diff --git a/.travis.yml b/.travis.yml index 449c433..d8c4d65 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,5 +8,7 @@ install: - script/before_install script: - script/test +after_success: + - coveralls matrix: fast_finish: true diff --git a/script/before_install b/script/before_install index 75fb5dd..f7ab09e 100755 --- a/script/before_install +++ b/script/before_install @@ -3,4 +3,4 @@ install: python3 -m pip install --upgrade requests>=2,<3 echo "Installing development dependencies.." - python3 -m pip install --upgrade flake8 pylint coveralls pytest pytest-cov + python3 -m pip install --upgrade flake8 pylint python-coveralls pytest pytest-cov diff --git a/script/test b/script/test index d407f57..b1d69f6 100755 --- a/script/test +++ b/script/test @@ -11,13 +11,8 @@ LINT_STATUS=$? echo "Running tests..." -if [ "$1" = "coverage" ]; then - py.test --cov --cov-report= - TEST_STATUS=$? -else - py.test - TEST_STATUS=$? -fi +py.test --cov=src +TEST_STATUS=$? if [ $LINT_STATUS -eq 0 ] then From 751c0643b909d4242fbbbff241b1de3d3465c7db Mon Sep 17 00:00:00 2001 From: bradsk88 <7kcwKg60tdcW> Date: Tue, 26 Apr 2016 22:19:40 -0600 Subject: [PATCH 062/178] Fix Status Updates My unit tests were making an incorrect assumption about the format of status dicts from the Wink API. This was causing all status updates to fail. This should fix issue #35 --- CHANGELOG.md | 3 + src/pywink/devices/base.py | 13 +- src/pywink/devices/standard/base.py | 5 - src/pywink/domain/devices.py | 8 +- .../standard/api_responses/__init__.py | 14 ++ ...light_bulb_with_desired_state_reached.json | 128 ++++++++++++++++++ src/pywink/test/domain/devices_test.py | 49 +++++-- src/setup.py | 2 +- 8 files changed, 199 insertions(+), 23 deletions(-) create mode 100644 src/pywink/test/devices/standard/api_responses/__init__.py create mode 100644 src/pywink/test/devices/standard/api_responses/light_bulb_with_desired_state_reached.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f6a812..b3b2a0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 0.7.5 +- Fixed bug where light bulb states were not updating. + ## 0.7.4 - Fixed bug where we shouldn't have been indexing into an object diff --git a/src/pywink/devices/base.py b/src/pywink/devices/base.py index 0d579f6..712c7e4 100644 --- a/src/pywink/devices/base.py +++ b/src/pywink/devices/base.py @@ -42,13 +42,14 @@ def _update_state_from_response(self, response_json, require_desired_state_fulfi :param response_json: the json obj returned from query :return: """ - response_json = response_json.get('data') - if response_json and require_desired_state_fulfilled: - if not is_desired_state_reached(response_json): - return - self.json_state = response_json + _response_json = response_json.get('data') + if _response_json and require_desired_state_fulfilled: + if not is_desired_state_reached(_response_json): + return False + self.json_state = _response_json + return True def update_state(self, require_desired_state_fulfilled=False): """ Update state with latest info from Wink API. """ response = self.api_interface.get_device_state(self) - self._update_state_from_response(response, require_desired_state_fulfilled) + return self._update_state_from_response(response, require_desired_state_fulfilled) diff --git a/src/pywink/devices/standard/base.py b/src/pywink/devices/standard/base.py index 02b1598..34a6e4a 100644 --- a/src/pywink/devices/standard/base.py +++ b/src/pywink/devices/standard/base.py @@ -22,11 +22,6 @@ def __repr__(self): 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): diff --git a/src/pywink/domain/devices.py b/src/pywink/domain/devices.py index 336b369..e95dbc3 100644 --- a/src/pywink/domain/devices.py +++ b/src/pywink/domain/devices.py @@ -3,8 +3,12 @@ def is_desired_state_reached(wink_device_state): :type wink_device: dict """ desired_state = wink_device_state.get('desired_state', {}) - for name, value in desired_state.items(): - if value != wink_device_state.get(name): + last_reading = wink_device_state.get('last_reading', {}) + if not last_reading.get('connection', True): + return True + for name, desired_value in desired_state.items(): + latest_value = last_reading.get(name) + if desired_value != latest_value: return False return True diff --git a/src/pywink/test/devices/standard/api_responses/__init__.py b/src/pywink/test/devices/standard/api_responses/__init__.py new file mode 100644 index 0000000..c576e6e --- /dev/null +++ b/src/pywink/test/devices/standard/api_responses/__init__.py @@ -0,0 +1,14 @@ +import json +import os + + +class ApiResponseJSONLoader(object): + + def __init__(self, file_name): + self.file_name = file_name + + def load(self): + with open('{}/{}'.format(os.path.dirname(__file__), + self.file_name)) as json_file: + response_dict = json.load(json_file) + return response_dict diff --git a/src/pywink/test/devices/standard/api_responses/light_bulb_with_desired_state_reached.json b/src/pywink/test/devices/standard/api_responses/light_bulb_with_desired_state_reached.json new file mode 100644 index 0000000..fac6e4a --- /dev/null +++ b/src/pywink/test/devices/standard/api_responses/light_bulb_with_desired_state_reached.json @@ -0,0 +1,128 @@ +{ + "pagination": {}, + "errors": [], + "data": { + "location": "", + "device_manufacturer": "eastfield", + "hidden_at": null, + "upc_id": "309", + "created_at": 1459823491, + "gang_id": null, + "light_bulb_id": "1591581", + "local_id": "5", + "manufacturer_device_id": null, + "capabilities": { + "fields": [ + { + "type": "boolean", + "field": "connection", + "mutability": "read-only" + }, + { + "type": "boolean", + "field": "powered", + "mutability": "read-write" + }, + { + "type": "percentage", + "field": "brightness", + "mutability": "read-write" + }, + { + "type": "string", + "choices": [ + "rgb", + "hsb", + "color_temperature" + ], + "field": "color_model" + }, + { + "type": "percentage", + "field": "hue", + "mutability": "read-write" + }, + { + "type": "percentage", + "field": "saturation", + "mutability": "read-write" + }, + { + "type": "integer", + "field": "color_temperature", + "mutability": "read-write", + "range": [ + 2700, + 6500 + ] + } + ], + "color_changeable": true + }, + "linked_service_id": null, + "triggers": [], + "lat_lng": [ + 52.113447, + -106.61415 + ], + "desired_state": { + "powered": true, + "color_temperature": 5000, + "hue": 0.0, + "saturation": 0.0, + "color_model": "hsb", + "brightness": 1.0 + }, + "name": "Brad's Room 2", + "hub_id": "300039", + "manufacturer_device_model": "eastfield_light_bulb_rgbw", + "uuid": "77773adb-93ba-40d9-9e6c-6ff562c810b5", + "upc_code": "eastfield_ecosmart_rgbw", + "locale": "en_us", + "last_reading": { + "firmware_version_updated_at": 1461722879.9097676, + "desired_color_temperature_updated_at": 1461694696.0741637, + "desired_color_model_changed_at": 1461694696.0741637, + "desired_saturation_updated_at": 1461694696.0741637, + "desired_brightness_updated_at": 1461694696.0741637, + "firmware_date_code_changed_at": 1459823493.5070922, + "desired_powered_changed_at": 1461694696.0741637, + "desired_brightness_changed_at": 1461694696.0741637, + "brightness": 1.0, + "color_model_changed_at": 1461641413.3958035, + "connection": true, + "brightness_changed_at": 1461722879.9097676, + "hue": 0.0, + "color_model_updated_at": 1461722879.9097676, + "desired_saturation_changed_at": 1461694696.0741637, + "color_model": "hsb", + "connection_changed_at": 1461722278.9044788, + "saturation": 0.0, + "desired_color_model_updated_at": 1461694696.0741637, + "desired_powered_updated_at": 1461694696.0741637, + "saturation_changed_at": 1461722279.1130762, + "powered": true, + "color_temperature": 5000, + "hue_updated_at": 1461722879.9097676, + "color_temperature_changed_at": 1461677499.2378418, + "connection_updated_at": 1461722879.9097676, + "firmware_version_changed_at": 1459823856.2130384, + "firmware_date_code_updated_at": 1461722879.9097676, + "powered_changed_at": 1461670354.5106528, + "color_temperature_updated_at": 1461722879.9097676, + "powered_updated_at": 1461722879.9097676, + "desired_hue_updated_at": 1461694696.0741637, + "desired_hue_changed_at": 1461694696.0741637, + "brightness_updated_at": 1461722879.9097676, + "desired_color_temperature_changed_at": 1461677683.7542028, + "firmware_version": "0.2b10 / 0.2b15", + "saturation_updated_at": 1461722879.9097676, + "hue_changed_at": 1461722278.9044788, + "firmware_date_code": "" + }, + "model_name": "EcoSmart Light RGBW Bulb", + "radio_type": "zigbee", + "order": 0, + "units": {} + } +} diff --git a/src/pywink/test/domain/devices_test.py b/src/pywink/test/domain/devices_test.py index 9f62181..4f3233c 100644 --- a/src/pywink/test/domain/devices_test.py +++ b/src/pywink/test/domain/devices_test.py @@ -1,7 +1,7 @@ import unittest from pywink.domain.devices import is_desired_state_reached - +from pywink.test.devices.standard.api_responses import ApiResponseJSONLoader class IsDesiredStateReachedTests(unittest.TestCase): @@ -11,7 +11,9 @@ def test_should_return_true_for_bulb_if_desired_brightness_matches_actual_bright 'desired_state': { 'brightness': brightness }, - 'brightness': brightness + 'last_reading': { + 'brightness': brightness + } } self.assertTrue(is_desired_state_reached(bulb_state)) @@ -22,7 +24,9 @@ def test_should_return_false_for_bulb_if_desired_brightness_does_not_match_actua 'desired_state': { 'brightness': brightness_1 }, - 'brightness': brightness_2 + 'last_reading': { + 'brightness': brightness_2 + } } self.assertFalse(is_desired_state_reached(bulb_state)) @@ -32,7 +36,9 @@ def test_should_return_true_for_bulb_if_desired_hue_matches_actual_hue(self): 'desired_state': { 'hue': hue }, - 'hue': hue + 'last_reading': { + 'hue': hue + } } self.assertTrue(is_desired_state_reached(bulb_state)) @@ -43,7 +49,9 @@ def test_should_return_false_for_bulb_if_desired_hue_does_not_match_actual_hue(s 'desired_state': { 'hue': hue_1 }, - 'hue': hue_2 + 'last_reading': { + 'hue': hue_2 + } } self.assertFalse(is_desired_state_reached(bulb_state)) @@ -53,7 +61,9 @@ def test_should_return_true_for_bulb_if_desired_saturation_matches_actual_satura 'desired_state': { 'saturation': saturation }, - 'saturation': saturation + 'last_reading': { + 'saturation': saturation + } } self.assertTrue(is_desired_state_reached(bulb_state)) @@ -64,7 +74,9 @@ def test_should_return_false_for_bulb_if_desired_saturation_does_not_match_actua 'desired_state': { 'saturation': saturation_1 }, - 'saturation': saturation_2 + 'last_reading': { + 'saturation': saturation_2 + } } self.assertFalse(is_desired_state_reached(bulb_state)) @@ -74,7 +86,9 @@ def test_should_return_true_for_bulb_if_desired_powered_matches_actual_powered(s 'desired_state': { 'powered': powered }, - 'powered': powered + 'last_reading': { + 'powered': powered + } } self.assertTrue(is_desired_state_reached(bulb_state)) @@ -85,7 +99,24 @@ def test_should_return_false_for_bulb_if_desired_powered_does_not_match_actual_p 'desired_state': { 'powered': powered_1 }, - 'powered': powered_2 + 'last_reading': { + 'powered': powered_2 + } } self.assertFalse(is_desired_state_reached(bulb_state)) + def test_should_return_true_for_real_state_which_where_desired_state_is_reached(self): + response_dict = ApiResponseJSONLoader('light_bulb_with_desired_state_reached.json').load()['data'] + self.assertTrue(is_desired_state_reached(response_dict)) + + def test_should_return_true_if_device_is_disconnected(self): + bulb_state = { + 'desired_state': { + 'powered': True + }, + 'last_reading': { + 'connection': False, + 'powered': False + } + } + self.assertTrue(is_desired_state_reached(bulb_state)) diff --git a/src/setup.py b/src/setup.py index eff4cf6..95f3d1f 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='0.7.4', + version='0.7.5', description='Access Wink devices via the Wink API', url='http://github.com/bradsk88/python-wink', author='Brad Johnson', From cdc4eefb9d95801376e9b34b346aa2bc492d53f2 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Thu, 14 Apr 2016 09:56:22 -0400 Subject: [PATCH 063/178] Added method to return battery level --- src/pywink/devices/base.py | 4 ++++ src/pywink/devices/sensors.py | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/src/pywink/devices/base.py b/src/pywink/devices/base.py index 712c7e4..e3be35e 100644 --- a/src/pywink/devices/base.py +++ b/src/pywink/devices/base.py @@ -37,6 +37,10 @@ def _last_reading(self): def available(self): return self._last_reading.get('connection', False) + @property + def battery_level(self): + return self._last_reading.get('battery', False) + def _update_state_from_response(self, response_json, require_desired_state_fulfilled=False): """ :param response_json: the json obj returned from query diff --git a/src/pywink/devices/sensors.py b/src/pywink/devices/sensors.py index cf0c08c..4008020 100644 --- a/src/pywink/devices/sensors.py +++ b/src/pywink/devices/sensors.py @@ -31,6 +31,13 @@ def name(self): name += " " + self._capability return name + @property + def battery_level(self): + if not self._last_reading.get('external_power', False): + return self._last_reading.get('battery', False) + else: + return False + def device_id(self): root_name = self.json_state.get('sensor_pod_id', self.name()) return '{}+{}'.format(root_name, self._capability) From b9f472923fc5ba55713fe89552f1de0c12e7b649 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Fri, 15 Apr 2016 08:20:42 -0400 Subject: [PATCH 064/178] Added tests and updated version --- CHANGELOG.md | 3 +++ src/pywink/devices/base.py | 2 +- src/pywink/devices/sensors.py | 4 ++-- .../api_responses/quirky_spotter_2.json | 2 +- src/pywink/test/devices/standard/init_test.py | 19 +++++++++++++++++++ src/setup.py | 2 +- 6 files changed, 27 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3b2a0e..be410c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 0.7.6 +- Added ability to return the battery level if a device is battery powered + ## 0.7.5 - Fixed bug where light bulb states were not updating. diff --git a/src/pywink/devices/base.py b/src/pywink/devices/base.py index e3be35e..fda720b 100644 --- a/src/pywink/devices/base.py +++ b/src/pywink/devices/base.py @@ -39,7 +39,7 @@ def available(self): @property def battery_level(self): - return self._last_reading.get('battery', False) + return self._last_reading.get('battery', None) def _update_state_from_response(self, response_json, require_desired_state_fulfilled=False): """ diff --git a/src/pywink/devices/sensors.py b/src/pywink/devices/sensors.py index 4008020..3d8453d 100644 --- a/src/pywink/devices/sensors.py +++ b/src/pywink/devices/sensors.py @@ -34,9 +34,9 @@ def name(self): @property def battery_level(self): if not self._last_reading.get('external_power', False): - return self._last_reading.get('battery', False) + return self._last_reading.get('battery', None) else: - return False + return None def device_id(self): root_name = self.json_state.get('sensor_pod_id', self.name()) diff --git a/src/pywink/test/devices/standard/api_responses/quirky_spotter_2.json b/src/pywink/test/devices/standard/api_responses/quirky_spotter_2.json index 51295f9..55b52ae 100644 --- a/src/pywink/test/devices/standard/api_responses/quirky_spotter_2.json +++ b/src/pywink/test/devices/standard/api_responses/quirky_spotter_2.json @@ -12,7 +12,7 @@ "battery_updated_at": 1453188090.419577, "brightness": 0, "brightness_updated_at": 1453188090.419577, - "external_power": true, + "external_power": false, "external_power_updated_at": 1453188090.419577, "humidity": 27, "humidity_updated_at": 1453188090.419577, diff --git a/src/pywink/test/devices/standard/init_test.py b/src/pywink/test/devices/standard/init_test.py index acda8c1..cc00e97 100644 --- a/src/pywink/test/devices/standard/init_test.py +++ b/src/pywink/test/devices/standard/init_test.py @@ -258,6 +258,24 @@ def test_device_id_should_start_with_a_number(self): device_id = sensor.device_id() self.assertRegex(device_id, "^[0-9]{4,6}") + def test_battery_level_should_return_none(self): + with open('{}/api_responses/quirky_spotter.json'.format(os.path.dirname(__file__))) as spotter_file: + response_dict = json.load(spotter_file) + + sensors = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SENSOR_POD]) + + for sensor in sensors: + self.assertIsNone(sensor.battery_level) + + def test_battery_level_should_return_float(self): + with open('{}/api_responses/quirky_spotter_2.json'.format(os.path.dirname(__file__))) as spotter_file: + response_dict = json.load(spotter_file) + + sensors = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SENSOR_POD]) + + for sensor in sensors: + self.assertEqual(sensor.battery_level, 0.86) + class WinkCapabilitySensorTests(unittest.TestCase): @@ -276,3 +294,4 @@ def test_should_call_get_state_endpoint_with_capability_removed_from_id(self): sensor.update_state() self.api_interface.get_device_state.assert_called_once_with(sensor, expected_id) + diff --git a/src/setup.py b/src/setup.py index 95f3d1f..1e2b800 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='0.7.5', + version='0.7.6', description='Access Wink devices via the Wink API', url='http://github.com/bradsk88/python-wink', author='Brad Johnson', From f4faef43e82c77377e33e03c48d74e128128cac5 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Wed, 27 Apr 2016 08:05:34 -0400 Subject: [PATCH 065/178] Changed False to None in sensor --- src/pywink/devices/sensors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pywink/devices/sensors.py b/src/pywink/devices/sensors.py index 3d8453d..c42203a 100644 --- a/src/pywink/devices/sensors.py +++ b/src/pywink/devices/sensors.py @@ -33,7 +33,7 @@ def name(self): @property def battery_level(self): - if not self._last_reading.get('external_power', False): + if not self._last_reading.get('external_power', None): return self._last_reading.get('battery', None) else: return None From 92a20e63b2a161e4c99aed917d38c2981ed99bc7 Mon Sep 17 00:00:00 2001 From: Phil Kates Date: Wed, 18 May 2016 18:37:21 -0700 Subject: [PATCH 066/178] Add support for Wink Shades Documentation here: http://docs.wink.apiary.io/#reference/device/blind --- src/pywink/__init__.py | 3 +- src/pywink/api.py | 4 ++ src/pywink/devices/factory.py | 4 +- src/pywink/devices/standard/__init__.py | 39 ++++++++++++++++ src/pywink/devices/types.py | 2 + .../devices/standard/api_responses/shade.json | 44 +++++++++++++++++++ src/pywink/test/devices/standard/bulb_test.py | 1 - src/pywink/test/devices/standard/init_test.py | 25 ++++++++++- 8 files changed, 116 insertions(+), 6 deletions(-) create mode 100644 src/pywink/test/devices/standard/api_responses/shade.json diff --git a/src/pywink/__init__.py b/src/pywink/__init__.py index 12d2f70..ff56951 100644 --- a/src/pywink/__init__.py +++ b/src/pywink/__init__.py @@ -4,5 +4,4 @@ # noqa from pywink.api import set_bearer_token, set_wink_credentials, get_bulbs, \ get_eggtrays, get_garage_doors, get_locks, get_powerstrip_outlets, \ - get_sensors, get_sirens, get_switches, get_devices, is_token_set - + get_sensors, get_shades, get_sirens, get_switches, get_devices, is_token_set diff --git a/src/pywink/api.py b/src/pywink/api.py index d0b0102..4758c1d 100644 --- a/src/pywink/api.py +++ b/src/pywink/api.py @@ -93,6 +93,10 @@ def get_garage_doors(): return get_devices(device_types.GARAGE_DOOR) +def get_shades(): + return get_devices(device_types.SHADE) + + def get_powerstrip_outlets(): return get_devices(device_types.POWER_STRIP) diff --git a/src/pywink/devices/factory.py b/src/pywink/devices/factory.py index 1d38ccf..a8df202 100644 --- a/src/pywink/devices/factory.py +++ b/src/pywink/devices/factory.py @@ -1,7 +1,7 @@ from pywink.devices.base import WinkDevice from pywink.devices.sensors import WinkSensorPod from pywink.devices.standard import WinkBulb, WinkBinarySwitch, WinkPowerStripOutlet, WinkLock, \ - WinkEggTray, WinkGarageDoor, WinkSiren + WinkEggTray, WinkGarageDoor, WinkShade, WinkSiren def build_device(device_state_as_json, api_interface): @@ -25,6 +25,8 @@ def build_device(device_state_as_json, api_interface): new_object = WinkEggTray(device_state_as_json, api_interface) elif "garage_door_id" in device_state_as_json: new_object = WinkGarageDoor(device_state_as_json, api_interface) + elif "shade_id" in device_state_as_json: + new_object = WinkShade(device_state_as_json, api_interface) elif "siren_id" in device_state_as_json: new_object = WinkSiren(device_state_as_json, api_interface) diff --git a/src/pywink/devices/standard/__init__.py b/src/pywink/devices/standard/__init__.py index eae197c..db63198 100644 --- a/src/pywink/devices/standard/__init__.py +++ b/src/pywink/devices/standard/__init__.py @@ -253,6 +253,44 @@ def _recent_state_set(self): return time.time() - self._last_call[0] < 15 +class WinkShade(WinkDevice): + def __init__(self, device_state_as_json, api_interface, objectprefix="shades"): + super(WinkShade, self).__init__(device_state_as_json, api_interface, + 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('shade_id', self.name()) + + 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 set_state(self, state): + """ + :param state: a number of 1 ('open') or 0 ('close') + :return: nothing + """ + values = {"desired_state": {"position": state}} + response = self.api_interface.set_device_state(self, values) + self._update_state_from_response(response) + + self._last_call = (time.time(), state) + # self._state = state + + 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) @@ -280,4 +318,5 @@ def device_id(self): WinkLock.__name__, WinkPowerStripOutlet.__name__, WinkGarageDoor.__name__, + WinkShade.__name__, WinkSiren.__name__] diff --git a/src/pywink/devices/types.py b/src/pywink/devices/types.py index 23904b4..b51f7cd 100644 --- a/src/pywink/devices/types.py +++ b/src/pywink/devices/types.py @@ -5,6 +5,7 @@ EGG_TRAY = 'eggtray' GARAGE_DOOR = 'garage_door' POWER_STRIP = 'powerstrip' +SHADE = 'shades' SIREN = 'siren' DEVICE_ID_KEYS = { @@ -15,5 +16,6 @@ LOCK: 'lock_id', POWER_STRIP: 'powerstrip_id', SENSOR_POD: 'sensor_pod_id', + SHADE: 'shade_id', SIREN: 'siren_id' } diff --git a/src/pywink/test/devices/standard/api_responses/shade.json b/src/pywink/test/devices/standard/api_responses/shade.json new file mode 100644 index 0000000..535157c --- /dev/null +++ b/src/pywink/test/devices/standard/api_responses/shade.json @@ -0,0 +1,44 @@ +{ + "data": [{ + "uuid": "07b44f75-c7ee-48f1-b43a-f564c15fed1b", + "desired_state": { + "position": null + }, + "last_reading": { + "connection": true, + "connection_updated_at": 1463494385.449129, + "position": null, + "position_updated_at": 1463494385.449129, + "desired_position_updated_at": 1463500774.348026, + "connection_changed_at": 1457674189.5065048, + "desired_position_changed_at": 1463500774.348026 + }, + "subscription": { + "pubnub": { + "subscribe_key": "sub-c-f7bf7f7e-0542-11e3-a5e8-02ee2ddab7fe", + "channel": "31be109cff295db0c6d08d411fac0133e46a6138|shade-5650|user-410445" + } + }, + "shade_id": "5650", + "name": "Left Bed Shade", + "locale": "en_us", + "units": {}, + "created_at": 1457674189, + "hidden_at": null, + "capabilities": {}, + "triggers": [], + "manufacturer_device_model": "somfy_bali", + "manufacturer_device_id": null, + "device_manufacturer": "somfy", + "model_name": "Shade", + "upc_id": "81", + "upc_code": "SOMFY", + "hub_id": "344883", + "local_id": "6", + "radio_type": "zwave", + "lat_lng": [37.813847, -122.277662], + "location": "94607" + }], + "errors": [], + "pagination": {} +} diff --git a/src/pywink/test/devices/standard/bulb_test.py b/src/pywink/test/devices/standard/bulb_test.py index bd526a8..94ee0bf 100644 --- a/src/pywink/test/devices/standard/bulb_test.py +++ b/src/pywink/test/devices/standard/bulb_test.py @@ -234,4 +234,3 @@ def test_device_id_should_be_number(self): wink_light = WinkBulb(light, self.api_interface) device_id = wink_light.device_id() self.assertRegex(device_id, "^[0-9]{4,6}$") - diff --git a/src/pywink/test/devices/standard/init_test.py b/src/pywink/test/devices/standard/init_test.py index cc00e97..5f4d58c 100644 --- a/src/pywink/test/devices/standard/init_test.py +++ b/src/pywink/test/devices/standard/init_test.py @@ -10,7 +10,7 @@ WinkSoundPresenceSensor, WinkVibrationPresenceSensor, WinkTemperatureSensor, \ _WinkCapabilitySensor from pywink.devices.standard import WinkBulb, WinkGarageDoor, WinkPowerStripOutlet, WinkSiren, WinkLock, \ - WinkBinarySwitch, WinkEggTray + WinkShade, WinkBinarySwitch, WinkEggTray from pywink.devices.types import DEVICE_ID_KEYS @@ -68,6 +68,28 @@ def test_device_id_should_be_number(self): self.assertRegex(device_id, "^[0-9]{4,6}$") +class ShadeTests(unittest.TestCase): + def setUp(self): + super(ShadeTests, self).setUp() + self.api_interface = mock.MagicMock() + + def test_should_handle_shade_response(self): + with open('{}/api_responses/shade.json'.format(os.path.dirname(__file__))) as shade_file: + response_dict = json.load(shade_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SHADE]) + self.assertEqual(1, len(devices)) + self.assertIsInstance(devices[0], WinkShade) + + def test_device_id_should_be_number(self): + with open('{}/api_responses/shade.json'.format(os.path.dirname(__file__))) as shade_file: + response_dict = json.load(shade_file) + print(response_dict) + shade = response_dict.get('data')[0] + wink_shade = WinkShade(shade, self.api_interface) + device_id = wink_shade.device_id() + self.assertRegex(device_id, "^[0-9]{4,6}$") + + class SirenTests(unittest.TestCase): def setUp(self): @@ -294,4 +316,3 @@ def test_should_call_get_state_endpoint_with_capability_removed_from_id(self): sensor.update_state() self.api_interface.get_device_state.assert_called_once_with(sensor, expected_id) - From 24c837604a095a8a52e79fa507aa5292577f93b2 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Tue, 24 May 2016 11:53:13 -0400 Subject: [PATCH 067/178] Fixed powerstrip update state --- src/pywink/devices/standard/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pywink/devices/standard/__init__.py b/src/pywink/devices/standard/__init__.py index eae197c..fa36dc1 100644 --- a/src/pywink/devices/standard/__init__.py +++ b/src/pywink/devices/standard/__init__.py @@ -126,7 +126,7 @@ def update_state(self, require_desired_state_fulfilled=False): response = self.api_interface.get_device_state(self, id_override=self.parent_id()) power_strip = response.get('data') if require_desired_state_fulfilled: - if not is_desired_state_reached(power_strip[0]): + if not is_desired_state_reached(power_strip[self.index]): return power_strip_reading = power_strip.get('last_reading') From f07381d7443e967175901ee94fe202823166f78b Mon Sep 17 00:00:00 2001 From: Brad Johnson Date: Wed, 25 May 2016 21:05:12 -0600 Subject: [PATCH 068/178] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e2c70bf..ee7ef3e 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ Python Wink API --------------- -[![Join the chat at https://gitter.im/bradsk88/python-wink](https://badges.gitter.im/bradsk88/python-wink.svg)](https://gitter.im/bradsk88/python-wink?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Build Status](https://travis-ci.org/bradsk88/python-wink.svg?branch=master)](https://travis-ci.org/bradsk88/python-wink) -[![Coverage Status](https://coveralls.io/repos/github/bradsk88/python-wink/badge.svg?branch=master)](https://coveralls.io/github/bradsk88/python-wink?branch=master) +[![Join the chat at https://gitter.im/python-wink/python-wink](https://badges.gitter.im/python-wink/python-wink.svg)](https://gitter.im/bradsk88/python-wink?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Build Status](https://travis-ci.org/python-wink/python-wink.svg?branch=master)](https://travis-ci.org/python-wink/python-wink) +[![Coverage Status](https://coveralls.io/repos/github/python-wink/python-wink/badge.svg?branch=master)](https://coveralls.io/github/python-wink/python-wink?branch=master) _This script used to be part of Home Assistant. It has been extracted to fit the goal of Home Assistant to not contain any device specific API implementations From f969fd56fa3f508b9d738298e76b2dd74c5c80db Mon Sep 17 00:00:00 2001 From: bradsk88 <7kcwKg60tdcW> Date: Wed, 25 May 2016 21:23:51 -0600 Subject: [PATCH 069/178] set_wink_credentials now returns the access token --- src/pywink/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pywink/api.py b/src/pywink/api.py index 4758c1d..ec311c2 100644 --- a/src/pywink/api.py +++ b/src/pywink/api.py @@ -67,6 +67,7 @@ def set_wink_credentials(client_id, client_secret, refresh_token): response_json = response.json() access_token = response_json.get('access_token') set_bearer_token(access_token) + return access_token def get_bulbs(): From dcc6fecbd0739bd7393f68579423debfdd6a6e0b Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Tue, 29 Mar 2016 19:33:08 -0400 Subject: [PATCH 070/178] Retrieve Pubnub details from json if it exists --- CHANGELOG.md | 3 +++ src/pywink/api.py | 4 ++++ src/pywink/devices/base.py | 11 +++++++++++ src/pywink/devices/sensors.py | 8 +++++++- src/pywink/devices/standard/__init__.py | 12 ++++++++++++ src/setup.py | 2 +- 6 files changed, 38 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index be410c2..fe7709f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 0.7.7 +- Added support for retrieving the Pubnub subscription details + ## 0.7.6 - Added ability to return the battery level if a device is battery powered diff --git a/src/pywink/api.py b/src/pywink/api.py index ec311c2..57f4cc1 100644 --- a/src/pywink/api.py +++ b/src/pywink/api.py @@ -184,6 +184,10 @@ def _get_subsensors_from_sensor_pod(item, api_interface): def __get_outlets_from_powerstrip(item, api_interface): outlets = item['outlets'] + for outlet in outlets: + if 'subscription' in item: + outlet['subscription'] = item['subscription'] + outlet['last_reading']['connection'] = item['last_reading']['connection'] return [build_device(outlet, api_interface) for outlet in outlets if __device_is_visible(outlet, 'outlet_id')] diff --git a/src/pywink/devices/base.py b/src/pywink/devices/base.py index fda720b..0704d57 100644 --- a/src/pywink/devices/base.py +++ b/src/pywink/devices/base.py @@ -11,6 +11,14 @@ def __init__(self, device_state_as_json, api_interface, objectprefix=None): self.api_interface = api_interface self.objectprefix = objectprefix self.json_state = device_state_as_json + subscription = self.json_state.get('subscription') + if subscription != {} and subscription is not None: + pubnub = subscription.get('pubnub') + self.pubnub_key = pubnub.get('subscribe_key') + self.pubnub_channel = pubnub.get('channel') + else: + self.pubnub_key = None + self.pubnub_channel = None def __str__(self): return "%s %s %s" % (self.name(), self.device_id(), self.state()) @@ -57,3 +65,6 @@ def update_state(self, require_desired_state_fulfilled=False): """ Update state with latest info from Wink API. """ response = self.api_interface.get_device_state(self) return self._update_state_from_response(response, require_desired_state_fulfilled) + + def pubnub_update(self, json_response): + self.json_state = json_response diff --git a/src/pywink/devices/sensors.py b/src/pywink/devices/sensors.py index c42203a..e94ce29 100644 --- a/src/pywink/devices/sensors.py +++ b/src/pywink/devices/sensors.py @@ -93,7 +93,13 @@ def humidity_percentage(self): :return: The relative humidity detected by the sensor (0% to 100%) :rtype: int """ - return self.last_reading() + # The subscription response returns a deciaml not an int + # Doubtful we will ever get a legitimate reading less than 1 + # so I think this should be safe + if self.last_reading() <= 1.0: + return int(self.last_reading() * 100) + else: + return self.last_reading() class WinkBrightnessSensor(_WinkCapabilitySensor): diff --git a/src/pywink/devices/standard/__init__.py b/src/pywink/devices/standard/__init__.py index db63198..a1ff8a4 100644 --- a/src/pywink/devices/standard/__init__.py +++ b/src/pywink/devices/standard/__init__.py @@ -129,6 +129,15 @@ def update_state(self, require_desired_state_fulfilled=False): if not is_desired_state_reached(power_strip[0]): return + def _update_state_from_response(self, response_json): + """ + :param response_json: the json obj returned from query + :return: + """ + if self.pubnub_key is not None: + power_strip = response_json + else: + 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: @@ -136,6 +145,9 @@ def update_state(self, require_desired_state_fulfilled=False): outlet['last_reading']['connection'] = power_strip_reading.get('connection') self.json_state = outlet + def pubnub_update(self, json_response): + self._update_state_from_response(json_response) + def index(self): return self.json_state.get('outlet_index', None) diff --git a/src/setup.py b/src/setup.py index 1e2b800..0aa97fc 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='0.7.6', + version='0.7.7', description='Access Wink devices via the Wink API', url='http://github.com/bradsk88/python-wink', author='Brad Johnson', From b3d11fcfc8d626bd2b649c4506c240aa048cd897 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Tue, 29 Mar 2016 19:35:57 -0400 Subject: [PATCH 071/178] Added json example with pubnub details --- .../api_responses/device_with_pubnub.json | 182 ++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 src/pywink/test/api_responses/device_with_pubnub.json diff --git a/src/pywink/test/api_responses/device_with_pubnub.json b/src/pywink/test/api_responses/device_with_pubnub.json new file mode 100644 index 0000000..c500513 --- /dev/null +++ b/src/pywink/test/api_responses/device_with_pubnub.json @@ -0,0 +1,182 @@ +{ + "data": { + "uuid": "1ccb4e6f-12d4-46a2-b9b8-e98d6f012345", + "desired_state": { + "locked": true, + "beeper_enabled": false, + "vacation_mode_enabled": false, + "auto_lock_enabled": false, + "key_code_length": 4, + "alarm_mode": null, + "alarm_sensitivity": 0.6, + "alarm_enabled": false + }, + "last_reading": { + "connection": true, + "connection_updated_at": 1458214863.9920943, + "locked": true, + "locked_updated_at": 1458214863.9920943, + "battery": 0.89, + "battery_updated_at": 1458214863.9920943, + "alarm_activated": null, + "alarm_activated_updated_at": null, + "beeper_enabled": false, + "beeper_enabled_updated_at": 1458214863.9920943, + "vacation_mode_enabled": false, + "vacation_mode_enabled_updated_at": 1458214863.9920943, + "auto_lock_enabled": false, + "auto_lock_enabled_updated_at": 1458214863.9920943, + "key_code_length": 4, + "key_code_length_updated_at": 1458214863.9920943, + "alarm_mode": null, + "alarm_mode_updated_at": 1458214863.9920943, + "alarm_sensitivity": 0.6, + "alarm_sensitivity_updated_at": 1458214863.9920943, + "alarm_enabled": false, + "alarm_enabled_updated_at": 1458214863.9920943, + "last_error": null, + "last_error_updated_at": 1450137143.224417, + "desired_locked_updated_at": 1458213884.4730809, + "desired_beeper_enabled_updated_at": 1458213884.4730809, + "desired_vacation_mode_enabled_updated_at": 1458213884.4730809, + "desired_auto_lock_enabled_updated_at": 1458213884.4730809, + "desired_key_code_length_updated_at": 1458213884.4730809, + "desired_alarm_mode_updated_at": 1458213884.4730809, + "desired_alarm_sensitivity_updated_at": 1458213884.4730809, + "desired_alarm_enabled_updated_at": 1458213884.4730809, + "connection_changed_at": 1458062585.6312222, + "locked_changed_at": 1458214863.9920943, + "battery_changed_at": 1458207220.5308204, + "beeper_enabled_changed_at": 1450888993.4345925, + "vacation_mode_enabled_changed_at": 1449960081.8146806, + "auto_lock_enabled_changed_at": 1449960081.8146806, + "alarm_enabled_changed_at": 1449960081.8146806, + "key_code_length_changed_at": 1449960081.8146806, + "alarm_sensitivity_changed_at": 1449960088.2434776, + "desired_locked_changed_at": 1458213884.4730809, + "desired_beeper_enabled_changed_at": 1450888993.5529706, + "desired_vacation_mode_enabled_changed_at": 1449960446.6464362, + "desired_auto_lock_enabled_changed_at": 1449960446.6464362, + "desired_key_code_length_changed_at": 1449960446.6464362, + "desired_alarm_mode_changed_at": 1450889063.721673, + "desired_alarm_sensitivity_changed_at": 1449960446.6464362, + "desired_alarm_enabled_changed_at": 1449960446.6464362, + "alarm_mode_changed_at": 1452080631.3350449 + }, + "subscription": { + "pubnub": { + "subscribe_key": "sub-c-f7bf7f7e-1234-11e3-a5e8-02ee2dd12345", + "channel": "2727c0b7576be60827dde4838a081ecfb6112345" + } + }, + "lock_id": "61123", + "name": "Front Door", + "locale": "en_us", + "units": {}, + "created_at": 1449960079, + "hidden_at": null, + "capabilities": { + "fields": [ + { + "field": "connection", + "type": "boolean", + "mutability": "read-only" + }, + { + "field": "locked", + "type": "boolean", + "mutability": "read-write" + }, + { + "field": "battery", + "type": "percentage", + "mutability": "read-only" + }, + { + "field": "alarm_activated", + "type": "boolean", + "mutability": "read-only" + }, + { + "field": "beeper_enabled", + "type": "boolean", + "mutability": "read-write" + }, + { + "field": "vacation_mode_enabled", + "type": "boolean", + "mutability": "read-write" + }, + { + "field": "auto_lock_enabled", + "type": "boolean", + "mutability": "read-write" + }, + { + "field": "key_code_length", + "type": "integer", + "range": [ + 4, + 8 + ], + "mutability": "read-write" + }, + { + "field": "alarm_mode", + "type": "string", + "choices": [ + "alert", + "tamper", + "forced_entry", + null + ], + "mutability": "read-write" + }, + { + "field": "alarm_sensitivity", + "type": "percentage", + "choices": [ + 0.2, + 0.4, + 0.6, + 0.8, + 1 + ], + "mutability": "read-write" + }, + { + "field": "alarm_enabled", + "type": "boolean", + "mutability": "read-write" + } + ], + "home_security_device": true + }, + "user_ids": [ + "377857" + ], + "triggers": [], + "manufacturer_device_model": "schlage_zwave_lock", + "manufacturer_device_id": null, + "device_manufacturer": "schlage", + "model_name": "BE469", + "upc_id": "11", + "upc_code": "043156312214", + "hub_id": "302123", + "local_id": "1", + "radio_type": "zwave", + "lat_lng": [ + 12.345678, + -98.765432 + ], + "location": "" + }, + "errors": [], + "pagination": {}, + "subscription": { + "pubnub": { + "subscribe_key": "sub-c-f7bf7f7e-1234-11e3-a5e8-02ee2dd12345", + "channel": "2727c0b7576be60827dde4838a081ecfb6112345" + } + } +} From 0ec2c777b85c76a47fceb4878e30112aa1ff4989 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Thu, 31 Mar 2016 10:48:58 -0400 Subject: [PATCH 072/178] Fixed powerstrip state update --- src/pywink/devices/standard/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pywink/devices/standard/__init__.py b/src/pywink/devices/standard/__init__.py index a1ff8a4..c864582 100644 --- a/src/pywink/devices/standard/__init__.py +++ b/src/pywink/devices/standard/__init__.py @@ -134,10 +134,10 @@ def _update_state_from_response(self, response_json): :param response_json: the json obj returned from query :return: """ - if self.pubnub_key is not None: - power_strip = response_json - else: + if 'data' in response_json: power_strip = response_json.get('data') + else: + power_strip = response_json power_strip_reading = power_strip.get('last_reading') outlets = power_strip.get('outlets', power_strip) for outlet in outlets: From ecff2e4c1d1a3ef07b4776dc9f7be1ba8a4ed08e Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Fri, 8 Apr 2016 08:14:47 -0400 Subject: [PATCH 073/178] Added require_desired_state_fulfilled to overridden method --- src/pywink/devices/standard/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pywink/devices/standard/__init__.py b/src/pywink/devices/standard/__init__.py index c864582..67cf008 100644 --- a/src/pywink/devices/standard/__init__.py +++ b/src/pywink/devices/standard/__init__.py @@ -129,7 +129,7 @@ def update_state(self, require_desired_state_fulfilled=False): if not is_desired_state_reached(power_strip[0]): return - def _update_state_from_response(self, response_json): + def _update_state_from_response(self, response_json, require_desired_state_fulfilled=False): """ :param response_json: the json obj returned from query :return: From 6a545989cba95237074c92e789422e66fbccc0af Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Wed, 4 May 2016 13:35:18 -0400 Subject: [PATCH 074/178] Added pubnub tests --- .../api_responses/device_with_pubnub.json | 182 ------------------ src/pywink/test/devices/standard/init_test.py | 22 +++ 2 files changed, 22 insertions(+), 182 deletions(-) delete mode 100644 src/pywink/test/api_responses/device_with_pubnub.json diff --git a/src/pywink/test/api_responses/device_with_pubnub.json b/src/pywink/test/api_responses/device_with_pubnub.json deleted file mode 100644 index c500513..0000000 --- a/src/pywink/test/api_responses/device_with_pubnub.json +++ /dev/null @@ -1,182 +0,0 @@ -{ - "data": { - "uuid": "1ccb4e6f-12d4-46a2-b9b8-e98d6f012345", - "desired_state": { - "locked": true, - "beeper_enabled": false, - "vacation_mode_enabled": false, - "auto_lock_enabled": false, - "key_code_length": 4, - "alarm_mode": null, - "alarm_sensitivity": 0.6, - "alarm_enabled": false - }, - "last_reading": { - "connection": true, - "connection_updated_at": 1458214863.9920943, - "locked": true, - "locked_updated_at": 1458214863.9920943, - "battery": 0.89, - "battery_updated_at": 1458214863.9920943, - "alarm_activated": null, - "alarm_activated_updated_at": null, - "beeper_enabled": false, - "beeper_enabled_updated_at": 1458214863.9920943, - "vacation_mode_enabled": false, - "vacation_mode_enabled_updated_at": 1458214863.9920943, - "auto_lock_enabled": false, - "auto_lock_enabled_updated_at": 1458214863.9920943, - "key_code_length": 4, - "key_code_length_updated_at": 1458214863.9920943, - "alarm_mode": null, - "alarm_mode_updated_at": 1458214863.9920943, - "alarm_sensitivity": 0.6, - "alarm_sensitivity_updated_at": 1458214863.9920943, - "alarm_enabled": false, - "alarm_enabled_updated_at": 1458214863.9920943, - "last_error": null, - "last_error_updated_at": 1450137143.224417, - "desired_locked_updated_at": 1458213884.4730809, - "desired_beeper_enabled_updated_at": 1458213884.4730809, - "desired_vacation_mode_enabled_updated_at": 1458213884.4730809, - "desired_auto_lock_enabled_updated_at": 1458213884.4730809, - "desired_key_code_length_updated_at": 1458213884.4730809, - "desired_alarm_mode_updated_at": 1458213884.4730809, - "desired_alarm_sensitivity_updated_at": 1458213884.4730809, - "desired_alarm_enabled_updated_at": 1458213884.4730809, - "connection_changed_at": 1458062585.6312222, - "locked_changed_at": 1458214863.9920943, - "battery_changed_at": 1458207220.5308204, - "beeper_enabled_changed_at": 1450888993.4345925, - "vacation_mode_enabled_changed_at": 1449960081.8146806, - "auto_lock_enabled_changed_at": 1449960081.8146806, - "alarm_enabled_changed_at": 1449960081.8146806, - "key_code_length_changed_at": 1449960081.8146806, - "alarm_sensitivity_changed_at": 1449960088.2434776, - "desired_locked_changed_at": 1458213884.4730809, - "desired_beeper_enabled_changed_at": 1450888993.5529706, - "desired_vacation_mode_enabled_changed_at": 1449960446.6464362, - "desired_auto_lock_enabled_changed_at": 1449960446.6464362, - "desired_key_code_length_changed_at": 1449960446.6464362, - "desired_alarm_mode_changed_at": 1450889063.721673, - "desired_alarm_sensitivity_changed_at": 1449960446.6464362, - "desired_alarm_enabled_changed_at": 1449960446.6464362, - "alarm_mode_changed_at": 1452080631.3350449 - }, - "subscription": { - "pubnub": { - "subscribe_key": "sub-c-f7bf7f7e-1234-11e3-a5e8-02ee2dd12345", - "channel": "2727c0b7576be60827dde4838a081ecfb6112345" - } - }, - "lock_id": "61123", - "name": "Front Door", - "locale": "en_us", - "units": {}, - "created_at": 1449960079, - "hidden_at": null, - "capabilities": { - "fields": [ - { - "field": "connection", - "type": "boolean", - "mutability": "read-only" - }, - { - "field": "locked", - "type": "boolean", - "mutability": "read-write" - }, - { - "field": "battery", - "type": "percentage", - "mutability": "read-only" - }, - { - "field": "alarm_activated", - "type": "boolean", - "mutability": "read-only" - }, - { - "field": "beeper_enabled", - "type": "boolean", - "mutability": "read-write" - }, - { - "field": "vacation_mode_enabled", - "type": "boolean", - "mutability": "read-write" - }, - { - "field": "auto_lock_enabled", - "type": "boolean", - "mutability": "read-write" - }, - { - "field": "key_code_length", - "type": "integer", - "range": [ - 4, - 8 - ], - "mutability": "read-write" - }, - { - "field": "alarm_mode", - "type": "string", - "choices": [ - "alert", - "tamper", - "forced_entry", - null - ], - "mutability": "read-write" - }, - { - "field": "alarm_sensitivity", - "type": "percentage", - "choices": [ - 0.2, - 0.4, - 0.6, - 0.8, - 1 - ], - "mutability": "read-write" - }, - { - "field": "alarm_enabled", - "type": "boolean", - "mutability": "read-write" - } - ], - "home_security_device": true - }, - "user_ids": [ - "377857" - ], - "triggers": [], - "manufacturer_device_model": "schlage_zwave_lock", - "manufacturer_device_id": null, - "device_manufacturer": "schlage", - "model_name": "BE469", - "upc_id": "11", - "upc_code": "043156312214", - "hub_id": "302123", - "local_id": "1", - "radio_type": "zwave", - "lat_lng": [ - 12.345678, - -98.765432 - ], - "location": "" - }, - "errors": [], - "pagination": {}, - "subscription": { - "pubnub": { - "subscribe_key": "sub-c-f7bf7f7e-1234-11e3-a5e8-02ee2dd12345", - "channel": "2727c0b7576be60827dde4838a081ecfb6112345" - } - } -} diff --git a/src/pywink/test/devices/standard/init_test.py b/src/pywink/test/devices/standard/init_test.py index 5f4d58c..54c04bd 100644 --- a/src/pywink/test/devices/standard/init_test.py +++ b/src/pywink/test/devices/standard/init_test.py @@ -316,3 +316,25 @@ def test_should_call_get_state_endpoint_with_capability_removed_from_id(self): sensor.update_state() self.api_interface.get_device_state.assert_called_once_with(sensor, expected_id) + +class WinkPubnubTests(unittest.TestCase): + + def setUp(self): + super(WinkPubnubTests, self).setUp() + self.api_interface = mock.MagicMock() + + def test_pubnub_key_and_channel_should_not_be_none(self): + with open('{}/api_responses/device_with_pubnub.json'.format(os.path.dirname(__file__))) as lock_file: + response_dict = json.load(lock_file) + device = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.LOCK])[0] + + self.assertIsNotNone(device.pubnub_key) + self.assertIsNotNone(device.pubnub_channel) + + def test_pubnub_key_and_channel_should_be_none(self): + with open('{}/api_responses/lock.json'.format(os.path.dirname(__file__))) as lock_file: + response_dict = json.load(lock_file) + device = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.LOCK])[0] + + self.assertIsNone(device.pubnub_key) + self.assertIsNone(device.pubnub_channel) From bb5832b55be3bcb503fdf8f01674beb1c144edbe Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Wed, 4 May 2016 13:37:17 -0400 Subject: [PATCH 075/178] Added pubnub json --- .../api_responses/device_with_pubnub.json | 183 ++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 src/pywink/test/devices/standard/api_responses/device_with_pubnub.json diff --git a/src/pywink/test/devices/standard/api_responses/device_with_pubnub.json b/src/pywink/test/devices/standard/api_responses/device_with_pubnub.json new file mode 100644 index 0000000..84616c8 --- /dev/null +++ b/src/pywink/test/devices/standard/api_responses/device_with_pubnub.json @@ -0,0 +1,183 @@ +{ + "data":[ + { + "uuid":"1ccb4e6f-45d4-46a2-b9b8-e98d6f0ad967", + "desired_state":{ + "locked":true, + "beeper_enabled":false, + "vacation_mode_enabled":false, + "auto_lock_enabled":false, + "key_code_length":4, + "alarm_mode":null, + "alarm_sensitivity":0.6, + "alarm_enabled":false + }, + "last_reading":{ + "connection":true, + "connection_updated_at":1462370596.2303777, + "locked":true, + "locked_updated_at":1462370596.2303777, + "battery":0.77, + "battery_updated_at":1462370596.2303777, + "alarm_activated":null, + "alarm_activated_updated_at":null, + "beeper_enabled":false, + "beeper_enabled_updated_at":1462370596.2303777, + "vacation_mode_enabled":false, + "vacation_mode_enabled_updated_at":1462370596.2303777, + "auto_lock_enabled":false, + "auto_lock_enabled_updated_at":1462370596.2303777, + "key_code_length":4, + "key_code_length_updated_at":1462370596.2303777, + "alarm_mode":null, + "alarm_mode_updated_at":1462370596.2303777, + "alarm_sensitivity":0.6, + "alarm_sensitivity_updated_at":1462370596.2303777, + "alarm_enabled":false, + "alarm_enabled_updated_at":1462370596.2303777, + "last_error":null, + "last_error_updated_at":1450137143.224417, + "desired_locked_updated_at":1462361916.879291, + "desired_beeper_enabled_updated_at":1462361916.879291, + "desired_vacation_mode_enabled_updated_at":1462361916.879291, + "desired_auto_lock_enabled_updated_at":1462361916.879291, + "desired_key_code_length_updated_at":1462361916.879291, + "desired_alarm_mode_updated_at":1462361916.879291, + "desired_alarm_sensitivity_updated_at":1462361916.879291, + "desired_alarm_enabled_updated_at":1462361916.879291, + "connection_changed_at":1458334995.4152315, + "locked_changed_at":1462361711.8780804, + "battery_changed_at":1462370596.2303777, + "beeper_enabled_changed_at":1450888993.4345925, + "vacation_mode_enabled_changed_at":1449960081.8146806, + "auto_lock_enabled_changed_at":1449960081.8146806, + "alarm_enabled_changed_at":1449960081.8146806, + "key_code_length_changed_at":1449960081.8146806, + "alarm_sensitivity_changed_at":1449960088.2434776, + "desired_locked_changed_at":1462361916.879291, + "desired_beeper_enabled_changed_at":1450888993.5529706, + "desired_vacation_mode_enabled_changed_at":1449960446.6464362, + "desired_auto_lock_enabled_changed_at":1449960446.6464362, + "desired_key_code_length_changed_at":1449960446.6464362, + "desired_alarm_mode_changed_at":1450889063.721673, + "desired_alarm_sensitivity_changed_at":1449960446.6464362, + "desired_alarm_enabled_changed_at":1449960446.6464362, + "alarm_mode_changed_at":1452080631.3350449 + }, + "subscription":{ + "pubnub":{ + "subscribe_key":"sub-c-f7bf7f7e-0542-11e3-a123-02ee2ddab7fe", + "channel":"9afda0e9bb607d520b01b9d58a24400e6b381ab7|lock-61708|user-377857" + } + }, + "lock_id":"61708", + "name":"Front Door", + "locale":"en_us", + "units":{ + + }, + "created_at":1449960079, + "hidden_at":null, + "capabilities":{ + "fields":[ + { + "type":"boolean", + "field":"connection", + "mutability":"read-only" + }, + { + "type":"boolean", + "field":"locked", + "mutability":"read-write" + }, + { + "type":"percentage", + "field":"battery", + "mutability":"read-only" + }, + { + "type":"boolean", + "field":"alarm_activated", + "mutability":"read-only" + }, + { + "type":"boolean", + "field":"beeper_enabled", + "mutability":"read-write" + }, + { + "type":"boolean", + "field":"vacation_mode_enabled", + "mutability":"read-write" + }, + { + "type":"boolean", + "field":"auto_lock_enabled", + "mutability":"read-write" + }, + { + "type":"integer", + "field":"key_code_length", + "range":[ + 4, + 8 + ], + "mutability":"read-write" + }, + { + "type":"string", + "field":"alarm_mode", + "choices":[ + "alert", + "tamper", + "forced_entry", + null + ], + "mutability":"read-write" + }, + { + "type":"percentage", + "field":"alarm_sensitivity", + "choices":[ + 0.2, + 0.4, + 0.6, + 0.8, + 1 + ], + "mutability":"read-write" + }, + { + "type":"boolean", + "field":"alarm_enabled", + "mutability":"read-write" + } + ], + "home_security_device":true + }, + "triggers":[ + + ], + "manufacturer_device_model":"schlage_zwave_lock", + "manufacturer_device_id":null, + "device_manufacturer":"schlage", + "model_name":"BE469", + "upc_id":"11", + "upc_code":"043156312214", + "hub_id":"302528", + "local_id":"1", + "radio_type":"zwave", + "lat_lng":[ + 39.019975, + -84.441513 + ], + "location":"" + } + ], + "errors":[ + + ], + "pagination":{ + "count":1 + } +} From 90f9d1836e69708957867f41322a5de6c37516d9 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Mon, 9 May 2016 18:09:49 -0400 Subject: [PATCH 076/178] Get subscription key from first device --- src/pywink/__init__.py | 2 ++ src/pywink/api.py | 17 +++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/pywink/__init__.py b/src/pywink/__init__.py index ff56951..0e75a4b 100644 --- a/src/pywink/__init__.py +++ b/src/pywink/__init__.py @@ -5,3 +5,5 @@ from pywink.api import set_bearer_token, set_wink_credentials, get_bulbs, \ get_eggtrays, get_garage_doors, get_locks, get_powerstrip_outlets, \ get_sensors, get_shades, get_sirens, get_switches, get_devices, is_token_set + get_subscription_key + diff --git a/src/pywink/api.py b/src/pywink/api.py index 57f4cc1..ba32f48 100644 --- a/src/pywink/api.py +++ b/src/pywink/api.py @@ -106,6 +106,23 @@ def get_sirens(): return get_devices(device_types.SIREN) +def get_subscription_key(): + arequest_url = "{}/users/me/wink_devices".format(WinkApiInterface.BASE_URL) + response = requests.get(arequest_url, headers=API_HEADERS) + if response.status_code == 200: + response_dict = response.json() + first_device = response_dict.get('data')[0] + if "subscription" in first_device: + return first_device.get("subscription").get("pubnub").get("subscribe_key") + else: + return None + + if response.status_code == 401: + raise WinkAPIException("401 Response from Wink API. Maybe Bearer token is expired?") + else: + raise WinkAPIException("Unexpected") + + def get_devices(device_type): arequest_url = "{}/users/me/wink_devices".format(WinkApiInterface.BASE_URL) response = requests.get(arequest_url, headers=API_HEADERS) From 0fc8184ca29007fad7e475ca4477b1412c7202c9 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Fri, 20 May 2016 09:51:23 -0400 Subject: [PATCH 077/178] Sanitize pubnub json --- src/pywink/devices/sensors.py | 15 ++++++++------- src/pywink/devices/standard/__init__.py | 8 +------- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/src/pywink/devices/sensors.py b/src/pywink/devices/sensors.py index e94ce29..1eda2fe 100644 --- a/src/pywink/devices/sensors.py +++ b/src/pywink/devices/sensors.py @@ -93,13 +93,14 @@ def humidity_percentage(self): :return: The relative humidity detected by the sensor (0% to 100%) :rtype: int """ - # The subscription response returns a deciaml not an int - # Doubtful we will ever get a legitimate reading less than 1 - # so I think this should be safe - if self.last_reading() <= 1.0: - return int(self.last_reading() * 100) - else: - return self.last_reading() + return self.last_reading() + + def pubnub_update(self, json_response): + # Pubnub returns the humidity as a decminal + # converting to a percentage + hum = json_response["last_reading"]["humidity"] * 100 + json_response["last_reading"]["humidity"] = hum + self.json_state = json_response class WinkBrightnessSensor(_WinkCapabilitySensor): diff --git a/src/pywink/devices/standard/__init__.py b/src/pywink/devices/standard/__init__.py index 67cf008..de26742 100644 --- a/src/pywink/devices/standard/__init__.py +++ b/src/pywink/devices/standard/__init__.py @@ -134,10 +134,7 @@ def _update_state_from_response(self, response_json, require_desired_state_fulfi :param response_json: the json obj returned from query :return: """ - if 'data' in response_json: - power_strip = response_json.get('data') - else: - power_strip = response_json + power_strip = response_json power_strip_reading = power_strip.get('last_reading') outlets = power_strip.get('outlets', power_strip) for outlet in outlets: @@ -145,9 +142,6 @@ def _update_state_from_response(self, response_json, require_desired_state_fulfi outlet['last_reading']['connection'] = power_strip_reading.get('connection') self.json_state = outlet - def pubnub_update(self, json_response): - self._update_state_from_response(json_response) - def index(self): return self.json_state.get('outlet_index', None) From 454afa34daed3e9be67d8f71be0ae339c41ce9f9 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Fri, 20 May 2016 11:21:32 -0400 Subject: [PATCH 078/178] Added spotter humidity update check --- src/pywink/test/devices/standard/init_test.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/pywink/test/devices/standard/init_test.py b/src/pywink/test/devices/standard/init_test.py index 54c04bd..d486d9e 100644 --- a/src/pywink/test/devices/standard/init_test.py +++ b/src/pywink/test/devices/standard/init_test.py @@ -299,6 +299,21 @@ def test_battery_level_should_return_float(self): self.assertEqual(sensor.battery_level, 0.86) + def test_humidity_is_percentage_after_update(self): + with open('{}/api_responses/quirky_spotter.json'.format(os.path.dirname(__file__))) as spotter_file: + response_dict = json.load(spotter_file) + + sensors = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SENSOR_POD]) + """:type : list of WinkHumiditySensor""" + humidity_sensor = [sensor for sensor in sensors if sensor.capability() is WinkHumiditySensor.CAPABILITY][0] + + with open('{}/api_responses/quirky_spotter_pubnub.json'.format(os.path.dirname(__file__))) as spotter_file: + update_response_dict = json.load(spotter_file) + + humidity_sensor.pubnub_update(update_response_dict) + expected_humidity = 24 + self.assertEquals(expected_humidity, humidity_sensor.humidity_percentage()) + class WinkCapabilitySensorTests(unittest.TestCase): def setUp(self): From 0dbba39fc056fe721f03eb014d0783ffe1813583 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Fri, 20 May 2016 11:24:02 -0400 Subject: [PATCH 079/178] Added pubunub json for spotter --- .../api_responses/quirky_spotter_pubnub.json | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 src/pywink/test/devices/standard/api_responses/quirky_spotter_pubnub.json diff --git a/src/pywink/test/devices/standard/api_responses/quirky_spotter_pubnub.json b/src/pywink/test/devices/standard/api_responses/quirky_spotter_pubnub.json new file mode 100644 index 0000000..dc57dd4 --- /dev/null +++ b/src/pywink/test/devices/standard/api_responses/quirky_spotter_pubnub.json @@ -0,0 +1,134 @@ +{ + "capabilities":{ + "fields":[ + { + "field":"battery", + "mutability":"read-only", + "type":"percentage" + }, + { + "field":"brightness", + "mutability":"read-only", + "type":"percentage" + }, + { + "field":"external_power", + "mutability":"read-only", + "type":"boolean" + }, + { + "field":"humidity", + "mutability":"read-only", + "type":"percentage" + }, + { + "field":"loudness", + "mutability":"read-only", + "type":"boolean" + }, + { + "field":"temperature", + "mutability":"read-only", + "type":"float" + }, + { + "field":"vibration", + "mutability":"read-only", + "type":"boolean" + } + ], + "needs_wifi_network_list":true + }, + "created_at":1453520234, + "desired_state":{ + + }, + "device_manufacturer":"quirky_ge", + "gang_id":null, + "hidden_at":null, + "hub_id":null, + "icon_code":null, + "icon_id":null, + "last_event":{ + "brightness_occurred_at":1463746054.0962152, + "loudness_occurred_at":1463751976.0731974, + "vibration_occurred_at":1463699867.6401784 + }, + "last_reading":{ + "agent_session_id":null, + "agent_session_id_updated_at":null, + "battery":1.0, + "battery_changed_at":1456972918.8235242, + "battery_updated_at":1463756286.1238914, + "brightness":0.0, + "brightness_changed_at":1463751865.5881312, + "brightness_true":"N/A", + "brightness_true_changed_at":1463746054.0962152, + "brightness_true_updated_at":null, + "brightness_updated_at":1463756286.1238914, + "connection":true, + "connection_changed_at":1453520234.8375406, + "connection_updated_at":1463756286.1238914, + "external_power":true, + "external_power_changed_at":1453520237.0879393, + "external_power_updated_at":1463756286.1238914, + "humidity":0.24, + "humidity_changed_at":1463744925.739977, + "humidity_updated_at":1463756286.1238914, + "loudness":false, + "loudness_changed_at":1463752033.569461, + "loudness_true":"N/A", + "loudness_true_changed_at":1463751976.0731974, + "loudness_true_updated_at":null, + "loudness_updated_at":1463756286.1238914, + "temperature":23.0, + "temperature_changed_at":1463756286.1238914, + "temperature_updated_at":1463756286.1238914, + "vibration":false, + "vibration_changed_at":1463699872.8421204, + "vibration_true":"N/A", + "vibration_true_changed_at":1463699867.6401784, + "vibration_true_updated_at":null, + "vibration_updated_at":1463756286.1238914 + }, + "lat_lng":[ + 12.345678, + -98.76543 + ], + "linked_service_id":null, + "local_id":null, + "locale":"en_us", + "location":"", + "mac_address":"0c2a69023e3b", + "manufacturer_device_id":null, + "manufacturer_device_model":"quirky_ge_spotter", + "model_name":"Spotter", + "name":"Spotter", + "object_id":"156123", + "object_type":"sensor_pod", + "radio_type":null, + "sensor_pod_id":"156123", + "sensor_threshold_events":[ + + ], + "serial":"ABAB00003314", + "subscription":{ + "pubnub":{ + "channel":"e7c46d25d265425356hsdf898a0d05bfc1762405|sensor_pod-156012|user-377857", + "subscribe_key":"sub-c-f7bf7f7e-1234-11e3-a5e8-123456" + } + }, + "triggers":[ + + ], + "units":{ + + }, + "upc_code":"quirky_ge_spotter", + "upc_id":"531", + "user_ids":[ + "377812" + ], + "uuid":"77ee9632-d1c6-4af7-aec3-474091234511", + "nonce":"156012_1463756285_539" +} From e5872431084442d296121471c3024d3d8193b530 Mon Sep 17 00:00:00 2001 From: William Date: Thu, 26 May 2016 15:22:54 -0400 Subject: [PATCH 080/178] Power strip fixes and subscription tests --- src/pywink/api.py | 32 +++++++++---------- src/pywink/devices/standard/__init__.py | 11 +++++-- src/pywink/test/devices/standard/init_test.py | 6 ++++ 3 files changed, 31 insertions(+), 18 deletions(-) diff --git a/src/pywink/api.py b/src/pywink/api.py index ba32f48..2bddbab 100644 --- a/src/pywink/api.py +++ b/src/pywink/api.py @@ -107,29 +107,23 @@ def get_sirens(): def get_subscription_key(): - arequest_url = "{}/users/me/wink_devices".format(WinkApiInterface.BASE_URL) - response = requests.get(arequest_url, headers=API_HEADERS) - if response.status_code == 200: - response_dict = response.json() - first_device = response_dict.get('data')[0] - if "subscription" in first_device: - return first_device.get("subscription").get("pubnub").get("subscribe_key") - else: - return None + response_dict = wink_api_fetch() + first_device = response_dict.get('data')[0] + return get_subscription_key_from_response_dict(first_device) - if response.status_code == 401: - raise WinkAPIException("401 Response from Wink API. Maybe Bearer token is expired?") + +def get_subscription_key_from_response_dict(device): + if "subscription" in device: + return device.get("subscription").get("pubnub").get("subscribe_key") else: - raise WinkAPIException("Unexpected") + return None -def get_devices(device_type): +def wink_api_fetch(): arequest_url = "{}/users/me/wink_devices".format(WinkApiInterface.BASE_URL) response = requests.get(arequest_url, headers=API_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) + return response.json() if response.status_code == 401: raise WinkAPIException("401 Response from Wink API. Maybe Bearer token is expired?") @@ -137,6 +131,12 @@ def get_devices(device_type): raise WinkAPIException("Unexpected") +def get_devices(device_type): + response_dict = wink_api_fetch() + filter_key = DEVICE_ID_KEYS.get(device_type) + return get_devices_from_response_dict(response_dict, filter_key) + + def get_devices_from_response_dict(response_dict, filter_key): """ :rtype: list of WinkDevice diff --git a/src/pywink/devices/standard/__init__.py b/src/pywink/devices/standard/__init__.py index de26742..36c1fe4 100644 --- a/src/pywink/devices/standard/__init__.py +++ b/src/pywink/devices/standard/__init__.py @@ -126,8 +126,11 @@ def update_state(self, require_desired_state_fulfilled=False): response = self.api_interface.get_device_state(self, id_override=self.parent_id()) power_strip = response.get('data') if require_desired_state_fulfilled: - if not is_desired_state_reached(power_strip[0]): + if not is_desired_state_reached(self.index): return + else: + self._update_state_from_response(power_strip) + return True def _update_state_from_response(self, response_json, require_desired_state_fulfilled=False): """ @@ -142,6 +145,9 @@ def _update_state_from_response(self, response_json, require_desired_state_fulfi outlet['last_reading']['connection'] = power_strip_reading.get('connection') self.json_state = outlet + def pubnub_update(self, json_response): + self._update_state_from_response(json_response) + def index(self): return self.json_state.get('outlet_index', None) @@ -165,7 +171,8 @@ def set_state(self, state, **kwargs): values = {"outlets": [{}, {"desired_state": {"powered": state}}]} response = self.api_interface.set_device_state(self, values, id_override=self.parent_id()) - self._update_state_from_response(response) + power_strip = response.get('data') + self._update_state_from_response(power_strip) self._last_call = (time.time(), state) diff --git a/src/pywink/test/devices/standard/init_test.py b/src/pywink/test/devices/standard/init_test.py index d486d9e..3a9219e 100644 --- a/src/pywink/test/devices/standard/init_test.py +++ b/src/pywink/test/devices/standard/init_test.py @@ -353,3 +353,9 @@ def test_pubnub_key_and_channel_should_be_none(self): self.assertIsNone(device.pubnub_key) self.assertIsNone(device.pubnub_channel) + + def test_pywink_api_pubnub_subscription_key_is_not_none(self): + with open('{}/api_responses/device_with_pubnub.json'.format(os.path.dirname(__file__))) as lock_file: + response_dict = json.load(lock_file) + + self.assertIsNotNone(self.api_interface.get_subscription_key_from_response_dict(response_dict)) From 160687dd89d87c726879b6f4ef2df3f68b11f5ae Mon Sep 17 00:00:00 2001 From: William Date: Thu, 26 May 2016 18:26:15 -0400 Subject: [PATCH 081/178] Fix indent error --- src/pywink/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pywink/__init__.py b/src/pywink/__init__.py index 0e75a4b..45d994d 100644 --- a/src/pywink/__init__.py +++ b/src/pywink/__init__.py @@ -4,6 +4,6 @@ # noqa from pywink.api import set_bearer_token, set_wink_credentials, get_bulbs, \ get_eggtrays, get_garage_doors, get_locks, get_powerstrip_outlets, \ - get_sensors, get_shades, get_sirens, get_switches, get_devices, is_token_set - get_subscription_key + get_sensors, get_shades, get_sirens, get_switches, get_devices, \ + is_token_set, get_subscription_key From 1ecbdf56dde5d8b71ff6b2d131b7fd0c8fae50c7 Mon Sep 17 00:00:00 2001 From: William Date: Thu, 26 May 2016 19:12:04 -0400 Subject: [PATCH 082/178] Fixed typo and powerstrip update method --- src/pywink/devices/sensors.py | 2 +- src/pywink/devices/standard/__init__.py | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/pywink/devices/sensors.py b/src/pywink/devices/sensors.py index 1eda2fe..62ed3a4 100644 --- a/src/pywink/devices/sensors.py +++ b/src/pywink/devices/sensors.py @@ -96,7 +96,7 @@ def humidity_percentage(self): return self.last_reading() def pubnub_update(self, json_response): - # Pubnub returns the humidity as a decminal + # Pubnub returns the humidity as a decimal # converting to a percentage hum = json_response["last_reading"]["humidity"] * 100 json_response["last_reading"]["humidity"] = hum diff --git a/src/pywink/devices/standard/__init__.py b/src/pywink/devices/standard/__init__.py index 36c1fe4..5410aac 100644 --- a/src/pywink/devices/standard/__init__.py +++ b/src/pywink/devices/standard/__init__.py @@ -126,11 +126,15 @@ def update_state(self, require_desired_state_fulfilled=False): response = self.api_interface.get_device_state(self, id_override=self.parent_id()) power_strip = response.get('data') if require_desired_state_fulfilled: - if not is_desired_state_reached(self.index): + if not is_desired_state_reached(power_strip[self.index]): return - else: - self._update_state_from_response(power_strip) - return True + + 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_from_response(self, response_json, require_desired_state_fulfilled=False): """ From d6e3558952d63bac32fde99029d9cec19f18bb09 Mon Sep 17 00:00:00 2001 From: Brad Johnson Date: Thu, 26 May 2016 20:02:53 -0600 Subject: [PATCH 083/178] Update README.md --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index ee7ef3e..5baa38e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -Python Wink API ---------------- + Python Wink API [![Join the chat at https://gitter.im/python-wink/python-wink](https://badges.gitter.im/python-wink/python-wink.svg)](https://gitter.im/bradsk88/python-wink?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Build Status](https://travis-ci.org/python-wink/python-wink.svg?branch=master)](https://travis-ci.org/python-wink/python-wink) [![Coverage Status](https://coveralls.io/repos/github/python-wink/python-wink/badge.svg?branch=master)](https://coveralls.io/github/python-wink/python-wink?branch=master) From b8ab26e1940f6b826343ded6073f41fcf3f61483 Mon Sep 17 00:00:00 2001 From: bradsk88 <7kcwKg60tdcW> Date: Mon, 6 Jun 2016 20:14:42 -0600 Subject: [PATCH 084/178] Added unit tests for 3 new devices. GoControl Motion Sensor GoControl Door Sensor GE/Jasco Z Wave Light Switch Stopped duplicating door switches in `get_devices_from_response_dict` --- .gitignore | 1 + src/pywink/api.py | 4 +- .../api_responses/door_sensor_gocontrol.json | 87 ++++++++++++++++++ .../light_switch_ge_jasco_z_wave.json | 71 +++++++++++++++ .../motion_sensor_gocontrol.json | 89 +++++++++++++++++++ src/pywink/test/devices/standard/init_test.py | 28 +++++- 6 files changed, 276 insertions(+), 4 deletions(-) create mode 100644 src/pywink/test/devices/standard/api_responses/door_sensor_gocontrol.json create mode 100644 src/pywink/test/devices/standard/api_responses/light_switch_ge_jasco_z_wave.json create mode 100644 src/pywink/test/devices/standard/api_responses/motion_sensor_gocontrol.json diff --git a/.gitignore b/.gitignore index a051acf..00c91e7 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ /.cache /src/python_wink.egg-info /.coverage +/.idea/* diff --git a/src/pywink/api.py b/src/pywink/api.py index ec311c2..3204e4b 100644 --- a/src/pywink/api.py +++ b/src/pywink/api.py @@ -147,6 +147,8 @@ def get_devices_from_response_dict(response_dict, filter_key): subsensors = _get_subsensors_from_sensor_pod(item, api_interface) if subsensors: devices.extend(subsensors) + if len(subsensors) == 1: + continue devices.append(build_device(item, api_interface)) @@ -157,7 +159,7 @@ def _get_subsensors_from_sensor_pod(item, api_interface): capabilities = [cap['field'] for cap in item.get('capabilities', {}).get('fields', [])] if not capabilities: - return + return [] subsensors = [] diff --git a/src/pywink/test/devices/standard/api_responses/door_sensor_gocontrol.json b/src/pywink/test/devices/standard/api_responses/door_sensor_gocontrol.json new file mode 100644 index 0000000..b8a1bef --- /dev/null +++ b/src/pywink/test/devices/standard/api_responses/door_sensor_gocontrol.json @@ -0,0 +1,87 @@ +{ + "data": [ + { + "last_event": { + "brightness_occurred_at": null, + "loudness_occurred_at": null, + "vibration_occurred_at": null + }, + "object_type": "sensor_pod", + "object_id": "192444", + "uuid": "REMOVED", + "icon_id": null, + "icon_code": null, + "desired_state": {}, + "last_reading": { + "opened": true, + "opened_updated_at": 1465222176.887616, + "tamper_detected": null, + "tamper_detected_updated_at": null, + "battery": 1, + "battery_updated_at": 1465222176.887616, + "tamper_detected_true": null, + "tamper_detected_true_updated_at": null, + "connection": true, + "connection_updated_at": 1465222176.887616, + "agent_session_id": null, + "agent_session_id_updated_at": null, + "connection_changed_at": 1462044709.6055737, + "battery_changed_at": 1462048973.2091925, + "opened_changed_at": 1465222176.887616 + }, + "subscription": { + "pubnub": { + "subscribe_key": "REMOVED", + "channel": "REMOVED" + } + }, + "sensor_pod_id": "192444", + "name": "Brad Door", + "locale": "en_us", + "units": {}, + "created_at": 1462044709, + "hidden_at": null, + "capabilities": { + "fields": [ + { + "type": "boolean", + "field": "opened", + "mutability": "read-only" + }, + { + "type": "boolean", + "field": "tamper_detected", + "mutability": "read-only" + }, + { + "type": "percentage", + "field": "battery", + "mutability": "read-only" + } + ], + "home_security_device": true + }, + "triggers": [], + "manufacturer_device_model": "linear_wadwaz_1", + "manufacturer_device_id": null, + "device_manufacturer": "linear", + "model_name": "Z-Wave Door / Window Transmitter", + "upc_id": "189", + "upc_code": "9386312509", + "gang_id": null, + "hub_id": "300039", + "local_id": "10", + "radio_type": "zwave", + "linked_service_id": null, + "lat_lng": [ + 0, + 0 + ], + "location": "" + } + ], + "errors": [], + "pagination": { + "count": 13 + } +} diff --git a/src/pywink/test/devices/standard/api_responses/light_switch_ge_jasco_z_wave.json b/src/pywink/test/devices/standard/api_responses/light_switch_ge_jasco_z_wave.json new file mode 100644 index 0000000..1fd4921 --- /dev/null +++ b/src/pywink/test/devices/standard/api_responses/light_switch_ge_jasco_z_wave.json @@ -0,0 +1,71 @@ +{ + "data": [ + { + "object_type": "binary_switch", + "object_id": "216721", + "uuid": "REMOVED", + "icon_id": "52", + "icon_code": "binary_switch-light_bulb_dumb", + "desired_state": {}, + "last_reading": { + "connection": true, + "connection_updated_at": 1465261453.03461, + "powered": true, + "powered_updated_at": 1465261453.03461, + "desired_powered_updated_at": 1465107431.1711504, + "connection_changed_at": 1465259877.993167, + "powered_changed_at": 1465261453.03461, + "desired_powered_changed_at": 1465107431.1711504 + }, + "subscription": { + "pubnub": { + "subscribe_key": "REMOVED", + "channel": "REMOVED" + } + }, + "binary_switch_id": "216721", + "name": "Hallway Switch", + "locale": "en_us", + "units": {}, + "created_at": 1464309200, + "hidden_at": null, + "capabilities": { + "fields": [ + { + "type": "boolean", + "field": "connection", + "mutability": "read-only" + }, + { + "type": "boolean", + "field": "powered", + "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": "300039", + "local_id": "12", + "radio_type": "zwave", + "linked_service_id": null, + "current_budget": null, + "lat_lng": [ + 0, + 0 + ], + "location": "", + "order": 0 + } + ], + "errors": [], + "pagination": { + "count": 1 + } +} diff --git a/src/pywink/test/devices/standard/api_responses/motion_sensor_gocontrol.json b/src/pywink/test/devices/standard/api_responses/motion_sensor_gocontrol.json new file mode 100644 index 0000000..4e6d026 --- /dev/null +++ b/src/pywink/test/devices/standard/api_responses/motion_sensor_gocontrol.json @@ -0,0 +1,89 @@ +{ + "data": [ + { + "last_event": { + "brightness_occurred_at": null, + "loudness_occurred_at": null, + "vibration_occurred_at": null + }, + "object_type": "sensor_pod", + "object_id": "192431", + "uuid": "REMOVED", + "icon_id": null, + "icon_code": null, + "desired_state": {}, + "last_reading": { + "motion": true, + "motion_updated_at": 1465262763.1264362, + "battery": 1, + "battery_updated_at": 1465262763.1264362, + "tamper_detected": null, + "tamper_detected_updated_at": 1462063497.4478416, + "motion_true": "N/A", + "motion_true_updated_at": null, + "tamper_detected_true": null, + "tamper_detected_true_updated_at": null, + "connection": true, + "connection_updated_at": 1465262763.1264362, + "agent_session_id": null, + "agent_session_id_updated_at": null, + "connection_changed_at": 1462043954.4886937, + "battery_changed_at": 1462043956.216641, + "motion_changed_at": 1465262763.1264362, + "motion_true_changed_at": 1465262763.1264362 + }, + "subscription": { + "pubnub": { + "subscribe_key": "REMOVED", + "channel": "REMOVED" + } + }, + "sensor_pod_id": "192431", + "name": "Brad's Room Motion", + "locale": "en_us", + "units": {}, + "created_at": 1462043954, + "hidden_at": null, + "capabilities": { + "fields": [ + { + "type": "boolean", + "field": "motion", + "mutability": "read-only" + }, + { + "type": "percentage", + "field": "battery", + "mutability": "read-only" + }, + { + "type": "boolean", + "field": "tamper_detected", + "mutability": "read-only" + } + ] + }, + "triggers": [], + "manufacturer_device_model": "linear_wapirz_1", + "manufacturer_device_id": null, + "device_manufacturer": "linear", + "model_name": "Z-Wave Passive Infrared (PIR) Sensor", + "upc_id": "207", + "upc_code": "093863125102", + "gang_id": null, + "hub_id": "300039", + "local_id": "9", + "radio_type": "zwave", + "linked_service_id": null, + "lat_lng": [ + 0, + 0 + ], + "location": "" + } + ], + "errors": [], + "pagination": { + "count": 13 + } +} diff --git a/src/pywink/test/devices/standard/init_test.py b/src/pywink/test/devices/standard/init_test.py index 5f4d58c..8f0d99f 100644 --- a/src/pywink/test/devices/standard/init_test.py +++ b/src/pywink/test/devices/standard/init_test.py @@ -1,17 +1,17 @@ import json import mock import unittest -import sys import os -from pywink.api import get_devices_from_response_dict, WinkApiInterface +from pywink.api import get_devices_from_response_dict from pywink.devices import types as device_types from pywink.devices.sensors import WinkSensorPod, WinkBrightnessSensor, WinkHumiditySensor, \ WinkSoundPresenceSensor, WinkVibrationPresenceSensor, WinkTemperatureSensor, \ _WinkCapabilitySensor -from pywink.devices.standard import WinkBulb, WinkGarageDoor, WinkPowerStripOutlet, WinkSiren, WinkLock, \ +from pywink.devices.standard import WinkGarageDoor, WinkPowerStripOutlet, WinkSiren, WinkLock, \ WinkShade, WinkBinarySwitch, WinkEggTray from pywink.devices.types import DEVICE_ID_KEYS +from pywink.test.devices.standard.api_responses import ApiResponseJSONLoader class PowerStripTests(unittest.TestCase): @@ -155,6 +155,12 @@ def test_device_id_should_be_number(self): device_id = wink_switch.device_id() self.assertRegex(device_id, "^[0-9]{4,6}$") + def test_ge_switch_should_be_identified(self): + response = ApiResponseJSONLoader('light_switch_ge_jasco_z_wave.json').load() + devices = get_devices_from_response_dict(response, DEVICE_ID_KEYS[device_types.BINARY_SWITCH]) + self.assertEqual(1, len(devices)) + self.assertIsInstance(devices[0], WinkBinarySwitch) + class BinarySensorTests(unittest.TestCase): @@ -298,6 +304,22 @@ def test_battery_level_should_return_float(self): for sensor in sensors: self.assertEqual(sensor.battery_level, 0.86) + def test_gocontrol_door_sensor_should_be_identified(self): + response = ApiResponseJSONLoader('door_sensor_gocontrol.json').load() + devices = get_devices_from_response_dict(response, + DEVICE_ID_KEYS[ + device_types.SENSOR_POD]) + self.assertEqual(1, len(devices)) + self.assertIsInstance(devices[0], WinkSensorPod) + + def test_gocontrol_motion_sensor_should_be_identified(self): + response = ApiResponseJSONLoader('motion_sensor_gocontrol.json').load() + devices = get_devices_from_response_dict(response, + DEVICE_ID_KEYS[ + device_types.SENSOR_POD]) + self.assertEqual(1, len(devices)) + self.assertIsInstance(devices[0], WinkSensorPod) + class WinkCapabilitySensorTests(unittest.TestCase): From 79be38421e8b32ae09527c3b614150072df74a6c Mon Sep 17 00:00:00 2001 From: bradsk88 <7kcwKg60tdcW> Date: Mon, 6 Jun 2016 20:22:07 -0600 Subject: [PATCH 085/178] Bumping version to 0.7.7 --- CHANGELOG.md | 4 ++++ src/setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index be410c2..3dc80b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Change Log +## 0.7.7 +- Stopped duplicating door switches in `get_devices_from_response_dict` +- Add support for Wink Shades + ## 0.7.6 - Added ability to return the battery level if a device is battery powered diff --git a/src/setup.py b/src/setup.py index 1e2b800..0aa97fc 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='0.7.6', + version='0.7.7', description='Access Wink devices via the Wink API', url='http://github.com/bradsk88/python-wink', author='Brad Johnson', From 301b5e7c51232b1961bf285989c1ab541bdcce5b Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Tue, 29 Mar 2016 19:33:08 -0400 Subject: [PATCH 086/178] Retrieve Pubnub details from json if it exists --- CHANGELOG.md | 3 +++ src/pywink/api.py | 4 ++++ src/pywink/devices/base.py | 11 +++++++++++ src/pywink/devices/sensors.py | 8 +++++++- src/pywink/devices/standard/__init__.py | 12 ++++++++++++ 5 files changed, 37 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dc80b8..5211cb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 0.7.8 +- Added support for retrieving the Pubnub subscription details + ## 0.7.7 - Stopped duplicating door switches in `get_devices_from_response_dict` - Add support for Wink Shades diff --git a/src/pywink/api.py b/src/pywink/api.py index 3204e4b..6dd9f75 100644 --- a/src/pywink/api.py +++ b/src/pywink/api.py @@ -186,6 +186,10 @@ def _get_subsensors_from_sensor_pod(item, api_interface): def __get_outlets_from_powerstrip(item, api_interface): outlets = item['outlets'] + for outlet in outlets: + if 'subscription' in item: + outlet['subscription'] = item['subscription'] + outlet['last_reading']['connection'] = item['last_reading']['connection'] return [build_device(outlet, api_interface) for outlet in outlets if __device_is_visible(outlet, 'outlet_id')] diff --git a/src/pywink/devices/base.py b/src/pywink/devices/base.py index fda720b..0704d57 100644 --- a/src/pywink/devices/base.py +++ b/src/pywink/devices/base.py @@ -11,6 +11,14 @@ def __init__(self, device_state_as_json, api_interface, objectprefix=None): self.api_interface = api_interface self.objectprefix = objectprefix self.json_state = device_state_as_json + subscription = self.json_state.get('subscription') + if subscription != {} and subscription is not None: + pubnub = subscription.get('pubnub') + self.pubnub_key = pubnub.get('subscribe_key') + self.pubnub_channel = pubnub.get('channel') + else: + self.pubnub_key = None + self.pubnub_channel = None def __str__(self): return "%s %s %s" % (self.name(), self.device_id(), self.state()) @@ -57,3 +65,6 @@ def update_state(self, require_desired_state_fulfilled=False): """ Update state with latest info from Wink API. """ response = self.api_interface.get_device_state(self) return self._update_state_from_response(response, require_desired_state_fulfilled) + + def pubnub_update(self, json_response): + self.json_state = json_response diff --git a/src/pywink/devices/sensors.py b/src/pywink/devices/sensors.py index c42203a..e94ce29 100644 --- a/src/pywink/devices/sensors.py +++ b/src/pywink/devices/sensors.py @@ -93,7 +93,13 @@ def humidity_percentage(self): :return: The relative humidity detected by the sensor (0% to 100%) :rtype: int """ - return self.last_reading() + # The subscription response returns a deciaml not an int + # Doubtful we will ever get a legitimate reading less than 1 + # so I think this should be safe + if self.last_reading() <= 1.0: + return int(self.last_reading() * 100) + else: + return self.last_reading() class WinkBrightnessSensor(_WinkCapabilitySensor): diff --git a/src/pywink/devices/standard/__init__.py b/src/pywink/devices/standard/__init__.py index db63198..a1ff8a4 100644 --- a/src/pywink/devices/standard/__init__.py +++ b/src/pywink/devices/standard/__init__.py @@ -129,6 +129,15 @@ def update_state(self, require_desired_state_fulfilled=False): if not is_desired_state_reached(power_strip[0]): return + def _update_state_from_response(self, response_json): + """ + :param response_json: the json obj returned from query + :return: + """ + if self.pubnub_key is not None: + power_strip = response_json + else: + 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: @@ -136,6 +145,9 @@ def update_state(self, require_desired_state_fulfilled=False): outlet['last_reading']['connection'] = power_strip_reading.get('connection') self.json_state = outlet + def pubnub_update(self, json_response): + self._update_state_from_response(json_response) + def index(self): return self.json_state.get('outlet_index', None) From 28a8b5bf3fd033f31cc50c0ff013e5c56283643b Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Tue, 29 Mar 2016 19:35:57 -0400 Subject: [PATCH 087/178] Added json example with pubnub details --- .../api_responses/device_with_pubnub.json | 182 ++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 src/pywink/test/api_responses/device_with_pubnub.json diff --git a/src/pywink/test/api_responses/device_with_pubnub.json b/src/pywink/test/api_responses/device_with_pubnub.json new file mode 100644 index 0000000..c500513 --- /dev/null +++ b/src/pywink/test/api_responses/device_with_pubnub.json @@ -0,0 +1,182 @@ +{ + "data": { + "uuid": "1ccb4e6f-12d4-46a2-b9b8-e98d6f012345", + "desired_state": { + "locked": true, + "beeper_enabled": false, + "vacation_mode_enabled": false, + "auto_lock_enabled": false, + "key_code_length": 4, + "alarm_mode": null, + "alarm_sensitivity": 0.6, + "alarm_enabled": false + }, + "last_reading": { + "connection": true, + "connection_updated_at": 1458214863.9920943, + "locked": true, + "locked_updated_at": 1458214863.9920943, + "battery": 0.89, + "battery_updated_at": 1458214863.9920943, + "alarm_activated": null, + "alarm_activated_updated_at": null, + "beeper_enabled": false, + "beeper_enabled_updated_at": 1458214863.9920943, + "vacation_mode_enabled": false, + "vacation_mode_enabled_updated_at": 1458214863.9920943, + "auto_lock_enabled": false, + "auto_lock_enabled_updated_at": 1458214863.9920943, + "key_code_length": 4, + "key_code_length_updated_at": 1458214863.9920943, + "alarm_mode": null, + "alarm_mode_updated_at": 1458214863.9920943, + "alarm_sensitivity": 0.6, + "alarm_sensitivity_updated_at": 1458214863.9920943, + "alarm_enabled": false, + "alarm_enabled_updated_at": 1458214863.9920943, + "last_error": null, + "last_error_updated_at": 1450137143.224417, + "desired_locked_updated_at": 1458213884.4730809, + "desired_beeper_enabled_updated_at": 1458213884.4730809, + "desired_vacation_mode_enabled_updated_at": 1458213884.4730809, + "desired_auto_lock_enabled_updated_at": 1458213884.4730809, + "desired_key_code_length_updated_at": 1458213884.4730809, + "desired_alarm_mode_updated_at": 1458213884.4730809, + "desired_alarm_sensitivity_updated_at": 1458213884.4730809, + "desired_alarm_enabled_updated_at": 1458213884.4730809, + "connection_changed_at": 1458062585.6312222, + "locked_changed_at": 1458214863.9920943, + "battery_changed_at": 1458207220.5308204, + "beeper_enabled_changed_at": 1450888993.4345925, + "vacation_mode_enabled_changed_at": 1449960081.8146806, + "auto_lock_enabled_changed_at": 1449960081.8146806, + "alarm_enabled_changed_at": 1449960081.8146806, + "key_code_length_changed_at": 1449960081.8146806, + "alarm_sensitivity_changed_at": 1449960088.2434776, + "desired_locked_changed_at": 1458213884.4730809, + "desired_beeper_enabled_changed_at": 1450888993.5529706, + "desired_vacation_mode_enabled_changed_at": 1449960446.6464362, + "desired_auto_lock_enabled_changed_at": 1449960446.6464362, + "desired_key_code_length_changed_at": 1449960446.6464362, + "desired_alarm_mode_changed_at": 1450889063.721673, + "desired_alarm_sensitivity_changed_at": 1449960446.6464362, + "desired_alarm_enabled_changed_at": 1449960446.6464362, + "alarm_mode_changed_at": 1452080631.3350449 + }, + "subscription": { + "pubnub": { + "subscribe_key": "sub-c-f7bf7f7e-1234-11e3-a5e8-02ee2dd12345", + "channel": "2727c0b7576be60827dde4838a081ecfb6112345" + } + }, + "lock_id": "61123", + "name": "Front Door", + "locale": "en_us", + "units": {}, + "created_at": 1449960079, + "hidden_at": null, + "capabilities": { + "fields": [ + { + "field": "connection", + "type": "boolean", + "mutability": "read-only" + }, + { + "field": "locked", + "type": "boolean", + "mutability": "read-write" + }, + { + "field": "battery", + "type": "percentage", + "mutability": "read-only" + }, + { + "field": "alarm_activated", + "type": "boolean", + "mutability": "read-only" + }, + { + "field": "beeper_enabled", + "type": "boolean", + "mutability": "read-write" + }, + { + "field": "vacation_mode_enabled", + "type": "boolean", + "mutability": "read-write" + }, + { + "field": "auto_lock_enabled", + "type": "boolean", + "mutability": "read-write" + }, + { + "field": "key_code_length", + "type": "integer", + "range": [ + 4, + 8 + ], + "mutability": "read-write" + }, + { + "field": "alarm_mode", + "type": "string", + "choices": [ + "alert", + "tamper", + "forced_entry", + null + ], + "mutability": "read-write" + }, + { + "field": "alarm_sensitivity", + "type": "percentage", + "choices": [ + 0.2, + 0.4, + 0.6, + 0.8, + 1 + ], + "mutability": "read-write" + }, + { + "field": "alarm_enabled", + "type": "boolean", + "mutability": "read-write" + } + ], + "home_security_device": true + }, + "user_ids": [ + "377857" + ], + "triggers": [], + "manufacturer_device_model": "schlage_zwave_lock", + "manufacturer_device_id": null, + "device_manufacturer": "schlage", + "model_name": "BE469", + "upc_id": "11", + "upc_code": "043156312214", + "hub_id": "302123", + "local_id": "1", + "radio_type": "zwave", + "lat_lng": [ + 12.345678, + -98.765432 + ], + "location": "" + }, + "errors": [], + "pagination": {}, + "subscription": { + "pubnub": { + "subscribe_key": "sub-c-f7bf7f7e-1234-11e3-a5e8-02ee2dd12345", + "channel": "2727c0b7576be60827dde4838a081ecfb6112345" + } + } +} From 75dce6ce2a963e9a79424d48cb9dd7d8eb829b9c Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Thu, 31 Mar 2016 10:48:58 -0400 Subject: [PATCH 088/178] Fixed powerstrip state update --- src/pywink/devices/standard/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pywink/devices/standard/__init__.py b/src/pywink/devices/standard/__init__.py index a1ff8a4..c864582 100644 --- a/src/pywink/devices/standard/__init__.py +++ b/src/pywink/devices/standard/__init__.py @@ -134,10 +134,10 @@ def _update_state_from_response(self, response_json): :param response_json: the json obj returned from query :return: """ - if self.pubnub_key is not None: - power_strip = response_json - else: + if 'data' in response_json: power_strip = response_json.get('data') + else: + power_strip = response_json power_strip_reading = power_strip.get('last_reading') outlets = power_strip.get('outlets', power_strip) for outlet in outlets: From 1f9ffd273e45e4afb2a5c4c023e050d8dc369aba Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Fri, 8 Apr 2016 08:14:47 -0400 Subject: [PATCH 089/178] Added require_desired_state_fulfilled to overridden method --- src/pywink/devices/standard/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pywink/devices/standard/__init__.py b/src/pywink/devices/standard/__init__.py index c864582..67cf008 100644 --- a/src/pywink/devices/standard/__init__.py +++ b/src/pywink/devices/standard/__init__.py @@ -129,7 +129,7 @@ def update_state(self, require_desired_state_fulfilled=False): if not is_desired_state_reached(power_strip[0]): return - def _update_state_from_response(self, response_json): + def _update_state_from_response(self, response_json, require_desired_state_fulfilled=False): """ :param response_json: the json obj returned from query :return: From dac5973b8bbaa4d396e2a310846a5054654bd84d Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Wed, 4 May 2016 13:35:18 -0400 Subject: [PATCH 090/178] Added pubnub tests --- .../api_responses/device_with_pubnub.json | 182 ------------------ src/pywink/test/devices/standard/init_test.py | 22 +++ 2 files changed, 22 insertions(+), 182 deletions(-) delete mode 100644 src/pywink/test/api_responses/device_with_pubnub.json diff --git a/src/pywink/test/api_responses/device_with_pubnub.json b/src/pywink/test/api_responses/device_with_pubnub.json deleted file mode 100644 index c500513..0000000 --- a/src/pywink/test/api_responses/device_with_pubnub.json +++ /dev/null @@ -1,182 +0,0 @@ -{ - "data": { - "uuid": "1ccb4e6f-12d4-46a2-b9b8-e98d6f012345", - "desired_state": { - "locked": true, - "beeper_enabled": false, - "vacation_mode_enabled": false, - "auto_lock_enabled": false, - "key_code_length": 4, - "alarm_mode": null, - "alarm_sensitivity": 0.6, - "alarm_enabled": false - }, - "last_reading": { - "connection": true, - "connection_updated_at": 1458214863.9920943, - "locked": true, - "locked_updated_at": 1458214863.9920943, - "battery": 0.89, - "battery_updated_at": 1458214863.9920943, - "alarm_activated": null, - "alarm_activated_updated_at": null, - "beeper_enabled": false, - "beeper_enabled_updated_at": 1458214863.9920943, - "vacation_mode_enabled": false, - "vacation_mode_enabled_updated_at": 1458214863.9920943, - "auto_lock_enabled": false, - "auto_lock_enabled_updated_at": 1458214863.9920943, - "key_code_length": 4, - "key_code_length_updated_at": 1458214863.9920943, - "alarm_mode": null, - "alarm_mode_updated_at": 1458214863.9920943, - "alarm_sensitivity": 0.6, - "alarm_sensitivity_updated_at": 1458214863.9920943, - "alarm_enabled": false, - "alarm_enabled_updated_at": 1458214863.9920943, - "last_error": null, - "last_error_updated_at": 1450137143.224417, - "desired_locked_updated_at": 1458213884.4730809, - "desired_beeper_enabled_updated_at": 1458213884.4730809, - "desired_vacation_mode_enabled_updated_at": 1458213884.4730809, - "desired_auto_lock_enabled_updated_at": 1458213884.4730809, - "desired_key_code_length_updated_at": 1458213884.4730809, - "desired_alarm_mode_updated_at": 1458213884.4730809, - "desired_alarm_sensitivity_updated_at": 1458213884.4730809, - "desired_alarm_enabled_updated_at": 1458213884.4730809, - "connection_changed_at": 1458062585.6312222, - "locked_changed_at": 1458214863.9920943, - "battery_changed_at": 1458207220.5308204, - "beeper_enabled_changed_at": 1450888993.4345925, - "vacation_mode_enabled_changed_at": 1449960081.8146806, - "auto_lock_enabled_changed_at": 1449960081.8146806, - "alarm_enabled_changed_at": 1449960081.8146806, - "key_code_length_changed_at": 1449960081.8146806, - "alarm_sensitivity_changed_at": 1449960088.2434776, - "desired_locked_changed_at": 1458213884.4730809, - "desired_beeper_enabled_changed_at": 1450888993.5529706, - "desired_vacation_mode_enabled_changed_at": 1449960446.6464362, - "desired_auto_lock_enabled_changed_at": 1449960446.6464362, - "desired_key_code_length_changed_at": 1449960446.6464362, - "desired_alarm_mode_changed_at": 1450889063.721673, - "desired_alarm_sensitivity_changed_at": 1449960446.6464362, - "desired_alarm_enabled_changed_at": 1449960446.6464362, - "alarm_mode_changed_at": 1452080631.3350449 - }, - "subscription": { - "pubnub": { - "subscribe_key": "sub-c-f7bf7f7e-1234-11e3-a5e8-02ee2dd12345", - "channel": "2727c0b7576be60827dde4838a081ecfb6112345" - } - }, - "lock_id": "61123", - "name": "Front Door", - "locale": "en_us", - "units": {}, - "created_at": 1449960079, - "hidden_at": null, - "capabilities": { - "fields": [ - { - "field": "connection", - "type": "boolean", - "mutability": "read-only" - }, - { - "field": "locked", - "type": "boolean", - "mutability": "read-write" - }, - { - "field": "battery", - "type": "percentage", - "mutability": "read-only" - }, - { - "field": "alarm_activated", - "type": "boolean", - "mutability": "read-only" - }, - { - "field": "beeper_enabled", - "type": "boolean", - "mutability": "read-write" - }, - { - "field": "vacation_mode_enabled", - "type": "boolean", - "mutability": "read-write" - }, - { - "field": "auto_lock_enabled", - "type": "boolean", - "mutability": "read-write" - }, - { - "field": "key_code_length", - "type": "integer", - "range": [ - 4, - 8 - ], - "mutability": "read-write" - }, - { - "field": "alarm_mode", - "type": "string", - "choices": [ - "alert", - "tamper", - "forced_entry", - null - ], - "mutability": "read-write" - }, - { - "field": "alarm_sensitivity", - "type": "percentage", - "choices": [ - 0.2, - 0.4, - 0.6, - 0.8, - 1 - ], - "mutability": "read-write" - }, - { - "field": "alarm_enabled", - "type": "boolean", - "mutability": "read-write" - } - ], - "home_security_device": true - }, - "user_ids": [ - "377857" - ], - "triggers": [], - "manufacturer_device_model": "schlage_zwave_lock", - "manufacturer_device_id": null, - "device_manufacturer": "schlage", - "model_name": "BE469", - "upc_id": "11", - "upc_code": "043156312214", - "hub_id": "302123", - "local_id": "1", - "radio_type": "zwave", - "lat_lng": [ - 12.345678, - -98.765432 - ], - "location": "" - }, - "errors": [], - "pagination": {}, - "subscription": { - "pubnub": { - "subscribe_key": "sub-c-f7bf7f7e-1234-11e3-a5e8-02ee2dd12345", - "channel": "2727c0b7576be60827dde4838a081ecfb6112345" - } - } -} diff --git a/src/pywink/test/devices/standard/init_test.py b/src/pywink/test/devices/standard/init_test.py index 8f0d99f..c224ebb 100644 --- a/src/pywink/test/devices/standard/init_test.py +++ b/src/pywink/test/devices/standard/init_test.py @@ -338,3 +338,25 @@ def test_should_call_get_state_endpoint_with_capability_removed_from_id(self): sensor.update_state() self.api_interface.get_device_state.assert_called_once_with(sensor, expected_id) + +class WinkPubnubTests(unittest.TestCase): + + def setUp(self): + super(WinkPubnubTests, self).setUp() + self.api_interface = mock.MagicMock() + + def test_pubnub_key_and_channel_should_not_be_none(self): + with open('{}/api_responses/device_with_pubnub.json'.format(os.path.dirname(__file__))) as lock_file: + response_dict = json.load(lock_file) + device = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.LOCK])[0] + + self.assertIsNotNone(device.pubnub_key) + self.assertIsNotNone(device.pubnub_channel) + + def test_pubnub_key_and_channel_should_be_none(self): + with open('{}/api_responses/lock.json'.format(os.path.dirname(__file__))) as lock_file: + response_dict = json.load(lock_file) + device = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.LOCK])[0] + + self.assertIsNone(device.pubnub_key) + self.assertIsNone(device.pubnub_channel) From 266f5efd4b8a3b50df4d64d3d35e6c775e886249 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Wed, 4 May 2016 13:37:17 -0400 Subject: [PATCH 091/178] Added pubnub json --- .../api_responses/device_with_pubnub.json | 183 ++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 src/pywink/test/devices/standard/api_responses/device_with_pubnub.json diff --git a/src/pywink/test/devices/standard/api_responses/device_with_pubnub.json b/src/pywink/test/devices/standard/api_responses/device_with_pubnub.json new file mode 100644 index 0000000..84616c8 --- /dev/null +++ b/src/pywink/test/devices/standard/api_responses/device_with_pubnub.json @@ -0,0 +1,183 @@ +{ + "data":[ + { + "uuid":"1ccb4e6f-45d4-46a2-b9b8-e98d6f0ad967", + "desired_state":{ + "locked":true, + "beeper_enabled":false, + "vacation_mode_enabled":false, + "auto_lock_enabled":false, + "key_code_length":4, + "alarm_mode":null, + "alarm_sensitivity":0.6, + "alarm_enabled":false + }, + "last_reading":{ + "connection":true, + "connection_updated_at":1462370596.2303777, + "locked":true, + "locked_updated_at":1462370596.2303777, + "battery":0.77, + "battery_updated_at":1462370596.2303777, + "alarm_activated":null, + "alarm_activated_updated_at":null, + "beeper_enabled":false, + "beeper_enabled_updated_at":1462370596.2303777, + "vacation_mode_enabled":false, + "vacation_mode_enabled_updated_at":1462370596.2303777, + "auto_lock_enabled":false, + "auto_lock_enabled_updated_at":1462370596.2303777, + "key_code_length":4, + "key_code_length_updated_at":1462370596.2303777, + "alarm_mode":null, + "alarm_mode_updated_at":1462370596.2303777, + "alarm_sensitivity":0.6, + "alarm_sensitivity_updated_at":1462370596.2303777, + "alarm_enabled":false, + "alarm_enabled_updated_at":1462370596.2303777, + "last_error":null, + "last_error_updated_at":1450137143.224417, + "desired_locked_updated_at":1462361916.879291, + "desired_beeper_enabled_updated_at":1462361916.879291, + "desired_vacation_mode_enabled_updated_at":1462361916.879291, + "desired_auto_lock_enabled_updated_at":1462361916.879291, + "desired_key_code_length_updated_at":1462361916.879291, + "desired_alarm_mode_updated_at":1462361916.879291, + "desired_alarm_sensitivity_updated_at":1462361916.879291, + "desired_alarm_enabled_updated_at":1462361916.879291, + "connection_changed_at":1458334995.4152315, + "locked_changed_at":1462361711.8780804, + "battery_changed_at":1462370596.2303777, + "beeper_enabled_changed_at":1450888993.4345925, + "vacation_mode_enabled_changed_at":1449960081.8146806, + "auto_lock_enabled_changed_at":1449960081.8146806, + "alarm_enabled_changed_at":1449960081.8146806, + "key_code_length_changed_at":1449960081.8146806, + "alarm_sensitivity_changed_at":1449960088.2434776, + "desired_locked_changed_at":1462361916.879291, + "desired_beeper_enabled_changed_at":1450888993.5529706, + "desired_vacation_mode_enabled_changed_at":1449960446.6464362, + "desired_auto_lock_enabled_changed_at":1449960446.6464362, + "desired_key_code_length_changed_at":1449960446.6464362, + "desired_alarm_mode_changed_at":1450889063.721673, + "desired_alarm_sensitivity_changed_at":1449960446.6464362, + "desired_alarm_enabled_changed_at":1449960446.6464362, + "alarm_mode_changed_at":1452080631.3350449 + }, + "subscription":{ + "pubnub":{ + "subscribe_key":"sub-c-f7bf7f7e-0542-11e3-a123-02ee2ddab7fe", + "channel":"9afda0e9bb607d520b01b9d58a24400e6b381ab7|lock-61708|user-377857" + } + }, + "lock_id":"61708", + "name":"Front Door", + "locale":"en_us", + "units":{ + + }, + "created_at":1449960079, + "hidden_at":null, + "capabilities":{ + "fields":[ + { + "type":"boolean", + "field":"connection", + "mutability":"read-only" + }, + { + "type":"boolean", + "field":"locked", + "mutability":"read-write" + }, + { + "type":"percentage", + "field":"battery", + "mutability":"read-only" + }, + { + "type":"boolean", + "field":"alarm_activated", + "mutability":"read-only" + }, + { + "type":"boolean", + "field":"beeper_enabled", + "mutability":"read-write" + }, + { + "type":"boolean", + "field":"vacation_mode_enabled", + "mutability":"read-write" + }, + { + "type":"boolean", + "field":"auto_lock_enabled", + "mutability":"read-write" + }, + { + "type":"integer", + "field":"key_code_length", + "range":[ + 4, + 8 + ], + "mutability":"read-write" + }, + { + "type":"string", + "field":"alarm_mode", + "choices":[ + "alert", + "tamper", + "forced_entry", + null + ], + "mutability":"read-write" + }, + { + "type":"percentage", + "field":"alarm_sensitivity", + "choices":[ + 0.2, + 0.4, + 0.6, + 0.8, + 1 + ], + "mutability":"read-write" + }, + { + "type":"boolean", + "field":"alarm_enabled", + "mutability":"read-write" + } + ], + "home_security_device":true + }, + "triggers":[ + + ], + "manufacturer_device_model":"schlage_zwave_lock", + "manufacturer_device_id":null, + "device_manufacturer":"schlage", + "model_name":"BE469", + "upc_id":"11", + "upc_code":"043156312214", + "hub_id":"302528", + "local_id":"1", + "radio_type":"zwave", + "lat_lng":[ + 39.019975, + -84.441513 + ], + "location":"" + } + ], + "errors":[ + + ], + "pagination":{ + "count":1 + } +} From 46d5551dce031dbcc34e04b3e6a8840a5c7ef157 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Mon, 9 May 2016 18:09:49 -0400 Subject: [PATCH 092/178] Get subscription key from first device --- src/pywink/__init__.py | 2 ++ src/pywink/api.py | 17 +++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/pywink/__init__.py b/src/pywink/__init__.py index ff56951..0e75a4b 100644 --- a/src/pywink/__init__.py +++ b/src/pywink/__init__.py @@ -5,3 +5,5 @@ from pywink.api import set_bearer_token, set_wink_credentials, get_bulbs, \ get_eggtrays, get_garage_doors, get_locks, get_powerstrip_outlets, \ get_sensors, get_shades, get_sirens, get_switches, get_devices, is_token_set + get_subscription_key + diff --git a/src/pywink/api.py b/src/pywink/api.py index 6dd9f75..4431df8 100644 --- a/src/pywink/api.py +++ b/src/pywink/api.py @@ -106,6 +106,23 @@ def get_sirens(): return get_devices(device_types.SIREN) +def get_subscription_key(): + arequest_url = "{}/users/me/wink_devices".format(WinkApiInterface.BASE_URL) + response = requests.get(arequest_url, headers=API_HEADERS) + if response.status_code == 200: + response_dict = response.json() + first_device = response_dict.get('data')[0] + if "subscription" in first_device: + return first_device.get("subscription").get("pubnub").get("subscribe_key") + else: + return None + + if response.status_code == 401: + raise WinkAPIException("401 Response from Wink API. Maybe Bearer token is expired?") + else: + raise WinkAPIException("Unexpected") + + def get_devices(device_type): arequest_url = "{}/users/me/wink_devices".format(WinkApiInterface.BASE_URL) response = requests.get(arequest_url, headers=API_HEADERS) From b4579956c02189720380c8f47175eef29a90e43e Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Fri, 20 May 2016 09:51:23 -0400 Subject: [PATCH 093/178] Sanitize pubnub json --- src/pywink/devices/sensors.py | 15 ++++++++------- src/pywink/devices/standard/__init__.py | 8 +------- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/src/pywink/devices/sensors.py b/src/pywink/devices/sensors.py index e94ce29..1eda2fe 100644 --- a/src/pywink/devices/sensors.py +++ b/src/pywink/devices/sensors.py @@ -93,13 +93,14 @@ def humidity_percentage(self): :return: The relative humidity detected by the sensor (0% to 100%) :rtype: int """ - # The subscription response returns a deciaml not an int - # Doubtful we will ever get a legitimate reading less than 1 - # so I think this should be safe - if self.last_reading() <= 1.0: - return int(self.last_reading() * 100) - else: - return self.last_reading() + return self.last_reading() + + def pubnub_update(self, json_response): + # Pubnub returns the humidity as a decminal + # converting to a percentage + hum = json_response["last_reading"]["humidity"] * 100 + json_response["last_reading"]["humidity"] = hum + self.json_state = json_response class WinkBrightnessSensor(_WinkCapabilitySensor): diff --git a/src/pywink/devices/standard/__init__.py b/src/pywink/devices/standard/__init__.py index 67cf008..de26742 100644 --- a/src/pywink/devices/standard/__init__.py +++ b/src/pywink/devices/standard/__init__.py @@ -134,10 +134,7 @@ def _update_state_from_response(self, response_json, require_desired_state_fulfi :param response_json: the json obj returned from query :return: """ - if 'data' in response_json: - power_strip = response_json.get('data') - else: - power_strip = response_json + power_strip = response_json power_strip_reading = power_strip.get('last_reading') outlets = power_strip.get('outlets', power_strip) for outlet in outlets: @@ -145,9 +142,6 @@ def _update_state_from_response(self, response_json, require_desired_state_fulfi outlet['last_reading']['connection'] = power_strip_reading.get('connection') self.json_state = outlet - def pubnub_update(self, json_response): - self._update_state_from_response(json_response) - def index(self): return self.json_state.get('outlet_index', None) From 147aeb691ff59add42889ccaf1f6c639f851db35 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Fri, 20 May 2016 11:21:32 -0400 Subject: [PATCH 094/178] Added spotter humidity update check --- src/pywink/test/devices/standard/init_test.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/pywink/test/devices/standard/init_test.py b/src/pywink/test/devices/standard/init_test.py index c224ebb..c277d14 100644 --- a/src/pywink/test/devices/standard/init_test.py +++ b/src/pywink/test/devices/standard/init_test.py @@ -321,6 +321,21 @@ def test_gocontrol_motion_sensor_should_be_identified(self): self.assertIsInstance(devices[0], WinkSensorPod) + def test_humidity_is_percentage_after_update(self): + with open('{}/api_responses/quirky_spotter.json'.format(os.path.dirname(__file__))) as spotter_file: + response_dict = json.load(spotter_file) + + sensors = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SENSOR_POD]) + """:type : list of WinkHumiditySensor""" + humidity_sensor = [sensor for sensor in sensors if sensor.capability() is WinkHumiditySensor.CAPABILITY][0] + + with open('{}/api_responses/quirky_spotter_pubnub.json'.format(os.path.dirname(__file__))) as spotter_file: + update_response_dict = json.load(spotter_file) + + humidity_sensor.pubnub_update(update_response_dict) + expected_humidity = 24 + self.assertEquals(expected_humidity, humidity_sensor.humidity_percentage()) + class WinkCapabilitySensorTests(unittest.TestCase): def setUp(self): From 8ae9171d86202372c5a0bd8c89870c901622f0ff Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Fri, 20 May 2016 11:24:02 -0400 Subject: [PATCH 095/178] Added pubunub json for spotter --- .../api_responses/quirky_spotter_pubnub.json | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 src/pywink/test/devices/standard/api_responses/quirky_spotter_pubnub.json diff --git a/src/pywink/test/devices/standard/api_responses/quirky_spotter_pubnub.json b/src/pywink/test/devices/standard/api_responses/quirky_spotter_pubnub.json new file mode 100644 index 0000000..dc57dd4 --- /dev/null +++ b/src/pywink/test/devices/standard/api_responses/quirky_spotter_pubnub.json @@ -0,0 +1,134 @@ +{ + "capabilities":{ + "fields":[ + { + "field":"battery", + "mutability":"read-only", + "type":"percentage" + }, + { + "field":"brightness", + "mutability":"read-only", + "type":"percentage" + }, + { + "field":"external_power", + "mutability":"read-only", + "type":"boolean" + }, + { + "field":"humidity", + "mutability":"read-only", + "type":"percentage" + }, + { + "field":"loudness", + "mutability":"read-only", + "type":"boolean" + }, + { + "field":"temperature", + "mutability":"read-only", + "type":"float" + }, + { + "field":"vibration", + "mutability":"read-only", + "type":"boolean" + } + ], + "needs_wifi_network_list":true + }, + "created_at":1453520234, + "desired_state":{ + + }, + "device_manufacturer":"quirky_ge", + "gang_id":null, + "hidden_at":null, + "hub_id":null, + "icon_code":null, + "icon_id":null, + "last_event":{ + "brightness_occurred_at":1463746054.0962152, + "loudness_occurred_at":1463751976.0731974, + "vibration_occurred_at":1463699867.6401784 + }, + "last_reading":{ + "agent_session_id":null, + "agent_session_id_updated_at":null, + "battery":1.0, + "battery_changed_at":1456972918.8235242, + "battery_updated_at":1463756286.1238914, + "brightness":0.0, + "brightness_changed_at":1463751865.5881312, + "brightness_true":"N/A", + "brightness_true_changed_at":1463746054.0962152, + "brightness_true_updated_at":null, + "brightness_updated_at":1463756286.1238914, + "connection":true, + "connection_changed_at":1453520234.8375406, + "connection_updated_at":1463756286.1238914, + "external_power":true, + "external_power_changed_at":1453520237.0879393, + "external_power_updated_at":1463756286.1238914, + "humidity":0.24, + "humidity_changed_at":1463744925.739977, + "humidity_updated_at":1463756286.1238914, + "loudness":false, + "loudness_changed_at":1463752033.569461, + "loudness_true":"N/A", + "loudness_true_changed_at":1463751976.0731974, + "loudness_true_updated_at":null, + "loudness_updated_at":1463756286.1238914, + "temperature":23.0, + "temperature_changed_at":1463756286.1238914, + "temperature_updated_at":1463756286.1238914, + "vibration":false, + "vibration_changed_at":1463699872.8421204, + "vibration_true":"N/A", + "vibration_true_changed_at":1463699867.6401784, + "vibration_true_updated_at":null, + "vibration_updated_at":1463756286.1238914 + }, + "lat_lng":[ + 12.345678, + -98.76543 + ], + "linked_service_id":null, + "local_id":null, + "locale":"en_us", + "location":"", + "mac_address":"0c2a69023e3b", + "manufacturer_device_id":null, + "manufacturer_device_model":"quirky_ge_spotter", + "model_name":"Spotter", + "name":"Spotter", + "object_id":"156123", + "object_type":"sensor_pod", + "radio_type":null, + "sensor_pod_id":"156123", + "sensor_threshold_events":[ + + ], + "serial":"ABAB00003314", + "subscription":{ + "pubnub":{ + "channel":"e7c46d25d265425356hsdf898a0d05bfc1762405|sensor_pod-156012|user-377857", + "subscribe_key":"sub-c-f7bf7f7e-1234-11e3-a5e8-123456" + } + }, + "triggers":[ + + ], + "units":{ + + }, + "upc_code":"quirky_ge_spotter", + "upc_id":"531", + "user_ids":[ + "377812" + ], + "uuid":"77ee9632-d1c6-4af7-aec3-474091234511", + "nonce":"156012_1463756285_539" +} From 5a9df2b348a6162565cbae0fc579ffbe07d01606 Mon Sep 17 00:00:00 2001 From: William Date: Thu, 26 May 2016 15:22:54 -0400 Subject: [PATCH 096/178] Power strip fixes and subscription tests --- src/pywink/api.py | 32 +++++++++---------- src/pywink/devices/standard/__init__.py | 11 +++++-- src/pywink/test/devices/standard/init_test.py | 6 ++++ 3 files changed, 31 insertions(+), 18 deletions(-) diff --git a/src/pywink/api.py b/src/pywink/api.py index 4431df8..0a8e07d 100644 --- a/src/pywink/api.py +++ b/src/pywink/api.py @@ -107,29 +107,23 @@ def get_sirens(): def get_subscription_key(): - arequest_url = "{}/users/me/wink_devices".format(WinkApiInterface.BASE_URL) - response = requests.get(arequest_url, headers=API_HEADERS) - if response.status_code == 200: - response_dict = response.json() - first_device = response_dict.get('data')[0] - if "subscription" in first_device: - return first_device.get("subscription").get("pubnub").get("subscribe_key") - else: - return None + response_dict = wink_api_fetch() + first_device = response_dict.get('data')[0] + return get_subscription_key_from_response_dict(first_device) - if response.status_code == 401: - raise WinkAPIException("401 Response from Wink API. Maybe Bearer token is expired?") + +def get_subscription_key_from_response_dict(device): + if "subscription" in device: + return device.get("subscription").get("pubnub").get("subscribe_key") else: - raise WinkAPIException("Unexpected") + return None -def get_devices(device_type): +def wink_api_fetch(): arequest_url = "{}/users/me/wink_devices".format(WinkApiInterface.BASE_URL) response = requests.get(arequest_url, headers=API_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) + return response.json() if response.status_code == 401: raise WinkAPIException("401 Response from Wink API. Maybe Bearer token is expired?") @@ -137,6 +131,12 @@ def get_devices(device_type): raise WinkAPIException("Unexpected") +def get_devices(device_type): + response_dict = wink_api_fetch() + filter_key = DEVICE_ID_KEYS.get(device_type) + return get_devices_from_response_dict(response_dict, filter_key) + + def get_devices_from_response_dict(response_dict, filter_key): """ :rtype: list of WinkDevice diff --git a/src/pywink/devices/standard/__init__.py b/src/pywink/devices/standard/__init__.py index de26742..36c1fe4 100644 --- a/src/pywink/devices/standard/__init__.py +++ b/src/pywink/devices/standard/__init__.py @@ -126,8 +126,11 @@ def update_state(self, require_desired_state_fulfilled=False): response = self.api_interface.get_device_state(self, id_override=self.parent_id()) power_strip = response.get('data') if require_desired_state_fulfilled: - if not is_desired_state_reached(power_strip[0]): + if not is_desired_state_reached(self.index): return + else: + self._update_state_from_response(power_strip) + return True def _update_state_from_response(self, response_json, require_desired_state_fulfilled=False): """ @@ -142,6 +145,9 @@ def _update_state_from_response(self, response_json, require_desired_state_fulfi outlet['last_reading']['connection'] = power_strip_reading.get('connection') self.json_state = outlet + def pubnub_update(self, json_response): + self._update_state_from_response(json_response) + def index(self): return self.json_state.get('outlet_index', None) @@ -165,7 +171,8 @@ def set_state(self, state, **kwargs): values = {"outlets": [{}, {"desired_state": {"powered": state}}]} response = self.api_interface.set_device_state(self, values, id_override=self.parent_id()) - self._update_state_from_response(response) + power_strip = response.get('data') + self._update_state_from_response(power_strip) self._last_call = (time.time(), state) diff --git a/src/pywink/test/devices/standard/init_test.py b/src/pywink/test/devices/standard/init_test.py index c277d14..8a92fb7 100644 --- a/src/pywink/test/devices/standard/init_test.py +++ b/src/pywink/test/devices/standard/init_test.py @@ -375,3 +375,9 @@ def test_pubnub_key_and_channel_should_be_none(self): self.assertIsNone(device.pubnub_key) self.assertIsNone(device.pubnub_channel) + + def test_pywink_api_pubnub_subscription_key_is_not_none(self): + with open('{}/api_responses/device_with_pubnub.json'.format(os.path.dirname(__file__))) as lock_file: + response_dict = json.load(lock_file) + + self.assertIsNotNone(self.api_interface.get_subscription_key_from_response_dict(response_dict)) From 79a28094e589ab0e44ac91cd9c508529b95c219d Mon Sep 17 00:00:00 2001 From: William Date: Thu, 26 May 2016 18:26:15 -0400 Subject: [PATCH 097/178] Fix indent error --- src/pywink/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pywink/__init__.py b/src/pywink/__init__.py index 0e75a4b..45d994d 100644 --- a/src/pywink/__init__.py +++ b/src/pywink/__init__.py @@ -4,6 +4,6 @@ # noqa from pywink.api import set_bearer_token, set_wink_credentials, get_bulbs, \ get_eggtrays, get_garage_doors, get_locks, get_powerstrip_outlets, \ - get_sensors, get_shades, get_sirens, get_switches, get_devices, is_token_set - get_subscription_key + get_sensors, get_shades, get_sirens, get_switches, get_devices, \ + is_token_set, get_subscription_key From cd2c63fe02e0b9c5c474f198dc7bbac9999cd04d Mon Sep 17 00:00:00 2001 From: William Date: Thu, 26 May 2016 19:12:04 -0400 Subject: [PATCH 098/178] Fixed typo and powerstrip update method --- src/pywink/devices/sensors.py | 2 +- src/pywink/devices/standard/__init__.py | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/pywink/devices/sensors.py b/src/pywink/devices/sensors.py index 1eda2fe..62ed3a4 100644 --- a/src/pywink/devices/sensors.py +++ b/src/pywink/devices/sensors.py @@ -96,7 +96,7 @@ def humidity_percentage(self): return self.last_reading() def pubnub_update(self, json_response): - # Pubnub returns the humidity as a decminal + # Pubnub returns the humidity as a decimal # converting to a percentage hum = json_response["last_reading"]["humidity"] * 100 json_response["last_reading"]["humidity"] = hum diff --git a/src/pywink/devices/standard/__init__.py b/src/pywink/devices/standard/__init__.py index 36c1fe4..5410aac 100644 --- a/src/pywink/devices/standard/__init__.py +++ b/src/pywink/devices/standard/__init__.py @@ -126,11 +126,15 @@ def update_state(self, require_desired_state_fulfilled=False): response = self.api_interface.get_device_state(self, id_override=self.parent_id()) power_strip = response.get('data') if require_desired_state_fulfilled: - if not is_desired_state_reached(self.index): + if not is_desired_state_reached(power_strip[self.index]): return - else: - self._update_state_from_response(power_strip) - return True + + 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_from_response(self, response_json, require_desired_state_fulfilled=False): """ From 1b6b78a3ffb3ce6706db2d620f516fe35db670d3 Mon Sep 17 00:00:00 2001 From: William Date: Thu, 9 Jun 2016 15:00:04 -0400 Subject: [PATCH 099/178] Update version --- src/pywink/test/devices/standard/init_test.py | 6 +++--- src/setup.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pywink/test/devices/standard/init_test.py b/src/pywink/test/devices/standard/init_test.py index 8a92fb7..b9fad5a 100644 --- a/src/pywink/test/devices/standard/init_test.py +++ b/src/pywink/test/devices/standard/init_test.py @@ -320,7 +320,6 @@ def test_gocontrol_motion_sensor_should_be_identified(self): self.assertEqual(1, len(devices)) self.assertIsInstance(devices[0], WinkSensorPod) - def test_humidity_is_percentage_after_update(self): with open('{}/api_responses/quirky_spotter.json'.format(os.path.dirname(__file__))) as spotter_file: response_dict = json.load(spotter_file) @@ -328,7 +327,7 @@ def test_humidity_is_percentage_after_update(self): sensors = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SENSOR_POD]) """:type : list of WinkHumiditySensor""" humidity_sensor = [sensor for sensor in sensors if sensor.capability() is WinkHumiditySensor.CAPABILITY][0] - + with open('{}/api_responses/quirky_spotter_pubnub.json'.format(os.path.dirname(__file__))) as spotter_file: update_response_dict = json.load(spotter_file) @@ -336,6 +335,7 @@ def test_humidity_is_percentage_after_update(self): expected_humidity = 24 self.assertEquals(expected_humidity, humidity_sensor.humidity_percentage()) + class WinkCapabilitySensorTests(unittest.TestCase): def setUp(self): @@ -353,7 +353,7 @@ def test_should_call_get_state_endpoint_with_capability_removed_from_id(self): sensor.update_state() self.api_interface.get_device_state.assert_called_once_with(sensor, expected_id) - + class WinkPubnubTests(unittest.TestCase): def setUp(self): diff --git a/src/setup.py b/src/setup.py index 0aa97fc..d3e41da 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='0.7.7', + version='0.7.8', description='Access Wink devices via the Wink API', url='http://github.com/bradsk88/python-wink', author='Brad Johnson', From e451b00612fc4ce446bfbcf654bc5504630da866 Mon Sep 17 00:00:00 2001 From: William Date: Thu, 9 Jun 2016 15:49:49 -0400 Subject: [PATCH 100/178] Fix rebase error --- src/pywink/test/devices/standard/init_test.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/src/pywink/test/devices/standard/init_test.py b/src/pywink/test/devices/standard/init_test.py index d8c2fd9..b9fad5a 100644 --- a/src/pywink/test/devices/standard/init_test.py +++ b/src/pywink/test/devices/standard/init_test.py @@ -336,21 +336,6 @@ def test_humidity_is_percentage_after_update(self): self.assertEquals(expected_humidity, humidity_sensor.humidity_percentage()) - def test_humidity_is_percentage_after_update(self): - with open('{}/api_responses/quirky_spotter.json'.format(os.path.dirname(__file__))) as spotter_file: - response_dict = json.load(spotter_file) - - sensors = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SENSOR_POD]) - """:type : list of WinkHumiditySensor""" - humidity_sensor = [sensor for sensor in sensors if sensor.capability() is WinkHumiditySensor.CAPABILITY][0] - - with open('{}/api_responses/quirky_spotter_pubnub.json'.format(os.path.dirname(__file__))) as spotter_file: - update_response_dict = json.load(spotter_file) - - humidity_sensor.pubnub_update(update_response_dict) - expected_humidity = 24 - self.assertEquals(expected_humidity, humidity_sensor.humidity_percentage()) - class WinkCapabilitySensorTests(unittest.TestCase): def setUp(self): @@ -368,11 +353,7 @@ def test_should_call_get_state_endpoint_with_capability_removed_from_id(self): sensor.update_state() self.api_interface.get_device_state.assert_called_once_with(sensor, expected_id) -<<<<<<< HEAD -======= - ->>>>>>> 1ecbdf56dde5d8b71ff6b2d131b7fd0c8fae50c7 class WinkPubnubTests(unittest.TestCase): def setUp(self): From 79a9b4ca45ba68d374336a0452d5514e0c7d7343 Mon Sep 17 00:00:00 2001 From: bradsk88 <7kcwKg60tdcW> Date: Fri, 17 Jun 2016 17:05:29 -0600 Subject: [PATCH 101/178] Fixing broken merge in setup.py --- src/setup.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/setup.py b/src/setup.py index 40e102a..d3e41da 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,11 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', -<<<<<<< HEAD version='0.7.8', -======= - version='0.7.7', ->>>>>>> 1ecbdf56dde5d8b71ff6b2d131b7fd0c8fae50c7 description='Access Wink devices via the Wink API', url='http://github.com/bradsk88/python-wink', author='Brad Johnson', From 856f4bd8b8840dbfbdc0643859b7655a49dd6670 Mon Sep 17 00:00:00 2001 From: William Date: Wed, 22 Jun 2016 11:16:35 -0400 Subject: [PATCH 102/178] Wink Key support --- CHANGELOG.md | 3 ++ src/pywink/__init__.py | 2 +- src/pywink/api.py | 4 ++ src/pywink/devices/factory.py | 4 +- src/pywink/devices/standard/__init__.py | 43 +++++++++++++++++++ src/pywink/devices/types.py | 4 +- .../devices/standard/api_responses/key.json | 33 ++++++++++++++ src/pywink/test/devices/standard/init_test.py | 27 +++++++++++- src/setup.py | 2 +- 9 files changed, 117 insertions(+), 5 deletions(-) create mode 100644 src/pywink/test/devices/standard/api_responses/key.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 5211cb4..de2c233 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 0.7.9 +- Added Wink keys (Wink Lock user codes) + ## 0.7.8 - Added support for retrieving the Pubnub subscription details diff --git a/src/pywink/__init__.py b/src/pywink/__init__.py index 45d994d..c223189 100644 --- a/src/pywink/__init__.py +++ b/src/pywink/__init__.py @@ -5,5 +5,5 @@ from pywink.api import set_bearer_token, set_wink_credentials, get_bulbs, \ get_eggtrays, get_garage_doors, get_locks, get_powerstrip_outlets, \ get_sensors, get_shades, get_sirens, get_switches, get_devices, \ - is_token_set, get_subscription_key + is_token_set, get_subscription_key, get_keys diff --git a/src/pywink/api.py b/src/pywink/api.py index 0a8e07d..f278491 100644 --- a/src/pywink/api.py +++ b/src/pywink/api.py @@ -106,6 +106,10 @@ def get_sirens(): return get_devices(device_types.SIREN) +def get_keys(): + return get_devices(device_types.KEY) + + def get_subscription_key(): response_dict = wink_api_fetch() first_device = response_dict.get('data')[0] diff --git a/src/pywink/devices/factory.py b/src/pywink/devices/factory.py index a8df202..f819e39 100644 --- a/src/pywink/devices/factory.py +++ b/src/pywink/devices/factory.py @@ -1,7 +1,7 @@ from pywink.devices.base import WinkDevice from pywink.devices.sensors import WinkSensorPod from pywink.devices.standard import WinkBulb, WinkBinarySwitch, WinkPowerStripOutlet, WinkLock, \ - WinkEggTray, WinkGarageDoor, WinkShade, WinkSiren + WinkEggTray, WinkGarageDoor, WinkShade, WinkSiren, WinkKey def build_device(device_state_as_json, api_interface): @@ -29,5 +29,7 @@ def build_device(device_state_as_json, api_interface): new_object = WinkShade(device_state_as_json, api_interface) elif "siren_id" in device_state_as_json: new_object = WinkSiren(device_state_as_json, api_interface) + elif "key_id" in device_state_as_json: + new_object = WinkKey(device_state_as_json, api_interface) return new_object or WinkDevice(device_state_as_json, api_interface) diff --git a/src/pywink/devices/standard/__init__.py b/src/pywink/devices/standard/__init__.py index 5410aac..2261d01 100644 --- a/src/pywink/devices/standard/__init__.py +++ b/src/pywink/devices/standard/__init__.py @@ -328,6 +328,49 @@ def device_id(self): return self.json_state.get('siren_id', self.name()) +class WinkKey(WinkDevice): + """ represents a wink.py key + 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 + """ + UNIT = None + + def __init__(self, device_state_as_json, api_interface, objectprefix="keys"): + super(WinkKey, self).__init__(device_state_as_json, api_interface, + objectprefix=objectprefix) + self._capability = "opening" + + def __repr__(self): + return "".format(name=self.name(), + device=self.device_id(), + parent_id=self.parent_id(), + state=self.state()) + + def state(self): + if 'activity_detected' in self._last_reading: + return self._last_reading['activity_detected'] + return False + + def device_id(self): + return self.json_state.get('key_id', self.name()) + + def parent_id(self): + return self.json_state.get('parent_object_id', + self.json_state.get('lock_id')) + + def capability(self): + """Return opening for all keys.""" + return self._capability + + @property + def available(self): + """Keys are virtual therefore they don't have a connection status + always return True + """ + return True + + # pylint-disable: undefined-all-variable __all__ = [WinkEggTray.__name__, WinkBinarySwitch.__name__, diff --git a/src/pywink/devices/types.py b/src/pywink/devices/types.py index b51f7cd..1cdc105 100644 --- a/src/pywink/devices/types.py +++ b/src/pywink/devices/types.py @@ -7,6 +7,7 @@ POWER_STRIP = 'powerstrip' SHADE = 'shades' SIREN = 'siren' +KEY = 'key' DEVICE_ID_KEYS = { BINARY_SWITCH: 'binary_switch_id', @@ -17,5 +18,6 @@ POWER_STRIP: 'powerstrip_id', SENSOR_POD: 'sensor_pod_id', SHADE: 'shade_id', - SIREN: 'siren_id' + SIREN: 'siren_id', + KEY: 'key_id' } diff --git a/src/pywink/test/devices/standard/api_responses/key.json b/src/pywink/test/devices/standard/api_responses/key.json new file mode 100644 index 0000000..e389191 --- /dev/null +++ b/src/pywink/test/devices/standard/api_responses/key.json @@ -0,0 +1,33 @@ +{ + "data":{ + "name":"Key 1", + "last_reading":{ + "slot_id":3, + "slot_id_updated_at":1449960280.1004233, + "activity_detected":true, + "activity_detected_updated_at":1466540653.7276487, + "activity_detected_changed_at":1466540653.7276487 + }, + "key_id":"257123", + "icon_id":null, + "verified_at":null, + "uuid":"b6d02600-3aed-425c-b6f7-77a4701ce9ea", + "parent_object_type":"lock", + "parent_object_id":"61123", + "desired_state":{ + "code":null + }, + "subscription":{ + "pubnub":{ + "subscribe_key":"sub-c-f7bf7f7e-0542-11e3-a5e8-02e1234567fe", + "channel":"d7f700cb00b859eb33bd2fe8344136bc91234560" + } + } + }, + "errors":[ + + ], + "pagination":{ + + } +} diff --git a/src/pywink/test/devices/standard/init_test.py b/src/pywink/test/devices/standard/init_test.py index b9fad5a..098a79f 100644 --- a/src/pywink/test/devices/standard/init_test.py +++ b/src/pywink/test/devices/standard/init_test.py @@ -9,7 +9,7 @@ WinkSoundPresenceSensor, WinkVibrationPresenceSensor, WinkTemperatureSensor, \ _WinkCapabilitySensor from pywink.devices.standard import WinkGarageDoor, WinkPowerStripOutlet, WinkSiren, WinkLock, \ - WinkShade, WinkBinarySwitch, WinkEggTray + WinkShade, WinkBinarySwitch, WinkEggTray, WinkKey from pywink.devices.types import DEVICE_ID_KEYS from pywink.test.devices.standard.api_responses import ApiResponseJSONLoader @@ -381,3 +381,28 @@ def test_pywink_api_pubnub_subscription_key_is_not_none(self): response_dict = json.load(lock_file) self.assertIsNotNone(self.api_interface.get_subscription_key_from_response_dict(response_dict)) + + + +class WinkKeyTests(unittest.TestCase): + + def setUp(self): + super(WinkKeyTests, self).setUp() + self.api_interface = mock.MagicMock() + + def test_device_id_should_be_number(self): + with open('{}/api_responses/key.json'.format(os.path.dirname(__file__))) as keys_file: + response_dict = json.load(keys_file) + key = response_dict.get('data') + + wink_key = WinkKey(key, self.api_interface) + device_id = wink_key.device_id() + self.assertRegex(device_id, "^[0-9]{4,6}$") + + def test_state_should_be_true_or_false(self): + with open('{}/api_responses/key.json'.format(os.path.dirname(__file__))) as keys_file: + response_dict = json.load(keys_file) + key = response_dict.get('data') + + wink_true_key = WinkKey(key, self.api_interface) + self.assertTrue(wink_true_key.state()) diff --git a/src/setup.py b/src/setup.py index d3e41da..1ba2cf8 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='0.7.8', + version='0.7.9', description='Access Wink devices via the Wink API', url='http://github.com/bradsk88/python-wink', author='Brad Johnson', From 0d5a57fbf5edae35d2efe9ed4dda9881d4e916e5 Mon Sep 17 00:00:00 2001 From: bradsk88 <7kcwKg60tdcW> Date: Mon, 11 Jul 2016 16:13:44 -0600 Subject: [PATCH 103/178] Fixing pylint --- src/pywink/__init__.py | 1 - src/pywink/color.py | 1 - 2 files changed, 2 deletions(-) diff --git a/src/pywink/__init__.py b/src/pywink/__init__.py index c223189..c427f8a 100644 --- a/src/pywink/__init__.py +++ b/src/pywink/__init__.py @@ -6,4 +6,3 @@ get_eggtrays, get_garage_doors, get_locks, get_powerstrip_outlets, \ get_sensors, get_shades, get_sirens, get_switches, get_devices, \ is_token_set, get_subscription_key, get_keys - diff --git a/src/pywink/color.py b/src/pywink/color.py index 3424690..c6c70dd 100644 --- a/src/pywink/color.py +++ b/src/pywink/color.py @@ -66,7 +66,6 @@ def color_temperature_to_rgb(color_temperature_kelvin): # taken from # https://github.com/benknight/hue-python-rgb-converter/blob/master/rgb_cie.py # Copyright (c) 2014 Benjamin Knight / MIT License. -# pylint: disable=bad-builtin def color_xy_brightness_to_rgb(vX, vY, brightness): """ Convert from XYZ to RGB. From e3c683958b3c25d0e04b3f26e18990247b3851e0 Mon Sep 17 00:00:00 2001 From: William Date: Tue, 12 Jul 2016 08:27:36 -0400 Subject: [PATCH 104/178] Switched API url --- src/pywink/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pywink/api.py b/src/pywink/api.py index f278491..508e400 100644 --- a/src/pywink/api.py +++ b/src/pywink/api.py @@ -13,7 +13,7 @@ class WinkApiInterface(object): - BASE_URL = "https://winkapi.quirky.com" + BASE_URL = "https://api.wink.com" def set_device_state(self, device, state, id_override=None): """ From 9614bffd04d587d1e7353c67afad4516063873b1 Mon Sep 17 00:00:00 2001 From: William Date: Tue, 12 Jul 2016 10:34:38 -0400 Subject: [PATCH 105/178] Updated version --- CHANGELOG.md | 3 +++ src/setup.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de2c233..7ed4d8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 0.7.10 +- Changed API URL + ## 0.7.9 - Added Wink keys (Wink Lock user codes) diff --git a/src/setup.py b/src/setup.py index 1ba2cf8..c408096 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='0.7.9', + version='0.7.10', description='Access Wink devices via the Wink API', url='http://github.com/bradsk88/python-wink', author='Brad Johnson', From 66dc2ce5722f9a346493df33837c113ab782d242 Mon Sep 17 00:00:00 2001 From: William Date: Sat, 16 Jul 2016 20:25:49 -0400 Subject: [PATCH 106/178] Added support for leak sensors --- src/pywink/api.py | 5 +- src/pywink/devices/sensors.py | 18 ++++ .../standard/api_responses/liquid_sensor.json | 83 +++++++++++++++++++ src/pywink/test/devices/standard/init_test.py | 12 ++- 4 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 src/pywink/test/devices/standard/api_responses/liquid_sensor.json diff --git a/src/pywink/api.py b/src/pywink/api.py index 508e400..b9273fd 100644 --- a/src/pywink/api.py +++ b/src/pywink/api.py @@ -5,7 +5,7 @@ from pywink.devices import types as device_types from pywink.devices.factory import build_device from pywink.devices.sensors import WinkSensorPod, WinkHumiditySensor, WinkBrightnessSensor, WinkSoundPresenceSensor, \ - WinkTemperatureSensor, WinkVibrationPresenceSensor + WinkTemperatureSensor, WinkVibrationPresenceSensor, WinkLiquidPresenceSensor from pywink.devices.types import DEVICE_ID_KEYS API_HEADERS = {} @@ -199,6 +199,9 @@ def _get_subsensors_from_sensor_pod(item, api_interface): if WinkVibrationPresenceSensor.CAPABILITY in capabilities: subsensors.append(WinkVibrationPresenceSensor(item, api_interface)) + if WinkLiquidPresenceSensor.CAPABILITY in capabilities: + subsensors.append(WinkLiquidPresenceSensor(item, api_interface)) + if WinkSensorPod.CAPABILITY in capabilities: subsensors.append(WinkSensorPod(item, api_interface)) diff --git a/src/pywink/devices/sensors.py b/src/pywink/devices/sensors.py index 62ed3a4..2aa0e3a 100644 --- a/src/pywink/devices/sensors.py +++ b/src/pywink/devices/sensors.py @@ -173,3 +173,21 @@ def vibration_boolean(self): :rtype: bool """ return self.last_reading() + + +class WinkLiquidPresenceSensor(_WinkCapabilitySensor): + + CAPABILITY = 'liquid_detected' + UNIT = None + + def __init__(self, device_state_as_json, api_interface): + super(WinkLiquidPresenceSensor, self).__init__(device_state_as_json, api_interface, + self.CAPABILITY, + self.UNIT) + + def liquid_boolean(self): + """ + :return: Returns True if liquid is detected. + :rtype: bool + """ + return self.last_reading() diff --git a/src/pywink/test/devices/standard/api_responses/liquid_sensor.json b/src/pywink/test/devices/standard/api_responses/liquid_sensor.json new file mode 100644 index 0000000..64a147d --- /dev/null +++ b/src/pywink/test/devices/standard/api_responses/liquid_sensor.json @@ -0,0 +1,83 @@ +{ + "data":[ + { + "last_event":{ + "brightness_occurred_at":null, + "loudness_occurred_at":null, + "vibration_occurred_at":null + }, + "uuid":"0e614bf2-ef85-4bc9-90d8-8f07418a7123", + "desired_state":{ + + }, + "last_reading":{ + "liquid_detected":false, + "liquid_detected_updated_at":1468619519.7377036, + "battery":1, + "battery_updated_at":1468619519.7377036, + "liquid_detected_true":null, + "liquid_detected_true_updated_at":null, + "connection":true, + "connection_updated_at":1468619519.7377036, + "agent_session_id":null, + "agent_session_id_updated_at":null, + "connection_changed_at":1468619516.3395233, + "liquid_detected_changed_at":1468619516.8365848, + "battery_changed_at":1468619516.8365848 + }, + "subscription":{ + "pubnub":{ + "subscribe_key":"sub-c-f7b1237e-0542-1233-a5e8-02ee354b7fe", + "channel":"5d54ad5517ccc1c05695704a5c1b8c3234e2342e|sensor_pod-240456|user-377857" + } + }, + "sensor_pod_id":"241253", + "name":"Water", + "locale":"en_us", + "units":{ + + }, + "created_at":1468619516, + "hidden_at":null, + "capabilities":{ + "fields":[ + { + "type":"boolean", + "field":"liquid_detected", + "mutability":"read-only" + }, + { + "type":"percentage", + "field":"battery", + "mutability":"read-only" + } + ] + }, + "triggers":[ + + ], + "manufacturer_device_model":"aeon_labs_dsb45_zwus", + "manufacturer_device_id":null, + "device_manufacturer":"aeon_labs", + "model_name":"Z-Wave Water Sensor Sensor", + "upc_id":"339", + "upc_code":"generic_water_sensor", + "gang_id":null, + "hub_id":"312328", + "local_id":"23", + "radio_type":"zwave", + "linked_service_id":null, + "lat_lng":[ + "12.34567", + "-89.76543" + ], + "location":"" + } + ], + "errors":[ + + ], + "pagination":{ + + } +} diff --git a/src/pywink/test/devices/standard/init_test.py b/src/pywink/test/devices/standard/init_test.py index 098a79f..8621166 100644 --- a/src/pywink/test/devices/standard/init_test.py +++ b/src/pywink/test/devices/standard/init_test.py @@ -7,7 +7,7 @@ from pywink.devices import types as device_types from pywink.devices.sensors import WinkSensorPod, WinkBrightnessSensor, WinkHumiditySensor, \ WinkSoundPresenceSensor, WinkVibrationPresenceSensor, WinkTemperatureSensor, \ - _WinkCapabilitySensor + _WinkCapabilitySensor, WinkLiquidPresenceSensor from pywink.devices.standard import WinkGarageDoor, WinkPowerStripOutlet, WinkSiren, WinkLock, \ WinkShade, WinkBinarySwitch, WinkEggTray, WinkKey from pywink.devices.types import DEVICE_ID_KEYS @@ -335,6 +335,16 @@ def test_humidity_is_percentage_after_update(self): expected_humidity = 24 self.assertEquals(expected_humidity, humidity_sensor.humidity_percentage()) + def test_liquid_detected_should_have_correct_value(self): + with open('{}/api_responses/liquid_sensor.json'.format(os.path.dirname(__file__))) as spotter_file: + response_dict = json.load(spotter_file) + + sensors = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SENSOR_POD]) + """:type : list of WinkLiquidPresenceSensor""" + liquid_sensor = [sensor for sensor in sensors if sensor.capability() is WinkLiquidPresenceSensor.CAPABILITY][0] + expected_liquid_presence = False + self.assertEquals(expected_liquid_presence, liquid_sensor.liquid_boolean()) + class WinkCapabilitySensorTests(unittest.TestCase): From c8a941f983b737153842904fc03543557e5814d1 Mon Sep 17 00:00:00 2001 From: William Date: Sat, 16 Jul 2016 20:27:02 -0400 Subject: [PATCH 107/178] Updated version --- CHANGELOG.md | 3 +++ src/setup.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ed4d8f..6a1f94e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 0.7.11 +- Added Wink leak sensor support + ## 0.7.10 - Changed API URL diff --git a/src/setup.py b/src/setup.py index c408096..6e1e67d 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='0.7.10', + version='0.7.11', description='Access Wink devices via the Wink API', url='http://github.com/bradsk88/python-wink', author='Brad Johnson', From 37cffbd8938320d3c778412d001131186e7820bd Mon Sep 17 00:00:00 2001 From: p Date: Sun, 24 Jul 2016 11:15:26 -0700 Subject: [PATCH 108/178] Fix bulb constructor --- src/pywink/devices/standard/bulb.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pywink/devices/standard/bulb.py b/src/pywink/devices/standard/bulb.py index 6c3d4b5..e401ca4 100644 --- a/src/pywink/devices/standard/bulb.py +++ b/src/pywink/devices/standard/bulb.py @@ -16,8 +16,8 @@ class WinkBulb(WinkBinarySwitch): json_state = {} def __init__(self, device_state_as_json, api_interface): - super().__init__(device_state_as_json, api_interface, - objectprefix="light_bulbs") + super(WinkBulb, self).__init__(device_state_as_json, api_interface, + objectprefix="light_bulbs") def device_id(self): return self.json_state.get('light_bulb_id', self.name()) From 22ba866a964497acf6324e9c6649e39748932527 Mon Sep 17 00:00:00 2001 From: bradsk88 <7kcwKg60tdcW> Date: Sun, 24 Jul 2016 13:51:36 -0600 Subject: [PATCH 109/178] Upping version --- CHANGELOG.md | 3 +++ src/setup.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a1f94e..1fad56b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 0.7.12 +- Made WinkBulb constructor python 3-compatible. + ## 0.7.11 - Added Wink leak sensor support diff --git a/src/setup.py b/src/setup.py index 6e1e67d..dfab82d 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='0.7.11', + version='0.7.12', description='Access Wink devices via the Wink API', url='http://github.com/bradsk88/python-wink', author='Brad Johnson', From b7db3bd45a21ad36e6e47ff0dc40f519a525b9db Mon Sep 17 00:00:00 2001 From: William Date: Wed, 6 Jul 2016 15:57:45 -0400 Subject: [PATCH 110/178] Support for Quirky Porkfolio Updated JSON and fixed update_state on Nose Fixed imports in api.py --- src/pywink/__init__.py | 2 +- src/pywink/api.py | 19 +++- src/pywink/devices/sensors.py | 22 +++++ src/pywink/devices/standard/__init__.py | 52 +++++++++- src/pywink/devices/types.py | 4 +- .../standard/api_responses/porkfolio.json | 94 +++++++++++++++++++ src/pywink/test/devices/standard/init_test.py | 30 +++++- 7 files changed, 216 insertions(+), 7 deletions(-) create mode 100644 src/pywink/test/devices/standard/api_responses/porkfolio.json diff --git a/src/pywink/__init__.py b/src/pywink/__init__.py index c427f8a..e142ffb 100644 --- a/src/pywink/__init__.py +++ b/src/pywink/__init__.py @@ -5,4 +5,4 @@ from pywink.api import set_bearer_token, set_wink_credentials, get_bulbs, \ get_eggtrays, get_garage_doors, get_locks, get_powerstrip_outlets, \ get_sensors, get_shades, get_sirens, get_switches, get_devices, \ - is_token_set, get_subscription_key, get_keys + is_token_set, get_subscription_key, get_keys, get_piggy_banks diff --git a/src/pywink/api.py b/src/pywink/api.py index b9273fd..79d3f0e 100644 --- a/src/pywink/api.py +++ b/src/pywink/api.py @@ -4,8 +4,10 @@ from pywink.devices import types as device_types from pywink.devices.factory import build_device +from pywink.devices.standard import WinkPorkfolioNose from pywink.devices.sensors import WinkSensorPod, WinkHumiditySensor, WinkBrightnessSensor, WinkSoundPresenceSensor, \ - WinkTemperatureSensor, WinkVibrationPresenceSensor, WinkLiquidPresenceSensor + WinkTemperatureSensor, WinkVibrationPresenceSensor, \ + WinkLiquidPresenceSensor, WinkCurrencySensor from pywink.devices.types import DEVICE_ID_KEYS API_HEADERS = {} @@ -110,6 +112,10 @@ def get_keys(): return get_devices(device_types.KEY) +def get_piggy_banks(): + return get_devices(device_types.PIGGY_BANK) + + def get_subscription_key(): response_dict = wink_api_fetch() first_device = response_dict.get('data')[0] @@ -171,6 +177,10 @@ def get_devices_from_response_dict(response_dict, filter_key): if len(subsensors) == 1: continue + if key == "piggy_bank_id": + devices.extend(__get_devices_from_piggy_bank(item, api_interface)) + continue # Don't capture the porkfolio itself as a device + devices.append(build_device(item, api_interface)) return devices @@ -217,6 +227,13 @@ def __get_outlets_from_powerstrip(item, api_interface): return [build_device(outlet, api_interface) for outlet in outlets if __device_is_visible(outlet, 'outlet_id')] +def __get_devices_from_piggy_bank(item, api_interface): + subdevices = [] + subdevices.append(WinkCurrencySensor(item, api_interface)) + subdevices.append(WinkPorkfolioNose(item, api_interface)) + return subdevices + + def __device_is_visible(item, key): is_correctly_structured = bool(item.get(key)) is_visible = not item.get('hidden_at') diff --git a/src/pywink/devices/sensors.py b/src/pywink/devices/sensors.py index 2aa0e3a..3a8b895 100644 --- a/src/pywink/devices/sensors.py +++ b/src/pywink/devices/sensors.py @@ -191,3 +191,25 @@ def liquid_boolean(self): :rtype: bool """ return self.last_reading() + + +class WinkCurrencySensor(_WinkCapabilitySensor): + + CAPABILITY = 'balance' + UNIT = 'USD' + + def __init__(self, device_state_as_json, api_interface): + super(WinkCurrencySensor, self).__init__(device_state_as_json, api_interface, + self.CAPABILITY, + self.UNIT) + + def device_id(self): + root_name = self.json_state.get('piggy_bank_id', self.name()) + return '{}+{}'.format(root_name, self._capability) + + def balance(self): + """ + :return: Returns the balance in cents. + :rtype: int + """ + return self.last_reading() diff --git a/src/pywink/devices/standard/__init__.py b/src/pywink/devices/standard/__init__.py index 2261d01..fd84aba 100644 --- a/src/pywink/devices/standard/__init__.py +++ b/src/pywink/devices/standard/__init__.py @@ -371,6 +371,55 @@ def available(self): return True +class WinkPorkfolioNose(WinkDevice): + """ + Represents a Wink Porkfolio nose + 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, api_interface): + super().__init__(device_state_as_json, api_interface, + objectprefix="piggy_banks") + + @property + def available(self): + """ + connection variable isn't stable. + Porkfolio can be offline, but updates will continue to occur. + always returning True to avoid this issue. + """ + return True + + def device_id(self): + root_name = self.json_state.get('piggy_bank_id', self.name()) + return '{}+{}'.format(root_name, "nose") + + def set_state(self, color_hex): + """ + :param nose_color: a hex string indicating the color of the porkfolio nose + :return: nothing + From the api... + "the color of the nose is not in the desired_state + but on the object itself." + """ + root_name = self.json_state.get('piggy_bank_id', self.name()) + response = self.api_interface.set_device_state(self, { + "nose_color": color_hex + }, root_name) + self._update_state_from_response(response) + + def state(self): + """ + Hex colour value: String or None + :rtype: list float + """ + return self.json_state.get('nose_color', None) + + # pylint-disable: undefined-all-variable __all__ = [WinkEggTray.__name__, WinkBinarySwitch.__name__, @@ -379,4 +428,5 @@ def available(self): WinkPowerStripOutlet.__name__, WinkGarageDoor.__name__, WinkShade.__name__, - WinkSiren.__name__] + WinkSiren.__name__, + WinkPorkfolioNose.__name__] diff --git a/src/pywink/devices/types.py b/src/pywink/devices/types.py index 1cdc105..7428587 100644 --- a/src/pywink/devices/types.py +++ b/src/pywink/devices/types.py @@ -8,6 +8,7 @@ SHADE = 'shades' SIREN = 'siren' KEY = 'key' +PIGGY_BANK = 'piggybank' DEVICE_ID_KEYS = { BINARY_SWITCH: 'binary_switch_id', @@ -19,5 +20,6 @@ SENSOR_POD: 'sensor_pod_id', SHADE: 'shade_id', SIREN: 'siren_id', - KEY: 'key_id' + KEY: 'key_id', + PIGGY_BANK: 'piggy_bank_id' } diff --git a/src/pywink/test/devices/standard/api_responses/porkfolio.json b/src/pywink/test/devices/standard/api_responses/porkfolio.json new file mode 100644 index 0000000..fcf7d25 --- /dev/null +++ b/src/pywink/test/devices/standard/api_responses/porkfolio.json @@ -0,0 +1,94 @@ +{ + "data":[ + { + "balance":0, + "nose_color":"2400FF", + "last_deposit_amount":-345, + "savings_goal":5000, + "orientation":false, + "vibration":false, + "uuid":"09e5977d-838f-1234-9eca-d5b715008dac", + "desired_state":{ + "nose_color":"2400FF" + }, + "last_reading":{ + "connection":true, + "connection_updated_at":1467473921.827009, + "amount":-345, + "amount_updated_at":1467475089.9699109, + "battery":null, + "battery_updated_at":null, + "balance":0, + "balance_updated_at":1467475089.9699109, + "orientation":null, + "orientation_updated_at":null, + "units_updated_at":1467473921.6321552, + "vibration":null, + "vibration_updated_at":null, + "units_changed_at":1467473921.6321552, + "connection_changed_at":1467473921.827009, + "amount_changed_at":1467475089.9699109, + "balance_changed_at":1467475089.9699109 + }, + "subscription":{ + "pubnub":{ + "subscribe_key":"sub-c-f7bf23234f7e-0542-11e3-a5e8-02ee2dd1241241241", + "channel":"0441c5de80107a19b8fa9dac6c90505a1f3dd05f|piggy_bank-15215|user-377123" + } + }, + "piggy_bank_id":"15266", + "name":"Porkfolio", + "locale":"en_us", + "units":{ + "currency":"USD" + }, + "created_at":1467473921, + "hidden_at":null, + "capabilities":{ + "needs_wifi_network_list":true + }, + "triggers":[ + { + "trigger_id":"223812", + "name":"Porkfolio vibration", + "enabled":false, + "trigger_configuration":{ + "reading_type":"vibration", + "edge":"rising", + "threshold":1, + "object_id":"15243", + "object_type":"piggy_bank" + }, + "channel_configuration":{ + "recipient_user_ids":[ + "*" + ], + "channel_id":"15", + "object_type":null, + "object_id":null + }, + "robot_id":"3389366", + "triggered_at":null, + "piggy_bank_alert_id":"223838" + } + ], + "device_manufacturer":"quirky", + "model_name":"Porkfolio", + "upc_id":"526", + "upc_code":"quirky_porkfolio", + "lat_lng":[ + 12.345678, + -89.765432 + ], + "location":"", + "mac_address":"0ccccc02de1b", + "serial":"ACA123427412" + } + ], + "errors":[ + + ], + "pagination":{ + + } +} diff --git a/src/pywink/test/devices/standard/init_test.py b/src/pywink/test/devices/standard/init_test.py index 8621166..e5b68af 100644 --- a/src/pywink/test/devices/standard/init_test.py +++ b/src/pywink/test/devices/standard/init_test.py @@ -7,9 +7,9 @@ from pywink.devices import types as device_types from pywink.devices.sensors import WinkSensorPod, WinkBrightnessSensor, WinkHumiditySensor, \ WinkSoundPresenceSensor, WinkVibrationPresenceSensor, WinkTemperatureSensor, \ - _WinkCapabilitySensor, WinkLiquidPresenceSensor + _WinkCapabilitySensor, WinkLiquidPresenceSensor, WinkCurrencySensor from pywink.devices.standard import WinkGarageDoor, WinkPowerStripOutlet, WinkSiren, WinkLock, \ - WinkShade, WinkBinarySwitch, WinkEggTray, WinkKey + WinkShade, WinkBinarySwitch, WinkEggTray, WinkKey, WinkPorkfolioNose from pywink.devices.types import DEVICE_ID_KEYS from pywink.test.devices.standard.api_responses import ApiResponseJSONLoader @@ -393,7 +393,6 @@ def test_pywink_api_pubnub_subscription_key_is_not_none(self): self.assertIsNotNone(self.api_interface.get_subscription_key_from_response_dict(response_dict)) - class WinkKeyTests(unittest.TestCase): def setUp(self): @@ -416,3 +415,28 @@ def test_state_should_be_true_or_false(self): wink_true_key = WinkKey(key, self.api_interface) self.assertTrue(wink_true_key.state()) + + +class PorkfolioTests(unittest.TestCase): + + def setUp(self): + super(PorkfolioTests, self).setUp() + self.api_interface = mock.MagicMock() + + def test_should_handle_porkfolio_response(self): + with open('{}/api_responses/porkfolio.json'.format(os.path.dirname(__file__))) as porkfolio_file: + response_dict = json.load(porkfolio_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.PIGGY_BANK]) + self.assertEqual(2, len(devices)) + self.assertIsInstance(devices[0], WinkCurrencySensor) + self.assertIsInstance(devices[1], WinkPorkfolioNose) + + def test_device_id_should_be_number(self): + with open('{}/api_responses/porkfolio.json'.format(os.path.dirname(__file__))) as porkfolio_file: + response_dict = json.load(porkfolio_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.PIGGY_BANK]) + device_id = devices[0].device_id() + self.assertRegex(device_id, "^[0-9]{4,6}") + + device_id = devices[1].device_id() + self.assertRegex(device_id, "^[0-9]{4,6}") From 01917c1b85964ea1956ae525261b07564994195d Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Mon, 15 Aug 2016 18:18:00 -0400 Subject: [PATCH 111/178] Fix detection of bulb capabilites --- src/pywink/devices/standard/bulb.py | 55 +++++++++++++++++------------ 1 file changed, 33 insertions(+), 22 deletions(-) diff --git a/src/pywink/devices/standard/bulb.py b/src/pywink/devices/standard/bulb.py index e401ca4..929c630 100644 --- a/src/pywink/devices/standard/bulb.py +++ b/src/pywink/devices/standard/bulb.py @@ -123,44 +123,55 @@ def _format_color_data(self, color_hue_saturation, color_kelvin, color_xy): return {} def supports_rgb(self): - capabilities = self._last_reading.get('capabilities', {}) - if not capabilities.get('color_changeable', False): + capabilities = self.json_state.get('capabilities', {}) + cap_fields = capabilities.get('fields', False) + if not cap_fields: return False - # cap_fields = capabilities.get('fields', []) - # TODO: Do any wink bulbs support RGB specification? + for field in cap_fields: + _field = field.get('field') + if _field == 'color_model': + choices = field.get('choices') + if "rgb" in choices: + return True return False def supports_hue_saturation(self): capabilities = self.json_state.get('capabilities', {}) - if not capabilities.get('color_changeable', False): + cap_fields = capabilities.get('fields', False) + if not cap_fields: return False - cap_fields = capabilities.get('fields', []) - supports_hue = False - supports_saturation = False for field in cap_fields: _field = field.get('field') - if _field == 'hue': - supports_hue = True - if _field == 'saturation': - supports_saturation = True - if supports_hue and supports_saturation: - return True - if supports_hue and supports_saturation: - return True + if _field == 'color_model': + choices = field.get('choices') + if "hsb" in choices: + return True + return False def supports_xy_color(self): - # TODO: Do any wink bulbs support XY color? - return self.json_state.get("todo", False) + capabilities = self.json_state.get('capabilities', {}) + cap_fields = capabilities.get('fields', False) + if not cap_fields: + return False + for field in cap_fields: + _field = field.get('field') + if _field == 'color_model': + choices = field.get('choices') + if "xy" in choices: + return True + return False def supports_temperature(self): capabilities = self.json_state.get('capabilities', {}) - if not capabilities.get('color_changeable', False): + cap_fields = capabilities.get('fields', False) + if not cap_fields: return False - cap_fields = capabilities.get('fields', []) for field in cap_fields: _field = field.get('field') - if _field == 'color_temperature': - return True + if _field == 'color_model': + choices = field.get('choices') + if "color_temperature" in choices: + return True return False def __repr__(self): From c99df8e52c5f92fbfc87ed1db595ab97596f7045 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Mon, 15 Aug 2016 18:26:22 -0400 Subject: [PATCH 112/178] Updated version --- CHANGELOG.md | 3 +++ src/setup.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fad56b..97353cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 0.7.13 +- Changed method of detecting WinkBulb capabilities + ## 0.7.12 - Made WinkBulb constructor python 3-compatible. diff --git a/src/setup.py b/src/setup.py index dfab82d..8590b4a 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='0.7.12', + version='0.7.13', description='Access Wink devices via the Wink API', url='http://github.com/bradsk88/python-wink', author='Brad Johnson', From 5798aef7cdb7272d69531dfe0642967c4ae84a3d Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Mon, 15 Aug 2016 18:56:42 -0400 Subject: [PATCH 113/178] Updated hardcoded api responses in tests --- .../api_responses/temperature_present.json | 3 +- src/pywink/test/devices/standard/bulb_test.py | 63 ++++++------------- 2 files changed, 21 insertions(+), 45 deletions(-) diff --git a/src/pywink/test/devices/standard/api_responses/temperature_present.json b/src/pywink/test/devices/standard/api_responses/temperature_present.json index ddc53a1..ae4e04b 100644 --- a/src/pywink/test/devices/standard/api_responses/temperature_present.json +++ b/src/pywink/test/devices/standard/api_responses/temperature_present.json @@ -80,7 +80,8 @@ "type": "string", "choices": [ "rgb", - "hsb" + "hsb", + "color_temperature" ] }, { diff --git a/src/pywink/test/devices/standard/bulb_test.py b/src/pywink/test/devices/standard/bulb_test.py index 94ee0bf..6390531 100644 --- a/src/pywink/test/devices/standard/bulb_test.py +++ b/src/pywink/test/devices/standard/bulb_test.py @@ -74,10 +74,8 @@ def test_should_send_current_brightness_to_api_if_only_color_temperature_is_prov 'desired_brightness': original_brightness }, 'capabilities': { - 'color_changeable': True, - 'fields': [{ - 'field': 'color_temperature' - }] + 'fields': [{'field': 'color_model', + 'choices':["color_temperature"]}] } }, self.api_interface) bulb.set_state(True, color_kelvin=4000) @@ -88,10 +86,8 @@ def test_should_send_current_brightness_to_api_if_only_color_temperature_is_prov def test_should_send_color_temperature_to_api_if_color_temp_is_provided_and_bulb_only_supports_temperature(self): bulb = WinkBulb({ 'capabilities': { - 'color_changeable': True, - 'fields': [{ - 'field': 'color_temperature' - }] + 'fields': [{'field': 'color_model', + 'choices':["color_temperature"]}] } }, self.api_interface) color_kelvin = 4000 @@ -108,9 +104,8 @@ def test_should_send_current_brightness_to_api_if_only_color_temperature_is_prov 'desired_brightness': original_brightness }, 'capabilities': { - 'color_changeable': True, - 'fields': [{'field': 'hue'}, - {'field': 'saturation'}] + 'fields': [{'field': 'color_model', + 'choices':["hsb"]}] } }, self.api_interface) bulb.set_state(True, color_kelvin=4000) @@ -121,9 +116,8 @@ def test_should_send_current_brightness_to_api_if_only_color_temperature_is_prov def test_should_send_current_hue_and_saturation_to_api_if_hue_and_sat_are_provided_and_bulb_only_supports_hue_sat(self): bulb = WinkBulb({ 'capabilities': { - 'color_changeable': True, - 'fields': [{'field': 'hue'}, - {'field': 'saturation'}] + 'fields': [{'field': 'color_model', + 'choices':["hsb"]}] } }, self.api_interface) hue = 0.2 @@ -141,9 +135,8 @@ def test_should_send_original_brightness_when_only_xy_color_given_and_only_hue_s 'desired_brightness': original_brightness }, 'capabilities': { - 'color_changeable': True, - 'fields': [{'field': 'hue'}, - {'field': 'saturation'}] + 'fields': [{'field': 'color_model', + 'choices':["hsb"]}] } }, self.api_interface) bulb.set_state(True, color_xy=[0.5, 0.5]) @@ -168,16 +161,9 @@ def test_should_handle_light_bulb_response(self): @mock.patch('requests.put') def test_should_send_correct_color_hsb_values_to_wink_api(self, put_mock): bulb = WinkBulb({ - "capabilities": { - "fields": [ - { - "field": "hue" - }, - { - "field": "saturation" - } - ], - "color_changeable": True + 'capabilities': { + 'fields': [{'field': 'color_model', + 'choices':["hsb"]}] } }, self.api_interface) hue = 0.75 @@ -191,13 +177,9 @@ def test_should_send_correct_color_hsb_values_to_wink_api(self, put_mock): @mock.patch('requests.put') def test_should_send_correct_color_temperature_values_to_wink_api(self, put_mock): bulb = WinkBulb({ - "capabilities": { - "fields": [ - { - "field": "color_temperature" - } - ], - "color_changeable": True + 'capabilities': { + 'fields': [{'field': 'color_model', + 'choices':["color_temperature"]}] } }, self.api_interface) arbitrary_kelvin_color = 4950 @@ -209,16 +191,9 @@ def test_should_send_correct_color_temperature_values_to_wink_api(self, put_mock @mock.patch('requests.put') def test_should_only_send_color_hsb_if_both_color_hsb_and_color_temperature_are_given(self, put_mock): bulb = WinkBulb({ - "capabilities": { - "fields": [ - { - "field": "hue" - }, - { - "field": "saturation" - } - ], - "color_changeable": True + 'capabilities': { + 'fields': [{'field': 'color_model', + 'choices':["hsb"]}] } }, self.api_interface) arbitrary_kelvin_color = 4950 From 5be3450677ddcc52edb15e15db0c96ec0705973d Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Thu, 18 Aug 2016 16:02:30 -0400 Subject: [PATCH 114/178] Added tests for xy and rgb --- .../standard/api_responses/rgb_absent.json | 119 ++++++++++++++++++ .../standard/api_responses/rgb_present.json | 119 ++++++++++++++++++ .../standard/api_responses/xy_absent.json | 119 ++++++++++++++++++ .../standard/api_responses/xy_present.json | 119 ++++++++++++++++++ src/pywink/test/devices/standard/bulb_test.py | 49 ++++++++ 5 files changed, 525 insertions(+) create mode 100644 src/pywink/test/devices/standard/api_responses/rgb_absent.json create mode 100644 src/pywink/test/devices/standard/api_responses/rgb_present.json create mode 100644 src/pywink/test/devices/standard/api_responses/xy_absent.json create mode 100644 src/pywink/test/devices/standard/api_responses/xy_present.json diff --git a/src/pywink/test/devices/standard/api_responses/rgb_absent.json b/src/pywink/test/devices/standard/api_responses/rgb_absent.json new file mode 100644 index 0000000..ec2f312 --- /dev/null +++ b/src/pywink/test/devices/standard/api_responses/rgb_absent.json @@ -0,0 +1,119 @@ +{ + "data": [ + { + "uuid": "238539e2-1ad6-44ba-bc53-33c684c36e1d", + "desired_state": { + "powered": false, + "brightness": 1, + "color_model": "hsb", + "hue": 0.35, + "saturation": 1, + "color_temperature": 1901 + }, + "last_reading": { + "connection": true, + "connection_updated_at": 1458628651.862995, + "powered": false, + "powered_updated_at": 1458628651.862995, + "brightness": 1, + "brightness_updated_at": 1458628651.862995, + "color_model": "hsb", + "color_model_updated_at": 1458628651.862995, + "hue": 0.35, + "hue_updated_at": 1458628651.862995, + "saturation": 1, + "saturation_updated_at": 1458628651.862995, + "color_temperature": 1901, + "color_temperature_updated_at": 1458628651.862995, + "firmware_version": "0.1b02 / 0.3b22", + "firmware_version_updated_at": 1458628651.862995, + "firmware_date_code": "20150929N****", + "firmware_date_code_updated_at": 1458628651.862995, + "desired_powered_updated_at": 1458628650.8619466, + "desired_brightness_updated_at": 1458628820.0301423, + "desired_color_model_updated_at": 1458628820.0301423, + "desired_hue_updated_at": 1458628820.0301423, + "desired_saturation_updated_at": 1458628820.0301423, + "desired_color_temperature_updated_at": 1458628820.0301423, + "powered_changed_at": 1458628650.8134031, + "brightness_changed_at": 1458122238.7788615, + "connection_changed_at": 1457517588.4372394, + "desired_powered_changed_at": 1458628650.8619466, + "desired_brightness_changed_at": 1458628381.8566465, + "firmware_date_code_changed_at": 1457521561.0603704, + "color_model_changed_at": 1457521797.6389458, + "hue_changed_at": 1457595786.5472758, + "saturation_changed_at": 1457595782.71269, + "color_temperature_changed_at": 1457521786.911106, + "firmware_version_changed_at": 1457521561.0603704, + "desired_color_model_changed_at": 1458620834.569744, + "desired_hue_changed_at": 1457595786.605935, + "desired_saturation_changed_at": 1457595782.8273423, + "desired_color_temperature_changed_at": 1457521921.4747667 + }, + "light_bulb_id": "1515274", + "name": "Bat Signal", + "locale": "en_us", + "units": { + }, + "created_at": 1457517586, + "hidden_at": null, + "capabilities": { + "fields": [ + { + "field": "connection", + "type": "boolean", + "mutability": "read-only" + }, + { + "field": "powered", + "type": "boolean", + "mutability": "read-write" + }, + { + "field": "brightness", + "type": "percentage", + "mutability": "read-write" + }, + { + "field": "color_model", + "type": "string", + "choices": [ + "temperature", + "hsb" + ] + }, + { + "field": "hue", + "type": "percentage", + "mutability": "read-write" + }, + { + "field": "saturation", + "type": "percentage", + "mutability": "read-write" + } + ], + "color_changeable": true + }, + "triggers": [], + "manufacturer_device_model": "sylvania_sylvania_rgbw", + "manufacturer_device_id": null, + "device_manufacturer": "sylvania", + "model_name": "Lightify RGBW Bulb", + "upc_id": "509", + "upc_code": "4613573703", + "gang_id": null, + "hub_id": "381678", + "local_id": "37", + "radio_type": "zigbee", + "linked_service_id": null, + "lat_lng": [ + null, + null + ], + "location": "", + "order": 0 + } + ] +} diff --git a/src/pywink/test/devices/standard/api_responses/rgb_present.json b/src/pywink/test/devices/standard/api_responses/rgb_present.json new file mode 100644 index 0000000..cc5f4e9 --- /dev/null +++ b/src/pywink/test/devices/standard/api_responses/rgb_present.json @@ -0,0 +1,119 @@ +{ + "data": [ + { + "uuid": "238539e2-1ad6-44ba-bc53-33c684c36e1d", + "desired_state": { + "powered": false, + "brightness": 1, + "color_model": "hsb", + "hue": 0.35, + "saturation": 1, + "color_temperature": 1901 + }, + "last_reading": { + "connection": true, + "connection_updated_at": 1458628651.862995, + "powered": false, + "powered_updated_at": 1458628651.862995, + "brightness": 1, + "brightness_updated_at": 1458628651.862995, + "color_model": "hsb", + "color_model_updated_at": 1458628651.862995, + "hue": 0.35, + "hue_updated_at": 1458628651.862995, + "saturation": 1, + "saturation_updated_at": 1458628651.862995, + "color_temperature": 1901, + "color_temperature_updated_at": 1458628651.862995, + "firmware_version": "0.1b02 / 0.3b22", + "firmware_version_updated_at": 1458628651.862995, + "firmware_date_code": "20150929N****", + "firmware_date_code_updated_at": 1458628651.862995, + "desired_powered_updated_at": 1458628650.8619466, + "desired_brightness_updated_at": 1458628820.0301423, + "desired_color_model_updated_at": 1458628820.0301423, + "desired_hue_updated_at": 1458628820.0301423, + "desired_saturation_updated_at": 1458628820.0301423, + "desired_color_temperature_updated_at": 1458628820.0301423, + "powered_changed_at": 1458628650.8134031, + "brightness_changed_at": 1458122238.7788615, + "connection_changed_at": 1457517588.4372394, + "desired_powered_changed_at": 1458628650.8619466, + "desired_brightness_changed_at": 1458628381.8566465, + "firmware_date_code_changed_at": 1457521561.0603704, + "color_model_changed_at": 1457521797.6389458, + "hue_changed_at": 1457595786.5472758, + "saturation_changed_at": 1457595782.71269, + "color_temperature_changed_at": 1457521786.911106, + "firmware_version_changed_at": 1457521561.0603704, + "desired_color_model_changed_at": 1458620834.569744, + "desired_hue_changed_at": 1457595786.605935, + "desired_saturation_changed_at": 1457595782.8273423, + "desired_color_temperature_changed_at": 1457521921.4747667 + }, + "light_bulb_id": "1515274", + "name": "Bat Signal", + "locale": "en_us", + "units": { + }, + "created_at": 1457517586, + "hidden_at": null, + "capabilities": { + "fields": [ + { + "field": "connection", + "type": "boolean", + "mutability": "read-only" + }, + { + "field": "powered", + "type": "boolean", + "mutability": "read-write" + }, + { + "field": "brightness", + "type": "percentage", + "mutability": "read-write" + }, + { + "field": "color_model", + "type": "string", + "choices": [ + "rgb", + "hsb" + ] + }, + { + "field": "hue", + "type": "percentage", + "mutability": "read-write" + }, + { + "field": "saturation", + "type": "percentage", + "mutability": "read-write" + } + ], + "color_changeable": true + }, + "triggers": [], + "manufacturer_device_model": "sylvania_sylvania_rgbw", + "manufacturer_device_id": null, + "device_manufacturer": "sylvania", + "model_name": "Lightify RGBW Bulb", + "upc_id": "509", + "upc_code": "4613573703", + "gang_id": null, + "hub_id": "381678", + "local_id": "37", + "radio_type": "zigbee", + "linked_service_id": null, + "lat_lng": [ + null, + null + ], + "location": "", + "order": 0 + } + ] +} diff --git a/src/pywink/test/devices/standard/api_responses/xy_absent.json b/src/pywink/test/devices/standard/api_responses/xy_absent.json new file mode 100644 index 0000000..cc5f4e9 --- /dev/null +++ b/src/pywink/test/devices/standard/api_responses/xy_absent.json @@ -0,0 +1,119 @@ +{ + "data": [ + { + "uuid": "238539e2-1ad6-44ba-bc53-33c684c36e1d", + "desired_state": { + "powered": false, + "brightness": 1, + "color_model": "hsb", + "hue": 0.35, + "saturation": 1, + "color_temperature": 1901 + }, + "last_reading": { + "connection": true, + "connection_updated_at": 1458628651.862995, + "powered": false, + "powered_updated_at": 1458628651.862995, + "brightness": 1, + "brightness_updated_at": 1458628651.862995, + "color_model": "hsb", + "color_model_updated_at": 1458628651.862995, + "hue": 0.35, + "hue_updated_at": 1458628651.862995, + "saturation": 1, + "saturation_updated_at": 1458628651.862995, + "color_temperature": 1901, + "color_temperature_updated_at": 1458628651.862995, + "firmware_version": "0.1b02 / 0.3b22", + "firmware_version_updated_at": 1458628651.862995, + "firmware_date_code": "20150929N****", + "firmware_date_code_updated_at": 1458628651.862995, + "desired_powered_updated_at": 1458628650.8619466, + "desired_brightness_updated_at": 1458628820.0301423, + "desired_color_model_updated_at": 1458628820.0301423, + "desired_hue_updated_at": 1458628820.0301423, + "desired_saturation_updated_at": 1458628820.0301423, + "desired_color_temperature_updated_at": 1458628820.0301423, + "powered_changed_at": 1458628650.8134031, + "brightness_changed_at": 1458122238.7788615, + "connection_changed_at": 1457517588.4372394, + "desired_powered_changed_at": 1458628650.8619466, + "desired_brightness_changed_at": 1458628381.8566465, + "firmware_date_code_changed_at": 1457521561.0603704, + "color_model_changed_at": 1457521797.6389458, + "hue_changed_at": 1457595786.5472758, + "saturation_changed_at": 1457595782.71269, + "color_temperature_changed_at": 1457521786.911106, + "firmware_version_changed_at": 1457521561.0603704, + "desired_color_model_changed_at": 1458620834.569744, + "desired_hue_changed_at": 1457595786.605935, + "desired_saturation_changed_at": 1457595782.8273423, + "desired_color_temperature_changed_at": 1457521921.4747667 + }, + "light_bulb_id": "1515274", + "name": "Bat Signal", + "locale": "en_us", + "units": { + }, + "created_at": 1457517586, + "hidden_at": null, + "capabilities": { + "fields": [ + { + "field": "connection", + "type": "boolean", + "mutability": "read-only" + }, + { + "field": "powered", + "type": "boolean", + "mutability": "read-write" + }, + { + "field": "brightness", + "type": "percentage", + "mutability": "read-write" + }, + { + "field": "color_model", + "type": "string", + "choices": [ + "rgb", + "hsb" + ] + }, + { + "field": "hue", + "type": "percentage", + "mutability": "read-write" + }, + { + "field": "saturation", + "type": "percentage", + "mutability": "read-write" + } + ], + "color_changeable": true + }, + "triggers": [], + "manufacturer_device_model": "sylvania_sylvania_rgbw", + "manufacturer_device_id": null, + "device_manufacturer": "sylvania", + "model_name": "Lightify RGBW Bulb", + "upc_id": "509", + "upc_code": "4613573703", + "gang_id": null, + "hub_id": "381678", + "local_id": "37", + "radio_type": "zigbee", + "linked_service_id": null, + "lat_lng": [ + null, + null + ], + "location": "", + "order": 0 + } + ] +} diff --git a/src/pywink/test/devices/standard/api_responses/xy_present.json b/src/pywink/test/devices/standard/api_responses/xy_present.json new file mode 100644 index 0000000..9169267 --- /dev/null +++ b/src/pywink/test/devices/standard/api_responses/xy_present.json @@ -0,0 +1,119 @@ +{ + "data": [ + { + "uuid": "238539e2-1ad6-44ba-bc53-33c684c36e1d", + "desired_state": { + "powered": false, + "brightness": 1, + "color_model": "hsb", + "hue": 0.35, + "saturation": 1, + "color_temperature": 1901 + }, + "last_reading": { + "connection": true, + "connection_updated_at": 1458628651.862995, + "powered": false, + "powered_updated_at": 1458628651.862995, + "brightness": 1, + "brightness_updated_at": 1458628651.862995, + "color_model": "hsb", + "color_model_updated_at": 1458628651.862995, + "hue": 0.35, + "hue_updated_at": 1458628651.862995, + "saturation": 1, + "saturation_updated_at": 1458628651.862995, + "color_temperature": 1901, + "color_temperature_updated_at": 1458628651.862995, + "firmware_version": "0.1b02 / 0.3b22", + "firmware_version_updated_at": 1458628651.862995, + "firmware_date_code": "20150929N****", + "firmware_date_code_updated_at": 1458628651.862995, + "desired_powered_updated_at": 1458628650.8619466, + "desired_brightness_updated_at": 1458628820.0301423, + "desired_color_model_updated_at": 1458628820.0301423, + "desired_hue_updated_at": 1458628820.0301423, + "desired_saturation_updated_at": 1458628820.0301423, + "desired_color_temperature_updated_at": 1458628820.0301423, + "powered_changed_at": 1458628650.8134031, + "brightness_changed_at": 1458122238.7788615, + "connection_changed_at": 1457517588.4372394, + "desired_powered_changed_at": 1458628650.8619466, + "desired_brightness_changed_at": 1458628381.8566465, + "firmware_date_code_changed_at": 1457521561.0603704, + "color_model_changed_at": 1457521797.6389458, + "hue_changed_at": 1457595786.5472758, + "saturation_changed_at": 1457595782.71269, + "color_temperature_changed_at": 1457521786.911106, + "firmware_version_changed_at": 1457521561.0603704, + "desired_color_model_changed_at": 1458620834.569744, + "desired_hue_changed_at": 1457595786.605935, + "desired_saturation_changed_at": 1457595782.8273423, + "desired_color_temperature_changed_at": 1457521921.4747667 + }, + "light_bulb_id": "1515274", + "name": "Bat Signal", + "locale": "en_us", + "units": { + }, + "created_at": 1457517586, + "hidden_at": null, + "capabilities": { + "fields": [ + { + "field": "connection", + "type": "boolean", + "mutability": "read-only" + }, + { + "field": "powered", + "type": "boolean", + "mutability": "read-write" + }, + { + "field": "brightness", + "type": "percentage", + "mutability": "read-write" + }, + { + "field": "color_model", + "type": "string", + "choices": [ + "xy", + "hsb" + ] + }, + { + "field": "hue", + "type": "percentage", + "mutability": "read-write" + }, + { + "field": "saturation", + "type": "percentage", + "mutability": "read-write" + } + ], + "color_changeable": true + }, + "triggers": [], + "manufacturer_device_model": "sylvania_sylvania_rgbw", + "manufacturer_device_id": null, + "device_manufacturer": "sylvania", + "model_name": "Lightify RGBW Bulb", + "upc_id": "509", + "upc_code": "4613573703", + "gang_id": null, + "hub_id": "381678", + "local_id": "37", + "radio_type": "zigbee", + "linked_service_id": null, + "lat_lng": [ + null, + null + ], + "location": "", + "order": 0 + } + ] +} diff --git a/src/pywink/test/devices/standard/bulb_test.py b/src/pywink/test/devices/standard/bulb_test.py index 6390531..5bb85f1 100644 --- a/src/pywink/test/devices/standard/bulb_test.py +++ b/src/pywink/test/devices/standard/bulb_test.py @@ -61,6 +61,55 @@ def test_should_be_false_if_response_does_not_contain_temperature_capabilities(s msg="Expected temperature to be un-supported") +class BulbSupportsXYTest(unittest.TestCase): + + def test_should_be_false_if_response_does_not_contain_xy_capabilities(self): + with open('{}/api_responses/xy_absent.json'.format(os.path.dirname(__file__))) as light_file: + response_dict = json.load(light_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.LIGHT_BULB]) + + bulb = devices[0] + """ :type bulb: pywink.devices.standard.WinkBulb """ + supports_xy = bulb.supports_xy_color() + self.assertFalse(supports_xy, + msg="Expected xy to be un-supported") + + def test_should_be_true_if_response_contains_xy_capabilities(self): + with open('{}/api_responses/xy_present.json'.format(os.path.dirname(__file__))) as light_file: + response_dict = json.load(light_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.LIGHT_BULB]) + + bulb = devices[0] + """ :type bulb: pywink.devices.standard.WinkBulb """ + supports_xy = bulb.supports_xy_color() + self.assertTrue(supports_xy, + msg="Expected xy to be supported") + + +class BulbSupportsRGBTest(unittest.TestCase): + + def test_should_be_false_if_response_does_not_contain_rgb_capabilities(self): + with open('{}/api_responses/rgb_absent.json'.format(os.path.dirname(__file__))) as light_file: + response_dict = json.load(light_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.LIGHT_BULB]) + + bulb = devices[0] + """ :type bulb: pywink.devices.standard.WinkBulb """ + supports_rgb = bulb.supports_rgb() + self.assertFalse(supports_rgb, + msg="Expected rgb to be un-supported") + + def test_should_be_true_if_response_contains_rgb_capabilities(self): + with open('{}/api_responses/rgb_present.json'.format(os.path.dirname(__file__))) as light_file: + response_dict = json.load(light_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.LIGHT_BULB]) + + bulb = devices[0] + """ :type bulb: pywink.devices.standard.WinkBulb """ + supports_rgb = bulb.supports_rgb() + self.assertTrue(supports_rgb, + msg="Expected rgb to be supported") + class SetStateTests(unittest.TestCase): def setUp(self): From 9e70322fbb3a876ad68ca6218e9c11ddb495b280 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Sun, 21 Aug 2016 12:02:13 -0400 Subject: [PATCH 115/178] Changed False to [] --- src/pywink/devices/standard/bulb.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pywink/devices/standard/bulb.py b/src/pywink/devices/standard/bulb.py index 929c630..2553483 100644 --- a/src/pywink/devices/standard/bulb.py +++ b/src/pywink/devices/standard/bulb.py @@ -124,7 +124,7 @@ def _format_color_data(self, color_hue_saturation, color_kelvin, color_xy): def supports_rgb(self): capabilities = self.json_state.get('capabilities', {}) - cap_fields = capabilities.get('fields', False) + cap_fields = capabilities.get('fields', []) if not cap_fields: return False for field in cap_fields: @@ -137,7 +137,7 @@ def supports_rgb(self): def supports_hue_saturation(self): capabilities = self.json_state.get('capabilities', {}) - cap_fields = capabilities.get('fields', False) + cap_fields = capabilities.get('fields', []) if not cap_fields: return False for field in cap_fields: @@ -150,7 +150,7 @@ def supports_hue_saturation(self): def supports_xy_color(self): capabilities = self.json_state.get('capabilities', {}) - cap_fields = capabilities.get('fields', False) + cap_fields = capabilities.get('fields', []) if not cap_fields: return False for field in cap_fields: @@ -163,7 +163,7 @@ def supports_xy_color(self): def supports_temperature(self): capabilities = self.json_state.get('capabilities', {}) - cap_fields = capabilities.get('fields', False) + cap_fields = capabilities.get('fields', []) if not cap_fields: return False for field in cap_fields: From 7a2f0ddb594ae15b95c82a8857aa68591b041003 Mon Sep 17 00:00:00 2001 From: bradsk88 <7kcwKg60tdcW> Date: Sun, 21 Aug 2016 14:48:10 -0600 Subject: [PATCH 116/178] Removing unnecessary logic branch --- src/pywink/devices/standard/bulb.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/pywink/devices/standard/bulb.py b/src/pywink/devices/standard/bulb.py index 2553483..d10a58e 100644 --- a/src/pywink/devices/standard/bulb.py +++ b/src/pywink/devices/standard/bulb.py @@ -125,8 +125,6 @@ def _format_color_data(self, color_hue_saturation, color_kelvin, color_xy): def supports_rgb(self): capabilities = self.json_state.get('capabilities', {}) cap_fields = capabilities.get('fields', []) - if not cap_fields: - return False for field in cap_fields: _field = field.get('field') if _field == 'color_model': @@ -138,8 +136,6 @@ def supports_rgb(self): def supports_hue_saturation(self): capabilities = self.json_state.get('capabilities', {}) cap_fields = capabilities.get('fields', []) - if not cap_fields: - return False for field in cap_fields: _field = field.get('field') if _field == 'color_model': @@ -151,8 +147,6 @@ def supports_hue_saturation(self): def supports_xy_color(self): capabilities = self.json_state.get('capabilities', {}) cap_fields = capabilities.get('fields', []) - if not cap_fields: - return False for field in cap_fields: _field = field.get('field') if _field == 'color_model': @@ -164,8 +158,6 @@ def supports_xy_color(self): def supports_temperature(self): capabilities = self.json_state.get('capabilities', {}) cap_fields = capabilities.get('fields', []) - if not cap_fields: - return False for field in cap_fields: _field = field.get('field') if _field == 'color_model': From f81fd865805959dad80856cf079b2c1f211c5d86 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Tue, 6 Sep 2016 16:15:46 -0400 Subject: [PATCH 117/178] Return False for supports_rgb if hsb is supported --- CHANGELOG.md | 3 +++ src/pywink/devices/standard/bulb.py | 5 ++++- src/pywink/test/devices/standard/bulb_test.py | 4 ++-- src/setup.py | 2 +- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97353cc..ffa4f3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 0.7.14 +- Return False on RGB support if HSB is also supported. + ## 0.7.13 - Changed method of detecting WinkBulb capabilities diff --git a/src/pywink/devices/standard/bulb.py b/src/pywink/devices/standard/bulb.py index d10a58e..02c7281 100644 --- a/src/pywink/devices/standard/bulb.py +++ b/src/pywink/devices/standard/bulb.py @@ -123,12 +123,15 @@ def _format_color_data(self, color_hue_saturation, color_kelvin, color_xy): return {} def supports_rgb(self): + # TODO: Find out if any bulbs actually support RGB capabilities = self.json_state.get('capabilities', {}) cap_fields = capabilities.get('fields', []) for field in cap_fields: _field = field.get('field') if _field == 'color_model': choices = field.get('choices') + if "hsb" in choices: + return False if "rgb" in choices: return True return False @@ -199,7 +202,7 @@ def _format_xy(xy): def _get_color_as_rgb(hue_sat, kelvin, xy): if hue_sat is not None: h, s, v = colorsys.hsv_to_rgb(hue_sat[0], hue_sat[1], 1) - return tuple(h, s, v) + return h, s, v if kelvin is not None: return color_temperature_to_rgb(kelvin) if xy is not None: diff --git a/src/pywink/test/devices/standard/bulb_test.py b/src/pywink/test/devices/standard/bulb_test.py index 5bb85f1..9db7bb2 100644 --- a/src/pywink/test/devices/standard/bulb_test.py +++ b/src/pywink/test/devices/standard/bulb_test.py @@ -99,7 +99,7 @@ def test_should_be_false_if_response_does_not_contain_rgb_capabilities(self): self.assertFalse(supports_rgb, msg="Expected rgb to be un-supported") - def test_should_be_true_if_response_contains_rgb_capabilities(self): + def test_should_be_false_if_response_contains_rgb_capabilities_and_hsb_capabilities(self): with open('{}/api_responses/rgb_present.json'.format(os.path.dirname(__file__))) as light_file: response_dict = json.load(light_file) devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.LIGHT_BULB]) @@ -107,7 +107,7 @@ def test_should_be_true_if_response_contains_rgb_capabilities(self): bulb = devices[0] """ :type bulb: pywink.devices.standard.WinkBulb """ supports_rgb = bulb.supports_rgb() - self.assertTrue(supports_rgb, + self.assertFalse(supports_rgb, msg="Expected rgb to be supported") class SetStateTests(unittest.TestCase): diff --git a/src/setup.py b/src/setup.py index 8590b4a..eb4a647 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='0.7.13', + version='0.7.14', description='Access Wink devices via the Wink API', url='http://github.com/bradsk88/python-wink', author='Brad Johnson', From 08e0875d5ef58f8a9a163823f0f966cbbf78b224 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Wed, 14 Sep 2016 19:37:12 -0400 Subject: [PATCH 118/178] Fix for motion sensors --- CHANGELOG.md | 3 + src/pywink/api.py | 7 +- src/pywink/devices/sensors.py | 20 +- .../motion_sensor_gocontrol.json | 186 +++++++++--------- src/pywink/test/devices/standard/init_test.py | 11 +- src/setup.py | 2 +- 6 files changed, 132 insertions(+), 97 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffa4f3e..31fef7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 0.7.15 +- Fix for PIR multisensors + ## 0.7.14 - Return False on RGB support if HSB is also supported. diff --git a/src/pywink/api.py b/src/pywink/api.py index 79d3f0e..4569599 100644 --- a/src/pywink/api.py +++ b/src/pywink/api.py @@ -7,7 +7,7 @@ from pywink.devices.standard import WinkPorkfolioNose from pywink.devices.sensors import WinkSensorPod, WinkHumiditySensor, WinkBrightnessSensor, WinkSoundPresenceSensor, \ WinkTemperatureSensor, WinkVibrationPresenceSensor, \ - WinkLiquidPresenceSensor, WinkCurrencySensor + WinkLiquidPresenceSensor, WinkCurrencySensor, WinkMotionSensor from pywink.devices.types import DEVICE_ID_KEYS API_HEADERS = {} @@ -174,6 +174,7 @@ def get_devices_from_response_dict(response_dict, filter_key): subsensors = _get_subsensors_from_sensor_pod(item, api_interface) if subsensors: devices.extend(subsensors) + continue # Don't capture the base device if len(subsensors) == 1: continue @@ -189,6 +190,7 @@ def get_devices_from_response_dict(response_dict, filter_key): def _get_subsensors_from_sensor_pod(item, api_interface): capabilities = [cap['field'] for cap in item.get('capabilities', {}).get('fields', [])] + if not capabilities: return [] @@ -212,6 +214,9 @@ def _get_subsensors_from_sensor_pod(item, api_interface): if WinkLiquidPresenceSensor.CAPABILITY in capabilities: subsensors.append(WinkLiquidPresenceSensor(item, api_interface)) + if WinkMotionSensor.CAPABILITY in capabilities: + subsensors.append(WinkMotionSensor(item, api_interface)) + if WinkSensorPod.CAPABILITY in capabilities: subsensors.append(WinkSensorPod(item, api_interface)) diff --git a/src/pywink/devices/sensors.py b/src/pywink/devices/sensors.py index 3a8b895..b4c6a14 100644 --- a/src/pywink/devices/sensors.py +++ b/src/pywink/devices/sensors.py @@ -70,8 +70,6 @@ def __repr__(self): 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): @@ -193,6 +191,24 @@ def liquid_boolean(self): return self.last_reading() +class WinkMotionSensor(_WinkCapabilitySensor): + + CAPABILITY = 'motion' + UNIT = None + + def __init__(self, device_state_as_json, api_interface): + super(WinkMotionSensor, self).__init__(device_state_as_json, api_interface, + self.CAPABILITY, + self.UNIT) + + def motion_boolean(self): + """ + :return: Returns True if motion is detected. + :rtype: bool + """ + return self.last_reading() + + class WinkCurrencySensor(_WinkCapabilitySensor): CAPABILITY = 'balance' diff --git a/src/pywink/test/devices/standard/api_responses/motion_sensor_gocontrol.json b/src/pywink/test/devices/standard/api_responses/motion_sensor_gocontrol.json index 4e6d026..6824d5f 100644 --- a/src/pywink/test/devices/standard/api_responses/motion_sensor_gocontrol.json +++ b/src/pywink/test/devices/standard/api_responses/motion_sensor_gocontrol.json @@ -1,89 +1,99 @@ -{ - "data": [ - { - "last_event": { - "brightness_occurred_at": null, - "loudness_occurred_at": null, - "vibration_occurred_at": null - }, - "object_type": "sensor_pod", - "object_id": "192431", - "uuid": "REMOVED", - "icon_id": null, - "icon_code": null, - "desired_state": {}, - "last_reading": { - "motion": true, - "motion_updated_at": 1465262763.1264362, - "battery": 1, - "battery_updated_at": 1465262763.1264362, - "tamper_detected": null, - "tamper_detected_updated_at": 1462063497.4478416, - "motion_true": "N/A", - "motion_true_updated_at": null, - "tamper_detected_true": null, - "tamper_detected_true_updated_at": null, - "connection": true, - "connection_updated_at": 1465262763.1264362, - "agent_session_id": null, - "agent_session_id_updated_at": null, - "connection_changed_at": 1462043954.4886937, - "battery_changed_at": 1462043956.216641, - "motion_changed_at": 1465262763.1264362, - "motion_true_changed_at": 1465262763.1264362 - }, - "subscription": { - "pubnub": { - "subscribe_key": "REMOVED", - "channel": "REMOVED" - } - }, - "sensor_pod_id": "192431", - "name": "Brad's Room Motion", - "locale": "en_us", - "units": {}, - "created_at": 1462043954, - "hidden_at": null, - "capabilities": { - "fields": [ - { - "type": "boolean", - "field": "motion", - "mutability": "read-only" - }, - { - "type": "percentage", - "field": "battery", - "mutability": "read-only" - }, - { - "type": "boolean", - "field": "tamper_detected", - "mutability": "read-only" - } - ] - }, - "triggers": [], - "manufacturer_device_model": "linear_wapirz_1", - "manufacturer_device_id": null, - "device_manufacturer": "linear", - "model_name": "Z-Wave Passive Infrared (PIR) Sensor", - "upc_id": "207", - "upc_code": "093863125102", - "gang_id": null, - "hub_id": "300039", - "local_id": "9", - "radio_type": "zwave", - "linked_service_id": null, - "lat_lng": [ - 0, - 0 - ], - "location": "" - } - ], - "errors": [], - "pagination": { - "count": 13 - } +{ + "data":[ + { + "last_event":{ + "brightness_occurred_at":null, + "loudness_occurred_at":null, + "vibration_occurred_at":null + }, + "uuid":"4c93936f-840e-40fc1232184-ee3583408d71", + "desired_state":{ + + }, + "last_reading":{ + "motion":false, + "motion_updated_at":1473870334.9673228, + "battery":1, + "battery_updated_at":1473870334.9673228, + "tamper_detected":null, + "tamper_detected_updated_at":null, + "temperature":21.666666666666668, + "temperature_updated_at":1473870334.9673228, + "motion_true":"N/A", + "motion_true_updated_at":1473870129.6595004, + "tamper_detected_true":null, + "tamper_detected_true_updated_at":null, + "connection":true, + "connection_updated_at":1473870334.9673228, + "agent_session_id":null, + "agent_session_id_updated_at":null, + "motion_changed_at":1473870334.9673228, + "motion_true_changed_at":1473870129.6595004, + "temperature_changed_at":1473862426.2809358 + }, + "subscription":{ + "pubnub":{ + "subscribe_key":"sub-c-f7-11e3-a5e8-02ee2ddab7fe", + "channel":"b60201ecf033b6e7181|sensor_pod-152619|user-377857" + } + }, + "sensor_pod_id":"151234", + "name":"Living room", + "locale":"en_us", + "units":{ + + }, + "created_at":1452810755, + "hidden_at":null, + "capabilities":{ + "fields":[ + { + "type":"boolean", + "field":"motion", + "mutability":"read-only" + }, + { + "type":"percentage", + "field":"battery", + "mutability":"read-only" + }, + { + "type":"boolean", + "field":"tamper_detected", + "mutability":"read-only" + }, + { + "type":"float", + "field":"temperature", + "mutability":"read-only" + } + ] + }, + "triggers":[ + + ], + "manufacturer_device_model":"linear_wapirz_1", + "manufacturer_device_id":null, + "device_manufacturer":"linear", + "model_name":"Z-Wave Passive Infrared (PIR) Sensor", + "upc_id":"207", + "upc_code":"093863125102", + "gang_id":null, + "hub_id":"302528", + "local_id":"6", + "radio_type":"zwave", + "linked_service_id":null, + "lat_lng":[ + 12.345678, + -89.765432 + ], + "location":"" + } + ], + "errors":[ + + ], + "pagination":{ + "count":13 + } } diff --git a/src/pywink/test/devices/standard/init_test.py b/src/pywink/test/devices/standard/init_test.py index e5b68af..4c42fe5 100644 --- a/src/pywink/test/devices/standard/init_test.py +++ b/src/pywink/test/devices/standard/init_test.py @@ -7,7 +7,7 @@ from pywink.devices import types as device_types from pywink.devices.sensors import WinkSensorPod, WinkBrightnessSensor, WinkHumiditySensor, \ WinkSoundPresenceSensor, WinkVibrationPresenceSensor, WinkTemperatureSensor, \ - _WinkCapabilitySensor, WinkLiquidPresenceSensor, WinkCurrencySensor + _WinkCapabilitySensor, WinkLiquidPresenceSensor, WinkCurrencySensor, WinkMotionSensor from pywink.devices.standard import WinkGarageDoor, WinkPowerStripOutlet, WinkSiren, WinkLock, \ WinkShade, WinkBinarySwitch, WinkEggTray, WinkKey, WinkPorkfolioNose from pywink.devices.types import DEVICE_ID_KEYS @@ -217,14 +217,14 @@ def test_quirky_spotter_api_response_should_create_unique_one_primary_sensor_and response_dict = json.load(spotter_file) sensors = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SENSOR_POD]) - self.assertEquals(1 + 5, len(sensors)) + self.assertEquals(5, len(sensors)) def test_alternative_quirky_spotter_api_response_should_create_one_primary_sensor_and_five_subsensors(self): with open('{}/api_responses/quirky_spotter_2.json'.format(os.path.dirname(__file__))) as spotter_file: response_dict = json.load(spotter_file) sensors = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SENSOR_POD]) - self.assertEquals(1 + 5, len(sensors)) + self.assertEquals(5, len(sensors)) def test_brightness_should_have_correct_value(self): with open('{}/api_responses/quirky_spotter.json'.format(os.path.dirname(__file__))) as spotter_file: @@ -317,8 +317,9 @@ def test_gocontrol_motion_sensor_should_be_identified(self): devices = get_devices_from_response_dict(response, DEVICE_ID_KEYS[ device_types.SENSOR_POD]) - self.assertEqual(1, len(devices)) - self.assertIsInstance(devices[0], WinkSensorPod) + self.assertEqual(2, len(devices)) + self.assertIsInstance(devices[1], WinkMotionSensor) + self.assertIsInstance(devices[0], WinkTemperatureSensor) def test_humidity_is_percentage_after_update(self): with open('{}/api_responses/quirky_spotter.json'.format(os.path.dirname(__file__))) as spotter_file: diff --git a/src/setup.py b/src/setup.py index eb4a647..db985fa 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='0.7.14', + version='0.7.15', description='Access Wink devices via the Wink API', url='http://github.com/bradsk88/python-wink', author='Brad Johnson', From 3b62ff53f245b3ff1d3efe7ec5422e6bc979878c Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Mon, 19 Sep 2016 11:15:53 -0400 Subject: [PATCH 119/178] Support for Wink relay sensors --- src/pywink/api.py | 14 ++- src/pywink/devices/sensors.py | 36 ++++++ .../api_responses/wink_relay_sensor.json | 107 ++++++++++++++++++ 3 files changed, 155 insertions(+), 2 deletions(-) create mode 100644 src/pywink/test/devices/standard/api_responses/wink_relay_sensor.json diff --git a/src/pywink/api.py b/src/pywink/api.py index 4569599..dc2268c 100644 --- a/src/pywink/api.py +++ b/src/pywink/api.py @@ -7,7 +7,8 @@ from pywink.devices.standard import WinkPorkfolioNose from pywink.devices.sensors import WinkSensorPod, WinkHumiditySensor, WinkBrightnessSensor, WinkSoundPresenceSensor, \ WinkTemperatureSensor, WinkVibrationPresenceSensor, \ - WinkLiquidPresenceSensor, WinkCurrencySensor, WinkMotionSensor + WinkLiquidPresenceSensor, WinkCurrencySensor, WinkMotionSensor, \ + WinkPresenceSensor, WinkProximitySensor from pywink.devices.types import DEVICE_ID_KEYS API_HEADERS = {} @@ -44,13 +45,15 @@ def get_device_state(self, device, id_override=None): return arequest.json() -def set_bearer_token(token): +def set_bearer_token(token, user_agent=None): global API_HEADERS API_HEADERS = { "Content-Type": "application/json", "Authorization": "Bearer {}".format(token) } + if user_agent: + API_HEADERS["User-Agent"] = user_agent def set_wink_credentials(client_id, client_secret, refresh_token): @@ -190,6 +193,7 @@ def get_devices_from_response_dict(response_dict, filter_key): def _get_subsensors_from_sensor_pod(item, api_interface): capabilities = [cap['field'] for cap in item.get('capabilities', {}).get('fields', [])] + capabilities.extend([cap['field'] for cap in item.get('capabilities', {}).get('sensor_types', [])]) if not capabilities: return [] @@ -217,6 +221,12 @@ def _get_subsensors_from_sensor_pod(item, api_interface): if WinkMotionSensor.CAPABILITY in capabilities: subsensors.append(WinkMotionSensor(item, api_interface)) + if WinkPresenceSensor.CAPABILITY in capabilities: + subsensors.append(WinkPresenceSensor(item, api_interface)) + + if WinkProximitySensor.CAPABILITY in capabilities: + subsensors.append(WinkProximitySensor(item, api_interface)) + if WinkSensorPod.CAPABILITY in capabilities: subsensors.append(WinkSensorPod(item, api_interface)) diff --git a/src/pywink/devices/sensors.py b/src/pywink/devices/sensors.py index b4c6a14..4d4fe58 100644 --- a/src/pywink/devices/sensors.py +++ b/src/pywink/devices/sensors.py @@ -209,6 +209,42 @@ def motion_boolean(self): return self.last_reading() +class WinkPresenceSensor(_WinkCapabilitySensor): + + CAPABILITY = 'presence' + UNIT = None + + def __init__(self, device_state_as_json, api_interface): + super(WinkPresenceSensor, self).__init__(device_state_as_json, api_interface, + self.CAPABILITY, + self.UNIT) + + def presence_boolean(self): + """ + :return: Returns True if presence is detected. + :rtype: bool + """ + return self.last_reading() + + +class WinkProximitySensor(_WinkCapabilitySensor): + + CAPABILITY = 'proximity' + UNIT = None + + def __init__(self, device_state_as_json, api_interface): + super(WinkProximitySensor, self).__init__(device_state_as_json, api_interface, + self.CAPABILITY, + self.UNIT) + + def proximity_float(self): + """ + :return: A float indicating the proximity. + :rtype: float + """ + return self.last_reading() + + class WinkCurrencySensor(_WinkCapabilitySensor): CAPABILITY = 'balance' diff --git a/src/pywink/test/devices/standard/api_responses/wink_relay_sensor.json b/src/pywink/test/devices/standard/api_responses/wink_relay_sensor.json new file mode 100644 index 0000000..637d982 --- /dev/null +++ b/src/pywink/test/devices/standard/api_responses/wink_relay_sensor.json @@ -0,0 +1,107 @@ +{ + "data":[ + { + "hidden_at":null, + "manufacturer_device_model":"wink_relay_sensor", + "object_id":"231234", + "radio_type":"project_one", + "manufacturer_device_id":null, + "locale":"en_us", + "upc_id":"188", + "location":"", + "gang_id":"40123", + "desired_state":{ + + }, + "subscription":{ + "pubnub":{ + "subscribe_key":"sub-c-f7bf7f7e-0542-11e323456-12142ddab7fe", + "channel":"a82816f0ddb9fa5448c5cb471d21716564871164|sensor_pod-123787|user-220271" + } + }, + "created_at":1468460695, + "lat_lng":[ + null, + null + ], + "triggers":[ + + ], + "units":{ + + }, + "model_name":"Wink Relay Sensor", + "name":"Great Room Relay", + "device_manufacturer":"wink", + "icon_id":null, + "last_event":{ + "vibration_occurred_at":null, + "loudness_occurred_at":null, + "brightness_occurred_at":null + }, + "local_id":"3", + "capabilities":{ + "desired_state_fields":[ + + ], + "sensor_types":[ + { + "mutability":"read-only", + "field":"temperature", + "attribute_id":1, + "type":"float" + }, + { + "mutability":"read-only", + "field":"humidity", + "attribute_id":2, + "type":"percentage" + }, + { + "mutability":"read-only", + "field":"presence", + "attribute_id":3, + "type":"boolean" + }, + { + "mutability":"read-only", + "field":"proximity", + "attribute_id":4, + "type":"float" + } + ] + }, + "sensor_pod_id":"212345", + "last_reading":{ + "temperature_updated_at":1474295085.8156993, + "agent_session_id_updated_at":null, + "presence":false, + "agent_session_id":null, + "temperature":19.87, + "humidity_changed_at":1474295085.8156993, + "proximity":2512.0, + "presence_updated_at":1474295085.8156993, + "proximity_updated_at":1474295085.8156993, + "proximity_changed_at":1474295085.8156993, + "presence_changed_at":1474281889.6097996, + "connection":true, + "humidity":0.69, + "connection_updated_at":1474295085.8156993, + "temperature_changed_at":1474295085.8156993, + "humidity_updated_at":1474295085.8156993 + }, + "uuid":"9bb49bfe-0d6a-4e9b-9896-f33c37ec969b", + "icon_code":null, + "object_type":"sensor_pod", + "upc_code":"wink_p1_sensor", + "linked_service_id":null, + "hub_id":"451234" + } + ], + "errors":[ + + ], + "pagination":{ + "count":13 + } +} From b0a247b4ec5eac2b6d1a60f0d983a7f7a5d1077a Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Mon, 19 Sep 2016 11:38:20 -0400 Subject: [PATCH 120/178] Added tests --- src/pywink/test/devices/standard/init_test.py | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/pywink/test/devices/standard/init_test.py b/src/pywink/test/devices/standard/init_test.py index 4c42fe5..a0daab1 100644 --- a/src/pywink/test/devices/standard/init_test.py +++ b/src/pywink/test/devices/standard/init_test.py @@ -3,11 +3,12 @@ import unittest import os -from pywink.api import get_devices_from_response_dict +from pywink.api import get_devices_from_response_dict, set_bearer_token from pywink.devices import types as device_types from pywink.devices.sensors import WinkSensorPod, WinkBrightnessSensor, WinkHumiditySensor, \ WinkSoundPresenceSensor, WinkVibrationPresenceSensor, WinkTemperatureSensor, \ - _WinkCapabilitySensor, WinkLiquidPresenceSensor, WinkCurrencySensor, WinkMotionSensor + _WinkCapabilitySensor, WinkLiquidPresenceSensor, WinkCurrencySensor, WinkMotionSensor, \ + WinkProximitySensor, WinkPresenceSensor from pywink.devices.standard import WinkGarageDoor, WinkPowerStripOutlet, WinkSiren, WinkLock, \ WinkShade, WinkBinarySwitch, WinkEggTray, WinkKey, WinkPorkfolioNose from pywink.devices.types import DEVICE_ID_KEYS @@ -441,3 +442,20 @@ def test_device_id_should_be_number(self): device_id = devices[1].device_id() self.assertRegex(device_id, "^[0-9]{4,6}") + +class RelaySensorTests(unittest.TestCase): + + def setUp(self): + super(RelaySensorTests, self).setUp() + self.api_interface = mock.MagicMock() + + def test_should_handle_relay_response(self): + with open('{}/api_responses/wink_relay_sensor.json'.format(os.path.dirname(__file__))) as relay_file: + response_dict = json.load(relay_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SENSOR_POD]) + self.assertEqual(4, len(devices)) + self.assertIsInstance(devices[0], WinkHumiditySensor) + self.assertIsInstance(devices[1], WinkTemperatureSensor) + self.assertIsInstance(devices[2], WinkPresenceSensor) + self.assertIsInstance(devices[3], WinkProximitySensor) + From d8d34d3e0f40ee6fbb32da31f2545ae5c41076e8 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Mon, 19 Sep 2016 15:45:03 -0400 Subject: [PATCH 121/178] Automatic access token refreshing --- src/pywink/__init__.py | 8 +++--- src/pywink/api.py | 60 +++++++++++++++++++++++++++++++++++++----- 2 files changed, 57 insertions(+), 11 deletions(-) diff --git a/src/pywink/__init__.py b/src/pywink/__init__.py index e142ffb..455b8d0 100644 --- a/src/pywink/__init__.py +++ b/src/pywink/__init__.py @@ -2,7 +2,7 @@ Top level functions """ # noqa -from pywink.api import set_bearer_token, set_wink_credentials, get_bulbs, \ - get_eggtrays, get_garage_doors, get_locks, get_powerstrip_outlets, \ - get_sensors, get_shades, get_sirens, get_switches, get_devices, \ - is_token_set, get_subscription_key, get_keys, get_piggy_banks +from pywink.api import set_bearer_token, refresh_access_token, set_wink_credentials, \ + get_bulbs, get_eggtrays, get_garage_doors, get_locks, \ + get_powerstrip_outlets, get_sensors, get_shades, get_sirens, get_switches, \ + get_devices, is_token_set, get_subscription_key, get_keys, get_piggy_banks diff --git a/src/pywink/api.py b/src/pywink/api.py index dc2268c..d42c0cc 100644 --- a/src/pywink/api.py +++ b/src/pywink/api.py @@ -12,6 +12,10 @@ from pywink.devices.types import DEVICE_ID_KEYS API_HEADERS = {} +CLIENT_ID = None +CLIENT_SECRET = None +REFRESH_TOKEN = None +USER_AGENT = None class WinkApiInterface(object): @@ -32,6 +36,14 @@ def set_device_state(self, device, state, id_override=None): arequest = requests.put(url_string, data=json.dumps(state), headers=API_HEADERS) + if arequest.status == 401: + new_token = refresh_access_token() + if new_token: + arequest = requests.put(url_string, + data=json.dumps(state), + headers=API_HEADERS) + else: + raise WinkAPIException("Failed to refresh access token.") return arequest.json() def get_device_state(self, device, id_override=None): @@ -45,23 +57,35 @@ def get_device_state(self, device, id_override=None): return arequest.json() -def set_bearer_token(token, user_agent=None): +def set_bearer_token(token): global API_HEADERS API_HEADERS = { "Content-Type": "application/json", "Authorization": "Bearer {}".format(token) } - if user_agent: - API_HEADERS["User-Agent"] = user_agent + if USER_AGENT: + API_HEADERS["User-Agent"] = USER_AGENT -def set_wink_credentials(client_id, client_secret, refresh_token): +def set_user_agent(user_agent): + global USER_AGENT + + USER_AGENT = user_agent + + +def set_wink_credentials(email, password, client_id, client_secret): + global CLIENT_ID, CLIENT_SECRET, REFRESH_TOKEN + + CLIENT_ID = client_id + CLIENT_SECRET = client_secret + data = { "client_id": client_id, "client_secret": client_secret, - "grant_type": "refresh_token", - "refresh_token": refresh_token + "grant_type": "password", + "email": email, + "password": password } headers = { 'Content-Type': 'application/json' @@ -71,8 +95,30 @@ def set_wink_credentials(client_id, client_secret, refresh_token): headers=headers) response_json = response.json() access_token = response_json.get('access_token') + REFRESH_TOKEN = response_json.get('refresh_token') set_bearer_token(access_token) - return access_token + + +def refresh_access_token(): + if CLIENT_ID and CLIENT_SECRET and REFRESH_TOKEN: + data = { + "client_id": CLIENT_ID, + "client_secret": CLIENT_SECRET, + "grant_type": "refresh_token", + "refresh_token": REFRESH_TOKEN + } + headers = { + 'Content-Type': 'application/json' + } + response = requests.post('{}/oauth2/token'.format(WinkApiInterface.BASE_URL), + data=json.dumps(data), + headers=headers) + response_json = response.json() + access_token = response_json.get('access_token') + set_bearer_token(access_token) + return access_token + else: + return None def get_bulbs(): From 459428ba7b01f4acaf588f1374306ca3095c425c Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Mon, 19 Sep 2016 15:48:42 -0400 Subject: [PATCH 122/178] Return true on current sensor connection --- src/pywink/devices/sensors.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/pywink/devices/sensors.py b/src/pywink/devices/sensors.py index 4d4fe58..c8ebe2f 100644 --- a/src/pywink/devices/sensors.py +++ b/src/pywink/devices/sensors.py @@ -255,6 +255,15 @@ def __init__(self, device_state_as_json, api_interface): self.CAPABILITY, self.UNIT) + @property + def available(self): + """ + connection variable isn't stable. + Porkfolio can be offline, but updates will continue to occur. + always returning True to avoid this issue. + """ + return True + def device_id(self): root_name = self.json_state.get('piggy_bank_id', self.name()) return '{}+{}'.format(root_name, self._capability) From 942627101cc63ce9019d4474c489e70fa1b33262 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Mon, 19 Sep 2016 16:02:09 -0400 Subject: [PATCH 123/178] Updated version --- CHANGELOG.md | 3 +++ src/setup.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 31fef7f..9fd2e0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 0.8.0 +- Support for Wink relay sensors and email/password auth + ## 0.7.15 - Fix for PIR multisensors diff --git a/src/setup.py b/src/setup.py index db985fa..d1e317e 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='0.7.15', + version='0.8.0', description='Access Wink devices via the Wink API', url='http://github.com/bradsk88/python-wink', author='Brad Johnson', From a7edc0551a4d8ef105063872b867f2546a8e941d Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Wed, 21 Sep 2016 12:36:52 -0400 Subject: [PATCH 124/178] Fixed status check and relay humidity --- src/pywink/__init__.py | 9 +++++---- src/pywink/api.py | 2 +- src/pywink/devices/sensors.py | 6 +++++- src/pywink/test/devices/standard/init_test.py | 6 ++++++ 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/pywink/__init__.py b/src/pywink/__init__.py index 455b8d0..2211acc 100644 --- a/src/pywink/__init__.py +++ b/src/pywink/__init__.py @@ -2,7 +2,8 @@ Top level functions """ # noqa -from pywink.api import set_bearer_token, refresh_access_token, set_wink_credentials, \ - get_bulbs, get_eggtrays, get_garage_doors, get_locks, \ - get_powerstrip_outlets, get_sensors, get_shades, get_sirens, get_switches, \ - get_devices, is_token_set, get_subscription_key, get_keys, get_piggy_banks +from pywink.api import set_bearer_token, refresh_access_token, \ + set_wink_credentials, set_user_agent, get_bulbs, get_eggtrays, \ + get_garage_doors, get_locks, get_powerstrip_outlets, get_sensors, \ + get_shades, get_sirens, get_switches, get_devices, is_token_set, \ + get_subscription_key, get_keys, get_piggy_banks diff --git a/src/pywink/api.py b/src/pywink/api.py index d42c0cc..6351ee3 100644 --- a/src/pywink/api.py +++ b/src/pywink/api.py @@ -36,7 +36,7 @@ def set_device_state(self, device, state, id_override=None): arequest = requests.put(url_string, data=json.dumps(state), headers=API_HEADERS) - if arequest.status == 401: + if arequest.status_code == 401: new_token = refresh_access_token() if new_token: arequest = requests.put(url_string, diff --git a/src/pywink/devices/sensors.py b/src/pywink/devices/sensors.py index c8ebe2f..d53c9cd 100644 --- a/src/pywink/devices/sensors.py +++ b/src/pywink/devices/sensors.py @@ -91,7 +91,11 @@ def humidity_percentage(self): :return: The relative humidity detected by the sensor (0% to 100%) :rtype: int """ - return self.last_reading() + # Relay returns humidity as a decimal + if self.last_reading() < 1.0: + return int(round(self.last_reading() * 100)) + else: + return self.last_reading() def pubnub_update(self, json_response): # Pubnub returns the humidity as a decimal diff --git a/src/pywink/test/devices/standard/init_test.py b/src/pywink/test/devices/standard/init_test.py index a0daab1..806215b 100644 --- a/src/pywink/test/devices/standard/init_test.py +++ b/src/pywink/test/devices/standard/init_test.py @@ -459,3 +459,9 @@ def test_should_handle_relay_response(self): self.assertIsInstance(devices[2], WinkPresenceSensor) self.assertIsInstance(devices[3], WinkProximitySensor) + def test_should_convert_humidity_to_percentage(self): + with open('{}/api_responses/wink_relay_sensor.json'.format(os.path.dirname(__file__))) as relay_file: + response_dict = json.load(relay_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SENSOR_POD]) + self.assertEqual(devices[0].humidity_percentage(), 69) + From 073aac34680a89225d3b2fe8406c3c34244c61a3 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Thu, 29 Sep 2016 10:07:58 -0400 Subject: [PATCH 125/178] Support for smoke and co detectors. --- CHANGELOG.md | 3 + src/pywink/__init__.py | 3 +- src/pywink/api.py | 18 +++++- src/pywink/devices/sensors.py | 40 +++++++++++- src/pywink/devices/types.py | 4 +- .../api_responses/smoke_detector.json | 61 +++++++++++++++++++ src/pywink/test/devices/standard/init_test.py | 28 ++++++++- src/setup.py | 2 +- 8 files changed, 153 insertions(+), 6 deletions(-) create mode 100644 src/pywink/test/devices/standard/api_responses/smoke_detector.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fd2e0e..019fa52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 0.8.1 +- Support for Wink Smoke and CO detectors + ## 0.8.0 - Support for Wink relay sensors and email/password auth diff --git a/src/pywink/__init__.py b/src/pywink/__init__.py index 2211acc..b3ffdb9 100644 --- a/src/pywink/__init__.py +++ b/src/pywink/__init__.py @@ -6,4 +6,5 @@ set_wink_credentials, set_user_agent, get_bulbs, get_eggtrays, \ get_garage_doors, get_locks, get_powerstrip_outlets, get_sensors, \ get_shades, get_sirens, get_switches, get_devices, is_token_set, \ - get_subscription_key, get_keys, get_piggy_banks + get_subscription_key, get_keys, get_piggy_banks, \ + get_smoke_and_co_detectors diff --git a/src/pywink/api.py b/src/pywink/api.py index 6351ee3..ee3bfbe 100644 --- a/src/pywink/api.py +++ b/src/pywink/api.py @@ -8,7 +8,8 @@ from pywink.devices.sensors import WinkSensorPod, WinkHumiditySensor, WinkBrightnessSensor, WinkSoundPresenceSensor, \ WinkTemperatureSensor, WinkVibrationPresenceSensor, \ WinkLiquidPresenceSensor, WinkCurrencySensor, WinkMotionSensor, \ - WinkPresenceSensor, WinkProximitySensor + WinkPresenceSensor, WinkProximitySensor, WinkSmokeDetector, \ + WinkCoDetector from pywink.devices.types import DEVICE_ID_KEYS API_HEADERS = {} @@ -165,6 +166,10 @@ def get_piggy_banks(): return get_devices(device_types.PIGGY_BANK) +def get_smoke_and_co_detectors(): + return get_devices(device_types.SMOKE_DETECTOR) + + def get_subscription_key(): response_dict = wink_api_fetch() first_device = response_dict.get('data')[0] @@ -231,6 +236,10 @@ def get_devices_from_response_dict(response_dict, filter_key): devices.extend(__get_devices_from_piggy_bank(item, api_interface)) continue # Don't capture the porkfolio itself as a device + if key == "smoke_detector_id": + devices.extend(__get_subsensors_from_smoke_detector(item, api_interface)) + continue # Don't capture the base device + devices.append(build_device(item, api_interface)) return devices @@ -295,6 +304,13 @@ def __get_devices_from_piggy_bank(item, api_interface): return subdevices +def __get_subsensors_from_smoke_detector(item, api_interface): + subsensors = [] + subsensors.append(WinkSmokeDetector(item, api_interface)) + subsensors.append(WinkCoDetector(item, api_interface)) + return subsensors + + def __device_is_visible(item, key): is_correctly_structured = bool(item.get(key)) is_visible = not item.get('hidden_at') diff --git a/src/pywink/devices/sensors.py b/src/pywink/devices/sensors.py index d53c9cd..6e44922 100644 --- a/src/pywink/devices/sensors.py +++ b/src/pywink/devices/sensors.py @@ -39,7 +39,9 @@ def battery_level(self): return None def device_id(self): - root_name = self.json_state.get('sensor_pod_id', self.name()) + root_name = self.json_state.get('sensor_pod_id', None) + if root_name is None: + root_name = self.json_state.get('smoke_detector_id', self.name()) return '{}+{}'.format(root_name, self._capability) def update_state(self, require_desired_state_fulfilled=False): @@ -278,3 +280,39 @@ def balance(self): :rtype: int """ return self.last_reading() + + +class WinkSmokeDetector(_WinkCapabilitySensor): + + CAPABILITY = 'smoke_detected' + UNIT = None + + def __init__(self, device_state_as_json, api_interface): + super(WinkSmokeDetector, self).__init__(device_state_as_json, api_interface, + self.CAPABILITY, + self.UNIT) + + def smoke_detected_boolean(self): + """ + :return: Returns True if smoke is detected. + :rtype: bool + """ + return self.last_reading() + + +class WinkCoDetector(_WinkCapabilitySensor): + + CAPABILITY = 'co_detected' + UNIT = None + + def __init__(self, device_state_as_json, api_interface): + super(WinkCoDetector, self).__init__(device_state_as_json, api_interface, + self.CAPABILITY, + self.UNIT) + + def co_detected_boolean(self): + """ + :return: Returns True if CO is detected. + :rtype: bool + """ + return self.last_reading() diff --git a/src/pywink/devices/types.py b/src/pywink/devices/types.py index 7428587..b1e26bf 100644 --- a/src/pywink/devices/types.py +++ b/src/pywink/devices/types.py @@ -9,6 +9,7 @@ SIREN = 'siren' KEY = 'key' PIGGY_BANK = 'piggybank' +SMOKE_DETECTOR = 'smoke_detector' DEVICE_ID_KEYS = { BINARY_SWITCH: 'binary_switch_id', @@ -21,5 +22,6 @@ SHADE: 'shade_id', SIREN: 'siren_id', KEY: 'key_id', - PIGGY_BANK: 'piggy_bank_id' + PIGGY_BANK: 'piggy_bank_id', + SMOKE_DETECTOR: 'smoke_detector_id' } diff --git a/src/pywink/test/devices/standard/api_responses/smoke_detector.json b/src/pywink/test/devices/standard/api_responses/smoke_detector.json new file mode 100644 index 0000000..d13435d --- /dev/null +++ b/src/pywink/test/devices/standard/api_responses/smoke_detector.json @@ -0,0 +1,61 @@ +{ + "data":[ + { + "object_type":"smoke_detector", + "object_id":"12345", + "uuid":"54feac93-327e-4f04-a8ba-500c88e95245", + "icon_id":null, + "icon_code":null, + "last_reading":{ + "connection":true, + "connection_updated_at":1462586187.9383092, + "battery":0.9, + "battery_updated_at":1462586188.4449866, + "co_detected":true, + "co_detected_updated_at":1462586188.4449866, + "smoke_detected":false, + "smoke_detected_updated_at":1471015253.2221863, + "test_activated":false, + "test_activated_updated_at":1462997176.8458738 + }, + "subscription":{ + "pubnub":{ + "subscribe_key":"REMOVED", + "channel":"Removed|smoke_detector-REMOVED|user-REMOVED" + } + }, + "smoke_detector_id":"12345", + "name":"Hallway Smoke Detector", + "locale":"en_us", + "units":{ + + }, + "created_at":1462586187, + "hidden_at":null, + "capabilities":{ + + }, + "manufacturer_device_model":"kidde_smoke_alarm", + "manufacturer_device_id":null, + "device_manufacturer":"kidde", + "model_name":"Smoke Alarm", + "upc_id":"524", + "upc_code":"kidde_smoke_alarm", + "hub_id":"REMOVED", + "local_id":null, + "radio_type":"kidde", + "linked_service_id":null, + "lat_lng":[ + 39.00000, + -77.00000 + ], + "location":"REMOVED" + } + ], + "errors":[ + + ], + "pagination":{ + "count":13 + } +} diff --git a/src/pywink/test/devices/standard/init_test.py b/src/pywink/test/devices/standard/init_test.py index 806215b..fab690a 100644 --- a/src/pywink/test/devices/standard/init_test.py +++ b/src/pywink/test/devices/standard/init_test.py @@ -8,7 +8,7 @@ from pywink.devices.sensors import WinkSensorPod, WinkBrightnessSensor, WinkHumiditySensor, \ WinkSoundPresenceSensor, WinkVibrationPresenceSensor, WinkTemperatureSensor, \ _WinkCapabilitySensor, WinkLiquidPresenceSensor, WinkCurrencySensor, WinkMotionSensor, \ - WinkProximitySensor, WinkPresenceSensor + WinkProximitySensor, WinkPresenceSensor, WinkSmokeDetector, WinkCoDetector from pywink.devices.standard import WinkGarageDoor, WinkPowerStripOutlet, WinkSiren, WinkLock, \ WinkShade, WinkBinarySwitch, WinkEggTray, WinkKey, WinkPorkfolioNose from pywink.devices.types import DEVICE_ID_KEYS @@ -465,3 +465,29 @@ def test_should_convert_humidity_to_percentage(self): devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SENSOR_POD]) self.assertEqual(devices[0].humidity_percentage(), 69) + +class SmokeDetectorTests(unittest.TestCase): + + def setUp(self): + super(SmokeDetectorTests, self).setUp() + self.api_interface = mock.MagicMock() + + def test_should_handle_smoke_detector_response(self): + with open('{}/api_responses/smoke_detector.json'.format(os.path.dirname(__file__))) as smoke_detector_file: + response_dict = json.load(smoke_detector_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SMOKE_DETECTOR]) + self.assertEqual(2, len(devices)) + smoke = devices[0] + co = devices[1] + self.assertIsInstance(smoke, WinkSmokeDetector) + self.assertIsInstance(co, WinkCoDetector) + self.assertFalse(smoke.smoke_detected_boolean()) + self.assertTrue(co.co_detected_boolean()) + + def test_device_id_should_be_number(self): + with open('{}/api_responses/smoke_detector.json'.format(os.path.dirname(__file__))) as smoke_detector_file: + response_dict = json.load(smoke_detector_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SMOKE_DETECTOR]) + device_id = devices[0].device_id() + self.assertRegex(device_id, "^[0-9]{4,6}") + diff --git a/src/setup.py b/src/setup.py index d1e317e..ba58abe 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='0.8.0', + version='0.8.1', description='Access Wink devices via the Wink API', url='http://github.com/bradsk88/python-wink', author='Brad Johnson', From 5eab0c4982a2a36512eb48d67580eda35d047bb1 Mon Sep 17 00:00:00 2001 From: bradsk88 <7kcwKg60tdcW> Date: Sat, 1 Oct 2016 11:08:27 -0600 Subject: [PATCH 126/178] Correcting Version Number --- CHANGELOG.md | 2 +- src/setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 019fa52..abcfb81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Change Log -## 0.8.1 +## 0.9.0 - Support for Wink Smoke and CO detectors ## 0.8.0 diff --git a/src/setup.py b/src/setup.py index ba58abe..993769c 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='0.8.1', + version='0.9.0', description='Access Wink devices via the Wink API', url='http://github.com/bradsk88/python-wink', author='Brad Johnson', From 6b7a44ab96f12e04231d5bd006be22edaa23cf88 Mon Sep 17 00:00:00 2001 From: bradsk88 <7kcwKg60tdcW> Date: Fri, 21 Oct 2016 17:31:46 -0600 Subject: [PATCH 127/178] Removed accidental r --- src/pywink/devices/standard/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pywink/devices/standard/__init__.py b/src/pywink/devices/standard/__init__.py index fd84aba..80db128 100644 --- a/src/pywink/devices/standard/__init__.py +++ b/src/pywink/devices/standard/__init__.py @@ -206,7 +206,7 @@ def _recent_state_set(self): class WinkGarageDoor(WinkDevice): - r""" represents a wink.py garage door + """ 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: From 9694d219ef4d99ea87e3d1d21bcb4fee7771830d Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Wed, 2 Nov 2016 00:31:48 -0400 Subject: [PATCH 128/178] Thermostat support (#58) --- src/pywink/__init__.py | 2 +- src/pywink/api.py | 12 + src/pywink/devices/factory.py | 4 +- src/pywink/devices/standard/__init__.py | 4 +- src/pywink/devices/standard/thermostat.py | 193 +++++++++++ src/pywink/devices/types.py | 4 +- .../api_responses/gocontrol_thermostat.json | 181 +++++++++++ .../devices/standard/api_responses/nest.json | 241 ++++++++++++++ .../devices/standard/api_responses/sensi.json | 234 ++++++++++++++ .../test/devices/standard/thermostat_test.py | 299 ++++++++++++++++++ 10 files changed, 1170 insertions(+), 4 deletions(-) create mode 100644 src/pywink/devices/standard/thermostat.py create mode 100644 src/pywink/test/devices/standard/api_responses/gocontrol_thermostat.json create mode 100644 src/pywink/test/devices/standard/api_responses/nest.json create mode 100644 src/pywink/test/devices/standard/api_responses/sensi.json create mode 100644 src/pywink/test/devices/standard/thermostat_test.py diff --git a/src/pywink/__init__.py b/src/pywink/__init__.py index b3ffdb9..4165198 100644 --- a/src/pywink/__init__.py +++ b/src/pywink/__init__.py @@ -7,4 +7,4 @@ get_garage_doors, get_locks, get_powerstrip_outlets, get_sensors, \ get_shades, get_sirens, get_switches, get_devices, is_token_set, \ get_subscription_key, get_keys, get_piggy_banks, \ - get_smoke_and_co_detectors + get_smoke_and_co_detectors, get_thermostats, get_set_access_token diff --git a/src/pywink/api.py b/src/pywink/api.py index ee3bfbe..c039a76 100644 --- a/src/pywink/api.py +++ b/src/pywink/api.py @@ -58,6 +58,14 @@ def get_device_state(self, device, id_override=None): return arequest.json() +def get_set_access_token(): + auth = API_HEADERS.get("Authorization") + if auth is not None: + return auth.split()[1] + else: + return None + + def set_bearer_token(token): global API_HEADERS @@ -170,6 +178,10 @@ def get_smoke_and_co_detectors(): return get_devices(device_types.SMOKE_DETECTOR) +def get_thermostats(): + return get_devices(device_types.THERMOSTAT) + + def get_subscription_key(): response_dict = wink_api_fetch() first_device = response_dict.get('data')[0] diff --git a/src/pywink/devices/factory.py b/src/pywink/devices/factory.py index f819e39..9216966 100644 --- a/src/pywink/devices/factory.py +++ b/src/pywink/devices/factory.py @@ -1,7 +1,7 @@ from pywink.devices.base import WinkDevice from pywink.devices.sensors import WinkSensorPod from pywink.devices.standard import WinkBulb, WinkBinarySwitch, WinkPowerStripOutlet, WinkLock, \ - WinkEggTray, WinkGarageDoor, WinkShade, WinkSiren, WinkKey + WinkEggTray, WinkGarageDoor, WinkShade, WinkSiren, WinkKey, WinkThermostat def build_device(device_state_as_json, api_interface): @@ -31,5 +31,7 @@ def build_device(device_state_as_json, api_interface): new_object = WinkSiren(device_state_as_json, api_interface) elif "key_id" in device_state_as_json: new_object = WinkKey(device_state_as_json, api_interface) + elif "thermostat_id" in device_state_as_json: + new_object = WinkThermostat(device_state_as_json, api_interface) return new_object or WinkDevice(device_state_as_json, api_interface) diff --git a/src/pywink/devices/standard/__init__.py b/src/pywink/devices/standard/__init__.py index 80db128..d31c1d0 100644 --- a/src/pywink/devices/standard/__init__.py +++ b/src/pywink/devices/standard/__init__.py @@ -6,6 +6,7 @@ from pywink.devices.base import WinkDevice from pywink.devices.standard.base import WinkBinarySwitch from pywink.devices.standard.bulb import WinkBulb +from pywink.devices.standard.thermostat import WinkThermostat from pywink.domain.devices import is_desired_state_reached @@ -429,4 +430,5 @@ def state(self): WinkGarageDoor.__name__, WinkShade.__name__, WinkSiren.__name__, - WinkPorkfolioNose.__name__] + WinkPorkfolioNose.__name__, + WinkThermostat.__name__] diff --git a/src/pywink/devices/standard/thermostat.py b/src/pywink/devices/standard/thermostat.py new file mode 100644 index 0000000..c9fe375 --- /dev/null +++ b/src/pywink/devices/standard/thermostat.py @@ -0,0 +1,193 @@ +from pywink.devices.standard.base import WinkDevice + + +# pylint: disable=too-many-public-methods +class WinkThermostat(WinkDevice): + """ + Represents a Wink thermostat + 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, api_interface): + super(WinkThermostat, self).__init__(device_state_as_json, api_interface, + objectprefix="thermostats") + + def state(self): + return self.current_hvac_mode() + + def device_id(self): + return self.json_state.get('thermostat_id', self.name()) + + def fan_modes(self): + capabilities = self.json_state.get('capabilities', {}) + cap_fields = capabilities.get('fields', []) + fan_modes = None + for field in cap_fields: + _field = field.get('field') + if _field == 'fan_mode': + fan_modes = field.get('choices') + return fan_modes + + def hvac_modes(self): + capabilities = self.json_state.get('capabilities', {}) + cap_fields = capabilities.get('fields', []) + hvac_modes = None + for field in cap_fields: + _field = field.get('field') + if _field == 'mode': + hvac_modes = field.get('choices') + return hvac_modes + + def away(self): + return self._last_reading.get('users_away', False) + + def current_hvac_mode(self): + return self._last_reading.get('mode', None) + + def current_fan_mode(self): + return self._last_reading.get('fan_mode', None) + + def current_units(self): + return self._last_reading.get('units', None) + + def current_temperature(self): + return self._last_reading.get('temperature', None) + + def current_external_temperature(self): + return self._last_reading.get('external_temperature', None) + + def current_smart_temperature(self): + return self._last_reading.get('smart_temperature', None) + + def current_humidity(self): + return self._last_reading.get('humidity', None) + + def current_max_set_point(self): + return self._last_reading.get('max_set_point', None) + + def current_min_set_point(self): + return self._last_reading.get('min_set_point', None) + + def current_humidifier_mode(self): + return self._last_reading.get('humidifier_mode', None) + + def current_dehumidifier_mode(self): + return self._last_reading.get('dehumidifier_mode', None) + + def current_humidifier_set_point(self): + return self._last_reading.get('humidifier_set_point', None) + + def current_dehumidifier_set_point(self): + return self._last_reading.get('dehumidifier_set_point', None) + + def min_min_set_point(self): + return self._last_reading.get('min_min_set_point', None) + + def max_min_set_point(self): + return self._last_reading.get('max_min_set_point', None) + + def min_max_set_point(self): + return self._last_reading.get('min_max_set_point', None) + + def max_max_set_point(self): + return self._last_reading.get('max_max_set_point', None) + + def eco_target(self): + return self._last_reading.get('eco_target', None) + + def occupied(self): + return self._last_reading.get('occupied', None) + + def deadband(self): + return self._last_reading.get('deadband', None) + + def fan_on(self): + if self.has_fan(): + return self._last_reading.get('fan_active', False) + return False + + def has_fan(self): + return self._last_reading.get('has_fan', False) + + def is_on(self): + return self._last_reading.get('powered', False) + + def set_fan_mode(self, mode): + """ + :param mode: a string one of ["on", "auto"] + :return: nothing + """ + desired_state = {"fan_mode": mode} + + response = self.api_interface.set_device_state(self, { + "desired_state": desired_state + }) + + self._update_state_from_response(response) + + def set_away(self, away=True): + """ + :param away: a boolean of true (away) or false ('home') + :return nothing + """ + desired_state = {"users_away": away} + + response = self.api_interface.set_device_state(self, { + "desired_state": desired_state + }) + + self._update_state_from_response(response) + + def set_away_mode(self, away=True): + """ + :param away: a boolean True for away False for Home + :return: nothing + """ + desired_state = {"users_away": away} + + response = self.api_interface.set_device_state(self, { + "desired_state": desired_state + }) + self._update_state_from_response(response) + + def set_operation_mode(self, mode): + """ + :param mode: a string one of ["cool_only", "heat_only", "auto", "aux", "off"] + :return: nothing + """ + if mode == "off": + desired_state = {"powered": False} + else: + desired_state = {"powered": True, "mode": mode} + + response = self.api_interface.set_device_state(self, { + "desired_state": desired_state + }) + + self._update_state_from_response(response) + + def set_temperature(self, min_set_point=None, max_set_point=None): + """ + :param temperature: a float for the temperature value in celsius + :return: nothing + """ + desired_state = {} + + if min_set_point: + desired_state['min_set_point'] = min_set_point + if max_set_point: + desired_state['max_set_point'] = max_set_point + + response = self.api_interface.set_device_state(self, { + "desired_state": desired_state + }) + + self._update_state_from_response(response) + + def __repr__(self): + return "" % ( + self.name(), self.device_id()) diff --git a/src/pywink/devices/types.py b/src/pywink/devices/types.py index b1e26bf..8d2a658 100644 --- a/src/pywink/devices/types.py +++ b/src/pywink/devices/types.py @@ -10,6 +10,7 @@ KEY = 'key' PIGGY_BANK = 'piggybank' SMOKE_DETECTOR = 'smoke_detector' +THERMOSTAT = 'thermostat' DEVICE_ID_KEYS = { BINARY_SWITCH: 'binary_switch_id', @@ -23,5 +24,6 @@ SIREN: 'siren_id', KEY: 'key_id', PIGGY_BANK: 'piggy_bank_id', - SMOKE_DETECTOR: 'smoke_detector_id' + SMOKE_DETECTOR: 'smoke_detector_id', + THERMOSTAT: 'thermostat_id' } diff --git a/src/pywink/test/devices/standard/api_responses/gocontrol_thermostat.json b/src/pywink/test/devices/standard/api_responses/gocontrol_thermostat.json new file mode 100644 index 0000000..a41ce50 --- /dev/null +++ b/src/pywink/test/devices/standard/api_responses/gocontrol_thermostat.json @@ -0,0 +1,181 @@ +{ + "data":[ + { + "model_name":null, + "capabilities":{ + "notification_robots":[ + "aux_active_notification" + ], + "fields":[ + { + "field":"max_set_point", + "type":"float", + "mutability":"read-write" + }, + { + "field":"min_set_point", + "type":"float", + "mutability":"read-write" + }, + { + "field":"powered", + "type":"boolean", + "mutability":"read-write" + }, + { + "field":"units", + "type":"nested_hash", + "mutability":"read-only" + }, + { + "field":"temperature", + "type":"float", + "mutability":"read-only" + }, + { + "field":"external_temperature", + "type":"float", + "mutability":"read-only" + }, + { + "field":"min_min_set_point", + "type":"float", + "mutability":"read-only" + }, + { + "field":"max_min_set_point", + "type":"float", + "mutability":"read-only" + }, + { + "field":"min_max_set_point", + "type":"float", + "mutability":"read-only" + }, + { + "field":"max_max_set_point", + "type":"float", + "mutability":"read-only" + }, + { + "field":"deadband", + "type":"float", + "mutability":"read-only" + }, + { + "field":"connection", + "type":"boolean", + "mutability":"read-only" + }, + { + "field":"fan_mode", + "choices":[ + "on", + "auto" + ], + "type":"selection", + "mutability":"read-write" + }, + { + "field":"mode", + "choices":[ + "heat_only", + "cool_only", + "auto", + "aux" + ], + "type":"selection", + "mutability":"read-write" + } + ] + }, + "location":"", + "units":{ + "temperature":"f" + }, + "name":"Thermostat", + "smart_schedule_enabled":false, + "upc_id":"129", + "manufacturer_device_id":null, + "lat_lng":[ + 98.765432, + 12.345678 + ], + "thermostat_id":"115090", + "device_manufacturer":null, + "local_id":"18", + "hidden_at":null, + "manufacturer_device_model":null, + "linked_service_id":null, + "created_at":1455335503, + "subscription":{ + "pubnub":{ + "channel":"[removed]", + "subscribe_key":"[removed]" + } + }, + "hub_id":"196567", + "locale":"en_us", + "last_reading":{ + "desired_min_set_point_updated_at":1463210230.8420756, + "modes_allowed_updated_at":1475953458.4724526, + "connection_updated_at":1475953458.4724526, + "min_min_set_point":null, + "desired_powered_updated_at":1461454387.768337, + "connection":true, + "fan_mode":"auto", + "external_temperature_updated_at":null, + "min_min_set_point_updated_at":null, + "desired_units_updated_at":1455335651.9014053, + "deadband_updated_at":null, + "units":"f", + "desired_fan_mode_updated_at":1461454387.768337, + "deadband":null, + "max_set_point":21.11111111111111, + "powered":false, + "max_max_set_point":null, + "mode_updated_at":1475953458.4724526, + "powered_updated_at":1475953458.4724526, + "desired_max_set_point_updated_at":1455335566.047624, + "max_min_set_point":null, + "desired_mode_updated_at":1461896712.6109614, + "fan_mode_updated_at":1475953458.4724526, + "min_max_set_point":null, + "modes_allowed":[ + "heat_only", + "cool_only", + "auto", + "aux" + ], + "max_min_set_point_updated_at":null, + "mode":"heat_only", + "units_updated_at":1475953458.4724526, + "max_max_set_point_updated_at":null, + "min_max_set_point_updated_at":null, + "min_set_point":18.333333333333332, + "temperature_updated_at":1475953458.4724526, + "external_temperature":null, + "temperature":23.88888888888889, + "min_set_point_updated_at":1475953458.4724526, + "max_set_point_updated_at":1475953458.4724526 + }, + "upc_code":"generic_zwave_thermostat", + "uuid":"9d25c28e-bb1f-4ae1-b2af-036494f4796b", + "radio_type":"zwave", + "desired_state":{ + "units":"f", + "powered":false, + "fan_mode":"auto", + "max_set_point":21.11111111111111, + "min_set_point":18.333333333333332, + "mode":"heat_only" + } + } + ], + "errors":[ + + ], + "pagination":{ + "count":13 + } +} diff --git a/src/pywink/test/devices/standard/api_responses/nest.json b/src/pywink/test/devices/standard/api_responses/nest.json new file mode 100644 index 0000000..756f024 --- /dev/null +++ b/src/pywink/test/devices/standard/api_responses/nest.json @@ -0,0 +1,241 @@ +{ + "data":[ + { + "uuid":"98e6366f-1d86-417d-8610-6a81e1234567G", + "desired_state":{ + "max_set_point":23, + "min_set_point":20.5, + "powered":true, + "users_away":true, + "fan_timer_active":false, + "mode":"auto", + "short_name":"Hallway" + }, + "last_reading":{ + "max_set_point":23, + "max_set_point_updated_at":1477509791.279, + "min_set_point":20.5, + "min_set_point_updated_at":1477509791.279, + "powered":true, + "powered_updated_at":1477509791.279, + "users_away":true, + "users_away_updated_at":1477509791.279, + "fan_timer_active":false, + "fan_timer_active_updated_at":1477509791.279, + "units":"c", + "units_updated_at":1477509791.279, + "temperature":21, + "temperature_updated_at":1477509791.279, + "external_temperature":null, + "external_temperature_updated_at":null, + "min_min_set_point":null, + "min_min_set_point_updated_at":null, + "max_min_set_point":null, + "max_min_set_point_updated_at":null, + "min_max_set_point":null, + "min_max_set_point_updated_at":null, + "max_max_set_point":null, + "max_max_set_point_updated_at":null, + "deadband":1.5, + "deadband_updated_at":1477509791.279, + "eco_target":true, + "eco_target_updated_at":1477509791.279, + "manufacturer_structure_id":"VR4O6oe42tRtUfVtUqrSV3MqLdRNn93khAIu3iy3vxM0nUKe1234456", + "manufacturer_structure_id_updated_at":1477509791.279, + "has_fan":true, + "has_fan_updated_at":1477509791.279, + "fan_duration":0, + "fan_duration_updated_at":1477509791.279, + "last_error":null, + "last_error_updated_at":1477508548.9048016, + "connection":true, + "connection_updated_at":1477509791.279, + "mode":"auto", + "mode_updated_at":1477509791.279, + "short_name":"Hallway", + "short_name_updated_at":1477509791.279, + "modes_allowed":[ + "auto", + "heat_only", + "cool_only" + ], + "modes_allowed_updated_at":1477509791.279, + "desired_max_set_point_updated_at":1477509906.2907016, + "desired_min_set_point_updated_at":1477509906.2907016, + "desired_powered_updated_at":1477509907.2537994, + "desired_users_away_updated_at":1477509779.2279897, + "desired_fan_timer_active_updated_at":1477509906.2907016, + "desired_mode_updated_at":1477509907.2537994, + "desired_short_name_updated_at":1477509906.2907016, + "mode_changed_at":1477509769.941, + "max_set_point_changed_at":1477449273.651, + "min_set_point_changed_at":1477502492.1976361, + "temperature_changed_at":1477499242.523, + "fan_timer_active_changed_at":1477449301.854, + "users_away_changed_at":1477509778.566, + "eco_target_changed_at":1477509791.279, + "desired_users_away_changed_at":1477509779.2279897, + "desired_powered_changed_at":1477509770.4954133, + "powered_changed_at":1477449121.484, + "last_error_changed_at":1477508548.9048016, + "desired_fan_timer_active_changed_at":1477426055.5319712, + "desired_mode_changed_at":1477509770.4954133, + "desired_min_set_point_changed_at":1477503213.0961704, + "desired_max_set_point_changed_at":1477503092.5332086 + }, + "subscription":{ + "pubnub":{ + "subscribe_key":"sub-c-f7bf7f7e-0542-11e312345-02ee2ddab7fe", + "channel":"0b4d044513a22ab9ae821071234455a|thermostat-12349|user-312345" + } + }, + "thermostat_id":"96559", + "name":"Home Hallway Thermostat", + "locale":"en_us", + "units":{ + "temperature":"c" + }, + "created_at":1449443871, + "hidden_at":null, + "capabilities":{ + "fields":[ + { + "type":"float", + "field":"max_set_point", + "mutability":"read-write", + "clear_desired_state_tolerance":0.5 + }, + { + "type":"float", + "field":"min_set_point", + "mutability":"read-write", + "clear_desired_state_tolerance":0.5 + }, + { + "type":"boolean", + "field":"powered", + "mutability":"read-write" + }, + { + "type":"boolean", + "field":"users_away", + "mutability":"read-write" + }, + { + "type":"boolean", + "field":"fan_timer_active", + "mutability":"read-write" + }, + { + "type":"nested_hash", + "field":"units", + "mutability":"read-only" + }, + { + "type":"float", + "field":"temperature", + "mutability":"read-only" + }, + { + "type":"float", + "field":"external_temperature", + "mutability":"read-only" + }, + { + "type":"float", + "field":"min_min_set_point", + "mutability":"read-only" + }, + { + "type":"float", + "field":"max_min_set_point", + "mutability":"read-only" + }, + { + "type":"float", + "field":"min_max_set_point", + "mutability":"read-only" + }, + { + "type":"float", + "field":"max_max_set_point", + "mutability":"read-only" + }, + { + "type":"float", + "field":"deadband", + "mutability":"read-only" + }, + { + "type":"boolean", + "field":"eco_target", + "mutability":"read-only" + }, + { + "type":"string", + "field":"manufacturer_structure_id", + "mutability":"read-only" + }, + { + "type":"boolean", + "field":"has_fan", + "mutability":"read-only" + }, + { + "type":"integer", + "field":"fan_duration", + "mutability":"read-only" + }, + { + "type":"string", + "field":"last_error", + "mutability":"read-only" + }, + { + "type":"boolean", + "field":"connection", + "mutability":"read-only" + }, + { + "field":"mode", + "type":"selection", + "mutability":"read-write", + "choices":[ + "auto", + "heat_only", + "cool_only" + ] + } + ], + "notification_robots":[ + "aux_active_notification" + ] + }, + "triggers":[ + + ], + "manufacturer_device_model":"nest_thermostat", + "manufacturer_device_id":"51qQw2vE1El36wrj5PoRkUxx11Q12345", + "device_manufacturer":"nest", + "model_name":"Learning Thermostat", + "upc_id":"557", + "upc_code":"nest_thermostat", + "hub_id":null, + "local_id":null, + "radio_type":null, + "linked_service_id":"241420", + "lat_lng":[ + 12.345678, + -98.765432 + ], + "location":"", + "smart_schedule_enabled":false + } + ], + "errors":[ + + ], + "pagination":{ + "count":13 + } +} diff --git a/src/pywink/test/devices/standard/api_responses/sensi.json b/src/pywink/test/devices/standard/api_responses/sensi.json new file mode 100644 index 0000000..3fcfb9d --- /dev/null +++ b/src/pywink/test/devices/standard/api_responses/sensi.json @@ -0,0 +1,234 @@ +{ + "data":[ + { + "uuid":"4890fa42-4339-4824-a017-c874bb123456", + "desired_state":{ + "max_set_point":22.22222222222222, + "min_set_point":22.22222222222222, + "fan_mode":"auto", + "powered":false, + "mode":"cool_only" + }, + "last_reading":{ + "max_set_point":22.22222222222222, + "max_set_point_updated_at":1477511203.5183966, + "min_set_point":22.22222222222222, + "min_set_point_updated_at":1477511203.5183966, + "fan_mode":"auto", + "fan_mode_updated_at":1477511203.5183966, + "powered":true, + "has_fan":true, + "powered_updated_at":1477511203.5183966, + "smart_temperature":20.555555555555557, + "humidifier_mode":"auto", + "humidifier_set_point":0.2, + "dehumidifier_mode":"auto", + "dehumidifier_set_point":0.6, + "occupied":true, + "temperature":20.555555555555557, + "temperature_updated_at":1477511203.5183966, + "external_temperature":16.1, + "external_temperature_updated_at":null, + "min_min_set_point":7.222222222222222, + "min_min_set_point_updated_at":1477511203.5183966, + "max_min_set_point":37.22222222222222, + "max_min_set_point_updated_at":1477511203.5183966, + "min_max_set_point":7.222222222222222, + "min_max_set_point_updated_at":1477511203.5183966, + "max_max_set_point":37.22222222222222, + "max_max_set_point_updated_at":1477511203.5183966, + "deadband":1.1111111111111112, + "deadband_updated_at":1477511203.5183966, + "humidity":40, + "humidity_updated_at":1477511203.5183966, + "cool_active":false, + "cool_active_updated_at":1477511203.5183966, + "heat_active":false, + "heat_active_updated_at":1477511203.5183966, + "aux_active":false, + "aux_active_updated_at":1477511203.5183966, + "fan_active":true, + "fan_active_updated_at":1477511203.5183966, + "firmware_version":"6003980915", + "firmware_version_updated_at":1477511203.5183966, + "connection":true, + "connection_updated_at":1477511203.5183966, + "mode":"cool_only", + "mode_updated_at":1477511203.5183966, + "modes_allowed":[ + "heat_only", + "cool_only", + "auto" + ], + "modes_allowed_updated_at":1477511203.5183966, + "units":"f", + "units_updated_at":1477511203.5183966, + "desired_max_set_point_updated_at":1476998924.056839, + "desired_min_set_point_updated_at":1476998924.056839, + "desired_fan_mode_updated_at":1476998924.056839, + "desired_powered_updated_at":1476998788.717325, + "desired_mode_updated_at":1476998924.056839, + "humidity_changed_at":1477509616.356899, + "temperature_changed_at":1477510096.4845245, + "desired_powered_changed_at":1476998788.717325, + "desired_mode_changed_at":1476725230.5535583, + "powered_changed_at":1476998788.5916903, + "cool_active_changed_at":1476998788.5916903, + "fan_active_changed_at":1476998788.5916903, + "max_set_point_changed_at":1476745575.4795704, + "connection_changed_at":1477511203.5183966, + "firmware_version_changed_at":1477274231.5533497 + }, + "subscription":{ + "pubnub":{ + "subscribe_key":"sub-c-f7bf7f7e-0542-11e3-a5e8-02ee2d123456", + "channel":"4d181499722e8b9c0f823aaad005678963325e53|thermostat-140234|user-40345" + } + }, + "thermostat_id":"140295", + "name":"Scanlon", + "locale":"en_us", + "units":{ + "temperature":"f" + }, + "created_at":1465337429, + "hidden_at":null, + "capabilities":{ + "fields":[ + { + "type":"float", + "field":"max_set_point", + "mutability":"read-write" + }, + { + "type":"float", + "field":"min_set_point", + "mutability":"read-write" + }, + { + "type":"selection", + "field":"fan_mode", + "choices":[ + "auto", + "on" + ], + "mutability":"read-write" + }, + { + "type":"boolean", + "field":"powered", + "mutability":"read-write" + }, + { + "type":"float", + "field":"temperature", + "mutability":"read-only" + }, + { + "type":"float", + "field":"external_temperature", + "mutability":"read-only" + }, + { + "type":"float", + "field":"min_min_set_point", + "mutability":"read-only" + }, + { + "type":"float", + "field":"max_min_set_point", + "mutability":"read-only" + }, + { + "type":"float", + "field":"min_max_set_point", + "mutability":"read-only" + }, + { + "type":"float", + "field":"max_max_set_point", + "mutability":"read-only" + }, + { + "type":"float", + "field":"deadband", + "mutability":"read-only" + }, + { + "type":"percentage", + "field":"humidity", + "mutability":"read-only" + }, + { + "type":"boolean", + "field":"cool_active", + "mutability":"read-only" + }, + { + "type":"boolean", + "field":"heat_active", + "mutability":"read-only" + }, + { + "type":"boolean", + "field":"aux_active", + "mutability":"read-only" + }, + { + "type":"boolean", + "field":"fan_active", + "mutability":"read-only" + }, + { + "type":"string", + "field":"firmware_version", + "mutability":"read-only" + }, + { + "type":"boolean", + "field":"connection", + "mutability":"read-only" + }, + { + "field":"mode", + "type":"selection", + "mutability":"read-write", + "choices":[ + "heat_only", + "cool_only", + "auto" + ] + } + ], + "notification_robots":[ + "aux_active_notification" + ] + }, + "triggers":[ + + ], + "manufacturer_device_model":"emerson_up500_wb1", + "manufacturer_device_id":"36-6f-92-ff-fe-04-34-12", + "device_manufacturer":"emerson", + "model_name":"Sensi Wi-Fi Programmable Thermostat", + "upc_id":"652", + "upc_code":"emerson_up500wb1", + "hub_id":null, + "local_id":null, + "radio_type":null, + "linked_service_id":"365180", + "lat_lng":[ + null, + null + ], + "location":"", + "smart_schedule_enabled":false + } + ], + "errors":[ + + ], + "pagination":{ + "count":13 + } +} diff --git a/src/pywink/test/devices/standard/thermostat_test.py b/src/pywink/test/devices/standard/thermostat_test.py new file mode 100644 index 0000000..483d9f8 --- /dev/null +++ b/src/pywink/test/devices/standard/thermostat_test.py @@ -0,0 +1,299 @@ +import json +import os +import unittest + +import mock + +from pywink.api import get_devices_from_response_dict, WinkApiInterface +from pywink.devices import types as device_types +from pywink.devices.standard import WinkThermostat +from pywink.devices.types import DEVICE_ID_KEYS + + +class ThermostatModeTests(unittest.TestCase): + + def test_should_be_true_if_thermostat_is_on(self): + with open('{}/api_responses/nest.json'.format(os.path.dirname(__file__))) as thermostat_file: + response_dict = json.load(thermostat_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) + + thermostat = devices[0] + """ :type thermostat: pywink.devices.standard.WinkThermostat """ + self.assertTrue(thermostat.is_on()) + + def test_should_be_true_if_response_contains_heat_capabilities(self): + with open('{}/api_responses/nest.json'.format(os.path.dirname(__file__))) as thermostat_file: + response_dict = json.load(thermostat_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) + + thermostat = devices[0] + """ :type thermostat: pywink.devices.standard.WinkThermostat """ + self.assertTrue('heat_only' in thermostat.hvac_modes()) + + def test_should_be_true_if_response_contains_cool_capabilities(self): + with open('{}/api_responses/nest.json'.format(os.path.dirname(__file__))) as thermostat_file: + response_dict = json.load(thermostat_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) + + thermostat = devices[0] + """ :type thermostat: pywink.devices.standard.WinkThermostat """ + self.assertTrue('cool_only' in thermostat.hvac_modes()) + + def test_should_be_true_if_response_contains_auto_capabilities(self): + with open('{}/api_responses/nest.json'.format(os.path.dirname(__file__))) as thermostat_file: + response_dict = json.load(thermostat_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) + + thermostat = devices[0] + """ :type thermostat: pywink.devices.standard.WinkThermostat """ + self.assertTrue('auto' in thermostat.hvac_modes()) + + def test_should_be_false_if_response_doesnt_contains_aux_capabilities(self): + with open('{}/api_responses/nest.json'.format(os.path.dirname(__file__))) as thermostat_file: + response_dict = json.load(thermostat_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) + + thermostat = devices[0] + """ :type thermostat: pywink.devices.standard.WinkThermostat """ + self.assertFalse('aux' in thermostat.hvac_modes()) + + def test_should_be_cool_only_for_current_hvac_mode(self): + with open('{}/api_responses/sensi.json'.format(os.path.dirname(__file__))) as thermostat_file: + response_dict = json.load(thermostat_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) + + thermostat = devices[0] + """ :type thermostat: pywink.devices.standard.WinkThermostat """ + self.assertEqual('cool_only', thermostat.current_hvac_mode()) + +class ThermostatFanTests(unittest.TestCase): + + def test_should_be_true_if_response_contains_fan_on_capabilities(self): + with open('{}/api_responses/sensi.json'.format(os.path.dirname(__file__))) as thermostat_file: + response_dict = json.load(thermostat_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) + + thermostat = devices[0] + """ :type thermostat: pywink.devices.standard.WinkThermostat """ + self.assertTrue('on' in thermostat.fan_modes()) + + def test_should_be_true_if_response_contains_fan_auto_capabilities(self): + with open('{}/api_responses/sensi.json'.format(os.path.dirname(__file__))) as thermostat_file: + response_dict = json.load(thermostat_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) + + thermostat = devices[0] + """ :type thermostat: pywink.devices.standard.WinkThermostat """ + self.assertTrue('auto' in thermostat.fan_modes()) + + def test_should_be_true_if_response_contains_fan_on_capabilities(self): + with open('{}/api_responses/sensi.json'.format(os.path.dirname(__file__))) as thermostat_file: + response_dict = json.load(thermostat_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) + + thermostat = devices[0] + """ :type thermostat: pywink.devices.standard.WinkThermostat """ + self.assertTrue('auto' in thermostat.fan_modes()) + + def test_should_be_true_if_thermostat_fan_is_on(self): + with open('{}/api_responses/sensi.json'.format(os.path.dirname(__file__))) as thermostat_file: + response_dict = json.load(thermostat_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) + + thermostat = devices[0] + """ :type thermostat: pywink.devices.standard.WinkThermostat """ + self.assertTrue(thermostat.fan_on()) + + def test_should_be_true_if_thermostat_has_fan(self): + with open('{}/api_responses/sensi.json'.format(os.path.dirname(__file__))) as thermostat_file: + response_dict = json.load(thermostat_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) + + thermostat = devices[0] + """ :type thermostat: pywink.devices.standard.WinkThermostat """ + self.assertTrue(thermostat.has_fan()) + + def test_should_be_auto_for_current_fan_mode(self): + with open('{}/api_responses/sensi.json'.format(os.path.dirname(__file__))) as thermostat_file: + response_dict = json.load(thermostat_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) + + thermostat = devices[0] + """ :type thermostat: pywink.devices.standard.WinkThermostat """ + self.assertEqual('auto', thermostat.current_fan_mode()) + + +class ThermostatAvailableOptionsTests(unittest.TestCase): + + def test_should_be_true_if_thermostat_has_detected_occupancy(self): + # sensi.json been faked to add in the occupied field for testing. + with open('{}/api_responses/sensi.json'.format(os.path.dirname(__file__))) as thermostat_file: + response_dict = json.load(thermostat_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) + + thermostat = devices[0] + """ :type thermostat: pywink.devices.standard.WinkThermostat """ + self.assertTrue(thermostat.occupied()) + + def test_should_be_true_if_thermostat_set_to_eco_mode(self): + with open('{}/api_responses/sensi.json'.format(os.path.dirname(__file__))) as thermostat_file: + response_dict = json.load(thermostat_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) + + thermostat = devices[0] + """ :type thermostat: pywink.devices.standard.WinkThermostat """ + self.assertFalse(thermostat.eco_target()) + + def test_should_be_true_if_thermostat_set_to_away(self): + with open('{}/api_responses/sensi.json'.format(os.path.dirname(__file__))) as thermostat_file: + response_dict = json.load(thermostat_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) + + thermostat = devices[0] + """ :type thermostat: pywink.devices.standard.WinkThermostat """ + self.assertFalse(thermostat.away()) + + def test_current_units_should_be_f(self): + with open('{}/api_responses/sensi.json'.format(os.path.dirname(__file__))) as thermostat_file: + response_dict = json.load(thermostat_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) + + thermostat = devices[0] + """ :type thermostat: pywink.devices.standard.WinkThermostat """ + self.assertEqual('f', thermostat.current_units()) + +class ThermostatTemperatureTests(unittest.TestCase): + + def test_set_point_limits(self): + with open('{}/api_responses/sensi.json'.format(os.path.dirname(__file__))) as thermostat_file: + response_dict = json.load(thermostat_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) + + thermostat = devices[0] + """ :type thermostat: pywink.devices.standard.WinkThermostat """ + self.assertEqual(7.222222222222222, thermostat.min_min_set_point()) + self.assertEqual(7.222222222222222, thermostat.min_max_set_point()) + self.assertEqual(37.22222222222222, thermostat.max_min_set_point()) + self.assertEqual(37.22222222222222, thermostat.max_max_set_point()) + + def test_deadband(self): + with open('{}/api_responses/sensi.json'.format(os.path.dirname(__file__))) as thermostat_file: + response_dict = json.load(thermostat_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) + + thermostat = devices[0] + """ :type thermostat: pywink.devices.standard.WinkThermostat """ + self.assertEqual(1.1111111111111112, thermostat.deadband()) + + def test_current_external_temp(self): + with open('{}/api_responses/sensi.json'.format(os.path.dirname(__file__))) as thermostat_file: + response_dict = json.load(thermostat_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) + + thermostat = devices[0] + """ :type thermostat: pywink.devices.standard.WinkThermostat """ + self.assertEqual(16.1, thermostat.current_external_temperature()) + + def test_current_temp(self): + with open('{}/api_responses/sensi.json'.format(os.path.dirname(__file__))) as thermostat_file: + response_dict = json.load(thermostat_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) + + thermostat = devices[0] + """ :type thermostat: pywink.devices.standard.WinkThermostat """ + self.assertEqual(20.555555555555557, thermostat.current_temperature()) + + def test_current_max_set_point(self): + with open('{}/api_responses/sensi.json'.format(os.path.dirname(__file__))) as thermostat_file: + response_dict = json.load(thermostat_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) + + thermostat = devices[0] + """ :type thermostat: pywink.devices.standard.WinkThermostat """ + self.assertEqual(22.22222222222222, thermostat.current_max_set_point()) + + def test_current_min_set_point(self): + with open('{}/api_responses/sensi.json'.format(os.path.dirname(__file__))) as thermostat_file: + response_dict = json.load(thermostat_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) + + thermostat = devices[0] + """ :type thermostat: pywink.devices.standard.WinkThermostat """ + self.assertEqual(22.22222222222222, thermostat.current_min_set_point()) + + def test_current_smart_temperature(self): + # This result is only present on ecobee thermostats, the sensi.json has + # been faked to add in the smart_temperature field for testing. + with open('{}/api_responses/sensi.json'.format(os.path.dirname(__file__))) as thermostat_file: + response_dict = json.load(thermostat_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) + + thermostat = devices[0] + """ :type thermostat: pywink.devices.standard.WinkThermostat """ + self.assertEqual(20.555555555555557, thermostat.current_smart_temperature()) + + +class ThermostatHumidityTests(unittest.TestCase): + + def test_current_humidity(self): + with open('{}/api_responses/sensi.json'.format(os.path.dirname(__file__))) as thermostat_file: + response_dict = json.load(thermostat_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) + + thermostat = devices[0] + """ :type thermostat: pywink.devices.standard.WinkThermostat """ + self.assertEqual(40, thermostat.current_humidity()) + + def test_current_humidifier_mode(self): + # sensi.json been faked to add in the humidifier_mode field for testing. + with open('{}/api_responses/sensi.json'.format(os.path.dirname(__file__))) as thermostat_file: + response_dict = json.load(thermostat_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) + + thermostat = devices[0] + """ :type thermostat: pywink.devices.standard.WinkThermostat """ + self.assertEqual('auto', thermostat.current_humidifier_mode()) + + def test_current_dehumidifier_mode(self): + # sensi.json been faked to add in the dehumidifier_mode field + # for testing. + with open('{}/api_responses/sensi.json'.format(os.path.dirname(__file__))) as thermostat_file: + response_dict = json.load(thermostat_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) + + thermostat = devices[0] + """ :type thermostat: pywink.devices.standard.WinkThermostat """ + self.assertEqual('auto', thermostat.current_dehumidifier_mode()) + + def test_current_humidifier_set_point(self): + # sensi.json has been faked to add in the humidifier_set_point + # field for testing. + with open('{}/api_responses/sensi.json'.format(os.path.dirname(__file__))) as thermostat_file: + response_dict = json.load(thermostat_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) + + thermostat = devices[0] + """ :type thermostat: pywink.devices.standard.WinkThermostat """ + self.assertEqual(0.2, thermostat.current_humidifier_set_point()) + + def test_current_dehumidifier_set_point(self): + # sensi.json has been faked to add in the dehumidifier_set_point + # field for testing. + with open('{}/api_responses/sensi.json'.format(os.path.dirname(__file__))) as thermostat_file: + response_dict = json.load(thermostat_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) + + thermostat = devices[0] + """ :type thermostat: pywink.devices.standard.WinkThermostat """ + self.assertEqual(0.6, thermostat.current_dehumidifier_set_point()) + +class GenericZwaveThermostatTests(unittest.TestCase): + + def test_should_be_true_if_response_contains_aux_capabilities(self): + with open('{}/api_responses/gocontrol_thermostat.json'.format(os.path.dirname(__file__))) as thermostat_file: + response_dict = json.load(thermostat_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) + + thermostat = devices[0] + """ :type thermostat: pywink.devices.standard.WinkThermostat """ + self.assertTrue('aux' in thermostat.hvac_modes()) From 6caf2aea12dfd4db5894b0dde66a1680ff23c537 Mon Sep 17 00:00:00 2001 From: bradsk88 <7kcwKg60tdcW> Date: Tue, 1 Nov 2016 22:33:40 -0600 Subject: [PATCH 129/178] Updating version and changelog --- CHANGELOG.md | 2 ++ src/setup.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index abcfb81..d0ea131 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,6 @@ # Change Log +## 0.10.0 +- Support for Thermostats ## 0.9.0 - Support for Wink Smoke and CO detectors diff --git a/src/setup.py b/src/setup.py index 993769c..404fc73 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='0.9.0', + version='0.10.0', description='Access Wink devices via the Wink API', url='http://github.com/bradsk88/python-wink', author='Brad Johnson', From 65be374fff7e2a6a36fd99d9d75c4ffc8cbb5790 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Tue, 22 Nov 2016 21:23:14 -0500 Subject: [PATCH 130/178] Set objectprefix for smoke/co and piggy banks and removed desired state code (#60) --- CHANGELOG.md | 4 + pylintrc | 2 - src/pywink/__init__.py | 15 ++- src/pywink/devices/base.py | 10 +- src/pywink/devices/sensors.py | 43 ++++-- src/pywink/devices/standard/__init__.py | 103 +-------------- src/pywink/devices/standard/base.py | 25 ---- src/pywink/domain/__init__.py | 6 - src/pywink/domain/devices.py | 14 -- src/pywink/test/devices/standard/init_test.py | 18 +++ src/pywink/test/domain/__init__.py | 0 src/pywink/test/domain/devices_test.py | 122 ------------------ src/setup.py | 2 +- 13 files changed, 72 insertions(+), 292 deletions(-) delete mode 100644 src/pywink/domain/__init__.py delete mode 100644 src/pywink/domain/devices.py delete mode 100644 src/pywink/test/domain/__init__.py delete mode 100644 src/pywink/test/domain/devices_test.py diff --git a/CHANGELOG.md b/CHANGELOG.md index d0ea131..4d1df7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,8 @@ # Change Log +## 0.10.1 +- Set the correct objectprefix for Wink Smoke/CO detectors and Piggy banks +- Remove all desired state code + ## 0.10.0 - Support for Thermostats diff --git a/pylintrc b/pylintrc index 20c728b..41988f8 100644 --- a/pylintrc +++ b/pylintrc @@ -7,13 +7,11 @@ ignore=test # missing-docstring - Document as you like. Good, descriptive method names and variables are preferred over docstrings. # global-statement - used for the on-demand requirement installation # invalid-name - this warning flags short names but I'm fine with short names when used correctly -# duplicate-code - I'd like to re-enabled this but 'wait_till_desired_reached' is duplicated. Fix this later. # fixme - TODOs are allowed prior to v1.0 # locally-disabled - Because that's the whole point! disable= missing-docstring , global-statement , invalid-name - , duplicate-code , fixme , locally-disabled diff --git a/src/pywink/__init__.py b/src/pywink/__init__.py index 4165198..c27559e 100644 --- a/src/pywink/__init__.py +++ b/src/pywink/__init__.py @@ -3,8 +3,13 @@ """ # noqa from pywink.api import set_bearer_token, refresh_access_token, \ - set_wink_credentials, set_user_agent, get_bulbs, get_eggtrays, \ - get_garage_doors, get_locks, get_powerstrip_outlets, get_sensors, \ - get_shades, get_sirens, get_switches, get_devices, is_token_set, \ - get_subscription_key, get_keys, get_piggy_banks, \ - get_smoke_and_co_detectors, get_thermostats, get_set_access_token + set_wink_credentials, set_user_agent, wink_api_fetch, \ + get_set_access_token, is_token_set, get_devices, \ + get_subscription_key + +from pywink.api import get_bulbs, get_garage_doors, get_locks, \ + get_powerstrip_outlets, get_shades, get_sirens, \ + get_switches, get_thermostats + +from pywink.api import get_eggtrays, get_sensors, \ + get_keys, get_piggy_banks, get_smoke_and_co_detectors diff --git a/src/pywink/devices/base.py b/src/pywink/devices/base.py index 0704d57..95e0328 100644 --- a/src/pywink/devices/base.py +++ b/src/pywink/devices/base.py @@ -1,4 +1,3 @@ -from pywink.domain.devices import is_desired_state_reached class WinkDevice(object): @@ -49,22 +48,19 @@ def available(self): def battery_level(self): return self._last_reading.get('battery', None) - def _update_state_from_response(self, response_json, require_desired_state_fulfilled=False): + def _update_state_from_response(self, response_json): """ :param response_json: the json obj returned from query :return: """ _response_json = response_json.get('data') - if _response_json and require_desired_state_fulfilled: - if not is_desired_state_reached(_response_json): - return False self.json_state = _response_json return True - def update_state(self, require_desired_state_fulfilled=False): + def update_state(self): """ Update state with latest info from Wink API. """ response = self.api_interface.get_device_state(self) - return self._update_state_from_response(response, require_desired_state_fulfilled) + return self._update_state_from_response(response) def pubnub_update(self, json_response): self.json_state = json_response diff --git a/src/pywink/devices/sensors.py b/src/pywink/devices/sensors.py index 6e44922..84bafb3 100644 --- a/src/pywink/devices/sensors.py +++ b/src/pywink/devices/sensors.py @@ -4,9 +4,9 @@ class _WinkCapabilitySensor(WinkDevice): - def __init__(self, device_state_as_json, api_interface, capability, unit): + def __init__(self, device_state_as_json, api_interface, capability, unit, objectprefix="sensor_pods"): super(_WinkCapabilitySensor, self).__init__(device_state_as_json, api_interface, - objectprefix="sensor_pods") + objectprefix=objectprefix) self._capability = capability self.unit = unit @@ -40,16 +40,13 @@ def battery_level(self): def device_id(self): root_name = self.json_state.get('sensor_pod_id', None) - if root_name is None: - root_name = self.json_state.get('smoke_detector_id', self.name()) return '{}+{}'.format(root_name, self._capability) - def update_state(self, require_desired_state_fulfilled=False): + def update_state(self): """ Update state with latest info from Wink API. """ root_name = self.json_state.get('sensor_pod_id', self.name()) response = self.api_interface.get_device_state(self, root_name) - self._update_state_from_response(response, - require_desired_state_fulfilled=require_desired_state_fulfilled) + self._update_state_from_response(response) class WinkSensorPod(_WinkCapabilitySensor): @@ -259,7 +256,7 @@ class WinkCurrencySensor(_WinkCapabilitySensor): def __init__(self, device_state_as_json, api_interface): super(WinkCurrencySensor, self).__init__(device_state_as_json, api_interface, self.CAPABILITY, - self.UNIT) + self.UNIT, 'piggy_bank') @property def available(self): @@ -281,6 +278,12 @@ def balance(self): """ return self.last_reading() + def update_state(self): + """ Update state with latest info from Wink API. """ + root_name = self.json_state.get('piggy_bank_id', self.name()) + response = self.api_interface.get_device_state(self, root_name) + self._update_state_from_response(response) + class WinkSmokeDetector(_WinkCapabilitySensor): @@ -290,7 +293,7 @@ class WinkSmokeDetector(_WinkCapabilitySensor): def __init__(self, device_state_as_json, api_interface): super(WinkSmokeDetector, self).__init__(device_state_as_json, api_interface, self.CAPABILITY, - self.UNIT) + self.UNIT, 'smoke_detectors') def smoke_detected_boolean(self): """ @@ -299,6 +302,16 @@ def smoke_detected_boolean(self): """ return self.last_reading() + def device_id(self): + root_name = self.json_state.get('smoke_detector_id', None) + return '{}+{}'.format(root_name, self._capability) + + def update_state(self): + """ Update state with latest info from Wink API. """ + root_name = self.json_state.get('smoke_detector_id', self.name()) + response = self.api_interface.get_device_state(self, root_name) + self._update_state_from_response(response) + class WinkCoDetector(_WinkCapabilitySensor): @@ -308,7 +321,7 @@ class WinkCoDetector(_WinkCapabilitySensor): def __init__(self, device_state_as_json, api_interface): super(WinkCoDetector, self).__init__(device_state_as_json, api_interface, self.CAPABILITY, - self.UNIT) + self.UNIT, 'smoke_detectors') def co_detected_boolean(self): """ @@ -316,3 +329,13 @@ def co_detected_boolean(self): :rtype: bool """ return self.last_reading() + + def device_id(self): + root_name = self.json_state.get('smoke_detector_id', None) + return '{}+{}'.format(root_name, self._capability) + + def update_state(self): + """ Update state with latest info from Wink API. """ + root_name = self.json_state.get('smoke_detector_id', self.name()) + response = self.api_interface.get_device_state(self, root_name) + self._update_state_from_response(response) diff --git a/src/pywink/devices/standard/__init__.py b/src/pywink/devices/standard/__init__.py index d31c1d0..2f1adec 100644 --- a/src/pywink/devices/standard/__init__.py +++ b/src/pywink/devices/standard/__init__.py @@ -7,7 +7,6 @@ from pywink.devices.standard.base import WinkBinarySwitch from pywink.devices.standard.bulb import WinkBulb from pywink.devices.standard.thermostat import WinkThermostat -from pywink.domain.devices import is_desired_state_reached class WinkEggTray(WinkDevice): @@ -52,11 +51,6 @@ def __repr__(self): 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): @@ -72,31 +66,6 @@ def set_state(self, state): self._update_state_from_response(response) self._last_call = (time.time(), state) - # pylint: disable=duplicate-code - 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 @@ -122,13 +91,10 @@ def __repr__(self): def _last_reading(self): return self.json_state.get('last_reading') or {} - def update_state(self, require_desired_state_fulfilled=False): + def update_state(self): """ Update state with latest info from Wink API. """ response = self.api_interface.get_device_state(self, id_override=self.parent_id()) power_strip = response.get('data') - if require_desired_state_fulfilled: - if not is_desired_state_reached(power_strip[self.index]): - return power_strip_reading = power_strip.get('last_reading') outlets = power_strip.get('outlets', power_strip) @@ -137,7 +103,7 @@ def update_state(self, require_desired_state_fulfilled=False): outlet['last_reading']['connection'] = power_strip_reading.get('connection') self.json_state = outlet - def _update_state_from_response(self, response_json, require_desired_state_fulfilled=False): + def _update_state_from_response(self, response_json): """ :param response_json: the json obj returned from query :return: @@ -181,30 +147,6 @@ def set_state(self, state, **kwargs): 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): """ represents a wink.py garage door @@ -223,11 +165,6 @@ 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): @@ -244,32 +181,6 @@ def set_state(self, state): self._last_call = (time.time(), state) - # pylint: disable=duplicate-code - 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 WinkShade(WinkDevice): def __init__(self, device_state_as_json, api_interface, objectprefix="shades"): @@ -286,11 +197,6 @@ def device_id(self): return self.json_state.get('shade_id', self.name()) 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 set_state(self, state): @@ -303,10 +209,6 @@ def set_state(self, state): self._update_state_from_response(response) self._last_call = (time.time(), state) - # self._state = state - - def _recent_state_set(self): - return time.time() - self._last_call[0] < 15 class WinkSiren(WinkBinarySwitch): @@ -392,6 +294,7 @@ def available(self): connection variable isn't stable. Porkfolio can be offline, but updates will continue to occur. always returning True to avoid this issue. + This is the same for the PorkFolio balance sensor. """ return True diff --git a/src/pywink/devices/standard/base.py b/src/pywink/devices/standard/base.py index 34a6e4a..a1961cb 100644 --- a/src/pywink/devices/standard/base.py +++ b/src/pywink/devices/standard/base.py @@ -43,28 +43,3 @@ def set_state(self, state, **kwargs): self._update_state_from_response(response) self._last_call = (time.time(), state) - - def wait_till_desired_reached(self): - """ Wait till desired state reached. Max 10s. """ - # TODO: Get rid of this. Busy-wait loops can go in whatever project is making use of this library. - 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 diff --git a/src/pywink/domain/__init__.py b/src/pywink/domain/__init__.py deleted file mode 100644 index ece52c2..0000000 --- a/src/pywink/domain/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -""" -Helper classes and functions for python-wink entities. - -Any behaviour which is not directly related to storing device state or -interfacing with the Wink API should live in here. -""" diff --git a/src/pywink/domain/devices.py b/src/pywink/domain/devices.py deleted file mode 100644 index e95dbc3..0000000 --- a/src/pywink/domain/devices.py +++ /dev/null @@ -1,14 +0,0 @@ -def is_desired_state_reached(wink_device_state): - """ - :type wink_device: dict - """ - desired_state = wink_device_state.get('desired_state', {}) - last_reading = wink_device_state.get('last_reading', {}) - if not last_reading.get('connection', True): - return True - for name, desired_value in desired_state.items(): - latest_value = last_reading.get(name) - if desired_value != latest_value: - return False - - return True diff --git a/src/pywink/test/devices/standard/init_test.py b/src/pywink/test/devices/standard/init_test.py index fab690a..7d54a3d 100644 --- a/src/pywink/test/devices/standard/init_test.py +++ b/src/pywink/test/devices/standard/init_test.py @@ -443,6 +443,15 @@ def test_device_id_should_be_number(self): device_id = devices[1].device_id() self.assertRegex(device_id, "^[0-9]{4,6}") + def test_objectprefix_should_be_correct(self): + with open('{}/api_responses/porkfolio.json'.format(os.path.dirname(__file__))) as porkfolio_file: + response_dict = json.load(porkfolio_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.PIGGY_BANK]) + objectprefix = devices[0].objectprefix + self.assertRegex(objectprefix, "piggy_bank") + objectprefix = devices[1].objectprefix + self.assertRegex(objectprefix, "piggy_bank") + class RelaySensorTests(unittest.TestCase): def setUp(self): @@ -491,3 +500,12 @@ def test_device_id_should_be_number(self): device_id = devices[0].device_id() self.assertRegex(device_id, "^[0-9]{4,6}") + def test_objectprefix_should_be_correct(self): + with open('{}/api_responses/smoke_detector.json'.format(os.path.dirname(__file__))) as smoke_detector_file: + response_dict = json.load(smoke_detector_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SMOKE_DETECTOR]) + objectprefix = devices[0].objectprefix + self.assertRegex(objectprefix, "smoke_detectors") + objectprefix = devices[1].objectprefix + self.assertRegex(objectprefix, "smoke_detectors") + diff --git a/src/pywink/test/domain/__init__.py b/src/pywink/test/domain/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/pywink/test/domain/devices_test.py b/src/pywink/test/domain/devices_test.py deleted file mode 100644 index 4f3233c..0000000 --- a/src/pywink/test/domain/devices_test.py +++ /dev/null @@ -1,122 +0,0 @@ -import unittest - -from pywink.domain.devices import is_desired_state_reached -from pywink.test.devices.standard.api_responses import ApiResponseJSONLoader - -class IsDesiredStateReachedTests(unittest.TestCase): - - def test_should_return_true_for_bulb_if_desired_brightness_matches_actual_brightness(self): - brightness = 0.4 - bulb_state = { - 'desired_state': { - 'brightness': brightness - }, - 'last_reading': { - 'brightness': brightness - } - } - self.assertTrue(is_desired_state_reached(bulb_state)) - - def test_should_return_false_for_bulb_if_desired_brightness_does_not_match_actual_brightness(self): - brightness_1 = 0.4 - brightness_2 = 0.5 - bulb_state = { - 'desired_state': { - 'brightness': brightness_1 - }, - 'last_reading': { - 'brightness': brightness_2 - } - } - self.assertFalse(is_desired_state_reached(bulb_state)) - - def test_should_return_true_for_bulb_if_desired_hue_matches_actual_hue(self): - hue = 0.5 - bulb_state = { - 'desired_state': { - 'hue': hue - }, - 'last_reading': { - 'hue': hue - } - } - self.assertTrue(is_desired_state_reached(bulb_state)) - - def test_should_return_false_for_bulb_if_desired_hue_does_not_match_actual_hue(self): - hue_1 = 0.4 - hue_2 = 0.5 - bulb_state = { - 'desired_state': { - 'hue': hue_1 - }, - 'last_reading': { - 'hue': hue_2 - } - } - self.assertFalse(is_desired_state_reached(bulb_state)) - - def test_should_return_true_for_bulb_if_desired_saturation_matches_actual_saturation(self): - saturation = 0.5 - bulb_state = { - 'desired_state': { - 'saturation': saturation - }, - 'last_reading': { - 'saturation': saturation - } - } - self.assertTrue(is_desired_state_reached(bulb_state)) - - def test_should_return_false_for_bulb_if_desired_saturation_does_not_match_actual_saturation(self): - saturation_1 = 0.4 - saturation_2 = 0.5 - bulb_state = { - 'desired_state': { - 'saturation': saturation_1 - }, - 'last_reading': { - 'saturation': saturation_2 - } - } - self.assertFalse(is_desired_state_reached(bulb_state)) - - def test_should_return_true_for_bulb_if_desired_powered_matches_actual_powered(self): - powered = True - bulb_state = { - 'desired_state': { - 'powered': powered - }, - 'last_reading': { - 'powered': powered - } - } - self.assertTrue(is_desired_state_reached(bulb_state)) - - def test_should_return_false_for_bulb_if_desired_powered_does_not_match_actual_powered(self): - powered_1 = True - powered_2 = False - bulb_state = { - 'desired_state': { - 'powered': powered_1 - }, - 'last_reading': { - 'powered': powered_2 - } - } - self.assertFalse(is_desired_state_reached(bulb_state)) - - def test_should_return_true_for_real_state_which_where_desired_state_is_reached(self): - response_dict = ApiResponseJSONLoader('light_bulb_with_desired_state_reached.json').load()['data'] - self.assertTrue(is_desired_state_reached(response_dict)) - - def test_should_return_true_if_device_is_disconnected(self): - bulb_state = { - 'desired_state': { - 'powered': True - }, - 'last_reading': { - 'connection': False, - 'powered': False - } - } - self.assertTrue(is_desired_state_reached(bulb_state)) diff --git a/src/setup.py b/src/setup.py index 404fc73..1fe6966 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='0.10.0', + version='0.10.1', description='Access Wink devices via the Wink API', url='http://github.com/bradsk88/python-wink', author='Brad Johnson', From 9981088c644d4783cc9102c785308bcf0d23199f Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Sat, 3 Dec 2016 23:07:13 -0500 Subject: [PATCH 131/178] Get Wink hubs as a sensor (#61) * Wink hub as a sensor --- CHANGELOG.md | 5 + src/pywink/__init__.py | 3 +- src/pywink/api.py | 83 +++++++++------- src/pywink/devices/base.py | 16 +++ src/pywink/devices/factory.py | 4 +- src/pywink/devices/sensors.py | 37 +++++++ src/pywink/devices/types.py | 4 +- .../standard/api_responses/v1_hub.json | 97 +++++++++++++++++++ src/pywink/test/devices/standard/init_test.py | 88 ++++++++++++++++- src/setup.py | 2 +- 10 files changed, 301 insertions(+), 38 deletions(-) create mode 100644 src/pywink/test/devices/standard/api_responses/v1_hub.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d1df7c..132b8c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,9 @@ # Change Log + +## 0.11.0 +- Support for Wink hubs as sensors +- Added more generic attributes to base Wink device (manufacturer_device_model, manufacturer_device_id, device_manufacturer, model_name) + ## 0.10.1 - Set the correct objectprefix for Wink Smoke/CO detectors and Piggy banks - Remove all desired state code diff --git a/src/pywink/__init__.py b/src/pywink/__init__.py index c27559e..0cf99b0 100644 --- a/src/pywink/__init__.py +++ b/src/pywink/__init__.py @@ -12,4 +12,5 @@ get_switches, get_thermostats from pywink.api import get_eggtrays, get_sensors, \ - get_keys, get_piggy_banks, get_smoke_and_co_detectors + get_keys, get_piggy_banks, get_smoke_and_co_detectors, \ + get_hubs diff --git a/src/pywink/api.py b/src/pywink/api.py index c039a76..523fb46 100644 --- a/src/pywink/api.py +++ b/src/pywink/api.py @@ -9,7 +9,7 @@ WinkTemperatureSensor, WinkVibrationPresenceSensor, \ WinkLiquidPresenceSensor, WinkCurrencySensor, WinkMotionSensor, \ WinkPresenceSensor, WinkProximitySensor, WinkSmokeDetector, \ - WinkCoDetector + WinkCoDetector, WinkHub from pywink.devices.types import DEVICE_ID_KEYS API_HEADERS = {} @@ -182,6 +182,10 @@ def get_thermostats(): return get_devices(device_types.THERMOSTAT) +def get_hubs(): + return get_devices(device_types.HUB) + + def get_subscription_key(): response_dict = wink_api_fetch() first_device = response_dict.get('data')[0] @@ -221,43 +225,50 @@ def get_devices_from_response_dict(response_dict, filter_key): devices = [] - keys = DEVICE_ID_KEYS.values() - if filter_key: - keys = [filter_key] - api_interface = WinkApiInterface() - for item in items: - for key in keys: - if not __device_is_visible(item, key): - continue - - if key == "powerstrip_id": - devices.extend(__get_outlets_from_powerstrip(item, api_interface)) - continue # Don't capture the powerstrip itself as a device, only the individual outlets - - if key == "sensor_pod_id": - subsensors = _get_subsensors_from_sensor_pod(item, api_interface) - if subsensors: - devices.extend(subsensors) - continue # Don't capture the base device - if len(subsensors) == 1: - continue - - if key == "piggy_bank_id": - devices.extend(__get_devices_from_piggy_bank(item, api_interface)) - continue # Don't capture the porkfolio itself as a device - - if key == "smoke_detector_id": - devices.extend(__get_subsensors_from_smoke_detector(item, api_interface)) - continue # Don't capture the base device + keys = ['powerstrip_id', 'sensor_pod_id', 'piggy_bank_id', + 'smoke_detector_id', 'hub_id'] + for item in items: + if item.get(filter_key, None) is None: + continue + elif not __device_is_visible(item, filter_key): + continue + elif filter_key in keys: + devices.extend(__get_outlets_from_powerstrip(item, api_interface, filter_key)) + devices.extend(__get_subsensors_from_sensor_pod(item, api_interface, filter_key)) + devices.extend(__get_devices_from_piggy_bank(item, api_interface, filter_key)) + devices.extend(__get_subsensors_from_smoke_detector(item, api_interface, filter_key)) + devices.extend(__get_sensor_from_hub(item, api_interface, filter_key)) + else: devices.append(build_device(item, api_interface)) return devices -def _get_subsensors_from_sensor_pod(item, api_interface): +def __get_sensor_from_hub(item, api_interface, filter_key): + if filter_key != 'hub_id': + return [] + keys = list(DEVICE_ID_KEYS.values()) + # Most devices have a hub_id, but we only want the actual hub. + # This will only return hubs by checking for any other keys + # being present along with the hub_id + skip = False + for key in keys: + if key == "hub_id": + continue + if item.get(key, None) is not None: + skip = True + if skip: + return [] + else: + return [WinkHub(item, api_interface)] + + +def __get_subsensors_from_sensor_pod(item, api_interface, filter_key): + if filter_key != 'sensor_pod_id': + return [] capabilities = [cap['field'] for cap in item.get('capabilities', {}).get('fields', [])] capabilities.extend([cap['field'] for cap in item.get('capabilities', {}).get('sensor_types', [])]) @@ -300,7 +311,9 @@ def _get_subsensors_from_sensor_pod(item, api_interface): return subsensors -def __get_outlets_from_powerstrip(item, api_interface): +def __get_outlets_from_powerstrip(item, api_interface, filter_key): + if filter_key != 'powerstrip_id': + return [] outlets = item['outlets'] for outlet in outlets: if 'subscription' in item: @@ -309,14 +322,18 @@ def __get_outlets_from_powerstrip(item, api_interface): return [build_device(outlet, api_interface) for outlet in outlets if __device_is_visible(outlet, 'outlet_id')] -def __get_devices_from_piggy_bank(item, api_interface): +def __get_devices_from_piggy_bank(item, api_interface, filter_key): + if filter_key != 'piggy_bank_id': + return [] subdevices = [] subdevices.append(WinkCurrencySensor(item, api_interface)) subdevices.append(WinkPorkfolioNose(item, api_interface)) return subdevices -def __get_subsensors_from_smoke_detector(item, api_interface): +def __get_subsensors_from_smoke_detector(item, api_interface, filter_key): + if filter_key != 'smoke_detector_id': + return [] subsensors = [] subsensors.append(WinkSmokeDetector(item, api_interface)) subsensors.append(WinkCoDetector(item, api_interface)) diff --git a/src/pywink/devices/base.py b/src/pywink/devices/base.py index 95e0328..4ab03d1 100644 --- a/src/pywink/devices/base.py +++ b/src/pywink/devices/base.py @@ -48,6 +48,22 @@ def available(self): def battery_level(self): return self._last_reading.get('battery', None) + @property + def manufacturer_device_model(self): + return self.json_state.get('manufacturer_device_model', None) + + @property + def manufacturer_device_id(self): + return self.json_state.get('manufacturer_device_id', None) + + @property + def device_manufacturer(self): + return self.json_state.get('device_manufacturer', None) + + @property + def model_name(self): + return self.json_state.get('model_name', None) + def _update_state_from_response(self, response_json): """ :param response_json: the json obj returned from query diff --git a/src/pywink/devices/factory.py b/src/pywink/devices/factory.py index 9216966..a0bc79e 100644 --- a/src/pywink/devices/factory.py +++ b/src/pywink/devices/factory.py @@ -1,5 +1,5 @@ from pywink.devices.base import WinkDevice -from pywink.devices.sensors import WinkSensorPod +from pywink.devices.sensors import WinkSensorPod, WinkHub from pywink.devices.standard import WinkBulb, WinkBinarySwitch, WinkPowerStripOutlet, WinkLock, \ WinkEggTray, WinkGarageDoor, WinkShade, WinkSiren, WinkKey, WinkThermostat @@ -33,5 +33,7 @@ def build_device(device_state_as_json, api_interface): new_object = WinkKey(device_state_as_json, api_interface) elif "thermostat_id" in device_state_as_json: new_object = WinkThermostat(device_state_as_json, api_interface) + elif "hub_id" in device_state_as_json: + new_object = WinkHub(device_state_as_json, api_interface) return new_object or WinkDevice(device_state_as_json, api_interface) diff --git a/src/pywink/devices/sensors.py b/src/pywink/devices/sensors.py index 84bafb3..be5f038 100644 --- a/src/pywink/devices/sensors.py +++ b/src/pywink/devices/sensors.py @@ -339,3 +339,40 @@ def update_state(self): root_name = self.json_state.get('smoke_detector_id', self.name()) response = self.api_interface.get_device_state(self, root_name) self._update_state_from_response(response) + + +class WinkHub(WinkDevice): + + def __init__(self, device_state_as_json, api_interface, objectprefix="hub_id"): + super(WinkHub, self).__init__(device_state_as_json, api_interface) + + def state(self): + return self._last_reading.get('connection', False) + + def name(self): + name = self.json_state.get('name', "Unknown Name") + name += " hub" + return name + + def device_id(self): + root_name = self.json_state.get('hub_id', None) + return '{}+{}'.format(root_name, 'hub') + + def kidde_radio_code(self): + config = self.json_state.get('configuration') + return config.get('kidde_radio_code') + + def update_needed(self): + return self._last_reading.get('update_needed') + + def ip_address(self): + return self._last_reading.get('ip_address') + + def firmware_version(self): + return self._last_reading.get('firmware_version') + + def update_state(self): + """ Update state with latest info from Wink API. """ + root_name = self.json_state.get('hub_id', self.name()) + response = self.api_interface.get_device_state(self, root_name) + self._update_state_from_response(response) diff --git a/src/pywink/devices/types.py b/src/pywink/devices/types.py index 8d2a658..0bc3d78 100644 --- a/src/pywink/devices/types.py +++ b/src/pywink/devices/types.py @@ -11,6 +11,7 @@ PIGGY_BANK = 'piggybank' SMOKE_DETECTOR = 'smoke_detector' THERMOSTAT = 'thermostat' +HUB = 'hub' DEVICE_ID_KEYS = { BINARY_SWITCH: 'binary_switch_id', @@ -25,5 +26,6 @@ KEY: 'key_id', PIGGY_BANK: 'piggy_bank_id', SMOKE_DETECTOR: 'smoke_detector_id', - THERMOSTAT: 'thermostat_id' + THERMOSTAT: 'thermostat_id', + HUB: 'hub_id' } diff --git a/src/pywink/test/devices/standard/api_responses/v1_hub.json b/src/pywink/test/devices/standard/api_responses/v1_hub.json new file mode 100644 index 0000000..284e2a5 --- /dev/null +++ b/src/pywink/test/devices/standard/api_responses/v1_hub.json @@ -0,0 +1,97 @@ +{ + "data":[ + { + "uuid":"d16117dd-6565-4341-9078-f57282123456", + "desired_state":{ + "pairing_mode":null, + "pairing_prefix":null, + "pairing_mode_duration":0 + }, + "last_reading":{ + "connection":false, + "connection_updated_at":1480783433.1703727, + "agent_session_id":null, + "agent_session_id_updated_at":1480783433.1703727, + "pairing_mode":null, + "pairing_mode_updated_at":1480735480.2006376, + "pairing_prefix":null, + "pairing_prefix_updated_at":null, + "kidde_radio_code_updated_at":1449442170.975126, + "pairing_mode_duration":0, + "pairing_mode_duration_updated_at":1480735480.2006376, + "updating_firmware":false, + "updating_firmware_updated_at":1480735478.8520393, + "firmware_version":"3.3.26-0-gf4fa1428f9", + "firmware_version_updated_at":1480735481.970308, + "update_needed":false, + "update_needed_updated_at":1480735481.970308, + "mac_address":"0", + "mac_address_updated_at":1480735481.970308, + "zigbee_mac_address":"000D6F0005381234", + "zigbee_mac_address_updated_at":1480735480.2006376, + "ip_address":"192.168.1.2", + "ip_address_updated_at":1480735481.970308, + "hub_version":"00.01", + "hub_version_updated_at":1480735481.970308, + "app_version":"0.1.0", + "app_version_updated_at":1480735481.970308, + "transfer_mode":null, + "transfer_mode_updated_at":1480735479.4155467, + "remote_pairable":null, + "remote_pairable_updated_at":null, + "local_control_public_key_hash":"E3:03:48:35:D4:92:46:EE:9E:DD:AC:3B:59:72:78:C7:22:8C:4E:C5:52:85:A3:D2:19:49:40:90:78:56:34:12", + "local_control_public_key_hash_updated_at":1480735490.8312745, + "local_control_id":"c2d9c107-55cb-41b2-b330-fb0e1e123456", + "local_control_id_updated_at":1480735490.8312745, + "desired_pairing_mode_updated_at":1480196216.2784698, + "desired_pairing_prefix_updated_at":1449960168.9824853, + "desired_kidde_radio_code_updated_at":1449442171.0061595, + "desired_pairing_mode_duration_updated_at":1480196216.2784698, + "agent_session_id_changed_at":1480783433.1703727, + "mac_address_changed_at":1480735480.6162705, + "ip_address_changed_at":1480735480.6162705, + "connection_changed_at":1480783433.1703727 + }, + "subscription":{ + "pubnub":{ + "subscribe_key":"sub-c-f7bf7f7e-0542-11e3-a5e8-02ee2d123456", + "channel":"79b3abc01f3a3cf9d9cae79c071df059a4c1a6d2|hub-302123|user-377123" + } + }, + "hub_id":"302123", + "name":"Hub", + "locale":"en_us", + "units":{ + + }, + "created_at":1449442170, + "hidden_at":null, + "capabilities":{ + "oauth2_clients":[ + "wink_hub" + ], + "home_security_device":true, + "provisioning_version":"8a", + "needs_wifi_network_list":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":[ + 12.345678, + -98.765432 + ], + "location":null, + "update_needed":false, + "configuration":{ + "kidde_radio_code":0 + } + } + ] +} diff --git a/src/pywink/test/devices/standard/init_test.py b/src/pywink/test/devices/standard/init_test.py index 7d54a3d..f5ceecd 100644 --- a/src/pywink/test/devices/standard/init_test.py +++ b/src/pywink/test/devices/standard/init_test.py @@ -8,7 +8,8 @@ from pywink.devices.sensors import WinkSensorPod, WinkBrightnessSensor, WinkHumiditySensor, \ WinkSoundPresenceSensor, WinkVibrationPresenceSensor, WinkTemperatureSensor, \ _WinkCapabilitySensor, WinkLiquidPresenceSensor, WinkCurrencySensor, WinkMotionSensor, \ - WinkProximitySensor, WinkPresenceSensor, WinkSmokeDetector, WinkCoDetector + WinkProximitySensor, WinkPresenceSensor, WinkSmokeDetector, WinkCoDetector, \ + WinkHub from pywink.devices.standard import WinkGarageDoor, WinkPowerStripOutlet, WinkSiren, WinkLock, \ WinkShade, WinkBinarySwitch, WinkEggTray, WinkKey, WinkPorkfolioNose from pywink.devices.types import DEVICE_ID_KEYS @@ -509,3 +510,88 @@ def test_objectprefix_should_be_correct(self): objectprefix = devices[1].objectprefix self.assertRegex(objectprefix, "smoke_detectors") + +class HubTests(unittest.TestCase): + + def setUp(self): + super(HubTests, self).setUp() + self.api_interface = mock.MagicMock() + + def test_should_hub_response(self): + with open('{}/api_responses/v1_hub.json'.format(os.path.dirname(__file__))) as hub_file: + response_dict = json.load(hub_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.HUB]) + self.assertEqual(1, len(devices)) + self.assertIsInstance(devices[0], WinkHub) + + def test_device_id_should_be_number(self): + with open('{}/api_responses/v1_hub.json'.format(os.path.dirname(__file__))) as hub_file: + response_dict = json.load(hub_file) + hub = response_dict.get('data')[0] + wink_hub = WinkHub(hub, self.api_interface) + device_id = wink_hub.device_id() + self.assertRegex(device_id, "^[0-9]{4,6}") + + def test_kidde_radio_code(self): + with open('{}/api_responses/v1_hub.json'.format(os.path.dirname(__file__))) as hub_file: + response_dict = json.load(hub_file) + hub = response_dict.get('data')[0] + wink_hub = WinkHub(hub, self.api_interface) + code = wink_hub.kidde_radio_code() + self.assertEqual(code, 0) + + def test_update_needed(self): + with open('{}/api_responses/v1_hub.json'.format(os.path.dirname(__file__))) as hub_file: + response_dict = json.load(hub_file) + hub = response_dict.get('data')[0] + wink_hub = WinkHub(hub, self.api_interface) + update = wink_hub.update_needed() + self.assertFalse(update) + + def test_ip_address(self): + with open('{}/api_responses/v1_hub.json'.format(os.path.dirname(__file__))) as hub_file: + response_dict = json.load(hub_file) + hub = response_dict.get('data')[0] + wink_hub = WinkHub(hub, self.api_interface) + ip = wink_hub.ip_address() + self.assertEqual(ip, '192.168.1.2') + + def test_firmware_version(self): + with open('{}/api_responses/v1_hub.json'.format(os.path.dirname(__file__))) as hub_file: + response_dict = json.load(hub_file) + hub = response_dict.get('data')[0] + wink_hub = WinkHub(hub, self.api_interface) + firmware = wink_hub.firmware_version() + self.assertEqual(firmware, '3.3.26-0-gf4fa1428f9') + + def test_manufacturer_device_id(self): + with open('{}/api_responses/v1_hub.json'.format(os.path.dirname(__file__))) as hub_file: + response_dict = json.load(hub_file) + hub = response_dict.get('data')[0] + wink_hub = WinkHub(hub, self.api_interface) + id = wink_hub.manufacturer_device_id + self.assertEqual(id, None) + + def test_manufacturer(self): + with open('{}/api_responses/v1_hub.json'.format(os.path.dirname(__file__))) as hub_file: + response_dict = json.load(hub_file) + hub = response_dict.get('data')[0] + wink_hub = WinkHub(hub, self.api_interface) + manufacturer = wink_hub.device_manufacturer + self.assertEqual(manufacturer, 'wink') + + def test_model(self): + with open('{}/api_responses/v1_hub.json'.format(os.path.dirname(__file__))) as hub_file: + response_dict = json.load(hub_file) + hub = response_dict.get('data')[0] + wink_hub = WinkHub(hub, self.api_interface) + model = wink_hub.manufacturer_device_model + self.assertEqual(model, 'wink_hub') + + def test_model_name(self): + with open('{}/api_responses/v1_hub.json'.format(os.path.dirname(__file__))) as hub_file: + response_dict = json.load(hub_file) + hub = response_dict.get('data')[0] + wink_hub = WinkHub(hub, self.api_interface) + model_name = wink_hub.model_name + self.assertEqual(model_name, 'Hub') diff --git a/src/setup.py b/src/setup.py index 1fe6966..508ddee 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='0.10.1', + version='0.11.0', description='Access Wink devices via the Wink API', url='http://github.com/bradsk88/python-wink', author='Brad Johnson', From c27c8ed8dceb5950915fbec93e122937e84359f9 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Tue, 3 Jan 2017 21:46:29 -0500 Subject: [PATCH 132/178] Wink fan support (#63) * Fix to thermostats to detect fan * Initial support for fans * Added first fan test, fixed factory * Added more tests --- src/pywink/__init__.py | 2 +- src/pywink/api.py | 4 + src/pywink/devices/factory.py | 9 +- src/pywink/devices/standard/__init__.py | 4 +- src/pywink/devices/standard/fan.py | 119 ++++++++++++++++++ src/pywink/devices/standard/thermostat.py | 4 + src/pywink/devices/types.py | 4 +- .../devices/standard/api_responses/fan.json | 119 ++++++++++++++++++ src/pywink/test/devices/standard/fan_test.py | 93 ++++++++++++++ 9 files changed, 353 insertions(+), 5 deletions(-) create mode 100644 src/pywink/devices/standard/fan.py create mode 100644 src/pywink/test/devices/standard/api_responses/fan.json create mode 100644 src/pywink/test/devices/standard/fan_test.py diff --git a/src/pywink/__init__.py b/src/pywink/__init__.py index 0cf99b0..c7ae03e 100644 --- a/src/pywink/__init__.py +++ b/src/pywink/__init__.py @@ -9,7 +9,7 @@ from pywink.api import get_bulbs, get_garage_doors, get_locks, \ get_powerstrip_outlets, get_shades, get_sirens, \ - get_switches, get_thermostats + get_switches, get_thermostats, get_fans from pywink.api import get_eggtrays, get_sensors, \ get_keys, get_piggy_banks, get_smoke_and_co_detectors, \ diff --git a/src/pywink/api.py b/src/pywink/api.py index 523fb46..197c00e 100644 --- a/src/pywink/api.py +++ b/src/pywink/api.py @@ -186,6 +186,10 @@ def get_hubs(): return get_devices(device_types.HUB) +def get_fans(): + return get_devices(device_types.FAN) + + def get_subscription_key(): response_dict = wink_api_fetch() first_device = response_dict.get('data')[0] diff --git a/src/pywink/devices/factory.py b/src/pywink/devices/factory.py index a0bc79e..ad4314e 100644 --- a/src/pywink/devices/factory.py +++ b/src/pywink/devices/factory.py @@ -1,14 +1,15 @@ from pywink.devices.base import WinkDevice from pywink.devices.sensors import WinkSensorPod, WinkHub from pywink.devices.standard import WinkBulb, WinkBinarySwitch, WinkPowerStripOutlet, WinkLock, \ - WinkEggTray, WinkGarageDoor, WinkShade, WinkSiren, WinkKey, WinkThermostat + WinkEggTray, WinkGarageDoor, WinkShade, WinkSiren, WinkKey, WinkThermostat, \ + WinkFan +# pylint: disable=redefined-variable-type,too-many-branches def build_device(device_state_as_json, api_interface): 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: @@ -33,6 +34,10 @@ def build_device(device_state_as_json, api_interface): new_object = WinkKey(device_state_as_json, api_interface) elif "thermostat_id" in device_state_as_json: new_object = WinkThermostat(device_state_as_json, api_interface) + elif "fan_id" in device_state_as_json: + new_object = WinkFan(device_state_as_json, api_interface) + # This must be at the bottom most devices have a hub_id listed + # as their associated hub. elif "hub_id" in device_state_as_json: new_object = WinkHub(device_state_as_json, api_interface) diff --git a/src/pywink/devices/standard/__init__.py b/src/pywink/devices/standard/__init__.py index 2f1adec..29fe7c7 100644 --- a/src/pywink/devices/standard/__init__.py +++ b/src/pywink/devices/standard/__init__.py @@ -7,6 +7,7 @@ from pywink.devices.standard.base import WinkBinarySwitch from pywink.devices.standard.bulb import WinkBulb from pywink.devices.standard.thermostat import WinkThermostat +from pywink.devices.standard.fan import WinkFan class WinkEggTray(WinkDevice): @@ -334,4 +335,5 @@ def state(self): WinkShade.__name__, WinkSiren.__name__, WinkPorkfolioNose.__name__, - WinkThermostat.__name__] + WinkThermostat.__name__, + WinkFan.__name__] diff --git a/src/pywink/devices/standard/fan.py b/src/pywink/devices/standard/fan.py new file mode 100644 index 0000000..7233532 --- /dev/null +++ b/src/pywink/devices/standard/fan.py @@ -0,0 +1,119 @@ +from pywink.devices.standard.base import WinkDevice + + +# pylint: disable=too-many-public-methods +class WinkFan(WinkDevice): + """ + Represents a Wink fan + 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, api_interface): + super(WinkFan, self).__init__(device_state_as_json, api_interface, + objectprefix="fans") + + def device_id(self): + return self.json_state.get('fan_id', self.name()) + + def fan_speeds(self): + capabilities = self.json_state.get('capabilities', {}) + cap_fields = capabilities.get('fields', []) + fan_speeds = None + for field in cap_fields: + _field = field.get('field') + if _field == 'mode': + fan_speeds = field.get('choices') + return fan_speeds + + def fan_directions(self): + capabilities = self.json_state.get('capabilities', {}) + cap_fields = capabilities.get('fields', []) + fan_directions = None + for field in cap_fields: + _field = field.get('field') + if _field == 'direction': + fan_directions = field.get('choices') + return fan_directions + + def fan_timer_range(self): + capabilities = self.json_state.get('capabilities', {}) + cap_fields = capabilities.get('fields', []) + fan_timer_range = None + for field in cap_fields: + _field = field.get('field') + if _field == 'timer': + fan_timer_range = field.get('range') + return fan_timer_range + + def current_fan_speed(self): + return self._last_reading.get('mode', None) + + def current_fan_direction(self): + return self._last_reading.get('direction', None) + + def current_timer(self): + return self._last_reading.get('timer', None) + + def state(self): + return self._last_reading.get('powered', False) + + def set_state(self, state): + """ + :param powered: bool + :return: nothing + """ + desired_state = {"powered": state} + + response = self.api_interface.set_device_state(self, { + "desired_state": desired_state + }) + + self._update_state_from_response(response) + + def set_fan_speed(self, speed): + """ + :param speed: a string one of ["lowest", "low", + "medium", "high", "auto"] + :return: nothing + """ + desired_state = {"mode": speed} + + response = self.api_interface.set_device_state(self, { + "desired_state": desired_state + }) + + self._update_state_from_response(response) + + def set_fan_direction(self, direction): + """ + :param speed: a string one of ["forward", "reverse"] + :return: nothing + """ + desired_state = {"direction": direction} + + response = self.api_interface.set_device_state(self, { + "desired_state": desired_state + }) + + self._update_state_from_response(response) + + def set_fan_timer(self, timer): + """ + :param timer: an int between fan_timer_range + :return: nothing + """ + desired_state = {"timer": timer} + + resp = self.api_interface.set_device_state(self, { + "desired_state": desired_state + }) + + self._update_state_from_response(resp) + + def __repr__(self): + return "" % ( + self.name(), self.device_id()) diff --git a/src/pywink/devices/standard/thermostat.py b/src/pywink/devices/standard/thermostat.py index c9fe375..5089f0c 100644 --- a/src/pywink/devices/standard/thermostat.py +++ b/src/pywink/devices/standard/thermostat.py @@ -111,6 +111,10 @@ def fan_on(self): return False def has_fan(self): + cap_fields = self.json_state.get('capabilities').get('fields') + for field in cap_fields: + if field.get('field') == "fan_mode": + return True return self._last_reading.get('has_fan', False) def is_on(self): diff --git a/src/pywink/devices/types.py b/src/pywink/devices/types.py index 0bc3d78..097960b 100644 --- a/src/pywink/devices/types.py +++ b/src/pywink/devices/types.py @@ -12,6 +12,7 @@ SMOKE_DETECTOR = 'smoke_detector' THERMOSTAT = 'thermostat' HUB = 'hub' +FAN = 'fan' DEVICE_ID_KEYS = { BINARY_SWITCH: 'binary_switch_id', @@ -27,5 +28,6 @@ PIGGY_BANK: 'piggy_bank_id', SMOKE_DETECTOR: 'smoke_detector_id', THERMOSTAT: 'thermostat_id', - HUB: 'hub_id' + HUB: 'hub_id', + FAN: 'fan_id' } diff --git a/src/pywink/test/devices/standard/api_responses/fan.json b/src/pywink/test/devices/standard/api_responses/fan.json new file mode 100644 index 0000000..dd0dfda --- /dev/null +++ b/src/pywink/test/devices/standard/api_responses/fan.json @@ -0,0 +1,119 @@ +{ + "data":[ + { + "object_type":"fan", + "object_id":"1359", + "uuid":"2cf8d024-6838-4db6-82f6-0eca51123456", + "icon_id":null, + "icon_code":null, + "desired_state":{ + "mode":"lowest", + "powered":true, + "timer":0, + "direction":null + }, + "last_reading":{ + "mode":"lowest", + "mode_updated_at":1481335377.2255096, + "powered":true, + "powered_updated_at":1481335377.2255096, + "timer":0, + "timer_updated_at":1481335377.2255096, + "direction":"forward", + "direction_updated_at":null, + "connection":true, + "connection_updated_at":1481714301.2966304, + "firmware_version":"0.0b00 / 0.0b0e", + "firmware_version_updated_at":1481335377.2255096, + "firmware_date_code":null, + "firmware_date_code_updated_at":null, + "desired_mode_updated_at":1481335684.7678573, + "desired_powered_updated_at":1481335684.7678573, + "desired_timer_updated_at":1481335810.1496878, + "desired_direction_updated_at":1481335810.1496878 + }, + "subscription":{ + "pubnub":{ + "subscribe_key":"sub-c-f7bf7f7e-1234-11e3-a5e8-02ee2d123456", + "channel":"7ab843be9ac23f543f907798fc1508c214ee06f8|fan-1359|user-212345" + } + }, + "fan_id":"1359", + "name":"Fan", + "locale":"en_us", + "units":{ + + }, + "created_at":1481335344, + "hidden_at":null, + "capabilities":{ + "fields":[ + { + "type":"selection", + "field":"mode", + "choices":[ + "lowest", + "low", + "medium", + "high", + "auto" + ], + "mutability":"read-write" + }, + { + "type":"boolean", + "field":"powered", + "mutability":"read-write" + }, + { + "type":"integer", + "field":"timer", + "range":[ + 0, + 65535 + ], + "mutability":"read-write" + }, + { + "type":"selection", + "field":"direction", + "choices":[ + "forward", + "reverse" + ], + "mutability":"read-write" + }, + { + "type":"boolean", + "field":"connection", + "mutability":"read-only" + } + ] + }, + "triggers":[ + + ], + "manufacturer_device_model":"home_decorators_home_decorators_fan", + "manufacturer_device_id":null, + "device_manufacturer":"home_decorators", + "model_name":"Ceiling Fan", + "upc_id":"486", + "upc_code":"home_decorators_fan", + "gang_id":"56113", + "hub_id":"288962", + "local_id":"48.1", + "radio_type":"zigbee", + "lat_lng":[ + 12.345678, + -98.765432 + ], + "location":"" + } + ], + "errors":[ + + ], + "pagination":{ + + } +} diff --git a/src/pywink/test/devices/standard/fan_test.py b/src/pywink/test/devices/standard/fan_test.py new file mode 100644 index 0000000..8c6930e --- /dev/null +++ b/src/pywink/test/devices/standard/fan_test.py @@ -0,0 +1,93 @@ +import json +import os +import unittest + +import mock + +from pywink.api import get_devices_from_response_dict, WinkApiInterface +from pywink.devices import types as device_types +from pywink.devices.standard import WinkFan +from pywink.devices.types import DEVICE_ID_KEYS + + +class FanSpeedTests(unittest.TestCase): + + def test_fan_speed_should_be_lowest(self): + with open('{}/api_responses/fan.json'.format(os.path.dirname(__file__))) as fan_file: + response_dict = json.load(fan_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.FAN]) + + fan = devices[0] + """ :type fan: pywink.devices.standard.WinkFan """ + self.assertEqual(fan.current_fan_speed(), "lowest") + + def test_fan_speeds_should_be_present(self): + with open('{}/api_responses/fan.json'.format(os.path.dirname(__file__))) as fan_file: + response_dict = json.load(fan_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.FAN]) + + fan = devices[0] + """ :type fan: pywink.devices.standard.WinkFan """ + speeds = fan.fan_speeds() + self.assertTrue("lowest" in speeds) + self.assertTrue("low" in speeds) + self.assertTrue("medium" in speeds) + self.assertTrue("high" in speeds) + self.assertTrue("auto" in speeds) + + +class FanDirectionTests(unittest.TestCase): + + def test_fan_direction_should_be_forward(self): + with open('{}/api_responses/fan.json'.format(os.path.dirname(__file__))) as fan_file: + response_dict = json.load(fan_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.FAN]) + + fan = devices[0] + """ :type fan: pywink.devices.standard.WinkFan """ + self.assertEqual(fan.current_fan_direction(), "forward") + + def test_fan_directions_should_be_present(self): + with open('{}/api_responses/fan.json'.format(os.path.dirname(__file__))) as fan_file: + response_dict = json.load(fan_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.FAN]) + + fan = devices[0] + """ :type fan: pywink.devices.standard.WinkFan """ + directions = fan.fan_directions() + self.assertTrue("forward" in directions) + self.assertTrue("reverse" in directions) + +class FanTimerTests(unittest.TestCase): + + def test_fan_timer_range_should_be_present(self): + with open('{}/api_responses/fan.json'.format(os.path.dirname(__file__))) as fan_file: + response_dict = json.load(fan_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.FAN]) + + fan = devices[0] + """ :type fan: pywink.devices.standard.WinkFan """ + timer_range = fan.fan_timer_range() + self.assertTrue(0 in timer_range) + self.assertTrue(65535 in timer_range) + + def test_current_fan_timer_should_be_zero(self): + with open('{}/api_responses/fan.json'.format(os.path.dirname(__file__))) as fan_file: + response_dict = json.load(fan_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.FAN]) + + fan = devices[0] + """ :type fan: pywink.devices.standard.WinkFan """ + self.assertEqual(fan.current_timer(), 0) + + +class FanStateTests(unittest.TestCase): + + def test_fan_state_is_off(self): + with open('{}/api_responses/fan.json'.format(os.path.dirname(__file__))) as fan_file: + response_dict = json.load(fan_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.FAN]) + + fan = devices[0] + """ :type fan: pywink.devices.standard.WinkFan """ + self.assertTrue(fan.state()) From a5b9b2d950a76705a45f459ee3d1079ff7236f98 Mon Sep 17 00:00:00 2001 From: bradsk88 <7kcwKg60tdcW> Date: Tue, 3 Jan 2017 20:48:15 -0600 Subject: [PATCH 133/178] Up version --- CHANGELOG.md | 4 ++++ src/setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 132b8c8..cffc790 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Change Log +## 0.12.0 +- Wink fan support + + ## 0.11.0 - Support for Wink hubs as sensors - Added more generic attributes to base Wink device (manufacturer_device_model, manufacturer_device_id, device_manufacturer, model_name) diff --git a/src/setup.py b/src/setup.py index 508ddee..ee1dfed 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='0.11.0', + version='0.12.0', description='Access Wink devices via the Wink API', url='http://github.com/bradsk88/python-wink', author='Brad Johnson', From 7556a7c4079b7aeab5f3a6bf06eb0b7643adeee2 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Tue, 10 Jan 2017 22:38:05 -0500 Subject: [PATCH 134/178] Add support for missing features (#66) * Added missing lock, siren, and sensor features * Fixed lock properties. * Added tests * Added remote and button ids so they can be ignored while getting hub sensors --- CHANGELOG.md | 4 +- src/pywink/devices/factory.py | 6 +- src/pywink/devices/sensors.py | 7 ++ src/pywink/devices/standard/__init__.py | 111 ++++++++++++++++++ src/pywink/devices/types.py | 6 +- src/pywink/test/devices/standard/init_test.py | 72 ++++++++++++ src/setup.py | 2 +- 7 files changed, 200 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cffc790..0e518e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,11 @@ # Change Log +## 0.12.1 +- Added support for additional lock, garage door, siren, and sensor attributes/features + ## 0.12.0 - Wink fan support - ## 0.11.0 - Support for Wink hubs as sensors - Added more generic attributes to base Wink device (manufacturer_device_model, manufacturer_device_id, device_manufacturer, model_name) diff --git a/src/pywink/devices/factory.py b/src/pywink/devices/factory.py index ad4314e..8821460 100644 --- a/src/pywink/devices/factory.py +++ b/src/pywink/devices/factory.py @@ -1,5 +1,5 @@ from pywink.devices.base import WinkDevice -from pywink.devices.sensors import WinkSensorPod, WinkHub +from pywink.devices.sensors import WinkSensorPod from pywink.devices.standard import WinkBulb, WinkBinarySwitch, WinkPowerStripOutlet, WinkLock, \ WinkEggTray, WinkGarageDoor, WinkShade, WinkSiren, WinkKey, WinkThermostat, \ WinkFan @@ -36,9 +36,5 @@ def build_device(device_state_as_json, api_interface): new_object = WinkThermostat(device_state_as_json, api_interface) elif "fan_id" in device_state_as_json: new_object = WinkFan(device_state_as_json, api_interface) - # This must be at the bottom most devices have a hub_id listed - # as their associated hub. - elif "hub_id" in device_state_as_json: - new_object = WinkHub(device_state_as_json, api_interface) return new_object or WinkDevice(device_state_as_json, api_interface) diff --git a/src/pywink/devices/sensors.py b/src/pywink/devices/sensors.py index be5f038..61f78cb 100644 --- a/src/pywink/devices/sensors.py +++ b/src/pywink/devices/sensors.py @@ -31,6 +31,13 @@ def name(self): name += " " + self._capability return name + @property + def tamper_detected(self): + tamper = self._last_reading.get('tamper_detected', False) + if tamper is None: + tamper = False + return tamper + @property def battery_level(self): if not self._last_reading.get('external_power', None): diff --git a/src/pywink/devices/standard/__init__.py b/src/pywink/devices/standard/__init__.py index 29fe7c7..17b2a4d 100644 --- a/src/pywink/devices/standard/__init__.py +++ b/src/pywink/devices/standard/__init__.py @@ -57,6 +57,76 @@ def state(self): def device_id(self): return self.json_state.get('lock_id', self.name()) + @property + def alarm_enabled(self): + return self._last_reading.get('alarm_enabled', False) + + @property + def alarm_mode(self): + return self._last_reading.get('alarm_mode', None) + + @property + def vacation_mode_enabled(self): + return self._last_reading.get('vacation_mode_enabled', False) + + @property + def beeper_enabled(self): + return self._last_reading.get('beeper_enabled', False) + + @property + def auto_lock_enabled(self): + return self._last_reading.get('auto_lock_enabled', False) + + @property + def alarm_sensitivity(self): + return self._last_reading.get('alarm_sensitivity', None) + + def set_alarm_sensitivity(self, mode): + """ + :param mode: 1.0 for Very sensitive, 0.2 for not sensitive. + Steps in values of 0.2. + :return: nothing + """ + values = {"desired_state": {"alarm_sensitivity": mode}} + response = self.api_interface.set_device_state(self, values) + self._update_state_from_response(response) + + def set_alarm_mode(self, mode): + """ + :param mode: one of [None, "activity", "tamper", "forced_entry"] + :return: nothing + """ + values = {"desired_state": {"alarm_mode": mode}} + response = self.api_interface.set_device_state(self, values) + self._update_state_from_response(response) + + def set_alarm_state(self, state): + """ + :param state: a boolean of ture (on) or false ('off') + :return: nothing + """ + values = {"desired_state": {"alarm_enabled": state}} + response = self.api_interface.set_device_state(self, values) + self._update_state_from_response(response) + + def set_vacation_mode(self, state): + """ + :param state: a boolean of ture (on) or false ('off') + :return: nothing + """ + values = {"desired_state": {"vacation_mode_enabled": state}} + response = self.api_interface.set_device_state(self, values) + self._update_state_from_response(response) + + def set_beeper_mode(self, state): + """ + :param state: a boolean of ture (on) or false ('off') + :return: nothing + """ + values = {"desired_state": {"beeper_enabled": state}} + response = self.api_interface.set_device_state(self, values) + self._update_state_from_response(response) + def set_state(self, state): """ :param state: a boolean of true (on) or false ('off') @@ -171,6 +241,13 @@ def state(self): def device_id(self): return self.json_state.get('garage_door_id', self.name()) + @property + def tamper_detected(self): + tamper = self._last_reading.get('tamper_detected_true', False) + if tamper is None: + tamper = False + return tamper + def set_state(self, state): """ :param state: a number of 1 ('open') or 0 ('close') @@ -231,6 +308,40 @@ def __repr__(self): def device_id(self): return self.json_state.get('siren_id', self.name()) + @property + def mode(self): + return self._last_reading.get('mode', None) + + @property + def auto_shutoff(self): + return self._last_reading.get('auto_shutoff', None) + + def set_mode(self, mode): + """ + :param mode: a str, one of [siren_only, strobe_only, siren_and_strobe] + :return: nothing + """ + values = { + "desired_state": { + "mode": mode + } + } + response = self.api_interface.set_device_state(self, values) + self._update_state_from_response(response) + + def set_auto_shutoff(self, timer): + """ + :param time: an int, one of [None (never), 30, 60, 120] + :return: nothing + """ + values = { + "desired_state": { + "auto_shutoff": timer + } + } + response = self.api_interface.set_device_state(self, values) + self._update_state_from_response(response) + class WinkKey(WinkDevice): """ represents a wink.py key diff --git a/src/pywink/devices/types.py b/src/pywink/devices/types.py index 097960b..effce4c 100644 --- a/src/pywink/devices/types.py +++ b/src/pywink/devices/types.py @@ -13,6 +13,8 @@ THERMOSTAT = 'thermostat' HUB = 'hub' FAN = 'fan' +BUTTON = 'button' +REMOTE = 'remote' DEVICE_ID_KEYS = { BINARY_SWITCH: 'binary_switch_id', @@ -29,5 +31,7 @@ SMOKE_DETECTOR: 'smoke_detector_id', THERMOSTAT: 'thermostat_id', HUB: 'hub_id', - FAN: 'fan_id' + FAN: 'fan_id', + BUTTON: 'button_id', + REMOTE: 'remote_id' } diff --git a/src/pywink/test/devices/standard/init_test.py b/src/pywink/test/devices/standard/init_test.py index f5ceecd..8bc8616 100644 --- a/src/pywink/test/devices/standard/init_test.py +++ b/src/pywink/test/devices/standard/init_test.py @@ -69,6 +69,14 @@ def test_device_id_should_be_number(self): device_id = wink_garage_door.device_id() self.assertRegex(device_id, "^[0-9]{4,6}$") + def test_tamper_detected_should_be_false(self): + with open('{}/api_responses/garage_door.json'.format(os.path.dirname(__file__))) as garage_door_file: + response_dict = json.load(garage_door_file) + garage_door = response_dict.get('data')[0] + wink_garage_door = WinkGarageDoor(garage_door, self.api_interface) + tamper = wink_garage_door.tamper_detected + self.assertFalse(tamper) + class ShadeTests(unittest.TestCase): def setUp(self): @@ -113,6 +121,22 @@ def test_device_id_should_be_number(self): device_id = wink_siren.device_id() self.assertRegex(device_id, "^[0-9]{4,6}$") + def test_auto_shutoff_should_be_30(self): + with open('{}/api_responses/siren.json'.format(os.path.dirname(__file__))) as siren_file: + response_dict = json.load(siren_file) + siren = response_dict.get('data')[0] + wink_siren = WinkSiren(siren, self.api_interface) + auto_shutoff = wink_siren.auto_shutoff + self.assertEqual(auto_shutoff, 30) + + def test_mode_should_be_siren_and_strobe(self): + with open('{}/api_responses/siren.json'.format(os.path.dirname(__file__))) as siren_file: + response_dict = json.load(siren_file) + siren = response_dict.get('data')[0] + wink_siren = WinkSiren(siren, self.api_interface) + mode = wink_siren.mode + self.assertEqual(mode, "siren_and_strobe") + class LockTests(unittest.TestCase): @@ -135,6 +159,46 @@ def test_device_id_should_be_number(self): device_id = wink_lock.device_id() self.assertRegex(device_id, "^[0-9]{4,6}$") + def test_alarm_mode_should_be_null(self): + with open('{}/api_responses/lock.json'.format(os.path.dirname(__file__))) as lock_file: + response_dict = json.load(lock_file) + lock = response_dict.get('data')[0] + wink_lock = WinkLock(lock, self.api_interface) + alarm_mode = wink_lock.alarm_mode + self.assertEqual(alarm_mode, None) + + def test_alarm_sensitivity_should_be_6(self): + with open('{}/api_responses/lock.json'.format(os.path.dirname(__file__))) as lock_file: + response_dict = json.load(lock_file) + lock = response_dict.get('data')[0] + wink_lock = WinkLock(lock, self.api_interface) + alarm_sensitivity = wink_lock.alarm_sensitivity + self.assertEqual(alarm_sensitivity, 0.6) + + def test_alarm_enabled_should_be_true(self): + with open('{}/api_responses/lock.json'.format(os.path.dirname(__file__))) as lock_file: + response_dict = json.load(lock_file) + lock = response_dict.get('data')[0] + wink_lock = WinkLock(lock, self.api_interface) + alarm_enabled = wink_lock.alarm_enabled + self.assertTrue(alarm_enabled) + + def test_beeper_enabled_should_be_true(self): + with open('{}/api_responses/lock.json'.format(os.path.dirname(__file__))) as lock_file: + response_dict = json.load(lock_file) + lock = response_dict.get('data')[0] + wink_lock = WinkLock(lock, self.api_interface) + beeper_enabled = wink_lock.beeper_enabled + self.assertTrue(beeper_enabled) + + def test_vacation_mode_enabled_should_be_false(self): + with open('{}/api_responses/lock.json'.format(os.path.dirname(__file__))) as lock_file: + response_dict = json.load(lock_file) + lock = response_dict.get('data')[0] + wink_lock = WinkLock(lock, self.api_interface) + vacation_mode_enabled = wink_lock.vacation_mode_enabled + self.assertFalse(vacation_mode_enabled) + class BinarySwitchTests(unittest.TestCase): @@ -306,6 +370,14 @@ def test_battery_level_should_return_float(self): for sensor in sensors: self.assertEqual(sensor.battery_level, 0.86) + def test_sensor_tamper_detected_should_be_false(self): + response = ApiResponseJSONLoader('door_sensor_gocontrol.json').load() + devices = get_devices_from_response_dict(response, + DEVICE_ID_KEYS[ + device_types.SENSOR_POD]) + tamper = devices[0].tamper_detected + self.assertFalse(tamper) + def test_gocontrol_door_sensor_should_be_identified(self): response = ApiResponseJSONLoader('door_sensor_gocontrol.json').load() devices = get_devices_from_response_dict(response, diff --git a/src/setup.py b/src/setup.py index ee1dfed..653a4b2 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='0.12.0', + version='0.12.1', description='Access Wink devices via the Wink API', url='http://github.com/bradsk88/python-wink', author='Brad Johnson', From ff5f250e102afa91a3d9e2e0e02e51af2a4ae3c7 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Thu, 12 Jan 2017 00:16:13 -0500 Subject: [PATCH 135/178] Support for Ring door bell motion and button press (#67) * Support for Ring door bell motion and button press. --- CHANGELOG.md | 3 + src/pywink/__init__.py | 2 +- src/pywink/api.py | 27 ++++++- src/pywink/devices/sensors.py | 56 +++++++++++++ src/pywink/devices/types.py | 4 +- .../api_responses/ring_door_bell.json | 80 +++++++++++++++++++ src/pywink/test/devices/standard/init_test.py | 33 +++++++- src/setup.py | 2 +- 8 files changed, 200 insertions(+), 7 deletions(-) create mode 100644 src/pywink/test/devices/standard/api_responses/ring_door_bell.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e518e7..5d2ed14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 0.13.0 +- Support for Ring door bell motion and button press. + ## 0.12.1 - Added support for additional lock, garage door, siren, and sensor attributes/features diff --git a/src/pywink/__init__.py b/src/pywink/__init__.py index c7ae03e..b6bb4df 100644 --- a/src/pywink/__init__.py +++ b/src/pywink/__init__.py @@ -13,4 +13,4 @@ from pywink.api import get_eggtrays, get_sensors, \ get_keys, get_piggy_banks, get_smoke_and_co_detectors, \ - get_hubs + get_hubs, get_door_bells diff --git a/src/pywink/api.py b/src/pywink/api.py index 197c00e..21f7c5c 100644 --- a/src/pywink/api.py +++ b/src/pywink/api.py @@ -9,7 +9,7 @@ WinkTemperatureSensor, WinkVibrationPresenceSensor, \ WinkLiquidPresenceSensor, WinkCurrencySensor, WinkMotionSensor, \ WinkPresenceSensor, WinkProximitySensor, WinkSmokeDetector, \ - WinkCoDetector, WinkHub + WinkCoDetector, WinkHub, WinkDoorBellButton, WinkDoorBellMotion from pywink.devices.types import DEVICE_ID_KEYS API_HEADERS = {} @@ -190,6 +190,10 @@ def get_fans(): return get_devices(device_types.FAN) +def get_door_bells(): + return get_devices(device_types.DOOR_BELL) + + def get_subscription_key(): response_dict = wink_api_fetch() first_device = response_dict.get('data')[0] @@ -232,7 +236,7 @@ def get_devices_from_response_dict(response_dict, filter_key): api_interface = WinkApiInterface() keys = ['powerstrip_id', 'sensor_pod_id', 'piggy_bank_id', - 'smoke_detector_id', 'hub_id'] + 'smoke_detector_id', 'hub_id', 'door_bell_id'] for item in items: if item.get(filter_key, None) is None: @@ -245,6 +249,7 @@ def get_devices_from_response_dict(response_dict, filter_key): devices.extend(__get_devices_from_piggy_bank(item, api_interface, filter_key)) devices.extend(__get_subsensors_from_smoke_detector(item, api_interface, filter_key)) devices.extend(__get_sensor_from_hub(item, api_interface, filter_key)) + devices.extend(__get_subsensors_from_door_bell(item, api_interface, filter_key)) else: devices.append(build_device(item, api_interface)) @@ -344,6 +349,24 @@ def __get_subsensors_from_smoke_detector(item, api_interface, filter_key): return subsensors +def __get_subsensors_from_door_bell(item, api_interface, filter_key): + if filter_key != 'door_bell_id': + return [] + + capabilities = [cap['field'] for cap in item.get('capabilities', {}).get('fields', [])] + + if not capabilities: + return [] + + subsensors = [] + + if WinkDoorBellMotion.CAPABILITY in capabilities: + subsensors.append(WinkDoorBellMotion(item, api_interface)) + if WinkDoorBellButton.CAPABILITY in capabilities: + subsensors.append(WinkDoorBellButton(item, api_interface)) + return subsensors + + def __device_is_visible(item, key): is_correctly_structured = bool(item.get(key)) is_visible = not item.get('hidden_at') diff --git a/src/pywink/devices/sensors.py b/src/pywink/devices/sensors.py index 61f78cb..e21715c 100644 --- a/src/pywink/devices/sensors.py +++ b/src/pywink/devices/sensors.py @@ -383,3 +383,59 @@ def update_state(self): root_name = self.json_state.get('hub_id', self.name()) response = self.api_interface.get_device_state(self, root_name) self._update_state_from_response(response) + + +class WinkDoorBellButton(_WinkCapabilitySensor): + + CAPABILITY = 'button_pressed' + UNIT = None + + def __init__(self, device_state_as_json, api_interface): + super(WinkDoorBellButton, self).__init__(device_state_as_json, api_interface, + self.CAPABILITY, + self.UNIT, 'door_bells') + + def button_pressed_boolean(self): + """ + :return: Returns True if button was pressed. + :rtype: bool + """ + return self.last_reading() + + def device_id(self): + root_name = self.json_state.get('door_bell_id', None) + return '{}+{}'.format(root_name, self._capability) + + def update_state(self): + """ Update state with latest info from Wink API. """ + root_name = self.json_state.get('door_bell_id', self.name()) + response = self.api_interface.get_device_state(self, root_name) + self._update_state_from_response(response) + + +class WinkDoorBellMotion(_WinkCapabilitySensor): + + CAPABILITY = 'motion' + UNIT = None + + def __init__(self, device_state_as_json, api_interface): + super(WinkDoorBellMotion, self).__init__(device_state_as_json, api_interface, + self.CAPABILITY, + self.UNIT, 'door_bells') + + def motion_boolean(self): + """ + :return: Returns True if motion is detected. + :rtype: bool + """ + return self.last_reading() + + def device_id(self): + root_name = self.json_state.get('door_bell_id', None) + return '{}+{}'.format(root_name, self._capability) + + def update_state(self): + """ Update state with latest info from Wink API. """ + root_name = self.json_state.get('door_bell_id', self.name()) + response = self.api_interface.get_device_state(self, root_name) + self._update_state_from_response(response) diff --git a/src/pywink/devices/types.py b/src/pywink/devices/types.py index effce4c..fdbbe39 100644 --- a/src/pywink/devices/types.py +++ b/src/pywink/devices/types.py @@ -15,6 +15,7 @@ FAN = 'fan' BUTTON = 'button' REMOTE = 'remote' +DOOR_BELL = 'door_bell' DEVICE_ID_KEYS = { BINARY_SWITCH: 'binary_switch_id', @@ -33,5 +34,6 @@ HUB: 'hub_id', FAN: 'fan_id', BUTTON: 'button_id', - REMOTE: 'remote_id' + REMOTE: 'remote_id', + DOOR_BELL: 'door_bell_id' } diff --git a/src/pywink/test/devices/standard/api_responses/ring_door_bell.json b/src/pywink/test/devices/standard/api_responses/ring_door_bell.json new file mode 100644 index 0000000..927a932 --- /dev/null +++ b/src/pywink/test/devices/standard/api_responses/ring_door_bell.json @@ -0,0 +1,80 @@ +{ + "data":[ + { + "uuid":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "desired_state":{ + + }, + "last_reading":{ + "battery":1.0, + "battery_updated_at":1483588707.0458965, + "motion":false, + "motion_updated_at":1483717738.8899395, + "button_pressed":false, + "button_pressed_updated_at":1483658161.5844142, + "motion_true":"N/A", + "motion_true_updated_at":1483717728.305417, + "button_pressed_true":"N/A", + "button_pressed_true_updated_at":1483658092.2301679, + "connection":true, + "connection_updated_at":1442873339.2134194, + "last_recording_cuepoint_id":"1722851976", + "last_recording_cuepoint_id_updated_at":1483717787.6740644, + "motion_changed_at":1483717738.8899395, + "motion_true_changed_at":1483717728.305417, + "button_pressed_changed_at":1483658161.5844142, + "button_pressed_true_changed_at":1483658092.2301679, + "battery_changed_at":1483588707.0458965, + "last_recording_cuepoint_id_changed_at":1483717787.6740644 + }, + "subscription":{ + "pubnub":{ + "subscribe_key":"sub-c-xxxxxxxx-xxxx-xxxxxxx-xxxxxxxxxxxx", + "channel":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx|door_bell-xx|user-xxxxxx" + } + }, + "door_bell_id":"123456", + "name":"Home", + "locale":"en_us", + "units":{ + + }, + "created_at":1442873339, + "hidden_at":null, + "capabilities":{ + "fields":[ + { + "type":"percentage", + "field":"battery", + "mutability":"read-only" + }, + { + "type":"boolean", + "field":"motion", + "mutability":"read-only" + }, + { + "type":"boolean", + "field":"button_pressed", + "mutability":"read-only" + } + ] + }, + "user_ids":[ + "xxxxxx" + ], + "manufacturer_device_model":"doorbell", + "manufacturer_device_id":"12345", + "device_manufacturer":"ring", + "model_name":"Ring Video Doorbell", + "upc_id":"xxx", + "upc_code":"xxxxxxxxxxxx", + "linked_service_id":"xxxxxx", + "lat_lng":[ + 98.765432, + -12.345678 + ], + "location":"1234 Main St, City, ST XXXXX, USA" + } + ] +} diff --git a/src/pywink/test/devices/standard/init_test.py b/src/pywink/test/devices/standard/init_test.py index 8bc8616..ca8c042 100644 --- a/src/pywink/test/devices/standard/init_test.py +++ b/src/pywink/test/devices/standard/init_test.py @@ -9,7 +9,7 @@ WinkSoundPresenceSensor, WinkVibrationPresenceSensor, WinkTemperatureSensor, \ _WinkCapabilitySensor, WinkLiquidPresenceSensor, WinkCurrencySensor, WinkMotionSensor, \ WinkProximitySensor, WinkPresenceSensor, WinkSmokeDetector, WinkCoDetector, \ - WinkHub + WinkHub, WinkDoorBellMotion, WinkDoorBellButton from pywink.devices.standard import WinkGarageDoor, WinkPowerStripOutlet, WinkSiren, WinkLock, \ WinkShade, WinkBinarySwitch, WinkEggTray, WinkKey, WinkPorkfolioNose from pywink.devices.types import DEVICE_ID_KEYS @@ -589,7 +589,7 @@ def setUp(self): super(HubTests, self).setUp() self.api_interface = mock.MagicMock() - def test_should_hub_response(self): + def test_should_handle_hub_response(self): with open('{}/api_responses/v1_hub.json'.format(os.path.dirname(__file__))) as hub_file: response_dict = json.load(hub_file) devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.HUB]) @@ -667,3 +667,32 @@ def test_model_name(self): wink_hub = WinkHub(hub, self.api_interface) model_name = wink_hub.model_name self.assertEqual(model_name, 'Hub') + + +class DoorBellTests(unittest.TestCase): + + def setUp(self): + super(DoorBellTests, self).setUp() + self.api_interface = mock.MagicMock() + + def test_should_handle_door_bell_response(self): + with open('{}/api_responses/ring_door_bell.json'.format(os.path.dirname(__file__))) as door_bell_file: + response_dict = json.load(door_bell_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.DOOR_BELL]) + self.assertEqual(2, len(devices)) + self.assertIsInstance(devices[0], WinkDoorBellMotion) + self.assertIsInstance(devices[1], WinkDoorBellButton) + + def test_door_bell_motion_should_be_false(self): + with open('{}/api_responses/ring_door_bell.json'.format(os.path.dirname(__file__))) as door_bell_file: + response_dict = json.load(door_bell_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.DOOR_BELL]) + door_bell_motion = devices[0].motion_boolean() + self.assertFalse(door_bell_motion) + + def test_door_bell_button_pressed_should_be_false(self): + with open('{}/api_responses/ring_door_bell.json'.format(os.path.dirname(__file__))) as door_bell_file: + response_dict = json.load(door_bell_file) + devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.DOOR_BELL]) + door_bell_button_pressed = devices[1].button_pressed_boolean() + self.assertFalse(door_bell_button_pressed) diff --git a/src/setup.py b/src/setup.py index 653a4b2..7a02aec 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='0.12.1', + version='0.13.0', description='Access Wink devices via the Wink API', url='http://github.com/bradsk88/python-wink', author='Brad Johnson', From a2130efa83aa37e41fba2358a2ad9a7be2caf2f8 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Sun, 22 Jan 2017 12:04:06 -0500 Subject: [PATCH 136/178] Switch to object type (version 1.0.0) (#68) * python-wink version 1.0.0 --- .coveragerc | 8 + CHANGELOG.md | 12 + script/__init__.py | 0 src/pywink/__init__.py | 9 +- src/pywink/api.py | 239 ++---- src/pywink/devices/base.py | 49 +- src/pywink/devices/binary_switch.py | 29 + src/pywink/devices/button.py | 26 + src/pywink/devices/camera.py | 40 + src/pywink/devices/eggtray.py | 22 + src/pywink/devices/factory.py | 151 +++- src/pywink/devices/{standard => }/fan.py | 20 +- src/pywink/devices/gang.py | 19 + src/pywink/devices/garage_door.py | 28 + src/pywink/devices/hub.py | 30 + src/pywink/devices/key.py | 32 + .../{standard/bulb.py => light_bulb.py} | 29 +- src/pywink/devices/lock.py | 86 +++ src/pywink/devices/piggy_bank.py | 81 ++ src/pywink/devices/powerstrip.py | 95 +++ src/pywink/devices/remote.py | 37 + src/pywink/devices/sensor.py | 43 ++ src/pywink/devices/sensors.py | 441 ----------- src/pywink/devices/shade.py | 22 + src/pywink/devices/siren.py | 52 ++ src/pywink/devices/smoke_detector.py | 109 +++ src/pywink/devices/sprinkler.py | 20 + src/pywink/devices/standard/__init__.py | 450 ----------- src/pywink/devices/standard/base.py | 45 -- .../devices/{standard => }/thermostat.py | 33 +- src/pywink/devices/types.py | 43 +- src/pywink/test/__init__.py | 17 + src/pywink/test/api_test.py | 424 +++++++++++ .../test/devices/api_responses/canary.json | 100 +++ .../test/devices/api_responses/door_bell.json | 77 ++ .../api_responses/ecobee_thermostat.json | 279 +++++++ .../test/devices/api_responses/eggminder.json | 108 +++ .../test/devices/api_responses/fan.json | 109 +++ .../devices/api_responses/garage_door.json | 64 ++ .../devices/api_responses/ge_link_bulb.json | 61 ++ .../api_responses/ge_zwave_switch.json | 64 ++ .../generic_liquid_detected_sensor.json | 70 ++ .../go_control_door_window_sensor.json | 78 ++ .../go_control_motion_temperature_sensor.json | 88 +++ .../api_responses/go_control_siren.json | 55 ++ .../api_responses/go_control_thermostat.json | 193 +++++ .../test/devices/api_responses/hue_hub.json | 81 ++ .../api_responses/kidde_smoke_detector.json | 51 ++ .../api_responses/lightify_rgbw_bulb.json | 131 ++++ .../lightify_temperature_bulb.json | 111 +++ .../test/devices/api_responses/lock_key.json | 28 + .../lutron_connected_bulb_remote.json | 62 ++ .../api_responses/lutron_pico_remote.json | 62 ++ .../test/devices/api_responses/nest.json | 247 +++++++ .../api_responses/nest_smoke_co_detector.json | 51 ++ .../api_responses/pivot_power_genius.json | 87 +++ .../test/devices/api_responses/porkfoilo.json | 87 +++ .../api_responses/relay_switch_dump.json | 78 ++ .../api_responses/relay_switch_smart.json | 76 ++ .../devices/api_responses/schlage_lock.json | 163 ++++ .../api_responses/sensi_thermostat.json | 226 ++++++ .../test/devices/api_responses/shade.json | 51 ++ .../devices/api_responses/spotter_v1.json | 119 +++ .../test/devices/api_responses/sprinkler.json | 61 ++ .../devices/api_responses/v1_wink_hub.json | 106 +++ .../devices/api_responses/v2_wink_hub.json | 96 +++ .../api_responses/wink_relay_button.json | 48 ++ .../api_responses/wink_relay_gang.json | 40 + .../devices/api_responses/wink_relay_hub.json | 93 +++ .../api_responses/wink_relay_sensor.json | 89 +++ src/pywink/test/devices/base_test.py | 145 ++++ src/pywink/test/devices/fan_test.py | 62 ++ src/pywink/test/devices/garage_door_test.py | 37 + src/pywink/test/devices/hub_test.py | 61 ++ src/pywink/test/devices/light_bulb_test.py | 42 ++ src/pywink/test/devices/lock_test.py | 49 ++ src/pywink/test/devices/powerstrip_test.py | 65 ++ src/pywink/test/devices/sensor_test.py | 220 ++++++ src/pywink/test/devices/siren_test.py | 33 + src/pywink/test/devices/standard/__init__.py | 0 .../standard/api_responses/__init__.py | 14 - .../standard/api_responses/binary_sensor.json | 67 -- .../standard/api_responses/binary_switch.json | 62 -- .../api_responses/device_with_pubnub.json | 183 ----- .../api_responses/door_sensor_gocontrol.json | 87 --- .../standard/api_responses/eggtray.json | 80 -- .../devices/standard/api_responses/fan.json | 119 --- .../standard/api_responses/garage_door.json | 64 -- .../api_responses/gocontrol_thermostat.json | 181 ----- .../hue_and_saturation_absent.json | 118 --- .../hue_and_saturation_present.json | 129 ---- .../devices/standard/api_responses/key.json | 33 - .../standard/api_responses/light_bulb.json | 41 - ...light_bulb_with_desired_state_reached.json | 128 ---- .../light_switch_ge_jasco_z_wave.json | 71 -- .../standard/api_responses/liquid_sensor.json | 83 --- .../devices/standard/api_responses/lock.json | 136 ---- .../motion_sensor_gocontrol.json | 99 --- .../devices/standard/api_responses/nest.json | 241 ------ .../standard/api_responses/porkfolio.json | 94 --- .../standard/api_responses/power_strip.json | 75 -- .../api_responses/quirky_spotter.json | 123 --- .../api_responses/quirky_spotter_2.json | 116 --- .../api_responses/quirky_spotter_pubnub.json | 134 ---- .../standard/api_responses/rgb_absent.json | 119 --- .../standard/api_responses/rgb_present.json | 119 --- .../api_responses/ring_door_bell.json | 80 -- .../devices/standard/api_responses/sensi.json | 234 ------ .../devices/standard/api_responses/shade.json | 44 -- .../devices/standard/api_responses/siren.json | 63 -- .../api_responses/smoke_detector.json | 61 -- .../api_responses/temperature_absent.json | 119 --- .../api_responses/temperature_present.json | 129 ---- .../standard/api_responses/v1_hub.json | 97 --- .../api_responses/wink_relay_sensor.json | 107 --- .../standard/api_responses/xy_absent.json | 119 --- .../standard/api_responses/xy_present.json | 119 --- src/pywink/test/devices/standard/bulb_test.py | 260 ------- src/pywink/test/devices/standard/fan_test.py | 93 --- src/pywink/test/devices/standard/init_test.py | 698 ------------------ .../test/devices/standard/thermostat_test.py | 299 -------- src/pywink/test/devices/switch_test.py | 21 + src/pywink/test/devices/thermostat_test.py | 300 ++++++++ src/setup.py | 4 +- 124 files changed, 6107 insertions(+), 6501 deletions(-) mode change 100644 => 100755 script/__init__.py create mode 100644 src/pywink/devices/binary_switch.py create mode 100644 src/pywink/devices/button.py create mode 100644 src/pywink/devices/camera.py create mode 100644 src/pywink/devices/eggtray.py rename src/pywink/devices/{standard => }/fan.py (85%) create mode 100644 src/pywink/devices/gang.py create mode 100644 src/pywink/devices/garage_door.py create mode 100644 src/pywink/devices/hub.py create mode 100644 src/pywink/devices/key.py rename src/pywink/devices/{standard/bulb.py => light_bulb.py} (88%) create mode 100644 src/pywink/devices/lock.py create mode 100644 src/pywink/devices/piggy_bank.py create mode 100644 src/pywink/devices/powerstrip.py create mode 100644 src/pywink/devices/remote.py create mode 100644 src/pywink/devices/sensor.py delete mode 100644 src/pywink/devices/sensors.py create mode 100644 src/pywink/devices/shade.py create mode 100644 src/pywink/devices/siren.py create mode 100644 src/pywink/devices/smoke_detector.py create mode 100644 src/pywink/devices/sprinkler.py delete mode 100644 src/pywink/devices/standard/__init__.py delete mode 100644 src/pywink/devices/standard/base.py rename src/pywink/devices/{standard => }/thermostat.py (84%) create mode 100644 src/pywink/test/api_test.py create mode 100644 src/pywink/test/devices/api_responses/canary.json create mode 100644 src/pywink/test/devices/api_responses/door_bell.json create mode 100644 src/pywink/test/devices/api_responses/ecobee_thermostat.json create mode 100644 src/pywink/test/devices/api_responses/eggminder.json create mode 100644 src/pywink/test/devices/api_responses/fan.json create mode 100644 src/pywink/test/devices/api_responses/garage_door.json create mode 100644 src/pywink/test/devices/api_responses/ge_link_bulb.json create mode 100644 src/pywink/test/devices/api_responses/ge_zwave_switch.json create mode 100644 src/pywink/test/devices/api_responses/generic_liquid_detected_sensor.json create mode 100644 src/pywink/test/devices/api_responses/go_control_door_window_sensor.json create mode 100644 src/pywink/test/devices/api_responses/go_control_motion_temperature_sensor.json create mode 100644 src/pywink/test/devices/api_responses/go_control_siren.json create mode 100644 src/pywink/test/devices/api_responses/go_control_thermostat.json create mode 100644 src/pywink/test/devices/api_responses/hue_hub.json create mode 100644 src/pywink/test/devices/api_responses/kidde_smoke_detector.json create mode 100644 src/pywink/test/devices/api_responses/lightify_rgbw_bulb.json create mode 100644 src/pywink/test/devices/api_responses/lightify_temperature_bulb.json create mode 100644 src/pywink/test/devices/api_responses/lock_key.json create mode 100644 src/pywink/test/devices/api_responses/lutron_connected_bulb_remote.json create mode 100644 src/pywink/test/devices/api_responses/lutron_pico_remote.json create mode 100644 src/pywink/test/devices/api_responses/nest.json create mode 100644 src/pywink/test/devices/api_responses/nest_smoke_co_detector.json create mode 100644 src/pywink/test/devices/api_responses/pivot_power_genius.json create mode 100644 src/pywink/test/devices/api_responses/porkfoilo.json create mode 100644 src/pywink/test/devices/api_responses/relay_switch_dump.json create mode 100644 src/pywink/test/devices/api_responses/relay_switch_smart.json create mode 100644 src/pywink/test/devices/api_responses/schlage_lock.json create mode 100644 src/pywink/test/devices/api_responses/sensi_thermostat.json create mode 100644 src/pywink/test/devices/api_responses/shade.json create mode 100644 src/pywink/test/devices/api_responses/spotter_v1.json create mode 100644 src/pywink/test/devices/api_responses/sprinkler.json create mode 100644 src/pywink/test/devices/api_responses/v1_wink_hub.json create mode 100644 src/pywink/test/devices/api_responses/v2_wink_hub.json create mode 100644 src/pywink/test/devices/api_responses/wink_relay_button.json create mode 100644 src/pywink/test/devices/api_responses/wink_relay_gang.json create mode 100644 src/pywink/test/devices/api_responses/wink_relay_hub.json create mode 100644 src/pywink/test/devices/api_responses/wink_relay_sensor.json create mode 100644 src/pywink/test/devices/base_test.py create mode 100644 src/pywink/test/devices/fan_test.py create mode 100644 src/pywink/test/devices/garage_door_test.py create mode 100644 src/pywink/test/devices/hub_test.py create mode 100644 src/pywink/test/devices/light_bulb_test.py create mode 100644 src/pywink/test/devices/lock_test.py create mode 100644 src/pywink/test/devices/powerstrip_test.py create mode 100644 src/pywink/test/devices/sensor_test.py create mode 100644 src/pywink/test/devices/siren_test.py delete mode 100644 src/pywink/test/devices/standard/__init__.py delete mode 100644 src/pywink/test/devices/standard/api_responses/__init__.py delete mode 100644 src/pywink/test/devices/standard/api_responses/binary_sensor.json delete mode 100644 src/pywink/test/devices/standard/api_responses/binary_switch.json delete mode 100644 src/pywink/test/devices/standard/api_responses/device_with_pubnub.json delete mode 100644 src/pywink/test/devices/standard/api_responses/door_sensor_gocontrol.json delete mode 100644 src/pywink/test/devices/standard/api_responses/eggtray.json delete mode 100644 src/pywink/test/devices/standard/api_responses/fan.json delete mode 100644 src/pywink/test/devices/standard/api_responses/garage_door.json delete mode 100644 src/pywink/test/devices/standard/api_responses/gocontrol_thermostat.json delete mode 100644 src/pywink/test/devices/standard/api_responses/hue_and_saturation_absent.json delete mode 100644 src/pywink/test/devices/standard/api_responses/hue_and_saturation_present.json delete mode 100644 src/pywink/test/devices/standard/api_responses/key.json delete mode 100644 src/pywink/test/devices/standard/api_responses/light_bulb.json delete mode 100644 src/pywink/test/devices/standard/api_responses/light_bulb_with_desired_state_reached.json delete mode 100644 src/pywink/test/devices/standard/api_responses/light_switch_ge_jasco_z_wave.json delete mode 100644 src/pywink/test/devices/standard/api_responses/liquid_sensor.json delete mode 100644 src/pywink/test/devices/standard/api_responses/lock.json delete mode 100644 src/pywink/test/devices/standard/api_responses/motion_sensor_gocontrol.json delete mode 100644 src/pywink/test/devices/standard/api_responses/nest.json delete mode 100644 src/pywink/test/devices/standard/api_responses/porkfolio.json delete mode 100644 src/pywink/test/devices/standard/api_responses/power_strip.json delete mode 100644 src/pywink/test/devices/standard/api_responses/quirky_spotter.json delete mode 100644 src/pywink/test/devices/standard/api_responses/quirky_spotter_2.json delete mode 100644 src/pywink/test/devices/standard/api_responses/quirky_spotter_pubnub.json delete mode 100644 src/pywink/test/devices/standard/api_responses/rgb_absent.json delete mode 100644 src/pywink/test/devices/standard/api_responses/rgb_present.json delete mode 100644 src/pywink/test/devices/standard/api_responses/ring_door_bell.json delete mode 100644 src/pywink/test/devices/standard/api_responses/sensi.json delete mode 100644 src/pywink/test/devices/standard/api_responses/shade.json delete mode 100644 src/pywink/test/devices/standard/api_responses/siren.json delete mode 100644 src/pywink/test/devices/standard/api_responses/smoke_detector.json delete mode 100644 src/pywink/test/devices/standard/api_responses/temperature_absent.json delete mode 100644 src/pywink/test/devices/standard/api_responses/temperature_present.json delete mode 100644 src/pywink/test/devices/standard/api_responses/v1_hub.json delete mode 100644 src/pywink/test/devices/standard/api_responses/wink_relay_sensor.json delete mode 100644 src/pywink/test/devices/standard/api_responses/xy_absent.json delete mode 100644 src/pywink/test/devices/standard/api_responses/xy_present.json delete mode 100644 src/pywink/test/devices/standard/bulb_test.py delete mode 100644 src/pywink/test/devices/standard/fan_test.py delete mode 100644 src/pywink/test/devices/standard/init_test.py delete mode 100644 src/pywink/test/devices/standard/thermostat_test.py create mode 100644 src/pywink/test/devices/switch_test.py create mode 100644 src/pywink/test/devices/thermostat_test.py diff --git a/.coveragerc b/.coveragerc index 54fd6ad..906139c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,2 +1,10 @@ [run] omit = src/setup.py, src/*/__init__.py + +[report] +exclude_lines = + pragma: no cover + def __repr__ + def __str__ + raise AssertionError + raise NotImplementedError diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d2ed14..e4f0cbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Change Log +## 1.0.0 +- Switch to object_type for device type detction +- Hard coded user agent +- Support for Lutron connected bulb remotes +- Support for Sprinklers +- Support for main Powerstrip device +- Support for Wink Relay buttons +- Support for Smoke and CO severity sensors +- Support for Canary cameras +- Throttle API calls to /users/me/wink_devices to once every 60 seconds +- Sensor object are built based on capability not returned object_type + ## 0.13.0 - Support for Ring door bell motion and button press. diff --git a/script/__init__.py b/script/__init__.py old mode 100644 new mode 100755 diff --git a/src/pywink/__init__.py b/src/pywink/__init__.py index b6bb4df..e085d06 100644 --- a/src/pywink/__init__.py +++ b/src/pywink/__init__.py @@ -7,10 +7,11 @@ get_set_access_token, is_token_set, get_devices, \ get_subscription_key -from pywink.api import get_bulbs, get_garage_doors, get_locks, \ - get_powerstrip_outlets, get_shades, get_sirens, \ +from pywink.api import get_light_bulbs, get_garage_doors, get_locks, \ + get_powerstrips, get_shades, get_sirens, \ get_switches, get_thermostats, get_fans -from pywink.api import get_eggtrays, get_sensors, \ +from pywink.api import get_all_devices, get_eggtrays, get_sensors, \ get_keys, get_piggy_banks, get_smoke_and_co_detectors, \ - get_hubs, get_door_bells + get_hubs, get_door_bells, get_remotes, get_sprinklers, get_buttons, \ + get_gangs, get_cameras diff --git a/src/pywink/api.py b/src/pywink/api.py index 21f7c5c..ed6f8e4 100644 --- a/src/pywink/api.py +++ b/src/pywink/api.py @@ -1,39 +1,36 @@ import json +import time import requests from pywink.devices import types as device_types from pywink.devices.factory import build_device -from pywink.devices.standard import WinkPorkfolioNose -from pywink.devices.sensors import WinkSensorPod, WinkHumiditySensor, WinkBrightnessSensor, WinkSoundPresenceSensor, \ - WinkTemperatureSensor, WinkVibrationPresenceSensor, \ - WinkLiquidPresenceSensor, WinkCurrencySensor, WinkMotionSensor, \ - WinkPresenceSensor, WinkProximitySensor, WinkSmokeDetector, \ - WinkCoDetector, WinkHub, WinkDoorBellButton, WinkDoorBellMotion -from pywink.devices.types import DEVICE_ID_KEYS API_HEADERS = {} CLIENT_ID = None CLIENT_SECRET = None REFRESH_TOKEN = None -USER_AGENT = None +USER_AGENT = "Manufacturer/python-wink python/3 Wink/3" +ALL_DEVICES = None +LAST_UPDATE = None class WinkApiInterface(object): BASE_URL = "https://api.wink.com" - def set_device_state(self, device, state, id_override=None): + def set_device_state(self, device, state, id_override=None, type_override=None): """ :type device: WinkDevice :param state: a boolean of true (on) or false ('off') :return: The JSON response from the API (new device state) """ - _id = device.device_id() - if id_override: - _id = id_override - url_string = "{}/{}/{}".format(self.BASE_URL, - device.objectprefix, _id) + object_id = id_override or device.object_id() + object_type = type_override or device.object_type() + url_string = "{}/{}s/{}".format(self.BASE_URL, + object_type, + object_id) + print(url_string) arequest = requests.put(url_string, data=json.dumps(state), headers=API_HEADERS) @@ -45,15 +42,17 @@ def set_device_state(self, device, state, id_override=None): headers=API_HEADERS) else: raise WinkAPIException("Failed to refresh access token.") + print(str(arequest.json())) return arequest.json() - def get_device_state(self, device, id_override=None): + def get_device_state(self, device, id_override=None, type_override=None): """ :type device: WinkDevice """ - device_id = id_override or device.device_id() - url_string = "{}/{}/{}".format(self.BASE_URL, - device.objectprefix, device_id) + object_id = id_override or device.object_id() + object_type = type_override or device.object_type() + url_string = "{}/{}s/{}".format(self.BASE_URL, + object_type, object_id) arequest = requests.get(url_string, headers=API_HEADERS) return arequest.json() @@ -130,7 +129,16 @@ def refresh_access_token(): return None -def get_bulbs(): +def is_token_set(): + """ Returns if an auth token has been set. """ + return bool(API_HEADERS) + + +def get_all_devices(): + return get_devices(device_types.ALL_SUPPORTED_DEVICES) + + +def get_light_bulbs(): return get_devices(device_types.LIGHT_BULB) @@ -147,7 +155,7 @@ def get_locks(): def get_eggtrays(): - return get_devices(device_types.EGG_TRAY) + return get_devices(device_types.EGGTRAY) def get_garage_doors(): @@ -158,8 +166,8 @@ def get_shades(): return get_devices(device_types.SHADE) -def get_powerstrip_outlets(): - return get_devices(device_types.POWER_STRIP) +def get_powerstrips(): + return get_devices(device_types.POWERSTRIP) def get_sirens(): @@ -194,6 +202,26 @@ def get_door_bells(): return get_devices(device_types.DOOR_BELL) +def get_remotes(): + return get_devices(device_types.REMOTE) + + +def get_sprinklers(): + return get_devices(device_types.SPRINKLER) + + +def get_buttons(): + return get_devices(device_types.BUTTON) + + +def get_gangs(): + return get_devices(device_types.GANG) + + +def get_cameras(): + return get_devices(device_types.CAMERA) + + def get_subscription_key(): response_dict = wink_api_fetch() first_device = response_dict.get('data')[0] @@ -220,12 +248,17 @@ def wink_api_fetch(): def get_devices(device_type): - response_dict = wink_api_fetch() - filter_key = DEVICE_ID_KEYS.get(device_type) - return get_devices_from_response_dict(response_dict, filter_key) + global ALL_DEVICES, LAST_UPDATE + now = time.time() + # Only call the API once to obtain all devices + if LAST_UPDATE is None or (now - LAST_UPDATE) > 60: + ALL_DEVICES = wink_api_fetch() + LAST_UPDATE = now + return get_devices_from_response_dict(ALL_DEVICES, device_type) -def get_devices_from_response_dict(response_dict, filter_key): + +def get_devices_from_response_dict(response_dict, device_type): """ :rtype: list of WinkDevice """ @@ -235,160 +268,14 @@ def get_devices_from_response_dict(response_dict, filter_key): api_interface = WinkApiInterface() - keys = ['powerstrip_id', 'sensor_pod_id', 'piggy_bank_id', - 'smoke_detector_id', 'hub_id', 'door_bell_id'] - for item in items: - if item.get(filter_key, None) is None: - continue - elif not __device_is_visible(item, filter_key): - continue - elif filter_key in keys: - devices.extend(__get_outlets_from_powerstrip(item, api_interface, filter_key)) - devices.extend(__get_subsensors_from_sensor_pod(item, api_interface, filter_key)) - devices.extend(__get_devices_from_piggy_bank(item, api_interface, filter_key)) - devices.extend(__get_subsensors_from_smoke_detector(item, api_interface, filter_key)) - devices.extend(__get_sensor_from_hub(item, api_interface, filter_key)) - devices.extend(__get_subsensors_from_door_bell(item, api_interface, filter_key)) - else: - devices.append(build_device(item, api_interface)) + if item.get("object_type") in device_type: + _devices = build_device(item, api_interface) + for device in _devices: + devices.append(device) return devices -def __get_sensor_from_hub(item, api_interface, filter_key): - if filter_key != 'hub_id': - return [] - keys = list(DEVICE_ID_KEYS.values()) - # Most devices have a hub_id, but we only want the actual hub. - # This will only return hubs by checking for any other keys - # being present along with the hub_id - skip = False - for key in keys: - if key == "hub_id": - continue - if item.get(key, None) is not None: - skip = True - if skip: - return [] - else: - return [WinkHub(item, api_interface)] - - -def __get_subsensors_from_sensor_pod(item, api_interface, filter_key): - if filter_key != 'sensor_pod_id': - return [] - - capabilities = [cap['field'] for cap in item.get('capabilities', {}).get('fields', [])] - capabilities.extend([cap['field'] for cap in item.get('capabilities', {}).get('sensor_types', [])]) - - if not capabilities: - return [] - - subsensors = [] - - if WinkHumiditySensor.CAPABILITY in capabilities: - subsensors.append(WinkHumiditySensor(item, api_interface)) - - if WinkBrightnessSensor.CAPABILITY in capabilities: - subsensors.append(WinkBrightnessSensor(item, api_interface)) - - if WinkSoundPresenceSensor.CAPABILITY in capabilities: - subsensors.append(WinkSoundPresenceSensor(item, api_interface)) - - if WinkTemperatureSensor.CAPABILITY in capabilities: - subsensors.append(WinkTemperatureSensor(item, api_interface)) - - if WinkVibrationPresenceSensor.CAPABILITY in capabilities: - subsensors.append(WinkVibrationPresenceSensor(item, api_interface)) - - if WinkLiquidPresenceSensor.CAPABILITY in capabilities: - subsensors.append(WinkLiquidPresenceSensor(item, api_interface)) - - if WinkMotionSensor.CAPABILITY in capabilities: - subsensors.append(WinkMotionSensor(item, api_interface)) - - if WinkPresenceSensor.CAPABILITY in capabilities: - subsensors.append(WinkPresenceSensor(item, api_interface)) - - if WinkProximitySensor.CAPABILITY in capabilities: - subsensors.append(WinkProximitySensor(item, api_interface)) - - if WinkSensorPod.CAPABILITY in capabilities: - subsensors.append(WinkSensorPod(item, api_interface)) - - return subsensors - - -def __get_outlets_from_powerstrip(item, api_interface, filter_key): - if filter_key != 'powerstrip_id': - return [] - outlets = item['outlets'] - for outlet in outlets: - if 'subscription' in item: - outlet['subscription'] = item['subscription'] - outlet['last_reading']['connection'] = item['last_reading']['connection'] - return [build_device(outlet, api_interface) for outlet in outlets if __device_is_visible(outlet, 'outlet_id')] - - -def __get_devices_from_piggy_bank(item, api_interface, filter_key): - if filter_key != 'piggy_bank_id': - return [] - subdevices = [] - subdevices.append(WinkCurrencySensor(item, api_interface)) - subdevices.append(WinkPorkfolioNose(item, api_interface)) - return subdevices - - -def __get_subsensors_from_smoke_detector(item, api_interface, filter_key): - if filter_key != 'smoke_detector_id': - return [] - subsensors = [] - subsensors.append(WinkSmokeDetector(item, api_interface)) - subsensors.append(WinkCoDetector(item, api_interface)) - return subsensors - - -def __get_subsensors_from_door_bell(item, api_interface, filter_key): - if filter_key != 'door_bell_id': - return [] - - capabilities = [cap['field'] for cap in item.get('capabilities', {}).get('fields', [])] - - if not capabilities: - return [] - - subsensors = [] - - if WinkDoorBellMotion.CAPABILITY in capabilities: - subsensors.append(WinkDoorBellMotion(item, api_interface)) - if WinkDoorBellButton.CAPABILITY in capabilities: - subsensors.append(WinkDoorBellButton(item, api_interface)) - return subsensors - - -def __device_is_visible(item, key): - is_correctly_structured = bool(item.get(key)) - is_visible = not item.get('hidden_at') - return is_correctly_structured and is_visible - - -def refresh_state_at_hub(device): - """ - Tell hub to query latest status from device and upload to Wink. - PS: Not sure if this even works.. - :type device: WinkDevice - """ - url_string = "{}/{}/{}/refresh".format(WinkApiInterface.BASE_URL, - device.objectprefix, - device.device_id()) - requests.get(url_string, headers=API_HEADERS) - - -def is_token_set(): - """ Returns if an auth token has been set. """ - return bool(API_HEADERS) - - class WinkAPIException(Exception): pass diff --git a/src/pywink/devices/base.py b/src/pywink/devices/base.py index 4ab03d1..f1fcd86 100644 --- a/src/pywink/devices/base.py +++ b/src/pywink/devices/base.py @@ -1,68 +1,59 @@ - - class WinkDevice(object): + """ + This is a generic Wink device, all other object inherit from this. + """ - def __init__(self, device_state_as_json, api_interface, objectprefix=None): + def __init__(self, device_state_as_json, api_interface): """ :type api_interface pywink.api.WinkApiInterface: :return: """ self.api_interface = api_interface - self.objectprefix = objectprefix self.json_state = device_state_as_json + self.pubnub_key = None + self.pubnub_channel = None subscription = self.json_state.get('subscription') if subscription != {} and subscription is not None: pubnub = subscription.get('pubnub') self.pubnub_key = pubnub.get('subscribe_key') self.pubnub_channel = pubnub.get('channel') - else: - self.pubnub_key = None - self.pubnub_channel = None - - 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") + return self.json_state.get('name') def state(self): raise NotImplementedError("Must implement state") - def device_id(self): - raise NotImplementedError("Must implement device_id") + def object_id(self): + return self.json_state.get('object_id') + + def object_type(self): + return self.json_state.get("object_type") @property def _last_reading(self): return self.json_state.get('last_reading') or {} - @property def available(self): return self._last_reading.get('connection', False) - @property def battery_level(self): - return self._last_reading.get('battery', None) + if not self._last_reading.get('external_power'): + return self._last_reading.get('battery') + else: + return None - @property def manufacturer_device_model(self): - return self.json_state.get('manufacturer_device_model', None) + return self.json_state.get('manufacturer_device_model') - @property def manufacturer_device_id(self): - return self.json_state.get('manufacturer_device_id', None) + return self.json_state.get('manufacturer_device_id') - @property def device_manufacturer(self): - return self.json_state.get('device_manufacturer', None) + return self.json_state.get('device_manufacturer') - @property def model_name(self): - return self.json_state.get('model_name', None) + return self.json_state.get('model_name') def _update_state_from_response(self, response_json): """ diff --git a/src/pywink/devices/binary_switch.py b/src/pywink/devices/binary_switch.py new file mode 100644 index 0000000..d82c940 --- /dev/null +++ b/src/pywink/devices/binary_switch.py @@ -0,0 +1,29 @@ +from pywink.devices.base import WinkDevice + + +class WinkBinarySwitch(WinkDevice): + """ + Represents a Wink binary switch. + """ + + def __init__(self, device_state_as_json, api_interface): + super(WinkBinarySwitch, self).__init__(device_state_as_json, api_interface) + + def state(self): + return self._last_reading.get('powered', False) + + def set_state(self, state): + """ + :param state: a boolean of true (on) or false ('off') + :return: nothing + """ + values = {"desired_state": {"powered": state}} + response = self.api_interface.set_device_state(self, values, type_override="binary_switche") + self._update_state_from_response(response) + + def update_state(self): + """ + Update state with latest info from Wink API. + """ + response = self.api_interface.get_device_state(self, type_override="binary_switche") + return self._update_state_from_response(response) diff --git a/src/pywink/devices/button.py b/src/pywink/devices/button.py new file mode 100644 index 0000000..7bc1880 --- /dev/null +++ b/src/pywink/devices/button.py @@ -0,0 +1,26 @@ +from pywink.devices.binary_switch import WinkBinarySwitch + + +class WinkButton(WinkBinarySwitch): + """ + Represents a Wink relay button. + """ + + def __init__(self, device_state_as_json, api_interface): + super(WinkButton, self).__init__(device_state_as_json, api_interface) + + def state(self): + return bool(self.long_pressed() or self.pressed()) + + def long_pressed(self): + return self._last_reading.get('long_pressed') or False + + def pressed(self): + return self._last_reading.get('pressed') or False + + def update_state(self): + """ + Update state with latest info from Wink API. + """ + response = self.api_interface.get_device_state(self, type_override="button") + return self._update_state_from_response(response) diff --git a/src/pywink/devices/camera.py b/src/pywink/devices/camera.py new file mode 100644 index 0000000..4805ac8 --- /dev/null +++ b/src/pywink/devices/camera.py @@ -0,0 +1,40 @@ +from pywink.devices.base import WinkDevice + + +class WinkCanaryCamera(WinkDevice): + """ + Represents a Wink Canary camera. + + The Canary camera has three modes "home", "away", or "night" these avaible modes + are not listed in the device's JSON. + """ + + def __init__(self, device_state_as_json, api_interface): + super(WinkCanaryCamera, self).__init__(device_state_as_json, api_interface) + + def state(self): + return self.mode() + + def mode(self): + return self._last_reading.get('mode') + + def private(self): + return self._last_reading.get('private') + + def set_mode(self, mode): + """ + :param mode: a str, one of [home, away, night] + :return: nothing + """ + values = {"desired_state": {"mode": mode}} + response = self.api_interface.set_device_state(self, values) + self._update_state_from_response(response) + + def set_privacy(self, state): + """ + :param state: True or False + :return: nothing + """ + values = {"desired_state": {"private": state}} + response = self.api_interface.set_device_state(self, values) + self._update_state_from_response(response) diff --git a/src/pywink/devices/eggtray.py b/src/pywink/devices/eggtray.py new file mode 100644 index 0000000..4bffc70 --- /dev/null +++ b/src/pywink/devices/eggtray.py @@ -0,0 +1,22 @@ +from pywink.devices.base import WinkDevice + + +class WinkEggtray(WinkDevice): + """ + Represents a Wink eggminder egg tray. + """ + + def __init__(self, device_state_as_json, api_interface): + super(WinkEggtray, self).__init__(device_state_as_json, api_interface) + self._cap = None + self._unit = "eggs" + + def capability(self): + # Eggtray has no capability. + return self._cap + + def unit(self): + return self._unit + + def state(self): + return self._last_reading.get("inventory") diff --git a/src/pywink/devices/factory.py b/src/pywink/devices/factory.py index 8821460..07ddedb 100644 --- a/src/pywink/devices/factory.py +++ b/src/pywink/devices/factory.py @@ -1,40 +1,143 @@ -from pywink.devices.base import WinkDevice -from pywink.devices.sensors import WinkSensorPod -from pywink.devices.standard import WinkBulb, WinkBinarySwitch, WinkPowerStripOutlet, WinkLock, \ - WinkEggTray, WinkGarageDoor, WinkShade, WinkSiren, WinkKey, WinkThermostat, \ - WinkFan +""" +Build Wink devices. +""" +from pywink.devices import types as device_types +from pywink.devices.sensor import WinkSensor +from pywink.devices.light_bulb import WinkLightBulb +from pywink.devices.binary_switch import WinkBinarySwitch +from pywink.devices.lock import WinkLock +from pywink.devices.eggtray import WinkEggtray +from pywink.devices.garage_door import WinkGarageDoor +from pywink.devices.shade import WinkShade +from pywink.devices.siren import WinkSiren +from pywink.devices.key import WinkKey +from pywink.devices.thermostat import WinkThermostat +from pywink.devices.fan import WinkFan +from pywink.devices.remote import WinkRemote +from pywink.devices.hub import WinkHub +from pywink.devices.powerstrip import WinkPowerStrip, WinkPowerStripOutlet +from pywink.devices.piggy_bank import WinkPorkfolioBalanceSensor, WinkPorkfolioNose +from pywink.devices.sprinkler import WinkSprinkler +from pywink.devices.button import WinkButton +from pywink.devices.gang import WinkGang +from pywink.devices.smoke_detector import WinkSmokeDetector, WinkSmokeSeverity, WinkCoDetector, WinkCoSeverity +from pywink.devices.camera import WinkCanaryCamera -# pylint: disable=redefined-variable-type,too-many-branches + +# pylint: disable=redefined-variable-type,too-many-branches, too-many-statements def build_device(device_state_as_json, api_interface): new_object = None + new_objects = None # These objects all share the same base class: WinkDevice + object_type = device_state_as_json.get("object_type") - if "light_bulb_id" in device_state_as_json: - new_object = WinkBulb(device_state_as_json, api_interface) - elif "sensor_pod_id" in device_state_as_json: - new_object = WinkSensorPod(device_state_as_json, api_interface) - elif "binary_switch_id" in device_state_as_json: - new_object = WinkBinarySwitch(device_state_as_json, api_interface) - elif "outlet_id" in device_state_as_json: - new_object = WinkPowerStripOutlet(device_state_as_json, api_interface) - elif "lock_id" in device_state_as_json: + if object_type == device_types.LIGHT_BULB: + new_object = WinkLightBulb(device_state_as_json, api_interface) + elif object_type == device_types.BINARY_SWITCH: + # Skip relay switches that aren't controlling a load. The binary_switch can't be used. + if device_state_as_json.get("last_reading").get("powering_mode") is not None: + mode = device_state_as_json["last_reading"]["powering_mode"] + if mode == "dumb": + new_object = WinkBinarySwitch(device_state_as_json, api_interface) + else: + new_object = WinkBinarySwitch(device_state_as_json, api_interface) + elif object_type == device_types.LOCK: new_object = WinkLock(device_state_as_json, api_interface) - elif "eggtray_id" in device_state_as_json: - new_object = WinkEggTray(device_state_as_json, api_interface) - elif "garage_door_id" in device_state_as_json: + elif object_type == device_types.EGGTRAY: + new_object = WinkEggtray(device_state_as_json, api_interface) + elif object_type == device_types.GARAGE_DOOR: new_object = WinkGarageDoor(device_state_as_json, api_interface) - elif "shade_id" in device_state_as_json: + elif object_type == device_types.SHADE: new_object = WinkShade(device_state_as_json, api_interface) - elif "siren_id" in device_state_as_json: + elif object_type == device_types.SIREN: new_object = WinkSiren(device_state_as_json, api_interface) - elif "key_id" in device_state_as_json: + elif object_type == device_types.KEY: new_object = WinkKey(device_state_as_json, api_interface) - elif "thermostat_id" in device_state_as_json: + elif object_type == device_types.THERMOSTAT: new_object = WinkThermostat(device_state_as_json, api_interface) - elif "fan_id" in device_state_as_json: + elif object_type == device_types.FAN: new_object = WinkFan(device_state_as_json, api_interface) + elif object_type == device_types.REMOTE: + # The lutron Pico remote doesn't follow the API spec and + # provides no benefit as a device in this library. + if device_state_as_json.get("model_name") != "Pico": + new_object = WinkRemote(device_state_as_json, api_interface) + elif object_type == device_types.HUB: + new_object = WinkHub(device_state_as_json, api_interface) + elif object_type == device_types.SENSOR_POD: + new_objects = __get_subsensors_from_device(device_state_as_json, api_interface) + elif object_type == device_types.POWERSTRIP: + new_objects = __get_outlets_from_powerstrip(device_state_as_json, api_interface) + new_objects.append(WinkPowerStrip(device_state_as_json, api_interface)) + elif object_type == device_types.PIGGY_BANK: + new_objects = __get_devices_from_piggy_bank(device_state_as_json, api_interface) + elif object_type == device_types.DOOR_BELL: + new_objects = __get_subsensors_from_device(device_state_as_json, api_interface) + elif object_type == device_types.SPRINKLER: + new_object = WinkSprinkler(device_state_as_json, api_interface) + elif object_type == device_types.BUTTON: + new_object = WinkButton(device_state_as_json, api_interface) + elif object_type == device_types.GANG: + new_object = WinkGang(device_state_as_json, api_interface) + elif object_type == device_types.SMOKE_DETECTOR: + new_objects = __get_sensors_from_smoke_detector(device_state_as_json, api_interface) + elif object_type == device_types.CAMERA: + if device_state_as_json.get("device_manufacturer") == "canary": + new_object = WinkCanaryCamera(device_state_as_json, api_interface) + + if new_object is not None: + return [new_object] + elif new_objects is not None: + return new_objects + else: + return [] + + +def __get_subsensors_from_device(item, api_interface): + sensor_types = item.get('capabilities', {}).get('fields', []) + sensor_types.extend(item.get('capabilities', {}).get('sensor_types', [])) + + # These are attributes of the sensor, not the main sensor to track. + ignored_sensors = ["battery", "powered", "connection", "tamper_detected", + "external_power"] + + subsensors = [] + + for sensor_type in sensor_types: + if sensor_type.get("field") in ignored_sensors: + continue + else: + subsensors.append(WinkSensor(item, api_interface, sensor_type)) + + return subsensors + + +def __get_outlets_from_powerstrip(item, api_interface): + _outlets = [] + outlets = item['outlets'] + for outlet in outlets: + if 'subscription' in item: + outlet['subscription'] = item['subscription'] + outlet['last_reading']['connection'] = item['last_reading']['connection'] + _outlets.append(WinkPowerStripOutlet(outlet, api_interface)) + return _outlets + + +def __get_devices_from_piggy_bank(item, api_interface): + subdevices = [] + subdevices.append(WinkPorkfolioBalanceSensor(item, api_interface)) + subdevices.append(WinkPorkfolioNose(item, api_interface)) + return subdevices + - return new_object or WinkDevice(device_state_as_json, api_interface) +def __get_sensors_from_smoke_detector(item, api_interface): + sensors = [] + sensors.append(WinkSmokeDetector(item, api_interface)) + sensors.append(WinkCoDetector(item, api_interface)) + if item.get("manufacturer_device_model") == "nest": + sensors.append(WinkSmokeSeverity(item, api_interface)) + sensors.append(WinkCoSeverity(item, api_interface)) + return sensors diff --git a/src/pywink/devices/standard/fan.py b/src/pywink/devices/fan.py similarity index 85% rename from src/pywink/devices/standard/fan.py rename to src/pywink/devices/fan.py index 7233532..aa578e8 100644 --- a/src/pywink/devices/standard/fan.py +++ b/src/pywink/devices/fan.py @@ -1,23 +1,15 @@ -from pywink.devices.standard.base import WinkDevice +from pywink.devices.base import WinkDevice # pylint: disable=too-many-public-methods class WinkFan(WinkDevice): """ - Represents a Wink fan - 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. - """ + Represents a Wink fan. + """ json_state = {} def __init__(self, device_state_as_json, api_interface): - super(WinkFan, self).__init__(device_state_as_json, api_interface, - objectprefix="fans") - - def device_id(self): - return self.json_state.get('fan_id', self.name()) + super(WinkFan, self).__init__(device_state_as_json, api_interface) def fan_speeds(self): capabilities = self.json_state.get('capabilities', {}) @@ -113,7 +105,3 @@ def set_fan_timer(self, timer): }) self._update_state_from_response(resp) - - def __repr__(self): - return "" % ( - self.name(), self.device_id()) diff --git a/src/pywink/devices/gang.py b/src/pywink/devices/gang.py new file mode 100644 index 0000000..3f27965 --- /dev/null +++ b/src/pywink/devices/gang.py @@ -0,0 +1,19 @@ +from pywink.devices.base import WinkDevice + + +class WinkGang(WinkDevice): + """ + Represents a Wink relay gang. + """ + + def __init__(self, device_state_as_json, api_interface): + super(WinkGang, self).__init__(device_state_as_json, api_interface) + self._unit = None + + def unit(self): + # Gang has no unit + return self._unit + + def state(self): + # Gang has no state, reporting it's connection status + return self.available() diff --git a/src/pywink/devices/garage_door.py b/src/pywink/devices/garage_door.py new file mode 100644 index 0000000..6109f62 --- /dev/null +++ b/src/pywink/devices/garage_door.py @@ -0,0 +1,28 @@ +from pywink.devices.base import WinkDevice + + +class WinkGarageDoor(WinkDevice): + """ + Represents a Wink garage door. + """ + + def __init__(self, device_state_as_json, api_interface): + super(WinkGarageDoor, self).__init__(device_state_as_json, api_interface) + + def state(self): + return self._last_reading.get('position', 0) + + def tamper_detected(self): + tamper = self._last_reading.get('tamper_detected_true', False) + if tamper is None: + tamper = False + return tamper + + def set_state(self, state): + """ + :param state: a number of 1 ('open') or 0 ('close') + :return: nothing + """ + values = {"desired_state": {"position": state}} + response = self.api_interface.set_device_state(self, values) + self._update_state_from_response(response) diff --git a/src/pywink/devices/hub.py b/src/pywink/devices/hub.py new file mode 100644 index 0000000..f120a99 --- /dev/null +++ b/src/pywink/devices/hub.py @@ -0,0 +1,30 @@ +from pywink.devices.base import WinkDevice + + +class WinkHub(WinkDevice): + """ + Represents a Wink Hub. + """ + + def __init__(self, device_state_as_json, api_interface): + super(WinkHub, self).__init__(device_state_as_json, api_interface) + self._unit = None + + def unit(self): + return self._unit + + def state(self): + return self.available() + + def kidde_radio_code(self): + config = self.json_state.get('configuration') + return config.get('kidde_radio_code') + + def update_needed(self): + return self._last_reading.get('update_needed') + + def ip_address(self): + return self._last_reading.get('ip_address') + + def firmware_version(self): + return self._last_reading.get('firmware_version') diff --git a/src/pywink/devices/key.py b/src/pywink/devices/key.py new file mode 100644 index 0000000..efd731d --- /dev/null +++ b/src/pywink/devices/key.py @@ -0,0 +1,32 @@ +from pywink.devices.base import WinkDevice + + +class WinkKey(WinkDevice): + """ + Represents a Wink key. + """ + + def __init__(self, device_state_as_json, api_interface): + super(WinkKey, self).__init__(device_state_as_json, api_interface) + self._available = True + self._unit = None + self._cap = "activity_detected" + + def state(self): + return self._last_reading.get(self.capability(), False) + + def parent_id(self): + return self.json_state.get('parent_object_id') + + def available(self): + """Keys are virtual therefore they don't have a connection status + always return True + """ + return self._available + + def unit(self): + # Keys are a boolean sensor, they have no unit. + return self._unit + + def capability(self): + return self._cap diff --git a/src/pywink/devices/standard/bulb.py b/src/pywink/devices/light_bulb.py similarity index 88% rename from src/pywink/devices/standard/bulb.py rename to src/pywink/devices/light_bulb.py index 02c7281..5f11d85 100644 --- a/src/pywink/devices/standard/bulb.py +++ b/src/pywink/devices/light_bulb.py @@ -1,26 +1,19 @@ import colorsys -import time -from pywink.devices.standard.base import WinkBinarySwitch +from pywink.devices.base import WinkDevice from pywink.color import color_temperature_to_rgb, color_xy_brightness_to_rgb -class WinkBulb(WinkBinarySwitch): +class WinkLightBulb(WinkDevice): + """ + Represents a Wink light bulb. """ - 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, api_interface): - super(WinkBulb, self).__init__(device_state_as_json, api_interface, - objectprefix="light_bulbs") + super(WinkLightBulb, self).__init__(device_state_as_json, api_interface) - def device_id(self): - return self.json_state.get('light_bulb_id', self.name()) + def state(self): + return self._last_reading.get('powered', False) def brightness(self): return self._last_reading.get('brightness') @@ -61,7 +54,7 @@ def color_saturation(self): def set_state(self, state, brightness=None, color_kelvin=None, color_xy=None, - color_hue_saturation=None, **kwargs): + color_hue_saturation=None): """ :param state: a boolean of true (on) or false ('off') :param brightness: a float from 0 to 1 to set the brightness of @@ -91,8 +84,6 @@ def set_state(self, state, brightness=None, }) self._update_state_from_response(response) - self._last_call = (time.time(), state) - def _format_color_data(self, color_hue_saturation, color_kelvin, color_xy): if color_hue_saturation is None and color_kelvin is None and color_xy is None: return {} @@ -169,10 +160,6 @@ def supports_temperature(self): return True return False - def __repr__(self): - return "" % ( - self.name(), self.device_id(), self.state()) - def _format_temperature(kelvin): return { diff --git a/src/pywink/devices/lock.py b/src/pywink/devices/lock.py new file mode 100644 index 0000000..0227bdd --- /dev/null +++ b/src/pywink/devices/lock.py @@ -0,0 +1,86 @@ +from pywink.devices.base import WinkDevice + + +class WinkLock(WinkDevice): + """ + Represents a Wink lock. + """ + + def __init__(self, device_state_as_json, api_interface): + super(WinkLock, self).__init__(device_state_as_json, api_interface) + + def state(self): + return self._last_reading.get('locked', False) + + def alarm_enabled(self): + return self._last_reading.get('alarm_enabled', False) + + def alarm_mode(self): + return self._last_reading.get('alarm_mode') + + def vacation_mode_enabled(self): + return self._last_reading.get('vacation_mode_enabled', False) + + def beeper_enabled(self): + return self._last_reading.get('beeper_enabled', False) + + def auto_lock_enabled(self): + return self._last_reading.get('auto_lock_enabled', False) + + def alarm_sensitivity(self): + return self._last_reading.get('alarm_sensitivity') + + def set_alarm_sensitivity(self, mode): + """ + :param mode: 1.0 for Very sensitive, 0.2 for not sensitive. + Steps in values of 0.2. + :return: nothing + """ + values = {"desired_state": {"alarm_sensitivity": mode}} + response = self.api_interface.set_device_state(self, values) + self._update_state_from_response(response) + + def set_alarm_mode(self, mode): + """ + :param mode: one of [None, "activity", "tamper", "forced_entry"] + :return: nothing + """ + values = {"desired_state": {"alarm_mode": mode}} + response = self.api_interface.set_device_state(self, values) + self._update_state_from_response(response) + + def set_alarm_state(self, state): + """ + :param state: a boolean of ture (on) or false ('off') + :return: nothing + """ + values = {"desired_state": {"alarm_enabled": state}} + response = self.api_interface.set_device_state(self, values) + self._update_state_from_response(response) + + def set_vacation_mode(self, state): + """ + :param state: a boolean of ture (on) or false ('off') + :return: nothing + """ + values = {"desired_state": {"vacation_mode_enabled": state}} + response = self.api_interface.set_device_state(self, values) + self._update_state_from_response(response) + + def set_beeper_mode(self, state): + """ + :param state: a boolean of ture (on) or false ('off') + :return: nothing + """ + values = {"desired_state": {"beeper_enabled": state}} + response = self.api_interface.set_device_state(self, values) + self._update_state_from_response(response) + + def set_state(self, state): + """ + :param state: a boolean of true (on) or false ('off') + :return: nothing + """ + values = {"desired_state": {"locked": state}} + response = self.api_interface.set_device_state(self, values) + self._update_state_from_response(response) diff --git a/src/pywink/devices/piggy_bank.py b/src/pywink/devices/piggy_bank.py new file mode 100644 index 0000000..d3818bf --- /dev/null +++ b/src/pywink/devices/piggy_bank.py @@ -0,0 +1,81 @@ +from pywink.devices.base import WinkDevice + + +class WinkPorkfolioNose(WinkDevice): + """ + Represents a Wink Porkfolio nose. + """ + + def __init__(self, device_state_as_json, api_interface): + super(WinkPorkfolioNose, self).__init__(device_state_as_json, api_interface) + self._available = True + + def available(self): + """ + connection variable isn't stable. + Porkfolio can be offline, but updates will continue to occur. + always returning True to avoid this issue. + This is the same for the PorkFolio balance sensor. + """ + return self._available + + def set_state(self, color_hex): + """ + :param nose_color: a hex string indicating the color of the porkfolio nose + :return: nothing + From the api... + "the color of the nose is not in the desired_state + but on the object itself." + """ + root_name = self.json_state.get('piggy_bank_id', self.name()) + response = self.api_interface.set_device_state(self, { + "nose_color": color_hex + }, root_name) + self._update_state_from_response(response) + + def state(self): + """ + Hex colour value: String or None + :rtype: list float + """ + return self.json_state.get('nose_color') + + def name(self): + name = self.json_state.get('name') + name += " Nose" + return name + + +class WinkPorkfolioBalanceSensor(WinkDevice): + """ + Represents a Wink Porkfolio balance sensor. + """ + + def __init__(self, device_state_as_json, api_interface): + super(WinkPorkfolioBalanceSensor, self).__init__(device_state_as_json, api_interface) + self._unit = 'USD' + self._cap = 'balance' + self._available = True + + def unit(self): + return self._unit + + def capability(self): + # Legacy device, doesn't have a capability list. + return self._cap + + def state(self): + return self._last_reading.get(self.capability()) + + def name(self): + name = self.json_state.get('name') + name += " " + self.capability() + return name + + def available(self): + """ + connection variable isn't stable. + Porkfolio can be offline, but updates will continue to occur. + always returning True to avoid this issue. + """ + return self._available diff --git a/src/pywink/devices/powerstrip.py b/src/pywink/devices/powerstrip.py new file mode 100644 index 0000000..c52221b --- /dev/null +++ b/src/pywink/devices/powerstrip.py @@ -0,0 +1,95 @@ +from pywink.devices.base import WinkDevice + + +class WinkPowerStrip(WinkDevice): + """ + Represents a Wink Powerstrip. + The state of the power strip is Ture if one outlet is on, and False if both are off. + Setting the state will set the state of both outlets. + """ + + def __init__(self, device_state_as_json, api_interface): + super(WinkPowerStrip, self).__init__(device_state_as_json, api_interface) + + def state(self): + outlets = self.json_state.get('outlets') + state = False + for outlet in outlets: + if outlet.get('last_reading').get('powered'): + state = True + return state + + def set_state(self, state): + """ + :param state: a boolean of true (on) or false ('off') + :return: nothing + """ + values = {"outlets": [{"desired_state": {"powered": state}}, {"desired_state": {"powered": state}}]} + + response = self.api_interface.set_device_state(self, values) + self._update_state_from_response(response) + + +class WinkPowerStripOutlet(WinkDevice): + """ + Represents a Wink Powerstrip outlet. + """ + + def __init__(self, device_state_as_json, api_interface): + super(WinkPowerStripOutlet, self).__init__(device_state_as_json, api_interface) + + def state(self): + return self._last_reading.get('powered', False) + + def update_state(self): + """ Update state with latest info from Wink API. """ + response = self.api_interface.get_device_state(self, id_override=self.parent_id(), + type_override=self.parent_object_type()) + power_strip = response.get('data') + + power_strip_reading = power_strip.get('last_reading') + outlets = power_strip.get('outlets') + for outlet in outlets: + if outlet.get('object_id') == self.object_id(): + outlet['last_reading']['connection'] = power_strip_reading.get('connection') + self.json_state = outlet + + 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') + for outlet in outlets: + if outlet.get('outlet_id') == str(self.object_id()): + outlet['last_reading']['connection'] = power_strip_reading.get('connection') + self.json_state = outlet + + def pubnub_update(self, json_response): + self._update_state_from_response(json_response) + + def index(self): + return self.json_state.get('outlet_index', None) + + def parent_id(self): + return self.json_state.get('parent_object_id') + + def parent_object_type(self): + return self.json_state.get('parent_object_type') + + def set_state(self, state): + """ + :param state: a boolean of true (on) or false ('off') + :return: nothing + """ + if self.index() == 0: + values = {"outlets": [{"desired_state": {"powered": state}}, {}]} + else: + values = {"outlets": [{}, {"desired_state": {"powered": state}}]} + + response = self.api_interface.set_device_state(self, values, id_override=self.parent_id(), + type_override="powerstrip") + self._update_state_from_response(response) diff --git a/src/pywink/devices/remote.py b/src/pywink/devices/remote.py new file mode 100644 index 0000000..8d1a85e --- /dev/null +++ b/src/pywink/devices/remote.py @@ -0,0 +1,37 @@ +from pywink.devices.base import WinkDevice + + +class WinkRemote(WinkDevice): + """ + Represents a Wink/Lutron connected bulb remote. + """ + + def __init__(self, device_state_as_json, api_interface): + super(WinkRemote, self).__init__(device_state_as_json, api_interface) + self._unit = None + self._cap = 'opened' + self._available = True + + def unit(self): + # Remote has no unit + return self._unit + + def capability(self): + # Remote has no capability. + return self._cap + + def state(self): + return bool(self.button_on_pressed() or self.button_off_pressed() or + self.button_up_pressed() or self.button_down_pressed()) + + def button_on_pressed(self): + return self._last_reading.get("button_on_pressed") or False + + def button_off_pressed(self): + return self._last_reading.get("button_off_pressed") or False + + def button_down_pressed(self): + return self._last_reading.get("button_down_pressed") or False + + def button_up_pressed(self): + return self._last_reading.get("button_up_pressed") or False diff --git a/src/pywink/devices/sensor.py b/src/pywink/devices/sensor.py new file mode 100644 index 0000000..2c43b69 --- /dev/null +++ b/src/pywink/devices/sensor.py @@ -0,0 +1,43 @@ +from pywink.devices.base import WinkDevice + +SENSOR_FIELDS_TO_UNITS = {"humidity": "%", "temperature": u'\N{DEGREE SIGN}', "brightness": "%", "proximity": ""} + + +class WinkSensor(WinkDevice): + """ + Represents a Wink sensor. + """ + + def __init__(self, device_state_as_json, api_interface, sensor_type_info): + super(WinkSensor, self).__init__(device_state_as_json, api_interface) + self.sensor_type_info = sensor_type_info + + def unit(self): + return SENSOR_FIELDS_TO_UNITS.get(self.capability(), None) + + def unit_type(self): + return self.sensor_type_info.get("type") + + def capability(self): + return self.sensor_type_info.get("field") + + def tamper_detected(self): + tamper = self._last_reading.get('tamper_detected', False) + # If tamper was never detected it is set to None, not False + if tamper is None: + tamper = False + return tamper + + def name(self): + return self.json_state.get("name") + " " + self.capability() + + def state(self): + return self._last_reading.get(self.capability()) + + def pubnub_update(self, json_response): + humidity = json_response['last_reading'].get("humidity") + # humidity is returned from pubnub on some sensors as a float + if humidity is not None: + if humidity < 1.0: + json_response["last_reading"]["humidity"] = humidity * 100 + self.json_state = json_response diff --git a/src/pywink/devices/sensors.py b/src/pywink/devices/sensors.py deleted file mode 100644 index e21715c..0000000 --- a/src/pywink/devices/sensors.py +++ /dev/null @@ -1,441 +0,0 @@ -# -*- coding: utf-8 -*- -from pywink.devices.base import WinkDevice - - -class _WinkCapabilitySensor(WinkDevice): - - def __init__(self, device_state_as_json, api_interface, capability, unit, objectprefix="sensor_pods"): - super(_WinkCapabilitySensor, self).__init__(device_state_as_json, api_interface, - objectprefix=objectprefix) - self._capability = capability - self.unit = unit - - def __repr__(self): - return "".format(name=self.name(), - dev_id=self.device_id(), - reading=self._last_reading.get(self._capability), - unit='' if self.unit is None else self.unit) - - def state(self): - return self._last_reading.get('connection', False) - - def last_reading(self): - return self._last_reading.get(self._capability) - - def capability(self): - return self._capability - - def name(self): - name = self.json_state.get('name', "Unknown Name") - if self._capability != "opened": - name += " " + self._capability - return name - - @property - def tamper_detected(self): - tamper = self._last_reading.get('tamper_detected', False) - if tamper is None: - tamper = False - return tamper - - @property - def battery_level(self): - if not self._last_reading.get('external_power', None): - return self._last_reading.get('battery', None) - else: - return None - - def device_id(self): - root_name = self.json_state.get('sensor_pod_id', None) - return '{}+{}'.format(root_name, self._capability) - - def update_state(self): - """ Update state with latest info from Wink API. """ - root_name = self.json_state.get('sensor_pod_id', self.name()) - response = self.api_interface.get_device_state(self, root_name) - self._update_state_from_response(response) - - -class WinkSensorPod(_WinkCapabilitySensor): - """ 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: - """ - CAPABILITY = 'opened' - UNIT = None - - def __init__(self, device_state_as_json, api_interface): - super(WinkSensorPod, self).__init__(device_state_as_json, api_interface, - self.CAPABILITY, self.UNIT) - - 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'] - return False - - def device_id(self): - return self.json_state.get('sensor_pod_id', self.name()) - - -class WinkHumiditySensor(_WinkCapabilitySensor): - - CAPABILITY = 'humidity' - UNIT = '%' - - def __init__(self, device_state_as_json, api_interface): - super(WinkHumiditySensor, self).__init__(device_state_as_json, api_interface, - self.CAPABILITY, - self.UNIT) - - def humidity_percentage(self): - """ - :return: The relative humidity detected by the sensor (0% to 100%) - :rtype: int - """ - # Relay returns humidity as a decimal - if self.last_reading() < 1.0: - return int(round(self.last_reading() * 100)) - else: - return self.last_reading() - - def pubnub_update(self, json_response): - # Pubnub returns the humidity as a decimal - # converting to a percentage - hum = json_response["last_reading"]["humidity"] * 100 - json_response["last_reading"]["humidity"] = hum - self.json_state = json_response - - -class WinkBrightnessSensor(_WinkCapabilitySensor): - - CAPABILITY = 'brightness' - UNIT = None - - def __init__(self, device_state_as_json, api_interface): - super(WinkBrightnessSensor, self).__init__(device_state_as_json, api_interface, - self.CAPABILITY, - self.UNIT) - - def brightness_boolean(self): - """ - :return: True if light is detected. False if light is below detection threshold (varies by device) - :rtype: bool - """ - return self.last_reading() - - -class WinkSoundPresenceSensor(_WinkCapabilitySensor): - - CAPABILITY = 'loudness' - UNIT = None - - def __init__(self, device_state_as_json, api_interface): - super(WinkSoundPresenceSensor, self).__init__(device_state_as_json, api_interface, - self.CAPABILITY, - self.UNIT) - - def loudness_boolean(self): - """ - :return: True if sound is detected. False if sound is below detection threshold (varies by device) - :rtype: bool - """ - return self.last_reading() - - -class WinkTemperatureSensor(_WinkCapabilitySensor): - - CAPABILITY = 'temperature' - UNIT = u'\N{DEGREE SIGN}' - - def __init__(self, device_state_as_json, api_interface): - super(WinkTemperatureSensor, self).__init__(device_state_as_json, api_interface, - self.CAPABILITY, - self.UNIT) - - def temperature_float(self): - """ - :return: A float indicating the temperature. Units are determined by the sensor. - :rtype: float - """ - return self.last_reading() - - -class WinkVibrationPresenceSensor(_WinkCapabilitySensor): - - CAPABILITY = 'vibration' - UNIT = None - - def __init__(self, device_state_as_json, api_interface): - super(WinkVibrationPresenceSensor, self).__init__(device_state_as_json, api_interface, - self.CAPABILITY, - self.UNIT) - - def vibration_boolean(self): - """ - :return: Returns True if vibration is detected. - :rtype: bool - """ - return self.last_reading() - - -class WinkLiquidPresenceSensor(_WinkCapabilitySensor): - - CAPABILITY = 'liquid_detected' - UNIT = None - - def __init__(self, device_state_as_json, api_interface): - super(WinkLiquidPresenceSensor, self).__init__(device_state_as_json, api_interface, - self.CAPABILITY, - self.UNIT) - - def liquid_boolean(self): - """ - :return: Returns True if liquid is detected. - :rtype: bool - """ - return self.last_reading() - - -class WinkMotionSensor(_WinkCapabilitySensor): - - CAPABILITY = 'motion' - UNIT = None - - def __init__(self, device_state_as_json, api_interface): - super(WinkMotionSensor, self).__init__(device_state_as_json, api_interface, - self.CAPABILITY, - self.UNIT) - - def motion_boolean(self): - """ - :return: Returns True if motion is detected. - :rtype: bool - """ - return self.last_reading() - - -class WinkPresenceSensor(_WinkCapabilitySensor): - - CAPABILITY = 'presence' - UNIT = None - - def __init__(self, device_state_as_json, api_interface): - super(WinkPresenceSensor, self).__init__(device_state_as_json, api_interface, - self.CAPABILITY, - self.UNIT) - - def presence_boolean(self): - """ - :return: Returns True if presence is detected. - :rtype: bool - """ - return self.last_reading() - - -class WinkProximitySensor(_WinkCapabilitySensor): - - CAPABILITY = 'proximity' - UNIT = None - - def __init__(self, device_state_as_json, api_interface): - super(WinkProximitySensor, self).__init__(device_state_as_json, api_interface, - self.CAPABILITY, - self.UNIT) - - def proximity_float(self): - """ - :return: A float indicating the proximity. - :rtype: float - """ - return self.last_reading() - - -class WinkCurrencySensor(_WinkCapabilitySensor): - - CAPABILITY = 'balance' - UNIT = 'USD' - - def __init__(self, device_state_as_json, api_interface): - super(WinkCurrencySensor, self).__init__(device_state_as_json, api_interface, - self.CAPABILITY, - self.UNIT, 'piggy_bank') - - @property - def available(self): - """ - connection variable isn't stable. - Porkfolio can be offline, but updates will continue to occur. - always returning True to avoid this issue. - """ - return True - - def device_id(self): - root_name = self.json_state.get('piggy_bank_id', self.name()) - return '{}+{}'.format(root_name, self._capability) - - def balance(self): - """ - :return: Returns the balance in cents. - :rtype: int - """ - return self.last_reading() - - def update_state(self): - """ Update state with latest info from Wink API. """ - root_name = self.json_state.get('piggy_bank_id', self.name()) - response = self.api_interface.get_device_state(self, root_name) - self._update_state_from_response(response) - - -class WinkSmokeDetector(_WinkCapabilitySensor): - - CAPABILITY = 'smoke_detected' - UNIT = None - - def __init__(self, device_state_as_json, api_interface): - super(WinkSmokeDetector, self).__init__(device_state_as_json, api_interface, - self.CAPABILITY, - self.UNIT, 'smoke_detectors') - - def smoke_detected_boolean(self): - """ - :return: Returns True if smoke is detected. - :rtype: bool - """ - return self.last_reading() - - def device_id(self): - root_name = self.json_state.get('smoke_detector_id', None) - return '{}+{}'.format(root_name, self._capability) - - def update_state(self): - """ Update state with latest info from Wink API. """ - root_name = self.json_state.get('smoke_detector_id', self.name()) - response = self.api_interface.get_device_state(self, root_name) - self._update_state_from_response(response) - - -class WinkCoDetector(_WinkCapabilitySensor): - - CAPABILITY = 'co_detected' - UNIT = None - - def __init__(self, device_state_as_json, api_interface): - super(WinkCoDetector, self).__init__(device_state_as_json, api_interface, - self.CAPABILITY, - self.UNIT, 'smoke_detectors') - - def co_detected_boolean(self): - """ - :return: Returns True if CO is detected. - :rtype: bool - """ - return self.last_reading() - - def device_id(self): - root_name = self.json_state.get('smoke_detector_id', None) - return '{}+{}'.format(root_name, self._capability) - - def update_state(self): - """ Update state with latest info from Wink API. """ - root_name = self.json_state.get('smoke_detector_id', self.name()) - response = self.api_interface.get_device_state(self, root_name) - self._update_state_from_response(response) - - -class WinkHub(WinkDevice): - - def __init__(self, device_state_as_json, api_interface, objectprefix="hub_id"): - super(WinkHub, self).__init__(device_state_as_json, api_interface) - - def state(self): - return self._last_reading.get('connection', False) - - def name(self): - name = self.json_state.get('name', "Unknown Name") - name += " hub" - return name - - def device_id(self): - root_name = self.json_state.get('hub_id', None) - return '{}+{}'.format(root_name, 'hub') - - def kidde_radio_code(self): - config = self.json_state.get('configuration') - return config.get('kidde_radio_code') - - def update_needed(self): - return self._last_reading.get('update_needed') - - def ip_address(self): - return self._last_reading.get('ip_address') - - def firmware_version(self): - return self._last_reading.get('firmware_version') - - def update_state(self): - """ Update state with latest info from Wink API. """ - root_name = self.json_state.get('hub_id', self.name()) - response = self.api_interface.get_device_state(self, root_name) - self._update_state_from_response(response) - - -class WinkDoorBellButton(_WinkCapabilitySensor): - - CAPABILITY = 'button_pressed' - UNIT = None - - def __init__(self, device_state_as_json, api_interface): - super(WinkDoorBellButton, self).__init__(device_state_as_json, api_interface, - self.CAPABILITY, - self.UNIT, 'door_bells') - - def button_pressed_boolean(self): - """ - :return: Returns True if button was pressed. - :rtype: bool - """ - return self.last_reading() - - def device_id(self): - root_name = self.json_state.get('door_bell_id', None) - return '{}+{}'.format(root_name, self._capability) - - def update_state(self): - """ Update state with latest info from Wink API. """ - root_name = self.json_state.get('door_bell_id', self.name()) - response = self.api_interface.get_device_state(self, root_name) - self._update_state_from_response(response) - - -class WinkDoorBellMotion(_WinkCapabilitySensor): - - CAPABILITY = 'motion' - UNIT = None - - def __init__(self, device_state_as_json, api_interface): - super(WinkDoorBellMotion, self).__init__(device_state_as_json, api_interface, - self.CAPABILITY, - self.UNIT, 'door_bells') - - def motion_boolean(self): - """ - :return: Returns True if motion is detected. - :rtype: bool - """ - return self.last_reading() - - def device_id(self): - root_name = self.json_state.get('door_bell_id', None) - return '{}+{}'.format(root_name, self._capability) - - def update_state(self): - """ Update state with latest info from Wink API. """ - root_name = self.json_state.get('door_bell_id', self.name()) - response = self.api_interface.get_device_state(self, root_name) - self._update_state_from_response(response) diff --git a/src/pywink/devices/shade.py b/src/pywink/devices/shade.py new file mode 100644 index 0000000..b01cc95 --- /dev/null +++ b/src/pywink/devices/shade.py @@ -0,0 +1,22 @@ +from pywink.devices.base import WinkDevice + + +class WinkShade(WinkDevice): + """ + Represents a Wink Shade. + """ + + def __init__(self, device_state_as_json, api_interface): + super(WinkShade, self).__init__(device_state_as_json, api_interface) + + def state(self): + return self._last_reading.get('position', 0) + + def set_state(self, state): + """ + :param state: a number of 1 ('open') or 0 ('close') + :return: nothing + """ + values = {"desired_state": {"position": state}} + response = self.api_interface.set_device_state(self, values) + self._update_state_from_response(response) diff --git a/src/pywink/devices/siren.py b/src/pywink/devices/siren.py new file mode 100644 index 0000000..1233ce6 --- /dev/null +++ b/src/pywink/devices/siren.py @@ -0,0 +1,52 @@ +from pywink.devices.binary_switch import WinkBinarySwitch + + +class WinkSiren(WinkBinarySwitch): + """ + Represents a Wink Siren. + """ + + def __init__(self, device_state_as_json, api_interface): + super(WinkSiren, self).__init__(device_state_as_json, api_interface) + + def state(self): + return self._last_reading.get('powered', False) + + def mode(self): + return self._last_reading.get('mode', None) + + def auto_shutoff(self): + return self._last_reading.get('auto_shutoff', None) + + def set_mode(self, mode): + """ + :param mode: a str, one of [siren_only, strobe_only, siren_and_strobe] + :return: nothing + """ + values = { + "desired_state": { + "mode": mode + } + } + response = self.api_interface.set_device_state(self, values) + self._update_state_from_response(response) + + def set_auto_shutoff(self, timer): + """ + :param time: an int, one of [None (never), 30, 60, 120] + :return: nothing + """ + values = { + "desired_state": { + "auto_shutoff": timer + } + } + response = self.api_interface.set_device_state(self, values) + self._update_state_from_response(response) + + def update_state(self): + """ + Update state with latest info from Wink API. + """ + response = self.api_interface.get_device_state(self, type_override="siren") + return self._update_state_from_response(response) diff --git a/src/pywink/devices/smoke_detector.py b/src/pywink/devices/smoke_detector.py new file mode 100644 index 0000000..3a1fa60 --- /dev/null +++ b/src/pywink/devices/smoke_detector.py @@ -0,0 +1,109 @@ +from pywink.devices.sensor import WinkDevice + + +class WinkSmokeDetector(WinkDevice): + """ + Represents a Wink Smoke detector. + """ + + def __init__(self, device_state_as_json, api_interface): + super(WinkSmokeDetector, self).__init__(device_state_as_json, api_interface) + self._unit = None + self._cap = "smoke_detected" + self._unit_type = "boolean" + + def unit(self): + return self._unit + + def unit_type(self): + return self._unit_type + + def capability(self): + return self._cap + + def name(self): + return self.json_state.get("name") + " " + self.capability() + + def state(self): + return self._last_reading.get(self.capability()) + + +class WinkSmokeSeverity(WinkDevice): + """ + Represents a Wink/Nest Smoke severity sensor. + """ + + def __init__(self, device_state_as_json, api_interface): + super(WinkSmokeSeverity, self).__init__(device_state_as_json, api_interface) + self._unit = None + self._cap = "smoke_severity" + self._unit_type = None + + def unit(self): + return self._unit + + def unit_type(self): + return self._unit_type + + def capability(self): + return self._cap + + def name(self): + return self.json_state.get("name") + " " + self.capability() + + def state(self): + return self._last_reading.get(self.capability()) + + +class WinkCoDetector(WinkDevice): + """ + Represents a Wink CO detector. + """ + + def __init__(self, device_state_as_json, api_interface): + super(WinkCoDetector, self).__init__(device_state_as_json, api_interface) + self._unit = None + self._cap = "co_detected" + self._unit_type = "boolean" + + def unit(self): + return self._unit + + def unit_type(self): + return self._unit_type + + def capability(self): + return self._cap + + def name(self): + return self.json_state.get("name") + " " + self.capability() + + def state(self): + return self._last_reading.get(self.capability()) + + +class WinkCoSeverity(WinkDevice): + """ + Represents a Wink/Nest CO severity sensor. + """ + + def __init__(self, device_state_as_json, api_interface): + super(WinkCoSeverity, self).__init__(device_state_as_json, api_interface) + self._unit = None + self._cap = "co_severity" + self._unit_type = None + + def unit(self): + return self._unit + + def unit_type(self): + return self._unit_type + + def capability(self): + return self._cap + + def name(self): + return self.json_state.get("name") + " " + self.capability() + + def state(self): + return self._last_reading.get(self.capability()) diff --git a/src/pywink/devices/sprinkler.py b/src/pywink/devices/sprinkler.py new file mode 100644 index 0000000..48e514b --- /dev/null +++ b/src/pywink/devices/sprinkler.py @@ -0,0 +1,20 @@ +from pywink.devices.binary_switch import WinkBinarySwitch + + +class WinkSprinkler(WinkBinarySwitch): + """ + Represents a Wink Sprinkler. + """ + + def __init__(self, device_state_as_json, api_interface): + super(WinkSprinkler, self).__init__(device_state_as_json, api_interface) + + def state(self): + return self._last_reading.get('powered', False) + + def update_state(self): + """ + Update state with latest info from Wink API. + """ + response = self.api_interface.get_device_state(self, type_override="sprinkler") + return self._update_state_from_response(response) diff --git a/src/pywink/devices/standard/__init__.py b/src/pywink/devices/standard/__init__.py deleted file mode 100644 index 17b2a4d..0000000 --- a/src/pywink/devices/standard/__init__.py +++ /dev/null @@ -1,450 +0,0 @@ -""" -Objects for interfacing with the Wink API -""" -import time - -from pywink.devices.base import WinkDevice -from pywink.devices.standard.base import WinkBinarySwitch -from pywink.devices.standard.bulb import WinkBulb -from pywink.devices.standard.thermostat import WinkThermostat -from pywink.devices.standard.fan import WinkFan - - -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, api_interface, objectprefix="eggtrays"): - super(WinkEggTray, self).__init__(device_state_as_json, api_interface, - 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 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, api_interface, objectprefix="locks"): - super(WinkLock, self).__init__(device_state_as_json, api_interface, - 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): - return self._last_reading.get('locked', False) - - def device_id(self): - return self.json_state.get('lock_id', self.name()) - - @property - def alarm_enabled(self): - return self._last_reading.get('alarm_enabled', False) - - @property - def alarm_mode(self): - return self._last_reading.get('alarm_mode', None) - - @property - def vacation_mode_enabled(self): - return self._last_reading.get('vacation_mode_enabled', False) - - @property - def beeper_enabled(self): - return self._last_reading.get('beeper_enabled', False) - - @property - def auto_lock_enabled(self): - return self._last_reading.get('auto_lock_enabled', False) - - @property - def alarm_sensitivity(self): - return self._last_reading.get('alarm_sensitivity', None) - - def set_alarm_sensitivity(self, mode): - """ - :param mode: 1.0 for Very sensitive, 0.2 for not sensitive. - Steps in values of 0.2. - :return: nothing - """ - values = {"desired_state": {"alarm_sensitivity": mode}} - response = self.api_interface.set_device_state(self, values) - self._update_state_from_response(response) - - def set_alarm_mode(self, mode): - """ - :param mode: one of [None, "activity", "tamper", "forced_entry"] - :return: nothing - """ - values = {"desired_state": {"alarm_mode": mode}} - response = self.api_interface.set_device_state(self, values) - self._update_state_from_response(response) - - def set_alarm_state(self, state): - """ - :param state: a boolean of ture (on) or false ('off') - :return: nothing - """ - values = {"desired_state": {"alarm_enabled": state}} - response = self.api_interface.set_device_state(self, values) - self._update_state_from_response(response) - - def set_vacation_mode(self, state): - """ - :param state: a boolean of ture (on) or false ('off') - :return: nothing - """ - values = {"desired_state": {"vacation_mode_enabled": state}} - response = self.api_interface.set_device_state(self, values) - self._update_state_from_response(response) - - def set_beeper_mode(self, state): - """ - :param state: a boolean of ture (on) or false ('off') - :return: nothing - """ - values = {"desired_state": {"beeper_enabled": state}} - response = self.api_interface.set_device_state(self, values) - self._update_state_from_response(response) - - def set_state(self, state): - """ - :param state: a boolean of true (on) or false ('off') - :return: nothing - """ - values = {"desired_state": {"locked": state}} - response = self.api_interface.set_device_state(self, values) - self._update_state_from_response(response) - self._last_call = (time.time(), state) - - -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, api_interface, objectprefix="powerstrips"): - super(WinkPowerStripOutlet, self).__init__(device_state_as_json, api_interface, - 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(self): - """ Update state with latest info from Wink API. """ - response = self.api_interface.get_device_state(self, id_override=self.parent_id()) - power_strip = response.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_from_response(self, response_json): - """ - :param response_json: the json obj returned from query - :return: - """ - power_strip = response_json - 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 pubnub_update(self, json_response): - self._update_state_from_response(json_response) - - 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 - """ - if self.index() == 0: - values = {"outlets": [{"desired_state": {"powered": state}}, {}]} - else: - values = {"outlets": [{}, {"desired_state": {"powered": state}}]} - - response = self.api_interface.set_device_state(self, values, id_override=self.parent_id()) - power_strip = response.get('data') - self._update_state_from_response(power_strip) - - self._last_call = (time.time(), state) - - -class WinkGarageDoor(WinkDevice): - """ 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, api_interface, objectprefix="garage_doors"): - super(WinkGarageDoor, self).__init__(device_state_as_json, api_interface, - 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): - return self._last_reading.get('position', 0) - - def device_id(self): - return self.json_state.get('garage_door_id', self.name()) - - @property - def tamper_detected(self): - tamper = self._last_reading.get('tamper_detected_true', False) - if tamper is None: - tamper = False - return tamper - - def set_state(self, state): - """ - :param state: a number of 1 ('open') or 0 ('close') - :return: nothing - """ - values = {"desired_state": {"position": state}} - response = self.api_interface.set_device_state(self, values) - self._update_state_from_response(response) - - self._last_call = (time.time(), state) - - -class WinkShade(WinkDevice): - def __init__(self, device_state_as_json, api_interface, objectprefix="shades"): - super(WinkShade, self).__init__(device_state_as_json, api_interface, - 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('shade_id', self.name()) - - def state(self): - return self._last_reading.get('position', 0) - - def set_state(self, state): - """ - :param state: a number of 1 ('open') or 0 ('close') - :return: nothing - """ - values = {"desired_state": {"position": state}} - response = self.api_interface.set_device_state(self, values) - self._update_state_from_response(response) - - self._last_call = (time.time(), state) - - -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, api_interface, objectprefix="sirens"): - super(WinkSiren, self).__init__(device_state_as_json, api_interface, - 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()) - - @property - def mode(self): - return self._last_reading.get('mode', None) - - @property - def auto_shutoff(self): - return self._last_reading.get('auto_shutoff', None) - - def set_mode(self, mode): - """ - :param mode: a str, one of [siren_only, strobe_only, siren_and_strobe] - :return: nothing - """ - values = { - "desired_state": { - "mode": mode - } - } - response = self.api_interface.set_device_state(self, values) - self._update_state_from_response(response) - - def set_auto_shutoff(self, timer): - """ - :param time: an int, one of [None (never), 30, 60, 120] - :return: nothing - """ - values = { - "desired_state": { - "auto_shutoff": timer - } - } - response = self.api_interface.set_device_state(self, values) - self._update_state_from_response(response) - - -class WinkKey(WinkDevice): - """ represents a wink.py key - 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 - """ - UNIT = None - - def __init__(self, device_state_as_json, api_interface, objectprefix="keys"): - super(WinkKey, self).__init__(device_state_as_json, api_interface, - objectprefix=objectprefix) - self._capability = "opening" - - def __repr__(self): - return "".format(name=self.name(), - device=self.device_id(), - parent_id=self.parent_id(), - state=self.state()) - - def state(self): - if 'activity_detected' in self._last_reading: - return self._last_reading['activity_detected'] - return False - - def device_id(self): - return self.json_state.get('key_id', self.name()) - - def parent_id(self): - return self.json_state.get('parent_object_id', - self.json_state.get('lock_id')) - - def capability(self): - """Return opening for all keys.""" - return self._capability - - @property - def available(self): - """Keys are virtual therefore they don't have a connection status - always return True - """ - return True - - -class WinkPorkfolioNose(WinkDevice): - """ - Represents a Wink Porkfolio nose - 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, api_interface): - super().__init__(device_state_as_json, api_interface, - objectprefix="piggy_banks") - - @property - def available(self): - """ - connection variable isn't stable. - Porkfolio can be offline, but updates will continue to occur. - always returning True to avoid this issue. - This is the same for the PorkFolio balance sensor. - """ - return True - - def device_id(self): - root_name = self.json_state.get('piggy_bank_id', self.name()) - return '{}+{}'.format(root_name, "nose") - - def set_state(self, color_hex): - """ - :param nose_color: a hex string indicating the color of the porkfolio nose - :return: nothing - From the api... - "the color of the nose is not in the desired_state - but on the object itself." - """ - root_name = self.json_state.get('piggy_bank_id', self.name()) - response = self.api_interface.set_device_state(self, { - "nose_color": color_hex - }, root_name) - self._update_state_from_response(response) - - def state(self): - """ - Hex colour value: String or None - :rtype: list float - """ - return self.json_state.get('nose_color', None) - - -# pylint-disable: undefined-all-variable -__all__ = [WinkEggTray.__name__, - WinkBinarySwitch.__name__, - WinkBulb.__name__, - WinkLock.__name__, - WinkPowerStripOutlet.__name__, - WinkGarageDoor.__name__, - WinkShade.__name__, - WinkSiren.__name__, - WinkPorkfolioNose.__name__, - WinkThermostat.__name__, - WinkFan.__name__] diff --git a/src/pywink/devices/standard/base.py b/src/pywink/devices/standard/base.py deleted file mode 100644 index a1961cb..0000000 --- a/src/pywink/devices/standard/base.py +++ /dev/null @@ -1,45 +0,0 @@ -import time - -from pywink.devices.base import WinkDevice - - -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, api_interface, objectprefix="binary_switches"): - super(WinkBinarySwitch, self).__init__(device_state_as_json, api_interface, - 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 - 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 - """ - values = { - "desired_state": { - "powered": state - } - } - response = self.api_interface.set_device_state(self, values) - self._update_state_from_response(response) - - self._last_call = (time.time(), state) diff --git a/src/pywink/devices/standard/thermostat.py b/src/pywink/devices/thermostat.py similarity index 84% rename from src/pywink/devices/standard/thermostat.py rename to src/pywink/devices/thermostat.py index 5089f0c..963a76b 100644 --- a/src/pywink/devices/standard/thermostat.py +++ b/src/pywink/devices/thermostat.py @@ -1,27 +1,18 @@ -from pywink.devices.standard.base import WinkDevice +from pywink.devices.base import WinkDevice # pylint: disable=too-many-public-methods class WinkThermostat(WinkDevice): """ - Represents a Wink thermostat - 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 = {} + Represents a Wink thermostat. + """ def __init__(self, device_state_as_json, api_interface): - super(WinkThermostat, self).__init__(device_state_as_json, api_interface, - objectprefix="thermostats") + super(WinkThermostat, self).__init__(device_state_as_json, api_interface) def state(self): return self.current_hvac_mode() - def device_id(self): - return self.json_state.get('thermostat_id', self.name()) - def fan_modes(self): capabilities = self.json_state.get('capabilities', {}) cap_fields = capabilities.get('fields', []) @@ -146,18 +137,6 @@ def set_away(self, away=True): self._update_state_from_response(response) - def set_away_mode(self, away=True): - """ - :param away: a boolean True for away False for Home - :return: nothing - """ - desired_state = {"users_away": away} - - response = self.api_interface.set_device_state(self, { - "desired_state": desired_state - }) - self._update_state_from_response(response) - def set_operation_mode(self, mode): """ :param mode: a string one of ["cool_only", "heat_only", "auto", "aux", "off"] @@ -191,7 +170,3 @@ def set_temperature(self, min_set_point=None, max_set_point=None): }) self._update_state_from_response(response) - - def __repr__(self): - return "" % ( - self.name(), self.device_id()) diff --git a/src/pywink/devices/types.py b/src/pywink/devices/types.py index fdbbe39..9d19b7b 100644 --- a/src/pywink/devices/types.py +++ b/src/pywink/devices/types.py @@ -1,39 +1,30 @@ +""" +These are all the devices we currently support. +""" + LIGHT_BULB = 'light_bulb' BINARY_SWITCH = 'binary_switch' SENSOR_POD = 'sensor_pod' LOCK = 'lock' -EGG_TRAY = 'eggtray' +EGGTRAY = 'eggtray' GARAGE_DOOR = 'garage_door' -POWER_STRIP = 'powerstrip' -SHADE = 'shades' +POWERSTRIP = 'powerstrip' +SHADE = 'shade' SIREN = 'siren' KEY = 'key' -PIGGY_BANK = 'piggybank' +PIGGY_BANK = 'piggy_bank' SMOKE_DETECTOR = 'smoke_detector' THERMOSTAT = 'thermostat' HUB = 'hub' FAN = 'fan' -BUTTON = 'button' -REMOTE = 'remote' DOOR_BELL = 'door_bell' +REMOTE = 'remote' +SPRINKLER = 'sprinkler' +BUTTON = 'button' +GANG = 'gang' +CAMERA = 'camera' -DEVICE_ID_KEYS = { - BINARY_SWITCH: 'binary_switch_id', - EGG_TRAY: 'eggtray_id', - GARAGE_DOOR: 'garage_door_id', - LIGHT_BULB: 'light_bulb_id', - LOCK: 'lock_id', - POWER_STRIP: 'powerstrip_id', - SENSOR_POD: 'sensor_pod_id', - SHADE: 'shade_id', - SIREN: 'siren_id', - KEY: 'key_id', - PIGGY_BANK: 'piggy_bank_id', - SMOKE_DETECTOR: 'smoke_detector_id', - THERMOSTAT: 'thermostat_id', - HUB: 'hub_id', - FAN: 'fan_id', - BUTTON: 'button_id', - REMOTE: 'remote_id', - DOOR_BELL: 'door_bell_id' -} +ALL_SUPPORTED_DEVICES = [LIGHT_BULB, BINARY_SWITCH, SENSOR_POD, LOCK, EGGTRAY, + GARAGE_DOOR, POWERSTRIP, SHADE, SIREN, KEY, PIGGY_BANK, + SMOKE_DETECTOR, THERMOSTAT, HUB, FAN, DOOR_BELL, REMOTE, + SPRINKLER, BUTTON, GANG, CAMERA] diff --git a/src/pywink/test/__init__.py b/src/pywink/test/__init__.py index e69de29..e085d06 100644 --- a/src/pywink/test/__init__.py +++ b/src/pywink/test/__init__.py @@ -0,0 +1,17 @@ +""" +Top level functions +""" +# noqa +from pywink.api import set_bearer_token, refresh_access_token, \ + set_wink_credentials, set_user_agent, wink_api_fetch, \ + get_set_access_token, is_token_set, get_devices, \ + get_subscription_key + +from pywink.api import get_light_bulbs, get_garage_doors, get_locks, \ + get_powerstrips, get_shades, get_sirens, \ + get_switches, get_thermostats, get_fans + +from pywink.api import get_all_devices, get_eggtrays, get_sensors, \ + get_keys, get_piggy_banks, get_smoke_and_co_detectors, \ + get_hubs, get_door_bells, get_remotes, get_sprinklers, get_buttons, \ + get_gangs, get_cameras diff --git a/src/pywink/test/api_test.py b/src/pywink/test/api_test.py new file mode 100644 index 0000000..81d68da --- /dev/null +++ b/src/pywink/test/api_test.py @@ -0,0 +1,424 @@ +# Standard library imports... +from http.server import BaseHTTPRequestHandler, HTTPServer +import json +import re +import socket +from threading import Thread +import unittest +import os + +# Third-party imports... +import requests +from mock import patch + +from pywink.api import * +from pywink.devices import types as device_types +from pywink.api import WinkApiInterface +from pywink.devices.sensor import WinkSensor +from pywink.devices.hub import WinkHub +from pywink.devices.piggy_bank import WinkPorkfolioBalanceSensor, WinkPorkfolioNose +from pywink.devices.key import WinkKey +from pywink.devices.remote import WinkRemote +from pywink.devices.powerstrip import WinkPowerStrip, WinkPowerStripOutlet +from pywink.devices.light_bulb import WinkLightBulb +from pywink.devices.binary_switch import WinkBinarySwitch +from pywink.devices.lock import WinkLock +from pywink.devices.eggtray import WinkEggtray +from pywink.devices.garage_door import WinkGarageDoor +from pywink.devices.shade import WinkShade +from pywink.devices.siren import WinkSiren +from pywink.devices.fan import WinkFan +from pywink.devices.thermostat import WinkThermostat +from pywink.devices.button import WinkButton +from pywink.devices.gang import WinkGang +from pywink.devices.smoke_detector import WinkSmokeDetector, WinkSmokeSeverity, WinkCoDetector, WinkCoSeverity +from pywink.devices.sprinkler import WinkSprinkler + +USERS_ME_WINK_DEVICES = {} + + +class ApiTests(unittest.TestCase): + + + def setUp(self): + global USERS_ME_WINK_DEVICES + super(ApiTests, self).setUp() + all_devices = os.listdir('{}/devices/api_responses/'.format(os.path.dirname(__file__))) + device_list = [] + for json_file in all_devices: + _json_file = open('{}/devices/api_responses/{}'.format(os.path.dirname(__file__), json_file)) + device_list.append(json.load(_json_file)) + _json_file.close() + USERS_ME_WINK_DEVICES["data"] = device_list + self.port = get_free_port() + start_mock_server(self.port) + self.api_interface = MockApiInterface() + + def test_bad_status_codes(self): + try: + WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) + "/401/" + get_all_devices() + except Exception as e: + self.assertTrue(type(e), WinkAPIException) + try: + WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) + "/404/" + get_all_devices() + except Exception as e: + self.assertTrue(type(e), WinkAPIException) + + def test_set_bearer_token(self): + self.assertIsNone(get_set_access_token()) + set_bearer_token("THIS_IS_A_TEST") + self.assertEqual("THIS_IS_A_TEST", get_set_access_token()) + self.assertTrue(is_token_set()) + + def test_get_subscription_key(self): + WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) + get_all_devices() + self.assertIsNotNone(get_subscription_key()) + + def test_get_all_devices_from_api(self): + WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) + devices = get_all_devices() + self.assertEqual(len(devices), 51) + lights = get_light_bulbs() + for light in lights: + self.assertTrue(isinstance(light, WinkLightBulb)) + sensors = get_sensors() + sensors.extend(get_door_bells()) + for sensor in sensors: + self.assertTrue(isinstance(sensor, WinkSensor)) + smoke_detectors = get_smoke_and_co_detectors() + for device in smoke_detectors: + self.assertTrue(isinstance(device, WinkSmokeDetector) or isinstance(device, WinkSmokeSeverity) or + isinstance(device, WinkCoDetector) or isinstance(device, WinkCoSeverity)) + keys = get_keys() + for key in keys: + self.assertTrue(isinstance(key, WinkKey)) + switches = get_switches() + for switch in switches: + self.assertTrue(isinstance(switch, WinkBinarySwitch)) + locks = get_locks() + for lock in locks: + self.assertTrue(isinstance(lock, WinkLock)) + eggtrays = get_eggtrays() + for eggtray in eggtrays: + self.assertTrue(isinstance(eggtray, WinkEggtray)) + garage_doors = get_garage_doors() + for garage_door in garage_doors: + self.assertTrue(isinstance(garage_door, WinkGarageDoor)) + powerstrip = get_powerstrips() + self.assertEqual(len(powerstrip), 3) + for device in powerstrip: + self.assertTrue(isinstance(device, WinkPowerStrip) or isinstance(device, WinkPowerStripOutlet)) + shades = get_shades() + for shade in shades: + self.assertTrue(isinstance(shade, WinkShade)) + sirens = get_sirens() + for siren in sirens: + self.assertTrue(isinstance(siren, WinkSiren)) + keys = get_keys() + for key in keys: + self.assertTrue(isinstance(key, WinkKey)) + porkfolio = get_piggy_banks() + self.assertEqual(len(porkfolio), 2) + for device in porkfolio: + self.assertTrue(isinstance(device, WinkPorkfolioBalanceSensor) or isinstance(device, WinkPorkfolioNose)) + thermostats = get_thermostats() + for thermostat in thermostats: + self.assertTrue(isinstance(thermostat, WinkThermostat)) + hubs = get_hubs() + for hub in hubs: + self.assertTrue(isinstance(hub, WinkHub)) + fans = get_fans() + for fan in fans: + self.assertTrue(isinstance(fan, WinkFan)) + buttons = get_buttons() + for button in buttons: + self.assertTrue(isinstance(button, WinkButton)) + + def test_get_sensor_and_binary_switch_updated_states_from_api(self): + WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) + sensor_types = [WinkSensor, WinkHub, WinkPorkfolioBalanceSensor, WinkKey, WinkRemote, + WinkGang, WinkSmokeDetector, WinkSmokeSeverity, + WinkCoDetector, WinkCoSeverity, WinkButton] + skip_types = [WinkPowerStripOutlet] + devices = get_all_devices() + old_states = {} + for device in devices: + if type(device) in skip_types: + continue + device.api_interface = self.api_interface + if type(device) in sensor_types: + old_states[device.object_id() + device.name()] = device.state() + elif isinstance(device, WinkPorkfolioNose): + device.set_state("FFFF00") + elif device.state() is False or device.state() is True: + old_states[device.object_id()] = device.state() + device.set_state(not device.state()) + device.update_state() + for device in devices: + if type(device) in skip_types: + continue + if isinstance(device, WinkPorkfolioNose): + self.assertEqual(device.state(), "FFFF00") + elif type(device) in sensor_types: + self.assertEqual(device.state(), old_states.get(device.object_id() + device.name())) + elif device.object_id() in old_states: + self.assertEqual(not device.state(), old_states.get(device.object_id())) + + def test_get_light_bulbs_updated_states_from_api(self): + WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) + devices = get_light_bulbs() + old_states = {} + # Set states + for device in devices: + device.api_interface = self.api_interface + # Test HSB and powered + if device.supports_hue_saturation(): + old_states[device.object_id()] = device.state() + device.set_state(not device.state(), 0.5, color_hue_saturation=[0.5, 0.5]) + # Test temperature and powered + elif not device.supports_hue_saturation() and device.supports_temperature(): + old_states[device.object_id()] = device.state() + device.set_state(not device.state(), 0.5, color_kelvin=2500) + # Test Brightness and powered + else: + old_states[device.object_id()] = device.state() + device.set_state(not device.state(), 0.5) + # Check states + for device in devices: + # Test HSB and powered + if device.supports_hue_saturation(): + self.assertEqual([old_states.get(device.object_id()), 0.5, [0.5, 0.5]], + [not device.state(), device.brightness(), [device.color_saturation(), device.color_hue()]]) + # Test temperature and powered + elif not device.supports_hue_saturation() and device.supports_temperature(): + self.assertEqual([not old_states.get(device.object_id()), 0.5, 2500], [device.state(), device.brightness(), device.color_temperature_kelvin()]) + # Test Brightness and powered + else: + self.assertEqual([old_states.get(device.object_id()), 0.5], [not device.state(), device.brightness()]) + + def test_get_shade_updated_states_from_api(self): + WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) + devices = get_shades() + for device in devices: + device.api_interface = self.api_interface + device.set_state(1.0) + device.update_state() + for device in devices: + self.assertEqual(1.0, device.state()) + + def test_get_garage_door_updated_states_from_api(self): + WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) + devices = get_garage_doors() + for device in devices: + device.api_interface = self.api_interface + device.set_state(1) + device.update_state() + for device in devices: + self.assertEqual(1, device.state()) + + def test_get_powerstrip_outlets_updated_states_from_api(self): + WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) + skip_types = [WinkPowerStrip] + devices = get_powerstrips() + old_states = {} + for device in devices: + if type(device) in skip_types: + continue + device.api_interface = self.api_interface + if device.state() is False or device.state() is True: + old_states[device.object_id()] = device.state() + device.set_state(not device.state()) + device.update_state() + for device in devices: + if device.object_id() in old_states: + self.assertEqual(not device.state(), old_states.get(device.object_id())) + + def test_get_siren_updated_states_from_api(self): + WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) + devices = get_sirens() + old_states = {} + for device in devices: + device.api_interface = self.api_interface + old_states[device.object_id()] = device.state() + device.set_state(not device.state()) + device.set_mode("strobe") + device.set_auto_shutoff(120) + device.update_state() + self.assertEqual(not device.state(), old_states.get(device.object_id())) + self.assertEqual(device.mode(), "strobe") + self.assertEqual(device.auto_shutoff(), 120) + + def test_get_lock_updated_states_from_api(self): + WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) + devices = get_locks() + old_states = {} + for device in devices: + device.api_interface = self.api_interface + old_states[device.object_id()] = device.state() + device.set_state(not device.state()) + device.set_alarm_sensitivity(0.22) + device.set_alarm_mode("alert") + device.set_alarm_state(False) + device.set_vacation_mode(True) + device.set_beeper_mode(True) + device.update_state() + self.assertEqual(not device.state(), old_states.get(device.object_id())) + self.assertEqual(device.alarm_mode(), "alert") + self.assertFalse(device.alarm_enabled()) + self.assertTrue(device.vacation_mode_enabled()) + self.assertTrue(device.beeper_enabled()) + + def test_get_thermostat_updated_states_from_api(self): + WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) + devices = get_thermostats() + old_states = {} + for device in devices: + device.api_interface = self.api_interface + old_states[device.object_id()] = device.state() + if device.name() == "Home Hallway Thermostat": + device.set_operation_mode("off") + else: + device.set_operation_mode("auto") + device.set_away(True) + if device.has_fan(): + device.set_fan_mode("auto") + device.set_temperature(10, 50) + for device in devices: + if device.name() == "Home Hallway Thermostat": + self.assertFalse(device.is_on()) + else: + self.assertEqual(device.current_hvac_mode(), "auto") + self.assertTrue(device.away()) + if device.has_fan(): + self.assertEqual(device.current_fan_mode(), "auto") + self.assertEqual(10, device.current_min_set_point()) + self.assertEqual(50, device.current_max_set_point()) + + def test_get_camera_updated_states_from_api(self): + WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) + devices = get_cameras() + old_states = {} + for device in devices: + device.api_interface = self.api_interface + device.set_mode("away") + device.set_privacy(True) + device.update_state() + self.assertEqual(device.state(), "away") + self.assertTrue(device.private()) + + def test_get_fan_updated_states_from_api(self): + WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) + devices = get_fans() + old_states = {} + for device in devices: + device.api_interface = self.api_interface + device.set_fan_speed("auto") + device.set_fan_direction("reverse") + device.set_fan_timer(300) + device.update_state() + self.assertEqual(device.current_fan_speed(), "auto") + self.assertEqual(device.current_fan_direction(), "reverse") + self.assertEqual(device.current_timer(), 300) + + +class MockServerRequestHandler(BaseHTTPRequestHandler): + USERS_ME_WINK_DEVICES_PATTERN = re.compile(r'/users/me/wink_devices') + BAD_STATUS_PATTERN = re.compile(r'/401/') + NOT_FOUND_PATTERN = re.compile(r'/404/') + REFRESH_TOKEN_PATTERN = re.compile(r'/oauth2/token') + DEVICE_SPECIFIC_PATTERN = re.compile(r'/*/[0-9]*') + + def do_GET(self): + if re.search(self.BAD_STATUS_PATTERN, self.path): + # Add response status code. + self.send_response(requests.codes.unauthorized) + + # Add response headers. + self.send_header('Content-Type', 'application/json; charset=utf-8') + self.end_headers() + + return + elif re.search(self.NOT_FOUND_PATTERN, self.path): + # Add response status code. + self.send_response(requests.codes.not_found) + + # Add response headers. + self.send_header('Content-Type', 'application/json; charset=utf-8') + self.end_headers() + + return + elif re.search(self.USERS_ME_WINK_DEVICES_PATTERN, self.path): + # Add response status code. + self.send_response(requests.codes.ok) + + # Add response headers. + self.send_header('Content-Type', 'application/json; charset=utf-8') + self.end_headers() + + # Add response content. + response_content = json.dumps(USERS_ME_WINK_DEVICES) + self.wfile.write(response_content.encode('utf-8')) + return + + +def get_free_port(): + s = socket.socket(socket.AF_INET, type=socket.SOCK_STREAM) + s.bind(('localhost', 0)) + address, port = s.getsockname() + s.close() + return port + + +def start_mock_server(port): + mock_server = HTTPServer(('localhost', port), MockServerRequestHandler) + mock_server_thread = Thread(target=mock_server.serve_forever) + mock_server_thread.setDaemon(True) + mock_server_thread.start() + + +class MockApiInterface(): + + def set_device_state(self, device, state, id_override=None, type_override=None): + """ + :type device: WinkDevice + """ + object_id = id_override or device.object_id() + device_object_type = device.object_type() + object_type = type_override or device_object_type + return_dict = {} + for dict_device in USERS_ME_WINK_DEVICES.get('data'): + _object_id = dict_device.get("object_id") + if _object_id == object_id: + if device_object_type == "powerstrip": + set_state = state["outlets"][0]["desired_state"]["powered"] + dict_device["outlets"][0]["last_reading"]["powered"] = set_state + dict_device["outlets"][1]["last_reading"]["powered"] = set_state + return_dict["data"] = dict_device + elif device_object_type == "outlet": + index = device.index() + set_state = state["outlets"][index]["desired_state"]["powered"] + dict_device["outlets"][index]["last_reading"]["powered"] = set_state + return_dict["data"] = dict_device + else: + if "nose_color" in state: + dict_device["nose_color"] = state.get("nose_color") + else: + for key, value in state.get('desired_state').items(): + dict_device["last_reading"][key] = value + return_dict["data"] = dict_device + return return_dict + + def get_device_state(self, device, id_override=None, type_override=None): + """ + :type device: WinkDevice + """ + object_id = id_override or device.object_id() + return_dict = {} + for device in USERS_ME_WINK_DEVICES.get('data'): + _object_id = device.get("object_id") + if _object_id == object_id: + return_dict["data"] = device + return return_dict diff --git a/src/pywink/test/devices/api_responses/canary.json b/src/pywink/test/devices/api_responses/canary.json new file mode 100644 index 0000000..20dffdf --- /dev/null +++ b/src/pywink/test/devices/api_responses/canary.json @@ -0,0 +1,100 @@ +{ + "object_type": "camera", + "object_id": "44575", + "uuid": "03773fb5-1762-412c-9XXXXXXXXXXX", + "icon_id": null, + "icon_code": null, + "desired_state": { + "mode": "home", + "private": false + }, + "last_reading": { + "connection": true, + "connection_updated_at": 1470620172.44599, + "mode": "home", + "mode_updated_at": 1484410284.5465834, + "private": false, + "private_updated_at": 1484410284.5465834, + "serial_number": "C100G1504230", + "serial_number_updated_at": 1484410284.5465834, + "firmware_version": "v0.2.37", + "firmware_version_updated_at": 1484410284.5465834, + "location_id": "236092", + "location_id_updated_at": 1484410284.5465834, + "location_name": "Home", + "location_name_updated_at": 1484410284.5465834, + "motion_true": null, + "motion_true_updated_at": null, + "loudness_true": null, + "loudness_true_updated_at": null, + "desired_mode_updated_at": 1482118302.2474022, + "desired_private_updated_at": 1482118302.2474022, + "mode_changed_at": 1484410284.5465834, + "desired_mode_changed_at": 1482118302.2474022, + "desired_private_changed_at": 1482118302.2474022 + }, + "subscription": { + "pubnub": { + "subscribe_key": "sub-c-f7bf7f7exxxxxxxxxxxxxxxxxxxxxx", + "channel": "camera-44575|cd9d39XXXXXXXXXXXXXX6f430671440d39008|user-123465" + } + }, + "camera_id": "44575", + "name": "Kitchen", + "locale": "en_us", + "units": {}, + "created_at": 1470620172, + "hidden_at": null, + "capabilities": { + "fields": [ + { + "type": "boolean", + "field": "connection", + "mutability": "read-only" + }, + { + "type": "string", + "field": "mode", + "choices": [ + "armed", + "disarmed", + "privacy" + ], + "mutability": "read-write" + }, + { + "type": "string", + "field": "serial_number", + "mutability": "read-only" + }, + { + "type": "string", + "field": "firmware_version", + "mutability": "read-only" + }, + { + "type": "string", + "field": "location_id", + "mutability": "read-only" + }, + { + "type": "string", + "field": "location_name", + "mutability": "read-only" + } + ], + "home_security_device": true + }, + "manufacturer_device_model": "canary", + "manufacturer_device_id": "104eec7a985d42999b579967bc48aeb9", + "device_manufacturer": "canary", + "model_name": "Canary", + "upc_id": "371", + "upc_code": "canary", + "linked_service_id": "411738", + "lat_lng": [ + null, + null + ], + "location": "" +} \ No newline at end of file diff --git a/src/pywink/test/devices/api_responses/door_bell.json b/src/pywink/test/devices/api_responses/door_bell.json new file mode 100644 index 0000000..0af4622 --- /dev/null +++ b/src/pywink/test/devices/api_responses/door_bell.json @@ -0,0 +1,77 @@ +{ + "object_type":"door_bell", + "object_id":"45618", + "uuid":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "icon_id":null, + "icon_code":null, + "desired_state":{ + + }, + "last_reading":{ + "battery":1.0, + "battery_updated_at":1483588707.0458965, + "motion":false, + "motion_updated_at":1484327164.8382845, + "button_pressed":false, + "button_pressed_updated_at":1484257789.6575472, + "motion_true":"N/A", + "motion_true_updated_at":1484327124.6115115, + "button_pressed_true":"N/A", + "button_pressed_true_updated_at":1484257783.5323548, + "connection":true, + "connection_updated_at":1442873339.2134194, + "last_recording_cuepoint_id":"1790157958", + "last_recording_cuepoint_id_updated_at":1484327268.7613738, + "motion_changed_at":1484327164.8382845, + "motion_true_changed_at":1484327124.6115115, + "button_pressed_changed_at":1484257789.6575472, + "button_pressed_true_changed_at":1484257783.5323548, + "battery_changed_at":1483588707.0458965, + "last_recording_cuepoint_id_changed_at":1484327268.7613738 + }, + "subscription":{ + "pubnub":{ + "subscribe_key":"sub-x-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "channel":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx|door_bell-xx|user-xxxxxx" + } + }, + "door_bell_id":"45618", + "name":"Ring doorbell", + "locale":"en_us", + "units":{ + + }, + "created_at":1442873339, + "hidden_at":null, + "capabilities":{ + "fields":[ + { + "type":"percentage", + "field":"battery", + "mutability":"read-only" + }, + { + "type":"boolean", + "field":"motion", + "mutability":"read-only" + }, + { + "type":"boolean", + "field":"button_pressed", + "mutability":"read-only" + } + ] + }, + "manufacturer_device_model":"doorbell", + "manufacturer_device_id":"xxxxx", + "device_manufacturer":"ring", + "model_name":"Ring Video Doorbell", + "upc_id":"xxx", + "upc_code":"xxxxxxxxxxxx", + "linked_service_id":"xxxxxx", + "lat_lng":[ + 98.765432, + 12.345678 + ], + "location":"1234 Main St, City, ST XXXXX, USA" +} diff --git a/src/pywink/test/devices/api_responses/ecobee_thermostat.json b/src/pywink/test/devices/api_responses/ecobee_thermostat.json new file mode 100644 index 0000000..86d1fef --- /dev/null +++ b/src/pywink/test/devices/api_responses/ecobee_thermostat.json @@ -0,0 +1,279 @@ +{ + "object_type": "thermostat", + "object_id": "151594", + "uuid": "95cfc6b3-e47e-49XXXXXXXXXX", + "icon_id": null, + "icon_code": null, + "desired_state": { + "max_set_point": 25.555555555555557, + "min_set_point": 21.666666666666668, + "powered": true, + "fan_mode": "auto", + "units": { + "temperature": "f" + }, + "profile": "away", + "fan_run_time": 0, + "mode": "heat_only" + }, + "last_reading": { + "max_set_point": 25.555555555555557, + "max_set_point_updated_at": 1484495928.9564533, + "min_set_point": 21.666666666666668, + "min_set_point_updated_at": 1484495928.9564533, + "powered": true, + "powered_updated_at": 1484495928.9564533, + "fan_mode": "auto", + "fan_mode_updated_at": 1484495928.9564533, + "units": { + "temperature": "f" + }, + "units_updated_at": 1484495928.9564533, + "temperature": 22.388888888888889, + "temperature_updated_at": 1484495928.9564533, + "external_temperature": 0.83333333333333337, + "external_temperature_updated_at": 1484495928.9564533, + "humidity": 23, + "humidity_updated_at": 1484495928.9564533, + "cool_active": null, + "cool_active_updated_at": null, + "heat_active": null, + "heat_active_updated_at": null, + "aux_active": false, + "aux_active_updated_at": 1484495928.9564533, + "fan_active": null, + "fan_active_updated_at": null, + "min_min_set_point": 18.333333333333332, + "min_min_set_point_updated_at": 1484495928.9564533, + "max_min_set_point": 25.0, + "max_min_set_point_updated_at": 1484495928.9564533, + "min_max_set_point": 18.333333333333332, + "min_max_set_point_updated_at": 1484495928.9564533, + "max_max_set_point": 33.333333333333336, + "max_max_set_point_updated_at": 1484495928.9564533, + "deadband": 2.7777777777777777, + "deadband_updated_at": 1484495928.9564533, + "connection": true, + "connection_updated_at": 1484495928.9564533, + "firmware_version": "3.7.0.950", + "firmware_version_updated_at": 1484495928.9564533, + "revision": "170115083652", + "revision_updated_at": 1484495928.6663239, + "profile": "away", + "profile_updated_at": 1484495928.9564533, + "last_error": null, + "last_error_updated_at": 1484495928.9564533, + "fan_run_time": 0, + "fan_run_time_updated_at": 1484495928.9564533, + "smart_temperature": 21.5, + "smart_temperature_updated_at": 1484495928.9564533, + "occupied": false, + "occupied_updated_at": 1484495928.9564533, + "mode": "heat_only", + "mode_updated_at": 1484495928.9564533, + "modes_allowed": [ + "heat_only" + ], + "modes_allowed_updated_at": 1484495928.9564533, + "desired_max_set_point_updated_at": 1484075716.794369, + "desired_min_set_point_updated_at": 1484075718.4934707, + "desired_powered_updated_at": 1484075716.794369, + "desired_fan_mode_updated_at": 1484075716.794369, + "desired_units_updated_at": 1484075716.794369, + "desired_profile_updated_at": 1484075716.794369, + "desired_fan_run_time_updated_at": 1484075716.794369, + "desired_mode_updated_at": 1484075716.794369, + "revision_changed_at": 1484495928.6663239, + "temperature_changed_at": 1484495928.9564533, + "smart_temperature_changed_at": 1484495928.9564533, + "occupied_changed_at": 1484495798.3430297, + "external_temperature_changed_at": 1484495798.3430297, + "humidity_changed_at": 1484495798.3430297, + "max_set_point_changed_at": 1484485573.1182661, + "min_set_point_changed_at": 1484485573.1182661, + "connection_changed_at": 1481198069.3895316, + "last_error_changed_at": 1482772012.5994632, + "desired_min_set_point_changed_at": 1484075718.4934707 + }, + "subscription": { + "pubnub": { + "subscribe_key": "sub-c-f7bf7f7e-054XXXXXXXXXXX", + "channel": "d94450dcf3fb1ed02aXXXXXXXXXXXX08|thermostat-151594|user-123456" + } + }, + "thermostat_id": "151594", + "name": "Main Floor", + "locale": "en_us", + "units": {}, + "created_at": 1470784571, + "hidden_at": null, + "capabilities": { + "fields": [ + { + "type": "float", + "field": "max_set_point", + "mutability": "read-write" + }, + { + "type": "float", + "field": "min_set_point", + "mutability": "read-write" + }, + { + "type": "boolean", + "field": "powered", + "mutability": "read-write" + }, + { + "type": "selection", + "field": "fan_mode", + "choices": [ + "on", + "auto" + ], + "mutability": "read-write" + }, + { + "type": "nested_hash", + "field": "units", + "mutability": "read-write" + }, + { + "type": "float", + "field": "temperature", + "mutability": "read-only" + }, + { + "type": "float", + "field": "external_temperature", + "mutability": "read-only" + }, + { + "type": "percentage", + "field": "humidity", + "mutability": "read-only" + }, + { + "type": "boolean", + "field": "cool_active", + "mutability": "read-only" + }, + { + "type": "boolean", + "field": "heat_active", + "mutability": "read-only" + }, + { + "type": "boolean", + "field": "aux_active", + "mutability": "read-only" + }, + { + "type": "boolean", + "field": "fan_active", + "mutability": "read-only" + }, + { + "type": "float", + "field": "min_min_set_point", + "mutability": "read-only" + }, + { + "type": "float", + "field": "max_min_set_point", + "mutability": "read-only" + }, + { + "type": "float", + "field": "min_max_set_point", + "mutability": "read-only" + }, + { + "type": "float", + "field": "max_max_set_point", + "mutability": "read-only" + }, + { + "type": "float", + "field": "deadband", + "mutability": "read-only" + }, + { + "type": "boolean", + "field": "connection", + "mutability": "read-only" + }, + { + "type": "string", + "field": "firmware_version", + "mutability": "read-only" + }, + { + "type": "string", + "field": "revision", + "mutability": "read-only" + }, + { + "type": "selection", + "field": "profile", + "choices": [ + "home", + "away" + ], + "mutability": "read-write" + }, + { + "type": "string", + "field": "last_error", + "mutability": "read-only" + }, + { + "type": "integer", + "field": "fan_run_time", + "range": [ + 0, + 3300 + ], + "mutability": "read-write" + }, + { + "type": "float", + "field": "smart_temperature", + "mutability": "read-only" + }, + { + "type": "boolean", + "field": "occupied", + "mutability": "read-only" + }, + { + "field": "mode", + "type": "selection", + "mutability": "read-write", + "choices": [ + "heat_only" + ] + } + ], + "notification_robots": [ + "aux_active_notification" + ] + }, + "triggers": [], + "manufacturer_device_model": "athenaSmart", + "manufacturer_device_id": "317422180837", + "device_manufacturer": "ecobee", + "model_name": "ecobee3", + "upc_id": "333", + "upc_code": "627988301006", + "hub_id": null, + "local_id": null, + "radio_type": null, + "linked_service_id": "412774", + "lat_lng": [ + 12.34567, + -98.76543 + ], + "location": "", + "smart_schedule_enabled": false +} \ No newline at end of file diff --git a/src/pywink/test/devices/api_responses/eggminder.json b/src/pywink/test/devices/api_responses/eggminder.json new file mode 100644 index 0000000..493a88f --- /dev/null +++ b/src/pywink/test/devices/api_responses/eggminder.json @@ -0,0 +1,108 @@ +{ + "last_reading": { + "connection": true, + "connection_updated_at": 1449705443.6858568, + "battery": 0.48, + "battery_updated_at": 1484303694.7444918, + "inventory": 2, + "inventory_updated_at": 1484003813.3651533, + "age": 1483885579, + "age_updated_at": 1484003813.3651586, + "freshness_remaining": 2300966, + "freshness_remaining_updated_at": 1484003813.3651629, + "next_trigger_at": null, + "next_trigger_at_updated_at": null, + "egg_1_timestamp": 0.0, + "egg_1_timestamp_updated_at": 1484303694.693856, + "egg_2_timestamp": 0.0, + "egg_2_timestamp_updated_at": 1484303694.693856, + "egg_3_timestamp": 0.0, + "egg_3_timestamp_updated_at": 1484303694.693856, + "egg_4_timestamp": 0.0, + "egg_4_timestamp_updated_at": 1484303694.693856, + "egg_5_timestamp": 0.0, + "egg_5_timestamp_updated_at": 1484303694.693856, + "egg_6_timestamp": 0.0, + "egg_6_timestamp_updated_at": 1484303694.693856, + "egg_7_timestamp": 0.0, + "egg_7_timestamp_updated_at": 1484303694.693856, + "egg_8_timestamp": 1483885579.0, + "egg_8_timestamp_updated_at": 1484303694.693856, + "egg_9_timestamp": 1483885582.0, + "egg_9_timestamp_updated_at": 1484303694.693856, + "egg_10_timestamp": 0.0, + "egg_10_timestamp_updated_at": 1484303694.693856, + "egg_11_timestamp": 0.0, + "egg_11_timestamp_updated_at": 1484303694.693856, + "egg_12_timestamp": 0.0, + "egg_12_timestamp_updated_at": 1484303694.693856, + "egg_13_timestamp": 0.0, + "egg_13_timestamp_updated_at": 1484303694.693856, + "egg_14_timestamp": 0.0, + "egg_14_timestamp_updated_at": 1484303694.693856, + "egg_2_timestamp_changed_at": 1483370989.837141, + "egg_3_timestamp_changed_at": 1483886546.9117394, + "egg_4_timestamp_changed_at": 1483845698.9563267, + "egg_8_timestamp_changed_at": 1483885477.468328, + "egg_9_timestamp_changed_at": 1483885480.1099362, + "egg_10_timestamp_changed_at": 1483886545.5761147, + "egg_11_timestamp_changed_at": 1483885473.66499, + "egg_12_timestamp_changed_at": 1483885476.2160401, + "battery_changed_at": 1484003813.3443286, + "egg_1_timestamp_changed_at": 1484002038.8975396, + "egg_5_timestamp_changed_at": 1483885478.799624, + "egg_6_timestamp_changed_at": 1483885481.5076609, + "egg_13_timestamp_changed_at": 1483885480.1099362, + "egg_7_timestamp_changed_at": 1483885485.4073744, + "egg_14_timestamp_changed_at": 1483885482.7856996 + }, + "eggs": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1483885579.0, + 1483885582.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "freshness_period": 2419200, + "object_type": "eggtray", + "object_id": "153869", + "uuid": "3dad0619-2914-47e3-b49d-393dbbba0c3b", + "icon_id": null, + "icon_code": null, + "subscription": { + "pubnub": { + "subscribe_key": "sub-c-f7bf7fXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "channel": "25a3bd1031XXXXXXXXXXXXXXXXXXXXXXcfc|eggtray-153869|user-XXXXXX" + } + }, + "eggtray_id": "153869", + "name": "Egg Minder", + "locale": "en_us", + "units": {}, + "created_at": 1449705443, + "hidden_at": null, + "capabilities": { + "needs_wifi_network_list": true + }, + "triggers": [], + "device_manufacturer": "quirky_ge", + "model_name": "Egg Minder", + "upc_id": "23", + "upc_code": "814434017233", + "lat_lng": [ + 98.765432, + -12.345678 + ], + "location": "", + "mac_address": "0c2a69012345", + "serial": "ABAA00012345" +} diff --git a/src/pywink/test/devices/api_responses/fan.json b/src/pywink/test/devices/api_responses/fan.json new file mode 100644 index 0000000..d85d445 --- /dev/null +++ b/src/pywink/test/devices/api_responses/fan.json @@ -0,0 +1,109 @@ +{ + "object_type":"fan", + "object_id":"1359", + "uuid":"2cf8d024-6838-4db6-82f6-0eca51123456", + "icon_id":null, + "icon_code":null, + "desired_state":{ + "mode":"lowest", + "powered":true, + "timer":0, + "direction":null + }, + "last_reading":{ + "mode":"lowest", + "mode_updated_at":1481335377.2255096, + "powered":true, + "powered_updated_at":1481335377.2255096, + "timer":0, + "timer_updated_at":1481335377.2255096, + "direction":"forward", + "direction_updated_at":null, + "connection":true, + "connection_updated_at":1481714301.2966304, + "firmware_version":"0.0b00 / 0.0b0e", + "firmware_version_updated_at":1481335377.2255096, + "firmware_date_code":null, + "firmware_date_code_updated_at":null, + "desired_mode_updated_at":1481335684.7678573, + "desired_powered_updated_at":1481335684.7678573, + "desired_timer_updated_at":1481335810.1496878, + "desired_direction_updated_at":1481335810.1496878 + }, + "subscription":{ + "pubnub":{ + "subscribe_key":"sub-c-f7bf7f7e-1234-11e3-a5e8-02ee2d123456", + "channel":"7ab843be9ac23f543f907798fc1508c214ee06f8|fan-1359|user-212345" + } + }, + "fan_id":"1359", + "name":"Fan", + "locale":"en_us", + "units":{ + + }, + "created_at":1481335344, + "hidden_at":null, + "capabilities":{ + "fields":[ + { + "type":"selection", + "field":"mode", + "choices":[ + "lowest", + "low", + "medium", + "high", + "auto" + ], + "mutability":"read-write" + }, + { + "type":"boolean", + "field":"powered", + "mutability":"read-write" + }, + { + "type":"integer", + "field":"timer", + "range":[ + 0, + 65535 + ], + "mutability":"read-write" + }, + { + "type":"selection", + "field":"direction", + "choices":[ + "forward", + "reverse" + ], + "mutability":"read-write" + }, + { + "type":"boolean", + "field":"connection", + "mutability":"read-only" + } + ] + }, + "triggers":[ + + ], + "manufacturer_device_model":"home_decorators_home_decorators_fan", + "manufacturer_device_id":null, + "device_manufacturer":"home_decorators", + "model_name":"Ceiling Fan", + "upc_id":"486", + "upc_code":"home_decorators_fan", + "gang_id":"56113", + "hub_id":"288962", + "local_id":"48.1", + "radio_type":"zigbee", + "lat_lng":[ + 12.345678, + -98.765432 + ], + "location":"" +} diff --git a/src/pywink/test/devices/api_responses/garage_door.json b/src/pywink/test/devices/api_responses/garage_door.json new file mode 100644 index 0000000..91f5356 --- /dev/null +++ b/src/pywink/test/devices/api_responses/garage_door.json @@ -0,0 +1,64 @@ +{ + "object_type": "garage_door", + "object_id": "55086", + "uuid": "7b967c4c-7484-4dd1-adf9-c410ea6caa8b", + "icon_id": null, + "icon_code": null, + "desired_state": { + "position": 0.0 + }, + "last_reading": { + "position_opened": "N/A", + "position_opened_updated_at": 1484333056.554, + "tamper_detected_true": null, + "tamper_detected_true_updated_at": null, + "connection": true, + "connection_updated_at": 1484333392.2095273, + "position": 0.0, + "position_updated_at": 1484333391.737, + "battery": 1.0, + "battery_updated_at": 1482765045.4644408, + "fault": false, + "fault_updated_at": 1482765045.4644408, + "disabled": null, + "disabled_updated_at": null, + "control_enabled": true, + "control_enabled_updated_at": 1482765045.4644408, + "desired_position_updated_at": 1482764972.9735985, + "position_changed_at": 1484333391.737, + "position_opened_changed_at": 1484333056.554, + "desired_position_changed_at": 1482764972.9735985 + }, + "subscription": { + "pubnub": { + "subscribe_key": "sub-c-f7bf7f7e-054XXXXXXXXXXXXXXXXXXXXXX", + "channel": "046eed818f83b055XXXXXXXXXXXXXXXXXXXXXXX|garage_door-55086|user-123456" + } + }, + "garage_door_id": "55086", + "name": "Garage Door Opener", + "locale": "en_us", + "units": {}, + "created_at": 1466884441, + "hidden_at": null, + "capabilities": { + "home_security_device": true + }, + "triggers": [], + "manufacturer_device_model": "chamberlain_vgdo", + "manufacturer_device_id": "9796572", + "device_manufacturer": "chamberlain", + "model_name": "3/4 HPS Wi-Fi Garage Door Opener", + "upc_id": "640", + "upc_code": "chamberlain_hd750wf", + "hub_id": null, + "local_id": null, + "radio_type": null, + "linked_service_id": "378592", + "lat_lng": [ + 98.765432, + -12.345678 + ], + "location": "", + "order": null +} diff --git a/src/pywink/test/devices/api_responses/ge_link_bulb.json b/src/pywink/test/devices/api_responses/ge_link_bulb.json new file mode 100644 index 0000000..6504125 --- /dev/null +++ b/src/pywink/test/devices/api_responses/ge_link_bulb.json @@ -0,0 +1,61 @@ +{ + "object_type": "light_bulb", + "object_id": "2194253", + "uuid": "9152b9fd-94c2-4214-b4ce-723s4f6da54f", + "icon_id": "71", + "icon_code": "light_bulb-light_bulb", + "desired_state": { + "powered": false, + "brightness": 1.0 + }, + "last_reading": { + "connection": true, + "connection_updated_at": 1484289307.4707525, + "firmware_version": "0.1b03 / 0.4b00", + "firmware_version_updated_at": 1484289307.4707525, + "firmware_date_code": "20140812", + "firmware_date_code_updated_at": 1484289307.4707525, + "powered": false, + "powered_updated_at": 1484289307.4707525, + "brightness": 1.0, + "brightness_updated_at": 1484289307.4707525, + "desired_powered_updated_at": 1484278871.427912, + "desired_brightness_updated_at": 1484278871.427912, + "desired_powered_changed_at": 1484278871.427912, + "desired_brightness_changed_at": 1484278871.427912, + "powered_changed_at": 1484278871.3907311, + "connection_changed_at": 1481852111.8699017, + "brightness_changed_at": 1482898401.8100445 + }, + "subscription": { + "pubnub": { + "subscribe_key": "sub-c-f7bf7f7e-XXXXXXXXXXXXXXXXXXXXXXXXXXX", + "channel": "7ceb972637bfc74e5XXXXXXXXXXXXXXXXXXXXXXXX|light_bulb-2194253|user-123456" + } + }, + "light_bulb_id": "2194253", + "name": "Bedroom lamp", + "locale": "en_us", + "units": {}, + "created_at": 1480089549, + "hidden_at": null, + "capabilities": {}, + "triggers": [], + "manufacturer_device_model": "ge_bulb", + "manufacturer_device_id": null, + "device_manufacturer": "ge", + "model_name": "GE Light Bulb", + "upc_id": "483", + "upc_code": "ge_zigbee4", + "gang_id": null, + "hub_id": "302528", + "local_id": "29", + "radio_type": "zigbee", + "linked_service_id": null, + "lat_lng": [ + 98.76543, + -12.345678 + ], + "location": "", + "order": 0 +} diff --git a/src/pywink/test/devices/api_responses/ge_zwave_switch.json b/src/pywink/test/devices/api_responses/ge_zwave_switch.json new file mode 100644 index 0000000..b33e891 --- /dev/null +++ b/src/pywink/test/devices/api_responses/ge_zwave_switch.json @@ -0,0 +1,64 @@ +{ + "object_type": "binary_switch", + "object_id": "239549", + "uuid": "e055f78d-f865-492XXXXXXXXXXX", + "icon_id": "52", + "icon_code": "binary_switch-light_bulb_dumb", + "desired_state": { + "powered": false + }, + "last_reading": { + "connection": true, + "connection_updated_at": 1484289288.6979613, + "powered": false, + "powered_updated_at": 1484289288.6979613, + "desired_powered_updated_at": 1484278866.6923306, + "powered_changed_at": 1484278866.659353, + "desired_powered_changed_at": 1484278866.6923306 + }, + "subscription": { + "pubnub": { + "subscribe_key": "sub-c-f7bf7f7e-054XXXXXXXXXXXXXX", + "channel": "024254c58e06548f0aXXXXXXXXXXXXXXXXX0ce|binary_switch-239549|user-123456" + } + }, + "binary_switch_id": "239549", + "name": "GE Zwave Switch", + "locale": "en_us", + "units": {}, + "created_at": 1468703723, + "hidden_at": null, + "capabilities": { + "fields": [ + { + "type": "boolean", + "field": "connection", + "mutability": "read-only" + }, + { + "type": "boolean", + "field": "powered", + "mutability": "read-write" + } + ] + }, + "triggers": [], + "manufacturer_device_model": "leviton_dzs15", + "manufacturer_device_id": null, + "device_manufacturer": "leviton", + "model_name": "Switch", + "upc_id": "95", + "upc_code": "078477389362", + "gang_id": null, + "hub_id": "302528", + "local_id": "24", + "radio_type": "zwave", + "linked_service_id": null, + "current_budget": null, + "lat_lng": [ + 98.765432, + -12.345678 + ], + "location": "", + "order": 0 +} diff --git a/src/pywink/test/devices/api_responses/generic_liquid_detected_sensor.json b/src/pywink/test/devices/api_responses/generic_liquid_detected_sensor.json new file mode 100644 index 0000000..5c02e55 --- /dev/null +++ b/src/pywink/test/devices/api_responses/generic_liquid_detected_sensor.json @@ -0,0 +1,70 @@ +{ + "last_event": { + "brightness_occurred_at": null, + "loudness_occurred_at": null, + "vibration_occurred_at": null + }, + "object_type": "sensor_pod", + "object_id": "240456", + "uuid": "0e614bf2-ef85-4bc9-90d8-8XXXXXXXXXXXX", + "icon_id": null, + "icon_code": null, + "desired_state": {}, + "last_reading": { + "liquid_detected": false, + "liquid_detected_updated_at": 1484311499.5859754, + "battery": 0.7, + "battery_updated_at": 1484311499.5859754, + "liquid_detected_true": "N/A", + "liquid_detected_true_updated_at": 1468712105.5921495, + "connection": true, + "connection_updated_at": 1484311499.5859754, + "agent_session_id": null, + "agent_session_id_updated_at": null, + "battery_changed_at": 1484311499.5859754 + }, + "subscription": { + "pubnub": { + "subscribe_key": "sub-c-f7bf7f7e-XXXXXXXXXXXXXXXXXXXX", + "channel": "5d54ad5517ccc1c0569XXXXXXXXXXXXX|sensor_pod-240456|user-123456" + } + }, + "sensor_pod_id": "240456", + "name": "Dishwasher", + "locale": "en_us", + "units": {}, + "created_at": 1468619516, + "hidden_at": null, + "capabilities": { + "sensor_types": [ + { + "type": "boolean", + "field": "liquid_detected", + "mutability": "read-only" + }, + { + "type": "percentage", + "field": "battery", + "mutability": "read-only" + } + ], + "desired_state_fields": [] + }, + "triggers": [], + "manufacturer_device_model": "aeon_labs_dsb45_zwus", + "manufacturer_device_id": null, + "device_manufacturer": "aeon_labs", + "model_name": "Z-Wave Water Sensor Sensor", + "upc_id": "339", + "upc_code": "generic_water_sensor", + "gang_id": null, + "hub_id": "302528", + "local_id": "23", + "radio_type": "zwave", + "linked_service_id": null, + "lat_lng": [ + 98.76543, + -12.345678 + ], + "location": "" +} diff --git a/src/pywink/test/devices/api_responses/go_control_door_window_sensor.json b/src/pywink/test/devices/api_responses/go_control_door_window_sensor.json new file mode 100644 index 0000000..11ce00e --- /dev/null +++ b/src/pywink/test/devices/api_responses/go_control_door_window_sensor.json @@ -0,0 +1,78 @@ +{ + "last_event": { + "brightness_occurred_at": null, + "loudness_occurred_at": null, + "vibration_occurred_at": null + }, + "object_type": "sensor_pod", + "object_id": "198184", + "uuid": "8cbfa16d-4507-46XXXXXXXXXXXXXX", + "icon_id": null, + "icon_code": null, + "desired_state": {}, + "last_reading": { + "opened": true, + "opened_updated_at": 1484290084.0072639, + "tamper_detected": null, + "tamper_detected_updated_at": 1463850069.60705, + "battery": 1.0, + "battery_updated_at": 1484290084.0072639, + "tamper_detected_true": "N/A", + "tamper_detected_true_updated_at": 1463850006.8764803, + "connection": true, + "connection_updated_at": 1484290084.0072639, + "agent_session_id": null, + "agent_session_id_updated_at": null, + "opened_changed_at": 1484178922.6510079 + }, + "subscription": { + "pubnub": { + "subscribe_key": "sub-c-f7bf7fXXXXXXXXXXXXXXXXXX", + "channel": "51bc31ce6300aXXXXXXXXXXXXXXXXXXXXXXXX|sensor_pod-198184|user-123456" + } + }, + "sensor_pod_id": "198184", + "name": "Bedroom door", + "locale": "en_us", + "units": {}, + "created_at": 1463849993, + "hidden_at": null, + "capabilities": { + "home_security_device": true, + "sensor_types": [ + { + "type": "boolean", + "field": "opened", + "mutability": "read-only" + }, + { + "type": "boolean", + "field": "tamper_detected", + "mutability": "read-only" + }, + { + "type": "percentage", + "field": "battery", + "mutability": "read-only" + } + ], + "desired_state_fields": [] + }, + "triggers": [], + "manufacturer_device_model": "linear_wadwaz_1", + "manufacturer_device_id": null, + "device_manufacturer": "linear", + "model_name": "Z-Wave Door / Window Transmitter", + "upc_id": "189", + "upc_code": "9386312509", + "gang_id": null, + "hub_id": "302528", + "local_id": "17", + "radio_type": "zwave", + "linked_service_id": null, + "lat_lng": [ + 98.765432, + -12.345678 + ], + "location": "" +} diff --git a/src/pywink/test/devices/api_responses/go_control_motion_temperature_sensor.json b/src/pywink/test/devices/api_responses/go_control_motion_temperature_sensor.json new file mode 100644 index 0000000..28b86f6 --- /dev/null +++ b/src/pywink/test/devices/api_responses/go_control_motion_temperature_sensor.json @@ -0,0 +1,88 @@ +{ + "last_event": { + "brightness_occurred_at": null, + "loudness_occurred_at": null, + "vibration_occurred_at": null + }, + "object_type": "sensor_pod", + "object_id": "198198", + "uuid": "ac7e2368-18d9-4a0XXXXXXXXXXXX", + "icon_id": null, + "icon_code": null, + "desired_state": {}, + "last_reading": { + "motion": true, + "motion_updated_at": 1484301541.856606, + "battery": 1.0, + "battery_updated_at": 1484301541.856606, + "tamper_detected": false, + "tamper_detected_updated_at": 1465755520.3955958, + "temperature": 18.888888888888889, + "temperature_updated_at": 1484301541.856606, + "motion_true": "N/A", + "motion_true_updated_at": 1484289240.1748033, + "tamper_detected_true": "N/A", + "tamper_detected_true_updated_at": 1463851770.2494435, + "connection": true, + "connection_updated_at": 1484301541.856606, + "agent_session_id": null, + "agent_session_id_updated_at": null, + "temperature_changed_at": 1484301541.856606, + "motion_changed_at": 1484289240.1748033, + "motion_true_changed_at": 1484289240.1748033 + }, + "subscription": { + "pubnub": { + "subscribe_key": "sub-c-f7bf7f7e-05XXXXXXXXXXXXXXXXXXX", + "channel": "a3a799039b70c913XXXXXXXXXXXXXXXXXXXXXXXXXXX|sensor_pod-198198|user-123456" + } + }, + "sensor_pod_id": "198198", + "name": "Bedroom", + "locale": "en_us", + "units": {}, + "created_at": 1463851768, + "hidden_at": null, + "capabilities": { + "sensor_types": [ + { + "type": "boolean", + "field": "motion", + "mutability": "read-only" + }, + { + "type": "percentage", + "field": "battery", + "mutability": "read-only" + }, + { + "type": "boolean", + "field": "tamper_detected", + "mutability": "read-only" + }, + { + "type": "float", + "field": "temperature", + "mutability": "read-only" + } + ], + "desired_state_fields": [] + }, + "triggers": [], + "manufacturer_device_model": "linear_wapirz_1", + "manufacturer_device_id": null, + "device_manufacturer": "linear", + "model_name": "Z-Wave Passive Infrared (PIR) Sensor", + "upc_id": "207", + "upc_code": "093863125102", + "gang_id": null, + "hub_id": "302528", + "local_id": "19", + "radio_type": "zwave", + "linked_service_id": null, + "lat_lng": [ + 98.765432, + -12.345678 + ], + "location": "" +} diff --git a/src/pywink/test/devices/api_responses/go_control_siren.json b/src/pywink/test/devices/api_responses/go_control_siren.json new file mode 100644 index 0000000..56ed87f --- /dev/null +++ b/src/pywink/test/devices/api_responses/go_control_siren.json @@ -0,0 +1,55 @@ +{ + "object_type": "siren", + "object_id": "6379", + "uuid": "8ec0d088-86ac-40XXXXXXXXXXXXXXX", + "icon_id": null, + "icon_code": null, + "desired_state": { + "auto_shutoff": 60, + "mode": "siren_and_strobe", + "powered": false + }, + "last_reading": { + "connection": true, + "connection_updated_at": 1484310589.6596723, + "battery": 1.0, + "battery_updated_at": 1484310589.6596723, + "auto_shutoff": 60, + "auto_shutoff_updated_at": 1484310589.6596723, + "mode": "siren_and_strobe", + "mode_updated_at": 1484310589.6596723, + "powered": false, + "powered_updated_at": 1484310589.6596723, + "desired_auto_shutoff_updated_at": 1483989455.601872, + "desired_mode_updated_at": 1483989581.8294075, + "desired_powered_updated_at": 1483989581.8294075, + "battery_changed_at": 1484310589.6596723, + "desired_auto_shutoff_changed_at": 1483989455.601872, + "auto_shutoff_changed_at": 1484169637.6828098 + }, + "subscription": { + "pubnub": { + "subscribe_key": "sub-c-f7bf7f7e-0542-1XXXXXXXXXXXX", + "channel": "464018bc24211fXXXXXXXXXXXXXX|siren-6379|user-123456" + } + }, + "siren_id": "6379", + "name": "Siren", + "locale": "en_us", + "units": {}, + "created_at": 1452812587, + "hidden_at": null, + "capabilities": {}, + "device_manufacturer": "linear", + "model_name": "Wireless Siren & Strobe (Wireless)", + "upc_id": "243", + "upc_code": "wireless_linear_siren", + "hub_id": "302528", + "local_id": "8", + "radio_type": "zwave", + "lat_lng": [ + 98.765432, + -12.345678 + ], + "location": "" +} diff --git a/src/pywink/test/devices/api_responses/go_control_thermostat.json b/src/pywink/test/devices/api_responses/go_control_thermostat.json new file mode 100644 index 0000000..59502b5 --- /dev/null +++ b/src/pywink/test/devices/api_responses/go_control_thermostat.json @@ -0,0 +1,193 @@ +{ + "object_type": "thermostat", + "object_id": "180437", + "uuid": "02ebfab9-b143-4d7XXXXXXXXXXXXXX", + "icon_id": null, + "icon_code": null, + "desired_state": { + "max_set_point": 23.888888888888889, + "min_set_point": 21.111111111111111, + "powered": true, + "fan_mode": "auto", + "mode": "heat_only", + "units": { + "temperature": "f" + } + }, + "last_reading": { + "max_set_point": 23.888888888888889, + "max_set_point_updated_at": 1484306655.1207185, + "min_set_point": 21.111111111111111, + "min_set_point_updated_at": 1484306655.1207185, + "powered": true, + "powered_updated_at": 1484306655.1207185, + "units": { + "temperature": "f" + }, + "units_updated_at": 1484306655.1207185, + "temperature": 20.0, + "temperature_updated_at": 1484306655.1207185, + "external_temperature": null, + "external_temperature_updated_at": null, + "min_min_set_point": null, + "min_min_set_point_updated_at": null, + "max_min_set_point": null, + "max_min_set_point_updated_at": null, + "min_max_set_point": null, + "min_max_set_point_updated_at": null, + "max_max_set_point": null, + "max_max_set_point_updated_at": null, + "deadband": null, + "deadband_updated_at": null, + "connection": true, + "connection_updated_at": 1484306655.1207185, + "fan_mode": "auto", + "fan_mode_updated_at": 1484306655.1207185, + "mode": "heat_only", + "mode_updated_at": 1484306655.1207185, + "modes_allowed": [ + "heat_only", + "cool_only", + "auto", + "aux" + ], + "modes_allowed_updated_at": 1484306655.1207185, + "desired_max_set_point_updated_at": 1483674322.833992, + "desired_min_set_point_updated_at": 1483674177.4763274, + "desired_powered_updated_at": 1483674322.833992, + "desired_fan_mode_updated_at": 1483674322.833992, + "desired_mode_updated_at": 1483674322.833992, + "desired_units_updated_at": 1483674322.833992, + "connection_changed_at": 1481846030.3876073, + "powered_changed_at": 1481910720.9456556, + "mode_changed_at": 1482284867.5793991, + "min_set_point_changed_at": 1484175926.448101, + "max_set_point_changed_at": 1483135409.1739059, + "modes_allowed_changed_at": 1481592666.1772993, + "fan_mode_changed_at": 1481666322.8171082, + "units_changed_at": 1481592666.1772993, + "temperature_changed_at": 1484306655.1207185, + "desired_powered_changed_at": 1482284867.7936459, + "desired_max_set_point_changed_at": 1481598177.4249227, + "desired_min_set_point_changed_at": 1483674177.4763274, + "desired_fan_mode_changed_at": 1481666323.3382764, + "desired_mode_changed_at": 1482284867.7936459, + "desired_units_changed_at": 1481598177.4249227 + }, + "subscription": { + "pubnub": { + "subscribe_key": "sub-c-f7bf7f7e-054XXXXXXXXXXXXXXXXXXXXXX", + "channel": "ed68f119739af6328d85d42538cefff10691fbfe|thermostat-180437|user-123456" + } + }, + "thermostat_id": "180437", + "name": "GoControl Thermostat", + "locale": "en_us", + "units": {}, + "created_at": 1481592656, + "hidden_at": null, + "capabilities": { + "fields": [ + { + "type": "float", + "field": "max_set_point", + "mutability": "read-write" + }, + { + "type": "float", + "field": "min_set_point", + "mutability": "read-write" + }, + { + "type": "boolean", + "field": "powered", + "mutability": "read-write" + }, + { + "type": "nested_hash", + "field": "units", + "mutability": "read-only" + }, + { + "type": "float", + "field": "temperature", + "mutability": "read-only" + }, + { + "type": "float", + "field": "external_temperature", + "mutability": "read-only" + }, + { + "type": "float", + "field": "min_min_set_point", + "mutability": "read-only" + }, + { + "type": "float", + "field": "max_min_set_point", + "mutability": "read-only" + }, + { + "type": "float", + "field": "min_max_set_point", + "mutability": "read-only" + }, + { + "type": "float", + "field": "max_max_set_point", + "mutability": "read-only" + }, + { + "type": "float", + "field": "deadband", + "mutability": "read-only" + }, + { + "type": "boolean", + "field": "connection", + "mutability": "read-only" + }, + { + "type": "selection", + "field": "fan_mode", + "choices": [ + "on", + "auto" + ], + "mutability": "read-write" + }, + { + "field": "mode", + "type": "selection", + "mutability": "read-write", + "choices": [ + "heat_only", + "cool_only", + "auto", + "aux" + ] + } + ], + "notification_robots": [ + "aux_active_notification" + ] + }, + "triggers": [], + "manufacturer_device_model": null, + "manufacturer_device_id": null, + "device_manufacturer": null, + "model_name": null, + "upc_id": "129", + "upc_code": "generic_zwave_thermostat", + "hub_id": "302528", + "local_id": "31", + "radio_type": "zwave", + "linked_service_id": null, + "lat_lng": [ + 98.765432, + -12.345678 + ], + "location": "", + "smart_schedule_enabled": false +} diff --git a/src/pywink/test/devices/api_responses/hue_hub.json b/src/pywink/test/devices/api_responses/hue_hub.json new file mode 100644 index 0000000..f577643 --- /dev/null +++ b/src/pywink/test/devices/api_responses/hue_hub.json @@ -0,0 +1,81 @@ +{ + "object_type": "hub", + "object_id": "278026", + "uuid": "04f17dcc-b159-44bf-b4XXXXXXXXXX", + "icon_id": null, + "icon_code": null, + "desired_state": { + "pairing_mode": null, + "pairing_device_type_selector": null + }, + "last_reading": { + "connection": true, + "connection_updated_at": 1484457292.1078665, + "agent_session_id": null, + "agent_session_id_updated_at": null, + "pairing_mode": null, + "pairing_mode_updated_at": null, + "pairing_device_type_selector": null, + "pairing_device_type_selector_updated_at": null, + "kidde_radio_code_updated_at": null, + "updating_firmware": null, + "updating_firmware_updated_at": null, + "firmware_version": null, + "firmware_version_updated_at": null, + "update_needed": false, + "update_needed_updated_at": 1446676515.966435, + "mac_address": null, + "mac_address_updated_at": null, + "zigbee_mac_address": null, + "zigbee_mac_address_updated_at": null, + "ip_address": null, + "ip_address_updated_at": null, + "hub_version": null, + "hub_version_updated_at": null, + "app_version": null, + "app_version_updated_at": null, + "transfer_mode": null, + "transfer_mode_updated_at": null, + "connection_type": null, + "connection_type_updated_at": null, + "wifi_credentials_present": null, + "wifi_credentials_present_updated_at": null, + "desired_pairing_mode": "idle", + "desired_pairing_mode_updated_at": null, + "desired_pairing_device_type_selector": null, + "desired_pairing_device_type_selector_updated_at": null, + "desired_kidde_radio_code": null, + "desired_kidde_radio_code_updated_at": null, + "connection_changed_at": 1484372222.3153114 + }, + "subscription": { + "pubnub": { + "subscribe_key": "sub-c-f7bf7f7e-054XXXXXXXXXXXXXXX", + "channel": "a95f0e3aed9516151de64a401b690d8a48b21bd6|hub-278026|user-123456" + } + }, + "hub_id": "278026", + "name": "Philips hue", + "locale": "en_us", + "units": {}, + "created_at": 1446676515, + "hidden_at": null, + "capabilities": {}, + "triggers": [], + "manufacturer_device_model": "philips", + "manufacturer_device_id": "00:17:88:19:24:12", + "device_manufacturer": "philips", + "model_name": "Philips", + "upc_id": "321", + "upc_code": "philips_hue_hub", + "linked_service_id": "219727", + "lat_lng": [ + null, + null + ], + "location": "", + "update_needed": false, + "configuration": { + "kidde_radio_code": null + } +} \ No newline at end of file diff --git a/src/pywink/test/devices/api_responses/kidde_smoke_detector.json b/src/pywink/test/devices/api_responses/kidde_smoke_detector.json new file mode 100644 index 0000000..ac318fe --- /dev/null +++ b/src/pywink/test/devices/api_responses/kidde_smoke_detector.json @@ -0,0 +1,51 @@ +{ + "object_type":"smoke_detector", + "object_id":"12345", + "uuid":"54feac93-327e-4f04-a8ba-500c88e95245", + "icon_id":null, + "icon_code":null, + "last_reading":{ + "connection":true, + "connection_updated_at":1462586187.9383092, + "battery":0.9, + "battery_updated_at":1462586188.4449866, + "co_detected":true, + "co_detected_updated_at":1462586188.4449866, + "smoke_detected":false, + "smoke_detected_updated_at":1471015253.2221863, + "test_activated":false, + "test_activated_updated_at":1462997176.8458738 + }, + "subscription":{ + "pubnub":{ + "subscribe_key":"REMOVED", + "channel":"Removed|smoke_detector-REMOVED|user-REMOVED" + } + }, + "smoke_detector_id":"12345", + "name":"Hallway Smoke Detector", + "locale":"en_us", + "units":{ + + }, + "created_at":1462586187, + "hidden_at":null, + "capabilities":{ + + }, + "manufacturer_device_model":"kidde_smoke_alarm", + "manufacturer_device_id":null, + "device_manufacturer":"kidde", + "model_name":"Smoke Alarm", + "upc_id":"524", + "upc_code":"kidde_smoke_alarm", + "hub_id":"REMOVED", + "local_id":null, + "radio_type":"kidde", + "linked_service_id":null, + "lat_lng":[ + 39.00000, + -77.00000 + ], + "location":"REMOVED" +} \ No newline at end of file diff --git a/src/pywink/test/devices/api_responses/lightify_rgbw_bulb.json b/src/pywink/test/devices/api_responses/lightify_rgbw_bulb.json new file mode 100644 index 0000000..4e19d4c --- /dev/null +++ b/src/pywink/test/devices/api_responses/lightify_rgbw_bulb.json @@ -0,0 +1,131 @@ +{ + "object_type": "light_bulb", + "object_id": "1972803", + "uuid": "e16ce71d-0e92-4ddf-9XXXXXXXXXX", + "icon_id": "76", + "icon_code": "light_bulb-sylvania_light", + "desired_state": { + "powered": true, + "brightness": 0.02, + "color_model": "hsb", + "hue": 0.0, + "saturation": 1.0, + "color_temperature": 2755 + }, + "last_reading": { + "connection": true, + "connection_updated_at": 1484308234.6190915, + "powered": true, + "powered_updated_at": 1484308234.6190915, + "brightness": 0.02, + "brightness_updated_at": 1484308234.6190915, + "color_model": "hsb", + "color_model_updated_at": 1484308234.6190915, + "hue": 0.0, + "hue_updated_at": 1484308234.6190915, + "saturation": 1.0, + "saturation_updated_at": 1484308234.6190915, + "color_temperature": 2755, + "color_temperature_updated_at": 1484308234.6190915, + "firmware_version": "0.1b02 / 0.4b92", + "firmware_version_updated_at": 1484308234.6190915, + "firmware_date_code": "20151012N****", + "firmware_date_code_updated_at": 1484308234.6190915, + "desired_powered_updated_at": 1484308234.6853855, + "desired_brightness_updated_at": 1484308234.6853855, + "desired_color_model_updated_at": 1484308358.5987234, + "desired_hue_updated_at": 1484308358.5987234, + "desired_saturation_updated_at": 1484308358.5987234, + "desired_color_temperature_updated_at": 1484308358.5987234, + "desired_powered_changed_at": 1484308234.6853855, + "desired_brightness_changed_at": 1484308234.6853855, + "powered_changed_at": 1484308234.6190915, + "brightness_changed_at": 1484308234.6190915, + "desired_color_model_changed_at": 1484289242.4464757, + "desired_hue_changed_at": 1484289242.4464757, + "desired_saturation_changed_at": 1484289242.4464757, + "desired_color_temperature_changed_at": 1484101394.7322612, + "color_temperature_changed_at": 1484169623.6852453, + "color_model_changed_at": 1484105301.2582603, + "saturation_changed_at": 1483529729.3135121, + "hue_changed_at": 1484105315.2207379 + }, + "subscription": { + "pubnub": { + "subscribe_key": "sub-c-f7bf7fXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "channel": "17067b8d5d901196e5b1a4c623b26a057a6abc97|light_bulb-1972803|user-123456" + } + }, + "light_bulb_id": "1972803", + "name": "night stand", + "locale": "en_us", + "units": {}, + "created_at": 1472923653, + "hidden_at": null, + "capabilities": { + "fields": [ + { + "type": "boolean", + "field": "connection", + "mutability": "read-only" + }, + { + "type": "boolean", + "field": "powered", + "mutability": "read-write" + }, + { + "type": "percentage", + "field": "brightness", + "mutability": "read-write" + }, + { + "type": "string", + "field": "color_model", + "choices": [ + "rgb", + "hsb", + "color_temperature" + ] + }, + { + "type": "percentage", + "field": "hue", + "mutability": "read-write" + }, + { + "type": "percentage", + "field": "saturation", + "mutability": "read-write" + }, + { + "type": "integer", + "field": "color_temperature", + "range": [ + 2000, + 6500 + ], + "mutability": "read-write" + } + ], + "color_changeable": true + }, + "triggers": [], + "manufacturer_device_model": "sylvania_sylvania_rgbw", + "manufacturer_device_id": null, + "device_manufacturer": "sylvania", + "model_name": "Lightify RGBW Bulb", + "upc_id": "593", + "upc_code": "046135737039", + "gang_id": null, + "hub_id": "302528", + "local_id": "26", + "radio_type": "zigbee", + "linked_service_id": null, + "lat_lng": [ + null, + null + ], + "location": "", + "order": 0 +} diff --git a/src/pywink/test/devices/api_responses/lightify_temperature_bulb.json b/src/pywink/test/devices/api_responses/lightify_temperature_bulb.json new file mode 100644 index 0000000..a4f0b7b --- /dev/null +++ b/src/pywink/test/devices/api_responses/lightify_temperature_bulb.json @@ -0,0 +1,111 @@ +{ + "object_type": "light_bulb", + "object_id": "2366307", + "uuid": "146be9cf-4dfb-XXXXXXXXXXXXXXXXXXXXX", + "icon_id": "76", + "icon_code": "light_bulb-sylvania_light", + "desired_state": { + "powered": false, + "brightness": 1.0, + "color_model": "color_temperature", + "color_temperature": 2703 + }, + "last_reading": { + "connection": true, + "connection_updated_at": 1484289305.3739712, + "powered": false, + "powered_updated_at": 1484289305.3739712, + "brightness": 1.0, + "brightness_updated_at": 1484289305.3739712, + "color_model": "color_temperature", + "color_model_updated_at": 1484289305.3739712, + "color_temperature": 2703, + "color_temperature_updated_at": 1484289305.3739712, + "firmware_version": "0.1b02 / 0.4b92", + "firmware_version_updated_at": 1484289305.3739712, + "firmware_date_code": "20140331CNLS****", + "firmware_date_code_updated_at": 1484289305.3739712, + "desired_powered_updated_at": 1484277426.949424, + "desired_brightness_updated_at": 1484277426.949424, + "desired_color_model_updated_at": 1484277545.55024, + "desired_color_temperature_updated_at": 1484277545.55024, + "powered_changed_at": 1484277426.7937551, + "brightness_changed_at": 1484156321.553216, + "connection_changed_at": 1483842259.8092032, + "firmware_date_code_changed_at": 1483842262.803009, + "firmware_version_changed_at": 1484014789.5495606, + "color_model_changed_at": 1483842262.803009, + "color_temperature_changed_at": 1483842818.9068251, + "desired_powered_changed_at": 1484277426.949424, + "desired_color_model_changed_at": 1483842819.1898041, + "desired_color_temperature_changed_at": 1483842945.1647821, + "desired_brightness_changed_at": 1484277426.949424 + }, + "subscription": { + "pubnub": { + "subscribe_key": "sub-c-f7bf7f7e-0XXXXXXXXXXXXXXXXXXXXXXXXXX", + "channel": "57b33edf767d8ddbc970246b29f07530f4082857|light_bulb-2366307|user-123456" + } + }, + "light_bulb_id": "2366307", + "name": "Office lamp", + "locale": "en_us", + "units": {}, + "created_at": 1483842259, + "hidden_at": null, + "capabilities": { + "fields": [ + { + "type": "boolean", + "field": "connection", + "mutability": "read-only" + }, + { + "type": "boolean", + "field": "powered", + "mutability": "read-write" + }, + { + "type": "percentage", + "field": "brightness", + "mutability": "read-write" + }, + { + "type": "string", + "field": "color_model", + "choices": [ + "color_temperature" + ], + "mutability": "read-write", + "legacy_type": "colorModel" + }, + { + "type": "integer", + "field": "color_temperature", + "range": [ + 2700, + 6500 + ], + "mutability": "read-write" + } + ] + }, + "triggers": [], + "manufacturer_device_model": "sylvania_sylvania_ct", + "manufacturer_device_id": null, + "device_manufacturer": "sylvania", + "model_name": "Lightify Tunable White", + "upc_id": "279", + "upc_code": "sylvania2", + "gang_id": null, + "hub_id": "302528", + "local_id": "33", + "radio_type": "zigbee", + "linked_service_id": null, + "lat_lng": [ + 12.34567, + -89.123456 + ], + "location": "", + "order": 0 +} diff --git a/src/pywink/test/devices/api_responses/lock_key.json b/src/pywink/test/devices/api_responses/lock_key.json new file mode 100644 index 0000000..2d5d6ed --- /dev/null +++ b/src/pywink/test/devices/api_responses/lock_key.json @@ -0,0 +1,28 @@ +{ + "name": "w1ll1am", + "last_reading": { + "slot_id": 2, + "slot_id_updated_at": 1478399523.2248833, + "activity_detected": false, + "activity_detected_updated_at": 1484259955.17631, + "activity_detected_changed_at": 1484259955.17631 + }, + "key_id": "792563", + "icon_id": null, + "verified_at": null, + "object_type": "key", + "object_id": "792563", + "uuid": "3b408d13-47c4-41b3-XXXXXXXXXXXXXXXXXXX", + "parent_object_type": "lock", + "parent_object_id": "109898", + "icon_code": null, + "desired_state": { + "code": null + }, + "subscription": { + "pubnub": { + "subscribe_key": "sub-c-f7bf7f7XXXXXXXXXXXXXXXXXXXXXX", + "channel": "700cedef71549c8d3XXXXXXXXXXXXXXXXXXXX" + } + } +} diff --git a/src/pywink/test/devices/api_responses/lutron_connected_bulb_remote.json b/src/pywink/test/devices/api_responses/lutron_connected_bulb_remote.json new file mode 100644 index 0000000..ad407c9 --- /dev/null +++ b/src/pywink/test/devices/api_responses/lutron_connected_bulb_remote.json @@ -0,0 +1,62 @@ +{ + "remote_id": "123012", + "name": "Remote", + "members": [], + "object_type": "remote", + "object_id": "123012", + "uuid": "340956b9-c530-4XXXXXXXXXXXXXXXXX", + "icon_id": null, + "icon_code": null, + "desired_state": { + "assignment_mode": "local_group" + }, + "last_reading": { + "connection": true, + "connection_updated_at": 1484289280.0345233, + "firmware_version": null, + "firmware_version_updated_at": null, + "firmware_date_code": " 20150708", + "firmware_date_code_updated_at": 1484289280.0345233, + "group_id": "6866747", + "group_id_updated_at": 1484178963.2148762, + "button_on_pressed": false, + "button_on_pressed_updated_at": 1484187923.6756396, + "button_off_pressed": null, + "button_off_pressed_updated_at": null, + "button_up_pressed": null, + "button_up_pressed_updated_at": null, + "button_down_pressed": null, + "button_down_pressed_updated_at": null, + "ready": true, + "ready_updated_at": 1484289258.3489428, + "assignment_mode": "local_group", + "assignment_mode_updated_at": 1484178964.2056146, + "desired_assignment_mode_updated_at": 1484178964.2578514 + }, + "subscription": { + "pubnub": { + "subscribe_key": "sub-c-f7bf7f7eXXXXXXXXXXXXXX", + "channel": "851a02ca652218b604f6XXXXXXXXXXXXX9d|remote-123012|user-123456" + } + }, + "locale": "en_us", + "units": {}, + "created_at": 1484178962, + "hidden_at": null, + "capabilities": { + "is_sleepy": true, + "polling_interval": 0 + }, + "device_manufacturer": "lutron", + "model_name": "Connected Bulb Remote", + "upc_id": "312", + "upc_code": "lutron_zigbee_remote", + "hub_id": "302528", + "local_id": "34", + "radio_type": "zigbee", + "lat_lng": [ + 12.345678, + -98.765432 + ], + "location": "" +} diff --git a/src/pywink/test/devices/api_responses/lutron_pico_remote.json b/src/pywink/test/devices/api_responses/lutron_pico_remote.json new file mode 100644 index 0000000..ab49ef3 --- /dev/null +++ b/src/pywink/test/devices/api_responses/lutron_pico_remote.json @@ -0,0 +1,62 @@ +{ + "remote_id":"102782", + "name":"Garage Entry Remote", + "members":[ + { + "object_type":"light_bulb", + "object_id":"2042090", + "desired_state":{ + "powered_updated_at":1483472072.9911556, + "brightness_updated_at":1483472072.9911556, + "actor_id_updated_at":1483472072.9911556, + "actor_type_updated_at":1483472072.9911556, + "triggering_object_id_updated_at":1482936120.2109358, + "triggering_object_type_updated_at":1482936120.2109358, + "oauth_client_id_updated_at":1483472072.9911556 + } + } + ], + "object_type":"remote", + "object_id":"102782", + "uuid":"93b1fa4f-21f7-404XXXXXXXXXXXXXXXXX", + "icon_id":null, + "icon_code":null, + "desired_state":{ + + }, + "last_reading":{ + "connection":true, + "connection_updated_at":1483471807.7003415, + "needs_repair":false, + "needs_repair_updated_at":1483471807.7003415, + "remote_pairable":true, + "remote_pairable_updated_at":1483474080.9908342 + }, + "subscription":{ + "pubnub":{ + "subscribe_key":"sub-c-f7bf7f7eXXXXXXXXXXXXXXXXXXX", + "channel":"8470447c6898a4b39XXXXXXXXXXXXXXXXXX|remote-102782|user-123456" + } + }, + "locale":"en_ca", + "units":{ + + }, + "created_at":1475450121, + "hidden_at":null, + "capabilities":{ + + }, + "device_manufacturer":"lutron", + "model_name":"Pico", + "upc_id":"545", + "upc_code":"lutron_pico", + "hub_id":"89736", + "local_id":"89", + "radio_type":"lutron", + "lat_lng":[ + 98.76543, + 12.34567 + ], + "location":"Home" +} diff --git a/src/pywink/test/devices/api_responses/nest.json b/src/pywink/test/devices/api_responses/nest.json new file mode 100644 index 0000000..bdd2831 --- /dev/null +++ b/src/pywink/test/devices/api_responses/nest.json @@ -0,0 +1,247 @@ +{ + "object_type": "thermostat", + "object_id": "151921", + "uuid": "6eab26b5-035b-4451-XXXXXXXXXXXXXXXXX", + "icon_id": null, + "icon_code": null, + "desired_state": { + "max_set_point": 23.888888888888889, + "min_set_point": 17.222222222222221, + "eco_max_set_point": null, + "eco_min_set_point": null, + "powered": true, + "users_away": false, + "fan_timer_active": false, + "mode": "heat_only", + "short_name": "Hallway" + }, + "last_reading": { + "max_set_point": 23.888888888888889, + "max_set_point_updated_at": 1484439443.99, + "min_set_point": 17.222222222222221, + "min_set_point_updated_at": 1484439443.99, + "eco_max_set_point": null, + "eco_max_set_point_updated_at": 1484439443.99, + "eco_min_set_point": null, + "eco_min_set_point_updated_at": 1484439443.99, + "powered": true, + "powered_updated_at": 1484439443.99, + "users_away": true, + "users_away_updated_at": 1484439443.99, + "fan_timer_active": false, + "fan_timer_active_updated_at": 1484439443.99, + "units": { + "temperature": "f" + }, + "units_updated_at": 1484439443.99, + "temperature": 17.5, + "temperature_updated_at": 1484439443.99, + "external_temperature": null, + "external_temperature_updated_at": null, + "min_min_set_point": null, + "min_min_set_point_updated_at": null, + "max_min_set_point": null, + "max_min_set_point_updated_at": null, + "min_max_set_point": null, + "min_max_set_point_updated_at": null, + "max_max_set_point": null, + "max_max_set_point_updated_at": null, + "deadband": 1.5, + "deadband_updated_at": 1484439443.99, + "eco_target": false, + "eco_target_updated_at": 1484439443.99, + "manufacturer_structure_id": "fxHU4KLXorgmVXd3QX16rQ92FlxovuUp4rCK_6QAsCeo_4sj47v6fA", + "manufacturer_structure_id_updated_at": 1484439443.99, + "has_fan": false, + "has_fan_updated_at": 1484439443.99, + "fan_duration": 0, + "fan_duration_updated_at": 1484439443.99, + "last_error": null, + "last_error_updated_at": 1477416155.9555283, + "connection": true, + "connection_updated_at": 1484439443.99, + "mode": "heat_only", + "mode_updated_at": 1484439443.99, + "short_name": "Hallway", + "short_name_updated_at": 1484439443.99, + "modes_allowed": [ + "auto", + "heat_only", + "cool_only" + ], + "modes_allowed_updated_at": 1484439443.99, + "desired_max_set_point_updated_at": 1477416261.4186657, + "desired_min_set_point_updated_at": 1477416261.4186657, + "desired_eco_max_set_point": null, + "desired_eco_max_set_point_updated_at": null, + "desired_eco_min_set_point": null, + "desired_eco_min_set_point_updated_at": null, + "desired_powered_updated_at": 1477416156.6140163, + "desired_users_away_updated_at": 1477416261.4186657, + "desired_fan_timer_active_updated_at": 1477416261.4186657, + "desired_mode_updated_at": 1477416145.5000494, + "desired_short_name_updated_at": 1477416261.4186657, + "temperature_changed_at": 1484439443.99, + "mode_changed_at": 1484435206.343, + "fan_timer_active_changed_at": 1484439297.216, + "min_set_point_changed_at": 1484435206.343, + "users_away_changed_at": 1484435241.2193711, + "connection_changed_at": 1483645994.085, + "eco_target_changed_at": 1484382288.005 + }, + "subscription": { + "pubnub": { + "subscribe_key": "sub-c-f7bf7f7e-0542xxxxxxxxxxxxxxxxxxxxxx", + "channel": "706e52e15aa16dbc914273786be95f321e47774c|thermostat-151921|user-123456" + } + }, + "thermostat_id": "151921", + "name": "Home Hallway Thermostat", + "locale": "en_us", + "units": {}, + "created_at": 1470946957, + "hidden_at": null, + "capabilities": { + "fields": [ + { + "type": "float", + "field": "max_set_point", + "mutability": "read-write", + "clear_desired_state_tolerance": 0.5 + }, + { + "type": "float", + "field": "min_set_point", + "mutability": "read-write", + "clear_desired_state_tolerance": 0.5 + }, + { + "type": "float", + "field": "eco_max_set_point", + "mutability": "read-write", + "clear_desired_state_tolerance": 0.5 + }, + { + "type": "float", + "field": "eco_min_set_point", + "mutability": "read-write", + "clear_desired_state_tolerance": 0.5 + }, + { + "type": "boolean", + "field": "powered", + "mutability": "read-write" + }, + { + "type": "boolean", + "field": "users_away", + "mutability": "read-write" + }, + { + "type": "boolean", + "field": "fan_timer_active", + "mutability": "read-write" + }, + { + "type": "nested_hash", + "field": "units", + "mutability": "read-only" + }, + { + "type": "float", + "field": "temperature", + "mutability": "read-only" + }, + { + "type": "float", + "field": "external_temperature", + "mutability": "read-only" + }, + { + "type": "float", + "field": "min_min_set_point", + "mutability": "read-only" + }, + { + "type": "float", + "field": "max_min_set_point", + "mutability": "read-only" + }, + { + "type": "float", + "field": "min_max_set_point", + "mutability": "read-only" + }, + { + "type": "float", + "field": "max_max_set_point", + "mutability": "read-only" + }, + { + "type": "float", + "field": "deadband", + "mutability": "read-only" + }, + { + "type": "boolean", + "field": "eco_target", + "mutability": "read-only" + }, + { + "type": "string", + "field": "manufacturer_structure_id", + "mutability": "read-only" + }, + { + "type": "boolean", + "field": "has_fan", + "mutability": "read-only" + }, + { + "type": "integer", + "field": "fan_duration", + "mutability": "read-only" + }, + { + "type": "string", + "field": "last_error", + "mutability": "read-only" + }, + { + "type": "boolean", + "field": "connection", + "mutability": "read-only" + }, + { + "field": "mode", + "type": "selection", + "mutability": "read-write", + "choices": [ + "auto", + "heat_only", + "cool_only" + ] + } + ], + "notification_robots": [ + "aux_active_notification" + ] + }, + "triggers": [], + "manufacturer_device_model": "nest", + "manufacturer_device_id": "OM4YDFT3IVcXLjmLn2pZNkxx11Qobba_", + "device_manufacturer": "nest", + "model_name": "Learning Thermostat", + "upc_id": "557", + "upc_code": "nest_thermostat", + "hub_id": null, + "local_id": null, + "radio_type": null, + "linked_service_id": "413881", + "lat_lng": [ + null, + null + ], + "location": "", + "smart_schedule_enabled": false +} diff --git a/src/pywink/test/devices/api_responses/nest_smoke_co_detector.json b/src/pywink/test/devices/api_responses/nest_smoke_co_detector.json new file mode 100644 index 0000000..c33e567 --- /dev/null +++ b/src/pywink/test/devices/api_responses/nest_smoke_co_detector.json @@ -0,0 +1,51 @@ +{ + "object_type": "smoke_detector", + "object_id": "40885", + "uuid": "936cc3c2-4fdf-4XXXXXXXXXXXXXX", + "icon_id": null, + "icon_code": null, + "last_reading": { + "connection": true, + "connection_updated_at": 1484456882.749, + "battery": 1.0, + "battery_updated_at": 1484456882.749, + "co_detected": false, + "co_detected_updated_at": 1484456882.749, + "smoke_detected": false, + "smoke_detected_updated_at": 1484456882.749, + "test_activated": null, + "test_activated_updated_at": 1484456882.749, + "smoke_severity": 0.0, + "smoke_severity_updated_at": 1484456882.749, + "co_severity": 0.0, + "co_severity_updated_at": 1484456882.749 + }, + "subscription": { + "pubnub": { + "subscribe_key": "sub-c-f7bf7f7e-0XXXXXXXXXXXXXXx", + "channel": "01d6dc8aa18c17cddb6caab2c65c8df27da935f9|smoke_detector-40885|user-123456" + } + }, + "smoke_detector_id": "40885", + "name": "Home Upstairs Nest Protect", + "locale": "en_us", + "units": {}, + "created_at": 1443882923, + "hidden_at": null, + "capabilities": {}, + "manufacturer_device_model": "nest", + "manufacturer_device_id": "VpXN4GQ7MUBZxXBXiitT9Exx11Qobba_", + "device_manufacturer": "nest", + "model_name": "Smoke + Carbon Monoxide Detector", + "upc_id": "558", + "upc_code": "nest_protect", + "hub_id": null, + "local_id": null, + "radio_type": null, + "linked_service_id": "200261", + "lat_lng": [ + 12.345678, + -98.76543 + ], + "location": "" +} \ No newline at end of file diff --git a/src/pywink/test/devices/api_responses/pivot_power_genius.json b/src/pywink/test/devices/api_responses/pivot_power_genius.json new file mode 100644 index 0000000..d0b2975 --- /dev/null +++ b/src/pywink/test/devices/api_responses/pivot_power_genius.json @@ -0,0 +1,87 @@ +{ + "object_type": "powerstrip", + "object_id": "24313", + "uuid": "d5e7ca94-117e-495XXXXXXXXXXX", + "icon_id": null, + "icon_code": null, + "desired_state": {}, + "last_reading": { + "connection": true, + "connection_updated_at": 1484187252.9797638 + }, + "subscription": { + "pubnub": { + "subscribe_key": "sub-c-f7bf7f7eXXXXXXXXXXXXX", + "channel": "79e5bf7da7521b337648b92a9dXXXXXXXXXXXXX|powerstrip-24313|user-123456" + } + }, + "powerstrip_id": "24313", + "name": "Power strip", + "locale": "en_us", + "units": {}, + "created_at": 1451578768, + "hidden_at": null, + "capabilities": { + "needs_wifi_network_list": true + }, + "triggers": [], + "device_manufacturer": "quirky_ge", + "model_name": "Pivot Power Genius", + "upc_id": "24", + "upc_code": "814434017226", + "lat_lng": [ + 89.7654, + -12.564543 + ], + "location": "", + "mac_address": "0c2a69012345", + "serial": "AAAA00012345", + "outlets": [ + { + "powered": false, + "scheduled_outlet_states": [], + "name": "office lamp #1", + "outlet_index": 0, + "outlet_id": "48628", + "icon_id": "8", + "object_type": "outlet", + "object_id": "48628", + "uuid": "d5e7ca94-117e-4954-b5d8-XXXXXXXXXXXXXX", + "parent_object_type": "powerstrip", + "parent_object_id": "24313", + "icon_code": "outlet-lamp", + "desired_state": { + "powered": false + }, + "last_reading": { + "powered": false, + "powered_updated_at": 1484278867.9380207, + "powered_changed_at": 1484270396.0431936, + "desired_powered_updated_at": 1484278999.3652444 + } + }, + { + "powered": true, + "scheduled_outlet_states": [], + "name": "office lamp #2", + "outlet_index": 1, + "outlet_id": "48629", + "icon_id": "4", + "object_type": "outlet", + "object_id": "48629", + "uuid": "d5e7ca94-117e-4954-bXXXXXXXXXXXXXXX", + "parent_object_type": "powerstrip", + "parent_object_id": "24313", + "icon_code": "outlet-default", + "desired_state": { + "powered": true + }, + "last_reading": { + "powered": true, + "powered_updated_at": 1484278867.9380207, + "powered_changed_at": 1484277422.8034022, + "desired_powered_updated_at": 1484278867.9489951 + } + } + ] +} diff --git a/src/pywink/test/devices/api_responses/porkfoilo.json b/src/pywink/test/devices/api_responses/porkfoilo.json new file mode 100644 index 0000000..7621c50 --- /dev/null +++ b/src/pywink/test/devices/api_responses/porkfoilo.json @@ -0,0 +1,87 @@ +{ + "balance": 180, + "nose_color": "5E7E00", + "last_deposit_amount": 10, + "savings_goal": 5000, + "orientation": 0, + "vibration": true, + "object_type": "piggy_bank", + "object_id": "15215", + "uuid": "09e5977d-838f-XXXXXXXXXXXXXXXX", + "icon_id": null, + "icon_code": null, + "desired_state": { + "nose_color": null + }, + "last_reading": { + "connection": false, + "connection_updated_at": 1467487336.1350329, + "amount": 10, + "amount_updated_at": 1481498183.4186645, + "battery": null, + "battery_updated_at": null, + "balance": 180, + "balance_updated_at": 1481851991.3046694, + "orientation": 0, + "orientation_updated_at": 1467553983.0383396, + "units": { + "currency": "USD" + }, + "units_updated_at": 1467473921.6321552, + "vibration": true, + "vibration_updated_at": 1467553983.8375771 + }, + "subscription": { + "pubnub": { + "subscribe_key": "sub-c-f7bf7f7eXXXXXXXXXXXXXXXX", + "channel": "0441c5de80107a19b8fa9dac6XXXXXXXXXXXX|piggy_bank-15215|user-123456" + } + }, + "piggy_bank_id": "15215", + "name": "Porkfolio", + "locale": "en_us", + "units": {}, + "created_at": 1467473921, + "hidden_at": null, + "capabilities": { + "needs_wifi_network_list": true + }, + "triggers": [ + { + "object_type": "trigger", + "object_id": "223838", + "trigger_id": "223838", + "name": "Porkfolio vibration", + "enabled": false, + "trigger_configuration": { + "reading_type": "vibration", + "edge": "rising", + "threshold": 1.0, + "object_id": "15215", + "object_type": "piggy_bank" + }, + "channel_configuration": { + "recipient_user_ids": [ + "*" + ], + "channel_id": "15", + "object_type": null, + "object_id": null + }, + "robot_id": "3389366", + "triggered_at": null, + "piggy_bank_alert_id": "223838" + } + ], + "device_manufacturer": "quirky", + "model_name": "Porkfolio", + "upc_id": "526", + "upc_code": "quirky_porkfolio", + "lat_lng": [ + 98.765432, + -12.34567 + ], + "location": "", + "mac_address": "0c2a69012345", + "serial": "ACAA00012345" +} diff --git a/src/pywink/test/devices/api_responses/relay_switch_dump.json b/src/pywink/test/devices/api_responses/relay_switch_dump.json new file mode 100644 index 0000000..6ae099f --- /dev/null +++ b/src/pywink/test/devices/api_responses/relay_switch_dump.json @@ -0,0 +1,78 @@ +{ + "object_type": "binary_switch", + "object_id": "233551", + "uuid": "3c78d83c-b23a-4839XXXXXXXXXXX", + "icon_id": "52", + "icon_code": "binary_switch-light_bulb_dumb", + "desired_state": { + "powered": false, + "powering_mode": "dumb" + }, + "last_reading": { + "connection": true, + "connection_updated_at": 1484485952.7274327, + "powered": false, + "powered_updated_at": 1484485952.7274327, + "powering_mode": "dumb", + "powering_mode_updated_at": 1467647289.9218059, + "desired_powered_updated_at": 1484457037.7071941, + "desired_powering_mode_updated_at": 1484457037.7071941, + "desired_powered_changed_at": 1484457037.7071941, + "powered_changed_at": 1484345614.0569744 + }, + "subscription": { + "pubnub": { + "subscribe_key": "sub-c-f7bf7f7e-0542-11e3-a5e8-02ee2ddab7fe", + "channel": "206005a39d13744eXXXXXXXXXXXXXXX|binary_switch-233551|user-123456" + } + }, + "binary_switch_id": "233551", + "name": "Top Light Load", + "locale": "en_us", + "units": {}, + "created_at": 1467645109, + "hidden_at": null, + "capabilities": { + "fields": [ + { + "type": "boolean", + "field": "connection", + "mutability": "read-only" + }, + { + "type": "boolean", + "field": "powered", + "mutability": "read-write" + }, + { + "type": "selection", + "field": "powering_mode", + "choices": [ + "smart", + "dumb", + "none" + ], + "mutability": "read-write" + } + ] + }, + "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": "39320", + "hub_id": "450788", + "local_id": "1", + "radio_type": "project_one", + "linked_service_id": null, + "current_budget": null, + "lat_lng": [ + null, + null + ], + "location": "", + "order": 0 +} \ No newline at end of file diff --git a/src/pywink/test/devices/api_responses/relay_switch_smart.json b/src/pywink/test/devices/api_responses/relay_switch_smart.json new file mode 100644 index 0000000..ea1a592 --- /dev/null +++ b/src/pywink/test/devices/api_responses/relay_switch_smart.json @@ -0,0 +1,76 @@ +{ + "object_type": "binary_switch", + "object_id": "233550", + "uuid": "f56ed8c5-14b7-XXXXXXXXXXXX", + "icon_id": "52", + "icon_code": "binary_switch-light_bulb_dumb", + "desired_state": { + "powered": false, + "powering_mode": "none" + }, + "last_reading": { + "connection": true, + "connection_updated_at": 1484485952.6648619, + "powered": false, + "powered_updated_at": 1484485952.6648619, + "powering_mode": "none", + "powering_mode_updated_at": 1467647295.4216631, + "desired_powered_updated_at": 1478613226.7952232, + "desired_powering_mode_updated_at": 1467647295.4648318 + }, + "subscription": { + "pubnub": { + "subscribe_key": "sub-c-f7bf7fXXXXXXXXXXXXXXXXXXXXXXX", + "channel": "binary_switch-233550|f8662cdb80a47e0f5aXXXXXXXXXXXX|user-123465" + } + }, + "binary_switch_id": "233550", + "name": "Bottom Light Load", + "locale": "en_us", + "units": {}, + "created_at": 1467645109, + "hidden_at": null, + "capabilities": { + "fields": [ + { + "type": "boolean", + "field": "connection", + "mutability": "read-only" + }, + { + "type": "boolean", + "field": "powered", + "mutability": "read-write" + }, + { + "type": "selection", + "field": "powering_mode", + "choices": [ + "smart", + "dumb", + "none" + ], + "mutability": "read-write" + } + ] + }, + "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": "39320", + "hub_id": "450788", + "local_id": "2", + "radio_type": "project_one", + "linked_service_id": null, + "current_budget": null, + "lat_lng": [ + null, + null + ], + "location": "", + "order": 0 +} \ No newline at end of file diff --git a/src/pywink/test/devices/api_responses/schlage_lock.json b/src/pywink/test/devices/api_responses/schlage_lock.json new file mode 100644 index 0000000..7a231b4 --- /dev/null +++ b/src/pywink/test/devices/api_responses/schlage_lock.json @@ -0,0 +1,163 @@ +{ + "object_type": "lock", + "object_id": "109898", + "uuid": "0aa5f3f4-200c-4a93-XXXXXXXXXXX", + "icon_id": null, + "icon_code": null, + "desired_state": { + "locked": true, + "beeper_enabled": false, + "vacation_mode_enabled": false, + "auto_lock_enabled": false, + "key_code_length": 4, + "alarm_mode": null, + "alarm_sensitivity": 0.6, + "alarm_enabled": true + }, + "last_reading": { + "connection": true, + "connection_updated_at": 1484310269.3183775, + "locked": true, + "locked_updated_at": 1484310269.3183775, + "battery": 0.7, + "battery_updated_at": 1484310269.3183775, + "alarm_activated": null, + "alarm_activated_updated_at": null, + "beeper_enabled": true, + "beeper_enabled_updated_at": 1484310269.3183775, + "vacation_mode_enabled": false, + "vacation_mode_enabled_updated_at": 1484310269.3183775, + "auto_lock_enabled": true, + "auto_lock_enabled_updated_at": 1484310269.3183775, + "key_code_length": 4, + "key_code_length_updated_at": 1484310269.3183775, + "alarm_mode": "tamper", + "alarm_mode_updated_at": 1484310269.3183775, + "alarm_sensitivity": 0.6, + "alarm_sensitivity_updated_at": 1484310269.3183775, + "alarm_enabled": true, + "alarm_enabled_updated_at": 1484310269.3183775, + "last_error": null, + "last_error_updated_at": 1483357603.582, + "desired_locked_updated_at": 1484309210.3862641, + "desired_beeper_enabled_updated_at": 1484309356.3939486, + "desired_vacation_mode_enabled_updated_at": 1484309356.3939486, + "desired_auto_lock_enabled_updated_at": 1484309356.3939486, + "desired_key_code_length_updated_at": 1484309356.3939486, + "desired_alarm_mode_updated_at": 1484309356.3939486, + "desired_alarm_sensitivity_updated_at": 1484309356.3939486, + "desired_alarm_enabled_updated_at": 1484309356.3939486, + "locked_changed_at": 1484308713.9324966, + "battery_changed_at": 1484310269.3183775, + "desired_locked_changed_at": 1484309210.3862641, + "desired_alarm_mode_changed_at": 1483990975.2186949, + "alarm_mode_changed_at": 1483990975.1604912, + "desired_alarm_enabled_changed_at": 1483991063.34096, + "alarm_enabled_changed_at": 1483992890.7074449 + }, + "subscription": { + "pubnub": { + "subscribe_key": "sub-c-f7bf7f7e-0542-XXXXXXXXXXXXX", + "channel": "2a3dcadbc3fffe07404XXXXXXXXXXXXXXXXX|lock-109898|user-123456" + } + }, + "lock_id": "109898", + "name": "Front door", + "locale": "en_us", + "units": {}, + "created_at": 1478398543, + "hidden_at": null, + "capabilities": { + "fields": [ + { + "type": "boolean", + "field": "connection", + "mutability": "read-only" + }, + { + "type": "boolean", + "field": "locked", + "mutability": "read-write" + }, + { + "type": "percentage", + "field": "battery", + "mutability": "read-only" + }, + { + "type": "boolean", + "field": "alarm_activated", + "mutability": "read-only" + }, + { + "type": "boolean", + "field": "beeper_enabled", + "mutability": "read-write" + }, + { + "type": "boolean", + "field": "vacation_mode_enabled", + "mutability": "read-write" + }, + { + "type": "boolean", + "field": "auto_lock_enabled", + "mutability": "read-write" + }, + { + "type": "integer", + "field": "key_code_length", + "range": [ + 4, + 8 + ], + "mutability": "read-write" + }, + { + "type": "string", + "field": "alarm_mode", + "choices": [ + "alert", + "tamper", + "forced_entry", + null + ], + "mutability": "read-write" + }, + { + "type": "percentage", + "field": "alarm_sensitivity", + "choices": [ + 0.2, + 0.4, + 0.6, + 0.8, + 1 + ], + "mutability": "read-write" + }, + { + "type": "boolean", + "field": "alarm_enabled", + "mutability": "read-write" + } + ], + "home_security_device": true + }, + "triggers": [], + "manufacturer_device_model": "schlage_zwave_lock", + "manufacturer_device_id": null, + "device_manufacturer": "schlage", + "model_name": "BE469", + "upc_id": "11", + "upc_code": "043156312214", + "hub_id": "302528", + "local_id": "27", + "radio_type": "zwave", + "linked_service_id": null, + "lat_lng": [ + 12.345678, + -98.675432 + ], + "location": "" +} diff --git a/src/pywink/test/devices/api_responses/sensi_thermostat.json b/src/pywink/test/devices/api_responses/sensi_thermostat.json new file mode 100644 index 0000000..e2c0281 --- /dev/null +++ b/src/pywink/test/devices/api_responses/sensi_thermostat.json @@ -0,0 +1,226 @@ +{ + "object_type":"thermostat", + "object_id":"140295", + "uuid":"4890fa42-4339-4824-a017-c874bb123456", + "desired_state":{ + "max_set_point":22.22222222222222, + "min_set_point":22.22222222222222, + "fan_mode":"auto", + "powered":false, + "mode":"cool_only" + }, + "last_reading":{ + "max_set_point":22.22222222222222, + "max_set_point_updated_at":1477511203.5183966, + "min_set_point":22.22222222222222, + "min_set_point_updated_at":1477511203.5183966, + "fan_mode":"auto", + "fan_mode_updated_at":1477511203.5183966, + "powered":true, + "has_fan":true, + "powered_updated_at":1477511203.5183966, + "smart_temperature":20.555555555555557, + "humidifier_mode":"auto", + "humidifier_set_point":0.2, + "dehumidifier_mode":"auto", + "dehumidifier_set_point":0.6, + "occupied":true, + "temperature":20.555555555555557, + "temperature_updated_at":1477511203.5183966, + "external_temperature":16.1, + "external_temperature_updated_at":null, + "min_min_set_point":7.222222222222222, + "min_min_set_point_updated_at":1477511203.5183966, + "max_min_set_point":37.22222222222222, + "max_min_set_point_updated_at":1477511203.5183966, + "min_max_set_point":7.222222222222222, + "min_max_set_point_updated_at":1477511203.5183966, + "max_max_set_point":37.22222222222222, + "max_max_set_point_updated_at":1477511203.5183966, + "deadband":1.1111111111111112, + "deadband_updated_at":1477511203.5183966, + "humidity":40, + "humidity_updated_at":1477511203.5183966, + "cool_active":false, + "cool_active_updated_at":1477511203.5183966, + "heat_active":false, + "heat_active_updated_at":1477511203.5183966, + "aux_active":false, + "aux_active_updated_at":1477511203.5183966, + "fan_active":true, + "fan_active_updated_at":1477511203.5183966, + "firmware_version":"6003980915", + "firmware_version_updated_at":1477511203.5183966, + "connection":true, + "connection_updated_at":1477511203.5183966, + "mode":"cool_only", + "mode_updated_at":1477511203.5183966, + "modes_allowed":[ + "heat_only", + "cool_only", + "auto" + ], + "modes_allowed_updated_at":1477511203.5183966, + "units":"f", + "units_updated_at":1477511203.5183966, + "desired_max_set_point_updated_at":1476998924.056839, + "desired_min_set_point_updated_at":1476998924.056839, + "desired_fan_mode_updated_at":1476998924.056839, + "desired_powered_updated_at":1476998788.717325, + "desired_mode_updated_at":1476998924.056839, + "humidity_changed_at":1477509616.356899, + "temperature_changed_at":1477510096.4845245, + "desired_powered_changed_at":1476998788.717325, + "desired_mode_changed_at":1476725230.5535583, + "powered_changed_at":1476998788.5916903, + "cool_active_changed_at":1476998788.5916903, + "fan_active_changed_at":1476998788.5916903, + "max_set_point_changed_at":1476745575.4795704, + "connection_changed_at":1477511203.5183966, + "firmware_version_changed_at":1477274231.5533497 + }, + "subscription":{ + "pubnub":{ + "subscribe_key":"sub-c-f7bf7f7e-0542-11e3-a5e8-02ee2d123456", + "channel":"4d181499722e8b9c0f823aaad005678963325e53|thermostat-140234|user-40345" + } + }, + "thermostat_id":"140295", + "name":"Sensi Thermostat", + "locale":"en_us", + "units":{ + "temperature":"f" + }, + "created_at":1465337429, + "hidden_at":null, + "capabilities":{ + "fields":[ + { + "type":"float", + "field":"max_set_point", + "mutability":"read-write" + }, + { + "type":"float", + "field":"min_set_point", + "mutability":"read-write" + }, + { + "type":"selection", + "field":"fan_mode", + "choices":[ + "auto", + "on" + ], + "mutability":"read-write" + }, + { + "type":"boolean", + "field":"powered", + "mutability":"read-write" + }, + { + "type":"float", + "field":"temperature", + "mutability":"read-only" + }, + { + "type":"float", + "field":"external_temperature", + "mutability":"read-only" + }, + { + "type":"float", + "field":"min_min_set_point", + "mutability":"read-only" + }, + { + "type":"float", + "field":"max_min_set_point", + "mutability":"read-only" + }, + { + "type":"float", + "field":"min_max_set_point", + "mutability":"read-only" + }, + { + "type":"float", + "field":"max_max_set_point", + "mutability":"read-only" + }, + { + "type":"float", + "field":"deadband", + "mutability":"read-only" + }, + { + "type":"percentage", + "field":"humidity", + "mutability":"read-only" + }, + { + "type":"boolean", + "field":"cool_active", + "mutability":"read-only" + }, + { + "type":"boolean", + "field":"heat_active", + "mutability":"read-only" + }, + { + "type":"boolean", + "field":"aux_active", + "mutability":"read-only" + }, + { + "type":"boolean", + "field":"fan_active", + "mutability":"read-only" + }, + { + "type":"string", + "field":"firmware_version", + "mutability":"read-only" + }, + { + "type":"boolean", + "field":"connection", + "mutability":"read-only" + }, + { + "field":"mode", + "type":"selection", + "mutability":"read-write", + "choices":[ + "heat_only", + "cool_only", + "auto" + ] + } + ], + "notification_robots":[ + "aux_active_notification" + ] + }, + "triggers":[ + + ], + "manufacturer_device_model":"emerson_up500_wb1", + "manufacturer_device_id":"36-6f-92-ff-fe-04-34-12", + "device_manufacturer":"emerson", + "model_name":"Sensi Wi-Fi Programmable Thermostat", + "upc_id":"652", + "upc_code":"emerson_up500wb1", + "hub_id":null, + "local_id":null, + "radio_type":null, + "linked_service_id":"365180", + "lat_lng":[ + null, + null + ], + "location":"", + "smart_schedule_enabled":false +} \ No newline at end of file diff --git a/src/pywink/test/devices/api_responses/shade.json b/src/pywink/test/devices/api_responses/shade.json new file mode 100644 index 0000000..7112cc1 --- /dev/null +++ b/src/pywink/test/devices/api_responses/shade.json @@ -0,0 +1,51 @@ +{ + "object_type":"shade", + "object_id":"5650", + "uuid":"07b44f75-c7ee-48f1-XXXXXXXX", + "desired_state":{ + "position":0.0 + }, + "last_reading":{ + "connection":true, + "connection_updated_at":1463494385.449129, + "position":1.0, + "position_updated_at":1463494385.449129, + "desired_position_updated_at":1463500774.348026, + "connection_changed_at":1457674189.5065048, + "desired_position_changed_at":1463500774.348026 + }, + "subscription":{ + "pubnub":{ + "subscribe_key":"sub-c-f7bf7f7e-0XXXXXXXXXXXXXXXXXXXX", + "channel":"31be109cff295db0c6d08d411fac0133e46a6138|shade-5650|user-410445" + } + }, + "shade_id":"5650", + "name":"Left Bed Shade", + "locale":"en_us", + "units":{ + + }, + "created_at":1457674189, + "hidden_at":null, + "capabilities":{ + + }, + "triggers":[ + + ], + "manufacturer_device_model":"somfy_bali", + "manufacturer_device_id":null, + "device_manufacturer":"somfy", + "model_name":"Shade", + "upc_id":"81", + "upc_code":"SOMFY", + "hub_id":"344883", + "local_id":"6", + "radio_type":"zwave", + "lat_lng":[ + 98.76543, + -11.654125 + ], + "location":"" +} \ No newline at end of file diff --git a/src/pywink/test/devices/api_responses/spotter_v1.json b/src/pywink/test/devices/api_responses/spotter_v1.json new file mode 100644 index 0000000..e4b24c1 --- /dev/null +++ b/src/pywink/test/devices/api_responses/spotter_v1.json @@ -0,0 +1,119 @@ +{ + "last_event": { + "brightness_occurred_at": 1484199467.7482638, + "loudness_occurred_at": 1484310427.9610171, + "vibration_occurred_at": 1484276838.5191174 + }, + "object_type": "sensor_pod", + "object_id": "156012", + "uuid": "77ee9632-d1c6-4af7-XXXXXXXXXXXXXXXXX", + "icon_id": null, + "icon_code": null, + "desired_state": {}, + "last_reading": { + "battery": 1.0, + "battery_updated_at": 1484310490.9588282, + "brightness": 0.0, + "brightness_updated_at": 1484310490.9588282, + "external_power": true, + "external_power_updated_at": 1484310490.9588282, + "humidity": 23, + "humidity_updated_at": 1484310490.9588282, + "loudness": false, + "loudness_updated_at": 1484310490.9588282, + "temperature": 22.0, + "temperature_updated_at": 1484310490.9588282, + "vibration": false, + "vibration_updated_at": 1484310490.9588282, + "brightness_true": "N/A", + "brightness_true_updated_at": 1484199467.7482638, + "loudness_true": "N/A", + "loudness_true_updated_at": 1484310427.9610171, + "vibration_true": "N/A", + "vibration_true_updated_at": 1484276838.5191174, + "connection": true, + "connection_updated_at": 1484310490.9588282, + "agent_session_id": null, + "agent_session_id_updated_at": null, + "humidity_changed_at": 1484305792.2123559, + "loudness_changed_at": 1484310490.9588282, + "loudness_true_changed_at": 1484310427.9610171, + "vibration_changed_at": 1484276843.7636323, + "vibration_true_changed_at": 1484276838.5191174, + "temperature_changed_at": 1484298646.9891379, + "brightness_changed_at": 1484221745.1632764, + "brightness_true_changed_at": 1484199467.7482638 + }, + "subscription": { + "pubnub": { + "subscribe_key": "sub-c-f7bf7f7e-054XXXXXXXXXXXXXXXXXXXX", + "channel": "e7c46d25d2654253567XXXXXXXXXXXXXXXXXXXXXXXXX|sensor_pod-156012|user-123456" + } + }, + "sensor_pod_id": "156012", + "name": "Spotter", + "locale": "en_us", + "units": {}, + "created_at": 1453520234, + "hidden_at": null, + "capabilities": { + "needs_wifi_network_list": true, + "sensor_types": [ + { + "type": "percentage", + "field": "battery", + "mutability": "read-only" + }, + { + "type": "percentage", + "field": "brightness", + "mutability": "read-only" + }, + { + "type": "boolean", + "field": "external_power", + "mutability": "read-only" + }, + { + "type": "percentage", + "field": "humidity", + "mutability": "read-only" + }, + { + "type": "boolean", + "field": "loudness", + "mutability": "read-only" + }, + { + "type": "float", + "field": "temperature", + "mutability": "read-only" + }, + { + "type": "boolean", + "field": "vibration", + "mutability": "read-only" + } + ], + "desired_state_fields": [] + }, + "triggers": [], + "manufacturer_device_model": "quirky_ge_spotter", + "manufacturer_device_id": null, + "device_manufacturer": "quirky_ge", + "model_name": "Spotter", + "upc_id": "531", + "upc_code": "quirky_ge_spotter", + "gang_id": null, + "hub_id": null, + "local_id": null, + "radio_type": null, + "linked_service_id": null, + "lat_lng": [ + 98.76543, + -12.34567 + ], + "location": "", + "mac_address": "0c2a69012345", + "serial": "ABAB00012345" +} diff --git a/src/pywink/test/devices/api_responses/sprinkler.json b/src/pywink/test/devices/api_responses/sprinkler.json new file mode 100644 index 0000000..2e33827 --- /dev/null +++ b/src/pywink/test/devices/api_responses/sprinkler.json @@ -0,0 +1,61 @@ +{ + "object_type":"sprinkler", + "object_id":"58796", + "uuid":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "icon_id":null, + "icon_code":null, + "desired_state":{ + "powered":true + }, + "last_reading":{ + "connection":true, + "connection_updated_at":1477933016.6407745, + "powered":true, + "powered_updated_at":1466562190.6761053, + "desired_powered":null, + "desired_powered_updated_at":null + }, + "subscription":{ + "pubnub":{ + "subscribe_key":"sub-x-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "channel":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx|sprinkler-xxxx|user-xxxxxx" + } + }, + "sprinkler_id":"58796", + "name":"Sprinkler", + "locale":"en_us", + "units":{ + + }, + "created_at":1463026663, + "hidden_at":null, + "capabilities":{ + "fields":[ + { + "type":"boolean", + "field":"connection", + "mutability":"read-only" + }, + { + "type":"boolean", + "field":"powered", + "mutability":"read-write" + } + ] + }, + "manufacturer_device_model":"rachio_iro_v2", + "manufacturer_device_id":"123456", + "device_manufacturer":"rachio", + "model_name":"Iro", + "upc_id":"123", + "upc_code":"wifi_rachio_v2", + "linked_service_id":"12345", + "lat_lng":[ + 98.76543, + 12.34567 + ], + "location":"", + "zones":[ + + ] +} diff --git a/src/pywink/test/devices/api_responses/v1_wink_hub.json b/src/pywink/test/devices/api_responses/v1_wink_hub.json new file mode 100644 index 0000000..f60078c --- /dev/null +++ b/src/pywink/test/devices/api_responses/v1_wink_hub.json @@ -0,0 +1,106 @@ +{ + "object_type": "hub", + "object_id": "302528", + "uuid": "d16117dd-6565-4XXXXXXXXXXXXXXXXXX", + "icon_id": null, + "icon_code": null, + "desired_state": { + "pairing_mode": null, + "pairing_device_type_selector": null, + "pairing_mode_duration": 0 + }, + "last_reading": { + "connection": true, + "connection_updated_at": 1484289277.6576238, + "agent_session_id": "729659cf991b8f29fe50b9b7eef8e50e", + "agent_session_id_updated_at": 1484289237.9619641, + "pairing_mode": null, + "pairing_mode_updated_at": 1484289277.6576238, + "pairing_device_type_selector": null, + "pairing_device_type_selector_updated_at": null, + "kidde_radio_code_updated_at": 1449442170.975126, + "pairing_mode_duration": 0, + "pairing_mode_duration_updated_at": 1484289277.6576238, + "updating_firmware": false, + "updating_firmware_updated_at": 1484289237.2336402, + "firmware_version": "3.4.4-0-g239bc70fc1-hub-app", + "firmware_version_updated_at": 1484289240.7560096, + "update_needed": false, + "update_needed_updated_at": 1484289240.7560096, + "mac_address": "B4:79:A7:1C:40:A6", + "mac_address_updated_at": 1484289240.7560096, + "zigbee_mac_address": "000D6F000538CAF5", + "zigbee_mac_address_updated_at": 1484289277.6576238, + "ip_address": "192.168.1.2", + "ip_address_updated_at": 1484289240.7560096, + "hub_version": "00.01", + "hub_version_updated_at": 1484289240.7560096, + "app_version": "0.1.0", + "app_version_updated_at": 1484289240.7560096, + "transfer_mode": null, + "transfer_mode_updated_at": 1484289237.9619641, + "connection_type": null, + "connection_type_updated_at": null, + "wifi_credentials_present": null, + "wifi_credentials_present_updated_at": null, + "remote_pairable": null, + "remote_pairable_updated_at": null, + "local_control_public_key_hash": "E3:03:48:35:D4:92:46:EE:9E:DD:AC:3B:59:72:78:C7:22:8C:4E:C5:52:85:A3:D2:19:49:40:90:79:70:D5:0A", + "local_control_public_key_hash_updated_at": 1484289276.8685172, + "local_control_id": "c2d9c107-55cb-41b2XXXXXXXXXXXXXX", + "local_control_id_updated_at": 1484289276.8685172, + "desired_pairing_mode_updated_at": 1484178952.08271, + "desired_pairing_device_type_selector_updated_at": 1484179084.9871554, + "desired_kidde_radio_code_updated_at": 1484179084.9871554, + "desired_pairing_mode_duration_updated_at": 1484178952.08271, + "connection_changed_at": 1484199457.326752, + "agent_session_id_changed_at": 1484289237.9619641, + "mac_address_changed_at": 1481728171.1006608, + "ip_address_changed_at": 1481728171.1006608, + "desired_pairing_mode_changed_at": 1484178952.08271, + "desired_pairing_mode_duration_changed_at": 1484178952.08271, + "pairing_mode_changed_at": 1484178961.2074077, + "pairing_mode_duration_changed_at": 1484178961.2074077, + "update_needed_changed_at": 1481760653.5433812, + "updating_firmware_changed_at": 1481760649.8420711, + "firmware_version_changed_at": 1481760653.5433812, + "desired_pairing_device_type_selector_changed_at": 1483490750.6303914 + }, + "subscription": { + "pubnub": { + "subscribe_key": "sub-c-f7bf7f7e-05XXXXXXXXXXXXX", + "channel": "79b3abc01f3a3XXXXXXXXXXXXXXXXXXXXXXX|hub-302528|user-123456" + } + }, + "hub_id": "302528", + "name": "Hub", + "locale": "en_us", + "units": {}, + "created_at": 1449442170, + "hidden_at": null, + "capabilities": { + "oauth2_clients": [ + "wink_hub" + ], + "home_security_device": true, + "provisioning_version": "8a", + "needs_wifi_network_list": true + }, + "triggers": [], + "manufacturer_device_model": "wink_hub", + "manufacturer_device_id": null, + "device_manufacturer": "wink", + "model_name": "Hub", + "upc_id": "15", + "upc_code": "840410102358", + "linked_service_id": null, + "lat_lng": [ + 12.345678, + -98.765432 + ], + "location": null, + "update_needed": false, + "configuration": { + "kidde_radio_code": 0 + } +} diff --git a/src/pywink/test/devices/api_responses/v2_wink_hub.json b/src/pywink/test/devices/api_responses/v2_wink_hub.json new file mode 100644 index 0000000..279d4ff --- /dev/null +++ b/src/pywink/test/devices/api_responses/v2_wink_hub.json @@ -0,0 +1,96 @@ +{ + "object_type": "hub", + "object_id": "511438", + "uuid": "bd9c6bbe-8acb-425XXXXXXXXXXXXXX", + "icon_id": null, + "icon_code": null, + "desired_state": { + "pairing_mode": null, + "pairing_device_type_selector": null, + "pairing_mode_duration": 0 + }, + "last_reading": { + "connection": true, + "connection_updated_at": 1484328797.3590808, + "agent_session_id": "10744f004cca1d3d8d1b52c6f09f950b", + "agent_session_id_updated_at": 1484328796.1098533, + "pairing_mode": null, + "pairing_mode_updated_at": 1484328797.3293278, + "pairing_device_type_selector": null, + "pairing_device_type_selector_updated_at": null, + "kidde_radio_code_updated_at": 1477618454.0359025, + "pairing_mode_duration": 0, + "pairing_mode_duration_updated_at": 1484328797.3293278, + "updating_firmware": false, + "updating_firmware_updated_at": 1484328795.718946, + "firmware_version": "3.4.4-0-g239bc70fc1-hub2-app", + "firmware_version_updated_at": 1484328797.3590808, + "update_needed": false, + "update_needed_updated_at": 1484328797.3590808, + "mac_address": "D8:C4:6A:5E:50:27", + "mac_address_updated_at": 1484328797.3590808, + "zigbee_mac_address": "000D6F000CD28630", + "zigbee_mac_address_updated_at": 1484328797.3293278, + "ip_address": "192.168.1.2", + "ip_address_updated_at": 1484328797.3590808, + "hub_version": "00.01", + "hub_version_updated_at": 1484328797.3590808, + "app_version": "3.4.4-0-g239bc70fc1-hub2-app", + "app_version_updated_at": 1484328797.3590808, + "transfer_mode": null, + "transfer_mode_updated_at": 1484328796.1098533, + "connection_type": null, + "connection_type_updated_at": null, + "wifi_credentials_present": null, + "wifi_credentials_present_updated_at": null, + "remote_pairable": null, + "remote_pairable_updated_at": null, + "local_control_public_key_hash": "47:9E:59:47:B4:B2:8E:00:7A:CC:37:B2:8B:02:20:EF:2D:87:10:07:E6:4E:64:20:7D:3E:3D:91:C8:BA:2F:02", + "local_control_public_key_hash_updated_at": 1484328797.249866, + "local_control_id": "3c30a131-689XXXXXXXXXXXXXXXXX", + "local_control_id_updated_at": 1484328797.249866, + "desired_pairing_mode_updated_at": 1482191959.3433225, + "desired_pairing_device_type_selector_updated_at": 1482192145.5871685, + "desired_kidde_radio_code_updated_at": 1477618454.0804579, + "desired_pairing_mode_duration_updated_at": 1482191959.3433225, + "connection_changed_at": 1484328795.718946, + "agent_session_id_changed_at": 1484328796.1098533 + }, + "subscription": { + "pubnub": { + "subscribe_key": "sub-c-f7bf7f7XXXXXXXXXXXXXXXXXXXXXXXXXX", + "channel": "d19f9034b6cc8beac59b670e73fbe6a0c1eae07a|hub-511438|user-123465" + } + }, + "hub_id": "511438", + "name": "Hub V2", + "locale": "en_us", + "units": {}, + "created_at": 1477618453, + "hidden_at": null, + "capabilities": { + "oauth2_clients": [ + "wink_hub_2" + ], + "home_security_device": true, + "provisioning_version": "1", + "needs_wifi_network_list": true + }, + "triggers": [], + "manufacturer_device_model": "wink_hub2", + "manufacturer_device_id": null, + "device_manufacturer": "wink", + "model_name": "Hub2", + "upc_id": "821", + "upc_code": "wink_hub_2", + "linked_service_id": null, + "lat_lng": [ + 12.345678, + -89.765432 + ], + "location": "", + "update_needed": false, + "configuration": { + "kidde_radio_code": 0 + } +} \ No newline at end of file diff --git a/src/pywink/test/devices/api_responses/wink_relay_button.json b/src/pywink/test/devices/api_responses/wink_relay_button.json new file mode 100644 index 0000000..2eaeeed --- /dev/null +++ b/src/pywink/test/devices/api_responses/wink_relay_button.json @@ -0,0 +1,48 @@ +{ + "object_type": "button", + "object_id": "78535", + "uuid": "65eb2cc6-72e6-4ed2-XXXXXXXXXXXXXXXX", + "icon_id": null, + "icon_code": null, + "last_reading": { + "connection": true, + "connection_updated_at": 1484059107.4232388, + "pressed": false, + "pressed_updated_at": 1484059107.4232388, + "long_pressed": null, + "long_pressed_updated_at": null, + "ifttt_connected": null, + "ifttt_connected_updated_at": null, + "ifttt_identifier": null, + "ifttt_identifier_updated_at": null, + "pressed_changed_at": 1483744049.8844638 + }, + "subscription": { + "pubnub": { + "subscribe_key": "sub-c-f7bf7f7e-0542XXXXXXXXXXXXXXXXX", + "channel": "bc3a85beec5712f25dce32b5dbdfaf1a43e0c2af|button-78535|user-123465" + } + }, + "button_id": "78535", + "name": "Wink Relay's Top Button", + "locale": "en_us", + "units": {}, + "created_at": 1467645108, + "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": "39320", + "hub_id": "450788", + "local_id": "4", + "radio_type": "project_one", + "lat_lng": [ + null, + null + ], + "location": "" +} \ No newline at end of file diff --git a/src/pywink/test/devices/api_responses/wink_relay_gang.json b/src/pywink/test/devices/api_responses/wink_relay_gang.json new file mode 100644 index 0000000..3b5499e --- /dev/null +++ b/src/pywink/test/devices/api_responses/wink_relay_gang.json @@ -0,0 +1,40 @@ +{ + "object_type": "gang", + "object_id": "39320", + "uuid": "cc6fe63d-3de8-4d1XXXXXXXXX", + "icon_id": null, + "icon_code": null, + "desired_state": {}, + "last_reading": { + "connection": true, + "connection_updated_at": 1467645106.3497777 + }, + "subscription": { + "pubnub": { + "subscribe_key": "sub-c-f7bf7f7e-0542-11e3-a5e8-02ee2ddab7fe", + "channel": "aa22033a2154d82847f7d2fd2f21165b10b13e12|gang-39320|user-123456" + } + }, + "gang_id": "39320", + "name": "Gang", + "locale": "en_us", + "units": {}, + "created_at": 1467645106, + "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": "450788", + "local_id": null, + "radio_type": null, + "linked_service_id": null, + "lat_lng": [ + null, + null + ], + "location": "" +} \ No newline at end of file diff --git a/src/pywink/test/devices/api_responses/wink_relay_hub.json b/src/pywink/test/devices/api_responses/wink_relay_hub.json new file mode 100644 index 0000000..7a8515f --- /dev/null +++ b/src/pywink/test/devices/api_responses/wink_relay_hub.json @@ -0,0 +1,93 @@ +{ + "object_type": "hub", + "object_id": "456369", + "uuid": "7f78bb1d-78c0-4XXXXXXXXXXXXXXXXx", + "icon_id": null, + "icon_code": null, + "desired_state": { + "pairing_mode": null, + "pairing_device_type_selector": null, + "pairing_mode_duration": null + }, + "last_reading": { + "connection": true, + "connection_updated_at": 1484423809.3030915, + "agent_session_id": "8fb2016077cd296b7620d6b1abcfe991", + "agent_session_id_updated_at": 1484423809.0880563, + "pairing_mode": null, + "pairing_mode_updated_at": null, + "pairing_device_type_selector": null, + "pairing_device_type_selector_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": 1484423807.2065063, + "firmware_version": "1.0.585", + "firmware_version_updated_at": 1484423809.3030915, + "update_needed": false, + "update_needed_updated_at": 1484423809.3030915, + "mac_address": "B4:79:A7:04:9C:84", + "mac_address_updated_at": 1484423809.3030915, + "zigbee_mac_address": null, + "zigbee_mac_address_updated_at": null, + "ip_address": "192.168.1.66", + "ip_address_updated_at": 1484423809.3030915, + "hub_version": "user", + "hub_version_updated_at": 1484423809.3030915, + "app_version": "3.4.2", + "app_version_updated_at": 1484423809.3030915, + "transfer_mode": null, + "transfer_mode_updated_at": 1484423809.0880563, + "connection_type": null, + "connection_type_updated_at": null, + "wifi_credentials_present": null, + "wifi_credentials_present_updated_at": null, + "local_control_public_key_hash": "0B:BF:8D:C6:03:7E:56:CD:B6:9D:BE:DE:F2:56:0C:E2:F2:9A:38:46:7D:CB:9C:69:1E:E0:AA:88:D1:3B:68:FF", + "local_control_public_key_hash_updated_at": 1468461288.1613514, + "local_control_id": "a80f56b5-eb7XXXXXXXXXXXXXXXXX", + "local_control_id_updated_at": 1468461288.1613514, + "desired_pairing_mode_updated_at": 1468461448.067497, + "desired_pairing_device_type_selector": null, + "desired_pairing_device_type_selector_updated_at": null, + "desired_kidde_radio_code_updated_at": 1468461437.938937, + "desired_pairing_mode_duration_updated_at": 1468461448.067497, + "connection_changed_at": 1484423807.2065063, + "agent_session_id_changed_at": 1484423809.0880563 + }, + "subscription": { + "pubnub": { + "subscribe_key": "sub-c-f7XXXXXXXXXXXXXXXXXXXXXXXXXX", + "channel": "5adbae35747124ea1d59399f2e27401b71b6decf|hub-456369|user-132456" + } + }, + "hub_id": "456369", + "name": "Great Room Relay", + "locale": "en_us", + "units": {}, + "created_at": 1468460685, + "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", + "linked_service_id": null, + "lat_lng": [ + 12.123465, + -89.54654 + ], + "location": "", + "update_needed": false, + "configuration": { + "kidde_radio_code": null + } +} \ No newline at end of file diff --git a/src/pywink/test/devices/api_responses/wink_relay_sensor.json b/src/pywink/test/devices/api_responses/wink_relay_sensor.json new file mode 100644 index 0000000..65f7dd0 --- /dev/null +++ b/src/pywink/test/devices/api_responses/wink_relay_sensor.json @@ -0,0 +1,89 @@ +{ + "last_event": { + "brightness_occurred_at": null, + "loudness_occurred_at": null, + "vibration_occurred_at": null + }, + "object_type": "sensor_pod", + "object_id": "239787", + "uuid": "9bb49bfe-0d6a-4e9bXXXXXXXXXXXXXXX", + "icon_id": null, + "icon_code": null, + "desired_state": {}, + "last_reading": { + "temperature": 16.556, + "temperature_updated_at": 1484445616.4226761, + "humidity": 71, + "humidity_updated_at": 1484445616.4226761, + "presence": false, + "presence_updated_at": 1484445616.4226761, + "proximity": 2578.0, + "proximity_updated_at": 1484445616.4226761, + "connection": true, + "connection_updated_at": 1484445616.4226761, + "agent_session_id": null, + "agent_session_id_updated_at": null, + "temperature_changed_at": 1484445616.4226761, + "proximity_changed_at": 1484445616.4226761, + "humidity_changed_at": 1484445616.4226761, + "presence_changed_at": 1483227553.6322503 + }, + "subscription": { + "pubnub": { + "subscribe_key": "sub-c-f7bf7f7e-0542-11e3-xxxxxxxxxxxxxxx", + "channel": "a82816f0ddbxxxxxxxxxxxxxxxxebcbd264|sensor_pod-239787|user-123456" + } + }, + "sensor_pod_id": "239787", + "name": "Great Room Relay", + "locale": "en_us", + "units": {}, + "created_at": 1468460695, + "hidden_at": null, + "capabilities": { + "sensor_types": [ + { + "type": "float", + "field": "temperature", + "mutability": "read-only", + "attribute_id": 1 + }, + { + "type": "percentage", + "field": "humidity", + "mutability": "read-only", + "attribute_id": 2 + }, + { + "type": "boolean", + "field": "presence", + "mutability": "read-only", + "attribute_id": 3 + }, + { + "type": "float", + "field": "proximity", + "mutability": "read-only", + "attribute_id": 4 + } + ], + "desired_state_fields": [] + }, + "triggers": [], + "manufacturer_device_model": "wink_relay_sensor", + "manufacturer_device_id": null, + "device_manufacturer": "wink", + "model_name": "Wink Relay Sensor", + "upc_id": "188", + "upc_code": "wink_p1_sensor", + "gang_id": "40406", + "hub_id": "456369", + "local_id": "3", + "radio_type": "project_one", + "linked_service_id": null, + "lat_lng": [ + null, + null + ], + "location": "" +} \ No newline at end of file diff --git a/src/pywink/test/devices/base_test.py b/src/pywink/test/devices/base_test.py new file mode 100644 index 0000000..26f89fb --- /dev/null +++ b/src/pywink/test/devices/base_test.py @@ -0,0 +1,145 @@ +import json +import os +import unittest + +import mock + +from pywink.api import get_devices_from_response_dict, WinkApiInterface +from pywink.devices import types as device_types +from pywink.devices.key import WinkKey +from pywink.devices.powerstrip import WinkPowerStripOutlet, WinkPowerStrip +from pywink.devices.piggy_bank import WinkPorkfolioBalanceSensor, WinkPorkfolioNose +from pywink.devices.siren import WinkSiren +from pywink.devices.eggtray import WinkEggtray +from pywink.devices.remote import WinkRemote +from pywink.devices.fan import WinkFan +from pywink.devices.binary_switch import WinkBinarySwitch +from pywink.devices.hub import WinkHub +from pywink.devices.light_bulb import WinkLightBulb +from pywink.devices.thermostat import WinkThermostat +from pywink.devices.shade import WinkShade +from pywink.devices.sprinkler import WinkSprinkler +from pywink.devices.button import WinkButton +from pywink.devices.gang import WinkGang +from pywink.devices.camera import WinkCanaryCamera + + +class BaseTests(unittest.TestCase): + + def setUp(self): + super(BaseTests, self).setUp() + self.api_interface = mock.MagicMock() + all_devices = os.listdir('{}/api_responses/'.format(os.path.dirname(__file__))) + self.response_dict = {} + device_list = [] + for json_file in all_devices: + _json_file = open('{}/api_responses/{}'.format(os.path.dirname(__file__), json_file)) + device_list.append(json.load(_json_file)) + _json_file.close() + self.response_dict["data"] = device_list + + def test_all_devices_are_available(self): + devices = get_devices_from_response_dict(self.response_dict, device_types.ALL_SUPPORTED_DEVICES) + for device in devices: + self.assertTrue(device.available()) + + def test_all_devices_have_pubnub_channel(self): + devices = get_devices_from_response_dict(self.response_dict, device_types.ALL_SUPPORTED_DEVICES) + for device in devices: + self.assertIsNotNone(device.pubnub_channel) + + def test_all_devices_have_pubnub_key(self): + devices = get_devices_from_response_dict(self.response_dict, device_types.ALL_SUPPORTED_DEVICES) + for device in devices: + self.assertIsNotNone(device.pubnub_key) + + def test_all_devices_have_object_id(self): + devices = get_devices_from_response_dict(self.response_dict, device_types.ALL_SUPPORTED_DEVICES) + for device in devices: + self.assertIsNotNone(device.object_id()) + self.assertRegex(device.object_id(), "^[0-9]{3,7}$") + + def test_all_devices_state_should_not_be_none(self): + devices = get_devices_from_response_dict(self.response_dict, device_types.ALL_SUPPORTED_DEVICES) + for device in devices: + self.assertIsNotNone(device.state()) + + def test_all_devices_name_is_not_none(self): + devices = get_devices_from_response_dict(self.response_dict, device_types.ALL_SUPPORTED_DEVICES) + for device in devices: + self.assertIsNotNone(device.name()) + + def test_all_devices_state_is_not_none(self): + devices = get_devices_from_response_dict(self.response_dict, device_types.ALL_SUPPORTED_DEVICES) + for device in devices: + self.assertIsNotNone(device.state()) + + def test_all_devices_battery_is_valid(self): + devices = get_devices_from_response_dict(self.response_dict, device_types.ALL_SUPPORTED_DEVICES) + skip_types = [WinkFan, WinkPorkfolioBalanceSensor, WinkPorkfolioNose, WinkBinarySwitch, WinkHub, + WinkLightBulb, WinkThermostat, WinkKey, WinkPowerStrip, WinkPowerStripOutlet, + WinkRemote, WinkShade, WinkSprinkler, WinkButton, WinkGang, WinkCanaryCamera] + for device in devices: + if type(device) in skip_types: + self.assertIsNone(device.battery_level()) + elif device.manufacturer_device_model() == "wink_relay_sensor": + self.assertIsNone(device.manufacturer_device_id()) + elif device._last_reading.get('external_power'): + self.assertIsNone(device.battery_level()) + else: + self.assertIsNotNone(device.battery_level()) + + def test_all_devices_manufacturer_device_model_state_is_valid(self): + devices = get_devices_from_response_dict(self.response_dict, device_types.ALL_SUPPORTED_DEVICES) + skip_types = [WinkKey, WinkPorkfolioBalanceSensor, WinkPorkfolioNose, WinkPowerStripOutlet, + WinkSiren, WinkEggtray, WinkRemote, WinkPowerStrip] + for device in devices: + if type(device) in skip_types: + self.assertIsNone(device.manufacturer_device_model()) + elif device.name() == "GoControl Thermostat": + self.assertIsNone(device.manufacturer_device_model()) + else: + self.assertIsNotNone(device.manufacturer_device_model()) + + def test_all_devices_manufacturer_device_id_state_is_valid(self): + devices = get_devices_from_response_dict(self.response_dict, device_types.ALL_SUPPORTED_DEVICES) + skip_types = [WinkKey, WinkPowerStrip, WinkPowerStripOutlet, WinkPorkfolioBalanceSensor, WinkPorkfolioNose, + WinkSiren, WinkEggtray, WinkRemote, WinkButton] + skip_manufactuer_device_models = ["linear_wadwaz_1", "linear_wapirz_1", "aeon_labs_dsb45_zwus", "wink_hub", "wink_hub2", "sylvania_sylvania_ct", + "ge_bulb", "quirky_ge_spotter", "schlage_zwave_lock", "home_decorators_home_decorators_fan", + "sylvania_sylvania_rgbw", "somfy_bali", "wink_relay_sensor", "wink_project_one", "kidde_smoke_alarm", + "wink_relay_switch"] + skip_names = ["GoControl Thermostat", "GE Zwave Switch"] + for device in devices: + if device.name() in skip_names: + self.assertIsNone(device.manufacturer_device_id()) + elif device.manufacturer_device_model() in skip_manufactuer_device_models: + self.assertIsNone(device.manufacturer_device_id()) + elif type(device) in skip_types: + self.assertIsNone(device.manufacturer_device_id()) + else: + self.assertIsNotNone(device.manufacturer_device_id()) + + def test_all_devices_device_manufacturer_is_valid(self): + devices = get_devices_from_response_dict(self.response_dict, device_types.ALL_SUPPORTED_DEVICES) + for device in devices: + if type(device) is WinkKey: + self.assertIsNone(device.device_manufacturer()) + elif device.name() == "GoControl Thermostat": + self.assertIsNone(device.device_manufacturer()) + elif type(device) is WinkPowerStripOutlet: + self.assertIsNone(device.device_manufacturer()) + else: + self.assertIsNotNone(device.device_manufacturer()) + + def test_all_devices_model_name_is_valid(self): + devices = get_devices_from_response_dict(self.response_dict, device_types.ALL_SUPPORTED_DEVICES) + for device in devices: + if type(device) is WinkKey: + self.assertIsNone(device.model_name()) + elif device.name() == "GoControl Thermostat": + self.assertIsNone(device.model_name()) + elif type(device) is WinkPowerStripOutlet: + self.assertIsNone(device.model_name()) + else: + self.assertIsNotNone(device.model_name()) diff --git a/src/pywink/test/devices/fan_test.py b/src/pywink/test/devices/fan_test.py new file mode 100644 index 0000000..9c89359 --- /dev/null +++ b/src/pywink/test/devices/fan_test.py @@ -0,0 +1,62 @@ +import json +import os +import unittest + +import mock + +from pywink.api import get_devices_from_response_dict, WinkApiInterface +from pywink.devices import types as device_types +from pywink.devices.fan import WinkFan + + +class FanTests(unittest.TestCase): + + def setUp(self): + super(FanTests, self).setUp() + self.api_interface = mock.MagicMock() + device_list = [] + self.response_dict = {} + _json_file = open('{}/api_responses/fan.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + self.response_dict["data"] = device_list + + def test_fan_speeds(self): + fan = get_devices_from_response_dict(self.response_dict, device_types.FAN)[0] + has_speeds = fan.fan_speeds() + self.assertEqual(len(has_speeds), 5) + speeds = ['lowest', 'low', 'medium', 'high', 'auto'] + for speed in has_speeds: + self.assertTrue(speed in speeds) + + def test_fan_directions(self): + fan = get_devices_from_response_dict(self.response_dict, device_types.FAN)[0] + has_directions = fan.fan_directions() + self.assertEqual(len(has_directions), 2) + directions = ['forward', 'reverse'] + for direction in has_directions: + self.assertTrue(direction in directions) + + def test_fan_timer_range(self): + fan = get_devices_from_response_dict(self.response_dict, device_types.FAN)[0] + has_timer_range = fan.fan_timer_range() + self.assertEqual(len(has_timer_range), 2) + times = [0, 65535] + for time in has_timer_range: + self.assertTrue(time in times) + + def test_fan_speed_is_low(self): + fan = get_devices_from_response_dict(self.response_dict, device_types.FAN)[0] + self.assertEqual(fan.current_fan_speed(), "lowest") + + def test_fan_direction_is_forward(self): + fan = get_devices_from_response_dict(self.response_dict, device_types.FAN)[0] + self.assertEqual(fan.current_fan_direction(), "forward") + + def test_fan_timer_is_0(self): + fan = get_devices_from_response_dict(self.response_dict, device_types.FAN)[0] + self.assertEqual(fan.current_timer(), 0) + + def test_fan_state(self): + fan = get_devices_from_response_dict(self.response_dict, device_types.FAN)[0] + self.assertTrue(fan.state()) diff --git a/src/pywink/test/devices/garage_door_test.py b/src/pywink/test/devices/garage_door_test.py new file mode 100644 index 0000000..57d27bc --- /dev/null +++ b/src/pywink/test/devices/garage_door_test.py @@ -0,0 +1,37 @@ +import json +import os +import unittest + +import mock + +from pywink.api import get_devices_from_response_dict, WinkApiInterface +from pywink.devices import types as device_types +from pywink.devices.garage_door import WinkGarageDoor + + +class GarageDoorTests(unittest.TestCase): + + def setUp(self): + super(GarageDoorTests, self).setUp() + self.api_interface = mock.MagicMock() + all_devices = os.listdir('{}/api_responses/'.format(os.path.dirname(__file__))) + self.response_dict = {} + device_list = [] + for json_file in all_devices: + _json_file = open('{}/api_responses/{}'.format(os.path.dirname(__file__), json_file)) + device_list.append(json.load(_json_file)) + _json_file.close() + self.response_dict["data"] = device_list + + def test_tamper_detected_is_false(self): + devices = get_devices_from_response_dict(self.response_dict, device_types.GARAGE_DOOR) + self.assertEqual(len(devices), 1) + for device in devices: + self.assertFalse(device.tamper_detected()) + + def test_state_is_0(self): + devices = get_devices_from_response_dict(self.response_dict, device_types.GARAGE_DOOR) + self.assertEqual(len(devices), 1) + for device in devices: + self.assertEqual(device.state(), 0) + diff --git a/src/pywink/test/devices/hub_test.py b/src/pywink/test/devices/hub_test.py new file mode 100644 index 0000000..80acb20 --- /dev/null +++ b/src/pywink/test/devices/hub_test.py @@ -0,0 +1,61 @@ +import json +import os +import unittest + +import mock + +from pywink.api import get_devices_from_response_dict, WinkApiInterface +from pywink.devices import types as device_types +from pywink.devices.hub import WinkHub + + +class HubTests(unittest.TestCase): + + def setUp(self): + super(HubTests, self).setUp() + self.api_interface = mock.MagicMock() + all_devices = os.listdir('{}/api_responses/'.format(os.path.dirname(__file__))) + self.response_dict = {} + device_list = [] + for json_file in all_devices: + _json_file = open('{}/api_responses/{}'.format(os.path.dirname(__file__), json_file)) + device_list.append(json.load(_json_file)) + _json_file.close() + self.response_dict["data"] = device_list + + def test_unit_should_be_none(self): + devices = get_devices_from_response_dict(self.response_dict, device_types.HUB) + for device in devices: + self.assertIsNone(device.unit()) + + def test_kidde_radio_code_should_not_be_none(self): + devices = get_devices_from_response_dict(self.response_dict, device_types.HUB) + for device in devices: + if device.manufacturer_device_model() == "wink_project_one": + continue + if device.manufacturer_device_model() == "philips": + continue + else: + self.assertIsNotNone(device.kidde_radio_code()) + + def test_update_needed_is_false(self): + devices = get_devices_from_response_dict(self.response_dict, device_types.HUB) + for device in devices: + self.assertFalse(device.update_needed()) + + def test_ip_address_is_not_none(self): + devices = get_devices_from_response_dict(self.response_dict, device_types.HUB) + for device in devices: + if device.manufacturer_device_model() == "philips": + continue + else: + self.assertIsNotNone(device.ip_address()) + + def test_firmware_version_is_not_none(self): + devices = get_devices_from_response_dict(self.response_dict, device_types.HUB) + for device in devices: + if device.manufacturer_device_model() == "philips": + continue + else: + self.assertIsNotNone(device.firmware_version()) + diff --git a/src/pywink/test/devices/light_bulb_test.py b/src/pywink/test/devices/light_bulb_test.py new file mode 100644 index 0000000..20172ea --- /dev/null +++ b/src/pywink/test/devices/light_bulb_test.py @@ -0,0 +1,42 @@ +import json +import os +import unittest + +import mock + +from pywink.api import get_devices_from_response_dict, WinkApiInterface +from pywink.devices import types as device_types +from pywink.devices.light_bulb import WinkLightBulb + + +class LightBulbTests(unittest.TestCase): + + def setUp(self): + super(LightBulbTests, self).setUp() + self.api_interface = mock.MagicMock() + + def test_bulb_brightness(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/lightify_rgbw_bulb.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + bulb = get_devices_from_response_dict(response_dict, device_types.LIGHT_BULB)[0] + self.assertEqual(bulb.brightness(), 0.02) + + def test_bulb_color(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/lightify_rgbw_bulb.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + bulb = get_devices_from_response_dict(response_dict, device_types.LIGHT_BULB)[0] + self.assertFalse(bulb.supports_rgb()) + self.assertFalse(bulb.supports_xy_color()) + self.assertTrue(bulb.supports_hue_saturation()) + self.assertTrue(bulb.supports_temperature()) + self.assertEqual(bulb.color_temperature_kelvin(), 2755) + self.assertEqual(bulb.color_hue(), 0.0) + self.assertEqual(bulb.color_saturation(), 1.0) diff --git a/src/pywink/test/devices/lock_test.py b/src/pywink/test/devices/lock_test.py new file mode 100644 index 0000000..dba6ee5 --- /dev/null +++ b/src/pywink/test/devices/lock_test.py @@ -0,0 +1,49 @@ +import json +import os +import unittest + +import mock + +from pywink.api import get_devices_from_response_dict, WinkApiInterface +from pywink.devices import types as device_types + + +class LockTests(unittest.TestCase): + + def setUp(self): + super(LockTests, self).setUp() + self.api_interface = mock.MagicMock() + device_list = [] + self.response_dict = {} + _json_file = open('{}/api_responses/schlage_lock.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + self.response_dict["data"] = device_list + + def test_lock_state(self): + lock = get_devices_from_response_dict(self.response_dict, device_types.LOCK)[0] + self.assertTrue(lock.state()) + + def test_lock_alarm_enabled(self): + lock = get_devices_from_response_dict(self.response_dict, device_types.LOCK)[0] + self.assertTrue(lock.alarm_enabled()) + + def test_lock_alarm_mode(self): + lock = get_devices_from_response_dict(self.response_dict, device_types.LOCK)[0] + self.assertEqual(lock.alarm_mode(), "tamper") + + def test_lock_vaction_mode_enabled(self): + lock = get_devices_from_response_dict(self.response_dict, device_types.LOCK)[0] + self.assertFalse(lock.vacation_mode_enabled()) + + def test_beeper_enabled(self): + lock = get_devices_from_response_dict(self.response_dict, device_types.LOCK)[0] + self.assertTrue(lock.beeper_enabled()) + + def test_auto_lock_enabled(self): + lock = get_devices_from_response_dict(self.response_dict, device_types.LOCK)[0] + self.assertTrue(lock.auto_lock_enabled()) + + def test_lock_alarm_sensitivity(self): + lock = get_devices_from_response_dict(self.response_dict, device_types.LOCK)[0] + self.assertEqual(lock.alarm_sensitivity(), 0.6) diff --git a/src/pywink/test/devices/powerstrip_test.py b/src/pywink/test/devices/powerstrip_test.py new file mode 100644 index 0000000..a662b5e --- /dev/null +++ b/src/pywink/test/devices/powerstrip_test.py @@ -0,0 +1,65 @@ +import json +import os +import unittest + +import mock + +from pywink.api import get_devices_from_response_dict, WinkApiInterface +from pywink.devices import types as device_types +from pywink.devices.powerstrip import WinkPowerStrip, WinkPowerStripOutlet + + +class PowerstripTests(unittest.TestCase): + + def test_state_powerstrip_state_should_be_true_if_one_outlet_is_true(self): + with open('{}/api_responses/pivot_power_genius.json'.format(os.path.dirname(__file__))) as powerstrip_file: + response_dict = json.load(powerstrip_file) + response_dict = {"data": [response_dict]} + devices = get_devices_from_response_dict(response_dict, device_types.POWERSTRIP) + + self.assertEqual(len(devices), 3) + powerstrip = devices[-1] + outlet_1 = devices[0] + outlet_2 = devices[1] + self.assertTrue(powerstrip.state()) + self.assertFalse(outlet_1.state()) + self.assertTrue(outlet_2.state()) + + def test_outlet_parent_id_matches_powerstrip_id(self): + with open('{}/api_responses/pivot_power_genius.json'.format(os.path.dirname(__file__))) as powerstrip_file: + response_dict = json.load(powerstrip_file) + response_dict = {"data": [response_dict]} + devices = get_devices_from_response_dict(response_dict, device_types.POWERSTRIP) + + self.assertEqual(len(devices), 3) + powerstrip = devices[-1] + outlet_1 = devices[0] + outlet_2 = devices[1] + self.assertEqual(powerstrip.object_id(), outlet_1.parent_id()) + self.assertEqual(powerstrip.object_id(), outlet_2.parent_id()) + + def test_outlet_index(self): + with open('{}/api_responses/pivot_power_genius.json'.format(os.path.dirname(__file__))) as powerstrip_file: + response_dict = json.load(powerstrip_file) + response_dict = {"data": [response_dict]} + devices = get_devices_from_response_dict(response_dict, device_types.POWERSTRIP) + + self.assertEqual(len(devices), 3) + powerstrip = devices[-1] + outlet_1 = devices[0] + outlet_2 = devices[1] + self.assertEqual(0, outlet_1.index()) + self.assertEqual(1, outlet_2.index()) + + def test_outlet_parent_object_type(self): + with open('{}/api_responses/pivot_power_genius.json'.format(os.path.dirname(__file__))) as powerstrip_file: + response_dict = json.load(powerstrip_file) + response_dict = {"data": [response_dict]} + devices = get_devices_from_response_dict(response_dict, device_types.POWERSTRIP) + + self.assertEqual(len(devices), 3) + powerstrip = devices[-1] + outlet_1 = devices[0] + outlet_2 = devices[1] + self.assertEqual(powerstrip.object_type(), outlet_1.parent_object_type()) + self.assertEqual(powerstrip.object_type(), outlet_2.parent_object_type()) diff --git a/src/pywink/test/devices/sensor_test.py b/src/pywink/test/devices/sensor_test.py new file mode 100644 index 0000000..ffc0105 --- /dev/null +++ b/src/pywink/test/devices/sensor_test.py @@ -0,0 +1,220 @@ +import json +import os +import unittest + +import mock + +from pywink.api import get_devices_from_response_dict, WinkApiInterface +from pywink.devices import types as device_types +from pywink.devices.sensor import WinkSensor +from pywink.devices.piggy_bank import WinkPorkfolioBalanceSensor +from pywink.devices.smoke_detector import WinkSmokeDetector, WinkCoDetector, WinkSmokeSeverity, WinkCoSeverity + +class SensorTests(unittest.TestCase): + + def setUp(self): + super(SensorTests, self).setUp() + self.api_interface = mock.MagicMock() + all_devices = os.listdir('{}/api_responses/'.format(os.path.dirname(__file__))) + self.response_dict = {} + device_list = [] + for json_file in all_devices: + _json_file = open('{}/api_responses/{}'.format(os.path.dirname(__file__), json_file)) + device_list.append(json.load(_json_file)) + _json_file.close() + self.response_dict["data"] = device_list + + def test_capability_should_not_be_none(self): + devices = get_devices_from_response_dict(self.response_dict, device_types.SENSOR_POD) + for device in devices: + self.assertIsNotNone(device.capability()) + + def test_tamper_detected_should_be_false(self): + devices = get_devices_from_response_dict(self.response_dict, device_types.SENSOR_POD) + for device in devices: + self.assertFalse(device.tamper_detected()) + + def test_unit_is_valid(self): + devices = get_devices_from_response_dict(self.response_dict, device_types.SENSOR_POD) + for device in devices: + if device.unit_type() == "boolean": + self.assertIsNone(device.unit()) + else: + self.assertIsNotNone(device.unit()) + + +class EggtrayTests(unittest.TestCase): + + def setUp(self): + super(EggtrayTests, self).setUp() + self.api_interface = mock.MagicMock() + all_devices = os.listdir('{}/api_responses/'.format(os.path.dirname(__file__))) + self.response_dict = {} + device_list = [] + for json_file in all_devices: + _json_file = open('{}/api_responses/{}'.format(os.path.dirname(__file__), json_file)) + device_list.append(json.load(_json_file)) + _json_file.close() + self.response_dict["data"] = device_list + + def test_state_should_be_2(self): + devices = get_devices_from_response_dict(self.response_dict, device_types.EGGTRAY) + for device in devices: + self.assertEqual(device.state(), 2) + +class KeyTests(unittest.TestCase): + + def setUp(self): + super(KeyTests, self).setUp() + self.api_interface = mock.MagicMock() + all_devices = os.listdir('{}/api_responses/'.format(os.path.dirname(__file__))) + self.response_dict = {} + device_list = [] + for json_file in all_devices: + _json_file = open('{}/api_responses/{}'.format(os.path.dirname(__file__), json_file)) + device_list.append(json.load(_json_file)) + _json_file.close() + self.response_dict["data"] = device_list + + def test_state_should_be_false(self): + devices = get_devices_from_response_dict(self.response_dict, device_types.KEY) + self.assertEqual(len(devices), 1) + for device in devices: + self.assertFalse(device.state()) + + def test_parent_id_should_not_be_none(self): + devices = get_devices_from_response_dict(self.response_dict, device_types.KEY) + for device in devices: + self.assertIsNotNone(device.parent_id()) + + def test_availble_is_true(self): + devices = get_devices_from_response_dict(self.response_dict, device_types.KEY) + for device in devices: + self.assertTrue(device.available()) + + def test_capability_is_activity_detected(self): + devices = get_devices_from_response_dict(self.response_dict, device_types.KEY) + for device in devices: + self.assertEqual(device.capability(), "activity_detected") + + def test_unit_is_none(self): + devices = get_devices_from_response_dict(self.response_dict, device_types.KEY) + for device in devices: + self.assertIsNone(device.unit()) + +class PorkfolioTests(unittest.TestCase): + + def setUp(self): + super(PorkfolioTests, self).setUp() + self.api_interface = mock.MagicMock() + all_devices = os.listdir('{}/api_responses/'.format(os.path.dirname(__file__))) + self.response_dict = {} + device_list = [] + for json_file in all_devices: + _json_file = open('{}/api_responses/{}'.format(os.path.dirname(__file__), json_file)) + device_list.append(json.load(_json_file)) + _json_file.close() + self.response_dict["data"] = device_list + + def test_unit_is_usd(self): + devices = get_devices_from_response_dict(self.response_dict, device_types.PIGGY_BANK) + self.assertEqual(len(devices), 2) + for device in devices: + if isinstance(device, WinkPorkfolioBalanceSensor): + self.assertEqual(device.unit(), "USD") + + def test_capability_is_balance(self): + devices = get_devices_from_response_dict(self.response_dict, device_types.PIGGY_BANK) + for device in devices: + if isinstance(device, WinkPorkfolioBalanceSensor): + self.assertEqual(device.capability(), "balance") + + def test_state_is_180(self): + devices = get_devices_from_response_dict(self.response_dict, device_types.PIGGY_BANK) + for device in devices: + if isinstance(device, WinkPorkfolioBalanceSensor): + self.assertEqual(device.state(), 180) + + def test_available_is_true(self): + devices = get_devices_from_response_dict(self.response_dict, device_types.PIGGY_BANK) + for device in devices: + if isinstance(device, WinkPorkfolioBalanceSensor): + self.assertTrue(device.available()) + +class GangTests(unittest.TestCase): + + def setUp(self): + super(GangTests, self).setUp() + self.api_interface = mock.MagicMock() + all_devices = os.listdir('{}/api_responses/'.format(os.path.dirname(__file__))) + self.response_dict = {} + device_list = [] + for json_file in all_devices: + _json_file = open('{}/api_responses/{}'.format(os.path.dirname(__file__), json_file)) + device_list.append(json.load(_json_file)) + _json_file.close() + self.response_dict["data"] = device_list + + def test_unit_is_none(self): + devices = get_devices_from_response_dict(self.response_dict, device_types.GANG) + for device in devices: + self.assertIsNone(device.unit()) + +class SmokeDetectorTests(unittest.TestCase): + + def setUp(self): + super(SmokeDetectorTests, self).setUp() + self.api_interface = mock.MagicMock() + all_devices = os.listdir('{}/api_responses/'.format(os.path.dirname(__file__))) + self.response_dict = {} + device_list = [] + for json_file in all_devices: + _json_file = open('{}/api_responses/{}'.format(os.path.dirname(__file__), json_file)) + device_list.append(json.load(_json_file)) + _json_file.close() + self.response_dict["data"] = device_list + + def test_unit_is_none(self): + devices = get_devices_from_response_dict(self.response_dict, device_types.SMOKE_DETECTOR) + for device in devices: + if isinstance(device, WinkSmokeDetector): + self.assertIsNone(device.unit()) + self.assertEqual(device.unit_type(), "boolean") + if isinstance(device, WinkCoDetector): + self.assertIsNone(device.unit()) + self.assertEqual(device.unit_type(), "boolean") + if isinstance(device, WinkSmokeSeverity): + self.assertIsNone(device.unit()) + self.assertIsNone(device.unit_type()) + if isinstance(device, WinkCoSeverity): + self.assertIsNone(device.unit()) + self.assertIsNone(device.unit_type()) + + +class RemoteTests(unittest.TestCase): + + def setUp(self): + super(RemoteTests, self).setUp() + self.api_interface = mock.MagicMock() + all_devices = os.listdir('{}/api_responses/'.format(os.path.dirname(__file__))) + self.response_dict = {} + device_list = [] + for json_file in all_devices: + _json_file = open('{}/api_responses/{}'.format(os.path.dirname(__file__), json_file)) + device_list.append(json.load(_json_file)) + _json_file.close() + self.response_dict["data"] = device_list + + def test_buttons_press_is_false(self): + devices = get_devices_from_response_dict(self.response_dict, device_types.REMOTE) + remote = devices[0] + self.assertFalse(remote.button_on_pressed()) + self.assertFalse(remote.button_off_pressed()) + self.assertFalse(remote.button_up_pressed()) + self.assertFalse(remote.button_down_pressed()) + + def test_unit_and_capability(self): + devices = get_devices_from_response_dict(self.response_dict, device_types.REMOTE) + remote = devices[0] + self.assertIsNone(remote.unit()) + self.assertEqual(remote.capability(), "opened") diff --git a/src/pywink/test/devices/siren_test.py b/src/pywink/test/devices/siren_test.py new file mode 100644 index 0000000..65dc947 --- /dev/null +++ b/src/pywink/test/devices/siren_test.py @@ -0,0 +1,33 @@ +import json +import os +import unittest + +import mock + +from pywink.api import get_devices_from_response_dict, WinkApiInterface +from pywink.devices import types as device_types + + +class SirenTests(unittest.TestCase): + + def setUp(self): + super(SirenTests, self).setUp() + self.api_interface = mock.MagicMock() + device_list = [] + self.response_dict = {} + _json_file = open('{}/api_responses/go_control_siren.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + self.response_dict["data"] = device_list + + def test_siren_state(self): + siren = get_devices_from_response_dict(self.response_dict, device_types.SIREN)[0] + self.assertFalse(siren.state()) + + def test_siren_mode(self): + siren = get_devices_from_response_dict(self.response_dict, device_types.SIREN)[0] + self.assertEqual(siren.mode(), "siren_and_strobe") + + def test_siren_auto_shutoff(self): + siren = get_devices_from_response_dict(self.response_dict, device_types.SIREN)[0] + self.assertEqual(siren.auto_shutoff(), 60) diff --git a/src/pywink/test/devices/standard/__init__.py b/src/pywink/test/devices/standard/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/pywink/test/devices/standard/api_responses/__init__.py b/src/pywink/test/devices/standard/api_responses/__init__.py deleted file mode 100644 index c576e6e..0000000 --- a/src/pywink/test/devices/standard/api_responses/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -import json -import os - - -class ApiResponseJSONLoader(object): - - def __init__(self, file_name): - self.file_name = file_name - - def load(self): - with open('{}/{}'.format(os.path.dirname(__file__), - self.file_name)) as json_file: - response_dict = json.load(json_file) - return response_dict diff --git a/src/pywink/test/devices/standard/api_responses/binary_sensor.json b/src/pywink/test/devices/standard/api_responses/binary_sensor.json deleted file mode 100644 index 93404c9..0000000 --- a/src/pywink/test/devices/standard/api_responses/binary_sensor.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "data": [{ - "last_event": { - "brightness_occurred_at": null, - "loudness_occurred_at": null, - "vibration_occurred_at": null - }, - "model_name": "Tripper", - "capabilities": { - "sensor_types": [ - { - "field": "opened", - "type": "boolean" - }, - { - "field": "battery", - "type": "percentage" - } - ] - }, - "manufacturer_device_model": "quirky_ge_tripper", - "location": "", - "radio_type": "zigbee", - "manufacturer_device_id": null, - "gang_id": null, - "sensor_pod_id": "37614", - "subscription": { - }, - "units": { - }, - "upc_id": "184", - "hidden_at": null, - "last_reading": { - "battery_voltage_threshold_2": 0, - "opened": false, - "battery_alarm_mask": 0, - "opened_updated_at": 1421697092.7347496, - "battery_voltage_min_threshold_updated_at": 1421697092.7347229, - "battery_voltage_min_threshold": 0, - "connection": null, - "battery_voltage": 25, - "battery_voltage_threshold_1": 25, - "connection_updated_at": null, - "battery_voltage_threshold_3": 0, - "battery_voltage_updated_at": 1421697092.7347066, - "battery_voltage_threshold_1_updated_at": 1421697092.7347302, - "battery_voltage_threshold_3_updated_at": 1421697092.7347434, - "battery_voltage_threshold_2_updated_at": 1421697092.7347374, - "battery": 1.0, - "battery_updated_at": 1421697092.7347553, - "battery_alarm_mask_updated_at": 1421697092.734716 - }, - "triggers": [ - ], - "name": "MasterBathroom", - "lat_lng": [ - 37.550773, - -122.279182 - ], - "uuid": "a2cb868a-dda3-4211-ab73-fc08087aeed7", - "locale": "en_us", - "device_manufacturer": "quirky_ge", - "created_at": 1421523277, - "local_id": "2", - "hub_id": "88264" - }] -} \ No newline at end of file diff --git a/src/pywink/test/devices/standard/api_responses/binary_switch.json b/src/pywink/test/devices/standard/api_responses/binary_switch.json deleted file mode 100644 index 737678f..0000000 --- a/src/pywink/test/devices/standard/api_responses/binary_switch.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "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": {} -} \ No newline at end of file diff --git a/src/pywink/test/devices/standard/api_responses/device_with_pubnub.json b/src/pywink/test/devices/standard/api_responses/device_with_pubnub.json deleted file mode 100644 index 84616c8..0000000 --- a/src/pywink/test/devices/standard/api_responses/device_with_pubnub.json +++ /dev/null @@ -1,183 +0,0 @@ -{ - "data":[ - { - "uuid":"1ccb4e6f-45d4-46a2-b9b8-e98d6f0ad967", - "desired_state":{ - "locked":true, - "beeper_enabled":false, - "vacation_mode_enabled":false, - "auto_lock_enabled":false, - "key_code_length":4, - "alarm_mode":null, - "alarm_sensitivity":0.6, - "alarm_enabled":false - }, - "last_reading":{ - "connection":true, - "connection_updated_at":1462370596.2303777, - "locked":true, - "locked_updated_at":1462370596.2303777, - "battery":0.77, - "battery_updated_at":1462370596.2303777, - "alarm_activated":null, - "alarm_activated_updated_at":null, - "beeper_enabled":false, - "beeper_enabled_updated_at":1462370596.2303777, - "vacation_mode_enabled":false, - "vacation_mode_enabled_updated_at":1462370596.2303777, - "auto_lock_enabled":false, - "auto_lock_enabled_updated_at":1462370596.2303777, - "key_code_length":4, - "key_code_length_updated_at":1462370596.2303777, - "alarm_mode":null, - "alarm_mode_updated_at":1462370596.2303777, - "alarm_sensitivity":0.6, - "alarm_sensitivity_updated_at":1462370596.2303777, - "alarm_enabled":false, - "alarm_enabled_updated_at":1462370596.2303777, - "last_error":null, - "last_error_updated_at":1450137143.224417, - "desired_locked_updated_at":1462361916.879291, - "desired_beeper_enabled_updated_at":1462361916.879291, - "desired_vacation_mode_enabled_updated_at":1462361916.879291, - "desired_auto_lock_enabled_updated_at":1462361916.879291, - "desired_key_code_length_updated_at":1462361916.879291, - "desired_alarm_mode_updated_at":1462361916.879291, - "desired_alarm_sensitivity_updated_at":1462361916.879291, - "desired_alarm_enabled_updated_at":1462361916.879291, - "connection_changed_at":1458334995.4152315, - "locked_changed_at":1462361711.8780804, - "battery_changed_at":1462370596.2303777, - "beeper_enabled_changed_at":1450888993.4345925, - "vacation_mode_enabled_changed_at":1449960081.8146806, - "auto_lock_enabled_changed_at":1449960081.8146806, - "alarm_enabled_changed_at":1449960081.8146806, - "key_code_length_changed_at":1449960081.8146806, - "alarm_sensitivity_changed_at":1449960088.2434776, - "desired_locked_changed_at":1462361916.879291, - "desired_beeper_enabled_changed_at":1450888993.5529706, - "desired_vacation_mode_enabled_changed_at":1449960446.6464362, - "desired_auto_lock_enabled_changed_at":1449960446.6464362, - "desired_key_code_length_changed_at":1449960446.6464362, - "desired_alarm_mode_changed_at":1450889063.721673, - "desired_alarm_sensitivity_changed_at":1449960446.6464362, - "desired_alarm_enabled_changed_at":1449960446.6464362, - "alarm_mode_changed_at":1452080631.3350449 - }, - "subscription":{ - "pubnub":{ - "subscribe_key":"sub-c-f7bf7f7e-0542-11e3-a123-02ee2ddab7fe", - "channel":"9afda0e9bb607d520b01b9d58a24400e6b381ab7|lock-61708|user-377857" - } - }, - "lock_id":"61708", - "name":"Front Door", - "locale":"en_us", - "units":{ - - }, - "created_at":1449960079, - "hidden_at":null, - "capabilities":{ - "fields":[ - { - "type":"boolean", - "field":"connection", - "mutability":"read-only" - }, - { - "type":"boolean", - "field":"locked", - "mutability":"read-write" - }, - { - "type":"percentage", - "field":"battery", - "mutability":"read-only" - }, - { - "type":"boolean", - "field":"alarm_activated", - "mutability":"read-only" - }, - { - "type":"boolean", - "field":"beeper_enabled", - "mutability":"read-write" - }, - { - "type":"boolean", - "field":"vacation_mode_enabled", - "mutability":"read-write" - }, - { - "type":"boolean", - "field":"auto_lock_enabled", - "mutability":"read-write" - }, - { - "type":"integer", - "field":"key_code_length", - "range":[ - 4, - 8 - ], - "mutability":"read-write" - }, - { - "type":"string", - "field":"alarm_mode", - "choices":[ - "alert", - "tamper", - "forced_entry", - null - ], - "mutability":"read-write" - }, - { - "type":"percentage", - "field":"alarm_sensitivity", - "choices":[ - 0.2, - 0.4, - 0.6, - 0.8, - 1 - ], - "mutability":"read-write" - }, - { - "type":"boolean", - "field":"alarm_enabled", - "mutability":"read-write" - } - ], - "home_security_device":true - }, - "triggers":[ - - ], - "manufacturer_device_model":"schlage_zwave_lock", - "manufacturer_device_id":null, - "device_manufacturer":"schlage", - "model_name":"BE469", - "upc_id":"11", - "upc_code":"043156312214", - "hub_id":"302528", - "local_id":"1", - "radio_type":"zwave", - "lat_lng":[ - 39.019975, - -84.441513 - ], - "location":"" - } - ], - "errors":[ - - ], - "pagination":{ - "count":1 - } -} diff --git a/src/pywink/test/devices/standard/api_responses/door_sensor_gocontrol.json b/src/pywink/test/devices/standard/api_responses/door_sensor_gocontrol.json deleted file mode 100644 index b8a1bef..0000000 --- a/src/pywink/test/devices/standard/api_responses/door_sensor_gocontrol.json +++ /dev/null @@ -1,87 +0,0 @@ -{ - "data": [ - { - "last_event": { - "brightness_occurred_at": null, - "loudness_occurred_at": null, - "vibration_occurred_at": null - }, - "object_type": "sensor_pod", - "object_id": "192444", - "uuid": "REMOVED", - "icon_id": null, - "icon_code": null, - "desired_state": {}, - "last_reading": { - "opened": true, - "opened_updated_at": 1465222176.887616, - "tamper_detected": null, - "tamper_detected_updated_at": null, - "battery": 1, - "battery_updated_at": 1465222176.887616, - "tamper_detected_true": null, - "tamper_detected_true_updated_at": null, - "connection": true, - "connection_updated_at": 1465222176.887616, - "agent_session_id": null, - "agent_session_id_updated_at": null, - "connection_changed_at": 1462044709.6055737, - "battery_changed_at": 1462048973.2091925, - "opened_changed_at": 1465222176.887616 - }, - "subscription": { - "pubnub": { - "subscribe_key": "REMOVED", - "channel": "REMOVED" - } - }, - "sensor_pod_id": "192444", - "name": "Brad Door", - "locale": "en_us", - "units": {}, - "created_at": 1462044709, - "hidden_at": null, - "capabilities": { - "fields": [ - { - "type": "boolean", - "field": "opened", - "mutability": "read-only" - }, - { - "type": "boolean", - "field": "tamper_detected", - "mutability": "read-only" - }, - { - "type": "percentage", - "field": "battery", - "mutability": "read-only" - } - ], - "home_security_device": true - }, - "triggers": [], - "manufacturer_device_model": "linear_wadwaz_1", - "manufacturer_device_id": null, - "device_manufacturer": "linear", - "model_name": "Z-Wave Door / Window Transmitter", - "upc_id": "189", - "upc_code": "9386312509", - "gang_id": null, - "hub_id": "300039", - "local_id": "10", - "radio_type": "zwave", - "linked_service_id": null, - "lat_lng": [ - 0, - 0 - ], - "location": "" - } - ], - "errors": [], - "pagination": { - "count": 13 - } -} diff --git a/src/pywink/test/devices/standard/api_responses/eggtray.json b/src/pywink/test/devices/standard/api_responses/eggtray.json deleted file mode 100644 index 0a1ebde..0000000 --- a/src/pywink/test/devices/standard/api_responses/eggtray.json +++ /dev/null @@ -1,80 +0,0 @@ -{ - "data": [{ - "last_reading": { - "connection": true, - "connection_updated_at": 1417823487.490747, - "battery": 0.83, - "battery_updated_at": 1417823487.490747, - "inventory": 3, - "inventory_updated_at": 1449705551.7313306, - "freshness_remaining": 2419191, - "freshness_remaining_updated_at": 1449705551.7313495, - "age_updated_at": 1449705551.7313418, - "age": 1449705542, - "connection_changed_at": 1449705443.6858568, - "next_trigger_at_updated_at": null, - "next_trigger_at": null, - "egg_1_timestamp_updated_at": 1449753143.8631344, - "egg_1_timestamp_changed_at": 1449705534.0782206, - "egg_1_timestamp": 1449705545.0, - "egg_2_timestamp_updated_at": 1449753143.8631344, - "egg_2_timestamp_changed_at": 1449705534.0782206, - "egg_2_timestamp": 1449705545.0, - "egg_3_timestamp_updated_at": 1449753143.8631344, - "egg_3_timestamp_changed_at": 1449705534.0782206, - "egg_3_timestamp": 1449705545.0, - "egg_4_timestamp_updated_at": 1449753143.8631344, - "egg_4_timestamp_changed_at": 1449705534.0782206, - "egg_4_timestamp": 1449705545.0, - "egg_5_timestamp_updated_at": 1449753143.8631344, - "egg_5_timestamp_changed_at": 1449705534.0782206, - "egg_5_timestamp": 1449705545.0, - "egg_6_timestamp_updated_at": 1449753143.8631344, - "egg_6_timestamp_changed_at": 1449705534.0782206, - "egg_6_timestamp": 1449705545.0, - "egg_7_timestamp_updated_at": 1449753143.8631344, - "egg_7_timestamp_changed_at": 1449705534.0782206, - "egg_7_timestamp": 1449705545.0, - "egg_8_timestamp_updated_at": 1449753143.8631344, - "egg_8_timestamp_changed_at": 1449705534.0782206, - "egg_8_timestamp": 1449705545.0, - "egg_9_timestamp_updated_at": 1449753143.8631344, - "egg_9_timestamp_changed_at": 1449705534.0782206, - "egg_9_timestamp": 1449705545.0, - "egg_10_timestamp_updated_at": 1449753143.8631344, - "egg_10_timestamp_changed_at": 1449705534.0782206, - "egg_10_timestamp": 1449705545.0, - "egg_11_timestamp_updated_at": 1449753143.8631344, - "egg_11_timestamp_changed_at": 1449705534.0782206, - "egg_11_timestamp": 1449705545.0, - "egg_12_timestamp_updated_at": 1449753143.8631344, - "egg_12_timestamp_changed_at": 1449705534.0782206, - "egg_12_timestamp": 1449705545.0, - "egg_13_timestamp_updated_at": 1449753143.8631344, - "egg_13_timestamp_changed_at": 1449705534.0782206, - "egg_13_timestamp": 1449705545.0, - "egg_14_timestamp_updated_at": 1449753143.8631344, - "egg_14_timestamp_changed_at": 1449705534.0782206, - "egg_14_timestamp": 1449705545.0 - }, - "eggtray_id": "153869", - "name": "Egg Minder", - "freshness_period": 2419200, - "locale": "en_us", - "units": {}, - "created_at": 1417823382, - "hidden_at": null, - "capabilities": {}, - "triggers": [], - "device_manufacturer": "quirky_ge", - "model_name": "Egg Minder", - "upc_id": "23", - "upc_code": "814434017233", - "lat_lng": [38.429962, -122.653715], - "location": "" - }], - "errors": [], - "pagination": { - "count": 1 - } -} \ No newline at end of file diff --git a/src/pywink/test/devices/standard/api_responses/fan.json b/src/pywink/test/devices/standard/api_responses/fan.json deleted file mode 100644 index dd0dfda..0000000 --- a/src/pywink/test/devices/standard/api_responses/fan.json +++ /dev/null @@ -1,119 +0,0 @@ -{ - "data":[ - { - "object_type":"fan", - "object_id":"1359", - "uuid":"2cf8d024-6838-4db6-82f6-0eca51123456", - "icon_id":null, - "icon_code":null, - "desired_state":{ - "mode":"lowest", - "powered":true, - "timer":0, - "direction":null - }, - "last_reading":{ - "mode":"lowest", - "mode_updated_at":1481335377.2255096, - "powered":true, - "powered_updated_at":1481335377.2255096, - "timer":0, - "timer_updated_at":1481335377.2255096, - "direction":"forward", - "direction_updated_at":null, - "connection":true, - "connection_updated_at":1481714301.2966304, - "firmware_version":"0.0b00 / 0.0b0e", - "firmware_version_updated_at":1481335377.2255096, - "firmware_date_code":null, - "firmware_date_code_updated_at":null, - "desired_mode_updated_at":1481335684.7678573, - "desired_powered_updated_at":1481335684.7678573, - "desired_timer_updated_at":1481335810.1496878, - "desired_direction_updated_at":1481335810.1496878 - }, - "subscription":{ - "pubnub":{ - "subscribe_key":"sub-c-f7bf7f7e-1234-11e3-a5e8-02ee2d123456", - "channel":"7ab843be9ac23f543f907798fc1508c214ee06f8|fan-1359|user-212345" - } - }, - "fan_id":"1359", - "name":"Fan", - "locale":"en_us", - "units":{ - - }, - "created_at":1481335344, - "hidden_at":null, - "capabilities":{ - "fields":[ - { - "type":"selection", - "field":"mode", - "choices":[ - "lowest", - "low", - "medium", - "high", - "auto" - ], - "mutability":"read-write" - }, - { - "type":"boolean", - "field":"powered", - "mutability":"read-write" - }, - { - "type":"integer", - "field":"timer", - "range":[ - 0, - 65535 - ], - "mutability":"read-write" - }, - { - "type":"selection", - "field":"direction", - "choices":[ - "forward", - "reverse" - ], - "mutability":"read-write" - }, - { - "type":"boolean", - "field":"connection", - "mutability":"read-only" - } - ] - }, - "triggers":[ - - ], - "manufacturer_device_model":"home_decorators_home_decorators_fan", - "manufacturer_device_id":null, - "device_manufacturer":"home_decorators", - "model_name":"Ceiling Fan", - "upc_id":"486", - "upc_code":"home_decorators_fan", - "gang_id":"56113", - "hub_id":"288962", - "local_id":"48.1", - "radio_type":"zigbee", - "lat_lng":[ - 12.345678, - -98.765432 - ], - "location":"" - } - ], - "errors":[ - - ], - "pagination":{ - - } -} diff --git a/src/pywink/test/devices/standard/api_responses/garage_door.json b/src/pywink/test/devices/standard/api_responses/garage_door.json deleted file mode 100644 index b12f2a6..0000000 --- a/src/pywink/test/devices/standard/api_responses/garage_door.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "data": [{ - "desired_state": { - "position": 0 - }, - "last_reading": { - "position_opened": "N\/A", - "position_opened_updated_at": 1450357467.371, - "tamper_detected_true": null, - "tamper_detected_true_updated_at": null, - "connection": true, - "connection_updated_at": 1450357538.2715, - "position": 0, - "position_updated_at": 1450357537.836, - "battery": null, - "battery_updated_at": null, - "fault": false, - "fault_updated_at": 1447976866.0784, - "disabled": null, - "disabled_updated_at": null, - "control_enabled": true, - "control_enabled_updated_at": 1447976866.0784, - "desired_position_updated_at": 1447976846.8869, - "connection_changed_at": 1444775470.5484, - "position_changed_at": 1450357537.836, - "control_enabled_changed_at": 1444775472.2474, - "fault_changed_at": 1444775472.2474, - "position_opened_changed_at": 1450357467.371, - "desired_position_changed_at": 1447976846.8869 - }, - "garage_door_id": "30528", - "name": "Garage Door", - "locale": "en_us", - "units": { - - }, - "created_at": 1444775470, - "hidden_at": null, - "capabilities": { - "home_security_device": true - }, - "triggers": [ - - ], - "manufacturer_device_model": "chamberlain_garage_door_opener", - "manufacturer_device_id": "1133930", - "device_manufacturer": "chamberlain", - "model_name": "MyQ Garage Door Controller", - "upc_id": "26", - "upc_code": "012381109302", - "hub_id": null, - "local_id": null, - "radio_type": null, - "linked_service_id": "206203", - "lat_lng": [ - 0, - 0 - ], - "location": "", - "order": null - }], - "errors": [], - "pagination": {} -} \ No newline at end of file diff --git a/src/pywink/test/devices/standard/api_responses/gocontrol_thermostat.json b/src/pywink/test/devices/standard/api_responses/gocontrol_thermostat.json deleted file mode 100644 index a41ce50..0000000 --- a/src/pywink/test/devices/standard/api_responses/gocontrol_thermostat.json +++ /dev/null @@ -1,181 +0,0 @@ -{ - "data":[ - { - "model_name":null, - "capabilities":{ - "notification_robots":[ - "aux_active_notification" - ], - "fields":[ - { - "field":"max_set_point", - "type":"float", - "mutability":"read-write" - }, - { - "field":"min_set_point", - "type":"float", - "mutability":"read-write" - }, - { - "field":"powered", - "type":"boolean", - "mutability":"read-write" - }, - { - "field":"units", - "type":"nested_hash", - "mutability":"read-only" - }, - { - "field":"temperature", - "type":"float", - "mutability":"read-only" - }, - { - "field":"external_temperature", - "type":"float", - "mutability":"read-only" - }, - { - "field":"min_min_set_point", - "type":"float", - "mutability":"read-only" - }, - { - "field":"max_min_set_point", - "type":"float", - "mutability":"read-only" - }, - { - "field":"min_max_set_point", - "type":"float", - "mutability":"read-only" - }, - { - "field":"max_max_set_point", - "type":"float", - "mutability":"read-only" - }, - { - "field":"deadband", - "type":"float", - "mutability":"read-only" - }, - { - "field":"connection", - "type":"boolean", - "mutability":"read-only" - }, - { - "field":"fan_mode", - "choices":[ - "on", - "auto" - ], - "type":"selection", - "mutability":"read-write" - }, - { - "field":"mode", - "choices":[ - "heat_only", - "cool_only", - "auto", - "aux" - ], - "type":"selection", - "mutability":"read-write" - } - ] - }, - "location":"", - "units":{ - "temperature":"f" - }, - "name":"Thermostat", - "smart_schedule_enabled":false, - "upc_id":"129", - "manufacturer_device_id":null, - "lat_lng":[ - 98.765432, - 12.345678 - ], - "thermostat_id":"115090", - "device_manufacturer":null, - "local_id":"18", - "hidden_at":null, - "manufacturer_device_model":null, - "linked_service_id":null, - "created_at":1455335503, - "subscription":{ - "pubnub":{ - "channel":"[removed]", - "subscribe_key":"[removed]" - } - }, - "hub_id":"196567", - "locale":"en_us", - "last_reading":{ - "desired_min_set_point_updated_at":1463210230.8420756, - "modes_allowed_updated_at":1475953458.4724526, - "connection_updated_at":1475953458.4724526, - "min_min_set_point":null, - "desired_powered_updated_at":1461454387.768337, - "connection":true, - "fan_mode":"auto", - "external_temperature_updated_at":null, - "min_min_set_point_updated_at":null, - "desired_units_updated_at":1455335651.9014053, - "deadband_updated_at":null, - "units":"f", - "desired_fan_mode_updated_at":1461454387.768337, - "deadband":null, - "max_set_point":21.11111111111111, - "powered":false, - "max_max_set_point":null, - "mode_updated_at":1475953458.4724526, - "powered_updated_at":1475953458.4724526, - "desired_max_set_point_updated_at":1455335566.047624, - "max_min_set_point":null, - "desired_mode_updated_at":1461896712.6109614, - "fan_mode_updated_at":1475953458.4724526, - "min_max_set_point":null, - "modes_allowed":[ - "heat_only", - "cool_only", - "auto", - "aux" - ], - "max_min_set_point_updated_at":null, - "mode":"heat_only", - "units_updated_at":1475953458.4724526, - "max_max_set_point_updated_at":null, - "min_max_set_point_updated_at":null, - "min_set_point":18.333333333333332, - "temperature_updated_at":1475953458.4724526, - "external_temperature":null, - "temperature":23.88888888888889, - "min_set_point_updated_at":1475953458.4724526, - "max_set_point_updated_at":1475953458.4724526 - }, - "upc_code":"generic_zwave_thermostat", - "uuid":"9d25c28e-bb1f-4ae1-b2af-036494f4796b", - "radio_type":"zwave", - "desired_state":{ - "units":"f", - "powered":false, - "fan_mode":"auto", - "max_set_point":21.11111111111111, - "min_set_point":18.333333333333332, - "mode":"heat_only" - } - } - ], - "errors":[ - - ], - "pagination":{ - "count":13 - } -} diff --git a/src/pywink/test/devices/standard/api_responses/hue_and_saturation_absent.json b/src/pywink/test/devices/standard/api_responses/hue_and_saturation_absent.json deleted file mode 100644 index 4efd714..0000000 --- a/src/pywink/test/devices/standard/api_responses/hue_and_saturation_absent.json +++ /dev/null @@ -1,118 +0,0 @@ -{ - "data": [ - { - "uuid": "238539e2-1ad6-44ba-bc53-33c684c36e1d", - "desired_state": { - "powered": false, - "brightness": 1, - "color_model": "hsb", - "hue": 0.35, - "saturation": 1, - "color_temperature": 1901 - }, - "last_reading": { - "connection": true, - "connection_updated_at": 1458628651.862995, - "powered": false, - "powered_updated_at": 1458628651.862995, - "brightness": 1, - "brightness_updated_at": 1458628651.862995, - "color_model": "hsb", - "color_model_updated_at": 1458628651.862995, - "hue": 0.35, - "hue_updated_at": 1458628651.862995, - "saturation": 1, - "saturation_updated_at": 1458628651.862995, - "color_temperature": 1901, - "color_temperature_updated_at": 1458628651.862995, - "firmware_version": "0.1b02 / 0.3b22", - "firmware_version_updated_at": 1458628651.862995, - "firmware_date_code": "20150929N****", - "firmware_date_code_updated_at": 1458628651.862995, - "desired_powered_updated_at": 1458628650.8619466, - "desired_brightness_updated_at": 1458628820.0301423, - "desired_color_model_updated_at": 1458628820.0301423, - "desired_hue_updated_at": 1458628820.0301423, - "desired_saturation_updated_at": 1458628820.0301423, - "desired_color_temperature_updated_at": 1458628820.0301423, - "powered_changed_at": 1458628650.8134031, - "brightness_changed_at": 1458122238.7788615, - "connection_changed_at": 1457517588.4372394, - "desired_powered_changed_at": 1458628650.8619466, - "desired_brightness_changed_at": 1458628381.8566465, - "firmware_date_code_changed_at": 1457521561.0603704, - "color_model_changed_at": 1457521797.6389458, - "hue_changed_at": 1457595786.5472758, - "saturation_changed_at": 1457595782.71269, - "color_temperature_changed_at": 1457521786.911106, - "firmware_version_changed_at": 1457521561.0603704, - "desired_color_model_changed_at": 1458620834.569744, - "desired_hue_changed_at": 1457595786.605935, - "desired_saturation_changed_at": 1457595782.8273423, - "desired_color_temperature_changed_at": 1457521921.4747667 - }, - "light_bulb_id": "1515274", - "name": "Bat Signal", - "locale": "en_us", - "units": { - }, - "created_at": 1457517586, - "hidden_at": null, - "capabilities": { - "fields": [ - { - "field": "connection", - "type": "boolean", - "mutability": "read-only" - }, - { - "field": "powered", - "type": "boolean", - "mutability": "read-write" - }, - { - "field": "brightness", - "type": "percentage", - "mutability": "read-write" - }, - { - "field": "color_model", - "type": "string", - "choices": [ - "rgb", - "color_temperature" - ] - }, - { - "field": "color_temperature", - "range": [ - 1900, - 6500 - ], - "type": "integer", - "mutability": "read-write" - } - ], - "color_changeable": true - }, - "triggers": [], - "manufacturer_device_model": "sylvania_sylvania_rgbw", - "manufacturer_device_id": null, - "device_manufacturer": "sylvania", - "model_name": "Lightify RGBW Bulb", - "upc_id": "509", - "upc_code": "4613573703", - "gang_id": null, - "hub_id": "381678", - "local_id": "37", - "radio_type": "zigbee", - "linked_service_id": null, - "lat_lng": [ - null, - null - ], - "location": "", - "order": 0 - } - ] -} diff --git a/src/pywink/test/devices/standard/api_responses/hue_and_saturation_present.json b/src/pywink/test/devices/standard/api_responses/hue_and_saturation_present.json deleted file mode 100644 index ae4e04b..0000000 --- a/src/pywink/test/devices/standard/api_responses/hue_and_saturation_present.json +++ /dev/null @@ -1,129 +0,0 @@ -{ - "data": [ - { - "uuid": "238539e2-1ad6-44ba-bc53-33c684c36e1d", - "desired_state": { - "powered": false, - "brightness": 1, - "color_model": "hsb", - "hue": 0.35, - "saturation": 1, - "color_temperature": 1901 - }, - "last_reading": { - "connection": true, - "connection_updated_at": 1458628651.862995, - "powered": false, - "powered_updated_at": 1458628651.862995, - "brightness": 1, - "brightness_updated_at": 1458628651.862995, - "color_model": "hsb", - "color_model_updated_at": 1458628651.862995, - "hue": 0.35, - "hue_updated_at": 1458628651.862995, - "saturation": 1, - "saturation_updated_at": 1458628651.862995, - "color_temperature": 1901, - "color_temperature_updated_at": 1458628651.862995, - "firmware_version": "0.1b02 / 0.3b22", - "firmware_version_updated_at": 1458628651.862995, - "firmware_date_code": "20150929N****", - "firmware_date_code_updated_at": 1458628651.862995, - "desired_powered_updated_at": 1458628650.8619466, - "desired_brightness_updated_at": 1458628820.0301423, - "desired_color_model_updated_at": 1458628820.0301423, - "desired_hue_updated_at": 1458628820.0301423, - "desired_saturation_updated_at": 1458628820.0301423, - "desired_color_temperature_updated_at": 1458628820.0301423, - "powered_changed_at": 1458628650.8134031, - "brightness_changed_at": 1458122238.7788615, - "connection_changed_at": 1457517588.4372394, - "desired_powered_changed_at": 1458628650.8619466, - "desired_brightness_changed_at": 1458628381.8566465, - "firmware_date_code_changed_at": 1457521561.0603704, - "color_model_changed_at": 1457521797.6389458, - "hue_changed_at": 1457595786.5472758, - "saturation_changed_at": 1457595782.71269, - "color_temperature_changed_at": 1457521786.911106, - "firmware_version_changed_at": 1457521561.0603704, - "desired_color_model_changed_at": 1458620834.569744, - "desired_hue_changed_at": 1457595786.605935, - "desired_saturation_changed_at": 1457595782.8273423, - "desired_color_temperature_changed_at": 1457521921.4747667 - }, - "light_bulb_id": "1515274", - "name": "Bat Signal", - "locale": "en_us", - "units": { - }, - "created_at": 1457517586, - "hidden_at": null, - "capabilities": { - "fields": [ - { - "field": "connection", - "type": "boolean", - "mutability": "read-only" - }, - { - "field": "powered", - "type": "boolean", - "mutability": "read-write" - }, - { - "field": "brightness", - "type": "percentage", - "mutability": "read-write" - }, - { - "field": "color_model", - "type": "string", - "choices": [ - "rgb", - "hsb", - "color_temperature" - ] - }, - { - "field": "hue", - "type": "percentage", - "mutability": "read-write" - }, - { - "field": "saturation", - "type": "percentage", - "mutability": "read-write" - }, - { - "field": "color_temperature", - "range": [ - 1900, - 6500 - ], - "type": "integer", - "mutability": "read-write" - } - ], - "color_changeable": true - }, - "triggers": [], - "manufacturer_device_model": "sylvania_sylvania_rgbw", - "manufacturer_device_id": null, - "device_manufacturer": "sylvania", - "model_name": "Lightify RGBW Bulb", - "upc_id": "509", - "upc_code": "4613573703", - "gang_id": null, - "hub_id": "381678", - "local_id": "37", - "radio_type": "zigbee", - "linked_service_id": null, - "lat_lng": [ - null, - null - ], - "location": "", - "order": 0 - } - ] -} diff --git a/src/pywink/test/devices/standard/api_responses/key.json b/src/pywink/test/devices/standard/api_responses/key.json deleted file mode 100644 index e389191..0000000 --- a/src/pywink/test/devices/standard/api_responses/key.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "data":{ - "name":"Key 1", - "last_reading":{ - "slot_id":3, - "slot_id_updated_at":1449960280.1004233, - "activity_detected":true, - "activity_detected_updated_at":1466540653.7276487, - "activity_detected_changed_at":1466540653.7276487 - }, - "key_id":"257123", - "icon_id":null, - "verified_at":null, - "uuid":"b6d02600-3aed-425c-b6f7-77a4701ce9ea", - "parent_object_type":"lock", - "parent_object_id":"61123", - "desired_state":{ - "code":null - }, - "subscription":{ - "pubnub":{ - "subscribe_key":"sub-c-f7bf7f7e-0542-11e3-a5e8-02e1234567fe", - "channel":"d7f700cb00b859eb33bd2fe8344136bc91234560" - } - } - }, - "errors":[ - - ], - "pagination":{ - - } -} diff --git a/src/pywink/test/devices/standard/api_responses/light_bulb.json b/src/pywink/test/devices/standard/api_responses/light_bulb.json deleted file mode 100644 index 460c2a5..0000000 --- a/src/pywink/test/devices/standard/api_responses/light_bulb.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "data": [{ - "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 - }] -} \ No newline at end of file diff --git a/src/pywink/test/devices/standard/api_responses/light_bulb_with_desired_state_reached.json b/src/pywink/test/devices/standard/api_responses/light_bulb_with_desired_state_reached.json deleted file mode 100644 index fac6e4a..0000000 --- a/src/pywink/test/devices/standard/api_responses/light_bulb_with_desired_state_reached.json +++ /dev/null @@ -1,128 +0,0 @@ -{ - "pagination": {}, - "errors": [], - "data": { - "location": "", - "device_manufacturer": "eastfield", - "hidden_at": null, - "upc_id": "309", - "created_at": 1459823491, - "gang_id": null, - "light_bulb_id": "1591581", - "local_id": "5", - "manufacturer_device_id": null, - "capabilities": { - "fields": [ - { - "type": "boolean", - "field": "connection", - "mutability": "read-only" - }, - { - "type": "boolean", - "field": "powered", - "mutability": "read-write" - }, - { - "type": "percentage", - "field": "brightness", - "mutability": "read-write" - }, - { - "type": "string", - "choices": [ - "rgb", - "hsb", - "color_temperature" - ], - "field": "color_model" - }, - { - "type": "percentage", - "field": "hue", - "mutability": "read-write" - }, - { - "type": "percentage", - "field": "saturation", - "mutability": "read-write" - }, - { - "type": "integer", - "field": "color_temperature", - "mutability": "read-write", - "range": [ - 2700, - 6500 - ] - } - ], - "color_changeable": true - }, - "linked_service_id": null, - "triggers": [], - "lat_lng": [ - 52.113447, - -106.61415 - ], - "desired_state": { - "powered": true, - "color_temperature": 5000, - "hue": 0.0, - "saturation": 0.0, - "color_model": "hsb", - "brightness": 1.0 - }, - "name": "Brad's Room 2", - "hub_id": "300039", - "manufacturer_device_model": "eastfield_light_bulb_rgbw", - "uuid": "77773adb-93ba-40d9-9e6c-6ff562c810b5", - "upc_code": "eastfield_ecosmart_rgbw", - "locale": "en_us", - "last_reading": { - "firmware_version_updated_at": 1461722879.9097676, - "desired_color_temperature_updated_at": 1461694696.0741637, - "desired_color_model_changed_at": 1461694696.0741637, - "desired_saturation_updated_at": 1461694696.0741637, - "desired_brightness_updated_at": 1461694696.0741637, - "firmware_date_code_changed_at": 1459823493.5070922, - "desired_powered_changed_at": 1461694696.0741637, - "desired_brightness_changed_at": 1461694696.0741637, - "brightness": 1.0, - "color_model_changed_at": 1461641413.3958035, - "connection": true, - "brightness_changed_at": 1461722879.9097676, - "hue": 0.0, - "color_model_updated_at": 1461722879.9097676, - "desired_saturation_changed_at": 1461694696.0741637, - "color_model": "hsb", - "connection_changed_at": 1461722278.9044788, - "saturation": 0.0, - "desired_color_model_updated_at": 1461694696.0741637, - "desired_powered_updated_at": 1461694696.0741637, - "saturation_changed_at": 1461722279.1130762, - "powered": true, - "color_temperature": 5000, - "hue_updated_at": 1461722879.9097676, - "color_temperature_changed_at": 1461677499.2378418, - "connection_updated_at": 1461722879.9097676, - "firmware_version_changed_at": 1459823856.2130384, - "firmware_date_code_updated_at": 1461722879.9097676, - "powered_changed_at": 1461670354.5106528, - "color_temperature_updated_at": 1461722879.9097676, - "powered_updated_at": 1461722879.9097676, - "desired_hue_updated_at": 1461694696.0741637, - "desired_hue_changed_at": 1461694696.0741637, - "brightness_updated_at": 1461722879.9097676, - "desired_color_temperature_changed_at": 1461677683.7542028, - "firmware_version": "0.2b10 / 0.2b15", - "saturation_updated_at": 1461722879.9097676, - "hue_changed_at": 1461722278.9044788, - "firmware_date_code": "" - }, - "model_name": "EcoSmart Light RGBW Bulb", - "radio_type": "zigbee", - "order": 0, - "units": {} - } -} diff --git a/src/pywink/test/devices/standard/api_responses/light_switch_ge_jasco_z_wave.json b/src/pywink/test/devices/standard/api_responses/light_switch_ge_jasco_z_wave.json deleted file mode 100644 index 1fd4921..0000000 --- a/src/pywink/test/devices/standard/api_responses/light_switch_ge_jasco_z_wave.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "data": [ - { - "object_type": "binary_switch", - "object_id": "216721", - "uuid": "REMOVED", - "icon_id": "52", - "icon_code": "binary_switch-light_bulb_dumb", - "desired_state": {}, - "last_reading": { - "connection": true, - "connection_updated_at": 1465261453.03461, - "powered": true, - "powered_updated_at": 1465261453.03461, - "desired_powered_updated_at": 1465107431.1711504, - "connection_changed_at": 1465259877.993167, - "powered_changed_at": 1465261453.03461, - "desired_powered_changed_at": 1465107431.1711504 - }, - "subscription": { - "pubnub": { - "subscribe_key": "REMOVED", - "channel": "REMOVED" - } - }, - "binary_switch_id": "216721", - "name": "Hallway Switch", - "locale": "en_us", - "units": {}, - "created_at": 1464309200, - "hidden_at": null, - "capabilities": { - "fields": [ - { - "type": "boolean", - "field": "connection", - "mutability": "read-only" - }, - { - "type": "boolean", - "field": "powered", - "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": "300039", - "local_id": "12", - "radio_type": "zwave", - "linked_service_id": null, - "current_budget": null, - "lat_lng": [ - 0, - 0 - ], - "location": "", - "order": 0 - } - ], - "errors": [], - "pagination": { - "count": 1 - } -} diff --git a/src/pywink/test/devices/standard/api_responses/liquid_sensor.json b/src/pywink/test/devices/standard/api_responses/liquid_sensor.json deleted file mode 100644 index 64a147d..0000000 --- a/src/pywink/test/devices/standard/api_responses/liquid_sensor.json +++ /dev/null @@ -1,83 +0,0 @@ -{ - "data":[ - { - "last_event":{ - "brightness_occurred_at":null, - "loudness_occurred_at":null, - "vibration_occurred_at":null - }, - "uuid":"0e614bf2-ef85-4bc9-90d8-8f07418a7123", - "desired_state":{ - - }, - "last_reading":{ - "liquid_detected":false, - "liquid_detected_updated_at":1468619519.7377036, - "battery":1, - "battery_updated_at":1468619519.7377036, - "liquid_detected_true":null, - "liquid_detected_true_updated_at":null, - "connection":true, - "connection_updated_at":1468619519.7377036, - "agent_session_id":null, - "agent_session_id_updated_at":null, - "connection_changed_at":1468619516.3395233, - "liquid_detected_changed_at":1468619516.8365848, - "battery_changed_at":1468619516.8365848 - }, - "subscription":{ - "pubnub":{ - "subscribe_key":"sub-c-f7b1237e-0542-1233-a5e8-02ee354b7fe", - "channel":"5d54ad5517ccc1c05695704a5c1b8c3234e2342e|sensor_pod-240456|user-377857" - } - }, - "sensor_pod_id":"241253", - "name":"Water", - "locale":"en_us", - "units":{ - - }, - "created_at":1468619516, - "hidden_at":null, - "capabilities":{ - "fields":[ - { - "type":"boolean", - "field":"liquid_detected", - "mutability":"read-only" - }, - { - "type":"percentage", - "field":"battery", - "mutability":"read-only" - } - ] - }, - "triggers":[ - - ], - "manufacturer_device_model":"aeon_labs_dsb45_zwus", - "manufacturer_device_id":null, - "device_manufacturer":"aeon_labs", - "model_name":"Z-Wave Water Sensor Sensor", - "upc_id":"339", - "upc_code":"generic_water_sensor", - "gang_id":null, - "hub_id":"312328", - "local_id":"23", - "radio_type":"zwave", - "linked_service_id":null, - "lat_lng":[ - "12.34567", - "-89.76543" - ], - "location":"" - } - ], - "errors":[ - - ], - "pagination":{ - - } -} diff --git a/src/pywink/test/devices/standard/api_responses/lock.json b/src/pywink/test/devices/standard/api_responses/lock.json deleted file mode 100644 index 54b733d..0000000 --- a/src/pywink/test/devices/standard/api_responses/lock.json +++ /dev/null @@ -1,136 +0,0 @@ -{ - "data": [ - { - "desired_state": { - "locked": true, - "beeper_enabled": true, - "vacation_mode_enabled": false, - "auto_lock_enabled": false, - "key_code_length": 4, - "alarm_mode": null, - "alarm_sensitivity": 0.6, - "alarm_enabled": false - }, - "last_reading": { - "locked": true, - "locked_updated_at": 1417823487.490747, - "connection": true, - "connection_updated_at": 1417823487.490747, - "battery": 0.83, - "battery_updated_at": 1417823487.490747, - "alarm_activated": null, - "alarm_activated_updated_at": null, - "beeper_enabled": true, - "beeper_enabled_updated_at": 1417823487.490747, - "vacation_mode_enabled": false, - "vacation_mode_enabled_updated_at": 1417823487.490747, - "auto_lock_enabled": false, - "auto_lock_enabled_updated_at": 1417823487.490747, - "key_code_length": 4, - "key_code_length_updated_at": 1417823487.490747, - "alarm_mode": null, - "alarm_mode_updated_at": 1417823487.490747, - "alarm_sensitivity": 0.6, - "alarm_sensitivity_updated_at": 1417823487.490747, - "alarm_enabled": true, - "alarm_enabled_updated_at": 1417823487.490747, - "last_error": null, - "last_error_updated_at": 1417823487.490747, - "desired_locked_updated_at": 1417823487.490747, - "desired_beeper_enabled_updated_at": 1417823487.490747, - "desired_vacation_mode_enabled_updated_at": 1417823487.490747, - "desired_auto_lock_enabled_updated_at": 1417823487.490747, - "desired_key_code_length_updated_at": 1417823487.490747, - "desired_alarm_mode_updated_at": 1417823487.490747, - "desired_alarm_sensitivity_updated_at": 1417823487.490747, - "desired_alarm_enabled_updated_at": 1417823487.490747, - "locked_changed_at": 1417823487.490747, - "battery_changed_at": 1417823487.490747, - "desired_locked_changed_at": 1417823487.490747, - "desired_beeper_enabled_changed_at": 1417823487.490747, - "desired_vacation_mode_enabled_changed_at": 1417823487.490747, - "desired_auto_lock_enabled_changed_at": 1417823487.490747, - "desired_key_code_length_changed_at": 1417823487.490747, - "desired_alarm_mode_changed_at": 1417823487.490747, - "desired_alarm_sensitivity_changed_at": 1417823487.490747, - "desired_alarm_enabled_changed_at": 1417823487.490747, - "last_error_changed_at": 1417823487.490747 - }, - "lock_id": "5304", - "name": "Main", - "locale": "en_us", - "units": {}, - "created_at": 1417823382, - "hidden_at": null, - "capabilities": { - "fields": [ - { - "field": "locked", - "type": "boolean", - "mutability": "read-write" - }, - { - "field": "connection", - "mutability": "read-only", - "type": "boolean" - }, - { - "field": "battery", - "mutability": "read-only", - "type": "percentage" - }, - { - "field": "alarm_activated", - "mutability": "read-only", - "type": "boolean" - }, - { - "field": "beeper_enabled", - "type": "boolean" - }, - { - "field": "vacation_mode_enabled", - "type": "boolean" - }, - { - "field": "auto_lock_enabled", - "type": "boolean" - }, - { - "field": "key_code_length", - "type": "integer" - }, - { - "field": "alarm_mode", - "type": "string" - }, - { - "field": "alarm_sensitivity", - "type": "percentage" - }, - { - "field": "alarm_enabled", - "type": "boolean" - } - ], - "home_security_device": true - }, - "triggers": [], - "manufacturer_device_model": "schlage_zwave_lock", - "manufacturer_device_id": null, - "device_manufacturer": "schlage", - "model_name": "BE469", - "upc_id": "11", - "upc_code": "043156312214", - "hub_id": "11780", - "local_id": "1", - "radio_type": "zwave", - "lat_lng": [38.429962, -122.653715], - "location": "" - } - ], - "errors": [], - "pagination": { - "count": 1 - } -} \ No newline at end of file diff --git a/src/pywink/test/devices/standard/api_responses/motion_sensor_gocontrol.json b/src/pywink/test/devices/standard/api_responses/motion_sensor_gocontrol.json deleted file mode 100644 index 6824d5f..0000000 --- a/src/pywink/test/devices/standard/api_responses/motion_sensor_gocontrol.json +++ /dev/null @@ -1,99 +0,0 @@ -{ - "data":[ - { - "last_event":{ - "brightness_occurred_at":null, - "loudness_occurred_at":null, - "vibration_occurred_at":null - }, - "uuid":"4c93936f-840e-40fc1232184-ee3583408d71", - "desired_state":{ - - }, - "last_reading":{ - "motion":false, - "motion_updated_at":1473870334.9673228, - "battery":1, - "battery_updated_at":1473870334.9673228, - "tamper_detected":null, - "tamper_detected_updated_at":null, - "temperature":21.666666666666668, - "temperature_updated_at":1473870334.9673228, - "motion_true":"N/A", - "motion_true_updated_at":1473870129.6595004, - "tamper_detected_true":null, - "tamper_detected_true_updated_at":null, - "connection":true, - "connection_updated_at":1473870334.9673228, - "agent_session_id":null, - "agent_session_id_updated_at":null, - "motion_changed_at":1473870334.9673228, - "motion_true_changed_at":1473870129.6595004, - "temperature_changed_at":1473862426.2809358 - }, - "subscription":{ - "pubnub":{ - "subscribe_key":"sub-c-f7-11e3-a5e8-02ee2ddab7fe", - "channel":"b60201ecf033b6e7181|sensor_pod-152619|user-377857" - } - }, - "sensor_pod_id":"151234", - "name":"Living room", - "locale":"en_us", - "units":{ - - }, - "created_at":1452810755, - "hidden_at":null, - "capabilities":{ - "fields":[ - { - "type":"boolean", - "field":"motion", - "mutability":"read-only" - }, - { - "type":"percentage", - "field":"battery", - "mutability":"read-only" - }, - { - "type":"boolean", - "field":"tamper_detected", - "mutability":"read-only" - }, - { - "type":"float", - "field":"temperature", - "mutability":"read-only" - } - ] - }, - "triggers":[ - - ], - "manufacturer_device_model":"linear_wapirz_1", - "manufacturer_device_id":null, - "device_manufacturer":"linear", - "model_name":"Z-Wave Passive Infrared (PIR) Sensor", - "upc_id":"207", - "upc_code":"093863125102", - "gang_id":null, - "hub_id":"302528", - "local_id":"6", - "radio_type":"zwave", - "linked_service_id":null, - "lat_lng":[ - 12.345678, - -89.765432 - ], - "location":"" - } - ], - "errors":[ - - ], - "pagination":{ - "count":13 - } -} diff --git a/src/pywink/test/devices/standard/api_responses/nest.json b/src/pywink/test/devices/standard/api_responses/nest.json deleted file mode 100644 index 756f024..0000000 --- a/src/pywink/test/devices/standard/api_responses/nest.json +++ /dev/null @@ -1,241 +0,0 @@ -{ - "data":[ - { - "uuid":"98e6366f-1d86-417d-8610-6a81e1234567G", - "desired_state":{ - "max_set_point":23, - "min_set_point":20.5, - "powered":true, - "users_away":true, - "fan_timer_active":false, - "mode":"auto", - "short_name":"Hallway" - }, - "last_reading":{ - "max_set_point":23, - "max_set_point_updated_at":1477509791.279, - "min_set_point":20.5, - "min_set_point_updated_at":1477509791.279, - "powered":true, - "powered_updated_at":1477509791.279, - "users_away":true, - "users_away_updated_at":1477509791.279, - "fan_timer_active":false, - "fan_timer_active_updated_at":1477509791.279, - "units":"c", - "units_updated_at":1477509791.279, - "temperature":21, - "temperature_updated_at":1477509791.279, - "external_temperature":null, - "external_temperature_updated_at":null, - "min_min_set_point":null, - "min_min_set_point_updated_at":null, - "max_min_set_point":null, - "max_min_set_point_updated_at":null, - "min_max_set_point":null, - "min_max_set_point_updated_at":null, - "max_max_set_point":null, - "max_max_set_point_updated_at":null, - "deadband":1.5, - "deadband_updated_at":1477509791.279, - "eco_target":true, - "eco_target_updated_at":1477509791.279, - "manufacturer_structure_id":"VR4O6oe42tRtUfVtUqrSV3MqLdRNn93khAIu3iy3vxM0nUKe1234456", - "manufacturer_structure_id_updated_at":1477509791.279, - "has_fan":true, - "has_fan_updated_at":1477509791.279, - "fan_duration":0, - "fan_duration_updated_at":1477509791.279, - "last_error":null, - "last_error_updated_at":1477508548.9048016, - "connection":true, - "connection_updated_at":1477509791.279, - "mode":"auto", - "mode_updated_at":1477509791.279, - "short_name":"Hallway", - "short_name_updated_at":1477509791.279, - "modes_allowed":[ - "auto", - "heat_only", - "cool_only" - ], - "modes_allowed_updated_at":1477509791.279, - "desired_max_set_point_updated_at":1477509906.2907016, - "desired_min_set_point_updated_at":1477509906.2907016, - "desired_powered_updated_at":1477509907.2537994, - "desired_users_away_updated_at":1477509779.2279897, - "desired_fan_timer_active_updated_at":1477509906.2907016, - "desired_mode_updated_at":1477509907.2537994, - "desired_short_name_updated_at":1477509906.2907016, - "mode_changed_at":1477509769.941, - "max_set_point_changed_at":1477449273.651, - "min_set_point_changed_at":1477502492.1976361, - "temperature_changed_at":1477499242.523, - "fan_timer_active_changed_at":1477449301.854, - "users_away_changed_at":1477509778.566, - "eco_target_changed_at":1477509791.279, - "desired_users_away_changed_at":1477509779.2279897, - "desired_powered_changed_at":1477509770.4954133, - "powered_changed_at":1477449121.484, - "last_error_changed_at":1477508548.9048016, - "desired_fan_timer_active_changed_at":1477426055.5319712, - "desired_mode_changed_at":1477509770.4954133, - "desired_min_set_point_changed_at":1477503213.0961704, - "desired_max_set_point_changed_at":1477503092.5332086 - }, - "subscription":{ - "pubnub":{ - "subscribe_key":"sub-c-f7bf7f7e-0542-11e312345-02ee2ddab7fe", - "channel":"0b4d044513a22ab9ae821071234455a|thermostat-12349|user-312345" - } - }, - "thermostat_id":"96559", - "name":"Home Hallway Thermostat", - "locale":"en_us", - "units":{ - "temperature":"c" - }, - "created_at":1449443871, - "hidden_at":null, - "capabilities":{ - "fields":[ - { - "type":"float", - "field":"max_set_point", - "mutability":"read-write", - "clear_desired_state_tolerance":0.5 - }, - { - "type":"float", - "field":"min_set_point", - "mutability":"read-write", - "clear_desired_state_tolerance":0.5 - }, - { - "type":"boolean", - "field":"powered", - "mutability":"read-write" - }, - { - "type":"boolean", - "field":"users_away", - "mutability":"read-write" - }, - { - "type":"boolean", - "field":"fan_timer_active", - "mutability":"read-write" - }, - { - "type":"nested_hash", - "field":"units", - "mutability":"read-only" - }, - { - "type":"float", - "field":"temperature", - "mutability":"read-only" - }, - { - "type":"float", - "field":"external_temperature", - "mutability":"read-only" - }, - { - "type":"float", - "field":"min_min_set_point", - "mutability":"read-only" - }, - { - "type":"float", - "field":"max_min_set_point", - "mutability":"read-only" - }, - { - "type":"float", - "field":"min_max_set_point", - "mutability":"read-only" - }, - { - "type":"float", - "field":"max_max_set_point", - "mutability":"read-only" - }, - { - "type":"float", - "field":"deadband", - "mutability":"read-only" - }, - { - "type":"boolean", - "field":"eco_target", - "mutability":"read-only" - }, - { - "type":"string", - "field":"manufacturer_structure_id", - "mutability":"read-only" - }, - { - "type":"boolean", - "field":"has_fan", - "mutability":"read-only" - }, - { - "type":"integer", - "field":"fan_duration", - "mutability":"read-only" - }, - { - "type":"string", - "field":"last_error", - "mutability":"read-only" - }, - { - "type":"boolean", - "field":"connection", - "mutability":"read-only" - }, - { - "field":"mode", - "type":"selection", - "mutability":"read-write", - "choices":[ - "auto", - "heat_only", - "cool_only" - ] - } - ], - "notification_robots":[ - "aux_active_notification" - ] - }, - "triggers":[ - - ], - "manufacturer_device_model":"nest_thermostat", - "manufacturer_device_id":"51qQw2vE1El36wrj5PoRkUxx11Q12345", - "device_manufacturer":"nest", - "model_name":"Learning Thermostat", - "upc_id":"557", - "upc_code":"nest_thermostat", - "hub_id":null, - "local_id":null, - "radio_type":null, - "linked_service_id":"241420", - "lat_lng":[ - 12.345678, - -98.765432 - ], - "location":"", - "smart_schedule_enabled":false - } - ], - "errors":[ - - ], - "pagination":{ - "count":13 - } -} diff --git a/src/pywink/test/devices/standard/api_responses/porkfolio.json b/src/pywink/test/devices/standard/api_responses/porkfolio.json deleted file mode 100644 index fcf7d25..0000000 --- a/src/pywink/test/devices/standard/api_responses/porkfolio.json +++ /dev/null @@ -1,94 +0,0 @@ -{ - "data":[ - { - "balance":0, - "nose_color":"2400FF", - "last_deposit_amount":-345, - "savings_goal":5000, - "orientation":false, - "vibration":false, - "uuid":"09e5977d-838f-1234-9eca-d5b715008dac", - "desired_state":{ - "nose_color":"2400FF" - }, - "last_reading":{ - "connection":true, - "connection_updated_at":1467473921.827009, - "amount":-345, - "amount_updated_at":1467475089.9699109, - "battery":null, - "battery_updated_at":null, - "balance":0, - "balance_updated_at":1467475089.9699109, - "orientation":null, - "orientation_updated_at":null, - "units_updated_at":1467473921.6321552, - "vibration":null, - "vibration_updated_at":null, - "units_changed_at":1467473921.6321552, - "connection_changed_at":1467473921.827009, - "amount_changed_at":1467475089.9699109, - "balance_changed_at":1467475089.9699109 - }, - "subscription":{ - "pubnub":{ - "subscribe_key":"sub-c-f7bf23234f7e-0542-11e3-a5e8-02ee2dd1241241241", - "channel":"0441c5de80107a19b8fa9dac6c90505a1f3dd05f|piggy_bank-15215|user-377123" - } - }, - "piggy_bank_id":"15266", - "name":"Porkfolio", - "locale":"en_us", - "units":{ - "currency":"USD" - }, - "created_at":1467473921, - "hidden_at":null, - "capabilities":{ - "needs_wifi_network_list":true - }, - "triggers":[ - { - "trigger_id":"223812", - "name":"Porkfolio vibration", - "enabled":false, - "trigger_configuration":{ - "reading_type":"vibration", - "edge":"rising", - "threshold":1, - "object_id":"15243", - "object_type":"piggy_bank" - }, - "channel_configuration":{ - "recipient_user_ids":[ - "*" - ], - "channel_id":"15", - "object_type":null, - "object_id":null - }, - "robot_id":"3389366", - "triggered_at":null, - "piggy_bank_alert_id":"223838" - } - ], - "device_manufacturer":"quirky", - "model_name":"Porkfolio", - "upc_id":"526", - "upc_code":"quirky_porkfolio", - "lat_lng":[ - 12.345678, - -89.765432 - ], - "location":"", - "mac_address":"0ccccc02de1b", - "serial":"ACA123427412" - } - ], - "errors":[ - - ], - "pagination":{ - - } -} diff --git a/src/pywink/test/devices/standard/api_responses/power_strip.json b/src/pywink/test/devices/standard/api_responses/power_strip.json deleted file mode 100644 index 463ba26..0000000 --- a/src/pywink/test/devices/standard/api_responses/power_strip.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "data": [ - { - "desired_state": {}, - "last_reading": { - "connection": false, - "connection_updated_at": 1452306146.129263, - "connection_changed_at": 1452306144.425378 - }, - "powerstrip_id": "24123", - "name": "Power strip", - "locale": "en_us", - "units": {}, - "created_at": 1451578768, - "hidden_at": null, - "capabilities": {}, - "triggers": [], - "device_manufacturer": "quirky_ge", - "model_name": "Pivot Power Genius", - "upc_id": "24", - "upc_code": "814434017226", - "lat_lng": [ - 12.123456, - -98.765432 - ], - "location": "", - "mac_address": "0c2a69123456", - "serial": "AAAA00123456", - "outlets": [ - { - "powered": false, - "scheduled_outlet_states": [], - "name": "First", - "outlet_index": 0, - "outlet_id": "48123", - "icon_id": "4", - "parent_object_type": "powerstrip", - "parent_object_id": "24123", - "desired_state": { - "powered": false - }, - "last_reading": { - "powered": true, - "powered_updated_at": 1452306146.0882413, - "powered_changed_at": 1452306004.7519948, - "desired_powered_updated_at": 1452306008.2215497 - } - }, - { - "powered": false, - "scheduled_outlet_states": [], - "name": "Second", - "outlet_index": 1, - "outlet_id": "48124", - "icon_id": "4", - "parent_object_type": "powerstrip", - "parent_object_id": "24123", - "desired_state": { - "powered": false - }, - "last_reading": { - "powered": true, - "powered_updated_at": 1452311731.8861659, - "powered_changed_at": 1452311731.8861659, - "desired_powered_updated_at": 1452311885.3523679 - } - } - ] - } - ], - "errors": [], - "pagination": { - "count": 10 - } -} \ No newline at end of file diff --git a/src/pywink/test/devices/standard/api_responses/quirky_spotter.json b/src/pywink/test/devices/standard/api_responses/quirky_spotter.json deleted file mode 100644 index e9d4352..0000000 --- a/src/pywink/test/devices/standard/api_responses/quirky_spotter.json +++ /dev/null @@ -1,123 +0,0 @@ -{ - "data": [ - { - "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" - } - ], - "errors": [], - "pagination": { - "count": 24 - } -} diff --git a/src/pywink/test/devices/standard/api_responses/quirky_spotter_2.json b/src/pywink/test/devices/standard/api_responses/quirky_spotter_2.json deleted file mode 100644 index 55b52ae..0000000 --- a/src/pywink/test/devices/standard/api_responses/quirky_spotter_2.json +++ /dev/null @@ -1,116 +0,0 @@ -{ - "data": [ - { - "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": false, - "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" - } - ], - "errors": [], - "pagination": { - "count": 24 - } -} diff --git a/src/pywink/test/devices/standard/api_responses/quirky_spotter_pubnub.json b/src/pywink/test/devices/standard/api_responses/quirky_spotter_pubnub.json deleted file mode 100644 index dc57dd4..0000000 --- a/src/pywink/test/devices/standard/api_responses/quirky_spotter_pubnub.json +++ /dev/null @@ -1,134 +0,0 @@ -{ - "capabilities":{ - "fields":[ - { - "field":"battery", - "mutability":"read-only", - "type":"percentage" - }, - { - "field":"brightness", - "mutability":"read-only", - "type":"percentage" - }, - { - "field":"external_power", - "mutability":"read-only", - "type":"boolean" - }, - { - "field":"humidity", - "mutability":"read-only", - "type":"percentage" - }, - { - "field":"loudness", - "mutability":"read-only", - "type":"boolean" - }, - { - "field":"temperature", - "mutability":"read-only", - "type":"float" - }, - { - "field":"vibration", - "mutability":"read-only", - "type":"boolean" - } - ], - "needs_wifi_network_list":true - }, - "created_at":1453520234, - "desired_state":{ - - }, - "device_manufacturer":"quirky_ge", - "gang_id":null, - "hidden_at":null, - "hub_id":null, - "icon_code":null, - "icon_id":null, - "last_event":{ - "brightness_occurred_at":1463746054.0962152, - "loudness_occurred_at":1463751976.0731974, - "vibration_occurred_at":1463699867.6401784 - }, - "last_reading":{ - "agent_session_id":null, - "agent_session_id_updated_at":null, - "battery":1.0, - "battery_changed_at":1456972918.8235242, - "battery_updated_at":1463756286.1238914, - "brightness":0.0, - "brightness_changed_at":1463751865.5881312, - "brightness_true":"N/A", - "brightness_true_changed_at":1463746054.0962152, - "brightness_true_updated_at":null, - "brightness_updated_at":1463756286.1238914, - "connection":true, - "connection_changed_at":1453520234.8375406, - "connection_updated_at":1463756286.1238914, - "external_power":true, - "external_power_changed_at":1453520237.0879393, - "external_power_updated_at":1463756286.1238914, - "humidity":0.24, - "humidity_changed_at":1463744925.739977, - "humidity_updated_at":1463756286.1238914, - "loudness":false, - "loudness_changed_at":1463752033.569461, - "loudness_true":"N/A", - "loudness_true_changed_at":1463751976.0731974, - "loudness_true_updated_at":null, - "loudness_updated_at":1463756286.1238914, - "temperature":23.0, - "temperature_changed_at":1463756286.1238914, - "temperature_updated_at":1463756286.1238914, - "vibration":false, - "vibration_changed_at":1463699872.8421204, - "vibration_true":"N/A", - "vibration_true_changed_at":1463699867.6401784, - "vibration_true_updated_at":null, - "vibration_updated_at":1463756286.1238914 - }, - "lat_lng":[ - 12.345678, - -98.76543 - ], - "linked_service_id":null, - "local_id":null, - "locale":"en_us", - "location":"", - "mac_address":"0c2a69023e3b", - "manufacturer_device_id":null, - "manufacturer_device_model":"quirky_ge_spotter", - "model_name":"Spotter", - "name":"Spotter", - "object_id":"156123", - "object_type":"sensor_pod", - "radio_type":null, - "sensor_pod_id":"156123", - "sensor_threshold_events":[ - - ], - "serial":"ABAB00003314", - "subscription":{ - "pubnub":{ - "channel":"e7c46d25d265425356hsdf898a0d05bfc1762405|sensor_pod-156012|user-377857", - "subscribe_key":"sub-c-f7bf7f7e-1234-11e3-a5e8-123456" - } - }, - "triggers":[ - - ], - "units":{ - - }, - "upc_code":"quirky_ge_spotter", - "upc_id":"531", - "user_ids":[ - "377812" - ], - "uuid":"77ee9632-d1c6-4af7-aec3-474091234511", - "nonce":"156012_1463756285_539" -} diff --git a/src/pywink/test/devices/standard/api_responses/rgb_absent.json b/src/pywink/test/devices/standard/api_responses/rgb_absent.json deleted file mode 100644 index ec2f312..0000000 --- a/src/pywink/test/devices/standard/api_responses/rgb_absent.json +++ /dev/null @@ -1,119 +0,0 @@ -{ - "data": [ - { - "uuid": "238539e2-1ad6-44ba-bc53-33c684c36e1d", - "desired_state": { - "powered": false, - "brightness": 1, - "color_model": "hsb", - "hue": 0.35, - "saturation": 1, - "color_temperature": 1901 - }, - "last_reading": { - "connection": true, - "connection_updated_at": 1458628651.862995, - "powered": false, - "powered_updated_at": 1458628651.862995, - "brightness": 1, - "brightness_updated_at": 1458628651.862995, - "color_model": "hsb", - "color_model_updated_at": 1458628651.862995, - "hue": 0.35, - "hue_updated_at": 1458628651.862995, - "saturation": 1, - "saturation_updated_at": 1458628651.862995, - "color_temperature": 1901, - "color_temperature_updated_at": 1458628651.862995, - "firmware_version": "0.1b02 / 0.3b22", - "firmware_version_updated_at": 1458628651.862995, - "firmware_date_code": "20150929N****", - "firmware_date_code_updated_at": 1458628651.862995, - "desired_powered_updated_at": 1458628650.8619466, - "desired_brightness_updated_at": 1458628820.0301423, - "desired_color_model_updated_at": 1458628820.0301423, - "desired_hue_updated_at": 1458628820.0301423, - "desired_saturation_updated_at": 1458628820.0301423, - "desired_color_temperature_updated_at": 1458628820.0301423, - "powered_changed_at": 1458628650.8134031, - "brightness_changed_at": 1458122238.7788615, - "connection_changed_at": 1457517588.4372394, - "desired_powered_changed_at": 1458628650.8619466, - "desired_brightness_changed_at": 1458628381.8566465, - "firmware_date_code_changed_at": 1457521561.0603704, - "color_model_changed_at": 1457521797.6389458, - "hue_changed_at": 1457595786.5472758, - "saturation_changed_at": 1457595782.71269, - "color_temperature_changed_at": 1457521786.911106, - "firmware_version_changed_at": 1457521561.0603704, - "desired_color_model_changed_at": 1458620834.569744, - "desired_hue_changed_at": 1457595786.605935, - "desired_saturation_changed_at": 1457595782.8273423, - "desired_color_temperature_changed_at": 1457521921.4747667 - }, - "light_bulb_id": "1515274", - "name": "Bat Signal", - "locale": "en_us", - "units": { - }, - "created_at": 1457517586, - "hidden_at": null, - "capabilities": { - "fields": [ - { - "field": "connection", - "type": "boolean", - "mutability": "read-only" - }, - { - "field": "powered", - "type": "boolean", - "mutability": "read-write" - }, - { - "field": "brightness", - "type": "percentage", - "mutability": "read-write" - }, - { - "field": "color_model", - "type": "string", - "choices": [ - "temperature", - "hsb" - ] - }, - { - "field": "hue", - "type": "percentage", - "mutability": "read-write" - }, - { - "field": "saturation", - "type": "percentage", - "mutability": "read-write" - } - ], - "color_changeable": true - }, - "triggers": [], - "manufacturer_device_model": "sylvania_sylvania_rgbw", - "manufacturer_device_id": null, - "device_manufacturer": "sylvania", - "model_name": "Lightify RGBW Bulb", - "upc_id": "509", - "upc_code": "4613573703", - "gang_id": null, - "hub_id": "381678", - "local_id": "37", - "radio_type": "zigbee", - "linked_service_id": null, - "lat_lng": [ - null, - null - ], - "location": "", - "order": 0 - } - ] -} diff --git a/src/pywink/test/devices/standard/api_responses/rgb_present.json b/src/pywink/test/devices/standard/api_responses/rgb_present.json deleted file mode 100644 index cc5f4e9..0000000 --- a/src/pywink/test/devices/standard/api_responses/rgb_present.json +++ /dev/null @@ -1,119 +0,0 @@ -{ - "data": [ - { - "uuid": "238539e2-1ad6-44ba-bc53-33c684c36e1d", - "desired_state": { - "powered": false, - "brightness": 1, - "color_model": "hsb", - "hue": 0.35, - "saturation": 1, - "color_temperature": 1901 - }, - "last_reading": { - "connection": true, - "connection_updated_at": 1458628651.862995, - "powered": false, - "powered_updated_at": 1458628651.862995, - "brightness": 1, - "brightness_updated_at": 1458628651.862995, - "color_model": "hsb", - "color_model_updated_at": 1458628651.862995, - "hue": 0.35, - "hue_updated_at": 1458628651.862995, - "saturation": 1, - "saturation_updated_at": 1458628651.862995, - "color_temperature": 1901, - "color_temperature_updated_at": 1458628651.862995, - "firmware_version": "0.1b02 / 0.3b22", - "firmware_version_updated_at": 1458628651.862995, - "firmware_date_code": "20150929N****", - "firmware_date_code_updated_at": 1458628651.862995, - "desired_powered_updated_at": 1458628650.8619466, - "desired_brightness_updated_at": 1458628820.0301423, - "desired_color_model_updated_at": 1458628820.0301423, - "desired_hue_updated_at": 1458628820.0301423, - "desired_saturation_updated_at": 1458628820.0301423, - "desired_color_temperature_updated_at": 1458628820.0301423, - "powered_changed_at": 1458628650.8134031, - "brightness_changed_at": 1458122238.7788615, - "connection_changed_at": 1457517588.4372394, - "desired_powered_changed_at": 1458628650.8619466, - "desired_brightness_changed_at": 1458628381.8566465, - "firmware_date_code_changed_at": 1457521561.0603704, - "color_model_changed_at": 1457521797.6389458, - "hue_changed_at": 1457595786.5472758, - "saturation_changed_at": 1457595782.71269, - "color_temperature_changed_at": 1457521786.911106, - "firmware_version_changed_at": 1457521561.0603704, - "desired_color_model_changed_at": 1458620834.569744, - "desired_hue_changed_at": 1457595786.605935, - "desired_saturation_changed_at": 1457595782.8273423, - "desired_color_temperature_changed_at": 1457521921.4747667 - }, - "light_bulb_id": "1515274", - "name": "Bat Signal", - "locale": "en_us", - "units": { - }, - "created_at": 1457517586, - "hidden_at": null, - "capabilities": { - "fields": [ - { - "field": "connection", - "type": "boolean", - "mutability": "read-only" - }, - { - "field": "powered", - "type": "boolean", - "mutability": "read-write" - }, - { - "field": "brightness", - "type": "percentage", - "mutability": "read-write" - }, - { - "field": "color_model", - "type": "string", - "choices": [ - "rgb", - "hsb" - ] - }, - { - "field": "hue", - "type": "percentage", - "mutability": "read-write" - }, - { - "field": "saturation", - "type": "percentage", - "mutability": "read-write" - } - ], - "color_changeable": true - }, - "triggers": [], - "manufacturer_device_model": "sylvania_sylvania_rgbw", - "manufacturer_device_id": null, - "device_manufacturer": "sylvania", - "model_name": "Lightify RGBW Bulb", - "upc_id": "509", - "upc_code": "4613573703", - "gang_id": null, - "hub_id": "381678", - "local_id": "37", - "radio_type": "zigbee", - "linked_service_id": null, - "lat_lng": [ - null, - null - ], - "location": "", - "order": 0 - } - ] -} diff --git a/src/pywink/test/devices/standard/api_responses/ring_door_bell.json b/src/pywink/test/devices/standard/api_responses/ring_door_bell.json deleted file mode 100644 index 927a932..0000000 --- a/src/pywink/test/devices/standard/api_responses/ring_door_bell.json +++ /dev/null @@ -1,80 +0,0 @@ -{ - "data":[ - { - "uuid":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", - "desired_state":{ - - }, - "last_reading":{ - "battery":1.0, - "battery_updated_at":1483588707.0458965, - "motion":false, - "motion_updated_at":1483717738.8899395, - "button_pressed":false, - "button_pressed_updated_at":1483658161.5844142, - "motion_true":"N/A", - "motion_true_updated_at":1483717728.305417, - "button_pressed_true":"N/A", - "button_pressed_true_updated_at":1483658092.2301679, - "connection":true, - "connection_updated_at":1442873339.2134194, - "last_recording_cuepoint_id":"1722851976", - "last_recording_cuepoint_id_updated_at":1483717787.6740644, - "motion_changed_at":1483717738.8899395, - "motion_true_changed_at":1483717728.305417, - "button_pressed_changed_at":1483658161.5844142, - "button_pressed_true_changed_at":1483658092.2301679, - "battery_changed_at":1483588707.0458965, - "last_recording_cuepoint_id_changed_at":1483717787.6740644 - }, - "subscription":{ - "pubnub":{ - "subscribe_key":"sub-c-xxxxxxxx-xxxx-xxxxxxx-xxxxxxxxxxxx", - "channel":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx|door_bell-xx|user-xxxxxx" - } - }, - "door_bell_id":"123456", - "name":"Home", - "locale":"en_us", - "units":{ - - }, - "created_at":1442873339, - "hidden_at":null, - "capabilities":{ - "fields":[ - { - "type":"percentage", - "field":"battery", - "mutability":"read-only" - }, - { - "type":"boolean", - "field":"motion", - "mutability":"read-only" - }, - { - "type":"boolean", - "field":"button_pressed", - "mutability":"read-only" - } - ] - }, - "user_ids":[ - "xxxxxx" - ], - "manufacturer_device_model":"doorbell", - "manufacturer_device_id":"12345", - "device_manufacturer":"ring", - "model_name":"Ring Video Doorbell", - "upc_id":"xxx", - "upc_code":"xxxxxxxxxxxx", - "linked_service_id":"xxxxxx", - "lat_lng":[ - 98.765432, - -12.345678 - ], - "location":"1234 Main St, City, ST XXXXX, USA" - } - ] -} diff --git a/src/pywink/test/devices/standard/api_responses/sensi.json b/src/pywink/test/devices/standard/api_responses/sensi.json deleted file mode 100644 index 3fcfb9d..0000000 --- a/src/pywink/test/devices/standard/api_responses/sensi.json +++ /dev/null @@ -1,234 +0,0 @@ -{ - "data":[ - { - "uuid":"4890fa42-4339-4824-a017-c874bb123456", - "desired_state":{ - "max_set_point":22.22222222222222, - "min_set_point":22.22222222222222, - "fan_mode":"auto", - "powered":false, - "mode":"cool_only" - }, - "last_reading":{ - "max_set_point":22.22222222222222, - "max_set_point_updated_at":1477511203.5183966, - "min_set_point":22.22222222222222, - "min_set_point_updated_at":1477511203.5183966, - "fan_mode":"auto", - "fan_mode_updated_at":1477511203.5183966, - "powered":true, - "has_fan":true, - "powered_updated_at":1477511203.5183966, - "smart_temperature":20.555555555555557, - "humidifier_mode":"auto", - "humidifier_set_point":0.2, - "dehumidifier_mode":"auto", - "dehumidifier_set_point":0.6, - "occupied":true, - "temperature":20.555555555555557, - "temperature_updated_at":1477511203.5183966, - "external_temperature":16.1, - "external_temperature_updated_at":null, - "min_min_set_point":7.222222222222222, - "min_min_set_point_updated_at":1477511203.5183966, - "max_min_set_point":37.22222222222222, - "max_min_set_point_updated_at":1477511203.5183966, - "min_max_set_point":7.222222222222222, - "min_max_set_point_updated_at":1477511203.5183966, - "max_max_set_point":37.22222222222222, - "max_max_set_point_updated_at":1477511203.5183966, - "deadband":1.1111111111111112, - "deadband_updated_at":1477511203.5183966, - "humidity":40, - "humidity_updated_at":1477511203.5183966, - "cool_active":false, - "cool_active_updated_at":1477511203.5183966, - "heat_active":false, - "heat_active_updated_at":1477511203.5183966, - "aux_active":false, - "aux_active_updated_at":1477511203.5183966, - "fan_active":true, - "fan_active_updated_at":1477511203.5183966, - "firmware_version":"6003980915", - "firmware_version_updated_at":1477511203.5183966, - "connection":true, - "connection_updated_at":1477511203.5183966, - "mode":"cool_only", - "mode_updated_at":1477511203.5183966, - "modes_allowed":[ - "heat_only", - "cool_only", - "auto" - ], - "modes_allowed_updated_at":1477511203.5183966, - "units":"f", - "units_updated_at":1477511203.5183966, - "desired_max_set_point_updated_at":1476998924.056839, - "desired_min_set_point_updated_at":1476998924.056839, - "desired_fan_mode_updated_at":1476998924.056839, - "desired_powered_updated_at":1476998788.717325, - "desired_mode_updated_at":1476998924.056839, - "humidity_changed_at":1477509616.356899, - "temperature_changed_at":1477510096.4845245, - "desired_powered_changed_at":1476998788.717325, - "desired_mode_changed_at":1476725230.5535583, - "powered_changed_at":1476998788.5916903, - "cool_active_changed_at":1476998788.5916903, - "fan_active_changed_at":1476998788.5916903, - "max_set_point_changed_at":1476745575.4795704, - "connection_changed_at":1477511203.5183966, - "firmware_version_changed_at":1477274231.5533497 - }, - "subscription":{ - "pubnub":{ - "subscribe_key":"sub-c-f7bf7f7e-0542-11e3-a5e8-02ee2d123456", - "channel":"4d181499722e8b9c0f823aaad005678963325e53|thermostat-140234|user-40345" - } - }, - "thermostat_id":"140295", - "name":"Scanlon", - "locale":"en_us", - "units":{ - "temperature":"f" - }, - "created_at":1465337429, - "hidden_at":null, - "capabilities":{ - "fields":[ - { - "type":"float", - "field":"max_set_point", - "mutability":"read-write" - }, - { - "type":"float", - "field":"min_set_point", - "mutability":"read-write" - }, - { - "type":"selection", - "field":"fan_mode", - "choices":[ - "auto", - "on" - ], - "mutability":"read-write" - }, - { - "type":"boolean", - "field":"powered", - "mutability":"read-write" - }, - { - "type":"float", - "field":"temperature", - "mutability":"read-only" - }, - { - "type":"float", - "field":"external_temperature", - "mutability":"read-only" - }, - { - "type":"float", - "field":"min_min_set_point", - "mutability":"read-only" - }, - { - "type":"float", - "field":"max_min_set_point", - "mutability":"read-only" - }, - { - "type":"float", - "field":"min_max_set_point", - "mutability":"read-only" - }, - { - "type":"float", - "field":"max_max_set_point", - "mutability":"read-only" - }, - { - "type":"float", - "field":"deadband", - "mutability":"read-only" - }, - { - "type":"percentage", - "field":"humidity", - "mutability":"read-only" - }, - { - "type":"boolean", - "field":"cool_active", - "mutability":"read-only" - }, - { - "type":"boolean", - "field":"heat_active", - "mutability":"read-only" - }, - { - "type":"boolean", - "field":"aux_active", - "mutability":"read-only" - }, - { - "type":"boolean", - "field":"fan_active", - "mutability":"read-only" - }, - { - "type":"string", - "field":"firmware_version", - "mutability":"read-only" - }, - { - "type":"boolean", - "field":"connection", - "mutability":"read-only" - }, - { - "field":"mode", - "type":"selection", - "mutability":"read-write", - "choices":[ - "heat_only", - "cool_only", - "auto" - ] - } - ], - "notification_robots":[ - "aux_active_notification" - ] - }, - "triggers":[ - - ], - "manufacturer_device_model":"emerson_up500_wb1", - "manufacturer_device_id":"36-6f-92-ff-fe-04-34-12", - "device_manufacturer":"emerson", - "model_name":"Sensi Wi-Fi Programmable Thermostat", - "upc_id":"652", - "upc_code":"emerson_up500wb1", - "hub_id":null, - "local_id":null, - "radio_type":null, - "linked_service_id":"365180", - "lat_lng":[ - null, - null - ], - "location":"", - "smart_schedule_enabled":false - } - ], - "errors":[ - - ], - "pagination":{ - "count":13 - } -} diff --git a/src/pywink/test/devices/standard/api_responses/shade.json b/src/pywink/test/devices/standard/api_responses/shade.json deleted file mode 100644 index 535157c..0000000 --- a/src/pywink/test/devices/standard/api_responses/shade.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "data": [{ - "uuid": "07b44f75-c7ee-48f1-b43a-f564c15fed1b", - "desired_state": { - "position": null - }, - "last_reading": { - "connection": true, - "connection_updated_at": 1463494385.449129, - "position": null, - "position_updated_at": 1463494385.449129, - "desired_position_updated_at": 1463500774.348026, - "connection_changed_at": 1457674189.5065048, - "desired_position_changed_at": 1463500774.348026 - }, - "subscription": { - "pubnub": { - "subscribe_key": "sub-c-f7bf7f7e-0542-11e3-a5e8-02ee2ddab7fe", - "channel": "31be109cff295db0c6d08d411fac0133e46a6138|shade-5650|user-410445" - } - }, - "shade_id": "5650", - "name": "Left Bed Shade", - "locale": "en_us", - "units": {}, - "created_at": 1457674189, - "hidden_at": null, - "capabilities": {}, - "triggers": [], - "manufacturer_device_model": "somfy_bali", - "manufacturer_device_id": null, - "device_manufacturer": "somfy", - "model_name": "Shade", - "upc_id": "81", - "upc_code": "SOMFY", - "hub_id": "344883", - "local_id": "6", - "radio_type": "zwave", - "lat_lng": [37.813847, -122.277662], - "location": "94607" - }], - "errors": [], - "pagination": {} -} diff --git a/src/pywink/test/devices/standard/api_responses/siren.json b/src/pywink/test/devices/standard/api_responses/siren.json deleted file mode 100644 index 2786275..0000000 --- a/src/pywink/test/devices/standard/api_responses/siren.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "data":[ - { - "desired_state":{ - "auto_shutoff":30, - "mode":"siren_and_strobe", - "powered":false - }, - "last_reading":{ - "connection":true, - "connection_updated_at":1453249957.2466462, - "battery":1, - "battery_updated_at":1453249957.2466462, - "auto_shutoff":30, - "auto_shutoff_updated_at":1453249957.2466462, - "mode":"siren_and_strobe", - "mode_updated_at":1453249957.2466462, - "powered":false, - "powered_updated_at":1453249957.2466462, - "desired_auto_shutoff_updated_at":1452812848.5178623, - "desired_mode_updated_at":1452812848.5178623, - "desired_powered_updated_at":1452812668.1190264, - "connection_changed_at":1452812587.0312104, - "powered_changed_at":1452812668.0807295, - "battery_changed_at":1453032821.1796713, - "mode_changed_at":1452812589.8262901, - "auto_shutoff_changed_at":1452812589.8262901, - "desired_auto_shutoff_changed_at":1452812590.029748, - "desired_powered_changed_at":1452812668.1190264, - "desired_mode_changed_at":1452812848.5178623 - }, - "siren_id":"6123", - "name":"Alarm", - "locale":"en_us", - "units":{ - - }, - "created_at":1452812587, - "hidden_at":null, - "capabilities":{ - - }, - "device_manufacturer":"linear", - "model_name":"Wireless Siren & Strobe (Wireless)", - "upc_id":"243", - "upc_code":"wireless_linear_siren", - "hub_id":"30123", - "local_id":"8", - "radio_type":"zwave", - "lat_lng":[ - 12.1345678, - -98.765432 - ], - "location":"" - } - ], - "errors":[ - - ], - "pagination":{ - "count":17 - } -} \ No newline at end of file diff --git a/src/pywink/test/devices/standard/api_responses/smoke_detector.json b/src/pywink/test/devices/standard/api_responses/smoke_detector.json deleted file mode 100644 index d13435d..0000000 --- a/src/pywink/test/devices/standard/api_responses/smoke_detector.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "data":[ - { - "object_type":"smoke_detector", - "object_id":"12345", - "uuid":"54feac93-327e-4f04-a8ba-500c88e95245", - "icon_id":null, - "icon_code":null, - "last_reading":{ - "connection":true, - "connection_updated_at":1462586187.9383092, - "battery":0.9, - "battery_updated_at":1462586188.4449866, - "co_detected":true, - "co_detected_updated_at":1462586188.4449866, - "smoke_detected":false, - "smoke_detected_updated_at":1471015253.2221863, - "test_activated":false, - "test_activated_updated_at":1462997176.8458738 - }, - "subscription":{ - "pubnub":{ - "subscribe_key":"REMOVED", - "channel":"Removed|smoke_detector-REMOVED|user-REMOVED" - } - }, - "smoke_detector_id":"12345", - "name":"Hallway Smoke Detector", - "locale":"en_us", - "units":{ - - }, - "created_at":1462586187, - "hidden_at":null, - "capabilities":{ - - }, - "manufacturer_device_model":"kidde_smoke_alarm", - "manufacturer_device_id":null, - "device_manufacturer":"kidde", - "model_name":"Smoke Alarm", - "upc_id":"524", - "upc_code":"kidde_smoke_alarm", - "hub_id":"REMOVED", - "local_id":null, - "radio_type":"kidde", - "linked_service_id":null, - "lat_lng":[ - 39.00000, - -77.00000 - ], - "location":"REMOVED" - } - ], - "errors":[ - - ], - "pagination":{ - "count":13 - } -} diff --git a/src/pywink/test/devices/standard/api_responses/temperature_absent.json b/src/pywink/test/devices/standard/api_responses/temperature_absent.json deleted file mode 100644 index cc5f4e9..0000000 --- a/src/pywink/test/devices/standard/api_responses/temperature_absent.json +++ /dev/null @@ -1,119 +0,0 @@ -{ - "data": [ - { - "uuid": "238539e2-1ad6-44ba-bc53-33c684c36e1d", - "desired_state": { - "powered": false, - "brightness": 1, - "color_model": "hsb", - "hue": 0.35, - "saturation": 1, - "color_temperature": 1901 - }, - "last_reading": { - "connection": true, - "connection_updated_at": 1458628651.862995, - "powered": false, - "powered_updated_at": 1458628651.862995, - "brightness": 1, - "brightness_updated_at": 1458628651.862995, - "color_model": "hsb", - "color_model_updated_at": 1458628651.862995, - "hue": 0.35, - "hue_updated_at": 1458628651.862995, - "saturation": 1, - "saturation_updated_at": 1458628651.862995, - "color_temperature": 1901, - "color_temperature_updated_at": 1458628651.862995, - "firmware_version": "0.1b02 / 0.3b22", - "firmware_version_updated_at": 1458628651.862995, - "firmware_date_code": "20150929N****", - "firmware_date_code_updated_at": 1458628651.862995, - "desired_powered_updated_at": 1458628650.8619466, - "desired_brightness_updated_at": 1458628820.0301423, - "desired_color_model_updated_at": 1458628820.0301423, - "desired_hue_updated_at": 1458628820.0301423, - "desired_saturation_updated_at": 1458628820.0301423, - "desired_color_temperature_updated_at": 1458628820.0301423, - "powered_changed_at": 1458628650.8134031, - "brightness_changed_at": 1458122238.7788615, - "connection_changed_at": 1457517588.4372394, - "desired_powered_changed_at": 1458628650.8619466, - "desired_brightness_changed_at": 1458628381.8566465, - "firmware_date_code_changed_at": 1457521561.0603704, - "color_model_changed_at": 1457521797.6389458, - "hue_changed_at": 1457595786.5472758, - "saturation_changed_at": 1457595782.71269, - "color_temperature_changed_at": 1457521786.911106, - "firmware_version_changed_at": 1457521561.0603704, - "desired_color_model_changed_at": 1458620834.569744, - "desired_hue_changed_at": 1457595786.605935, - "desired_saturation_changed_at": 1457595782.8273423, - "desired_color_temperature_changed_at": 1457521921.4747667 - }, - "light_bulb_id": "1515274", - "name": "Bat Signal", - "locale": "en_us", - "units": { - }, - "created_at": 1457517586, - "hidden_at": null, - "capabilities": { - "fields": [ - { - "field": "connection", - "type": "boolean", - "mutability": "read-only" - }, - { - "field": "powered", - "type": "boolean", - "mutability": "read-write" - }, - { - "field": "brightness", - "type": "percentage", - "mutability": "read-write" - }, - { - "field": "color_model", - "type": "string", - "choices": [ - "rgb", - "hsb" - ] - }, - { - "field": "hue", - "type": "percentage", - "mutability": "read-write" - }, - { - "field": "saturation", - "type": "percentage", - "mutability": "read-write" - } - ], - "color_changeable": true - }, - "triggers": [], - "manufacturer_device_model": "sylvania_sylvania_rgbw", - "manufacturer_device_id": null, - "device_manufacturer": "sylvania", - "model_name": "Lightify RGBW Bulb", - "upc_id": "509", - "upc_code": "4613573703", - "gang_id": null, - "hub_id": "381678", - "local_id": "37", - "radio_type": "zigbee", - "linked_service_id": null, - "lat_lng": [ - null, - null - ], - "location": "", - "order": 0 - } - ] -} diff --git a/src/pywink/test/devices/standard/api_responses/temperature_present.json b/src/pywink/test/devices/standard/api_responses/temperature_present.json deleted file mode 100644 index ae4e04b..0000000 --- a/src/pywink/test/devices/standard/api_responses/temperature_present.json +++ /dev/null @@ -1,129 +0,0 @@ -{ - "data": [ - { - "uuid": "238539e2-1ad6-44ba-bc53-33c684c36e1d", - "desired_state": { - "powered": false, - "brightness": 1, - "color_model": "hsb", - "hue": 0.35, - "saturation": 1, - "color_temperature": 1901 - }, - "last_reading": { - "connection": true, - "connection_updated_at": 1458628651.862995, - "powered": false, - "powered_updated_at": 1458628651.862995, - "brightness": 1, - "brightness_updated_at": 1458628651.862995, - "color_model": "hsb", - "color_model_updated_at": 1458628651.862995, - "hue": 0.35, - "hue_updated_at": 1458628651.862995, - "saturation": 1, - "saturation_updated_at": 1458628651.862995, - "color_temperature": 1901, - "color_temperature_updated_at": 1458628651.862995, - "firmware_version": "0.1b02 / 0.3b22", - "firmware_version_updated_at": 1458628651.862995, - "firmware_date_code": "20150929N****", - "firmware_date_code_updated_at": 1458628651.862995, - "desired_powered_updated_at": 1458628650.8619466, - "desired_brightness_updated_at": 1458628820.0301423, - "desired_color_model_updated_at": 1458628820.0301423, - "desired_hue_updated_at": 1458628820.0301423, - "desired_saturation_updated_at": 1458628820.0301423, - "desired_color_temperature_updated_at": 1458628820.0301423, - "powered_changed_at": 1458628650.8134031, - "brightness_changed_at": 1458122238.7788615, - "connection_changed_at": 1457517588.4372394, - "desired_powered_changed_at": 1458628650.8619466, - "desired_brightness_changed_at": 1458628381.8566465, - "firmware_date_code_changed_at": 1457521561.0603704, - "color_model_changed_at": 1457521797.6389458, - "hue_changed_at": 1457595786.5472758, - "saturation_changed_at": 1457595782.71269, - "color_temperature_changed_at": 1457521786.911106, - "firmware_version_changed_at": 1457521561.0603704, - "desired_color_model_changed_at": 1458620834.569744, - "desired_hue_changed_at": 1457595786.605935, - "desired_saturation_changed_at": 1457595782.8273423, - "desired_color_temperature_changed_at": 1457521921.4747667 - }, - "light_bulb_id": "1515274", - "name": "Bat Signal", - "locale": "en_us", - "units": { - }, - "created_at": 1457517586, - "hidden_at": null, - "capabilities": { - "fields": [ - { - "field": "connection", - "type": "boolean", - "mutability": "read-only" - }, - { - "field": "powered", - "type": "boolean", - "mutability": "read-write" - }, - { - "field": "brightness", - "type": "percentage", - "mutability": "read-write" - }, - { - "field": "color_model", - "type": "string", - "choices": [ - "rgb", - "hsb", - "color_temperature" - ] - }, - { - "field": "hue", - "type": "percentage", - "mutability": "read-write" - }, - { - "field": "saturation", - "type": "percentage", - "mutability": "read-write" - }, - { - "field": "color_temperature", - "range": [ - 1900, - 6500 - ], - "type": "integer", - "mutability": "read-write" - } - ], - "color_changeable": true - }, - "triggers": [], - "manufacturer_device_model": "sylvania_sylvania_rgbw", - "manufacturer_device_id": null, - "device_manufacturer": "sylvania", - "model_name": "Lightify RGBW Bulb", - "upc_id": "509", - "upc_code": "4613573703", - "gang_id": null, - "hub_id": "381678", - "local_id": "37", - "radio_type": "zigbee", - "linked_service_id": null, - "lat_lng": [ - null, - null - ], - "location": "", - "order": 0 - } - ] -} diff --git a/src/pywink/test/devices/standard/api_responses/v1_hub.json b/src/pywink/test/devices/standard/api_responses/v1_hub.json deleted file mode 100644 index 284e2a5..0000000 --- a/src/pywink/test/devices/standard/api_responses/v1_hub.json +++ /dev/null @@ -1,97 +0,0 @@ -{ - "data":[ - { - "uuid":"d16117dd-6565-4341-9078-f57282123456", - "desired_state":{ - "pairing_mode":null, - "pairing_prefix":null, - "pairing_mode_duration":0 - }, - "last_reading":{ - "connection":false, - "connection_updated_at":1480783433.1703727, - "agent_session_id":null, - "agent_session_id_updated_at":1480783433.1703727, - "pairing_mode":null, - "pairing_mode_updated_at":1480735480.2006376, - "pairing_prefix":null, - "pairing_prefix_updated_at":null, - "kidde_radio_code_updated_at":1449442170.975126, - "pairing_mode_duration":0, - "pairing_mode_duration_updated_at":1480735480.2006376, - "updating_firmware":false, - "updating_firmware_updated_at":1480735478.8520393, - "firmware_version":"3.3.26-0-gf4fa1428f9", - "firmware_version_updated_at":1480735481.970308, - "update_needed":false, - "update_needed_updated_at":1480735481.970308, - "mac_address":"0", - "mac_address_updated_at":1480735481.970308, - "zigbee_mac_address":"000D6F0005381234", - "zigbee_mac_address_updated_at":1480735480.2006376, - "ip_address":"192.168.1.2", - "ip_address_updated_at":1480735481.970308, - "hub_version":"00.01", - "hub_version_updated_at":1480735481.970308, - "app_version":"0.1.0", - "app_version_updated_at":1480735481.970308, - "transfer_mode":null, - "transfer_mode_updated_at":1480735479.4155467, - "remote_pairable":null, - "remote_pairable_updated_at":null, - "local_control_public_key_hash":"E3:03:48:35:D4:92:46:EE:9E:DD:AC:3B:59:72:78:C7:22:8C:4E:C5:52:85:A3:D2:19:49:40:90:78:56:34:12", - "local_control_public_key_hash_updated_at":1480735490.8312745, - "local_control_id":"c2d9c107-55cb-41b2-b330-fb0e1e123456", - "local_control_id_updated_at":1480735490.8312745, - "desired_pairing_mode_updated_at":1480196216.2784698, - "desired_pairing_prefix_updated_at":1449960168.9824853, - "desired_kidde_radio_code_updated_at":1449442171.0061595, - "desired_pairing_mode_duration_updated_at":1480196216.2784698, - "agent_session_id_changed_at":1480783433.1703727, - "mac_address_changed_at":1480735480.6162705, - "ip_address_changed_at":1480735480.6162705, - "connection_changed_at":1480783433.1703727 - }, - "subscription":{ - "pubnub":{ - "subscribe_key":"sub-c-f7bf7f7e-0542-11e3-a5e8-02ee2d123456", - "channel":"79b3abc01f3a3cf9d9cae79c071df059a4c1a6d2|hub-302123|user-377123" - } - }, - "hub_id":"302123", - "name":"Hub", - "locale":"en_us", - "units":{ - - }, - "created_at":1449442170, - "hidden_at":null, - "capabilities":{ - "oauth2_clients":[ - "wink_hub" - ], - "home_security_device":true, - "provisioning_version":"8a", - "needs_wifi_network_list":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":[ - 12.345678, - -98.765432 - ], - "location":null, - "update_needed":false, - "configuration":{ - "kidde_radio_code":0 - } - } - ] -} diff --git a/src/pywink/test/devices/standard/api_responses/wink_relay_sensor.json b/src/pywink/test/devices/standard/api_responses/wink_relay_sensor.json deleted file mode 100644 index 637d982..0000000 --- a/src/pywink/test/devices/standard/api_responses/wink_relay_sensor.json +++ /dev/null @@ -1,107 +0,0 @@ -{ - "data":[ - { - "hidden_at":null, - "manufacturer_device_model":"wink_relay_sensor", - "object_id":"231234", - "radio_type":"project_one", - "manufacturer_device_id":null, - "locale":"en_us", - "upc_id":"188", - "location":"", - "gang_id":"40123", - "desired_state":{ - - }, - "subscription":{ - "pubnub":{ - "subscribe_key":"sub-c-f7bf7f7e-0542-11e323456-12142ddab7fe", - "channel":"a82816f0ddb9fa5448c5cb471d21716564871164|sensor_pod-123787|user-220271" - } - }, - "created_at":1468460695, - "lat_lng":[ - null, - null - ], - "triggers":[ - - ], - "units":{ - - }, - "model_name":"Wink Relay Sensor", - "name":"Great Room Relay", - "device_manufacturer":"wink", - "icon_id":null, - "last_event":{ - "vibration_occurred_at":null, - "loudness_occurred_at":null, - "brightness_occurred_at":null - }, - "local_id":"3", - "capabilities":{ - "desired_state_fields":[ - - ], - "sensor_types":[ - { - "mutability":"read-only", - "field":"temperature", - "attribute_id":1, - "type":"float" - }, - { - "mutability":"read-only", - "field":"humidity", - "attribute_id":2, - "type":"percentage" - }, - { - "mutability":"read-only", - "field":"presence", - "attribute_id":3, - "type":"boolean" - }, - { - "mutability":"read-only", - "field":"proximity", - "attribute_id":4, - "type":"float" - } - ] - }, - "sensor_pod_id":"212345", - "last_reading":{ - "temperature_updated_at":1474295085.8156993, - "agent_session_id_updated_at":null, - "presence":false, - "agent_session_id":null, - "temperature":19.87, - "humidity_changed_at":1474295085.8156993, - "proximity":2512.0, - "presence_updated_at":1474295085.8156993, - "proximity_updated_at":1474295085.8156993, - "proximity_changed_at":1474295085.8156993, - "presence_changed_at":1474281889.6097996, - "connection":true, - "humidity":0.69, - "connection_updated_at":1474295085.8156993, - "temperature_changed_at":1474295085.8156993, - "humidity_updated_at":1474295085.8156993 - }, - "uuid":"9bb49bfe-0d6a-4e9b-9896-f33c37ec969b", - "icon_code":null, - "object_type":"sensor_pod", - "upc_code":"wink_p1_sensor", - "linked_service_id":null, - "hub_id":"451234" - } - ], - "errors":[ - - ], - "pagination":{ - "count":13 - } -} diff --git a/src/pywink/test/devices/standard/api_responses/xy_absent.json b/src/pywink/test/devices/standard/api_responses/xy_absent.json deleted file mode 100644 index cc5f4e9..0000000 --- a/src/pywink/test/devices/standard/api_responses/xy_absent.json +++ /dev/null @@ -1,119 +0,0 @@ -{ - "data": [ - { - "uuid": "238539e2-1ad6-44ba-bc53-33c684c36e1d", - "desired_state": { - "powered": false, - "brightness": 1, - "color_model": "hsb", - "hue": 0.35, - "saturation": 1, - "color_temperature": 1901 - }, - "last_reading": { - "connection": true, - "connection_updated_at": 1458628651.862995, - "powered": false, - "powered_updated_at": 1458628651.862995, - "brightness": 1, - "brightness_updated_at": 1458628651.862995, - "color_model": "hsb", - "color_model_updated_at": 1458628651.862995, - "hue": 0.35, - "hue_updated_at": 1458628651.862995, - "saturation": 1, - "saturation_updated_at": 1458628651.862995, - "color_temperature": 1901, - "color_temperature_updated_at": 1458628651.862995, - "firmware_version": "0.1b02 / 0.3b22", - "firmware_version_updated_at": 1458628651.862995, - "firmware_date_code": "20150929N****", - "firmware_date_code_updated_at": 1458628651.862995, - "desired_powered_updated_at": 1458628650.8619466, - "desired_brightness_updated_at": 1458628820.0301423, - "desired_color_model_updated_at": 1458628820.0301423, - "desired_hue_updated_at": 1458628820.0301423, - "desired_saturation_updated_at": 1458628820.0301423, - "desired_color_temperature_updated_at": 1458628820.0301423, - "powered_changed_at": 1458628650.8134031, - "brightness_changed_at": 1458122238.7788615, - "connection_changed_at": 1457517588.4372394, - "desired_powered_changed_at": 1458628650.8619466, - "desired_brightness_changed_at": 1458628381.8566465, - "firmware_date_code_changed_at": 1457521561.0603704, - "color_model_changed_at": 1457521797.6389458, - "hue_changed_at": 1457595786.5472758, - "saturation_changed_at": 1457595782.71269, - "color_temperature_changed_at": 1457521786.911106, - "firmware_version_changed_at": 1457521561.0603704, - "desired_color_model_changed_at": 1458620834.569744, - "desired_hue_changed_at": 1457595786.605935, - "desired_saturation_changed_at": 1457595782.8273423, - "desired_color_temperature_changed_at": 1457521921.4747667 - }, - "light_bulb_id": "1515274", - "name": "Bat Signal", - "locale": "en_us", - "units": { - }, - "created_at": 1457517586, - "hidden_at": null, - "capabilities": { - "fields": [ - { - "field": "connection", - "type": "boolean", - "mutability": "read-only" - }, - { - "field": "powered", - "type": "boolean", - "mutability": "read-write" - }, - { - "field": "brightness", - "type": "percentage", - "mutability": "read-write" - }, - { - "field": "color_model", - "type": "string", - "choices": [ - "rgb", - "hsb" - ] - }, - { - "field": "hue", - "type": "percentage", - "mutability": "read-write" - }, - { - "field": "saturation", - "type": "percentage", - "mutability": "read-write" - } - ], - "color_changeable": true - }, - "triggers": [], - "manufacturer_device_model": "sylvania_sylvania_rgbw", - "manufacturer_device_id": null, - "device_manufacturer": "sylvania", - "model_name": "Lightify RGBW Bulb", - "upc_id": "509", - "upc_code": "4613573703", - "gang_id": null, - "hub_id": "381678", - "local_id": "37", - "radio_type": "zigbee", - "linked_service_id": null, - "lat_lng": [ - null, - null - ], - "location": "", - "order": 0 - } - ] -} diff --git a/src/pywink/test/devices/standard/api_responses/xy_present.json b/src/pywink/test/devices/standard/api_responses/xy_present.json deleted file mode 100644 index 9169267..0000000 --- a/src/pywink/test/devices/standard/api_responses/xy_present.json +++ /dev/null @@ -1,119 +0,0 @@ -{ - "data": [ - { - "uuid": "238539e2-1ad6-44ba-bc53-33c684c36e1d", - "desired_state": { - "powered": false, - "brightness": 1, - "color_model": "hsb", - "hue": 0.35, - "saturation": 1, - "color_temperature": 1901 - }, - "last_reading": { - "connection": true, - "connection_updated_at": 1458628651.862995, - "powered": false, - "powered_updated_at": 1458628651.862995, - "brightness": 1, - "brightness_updated_at": 1458628651.862995, - "color_model": "hsb", - "color_model_updated_at": 1458628651.862995, - "hue": 0.35, - "hue_updated_at": 1458628651.862995, - "saturation": 1, - "saturation_updated_at": 1458628651.862995, - "color_temperature": 1901, - "color_temperature_updated_at": 1458628651.862995, - "firmware_version": "0.1b02 / 0.3b22", - "firmware_version_updated_at": 1458628651.862995, - "firmware_date_code": "20150929N****", - "firmware_date_code_updated_at": 1458628651.862995, - "desired_powered_updated_at": 1458628650.8619466, - "desired_brightness_updated_at": 1458628820.0301423, - "desired_color_model_updated_at": 1458628820.0301423, - "desired_hue_updated_at": 1458628820.0301423, - "desired_saturation_updated_at": 1458628820.0301423, - "desired_color_temperature_updated_at": 1458628820.0301423, - "powered_changed_at": 1458628650.8134031, - "brightness_changed_at": 1458122238.7788615, - "connection_changed_at": 1457517588.4372394, - "desired_powered_changed_at": 1458628650.8619466, - "desired_brightness_changed_at": 1458628381.8566465, - "firmware_date_code_changed_at": 1457521561.0603704, - "color_model_changed_at": 1457521797.6389458, - "hue_changed_at": 1457595786.5472758, - "saturation_changed_at": 1457595782.71269, - "color_temperature_changed_at": 1457521786.911106, - "firmware_version_changed_at": 1457521561.0603704, - "desired_color_model_changed_at": 1458620834.569744, - "desired_hue_changed_at": 1457595786.605935, - "desired_saturation_changed_at": 1457595782.8273423, - "desired_color_temperature_changed_at": 1457521921.4747667 - }, - "light_bulb_id": "1515274", - "name": "Bat Signal", - "locale": "en_us", - "units": { - }, - "created_at": 1457517586, - "hidden_at": null, - "capabilities": { - "fields": [ - { - "field": "connection", - "type": "boolean", - "mutability": "read-only" - }, - { - "field": "powered", - "type": "boolean", - "mutability": "read-write" - }, - { - "field": "brightness", - "type": "percentage", - "mutability": "read-write" - }, - { - "field": "color_model", - "type": "string", - "choices": [ - "xy", - "hsb" - ] - }, - { - "field": "hue", - "type": "percentage", - "mutability": "read-write" - }, - { - "field": "saturation", - "type": "percentage", - "mutability": "read-write" - } - ], - "color_changeable": true - }, - "triggers": [], - "manufacturer_device_model": "sylvania_sylvania_rgbw", - "manufacturer_device_id": null, - "device_manufacturer": "sylvania", - "model_name": "Lightify RGBW Bulb", - "upc_id": "509", - "upc_code": "4613573703", - "gang_id": null, - "hub_id": "381678", - "local_id": "37", - "radio_type": "zigbee", - "linked_service_id": null, - "lat_lng": [ - null, - null - ], - "location": "", - "order": 0 - } - ] -} diff --git a/src/pywink/test/devices/standard/bulb_test.py b/src/pywink/test/devices/standard/bulb_test.py deleted file mode 100644 index 9db7bb2..0000000 --- a/src/pywink/test/devices/standard/bulb_test.py +++ /dev/null @@ -1,260 +0,0 @@ -import json -import os -import unittest - -import mock - -from pywink.api import get_devices_from_response_dict, WinkApiInterface -from pywink.devices import types as device_types -from pywink.devices.standard import WinkBulb -from pywink.devices.types import DEVICE_ID_KEYS - - -class BulbSupportsHueSaturationTest(unittest.TestCase): - - def test_should_be_true_if_response_contains_hue_and_saturation_capabilities(self): - with open('{}/api_responses/hue_and_saturation_present.json'.format(os.path.dirname(__file__))) as light_file: - response_dict = json.load(light_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.LIGHT_BULB]) - - bulb = devices[0] - """ :type bulb: pywink.devices.standard.WinkBulb """ - supports_hs = bulb.supports_hue_saturation() - self.assertTrue(supports_hs, - msg="Expected hue and saturation to be supported") - - def test_should_be_false_if_response_does_not_contain_hue_and_saturation_capabilities(self): - with open('{}/api_responses/hue_and_saturation_absent.json'.format(os.path.dirname(__file__))) as light_file: - response_dict = json.load(light_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.LIGHT_BULB]) - - bulb = devices[0] - """ :type bulb: pywink.devices.standard.WinkBulb """ - supports_hs = bulb.supports_hue_saturation() - self.assertFalse(supports_hs, - msg="Expected hue and saturation to be supported") - - -class BulbSupportsTemperatureTest(unittest.TestCase): - - def test_should_be_true_if_response_contains_temperature_capabilities(self): - with open('{}/api_responses/temperature_present.json'.format(os.path.dirname(__file__))) as light_file: - response_dict = json.load(light_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.LIGHT_BULB]) - - bulb = devices[0] - """ :type bulb: pywink.devices.standard.WinkBulb """ - supports_temperature = bulb.supports_temperature() - self.assertTrue(supports_temperature, - msg="Expected temperature to be supported") - - - def test_should_be_false_if_response_does_not_contain_temperature_capabilities(self): - with open('{}/api_responses/temperature_absent.json'.format(os.path.dirname(__file__))) as light_file: - response_dict = json.load(light_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.LIGHT_BULB]) - - bulb = devices[0] - """ :type bulb: pywink.devices.standard.WinkBulb """ - supports_temperature = bulb.supports_temperature() - self.assertFalse(supports_temperature, - msg="Expected temperature to be un-supported") - - -class BulbSupportsXYTest(unittest.TestCase): - - def test_should_be_false_if_response_does_not_contain_xy_capabilities(self): - with open('{}/api_responses/xy_absent.json'.format(os.path.dirname(__file__))) as light_file: - response_dict = json.load(light_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.LIGHT_BULB]) - - bulb = devices[0] - """ :type bulb: pywink.devices.standard.WinkBulb """ - supports_xy = bulb.supports_xy_color() - self.assertFalse(supports_xy, - msg="Expected xy to be un-supported") - - def test_should_be_true_if_response_contains_xy_capabilities(self): - with open('{}/api_responses/xy_present.json'.format(os.path.dirname(__file__))) as light_file: - response_dict = json.load(light_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.LIGHT_BULB]) - - bulb = devices[0] - """ :type bulb: pywink.devices.standard.WinkBulb """ - supports_xy = bulb.supports_xy_color() - self.assertTrue(supports_xy, - msg="Expected xy to be supported") - - -class BulbSupportsRGBTest(unittest.TestCase): - - def test_should_be_false_if_response_does_not_contain_rgb_capabilities(self): - with open('{}/api_responses/rgb_absent.json'.format(os.path.dirname(__file__))) as light_file: - response_dict = json.load(light_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.LIGHT_BULB]) - - bulb = devices[0] - """ :type bulb: pywink.devices.standard.WinkBulb """ - supports_rgb = bulb.supports_rgb() - self.assertFalse(supports_rgb, - msg="Expected rgb to be un-supported") - - def test_should_be_false_if_response_contains_rgb_capabilities_and_hsb_capabilities(self): - with open('{}/api_responses/rgb_present.json'.format(os.path.dirname(__file__))) as light_file: - response_dict = json.load(light_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.LIGHT_BULB]) - - bulb = devices[0] - """ :type bulb: pywink.devices.standard.WinkBulb """ - supports_rgb = bulb.supports_rgb() - self.assertFalse(supports_rgb, - msg="Expected rgb to be supported") - -class SetStateTests(unittest.TestCase): - - def setUp(self): - super(SetStateTests, self).setUp() - self.api_interface = mock.Mock() - - def test_should_send_current_brightness_to_api_if_only_color_temperature_is_provided_and_bulb_only_supports_temperature(self): - original_brightness = 0.5 - bulb = WinkBulb({ - 'last_reading': { - 'desired_brightness': original_brightness - }, - 'capabilities': { - 'fields': [{'field': 'color_model', - 'choices':["color_temperature"]}] - } - }, self.api_interface) - bulb.set_state(True, color_kelvin=4000) - set_state_mock = self.api_interface.set_device_state - sent_desired_state = set_state_mock.call_args[0][1]['desired_state'] - self.assertEquals(original_brightness, sent_desired_state.get('brightness')) - - def test_should_send_color_temperature_to_api_if_color_temp_is_provided_and_bulb_only_supports_temperature(self): - bulb = WinkBulb({ - 'capabilities': { - 'fields': [{'field': 'color_model', - 'choices':["color_temperature"]}] - } - }, self.api_interface) - color_kelvin = 4000 - bulb.set_state(True, color_kelvin=color_kelvin) - set_state_mock = self.api_interface.set_device_state - sent_desired_state = set_state_mock.call_args[0][1]['desired_state'] - self.assertEquals(color_kelvin, sent_desired_state.get('color_temperature')) - - def test_should_send_current_brightness_to_api_if_only_color_temperature_is_provided_and_bulb_only_supports_hue_sat( - self): - original_brightness = 0.5 - bulb = WinkBulb({ - 'last_reading': { - 'desired_brightness': original_brightness - }, - 'capabilities': { - 'fields': [{'field': 'color_model', - 'choices':["hsb"]}] - } - }, self.api_interface) - bulb.set_state(True, color_kelvin=4000) - set_state_mock = self.api_interface.set_device_state - sent_desired_state = set_state_mock.call_args[0][1]['desired_state'] - self.assertEquals(original_brightness, sent_desired_state.get('brightness')) - - def test_should_send_current_hue_and_saturation_to_api_if_hue_and_sat_are_provided_and_bulb_only_supports_hue_sat(self): - bulb = WinkBulb({ - 'capabilities': { - 'fields': [{'field': 'color_model', - 'choices':["hsb"]}] - } - }, self.api_interface) - hue = 0.2 - saturation = 0.3 - bulb.set_state(True, color_hue_saturation=[hue, saturation]) - set_state_mock = self.api_interface.set_device_state - sent_desired_state = set_state_mock.call_args[0][1]['desired_state'] - self.assertEquals(hue, sent_desired_state.get('hue')) - self.assertEquals(saturation, sent_desired_state.get('saturation')) - - def test_should_send_original_brightness_when_only_xy_color_given_and_only_hue_saturation_supported(self): - original_brightness = 0.5 - bulb = WinkBulb({ - 'last_reading': { - 'desired_brightness': original_brightness - }, - 'capabilities': { - 'fields': [{'field': 'color_model', - 'choices':["hsb"]}] - } - }, self.api_interface) - bulb.set_state(True, color_xy=[0.5, 0.5]) - set_state_mock = self.api_interface.set_device_state - sent_desired_state = set_state_mock.call_args[0][1]['desired_state'] - self.assertEquals(original_brightness, sent_desired_state.get('brightness')) - - -class LightTests(unittest.TestCase): - - def setUp(self): - super(LightTests, self).setUp() - self.api_interface = WinkApiInterface() - - def test_should_handle_light_bulb_response(self): - with open('{}/api_responses/light_bulb.json'.format(os.path.dirname(__file__))) as light_file: - response_dict = json.load(light_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.LIGHT_BULB]) - self.assertEqual(1, len(devices)) - self.assertIsInstance(devices[0], WinkBulb) - - @mock.patch('requests.put') - def test_should_send_correct_color_hsb_values_to_wink_api(self, put_mock): - bulb = WinkBulb({ - 'capabilities': { - 'fields': [{'field': 'color_model', - 'choices':["hsb"]}] - } - }, self.api_interface) - hue = 0.75 - saturation = 0.25 - bulb.set_state(True, color_hue_saturation=[hue, saturation]) - sent_data = json.loads(put_mock.call_args[1].get('data')) - self.assertEquals(hue, sent_data.get('desired_state', {}).get('hue')) - self.assertEquals(saturation, sent_data.get('desired_state', {}).get('saturation')) - self.assertEquals('hsb', sent_data['desired_state'].get('color_model')) - - @mock.patch('requests.put') - def test_should_send_correct_color_temperature_values_to_wink_api(self, put_mock): - bulb = WinkBulb({ - 'capabilities': { - 'fields': [{'field': 'color_model', - 'choices':["color_temperature"]}] - } - }, self.api_interface) - arbitrary_kelvin_color = 4950 - bulb.set_state(True, color_kelvin=arbitrary_kelvin_color) - sent_data = json.loads(put_mock.call_args[1].get('data')) - self.assertEquals('color_temperature', sent_data['desired_state'].get('color_model')) - self.assertEquals(arbitrary_kelvin_color, sent_data['desired_state'].get('color_temperature')) - - @mock.patch('requests.put') - def test_should_only_send_color_hsb_if_both_color_hsb_and_color_temperature_are_given(self, put_mock): - bulb = WinkBulb({ - 'capabilities': { - 'fields': [{'field': 'color_model', - 'choices':["hsb"]}] - } - }, self.api_interface) - arbitrary_kelvin_color = 4950 - bulb.set_state(True, color_kelvin=arbitrary_kelvin_color, color_hue_saturation=[0, 1]) - sent_data = json.loads(put_mock.call_args[1].get('data')) - self.assertEquals('hsb', sent_data['desired_state'].get('color_model')) - self.assertNotIn('color_temperature', sent_data['desired_state']) - - def test_device_id_should_be_number(self): - with open('{}/api_responses/light_bulb.json'.format(os.path.dirname(__file__))) as light_file: - response_dict = json.load(light_file) - light = response_dict.get('data')[0] - wink_light = WinkBulb(light, self.api_interface) - device_id = wink_light.device_id() - self.assertRegex(device_id, "^[0-9]{4,6}$") diff --git a/src/pywink/test/devices/standard/fan_test.py b/src/pywink/test/devices/standard/fan_test.py deleted file mode 100644 index 8c6930e..0000000 --- a/src/pywink/test/devices/standard/fan_test.py +++ /dev/null @@ -1,93 +0,0 @@ -import json -import os -import unittest - -import mock - -from pywink.api import get_devices_from_response_dict, WinkApiInterface -from pywink.devices import types as device_types -from pywink.devices.standard import WinkFan -from pywink.devices.types import DEVICE_ID_KEYS - - -class FanSpeedTests(unittest.TestCase): - - def test_fan_speed_should_be_lowest(self): - with open('{}/api_responses/fan.json'.format(os.path.dirname(__file__))) as fan_file: - response_dict = json.load(fan_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.FAN]) - - fan = devices[0] - """ :type fan: pywink.devices.standard.WinkFan """ - self.assertEqual(fan.current_fan_speed(), "lowest") - - def test_fan_speeds_should_be_present(self): - with open('{}/api_responses/fan.json'.format(os.path.dirname(__file__))) as fan_file: - response_dict = json.load(fan_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.FAN]) - - fan = devices[0] - """ :type fan: pywink.devices.standard.WinkFan """ - speeds = fan.fan_speeds() - self.assertTrue("lowest" in speeds) - self.assertTrue("low" in speeds) - self.assertTrue("medium" in speeds) - self.assertTrue("high" in speeds) - self.assertTrue("auto" in speeds) - - -class FanDirectionTests(unittest.TestCase): - - def test_fan_direction_should_be_forward(self): - with open('{}/api_responses/fan.json'.format(os.path.dirname(__file__))) as fan_file: - response_dict = json.load(fan_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.FAN]) - - fan = devices[0] - """ :type fan: pywink.devices.standard.WinkFan """ - self.assertEqual(fan.current_fan_direction(), "forward") - - def test_fan_directions_should_be_present(self): - with open('{}/api_responses/fan.json'.format(os.path.dirname(__file__))) as fan_file: - response_dict = json.load(fan_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.FAN]) - - fan = devices[0] - """ :type fan: pywink.devices.standard.WinkFan """ - directions = fan.fan_directions() - self.assertTrue("forward" in directions) - self.assertTrue("reverse" in directions) - -class FanTimerTests(unittest.TestCase): - - def test_fan_timer_range_should_be_present(self): - with open('{}/api_responses/fan.json'.format(os.path.dirname(__file__))) as fan_file: - response_dict = json.load(fan_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.FAN]) - - fan = devices[0] - """ :type fan: pywink.devices.standard.WinkFan """ - timer_range = fan.fan_timer_range() - self.assertTrue(0 in timer_range) - self.assertTrue(65535 in timer_range) - - def test_current_fan_timer_should_be_zero(self): - with open('{}/api_responses/fan.json'.format(os.path.dirname(__file__))) as fan_file: - response_dict = json.load(fan_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.FAN]) - - fan = devices[0] - """ :type fan: pywink.devices.standard.WinkFan """ - self.assertEqual(fan.current_timer(), 0) - - -class FanStateTests(unittest.TestCase): - - def test_fan_state_is_off(self): - with open('{}/api_responses/fan.json'.format(os.path.dirname(__file__))) as fan_file: - response_dict = json.load(fan_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.FAN]) - - fan = devices[0] - """ :type fan: pywink.devices.standard.WinkFan """ - self.assertTrue(fan.state()) diff --git a/src/pywink/test/devices/standard/init_test.py b/src/pywink/test/devices/standard/init_test.py deleted file mode 100644 index ca8c042..0000000 --- a/src/pywink/test/devices/standard/init_test.py +++ /dev/null @@ -1,698 +0,0 @@ -import json -import mock -import unittest -import os - -from pywink.api import get_devices_from_response_dict, set_bearer_token -from pywink.devices import types as device_types -from pywink.devices.sensors import WinkSensorPod, WinkBrightnessSensor, WinkHumiditySensor, \ - WinkSoundPresenceSensor, WinkVibrationPresenceSensor, WinkTemperatureSensor, \ - _WinkCapabilitySensor, WinkLiquidPresenceSensor, WinkCurrencySensor, WinkMotionSensor, \ - WinkProximitySensor, WinkPresenceSensor, WinkSmokeDetector, WinkCoDetector, \ - WinkHub, WinkDoorBellMotion, WinkDoorBellButton -from pywink.devices.standard import WinkGarageDoor, WinkPowerStripOutlet, WinkSiren, WinkLock, \ - WinkShade, WinkBinarySwitch, WinkEggTray, WinkKey, WinkPorkfolioNose -from pywink.devices.types import DEVICE_ID_KEYS -from pywink.test.devices.standard.api_responses import ApiResponseJSONLoader - - -class PowerStripTests(unittest.TestCase): - - def setUp(self): - super(PowerStripTests, self).setUp() - self.api_interface = mock.MagicMock() - - def test_should_handle_power_strip_response(self): - with open('{}/api_responses/power_strip.json'.format(os.path.dirname(__file__))) as powerstrip_file: - response_dict = json.load(powerstrip_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.POWER_STRIP]) - self.assertEqual(2, len(devices)) - self.assertIsInstance(devices[0], WinkPowerStripOutlet) - self.assertIsInstance(devices[1], WinkPowerStripOutlet) - - def test_should_show_powered_state_as_false_if_device_is_disconnected(self): - with open('{}/api_responses/power_strip.json'.format(os.path.dirname(__file__))) as powerstrip_file: - response_dict = json.load(powerstrip_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.POWER_STRIP]) - self.assertFalse(devices[0].state()) - - def test_device_id_should_be_number(self): - with open('{}/api_responses/power_strip.json'.format(os.path.dirname(__file__))) as powerstrip_file: - response_dict = json.load(powerstrip_file) - power_strip = response_dict.get('data') - outlets = power_strip[0].get('outlets') - - for outlet in outlets: - wink_outlet = WinkPowerStripOutlet(outlet, self.api_interface) - device_id = wink_outlet.device_id() - self.assertRegex(device_id, "^[0-9]{4,6}$") - - -class GarageDoorTests(unittest.TestCase): - - def setUp(self): - super(GarageDoorTests, self).setUp() - self.api_interface = mock.MagicMock() - - def test_should_handle_garage_door_opener_response(self): - with open('{}/api_responses/garage_door.json'.format(os.path.dirname(__file__))) as garage_door_file: - response_dict = json.load(garage_door_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.GARAGE_DOOR]) - self.assertEqual(1, len(devices)) - self.assertIsInstance(devices[0], WinkGarageDoor) - - def test_device_id_should_be_number(self): - with open('{}/api_responses/garage_door.json'.format(os.path.dirname(__file__))) as garage_door_file: - response_dict = json.load(garage_door_file) - garage_door = response_dict.get('data')[0] - wink_garage_door = WinkGarageDoor(garage_door, self.api_interface) - device_id = wink_garage_door.device_id() - self.assertRegex(device_id, "^[0-9]{4,6}$") - - def test_tamper_detected_should_be_false(self): - with open('{}/api_responses/garage_door.json'.format(os.path.dirname(__file__))) as garage_door_file: - response_dict = json.load(garage_door_file) - garage_door = response_dict.get('data')[0] - wink_garage_door = WinkGarageDoor(garage_door, self.api_interface) - tamper = wink_garage_door.tamper_detected - self.assertFalse(tamper) - - -class ShadeTests(unittest.TestCase): - def setUp(self): - super(ShadeTests, self).setUp() - self.api_interface = mock.MagicMock() - - def test_should_handle_shade_response(self): - with open('{}/api_responses/shade.json'.format(os.path.dirname(__file__))) as shade_file: - response_dict = json.load(shade_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SHADE]) - self.assertEqual(1, len(devices)) - self.assertIsInstance(devices[0], WinkShade) - - def test_device_id_should_be_number(self): - with open('{}/api_responses/shade.json'.format(os.path.dirname(__file__))) as shade_file: - response_dict = json.load(shade_file) - print(response_dict) - shade = response_dict.get('data')[0] - wink_shade = WinkShade(shade, self.api_interface) - device_id = wink_shade.device_id() - self.assertRegex(device_id, "^[0-9]{4,6}$") - - -class SirenTests(unittest.TestCase): - - def setUp(self): - super(SirenTests, self).setUp() - self.api_interface = mock.MagicMock() - - def test_should_handle_siren_response(self): - with open('{}/api_responses/siren.json'.format(os.path.dirname(__file__))) as siren_file: - response_dict = json.load(siren_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SIREN]) - self.assertEqual(1, len(devices)) - self.assertIsInstance(devices[0], WinkSiren) - - def test_device_id_should_be_number(self): - with open('{}/api_responses/siren.json'.format(os.path.dirname(__file__))) as siren_file: - response_dict = json.load(siren_file) - siren = response_dict.get('data')[0] - wink_siren = WinkSiren(siren, self.api_interface) - device_id = wink_siren.device_id() - self.assertRegex(device_id, "^[0-9]{4,6}$") - - def test_auto_shutoff_should_be_30(self): - with open('{}/api_responses/siren.json'.format(os.path.dirname(__file__))) as siren_file: - response_dict = json.load(siren_file) - siren = response_dict.get('data')[0] - wink_siren = WinkSiren(siren, self.api_interface) - auto_shutoff = wink_siren.auto_shutoff - self.assertEqual(auto_shutoff, 30) - - def test_mode_should_be_siren_and_strobe(self): - with open('{}/api_responses/siren.json'.format(os.path.dirname(__file__))) as siren_file: - response_dict = json.load(siren_file) - siren = response_dict.get('data')[0] - wink_siren = WinkSiren(siren, self.api_interface) - mode = wink_siren.mode - self.assertEqual(mode, "siren_and_strobe") - - -class LockTests(unittest.TestCase): - - def setUp(self): - super(LockTests, self).setUp() - self.api_interface = mock.MagicMock() - - def test_should_handle_lock_response(self): - with open('{}/api_responses/lock.json'.format(os.path.dirname(__file__))) as lock_file: - response_dict = json.load(lock_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.LOCK]) - self.assertEqual(1, len(devices)) - self.assertIsInstance(devices[0], WinkLock) - - def test_device_id_should_be_number(self): - with open('{}/api_responses/lock.json'.format(os.path.dirname(__file__))) as lock_file: - response_dict = json.load(lock_file) - lock = response_dict.get('data')[0] - wink_lock = WinkLock(lock, self.api_interface) - device_id = wink_lock.device_id() - self.assertRegex(device_id, "^[0-9]{4,6}$") - - def test_alarm_mode_should_be_null(self): - with open('{}/api_responses/lock.json'.format(os.path.dirname(__file__))) as lock_file: - response_dict = json.load(lock_file) - lock = response_dict.get('data')[0] - wink_lock = WinkLock(lock, self.api_interface) - alarm_mode = wink_lock.alarm_mode - self.assertEqual(alarm_mode, None) - - def test_alarm_sensitivity_should_be_6(self): - with open('{}/api_responses/lock.json'.format(os.path.dirname(__file__))) as lock_file: - response_dict = json.load(lock_file) - lock = response_dict.get('data')[0] - wink_lock = WinkLock(lock, self.api_interface) - alarm_sensitivity = wink_lock.alarm_sensitivity - self.assertEqual(alarm_sensitivity, 0.6) - - def test_alarm_enabled_should_be_true(self): - with open('{}/api_responses/lock.json'.format(os.path.dirname(__file__))) as lock_file: - response_dict = json.load(lock_file) - lock = response_dict.get('data')[0] - wink_lock = WinkLock(lock, self.api_interface) - alarm_enabled = wink_lock.alarm_enabled - self.assertTrue(alarm_enabled) - - def test_beeper_enabled_should_be_true(self): - with open('{}/api_responses/lock.json'.format(os.path.dirname(__file__))) as lock_file: - response_dict = json.load(lock_file) - lock = response_dict.get('data')[0] - wink_lock = WinkLock(lock, self.api_interface) - beeper_enabled = wink_lock.beeper_enabled - self.assertTrue(beeper_enabled) - - def test_vacation_mode_enabled_should_be_false(self): - with open('{}/api_responses/lock.json'.format(os.path.dirname(__file__))) as lock_file: - response_dict = json.load(lock_file) - lock = response_dict.get('data')[0] - wink_lock = WinkLock(lock, self.api_interface) - vacation_mode_enabled = wink_lock.vacation_mode_enabled - self.assertFalse(vacation_mode_enabled) - - -class BinarySwitchTests(unittest.TestCase): - - def setUp(self): - super(BinarySwitchTests, self).setUp() - self.api_interface = mock.MagicMock() - - def test_should_handle_binary_switch_response(self): - with open('{}/api_responses/binary_switch.json'.format(os.path.dirname(__file__))) as binary_switch_file: - response_dict = json.load(binary_switch_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.BINARY_SWITCH]) - self.assertEqual(1, len(devices)) - self.assertIsInstance(devices[0], WinkBinarySwitch) - - def test_device_id_should_be_number(self): - with open('{}/api_responses/binary_switch.json'.format(os.path.dirname(__file__))) as binary_switch_file: - response_dict = json.load(binary_switch_file) - switch = response_dict.get('data')[0] - wink_switch = WinkBinarySwitch(switch, self.api_interface) - device_id = wink_switch.device_id() - self.assertRegex(device_id, "^[0-9]{4,6}$") - - def test_ge_switch_should_be_identified(self): - response = ApiResponseJSONLoader('light_switch_ge_jasco_z_wave.json').load() - devices = get_devices_from_response_dict(response, DEVICE_ID_KEYS[device_types.BINARY_SWITCH]) - self.assertEqual(1, len(devices)) - self.assertIsInstance(devices[0], WinkBinarySwitch) - - -class BinarySensorTests(unittest.TestCase): - - def setUp(self): - super(BinarySensorTests, self).setUp() - self.api_interface = mock.MagicMock() - - def test_should_handle_sensor_pod_response(self): - with open('{}/api_responses/binary_sensor.json'.format(os.path.dirname(__file__))) as binary_sensor_file: - response_dict = json.load(binary_sensor_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SENSOR_POD]) - self.assertEqual(1, len(devices)) - self.assertIsInstance(devices[0], WinkSensorPod) - - def test_device_id_should_be_number(self): - with open('{}/api_responses/binary_sensor.json'.format(os.path.dirname(__file__))) as binary_sensor_file: - response_dict = json.load(binary_sensor_file) - sensor = response_dict.get('data')[0] - wink_binary_sensor = WinkSensorPod(sensor, self.api_interface) - device_id = wink_binary_sensor.device_id() - self.assertRegex(device_id, "^[0-9]{4,6}$") - - -class EggtrayTests(unittest.TestCase): - - def setUp(self): - super(EggtrayTests, self).setUp() - self.api_interface = mock.MagicMock() - - def test_should_handle_egg_tray_response(self): - with open('{}/api_responses/eggtray.json'.format(os.path.dirname(__file__))) as eggtray_file: - response_dict = json.load(eggtray_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.EGG_TRAY]) - self.assertEqual(1, len(devices)) - self.assertIsInstance(devices[0], WinkEggTray) - - def test_device_id_should_be_number(self): - with open('{}/api_responses/eggtray.json'.format(os.path.dirname(__file__))) as eggtray_file: - response_dict = json.load(eggtray_file) - eggtray = response_dict.get('data')[0] - wink_eggtray = WinkEggTray(eggtray, self.api_interface) - device_id = wink_eggtray.device_id() - self.assertRegex(device_id, "^[0-9]{4,6}$") - - -class SensorTests(unittest.TestCase): - - def setUp(self): - super(SensorTests, self).setUp() - self.api_interface = mock.MagicMock() - - def test_quirky_spotter_api_response_should_create_unique_one_primary_sensor_and_five_subsensors(self): - with open('{}/api_responses/quirky_spotter.json'.format(os.path.dirname(__file__))) as spotter_file: - response_dict = json.load(spotter_file) - - sensors = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SENSOR_POD]) - self.assertEquals(5, len(sensors)) - - def test_alternative_quirky_spotter_api_response_should_create_one_primary_sensor_and_five_subsensors(self): - with open('{}/api_responses/quirky_spotter_2.json'.format(os.path.dirname(__file__))) as spotter_file: - response_dict = json.load(spotter_file) - - sensors = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SENSOR_POD]) - self.assertEquals(5, len(sensors)) - - def test_brightness_should_have_correct_value(self): - with open('{}/api_responses/quirky_spotter.json'.format(os.path.dirname(__file__))) as spotter_file: - response_dict = json.load(spotter_file) - - sensors = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SENSOR_POD]) - """:type : list of WinkBrightnessSensor""" - brightness_sensor = [sensor for sensor in sensors if sensor.capability() is WinkBrightnessSensor.CAPABILITY][0] - expected_brightness = 1 - self.assertEquals(expected_brightness, brightness_sensor.brightness_boolean()) - - def test_humidity_should_have_correct_value(self): - with open('{}/api_responses/quirky_spotter.json'.format(os.path.dirname(__file__))) as spotter_file: - response_dict = json.load(spotter_file) - - sensors = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SENSOR_POD]) - """:type : list of WinkHumiditySensor""" - humidity_sensor = [sensor for sensor in sensors if sensor.capability() is WinkHumiditySensor.CAPABILITY][0] - expected_humidity = 48 - self.assertEquals(expected_humidity, humidity_sensor.humidity_percentage()) - - def test_loudness_should_have_correct_value(self): - with open('{}/api_responses/quirky_spotter.json'.format(os.path.dirname(__file__))) as spotter_file: - response_dict = json.load(spotter_file) - - sensors = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SENSOR_POD]) - """:type : list of WinkSoundPresenceSensor""" - sound_sensor = [sensor for sensor in sensors if sensor.capability() is WinkSoundPresenceSensor.CAPABILITY][0] - expected_sound_presence = False - self.assertEquals(expected_sound_presence, sound_sensor.loudness_boolean()) - - def test_vibration_should_have_correct_value(self): - with open('{}/api_responses/quirky_spotter.json'.format(os.path.dirname(__file__))) as spotter_file: - response_dict = json.load(spotter_file) - - sensors = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SENSOR_POD]) - """:type : list of WinkVibrationPresenceSensor""" - vibration_sensor = [sensor for sensor in sensors if sensor.capability() is WinkVibrationPresenceSensor.CAPABILITY][0] - expected_vibrartion_presence = False - self.assertEquals(expected_vibrartion_presence, vibration_sensor.vibration_boolean()) - - def test_temperature_should_have_correct_value(self): - with open('{}/api_responses/quirky_spotter.json'.format(os.path.dirname(__file__))) as spotter_file: - response_dict = json.load(spotter_file) - - sensors = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SENSOR_POD]) - """:type : list of WinkTemperatureSensor""" - temp_sensor = [sensor for sensor in sensors if sensor.capability() is WinkTemperatureSensor.CAPABILITY][0] - expected_temperature = 5 - self.assertEquals(expected_temperature, temp_sensor.temperature_float()) - - def test_device_id_should_start_with_a_number(self): - with open('{}/api_responses/quirky_spotter.json'.format(os.path.dirname(__file__))) as spotter_file: - response_dict = json.load(spotter_file) - - sensors = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SENSOR_POD]) - - for sensor in sensors: - device_id = sensor.device_id() - self.assertRegex(device_id, "^[0-9]{4,6}") - - def test_battery_level_should_return_none(self): - with open('{}/api_responses/quirky_spotter.json'.format(os.path.dirname(__file__))) as spotter_file: - response_dict = json.load(spotter_file) - - sensors = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SENSOR_POD]) - - for sensor in sensors: - self.assertIsNone(sensor.battery_level) - - def test_battery_level_should_return_float(self): - with open('{}/api_responses/quirky_spotter_2.json'.format(os.path.dirname(__file__))) as spotter_file: - response_dict = json.load(spotter_file) - - sensors = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SENSOR_POD]) - - for sensor in sensors: - self.assertEqual(sensor.battery_level, 0.86) - - def test_sensor_tamper_detected_should_be_false(self): - response = ApiResponseJSONLoader('door_sensor_gocontrol.json').load() - devices = get_devices_from_response_dict(response, - DEVICE_ID_KEYS[ - device_types.SENSOR_POD]) - tamper = devices[0].tamper_detected - self.assertFalse(tamper) - - def test_gocontrol_door_sensor_should_be_identified(self): - response = ApiResponseJSONLoader('door_sensor_gocontrol.json').load() - devices = get_devices_from_response_dict(response, - DEVICE_ID_KEYS[ - device_types.SENSOR_POD]) - self.assertEqual(1, len(devices)) - self.assertIsInstance(devices[0], WinkSensorPod) - - def test_gocontrol_motion_sensor_should_be_identified(self): - response = ApiResponseJSONLoader('motion_sensor_gocontrol.json').load() - devices = get_devices_from_response_dict(response, - DEVICE_ID_KEYS[ - device_types.SENSOR_POD]) - self.assertEqual(2, len(devices)) - self.assertIsInstance(devices[1], WinkMotionSensor) - self.assertIsInstance(devices[0], WinkTemperatureSensor) - - def test_humidity_is_percentage_after_update(self): - with open('{}/api_responses/quirky_spotter.json'.format(os.path.dirname(__file__))) as spotter_file: - response_dict = json.load(spotter_file) - - sensors = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SENSOR_POD]) - """:type : list of WinkHumiditySensor""" - humidity_sensor = [sensor for sensor in sensors if sensor.capability() is WinkHumiditySensor.CAPABILITY][0] - - with open('{}/api_responses/quirky_spotter_pubnub.json'.format(os.path.dirname(__file__))) as spotter_file: - update_response_dict = json.load(spotter_file) - - humidity_sensor.pubnub_update(update_response_dict) - expected_humidity = 24 - self.assertEquals(expected_humidity, humidity_sensor.humidity_percentage()) - - def test_liquid_detected_should_have_correct_value(self): - with open('{}/api_responses/liquid_sensor.json'.format(os.path.dirname(__file__))) as spotter_file: - response_dict = json.load(spotter_file) - - sensors = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SENSOR_POD]) - """:type : list of WinkLiquidPresenceSensor""" - liquid_sensor = [sensor for sensor in sensors if sensor.capability() is WinkLiquidPresenceSensor.CAPABILITY][0] - expected_liquid_presence = False - self.assertEquals(expected_liquid_presence, liquid_sensor.liquid_boolean()) - - -class WinkCapabilitySensorTests(unittest.TestCase): - - def setUp(self): - super(WinkCapabilitySensorTests, self).setUp() - self.api_interface = mock.MagicMock() - - def test_should_call_get_state_endpoint_with_capability_removed_from_id(self): - expected_id = '72503' - unit = 'DEG' # mock doesn't like unicode - capability = "Test" - sensor = _WinkCapabilitySensor({ - 'sensor_pod_id': expected_id - }, self.api_interface, unit, capability) - - sensor.update_state() - self.api_interface.get_device_state.assert_called_once_with(sensor, expected_id) - - -class WinkPubnubTests(unittest.TestCase): - - def setUp(self): - super(WinkPubnubTests, self).setUp() - self.api_interface = mock.MagicMock() - - def test_pubnub_key_and_channel_should_not_be_none(self): - with open('{}/api_responses/device_with_pubnub.json'.format(os.path.dirname(__file__))) as lock_file: - response_dict = json.load(lock_file) - device = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.LOCK])[0] - - self.assertIsNotNone(device.pubnub_key) - self.assertIsNotNone(device.pubnub_channel) - - def test_pubnub_key_and_channel_should_be_none(self): - with open('{}/api_responses/lock.json'.format(os.path.dirname(__file__))) as lock_file: - response_dict = json.load(lock_file) - device = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.LOCK])[0] - - self.assertIsNone(device.pubnub_key) - self.assertIsNone(device.pubnub_channel) - - def test_pywink_api_pubnub_subscription_key_is_not_none(self): - with open('{}/api_responses/device_with_pubnub.json'.format(os.path.dirname(__file__))) as lock_file: - response_dict = json.load(lock_file) - - self.assertIsNotNone(self.api_interface.get_subscription_key_from_response_dict(response_dict)) - - -class WinkKeyTests(unittest.TestCase): - - def setUp(self): - super(WinkKeyTests, self).setUp() - self.api_interface = mock.MagicMock() - - def test_device_id_should_be_number(self): - with open('{}/api_responses/key.json'.format(os.path.dirname(__file__))) as keys_file: - response_dict = json.load(keys_file) - key = response_dict.get('data') - - wink_key = WinkKey(key, self.api_interface) - device_id = wink_key.device_id() - self.assertRegex(device_id, "^[0-9]{4,6}$") - - def test_state_should_be_true_or_false(self): - with open('{}/api_responses/key.json'.format(os.path.dirname(__file__))) as keys_file: - response_dict = json.load(keys_file) - key = response_dict.get('data') - - wink_true_key = WinkKey(key, self.api_interface) - self.assertTrue(wink_true_key.state()) - - -class PorkfolioTests(unittest.TestCase): - - def setUp(self): - super(PorkfolioTests, self).setUp() - self.api_interface = mock.MagicMock() - - def test_should_handle_porkfolio_response(self): - with open('{}/api_responses/porkfolio.json'.format(os.path.dirname(__file__))) as porkfolio_file: - response_dict = json.load(porkfolio_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.PIGGY_BANK]) - self.assertEqual(2, len(devices)) - self.assertIsInstance(devices[0], WinkCurrencySensor) - self.assertIsInstance(devices[1], WinkPorkfolioNose) - - def test_device_id_should_be_number(self): - with open('{}/api_responses/porkfolio.json'.format(os.path.dirname(__file__))) as porkfolio_file: - response_dict = json.load(porkfolio_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.PIGGY_BANK]) - device_id = devices[0].device_id() - self.assertRegex(device_id, "^[0-9]{4,6}") - - device_id = devices[1].device_id() - self.assertRegex(device_id, "^[0-9]{4,6}") - - def test_objectprefix_should_be_correct(self): - with open('{}/api_responses/porkfolio.json'.format(os.path.dirname(__file__))) as porkfolio_file: - response_dict = json.load(porkfolio_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.PIGGY_BANK]) - objectprefix = devices[0].objectprefix - self.assertRegex(objectprefix, "piggy_bank") - objectprefix = devices[1].objectprefix - self.assertRegex(objectprefix, "piggy_bank") - -class RelaySensorTests(unittest.TestCase): - - def setUp(self): - super(RelaySensorTests, self).setUp() - self.api_interface = mock.MagicMock() - - def test_should_handle_relay_response(self): - with open('{}/api_responses/wink_relay_sensor.json'.format(os.path.dirname(__file__))) as relay_file: - response_dict = json.load(relay_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SENSOR_POD]) - self.assertEqual(4, len(devices)) - self.assertIsInstance(devices[0], WinkHumiditySensor) - self.assertIsInstance(devices[1], WinkTemperatureSensor) - self.assertIsInstance(devices[2], WinkPresenceSensor) - self.assertIsInstance(devices[3], WinkProximitySensor) - - def test_should_convert_humidity_to_percentage(self): - with open('{}/api_responses/wink_relay_sensor.json'.format(os.path.dirname(__file__))) as relay_file: - response_dict = json.load(relay_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SENSOR_POD]) - self.assertEqual(devices[0].humidity_percentage(), 69) - - -class SmokeDetectorTests(unittest.TestCase): - - def setUp(self): - super(SmokeDetectorTests, self).setUp() - self.api_interface = mock.MagicMock() - - def test_should_handle_smoke_detector_response(self): - with open('{}/api_responses/smoke_detector.json'.format(os.path.dirname(__file__))) as smoke_detector_file: - response_dict = json.load(smoke_detector_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SMOKE_DETECTOR]) - self.assertEqual(2, len(devices)) - smoke = devices[0] - co = devices[1] - self.assertIsInstance(smoke, WinkSmokeDetector) - self.assertIsInstance(co, WinkCoDetector) - self.assertFalse(smoke.smoke_detected_boolean()) - self.assertTrue(co.co_detected_boolean()) - - def test_device_id_should_be_number(self): - with open('{}/api_responses/smoke_detector.json'.format(os.path.dirname(__file__))) as smoke_detector_file: - response_dict = json.load(smoke_detector_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SMOKE_DETECTOR]) - device_id = devices[0].device_id() - self.assertRegex(device_id, "^[0-9]{4,6}") - - def test_objectprefix_should_be_correct(self): - with open('{}/api_responses/smoke_detector.json'.format(os.path.dirname(__file__))) as smoke_detector_file: - response_dict = json.load(smoke_detector_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.SMOKE_DETECTOR]) - objectprefix = devices[0].objectprefix - self.assertRegex(objectprefix, "smoke_detectors") - objectprefix = devices[1].objectprefix - self.assertRegex(objectprefix, "smoke_detectors") - - -class HubTests(unittest.TestCase): - - def setUp(self): - super(HubTests, self).setUp() - self.api_interface = mock.MagicMock() - - def test_should_handle_hub_response(self): - with open('{}/api_responses/v1_hub.json'.format(os.path.dirname(__file__))) as hub_file: - response_dict = json.load(hub_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.HUB]) - self.assertEqual(1, len(devices)) - self.assertIsInstance(devices[0], WinkHub) - - def test_device_id_should_be_number(self): - with open('{}/api_responses/v1_hub.json'.format(os.path.dirname(__file__))) as hub_file: - response_dict = json.load(hub_file) - hub = response_dict.get('data')[0] - wink_hub = WinkHub(hub, self.api_interface) - device_id = wink_hub.device_id() - self.assertRegex(device_id, "^[0-9]{4,6}") - - def test_kidde_radio_code(self): - with open('{}/api_responses/v1_hub.json'.format(os.path.dirname(__file__))) as hub_file: - response_dict = json.load(hub_file) - hub = response_dict.get('data')[0] - wink_hub = WinkHub(hub, self.api_interface) - code = wink_hub.kidde_radio_code() - self.assertEqual(code, 0) - - def test_update_needed(self): - with open('{}/api_responses/v1_hub.json'.format(os.path.dirname(__file__))) as hub_file: - response_dict = json.load(hub_file) - hub = response_dict.get('data')[0] - wink_hub = WinkHub(hub, self.api_interface) - update = wink_hub.update_needed() - self.assertFalse(update) - - def test_ip_address(self): - with open('{}/api_responses/v1_hub.json'.format(os.path.dirname(__file__))) as hub_file: - response_dict = json.load(hub_file) - hub = response_dict.get('data')[0] - wink_hub = WinkHub(hub, self.api_interface) - ip = wink_hub.ip_address() - self.assertEqual(ip, '192.168.1.2') - - def test_firmware_version(self): - with open('{}/api_responses/v1_hub.json'.format(os.path.dirname(__file__))) as hub_file: - response_dict = json.load(hub_file) - hub = response_dict.get('data')[0] - wink_hub = WinkHub(hub, self.api_interface) - firmware = wink_hub.firmware_version() - self.assertEqual(firmware, '3.3.26-0-gf4fa1428f9') - - def test_manufacturer_device_id(self): - with open('{}/api_responses/v1_hub.json'.format(os.path.dirname(__file__))) as hub_file: - response_dict = json.load(hub_file) - hub = response_dict.get('data')[0] - wink_hub = WinkHub(hub, self.api_interface) - id = wink_hub.manufacturer_device_id - self.assertEqual(id, None) - - def test_manufacturer(self): - with open('{}/api_responses/v1_hub.json'.format(os.path.dirname(__file__))) as hub_file: - response_dict = json.load(hub_file) - hub = response_dict.get('data')[0] - wink_hub = WinkHub(hub, self.api_interface) - manufacturer = wink_hub.device_manufacturer - self.assertEqual(manufacturer, 'wink') - - def test_model(self): - with open('{}/api_responses/v1_hub.json'.format(os.path.dirname(__file__))) as hub_file: - response_dict = json.load(hub_file) - hub = response_dict.get('data')[0] - wink_hub = WinkHub(hub, self.api_interface) - model = wink_hub.manufacturer_device_model - self.assertEqual(model, 'wink_hub') - - def test_model_name(self): - with open('{}/api_responses/v1_hub.json'.format(os.path.dirname(__file__))) as hub_file: - response_dict = json.load(hub_file) - hub = response_dict.get('data')[0] - wink_hub = WinkHub(hub, self.api_interface) - model_name = wink_hub.model_name - self.assertEqual(model_name, 'Hub') - - -class DoorBellTests(unittest.TestCase): - - def setUp(self): - super(DoorBellTests, self).setUp() - self.api_interface = mock.MagicMock() - - def test_should_handle_door_bell_response(self): - with open('{}/api_responses/ring_door_bell.json'.format(os.path.dirname(__file__))) as door_bell_file: - response_dict = json.load(door_bell_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.DOOR_BELL]) - self.assertEqual(2, len(devices)) - self.assertIsInstance(devices[0], WinkDoorBellMotion) - self.assertIsInstance(devices[1], WinkDoorBellButton) - - def test_door_bell_motion_should_be_false(self): - with open('{}/api_responses/ring_door_bell.json'.format(os.path.dirname(__file__))) as door_bell_file: - response_dict = json.load(door_bell_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.DOOR_BELL]) - door_bell_motion = devices[0].motion_boolean() - self.assertFalse(door_bell_motion) - - def test_door_bell_button_pressed_should_be_false(self): - with open('{}/api_responses/ring_door_bell.json'.format(os.path.dirname(__file__))) as door_bell_file: - response_dict = json.load(door_bell_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.DOOR_BELL]) - door_bell_button_pressed = devices[1].button_pressed_boolean() - self.assertFalse(door_bell_button_pressed) diff --git a/src/pywink/test/devices/standard/thermostat_test.py b/src/pywink/test/devices/standard/thermostat_test.py deleted file mode 100644 index 483d9f8..0000000 --- a/src/pywink/test/devices/standard/thermostat_test.py +++ /dev/null @@ -1,299 +0,0 @@ -import json -import os -import unittest - -import mock - -from pywink.api import get_devices_from_response_dict, WinkApiInterface -from pywink.devices import types as device_types -from pywink.devices.standard import WinkThermostat -from pywink.devices.types import DEVICE_ID_KEYS - - -class ThermostatModeTests(unittest.TestCase): - - def test_should_be_true_if_thermostat_is_on(self): - with open('{}/api_responses/nest.json'.format(os.path.dirname(__file__))) as thermostat_file: - response_dict = json.load(thermostat_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) - - thermostat = devices[0] - """ :type thermostat: pywink.devices.standard.WinkThermostat """ - self.assertTrue(thermostat.is_on()) - - def test_should_be_true_if_response_contains_heat_capabilities(self): - with open('{}/api_responses/nest.json'.format(os.path.dirname(__file__))) as thermostat_file: - response_dict = json.load(thermostat_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) - - thermostat = devices[0] - """ :type thermostat: pywink.devices.standard.WinkThermostat """ - self.assertTrue('heat_only' in thermostat.hvac_modes()) - - def test_should_be_true_if_response_contains_cool_capabilities(self): - with open('{}/api_responses/nest.json'.format(os.path.dirname(__file__))) as thermostat_file: - response_dict = json.load(thermostat_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) - - thermostat = devices[0] - """ :type thermostat: pywink.devices.standard.WinkThermostat """ - self.assertTrue('cool_only' in thermostat.hvac_modes()) - - def test_should_be_true_if_response_contains_auto_capabilities(self): - with open('{}/api_responses/nest.json'.format(os.path.dirname(__file__))) as thermostat_file: - response_dict = json.load(thermostat_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) - - thermostat = devices[0] - """ :type thermostat: pywink.devices.standard.WinkThermostat """ - self.assertTrue('auto' in thermostat.hvac_modes()) - - def test_should_be_false_if_response_doesnt_contains_aux_capabilities(self): - with open('{}/api_responses/nest.json'.format(os.path.dirname(__file__))) as thermostat_file: - response_dict = json.load(thermostat_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) - - thermostat = devices[0] - """ :type thermostat: pywink.devices.standard.WinkThermostat """ - self.assertFalse('aux' in thermostat.hvac_modes()) - - def test_should_be_cool_only_for_current_hvac_mode(self): - with open('{}/api_responses/sensi.json'.format(os.path.dirname(__file__))) as thermostat_file: - response_dict = json.load(thermostat_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) - - thermostat = devices[0] - """ :type thermostat: pywink.devices.standard.WinkThermostat """ - self.assertEqual('cool_only', thermostat.current_hvac_mode()) - -class ThermostatFanTests(unittest.TestCase): - - def test_should_be_true_if_response_contains_fan_on_capabilities(self): - with open('{}/api_responses/sensi.json'.format(os.path.dirname(__file__))) as thermostat_file: - response_dict = json.load(thermostat_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) - - thermostat = devices[0] - """ :type thermostat: pywink.devices.standard.WinkThermostat """ - self.assertTrue('on' in thermostat.fan_modes()) - - def test_should_be_true_if_response_contains_fan_auto_capabilities(self): - with open('{}/api_responses/sensi.json'.format(os.path.dirname(__file__))) as thermostat_file: - response_dict = json.load(thermostat_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) - - thermostat = devices[0] - """ :type thermostat: pywink.devices.standard.WinkThermostat """ - self.assertTrue('auto' in thermostat.fan_modes()) - - def test_should_be_true_if_response_contains_fan_on_capabilities(self): - with open('{}/api_responses/sensi.json'.format(os.path.dirname(__file__))) as thermostat_file: - response_dict = json.load(thermostat_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) - - thermostat = devices[0] - """ :type thermostat: pywink.devices.standard.WinkThermostat """ - self.assertTrue('auto' in thermostat.fan_modes()) - - def test_should_be_true_if_thermostat_fan_is_on(self): - with open('{}/api_responses/sensi.json'.format(os.path.dirname(__file__))) as thermostat_file: - response_dict = json.load(thermostat_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) - - thermostat = devices[0] - """ :type thermostat: pywink.devices.standard.WinkThermostat """ - self.assertTrue(thermostat.fan_on()) - - def test_should_be_true_if_thermostat_has_fan(self): - with open('{}/api_responses/sensi.json'.format(os.path.dirname(__file__))) as thermostat_file: - response_dict = json.load(thermostat_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) - - thermostat = devices[0] - """ :type thermostat: pywink.devices.standard.WinkThermostat """ - self.assertTrue(thermostat.has_fan()) - - def test_should_be_auto_for_current_fan_mode(self): - with open('{}/api_responses/sensi.json'.format(os.path.dirname(__file__))) as thermostat_file: - response_dict = json.load(thermostat_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) - - thermostat = devices[0] - """ :type thermostat: pywink.devices.standard.WinkThermostat """ - self.assertEqual('auto', thermostat.current_fan_mode()) - - -class ThermostatAvailableOptionsTests(unittest.TestCase): - - def test_should_be_true_if_thermostat_has_detected_occupancy(self): - # sensi.json been faked to add in the occupied field for testing. - with open('{}/api_responses/sensi.json'.format(os.path.dirname(__file__))) as thermostat_file: - response_dict = json.load(thermostat_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) - - thermostat = devices[0] - """ :type thermostat: pywink.devices.standard.WinkThermostat """ - self.assertTrue(thermostat.occupied()) - - def test_should_be_true_if_thermostat_set_to_eco_mode(self): - with open('{}/api_responses/sensi.json'.format(os.path.dirname(__file__))) as thermostat_file: - response_dict = json.load(thermostat_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) - - thermostat = devices[0] - """ :type thermostat: pywink.devices.standard.WinkThermostat """ - self.assertFalse(thermostat.eco_target()) - - def test_should_be_true_if_thermostat_set_to_away(self): - with open('{}/api_responses/sensi.json'.format(os.path.dirname(__file__))) as thermostat_file: - response_dict = json.load(thermostat_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) - - thermostat = devices[0] - """ :type thermostat: pywink.devices.standard.WinkThermostat """ - self.assertFalse(thermostat.away()) - - def test_current_units_should_be_f(self): - with open('{}/api_responses/sensi.json'.format(os.path.dirname(__file__))) as thermostat_file: - response_dict = json.load(thermostat_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) - - thermostat = devices[0] - """ :type thermostat: pywink.devices.standard.WinkThermostat """ - self.assertEqual('f', thermostat.current_units()) - -class ThermostatTemperatureTests(unittest.TestCase): - - def test_set_point_limits(self): - with open('{}/api_responses/sensi.json'.format(os.path.dirname(__file__))) as thermostat_file: - response_dict = json.load(thermostat_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) - - thermostat = devices[0] - """ :type thermostat: pywink.devices.standard.WinkThermostat """ - self.assertEqual(7.222222222222222, thermostat.min_min_set_point()) - self.assertEqual(7.222222222222222, thermostat.min_max_set_point()) - self.assertEqual(37.22222222222222, thermostat.max_min_set_point()) - self.assertEqual(37.22222222222222, thermostat.max_max_set_point()) - - def test_deadband(self): - with open('{}/api_responses/sensi.json'.format(os.path.dirname(__file__))) as thermostat_file: - response_dict = json.load(thermostat_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) - - thermostat = devices[0] - """ :type thermostat: pywink.devices.standard.WinkThermostat """ - self.assertEqual(1.1111111111111112, thermostat.deadband()) - - def test_current_external_temp(self): - with open('{}/api_responses/sensi.json'.format(os.path.dirname(__file__))) as thermostat_file: - response_dict = json.load(thermostat_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) - - thermostat = devices[0] - """ :type thermostat: pywink.devices.standard.WinkThermostat """ - self.assertEqual(16.1, thermostat.current_external_temperature()) - - def test_current_temp(self): - with open('{}/api_responses/sensi.json'.format(os.path.dirname(__file__))) as thermostat_file: - response_dict = json.load(thermostat_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) - - thermostat = devices[0] - """ :type thermostat: pywink.devices.standard.WinkThermostat """ - self.assertEqual(20.555555555555557, thermostat.current_temperature()) - - def test_current_max_set_point(self): - with open('{}/api_responses/sensi.json'.format(os.path.dirname(__file__))) as thermostat_file: - response_dict = json.load(thermostat_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) - - thermostat = devices[0] - """ :type thermostat: pywink.devices.standard.WinkThermostat """ - self.assertEqual(22.22222222222222, thermostat.current_max_set_point()) - - def test_current_min_set_point(self): - with open('{}/api_responses/sensi.json'.format(os.path.dirname(__file__))) as thermostat_file: - response_dict = json.load(thermostat_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) - - thermostat = devices[0] - """ :type thermostat: pywink.devices.standard.WinkThermostat """ - self.assertEqual(22.22222222222222, thermostat.current_min_set_point()) - - def test_current_smart_temperature(self): - # This result is only present on ecobee thermostats, the sensi.json has - # been faked to add in the smart_temperature field for testing. - with open('{}/api_responses/sensi.json'.format(os.path.dirname(__file__))) as thermostat_file: - response_dict = json.load(thermostat_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) - - thermostat = devices[0] - """ :type thermostat: pywink.devices.standard.WinkThermostat """ - self.assertEqual(20.555555555555557, thermostat.current_smart_temperature()) - - -class ThermostatHumidityTests(unittest.TestCase): - - def test_current_humidity(self): - with open('{}/api_responses/sensi.json'.format(os.path.dirname(__file__))) as thermostat_file: - response_dict = json.load(thermostat_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) - - thermostat = devices[0] - """ :type thermostat: pywink.devices.standard.WinkThermostat """ - self.assertEqual(40, thermostat.current_humidity()) - - def test_current_humidifier_mode(self): - # sensi.json been faked to add in the humidifier_mode field for testing. - with open('{}/api_responses/sensi.json'.format(os.path.dirname(__file__))) as thermostat_file: - response_dict = json.load(thermostat_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) - - thermostat = devices[0] - """ :type thermostat: pywink.devices.standard.WinkThermostat """ - self.assertEqual('auto', thermostat.current_humidifier_mode()) - - def test_current_dehumidifier_mode(self): - # sensi.json been faked to add in the dehumidifier_mode field - # for testing. - with open('{}/api_responses/sensi.json'.format(os.path.dirname(__file__))) as thermostat_file: - response_dict = json.load(thermostat_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) - - thermostat = devices[0] - """ :type thermostat: pywink.devices.standard.WinkThermostat """ - self.assertEqual('auto', thermostat.current_dehumidifier_mode()) - - def test_current_humidifier_set_point(self): - # sensi.json has been faked to add in the humidifier_set_point - # field for testing. - with open('{}/api_responses/sensi.json'.format(os.path.dirname(__file__))) as thermostat_file: - response_dict = json.load(thermostat_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) - - thermostat = devices[0] - """ :type thermostat: pywink.devices.standard.WinkThermostat """ - self.assertEqual(0.2, thermostat.current_humidifier_set_point()) - - def test_current_dehumidifier_set_point(self): - # sensi.json has been faked to add in the dehumidifier_set_point - # field for testing. - with open('{}/api_responses/sensi.json'.format(os.path.dirname(__file__))) as thermostat_file: - response_dict = json.load(thermostat_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) - - thermostat = devices[0] - """ :type thermostat: pywink.devices.standard.WinkThermostat """ - self.assertEqual(0.6, thermostat.current_dehumidifier_set_point()) - -class GenericZwaveThermostatTests(unittest.TestCase): - - def test_should_be_true_if_response_contains_aux_capabilities(self): - with open('{}/api_responses/gocontrol_thermostat.json'.format(os.path.dirname(__file__))) as thermostat_file: - response_dict = json.load(thermostat_file) - devices = get_devices_from_response_dict(response_dict, DEVICE_ID_KEYS[device_types.THERMOSTAT]) - - thermostat = devices[0] - """ :type thermostat: pywink.devices.standard.WinkThermostat """ - self.assertTrue('aux' in thermostat.hvac_modes()) diff --git a/src/pywink/test/devices/switch_test.py b/src/pywink/test/devices/switch_test.py new file mode 100644 index 0000000..dfde93f --- /dev/null +++ b/src/pywink/test/devices/switch_test.py @@ -0,0 +1,21 @@ +import json +import os +import unittest + +import mock + +from pywink.api import get_devices_from_response_dict, WinkApiInterface +from pywink.devices import types as device_types +from pywink.devices.binary_switch import WinkBinarySwitch + + +class BinarySwitchTests(unittest.TestCase): + + def test_state_should_be_false(self): + with open('{}/api_responses/ge_zwave_switch.json'.format(os.path.dirname(__file__))) as binary_switch_file: + response_dict = json.load(binary_switch_file) + response_dict = {"data": [response_dict]} + devices = get_devices_from_response_dict(response_dict, device_types.BINARY_SWITCH) + + switch = devices[0] + self.assertFalse(switch.state()) diff --git a/src/pywink/test/devices/thermostat_test.py b/src/pywink/test/devices/thermostat_test.py new file mode 100644 index 0000000..1772f02 --- /dev/null +++ b/src/pywink/test/devices/thermostat_test.py @@ -0,0 +1,300 @@ +import json +import os +import unittest + +import mock + +from pywink.api import get_devices_from_response_dict, WinkApiInterface +from pywink.devices import types as device_types +from pywink.devices.thermostat import WinkThermostat + + +class FanTests(unittest.TestCase): + + def setUp(self): + super(FanTests, self).setUp() + self.api_interface = mock.MagicMock() + + def test_thermostat_state(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/nest.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + thermostat = get_devices_from_response_dict(response_dict, device_types.THERMOSTAT)[0] + self.assertEqual(thermostat.state(), "heat_only") + + def test_thermostat_fan_modes(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/go_control_thermostat.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + thermostat = get_devices_from_response_dict(response_dict, device_types.THERMOSTAT)[0] + self.assertEqual(thermostat.fan_modes(), ["on", "auto"]) + + def test_thermostat_hvac_modes(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/go_control_thermostat.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + thermostat = get_devices_from_response_dict(response_dict, device_types.THERMOSTAT)[0] + self.assertEqual(thermostat.hvac_modes(), ["heat_only", "cool_only", "auto", "aux"]) + + def test_thermostat_users_away(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/nest.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + thermostat = get_devices_from_response_dict(response_dict, device_types.THERMOSTAT)[0] + self.assertTrue(thermostat.away()) + + def test_thermostat_current_fan_modes(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/go_control_thermostat.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + thermostat = get_devices_from_response_dict(response_dict, device_types.THERMOSTAT)[0] + self.assertEqual(thermostat.current_fan_mode(), "auto") + + def test_thermostat_current_units(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/go_control_thermostat.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + thermostat = get_devices_from_response_dict(response_dict, device_types.THERMOSTAT)[0] + self.assertEqual(thermostat.current_units().get('temperature'), "f") + + def test_thermostat_current_temperature(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/go_control_thermostat.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + thermostat = get_devices_from_response_dict(response_dict, device_types.THERMOSTAT)[0] + self.assertEqual(thermostat.current_temperature(), 20.0) + + def test_thermostat_current_external_temperature(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/sensi_thermostat.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + thermostat = get_devices_from_response_dict(response_dict, device_types.THERMOSTAT)[0] + self.assertEqual(thermostat.current_external_temperature(), 16.1) + + def test_thermostat_current_humidity(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/sensi_thermostat.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + thermostat = get_devices_from_response_dict(response_dict, device_types.THERMOSTAT)[0] + self.assertEqual(thermostat.current_humidity(), 40) + + def test_thermostat_smart_temperature(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/ecobee_thermostat.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + thermostat = get_devices_from_response_dict(response_dict, device_types.THERMOSTAT)[0] + self.assertEqual(thermostat.current_smart_temperature(), 21.5) + + def test_thermostat_max_set_point(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/ecobee_thermostat.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + thermostat = get_devices_from_response_dict(response_dict, device_types.THERMOSTAT)[0] + self.assertEqual(thermostat.current_max_set_point(), 25.555555555555557) + + def test_thermostat_min_set_point(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/ecobee_thermostat.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + thermostat = get_devices_from_response_dict(response_dict, device_types.THERMOSTAT)[0] + self.assertEqual(thermostat.current_min_set_point(), 21.666666666666668) + + def test_thermostat_current_humidifier_mode(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/sensi_thermostat.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + thermostat = get_devices_from_response_dict(response_dict, device_types.THERMOSTAT)[0] + self.assertEqual(thermostat.current_humidifier_mode(), "auto") + + def test_thermostat_current_dehumidifier_mode(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/sensi_thermostat.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + thermostat = get_devices_from_response_dict(response_dict, device_types.THERMOSTAT)[0] + self.assertEqual(thermostat.current_dehumidifier_mode(), "auto") + + def test_thermostat_current_humidifier_set_point(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/sensi_thermostat.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + thermostat = get_devices_from_response_dict(response_dict, device_types.THERMOSTAT)[0] + self.assertEqual(thermostat.current_humidifier_set_point(), 0.2) + + def test_thermostat_current_dehumidifier_set_point(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/sensi_thermostat.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + thermostat = get_devices_from_response_dict(response_dict, device_types.THERMOSTAT)[0] + self.assertEqual(thermostat.current_dehumidifier_set_point(), 0.6) + + def test_thermostat_min_min_set_point(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/sensi_thermostat.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + thermostat = get_devices_from_response_dict(response_dict, device_types.THERMOSTAT)[0] + self.assertEqual(thermostat.min_min_set_point(), 7.222222222222222) + + def test_thermostat_min_max_set_point(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/sensi_thermostat.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + thermostat = get_devices_from_response_dict(response_dict, device_types.THERMOSTAT)[0] + self.assertEqual(thermostat.min_max_set_point(), 7.222222222222222) + + def test_thermostat_max_min_set_point(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/sensi_thermostat.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + thermostat = get_devices_from_response_dict(response_dict, device_types.THERMOSTAT)[0] + self.assertEqual(thermostat.max_min_set_point(), 37.22222222222222) + + def test_thermostat_max_max_set_point(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/sensi_thermostat.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + thermostat = get_devices_from_response_dict(response_dict, device_types.THERMOSTAT)[0] + self.assertEqual(thermostat.max_max_set_point(), 37.22222222222222) + + def test_thermostat_eco_target(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/nest.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + thermostat = get_devices_from_response_dict(response_dict, device_types.THERMOSTAT)[0] + self.assertFalse(thermostat.eco_target()) + + def test_thermostat_occupied(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/ecobee_thermostat.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + thermostat = get_devices_from_response_dict(response_dict, device_types.THERMOSTAT)[0] + self.assertFalse(thermostat.occupied()) + + def test_thermostat_deadband(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/ecobee_thermostat.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + thermostat = get_devices_from_response_dict(response_dict, device_types.THERMOSTAT)[0] + self.assertEqual(thermostat.deadband(), 2.7777777777777777) + + def test_thermostat_fan_active(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/sensi_thermostat.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + thermostat = get_devices_from_response_dict(response_dict, device_types.THERMOSTAT)[0] + self.assertTrue(thermostat.fan_on()) + + def test_thermostat_has_fan(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/sensi_thermostat.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + thermostat = get_devices_from_response_dict(response_dict, device_types.THERMOSTAT)[0] + self.assertTrue(thermostat.has_fan()) + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/ecobee_thermostat.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + thermostat = get_devices_from_response_dict(response_dict, device_types.THERMOSTAT)[0] + self.assertTrue(thermostat.has_fan()) + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/nest.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + thermostat = get_devices_from_response_dict(response_dict, device_types.THERMOSTAT)[0] + self.assertFalse(thermostat.has_fan()) + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/go_control_thermostat.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + thermostat = get_devices_from_response_dict(response_dict, device_types.THERMOSTAT)[0] + self.assertTrue(thermostat.has_fan()) + + def test_thermostat_is_on(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/sensi_thermostat.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + thermostat = get_devices_from_response_dict(response_dict, device_types.THERMOSTAT)[0] + self.assertTrue(thermostat.is_on()) diff --git a/src/setup.py b/src/setup.py index 7a02aec..f4ce2ab 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,9 +1,9 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='0.13.0', + version='1.0.0', description='Access Wink devices via the Wink API', - url='http://github.com/bradsk88/python-wink', + url='http://github.com/python-wink/python-wink', author='Brad Johnson', license='MIT', install_requires=['requests>=2.0'], From 05175b24b56afc86dc5fa69afaad69c80d0a8546 Mon Sep 17 00:00:00 2001 From: Brad Johnson Date: Mon, 30 Jan 2017 11:40:05 -0600 Subject: [PATCH 137/178] Fix CI (#71) --- script/before_install | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/before_install b/script/before_install index f7ab09e..8a75877 100755 --- a/script/before_install +++ b/script/before_install @@ -3,4 +3,4 @@ install: python3 -m pip install --upgrade requests>=2,<3 echo "Installing development dependencies.." - python3 -m pip install --upgrade flake8 pylint python-coveralls pytest pytest-cov + python3 -m pip install --upgrade pip setuptools flake8 pylint python-coveralls pytest pytest-cov From dc2902a9a4853310b5e687a084af6675e3b58f7e Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Mon, 30 Jan 2017 13:39:20 -0500 Subject: [PATCH 138/178] Additional device support plus fixes (#70) * Support for dropcam, Aros AC, Refuel, and leaksmart valve. * Bump version to 1.1.0 --- CHANGELOG.md | 6 + pylintrc | 1 + src/pywink/__init__.py | 3 +- src/pywink/api.py | 8 + src/pywink/devices/air_conditioner.py | 104 +++++++++++++ src/pywink/devices/binary_switch.py | 24 +++ src/pywink/devices/factory.py | 12 +- src/pywink/devices/propane_tank.py | 34 ++++ src/pywink/devices/smoke_detector.py | 94 ++++------- src/pywink/devices/types.py | 5 +- src/pywink/test/api_test.py | 62 +++++++- .../test/devices/air_conditioner_test.py | 86 ++++++++++ .../test/devices/api_responses/dropcam.json | 99 ++++++++++++ .../api_responses/ihome_smart_plug.json | 69 ++++++++ .../api_responses/leaksmart_valve.json | 147 ++++++++++++++++++ .../devices/api_responses/quirky_aros.json | 136 ++++++++++++++++ .../devices/api_responses/quirky_refuel.json | 64 ++++++++ src/pywink/test/devices/base_test.py | 19 ++- src/pywink/test/devices/leaksmart_test.py | 33 ++++ src/pywink/test/devices/sensor_test.py | 27 ++++ src/pywink/test/devices/thermostat_test.py | 2 +- src/setup.py | 2 +- 22 files changed, 952 insertions(+), 85 deletions(-) create mode 100644 src/pywink/devices/air_conditioner.py create mode 100644 src/pywink/devices/propane_tank.py create mode 100644 src/pywink/test/devices/air_conditioner_test.py create mode 100644 src/pywink/test/devices/api_responses/dropcam.json create mode 100644 src/pywink/test/devices/api_responses/ihome_smart_plug.json create mode 100644 src/pywink/test/devices/api_responses/leaksmart_valve.json create mode 100644 src/pywink/test/devices/api_responses/quirky_aros.json create mode 100644 src/pywink/test/devices/api_responses/quirky_refuel.json create mode 100644 src/pywink/test/devices/leaksmart_test.py diff --git a/CHANGELOG.md b/CHANGELOG.md index e4f0cbe..77586dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## 1.1.0 +- Support for Quirky Aros AC units +- Support for Quirky Refuel +- Support for Dropcam sensors +- Fix for leaksmart valves + ## 1.0.0 - Switch to object_type for device type detction - Hard coded user agent diff --git a/pylintrc b/pylintrc index 41988f8..b15d618 100644 --- a/pylintrc +++ b/pylintrc @@ -15,3 +15,4 @@ disable= , invalid-name , fixme , locally-disabled + , duplicate-code diff --git a/src/pywink/__init__.py b/src/pywink/__init__.py index e085d06..4c129f3 100644 --- a/src/pywink/__init__.py +++ b/src/pywink/__init__.py @@ -9,7 +9,8 @@ from pywink.api import get_light_bulbs, get_garage_doors, get_locks, \ get_powerstrips, get_shades, get_sirens, \ - get_switches, get_thermostats, get_fans + get_switches, get_thermostats, get_fans, get_air_conditioners, \ + get_propane_tanks from pywink.api import get_all_devices, get_eggtrays, get_sensors, \ get_keys, get_piggy_banks, get_smoke_and_co_detectors, \ diff --git a/src/pywink/api.py b/src/pywink/api.py index ed6f8e4..fd5c87f 100644 --- a/src/pywink/api.py +++ b/src/pywink/api.py @@ -222,6 +222,14 @@ def get_cameras(): return get_devices(device_types.CAMERA) +def get_air_conditioners(): + return get_devices(device_types.AIR_CONDITIONER) + + +def get_propane_tanks(): + return get_devices(device_types.PROPANE_TANK) + + def get_subscription_key(): response_dict = wink_api_fetch() first_device = response_dict.get('data')[0] diff --git a/src/pywink/devices/air_conditioner.py b/src/pywink/devices/air_conditioner.py new file mode 100644 index 0000000..c22ddde --- /dev/null +++ b/src/pywink/devices/air_conditioner.py @@ -0,0 +1,104 @@ +from pywink.devices.base import WinkDevice + + +# pylint: disable=too-many-public-methods +class WinkAirConditioner(WinkDevice): + """ + Represents a Wink air conditioner. + """ + + def __init__(self, device_state_as_json, api_interface): + super(WinkAirConditioner, self).__init__(device_state_as_json, api_interface) + + def state(self): + return self.current_mode() + + def modes(self): + capabilities = self.json_state.get('capabilities', {}) + cap_fields = capabilities.get('fields', []) + modes = None + for field in cap_fields: + _field = field.get('field') + if _field == 'mode': + modes = field.get('choices') + return modes + + def current_mode(self): + return self._last_reading.get('mode') + + def current_temperature(self): + return self._last_reading.get('temperature') + + def current_max_set_point(self): + return self._last_reading.get('max_set_point') + + def current_fan_speed(self): + return self._last_reading.get('fan_speed') + + def is_on(self): + return self._last_reading.get('powered', False) + + def schedule_enabled(self): + return self._last_reading.get('schedule_enabled') + + def total_consumption(self): + # This is the total consumption of watts + return self._last_reading.get('consumption') + + def set_schedule_enabled(self, state): + """ + :param state: a boolean True (on) or False (off) + :return: nothing + """ + desired_state = {"schedule_enabled": state} + + response = self.api_interface.set_device_state(self, { + "desired_state": desired_state + }) + + self._update_state_from_response(response) + + def set_ac_fan_speed(self, speed): + """ + :param speed: a float from 0.0 to 1.0 (0 - 100%) + :return: nothing + """ + desired_state = {"fan_speed": speed} + + response = self.api_interface.set_device_state(self, { + "desired_state": desired_state + }) + + self._update_state_from_response(response) + + def set_mode(self, mode): + """ + :param mode: a string one of ["off", "auto_eco", "cool_only", "fan_only"] + :return: nothing + """ + if mode == "off": + desired_state = {"powered": False} + else: + desired_state = {"powered": True, "mode": mode} + + response = self.api_interface.set_device_state(self, { + "desired_state": desired_state + }) + + self._update_state_from_response(response) + + def set_temperature(self, max_set_point=None): + """ + :param temperature: a float for the temperature value in celsius + :return: nothing + """ + desired_state = {} + + if max_set_point: + desired_state['max_set_point'] = max_set_point + + response = self.api_interface.set_device_state(self, { + "desired_state": desired_state + }) + + self._update_state_from_response(response) diff --git a/src/pywink/devices/binary_switch.py b/src/pywink/devices/binary_switch.py index d82c940..a1a37c6 100644 --- a/src/pywink/devices/binary_switch.py +++ b/src/pywink/devices/binary_switch.py @@ -27,3 +27,27 @@ def update_state(self): """ response = self.api_interface.get_device_state(self, type_override="binary_switche") return self._update_state_from_response(response) + + +class WinkLeakSmartValve(WinkBinarySwitch): + """ + Represents a Wink leaksmart valve.. + """ + + def __init__(self, device_state_as_json, api_interface): + super(WinkLeakSmartValve, self).__init__(device_state_as_json, api_interface) + + def state(self): + return self._last_reading.get('opened', False) + + def set_state(self, state): + """ + :param state: a boolean of true (on) or false ('off') + :return: nothing + """ + values = {"desired_state": {"opened": state}} + response = self.api_interface.set_device_state(self, values, type_override="binary_switche") + self._update_state_from_response(response) + + def last_event(self): + return self._last_reading.get("last_event") diff --git a/src/pywink/devices/factory.py b/src/pywink/devices/factory.py index 07ddedb..20252c2 100644 --- a/src/pywink/devices/factory.py +++ b/src/pywink/devices/factory.py @@ -5,7 +5,7 @@ from pywink.devices import types as device_types from pywink.devices.sensor import WinkSensor from pywink.devices.light_bulb import WinkLightBulb -from pywink.devices.binary_switch import WinkBinarySwitch +from pywink.devices.binary_switch import WinkBinarySwitch, WinkLeakSmartValve from pywink.devices.lock import WinkLock from pywink.devices.eggtray import WinkEggtray from pywink.devices.garage_door import WinkGarageDoor @@ -23,6 +23,8 @@ from pywink.devices.gang import WinkGang from pywink.devices.smoke_detector import WinkSmokeDetector, WinkSmokeSeverity, WinkCoDetector, WinkCoSeverity from pywink.devices.camera import WinkCanaryCamera +from pywink.devices.air_conditioner import WinkAirConditioner +from pywink.devices.propane_tank import WinkPropaneTank # pylint: disable=redefined-variable-type,too-many-branches, too-many-statements @@ -42,6 +44,8 @@ def build_device(device_state_as_json, api_interface): mode = device_state_as_json["last_reading"]["powering_mode"] if mode == "dumb": new_object = WinkBinarySwitch(device_state_as_json, api_interface) + elif device_state_as_json.get("model_name") == "leakSMART Valve": + new_object = WinkLeakSmartValve(device_state_as_json, api_interface) else: new_object = WinkBinarySwitch(device_state_as_json, api_interface) elif object_type == device_types.LOCK: @@ -87,6 +91,12 @@ def build_device(device_state_as_json, api_interface): elif object_type == device_types.CAMERA: if device_state_as_json.get("device_manufacturer") == "canary": new_object = WinkCanaryCamera(device_state_as_json, api_interface) + elif device_state_as_json.get("device_manufacturer") == "dropcam": + new_objects = __get_subsensors_from_device(device_state_as_json, api_interface) + elif object_type == device_types.AIR_CONDITIONER: + new_object = WinkAirConditioner(device_state_as_json, api_interface) + elif object_type == device_types.PROPANE_TANK: + new_object = WinkPropaneTank(device_state_as_json, api_interface) if new_object is not None: return [new_object] diff --git a/src/pywink/devices/propane_tank.py b/src/pywink/devices/propane_tank.py new file mode 100644 index 0000000..d5daade --- /dev/null +++ b/src/pywink/devices/propane_tank.py @@ -0,0 +1,34 @@ +from pywink.devices.base import WinkDevice + + +class WinkPropaneTank(WinkDevice): + """ + Represents a Wink refuel. + """ + + def __init__(self, device_state_as_json, api_interface): + super(WinkPropaneTank, self).__init__(device_state_as_json, api_interface) + self._cap = None + self._unit = None + + def capability(self): + # Propane tanks have no capability. + return self._cap + + def unit(self): + return self._unit + + def state(self): + return self._last_reading.get("remaining") + + def tare(self): + return self.json_state.get("tare") + + def set_tare(self, tare): + """ + :param tare: weight of tank as printed on can + :return: nothing + tare is not set in desired state, but on the main device. + """ + response = self.api_interface.set_device_state(self, {"tare": tare}) + self._update_state_from_response(response) diff --git a/src/pywink/devices/smoke_detector.py b/src/pywink/devices/smoke_detector.py index 3a1fa60..fdce496 100644 --- a/src/pywink/devices/smoke_detector.py +++ b/src/pywink/devices/smoke_detector.py @@ -1,16 +1,14 @@ from pywink.devices.sensor import WinkDevice -class WinkSmokeDetector(WinkDevice): - """ - Represents a Wink Smoke detector. - """ +class WinkBaseSmokeDetector(WinkDevice): + """Represents a base smoke detector sensor.""" - def __init__(self, device_state_as_json, api_interface): - super(WinkSmokeDetector, self).__init__(device_state_as_json, api_interface) + def __init__(self, device_state_as_json, api_interface, unit_type, capability): + super(WinkBaseSmokeDetector, self).__init__(device_state_as_json, api_interface) self._unit = None - self._cap = "smoke_detected" - self._unit_type = "boolean" + self._unit_type = unit_type + self._cap = capability def unit(self): return self._unit @@ -27,83 +25,47 @@ def name(self): def state(self): return self._last_reading.get(self.capability()) + def test_activated(self): + return self._last_reading.get("test_activated") -class WinkSmokeSeverity(WinkDevice): + +class WinkSmokeDetector(WinkBaseSmokeDetector): """ - Represents a Wink/Nest Smoke severity sensor. + Represents a Wink Smoke detector. """ def __init__(self, device_state_as_json, api_interface): - super(WinkSmokeSeverity, self).__init__(device_state_as_json, api_interface) - self._unit = None - self._cap = "smoke_severity" - self._unit_type = None - - def unit(self): - return self._unit - - def unit_type(self): - return self._unit_type + capability = "smoke_detected" + unit_type = "boolean" + super(WinkSmokeDetector, self).__init__(device_state_as_json, api_interface, unit_type, capability) - def capability(self): - return self._cap - def name(self): - return self.json_state.get("name") + " " + self.capability() +class WinkSmokeSeverity(WinkBaseSmokeDetector): + """ + Represents a Wink/Nest Smoke severity sensor. + """ - def state(self): - return self._last_reading.get(self.capability()) + def __init__(self, device_state_as_json, api_interface): + capability = "smoke_severity" + super(WinkSmokeSeverity, self).__init__(device_state_as_json, api_interface, None, capability) -class WinkCoDetector(WinkDevice): +class WinkCoDetector(WinkBaseSmokeDetector): """ Represents a Wink CO detector. """ def __init__(self, device_state_as_json, api_interface): - super(WinkCoDetector, self).__init__(device_state_as_json, api_interface) - self._unit = None - self._cap = "co_detected" - self._unit_type = "boolean" - - def unit(self): - return self._unit + capability = "co_detected" + unit_type = "boolean" + super(WinkCoDetector, self).__init__(device_state_as_json, api_interface, unit_type, capability) - def unit_type(self): - return self._unit_type - - def capability(self): - return self._cap - - def name(self): - return self.json_state.get("name") + " " + self.capability() - - def state(self): - return self._last_reading.get(self.capability()) - -class WinkCoSeverity(WinkDevice): +class WinkCoSeverity(WinkBaseSmokeDetector): """ Represents a Wink/Nest CO severity sensor. """ def __init__(self, device_state_as_json, api_interface): - super(WinkCoSeverity, self).__init__(device_state_as_json, api_interface) - self._unit = None - self._cap = "co_severity" - self._unit_type = None - - def unit(self): - return self._unit - - def unit_type(self): - return self._unit_type - - def capability(self): - return self._cap - - def name(self): - return self.json_state.get("name") + " " + self.capability() - - def state(self): - return self._last_reading.get(self.capability()) + capability = "co_severity" + super(WinkCoSeverity, self).__init__(device_state_as_json, api_interface, None, capability) diff --git a/src/pywink/devices/types.py b/src/pywink/devices/types.py index 9d19b7b..b5c42fa 100644 --- a/src/pywink/devices/types.py +++ b/src/pywink/devices/types.py @@ -23,8 +23,11 @@ BUTTON = 'button' GANG = 'gang' CAMERA = 'camera' +AIR_CONDITIONER = 'air_conditioner' +PROPANE_TANK = 'propane_tank' ALL_SUPPORTED_DEVICES = [LIGHT_BULB, BINARY_SWITCH, SENSOR_POD, LOCK, EGGTRAY, GARAGE_DOOR, POWERSTRIP, SHADE, SIREN, KEY, PIGGY_BANK, SMOKE_DETECTOR, THERMOSTAT, HUB, FAN, DOOR_BELL, REMOTE, - SPRINKLER, BUTTON, GANG, CAMERA] + SPRINKLER, BUTTON, GANG, CAMERA, AIR_CONDITIONER, + PROPANE_TANK] diff --git a/src/pywink/test/api_test.py b/src/pywink/test/api_test.py index 81d68da..4dc2f61 100644 --- a/src/pywink/test/api_test.py +++ b/src/pywink/test/api_test.py @@ -33,6 +33,9 @@ from pywink.devices.gang import WinkGang from pywink.devices.smoke_detector import WinkSmokeDetector, WinkSmokeSeverity, WinkCoDetector, WinkCoSeverity from pywink.devices.sprinkler import WinkSprinkler +from pywink.devices.camera import WinkCanaryCamera +from pywink.devices.air_conditioner import WinkAirConditioner +from pywink.devices.propane_tank import WinkPropaneTank USERS_ME_WINK_DEVICES = {} @@ -80,7 +83,7 @@ def test_get_subscription_key(self): def test_get_all_devices_from_api(self): WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) devices = get_all_devices() - self.assertEqual(len(devices), 51) + self.assertEqual(len(devices), 61) lights = get_light_bulbs() for light in lights: self.assertTrue(isinstance(light, WinkLightBulb)) @@ -136,13 +139,19 @@ def test_get_all_devices_from_api(self): buttons = get_buttons() for button in buttons: self.assertTrue(isinstance(button, WinkButton)) + acs = get_air_conditioners() + for ac in acs: + self.assertTrue(isinstance(ac, WinkAirConditioner)) + propane_tanks = get_propane_tanks() + for tank in propane_tanks: + self.assertTrue(isinstance(tank, WinkPropaneTank)) def test_get_sensor_and_binary_switch_updated_states_from_api(self): WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) sensor_types = [WinkSensor, WinkHub, WinkPorkfolioBalanceSensor, WinkKey, WinkRemote, WinkGang, WinkSmokeDetector, WinkSmokeSeverity, WinkCoDetector, WinkCoSeverity, WinkButton] - skip_types = [WinkPowerStripOutlet] + skip_types = [WinkPowerStripOutlet, WinkCanaryCamera] devices = get_all_devices() old_states = {} for device in devices: @@ -271,6 +280,27 @@ def test_get_lock_updated_states_from_api(self): self.assertTrue(device.vacation_mode_enabled()) self.assertTrue(device.beeper_enabled()) + def test_get_air_conditioner_updated_states_from_api(self): + WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) + devices = get_air_conditioners() + old_states = {} + for device in devices: + device.api_interface = self.api_interface + old_states[device.object_id()] = device.state() + device.set_mode("cool_only") + device.set_temperature(70) + device.set_schedule_enabled(False) + device.set_ac_fan_speed(0.5) + for device in devices: + self.assertEqual(device.state(), "cool_only") + self.assertEqual(70, device.current_max_set_point()) + self.assertFalse(device.schedule_enabled()) + self.assertEqual(0.5, device.current_fan_speed()) + + def test_get_camera_updated_states_from_api(self): + WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) + devices = get_cameras() + def test_get_thermostat_updated_states_from_api(self): WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) devices = get_thermostats() @@ -302,12 +332,14 @@ def test_get_camera_updated_states_from_api(self): devices = get_cameras() old_states = {} for device in devices: - device.api_interface = self.api_interface - device.set_mode("away") - device.set_privacy(True) - device.update_state() - self.assertEqual(device.state(), "away") - self.assertTrue(device.private()) + if isinstance(device, WinkCanaryCamera): + device.api_interface = self.api_interface + device.set_mode("away") + device.set_privacy(True) + device.update_state() + if isinstance(device, WinkCanaryCamera): + self.assertEqual(device.state(), "away") + self.assertTrue(device.private()) def test_get_fan_updated_states_from_api(self): WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) @@ -324,6 +356,18 @@ def test_get_fan_updated_states_from_api(self): self.assertEqual(device.current_timer(), 300) + def test_get_propane_tank_updated_states_from_api(self): + WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) + devices = get_propane_tanks() + old_states = {} + for device in devices: + device.api_interface = self.api_interface + device.set_tare(5.0) + device.update_state() + self.assertEqual(device.tare(), 5.0) + + + class MockServerRequestHandler(BaseHTTPRequestHandler): USERS_ME_WINK_DEVICES_PATTERN = re.compile(r'/users/me/wink_devices') BAD_STATUS_PATTERN = re.compile(r'/401/') @@ -405,6 +449,8 @@ def set_device_state(self, device, state, id_override=None, type_override=None): else: if "nose_color" in state: dict_device["nose_color"] = state.get("nose_color") + elif "tare" in state: + dict_device["tare"] = state.get("tare") else: for key, value in state.get('desired_state').items(): dict_device["last_reading"][key] = value diff --git a/src/pywink/test/devices/air_conditioner_test.py b/src/pywink/test/devices/air_conditioner_test.py new file mode 100644 index 0000000..cf1e1dc --- /dev/null +++ b/src/pywink/test/devices/air_conditioner_test.py @@ -0,0 +1,86 @@ +import json +import os +import unittest + +import mock + +from pywink.api import get_devices_from_response_dict, WinkApiInterface +from pywink.devices import types as device_types +from pywink.devices.air_conditioner import WinkAirConditioner + + +class FanTests(unittest.TestCase): + + def setUp(self): + super(FanTests, self).setUp() + self.api_interface = mock.MagicMock() + + def test_ac_state(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/quirky_aros.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + ac = get_devices_from_response_dict(response_dict, device_types.AIR_CONDITIONER)[0] + self.assertEqual(ac.state(), "auto_eco") + + def test_ac_modes(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/quirky_aros.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + ac = get_devices_from_response_dict(response_dict, device_types.AIR_CONDITIONER)[0] + self.assertEqual(ac.modes(), ["auto_eco", "cool_only", "fan_only"]) + + def test_thermostat_current_fan_speed(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/quirky_aros.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + ac = get_devices_from_response_dict(response_dict, device_types.AIR_CONDITIONER)[0] + self.assertEqual(ac.current_fan_speed(), 1.0) + + def test_ac_current_temperature(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/quirky_aros.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + ac = get_devices_from_response_dict(response_dict, device_types.AIR_CONDITIONER)[0] + self.assertEqual(ac.current_temperature(), 17.777777777777779) + + def test_thermostat_max_set_point(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/quirky_aros.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + ac = get_devices_from_response_dict(response_dict, device_types.AIR_CONDITIONER)[0] + self.assertEqual(ac.current_max_set_point(), 20.0) + + def test_thermostat_is_on(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/quirky_aros.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + ac = get_devices_from_response_dict(response_dict, device_types.AIR_CONDITIONER)[0] + self.assertFalse(ac.is_on()) + + def test_schedule_enabled(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/quirky_aros.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + ac = get_devices_from_response_dict(response_dict, device_types.AIR_CONDITIONER)[0] + self.assertTrue(ac.schedule_enabled()) diff --git a/src/pywink/test/devices/api_responses/dropcam.json b/src/pywink/test/devices/api_responses/dropcam.json new file mode 100644 index 0000000..96607d6 --- /dev/null +++ b/src/pywink/test/devices/api_responses/dropcam.json @@ -0,0 +1,99 @@ +{ + "object_type":"camera", + "object_id":"11115", + "uuid":"81d81b60-28bb-429d-XXXXXXXXXXXXXXX", + "icon_id":null, + "icon_code":null, + "desired_state":{ + "capturing_video":true + }, + "last_reading":{ + "connection":true, + "connection_updated_at":1485261496.1719434, + "capturing_audio":true, + "capturing_audio_updated_at":1485261462.2724335, + "capturing_video":true, + "capturing_video_updated_at":1485261479.1167798, + "motion":false, + "motion_updated_at":1452211428.5470002, + "noise":false, + "noise_updated_at":1444328639.95, + "loudness":false, + "loudness_updated_at":1444328639.95, + "has_recording_plan":false, + "has_recording_plan_updated_at":1485261462.2724335, + "motion_true":"N/A", + "motion_true_updated_at":1426382914.1699998, + "loudness_true":"N/A", + "loudness_true_updated_at":1441272988.1036048, + "desired_capturing_video_updated_at":1485261479.1736128, + "desired_capturing_video_changed_at":1485261479.1736128, + "capturing_video_changed_at":1485261479.1167798 + }, + "subscription":{ + "pubnub":{ + "subscribe_key":"sub-c-f7bf7f7eXXXXXXXXXXXXXXX", + "channel":"135122aa71b4df223c4XXXXXXXXXXX|camera-11115|user-123456" + } + }, + "camera_id":"11115", + "name":"Camera", + "locale":"en_us", + "units":{ + + }, + "created_at":1426357721, + "hidden_at":null, + "capabilities":{ + "fields":[ + { + "type":"boolean", + "field":"connection", + "mutability":"read-only" + }, + { + "type":"boolean", + "field":"capturing_audio", + "mutability":"read-only" + }, + { + "type":"boolean", + "field":"capturing_video", + "mutability":"read-write" + }, + { + "type":"boolean", + "field":"motion", + "mutability":"read-only" + }, + { + "type":"boolean", + "field":"noise", + "mutability":"read-only" + }, + { + "type":"boolean", + "field":"loudness", + "mutability":"read-only" + }, + { + "type":"boolean", + "field":"has_recording_plan", + "mutability":"read-only" + } + ], + "home_security_device":true + }, + "manufacturer_device_model":"dropcam_pro", + "manufacturer_device_id":"67b1aeb2e1254a37a8d67f86bf89fdee", + "device_manufacturer":"dropcam", + "model_name":"Dropcam Pro", + "upc_id":"8", + "upc_code":"856568003164", + "linked_service_id":"89952", + "lat_lng":[ + 98.7654, + -12.3456 + ], + "location":"Home" +} diff --git a/src/pywink/test/devices/api_responses/ihome_smart_plug.json b/src/pywink/test/devices/api_responses/ihome_smart_plug.json new file mode 100644 index 0000000..58f85aa --- /dev/null +++ b/src/pywink/test/devices/api_responses/ihome_smart_plug.json @@ -0,0 +1,69 @@ +{ + "object_type":"binary_switch", + "object_id":"227395", + "uuid":"_REDACTED_", + "icon_id":"258", + "icon_code":"binary_switch-ihome_smartplug", + "desired_state":{ + "powered":false + }, + "last_reading":{ + "connection":true, + "connection_updated_at":1484507284.87917, + "powered":false, + "powered_updated_at":1485235214.0, + "desired_powered_updated_at":1485235512.1232858, + "desired_powered_changed_at":1485235512.1232858, + "powered_changed_at":1484619628.0, + "connection_changed_at":1482968739.0 + }, + "subscription":{ + "pubnub":{ + "subscribe_key":"_REDACTED_", + "channel":"_REDACTED_" + } + }, + "binary_switch_id":"227395", + "name":"Basement Heater", + "locale":"en_us", + "units":{ + + }, + "created_at":1466779445, + "hidden_at":null, + "capabilities":{ + "fields":[ + { + "type":"boolean", + "field":"connection", + "mutability":"read-only" + }, + { + "type":"boolean", + "field":"powered", + "mutability":"read-write" + } + ] + }, + "triggers":[ + + ], + "manufacturer_device_model":"ihome_i_sp5", + "manufacturer_device_id":"UEgNegsh62pe2xVbMfMaYcNa", + "device_manufacturer":"ihome", + "model_name":"SmartPlug", + "upc_id":"358", + "upc_code":"047532907315", + "gang_id":null, + "hub_id":null, + "local_id":null, + "radio_type":null, + "linked_service_id":"335542", + "current_budget":null, + "lat_lng":[ + 98.765432, + 12.345678 + ], + "location":"_REDACTED_", + "order":0 +} diff --git a/src/pywink/test/devices/api_responses/leaksmart_valve.json b/src/pywink/test/devices/api_responses/leaksmart_valve.json new file mode 100644 index 0000000..882fe2f --- /dev/null +++ b/src/pywink/test/devices/api_responses/leaksmart_valve.json @@ -0,0 +1,147 @@ +{ + "object_type":"binary_switch", + "object_id":"383692", + "uuid":"5bfceb54-28bc-41de-966c-XXXXXXXXXX", + "icon_id":"52", + "icon_code":"binary_switch-light_bulb_dumb", + "desired_state":{ + "opened":true, + "identify_mode":false + }, + "last_reading":{ + "connection":true, + "connection_updated_at":1485723151.1496468, + "opened":true, + "opened_updated_at":1485720584.6081653, + "identify_mode":false, + "identify_mode_updated_at":1485720584.6081653, + "last_event":"monthly_cycle_success", + "last_event_updated_at":null, + "battery":1.0, + "battery_updated_at":1485720584.6081653, + "firmware_version":"0.0b00 / 0.0b19", + "firmware_version_updated_at":1485720584.6081653, + "firmware_date_code":"20150723", + "firmware_date_code_updated_at":1485720584.6081653, + "desired_opened_updated_at":1485562578.0845749, + "desired_identify_mode_updated_at":1485562700.4856005, + "connection_changed_at":1485721609.4953206, + "opened_changed_at":1485562578.0012438, + "firmware_date_code_changed_at":1485462807.4100585, + "battery_changed_at":1485462807.4100585, + "identify_mode_changed_at":1485462807.4100585, + "firmware_version_changed_at":1485463188.7705858, + "desired_opened_changed_at":1485562578.0845749, + "desired_identify_mode_changed_at":1485462991.2863882 + }, + "subscription":{ + "pubnub":{ + "subscribe_key":"sub-c-XXXXXXXXx", + "channel":"8efd650aabe470340e8f080dX|binary_switch-383692|user-252560" + } + }, + "binary_switch_id":"383692", + "name":"Water Valve", + "locale":"en_us", + "units":{ + + }, + "created_at":1485462804, + "hidden_at":null, + "capabilities":{ + "fields":[ + { + "type":"boolean", + "field":"connection", + "mutability":"read-only" + }, + { + "type":"boolean", + "field":"opened", + "mutability":"read-write", + "attribute_id":1 + }, + { + "type":"boolean", + "field":"identify_mode", + "mutability":"read-write", + "attribute_id":258048 + }, + { + "type":"selection", + "field":"last_event", + "choices":[ + "no_event", + "monthly_cycle_start", + "monthly_cycle_success", + "monthly_cycle_skipped", + "valve_operation_failure" + ], + "mutability":"read-only" + }, + { + "type":"percentage", + "field":"battery", + "mutability":"read-only", + "attribute_id":127009 + } + ], + "automation_robots":[ + { + "name":"Auto Shutoff Valve", + "causes":[ + { + "value":"true", + "operator":"==", + "observed_field":"liquid_detected.or", + "observed_object_id":"**SPECIAL_GROUP**:.sensors", + "observed_object_type":"Group" + } + ], + "effects":[ + { + "scene":{ + "members":[ + { + "object_id":"**SELF**:id", + "object_type":"**SELF**:api_name", + "desired_state":{ + "opened":false + } + } + ] + } + } + ], + "enabled":false, + "automation_mode":"valve_auto_close" + } + ], + "notification_robots":[ + "operation_failure_notification", + "low_battery_notification", + "offline_notification" + ] + }, + "triggers":[ + + ], + "manufacturer_device_model":"leaksmart_valve", + "manufacturer_device_id":null, + "device_manufacturer":"leaksmart", + "model_name":"leakSMART Valve", + "upc_id":"463", + "upc_code":"waxman_shutoff_valve", + "gang_id":null, + "hub_id":"236929", + "local_id":"187", + "radio_type":"zigbee", + "linked_service_id":null, + "current_budget":null, + "lat_lng":[ + 98.76543, + 12.1235 + ], + "location":null, + "order":0 +} diff --git a/src/pywink/test/devices/api_responses/quirky_aros.json b/src/pywink/test/devices/api_responses/quirky_aros.json new file mode 100644 index 0000000..c6eb5ba --- /dev/null +++ b/src/pywink/test/devices/api_responses/quirky_aros.json @@ -0,0 +1,136 @@ +{ + "object_type":"air_conditioner", + "object_id":"50783", + "uuid":"_REDACTED_", + "icon_id":null, + "icon_code":null, + "desired_state":{ + "schedule_enabled":true, + "fan_speed":1.0, + "mode":"auto_eco", + "powered":false, + "max_set_point":20.0, + "units":null + }, + "last_reading":{ + "units":null, + "units_updated_at":null, + "min_set_point":null, + "min_set_point_updated_at":null, + "mode":"auto_eco", + "mode_updated_at":1475247681.5949428, + "powered":false, + "powered_updated_at":1483230400.218291, + "temperature":17.777777777777779, + "temperature_updated_at":1485150377.8355598, + "fan_speed":1.0, + "fan_speed_updated_at":1475247682.9838746, + "connection":true, + "connection_updated_at":1485242881.9551878, + "schedule_enabled":true, + "schedule_enabled_updated_at":1471371601.0743012, + "consumption":471016.0, + "consumption_updated_at":1485242881.9551878, + "cost":6.964E-05, + "cost_updated_at":1485242881.9551878, + "budget_percentage":0.0, + "budget_percentage_updated_at":1471372102.8071487, + "budget_velocity":0.0, + "budget_velocity_updated_at":1471372102.8071487, + "max_set_point":20.0, + "max_set_point_updated_at":1483230400.218291, + "external_temperature":null, + "external_temperature_updated_at":null, + "users_away":null, + "users_away_updated_at":null, + "desired_schedule_enabled_updated_at":1485237776.490891, + "desired_fan_speed_updated_at":1485237776.490891, + "desired_mode_updated_at":1485237776.490891, + "desired_powered_updated_at":1485237776.490891, + "desired_max_set_point_updated_at":1485237776.490891, + "desired_units_updated_at":1485237776.490891, + "consumption_changed_at":1485242881.9551878, + "desired_fan_speed_changed_at":1485237776.490891, + "desired_mode_changed_at":1485237776.490891, + "desired_powered_changed_at":1485237776.490891, + "desired_max_set_point_changed_at":1485237776.490891, + "temperature_changed_at":1485150377.8355598, + "connection_changed_at":1484887329.0392363 + }, + "subscription":{ + "pubnub":{ + "subscribe_key":"_REDACTED_", + "channel":"_REDACTED_" + } + }, + "air_conditioner_id":"50783", + "name":"Window Unit", + "locale":"en_us", + "units":{ + + }, + "created_at":1471371601, + "hidden_at":null, + "capabilities":{ + "fields":[ + { + "type":"nested_hash", + "field":"units", + "mutability":"read-write" + }, + { + "type":"float", + "field":"min_set_point", + "mutability":"read-write" + }, + { + "type":"selection", + "field":"mode", + "choices":[ + "auto_eco", + "cool_only", + "fan_only" + ], + "mutability":"read-write" + }, + { + "type":"boolean", + "field":"powered", + "mutability":"read-write" + }, + { + "type":"float", + "field":"temperature", + "mutability":"read-only" + }, + { + "type":"percentage", + "field":"fan_speed", + "mutability":"read-write" + }, + { + "type":"boolean", + "field":"connection", + "mutability":"read-only" + } + ], + "needs_wifi_network_list":true + }, + "triggers":[ + + ], + "device_manufacturer":"quirky_ge", + "model_name":"Aros", + "upc_id":"535", + "upc_code":"quirky_ge_aros", + "current_budget":null, + "lat_lng":[ + 98.76543, + 12.34567 + ], + "location":"_REDACTED_", + "mac_address":"_REDACTED_", + "serial":"AEAA00012899", + "electric_rate":0.069640000000000007, + "smart_schedule_enabled":false +} diff --git a/src/pywink/test/devices/api_responses/quirky_refuel.json b/src/pywink/test/devices/api_responses/quirky_refuel.json new file mode 100644 index 0000000..be56303 --- /dev/null +++ b/src/pywink/test/devices/api_responses/quirky_refuel.json @@ -0,0 +1,64 @@ +{ + "object_type": "propane_tank", + "object_id": "13555", + "uuid": "e0169b3e-e12e-40e6-8ae3-d2f6ca4a7066", + "icon_id": null, + "icon_code": null, + "last_reading": { + "connection": true, + "battery": 1.0, + "remaining": 0.0 + }, + "subscription": { + "pubnub": { + "subscribe_key": "sub-c-f7bf7f7e-0542-11e3-a5e8-02ee2ddab7fe", + "channel": "da9a307e0cfbed0f6a5814eef4504deec021f0e0|propane_tank-13555|user-377857" + } + }, + "propane_tank_id": "13555", + "name": "Refuel", + "locale": "en_us", + "units": {}, + "created_at": 1485560255, + "hidden_at": null, + "capabilities": { + "needs_wifi_network_list": true + }, + "triggers": [ + { + "object_type": "trigger", + "object_id": "226365", + "trigger_id": "226365", + "name": "Refuel remaining", + "enabled": true, + "trigger_configuration": { + "reading_type": "remaining", + "edge": "falling", + "threshold": 0.125, + "object_id": "13555", + "object_type": "propane_tank" + }, + "channel_configuration": { + "recipient_user_ids": [], + "channel_id": "15", + "object_type": null, + "object_id": null + }, + "robot_id": "4658878", + "triggered_at": null + } + ], + "device_manufacturer": "quirky_ge", + "model_name": "Refuel", + "upc_id": "525", + "upc_code": "quirky_ge_refuel", + "lat_lng": [ + null, + null + ], + "location": "", + "mac_address": "0c2a69073fda", + "serial": "ACAB00034466", + "tare": 0.0, + "tank_changed_at": 1485560407 +} diff --git a/src/pywink/test/devices/base_test.py b/src/pywink/test/devices/base_test.py index 26f89fb..37ab5cc 100644 --- a/src/pywink/test/devices/base_test.py +++ b/src/pywink/test/devices/base_test.py @@ -22,6 +22,8 @@ from pywink.devices.button import WinkButton from pywink.devices.gang import WinkGang from pywink.devices.camera import WinkCanaryCamera +from pywink.devices.air_conditioner import WinkAirConditioner +from pywink.devices.propane_tank import WinkPropaneTank class BaseTests(unittest.TestCase): @@ -78,12 +80,17 @@ def test_all_devices_battery_is_valid(self): devices = get_devices_from_response_dict(self.response_dict, device_types.ALL_SUPPORTED_DEVICES) skip_types = [WinkFan, WinkPorkfolioBalanceSensor, WinkPorkfolioNose, WinkBinarySwitch, WinkHub, WinkLightBulb, WinkThermostat, WinkKey, WinkPowerStrip, WinkPowerStripOutlet, - WinkRemote, WinkShade, WinkSprinkler, WinkButton, WinkGang, WinkCanaryCamera] + WinkRemote, WinkShade, WinkSprinkler, WinkButton, WinkGang, WinkCanaryCamera, + WinkAirConditioner] for device in devices: - if type(device) in skip_types: + if device.manufacturer_device_model() == "leaksmart_valve": + self.assertIsNotNone(device.battery_level()) + elif type(device) in skip_types: self.assertIsNone(device.battery_level()) elif device.manufacturer_device_model() == "wink_relay_sensor": - self.assertIsNone(device.manufacturer_device_id()) + self.assertIsNone(device.battery_level()) + elif device.device_manufacturer() == "dropcam": + self.assertIsNone(device.battery_level()) elif device._last_reading.get('external_power'): self.assertIsNone(device.battery_level()) else: @@ -92,7 +99,7 @@ def test_all_devices_battery_is_valid(self): def test_all_devices_manufacturer_device_model_state_is_valid(self): devices = get_devices_from_response_dict(self.response_dict, device_types.ALL_SUPPORTED_DEVICES) skip_types = [WinkKey, WinkPorkfolioBalanceSensor, WinkPorkfolioNose, WinkPowerStripOutlet, - WinkSiren, WinkEggtray, WinkRemote, WinkPowerStrip] + WinkSiren, WinkEggtray, WinkRemote, WinkPowerStrip, WinkAirConditioner, WinkPropaneTank] for device in devices: if type(device) in skip_types: self.assertIsNone(device.manufacturer_device_model()) @@ -104,11 +111,11 @@ def test_all_devices_manufacturer_device_model_state_is_valid(self): def test_all_devices_manufacturer_device_id_state_is_valid(self): devices = get_devices_from_response_dict(self.response_dict, device_types.ALL_SUPPORTED_DEVICES) skip_types = [WinkKey, WinkPowerStrip, WinkPowerStripOutlet, WinkPorkfolioBalanceSensor, WinkPorkfolioNose, - WinkSiren, WinkEggtray, WinkRemote, WinkButton] + WinkSiren, WinkEggtray, WinkRemote, WinkButton, WinkAirConditioner, WinkPropaneTank] skip_manufactuer_device_models = ["linear_wadwaz_1", "linear_wapirz_1", "aeon_labs_dsb45_zwus", "wink_hub", "wink_hub2", "sylvania_sylvania_ct", "ge_bulb", "quirky_ge_spotter", "schlage_zwave_lock", "home_decorators_home_decorators_fan", "sylvania_sylvania_rgbw", "somfy_bali", "wink_relay_sensor", "wink_project_one", "kidde_smoke_alarm", - "wink_relay_switch"] + "wink_relay_switch", "leaksmart_valve"] skip_names = ["GoControl Thermostat", "GE Zwave Switch"] for device in devices: if device.name() in skip_names: diff --git a/src/pywink/test/devices/leaksmart_test.py b/src/pywink/test/devices/leaksmart_test.py new file mode 100644 index 0000000..096c4d8 --- /dev/null +++ b/src/pywink/test/devices/leaksmart_test.py @@ -0,0 +1,33 @@ +import json +import os +import unittest + +import mock + +from pywink.api import get_devices_from_response_dict, WinkApiInterface +from pywink.devices import types as device_types + + +class LeakSmartTests(unittest.TestCase): + + def setUp(self): + super(LeakSmartTests, self).setUp() + self.api_interface = mock.MagicMock() + device_list = [] + self.response_dict = {} + _json_file = open('{}/api_responses/leaksmart_valve.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + self.response_dict["data"] = device_list + + def test_siren_state(self): + binary_switches = get_devices_from_response_dict(self.response_dict, device_types.BINARY_SWITCH) + for switch in binary_switches: + if switch.model_name() == "leakSMART Valve": + self.assertTrue(switch.state()) + + def test_last_event(self): + binary_switches = get_devices_from_response_dict(self.response_dict, device_types.BINARY_SWITCH) + for switch in binary_switches: + if switch.model_name() == "leakSMART Valve": + self.assertEqual(switch.last_event(), "monthly_cycle_success") diff --git a/src/pywink/test/devices/sensor_test.py b/src/pywink/test/devices/sensor_test.py index ffc0105..0016db6 100644 --- a/src/pywink/test/devices/sensor_test.py +++ b/src/pywink/test/devices/sensor_test.py @@ -9,6 +9,7 @@ from pywink.devices.sensor import WinkSensor from pywink.devices.piggy_bank import WinkPorkfolioBalanceSensor from pywink.devices.smoke_detector import WinkSmokeDetector, WinkCoDetector, WinkSmokeSeverity, WinkCoSeverity +from pywink.devices.propane_tank import WinkPropaneTank class SensorTests(unittest.TestCase): @@ -174,6 +175,11 @@ def setUp(self): _json_file.close() self.response_dict["data"] = device_list + def test_test_activated_is_false(self): + devices = get_devices_from_response_dict(self.response_dict, device_types.SMOKE_DETECTOR) + for device in devices: + self.assertFalse(device.test_activated()) + def test_unit_is_none(self): devices = get_devices_from_response_dict(self.response_dict, device_types.SMOKE_DETECTOR) for device in devices: @@ -218,3 +224,24 @@ def test_unit_and_capability(self): remote = devices[0] self.assertIsNone(remote.unit()) self.assertEqual(remote.capability(), "opened") + + +class PropaneTankTests(unittest.TestCase): + + def setUp(self): + super(PropaneTankTests, self).setUp() + self.api_interface = mock.MagicMock() + all_devices = os.listdir('{}/api_responses/'.format(os.path.dirname(__file__))) + self.response_dict = {} + device_list = [] + for json_file in all_devices: + _json_file = open('{}/api_responses/{}'.format(os.path.dirname(__file__), json_file)) + device_list.append(json.load(_json_file)) + _json_file.close() + self.response_dict["data"] = device_list + + def test_unit_and_capability(self): + devices = get_devices_from_response_dict(self.response_dict, device_types.PROPANE_TANK) + tank = devices[0] + self.assertIsNone(tank.unit()) + self.assertIsNone(tank.capability()) diff --git a/src/pywink/test/devices/thermostat_test.py b/src/pywink/test/devices/thermostat_test.py index 1772f02..9bcb0be 100644 --- a/src/pywink/test/devices/thermostat_test.py +++ b/src/pywink/test/devices/thermostat_test.py @@ -55,7 +55,7 @@ def test_thermostat_users_away(self): thermostat = get_devices_from_response_dict(response_dict, device_types.THERMOSTAT)[0] self.assertTrue(thermostat.away()) - def test_thermostat_current_fan_modes(self): + def test_thermostat_current_fan_mode(self): device_list = [] response_dict = {} _json_file = open('{}/api_responses/go_control_thermostat.json'.format(os.path.dirname(__file__))) diff --git a/src/setup.py b/src/setup.py index f4ce2ab..b2d068c 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='1.0.0', + version='1.1.0', description='Access Wink devices via the Wink API', url='http://github.com/python-wink/python-wink', author='Brad Johnson', From 8dd90ed249decb3eb71641c4d6d9ecb0bf638ced Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Fri, 3 Feb 2017 08:28:40 -0500 Subject: [PATCH 139/178] Set object_id and object_type when creating device. (#72) --- CHANGELOG.md | 3 +++ src/pywink/devices/base.py | 18 +++++++++++++----- src/pywink/devices/light_bulb.py | 12 +++++------- src/setup.py | 2 +- 4 files changed, 22 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77586dc..705b663 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 1.1.1 +- Bugfix for lutron lights missing object_id and object_type + ## 1.1.0 - Support for Quirky Aros AC units - Support for Quirky Refuel diff --git a/src/pywink/devices/base.py b/src/pywink/devices/base.py index f1fcd86..298946e 100644 --- a/src/pywink/devices/base.py +++ b/src/pywink/devices/base.py @@ -12,6 +12,8 @@ def __init__(self, device_state_as_json, api_interface): self.json_state = device_state_as_json self.pubnub_key = None self.pubnub_channel = None + self.obj_id = self.json_state.get('object_id') + self.obj_type = self.json_state.get('object_type') subscription = self.json_state.get('subscription') if subscription != {} and subscription is not None: pubnub = subscription.get('pubnub') @@ -25,10 +27,10 @@ def state(self): raise NotImplementedError("Must implement state") def object_id(self): - return self.json_state.get('object_id') + return self.obj_id def object_type(self): - return self.json_state.get("object_type") + return self.obj_type @property def _last_reading(self): @@ -61,8 +63,11 @@ def _update_state_from_response(self, response_json): :return: """ _response_json = response_json.get('data') - self.json_state = _response_json - return True + if _response_json is not None: + self.json_state = _response_json + return True + else: + return False def update_state(self): """ Update state with latest info from Wink API. """ @@ -70,4 +75,7 @@ def update_state(self): return self._update_state_from_response(response) def pubnub_update(self, json_response): - self.json_state = json_response + if json_response is not None: + self.json_state = json_response + else: + self.update_state() diff --git a/src/pywink/devices/light_bulb.py b/src/pywink/devices/light_bulb.py index 5f11d85..fde9ce1 100644 --- a/src/pywink/devices/light_bulb.py +++ b/src/pywink/devices/light_bulb.py @@ -71,13 +71,11 @@ def set_state(self, state, brightness=None, desired_state = {"powered": state} color_state = self._format_color_data(color_hue_saturation, color_kelvin, color_xy) - desired_state.update(color_state) + if color_state is not None: + desired_state.update(color_state) - brightness = brightness if brightness is not None \ - else self.json_state.get('last_reading', {}).get('desired_brightness', 1) - desired_state.update({ - 'brightness': brightness - }) + if brightness is not None: + desired_state.update({'brightness': brightness}) response = self.api_interface.set_device_state(self, { "desired_state": desired_state @@ -86,7 +84,7 @@ def set_state(self, state, brightness=None, def _format_color_data(self, color_hue_saturation, color_kelvin, color_xy): if color_hue_saturation is None and color_kelvin is None and color_xy is None: - return {} + return None if self.supports_rgb(): rgb = _get_color_as_rgb(color_hue_saturation, color_kelvin, color_xy) diff --git a/src/setup.py b/src/setup.py index b2d068c..eb5868a 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='1.1.0', + version='1.1.1', description='Access Wink devices via the Wink API', url='http://github.com/python-wink/python-wink', author='Brad Johnson', From 7af4d3af29f5ef7b1b9622823665eddd289df439 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Tue, 21 Feb 2017 12:56:38 -0500 Subject: [PATCH 140/178] Support for robots and scenes (#73) --- CHANGELOG.md | 3 + src/pywink/__init__.py | 2 +- src/pywink/api.py | 44 +++++++---- src/pywink/devices/eggtray.py | 4 +- src/pywink/devices/factory.py | 6 ++ src/pywink/devices/key.py | 4 +- src/pywink/devices/light_bulb.py | 3 + src/pywink/devices/propane_tank.py | 4 +- src/pywink/devices/robot.py | 30 +++++++ src/pywink/devices/scene.py | 37 +++++++++ src/pywink/devices/smoke_detector.py | 4 +- src/pywink/devices/types.py | 4 +- src/pywink/test/api_test.py | 9 ++- .../test/devices/api_responses/robot.json | 79 +++++++++++++++++++ .../test/devices/api_responses/scene.json | 27 +++++++ src/pywink/test/devices/base_test.py | 15 ++-- src/setup.py | 2 +- 17 files changed, 244 insertions(+), 33 deletions(-) create mode 100644 src/pywink/devices/robot.py create mode 100644 src/pywink/devices/scene.py create mode 100644 src/pywink/test/devices/api_responses/robot.json create mode 100644 src/pywink/test/devices/api_responses/scene.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 705b663..2146950 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 1.2.0 +- Robot and Scene support + ## 1.1.1 - Bugfix for lutron lights missing object_id and object_type diff --git a/src/pywink/__init__.py b/src/pywink/__init__.py index 4c129f3..943901f 100644 --- a/src/pywink/__init__.py +++ b/src/pywink/__init__.py @@ -10,7 +10,7 @@ from pywink.api import get_light_bulbs, get_garage_doors, get_locks, \ get_powerstrips, get_shades, get_sirens, \ get_switches, get_thermostats, get_fans, get_air_conditioners, \ - get_propane_tanks + get_propane_tanks, get_robots, get_scenes from pywink.api import get_all_devices, get_eggtrays, get_sensors, \ get_keys, get_piggy_banks, get_smoke_and_co_detectors, \ diff --git a/src/pywink/api.py b/src/pywink/api.py index fd5c87f..8c55b0b 100644 --- a/src/pywink/api.py +++ b/src/pywink/api.py @@ -30,10 +30,14 @@ def set_device_state(self, device, state, id_override=None, type_override=None): url_string = "{}/{}s/{}".format(self.BASE_URL, object_type, object_id) - print(url_string) - arequest = requests.put(url_string, - data=json.dumps(state), - headers=API_HEADERS) + if state is None: + url_string += "/activate" + arequest = requests.post(url_string, + headers=API_HEADERS) + else: + arequest = requests.put(url_string, + data=json.dumps(state), + headers=API_HEADERS) if arequest.status_code == 401: new_token = refresh_access_token() if new_token: @@ -230,8 +234,16 @@ def get_propane_tanks(): return get_devices(device_types.PROPANE_TANK) +def get_robots(): + return get_devices(device_types.ROBOT, "robots") + + +def get_scenes(): + return get_devices(device_types.SCENE, "scenes") + + def get_subscription_key(): - response_dict = wink_api_fetch() + response_dict = wink_api_fetch('wink_devices') first_device = response_dict.get('data')[0] return get_subscription_key_from_response_dict(first_device) @@ -243,8 +255,8 @@ def get_subscription_key_from_response_dict(device): return None -def wink_api_fetch(): - arequest_url = "{}/users/me/wink_devices".format(WinkApiInterface.BASE_URL) +def wink_api_fetch(end_point): + arequest_url = "{}/users/me/{}".format(WinkApiInterface.BASE_URL, end_point) response = requests.get(arequest_url, headers=API_HEADERS) if response.status_code == 200: return response.json() @@ -255,15 +267,19 @@ def wink_api_fetch(): raise WinkAPIException("Unexpected") -def get_devices(device_type): +def get_devices(device_type, end_point="wink_devices"): global ALL_DEVICES, LAST_UPDATE - now = time.time() - # Only call the API once to obtain all devices - if LAST_UPDATE is None or (now - LAST_UPDATE) > 60: - ALL_DEVICES = wink_api_fetch() - LAST_UPDATE = now - return get_devices_from_response_dict(ALL_DEVICES, device_type) + if end_point == "wink_devices": + now = time.time() + # Only call the API once to obtain all devices + if LAST_UPDATE is None or (now - LAST_UPDATE) > 60: + ALL_DEVICES = wink_api_fetch(end_point) + LAST_UPDATE = now + return get_devices_from_response_dict(ALL_DEVICES, device_type) + elif end_point == "robots" or end_point == "scenes": + json_data = wink_api_fetch(end_point) + return get_devices_from_response_dict(json_data, device_type) def get_devices_from_response_dict(response_dict, device_type): diff --git a/src/pywink/devices/eggtray.py b/src/pywink/devices/eggtray.py index 4bffc70..eb41d60 100644 --- a/src/pywink/devices/eggtray.py +++ b/src/pywink/devices/eggtray.py @@ -8,12 +8,12 @@ class WinkEggtray(WinkDevice): def __init__(self, device_state_as_json, api_interface): super(WinkEggtray, self).__init__(device_state_as_json, api_interface) - self._cap = None + self._capability = None self._unit = "eggs" def capability(self): # Eggtray has no capability. - return self._cap + return self._capability def unit(self): return self._unit diff --git a/src/pywink/devices/factory.py b/src/pywink/devices/factory.py index 20252c2..e9da6fb 100644 --- a/src/pywink/devices/factory.py +++ b/src/pywink/devices/factory.py @@ -25,6 +25,8 @@ from pywink.devices.camera import WinkCanaryCamera from pywink.devices.air_conditioner import WinkAirConditioner from pywink.devices.propane_tank import WinkPropaneTank +from pywink.devices.robot import WinkRobot +from pywink.devices.scene import WinkScene # pylint: disable=redefined-variable-type,too-many-branches, too-many-statements @@ -97,6 +99,10 @@ def build_device(device_state_as_json, api_interface): new_object = WinkAirConditioner(device_state_as_json, api_interface) elif object_type == device_types.PROPANE_TANK: new_object = WinkPropaneTank(device_state_as_json, api_interface) + elif object_type == device_types.ROBOT: + new_object = WinkRobot(device_state_as_json, api_interface) + elif object_type == device_types.SCENE: + new_object = WinkScene(device_state_as_json, api_interface) if new_object is not None: return [new_object] diff --git a/src/pywink/devices/key.py b/src/pywink/devices/key.py index efd731d..a0d837f 100644 --- a/src/pywink/devices/key.py +++ b/src/pywink/devices/key.py @@ -10,7 +10,7 @@ def __init__(self, device_state_as_json, api_interface): super(WinkKey, self).__init__(device_state_as_json, api_interface) self._available = True self._unit = None - self._cap = "activity_detected" + self._capability = "activity_detected" def state(self): return self._last_reading.get(self.capability(), False) @@ -29,4 +29,4 @@ def unit(self): return self._unit def capability(self): - return self._cap + return self._capability diff --git a/src/pywink/devices/light_bulb.py b/src/pywink/devices/light_bulb.py index fde9ce1..3844f4b 100644 --- a/src/pywink/devices/light_bulb.py +++ b/src/pywink/devices/light_bulb.py @@ -18,6 +18,9 @@ def state(self): def brightness(self): return self._last_reading.get('brightness') + def color_model(self): + return self._last_reading.get('color_model') + def color_xy(self): """ XY colour value: [float, float] or None diff --git a/src/pywink/devices/propane_tank.py b/src/pywink/devices/propane_tank.py index d5daade..b25318d 100644 --- a/src/pywink/devices/propane_tank.py +++ b/src/pywink/devices/propane_tank.py @@ -8,12 +8,12 @@ class WinkPropaneTank(WinkDevice): def __init__(self, device_state_as_json, api_interface): super(WinkPropaneTank, self).__init__(device_state_as_json, api_interface) - self._cap = None + self._capability = None self._unit = None def capability(self): # Propane tanks have no capability. - return self._cap + return self._capability def unit(self): return self._unit diff --git a/src/pywink/devices/robot.py b/src/pywink/devices/robot.py new file mode 100644 index 0000000..83b1f3e --- /dev/null +++ b/src/pywink/devices/robot.py @@ -0,0 +1,30 @@ +from pywink.devices.base import WinkDevice + + +class WinkRobot(WinkDevice): + """ + Represents a Wink robot. + """ + + def __init__(self, device_state_as_json, api_interface): + super(WinkRobot, self).__init__(device_state_as_json, api_interface) + self._available = True + self._capability = "fired" + self._unit = None + + def state(self): + return self._last_reading.get(self.capability(), False) + + def available(self): + """ + Robots are virtual therefore they don't have a connection status + always return True. + """ + return self._available + + def unit(self): + # Robots are a boolean sensor, they have no unit. + return self._unit + + def capability(self): + return self._capability diff --git a/src/pywink/devices/scene.py b/src/pywink/devices/scene.py new file mode 100644 index 0000000..0c5e0b6 --- /dev/null +++ b/src/pywink/devices/scene.py @@ -0,0 +1,37 @@ +from pywink.devices.base import WinkDevice + + +class WinkScene(WinkDevice): + """ + Represents a Wink scene. + """ + + def __init__(self, device_state_as_json, api_interface): + super(WinkScene, self).__init__(device_state_as_json, api_interface) + + def state(self): + """ + Scenes don't have a state, they can only be triggered. + Always return a state of False. + """ + return False + + def available(self): + """ + Scenes are virtual therefore they don't have a connection status + always return True. + """ + return True + + def activate(self): + """ + Activate the scene. + """ + response = self.api_interface.set_device_state(self, None) + self._update_state_from_response(response) + + def update_state(self): + """ + Nothing changes in the JSON state of this device. + """ + return True diff --git a/src/pywink/devices/smoke_detector.py b/src/pywink/devices/smoke_detector.py index fdce496..f35454c 100644 --- a/src/pywink/devices/smoke_detector.py +++ b/src/pywink/devices/smoke_detector.py @@ -8,7 +8,7 @@ def __init__(self, device_state_as_json, api_interface, unit_type, capability): super(WinkBaseSmokeDetector, self).__init__(device_state_as_json, api_interface) self._unit = None self._unit_type = unit_type - self._cap = capability + self._capability = capability def unit(self): return self._unit @@ -17,7 +17,7 @@ def unit_type(self): return self._unit_type def capability(self): - return self._cap + return self._capability def name(self): return self.json_state.get("name") + " " + self.capability() diff --git a/src/pywink/devices/types.py b/src/pywink/devices/types.py index b5c42fa..af92974 100644 --- a/src/pywink/devices/types.py +++ b/src/pywink/devices/types.py @@ -25,9 +25,11 @@ CAMERA = 'camera' AIR_CONDITIONER = 'air_conditioner' PROPANE_TANK = 'propane_tank' +ROBOT = 'robot' +SCENE = 'scene' ALL_SUPPORTED_DEVICES = [LIGHT_BULB, BINARY_SWITCH, SENSOR_POD, LOCK, EGGTRAY, GARAGE_DOOR, POWERSTRIP, SHADE, SIREN, KEY, PIGGY_BANK, SMOKE_DETECTOR, THERMOSTAT, HUB, FAN, DOOR_BELL, REMOTE, SPRINKLER, BUTTON, GANG, CAMERA, AIR_CONDITIONER, - PROPANE_TANK] + PROPANE_TANK, ROBOT, SCENE] diff --git a/src/pywink/test/api_test.py b/src/pywink/test/api_test.py index 4dc2f61..7885a56 100644 --- a/src/pywink/test/api_test.py +++ b/src/pywink/test/api_test.py @@ -36,6 +36,8 @@ from pywink.devices.camera import WinkCanaryCamera from pywink.devices.air_conditioner import WinkAirConditioner from pywink.devices.propane_tank import WinkPropaneTank +from pywink.devices.scene import WinkScene +from pywink.devices.robot import WinkRobot USERS_ME_WINK_DEVICES = {} @@ -83,7 +85,7 @@ def test_get_subscription_key(self): def test_get_all_devices_from_api(self): WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) devices = get_all_devices() - self.assertEqual(len(devices), 61) + self.assertEqual(len(devices), 63) lights = get_light_bulbs() for light in lights: self.assertTrue(isinstance(light, WinkLightBulb)) @@ -150,8 +152,9 @@ def test_get_sensor_and_binary_switch_updated_states_from_api(self): WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) sensor_types = [WinkSensor, WinkHub, WinkPorkfolioBalanceSensor, WinkKey, WinkRemote, WinkGang, WinkSmokeDetector, WinkSmokeSeverity, - WinkCoDetector, WinkCoSeverity, WinkButton] - skip_types = [WinkPowerStripOutlet, WinkCanaryCamera] + WinkCoDetector, WinkCoSeverity, WinkButton, WinkRobot] + # No way to validate scene is activated, so skipping. + skip_types = [WinkPowerStripOutlet, WinkCanaryCamera, WinkScene] devices = get_all_devices() old_states = {} for device in devices: diff --git a/src/pywink/test/devices/api_responses/robot.json b/src/pywink/test/devices/api_responses/robot.json new file mode 100644 index 0000000..376db0a --- /dev/null +++ b/src/pywink/test/devices/api_responses/robot.json @@ -0,0 +1,79 @@ +{ + "robot_id": "4830303", + "name": "Test robot", + "enabled": true, + "creating_actor_type": "user", + "creating_actor_id": "377857", + "automation_mode": null, + "fired_limit": 0, + "last_fired": null, + "object_type": "robot", + "object_id": "4830303", + "uuid": "ca476195-6780-46ddXXXXXXXXXXXXXXXXXx", + "icon_id": "191", + "icon_code": "robot-robot", + "desired_state": { + "enabled": true, + "fired_limit": 0 + }, + "last_reading": { + "fired_true": "N/A", + "fired_true_updated_at": null, + "enabled": true, + "enabled_updated_at": 1487361095.4796422, + "fired_limit": 0, + "fired_limit_updated_at": null, + "fired_count": 0, + "fired_count_updated_at": null, + "fired": false, + "fired_updated_at": null, + "failure_email_sent": null, + "failure_email_sent_updated_at": null, + "desired_enabled_updated_at": 1487361228.0143569, + "desired_fired_limit_updated_at": 1487361228.0143569, + "desired_enabled_changed_at": 1487361095.483048, + "desired_fired_limit_changed_at": 1487361228.0143569 + }, + "subscription": { + "pubnub": { + "subscribe_key": "sub-c-XXXXXXXXXXx-XXXXXXXXXXXXXXXx", + "channel": "1340f4f4ec705946fXXXXXXXXXXXXx" + } + }, + "effects": [ + { + "effect_id": "8966813", + "robot_id": "4830303", + "notification_type": "push", + "recipient_actor_type": "user", + "recipient_actor_id": "377857", + "scene": null, + "note": null, + "reference_object_type": null, + "reference_object_id": null, + "object_type": "effect", + "object_id": "8966813" + } + ], + "causes": [ + { + "next_at": null, + "recurrence": null, + "object_type": "condition", + "object_id": "7534749", + "condition_id": "7534749", + "robot_id": "4830303", + "observed_object_id": "4438018", + "observed_object_type": "group", + "observed_field": "liquid_detected.or", + "operator": "==", + "value": "true", + "delay": null, + "restricted_object_id": null, + "restricted_object_type": null, + "restriction_join": null, + "restrictions": [] + } + ], + "restrictions": [] +} diff --git a/src/pywink/test/devices/api_responses/scene.json b/src/pywink/test/devices/api_responses/scene.json new file mode 100644 index 0000000..53a92de --- /dev/null +++ b/src/pywink/test/devices/api_responses/scene.json @@ -0,0 +1,27 @@ +{ + "scene_id": "5156247", + "name": "New Shortcut", + "order": 0, + "members": [ + { + "object_type": "binary_switch", + "object_id": "292860", + "desired_state": { + "powered": true + }, + "local_scene_id": null + } + ], + "icon_id": "224", + "automation_mode": null, + "object_type": "scene", + "object_id": "5156247", + "uuid": "b0e859ba-a270-4139-8XXXXXXXXXX", + "icon_code": "scene-shortcut", + "subscription": { + "pubnub": { + "subscribe_key": "sub-c-XXXXXXXXXXXXXXXXXXXXXX", + "channel": "2c78283d58f4559a2" + } + } +} diff --git a/src/pywink/test/devices/base_test.py b/src/pywink/test/devices/base_test.py index 37ab5cc..c970b88 100644 --- a/src/pywink/test/devices/base_test.py +++ b/src/pywink/test/devices/base_test.py @@ -24,6 +24,8 @@ from pywink.devices.camera import WinkCanaryCamera from pywink.devices.air_conditioner import WinkAirConditioner from pywink.devices.propane_tank import WinkPropaneTank +from pywink.devices.scene import WinkScene +from pywink.devices.robot import WinkRobot class BaseTests(unittest.TestCase): @@ -81,7 +83,7 @@ def test_all_devices_battery_is_valid(self): skip_types = [WinkFan, WinkPorkfolioBalanceSensor, WinkPorkfolioNose, WinkBinarySwitch, WinkHub, WinkLightBulb, WinkThermostat, WinkKey, WinkPowerStrip, WinkPowerStripOutlet, WinkRemote, WinkShade, WinkSprinkler, WinkButton, WinkGang, WinkCanaryCamera, - WinkAirConditioner] + WinkAirConditioner, WinkScene, WinkRobot] for device in devices: if device.manufacturer_device_model() == "leaksmart_valve": self.assertIsNotNone(device.battery_level()) @@ -100,10 +102,11 @@ def test_all_devices_manufacturer_device_model_state_is_valid(self): devices = get_devices_from_response_dict(self.response_dict, device_types.ALL_SUPPORTED_DEVICES) skip_types = [WinkKey, WinkPorkfolioBalanceSensor, WinkPorkfolioNose, WinkPowerStripOutlet, WinkSiren, WinkEggtray, WinkRemote, WinkPowerStrip, WinkAirConditioner, WinkPropaneTank] + devices_with_no_device_model = ["GoControl Thermostat", "New Shortcut", "Test robot"] for device in devices: if type(device) in skip_types: self.assertIsNone(device.manufacturer_device_model()) - elif device.name() == "GoControl Thermostat": + elif device.name() in devices_with_no_device_model: self.assertIsNone(device.manufacturer_device_model()) else: self.assertIsNotNone(device.manufacturer_device_model()) @@ -116,7 +119,7 @@ def test_all_devices_manufacturer_device_id_state_is_valid(self): "ge_bulb", "quirky_ge_spotter", "schlage_zwave_lock", "home_decorators_home_decorators_fan", "sylvania_sylvania_rgbw", "somfy_bali", "wink_relay_sensor", "wink_project_one", "kidde_smoke_alarm", "wink_relay_switch", "leaksmart_valve"] - skip_names = ["GoControl Thermostat", "GE Zwave Switch"] + skip_names = ["GoControl Thermostat", "GE Zwave Switch", "New Shortcut", "Test robot"] for device in devices: if device.name() in skip_names: self.assertIsNone(device.manufacturer_device_id()) @@ -129,10 +132,11 @@ def test_all_devices_manufacturer_device_id_state_is_valid(self): def test_all_devices_device_manufacturer_is_valid(self): devices = get_devices_from_response_dict(self.response_dict, device_types.ALL_SUPPORTED_DEVICES) + device_with_no_manufacturer = ["GoControl Thermostat", "New Shortcut", "Test robot"] for device in devices: if type(device) is WinkKey: self.assertIsNone(device.device_manufacturer()) - elif device.name() == "GoControl Thermostat": + elif device.name() in device_with_no_manufacturer: self.assertIsNone(device.device_manufacturer()) elif type(device) is WinkPowerStripOutlet: self.assertIsNone(device.device_manufacturer()) @@ -141,10 +145,11 @@ def test_all_devices_device_manufacturer_is_valid(self): def test_all_devices_model_name_is_valid(self): devices = get_devices_from_response_dict(self.response_dict, device_types.ALL_SUPPORTED_DEVICES) + devices_with_no_model_name = ["GoControl Thermostat", "New Shortcut", "Test robot"] for device in devices: if type(device) is WinkKey: self.assertIsNone(device.model_name()) - elif device.name() == "GoControl Thermostat": + elif device.name() in devices_with_no_model_name: self.assertIsNone(device.model_name()) elif type(device) is WinkPowerStripOutlet: self.assertIsNone(device.model_name()) diff --git a/src/setup.py b/src/setup.py index eb5868a..63c43da 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='1.1.1', + version='1.2.0', description='Access Wink devices via the Wink API', url='http://github.com/python-wink/python-wink', author='Brad Johnson', From 3a40fc38f4e0c344ce083381158fa14f29a7be55 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Sat, 11 Mar 2017 09:12:42 -0500 Subject: [PATCH 141/178] Set api fetch default endpoint (#75) * Set default endpoint --- CHANGELOG.md | 3 +++ src/pywink/api.py | 4 ++-- src/setup.py | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2146950..adeb5a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 1.2.1 +- Set default endpoint in wink_api_fetch + ## 1.2.0 - Robot and Scene support diff --git a/src/pywink/api.py b/src/pywink/api.py index 8c55b0b..37f777f 100644 --- a/src/pywink/api.py +++ b/src/pywink/api.py @@ -243,7 +243,7 @@ def get_scenes(): def get_subscription_key(): - response_dict = wink_api_fetch('wink_devices') + response_dict = wink_api_fetch() first_device = response_dict.get('data')[0] return get_subscription_key_from_response_dict(first_device) @@ -255,7 +255,7 @@ def get_subscription_key_from_response_dict(device): return None -def wink_api_fetch(end_point): +def wink_api_fetch(end_point='wink_devices'): arequest_url = "{}/users/me/{}".format(WinkApiInterface.BASE_URL, end_point) response = requests.get(arequest_url, headers=API_HEADERS) if response.status_code == 200: diff --git a/src/setup.py b/src/setup.py index 63c43da..17ed8dd 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='1.2.0', + version='1.2.1', description='Access Wink devices via the Wink API', url='http://github.com/python-wink/python-wink', author='Brad Johnson', From 788e9e54abb2bdf8587f461ea33bf3d85e4f2edf Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Fri, 24 Mar 2017 19:08:09 -0400 Subject: [PATCH 142/178] Fix sirens (#78) * Added update_state to siren for correct object_type override --- CHANGELOG.md | 3 +++ src/pywink/devices/siren.py | 15 ++++++++++++--- src/setup.py | 2 +- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index adeb5a9..5196a23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 1.2.2 +- Siren inherts from Base device + ## 1.2.1 - Set default endpoint in wink_api_fetch diff --git a/src/pywink/devices/siren.py b/src/pywink/devices/siren.py index 1233ce6..0fd53ea 100644 --- a/src/pywink/devices/siren.py +++ b/src/pywink/devices/siren.py @@ -1,7 +1,7 @@ -from pywink.devices.binary_switch import WinkBinarySwitch +from pywink.devices.base import WinkDevice -class WinkSiren(WinkBinarySwitch): +class WinkSiren(WinkDevice): """ Represents a Wink Siren. """ @@ -48,5 +48,14 @@ def update_state(self): """ Update state with latest info from Wink API. """ - response = self.api_interface.get_device_state(self, type_override="siren") + response = self.api_interface.get_device_state(self) return self._update_state_from_response(response) + + def set_state(self, state): + """ + :param state: a boolean of true (on) or false ('off') + :return: nothing + """ + values = {"desired_state": {"powered": state}} + response = self.api_interface.set_device_state(self, values) + self._update_state_from_response(response) diff --git a/src/setup.py b/src/setup.py index 17ed8dd..367f399 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='1.2.1', + version='1.2.2', description='Access Wink devices via the Wink API', url='http://github.com/python-wink/python-wink', author='Brad Johnson', From 89cbfe22fe45780a2df52d78eeda34fa30d5933b Mon Sep 17 00:00:00 2001 From: geekofweek Date: Fri, 24 Mar 2017 19:12:14 -0500 Subject: [PATCH 143/178] Wink Aros Fix (#77) * Wink Aros Fix --- CHANGELOG.md | 3 +++ src/pywink/devices/air_conditioner.py | 2 +- src/pywink/test/api_test.py | 2 +- src/setup.py | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5196a23..fccc305 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 1.2.3 +- Wink Aros Bugfix + ## 1.2.2 - Siren inherts from Base device diff --git a/src/pywink/devices/air_conditioner.py b/src/pywink/devices/air_conditioner.py index c22ddde..7b5f618 100644 --- a/src/pywink/devices/air_conditioner.py +++ b/src/pywink/devices/air_conditioner.py @@ -71,7 +71,7 @@ def set_ac_fan_speed(self, speed): self._update_state_from_response(response) - def set_mode(self, mode): + def set_operation_mode(self, mode): """ :param mode: a string one of ["off", "auto_eco", "cool_only", "fan_only"] :return: nothing diff --git a/src/pywink/test/api_test.py b/src/pywink/test/api_test.py index 7885a56..48741f8 100644 --- a/src/pywink/test/api_test.py +++ b/src/pywink/test/api_test.py @@ -290,7 +290,7 @@ def test_get_air_conditioner_updated_states_from_api(self): for device in devices: device.api_interface = self.api_interface old_states[device.object_id()] = device.state() - device.set_mode("cool_only") + device.set_operation_mode("cool_only") device.set_temperature(70) device.set_schedule_enabled(False) device.set_ac_fan_speed(0.5) diff --git a/src/setup.py b/src/setup.py index 367f399..1a8f820 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='1.2.2', + version='1.2.3', description='Access Wink devices via the Wink API', url='http://github.com/python-wink/python-wink', author='Brad Johnson', From 92340ef72fc0bbe144367720648fc3a552623267 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Tue, 11 Apr 2017 12:47:50 -0400 Subject: [PATCH 144/178] Added call to /users/me (#82) --- CHANGELOG.md | 3 ++ src/pywink/__init__.py | 2 +- src/pywink/api.py | 6 ++++ src/pywink/test/user.json | 62 +++++++++++++++++++++++++++++++++++++++ src/setup.py | 2 +- 5 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 src/pywink/test/user.json diff --git a/CHANGELOG.md b/CHANGELOG.md index fccc305..e6c6fe9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 1.2.4 +- Added call to return users account details /users/me + ## 1.2.3 - Wink Aros Bugfix diff --git a/src/pywink/__init__.py b/src/pywink/__init__.py index 943901f..c1107c3 100644 --- a/src/pywink/__init__.py +++ b/src/pywink/__init__.py @@ -5,7 +5,7 @@ from pywink.api import set_bearer_token, refresh_access_token, \ set_wink_credentials, set_user_agent, wink_api_fetch, \ get_set_access_token, is_token_set, get_devices, \ - get_subscription_key + get_subscription_key, get_user from pywink.api import get_light_bulbs, get_garage_doors, get_locks, \ get_powerstrips, get_shades, get_sirens, \ diff --git a/src/pywink/api.py b/src/pywink/api.py index 37f777f..d01db19 100644 --- a/src/pywink/api.py +++ b/src/pywink/api.py @@ -133,6 +133,12 @@ def refresh_access_token(): return None +def get_user(): + url_string = "{}/users/me".format(WinkApiInterface.BASE_URL) + arequest = requests.get(url_string, headers=API_HEADERS) + return arequest.json() + + def is_token_set(): """ Returns if an auth token has been set. """ return bool(API_HEADERS) diff --git a/src/pywink/test/user.json b/src/pywink/test/user.json new file mode 100644 index 0000000..9fc3770 --- /dev/null +++ b/src/pywink/test/user.json @@ -0,0 +1,62 @@ +{ + "data":{ + "user_id":"377857", + "first_name":"First", + "last_name":"Last", + "email":"first.last@wink.com", + "locale":"en_us", + "units":{ + + }, + "tos_accepted":true, + "confirmed":true, + "last_reading":{ + "units":{ + "temperature":"f" + }, + "units_updated_at":1453091233.737727, + "current_location":null, + "current_location_updated_at":null, + "home_geofence_id":null, + "home_geofence_id_updated_at":null, + "robot_subscription":null, + "robot_subscription_updated_at":null, + "current_location_data":null, + "current_location_data_updated_at":null, + "general_tos_version":"4", + "general_tos_version_updated_at":1466634432.1567028, + "premium_tos_version":null, + "premium_tos_version_updated_at":null, + "feature_flags":[ + "faster_lights" + ], + "feature_flags_updated_at":1455056917.7954974, + "desired_units":null, + "desired_units_updated_at":1453091233.7842777, + "desired_current_location_updated_at":1453091393.6379838, + "desired_home_geofence_id_updated_at":1453091393.6379838, + "desired_robot_subscription":null, + "desired_robot_subscription_updated_at":null + }, + "created_at":1449441909, + "uuid":"5c063d24-4ec0-4076-80ea-97b2252b4f59", + "desired_state":{ + "units":null, + "current_location":null, + "home_geofence_id":null, + "robot_subscription":null + }, + "subscription":{ + "pubnub":{ + "subscribe_key":"sub-c-123456789123456789", + "channel":"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" + } + } + }, + "errors":[ + + ], + "pagination":{ + + } +} diff --git a/src/setup.py b/src/setup.py index 1a8f820..39951ff 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='1.2.3', + version='1.2.4', description='Access Wink devices via the Wink API', url='http://github.com/python-wink/python-wink', author='Brad Johnson', From 45a2207bbc31893c9ef0eae30f3ef5af6952e54d Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Mon, 29 May 2017 12:39:34 -0400 Subject: [PATCH 145/178] Add methods for obtaining an authorization URL and getting an access token via an oauth code. (#84) --- CHANGELOG.md | 3 ++ src/pywink/__init__.py | 3 +- src/pywink/api.py | 48 ++++++++++++++--- src/pywink/devices/air_conditioner.py | 3 -- src/pywink/devices/base.py | 6 +-- src/pywink/devices/binary_switch.py | 6 --- src/pywink/devices/button.py | 3 -- src/pywink/devices/camera.py | 3 -- src/pywink/devices/factory.py | 74 ++++++++++++--------------- src/pywink/devices/fan.py | 3 -- src/pywink/devices/garage_door.py | 3 -- src/pywink/devices/light_bulb.py | 3 -- src/pywink/devices/lock.py | 3 -- src/pywink/devices/powerstrip.py | 6 --- src/pywink/devices/scene.py | 3 -- src/pywink/devices/shade.py | 3 -- src/pywink/devices/siren.py | 3 -- src/pywink/devices/sprinkler.py | 3 -- src/pywink/devices/thermostat.py | 3 -- src/pywink/test/api_test.py | 2 +- src/setup.py | 2 +- 21 files changed, 83 insertions(+), 103 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6c6fe9..a007eb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 1.2.5 +- Added functions for auth URL and getting token from code + ## 1.2.4 - Added call to return users account details /users/me diff --git a/src/pywink/__init__.py b/src/pywink/__init__.py index c1107c3..8944ec7 100644 --- a/src/pywink/__init__.py +++ b/src/pywink/__init__.py @@ -5,7 +5,8 @@ from pywink.api import set_bearer_token, refresh_access_token, \ set_wink_credentials, set_user_agent, wink_api_fetch, \ get_set_access_token, is_token_set, get_devices, \ - get_subscription_key, get_user + get_subscription_key, get_user, get_authorization_url, \ + request_token, legacy_set_wink_credentials from pywink.api import get_light_bulbs, get_garage_doors, get_locks, \ get_powerstrips, get_shades, get_sirens, \ diff --git a/src/pywink/api.py b/src/pywink/api.py index d01db19..5282c51 100644 --- a/src/pywink/api.py +++ b/src/pywink/api.py @@ -1,5 +1,6 @@ import json import time +import urllib.parse import requests @@ -13,6 +14,7 @@ USER_AGENT = "Manufacturer/python-wink python/3 Wink/3" ALL_DEVICES = None LAST_UPDATE = None +OAUTH_AUTHORIZE = "{}/oauth2/authorize?client_id={}&redirect_uri={}" class WinkApiInterface(object): @@ -65,8 +67,7 @@ def get_set_access_token(): auth = API_HEADERS.get("Authorization") if auth is not None: return auth.split()[1] - else: - return None + return None def set_bearer_token(token): @@ -86,7 +87,7 @@ def set_user_agent(user_agent): USER_AGENT = user_agent -def set_wink_credentials(email, password, client_id, client_secret): +def legacy_set_wink_credentials(email, password, client_id, client_secret): global CLIENT_ID, CLIENT_SECRET, REFRESH_TOKEN CLIENT_ID = client_id @@ -111,6 +112,15 @@ def set_wink_credentials(email, password, client_id, client_secret): set_bearer_token(access_token) +def set_wink_credentials(client_id, client_secret, access_token, refresh_token): + global CLIENT_ID, CLIENT_SECRET, REFRESH_TOKEN + + CLIENT_ID = client_id + CLIENT_SECRET = client_secret + REFRESH_TOKEN = refresh_token + set_bearer_token(access_token) + + def refresh_access_token(): if CLIENT_ID and CLIENT_SECRET and REFRESH_TOKEN: data = { @@ -129,8 +139,33 @@ def refresh_access_token(): access_token = response_json.get('access_token') set_bearer_token(access_token) return access_token - else: - return None + return None + + +def get_authorization_url(client_id, redirect_uri): + global CLIENT_ID + + CLIENT_ID = client_id + encoded_uri = urllib.parse.quote(redirect_uri) + return OAUTH_AUTHORIZE.format(WinkApiInterface.BASE_URL, client_id, encoded_uri) + + +def request_token(code, client_secret): + data = { + "client_secret": client_secret, + "grant_type": "authorization_code", + "code": code + } + headers = { + 'Content-Type': 'application/json' + } + response = requests.post('{}/oauth2/token'.format(WinkApiInterface.BASE_URL), + data=json.dumps(data), + headers=headers) + response_json = response.json() + access_token = response_json.get('access_token') + refresh_token = response_json.get('refresh_token') + return {"access_token": access_token, "refresh_token": refresh_token} def get_user(): @@ -257,8 +292,7 @@ def get_subscription_key(): def get_subscription_key_from_response_dict(device): if "subscription" in device: return device.get("subscription").get("pubnub").get("subscribe_key") - else: - return None + return None def wink_api_fetch(end_point='wink_devices'): diff --git a/src/pywink/devices/air_conditioner.py b/src/pywink/devices/air_conditioner.py index 7b5f618..d9e994a 100644 --- a/src/pywink/devices/air_conditioner.py +++ b/src/pywink/devices/air_conditioner.py @@ -7,9 +7,6 @@ class WinkAirConditioner(WinkDevice): Represents a Wink air conditioner. """ - def __init__(self, device_state_as_json, api_interface): - super(WinkAirConditioner, self).__init__(device_state_as_json, api_interface) - def state(self): return self.current_mode() diff --git a/src/pywink/devices/base.py b/src/pywink/devices/base.py index 298946e..8d5c06a 100644 --- a/src/pywink/devices/base.py +++ b/src/pywink/devices/base.py @@ -42,8 +42,7 @@ def available(self): def battery_level(self): if not self._last_reading.get('external_power'): return self._last_reading.get('battery') - else: - return None + return None def manufacturer_device_model(self): return self.json_state.get('manufacturer_device_model') @@ -66,8 +65,7 @@ def _update_state_from_response(self, response_json): if _response_json is not None: self.json_state = _response_json return True - else: - return False + return False def update_state(self): """ Update state with latest info from Wink API. """ diff --git a/src/pywink/devices/binary_switch.py b/src/pywink/devices/binary_switch.py index a1a37c6..e60b098 100644 --- a/src/pywink/devices/binary_switch.py +++ b/src/pywink/devices/binary_switch.py @@ -6,9 +6,6 @@ class WinkBinarySwitch(WinkDevice): Represents a Wink binary switch. """ - def __init__(self, device_state_as_json, api_interface): - super(WinkBinarySwitch, self).__init__(device_state_as_json, api_interface) - def state(self): return self._last_reading.get('powered', False) @@ -34,9 +31,6 @@ class WinkLeakSmartValve(WinkBinarySwitch): Represents a Wink leaksmart valve.. """ - def __init__(self, device_state_as_json, api_interface): - super(WinkLeakSmartValve, self).__init__(device_state_as_json, api_interface) - def state(self): return self._last_reading.get('opened', False) diff --git a/src/pywink/devices/button.py b/src/pywink/devices/button.py index 7bc1880..46425e3 100644 --- a/src/pywink/devices/button.py +++ b/src/pywink/devices/button.py @@ -6,9 +6,6 @@ class WinkButton(WinkBinarySwitch): Represents a Wink relay button. """ - def __init__(self, device_state_as_json, api_interface): - super(WinkButton, self).__init__(device_state_as_json, api_interface) - def state(self): return bool(self.long_pressed() or self.pressed()) diff --git a/src/pywink/devices/camera.py b/src/pywink/devices/camera.py index 4805ac8..a808445 100644 --- a/src/pywink/devices/camera.py +++ b/src/pywink/devices/camera.py @@ -9,9 +9,6 @@ class WinkCanaryCamera(WinkDevice): are not listed in the device's JSON. """ - def __init__(self, device_state_as_json, api_interface): - super(WinkCanaryCamera, self).__init__(device_state_as_json, api_interface) - def state(self): return self.mode() diff --git a/src/pywink/devices/factory.py b/src/pywink/devices/factory.py index e9da6fb..e932445 100644 --- a/src/pywink/devices/factory.py +++ b/src/pywink/devices/factory.py @@ -29,87 +29,79 @@ from pywink.devices.scene import WinkScene -# pylint: disable=redefined-variable-type,too-many-branches, too-many-statements +# pylint: disable=too-many-branches, too-many-statements def build_device(device_state_as_json, api_interface): - - new_object = None - new_objects = None - - # These objects all share the same base class: WinkDevice + # This is used to determine what type of object to create object_type = device_state_as_json.get("object_type") + new_objects = [] if object_type == device_types.LIGHT_BULB: - new_object = WinkLightBulb(device_state_as_json, api_interface) + new_objects.append(WinkLightBulb(device_state_as_json, api_interface)) elif object_type == device_types.BINARY_SWITCH: # Skip relay switches that aren't controlling a load. The binary_switch can't be used. if device_state_as_json.get("last_reading").get("powering_mode") is not None: mode = device_state_as_json["last_reading"]["powering_mode"] if mode == "dumb": - new_object = WinkBinarySwitch(device_state_as_json, api_interface) + new_objects.append(WinkBinarySwitch(device_state_as_json, api_interface)) elif device_state_as_json.get("model_name") == "leakSMART Valve": - new_object = WinkLeakSmartValve(device_state_as_json, api_interface) + new_objects.append(WinkLeakSmartValve(device_state_as_json, api_interface)) else: - new_object = WinkBinarySwitch(device_state_as_json, api_interface) + new_objects.append(WinkBinarySwitch(device_state_as_json, api_interface)) elif object_type == device_types.LOCK: - new_object = WinkLock(device_state_as_json, api_interface) + new_objects.append(WinkLock(device_state_as_json, api_interface)) elif object_type == device_types.EGGTRAY: - new_object = WinkEggtray(device_state_as_json, api_interface) + new_objects.append(WinkEggtray(device_state_as_json, api_interface)) elif object_type == device_types.GARAGE_DOOR: - new_object = WinkGarageDoor(device_state_as_json, api_interface) + new_objects.append(WinkGarageDoor(device_state_as_json, api_interface)) elif object_type == device_types.SHADE: - new_object = WinkShade(device_state_as_json, api_interface) + new_objects.append(WinkShade(device_state_as_json, api_interface)) elif object_type == device_types.SIREN: - new_object = WinkSiren(device_state_as_json, api_interface) + new_objects.append(WinkSiren(device_state_as_json, api_interface)) elif object_type == device_types.KEY: - new_object = WinkKey(device_state_as_json, api_interface) + new_objects.append(WinkKey(device_state_as_json, api_interface)) elif object_type == device_types.THERMOSTAT: - new_object = WinkThermostat(device_state_as_json, api_interface) + new_objects.append(WinkThermostat(device_state_as_json, api_interface)) elif object_type == device_types.FAN: - new_object = WinkFan(device_state_as_json, api_interface) + new_objects.append(WinkFan(device_state_as_json, api_interface)) elif object_type == device_types.REMOTE: # The lutron Pico remote doesn't follow the API spec and # provides no benefit as a device in this library. if device_state_as_json.get("model_name") != "Pico": - new_object = WinkRemote(device_state_as_json, api_interface) + new_objects.append(WinkRemote(device_state_as_json, api_interface)) elif object_type == device_types.HUB: - new_object = WinkHub(device_state_as_json, api_interface) + new_objects.append(WinkHub(device_state_as_json, api_interface)) elif object_type == device_types.SENSOR_POD: - new_objects = __get_subsensors_from_device(device_state_as_json, api_interface) + new_objects.extend(__get_subsensors_from_device(device_state_as_json, api_interface)) elif object_type == device_types.POWERSTRIP: - new_objects = __get_outlets_from_powerstrip(device_state_as_json, api_interface) + new_objects.extend(__get_outlets_from_powerstrip(device_state_as_json, api_interface)) new_objects.append(WinkPowerStrip(device_state_as_json, api_interface)) elif object_type == device_types.PIGGY_BANK: - new_objects = __get_devices_from_piggy_bank(device_state_as_json, api_interface) + new_objects.extend(__get_devices_from_piggy_bank(device_state_as_json, api_interface)) elif object_type == device_types.DOOR_BELL: - new_objects = __get_subsensors_from_device(device_state_as_json, api_interface) + new_objects.extend(__get_subsensors_from_device(device_state_as_json, api_interface)) elif object_type == device_types.SPRINKLER: - new_object = WinkSprinkler(device_state_as_json, api_interface) + new_objects.append(WinkSprinkler(device_state_as_json, api_interface)) elif object_type == device_types.BUTTON: - new_object = WinkButton(device_state_as_json, api_interface) + new_objects.append(WinkButton(device_state_as_json, api_interface)) elif object_type == device_types.GANG: - new_object = WinkGang(device_state_as_json, api_interface) + new_objects.append(WinkGang(device_state_as_json, api_interface)) elif object_type == device_types.SMOKE_DETECTOR: - new_objects = __get_sensors_from_smoke_detector(device_state_as_json, api_interface) + new_objects.extend(__get_sensors_from_smoke_detector(device_state_as_json, api_interface)) elif object_type == device_types.CAMERA: if device_state_as_json.get("device_manufacturer") == "canary": - new_object = WinkCanaryCamera(device_state_as_json, api_interface) + new_objects.append(WinkCanaryCamera(device_state_as_json, api_interface)) elif device_state_as_json.get("device_manufacturer") == "dropcam": - new_objects = __get_subsensors_from_device(device_state_as_json, api_interface) + new_objects.extend(__get_subsensors_from_device(device_state_as_json, api_interface)) elif object_type == device_types.AIR_CONDITIONER: - new_object = WinkAirConditioner(device_state_as_json, api_interface) + new_objects.append(WinkAirConditioner(device_state_as_json, api_interface)) elif object_type == device_types.PROPANE_TANK: - new_object = WinkPropaneTank(device_state_as_json, api_interface) + new_objects.append(WinkPropaneTank(device_state_as_json, api_interface)) elif object_type == device_types.ROBOT: - new_object = WinkRobot(device_state_as_json, api_interface) + new_objects.append(WinkRobot(device_state_as_json, api_interface)) elif object_type == device_types.SCENE: - new_object = WinkScene(device_state_as_json, api_interface) - - if new_object is not None: - return [new_object] - elif new_objects is not None: - return new_objects - else: - return [] + new_objects.append(WinkScene(device_state_as_json, api_interface)) + + return new_objects def __get_subsensors_from_device(item, api_interface): diff --git a/src/pywink/devices/fan.py b/src/pywink/devices/fan.py index aa578e8..2dd7d9d 100644 --- a/src/pywink/devices/fan.py +++ b/src/pywink/devices/fan.py @@ -8,9 +8,6 @@ class WinkFan(WinkDevice): """ json_state = {} - def __init__(self, device_state_as_json, api_interface): - super(WinkFan, self).__init__(device_state_as_json, api_interface) - def fan_speeds(self): capabilities = self.json_state.get('capabilities', {}) cap_fields = capabilities.get('fields', []) diff --git a/src/pywink/devices/garage_door.py b/src/pywink/devices/garage_door.py index 6109f62..ccb7e85 100644 --- a/src/pywink/devices/garage_door.py +++ b/src/pywink/devices/garage_door.py @@ -6,9 +6,6 @@ class WinkGarageDoor(WinkDevice): Represents a Wink garage door. """ - def __init__(self, device_state_as_json, api_interface): - super(WinkGarageDoor, self).__init__(device_state_as_json, api_interface) - def state(self): return self._last_reading.get('position', 0) diff --git a/src/pywink/devices/light_bulb.py b/src/pywink/devices/light_bulb.py index 3844f4b..19f4dd0 100644 --- a/src/pywink/devices/light_bulb.py +++ b/src/pywink/devices/light_bulb.py @@ -9,9 +9,6 @@ class WinkLightBulb(WinkDevice): Represents a Wink light bulb. """ - def __init__(self, device_state_as_json, api_interface): - super(WinkLightBulb, self).__init__(device_state_as_json, api_interface) - def state(self): return self._last_reading.get('powered', False) diff --git a/src/pywink/devices/lock.py b/src/pywink/devices/lock.py index 0227bdd..ca9372e 100644 --- a/src/pywink/devices/lock.py +++ b/src/pywink/devices/lock.py @@ -6,9 +6,6 @@ class WinkLock(WinkDevice): Represents a Wink lock. """ - def __init__(self, device_state_as_json, api_interface): - super(WinkLock, self).__init__(device_state_as_json, api_interface) - def state(self): return self._last_reading.get('locked', False) diff --git a/src/pywink/devices/powerstrip.py b/src/pywink/devices/powerstrip.py index c52221b..1bee861 100644 --- a/src/pywink/devices/powerstrip.py +++ b/src/pywink/devices/powerstrip.py @@ -8,9 +8,6 @@ class WinkPowerStrip(WinkDevice): Setting the state will set the state of both outlets. """ - def __init__(self, device_state_as_json, api_interface): - super(WinkPowerStrip, self).__init__(device_state_as_json, api_interface) - def state(self): outlets = self.json_state.get('outlets') state = False @@ -35,9 +32,6 @@ class WinkPowerStripOutlet(WinkDevice): Represents a Wink Powerstrip outlet. """ - def __init__(self, device_state_as_json, api_interface): - super(WinkPowerStripOutlet, self).__init__(device_state_as_json, api_interface) - def state(self): return self._last_reading.get('powered', False) diff --git a/src/pywink/devices/scene.py b/src/pywink/devices/scene.py index 0c5e0b6..fb852b3 100644 --- a/src/pywink/devices/scene.py +++ b/src/pywink/devices/scene.py @@ -6,9 +6,6 @@ class WinkScene(WinkDevice): Represents a Wink scene. """ - def __init__(self, device_state_as_json, api_interface): - super(WinkScene, self).__init__(device_state_as_json, api_interface) - def state(self): """ Scenes don't have a state, they can only be triggered. diff --git a/src/pywink/devices/shade.py b/src/pywink/devices/shade.py index b01cc95..f9a0588 100644 --- a/src/pywink/devices/shade.py +++ b/src/pywink/devices/shade.py @@ -6,9 +6,6 @@ class WinkShade(WinkDevice): Represents a Wink Shade. """ - def __init__(self, device_state_as_json, api_interface): - super(WinkShade, self).__init__(device_state_as_json, api_interface) - def state(self): return self._last_reading.get('position', 0) diff --git a/src/pywink/devices/siren.py b/src/pywink/devices/siren.py index 0fd53ea..664ea1a 100644 --- a/src/pywink/devices/siren.py +++ b/src/pywink/devices/siren.py @@ -6,9 +6,6 @@ class WinkSiren(WinkDevice): Represents a Wink Siren. """ - def __init__(self, device_state_as_json, api_interface): - super(WinkSiren, self).__init__(device_state_as_json, api_interface) - def state(self): return self._last_reading.get('powered', False) diff --git a/src/pywink/devices/sprinkler.py b/src/pywink/devices/sprinkler.py index 48e514b..2ccabdd 100644 --- a/src/pywink/devices/sprinkler.py +++ b/src/pywink/devices/sprinkler.py @@ -6,9 +6,6 @@ class WinkSprinkler(WinkBinarySwitch): Represents a Wink Sprinkler. """ - def __init__(self, device_state_as_json, api_interface): - super(WinkSprinkler, self).__init__(device_state_as_json, api_interface) - def state(self): return self._last_reading.get('powered', False) diff --git a/src/pywink/devices/thermostat.py b/src/pywink/devices/thermostat.py index 963a76b..c2bc0a4 100644 --- a/src/pywink/devices/thermostat.py +++ b/src/pywink/devices/thermostat.py @@ -7,9 +7,6 @@ class WinkThermostat(WinkDevice): Represents a Wink thermostat. """ - def __init__(self, device_state_as_json, api_interface): - super(WinkThermostat, self).__init__(device_state_as_json, api_interface) - def state(self): return self.current_hvac_mode() diff --git a/src/pywink/test/api_test.py b/src/pywink/test/api_test.py index 48741f8..a10e05a 100644 --- a/src/pywink/test/api_test.py +++ b/src/pywink/test/api_test.py @@ -1,4 +1,3 @@ -# Standard library imports... from http.server import BaseHTTPRequestHandler, HTTPServer import json import re @@ -471,3 +470,4 @@ def get_device_state(self, device, id_override=None, type_override=None): if _object_id == object_id: return_dict["data"] = device return return_dict + diff --git a/src/setup.py b/src/setup.py index 39951ff..b4cff5e 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='1.2.4', + version='1.2.5', description='Access Wink devices via the Wink API', url='http://github.com/python-wink/python-wink', author='Brad Johnson', From 29e5a75c82b44a6269e75e0388ec0006d9d9dcc1 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Sun, 25 Jun 2017 15:55:29 -0400 Subject: [PATCH 146/178] Set user-agent in headers (#85) --- CHANGELOG.md | 3 +++ src/pywink/api.py | 3 +++ src/setup.py | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a007eb4..9566f11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 1.2.6 +- Calling set_user_agent sets the API_HEADERS user_agent + ## 1.2.5 - Added functions for auth URL and getting token from code diff --git a/src/pywink/api.py b/src/pywink/api.py index 5282c51..2ba7d08 100644 --- a/src/pywink/api.py +++ b/src/pywink/api.py @@ -86,6 +86,9 @@ def set_user_agent(user_agent): USER_AGENT = user_agent + if USER_AGENT: + API_HEADERS["User-Agent"] = USER_AGENT + def legacy_set_wink_credentials(email, password, client_id, client_secret): global CLIENT_ID, CLIENT_SECRET, REFRESH_TOKEN diff --git a/src/setup.py b/src/setup.py index b4cff5e..0732984 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='1.2.5', + version='1.2.6', description='Access Wink devices via the Wink API', url='http://github.com/python-wink/python-wink', author='Brad Johnson', From 8f06a5211ce0d81d1896b9574b3131f35142a39d Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Fri, 14 Jul 2017 16:56:14 -0400 Subject: [PATCH 147/178] Support for switch and light groups (#86) --- CHANGELOG.md | 3 + src/pywink/__init__.py | 3 +- src/pywink/api.py | 35 +- src/pywink/color.py | 108 -- src/pywink/devices/binary_switch_group.py | 34 + src/pywink/devices/factory.py | 11 + src/pywink/devices/light_bulb.py | 52 +- src/pywink/devices/light_group.py | 62 + src/pywink/devices/types.py | 3 +- src/pywink/test/api_test.py | 132 +- .../api_responses/groups/light_group.json | 113 ++ .../groups/non_user_created_group_1.json | 1194 +++++++++++++++++ .../groups/non_user_created_group_2.json | 23 + .../api_responses/groups/switch_group.json | 46 + .../test/devices/api_responses/hue_bulb.json | 164 +++ src/pywink/test/devices/base_test.py | 7 +- src/pywink/test/devices/garage_door_test.py | 7 +- src/pywink/test/devices/hub_test.py | 7 +- src/pywink/test/devices/light_bulb_test.py | 143 +- src/pywink/test/devices/scene_test.py | 39 + src/pywink/test/devices/sensor_test.py | 66 +- src/pywink/test/devices/switch_test.py | 46 + src/setup.py | 2 +- 23 files changed, 2064 insertions(+), 236 deletions(-) delete mode 100644 src/pywink/color.py create mode 100644 src/pywink/devices/binary_switch_group.py create mode 100644 src/pywink/devices/light_group.py create mode 100644 src/pywink/test/devices/api_responses/groups/light_group.json create mode 100644 src/pywink/test/devices/api_responses/groups/non_user_created_group_1.json create mode 100644 src/pywink/test/devices/api_responses/groups/non_user_created_group_2.json create mode 100644 src/pywink/test/devices/api_responses/groups/switch_group.json create mode 100644 src/pywink/test/devices/api_responses/hue_bulb.json create mode 100644 src/pywink/test/devices/scene_test.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 9566f11..8f41328 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 1.3.0 +- Support for switch and light groups + ## 1.2.6 - Calling set_user_agent sets the API_HEADERS user_agent diff --git a/src/pywink/__init__.py b/src/pywink/__init__.py index 8944ec7..4399ff3 100644 --- a/src/pywink/__init__.py +++ b/src/pywink/__init__.py @@ -11,7 +11,8 @@ from pywink.api import get_light_bulbs, get_garage_doors, get_locks, \ get_powerstrips, get_shades, get_sirens, \ get_switches, get_thermostats, get_fans, get_air_conditioners, \ - get_propane_tanks, get_robots, get_scenes + get_propane_tanks, get_robots, get_scenes, get_light_groups, \ + get_binary_switch_groups from pywink.api import get_all_devices, get_eggtrays, get_sensors, \ get_keys, get_piggy_banks, get_smoke_and_co_detectors, \ diff --git a/src/pywink/api.py b/src/pywink/api.py index 2ba7d08..afcf0e6 100644 --- a/src/pywink/api.py +++ b/src/pywink/api.py @@ -1,4 +1,5 @@ import json +import sys import time import urllib.parse @@ -32,10 +33,17 @@ def set_device_state(self, device, state, id_override=None, type_override=None): url_string = "{}/{}s/{}".format(self.BASE_URL, object_type, object_id) - if state is None: + if state is None or object_type == "group": url_string += "/activate" - arequest = requests.post(url_string, - headers=API_HEADERS) + if state is None: + arequest = requests.post(url_string, + headers=API_HEADERS) + else: + print(url_string) + print(state) + arequest = requests.post(url_string, + data=json.dumps(state), + headers=API_HEADERS) else: arequest = requests.put(url_string, data=json.dumps(state), @@ -112,6 +120,7 @@ def legacy_set_wink_credentials(email, password, client_id, client_secret): response_json = response.json() access_token = response_json.get('access_token') REFRESH_TOKEN = response_json.get('refresh_token') + sys.stdout.write(access_token) set_bearer_token(access_token) @@ -286,6 +295,24 @@ def get_scenes(): return get_devices(device_types.SCENE, "scenes") +def get_light_groups(): + light_groups = [] + for group in get_devices(device_types.GROUP, "groups"): + # Only light groups have brightness + if group.json_state.get("reading_aggregation").get("brightness") is not None: + light_groups.append(group) + return light_groups + + +def get_binary_switch_groups(): + switch_groups = [] + for group in get_devices(device_types.GROUP, "groups"): + # Switches don't have brightness + if group.json_state.get("reading_aggregation").get("brightness") is None: + switch_groups.append(group) + return switch_groups + + def get_subscription_key(): response_dict = wink_api_fetch() first_device = response_dict.get('data')[0] @@ -320,7 +347,7 @@ def get_devices(device_type, end_point="wink_devices"): ALL_DEVICES = wink_api_fetch(end_point) LAST_UPDATE = now return get_devices_from_response_dict(ALL_DEVICES, device_type) - elif end_point == "robots" or end_point == "scenes": + elif end_point == "robots" or end_point == "scenes" or end_point == "groups": json_data = wink_api_fetch(end_point) return get_devices_from_response_dict(json_data, device_type) diff --git a/src/pywink/color.py b/src/pywink/color.py deleted file mode 100644 index c6c70dd..0000000 --- a/src/pywink/color.py +++ /dev/null @@ -1,108 +0,0 @@ -import math - - -# pylint: disable=too-many-branches -def color_temperature_to_rgb(color_temperature_kelvin): - """ - Return an RGB color from a color temperature in Kelvin. - This is a rough approximation, based on the formula provided by Tanner Helland - http://www.tannerhelland.com/4435/convert-temperature-rgb-algorithm-code/ - """ - # range check - if color_temperature_kelvin < 1000: - color_temperature_kelvin = 1000 - elif color_temperature_kelvin > 40000: - color_temperature_kelvin = 40000 - - tmp_internal = color_temperature_kelvin / 100.0 - - # red - if tmp_internal <= 66: - red = 255 - else: - tmp_red = 329.698727446 * math.pow(tmp_internal - 60, -0.1332047592) - if tmp_red < 0: - red = 0 - elif tmp_red > 255: - red = 255 - else: - red = tmp_red - - # green - if tmp_internal <= 66: - tmp_green = 99.4708025861 * math.log(tmp_internal) - 161.1195681661 - if tmp_green < 0: - green = 0 - elif tmp_green > 255: - green = 255 - else: - green = tmp_green - else: - tmp_green = 288.1221695283 * math.pow(tmp_internal - 60, -0.0755148492) - if tmp_green < 0: - green = 0 - elif tmp_green > 255: - green = 255 - else: - green = tmp_green - - # blue - if tmp_internal >= 66: - blue = 255 - elif tmp_internal <= 19: - blue = 0 - else: - tmp_blue = 138.5177312231 * math.log(tmp_internal - 10) - 305.0447927307 - if tmp_blue < 0: - blue = 0 - elif tmp_blue > 255: - blue = 255 - else: - blue = tmp_blue - - return (red, green, blue) - - -# taken from -# https://github.com/benknight/hue-python-rgb-converter/blob/master/rgb_cie.py -# Copyright (c) 2014 Benjamin Knight / MIT License. -def color_xy_brightness_to_rgb(vX, vY, brightness): - """ - Convert from XYZ to RGB. - :rtype: tuple of int - """ - brightness /= 255. - if brightness == 0: - return (0, 0, 0) - - Y = brightness - - if vY == 0: - vY += 0.00000000001 - - X = (Y / vY) * vX - Z = (Y / vY) * (1 - vX - vY) - - # Convert to RGB using Wide RGB D65 conversion. - r = X * 1.612 - Y * 0.203 - Z * 0.302 - g = -X * 0.509 + Y * 1.412 + Z * 0.066 - b = X * 0.026 - Y * 0.072 + Z * 0.962 - - # Apply reverse gamma correction. - r, g, b = map( - lambda x: (12.92 * x) if (x <= 0.0031308) else - ((1.0 + 0.055) * pow(x, (1.0 / 2.4)) - 0.055), - [r, g, b] - ) - - # Bring all negative components to zero. - r, g, b = map(lambda x: max(0, x), [r, g, b]) - - # If one component is greater than 1, weight components by that value. - max_component = max(r, g, b) - if max_component > 1: - r, g, b = map(lambda x: x / max_component, [r, g, b]) - - r, g, b = map(lambda x: int(x * 255), [r, g, b]) - - return (r, g, b) diff --git a/src/pywink/devices/binary_switch_group.py b/src/pywink/devices/binary_switch_group.py new file mode 100644 index 0000000..c72139e --- /dev/null +++ b/src/pywink/devices/binary_switch_group.py @@ -0,0 +1,34 @@ +from pywink.devices.base import WinkDevice + + +class WinkBinarySwitchGroup(WinkDevice): + """ + Represents a Wink binary switch group. + """ + + def state(self): + """ + Groups states is determined based on a combination of all devices states + """ + return bool(self.state_true_count() != 0) + + def reading_aggregation(self): + return self.json_state.get("reading_aggregation") + + def state_true_count(self): + return self.reading_aggregation().get("powered").get("true_count") + + def available(self): + count = self.reading_aggregation().get("connection").get("true_count") + if count > 0: + return True + return False + + def set_state(self, state): + """ + :param state: a boolean of true (on) or false ('off') + :return: nothing + """ + desired_state = {"desired_state": {"powered": state}} + response = self.api_interface.set_device_state(self, desired_state) + self._update_state_from_response(response) diff --git a/src/pywink/devices/factory.py b/src/pywink/devices/factory.py index e932445..7e2777e 100644 --- a/src/pywink/devices/factory.py +++ b/src/pywink/devices/factory.py @@ -27,6 +27,8 @@ from pywink.devices.propane_tank import WinkPropaneTank from pywink.devices.robot import WinkRobot from pywink.devices.scene import WinkScene +from pywink.devices.light_group import WinkLightGroup +from pywink.devices.binary_switch_group import WinkBinarySwitchGroup # pylint: disable=too-many-branches, too-many-statements @@ -100,6 +102,15 @@ def build_device(device_state_as_json, api_interface): new_objects.append(WinkRobot(device_state_as_json, api_interface)) elif object_type == device_types.SCENE: new_objects.append(WinkScene(device_state_as_json, api_interface)) + elif object_type == device_types.GROUP: + # This will skip auto created groups that Wink creates. + if device_state_as_json.get("name")[0] not in [".", "@"]: + # This is a group of swithces + if device_state_as_json.get("reading_aggregation").get("brightness") is None: + new_objects.append(WinkBinarySwitchGroup(device_state_as_json, api_interface)) + # This is a group of lights + else: + new_objects.append(WinkLightGroup(device_state_as_json, api_interface)) return new_objects diff --git a/src/pywink/devices/light_bulb.py b/src/pywink/devices/light_bulb.py index 19f4dd0..bac8801 100644 --- a/src/pywink/devices/light_bulb.py +++ b/src/pywink/devices/light_bulb.py @@ -1,7 +1,4 @@ -import colorsys - from pywink.devices.base import WinkDevice -from pywink.color import color_temperature_to_rgb, color_xy_brightness_to_rgb class WinkLightBulb(WinkDevice): @@ -25,10 +22,8 @@ def color_xy(self): """ color_x = self._last_reading.get('color_x') color_y = self._last_reading.get('color_y') - if color_x is not None and color_y is not None: return [float(color_x), float(color_y)] - return None def color_temperature_kelvin(self): @@ -62,7 +57,7 @@ def set_state(self, state, brightness=None, :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 + CIE 1931 x,y color coordinates :param color_hue_saturation: a pair of floats in a list which specify the desired hue and saturation in that order. Brightness can be supplied via the brightness param @@ -86,22 +81,11 @@ def _format_color_data(self, color_hue_saturation, color_kelvin, color_xy): if color_hue_saturation is None and color_kelvin is None and color_xy is None: return None - if self.supports_rgb(): - rgb = _get_color_as_rgb(color_hue_saturation, color_kelvin, color_xy) - if rgb: - return { - "color_model": "rgb", - "color_r": rgb[0], - "color_g": rgb[1], - "color_b": rgb[2] - } - # TODO: Find out if this is the correct format - if color_hue_saturation is None and color_kelvin is not None and self.supports_temperature(): return _format_temperature(color_kelvin) if self.supports_hue_saturation(): - hsv = _get_color_as_hue_saturation_brightness(color_hue_saturation, color_kelvin, color_xy) + hsv = _get_color_as_hue_saturation_brightness(color_hue_saturation) if hsv is not None: return _format_hue_saturation(hsv) @@ -111,20 +95,6 @@ def _format_color_data(self, color_hue_saturation, color_kelvin, color_xy): return {} - def supports_rgb(self): - # TODO: Find out if any bulbs actually support RGB - capabilities = self.json_state.get('capabilities', {}) - cap_fields = capabilities.get('fields', []) - for field in cap_fields: - _field = field.get('field') - if _field == 'color_model': - choices = field.get('choices') - if "hsb" in choices: - return False - if "rgb" in choices: - return True - return False - def supports_hue_saturation(self): capabilities = self.json_state.get('capabilities', {}) cap_fields = capabilities.get('fields', []) @@ -184,23 +154,7 @@ def _format_xy(xy): } -def _get_color_as_rgb(hue_sat, kelvin, xy): - if hue_sat is not None: - h, s, v = colorsys.hsv_to_rgb(hue_sat[0], hue_sat[1], 1) - return h, s, v - if kelvin is not None: - return color_temperature_to_rgb(kelvin) - if xy is not None: - return color_xy_brightness_to_rgb(xy[0], xy[1], 1) - return None - - -def _get_color_as_hue_saturation_brightness(hue_sat, kelvin, xy): +def _get_color_as_hue_saturation_brightness(hue_sat): if hue_sat: color_hs_iter = iter(hue_sat) return (next(color_hs_iter), next(color_hs_iter), 1) - rgb = _get_color_as_rgb(None, kelvin, xy) - if not rgb: - return None - h, s, v = colorsys.rgb_to_hsv(rgb[0], rgb[1], rgb[2]) - return (h, s, v) diff --git a/src/pywink/devices/light_group.py b/src/pywink/devices/light_group.py new file mode 100644 index 0000000..4246501 --- /dev/null +++ b/src/pywink/devices/light_group.py @@ -0,0 +1,62 @@ +from pywink.devices.light_bulb import WinkLightBulb + + +class WinkLightGroup(WinkLightBulb): + """ + Represents a Wink light group. + """ + + def state(self): + """ + Groups states is determined based on a combination of all devices states + """ + return bool(self.state_true_count() != 0) + + def reading_aggregation(self): + return self.json_state.get("reading_aggregation") + + def state_true_count(self): + return self.reading_aggregation().get("powered").get("true_count") + + def available(self): + count = self.reading_aggregation().get("connection").get("true_count") + if count > 0: + return True + return False + + def brightness(self): + return self.reading_aggregation().get("brightness").get("average") + + def color_model(self): + return self.reading_aggregation().get("color_model").get("mode") + + def color_temperature_kelvin(self): + """ + Color temperature, in degrees Kelvin. + Eg: "Daylight" light bulbs are 4600K + :rtype: int + """ + return self.reading_aggregation().get("color_temperature").get("average") + + def color_hue(self): + """ + Color hue from 0 to 1.0 + """ + return self.reading_aggregation().get("hue").get("average") + + def color_saturation(self): + """ + Color saturation from 0 to 1.0 + :return: + """ + return self.reading_aggregation().get("saturation").get("average") + + def supports_hue_saturation(self): + if self.reading_aggregation().get("hue") is not None: + return True + return False + + def supports_temperature(self): + if self.reading_aggregation().get("color_temperature") is not None: + return True + return False diff --git a/src/pywink/devices/types.py b/src/pywink/devices/types.py index af92974..c47d71d 100644 --- a/src/pywink/devices/types.py +++ b/src/pywink/devices/types.py @@ -27,9 +27,10 @@ PROPANE_TANK = 'propane_tank' ROBOT = 'robot' SCENE = 'scene' +GROUP = 'group' ALL_SUPPORTED_DEVICES = [LIGHT_BULB, BINARY_SWITCH, SENSOR_POD, LOCK, EGGTRAY, GARAGE_DOOR, POWERSTRIP, SHADE, SIREN, KEY, PIGGY_BANK, SMOKE_DETECTOR, THERMOSTAT, HUB, FAN, DOOR_BELL, REMOTE, SPRINKLER, BUTTON, GANG, CAMERA, AIR_CONDITIONER, - PROPANE_TANK, ROBOT, SCENE] + PROPANE_TANK, ROBOT, SCENE, GROUP] diff --git a/src/pywink/test/api_test.py b/src/pywink/test/api_test.py index a10e05a..67ee9e7 100644 --- a/src/pywink/test/api_test.py +++ b/src/pywink/test/api_test.py @@ -39,21 +39,31 @@ from pywink.devices.robot import WinkRobot USERS_ME_WINK_DEVICES = {} +GROUPS = {} class ApiTests(unittest.TestCase): def setUp(self): - global USERS_ME_WINK_DEVICES + global USERS_ME_WINK_DEVICES, GROUPS super(ApiTests, self).setUp() all_devices = os.listdir('{}/devices/api_responses/'.format(os.path.dirname(__file__))) device_list = [] for json_file in all_devices: - _json_file = open('{}/devices/api_responses/{}'.format(os.path.dirname(__file__), json_file)) - device_list.append(json.load(_json_file)) - _json_file.close() + if os.path.isfile('{}/devices/api_responses/{}'.format(os.path.dirname(__file__), json_file)): + _json_file = open('{}/devices/api_responses/{}'.format(os.path.dirname(__file__), json_file)) + device_list.append(json.load(_json_file)) + _json_file.close() USERS_ME_WINK_DEVICES["data"] = device_list + all_devices = os.listdir('{}/devices/api_responses/groups'.format(os.path.dirname(__file__))) + device_list = [] + for json_file in all_devices: + if os.path.isfile('{}/devices/api_responses/groups/{}'.format(os.path.dirname(__file__), json_file)): + _json_file = open('{}/devices/api_responses/groups/{}'.format(os.path.dirname(__file__), json_file)) + device_list.append(json.load(_json_file)) + _json_file.close() + GROUPS["data"] = device_list self.port = get_free_port() start_mock_server(self.port) self.api_interface = MockApiInterface() @@ -84,7 +94,7 @@ def test_get_subscription_key(self): def test_get_all_devices_from_api(self): WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) devices = get_all_devices() - self.assertEqual(len(devices), 63) + self.assertEqual(len(devices), 64) lights = get_light_bulbs() for light in lights: self.assertTrue(isinstance(light, WinkLightBulb)) @@ -185,8 +195,12 @@ def test_get_light_bulbs_updated_states_from_api(self): # Set states for device in devices: device.api_interface = self.api_interface + # Test xy color and powered + if device.supports_xy_color(): + old_states[device.object_id()] = device.state() + device.set_state(not device.state(), color_xy=[0.5, 0.5]) # Test HSB and powered - if device.supports_hue_saturation(): + elif device.supports_hue_saturation(): old_states[device.object_id()] = device.state() device.set_state(not device.state(), 0.5, color_hue_saturation=[0.5, 0.5]) # Test temperature and powered @@ -199,8 +213,11 @@ def test_get_light_bulbs_updated_states_from_api(self): device.set_state(not device.state(), 0.5) # Check states for device in devices: + # Test xy color and power + if device.supports_xy_color(): + self.assertEqual([not old_states.get(device.object_id()), [0.5, 0.5]], [device.state(), device.color_xy()]) # Test HSB and powered - if device.supports_hue_saturation(): + elif device.supports_hue_saturation(): self.assertEqual([old_states.get(device.object_id()), 0.5, [0.5, 0.5]], [not device.state(), device.brightness(), [device.color_saturation(), device.color_hue()]]) # Test temperature and powered @@ -210,6 +227,29 @@ def test_get_light_bulbs_updated_states_from_api(self): else: self.assertEqual([old_states.get(device.object_id()), 0.5], [not device.state(), device.brightness()]) + def test_get_switch_group_updated_state_from_api(self): + WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) + devices = get_binary_switch_groups() + for device in devices: + device.api_interface = self.api_interface + # The Mock API only changes the "powered" true_count and false_count + device.set_state(False) + device.update_state() + for device in devices: + self.assertFalse(device.state()) + + def test_get_light_group_updated_state_from_api(self): + WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) + devices = get_light_groups() + for device in devices: + device.api_interface = self.api_interface + # The Mock API only changes the "powered" true_count and false_count + device.set_state(True) + device.update_state() + for device in devices: + self.assertTrue(device.state()) + + def test_get_shade_updated_states_from_api(self): WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) devices = get_shades() @@ -299,10 +339,6 @@ def test_get_air_conditioner_updated_states_from_api(self): self.assertFalse(device.schedule_enabled()) self.assertEqual(0.5, device.current_fan_speed()) - def test_get_camera_updated_states_from_api(self): - WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) - devices = get_cameras() - def test_get_thermostat_updated_states_from_api(self): WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) devices = get_thermostats() @@ -339,9 +375,10 @@ def test_get_camera_updated_states_from_api(self): device.set_mode("away") device.set_privacy(True) device.update_state() - if isinstance(device, WinkCanaryCamera): - self.assertEqual(device.state(), "away") - self.assertTrue(device.private()) + for device in devices: + if isinstance(device, WinkCanaryCamera): + self.assertEqual(device.state(), "away") + self.assertTrue(device.private()) def test_get_fan_updated_states_from_api(self): WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) @@ -376,6 +413,7 @@ class MockServerRequestHandler(BaseHTTPRequestHandler): NOT_FOUND_PATTERN = re.compile(r'/404/') REFRESH_TOKEN_PATTERN = re.compile(r'/oauth2/token') DEVICE_SPECIFIC_PATTERN = re.compile(r'/*/[0-9]*') + GROUPS_PATTERN = re.compile(r'/groups') def do_GET(self): if re.search(self.BAD_STATUS_PATTERN, self.path): @@ -408,6 +446,18 @@ def do_GET(self): response_content = json.dumps(USERS_ME_WINK_DEVICES) self.wfile.write(response_content.encode('utf-8')) return + elif re.search(self.GROUPS_PATTERN, self.path): + # Add response status code. + self.send_response(requests.codes.ok) + + # Add response headers. + self.send_header('Content-Type', 'application/json; charset=utf-8') + self.end_headers() + + # Add response content. + response_content = json.dumps(GROUPS) + self.wfile.write(response_content.encode('utf-8')) + return def get_free_port(): @@ -435,28 +485,42 @@ def set_device_state(self, device, state, id_override=None, type_override=None): device_object_type = device.object_type() object_type = type_override or device_object_type return_dict = {} - for dict_device in USERS_ME_WINK_DEVICES.get('data'): - _object_id = dict_device.get("object_id") - if _object_id == object_id: - if device_object_type == "powerstrip": - set_state = state["outlets"][0]["desired_state"]["powered"] - dict_device["outlets"][0]["last_reading"]["powered"] = set_state - dict_device["outlets"][1]["last_reading"]["powered"] = set_state - return_dict["data"] = dict_device - elif device_object_type == "outlet": - index = device.index() - set_state = state["outlets"][index]["desired_state"]["powered"] - dict_device["outlets"][index]["last_reading"]["powered"] = set_state - return_dict["data"] = dict_device - else: - if "nose_color" in state: - dict_device["nose_color"] = state.get("nose_color") - elif "tare" in state: - dict_device["tare"] = state.get("tare") + if object_type != "group": + for dict_device in USERS_ME_WINK_DEVICES.get('data'): + _object_id = dict_device.get("object_id") + if _object_id == object_id: + if device_object_type == "powerstrip": + set_state = state["outlets"][0]["desired_state"]["powered"] + dict_device["outlets"][0]["last_reading"]["powered"] = set_state + dict_device["outlets"][1]["last_reading"]["powered"] = set_state + return_dict["data"] = dict_device + elif device_object_type == "outlet": + index = device.index() + set_state = state["outlets"][index]["desired_state"]["powered"] + dict_device["outlets"][index]["last_reading"]["powered"] = set_state + return_dict["data"] = dict_device else: - for key, value in state.get('desired_state').items(): - dict_device["last_reading"][key] = value + if "nose_color" in state: + dict_device["nose_color"] = state.get("nose_color") + elif "tare" in state: + dict_device["tare"] = state.get("tare") + else: + for key, value in state.get('desired_state').items(): + dict_device["last_reading"][key] = value + return_dict["data"] = dict_device + else: + for dict_device in GROUPS.get('data'): + _object_id = dict_device.get("object_id") + if _object_id == object_id: + set_state = state["desired_state"]["powered"] + if set_state: + dict_device["reading_aggregation"]["powered"]["true_count"] = 1 + dict_device["reading_aggregation"]["powered"]["false_count"] = 0 + else: + dict_device["reading_aggregation"]["powered"]["true_count"] = 0 + dict_device["reading_aggregation"]["powered"]["false_count"] = 1 return_dict["data"] = dict_device + return return_dict def get_device_state(self, device, id_override=None, type_override=None): diff --git a/src/pywink/test/devices/api_responses/groups/light_group.json b/src/pywink/test/devices/api_responses/groups/light_group.json new file mode 100644 index 0000000..9b536f6 --- /dev/null +++ b/src/pywink/test/devices/api_responses/groups/light_group.json @@ -0,0 +1,113 @@ +{ + "group_id":"7945183", + "name":"Bedroom lights", + "order":0, + "icon_id":"28", + "members":[ + { + "object_type":"light_bulb", + "object_id":"1254763", + "local_id":"2", + "hub_id":"302528", + "blacklisted_readings":[ + + ] + }, + { + "object_type":"light_bulb", + "object_id":"2194253", + "local_id":"29", + "hub_id":"302528", + "blacklisted_readings":[ + + ] + }, + { + "object_type":"light_bulb", + "object_id":"2559830", + "local_id":"36", + "hub_id":"302528", + "blacklisted_readings":[ + + ] + } + ], + "reading_aggregation":{ + "connection":{ + "or":true, + "and":true, + "true_count":3, + "false_count":0, + "updated_at":1499632866.0263805, + "changed_at":null + }, + "firmware_version":{ + "mode":"0.1b03 / 0.4b00", + "mode_count":2, + "other_count":1, + "updated_at":1499632866.0263805, + "changed_at":null + }, + "firmware_date_code":{ + "mode":"20140812", + "mode_count":2, + "other_count":1, + "updated_at":1499632866.0263805, + "changed_at":null + }, + "powered":{ + "or":false, + "and":false, + "true_count":0, + "false_count":3, + "updated_at":1499632866.0263805, + "changed_at":1499632866.0263805 + }, + "brightness":{ + "min":1, + "max":1, + "average":1, + "updated_at":1499632866.0263805, + "changed_at":1499629856.9164886 + }, + "color_model":{ + "mode":"hsb", + "mode_count":1, + "other_count":0, + "updated_at":1499632865.0642416, + "changed_at":null + }, + "hue":{ + "min":0.11, + "max":0.11, + "average":0.11, + "updated_at":1499632865.0642416, + "changed_at":1499627830.7312047 + }, + "saturation":{ + "min":0.13, + "max":0.13, + "average":0.13, + "updated_at":1499632865.0642416, + "changed_at":1499627830.7312047 + }, + "color_temperature":{ + "min":2326, + "max":2326, + "average":2326, + "updated_at":1499632865.0642416, + "changed_at":null + } + }, + "automation_mode":null, + "hidden_at":null, + "object_type":"group", + "object_id":"7945183", + "icon_code":"group-light_group", + "subscription":{ + "pubnub":{ + "subscribe_key":"sub-c-f7bf7f7e-0542-11e3-a5e8-02ee2ddab7fe", + "channel":"a20b15ec8118e2194992be349435394022bdd0df" + } + } +} \ No newline at end of file diff --git a/src/pywink/test/devices/api_responses/groups/non_user_created_group_1.json b/src/pywink/test/devices/api_responses/groups/non_user_created_group_1.json new file mode 100644 index 0000000..00ed38f --- /dev/null +++ b/src/pywink/test/devices/api_responses/groups/non_user_created_group_1.json @@ -0,0 +1,1194 @@ +{ + "group_id":"4438017", + "name":".all", + "order":0, + "icon_id":"28", + "members":[ + { + "object_type":"binary_switch", + "object_id":"292860", + "local_id":"28", + "hub_id":"302528", + "blacklisted_readings":[ + + ] + }, + { + "object_type":"binary_switch", + "object_id":"379278", + "local_id":"2", + "hub_id":"585269", + "blacklisted_readings":[ + + ] + }, + { + "object_type":"binary_switch", + "object_id":"379279", + "local_id":"1", + "hub_id":"585269", + "blacklisted_readings":[ + + ] + }, + { + "object_type":"button", + "object_id":"123004", + "local_id":"4", + "hub_id":"585269", + "blacklisted_readings":[ + + ] + }, + { + "object_type":"button", + "object_id":"123005", + "local_id":"5", + "hub_id":"585269", + "blacklisted_readings":[ + + ] + }, + { + "object_type":"eggtray", + "object_id":"153869", + "local_id":null, + "hub_id":null, + "blacklisted_readings":[ + + ] + }, + { + "object_type":"gang", + "object_id":"63809", + "local_id":"", + "hub_id":"585269", + "blacklisted_readings":[ + + ] + }, + { + "object_type":"hub", + "object_id":"302528", + "local_id":"0", + "hub_id":null, + "blacklisted_readings":[ + + ] + }, + { + "object_type":"hub", + "object_id":"585269", + "local_id":"0", + "hub_id":null, + "blacklisted_readings":[ + + ] + }, + { + "object_type":"key", + "object_id":"792563", + "local_id":null, + "hub_id":null, + "blacklisted_readings":[ + + ] + }, + { + "object_type":"key", + "object_id":"792565", + "local_id":null, + "hub_id":null, + "blacklisted_readings":[ + + ] + }, + { + "object_type":"key", + "object_id":"793889", + "local_id":null, + "hub_id":null, + "blacklisted_readings":[ + + ] + }, + { + "object_type":"key", + "object_id":"907066", + "local_id":null, + "hub_id":null, + "blacklisted_readings":[ + + ] + }, + { + "object_type":"light_bulb", + "object_id":"1254763", + "local_id":"2", + "hub_id":"302528", + "blacklisted_readings":[ + + ] + }, + { + "object_type":"light_bulb", + "object_id":"1302208", + "local_id":"3", + "hub_id":"302528", + "blacklisted_readings":[ + + ] + }, + { + "object_type":"light_bulb", + "object_id":"1302237", + "local_id":"4", + "hub_id":"302528", + "blacklisted_readings":[ + + ] + }, + { + "object_type":"light_bulb", + "object_id":"1303038", + "local_id":"5", + "hub_id":"302528", + "blacklisted_readings":[ + + ] + }, + { + "object_type":"light_bulb", + "object_id":"1432285", + "local_id":"13", + "hub_id":"302528", + "blacklisted_readings":[ + + ] + }, + { + "object_type":"light_bulb", + "object_id":"1713407", + "local_id":"15", + "hub_id":"302528", + "blacklisted_readings":[ + + ] + }, + { + "object_type":"light_bulb", + "object_id":"1713704", + "local_id":"16", + "hub_id":"302528", + "blacklisted_readings":[ + + ] + }, + { + "object_type":"light_bulb", + "object_id":"1928346", + "local_id":"25", + "hub_id":"302528", + "blacklisted_readings":[ + + ] + }, + { + "object_type":"light_bulb", + "object_id":"2194253", + "local_id":"29", + "hub_id":"302528", + "blacklisted_readings":[ + + ] + }, + { + "object_type":"light_bulb", + "object_id":"2351039", + "local_id":"32", + "hub_id":"302528", + "blacklisted_readings":[ + + ] + }, + { + "object_type":"light_bulb", + "object_id":"2366307", + "local_id":"33", + "hub_id":"302528", + "blacklisted_readings":[ + + ] + }, + { + "object_type":"light_bulb", + "object_id":"2559830", + "local_id":"36", + "hub_id":"302528", + "blacklisted_readings":[ + + ] + }, + { + "object_type":"lock", + "object_id":"109898", + "local_id":"27", + "hub_id":"302528", + "blacklisted_readings":[ + + ] + }, + { + "object_type":"piggy_bank", + "object_id":"15215", + "local_id":null, + "hub_id":null, + "blacklisted_readings":[ + + ] + }, + { + "object_type":"powerstrip", + "object_id":"24313", + "local_id":null, + "hub_id":null, + "blacklisted_readings":[ + + ] + }, + { + "object_type":"powerstrip", + "object_id":"37942", + "local_id":null, + "hub_id":null, + "blacklisted_readings":[ + + ] + }, + { + "object_type":"remote", + "object_id":"123012", + "local_id":"34", + "hub_id":"302528", + "blacklisted_readings":[ + + ] + }, + { + "object_type":"sensor_pod", + "object_id":"152619", + "local_id":"6", + "hub_id":"302528", + "blacklisted_readings":[ + + ] + }, + { + "object_type":"sensor_pod", + "object_id":"152622", + "local_id":"7", + "hub_id":"302528", + "blacklisted_readings":[ + + ] + }, + { + "object_type":"sensor_pod", + "object_id":"153374", + "local_id":"9", + "hub_id":"302528", + "blacklisted_readings":[ + + ] + }, + { + "object_type":"sensor_pod", + "object_id":"156012", + "local_id":"", + "hub_id":"", + "blacklisted_readings":[ + + ] + }, + { + "object_type":"sensor_pod", + "object_id":"165132", + "local_id":"10", + "hub_id":"302528", + "blacklisted_readings":[ + + ] + }, + { + "object_type":"sensor_pod", + "object_id":"165148", + "local_id":"11", + "hub_id":"302528", + "blacklisted_readings":[ + + ] + }, + { + "object_type":"sensor_pod", + "object_id":"165152", + "local_id":"12", + "hub_id":"302528", + "blacklisted_readings":[ + + ] + }, + { + "object_type":"sensor_pod", + "object_id":"165155", + "local_id":"14", + "hub_id":"302528", + "blacklisted_readings":[ + + ] + }, + { + "object_type":"sensor_pod", + "object_id":"198184", + "local_id":"17", + "hub_id":"302528", + "blacklisted_readings":[ + + ] + }, + { + "object_type":"sensor_pod", + "object_id":"198186", + "local_id":"18", + "hub_id":"302528", + "blacklisted_readings":[ + + ] + }, + { + "object_type":"sensor_pod", + "object_id":"198198", + "local_id":"19", + "hub_id":"302528", + "blacklisted_readings":[ + + ] + }, + { + "object_type":"sensor_pod", + "object_id":"240456", + "local_id":"23", + "hub_id":"302528", + "blacklisted_readings":[ + + ] + }, + { + "object_type":"sensor_pod", + "object_id":"311983", + "local_id":"3", + "hub_id":"585269", + "blacklisted_readings":[ + + ] + }, + { + "object_type":"siren", + "object_id":"6379", + "local_id":"8", + "hub_id":"302528", + "blacklisted_readings":[ + + ] + }, + { + "object_type":"thermostat", + "object_id":"180437", + "local_id":"31", + "hub_id":"302528", + "blacklisted_readings":[ + + ] + } + ], + "reading_aggregation":{ + "connection":{ + "or":true, + "and":false, + "true_count":40, + "false_count":1, + "updated_at":1499633027.9122903, + "changed_at":1499633002.9700658 + }, + "powered":{ + "or":true, + "and":false, + "true_count":1, + "false_count":16, + "updated_at":1499633027.9122903, + "changed_at":1499633024.4108276 + }, + "powering_mode":{ + "mode":"none", + "mode_count":1, + "other_count":1, + "updated_at":1485025990.6672056, + "changed_at":null + }, + "pressed":{ + "or":false, + "and":false, + "true_count":0, + "false_count":2, + "updated_at":1499632971.9792998, + "changed_at":1499632971.9792998 + }, + "long_pressed":{ + "or":false, + "and":false, + "true_count":0, + "false_count":0, + "updated_at":null, + "changed_at":null + }, + "ifttt_connected":{ + "or":false, + "and":false, + "true_count":0, + "false_count":0, + "updated_at":null, + "changed_at":null + }, + "ifttt_identifier":{ + "mode":null, + "mode_count":0, + "other_count":0, + "updated_at":null, + "changed_at":null + }, + "battery":{ + "min":0.39, + "max":1, + "average":0.8633333333333333, + "updated_at":1499633021.6075304, + "changed_at":1499631684.6124496 + }, + "inventory":{ + "min":7, + "max":7, + "average":7, + "updated_at":1499616274.7563095, + "changed_at":null + }, + "age":{ + "min":1499612418, + "max":1499612418, + "average":1499612418, + "updated_at":1499616274.7563126, + "changed_at":null + }, + "freshness_remaining":{ + "min":2415344, + "max":2415344, + "average":2415344, + "updated_at":1499616274.756315, + "changed_at":null + }, + "next_trigger_at":{ + "min":null, + "max":null, + "average":null, + "updated_at":null, + "changed_at":null + }, + "egg_1_timestamp":{ + "min":0, + "max":0, + "average":0, + "updated_at":1499627122.7979867, + "changed_at":1499470306.7215743 + }, + "egg_2_timestamp":{ + "min":1499612418, + "max":1499612418, + "average":1499612418, + "updated_at":1499627122.7979867, + "changed_at":1499612417.8791678 + }, + "egg_3_timestamp":{ + "min":1499612424, + "max":1499612424, + "average":1499612424, + "updated_at":1499627122.7979867, + "changed_at":1499612422.853475 + }, + "egg_4_timestamp":{ + "min":1499612426, + "max":1499612426, + "average":1499612426, + "updated_at":1499627122.7979867, + "changed_at":1499612424.1773279 + }, + "egg_5_timestamp":{ + "min":1499612457, + "max":1499612457, + "average":1499612457, + "updated_at":1499627122.7979867, + "changed_at":1499612453.4652672 + }, + "egg_6_timestamp":{ + "min":0, + "max":0, + "average":0, + "updated_at":1499627122.7979867, + "changed_at":1499175522.9439816 + }, + "egg_7_timestamp":{ + "min":0, + "max":0, + "average":0, + "updated_at":1499627122.7979867, + "changed_at":1497437619.16573 + }, + "egg_8_timestamp":{ + "min":0, + "max":0, + "average":0, + "updated_at":1499627122.7979867, + "changed_at":1499611579.647386 + }, + "egg_9_timestamp":{ + "min":0, + "max":0, + "average":0, + "updated_at":1499627122.7979867, + "changed_at":1499612338.0946956 + }, + "egg_10_timestamp":{ + "min":1499612428, + "max":1499612428, + "average":1499612428, + "updated_at":1499627122.7979867, + "changed_at":1499612420.392578 + }, + "egg_11_timestamp":{ + "min":1499612461, + "max":1499612461, + "average":1499612461, + "updated_at":1499627122.7979867, + "changed_at":1499612452.1595824 + }, + "egg_12_timestamp":{ + "min":1499612475, + "max":1499612475, + "average":1499612475, + "updated_at":1499627122.7979867, + "changed_at":1499612464.4722133 + }, + "egg_13_timestamp":{ + "min":0, + "max":0, + "average":0, + "updated_at":1499627122.7979867, + "changed_at":1499470276.2347097 + }, + "egg_14_timestamp":{ + "min":0, + "max":0, + "average":0, + "updated_at":1499627122.7979867, + "changed_at":null + }, + "agent_session_id":{ + "mode":"b25a49d3ee30f619a63d69b8b8ebd549", + "mode_count":1, + "other_count":1, + "updated_at":1499579795.1656625, + "changed_at":1499579795.1656625 + }, + "pairing_mode":{ + "mode":"idle", + "mode_count":2, + "other_count":0, + "updated_at":1499579833.3091419, + "changed_at":null + }, + "pairing_device_type_selector":{ + "mode":null, + "mode_count":0, + "other_count":0, + "updated_at":null, + "changed_at":null + }, + "kidde_radio_code":{ + "min":0, + "max":0, + "average":0, + "updated_at":1449442170.975126, + "changed_at":null + }, + "pairing_mode_duration":{ + "min":0, + "max":0, + "average":0, + "updated_at":1499579833.3091419, + "changed_at":null + }, + "ota_image_selection":{ + "mode":"", + "mode_count":1, + "other_count":0, + "updated_at":1499579833.3091419, + "changed_at":1496705202.5546632 + }, + "ota_window_start":{ + "min":0, + "max":0, + "average":0, + "updated_at":1499579833.3091419, + "changed_at":1496705202.5546632 + }, + "ota_window_end":{ + "min":0, + "max":0, + "average":0, + "updated_at":1499579833.3091419, + "changed_at":1496705202.5546632 + }, + "ota_enabled":{ + "or":true, + "and":true, + "true_count":1, + "false_count":0, + "updated_at":1499579833.3091419, + "changed_at":1496705202.5546632 + }, + "led_brightness":{ + "min":null, + "max":null, + "average":null, + "updated_at":1499579833.3091419, + "changed_at":null + }, + "updating_firmware":{ + "or":false, + "and":false, + "true_count":0, + "false_count":2, + "updated_at":1499579794.3214931, + "changed_at":1498181939.6225955 + }, + "firmware_version":{ + "mode":"0.1b03 / 0.4b00", + "mode_count":7, + "other_count":6, + "updated_at":1499632866.0263805, + "changed_at":null + }, + "update_needed":{ + "or":false, + "and":false, + "true_count":0, + "false_count":2, + "updated_at":1499579797.665719, + "changed_at":1498181946.615247 + }, + "mac_address":{ + "mode":"B4:79:A7:1C:40:A6", + "mode_count":1, + "other_count":1, + "updated_at":1499579797.665719, + "changed_at":null + }, + "zigbee_mac_address":{ + "mode":"000D6F000538CAF5", + "mode_count":1, + "other_count":0, + "updated_at":1499579833.3091419, + "changed_at":null + }, + "ip_address":{ + "mode":"192.168.5.3", + "mode_count":1, + "other_count":1, + "updated_at":1499579797.665719, + "changed_at":null + }, + "hub_version":{ + "mode":"00.01", + "mode_count":1, + "other_count":1, + "updated_at":1499579797.665719, + "changed_at":null + }, + "app_version":{ + "mode":"0.1.0", + "mode_count":1, + "other_count":1, + "updated_at":1499579797.665719, + "changed_at":1498181946.615247 + }, + "transfer_mode":{ + "mode":null, + "mode_count":0, + "other_count":0, + "updated_at":1499579795.1656625, + "changed_at":null + }, + "connection_type":{ + "mode":"wifi", + "mode_count":1, + "other_count":0, + "updated_at":1499579833.3091419, + "changed_at":null + }, + "wifi_credentials_present":{ + "or":true, + "and":true, + "true_count":1, + "false_count":0, + "updated_at":1499579833.3091419, + "changed_at":null + }, + "bundle_id":{ + "mode":"None", + "mode_count":1, + "other_count":0, + "updated_at":1499579833.3091419, + "changed_at":null + }, + "remote_pairable":{ + "or":false, + "and":false, + "true_count":0, + "false_count":0, + "updated_at":null, + "changed_at":null + }, + "local_control_public_key_hash":{ + "mode":"E3:03:48:35:D4:92:46:EE:9E:DD:AC:3B:59:72:78:C7:22:8C:4E:C5:52:85:A3:D2:19:49:40:90:79:70:D5:0A", + "mode_count":1, + "other_count":1, + "updated_at":1499579832.713211, + "changed_at":null + }, + "local_control_id":{ + "mode":"cedce8fc-b692-4c8d-9e9c-cd1fc13f1db5", + "mode_count":1, + "other_count":1, + "updated_at":1499579832.713211, + "changed_at":null + }, + "health":{ + "min":null, + "max":null, + "average":null, + "updated_at":null, + "changed_at":null + }, + "slot_id":{ + "min":2, + "max":5, + "average":3.5, + "updated_at":1483357603.4477262, + "changed_at":null + }, + "activity_detected":{ + "or":false, + "and":false, + "true_count":0, + "false_count":4, + "updated_at":1499631375.987038, + "changed_at":1499631375.987038 + }, + "firmware_date_code":{ + "mode":"20140812", + "mode_count":7, + "other_count":4, + "updated_at":1499632866.0263805, + "changed_at":null + }, + "brightness":{ + "min":0, + "max":1, + "average":0.9053846153846153, + "updated_at":1499632866.0263805, + "changed_at":1499631480.5446444 + }, + "color_model":{ + "mode":"color_temperature", + "mode_count":2, + "other_count":1, + "updated_at":1499632865.0642416, + "changed_at":null + }, + "color_temperature":{ + "min":2326, + "max":5000, + "average":3343, + "updated_at":1499632865.0642416, + "changed_at":null + }, + "hue":{ + "min":0.11, + "max":0.11, + "average":0.11, + "updated_at":1499632865.0642416, + "changed_at":1499627830.7312047 + }, + "saturation":{ + "min":0.13, + "max":0.13, + "average":0.13, + "updated_at":1499632865.0642416, + "changed_at":1499627830.7312047 + }, + "locked":{ + "or":true, + "and":true, + "true_count":1, + "false_count":0, + "updated_at":1499631684.6124496, + "changed_at":1499631673.8019314 + }, + "alarm_activated":{ + "or":false, + "and":false, + "true_count":0, + "false_count":1, + "updated_at":1495286804.498106, + "changed_at":null + }, + "beeper_enabled":{ + "or":false, + "and":false, + "true_count":0, + "false_count":1, + "updated_at":1498604355.8187737, + "changed_at":null + }, + "vacation_mode_enabled":{ + "or":false, + "and":false, + "true_count":0, + "false_count":1, + "updated_at":1498604355.8187737, + "changed_at":null + }, + "auto_lock_enabled":{ + "or":false, + "and":false, + "true_count":0, + "false_count":1, + "updated_at":1498604355.8187737, + "changed_at":null + }, + "key_code_length":{ + "min":4, + "max":4, + "average":4, + "updated_at":1498604355.8187737, + "changed_at":null + }, + "alarm_mode":{ + "mode":"forced_entry", + "mode_count":1, + "other_count":0, + "updated_at":1498604355.8187737, + "changed_at":null + }, + "alarm_sensitivity":{ + "min":0.2, + "max":0.2, + "average":0.2, + "updated_at":1498604355.8187737, + "changed_at":null + }, + "alarm_enabled":{ + "or":true, + "and":true, + "true_count":1, + "false_count":0, + "updated_at":1498604355.8187737, + "changed_at":null + }, + "last_error":{ + "mode":"bolt_unknown_state", + "mode_count":1, + "other_count":0, + "updated_at":1498925557.468597, + "changed_at":1498925557.468597 + }, + "amount":{ + "min":25, + "max":25, + "average":25, + "updated_at":1497730523.1306367, + "changed_at":1497730523.1306367 + }, + "balance":{ + "min":1000, + "max":1000, + "average":1000, + "updated_at":1497730523.1306367, + "changed_at":1497730523.1306367 + }, + "orientation":{ + "min":0, + "max":0, + "average":0, + "updated_at":1467553983.0383396, + "changed_at":null + }, + "vibration":{ + "or":true, + "and":false, + "true_count":1, + "false_count":1, + "updated_at":1499632516.6070192, + "changed_at":1499630935.1837053 + }, + "group_id":{ + "mode":"6866747", + "mode_count":1, + "other_count":0, + "updated_at":1484178963.2148762, + "changed_at":null + }, + "button_on_pressed":{ + "or":false, + "and":false, + "true_count":0, + "false_count":1, + "updated_at":1498096855.4955213, + "changed_at":1498096855.4955213 + }, + "button_off_pressed":{ + "or":false, + "and":false, + "true_count":0, + "false_count":1, + "updated_at":1499626713.0544481, + "changed_at":1499626708.112714 + }, + "button_up_pressed":{ + "or":false, + "and":false, + "true_count":0, + "false_count":1, + "updated_at":1488137923.2546654, + "changed_at":null + }, + "button_down_pressed":{ + "or":false, + "and":false, + "true_count":0, + "false_count":0, + "updated_at":null, + "changed_at":null + }, + "ready":{ + "or":true, + "and":true, + "true_count":1, + "false_count":0, + "updated_at":1499627769.2357333, + "changed_at":null + }, + "assignment_mode":{ + "mode":"local_group", + "mode_count":1, + "other_count":0, + "updated_at":1489846831.8289862, + "changed_at":null + }, + "motion":{ + "or":true, + "and":false, + "true_count":1, + "false_count":2, + "updated_at":1499633021.6075304, + "changed_at":1499632960.3474858 + }, + "tamper_detected":{ + "or":false, + "and":false, + "true_count":0, + "false_count":6, + "updated_at":1469250213.2873414, + "changed_at":null + }, + "temperature":{ + "min":20.555555555555557, + "max":25, + "average":23.09601851851852, + "updated_at":1499633021.6075304, + "changed_at":1499633021.6075304 + }, + "motion_true":{ + "updated_at":1499632960.3474858, + "changed_at":1499632960.3474858 + }, + "tamper_detected_true":{ + "updated_at":null, + "changed_at":null + }, + "opened":{ + "or":true, + "and":false, + "true_count":2, + "false_count":5, + "updated_at":1499631783.8473804, + "changed_at":1499631783.8473804 + }, + "external_power":{ + "or":true, + "and":true, + "true_count":1, + "false_count":0, + "updated_at":1499632516.6070192, + "changed_at":null + }, + "humidity":{ + "min":0.37, + "max":0.88, + "average":0.625, + "updated_at":1499633002.8560781, + "changed_at":1499632611.5416102 + }, + "loudness":{ + "or":true, + "and":true, + "true_count":1, + "false_count":0, + "updated_at":1499632516.6070192, + "changed_at":1499545234.932832 + }, + "brightness_true":{ + "updated_at":1499626465.834425, + "changed_at":1499626465.834425 + }, + "loudness_true":{ + "updated_at":1499545234.932832, + "changed_at":1499545234.932832 + }, + "vibration_true":{ + "updated_at":1499626675.7427526, + "changed_at":1499626675.7427526 + }, + "liquid_detected":{ + "or":false, + "and":false, + "true_count":0, + "false_count":1, + "updated_at":1499579834.9109979, + "changed_at":1498769900.9117572 + }, + "liquid_detected_true":{ + "updated_at":1498769899.1052618, + "changed_at":1498769899.1052618 + }, + "presence":{ + "or":false, + "and":false, + "true_count":0, + "false_count":1, + "updated_at":1499633002.8560781, + "changed_at":1499339473.3062227 + }, + "proximity":{ + "min":2630, + "max":2630, + "average":2630, + "updated_at":1499633002.8560781, + "changed_at":1499633002.8560781 + }, + "mode":{ + "mode":"siren_and_strobe", + "mode_count":1, + "other_count":1, + "updated_at":1499631657.6924953, + "changed_at":null + }, + "auto_shutoff":{ + "mode":60, + "mode_count":1, + "other_count":0, + "updated_at":1499579872.5166438, + "changed_at":null + }, + "max_set_point":{ + "min":22.22222222222222, + "max":22.22222222222222, + "average":22.22222222222222, + "updated_at":1499631657.6924953, + "changed_at":null + }, + "min_set_point":{ + "min":17.77777777777778, + "max":17.77777777777778, + "average":17.77777777777778, + "updated_at":1499631657.6924953, + "changed_at":null + }, + "external_temperature":{ + "min":null, + "max":null, + "average":null, + "updated_at":null, + "changed_at":null + }, + "min_min_set_point":{ + "min":null, + "max":null, + "average":null, + "updated_at":null, + "changed_at":null + }, + "max_min_set_point":{ + "min":null, + "max":null, + "average":null, + "updated_at":null, + "changed_at":null + }, + "min_max_set_point":{ + "min":null, + "max":null, + "average":null, + "updated_at":null, + "changed_at":null + }, + "max_max_set_point":{ + "min":null, + "max":null, + "average":null, + "updated_at":null, + "changed_at":null + }, + "deadband":{ + "min":null, + "max":null, + "average":null, + "updated_at":null, + "changed_at":null + }, + "fan_mode":{ + "mode":"on", + "mode_count":1, + "other_count":0, + "updated_at":1499631657.6924953, + "changed_at":1497729879.9202905 + } + }, + "automation_mode":"system_category", + "hidden_at":null, + "object_type":"group", + "object_id":"4438017", + "icon_code":"group-light_group", + "subscription":{ + "pubnub":{ + "subscribe_key":"sub-c-f7bf7f7e-0542-11e3-a5e8-02ee2ddab7fe", + "channel":"0e37853633ae2ab34cea432027b417e1ef7cb6c6" + } + } +} \ No newline at end of file diff --git a/src/pywink/test/devices/api_responses/groups/non_user_created_group_2.json b/src/pywink/test/devices/api_responses/groups/non_user_created_group_2.json new file mode 100644 index 0000000..652e180 --- /dev/null +++ b/src/pywink/test/devices/api_responses/groups/non_user_created_group_2.json @@ -0,0 +1,23 @@ +{ + "group_id":"4438022", + "name":"@window_sensors", + "order":0, + "icon_id":"28", + "members":[ + + ], + "reading_aggregation":{ + + }, + "automation_mode":"user_category", + "hidden_at":null, + "object_type":"group", + "object_id":"4438022", + "icon_code":"group-light_group", + "subscription":{ + "pubnub":{ + "subscribe_key":"sub-c-f7bf7f7e-0542-11e3-a5e8-02ee2ddab7fe", + "channel":"5db3fce7c1bea34481c86e5acad218be4dd81390" + } + } +} \ No newline at end of file diff --git a/src/pywink/test/devices/api_responses/groups/switch_group.json b/src/pywink/test/devices/api_responses/groups/switch_group.json new file mode 100644 index 0000000..e841b6f --- /dev/null +++ b/src/pywink/test/devices/api_responses/groups/switch_group.json @@ -0,0 +1,46 @@ +{ + "group_id":"7945692", + "name":"Just switches", + "order":0, + "icon_id":"28", + "members":[ + { + "object_type":"binary_switch", + "object_id":"292860", + "local_id":"28", + "hub_id":"302528", + "blacklisted_readings":[ + + ] + } + ], + "reading_aggregation":{ + "connection":{ + "or":true, + "and":true, + "true_count":1, + "false_count":0, + "updated_at":1499633027.9122903, + "changed_at":null + }, + "powered":{ + "or":false, + "and":false, + "true_count":0, + "false_count":1, + "updated_at":1499633027.9122903, + "changed_at":1499633024.4108276 + } + }, + "automation_mode":null, + "hidden_at":null, + "object_type":"group", + "object_id":"7945692", + "icon_code":"group-light_group", + "subscription":{ + "pubnub":{ + "subscribe_key":"sub-c-f7bf7f7e-0542-11e3-a5e8-02ee2ddab7fe", + "channel":"j8a90sfdjasdkfkl;asdfj;akljsdf" + } + } +} \ No newline at end of file diff --git a/src/pywink/test/devices/api_responses/hue_bulb.json b/src/pywink/test/devices/api_responses/hue_bulb.json new file mode 100644 index 0000000..299977b --- /dev/null +++ b/src/pywink/test/devices/api_responses/hue_bulb.json @@ -0,0 +1,164 @@ +{ + "object_type": "light_bulb", + "object_id": "2533887", + "uuid": "2df746e3-dd5a-42aa-9622-a37fd0626231", + "icon_id": "66", + "icon_code": "light_bulb-philips_hue", + "desired_state": { + "powered": true, + "brightness": 1.0, + "color_model": "xy", + "color": null, + "color_x": 0.4571, + "color_y": 0.4097, + "hue": 0.228214, + "saturation": 0.551181, + "color_temperature": 2732 + }, + "last_reading": { + "connection": true, + "connection_updated_at": 1500040929.3020744, + "powered": true, + "powered_updated_at": 1500040929.3020744, + "brightness": 1.0, + "brightness_updated_at": 1500040929.3020744, + "color_model": "xy", + "color_model_updated_at": 1500040929.3020744, + "color": null, + "color_updated_at": null, + "color_x": 0.4571, + "color_x_updated_at": 1500040929.3020744, + "color_y": 0.4097, + "color_y_updated_at": 1500040929.3020744, + "hue": 0.228214, + "hue_updated_at": 1500040929.3020744, + "saturation": 0.551181, + "saturation_updated_at": 1500040929.3020744, + "color_temperature": 2732, + "color_temperature_updated_at": 1500040929.3020744, + "desired_powered_updated_at": 1500004039.8124363, + "desired_brightness_updated_at": 1500004039.8124363, + "desired_color_model_updated_at": 1500004135.0675166, + "desired_color_updated_at": 1500004135.0675166, + "desired_color_x_updated_at": 1500004135.0675166, + "desired_color_y_updated_at": 1500004135.0675166, + "desired_hue_updated_at": 1500004135.0675166, + "desired_saturation_updated_at": 1500004135.0675166, + "desired_color_temperature_updated_at": 1500004135.0675166, + "connection_changed_at": 1500040929.3020744, + "powered_changed_at": 1500040929.3020744, + "brightness_changed_at": 1500040929.3020744, + "color_model_changed_at": 1499994174.3735387, + "color_x_changed_at": 1499994174.3735387, + "color_y_changed_at": 1499994174.3735387, + "hue_changed_at": 1499283107.906975, + "saturation_changed_at": 1499283107.906975, + "color_temperature_changed_at": 1499283107.906975, + "desired_powered_changed_at": 1500004039.8124363, + "desired_brightness_changed_at": 1500004039.8124363 + }, + "subscription": { + "pubnub": { + "subscribe_key": "sub-c-f7bf7f7e-0542-11e3-a5e8-02ee2ddab7fe", + "channel": "(deleted)" + } + }, + "light_bulb_id": "2533887", + "name": "island 1", + "locale": "en_us", + "units": {}, + "created_at": 1489158015, + "hidden_at": null, + "capabilities": { + "fields": [ + { + "type": "boolean", + "field": "connection", + "mutability": "read-only" + }, + { + "type": "boolean", + "field": "powered", + "mutability": "read-write" + }, + { + "type": "percentage", + "field": "brightness", + "mutability": "read-write" + }, + { + "type": "string", + "field": "color_model", + "choices": [ + "rgb", + "xy", + "hsb", + "color_temperature" + ], + "mutability": "read-write", + "legacy_type": "colorModel" + }, + { + "type": "hexColorOrNil", + "field": "color", + "mutability": "read-write" + }, + { + "type": "percentage", + "field": "color_x", + "precision": 4, + "mutability": "read-write", + "legacy_type": "percentage4" + }, + { + "type": "percentage", + "field": "color_y", + "precision": 4, + "mutability": "read-write", + "legacy_type": "percentage4" + }, + { + "type": "percentage", + "field": "hue", + "precision": 6, + "mutability": "read-write", + "legacy_type": "percentage6" + }, + { + "type": "percentage", + "field": "saturation", + "precision": 6, + "mutability": "read-write", + "legacy_type": "percentage6" + }, + { + "type": "integer", + "field": "color_temperature", + "range": [ + 2000, + 6500 + ], + "mutability": "read-write" + } + ] + }, + "triggers": [], + "manufacturer_device_model": "philips_philips_hue_extended_color_light", + "manufacturer_device_id": "00:17:88:26:43:2e_9", + "device_manufacturer": "philips", + "model_name": "Hue Extended Color Light", + "upc_id": "477", + "upc_code": "philips_hue_extended_color_light", + "primary_upc_code": "philips_hue_extended_color_light", + "gang_id": null, + "hub_id": "614849", + "local_id": "9", + "radio_type": null, + "linked_service_id": "366609", + "lat_lng": [ + null, + null + ], + "location": "", + "order": 0 +} diff --git a/src/pywink/test/devices/base_test.py b/src/pywink/test/devices/base_test.py index c970b88..bd55d53 100644 --- a/src/pywink/test/devices/base_test.py +++ b/src/pywink/test/devices/base_test.py @@ -37,9 +37,10 @@ def setUp(self): self.response_dict = {} device_list = [] for json_file in all_devices: - _json_file = open('{}/api_responses/{}'.format(os.path.dirname(__file__), json_file)) - device_list.append(json.load(_json_file)) - _json_file.close() + if (os.path.isfile('{}/api_responses/{}'.format(os.path.dirname(__file__), json_file))): + _json_file = open('{}/api_responses/{}'.format(os.path.dirname(__file__), json_file)) + device_list.append(json.load(_json_file)) + _json_file.close() self.response_dict["data"] = device_list def test_all_devices_are_available(self): diff --git a/src/pywink/test/devices/garage_door_test.py b/src/pywink/test/devices/garage_door_test.py index 57d27bc..b8ea978 100644 --- a/src/pywink/test/devices/garage_door_test.py +++ b/src/pywink/test/devices/garage_door_test.py @@ -18,9 +18,10 @@ def setUp(self): self.response_dict = {} device_list = [] for json_file in all_devices: - _json_file = open('{}/api_responses/{}'.format(os.path.dirname(__file__), json_file)) - device_list.append(json.load(_json_file)) - _json_file.close() + if os.path.isfile('{}/api_responses/{}'.format(os.path.dirname(__file__), json_file)): + _json_file = open('{}/api_responses/{}'.format(os.path.dirname(__file__), json_file)) + device_list.append(json.load(_json_file)) + _json_file.close() self.response_dict["data"] = device_list def test_tamper_detected_is_false(self): diff --git a/src/pywink/test/devices/hub_test.py b/src/pywink/test/devices/hub_test.py index 80acb20..d0d7aa1 100644 --- a/src/pywink/test/devices/hub_test.py +++ b/src/pywink/test/devices/hub_test.py @@ -18,9 +18,10 @@ def setUp(self): self.response_dict = {} device_list = [] for json_file in all_devices: - _json_file = open('{}/api_responses/{}'.format(os.path.dirname(__file__), json_file)) - device_list.append(json.load(_json_file)) - _json_file.close() + if os.path.isfile('{}/api_responses/{}'.format(os.path.dirname(__file__), json_file)): + _json_file = open('{}/api_responses/{}'.format(os.path.dirname(__file__), json_file)) + device_list.append(json.load(_json_file)) + _json_file.close() self.response_dict["data"] = device_list def test_unit_should_be_none(self): diff --git a/src/pywink/test/devices/light_bulb_test.py b/src/pywink/test/devices/light_bulb_test.py index 20172ea..34a17da 100644 --- a/src/pywink/test/devices/light_bulb_test.py +++ b/src/pywink/test/devices/light_bulb_test.py @@ -7,6 +7,9 @@ from pywink.api import get_devices_from_response_dict, WinkApiInterface from pywink.devices import types as device_types from pywink.devices.light_bulb import WinkLightBulb +from pywink.devices.light_group import WinkLightGroup + +JSON_DATA = {} class LightBulbTests(unittest.TestCase): @@ -25,7 +28,7 @@ def test_bulb_brightness(self): bulb = get_devices_from_response_dict(response_dict, device_types.LIGHT_BULB)[0] self.assertEqual(bulb.brightness(), 0.02) - def test_bulb_color(self): + def test_bulb_hsb_color(self): device_list = [] response_dict = {} _json_file = open('{}/api_responses/lightify_rgbw_bulb.json'.format(os.path.dirname(__file__))) @@ -33,10 +36,140 @@ def test_bulb_color(self): _json_file.close() response_dict["data"] = device_list bulb = get_devices_from_response_dict(response_dict, device_types.LIGHT_BULB)[0] - self.assertFalse(bulb.supports_rgb()) - self.assertFalse(bulb.supports_xy_color()) self.assertTrue(bulb.supports_hue_saturation()) - self.assertTrue(bulb.supports_temperature()) - self.assertEqual(bulb.color_temperature_kelvin(), 2755) + self.assertEqual(bulb.color_model(), "hsb") self.assertEqual(bulb.color_hue(), 0.0) self.assertEqual(bulb.color_saturation(), 1.0) + + def test_bulb_xy_color(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/hue_bulb.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + bulb = get_devices_from_response_dict(response_dict, device_types.LIGHT_BULB)[0] + self.assertEqual(bulb.color_model(), "xy") + self.assertTrue(bulb.supports_xy_color()) + self.assertEqual(bulb.color_xy(), [0.4571, 0.4097]) + + def test_bulb_color_temperature(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/lightify_rgbw_bulb.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + bulb = get_devices_from_response_dict(response_dict, device_types.LIGHT_BULB)[0] + self.assertTrue(bulb.supports_temperature()) + self.assertEqual(bulb.color_temperature_kelvin(), 2755) + + def test_light_groups_are_created_correctly(self): + all_devices = os.listdir('{}/api_responses/groups/'.format(os.path.dirname(__file__))) + device_list = [] + for json_file in all_devices: + if os.path.isfile('{}/api_responses/groups/{}'.format(os.path.dirname(__file__), json_file)): + _json_file = open('{}/api_responses/groups/{}'.format(os.path.dirname(__file__), json_file)) + device_list.append(json.load(_json_file)) + _json_file.close() + JSON_DATA["data"] = device_list + all_groups = get_devices_from_response_dict(JSON_DATA, device_types.GROUP) + self.assertEqual(2, len(all_groups)) + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/groups/light_group.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + groups = get_devices_from_response_dict(response_dict, device_types.GROUP) + self.assertEqual(1, len(groups)) + self.assertTrue(isinstance(groups[0], WinkLightGroup)) + + def test_light_group_state_is_correct(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/groups/light_group.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + groups = get_devices_from_response_dict(response_dict, device_types.GROUP) + light_group = groups[0] + self.assertFalse(light_group.state()) + + def test_light_group_is_available(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/groups/light_group.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + groups = get_devices_from_response_dict(response_dict, device_types.GROUP) + light_group = groups[0] + self.assertTrue(light_group.available()) + + def test_light_group_brightness(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/groups/light_group.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + groups = get_devices_from_response_dict(response_dict, device_types.GROUP) + light_group = groups[0] + self.assertEqual(light_group.brightness(), 1) + + def test_light_group_color_model(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/groups/light_group.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + groups = get_devices_from_response_dict(response_dict, device_types.GROUP) + light_group = groups[0] + self.assertEqual(light_group.color_model(), "hsb") + + def test_light_group_supports_hsb_and_temperature(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/groups/light_group.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + groups = get_devices_from_response_dict(response_dict, device_types.GROUP) + light_group = groups[0] + self.assertTrue(light_group.supports_temperature()) + self.assertTrue(light_group.supports_hue_saturation()) + + def test_light_group_saturation(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/groups/light_group.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + groups = get_devices_from_response_dict(response_dict, device_types.GROUP) + light_group = groups[0] + self.assertEqual(light_group.color_saturation(), 0.13) + + def test_light_group_hue(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/groups/light_group.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + groups = get_devices_from_response_dict(response_dict, device_types.GROUP) + light_group = groups[0] + self.assertEqual(light_group.color_hue(), 0.11) + + def test_light_group_color_temperature_kelvin(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/groups/light_group.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + groups = get_devices_from_response_dict(response_dict, device_types.GROUP) + light_group = groups[0] + self.assertEqual(light_group.color_temperature_kelvin(), 2326) diff --git a/src/pywink/test/devices/scene_test.py b/src/pywink/test/devices/scene_test.py new file mode 100644 index 0000000..69ee498 --- /dev/null +++ b/src/pywink/test/devices/scene_test.py @@ -0,0 +1,39 @@ +import json +import os +import unittest + +import mock + +from pywink.api import get_devices_from_response_dict, WinkApiInterface +from pywink.devices import types as device_types +from pywink.devices.scene import WinkScene + + +class SceneTests(unittest.TestCase): + + def test_state_should_be_false(self): + with open('{}/api_responses/scene.json'.format(os.path.dirname(__file__))) as scene_file: + response_dict = json.load(scene_file) + response_dict = {"data": [response_dict]} + devices = get_devices_from_response_dict(response_dict, device_types.SCENE) + + scene = devices[0] + self.assertFalse(scene.state()) + + def test_update_state_should_be_true(self): + with open('{}/api_responses/scene.json'.format(os.path.dirname(__file__))) as scene_file: + response_dict = json.load(scene_file) + response_dict = {"data": [response_dict]} + devices = get_devices_from_response_dict(response_dict, device_types.SCENE) + + scene = devices[0] + self.assertTrue(scene.update_state()) + + def test_available_should_be_true(self): + with open('{}/api_responses/scene.json'.format(os.path.dirname(__file__))) as scene_file: + response_dict = json.load(scene_file) + response_dict = {"data": [response_dict]} + devices = get_devices_from_response_dict(response_dict, device_types.SCENE) + + scene = devices[0] + self.assertTrue(scene.available()) diff --git a/src/pywink/test/devices/sensor_test.py b/src/pywink/test/devices/sensor_test.py index 0016db6..96e91f8 100644 --- a/src/pywink/test/devices/sensor_test.py +++ b/src/pywink/test/devices/sensor_test.py @@ -20,9 +20,10 @@ def setUp(self): self.response_dict = {} device_list = [] for json_file in all_devices: - _json_file = open('{}/api_responses/{}'.format(os.path.dirname(__file__), json_file)) - device_list.append(json.load(_json_file)) - _json_file.close() + if os.path.isfile('{}/api_responses/{}'.format(os.path.dirname(__file__), json_file)): + _json_file = open('{}/api_responses/{}'.format(os.path.dirname(__file__), json_file)) + device_list.append(json.load(_json_file)) + _json_file.close() self.response_dict["data"] = device_list def test_capability_should_not_be_none(self): @@ -53,9 +54,10 @@ def setUp(self): self.response_dict = {} device_list = [] for json_file in all_devices: - _json_file = open('{}/api_responses/{}'.format(os.path.dirname(__file__), json_file)) - device_list.append(json.load(_json_file)) - _json_file.close() + if os.path.isfile('{}/api_responses/{}'.format(os.path.dirname(__file__), json_file)): + _json_file = open('{}/api_responses/{}'.format(os.path.dirname(__file__), json_file)) + device_list.append(json.load(_json_file)) + _json_file.close() self.response_dict["data"] = device_list def test_state_should_be_2(self): @@ -63,6 +65,16 @@ def test_state_should_be_2(self): for device in devices: self.assertEqual(device.state(), 2) + def test_capability_is_none(self): + devices = get_devices_from_response_dict(self.response_dict, device_types.EGGTRAY) + for device in devices: + self.assertEqual(device.capability(), None) + + def test_unit_is_eggs(self): + devices = get_devices_from_response_dict(self.response_dict, device_types.EGGTRAY) + for device in devices: + self.assertEqual(device.unit(), "eggs") + class KeyTests(unittest.TestCase): def setUp(self): @@ -72,9 +84,10 @@ def setUp(self): self.response_dict = {} device_list = [] for json_file in all_devices: - _json_file = open('{}/api_responses/{}'.format(os.path.dirname(__file__), json_file)) - device_list.append(json.load(_json_file)) - _json_file.close() + if os.path.isfile('{}/api_responses/{}'.format(os.path.dirname(__file__), json_file)): + _json_file = open('{}/api_responses/{}'.format(os.path.dirname(__file__), json_file)) + device_list.append(json.load(_json_file)) + _json_file.close() self.response_dict["data"] = device_list def test_state_should_be_false(self): @@ -112,9 +125,10 @@ def setUp(self): self.response_dict = {} device_list = [] for json_file in all_devices: - _json_file = open('{}/api_responses/{}'.format(os.path.dirname(__file__), json_file)) - device_list.append(json.load(_json_file)) - _json_file.close() + if os.path.isfile('{}/api_responses/{}'.format(os.path.dirname(__file__), json_file)): + _json_file = open('{}/api_responses/{}'.format(os.path.dirname(__file__), json_file)) + device_list.append(json.load(_json_file)) + _json_file.close() self.response_dict["data"] = device_list def test_unit_is_usd(self): @@ -151,9 +165,10 @@ def setUp(self): self.response_dict = {} device_list = [] for json_file in all_devices: - _json_file = open('{}/api_responses/{}'.format(os.path.dirname(__file__), json_file)) - device_list.append(json.load(_json_file)) - _json_file.close() + if os.path.isfile('{}/api_responses/{}'.format(os.path.dirname(__file__), json_file)): + _json_file = open('{}/api_responses/{}'.format(os.path.dirname(__file__), json_file)) + device_list.append(json.load(_json_file)) + _json_file.close() self.response_dict["data"] = device_list def test_unit_is_none(self): @@ -170,9 +185,10 @@ def setUp(self): self.response_dict = {} device_list = [] for json_file in all_devices: - _json_file = open('{}/api_responses/{}'.format(os.path.dirname(__file__), json_file)) - device_list.append(json.load(_json_file)) - _json_file.close() + if os.path.isfile('{}/api_responses/{}'.format(os.path.dirname(__file__), json_file)): + _json_file = open('{}/api_responses/{}'.format(os.path.dirname(__file__), json_file)) + device_list.append(json.load(_json_file)) + _json_file.close() self.response_dict["data"] = device_list def test_test_activated_is_false(self): @@ -206,9 +222,10 @@ def setUp(self): self.response_dict = {} device_list = [] for json_file in all_devices: - _json_file = open('{}/api_responses/{}'.format(os.path.dirname(__file__), json_file)) - device_list.append(json.load(_json_file)) - _json_file.close() + if os.path.isfile('{}/api_responses/{}'.format(os.path.dirname(__file__), json_file)): + _json_file = open('{}/api_responses/{}'.format(os.path.dirname(__file__), json_file)) + device_list.append(json.load(_json_file)) + _json_file.close() self.response_dict["data"] = device_list def test_buttons_press_is_false(self): @@ -235,9 +252,10 @@ def setUp(self): self.response_dict = {} device_list = [] for json_file in all_devices: - _json_file = open('{}/api_responses/{}'.format(os.path.dirname(__file__), json_file)) - device_list.append(json.load(_json_file)) - _json_file.close() + if os.path.isfile('{}/api_responses/{}'.format(os.path.dirname(__file__), json_file)): + _json_file = open('{}/api_responses/{}'.format(os.path.dirname(__file__), json_file)) + device_list.append(json.load(_json_file)) + _json_file.close() self.response_dict["data"] = device_list def test_unit_and_capability(self): diff --git a/src/pywink/test/devices/switch_test.py b/src/pywink/test/devices/switch_test.py index dfde93f..d703944 100644 --- a/src/pywink/test/devices/switch_test.py +++ b/src/pywink/test/devices/switch_test.py @@ -7,6 +7,9 @@ from pywink.api import get_devices_from_response_dict, WinkApiInterface from pywink.devices import types as device_types from pywink.devices.binary_switch import WinkBinarySwitch +from pywink.devices.binary_switch_group import WinkBinarySwitchGroup + +JSON_DATA = {} class BinarySwitchTests(unittest.TestCase): @@ -19,3 +22,46 @@ def test_state_should_be_false(self): switch = devices[0] self.assertFalse(switch.state()) + + def test_switch_groups_are_created_correctly(self): + all_devices = os.listdir('{}/api_responses/groups/'.format(os.path.dirname(__file__))) + device_list = [] + for json_file in all_devices: + if os.path.isfile('{}/api_responses/groups/{}'.format(os.path.dirname(__file__), json_file)): + _json_file = open('{}/api_responses/groups/{}'.format(os.path.dirname(__file__), json_file)) + device_list.append(json.load(_json_file)) + _json_file.close() + JSON_DATA["data"] = device_list + all_groups = get_devices_from_response_dict(JSON_DATA, device_types.GROUP) + self.assertEqual(2, len(all_groups)) + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/groups/switch_group.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + groups = get_devices_from_response_dict(response_dict, device_types.GROUP) + self.assertEqual(1, len(groups)) + self.assertTrue(isinstance(groups[0], WinkBinarySwitchGroup)) + + def test_switch_group_state_is_correct(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/groups/switch_group.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + groups = get_devices_from_response_dict(response_dict, device_types.GROUP) + switch_group = groups[0] + self.assertFalse(switch_group.state()) + + def test_switch_group_availble(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/groups/switch_group.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + groups = get_devices_from_response_dict(response_dict, device_types.GROUP) + switch_group = groups[0] + self.assertTrue(switch_group.available()) diff --git a/src/setup.py b/src/setup.py index 0732984..d06eea1 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='1.2.6', + version='1.3.0', description='Access Wink devices via the Wink API', url='http://github.com/python-wink/python-wink', author='Brad Johnson', From 97f2711a51b607c5f37ace70979152951df1cf75 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Sun, 16 Jul 2017 12:01:45 -0400 Subject: [PATCH 148/178] Fix fan speed (#87) * Set fan speed with fan state --- CHANGELOG.md | 3 +++ src/pywink/api.py | 5 ----- src/pywink/devices/fan.py | 24 ++++++++---------------- src/pywink/test/api_test.py | 2 +- src/setup.py | 2 +- 5 files changed, 13 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f41328..690f135 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 1.3.1 +- Fixed fans speed selection + ## 1.3.0 - Support for switch and light groups diff --git a/src/pywink/api.py b/src/pywink/api.py index afcf0e6..9992b73 100644 --- a/src/pywink/api.py +++ b/src/pywink/api.py @@ -1,5 +1,4 @@ import json -import sys import time import urllib.parse @@ -39,8 +38,6 @@ def set_device_state(self, device, state, id_override=None, type_override=None): arequest = requests.post(url_string, headers=API_HEADERS) else: - print(url_string) - print(state) arequest = requests.post(url_string, data=json.dumps(state), headers=API_HEADERS) @@ -56,7 +53,6 @@ def set_device_state(self, device, state, id_override=None, type_override=None): headers=API_HEADERS) else: raise WinkAPIException("Failed to refresh access token.") - print(str(arequest.json())) return arequest.json() def get_device_state(self, device, id_override=None, type_override=None): @@ -120,7 +116,6 @@ def legacy_set_wink_credentials(email, password, client_id, client_secret): response_json = response.json() access_token = response_json.get('access_token') REFRESH_TOKEN = response_json.get('refresh_token') - sys.stdout.write(access_token) set_bearer_token(access_token) diff --git a/src/pywink/devices/fan.py b/src/pywink/devices/fan.py index 2dd7d9d..a53eb7e 100644 --- a/src/pywink/devices/fan.py +++ b/src/pywink/devices/fan.py @@ -39,7 +39,7 @@ def fan_timer_range(self): return fan_timer_range def current_fan_speed(self): - return self._last_reading.get('mode', None) + return self._last_reading.get('mode', "lowest") def current_fan_direction(self): return self._last_reading.get('direction', None) @@ -50,26 +50,18 @@ def current_timer(self): def state(self): return self._last_reading.get('powered', False) - def set_state(self, state): + def set_state(self, state, speed=None): """ :param powered: bool - :return: nothing - """ - desired_state = {"powered": state} - - response = self.api_interface.set_device_state(self, { - "desired_state": desired_state - }) - - self._update_state_from_response(response) - - def set_fan_speed(self, speed): - """ :param speed: a string one of ["lowest", "low", - "medium", "high", "auto"] + "medium", "high", "auto"] defaults to last speed :return: nothing """ - desired_state = {"mode": speed} + speed = speed or self.current_fan_speed() + if state: + desired_state = {"powered": state, "mode": speed} + else: + desired_state = {"powered": state} response = self.api_interface.set_device_state(self, { "desired_state": desired_state diff --git a/src/pywink/test/api_test.py b/src/pywink/test/api_test.py index 67ee9e7..e323d45 100644 --- a/src/pywink/test/api_test.py +++ b/src/pywink/test/api_test.py @@ -386,7 +386,7 @@ def test_get_fan_updated_states_from_api(self): old_states = {} for device in devices: device.api_interface = self.api_interface - device.set_fan_speed("auto") + device.set_state(True, "auto") device.set_fan_direction("reverse") device.set_fan_timer(300) device.update_state() diff --git a/src/setup.py b/src/setup.py index d06eea1..42116a1 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='1.3.0', + version='1.3.1', description='Access Wink devices via the Wink API', url='http://github.com/python-wink/python-wink', author='Brad Johnson', From aa97a9a126713d7958aecacc6bdb8acff4b82cc3 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Sat, 22 Jul 2017 19:38:20 -0400 Subject: [PATCH 149/178] Local control for lights, locks and switches (#76) --- CHANGELOG.md | 17 +- script/lint | 2 + src/pywink/__init__.py | 6 +- src/pywink/api.py | 174 +++++++++++++++--- src/pywink/devices/base.py | 23 ++- src/pywink/devices/binary_switch.py | 11 +- src/pywink/devices/hub.py | 3 + src/pywink/devices/light_bulb.py | 7 +- src/pywink/devices/lock.py | 17 +- src/pywink/test/__init__.py | 3 +- src/pywink/test/api_test.py | 102 +++++++++- .../home_decorators_light_bulb.json | 81 ++++++++ src/pywink/test/devices/base_test.py | 2 +- src/setup.py | 2 +- 14 files changed, 387 insertions(+), 63 deletions(-) create mode 100644 src/pywink/test/devices/api_responses/home_decorators_light_bulb.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 690f135..3488f02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 1.4.0 +- Local control support for lights, locks, and switches + ## 1.3.1 - Fixed fans speed selection @@ -105,10 +108,10 @@ ## 0.7.6 - Added ability to return the battery level if a device is battery powered -## 0.7.5 +## 0.7.5 - Fixed bug where light bulb states were not updating. -## 0.7.4 +## 0.7.4 - Fixed bug where we shouldn't have been indexing into an object ## 0.7.3 @@ -117,12 +120,12 @@ ## 0.7.2 - Conserving brightness when setting color (temperature, hue sat, etc.) -## 0.7.1 +## 0.7.1 - Exposed bulb color support methods (E.g. supports_hue_saturation()) ## 0.7.0 - Expanded color support for WinkBulbs -- Added ability to supply client_id, client_secret, and refresh_token +- Added ability to supply client_id, client_secret, and refresh_token instead of access_token. This should get around tokens expiring. ## 0.6.4 @@ -138,11 +141,11 @@ instead of access_token. This should get around tokens expiring. ## 0.6.1 - Return the capability of a sensor as part of the name. -## 0.6.0 +## 0.6.0 - Major structural change. Using modules to avoid circular dependencies. - Added support for devices that contain multiple onboard sensors. -## 0.5.0 +## 0.5.0 - Major bug fix. Methods like `get_bulbs` were always returning empty lists. ## 0.4.3 @@ -170,7 +173,7 @@ instead of access_token. This should get around tokens expiring. ## 0.3.1 - Added init method for WinkEggTray -## 0.3.0 +## 0.3.0 - Breaking change: Renamed classes to satisfy pylint ## 0.2.1 diff --git a/script/lint b/script/lint index 32646ba..94f51f2 100755 --- a/script/lint +++ b/script/lint @@ -2,11 +2,13 @@ cd "$(dirname "$0")/.." +flake8 --version echo "Checking style with flake8..." flake8 src/pywink FLAKE8_STATUS=$? +pylint --version echo "Checking style with pylint..." pylint src/pywink PYLINT_STATUS=$? diff --git a/src/pywink/__init__.py b/src/pywink/__init__.py index 4399ff3..283e15c 100644 --- a/src/pywink/__init__.py +++ b/src/pywink/__init__.py @@ -3,10 +3,10 @@ """ # noqa from pywink.api import set_bearer_token, refresh_access_token, \ - set_wink_credentials, set_user_agent, wink_api_fetch, \ - get_set_access_token, is_token_set, get_devices, \ + set_wink_credentials, set_user_agent, wink_api_fetch, get_devices, \ get_subscription_key, get_user, get_authorization_url, \ - request_token, legacy_set_wink_credentials + request_token, legacy_set_wink_credentials, get_current_oauth_credentials, \ + disable_local_control from pywink.api import get_light_bulbs, get_garage_doors, get_locks, \ get_powerstrips, get_shades, get_sirens, \ diff --git a/src/pywink/api.py b/src/pywink/api.py index 9992b73..acd2733 100644 --- a/src/pywink/api.py +++ b/src/pywink/api.py @@ -1,20 +1,33 @@ import json import time +import logging import urllib.parse -import requests - from pywink.devices import types as device_types from pywink.devices.factory import build_device -API_HEADERS = {} +import requests +try: + import urllib3 + from urllib3.exceptions import InsecureRequestWarning + urllib3.disable_warnings(InsecureRequestWarning) +except ImportError: + pass + CLIENT_ID = None CLIENT_SECRET = None REFRESH_TOKEN = None USER_AGENT = "Manufacturer/python-wink python/3 Wink/3" +API_HEADERS = {"User-Agent": USER_AGENT} ALL_DEVICES = None LAST_UPDATE = None OAUTH_AUTHORIZE = "{}/oauth2/authorize?client_id={}&redirect_uri={}" +LOCAL_API_HEADERS = {} +HUBS = {} +SUPPORTS_LOCAL_CONTROL = ["wink_hub", "wink_hub2"] +ALLOW_LOCAL_CONTROL = True + +_LOGGER = logging.getLogger(__name__) class WinkApiInterface(object): @@ -27,6 +40,7 @@ def set_device_state(self, device, state, id_override=None, type_override=None): :param state: a boolean of true (on) or false ('off') :return: The JSON response from the API (new device state) """ + _LOGGER.info("Setting state via online API") object_id = id_override or device.object_id() object_type = type_override or device.object_type() url_string = "{}/{}s/{}".format(self.BASE_URL, @@ -53,48 +67,107 @@ def set_device_state(self, device, state, id_override=None, type_override=None): headers=API_HEADERS) else: raise WinkAPIException("Failed to refresh access token.") - return arequest.json() + response_json = arequest.json() + _LOGGER.debug(response_json) + return response_json + + def local_set_state(self, device, state, id_override=None, type_override=None): + if ALLOW_LOCAL_CONTROL: + if device.local_id() is not None: + hub = HUBS.get(device.hub_id()) + if hub is None: + return self.set_device_state(device, state, id_override, type_override) + else: + return self.set_device_state(device, state, id_override, type_override) + _LOGGER.info("Setting local state") + local_id = id_override or device.local_id().split(".")[0] + object_type = type_override or device.object_type() + LOCAL_API_HEADERS['Authorization'] = "Bearer " + hub["token"] + url_string = "https://{}:8888/{}s/{}".format(hub["ip"], + object_type, + local_id) + arequest = requests.put(url_string, + data=json.dumps(state), + headers=LOCAL_API_HEADERS, + verify=False) + response_json = arequest.json() + _LOGGER.debug(response_json) + temp_state = device.json_state + for key, value in response_json["data"]["last_reading"].items(): + temp_state["last_reading"][key] = value + return temp_state + else: + return self.set_device_state(device, state, id_override, type_override) def get_device_state(self, device, id_override=None, type_override=None): """ :type device: WinkDevice """ + _LOGGER.info("Getting state via online API") object_id = id_override or device.object_id() object_type = type_override or device.object_type() url_string = "{}/{}s/{}".format(self.BASE_URL, object_type, object_id) arequest = requests.get(url_string, headers=API_HEADERS) - return arequest.json() - - -def get_set_access_token(): - auth = API_HEADERS.get("Authorization") - if auth is not None: - return auth.split()[1] - return None + response_json = arequest.json() + _LOGGER.debug(response_json) + return response_json + def local_get_state(self, device, id_override=None, type_override=None): + """ + :type device: WinkDevice + """ + if ALLOW_LOCAL_CONTROL: + if device.local_id() is not None: + hub = HUBS.get(device.hub_id()) + if hub is not None: + ip = hub["ip"] + access_token = hub["token"] + else: + return self.get_device_state(device, id_override, type_override) + else: + return self.get_device_state(device, id_override, type_override) + _LOGGER.info("Getting local state") + local_id = id_override or device.local_id() + object_type = type_override or device.object_type() + LOCAL_API_HEADERS['Authorization'] = "Bearer " + access_token + url_string = "https://{}:8888/{}s/{}".format(ip, + object_type, + local_id) + arequest = requests.get(url_string, + headers=LOCAL_API_HEADERS, + verify=False) + response_json = arequest.json() + _LOGGER.debug(response_json) + temp_state = device.json_state + for key, value in response_json["data"]["last_reading"].items(): + temp_state["last_reading"][key] = value + return temp_state + else: + return self.get_device_state(device, id_override, type_override) -def set_bearer_token(token): - global API_HEADERS - API_HEADERS = { - "Content-Type": "application/json", - "Authorization": "Bearer {}".format(token) - } - if USER_AGENT: - API_HEADERS["User-Agent"] = USER_AGENT +def disable_local_control(): + global ALLOW_LOCAL_CONTROL + ALLOW_LOCAL_CONTROL = False def set_user_agent(user_agent): - global USER_AGENT + _LOGGER.info("Setting user agent to " + user_agent) + API_HEADERS["User-Agent"] = user_agent - USER_AGENT = user_agent - if USER_AGENT: - API_HEADERS["User-Agent"] = USER_AGENT +def set_bearer_token(token): + global LOCAL_API_HEADERS + + API_HEADERS["Content-Type"] = "application/json" + API_HEADERS["Authorization"] = "Bearer {}".format(token) + LOCAL_API_HEADERS = API_HEADERS def legacy_set_wink_credentials(email, password, client_id, client_secret): + log_string = "Email: %s Password: %s Client_id: %s Client_secret: %s" % (email, password, client_id, client_secret) + _LOGGER.debug(log_string) global CLIENT_ID, CLIENT_SECRET, REFRESH_TOKEN CLIENT_ID = client_id @@ -120,6 +193,9 @@ def legacy_set_wink_credentials(email, password, client_id, client_secret): def set_wink_credentials(client_id, client_secret, access_token, refresh_token): + log_string = "Client_id: %s Client_secret: %s Access_token: %s Refreash_token: %s" % (client_id, client_secret, + access_token, refresh_token) + _LOGGER.debug(log_string) global CLIENT_ID, CLIENT_SECRET, REFRESH_TOKEN CLIENT_ID = client_id @@ -128,7 +204,15 @@ def set_wink_credentials(client_id, client_secret, access_token, refresh_token): set_bearer_token(access_token) +def get_current_oauth_credentials(): + access_token = API_HEADERS.get("Authorization").split()[1] + return {"access_token": access_token, "refresh_token": REFRESH_TOKEN, + "client_id": CLIENT_ID, "client_secret": CLIENT_SECRET} + + def refresh_access_token(): + global REFRESH_TOKEN + _LOGGER.info("Attempting to refresh access token") if CLIENT_ID and CLIENT_SECRET and REFRESH_TOKEN: data = { "client_id": CLIENT_ID, @@ -144,12 +228,14 @@ def refresh_access_token(): headers=headers) response_json = response.json() access_token = response_json.get('access_token') + REFRESH_TOKEN = response_json.get('refresh_token') set_bearer_token(access_token) return access_token return None def get_authorization_url(client_id, redirect_uri): + _LOGGER.debug("Client_id: " + client_id + " redirect_uri: " + redirect_uri) global CLIENT_ID CLIENT_ID = client_id @@ -158,6 +244,7 @@ def get_authorization_url(client_id, redirect_uri): def request_token(code, client_secret): + _LOGGER.debug("code: " + code + " Client_secret: " + client_secret) data = { "client_secret": client_secret, "grant_type": "authorization_code", @@ -169,6 +256,7 @@ def request_token(code, client_secret): response = requests.post('{}/oauth2/token'.format(WinkApiInterface.BASE_URL), data=json.dumps(data), headers=headers) + _LOGGER.debug(response) response_json = response.json() access_token = response_json.get('access_token') refresh_token = response_json.get('refresh_token') @@ -178,12 +266,34 @@ def request_token(code, client_secret): def get_user(): url_string = "{}/users/me".format(WinkApiInterface.BASE_URL) arequest = requests.get(url_string, headers=API_HEADERS) + _LOGGER.debug(arequest) return arequest.json() -def is_token_set(): - """ Returns if an auth token has been set. """ - return bool(API_HEADERS) +def get_local_control_access_token(local_control_id): + _LOGGER.debug("Local_control_id: " + local_control_id) + if CLIENT_ID and CLIENT_SECRET and REFRESH_TOKEN: + data = { + "client_id": CLIENT_ID, + "client_secret": CLIENT_SECRET, + "grant_type": "refresh_token", + "refresh_token": REFRESH_TOKEN, + "scope": "local_control", + "local_control_id": local_control_id + } + headers = { + 'Content-Type': 'application/json' + } + response = requests.post('{}/oauth2/token'.format(WinkApiInterface.BASE_URL), + data=json.dumps(data), + headers=headers) + _LOGGER.debug(response) + response_json = response.json() + access_token = response_json.get('access_token') + return access_token + _LOGGER.error("Failed to get local control access, reverting to online API") + disable_local_control() + return None def get_all_devices(): @@ -243,7 +353,14 @@ def get_thermostats(): def get_hubs(): - return get_devices(device_types.HUB) + hubs = get_devices(device_types.HUB) + for hub in hubs: + if hub.manufacturer_device_model() in SUPPORTS_LOCAL_CONTROL: + _id = hub.local_control_id() + token = get_local_control_access_token(_id) + ip = hub.ip_address() + HUBS[hub.object_id()] = {"ip": ip, "token": token, "id": _id} + return hubs def get_fans(): @@ -323,6 +440,7 @@ def get_subscription_key_from_response_dict(device): def wink_api_fetch(end_point='wink_devices'): arequest_url = "{}/users/me/{}".format(WinkApiInterface.BASE_URL, end_point) response = requests.get(arequest_url, headers=API_HEADERS) + _LOGGER.debug(response) if response.status_code == 200: return response.json() diff --git a/src/pywink/devices/base.py b/src/pywink/devices/base.py index 8d5c06a..a2c4e6a 100644 --- a/src/pywink/devices/base.py +++ b/src/pywink/devices/base.py @@ -32,6 +32,19 @@ def object_id(self): def object_type(self): return self.obj_type + def hub_id(self): + return self.json_state.get('hub_id') + + def local_id(self): + # Devices with a "gang" controlling them (Ceiling fans and their associated light) + # must be controlled by the gang's local control ID. + # These devices local control ID is in the following format (gang_id.their_id) + # Stripping the trailing ID so the first ID is always used. + _local_id = self.json_state.get('local_id') + if _local_id is not None: + _local_id = _local_id.split(".")[0] + return _local_id + @property def _last_reading(self): return self.json_state.get('last_reading') or {} @@ -41,8 +54,14 @@ def available(self): def battery_level(self): if not self._last_reading.get('external_power'): - return self._last_reading.get('battery') - return None + try: + _battery = self._last_reading.get('battery') + _battery = float(_battery) + return _battery + except TypeError: + return None + else: + return None def manufacturer_device_model(self): return self.json_state.get('manufacturer_device_model') diff --git a/src/pywink/devices/binary_switch.py b/src/pywink/devices/binary_switch.py index e60b098..7b4fce3 100644 --- a/src/pywink/devices/binary_switch.py +++ b/src/pywink/devices/binary_switch.py @@ -15,14 +15,14 @@ def set_state(self, state): :return: nothing """ values = {"desired_state": {"powered": state}} - response = self.api_interface.set_device_state(self, values, type_override="binary_switche") + response = self.api_interface.local_set_state(self, values, type_override="binary_switche") self._update_state_from_response(response) def update_state(self): """ Update state with latest info from Wink API. """ - response = self.api_interface.get_device_state(self, type_override="binary_switche") + response = self.api_interface.local_get_state(self, type_override="binary_switche") return self._update_state_from_response(response) @@ -40,8 +40,13 @@ def set_state(self, state): :return: nothing """ values = {"desired_state": {"opened": state}} - response = self.api_interface.set_device_state(self, values, type_override="binary_switche") + response = self.api_interface.local_set_state(self, values, type_override="binary_switche") self._update_state_from_response(response) def last_event(self): return self._last_reading.get("last_event") + + def update_state(self): + """ Update state with latest info from Wink API. """ + response = self.api_interface.local_get_state(self) + return self._update_state_from_response(response) diff --git a/src/pywink/devices/hub.py b/src/pywink/devices/hub.py index f120a99..e1502d3 100644 --- a/src/pywink/devices/hub.py +++ b/src/pywink/devices/hub.py @@ -28,3 +28,6 @@ def ip_address(self): def firmware_version(self): return self._last_reading.get('firmware_version') + + def local_control_id(self): + return self._last_reading.get('local_control_id') diff --git a/src/pywink/devices/light_bulb.py b/src/pywink/devices/light_bulb.py index bac8801..3cfd8cd 100644 --- a/src/pywink/devices/light_bulb.py +++ b/src/pywink/devices/light_bulb.py @@ -47,6 +47,11 @@ def color_saturation(self): """ return self._last_reading.get('saturation') + def update_state(self): + """ Update state with latest info from Wink API. """ + response = self.api_interface.local_get_state(self) + return self._update_state_from_response(response) + def set_state(self, state, brightness=None, color_kelvin=None, color_xy=None, color_hue_saturation=None): @@ -72,7 +77,7 @@ def set_state(self, state, brightness=None, if brightness is not None: desired_state.update({'brightness': brightness}) - response = self.api_interface.set_device_state(self, { + response = self.api_interface.local_set_state(self, { "desired_state": desired_state }) self._update_state_from_response(response) diff --git a/src/pywink/devices/lock.py b/src/pywink/devices/lock.py index ca9372e..9b10050 100644 --- a/src/pywink/devices/lock.py +++ b/src/pywink/devices/lock.py @@ -34,7 +34,7 @@ def set_alarm_sensitivity(self, mode): :return: nothing """ values = {"desired_state": {"alarm_sensitivity": mode}} - response = self.api_interface.set_device_state(self, values) + response = self.api_interface.local_set_state(self, values) self._update_state_from_response(response) def set_alarm_mode(self, mode): @@ -43,7 +43,7 @@ def set_alarm_mode(self, mode): :return: nothing """ values = {"desired_state": {"alarm_mode": mode}} - response = self.api_interface.set_device_state(self, values) + response = self.api_interface.local_set_state(self, values) self._update_state_from_response(response) def set_alarm_state(self, state): @@ -52,7 +52,7 @@ def set_alarm_state(self, state): :return: nothing """ values = {"desired_state": {"alarm_enabled": state}} - response = self.api_interface.set_device_state(self, values) + response = self.api_interface.local_set_state(self, values) self._update_state_from_response(response) def set_vacation_mode(self, state): @@ -61,7 +61,7 @@ def set_vacation_mode(self, state): :return: nothing """ values = {"desired_state": {"vacation_mode_enabled": state}} - response = self.api_interface.set_device_state(self, values) + response = self.api_interface.local_set_state(self, values) self._update_state_from_response(response) def set_beeper_mode(self, state): @@ -70,7 +70,7 @@ def set_beeper_mode(self, state): :return: nothing """ values = {"desired_state": {"beeper_enabled": state}} - response = self.api_interface.set_device_state(self, values) + response = self.api_interface.local_set_state(self, values) self._update_state_from_response(response) def set_state(self, state): @@ -79,5 +79,10 @@ def set_state(self, state): :return: nothing """ values = {"desired_state": {"locked": state}} - response = self.api_interface.set_device_state(self, values) + response = self.api_interface.local_set_state(self, values) self._update_state_from_response(response) + + def update_state(self): + """ Update state with latest info from Wink API. """ + response = self.api_interface.local_get_state(self) + return self._update_state_from_response(response) diff --git a/src/pywink/test/__init__.py b/src/pywink/test/__init__.py index e085d06..f648b26 100644 --- a/src/pywink/test/__init__.py +++ b/src/pywink/test/__init__.py @@ -3,8 +3,7 @@ """ # noqa from pywink.api import set_bearer_token, refresh_access_token, \ - set_wink_credentials, set_user_agent, wink_api_fetch, \ - get_set_access_token, is_token_set, get_devices, \ + set_wink_credentials, set_user_agent, wink_api_fetch, get_devices, \ get_subscription_key from pywink.api import get_light_bulbs, get_garage_doors, get_locks, \ diff --git a/src/pywink/test/api_test.py b/src/pywink/test/api_test.py index e323d45..6792e70 100644 --- a/src/pywink/test/api_test.py +++ b/src/pywink/test/api_test.py @@ -9,6 +9,7 @@ # Third-party imports... import requests from mock import patch +from unittest.mock import MagicMock, Mock from pywink.api import * from pywink.devices import types as device_types @@ -68,24 +69,43 @@ def setUp(self): start_mock_server(self.port) self.api_interface = MockApiInterface() + def test_local_control_enabled_by_default(self): + self.assertTrue(ALLOW_LOCAL_CONTROL) + + def test_that_disable_local_control_works(self): + from pywink.api import ALLOW_LOCAL_CONTROL + disable_local_control() + self.assertFalse(ALLOW_LOCAL_CONTROL) + + def test_set_user_agent(self): + from pywink.api import API_HEADERS + set_user_agent("THIS IS A TEST") + self.assertEqual("THIS IS A TEST", API_HEADERS["User-Agent"]) + + def test_set_bearer_token(self): + from pywink.api import API_HEADERS, LOCAL_API_HEADERS + set_bearer_token("THIS IS A TEST") + self.assertEqual("Bearer THIS IS A TEST", API_HEADERS["Authorization"]) + + + def test_get_authorization_url(self): + WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) + url = get_authorization_url("TEST", "127.0.0.1") + comparison_url = "%s/oauth2/authorize?client_id=TEST&redirect_uri=127.0.0.1" % ("http://localhost:" + str(self.port)) + self.assertEqual(comparison_url, url) + def test_bad_status_codes(self): try: WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) + "/401/" - get_all_devices() + wink_api_fetch() except Exception as e: self.assertTrue(type(e), WinkAPIException) try: WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) + "/404/" - get_all_devices() + wink_api_fetch() except Exception as e: self.assertTrue(type(e), WinkAPIException) - def test_set_bearer_token(self): - self.assertIsNone(get_set_access_token()) - set_bearer_token("THIS_IS_A_TEST") - self.assertEqual("THIS_IS_A_TEST", get_set_access_token()) - self.assertTrue(is_token_set()) - def test_get_subscription_key(self): WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) get_all_devices() @@ -94,7 +114,7 @@ def test_get_subscription_key(self): def test_get_all_devices_from_api(self): WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) devices = get_all_devices() - self.assertEqual(len(devices), 64) + self.assertEqual(len(devices), 65) lights = get_light_bulbs() for light in lights: self.assertTrue(isinstance(light, WinkLightBulb)) @@ -249,6 +269,64 @@ def test_get_light_group_updated_state_from_api(self): for device in devices: self.assertTrue(device.state()) + def test_all_devices_local_control_id_is_not_decimal(self): + WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) + devices = get_all_devices() + for device in devices: + if device.local_id() is not None: + _temp = float(device.local_id()) + _temp2 = int(device.local_id()) + self.assertEqual(_temp, _temp2) + + def test_local_control_get_state_is_being_called(self): + mock_api_object = Mock() + mock_api_object.local_get_state = MagicMock() + mock_api_object.get_device_state = MagicMock() + devices = get_light_bulbs() + devices[0].api_interface = mock_api_object + devices[0].update_state() + mock_api_object.local_get_state.assert_called_with(devices[0]) + + def test_local_control_set_state_is_being_called(self): + + def Any(cls): + class Any(cls): + def __eq__(self, other): + return True + return Any() + + mock_api_object = Mock() + mock_api_object.local_set_state = MagicMock() + mock_api_object.set_device_state = MagicMock() + devices = get_light_bulbs() + devices[0].api_interface = mock_api_object + devices[0].set_state(True) + mock_api_object.local_set_state.assert_called_with(devices[0], Any(str)) + + def test_local_control_get_state_is_not_being_called(self): + mock_api_object = Mock() + mock_api_object.local_get_state = MagicMock() + mock_api_object.get_device_state = MagicMock() + devices = get_piggy_banks() + devices[0].api_interface = mock_api_object + devices[0].update_state() + mock_api_object.get_device_state.assert_called_with(devices[0]) + + def test_local_control_set_state_is_not_being_called(self): + + def Any(cls): + class Any(cls): + def __eq__(self, other): + return True + return Any() + + mock_api_object = Mock() + mock_api_object.local_set_state = MagicMock() + mock_api_object.set_device_state = MagicMock() + devices = get_thermostats() + devices[0].api_interface = mock_api_object + devices[0].set_operation_mode("auto") + mock_api_object.set_device_state.assert_called_with(devices[0], Any(str)) def test_get_shade_updated_states_from_api(self): WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) @@ -523,6 +601,9 @@ def set_device_state(self, device, state, id_override=None, type_override=None): return return_dict + def local_set_state(self, device, state, id_override=None, type_override=None): + return self.set_device_state(device, state, id_override, type_override) + def get_device_state(self, device, id_override=None, type_override=None): """ :type device: WinkDevice @@ -535,3 +616,6 @@ def get_device_state(self, device, id_override=None, type_override=None): return_dict["data"] = device return return_dict + def local_get_state(self, device, id_override=None, type_override=None): + return self.get_device_state(device, id_override, type_override) + diff --git a/src/pywink/test/devices/api_responses/home_decorators_light_bulb.json b/src/pywink/test/devices/api_responses/home_decorators_light_bulb.json new file mode 100644 index 0000000..9d89c61 --- /dev/null +++ b/src/pywink/test/devices/api_responses/home_decorators_light_bulb.json @@ -0,0 +1,81 @@ +{ + "object_type": "light_bulb", + "object_id": "2816576", + "uuid": "27ead441-9df5-4068-8880-7c1520880d1d", + "icon_id": "71", + "icon_code": "light_bulb-light_bulb", + "desired_state": { + "powered": false, + "brightness": 1.0 + }, + "last_reading": { + "powered": false, + "powered_updated_at": 1500756445.3817778, + "brightness": 1.0, + "brightness_updated_at": 1500756445.3817778, + "connection": true, + "connection_updated_at": 1500756445.3817778, + "firmware_version": "0.0b00 / 0.0b0f", + "firmware_version_updated_at": 1500756445.3817778, + "firmware_date_code": null, + "firmware_date_code_updated_at": null, + "desired_powered_updated_at": 1500744848.702631, + "desired_brightness_updated_at": 1500744981.9335368, + "powered_changed_at": 1500744848.5510266, + "brightness_changed_at": 1500136535.6655562, + "connection_changed_at": 1500139260.8078806, + "firmware_version_changed_at": 1500069548.8840287, + "desired_powered_changed_at": 1500744848.702631, + "desired_brightness_changed_at": 1500139582.3379252 + }, + "subscription": { + "pubnub": { + "subscribe_key": "sub-c-f7bf7f7e-0542-11e3-a5e8-02ee2ddab7fe", + "channel": "fsdjafkljakl;sdjf;lkadsfkl;djasd" + } + }, + "light_bulb_id": "2816576", + "name": "Dining room fan light", + "locale": "en_us", + "units": {}, + "created_at": 1500069521, + "hidden_at": null, + "capabilities": { + "fields": [ + { + "type": "boolean", + "field": "powered", + "mutability": "read-write" + }, + { + "type": "percentage", + "field": "brightness", + "mutability": "read-write" + }, + { + "type": "boolean", + "field": "connection", + "mutability": "read-only" + } + ] + }, + "triggers": [], + "manufacturer_device_model": "home_decorators_home_decorators_light_bulb", + "manufacturer_device_id": null, + "device_manufacturer": "home_decorators", + "model_name": "Ceiling Fan", + "upc_id": "485", + "upc_code": "home_decorators_light_bulb", + "primary_upc_code": "home_decorators_light_bulb", + "gang_id": "91031", + "hub_id": "302528", + "local_id": "37.2", + "radio_type": "zigbee", + "linked_service_id": null, + "lat_lng": [ + null, + null + ], + "location": "", + "order": 0 +} \ No newline at end of file diff --git a/src/pywink/test/devices/base_test.py b/src/pywink/test/devices/base_test.py index bd55d53..1449848 100644 --- a/src/pywink/test/devices/base_test.py +++ b/src/pywink/test/devices/base_test.py @@ -119,7 +119,7 @@ def test_all_devices_manufacturer_device_id_state_is_valid(self): skip_manufactuer_device_models = ["linear_wadwaz_1", "linear_wapirz_1", "aeon_labs_dsb45_zwus", "wink_hub", "wink_hub2", "sylvania_sylvania_ct", "ge_bulb", "quirky_ge_spotter", "schlage_zwave_lock", "home_decorators_home_decorators_fan", "sylvania_sylvania_rgbw", "somfy_bali", "wink_relay_sensor", "wink_project_one", "kidde_smoke_alarm", - "wink_relay_switch", "leaksmart_valve"] + "wink_relay_switch", "leaksmart_valve", "home_decorators_home_decorators_light_bulb"] skip_names = ["GoControl Thermostat", "GE Zwave Switch", "New Shortcut", "Test robot"] for device in devices: if device.name() in skip_names: diff --git a/src/setup.py b/src/setup.py index 42116a1..93a5487 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='1.3.1', + version='1.4.0', description='Access Wink devices via the Wink API', url='http://github.com/python-wink/python-wink', author='Brad Johnson', From 57517571fb456a167db42a854cf46b450b279407 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Tue, 25 Jul 2017 17:23:12 -0400 Subject: [PATCH 150/178] Added timeout to local control calls (#88) * Added timeout to local control calls --- CHANGELOG.md | 3 +++ src/pywink/api.py | 22 +++++++++++++++------- src/setup.py | 2 +- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3488f02..c322319 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 1.4.1 +- Added timeout to local control calls + ## 1.4.0 - Local control support for lights, locks, and switches diff --git a/src/pywink/api.py b/src/pywink/api.py index acd2733..5cb4375 100644 --- a/src/pywink/api.py +++ b/src/pywink/api.py @@ -86,10 +86,14 @@ def local_set_state(self, device, state, id_override=None, type_override=None): url_string = "https://{}:8888/{}s/{}".format(hub["ip"], object_type, local_id) - arequest = requests.put(url_string, - data=json.dumps(state), - headers=LOCAL_API_HEADERS, - verify=False) + try: + arequest = requests.put(url_string, + data=json.dumps(state), + headers=LOCAL_API_HEADERS, + verify=False, timeout=3) + except requests.exceptions.ReadTimeout: + _LOGGER.error("Timeout sending local control request. Sending request online") + return self.set_device_state(device, state, id_override, type_override) response_json = arequest.json() _LOGGER.debug(response_json) temp_state = device.json_state @@ -134,9 +138,13 @@ def local_get_state(self, device, id_override=None, type_override=None): url_string = "https://{}:8888/{}s/{}".format(ip, object_type, local_id) - arequest = requests.get(url_string, - headers=LOCAL_API_HEADERS, - verify=False) + try: + arequest = requests.get(url_string, + headers=LOCAL_API_HEADERS, + verify=False, timeout=3) + except requests.exceptions.ReadTimeout: + _LOGGER.error("Timeout sending local control request. Sending request online") + return self.get_device_state(device, id_override, type_override) response_json = arequest.json() _LOGGER.debug(response_json) temp_state = device.json_state diff --git a/src/setup.py b/src/setup.py index 93a5487..7239ca7 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='1.4.0', + version='1.4.1', description='Access Wink devices via the Wink API', url='http://github.com/python-wink/python-wink', author='Brad Johnson', From d6f09eacc5c4a41f1abea27d1472bb0caeb73562 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Fri, 28 Jul 2017 08:44:12 -0400 Subject: [PATCH 151/178] Changed try/except in local control calls to catch all (#89) * Changed try/except in local control calls to catch all --- CHANGELOG.md | 3 +++ src/pywink/api.py | 10 ++++++---- src/setup.py | 2 +- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c322319..ac8969e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 1.4.2 +- Changed try/except in local control calls to catch all errors + ## 1.4.1 - Added timeout to local control calls diff --git a/src/pywink/api.py b/src/pywink/api.py index 5cb4375..7d77188 100644 --- a/src/pywink/api.py +++ b/src/pywink/api.py @@ -71,6 +71,7 @@ def set_device_state(self, device, state, id_override=None, type_override=None): _LOGGER.debug(response_json) return response_json + # pylint: disable=bare-except def local_set_state(self, device, state, id_override=None, type_override=None): if ALLOW_LOCAL_CONTROL: if device.local_id() is not None: @@ -91,8 +92,8 @@ def local_set_state(self, device, state, id_override=None, type_override=None): data=json.dumps(state), headers=LOCAL_API_HEADERS, verify=False, timeout=3) - except requests.exceptions.ReadTimeout: - _LOGGER.error("Timeout sending local control request. Sending request online") + except: + _LOGGER.error("Error sending local control request. Sending request online") return self.set_device_state(device, state, id_override, type_override) response_json = arequest.json() _LOGGER.debug(response_json) @@ -117,6 +118,7 @@ def get_device_state(self, device, id_override=None, type_override=None): _LOGGER.debug(response_json) return response_json + # pylint: disable=bare-except def local_get_state(self, device, id_override=None, type_override=None): """ :type device: WinkDevice @@ -142,8 +144,8 @@ def local_get_state(self, device, id_override=None, type_override=None): arequest = requests.get(url_string, headers=LOCAL_API_HEADERS, verify=False, timeout=3) - except requests.exceptions.ReadTimeout: - _LOGGER.error("Timeout sending local control request. Sending request online") + except: + _LOGGER.error("Error sending local control request. Sending request online") return self.get_device_state(device, id_override, type_override) response_json = arequest.json() _LOGGER.debug(response_json) diff --git a/src/setup.py b/src/setup.py index 7239ca7..91cdd58 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='1.4.1', + version='1.4.2', description='Access Wink devices via the Wink API', url='http://github.com/python-wink/python-wink', author='Brad Johnson', From 14a87711495dae2e9e2b906744f274462f06286c Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Wed, 9 Aug 2017 15:39:06 -0500 Subject: [PATCH 152/178] Set device names and added hub pairing commands (#90) --- CHANGELOG.md | 3 + src/pywink/devices/base.py | 6 ++ src/pywink/devices/factory.py | 4 +- src/pywink/devices/hub.py | 56 +++++++++++++++++++ src/pywink/devices/powerstrip.py | 9 +++ src/pywink/devices/scene.py | 6 -- src/pywink/test/api_test.py | 27 ++++++++- .../groups/empty_user_group.json | 19 +++++++ src/pywink/test/devices/scene_test.py | 9 --- src/setup.py | 2 +- 10 files changed, 122 insertions(+), 19 deletions(-) create mode 100644 src/pywink/test/devices/api_responses/groups/empty_user_group.json diff --git a/CHANGELOG.md b/CHANGELOG.md index ac8969e..33b06b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 1.5.0 +- Change device names and hub pairing commands + ## 1.4.2 - Changed try/except in local control calls to catch all errors diff --git a/src/pywink/devices/base.py b/src/pywink/devices/base.py index a2c4e6a..80eaf32 100644 --- a/src/pywink/devices/base.py +++ b/src/pywink/devices/base.py @@ -23,6 +23,12 @@ def __init__(self, device_state_as_json, api_interface): def name(self): return self.json_state.get('name') + def set_name(self, name): + response = self.api_interface.set_device_state(self, { + "name": name + }) + self._update_state_from_response(response) + def state(self): raise NotImplementedError("Must implement state") diff --git a/src/pywink/devices/factory.py b/src/pywink/devices/factory.py index 7e2777e..62998d6 100644 --- a/src/pywink/devices/factory.py +++ b/src/pywink/devices/factory.py @@ -103,8 +103,8 @@ def build_device(device_state_as_json, api_interface): elif object_type == device_types.SCENE: new_objects.append(WinkScene(device_state_as_json, api_interface)) elif object_type == device_types.GROUP: - # This will skip auto created groups that Wink creates. - if device_state_as_json.get("name")[0] not in [".", "@"]: + # This will skip auto created groups that Wink creates as well has empty groups + if device_state_as_json.get("name")[0] not in [".", "@"] and device_state_as_json.get("members"): # This is a group of swithces if device_state_as_json.get("reading_aggregation").get("brightness") is None: new_objects.append(WinkBinarySwitchGroup(device_state_as_json, api_interface)) diff --git a/src/pywink/devices/hub.py b/src/pywink/devices/hub.py index e1502d3..380c24f 100644 --- a/src/pywink/devices/hub.py +++ b/src/pywink/devices/hub.py @@ -1,5 +1,11 @@ +import logging + from pywink.devices.base import WinkDevice +import requests + +_LOGGER = logging.getLogger(__name__) + class WinkHub(WinkDevice): """ @@ -31,3 +37,53 @@ def firmware_version(self): def local_control_id(self): return self._last_reading.get('local_control_id') + + def pairing_mode(self): + return self._last_reading.get('pairing_mode') + + def update_firmware(self): + url_string = "{}/{}s/{}/update_firmware".format(self.api_interface.BASE_URL, + self.object_type, + self.object_id) + arequest = requests.post(url_string, + headers=self.api_interface.API_HEADERS) + response_json = arequest.json() + return response_json + + def pair_new_device(self, pairing_mode, pairing_mode_duration=60, pairing_device_type_selector=None, + kidde_radio_code=None): + """ + :param pairing_mode: a string one of ["zigbee", "zwave", "zwave_exclusion", + "zwave_network_rediscovery", "lutron", "bluetooth", "kidde"] + :param pairing_mode_duration: an int in seconds defaults to 60 + :param pairing_device_type_selector: a string I believe this is only for bluetooth devices. + :param kidde_radio_code: a string of 8 1s and 0s one for each dip switch on the kidde device + left --> right = 1 --> 8 + :return: nothing + """ + if pairing_mode == "lutron" and pairing_mode_duration < 120: + pairing_mode_duration = 120 + elif pairing_mode == "zwave_network_rediscovery": + pairing_mode_duration = 0 + elif pairing_mode == "bluetooth" and pairing_device_type_selector is None: + pairing_device_type_selector = "switchmate" + + desired_state = {"pairing_mode": pairing_mode, + "pairing_mode_duration": pairing_mode_duration} + + if pairing_mode == "kidde" and kidde_radio_code is not None: + # Convert dip switch 1 and 0s to an int + try: + kidde_radio_code_int = int(kidde_radio_code, 2) + desired_state = {"kidde_radio_code": kidde_radio_code_int, "pairing_mode": None} + except (TypeError, ValueError): + _LOGGER.error("An invalid Kidde radio code was provided. " + kidde_radio_code) + + if pairing_device_type_selector is not None: + desired_state.update({"pairing_device_type_selector": pairing_device_type_selector}) + + response = self.api_interface.set_device_state(self, { + "desired_state": desired_state + }) + + self._update_state_from_response(response) diff --git a/src/pywink/devices/powerstrip.py b/src/pywink/devices/powerstrip.py index 1bee861..788eba6 100644 --- a/src/pywink/devices/powerstrip.py +++ b/src/pywink/devices/powerstrip.py @@ -74,6 +74,15 @@ def parent_id(self): def parent_object_type(self): return self.json_state.get('parent_object_type') + def set_name(self, name): + if self.index() == 0: + values = {"outlets": [{"name": name}, {}]} + else: + values = {"outlets": [{}, {"name": name}]} + response = self.api_interface.set_device_state(self, values, id_override=self.parent_id(), + type_override="powerstrip") + self._update_state_from_response(response) + def set_state(self, state): """ :param state: a boolean of true (on) or false ('off') diff --git a/src/pywink/devices/scene.py b/src/pywink/devices/scene.py index fb852b3..834afec 100644 --- a/src/pywink/devices/scene.py +++ b/src/pywink/devices/scene.py @@ -26,9 +26,3 @@ def activate(self): """ response = self.api_interface.set_device_state(self, None) self._update_state_from_response(response) - - def update_state(self): - """ - Nothing changes in the JSON state of this device. - """ - return True diff --git a/src/pywink/test/api_test.py b/src/pywink/test/api_test.py index 6792e70..ed1d7da 100644 --- a/src/pywink/test/api_test.py +++ b/src/pywink/test/api_test.py @@ -483,6 +483,16 @@ def test_get_propane_tank_updated_states_from_api(self): device.update_state() self.assertEqual(device.tare(), 5.0) + def test_set_all_device_names(self): + WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) + devices = get_all_devices() + old_states = {} + for device in devices: + device.api_interface = self.api_interface + device.set_name("TEST_NAME") + device.update_state() + for device in devices: + self.assertTrue(device.name().startswith("TEST_NAME")) class MockServerRequestHandler(BaseHTTPRequestHandler): @@ -563,7 +573,22 @@ def set_device_state(self, device, state, id_override=None, type_override=None): device_object_type = device.object_type() object_type = type_override or device_object_type return_dict = {} - if object_type != "group": + if "name" in str(state): + for dict_device in USERS_ME_WINK_DEVICES.get('data'): + _object_id = dict_device.get("object_id") + if _object_id == object_id: + if device_object_type == "outlet": + index = device.index() + set_state = state["outlets"][index]["name"] + dict_device["outlets"][index]["name"] = set_state + return_dict["data"] = dict_device + else: + dict_device["name"] = state.get("name") + for dict_device in GROUPS.get('data'): + _object_id = dict_device.get("object_id") + if _object_id == object_id: + dict_device["name"] = state.get("name") + elif object_type != "group": for dict_device in USERS_ME_WINK_DEVICES.get('data'): _object_id = dict_device.get("object_id") if _object_id == object_id: diff --git a/src/pywink/test/devices/api_responses/groups/empty_user_group.json b/src/pywink/test/devices/api_responses/groups/empty_user_group.json new file mode 100644 index 0000000..75f4770 --- /dev/null +++ b/src/pywink/test/devices/api_responses/groups/empty_user_group.json @@ -0,0 +1,19 @@ +{ + "group_id": "2086633", + "name": "Living Room front", + "order": 0, + "icon_id": "28", + "members": [], + "reading_aggregation": {}, + "automation_mode": null, + "hidden_at": null, + "object_type": "group", + "object_id": "2086633", + "icon_code": "group-light_group", + "subscription": { + "pubnub": { + "subscribe_key": "sub-c-f7bf7f7e-0542-11edfadfadgdage2ddab7fe", + "channel": "jkasdjfa8r390uq4jasdfkljasd09i834" + } + } +} \ No newline at end of file diff --git a/src/pywink/test/devices/scene_test.py b/src/pywink/test/devices/scene_test.py index 69ee498..0e51fca 100644 --- a/src/pywink/test/devices/scene_test.py +++ b/src/pywink/test/devices/scene_test.py @@ -20,15 +20,6 @@ def test_state_should_be_false(self): scene = devices[0] self.assertFalse(scene.state()) - def test_update_state_should_be_true(self): - with open('{}/api_responses/scene.json'.format(os.path.dirname(__file__))) as scene_file: - response_dict = json.load(scene_file) - response_dict = {"data": [response_dict]} - devices = get_devices_from_response_dict(response_dict, device_types.SCENE) - - scene = devices[0] - self.assertTrue(scene.update_state()) - def test_available_should_be_true(self): with open('{}/api_responses/scene.json'.format(os.path.dirname(__file__))) as scene_file: response_dict = json.load(scene_file) diff --git a/src/setup.py b/src/setup.py index 91cdd58..e3e4b36 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='1.4.2', + version='1.5.0', description='Access Wink devices via the Wink API', url='http://github.com/python-wink/python-wink', author='Brad Johnson', From 5105422039e7174e506729e206f6456c9deb323c Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Thu, 10 Aug 2017 23:41:50 -0400 Subject: [PATCH 153/178] Added support for the Dome main water valve (#91) --- CHANGELOG.md | 3 + src/pywink/devices/binary_switch.py | 48 ++++---- src/pywink/devices/factory.py | 4 +- src/pywink/test/api_test.py | 2 +- .../api_responses/dome_water_main_valve.json | 108 ++++++++++++++++++ src/pywink/test/devices/base_test.py | 2 +- src/pywink/test/devices/leaksmart_test.py | 33 ------ src/pywink/test/devices/switch_test.py | 9 ++ src/setup.py | 2 +- 9 files changed, 146 insertions(+), 65 deletions(-) create mode 100644 src/pywink/test/devices/api_responses/dome_water_main_valve.json delete mode 100644 src/pywink/test/devices/leaksmart_test.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 33b06b2..ea6ebe7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 1.5.1 +- Support all binary_switches which use a binary field of powered or opened + ## 1.5.0 - Change device names and hub pairing commands diff --git a/src/pywink/devices/binary_switch.py b/src/pywink/devices/binary_switch.py index 7b4fce3..c21f689 100644 --- a/src/pywink/devices/binary_switch.py +++ b/src/pywink/devices/binary_switch.py @@ -1,5 +1,7 @@ from pywink.devices.base import WinkDevice +SUPPORTED_BINARY_STATE_FIELDS = ['powered', 'opened'] + class WinkBinarySwitch(WinkDevice): """ @@ -7,46 +9,40 @@ class WinkBinarySwitch(WinkDevice): """ def state(self): - return self._last_reading.get('powered', False) + _field = self.binary_state_name() + return self._last_reading.get(_field, False) def set_state(self, state): """ :param state: a boolean of true (on) or false ('off') :return: nothing """ - values = {"desired_state": {"powered": state}} + _field = self.binary_state_name() + values = {"desired_state": {_field: state}} response = self.api_interface.local_set_state(self, values, type_override="binary_switche") self._update_state_from_response(response) - def update_state(self): + def binary_state_name(self): """ - Update state with latest info from Wink API. + Search all of the capabilities of the device and return the supported binary state field. + Default to returning powered. """ - response = self.api_interface.local_get_state(self, type_override="binary_switche") - return self._update_state_from_response(response) - - -class WinkLeakSmartValve(WinkBinarySwitch): - """ - Represents a Wink leaksmart valve.. - """ - - def state(self): - return self._last_reading.get('opened', False) - - def set_state(self, state): - """ - :param state: a boolean of true (on) or false ('off') - :return: nothing - """ - values = {"desired_state": {"opened": state}} - response = self.api_interface.local_set_state(self, values, type_override="binary_switche") - self._update_state_from_response(response) + return_field = "powered" + _capabilities = self.json_state.get('capabilities') + if _capabilities is not None: + _fields = _capabilities.get('fields') + if _fields is not None: + for field in _fields: + if field.get('field') in SUPPORTED_BINARY_STATE_FIELDS: + return_field = field.get('field') + return return_field def last_event(self): return self._last_reading.get("last_event") def update_state(self): - """ Update state with latest info from Wink API. """ - response = self.api_interface.local_get_state(self) + """ + Update state with latest info from Wink API. + """ + response = self.api_interface.local_get_state(self, type_override="binary_switche") return self._update_state_from_response(response) diff --git a/src/pywink/devices/factory.py b/src/pywink/devices/factory.py index 62998d6..402e115 100644 --- a/src/pywink/devices/factory.py +++ b/src/pywink/devices/factory.py @@ -5,7 +5,7 @@ from pywink.devices import types as device_types from pywink.devices.sensor import WinkSensor from pywink.devices.light_bulb import WinkLightBulb -from pywink.devices.binary_switch import WinkBinarySwitch, WinkLeakSmartValve +from pywink.devices.binary_switch import WinkBinarySwitch from pywink.devices.lock import WinkLock from pywink.devices.eggtray import WinkEggtray from pywink.devices.garage_door import WinkGarageDoor @@ -45,8 +45,6 @@ def build_device(device_state_as_json, api_interface): mode = device_state_as_json["last_reading"]["powering_mode"] if mode == "dumb": new_objects.append(WinkBinarySwitch(device_state_as_json, api_interface)) - elif device_state_as_json.get("model_name") == "leakSMART Valve": - new_objects.append(WinkLeakSmartValve(device_state_as_json, api_interface)) else: new_objects.append(WinkBinarySwitch(device_state_as_json, api_interface)) elif object_type == device_types.LOCK: diff --git a/src/pywink/test/api_test.py b/src/pywink/test/api_test.py index ed1d7da..e381336 100644 --- a/src/pywink/test/api_test.py +++ b/src/pywink/test/api_test.py @@ -114,7 +114,7 @@ def test_get_subscription_key(self): def test_get_all_devices_from_api(self): WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) devices = get_all_devices() - self.assertEqual(len(devices), 65) + self.assertEqual(len(devices), 66) lights = get_light_bulbs() for light in lights: self.assertTrue(isinstance(light, WinkLightBulb)) diff --git a/src/pywink/test/devices/api_responses/dome_water_main_valve.json b/src/pywink/test/devices/api_responses/dome_water_main_valve.json new file mode 100644 index 0000000..9b6d0ba --- /dev/null +++ b/src/pywink/test/devices/api_responses/dome_water_main_valve.json @@ -0,0 +1,108 @@ +{ + "object_type":"binary_switch", + "object_id":"533047", + "uuid":"0b7eee51-dafdafdsfa81fbcdb3", + "icon_id":"52", + "icon_code":"binary_switch-light_bulb_dumb", + "desired_state":{ + "opened":false + }, + "last_reading":{ + "connection":true, + "connection_updated_at":1502418632.9629102, + "opened":false, + "opened_updated_at":1502418632.9629102, + "desired_opened_updated_at":1502418630.3983407, + "connection_changed_at":1502405364.6534495, + "opened_changed_at":1502418629.8664541, + "desired_opened_changed_at":1502418630.3983407 + }, + "subscription":{ + "pubnub":{ + "subscribe_key":"sub-c-f7bf7f7e-0542dafdfasdfaddab7fe", + "channel":"binary_switch-533047|f81666ab8671d046dfasdfadfasdf3" + } + }, + "binary_switch_id":"533047", + "name":"Water Main", + "locale":"en_us", + "units":{ + + }, + "created_at":1502405364, + "hidden_at":null, + "capabilities":{ + "fields":[ + { + "type":"boolean", + "field":"connection", + "mutability":"read-only" + }, + { + "type":"boolean", + "field":"opened", + "mutability":"read-write", + "attribute_id":2 + } + ], + "polling_interval":3600, + "automation_robots":[ + { + "name":"Auto Shutoff Valve", + "causes":[ + { + "value":"true", + "operator":"==", + "observed_field":"liquid_detected.or", + "observed_object_id":"SPECIAL_GROUP:.sensors", + "observed_object_type":"Group" + } + ], + "effects":[ + { + "scene":{ + "members":[ + { + "object_id":"SELF:id", + "object_type":"SELF:api_name", + "desired_state":{ + "opened":false + } + } + ] + } + } + ], + "enabled":false, + "automation_mode":"valve_auto_close" + } + ], + "notification_robots":[ + "operation_failure_notification", + "low_battery_notification", + "offline_notification" + ] + }, + "triggers":[ + + ], + "manufacturer_device_model":"dome_dmwv1", + "manufacturer_device_id":null, + "device_manufacturer":"dome", + "model_name":"Water Main Shut Off", + "upc_id":"908", + "upc_code":"dome_valve", + "primary_upc_code":"dome_valve", + "gang_id":null, + "hub_id":"696658", + "local_id":"3", + "radio_type":"zwave", + "linked_service_id":null, + "current_budget":null, + "lat_lng":[ + 12.34567, + -98.76543 + ], + "location":null, + "order":0 +} \ No newline at end of file diff --git a/src/pywink/test/devices/base_test.py b/src/pywink/test/devices/base_test.py index 1449848..765014d 100644 --- a/src/pywink/test/devices/base_test.py +++ b/src/pywink/test/devices/base_test.py @@ -119,7 +119,7 @@ def test_all_devices_manufacturer_device_id_state_is_valid(self): skip_manufactuer_device_models = ["linear_wadwaz_1", "linear_wapirz_1", "aeon_labs_dsb45_zwus", "wink_hub", "wink_hub2", "sylvania_sylvania_ct", "ge_bulb", "quirky_ge_spotter", "schlage_zwave_lock", "home_decorators_home_decorators_fan", "sylvania_sylvania_rgbw", "somfy_bali", "wink_relay_sensor", "wink_project_one", "kidde_smoke_alarm", - "wink_relay_switch", "leaksmart_valve", "home_decorators_home_decorators_light_bulb"] + "wink_relay_switch", "leaksmart_valve", "home_decorators_home_decorators_light_bulb", "dome_dmwv1"] skip_names = ["GoControl Thermostat", "GE Zwave Switch", "New Shortcut", "Test robot"] for device in devices: if device.name() in skip_names: diff --git a/src/pywink/test/devices/leaksmart_test.py b/src/pywink/test/devices/leaksmart_test.py deleted file mode 100644 index 096c4d8..0000000 --- a/src/pywink/test/devices/leaksmart_test.py +++ /dev/null @@ -1,33 +0,0 @@ -import json -import os -import unittest - -import mock - -from pywink.api import get_devices_from_response_dict, WinkApiInterface -from pywink.devices import types as device_types - - -class LeakSmartTests(unittest.TestCase): - - def setUp(self): - super(LeakSmartTests, self).setUp() - self.api_interface = mock.MagicMock() - device_list = [] - self.response_dict = {} - _json_file = open('{}/api_responses/leaksmart_valve.json'.format(os.path.dirname(__file__))) - device_list.append(json.load(_json_file)) - _json_file.close() - self.response_dict["data"] = device_list - - def test_siren_state(self): - binary_switches = get_devices_from_response_dict(self.response_dict, device_types.BINARY_SWITCH) - for switch in binary_switches: - if switch.model_name() == "leakSMART Valve": - self.assertTrue(switch.state()) - - def test_last_event(self): - binary_switches = get_devices_from_response_dict(self.response_dict, device_types.BINARY_SWITCH) - for switch in binary_switches: - if switch.model_name() == "leakSMART Valve": - self.assertEqual(switch.last_event(), "monthly_cycle_success") diff --git a/src/pywink/test/devices/switch_test.py b/src/pywink/test/devices/switch_test.py index d703944..9066188 100644 --- a/src/pywink/test/devices/switch_test.py +++ b/src/pywink/test/devices/switch_test.py @@ -65,3 +65,12 @@ def test_switch_group_availble(self): groups = get_devices_from_response_dict(response_dict, device_types.GROUP) switch_group = groups[0] self.assertTrue(switch_group.available()) + + def test_last_event(self): + with open('{}/api_responses/leaksmart_valve.json'.format(os.path.dirname(__file__))) as binary_switch_file: + response_dict = json.load(binary_switch_file) + response_dict = {"data": [response_dict]} + binary_switches = get_devices_from_response_dict(response_dict, device_types.BINARY_SWITCH) + for switch in binary_switches: + if switch.model_name() == "leakSMART Valve": + self.assertEqual(switch.last_event(), "monthly_cycle_success") \ No newline at end of file diff --git a/src/setup.py b/src/setup.py index e3e4b36..b3ae235 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='1.5.0', + version='1.5.1', description='Access Wink devices via the Wink API', url='http://github.com/python-wink/python-wink', author='Brad Johnson', From 90a44ae537ee12e1d7935229f9acf8cd6eecdb69 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Sun, 3 Sep 2017 09:46:13 -0400 Subject: [PATCH 154/178] Fixed firmware update and added device deletion (#92) --- CHANGELOG.md | 3 ++ README.md | 39 +++++++++++--- src/pywink/api.py | 106 +++++++++++++++++++++++++++++++++++-- src/pywink/devices/base.py | 3 ++ src/pywink/devices/hub.py | 10 +--- src/setup.py | 4 +- 6 files changed, 142 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea6ebe7..61ad3cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 1.5.2 +- Fixed firmware update and added device deletion + ## 1.5.1 - Support all binary_switches which use a binary field of powered or opened diff --git a/README.md b/README.md index 5baa38e..43eab92 100644 --- a/README.md +++ b/README.md @@ -3,17 +3,42 @@ [![Join the chat at https://gitter.im/python-wink/python-wink](https://badges.gitter.im/python-wink/python-wink.svg)](https://gitter.im/bradsk88/python-wink?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Build Status](https://travis-ci.org/python-wink/python-wink.svg?branch=master)](https://travis-ci.org/python-wink/python-wink) [![Coverage Status](https://coveralls.io/repos/github/python-wink/python-wink/badge.svg?branch=master)](https://coveralls.io/github/python-wink/python-wink?branch=master) -_This script used to be part of Home Assistant. It has been extracted to fit -the goal of Home Assistant to not contain any device specific API implementations -but rely on open-source implementations of the API._ +_This library is an attempt to implement the entire Wink API in Python 3._ +_Documentation for the Wink API can be found here http://docs.winkapiv2.apiary.io/# however, from my experience it isn't kept up-to-date._ + +_This library also has support for the unoffical local API and doesn't require a rooted hub._ + +This library provides support for Wink in Home Assistant! + +## To install +```bash +pip3 install python-wink +``` + +## Get developer credentials + +1. Vist https://developer.wink.com/login +2. Crate an account and request your credentials. (Approval can take several days) +3. Enter in a redirect URL to point at your application. +4. Plug in your details into the test script below. ## Example usage +Print all light names and state, and toggle their states. + ```python import pywink -pywink.set_bearer_token('YOUR_BEARER_TOKEN') -for switch in pywink.get_switches(): - print(switch.name(), switch.state()) - switch.set_state(not switch.state()) +print("Please vist the following URL to authenticate.") +print(pywink.get_authorization_url("YOUR_CLIENT_ID", "YOUR_REDIRECT_URL")) +code = input("Enter code from URL:") +auth = pywink.request_token(code, "YOUR_CLIENT_SECRET") +pywink.set_wink_credentials("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET", + auth.get("access_token"), auth.get("refresh_token")) + +lights = pywink.get_light_bulbs() +for light in lights: + print("Name: " + light.name()) + print("State: " + light.state()) + light.set_state(not light.state()) ``` diff --git a/src/pywink/api.py b/src/pywink/api.py index 7d77188..140479e 100644 --- a/src/pywink/api.py +++ b/src/pywink/api.py @@ -33,12 +33,22 @@ class WinkApiInterface(object): BASE_URL = "https://api.wink.com" + api_headers = API_HEADERS def set_device_state(self, device, state, id_override=None, type_override=None): """ - :type device: WinkDevice - :param state: a boolean of true (on) or false ('off') - :return: The JSON response from the API (new device state) + Set device state via online API. + + Args: + device (WinkDevice): The device the change is being requested for. + state (Dict): The state being requested. + id_override (String, optional): A device ID used to override the + passed in device's ID. Used to make changes on sub-devices. + i.e. Outlet in a Powerstrip. The Parent device's ID. + type_override (String, optional): Used to override the device type + when a device inherits from a device other than WinkDevice. + Returns: + response_json (Dict): The API's response in dictionary format """ _LOGGER.info("Setting state via online API") object_id = id_override or device.object_id() @@ -73,6 +83,20 @@ def set_device_state(self, device, state, id_override=None, type_override=None): # pylint: disable=bare-except def local_set_state(self, device, state, id_override=None, type_override=None): + """ + Set device state via local API, and fall back to online API. + + Args: + device (WinkDevice): The device the change is being requested for. + state (Dict): The state being requested. + id_override (String, optional): A device ID used to override the + passed in device's ID. Used to make changes on sub-devices. + i.e. Outlet in a Powerstrip. The Parent device's ID. + type_override (String, optional): Used to override the device type + when a device inherits from a device other than WinkDevice. + Returns: + response_json (Dict): The API's response in dictionary format + """ if ALLOW_LOCAL_CONTROL: if device.local_id() is not None: hub = HUBS.get(device.hub_id()) @@ -106,7 +130,17 @@ def local_set_state(self, device, state, id_override=None, type_override=None): def get_device_state(self, device, id_override=None, type_override=None): """ - :type device: WinkDevice + Get device state via online API. + + Args: + device (WinkDevice): The device the change is being requested for. + id_override (String, optional): A device ID used to override the + passed in device's ID. Used to make changes on sub-devices. + i.e. Outlet in a Powerstrip. The Parent device's ID. + type_override (String, optional): Used to override the device type + when a device inherits from a device other than WinkDevice. + Returns: + response_json (Dict): The API's response in dictionary format """ _LOGGER.info("Getting state via online API") object_id = id_override or device.object_id() @@ -121,7 +155,18 @@ def get_device_state(self, device, id_override=None, type_override=None): # pylint: disable=bare-except def local_get_state(self, device, id_override=None, type_override=None): """ - :type device: WinkDevice + Get device state via local API, and fall back to online API. + + Args: + device (WinkDevice): The device the change is being requested for. + state (Dict): The state being requested. + id_override (String, optional): A device ID used to override the + passed in device's ID. Used to make changes on sub-devices. + i.e. Outlet in a Powerstrip. The Parent device's ID. + type_override (String, optional): Used to override the device type + when a device inherits from a device other than WinkDevice. + Returns: + response_json (Dict): The API's response in dictionary format """ if ALLOW_LOCAL_CONTROL: if device.local_id() is not None: @@ -156,6 +201,57 @@ def local_get_state(self, device, id_override=None, type_override=None): else: return self.get_device_state(device, id_override, type_override) + def update_firmware(self, device, id_override=None, type_override=None): + """ + Make a call to the update_firmware endpoint. As far as I know this + is only valid for Wink hubs. + + Args: + device (WinkDevice): The device the change is being requested for. + id_override (String, optional): A device ID used to override the + passed in device's ID. Used to make changes on sub-devices. + i.e. Outlet in a Powerstrip. The Parent device's ID. + type_override (String, optional): Used to override the device type + when a device inherits from a device other than WinkDevice. + Returns: + response_json (Dict): The API's response in dictionary format + """ + object_id = id_override or device.object_id() + object_type = type_override or device.object_type() + url_string = "{}/{}s/{}/update_firmware".format(self.BASE_URL, + object_type, + object_id) + arequest = requests.post(url_string, + headers=API_HEADERS) + response_json = arequest.json() + return response_json + + def remove_device(self, device, id_override=None, type_override=None): + """ + Remove a device. + + Args: + device (WinkDevice): The device the change is being requested for. + id_override (String, optional): A device ID used to override the + passed in device's ID. Used to make changes on sub-devices. + i.e. Outlet in a Powerstrip. The Parent device's ID. + type_override (String, optional): Used to override the device type + when a device inherits from a device other than WinkDevice. + Returns: + response_json (boolean): True if the device was removed. + """ + object_id = id_override or device.object_id() + object_type = type_override or device.object_type() + url_string = "{}/{}s/{}".format(self.BASE_URL, + object_type, + object_id) + arequest = requests.delete(url_string, + headers=API_HEADERS) + if arequest.status_code == 204: + return True + _LOGGER.error("Failed to remove device. Status code: " + arequest.status_code) + return False + def disable_local_control(): global ALLOW_LOCAL_CONTROL diff --git a/src/pywink/devices/base.py b/src/pywink/devices/base.py index 80eaf32..b055e23 100644 --- a/src/pywink/devices/base.py +++ b/src/pywink/devices/base.py @@ -81,6 +81,9 @@ def device_manufacturer(self): def model_name(self): return self.json_state.get('model_name') + def remove_device(self): + return self.api_interface.remove_device(self) + def _update_state_from_response(self, response_json): """ :param response_json: the json obj returned from query diff --git a/src/pywink/devices/hub.py b/src/pywink/devices/hub.py index 380c24f..c875ce1 100644 --- a/src/pywink/devices/hub.py +++ b/src/pywink/devices/hub.py @@ -2,8 +2,6 @@ from pywink.devices.base import WinkDevice -import requests - _LOGGER = logging.getLogger(__name__) @@ -42,13 +40,7 @@ def pairing_mode(self): return self._last_reading.get('pairing_mode') def update_firmware(self): - url_string = "{}/{}s/{}/update_firmware".format(self.api_interface.BASE_URL, - self.object_type, - self.object_id) - arequest = requests.post(url_string, - headers=self.api_interface.API_HEADERS) - response_json = arequest.json() - return response_json + return self.api_interface.update_firmware(self) def pair_new_device(self, pairing_mode, pairing_mode_duration=60, pairing_device_type_selector=None, kidde_radio_code=None): diff --git a/src/setup.py b/src/setup.py index b3ae235..4a18ed2 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,10 +1,10 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='1.5.1', + version='1.5.2', description='Access Wink devices via the Wink API', url='http://github.com/python-wink/python-wink', - author='Brad Johnson', + author='Brad Johnson, William Scanlon', license='MIT', install_requires=['requests>=2.0'], tests_require=['mock'], From 124afd0f89123302f95b8823a2669d2490fdbab5 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Sat, 9 Sep 2017 11:51:29 -0400 Subject: [PATCH 155/178] Support for water heaters and new lock key codes. (#93) --- CHANGELOG.md | 3 + src/pywink/__init__.py | 2 +- src/pywink/api.py | 32 +++++- src/pywink/devices/factory.py | 3 + src/pywink/devices/lock.py | 5 + src/pywink/devices/types.py | 3 +- src/pywink/devices/water_heater.py | 71 ++++++++++++ src/pywink/test/api_test.py | 21 +++- .../test/devices/air_conditioner_test.py | 10 +- .../devices/api_responses/water_heater.json | 101 ++++++++++++++++++ src/pywink/test/devices/base_test.py | 3 +- src/pywink/test/devices/water_heater_test.py | 96 +++++++++++++++++ src/setup.py | 2 +- 13 files changed, 341 insertions(+), 11 deletions(-) create mode 100644 src/pywink/devices/water_heater.py create mode 100644 src/pywink/test/devices/api_responses/water_heater.json create mode 100644 src/pywink/test/devices/water_heater_test.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 61ad3cf..703b7f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 1.6.0 +- Support for water heaters. Added support for adding new lock key codes. + ## 1.5.2 - Fixed firmware update and added device deletion diff --git a/src/pywink/__init__.py b/src/pywink/__init__.py index 283e15c..21f5c34 100644 --- a/src/pywink/__init__.py +++ b/src/pywink/__init__.py @@ -12,7 +12,7 @@ get_powerstrips, get_shades, get_sirens, \ get_switches, get_thermostats, get_fans, get_air_conditioners, \ get_propane_tanks, get_robots, get_scenes, get_light_groups, \ - get_binary_switch_groups + get_binary_switch_groups, get_water_heaters from pywink.api import get_all_devices, get_eggtrays, get_sensors, \ get_keys, get_piggy_banks, get_smoke_and_co_detectors, \ diff --git a/src/pywink/api.py b/src/pywink/api.py index 140479e..daea843 100644 --- a/src/pywink/api.py +++ b/src/pywink/api.py @@ -238,7 +238,7 @@ def remove_device(self, device, id_override=None, type_override=None): type_override (String, optional): Used to override the device type when a device inherits from a device other than WinkDevice. Returns: - response_json (boolean): True if the device was removed. + (boolean): True if the device was removed. """ object_id = id_override or device.object_id() object_type = type_override or device.object_type() @@ -252,6 +252,32 @@ def remove_device(self, device, id_override=None, type_override=None): _LOGGER.error("Failed to remove device. Status code: " + arequest.status_code) return False + def create_lock_key(self, device, new_device_json, id_override=None, type_override=None): + """ + Create a new lock key code. + + Args: + device (WinkDevice): The device the change is being requested for. + new_device_json (String): The JSON string required to create the device. + id_override (String, optional): A device ID used to override the + passed in device's ID. Used to make changes on sub-devices. + i.e. Outlet in a Powerstrip. The Parent device's ID. + type_override (String, optional): Used to override the device type + when a device inherits from a device other than WinkDevice. + Returns: + response_json (Dict): The API's response in dictionary format + """ + object_id = id_override or device.object_id() + object_type = type_override or device.object_type() + url_string = "{}/{}s/{}/keys".format(self.BASE_URL, + object_type, + object_id) + arequest = requests.post(url_string, + data=json.dumps(new_device_json), + headers=API_HEADERS) + response_json = arequest.json() + return response_json + def disable_local_control(): global ALLOW_LOCAL_CONTROL @@ -513,6 +539,10 @@ def get_scenes(): return get_devices(device_types.SCENE, "scenes") +def get_water_heaters(): + return get_devices(device_types.WATER_HEATER) + + def get_light_groups(): light_groups = [] for group in get_devices(device_types.GROUP, "groups"): diff --git a/src/pywink/devices/factory.py b/src/pywink/devices/factory.py index 402e115..f91c06b 100644 --- a/src/pywink/devices/factory.py +++ b/src/pywink/devices/factory.py @@ -29,6 +29,7 @@ from pywink.devices.scene import WinkScene from pywink.devices.light_group import WinkLightGroup from pywink.devices.binary_switch_group import WinkBinarySwitchGroup +from pywink.devices.water_heater import WinkWaterHeater # pylint: disable=too-many-branches, too-many-statements @@ -109,6 +110,8 @@ def build_device(device_state_as_json, api_interface): # This is a group of lights else: new_objects.append(WinkLightGroup(device_state_as_json, api_interface)) + elif object_type == device_types.WATER_HEATER: + new_objects.append(WinkWaterHeater(device_state_as_json, api_interface)) return new_objects diff --git a/src/pywink/devices/lock.py b/src/pywink/devices/lock.py index 9b10050..e8e5f06 100644 --- a/src/pywink/devices/lock.py +++ b/src/pywink/devices/lock.py @@ -86,3 +86,8 @@ def update_state(self): """ Update state with latest info from Wink API. """ response = self.api_interface.local_get_state(self) return self._update_state_from_response(response) + + def add_new_key(self, code, name): + """Add a new user key code.""" + device_json = {"code": code, "name": name} + return self.api_interface.create_lock_key(self, device_json) diff --git a/src/pywink/devices/types.py b/src/pywink/devices/types.py index c47d71d..d659e68 100644 --- a/src/pywink/devices/types.py +++ b/src/pywink/devices/types.py @@ -28,9 +28,10 @@ ROBOT = 'robot' SCENE = 'scene' GROUP = 'group' +WATER_HEATER = 'water_heater' ALL_SUPPORTED_DEVICES = [LIGHT_BULB, BINARY_SWITCH, SENSOR_POD, LOCK, EGGTRAY, GARAGE_DOOR, POWERSTRIP, SHADE, SIREN, KEY, PIGGY_BANK, SMOKE_DETECTOR, THERMOSTAT, HUB, FAN, DOOR_BELL, REMOTE, SPRINKLER, BUTTON, GANG, CAMERA, AIR_CONDITIONER, - PROPANE_TANK, ROBOT, SCENE, GROUP] + PROPANE_TANK, ROBOT, SCENE, GROUP, WATER_HEATER] diff --git a/src/pywink/devices/water_heater.py b/src/pywink/devices/water_heater.py new file mode 100644 index 0000000..02d57b2 --- /dev/null +++ b/src/pywink/devices/water_heater.py @@ -0,0 +1,71 @@ +from pywink.devices.base import WinkDevice + + +# pylint: disable=too-many-public-methods +class WinkWaterHeater(WinkDevice): + """ + Represents a Wink water heater. + """ + + def state(self): + return self.current_mode() + + def modes(self): + return self._last_reading.get('modes_allowed') + + def current_mode(self): + return self._last_reading.get('mode') + + def current_set_point(self): + return self._last_reading.get('set_point') + + def max_set_point(self): + return self._last_reading.get('max_set_point_allowed') + + def min_set_point(self): + return self._last_reading.get('min_set_point_allowed') + + def is_on(self): + return self._last_reading.get('powered', False) + + def vacation_mode_enabled(self): + return self._last_reading.get('vacation_mode', False) + + def rheem_type(self): + return self._last_reading.get('rheem_type') + + def set_operation_mode(self, mode): + """ + :param mode: a string one of self.modes() + :return: nothing + """ + if mode == "off": + desired_state = {"powered": False} + else: + desired_state = {"powered": True, "mode": mode} + + response = self.api_interface.set_device_state(self, { + "desired_state": desired_state + }) + + self._update_state_from_response(response) + + def set_temperature(self, set_point): + """ + :param temperature: a float for the temperature value in celsius + :return: nothing + """ + response = self.api_interface.set_device_state(self, { + "desired_state": {'set_point': set_point} + }) + + self._update_state_from_response(response) + + def set_vacation_mode(self, state): + """ + :param state: a boolean of ture (on) or false ('off') + :return: nothing + """ + values = {"desired_state": {"vacation_mode": state}} + response = self.api_interface.local_set_state(self, values) + self._update_state_from_response(response) diff --git a/src/pywink/test/api_test.py b/src/pywink/test/api_test.py index e381336..9426316 100644 --- a/src/pywink/test/api_test.py +++ b/src/pywink/test/api_test.py @@ -38,6 +38,7 @@ from pywink.devices.propane_tank import WinkPropaneTank from pywink.devices.scene import WinkScene from pywink.devices.robot import WinkRobot +from pywink.devices.water_heater import WinkWaterHeater USERS_ME_WINK_DEVICES = {} GROUPS = {} @@ -114,7 +115,7 @@ def test_get_subscription_key(self): def test_get_all_devices_from_api(self): WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) devices = get_all_devices() - self.assertEqual(len(devices), 66) + self.assertEqual(len(devices), 67) lights = get_light_bulbs() for light in lights: self.assertTrue(isinstance(light, WinkLightBulb)) @@ -176,6 +177,9 @@ def test_get_all_devices_from_api(self): propane_tanks = get_propane_tanks() for tank in propane_tanks: self.assertTrue(isinstance(tank, WinkPropaneTank)) + water_heaters = get_water_heaters() + for water_heater in water_heaters: + self.assertTrue(isinstance(water_heater, WinkWaterHeater)) def test_get_sensor_and_binary_switch_updated_states_from_api(self): WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) @@ -443,6 +447,21 @@ def test_get_thermostat_updated_states_from_api(self): self.assertEqual(10, device.current_min_set_point()) self.assertEqual(50, device.current_max_set_point()) + def test_get_water_heater_updated_states_from_api(self): + WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) + devices = get_water_heaters() + old_states = {} + for device in devices: + device.api_interface = self.api_interface + old_states[device.object_id()] = device.state() + device.set_operation_mode("heat_pump") + device.set_temperature(70) + device.set_vacation_mode(True) + for device in devices: + self.assertEqual(device.state(), "heat_pump") + self.assertEqual(70, device.current_set_point()) + self.assertTrue(device.vacation_mode_enabled()) + def test_get_camera_updated_states_from_api(self): WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) devices = get_cameras() diff --git a/src/pywink/test/devices/air_conditioner_test.py b/src/pywink/test/devices/air_conditioner_test.py index cf1e1dc..d5be48f 100644 --- a/src/pywink/test/devices/air_conditioner_test.py +++ b/src/pywink/test/devices/air_conditioner_test.py @@ -9,10 +9,10 @@ from pywink.devices.air_conditioner import WinkAirConditioner -class FanTests(unittest.TestCase): +class AirConditionerTests(unittest.TestCase): def setUp(self): - super(FanTests, self).setUp() + super(AirConditionerTests, self).setUp() self.api_interface = mock.MagicMock() def test_ac_state(self): @@ -35,7 +35,7 @@ def test_ac_modes(self): ac = get_devices_from_response_dict(response_dict, device_types.AIR_CONDITIONER)[0] self.assertEqual(ac.modes(), ["auto_eco", "cool_only", "fan_only"]) - def test_thermostat_current_fan_speed(self): + def test_ac_current_fan_speed(self): device_list = [] response_dict = {} _json_file = open('{}/api_responses/quirky_aros.json'.format(os.path.dirname(__file__))) @@ -55,7 +55,7 @@ def test_ac_current_temperature(self): ac = get_devices_from_response_dict(response_dict, device_types.AIR_CONDITIONER)[0] self.assertEqual(ac.current_temperature(), 17.777777777777779) - def test_thermostat_max_set_point(self): + def test_ac_max_set_point(self): device_list = [] response_dict = {} _json_file = open('{}/api_responses/quirky_aros.json'.format(os.path.dirname(__file__))) @@ -65,7 +65,7 @@ def test_thermostat_max_set_point(self): ac = get_devices_from_response_dict(response_dict, device_types.AIR_CONDITIONER)[0] self.assertEqual(ac.current_max_set_point(), 20.0) - def test_thermostat_is_on(self): + def test_ac_is_on(self): device_list = [] response_dict = {} _json_file = open('{}/api_responses/quirky_aros.json'.format(os.path.dirname(__file__))) diff --git a/src/pywink/test/devices/api_responses/water_heater.json b/src/pywink/test/devices/api_responses/water_heater.json new file mode 100644 index 0000000..f247dc2 --- /dev/null +++ b/src/pywink/test/devices/api_responses/water_heater.json @@ -0,0 +1,101 @@ +{ + "object_type": "water_heater", + "object_id": "2729", + "uuid": "REMOVED", + "icon_id": null, + "icon_code": null, + "desired_state": { + "set_point": 48.888888888888886, + "mode": "eco", + "powered": true, + "vacation_mode": false + }, + "last_reading": { + "units": { + "temperature": "f" + }, + "units_updated_at": 1504666537.7042785, + "connection": true, + "connection_updated_at": 1504666537.7042785, + "set_point": 48.888888888888886, + "set_point_updated_at": 1504666537.7042785, + "mode": "eco", + "mode_updated_at": 1504666537.7042785, + "powered": true, + "powered_updated_at": 1504666537.7042785, + "vacation_mode": false, + "vacation_mode_updated_at": 1504666537.7042785, + "max_set_point_allowed": 60.0, + "max_set_point_allowed_updated_at": 1504666537.7042785, + "min_set_point_allowed": 43.333333333333336, + "min_set_point_allowed_updated_at": 1504666537.7042785, + "scald_message": "CAUTION. HOT WATER. Contact may cause serious burns to skin.", + "scald_message_updated_at": 1504666537.7042785, + "rheem_type": "Heat Pump Water Heater", + "rheem_type_updated_at": 1504666537.7042785, + "modes_allowed": [ + "eco", + "heat_pump", + "high_demand", + "electric_only" + ], + "modes_allowed_updated_at": 1504666537.7042785, + "alarm_message": null, + "alarm_message_updated_at": null, + "desired_set_point": null, + "desired_set_point_updated_at": null, + "desired_mode": null, + "desired_mode_updated_at": null, + "desired_powered": null, + "desired_powered_updated_at": null, + "desired_vacation_mode": null, + "desired_vacation_mode_updated_at": null, + "connection_changed_at": 1504666416.2879331, + "mode_changed_at": 1504666416.634584, + "set_point_changed_at": 1504666416.634584, + "powered_changed_at": 1504666416.634584, + "vacation_mode_changed_at": 1504666416.634584, + "max_set_point_allowed_changed_at": 1504666416.634584, + "min_set_point_allowed_changed_at": 1504666416.634584, + "units_changed_at": 1504666416.634584, + "scald_message_changed_at": 1504666416.634584, + "modes_allowed_changed_at": 1504666416.634584, + "rheem_type_changed_at": 1504666416.634584 + }, + "subscription": { + "pubnub": { + "subscribe_key": "REMOVED", + "channel": "REMOVED" + } + }, + "water_heater_id": "2729", + "name": "Garage Gen 4", + "locale": "en_us", + "units": {}, + "created_at": 1504666416, + "hidden_at": null, + "capabilities": { + "fields": [ + { + "type": "nested_hash", + "field": "units", + "mutability": "read-only" + } + ], + "excludes_naming": true, + "excludes_robot_cause": true + }, + "manufacturer_device_model": "Water Heater Heat Pump Water Heater", + "manufacturer_device_id": "51688", + "device_manufacturer": "rheem", + "model_name": "Water Heater", + "upc_id": "143", + "upc_code": "020352614724", + "primary_upc_code": "rheem_water_heater", + "linked_service_id": "793030", + "lat_lng": [ + null, + null + ], + "location": "" +} \ No newline at end of file diff --git a/src/pywink/test/devices/base_test.py b/src/pywink/test/devices/base_test.py index 765014d..ce1a8e3 100644 --- a/src/pywink/test/devices/base_test.py +++ b/src/pywink/test/devices/base_test.py @@ -26,6 +26,7 @@ from pywink.devices.propane_tank import WinkPropaneTank from pywink.devices.scene import WinkScene from pywink.devices.robot import WinkRobot +from pywink.devices.water_heater import WinkWaterHeater class BaseTests(unittest.TestCase): @@ -84,7 +85,7 @@ def test_all_devices_battery_is_valid(self): skip_types = [WinkFan, WinkPorkfolioBalanceSensor, WinkPorkfolioNose, WinkBinarySwitch, WinkHub, WinkLightBulb, WinkThermostat, WinkKey, WinkPowerStrip, WinkPowerStripOutlet, WinkRemote, WinkShade, WinkSprinkler, WinkButton, WinkGang, WinkCanaryCamera, - WinkAirConditioner, WinkScene, WinkRobot] + WinkAirConditioner, WinkScene, WinkRobot, WinkWaterHeater] for device in devices: if device.manufacturer_device_model() == "leaksmart_valve": self.assertIsNotNone(device.battery_level()) diff --git a/src/pywink/test/devices/water_heater_test.py b/src/pywink/test/devices/water_heater_test.py new file mode 100644 index 0000000..14bcbe0 --- /dev/null +++ b/src/pywink/test/devices/water_heater_test.py @@ -0,0 +1,96 @@ +import json +import os +import unittest + +import mock + +from pywink.api import get_devices_from_response_dict, WinkApiInterface +from pywink.devices import types as device_types +from pywink.devices.water_heater import WinkWaterHeater + + +class WaterHeaterTests(unittest.TestCase): + + def setUp(self): + super(WaterHeaterTests, self).setUp() + self.api_interface = mock.MagicMock() + + def test_current_state(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/water_heater.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + water_heater = get_devices_from_response_dict(response_dict, device_types.WATER_HEATER)[0] + self.assertEqual(water_heater.state(), "eco") + + def test_current_set_point(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/water_heater.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + water_heater = get_devices_from_response_dict(response_dict, device_types.WATER_HEATER)[0] + self.assertEqual(water_heater.current_set_point(), 48.888888888888886) + + def test_water_heater_modes(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/water_heater.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + water_heater = get_devices_from_response_dict(response_dict, device_types.WATER_HEATER)[0] + self.assertEqual(water_heater.modes(), ["eco", "heat_pump", "high_demand", "electric_only"]) + + def test_water_heater_max_set_point(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/water_heater.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + water_heater = get_devices_from_response_dict(response_dict, device_types.WATER_HEATER)[0] + self.assertEqual(water_heater.max_set_point(), 60.0) + + def test_water_heater_min_set_point(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/water_heater.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + water_heater = get_devices_from_response_dict(response_dict, device_types.WATER_HEATER)[0] + self.assertEqual(water_heater.min_set_point(), 43.333333333333336) + + def test_water_heater_is_on(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/water_heater.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + water_heater = get_devices_from_response_dict(response_dict, device_types.WATER_HEATER)[0] + self.assertTrue(water_heater.is_on()) + + def test_vacation_mode_not_enabled(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/water_heater.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + water_heater = get_devices_from_response_dict(response_dict, device_types.WATER_HEATER)[0] + self.assertFalse(water_heater.vacation_mode_enabled()) + + def test_rheem_type(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/water_heater.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + water_heater = get_devices_from_response_dict(response_dict, device_types.WATER_HEATER)[0] + self.assertEqual(water_heater.rheem_type(), "Heat Pump Water Heater") diff --git a/src/setup.py b/src/setup.py index 4a18ed2..dc2a2b0 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='1.5.2', + version='1.6.0', description='Access Wink devices via the Wink API', url='http://github.com/python-wink/python-wink', author='Brad Johnson, William Scanlon', From b87ddb89109d5b08ad3c039e10ce71677f85b9a6 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Tue, 26 Sep 2017 22:25:15 -0400 Subject: [PATCH 156/178] Support for Dome siren/chime (#95) --- CHANGELOG.md | 4 + src/pywink/devices/hub.py | 5 +- src/pywink/devices/siren.py | 102 ++++++- src/pywink/devices/thermostat.py | 29 +- src/pywink/test/api_test.py | 15 +- .../devices/api_responses/dome_siren.json | 282 ++++++++++++++++++ src/pywink/test/devices/hub_test.py | 6 +- src/pywink/test/devices/siren_test.py | 35 +++ src/pywink/test/devices/thermostat_test.py | 30 +- src/setup.py | 2 +- 10 files changed, 499 insertions(+), 11 deletions(-) create mode 100644 src/pywink/test/devices/api_responses/dome_siren.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 703b7f3..4633642 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Change Log +## 1.7.0 +- Thermostat fixes for away mode. +- Support for Dome Siren/Chime + ## 1.6.0 - Support for water heaters. Added support for adding new lock key codes. diff --git a/src/pywink/devices/hub.py b/src/pywink/devices/hub.py index c875ce1..e4ff986 100644 --- a/src/pywink/devices/hub.py +++ b/src/pywink/devices/hub.py @@ -22,7 +22,10 @@ def state(self): def kidde_radio_code(self): config = self.json_state.get('configuration') - return config.get('kidde_radio_code') + # Only Wink hub v1 and v2 support kidde + if config is not None: + return config.get('kidde_radio_code') + return config def update_needed(self): return self._last_reading.get('update_needed') diff --git a/src/pywink/devices/siren.py b/src/pywink/devices/siren.py index 664ea1a..496a11e 100644 --- a/src/pywink/devices/siren.py +++ b/src/pywink/devices/siren.py @@ -12,9 +12,54 @@ def state(self): def mode(self): return self._last_reading.get('mode', None) + def siren_volume(self): + return self._last_reading.get('siren_volume', None) + + def chime_volume(self): + return self._last_reading.get('chime_volume', None) + def auto_shutoff(self): return self._last_reading.get('auto_shutoff', None) + def strobe_enabled(self): + return self._last_reading.get('strobe_enabled', None) + + def chime_strobe_enabled(self): + return self._last_reading.get('chime_strobe_enabled', None) + + def siren_sound(self): + return self._last_reading.get('siren_sound', None) + + def chime_mode(self): + return self._last_reading.get('activate_chime', None) + + def chime_cycles(self): + return self._last_reading.get('chime_cycles', None) + + def set_siren_volume(self, volume): + """ + :param volume: one of [low, medium, high] + """ + values = { + "desired_state": { + "siren_volume": volume + } + } + response = self.api_interface.set_device_state(self, values) + self._update_state_from_response(response) + + def set_chime_volume(self, volume): + """ + :param volume: one of [low, medium, high] + """ + values = { + "desired_state": { + "chime_volume": volume + } + } + response = self.api_interface.set_device_state(self, values) + self._update_state_from_response(response) + def set_mode(self, mode): """ :param mode: a str, one of [siren_only, strobe_only, siren_and_strobe] @@ -28,9 +73,64 @@ def set_mode(self, mode): response = self.api_interface.set_device_state(self, values) self._update_state_from_response(response) + def set_siren_strobe_enabled(self, enabled): + """ + :param enabled: True or False + :return: nothing + """ + values = { + "desired_state": { + "strobe_enabled": enabled + } + } + response = self.api_interface.set_device_state(self, values) + self._update_state_from_response(response) + + def set_chime_strobe_enabled(self, enabled): + """ + :param enabled: True or False + :return: nothing + """ + values = { + "desired_state": { + "chime_strobe_enabled": enabled + } + } + response = self.api_interface.set_device_state(self, values) + self._update_state_from_response(response) + + def set_siren_sound(self, sound): + """ + :param sound: a str, one of ["doorbell", "fur_elise", "doorbell_extended", "alert", + "william_tell", "rondo_alla_turca", "police_siren", + ""evacuation", "beep_beep", "beep"] + :return: nothing + """ + values = { + "desired_state": { + "siren_sound": sound + } + } + response = self.api_interface.set_device_state(self, values) + self._update_state_from_response(response) + + def set_chime(self, sound, cycles=None): + """ + :param sound: a str, one of ["doorbell", "fur_elise", "doorbell_extended", "alert", + "william_tell", "rondo_alla_turca", "police_siren", + ""evacuation", "beep_beep", "beep", "inactive"] + :return: nothing + """ + desired_state = {"activate_chime": sound} + if cycles is not None: + desired_state.update({"chime_cycles": cycles}) + response = self.api_interface.set_device_state(self, + {"desired_state": desired_state}) + self._update_state_from_response(response) + def set_auto_shutoff(self, timer): """ - :param time: an int, one of [None (never), 30, 60, 120] + :param time: an int, one of [None (never), -1, 30, 60, 120] :return: nothing """ values = { diff --git a/src/pywink/devices/thermostat.py b/src/pywink/devices/thermostat.py index c2bc0a4..69bde1a 100644 --- a/src/pywink/devices/thermostat.py +++ b/src/pywink/devices/thermostat.py @@ -31,7 +31,20 @@ def hvac_modes(self): return hvac_modes def away(self): - return self._last_reading.get('users_away', False) + """ + This function handles both ecobee and nest thermostats + which use a different field for away/home status. + """ + nest = self._last_reading.get('users_away', None) + ecobee = self.profile() + if nest is None and ecobee is None: + return None + elif nest is not None: + return nest + elif ecobee is not None: + if ecobee == "home": + return False + return True def current_hvac_mode(self): return self._last_reading.get('mode', None) @@ -90,6 +103,9 @@ def eco_target(self): def occupied(self): return self._last_reading.get('occupied', None) + def profile(self): + return self._last_reading.get('profile') + def deadband(self): return self._last_reading.get('deadband', None) @@ -125,8 +141,17 @@ def set_away(self, away=True): """ :param away: a boolean of true (away) or false ('home') :return nothing + + This function handles both ecobee and nest thermostats + which use a different field for away/home status. """ - desired_state = {"users_away": away} + if self.profile() is not None: + if away: + desired_state = {"profile": "away"} + else: + desired_state = {"profile": "home"} + else: + desired_state = {"users_away": away} response = self.api_interface.set_device_state(self, { "desired_state": desired_state diff --git a/src/pywink/test/api_test.py b/src/pywink/test/api_test.py index 9426316..e0c8c96 100644 --- a/src/pywink/test/api_test.py +++ b/src/pywink/test/api_test.py @@ -115,7 +115,7 @@ def test_get_subscription_key(self): def test_get_all_devices_from_api(self): WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) devices = get_all_devices() - self.assertEqual(len(devices), 67) + self.assertEqual(len(devices), 68) lights = get_light_bulbs() for light in lights: self.assertTrue(isinstance(light, WinkLightBulb)) @@ -379,10 +379,23 @@ def test_get_siren_updated_states_from_api(self): device.set_state(not device.state()) device.set_mode("strobe") device.set_auto_shutoff(120) + device.set_siren_volume("medium") + device.set_chime_volume("medium") + device.set_siren_sound("test_sound") + device.set_chime("test_sound", 10) + device.set_chime_strobe_enabled(True) + device.set_siren_strobe_enabled(False) device.update_state() self.assertEqual(not device.state(), old_states.get(device.object_id())) self.assertEqual(device.mode(), "strobe") self.assertEqual(device.auto_shutoff(), 120) + self.assertEqual(device.siren_volume(), "medium") + self.assertEqual(device.chime_volume(), "medium") + self.assertEqual(device.chime_mode(), "test_sound") + self.assertEqual(device.siren_sound(), "test_sound") + self.assertTrue(device.chime_strobe_enabled()) + self.assertFalse(device.strobe_enabled()) + self.assertEqual(device.chime_cycles(), 10) def test_get_lock_updated_states_from_api(self): WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) diff --git a/src/pywink/test/devices/api_responses/dome_siren.json b/src/pywink/test/devices/api_responses/dome_siren.json new file mode 100644 index 0000000..56ef3e2 --- /dev/null +++ b/src/pywink/test/devices/api_responses/dome_siren.json @@ -0,0 +1,282 @@ +{ + "object_type":"siren", + "object_id":"21666", + "uuid":"ae9dfb54-de0a-44b7-91ef-123456", + "icon_id":null, + "icon_code":null, + "desired_state":{ + "powered":false, + "siren_volume":"high", + "auto_shutoff":60, + "chime_cycles":null, + "chime_volume":"low", + "strobe_enabled":true, + "chime_strobe_enabled":false, + "siren_sound":"beep", + "activate_chime":"inactive", + "pending_action":null + }, + "last_reading":{ + "connection":true, + "connection_updated_at":1505943789.5072866, + "powered":false, + "powered_updated_at":1505943789.5072866, + "battery":1.0, + "battery_updated_at":1505943789.5072866, + "siren_volume":"high", + "siren_volume_updated_at":1505943789.5072866, + "auto_shutoff":60, + "auto_shutoff_updated_at":1505943789.5072866, + "chime_cycles":null, + "chime_cycles_updated_at":null, + "chime_volume":"low", + "chime_volume_updated_at":1505943789.5072866, + "strobe_enabled":true, + "strobe_enabled_updated_at":1505943789.5072866, + "chime_strobe_enabled":false, + "chime_strobe_enabled_updated_at":1505943789.5072866, + "siren_sound":"beep", + "siren_sound_updated_at":1505943789.5072866, + "activate_chime":"inactive", + "activate_chime_updated_at":1505943789.5072866, + "pending_action":null, + "pending_action_updated_at":null, + "desired_powered":null, + "desired_powered_updated_at":null, + "desired_siren_volume":null, + "desired_siren_volume_updated_at":null, + "desired_auto_shutoff":null, + "desired_auto_shutoff_updated_at":null, + "desired_chime_cycles":null, + "desired_chime_cycles_updated_at":null, + "desired_chime_volume":null, + "desired_chime_volume_updated_at":null, + "desired_strobe_enabled":null, + "desired_strobe_enabled_updated_at":null, + "desired_chime_strobe_enabled":null, + "desired_chime_strobe_enabled_updated_at":null, + "desired_siren_sound":null, + "desired_siren_sound_updated_at":null, + "desired_activate_chime":null, + "desired_activate_chime_updated_at":null, + "desired_pending_action":null, + "desired_pending_action_updated_at":null, + "connection_changed_at":1505943782.4097323, + "powered_changed_at":1505943782.4097323, + "battery_changed_at":1505943785.9981859, + "activate_chime_changed_at":1505943782.4097323, + "siren_volume_changed_at":1505943785.9981859, + "auto_shutoff_changed_at":1505943785.9981859, + "chime_volume_changed_at":1505943786.4888902, + "siren_sound_changed_at":1505943789.5072866, + "strobe_enabled_changed_at":1505943789.5072866, + "chime_strobe_enabled_changed_at":1505943789.5072866 + }, + "subscription":{ + "pubnub":{ + "subscribe_key":"sub-c-f7be8-02e", + "channel":"800eb0634011b2" + } + }, + "siren_id":"21666", + "name":"Siren & Strobe", + "locale":"en_us", + "units":{ + + }, + "created_at":1505943780, + "hidden_at":null, + "capabilities":{ + "fields":[ + { + "type":"boolean", + "field":"connection", + "mutability":"read-only" + }, + { + "type":"boolean", + "field":"powered", + "mutability":"read-write", + "attribute_id":2 + }, + { + "type":"percentage", + "field":"battery", + "mutability":"read-only", + "attribute_id":15 + }, + { + "type":"selection", + "field":"siren_volume", + "choices":[ + "high", + "medium", + "low" + ], + "mutability":"read-write", + "attribute_id":101, + "attribute_value_mapping":{ + "low":"1", + "high":"3", + "medium":"2" + } + }, + { + "type":"selection", + "field":"auto_shutoff", + "choices":[ + -1, + 30, + 60, + 300 + ], + "mutability":"read-write", + "attribute_id":102, + "attribute_value_mapping":{ + "30":"1", + "60":"2", + "300":"3", + "-1":"255" + } + }, + { + "type":"integer", + "field":"chime_cycles", + "range":[ + 1, + 255 + ], + "mutability":"read-write", + "attribute_id":103 + }, + { + "type":"selection", + "field":"chime_volume", + "choices":[ + "high", + "medium", + "low" + ], + "mutability":"read-write", + "attribute_id":104, + "attribute_value_mapping":{ + "low":"1", + "high":"3", + "medium":"2" + } + }, + { + "type":"boolean", + "field":"strobe_enabled", + "mutability":"read-write", + "attribute_id":108, + "attribute_value_mapping":{ + "true":"1", + "false":"0" + } + }, + { + "type":"boolean", + "field":"chime_strobe_enabled", + "mutability":"read-write", + "attribute_id":109, + "attribute_value_mapping":{ + "true":"1", + "false":"0" + } + }, + { + "type":"selection", + "field":"siren_sound", + "choices":[ + "doorbell", + "fur_elise", + "doorbell_extended", + "alert", + "william_tell", + "rondo_alla_turca", + "police_siren", + "evacuation", + "beep_beep", + "beep" + ], + "mutability":"read-write", + "attribute_id":105, + "attribute_value_mapping":{ + "beep":"10", + "alert":"4", + "doorbell":"1", + "beep_beep":"9", + "fur_elise":"2", + "evacuation":"8", + "police_siren":"7", + "william_tell":"5", + "rondo_alla_turca":"6", + "doorbell_extended":"3" + } + }, + { + "type":"selection", + "field":"activate_chime", + "choices":[ + "doorbell", + "fur_elise", + "doorbell_extended", + "alert", + "william_tell", + "rondo_alla_turca", + "police_siren", + "evacuation", + "beep_beep", + "beep", + "inactive" + ], + "mutability":"read-write", + "attribute_id":110, + "attribute_value_mapping":{ + "beep":"10", + "alert":"4", + "doorbell":"1", + "inactive":"0", + "beep_beep":"9", + "fur_elise":"2", + "evacuation":"8", + "police_siren":"7", + "william_tell":"5", + "rondo_alla_turca":"6", + "doorbell_extended":"3" + } + }, + { + "type":"selection", + "field":"pending_action", + "choices":[ + "none", + "lookout_bundle_setup" + ], + "mutability":"read-write" + } + ], + "is_sleepy":true, + "polling_interval":43200, + "notification_robots":[ + "offline_notification", + "low_battery_notification" + ], + "excludes_robot_cause":true, + "home_security_device":true + }, + "device_manufacturer":"dome", + "model_name":"Siren", + "upc_id":"907", + "upc_code":"dome_siren", + "primary_upc_code":"dome_siren", + "hub_id":"249327", + "local_id":"32", + "radio_type":"zwave", + "lat_lng":[ + null, + null + ], + "location":"" +} \ No newline at end of file diff --git a/src/pywink/test/devices/hub_test.py b/src/pywink/test/devices/hub_test.py index d0d7aa1..e7f4ee0 100644 --- a/src/pywink/test/devices/hub_test.py +++ b/src/pywink/test/devices/hub_test.py @@ -33,9 +33,9 @@ def test_kidde_radio_code_should_not_be_none(self): devices = get_devices_from_response_dict(self.response_dict, device_types.HUB) for device in devices: if device.manufacturer_device_model() == "wink_project_one": - continue - if device.manufacturer_device_model() == "philips": - continue + self.assertIsNone(device.kidde_radio_code()) + elif device.manufacturer_device_model() == "philips": + self.assertIsNone(device.kidde_radio_code()) else: self.assertIsNotNone(device.kidde_radio_code()) diff --git a/src/pywink/test/devices/siren_test.py b/src/pywink/test/devices/siren_test.py index 65dc947..4b16ed0 100644 --- a/src/pywink/test/devices/siren_test.py +++ b/src/pywink/test/devices/siren_test.py @@ -18,6 +18,9 @@ def setUp(self): _json_file = open('{}/api_responses/go_control_siren.json'.format(os.path.dirname(__file__))) device_list.append(json.load(_json_file)) _json_file.close() + _json_file = open('{}/api_responses/dome_siren.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() self.response_dict["data"] = device_list def test_siren_state(self): @@ -31,3 +34,35 @@ def test_siren_mode(self): def test_siren_auto_shutoff(self): siren = get_devices_from_response_dict(self.response_dict, device_types.SIREN)[0] self.assertEqual(siren.auto_shutoff(), 60) + + def test_siren_mode_is_none_for_dome_siren(self): + dome_siren = get_devices_from_response_dict(self.response_dict, device_types.SIREN)[1] + self.assertIsNone(dome_siren.mode()) + + def test_siren_volume_for_dome_siren(self): + dome_siren = get_devices_from_response_dict(self.response_dict, device_types.SIREN)[1] + self.assertEqual(dome_siren.siren_volume(), "high") + + def test_chime_volume_for_dome_siren(self): + dome_siren = get_devices_from_response_dict(self.response_dict, device_types.SIREN)[1] + self.assertEqual(dome_siren.chime_volume(), "low") + + def test_dome_siren_strobe_enabled(self): + dome_siren = get_devices_from_response_dict(self.response_dict, device_types.SIREN)[1] + self.assertTrue(dome_siren.strobe_enabled()) + + def test_dome_siren_chime_strobe_enabled(self): + dome_siren = get_devices_from_response_dict(self.response_dict, device_types.SIREN)[1] + self.assertFalse(dome_siren.chime_strobe_enabled()) + + def test_dome_siren_sound(self): + dome_siren = get_devices_from_response_dict(self.response_dict, device_types.SIREN)[1] + self.assertEqual(dome_siren.siren_sound(), "beep") + + def test_dome_siren_chime_mode(self): + dome_siren = get_devices_from_response_dict(self.response_dict, device_types.SIREN)[1] + self.assertEqual(dome_siren.chime_mode(), "inactive") + + def test_dome_siren_chime_cycles(self): + dome_siren = get_devices_from_response_dict(self.response_dict, device_types.SIREN)[1] + self.assertIsNone(dome_siren.chime_cycles()) diff --git a/src/pywink/test/devices/thermostat_test.py b/src/pywink/test/devices/thermostat_test.py index 9bcb0be..6f01744 100644 --- a/src/pywink/test/devices/thermostat_test.py +++ b/src/pywink/test/devices/thermostat_test.py @@ -9,10 +9,10 @@ from pywink.devices.thermostat import WinkThermostat -class FanTests(unittest.TestCase): +class ThermostatTests(unittest.TestCase): def setUp(self): - super(FanTests, self).setUp() + super(ThermostatTests, self).setUp() self.api_interface = mock.MagicMock() def test_thermostat_state(self): @@ -51,6 +51,32 @@ def test_thermostat_users_away(self): _json_file = open('{}/api_responses/nest.json'.format(os.path.dirname(__file__))) device_list.append(json.load(_json_file)) _json_file.close() + _json_file = open('{}/api_responses/go_control_thermostat.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + thermostat = get_devices_from_response_dict(response_dict, device_types.THERMOSTAT)[0] + thermostat2 = get_devices_from_response_dict(response_dict, device_types.THERMOSTAT)[1] + self.assertTrue(thermostat.away()) + self.assertEqual(thermostat2.away(), None) + + + def test_thermostat_users_away_generic(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/go_control_thermostat.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + thermostat = get_devices_from_response_dict(response_dict, device_types.THERMOSTAT)[0] + self.assertTrue(thermostat.away() is None) + + def test_thermostat_profile(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/ecobee_thermostat.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() response_dict["data"] = device_list thermostat = get_devices_from_response_dict(response_dict, device_types.THERMOSTAT)[0] self.assertTrue(thermostat.away()) diff --git a/src/setup.py b/src/setup.py index dc2a2b0..8aa1ca1 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='1.6.0', + version='1.7.0', description='Access Wink devices via the Wink API', url='http://github.com/python-wink/python-wink', author='Brad Johnson, William Scanlon', From b1f5f899a8ec7d90f48bb4ece54a764105293750 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Fri, 24 Nov 2017 22:35:27 -0500 Subject: [PATCH 157/178] Fixed setting extended lock features (#96) * Fixed setting extended lock features --- CHANGELOG.md | 12 +++-- src/pywink/api.py | 53 +++++++++++-------- src/pywink/devices/air_conditioner.py | 2 +- src/pywink/devices/factory.py | 11 ++-- src/pywink/devices/fan.py | 4 +- src/pywink/devices/light_bulb.py | 2 +- src/pywink/devices/lock.py | 12 ++--- src/pywink/devices/piggy_bank.py | 2 +- src/pywink/devices/siren.py | 3 +- src/pywink/devices/thermostat.py | 3 +- src/pywink/devices/water_heater.py | 2 +- src/pywink/test/api_test.py | 6 --- .../test/devices/air_conditioner_test.py | 7 ++- src/pywink/test/devices/base_test.py | 6 +-- src/pywink/test/devices/fan_test.py | 7 ++- src/pywink/test/devices/garage_door_test.py | 7 ++- src/pywink/test/devices/hub_test.py | 7 ++- src/pywink/test/devices/light_bulb_test.py | 7 ++- src/pywink/test/devices/lock_test.py | 6 +-- src/pywink/test/devices/powerstrip_test.py | 6 +-- src/pywink/test/devices/scene_test.py | 5 +- src/pywink/test/devices/sensor_test.py | 27 +++++----- src/pywink/test/devices/siren_test.py | 6 +-- src/pywink/test/devices/switch_test.py | 5 +- src/pywink/test/devices/thermostat_test.py | 8 ++- src/pywink/test/devices/water_heater_test.py | 7 ++- src/setup.py | 2 +- 27 files changed, 109 insertions(+), 116 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4633642..a9bc165 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Change Log +## 1.7.1 +- Extended lock feature can't be performed via local control. Set with online. +- Cleaned up imports/tests/style + ## 1.7.0 - Thermostat fixes for away mode. - Support for Dome Siren/Chime @@ -44,7 +48,7 @@ - Wink Aros Bugfix ## 1.2.2 -- Siren inherts from Base device +- Siren inherits from Base device ## 1.2.1 - Set default endpoint in wink_api_fetch @@ -53,7 +57,7 @@ - Robot and Scene support ## 1.1.1 -- Bugfix for lutron lights missing object_id and object_type +- Bugfix for Lutron lights missing object_id and object_type ## 1.1.0 - Support for Quirky Aros AC units @@ -62,7 +66,7 @@ - Fix for leaksmart valves ## 1.0.0 -- Switch to object_type for device type detction +- Switch to object_type for device type detection - Hard coded user agent - Support for Lutron connected bulb remotes - Support for Sprinklers @@ -121,7 +125,7 @@ - Added Wink keys (Wink Lock user codes) ## 0.7.8 -- Added support for retrieving the Pubnub subscription details +- Added support for retrieving the PubNub subscription details ## 0.7.7 - Stopped duplicating door switches in `get_devices_from_response_dict` diff --git a/src/pywink/api.py b/src/pywink/api.py index daea843..a0dbda6 100644 --- a/src/pywink/api.py +++ b/src/pywink/api.py @@ -116,7 +116,7 @@ def local_set_state(self, device, state, id_override=None, type_override=None): data=json.dumps(state), headers=LOCAL_API_HEADERS, verify=False, timeout=3) - except: + except requests.exceptions.RequestException: _LOGGER.error("Error sending local control request. Sending request online") return self.set_device_state(device, state, id_override, type_override) response_json = arequest.json() @@ -159,7 +159,6 @@ def local_get_state(self, device, id_override=None, type_override=None): Args: device (WinkDevice): The device the change is being requested for. - state (Dict): The state being requested. id_override (String, optional): A device ID used to override the passed in device's ID. Used to make changes on sub-devices. i.e. Outlet in a Powerstrip. The Parent device's ID. @@ -189,7 +188,7 @@ def local_get_state(self, device, id_override=None, type_override=None): arequest = requests.get(url_string, headers=LOCAL_API_HEADERS, verify=False, timeout=3) - except: + except requests.exceptions.RequestException: _LOGGER.error("Error sending local control request. Sending request online") return self.get_device_state(device, id_override, type_override) response_json = arequest.json() @@ -221,10 +220,13 @@ def update_firmware(self, device, id_override=None, type_override=None): url_string = "{}/{}s/{}/update_firmware".format(self.BASE_URL, object_type, object_id) - arequest = requests.post(url_string, - headers=API_HEADERS) - response_json = arequest.json() - return response_json + try: + arequest = requests.post(url_string, + headers=API_HEADERS) + response_json = arequest.json() + return response_json + except requests.exceptions.RequestException: + return None def remove_device(self, device, id_override=None, type_override=None): """ @@ -245,12 +247,16 @@ def remove_device(self, device, id_override=None, type_override=None): url_string = "{}/{}s/{}".format(self.BASE_URL, object_type, object_id) - arequest = requests.delete(url_string, - headers=API_HEADERS) - if arequest.status_code == 204: - return True - _LOGGER.error("Failed to remove device. Status code: " + arequest.status_code) - return False + try: + arequest = requests.delete(url_string, + headers=API_HEADERS) + if arequest.status_code == 204: + return True + _LOGGER.error("Failed to remove device. Status code: " + arequest.status_code) + return False + except requests.exceptions.RequestException: + _LOGGER.error("Failed to remove device. Status code: " + arequest.status_code) + return False def create_lock_key(self, device, new_device_json, id_override=None, type_override=None): """ @@ -272,11 +278,14 @@ def create_lock_key(self, device, new_device_json, id_override=None, type_overri url_string = "{}/{}s/{}/keys".format(self.BASE_URL, object_type, object_id) - arequest = requests.post(url_string, - data=json.dumps(new_device_json), - headers=API_HEADERS) - response_json = arequest.json() - return response_json + try: + arequest = requests.post(url_string, + data=json.dumps(new_device_json), + headers=API_HEADERS) + response_json = arequest.json() + return response_json + except requests.exceptions.RequestException: + return None def disable_local_control(): @@ -563,8 +572,11 @@ def get_binary_switch_groups(): def get_subscription_key(): response_dict = wink_api_fetch() - first_device = response_dict.get('data')[0] - return get_subscription_key_from_response_dict(first_device) + try: + first_device = response_dict.get('data')[0] + return get_subscription_key_from_response_dict(first_device) + except IndexError: + raise WinkAPIException("No Wink devices associated with account.") def get_subscription_key_from_response_dict(device): @@ -579,7 +591,6 @@ def wink_api_fetch(end_point='wink_devices'): _LOGGER.debug(response) if response.status_code == 200: return response.json() - if response.status_code == 401: raise WinkAPIException("401 Response from Wink API. Maybe Bearer token is expired?") else: diff --git a/src/pywink/devices/air_conditioner.py b/src/pywink/devices/air_conditioner.py index d9e994a..b7df68e 100644 --- a/src/pywink/devices/air_conditioner.py +++ b/src/pywink/devices/air_conditioner.py @@ -86,7 +86,7 @@ def set_operation_mode(self, mode): def set_temperature(self, max_set_point=None): """ - :param temperature: a float for the temperature value in celsius + :param max_set_point: a float for the max set point value in celsius :return: nothing """ desired_state = {} diff --git a/src/pywink/devices/factory.py b/src/pywink/devices/factory.py index f91c06b..44004bc 100644 --- a/src/pywink/devices/factory.py +++ b/src/pywink/devices/factory.py @@ -147,16 +147,13 @@ def __get_outlets_from_powerstrip(item, api_interface): def __get_devices_from_piggy_bank(item, api_interface): - subdevices = [] - subdevices.append(WinkPorkfolioBalanceSensor(item, api_interface)) - subdevices.append(WinkPorkfolioNose(item, api_interface)) - return subdevices + return [WinkPorkfolioBalanceSensor(item, api_interface), + WinkPorkfolioNose(item, api_interface)] def __get_sensors_from_smoke_detector(item, api_interface): - sensors = [] - sensors.append(WinkSmokeDetector(item, api_interface)) - sensors.append(WinkCoDetector(item, api_interface)) + sensors = [WinkSmokeDetector(item, api_interface), + WinkCoDetector(item, api_interface)] if item.get("manufacturer_device_model") == "nest": sensors.append(WinkSmokeSeverity(item, api_interface)) sensors.append(WinkCoSeverity(item, api_interface)) diff --git a/src/pywink/devices/fan.py b/src/pywink/devices/fan.py index a53eb7e..c76fec4 100644 --- a/src/pywink/devices/fan.py +++ b/src/pywink/devices/fan.py @@ -52,7 +52,7 @@ def state(self): def set_state(self, state, speed=None): """ - :param powered: bool + :param state: bool :param speed: a string one of ["lowest", "low", "medium", "high", "auto"] defaults to last speed :return: nothing @@ -71,7 +71,7 @@ def set_state(self, state, speed=None): def set_fan_direction(self, direction): """ - :param speed: a string one of ["forward", "reverse"] + :param direction: a string one of ["forward", "reverse"] :return: nothing """ desired_state = {"direction": direction} diff --git a/src/pywink/devices/light_bulb.py b/src/pywink/devices/light_bulb.py index 3cfd8cd..a1273c6 100644 --- a/src/pywink/devices/light_bulb.py +++ b/src/pywink/devices/light_bulb.py @@ -162,4 +162,4 @@ def _format_xy(xy): def _get_color_as_hue_saturation_brightness(hue_sat): if hue_sat: color_hs_iter = iter(hue_sat) - return (next(color_hs_iter), next(color_hs_iter), 1) + return next(color_hs_iter), next(color_hs_iter), 1 diff --git a/src/pywink/devices/lock.py b/src/pywink/devices/lock.py index e8e5f06..e39bcde 100644 --- a/src/pywink/devices/lock.py +++ b/src/pywink/devices/lock.py @@ -34,7 +34,7 @@ def set_alarm_sensitivity(self, mode): :return: nothing """ values = {"desired_state": {"alarm_sensitivity": mode}} - response = self.api_interface.local_set_state(self, values) + response = self.api_interface.set_device_state(self, values) self._update_state_from_response(response) def set_alarm_mode(self, mode): @@ -43,7 +43,7 @@ def set_alarm_mode(self, mode): :return: nothing """ values = {"desired_state": {"alarm_mode": mode}} - response = self.api_interface.local_set_state(self, values) + response = self.api_interface.set_device_state(self, values) self._update_state_from_response(response) def set_alarm_state(self, state): @@ -52,7 +52,7 @@ def set_alarm_state(self, state): :return: nothing """ values = {"desired_state": {"alarm_enabled": state}} - response = self.api_interface.local_set_state(self, values) + response = self.api_interface.set_device_state(self, values) self._update_state_from_response(response) def set_vacation_mode(self, state): @@ -61,7 +61,7 @@ def set_vacation_mode(self, state): :return: nothing """ values = {"desired_state": {"vacation_mode_enabled": state}} - response = self.api_interface.local_set_state(self, values) + response = self.api_interface.set_device_state(self, values) self._update_state_from_response(response) def set_beeper_mode(self, state): @@ -70,7 +70,7 @@ def set_beeper_mode(self, state): :return: nothing """ values = {"desired_state": {"beeper_enabled": state}} - response = self.api_interface.local_set_state(self, values) + response = self.api_interface.set_device_state(self, values) self._update_state_from_response(response) def set_state(self, state): @@ -83,7 +83,7 @@ def set_state(self, state): self._update_state_from_response(response) def update_state(self): - """ Update state with latest info from Wink API. """ + """Update state with latest info from Wink API.""" response = self.api_interface.local_get_state(self) return self._update_state_from_response(response) diff --git a/src/pywink/devices/piggy_bank.py b/src/pywink/devices/piggy_bank.py index d3818bf..3bbf544 100644 --- a/src/pywink/devices/piggy_bank.py +++ b/src/pywink/devices/piggy_bank.py @@ -21,7 +21,7 @@ def available(self): def set_state(self, color_hex): """ - :param nose_color: a hex string indicating the color of the porkfolio nose + :param color_hex: a hex string indicating the color of the porkfolio nose :return: nothing From the api... "the color of the nose is not in the desired_state diff --git a/src/pywink/devices/siren.py b/src/pywink/devices/siren.py index 496a11e..2428e6e 100644 --- a/src/pywink/devices/siren.py +++ b/src/pywink/devices/siren.py @@ -119,6 +119,7 @@ def set_chime(self, sound, cycles=None): :param sound: a str, one of ["doorbell", "fur_elise", "doorbell_extended", "alert", "william_tell", "rondo_alla_turca", "police_siren", ""evacuation", "beep_beep", "beep", "inactive"] + :param cycles: Undocumented seems to have no effect? :return: nothing """ desired_state = {"activate_chime": sound} @@ -130,7 +131,7 @@ def set_chime(self, sound, cycles=None): def set_auto_shutoff(self, timer): """ - :param time: an int, one of [None (never), -1, 30, 60, 120] + :param timer: an int, one of [None (never), -1, 30, 60, 120] :return: nothing """ values = { diff --git a/src/pywink/devices/thermostat.py b/src/pywink/devices/thermostat.py index 69bde1a..502210e 100644 --- a/src/pywink/devices/thermostat.py +++ b/src/pywink/devices/thermostat.py @@ -177,7 +177,8 @@ def set_operation_mode(self, mode): def set_temperature(self, min_set_point=None, max_set_point=None): """ - :param temperature: a float for the temperature value in celsius + :param min_set_point: a float for the min set point value in celsius + :param max_set_point: a float for the max set point value in celsius :return: nothing """ desired_state = {} diff --git a/src/pywink/devices/water_heater.py b/src/pywink/devices/water_heater.py index 02d57b2..d36ff17 100644 --- a/src/pywink/devices/water_heater.py +++ b/src/pywink/devices/water_heater.py @@ -52,7 +52,7 @@ def set_operation_mode(self, mode): def set_temperature(self, set_point): """ - :param temperature: a float for the temperature value in celsius + :param set_point: a float for the set point value in celsius :return: nothing """ response = self.api_interface.set_device_state(self, { diff --git a/src/pywink/test/api_test.py b/src/pywink/test/api_test.py index e0c8c96..1cb5eda 100644 --- a/src/pywink/test/api_test.py +++ b/src/pywink/test/api_test.py @@ -1,5 +1,4 @@ from http.server import BaseHTTPRequestHandler, HTTPServer -import json import re import socket from threading import Thread @@ -7,12 +6,9 @@ import os # Third-party imports... -import requests -from mock import patch from unittest.mock import MagicMock, Mock from pywink.api import * -from pywink.devices import types as device_types from pywink.api import WinkApiInterface from pywink.devices.sensor import WinkSensor from pywink.devices.hub import WinkHub @@ -32,7 +28,6 @@ from pywink.devices.button import WinkButton from pywink.devices.gang import WinkGang from pywink.devices.smoke_detector import WinkSmokeDetector, WinkSmokeSeverity, WinkCoDetector, WinkCoSeverity -from pywink.devices.sprinkler import WinkSprinkler from pywink.devices.camera import WinkCanaryCamera from pywink.devices.air_conditioner import WinkAirConditioner from pywink.devices.propane_tank import WinkPropaneTank @@ -46,7 +41,6 @@ class ApiTests(unittest.TestCase): - def setUp(self): global USERS_ME_WINK_DEVICES, GROUPS super(ApiTests, self).setUp() diff --git a/src/pywink/test/devices/air_conditioner_test.py b/src/pywink/test/devices/air_conditioner_test.py index d5be48f..a1850d6 100644 --- a/src/pywink/test/devices/air_conditioner_test.py +++ b/src/pywink/test/devices/air_conditioner_test.py @@ -2,18 +2,17 @@ import os import unittest -import mock +from unittest.mock import MagicMock -from pywink.api import get_devices_from_response_dict, WinkApiInterface +from pywink.api import get_devices_from_response_dict from pywink.devices import types as device_types -from pywink.devices.air_conditioner import WinkAirConditioner class AirConditionerTests(unittest.TestCase): def setUp(self): super(AirConditionerTests, self).setUp() - self.api_interface = mock.MagicMock() + self.api_interface = MagicMock() def test_ac_state(self): device_list = [] diff --git a/src/pywink/test/devices/base_test.py b/src/pywink/test/devices/base_test.py index ce1a8e3..7e75901 100644 --- a/src/pywink/test/devices/base_test.py +++ b/src/pywink/test/devices/base_test.py @@ -2,9 +2,9 @@ import os import unittest -import mock +from unittest.mock import MagicMock -from pywink.api import get_devices_from_response_dict, WinkApiInterface +from pywink.api import get_devices_from_response_dict from pywink.devices import types as device_types from pywink.devices.key import WinkKey from pywink.devices.powerstrip import WinkPowerStripOutlet, WinkPowerStrip @@ -33,7 +33,7 @@ class BaseTests(unittest.TestCase): def setUp(self): super(BaseTests, self).setUp() - self.api_interface = mock.MagicMock() + self.api_interface = MagicMock() all_devices = os.listdir('{}/api_responses/'.format(os.path.dirname(__file__))) self.response_dict = {} device_list = [] diff --git a/src/pywink/test/devices/fan_test.py b/src/pywink/test/devices/fan_test.py index 9c89359..a1a4c33 100644 --- a/src/pywink/test/devices/fan_test.py +++ b/src/pywink/test/devices/fan_test.py @@ -2,18 +2,17 @@ import os import unittest -import mock +from unittest.mock import MagicMock -from pywink.api import get_devices_from_response_dict, WinkApiInterface +from pywink.api import get_devices_from_response_dict from pywink.devices import types as device_types -from pywink.devices.fan import WinkFan class FanTests(unittest.TestCase): def setUp(self): super(FanTests, self).setUp() - self.api_interface = mock.MagicMock() + self.api_interface = MagicMock() device_list = [] self.response_dict = {} _json_file = open('{}/api_responses/fan.json'.format(os.path.dirname(__file__))) diff --git a/src/pywink/test/devices/garage_door_test.py b/src/pywink/test/devices/garage_door_test.py index b8ea978..448c9b4 100644 --- a/src/pywink/test/devices/garage_door_test.py +++ b/src/pywink/test/devices/garage_door_test.py @@ -2,18 +2,17 @@ import os import unittest -import mock +from unittest.mock import MagicMock -from pywink.api import get_devices_from_response_dict, WinkApiInterface +from pywink.api import get_devices_from_response_dict from pywink.devices import types as device_types -from pywink.devices.garage_door import WinkGarageDoor class GarageDoorTests(unittest.TestCase): def setUp(self): super(GarageDoorTests, self).setUp() - self.api_interface = mock.MagicMock() + self.api_interface = MagicMock() all_devices = os.listdir('{}/api_responses/'.format(os.path.dirname(__file__))) self.response_dict = {} device_list = [] diff --git a/src/pywink/test/devices/hub_test.py b/src/pywink/test/devices/hub_test.py index e7f4ee0..4232d74 100644 --- a/src/pywink/test/devices/hub_test.py +++ b/src/pywink/test/devices/hub_test.py @@ -2,18 +2,17 @@ import os import unittest -import mock +from unittest.mock import MagicMock -from pywink.api import get_devices_from_response_dict, WinkApiInterface +from pywink.api import get_devices_from_response_dict from pywink.devices import types as device_types -from pywink.devices.hub import WinkHub class HubTests(unittest.TestCase): def setUp(self): super(HubTests, self).setUp() - self.api_interface = mock.MagicMock() + self.api_interface = MagicMock() all_devices = os.listdir('{}/api_responses/'.format(os.path.dirname(__file__))) self.response_dict = {} device_list = [] diff --git a/src/pywink/test/devices/light_bulb_test.py b/src/pywink/test/devices/light_bulb_test.py index 34a17da..80e10f0 100644 --- a/src/pywink/test/devices/light_bulb_test.py +++ b/src/pywink/test/devices/light_bulb_test.py @@ -2,11 +2,10 @@ import os import unittest -import mock +from unittest.mock import MagicMock -from pywink.api import get_devices_from_response_dict, WinkApiInterface +from pywink.api import get_devices_from_response_dict from pywink.devices import types as device_types -from pywink.devices.light_bulb import WinkLightBulb from pywink.devices.light_group import WinkLightGroup JSON_DATA = {} @@ -16,7 +15,7 @@ class LightBulbTests(unittest.TestCase): def setUp(self): super(LightBulbTests, self).setUp() - self.api_interface = mock.MagicMock() + self.api_interface = MagicMock() def test_bulb_brightness(self): device_list = [] diff --git a/src/pywink/test/devices/lock_test.py b/src/pywink/test/devices/lock_test.py index dba6ee5..2e3b514 100644 --- a/src/pywink/test/devices/lock_test.py +++ b/src/pywink/test/devices/lock_test.py @@ -2,9 +2,9 @@ import os import unittest -import mock +from unittest.mock import MagicMock -from pywink.api import get_devices_from_response_dict, WinkApiInterface +from pywink.api import get_devices_from_response_dict from pywink.devices import types as device_types @@ -12,7 +12,7 @@ class LockTests(unittest.TestCase): def setUp(self): super(LockTests, self).setUp() - self.api_interface = mock.MagicMock() + self.api_interface = MagicMock() device_list = [] self.response_dict = {} _json_file = open('{}/api_responses/schlage_lock.json'.format(os.path.dirname(__file__))) diff --git a/src/pywink/test/devices/powerstrip_test.py b/src/pywink/test/devices/powerstrip_test.py index a662b5e..046fd9e 100644 --- a/src/pywink/test/devices/powerstrip_test.py +++ b/src/pywink/test/devices/powerstrip_test.py @@ -2,11 +2,8 @@ import os import unittest -import mock - -from pywink.api import get_devices_from_response_dict, WinkApiInterface +from pywink.api import get_devices_from_response_dict from pywink.devices import types as device_types -from pywink.devices.powerstrip import WinkPowerStrip, WinkPowerStripOutlet class PowerstripTests(unittest.TestCase): @@ -45,7 +42,6 @@ def test_outlet_index(self): devices = get_devices_from_response_dict(response_dict, device_types.POWERSTRIP) self.assertEqual(len(devices), 3) - powerstrip = devices[-1] outlet_1 = devices[0] outlet_2 = devices[1] self.assertEqual(0, outlet_1.index()) diff --git a/src/pywink/test/devices/scene_test.py b/src/pywink/test/devices/scene_test.py index 0e51fca..4ba7f8e 100644 --- a/src/pywink/test/devices/scene_test.py +++ b/src/pywink/test/devices/scene_test.py @@ -2,11 +2,8 @@ import os import unittest -import mock - -from pywink.api import get_devices_from_response_dict, WinkApiInterface +from pywink.api import get_devices_from_response_dict from pywink.devices import types as device_types -from pywink.devices.scene import WinkScene class SceneTests(unittest.TestCase): diff --git a/src/pywink/test/devices/sensor_test.py b/src/pywink/test/devices/sensor_test.py index 96e91f8..48fcd20 100644 --- a/src/pywink/test/devices/sensor_test.py +++ b/src/pywink/test/devices/sensor_test.py @@ -2,20 +2,19 @@ import os import unittest -import mock +from unittest.mock import MagicMock -from pywink.api import get_devices_from_response_dict, WinkApiInterface +from pywink.api import get_devices_from_response_dict from pywink.devices import types as device_types -from pywink.devices.sensor import WinkSensor from pywink.devices.piggy_bank import WinkPorkfolioBalanceSensor from pywink.devices.smoke_detector import WinkSmokeDetector, WinkCoDetector, WinkSmokeSeverity, WinkCoSeverity -from pywink.devices.propane_tank import WinkPropaneTank + class SensorTests(unittest.TestCase): def setUp(self): super(SensorTests, self).setUp() - self.api_interface = mock.MagicMock() + self.api_interface = MagicMock() all_devices = os.listdir('{}/api_responses/'.format(os.path.dirname(__file__))) self.response_dict = {} device_list = [] @@ -49,7 +48,7 @@ class EggtrayTests(unittest.TestCase): def setUp(self): super(EggtrayTests, self).setUp() - self.api_interface = mock.MagicMock() + self.api_interface = MagicMock() all_devices = os.listdir('{}/api_responses/'.format(os.path.dirname(__file__))) self.response_dict = {} device_list = [] @@ -75,11 +74,12 @@ def test_unit_is_eggs(self): for device in devices: self.assertEqual(device.unit(), "eggs") + class KeyTests(unittest.TestCase): def setUp(self): super(KeyTests, self).setUp() - self.api_interface = mock.MagicMock() + self.api_interface = MagicMock() all_devices = os.listdir('{}/api_responses/'.format(os.path.dirname(__file__))) self.response_dict = {} device_list = [] @@ -116,11 +116,12 @@ def test_unit_is_none(self): for device in devices: self.assertIsNone(device.unit()) + class PorkfolioTests(unittest.TestCase): def setUp(self): super(PorkfolioTests, self).setUp() - self.api_interface = mock.MagicMock() + self.api_interface = MagicMock() all_devices = os.listdir('{}/api_responses/'.format(os.path.dirname(__file__))) self.response_dict = {} device_list = [] @@ -156,11 +157,12 @@ def test_available_is_true(self): if isinstance(device, WinkPorkfolioBalanceSensor): self.assertTrue(device.available()) + class GangTests(unittest.TestCase): def setUp(self): super(GangTests, self).setUp() - self.api_interface = mock.MagicMock() + self.api_interface = MagicMock() all_devices = os.listdir('{}/api_responses/'.format(os.path.dirname(__file__))) self.response_dict = {} device_list = [] @@ -176,11 +178,12 @@ def test_unit_is_none(self): for device in devices: self.assertIsNone(device.unit()) + class SmokeDetectorTests(unittest.TestCase): def setUp(self): super(SmokeDetectorTests, self).setUp() - self.api_interface = mock.MagicMock() + self.api_interface = MagicMock() all_devices = os.listdir('{}/api_responses/'.format(os.path.dirname(__file__))) self.response_dict = {} device_list = [] @@ -217,7 +220,7 @@ class RemoteTests(unittest.TestCase): def setUp(self): super(RemoteTests, self).setUp() - self.api_interface = mock.MagicMock() + self.api_interface = MagicMock() all_devices = os.listdir('{}/api_responses/'.format(os.path.dirname(__file__))) self.response_dict = {} device_list = [] @@ -247,7 +250,7 @@ class PropaneTankTests(unittest.TestCase): def setUp(self): super(PropaneTankTests, self).setUp() - self.api_interface = mock.MagicMock() + self.api_interface = MagicMock() all_devices = os.listdir('{}/api_responses/'.format(os.path.dirname(__file__))) self.response_dict = {} device_list = [] diff --git a/src/pywink/test/devices/siren_test.py b/src/pywink/test/devices/siren_test.py index 4b16ed0..7f8d6cc 100644 --- a/src/pywink/test/devices/siren_test.py +++ b/src/pywink/test/devices/siren_test.py @@ -2,9 +2,9 @@ import os import unittest -import mock +from unittest.mock import MagicMock -from pywink.api import get_devices_from_response_dict, WinkApiInterface +from pywink.api import get_devices_from_response_dict from pywink.devices import types as device_types @@ -12,7 +12,7 @@ class SirenTests(unittest.TestCase): def setUp(self): super(SirenTests, self).setUp() - self.api_interface = mock.MagicMock() + self.api_interface = MagicMock() device_list = [] self.response_dict = {} _json_file = open('{}/api_responses/go_control_siren.json'.format(os.path.dirname(__file__))) diff --git a/src/pywink/test/devices/switch_test.py b/src/pywink/test/devices/switch_test.py index 9066188..882ac8a 100644 --- a/src/pywink/test/devices/switch_test.py +++ b/src/pywink/test/devices/switch_test.py @@ -2,11 +2,8 @@ import os import unittest -import mock - -from pywink.api import get_devices_from_response_dict, WinkApiInterface +from pywink.api import get_devices_from_response_dict from pywink.devices import types as device_types -from pywink.devices.binary_switch import WinkBinarySwitch from pywink.devices.binary_switch_group import WinkBinarySwitchGroup JSON_DATA = {} diff --git a/src/pywink/test/devices/thermostat_test.py b/src/pywink/test/devices/thermostat_test.py index 6f01744..7e31c0a 100644 --- a/src/pywink/test/devices/thermostat_test.py +++ b/src/pywink/test/devices/thermostat_test.py @@ -2,18 +2,17 @@ import os import unittest -import mock +from unittest.mock import MagicMock -from pywink.api import get_devices_from_response_dict, WinkApiInterface +from pywink.api import get_devices_from_response_dict from pywink.devices import types as device_types -from pywink.devices.thermostat import WinkThermostat class ThermostatTests(unittest.TestCase): def setUp(self): super(ThermostatTests, self).setUp() - self.api_interface = mock.MagicMock() + self.api_interface = MagicMock() def test_thermostat_state(self): device_list = [] @@ -60,7 +59,6 @@ def test_thermostat_users_away(self): self.assertTrue(thermostat.away()) self.assertEqual(thermostat2.away(), None) - def test_thermostat_users_away_generic(self): device_list = [] response_dict = {} diff --git a/src/pywink/test/devices/water_heater_test.py b/src/pywink/test/devices/water_heater_test.py index 14bcbe0..0769843 100644 --- a/src/pywink/test/devices/water_heater_test.py +++ b/src/pywink/test/devices/water_heater_test.py @@ -2,18 +2,17 @@ import os import unittest -import mock +from unittest.mock import MagicMock -from pywink.api import get_devices_from_response_dict, WinkApiInterface +from pywink.api import get_devices_from_response_dict from pywink.devices import types as device_types -from pywink.devices.water_heater import WinkWaterHeater class WaterHeaterTests(unittest.TestCase): def setUp(self): super(WaterHeaterTests, self).setUp() - self.api_interface = mock.MagicMock() + self.api_interface = MagicMock() def test_current_state(self): device_list = [] diff --git a/src/setup.py b/src/setup.py index 8aa1ca1..8c82c62 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='1.7.0', + version='1.7.1', description='Access Wink devices via the Wink API', url='http://github.com/python-wink/python-wink', author='Brad Johnson, William Scanlon', From 8b3a630504a2cc091f83553f73e11f95429f5df7 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Mon, 4 Dec 2017 19:36:30 -0500 Subject: [PATCH 158/178] String formatting for logging, new device JSON, and fix for hubs without local control ID (#98) * Use string formatting for logging and add new device JSON. Also don't request local control access token if no local control ID is present. --- CHANGELOG.md | 1 + src/pywink/api.py | 46 +-- src/pywink/test/api_test.py | 2 +- .../devices/api_responses/august_lock.json | 61 ++++ .../api_responses/wink_siren_chime.json | 280 ++++++++++++++++++ src/pywink/test/devices/base_test.py | 6 +- 6 files changed, 370 insertions(+), 26 deletions(-) create mode 100644 src/pywink/test/devices/api_responses/august_lock.json create mode 100644 src/pywink/test/devices/api_responses/wink_siren_chime.json diff --git a/CHANGELOG.md b/CHANGELOG.md index a9bc165..9e93efe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 1.7.1 - Extended lock feature can't be performed via local control. Set with online. - Cleaned up imports/tests/style +- Use string formatting when logging ## 1.7.0 - Thermostat fixes for away mode. diff --git a/src/pywink/api.py b/src/pywink/api.py index a0dbda6..63139fb 100644 --- a/src/pywink/api.py +++ b/src/pywink/api.py @@ -78,7 +78,7 @@ def set_device_state(self, device, state, id_override=None, type_override=None): else: raise WinkAPIException("Failed to refresh access token.") response_json = arequest.json() - _LOGGER.debug(response_json) + _LOGGER.debug('%s', response_json) return response_json # pylint: disable=bare-except @@ -120,7 +120,7 @@ def local_set_state(self, device, state, id_override=None, type_override=None): _LOGGER.error("Error sending local control request. Sending request online") return self.set_device_state(device, state, id_override, type_override) response_json = arequest.json() - _LOGGER.debug(response_json) + _LOGGER.debug('%s', response_json) temp_state = device.json_state for key, value in response_json["data"]["last_reading"].items(): temp_state["last_reading"][key] = value @@ -149,7 +149,7 @@ def get_device_state(self, device, id_override=None, type_override=None): object_type, object_id) arequest = requests.get(url_string, headers=API_HEADERS) response_json = arequest.json() - _LOGGER.debug(response_json) + _LOGGER.debug('%s', response_json) return response_json # pylint: disable=bare-except @@ -192,7 +192,7 @@ def local_get_state(self, device, id_override=None, type_override=None): _LOGGER.error("Error sending local control request. Sending request online") return self.get_device_state(device, id_override, type_override) response_json = arequest.json() - _LOGGER.debug(response_json) + _LOGGER.debug('%s', response_json) temp_state = device.json_state for key, value in response_json["data"]["last_reading"].items(): temp_state["last_reading"][key] = value @@ -247,15 +247,16 @@ def remove_device(self, device, id_override=None, type_override=None): url_string = "{}/{}s/{}".format(self.BASE_URL, object_type, object_id) + try: arequest = requests.delete(url_string, headers=API_HEADERS) if arequest.status_code == 204: return True - _LOGGER.error("Failed to remove device. Status code: " + arequest.status_code) + _LOGGER.error("Failed to remove device. Status code: %s", arequest.status_code) return False except requests.exceptions.RequestException: - _LOGGER.error("Failed to remove device. Status code: " + arequest.status_code) + _LOGGER.error("Failed to remove device.") return False def create_lock_key(self, device, new_device_json, id_override=None, type_override=None): @@ -294,7 +295,7 @@ def disable_local_control(): def set_user_agent(user_agent): - _LOGGER.info("Setting user agent to " + user_agent) + _LOGGER.info("Setting user agent to %s", user_agent) API_HEADERS["User-Agent"] = user_agent @@ -307,8 +308,7 @@ def set_bearer_token(token): def legacy_set_wink_credentials(email, password, client_id, client_secret): - log_string = "Email: %s Password: %s Client_id: %s Client_secret: %s" % (email, password, client_id, client_secret) - _LOGGER.debug(log_string) + _LOGGER.debug("Email: %s Password: %s Client_id: %s Client_secret: %s", email, password, client_id, client_secret) global CLIENT_ID, CLIENT_SECRET, REFRESH_TOKEN CLIENT_ID = client_id @@ -334,9 +334,8 @@ def legacy_set_wink_credentials(email, password, client_id, client_secret): def set_wink_credentials(client_id, client_secret, access_token, refresh_token): - log_string = "Client_id: %s Client_secret: %s Access_token: %s Refreash_token: %s" % (client_id, client_secret, - access_token, refresh_token) - _LOGGER.debug(log_string) + _LOGGER.debug("Client_id: %s Client_secret: %s Access_token: %s Refreash_token: %s", + client_id, client_secret, access_token, refresh_token) global CLIENT_ID, CLIENT_SECRET, REFRESH_TOKEN CLIENT_ID = client_id @@ -376,7 +375,7 @@ def refresh_access_token(): def get_authorization_url(client_id, redirect_uri): - _LOGGER.debug("Client_id: " + client_id + " redirect_uri: " + redirect_uri) + _LOGGER.debug("Client_id: %s redirect_uri: %s", client_id, redirect_uri) global CLIENT_ID CLIENT_ID = client_id @@ -385,7 +384,7 @@ def get_authorization_url(client_id, redirect_uri): def request_token(code, client_secret): - _LOGGER.debug("code: " + code + " Client_secret: " + client_secret) + _LOGGER.debug("code: %s Client_secret: %s", code, client_secret) data = { "client_secret": client_secret, "grant_type": "authorization_code", @@ -397,7 +396,7 @@ def request_token(code, client_secret): response = requests.post('{}/oauth2/token'.format(WinkApiInterface.BASE_URL), data=json.dumps(data), headers=headers) - _LOGGER.debug(response) + _LOGGER.debug('%s', response) response_json = response.json() access_token = response_json.get('access_token') refresh_token = response_json.get('refresh_token') @@ -407,12 +406,12 @@ def request_token(code, client_secret): def get_user(): url_string = "{}/users/me".format(WinkApiInterface.BASE_URL) arequest = requests.get(url_string, headers=API_HEADERS) - _LOGGER.debug(arequest) + _LOGGER.debug('%s', arequest) return arequest.json() def get_local_control_access_token(local_control_id): - _LOGGER.debug("Local_control_id: " + local_control_id) + _LOGGER.debug("Local_control_id: %s", local_control_id) if CLIENT_ID and CLIENT_SECRET and REFRESH_TOKEN: data = { "client_id": CLIENT_ID, @@ -428,7 +427,7 @@ def get_local_control_access_token(local_control_id): response = requests.post('{}/oauth2/token'.format(WinkApiInterface.BASE_URL), data=json.dumps(data), headers=headers) - _LOGGER.debug(response) + _LOGGER.debug('%s', response) response_json = response.json() access_token = response_json.get('access_token') return access_token @@ -498,9 +497,12 @@ def get_hubs(): for hub in hubs: if hub.manufacturer_device_model() in SUPPORTS_LOCAL_CONTROL: _id = hub.local_control_id() - token = get_local_control_access_token(_id) - ip = hub.ip_address() - HUBS[hub.object_id()] = {"ip": ip, "token": token, "id": _id} + if _id is not None: + token = get_local_control_access_token(_id) + ip = hub.ip_address() + HUBS[hub.object_id()] = {"ip": ip, "token": token, "id": _id} + else: + _LOGGER.error("%s is missing local control ID.", hub.name()) return hubs @@ -588,7 +590,7 @@ def get_subscription_key_from_response_dict(device): def wink_api_fetch(end_point='wink_devices'): arequest_url = "{}/users/me/{}".format(WinkApiInterface.BASE_URL, end_point) response = requests.get(arequest_url, headers=API_HEADERS) - _LOGGER.debug(response) + _LOGGER.debug('%s', response) if response.status_code == 200: return response.json() if response.status_code == 401: diff --git a/src/pywink/test/api_test.py b/src/pywink/test/api_test.py index 1cb5eda..3f0c94d 100644 --- a/src/pywink/test/api_test.py +++ b/src/pywink/test/api_test.py @@ -109,7 +109,7 @@ def test_get_subscription_key(self): def test_get_all_devices_from_api(self): WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) devices = get_all_devices() - self.assertEqual(len(devices), 68) + self.assertEqual(len(devices), 70) lights = get_light_bulbs() for light in lights: self.assertTrue(isinstance(light, WinkLightBulb)) diff --git a/src/pywink/test/devices/api_responses/august_lock.json b/src/pywink/test/devices/api_responses/august_lock.json new file mode 100644 index 0000000..8c1c03e --- /dev/null +++ b/src/pywink/test/devices/api_responses/august_lock.json @@ -0,0 +1,61 @@ +{ + "object_type":"lock", + "object_id":"19134", + "uuid":"4500ff6c-00f4-4b27-887a-6273c713456", + "icon_id":null, + "icon_code":null, + "desired_state":{ + "locked":true + }, + "last_reading":{ + "connection":true, + "connection_updated_at":1510865414.4690557, + "locked":true, + "locked_updated_at":1511899061.7470028, + "battery":0.99, + "battery_updated_at":1511826054.0000897, + "last_error":null, + "last_error_updated_at":null, + "desired_locked_updated_at":1511829238.0223362, + "connection_changed_at":1510865414.4690557, + "battery_changed_at":1511308024.8441072, + "locked_changed_at":1511899061.7470028, + "desired_locked_changed_at":1511829238.0223362 + }, + "subscription":{ + "pubnub":{ + "subscribe_key":"sub-c-f7bf7f7e-0542-11e3-a5e8-02ee2ddab7fe", + "channel":"5e6352fdcfb6baf8f845e3b6bb132465|lock-19134|user-76134" + } + }, + "lock_id":"191627", + "name":"August lock", + "locale":"en_us", + "units":{ + + }, + "created_at":1510865414, + "hidden_at":null, + "capabilities":{ + + }, + "triggers":[ + + ], + "manufacturer_device_model":null, + "manufacturer_device_id":"august:9458EEF9492D4A2E9AE567FE4C132456", + "device_manufacturer":null, + "model_name":null, + "upc_id":"", + "upc_code":"august_lock_4", + "primary_upc_code":null, + "hub_id":null, + "local_id":null, + "radio_type":null, + "linked_service_id":"857123", + "lat_lng":[ + null, + null + ], + "location":"" +} diff --git a/src/pywink/test/devices/api_responses/wink_siren_chime.json b/src/pywink/test/devices/api_responses/wink_siren_chime.json new file mode 100644 index 0000000..23f5a8e --- /dev/null +++ b/src/pywink/test/devices/api_responses/wink_siren_chime.json @@ -0,0 +1,280 @@ +{ + "object_type":"siren", + "object_id":"23123", + "uuid":"5cb3a3cb-2bd1-4e18-a6d7-8194c1234657", + "icon_id":null, + "icon_code":null, + "desired_state":{ + "powered":false, + "siren_volume":"medium", + "auto_shutoff":60, + "chime_cycles":1, + "chime_volume":"medium", + "strobe_enabled":true, + "chime_strobe_enabled":true, + "siren_sound":"police_siren", + "activate_chime":"inactive", + "pending_action":null + }, + "last_reading":{ + "connection":true, + "connection_updated_at":1512277890.4193873, + "powered":false, + "powered_updated_at":1512277890.4193873, + "battery":1.0, + "battery_updated_at":1512277890.4193873, + "siren_volume":"medium", + "siren_volume_updated_at":1512277890.4193873, + "auto_shutoff":60, + "auto_shutoff_updated_at":1512277890.4193873, + "chime_cycles":1, + "chime_cycles_updated_at":1512277890.4193873, + "chime_volume":"medium", + "chime_volume_updated_at":1512277890.4193873, + "strobe_enabled":true, + "strobe_enabled_updated_at":1512277890.4193873, + "chime_strobe_enabled":true, + "chime_strobe_enabled_updated_at":1512277890.4193873, + "siren_sound":"police_siren", + "siren_sound_updated_at":1512277890.4193873, + "activate_chime":"inactive", + "activate_chime_updated_at":1512277890.4193873, + "pending_action":null, + "pending_action_updated_at":null, + "desired_powered_updated_at":1512272569.8307827, + "desired_siren_volume_updated_at":1512272569.8307827, + "desired_auto_shutoff_updated_at":1512272569.8307827, + "desired_chime_cycles_updated_at":1512272569.8307827, + "desired_chime_volume_updated_at":1512272569.8307827, + "desired_strobe_enabled_updated_at":1512272569.8307827, + "desired_chime_strobe_enabled_updated_at":1512272569.8307827, + "desired_siren_sound_updated_at":1512272569.8307827, + "desired_activate_chime_updated_at":1512272569.8307827, + "desired_pending_action_updated_at":1512272569.8307827, + "connection_changed_at":1512253160.5966454, + "battery_changed_at":1512253166.2208192, + "powered_changed_at":1512268074.4094055, + "siren_volume_changed_at":1512253221.6845472, + "auto_shutoff_changed_at":1512253166.2208192, + "chime_cycles_changed_at":1512253166.2208192, + "chime_volume_changed_at":1512253238.1547711, + "siren_sound_changed_at":1512253166.2208192, + "activate_chime_changed_at":1512272435.2196875, + "strobe_enabled_changed_at":1512253167.2517407, + "chime_strobe_enabled_changed_at":1512253249.8501527, + "desired_siren_volume_changed_at":1512253221.7828054, + "desired_chime_volume_changed_at":1512253238.2662842, + "desired_activate_chime_changed_at":1512272431.5465739, + "desired_chime_strobe_enabled_changed_at":1512253253.806253, + "desired_powered_changed_at":1512268074.588022, + "desired_auto_shutoff_changed_at":1512253392.6598389, + "desired_chime_cycles_changed_at":1512253392.6598389, + "desired_strobe_enabled_changed_at":1512253392.6598389, + "desired_siren_sound_changed_at":1512253392.6598389, + "desired_pending_action_changed_at":1512253392.6598389 + }, + "subscription":{ + "pubnub":{ + "subscribe_key":"sub-c-f7bf7f7e-0542-11e3-a5e8-02ee2ddab7fe", + "channel":"a1cdea0ff21d03ce81b5cf321b1324567|siren-23123|user-71234" + } + }, + "siren_id":"23637", + "name":"Siren-Upstairs", + "locale":"en_us", + "units":{ + + }, + "created_at":1512253158, + "hidden_at":null, + "capabilities":{ + "fields":[ + { + "type":"boolean", + "field":"connection", + "mutability":"read-only" + }, + { + "type":"boolean", + "field":"powered", + "mutability":"read-write", + "attribute_id":2 + }, + { + "type":"percentage", + "field":"battery", + "mutability":"read-only", + "attribute_id":15 + }, + { + "type":"selection", + "field":"siren_volume", + "choices":[ + "high", + "medium", + "low" + ], + "mutability":"read-write", + "attribute_id":101, + "attribute_value_mapping":{ + "low":"1", + "high":"3", + "medium":"2" + } + }, + { + "type":"selection", + "field":"auto_shutoff", + "choices":[ + 30, + 60, + 300, + -1 + ], + "mutability":"read-write", + "attribute_id":102, + "attribute_value_mapping":{ + "30":"1", + "60":"2", + "300":"3", + "-1":"255" + } + }, + { + "type":"integer", + "field":"chime_cycles", + "range":[ + 1, + 255 + ], + "mutability":"read-write", + "attribute_id":103 + }, + { + "type":"selection", + "field":"chime_volume", + "choices":[ + "high", + "medium", + "low" + ], + "mutability":"read-write", + "attribute_id":104, + "attribute_value_mapping":{ + "low":"1", + "high":"3", + "medium":"2" + } + }, + { + "type":"boolean", + "field":"strobe_enabled", + "mutability":"read-write", + "attribute_id":108, + "attribute_value_mapping":{ + "true":"1", + "false":"0" + } + }, + { + "type":"boolean", + "field":"chime_strobe_enabled", + "mutability":"read-write", + "attribute_id":109, + "attribute_value_mapping":{ + "true":"1", + "false":"0" + } + }, + { + "type":"selection", + "field":"siren_sound", + "choices":[ + "doorbell", + "fur_elise", + "doorbell_extended", + "alert", + "william_tell", + "rondo_alla_turca", + "police_siren", + "evacuation", + "beep_beep", + "beep" + ], + "mutability":"read-write", + "attribute_id":105, + "attribute_value_mapping":{ + "beep":"10", + "alert":"4", + "doorbell":"1", + "beep_beep":"9", + "fur_elise":"2", + "evacuation":"8", + "police_siren":"7", + "william_tell":"5", + "rondo_alla_turca":"6", + "doorbell_extended":"3" + } + }, + { + "type":"selection", + "field":"activate_chime", + "choices":[ + "doorbell", + "fur_elise", + "doorbell_extended", + "alert", + "william_tell", + "rondo_alla_turca", + "police_siren", + "evacuation", + "beep_beep", + "beep", + "inactive" + ], + "mutability":"read-write", + "propagation":"unacknowledged", + "attribute_id":110, + "attribute_value_mapping":{ + "beep":"10", + "alert":"4", + "doorbell":"1", + "inactive":"0", + "beep_beep":"9", + "fur_elise":"2", + "evacuation":"8", + "police_siren":"7", + "william_tell":"5", + "rondo_alla_turca":"6", + "doorbell_extended":"3" + } + }, + { + "type":"selection", + "field":"pending_action", + "choices":[ + "none", + "lookout_bundle_setup" + ], + "mutability":"read-write" + } + ], + "is_sleepy":true, + "polling_interval":43200, + "notification_robots":[ + "offline_notification", + "low_battery_notification" + ], + "excludes_robot_cause":true, + "home_security_device":true, + "required_feature_flag":"a2w_wink_sensors" + }, + "device_manufacturer":"wink", + "model_name":"Siren & Chime", + "upc_id":"1114", + "upc_code":"wink_siren", + "primary_upc_code":"wink_siren", + "hub_id":"143675", + "local_id":"29", + "radio_type":"zwave" +} diff --git a/src/pywink/test/devices/base_test.py b/src/pywink/test/devices/base_test.py index 7e75901..d226ba7 100644 --- a/src/pywink/test/devices/base_test.py +++ b/src/pywink/test/devices/base_test.py @@ -104,7 +104,7 @@ def test_all_devices_manufacturer_device_model_state_is_valid(self): devices = get_devices_from_response_dict(self.response_dict, device_types.ALL_SUPPORTED_DEVICES) skip_types = [WinkKey, WinkPorkfolioBalanceSensor, WinkPorkfolioNose, WinkPowerStripOutlet, WinkSiren, WinkEggtray, WinkRemote, WinkPowerStrip, WinkAirConditioner, WinkPropaneTank] - devices_with_no_device_model = ["GoControl Thermostat", "New Shortcut", "Test robot"] + devices_with_no_device_model = ["GoControl Thermostat", "New Shortcut", "Test robot", "August lock"] for device in devices: if type(device) in skip_types: self.assertIsNone(device.manufacturer_device_model()) @@ -134,7 +134,7 @@ def test_all_devices_manufacturer_device_id_state_is_valid(self): def test_all_devices_device_manufacturer_is_valid(self): devices = get_devices_from_response_dict(self.response_dict, device_types.ALL_SUPPORTED_DEVICES) - device_with_no_manufacturer = ["GoControl Thermostat", "New Shortcut", "Test robot"] + device_with_no_manufacturer = ["GoControl Thermostat", "New Shortcut", "Test robot", "August lock"] for device in devices: if type(device) is WinkKey: self.assertIsNone(device.device_manufacturer()) @@ -147,7 +147,7 @@ def test_all_devices_device_manufacturer_is_valid(self): def test_all_devices_model_name_is_valid(self): devices = get_devices_from_response_dict(self.response_dict, device_types.ALL_SUPPORTED_DEVICES) - devices_with_no_model_name = ["GoControl Thermostat", "New Shortcut", "Test robot"] + devices_with_no_model_name = ["GoControl Thermostat", "New Shortcut", "Test robot", "August lock"] for device in devices: if type(device) is WinkKey: self.assertIsNone(device.model_name()) From 3e2f956e4a682d9c29d896c041e644f1b470611b Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Tue, 26 Dec 2017 12:47:01 -0500 Subject: [PATCH 159/178] Added Nest cam sensor support. (#100) * Added Nest cam sensor support. --- CHANGELOG.md | 3 + src/pywink/api.py | 8 +- src/pywink/devices/factory.py | 2 +- src/pywink/devices/hub.py | 2 +- src/pywink/devices/light_bulb.py | 1 + src/pywink/devices/thermostat.py | 6 +- src/pywink/test/api_test.py | 2 +- .../test/devices/api_responses/nest_cam.json | 106 ++++++++++++++++++ src/pywink/test/devices/base_test.py | 2 + src/setup.py | 2 +- 10 files changed, 125 insertions(+), 9 deletions(-) create mode 100644 src/pywink/test/devices/api_responses/nest_cam.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e93efe..76d8ec0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 1.7.2 +- Added Nest Cam sensor support and fixed some lint errors + ## 1.7.1 - Extended lock feature can't be performed via local control. Set with online. - Cleaned up imports/tests/style diff --git a/src/pywink/api.py b/src/pywink/api.py index 63139fb..1c19d52 100644 --- a/src/pywink/api.py +++ b/src/pywink/api.py @@ -3,10 +3,11 @@ import logging import urllib.parse +import requests + from pywink.devices import types as device_types from pywink.devices.factory import build_device -import requests try: import urllib3 from urllib3.exceptions import InsecureRequestWarning @@ -152,7 +153,7 @@ def get_device_state(self, device, id_override=None, type_override=None): _LOGGER.debug('%s', response_json) return response_json - # pylint: disable=bare-except + # pylint: disable=bare-except, too-many-locals def local_get_state(self, device, id_override=None, type_override=None): """ Get device state via local API, and fall back to online API. @@ -612,6 +613,9 @@ def get_devices(device_type, end_point="wink_devices"): elif end_point == "robots" or end_point == "scenes" or end_point == "groups": json_data = wink_api_fetch(end_point) return get_devices_from_response_dict(json_data, device_type) + else: + _LOGGER.error("Invalid endpoint %s", end_point) + return {} def get_devices_from_response_dict(response_dict, device_type): diff --git a/src/pywink/devices/factory.py b/src/pywink/devices/factory.py index 44004bc..173412a 100644 --- a/src/pywink/devices/factory.py +++ b/src/pywink/devices/factory.py @@ -91,7 +91,7 @@ def build_device(device_state_as_json, api_interface): elif object_type == device_types.CAMERA: if device_state_as_json.get("device_manufacturer") == "canary": new_objects.append(WinkCanaryCamera(device_state_as_json, api_interface)) - elif device_state_as_json.get("device_manufacturer") == "dropcam": + else: new_objects.extend(__get_subsensors_from_device(device_state_as_json, api_interface)) elif object_type == device_types.AIR_CONDITIONER: new_objects.append(WinkAirConditioner(device_state_as_json, api_interface)) diff --git a/src/pywink/devices/hub.py b/src/pywink/devices/hub.py index e4ff986..b285ee6 100644 --- a/src/pywink/devices/hub.py +++ b/src/pywink/devices/hub.py @@ -72,7 +72,7 @@ def pair_new_device(self, pairing_mode, pairing_mode_duration=60, pairing_device kidde_radio_code_int = int(kidde_radio_code, 2) desired_state = {"kidde_radio_code": kidde_radio_code_int, "pairing_mode": None} except (TypeError, ValueError): - _LOGGER.error("An invalid Kidde radio code was provided. " + kidde_radio_code) + _LOGGER.error("An invalid Kidde radio code was provided. %s", kidde_radio_code) if pairing_device_type_selector is not None: desired_state.update({"pairing_device_type_selector": pairing_device_type_selector}) diff --git a/src/pywink/devices/light_bulb.py b/src/pywink/devices/light_bulb.py index a1273c6..c254fae 100644 --- a/src/pywink/devices/light_bulb.py +++ b/src/pywink/devices/light_bulb.py @@ -163,3 +163,4 @@ def _get_color_as_hue_saturation_brightness(hue_sat): if hue_sat: color_hs_iter = iter(hue_sat) return next(color_hs_iter), next(color_hs_iter), 1 + return None diff --git a/src/pywink/devices/thermostat.py b/src/pywink/devices/thermostat.py index 502210e..2ebdfda 100644 --- a/src/pywink/devices/thermostat.py +++ b/src/pywink/devices/thermostat.py @@ -37,14 +37,14 @@ def away(self): """ nest = self._last_reading.get('users_away', None) ecobee = self.profile() - if nest is None and ecobee is None: - return None - elif nest is not None: + if nest is not None: return nest elif ecobee is not None: if ecobee == "home": return False return True + else: + return None def current_hvac_mode(self): return self._last_reading.get('mode', None) diff --git a/src/pywink/test/api_test.py b/src/pywink/test/api_test.py index 3f0c94d..11f0bd7 100644 --- a/src/pywink/test/api_test.py +++ b/src/pywink/test/api_test.py @@ -109,7 +109,7 @@ def test_get_subscription_key(self): def test_get_all_devices_from_api(self): WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) devices = get_all_devices() - self.assertEqual(len(devices), 70) + self.assertEqual(len(devices), 76) lights = get_light_bulbs() for light in lights: self.assertTrue(isinstance(light, WinkLightBulb)) diff --git a/src/pywink/test/devices/api_responses/nest_cam.json b/src/pywink/test/devices/api_responses/nest_cam.json new file mode 100644 index 0000000..160b441 --- /dev/null +++ b/src/pywink/test/devices/api_responses/nest_cam.json @@ -0,0 +1,106 @@ +{ + "object_type": "camera", + "object_id": "180660", + "uuid": "589d8030-e993-433f-b9d9-df8b308d6380", + "icon_id": null, + "icon_code": null, + "desired_state": { + "capturing_video": false + }, + "last_reading": { + "connection": true, + "connection_updated_at": 1514258320.157, + "capturing_audio": true, + "capturing_audio_updated_at": 1514258320.157, + "capturing_video": false, + "capturing_video_updated_at": 1514258320.157, + "motion": false, + "motion_updated_at": 1514258320.157, + "loudness": false, + "loudness_updated_at": 1514258346.0296662, + "has_recording_plan": false, + "has_recording_plan_updated_at": 1514258320.157, + "snapshot_url": "https://www.dropcam.com/api/wwn.get_snapshot/CjZLUUlDMjViUVRNNmlEVWRVanVzZWdXZGRfRGJLODYxVnQ5VE1ES2ExYkMxTWNkZFVLRzIydncSFlBuR3dFZFYtRXhrM0pyWXFiVDEyX1EaNkZlQnhYWmR4RnZ2bU9PbFdsTkpLaHlpUnhGMVFQRTJjMGtYSFBENUhOYVF3VGRsSndybFJMQQ", + "snapshot_url_updated_at": 1514258320.157, + "motion_true": "N/A", + "motion_true_updated_at": 1514256558.012, + "loudness_true": null, + "loudness_true_updated_at": null, + "desired_capturing_video_updated_at": 1514258320.3500347, + "connection_changed_at": 1514156780.0832539, + "capturing_audio_changed_at": 1514156780.3520112, + "capturing_video_changed_at": 1514258320.157, + "has_recording_plan_changed_at": 1514156780.3520112, + "snapshot_url_changed_at": 1514258320.157, + "motion_changed_at": 1514256566.2055306, + "loudness_changed_at": 1514258346.0296662, + "motion_true_changed_at": 1514256558.012, + "desired_capturing_video_changed_at": 1514258320.3500347 + }, + "subscription": { + "pubnub": { + "subscribe_key": "sub-c-f7bf7f7e-0542-11a5e8-02ee2ddafe", + "channel": "bb09aa7d1ac2319b0923289bda8d77877bf|camera-1800|user-7684" + } + }, + "camera_id": "180660", + "name": "Backyard Camera", + "locale": "en_us", + "units": {}, + "created_at": 1514156780, + "hidden_at": null, + "capabilities": { + "fields": [ + { + "type": "boolean", + "field": "connection", + "mutability": "read-only" + }, + { + "type": "boolean", + "field": "capturing_audio", + "mutability": "read-only" + }, + { + "type": "boolean", + "field": "capturing_video", + "mutability": "read-write" + }, + { + "type": "boolean", + "field": "motion", + "mutability": "read-only" + }, + { + "type": "boolean", + "field": "loudness", + "mutability": "read-only" + }, + { + "type": "boolean", + "field": "has_recording_plan", + "mutability": "read-only" + }, + { + "type": "string", + "field": "snapshot_url", + "mutability": "read-only" + } + ], + "home_security_device": true, + "excludes_robot_effect": true + }, + "manufacturer_device_model": "nest", + "manufacturer_device_id": "KQIC25bQTM6iDUdUjusegWdd_DbK861Vt9TMDKa1bC1McddUKG22vw", + "device_manufacturer": "nest", + "model_name": "Nest Cam", + "upc_id": "754", + "upc_code": "nest_cam", + "primary_upc_code": "nest_cam", + "linked_service_id": "914890", + "lat_lng": [ + null, + null + ], + "location": "" +} diff --git a/src/pywink/test/devices/base_test.py b/src/pywink/test/devices/base_test.py index d226ba7..53b78c0 100644 --- a/src/pywink/test/devices/base_test.py +++ b/src/pywink/test/devices/base_test.py @@ -95,6 +95,8 @@ def test_all_devices_battery_is_valid(self): self.assertIsNone(device.battery_level()) elif device.device_manufacturer() == "dropcam": self.assertIsNone(device.battery_level()) + elif device.model_name() == "Nest Cam": + self.assertIsNone(device.battery_level()) elif device._last_reading.get('external_power'): self.assertIsNone(device.battery_level()) else: diff --git a/src/setup.py b/src/setup.py index 8c82c62..5354bb1 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='1.7.1', + version='1.7.2', description='Access Wink devices via the Wink API', url='http://github.com/python-wink/python-wink', author='Brad Johnson, William Scanlon', From 766e35f715111379f4e9b43ae20aea9c282f3366 Mon Sep 17 00:00:00 2001 From: Keith Pine Date: Tue, 26 Dec 2017 09:50:31 -0800 Subject: [PATCH 160/178] Add thermostat fields heat_on/cool_on (#99) Similar to fan_on, these expose the "heat_active" and "cool_active" last reading fields. --- src/pywink/devices/thermostat.py | 6 ++++++ .../api_responses/sensi_thermostat.json | 2 +- src/pywink/test/devices/thermostat_test.py | 20 +++++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/pywink/devices/thermostat.py b/src/pywink/devices/thermostat.py index 2ebdfda..796eec9 100644 --- a/src/pywink/devices/thermostat.py +++ b/src/pywink/devices/thermostat.py @@ -193,3 +193,9 @@ def set_temperature(self, min_set_point=None, max_set_point=None): }) self._update_state_from_response(response) + + def cool_on(self): + return self._last_reading.get('cool_active', False) + + def heat_on(self): + return self._last_reading.get('heat_active', False) diff --git a/src/pywink/test/devices/api_responses/sensi_thermostat.json b/src/pywink/test/devices/api_responses/sensi_thermostat.json index e2c0281..6b10d0e 100644 --- a/src/pywink/test/devices/api_responses/sensi_thermostat.json +++ b/src/pywink/test/devices/api_responses/sensi_thermostat.json @@ -43,7 +43,7 @@ "humidity_updated_at":1477511203.5183966, "cool_active":false, "cool_active_updated_at":1477511203.5183966, - "heat_active":false, + "heat_active":true, "heat_active_updated_at":1477511203.5183966, "aux_active":false, "aux_active_updated_at":1477511203.5183966, diff --git a/src/pywink/test/devices/thermostat_test.py b/src/pywink/test/devices/thermostat_test.py index 7e31c0a..3e8254c 100644 --- a/src/pywink/test/devices/thermostat_test.py +++ b/src/pywink/test/devices/thermostat_test.py @@ -322,3 +322,23 @@ def test_thermostat_is_on(self): response_dict["data"] = device_list thermostat = get_devices_from_response_dict(response_dict, device_types.THERMOSTAT)[0] self.assertTrue(thermostat.is_on()) + + def test_thermostat_heat_on(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/sensi_thermostat.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + thermostat = get_devices_from_response_dict(response_dict, device_types.THERMOSTAT)[0] + self.assertTrue(thermostat.heat_on()) + + def test_thermostat_cool_on(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/sensi_thermostat.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + thermostat = get_devices_from_response_dict(response_dict, device_types.THERMOSTAT)[0] + self.assertFalse(thermostat.cool_on()) From b69878afa5906678e8828aa16c39ff5e4a45716a Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Sat, 20 Jan 2018 11:34:17 -0500 Subject: [PATCH 161/178] Return None for heat_on and cool_on if not supported by Thermostat (#101) --- CHANGELOG.md | 3 +++ src/pywink/devices/thermostat.py | 4 ++-- src/setup.py | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76d8ec0..3652108 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 1.7.3 +- Return none for heat_on and cool_on if not supported by thermostat + ## 1.7.2 - Added Nest Cam sensor support and fixed some lint errors diff --git a/src/pywink/devices/thermostat.py b/src/pywink/devices/thermostat.py index 796eec9..4809828 100644 --- a/src/pywink/devices/thermostat.py +++ b/src/pywink/devices/thermostat.py @@ -195,7 +195,7 @@ def set_temperature(self, min_set_point=None, max_set_point=None): self._update_state_from_response(response) def cool_on(self): - return self._last_reading.get('cool_active', False) + return self._last_reading.get('cool_active') def heat_on(self): - return self._last_reading.get('heat_active', False) + return self._last_reading.get('heat_active') diff --git a/src/setup.py b/src/setup.py index 5354bb1..936b60e 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='1.7.2', + version='1.7.3', description='Access Wink devices via the Wink API', url='http://github.com/python-wink/python-wink', author='Brad Johnson, William Scanlon', From 09bbca2b35ea0fb78efdc2bd57ac8bff63f99b18 Mon Sep 17 00:00:00 2001 From: Vignesh Venkatasubramanian Date: Wed, 6 Jun 2018 12:10:47 -0700 Subject: [PATCH 162/178] Expose GE Z-Wave in wall fan switch as a fan Wink recognizes this as a dimmer switch for some reason. But in reality it is just a fan switch with 3 speeds (translating to brightness levels of 33%, 66% and 100% in the dimmer switch). This patch extends WinkFan to specialize the case of GE Z-Wave in wall fan switch and exposes it as a fan instead of a light bulb. I've also added a sample json for the tests. This way home assistant will expose this as a fan without any changes on the user's part. --- src/pywink/api.py | 4 +- src/pywink/devices/factory.py | 19 +++++- src/pywink/devices/fan.py | 63 ++++++++++++++++++ src/pywink/test/api_test.py | 25 ++++--- .../devices/api_responses/ge_zwave_fan.json | 65 +++++++++++++++++++ src/pywink/test/devices/base_test.py | 7 +- src/pywink/test/devices/ge_zwave_fan_test.py | 47 ++++++++++++++ 7 files changed, 213 insertions(+), 17 deletions(-) create mode 100644 src/pywink/test/devices/api_responses/ge_zwave_fan.json create mode 100644 src/pywink/test/devices/ge_zwave_fan_test.py diff --git a/src/pywink/api.py b/src/pywink/api.py index 1c19d52..de32041 100644 --- a/src/pywink/api.py +++ b/src/pywink/api.py @@ -6,7 +6,7 @@ import requests from pywink.devices import types as device_types -from pywink.devices.factory import build_device +from pywink.devices.factory import build_device, get_object_type try: import urllib3 @@ -629,7 +629,7 @@ def get_devices_from_response_dict(response_dict, device_type): api_interface = WinkApiInterface() for item in items: - if item.get("object_type") in device_type: + if get_object_type(item) in device_type: _devices = build_device(item, api_interface) for device in _devices: devices.append(device) diff --git a/src/pywink/devices/factory.py b/src/pywink/devices/factory.py index 173412a..f756a91 100644 --- a/src/pywink/devices/factory.py +++ b/src/pywink/devices/factory.py @@ -13,7 +13,7 @@ from pywink.devices.siren import WinkSiren from pywink.devices.key import WinkKey from pywink.devices.thermostat import WinkThermostat -from pywink.devices.fan import WinkFan +from pywink.devices.fan import WinkFan, WinkGeZwaveFan from pywink.devices.remote import WinkRemote from pywink.devices.hub import WinkHub from pywink.devices.powerstrip import WinkPowerStrip, WinkPowerStripOutlet @@ -35,7 +35,7 @@ # pylint: disable=too-many-branches, too-many-statements def build_device(device_state_as_json, api_interface): # This is used to determine what type of object to create - object_type = device_state_as_json.get("object_type") + object_type = get_object_type(device_state_as_json) new_objects = [] if object_type == device_types.LIGHT_BULB: @@ -63,7 +63,10 @@ def build_device(device_state_as_json, api_interface): elif object_type == device_types.THERMOSTAT: new_objects.append(WinkThermostat(device_state_as_json, api_interface)) elif object_type == device_types.FAN: - new_objects.append(WinkFan(device_state_as_json, api_interface)) + if __is_ge_zwave_fan(device_state_as_json): + new_objects.append(WinkGeZwaveFan(device_state_as_json, api_interface)) + else: + new_objects.append(WinkFan(device_state_as_json, api_interface)) elif object_type == device_types.REMOTE: # The lutron Pico remote doesn't follow the API spec and # provides no benefit as a device in this library. @@ -116,6 +119,16 @@ def build_device(device_state_as_json, api_interface): return new_objects +def get_object_type(device_state_as_json): + if __is_ge_zwave_fan(device_state_as_json): + return device_types.FAN + return device_state_as_json.get("object_type") + + +def __is_ge_zwave_fan(device_state_as_json): + return device_state_as_json.get("manufacturer_device_model") == "ge_jasco_in_wall_fan" + + def __get_subsensors_from_device(item, api_interface): sensor_types = item.get('capabilities', {}).get('fields', []) sensor_types.extend(item.get('capabilities', {}).get('sensor_types', [])) diff --git a/src/pywink/devices/fan.py b/src/pywink/devices/fan.py index c76fec4..b35b9d1 100644 --- a/src/pywink/devices/fan.py +++ b/src/pywink/devices/fan.py @@ -94,3 +94,66 @@ def set_fan_timer(self, timer): }) self._update_state_from_response(resp) + + +# pylint: disable=too-many-public-methods +class WinkGeZwaveFan(WinkFan): + """ + Represents a GE-Z-Wave In wall fan switch. Wink recognizes this as a light + bulb dimmer switch but in reality it is a fan with 3 speeds. + """ + _to_brightness = { + "lowest": 0.33, + "low": 0.33, + "medium": 0.66, + "high": 1.0, + "auto": 0.66 + } + + _to_speed = { + 0.33: "low", + 0.66: "medium", + 1.0: "high" + } + + def fan_speeds(self): + return ["low", "medium", "high"] + + def fan_directions(self): + return [] + + def fan_timer_range(self): + return [] + + def current_fan_speed(self): + return self._to_speed[self._last_reading.get('brightness', 0.33)] + + def current_fan_direction(self): + return None + + def current_timer(self): + return None + + def set_state(self, state, speed=None): + """ + :param state: bool + :param speed: a string one of ["lowest", "low", + "medium", "high", "auto"] defaults to last speed + :return: nothing + """ + desired_state = {"powered": state} + if state: + brightness = self._to_brightness.get(speed or self.current_fan_speed(), 0.33) + desired_state.update({'brightness': brightness}) + + response = self.api_interface.set_device_state(self, { + "desired_state": desired_state + }) + + self._update_state_from_response(response) + + def set_fan_direction(self, direction): + pass + + def set_fan_timer(self, timer): + pass diff --git a/src/pywink/test/api_test.py b/src/pywink/test/api_test.py index 11f0bd7..118b1e6 100644 --- a/src/pywink/test/api_test.py +++ b/src/pywink/test/api_test.py @@ -23,7 +23,7 @@ from pywink.devices.garage_door import WinkGarageDoor from pywink.devices.shade import WinkShade from pywink.devices.siren import WinkSiren -from pywink.devices.fan import WinkFan +from pywink.devices.fan import WinkFan, WinkGeZwaveFan from pywink.devices.thermostat import WinkThermostat from pywink.devices.button import WinkButton from pywink.devices.gang import WinkGang @@ -109,7 +109,7 @@ def test_get_subscription_key(self): def test_get_all_devices_from_api(self): WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) devices = get_all_devices() - self.assertEqual(len(devices), 76) + self.assertEqual(len(devices), 77) lights = get_light_bulbs() for light in lights: self.assertTrue(isinstance(light, WinkLightBulb)) @@ -161,7 +161,7 @@ def test_get_all_devices_from_api(self): self.assertTrue(isinstance(hub, WinkHub)) fans = get_fans() for fan in fans: - self.assertTrue(isinstance(fan, WinkFan)) + self.assertTrue(isinstance(fan, WinkFan) or isinstance(fan, WinkGeZwaveFan)) buttons = get_buttons() for button in buttons: self.assertTrue(isinstance(button, WinkButton)) @@ -490,13 +490,20 @@ def test_get_fan_updated_states_from_api(self): old_states = {} for device in devices: device.api_interface = self.api_interface - device.set_state(True, "auto") - device.set_fan_direction("reverse") - device.set_fan_timer(300) + if isinstance(device, WinkGeZwaveFan): + device.set_state(True, "high") + else: + device.set_state(True, "auto") + device.set_fan_direction("reverse") + device.set_fan_timer(300) device.update_state() - self.assertEqual(device.current_fan_speed(), "auto") - self.assertEqual(device.current_fan_direction(), "reverse") - self.assertEqual(device.current_timer(), 300) + for device in devices: + if isinstance(device, WinkGeZwaveFan): + self.assertEqual(device.current_fan_speed(), "high") + else: + self.assertEqual(device.current_fan_speed(), "auto") + self.assertEqual(device.current_fan_direction(), "reverse") + self.assertEqual(device.current_timer(), 300) def test_get_propane_tank_updated_states_from_api(self): diff --git a/src/pywink/test/devices/api_responses/ge_zwave_fan.json b/src/pywink/test/devices/api_responses/ge_zwave_fan.json new file mode 100644 index 0000000..371be46 --- /dev/null +++ b/src/pywink/test/devices/api_responses/ge_zwave_fan.json @@ -0,0 +1,65 @@ +{ + "device_manufacturer":"ge", + "upc_id":"11", + "manufacturer_device_id":null, + "capabilities":{ + + }, + "order":0, + "linked_service_id":null, + "model_name":"In-Wall Fan Control", + "hidden_at":null, + "light_bulb_id":"239549", + "locale":"en_us", + "icon_id":"18", + "triggers":[ + + ], + "icon_code":"light_bulb-fan_light", + "uuid":"4bf38640-ad82-xxxxxxx-yyyyyy", + "radio_type":"zwave", + "gang_id":null, + "local_id":"25", + "object_type":"light_bulb", + "location":"", + "upc_code":"ge_jasco-in-wall-fan2", + "units":{ + + }, + "manufacturer_device_model":"ge_jasco_in_wall_fan", + "created_at":1504074521, + "object_id":"2928587", + "updated_at":1510211919, + "lat_lng":[ + null, + null + ], + "name":"Ceiling Fan", + "primary_upc_code":"ge_jasco-in-wall-fan", + "hub_id":"302528", + "last_reading":{ + "powered":false, + "desired_brightness_updated_at":1528308568.0060544, + "desired_brightness_changed_at":1528308568.0060544, + "connection_updated_at":1528308785.0084982, + "powered_updated_at":1528308785.0084982, + "brightness_updated_at":1528308785.0084982, + "connection":true, + "desired_powered_changed_at":1528308566.4751642, + "connection_changed_at":1526605027.361241, + "desired_powered_updated_at":1528308739.3037217, + "powered_changed_at":1528308768.4366276, + "brightness_changed_at":1528308567.9741776, + "brightness":0.66 + }, + "desired_state":{ + "powered":false, + "brightness":0.66 + }, + "subscription": { + "pubnub": { + "subscribe_key": "sub-c-f7bf7f7e-054XXXXXXXXXXXXXX", + "channel": "024254c58e06548f0aXXXXXXXXXXXXXXXXX0ce|light_bulb-239549|user-123456" + } + } +} diff --git a/src/pywink/test/devices/base_test.py b/src/pywink/test/devices/base_test.py index 53b78c0..c9868c6 100644 --- a/src/pywink/test/devices/base_test.py +++ b/src/pywink/test/devices/base_test.py @@ -12,7 +12,7 @@ from pywink.devices.siren import WinkSiren from pywink.devices.eggtray import WinkEggtray from pywink.devices.remote import WinkRemote -from pywink.devices.fan import WinkFan +from pywink.devices.fan import WinkFan, WinkGeZwaveFan from pywink.devices.binary_switch import WinkBinarySwitch from pywink.devices.hub import WinkHub from pywink.devices.light_bulb import WinkLightBulb @@ -82,7 +82,7 @@ def test_all_devices_state_is_not_none(self): def test_all_devices_battery_is_valid(self): devices = get_devices_from_response_dict(self.response_dict, device_types.ALL_SUPPORTED_DEVICES) - skip_types = [WinkFan, WinkPorkfolioBalanceSensor, WinkPorkfolioNose, WinkBinarySwitch, WinkHub, + skip_types = [WinkFan, WinkGeZwaveFan, WinkPorkfolioBalanceSensor, WinkPorkfolioNose, WinkBinarySwitch, WinkHub, WinkLightBulb, WinkThermostat, WinkKey, WinkPowerStrip, WinkPowerStripOutlet, WinkRemote, WinkShade, WinkSprinkler, WinkButton, WinkGang, WinkCanaryCamera, WinkAirConditioner, WinkScene, WinkRobot, WinkWaterHeater] @@ -118,7 +118,8 @@ def test_all_devices_manufacturer_device_model_state_is_valid(self): def test_all_devices_manufacturer_device_id_state_is_valid(self): devices = get_devices_from_response_dict(self.response_dict, device_types.ALL_SUPPORTED_DEVICES) skip_types = [WinkKey, WinkPowerStrip, WinkPowerStripOutlet, WinkPorkfolioBalanceSensor, WinkPorkfolioNose, - WinkSiren, WinkEggtray, WinkRemote, WinkButton, WinkAirConditioner, WinkPropaneTank] + WinkSiren, WinkEggtray, WinkRemote, WinkButton, WinkAirConditioner, WinkPropaneTank, + WinkGeZwaveFan] skip_manufactuer_device_models = ["linear_wadwaz_1", "linear_wapirz_1", "aeon_labs_dsb45_zwus", "wink_hub", "wink_hub2", "sylvania_sylvania_ct", "ge_bulb", "quirky_ge_spotter", "schlage_zwave_lock", "home_decorators_home_decorators_fan", "sylvania_sylvania_rgbw", "somfy_bali", "wink_relay_sensor", "wink_project_one", "kidde_smoke_alarm", diff --git a/src/pywink/test/devices/ge_zwave_fan_test.py b/src/pywink/test/devices/ge_zwave_fan_test.py new file mode 100644 index 0000000..596b3b4 --- /dev/null +++ b/src/pywink/test/devices/ge_zwave_fan_test.py @@ -0,0 +1,47 @@ +import json +import os +import unittest + +from unittest.mock import MagicMock + +from pywink.api import get_devices_from_response_dict +from pywink.devices import types as device_types + + +class GeZwaveFanTests(unittest.TestCase): + + def setUp(self): + super(GeZwaveFanTests, self).setUp() + self.api_interface = MagicMock() + device_list = [] + self.response_dict = {} + _json_file = open('{}/api_responses/ge_zwave_fan.json'.format(os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + self.response_dict["data"] = device_list + + def test_fan_speeds(self): + fan = get_devices_from_response_dict(self.response_dict, device_types.FAN)[0] + has_speeds = fan.fan_speeds() + self.assertEqual(len(has_speeds), 3) + speeds = ['low', 'medium', 'high'] + for speed in has_speeds: + self.assertTrue(speed in speeds) + + def test_fan_directions(self): + fan = get_devices_from_response_dict(self.response_dict, device_types.FAN)[0] + has_directions = fan.fan_directions() + self.assertEqual(len(has_directions), 0) + + def test_fan_timer_range(self): + fan = get_devices_from_response_dict(self.response_dict, device_types.FAN)[0] + has_timer_range = fan.fan_timer_range() + self.assertEqual(len(has_timer_range), 0) + + def test_fan_speed_is_medium(self): + fan = get_devices_from_response_dict(self.response_dict, device_types.FAN)[0] + self.assertEqual(fan.current_fan_speed(), "medium") + + def test_fan_state(self): + fan = get_devices_from_response_dict(self.response_dict, device_types.FAN)[0] + self.assertFalse(fan.state()) From 7a29939d83a74aa380d9acba12ca383bb1c84672 Mon Sep 17 00:00:00 2001 From: Brad Johnson Date: Fri, 8 Jun 2018 09:53:26 -0600 Subject: [PATCH 163/178] Update setup.py --- src/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/setup.py b/src/setup.py index 936b60e..16743b0 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='1.7.3', + version='1.8.0', description='Access Wink devices via the Wink API', url='http://github.com/python-wink/python-wink', author='Brad Johnson, William Scanlon', From 853e3444b9a7f2ab243e8b810318ad8c7a0afb75 Mon Sep 17 00:00:00 2001 From: Brad Johnson Date: Fri, 8 Jun 2018 09:54:04 -0600 Subject: [PATCH 164/178] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3652108..93e9ce2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 1.8.0 +- Special behaviour for recognizing GE fan as fan, rather than dimmable light. + ## 1.7.3 - Return none for heat_on and cool_on if not supported by thermostat From ee0580290b3967e03d952183193587c1822335fe Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Mon, 18 Jun 2018 22:03:10 -0400 Subject: [PATCH 165/178] Support for shade groups. Attempted fix of #102. (#104) * Support for shade groups. Attempted fix of #102. Fixed a crash when the hub id isn't returned. --- .gitignore | 2 + CHANGELOG.md | 3 + src/pywink/__init__.py | 2 +- src/pywink/api.py | 23 +++++- src/pywink/devices/factory.py | 8 +- src/pywink/devices/shade_group.py | 31 ++++++++ src/pywink/test/api_test.py | 30 +++++-- .../api_responses/groups/shade_group.json | 79 +++++++++++++++++++ src/pywink/test/devices/light_bulb_test.py | 2 +- src/pywink/test/devices/shade_test.py | 69 ++++++++++++++++ src/pywink/test/devices/switch_test.py | 2 +- src/setup.py | 2 +- 12 files changed, 236 insertions(+), 17 deletions(-) create mode 100644 src/pywink/devices/shade_group.py create mode 100644 src/pywink/test/devices/api_responses/groups/shade_group.json create mode 100644 src/pywink/test/devices/shade_test.py diff --git a/.gitignore b/.gitignore index 00c91e7..a7f2abb 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ /src/python_wink.egg-info /.coverage /.idea/* + +src/*tst\.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 93e9ce2..2897d10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 1.9.0 +- Added support for fan groups. Refresh Wink token on first device fetch. + ## 1.8.0 - Special behaviour for recognizing GE fan as fan, rather than dimmable light. diff --git a/src/pywink/__init__.py b/src/pywink/__init__.py index 21f5c34..8d24239 100644 --- a/src/pywink/__init__.py +++ b/src/pywink/__init__.py @@ -12,7 +12,7 @@ get_powerstrips, get_shades, get_sirens, \ get_switches, get_thermostats, get_fans, get_air_conditioners, \ get_propane_tanks, get_robots, get_scenes, get_light_groups, \ - get_binary_switch_groups, get_water_heaters + get_binary_switch_groups, get_water_heaters, get_shade_groups from pywink.api import get_all_devices, get_eggtrays, get_sensors, \ get_keys, get_piggy_banks, get_smoke_and_co_detectors, \ diff --git a/src/pywink/api.py b/src/pywink/api.py index de32041..fd60a7a 100644 --- a/src/pywink/api.py +++ b/src/pywink/api.py @@ -101,7 +101,7 @@ def local_set_state(self, device, state, id_override=None, type_override=None): if ALLOW_LOCAL_CONTROL: if device.local_id() is not None: hub = HUBS.get(device.hub_id()) - if hub is None: + if hub is None or hub["token"] is None: return self.set_device_state(device, state, id_override, type_override) else: return self.set_device_state(device, state, id_override, type_override) @@ -171,7 +171,7 @@ def local_get_state(self, device, id_override=None, type_override=None): if ALLOW_LOCAL_CONTROL: if device.local_id() is not None: hub = HUBS.get(device.hub_id()) - if hub is not None: + if hub is not None and hub["token"] is not None: ip = hub["ip"] access_token = hub["token"] else: @@ -573,6 +573,15 @@ def get_binary_switch_groups(): return switch_groups +def get_shade_groups(): + shade_groups = [] + for group in get_devices(device_types.GROUP, "groups"): + # Shades have a position + if group.json_state.get("reading_aggregation").get("position") is not None: + shade_groups.append(group) + return shade_groups + + def get_subscription_key(): response_dict = wink_api_fetch() try: @@ -588,14 +597,20 @@ def get_subscription_key_from_response_dict(device): return None -def wink_api_fetch(end_point='wink_devices'): +def wink_api_fetch(end_point='wink_devices', retry=True): arequest_url = "{}/users/me/{}".format(WinkApiInterface.BASE_URL, end_point) response = requests.get(arequest_url, headers=API_HEADERS) _LOGGER.debug('%s', response) if response.status_code == 200: return response.json() if response.status_code == 401: - raise WinkAPIException("401 Response from Wink API. Maybe Bearer token is expired?") + # Attempt a token refresh and retry the fetch call + if retry: + refresh_access_token() + # Only retry once so pass in False for retry value + return wink_api_fetch(end_point, False) + else: + raise WinkAPIException("401 Response from Wink API.") else: raise WinkAPIException("Unexpected") diff --git a/src/pywink/devices/factory.py b/src/pywink/devices/factory.py index f756a91..4f3dc23 100644 --- a/src/pywink/devices/factory.py +++ b/src/pywink/devices/factory.py @@ -30,6 +30,7 @@ from pywink.devices.light_group import WinkLightGroup from pywink.devices.binary_switch_group import WinkBinarySwitchGroup from pywink.devices.water_heater import WinkWaterHeater +from pywink.devices.shade_group import WinkShadeGroup # pylint: disable=too-many-branches, too-many-statements @@ -107,8 +108,11 @@ def build_device(device_state_as_json, api_interface): elif object_type == device_types.GROUP: # This will skip auto created groups that Wink creates as well has empty groups if device_state_as_json.get("name")[0] not in [".", "@"] and device_state_as_json.get("members"): - # This is a group of swithces - if device_state_as_json.get("reading_aggregation").get("brightness") is None: + # This is a group of shades + if device_state_as_json.get("reading_aggregation").get("position") is not None: + new_objects.append(WinkShadeGroup(device_state_as_json, api_interface)) + # This is a group of switches + elif device_state_as_json.get("reading_aggregation").get("brightness") is None: new_objects.append(WinkBinarySwitchGroup(device_state_as_json, api_interface)) # This is a group of lights else: diff --git a/src/pywink/devices/shade_group.py b/src/pywink/devices/shade_group.py new file mode 100644 index 0000000..5ff0c22 --- /dev/null +++ b/src/pywink/devices/shade_group.py @@ -0,0 +1,31 @@ +from pywink.devices.base import WinkDevice + + +class WinkShadeGroup(WinkDevice): + """ + Represents a Wink shade group. + """ + + def state(self): + """ + Groups states is calculated by Wink in the positions "average" field. + """ + return self.reading_aggregation().get("position").get("average") + + def reading_aggregation(self): + return self.json_state.get("reading_aggregation") + + def available(self): + count = self.reading_aggregation().get("connection").get("true_count") + if count > 0: + return True + return False + + def set_state(self, state): + """ + :param state: a number of 1 ('open') or 0 ('close') + :return: nothing + """ + values = {"desired_state": {"position": state}} + response = self.api_interface.set_device_state(self, values) + self._update_state_from_response(response) diff --git a/src/pywink/test/api_test.py b/src/pywink/test/api_test.py index 118b1e6..f29257a 100644 --- a/src/pywink/test/api_test.py +++ b/src/pywink/test/api_test.py @@ -267,6 +267,17 @@ def test_get_light_group_updated_state_from_api(self): for device in devices: self.assertTrue(device.state()) + def test_get_shade_group_updated_state_from_api(self): + WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) + devices = get_shade_groups() + for device in devices: + device.api_interface = self.api_interface + # The Mock API only changes the "position" average + device.set_state(1.0) + device.update_state() + for device in devices: + self.assertEqual(device.state(), 1.0) + def test_all_devices_local_control_id_is_not_decimal(self): WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) devices = get_all_devices() @@ -648,14 +659,19 @@ def set_device_state(self, device, state, id_override=None, type_override=None): for dict_device in GROUPS.get('data'): _object_id = dict_device.get("object_id") if _object_id == object_id: - set_state = state["desired_state"]["powered"] - if set_state: - dict_device["reading_aggregation"]["powered"]["true_count"] = 1 - dict_device["reading_aggregation"]["powered"]["false_count"] = 0 + set_state = state["desired_state"].get("powered") + if set_state is not None: + if set_state: + dict_device["reading_aggregation"]["powered"]["true_count"] = 1 + dict_device["reading_aggregation"]["powered"]["false_count"] = 0 + else: + dict_device["reading_aggregation"]["powered"]["true_count"] = 0 + dict_device["reading_aggregation"]["powered"]["false_count"] = 1 + return_dict["data"] = dict_device else: - dict_device["reading_aggregation"]["powered"]["true_count"] = 0 - dict_device["reading_aggregation"]["powered"]["false_count"] = 1 - return_dict["data"] = dict_device + set_state = state["desired_state"].get("position") + dict_device["reading_aggregation"]["position"]["average"] = set_state + return_dict["data"] = dict_device return return_dict diff --git a/src/pywink/test/devices/api_responses/groups/shade_group.json b/src/pywink/test/devices/api_responses/groups/shade_group.json new file mode 100644 index 0000000..c3be15a --- /dev/null +++ b/src/pywink/test/devices/api_responses/groups/shade_group.json @@ -0,0 +1,79 @@ +{ + "group_id":"98985", + "name":"Sun room shades", + "order":0, + "icon_id":"28", + "members":[ + { + "object_type":"shade", + "object_id":"1889", + "local_id":"36", + "hub_id":"57222", + "blacklisted_readings":[ + + ] + }, + { + "object_type":"shade", + "object_id":"1889", + "local_id":"37", + "hub_id":"57222", + "blacklisted_readings":[ + + ] + }, + { + "object_type":"shade", + "object_id":"1889", + "local_id":"38", + "hub_id":"57222", + "blacklisted_readings":[ + + ] + }, + { + "object_type":"shade", + "object_id":"1944", + "local_id":"46", + "hub_id":"57222", + "blacklisted_readings":[ + + ] + } + ], + "reading_aggregation":{ + "level":{ + "min":null, + "max":null, + "average":null, + "updated_at":null, + "changed_at":null + }, + "connection":{ + "or":true, + "and":true, + "true_count":4, + "false_count":0, + "updated_at":1519745868.4973416, + "changed_at":1518642700.4076774 + }, + "position":{ + "min":0.0, + "max":0.0, + "average":0.0, + "updated_at":1519745868.4973416, + "changed_at":1519745867.0899253 + } + }, + "automation_mode":null, + "hidden_at":null, + "object_type":"group", + "object_id":"98985", + "icon_code":"group-light_group", + "subscription":{ + "pubnub":{ + "subscribe_key":"sub-c-f7bf7f7e-0542-11e3-a5e8-", + "channel":"" + } + } +} \ No newline at end of file diff --git a/src/pywink/test/devices/light_bulb_test.py b/src/pywink/test/devices/light_bulb_test.py index 80e10f0..3a457b4 100644 --- a/src/pywink/test/devices/light_bulb_test.py +++ b/src/pywink/test/devices/light_bulb_test.py @@ -73,7 +73,7 @@ def test_light_groups_are_created_correctly(self): _json_file.close() JSON_DATA["data"] = device_list all_groups = get_devices_from_response_dict(JSON_DATA, device_types.GROUP) - self.assertEqual(2, len(all_groups)) + self.assertEqual(3, len(all_groups)) device_list = [] response_dict = {} _json_file = open('{}/api_responses/groups/light_group.json'.format(os.path.dirname(__file__))) diff --git a/src/pywink/test/devices/shade_test.py b/src/pywink/test/devices/shade_test.py new file mode 100644 index 0000000..ecb3a04 --- /dev/null +++ b/src/pywink/test/devices/shade_test.py @@ -0,0 +1,69 @@ +import json +import os +import unittest + +from unittest.mock import MagicMock + +from pywink.api import get_devices_from_response_dict +from pywink.devices import types as device_types +from pywink.devices.shade_group import WinkShadeGroup + +JSON_DATA = {} + + +class ShadeTests(unittest.TestCase): + + def setUp(self): + super(ShadeTests, self).setUp() + self.api_interface = MagicMock() + + def test_shade_groups_are_created_correctly(self): + all_devices = os.listdir( + '{}/api_responses/groups/'.format(os.path.dirname(__file__))) + device_list = [] + for json_file in all_devices: + if os.path.isfile( + '{}/api_responses/groups/{}'.format(os.path.dirname(__file__), + json_file)): + _json_file = open( + '{}/api_responses/groups/{}'.format(os.path.dirname(__file__), + json_file)) + device_list.append(json.load(_json_file)) + _json_file.close() + JSON_DATA["data"] = device_list + all_groups = get_devices_from_response_dict(JSON_DATA, device_types.GROUP) + self.assertEqual(3, len(all_groups)) + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/groups/shade_group.json'.format( + os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + groups = get_devices_from_response_dict(response_dict, device_types.GROUP) + self.assertEqual(1, len(groups)) + self.assertTrue(isinstance(groups[0], WinkShadeGroup)) + + def test_light_group_state_is_correct(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/groups/shade_group.json'.format( + os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + groups = get_devices_from_response_dict(response_dict, device_types.GROUP) + shade_group = groups[0] + self.assertEqual(shade_group.state(), 0.0) + + def test_light_group_is_available(self): + device_list = [] + response_dict = {} + _json_file = open('{}/api_responses/groups/shade_group.json'.format( + os.path.dirname(__file__))) + device_list.append(json.load(_json_file)) + _json_file.close() + response_dict["data"] = device_list + groups = get_devices_from_response_dict(response_dict, device_types.GROUP) + shade_group = groups[0] + self.assertTrue(shade_group.available()) \ No newline at end of file diff --git a/src/pywink/test/devices/switch_test.py b/src/pywink/test/devices/switch_test.py index 882ac8a..e9efd5e 100644 --- a/src/pywink/test/devices/switch_test.py +++ b/src/pywink/test/devices/switch_test.py @@ -30,7 +30,7 @@ def test_switch_groups_are_created_correctly(self): _json_file.close() JSON_DATA["data"] = device_list all_groups = get_devices_from_response_dict(JSON_DATA, device_types.GROUP) - self.assertEqual(2, len(all_groups)) + self.assertEqual(3, len(all_groups)) device_list = [] response_dict = {} _json_file = open('{}/api_responses/groups/switch_group.json'.format(os.path.dirname(__file__))) diff --git a/src/setup.py b/src/setup.py index 16743b0..91ab331 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='1.8.0', + version='1.9.0', description='Access Wink devices via the Wink API', url='http://github.com/python-wink/python-wink', author='Brad Johnson, William Scanlon', From 3c887d7623a81f1f1de095ad234bc28c15f051d4 Mon Sep 17 00:00:00 2001 From: Vignesh Venkat Date: Thu, 28 Jun 2018 16:35:51 -0700 Subject: [PATCH 166/178] Allow any brightness value for ge z-wave fan (#108) * Allow any brightness value for ge z-wave fan Brightness can be anything from 0.0 to 1.0 when it is set from the wink app's UI. Handle all the values instead of assuming a discrete set of values supported by this library. Fixes #107 * Update unit tests for ge z-wave fan speed --- src/pywink/devices/fan.py | 13 ++++++------- .../test/devices/api_responses/ge_zwave_fan.json | 2 +- src/pywink/test/devices/ge_zwave_fan_test.py | 4 ++-- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/pywink/devices/fan.py b/src/pywink/devices/fan.py index b35b9d1..8fc0a55 100644 --- a/src/pywink/devices/fan.py +++ b/src/pywink/devices/fan.py @@ -110,12 +110,6 @@ class WinkGeZwaveFan(WinkFan): "auto": 0.66 } - _to_speed = { - 0.33: "low", - 0.66: "medium", - 1.0: "high" - } - def fan_speeds(self): return ["low", "medium", "high"] @@ -126,7 +120,12 @@ def fan_timer_range(self): return [] def current_fan_speed(self): - return self._to_speed[self._last_reading.get('brightness', 0.33)] + brightness = self._last_reading.get('brightness', 0.33) + if brightness <= 0.33: + return "low" + elif brightness <= 0.66: + return "medium" + return "high" def current_fan_direction(self): return None diff --git a/src/pywink/test/devices/api_responses/ge_zwave_fan.json b/src/pywink/test/devices/api_responses/ge_zwave_fan.json index 371be46..374a4d3 100644 --- a/src/pywink/test/devices/api_responses/ge_zwave_fan.json +++ b/src/pywink/test/devices/api_responses/ge_zwave_fan.json @@ -50,7 +50,7 @@ "desired_powered_updated_at":1528308739.3037217, "powered_changed_at":1528308768.4366276, "brightness_changed_at":1528308567.9741776, - "brightness":0.66 + "brightness":0.1 }, "desired_state":{ "powered":false, diff --git a/src/pywink/test/devices/ge_zwave_fan_test.py b/src/pywink/test/devices/ge_zwave_fan_test.py index 596b3b4..af2c437 100644 --- a/src/pywink/test/devices/ge_zwave_fan_test.py +++ b/src/pywink/test/devices/ge_zwave_fan_test.py @@ -38,9 +38,9 @@ def test_fan_timer_range(self): has_timer_range = fan.fan_timer_range() self.assertEqual(len(has_timer_range), 0) - def test_fan_speed_is_medium(self): + def test_fan_speed_is_low(self): fan = get_devices_from_response_dict(self.response_dict, device_types.FAN)[0] - self.assertEqual(fan.current_fan_speed(), "medium") + self.assertEqual(fan.current_fan_speed(), "low") def test_fan_state(self): fan = get_devices_from_response_dict(self.response_dict, device_types.FAN)[0] From 89ce81205bcebb97f26b14c4a7a3a0abc98b4427 Mon Sep 17 00:00:00 2001 From: Brad Johnson Date: Thu, 28 Jun 2018 21:05:23 -0600 Subject: [PATCH 167/178] Up version --- CHANGELOG.md | 3 +++ src/setup.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2897d10..4a47c26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 1.9.1 +- Support all float values from "dimmable fans". + ## 1.9.0 - Added support for fan groups. Refresh Wink token on first device fetch. diff --git a/src/setup.py b/src/setup.py index 91ab331..0f259e1 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='1.9.0', + version='1.9.1', description='Access Wink devices via the Wink API', url='http://github.com/python-wink/python-wink', author='Brad Johnson, William Scanlon', From 946d119d80f58a4fdc0e5e4d9f516cedbe11e676 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Sun, 2 Sep 2018 09:17:48 -0400 Subject: [PATCH 168/178] [WIP] Support for the Quirky Nimbus (#111) Support for the Quirky Nimbus --- .travis.yml | 18 +- CHANGELOG.md | 3 + src/pywink/__init__.py | 3 +- src/pywink/api.py | 78 ++++- src/pywink/devices/air_conditioner.py | 2 +- src/pywink/devices/base.py | 2 +- src/pywink/devices/binary_switch.py | 2 +- src/pywink/devices/binary_switch_group.py | 2 +- src/pywink/devices/button.py | 2 +- src/pywink/devices/camera.py | 2 +- src/pywink/devices/cloud_clock.py | 315 ++++++++++++++++++ src/pywink/devices/eggtray.py | 2 +- src/pywink/devices/factory.py | 89 +++-- src/pywink/devices/fan.py | 4 +- src/pywink/devices/gang.py | 2 +- src/pywink/devices/garage_door.py | 2 +- src/pywink/devices/hub.py | 2 +- src/pywink/devices/key.py | 2 +- src/pywink/devices/light_bulb.py | 2 +- src/pywink/devices/light_group.py | 2 +- src/pywink/devices/lock.py | 2 +- src/pywink/devices/piggy_bank.py | 10 +- src/pywink/devices/powerstrip.py | 11 +- src/pywink/devices/propane_tank.py | 2 +- src/pywink/devices/remote.py | 2 +- src/pywink/devices/robot.py | 2 +- src/pywink/devices/scene.py | 2 +- src/pywink/devices/sensor.py | 2 +- src/pywink/devices/shade.py | 2 +- src/pywink/devices/shade_group.py | 2 +- src/pywink/devices/siren.py | 2 +- src/pywink/devices/smoke_detector.py | 2 +- src/pywink/devices/sprinkler.py | 2 +- src/pywink/devices/thermostat.py | 7 +- src/pywink/devices/types.py | 4 +- src/pywink/devices/water_heater.py | 2 +- src/pywink/test/api_test.py | 123 ++++--- .../test/devices/api_responses/nimbus.json | 226 +++++++++++++ src/pywink/test/devices/base_test.py | 65 ++-- src/pywink/test/devices/nimbus_test.py | 119 +++++++ src/setup.py | 2 +- 41 files changed, 959 insertions(+), 168 deletions(-) create mode 100644 src/pywink/devices/cloud_clock.py create mode 100644 src/pywink/test/devices/api_responses/nimbus.json create mode 100644 src/pywink/test/devices/nimbus_test.py diff --git a/.travis.yml b/.travis.yml index d8c4d65..8baa847 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,13 +2,21 @@ sudo: false language: python cache: pip python: - - 3.4 - - 3.5 +- 3.4 +- 3.5 install: - - script/before_install +- script/before_install script: - - script/test +- script/test after_success: - - coveralls +- coveralls matrix: fast_finish: true +deploy: + provider: pypi + distributions: sdist + user: w1ll1am23 + password: + secure: kI2djVwVrBKbAVEcTzOStKXHL1TSuXtTD9pTWtBJF8o/yFlib7HMWPo6p2PY9jEQK82ADo7dUVZphpqshteM8vjKSsn8pDjK+Tzt2uwpKhRpu63QxJVKCQ+GrZ9QJPrHSA4t6feZsq8lK650/xht/yXMeV2pNCRJxRiLJNxhChNOasSOEHtRxBXrZ87y5a/ZjKWzl1Q48FhYjIpdV35eDoeHVOtV1mDED2Zqypqtm39FFtL2kh7GjkAzq6xEwXcwBBkthkhsSF8RQ4MJ4cyN6Z8mzKektGVCwQif8gYsVwDhwjNQhaagoW4oI/AvQ8wLFenmrV8i68LK61InCsZFiImtgufp8t1p7i5Bclxfby0ZPvYw/MrL0xnxbOppbRZmUwi2aV5xI8aVzpOponPq/mXHqLZ952Cy2E/jEl3lO5y2x2iydbSlaMMcceMAXdTHjqEf+AIJPXsOjVzBqpyoA7HpfMIA0x49FiIKRj1Tyx7VR7VdXBdpS5d5tnLrVAtnQI6zIa84btUPAGT2c5Nuk8k/M3DI4Dn0X48NwGXa/oimNaniAAeYPeKoMEfI99gH17D3volBLioVR3OYQGGbLubMojLQxS4kcxm/Qog4FMU89boHQxCoofAGmT8DC3tphGhrzz8NmpPUSul5nE3QVHl+DTCbzHe7oHvIoiT0uf4= + on: + branch: master diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a47c26..183fb39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 2.0.0 +- Support for Quirky Nimbus + ## 1.9.1 - Support all float values from "dimmable fans". diff --git a/src/pywink/__init__.py b/src/pywink/__init__.py index 8d24239..5389198 100644 --- a/src/pywink/__init__.py +++ b/src/pywink/__init__.py @@ -12,7 +12,8 @@ get_powerstrips, get_shades, get_sirens, \ get_switches, get_thermostats, get_fans, get_air_conditioners, \ get_propane_tanks, get_robots, get_scenes, get_light_groups, \ - get_binary_switch_groups, get_water_heaters, get_shade_groups + get_binary_switch_groups, get_water_heaters, get_shade_groups, \ + get_cloud_clocks from pywink.api import get_all_devices, get_eggtrays, get_sensors, \ get_keys, get_piggy_banks, get_smoke_and_co_detectors, \ diff --git a/src/pywink/api.py b/src/pywink/api.py index fd60a7a..152a64b 100644 --- a/src/pywink/api.py +++ b/src/pywink/api.py @@ -5,8 +5,8 @@ import requests -from pywink.devices import types as device_types -from pywink.devices.factory import build_device, get_object_type +from .devices import types as device_types +from .devices.factory import build_device, get_object_type try: import urllib3 @@ -31,7 +31,7 @@ _LOGGER = logging.getLogger(__name__) -class WinkApiInterface(object): +class WinkApiInterface: BASE_URL = "https://api.wink.com" api_headers = API_HEADERS @@ -289,6 +289,57 @@ def create_lock_key(self, device, new_device_json, id_override=None, type_overri except requests.exceptions.RequestException: return None + def create_cloud_clock_alarm(self, device, new_device_json, id_override=None, type_override=None): + """ + Create a new alarm on the provided Nimbus. + + Args: + device (WinkDevice): The device the change is being requested for. + new_device_json (String): The JSON string required to create the device. + id_override (String, optional): A device ID used to override the + passed in device's ID. Used to make changes on sub-devices. + i.e. Outlet in a Powerstrip. The Parent device's ID. + type_override (String, optional): Used to override the device type + when a device inherits from a device other than WinkDevice. + Returns: + response_json (Dict): The API's response in dictionary format + """ + object_id = id_override or device.object_id() + object_type = type_override or device.object_type() + url_string = "{}/{}s/{}/alarms".format(self.BASE_URL, + object_type, + object_id) + try: + arequest = requests.post(url_string, + data=json.dumps(new_device_json), + headers=API_HEADERS) + response_json = arequest.json() + return response_json + except requests.exceptions.RequestException: + return None + + def piggy_bank_deposit(self, device, _json): + """ + Args: + device (WinkPorkfolioBalanceSensor): The piggy bank device to deposit to/withdrawal from. + _json (String): The JSON string to perform the deposit/withdrawal. + Returns: + response_json (Dict): The API's response in dictionary format + """ + url_string = "{}/{}s/{}/deposits".format(self.BASE_URL, + device.object_type(), + device.object_id()) + print(url_string) + try: + arequest = requests.post(url_string, + data=json.dumps(_json), + headers=API_HEADERS) + response_json = arequest.json() + print(json.dumps(response_json, indent=4, sort_keys=True)) + return response_json + except requests.exceptions.RequestException: + return None + def disable_local_control(): global ALLOW_LOCAL_CONTROL @@ -555,6 +606,10 @@ def get_water_heaters(): return get_devices(device_types.WATER_HEATER) +def get_cloud_clocks(): + return get_devices(device_types.CLOUD_CLOCK) + + def get_light_groups(): light_groups = [] for group in get_devices(device_types.GROUP, "groups"): @@ -609,10 +664,8 @@ def wink_api_fetch(end_point='wink_devices', retry=True): refresh_access_token() # Only retry once so pass in False for retry value return wink_api_fetch(end_point, False) - else: - raise WinkAPIException("401 Response from Wink API.") - else: - raise WinkAPIException("Unexpected") + raise WinkAPIException("401 Response from Wink API.") + raise WinkAPIException("Unexpected") def get_devices(device_type, end_point="wink_devices"): @@ -625,12 +678,11 @@ def get_devices(device_type, end_point="wink_devices"): ALL_DEVICES = wink_api_fetch(end_point) LAST_UPDATE = now return get_devices_from_response_dict(ALL_DEVICES, device_type) - elif end_point == "robots" or end_point == "scenes" or end_point == "groups": + if end_point in ("robots", "scenes", "groups"): json_data = wink_api_fetch(end_point) return get_devices_from_response_dict(json_data, device_type) - else: - _LOGGER.error("Invalid endpoint %s", end_point) - return {} + _LOGGER.error("Invalid endpoint %s", end_point) + return {} def get_devices_from_response_dict(response_dict, device_type): @@ -642,9 +694,11 @@ def get_devices_from_response_dict(response_dict, device_type): devices = [] api_interface = WinkApiInterface() + check_list = isinstance(device_type, (list,)) for item in items: - if get_object_type(item) in device_type: + if (check_list and get_object_type(item) in device_type) or \ + (not check_list and get_object_type(item) == device_type): _devices = build_device(item, api_interface) for device in _devices: devices.append(device) diff --git a/src/pywink/devices/air_conditioner.py b/src/pywink/devices/air_conditioner.py index b7df68e..57b786d 100644 --- a/src/pywink/devices/air_conditioner.py +++ b/src/pywink/devices/air_conditioner.py @@ -1,4 +1,4 @@ -from pywink.devices.base import WinkDevice +from ..devices.base import WinkDevice # pylint: disable=too-many-public-methods diff --git a/src/pywink/devices/base.py b/src/pywink/devices/base.py index b055e23..3359ff2 100644 --- a/src/pywink/devices/base.py +++ b/src/pywink/devices/base.py @@ -1,4 +1,4 @@ -class WinkDevice(object): +class WinkDevice: """ This is a generic Wink device, all other object inherit from this. """ diff --git a/src/pywink/devices/binary_switch.py b/src/pywink/devices/binary_switch.py index c21f689..ebce2a6 100644 --- a/src/pywink/devices/binary_switch.py +++ b/src/pywink/devices/binary_switch.py @@ -1,4 +1,4 @@ -from pywink.devices.base import WinkDevice +from ..devices.base import WinkDevice SUPPORTED_BINARY_STATE_FIELDS = ['powered', 'opened'] diff --git a/src/pywink/devices/binary_switch_group.py b/src/pywink/devices/binary_switch_group.py index c72139e..be7992b 100644 --- a/src/pywink/devices/binary_switch_group.py +++ b/src/pywink/devices/binary_switch_group.py @@ -1,4 +1,4 @@ -from pywink.devices.base import WinkDevice +from ..devices.base import WinkDevice class WinkBinarySwitchGroup(WinkDevice): diff --git a/src/pywink/devices/button.py b/src/pywink/devices/button.py index 46425e3..b6c2c73 100644 --- a/src/pywink/devices/button.py +++ b/src/pywink/devices/button.py @@ -1,4 +1,4 @@ -from pywink.devices.binary_switch import WinkBinarySwitch +from ..devices.binary_switch import WinkBinarySwitch class WinkButton(WinkBinarySwitch): diff --git a/src/pywink/devices/camera.py b/src/pywink/devices/camera.py index a808445..67016cd 100644 --- a/src/pywink/devices/camera.py +++ b/src/pywink/devices/camera.py @@ -1,4 +1,4 @@ -from pywink.devices.base import WinkDevice +from ..devices.base import WinkDevice class WinkCanaryCamera(WinkDevice): diff --git a/src/pywink/devices/cloud_clock.py b/src/pywink/devices/cloud_clock.py new file mode 100644 index 0000000..0b90a07 --- /dev/null +++ b/src/pywink/devices/cloud_clock.py @@ -0,0 +1,315 @@ +from datetime import datetime +import logging +import random + +from ..devices.base import WinkDevice + + +DTSTART = "DTSTART;TZID=" +REPEAT = "RRULE:FREQ=WEEKLY;BYDAY=" + +_LOGGER = logging.getLogger(__name__) + + +class WinkCloudClock(WinkDevice): + """ + Represents a Quirky Nimbus. + """ + + def state(self): + return self.available() + + def set_dial(self, json_value, index, timezone=None): + """ + :param json_value: The value to set + :param index: The dials index + :param timezone: The time zone to use for a time dial + :return: + """ + + values = self.json_state + if timezone is None: + json_value["channel_configuration"] = {"channel_id": "10"} + values["dials"][index] = json_value + + response = self.api_interface.set_device_state(self, values) + else: + json_value["channel_configuration"] = {"channel_id": "1", "timezone": timezone} + values["dials"][index] = json_value + response = self.api_interface.set_device_state(self, values) + return response + + def get_time_dial(self): + for dial in self.json_state.get("dials", {}): + if dial['channel_configuration']['channel_id'] == "1": + if dial['name'] == "Time": + return dial + return None + return None + + def create_alarm(self, date, days=None, name=None): + if self.get_time_dial() is None: + _LOGGER.error("Not creating alarm, no time dial.") + return False + timezone_string = self.get_time_dial()["channel_configuration"]["timezone"] + ical_string = _create_ical_string(timezone_string, date, days) + + nonce = str(random.randint(0, 1000000000)) + + _json = {'recurrence': ical_string, 'enabled': True, 'nonce': nonce} + if name: + _json['name'] = name + self.api_interface.create_cloud_clock_alarm(self, _json) + return True + + +class WinkCloudClockAlarm(WinkDevice): + """ + Represents a Quirky Nimbus alarm. + """ + + def __init__(self, device_state_as_json, api_interface): + super().__init__(device_state_as_json, api_interface) + self.parent = None + self.start_time, self.days = _parse_ical_string(device_state_as_json.get('recurrence')) + + def state(self): + return self.json_state['next_at'] + + def set_parent(self, parent): + self.parent = parent + + def available(self): + enabled = self.json_state['enabled'] + clock = self.parent.get_time_dial() + return bool(enabled and clock is not None) + + def recurrence(self): + return self.json_state['recurrence'] + + def set_enabled(self, enabled): + self.api_interface.set_device_state(self, {"enabled": enabled}) + + def update_state(self): + """ Update state with latest info from Wink API. """ + response = self.api_interface.get_device_state(self, id_override=self.parent.object_id(), + type_override=self.parent.object_type()) + self._update_state_from_response(response) + + def _update_state_from_response(self, response_json): + """ + :param response_json: the json obj returned from query + :return: + """ + if 'data' in response_json and response_json['data']['object_type'] == "cloud_clock": + cloud_clock = response_json.get('data') + if cloud_clock is None: + return False + + alarms = cloud_clock.get('alarms') + for alarm in alarms: + if alarm.get('object_id') == self.object_id(): + self.json_state = alarm + return True + return False + if 'data' in response_json: + alarm = response_json.get('data') + self.json_state = alarm + return True + self.json_state = response_json + return True + + def set_recurrence(self, date, days=None): + """ + + :param date: Datetime object time to start/repeat + :param days: days to repeat (Defaults to one time alarm) + :return: + """ + if self.parent.get_time_dial() is None: + _LOGGER.error("Not setting alarm, no time dial.") + return False + timezone_string = self.parent.get_time_dial()["channel_configuration"]["timezone"] + ical_string = _create_ical_string(timezone_string, date, days) + _json = {'recurrence': ical_string, 'enabled': True} + + self.api_interface.set_device_state(self, _json) + return True + + +class WinkCloudClockDial(WinkDevice): + """ + Represents a Quirky nimbus dial. + """ + + def __init__(self, device_state_as_json, api_interface): + super().__init__(device_state_as_json, api_interface) + self.parent = None + + def state(self): + return self.json_state.get('value') + + def position(self): + return self.json_state.get('position') + + def labels(self): + return self.json_state.get('labels') + + def rotation(self): + return self.json_state['dial_configuration'].get('rotation') + + def max_value(self): + return self.json_state['dial_configuration'].get('max_value') + + def min_value(self): + return self.json_state['dial_configuration'].get('min_value') + + def ticks(self): + return self.json_state['dial_configuration'].get('num_ticks') + + def min_position(self): + return self.json_state['dial_configuration'].get('min_position') + + def max_position(self): + return self.json_state['dial_configuration'].get('max_position') + + def available(self): + return self.json_state.get('connection', False) + + def update_state(self): + """ Update state with latest info from Wink API. """ + response = self.api_interface.get_device_state(self, id_override=self.parent_id(), + type_override=self.parent_object_type()) + self._update_state_from_response(response) + + def set_parent(self, parent): + self.parent = parent + + def _update_state_from_response(self, response_json): + """ + :param response_json: the json obj returned from query + :return: + """ + cloud_clock = response_json.get('data') + self.parent.json_state = cloud_clock + + if cloud_clock is None: + return False + + cloud_clock_last_reading = cloud_clock.get('last_reading') + dials = cloud_clock.get('dials') + for dial in dials: + if dial.get('object_id') == self.object_id(): + dial['connection'] = cloud_clock_last_reading.get('connection') + self.json_state = dial + return True + return False + + def pubnub_update(self, json_response): + self._update_state_from_response(json_response) + + def index(self): + return self.json_state.get('dial_index', None) + + def parent_id(self): + return self.json_state.get('parent_object_id') + + def parent_object_type(self): + return self.json_state.get('parent_object_type') + + def set_name(self, name): + value = self.parent.json_state + _json = {"name": name} + value["dials"][self.index()] = _json + response = self.api_interface.set_device_state(self, value, self.parent_id(), self.parent_object_type()) + self._update_state_from_response(response) + + def set_configuration(self, min_value, max_value, rotation="cw", scale="linear", ticks=12, min_position=0, + max_position=360): + """ + + :param min_value: Any number + :param max_value: Any number above min_value + :param rotation: (String) cw or ccw + :param scale: (String) Linear and ... + :param ticks:(Int) number of ticks of the clock up to 360? + :param min_position: (Int) 0-360 + :param max_position: (Int) 0-360 + :return: + """ + + _json = {"min_value": min_value, "max_value": max_value, "rotation": rotation, "scale": scale, "ticks": ticks, + "min_position": min_position, "max_position": max_position} + + dial_config = {"dial_configuration": _json} + + self._update_state_from_response(self.parent.set_dial(dial_config, self.index())) + + def set_state(self, value, labels=None): + """ + + :param value: Any number + :param labels: A list of two Strings sending None won't change the current values. + :return: + """ + + values = {"value": value} + json_labels = [] + if labels: + for label in labels: + json_labels.append(str(label).upper()) + values["labels"] = json_labels + + self._update_state_from_response(self.parent.set_dial(values, self.index())) + + def make_time_dial(self, timezone_string): + """ + + :param timezone_string: + :return: + """ + self._update_state_from_response(self.parent.set_dial({}, self.index(), timezone_string)) + + +def _create_ical_string(timezone_string, date, days=None): + valid_days = ["SU", "MO", "TU", "WE", "TH", "FR", "SA"] + ical_string = DTSTART + timezone_string + ":" + date.strftime("%Y%m%dT%H%M%S") + if days is not None: + if days == "DAILY": + ical_string = ical_string + "\nRRULE:FREQ=DAILY" + else: + ical_string = ical_string + "\n" + REPEAT + for day in days: + if day in valid_days: + if ical_string[-1] == "=": + ical_string = ical_string + day + else: + ical_string = ical_string + ',' + day + else: + error = "Invalid repeat day {}".format(day) + + _LOGGER.error(error) + + return ical_string + + +def _parse_ical_string(ical_string): + """ + SU,MO,TU,WE,TH,FR,SA + DTSTART;TZID=America/New_York:20180804T233251\nRRULE:FREQ=WEEKLY;BYDAY=SA + DTSTART;TZID=America/New_York:20180804T233251\nRRULE:FREQ=DAILY + DTSTART;TZID=America/New_York:20180804T233251\nRRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR,SA + DTSTART;TZID=America/New_York:20180718T174500 + """ + start_time = ical_string.splitlines()[0].replace(DTSTART, '') + if "RRULE" in ical_string: + days = ical_string.splitlines()[1].replace(REPEAT, '') + if days == "RRULE:FREQ=DAILY": + days = ['DAILY'] + else: + days = days.split(',') + else: + days = None + start_time = start_time.splitlines()[0].split(':')[1] + datetime_object = datetime.strptime(start_time, '%Y%m%dT%H%M%S') + return datetime_object, days diff --git a/src/pywink/devices/eggtray.py b/src/pywink/devices/eggtray.py index eb41d60..ac3dc27 100644 --- a/src/pywink/devices/eggtray.py +++ b/src/pywink/devices/eggtray.py @@ -1,4 +1,4 @@ -from pywink.devices.base import WinkDevice +from ..devices.base import WinkDevice class WinkEggtray(WinkDevice): diff --git a/src/pywink/devices/factory.py b/src/pywink/devices/factory.py index 4f3dc23..ce9c580 100644 --- a/src/pywink/devices/factory.py +++ b/src/pywink/devices/factory.py @@ -2,35 +2,36 @@ Build Wink devices. """ -from pywink.devices import types as device_types -from pywink.devices.sensor import WinkSensor -from pywink.devices.light_bulb import WinkLightBulb -from pywink.devices.binary_switch import WinkBinarySwitch -from pywink.devices.lock import WinkLock -from pywink.devices.eggtray import WinkEggtray -from pywink.devices.garage_door import WinkGarageDoor -from pywink.devices.shade import WinkShade -from pywink.devices.siren import WinkSiren -from pywink.devices.key import WinkKey -from pywink.devices.thermostat import WinkThermostat -from pywink.devices.fan import WinkFan, WinkGeZwaveFan -from pywink.devices.remote import WinkRemote -from pywink.devices.hub import WinkHub -from pywink.devices.powerstrip import WinkPowerStrip, WinkPowerStripOutlet -from pywink.devices.piggy_bank import WinkPorkfolioBalanceSensor, WinkPorkfolioNose -from pywink.devices.sprinkler import WinkSprinkler -from pywink.devices.button import WinkButton -from pywink.devices.gang import WinkGang -from pywink.devices.smoke_detector import WinkSmokeDetector, WinkSmokeSeverity, WinkCoDetector, WinkCoSeverity -from pywink.devices.camera import WinkCanaryCamera -from pywink.devices.air_conditioner import WinkAirConditioner -from pywink.devices.propane_tank import WinkPropaneTank -from pywink.devices.robot import WinkRobot -from pywink.devices.scene import WinkScene -from pywink.devices.light_group import WinkLightGroup -from pywink.devices.binary_switch_group import WinkBinarySwitchGroup -from pywink.devices.water_heater import WinkWaterHeater -from pywink.devices.shade_group import WinkShadeGroup +from ..devices import types as device_types +from ..devices.sensor import WinkSensor +from ..devices.light_bulb import WinkLightBulb +from ..devices.binary_switch import WinkBinarySwitch +from ..devices.lock import WinkLock +from ..devices.eggtray import WinkEggtray +from ..devices.garage_door import WinkGarageDoor +from ..devices.shade import WinkShade +from ..devices.siren import WinkSiren +from ..devices.key import WinkKey +from ..devices.thermostat import WinkThermostat +from ..devices.fan import WinkFan, WinkGeZwaveFan +from ..devices.remote import WinkRemote +from ..devices.hub import WinkHub +from ..devices.powerstrip import WinkPowerStrip, WinkPowerStripOutlet +from ..devices.piggy_bank import WinkPorkfolioBalanceSensor, WinkPorkfolioNose +from ..devices.sprinkler import WinkSprinkler +from ..devices.button import WinkButton +from ..devices.gang import WinkGang +from ..devices.smoke_detector import WinkSmokeDetector, WinkSmokeSeverity, WinkCoDetector, WinkCoSeverity +from ..devices.camera import WinkCanaryCamera +from ..devices.air_conditioner import WinkAirConditioner +from ..devices.propane_tank import WinkPropaneTank +from ..devices.robot import WinkRobot +from ..devices.scene import WinkScene +from ..devices.light_group import WinkLightGroup +from ..devices.binary_switch_group import WinkBinarySwitchGroup +from ..devices.water_heater import WinkWaterHeater +from ..devices.shade_group import WinkShadeGroup +from ..devices.cloud_clock import WinkCloudClock, WinkCloudClockDial, WinkCloudClockAlarm # pylint: disable=too-many-branches, too-many-statements @@ -119,6 +120,11 @@ def build_device(device_state_as_json, api_interface): new_objects.append(WinkLightGroup(device_state_as_json, api_interface)) elif object_type == device_types.WATER_HEATER: new_objects.append(WinkWaterHeater(device_state_as_json, api_interface)) + elif object_type == device_types.CLOUD_CLOCK: + cloud_clock = WinkCloudClock(device_state_as_json, api_interface) + new_objects.append(cloud_clock) + new_objects.extend(__get_dials_from_cloudclock(device_state_as_json, api_interface, cloud_clock)) + new_objects.extend(__get_alarms_from_cloudclock(device_state_as_json, api_interface, cloud_clock)) return new_objects @@ -175,3 +181,28 @@ def __get_sensors_from_smoke_detector(item, api_interface): sensors.append(WinkSmokeSeverity(item, api_interface)) sensors.append(WinkCoSeverity(item, api_interface)) return sensors + + +def __get_dials_from_cloudclock(item, api_interface, parent): + _dials = [] + dials = item['dials'] + for dial in dials: + if 'subscription' in item: + dial['subscription'] = item['subscription'] + dial['connection'] = item['last_reading']['connection'] + dial_obj = WinkCloudClockDial(dial, api_interface) + dial_obj.set_parent(parent) + _dials.append(dial_obj) + return _dials + + +def __get_alarms_from_cloudclock(item, api_interface, parent): + _alarms = [] + alarms = item['alarms'] + for alarm in alarms: + alarm['subscription'] = item['subscription'] + alarm['connection'] = item['last_reading']['connection'] + alarm_obj = WinkCloudClockAlarm(alarm, api_interface) + alarm_obj.set_parent(parent) + _alarms.append(alarm_obj) + return _alarms diff --git a/src/pywink/devices/fan.py b/src/pywink/devices/fan.py index 8fc0a55..4c58248 100644 --- a/src/pywink/devices/fan.py +++ b/src/pywink/devices/fan.py @@ -1,4 +1,4 @@ -from pywink.devices.base import WinkDevice +from ..devices.base import WinkDevice # pylint: disable=too-many-public-methods @@ -123,7 +123,7 @@ def current_fan_speed(self): brightness = self._last_reading.get('brightness', 0.33) if brightness <= 0.33: return "low" - elif brightness <= 0.66: + if brightness <= 0.66: return "medium" return "high" diff --git a/src/pywink/devices/gang.py b/src/pywink/devices/gang.py index 3f27965..fdbc505 100644 --- a/src/pywink/devices/gang.py +++ b/src/pywink/devices/gang.py @@ -1,4 +1,4 @@ -from pywink.devices.base import WinkDevice +from ..devices.base import WinkDevice class WinkGang(WinkDevice): diff --git a/src/pywink/devices/garage_door.py b/src/pywink/devices/garage_door.py index ccb7e85..7437b12 100644 --- a/src/pywink/devices/garage_door.py +++ b/src/pywink/devices/garage_door.py @@ -1,4 +1,4 @@ -from pywink.devices.base import WinkDevice +from ..devices.base import WinkDevice class WinkGarageDoor(WinkDevice): diff --git a/src/pywink/devices/hub.py b/src/pywink/devices/hub.py index b285ee6..c4a47ec 100644 --- a/src/pywink/devices/hub.py +++ b/src/pywink/devices/hub.py @@ -1,6 +1,6 @@ import logging -from pywink.devices.base import WinkDevice +from ..devices.base import WinkDevice _LOGGER = logging.getLogger(__name__) diff --git a/src/pywink/devices/key.py b/src/pywink/devices/key.py index a0d837f..c3cade2 100644 --- a/src/pywink/devices/key.py +++ b/src/pywink/devices/key.py @@ -1,4 +1,4 @@ -from pywink.devices.base import WinkDevice +from ..devices.base import WinkDevice class WinkKey(WinkDevice): diff --git a/src/pywink/devices/light_bulb.py b/src/pywink/devices/light_bulb.py index c254fae..d59de20 100644 --- a/src/pywink/devices/light_bulb.py +++ b/src/pywink/devices/light_bulb.py @@ -1,4 +1,4 @@ -from pywink.devices.base import WinkDevice +from ..devices.base import WinkDevice class WinkLightBulb(WinkDevice): diff --git a/src/pywink/devices/light_group.py b/src/pywink/devices/light_group.py index 4246501..234622d 100644 --- a/src/pywink/devices/light_group.py +++ b/src/pywink/devices/light_group.py @@ -1,4 +1,4 @@ -from pywink.devices.light_bulb import WinkLightBulb +from ..devices.light_bulb import WinkLightBulb class WinkLightGroup(WinkLightBulb): diff --git a/src/pywink/devices/lock.py b/src/pywink/devices/lock.py index e39bcde..e706d6b 100644 --- a/src/pywink/devices/lock.py +++ b/src/pywink/devices/lock.py @@ -1,4 +1,4 @@ -from pywink.devices.base import WinkDevice +from ..devices.base import WinkDevice class WinkLock(WinkDevice): diff --git a/src/pywink/devices/piggy_bank.py b/src/pywink/devices/piggy_bank.py index 3bbf544..f7e017a 100644 --- a/src/pywink/devices/piggy_bank.py +++ b/src/pywink/devices/piggy_bank.py @@ -1,4 +1,4 @@ -from pywink.devices.base import WinkDevice +from ..devices.base import WinkDevice class WinkPorkfolioNose(WinkDevice): @@ -79,3 +79,11 @@ def available(self): always returning True to avoid this issue. """ return self._available + + def deposit(self, amount): + """ + + :param amount: (int +/-) amount to be deposited or withdrawn in cents + """ + _json = {"amount": amount} + self.api_interface.piggy_bank_deposit(self, _json) diff --git a/src/pywink/devices/powerstrip.py b/src/pywink/devices/powerstrip.py index 788eba6..84514ad 100644 --- a/src/pywink/devices/powerstrip.py +++ b/src/pywink/devices/powerstrip.py @@ -1,4 +1,4 @@ -from pywink.devices.base import WinkDevice +from ..devices.base import WinkDevice class WinkPowerStrip(WinkDevice): @@ -39,14 +39,7 @@ def update_state(self): """ Update state with latest info from Wink API. """ response = self.api_interface.get_device_state(self, id_override=self.parent_id(), type_override=self.parent_object_type()) - power_strip = response.get('data') - - power_strip_reading = power_strip.get('last_reading') - outlets = power_strip.get('outlets') - for outlet in outlets: - if outlet.get('object_id') == self.object_id(): - outlet['last_reading']['connection'] = power_strip_reading.get('connection') - self.json_state = outlet + self._update_state_from_response(response) def _update_state_from_response(self, response_json): """ diff --git a/src/pywink/devices/propane_tank.py b/src/pywink/devices/propane_tank.py index b25318d..5f86ea8 100644 --- a/src/pywink/devices/propane_tank.py +++ b/src/pywink/devices/propane_tank.py @@ -1,4 +1,4 @@ -from pywink.devices.base import WinkDevice +from ..devices.base import WinkDevice class WinkPropaneTank(WinkDevice): diff --git a/src/pywink/devices/remote.py b/src/pywink/devices/remote.py index 8d1a85e..3e7f959 100644 --- a/src/pywink/devices/remote.py +++ b/src/pywink/devices/remote.py @@ -1,4 +1,4 @@ -from pywink.devices.base import WinkDevice +from ..devices.base import WinkDevice class WinkRemote(WinkDevice): diff --git a/src/pywink/devices/robot.py b/src/pywink/devices/robot.py index 83b1f3e..3492762 100644 --- a/src/pywink/devices/robot.py +++ b/src/pywink/devices/robot.py @@ -1,4 +1,4 @@ -from pywink.devices.base import WinkDevice +from ..devices.base import WinkDevice class WinkRobot(WinkDevice): diff --git a/src/pywink/devices/scene.py b/src/pywink/devices/scene.py index 834afec..c84c4b9 100644 --- a/src/pywink/devices/scene.py +++ b/src/pywink/devices/scene.py @@ -1,4 +1,4 @@ -from pywink.devices.base import WinkDevice +from ..devices.base import WinkDevice class WinkScene(WinkDevice): diff --git a/src/pywink/devices/sensor.py b/src/pywink/devices/sensor.py index 2c43b69..def1173 100644 --- a/src/pywink/devices/sensor.py +++ b/src/pywink/devices/sensor.py @@ -1,4 +1,4 @@ -from pywink.devices.base import WinkDevice +from ..devices.base import WinkDevice SENSOR_FIELDS_TO_UNITS = {"humidity": "%", "temperature": u'\N{DEGREE SIGN}', "brightness": "%", "proximity": ""} diff --git a/src/pywink/devices/shade.py b/src/pywink/devices/shade.py index f9a0588..fbc360c 100644 --- a/src/pywink/devices/shade.py +++ b/src/pywink/devices/shade.py @@ -1,4 +1,4 @@ -from pywink.devices.base import WinkDevice +from ..devices.base import WinkDevice class WinkShade(WinkDevice): diff --git a/src/pywink/devices/shade_group.py b/src/pywink/devices/shade_group.py index 5ff0c22..2c9de6d 100644 --- a/src/pywink/devices/shade_group.py +++ b/src/pywink/devices/shade_group.py @@ -1,4 +1,4 @@ -from pywink.devices.base import WinkDevice +from ..devices.base import WinkDevice class WinkShadeGroup(WinkDevice): diff --git a/src/pywink/devices/siren.py b/src/pywink/devices/siren.py index 2428e6e..1f57a9e 100644 --- a/src/pywink/devices/siren.py +++ b/src/pywink/devices/siren.py @@ -1,4 +1,4 @@ -from pywink.devices.base import WinkDevice +from ..devices.base import WinkDevice class WinkSiren(WinkDevice): diff --git a/src/pywink/devices/smoke_detector.py b/src/pywink/devices/smoke_detector.py index f35454c..e618742 100644 --- a/src/pywink/devices/smoke_detector.py +++ b/src/pywink/devices/smoke_detector.py @@ -1,4 +1,4 @@ -from pywink.devices.sensor import WinkDevice +from ..devices.sensor import WinkDevice class WinkBaseSmokeDetector(WinkDevice): diff --git a/src/pywink/devices/sprinkler.py b/src/pywink/devices/sprinkler.py index 2ccabdd..393bc55 100644 --- a/src/pywink/devices/sprinkler.py +++ b/src/pywink/devices/sprinkler.py @@ -1,4 +1,4 @@ -from pywink.devices.binary_switch import WinkBinarySwitch +from ..devices.binary_switch import WinkBinarySwitch class WinkSprinkler(WinkBinarySwitch): diff --git a/src/pywink/devices/thermostat.py b/src/pywink/devices/thermostat.py index 4809828..a2b7854 100644 --- a/src/pywink/devices/thermostat.py +++ b/src/pywink/devices/thermostat.py @@ -1,4 +1,4 @@ -from pywink.devices.base import WinkDevice +from ..devices.base import WinkDevice # pylint: disable=too-many-public-methods @@ -39,12 +39,11 @@ def away(self): ecobee = self.profile() if nest is not None: return nest - elif ecobee is not None: + if ecobee is not None: if ecobee == "home": return False return True - else: - return None + return None def current_hvac_mode(self): return self._last_reading.get('mode', None) diff --git a/src/pywink/devices/types.py b/src/pywink/devices/types.py index d659e68..c69d6d7 100644 --- a/src/pywink/devices/types.py +++ b/src/pywink/devices/types.py @@ -29,9 +29,11 @@ SCENE = 'scene' GROUP = 'group' WATER_HEATER = 'water_heater' +CLOUD_CLOCK = 'cloud_clock' ALL_SUPPORTED_DEVICES = [LIGHT_BULB, BINARY_SWITCH, SENSOR_POD, LOCK, EGGTRAY, GARAGE_DOOR, POWERSTRIP, SHADE, SIREN, KEY, PIGGY_BANK, SMOKE_DETECTOR, THERMOSTAT, HUB, FAN, DOOR_BELL, REMOTE, SPRINKLER, BUTTON, GANG, CAMERA, AIR_CONDITIONER, - PROPANE_TANK, ROBOT, SCENE, GROUP, WATER_HEATER] + PROPANE_TANK, ROBOT, SCENE, GROUP, WATER_HEATER, + CLOUD_CLOCK] diff --git a/src/pywink/devices/water_heater.py b/src/pywink/devices/water_heater.py index d36ff17..fbf0461 100644 --- a/src/pywink/devices/water_heater.py +++ b/src/pywink/devices/water_heater.py @@ -1,4 +1,4 @@ -from pywink.devices.base import WinkDevice +from ..devices.base import WinkDevice # pylint: disable=too-many-public-methods diff --git a/src/pywink/test/api_test.py b/src/pywink/test/api_test.py index f29257a..5ac4030 100644 --- a/src/pywink/test/api_test.py +++ b/src/pywink/test/api_test.py @@ -7,33 +7,34 @@ # Third-party imports... from unittest.mock import MagicMock, Mock - -from pywink.api import * -from pywink.api import WinkApiInterface -from pywink.devices.sensor import WinkSensor -from pywink.devices.hub import WinkHub -from pywink.devices.piggy_bank import WinkPorkfolioBalanceSensor, WinkPorkfolioNose -from pywink.devices.key import WinkKey -from pywink.devices.remote import WinkRemote -from pywink.devices.powerstrip import WinkPowerStrip, WinkPowerStripOutlet -from pywink.devices.light_bulb import WinkLightBulb -from pywink.devices.binary_switch import WinkBinarySwitch -from pywink.devices.lock import WinkLock -from pywink.devices.eggtray import WinkEggtray -from pywink.devices.garage_door import WinkGarageDoor -from pywink.devices.shade import WinkShade -from pywink.devices.siren import WinkSiren -from pywink.devices.fan import WinkFan, WinkGeZwaveFan -from pywink.devices.thermostat import WinkThermostat -from pywink.devices.button import WinkButton -from pywink.devices.gang import WinkGang -from pywink.devices.smoke_detector import WinkSmokeDetector, WinkSmokeSeverity, WinkCoDetector, WinkCoSeverity -from pywink.devices.camera import WinkCanaryCamera -from pywink.devices.air_conditioner import WinkAirConditioner -from pywink.devices.propane_tank import WinkPropaneTank -from pywink.devices.scene import WinkScene -from pywink.devices.robot import WinkRobot -from pywink.devices.water_heater import WinkWaterHeater +from requests import * + +from ..api import * +from ..devices.sensor import WinkSensor +from ..devices.hub import WinkHub +from ..devices.piggy_bank import WinkPorkfolioBalanceSensor, WinkPorkfolioNose +from ..devices.key import WinkKey +from ..devices.remote import WinkRemote +from ..devices.powerstrip import WinkPowerStrip, WinkPowerStripOutlet +from ..devices.light_bulb import WinkLightBulb +from ..devices.binary_switch import WinkBinarySwitch +from ..devices.lock import WinkLock +from ..devices.eggtray import WinkEggtray +from ..devices.garage_door import WinkGarageDoor +from ..devices.shade import WinkShade +from ..devices.siren import WinkSiren +from ..devices.fan import WinkFan, WinkGeZwaveFan +from ..devices.thermostat import WinkThermostat +from ..devices.button import WinkButton +from ..devices.gang import WinkGang +from ..devices.smoke_detector import WinkSmokeDetector, WinkSmokeSeverity, WinkCoDetector, WinkCoSeverity +from ..devices.camera import WinkCanaryCamera +from ..devices.air_conditioner import WinkAirConditioner +from ..devices.propane_tank import WinkPropaneTank +from ..devices.scene import WinkScene +from ..devices.robot import WinkRobot +from ..devices.water_heater import WinkWaterHeater +from ..devices.cloud_clock import WinkCloudClock, WinkCloudClockDial, WinkCloudClockAlarm USERS_ME_WINK_DEVICES = {} GROUPS = {} @@ -68,21 +69,18 @@ def test_local_control_enabled_by_default(self): self.assertTrue(ALLOW_LOCAL_CONTROL) def test_that_disable_local_control_works(self): - from pywink.api import ALLOW_LOCAL_CONTROL + from ..api import ALLOW_LOCAL_CONTROL disable_local_control() self.assertFalse(ALLOW_LOCAL_CONTROL) def test_set_user_agent(self): - from pywink.api import API_HEADERS set_user_agent("THIS IS A TEST") self.assertEqual("THIS IS A TEST", API_HEADERS["User-Agent"]) def test_set_bearer_token(self): - from pywink.api import API_HEADERS, LOCAL_API_HEADERS set_bearer_token("THIS IS A TEST") self.assertEqual("Bearer THIS IS A TEST", API_HEADERS["Authorization"]) - def test_get_authorization_url(self): WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) url = get_authorization_url("TEST", "127.0.0.1") @@ -109,7 +107,7 @@ def test_get_subscription_key(self): def test_get_all_devices_from_api(self): WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) devices = get_all_devices() - self.assertEqual(len(devices), 77) + self.assertEqual(len(devices), 85) lights = get_light_bulbs() for light in lights: self.assertTrue(isinstance(light, WinkLightBulb)) @@ -181,7 +179,7 @@ def test_get_sensor_and_binary_switch_updated_states_from_api(self): WinkGang, WinkSmokeDetector, WinkSmokeSeverity, WinkCoDetector, WinkCoSeverity, WinkButton, WinkRobot] # No way to validate scene is activated, so skipping. - skip_types = [WinkPowerStripOutlet, WinkCanaryCamera, WinkScene] + skip_types = [WinkPowerStripOutlet, WinkCanaryCamera, WinkScene, WinkCloudClock, WinkCloudClockDial, WinkCloudClockAlarm] devices = get_all_devices() old_states = {} for device in devices: @@ -483,7 +481,6 @@ def test_get_water_heater_updated_states_from_api(self): def test_get_camera_updated_states_from_api(self): WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) devices = get_cameras() - old_states = {} for device in devices: if isinstance(device, WinkCanaryCamera): device.api_interface = self.api_interface @@ -498,7 +495,6 @@ def test_get_camera_updated_states_from_api(self): def test_get_fan_updated_states_from_api(self): WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) devices = get_fans() - old_states = {} for device in devices: device.api_interface = self.api_interface if isinstance(device, WinkGeZwaveFan): @@ -516,27 +512,39 @@ def test_get_fan_updated_states_from_api(self): self.assertEqual(device.current_fan_direction(), "reverse") self.assertEqual(device.current_timer(), 300) - def test_get_propane_tank_updated_states_from_api(self): WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) devices = get_propane_tanks() - old_states = {} for device in devices: device.api_interface = self.api_interface device.set_tare(5.0) device.update_state() self.assertEqual(device.tare(), 5.0) + def test_get_cloud_clock_dial_updated_states_from_api(self): + WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) + devices = get_cloud_clocks() + dial = devices[1] + for device in devices: + device.api_interface = self.api_interface + dial.set_configuration(0, 123, "ccw", min_position=0, max_position=125) + dial.update_state() + + self.assertEqual(dial.max_position(), 125) + self.assertEqual(dial.max_value(), 123) + self.assertEquals(dial.rotation(), "ccw") + def test_set_all_device_names(self): WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) devices = get_all_devices() - old_states = {} for device in devices: - device.api_interface = self.api_interface - device.set_name("TEST_NAME") - device.update_state() + if not isinstance(device, WinkCloudClockAlarm) and not isinstance(device, WinkCloudClockDial): + device.api_interface = self.api_interface + device.set_name("TEST_NAME") + device.update_state() for device in devices: - self.assertTrue(device.name().startswith("TEST_NAME")) + if not isinstance(device, WinkCloudClockAlarm) and not isinstance(device, WinkCloudClockDial): + self.assertTrue(device.name().startswith("TEST_NAME")) class MockServerRequestHandler(BaseHTTPRequestHandler): @@ -607,17 +615,22 @@ def start_mock_server(port): mock_server_thread.start() -class MockApiInterface(): +class MockApiInterface: def set_device_state(self, device, state, id_override=None, type_override=None): """ - :type device: WinkDevice + + :param device: + :param state: + :param id_override: + :param type_override: + :return: """ object_id = id_override or device.object_id() device_object_type = device.object_type() object_type = type_override or device_object_type return_dict = {} - if "name" in str(state): + if "TEST_NAME" in str(state): for dict_device in USERS_ME_WINK_DEVICES.get('data'): _object_id = dict_device.get("object_id") if _object_id == object_id: @@ -626,6 +639,11 @@ def set_device_state(self, device, state, id_override=None, type_override=None): set_state = state["outlets"][index]["name"] dict_device["outlets"][index]["name"] = set_state return_dict["data"] = dict_device + elif device_object_type == "dial": + index = device.index() + set_state = state["dials"][index]["name"] + dict_device["dials"][index]["name"] = set_state + return_dict["data"] = dict_device else: dict_device["name"] = state.get("name") for dict_device in GROUPS.get('data'): @@ -646,6 +664,15 @@ def set_device_state(self, device, state, id_override=None, type_override=None): set_state = state["outlets"][index]["desired_state"]["powered"] dict_device["outlets"][index]["last_reading"]["powered"] = set_state return_dict["data"] = dict_device + elif device_object_type == "cloud_clock": + index = 0 + for dial in state["dials"]: + for key, value in dial.get("channel_configuration").items(): + dict_device["dials"][index]["channel_configuration"][key] = value + for key, value in dial.get("dial_configuration").items(): + dict_device["dials"][index]["dial_configuration"][key] = value + index = index + 1 + return_dict["data"] = dict_device else: if "nose_color" in state: dict_device["nose_color"] = state.get("nose_color") @@ -680,7 +707,11 @@ def local_set_state(self, device, state, id_override=None, type_override=None): def get_device_state(self, device, id_override=None, type_override=None): """ - :type device: WinkDevice + + :param device: + :param id_override: + :param type_override: + :return: """ object_id = id_override or device.object_id() return_dict = {} diff --git a/src/pywink/test/devices/api_responses/nimbus.json b/src/pywink/test/devices/api_responses/nimbus.json new file mode 100644 index 0000000..ad4793d --- /dev/null +++ b/src/pywink/test/devices/api_responses/nimbus.json @@ -0,0 +1,226 @@ +{ + "last_reading": { + "connection": true, + "connection_updated_at": 1533401140.762892, + "connection_changed_at": 1533397680.0599086 + }, + "dials": [ + { + "name": "Time", + "value": 41639.0, + "position": 346.99166666666667, + "label": "11:33 AM", + "labels": [ + "11:33 AM", + "New York" + ], + "brightness": 100, + "channel_configuration": { + "locale": "en_us", + "timezone": "America/New_York", + "channel_id": "1", + "object_type": null, + "object_id": null + }, + "dial_configuration": { + "min_value": 0, + "max_value": 86400, + "min_position": 0, + "max_position": 720, + "scale_type": "linear", + "rotation": "cw", + "num_ticks": 12 + }, + "dial_index": 0, + "dial_id": "78830", + "refreshed_at": 1533483239, + "object_type": "dial", + "object_id": "78830", + "created_at": 1533397676, + "updated_at": 1533483196, + "parent_object_type": "cloud_clock", + "parent_object_id": "19596" + }, + { + "name": "Porkfolio", + "value": 279.0, + "position": 279.0, + "label": "279", + "labels": [ + "279" + ], + "brightness": 25, + "channel_configuration": { + "channel_id": "10", + "object_type": null, + "object_id": null + }, + "dial_configuration": { + "min_value": 0, + "max_value": 360, + "rotation": "cw", + "min_position": 0, + "max_position": 360, + "scale_type": "linear", + "num_ticks": 12 + }, + "dial_index": 1, + "dial_id": "78831", + "refreshed_at": 1533440249, + "object_type": "dial", + "object_id": "78831", + "created_at": 1533397676, + "updated_at": 1533483182, + "parent_object_type": "cloud_clock", + "parent_object_id": "19596" + }, + { + "name": "Porkfolio", + "value": 1327.0, + "position": 296.658, + "label": "$13.27", + "labels": [ + "$13.27", + "PORK" + ], + "brightness": 25, + "channel_configuration": { + "locale": "en_us", + "reading_type": "balance", + "channel_id": "12", + "wink_device_types": [ + "piggy_bank" + ], + "wink_device_ids": [ + "15212" + ], + "object_type": "piggy_bank", + "object_id": "15212" + }, + "dial_configuration": { + "min_value": 0, + "max_value": 5000, + "min_position": -135, + "max_position": 135, + "scale_type": "linear", + "rotation": "cw", + "num_ticks": 12 + }, + "dial_index": 2, + "dial_id": "78832", + "refreshed_at": 1533483196, + "object_type": "dial", + "object_id": "78832", + "created_at": 1533397676, + "updated_at": 1533483196, + "parent_object_type": "cloud_clock", + "parent_object_id": "19596" + }, + { + "name": "Dial #4", + "value": 212.0, + "position": 212.0, + "label": "212", + "labels": [ + "212" + ], + "brightness": 25, + "channel_configuration": { + "channel_id": "10", + "object_type": null, + "object_id": null + }, + "dial_configuration": { + "min_value": 0, + "max_value": 360, + "rotation": "ccw", + "min_position": 0, + "max_position": 360, + "scale_type": "linear", + "num_ticks": 12 + }, + "dial_index": 3, + "dial_id": "78833", + "refreshed_at": 1533405097, + "object_type": "dial", + "object_id": "78833", + "created_at": 1533397676, + "updated_at": 1533483182, + "parent_object_type": "cloud_clock", + "parent_object_id": "19596" + } + ], + "alarms": [ + { + "next_at": 1533576003, + "enabled": true, + "recurrence": "DTSTART;TZID=America/New_York:20180804T233251\nRRULE:FREQ=WEEKLY;BYDAY=SA", + "alarm_id": "11740", + "name": "", + "media_id": "1", + "object_type": "alarm", + "object_id": "11740", + "created_at": 1533439680, + "updated_at": 1533441551 + }, + { + "next_at": 1533576003, + "enabled": true, + "recurrence": "DTSTART;TZID=America/New_York:20180805T114547\nRRULE:FREQ=WEEKLY;BYDAY=MO", + "alarm_id": "11741", + "name": "", + "media_id": "1", + "object_type": "alarm", + "object_id": "11741", + "created_at": 1533440761, + "updated_at": 1533441553 + }, + { + "next_at": 1533576003, + "enabled": true, + "recurrence": "DTSTART;TZID=America/New_York:20180806T132003\nRRULE:FREQ=WEEKLY;BYDAY=SU,MO,TU", + "alarm_id": "11742", + "name": "", + "media_id": "1", + "object_type": "alarm", + "object_id": "11742", + "created_at": 1533489368, + "updated_at": 1533490109 + } + ], + "object_type": "cloud_clock", + "object_id": "19596", + "uuid": "002515db-46c4-4dd0-a20f-1bb3f658d82b", + "created_at": 1533397676, + "updated_at": 1533440084, + "icon_id": null, + "icon_code": null, + "subscription": { + "pubnub": { + "subscribe_key": "sub-c-f7bf7f7e-0542-11e3-a5e8-02ee2ddab7fe", + "channel": "9870af0c123456938c54dd4f8fc630ef|cloud_clock-19596|user-377123" + } + }, + "cloud_clock_id": "19596", + "name": "Nimbus", + "locale": "en_us", + "units": {}, + "hidden_at": null, + "capabilities": { + "supports_electric_imp": true, + "needs_wifi_network_list": true + }, + "triggers": [], + "device_manufacturer": "quirky_ge", + "model_name": "Nimbus", + "upc_id": "527", + "upc_code": "quirky_ge_nimbus", + "primary_upc_code": "quirky_ge_nimbus", + "lat_lng": [ + null, + null + ], + "location": "", + "mac_address": "0c2a6906b4c4", + "serial": "ADAA00048839" +} \ No newline at end of file diff --git a/src/pywink/test/devices/base_test.py b/src/pywink/test/devices/base_test.py index c9868c6..b10a114 100644 --- a/src/pywink/test/devices/base_test.py +++ b/src/pywink/test/devices/base_test.py @@ -4,29 +4,30 @@ from unittest.mock import MagicMock -from pywink.api import get_devices_from_response_dict -from pywink.devices import types as device_types -from pywink.devices.key import WinkKey -from pywink.devices.powerstrip import WinkPowerStripOutlet, WinkPowerStrip -from pywink.devices.piggy_bank import WinkPorkfolioBalanceSensor, WinkPorkfolioNose -from pywink.devices.siren import WinkSiren -from pywink.devices.eggtray import WinkEggtray -from pywink.devices.remote import WinkRemote -from pywink.devices.fan import WinkFan, WinkGeZwaveFan -from pywink.devices.binary_switch import WinkBinarySwitch -from pywink.devices.hub import WinkHub -from pywink.devices.light_bulb import WinkLightBulb -from pywink.devices.thermostat import WinkThermostat -from pywink.devices.shade import WinkShade -from pywink.devices.sprinkler import WinkSprinkler -from pywink.devices.button import WinkButton -from pywink.devices.gang import WinkGang -from pywink.devices.camera import WinkCanaryCamera -from pywink.devices.air_conditioner import WinkAirConditioner -from pywink.devices.propane_tank import WinkPropaneTank -from pywink.devices.scene import WinkScene -from pywink.devices.robot import WinkRobot -from pywink.devices.water_heater import WinkWaterHeater +from ...api import get_devices_from_response_dict +from ...devices import types as device_types +from ...devices.key import WinkKey +from ...devices.powerstrip import WinkPowerStripOutlet, WinkPowerStrip +from ...devices.piggy_bank import WinkPorkfolioBalanceSensor, WinkPorkfolioNose +from ...devices.siren import WinkSiren +from ...devices.eggtray import WinkEggtray +from ...devices.remote import WinkRemote +from ...devices.fan import WinkFan, WinkGeZwaveFan +from ...devices.binary_switch import WinkBinarySwitch +from ...devices.hub import WinkHub +from ...devices.light_bulb import WinkLightBulb +from ...devices.thermostat import WinkThermostat +from ...devices.shade import WinkShade +from ...devices.sprinkler import WinkSprinkler +from ...devices.button import WinkButton +from ...devices.gang import WinkGang +from ...devices.camera import WinkCanaryCamera +from ...devices.air_conditioner import WinkAirConditioner +from ...devices.propane_tank import WinkPropaneTank +from ...devices.scene import WinkScene +from ...devices.robot import WinkRobot +from ...devices.water_heater import WinkWaterHeater +from ...devices.cloud_clock import WinkCloudClock, WinkCloudClockDial, WinkCloudClockAlarm class BaseTests(unittest.TestCase): @@ -85,7 +86,8 @@ def test_all_devices_battery_is_valid(self): skip_types = [WinkFan, WinkGeZwaveFan, WinkPorkfolioBalanceSensor, WinkPorkfolioNose, WinkBinarySwitch, WinkHub, WinkLightBulb, WinkThermostat, WinkKey, WinkPowerStrip, WinkPowerStripOutlet, WinkRemote, WinkShade, WinkSprinkler, WinkButton, WinkGang, WinkCanaryCamera, - WinkAirConditioner, WinkScene, WinkRobot, WinkWaterHeater] + WinkAirConditioner, WinkScene, WinkRobot, WinkWaterHeater, WinkCloudClock, + WinkCloudClockDial, WinkCloudClockAlarm] for device in devices: if device.manufacturer_device_model() == "leaksmart_valve": self.assertIsNotNone(device.battery_level()) @@ -105,7 +107,8 @@ def test_all_devices_battery_is_valid(self): def test_all_devices_manufacturer_device_model_state_is_valid(self): devices = get_devices_from_response_dict(self.response_dict, device_types.ALL_SUPPORTED_DEVICES) skip_types = [WinkKey, WinkPorkfolioBalanceSensor, WinkPorkfolioNose, WinkPowerStripOutlet, - WinkSiren, WinkEggtray, WinkRemote, WinkPowerStrip, WinkAirConditioner, WinkPropaneTank] + WinkSiren, WinkEggtray, WinkRemote, WinkPowerStrip, WinkAirConditioner, WinkPropaneTank, + WinkCloudClockDial, WinkCloudClockAlarm, WinkCloudClock] devices_with_no_device_model = ["GoControl Thermostat", "New Shortcut", "Test robot", "August lock"] for device in devices: if type(device) in skip_types: @@ -119,7 +122,7 @@ def test_all_devices_manufacturer_device_id_state_is_valid(self): devices = get_devices_from_response_dict(self.response_dict, device_types.ALL_SUPPORTED_DEVICES) skip_types = [WinkKey, WinkPowerStrip, WinkPowerStripOutlet, WinkPorkfolioBalanceSensor, WinkPorkfolioNose, WinkSiren, WinkEggtray, WinkRemote, WinkButton, WinkAirConditioner, WinkPropaneTank, - WinkGeZwaveFan] + WinkGeZwaveFan, WinkCloudClockDial, WinkCloudClockAlarm, WinkCloudClock] skip_manufactuer_device_models = ["linear_wadwaz_1", "linear_wapirz_1", "aeon_labs_dsb45_zwus", "wink_hub", "wink_hub2", "sylvania_sylvania_ct", "ge_bulb", "quirky_ge_spotter", "schlage_zwave_lock", "home_decorators_home_decorators_fan", "sylvania_sylvania_rgbw", "somfy_bali", "wink_relay_sensor", "wink_project_one", "kidde_smoke_alarm", @@ -138,25 +141,23 @@ def test_all_devices_manufacturer_device_id_state_is_valid(self): def test_all_devices_device_manufacturer_is_valid(self): devices = get_devices_from_response_dict(self.response_dict, device_types.ALL_SUPPORTED_DEVICES) device_with_no_manufacturer = ["GoControl Thermostat", "New Shortcut", "Test robot", "August lock"] + skip_types = [WinkKey, WinkPowerStripOutlet, WinkCloudClockDial, WinkCloudClockAlarm] for device in devices: - if type(device) is WinkKey: + if type(device) in skip_types: self.assertIsNone(device.device_manufacturer()) elif device.name() in device_with_no_manufacturer: self.assertIsNone(device.device_manufacturer()) - elif type(device) is WinkPowerStripOutlet: - self.assertIsNone(device.device_manufacturer()) else: self.assertIsNotNone(device.device_manufacturer()) def test_all_devices_model_name_is_valid(self): devices = get_devices_from_response_dict(self.response_dict, device_types.ALL_SUPPORTED_DEVICES) devices_with_no_model_name = ["GoControl Thermostat", "New Shortcut", "Test robot", "August lock"] + skip_types = [WinkKey, WinkPowerStripOutlet, WinkCloudClockDial, WinkCloudClockAlarm] for device in devices: - if type(device) is WinkKey: + if type(device) in skip_types: self.assertIsNone(device.model_name()) elif device.name() in devices_with_no_model_name: self.assertIsNone(device.model_name()) - elif type(device) is WinkPowerStripOutlet: - self.assertIsNone(device.model_name()) else: self.assertIsNotNone(device.model_name()) diff --git a/src/pywink/test/devices/nimbus_test.py b/src/pywink/test/devices/nimbus_test.py new file mode 100644 index 0000000..d4a2399 --- /dev/null +++ b/src/pywink/test/devices/nimbus_test.py @@ -0,0 +1,119 @@ +import json +import os +import unittest +from datetime import datetime + +from pywink.api import get_devices_from_response_dict +from pywink.devices import types as device_types +from pywink.devices.cloud_clock import WinkCloudClock, WinkCloudClockDial, WinkCloudClockAlarm, _create_ical_string + + +class NimbusTests(unittest.TestCase): + + def test_cloud_clock_json_parses_into_the_correct_objects(self): + with open('{}/api_responses/nimbus.json'.format(os.path.dirname(__file__))) as nimbus_file: + response_dict = json.load(nimbus_file) + response_dict = {"data": [response_dict]} + devices = get_devices_from_response_dict(response_dict, device_types.CLOUD_CLOCK) + + self.assertEqual(len(devices), 8) + cloud_clock = devices[0] + self.assertTrue(isinstance(devices[0], WinkCloudClock)) + + self.assertTrue(isinstance(devices[1], WinkCloudClockDial)) + self.assertTrue(isinstance(devices[2], WinkCloudClockDial)) + self.assertTrue(isinstance(devices[3], WinkCloudClockDial)) + self.assertTrue(isinstance(devices[4], WinkCloudClockDial)) + self.assertTrue(isinstance(devices[5], WinkCloudClockAlarm)) + self.assertTrue(isinstance(devices[6], WinkCloudClockAlarm)) + self.assertTrue(isinstance(devices[7], WinkCloudClockAlarm)) + self.assertEqual(devices[1].parent, cloud_clock) + self.assertEqual(devices[2].parent, cloud_clock) + self.assertEqual(devices[3].parent, cloud_clock) + self.assertEqual(devices[4].parent, cloud_clock) + self.assertEqual(devices[5].parent, cloud_clock) + self.assertEqual(devices[6].parent, cloud_clock) + self.assertEqual(devices[7].parent, cloud_clock) + + def test_cloud_clock_dials_have_the_correct_value(self): + with open('{}/api_responses/nimbus.json'.format(os.path.dirname(__file__))) as nimbus_file: + response_dict = json.load(nimbus_file) + response_dict = {"data": [response_dict]} + devices = get_devices_from_response_dict(response_dict, device_types.CLOUD_CLOCK) + + time_dial = devices[1] + porkfolio_dial = devices[3] + custom_dial = devices[4] + + self.assertEqual(time_dial.state(), 41639.0) + self.assertEqual(porkfolio_dial.state(), 1327.0) + self.assertEqual(custom_dial.state(), 212.0) + + self.assertEqual(time_dial.position(), 346.99166666666667) + self.assertEqual(porkfolio_dial.position(), 296.658) + self.assertEqual(custom_dial.position(), 212.0) + + self.assertEqual(time_dial.labels(), ["11:33 AM", "New York"]) + self.assertEqual(porkfolio_dial.labels(), ["$13.27", "PORK"]) + self.assertEqual(custom_dial.labels(), ["212"]) + + self.assertEqual(time_dial.rotation(), "cw") + self.assertEqual(porkfolio_dial.rotation(), "cw") + self.assertEqual(custom_dial.rotation(), "ccw") + + self.assertEqual(time_dial.position(), 346.99166666666667) + self.assertEqual(porkfolio_dial.position(), 296.658) + self.assertEqual(custom_dial.position(), 212.0) + + self.assertEqual(time_dial.max_value(), 86400) + self.assertEqual(porkfolio_dial.max_value(), 5000) + self.assertEqual(custom_dial.max_value(), 360) + + self.assertEqual(time_dial.min_value(), 0) + self.assertEqual(porkfolio_dial.min_value(), 0) + self.assertEqual(custom_dial.min_value(), 0) + + self.assertEqual(time_dial.ticks(), 12) + self.assertEqual(porkfolio_dial.ticks(), 12) + self.assertEqual(custom_dial.ticks(), 12) + + self.assertEqual(time_dial.min_position(), 0) + self.assertEqual(porkfolio_dial.min_position(), -135) + self.assertEqual(custom_dial.min_position(), 0) + + self.assertEqual(time_dial.max_position(), 720) + self.assertEqual(porkfolio_dial.max_position(), 135) + self.assertEqual(custom_dial.max_position(), 360) + + def test_get_time_dial_returns_the_time_dial(self): + with open('{}/api_responses/nimbus.json'.format(os.path.dirname(__file__))) as nimbus_file: + response_dict = json.load(nimbus_file) + response_dict = {"data": [response_dict]} + devices = get_devices_from_response_dict(response_dict, device_types.CLOUD_CLOCK) + + cloud_clock = devices[0] + time_dial = devices[1] + + self.assertEqual(cloud_clock.get_time_dial(), time_dial.json_state) + + def test_get_alarm_state_returns_the_correct_value(self): + with open('{}/api_responses/nimbus.json'.format(os.path.dirname(__file__))) as nimbus_file: + response_dict = json.load(nimbus_file) + response_dict = {"data": [response_dict]} + devices = get_devices_from_response_dict(response_dict, device_types.CLOUD_CLOCK) + + first_alarm = devices[5] + self.assertEqual(first_alarm.state(), 1533576003) + + def test_create_ical_string(self): + with open('{}/api_responses/nimbus.json'.format(os.path.dirname(__file__))) as nimbus_file: + response_dict = json.load(nimbus_file) + response_dict = {"data": [response_dict]} + devices = get_devices_from_response_dict(response_dict, device_types.CLOUD_CLOCK) + + first_alarm = devices[5] + the_date = datetime.strptime('20180804T233251', '%Y%m%dT%H%M%S') + self.assertEqual(first_alarm.recurrence(), _create_ical_string("America/New_York", the_date, ["SA"])) + self.assertEqual("DTSTART;TZID=America/New_York:20180804T233251\nRRULE:FREQ=DAILY", _create_ical_string("America/New_York", the_date, "DAILY")) + self.assertEquals("DTSTART;TZID=America/New_York:20180804T233251\nRRULE:FREQ=WEEKLY;BYDAY=MO", _create_ical_string("America/New_York", the_date, ["TEST", "MO"])) + diff --git a/src/setup.py b/src/setup.py index 0f259e1..8c529e7 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='1.9.1', + version='1.10.0', description='Access Wink devices via the Wink API', url='http://github.com/python-wink/python-wink', author='Brad Johnson, William Scanlon', From 88df2fba78cedc626761927e8efd8276476c9f6d Mon Sep 17 00:00:00 2001 From: w1ll1am23 Date: Sun, 2 Sep 2018 09:32:19 -0400 Subject: [PATCH 169/178] Updates to before_install script to try to fix pypi deploy --- script/before_install | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/script/before_install b/script/before_install index 8a75877..4ffafc4 100755 --- a/script/before_install +++ b/script/before_install @@ -1,6 +1,6 @@ -install: - echo "Installing dependencies..." - python3 -m pip install --upgrade requests>=2,<3 +echo "Installing dependencies..." +python3 -m pip install --upgrade "requests>=2,<3" - echo "Installing development dependencies.." - python3 -m pip install --upgrade pip setuptools flake8 pylint python-coveralls pytest pytest-cov +echo "Installing development dependencies.." +python3 -m pip install --upgrade pip setuptools flake8 pylint python-coveralls pytest pytest-cov +rm -r .pytest_cache From 528f5a8b8f557621fd185c0f335d2fc3781ddabe Mon Sep 17 00:00:00 2001 From: w1ll1am23 Date: Sun, 2 Sep 2018 09:34:17 -0400 Subject: [PATCH 170/178] Removed unnecessary rm at end of before_install --- script/before_install | 1 - 1 file changed, 1 deletion(-) diff --git a/script/before_install b/script/before_install index 4ffafc4..1244f9e 100755 --- a/script/before_install +++ b/script/before_install @@ -3,4 +3,3 @@ python3 -m pip install --upgrade "requests>=2,<3" echo "Installing development dependencies.." python3 -m pip install --upgrade pip setuptools flake8 pylint python-coveralls pytest pytest-cov -rm -r .pytest_cache From 3f7d6040690433d28c51325e79885120d51e62fe Mon Sep 17 00:00:00 2001 From: w1ll1am23 Date: Sun, 2 Sep 2018 09:38:58 -0400 Subject: [PATCH 171/178] Added pytest_cache directory removal to test script --- script/test | 2 ++ 1 file changed, 2 insertions(+) diff --git a/script/test b/script/test index b1d69f6..f7ffc57 100755 --- a/script/test +++ b/script/test @@ -20,3 +20,5 @@ then else exit $LINT_STATUS fi + +rm -r .pytest_cache From 9883e181992d14b94969cbbbb4ac0506b387952f Mon Sep 17 00:00:00 2001 From: w1ll1am23 Date: Sun, 2 Sep 2018 09:47:37 -0400 Subject: [PATCH 172/178] Added before_deploy to try to navigate to the src directory --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 8baa847..de9b0fd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,6 +12,8 @@ after_success: - coveralls matrix: fast_finish: true +before_deploy: +- cd src/ deploy: provider: pypi distributions: sdist From 6c751df545e8a49c650a39d85b7622c6c5117c92 Mon Sep 17 00:00:00 2001 From: w1ll1am23 Date: Sun, 9 Sep 2018 10:22:24 -0400 Subject: [PATCH 173/178] Fixed nimbus bugs --- CHANGELOG.md | 5 ++++- src/pywink/devices/cloud_clock.py | 24 ++++++++++++++---------- src/setup.py | 2 +- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 183fb39..897a2d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ # Change Log -## 2.0.0 +## 1.10.1 +- Fixed nimbus bugs found while integrating with Home Assistant + +## 1.10.0 - Support for Quirky Nimbus ## 1.9.1 diff --git a/src/pywink/devices/cloud_clock.py b/src/pywink/devices/cloud_clock.py index 0b90a07..cf40f7a 100644 --- a/src/pywink/devices/cloud_clock.py +++ b/src/pywink/devices/cloud_clock.py @@ -28,10 +28,10 @@ def set_dial(self, json_value, index, timezone=None): """ values = self.json_state + values["nonce"] = str(random.randint(0, 1000000000)) if timezone is None: json_value["channel_configuration"] = {"channel_id": "10"} values["dials"][index] = json_value - response = self.api_interface.set_device_state(self, values) else: json_value["channel_configuration"] = {"channel_id": "1", "timezone": timezone} @@ -137,6 +137,7 @@ def set_recurrence(self, date, days=None): return True +# pylint: disable=too-many-public-methods class WinkCloudClockDial(WinkDevice): """ Represents a Quirky nimbus dial. @@ -173,6 +174,9 @@ def min_position(self): def max_position(self): return self.json_state['dial_configuration'].get('max_position') + def scale(self): + return self.json_state['dial_configuration'].get('scale_type') + def available(self): return self.json_state.get('connection', False) @@ -190,18 +194,18 @@ def _update_state_from_response(self, response_json): :param response_json: the json obj returned from query :return: """ - cloud_clock = response_json.get('data') - self.parent.json_state = cloud_clock - - if cloud_clock is None: - return False + if response_json.get('data') is not None: + cloud_clock = response_json.get('data') + else: + cloud_clock = response_json + self.parent.json_state = {**self.parent.json_state, **cloud_clock} cloud_clock_last_reading = cloud_clock.get('last_reading') dials = cloud_clock.get('dials') for dial in dials: if dial.get('object_id') == self.object_id(): dial['connection'] = cloud_clock_last_reading.get('connection') - self.json_state = dial + self.json_state = {**self.json_state, **dial} return True return False @@ -231,15 +235,15 @@ def set_configuration(self, min_value, max_value, rotation="cw", scale="linear", :param min_value: Any number :param max_value: Any number above min_value :param rotation: (String) cw or ccw - :param scale: (String) Linear and ... + :param scale: (String) linear or log :param ticks:(Int) number of ticks of the clock up to 360? :param min_position: (Int) 0-360 :param max_position: (Int) 0-360 :return: """ - _json = {"min_value": min_value, "max_value": max_value, "rotation": rotation, "scale": scale, "ticks": ticks, - "min_position": min_position, "max_position": max_position} + _json = {"min_value": min_value, "max_value": max_value, "rotation": rotation, "scale_type": scale, + "num_ticks": ticks, "min_position": min_position, "max_position": max_position} dial_config = {"dial_configuration": _json} diff --git a/src/setup.py b/src/setup.py index 8c529e7..c138685 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='1.10.0', + version='1.10.1', description='Access Wink devices via the Wink API', url='http://github.com/python-wink/python-wink', author='Brad Johnson, William Scanlon', From a1e244fc03a8472723944caeae7f40fa93fdb451 Mon Sep 17 00:00:00 2001 From: w1ll1am23 Date: Sun, 9 Sep 2018 10:30:05 -0400 Subject: [PATCH 174/178] Removed python 3.5 only dictionary update. --- src/pywink/devices/cloud_clock.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pywink/devices/cloud_clock.py b/src/pywink/devices/cloud_clock.py index cf40f7a..f7a1508 100644 --- a/src/pywink/devices/cloud_clock.py +++ b/src/pywink/devices/cloud_clock.py @@ -198,14 +198,14 @@ def _update_state_from_response(self, response_json): cloud_clock = response_json.get('data') else: cloud_clock = response_json - self.parent.json_state = {**self.parent.json_state, **cloud_clock} + self.parent.json_state = cloud_clock cloud_clock_last_reading = cloud_clock.get('last_reading') dials = cloud_clock.get('dials') for dial in dials: if dial.get('object_id') == self.object_id(): dial['connection'] = cloud_clock_last_reading.get('connection') - self.json_state = {**self.json_state, **dial} + self.json_state = dial return True return False From 22816732603d9909ebb9bf5266f6eb38e9ea67f7 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Sat, 26 Jan 2019 18:45:39 -0500 Subject: [PATCH 175/178] Added eggtray egg times (#114) * Added eggtray egg times --- CHANGELOG.md | 3 +++ script/before_install | 3 ++- src/pywink/devices/eggtray.py | 3 +++ src/pywink/test/api_test.py | 2 +- src/pywink/test/devices/nimbus_test.py | 2 +- src/pywink/test/devices/sensor_test.py | 9 +++++++++ src/setup.py | 2 +- 7 files changed, 20 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 897a2d9..681dfc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 1.10.2 +- Added get_eggs which returns the times of each egg in an eggminder + ## 1.10.1 - Fixed nimbus bugs found while integrating with Home Assistant diff --git a/script/before_install b/script/before_install index 1244f9e..381e442 100755 --- a/script/before_install +++ b/script/before_install @@ -2,4 +2,5 @@ echo "Installing dependencies..." python3 -m pip install --upgrade "requests>=2,<3" echo "Installing development dependencies.." -python3 -m pip install --upgrade pip setuptools flake8 pylint python-coveralls pytest pytest-cov +python3 -m pip install --upgrade pip setuptools flake8 pylint coverage python-coveralls +python3 -m pip install --upgrade pip setuptools flake8 pytest pytest-cov diff --git a/src/pywink/devices/eggtray.py b/src/pywink/devices/eggtray.py index ac3dc27..bec8d2c 100644 --- a/src/pywink/devices/eggtray.py +++ b/src/pywink/devices/eggtray.py @@ -20,3 +20,6 @@ def unit(self): def state(self): return self._last_reading.get("inventory") + + def eggs(self): + return self.json_state.get("eggs") diff --git a/src/pywink/test/api_test.py b/src/pywink/test/api_test.py index 5ac4030..4cc5471 100644 --- a/src/pywink/test/api_test.py +++ b/src/pywink/test/api_test.py @@ -532,7 +532,7 @@ def test_get_cloud_clock_dial_updated_states_from_api(self): self.assertEqual(dial.max_position(), 125) self.assertEqual(dial.max_value(), 123) - self.assertEquals(dial.rotation(), "ccw") + self.assertEqual(dial.rotation(), "ccw") def test_set_all_device_names(self): WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) diff --git a/src/pywink/test/devices/nimbus_test.py b/src/pywink/test/devices/nimbus_test.py index d4a2399..5c4818e 100644 --- a/src/pywink/test/devices/nimbus_test.py +++ b/src/pywink/test/devices/nimbus_test.py @@ -115,5 +115,5 @@ def test_create_ical_string(self): the_date = datetime.strptime('20180804T233251', '%Y%m%dT%H%M%S') self.assertEqual(first_alarm.recurrence(), _create_ical_string("America/New_York", the_date, ["SA"])) self.assertEqual("DTSTART;TZID=America/New_York:20180804T233251\nRRULE:FREQ=DAILY", _create_ical_string("America/New_York", the_date, "DAILY")) - self.assertEquals("DTSTART;TZID=America/New_York:20180804T233251\nRRULE:FREQ=WEEKLY;BYDAY=MO", _create_ical_string("America/New_York", the_date, ["TEST", "MO"])) + self.assertEqual("DTSTART;TZID=America/New_York:20180804T233251\nRRULE:FREQ=WEEKLY;BYDAY=MO", _create_ical_string("America/New_York", the_date, ["TEST", "MO"])) diff --git a/src/pywink/test/devices/sensor_test.py b/src/pywink/test/devices/sensor_test.py index 48fcd20..e14db0e 100644 --- a/src/pywink/test/devices/sensor_test.py +++ b/src/pywink/test/devices/sensor_test.py @@ -74,6 +74,15 @@ def test_unit_is_eggs(self): for device in devices: self.assertEqual(device.unit(), "eggs") + def test_eggs(self): + devices = get_devices_from_response_dict(self.response_dict, device_types.EGGTRAY) + for device in devices: + for egg in device.eggs(): + try: + val = float(egg) + except ValueError: + self.fail("test_eggs raised ValueError unexpectedly.") + class KeyTests(unittest.TestCase): diff --git a/src/setup.py b/src/setup.py index c138685..d4ba04c 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='1.10.1', + version='1.10.2', description='Access Wink devices via the Wink API', url='http://github.com/python-wink/python-wink', author='Brad Johnson, William Scanlon', From cf8bdce8c6518f30b91b23aa7aa32e89c2ce48da Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Sun, 27 Jan 2019 17:28:55 -0500 Subject: [PATCH 176/178] Added session post call (#115) --- CHANGELOG.md | 3 +++ src/pywink/__init__.py | 2 +- src/pywink/api.py | 25 +++++++++++++++++++++++-- src/setup.py | 2 +- 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 681dfc2..e6d9230 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 1.10.3 +- Added the ability to send a POST to the session endpoint for PubNub fix + ## 1.10.2 - Added get_eggs which returns the times of each egg in an eggminder diff --git a/src/pywink/__init__.py b/src/pywink/__init__.py index 5389198..403f4e6 100644 --- a/src/pywink/__init__.py +++ b/src/pywink/__init__.py @@ -6,7 +6,7 @@ set_wink_credentials, set_user_agent, wink_api_fetch, get_devices, \ get_subscription_key, get_user, get_authorization_url, \ request_token, legacy_set_wink_credentials, get_current_oauth_credentials, \ - disable_local_control + disable_local_control, post_session from pywink.api import get_light_bulbs, get_garage_doors, get_locks, \ get_powerstrips, get_shades, get_sirens, \ diff --git a/src/pywink/api.py b/src/pywink/api.py index 152a64b..d57f7b0 100644 --- a/src/pywink/api.py +++ b/src/pywink/api.py @@ -2,6 +2,7 @@ import time import logging import urllib.parse +import random import requests @@ -329,13 +330,11 @@ def piggy_bank_deposit(self, device, _json): url_string = "{}/{}s/{}/deposits".format(self.BASE_URL, device.object_type(), device.object_id()) - print(url_string) try: arequest = requests.post(url_string, data=json.dumps(_json), headers=API_HEADERS) response_json = arequest.json() - print(json.dumps(response_json, indent=4, sort_keys=True)) return response_json except requests.exceptions.RequestException: return None @@ -462,6 +461,28 @@ def get_user(): return arequest.json() +def post_session(): + """ + This endpoint appears to be required in order to keep pubnub updates flowing for some user. + + This just posts a random nonce to the /users/me/session endpoint and returns the result. + """ + + url_string = "{}/users/me/session".format(WinkApiInterface.BASE_URL) + + nonce = ''.join([str(random.randint(0, 9)) for i in range(9)]) + _json = {"nonce": str(nonce)} + + try: + arequest = requests.post(url_string, + data=json.dumps(_json), + headers=API_HEADERS) + response_json = arequest.json() + return response_json + except requests.exceptions.RequestException: + return None + + def get_local_control_access_token(local_control_id): _LOGGER.debug("Local_control_id: %s", local_control_id) if CLIENT_ID and CLIENT_SECRET and REFRESH_TOKEN: diff --git a/src/setup.py b/src/setup.py index d4ba04c..4e2fbdc 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='1.10.2', + version='1.10.3', description='Access Wink devices via the Wink API', url='http://github.com/python-wink/python-wink', author='Brad Johnson, William Scanlon', From 46f6ada704177f53f6c111a902f4805739a68ba2 Mon Sep 17 00:00:00 2001 From: w1ll1am23 Date: Sun, 5 May 2019 19:34:23 -0400 Subject: [PATCH 177/178] Consume the origin URL for use with subscriptions --- CHANGELOG.md | 3 +++ src/pywink/__init__.py | 2 +- src/pywink/api.py | 11 +++++++++-- src/setup.py | 2 +- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6d9230..e76abb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 1.10.4 +- Consume the origin URL in the subscription block + ## 1.10.3 - Added the ability to send a POST to the session endpoint for PubNub fix diff --git a/src/pywink/__init__.py b/src/pywink/__init__.py index 403f4e6..3fdfacf 100644 --- a/src/pywink/__init__.py +++ b/src/pywink/__init__.py @@ -4,7 +4,7 @@ # noqa from pywink.api import set_bearer_token, refresh_access_token, \ set_wink_credentials, set_user_agent, wink_api_fetch, get_devices, \ - get_subscription_key, get_user, get_authorization_url, \ + get_subscription_details, get_user, get_authorization_url, \ request_token, legacy_set_wink_credentials, get_current_oauth_credentials, \ disable_local_control, post_session diff --git a/src/pywink/api.py b/src/pywink/api.py index d57f7b0..e840321 100644 --- a/src/pywink/api.py +++ b/src/pywink/api.py @@ -658,11 +658,13 @@ def get_shade_groups(): return shade_groups -def get_subscription_key(): +def get_subscription_details(): response_dict = wink_api_fetch() try: first_device = response_dict.get('data')[0] - return get_subscription_key_from_response_dict(first_device) + origin = get_subscription_origin(first_device) + key = get_subscription_key_from_response_dict(first_device) + return key, origin except IndexError: raise WinkAPIException("No Wink devices associated with account.") @@ -672,6 +674,11 @@ def get_subscription_key_from_response_dict(device): return device.get("subscription").get("pubnub").get("subscribe_key") return None +def get_subscription_origin(device): + if "subscription" in device: + return device.get("subscription").get("pubnub").get("origin") + return None + def wink_api_fetch(end_point='wink_devices', retry=True): arequest_url = "{}/users/me/{}".format(WinkApiInterface.BASE_URL, end_point) diff --git a/src/setup.py b/src/setup.py index 4e2fbdc..0ecdf6d 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='1.10.3', + version='1.10.4', description='Access Wink devices via the Wink API', url='http://github.com/python-wink/python-wink', author='Brad Johnson, William Scanlon', From 908e89c0e49ed8d6f1352d2c9d547985dec0ab31 Mon Sep 17 00:00:00 2001 From: w1ll1am23 Date: Mon, 6 May 2019 09:26:39 -0400 Subject: [PATCH 178/178] Bumped version --- src/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/setup.py b/src/setup.py index 0ecdf6d..15aab8c 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='python-wink', - version='1.10.4', + version='1.10.5', description='Access Wink devices via the Wink API', url='http://github.com/python-wink/python-wink', author='Brad Johnson, William Scanlon',