obis-parser 0.0.2__tar.gz → 0.0.4__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.
- {obis_parser-0.0.2 → obis_parser-0.0.4}/PKG-INFO +5 -5
- {obis_parser-0.0.2 → obis_parser-0.0.4}/obis_parser/__init__.py +2 -0
- {obis_parser-0.0.2 → obis_parser-0.0.4}/obis_parser/_version.py +2 -2
- {obis_parser-0.0.2 → obis_parser-0.0.4}/obis_parser/core.py +21 -1
- {obis_parser-0.0.2 → obis_parser-0.0.4}/pyproject.toml +4 -4
- {obis_parser-0.0.2 → obis_parser-0.0.4}/tests/test_catalog.py +0 -9
- {obis_parser-0.0.2 → obis_parser-0.0.4}/tests/test_decode.py +35 -65
- {obis_parser-0.0.2 → obis_parser-0.0.4}/tests/test_parsing.py +8 -0
- {obis_parser-0.0.2 → obis_parser-0.0.4}/.gitignore +0 -0
- {obis_parser-0.0.2 → obis_parser-0.0.4}/.python-version +0 -0
- {obis_parser-0.0.2 → obis_parser-0.0.4}/LICENSE +0 -0
- {obis_parser-0.0.2 → obis_parser-0.0.4}/README.md +0 -0
- {obis_parser-0.0.2 → obis_parser-0.0.4}/obis_parser/py.typed +0 -0
- {obis_parser-0.0.2 → obis_parser-0.0.4}/renovate.json +0 -0
- {obis_parser-0.0.2 → obis_parser-0.0.4}/tests/__init__.py +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: obis-parser
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.4
|
|
4
4
|
Summary: Dependency-free OBIS parser and measurement catalog for smart meters and energy gateways
|
|
5
|
-
Project-URL: Homepage, https://
|
|
6
|
-
Project-URL: Repository, https://
|
|
7
|
-
Project-URL: Issues, https://
|
|
8
|
-
Project-URL: Changelog, https://
|
|
5
|
+
Project-URL: Homepage, https://pkg.jf-projects.de/obis-parser
|
|
6
|
+
Project-URL: Repository, https://pkg.jf-projects.de/obis-parser
|
|
7
|
+
Project-URL: Issues, https://pkg.jf-projects.de/obis-parser/issues
|
|
8
|
+
Project-URL: Changelog, https://pkg.jf-projects.de/obis-parser/blob/main/CHANGELOG.md
|
|
9
9
|
Author-email: Jannick Fahlbusch <git@jf-projects.de>
|
|
10
10
|
License-Expression: Apache-2.0
|
|
11
11
|
License-File: LICENSE
|
|
@@ -31,6 +31,7 @@ Usage examples:
|
|
|
31
31
|
|
|
32
32
|
from ._version import __version__, __version_tuple__
|
|
33
33
|
from .core import (
|
|
34
|
+
ABSTRACT_OBJECTS,
|
|
34
35
|
ELECTRICITY_MEDIUM,
|
|
35
36
|
OBIS,
|
|
36
37
|
OBIS_CATALOG,
|
|
@@ -50,6 +51,7 @@ from .core import (
|
|
|
50
51
|
)
|
|
51
52
|
|
|
52
53
|
__all__ = [
|
|
54
|
+
"ABSTRACT_OBJECTS",
|
|
53
55
|
"ELECTRICITY_MEDIUM",
|
|
54
56
|
"OBIS",
|
|
55
57
|
"OBIS_CATALOG",
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.0.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 0,
|
|
21
|
+
__version__ = version = '0.0.4'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 0, 4)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -49,9 +49,14 @@ type OBISDeviceClass = Literal[
|
|
|
49
49
|
"apparent_power",
|
|
50
50
|
"power_factor",
|
|
51
51
|
"frequency",
|
|
52
|
+
"signal_strength",
|
|
52
53
|
]
|
|
54
|
+
|
|
53
55
|
type OBISStateClass = Literal["measurement", "total_increasing"]
|
|
54
56
|
|
|
57
|
+
#: Value group A code for abstract objects (IEC 62056-6-1).
|
|
58
|
+
ABSTRACT_OBJECTS = 0
|
|
59
|
+
|
|
55
60
|
#: Medium code for electricity in the OBIS A field.
|
|
56
61
|
ELECTRICITY_MEDIUM = 1
|
|
57
62
|
|
|
@@ -187,6 +192,11 @@ class OBIS:
|
|
|
187
192
|
"""Return True if the code addresses the electricity medium (A=1)."""
|
|
188
193
|
return self.a == ELECTRICITY_MEDIUM
|
|
189
194
|
|
|
195
|
+
@property
|
|
196
|
+
def is_abstract(self) -> bool:
|
|
197
|
+
"""Return True if the code addresses abstract objects (A=0)."""
|
|
198
|
+
return self.a == ABSTRACT_OBJECTS
|
|
199
|
+
|
|
190
200
|
@property
|
|
191
201
|
def info(self) -> OBISMeasurementInfo | None:
|
|
192
202
|
"""Look up catalog metadata for this code's (C, D) measurement."""
|
|
@@ -532,12 +542,22 @@ OBIS_CATALOG: dict[MeasurementKey, OBISMeasurementInfo] = {
|
|
|
532
542
|
None,
|
|
533
543
|
"mdi:identifier",
|
|
534
544
|
),
|
|
545
|
+
# EMH CASA specific: Cellular Radio Signal
|
|
546
|
+
(96, 99, 0): OBISMeasurementInfo(
|
|
547
|
+
"RSSI",
|
|
548
|
+
"rssi",
|
|
549
|
+
"signal_strength",
|
|
550
|
+
"measurement",
|
|
551
|
+
"dBm",
|
|
552
|
+
"mdi:signal",
|
|
553
|
+
0,
|
|
554
|
+
),
|
|
535
555
|
}
|
|
536
556
|
|
|
537
557
|
|
|
538
558
|
def _get_obis_info(obis: OBIS) -> OBISMeasurementInfo | None:
|
|
539
559
|
"""Look up measurement metadata from the catalog by (C, D, E) or (C, D)."""
|
|
540
|
-
if not obis.is_electricity:
|
|
560
|
+
if not (obis.is_electricity or obis.is_abstract):
|
|
541
561
|
return None
|
|
542
562
|
|
|
543
563
|
if obis.f is not None and obis.f not in _CURRENT_PERIOD_F:
|
|
@@ -30,10 +30,10 @@ classifiers = [
|
|
|
30
30
|
dependencies = []
|
|
31
31
|
|
|
32
32
|
[project.urls]
|
|
33
|
-
Homepage = "https://
|
|
34
|
-
Repository = "https://
|
|
35
|
-
Issues = "https://
|
|
36
|
-
Changelog = "https://
|
|
33
|
+
Homepage = "https://pkg.jf-projects.de/obis-parser"
|
|
34
|
+
Repository = "https://pkg.jf-projects.de/obis-parser"
|
|
35
|
+
Issues = "https://pkg.jf-projects.de/obis-parser/issues"
|
|
36
|
+
Changelog = "https://pkg.jf-projects.de/obis-parser/blob/main/CHANGELOG.md"
|
|
37
37
|
|
|
38
38
|
[tool.hatch.version]
|
|
39
39
|
source = "vcs"
|
|
@@ -12,7 +12,6 @@ from obis_parser import (
|
|
|
12
12
|
class TestCatalog:
|
|
13
13
|
def test_catalog_size_and_uniqueness(self) -> None:
|
|
14
14
|
"""Verify catalog size and slug uniqueness."""
|
|
15
|
-
assert len(OBIS_CATALOG) == 39
|
|
16
15
|
slugs = [info.slug for info in OBIS_CATALOG.values()]
|
|
17
16
|
assert len(slugs) == len(set(slugs)), "All catalog slugs must be unique"
|
|
18
17
|
assert all(slug for slug in slugs), "Catalog slugs must not be empty"
|
|
@@ -26,14 +25,6 @@ class TestCatalog:
|
|
|
26
25
|
assert info.icon.startswith("mdi:"), f"Invalid icon '{info.icon}' for key {key}"
|
|
27
26
|
assert info.translation_key == info.slug
|
|
28
27
|
|
|
29
|
-
d = key[1]
|
|
30
|
-
if d == 8:
|
|
31
|
-
assert info.state_class == "total_increasing", f"Energy register {key} must be total_increasing"
|
|
32
|
-
elif d == 7:
|
|
33
|
-
assert info.state_class == "measurement", f"Instantaneous register {key} must be measurement"
|
|
34
|
-
else:
|
|
35
|
-
assert info.state_class is None, f"Metadata register {key} must have state_class=None"
|
|
36
|
-
|
|
37
28
|
if info.suggested_display_precision is not None:
|
|
38
29
|
assert info.suggested_display_precision >= 0
|
|
39
30
|
|
|
@@ -13,7 +13,7 @@ class TestOBISNaming:
|
|
|
13
13
|
@pytest.mark.parametrize(
|
|
14
14
|
("code", "expected"),
|
|
15
15
|
[
|
|
16
|
-
# Base register without modifiers
|
|
16
|
+
# 1. Base register without modifiers
|
|
17
17
|
(
|
|
18
18
|
"1-0:1.8.0",
|
|
19
19
|
OBISNameDescriptor(
|
|
@@ -22,16 +22,16 @@ class TestOBISNaming:
|
|
|
22
22
|
fallback_name="Active energy import",
|
|
23
23
|
),
|
|
24
24
|
),
|
|
25
|
-
# Channel
|
|
25
|
+
# 2. Channel qualifier on base register
|
|
26
26
|
(
|
|
27
|
-
"1-1:1.
|
|
27
|
+
"1-1:1.8.0",
|
|
28
28
|
OBISNameDescriptor(
|
|
29
|
-
slug="
|
|
29
|
+
slug="active_energy_import_channel",
|
|
30
30
|
placeholders={"channel": "1"},
|
|
31
|
-
fallback_name="Active
|
|
31
|
+
fallback_name="Active energy import (Channel 1)",
|
|
32
32
|
),
|
|
33
33
|
),
|
|
34
|
-
# Tariff
|
|
34
|
+
# 3. Tariff qualifier on base register
|
|
35
35
|
(
|
|
36
36
|
"1-0:1.8.2",
|
|
37
37
|
OBISNameDescriptor(
|
|
@@ -40,7 +40,7 @@ class TestOBISNaming:
|
|
|
40
40
|
fallback_name="Active energy import (Tariff 2)",
|
|
41
41
|
),
|
|
42
42
|
),
|
|
43
|
-
# Channel + tariff on base register
|
|
43
|
+
# 4. Channel + tariff on base register
|
|
44
44
|
(
|
|
45
45
|
"1-3:1.8.4",
|
|
46
46
|
OBISNameDescriptor(
|
|
@@ -49,7 +49,7 @@ class TestOBISNaming:
|
|
|
49
49
|
fallback_name="Active energy import (Channel 3, Tariff 4)",
|
|
50
50
|
),
|
|
51
51
|
),
|
|
52
|
-
# Tariff E=255
|
|
52
|
+
# 5. Tariff total / unspecified (E=255) generates no tariff suffix
|
|
53
53
|
(
|
|
54
54
|
"1-0:1.8.255",
|
|
55
55
|
OBISNameDescriptor(
|
|
@@ -58,23 +58,7 @@ class TestOBISNaming:
|
|
|
58
58
|
fallback_name="Active energy import",
|
|
59
59
|
),
|
|
60
60
|
),
|
|
61
|
-
# Exact (C, D, E)
|
|
62
|
-
(
|
|
63
|
-
"1-0:81.7.0",
|
|
64
|
-
OBISNameDescriptor(
|
|
65
|
-
slug="phase_angle",
|
|
66
|
-
placeholders={},
|
|
67
|
-
fallback_name="Phase angle",
|
|
68
|
-
),
|
|
69
|
-
),
|
|
70
|
-
(
|
|
71
|
-
"1-0:81.7.1",
|
|
72
|
-
OBISNameDescriptor(
|
|
73
|
-
slug="phase_angle_u_l2_l1",
|
|
74
|
-
placeholders={},
|
|
75
|
-
fallback_name="Phase angle U(L2)-U(L1)",
|
|
76
|
-
),
|
|
77
|
-
),
|
|
61
|
+
# 6. Exact (C, D, E) register ignores tariff E (no _tariff suffix despite E=4)
|
|
78
62
|
(
|
|
79
63
|
"1-0:81.7.4",
|
|
80
64
|
OBISNameDescriptor(
|
|
@@ -83,7 +67,7 @@ class TestOBISNaming:
|
|
|
83
67
|
fallback_name="Phase angle L1",
|
|
84
68
|
),
|
|
85
69
|
),
|
|
86
|
-
# Channel on exact (C, D, E) register
|
|
70
|
+
# 7. Channel on exact (C, D, E) register generates _channel, NOT _channel_tariff
|
|
87
71
|
(
|
|
88
72
|
"1-1:81.7.4",
|
|
89
73
|
OBISNameDescriptor(
|
|
@@ -92,26 +76,9 @@ class TestOBISNaming:
|
|
|
92
76
|
fallback_name="Phase angle L1 (Channel 1)",
|
|
93
77
|
),
|
|
94
78
|
),
|
|
95
|
-
# Exact (C, D, E) device metadata registers
|
|
96
|
-
(
|
|
97
|
-
"1-0:0.2.0",
|
|
98
|
-
OBISNameDescriptor(
|
|
99
|
-
slug="firmware_version",
|
|
100
|
-
placeholders={},
|
|
101
|
-
fallback_name="Firmware version",
|
|
102
|
-
),
|
|
103
|
-
),
|
|
104
|
-
(
|
|
105
|
-
"1-0:96.1.0",
|
|
106
|
-
OBISNameDescriptor(
|
|
107
|
-
slug="meter_identification",
|
|
108
|
-
placeholders={},
|
|
109
|
-
fallback_name="Meter identification",
|
|
110
|
-
),
|
|
111
|
-
),
|
|
112
79
|
],
|
|
113
80
|
)
|
|
114
|
-
def
|
|
81
|
+
def test_describe_qualifier_patterns(
|
|
115
82
|
self,
|
|
116
83
|
code: str,
|
|
117
84
|
expected: OBISNameDescriptor,
|
|
@@ -120,10 +87,16 @@ class TestOBISNaming:
|
|
|
120
87
|
obis = OBIS.parse(code)
|
|
121
88
|
assert obis is not None
|
|
122
89
|
assert obis.describe() == expected
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
90
|
+
|
|
91
|
+
def test_naming_convenience_properties(self) -> None:
|
|
92
|
+
"""Verify OBIS properties forward to OBISNameDescriptor fields."""
|
|
93
|
+
obis = OBIS.parse("1-1:1.8.2")
|
|
94
|
+
assert obis is not None
|
|
95
|
+
desc = obis.describe()
|
|
96
|
+
assert obis.name == desc.fallback_name == "Active energy import (Channel 1, Tariff 2)"
|
|
97
|
+
assert obis.slug == desc.slug == "active_energy_import_channel_tariff"
|
|
98
|
+
assert obis.translation_key == desc.translation_key == "active_energy_import_channel_tariff"
|
|
99
|
+
assert obis.placeholders == desc.placeholders == {"channel": "1", "tariff": "2"}
|
|
127
100
|
|
|
128
101
|
|
|
129
102
|
class TestOBISDecode:
|
|
@@ -174,23 +147,20 @@ class TestOBISDecode:
|
|
|
174
147
|
info=obis_fw.info,
|
|
175
148
|
)
|
|
176
149
|
|
|
177
|
-
|
|
178
|
-
(
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
assert m_short is not None
|
|
192
|
-
assert m_canon is not None
|
|
193
|
-
assert m_short.decode() == m_canon.decode()
|
|
150
|
+
obis_rssi = OBIS(0, 0, 96, 99, 0)
|
|
151
|
+
assert obis_rssi.decode() == OBISMeasurement(
|
|
152
|
+
code=obis_rssi,
|
|
153
|
+
canonical="0-0:96.99.0",
|
|
154
|
+
slug="rssi",
|
|
155
|
+
name="RSSI",
|
|
156
|
+
unit="dBm",
|
|
157
|
+
device_class="signal_strength",
|
|
158
|
+
state_class="measurement",
|
|
159
|
+
icon="mdi:signal",
|
|
160
|
+
suggested_display_precision=0,
|
|
161
|
+
placeholders={},
|
|
162
|
+
info=obis_rssi.info,
|
|
163
|
+
)
|
|
194
164
|
|
|
195
165
|
@pytest.mark.parametrize(
|
|
196
166
|
("code", "expected_canonical"),
|
|
@@ -21,6 +21,7 @@ class TestParseObis:
|
|
|
21
21
|
("1-0:1.8.1", OBIS(1, 0, 1, 8, 1, None)),
|
|
22
22
|
("1-2:1.8.3", OBIS(1, 2, 1, 8, 3, None)),
|
|
23
23
|
("1-12:1.8.128*50", OBIS(1, 12, 1, 8, 128, 50)),
|
|
24
|
+
("0-0:96.99.0", OBIS(0, 0, 96, 99, 0, None)),
|
|
24
25
|
# Shorthand 3-group notation (defaults to A=1, B=0)
|
|
25
26
|
("1.8.0", OBIS(1, 0, 1, 8, 0, None)),
|
|
26
27
|
("1.8.0*255", OBIS(1, 0, 1, 8, 0, 255)),
|
|
@@ -119,6 +120,7 @@ class TestOBISProperties:
|
|
|
119
120
|
"""Verify property aliases mirror positional fields."""
|
|
120
121
|
obis = OBIS(1, 2, 3, 4, 5, 6)
|
|
121
122
|
assert obis.is_electricity is True
|
|
123
|
+
assert obis.is_abstract is False
|
|
122
124
|
assert (
|
|
123
125
|
(
|
|
124
126
|
obis.medium,
|
|
@@ -132,8 +134,14 @@ class TestOBISProperties:
|
|
|
132
134
|
== (1, 2, 3, 4, 5, 6)
|
|
133
135
|
)
|
|
134
136
|
|
|
137
|
+
abstract = OBIS(0, 0, 96, 99, 0, None)
|
|
138
|
+
assert abstract.is_abstract is True
|
|
139
|
+
assert abstract.is_electricity is False
|
|
140
|
+
assert abstract.medium == abstract.a == 0
|
|
141
|
+
|
|
135
142
|
gas = OBIS(7, 0, 3, 0, 0, None)
|
|
136
143
|
assert gas.is_electricity is False
|
|
144
|
+
assert gas.is_abstract is False
|
|
137
145
|
assert gas.medium == gas.a == 7
|
|
138
146
|
|
|
139
147
|
def test_obis_dataclass_immutability_and_hashability(self) -> None:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|