diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000..906139c
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +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/.gitignore b/.gitignore
index 9886e1e..a7f2abb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,10 @@
+/python_wink.egg-info
+/dist
*.py[cod]
+/test.py
+/.cache
+/src/python_wink.egg-info
+/.coverage
+/.idea/*
+
+src/*tst\.py
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..de9b0fd
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,24 @@
+sudo: false
+language: python
+cache: pip
+python:
+- 3.4
+- 3.5
+install:
+- script/before_install
+script:
+- script/test
+after_success:
+- coveralls
+matrix:
+ fast_finish: true
+before_deploy:
+- cd src/
+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
new file mode 100644
index 0000000..e76abb7
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,241 @@
+# 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
+
+## 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
+
+## 1.10.0
+- Support for Quirky Nimbus
+
+## 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.
+
+## 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
+
+## 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
+- Use string formatting when logging
+
+## 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.
+
+## 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
+
+## 1.5.0
+- Change device names and hub pairing commands
+
+## 1.4.2
+- Changed try/except in local control calls to catch all errors
+
+## 1.4.1
+- Added timeout to local control calls
+
+## 1.4.0
+- Local control support for lights, locks, and switches
+
+## 1.3.1
+- Fixed fans speed selection
+
+## 1.3.0
+- Support for switch and light groups
+
+## 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
+
+## 1.2.4
+- Added call to return users account details /users/me
+
+## 1.2.3
+- Wink Aros Bugfix
+
+## 1.2.2
+- Siren inherits from Base device
+
+## 1.2.1
+- Set default endpoint in wink_api_fetch
+
+## 1.2.0
+- Robot and Scene support
+
+## 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
+- Support for Dropcam sensors
+- Fix for leaksmart valves
+
+## 1.0.0
+- Switch to object_type for device type detection
+- 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.
+
+## 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)
+
+## 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
+
+## 0.9.0
+- Support for Wink Smoke and CO detectors
+
+## 0.8.0
+- Support for Wink relay sensors and email/password auth
+
+## 0.7.15
+- Fix for PIR multisensors
+
+## 0.7.14
+- Return False on RGB support if HSB is also supported.
+
+## 0.7.13
+- Changed method of detecting WinkBulb capabilities
+
+## 0.7.12
+- Made WinkBulb constructor python 3-compatible.
+
+## 0.7.11
+- Added Wink leak sensor support
+
+## 0.7.10
+- Changed API URL
+
+## 0.7.9
+- Added Wink keys (Wink Lock user codes)
+
+## 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
+
+## 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.
+
+## 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
+
+## 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())
+
+## 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
+
+## 0.6.3
+- Override capability sensor device_id during update.
+
+## 0.6.2
+- Changed sensor brightness to boolean.
+- Added UNIT to all sensors.
+
+## 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.
+
+## 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.
+
+## 0.4.2
+- Added init method for WinkSiren
+
+## 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
+
+## 0.3.3
+- Added init method for Wink Power strip
+
+## 0.3.2
+- Added init method for WinkGarageDoor
+
+## 0.3.1.1
+- Changed mock to test-only dependency
+
+## 0.3.1
+- Added init method for WinkEggTray
+
+## 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
+
+## 0.2.0
+- Initial work by balloob, ryanturner, and miniconfig
diff --git a/README.md b/README.md
index 68dd088..43eab92 100644
--- a/README.md
+++ b/README.md
@@ -1,29 +1,44 @@
-Python Wink API
----------------
+
Python Wink API
-### Script works but no longer maintained. Looking for maintainers.
+[](https://gitter.im/bradsk88/python-wink?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [](https://travis-ci.org/python-wink/python-wink)
+[](https://coveralls.io/github/python-wink/python-wink?branch=master)
-Python implementation of the Wink API supporting switches, light bulbs and sensors.
+_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._
-Script by [John McLaughlin](https://github.com/loghound).
+_This library also has support for the unoffical local API and doesn't require a rooted hub._
-_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 provides support for Wink in Home Assistant!
-## Authentication
+## To install
+```bash
+pip3 install python-wink
+```
-You will need a Wink API bearer token to communicate with the Wink server.
+## Get developer credentials
-[Get yours using this web app.](https://winkbearertoken.appspot.com/)
+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.setState(!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/pylintrc b/pylintrc
new file mode 100644
index 0000000..b15d618
--- /dev/null
+++ b/pylintrc
@@ -0,0 +1,18 @@
+[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
+# fixme - TODOs are allowed prior to v1.0
+# locally-disabled - Because that's the whole point!
+disable=
+ missing-docstring
+ , global-statement
+ , invalid-name
+ , fixme
+ , locally-disabled
+ , duplicate-code
diff --git a/pywink/__init__.py b/pywink/__init__.py
deleted file mode 100644
index 74fbde6..0000000
--- a/pywink/__init__.py
+++ /dev/null
@@ -1,408 +0,0 @@
-__author__ = 'JOHNMCL'
-
-import json
-import time
-
-import requests
-
-baseUrl = "https://winkapi.quirky.com"
-
-headers = {}
-
-class wink_device(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 "thermostat_id" in 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())
- arequest = requests.get(urlString, headers=headers)
- self._updateStateFromResponse(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)
-
-
-class wink_sensor_pod(wink_device) :
- """ 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
- 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, aJSonObj, objectprefix="sensor_pods"):
- self.jsonState = aJSonObj
- self.objectprefix = objectprefix
-
- def __repr__(self):
- return "" % (self.name(), self.deviceId(), 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 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
- 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, aJSonObj, objectprefix="binary_switches"):
- 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('powered', False)
-
- def deviceId(self):
- return self.jsonState.get('binary_switch_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": {"powered": 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_powered') == last_read.get('powered') \
- 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
-
-
-class wink_bulb(wink_binary_switch):
- """ represents a wink.py bulb
- json_obj holds the json stat at init (and if there is a refresh it's updated
- it's the native format for this objects methods
- and looks like so:
-
- "light_bulb_id": "33990",
- "name": "downstaurs lamp",
- "locale": "en_us",
- "units":{},
- "created_at": 1410925804,
- "hidden_at": null,
- "capabilities":{},
- "subscription":{},
- "triggers":[],
- "desired_state":{"powered": true, "brightness": 1},
- "manufacturer_device_model": "lutron_p_pkg1_w_wh_d",
- "manufacturer_device_id": null,
- "device_manufacturer": "lutron",
- "model_name": "Caseta Wireless Dimmer & Pico",
- "upc_id": "3",
- "hub_id": "11780",
- "local_id": "8",
- "radio_type": "lutron",
- "linked_service_id": null,
- "last_reading":{
- "brightness": 1,
- "brightness_updated_at": 1417823487.490747,
- "connection": true,
- "connection_updated_at": 1417823487.4907365,
- "powered": true,
- "powered_updated_at": 1417823487.4907532,
- "desired_powered": true,
- "desired_powered_updated_at": 1417823485.054675,
- "desired_brightness": 1,
- "desired_brightness_updated_at": 1417409293.2591703
- },
- "lat_lng":[38.429962, -122.653715],
- "location": "",
- "order": 0
-
- """
- jsonState = {}
-
- def __init__(self, ajsonobj):
- super().__init__(ajsonobj, "light_bulbs")
-
- def deviceId(self):
- return self.jsonState.get('light_bulb_id', self.name())
-
- def brightness(self):
- return self._last_reading.get('brightness')
-
- def setState(self, state, brightness=None):
- """
- :param state: a boolean of true (on) or false ('off')
- :return: nothing
- """
- urlString = baseUrl + "/light_bulbs/%s" % self.deviceId()
- values = {
- "desired_state": {
- "powered": state
- }
- }
-
- 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)
- self._updateStateFromResponse(arequest.json())
-
- self._last_call = (time.time(), state)
-
- def __repr__(self):
- return "" % (
- self.name(), self.deviceId(), self.state())
-
-
-def get_devices(filter):
- arequestUrl = baseUrl + "/users/me/wink_devices"
- j = requests.get(arequestUrl, 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))
-
- return devices
-
-
-def get_bulbs():
- return get_devices('light_bulb_id')
-
-
-def get_switches():
- return get_devices('binary_switch_id')
-
-
-def get_sensors():
- return get_devices('sensor_pod_id')
-
-
-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)
- }
-
-if __name__ == "__main__":
- sw = get_bulbs()
- lamp = sw[3]
- lamp.setState(False)
diff --git a/script/__init__.py b/script/__init__.py
new file mode 100755
index 0000000..e69de29
diff --git a/script/before_install b/script/before_install
new file mode 100755
index 0000000..381e442
--- /dev/null
+++ b/script/before_install
@@ -0,0 +1,6 @@
+echo "Installing dependencies..."
+python3 -m pip install --upgrade "requests>=2,<3"
+
+echo "Installing development dependencies.."
+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/script/lint b/script/lint
new file mode 100755
index 0000000..94f51f2
--- /dev/null
+++ b/script/lint
@@ -0,0 +1,21 @@
+# Run style checks
+
+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=$?
+
+if [ $FLAKE8_STATUS -eq 0 ]
+then
+ exit $PYLINT_STATUS
+else
+ exit $FLAKE8_STATUS
+fi
diff --git a/script/test b/script/test
new file mode 100755
index 0000000..f7ffc57
--- /dev/null
+++ b/script/test
@@ -0,0 +1,24 @@
+#!/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")/.."
+
+script/lint
+
+LINT_STATUS=$?
+
+echo "Running tests..."
+
+py.test --cov=src
+TEST_STATUS=$?
+
+if [ $LINT_STATUS -eq 0 ]
+then
+ exit $TEST_STATUS
+else
+ exit $LINT_STATUS
+fi
+
+rm -r .pytest_cache
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
diff --git a/setup.py b/setup.py
deleted file mode 100644
index b34d8e1..0000000
--- a/setup.py
+++ /dev/null
@@ -1,11 +0,0 @@
-from setuptools import setup
-
-setup(name='python-wink',
- version='0.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'],
- packages=['pywink'],
- zip_safe=True)
diff --git a/src/pywink/__init__.py b/src/pywink/__init__.py
new file mode 100644
index 0000000..3fdfacf
--- /dev/null
+++ b/src/pywink/__init__.py
@@ -0,0 +1,21 @@
+"""
+Top level functions
+"""
+# noqa
+from pywink.api import set_bearer_token, refresh_access_token, \
+ set_wink_credentials, set_user_agent, wink_api_fetch, get_devices, \
+ get_subscription_details, get_user, get_authorization_url, \
+ request_token, legacy_set_wink_credentials, get_current_oauth_credentials, \
+ disable_local_control, post_session
+
+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_light_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, \
+ 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
new file mode 100644
index 0000000..e840321
--- /dev/null
+++ b/src/pywink/api.py
@@ -0,0 +1,738 @@
+import json
+import time
+import logging
+import urllib.parse
+import random
+
+import requests
+
+from .devices import types as device_types
+from .devices.factory import build_device, get_object_type
+
+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:
+
+ BASE_URL = "https://api.wink.com"
+ api_headers = API_HEADERS
+
+ def set_device_state(self, device, state, id_override=None, type_override=None):
+ """
+ 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()
+ object_type = type_override or device.object_type()
+ url_string = "{}/{}s/{}".format(self.BASE_URL,
+ object_type,
+ object_id)
+ if state is None or object_type == "group":
+ url_string += "/activate"
+ if state is None:
+ arequest = requests.post(url_string,
+ headers=API_HEADERS)
+ else:
+ arequest = requests.post(url_string,
+ data=json.dumps(state),
+ 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:
+ arequest = requests.put(url_string,
+ data=json.dumps(state),
+ headers=API_HEADERS)
+ else:
+ raise WinkAPIException("Failed to refresh access token.")
+ response_json = arequest.json()
+ _LOGGER.debug('%s', response_json)
+ return response_json
+
+ # 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())
+ 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)
+ _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)
+ try:
+ arequest = requests.put(url_string,
+ data=json.dumps(state),
+ headers=LOCAL_API_HEADERS,
+ verify=False, timeout=3)
+ 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()
+ _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
+ 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):
+ """
+ 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()
+ 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)
+ response_json = arequest.json()
+ _LOGGER.debug('%s', response_json)
+ return response_json
+
+ # 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.
+
+ 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
+ """
+ if ALLOW_LOCAL_CONTROL:
+ if device.local_id() is not None:
+ hub = HUBS.get(device.hub_id())
+ if hub is not None and hub["token"] 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)
+ try:
+ arequest = requests.get(url_string,
+ headers=LOCAL_API_HEADERS,
+ verify=False, timeout=3)
+ 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()
+ _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
+ return temp_state
+ 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)
+ 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):
+ """
+ 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:
+ (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)
+
+ try:
+ arequest = requests.delete(url_string,
+ headers=API_HEADERS)
+ if arequest.status_code == 204:
+ return True
+ _LOGGER.error("Failed to remove device. Status code: %s", arequest.status_code)
+ return False
+ except requests.exceptions.RequestException:
+ _LOGGER.error("Failed to remove device.")
+ 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)
+ 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 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())
+ 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 disable_local_control():
+ global ALLOW_LOCAL_CONTROL
+ ALLOW_LOCAL_CONTROL = False
+
+
+def set_user_agent(user_agent):
+ _LOGGER.info("Setting user agent to %s", 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):
+ _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
+ CLIENT_SECRET = client_secret
+
+ data = {
+ "client_id": client_id,
+ "client_secret": client_secret,
+ "grant_type": "password",
+ "email": email,
+ "password": password
+ }
+ 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')
+ set_bearer_token(access_token)
+
+
+def set_wink_credentials(client_id, client_secret, access_token, refresh_token):
+ _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
+ CLIENT_SECRET = client_secret
+ REFRESH_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,
+ "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')
+ 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: %s redirect_uri: %s", 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):
+ _LOGGER.debug("code: %s Client_secret: %s", 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)
+ _LOGGER.debug('%s', response)
+ 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():
+ url_string = "{}/users/me".format(WinkApiInterface.BASE_URL)
+ arequest = requests.get(url_string, headers=API_HEADERS)
+ _LOGGER.debug('%s', arequest)
+ 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:
+ 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('%s', 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():
+ return get_devices(device_types.ALL_SUPPORTED_DEVICES)
+
+
+def get_light_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.EGGTRAY)
+
+
+def get_garage_doors():
+ return get_devices(device_types.GARAGE_DOOR)
+
+
+def get_shades():
+ return get_devices(device_types.SHADE)
+
+
+def get_powerstrips():
+ return get_devices(device_types.POWERSTRIP)
+
+
+def get_sirens():
+ return get_devices(device_types.SIREN)
+
+
+def get_keys():
+ return get_devices(device_types.KEY)
+
+
+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_thermostats():
+ return get_devices(device_types.THERMOSTAT)
+
+
+def get_hubs():
+ hubs = get_devices(device_types.HUB)
+ for hub in hubs:
+ if hub.manufacturer_device_model() in SUPPORTS_LOCAL_CONTROL:
+ _id = hub.local_control_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
+
+
+def get_fans():
+ return get_devices(device_types.FAN)
+
+
+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_air_conditioners():
+ return get_devices(device_types.AIR_CONDITIONER)
+
+
+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_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"):
+ # 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_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_details():
+ response_dict = wink_api_fetch()
+ try:
+ first_device = response_dict.get('data')[0]
+ 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.")
+
+
+def get_subscription_key_from_response_dict(device):
+ if "subscription" in 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)
+ 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:
+ # 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)
+ raise WinkAPIException("401 Response from Wink API.")
+ raise WinkAPIException("Unexpected")
+
+
+def get_devices(device_type, end_point="wink_devices"):
+ global ALL_DEVICES, LAST_UPDATE
+
+ 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)
+ if end_point in ("robots", "scenes", "groups"):
+ json_data = wink_api_fetch(end_point)
+ return get_devices_from_response_dict(json_data, device_type)
+ _LOGGER.error("Invalid endpoint %s", end_point)
+ return {}
+
+
+def get_devices_from_response_dict(response_dict, device_type):
+ """
+ :rtype: list of WinkDevice
+ """
+ items = response_dict.get('data')
+
+ devices = []
+
+ api_interface = WinkApiInterface()
+ check_list = isinstance(device_type, (list,))
+
+ for item in items:
+ 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)
+
+ return devices
+
+
+class WinkAPIException(Exception):
+ pass
diff --git a/src/pywink/devices/__init__.py b/src/pywink/devices/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/pywink/devices/air_conditioner.py b/src/pywink/devices/air_conditioner.py
new file mode 100644
index 0000000..57b786d
--- /dev/null
+++ b/src/pywink/devices/air_conditioner.py
@@ -0,0 +1,101 @@
+from ..devices.base import WinkDevice
+
+
+# pylint: disable=too-many-public-methods
+class WinkAirConditioner(WinkDevice):
+ """
+ Represents a Wink air conditioner.
+ """
+
+ 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_operation_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 max_set_point: a float for the max set point 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/base.py b/src/pywink/devices/base.py
new file mode 100644
index 0000000..3359ff2
--- /dev/null
+++ b/src/pywink/devices/base.py
@@ -0,0 +1,107 @@
+class WinkDevice:
+ """
+ This is a generic Wink device, all other object inherit from this.
+ """
+
+ def __init__(self, device_state_as_json, api_interface):
+ """
+ :type api_interface pywink.api.WinkApiInterface:
+ :return:
+ """
+ self.api_interface = 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')
+ self.pubnub_key = pubnub.get('subscribe_key')
+ self.pubnub_channel = pubnub.get('channel')
+
+ 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")
+
+ def object_id(self):
+ return self.obj_id
+
+ 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 {}
+
+ def available(self):
+ return self._last_reading.get('connection', False)
+
+ def battery_level(self):
+ if not self._last_reading.get('external_power'):
+ 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')
+
+ def manufacturer_device_id(self):
+ return self.json_state.get('manufacturer_device_id')
+
+ def device_manufacturer(self):
+ return self.json_state.get('device_manufacturer')
+
+ 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
+ :return:
+ """
+ _response_json = response_json.get('data')
+ if _response_json is not None:
+ self.json_state = _response_json
+ return True
+ return 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)
+
+ def pubnub_update(self, json_response):
+ if json_response is not None:
+ self.json_state = json_response
+ else:
+ self.update_state()
diff --git a/src/pywink/devices/binary_switch.py b/src/pywink/devices/binary_switch.py
new file mode 100644
index 0000000..ebce2a6
--- /dev/null
+++ b/src/pywink/devices/binary_switch.py
@@ -0,0 +1,48 @@
+from ..devices.base import WinkDevice
+
+SUPPORTED_BINARY_STATE_FIELDS = ['powered', 'opened']
+
+
+class WinkBinarySwitch(WinkDevice):
+ """
+ Represents a Wink binary switch.
+ """
+
+ def state(self):
+ _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
+ """
+ _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 binary_state_name(self):
+ """
+ Search all of the capabilities of the device and return the supported binary state field.
+ Default to returning powered.
+ """
+ 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, type_override="binary_switche")
+ return self._update_state_from_response(response)
diff --git a/src/pywink/devices/binary_switch_group.py b/src/pywink/devices/binary_switch_group.py
new file mode 100644
index 0000000..be7992b
--- /dev/null
+++ b/src/pywink/devices/binary_switch_group.py
@@ -0,0 +1,34 @@
+from ..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/button.py b/src/pywink/devices/button.py
new file mode 100644
index 0000000..b6c2c73
--- /dev/null
+++ b/src/pywink/devices/button.py
@@ -0,0 +1,23 @@
+from ..devices.binary_switch import WinkBinarySwitch
+
+
+class WinkButton(WinkBinarySwitch):
+ """
+ Represents a Wink relay button.
+ """
+
+ 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..67016cd
--- /dev/null
+++ b/src/pywink/devices/camera.py
@@ -0,0 +1,37 @@
+from ..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 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/cloud_clock.py b/src/pywink/devices/cloud_clock.py
new file mode 100644
index 0000000..f7a1508
--- /dev/null
+++ b/src/pywink/devices/cloud_clock.py
@@ -0,0 +1,319 @@
+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
+ 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}
+ 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
+
+
+# pylint: disable=too-many-public-methods
+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 scale(self):
+ return self.json_state['dial_configuration'].get('scale_type')
+
+ 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:
+ """
+ if response_json.get('data') is not None:
+ cloud_clock = response_json.get('data')
+ else:
+ cloud_clock = response_json
+ 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
+ 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 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_type": scale,
+ "num_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
new file mode 100644
index 0000000..bec8d2c
--- /dev/null
+++ b/src/pywink/devices/eggtray.py
@@ -0,0 +1,25 @@
+from ..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._capability = None
+ self._unit = "eggs"
+
+ def capability(self):
+ # Eggtray has no capability.
+ return self._capability
+
+ def unit(self):
+ return self._unit
+
+ def state(self):
+ return self._last_reading.get("inventory")
+
+ def eggs(self):
+ return self.json_state.get("eggs")
diff --git a/src/pywink/devices/factory.py b/src/pywink/devices/factory.py
new file mode 100644
index 0000000..ce9c580
--- /dev/null
+++ b/src/pywink/devices/factory.py
@@ -0,0 +1,208 @@
+"""
+Build Wink devices.
+"""
+
+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
+def build_device(device_state_as_json, api_interface):
+ # This is used to determine what type of object to create
+ object_type = get_object_type(device_state_as_json)
+ new_objects = []
+
+ if object_type == device_types.LIGHT_BULB:
+ 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_objects.append(WinkBinarySwitch(device_state_as_json, api_interface))
+ else:
+ new_objects.append(WinkBinarySwitch(device_state_as_json, api_interface))
+ elif object_type == device_types.LOCK:
+ new_objects.append(WinkLock(device_state_as_json, api_interface))
+ elif object_type == device_types.EGGTRAY:
+ new_objects.append(WinkEggtray(device_state_as_json, api_interface))
+ elif object_type == device_types.GARAGE_DOOR:
+ new_objects.append(WinkGarageDoor(device_state_as_json, api_interface))
+ elif object_type == device_types.SHADE:
+ new_objects.append(WinkShade(device_state_as_json, api_interface))
+ elif object_type == device_types.SIREN:
+ new_objects.append(WinkSiren(device_state_as_json, api_interface))
+ elif object_type == device_types.KEY:
+ new_objects.append(WinkKey(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:
+ 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.
+ if device_state_as_json.get("model_name") != "Pico":
+ new_objects.append(WinkRemote(device_state_as_json, api_interface))
+ elif object_type == device_types.HUB:
+ new_objects.append(WinkHub(device_state_as_json, api_interface))
+ elif object_type == device_types.SENSOR_POD:
+ new_objects.extend(__get_subsensors_from_device(device_state_as_json, api_interface))
+ elif object_type == device_types.POWERSTRIP:
+ 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.extend(__get_devices_from_piggy_bank(device_state_as_json, api_interface))
+ elif object_type == device_types.DOOR_BELL:
+ new_objects.extend(__get_subsensors_from_device(device_state_as_json, api_interface))
+ elif object_type == device_types.SPRINKLER:
+ new_objects.append(WinkSprinkler(device_state_as_json, api_interface))
+ elif object_type == device_types.BUTTON:
+ new_objects.append(WinkButton(device_state_as_json, api_interface))
+ elif object_type == device_types.GANG:
+ new_objects.append(WinkGang(device_state_as_json, api_interface))
+ elif object_type == device_types.SMOKE_DETECTOR:
+ 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_objects.append(WinkCanaryCamera(device_state_as_json, api_interface))
+ 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))
+ elif object_type == device_types.PROPANE_TANK:
+ new_objects.append(WinkPropaneTank(device_state_as_json, api_interface))
+ elif object_type == device_types.ROBOT:
+ 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 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 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:
+ 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
+
+
+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', []))
+
+ # 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):
+ return [WinkPorkfolioBalanceSensor(item, api_interface),
+ WinkPorkfolioNose(item, api_interface)]
+
+
+def __get_sensors_from_smoke_detector(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))
+ 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
new file mode 100644
index 0000000..4c58248
--- /dev/null
+++ b/src/pywink/devices/fan.py
@@ -0,0 +1,158 @@
+from ..devices.base import WinkDevice
+
+
+# pylint: disable=too-many-public-methods
+class WinkFan(WinkDevice):
+ """
+ Represents a Wink fan.
+ """
+ json_state = {}
+
+ 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', "lowest")
+
+ 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, speed=None):
+ """
+ :param state: bool
+ :param speed: a string one of ["lowest", "low",
+ "medium", "high", "auto"] defaults to last speed
+ :return: nothing
+ """
+ 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
+ })
+
+ self._update_state_from_response(response)
+
+ def set_fan_direction(self, direction):
+ """
+ :param direction: 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)
+
+
+# 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
+ }
+
+ def fan_speeds(self):
+ return ["low", "medium", "high"]
+
+ def fan_directions(self):
+ return []
+
+ def fan_timer_range(self):
+ return []
+
+ def current_fan_speed(self):
+ brightness = self._last_reading.get('brightness', 0.33)
+ if brightness <= 0.33:
+ return "low"
+ if brightness <= 0.66:
+ return "medium"
+ return "high"
+
+ 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/devices/gang.py b/src/pywink/devices/gang.py
new file mode 100644
index 0000000..fdbc505
--- /dev/null
+++ b/src/pywink/devices/gang.py
@@ -0,0 +1,19 @@
+from ..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..7437b12
--- /dev/null
+++ b/src/pywink/devices/garage_door.py
@@ -0,0 +1,25 @@
+from ..devices.base import WinkDevice
+
+
+class WinkGarageDoor(WinkDevice):
+ """
+ Represents a Wink garage door.
+ """
+
+ 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..c4a47ec
--- /dev/null
+++ b/src/pywink/devices/hub.py
@@ -0,0 +1,84 @@
+import logging
+
+from ..devices.base import WinkDevice
+
+_LOGGER = logging.getLogger(__name__)
+
+
+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')
+ # 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')
+
+ def ip_address(self):
+ return self._last_reading.get('ip_address')
+
+ def firmware_version(self):
+ return self._last_reading.get('firmware_version')
+
+ 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):
+ 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):
+ """
+ :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. %s", 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/key.py b/src/pywink/devices/key.py
new file mode 100644
index 0000000..c3cade2
--- /dev/null
+++ b/src/pywink/devices/key.py
@@ -0,0 +1,32 @@
+from ..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._capability = "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._capability
diff --git a/src/pywink/devices/light_bulb.py b/src/pywink/devices/light_bulb.py
new file mode 100644
index 0000000..d59de20
--- /dev/null
+++ b/src/pywink/devices/light_bulb.py
@@ -0,0 +1,166 @@
+from ..devices.base import WinkDevice
+
+
+class WinkLightBulb(WinkDevice):
+ """
+ Represents a Wink light bulb.
+ """
+
+ def state(self):
+ return self._last_reading.get('powered', False)
+
+ 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
+ :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 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):
+ """
+ :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)
+ if color_state is not None:
+ desired_state.update(color_state)
+
+ if brightness is not None:
+ desired_state.update({'brightness': brightness})
+
+ response = self.api_interface.local_set_state(self, {
+ "desired_state": desired_state
+ })
+ self._update_state_from_response(response)
+
+ 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 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)
+ if hsv is not None:
+ return _format_hue_saturation(hsv)
+
+ if self.supports_xy_color():
+ if color_xy is not None:
+ return _format_xy(color_xy)
+
+ return {}
+
+ def supports_hue_saturation(self):
+ 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 True
+ return False
+
+ def supports_xy_color(self):
+ 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 "xy" in choices:
+ return True
+ return False
+
+ def supports_temperature(self):
+ 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 "color_temperature" in choices:
+ return True
+ return False
+
+
+def _format_temperature(kelvin):
+ return {
+ "color_model": "color_temperature",
+ "color_temperature": kelvin,
+ }
+
+
+def _format_hue_saturation(hue_saturation):
+ hsv_iter = iter(hue_saturation)
+ return {
+ "color_model": "hsb",
+ "hue": next(hsv_iter),
+ "saturation": 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_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/light_group.py b/src/pywink/devices/light_group.py
new file mode 100644
index 0000000..234622d
--- /dev/null
+++ b/src/pywink/devices/light_group.py
@@ -0,0 +1,62 @@
+from ..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/lock.py b/src/pywink/devices/lock.py
new file mode 100644
index 0000000..e706d6b
--- /dev/null
+++ b/src/pywink/devices/lock.py
@@ -0,0 +1,93 @@
+from ..devices.base import WinkDevice
+
+
+class WinkLock(WinkDevice):
+ """
+ Represents a Wink lock.
+ """
+
+ 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.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)
+
+ 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/piggy_bank.py b/src/pywink/devices/piggy_bank.py
new file mode 100644
index 0000000..f7e017a
--- /dev/null
+++ b/src/pywink/devices/piggy_bank.py
@@ -0,0 +1,89 @@
+from ..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 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
+ 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
+
+ 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
new file mode 100644
index 0000000..84514ad
--- /dev/null
+++ b/src/pywink/devices/powerstrip.py
@@ -0,0 +1,91 @@
+from ..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 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 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())
+ self._update_state_from_response(response)
+
+ 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_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')
+ :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/propane_tank.py b/src/pywink/devices/propane_tank.py
new file mode 100644
index 0000000..5f86ea8
--- /dev/null
+++ b/src/pywink/devices/propane_tank.py
@@ -0,0 +1,34 @@
+from ..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._capability = None
+ self._unit = None
+
+ def capability(self):
+ # Propane tanks have no capability.
+ return self._capability
+
+ 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/remote.py b/src/pywink/devices/remote.py
new file mode 100644
index 0000000..3e7f959
--- /dev/null
+++ b/src/pywink/devices/remote.py
@@ -0,0 +1,37 @@
+from ..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/robot.py b/src/pywink/devices/robot.py
new file mode 100644
index 0000000..3492762
--- /dev/null
+++ b/src/pywink/devices/robot.py
@@ -0,0 +1,30 @@
+from ..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..c84c4b9
--- /dev/null
+++ b/src/pywink/devices/scene.py
@@ -0,0 +1,28 @@
+from ..devices.base import WinkDevice
+
+
+class WinkScene(WinkDevice):
+ """
+ Represents a Wink scene.
+ """
+
+ 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)
diff --git a/src/pywink/devices/sensor.py b/src/pywink/devices/sensor.py
new file mode 100644
index 0000000..def1173
--- /dev/null
+++ b/src/pywink/devices/sensor.py
@@ -0,0 +1,43 @@
+from ..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/shade.py b/src/pywink/devices/shade.py
new file mode 100644
index 0000000..fbc360c
--- /dev/null
+++ b/src/pywink/devices/shade.py
@@ -0,0 +1,19 @@
+from ..devices.base import WinkDevice
+
+
+class WinkShade(WinkDevice):
+ """
+ Represents a Wink Shade.
+ """
+
+ 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/shade_group.py b/src/pywink/devices/shade_group.py
new file mode 100644
index 0000000..2c9de6d
--- /dev/null
+++ b/src/pywink/devices/shade_group.py
@@ -0,0 +1,31 @@
+from ..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/devices/siren.py b/src/pywink/devices/siren.py
new file mode 100644
index 0000000..1f57a9e
--- /dev/null
+++ b/src/pywink/devices/siren.py
@@ -0,0 +1,159 @@
+from ..devices.base import WinkDevice
+
+
+class WinkSiren(WinkDevice):
+ """
+ Represents a Wink Siren.
+ """
+
+ def state(self):
+ return self._last_reading.get('powered', False)
+
+ 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]
+ :return: nothing
+ """
+ values = {
+ "desired_state": {
+ "mode": 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"]
+ :param cycles: Undocumented seems to have no effect?
+ :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 timer: an int, one of [None (never), -1, 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)
+ 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/pywink/devices/smoke_detector.py b/src/pywink/devices/smoke_detector.py
new file mode 100644
index 0000000..e618742
--- /dev/null
+++ b/src/pywink/devices/smoke_detector.py
@@ -0,0 +1,71 @@
+from ..devices.sensor import WinkDevice
+
+
+class WinkBaseSmokeDetector(WinkDevice):
+ """Represents a base smoke detector sensor."""
+
+ 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._capability = capability
+
+ def unit(self):
+ return self._unit
+
+ def unit_type(self):
+ return self._unit_type
+
+ def capability(self):
+ return self._capability
+
+ def name(self):
+ return self.json_state.get("name") + " " + self.capability()
+
+ def state(self):
+ return self._last_reading.get(self.capability())
+
+ def test_activated(self):
+ return self._last_reading.get("test_activated")
+
+
+class WinkSmokeDetector(WinkBaseSmokeDetector):
+ """
+ Represents a Wink Smoke detector.
+ """
+
+ def __init__(self, device_state_as_json, api_interface):
+ capability = "smoke_detected"
+ unit_type = "boolean"
+ super(WinkSmokeDetector, self).__init__(device_state_as_json, api_interface, unit_type, capability)
+
+
+class WinkSmokeSeverity(WinkBaseSmokeDetector):
+ """
+ Represents a Wink/Nest Smoke severity sensor.
+ """
+
+ 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(WinkBaseSmokeDetector):
+ """
+ Represents a Wink CO detector.
+ """
+
+ def __init__(self, device_state_as_json, api_interface):
+ capability = "co_detected"
+ unit_type = "boolean"
+ super(WinkCoDetector, self).__init__(device_state_as_json, api_interface, unit_type, capability)
+
+
+class WinkCoSeverity(WinkBaseSmokeDetector):
+ """
+ Represents a Wink/Nest CO severity sensor.
+ """
+
+ def __init__(self, device_state_as_json, api_interface):
+ capability = "co_severity"
+ super(WinkCoSeverity, self).__init__(device_state_as_json, api_interface, None, capability)
diff --git a/src/pywink/devices/sprinkler.py b/src/pywink/devices/sprinkler.py
new file mode 100644
index 0000000..393bc55
--- /dev/null
+++ b/src/pywink/devices/sprinkler.py
@@ -0,0 +1,17 @@
+from ..devices.binary_switch import WinkBinarySwitch
+
+
+class WinkSprinkler(WinkBinarySwitch):
+ """
+ Represents a Wink Sprinkler.
+ """
+
+ 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/thermostat.py b/src/pywink/devices/thermostat.py
new file mode 100644
index 0000000..a2b7854
--- /dev/null
+++ b/src/pywink/devices/thermostat.py
@@ -0,0 +1,200 @@
+from ..devices.base import WinkDevice
+
+
+# pylint: disable=too-many-public-methods
+class WinkThermostat(WinkDevice):
+ """
+ Represents a Wink thermostat.
+ """
+
+ def state(self):
+ return self.current_hvac_mode()
+
+ 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):
+ """
+ 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 not None:
+ return nest
+ if ecobee is not None:
+ if ecobee == "home":
+ return False
+ return True
+ return None
+
+ 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 profile(self):
+ return self._last_reading.get('profile')
+
+ 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):
+ 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):
+ 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
+
+ This function handles both ecobee and nest thermostats
+ which use a different field for away/home status.
+ """
+ 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
+ })
+
+ 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 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 = {}
+
+ 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 cool_on(self):
+ return self._last_reading.get('cool_active')
+
+ def heat_on(self):
+ return self._last_reading.get('heat_active')
diff --git a/src/pywink/devices/types.py b/src/pywink/devices/types.py
new file mode 100644
index 0000000..c69d6d7
--- /dev/null
+++ b/src/pywink/devices/types.py
@@ -0,0 +1,39 @@
+"""
+These are all the devices we currently support.
+"""
+
+LIGHT_BULB = 'light_bulb'
+BINARY_SWITCH = 'binary_switch'
+SENSOR_POD = 'sensor_pod'
+LOCK = 'lock'
+EGGTRAY = 'eggtray'
+GARAGE_DOOR = 'garage_door'
+POWERSTRIP = 'powerstrip'
+SHADE = 'shade'
+SIREN = 'siren'
+KEY = 'key'
+PIGGY_BANK = 'piggy_bank'
+SMOKE_DETECTOR = 'smoke_detector'
+THERMOSTAT = 'thermostat'
+HUB = 'hub'
+FAN = 'fan'
+DOOR_BELL = 'door_bell'
+REMOTE = 'remote'
+SPRINKLER = 'sprinkler'
+BUTTON = 'button'
+GANG = 'gang'
+CAMERA = 'camera'
+AIR_CONDITIONER = 'air_conditioner'
+PROPANE_TANK = 'propane_tank'
+ROBOT = 'robot'
+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,
+ CLOUD_CLOCK]
diff --git a/src/pywink/devices/water_heater.py b/src/pywink/devices/water_heater.py
new file mode 100644
index 0000000..fbf0461
--- /dev/null
+++ b/src/pywink/devices/water_heater.py
@@ -0,0 +1,71 @@
+from ..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 set_point: a float for the set point 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/__init__.py b/src/pywink/test/__init__.py
new file mode 100644
index 0000000..f648b26
--- /dev/null
+++ b/src/pywink/test/__init__.py
@@ -0,0 +1,16 @@
+"""
+Top level functions
+"""
+# 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
+
+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..4cc5471
--- /dev/null
+++ b/src/pywink/test/api_test.py
@@ -0,0 +1,726 @@
+from http.server import BaseHTTPRequestHandler, HTTPServer
+import re
+import socket
+from threading import Thread
+import unittest
+import os
+
+# Third-party imports...
+from unittest.mock import MagicMock, Mock
+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 = {}
+
+
+class ApiTests(unittest.TestCase):
+
+ def setUp(self):
+ 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:
+ 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()
+
+ def test_local_control_enabled_by_default(self):
+ self.assertTrue(ALLOW_LOCAL_CONTROL)
+
+ def test_that_disable_local_control_works(self):
+ from ..api import ALLOW_LOCAL_CONTROL
+ disable_local_control()
+ self.assertFalse(ALLOW_LOCAL_CONTROL)
+
+ def test_set_user_agent(self):
+ set_user_agent("THIS IS A TEST")
+ self.assertEqual("THIS IS A TEST", API_HEADERS["User-Agent"])
+
+ def test_set_bearer_token(self):
+ 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/"
+ wink_api_fetch()
+ except Exception as e:
+ self.assertTrue(type(e), WinkAPIException)
+ try:
+ WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port) + "/404/"
+ wink_api_fetch()
+ except Exception as e:
+ self.assertTrue(type(e), WinkAPIException)
+
+ 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), 85)
+ 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) or isinstance(fan, WinkGeZwaveFan))
+ 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))
+ 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)
+ sensor_types = [WinkSensor, WinkHub, WinkPorkfolioBalanceSensor, WinkKey, WinkRemote,
+ WinkGang, WinkSmokeDetector, WinkSmokeSeverity,
+ WinkCoDetector, WinkCoSeverity, WinkButton, WinkRobot]
+ # No way to validate scene is activated, so skipping.
+ skip_types = [WinkPowerStripOutlet, WinkCanaryCamera, WinkScene, WinkCloudClock, WinkCloudClockDial, WinkCloudClockAlarm]
+ 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 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
+ 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
+ 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 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
+ 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
+ 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_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_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()
+ 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)
+ 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.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)
+ 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_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_operation_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_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_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()
+ for device in devices:
+ if isinstance(device, WinkCanaryCamera):
+ device.api_interface = self.api_interface
+ device.set_mode("away")
+ device.set_privacy(True)
+ device.update_state()
+ 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)
+ devices = get_fans()
+ for device in devices:
+ device.api_interface = self.api_interface
+ 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()
+ 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):
+ WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port)
+ devices = get_propane_tanks()
+ 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.assertEqual(dial.rotation(), "ccw")
+
+ def test_set_all_device_names(self):
+ WinkApiInterface.BASE_URL = "http://localhost:" + str(self.port)
+ devices = get_all_devices()
+ for device in devices:
+ 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:
+ if not isinstance(device, WinkCloudClockAlarm) and not isinstance(device, WinkCloudClockDial):
+ self.assertTrue(device.name().startswith("TEST_NAME"))
+
+
+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]*')
+ GROUPS_PATTERN = re.compile(r'/groups')
+
+ 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
+ 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():
+ 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):
+ """
+
+ :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 "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:
+ 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
+ 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'):
+ _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:
+ 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
+ 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")
+ 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"].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:
+ set_state = state["desired_state"].get("position")
+ dict_device["reading_aggregation"]["position"]["average"] = set_state
+ return_dict["data"] = dict_device
+
+ 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):
+ """
+
+ :param device:
+ :param id_override:
+ :param type_override:
+ :return:
+ """
+ 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
+
+ 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/__init__.py b/src/pywink/test/devices/__init__.py
new file mode 100644
index 0000000..e69de29
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..a1850d6
--- /dev/null
+++ b/src/pywink/test/devices/air_conditioner_test.py
@@ -0,0 +1,85 @@
+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 AirConditionerTests(unittest.TestCase):
+
+ def setUp(self):
+ super(AirConditionerTests, self).setUp()
+ self.api_interface = 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_ac_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_ac_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_ac_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/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/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/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/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/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/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/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_fan.json b/src/pywink/test/devices/api_responses/ge_zwave_fan.json
new file mode 100644
index 0000000..374a4d3
--- /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.1
+ },
+ "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/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/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/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/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/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/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/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/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/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/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/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/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_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/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/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/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/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/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/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/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..6b10d0e
--- /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":true,
+ "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/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/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/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
new file mode 100644
index 0000000..b10a114
--- /dev/null
+++ b/src/pywink/test/devices/base_test.py
@@ -0,0 +1,163 @@
+import json
+import os
+import unittest
+
+from unittest.mock import MagicMock
+
+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):
+
+ def setUp(self):
+ super(BaseTests, self).setUp()
+ self.api_interface = MagicMock()
+ all_devices = os.listdir('{}/api_responses/'.format(os.path.dirname(__file__)))
+ self.response_dict = {}
+ device_list = []
+ for json_file in all_devices:
+ 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):
+ 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, WinkGeZwaveFan, WinkPorkfolioBalanceSensor, WinkPorkfolioNose, WinkBinarySwitch, WinkHub,
+ WinkLightBulb, WinkThermostat, WinkKey, WinkPowerStrip, WinkPowerStripOutlet,
+ WinkRemote, WinkShade, WinkSprinkler, WinkButton, WinkGang, WinkCanaryCamera,
+ WinkAirConditioner, WinkScene, WinkRobot, WinkWaterHeater, WinkCloudClock,
+ WinkCloudClockDial, WinkCloudClockAlarm]
+ for device in devices:
+ 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.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:
+ 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, 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:
+ self.assertIsNone(device.manufacturer_device_model())
+ elif device.name() in devices_with_no_device_model:
+ 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, WinkAirConditioner, WinkPropaneTank,
+ 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",
+ "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:
+ 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)
+ 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) in skip_types:
+ self.assertIsNone(device.device_manufacturer())
+ elif device.name() in device_with_no_manufacturer:
+ 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) in skip_types:
+ self.assertIsNone(device.model_name())
+ elif device.name() in devices_with_no_model_name:
+ 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..a1a4c33
--- /dev/null
+++ b/src/pywink/test/devices/fan_test.py
@@ -0,0 +1,61 @@
+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 FanTests(unittest.TestCase):
+
+ def setUp(self):
+ super(FanTests, self).setUp()
+ self.api_interface = 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..448c9b4
--- /dev/null
+++ b/src/pywink/test/devices/garage_door_test.py
@@ -0,0 +1,37 @@
+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 GarageDoorTests(unittest.TestCase):
+
+ def setUp(self):
+ super(GarageDoorTests, self).setUp()
+ self.api_interface = MagicMock()
+ all_devices = os.listdir('{}/api_responses/'.format(os.path.dirname(__file__)))
+ self.response_dict = {}
+ device_list = []
+ for json_file in all_devices:
+ 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):
+ 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/ge_zwave_fan_test.py b/src/pywink/test/devices/ge_zwave_fan_test.py
new file mode 100644
index 0000000..af2c437
--- /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_low(self):
+ fan = get_devices_from_response_dict(self.response_dict, device_types.FAN)[0]
+ 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]
+ self.assertFalse(fan.state())
diff --git a/src/pywink/test/devices/hub_test.py b/src/pywink/test/devices/hub_test.py
new file mode 100644
index 0000000..4232d74
--- /dev/null
+++ b/src/pywink/test/devices/hub_test.py
@@ -0,0 +1,61 @@
+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 HubTests(unittest.TestCase):
+
+ def setUp(self):
+ super(HubTests, self).setUp()
+ self.api_interface = MagicMock()
+ all_devices = os.listdir('{}/api_responses/'.format(os.path.dirname(__file__)))
+ self.response_dict = {}
+ device_list = []
+ for json_file in all_devices:
+ 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):
+ 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":
+ 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())
+
+ 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..3a457b4
--- /dev/null
+++ b/src/pywink/test/devices/light_bulb_test.py
@@ -0,0 +1,174 @@
+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.light_group import WinkLightGroup
+
+JSON_DATA = {}
+
+
+class LightBulbTests(unittest.TestCase):
+
+ def setUp(self):
+ super(LightBulbTests, self).setUp()
+ self.api_interface = 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_hsb_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.assertTrue(bulb.supports_hue_saturation())
+ 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(3, 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/lock_test.py b/src/pywink/test/devices/lock_test.py
new file mode 100644
index 0000000..2e3b514
--- /dev/null
+++ b/src/pywink/test/devices/lock_test.py
@@ -0,0 +1,49 @@
+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 LockTests(unittest.TestCase):
+
+ def setUp(self):
+ super(LockTests, self).setUp()
+ self.api_interface = 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/nimbus_test.py b/src/pywink/test/devices/nimbus_test.py
new file mode 100644
index 0000000..5c4818e
--- /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.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/powerstrip_test.py b/src/pywink/test/devices/powerstrip_test.py
new file mode 100644
index 0000000..046fd9e
--- /dev/null
+++ b/src/pywink/test/devices/powerstrip_test.py
@@ -0,0 +1,61 @@
+import json
+import os
+import unittest
+
+from pywink.api import get_devices_from_response_dict
+from pywink.devices import types as device_types
+
+
+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)
+ 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/scene_test.py b/src/pywink/test/devices/scene_test.py
new file mode 100644
index 0000000..4ba7f8e
--- /dev/null
+++ b/src/pywink/test/devices/scene_test.py
@@ -0,0 +1,27 @@
+import json
+import os
+import unittest
+
+from pywink.api import get_devices_from_response_dict
+from pywink.devices import types as device_types
+
+
+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_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
new file mode 100644
index 0000000..e14db0e
--- /dev/null
+++ b/src/pywink/test/devices/sensor_test.py
@@ -0,0 +1,277 @@
+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.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 = MagicMock()
+ all_devices = os.listdir('{}/api_responses/'.format(os.path.dirname(__file__)))
+ self.response_dict = {}
+ device_list = []
+ for json_file in all_devices:
+ 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):
+ 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 = MagicMock()
+ all_devices = os.listdir('{}/api_responses/'.format(os.path.dirname(__file__)))
+ self.response_dict = {}
+ device_list = []
+ for json_file in all_devices:
+ 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):
+ devices = get_devices_from_response_dict(self.response_dict, device_types.EGGTRAY)
+ 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")
+
+ 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):
+
+ def setUp(self):
+ super(KeyTests, self).setUp()
+ self.api_interface = MagicMock()
+ all_devices = os.listdir('{}/api_responses/'.format(os.path.dirname(__file__)))
+ self.response_dict = {}
+ device_list = []
+ for json_file in all_devices:
+ 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):
+ 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 = MagicMock()
+ all_devices = os.listdir('{}/api_responses/'.format(os.path.dirname(__file__)))
+ self.response_dict = {}
+ device_list = []
+ for json_file in all_devices:
+ 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):
+ 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 = MagicMock()
+ all_devices = os.listdir('{}/api_responses/'.format(os.path.dirname(__file__)))
+ self.response_dict = {}
+ device_list = []
+ for json_file in all_devices:
+ 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):
+ 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 = MagicMock()
+ all_devices = os.listdir('{}/api_responses/'.format(os.path.dirname(__file__)))
+ self.response_dict = {}
+ device_list = []
+ for json_file in all_devices:
+ 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):
+ 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:
+ 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 = MagicMock()
+ all_devices = os.listdir('{}/api_responses/'.format(os.path.dirname(__file__)))
+ self.response_dict = {}
+ device_list = []
+ for json_file in all_devices:
+ 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):
+ 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")
+
+
+class PropaneTankTests(unittest.TestCase):
+
+ def setUp(self):
+ super(PropaneTankTests, self).setUp()
+ self.api_interface = MagicMock()
+ all_devices = os.listdir('{}/api_responses/'.format(os.path.dirname(__file__)))
+ self.response_dict = {}
+ device_list = []
+ for json_file in all_devices:
+ 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):
+ 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/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/siren_test.py b/src/pywink/test/devices/siren_test.py
new file mode 100644
index 0000000..7f8d6cc
--- /dev/null
+++ b/src/pywink/test/devices/siren_test.py
@@ -0,0 +1,68 @@
+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 SirenTests(unittest.TestCase):
+
+ def setUp(self):
+ super(SirenTests, self).setUp()
+ self.api_interface = 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()
+ _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):
+ 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)
+
+ 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/switch_test.py b/src/pywink/test/devices/switch_test.py
new file mode 100644
index 0000000..e9efd5e
--- /dev/null
+++ b/src/pywink/test/devices/switch_test.py
@@ -0,0 +1,73 @@
+import json
+import os
+import unittest
+
+from pywink.api import get_devices_from_response_dict
+from pywink.devices import types as device_types
+from pywink.devices.binary_switch_group import WinkBinarySwitchGroup
+
+JSON_DATA = {}
+
+
+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())
+
+ 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(3, 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())
+
+ 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/pywink/test/devices/thermostat_test.py b/src/pywink/test/devices/thermostat_test.py
new file mode 100644
index 0000000..3e8254c
--- /dev/null
+++ b/src/pywink/test/devices/thermostat_test.py
@@ -0,0 +1,344 @@
+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 ThermostatTests(unittest.TestCase):
+
+ def setUp(self):
+ super(ThermostatTests, self).setUp()
+ self.api_interface = 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()
+ _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())
+
+ 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__)))
+ 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())
+
+ 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())
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..0769843
--- /dev/null
+++ b/src/pywink/test/devices/water_heater_test.py
@@ -0,0 +1,95 @@
+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 WaterHeaterTests(unittest.TestCase):
+
+ def setUp(self):
+ super(WaterHeaterTests, self).setUp()
+ self.api_interface = 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/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
new file mode 100644
index 0000000..15aab8c
--- /dev/null
+++ b/src/setup.py
@@ -0,0 +1,13 @@
+from setuptools import setup, find_packages
+
+setup(name='python-wink',
+ 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',
+ license='MIT',
+ install_requires=['requests>=2.0'],
+ tests_require=['mock'],
+ test_suite='tests',
+ packages=find_packages(exclude=["dist", "*.test", "*.test.*", "test.*", "test"]),
+ zip_safe=True)
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..d20f9fb
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,3 @@
+[flake8]
+max-line-length = 120
+exclude = src/pywink/__init__.py,src/pywink/test