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.
Files changed (39) hide show
  1. {loqedapi-2.1.14 → loqedapi-2.1.16}/PKG-INFO +9 -2
  2. {loqedapi-2.1.14 → loqedapi-2.1.16}/README.md +6 -0
  3. {loqedapi-2.1.14 → loqedapi-2.1.16}/pyproject.toml +5 -3
  4. {loqedapi-2.1.14 → loqedapi-2.1.16}/src/loqedAPI/loqed.py +25 -2
  5. {loqedapi-2.1.14 → loqedapi-2.1.16}/src/loqedAPI.egg-info/PKG-INFO +9 -2
  6. {loqedapi-2.1.14 → loqedapi-2.1.16}/src/loqedAPI.egg-info/requires.txt +2 -1
  7. loqedapi-2.1.16/src/loqedAPI.egg-info/scm_version.json +8 -0
  8. {loqedapi-2.1.14 → loqedapi-2.1.16}/tests/test_loqed.py +86 -0
  9. {loqedapi-2.1.14 → loqedapi-2.1.16}/uv.lock +3 -1
  10. loqedapi-2.1.14/src/loqedAPI.egg-info/scm_version.json +0 -8
  11. {loqedapi-2.1.14 → loqedapi-2.1.16}/.github/workflows/ci.yml +0 -0
  12. {loqedapi-2.1.14 → loqedapi-2.1.16}/.github/workflows/release.yml +0 -0
  13. {loqedapi-2.1.14 → loqedapi-2.1.16}/.gitignore +0 -0
  14. {loqedapi-2.1.14 → loqedapi-2.1.16}/.vscode/launch.json +0 -0
  15. {loqedapi-2.1.14 → loqedapi-2.1.16}/.vscode/settings.json +0 -0
  16. {loqedapi-2.1.14 → loqedapi-2.1.16}/LICENSE +0 -0
  17. {loqedapi-2.1.14 → loqedapi-2.1.16}/docs/Makefile +0 -0
  18. {loqedapi-2.1.14 → loqedapi-2.1.16}/docs/conf.py +0 -0
  19. {loqedapi-2.1.14 → loqedapi-2.1.16}/docs/index.rst +0 -0
  20. {loqedapi-2.1.14 → loqedapi-2.1.16}/docs/mpi4py_module.rst +0 -0
  21. {loqedapi-2.1.14 → loqedapi-2.1.16}/docs/readme.rst +0 -0
  22. {loqedapi-2.1.14 → loqedapi-2.1.16}/pytest.ini +0 -0
  23. {loqedapi-2.1.14 → loqedapi-2.1.16}/setup.cfg +0 -0
  24. {loqedapi-2.1.14 → loqedapi-2.1.16}/src/.DS_Store +0 -0
  25. {loqedapi-2.1.14 → loqedapi-2.1.16}/src/loqedAPI/__init__.py +0 -0
  26. {loqedapi-2.1.14 → loqedapi-2.1.16}/src/loqedAPI/apiclient.py +0 -0
  27. {loqedapi-2.1.14 → loqedapi-2.1.16}/src/loqedAPI/cloud_loqed.py +0 -0
  28. {loqedapi-2.1.14 → loqedapi-2.1.16}/src/loqedAPI/exceptions.py +0 -0
  29. {loqedapi-2.1.14 → loqedapi-2.1.16}/src/loqedAPI/local_loqed.py +0 -0
  30. {loqedapi-2.1.14 → loqedapi-2.1.16}/src/loqedAPI/urls.py +0 -0
  31. {loqedapi-2.1.14 → loqedapi-2.1.16}/src/loqedAPI.egg-info/SOURCES.txt +0 -0
  32. {loqedapi-2.1.14 → loqedapi-2.1.16}/src/loqedAPI.egg-info/dependency_links.txt +0 -0
  33. {loqedapi-2.1.14 → loqedapi-2.1.16}/src/loqedAPI.egg-info/scm_file_list.json +0 -0
  34. {loqedapi-2.1.14 → loqedapi-2.1.16}/src/loqedAPI.egg-info/top_level.txt +0 -0
  35. {loqedapi-2.1.14 → loqedapi-2.1.16}/tests/__init__.py +0 -0
  36. {loqedapi-2.1.14 → loqedapi-2.1.16}/tests/temp.py +0 -0
  37. {loqedapi-2.1.14 → loqedapi-2.1.16}/tests/test.py +0 -0
  38. {loqedapi-2.1.14 → loqedapi-2.1.16}/tests/test_cloud_loqed.py +0 -0
  39. {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.14
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<3.14,>=3.8
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
- # aioresponses 0.7.9 (latest) doesn't yet support aiohttp's ClientResponse
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
- elif "ble_strength" in data:
294
- self.raw_data["ble_strength"] = data["ble_strength"]
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.14
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<3.14,>=3.8
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`).
@@ -1,4 +1,4 @@
1
- aiohttp<3.14,>=3.8
1
+ aiohttp>=3.8
2
2
  async-timeout
3
3
 
4
4
  [dev]
@@ -6,3 +6,4 @@ pytest
6
6
  pytest-asyncio
7
7
  pytest-cov
8
8
  aioresponses
9
+ aiohttp<3.14
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "2.1.16",
3
+ "distance": 0,
4
+ "node": "gd3a4d284d737b09782775ec779704e30b360bf68",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-09-30"
8
+ }
@@ -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,<3.14" },
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'" },
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "2.1.14",
3
- "distance": 0,
4
- "node": "gdec8ab9a01e406d877f09f9e57ad80c79316d029",
5
- "dirty": false,
6
- "branch": "HEAD",
7
- "node_date": "2026-09-17"
8
- }
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