loqedAPI 2.1.10__tar.gz → 2.1.11__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 (26) hide show
  1. loqedapi-2.1.11/PKG-INFO +110 -0
  2. loqedapi-2.1.11/README.md +77 -0
  3. {loqedAPI-2.1.10 → loqedapi-2.1.11}/setup.cfg +1 -1
  4. {loqedAPI-2.1.10 → loqedapi-2.1.11}/setup.py +15 -9
  5. {loqedAPI-2.1.10 → loqedapi-2.1.11}/src/loqedAPI/loqed.py +8 -8
  6. loqedapi-2.1.11/src/loqedAPI.egg-info/PKG-INFO +110 -0
  7. {loqedAPI-2.1.10 → loqedapi-2.1.11}/src/loqedAPI.egg-info/SOURCES.txt +4 -1
  8. loqedapi-2.1.11/src/loqedAPI.egg-info/requires.txt +8 -0
  9. loqedapi-2.1.11/tests/test_cloud_loqed.py +18 -0
  10. loqedapi-2.1.11/tests/test_local_loqed.py +60 -0
  11. loqedapi-2.1.11/tests/test_loqed.py +286 -0
  12. loqedAPI-2.1.10/PKG-INFO +0 -25
  13. loqedAPI-2.1.10/README.md +0 -1
  14. loqedAPI-2.1.10/src/loqedAPI.egg-info/PKG-INFO +0 -25
  15. loqedAPI-2.1.10/src/loqedAPI.egg-info/requires.txt +0 -2
  16. {loqedAPI-2.1.10 → loqedapi-2.1.11}/LICENSE +0 -0
  17. {loqedAPI-2.1.10 → loqedapi-2.1.11}/pyproject.toml +0 -0
  18. {loqedAPI-2.1.10 → loqedapi-2.1.11}/src/loqedAPI/__init__.py +0 -0
  19. {loqedAPI-2.1.10 → loqedapi-2.1.11}/src/loqedAPI/apiclient.py +0 -0
  20. {loqedAPI-2.1.10 → loqedapi-2.1.11}/src/loqedAPI/cloud_loqed.py +0 -0
  21. {loqedAPI-2.1.10 → loqedapi-2.1.11}/src/loqedAPI/exceptions.py +0 -0
  22. {loqedAPI-2.1.10 → loqedapi-2.1.11}/src/loqedAPI/local_loqed.py +0 -0
  23. {loqedAPI-2.1.10 → loqedapi-2.1.11}/src/loqedAPI/urls.py +0 -0
  24. {loqedAPI-2.1.10 → loqedapi-2.1.11}/src/loqedAPI.egg-info/dependency_links.txt +0 -0
  25. {loqedAPI-2.1.10 → loqedapi-2.1.11}/src/loqedAPI.egg-info/top_level.txt +0 -0
  26. {loqedAPI-2.1.10 → loqedapi-2.1.11}/tests/test.py +0 -0
@@ -0,0 +1,110 @@
1
+ Metadata-Version: 2.4
2
+ Name: loqedAPI
3
+ Version: 2.1.11
4
+ Summary: Python package to use the Loqed Smart Door Lock APIs in a local network. To be used by Home Assistant.
5
+ Home-page: https://github.com/cpolhout/loqedAPI
6
+ Author: Casper Polhout
7
+ Author-email: cpolhout@gmail.com
8
+ License: BSD 2-clause
9
+ Project-URL: Bug Tracker, https://github.com/cpolhout/loqedAPI/issues
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: BSD License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Requires-Python: >=3.6
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: aiohttp
18
+ Requires-Dist: async-timeout
19
+ Provides-Extra: dev
20
+ Requires-Dist: pytest; extra == "dev"
21
+ Requires-Dist: pytest-asyncio; extra == "dev"
22
+ Requires-Dist: pytest-cov; extra == "dev"
23
+ Requires-Dist: aioresponses; extra == "dev"
24
+ Dynamic: author
25
+ Dynamic: author-email
26
+ Dynamic: classifier
27
+ Dynamic: home-page
28
+ Dynamic: license
29
+ Dynamic: license-file
30
+ Dynamic: provides-extra
31
+ Dynamic: requires-dist
32
+ Dynamic: summary
33
+
34
+ # Loqed Touch Smart Lock — Python client
35
+
36
+ This repository provides a small async-first Python client for the Loqed Touch smart lock. It wraps [the HTTP API used by Loqed devices](https://support.loqed.com/en/articles/6127856-loqed-local-bridge-api-integration) and exposes convenient helpers to read lock state, send lock/unlock commands, and manage webhooks.
37
+
38
+ ## Quick start
39
+
40
+ 1. Install the package (recommended: in a virtualenv):
41
+
42
+ ```bash
43
+ python -m venv .venv
44
+ source .venv/bin/activate
45
+ pip install -e .
46
+ ```
47
+
48
+ 2. Install dev/test dependencies (optional):
49
+
50
+ ```bash
51
+ pip install -e .[dev]
52
+ ```
53
+
54
+ 3. Use the client (async):
55
+
56
+ ```python
57
+ import asyncio
58
+ from loqedAPI.loqed import APIClient, LoqedAPI
59
+
60
+ async def main():
61
+ client = APIClient(base_url="https://api.loqed.com", token="your-api-token")
62
+ api = LoqedAPI(client)
63
+
64
+ # fetch a lock by id
65
+ lock = await api.async_get_lock("lock-id")
66
+ print("state:", lock.bolt_state)
67
+
68
+ # send an unlock command
69
+ await lock.unlock()
70
+
71
+ asyncio.run(main())
72
+ ```
73
+
74
+ Notes:
75
+
76
+ - The library is async-first and uses `aiohttp` for HTTP calls.
77
+ - `APIClient` accepts `base_url` and an optional `token` (for authorization) or you can provide your own `aiohttp.ClientSession`.
78
+
79
+ ## Testing
80
+
81
+ Run the test suite with pytest (the project uses `pytest-asyncio` and `aioresponses`):
82
+
83
+ ```bash
84
+ source .venv/bin/activate
85
+ pytest --cov=src/loqedAPI --cov-report=term-missing
86
+ ```
87
+
88
+ ## Development
89
+
90
+ - The package follows standard Python packaging with `pyproject.toml`/`setup.py`.
91
+ - Dev/test dependencies are available under the `dev` extra: `pip install -e .[dev]`.
92
+ - When editing code, run the tests frequently. The tests are fast and designed to mock network calls.
93
+
94
+ ## Contributing
95
+
96
+ If you want to contribute:
97
+
98
+ 1. Fork the repo, create a branch, and make your changes.
99
+ 2. Add or update tests for new behavior.
100
+ 3. Run the test suite and ensure all tests pass.
101
+ 4. Open a pull request with a short description of the change.
102
+
103
+ ## Where to find docs
104
+
105
+ - Source code is under `src/loqedAPI`.
106
+ - Sphinx documentation lives in `docs/`.
107
+
108
+ ## License
109
+
110
+ See the `LICENSE` file in the project root.
@@ -0,0 +1,77 @@
1
+ # Loqed Touch Smart Lock — Python client
2
+
3
+ This repository provides a small async-first Python client for the Loqed Touch smart lock. It wraps [the HTTP API used by Loqed devices](https://support.loqed.com/en/articles/6127856-loqed-local-bridge-api-integration) and exposes convenient helpers to read lock state, send lock/unlock commands, and manage webhooks.
4
+
5
+ ## Quick start
6
+
7
+ 1. Install the package (recommended: in a virtualenv):
8
+
9
+ ```bash
10
+ python -m venv .venv
11
+ source .venv/bin/activate
12
+ pip install -e .
13
+ ```
14
+
15
+ 2. Install dev/test dependencies (optional):
16
+
17
+ ```bash
18
+ pip install -e .[dev]
19
+ ```
20
+
21
+ 3. Use the client (async):
22
+
23
+ ```python
24
+ import asyncio
25
+ from loqedAPI.loqed import APIClient, LoqedAPI
26
+
27
+ async def main():
28
+ client = APIClient(base_url="https://api.loqed.com", token="your-api-token")
29
+ api = LoqedAPI(client)
30
+
31
+ # fetch a lock by id
32
+ lock = await api.async_get_lock("lock-id")
33
+ print("state:", lock.bolt_state)
34
+
35
+ # send an unlock command
36
+ await lock.unlock()
37
+
38
+ asyncio.run(main())
39
+ ```
40
+
41
+ Notes:
42
+
43
+ - The library is async-first and uses `aiohttp` for HTTP calls.
44
+ - `APIClient` accepts `base_url` and an optional `token` (for authorization) or you can provide your own `aiohttp.ClientSession`.
45
+
46
+ ## Testing
47
+
48
+ Run the test suite with pytest (the project uses `pytest-asyncio` and `aioresponses`):
49
+
50
+ ```bash
51
+ source .venv/bin/activate
52
+ pytest --cov=src/loqedAPI --cov-report=term-missing
53
+ ```
54
+
55
+ ## Development
56
+
57
+ - The package follows standard Python packaging with `pyproject.toml`/`setup.py`.
58
+ - Dev/test dependencies are available under the `dev` extra: `pip install -e .[dev]`.
59
+ - When editing code, run the tests frequently. The tests are fast and designed to mock network calls.
60
+
61
+ ## Contributing
62
+
63
+ If you want to contribute:
64
+
65
+ 1. Fork the repo, create a branch, and make your changes.
66
+ 2. Add or update tests for new behavior.
67
+ 3. Run the test suite and ensure all tests pass.
68
+ 4. Open a pull request with a short description of the change.
69
+
70
+ ## Where to find docs
71
+
72
+ - Source code is under `src/loqedAPI`.
73
+ - Sphinx documentation lives in `docs/`.
74
+
75
+ ## License
76
+
77
+ See the `LICENSE` file in the project root.
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = loqedAPI
3
- version = 2.1.10
3
+ version = 2.1.11
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.
@@ -2,23 +2,29 @@ from setuptools import find_packages, setup
2
2
 
3
3
  setup(
4
4
  name="loqedAPI",
5
- version="2.1.10",
5
+ version="2.1.11",
6
6
  description="Python package to use the Loqed Smart Door Lock APIs in a local network. To be used by Home Assistant.",
7
7
  url="https://github.com/cpolhout/loqedAPI",
8
8
  author="Casper Polhout",
9
9
  author_email="cpolhout@gmail.com",
10
10
  license="BSD 2-clause",
11
11
  packages=find_packages(exclude=["tests", "generator"]),
12
- install_requires=["aiohttp", "async-timeout"],
12
+ install_requires=[
13
+ "aiohttp",
14
+ "async-timeout",
15
+ ],
16
+ extras_require={
17
+ "dev": [
18
+ "pytest",
19
+ "pytest-asyncio",
20
+ "pytest-cov",
21
+ "aioresponses",
22
+ ]
23
+ },
13
24
  classifiers=[
14
- "Development Status :: 1 - Planning",
15
- "Intended Audience :: Science/Research",
25
+ "Development Status :: 4 - Beta",
26
+ "Intended Audience :: Developers",
16
27
  "License :: OSI Approved :: BSD License",
17
- "Operating System :: POSIX :: Linux",
18
- "Programming Language :: Python :: 2",
19
- "Programming Language :: Python :: 2.7",
20
28
  "Programming Language :: Python :: 3",
21
- "Programming Language :: Python :: 3.4",
22
- "Programming Language :: Python :: 3.5",
23
29
  ],
24
30
  )
@@ -90,22 +90,22 @@ class Lock:
90
90
 
91
91
  @property
92
92
  def battery_voltage(self) -> str:
93
- """Return the ID of the lock."""
93
+ """Return the battery voltage of the lock."""
94
94
  return self.raw_data["battery_voltage"]
95
95
 
96
96
  @property
97
97
  def wifi_strength(self) -> str:
98
- """Return the ID of the lock."""
98
+ """Return the Wifi strength of the lock."""
99
99
  return self.raw_data["wifi_strength"]
100
100
 
101
101
  @property
102
102
  def ble_strength(self) -> str:
103
- """Return the ID of the lock."""
103
+ """Return the BLE strength of the lock."""
104
104
  return self.raw_data["ble_strength"]
105
105
 
106
106
  @property
107
107
  def battery_type(self) -> str:
108
- """Return the name of the lock."""
108
+ """Return the battery type of the lock."""
109
109
  return self.raw_data["battery_type"]
110
110
 
111
111
  def getcommand(self, action: Action) -> str:
@@ -166,7 +166,7 @@ class Lock:
166
166
  "Update status"
167
167
  resp = await self.apiclient.request("get", "status")
168
168
  resp.raise_for_status()
169
- json_data = await resp.json(content_type="text/html")
169
+ json_data = await resp.json(content_type=None)
170
170
  self.raw_data = json_data
171
171
  self.bolt_state = self.raw_data["bolt_state"]
172
172
  return json_data
@@ -180,7 +180,7 @@ class Lock:
180
180
  headers = {"TIMESTAMP": str(now), "HASH": hash}
181
181
  resp = await self.apiclient.request("get", f"webhooks", headers=headers)
182
182
  resp.raise_for_status()
183
- json_data = await resp.json(content_type="text/html")
183
+ json_data = await resp.json(content_type=None)
184
184
  _LOGGER.debug("get Webhooks Response: %s", str(json_data))
185
185
  self.webhooks = {}
186
186
  return json_data
@@ -322,7 +322,7 @@ class LoqedAPI:
322
322
  resp = await self.apiclient.request("get", "status")
323
323
  resp.raise_for_status()
324
324
  _LOGGER.debug("Response get lock details: %s", await resp.text())
325
- json_data = await resp.json(content_type="text/html")
325
+ json_data = await resp.json(content_type=None)
326
326
  return json_data
327
327
 
328
328
  async def async_get_lock(
@@ -331,7 +331,7 @@ class LoqedAPI:
331
331
  """Return the locks with lock-data"""
332
332
  if not json_data:
333
333
  resp = await self.apiclient.request("get", "status")
334
- json_data = await resp.json(content_type="text/html")
334
+ json_data = await resp.json(content_type=None)
335
335
  return Lock(json_data, secret, bridgekey, key_id, name, self.apiclient)
336
336
 
337
337
 
@@ -0,0 +1,110 @@
1
+ Metadata-Version: 2.4
2
+ Name: loqedAPI
3
+ Version: 2.1.11
4
+ Summary: Python package to use the Loqed Smart Door Lock APIs in a local network. To be used by Home Assistant.
5
+ Home-page: https://github.com/cpolhout/loqedAPI
6
+ Author: Casper Polhout
7
+ Author-email: cpolhout@gmail.com
8
+ License: BSD 2-clause
9
+ Project-URL: Bug Tracker, https://github.com/cpolhout/loqedAPI/issues
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: BSD License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Requires-Python: >=3.6
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: aiohttp
18
+ Requires-Dist: async-timeout
19
+ Provides-Extra: dev
20
+ Requires-Dist: pytest; extra == "dev"
21
+ Requires-Dist: pytest-asyncio; extra == "dev"
22
+ Requires-Dist: pytest-cov; extra == "dev"
23
+ Requires-Dist: aioresponses; extra == "dev"
24
+ Dynamic: author
25
+ Dynamic: author-email
26
+ Dynamic: classifier
27
+ Dynamic: home-page
28
+ Dynamic: license
29
+ Dynamic: license-file
30
+ Dynamic: provides-extra
31
+ Dynamic: requires-dist
32
+ Dynamic: summary
33
+
34
+ # Loqed Touch Smart Lock — Python client
35
+
36
+ This repository provides a small async-first Python client for the Loqed Touch smart lock. It wraps [the HTTP API used by Loqed devices](https://support.loqed.com/en/articles/6127856-loqed-local-bridge-api-integration) and exposes convenient helpers to read lock state, send lock/unlock commands, and manage webhooks.
37
+
38
+ ## Quick start
39
+
40
+ 1. Install the package (recommended: in a virtualenv):
41
+
42
+ ```bash
43
+ python -m venv .venv
44
+ source .venv/bin/activate
45
+ pip install -e .
46
+ ```
47
+
48
+ 2. Install dev/test dependencies (optional):
49
+
50
+ ```bash
51
+ pip install -e .[dev]
52
+ ```
53
+
54
+ 3. Use the client (async):
55
+
56
+ ```python
57
+ import asyncio
58
+ from loqedAPI.loqed import APIClient, LoqedAPI
59
+
60
+ async def main():
61
+ client = APIClient(base_url="https://api.loqed.com", token="your-api-token")
62
+ api = LoqedAPI(client)
63
+
64
+ # fetch a lock by id
65
+ lock = await api.async_get_lock("lock-id")
66
+ print("state:", lock.bolt_state)
67
+
68
+ # send an unlock command
69
+ await lock.unlock()
70
+
71
+ asyncio.run(main())
72
+ ```
73
+
74
+ Notes:
75
+
76
+ - The library is async-first and uses `aiohttp` for HTTP calls.
77
+ - `APIClient` accepts `base_url` and an optional `token` (for authorization) or you can provide your own `aiohttp.ClientSession`.
78
+
79
+ ## Testing
80
+
81
+ Run the test suite with pytest (the project uses `pytest-asyncio` and `aioresponses`):
82
+
83
+ ```bash
84
+ source .venv/bin/activate
85
+ pytest --cov=src/loqedAPI --cov-report=term-missing
86
+ ```
87
+
88
+ ## Development
89
+
90
+ - The package follows standard Python packaging with `pyproject.toml`/`setup.py`.
91
+ - Dev/test dependencies are available under the `dev` extra: `pip install -e .[dev]`.
92
+ - When editing code, run the tests frequently. The tests are fast and designed to mock network calls.
93
+
94
+ ## Contributing
95
+
96
+ If you want to contribute:
97
+
98
+ 1. Fork the repo, create a branch, and make your changes.
99
+ 2. Add or update tests for new behavior.
100
+ 3. Run the test suite and ensure all tests pass.
101
+ 4. Open a pull request with a short description of the change.
102
+
103
+ ## Where to find docs
104
+
105
+ - Source code is under `src/loqedAPI`.
106
+ - Sphinx documentation lives in `docs/`.
107
+
108
+ ## License
109
+
110
+ See the `LICENSE` file in the project root.
@@ -15,4 +15,7 @@ src/loqedAPI.egg-info/SOURCES.txt
15
15
  src/loqedAPI.egg-info/dependency_links.txt
16
16
  src/loqedAPI.egg-info/requires.txt
17
17
  src/loqedAPI.egg-info/top_level.txt
18
- tests/test.py
18
+ tests/test.py
19
+ tests/test_cloud_loqed.py
20
+ tests/test_local_loqed.py
21
+ tests/test_loqed.py
@@ -0,0 +1,8 @@
1
+ aiohttp
2
+ async-timeout
3
+
4
+ [dev]
5
+ pytest
6
+ pytest-asyncio
7
+ pytest-cov
8
+ aioresponses
@@ -0,0 +1,18 @@
1
+
2
+ import pytest
3
+ from aioresponses import aioresponses
4
+ import aiohttp
5
+ from loqedAPI.cloud_loqed import LoqedCloudAPI, CloudAPIClient, CLOUD_BASE_URL
6
+
7
+ @pytest.mark.asyncio
8
+ async def test_loqedcloudapi_get_locks():
9
+ with aioresponses() as m:
10
+ url = f"{CLOUD_BASE_URL}/api/locks/"
11
+ m.get(url, payload={"data": [{"id": "cloudlock1", "name": "Cloud Door"}]}, status=200)
12
+ async with aiohttp.ClientSession() as session:
13
+ client = CloudAPIClient(session, token="dummy-token")
14
+ api = LoqedCloudAPI(client)
15
+ locks = await api.async_get_locks()
16
+ assert "data" in locks
17
+ assert locks["data"][0]["id"] == "cloudlock1"
18
+ assert locks["data"][0]["name"] == "Cloud Door"
@@ -0,0 +1,60 @@
1
+ import pytest
2
+ from loqedAPI.local_loqed import Lock, APIClient, LoqedAPI
3
+ from loqedAPI.exceptions import LoqedException, LoqedAuthenticationException
4
+
5
+ class DummyAPIClient(APIClient):
6
+ def __init__(self):
7
+ pass
8
+ async def request(self, method, url, **kwargs):
9
+ class DummyResponse:
10
+ async def json(self):
11
+ return {"data": [{"id": "lock1", "name": "Front Door", "bolt_state": "locked", "battery_percentage": 80, "battery_type": "AA", "party_mode": False, "guest_access_mode": False, "twist_assist": True, "touch_to_connect": True, "lock_direction": "left", "mortise_lock_type": "euro", "supported_lock_states": "locked"}]}
12
+ async def text(self):
13
+ return "OK"
14
+ def raise_for_status(self):
15
+ pass
16
+ return DummyResponse()
17
+
18
+ @pytest.mark.asyncio
19
+ async def test_lock_properties():
20
+ raw_data = {
21
+ "id": "lock1",
22
+ "name": "Front Door",
23
+ "bolt_state": "locked",
24
+ "battery_percentage": 80,
25
+ "battery_type": "AA",
26
+ "party_mode": False,
27
+ "guest_access_mode": False,
28
+ "twist_assist": True,
29
+ "touch_to_connect": True,
30
+ "lock_direction": "left",
31
+ "mortise_lock_type": "euro",
32
+ "supported_lock_states": "locked"
33
+ }
34
+ lock = Lock(raw_data, DummyAPIClient())
35
+ assert lock.id == "lock1"
36
+ assert lock.name == "Front Door"
37
+ assert lock.battery_percentage == 80
38
+ assert lock.battery_type == "AA"
39
+ assert lock.party_mode is False
40
+ assert lock.guest_access_mode is False
41
+ assert lock.twist_assist is True
42
+ assert lock.touch_to_connect is True
43
+ assert lock.lock_direction == "left"
44
+ assert lock.mortise_lock_type == "euro"
45
+ assert lock.supported_lock_states == "locked"
46
+
47
+ @pytest.mark.asyncio
48
+ async def test_loqedapi_get_locks():
49
+ api = LoqedAPI(DummyAPIClient())
50
+ locks = await api.async_get_locks()
51
+ assert len(locks) == 1
52
+ assert locks[0].name == "Front Door"
53
+ assert locks[0].id == "lock1"
54
+
55
+ # Exception tests
56
+ def test_exceptions():
57
+ with pytest.raises(LoqedException):
58
+ raise LoqedException("Test exception")
59
+ with pytest.raises(LoqedAuthenticationException):
60
+ raise LoqedAuthenticationException("Auth exception")
@@ -0,0 +1,286 @@
1
+ import pytest
2
+ import pytest_asyncio
3
+ from aioresponses import aioresponses
4
+ import aiohttp
5
+ import base64
6
+ import time
7
+ import hashlib
8
+ import re
9
+ import types
10
+ from loqedAPI.loqed import Lock, LoqedAPI, APIClient, Action
11
+
12
+ # ---------------------------------------------------------------------------
13
+ # Fixtures
14
+ # ---------------------------------------------------------------------------
15
+
16
+ @pytest.fixture
17
+ def valid_base64():
18
+ # 44 chars -> 33 bytes when decoded; sufficient for HMAC key
19
+ return "U2VjcmV0U2VjcmV0U2VjcmV0U2VjcmV0U2VjcmV0U2VjcmU="
20
+
21
+ @pytest.fixture
22
+ def raw_data():
23
+ return {
24
+ "bolt_state": "locked",
25
+ "battery_percentage": 90,
26
+ "battery_type": "AA",
27
+ "bridge_mac_wifi": "lockid",
28
+ "wifi_strength": "good",
29
+ "ble_strength": "strong",
30
+ "battery_voltage": "3.7",
31
+ }
32
+
33
+ @pytest_asyncio.fixture
34
+ async def aiohttp_session():
35
+ async with aiohttp.ClientSession() as session:
36
+ yield session
37
+
38
+ @pytest.fixture
39
+ def base_url():
40
+ return "http://dummy"
41
+
42
+ @pytest.fixture
43
+ def client(aiohttp_session, base_url):
44
+ return APIClient(aiohttp_session, base_url)
45
+
46
+ @pytest.fixture
47
+ def lock(raw_data, valid_base64, client):
48
+ return Lock(raw_data.copy(), valid_base64, valid_base64, 1, "TestLock", client)
49
+
50
+
51
+ # Helper to compute webhook hash
52
+ def compute_webhook_hash(body: str, timestamp: int, bridgekey_b64: str) -> str:
53
+ return hashlib.sha256(
54
+ body.encode() + timestamp.to_bytes(8, "big", signed=False) + base64.b64decode(bridgekey_b64)
55
+ ).hexdigest()
56
+
57
+
58
+ # ---------------------------------------------------------------------------
59
+ # Command & property tests
60
+ # ---------------------------------------------------------------------------
61
+ @pytest.mark.asyncio
62
+ async def test_lock_properties_and_commands(lock):
63
+ # Monkeypatch underlying apiclient.request to avoid complex URL matching
64
+ class DummyResp:
65
+ status = 200
66
+ async def text(self):
67
+ return "OK"
68
+ def raise_for_status(self):
69
+ return None
70
+ async def fake_request(self, method, url, **kwargs):
71
+ return DummyResp()
72
+ lock.apiclient.request = types.MethodType(fake_request, lock.apiclient)
73
+ for action in (lock.open, lock.lock, lock.unlock):
74
+ resp = await action()
75
+ assert await resp.text() == "OK"
76
+ assert lock.bolt_state == "locked"
77
+ assert lock.battery_percentage == 90
78
+ assert lock.battery_type == "AA"
79
+ assert lock.id == "lockid"
80
+ assert lock.wifi_strength == "good"
81
+ assert lock.ble_strength == "strong"
82
+
83
+
84
+ @pytest.mark.asyncio
85
+ async def test_loqedapi_get_lock(valid_base64, raw_data, client):
86
+ api = LoqedAPI(client)
87
+ with aioresponses() as m:
88
+ m.get(f"{client.host}/status", payload=raw_data, status=200, headers={"Content-Type": "text/html"})
89
+ lock = await api.async_get_lock(valid_base64, valid_base64, 1, "TestLock")
90
+ assert lock.name == "TestLock"
91
+ assert lock.id == "lockid"
92
+ assert lock.bolt_state == "locked"
93
+
94
+
95
+ @pytest.mark.asyncio
96
+ async def test_async_get_lock_details(client, raw_data):
97
+ """LoqedAPI.async_get_lock_details should return the parsed JSON from /status"""
98
+ api = LoqedAPI(client)
99
+ with aioresponses() as m:
100
+ m.get(f"{client.host}/status", payload=raw_data, status=200, headers={"Content-Type": "text/html"})
101
+ result = await api.async_get_lock_details()
102
+ assert result == raw_data
103
+
104
+
105
+ # ---------------------------------------------------------------------------
106
+ # receiveWebhook tests (no network activity required)
107
+ # ---------------------------------------------------------------------------
108
+ @pytest.mark.asyncio
109
+ async def test_lock_receive_webhook_state_update(lock, valid_base64):
110
+ body = '{"battery_percentage": 80, "key_local_id": 1, "event_type": "STATE_CHANGED_NIGHT_LOCK"}'
111
+ timestamp = int(time.time())
112
+ hash_val = compute_webhook_hash(body, timestamp, valid_base64)
113
+ await lock.receiveWebhook(body, hash_val, str(timestamp))
114
+ assert lock.battery_percentage == 80
115
+
116
+
117
+ @pytest.mark.asyncio
118
+ async def test_lock_receive_webhook_invalid_hash(lock):
119
+ body = '{"battery_percentage": 80, "key_local_id": 1, "event_type": "STATE_CHANGED_NIGHT_LOCK"}'
120
+ timestamp = str(int(time.time()))
121
+ bad_hash = "deadbeef" * 8
122
+ result = await lock.receiveWebhook(body, bad_hash, timestamp)
123
+ assert result["error"] == "Hash incorrect"
124
+
125
+
126
+ @pytest.mark.asyncio
127
+ async def test_lock_receive_webhook_invalid_timestamp(lock, valid_base64):
128
+ body = '{"battery_percentage": 80, "key_local_id": 1, "event_type": "STATE_CHANGED_NIGHT_LOCK"}'
129
+ old_timestamp = int(time.time()) - 1000
130
+ hash_val = compute_webhook_hash(body, old_timestamp, valid_base64)
131
+ result = await lock.receiveWebhook(body, hash_val, str(old_timestamp))
132
+ assert result["error"] == "Timestamp incorrect, possible replaying"
133
+
134
+
135
+ @pytest.mark.asyncio
136
+ async def test_receive_webhook_empty_body(lock, valid_base64):
137
+ timestamp = int(time.time())
138
+ hash_val = compute_webhook_hash("", timestamp, valid_base64)
139
+ result = await lock.receiveWebhook("", hash_val, str(timestamp))
140
+ assert result["error"].startswith("Received invalid data from LOQED. Data needs to be formatted as JSON")
141
+
142
+
143
+ @pytest.mark.asyncio
144
+ async def test_receive_webhook_non_dict(lock, valid_base64):
145
+ body = '"notadict"'
146
+ timestamp = int(time.time())
147
+ hash_val = compute_webhook_hash(body, timestamp, valid_base64)
148
+ result = await lock.receiveWebhook(body, hash_val, str(timestamp))
149
+ assert result["error"].endswith("be a dictionary")
150
+
151
+
152
+ @pytest.mark.asyncio
153
+ async def test_receive_webhook_unknown_event(lock, valid_base64):
154
+ body = '{"key_local_id": 1, "event_type": "UNKNOWN_EVENT"}'
155
+ timestamp = int(time.time())
156
+ hash_val = compute_webhook_hash(body, timestamp, valid_base64)
157
+ await lock.receiveWebhook(body, hash_val, str(timestamp))
158
+ assert lock.last_event == "unknown_event"
159
+
160
+
161
+ @pytest.mark.asyncio
162
+ async def test_receive_webhook_ble_strength(lock, valid_base64):
163
+ body = '{"ble_strength": "weak", "key_local_id": 2, "event_type": "STATE_CHANGED_LATCH"}'
164
+ timestamp = int(time.time())
165
+ hash_val = compute_webhook_hash(body, timestamp, valid_base64)
166
+ await lock.receiveWebhook(body, hash_val, str(timestamp))
167
+ assert lock.raw_data["ble_strength"] == "weak"
168
+
169
+
170
+ @pytest.mark.asyncio
171
+ async def test_receive_webhook_bolt_state_change(lock, valid_base64):
172
+ body = '{"key_local_id": 3, "event_type": "STATE_CHANGED_NIGHT_LOCK"}'
173
+ timestamp = int(time.time())
174
+ hash_val = compute_webhook_hash(body, timestamp, valid_base64)
175
+ lock.bolt_state = "unlocked"
176
+ await lock.receiveWebhook(body, hash_val, str(timestamp))
177
+ assert lock.bolt_state == "night_lock"
178
+ assert lock.last_event == "state_changed_night_lock"
179
+ assert lock.last_key_id == 3
180
+
181
+
182
+ @pytest.mark.asyncio
183
+ async def test_receive_webhook_goto_state_night_lock(lock, valid_base64):
184
+ body = '{"key_local_id": 4, "event_type": "GOTO_STATE_NIGHT_LOCK"}'
185
+ timestamp = int(time.time())
186
+ hash_val = compute_webhook_hash(body, timestamp, valid_base64)
187
+ lock.bolt_state = "unlocked"
188
+ await lock.receiveWebhook(body, hash_val, str(timestamp))
189
+ assert lock.bolt_state == "locking"
190
+ assert lock.last_event == "goto_state_night_lock"
191
+ assert lock.last_key_id == 4
192
+
193
+
194
+ @pytest.mark.asyncio
195
+ async def test_receive_webhook_goto_state_open(lock, valid_base64):
196
+ body = '{"key_local_id": 5, "event_type": "GOTO_STATE_OPEN"}'
197
+ timestamp = int(time.time())
198
+ hash_val = compute_webhook_hash(body, timestamp, valid_base64)
199
+ lock.bolt_state = "locked"
200
+ await lock.receiveWebhook(body, hash_val, str(timestamp))
201
+ assert lock.bolt_state == "opening"
202
+ assert lock.last_event == "goto_state_open"
203
+ assert lock.last_key_id == 5
204
+
205
+
206
+ @pytest.mark.asyncio
207
+ async def test_receive_webhook_goto_state_latch(lock, valid_base64):
208
+ body = '{"key_local_id": 6, "event_type": "GOTO_STATE_LATCH"}'
209
+ timestamp = int(time.time())
210
+ hash_val = compute_webhook_hash(body, timestamp, valid_base64)
211
+ lock.bolt_state = "locked"
212
+ await lock.receiveWebhook(body, hash_val, str(timestamp))
213
+ assert lock.bolt_state == "unlocking"
214
+ assert lock.last_event == "goto_state_latch"
215
+ assert lock.last_key_id == 6
216
+
217
+
218
+ # ---------------------------------------------------------------------------
219
+ # Update & webhook management (network interactions via aioresponses)
220
+ # ---------------------------------------------------------------------------
221
+ @pytest.mark.asyncio
222
+ async def test_update(lock, raw_data):
223
+ with aioresponses() as m:
224
+ raw_data_updated = raw_data.copy()
225
+ raw_data_updated["bolt_state"] = "locked" # unchanged but explicit
226
+ m.get(f"{lock.apiclient.host}/status", payload=raw_data_updated, status=200, headers={"Content-Type": "text/html"})
227
+ result = await lock.update()
228
+ assert result["bolt_state"] == "locked"
229
+ assert lock.bolt_state == "locked"
230
+
231
+
232
+ @pytest.mark.asyncio
233
+ async def test_get_webhooks(lock):
234
+ with aioresponses() as m:
235
+ m.get(f"{lock.apiclient.host}/webhooks", payload=[{"url": "http://test.com"}], status=200, headers={"Content-Type": "text/html"})
236
+ webhooks = await lock.getWebhooks()
237
+ assert isinstance(webhooks, list)
238
+ assert webhooks[0]["url"] == "http://test.com"
239
+
240
+
241
+ @pytest.mark.asyncio
242
+ async def test_register_webhook_created(lock):
243
+ with aioresponses() as m:
244
+ # First call returns no webhooks -> triggers creation
245
+ m.get(f"{lock.apiclient.host}/webhooks", payload=[], status=200, headers={"Content-Type": "text/html"})
246
+ m.post(f"{lock.apiclient.host}/webhooks", payload={}, status=200)
247
+ result = await lock.registerWebhook("http://newwebhook.com")
248
+ assert result == "CREATED"
249
+
250
+
251
+ @pytest.mark.asyncio
252
+ async def test_register_webhook_exists_already(lock):
253
+ with aioresponses() as m:
254
+ m.get(f"{lock.apiclient.host}/webhooks", payload=[{"url": "http://test.com"}], status=200, headers={"Content-Type": "text/html"})
255
+ result = await lock.registerWebhook("http://test.com")
256
+ assert result == "EXISTS ALREADY"
257
+
258
+
259
+ @pytest.mark.asyncio
260
+ async def test_delete_webhook(lock):
261
+ with aioresponses() as m:
262
+ m.delete(re.compile(rf"{re.escape(lock.apiclient.host)}/webhooks/\d+"), status=200, body="OK")
263
+ await lock.deleteWebhook(123)
264
+
265
+
266
+ @pytest.mark.asyncio
267
+ async def test_update_state(lock):
268
+ await lock.updateState("unlocked")
269
+ assert lock.bolt_state == "unlocked"
270
+
271
+
272
+ # ---------------------------------------------------------------------------
273
+ # Additional: ensure command URL signature changes each call (basic sanity)
274
+ # ---------------------------------------------------------------------------
275
+ @pytest.mark.asyncio
276
+ async def test_command_signature_varies(lock, base_url):
277
+ with aioresponses() as m:
278
+ pattern = re.compile(rf"{re.escape(base_url)}/to_lock\?command_signed_base64=.*")
279
+ m.get(pattern, status=200, body="OK")
280
+ cmd1 = lock.getcommand(Action.OPEN)
281
+ time.sleep(1)
282
+ cmd2 = lock.getcommand(Action.OPEN)
283
+ assert cmd1 != cmd2 # timestamp included makes them different
284
+ # Execute one to ensure mocked endpoint matches
285
+ await lock.open()
286
+
loqedAPI-2.1.10/PKG-INFO DELETED
@@ -1,25 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: loqedAPI
3
- Version: 2.1.10
4
- Summary: Python package to use the Loqed Smart Door Lock APIs in a local network. To be used by Home Assistant.
5
- Home-page: https://github.com/cpolhout/loqedAPI
6
- Author: Casper Polhout
7
- Author-email: cpolhout@gmail.com
8
- License: BSD 2-clause
9
- Project-URL: Bug Tracker, https://github.com/cpolhout/loqedAPI/issues
10
- Classifier: Development Status :: 1 - Planning
11
- Classifier: Intended Audience :: Science/Research
12
- Classifier: License :: OSI Approved :: BSD License
13
- Classifier: Operating System :: POSIX :: Linux
14
- Classifier: Programming Language :: Python :: 2
15
- Classifier: Programming Language :: Python :: 2.7
16
- Classifier: Programming Language :: Python :: 3
17
- Classifier: Programming Language :: Python :: 3.4
18
- Classifier: Programming Language :: Python :: 3.5
19
- Requires-Python: >=3.6
20
- Description-Content-Type: text/markdown
21
- License-File: LICENSE
22
- Requires-Dist: aiohttp
23
- Requires-Dist: async-timeout
24
-
25
- # Loqed Touch Smart Lock Python library
loqedAPI-2.1.10/README.md DELETED
@@ -1 +0,0 @@
1
- # Loqed Touch Smart Lock Python library
@@ -1,25 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: loqedAPI
3
- Version: 2.1.10
4
- Summary: Python package to use the Loqed Smart Door Lock APIs in a local network. To be used by Home Assistant.
5
- Home-page: https://github.com/cpolhout/loqedAPI
6
- Author: Casper Polhout
7
- Author-email: cpolhout@gmail.com
8
- License: BSD 2-clause
9
- Project-URL: Bug Tracker, https://github.com/cpolhout/loqedAPI/issues
10
- Classifier: Development Status :: 1 - Planning
11
- Classifier: Intended Audience :: Science/Research
12
- Classifier: License :: OSI Approved :: BSD License
13
- Classifier: Operating System :: POSIX :: Linux
14
- Classifier: Programming Language :: Python :: 2
15
- Classifier: Programming Language :: Python :: 2.7
16
- Classifier: Programming Language :: Python :: 3
17
- Classifier: Programming Language :: Python :: 3.4
18
- Classifier: Programming Language :: Python :: 3.5
19
- Requires-Python: >=3.6
20
- Description-Content-Type: text/markdown
21
- License-File: LICENSE
22
- Requires-Dist: aiohttp
23
- Requires-Dist: async-timeout
24
-
25
- # Loqed Touch Smart Lock Python library
@@ -1,2 +0,0 @@
1
- aiohttp
2
- async-timeout
File without changes
File without changes
File without changes