loqedAPI 2.1.14__tar.gz → 2.1.16__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.
- {loqedapi-2.1.14 → loqedapi-2.1.16}/PKG-INFO +9 -2
- {loqedapi-2.1.14 → loqedapi-2.1.16}/README.md +6 -0
- {loqedapi-2.1.14 → loqedapi-2.1.16}/pyproject.toml +5 -3
- {loqedapi-2.1.14 → loqedapi-2.1.16}/src/loqedAPI/loqed.py +25 -2
- {loqedapi-2.1.14 → loqedapi-2.1.16}/src/loqedAPI.egg-info/PKG-INFO +9 -2
- {loqedapi-2.1.14 → loqedapi-2.1.16}/src/loqedAPI.egg-info/requires.txt +2 -1
- loqedapi-2.1.16/src/loqedAPI.egg-info/scm_version.json +8 -0
- {loqedapi-2.1.14 → loqedapi-2.1.16}/tests/test_loqed.py +86 -0
- {loqedapi-2.1.14 → loqedapi-2.1.16}/uv.lock +3 -1
- loqedapi-2.1.14/src/loqedAPI.egg-info/scm_version.json +0 -8
- {loqedapi-2.1.14 → loqedapi-2.1.16}/.github/workflows/ci.yml +0 -0
- {loqedapi-2.1.14 → loqedapi-2.1.16}/.github/workflows/release.yml +0 -0
- {loqedapi-2.1.14 → loqedapi-2.1.16}/.gitignore +0 -0
- {loqedapi-2.1.14 → loqedapi-2.1.16}/.vscode/launch.json +0 -0
- {loqedapi-2.1.14 → loqedapi-2.1.16}/.vscode/settings.json +0 -0
- {loqedapi-2.1.14 → loqedapi-2.1.16}/LICENSE +0 -0
- {loqedapi-2.1.14 → loqedapi-2.1.16}/docs/Makefile +0 -0
- {loqedapi-2.1.14 → loqedapi-2.1.16}/docs/conf.py +0 -0
- {loqedapi-2.1.14 → loqedapi-2.1.16}/docs/index.rst +0 -0
- {loqedapi-2.1.14 → loqedapi-2.1.16}/docs/mpi4py_module.rst +0 -0
- {loqedapi-2.1.14 → loqedapi-2.1.16}/docs/readme.rst +0 -0
- {loqedapi-2.1.14 → loqedapi-2.1.16}/pytest.ini +0 -0
- {loqedapi-2.1.14 → loqedapi-2.1.16}/setup.cfg +0 -0
- {loqedapi-2.1.14 → loqedapi-2.1.16}/src/.DS_Store +0 -0
- {loqedapi-2.1.14 → loqedapi-2.1.16}/src/loqedAPI/__init__.py +0 -0
- {loqedapi-2.1.14 → loqedapi-2.1.16}/src/loqedAPI/apiclient.py +0 -0
- {loqedapi-2.1.14 → loqedapi-2.1.16}/src/loqedAPI/cloud_loqed.py +0 -0
- {loqedapi-2.1.14 → loqedapi-2.1.16}/src/loqedAPI/exceptions.py +0 -0
- {loqedapi-2.1.14 → loqedapi-2.1.16}/src/loqedAPI/local_loqed.py +0 -0
- {loqedapi-2.1.14 → loqedapi-2.1.16}/src/loqedAPI/urls.py +0 -0
- {loqedapi-2.1.14 → loqedapi-2.1.16}/src/loqedAPI.egg-info/SOURCES.txt +0 -0
- {loqedapi-2.1.14 → loqedapi-2.1.16}/src/loqedAPI.egg-info/dependency_links.txt +0 -0
- {loqedapi-2.1.14 → loqedapi-2.1.16}/src/loqedAPI.egg-info/scm_file_list.json +0 -0
- {loqedapi-2.1.14 → loqedapi-2.1.16}/src/loqedAPI.egg-info/top_level.txt +0 -0
- {loqedapi-2.1.14 → loqedapi-2.1.16}/tests/__init__.py +0 -0
- {loqedapi-2.1.14 → loqedapi-2.1.16}/tests/temp.py +0 -0
- {loqedapi-2.1.14 → loqedapi-2.1.16}/tests/test.py +0 -0
- {loqedapi-2.1.14 → loqedapi-2.1.16}/tests/test_cloud_loqed.py +0 -0
- {loqedapi-2.1.14 → loqedapi-2.1.16}/tests/test_local_loqed.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: loqedAPI
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.16
|
|
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
|
Author-email: Casper Polhout <cpolhout@gmail.com>, Mike Woudenberg <mwoudenberg@xebia.com>
|
|
6
6
|
License: BSD 2-Clause License
|
|
@@ -39,13 +39,14 @@ Classifier: Operating System :: OS Independent
|
|
|
39
39
|
Requires-Python: >=3.9
|
|
40
40
|
Description-Content-Type: text/markdown
|
|
41
41
|
License-File: LICENSE
|
|
42
|
-
Requires-Dist: aiohttp
|
|
42
|
+
Requires-Dist: aiohttp>=3.8
|
|
43
43
|
Requires-Dist: async-timeout
|
|
44
44
|
Provides-Extra: dev
|
|
45
45
|
Requires-Dist: pytest; extra == "dev"
|
|
46
46
|
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
47
47
|
Requires-Dist: pytest-cov; extra == "dev"
|
|
48
48
|
Requires-Dist: aioresponses; extra == "dev"
|
|
49
|
+
Requires-Dist: aiohttp<3.14; extra == "dev"
|
|
49
50
|
Dynamic: license-file
|
|
50
51
|
|
|
51
52
|
# Loqed Touch Smart Lock — Python client
|
|
@@ -111,6 +112,12 @@ Notes:
|
|
|
111
112
|
- The library is async-first and uses `aiohttp` for HTTP calls.
|
|
112
113
|
- `APIClient` accepts `base_url` and an optional `token` (for authorization) or you can provide your own `aiohttp.ClientSession`.
|
|
113
114
|
|
|
115
|
+
### Online status
|
|
116
|
+
|
|
117
|
+
`Lock.online` is `True` when the lock is connected to the bridge. It is read from `lock_online` in the `/status` response (refreshed by `Lock.update()`) and kept current by `trigger_online_status` webhooks (bit 8, enabled by the default `registerWebhook` flags), which carry `ble_strength` (`-1` means not connected) and `wifi_strength`.
|
|
118
|
+
|
|
119
|
+
While the lock is offline the bridge reports `battery_percentage` as `-1`, so check `lock.online` before using the battery value.
|
|
120
|
+
|
|
114
121
|
## Testing
|
|
115
122
|
|
|
116
123
|
Run the test suite with pytest (the project uses `pytest-asyncio` and `aioresponses`).
|
|
@@ -61,6 +61,12 @@ Notes:
|
|
|
61
61
|
- The library is async-first and uses `aiohttp` for HTTP calls.
|
|
62
62
|
- `APIClient` accepts `base_url` and an optional `token` (for authorization) or you can provide your own `aiohttp.ClientSession`.
|
|
63
63
|
|
|
64
|
+
### Online status
|
|
65
|
+
|
|
66
|
+
`Lock.online` is `True` when the lock is connected to the bridge. It is read from `lock_online` in the `/status` response (refreshed by `Lock.update()`) and kept current by `trigger_online_status` webhooks (bit 8, enabled by the default `registerWebhook` flags), which carry `ble_strength` (`-1` means not connected) and `wifi_strength`.
|
|
67
|
+
|
|
68
|
+
While the lock is offline the bridge reports `battery_percentage` as `-1`, so check `lock.online` before using the battery value.
|
|
69
|
+
|
|
64
70
|
## Testing
|
|
65
71
|
|
|
66
72
|
Run the test suite with pytest (the project uses `pytest-asyncio` and `aioresponses`).
|
|
@@ -14,9 +14,7 @@ authors = [
|
|
|
14
14
|
]
|
|
15
15
|
requires-python = ">=3.9"
|
|
16
16
|
dependencies = [
|
|
17
|
-
|
|
18
|
-
# signature change in 3.14.0 (new required `stream_writer` kwarg).
|
|
19
|
-
"aiohttp>=3.8,<3.14",
|
|
17
|
+
"aiohttp>=3.8",
|
|
20
18
|
"async-timeout",
|
|
21
19
|
]
|
|
22
20
|
classifiers = [
|
|
@@ -36,7 +34,11 @@ dev = [
|
|
|
36
34
|
"pytest",
|
|
37
35
|
"pytest-asyncio",
|
|
38
36
|
"pytest-cov",
|
|
37
|
+
# aioresponses 0.7.9 (latest) doesn't yet support aiohttp's ClientResponse
|
|
38
|
+
# signature change in 3.14.0 (new required `stream_writer` kwarg), so the
|
|
39
|
+
# test suite is pinned below the runtime package's own aiohttp floor.
|
|
39
40
|
"aioresponses",
|
|
41
|
+
"aiohttp<3.14",
|
|
40
42
|
]
|
|
41
43
|
|
|
42
44
|
[tool.setuptools.packages.find]
|
|
@@ -103,6 +103,14 @@ class Lock:
|
|
|
103
103
|
"""Return the BLE strength of the lock."""
|
|
104
104
|
return self.raw_data["ble_strength"]
|
|
105
105
|
|
|
106
|
+
@property
|
|
107
|
+
def online(self) -> bool:
|
|
108
|
+
"""Return whether the lock is connected to the bridge."""
|
|
109
|
+
if "lock_online" in self.raw_data:
|
|
110
|
+
return bool(int(self.raw_data["lock_online"]))
|
|
111
|
+
# ble_strength of -1 means the lock is not connected to the bridge
|
|
112
|
+
return self.raw_data.get("ble_strength") != -1
|
|
113
|
+
|
|
106
114
|
@property
|
|
107
115
|
def battery_type(self) -> str:
|
|
108
116
|
"""Return the battery type of the lock."""
|
|
@@ -288,15 +296,30 @@ class Lock:
|
|
|
288
296
|
}
|
|
289
297
|
_LOGGER.error("ERROR: %s", error)
|
|
290
298
|
return error
|
|
299
|
+
# ONLINE STATUS
|
|
300
|
+
if "wifi_strength" in data:
|
|
301
|
+
self.raw_data["wifi_strength"] = data["wifi_strength"]
|
|
302
|
+
if "ble_strength" in data:
|
|
303
|
+
self.raw_data["ble_strength"] = data["ble_strength"]
|
|
304
|
+
self.raw_data["lock_online"] = 0 if data["ble_strength"] == -1 else 1
|
|
305
|
+
|
|
291
306
|
if "battery_percentage" in data:
|
|
292
307
|
self.battery_percentage = data["battery_percentage"]
|
|
293
|
-
|
|
294
|
-
|
|
308
|
+
if "battery_type" in data:
|
|
309
|
+
self.raw_data["battery_type"] = data["battery_type"]
|
|
310
|
+
# The bridge reports -1 while the lock is offline, so a real battery
|
|
311
|
+
# reading is what tells us it has reconnected (no ble_strength is sent then)
|
|
312
|
+
if "ble_strength" not in data:
|
|
313
|
+
self.raw_data["lock_online"] = 0 if data["battery_percentage"] == -1 else 1
|
|
314
|
+
elif "ble_strength" in data or "wifi_strength" in data:
|
|
315
|
+
pass
|
|
295
316
|
else:
|
|
296
317
|
self.last_event = data["event_type"].strip().lower()
|
|
297
318
|
# BOLT STATE CHANGE
|
|
298
319
|
if self.last_event.split("_")[0] == "state":
|
|
299
320
|
self.bolt_state = str.replace(self.last_event, "state_changed_", "")
|
|
321
|
+
# The lock itself reported a state, so it is connected to the bridge
|
|
322
|
+
self.raw_data["lock_online"] = 1
|
|
300
323
|
else:
|
|
301
324
|
# GOTO_STATE, only update the state if the target state is unequal to the current state
|
|
302
325
|
if "night_lock" in self.last_event and "night_lock" not in self.bolt_state:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: loqedAPI
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.16
|
|
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
|
Author-email: Casper Polhout <cpolhout@gmail.com>, Mike Woudenberg <mwoudenberg@xebia.com>
|
|
6
6
|
License: BSD 2-Clause License
|
|
@@ -39,13 +39,14 @@ Classifier: Operating System :: OS Independent
|
|
|
39
39
|
Requires-Python: >=3.9
|
|
40
40
|
Description-Content-Type: text/markdown
|
|
41
41
|
License-File: LICENSE
|
|
42
|
-
Requires-Dist: aiohttp
|
|
42
|
+
Requires-Dist: aiohttp>=3.8
|
|
43
43
|
Requires-Dist: async-timeout
|
|
44
44
|
Provides-Extra: dev
|
|
45
45
|
Requires-Dist: pytest; extra == "dev"
|
|
46
46
|
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
47
47
|
Requires-Dist: pytest-cov; extra == "dev"
|
|
48
48
|
Requires-Dist: aioresponses; extra == "dev"
|
|
49
|
+
Requires-Dist: aiohttp<3.14; extra == "dev"
|
|
49
50
|
Dynamic: license-file
|
|
50
51
|
|
|
51
52
|
# Loqed Touch Smart Lock — Python client
|
|
@@ -111,6 +112,12 @@ Notes:
|
|
|
111
112
|
- The library is async-first and uses `aiohttp` for HTTP calls.
|
|
112
113
|
- `APIClient` accepts `base_url` and an optional `token` (for authorization) or you can provide your own `aiohttp.ClientSession`.
|
|
113
114
|
|
|
115
|
+
### Online status
|
|
116
|
+
|
|
117
|
+
`Lock.online` is `True` when the lock is connected to the bridge. It is read from `lock_online` in the `/status` response (refreshed by `Lock.update()`) and kept current by `trigger_online_status` webhooks (bit 8, enabled by the default `registerWebhook` flags), which carry `ble_strength` (`-1` means not connected) and `wifi_strength`.
|
|
118
|
+
|
|
119
|
+
While the lock is offline the bridge reports `battery_percentage` as `-1`, so check `lock.online` before using the battery value.
|
|
120
|
+
|
|
114
121
|
## Testing
|
|
115
122
|
|
|
116
123
|
Run the test suite with pytest (the project uses `pytest-asyncio` and `aioresponses`).
|
|
@@ -28,6 +28,7 @@ def raw_data():
|
|
|
28
28
|
"wifi_strength": "good",
|
|
29
29
|
"ble_strength": "strong",
|
|
30
30
|
"battery_voltage": "3.7",
|
|
31
|
+
"lock_online": 1,
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
@pytest_asyncio.fixture
|
|
@@ -79,6 +80,7 @@ async def test_lock_properties_and_commands(lock):
|
|
|
79
80
|
assert lock.id == "lockid"
|
|
80
81
|
assert lock.wifi_strength == "good"
|
|
81
82
|
assert lock.ble_strength == "strong"
|
|
83
|
+
assert lock.online is True
|
|
82
84
|
|
|
83
85
|
|
|
84
86
|
@pytest.mark.asyncio
|
|
@@ -167,6 +169,90 @@ async def test_receive_webhook_ble_strength(lock, valid_base64):
|
|
|
167
169
|
assert lock.raw_data["ble_strength"] == "weak"
|
|
168
170
|
|
|
169
171
|
|
|
172
|
+
@pytest.mark.asyncio
|
|
173
|
+
async def test_online_falls_back_to_ble_strength(lock):
|
|
174
|
+
del lock.raw_data["lock_online"]
|
|
175
|
+
lock.raw_data["ble_strength"] = -1
|
|
176
|
+
assert lock.online is False
|
|
177
|
+
lock.raw_data["ble_strength"] = -60
|
|
178
|
+
assert lock.online is True
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@pytest.mark.asyncio
|
|
182
|
+
async def test_update_offline(lock, raw_data):
|
|
183
|
+
with aioresponses() as m:
|
|
184
|
+
offline = {**raw_data, "lock_online": 0, "battery_percentage": -1}
|
|
185
|
+
m.get(f"{lock.apiclient.host}/status", payload=offline, status=200, headers={"Content-Type": "text/html"})
|
|
186
|
+
await lock.update()
|
|
187
|
+
assert lock.online is False
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@pytest.mark.asyncio
|
|
191
|
+
async def test_receive_webhook_online_status(lock, valid_base64):
|
|
192
|
+
timestamp = int(time.time())
|
|
193
|
+
|
|
194
|
+
body = '{"ble_strength": -1, "wifi_strength": -50}'
|
|
195
|
+
await lock.receiveWebhook(body, compute_webhook_hash(body, timestamp, valid_base64), str(timestamp))
|
|
196
|
+
assert lock.online is False
|
|
197
|
+
assert lock.raw_data["wifi_strength"] == -50
|
|
198
|
+
|
|
199
|
+
body = '{"ble_strength": -60, "wifi_strength": -48}'
|
|
200
|
+
await lock.receiveWebhook(body, compute_webhook_hash(body, timestamp, valid_base64), str(timestamp))
|
|
201
|
+
assert lock.online is True
|
|
202
|
+
assert lock.ble_strength == -60
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@pytest.mark.asyncio
|
|
206
|
+
async def test_receive_webhook_offline_then_reconnect(lock, valid_base64):
|
|
207
|
+
"""Payloads captured from a real bridge when the battery was removed and re-inserted."""
|
|
208
|
+
timestamp = int(time.time())
|
|
209
|
+
|
|
210
|
+
body = '{"wifi_strength":51,"ble_strength":-1,"mac_wifi":"AA:BB:CC:00:00:01","mac_ble":"AA:BB:CC:00:00:02"}'
|
|
211
|
+
await lock.receiveWebhook(body, compute_webhook_hash(body, timestamp, valid_base64), str(timestamp))
|
|
212
|
+
assert lock.online is False
|
|
213
|
+
|
|
214
|
+
body = '{"battery_type":"NICKEL_METAL_HYDRIDE","battery_percentage":91,"mac_wifi":"AA:BB:CC:00:00:01","mac_ble":"AA:BB:CC:00:00:02"}'
|
|
215
|
+
await lock.receiveWebhook(body, compute_webhook_hash(body, timestamp, valid_base64), str(timestamp))
|
|
216
|
+
assert lock.online is True
|
|
217
|
+
assert lock.battery_percentage == 91
|
|
218
|
+
assert lock.battery_type == "NICKEL_METAL_HYDRIDE"
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@pytest.mark.asyncio
|
|
222
|
+
async def test_receive_webhook_state_change_marks_online(lock, valid_base64):
|
|
223
|
+
lock.raw_data["lock_online"] = 0
|
|
224
|
+
body = '{"key_local_id": 3, "event_type": "STATE_CHANGED_NIGHT_LOCK"}'
|
|
225
|
+
timestamp = int(time.time())
|
|
226
|
+
await lock.receiveWebhook(body, compute_webhook_hash(body, timestamp, valid_base64), str(timestamp))
|
|
227
|
+
assert lock.online is True
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@pytest.mark.asyncio
|
|
231
|
+
async def test_receive_webhook_goto_state_does_not_mark_online(lock, valid_base64):
|
|
232
|
+
lock.raw_data["lock_online"] = 0
|
|
233
|
+
body = '{"key_local_id": 4, "event_type": "GOTO_STATE_NIGHT_LOCK"}'
|
|
234
|
+
timestamp = int(time.time())
|
|
235
|
+
await lock.receiveWebhook(body, compute_webhook_hash(body, timestamp, valid_base64), str(timestamp))
|
|
236
|
+
assert lock.online is False
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@pytest.mark.asyncio
|
|
240
|
+
async def test_receive_webhook_battery_offline_marker(lock, valid_base64):
|
|
241
|
+
body = '{"battery_percentage": -1}'
|
|
242
|
+
timestamp = int(time.time())
|
|
243
|
+
await lock.receiveWebhook(body, compute_webhook_hash(body, timestamp, valid_base64), str(timestamp))
|
|
244
|
+
assert lock.online is False
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
@pytest.mark.asyncio
|
|
248
|
+
async def test_receive_webhook_battery_and_ble_strength(lock, valid_base64):
|
|
249
|
+
body = '{"battery_percentage": 70, "ble_strength": -1}'
|
|
250
|
+
timestamp = int(time.time())
|
|
251
|
+
await lock.receiveWebhook(body, compute_webhook_hash(body, timestamp, valid_base64), str(timestamp))
|
|
252
|
+
assert lock.battery_percentage == 70
|
|
253
|
+
assert lock.online is False
|
|
254
|
+
|
|
255
|
+
|
|
170
256
|
@pytest.mark.asyncio
|
|
171
257
|
async def test_receive_webhook_bolt_state_change(lock, valid_base64):
|
|
172
258
|
body = '{"key_local_id": 3, "event_type": "STATE_CHANGED_NIGHT_LOCK"}'
|
|
@@ -680,6 +680,7 @@ dependencies = [
|
|
|
680
680
|
|
|
681
681
|
[package.optional-dependencies]
|
|
682
682
|
dev = [
|
|
683
|
+
{ name = "aiohttp" },
|
|
683
684
|
{ name = "aioresponses" },
|
|
684
685
|
{ name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
|
685
686
|
{ name = "pytest", version = "9.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
|
@@ -690,7 +691,8 @@ dev = [
|
|
|
690
691
|
|
|
691
692
|
[package.metadata]
|
|
692
693
|
requires-dist = [
|
|
693
|
-
{ name = "aiohttp", specifier = ">=3.8
|
|
694
|
+
{ name = "aiohttp", specifier = ">=3.8" },
|
|
695
|
+
{ name = "aiohttp", marker = "extra == 'dev'", specifier = "<3.14" },
|
|
694
696
|
{ name = "aioresponses", marker = "extra == 'dev'" },
|
|
695
697
|
{ name = "async-timeout" },
|
|
696
698
|
{ name = "pytest", marker = "extra == 'dev'" },
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|