loqedAPI 2.1.6__tar.gz → 2.1.7__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: loqedAPI
3
- Version: 2.1.6
3
+ Version: 2.1.7
4
4
  Summary: Python package to use the Loqed Smart Door Lock APIs in a local network. To be used by Home Assistant.
5
5
  Home-page: https://github.com/cpolhout/loqedAPI
6
6
  Author: Casper Polhout
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = loqedAPI
3
- version = 2.1.6
3
+ version = 2.1.7
4
4
  author = Casper Polhout
5
5
  author_email = cpolhout@gmail.com
6
6
  description = Python package to use the Loqed Smart Door Lock APIs in a local network. To be used by Home Assistant.
@@ -0,0 +1,24 @@
1
+ from setuptools import find_packages, setup
2
+
3
+ setup(
4
+ name="loqedAPI",
5
+ version="2.1.7",
6
+ description="Python package to use the Loqed Smart Door Lock APIs in a local network. To be used by Home Assistant.",
7
+ url="https://github.com/cpolhout/loqedAPI",
8
+ author="Casper Polhout",
9
+ author_email="cpolhout@gmail.com",
10
+ license="BSD 2-clause",
11
+ packages=find_packages(exclude=["tests", "generator"]),
12
+ install_requires=["aiohttp", "async-timeout"],
13
+ classifiers=[
14
+ "Development Status :: 1 - Planning",
15
+ "Intended Audience :: Science/Research",
16
+ "License :: OSI Approved :: BSD License",
17
+ "Operating System :: POSIX :: Linux",
18
+ "Programming Language :: Python :: 2",
19
+ "Programming Language :: Python :: 2.7",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.4",
22
+ "Programming Language :: Python :: 3.5",
23
+ ],
24
+ )
@@ -0,0 +1,19 @@
1
+ from aiohttp import ClientSession
2
+ from .loqed import AbstractAPIClient
3
+ from .urls import CLOUD_BASE_URL
4
+
5
+
6
+ class CloudAPIClient(AbstractAPIClient):
7
+ def __init__(self, websession: ClientSession, token: str | None = None):
8
+ """Initialize the auth."""
9
+ super().__init__(websession, CLOUD_BASE_URL, token)
10
+
11
+
12
+ class LoqedCloudAPI:
13
+ def __init__(self, apiclient: CloudAPIClient):
14
+ self.apiclient = apiclient
15
+
16
+ async def async_get_locks(self):
17
+ resp = await self.apiclient.request("get", "api/locks/")
18
+ resp.raise_for_status()
19
+ return await resp.json()
@@ -0,0 +1,346 @@
1
+ """
2
+ Loqed API integration
3
+ This is the local API integration. For the remote integration look at LoqedAPI_internet
4
+ """
5
+
6
+ from enum import Enum
7
+ import logging
8
+ from typing import List
9
+ import os
10
+ import json
11
+ from abc import abstractmethod
12
+ from asyncio import get_event_loop
13
+ from aiohttp import ClientSession, ClientResponse
14
+ from typing import List
15
+ import struct
16
+ import time
17
+ import hmac
18
+ import base64
19
+ import hashlib
20
+ import urllib
21
+
22
+ logging.basicConfig(level=os.environ.get("LOGLEVEL", "INFO"))
23
+ _LOGGER = logging.getLogger(__name__)
24
+
25
+
26
+ class Action(Enum):
27
+ OPEN = 1
28
+ UNLOCK = 2
29
+ LOCK = 3
30
+
31
+
32
+ class AbstractAPIClient:
33
+ """Client to handle API calls."""
34
+
35
+ def __init__(self, websession: ClientSession, host: str, token: str | None = None):
36
+ """Initialize the client."""
37
+ self.websession = websession
38
+ self.host = host
39
+ self.token = token
40
+ _LOGGER.debug("API client created")
41
+
42
+ async def request(self, method, url, **kwargs) -> ClientResponse:
43
+ """Make a request."""
44
+ headers = kwargs.pop("headers", {})
45
+ headers = dict(headers)
46
+
47
+ if self.token:
48
+ headers["authorization"] = f"Bearer {self.token}"
49
+
50
+ return await self.websession.request(
51
+ method,
52
+ f"{self.host}/{url}",
53
+ **kwargs,
54
+ headers=headers,
55
+ )
56
+
57
+
58
+ class APIClient(AbstractAPIClient):
59
+ def __init__(self, websession: ClientSession, host: str):
60
+ """Initialize the auth."""
61
+ super().__init__(websession, host)
62
+
63
+
64
+ class Lock:
65
+ """Class that represents a Lock object in the LoqedAPI."""
66
+
67
+ def __init__(
68
+ self,
69
+ raw_data: dict,
70
+ secret: str,
71
+ bridgekey: str,
72
+ key_id: int,
73
+ name: str,
74
+ apiclient: APIClient,
75
+ ):
76
+ """Initialize a lock object."""
77
+ self.raw_data = raw_data
78
+ self.secret = secret
79
+ self.bridgekey = bridgekey
80
+ self.key_id = key_id
81
+ self.apiclient = apiclient
82
+ self.webhooks = {}
83
+ self.name = name
84
+ self.bolt_state = raw_data["bolt_state"]
85
+ self.battery_percentage = raw_data["battery_percentage"]
86
+ self.last_key_id = ""
87
+ self.waitforstate = False
88
+ self.last_event = ""
89
+ self.id = self.raw_data["bridge_mac_wifi"]
90
+
91
+ @property
92
+ def battery_voltage(self) -> str:
93
+ """Return the ID of the lock."""
94
+ return self.raw_data["battery_voltage"]
95
+
96
+ @property
97
+ def wifi_strength(self) -> str:
98
+ """Return the ID of the lock."""
99
+ return self.raw_data["wifi_strength"]
100
+
101
+ @property
102
+ def ble_strength(self) -> str:
103
+ """Return the ID of the lock."""
104
+ return self.raw_data["ble_strength"]
105
+
106
+ @property
107
+ def battery_type(self) -> str:
108
+ """Return the name of the lock."""
109
+ return self.raw_data["battery_type"]
110
+
111
+ def getcommand(self, action: Action) -> str:
112
+ """Creates a hashed command to send to the loqed lock"""
113
+ messageId_bin = struct.pack("Q", 0)
114
+ protocol_bin = struct.pack("B", 2)
115
+ command_type_bin = struct.pack("B", 7)
116
+ local_key_id_bin = struct.pack("B", self.key_id)
117
+ device_id_bin = struct.pack("B", 1)
118
+ action_bin = struct.pack("B", action.value)
119
+ now = int(time.time())
120
+ timenow_bin = now.to_bytes(8, "big", signed=False)
121
+ local_generated_binary_hash = (
122
+ protocol_bin
123
+ + command_type_bin
124
+ + timenow_bin
125
+ + local_key_id_bin
126
+ + device_id_bin
127
+ + action_bin
128
+ )
129
+ hm = hmac.new(
130
+ base64.b64decode(self.secret), local_generated_binary_hash, hashlib.sha256
131
+ ).digest()
132
+ command = (
133
+ messageId_bin
134
+ + protocol_bin
135
+ + command_type_bin
136
+ + timenow_bin
137
+ + hm
138
+ + local_key_id_bin
139
+ + device_id_bin
140
+ + action_bin
141
+ )
142
+ return urllib.parse.quote(base64.b64encode(command).decode("ascii"))
143
+
144
+ async def sendcommand(self, type: Action) -> ClientResponse:
145
+ """Sends the hashed command to the loqed lock"""
146
+ command = self.getcommand(type)
147
+ resp = await self.apiclient.request(
148
+ "get", f"to_lock?command_signed_base64={command}"
149
+ )
150
+ resp.raise_for_status()
151
+ return resp
152
+
153
+ async def open(self) -> ClientResponse:
154
+ "Open the lock"
155
+ return await self.sendcommand(Action.OPEN)
156
+
157
+ async def lock(self) -> ClientResponse:
158
+ "Set night-lock"
159
+ return await self.sendcommand(Action.LOCK)
160
+
161
+ async def unlock(self) -> ClientResponse:
162
+ "Set day-lock"
163
+ return await self.sendcommand(Action.UNLOCK)
164
+
165
+ async def update(self):
166
+ "Update status"
167
+ resp = await self.apiclient.request("get", "status")
168
+ resp.raise_for_status()
169
+ json_data = await resp.json(content_type="text/html")
170
+ self.raw_data = json_data
171
+ self.bolt_state = self.raw_data["bolt_state"]
172
+ return json_data
173
+
174
+ async def getWebhooks(self):
175
+ "Get webhooks for this lock"
176
+ now = int(time.time())
177
+ hash = hashlib.sha256(
178
+ now.to_bytes(8, "big", signed=False) + base64.b64decode(self.bridgekey)
179
+ ).hexdigest()
180
+ headers = {"TIMESTAMP": str(now), "HASH": hash}
181
+ resp = await self.apiclient.request("get", f"webhooks", headers=headers)
182
+ resp.raise_for_status()
183
+ json_data = await resp.json(content_type="text/html")
184
+ _LOGGER.debug("get Webhooks Response: %s", str(json_data))
185
+ self.webhooks = {}
186
+ return json_data
187
+
188
+ async def registerWebhook(self, url: str, flags: int = 511) -> str:
189
+ "Register webhook for this lock subscribed to all events, first checks if its not already there"
190
+ webhooks = await self.getWebhooks()
191
+ for hook in webhooks:
192
+ if hook["url"] == url:
193
+ return "EXISTS ALREADY"
194
+ now = int(time.time())
195
+ hash = hashlib.sha256(
196
+ url.encode()
197
+ + flags.to_bytes(4, "big")
198
+ + now.to_bytes(8, "big", signed=False)
199
+ + base64.b64decode(self.bridgekey)
200
+ ).hexdigest()
201
+ headers = {"TIMESTAMP": str(now), "HASH": hash}
202
+ json = {
203
+ "url": url,
204
+ "trigger_state_changed_open": flags >> 0 & 1,
205
+ "trigger_state_changed_latch": flags >> 1 & 1,
206
+ "trigger_state_changed_night_lock": flags >> 2 & 1,
207
+ "trigger_state_changed_unknown": flags >> 3 & 1,
208
+ "trigger_state_goto_open": flags >> 4 & 1,
209
+ "trigger_state_goto_latch": flags >> 5 & 1,
210
+ "trigger_state_goto_night_lock": flags >> 6 & 1,
211
+ "trigger_battery": flags >> 7 & 1,
212
+ "trigger_online_status": flags >> 8 & 1,
213
+ }
214
+ resp = await self.apiclient.request(
215
+ "post", "webhooks", json=json, headers=headers
216
+ )
217
+ resp.raise_for_status()
218
+ _LOGGER.debug("Create webhook Response: %s", await resp.text())
219
+ return "CREATED"
220
+
221
+ async def deleteWebhook(self, id: int) -> None:
222
+ "Delete webhook for this lock"
223
+ now = int(time.time())
224
+ hash = hashlib.sha256(
225
+ id.to_bytes(8, "big", signed=False)
226
+ + now.to_bytes(8, "big", signed=False)
227
+ + base64.b64decode(self.bridgekey)
228
+ ).hexdigest()
229
+ headers = {"TIMESTAMP": str(now), "HASH": hash}
230
+ resp = await self.apiclient.request("delete", f"webhooks/{id}", headers=headers)
231
+ resp.raise_for_status()
232
+ _LOGGER.debug("Delete webhook Response: %s", await resp.text())
233
+
234
+ async def receiveWebhook(self, body, hash: str, timestamp: str):
235
+ "Received webhook with hash. This method checks the hash."
236
+ timestamp = int(timestamp)
237
+ now = int(time.time())
238
+ data = json.loads(body) if body else {}
239
+ if data == {}:
240
+ error = {
241
+ "error": "Received invalid data from LOQED. Data needs to be formatted as JSON",
242
+ "body": body,
243
+ "hash": hash,
244
+ "timestamp": timestamp,
245
+ "now": now,
246
+ }
247
+ _LOGGER.error("ERROR: %s", error)
248
+ return error
249
+
250
+ if not isinstance(data, dict):
251
+ error = {
252
+ "error": "Received invalid data from LOQED. Data needs to be a dictionary",
253
+ "body": body,
254
+ "hash": hash,
255
+ "timestamp": timestamp,
256
+ "now": now,
257
+ }
258
+ _LOGGER.error("ERROR: %s", error)
259
+ return error
260
+ _LOGGER.debug(
261
+ " Received timestamp: %s , current timestamp: %s", str(timestamp), str(now)
262
+ )
263
+ # check timestamp within 10 seconds
264
+ if (now - timestamp > 10) or (timestamp - now > 10):
265
+ error = {
266
+ "error": "Timestamp incorrect, possible replaying",
267
+ "body": body,
268
+ "hash": hash,
269
+ "timestamp": timestamp,
270
+ "now": now,
271
+ }
272
+ _LOGGER.error("ERROR: %s", error)
273
+ return error
274
+ chash = hashlib.sha256(
275
+ body.encode()
276
+ + timestamp.to_bytes(8, "big", signed=False)
277
+ + base64.b64decode(self.bridgekey)
278
+ ).hexdigest()
279
+ _LOGGER.debug("Received hash: %s , calculated hash: %s", hash, chash)
280
+ if chash != hash:
281
+ error = {
282
+ "error": "Hash incorrect",
283
+ "body": body,
284
+ "hash": hash,
285
+ "calculated_hash": chash,
286
+ "timestamp": timestamp,
287
+ "now": now,
288
+ }
289
+ _LOGGER.error("ERROR: %s", error)
290
+ return error
291
+ if "battery_percentage" in data:
292
+ self.battery_percentage = data["battery_percentage"]
293
+ elif "ble_strength" in data:
294
+ self.raw_data["ble_strength"] = data["ble_strength"]
295
+ else:
296
+ self.last_event = data["event_type"].strip().lower()
297
+ # BOLT STATE CHANGE
298
+ if self.last_event.split("_")[0] == "state":
299
+ self.bolt_state = str.replace(self.last_event, "state_changed_", "")
300
+ else:
301
+ # BOLT IS CHANGING TO A STATE
302
+ if "night_lock" in self.last_event:
303
+ self.bolt_state = "locking"
304
+ if "open" in self.last_event:
305
+ self.bolt_state = "opening"
306
+ if "latch" in self.last_event:
307
+ self.bolt_state = "unlocking"
308
+ self.last_key_id = data["key_local_id"]
309
+ return data
310
+
311
+ async def updateState(self, state):
312
+ self.bolt_state = state
313
+
314
+
315
+ class LoqedAPI:
316
+ def __init__(self, apiclient: APIClient):
317
+ """Initialize the API and store the auth so we can make requests."""
318
+ self.apiclient = apiclient
319
+
320
+ async def async_get_lock_details(self):
321
+ """Return lock_info"""
322
+ resp = await self.apiclient.request("get", "status")
323
+ resp.raise_for_status()
324
+ _LOGGER.debug("Response get lock details: %s", await resp.text())
325
+ json_data = await resp.json(content_type="text/html")
326
+ return json_data
327
+
328
+ async def async_get_lock(
329
+ self, secret: str, bridgekey: str, key_id: int, name: str, json_data=None
330
+ ) -> Lock:
331
+ """Return the locks with lock-data"""
332
+ if not json_data:
333
+ resp = await self.apiclient.request("get", "status")
334
+ json_data = await resp.json(content_type="text/html")
335
+ return Lock(json_data, secret, bridgekey, key_id, name, self.apiclient)
336
+
337
+
338
+ """Loqed: Exceptions"""
339
+
340
+
341
+ class LoqedException(BaseException):
342
+ """Raise this when something is off."""
343
+
344
+
345
+ class LoqedAuthenticationException(LoqedException):
346
+ """Raise this when there is an authentication issue."""
@@ -0,0 +1,7 @@
1
+ """
2
+ Loqed URLs
3
+
4
+
5
+
6
+ """
7
+ CLOUD_BASE_URL = "https://integrations.production.loqed.com"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: loqedAPI
3
- Version: 2.1.6
3
+ Version: 2.1.7
4
4
  Summary: Python package to use the Loqed Smart Door Lock APIs in a local network. To be used by Home Assistant.
5
5
  Home-page: https://github.com/cpolhout/loqedAPI
6
6
  Author: Casper Polhout
@@ -5,9 +5,11 @@ setup.cfg
5
5
  setup.py
6
6
  src/loqedAPI/__init__.py
7
7
  src/loqedAPI/apiclient.py
8
+ src/loqedAPI/cloud_loqed.py
8
9
  src/loqedAPI/exceptions.py
9
10
  src/loqedAPI/local_loqed.py
10
11
  src/loqedAPI/loqed.py
12
+ src/loqedAPI/urls.py
11
13
  src/loqedAPI.egg-info/PKG-INFO
12
14
  src/loqedAPI.egg-info/SOURCES.txt
13
15
  src/loqedAPI.egg-info/dependency_links.txt
loqedAPI-2.1.6/setup.py DELETED
@@ -1,24 +0,0 @@
1
- from setuptools import find_packages, setup
2
-
3
- setup(
4
- name='loqedAPI',
5
- version='2.1.6',
6
- description='Python package to use the Loqed Smart Door Lock APIs in a local network. To be used by Home Assistant.',
7
- url='https://github.com/cpolhout/loqedAPI',
8
- author='Casper Polhout',
9
- author_email='cpolhout@gmail.com',
10
- license='BSD 2-clause',
11
- packages=find_packages(exclude=["tests", "generator"]),
12
- install_requires=["aiohttp", "async-timeout"],
13
- classifiers=[
14
- 'Development Status :: 1 - Planning',
15
- 'Intended Audience :: Science/Research',
16
- 'License :: OSI Approved :: BSD License',
17
- 'Operating System :: POSIX :: Linux',
18
- 'Programming Language :: Python :: 2',
19
- 'Programming Language :: Python :: 2.7',
20
- 'Programming Language :: Python :: 3',
21
- 'Programming Language :: Python :: 3.4',
22
- 'Programming Language :: Python :: 3.5',
23
- ],
24
- )
@@ -1,284 +0,0 @@
1
- """
2
- Loqed API integration
3
- This is the local API integration. For the remote integration look at LoqedAPI_internet
4
- """
5
-
6
- from enum import Enum
7
- import logging
8
- from typing import List
9
- import os
10
- import json
11
- from abc import abstractmethod
12
- from asyncio import get_event_loop
13
- from aiohttp import ClientSession, ClientResponse
14
- from typing import List
15
- import struct
16
- import time
17
- import hmac
18
- import base64
19
- import hashlib
20
- import urllib
21
-
22
- logging.basicConfig(level=os.environ.get("LOGLEVEL", "INFO"))
23
- _LOGGER = logging.getLogger(__name__)
24
-
25
- class Action(Enum):
26
- OPEN = 1
27
- UNLOCK = 2
28
- LOCK = 3
29
-
30
- class AbstractAPIClient():
31
- """Client to handle API calls."""
32
-
33
- def __init__(self, websession: ClientSession, host: str, token: str | None = None):
34
- """Initialize the client."""
35
- self.websession = websession
36
- self.host = host
37
- self.token = token
38
- _LOGGER.debug("API client created")
39
-
40
- async def request(self, method, url, **kwargs) -> ClientResponse:
41
- """Make a request."""
42
- headers = kwargs.get("headers")
43
-
44
- if headers is None:
45
- headers = {}
46
- else:
47
- headers = dict(headers)
48
-
49
- if self.token:
50
- headers["authorization"] = f"Bearer {self.token}"
51
-
52
- return await self.websession.request(
53
- method, f"{self.host}/{url}", **kwargs, headers=headers,
54
- )
55
-
56
- class APIClient(AbstractAPIClient):
57
- def __init__(self, websession: ClientSession, host: str, token: str | None = None):
58
- """Initialize the auth."""
59
- super().__init__(websession, host, token)
60
-
61
-
62
- class Lock:
63
- """Class that represents a Lock object in the LoqedAPI."""
64
-
65
- def __init__(self, raw_data: dict, secret: str, bridgekey: str, key_id: int, name: str, apiclient: APIClient):
66
- """Initialize a lock object."""
67
- self.raw_data = raw_data
68
- self.secret = secret
69
- self.bridgekey = bridgekey
70
- self.key_id=key_id
71
- self.apiclient = apiclient
72
- self.webhooks = {}
73
- self.name = name
74
- self.bolt_state= raw_data["bolt_state"]
75
- self.battery_percentage = raw_data["battery_percentage"]
76
- self.last_key_id = ""
77
- self.waitforstate = False
78
- self.last_event = ""
79
- self.id = self.raw_data["bridge_mac_wifi"]
80
-
81
- @property
82
- def battery_voltage(self) -> str:
83
- """Return the ID of the lock."""
84
- return self.raw_data["battery_voltage"]
85
-
86
- @property
87
- def wifi_strength(self) -> str:
88
- """Return the ID of the lock."""
89
- return self.raw_data["wifi_strength"]
90
-
91
- @property
92
- def ble_strength(self) -> str:
93
- """Return the ID of the lock."""
94
- return self.raw_data["ble_strength"]
95
-
96
- @property
97
- def battery_type(self) -> str:
98
- """Return the name of the lock."""
99
- return self.raw_data["battery_type"]
100
-
101
- def getcommand(self, action:Action) -> str:
102
- """Creates a hashed command to send to the loqed lock"""
103
- messageId_bin = struct.pack("Q", 0)
104
- protocol_bin = struct.pack("B", 2)
105
- command_type_bin = struct.pack("B", 7)
106
- local_key_id_bin = struct.pack("B", self.key_id)
107
- device_id_bin = struct.pack("B", 1)
108
- action_bin = struct.pack("B", action.value)
109
- now=int(time.time())
110
- timenow_bin=now.to_bytes(8, 'big', signed=False)
111
- local_generated_binary_hash = protocol_bin + command_type_bin + timenow_bin + local_key_id_bin + device_id_bin + action_bin
112
- hm=hmac.new(base64.b64decode(self.secret), local_generated_binary_hash,hashlib.sha256).digest()
113
- command = messageId_bin + protocol_bin + command_type_bin + timenow_bin + hm + local_key_id_bin + device_id_bin + action_bin
114
- return urllib.parse.quote(base64.b64encode(command).decode("ascii"))
115
-
116
- async def sendcommand(self, type: Action) -> ClientResponse:
117
- """Sends the hashed command to the loqed lock"""
118
- command=self.getcommand(type)
119
- resp = await self.apiclient.request("get", f"to_lock?command_signed_base64={command}")
120
- resp.raise_for_status()
121
- return resp
122
-
123
- async def open(self) -> ClientResponse:
124
- "Open the lock"
125
- return await self.sendcommand(Action.OPEN)
126
-
127
- async def lock(self) -> ClientResponse:
128
- "Set night-lock"
129
- return await self.sendcommand(Action.LOCK)
130
-
131
- async def unlock(self) -> ClientResponse:
132
- "Set day-lock"
133
- return await self.sendcommand(Action.UNLOCK)
134
-
135
- async def update(self):
136
- "Update status"
137
- resp = await self.apiclient.request("get", "status")
138
- resp.raise_for_status()
139
- json_data = await resp.json(content_type='text/html')
140
- self.raw_data=json_data
141
- self.bolt_state=self.raw_data["bolt_state"]
142
- return json_data
143
-
144
- async def getWebhooks(self):
145
- "Get webhooks for this lock"
146
- now=int(time.time())
147
- hash=hashlib.sha256(now.to_bytes(8, 'big', signed=False)+base64.b64decode(self.bridgekey)).hexdigest()
148
- headers = {'TIMESTAMP': str(now), 'HASH': hash}
149
- resp = await self.apiclient.request("get", f"webhooks", headers=headers)
150
- resp.raise_for_status()
151
- json_data = await resp.json(content_type='text/html')
152
- _LOGGER.debug("get Webhooks Response: %s", str(json_data))
153
- self.webhooks={}
154
- return json_data
155
-
156
- async def registerWebhook(self, url: str, flags:int = 511) -> str:
157
- "Register webhook for this lock subscribed to all events, first checks if its not already there"
158
- webhooks=await self.getWebhooks()
159
- for hook in webhooks:
160
- if hook["url"]==url: return "EXISTS ALREADY"
161
- now=int(time.time())
162
- hash=hashlib.sha256(url.encode() + flags.to_bytes(4, 'big') + now.to_bytes(8, 'big', signed=False)+base64.b64decode(self.bridgekey)).hexdigest()
163
- headers = {'TIMESTAMP': str(now), 'HASH': hash}
164
- json = {
165
- "url" : url,
166
- "trigger_state_changed_open" : flags >> 0 & 1,
167
- "trigger_state_changed_latch" : flags >> 1 & 1,
168
- "trigger_state_changed_night_lock" : flags >> 2 & 1,
169
- "trigger_state_changed_unknown" : flags >> 3 & 1,
170
- "trigger_state_goto_open" : flags >> 4 & 1,
171
- "trigger_state_goto_latch" : flags >> 5 & 1,
172
- "trigger_state_goto_night_lock" : flags >> 6 & 1,
173
- "trigger_battery" : flags >> 7 & 1,
174
- "trigger_online_status" : flags >> 8 & 1
175
- }
176
- resp = await self.apiclient.request("post", "webhooks", json=json, headers=headers)
177
- resp.raise_for_status()
178
- _LOGGER.debug("Create webhook Response: %s", await resp.text())
179
- return "CREATED"
180
-
181
- async def deleteWebhook(self, id: int) -> None:
182
- "Delete webhook for this lock"
183
- now=int(time.time())
184
- hash=hashlib.sha256(id.to_bytes(8, 'big', signed=False) + now.to_bytes(8, 'big', signed=False)+base64.b64decode(self.bridgekey)).hexdigest()
185
- headers = {'TIMESTAMP': str(now), 'HASH': hash}
186
- resp = await self.apiclient.request("delete", f"webhooks/{id}", headers=headers)
187
- resp.raise_for_status()
188
- _LOGGER.debug("Delete webhook Response: %s", await resp.text())
189
-
190
- async def receiveWebhook(self, body, hash: str, timestamp: str):
191
- "Received webhook with hash. This method checks the hash."
192
- timestamp=int(timestamp)
193
- now=int(time.time())
194
- data = json.loads(body) if body else {}
195
- if data=={}:
196
- error={"error": "Received invalid data from LOQED. Data needs to be formatted as JSON", "body": body, "hash": hash, "timestamp": timestamp, "now": now}
197
- _LOGGER.error(
198
- "ERROR: %s",error
199
- )
200
- return error
201
-
202
- if not isinstance(data, dict):
203
- error={"error": "Received invalid data from LOQED. Data needs to be a dictionary", "body": body, "hash": hash, "timestamp": timestamp, "now": now}
204
- _LOGGER.error(
205
- "ERROR: %s",error
206
- )
207
- return error
208
- _LOGGER.debug(" Received timestamp: %s , current timestamp: %s", str(timestamp), str(now))
209
- # check timestamp within 10 seconds
210
- if (now-timestamp>10) or (timestamp-now>10):
211
- error={"error": "Timestamp incorrect, possible replaying", "body": body, "hash": hash, "timestamp": timestamp, "now": now}
212
- _LOGGER.error(
213
- "ERROR: %s",error
214
- )
215
- return error
216
- chash=hashlib.sha256(body.encode() + timestamp.to_bytes(8, 'big', signed=False)+base64.b64decode(self.bridgekey)).hexdigest()
217
- _LOGGER.debug("Received hash: %s , calculated hash: %s", hash, chash)
218
- if chash!=hash:
219
- error={"error": "Hash incorrect", "body": body, "hash": hash, "calculated_hash": chash, "timestamp": timestamp, "now": now}
220
- _LOGGER.error(
221
- "ERROR: %s",error
222
- )
223
- return error
224
- if "battery_percentage" in data:
225
- self.battery_percentage=data["battery_percentage"]
226
- elif "ble_strength" in data:
227
- self.raw_data["ble_strength"] = data["ble_strength"]
228
- else:
229
- self.last_event=data["event_type"].strip().lower()
230
- # BOLT STATE CHANGE
231
- if self.last_event.split("_")[0]=="state":
232
- self.bolt_state=str.replace(self.last_event,"state_changed_","")
233
- else:
234
- # BOLT IS CHANGING TO A STATE
235
- if "night_lock" in self.last_event: self.bolt_state="locking"
236
- if "open" in self.last_event: self.bolt_state="opening"
237
- if "latch" in self.last_event: self.bolt_state="unlocking"
238
- self.last_key_id=data["key_local_id"]
239
- return data
240
-
241
- async def updateState(self, state):
242
- self.bolt_state=state
243
-
244
- class LoqedAPI:
245
-
246
- def __init__(self, apiclient: APIClient):
247
- """Initialize the API and store the auth so we can make requests."""
248
- self.apiclient = apiclient
249
-
250
- async def async_get_lock_details(self):
251
- """Return lock_info"""
252
- resp = await self.apiclient.request("get", "status")
253
- resp.raise_for_status()
254
- _LOGGER.debug("Response get lock details: %s",await resp.text())
255
- json_data = await resp.json(content_type='text/html')
256
- return json_data
257
-
258
- async def async_get_lock(self, secret: str, bridgekey: str, key_id: int, name: str, json_data=None) -> Lock:
259
- """Return the locks with lock-data"""
260
- if not json_data:
261
- resp = await self.apiclient.request("get", "status")
262
- json_data = await resp.json(content_type='text/html')
263
- return Lock(json_data, secret, bridgekey, key_id, name, self.apiclient)
264
-
265
- class LoqedCloudAPI:
266
- def __init__(self, apiclient: APIClient):
267
- self.apiclient = apiclient
268
-
269
- async def async_get_locks(self):
270
- resp = await self.apiclient.request("get", "/api/locks/")
271
- resp.raise_for_status()
272
- return await resp.json()
273
-
274
- """Loqed: Exceptions"""
275
-
276
-
277
- class LoqedException(BaseException):
278
- """Raise this when something is off."""
279
-
280
-
281
- class LoqedAuthenticationException(LoqedException):
282
- """Raise this when there is an authentication issue."""
283
-
284
-
File without changes
File without changes
File without changes
File without changes