├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.md ├── requirements-dev.txt ├── requirements.txt ├── setup.py ├── verisure.py └── verisure ├── __init__.py ├── __main__.py └── session.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | 59 | # OSX 60 | *DS_Store 61 | 62 | # Dev Env 63 | .devcontainer/ 64 | .vscode/ 65 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | language: python 3 | python: 4 | - "3.4" 5 | - "3.5" 6 | - "3.6" 7 | - "3.7" 8 | install: 9 | - pip install -r requirements-dev.txt 10 | script: 11 | flake8 --ignore=E402,W503 *.py verisure 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Per Sandström 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 | 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-verisure 2 | 3 | A python3 module for reading and changing status of verisure devices through verisure 4 | app API. 5 | 6 | ## Legal Disclaimer 7 | 8 | This software is not affiliated with Verisure Holding AB and the developers take no 9 | legal responsibility for the functionality or security of your Verisure Alarms and 10 | devices. 11 | 12 | ## Version History 13 | 14 | ```txt 15 | 2.6.8 Added prompt for password 16 | 2.6.7 Add logging option for CLI, do not send empty requests 17 | 2.6.6 Enable trust 18 | 2.6.5 Add logging 19 | 2.6.4 Handle "SYS_00004" response again (was removed in 2.6.3) 20 | 2.6.3 Differentiate response errors 21 | 2.6.2 Fix request wrapper 22 | 2.6.1 Move to automation subdomain 23 | 2.6.0 Add Get Firmware Version 24 | 2.5.6 Fix docstring, cookie lasts 15 minutes 25 | 2.5.5 Solved bug during response error except using CLI 26 | 2.5.4 Add possibility to set giid to all queries, refactoring and resolve lint warnings 27 | 2.5.3 Refactor login 28 | 2.5.2 Fix XBN Database is not activated 29 | 2.5.1 Update CLI, split cookie login to separate function, rename mfa functions 30 | 2.5.0 Add MFA login 31 | 2.4.1 Add download_image 32 | 2.4.0 Add camera support 33 | 2.3.0 Add event-log command 34 | 2.2.0 Add set-autolock-enabled command 35 | 2.1.2 Installation instructions for m-api branch 36 | 2.1.1 Cleaned up readme 37 | 2.1.0 Add door-lock-configuration command 38 | 2.0.0 Move to GraphQL API, major changes 39 | 1.0.0 Move to app-API, major changes 40 | ``` 41 | 42 | ## Installation 43 | 44 | ```sh 45 | pip install vsure 46 | ``` 47 | 48 | or 49 | 50 | ```sh 51 | pip install git+https://github.com/persandstrom/python-verisure.git@version-2 52 | ``` 53 | 54 | ## Usage 55 | 56 | ```py 57 | # example_usage.py 58 | 59 | import verisure 60 | 61 | USERNAME = "example@domain.org" 62 | PASSWORD = "MySuperSecretP@ssword" 63 | 64 | session = verisure.Session(USERNAME, PASSWORD) 65 | 66 | # Login without Multifactor Authentication 67 | installations = session.login() 68 | # Or with Multicator Authentication, check your phone and mailbox 69 | session.request_mfa() 70 | installations = session.validate_mfa(input("code:")) 71 | 72 | # Get the `giid` for your installation 73 | giids = { 74 | inst['alias']: inst['giid'] 75 | for inst in installations['data']['account']['installations'] 76 | } 77 | print(giids) 78 | # {'MY STREET': '123456789000'} 79 | 80 | # Set the giid 81 | session.set_giid(giids["MY STREET"]) 82 | ``` 83 | 84 | ### Read alarm status (py) 85 | 86 | ```py 87 | arm_state = session.request(session.arm_state()) 88 | ``` 89 | 90 | output 91 | 92 | ```json 93 | { 94 | "data": { 95 | "installation": { 96 | "armState": { 97 | "type": null, 98 | "statusType": "DISARMED", 99 | "date": "2020-03-11T21:04:40.000Z", 100 | "name": "Alex Poe", 101 | "changedVia": "CODE", 102 | "__typename": "ArmState" 103 | }, 104 | "__typename": "Installation" 105 | } 106 | } 107 | } 108 | ``` 109 | 110 | ### Read status from alarm and door-window (py) 111 | 112 | ```py 113 | output = session.request(session.arm_state(), session.door_window()) 114 | ``` 115 | 116 | output 117 | 118 | ```json 119 | [ 120 | { 121 | "data": { 122 | "installation": { 123 | "armState": { 124 | "type": null, 125 | "statusType": "DISARMED", 126 | "date": "2022-01-01T00:00:00.000Z", 127 | "name": "Alex Poe", 128 | "changedVia": "CODE", 129 | "__typename": "ArmState" 130 | }, 131 | "__typename": "Installation" 132 | } 133 | } 134 | }, 135 | { 136 | "data": { 137 | "installation": { 138 | "doorWindows": [ 139 | { 140 | "device": { 141 | "deviceLabel": "ABCD EFGH", 142 | "__typename": "Device" 143 | }, 144 | "type": null, 145 | "area": "Front Door", 146 | "state": "CLOSE", 147 | "wired": false, 148 | "reportTime": "2022-01-01T00:00:00.000Z", 149 | "__typename": "DoorWindow" 150 | }, 151 | { 152 | "device": { 153 | "deviceLabel": "IJKL MNOP", 154 | "__typename": "Device" 155 | }, 156 | "type": null, 157 | "area": "Back Door", 158 | "state": "CLOSE", 159 | "wired": false, 160 | "reportTime": "2022-01-01T00:00:00.000Z", 161 | "__typename": "DoorWindow" 162 | } 163 | ], 164 | "__typename": "Installation" 165 | } 166 | } 167 | } 168 | ] 169 | ``` 170 | 171 | ## Command line usage 172 | 173 | ```txt 174 | Usage: python -m verisure [OPTIONS] USERNAME PASSWORD 175 | 176 | Read and change status of verisure devices through verisure app API 177 | 178 | Options: 179 | -i, --installation INTEGER Installation number 180 | -c, --cookie TEXT File to store cookie in 181 | --mfa Login using MFA 182 | --arm-away CODE Set arm status away 183 | --arm-home CODE Set arm state home 184 | --arm-state Read arm state 185 | --broadband Get broadband status 186 | --camera-capture ... 187 | Capture a new image from a camera 188 | --camera-get-request-id DEVICELABEL 189 | Get requestId for camera_capture 190 | --cameras Get cameras state 191 | --cameras-image-series Get the cameras image series 192 | --cameras-last-image Get cameras last image 193 | --capability Get capability 194 | --charge-sms Charge SMS 195 | --climate Get climate 196 | --disarm CODE Disarm alarm 197 | --door-lock ... 198 | Lock door 199 | --door-lock-configuration DEVICELABEL 200 | Get door lock configuration 201 | --door-unlock ... 202 | Unlock door 203 | --door-window Read status of door and window sensors 204 | --event-log Read event log 205 | --fetch-all-installations Fetch installations 206 | --firmware Get firmware information 207 | --guardian-sos Guardian SOS 208 | --is-guardian-activated Is guardian activated 209 | --permissions Permissions 210 | --poll-arm-state ... 211 | Poll arm state 212 | --poll-lock-state ... 213 | Poll lock state 214 | --remaining-sms Get remaing number of SMS 215 | --set-autolock-enabled ... 216 | Enable or disable autolock 217 | --set-smartplug ... 218 | Set state of smart plug 219 | --smart-button Get smart button state 220 | --smart-lock Get smart lock state 221 | --smartplug DEVICELABEL Read status of a single smart plug 222 | --smartplugs Read status of all smart plugs 223 | --user-trackings Read user tracking status 224 | --help Show this message and exit. 225 | ``` 226 | 227 | ### Read alarm status (cli) 228 | 229 | ```sh 230 | vsure user@example.com mypassword --arm-state 231 | ``` 232 | 233 | The output is the same as above [Read alarm status (py)](#read-alarm-status-py) 234 | 235 | ```json 236 | { 237 | "data": { 238 | "installation": { 239 | "armState": { 240 | "type": null, 241 | "statusType": "DISARMED", 242 | "date": "2020-03-11T21:04:40.000Z", 243 | "name": "Alex Poe", 244 | "changedVia": "CODE", 245 | "__typename": "ArmState" 246 | }, 247 | "__typename": "Installation" 248 | } 249 | } 250 | } 251 | ``` 252 | 253 | ### Read status from alarm and door-window (cli) 254 | 255 | ```sh 256 | vsure user@example.com mypassword --arm-state --door-window 257 | ``` 258 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pep8 2 | flake8 3 | pycodestyle>=2.3.0 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests>=2.25.1 2 | click>=8.0.0a1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ Setup for python-verisure """ 2 | 3 | from setuptools import setup 4 | 5 | setup( 6 | name='vsure', 7 | version='2.6.8', 8 | description='Read and change status of verisure devices through mypages.', 9 | long_description='A python3 module for reading and changing status of ' 10 | + 'verisure devices through mypages.', 11 | url='http://github.com/persandstrom/python-verisure', 12 | author='Per Sandstrom', 13 | author_email='per.j.sandstrom@gmail.com', 14 | license='MIT', 15 | classifiers=[ 16 | 'Development Status :: 4 - Beta', 17 | 'Intended Audience :: Developers', 18 | 'Topic :: Home Automation', 19 | 'License :: OSI Approved :: MIT License', 20 | 'Programming Language :: Python :: 3.6', 21 | 'Programming Language :: Python :: 3.7', 22 | 'Programming Language :: Python :: 3.8', 23 | ], 24 | keywords='home automation verisure', 25 | setup_requires=['wheel'], 26 | install_requires=[ 27 | 'requests>=2.25.1', 28 | 'click>=8.0.0a1'], 29 | packages=['verisure'], 30 | zip_safe=True, 31 | entry_points=''' 32 | [console_scripts] 33 | vsure=verisure.__main__:cli 34 | ''' 35 | ) 36 | -------------------------------------------------------------------------------- /verisure.py: -------------------------------------------------------------------------------- 1 | """ Command line interface for Verisure MyPages """ 2 | from verisure import __main__ 3 | __main__.cli() 4 | -------------------------------------------------------------------------------- /verisure/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | A python module for reading and changing status of verisure devices through 3 | verisure app API. 4 | """ 5 | 6 | __all__ = [ 7 | 'Error', 8 | 'LoginError', 9 | 'ResponseError', 10 | 'Session', 11 | ] 12 | 13 | from .session import ( # NOQA 14 | Error, 15 | LoginError, 16 | VariableTypes, 17 | ResponseError, 18 | Session, 19 | ) 20 | 21 | ALARM_ARMED_HOME = 'ARMED_HOME' 22 | ALARM_ARMED_AWAY = 'ARMED_AWAY' 23 | ALARM_DISARMED = 'DISARMED' 24 | LOCK_LOCKED = 'LOCKED' 25 | LOCK_UNLOCKED = 'UNLOCKED' 26 | SMARTPLUG_ON = 'on' 27 | SMARTPLUG_OFF = 'off' 28 | -------------------------------------------------------------------------------- /verisure/__main__.py: -------------------------------------------------------------------------------- 1 | """ Command line interface for Verisure MyPages """ 2 | 3 | import inspect 4 | import json 5 | import re 6 | import click 7 | import logging 8 | import getpass 9 | from verisure import VariableTypes, Session, ResponseError, LoginError 10 | 11 | 12 | class DeviceLabel(click.ParamType): 13 | """Click param for device label""" 14 | name = "DeviceLabel" 15 | 16 | def convert(self, value, param, ctx): 17 | if re.match(r"^([A-Z]|[0-9]){4} ([A-Z]|[0-9]){4}$", value): 18 | return value 19 | self.fail(f"{value!r} is not a device label", param, ctx) 20 | 21 | 22 | class ArmFutureState(click.ParamType): 23 | """Click param for arm future state""" 24 | name = "FutureState" 25 | 26 | 27 | class LockFutureState(click.ParamType): 28 | """Click param for lock future state""" 29 | name = "FutureState" 30 | 31 | 32 | class TransactionId(click.ParamType): 33 | """Click param for transaction id""" 34 | name = "TransactionId" 35 | 36 | 37 | class RequestId(click.ParamType): 38 | """Click param for request id""" 39 | name = "RequestId" 40 | 41 | 42 | class Code(click.ParamType): 43 | """Click param for code""" 44 | name = "Code" 45 | 46 | def convert(self, value, param, ctx): 47 | if re.match(r"^[0-9]{4,6}$", value): 48 | return value 49 | self.fail(f"{value!r} is not a code", param, ctx) 50 | 51 | 52 | VariableTypeMap = { 53 | VariableTypes.DeviceLabel: DeviceLabel(), 54 | VariableTypes.ArmFutureState: ArmFutureState(), 55 | VariableTypes.LockFutureState: LockFutureState(), 56 | bool: click.BOOL, 57 | VariableTypes.TransactionId: TransactionId(), 58 | VariableTypes.RequestId: RequestId(), 59 | VariableTypes.Code: Code(), 60 | } 61 | 62 | 63 | def options_from_operator_list(): 64 | """Get all query operations and build query cli""" 65 | def decorator(f): 66 | ops = inspect.getmembers(Session, predicate=inspect.isfunction) 67 | for name, operation in reversed(ops): 68 | if not hasattr(operation, 'is_query'): 69 | continue 70 | variables = list(operation.__annotations__.values()) 71 | # Remove Giid type from variables, not supported by CLI 72 | if VariableTypes.Giid in variables: 73 | variables.remove(VariableTypes.Giid) 74 | dashed_name = name.replace('_', '-') 75 | if len(variables) == 0: 76 | click.option( 77 | '--'+dashed_name, 78 | is_flag=True, 79 | help=operation.__doc__)(f) 80 | elif len(variables) == 1: 81 | click.option( 82 | '--'+dashed_name, 83 | type=VariableTypeMap[variables[0]], 84 | help=operation.__doc__)(f) 85 | else: 86 | types = [VariableTypeMap[variable] for variable in variables] 87 | click.option( 88 | '--'+dashed_name, 89 | type=click.Tuple(types), 90 | help=operation.__doc__)(f) 91 | return f 92 | return decorator 93 | 94 | 95 | def make_query(session, name, arguments): 96 | """make query operation""" 97 | if arguments is True: 98 | return getattr(session, name)() 99 | if isinstance(arguments, str): 100 | return getattr(session, name)(arguments) 101 | return getattr(session, name)(*arguments) 102 | 103 | 104 | @click.command() 105 | @click.argument('username') 106 | @click.argument('password', required=False) 107 | @click.option('-i', '--installation', 'installation', help='Installation number', type=int, default=0) # noqa: E501 108 | @click.option('-c', '--cookie', 'cookie', help='File to store cookie in', default='~/.verisure-cookie') # noqa: E501 109 | @click.option('--mfa', 'mfa', help='Login using MFA', default=False, is_flag=True) # noqa: E501 110 | @click.option('--log-level', type=click.Choice(['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'], case_sensitive=False)) # noqa: E501 111 | @options_from_operator_list() 112 | def cli(username, password, installation, cookie, mfa, log_level, **kwargs): 113 | """ 114 | Read and change status of verisure devices through verisure app API\n 115 | PASSWORD will be prompted without echoing if not provided as an argument 116 | """ 117 | 118 | if not password: 119 | password = getpass.getpass(prompt='Password: ', stream=None) 120 | if log_level: 121 | logging.basicConfig(level=logging.getLevelName(log_level)) 122 | 123 | session = Session(username, password, cookie) 124 | 125 | try: 126 | # try using the cookie first 127 | installations = session.login_cookie() 128 | except LoginError: 129 | installations = None 130 | 131 | try: 132 | if mfa and not installations: 133 | session.request_mfa() 134 | code = input("Enter verification code: ") 135 | session.validate_mfa(code) 136 | installations = session.login_cookie() 137 | elif not installations: 138 | installations = session.login() 139 | 140 | session.set_giid( 141 | installations['data']['account'] 142 | ['installations'][installation]['giid']) 143 | queries = [ 144 | make_query(session, name, arguments) 145 | for name, arguments in kwargs.items() 146 | if arguments] 147 | result = session.request(*queries) 148 | click.echo(json.dumps(result, indent=4, separators=(',', ': '))) 149 | 150 | except ResponseError as ex: 151 | click.echo(ex,err=True) 152 | 153 | 154 | if __name__ == "__main__": 155 | # pylint: disable=no-value-for-parameter 156 | cli() 157 | -------------------------------------------------------------------------------- /verisure/session.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Verisure session, using verisure app api 3 | ''' 4 | 5 | import json 6 | import logging 7 | import os 8 | import pickle 9 | 10 | import requests 11 | 12 | LOGGER = logging.getLogger(__package__) 13 | 14 | 15 | class Error(Exception): 16 | ''' Verisure session error ''' 17 | 18 | 19 | class RequestError(Error): 20 | ''' Request ''' 21 | 22 | 23 | class LoginError(Error): 24 | ''' Login failed ''' 25 | 26 | 27 | class LogoutError(Error): 28 | ''' Logout failed ''' 29 | 30 | 31 | class ResponseError(Error): 32 | ''' Unexcpected response ''' 33 | def __init__(self, status_code, text): 34 | super().__init__( 35 | f'Invalid response, status code: {status_code} - Data: {text}') 36 | 37 | 38 | def query_func(f): 39 | """A wrapper that indicates that the function is a query (used by CLI)""" 40 | f.is_query = True 41 | return f 42 | 43 | 44 | class VariableTypes: 45 | """Types for query parameters""" 46 | class DeviceLabel(str): 47 | """Device label""" 48 | 49 | class TransactionId(str): 50 | """Transaction ID""" 51 | 52 | class RequestId(str): 53 | """Request ID""" 54 | 55 | class ArmFutureState(str): 56 | """Arm state""" 57 | # ARMED_AWAY, DISARMED, ARMED_HOME 58 | 59 | class LockFutureState(str): 60 | """Lock state""" 61 | 62 | class Code(str): 63 | """Code""" 64 | 65 | class Giid(str): 66 | """Giid""" 67 | 68 | 69 | class Session(object): 70 | """ Verisure app session 71 | 72 | Args: 73 | username (str): Username used to login to verisure app 74 | password (str): Password used to login to verisure app 75 | cookie_file_name (str): path to cookie file 76 | 77 | """ 78 | 79 | def __init__(self, username, password, 80 | cookie_file_name='~/.verisure-cookie'): 81 | LOGGER.info(f"Initialize Session ({username=}, {cookie_file_name=})") 82 | self._username = username 83 | self._password = password 84 | self._cookies = None 85 | self._cookie_file_name = os.path.expanduser(cookie_file_name) 86 | self._trust_token = None 87 | self._giid = None 88 | self._base_url = None 89 | self._base_urls = ['https://automation01.verisure.com', 90 | 'https://automation02.verisure.com'] 91 | self._post = self._wrap_request(requests.post) 92 | self._delete = self._wrap_request(requests.delete) 93 | self._get = self._wrap_request(requests.get) 94 | 95 | 96 | def _wrap_request(self, function): 97 | """ 98 | Used to wrap methods from the requests module to try both urls and remember 99 | the last working one. 100 | """ 101 | 102 | def wrapper(url, *args, **kwargs): 103 | last_exception = Error("Unknown error") 104 | base_urls = self._base_urls.copy() 105 | for base_url in base_urls: 106 | try: 107 | response = function(base_url+url, *args, **kwargs) 108 | if response.status_code > 200 or "errors" in response.text: 109 | LOGGER.debug( 110 | f"{response.request.method} {response.request.url} " 111 | f"{response.status_code} f{response.text}" 112 | ) 113 | if response.status_code >= 500: 114 | last_exception = ResponseError(response.status_code, response.text) 115 | self._base_urls.reverse() 116 | continue 117 | if response.status_code >= 400: 118 | last_exception = LoginError(response.text) 119 | break 120 | if response.status_code == 200: 121 | if "SYS_00004" in response.text: 122 | self._base_urls.reverse() 123 | continue 124 | return response 125 | 126 | except requests.exceptions.RequestException as ex: 127 | LOGGER.warning(f"Unexpected error on '{base_url}{url}' ({ex=})") 128 | last_exception = RequestError(str(ex)) 129 | self._base_urls.reverse() 130 | raise last_exception 131 | return wrapper 132 | 133 | 134 | def login(self): 135 | """ Login to verisure app api 136 | Login before calling any read or write commands 137 | Return installations 138 | """ 139 | 140 | response = self._post( 141 | "/auth/login", 142 | headers={'APPLICATION_ID': 'PS_PYTHON'}, 143 | auth=(self._username, self._password)) 144 | 145 | if "stepUpToken" in response.text: 146 | raise LoginError("Multifactor authentication enabled, " 147 | "disable or create MFA cookie") 148 | 149 | self._cookies = response.cookies 150 | with open(self._cookie_file_name, 'wb') as f: 151 | pickle.dump(self._cookies, f) 152 | 153 | installations = self.get_installations() 154 | if 'errors' not in installations: 155 | return installations 156 | 157 | raise LoginError("Failed to log in") 158 | 159 | def request_mfa(self): 160 | """ Request MFA verification code """ 161 | 162 | response = self._post( 163 | url="/auth/login", 164 | headers={'APPLICATION_ID': 'PS_PYTHON'}, 165 | auth=(self._username, self._password)) 166 | 167 | if "stepUpToken" not in response.text: 168 | raise LoginError("Multifactor authentication disabled, " 169 | "use regular login instead") 170 | 171 | self._cookies = response.cookies 172 | for mfa_type in ['phone', 'email']: 173 | try: 174 | mfa_response = self._post( 175 | url=f"/auth/mfa?type={mfa_type}", 176 | headers={'APPLICATION_ID': 'PS_PYTHON'}, 177 | cookies=self._cookies) 178 | if mfa_response.status_code == 200: 179 | return 180 | except Exception as ex: 181 | raise LoginError("Failed to request MFA type") from ex 182 | 183 | raise LoginError("Failed to log in") 184 | 185 | def validate_mfa(self, code): 186 | """ Validate mfa request 187 | Return installations 188 | """ 189 | 190 | response = self._post( 191 | url="/auth/mfa/validate", 192 | headers={ 193 | 'APPLICATION_ID': 'PS_PYTHON', 194 | 'Accept': 'application/json', 195 | 'Content-Type': 'application/json'}, 196 | cookies=self._cookies, 197 | data=json.dumps({"token": code})) 198 | self._cookies = response.cookies 199 | 200 | trust_response = self._post( 201 | url="/auth/trust", 202 | headers={ 203 | 'APPLICATION_ID': 'PS_PYTHON', 204 | 'Accept': 'application/json', 205 | }, 206 | cookies=self._cookies) 207 | self._cookies.update(trust_response.cookies) 208 | with open(self._cookie_file_name, 'wb') as cookie_file: 209 | pickle.dump(self._cookies, cookie_file) 210 | self._trust_token = trust_response.json() 211 | 212 | installations = self.get_installations() 213 | if 'errors' not in installations: 214 | return installations 215 | 216 | raise LoginError("Failed to log in") 217 | 218 | def login_cookie(self): 219 | """ Login using cookie 220 | Return installations 221 | """ 222 | 223 | # Load cookie from file 224 | try: 225 | with open(self._cookie_file_name, 'rb') as cookie_file: 226 | self._cookies = pickle.load(cookie_file) 227 | except Exception as ex: 228 | raise LoginError("Failed to read cookie") from ex 229 | 230 | # Login 231 | cookie_jar = requests.sessions.RequestsCookieJar() 232 | for name, value in self._cookies.items(): 233 | if 'vs-trust' in name: 234 | cookie_jar.set(name, value) 235 | response = self._post( 236 | url="/auth/login", 237 | headers={'APPLICATION_ID': 'PS_PYTHON'}, 238 | auth=(self._username, self._password), 239 | cookies=cookie_jar) 240 | self._cookies.update(response.cookies) 241 | with open(self._cookie_file_name, 'wb') as f: 242 | pickle.dump(self._cookies, f) 243 | 244 | installations = self.get_installations() 245 | if 'errors' not in installations: 246 | return installations 247 | 248 | raise LoginError("Failed to log in") 249 | 250 | def update_cookie(self): 251 | """ Update expired cookie 252 | Cookie can last 15 minutes before it needs to be updated. 253 | """ 254 | 255 | cookie_jar = requests.sessions.RequestsCookieJar() 256 | if self._cookies is not None: 257 | for name, value in self._cookies.items(): 258 | if name in ['vid', 'vs-refresh']: 259 | cookie_jar[name] = value 260 | response = self._get( 261 | url="/auth/token", 262 | headers={'APPLICATION_ID': 'PS_PYTHON'}, 263 | cookies=cookie_jar) 264 | 265 | self._cookies.update(response.cookies) 266 | with open(self._cookie_file_name, 'wb') as cookie_file: 267 | pickle.dump(self._cookies, cookie_file) 268 | LOGGER.debug(f"Saved cookies: {[cookie for cookie in self._cookies.keys()]}") 269 | 270 | def logout(self): 271 | """ Log out from the verisure app api """ 272 | try: 273 | if self._trust_token is not None: 274 | token = self._trust_token['trustTokenValue'] 275 | self._delete( 276 | url=f"/auth/trust/{token}", 277 | headers={ 278 | 'APPLICATION_ID': 'PS_PYTHON', 279 | 'Accept': 'application/json', 280 | }, 281 | cookies=self._cookies) 282 | self._delete( 283 | url="/auth/logout", 284 | headers={'APPLICATION_ID': 'PS_PYTHON'}, 285 | cookies=self._cookies) 286 | finally: 287 | self._base_url = None 288 | self._giid = None 289 | self._cookies = None 290 | self._trust_token = None 291 | if os.path.exists(self._cookie_file_name): 292 | os.remove(self._cookie_file_name) 293 | 294 | def request(self, *operations): 295 | """Request operations""" 296 | if not operations: 297 | # Return empty json if no operations were requested 298 | return json.loads("{}") 299 | response = self._post( 300 | '/graphql', 301 | headers={ 302 | 'APPLICATION_ID': 'PS_PYTHON', 303 | 'Accept': 'application/json'}, 304 | cookies=self._cookies, 305 | data=json.dumps(list(operations))) 306 | return json.loads(response.text) 307 | 308 | def get_installations(self): 309 | """ Get information about installations """ 310 | return self.request(self.fetch_all_installations()) 311 | 312 | def set_giid(self, giid): 313 | """ Set installation giid 314 | 315 | Args: 316 | giid (str): Installation identifier 317 | """ 318 | self._giid = giid 319 | LOGGER.info(f"Installation identifier set ({giid=})") 320 | 321 | @query_func 322 | def arm_away(self, 323 | code: VariableTypes.Code, 324 | giid: VariableTypes.Giid=None): 325 | """Set arm status away""" 326 | assert giid or self._giid, "Set default giid or pass explicit" 327 | return { 328 | "operationName": "armAway", 329 | "variables": { 330 | "giid": giid or self._giid, 331 | "code": code}, 332 | "query": "mutation armAway($giid: String!, $code: String!) {\n armStateArmAway(giid: $giid, code: $code)\n}\n", # noqa: E501 333 | } 334 | 335 | @query_func 336 | def arm_home(self, 337 | code: VariableTypes.Code, 338 | giid: VariableTypes.Giid=None): 339 | """Set arm state home""" 340 | assert giid or self._giid, "Set default giid or pass explicit" 341 | return { 342 | "operationName": "armHome", 343 | "variables": { 344 | "giid": giid or self._giid, 345 | "code": code}, 346 | "query": "mutation armHome($giid: String!, $code: String!) {\n armStateArmHome(giid: $giid, code: $code)\n}\n", # noqa: E501 347 | } 348 | 349 | @query_func 350 | def arm_state(self, 351 | giid: VariableTypes.Giid=None): 352 | """Read arm state""" 353 | assert giid or self._giid, "Set default giid or pass explicit" 354 | return { 355 | "operationName": "ArmState", 356 | "variables": { 357 | "giid": giid or self._giid}, 358 | "query": "query ArmState($giid: String!) {\n installation(giid: $giid) {\n armState {\n type\n statusType\n date\n name\n changedVia\n __typename\n }\n __typename\n }\n}\n", # noqa: E501 359 | } 360 | 361 | @query_func 362 | def broadband(self, 363 | giid: VariableTypes.Giid=None): 364 | """Get broadband status""" 365 | assert giid or self._giid, "Set default giid or pass explicit" 366 | return { 367 | "operationName": "Broadband", 368 | "variables": { 369 | "giid": giid or self._giid}, 370 | "query": "query Broadband($giid: String!) {\n installation(giid: $giid) {\n broadband {\n testDate\n isBroadbandConnected\n __typename\n }\n __typename\n }\n}\n", # noqa: E501 371 | } 372 | 373 | @query_func 374 | def capability(self, 375 | giid: VariableTypes.Giid=None): 376 | """Get capability""" 377 | assert giid or self._giid, "Set default giid or pass explicit" 378 | return { 379 | "operationName": "Capability", 380 | "variables": { 381 | "giid": giid or self._giid}, 382 | "query": "query Capability($giid: String!) {\n installation(giid: $giid) {\n capability {\n current\n gained {\n capability\n __typename\n }\n __typename\n }\n __typename\n }\n}\n", # noqa: E501 383 | } 384 | 385 | @query_func 386 | def charge_sms(self, 387 | giid: VariableTypes.Giid=None): 388 | """Charge SMS""" 389 | assert giid or self._giid, "Set default giid or pass explicit" 390 | return { 391 | "operationName": "ChargeSms", 392 | "variables": { 393 | "giid": giid or self._giid}, 394 | "query": "query ChargeSms($giid: String!) {\n installation(giid: $giid) {\n chargeSms {\n chargeSmartPlugOnOff\n chargeLockUnlock\n chargeArmDisarm\n chargeNotifications\n __typename\n }\n __typename\n }\n}\n", # noqa: E501 395 | } 396 | 397 | @query_func 398 | def climate(self, 399 | giid: VariableTypes.Giid=None): 400 | """Get climate""" 401 | assert giid or self._giid, "Set default giid or pass explicit" 402 | return { 403 | "operationName": "Climate", 404 | "variables": { 405 | "giid": giid or self._giid}, 406 | "query": "query Climate($giid: String!) {\n installation(giid: $giid) {\n climates {\n device {\n deviceLabel\n area\n gui {\n label\n __typename\n }\n __typename\n }\n humidityEnabled\n humidityTimestamp\n humidityValue\n temperatureTimestamp\n temperatureValue\n thresholds {\n aboveMaxAlert\n belowMinAlert\n sensorType\n __typename\n }\n __typename\n }\n __typename\n }\n}\n", # noqa: E501 407 | } 408 | 409 | @query_func 410 | def disarm(self, 411 | code: VariableTypes.Code, 412 | giid: VariableTypes.Giid=None): 413 | """Disarm alarm""" 414 | assert giid or self._giid, "Set default giid or pass explicit" 415 | return { 416 | "operationName": "disarm", 417 | "variables": { 418 | "giid": giid or self._giid, 419 | "code": code}, 420 | "query": "mutation disarm($giid: String!, $code: String!) {\n armStateDisarm(giid: $giid, code: $code)\n}\n", # noqa: E501 421 | } 422 | 423 | @query_func 424 | def door_lock(self, 425 | device_label: VariableTypes.DeviceLabel, 426 | code: VariableTypes.Code, 427 | giid: VariableTypes.Giid=None): 428 | """Lock door""" 429 | assert giid or self._giid, "Set default giid or pass explicit" 430 | return { 431 | "operationName": "DoorLock", 432 | "variables": { 433 | "giid": giid or self._giid, 434 | "deviceLabel": device_label, 435 | "input": { 436 | "code": code, 437 | }, 438 | }, 439 | "query": "mutation DoorLock($giid: String!, $deviceLabel: String!, $input: LockDoorInput!) {\n DoorLock(giid: $giid, deviceLabel: $deviceLabel, input: $input)\n}\n", # noqa: E501 440 | } 441 | 442 | @query_func 443 | def door_lock_configuration(self, 444 | device_label: VariableTypes.DeviceLabel, 445 | giid: VariableTypes.Giid=None): 446 | """Get door lock configuration""" 447 | assert giid or self._giid, "Set default giid or pass explicit" 448 | return { 449 | "operationName": "DoorLockConfiguration", 450 | "variables": { 451 | "giid": giid or self._giid, 452 | "deviceLabel": device_label}, 453 | "query": "query DoorLockConfiguration($giid: String!, $deviceLabel: String!) {\n installation(giid: $giid) {\n smartLocks(filter: {deviceLabels: [$deviceLabel]}) {\n device {\n area\n deviceLabel\n __typename\n }\n configuration {\n ... on YaleLockConfiguration {\n autoLockEnabled\n voiceLevel\n volume\n __typename\n }\n ... on DanaLockConfiguration {\n holdBackLatchDuration\n twistAssistEnabled\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n}\n", # noqa: E501 454 | } 455 | 456 | @query_func 457 | def set_autolock_enabled(self, 458 | device_label: VariableTypes.DeviceLabel, 459 | auto_lock_enabled: bool, 460 | giid: VariableTypes.Giid=None): 461 | """Enable or disable autolock""" 462 | assert giid or self._giid, "Set default giid or pass explicit" 463 | return { 464 | "operationName": "DoorLockUpdateConfig", 465 | "variables": { 466 | "giid": giid or self._giid, 467 | "deviceLabel": device_label, 468 | "input": { 469 | "autoLockEnabled": auto_lock_enabled 470 | } 471 | }, 472 | "query": "mutation DoorLockUpdateConfig($giid: String!, $deviceLabel: String!, $input: DoorLockUpdateConfigInput!) {\n DoorLockUpdateConfig(giid: $giid, deviceLabel: $deviceLabel, input: $input)\n}\n", # noqa: E501 473 | } 474 | 475 | @query_func 476 | def door_unlock(self, 477 | device_label: VariableTypes.DeviceLabel, 478 | code: VariableTypes.Code, 479 | giid: VariableTypes.Giid=None): 480 | """Unlock door""" 481 | assert giid or self._giid, "Set default giid or pass explicit" 482 | return { 483 | "operationName": "DoorUnlock", 484 | "variables": { 485 | "giid": giid or self._giid, 486 | "deviceLabel": device_label, 487 | "input": { 488 | "code": code, 489 | }, 490 | }, 491 | "query": "mutation DoorUnlock($giid: String!, $deviceLabel: String!, $input: LockDoorInput!) {\n DoorUnlock(giid: $giid, deviceLabel: $deviceLabel, input: $input)\n}\n", # noqa: E501 492 | } 493 | 494 | @query_func 495 | def door_window(self, 496 | giid: VariableTypes.Giid=None): 497 | """Read status of door and window sensors""" 498 | assert giid or self._giid, "Set default giid or pass explicit" 499 | return { 500 | "operationName": "DoorWindow", 501 | "variables": { 502 | "giid": giid or self._giid}, 503 | "query": "query DoorWindow($giid: String!) {\n installation(giid: $giid) {\n doorWindows {\n device {\n deviceLabel\n __typename\n }\n type\n area\n state\n wired\n reportTime\n __typename\n }\n __typename\n }\n}\n", # noqa: E501 504 | } 505 | 506 | @query_func 507 | def event_log(self, 508 | giid: VariableTypes.Giid=None): 509 | """Read event log""" 510 | assert giid or self._giid, "Set default giid or pass explicit" 511 | return { 512 | "operationName": "EventLog", 513 | "variables": { 514 | "giid": giid or self._giid, 515 | "offset": 0, 516 | "pagesize": 15, 517 | "eventCategories": ["INTRUSION", "FIRE", "SOS", "WATER", "ANIMAL", "TECHNICAL", "WARNING", "ARM", "DISARM", "LOCK", "UNLOCK", "PICTURE", "CLIMATE", "CAMERA_SETTINGS"], # noqa: E501 518 | "eventContactIds": [], 519 | "eventDeviceLabels": [], 520 | "fromDate": None, 521 | "toDate": None 522 | }, 523 | "query": "query EventLog($giid: String!, $offset: Int!, $pagesize: Int!, $eventCategories: [String], $fromDate: String, $toDate: String, $eventContactIds: [String], $eventDeviceLabels: [String]) {\n installation(giid: $giid) {\n eventLog(offset: $offset, pagesize: $pagesize, eventCategories: $eventCategories, eventContactIds: $eventContactIds, eventDeviceLabels: $eventDeviceLabels, fromDate: $fromDate, toDate: $toDate) {\n moreDataAvailable\n pagedList {\n device {\n deviceLabel\n area\n gui {\n label\n __typename\n }\n __typename\n }\n arloDevice {\n name\n __typename\n }\n gatewayArea\n eventType\n eventCategory\n eventSource\n eventId\n eventTime\n userName\n armState\n userType\n climateValue\n sensorType\n eventCount\n __typename\n }\n __typename\n }\n __typename\n }\n}\n", # noqa: E501 524 | } 525 | 526 | @query_func 527 | def fetch_all_installations(self): 528 | """Fetch installations""" 529 | return { 530 | "operationName": "fetchAllInstallations", 531 | "variables": { 532 | "email": self._username}, 533 | "query": "query fetchAllInstallations($email: String!){\n account(email: $email) {\n installations {\n giid\n alias\n customerType\n dealerId\n subsidiary\n pinCodeLength\n locale\n address {\n street\n city\n postalNumber\n __typename\n }\n __typename\n }\n __typename\n }\n}\n", # noqa: E501 534 | } 535 | 536 | @query_func 537 | def firmware(self, 538 | giid: VariableTypes.Giid=None): 539 | """Get firmware information""" 540 | assert giid or self._giid, "Set default giid or pass explicit" 541 | return { 542 | "operationName": "Firmware", 543 | "variables": { 544 | "giid": giid or self._giid 545 | }, 546 | "query": "query Firmware($giid: String!) {\n installation(giid: $giid) {\n firmware {\n status {\n latestFirmware\n requestedFirmware\n upgradeable\n status\n gateways {\n reportedRunningFirmware\n deviceLabel\n status\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n}\n" # noqa: E501 547 | } 548 | 549 | @query_func 550 | def guardian_sos(self): 551 | """Guardian SOS""" 552 | return { 553 | "operationName": "GuardianSos", 554 | "variables": {}, 555 | "query": "query GuardianSos {\n guardianSos {\n serverTime\n sos {\n fullName\n phone\n deviceId\n deviceName\n giid\n type\n username\n expireDate\n warnBeforeExpireDate\n contactId\n __typename\n }\n __typename\n }\n}\n", # noqa: E501 556 | } 557 | 558 | @query_func 559 | def is_guardian_activated(self, 560 | giid: VariableTypes.Giid=None): 561 | """Is guardian activated""" 562 | assert giid or self._giid, "Set default giid or pass explicit" 563 | return { 564 | "operationName": "IsGuardianActivated", 565 | "variables": { 566 | "giid": giid or self._giid, 567 | "featureName": "GUARDIAN"}, 568 | "query": "query IsGuardianActivated($giid: String!, $featureName: String!) {\n installation(giid: $giid) {\n activatedFeature {\n isFeatureActivated(featureName: $featureName)\n __typename\n }\n __typename\n }\n}\n", # noqa: E501 569 | } 570 | 571 | @query_func 572 | def permissions(self, 573 | giid: VariableTypes.Giid=None): 574 | """Permissions""" 575 | assert giid or self._giid, "Set default giid or pass explicit" 576 | return { 577 | "operationName": "Permissions", 578 | "variables": { 579 | "giid": giid or self._giid, 580 | "email": self._username}, 581 | "query": "query Permissions($giid: String!, $email: String!) {\n permissions(giid: $giid, email: $email) {\n accountPermissionsHash\n name\n __typename\n }\n}\n", # noqa: E501 582 | } 583 | 584 | @query_func 585 | def poll_arm_state(self, 586 | transaction_id: VariableTypes.TransactionId, 587 | future_state: VariableTypes.ArmFutureState, 588 | giid: VariableTypes.Giid=None): 589 | """Poll arm state""" 590 | assert giid or self._giid, "Set default giid or pass explicit" 591 | return { 592 | "operationName": "pollArmState", 593 | "variables": { 594 | "giid": giid or self._giid, 595 | "transactionId": transaction_id, 596 | "futureState": future_state}, 597 | "query": "query pollArmState($giid: String!, $transactionId: String, $futureState: ArmStateStatusTypes!) {\n installation(giid: $giid) {\n armStateChangePollResult(transactionId: $transactionId, futureState: $futureState) {\n result\n createTime\n __typename\n }\n __typename\n }\n}\n", # noqa: E501 598 | } 599 | 600 | @query_func 601 | def poll_lock_state(self, 602 | transaction_id: VariableTypes.TransactionId, 603 | device_label: VariableTypes.DeviceLabel, 604 | future_state: VariableTypes.LockFutureState, 605 | giid: VariableTypes.Giid=None): 606 | """Poll lock state""" 607 | assert giid or self._giid, "Set default giid or pass explicit" 608 | return { 609 | "operationName": "pollLockState", 610 | "variables": { 611 | "giid": giid or self._giid, 612 | "transactionId": transaction_id, 613 | "deviceLabel": device_label, 614 | "futureState": future_state}, 615 | "query": "query pollLockState($giid: String!, $transactionId: String, $deviceLabel: String!, $futureState: DoorLockState!) {\n installation(giid: $giid) {\n doorLockStateChangePollResult(transactionId: $transactionId, deviceLabel: $deviceLabel, futureState: $futureState) {\n result\n createTime\n __typename\n }\n __typename\n }\n}\n", # noqa: E501 616 | } 617 | 618 | @query_func 619 | def remaining_sms(self, 620 | giid: VariableTypes.Giid=None): 621 | """Get remaing number of SMS""" 622 | assert giid or self._giid, "Set default giid or pass explicit" 623 | return { 624 | "operationName": "RemainingSms", 625 | "variables": { 626 | "giid": giid or self._giid}, 627 | "query": "query RemainingSms($giid: String!) {\n installation(giid: $giid) {\n remainingSms\n __typename\n }\n}\n", # noqa: E501 628 | } 629 | 630 | @query_func 631 | def smart_button(self, 632 | giid: VariableTypes.Giid=None): 633 | """Get smart button state""" 634 | assert giid or self._giid, "Set default giid or pass explicit" 635 | return { 636 | "operationName": "SmartButton", 637 | "variables": { 638 | "giid": giid or self._giid}, 639 | "query": "query SmartButton($giid: String!) {\n installation(giid: $giid) {\n smartButton {\n entries {\n smartButtonId\n icon\n label\n color\n active\n action {\n actionType\n expectedState\n target {\n ... on Installation {\n alias\n __typename\n }\n ... on Device {\n deviceLabel\n area\n gui {\n label\n __typename\n }\n featureStatuses(type: \"SmartPlug\") {\n device {\n deviceLabel\n __typename\n }\n ... on SmartPlug {\n icon\n isHazardous\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n}\n", # noqa: E501 640 | } 641 | 642 | @query_func 643 | def smart_lock(self, 644 | giid: VariableTypes.Giid=None): 645 | """Get smart lock state""" 646 | assert giid or self._giid, "Set default giid or pass explicit" 647 | return { 648 | "operationName": "SmartLock", 649 | "variables": { 650 | "giid": giid or self._giid}, 651 | "query": "query SmartLock($giid: String!) {\n installation(giid: $giid) {\n smartLocks {\n lockStatus\n doorState\n lockMethod\n eventTime\n doorLockType\n secureMode\n device {\n deviceLabel\n area\n __typename\n }\n user {\n name\n __typename\n }\n __typename\n }\n __typename\n }\n}\n", # noqa: E501 652 | } 653 | 654 | @query_func 655 | def set_smartplug(self, 656 | device_label: VariableTypes.DeviceLabel, 657 | state: bool, 658 | giid: VariableTypes.Giid=None): 659 | """Set state of smart plug""" 660 | assert giid or self._giid, "Set default giid or pass explicit" 661 | return { 662 | "operationName": "UpdateState", 663 | "variables": { 664 | "giid": giid or self._giid, 665 | "deviceLabel": device_label, 666 | "state": state}, 667 | "query": "mutation UpdateState($giid: String!, $deviceLabel: String!, $state: Boolean!) {\n SmartPlugSetState(giid: $giid, input: [{deviceLabel: $deviceLabel, state: $state}])}", # noqa: E501 668 | } 669 | 670 | @query_func 671 | def smartplug(self, 672 | device_label: VariableTypes.DeviceLabel, 673 | giid: VariableTypes.Giid=None): 674 | """Read status of a single smart plug""" 675 | assert giid or self._giid, "Set default giid or pass explicit" 676 | return { 677 | "operationName": "SmartPlug", 678 | "variables": { 679 | "giid": giid or self._giid, 680 | "deviceLabel": device_label}, 681 | "query": "query SmartPlug($giid: String!, $deviceLabel: String!) {\n installation(giid: $giid) {\n smartplugs(filter: {deviceLabels: [$deviceLabel]}) {\n device {\n deviceLabel\n area\n __typename\n }\n currentState\n icon\n isHazardous\n __typename\n }\n __typename\n }\n}\n", # noqa: E501 682 | } 683 | 684 | @query_func 685 | def smartplugs(self, 686 | giid: VariableTypes.Giid=None): 687 | """Read status of all smart plugs""" 688 | assert giid or self._giid, "Set default giid or pass explicit" 689 | return { 690 | "operationName": "SmartPlug", 691 | "variables": { 692 | "giid": giid or self._giid}, 693 | "query": "query SmartPlug($giid: String!) {\n installation(giid: $giid) {\n smartplugs {\n device {\n deviceLabel\n area\n __typename\n }\n currentState\n icon\n isHazardous\n __typename\n }\n __typename\n }\n}\n", # noqa: E501 694 | } 695 | 696 | @query_func 697 | def user_trackings(self, 698 | giid: VariableTypes.Giid=None): 699 | """Read user tracking status""" 700 | assert giid or self._giid, "Set default giid or pass explicit" 701 | return { 702 | "operationName": "userTrackings", 703 | "variables": { 704 | "giid": giid or self._giid}, 705 | "query": "query userTrackings($giid: String!) {\n installation(giid: $giid) {\n userTrackings {\n isCallingUser\n webAccount\n status\n xbnContactId\n currentLocationName\n deviceId\n name\n initials\n currentLocationTimestamp\n deviceName\n currentLocationId\n __typename\n }\n __typename\n }\n}\n", # noqa: E501 706 | } 707 | 708 | @query_func 709 | def cameras(self, 710 | giid: VariableTypes.Giid=None): 711 | """Get cameras state""" 712 | assert giid or self._giid, "Set default giid or pass explicit" 713 | return { 714 | "operationName": "Camera", 715 | "variables": { 716 | "all": True, 717 | "giid": giid or self._giid}, 718 | "query": "query Camera($giid: String!, $all: Boolean!) {\n installation(giid: $giid) {\n cameras(allCameras: $all) {\n visibleOnCard\n initiallyConfigured\n imageCaptureAllowed\n imageCaptureAllowedByArmstate\n device {\n deviceLabel\n area\n __typename\n }\n latestCameraSeries {\n image {\n imageId\n imageStatus\n captureTime\n url\n }\n }\n }\n }\n}", # noqa: E501 719 | } 720 | 721 | @query_func 722 | def cameras_last_image(self, 723 | giid: VariableTypes.Giid=None): 724 | """Get cameras last image""" 725 | assert giid or self._giid, "Set default giid or pass explicit" 726 | return { 727 | "variables": { 728 | "giid": giid or self._giid}, 729 | "query": "query queryCaptureImageRequestStatus($giid: String!) {\n installation(giid: $giid) {\n cameraContentProvider {\n latestImage {\n deviceLabel\n mediaId\n contentType\n contentUrl\n timestamp\n duration\n thumbnailUrl\n bitRate\n width\n height\n codec\n }\n }\n }\n}", # noqa: E501 730 | } 731 | 732 | @query_func 733 | def cameras_image_series(self, 734 | limit=50, 735 | offset=0, 736 | giid: VariableTypes.Giid=None): 737 | """Get the cameras image series""" 738 | assert giid or self._giid, "Set default giid or pass explicit" 739 | return { 740 | "operationName": "GQL_CCCP_SearchMedia", 741 | "variables": { 742 | "giid": giid or self._giid, 743 | "limit": limit, 744 | "offset": offset}, 745 | "query": "mutation GQL_CCCP_SearchMedia(\n $giid: BigInt!\n $offset: Int\n $limit: Int\n $fromDate: Date\n $toDate: Date) {\n\n ContentProviderMediaSearch(\n giid: $giid\n offset: $offset\n limit: $limit\n fromDate: $fromDate\n toDate: $toDate\n ) {\n totalNumberOfMediaSeries\n mediaSeriesList {\n seriesId\n storageType\n viewed\n timestamp\n deviceMediaList {\n contentUrl\n mediaAvailable\n deviceLabel\n mediaId\n contentType\n timestamp\n requestTimestamp\n duration\n expiryDate\n viewed\n thumbnailUrl\n bitRate\n width\n height\n codec\n }\n }\n }\n}", # noqa: E501} 746 | } 747 | 748 | @query_func 749 | def camera_get_request_id(self, 750 | device_label: VariableTypes.DeviceLabel, 751 | giid: VariableTypes.Giid=None): 752 | """Get requestId for camera_capture""" 753 | assert giid or self._giid, "Set default giid or pass explicit" 754 | return { 755 | "variables": { 756 | "deviceIdentifier": "RandomString", 757 | "deviceLabel": device_label, 758 | "giid": giid or self._giid, 759 | "resolution": "high"}, 760 | "query": "mutation cccp($giid: String!, $deviceLabel: String!, $resolution: String!, $deviceIdentifier: String) {\n ContentProviderCaptureImageRequest(giid: $giid, deviceLabel: $deviceLabel, resolution: $resolution, deviceIdentifier: $deviceIdentifier) {\n requestId\n }\n}", # noqa: E501 761 | } 762 | 763 | @query_func 764 | def camera_capture(self, 765 | device_label: VariableTypes.DeviceLabel, 766 | request_id: VariableTypes.RequestId, 767 | giid: VariableTypes.Giid=None): 768 | """Capture a new image from a camera""" 769 | assert giid or self._giid, "Set default giid or pass explicit" 770 | return { 771 | "variables": { 772 | "deviceLabel": device_label, 773 | "giid": giid or self._giid, 774 | "requestId": request_id}, 775 | "query": "query queryCaptureImageRequestStatus($giid: String!, $deviceLabel: String!, $requestId: BigInt!) {\n installation(giid: $giid) {\n cameraContentProvider {\n captureImageRequestStatus(deviceLabel: $deviceLabel, requestId: $requestId) {\n mediaRequestStatus\n }\n }\n }\n}", # noqa: E501 776 | } 777 | 778 | def download_image(self, image_url, file_name): 779 | """Download image from url""" 780 | try: 781 | response = requests.get(image_url, stream=True) 782 | except requests.exceptions.RequestException as ex: 783 | raise RequestError("Failed to get image") from ex 784 | with open(file_name, 'wb') as image_file: 785 | for chunk in response.iter_content(chunk_size=1024): 786 | if chunk: 787 | image_file.write(chunk) 788 | --------------------------------------------------------------------------------