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. +[![Join the chat at https://gitter.im/python-wink/python-wink](https://badges.gitter.im/python-wink/python-wink.svg)](https://gitter.im/bradsk88/python-wink?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Build Status](https://travis-ci.org/python-wink/python-wink.svg?branch=master)](https://travis-ci.org/python-wink/python-wink) +[![Coverage Status](https://coveralls.io/repos/github/python-wink/python-wink/badge.svg?branch=master)](https://coveralls.io/github/python-wink/python-wink?branch=master) -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