rtlamr-python 1.0.0__py3-none-any.whl

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.
rtlamr_python/sdr.py ADDED
@@ -0,0 +1,102 @@
1
+ """SDR sample sources.
2
+
3
+ Two implementations share the SampleSource protocol:
4
+ UsbSdr — live RTL-SDR dongle via pyrtlsdr
5
+ FileSampleSource — reads raw IQ bytes from a file (loops on EOF)
6
+
7
+ Select via the --sample-file CLI flag; omit it for live USB.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import io
13
+ from typing import Protocol, runtime_checkable
14
+
15
+
16
+ @runtime_checkable
17
+ class SampleSource(Protocol):
18
+ def read_block(self, n_bytes: int) -> bytes:
19
+ """Return exactly *n_bytes* of raw uint8 IQ data."""
20
+ ...
21
+
22
+ def set_center_freq(self, freq: int) -> None:
23
+ """Retune the source to *freq* Hz."""
24
+ ...
25
+
26
+ def close(self) -> None:
27
+ ...
28
+
29
+
30
+ class UsbSdr:
31
+ """Live RTL-SDR dongle via pyrtlsdr."""
32
+
33
+ def __init__(self, center_freq: int, sample_rate: int, gain: str | float = "auto", ppm: int = 0) -> None:
34
+ try:
35
+ # pyrtlsdr ≤ 0.3.0 does `import pkg_resources` at module level for
36
+ # version detection. setuptools ≥ 80 removed pkg_resources, so
37
+ # inject a minimal stub when the real module is absent.
38
+ import sys
39
+ if 'pkg_resources' not in sys.modules:
40
+ import importlib.util
41
+ import types
42
+ if importlib.util.find_spec('pkg_resources') is None:
43
+ sys.modules['pkg_resources'] = types.ModuleType('pkg_resources')
44
+ from rtlsdr import RtlSdr
45
+ except ImportError:
46
+ raise ImportError(
47
+ "pyrtlsdr is required for live hardware. "
48
+ "Install it with: pip install 'rtlamr-python'"
49
+ )
50
+ self._sdr = RtlSdr()
51
+ self._sdr.center_freq = center_freq
52
+ self._sdr.sample_rate = sample_rate
53
+ self._sdr.ppm_error = ppm
54
+ if gain == "auto":
55
+ self._sdr.gain = "auto"
56
+ else:
57
+ self._sdr.gain = float(gain)
58
+
59
+ def read_block(self, n_bytes: int) -> bytes:
60
+ """Read *n_bytes* uint8 IQ samples from the dongle."""
61
+ return bytes(self._sdr.read_bytes(n_bytes))
62
+
63
+ def set_center_freq(self, freq: int) -> None:
64
+ self._sdr.center_freq = freq
65
+
66
+ def close(self) -> None:
67
+ self._sdr.close()
68
+
69
+
70
+ class FileSampleSource:
71
+ """Read raw IQ bytes from a binary file; loop on EOF for continuous testing."""
72
+
73
+ def __init__(self, path: str) -> None:
74
+ self._path = path
75
+ self._fh = open(path, "rb")
76
+
77
+ def read_block(self, n_bytes: int) -> bytes:
78
+ data = self._fh.read(n_bytes)
79
+ if len(data) < n_bytes:
80
+ # Loop back to the start of the file.
81
+ self._fh.seek(0)
82
+ data = self._fh.read(n_bytes)
83
+ return data
84
+
85
+ def set_center_freq(self, freq: int) -> None:
86
+ pass # no-op; file captures are frequency-agnostic
87
+
88
+ def close(self) -> None:
89
+ self._fh.close()
90
+
91
+
92
+ def open_source(
93
+ center_freq: int,
94
+ sample_rate: int,
95
+ gain: str | float = "auto",
96
+ ppm: int = 0,
97
+ sample_file: str | None = None,
98
+ ) -> SampleSource:
99
+ """Return the appropriate SampleSource based on whether a file path was given."""
100
+ if sample_file:
101
+ return FileSampleSource(sample_file)
102
+ return UsbSdr(center_freq, sample_rate, gain, ppm)
@@ -0,0 +1,154 @@
1
+ Metadata-Version: 2.4
2
+ Name: rtlamr-python
3
+ Version: 1.0.0
4
+ Summary: Python implementation of RTLAMR-go — a multi-protocol ERT smart meter receiver
5
+ Author-email: Ryan Bagwell <ryan@ryanbagwell.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/ryanbagwell/rtlamr-python
8
+ Project-URL: Repository, https://github.com/ryanbagwell/rtlamr-python
9
+ Project-URL: Issues, https://github.com/ryanbagwell/rtlamr-python/issues
10
+ Keywords: rtlamr,rtl-sdr,sdr,ert,smart-meter,scm,idm,r900
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Communications
20
+ Classifier: Topic :: Home Automation
21
+ Requires-Python: >=3.11
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: numpy>=1.24
25
+ Requires-Dist: pyrtlsdr==0.3.0
26
+ Requires-Dist: pyusb>=1.3.1
27
+ Requires-Dist: setuptools
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest; extra == "dev"
30
+ Dynamic: license-file
31
+
32
+ # rtlamr-python
33
+
34
+ A Python implementation of [rtlamr](https://github.com/bemasher/rtlamr) — a multi-protocol
35
+ ERT (Encoder Receiver Transmitter) smart meter receiver. Reads IQ samples from an RTL-SDR
36
+ dongle (or a raw sample file), decodes ERT packets, and prints each message as a JSON line
37
+ to stdout.
38
+
39
+ ## Supported protocols
40
+
41
+ | Protocol | Description |
42
+ |-----------|-----------------------------------------------------------------|
43
+ | `scmplus` | Standard Consumption Message Plus (16 bytes, most electric meters) |
44
+ | `scm` | Standard Consumption Message (12 bytes, older electric meters) |
45
+ | `idm` | Interval Data Message (92 bytes, hourly interval data) |
46
+ | `netidm` | Net Meter Interval Data Message (92 bytes, net-metering variant)|
47
+ | `r900` | Neptune R900 water meters (different center freq: 912.38 MHz) |
48
+
49
+ By default, all Manchester-encoded protocols (`scmplus`, `scm`, `idm`, `netidm`) are decoded
50
+ simultaneously. `r900` uses a different center frequency and must be selected explicitly
51
+ (alone, or alternated with the Manchester set).
52
+
53
+ ## Installation
54
+
55
+ Requires Python 3.11+ and an RTL-SDR dongle (or [librtlsdr](https://github.com/librtlsdr/librtlsdr) installed) for live capture.
56
+
57
+ ```bash
58
+ pip install rtlamr-python
59
+ ```
60
+
61
+ This installs an `rtlamr` command on your `PATH`.
62
+
63
+ ## Usage
64
+
65
+ ```bash
66
+ # All Manchester protocols on live hardware
67
+ rtlamr
68
+
69
+ # Single protocol
70
+ rtlamr --protocol scmplus
71
+
72
+ # From a recorded capture file
73
+ rtlamr --sample-file /path/to/capture.bin
74
+
75
+ # Filter to specific meters
76
+ rtlamr --meter-id 12345678
77
+
78
+ # Alternate between Manchester and R900 (different center frequencies)
79
+ rtlamr --protocol scmplus r900
80
+ ```
81
+
82
+ Run `rtlamr --help` for the full list of options (gain, frequency correction, chip length,
83
+ posting readings to a REST API, etc.).
84
+
85
+ ### Configuration file
86
+
87
+ Options can also be supplied via a TOML config file with `--config path/to/rtlamr.toml`.
88
+ Command-line flags always take precedence over the config file. Example:
89
+
90
+ ```toml
91
+ meter_ids = [12345678, 87654321]
92
+ protocol = ["scmplus", "scm"]
93
+ gain = "auto"
94
+ api_url = "http://localhost:8000/api"
95
+ api_key = "secret"
96
+ switch_timeout = 60.0
97
+ ```
98
+
99
+ ## Library usage
100
+
101
+ `rtlamr-python` can also be used as a library rather than a CLI, e.g. to decode readings
102
+ from within another application. All three forms below share the same options as the CLI
103
+ (`protocols`, `meter_id`, `chip_length`, `gain`, `freq_correction`, `sample_file`,
104
+ `switch_timeout`, `duration`, `verbose`).
105
+
106
+ ### Iterate over readings as they arrive
107
+
108
+ ```python
109
+ from rtlamr_python import listen
110
+
111
+ for reading in listen(protocols=["scmplus"], meter_id=[12345678]):
112
+ print(reading)
113
+ # break whenever you've got what you need — the SDR is closed on exit
114
+ ```
115
+
116
+ ### Block until a single reading decodes
117
+
118
+ ```python
119
+ from rtlamr_python import listen_once
120
+
121
+ reading = listen_once(protocols=["scmplus"], meter_id=[12345678])
122
+ print(reading) # SDR is already closed here
123
+ ```
124
+
125
+ ### Run in the background with a callback
126
+
127
+ ```python
128
+ from rtlamr_python import start_listening
129
+
130
+ def on_message(reading):
131
+ print(reading)
132
+
133
+ handle = start_listening(on_message, protocols=["scmplus"])
134
+ ...
135
+ handle.stop() # signals the background thread and waits for it to exit
136
+ ```
137
+
138
+ Each reading is a dict, e.g.:
139
+
140
+ ```json
141
+ {"time": "2026-01-01T00:00:00Z", "type": "SCM+", "endpoint_id": 12345678,
142
+ "endpoint_type": 4, "consumption": 112233, "tamper": "0x0000", "packet_crc": "0x972F"}
143
+ ```
144
+
145
+ ## Development
146
+
147
+ ```bash
148
+ pip install -e ".[dev]"
149
+ pytest
150
+ ```
151
+
152
+ ## License
153
+
154
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,22 @@
1
+ rtlamr_python/__init__.py,sha256=zJf0EA8egm-vibQMLYCQJbQb5QIxRg-LNnhWrGv2a28,290
2
+ rtlamr_python/__main__.py,sha256=ND39a5xe1Uq9cGh-ZJpfsOYIN8rFxcGASw6kz9Id44Q,92
3
+ rtlamr_python/cli.py,sha256=tm59q0lIIPuhRXRZ7zzMH8kMsv3r6Fck9VE_aFpAWuc,6860
4
+ rtlamr_python/crc.py,sha256=nXPLxCxE9CYC8t-PJ7zBg8Z1fweSL4kC_su9rmNb0cE,2157
5
+ rtlamr_python/decoder.py,sha256=tnvf_Hk3Q3u938tL-ggy-FNynucOY9w_fhCqWhyO5O0,10427
6
+ rtlamr_python/listener.py,sha256=KaHS2FeUFjz6Z7zEMaL-YK6MT0J3Au7Cbait5nNwdRQ,12568
7
+ rtlamr_python/poster.py,sha256=Y1mN5mhVv1GZiZL2sg5qMH7DqofIapa7sVZEAAROXYI,3382
8
+ rtlamr_python/r900_decoder.py,sha256=U3psGQlRtNqxjyCpQ2Ve14k4jHYFCW9HDVAkeWkYc2E,11667
9
+ rtlamr_python/sdr.py,sha256=uiLRQy_mCPWS_nD_qiEBME5el4QOaOomT_J5ARXiYoE,3217
10
+ rtlamr_python/protocols/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ rtlamr_python/protocols/commodity.py,sha256=_AIZFvhgkHArHXh1Ti6AnLcddhdYhqXDoMghy5kP-mA,1196
12
+ rtlamr_python/protocols/idm.py,sha256=OmDrBhdtjrzvqv369Vfyz30mxUFs94CCTShx2Ogjyqo,4616
13
+ rtlamr_python/protocols/netidm.py,sha256=9KoKfuo7DCkmXQRZQM2TbcM9eS180xtOMBlLES8Ywtw,3884
14
+ rtlamr_python/protocols/r900.py,sha256=W-eGGc0rl28_yhKOCFP31GQbIxX8Hkug3e4VkSxDJcM,2044
15
+ rtlamr_python/protocols/scm.py,sha256=FmZnXL2-ft8kOKuqA1-Fc46dn30kphdfI826zPdbRfU,2348
16
+ rtlamr_python/protocols/scmplus.py,sha256=Qr4HOUKtBdFrG3ZndnW3LfXL_jD_mFz4YdZSRRi-zuQ,2405
17
+ rtlamr_python-1.0.0.dist-info/licenses/LICENSE,sha256=xzcGj3DR_4EbJer9xFCahm4Q2n1ud8C8C9CnmEr7NPw,1069
18
+ rtlamr_python-1.0.0.dist-info/METADATA,sha256=KmBEx4jNlyq0otiPXBRHKw8j7lBc0NH3AU0C_h8m6eU,4783
19
+ rtlamr_python-1.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
20
+ rtlamr_python-1.0.0.dist-info/entry_points.txt,sha256=8LqK70DFQWCZTjeKsqMXZVd_UxTed4Q-HKmjHk8PWUs,50
21
+ rtlamr_python-1.0.0.dist-info/top_level.txt,sha256=GX1_wGjMfRbIUgHQjdY-tKIfm6u446OrI7-DCTsyRtk,14
22
+ rtlamr_python-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ rtlamr = rtlamr_python.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ryan Bagwell
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ rtlamr_python