sensecap-lorawan 0.1.0__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.
- sensecap_lorawan-0.1.0/PKG-INFO +205 -0
- sensecap_lorawan-0.1.0/README.md +181 -0
- sensecap_lorawan-0.1.0/pyproject.toml +51 -0
- sensecap_lorawan-0.1.0/src/sensecap_lorawan/__init__.py +108 -0
- sensecap_lorawan-0.1.0/src/sensecap_lorawan/__main__.py +14 -0
- sensecap_lorawan-0.1.0/src/sensecap_lorawan/blocks.py +216 -0
- sensecap_lorawan-0.1.0/src/sensecap_lorawan/device.py +105 -0
- sensecap_lorawan-0.1.0/src/sensecap_lorawan/models.py +377 -0
- sensecap_lorawan-0.1.0/src/sensecap_lorawan/payload.py +37 -0
- sensecap_lorawan-0.1.0/src/sensecap_lorawan/py.typed +0 -0
- sensecap_lorawan-0.1.0/src/sensecap_lorawan/records.py +47 -0
- sensecap_lorawan-0.1.0/src/sensecap_lorawan/trackers.py +396 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sensecap-lorawan
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: SenseCAP LoRaWAN device models and command-line reader
|
|
5
|
+
Author: Paulus Schoutsen
|
|
6
|
+
Author-email: Paulus Schoutsen <balloob@gmail.com>
|
|
7
|
+
License-Expression: Apache-2.0
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Framework :: AsyncIO
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
13
|
+
Classifier: Topic :: Home Automation
|
|
14
|
+
Classifier: Typing :: Typed
|
|
15
|
+
Requires-Dist: lorawan-connection>=0.14.0,<0.15
|
|
16
|
+
Requires-Dist: lorawan-connection[chirpstack]>=0.14.0,<0.15 ; extra == 'chirpstack'
|
|
17
|
+
Requires-Dist: lorawan-connection[tts]>=0.14.0,<0.15 ; extra == 'tts'
|
|
18
|
+
Requires-Python: >=3.12
|
|
19
|
+
Project-URL: Issues, https://github.com/balloob/sensecap-lorawan/issues
|
|
20
|
+
Project-URL: Repository, https://github.com/balloob/sensecap-lorawan
|
|
21
|
+
Provides-Extra: chirpstack
|
|
22
|
+
Provides-Extra: tts
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# SenseCAP LoRaWAN
|
|
26
|
+
|
|
27
|
+
Read SenseCAP sensors through ChirpStack or The Things Stack using
|
|
28
|
+
[`lorawan-connection`](https://github.com/home-assistant-libs/lorawan-connection).
|
|
29
|
+
|
|
30
|
+
| Model | Readings | Automatic discovery |
|
|
31
|
+
| --- | --- | --- |
|
|
32
|
+
| S2100 | Data logger input channels | ChirpStack, TTS |
|
|
33
|
+
| S2101 | Air temperature, humidity | ChirpStack, TTS |
|
|
34
|
+
| S2102 | Light intensity | ChirpStack, TTS |
|
|
35
|
+
| S2103 | Air temperature, humidity, CO₂ | ChirpStack, TTS |
|
|
36
|
+
| S2104 | Soil temperature, moisture | ChirpStack, TTS |
|
|
37
|
+
| S2105 | Soil temperature, moisture, conductivity | ChirpStack, TTS |
|
|
38
|
+
| S2106 | Temperature, pH | ChirpStack |
|
|
39
|
+
| S2107 | Three temperature probes, including buffered readings | ChirpStack |
|
|
40
|
+
| S2120 | Temperature, humidity, light, UV, wind, pressure, rainfall | ChirpStack, TTS |
|
|
41
|
+
| A1101 | Vision AI measurements | ChirpStack |
|
|
42
|
+
| A1102 | Vision detections and digital meter readings | Explicit Python use |
|
|
43
|
+
| T1000 A/B | Position, temperature, light, motion, Wi-Fi/BLE scans | ChirpStack, TTS |
|
|
44
|
+
| T1000-E | Position, temperature, light, acceleration, Wi-Fi/BLE scans | Explicit Python use |
|
|
45
|
+
| T2000 A/B/C | Position, acceleration, motion, Wi-Fi/BLE scans | ChirpStack, TTS |
|
|
46
|
+
| WM1110 | GNSS fragments, Wi-Fi/BLE scans, environmental and custom readings | Explicit Python use |
|
|
47
|
+
| Gen1 sensors | Air temperature/humidity, CO₂, light, pressure, soil temperature/moisture | TTS |
|
|
48
|
+
|
|
49
|
+
Coverage follows every device family in Seeed's
|
|
50
|
+
[SenseCAP-Decoder](https://github.com/Seeed-Solution/SenseCAP-Decoder/tree/d0a234205fb9574dd03abd49561b60c243c0d1e3).
|
|
51
|
+
The A1102, T1000-E, and WM1110 classes have no automatic discovery identifiers:
|
|
52
|
+
they are absent from the SenseCAP catalogs checked for this release. Use their
|
|
53
|
+
`decode()` methods directly, or subclass them with your deployment's model identity.
|
|
54
|
+
|
|
55
|
+
Tracker support requires SenseCAP LoRaWAN firmware. Meshtastic firmware and
|
|
56
|
+
T1000 Mapper payloads are outside this decoder set. Wi-Fi/BLE scans and GNSS
|
|
57
|
+
fragments remain scan data; the library does not resolve them through a location
|
|
58
|
+
service or assemble fragmented GNSS messages. This library decodes uplinks and
|
|
59
|
+
does not implement device configuration commands.
|
|
60
|
+
|
|
61
|
+
Raw payloads are decoded locally; a server-side payload formatter is not required.
|
|
62
|
+
|
|
63
|
+
## Command-line reader
|
|
64
|
+
|
|
65
|
+
The `sensecap-lorawan` command lists devices or prints readings as they arrive.
|
|
66
|
+
Install [uv](https://docs.astral.sh/uv/getting-started/installation/) to run it with
|
|
67
|
+
`uvx`, or use `pip install "sensecap-lorawan[chirpstack]"` for ChirpStack and
|
|
68
|
+
`pip install "sensecap-lorawan[tts]"` for The Things Stack.
|
|
69
|
+
|
|
70
|
+
### ChirpStack
|
|
71
|
+
|
|
72
|
+
Use a ChirpStack v4 gRPC endpoint, an API key, and the tenant UUID. The endpoint must
|
|
73
|
+
expose the gRPC API; forwarding only the web interface is not sufficient.
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
export CHIRPSTACK_API_KEY="your-api-key"
|
|
77
|
+
uvx --from "sensecap-lorawan[chirpstack]" sensecap-lorawan \
|
|
78
|
+
--backend chirpstack \
|
|
79
|
+
--server https://chirpstack.example.com:8080 \
|
|
80
|
+
--tenant YOUR_TENANT_UUID \
|
|
81
|
+
--application YOUR_APPLICATION_UUID
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Repeat `--application` to select more applications. If omitted, all accessible
|
|
85
|
+
applications in the selected tenant are used. A global key can omit `--tenant`
|
|
86
|
+
to discover its accessible tenants.
|
|
87
|
+
|
|
88
|
+
### The Things Stack
|
|
89
|
+
|
|
90
|
+
Use the Application Server gRPC endpoint and an application API key with device
|
|
91
|
+
read and application traffic read rights. Specify each application explicitly.
|
|
92
|
+
|
|
93
|
+
```sh
|
|
94
|
+
export TTS_API_KEY="your-api-key"
|
|
95
|
+
uvx --from "sensecap-lorawan[tts]" sensecap-lorawan \
|
|
96
|
+
--backend tts \
|
|
97
|
+
--server https://eu1.cloud.thethings.network:8884 \
|
|
98
|
+
--identity-server https://eu1.cloud.thethings.network:8884 \
|
|
99
|
+
--application YOUR_APPLICATION_ID
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Set `--identity-server` to your deployment's Identity Server if it differs from
|
|
103
|
+
the Application Server. Repeat `--application` for multiple applications.
|
|
104
|
+
|
|
105
|
+
Add `--list` to print the current devices and exit, or `--json` for newline-delimited
|
|
106
|
+
JSON output. Use `--api-key-file /path/to/key` instead of an environment variable
|
|
107
|
+
if preferred. Stop watching with Ctrl+C. Run `sensecap-lorawan --help` for all options.
|
|
108
|
+
|
|
109
|
+
With a pip installation, use `sensecap-lorawan` directly in place of the
|
|
110
|
+
`uvx --from ... sensecap-lorawan` prefix. `python -m sensecap_lorawan` also works.
|
|
111
|
+
|
|
112
|
+
## Python
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from lorawan_connection.backend.chirpstack import ChirpStackConnection
|
|
116
|
+
from sensecap_lorawan import SenseCapDeviceCollection
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
async def list_sensors(
|
|
120
|
+
endpoint: str, api_key: str, tenant: str, application: str
|
|
121
|
+
) -> None:
|
|
122
|
+
connection = ChirpStackConnection(
|
|
123
|
+
endpoint,
|
|
124
|
+
api_key,
|
|
125
|
+
tenant_id=tenant,
|
|
126
|
+
application_ids=[application],
|
|
127
|
+
network_id=endpoint,
|
|
128
|
+
)
|
|
129
|
+
devices = SenseCapDeviceCollection(connection)
|
|
130
|
+
try:
|
|
131
|
+
await connection.async_connect()
|
|
132
|
+
await devices.async_setup()
|
|
133
|
+
for device in devices.devices.values():
|
|
134
|
+
print(device.descriptor.name, type(device).__name__)
|
|
135
|
+
finally:
|
|
136
|
+
devices.close()
|
|
137
|
+
await connection.close()
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The collection also accepts a `TTSConnection`. Subscribe with
|
|
141
|
+
`devices.subscribe_device_added(listener)` and `device.add_update_listener(listener)`
|
|
142
|
+
to receive live readings. Values remain `None` until the sensor reports them.
|
|
143
|
+
|
|
144
|
+
### Reading measurements
|
|
145
|
+
|
|
146
|
+
Existing properties such as `S2101.temperature`, `S2101.humidity`, and
|
|
147
|
+
`S2102.illuminance` remain available. All models also expose
|
|
148
|
+
`measurements[channel][measurement_id]`, whose `Measurement` contains `value`,
|
|
149
|
+
`timestamp`, and protocol-specific `attributes`. Timestamps are timezone-aware
|
|
150
|
+
UTC values when transmitted by the device; otherwise they are `None`.
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
from sensecap_lorawan import S2107
|
|
154
|
+
|
|
155
|
+
payload = S2107.decode(bytes.fromhex("470000ff9c8000"))
|
|
156
|
+
for measurement in payload.measurements:
|
|
157
|
+
print(measurement.channel, measurement.value)
|
|
158
|
+
# 1 0.0
|
|
159
|
+
# 2 -1.0
|
|
160
|
+
# 3 None
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
For a live device, use `device.measurement_value(4203, channel=2)` or
|
|
164
|
+
`device.temperature(channel=2)` on an S2107. The S2100 uses input channel numbers as measurement
|
|
165
|
+
IDs within channel 1, matching Seeed's decoder. Unknown seven-byte measurement
|
|
166
|
+
IDs are retained. See Seeed's [measurement ID list](https://wiki.seeedstudio.com/sensecraft-data-platform/api/appendix/list_of_measurement_ids/)
|
|
167
|
+
for their meanings and units.
|
|
168
|
+
|
|
169
|
+
`metadata` contains reported versions, intervals, model information, and errors.
|
|
170
|
+
`battery_percentage` reads the battery value from the vendor payload;
|
|
171
|
+
`battery_level` remains the standardized LoRaWAN status value. Tracker event
|
|
172
|
+
measurements contain numeric event IDs, positioning status is a numeric code,
|
|
173
|
+
and Wi-Fi/BLE scan values are lists of `{mac, rssi}` dictionaries.
|
|
174
|
+
|
|
175
|
+
Direct decoding raises `ValueError` for malformed or unknown frame layouts. Live
|
|
176
|
+
models discard those payloads atomically. Partial updates retain other readings;
|
|
177
|
+
older readings cannot replace newer values on the same channel and measurement ID.
|
|
178
|
+
Device timestamps take precedence over receipt time for buffered readings.
|
|
179
|
+
Fault/absent values become `None`; zero remains a valid reading. The seven-byte
|
|
180
|
+
protocol's two-byte trailer is not CRC-validated, matching Seeed's decoder.
|
|
181
|
+
|
|
182
|
+
## Development
|
|
183
|
+
|
|
184
|
+
```sh
|
|
185
|
+
uv sync
|
|
186
|
+
uv run pytest
|
|
187
|
+
uv run ruff check .
|
|
188
|
+
uv run mypy
|
|
189
|
+
uv build
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Tests include captured S2101/S2102 uplinks, reference outputs for every decoder
|
|
193
|
+
family, and malformed, partial, and out-of-order messages.
|
|
194
|
+
See [fixture provenance and regeneration](tests/fixtures/README.md).
|
|
195
|
+
|
|
196
|
+
## Releases
|
|
197
|
+
|
|
198
|
+
The source version stays `0.0.0`. Publishing a GitHub release named `0.1.0`, for
|
|
199
|
+
example, builds that version and publishes it to PyPI through Trusted Publishing.
|
|
200
|
+
See [RELEASING.md](RELEASING.md) for the one-time publisher setup.
|
|
201
|
+
|
|
202
|
+
## License
|
|
203
|
+
|
|
204
|
+
Apache-2.0. Extracted from the SenseCAP examples in
|
|
205
|
+
[`lorawan-connection`](https://github.com/home-assistant-libs/lorawan-connection).
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# SenseCAP LoRaWAN
|
|
2
|
+
|
|
3
|
+
Read SenseCAP sensors through ChirpStack or The Things Stack using
|
|
4
|
+
[`lorawan-connection`](https://github.com/home-assistant-libs/lorawan-connection).
|
|
5
|
+
|
|
6
|
+
| Model | Readings | Automatic discovery |
|
|
7
|
+
| --- | --- | --- |
|
|
8
|
+
| S2100 | Data logger input channels | ChirpStack, TTS |
|
|
9
|
+
| S2101 | Air temperature, humidity | ChirpStack, TTS |
|
|
10
|
+
| S2102 | Light intensity | ChirpStack, TTS |
|
|
11
|
+
| S2103 | Air temperature, humidity, CO₂ | ChirpStack, TTS |
|
|
12
|
+
| S2104 | Soil temperature, moisture | ChirpStack, TTS |
|
|
13
|
+
| S2105 | Soil temperature, moisture, conductivity | ChirpStack, TTS |
|
|
14
|
+
| S2106 | Temperature, pH | ChirpStack |
|
|
15
|
+
| S2107 | Three temperature probes, including buffered readings | ChirpStack |
|
|
16
|
+
| S2120 | Temperature, humidity, light, UV, wind, pressure, rainfall | ChirpStack, TTS |
|
|
17
|
+
| A1101 | Vision AI measurements | ChirpStack |
|
|
18
|
+
| A1102 | Vision detections and digital meter readings | Explicit Python use |
|
|
19
|
+
| T1000 A/B | Position, temperature, light, motion, Wi-Fi/BLE scans | ChirpStack, TTS |
|
|
20
|
+
| T1000-E | Position, temperature, light, acceleration, Wi-Fi/BLE scans | Explicit Python use |
|
|
21
|
+
| T2000 A/B/C | Position, acceleration, motion, Wi-Fi/BLE scans | ChirpStack, TTS |
|
|
22
|
+
| WM1110 | GNSS fragments, Wi-Fi/BLE scans, environmental and custom readings | Explicit Python use |
|
|
23
|
+
| Gen1 sensors | Air temperature/humidity, CO₂, light, pressure, soil temperature/moisture | TTS |
|
|
24
|
+
|
|
25
|
+
Coverage follows every device family in Seeed's
|
|
26
|
+
[SenseCAP-Decoder](https://github.com/Seeed-Solution/SenseCAP-Decoder/tree/d0a234205fb9574dd03abd49561b60c243c0d1e3).
|
|
27
|
+
The A1102, T1000-E, and WM1110 classes have no automatic discovery identifiers:
|
|
28
|
+
they are absent from the SenseCAP catalogs checked for this release. Use their
|
|
29
|
+
`decode()` methods directly, or subclass them with your deployment's model identity.
|
|
30
|
+
|
|
31
|
+
Tracker support requires SenseCAP LoRaWAN firmware. Meshtastic firmware and
|
|
32
|
+
T1000 Mapper payloads are outside this decoder set. Wi-Fi/BLE scans and GNSS
|
|
33
|
+
fragments remain scan data; the library does not resolve them through a location
|
|
34
|
+
service or assemble fragmented GNSS messages. This library decodes uplinks and
|
|
35
|
+
does not implement device configuration commands.
|
|
36
|
+
|
|
37
|
+
Raw payloads are decoded locally; a server-side payload formatter is not required.
|
|
38
|
+
|
|
39
|
+
## Command-line reader
|
|
40
|
+
|
|
41
|
+
The `sensecap-lorawan` command lists devices or prints readings as they arrive.
|
|
42
|
+
Install [uv](https://docs.astral.sh/uv/getting-started/installation/) to run it with
|
|
43
|
+
`uvx`, or use `pip install "sensecap-lorawan[chirpstack]"` for ChirpStack and
|
|
44
|
+
`pip install "sensecap-lorawan[tts]"` for The Things Stack.
|
|
45
|
+
|
|
46
|
+
### ChirpStack
|
|
47
|
+
|
|
48
|
+
Use a ChirpStack v4 gRPC endpoint, an API key, and the tenant UUID. The endpoint must
|
|
49
|
+
expose the gRPC API; forwarding only the web interface is not sufficient.
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
export CHIRPSTACK_API_KEY="your-api-key"
|
|
53
|
+
uvx --from "sensecap-lorawan[chirpstack]" sensecap-lorawan \
|
|
54
|
+
--backend chirpstack \
|
|
55
|
+
--server https://chirpstack.example.com:8080 \
|
|
56
|
+
--tenant YOUR_TENANT_UUID \
|
|
57
|
+
--application YOUR_APPLICATION_UUID
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Repeat `--application` to select more applications. If omitted, all accessible
|
|
61
|
+
applications in the selected tenant are used. A global key can omit `--tenant`
|
|
62
|
+
to discover its accessible tenants.
|
|
63
|
+
|
|
64
|
+
### The Things Stack
|
|
65
|
+
|
|
66
|
+
Use the Application Server gRPC endpoint and an application API key with device
|
|
67
|
+
read and application traffic read rights. Specify each application explicitly.
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
export TTS_API_KEY="your-api-key"
|
|
71
|
+
uvx --from "sensecap-lorawan[tts]" sensecap-lorawan \
|
|
72
|
+
--backend tts \
|
|
73
|
+
--server https://eu1.cloud.thethings.network:8884 \
|
|
74
|
+
--identity-server https://eu1.cloud.thethings.network:8884 \
|
|
75
|
+
--application YOUR_APPLICATION_ID
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Set `--identity-server` to your deployment's Identity Server if it differs from
|
|
79
|
+
the Application Server. Repeat `--application` for multiple applications.
|
|
80
|
+
|
|
81
|
+
Add `--list` to print the current devices and exit, or `--json` for newline-delimited
|
|
82
|
+
JSON output. Use `--api-key-file /path/to/key` instead of an environment variable
|
|
83
|
+
if preferred. Stop watching with Ctrl+C. Run `sensecap-lorawan --help` for all options.
|
|
84
|
+
|
|
85
|
+
With a pip installation, use `sensecap-lorawan` directly in place of the
|
|
86
|
+
`uvx --from ... sensecap-lorawan` prefix. `python -m sensecap_lorawan` also works.
|
|
87
|
+
|
|
88
|
+
## Python
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from lorawan_connection.backend.chirpstack import ChirpStackConnection
|
|
92
|
+
from sensecap_lorawan import SenseCapDeviceCollection
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
async def list_sensors(
|
|
96
|
+
endpoint: str, api_key: str, tenant: str, application: str
|
|
97
|
+
) -> None:
|
|
98
|
+
connection = ChirpStackConnection(
|
|
99
|
+
endpoint,
|
|
100
|
+
api_key,
|
|
101
|
+
tenant_id=tenant,
|
|
102
|
+
application_ids=[application],
|
|
103
|
+
network_id=endpoint,
|
|
104
|
+
)
|
|
105
|
+
devices = SenseCapDeviceCollection(connection)
|
|
106
|
+
try:
|
|
107
|
+
await connection.async_connect()
|
|
108
|
+
await devices.async_setup()
|
|
109
|
+
for device in devices.devices.values():
|
|
110
|
+
print(device.descriptor.name, type(device).__name__)
|
|
111
|
+
finally:
|
|
112
|
+
devices.close()
|
|
113
|
+
await connection.close()
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The collection also accepts a `TTSConnection`. Subscribe with
|
|
117
|
+
`devices.subscribe_device_added(listener)` and `device.add_update_listener(listener)`
|
|
118
|
+
to receive live readings. Values remain `None` until the sensor reports them.
|
|
119
|
+
|
|
120
|
+
### Reading measurements
|
|
121
|
+
|
|
122
|
+
Existing properties such as `S2101.temperature`, `S2101.humidity`, and
|
|
123
|
+
`S2102.illuminance` remain available. All models also expose
|
|
124
|
+
`measurements[channel][measurement_id]`, whose `Measurement` contains `value`,
|
|
125
|
+
`timestamp`, and protocol-specific `attributes`. Timestamps are timezone-aware
|
|
126
|
+
UTC values when transmitted by the device; otherwise they are `None`.
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from sensecap_lorawan import S2107
|
|
130
|
+
|
|
131
|
+
payload = S2107.decode(bytes.fromhex("470000ff9c8000"))
|
|
132
|
+
for measurement in payload.measurements:
|
|
133
|
+
print(measurement.channel, measurement.value)
|
|
134
|
+
# 1 0.0
|
|
135
|
+
# 2 -1.0
|
|
136
|
+
# 3 None
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
For a live device, use `device.measurement_value(4203, channel=2)` or
|
|
140
|
+
`device.temperature(channel=2)` on an S2107. The S2100 uses input channel numbers as measurement
|
|
141
|
+
IDs within channel 1, matching Seeed's decoder. Unknown seven-byte measurement
|
|
142
|
+
IDs are retained. See Seeed's [measurement ID list](https://wiki.seeedstudio.com/sensecraft-data-platform/api/appendix/list_of_measurement_ids/)
|
|
143
|
+
for their meanings and units.
|
|
144
|
+
|
|
145
|
+
`metadata` contains reported versions, intervals, model information, and errors.
|
|
146
|
+
`battery_percentage` reads the battery value from the vendor payload;
|
|
147
|
+
`battery_level` remains the standardized LoRaWAN status value. Tracker event
|
|
148
|
+
measurements contain numeric event IDs, positioning status is a numeric code,
|
|
149
|
+
and Wi-Fi/BLE scan values are lists of `{mac, rssi}` dictionaries.
|
|
150
|
+
|
|
151
|
+
Direct decoding raises `ValueError` for malformed or unknown frame layouts. Live
|
|
152
|
+
models discard those payloads atomically. Partial updates retain other readings;
|
|
153
|
+
older readings cannot replace newer values on the same channel and measurement ID.
|
|
154
|
+
Device timestamps take precedence over receipt time for buffered readings.
|
|
155
|
+
Fault/absent values become `None`; zero remains a valid reading. The seven-byte
|
|
156
|
+
protocol's two-byte trailer is not CRC-validated, matching Seeed's decoder.
|
|
157
|
+
|
|
158
|
+
## Development
|
|
159
|
+
|
|
160
|
+
```sh
|
|
161
|
+
uv sync
|
|
162
|
+
uv run pytest
|
|
163
|
+
uv run ruff check .
|
|
164
|
+
uv run mypy
|
|
165
|
+
uv build
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Tests include captured S2101/S2102 uplinks, reference outputs for every decoder
|
|
169
|
+
family, and malformed, partial, and out-of-order messages.
|
|
170
|
+
See [fixture provenance and regeneration](tests/fixtures/README.md).
|
|
171
|
+
|
|
172
|
+
## Releases
|
|
173
|
+
|
|
174
|
+
The source version stays `0.0.0`. Publishing a GitHub release named `0.1.0`, for
|
|
175
|
+
example, builds that version and publishes it to PyPI through Trusted Publishing.
|
|
176
|
+
See [RELEASING.md](RELEASING.md) for the one-time publisher setup.
|
|
177
|
+
|
|
178
|
+
## License
|
|
179
|
+
|
|
180
|
+
Apache-2.0. Extracted from the SenseCAP examples in
|
|
181
|
+
[`lorawan-connection`](https://github.com/home-assistant-libs/lorawan-connection).
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "sensecap-lorawan"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "SenseCAP LoRaWAN device models and command-line reader"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "Apache-2.0"
|
|
7
|
+
authors = [{ name = "Paulus Schoutsen", email = "balloob@gmail.com" }]
|
|
8
|
+
requires-python = ">=3.12"
|
|
9
|
+
dependencies = ["lorawan-connection>=0.14.0,<0.15"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 3 - Alpha",
|
|
12
|
+
"Framework :: AsyncIO",
|
|
13
|
+
"Programming Language :: Python :: 3.12",
|
|
14
|
+
"Programming Language :: Python :: 3.13",
|
|
15
|
+
"Programming Language :: Python :: 3.14",
|
|
16
|
+
"Topic :: Home Automation",
|
|
17
|
+
"Typing :: Typed",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.optional-dependencies]
|
|
21
|
+
chirpstack = ["lorawan-connection[chirpstack]>=0.14.0,<0.15"]
|
|
22
|
+
tts = ["lorawan-connection[tts]>=0.14.0,<0.15"]
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
sensecap-lorawan = "sensecap_lorawan.__main__:main"
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Repository = "https://github.com/balloob/sensecap-lorawan"
|
|
29
|
+
Issues = "https://github.com/balloob/sensecap-lorawan/issues"
|
|
30
|
+
|
|
31
|
+
[build-system]
|
|
32
|
+
requires = ["uv_build>=0.8.4,<0.9.0"]
|
|
33
|
+
build-backend = "uv_build"
|
|
34
|
+
|
|
35
|
+
[dependency-groups]
|
|
36
|
+
dev = ["pytest>=9", "pytest-asyncio>=1", "pytest-cov>=6", "ruff>=0.11", "mypy>=1.19"]
|
|
37
|
+
|
|
38
|
+
[tool.pytest.ini_options]
|
|
39
|
+
asyncio_mode = "auto"
|
|
40
|
+
testpaths = ["tests"]
|
|
41
|
+
|
|
42
|
+
[tool.ruff]
|
|
43
|
+
target-version = "py312"
|
|
44
|
+
|
|
45
|
+
[tool.ruff.lint]
|
|
46
|
+
select = ["E", "F", "W", "I", "UP", "B"]
|
|
47
|
+
|
|
48
|
+
[tool.mypy]
|
|
49
|
+
python_version = "3.12"
|
|
50
|
+
strict = true
|
|
51
|
+
files = ["src"]
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""SenseCAP device models for ChirpStack and The Things Stack."""
|
|
2
|
+
|
|
3
|
+
from lorawan_connection import DeviceCollection
|
|
4
|
+
|
|
5
|
+
from .device import SenseCapDevice
|
|
6
|
+
from .models import (
|
|
7
|
+
A1101,
|
|
8
|
+
A1102,
|
|
9
|
+
S2100,
|
|
10
|
+
S2101,
|
|
11
|
+
S2102,
|
|
12
|
+
S2103,
|
|
13
|
+
S2104,
|
|
14
|
+
S2105,
|
|
15
|
+
S2106,
|
|
16
|
+
S2107,
|
|
17
|
+
S2120,
|
|
18
|
+
T1000,
|
|
19
|
+
T1000A,
|
|
20
|
+
T1000B,
|
|
21
|
+
T1000E,
|
|
22
|
+
T2000,
|
|
23
|
+
VENDOR_ID,
|
|
24
|
+
WM1110,
|
|
25
|
+
Gen1AirTemperatureHumidity,
|
|
26
|
+
Gen1CO2,
|
|
27
|
+
Gen1Light,
|
|
28
|
+
Gen1Pressure,
|
|
29
|
+
Gen1SoilTemperatureMoisture,
|
|
30
|
+
)
|
|
31
|
+
from .payload import DecodedPayload, Measurement
|
|
32
|
+
from .records import decode_records
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def decode_s2101(data: bytes) -> dict[str, float | None]:
|
|
36
|
+
"""Decode S2101 channel-one temperature and humidity records."""
|
|
37
|
+
result: dict[str, float | None] = {}
|
|
38
|
+
for item in decode_records(data).measurements:
|
|
39
|
+
key = {4097: "temperature", 4098: "humidity"}.get(item.id)
|
|
40
|
+
if item.channel != 1 or key is None:
|
|
41
|
+
continue
|
|
42
|
+
value = item.value
|
|
43
|
+
if value is not None:
|
|
44
|
+
assert isinstance(value, (float, int))
|
|
45
|
+
if key == "temperature" and not -40 <= value <= 85:
|
|
46
|
+
raise ValueError("Temperature outside S2101 measurement range")
|
|
47
|
+
if key == "humidity" and not 0 <= value <= 100:
|
|
48
|
+
raise ValueError("Humidity outside valid range")
|
|
49
|
+
result[key] = value
|
|
50
|
+
return result
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class SenseCapDeviceCollection(DeviceCollection[SenseCapDevice]):
|
|
54
|
+
"""Discover SenseCAP devices using official catalog identities."""
|
|
55
|
+
|
|
56
|
+
DEVICES = (
|
|
57
|
+
S2100,
|
|
58
|
+
S2101,
|
|
59
|
+
S2102,
|
|
60
|
+
S2103,
|
|
61
|
+
S2104,
|
|
62
|
+
S2105,
|
|
63
|
+
S2106,
|
|
64
|
+
S2107,
|
|
65
|
+
S2120,
|
|
66
|
+
A1101,
|
|
67
|
+
T1000,
|
|
68
|
+
T1000A,
|
|
69
|
+
T1000B,
|
|
70
|
+
T2000,
|
|
71
|
+
Gen1AirTemperatureHumidity,
|
|
72
|
+
Gen1CO2,
|
|
73
|
+
Gen1Light,
|
|
74
|
+
Gen1Pressure,
|
|
75
|
+
Gen1SoilTemperatureMoisture,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
__all__ = [
|
|
80
|
+
"S2100",
|
|
81
|
+
"S2101",
|
|
82
|
+
"S2102",
|
|
83
|
+
"S2103",
|
|
84
|
+
"S2104",
|
|
85
|
+
"S2105",
|
|
86
|
+
"S2106",
|
|
87
|
+
"S2107",
|
|
88
|
+
"S2120",
|
|
89
|
+
"A1101",
|
|
90
|
+
"A1102",
|
|
91
|
+
"T1000",
|
|
92
|
+
"T1000A",
|
|
93
|
+
"T1000B",
|
|
94
|
+
"T1000E",
|
|
95
|
+
"T2000",
|
|
96
|
+
"WM1110",
|
|
97
|
+
"Gen1AirTemperatureHumidity",
|
|
98
|
+
"Gen1CO2",
|
|
99
|
+
"Gen1Light",
|
|
100
|
+
"Gen1Pressure",
|
|
101
|
+
"Gen1SoilTemperatureMoisture",
|
|
102
|
+
"VENDOR_ID",
|
|
103
|
+
"DecodedPayload",
|
|
104
|
+
"Measurement",
|
|
105
|
+
"SenseCapDevice",
|
|
106
|
+
"SenseCapDeviceCollection",
|
|
107
|
+
"decode_s2101",
|
|
108
|
+
]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Discover SenseCAP devices and print their readings."""
|
|
2
|
+
|
|
3
|
+
from lorawan_connection.cli_helper import run
|
|
4
|
+
|
|
5
|
+
from . import SenseCapDeviceCollection
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main() -> None:
|
|
9
|
+
"""Run the SenseCAP command-line interface."""
|
|
10
|
+
run(SenseCapDeviceCollection.DEVICES)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
if __name__ == "__main__":
|
|
14
|
+
main()
|