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.
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: obis-parser
3
- Version: 0.0.2
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://github.com/jannickfahlbusch/obis-parser
6
- Project-URL: Repository, https://github.com/jannickfahlbusch/obis-parser
7
- Project-URL: Issues, https://github.com/jannickfahlbusch/obis-parser/issues
8
- Project-URL: Changelog, https://github.com/jannickfahlbusch/obis-parser/blob/main/CHANGELOG.md
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.2'
22
- __version_tuple__ = version_tuple = (0, 0, 2)
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://github.com/jannickfahlbusch/obis-parser"
34
- Repository = "https://github.com/jannickfahlbusch/obis-parser"
35
- Issues = "https://github.com/jannickfahlbusch/obis-parser/issues"
36
- Changelog = "https://github.com/jannickfahlbusch/obis-parser/blob/main/CHANGELOG.md"
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 modifier on base register
25
+ # 2. Channel qualifier on base register
26
26
  (
27
- "1-1:1.7.0",
27
+ "1-1:1.8.0",
28
28
  OBISNameDescriptor(
29
- slug="active_power_import_channel",
29
+ slug="active_energy_import_channel",
30
30
  placeholders={"channel": "1"},
31
- fallback_name="Active power import (Channel 1)",
31
+ fallback_name="Active energy import (Channel 1)",
32
32
  ),
33
33
  ),
34
- # Tariff modifier on base register
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 means total/unspecified (no tariff suffix)
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) phase angle registers
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 (must NOT generate tariff suffix!)
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 test_describe_and_naming_properties(
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
- assert obis.name == expected.fallback_name
124
- assert obis.slug == expected.slug
125
- assert obis.translation_key == expected.translation_key
126
- assert obis.placeholders == expected.placeholders
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
- @pytest.mark.parametrize(
178
- ("short_code", "canonical_code"),
179
- [
180
- ("1.8.0", "1-0:1.8.0"),
181
- ("1.8.2", "1-0:1.8.2"),
182
- ("81.7.4", "1-0:81.7.4"),
183
- ("0.2.0", "1-0:0.2.0"),
184
- ("96.1.0", "1-0:96.1.0"),
185
- ],
186
- )
187
- def test_decode_shorthand_matches_canonical(self, short_code: str, canonical_code: str) -> None:
188
- """Decoded shorthand object must be identical to canonical decoded object."""
189
- m_short = OBIS.parse(short_code)
190
- m_canon = OBIS.parse(canonical_code)
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