loqedAPI 2.1.9__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.
- loqedapi-2.1.11/PKG-INFO +110 -0
- loqedapi-2.1.11/README.md +77 -0
- {loqedAPI-2.1.9 → loqedapi-2.1.11}/setup.cfg +1 -1
- {loqedAPI-2.1.9 → loqedapi-2.1.11}/setup.py +15 -9
- {loqedAPI-2.1.9 → loqedapi-2.1.11}/src/loqedAPI/cloud_loqed.py +1 -0
- {loqedAPI-2.1.9 → loqedapi-2.1.11}/src/loqedAPI/loqed.py +8 -8
- loqedapi-2.1.11/src/loqedAPI.egg-info/PKG-INFO +110 -0
- {loqedAPI-2.1.9 → loqedapi-2.1.11}/src/loqedAPI.egg-info/SOURCES.txt +4 -1
- loqedapi-2.1.11/src/loqedAPI.egg-info/requires.txt +8 -0
- loqedapi-2.1.11/tests/test_cloud_loqed.py +18 -0
- loqedapi-2.1.11/tests/test_local_loqed.py +60 -0
- loqedapi-2.1.11/tests/test_loqed.py +286 -0
- loqedAPI-2.1.9/PKG-INFO +0 -25
- loqedAPI-2.1.9/README.md +0 -1
- loqedAPI-2.1.9/src/loqedAPI.egg-info/PKG-INFO +0 -25
- loqedAPI-2.1.9/src/loqedAPI.egg-info/requires.txt +0 -2
- {loqedAPI-2.1.9 → loqedapi-2.1.11}/LICENSE +0 -0
- {loqedAPI-2.1.9 → loqedapi-2.1.11}/pyproject.toml +0 -0
- {loqedAPI-2.1.9 → loqedapi-2.1.11}/src/loqedAPI/__init__.py +0 -0
- {loqedAPI-2.1.9 → loqedapi-2.1.11}/src/loqedAPI/apiclient.py +0 -0
- {loqedAPI-2.1.9 → loqedapi-2.1.11}/src/loqedAPI/exceptions.py +0 -0
- {loqedAPI-2.1.9 → loqedapi-2.1.11}/src/loqedAPI/local_loqed.py +0 -0
- {loqedAPI-2.1.9 → loqedapi-2.1.11}/src/loqedAPI/urls.py +0 -0
- {loqedAPI-2.1.9 → loqedapi-2.1.11}/src/loqedAPI.egg-info/dependency_links.txt +0 -0
- {loqedAPI-2.1.9 → loqedapi-2.1.11}/src/loqedAPI.egg-info/top_level.txt +0 -0
- {loqedAPI-2.1.9 → loqedapi-2.1.11}/tests/test.py +0 -0
loqedapi-2.1.11/PKG-INFO
ADDED
|
@@ -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.
|
|
@@ -2,23 +2,29 @@ from setuptools import find_packages, setup
|
|
|
2
2
|
|
|
3
3
|
setup(
|
|
4
4
|
name="loqedAPI",
|
|
5
|
-
version="2.1.
|
|
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=[
|
|
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 ::
|
|
15
|
-
"Intended Audience ::
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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,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.9/PKG-INFO
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: loqedAPI
|
|
3
|
-
Version: 2.1.9
|
|
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.9/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.9
|
|
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
|
|
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
|