├── tests ├── __init__.py ├── mock_server │ ├── .gitignore │ ├── package.json │ └── index.js ├── mock │ ├── oauth_claims.py │ ├── logout.py │ ├── devices │ │ ├── __init__.py │ │ ├── alarm.py │ │ ├── keypad.py │ │ ├── unknown.py │ │ ├── siren.py │ │ ├── pir.py │ │ ├── status_display.py │ │ ├── water_sensor.py │ │ ├── glass.py │ │ ├── lm.py │ │ ├── door_contact.py │ │ ├── remote_controller.py │ │ ├── door_lock.py │ │ ├── secure_barrier.py │ │ ├── valve.py │ │ ├── power_switch_meter.py │ │ ├── power_switch_sensor.py │ │ ├── ir_camera.py │ │ ├── dimmer.py │ │ ├── hue.py │ │ └── ipcam.py │ ├── __init__.py │ ├── automation.py │ ├── user.py │ ├── panel.py │ └── login.py ├── test_secure_barrier.py ├── test_valve.py ├── test_power_switch_meter.py ├── test_door_lock.py ├── test_power_switch_sensor.py ├── test_dimmer.py ├── test_binary_sensor.py ├── test_lm.py ├── test_alarm.py └── test_hue.py ├── .pytest_cache └── v │ └── cache │ └── .gitignore ├── abodepy ├── helpers │ ├── __init__.py │ ├── errors.py │ └── timeline.py ├── devices │ ├── binary_sensor.py │ ├── lock.py │ ├── valve.py │ ├── cover.py │ ├── switch.py │ ├── sensor.py │ ├── alarm.py │ ├── light.py │ ├── camera.py │ └── __init__.py ├── exceptions.py ├── utils.py ├── automation.py ├── socketio.py └── event_controller.py ├── requirements.txt ├── .coveragerc ├── requirements_test.txt ├── .travis.yml ├── pylintrc ├── LICENSE ├── tox.ini ├── setup.py ├── .gitignore └── CHANGES.rst /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Init file for tests directory.""" 2 | -------------------------------------------------------------------------------- /.pytest_cache/v/cache/.gitignore: -------------------------------------------------------------------------------- 1 | /nodeids 2 | /lastfailed 3 | -------------------------------------------------------------------------------- /abodepy/helpers/__init__.py: -------------------------------------------------------------------------------- 1 | """Init file for helpers directory.""" 2 | -------------------------------------------------------------------------------- /tests/mock_server/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /package-lock.json 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests>=2.12.4 2 | lomond>=0.3.3 3 | colorlog>=3.0.1 4 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | abodepy/__main__.py 4 | abodepy/helpers/* 5 | tests/* 6 | setup.py -------------------------------------------------------------------------------- /tests/mock_server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "abodepy_mock_server", 3 | "version": "0.0.1", 4 | "description": "mock server for abodepy testing", 5 | "dependencies": { 6 | "body-parser": "^1.18.2", 7 | "express": "^4.15.2", 8 | "socket.io": "^2.1.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /requirements_test.txt: -------------------------------------------------------------------------------- 1 | flake8>=3.6.0 2 | flake8-docstrings==1.1.0 3 | pylint==2.4.2 4 | pydocstyle==2.0.0 5 | pytest==5.2.4 6 | pytest-cov>=2.3.1 7 | pytest-sugar==0.9.2 8 | pytest-timeout>=1.0.0 9 | restructuredtext-lint>=1.0.1 10 | pygments>=2.2.0 11 | requests_mock>=1.3.0 12 | psutil==5.6.6 13 | tox==3.14.0 14 | -------------------------------------------------------------------------------- /tests/mock/oauth_claims.py: -------------------------------------------------------------------------------- 1 | """Mock Abode Claims Response.""" 2 | 3 | from tests.mock import OAUTH_TOKEN 4 | 5 | 6 | def get_response_ok(oauth_token=OAUTH_TOKEN): 7 | """Return the oauth2 claims token.""" 8 | return ''' 9 | { 10 | "token_type":"Bearer", 11 | "access_token":"''' + oauth_token + '''", 12 | "expires_in":3600 13 | }''' 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | matrix: 2 | fast_finish: true 3 | include: 4 | - python: "3.5" 5 | env: TOXENV=py35 6 | - python: "3.6" 7 | env: TOXENV=py36 8 | - python: "3.7" 9 | env: TOXENV=py37 10 | - python: "3.8" 11 | env: TOXENV=py38 12 | 13 | install: pip install -U tox coveralls 14 | language: python 15 | script: tox 16 | after_success: coveralls 17 | -------------------------------------------------------------------------------- /tests/mock/logout.py: -------------------------------------------------------------------------------- 1 | """Mock Abode Logout Response.""" 2 | 3 | 4 | def post_response_ok(): 5 | """Return the successful logout response json.""" 6 | return '{"code":200,"message":"Logout successful."}' 7 | 8 | 9 | def post_response_bad_request(): 10 | """Return the failed logout response json.""" 11 | return ''' 12 | { 13 | "code":400,"message":"Some logout error occurred.", 14 | "detail":null 15 | }''' 16 | -------------------------------------------------------------------------------- /tests/mock/devices/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Mock devices that mimic actual data from Abode servers. 3 | 4 | This file should be updated any time the Abode server responses 5 | change so we can test that abodepy can still communicate. 6 | """ 7 | 8 | EMPTY_DEVICE_RESPONSE = '[]' 9 | 10 | 11 | def status_put_response_ok(devid, status): 12 | """Return status change response json.""" 13 | return '{"id": "' + devid + '", "status": "' + str(status) + '"}' 14 | 15 | 16 | def level_put_response_ok(devid, level): 17 | """Return level change response json.""" 18 | return '{"id": "' + devid + '", "level": "' + str(level) + '"}' 19 | -------------------------------------------------------------------------------- /abodepy/devices/binary_sensor.py: -------------------------------------------------------------------------------- 1 | """Abode binary sensor device.""" 2 | 3 | from abodepy.devices import AbodeDevice 4 | import abodepy.helpers.constants as CONST 5 | 6 | 7 | class AbodeBinarySensor(AbodeDevice): 8 | """Class to represent an on / off, online/offline sensor.""" 9 | 10 | @property 11 | def is_on(self): 12 | """ 13 | Get sensor state. 14 | 15 | Assume offline or open (worst case). 16 | """ 17 | if self._type == 'Occupancy': 18 | return self.status not in CONST.STATUS_ONLINE 19 | return self.status not in (CONST.STATUS_OFF, CONST.STATUS_OFFLINE, 20 | CONST.STATUS_CLOSED) 21 | -------------------------------------------------------------------------------- /pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | reports=no 3 | good-names=m 4 | notes=FIXME,XXX 5 | 6 | # Reasons disabled: 7 | # locally-disabled - it spams too much 8 | # duplicate-code - it's annoying 9 | # unused-argument - generic callbacks and setup methods create a lot of warnings 10 | # too-many-* - are not enforced for the sake of readability 11 | # too-few-* - same as too-many-* 12 | 13 | disable= 14 | locally-disabled, 15 | unused-argument, 16 | duplicate-code, 17 | too-many-arguments, 18 | too-many-branches, 19 | too-many-instance-attributes, 20 | too-many-locals, 21 | too-many-public-methods, 22 | too-many-return-statements, 23 | too-many-statements, 24 | too-many-lines, 25 | too-few-public-methods, -------------------------------------------------------------------------------- /tests/mock/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Mock responses that mimic actual data from Abode servers. 3 | 4 | This file should be updated any time the Abode server responses 5 | change so we can test that abodepy can still communicate. 6 | """ 7 | 8 | AUTH_TOKEN = 'web-1eb04ba2236d85f49d4b9b4bb91665f2' 9 | OAUTH_TOKEN = 'ohyeahthisisanoauthtoken' 10 | 11 | 12 | def response_forbidden(): 13 | """Return the invalid API key response json.""" 14 | return '{"code":403,"message":"Invalid API Key"}' 15 | 16 | 17 | def generic_response_ok(): 18 | """ 19 | Return the successful generic change response json. 20 | 21 | Used for settings changes. 22 | """ 23 | return '{"code":200,"message":"OK"}' 24 | -------------------------------------------------------------------------------- /abodepy/exceptions.py: -------------------------------------------------------------------------------- 1 | """The exceptions used by AbodePy.""" 2 | 3 | 4 | class AbodeException(Exception): 5 | """Class to throw general abode exception.""" 6 | 7 | def __init__(self, error, details=None): 8 | """Initialize AbodeException.""" 9 | # Call the base class constructor with the parameters it needs 10 | super(AbodeException, self).__init__(error[1]) 11 | 12 | self.errcode = error[0] 13 | self.message = error[1] 14 | self.details = details 15 | 16 | 17 | class AbodeAuthenticationException(AbodeException): 18 | """Class to throw authentication exception.""" 19 | 20 | 21 | class SocketIOException(AbodeException): 22 | """Class to throw SocketIO Error exception.""" 23 | -------------------------------------------------------------------------------- /tests/mock/devices/alarm.py: -------------------------------------------------------------------------------- 1 | """ 2 | Mock Internal Alarm Device. 3 | 4 | AbodePy creates an internal device that is a standard panel response that 5 | includes a few required device fields. This is so that we can easily translate 6 | the panel/alarm itself into a Home Assistant device. 7 | """ 8 | import json 9 | import abodepy.helpers.constants as CONST 10 | 11 | import tests.mock.panel as PANEL 12 | 13 | 14 | def device(area='1', panel=PANEL.get_response_ok(mode=CONST.MODE_STANDBY)): 15 | """Alarm mock device.""" 16 | alarm = json.loads(panel) 17 | alarm['name'] = CONST.ALARM_NAME 18 | alarm['id'] = CONST.ALARM_DEVICE_ID + area 19 | alarm['type'] = CONST.ALARM_TYPE 20 | alarm['type_tag'] = CONST.DEVICE_ALARM 21 | alarm['generic_type'] = CONST.TYPE_ALARM 22 | alarm['uuid'] = alarm.get('mac').replace(':', '').lower() 23 | 24 | return alarm 25 | -------------------------------------------------------------------------------- /abodepy/devices/lock.py: -------------------------------------------------------------------------------- 1 | """Abode lock device.""" 2 | 3 | from abodepy.devices import AbodeDevice 4 | import abodepy.helpers.constants as CONST 5 | 6 | 7 | class AbodeLock(AbodeDevice): 8 | """Class to represent a door lock.""" 9 | 10 | def lock(self): 11 | """Lock the device.""" 12 | success = self.set_status(CONST.STATUS_LOCKCLOSED_INT) 13 | 14 | if success: 15 | self._json_state['status'] = CONST.STATUS_LOCKCLOSED 16 | 17 | return success 18 | 19 | def unlock(self): 20 | """Unlock the device.""" 21 | success = self.set_status(CONST.STATUS_LOCKOPEN_INT) 22 | 23 | if success: 24 | self._json_state['status'] = CONST.STATUS_LOCKOPEN 25 | 26 | return success 27 | 28 | @property 29 | def is_locked(self): 30 | """ 31 | Get locked state. 32 | 33 | Err on side of caution, assume if lock isn't closed then it's open. 34 | """ 35 | return self.status in CONST.STATUS_LOCKCLOSED 36 | -------------------------------------------------------------------------------- /abodepy/devices/valve.py: -------------------------------------------------------------------------------- 1 | """Abode valve device.""" 2 | 3 | from abodepy.devices.switch import AbodeSwitch 4 | import abodepy.helpers.constants as CONST 5 | 6 | 7 | class AbodeValve(AbodeSwitch): 8 | """Class to add valve functionality.""" 9 | 10 | def switch_on(self): 11 | """Open the valve.""" 12 | success = self.set_status(CONST.STATUS_ON_INT) 13 | 14 | if success: 15 | self._json_state['status'] = CONST.STATUS_OPEN 16 | 17 | return success 18 | 19 | def switch_off(self): 20 | """Close the valve.""" 21 | success = self.set_status(CONST.STATUS_OFF_INT) 22 | 23 | if success: 24 | self._json_state['status'] = CONST.STATUS_CLOSED 25 | 26 | return success 27 | 28 | @property 29 | def is_on(self): 30 | """ 31 | Get switch state. 32 | 33 | Assume switch is on. 34 | """ 35 | return self.status not in (CONST.STATUS_CLOSED, CONST.STATUS_OFFLINE) 36 | 37 | @property 38 | def is_dimmable(self): 39 | """Device dimmable.""" 40 | return False 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Mister Wil 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /abodepy/utils.py: -------------------------------------------------------------------------------- 1 | """Abodepy utility methods.""" 2 | import logging 3 | import pickle 4 | import uuid 5 | 6 | _LOGGER = logging.getLogger(__name__) 7 | 8 | 9 | def save_cache(data, filename): 10 | """Save cookies to a file.""" 11 | with open(filename, 'wb') as handle: 12 | pickle.dump(data, handle) 13 | 14 | 15 | def load_cache(filename): 16 | """Load cookies from a file.""" 17 | with open(filename, 'rb') as handle: 18 | try: 19 | return pickle.load(handle) 20 | except EOFError: 21 | _LOGGER.warning("Empty pickle file: %s", filename) 22 | except (pickle.UnpicklingError, ValueError): 23 | _LOGGER.warning("Corrupted pickle file: %s", filename) 24 | 25 | return None 26 | 27 | 28 | def gen_uuid(): 29 | """Generate a new Abode UUID.""" 30 | return str(uuid.uuid1()) 31 | 32 | 33 | def update(dct, dct_merge): 34 | """Recursively merge dicts.""" 35 | for key, value in dct_merge.items(): 36 | if key in dct and isinstance(dct[key], dict): 37 | dct[key] = update(dct[key], value) 38 | else: 39 | dct[key] = value 40 | return dct 41 | -------------------------------------------------------------------------------- /tests/mock/automation.py: -------------------------------------------------------------------------------- 1 | """Mock Abode Automation.""" 2 | 3 | 4 | def get_response_ok(name, enabled, aid): 5 | """Return automation json.""" 6 | return '''{ 7 | "name": "''' + name + '''", 8 | "enabled": "''' + str(enabled) + '''", 9 | "version": 2, 10 | "id": "''' + aid + '''", 11 | "subType": "", 12 | "actions": [{ 13 | "directive": { 14 | "trait": "panel.traits.panelMode", 15 | "name": "panel.directives.arm", 16 | "state": { 17 | "panelMode": "AWAY" 18 | } 19 | } 20 | }], 21 | "conditions": {}, 22 | "triggers": { 23 | "operator": "OR", 24 | "expressions": [{ 25 | "mobileDevices": ["89381", "658"], 26 | "property": { 27 | "trait": "mobile.traits.location", 28 | "name": "location", 29 | "rule": { 30 | "location": "31675", 31 | "equalTo": "LAST_OUT" 32 | } 33 | } 34 | }] 35 | } 36 | }''' 37 | -------------------------------------------------------------------------------- /tests/mock/user.py: -------------------------------------------------------------------------------- 1 | """Mock Abode User Response.""" 2 | 3 | 4 | def get_response_ok(): 5 | """Return the user response data.""" 6 | return '''{ 7 | "id":"user@email.com", 8 | "email":"user@email.com", 9 | "first_name":"John", 10 | "last_name":"Doe", 11 | "phone":"5555551212", 12 | "profile_pic":"https://website.com/default-image.svg", 13 | "address":"555 None St.", 14 | "city":"New York City", 15 | "state":"NY", 16 | "zip":"10108", 17 | "country":"US", 18 | "longitude":"0", 19 | "latitude":"0", 20 | "timezone":"America/New_York_City", 21 | "verified":"1", 22 | "plan":"Basic", 23 | "plan_id":"0", 24 | "plan_active":"1", 25 | "cms_code":"1111", 26 | "cms_active":"0", 27 | "cms_started_at":"", 28 | "cms_expiry":"", 29 | "cms_ondemand":"", 30 | "step":"-1", 31 | "cms_permit_no":"", 32 | "opted_plan_id":"", 33 | "stripe_account":"1", 34 | "plan_effective_from":"", 35 | "agreement":"1", 36 | "associate_users":"1", 37 | "owner":"1" 38 | }''' 39 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = build, py35, py36, py37, py38, lint 3 | skip_missing_interpreters = True 4 | skipsdist = True 5 | 6 | [testenv] 7 | whitelist_externals = 8 | npm 9 | node 10 | nodejs 11 | passenv = * 12 | setenv = 13 | LANG=en_US.UTF-8 14 | PYTHONPATH = {toxinidir}/abodepy 15 | commands = 16 | npm --prefix ./tests/mock_server install ./tests/mock_server 17 | py.test --timeout=30 --cov=abodepy --cov-report term-missing {posargs} -s 18 | deps = 19 | -r{toxinidir}/requirements.txt 20 | -r{toxinidir}/requirements_test.txt 21 | 22 | [testenv:lint] 23 | deps = 24 | -r{toxinidir}/requirements.txt 25 | -r{toxinidir}/requirements_test.txt 26 | basepython = python3 27 | ignore_errors = True 28 | commands = 29 | pylint --rcfile={toxinidir}/pylintrc abodepy tests 30 | flake8 abodepy tests 31 | pydocstyle abodepy tests 32 | rst-lint README.rst 33 | rst-lint CHANGES.rst 34 | 35 | [testenv:build] 36 | recreate = True 37 | skip_install = True 38 | whitelist_externals = 39 | /bin/sh 40 | /bin/rm 41 | deps = 42 | -r{toxinidir}/requirements.txt 43 | commands = 44 | /bin/rm -rf build dist 45 | python setup.py bdist_wheel 46 | /bin/sh -c "pip install --upgrade dist/*.whl" 47 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """AbodePy setup script.""" 3 | from setuptools import setup, find_packages 4 | from abodepy.helpers.constants import (__version__, PROJECT_PACKAGE_NAME, 5 | PROJECT_LICENSE, PROJECT_URL, 6 | PROJECT_EMAIL, PROJECT_DESCRIPTION, 7 | PROJECT_CLASSIFIERS, PROJECT_AUTHOR, 8 | PROJECT_LONG_DESCRIPTION) 9 | 10 | PACKAGES = find_packages(exclude=['tests', 'tests.*']) 11 | 12 | setup( 13 | name=PROJECT_PACKAGE_NAME, 14 | version=__version__, 15 | description=PROJECT_DESCRIPTION, 16 | long_description=PROJECT_LONG_DESCRIPTION, 17 | author=PROJECT_AUTHOR, 18 | author_email=PROJECT_EMAIL, 19 | license=PROJECT_LICENSE, 20 | url=PROJECT_URL, 21 | platforms='any', 22 | py_modules=['abodepy'], 23 | packages=PACKAGES, 24 | include_package_data=True, 25 | install_requires=[ 26 | 'requests>=2.12.4', 27 | 'lomond>=0.3.3', 28 | 'colorlog>=3.0.1', 29 | ], 30 | test_suite='tests', 31 | entry_points={ 32 | 'console_scripts': [ 33 | 'abodepy = abodepy.__main__:main' 34 | ] 35 | }, 36 | classifiers=PROJECT_CLASSIFIERS 37 | ) 38 | -------------------------------------------------------------------------------- /abodepy/devices/cover.py: -------------------------------------------------------------------------------- 1 | """Abode cover device.""" 2 | 3 | from abodepy.devices.switch import AbodeSwitch 4 | import abodepy.helpers.constants as CONST 5 | 6 | 7 | class AbodeCover(AbodeSwitch): 8 | """Class to add cover functionality.""" 9 | 10 | def switch_on(self): 11 | """Turn the switch on.""" 12 | success = self.set_status(CONST.STATUS_OPEN_INT) 13 | 14 | if success: 15 | self._json_state['status'] = CONST.STATUS_OPEN 16 | 17 | return success 18 | 19 | def switch_off(self): 20 | """Turn the switch off.""" 21 | success = self.set_status(CONST.STATUS_CLOSED_INT) 22 | 23 | if success: 24 | self._json_state['status'] = CONST.STATUS_CLOSED 25 | 26 | return success 27 | 28 | def open_cover(self): 29 | """Open the cover.""" 30 | return self.switch_on() 31 | 32 | def close_cover(self): 33 | """Close the cover.""" 34 | return self.switch_off() 35 | 36 | @property 37 | def is_open(self): 38 | """Get if the cover is open.""" 39 | return self.is_on 40 | 41 | @property 42 | def is_on(self): 43 | """ 44 | Get cover state. 45 | 46 | Assume cover is open. 47 | """ 48 | return self.status not in CONST.STATUS_CLOSED 49 | -------------------------------------------------------------------------------- /abodepy/devices/switch.py: -------------------------------------------------------------------------------- 1 | """Abode switch device.""" 2 | 3 | from abodepy.devices import AbodeDevice 4 | import abodepy.helpers.constants as CONST 5 | 6 | 7 | class AbodeSwitch(AbodeDevice): 8 | """Class to add switch functionality.""" 9 | 10 | def switch_on(self): 11 | """Turn the switch on.""" 12 | success = self.set_status(CONST.STATUS_ON_INT) 13 | 14 | if success: 15 | self._json_state['status'] = CONST.STATUS_ON 16 | 17 | return success 18 | 19 | def switch_off(self): 20 | """Turn the switch off.""" 21 | success = self.set_status(CONST.STATUS_OFF_INT) 22 | 23 | if success: 24 | self._json_state['status'] = CONST.STATUS_OFF 25 | 26 | return success 27 | 28 | @property 29 | def is_on(self): 30 | """ 31 | Get switch state. 32 | 33 | Assume switch is on. 34 | """ 35 | return self.status not in (CONST.STATUS_OFF, CONST.STATUS_OFFLINE) 36 | 37 | @property 38 | def is_dimmable(self): 39 | """Device dimmable.""" 40 | return False 41 | 42 | @property 43 | def is_color_capable(self): 44 | """Device is color compatible.""" 45 | # Prevents issues for switches that are specified as lights 46 | # in the Abode component of the Home Assistant config file 47 | return False 48 | 49 | @property 50 | def has_color(self): 51 | """Device is using color mode.""" 52 | # Prevents issues for switches that are specified as lights 53 | # in the Abode component of the Home Assistant config file 54 | return False 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | .pytest_cache/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # IPython Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # dotenv 80 | .env 81 | 82 | # virtualenv 83 | venv/ 84 | ENV/ 85 | 86 | # Spyder project settings 87 | .spyderproject 88 | 89 | # Rope project settings 90 | .ropeproject 91 | /.pypirc 92 | 93 | # LiClipse Project 94 | /.project 95 | /.pydevproject 96 | .settings/ 97 | *.pickle 98 | 99 | # PyCharm project settings 100 | .idea/ 101 | 102 | .vscode/settings.json 103 | .vscode/settings.json 104 | -------------------------------------------------------------------------------- /tests/mock/devices/keypad.py: -------------------------------------------------------------------------------- 1 | """Mock Abode Key Pad Device.""" 2 | import abodepy.helpers.constants as CONST 3 | 4 | DEVICE_ID = 'RF:00000002' 5 | 6 | 7 | def device(devid=DEVICE_ID, status=CONST.STATUS_ONLINE, 8 | low_battery=False, no_response=False): 9 | """Key pad mock device.""" 10 | return ''' 11 | { 12 | "id":"''' + devid + '''", 13 | "type_tag":"device_type.keypad", 14 | "type":"Keypad", 15 | "name":"Keypad", 16 | "area":"1", 17 | "zone":"10", 18 | "sort_order":null, 19 | "is_window":"", 20 | "bypass":"0", 21 | "schar_24hr":"0", 22 | "sresp_mode_0":"5", 23 | "sresp_entry_0":"5", 24 | "sresp_exit_0":"5", 25 | "sresp_mode_1":"5", 26 | "sresp_entry_1":"5", 27 | "sresp_exit_1":"5", 28 | "sresp_mode_2":"5", 29 | "sresp_entry_2":"5", 30 | "sresp_exit_2":"5", 31 | "sresp_mode_3":"5", 32 | "sresp_entry_3":"5", 33 | "sresp_exit_3":"5", 34 | "version":"", 35 | "origin":"abode", 36 | "control_url":"", 37 | "deep_link":null, 38 | "status_color":"#5cb85c", 39 | "faults":{ 40 | "low_battery":''' + str(int(low_battery)) + ''', 41 | "tempered":0, 42 | "supervision":0, 43 | "out_of_order":0, 44 | "no_response":''' + str(int(no_response)) + ''' 45 | }, 46 | "status":"''' + status + '''", 47 | "statuses":{ 48 | "hvac_mode":null 49 | }, 50 | "status_ex":"", 51 | "actions":[ 52 | ], 53 | "status_icons":[ 54 | ], 55 | "icon":"assets/icons/keypad-b.svg" 56 | }''' 57 | -------------------------------------------------------------------------------- /tests/mock/devices/unknown.py: -------------------------------------------------------------------------------- 1 | """Mock Non-Existent Device.""" 2 | import abodepy.helpers.constants as CONST 3 | 4 | DEVICE_ID = 'RF:deadbeef' 5 | 6 | 7 | def device(devid=DEVICE_ID, status=CONST.STATUS_ONLINE, 8 | low_battery=False, no_response=False): 9 | """Remote controller mock device.""" 10 | return ''' 11 | { 12 | "id":"''' + devid + '''", 13 | "type_tag":"device_type.dead_beef", 14 | "type":"Dead Beef Detector", 15 | "name":"Moo", 16 | "area":"1", 17 | "zone":"4", 18 | "sort_order":null, 19 | "is_window":"", 20 | "bypass":"0", 21 | "schar_24hr":"0", 22 | "sresp_mode_0":"0", 23 | "sresp_entry_0":"0", 24 | "sresp_exit_0":"0", 25 | "sresp_mode_1":"5", 26 | "sresp_entry_1":"4", 27 | "sresp_exit_1":"0", 28 | "sresp_mode_2":"0", 29 | "sresp_entry_2":"4", 30 | "sresp_exit_2":"0", 31 | "sresp_mode_3":"0", 32 | "sresp_entry_3":"0", 33 | "sresp_exit_3":"0", 34 | "version":"852_00.00.03.05TC", 35 | "origin":"abode", 36 | "control_url":"", 37 | "deep_link":null, 38 | "status_color":"#5cb85c", 39 | "faults":{ 40 | "low_battery":''' + str(int(low_battery)) + ''', 41 | "tempered":0, 42 | "supervision":0, 43 | "out_of_order":0, 44 | "no_response":''' + str(int(no_response)) + ''' 45 | }, 46 | "status":"''' + status + '''", 47 | "statuses":{ 48 | "hvac_mode":null 49 | }, 50 | "status_ex":"", 51 | "actions":[], 52 | "status_icons":[], 53 | "icon":"assets/icons/cow.svg" 54 | }''' 55 | -------------------------------------------------------------------------------- /tests/mock/devices/siren.py: -------------------------------------------------------------------------------- 1 | """Mock Abode Siren Device.""" 2 | import abodepy.helpers.constants as CONST 3 | 4 | DEVICE_ID = 'ZB:0005674fff' 5 | 6 | 7 | def device(devid=DEVICE_ID, status=CONST.STATUS_ONLINE, 8 | low_battery=False, no_response=False): 9 | """Siren mock device.""" 10 | return ''' 11 | { 12 | "id":"''' + devid + '''", 13 | "type_tag":"device_type.siren", 14 | "type":"Siren", 15 | "name":"Basement Siren", 16 | "area":"1", 17 | "zone":"8", 18 | "sort_order":null, 19 | "is_window":"", 20 | "bypass":"0", 21 | "schar_24hr":"0", 22 | "sresp_mode_0":"0", 23 | "sresp_entry_0":"0", 24 | "sresp_exit_0":"0", 25 | "sresp_mode_1":"5", 26 | "sresp_entry_1":"4", 27 | "sresp_exit_1":"0", 28 | "sresp_mode_2":"0", 29 | "sresp_entry_2":"4", 30 | "sresp_exit_2":"0", 31 | "sresp_mode_3":"0", 32 | "sresp_entry_3":"0", 33 | "sresp_exit_3":"0", 34 | "version":"852_00.00.03.05TC", 35 | "origin":"abode", 36 | "control_url":"", 37 | "deep_link":null, 38 | "status_color":"#5cb85c", 39 | "faults":{ 40 | "low_battery":''' + str(int(low_battery)) + ''', 41 | "tempered":0, 42 | "supervision":0, 43 | "out_of_order":0, 44 | "no_response":''' + str(int(no_response)) + ''' 45 | }, 46 | "status":"''' + status + '''", 47 | "statuses":{ 48 | "hvac_mode":null 49 | }, 50 | "status_ex":"", 51 | "actions":[], 52 | "status_icons":[ 53 | ], 54 | "motion_event":"1", 55 | "wide_angle":"0", 56 | "icon":"assets/icons/indoor-siren.svg" 57 | }''' 58 | -------------------------------------------------------------------------------- /tests/mock/devices/pir.py: -------------------------------------------------------------------------------- 1 | """Mock Abode PIR Device.""" 2 | import abodepy.helpers.constants as CONST 3 | 4 | DEVICE_ID = 'ZB:00000055' 5 | 6 | 7 | def device(devid=DEVICE_ID, status=CONST.STATUS_ONLINE, 8 | low_battery=False, no_response=False): 9 | """PIR mock device.""" 10 | return ''' 11 | { 12 | "id":"''' + devid + '''", 13 | "type_tag":"device_type.pir", 14 | "model":"L1", 15 | "type":"IR", 16 | "name":"Foyer Motion", 17 | "area":"1", 18 | "zone":"13", 19 | "sort_order":null, 20 | "is_window":"", 21 | "bypass":"0", 22 | "schar_24hr":"0", 23 | "sresp_mode_0":"0", 24 | "sresp_entry_0":"0", 25 | "sresp_exit_0":"0", 26 | "sresp_mode_1":"5", 27 | "sresp_entry_1":"4", 28 | "sresp_exit_1":"0", 29 | "sresp_mode_2":"0", 30 | "sresp_entry_2":"4", 31 | "sresp_exit_2":"0", 32 | "sresp_mode_3":"0", 33 | "sresp_entry_3":"0", 34 | "sresp_exit_3":"0", 35 | "version":"852_00.00.03.05TC", 36 | "origin":"abode", 37 | "control_url":"", 38 | "deep_link":null, 39 | "status_color":"#5cb85c", 40 | "faults":{ 41 | "low_battery":''' + str(int(low_battery)) + ''', 42 | "tempered":0, 43 | "supervision":0, 44 | "out_of_order":0, 45 | "no_response":''' + str(int(no_response)) + ''' 46 | }, 47 | "status":"''' + status + '''", 48 | "statuses":{ 49 | "hvac_mode":null 50 | }, 51 | "status_ex":"", 52 | "actions":[], 53 | "status_icons":[ 54 | ], 55 | "motion_event":"1", 56 | "wide_angle":"0", 57 | "icon":"assets/icons/motioncamera-a.svg" 58 | }''' 59 | -------------------------------------------------------------------------------- /tests/mock/devices/status_display.py: -------------------------------------------------------------------------------- 1 | """Mock Abode Status Display Device.""" 2 | import abodepy.helpers.constants as CONST 3 | 4 | DEVICE_ID = 'ZB:00000004' 5 | 6 | 7 | def device(devid=DEVICE_ID, 8 | status=CONST.STATUS_ONLINE, 9 | low_battery=False, no_response=False): 10 | """Status display mock device.""" 11 | return ''' 12 | { 13 | "id":"''' + devid + '''", 14 | "type_tag":"device_type.bx", 15 | "type":"Status Display", 16 | "name":"Status Indicator", 17 | "area":"1", 18 | "zone":"11", 19 | "sort_order":null, 20 | "is_window":"", 21 | "bypass":"0", 22 | "schar_24hr":"0", 23 | "sresp_mode_0":"5", 24 | "sresp_entry_0":"5", 25 | "sresp_exit_0":"5", 26 | "sresp_mode_1":"5", 27 | "sresp_entry_1":"5", 28 | "sresp_exit_1":"5", 29 | "sresp_mode_2":"5", 30 | "sresp_entry_2":"5", 31 | "sresp_exit_2":"5", 32 | "sresp_mode_3":"5", 33 | "sresp_entry_3":"5", 34 | "sresp_exit_3":"5", 35 | "version":"SSL_00.00.03.03TC", 36 | "origin":"abode", 37 | "control_url":"", 38 | "deep_link":null, 39 | "status_color":"#5cb85c", 40 | "faults":{ 41 | "low_battery":''' + str(int(low_battery)) + ''', 42 | "tempered":0, 43 | "supervision":0, 44 | "out_of_order":0, 45 | "no_response":''' + str(int(no_response)) + ''' 46 | }, 47 | "status":"''' + status + '''", 48 | "statuses":{ 49 | "hvac_mode":null 50 | }, 51 | "status_ex":"", 52 | "actions":[ 53 | ], 54 | "status_icons":[ 55 | ], 56 | "siren_default":null, 57 | "icon":"assets/icons/unknown.svg" 58 | }''' 59 | -------------------------------------------------------------------------------- /tests/mock/devices/water_sensor.py: -------------------------------------------------------------------------------- 1 | """Mock Abode Power Switch Sensor Device.""" 2 | import abodepy.helpers.constants as CONST 3 | 4 | DEVICE_ID = 'RF:00000008' 5 | 6 | 7 | def device(devid=DEVICE_ID, 8 | status=CONST.STATUS_OFF, 9 | low_battery=False, no_response=False): 10 | """Water sensor mock device.""" 11 | return ''' 12 | { 13 | "id":"''' + devid + '''", 14 | "type_tag":"device_type.water_sensor", 15 | "type":"Water Sensor", 16 | "name":"Downstairs Bathroo", 17 | "area":"1", 18 | "zone":"26", 19 | "sort_order":"0", 20 | "is_window":"0", 21 | "bypass":"0", 22 | "schar_24hr":"1", 23 | "sresp_mode_0":"0", 24 | "sresp_entry_0":"0", 25 | "sresp_exit_0":"0", 26 | "sresp_mode_1":"0", 27 | "sresp_entry_1":"0", 28 | "sresp_exit_1":"0", 29 | "sresp_mode_2":"0", 30 | "sresp_entry_2":"0", 31 | "sresp_exit_2":"0", 32 | "sresp_mode_3":"0", 33 | "sresp_entry_3":"0", 34 | "sresp_exit_3":"0", 35 | "capture_mode":null, 36 | "origin":"abode", 37 | "control_url":"", 38 | "deep_link":null, 39 | "status_color":"#5cb85c", 40 | "faults":{ 41 | "low_battery":''' + str(int(low_battery)) + ''', 42 | "tempered":0, 43 | "supervision":0, 44 | "out_of_order":0, 45 | "no_response":''' + str(int(no_response)) + ''' 46 | }, 47 | "status":"''' + status + '''", 48 | "statuses":{ 49 | "hvac_mode":null 50 | }, 51 | "status_ex":"", 52 | "actions":[ 53 | ], 54 | "status_icons":[ 55 | ], 56 | "icon":"assets/icons/water-value-shutoff.svg" 57 | }''' 58 | -------------------------------------------------------------------------------- /tests/mock/devices/glass.py: -------------------------------------------------------------------------------- 1 | """Mock Abode Glass Device.""" 2 | import abodepy.helpers.constants as CONST 3 | 4 | DEVICE_ID = 'RF:00000001' 5 | 6 | 7 | def device(devid=DEVICE_ID, status=CONST.STATUS_ONLINE, 8 | low_battery=False, no_response=False, 9 | out_of_order=False, tampered=False, 10 | uuid='91568b0d4c9d58c10d75fdeea887d4f4'): 11 | """Glass break sensor mock device.""" 12 | return ''' 13 | { 14 | "id":"''' + devid + '''", 15 | "type_tag":"device_type.glass", 16 | "type":"GLASS", 17 | "name":"Glass Break Sensor", 18 | "area":"1", 19 | "zone":"6", 20 | "sort_order":null, 21 | "is_window":"", 22 | "bypass":"0", 23 | "schar_24hr":"0", 24 | "sresp_mode_0":"0", 25 | "sresp_entry_0":"0", 26 | "sresp_exit_0":"0", 27 | "sresp_mode_1":"5", 28 | "sresp_entry_1":"5", 29 | "sresp_exit_1":"0", 30 | "sresp_mode_2":"5", 31 | "sresp_entry_2":"5", 32 | "sresp_exit_2":"0", 33 | "sresp_mode_3":"5", 34 | "sresp_entry_3":"5", 35 | "sresp_exit_3":"0", 36 | "version":"", 37 | "origin":"abode", 38 | "control_url":"", 39 | "deep_link":null, 40 | "status_color":"#5cb85c", 41 | "faults":{ 42 | "low_battery":''' + str(int(low_battery)) + ''', 43 | "tempered":''' + str(int(tampered)) + ''', 44 | "supervision":0, 45 | "out_of_order":''' + str(int(out_of_order)) + ''', 46 | "no_response":''' + str(int(no_response)) + ''' 47 | }, 48 | "status":"''' + status + '''", 49 | "statuses":{ 50 | "hvac_mode":null 51 | }, 52 | "status_ex":"", 53 | "actions":[ 54 | ], 55 | "status_icons":[ 56 | ], 57 | "icon":"assets/icons/unknown.svg", 58 | "uuid":"''' + uuid + '''" 59 | }''' 60 | -------------------------------------------------------------------------------- /tests/mock/devices/lm.py: -------------------------------------------------------------------------------- 1 | """Mock Abode LM (Light/Temp/Humidity) Device.""" 2 | DEVICE_ID = 'ZB:eee888801' 3 | 4 | TEMP_F = '72 °F' 5 | TEMP_C = '72 °C' 6 | 7 | LUX = '0 lx' 8 | 9 | HUMIDITY = '42 %' 10 | 11 | 12 | def device(devid=DEVICE_ID, status=TEMP_F, 13 | temp=TEMP_F, lux=LUX, humidity=HUMIDITY, 14 | low_battery=False, no_response=False): 15 | """PIR mock device.""" 16 | return ''' 17 | { 18 | "id":"''' + devid + '''", 19 | "type_tag":"device_type.lm", 20 | "type":"LM", 21 | "name":"Bedroom Temp", 22 | "area":"1", 23 | "zone":"15", 24 | "sort_order":null, 25 | "is_window":"", 26 | "bypass":"0", 27 | "schar_24hr":"0", 28 | "sresp_mode_0":"0", 29 | "sresp_entry_0":"0", 30 | "sresp_exit_0":"0", 31 | "sresp_mode_1":"5", 32 | "sresp_entry_1":"4", 33 | "sresp_exit_1":"0", 34 | "sresp_mode_2":"0", 35 | "sresp_entry_2":"4", 36 | "sresp_exit_2":"0", 37 | "sresp_mode_3":"0", 38 | "sresp_entry_3":"0", 39 | "sresp_exit_3":"0", 40 | "version":"LMHT_00.00.03.03TC", 41 | "origin":"abode", 42 | "deep_link":null, 43 | "status_color":"#5cb85c", 44 | "faults":{ 45 | "low_battery":''' + str(int(low_battery)) + ''', 46 | "tempered":0, 47 | "supervision":0, 48 | "out_of_order":0, 49 | "no_response":''' + str(int(no_response)) + ''' 50 | }, 51 | "status":"''' + status + '''", 52 | "statusEx": "0", 53 | "statuses":{ 54 | "hvac_mode":null, 55 | "temperature": "''' + temp + '''", 56 | "lux": "''' + lux + '''", 57 | "humidity": "''' + humidity + '''" 58 | }, 59 | "status_ex":"", 60 | "actions":[], 61 | "status_icons":[ 62 | ], 63 | "icon":"assets/icons/unknown.svg" 64 | }''' 65 | -------------------------------------------------------------------------------- /tests/mock/devices/door_contact.py: -------------------------------------------------------------------------------- 1 | """Mock Abode Door Contact Device.""" 2 | import abodepy.helpers.constants as CONST 3 | 4 | DEVICE_ID = 'RF:00000003' 5 | 6 | 7 | def device(devid=DEVICE_ID, 8 | status=CONST.STATUS_CLOSED, 9 | low_battery=False, no_response=False, window=False): 10 | """Door contact mock device.""" 11 | return ''' 12 | { 13 | "id":"''' + devid + '''", 14 | "type_tag":"device_type.door_contact", 15 | "type":"Door Contact", 16 | "name":"Back Door", 17 | "area":"1", 18 | "zone":"2", 19 | "sort_order":null, 20 | "is_window":''' + str(int(window)) + ''', 21 | "bypass":"0", 22 | "schar_24hr":"0", 23 | "sresp_mode_0":"3", 24 | "sresp_entry_0":"3", 25 | "sresp_exit_0":"0", 26 | "sresp_mode_1":"1", 27 | "sresp_entry_1":"1", 28 | "sresp_exit_1":"0", 29 | "sresp_mode_2":"1", 30 | "sresp_entry_2":"1", 31 | "sresp_exit_2":"0", 32 | "sresp_mode_3":"1", 33 | "sresp_entry_3":"1", 34 | "sresp_exit_3":"0", 35 | "version":"", 36 | "origin":"abode", 37 | "control_url":"", 38 | "deep_link":null, 39 | "status_color":"#5cb85c", 40 | "faults":{ 41 | "low_battery":''' + str(int(low_battery)) + ''', 42 | "tempered":0, 43 | "supervision":0, 44 | "out_of_order":0, 45 | "no_response":''' + str(int(no_response)) + ''' 46 | }, 47 | "status":"''' + status + '''", 48 | "statuses":{ 49 | "hvac_mode":null 50 | }, 51 | "status_ex":"", 52 | "actions":[ 53 | ], 54 | "status_icons":{ 55 | "Open":"assets/icons/door-open-red.svg", 56 | "Closed":"assets/icons/door-closed-green.svg" 57 | }, 58 | "sresp_trigger":"0", 59 | "sresp_restore":"0", 60 | "icon":"assets/icons/doorsensor-a.svg" 61 | }''' 62 | -------------------------------------------------------------------------------- /tests/mock/devices/remote_controller.py: -------------------------------------------------------------------------------- 1 | """Mock Abode Remote Controller (Keyfob) Device.""" 2 | import abodepy.helpers.constants as CONST 3 | 4 | DEVICE_ID = 'RF:0000416bb' 5 | 6 | 7 | def device(devid=DEVICE_ID, status=CONST.STATUS_ONLINE, 8 | low_battery=False, no_response=False): 9 | """Remote controller mock device.""" 10 | return ''' 11 | { 12 | "id":"''' + devid + '''", 13 | "type_tag":"device_type.remote_controller", 14 | "type":"Remote Controller", 15 | "name":"Remote", 16 | "area":"1", 17 | "zone":"4", 18 | "sort_order":null, 19 | "is_window":"", 20 | "bypass":"0", 21 | "schar_24hr":"0", 22 | "sresp_mode_0":"0", 23 | "sresp_entry_0":"0", 24 | "sresp_exit_0":"0", 25 | "sresp_mode_1":"5", 26 | "sresp_entry_1":"4", 27 | "sresp_exit_1":"0", 28 | "sresp_mode_2":"0", 29 | "sresp_entry_2":"4", 30 | "sresp_exit_2":"0", 31 | "sresp_mode_3":"0", 32 | "sresp_entry_3":"0", 33 | "sresp_exit_3":"0", 34 | "version":"852_00.00.03.05TC", 35 | "origin":"abode", 36 | "control_url":"", 37 | "deep_link":null, 38 | "status_color":"#5cb85c", 39 | "faults":{ 40 | "low_battery":''' + str(int(low_battery)) + ''', 41 | "tempered":0, 42 | "supervision":0, 43 | "out_of_order":0, 44 | "no_response":''' + str(int(no_response)) + ''' 45 | }, 46 | "status":"''' + status + '''", 47 | "statuses":{ 48 | "hvac_mode":null 49 | }, 50 | "status_ex":"", 51 | "actions":[ 52 | { 53 | "label":"Auto Flash", 54 | "value":"a=1&z=3&req=img;" 55 | }, 56 | { 57 | "label":"Never Flash", 58 | "value":"a=1&z=3&req=img_nf;" 59 | } 60 | ], 61 | "status_icons":[ 62 | ], 63 | "motion_event":"1", 64 | "wide_angle":"0", 65 | "icon":"assets/icons/key-fob.svg" 66 | }''' 67 | -------------------------------------------------------------------------------- /tests/mock/panel.py: -------------------------------------------------------------------------------- 1 | """Mock Abode Panel Response.""" 2 | 3 | import abodepy.helpers.constants as CONST 4 | 5 | 6 | def get_response_ok(mode=CONST.MODE_STANDBY, battery=False, is_cellular=False, 7 | mac='00:11:22:33:44:55'): 8 | """Return panel response json.""" 9 | return '''{ 10 | "version":"ABGW 0.0.2.17F ABGW-L1-XA36J 3.1.2.6.1 Z-Wave 3.95", 11 | "report_account":"5555", 12 | "online":"1", 13 | "initialized":"1", 14 | "net_version":"ABGW 0.0.2.17F", 15 | "rf_version":"ABGW-L1-XA36J", 16 | "zigbee_version":"3.1.2.6.1", 17 | "z_wave_version":"Z-Wave 3.95", 18 | "timezone":"America/New_York", 19 | "ac_fail":"0", 20 | "battery":"''' + str(int(battery)) + '''", 21 | "ip":"192.168.1.1", 22 | "jam":"0", 23 | "rssi":"2", 24 | "setup_zone_1":"1", 25 | "setup_zone_2":"1", 26 | "setup_zone_3":"1", 27 | "setup_zone_4":"1", 28 | "setup_zone_5":"1", 29 | "setup_zone_6":"1", 30 | "setup_zone_7":"1", 31 | "setup_zone_8":"1", 32 | "setup_zone_9":"1", 33 | "setup_zone_10":"1", 34 | "setup_gateway":"1", 35 | "setup_contacts":"1", 36 | "setup_billing":"1", 37 | "setup_users":"1", 38 | "is_cellular":"''' + str(int(is_cellular)) + '''", 39 | "plan_set_id":"1", 40 | "dealer_id":"0", 41 | "tz_diff":"-04:00", 42 | "is_demo":"0", 43 | "rf51_version":"ABGW-L1-XA36J", 44 | "model":"L1", 45 | "mac":"''' + mac + '''", 46 | "xml_version":"3", 47 | "dealer_name":"abode", 48 | "id":"0", 49 | "dealer_address":"2625 Middlefield Road #900 Palo Alto CA 94306", 50 | "dealer_domain":"https://my.goabode.com", 51 | "domain_alias":"https://test.goabode.com", 52 | "dealer_support_url":"https://support.goabode.com", 53 | "app_launch_url":"https://goabode.app.link/abode", 54 | "has_wifi":"0", 55 | "mode":{ 56 | "area_1":"''' + mode + '''", 57 | "area_2":"standby" 58 | } 59 | }''' 60 | 61 | 62 | def put_response_ok(area='1', mode=CONST.MODE_STANDBY): 63 | """Return panel mode response json.""" 64 | return '{"area": "' + area + '", "mode": "' + mode + '"}' 65 | -------------------------------------------------------------------------------- /tests/mock_server/index.js: -------------------------------------------------------------------------------- 1 | // For use in tox.ini some day: npm --prefix ./tests/mock_server install ./tests/mock_server 2 | 3 | var app = require('express')(); 4 | var http = require('http').Server(app); 5 | var io = require('socket.io')(http); 6 | var bodyParser = require('body-parser'); 7 | 8 | app.use(bodyParser.text({type:"*/*"})); 9 | 10 | var authError = false; 11 | 12 | app.post("/events/:name", function(req, res, next) { 13 | io.sockets.emit(req.params.name, req.body); 14 | res.send(); 15 | }); 16 | 17 | app.post("/killSockets", function(req, res, next) { 18 | Object.keys(io.sockets.sockets).forEach(function(s) { 19 | io.sockets.sockets[s].disconnect(true); 20 | }); 21 | 22 | res.send(); 23 | }); 24 | 25 | app.post("/authError/:val", function(req, res, next) { 26 | authError = (req.params.val == 'true'); 27 | 28 | res.send({authError}); 29 | }); 30 | 31 | io.on('connection', function(socket){ 32 | console.log('a user connected'); 33 | 34 | socket.on('disconnect', function(){ 35 | console.log('user disconnected'); 36 | }); 37 | }); 38 | 39 | io.use(function(socket, next) { 40 | if (authError === true) { 41 | next(new Error('Not Authorized')); 42 | } 43 | next(); 44 | }); 45 | 46 | io.on('error', function(socket){ 47 | // Do nothing 48 | }); 49 | 50 | var server = http.listen(3000, function(){ 51 | console.log('listening on *:3000'); 52 | }); 53 | 54 | var gracefulShutdown = function() { 55 | console.log("Received kill signal, shutting down gracefully."); 56 | server.close(function() { 57 | console.log("Closed out remaining connections."); 58 | process.exit() 59 | }); 60 | 61 | // if after 62 | setTimeout(function() { 63 | console.error("Could not close connections in time, forcefully shutting down"); 64 | process.exit() 65 | }, 3*1000); 66 | } 67 | 68 | // listen for TERM signal .e.g. kill 69 | process.on ('SIGTERM', gracefulShutdown); 70 | 71 | // listen for INT signal e.g. Ctrl-C 72 | process.on ('SIGINT', gracefulShutdown); 73 | 74 | 75 | app.post("/shutdown", function(req, res, next) { 76 | setTimeout(function() { 77 | gracefulShutdown(); 78 | }, 1*1000); 79 | 80 | res.send(); 81 | }); -------------------------------------------------------------------------------- /tests/mock/devices/door_lock.py: -------------------------------------------------------------------------------- 1 | """Mock Abode Door Lock Device.""" 2 | import abodepy.helpers.constants as CONST 3 | 4 | DEVICE_ID = 'ZW:0000006' 5 | CONTROL_URL = 'api/v1/control/lock/' + DEVICE_ID 6 | 7 | 8 | def device(devid=DEVICE_ID, status=CONST.STATUS_LOCKCLOSED, 9 | low_battery=False, no_response=False): 10 | """Door lock mock device.""" 11 | return ''' 12 | { 13 | "id":"''' + devid + '''", 14 | "type_tag":"device_type.door_lock", 15 | "type":"Door Lock", 16 | "name":"Back Door Deadbolt", 17 | "area":"1", 18 | "zone":"7", 19 | "sort_order":"0", 20 | "is_window":"0", 21 | "bypass":"0", 22 | "schar_24hr":"0", 23 | "sresp_mode_0":"0", 24 | "sresp_entry_0":"0", 25 | "sresp_exit_0":"0", 26 | "sresp_mode_1":"0", 27 | "sresp_entry_1":"0", 28 | "sresp_exit_1":"0", 29 | "sresp_mode_2":"0", 30 | "sresp_entry_2":"0", 31 | "sresp_exit_2":"0", 32 | "sresp_mode_3":"0", 33 | "sresp_entry_3":"0", 34 | "sresp_exit_3":"0", 35 | "version":"", 36 | "origin":"abode", 37 | "control_url":"''' + CONTROL_URL + '''", 38 | "deep_link":null, 39 | "status_color":"#5cb85c", 40 | "faults":{ 41 | "low_battery":''' + str(int(low_battery)) + ''', 42 | "tempered":0, 43 | "supervision":0, 44 | "out_of_order":0, 45 | "no_response":''' + str(int(no_response)) + ''' 46 | }, 47 | "status":"''' + status + '''", 48 | "statuses":{ 49 | "hvac_mode":null 50 | }, 51 | "status_ex":"", 52 | "actions":[ 53 | { 54 | "label":"Lock", 55 | "value":"a=1&z=7&sw=on;" 56 | }, 57 | { 58 | "label":"Unlock", 59 | "value":"a=1&z=7&sw=off;" 60 | } 61 | ], 62 | "status_icons":{ 63 | "LockOpen":"assets/icons/unlocked-red.svg", 64 | "LockClosed":"assets/icons/locked-green.svg" 65 | }, 66 | "automation_settings":null, 67 | "icon":"assets/icons/automation-lock.svg" 68 | }''' 69 | -------------------------------------------------------------------------------- /tests/mock/devices/secure_barrier.py: -------------------------------------------------------------------------------- 1 | """Mock Abode Power Switch Sensor Device.""" 2 | import abodepy.helpers.constants as CONST 3 | 4 | DEVICE_ID = 'ZW:0000000a' 5 | CONTROL_URL = 'api/v1/control/power_switch/' + DEVICE_ID 6 | 7 | 8 | def device(devid=DEVICE_ID, 9 | status=CONST.STATUS_OPEN, 10 | low_battery=False, no_response=False): 11 | """Secure barrier mock device.""" 12 | return ''' 13 | { 14 | "id":"''' + devid + '''", 15 | "type_tag":"device_type.secure_barrier", 16 | "type":"Secure Barrier", 17 | "name":"Garage Auto Door", 18 | "area":"1", 19 | "zone":"11", 20 | "sort_order":"0", 21 | "is_window":"0", 22 | "bypass":"0", 23 | "schar_24hr":"0", 24 | "sresp_mode_0":"0", 25 | "sresp_entry_0":"0", 26 | "sresp_exit_0":"0", 27 | "sresp_mode_1":"0", 28 | "sresp_entry_1":"0", 29 | "sresp_exit_1":"0", 30 | "sresp_mode_2":"0", 31 | "sresp_entry_2":"0", 32 | "sresp_exit_2":"0", 33 | "sresp_mode_3":"0", 34 | "sresp_entry_3":"0", 35 | "sresp_exit_3":"0", 36 | "capture_mode":null, 37 | "origin":"abode", 38 | "control_url":"''' + CONTROL_URL + '''", 39 | "deep_link":null, 40 | "status_color":"#5cb85c", 41 | "faults":{ 42 | "low_battery":''' + str(int(low_battery)) + ''', 43 | "tempered":0, 44 | "supervision":0, 45 | "out_of_order":0, 46 | "no_response":''' + str(int(no_response)) + ''' 47 | }, 48 | "status":"''' + status + '''", 49 | "statuses":{ 50 | "hvac_mode":null 51 | }, 52 | "status_ex":"", 53 | "actions":[ 54 | { 55 | "label":"Close", 56 | "value":"a=1&z=11&sw=off;" 57 | }, 58 | { 59 | "label":"Open", 60 | "value":"a=1&z=11&sw=on;" 61 | } 62 | ], 63 | "status_icons":{ 64 | "Open":"assets/icons/garage-door-red.svg", 65 | "Closed":"assets/icons/garage-door-green.svg" 66 | }, 67 | "icon":"assets/icons/garage-door.svg" 68 | }''' 69 | -------------------------------------------------------------------------------- /abodepy/devices/sensor.py: -------------------------------------------------------------------------------- 1 | """Abode sensor device.""" 2 | import re 3 | 4 | from abodepy.devices.binary_sensor import AbodeBinarySensor 5 | import abodepy.helpers.constants as CONST 6 | 7 | 8 | class AbodeSensor(AbodeBinarySensor): 9 | """Class to represent a sensor device.""" 10 | 11 | def _get_status(self, key): 12 | return self._json_state.get(CONST.STATUSES_KEY, {}).get(key) 13 | 14 | def _get_numeric_status(self, key): 15 | """Extract the numeric value from the statuses object.""" 16 | value = self._get_status(key) 17 | 18 | if value and any(i.isdigit() for i in value): 19 | return float(re.sub("[^0-9.]", "", value)) 20 | return None 21 | 22 | @property 23 | def temp(self): 24 | """Get device temp.""" 25 | return self._get_numeric_status(CONST.TEMP_STATUS_KEY) 26 | 27 | @property 28 | def temp_unit(self): 29 | """Get unit of temp.""" 30 | if CONST.UNIT_FAHRENHEIT in self._get_status(CONST.TEMP_STATUS_KEY): 31 | return CONST.UNIT_FAHRENHEIT 32 | 33 | if CONST.UNIT_CELSIUS in self._get_status(CONST.TEMP_STATUS_KEY): 34 | return CONST.UNIT_CELSIUS 35 | 36 | return None 37 | 38 | @property 39 | def humidity(self): 40 | """Get device humdity.""" 41 | return self._get_numeric_status(CONST.HUMI_STATUS_KEY) 42 | 43 | @property 44 | def humidity_unit(self): 45 | """Get unit of humidity.""" 46 | if CONST.UNIT_PERCENT in self._get_status(CONST.HUMI_STATUS_KEY): 47 | return CONST.UNIT_PERCENT 48 | return None 49 | 50 | @property 51 | def lux(self): 52 | """Get device lux.""" 53 | return self._get_numeric_status(CONST.LUX_STATUS_KEY) 54 | 55 | @property 56 | def lux_unit(self): 57 | """Get unit of lux.""" 58 | if CONST.UNIT_LUX in self._get_status(CONST.LUX_STATUS_KEY): 59 | return CONST.LUX 60 | return None 61 | 62 | @property 63 | def has_temp(self): 64 | """Device reports temperature.""" 65 | return self.temp is not None 66 | 67 | @property 68 | def has_humidity(self): 69 | """Device reports humidity level.""" 70 | return self.humidity is not None 71 | 72 | @property 73 | def has_lux(self): 74 | """Device reports light lux level.""" 75 | return self.lux is not None 76 | -------------------------------------------------------------------------------- /tests/mock/login.py: -------------------------------------------------------------------------------- 1 | """Mock Abode Login Response.""" 2 | 3 | from tests.mock import AUTH_TOKEN 4 | import tests.mock.panel as panel 5 | import tests.mock.user as user 6 | 7 | 8 | def post_response_ok(auth_token=AUTH_TOKEN, 9 | user_response=user.get_response_ok()): 10 | """Return the successful login response json.""" 11 | return ''' 12 | { 13 | "token":"''' + auth_token + '''", 14 | "expired_at":"2017-06-05 00:14:12", 15 | "initiate_screen":"timeline", 16 | "user":''' + user_response + ''', 17 | "panel":''' + panel.get_response_ok() + ''', 18 | "permissions":{ 19 | "premium_streaming":"0", 20 | "guest_app":"0", 21 | "family_app":"0", 22 | "multiple_accounts":"1", 23 | "google_voice":"1", 24 | "nest":"1", 25 | "alexa":"1", 26 | "ifttt":"1", 27 | "no_associates":"100", 28 | "no_contacts":"2", 29 | "no_devices":"155", 30 | "no_ipcam":"100", 31 | "no_quick_action":"25", 32 | "no_automation":"75", 33 | "media_storage":"3", 34 | "cellular_backup":"0", 35 | "cms_duration":"", 36 | "cms_included":"0" 37 | }, 38 | "integrations":{ 39 | "nest":{ 40 | "is_connected":0, 41 | "is_home_selected":0 42 | } 43 | } 44 | }''' 45 | 46 | 47 | def post_response_bad_request(): 48 | """Return the failed login response json.""" 49 | return ''' 50 | { 51 | "code":400,"message":"Username and password do not match.", 52 | "detail":null 53 | }''' 54 | 55 | 56 | def post_response_mfa_code_required(): 57 | """Return the MFA code required login response json.""" 58 | return ''' 59 | { 60 | "code":200,"mfa_type":"google_authenticator", 61 | "detail":null 62 | }''' 63 | 64 | 65 | def post_response_bad_mfa_code(): 66 | """Return the bad MFA code login response json.""" 67 | return ''' 68 | { 69 | "code":400,"message":"Invalid authentication key.", 70 | "detail":null 71 | }''' 72 | 73 | 74 | def post_response_unknown_mfa_type(): 75 | """Return a login response json with an unknown mfa type.""" 76 | return ''' 77 | { 78 | "code":200,"mfa_type":"sms", 79 | "detail":null 80 | }''' 81 | -------------------------------------------------------------------------------- /tests/mock/devices/valve.py: -------------------------------------------------------------------------------- 1 | """Mock Abode Power Switch Sensor Device.""" 2 | import abodepy.helpers.constants as CONST 3 | 4 | DEVICE_ID = 'ZW:00000113' 5 | CONTROL_URL = 'api/v1/control/power_switch/' + DEVICE_ID 6 | 7 | 8 | def device(devid=DEVICE_ID, status=CONST.STATUS_OPEN, 9 | low_battery=False, no_response=False): 10 | """Valve mock device.""" 11 | return ''' 12 | { 13 | "actions":[ 14 | { 15 | "label":"Close", 16 | "value":"a=1&z=18&sw=off;" 17 | }, 18 | { 19 | "label":"Open", 20 | "value":"a=1&z=18&sw=on;" 21 | } 22 | ], 23 | "area":"1", 24 | "bypass":"0", 25 | "control_url":"''' + CONTROL_URL + '''", 26 | "deep_link":null, 27 | "default_group_id":"1", 28 | "faults":{ 29 | "low_battery":''' + str(int(low_battery)) + ''', 30 | "tempered":0, 31 | "supervision":0, 32 | "out_of_order":0, 33 | "no_response":''' + str(int(no_response)) + ''' 34 | }, 35 | "generic_type":"valve", 36 | "group_id":"xxxxx", 37 | "group_name":"Water leak", 38 | "has_subscription":null, 39 | "icon":"assets/icons/water-value-shutoff.svg", 40 | "id":"''' + devid + '''", 41 | "is_window":"", 42 | "name":"Water shut-off valve", 43 | "onboard":"0", 44 | "origin":"abode", 45 | "s2_dsk":"", 46 | "s2_grnt_keys":"", 47 | "s2_keys_valid":"", 48 | "s2_propty":"", 49 | "schar_24hr":"0", 50 | "sort_id":"6", 51 | "sort_order":"", 52 | "sresp_24hr":"0", 53 | "sresp_entry_0":"0", 54 | "sresp_entry_1":"0", 55 | "sresp_entry_2":"0", 56 | "sresp_entry_3":"0", 57 | "sresp_entry_4":"0", 58 | "sresp_exit_0":"0", 59 | "sresp_exit_1":"0", 60 | "sresp_exit_2":"0", 61 | "sresp_exit_3":"0", 62 | "sresp_exit_4":"0", 63 | "sresp_mode_0":"0", 64 | "sresp_mode_1":"0", 65 | "sresp_mode_2":"0", 66 | "sresp_mode_3":"0", 67 | "sresp_mode_4":"0", 68 | "status":"''' + status + '''", 69 | "status_color":"#5cb85c", 70 | "status_display":"Open", 71 | "status_ex":"", 72 | "status_icons":[ 73 | 74 | ], 75 | "statuses":{ 76 | "switch":"1" 77 | }, 78 | "type":"Shutoff Valve", 79 | "type_tag":"device_type.valve", 80 | "uuid":"xxxxxxxxxxxxxxxxxxxxxxxxx", 81 | "version":"021f00030002", 82 | "zone":"18", 83 | "zwave_secure_protocol":"" 84 | }''' 85 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ----------- 3 | 4 | A list of changes between each release. 5 | 6 | 0.10.0 (2017-08-29) 7 | ^^^^^^^^^^^^^^^^^^^ 8 | - Full test coverage outside of event service. 9 | - Added support for automations and quick actions #7 and #8 10 | - Added PIR Device Offline/Online #13 11 | - Added Remote Controller Offline/Online #14 12 | - Added Siren Device #15 13 | - Added --json argument #11 14 | - Added support for timeline event via event service #12 15 | 16 | 0.9.0 (2017-08-25) 17 | ^^^^^^^^^^^^^^^^^^ 18 | - Refactored file structure. 19 | - Added power switch meter #9 20 | - More bug fixes. 21 | 22 | 0.8.2 (2017-08-22) 23 | ^^^^^^^^^^^^^^^^^^ 24 | - Fixed various bugs. 25 | 26 | 0.8.0 (2017-08-22) 27 | ^^^^^^^^^^^^^^^^^^ 28 | - Refactored almost the entire package layout 29 | - Command line is now called with the 'abodepy' command directly 30 | - Cleaned up command line help 31 | - Changed --setting to --set, --switchOn to --on, and --switchOff to --off 32 | - Modified get_devices() to update existing devices instead of creating new ones every time 33 | - Added additional property methods 34 | - Added support for more device types 35 | 36 | 0.7.2 (2017-08-21) 37 | ^^^^^^^^^^^^^^^^^^ 38 | - Small bug fix release regarding event callbacks for HASS testing 39 | 40 | 0.7.1 (2017-08-21) 41 | ^^^^^^^^^^^^^^^^^^ 42 | - Added siren settings for Issue #1 43 | 44 | 0.7.0 (2017-08-21) 45 | ^^^^^^^^^^^^^^^^^^ 46 | - Wrote tests for setting changes 47 | - Upgraded to correct version of socketio_client3, fixing Issue #4 48 | - Added switchOn, switchOff, lock, and unlock command line arguments 49 | - Now including abodecl with package for Issue #2 50 | - Modified abodepy to also execute abodecl as part of its main method. 51 | 52 | 0.6.0 (2017-08-20) 53 | ^^^^^^^^^^^^^^^^^^ 54 | - Added settings changes for Issue #1 55 | - Merge pull request #4 from amelchio/origin-slash 56 | 57 | 0.5.1 (2017-06-11) 58 | ^^^^^^^^^^^^^^^^^^ 59 | - Now referencing forked socketIO_client3. 60 | 61 | 0.5.0 (2017-06-11) 62 | ^^^^^^^^^^^^^^^^^^ 63 | - Fixed socketio errors. 64 | Apparently Abode updated to Socket IO 2.0 which was broken with socketio_client 0.7.2. Someone luckily fixed it in their own git repo so I am now referencing their git repo in which they released 0.7.3. 65 | 66 | 0.4.0 (2017-06-05) 67 | ^^^^^^^^^^^^^^^^^^ 68 | - Rewrote a lot of the SocketIO code to better handle connection errors. 69 | - Now manages to recover from connection errors cleanly and shutdown is smoother. 70 | - Added additional logging statements throughout. 71 | - Added --verbose command line argument to output info-level abodepy logs. 72 | 73 | 0.3.0 (2017-06-03) 74 | ^^^^^^^^^^^^^^^^^^ 75 | - More tests 76 | - Cleaner code (thanks pylint) 77 | - Likely ready to start implementing in HASS 78 | 79 | 0.2.0 (2017-06-01) 80 | ^^^^^^^^^^^^^^^^^^ 81 | - Added tests for the alarm device 82 | - Added mock responses for several other devices for tests that will be written 83 | - Reworked a significant chunk of the code to be more python-y. 84 | - Fixed a hand-full of bugs found while writing tests. 85 | 86 | 0.1.0 (2017-05-27) 87 | ^^^^^^^^^^^^^^^^^^ 88 | - Initial release of abodepy -------------------------------------------------------------------------------- /abodepy/helpers/errors.py: -------------------------------------------------------------------------------- 1 | """Errors for AbodePy.""" 2 | USERNAME = (0, "Username must be a non-empty string") 3 | 4 | PASSWORD = (1, "Password must be a non-empty string") 5 | 6 | REQUEST = (2, "Request failed") 7 | 8 | SET_STATUS_DEV_ID = ( 9 | 3, "Device status/level response ID does not match request ID") 10 | 11 | SET_STATUS_STATE = ( 12 | 4, "Device status/level value does not match request value") 13 | 14 | REFRESH = (5, "Failed to refresh device") 15 | 16 | SET_MODE = (6, "Failed to set alarm mode") 17 | 18 | SET_MODE_AREA = (7, "Set mode response area does not match request area") 19 | 20 | SET_MODE_MODE = (8, "Set mode response mode does not match request mode") 21 | 22 | INVALID_ALARM_MODE = (9, "Mode is not of a known alarm mode value") 23 | 24 | MISSING_ALARM_MODE = (10, "No alarm mode found in object") 25 | 26 | INVALID_DEFAULT_ALARM_MODE = ( 27 | 11, "Default alarm mode must be one of 'home' or 'away'") 28 | 29 | INVALID_DEVICE_ID = (12, "The given value is not a device or valid device ID") 30 | 31 | INVALID_SETTING = ( 32 | 13, "Setting is not valid") 33 | 34 | INVALID_SETTING_VALUE = ( 35 | 14, "Value for setting is not valid") 36 | 37 | INVALID_AUTOMATION_REFRESH_RESPONSE = ( 38 | 15, "Automation refresh response did not match expected values.") 39 | 40 | INVALID_AUTOMATION_EDIT_RESPONSE = ( 41 | 16, "Automation edit response did not match expected values.") 42 | 43 | # DEPRECATED 44 | # TRIGGER_NON_QUICKACTION = ( 45 | # 17, "Can not trigger an automation that is not a manual quick-action.") 46 | 47 | UNABLE_TO_MAP_DEVICE = ( 48 | 18, "Unable to map device json to device class - no type tag found.") 49 | 50 | EVENT_CODE_MISSING = ( 51 | 19, "Event is not valid, start and end event codes are missing.") 52 | 53 | EVENT_CODE_MISSING = ( 54 | 20, "Timeline event is not valid, event code missing.") 55 | 56 | INVALID_TIMELINE_EVENT = ( 57 | 21, "Timeline event received missing an event code or type.") 58 | 59 | EVENT_GROUP_INVALID = ( 60 | 22, "Timeline event group is not valid.") 61 | 62 | CAM_IMAGE_REFRESH_NO_FILE = ( 63 | 23, "Camera image refresh did not have a file path.") 64 | 65 | CAM_IMAGE_UNEXPECTED_RESPONSE = ( 66 | 24, "Unknown camera image response.") 67 | 68 | CAM_IMAGE_NO_LOCATION_HEADER = ( 69 | 25, "Camera file path did not redirect to image location.") 70 | 71 | CAM_TIMELINE_EVENT_INVALID = ( 72 | 26, "Timeline event_code invalid - expected 5001.") 73 | 74 | CAM_IMAGE_REQUEST_INVALID = ( 75 | 27, "Received an invalid response from AWS servers for image.") 76 | 77 | EVENT_DEVICE_INVALID = ( 78 | 28, "Object given to event registration service is not a device object") 79 | 80 | SOCKETIO_ERROR = ( 81 | 29, "SocketIO Error Packet Received") 82 | 83 | MISSING_CONTROL_URL = ( 84 | 30, "Control URL does not exist in device JSON.") 85 | 86 | SET_PRIVACY_MODE = ( 87 | 31, "Device privacy mode value does not match request value.") 88 | 89 | MFA_CODE_REQUIRED = ( 90 | 32, "Multifactor authentication code required for login.") 91 | 92 | UNKNOWN_MFA_TYPE = ( 93 | 33, "Unknown multifactor authentication type.") 94 | -------------------------------------------------------------------------------- /abodepy/automation.py: -------------------------------------------------------------------------------- 1 | """Representation of an automation configured in Abode.""" 2 | import json 3 | import logging 4 | 5 | from abodepy.exceptions import AbodeException 6 | 7 | import abodepy.helpers.constants as CONST 8 | import abodepy.helpers.errors as ERROR 9 | 10 | _LOGGER = logging.getLogger(__name__) 11 | 12 | 13 | class AbodeAutomation: 14 | """Class for viewing and controlling automations.""" 15 | 16 | def __init__(self, abode, automation): 17 | """Init AbodeAutomation class.""" 18 | self._abode = abode 19 | self._automation = automation 20 | 21 | def enable(self, enable): 22 | """Enable or disable the automation.""" 23 | url = str.replace(CONST.AUTOMATION_ID_URL, '$AUTOMATIONID$', 24 | self.automation_id) 25 | 26 | self._automation['enabled'] = enable 27 | 28 | response = self._abode.send_request( 29 | method="patch", url=url, data={'enabled': enable}) 30 | 31 | response_object = json.loads(response.text) 32 | 33 | if isinstance(response_object, (tuple, list)): 34 | response_object = response_object[0] 35 | 36 | if (str(response_object['id']) != str(self._automation['id']) or 37 | str(response_object['enabled']) != 38 | str(self._automation['enabled'])): 39 | raise AbodeException((ERROR.INVALID_AUTOMATION_EDIT_RESPONSE)) 40 | 41 | self.update(response_object) 42 | 43 | _LOGGER.info("Set automation %s enable to: %s", self.name, 44 | self.is_enabled) 45 | _LOGGER.debug("Automation response: %s", response.text) 46 | 47 | return True 48 | 49 | def trigger(self): 50 | """Trigger the automation.""" 51 | url = str.replace(CONST.AUTOMATION_APPLY_URL, '$AUTOMATIONID$', 52 | self.automation_id) 53 | 54 | self._abode.send_request(method="post", url=url) 55 | 56 | _LOGGER.info("Automation triggered: %s", self.name) 57 | 58 | return True 59 | 60 | def refresh(self): 61 | """Refresh the automation.""" 62 | url = str.replace(CONST.AUTOMATION_ID_URL, '$AUTOMATIONID$', 63 | self.automation_id) 64 | 65 | response = self._abode.send_request(method="get", url=url) 66 | response_object = json.loads(response.text) 67 | 68 | if isinstance(response_object, (tuple, list)): 69 | response_object = response_object[0] 70 | 71 | if str(response_object['id']) != self.automation_id: 72 | raise AbodeException((ERROR.INVALID_AUTOMATION_REFRESH_RESPONSE)) 73 | 74 | self.update(response_object) 75 | 76 | def update(self, automation): 77 | """Update the internal automation json.""" 78 | self._automation.update( 79 | {k: automation[k] for k in automation if self._automation.get(k)}) 80 | 81 | @property 82 | def automation_id(self): 83 | """Get the id of the automation.""" 84 | return str(self._automation['id']) 85 | 86 | @property 87 | def name(self): 88 | """Get the name of the automation.""" 89 | return self._automation['name'] 90 | 91 | @property 92 | def is_enabled(self): 93 | """Return True if the automation is enabled.""" 94 | return self._automation['enabled'] 95 | 96 | @property 97 | def desc(self): 98 | """Get a short description of the automation.""" 99 | return '{0} (ID: {1}, Enabled: {2})'.format( 100 | self.name, self.automation_id, self.is_enabled) 101 | -------------------------------------------------------------------------------- /tests/mock/devices/power_switch_meter.py: -------------------------------------------------------------------------------- 1 | """Mock Abode Power Switch Meter Device.""" 2 | import abodepy.helpers.constants as CONST 3 | 4 | DEVICE_ID = 'ZW:00000077' 5 | CONTROL_URL = 'api/v1/control/power_switch/' + DEVICE_ID 6 | 7 | 8 | def device(devid=DEVICE_ID, status=CONST.STATUS_OFF, 9 | low_battery=False, no_response=False): 10 | """Power switch mock device.""" 11 | return ''' 12 | { 13 | "id":"''' + devid + '''", 14 | "type_tag":"device_type.power_switch_meter", 15 | "type":"Power Switch Meter", 16 | "name":"Aeon Switch", 17 | "area":"1", 18 | "zone":"32", 19 | "sort_order":null, 20 | "is_window":"", 21 | "bypass":"0", 22 | "schar_24hr":"0", 23 | "sresp_mode_0":"0", 24 | "sresp_entry_0":"0", 25 | "sresp_exit_0":"0", 26 | "sresp_mode_1":"0", 27 | "sresp_entry_1":"0", 28 | "sresp_exit_1":"0", 29 | "sresp_mode_2":"0", 30 | "sresp_entry_2":"0", 31 | "sresp_exit_2":"0", 32 | "sresp_mode_3":"0", 33 | "sresp_entry_3":"0", 34 | "sresp_exit_3":"0", 35 | "capture_mode":null, 36 | "origin":"abode", 37 | "control_url":"''' + CONTROL_URL + '''", 38 | "deep_link":null, 39 | "status_color":"#5cb85c", 40 | "faults":{ 41 | "low_battery":''' + str(int(low_battery)) + ''', 42 | "tempered":0, 43 | "supervision":0, 44 | "out_of_order":0, 45 | "no_response":''' + str(int(no_response)) + ''' 46 | }, 47 | "status":"''' + status + '''", 48 | "statuses":{ 49 | "hvac_mode":null 50 | }, 51 | "status_ex":"", 52 | "actions":[ 53 | { 54 | "label":"Switch off", 55 | "value":"a=1&z=32&sw=off;" 56 | }, 57 | { 58 | "label":"Switch on", 59 | "value":"a=1&z=32&sw=on;" 60 | }, 61 | { 62 | "label":"Toggle", 63 | "value":"a=1&z=32&sw=toggle;" 64 | }, 65 | { 66 | "label":"Switch on for 5 min", 67 | "value":"a=1&z=32&sw=on&pd=5;" 68 | }, 69 | { 70 | "label":"Switch on for 10 min", 71 | "value":"a=1&z=32&sw=on&pd=10;" 72 | }, 73 | { 74 | "label":"Switch on for 15 min", 75 | "value":"a=1&z=32&sw=on&pd=15;" 76 | }, 77 | { 78 | "label":"Switch on for 20 min", 79 | "value":"a=1&z=32&sw=on&pd=20;" 80 | }, 81 | { 82 | "label":"Switch on for 25 min", 83 | "value":"a=1&z=32&sw=on&pd=25;" 84 | }, 85 | { 86 | "label":"Switch on for 30 min", 87 | "value":"a=1&z=32&sw=on&pd=30;" 88 | }, 89 | { 90 | "label":"Switch on for 45 min", 91 | "value":"a=1&z=32&sw=on&pd=45;" 92 | }, 93 | { 94 | "label":"Switch on for 1 hour", 95 | "value":"a=1&z=32&sw=on&pd=60;" 96 | }, 97 | { 98 | "label":"Switch on for 2 hours", 99 | "value":"a=1&z=32&sw=on&pd=120;" 100 | }, 101 | { 102 | "label":"Switch on for 5 hours", 103 | "value":"a=1&z=32&sw=on&pd=300;" 104 | }, 105 | { 106 | "label":"Switch on for 8 hours", 107 | "value":"a=1&z=32&sw=on&pd=480;" 108 | } 109 | ], 110 | "status_icons":[ 111 | ], 112 | "icon":"assets/icons/plug.svg" 113 | }''' 114 | -------------------------------------------------------------------------------- /tests/mock/devices/power_switch_sensor.py: -------------------------------------------------------------------------------- 1 | """Mock Abode Power Switch Sensor Device.""" 2 | import abodepy.helpers.constants as CONST 3 | 4 | DEVICE_ID = 'ZW:00000007' 5 | CONTROL_URL = 'api/v1/control/power_switch/' + DEVICE_ID 6 | 7 | 8 | def device(devid=DEVICE_ID, status=CONST.STATUS_OFF, 9 | low_battery=False, no_response=False): 10 | """Power switch mock device.""" 11 | return ''' 12 | { 13 | "id":"''' + devid + '''", 14 | "type_tag":"device_type.power_switch_sensor", 15 | "type":"Power Switch Sensor", 16 | "name":"Back Porch Light", 17 | "area":"1", 18 | "zone":"32", 19 | "sort_order":null, 20 | "is_window":"", 21 | "bypass":"0", 22 | "schar_24hr":"0", 23 | "sresp_mode_0":"0", 24 | "sresp_entry_0":"0", 25 | "sresp_exit_0":"0", 26 | "sresp_mode_1":"0", 27 | "sresp_entry_1":"0", 28 | "sresp_exit_1":"0", 29 | "sresp_mode_2":"0", 30 | "sresp_entry_2":"0", 31 | "sresp_exit_2":"0", 32 | "sresp_mode_3":"0", 33 | "sresp_entry_3":"0", 34 | "sresp_exit_3":"0", 35 | "capture_mode":null, 36 | "origin":"abode", 37 | "control_url":"''' + CONTROL_URL + '''", 38 | "deep_link":null, 39 | "status_color":"#5cb85c", 40 | "faults":{ 41 | "low_battery":''' + str(int(low_battery)) + ''', 42 | "tempered":0, 43 | "supervision":0, 44 | "out_of_order":0, 45 | "no_response":''' + str(int(no_response)) + ''' 46 | }, 47 | "status":"''' + status + '''", 48 | "statuses":{ 49 | "hvac_mode":null 50 | }, 51 | "status_ex":"", 52 | "actions":[ 53 | { 54 | "label":"Switch off", 55 | "value":"a=1&z=32&sw=off;" 56 | }, 57 | { 58 | "label":"Switch on", 59 | "value":"a=1&z=32&sw=on;" 60 | }, 61 | { 62 | "label":"Toggle", 63 | "value":"a=1&z=32&sw=toggle;" 64 | }, 65 | { 66 | "label":"Switch on for 5 min", 67 | "value":"a=1&z=32&sw=on&pd=5;" 68 | }, 69 | { 70 | "label":"Switch on for 10 min", 71 | "value":"a=1&z=32&sw=on&pd=10;" 72 | }, 73 | { 74 | "label":"Switch on for 15 min", 75 | "value":"a=1&z=32&sw=on&pd=15;" 76 | }, 77 | { 78 | "label":"Switch on for 20 min", 79 | "value":"a=1&z=32&sw=on&pd=20;" 80 | }, 81 | { 82 | "label":"Switch on for 25 min", 83 | "value":"a=1&z=32&sw=on&pd=25;" 84 | }, 85 | { 86 | "label":"Switch on for 30 min", 87 | "value":"a=1&z=32&sw=on&pd=30;" 88 | }, 89 | { 90 | "label":"Switch on for 45 min", 91 | "value":"a=1&z=32&sw=on&pd=45;" 92 | }, 93 | { 94 | "label":"Switch on for 1 hour", 95 | "value":"a=1&z=32&sw=on&pd=60;" 96 | }, 97 | { 98 | "label":"Switch on for 2 hours", 99 | "value":"a=1&z=32&sw=on&pd=120;" 100 | }, 101 | { 102 | "label":"Switch on for 5 hours", 103 | "value":"a=1&z=32&sw=on&pd=300;" 104 | }, 105 | { 106 | "label":"Switch on for 8 hours", 107 | "value":"a=1&z=32&sw=on&pd=480;" 108 | } 109 | ], 110 | "status_icons":[ 111 | ], 112 | "icon":"assets/icons/plug.svg" 113 | }''' 114 | -------------------------------------------------------------------------------- /tests/mock/devices/ir_camera.py: -------------------------------------------------------------------------------- 1 | """Mock Abode IR Camera Device.""" 2 | import abodepy.helpers.constants as CONST 3 | 4 | DEVICE_ID = 'ZB:00000005' 5 | CONTROL_URL = 'api/v1/cams/' + DEVICE_ID + '/capture' 6 | 7 | 8 | def device(devid=DEVICE_ID, status=CONST.STATUS_ONLINE, 9 | low_battery=False, no_response=False): 10 | """IR camera mock device.""" 11 | return ''' 12 | { 13 | "id":"''' + devid + '''", 14 | "type_tag":"device_type.ir_camera", 15 | "type":"Motion Camera", 16 | "name":"Downstairs Motion Camera", 17 | "area":"1", 18 | "zone":"3", 19 | "sort_order":null, 20 | "is_window":"", 21 | "bypass":"0", 22 | "schar_24hr":"0", 23 | "sresp_mode_0":"0", 24 | "sresp_entry_0":"0", 25 | "sresp_exit_0":"0", 26 | "sresp_mode_1":"5", 27 | "sresp_entry_1":"4", 28 | "sresp_exit_1":"0", 29 | "sresp_mode_2":"0", 30 | "sresp_entry_2":"4", 31 | "sresp_exit_2":"0", 32 | "sresp_mode_3":"0", 33 | "sresp_entry_3":"0", 34 | "sresp_exit_3":"0", 35 | "version":"852_00.00.03.05TC", 36 | "origin":"abode", 37 | "control_url":"''' + CONTROL_URL + '''", 38 | "deep_link":null, 39 | "status_color":"#5cb85c", 40 | "faults":{ 41 | "low_battery":''' + str(int(low_battery)) + ''', 42 | "tempered":0, 43 | "supervision":0, 44 | "out_of_order":0, 45 | "no_response":''' + str(int(no_response)) + ''' 46 | }, 47 | "status":"''' + status + '''", 48 | "statuses":{ 49 | "hvac_mode":null 50 | }, 51 | "status_ex":"", 52 | "actions":[ 53 | { 54 | "label":"Auto Flash", 55 | "value":"a=1&z=3&req=img;" 56 | }, 57 | { 58 | "label":"Never Flash", 59 | "value":"a=1&z=3&req=img_nf;" 60 | } 61 | ], 62 | "status_icons":[ 63 | ], 64 | "motion_event":"1", 65 | "wide_angle":"0", 66 | "icon":"assets/icons/motioncamera-b.svg" 67 | }''' 68 | 69 | 70 | def get_capture_timeout(): 71 | """Mock timeout response.""" 72 | return ''' 73 | { 74 | "code":600, 75 | "message":"Image Capture request has timed out.", 76 | "title":"", 77 | "detail":null 78 | }''' 79 | 80 | 81 | FILE_PATH_ID = 'ZB00000005' 82 | FILE_PATH = 'api/storage/' + FILE_PATH_ID + '/2017-08-23/195505UTC/001.jpg' 83 | 84 | LOCATION_HEADER = 'https://www.google.com/images/branding/googlelogo/' + \ 85 | '1x/googlelogo_color_272x92dp.png' 86 | 87 | 88 | def timeline_event(devid=DEVICE_ID, event_code='5001', file_path=FILE_PATH): 89 | """Camera Timeline Event Mockup.""" 90 | return ''' 91 | { 92 | "id": "71739948", 93 | "event_utc": "1503518105", 94 | "nest_activity_zones": null, 95 | "nest_has_motion": null, 96 | "nest_has_sound": null, 97 | "nest_has_person": null, 98 | "date": "08/23/2017", 99 | "time": "12:55 PM", 100 | "is_alarm": "0", 101 | "event_cid": "", 102 | "event_code": "''' + event_code + '''", 103 | "device_id": "''' + devid + '''", 104 | "device_type_id": "27", 105 | "device_type": "Motion Camera", 106 | "device_name": "Downstairs Motion Camera", 107 | "file_path":"''' + file_path + '''", 108 | "deep_link": null, 109 | "app_deep_link": null, 110 | "file_size": "30852", 111 | "file_count": "1", 112 | "file_is_del": "0", 113 | "event_type": "Image Capture", 114 | "severity": "6", 115 | "pos": "l", 116 | "color": "#40bbea", 117 | "viewed_by_uid": "", 118 | "user_id": "1234", 119 | "user_name": "Wil", 120 | "mobile_name": "", 121 | "parent_tid": "", 122 | "icon": "assets/email/motion-camera.png", 123 | "app_type": "WebApp", 124 | "file_del_at": "", 125 | "event_name": "Downstairs Motion Camera Image Capture", 126 | "event_by": "" 127 | }''' 128 | -------------------------------------------------------------------------------- /tests/mock/devices/dimmer.py: -------------------------------------------------------------------------------- 1 | """Mock Abode Power Switch Sensor Device.""" 2 | import abodepy.helpers.constants as CONST 3 | 4 | DEVICE_ID = 'ZW:00000105' 5 | CONTROL_URL = 'api/v1/control/light/' + DEVICE_ID 6 | 7 | 8 | def device(devid=DEVICE_ID, status=CONST.STATUS_OFF, 9 | level=0, low_battery=False, no_response=False): 10 | """Dimmer mock device.""" 11 | return ''' 12 | { 13 | "id":"''' + devid + '''", 14 | "type_tag":"device_type.dimmer", 15 | "type":"Dimmer", 16 | "name":"Kitchen Lights", 17 | "area":"1", 18 | "zone":"12", 19 | "sort_order":"", 20 | "is_window":"", 21 | "bypass":"0", 22 | "schar_24hr":"0", 23 | "sresp_24hr":"0", 24 | "sresp_mode_0":"0", 25 | "sresp_entry_0":"0", 26 | "sresp_exit_0":"0", 27 | "group_name":"Ungrouped", 28 | "group_id":"1", 29 | "default_group_id":"1", 30 | "sort_id":"10000", 31 | "sresp_mode_1":"0", 32 | "sresp_entry_1":"0", 33 | "sresp_exit_1":"0", 34 | "sresp_mode_2":"0", 35 | "sresp_entry_2":"0", 36 | "sresp_exit_2":"0", 37 | "sresp_mode_3":"0", 38 | "uuid":"fcc65fb7d52cdf7b080a2005539e30a4", 39 | "sresp_entry_3":"0", 40 | "sresp_exit_3":"0", 41 | "sresp_mode_4":"0", 42 | "sresp_entry_4":"0", 43 | "sresp_exit_4":"0", 44 | "version":"", 45 | "origin":"abode", 46 | "has_subscription":null, 47 | "onboard":"0", 48 | "s2_grnt_keys":"", 49 | "s2_dsk":"", 50 | "s2_propty":"", 51 | "s2_keys_valid":"", 52 | "zwave_secure_protocol":"", 53 | "control_url":"''' + CONTROL_URL + '''", 54 | "deep_link":null, 55 | "status_color":"#5cb85c", 56 | "faults":{ 57 | "low_battery":''' + str(int(low_battery)) + ''', 58 | "tempered":0, 59 | "supervision":0, 60 | "out_of_order":0, 61 | "no_response":''' + str(int(no_response)) + ''', 62 | "jammed":0, 63 | "zwave_fault":0 64 | }, 65 | "status":"''' + status + '''", 66 | "status_display":"OFF", 67 | "statuses":{ 68 | "saturation":"N/A", 69 | "hue":"N/A", 70 | "level":"''' + str(int(level)) + '''", 71 | "switch":"0", 72 | "color_temp":"N/A", 73 | "color_mode":"N/A" 74 | }, 75 | "status_ex":"", 76 | "actions":[ 77 | { 78 | "label":"Switch off", 79 | "value":"a=1&z=12&sw=off;" 80 | }, 81 | { 82 | "label":"Switch on", 83 | "value":"a=1&z=12&sw=on;" 84 | }, 85 | { 86 | "label":"Toggle", 87 | "value":"a=1&z=12&sw=toggle;" 88 | }, 89 | { 90 | "label":"0%", 91 | "value":"a=1&z=12&sw=0;" 92 | }, 93 | { 94 | "label":"10%", 95 | "value":"a=1&z=12&sw=10;" 96 | }, 97 | { 98 | "label":"20%", 99 | "value":"a=1&z=12&sw=20;" 100 | }, 101 | { 102 | "label":"30%", 103 | "value":"a=1&z=12&sw=30;" 104 | }, 105 | { 106 | "label":"40%", 107 | "value":"a=1&z=12&sw=40;" 108 | }, 109 | { 110 | "label":"50%", 111 | "value":"a=1&z=12&sw=50;" 112 | }, 113 | { 114 | "label":"60%", 115 | "value":"a=1&z=12&sw=60;" 116 | }, 117 | { 118 | "label":"70%", 119 | "value":"a=1&z=12&sw=70;" 120 | }, 121 | { 122 | "label":"80%", 123 | "value":"a=1&z=12&sw=80;" 124 | }, 125 | { 126 | "label":"90%", 127 | "value":"a=1&z=12&sw=90;" 128 | }, 129 | { 130 | "label":"100%", 131 | "value":"a=1&z=12&sw=99;" 132 | } 133 | ], 134 | "status_icons":[ 135 | 136 | ], 137 | "statusEx":"0", 138 | "icon":"assets/icons/bulb-1.svg" 139 | }''' 140 | -------------------------------------------------------------------------------- /abodepy/devices/alarm.py: -------------------------------------------------------------------------------- 1 | """Abode alarm device.""" 2 | import json 3 | import logging 4 | 5 | from abodepy.exceptions import AbodeException 6 | 7 | from abodepy.devices.switch import AbodeDevice, AbodeSwitch 8 | import abodepy.helpers.constants as CONST 9 | import abodepy.helpers.errors as ERROR 10 | 11 | _LOGGER = logging.getLogger(__name__) 12 | 13 | 14 | def create_alarm(panel_json, abode, area='1'): 15 | """Create a new alarm device from a panel response.""" 16 | panel_json['name'] = CONST.ALARM_NAME 17 | panel_json['id'] = CONST.ALARM_DEVICE_ID + area 18 | panel_json['type'] = CONST.ALARM_TYPE 19 | panel_json['type_tag'] = CONST.DEVICE_ALARM 20 | panel_json['generic_type'] = CONST.TYPE_ALARM 21 | panel_json['uuid'] = panel_json.get('mac').replace(':', '').lower() 22 | 23 | return AbodeAlarm(panel_json, abode, area) 24 | 25 | 26 | class AbodeAlarm(AbodeSwitch): 27 | """Class to represent the Abode alarm as a device.""" 28 | 29 | def __init__(self, json_obj, abode, area='1'): 30 | """Set up Abode alarm device.""" 31 | AbodeSwitch.__init__(self, json_obj, abode) 32 | self._area = area 33 | 34 | def set_mode(self, mode): 35 | """Set Abode alarm mode.""" 36 | if not mode: 37 | raise AbodeException(ERROR.MISSING_ALARM_MODE) 38 | 39 | if mode.lower() not in CONST.ALL_MODES: 40 | raise AbodeException(ERROR.INVALID_ALARM_MODE, CONST.ALL_MODES) 41 | 42 | mode = mode.lower() 43 | 44 | response = self._abode.send_request( 45 | "put", CONST.get_panel_mode_url(self._area, mode)) 46 | 47 | _LOGGER.debug("Set Alarm Home Response: %s", response.text) 48 | 49 | response_object = json.loads(response.text) 50 | 51 | if response_object['area'] != self._area: 52 | raise AbodeException(ERROR.SET_MODE_AREA) 53 | 54 | if response_object['mode'] != mode: 55 | raise AbodeException(ERROR.SET_MODE_MODE) 56 | 57 | self._json_state['mode'][(self.device_id)] = response_object['mode'] 58 | 59 | _LOGGER.info("Set alarm %s mode to: %s", 60 | self._device_id, response_object['mode']) 61 | 62 | return True 63 | 64 | def set_home(self): 65 | """Arm Abode to home mode.""" 66 | return self.set_mode(CONST.MODE_HOME) 67 | 68 | def set_away(self): 69 | """Arm Abode to home mode.""" 70 | return self.set_mode(CONST.MODE_AWAY) 71 | 72 | def set_standby(self): 73 | """Arm Abode to stay mode.""" 74 | return self.set_mode(CONST.MODE_STANDBY) 75 | 76 | def switch_on(self): 77 | """Arm Abode to default mode.""" 78 | return self.set_mode(self._abode.default_mode) 79 | 80 | def switch_off(self): 81 | """Arm Abode to home mode.""" 82 | return self.set_standby() 83 | 84 | def refresh(self, url=CONST.PANEL_URL): 85 | """Refresh the alarm device.""" 86 | response_object = AbodeDevice.refresh(self, url) 87 | # pylint: disable=W0212 88 | self._abode._panel.update(response_object[0]) 89 | 90 | return response_object 91 | 92 | @property 93 | def is_on(self): 94 | """Is alarm armed.""" 95 | return self.mode in (CONST.MODE_HOME, CONST.MODE_AWAY) 96 | 97 | @property 98 | def is_standby(self): 99 | """Is alarm in standby mode.""" 100 | return self.mode == CONST.MODE_STANDBY 101 | 102 | @property 103 | def is_home(self): 104 | """Is alarm in home mode.""" 105 | return self.mode == CONST.MODE_HOME 106 | 107 | @property 108 | def is_away(self): 109 | """Is alarm in away mode.""" 110 | return self.mode == CONST.MODE_AWAY 111 | 112 | @property 113 | def mode(self): 114 | """Get alarm mode.""" 115 | mode = self.get_value('mode').get(self.device_id, None) 116 | 117 | return mode.lower() 118 | 119 | @property 120 | def status(self): 121 | """To match existing property.""" 122 | return self.mode 123 | 124 | @property 125 | def battery(self): 126 | """Return true if base station on battery backup.""" 127 | return int(self._json_state.get('battery', '0')) == 1 128 | 129 | @property 130 | def is_cellular(self): 131 | """Return true if base station on cellular backup.""" 132 | return int(self._json_state.get('is_cellular', '0')) == 1 133 | 134 | @property 135 | def mac_address(self): 136 | """Get the hub mac address.""" 137 | return self._json_state.get('mac') 138 | -------------------------------------------------------------------------------- /tests/test_secure_barrier.py: -------------------------------------------------------------------------------- 1 | """Test the Abode device classes.""" 2 | import unittest 3 | 4 | import requests_mock 5 | 6 | import abodepy 7 | import abodepy.helpers.constants as CONST 8 | 9 | import tests.mock.login as LOGIN 10 | import tests.mock.oauth_claims as OAUTH_CLAIMS 11 | import tests.mock.logout as LOGOUT 12 | import tests.mock.panel as PANEL 13 | import tests.mock.devices as DEVICES 14 | import tests.mock.devices.secure_barrier as COVER 15 | 16 | 17 | USERNAME = 'foobar' 18 | PASSWORD = 'deadbeef' 19 | 20 | 21 | class TestSecureBarrier(unittest.TestCase): 22 | """Test the AbodePy cover class.""" 23 | 24 | def setUp(self): 25 | """Set up Abode module.""" 26 | self.abode = abodepy.Abode(username=USERNAME, 27 | password=PASSWORD, 28 | disable_cache=True) 29 | 30 | def tearDown(self): 31 | """Clean up after test.""" 32 | self.abode = None 33 | 34 | @requests_mock.mock() 35 | def tests_cover_device_properties(self, m): 36 | """Tests that cover devices properties work as expected.""" 37 | # Set up URL's 38 | m.post(CONST.LOGIN_URL, text=LOGIN.post_response_ok()) 39 | m.get(CONST.OAUTH_TOKEN_URL, text=OAUTH_CLAIMS.get_response_ok()) 40 | m.post(CONST.LOGOUT_URL, text=LOGOUT.post_response_ok()) 41 | m.get(CONST.PANEL_URL, 42 | text=PANEL.get_response_ok(mode=CONST.MODE_STANDBY)) 43 | m.get(CONST.DEVICES_URL, 44 | text=COVER.device(devid=COVER.DEVICE_ID, 45 | status=CONST.STATUS_CLOSED, 46 | low_battery=False, 47 | no_response=False)) 48 | 49 | # Logout to reset everything 50 | self.abode.logout() 51 | 52 | # Get our power switch 53 | device = self.abode.get_device(COVER.DEVICE_ID) 54 | 55 | # Test our device 56 | self.assertIsNotNone(device) 57 | self.assertEqual(device.status, CONST.STATUS_CLOSED) 58 | self.assertFalse(device.battery_low) 59 | self.assertFalse(device.no_response) 60 | self.assertFalse(device.is_on) 61 | self.assertFalse(device.is_open) 62 | 63 | # Set up our direct device get url 64 | device_url = str.replace(CONST.DEVICE_URL, 65 | '$DEVID$', COVER.DEVICE_ID) 66 | 67 | # Change device properties 68 | m.get(device_url, 69 | text=COVER.device(devid=COVER.DEVICE_ID, 70 | status=CONST.STATUS_OPEN, 71 | low_battery=True, 72 | no_response=True)) 73 | 74 | # Refesh device and test changes 75 | device.refresh() 76 | 77 | self.assertEqual(device.status, CONST.STATUS_OPEN) 78 | self.assertTrue(device.battery_low) 79 | self.assertTrue(device.no_response) 80 | self.assertTrue(device.is_on) 81 | self.assertTrue(device.is_open) 82 | 83 | @requests_mock.mock() 84 | def tests_cover_status_changes(self, m): 85 | """Tests that cover device changes work as expected.""" 86 | # Set up URL's 87 | m.post(CONST.LOGIN_URL, text=LOGIN.post_response_ok()) 88 | m.get(CONST.OAUTH_TOKEN_URL, text=OAUTH_CLAIMS.get_response_ok()) 89 | m.post(CONST.LOGOUT_URL, text=LOGOUT.post_response_ok()) 90 | m.get(CONST.PANEL_URL, 91 | text=PANEL.get_response_ok(mode=CONST.MODE_STANDBY)) 92 | m.get(CONST.DEVICES_URL, 93 | text=COVER.device(devid=COVER.DEVICE_ID, 94 | status=CONST.STATUS_CLOSED, 95 | low_battery=False, 96 | no_response=False)) 97 | 98 | # Logout to reset everything 99 | self.abode.logout() 100 | 101 | # Get our power switch 102 | device = self.abode.get_device(COVER.DEVICE_ID) 103 | 104 | # Test that we have our device 105 | self.assertIsNotNone(device) 106 | self.assertEqual(device.status, CONST.STATUS_CLOSED) 107 | self.assertFalse(device.is_open) 108 | 109 | # Set up control url response 110 | control_url = CONST.BASE_URL + COVER.CONTROL_URL 111 | m.put(control_url, 112 | text=DEVICES.status_put_response_ok( 113 | devid=COVER.DEVICE_ID, 114 | status=CONST.STATUS_OPEN_INT)) 115 | 116 | # Change the cover to open 117 | self.assertTrue(device.open_cover()) 118 | self.assertEqual(device.status, CONST.STATUS_OPEN) 119 | self.assertTrue(device.is_open) 120 | 121 | # Change response 122 | m.put(control_url, 123 | text=DEVICES.status_put_response_ok( 124 | devid=COVER.DEVICE_ID, 125 | status=CONST.STATUS_CLOSED_INT)) 126 | 127 | # Change the mode to "off" 128 | self.assertTrue(device.close_cover()) 129 | self.assertEqual(device.status, CONST.STATUS_CLOSED) 130 | self.assertFalse(device.is_open) 131 | -------------------------------------------------------------------------------- /tests/mock/devices/hue.py: -------------------------------------------------------------------------------- 1 | """Mock Abode Power Switch Sensor Device.""" 2 | import abodepy.helpers.constants as CONST 3 | 4 | DEVICE_ID = 'ZB:00000106' 5 | DEVICE_UUID = 'abcd33455232fff31232' 6 | CONTROL_URL = 'api/v1/control/light/' + DEVICE_ID 7 | INTEGRATIONS_URL = CONST.INTEGRATIONS_URL + DEVICE_UUID 8 | 9 | 10 | def color_temp_post_response_ok(devid, color_temp): 11 | """Return color temp change response json.""" 12 | return ''' 13 | { 14 | "idForPanel": "''' + devid + '''", 15 | "colorTemperature": ''' + str(int(color_temp)) + ''' 16 | }''' 17 | 18 | 19 | def color_post_response_ok(devid, hue, saturation): 20 | """Return color change response json.""" 21 | return ''' 22 | { 23 | "idForPanel": "''' + devid + '''", 24 | "hue": ''' + str(int(hue)) + ''', 25 | "saturation": ''' + str(int(saturation)) + ''' 26 | }''' 27 | 28 | 29 | def device(devid=DEVICE_ID, uuid=DEVICE_UUID, status=CONST.STATUS_OFF, 30 | level=0, saturation=57, hue=60, color_temp=6536, 31 | color_mode=CONST.COLOR_MODE_OFF, low_battery=False, 32 | no_response=False): 33 | """Hue mock device.""" 34 | return ''' 35 | { 36 | "id":"''' + devid + '''", 37 | "type_tag":"device_type.hue", 38 | "type":"RGB Dimmer", 39 | "name":"Overhead Light", 40 | "area":"1", 41 | "zone":"30", 42 | "sort_order":"", 43 | "is_window":"", 44 | "bypass":"0", 45 | "schar_24hr":"0", 46 | "sresp_24hr":"0", 47 | "sresp_mode_0":"0", 48 | "sresp_entry_0":"0", 49 | "sresp_exit_0":"0", 50 | "group_name":"Ungrouped", 51 | "group_id":"1", 52 | "default_group_id":"1", 53 | "sort_id":"10000", 54 | "sresp_mode_1":"0", 55 | "sresp_entry_1":"0", 56 | "sresp_exit_1":"0", 57 | "sresp_mode_2":"0", 58 | "sresp_entry_2":"0", 59 | "sresp_exit_2":"0", 60 | "sresp_mode_3":"0", 61 | "uuid":"''' + DEVICE_UUID + '''", 62 | "sresp_entry_3":"0", 63 | "sresp_exit_3":"0", 64 | "sresp_mode_4":"0", 65 | "sresp_entry_4":"0", 66 | "sresp_exit_4":"0", 67 | "version":"LST002", 68 | "origin":"abode", 69 | "has_subscription":null, 70 | "control_url":"''' + CONTROL_URL + '''", 71 | "deep_link":null, 72 | "status_color":"#5cb85c", 73 | "faults":{ 74 | "low_battery":''' + str(int(low_battery)) + ''', 75 | "tempered":0, 76 | "supervision":0, 77 | "out_of_order":0, 78 | "no_response":''' + str(int(no_response)) + ''', 79 | "jammed":0, 80 | "zwave_fault":0 81 | }, 82 | "status":"''' + status + '''", 83 | "statuses":{ 84 | "saturation":''' + str(int(saturation)) + ''', 85 | "hue":''' + str(int(hue)) + ''', 86 | "level":"''' + str(int(level)) + '''", 87 | "switch":"1", 88 | "color_temp":''' + str(int(color_temp)) + ''', 89 | "color_mode":"''' + str(int(color_mode)) + '''" 90 | }, 91 | "status_ex":"", 92 | "actions":[ 93 | { 94 | "label":"Switch off", 95 | "value":"a=1&z=30&sw=off;" 96 | }, 97 | { 98 | "label":"Switch on", 99 | "value":"a=1&z=30&sw=on;" 100 | }, 101 | { 102 | "label":"Toggle", 103 | "value":"a=1&z=30&sw=toggle;" 104 | }, 105 | { 106 | "label":"0%", 107 | "value":"a=1&z=30&sw=0;" 108 | }, 109 | { 110 | "label":"10%", 111 | "value":"a=1&z=30&sw=10;" 112 | }, 113 | { 114 | "label":"20%", 115 | "value":"a=1&z=30&sw=20;" 116 | }, 117 | { 118 | "label":"30%", 119 | "value":"a=1&z=30&sw=30;" 120 | }, 121 | { 122 | "label":"40%", 123 | "value":"a=1&z=30&sw=40;" 124 | }, 125 | { 126 | "label":"50%", 127 | "value":"a=1&z=30&sw=50;" 128 | }, 129 | { 130 | "label":"60%", 131 | "value":"a=1&z=30&sw=60;" 132 | }, 133 | { 134 | "label":"70%", 135 | "value":"a=1&z=30&sw=70;" 136 | }, 137 | { 138 | "label":"80%", 139 | "value":"a=1&z=30&sw=80;" 140 | }, 141 | { 142 | "label":"90%", 143 | "value":"a=1&z=30&sw=90;" 144 | }, 145 | { 146 | "label":"100%", 147 | "value":"a=1&z=30&sw=99;" 148 | } 149 | ], 150 | "status_icons":[ 151 | ], 152 | "statusEx":"37", 153 | "icon":"assets/icons/bulb-1.svg" 154 | }''' 155 | -------------------------------------------------------------------------------- /abodepy/devices/light.py: -------------------------------------------------------------------------------- 1 | """Abode light device.""" 2 | import json 3 | import logging 4 | import math 5 | 6 | from abodepy.exceptions import AbodeException 7 | 8 | from abodepy.devices.switch import AbodeSwitch 9 | import abodepy.helpers.constants as CONST 10 | import abodepy.helpers.errors as ERROR 11 | 12 | 13 | _LOGGER = logging.getLogger(__name__) 14 | 15 | 16 | class AbodeLight(AbodeSwitch): 17 | """Class for lights (dimmers).""" 18 | 19 | def set_color_temp(self, color_temp): 20 | """Set device color.""" 21 | if self._json_state['control_url']: 22 | url = CONST.INTEGRATIONS_URL + self._device_uuid 23 | 24 | color_data = { 25 | 'action': 'setcolortemperature', 26 | 'colorTemperature': int(color_temp) 27 | } 28 | 29 | response = self._abode.send_request("post", url, data=color_data) 30 | response_object = json.loads(response.text) 31 | 32 | _LOGGER.debug("Set Color Temp Response: %s", response.text) 33 | 34 | if response_object['idForPanel'] != self.device_id: 35 | raise AbodeException((ERROR.SET_STATUS_DEV_ID)) 36 | 37 | if response_object['colorTemperature'] != int(color_temp): 38 | _LOGGER.warning( 39 | ("Set color temp mismatch for device %s. " 40 | "Request val: %s, Response val: %s "), 41 | self.device_id, color_temp, 42 | response_object['colorTemperature']) 43 | 44 | color_temp = response_object['colorTemperature'] 45 | 46 | self.update({ 47 | 'statuses': { 48 | 'color_temp': color_temp 49 | } 50 | }) 51 | 52 | _LOGGER.info("Set device %s color_temp to: %s", 53 | self.device_id, color_temp) 54 | return True 55 | 56 | return False 57 | 58 | def set_color(self, color): 59 | """Set device color.""" 60 | if self._json_state['control_url']: 61 | url = CONST.INTEGRATIONS_URL + self._device_uuid 62 | 63 | hue, saturation = color 64 | color_data = { 65 | 'action': 'setcolor', 66 | 'hue': int(hue), 67 | 'saturation': int(saturation) 68 | } 69 | 70 | response = self._abode.send_request("post", url, data=color_data) 71 | response_object = json.loads(response.text) 72 | 73 | _LOGGER.debug("Set Color Response: %s", response.text) 74 | 75 | if response_object['idForPanel'] != self.device_id: 76 | raise AbodeException((ERROR.SET_STATUS_DEV_ID)) 77 | 78 | # Abode will sometimes return hue value off by 1 (rounding error) 79 | hue_comparison = math.isclose(response_object["hue"], 80 | int(hue), abs_tol=1) 81 | if not hue_comparison or (response_object["saturation"] 82 | != int(saturation)): 83 | _LOGGER.warning( 84 | ("Set color mismatch for device %s. " 85 | "Request val: %s, Response val: %s "), 86 | self.device_id, (hue, saturation), 87 | (response_object['hue'], response_object['saturation'])) 88 | 89 | hue = response_object['hue'] 90 | saturation = response_object['saturation'] 91 | 92 | self.update({ 93 | 'statuses': { 94 | 'hue': hue, 95 | 'saturation': saturation 96 | } 97 | }) 98 | 99 | _LOGGER.info("Set device %s color to: %s", 100 | self.device_id, (hue, saturation)) 101 | 102 | return True 103 | 104 | return False 105 | 106 | @property 107 | def brightness(self): 108 | """Get light brightness.""" 109 | return self.get_value(CONST.STATUSES_KEY).get('level') 110 | 111 | @property 112 | def color_temp(self): 113 | """Get light color temp.""" 114 | return self.get_value(CONST.STATUSES_KEY).get('color_temp') 115 | 116 | @property 117 | def color(self): 118 | """Get light color.""" 119 | return (self.get_value(CONST.STATUSES_KEY).get('hue'), 120 | self.get_value(CONST.STATUSES_KEY).get('saturation')) 121 | 122 | @property 123 | def has_brightness(self): 124 | """Device has brightness.""" 125 | return self.brightness 126 | 127 | @property 128 | def has_color(self): 129 | """Device is using color mode.""" 130 | if (self.get_value(CONST.STATUSES_KEY).get('color_mode') 131 | == str(CONST.COLOR_MODE_ON)): 132 | return True 133 | return False 134 | 135 | @property 136 | def is_color_capable(self): 137 | """Device is color compatible.""" 138 | return 'RGB' in self._type 139 | 140 | @property 141 | def is_dimmable(self): 142 | """Device is dimmable.""" 143 | return 'Dimmer' in self._type 144 | -------------------------------------------------------------------------------- /tests/test_valve.py: -------------------------------------------------------------------------------- 1 | """Test the Abode device classes.""" 2 | import unittest 3 | 4 | import requests_mock 5 | 6 | import abodepy 7 | import abodepy.helpers.constants as CONST 8 | 9 | import tests.mock.login as LOGIN 10 | import tests.mock.oauth_claims as OAUTH_CLAIMS 11 | import tests.mock.logout as LOGOUT 12 | import tests.mock.panel as PANEL 13 | import tests.mock.devices as DEVICES 14 | import tests.mock.devices.valve as VALVE 15 | 16 | 17 | USERNAME = 'foobar' 18 | PASSWORD = 'deadbeef' 19 | 20 | 21 | class TestValve(unittest.TestCase): 22 | """Test the AbodePy valve.""" 23 | 24 | def setUp(self): 25 | """Set up Abode module.""" 26 | self.abode = abodepy.Abode(username=USERNAME, 27 | password=PASSWORD, 28 | disable_cache=True) 29 | 30 | def tearDown(self): 31 | """Clean up after test.""" 32 | self.abode = None 33 | 34 | @requests_mock.mock() 35 | def tests_switch_device_properties(self, m): 36 | """Tests that switch devices properties work as expected.""" 37 | # Set up URL's 38 | m.post(CONST.LOGIN_URL, text=LOGIN.post_response_ok()) 39 | m.get(CONST.OAUTH_TOKEN_URL, text=OAUTH_CLAIMS.get_response_ok()) 40 | m.post(CONST.LOGOUT_URL, text=LOGOUT.post_response_ok()) 41 | m.get(CONST.PANEL_URL, 42 | text=PANEL.get_response_ok(mode=CONST.MODE_STANDBY)) 43 | m.get(CONST.DEVICES_URL, 44 | text=VALVE.device(devid=VALVE.DEVICE_ID, 45 | status=CONST.STATUS_CLOSED, 46 | low_battery=False, 47 | no_response=False)) 48 | 49 | # Logout to reset everything 50 | self.abode.logout() 51 | 52 | # Get our power switch 53 | device = self.abode.get_device(VALVE.DEVICE_ID) 54 | 55 | # Test our device 56 | self.assertIsNotNone(device) 57 | self.assertEqual(device.status, CONST.STATUS_CLOSED) 58 | self.assertFalse(device.battery_low) 59 | self.assertFalse(device.no_response) 60 | self.assertFalse(device.is_on) 61 | self.assertFalse(device.is_dimmable) 62 | 63 | # Set up our direct device get url 64 | device_url = str.replace(CONST.DEVICE_URL, 65 | '$DEVID$', VALVE.DEVICE_ID) 66 | 67 | # Change device properties 68 | m.get(device_url, 69 | text=VALVE.device(devid=VALVE.DEVICE_ID, 70 | status=CONST.STATUS_OPEN, 71 | low_battery=True, 72 | no_response=True)) 73 | 74 | # Refesh device and test changes 75 | device.refresh() 76 | 77 | self.assertEqual(device.status, CONST.STATUS_OPEN) 78 | self.assertTrue(device.battery_low) 79 | self.assertTrue(device.no_response) 80 | self.assertTrue(device.is_on) 81 | 82 | @requests_mock.mock() 83 | def tests_switch_status_changes(self, m): 84 | """Tests that switch device changes work as expected.""" 85 | # Set up URL's 86 | m.post(CONST.LOGIN_URL, text=LOGIN.post_response_ok()) 87 | m.get(CONST.OAUTH_TOKEN_URL, text=OAUTH_CLAIMS.get_response_ok()) 88 | m.post(CONST.LOGOUT_URL, text=LOGOUT.post_response_ok()) 89 | m.get(CONST.PANEL_URL, 90 | text=PANEL.get_response_ok(mode=CONST.MODE_STANDBY)) 91 | m.get(CONST.DEVICES_URL, 92 | text=VALVE.device(devid=VALVE.DEVICE_ID, 93 | status=CONST.STATUS_CLOSED, 94 | low_battery=False, 95 | no_response=False)) 96 | 97 | # Logout to reset everything 98 | self.abode.logout() 99 | 100 | # Get our power switch 101 | device = self.abode.get_device(VALVE.DEVICE_ID) 102 | 103 | # Test that we have our device 104 | self.assertIsNotNone(device) 105 | self.assertEqual(device.status, CONST.STATUS_CLOSED) 106 | self.assertFalse(device.is_on) 107 | 108 | # Set up control url response 109 | control_url = CONST.BASE_URL + VALVE.CONTROL_URL 110 | m.put(control_url, 111 | text=DEVICES.status_put_response_ok( 112 | devid=VALVE.DEVICE_ID, 113 | status=CONST.STATUS_OPEN_INT)) 114 | 115 | # Change the mode to "on" 116 | self.assertTrue(device.switch_on()) 117 | self.assertEqual(device.status, CONST.STATUS_OPEN) 118 | self.assertTrue(device.is_on) 119 | 120 | # Change response 121 | m.put(control_url, 122 | text=DEVICES.status_put_response_ok( 123 | devid=VALVE.DEVICE_ID, 124 | status=CONST.STATUS_CLOSED_INT)) 125 | 126 | # Change the mode to "off" 127 | self.assertTrue(device.switch_off()) 128 | self.assertEqual(device.status, CONST.STATUS_CLOSED) 129 | self.assertFalse(device.is_on) 130 | 131 | # Test that an invalid status response throws exception 132 | m.put(control_url, 133 | text=DEVICES.status_put_response_ok( 134 | devid=VALVE.DEVICE_ID, 135 | status=CONST.STATUS_CLOSED_INT)) 136 | 137 | with self.assertRaises(abodepy.AbodeException): 138 | device.switch_on() 139 | -------------------------------------------------------------------------------- /tests/test_power_switch_meter.py: -------------------------------------------------------------------------------- 1 | """Test the Abode device classes.""" 2 | import unittest 3 | 4 | import requests_mock 5 | 6 | import abodepy 7 | import abodepy.helpers.constants as CONST 8 | 9 | import tests.mock.login as LOGIN 10 | import tests.mock.oauth_claims as OAUTH_CLAIMS 11 | import tests.mock.logout as LOGOUT 12 | import tests.mock.panel as PANEL 13 | import tests.mock.devices as DEVICES 14 | import tests.mock.devices.power_switch_meter as POWERMETER 15 | 16 | 17 | USERNAME = 'foobar' 18 | PASSWORD = 'deadbeef' 19 | 20 | 21 | class TestPowerSwitchMeter(unittest.TestCase): 22 | """Test the AbodePy power switch meter class.""" 23 | 24 | def setUp(self): 25 | """Set up Abode module.""" 26 | self.abode = abodepy.Abode(username=USERNAME, 27 | password=PASSWORD, 28 | disable_cache=True) 29 | 30 | def tearDown(self): 31 | """Clean up after test.""" 32 | self.abode = None 33 | 34 | @requests_mock.mock() 35 | def tests_switch_device_properties(self, m): 36 | """Tests that switch devices properties work as expected.""" 37 | # Set up URL's 38 | m.post(CONST.LOGIN_URL, text=LOGIN.post_response_ok()) 39 | m.get(CONST.OAUTH_TOKEN_URL, text=OAUTH_CLAIMS.get_response_ok()) 40 | m.post(CONST.LOGOUT_URL, text=LOGOUT.post_response_ok()) 41 | m.get(CONST.PANEL_URL, 42 | text=PANEL.get_response_ok(mode=CONST.MODE_STANDBY)) 43 | m.get(CONST.DEVICES_URL, 44 | text=POWERMETER.device(devid=POWERMETER.DEVICE_ID, 45 | status=CONST.STATUS_OFF, 46 | low_battery=False, 47 | no_response=False)) 48 | 49 | # Logout to reset everything 50 | self.abode.logout() 51 | 52 | # Get our power switch 53 | device = self.abode.get_device(POWERMETER.DEVICE_ID) 54 | 55 | # Test our device 56 | self.assertIsNotNone(device) 57 | self.assertEqual(device.status, CONST.STATUS_OFF) 58 | self.assertFalse(device.battery_low) 59 | self.assertFalse(device.no_response) 60 | self.assertFalse(device.is_on) 61 | 62 | # Set up our direct device get url 63 | device_url = str.replace(CONST.DEVICE_URL, 64 | '$DEVID$', POWERMETER.DEVICE_ID) 65 | 66 | # Change device properties 67 | m.get(device_url, 68 | text=POWERMETER.device(devid=POWERMETER.DEVICE_ID, 69 | status=CONST.STATUS_ON, 70 | low_battery=True, 71 | no_response=True)) 72 | 73 | # Refesh device and test changes 74 | device.refresh() 75 | 76 | self.assertEqual(device.status, CONST.STATUS_ON) 77 | self.assertTrue(device.battery_low) 78 | self.assertTrue(device.no_response) 79 | self.assertTrue(device.is_on) 80 | 81 | @requests_mock.mock() 82 | def tests_switch_status_changes(self, m): 83 | """Tests that switch device changes work as expected.""" 84 | # Set up URL's 85 | m.post(CONST.LOGIN_URL, text=LOGIN.post_response_ok()) 86 | m.get(CONST.OAUTH_TOKEN_URL, text=OAUTH_CLAIMS.get_response_ok()) 87 | m.post(CONST.LOGOUT_URL, text=LOGOUT.post_response_ok()) 88 | m.get(CONST.PANEL_URL, 89 | text=PANEL.get_response_ok(mode=CONST.MODE_STANDBY)) 90 | m.get(CONST.DEVICES_URL, 91 | text=POWERMETER.device(devid=POWERMETER.DEVICE_ID, 92 | status=CONST.STATUS_OFF, 93 | low_battery=False, 94 | no_response=False)) 95 | 96 | # Logout to reset everything 97 | self.abode.logout() 98 | 99 | # Get our power switch 100 | device = self.abode.get_device(POWERMETER.DEVICE_ID) 101 | 102 | # Test that we have our device 103 | self.assertIsNotNone(device) 104 | self.assertEqual(device.status, CONST.STATUS_OFF) 105 | self.assertFalse(device.is_on) 106 | 107 | # Set up control url response 108 | control_url = CONST.BASE_URL + POWERMETER.CONTROL_URL 109 | m.put(control_url, 110 | text=DEVICES.status_put_response_ok( 111 | devid=POWERMETER.DEVICE_ID, 112 | status=CONST.STATUS_ON_INT)) 113 | 114 | # Change the mode to "on" 115 | self.assertTrue(device.switch_on()) 116 | self.assertEqual(device.status, CONST.STATUS_ON) 117 | self.assertTrue(device.is_on) 118 | 119 | # Change response 120 | m.put(control_url, 121 | text=DEVICES.status_put_response_ok( 122 | devid=POWERMETER.DEVICE_ID, 123 | status=CONST.STATUS_OFF_INT)) 124 | 125 | # Change the mode to "off" 126 | self.assertTrue(device.switch_off()) 127 | self.assertEqual(device.status, CONST.STATUS_OFF) 128 | self.assertFalse(device.is_on) 129 | 130 | # Test that an invalid status response throws exception 131 | m.put(control_url, 132 | text=DEVICES.status_put_response_ok( 133 | devid=POWERMETER.DEVICE_ID, 134 | status=CONST.STATUS_OFF_INT)) 135 | 136 | with self.assertRaises(abodepy.AbodeException): 137 | device.switch_on() 138 | -------------------------------------------------------------------------------- /tests/test_door_lock.py: -------------------------------------------------------------------------------- 1 | """Test the Abode device classes.""" 2 | import unittest 3 | 4 | import requests_mock 5 | 6 | import abodepy 7 | import abodepy.helpers.constants as CONST 8 | 9 | import tests.mock.login as LOGIN 10 | import tests.mock.oauth_claims as OAUTH_CLAIMS 11 | import tests.mock.logout as LOGOUT 12 | import tests.mock.panel as PANEL 13 | import tests.mock.devices as DEVICES 14 | import tests.mock.devices.door_lock as DOOR_LOCK 15 | 16 | 17 | USERNAME = 'foobar' 18 | PASSWORD = 'deadbeef' 19 | 20 | 21 | class TestDoorLock(unittest.TestCase): 22 | """Test the generic AbodePy device class.""" 23 | 24 | def setUp(self): 25 | """Set up Abode module.""" 26 | self.abode = abodepy.Abode(username=USERNAME, 27 | password=PASSWORD, 28 | disable_cache=True) 29 | 30 | def tearDown(self): 31 | """Clean up after test.""" 32 | self.abode = None 33 | 34 | @requests_mock.mock() 35 | def tests_lock_device_properties(self, m): 36 | """Tests that lock devices properties work as expected.""" 37 | # Set up URL's 38 | m.post(CONST.LOGIN_URL, text=LOGIN.post_response_ok()) 39 | m.get(CONST.OAUTH_TOKEN_URL, text=OAUTH_CLAIMS.get_response_ok()) 40 | m.post(CONST.LOGOUT_URL, text=LOGOUT.post_response_ok()) 41 | m.get(CONST.PANEL_URL, 42 | text=PANEL.get_response_ok(mode=CONST.MODE_STANDBY)) 43 | m.get(CONST.DEVICES_URL, 44 | text=DOOR_LOCK.device(devid=DOOR_LOCK.DEVICE_ID, 45 | status=CONST.STATUS_LOCKCLOSED, 46 | low_battery=False, 47 | no_response=False)) 48 | 49 | # Logout to reset everything 50 | self.abode.logout() 51 | 52 | # Get our lock 53 | device = self.abode.get_device(DOOR_LOCK.DEVICE_ID) 54 | 55 | # Test our device 56 | self.assertIsNotNone(device) 57 | self.assertEqual(device.status, CONST.STATUS_LOCKCLOSED) 58 | self.assertFalse(device.battery_low) 59 | self.assertFalse(device.no_response) 60 | self.assertTrue(device.is_locked) 61 | 62 | # Set up our direct device get url 63 | device_url = str.replace(CONST.DEVICE_URL, 64 | '$DEVID$', DOOR_LOCK.DEVICE_ID) 65 | 66 | # Change device properties 67 | m.get(device_url, 68 | text=DOOR_LOCK.device(devid=DOOR_LOCK.DEVICE_ID, 69 | status=CONST.STATUS_LOCKOPEN, 70 | low_battery=True, 71 | no_response=True)) 72 | 73 | # Refesh device and test changes 74 | device.refresh() 75 | 76 | self.assertEqual(device.status, CONST.STATUS_LOCKOPEN) 77 | self.assertTrue(device.battery_low) 78 | self.assertTrue(device.no_response) 79 | self.assertFalse(device.is_locked) 80 | 81 | @requests_mock.mock() 82 | def tests_lock_device_mode_changes(self, m): 83 | """Tests that lock device changes work as expected.""" 84 | # Set up URL's 85 | m.post(CONST.LOGIN_URL, text=LOGIN.post_response_ok()) 86 | m.get(CONST.OAUTH_TOKEN_URL, text=OAUTH_CLAIMS.get_response_ok()) 87 | m.post(CONST.LOGOUT_URL, text=LOGOUT.post_response_ok()) 88 | m.get(CONST.PANEL_URL, 89 | text=PANEL.get_response_ok(mode=CONST.MODE_STANDBY)) 90 | m.get(CONST.DEVICES_URL, 91 | text=DOOR_LOCK.device(devid=DOOR_LOCK.DEVICE_ID, 92 | status=CONST.STATUS_LOCKCLOSED, 93 | low_battery=False, 94 | no_response=False)) 95 | 96 | # Logout to reset everything 97 | self.abode.logout() 98 | 99 | # Get our power switch 100 | device = self.abode.get_device(DOOR_LOCK.DEVICE_ID) 101 | 102 | # Test that we have our device 103 | self.assertIsNotNone(device) 104 | self.assertEqual(device.status, CONST.STATUS_LOCKCLOSED) 105 | self.assertTrue(device.is_locked) 106 | 107 | # Set up control url response 108 | control_url = CONST.BASE_URL + DOOR_LOCK.CONTROL_URL 109 | m.put(control_url, 110 | text=DEVICES.status_put_response_ok( 111 | devid=DOOR_LOCK.DEVICE_ID, 112 | status=CONST.STATUS_LOCKOPEN_INT)) 113 | 114 | # Change the mode to "on" 115 | self.assertTrue(device.unlock()) 116 | self.assertEqual(device.status, CONST.STATUS_LOCKOPEN) 117 | self.assertFalse(device.is_locked) 118 | 119 | # Change response 120 | m.put(control_url, 121 | text=DEVICES.status_put_response_ok( 122 | devid=DOOR_LOCK.DEVICE_ID, 123 | status=CONST.STATUS_LOCKCLOSED_INT)) 124 | 125 | # Change the mode to "off" 126 | self.assertTrue(device.lock()) 127 | self.assertEqual(device.status, CONST.STATUS_LOCKCLOSED) 128 | self.assertTrue(device.is_locked) 129 | 130 | # Test that an invalid status response throws exception 131 | m.put(control_url, 132 | text=DEVICES.status_put_response_ok( 133 | devid=DOOR_LOCK.DEVICE_ID, 134 | status=CONST.STATUS_LOCKCLOSED_INT)) 135 | 136 | with self.assertRaises(abodepy.AbodeException): 137 | device.unlock() 138 | -------------------------------------------------------------------------------- /tests/test_power_switch_sensor.py: -------------------------------------------------------------------------------- 1 | """Test the Abode device classes.""" 2 | import unittest 3 | 4 | import requests_mock 5 | 6 | import abodepy 7 | import abodepy.helpers.constants as CONST 8 | 9 | import tests.mock.login as LOGIN 10 | import tests.mock.oauth_claims as OAUTH_CLAIMS 11 | import tests.mock.logout as LOGOUT 12 | import tests.mock.panel as PANEL 13 | import tests.mock.devices as DEVICES 14 | import tests.mock.devices.power_switch_sensor as POWERSENSOR 15 | 16 | 17 | USERNAME = 'foobar' 18 | PASSWORD = 'deadbeef' 19 | 20 | 21 | class TestPowerSwitchSensor(unittest.TestCase): 22 | """Test the AbodePy power switch sensor.""" 23 | 24 | def setUp(self): 25 | """Set up Abode module.""" 26 | self.abode = abodepy.Abode(username=USERNAME, 27 | password=PASSWORD, 28 | disable_cache=True) 29 | 30 | def tearDown(self): 31 | """Clean up after test.""" 32 | self.abode = None 33 | 34 | @requests_mock.mock() 35 | def tests_switch_device_properties(self, m): 36 | """Tests that switch devices properties work as expected.""" 37 | # Set up URL's 38 | m.post(CONST.LOGIN_URL, text=LOGIN.post_response_ok()) 39 | m.get(CONST.OAUTH_TOKEN_URL, text=OAUTH_CLAIMS.get_response_ok()) 40 | m.post(CONST.LOGOUT_URL, text=LOGOUT.post_response_ok()) 41 | m.get(CONST.PANEL_URL, 42 | text=PANEL.get_response_ok(mode=CONST.MODE_STANDBY)) 43 | m.get(CONST.DEVICES_URL, 44 | text=POWERSENSOR.device(devid=POWERSENSOR.DEVICE_ID, 45 | status=CONST.STATUS_OFF, 46 | low_battery=False, 47 | no_response=False)) 48 | 49 | # Logout to reset everything 50 | self.abode.logout() 51 | 52 | # Get our power switch 53 | device = self.abode.get_device(POWERSENSOR.DEVICE_ID) 54 | 55 | # Test our device 56 | self.assertIsNotNone(device) 57 | self.assertEqual(device.status, CONST.STATUS_OFF) 58 | self.assertFalse(device.battery_low) 59 | self.assertFalse(device.no_response) 60 | self.assertFalse(device.is_on) 61 | 62 | # Set up our direct device get url 63 | device_url = str.replace(CONST.DEVICE_URL, 64 | '$DEVID$', POWERSENSOR.DEVICE_ID) 65 | 66 | # Change device properties 67 | m.get(device_url, 68 | text=POWERSENSOR.device(devid=POWERSENSOR.DEVICE_ID, 69 | status=CONST.STATUS_ON, 70 | low_battery=True, 71 | no_response=True)) 72 | 73 | # Refesh device and test changes 74 | device.refresh() 75 | 76 | self.assertEqual(device.status, CONST.STATUS_ON) 77 | self.assertTrue(device.battery_low) 78 | self.assertTrue(device.no_response) 79 | self.assertTrue(device.is_on) 80 | 81 | @requests_mock.mock() 82 | def tests_switch_status_changes(self, m): 83 | """Tests that switch device changes work as expected.""" 84 | # Set up URL's 85 | m.post(CONST.LOGIN_URL, text=LOGIN.post_response_ok()) 86 | m.get(CONST.OAUTH_TOKEN_URL, text=OAUTH_CLAIMS.get_response_ok()) 87 | m.post(CONST.LOGOUT_URL, text=LOGOUT.post_response_ok()) 88 | m.get(CONST.PANEL_URL, 89 | text=PANEL.get_response_ok(mode=CONST.MODE_STANDBY)) 90 | m.get(CONST.DEVICES_URL, 91 | text=POWERSENSOR.device(devid=POWERSENSOR.DEVICE_ID, 92 | status=CONST.STATUS_OFF, 93 | low_battery=False, 94 | no_response=False)) 95 | 96 | # Logout to reset everything 97 | self.abode.logout() 98 | 99 | # Get our power switch 100 | device = self.abode.get_device(POWERSENSOR.DEVICE_ID) 101 | 102 | # Test that we have our device 103 | self.assertIsNotNone(device) 104 | self.assertEqual(device.status, CONST.STATUS_OFF) 105 | self.assertFalse(device.is_on) 106 | 107 | # Set up control url response 108 | control_url = CONST.BASE_URL + POWERSENSOR.CONTROL_URL 109 | m.put(control_url, 110 | text=DEVICES.status_put_response_ok( 111 | devid=POWERSENSOR.DEVICE_ID, 112 | status=CONST.STATUS_ON_INT)) 113 | 114 | # Change the mode to "on" 115 | self.assertTrue(device.switch_on()) 116 | self.assertEqual(device.status, CONST.STATUS_ON) 117 | self.assertTrue(device.is_on) 118 | 119 | # Change response 120 | m.put(control_url, 121 | text=DEVICES.status_put_response_ok( 122 | devid=POWERSENSOR.DEVICE_ID, 123 | status=CONST.STATUS_OFF_INT)) 124 | 125 | # Change the mode to "off" 126 | self.assertTrue(device.switch_off()) 127 | self.assertEqual(device.status, CONST.STATUS_OFF) 128 | self.assertFalse(device.is_on) 129 | 130 | # Test that an invalid status response throws exception 131 | m.put(control_url, 132 | text=DEVICES.status_put_response_ok( 133 | devid=POWERSENSOR.DEVICE_ID, 134 | status=CONST.STATUS_OFF_INT)) 135 | 136 | with self.assertRaises(abodepy.AbodeException): 137 | device.switch_on() 138 | -------------------------------------------------------------------------------- /tests/test_dimmer.py: -------------------------------------------------------------------------------- 1 | """Test the Abode device classes.""" 2 | import unittest 3 | 4 | import requests_mock 5 | 6 | import abodepy 7 | import abodepy.helpers.constants as CONST 8 | 9 | import tests.mock.login as LOGIN 10 | import tests.mock.oauth_claims as OAUTH_CLAIMS 11 | import tests.mock.logout as LOGOUT 12 | import tests.mock.panel as PANEL 13 | import tests.mock.devices as DEVICES 14 | import tests.mock.devices.dimmer as DIMMER 15 | 16 | 17 | USERNAME = 'foobar' 18 | PASSWORD = 'deadbeef' 19 | 20 | 21 | class TestDimmer(unittest.TestCase): 22 | """Test the AbodePy light device with a dimmer.""" 23 | 24 | def setUp(self): 25 | """Set up Abode module.""" 26 | self.abode = abodepy.Abode(username=USERNAME, 27 | password=PASSWORD, 28 | disable_cache=True) 29 | 30 | def tearDown(self): 31 | """Clean up after test.""" 32 | self.abode = None 33 | 34 | @requests_mock.mock() 35 | def tests_dimmer_device_properties(self, m): 36 | """Tests that dimmer light devices properties work as expected.""" 37 | # Set up URL's 38 | m.post(CONST.LOGIN_URL, text=LOGIN.post_response_ok()) 39 | m.get(CONST.OAUTH_TOKEN_URL, text=OAUTH_CLAIMS.get_response_ok()) 40 | m.post(CONST.LOGOUT_URL, text=LOGOUT.post_response_ok()) 41 | m.get(CONST.PANEL_URL, 42 | text=PANEL.get_response_ok(mode=CONST.MODE_STANDBY)) 43 | m.get(CONST.DEVICES_URL, 44 | text=DIMMER.device(devid=DIMMER.DEVICE_ID, 45 | status=CONST.STATUS_OFF, 46 | level=0, 47 | low_battery=False, 48 | no_response=False)) 49 | 50 | # Logout to reset everything 51 | self.abode.logout() 52 | 53 | # Get our dimmer 54 | device = self.abode.get_device(DIMMER.DEVICE_ID) 55 | 56 | # Test our device 57 | self.assertIsNotNone(device) 58 | self.assertEqual(device.status, CONST.STATUS_OFF) 59 | self.assertEqual(device.brightness, "0") 60 | self.assertTrue(device.has_brightness) 61 | self.assertTrue(device.is_dimmable) 62 | self.assertFalse(device.has_color) 63 | self.assertFalse(device.is_color_capable) 64 | self.assertFalse(device.battery_low) 65 | self.assertFalse(device.no_response) 66 | self.assertFalse(device.is_on) 67 | 68 | # Set up our direct device get url 69 | device_url = str.replace(CONST.DEVICE_URL, 70 | '$DEVID$', DIMMER.DEVICE_ID) 71 | 72 | # Change device properties 73 | m.get(device_url, 74 | text=DIMMER.device(devid=DIMMER.DEVICE_ID, 75 | status=CONST.STATUS_ON, 76 | level=87, 77 | low_battery=True, 78 | no_response=True)) 79 | 80 | # Refesh device and test changes 81 | device.refresh() 82 | 83 | self.assertEqual(device.status, CONST.STATUS_ON) 84 | self.assertEqual(device.brightness, "87") 85 | self.assertTrue(device.battery_low) 86 | self.assertTrue(device.no_response) 87 | self.assertTrue(device.is_on) 88 | 89 | @requests_mock.mock() 90 | def tests_dimmer_status_changes(self, m): 91 | """Tests that dimmer device changes work as expected.""" 92 | # Set up URL's 93 | m.post(CONST.LOGIN_URL, text=LOGIN.post_response_ok()) 94 | m.get(CONST.OAUTH_TOKEN_URL, text=OAUTH_CLAIMS.get_response_ok()) 95 | m.post(CONST.LOGOUT_URL, text=LOGOUT.post_response_ok()) 96 | m.get(CONST.PANEL_URL, 97 | text=PANEL.get_response_ok(mode=CONST.MODE_STANDBY)) 98 | m.get(CONST.DEVICES_URL, 99 | text=DIMMER.device(devid=DIMMER.DEVICE_ID, 100 | status=CONST.STATUS_OFF, 101 | level=0, 102 | low_battery=False, 103 | no_response=False)) 104 | 105 | # Logout to reset everything 106 | self.abode.logout() 107 | 108 | # Get our dimmer 109 | device = self.abode.get_device(DIMMER.DEVICE_ID) 110 | 111 | # Test that we have our device 112 | self.assertIsNotNone(device) 113 | self.assertEqual(device.status, CONST.STATUS_OFF) 114 | self.assertFalse(device.is_on) 115 | 116 | # Set up control url response 117 | control_url = CONST.BASE_URL + DIMMER.CONTROL_URL 118 | m.put(control_url, 119 | text=DEVICES.status_put_response_ok( 120 | devid=DIMMER.DEVICE_ID, 121 | status=CONST.STATUS_ON_INT)) 122 | 123 | # Change the mode to "on" 124 | self.assertTrue(device.switch_on()) 125 | self.assertEqual(device.status, CONST.STATUS_ON) 126 | self.assertTrue(device.is_on) 127 | 128 | # Change response 129 | m.put(control_url, 130 | text=DEVICES.status_put_response_ok( 131 | devid=DIMMER.DEVICE_ID, 132 | status=CONST.STATUS_OFF_INT)) 133 | 134 | # Change the mode to "off" 135 | self.assertTrue(device.switch_off()) 136 | self.assertEqual(device.status, CONST.STATUS_OFF) 137 | self.assertFalse(device.is_on) 138 | 139 | # Test that an invalid status response throws exception 140 | m.put(control_url, 141 | text=DEVICES.status_put_response_ok( 142 | devid=DIMMER.DEVICE_ID, 143 | status=CONST.STATUS_OFF_INT)) 144 | 145 | with self.assertRaises(abodepy.AbodeException): 146 | device.switch_on() 147 | -------------------------------------------------------------------------------- /abodepy/devices/camera.py: -------------------------------------------------------------------------------- 1 | """Abode camera device.""" 2 | import json 3 | import logging 4 | from shutil import copyfileobj 5 | import requests 6 | 7 | from abodepy.exceptions import AbodeException 8 | from abodepy.devices import AbodeDevice 9 | import abodepy.helpers.constants as CONST 10 | import abodepy.helpers.errors as ERROR 11 | import abodepy.helpers.timeline as TIMELINE 12 | 13 | _LOGGER = logging.getLogger(__name__) 14 | 15 | 16 | class AbodeCamera(AbodeDevice): 17 | """Class to represent a camera device.""" 18 | 19 | def __init__(self, json_obj, abode): 20 | """Set up Abode alarm device.""" 21 | AbodeDevice.__init__(self, json_obj, abode) 22 | self._image_url = None 23 | 24 | def capture(self): 25 | """Request a new camera image.""" 26 | # Abode IP cameras use a different URL for image captures. 27 | if 'control_url_snapshot' in self._json_state: 28 | url = CONST.BASE_URL + self._json_state['control_url_snapshot'] 29 | 30 | elif 'control_url' in self._json_state: 31 | url = CONST.BASE_URL + self._json_state['control_url'] 32 | 33 | else: 34 | raise AbodeException((ERROR.MISSING_CONTROL_URL)) 35 | 36 | try: 37 | response = self._abode.send_request("put", url) 38 | 39 | _LOGGER.debug("Capture image response: %s", response.text) 40 | 41 | return True 42 | 43 | except AbodeException as exc: 44 | _LOGGER.warning("Failed to capture image: %s", exc) 45 | 46 | return False 47 | 48 | def refresh_image(self): 49 | """Get the most recent camera image.""" 50 | url = str.replace(CONST.TIMELINE_IMAGES_ID_URL, 51 | '$DEVID$', self.device_id) 52 | response = self._abode.send_request("get", url) 53 | 54 | _LOGGER.debug("Get image response: %s", response.text) 55 | 56 | return self.update_image_location(json.loads(response.text)) 57 | 58 | def update_image_location(self, timeline_json): 59 | """Update the image location.""" 60 | if not timeline_json: 61 | return False 62 | 63 | # If we get a list of objects back (likely) 64 | # then we just want the first one as it should be the "newest" 65 | if isinstance(timeline_json, (tuple, list)): 66 | timeline_json = timeline_json[0] 67 | 68 | # Verify that the event code is of the "CAPTURE IMAGE" event 69 | event_code = timeline_json.get('event_code') 70 | if event_code != TIMELINE.CAPTURE_IMAGE['event_code']: 71 | raise AbodeException((ERROR.CAM_TIMELINE_EVENT_INVALID)) 72 | 73 | # The timeline response has an entry for "file_path" that acts as the 74 | # location of the image within the Abode servers. 75 | file_path = timeline_json.get('file_path') 76 | if not file_path: 77 | raise AbodeException((ERROR.CAM_IMAGE_REFRESH_NO_FILE)) 78 | 79 | # Perform a "head" request for the image and look for a 80 | # 302 Found response 81 | url = CONST.BASE_URL + file_path 82 | response = self._abode.send_request("head", url) 83 | 84 | if response.status_code != 302: 85 | _LOGGER.warning("Unexected response code %s with body: %s", 86 | str(response.status_code), response.text) 87 | raise AbodeException((ERROR.CAM_IMAGE_UNEXPECTED_RESPONSE)) 88 | 89 | # The response should have a location header that is the actual 90 | # location of the image stored on AWS 91 | location = response.headers.get('location') 92 | if not location: 93 | raise AbodeException((ERROR.CAM_IMAGE_NO_LOCATION_HEADER)) 94 | 95 | self._image_url = location 96 | 97 | return True 98 | 99 | def image_to_file(self, path, get_image=True): 100 | """Write the image to a file.""" 101 | if not self.image_url or get_image: 102 | if not self.refresh_image(): 103 | return False 104 | 105 | response = requests.get(self.image_url, stream=True) 106 | 107 | if response.status_code != 200: 108 | _LOGGER.warning( 109 | "Unexpected response code %s when requesting image: %s", 110 | str(response.status_code), response.text) 111 | raise AbodeException((ERROR.CAM_IMAGE_REQUEST_INVALID)) 112 | 113 | with open(path, 'wb') as imgfile: 114 | copyfileobj(response.raw, imgfile) 115 | 116 | return True 117 | 118 | def privacy_mode(self, enable): 119 | """Set camera privacy mode (camera on/off).""" 120 | if self._json_state['privacy']: 121 | privacy = '1' if enable else '0' 122 | 123 | url = CONST.PARAMS_URL + self.device_id 124 | 125 | camera_data = { 126 | 'mac': self._json_state['camera_mac'], 127 | 'privacy': privacy, 128 | 'action': 'setParam', 129 | 'id': self.device_id 130 | } 131 | 132 | response = self._abode.send_request( 133 | method="put", url=url, data=camera_data) 134 | response_object = json.loads(response.text) 135 | 136 | _LOGGER.debug("Camera Privacy Mode Response: %s", response.text) 137 | 138 | if response_object['id'] != self.device_id: 139 | raise AbodeException((ERROR.SET_STATUS_DEV_ID)) 140 | 141 | if response_object['privacy'] != str(privacy): 142 | raise AbodeException((ERROR.SET_PRIVACY_MODE)) 143 | 144 | _LOGGER.info("Set camera %s privacy mode to: %s", 145 | self.device_id, privacy) 146 | 147 | return True 148 | 149 | return False 150 | 151 | @property 152 | def image_url(self): 153 | """Get image URL.""" 154 | return self._image_url 155 | 156 | @property 157 | def is_on(self): 158 | """Get camera state (assumed on).""" 159 | return self.status not in (CONST.STATUS_OFF, CONST.STATUS_OFFLINE) 160 | -------------------------------------------------------------------------------- /tests/test_binary_sensor.py: -------------------------------------------------------------------------------- 1 | """Test the Abode binary sensors.""" 2 | import unittest 3 | 4 | import requests_mock 5 | 6 | import abodepy 7 | import abodepy.helpers.constants as CONST 8 | 9 | import tests.mock.login as LOGIN 10 | import tests.mock.oauth_claims as OAUTH_CLAIMS 11 | import tests.mock.logout as LOGOUT 12 | import tests.mock.panel as PANEL 13 | import tests.mock.devices.door_contact as DOOR_CONTACT 14 | import tests.mock.devices.glass as GLASS 15 | import tests.mock.devices.keypad as KEYPAD 16 | import tests.mock.devices.remote_controller as REMOTE_CONTROLLER 17 | import tests.mock.devices.siren as SIREN 18 | import tests.mock.devices.status_display as STATUS_DISPLAY 19 | import tests.mock.devices.water_sensor as WATER_SENSOR 20 | 21 | 22 | USERNAME = 'foobar' 23 | PASSWORD = 'deadbeef' 24 | 25 | 26 | class TestBinarySensors(unittest.TestCase): 27 | """Test the AbodePy binary sensors.""" 28 | 29 | def setUp(self): 30 | """Set up Abode module.""" 31 | self.abode = abodepy.Abode(username=USERNAME, 32 | password=PASSWORD, 33 | disable_cache=True) 34 | 35 | def tearDown(self): 36 | """Clean up after test.""" 37 | self.abode = None 38 | 39 | @requests_mock.mock() 40 | def tests_binary_sensor_properties(self, m): 41 | """Tests that binary sensor device properties work as expected.""" 42 | # Set up URL's 43 | m.post(CONST.LOGIN_URL, text=LOGIN.post_response_ok()) 44 | m.get(CONST.OAUTH_TOKEN_URL, text=OAUTH_CLAIMS.get_response_ok()) 45 | m.post(CONST.LOGOUT_URL, text=LOGOUT.post_response_ok()) 46 | m.get(CONST.PANEL_URL, 47 | text=PANEL.get_response_ok(mode=CONST.MODE_STANDBY)) 48 | 49 | # Set up all Binary Sensor Devices in "off states" 50 | all_devices = '[' + \ 51 | DOOR_CONTACT.device(devid=DOOR_CONTACT.DEVICE_ID, 52 | status=CONST.STATUS_CLOSED, 53 | low_battery=False, 54 | no_response=False) + ',' + \ 55 | GLASS.device(devid=GLASS.DEVICE_ID, 56 | status=CONST.STATUS_OFFLINE, 57 | low_battery=False, 58 | no_response=False) + ',' + \ 59 | KEYPAD.device(devid=KEYPAD.DEVICE_ID, 60 | status=CONST.STATUS_OFFLINE, 61 | low_battery=False, 62 | no_response=False) + ',' + \ 63 | REMOTE_CONTROLLER.device(devid=REMOTE_CONTROLLER.DEVICE_ID, 64 | status=CONST.STATUS_OFFLINE, 65 | low_battery=False, 66 | no_response=False) + ',' + \ 67 | SIREN.device(devid=SIREN.DEVICE_ID, 68 | status=CONST.STATUS_OFFLINE, 69 | low_battery=False, 70 | no_response=False) + ',' + \ 71 | STATUS_DISPLAY.device(devid=STATUS_DISPLAY.DEVICE_ID, 72 | status=CONST.STATUS_OFFLINE, 73 | low_battery=False, 74 | no_response=False) + ',' + \ 75 | WATER_SENSOR.device(devid=WATER_SENSOR.DEVICE_ID, 76 | status=CONST.STATUS_OFFLINE, 77 | low_battery=False, 78 | no_response=False) + ']' 79 | 80 | m.get(CONST.DEVICES_URL, text=all_devices) 81 | 82 | # Logout to reset everything 83 | self.abode.logout() 84 | 85 | # Test our devices 86 | for device in self.abode.get_devices(): 87 | # Skip alarm devices 88 | if device.type == CONST.DEVICE_ALARM: 89 | continue 90 | 91 | self.assertFalse(device.is_on, 92 | device.type + " is_on failed") 93 | self.assertFalse(device.battery_low, 94 | device.type + " battery_low failed") 95 | self.assertFalse(device.no_response, 96 | device.type + " no_response failed") 97 | 98 | # Set up all Binary Sensor Devices in "off states" 99 | all_devices = '[' + \ 100 | DOOR_CONTACT.device(devid=DOOR_CONTACT.DEVICE_ID, 101 | status=CONST.STATUS_OPEN, 102 | low_battery=True, 103 | no_response=True) + ',' + \ 104 | GLASS.device(devid=GLASS.DEVICE_ID, 105 | status=CONST.STATUS_ONLINE, 106 | low_battery=True, 107 | no_response=True) + ',' + \ 108 | KEYPAD.device(devid=KEYPAD.DEVICE_ID, 109 | status=CONST.STATUS_ONLINE, 110 | low_battery=True, 111 | no_response=True) + ',' + \ 112 | REMOTE_CONTROLLER.device(devid=REMOTE_CONTROLLER.DEVICE_ID, 113 | status=CONST.STATUS_ONLINE, 114 | low_battery=True, 115 | no_response=True) + ',' + \ 116 | SIREN.device(devid=SIREN.DEVICE_ID, 117 | status=CONST.STATUS_ONLINE, 118 | low_battery=True, 119 | no_response=True) + ',' + \ 120 | STATUS_DISPLAY.device(devid=STATUS_DISPLAY.DEVICE_ID, 121 | status=CONST.STATUS_ONLINE, 122 | low_battery=True, 123 | no_response=True) + ',' + \ 124 | WATER_SENSOR.device(devid=WATER_SENSOR.DEVICE_ID, 125 | status=CONST.STATUS_ONLINE, 126 | low_battery=True, 127 | no_response=True) + ']' 128 | 129 | m.get(CONST.DEVICES_URL, text=all_devices) 130 | 131 | # Refesh devices and test changes 132 | for device in self.abode.get_devices(refresh=True): 133 | # Skip alarm devices 134 | if device.type_tag == CONST.DEVICE_ALARM: 135 | continue 136 | 137 | self.assertTrue(device.is_on, 138 | device.type + " is_on failed") 139 | self.assertTrue(device.battery_low, 140 | device.type + " battery_low failed") 141 | self.assertTrue(device.no_response, 142 | device.type + " no_response failed") 143 | -------------------------------------------------------------------------------- /abodepy/devices/__init__.py: -------------------------------------------------------------------------------- 1 | """Init file for devices directory.""" 2 | import json 3 | import logging 4 | 5 | from abodepy.exceptions import AbodeException 6 | 7 | import abodepy.helpers.constants as CONST 8 | import abodepy.helpers.errors as ERROR 9 | 10 | 11 | _LOGGER = logging.getLogger(__name__) 12 | 13 | 14 | class AbodeDevice(): 15 | """Class to represent each Abode device.""" 16 | 17 | def __init__(self, json_obj, abode): 18 | """Set up Abode device.""" 19 | self._json_state = json_obj 20 | self._device_id = json_obj.get('id') 21 | self._device_uuid = json_obj.get('uuid') 22 | self._name = json_obj.get('name') 23 | self._type = json_obj.get('type') 24 | self._type_tag = json_obj.get('type_tag') 25 | self._generic_type = json_obj.get('generic_type') 26 | self._abode = abode 27 | 28 | self._update_name() 29 | 30 | def set_status(self, status): 31 | """Set device status.""" 32 | if self._json_state['control_url']: 33 | url = CONST.BASE_URL + self._json_state['control_url'] 34 | 35 | status_data = { 36 | 'status': str(status) 37 | } 38 | 39 | response = self._abode.send_request( 40 | method="put", url=url, data=status_data) 41 | response_object = json.loads(response.text) 42 | 43 | _LOGGER.debug("Set Status Response: %s", response.text) 44 | 45 | if response_object['id'] != self.device_id: 46 | raise AbodeException((ERROR.SET_STATUS_DEV_ID)) 47 | 48 | if response_object['status'] != str(status): 49 | raise AbodeException((ERROR.SET_STATUS_STATE)) 50 | 51 | # Note: Status result is of int type, not of new status of device. 52 | # Seriously, why would you do that? 53 | # So, can't set status here must be done at device level. 54 | 55 | _LOGGER.info("Set device %s status to: %s", self.device_id, status) 56 | 57 | return True 58 | 59 | return False 60 | 61 | def set_level(self, level): 62 | """Set device level.""" 63 | if self._json_state['control_url']: 64 | url = CONST.BASE_URL + self._json_state['control_url'] 65 | 66 | level_data = { 67 | 'level': str(level) 68 | } 69 | 70 | response = self._abode.send_request( 71 | "put", url, data=level_data) 72 | response_object = json.loads(response.text) 73 | 74 | _LOGGER.debug("Set Level Response: %s", response.text) 75 | 76 | if response_object['id'] != self.device_id: 77 | raise AbodeException((ERROR.SET_STATUS_DEV_ID)) 78 | 79 | if response_object['level'] != str(level): 80 | raise AbodeException((ERROR.SET_STATUS_STATE)) 81 | 82 | self.update(response_object) 83 | 84 | _LOGGER.info("Set device %s level to: %s", self.device_id, level) 85 | 86 | return True 87 | 88 | return False 89 | 90 | def get_value(self, name): 91 | """Get a value from the json object. 92 | 93 | This is the common data and is the best place to get state 94 | from if it has the data you require. 95 | This data is updated by the subscription service. 96 | """ 97 | return self._json_state.get(name.lower(), {}) 98 | 99 | def refresh(self, url=CONST.DEVICE_URL): 100 | """Refresh the devices json object data. 101 | 102 | Only needed if you're not using the notification service. 103 | """ 104 | url = url.replace('$DEVID$', self.device_id) 105 | 106 | response = self._abode.send_request(method="get", url=url) 107 | response_object = json.loads(response.text) 108 | 109 | _LOGGER.debug("Device Refresh Response: %s", response.text) 110 | 111 | if response_object and not isinstance(response_object, (tuple, list)): 112 | response_object = [response_object] 113 | 114 | for device in response_object: 115 | self.update(device) 116 | 117 | return response_object 118 | 119 | def update(self, json_state): 120 | """Update the json data from a dictionary. 121 | 122 | Only updates if it already exists in the device. 123 | """ 124 | self._json_state.update( 125 | {k: json_state[k] for k in json_state if self._json_state.get(k)}) 126 | self._update_name() 127 | 128 | def _update_name(self): 129 | """Set the device name from _json_state, with a sensible default.""" 130 | self._name = self._json_state.get('name') 131 | if not self._name: 132 | self._name = self.type + ' ' + self.device_id 133 | 134 | @property 135 | def status(self): 136 | """Shortcut to get the generic status of a device.""" 137 | return self.get_value('status') 138 | 139 | @property 140 | def battery_low(self): 141 | """Is battery level low.""" 142 | return int(self.get_value('faults').get('low_battery', '0')) == 1 143 | 144 | @property 145 | def no_response(self): 146 | """Is the device responding.""" 147 | return int(self.get_value('faults').get('no_response', '0')) == 1 148 | 149 | @property 150 | def out_of_order(self): 151 | """Is the device out of order.""" 152 | return int(self.get_value('faults').get('out_of_order', '0')) == 1 153 | 154 | @property 155 | def tampered(self): 156 | """Has the device been tampered with.""" 157 | # 'tempered' - Typo in API? 158 | return int(self.get_value('faults').get('tempered', '0')) == 1 159 | 160 | @property 161 | def name(self): 162 | """Get the name of this device.""" 163 | return self._name 164 | 165 | @property 166 | def generic_type(self): 167 | """Get the generic type of this device.""" 168 | return self._generic_type 169 | 170 | @property 171 | def type(self): 172 | """Get the type of this device.""" 173 | return self._type 174 | 175 | @property 176 | def type_tag(self): 177 | """Get the type tag of this device.""" 178 | return self._type_tag 179 | 180 | @property 181 | def device_id(self): 182 | """Get the device id.""" 183 | return self._device_id 184 | 185 | @property 186 | def device_uuid(self): 187 | """Get the device uuid.""" 188 | return self._device_uuid 189 | 190 | @property 191 | def desc(self): 192 | """Get a short description of the device.""" 193 | # Garage Entry Door (ZW:00000003) - Door Lock - Closed 194 | return '{0} (ID: {1}, UUID: {2}) - {3} - {4}'.format( 195 | self.name, self.device_id, self.device_uuid, 196 | self.type, self.status) 197 | -------------------------------------------------------------------------------- /tests/test_lm.py: -------------------------------------------------------------------------------- 1 | """Test the Abode device classes.""" 2 | import unittest 3 | 4 | import requests_mock 5 | 6 | import abodepy 7 | import abodepy.helpers.constants as CONST 8 | 9 | import tests.mock.login as LOGIN 10 | import tests.mock.oauth_claims as OAUTH_CLAIMS 11 | import tests.mock.logout as LOGOUT 12 | import tests.mock.panel as PANEL 13 | import tests.mock.devices.lm as LM 14 | 15 | 16 | USERNAME = 'foobar' 17 | PASSWORD = 'deadbeef' 18 | 19 | 20 | class TestLM(unittest.TestCase): 21 | """Test the AbodePy sensor class/LM.""" 22 | 23 | def setUp(self): 24 | """Set up Abode module.""" 25 | self.abode = abodepy.Abode(username=USERNAME, 26 | password=PASSWORD, 27 | disable_cache=True) 28 | 29 | def tearDown(self): 30 | """Clean up after test.""" 31 | self.abode = None 32 | 33 | @requests_mock.mock() 34 | def tests_cover_lm_properties(self, m): 35 | """Tests that sensor/LM devices properties work as expected.""" 36 | # Set up URL's 37 | m.post(CONST.LOGIN_URL, text=LOGIN.post_response_ok()) 38 | m.get(CONST.OAUTH_TOKEN_URL, text=OAUTH_CLAIMS.get_response_ok()) 39 | m.post(CONST.LOGOUT_URL, text=LOGOUT.post_response_ok()) 40 | m.get(CONST.PANEL_URL, 41 | text=PANEL.get_response_ok(mode=CONST.MODE_STANDBY)) 42 | m.get(CONST.DEVICES_URL, 43 | text=LM.device(devid=LM.DEVICE_ID, 44 | status='72 °F', 45 | temp='72 °F', 46 | lux='14 lx', 47 | humidity='34 %', 48 | low_battery=False, 49 | no_response=False)) 50 | 51 | # Logout to reset everything 52 | self.abode.logout() 53 | 54 | # Get our power switch 55 | device = self.abode.get_device(LM.DEVICE_ID) 56 | 57 | # Test our device 58 | self.assertIsNotNone(device) 59 | self.assertEqual(device.status, '72 °F') 60 | self.assertFalse(device.battery_low) 61 | self.assertFalse(device.no_response) 62 | self.assertTrue(device.has_temp) 63 | self.assertTrue(device.has_humidity) 64 | self.assertTrue(device.has_lux) 65 | self.assertEqual(device.temp, 72) 66 | self.assertEqual(device.temp_unit, '°F') 67 | self.assertEqual(device.humidity, 34) 68 | self.assertEqual(device.humidity_unit, '%') 69 | self.assertEqual(device.lux, 14) 70 | self.assertEqual(device.lux_unit, 'lux') 71 | 72 | # Set up our direct device get url 73 | device_url = str.replace(CONST.DEVICE_URL, 74 | '$DEVID$', LM.DEVICE_ID) 75 | 76 | # Change device properties 77 | m.get(device_url, 78 | text=LM.device(devid=LM.DEVICE_ID, 79 | status='12 °C', 80 | temp='12 °C', 81 | lux='100 lx', 82 | humidity='100 %', 83 | low_battery=True, 84 | no_response=True)) 85 | 86 | # Refesh device and test changes 87 | device.refresh() 88 | 89 | self.assertEqual(device.status, '12 °C') 90 | self.assertTrue(device.battery_low) 91 | self.assertTrue(device.no_response) 92 | self.assertTrue(device.has_temp) 93 | self.assertTrue(device.has_humidity) 94 | self.assertTrue(device.has_lux) 95 | self.assertEqual(device.temp, 12) 96 | self.assertEqual(device.temp_unit, '°C') 97 | self.assertEqual(device.humidity, 100) 98 | self.assertEqual(device.humidity_unit, '%') 99 | self.assertEqual(device.lux, 100) 100 | self.assertEqual(device.lux_unit, 'lux') 101 | 102 | @requests_mock.mock() 103 | def tests_lm_float_units(self, m): 104 | """Tests that sensor/LM devices properties work as expected.""" 105 | # Set up URL's 106 | m.post(CONST.LOGIN_URL, text=LOGIN.post_response_ok()) 107 | m.get(CONST.OAUTH_TOKEN_URL, text=OAUTH_CLAIMS.get_response_ok()) 108 | m.post(CONST.LOGOUT_URL, text=LOGOUT.post_response_ok()) 109 | m.get(CONST.PANEL_URL, 110 | text=PANEL.get_response_ok(mode=CONST.MODE_STANDBY)) 111 | m.get(CONST.DEVICES_URL, 112 | text=LM.device(devid=LM.DEVICE_ID, 113 | status='72.23 °F', 114 | temp='72.23 °F', 115 | lux='14.11 lx', 116 | humidity='34.38 %', 117 | low_battery=False, 118 | no_response=False)) 119 | 120 | # Logout to reset everything 121 | self.abode.logout() 122 | 123 | # Get our power switch 124 | device = self.abode.get_device(LM.DEVICE_ID) 125 | 126 | # Test our device 127 | self.assertIsNotNone(device) 128 | self.assertEqual(device.status, '72.23 °F') 129 | self.assertFalse(device.battery_low) 130 | self.assertFalse(device.no_response) 131 | self.assertTrue(device.has_temp) 132 | self.assertTrue(device.has_humidity) 133 | self.assertTrue(device.has_lux) 134 | self.assertEqual(device.temp, 72.23) 135 | self.assertEqual(device.temp_unit, '°F') 136 | self.assertEqual(device.humidity, 34.38) 137 | self.assertEqual(device.humidity_unit, '%') 138 | self.assertEqual(device.lux, 14.11) 139 | self.assertEqual(device.lux_unit, 'lux') 140 | 141 | @requests_mock.mock() 142 | def tests_lm_temp_only(self, m): 143 | """Tests that sensor/LM devices properties work as expected.""" 144 | # Set up URL's 145 | m.post(CONST.LOGIN_URL, text=LOGIN.post_response_ok()) 146 | m.get(CONST.OAUTH_TOKEN_URL, text=OAUTH_CLAIMS.get_response_ok()) 147 | m.post(CONST.LOGOUT_URL, text=LOGOUT.post_response_ok()) 148 | m.get(CONST.PANEL_URL, 149 | text=PANEL.get_response_ok(mode=CONST.MODE_STANDBY)) 150 | m.get(CONST.DEVICES_URL, 151 | text=LM.device(devid=LM.DEVICE_ID, 152 | status='72 °F', 153 | temp='72 °F', 154 | lux='', 155 | humidity='')) 156 | 157 | # Logout to reset everything 158 | self.abode.logout() 159 | 160 | # Get our power switch 161 | device = self.abode.get_device(LM.DEVICE_ID) 162 | 163 | # Test our device 164 | self.assertIsNotNone(device) 165 | self.assertEqual(device.status, '72 °F') 166 | self.assertTrue(device.has_temp) 167 | self.assertFalse(device.has_humidity) 168 | self.assertFalse(device.has_lux) 169 | self.assertEqual(device.temp, 72) 170 | self.assertEqual(device.temp_unit, '°F') 171 | self.assertIsNone(device.humidity) 172 | self.assertIsNone(device.humidity_unit) 173 | self.assertIsNone(device.lux) 174 | self.assertIsNone(device.lux_unit) 175 | -------------------------------------------------------------------------------- /tests/mock/devices/ipcam.py: -------------------------------------------------------------------------------- 1 | """Mock Abode IP Camera Device.""" 2 | import abodepy.helpers.constants as CONST 3 | 4 | DEVICE_ID = 'ZB:00000305' 5 | CONTROL_URL = 'api/v1/cams/' + DEVICE_ID + '/record' 6 | CONTROL_URL_SNAPSHOT = 'api/v1/cams/' + DEVICE_ID + '/capture' 7 | 8 | 9 | def device(devid=DEVICE_ID, status=CONST.STATUS_ONLINE, 10 | low_battery=False, no_response=False, privacy=1): 11 | """IP camera mock device.""" 12 | return ''' 13 | { 14 | "id":"''' + devid + '''", 15 | "type_tag": "device_type.ipcam", 16 | "type": "IP Cam", 17 | "name": "Living Room Camera", 18 | "area": "1", 19 | "zone": "1", 20 | "sort_order": "", 21 | "is_window": "", 22 | "bypass": "0", 23 | "schar_24hr": "1", 24 | "sresp_24hr": "5", 25 | "sresp_mode_0": "0", 26 | "sresp_entry_0": "0", 27 | "sresp_exit_0": "0", 28 | "group_name": "Streaming Camera", 29 | "group_id": "397974", 30 | "default_group_id": "1", 31 | "sort_id": "10000", 32 | "sresp_mode_1": "0", 33 | "sresp_entry_1": "0", 34 | "sresp_exit_1": "0", 35 | "sresp_mode_2": "0", 36 | "sresp_entry_2": "0", 37 | "sresp_exit_2": "0", 38 | "sresp_mode_3": "0", 39 | "uuid": "123456789", 40 | "sresp_entry_3": "0", 41 | "sresp_exit_3": "0", 42 | "sresp_mode_4": "0", 43 | "sresp_entry_4": "0", 44 | "sresp_exit_4": "0", 45 | "version": "1.0.2.22G_6.8E_homekit_2.0.9_s2 ABODE oz", 46 | "origin": "abode", 47 | "has_subscription": null, 48 | "onboard": "1", 49 | "s2_grnt_keys": "", 50 | "s2_dsk": "", 51 | "s2_propty": "", 52 | "s2_keys_valid": "", 53 | "zwave_secure_protocol": "", 54 | "control_url":"''' + CONTROL_URL + '''", 55 | "deep_link": null, 56 | "status_color": "#5cb85c", 57 | "faults": { 58 | "low_battery":''' + str(int(low_battery)) + ''', 59 | "tempered": 0, 60 | "supervision": 0, 61 | "out_of_order": 0, 62 | "no_response":''' + str(int(no_response)) + ''', 63 | "jammed": 0, 64 | "zwave_fault": 0 65 | }, 66 | "status":"''' + status + '''", 67 | "status_display": "Online", 68 | "statuses": [], 69 | "status_ex": "", 70 | "actions": [ 71 | { 72 | "label": "Capture Video", 73 | "value": "a=1&z=1&req=vid;" 74 | }, 75 | { 76 | "label": "Turn off Live Video", 77 | "value": "a=1&z=1&privacy=on;" 78 | }, 79 | { 80 | "label": "Turn on Live Video", 81 | "value": "a=1&z=1&privacy=off;" 82 | } 83 | ], 84 | "status_icons": [], 85 | "icon": "assets/icons/streaming-camaera-new.svg", 86 | "control_url_snapshot":"''' + CONTROL_URL_SNAPSHOT + '''", 87 | "ptt_supported": true, 88 | "is_new_camera": 1, 89 | "stream_quality": 3, 90 | "camera_mac": "AB:CD:EF:GF:HI", 91 | "privacy":"''' + str(privacy) + '''", 92 | "enable_audio": "1", 93 | "alarm_video": "25", 94 | "pre_alarm_video": "5", 95 | "mic_volume": "75", 96 | "speaker_volume": "75", 97 | "mic_default_volume": 40, 98 | "speaker_default_volume": 46, 99 | "bandwidth": { 100 | "slider_labels": [ 101 | { 102 | "name": "High", 103 | "value": 3 104 | }, 105 | { 106 | "name": "Medium", 107 | "value": 2 108 | }, 109 | { 110 | "name": "Low", 111 | "value": 1 112 | } 113 | ], 114 | "min": 1, 115 | "max": 3, 116 | "step": 1 117 | }, 118 | "volume": { 119 | "min": 0, 120 | "max": 100, 121 | "step": 1 122 | }, 123 | "video_flip": "0", 124 | "hframe": "1080P" 125 | }''' 126 | 127 | 128 | def get_capture_timeout(): 129 | """Mock timeout response.""" 130 | return ''' 131 | { 132 | "code":600, 133 | "message":"Image Capture request has timed out.", 134 | "title":"", 135 | "detail":null 136 | }''' 137 | 138 | 139 | FILE_PATH_ID = 'ZB00000305' 140 | FILE_PATH = 'api/storage/' + FILE_PATH_ID + '/2020-01-26/173238/0.jpg' 141 | 142 | LOCATION_HEADER = 'https://www.google.com/images/branding/googlelogo/' + \ 143 | '1x/googlelogo_color_272x92dp.png' 144 | 145 | 146 | def timeline_event(devid=DEVICE_ID, event_code='5001', file_path=FILE_PATH): 147 | """Camera Timeline Event Mockup.""" 148 | return ''' 149 | { 150 | "mac": "B0:C5:CZ:54:12:9A", 151 | "id": "1171272698", 152 | "xml": null, 153 | "date": "01/26/2020", 154 | "time": "05:32 PM", 155 | "event_utc": "1580088758", 156 | "event_cid": "", 157 | "event_code": "''' + event_code + '''", 158 | "device_id": "''' + devid + '''", 159 | "device_type_id": "69", 160 | "device_type": "IP Cam", 161 | "timeline_ha_device": null, 162 | "d_name": "Living Room Camera", 163 | "delete_by_user": null, 164 | "pin_code_user": " ", 165 | "file_del_at": "", 166 | "nest_has_motion": null, 167 | "nest_has_sound": null, 168 | "nest_has_person": null, 169 | "neaz": null, 170 | "hasFaults": "0", 171 | "file_path":"''' + file_path + '''", 172 | "deep_link": null, 173 | "file_name": "48755_b0c5ca37894b_2020-01-26_173238_0-M2+56431.jpg", 174 | "file_size": "197207", 175 | "file_count": "1", 176 | "file_is_del": "0", 177 | "event_type": "Image Capture", 178 | "severity": "6", 179 | "pos": "l", 180 | "color": "#40bbea", 181 | "is_alarm": "0", 182 | "triggered_by_str": null, 183 | "ha_type": null, 184 | "ha_device_name": null, 185 | "ha_location": null, 186 | "ha_mobile": null, 187 | "ha_cond": null, 188 | "h_location": null, 189 | "ha_trigger": null, 190 | "icon": "assets/email/motion-camera.png", 191 | "user_id": "95244", 192 | "user_name": "Shred", 193 | "mobile_name": "", 194 | "parent_tid": "", 195 | "app_type": "WebApp", 196 | "viewed_by_uid": null, 197 | "verified_by_tid": null, 198 | "la_applied_by": null, 199 | "la_event_type": null, 200 | "la_culprit_mobiles": null, 201 | "la_executed": null, 202 | "la_applied_at": null, 203 | "device_name": "Living Room Camera", 204 | "event_name": "Living Room Camera Image Capture", 205 | "event_by": "by Shred using WebApp", 206 | "file_delete_by": "" 207 | }''' 208 | -------------------------------------------------------------------------------- /tests/test_alarm.py: -------------------------------------------------------------------------------- 1 | """Test the Abode device classes.""" 2 | import unittest 3 | 4 | import requests_mock 5 | 6 | import abodepy 7 | import abodepy.helpers.constants as CONST 8 | 9 | import tests.mock.login as LOGIN 10 | import tests.mock.oauth_claims as OAUTH_CLAIMS 11 | import tests.mock.logout as LOGOUT 12 | import tests.mock.panel as PANEL 13 | import tests.mock.devices as DEVICES 14 | import tests.mock.devices.alarm as ALARM 15 | 16 | 17 | USERNAME = 'foobar' 18 | PASSWORD = 'deadbeef' 19 | 20 | 21 | class TestAlarm(unittest.TestCase): 22 | """Test the generic AbodePy device class.""" 23 | 24 | def setUp(self): 25 | """Set up Abode module.""" 26 | self.abode = abodepy.Abode(username=USERNAME, 27 | password=PASSWORD, 28 | disable_cache=True) 29 | 30 | def tearDown(self): 31 | """Clean up after test.""" 32 | self.abode = None 33 | 34 | @requests_mock.mock() 35 | def tests_abode_alarm_setup(self, m): 36 | """Check that Abode alarm device is set up properly.""" 37 | panel = PANEL.get_response_ok(mode=CONST.MODE_STANDBY) 38 | alarm = ALARM.device(area='1', panel=panel) 39 | 40 | m.post(CONST.LOGIN_URL, text=LOGIN.post_response_ok()) 41 | m.get(CONST.OAUTH_TOKEN_URL, text=OAUTH_CLAIMS.get_response_ok()) 42 | m.get(CONST.DEVICES_URL, text=DEVICES.EMPTY_DEVICE_RESPONSE) 43 | m.get(CONST.PANEL_URL, text=PANEL.get_response_ok()) 44 | 45 | alarm_device = self.abode.get_alarm() 46 | 47 | self.assertIsNotNone(alarm_device) 48 | # pylint: disable=W0212 49 | self.assertEqual(alarm_device._json_state, alarm) 50 | 51 | @requests_mock.mock() 52 | def tests_alarm_device_properties(self, m): 53 | """Check that the abode device properties are working.""" 54 | # Set up URL's 55 | m.post(CONST.LOGIN_URL, text=LOGIN.post_response_ok()) 56 | m.get(CONST.OAUTH_TOKEN_URL, text=OAUTH_CLAIMS.get_response_ok()) 57 | m.post(CONST.LOGOUT_URL, text=LOGOUT.post_response_ok()) 58 | m.get(CONST.PANEL_URL, text=PANEL.get_response_ok( 59 | mode=CONST.MODE_STANDBY, battery=True, is_cellular=True, 60 | mac='01:AA:b3:C4:d5:66')) 61 | m.get(CONST.DEVICES_URL, text=DEVICES.EMPTY_DEVICE_RESPONSE) 62 | 63 | # Logout to reset everything 64 | self.abode.logout() 65 | 66 | # Get alarm and test 67 | alarm = self.abode.get_alarm() 68 | self.assertIsNotNone(alarm) 69 | self.assertEqual(alarm.mode, CONST.MODE_STANDBY) 70 | self.assertEqual(alarm.status, CONST.MODE_STANDBY) 71 | self.assertTrue(alarm.battery) 72 | self.assertTrue(alarm.is_cellular) 73 | self.assertFalse(alarm.is_on) 74 | self.assertEqual(alarm.device_uuid, '01aab3c4d566') 75 | self.assertEqual(alarm.mac_address, '01:AA:b3:C4:d5:66') 76 | 77 | # Change alarm properties and state to away and test 78 | m.get(CONST.PANEL_URL, text=PANEL.get_response_ok( 79 | mode=CONST.MODE_AWAY, battery=False, is_cellular=False)) 80 | 81 | # Refresh alarm and test 82 | alarm.refresh() 83 | 84 | self.assertEqual(alarm.mode, CONST.MODE_AWAY) 85 | self.assertEqual(alarm.status, CONST.MODE_AWAY) 86 | self.assertFalse(alarm.battery) 87 | self.assertFalse(alarm.is_cellular) 88 | self.assertTrue(alarm.is_on) 89 | 90 | # Change alarm state to final on state and test 91 | m.get(CONST.PANEL_URL, 92 | text=PANEL.get_response_ok(mode=CONST.MODE_HOME)) 93 | 94 | # Refresh alarm and test 95 | alarm.refresh() 96 | self.assertEqual(alarm.mode, CONST.MODE_HOME) 97 | self.assertEqual(alarm.status, CONST.MODE_HOME) 98 | self.assertTrue(alarm.is_on) 99 | 100 | @requests_mock.mock() 101 | def tests_alarm_device_mode_changes(self, m): 102 | """Test that the abode alarm can change/report modes.""" 103 | # Set up URL's 104 | m.post(CONST.LOGIN_URL, text=LOGIN.post_response_ok()) 105 | m.get(CONST.OAUTH_TOKEN_URL, text=OAUTH_CLAIMS.get_response_ok()) 106 | m.post(CONST.LOGOUT_URL, text=LOGOUT.post_response_ok()) 107 | m.get(CONST.PANEL_URL, 108 | text=PANEL.get_response_ok(mode=CONST.MODE_STANDBY)) 109 | m.get(CONST.DEVICES_URL, text=DEVICES.EMPTY_DEVICE_RESPONSE) 110 | 111 | # Logout to reset everything 112 | self.abode.logout() 113 | 114 | # Assert that after login we have our alarm device with standby mode 115 | alarm = self.abode.get_alarm() 116 | 117 | self.assertIsNotNone(alarm) 118 | self.assertEqual(alarm.status, CONST.MODE_STANDBY) 119 | 120 | # Set mode URLs 121 | m.put(CONST.get_panel_mode_url('1', CONST.MODE_STANDBY), 122 | text=PANEL.put_response_ok(mode=CONST.MODE_STANDBY)) 123 | m.put(CONST.get_panel_mode_url('1', CONST.MODE_AWAY), 124 | text=PANEL.put_response_ok(mode=CONST.MODE_AWAY)) 125 | m.put(CONST.get_panel_mode_url('1', CONST.MODE_HOME), 126 | text=PANEL.put_response_ok(mode=CONST.MODE_HOME)) 127 | 128 | # Set and test text based mode changes 129 | self.assertTrue(alarm.set_mode(CONST.MODE_HOME)) 130 | self.assertEqual(alarm.mode, CONST.MODE_HOME) 131 | self.assertFalse(alarm.is_standby) 132 | self.assertTrue(alarm.is_home) 133 | self.assertFalse(alarm.is_away) 134 | 135 | self.assertTrue(alarm.set_mode(CONST.MODE_AWAY)) 136 | self.assertEqual(alarm.mode, CONST.MODE_AWAY) 137 | self.assertFalse(alarm.is_standby) 138 | self.assertFalse(alarm.is_home) 139 | self.assertTrue(alarm.is_away) 140 | 141 | self.assertTrue(alarm.set_mode(CONST.MODE_STANDBY)) 142 | self.assertEqual(alarm.mode, CONST.MODE_STANDBY) 143 | self.assertTrue(alarm.is_standby) 144 | self.assertFalse(alarm.is_home) 145 | self.assertFalse(alarm.is_away) 146 | 147 | # Set and test direct mode changes 148 | self.assertTrue(alarm.set_home()) 149 | self.assertEqual(alarm.mode, CONST.MODE_HOME) 150 | self.assertFalse(alarm.is_standby) 151 | self.assertTrue(alarm.is_home) 152 | self.assertFalse(alarm.is_away) 153 | 154 | self.assertTrue(alarm.set_away()) 155 | self.assertEqual(alarm.mode, CONST.MODE_AWAY) 156 | self.assertFalse(alarm.is_standby) 157 | self.assertFalse(alarm.is_home) 158 | self.assertTrue(alarm.is_away) 159 | 160 | self.assertTrue(alarm.set_standby()) 161 | self.assertEqual(alarm.mode, CONST.MODE_STANDBY) 162 | self.assertTrue(alarm.is_standby) 163 | self.assertFalse(alarm.is_home) 164 | self.assertFalse(alarm.is_away) 165 | 166 | # Set and test default mode changes 167 | self.assertTrue(alarm.switch_off()) 168 | self.assertEqual(alarm.mode, CONST.MODE_STANDBY) 169 | self.assertTrue(alarm.is_standby) 170 | self.assertFalse(alarm.is_home) 171 | self.assertFalse(alarm.is_away) 172 | 173 | self.abode.set_default_mode(CONST.MODE_HOME) 174 | self.assertTrue(alarm.switch_on()) 175 | self.assertEqual(alarm.mode, CONST.MODE_HOME) 176 | self.assertFalse(alarm.is_standby) 177 | self.assertTrue(alarm.is_home) 178 | self.assertFalse(alarm.is_away) 179 | 180 | self.assertTrue(alarm.switch_off()) 181 | self.assertEqual(alarm.mode, CONST.MODE_STANDBY) 182 | self.assertTrue(alarm.is_standby) 183 | self.assertFalse(alarm.is_home) 184 | self.assertFalse(alarm.is_away) 185 | 186 | self.abode.set_default_mode(CONST.MODE_AWAY) 187 | self.assertTrue(alarm.switch_on()) 188 | self.assertEqual(alarm.mode, CONST.MODE_AWAY) 189 | self.assertFalse(alarm.is_standby) 190 | self.assertFalse(alarm.is_home) 191 | self.assertTrue(alarm.is_away) 192 | 193 | # Test that no mode throws exception 194 | with self.assertRaises(abodepy.AbodeException): 195 | alarm.set_mode(mode=None) 196 | 197 | # Test that an invalid mode throws exception 198 | with self.assertRaises(abodepy.AbodeException): 199 | alarm.set_mode('chestnuts') 200 | 201 | # Test that an invalid mode change response throws exception 202 | m.put(CONST.get_panel_mode_url('1', CONST.MODE_HOME), 203 | text=PANEL.put_response_ok(mode=CONST.MODE_AWAY)) 204 | 205 | with self.assertRaises(abodepy.AbodeException): 206 | alarm.set_mode(CONST.MODE_HOME) 207 | 208 | # Test that an invalid area in mode change response throws exception 209 | m.put(CONST.get_panel_mode_url('1', CONST.MODE_HOME), 210 | text=PANEL.put_response_ok(area='2', mode=CONST.MODE_HOME)) 211 | 212 | with self.assertRaises(abodepy.AbodeException): 213 | alarm.set_mode(CONST.MODE_HOME) 214 | -------------------------------------------------------------------------------- /abodepy/socketio.py: -------------------------------------------------------------------------------- 1 | """Small SocketIO client via Websockets.""" 2 | import collections 3 | import json 4 | import logging 5 | import threading 6 | 7 | from datetime import datetime 8 | from random import random 9 | 10 | from lomond import WebSocket 11 | from lomond import events 12 | from lomond.persist import persist 13 | from lomond.errors import WebSocketError 14 | 15 | from abodepy.exceptions import SocketIOException 16 | import abodepy.helpers.errors as ERRORS 17 | 18 | STARTED = "started" 19 | STOPPED = "stopped" 20 | CONNECTED = "connected" 21 | DISCONNECTED = "disconnected" 22 | PING = "ping" 23 | PONG = "pong" 24 | POLL = "poll" 25 | EVENT = "event" 26 | ERROR = "error" 27 | 28 | PACKET_OPEN = "0" 29 | PACKET_CLOSE = "1" 30 | PACKET_PING = "2" 31 | PACKET_PONG = "3" 32 | PACKET_MESSAGE = "4" 33 | 34 | MESSAGE_CONNECT = "0" 35 | MESSAGE_DISCONNECT = "1" 36 | MESSAGE_EVENT = "2" 37 | MESSAGE_ERROR = "4" 38 | 39 | PING_INTERVAL = "pingInterval" 40 | PING_TIMEOUT = "pingTimeout" 41 | 42 | COOKIE_HEADER = str.encode("Cookie") 43 | ORIGIN_HEADER = str.encode("Origin") 44 | 45 | URL_PARAMS = "?EIO=3&transport=websocket" 46 | 47 | _LOGGER = logging.getLogger(__name__) 48 | 49 | 50 | class SocketIO(): 51 | """Class for using websockets to talk to a SocketIO server.""" 52 | 53 | def __init__(self, url, cookie=None, origin=None): 54 | """Init SocketIO class.""" 55 | self._url = url + URL_PARAMS 56 | 57 | if origin: 58 | self._origin = origin.encode() 59 | else: 60 | self._origin = None 61 | 62 | if cookie: 63 | self._cookie = cookie.encode() 64 | else: 65 | self._cookie = None 66 | 67 | self._thread = None 68 | self._websocket = None 69 | self._exit_event = None 70 | self._running = False 71 | 72 | self._websocket_connected = False 73 | self._engineio_connected = False 74 | self._socketio_connected = False 75 | 76 | self._ping_interval_ms = 25000 77 | self._ping_timeout_ms = 60000 78 | 79 | self._last_ping_time = datetime.min 80 | self._last_packet_time = datetime.min 81 | 82 | self._callbacks = collections.defaultdict(list) 83 | 84 | def set_origin(self, origin=None): 85 | """Set the Origin header.""" 86 | if origin: 87 | self._origin = origin.encode() 88 | else: 89 | self._origin = None 90 | 91 | def set_cookie(self, cookie=None): 92 | """Set the Cookie header.""" 93 | if cookie: 94 | self._cookie = cookie.encode() 95 | else: 96 | self._cookie = None 97 | 98 | # pylint: disable=C0103 99 | def on(self, event_name, callback): 100 | """Register callback for a SocketIO event.""" 101 | if not event_name: 102 | return False 103 | 104 | _LOGGER.debug("Adding callback for event name: %s", event_name) 105 | 106 | self._callbacks[event_name].append((callback)) 107 | 108 | return True 109 | 110 | def start(self): 111 | """Start a thread to handle SocketIO notifications.""" 112 | if not self._thread: 113 | _LOGGER.info("Starting SocketIO thread...") 114 | 115 | self._thread = threading.Thread(target=self._run_socketio_thread, 116 | name='SocketIOThread') 117 | self._thread.deamon = True 118 | self._thread.start() 119 | 120 | def stop(self): 121 | """Tell the SocketIO thread to terminate.""" 122 | if self._thread: 123 | _LOGGER.info("Stopping SocketIO thread...") 124 | 125 | # pylint: disable=W0212 126 | self._running = False 127 | 128 | if self._exit_event: 129 | self._exit_event.set() 130 | 131 | self._thread.join() 132 | 133 | def _run_socketio_thread(self): 134 | self._running = True 135 | 136 | # Back off for Error restarting 137 | min_wait = 5 138 | max_wait = 30 139 | 140 | retries = 0 141 | 142 | random_wait = max_wait - min_wait 143 | 144 | while self._running is True: 145 | _LOGGER.info( 146 | "Attempting to connect to SocketIO server...") 147 | 148 | try: 149 | retries += 1 150 | 151 | self._handle_event(STARTED, None) 152 | 153 | self._websocket = WebSocket(self._url) 154 | self._exit_event = threading.Event() 155 | 156 | if self._cookie: 157 | self._websocket.add_header(COOKIE_HEADER, self._cookie) 158 | 159 | if self._origin: 160 | self._websocket.add_header(ORIGIN_HEADER, self._origin) 161 | 162 | for event in persist(self._websocket, ping_rate=0, 163 | poll=5.0, exit_event=self._exit_event): 164 | if isinstance(event, events.Connected): 165 | retries = 0 166 | self._on_websocket_connected(event) 167 | elif isinstance(event, events.Disconnected): 168 | self._on_websocket_disconnected(event) 169 | elif isinstance(event, events.Text): 170 | self._on_websocket_text(event) 171 | elif isinstance(event, events.Poll): 172 | self._on_websocket_poll(event) 173 | elif isinstance(event, events.BackOff): 174 | self._on_websocket_backoff(event) 175 | 176 | if self._running is False: 177 | self._websocket.close() 178 | 179 | except SocketIOException as exc: 180 | _LOGGER.warning("SocketIO Error: %s", exc.details) 181 | 182 | except WebSocketError as exc: 183 | _LOGGER.warning("Websocket Error: %s", exc) 184 | 185 | if self._running: 186 | wait_for = min_wait + random() * min(random_wait, 2 ** retries) 187 | 188 | _LOGGER.info("Waiting %f seconds before reconnecting...", 189 | wait_for) 190 | 191 | if self._exit_event.wait(wait_for): 192 | break 193 | 194 | self._handle_event(STOPPED, None) 195 | 196 | def _on_websocket_connected(self, _event): 197 | self._websocket_connected = True 198 | 199 | _LOGGER.info("Websocket Connected") 200 | 201 | self._handle_event(CONNECTED, None) 202 | 203 | def _on_websocket_disconnected(self, _event): 204 | self._websocket_connected = False 205 | self._engineio_connected = False 206 | self._socketio_connected = False 207 | 208 | _LOGGER.info("Websocket Disconnected") 209 | 210 | self._handle_event(DISCONNECTED, None) 211 | 212 | def _on_websocket_poll(self, _event): 213 | last_packet_delta = datetime.now() - self._last_packet_time 214 | last_packet_ms = int(last_packet_delta.total_seconds() * 1000) 215 | 216 | if self._engineio_connected and last_packet_ms > self._ping_timeout_ms: 217 | _LOGGER.warning("SocketIO Server Ping Timeout") 218 | self._websocket.close() 219 | return 220 | 221 | last_ping_delta = datetime.now() - self._last_ping_time 222 | last_ping_ms = int(last_ping_delta.total_seconds() * 1000) 223 | 224 | if self._engineio_connected and last_ping_ms >= self._ping_interval_ms: 225 | self._websocket.send_text(PACKET_PING) 226 | self._last_ping_time = datetime.now() 227 | _LOGGER.debug("Client Ping") 228 | self._handle_event(PING, None) 229 | 230 | self._handle_event(POLL, None) 231 | 232 | def _on_websocket_text(self, _event): 233 | self._last_packet_time = datetime.now() 234 | 235 | packet_type = _event.text[:1] 236 | packet_data = _event.text[1:] 237 | 238 | if packet_type == PACKET_OPEN: 239 | self._on_engineio_opened(packet_data) 240 | elif packet_type == PACKET_CLOSE: 241 | self._on_engineio_closed() 242 | elif packet_type == PACKET_PONG: 243 | self._on_engineio_pong() 244 | elif packet_type == PACKET_MESSAGE: 245 | self._on_engineio_message(packet_data) 246 | else: 247 | _LOGGER.debug("Ignoring EngineIO packet: %s", _event.text) 248 | 249 | # pylint: disable=R0201 250 | def _on_websocket_backoff(self, _event): 251 | return 252 | 253 | def _on_engineio_opened(self, _packet_data): 254 | json_data = json.loads(_packet_data) 255 | 256 | if json_data and json_data[PING_INTERVAL]: 257 | ping_interval_ms = json_data[PING_INTERVAL] 258 | _LOGGER.debug("Set ping interval to: %d", ping_interval_ms) 259 | 260 | if json_data and json_data[PING_TIMEOUT]: 261 | ping_timeout_ms = json_data[PING_TIMEOUT] 262 | _LOGGER.debug("Set ping timeout to: %d", ping_timeout_ms) 263 | 264 | self._engineio_connected = True 265 | 266 | _LOGGER.debug("EngineIO Connected") 267 | 268 | def _on_engineio_closed(self): 269 | self._engineio_connected = False 270 | 271 | _LOGGER.debug("EngineIO Disconnected") 272 | 273 | self._websocket.close() 274 | 275 | def _on_engineio_pong(self): 276 | _LOGGER.debug("Server Pong") 277 | self._handle_event(PONG, None) 278 | 279 | def _on_engineio_message(self, _packet_data): 280 | message_type = _packet_data[:1] 281 | message_data = _packet_data[1:] 282 | 283 | if message_type == MESSAGE_CONNECT: 284 | self._on_socketio_connected() 285 | elif message_type == MESSAGE_DISCONNECT: 286 | self._on_socketio_disconnected() 287 | elif message_type == MESSAGE_ERROR: 288 | self._on_socketio_error(message_data) 289 | elif message_type == MESSAGE_EVENT: 290 | self._on_socketio_event(message_data) 291 | else: 292 | _LOGGER.debug("Ignoring SocketIO message: %s", _packet_data) 293 | 294 | def _on_socketio_connected(self): 295 | self._socketio_connected = True 296 | 297 | _LOGGER.debug("SocketIO Connected") 298 | 299 | def _on_socketio_disconnected(self): 300 | self._socketio_connected = False 301 | 302 | _LOGGER.debug("SocketIO Disconnected") 303 | 304 | self._websocket.close() 305 | 306 | def _on_socketio_error(self, _message_data): 307 | self._handle_event(ERROR, _message_data) 308 | 309 | raise SocketIOException(ERRORS.SOCKETIO_ERROR, details=_message_data) 310 | 311 | def _on_socketio_event(self, _message_data): 312 | l_bracket = _message_data.find("[") 313 | r_bracket = _message_data.rfind("]") 314 | 315 | if l_bracket == -1 or r_bracket == -1: 316 | _LOGGER.warning("Unable to find event [data]: %s", _message_data) 317 | return 318 | 319 | json_str = _message_data[l_bracket:r_bracket + 1] 320 | json_data = json.loads(json_str) 321 | 322 | self._handle_event(EVENT, _message_data) 323 | self._handle_event(json_data[0], json_data[1:]) 324 | 325 | def _handle_event(self, event_name, event_data): 326 | for callback in self._callbacks.get(event_name, ()): 327 | try: 328 | if event_data: 329 | callback(event_data) 330 | else: 331 | callback() 332 | # pylint: disable=W0703 333 | except Exception as exc: 334 | _LOGGER.exception( 335 | "Captured exception during SocketIO event callback: %s", 336 | exc) 337 | -------------------------------------------------------------------------------- /tests/test_hue.py: -------------------------------------------------------------------------------- 1 | """Test the Abode device classes.""" 2 | import unittest 3 | 4 | import requests_mock 5 | 6 | import abodepy 7 | import abodepy.helpers.constants as CONST 8 | 9 | import tests.mock.login as LOGIN 10 | import tests.mock.oauth_claims as OAUTH_CLAIMS 11 | import tests.mock.logout as LOGOUT 12 | import tests.mock.panel as PANEL 13 | import tests.mock.devices as DEVICES 14 | import tests.mock.devices.hue as HUE 15 | 16 | 17 | USERNAME = 'foobar' 18 | PASSWORD = 'deadbeef' 19 | 20 | 21 | class TestHue(unittest.TestCase): 22 | """Test the AbodePy light device with Hue.""" 23 | 24 | def setUp(self): 25 | """Set up Abode module.""" 26 | self.abode = abodepy.Abode(username=USERNAME, 27 | password=PASSWORD, 28 | disable_cache=True) 29 | 30 | def tearDown(self): 31 | """Clean up after test.""" 32 | self.abode = None 33 | 34 | @requests_mock.mock() 35 | def tests_hue_device_properties(self, m): 36 | """Tests that hue light devices properties work as expected.""" 37 | # Set up URL's 38 | m.post(CONST.LOGIN_URL, text=LOGIN.post_response_ok()) 39 | m.get(CONST.OAUTH_TOKEN_URL, text=OAUTH_CLAIMS.get_response_ok()) 40 | m.post(CONST.LOGOUT_URL, text=LOGOUT.post_response_ok()) 41 | m.get(CONST.PANEL_URL, 42 | text=PANEL.get_response_ok(mode=CONST.MODE_STANDBY)) 43 | m.get(CONST.DEVICES_URL, 44 | text=HUE.device(devid=HUE.DEVICE_ID, 45 | status=CONST.STATUS_OFF, 46 | level=0, 47 | saturation=57, 48 | hue=60, 49 | color_temp=6536, 50 | color_mode=CONST.COLOR_MODE_ON, 51 | low_battery=False, 52 | no_response=False)) 53 | 54 | # Logout to reset everything 55 | self.abode.logout() 56 | 57 | # Get our dimmer 58 | device = self.abode.get_device(HUE.DEVICE_ID) 59 | 60 | # Test our device 61 | self.assertIsNotNone(device) 62 | self.assertEqual(device.status, CONST.STATUS_OFF) 63 | self.assertEqual(device.brightness, "0") 64 | self.assertEqual(device.color, (60, 57)) # (hue, saturation) 65 | self.assertEqual(device.color_temp, 6536) 66 | self.assertTrue(device.has_brightness) 67 | self.assertTrue(device.is_dimmable) 68 | self.assertTrue(device.has_color) 69 | self.assertTrue(device.is_color_capable) 70 | self.assertFalse(device.battery_low) 71 | self.assertFalse(device.no_response) 72 | self.assertFalse(device.is_on) 73 | 74 | # Set up our direct device get url 75 | device_url = str.replace(CONST.DEVICE_URL, 76 | '$DEVID$', HUE.DEVICE_ID) 77 | 78 | # Change device properties 79 | m.get(device_url, 80 | text=HUE.device(devid=HUE.DEVICE_ID, 81 | status=CONST.STATUS_ON, 82 | level=45, 83 | saturation=22, 84 | hue=104, 85 | color_temp=4000, 86 | color_mode=CONST.COLOR_MODE_OFF, 87 | low_battery=True, 88 | no_response=True)) 89 | 90 | # Refesh device and test changes 91 | device.refresh() 92 | 93 | self.assertEqual(device.status, CONST.STATUS_ON) 94 | self.assertEqual(device.color, (104, 22)) # (hue, saturation) 95 | self.assertEqual(device.color_temp, 4000) 96 | self.assertTrue(device.has_brightness) 97 | self.assertTrue(device.is_dimmable) 98 | self.assertFalse(device.has_color) 99 | self.assertTrue(device.is_color_capable) 100 | self.assertTrue(device.battery_low) 101 | self.assertTrue(device.no_response) 102 | self.assertTrue(device.is_on) 103 | 104 | @requests_mock.mock() 105 | def tests_hue_status_changes(self, m): 106 | """Tests that hue device changes work as expected.""" 107 | # Set up URL's 108 | m.post(CONST.LOGIN_URL, text=LOGIN.post_response_ok()) 109 | m.get(CONST.OAUTH_TOKEN_URL, text=OAUTH_CLAIMS.get_response_ok()) 110 | m.post(CONST.LOGOUT_URL, text=LOGOUT.post_response_ok()) 111 | m.get(CONST.PANEL_URL, 112 | text=PANEL.get_response_ok(mode=CONST.MODE_STANDBY)) 113 | m.get(CONST.DEVICES_URL, 114 | text=HUE.device(devid=HUE.DEVICE_ID, 115 | status=CONST.STATUS_OFF, 116 | level=0, 117 | saturation=57, 118 | hue=60, 119 | color_temp=6536, 120 | color_mode=CONST.COLOR_MODE_ON, 121 | low_battery=False, 122 | no_response=False)) 123 | 124 | # Logout to reset everything 125 | self.abode.logout() 126 | 127 | # Get our hue device 128 | device = self.abode.get_device(HUE.DEVICE_ID) 129 | 130 | # Test that we have our device 131 | self.assertIsNotNone(device) 132 | self.assertEqual(device.status, CONST.STATUS_OFF) 133 | self.assertFalse(device.is_on) 134 | 135 | # Set up control url response 136 | control_url = CONST.BASE_URL + HUE.CONTROL_URL 137 | m.put(control_url, 138 | text=DEVICES.status_put_response_ok( 139 | devid=HUE.DEVICE_ID, 140 | status=CONST.STATUS_ON_INT)) 141 | 142 | # Change the mode to "on" 143 | self.assertTrue(device.switch_on()) 144 | self.assertEqual(device.status, CONST.STATUS_ON) 145 | self.assertTrue(device.is_on) 146 | 147 | # Change response 148 | m.put(control_url, 149 | text=DEVICES.status_put_response_ok( 150 | devid=HUE.DEVICE_ID, 151 | status=CONST.STATUS_OFF_INT)) 152 | 153 | # Change the mode to "off" 154 | self.assertTrue(device.switch_off()) 155 | self.assertEqual(device.status, CONST.STATUS_OFF) 156 | self.assertFalse(device.is_on) 157 | 158 | # Test that an invalid status response throws exception 159 | m.put(control_url, 160 | text=DEVICES.status_put_response_ok( 161 | devid=HUE.DEVICE_ID, 162 | status=CONST.STATUS_OFF_INT)) 163 | 164 | with self.assertRaises(abodepy.AbodeException): 165 | device.switch_on() 166 | 167 | @requests_mock.mock() 168 | def tests_hue_color_temp_changes(self, m): 169 | """Tests that hue device color temp changes work as expected.""" 170 | # Set up URL's 171 | m.post(CONST.LOGIN_URL, text=LOGIN.post_response_ok()) 172 | m.get(CONST.OAUTH_TOKEN_URL, text=OAUTH_CLAIMS.get_response_ok()) 173 | m.post(CONST.LOGOUT_URL, text=LOGOUT.post_response_ok()) 174 | m.get(CONST.PANEL_URL, 175 | text=PANEL.get_response_ok(mode=CONST.MODE_STANDBY)) 176 | m.get(CONST.DEVICES_URL, 177 | text=HUE.device(devid=HUE.DEVICE_ID, 178 | status=CONST.STATUS_OFF, 179 | level=0, 180 | saturation=57, 181 | hue=60, 182 | color_temp=6536, 183 | color_mode=CONST.COLOR_MODE_ON, 184 | low_battery=False, 185 | no_response=False)) 186 | 187 | # Logout to reset everything 188 | self.abode.logout() 189 | 190 | # Get our hue device 191 | device = self.abode.get_device(HUE.DEVICE_ID) 192 | 193 | # Test that we have our device 194 | self.assertIsNotNone(device) 195 | self.assertEqual(device.status, CONST.STATUS_OFF) 196 | self.assertFalse(device.is_on) 197 | self.assertEqual(device.color_temp, 6536) 198 | 199 | # Set up integrations url response 200 | m.post(HUE.INTEGRATIONS_URL, 201 | text=HUE.color_temp_post_response_ok( 202 | devid=HUE.DEVICE_ID, 203 | color_temp=5554)) 204 | 205 | # Change the color temp 206 | self.assertTrue(device.set_color_temp(5554)) 207 | self.assertEqual(device.color_temp, 5554) 208 | 209 | # Change response 210 | m.post(HUE.INTEGRATIONS_URL, 211 | text=HUE.color_temp_post_response_ok( 212 | devid=HUE.DEVICE_ID, 213 | color_temp=4434)) 214 | 215 | # Change the color to something that mismatches 216 | self.assertTrue(device.set_color_temp(4436)) 217 | 218 | # Assert that the color is set to the response color 219 | self.assertEqual(device.color_temp, 4434) 220 | 221 | # Test that an invalid ID in response throws exception 222 | m.post(HUE.INTEGRATIONS_URL, 223 | text=HUE.color_temp_post_response_ok( 224 | devid=(HUE.DEVICE_ID + "23"), 225 | color_temp=4434)) 226 | 227 | with self.assertRaises(abodepy.AbodeException): 228 | device.set_color_temp(4434) 229 | 230 | @requests_mock.mock() 231 | def tests_hue_color_changes(self, m): 232 | """Tests that hue device color changes work as expected.""" 233 | # Set up URL's 234 | m.post(CONST.LOGIN_URL, text=LOGIN.post_response_ok()) 235 | m.get(CONST.OAUTH_TOKEN_URL, text=OAUTH_CLAIMS.get_response_ok()) 236 | m.post(CONST.LOGOUT_URL, text=LOGOUT.post_response_ok()) 237 | m.get(CONST.PANEL_URL, 238 | text=PANEL.get_response_ok(mode=CONST.MODE_STANDBY)) 239 | m.get(CONST.DEVICES_URL, 240 | text=HUE.device(devid=HUE.DEVICE_ID, 241 | status=CONST.STATUS_OFF, 242 | level=0, 243 | saturation=57, 244 | hue=60, 245 | color_temp=6536, 246 | color_mode=CONST.COLOR_MODE_ON, 247 | low_battery=False, 248 | no_response=False)) 249 | 250 | # Logout to reset everything 251 | self.abode.logout() 252 | 253 | # Get our hue device 254 | device = self.abode.get_device(HUE.DEVICE_ID) 255 | 256 | # Test that we have our device 257 | self.assertIsNotNone(device) 258 | self.assertEqual(device.status, CONST.STATUS_OFF) 259 | self.assertFalse(device.is_on) 260 | self.assertEqual(device.color, (60, 57)) # (hue, saturation) 261 | 262 | # Set up integrations url response 263 | m.post(HUE.INTEGRATIONS_URL, 264 | text=HUE.color_post_response_ok( 265 | devid=HUE.DEVICE_ID, 266 | hue=70, 267 | saturation=80)) 268 | 269 | # Change the color temp 270 | self.assertTrue(device.set_color((70, 80))) 271 | self.assertEqual(device.color, (70, 80)) # (hue, saturation) 272 | 273 | # Change response 274 | m.post(HUE.INTEGRATIONS_URL, 275 | text=HUE.color_post_response_ok( 276 | devid=HUE.DEVICE_ID, 277 | hue=55, 278 | saturation=85)) 279 | 280 | # Change the color to something that mismatches 281 | self.assertTrue(device.set_color((44, 44))) 282 | 283 | # Assert that the color is set to the response color 284 | self.assertEqual(device.color, (55, 85)) # (hue, saturation) 285 | 286 | # Test that an invalid ID in response throws exception 287 | m.post(HUE.INTEGRATIONS_URL, 288 | text=HUE.color_post_response_ok( 289 | devid=(HUE.DEVICE_ID + "23"), 290 | hue=55, 291 | saturation=85)) 292 | 293 | with self.assertRaises(abodepy.AbodeException): 294 | device.set_color((44, 44)) 295 | -------------------------------------------------------------------------------- /abodepy/event_controller.py: -------------------------------------------------------------------------------- 1 | """Abode cloud push events.""" 2 | import collections 3 | import logging 4 | 5 | from abodepy.devices import AbodeDevice 6 | from abodepy.exceptions import AbodeException 7 | import abodepy.helpers.constants as CONST 8 | import abodepy.helpers.errors as ERROR 9 | import abodepy.helpers.timeline as TIMELINE 10 | import abodepy.socketio as sio 11 | 12 | _LOGGER = logging.getLogger(__name__) 13 | 14 | 15 | class AbodeEventController(): 16 | """Class for subscribing to abode events.""" 17 | 18 | def __init__(self, abode, url=CONST.SOCKETIO_URL): 19 | """Init event subscription class.""" 20 | self._abode = abode 21 | self._thread = None 22 | self._running = False 23 | self._connected = False 24 | 25 | # Setup callback dicts 26 | self._connection_status_callbacks = collections.defaultdict(list) 27 | self._device_callbacks = collections.defaultdict(list) 28 | self._event_callbacks = collections.defaultdict(list) 29 | self._timeline_callbacks = collections.defaultdict(list) 30 | 31 | # Setup SocketIO 32 | self._socketio = sio.SocketIO(url=url, 33 | origin=CONST.BASE_URL) 34 | 35 | # Setup SocketIO Callbacks 36 | self._socketio.on(sio.STARTED, self._on_socket_started) 37 | self._socketio.on(sio.CONNECTED, self._on_socket_connected) 38 | self._socketio.on(sio.DISCONNECTED, self._on_socket_disconnected) 39 | self._socketio.on(CONST.DEVICE_UPDATE_EVENT, self._on_device_update) 40 | self._socketio.on(CONST.GATEWAY_MODE_EVENT, self._on_mode_change) 41 | self._socketio.on(CONST.TIMELINE_EVENT, self._on_timeline_update) 42 | self._socketio.on(CONST.AUTOMATION_EVENT, self._on_automation_update) 43 | 44 | def start(self): 45 | """Start a thread to handle Abode SocketIO notifications.""" 46 | self._socketio.start() 47 | 48 | def stop(self): 49 | """Tell the subscription thread to terminate - will block.""" 50 | self._socketio.stop() 51 | 52 | def add_connection_status_callback(self, unique_id, callback): 53 | """Register callback for Abode server connection status.""" 54 | if not unique_id: 55 | return False 56 | 57 | _LOGGER.debug( 58 | "Subscribing to Abode connection updates for: %s", unique_id) 59 | 60 | self._connection_status_callbacks[unique_id].append((callback)) 61 | 62 | return True 63 | 64 | def remove_connection_status_callback(self, unique_id): 65 | """Unregister connection status callbacks.""" 66 | if not unique_id: 67 | return False 68 | 69 | _LOGGER.debug( 70 | "Unsubscribing from Abode connection updates for : %s", unique_id) 71 | 72 | self._connection_status_callbacks[unique_id].clear() 73 | 74 | return True 75 | 76 | def add_device_callback(self, devices, callback): 77 | """Register a device callback.""" 78 | if not devices: 79 | return False 80 | 81 | if not isinstance(devices, (tuple, list)): 82 | devices = [devices] 83 | 84 | for device in devices: 85 | # Device may be a device_id 86 | device_id = device 87 | 88 | # If they gave us an actual device, get that devices ID 89 | if isinstance(device, AbodeDevice): 90 | device_id = device.device_id 91 | 92 | # Validate the device is valid 93 | if not self._abode.get_device(device_id): 94 | raise AbodeException((ERROR.EVENT_DEVICE_INVALID)) 95 | 96 | _LOGGER.debug( 97 | "Subscribing to updates for device_id: %s", device_id) 98 | 99 | self._device_callbacks[device_id].append((callback)) 100 | 101 | return True 102 | 103 | def remove_all_device_callbacks(self, devices): 104 | """Unregister all callbacks for a device.""" 105 | if not devices: 106 | return False 107 | 108 | if not isinstance(devices, (tuple, list)): 109 | devices = [devices] 110 | 111 | for device in devices: 112 | device_id = device 113 | 114 | if isinstance(device, AbodeDevice): 115 | device_id = device.device_id 116 | 117 | if not self._abode.get_device(device_id): 118 | raise AbodeException((ERROR.EVENT_DEVICE_INVALID)) 119 | 120 | if device_id not in self._device_callbacks: 121 | return False 122 | 123 | _LOGGER.debug( 124 | "Unsubscribing from all updates for device_id: %s", device_id) 125 | 126 | self._device_callbacks[device_id].clear() 127 | 128 | return True 129 | 130 | def add_event_callback(self, event_groups, callback): 131 | """Register callback for a group of timeline events.""" 132 | if not event_groups: 133 | return False 134 | 135 | if not isinstance(event_groups, (tuple, list)): 136 | event_groups = [event_groups] 137 | 138 | for event_group in event_groups: 139 | if event_group not in TIMELINE.ALL_EVENT_GROUPS: 140 | raise AbodeException(ERROR.EVENT_GROUP_INVALID, 141 | TIMELINE.ALL_EVENT_GROUPS) 142 | 143 | _LOGGER.debug("Subscribing to event group: %s", event_group) 144 | 145 | self._event_callbacks[event_group].append((callback)) 146 | 147 | return True 148 | 149 | def add_timeline_callback(self, timeline_events, callback): 150 | """Register a callback for a specific timeline event.""" 151 | if not timeline_events: 152 | return False 153 | 154 | if not isinstance(timeline_events, (tuple, list)): 155 | timeline_events = [timeline_events] 156 | 157 | for timeline_event in timeline_events: 158 | if not isinstance(timeline_event, dict): 159 | raise AbodeException((ERROR.EVENT_CODE_MISSING)) 160 | 161 | event_code = timeline_event.get('event_code') 162 | 163 | if not event_code: 164 | raise AbodeException((ERROR.EVENT_CODE_MISSING)) 165 | 166 | _LOGGER.debug("Subscribing to timeline event: %s", timeline_event) 167 | 168 | self._timeline_callbacks[event_code].append((callback)) 169 | 170 | return True 171 | 172 | @property 173 | def connected(self): 174 | """Get the Abode connection status.""" 175 | return self._connected 176 | 177 | @property 178 | def socketio(self): 179 | """Get the SocketIO instance.""" 180 | return self._socketio 181 | 182 | def _on_socket_started(self): 183 | """Socket IO startup callback.""" 184 | # pylint: disable=W0212 185 | cookies = self._abode._get_session().cookies.get_dict() 186 | cookie_string = "; ".join( 187 | [str(x) + "=" + str(y) for x, y in cookies.items()]) 188 | 189 | self._socketio.set_cookie(cookie_string) 190 | 191 | def _on_socket_connected(self): 192 | """Socket IO connected callback.""" 193 | self._connected = True 194 | 195 | try: 196 | self._abode.refresh() 197 | # pylint: disable=W0703 198 | except Exception as exc: 199 | _LOGGER.warning("Captured exception during Abode refresh: %s", exc) 200 | finally: 201 | # Callbacks should still execute even if refresh fails (Abode 202 | # server issues) so that the entity availability in Home Assistant 203 | # is updated since we are in fact connected to the web socket. 204 | for callbacks in self._connection_status_callbacks.items(): 205 | for callback in callbacks[1]: 206 | _execute_callback(callback) 207 | 208 | def _on_socket_disconnected(self): 209 | """Socket IO disconnected callback.""" 210 | self._connected = False 211 | 212 | for callbacks in self._connection_status_callbacks.items(): 213 | # Check if list is not empty. 214 | # Applicable when remove_all_device_callbacks 215 | # is called before _on_socket_disconnected. 216 | if callbacks[1]: 217 | for callback in callbacks[1]: 218 | _execute_callback(callback) 219 | 220 | def _on_device_update(self, devid): 221 | """Device callback from Abode SocketIO server.""" 222 | if isinstance(devid, (tuple, list)): 223 | devid = devid[0] 224 | 225 | if devid is None: 226 | _LOGGER.warning("Device update with no device id.") 227 | return 228 | 229 | _LOGGER.debug("Device update event for device ID: %s", devid) 230 | 231 | device = self._abode.get_device(devid, True) 232 | 233 | if not device: 234 | _LOGGER.debug("Got device update for unknown device: %s", devid) 235 | return 236 | 237 | for callback in self._device_callbacks.get(device.device_id, ()): 238 | _execute_callback(callback, device) 239 | 240 | def _on_mode_change(self, mode): 241 | """Mode change broadcast from Abode SocketIO server.""" 242 | if isinstance(mode, (tuple, list)): 243 | mode = mode[0] 244 | 245 | if mode is None: 246 | _LOGGER.warning("Mode change event with no mode.") 247 | return 248 | 249 | if not mode or mode.lower() not in CONST.ALL_MODES: 250 | _LOGGER.warning("Mode change event with unknown mode: %s", mode) 251 | return 252 | 253 | _LOGGER.debug("Alarm mode change event to: %s", mode) 254 | 255 | # We're just going to convert it to an Alarm device 256 | alarm_device = self._abode.get_alarm(refresh=True) 257 | 258 | # At the time of development, refreshing after mode change notification 259 | # didn't seem to get the latest update immediately. As such, we will 260 | # force the mode status now to match the notification. 261 | # pylint: disable=W0212 262 | alarm_device._json_state['mode']['area_1'] = mode 263 | 264 | for callback in self._device_callbacks.get(alarm_device.device_id, ()): 265 | _execute_callback(callback, alarm_device) 266 | 267 | def _on_timeline_update(self, event): 268 | """Timeline update broadcast from Abode SocketIO server.""" 269 | if isinstance(event, (tuple, list)): 270 | event = event[0] 271 | 272 | event_type = event.get('event_type') 273 | event_code = event.get('event_code') 274 | 275 | if not event_type or not event_code: 276 | _LOGGER.warning("Invalid timeline update event: %s", event) 277 | return 278 | 279 | _LOGGER.debug("Timeline event received: %s - %s (%s)", 280 | event.get('event_name'), event_type, event_code) 281 | 282 | # Compress our callbacks into those that match this event_code 283 | # or ones registered to get callbacks for all events 284 | codes = (event_code, TIMELINE.ALL['event_code']) 285 | all_callbacks = [self._timeline_callbacks[code] for code in codes] 286 | 287 | for callbacks in all_callbacks: 288 | for callback in callbacks: 289 | _execute_callback(callback, event) 290 | 291 | # Attempt to map the event code to a group and callback 292 | event_group = TIMELINE.map_event_code(event_code) 293 | 294 | if event_group: 295 | for callback in self._event_callbacks.get(event_group, ()): 296 | _execute_callback(callback, event) 297 | 298 | def _on_automation_update(self, event): 299 | """Automation update broadcast from Abode SocketIO server.""" 300 | event_group = TIMELINE.AUTOMATION_EDIT_GROUP 301 | 302 | if isinstance(event, (tuple, list)): 303 | event = event[0] 304 | 305 | for callback in self._event_callbacks.get(event_group, ()): 306 | _execute_callback(callback, event) 307 | 308 | 309 | def _execute_callback(callback, *args, **kwargs): 310 | # Callback with some data, capturing any exceptions to prevent chaos 311 | try: 312 | callback(*args, **kwargs) 313 | # pylint: disable=W0703 314 | except Exception as exc: 315 | _LOGGER.warning("Captured exception during callback: %s", exc) 316 | -------------------------------------------------------------------------------- /abodepy/helpers/timeline.py: -------------------------------------------------------------------------------- 1 | """Timeline event constants.""" 2 | 3 | # Timeline event groups. 4 | 5 | ALARM_GROUP = 'abode_alarm' 6 | ALARM_END_GROUP = 'abode_alarm_end' 7 | PANEL_FAULT_GROUP = 'abode_panel_fault' 8 | PANEL_RESTORE_GROUP = 'abode_panel_restore' 9 | DISARM_GROUP = 'abode_disarm' 10 | ARM_GROUP = 'abode_arm' 11 | ARM_FAULT_GROUP = 'abode_arm_fault' 12 | TEST_GROUP = 'abode_test' 13 | CAPTURE_GROUP = 'abode_capture' 14 | DEVICE_GROUP = 'abode_device' 15 | AUTOMATION_GROUP = 'abode_automation' 16 | AUTOMATION_EDIT_GROUP = 'abode_automation_edited' 17 | 18 | ALL_EVENT_GROUPS = [ALARM_GROUP, ALARM_END_GROUP, PANEL_FAULT_GROUP, 19 | PANEL_RESTORE_GROUP, DISARM_GROUP, ARM_GROUP, 20 | ARM_FAULT_GROUP, TEST_GROUP, CAPTURE_GROUP, DEVICE_GROUP, 21 | AUTOMATION_GROUP, AUTOMATION_EDIT_GROUP] 22 | 23 | 24 | def map_event_code(event_code): 25 | """Map a specific event_code to an event group.""" 26 | event_code = int(event_code) 27 | 28 | # Honestly, these are just guessing based on the below event list. 29 | # It could be wrong, I have no idea. 30 | if 1100 <= event_code <= 1199: 31 | return ALARM_GROUP 32 | 33 | if 3100 <= event_code <= 3199: 34 | return ALARM_END_GROUP 35 | 36 | if 1300 <= event_code <= 1399: 37 | return PANEL_FAULT_GROUP 38 | 39 | if 3300 <= event_code <= 3399: 40 | return PANEL_RESTORE_GROUP 41 | 42 | if 1400 <= event_code <= 1499: 43 | return DISARM_GROUP 44 | 45 | if 3400 <= event_code <= 3799: 46 | return ARM_GROUP 47 | 48 | if 1600 <= event_code <= 1699: 49 | return TEST_GROUP 50 | 51 | if 5000 <= event_code <= 5099: 52 | return CAPTURE_GROUP 53 | 54 | if 5100 <= event_code <= 5199: 55 | return DEVICE_GROUP 56 | 57 | if 5200 <= event_code <= 5299: 58 | return AUTOMATION_GROUP 59 | 60 | if 6000 <= event_code <= 6100: 61 | return ARM_FAULT_GROUP 62 | 63 | return None 64 | 65 | 66 | # Specific timeline events by event code. 67 | 68 | ALL = { 69 | 'event_code': '0', 70 | 'event_type': 'All Timeline Events (AbodePy)' 71 | } 72 | 73 | MEDICAL = { 74 | 'event_code': '1100', 75 | 'event_type': 'Medical' 76 | } 77 | 78 | PERSONAL_EMERGENCY = { 79 | 'event_code': '1101', 80 | 'event_type': 'Personal Emergency' 81 | } 82 | 83 | FIRE_ALERT = { 84 | 'event_code': '1110', 85 | 'event_type': 'Fire Alert' 86 | } 87 | 88 | SMOKE_DETECTED = { 89 | 'event_code': '1111', 90 | 'event_type': 'Smoke Detected' 91 | } 92 | 93 | PANIC_ALERT = { 94 | 'event_code': '1120', 95 | 'event_type': 'Panic Alert' 96 | } 97 | 98 | DURESS_ALARM = { 99 | 'event_code': '1121', 100 | 'event_type': 'Duress Alarm' 101 | } 102 | 103 | SILENT_PANIC_ALERT = { 104 | 'event_code': '1122', 105 | 'event_type': 'Silent Panic Alert' 106 | } 107 | 108 | ALARM_TRIGGERED = { 109 | 'event_code': '1130', 110 | 'event_type': 'Alarm Triggered' 111 | } 112 | 113 | PERIMETER_ALARM_TRIGGERED = { 114 | 'event_code': '1131', 115 | 'event_type': 'Perimeter Alarm Triggered' 116 | } 117 | 118 | INTERIOR_ALARM_TRIGGERED = { 119 | 'event_code': '1132', 120 | 'event_type': 'Interior Alarm Triggered' 121 | } 122 | 123 | BURGLAR_ALARM_TRIGGERED = { 124 | 'event_code': '1133', 125 | 'event_type': 'Burglar Alarm Triggered' 126 | } 127 | 128 | OUTDOOR_ALARM_TRIGGERED = { 129 | 'event_code': '1136', 130 | 'event_type': 'Outdoor Alarm Triggered' 131 | } 132 | 133 | PANEL_TAMPER_SWITCH_TRIGGERED = { 134 | 'event_code': '1137', 135 | 'event_type': 'Panel Tamper Switch Triggered' 136 | } 137 | 138 | NOT_CONNECTED_TO_GATEWAY = { 139 | 'event_code': '1147', 140 | 'event_type': 'Not Connected to Gateway' 141 | } 142 | 143 | GAS_DETECTED = { 144 | 'event_code': '1151', 145 | 'event_type': 'Gas Detected' 146 | } 147 | 148 | WATER_LEAK_DETECTED = { 149 | 'event_code': '1154', 150 | 'event_type': 'Water Leak Detected' 151 | } 152 | 153 | CO_DETECTED = { 154 | 'event_code': '1162', 155 | 'event_type': 'CO Detected' 156 | } 157 | 158 | POWER_LOST = { 159 | 'event_code': '1301', 160 | 'event_type': 'Power Lost' 161 | } 162 | 163 | BATTERY_LOW = { 164 | 'event_code': '1302', 165 | 'event_type': 'Battery Low' 166 | } 167 | 168 | BATTERY_MISSING_DEAD = { 169 | 'event_code': '1311', 170 | 'event_type': 'Battery Missing/Dead' 171 | } 172 | 173 | JAM_DETECT = { 174 | 'event_code': '1344', 175 | 'event_type': 'Jam Detect/Signal Interference' 176 | } 177 | 178 | GPS_LOCATION_FAIL = { 179 | 'event_code': '1354', 180 | 'event_type': 'GPS Location Fail' 181 | } 182 | 183 | POLLING_FAILURE = { 184 | 'event_code': '1355', 185 | 'event_type': 'Polling Failure' 186 | } 187 | 188 | ARMED_WITH_FAULT = { 189 | 'event_code': '1374', 190 | 'event_type': 'Armed with Fault' 191 | } 192 | 193 | SENSOR_OFFLINE = { 194 | 'event_code': '1380', 195 | 'event_type': 'Sensor Offline' 196 | } 197 | 198 | SUPERVISION_FAIL = { 199 | 'event_code': '1381', 200 | 'event_type': 'Supervision Fail' 201 | } 202 | 203 | TAMPER_SWITCH_TRIGGERED = { 204 | 'event_code': '1383', 205 | 'event_type': 'Tamper Switch Triggered' 206 | } 207 | 208 | BATTERY_LOW = { 209 | 'event_code': '1384', 210 | 'event_type': 'Battery Low' 211 | } 212 | 213 | SYSTEM_DISARMED = { 214 | 'event_code': '1400', 215 | 'event_type': 'System Disarmed' 216 | } 217 | 218 | SYSTEM_DISARMED = { 219 | 'event_code': '1401', 220 | 'event_type': 'System Disarmed' 221 | } 222 | 223 | EVENT_CANCELED = { 224 | 'event_code': '1406', 225 | 'event_type': 'Event Canceled' 226 | } 227 | 228 | SYSTEM_DISARMED = { 229 | 'event_code': '1407', 230 | 'event_type': 'System Disarmed' 231 | } 232 | 233 | SET_UNSET_DISARM = { 234 | 'event_code': '1408', 235 | 'event_type': 'Set/Unset Disarm' 236 | } 237 | 238 | OFFLINE = { 239 | 'event_code': '1447', 240 | 'event_type': 'Offline' 241 | } 242 | 243 | FAIL_TO_CLOSE = { 244 | 'event_code': '1454', 245 | 'event_type': 'Fail to Close' 246 | } 247 | 248 | PARTIAL_ARM = { 249 | 'event_code': '1456', 250 | 'event_type': 'Partial Arm' 251 | } 252 | 253 | USER_ON_PREMISES = { 254 | 'event_code': '1458', 255 | 'event_type': 'User on Premises' 256 | } 257 | 258 | RECENT_CLOSE = { 259 | 'event_code': '1459', 260 | 'event_type': 'Recent Close' 261 | } 262 | 263 | ZONE_BYPASSED = { 264 | 'event_code': '1570', 265 | 'event_type': 'Zone Bypassed' 266 | } 267 | 268 | MANUAL_TEST_REPORT = { 269 | 'event_code': '1601', 270 | 'event_type': 'Manual Test Report' 271 | } 272 | 273 | PERIODIC_TEST = { 274 | 'event_code': '1602', 275 | 'event_type': 'Periodic Test' 276 | } 277 | 278 | POINT_TESTED_OK = { 279 | 'event_code': '1611', 280 | 'event_type': 'Point Tested OK/Technical Alarm' 281 | } 282 | 283 | CALL_REQUEST = { 284 | 'event_code': '1616', 285 | 'event_type': 'Call Request' 286 | } 287 | 288 | MOBILITY_ALARM = { 289 | 'event_code': '1641', 290 | 'event_type': 'Mobility Alarm/Sensor Watch Trouble' 291 | } 292 | 293 | GPS_LOCATION_HELP = { 294 | 'event_code': '1645', 295 | 'event_type': 'GPS Location Help' 296 | } 297 | 298 | GPS_LOCATION_REQUEST = { 299 | 'event_code': '1646', 300 | 'event_type': 'GPS Location Request' 301 | } 302 | 303 | GPS_LOCATION_TRACKER = { 304 | 'event_code': '1647', 305 | 'event_type': 'GPS Location Tracker' 306 | } 307 | 308 | SCREAM = { 309 | 'event_code': '1648', 310 | 'event_type': 'Scream' 311 | } 312 | 313 | MOBILE_UNIT_DISCONNECTED_FROM_BASE = { 314 | 'event_code': '1649', 315 | 'event_type': 'Mobile Unit Disconnected from Base' 316 | } 317 | 318 | TEST_REPORT = { 319 | 'event_code': '1655', 320 | 'event_type': 'Test Report' 321 | } 322 | 323 | ENTRY = { 324 | 'event_code': '1704', 325 | 'event_type': 'Entry' 326 | } 327 | 328 | DC_OPEN_MOBILITY = { 329 | 'event_code': '1750', 330 | 'event_type': 'DC Open - Mobility' 331 | } 332 | 333 | IR_ACTIVITY_MOBILITY = { 334 | 'event_code': '1751', 335 | 'event_type': 'IR Activity - Mobility' 336 | } 337 | 338 | SIREN_ON = { 339 | 'event_code': '1752', 340 | 'event_type': 'Siren On' 341 | } 342 | 343 | MOTION_DETECTED_AREA_2 = { 344 | 'event_code': '1788', 345 | 'event_type': 'Motion Detected Area 2' 346 | } 347 | 348 | MOTION_DETECTED_AREA_1 = { 349 | 'event_code': '1789', 350 | 'event_type': 'Motion Detected Area 1' 351 | } 352 | 353 | PANEL_TAMPER_SWITCH_RESTORED = { 354 | 'event_code': '3137', 355 | 'event_type': 'Panel Tamper Switch Restored' 356 | } 357 | 358 | RECONNECTED_TO_GATEWAY = { 359 | 'event_code': '3147', 360 | 'event_type': 'Re-Connected to Gateway' 361 | } 362 | 363 | POWER_RESTORED = { 364 | 'event_code': '3301', 365 | 'event_type': 'Power Restored' 366 | } 367 | 368 | BATTERY_NORMAL = { 369 | 'event_code': '3302', 370 | 'event_type': 'Battery Normal/OK' 371 | } 372 | 373 | BATTERY_NORMAL = { 374 | 'event_code': '3311', 375 | 'event_type': 'Battery Normal/OK' 376 | } 377 | 378 | SIGNAL_RESTORED = { 379 | 'event_code': '3344', 380 | 'event_type': 'No Jam/Signal Restored' 381 | } 382 | 383 | NET_DEVICE_FAILURE_RESTORED = { 384 | 'event_code': '3354', 385 | 'event_type': 'NET Device Failure Restored' 386 | } 387 | 388 | POLLING_FAILURE_RESTORED = { 389 | 'event_code': '3355', 390 | 'event_type': 'Polling Failure Restored' 391 | } 392 | 393 | TAMPER_SWITCH_RESTORED = { 394 | 'event_code': '3383', 395 | 'event_type': 'Tamper Switch Restored' 396 | } 397 | 398 | BATTERY_NORMAL = { 399 | 'event_code': '3384', 400 | 'event_type': 'Battery Normal/OK' 401 | } 402 | 403 | SYSTEM_ARMED = { 404 | 'event_code': '3400', 405 | 'event_type': 'System Armed' 406 | } 407 | 408 | SYSTEM_ARMED_AWAY = { 409 | 'event_code': '3401', 410 | 'event_type': 'System Armed - Away' 411 | } 412 | 413 | SYSTEM_ARMED = { 414 | 'event_code': '3407', 415 | 'event_type': 'System Armed' 416 | } 417 | 418 | SET_UNSET_ARM = { 419 | 'event_code': '3408', 420 | 'event_type': 'Set/Unset Arm' 421 | } 422 | 423 | ONLINE = { 424 | 'event_code': '3447', 425 | 'event_type': 'Online' 426 | } 427 | 428 | SYSTEM_ARMED_HOME = { 429 | 'event_code': '3456', 430 | 'event_type': 'System Armed - Home' 431 | } 432 | 433 | MOBILE_UNIT_CONNECTED_TO_BASE = { 434 | 'event_code': '3649', 435 | 'event_type': 'Mobile Unit Connected to Base' 436 | } 437 | 438 | DC_CLOSE_MOBILITY = { 439 | 'event_code': '3750', 440 | 'event_type': 'DC Close - Mobility' 441 | } 442 | 443 | SIREN_OFF = { 444 | 'event_code': '3752', 445 | 'event_type': 'Siren Off' 446 | } 447 | 448 | SYSTEM_ARMED_HOME = { 449 | 'event_code': '3758', 450 | 'event_type': 'System Armed - Home' 451 | } 452 | 453 | VEVENT_CODEEO = { 454 | 'event_code': '5000', 455 | 'event_type': 'Vevent_codeeo' 456 | } 457 | 458 | CAPTURE_IMAGE = { 459 | 'event_code': '5001', 460 | 'event_type': 'Capture Image' 461 | } 462 | 463 | BURGLAR_VEVENT_CODEEO = { 464 | 'event_code': '5002', 465 | 'event_type': 'Burglar Vevent_codeeo' 466 | } 467 | 468 | BURGLAR_CAPTURE_IMAGE = { 469 | 'event_code': '5003', 470 | 'event_type': 'Burglar Capture Image' 471 | } 472 | 473 | OPENED = { 474 | 'event_code': '5100', 475 | 'event_type': 'Opened' 476 | } 477 | 478 | CLOSED = { 479 | 'event_code': '5101', 480 | 'event_type': 'Closed' 481 | } 482 | 483 | UNLOCKED = { 484 | 'event_code': '5110', 485 | 'event_type': 'Unlocked' 486 | } 487 | 488 | LOCKED = { 489 | 'event_code': '5111', 490 | 'event_type': 'Locked' 491 | } 492 | 493 | STATUS_AUTOMATION = { 494 | 'event_code': '5201', 495 | 'event_type': 'Status Automation' 496 | } 497 | 498 | SCHEDULED_AUTOMATION = { 499 | 'event_code': '5202', 500 | 'event_type': 'Scheduled Automation' 501 | } 502 | 503 | LOCATION_AUTOMATION = { 504 | 'event_code': '5203', 505 | 'event_type': 'Location Automation' 506 | } 507 | 508 | QUICK_ACTION = { 509 | 'event_code': '5204', 510 | 'event_type': 'Quick Action' 511 | } 512 | 513 | AUTOMATION = { 514 | 'event_code': '5206', 515 | 'event_type': 'Automation' 516 | } 517 | 518 | ARMING_WITH_FAULT_AWAY = { 519 | 'event_code': '6055', 520 | 'event_type': 'Exit Time Started - Arming w/ Faults - Away' 521 | } 522 | 523 | ARMED_WITH_FAULT_AWAY = { 524 | 'event_code': '6071', 525 | 'event_type': 'Armed w/ Faults - Away' 526 | } 527 | 528 | ARMED_WITH_FAULT_HOME = { 529 | 'event_code': '6077', 530 | 'event_type': 'Armed w/ Faults - Home' 531 | } 532 | --------------------------------------------------------------------------------