vsure 2.6.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
verisure/__init__.py ADDED
@@ -0,0 +1,29 @@
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
+ 'RequestError',
10
+ 'ResponseError',
11
+ 'Session',
12
+ ]
13
+
14
+ from .session import ( # NOQA
15
+ Error,
16
+ LoginError,
17
+ RequestError,
18
+ VariableTypes,
19
+ ResponseError,
20
+ Session,
21
+ )
22
+
23
+ ALARM_ARMED_HOME = 'ARMED_HOME'
24
+ ALARM_ARMED_AWAY = 'ARMED_AWAY'
25
+ ALARM_DISARMED = 'DISARMED'
26
+ LOCK_LOCKED = 'LOCKED'
27
+ LOCK_UNLOCKED = 'UNLOCKED'
28
+ SMARTPLUG_ON = 'on'
29
+ SMARTPLUG_OFF = 'off'
verisure/__main__.py ADDED
@@ -0,0 +1,156 @@
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()
verisure/session.py ADDED
@@ -0,0 +1,799 @@
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 _load_cookie_file_into_memory(self):
219
+ """Populate ``_cookies`` from the persisted pickle (used by cookie login paths)."""
220
+
221
+ try:
222
+ with open(self._cookie_file_name, 'rb') as cookie_file:
223
+ self._cookies = pickle.load(cookie_file)
224
+ except OSError as ex:
225
+ raise LoginError("Failed to read cookie") from ex
226
+ except (EOFError, pickle.UnpicklingError, AttributeError, TypeError, ValueError) as ex:
227
+ raise LoginError("Failed to read cookie") from ex
228
+
229
+ def login_cookie(self):
230
+ """ Login using cookie
231
+ Return installations
232
+ """
233
+
234
+ self._load_cookie_file_into_memory()
235
+
236
+ # Login
237
+ cookie_jar = requests.sessions.RequestsCookieJar()
238
+ for name, value in self._cookies.items():
239
+ if 'vs-trust' in name:
240
+ cookie_jar.set(name, value)
241
+ response = self._post(
242
+ url="/auth/login",
243
+ headers={'APPLICATION_ID': 'PS_PYTHON'},
244
+ auth=(self._username, self._password),
245
+ cookies=cookie_jar)
246
+ self._cookies.update(response.cookies)
247
+ with open(self._cookie_file_name, 'wb') as f:
248
+ pickle.dump(self._cookies, f)
249
+
250
+ installations = self.get_installations()
251
+ if 'errors' not in installations:
252
+ return installations
253
+
254
+ raise LoginError("Failed to log in")
255
+
256
+ def update_cookie(self):
257
+ """ Update expired cookie
258
+ Cookie can last 15 minutes before it needs to be updated.
259
+
260
+ Long-running callers may reset ``self._cookies`` while a valid pickle remains
261
+ on disk; hydrate from the file before calling ``/auth/token`` so the request
262
+ is not sent with an empty cookie jar.
263
+ """
264
+ if self._cookies is None:
265
+ self._load_cookie_file_into_memory()
266
+
267
+ cookie_jar = requests.sessions.RequestsCookieJar()
268
+ if self._cookies is not None:
269
+ for name, value in self._cookies.items():
270
+ if name in ['vid', 'vs-refresh']:
271
+ cookie_jar[name] = value
272
+ response = self._get(
273
+ url="/auth/token",
274
+ headers={'APPLICATION_ID': 'PS_PYTHON'},
275
+ cookies=cookie_jar)
276
+
277
+ self._cookies.update(response.cookies)
278
+ with open(self._cookie_file_name, 'wb') as cookie_file:
279
+ pickle.dump(self._cookies, cookie_file)
280
+ LOGGER.debug(f"Saved cookies: {[cookie for cookie in self._cookies.keys()]}")
281
+
282
+ def logout(self):
283
+ """ Log out from the verisure app api """
284
+ try:
285
+ if self._trust_token is not None:
286
+ token = self._trust_token['trustTokenValue']
287
+ self._delete(
288
+ url=f"/auth/trust/{token}",
289
+ headers={
290
+ 'APPLICATION_ID': 'PS_PYTHON',
291
+ 'Accept': 'application/json',
292
+ },
293
+ cookies=self._cookies)
294
+ self._delete(
295
+ url="/auth/logout",
296
+ headers={'APPLICATION_ID': 'PS_PYTHON'},
297
+ cookies=self._cookies)
298
+ finally:
299
+ self._base_url = None
300
+ self._giid = None
301
+ self._cookies = None
302
+ self._trust_token = None
303
+ if os.path.exists(self._cookie_file_name):
304
+ os.remove(self._cookie_file_name)
305
+
306
+ def request(self, *operations):
307
+ """Request operations"""
308
+ if not operations:
309
+ # Return empty json if no operations were requested
310
+ return json.loads("{}")
311
+ response = self._post(
312
+ '/graphql',
313
+ headers={
314
+ 'APPLICATION_ID': 'PS_PYTHON',
315
+ 'Accept': 'application/json'},
316
+ cookies=self._cookies,
317
+ data=json.dumps(list(operations)))
318
+ return json.loads(response.text)
319
+
320
+ def get_installations(self):
321
+ """ Get information about installations """
322
+ return self.request(self.fetch_all_installations())
323
+
324
+ def set_giid(self, giid):
325
+ """ Set installation giid
326
+
327
+ Args:
328
+ giid (str): Installation identifier
329
+ """
330
+ self._giid = giid
331
+ LOGGER.info(f"Installation identifier set ({giid=})")
332
+
333
+ @query_func
334
+ def arm_away(self,
335
+ code: VariableTypes.Code,
336
+ giid: VariableTypes.Giid=None):
337
+ """Set arm status away"""
338
+ assert giid or self._giid, "Set default giid or pass explicit"
339
+ return {
340
+ "operationName": "armAway",
341
+ "variables": {
342
+ "giid": giid or self._giid,
343
+ "code": code},
344
+ "query": "mutation armAway($giid: String!, $code: String!) {\n armStateArmAway(giid: $giid, code: $code)\n}\n", # noqa: E501
345
+ }
346
+
347
+ @query_func
348
+ def arm_home(self,
349
+ code: VariableTypes.Code,
350
+ giid: VariableTypes.Giid=None):
351
+ """Set arm state home"""
352
+ assert giid or self._giid, "Set default giid or pass explicit"
353
+ return {
354
+ "operationName": "armHome",
355
+ "variables": {
356
+ "giid": giid or self._giid,
357
+ "code": code},
358
+ "query": "mutation armHome($giid: String!, $code: String!) {\n armStateArmHome(giid: $giid, code: $code)\n}\n", # noqa: E501
359
+ }
360
+
361
+ @query_func
362
+ def arm_state(self,
363
+ giid: VariableTypes.Giid=None):
364
+ """Read arm state"""
365
+ assert giid or self._giid, "Set default giid or pass explicit"
366
+ return {
367
+ "operationName": "ArmState",
368
+ "variables": {
369
+ "giid": giid or self._giid},
370
+ "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
371
+ }
372
+
373
+ @query_func
374
+ def broadband(self,
375
+ giid: VariableTypes.Giid=None):
376
+ """Get broadband status"""
377
+ assert giid or self._giid, "Set default giid or pass explicit"
378
+ return {
379
+ "operationName": "Broadband",
380
+ "variables": {
381
+ "giid": giid or self._giid},
382
+ "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
383
+ }
384
+
385
+ @query_func
386
+ def capability(self,
387
+ giid: VariableTypes.Giid=None):
388
+ """Get capability"""
389
+ assert giid or self._giid, "Set default giid or pass explicit"
390
+ return {
391
+ "operationName": "Capability",
392
+ "variables": {
393
+ "giid": giid or self._giid},
394
+ "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
395
+ }
396
+
397
+ @query_func
398
+ def charge_sms(self,
399
+ giid: VariableTypes.Giid=None):
400
+ """Charge SMS"""
401
+ assert giid or self._giid, "Set default giid or pass explicit"
402
+ return {
403
+ "operationName": "ChargeSms",
404
+ "variables": {
405
+ "giid": giid or self._giid},
406
+ "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
407
+ }
408
+
409
+ @query_func
410
+ def climate(self,
411
+ giid: VariableTypes.Giid=None):
412
+ """Get climate"""
413
+ assert giid or self._giid, "Set default giid or pass explicit"
414
+ return {
415
+ "operationName": "Climate",
416
+ "variables": {
417
+ "giid": giid or self._giid},
418
+ "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
419
+ }
420
+
421
+ @query_func
422
+ def disarm(self,
423
+ code: VariableTypes.Code,
424
+ giid: VariableTypes.Giid=None):
425
+ """Disarm alarm"""
426
+ assert giid or self._giid, "Set default giid or pass explicit"
427
+ return {
428
+ "operationName": "disarm",
429
+ "variables": {
430
+ "giid": giid or self._giid,
431
+ "code": code},
432
+ "query": "mutation disarm($giid: String!, $code: String!) {\n armStateDisarm(giid: $giid, code: $code)\n}\n", # noqa: E501
433
+ }
434
+
435
+ @query_func
436
+ def door_lock(self,
437
+ device_label: VariableTypes.DeviceLabel,
438
+ code: VariableTypes.Code,
439
+ giid: VariableTypes.Giid=None):
440
+ """Lock door"""
441
+ assert giid or self._giid, "Set default giid or pass explicit"
442
+ return {
443
+ "operationName": "DoorLock",
444
+ "variables": {
445
+ "giid": giid or self._giid,
446
+ "deviceLabel": device_label,
447
+ "input": {
448
+ "code": code,
449
+ },
450
+ },
451
+ "query": "mutation DoorLock($giid: String!, $deviceLabel: String!, $input: LockDoorInput!) {\n DoorLock(giid: $giid, deviceLabel: $deviceLabel, input: $input)\n}\n", # noqa: E501
452
+ }
453
+
454
+ @query_func
455
+ def door_lock_configuration(self,
456
+ device_label: VariableTypes.DeviceLabel,
457
+ giid: VariableTypes.Giid=None):
458
+ """Get door lock configuration"""
459
+ assert giid or self._giid, "Set default giid or pass explicit"
460
+ return {
461
+ "operationName": "DoorLockConfiguration",
462
+ "variables": {
463
+ "giid": giid or self._giid,
464
+ "deviceLabel": device_label},
465
+ "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
466
+ }
467
+
468
+ @query_func
469
+ def set_autolock_enabled(self,
470
+ device_label: VariableTypes.DeviceLabel,
471
+ auto_lock_enabled: bool,
472
+ giid: VariableTypes.Giid=None):
473
+ """Enable or disable autolock"""
474
+ assert giid or self._giid, "Set default giid or pass explicit"
475
+ return {
476
+ "operationName": "DoorLockUpdateConfig",
477
+ "variables": {
478
+ "giid": giid or self._giid,
479
+ "deviceLabel": device_label,
480
+ "input": {
481
+ "autoLockEnabled": auto_lock_enabled
482
+ }
483
+ },
484
+ "query": "mutation DoorLockUpdateConfig($giid: String!, $deviceLabel: String!, $input: DoorLockUpdateConfigInput!) {\n DoorLockUpdateConfig(giid: $giid, deviceLabel: $deviceLabel, input: $input)\n}\n", # noqa: E501
485
+ }
486
+
487
+ @query_func
488
+ def door_unlock(self,
489
+ device_label: VariableTypes.DeviceLabel,
490
+ code: VariableTypes.Code,
491
+ giid: VariableTypes.Giid=None):
492
+ """Unlock door"""
493
+ assert giid or self._giid, "Set default giid or pass explicit"
494
+ return {
495
+ "operationName": "DoorUnlock",
496
+ "variables": {
497
+ "giid": giid or self._giid,
498
+ "deviceLabel": device_label,
499
+ "input": {
500
+ "code": code,
501
+ },
502
+ },
503
+ "query": "mutation DoorUnlock($giid: String!, $deviceLabel: String!, $input: LockDoorInput!) {\n DoorUnlock(giid: $giid, deviceLabel: $deviceLabel, input: $input)\n}\n", # noqa: E501
504
+ }
505
+
506
+ @query_func
507
+ def door_window(self,
508
+ giid: VariableTypes.Giid=None):
509
+ """Read status of door and window sensors"""
510
+ assert giid or self._giid, "Set default giid or pass explicit"
511
+ return {
512
+ "operationName": "DoorWindow",
513
+ "variables": {
514
+ "giid": giid or self._giid},
515
+ "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
516
+ }
517
+
518
+ @query_func
519
+ def event_log(self,
520
+ giid: VariableTypes.Giid=None):
521
+ """Read event log"""
522
+ assert giid or self._giid, "Set default giid or pass explicit"
523
+ return {
524
+ "operationName": "EventLog",
525
+ "variables": {
526
+ "giid": giid or self._giid,
527
+ "offset": 0,
528
+ "pagesize": 15,
529
+ "eventCategories": ["INTRUSION", "FIRE", "SOS", "WATER", "ANIMAL", "TECHNICAL", "WARNING", "ARM", "DISARM", "LOCK", "UNLOCK", "PICTURE", "CLIMATE", "CAMERA_SETTINGS"], # noqa: E501
530
+ "eventContactIds": [],
531
+ "eventDeviceLabels": [],
532
+ "fromDate": None,
533
+ "toDate": None
534
+ },
535
+ "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
536
+ }
537
+
538
+ @query_func
539
+ def fetch_all_installations(self):
540
+ """Fetch installations"""
541
+ return {
542
+ "operationName": "fetchAllInstallations",
543
+ "variables": {
544
+ "email": self._username},
545
+ "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
546
+ }
547
+
548
+ @query_func
549
+ def firmware(self,
550
+ giid: VariableTypes.Giid=None):
551
+ """Get firmware information"""
552
+ assert giid or self._giid, "Set default giid or pass explicit"
553
+ return {
554
+ "operationName": "Firmware",
555
+ "variables": {
556
+ "giid": giid or self._giid
557
+ },
558
+ "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
559
+ }
560
+
561
+ @query_func
562
+ def guardian_sos(self):
563
+ """Guardian SOS"""
564
+ return {
565
+ "operationName": "GuardianSos",
566
+ "variables": {},
567
+ "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
568
+ }
569
+
570
+ @query_func
571
+ def is_guardian_activated(self,
572
+ giid: VariableTypes.Giid=None):
573
+ """Is guardian activated"""
574
+ assert giid or self._giid, "Set default giid or pass explicit"
575
+ return {
576
+ "operationName": "IsGuardianActivated",
577
+ "variables": {
578
+ "giid": giid or self._giid,
579
+ "featureName": "GUARDIAN"},
580
+ "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
581
+ }
582
+
583
+ @query_func
584
+ def permissions(self,
585
+ giid: VariableTypes.Giid=None):
586
+ """Permissions"""
587
+ assert giid or self._giid, "Set default giid or pass explicit"
588
+ return {
589
+ "operationName": "Permissions",
590
+ "variables": {
591
+ "giid": giid or self._giid,
592
+ "email": self._username},
593
+ "query": "query Permissions($giid: String!, $email: String!) {\n permissions(giid: $giid, email: $email) {\n accountPermissionsHash\n name\n __typename\n }\n}\n", # noqa: E501
594
+ }
595
+
596
+ @query_func
597
+ def poll_arm_state(self,
598
+ transaction_id: VariableTypes.TransactionId,
599
+ future_state: VariableTypes.ArmFutureState,
600
+ giid: VariableTypes.Giid=None):
601
+ """Poll arm state"""
602
+ assert giid or self._giid, "Set default giid or pass explicit"
603
+ return {
604
+ "operationName": "pollArmState",
605
+ "variables": {
606
+ "giid": giid or self._giid,
607
+ "transactionId": transaction_id,
608
+ "futureState": future_state},
609
+ "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
610
+ }
611
+
612
+ @query_func
613
+ def poll_lock_state(self,
614
+ transaction_id: VariableTypes.TransactionId,
615
+ device_label: VariableTypes.DeviceLabel,
616
+ future_state: VariableTypes.LockFutureState,
617
+ giid: VariableTypes.Giid=None):
618
+ """Poll lock state"""
619
+ assert giid or self._giid, "Set default giid or pass explicit"
620
+ return {
621
+ "operationName": "pollLockState",
622
+ "variables": {
623
+ "giid": giid or self._giid,
624
+ "transactionId": transaction_id,
625
+ "deviceLabel": device_label,
626
+ "futureState": future_state},
627
+ "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
628
+ }
629
+
630
+ @query_func
631
+ def remaining_sms(self,
632
+ giid: VariableTypes.Giid=None):
633
+ """Get remaing number of SMS"""
634
+ assert giid or self._giid, "Set default giid or pass explicit"
635
+ return {
636
+ "operationName": "RemainingSms",
637
+ "variables": {
638
+ "giid": giid or self._giid},
639
+ "query": "query RemainingSms($giid: String!) {\n installation(giid: $giid) {\n remainingSms\n __typename\n }\n}\n", # noqa: E501
640
+ }
641
+
642
+ @query_func
643
+ def smart_button(self,
644
+ giid: VariableTypes.Giid=None):
645
+ """Get smart button state"""
646
+ assert giid or self._giid, "Set default giid or pass explicit"
647
+ return {
648
+ "operationName": "SmartButton",
649
+ "variables": {
650
+ "giid": giid or self._giid},
651
+ "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
652
+ }
653
+
654
+ @query_func
655
+ def smart_lock(self,
656
+ giid: VariableTypes.Giid=None):
657
+ """Get smart lock state"""
658
+ assert giid or self._giid, "Set default giid or pass explicit"
659
+ return {
660
+ "operationName": "SmartLock",
661
+ "variables": {
662
+ "giid": giid or self._giid},
663
+ "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
664
+ }
665
+
666
+ @query_func
667
+ def set_smartplug(self,
668
+ device_label: VariableTypes.DeviceLabel,
669
+ state: bool,
670
+ giid: VariableTypes.Giid=None):
671
+ """Set state of smart plug"""
672
+ assert giid or self._giid, "Set default giid or pass explicit"
673
+ return {
674
+ "operationName": "UpdateState",
675
+ "variables": {
676
+ "giid": giid or self._giid,
677
+ "deviceLabel": device_label,
678
+ "state": state},
679
+ "query": "mutation UpdateState($giid: String!, $deviceLabel: String!, $state: Boolean!) {\n SmartPlugSetState(giid: $giid, input: [{deviceLabel: $deviceLabel, state: $state}])}", # noqa: E501
680
+ }
681
+
682
+ @query_func
683
+ def smartplug(self,
684
+ device_label: VariableTypes.DeviceLabel,
685
+ giid: VariableTypes.Giid=None):
686
+ """Read status of a single smart plug"""
687
+ assert giid or self._giid, "Set default giid or pass explicit"
688
+ return {
689
+ "operationName": "SmartPlug",
690
+ "variables": {
691
+ "giid": giid or self._giid,
692
+ "deviceLabel": device_label},
693
+ "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
694
+ }
695
+
696
+ @query_func
697
+ def smartplugs(self,
698
+ giid: VariableTypes.Giid=None):
699
+ """Read status of all smart plugs"""
700
+ assert giid or self._giid, "Set default giid or pass explicit"
701
+ return {
702
+ "operationName": "SmartPlug",
703
+ "variables": {
704
+ "giid": giid or self._giid},
705
+ "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
706
+ }
707
+
708
+ @query_func
709
+ def user_trackings(self,
710
+ giid: VariableTypes.Giid=None):
711
+ """Read user tracking status"""
712
+ assert giid or self._giid, "Set default giid or pass explicit"
713
+ return {
714
+ "operationName": "userTrackings",
715
+ "variables": {
716
+ "giid": giid or self._giid},
717
+ "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
718
+ }
719
+
720
+ @query_func
721
+ def cameras(self,
722
+ giid: VariableTypes.Giid=None):
723
+ """Get cameras state"""
724
+ assert giid or self._giid, "Set default giid or pass explicit"
725
+ return {
726
+ "operationName": "Camera",
727
+ "variables": {
728
+ "all": True,
729
+ "giid": giid or self._giid},
730
+ "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
731
+ }
732
+
733
+ @query_func
734
+ def cameras_last_image(self,
735
+ giid: VariableTypes.Giid=None):
736
+ """Get cameras last image"""
737
+ assert giid or self._giid, "Set default giid or pass explicit"
738
+ return {
739
+ "variables": {
740
+ "giid": giid or self._giid},
741
+ "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
742
+ }
743
+
744
+ @query_func
745
+ def cameras_image_series(self,
746
+ limit=50,
747
+ offset=0,
748
+ giid: VariableTypes.Giid=None):
749
+ """Get the cameras image series"""
750
+ assert giid or self._giid, "Set default giid or pass explicit"
751
+ return {
752
+ "operationName": "GQL_CCCP_SearchMedia",
753
+ "variables": {
754
+ "giid": giid or self._giid,
755
+ "limit": limit,
756
+ "offset": offset},
757
+ "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}
758
+ }
759
+
760
+ @query_func
761
+ def camera_get_request_id(self,
762
+ device_label: VariableTypes.DeviceLabel,
763
+ giid: VariableTypes.Giid=None):
764
+ """Get requestId for camera_capture"""
765
+ assert giid or self._giid, "Set default giid or pass explicit"
766
+ return {
767
+ "variables": {
768
+ "deviceIdentifier": "RandomString",
769
+ "deviceLabel": device_label,
770
+ "giid": giid or self._giid,
771
+ "resolution": "high"},
772
+ "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
773
+ }
774
+
775
+ @query_func
776
+ def camera_capture(self,
777
+ device_label: VariableTypes.DeviceLabel,
778
+ request_id: VariableTypes.RequestId,
779
+ giid: VariableTypes.Giid=None):
780
+ """Capture a new image from a camera"""
781
+ assert giid or self._giid, "Set default giid or pass explicit"
782
+ return {
783
+ "variables": {
784
+ "deviceLabel": device_label,
785
+ "giid": giid or self._giid,
786
+ "requestId": request_id},
787
+ "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
788
+ }
789
+
790
+ def download_image(self, image_url, file_name):
791
+ """Download image from url"""
792
+ try:
793
+ response = requests.get(image_url, stream=True)
794
+ except requests.exceptions.RequestException as ex:
795
+ raise RequestError("Failed to get image") from ex
796
+ with open(file_name, 'wb') as image_file:
797
+ for chunk in response.iter_content(chunk_size=1024):
798
+ if chunk:
799
+ image_file.write(chunk)
@@ -0,0 +1,31 @@
1
+ Metadata-Version: 2.4
2
+ Name: vsure
3
+ Version: 2.6.9
4
+ Summary: Read and change status of verisure devices through mypages.
5
+ Home-page: http://github.com/persandstrom/python-verisure
6
+ Author: Per Sandstrom
7
+ Author-email: per.j.sandstrom@gmail.com
8
+ License: MIT
9
+ Keywords: home automation verisure
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Topic :: Home Automation
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3.6
15
+ Classifier: Programming Language :: Python :: 3.7
16
+ Classifier: Programming Language :: Python :: 3.8
17
+ License-File: LICENSE
18
+ Requires-Dist: requests>=2.25.1
19
+ Requires-Dist: click>=8.0.0a1
20
+ Dynamic: author
21
+ Dynamic: author-email
22
+ Dynamic: classifier
23
+ Dynamic: description
24
+ Dynamic: home-page
25
+ Dynamic: keywords
26
+ Dynamic: license
27
+ Dynamic: license-file
28
+ Dynamic: requires-dist
29
+ Dynamic: summary
30
+
31
+ A python3 module for reading and changing status of verisure devices through mypages.
@@ -0,0 +1,10 @@
1
+ verisure/__init__.py,sha256=PuC4CPVYgYz4aeowwyR56uIv9-_0QWqho122TVs-JCI,518
2
+ verisure/__main__.py,sha256=yErcsdJP6LRsmk6ymBpyFJ7a1BFKWG5ca-anZ5Mx_ZY,5096
3
+ verisure/session.py,sha256=MGJiWEfbZUE74rlBVo4iSaWKvhuWHPkxRQ4-_3IJTpQ,37673
4
+ vsure-2.6.9.dist-info/licenses/LICENSE,sha256=vNvDUDugNXOM-bsuwrS_GvKmKEhdSJ08PY9jZT5tCWA,1082
5
+ vsure-2.6.9.dist-info/METADATA,sha256=zFk6PSuNjF_zkvTTzIybc2EZubyHhjn-B-W95o2x_XM,977
6
+ vsure-2.6.9.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
7
+ vsure-2.6.9.dist-info/entry_points.txt,sha256=qlRnwjOscU3jpgC8rgPMyQ_OtUacPXz6RguLBTfmoSo,48
8
+ vsure-2.6.9.dist-info/top_level.txt,sha256=-oNvWzMpVusfSzs0C1_43wC26DI9Bi7W6j-dGF65YGM,9
9
+ vsure-2.6.9.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
10
+ vsure-2.6.9.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ vsure = verisure.__main__:cli
@@ -0,0 +1,22 @@
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
+
@@ -0,0 +1 @@
1
+ verisure
@@ -0,0 +1 @@
1
+