obis-parser 0.0.1__tar.gz → 0.0.2__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,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: obis-parser
3
- Version: 0.0.1
3
+ Version: 0.0.2
4
4
  Summary: Dependency-free OBIS parser and measurement catalog for smart meters and energy gateways
5
5
  Project-URL: Homepage, https://github.com/jannickfahlbusch/obis-parser
6
6
  Project-URL: Repository, https://github.com/jannickfahlbusch/obis-parser
@@ -41,13 +41,14 @@ Extracts, parses, and normalizes OBIS (*Object Identification System*) codes (`A
41
41
  * **Zero External Dependencies:** Built entirely with standard library Python (`dataclasses`, `re`, `string`, `typing`).
42
42
  * **Universal Ingestion:** Auto-detects and parses:
43
43
  * Canonical strings: `1-0:1.8.0`, `1-0:1.8.0*255`, `1-1:1.7.0`
44
+ * Shorthand display strings: `1.8.0`, `16.7.0`, `81.7.4` (defaults to electricity A=1, channel B=0)
44
45
  * Bare COSEM 12-hex: `0100010800ff`, `0100100700ff`, `010001080000` (F=0)
45
46
  * COSEM logical names with meter suffixes: `0100010800ff.1lgz0067285558.sm`
46
47
  * Dot-separated hex: `01.00.01.08.00.FF`
47
48
  * **Single Domain Model (`OBIS`):** Everything is accessed directly on the `OBIS` instance (`.canonical`, `.name`, `.slug`, `.placeholders`, `.decode()`).
48
49
  * **Channel & Tariff Qualifiers:** Treats Channel ($B \neq 0$) and Tariff ($1 \le E < 255$) as name qualifiers on $(C, D)$ base measurements.
49
50
  * **4-Variant Localization Slugs:** Generates variant slugs (`base`, `_channel`, `_tariff`, `_channel_tariff`) with placeholder maps, ready for Home Assistant, Prometheus, or UI localization.
50
- * **Curated Catalog:** 31 standard German electricity registers (DIN EN 62056-6-1 / DIN 43863-3) with units, device classes, state classes, and icons.
51
+ * **Curated Catalog:** 39 standard German electricity registers (DIN EN 62056-6-1 / DIN 43863-3) with units, device classes, state classes, and icons.
51
52
 
52
53
  ---
53
54
 
@@ -75,12 +76,12 @@ from obis_parser import OBIS
75
76
  # Parses strings, bare-hex, dot-hex, or COSEM logical names
76
77
  obis = OBIS.parse("0100010800ff")
77
78
  if obis:
78
- print(obis.canonical) # "1-0:1.8.0" (or str(obis))
79
- print(obis.name) # "Active energy import"
80
- print(obis.slug) # "active_energy_import"
81
- print(obis.is_electricity) # True
82
- print(obis.channel) # 0
83
- print(obis.tariff) # 0
79
+ print(obis.canonical) # "1-0:1.8.0" (or str(obis))
80
+ print(obis.name) # "Active energy import"
81
+ print(obis.slug) # "active_energy_import"
82
+ print(obis.is_electricity) # True
83
+ print(obis.channel) # 0
84
+ print(obis.tariff) # 0
84
85
  ```
85
86
 
86
87
  ### 2. Multi-Meter Channels & Tariffs
@@ -89,10 +90,10 @@ if obis:
89
90
  # Multi-meter behind one gateway (Channel 1, Tariff 2)
90
91
  obis = OBIS.parse("1-1:1.8.2")
91
92
  if obis:
92
- print(obis.canonical) # "1-1:1.8.2"
93
- print(obis.name) # "Active energy import (Channel 1, Tariff 2)"
94
- print(obis.slug) # "active_energy_import_channel_tariff"
95
- print(obis.placeholders) # {"channel": "1", "tariff": "2"}
93
+ print(obis.canonical) # "1-1:1.8.2"
94
+ print(obis.name) # "Active energy import (Channel 1, Tariff 2)"
95
+ print(obis.slug) # "active_energy_import_channel_tariff"
96
+ print(obis.placeholders) # {"channel": "1", "tariff": "2"}
96
97
  ```
97
98
 
98
99
  ### 3. Full Metadata Decode (`obis.decode()`)
@@ -101,18 +102,18 @@ if obis:
101
102
  obis = OBIS.parse("0100200700ff") # Voltage L1 (32.7.0)
102
103
  if obis:
103
104
  m = obis.decode()
104
- print(m.name) # "Voltage L1"
105
- print(m.unit) # "V"
106
- print(m.device_class) # "voltage"
105
+ print(m.name) # "Voltage L1"
106
+ print(m.unit) # "V"
107
+ print(m.device_class) # "voltage"
107
108
  print(m.state_class) # "measurement"
108
- print(m.icon) # "mdi:sine-wave"
109
+ print(m.icon) # "mdi:sine-wave"
109
110
  ```
110
111
 
111
112
  ---
112
113
 
113
114
  ## Supported Registers (Catalog Overview)
114
115
 
115
- | (C, D) | Measurement | Slug | Unit | Device Class | State Class |
116
+ | (C, D, E) | Measurement | Slug | Unit | Device Class | State Class |
116
117
  |---|---|---|---|---|---|
117
118
  | **(1, 8)** | Active energy import | `active_energy_import` | `kWh` | `energy` | `total_increasing` |
118
119
  | **(2, 8)** | Active energy export | `active_energy_export` | `kWh` | `energy` | `total_increasing` |
@@ -135,6 +136,11 @@ if obis:
135
136
  | **(11, 7)** | Current (neutral/total) | `current` | `A` | `current` | `measurement` |
136
137
  | **(13, 7)** | Power factor ($\cos \varphi$) | `power_factor` | `None` | `power_factor` | `measurement` |
137
138
  | **(14, 7)** | Frequency | `frequency` | `Hz` | `frequency` | `measurement` |
139
+ | **(81, 7, 0)** | Phase angle | `phase_angle` | `°` | `None` | `measurement` |
140
+ | **(81, 7, 1/2)** | Phase angle U(L2)/U(L3) to U(L1) | `phase_angle_u_l2..3_l1` | `°` | `None` | `measurement` |
141
+ | **(81, 7, 4/15/26)** | Phase angle L1/L2/L3 (U to I) | `phase_angle_l1..3` | `°` | `None` | `measurement` |
142
+ | **(0, 2, 0)** | Firmware version | `firmware_version` | `None` | `None` | `None` |
143
+ | **(96, 1, 0)** | Meter identification / Serial | `meter_identification` | `None` | `None` | `None` |
138
144
 
139
145
  ---
140
146
 
@@ -15,13 +15,14 @@ Extracts, parses, and normalizes OBIS (*Object Identification System*) codes (`A
15
15
  * **Zero External Dependencies:** Built entirely with standard library Python (`dataclasses`, `re`, `string`, `typing`).
16
16
  * **Universal Ingestion:** Auto-detects and parses:
17
17
  * Canonical strings: `1-0:1.8.0`, `1-0:1.8.0*255`, `1-1:1.7.0`
18
+ * Shorthand display strings: `1.8.0`, `16.7.0`, `81.7.4` (defaults to electricity A=1, channel B=0)
18
19
  * Bare COSEM 12-hex: `0100010800ff`, `0100100700ff`, `010001080000` (F=0)
19
20
  * COSEM logical names with meter suffixes: `0100010800ff.1lgz0067285558.sm`
20
21
  * Dot-separated hex: `01.00.01.08.00.FF`
21
22
  * **Single Domain Model (`OBIS`):** Everything is accessed directly on the `OBIS` instance (`.canonical`, `.name`, `.slug`, `.placeholders`, `.decode()`).
22
23
  * **Channel & Tariff Qualifiers:** Treats Channel ($B \neq 0$) and Tariff ($1 \le E < 255$) as name qualifiers on $(C, D)$ base measurements.
23
24
  * **4-Variant Localization Slugs:** Generates variant slugs (`base`, `_channel`, `_tariff`, `_channel_tariff`) with placeholder maps, ready for Home Assistant, Prometheus, or UI localization.
24
- * **Curated Catalog:** 31 standard German electricity registers (DIN EN 62056-6-1 / DIN 43863-3) with units, device classes, state classes, and icons.
25
+ * **Curated Catalog:** 39 standard German electricity registers (DIN EN 62056-6-1 / DIN 43863-3) with units, device classes, state classes, and icons.
25
26
 
26
27
  ---
27
28
 
@@ -49,12 +50,12 @@ from obis_parser import OBIS
49
50
  # Parses strings, bare-hex, dot-hex, or COSEM logical names
50
51
  obis = OBIS.parse("0100010800ff")
51
52
  if obis:
52
- print(obis.canonical) # "1-0:1.8.0" (or str(obis))
53
- print(obis.name) # "Active energy import"
54
- print(obis.slug) # "active_energy_import"
55
- print(obis.is_electricity) # True
56
- print(obis.channel) # 0
57
- print(obis.tariff) # 0
53
+ print(obis.canonical) # "1-0:1.8.0" (or str(obis))
54
+ print(obis.name) # "Active energy import"
55
+ print(obis.slug) # "active_energy_import"
56
+ print(obis.is_electricity) # True
57
+ print(obis.channel) # 0
58
+ print(obis.tariff) # 0
58
59
  ```
59
60
 
60
61
  ### 2. Multi-Meter Channels & Tariffs
@@ -63,10 +64,10 @@ if obis:
63
64
  # Multi-meter behind one gateway (Channel 1, Tariff 2)
64
65
  obis = OBIS.parse("1-1:1.8.2")
65
66
  if obis:
66
- print(obis.canonical) # "1-1:1.8.2"
67
- print(obis.name) # "Active energy import (Channel 1, Tariff 2)"
68
- print(obis.slug) # "active_energy_import_channel_tariff"
69
- print(obis.placeholders) # {"channel": "1", "tariff": "2"}
67
+ print(obis.canonical) # "1-1:1.8.2"
68
+ print(obis.name) # "Active energy import (Channel 1, Tariff 2)"
69
+ print(obis.slug) # "active_energy_import_channel_tariff"
70
+ print(obis.placeholders) # {"channel": "1", "tariff": "2"}
70
71
  ```
71
72
 
72
73
  ### 3. Full Metadata Decode (`obis.decode()`)
@@ -75,18 +76,18 @@ if obis:
75
76
  obis = OBIS.parse("0100200700ff") # Voltage L1 (32.7.0)
76
77
  if obis:
77
78
  m = obis.decode()
78
- print(m.name) # "Voltage L1"
79
- print(m.unit) # "V"
80
- print(m.device_class) # "voltage"
79
+ print(m.name) # "Voltage L1"
80
+ print(m.unit) # "V"
81
+ print(m.device_class) # "voltage"
81
82
  print(m.state_class) # "measurement"
82
- print(m.icon) # "mdi:sine-wave"
83
+ print(m.icon) # "mdi:sine-wave"
83
84
  ```
84
85
 
85
86
  ---
86
87
 
87
88
  ## Supported Registers (Catalog Overview)
88
89
 
89
- | (C, D) | Measurement | Slug | Unit | Device Class | State Class |
90
+ | (C, D, E) | Measurement | Slug | Unit | Device Class | State Class |
90
91
  |---|---|---|---|---|---|
91
92
  | **(1, 8)** | Active energy import | `active_energy_import` | `kWh` | `energy` | `total_increasing` |
92
93
  | **(2, 8)** | Active energy export | `active_energy_export` | `kWh` | `energy` | `total_increasing` |
@@ -109,6 +110,11 @@ if obis:
109
110
  | **(11, 7)** | Current (neutral/total) | `current` | `A` | `current` | `measurement` |
110
111
  | **(13, 7)** | Power factor ($\cos \varphi$) | `power_factor` | `None` | `power_factor` | `measurement` |
111
112
  | **(14, 7)** | Frequency | `frequency` | `Hz` | `frequency` | `measurement` |
113
+ | **(81, 7, 0)** | Phase angle | `phase_angle` | `°` | `None` | `measurement` |
114
+ | **(81, 7, 1/2)** | Phase angle U(L2)/U(L3) to U(L1) | `phase_angle_u_l2..3_l1` | `°` | `None` | `measurement` |
115
+ | **(81, 7, 4/15/26)** | Phase angle L1/L2/L3 (U to I) | `phase_angle_l1..3` | `°` | `None` | `measurement` |
116
+ | **(0, 2, 0)** | Firmware version | `firmware_version` | `None` | `None` | `None` |
117
+ | **(96, 1, 0)** | Meter identification / Serial | `meter_identification` | `None` | `None` | `None` |
112
118
 
113
119
  ---
114
120
 
@@ -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.1'
22
- __version_tuple__ = version_tuple = (0, 0, 1)
21
+ __version__ = version = '0.0.2'
22
+ __version_tuple__ = version_tuple = (0, 0, 2)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -2,7 +2,7 @@
2
2
 
3
3
  This module is dependency-free (standard library only) and provides:
4
4
  - Object-oriented `OBIS` domain class with `OBIS.parse()`, canonicalization, and metadata.
5
- - Full catalog of 31 German smart meter electricity registers (IEC 62056-6-1 / DIN 43863-3).
5
+ - Full catalog of 39 German smart meter electricity registers (IEC 62056-6-1 / DIN 43863-3).
6
6
  - Resolved measurement metadata via `obis.decode()` with 4-variant localization slugs.
7
7
  """
8
8
 
@@ -15,6 +15,7 @@ from typing import Literal
15
15
 
16
16
  _HEXDIGITS = set(hexdigits)
17
17
  _OBIS_STRING_RE = re.compile(r"^(\d+)\s*-\s*(\d+)\s*:\s*(\d+)\s*\.\s*(\d+)\s*\.\s*(\d+)(?:\s*\*\s*(\d+))?$")
18
+ _OBIS_SHORT_STRING_RE = re.compile(r"^(\d+)\s*\.\s*(\d+)\s*\.\s*(\d+)(?:\s*\*\s*(\d+))?$")
18
19
  _OBIS_DOT_HEX_RE = re.compile(
19
20
  r"^([0-9a-fA-F]{2})\.([0-9a-fA-F]{2})\.([0-9a-fA-F]{2})\."
20
21
  r"([0-9a-fA-F]{2})\.([0-9a-fA-F]{2})\.([0-9a-fA-F]{2})$"
@@ -57,10 +58,11 @@ ELECTRICITY_MEDIUM = 1
57
58
  #: F (billing period) values meaning "current period": absent, 0, or 255.
58
59
  _CURRENT_PERIOD_F = (0, 255)
59
60
 
60
- #: A measurement's identity: the (C, D) OBIS value groups — C is the physical
61
- #: quantity, D the measurement/processing type (IEC 62056-6-1). The channel (B)
62
- #: and tariff (E) fields are qualifiers and are NOT part of this key.
63
- type MeasurementKey = tuple[OBISPhysicalQuantity, OBISMeasurementType]
61
+ #: A measurement's identity: either the (C, D) OBIS value groups or an exact (C, D, E)
62
+ #: triplet for registers where E represents a specific phase/angle (e.g. phase angles 81.7.x).
63
+ type MeasurementKey = (
64
+ tuple[OBISPhysicalQuantity, OBISMeasurementType] | tuple[OBISPhysicalQuantity, OBISMeasurementType, OBISTariff]
65
+ )
64
66
 
65
67
 
66
68
  @dataclass(frozen=True)
@@ -97,6 +99,16 @@ class OBIS:
97
99
  f=int(match.group(6)) if match.group(6) is not None else None,
98
100
  )
99
101
 
102
+ if match := _OBIS_SHORT_STRING_RE.match(code):
103
+ return cls(
104
+ a=ELECTRICITY_MEDIUM,
105
+ b=0,
106
+ c=int(match.group(1)),
107
+ d=int(match.group(2)),
108
+ e=int(match.group(3)),
109
+ f=int(match.group(4)) if match.group(4) is not None else None,
110
+ )
111
+
100
112
  if match := _OBIS_DOT_HEX_RE.match(code):
101
113
  return cls(
102
114
  a=int(match.group(1), 16),
@@ -247,7 +259,7 @@ class OBISNameDescriptor:
247
259
 
248
260
  @dataclass(frozen=True)
249
261
  class OBISMeasurement:
250
- """A fully-resolved OBIS measurement with values, units, and localized metadata."""
262
+ """A fully-resolved OBIS measurement descriptor with units, device classes, and localized metadata."""
251
263
 
252
264
  code: OBIS
253
265
  canonical: str
@@ -488,17 +500,52 @@ OBIS_CATALOG: dict[MeasurementKey, OBISMeasurementInfo] = {
488
500
  2,
489
501
  ),
490
502
  (14, 7): OBISMeasurementInfo("Frequency", "frequency", "frequency", "measurement", "Hz", "mdi:sine-wave", 2),
503
+ # Phase angles (C=81, D=7)
504
+ (81, 7, 0): OBISMeasurementInfo("Phase angle", "phase_angle", None, "measurement", "°", "mdi:angle-acute", 1),
505
+ (81, 7, 1): OBISMeasurementInfo(
506
+ "Phase angle U(L2)-U(L1)", "phase_angle_u_l2_l1", None, "measurement", "°", "mdi:angle-acute", 1
507
+ ),
508
+ (81, 7, 2): OBISMeasurementInfo(
509
+ "Phase angle U(L3)-U(L1)", "phase_angle_u_l3_l1", None, "measurement", "°", "mdi:angle-acute", 1
510
+ ),
511
+ (81, 7, 4): OBISMeasurementInfo("Phase angle L1", "phase_angle_l1", None, "measurement", "°", "mdi:angle-acute", 1),
512
+ (81, 7, 15): OBISMeasurementInfo(
513
+ "Phase angle L2", "phase_angle_l2", None, "measurement", "°", "mdi:angle-acute", 1
514
+ ),
515
+ (81, 7, 26): OBISMeasurementInfo(
516
+ "Phase angle L3", "phase_angle_l3", None, "measurement", "°", "mdi:angle-acute", 1
517
+ ),
518
+ # Device metadata & identification (exact E=0)
519
+ (0, 2, 0): OBISMeasurementInfo(
520
+ "Firmware version",
521
+ "firmware_version",
522
+ None,
523
+ None,
524
+ None,
525
+ "mdi:chip",
526
+ ),
527
+ (96, 1, 0): OBISMeasurementInfo(
528
+ "Meter identification",
529
+ "meter_identification",
530
+ None,
531
+ None,
532
+ None,
533
+ "mdi:identifier",
534
+ ),
491
535
  }
492
536
 
493
537
 
494
538
  def _get_obis_info(obis: OBIS) -> OBISMeasurementInfo | None:
495
- """Look up measurement metadata from the catalog by (C, D)."""
539
+ """Look up measurement metadata from the catalog by (C, D, E) or (C, D)."""
496
540
  if not obis.is_electricity:
497
541
  return None
498
542
 
499
543
  if obis.f is not None and obis.f not in _CURRENT_PERIOD_F:
500
544
  return None
501
545
 
546
+ if info := OBIS_CATALOG.get((obis.c, obis.d, obis.e)):
547
+ return info
548
+
502
549
  return OBIS_CATALOG.get((obis.c, obis.d))
503
550
 
504
551
 
@@ -518,7 +565,8 @@ def _describe_obis(obis: OBIS, info: OBISMeasurementInfo | None) -> OBISNameDesc
518
565
  )
519
566
 
520
567
  has_channel = obis.b != 0
521
- has_tariff = _has_tariff(obis.e)
568
+ is_exact_e = (obis.c, obis.d, obis.e) in OBIS_CATALOG
569
+ has_tariff = not is_exact_e and _has_tariff(obis.e)
522
570
 
523
571
  placeholders: dict[str, str] = {}
524
572
  fallback = info.name
@@ -0,0 +1,81 @@
1
+ """Tests for the OBIS catalog and metadata lookup."""
2
+
3
+ import pytest
4
+
5
+ from obis_parser import (
6
+ OBIS,
7
+ OBIS_CATALOG,
8
+ OBISMeasurementInfo,
9
+ )
10
+
11
+
12
+ class TestCatalog:
13
+ def test_catalog_size_and_uniqueness(self) -> None:
14
+ """Verify catalog size and slug uniqueness."""
15
+ assert len(OBIS_CATALOG) == 39
16
+ slugs = [info.slug for info in OBIS_CATALOG.values()]
17
+ assert len(slugs) == len(set(slugs)), "All catalog slugs must be unique"
18
+ assert all(slug for slug in slugs), "Catalog slugs must not be empty"
19
+
20
+ def test_catalog_schema_invariants(self) -> None:
21
+ """Verify all catalog entries satisfy structural domain invariants."""
22
+ for key, info in OBIS_CATALOG.items():
23
+ assert isinstance(info, OBISMeasurementInfo)
24
+ assert info.name.strip(), f"Empty name for key {key}"
25
+ assert info.slug.strip(), f"Empty slug for key {key}"
26
+ assert info.icon.startswith("mdi:"), f"Invalid icon '{info.icon}' for key {key}"
27
+ assert info.translation_key == info.slug
28
+
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
+ if info.suggested_display_precision is not None:
38
+ assert info.suggested_display_precision >= 0
39
+
40
+ @pytest.mark.parametrize(
41
+ "code",
42
+ [
43
+ "2-0:1.8.0", # Medium != 1
44
+ "7-0:3.0.0", # Gas
45
+ "1-0:1.8.0*1", # Historic billing period
46
+ "1-0:81.7.99", # Uncataloged phase angle E
47
+ "1-0:0.2.1", # Uncataloged metadata E
48
+ "1-0:96.1.1", # Uncataloged serial E
49
+ ],
50
+ )
51
+ def test_unmatched_codes_return_none(self, code: str) -> None:
52
+ """Non-electricity, historic periods, and uncataloged exact E return info=None."""
53
+ obis = OBIS.parse(code)
54
+ assert obis is not None
55
+ assert obis.info is None
56
+
57
+ @pytest.mark.parametrize(
58
+ ("code", "expected_slug"),
59
+ [
60
+ # Base (C, D) lookups ignore channel and tariff
61
+ ("1-0:1.8.0", "active_energy_import"),
62
+ ("1-1:1.8.0", "active_energy_import"),
63
+ ("1-0:1.8.1", "active_energy_import"),
64
+ ("1-2:1.8.3", "active_energy_import"),
65
+ # Current billing periods (F=None, 0, 255) resolve base info
66
+ ("1-0:1.8.0*0", "active_energy_import"),
67
+ ("1-0:1.8.0*255", "active_energy_import"),
68
+ # Exact (C, D, E) lookups for phase angles and device metadata
69
+ ("1-0:81.7.4", "phase_angle_l1"),
70
+ ("1-1:81.7.4", "phase_angle_l1"),
71
+ ("1-0:0.2.0", "firmware_version"),
72
+ ("1-0:96.1.0", "meter_identification"),
73
+ ],
74
+ )
75
+ def test_get_obis_info_lookup_resolution(self, code: str, expected_slug: str) -> None:
76
+ """info lookup resolves base (C, D) or exact (C, D, E) entries."""
77
+ obis = OBIS.parse(code)
78
+ assert obis is not None
79
+ info = obis.info
80
+ assert info is not None
81
+ assert info.slug == expected_slug
@@ -0,0 +1,218 @@
1
+ """Tests for OBIS.decode() and OBIS naming methods."""
2
+
3
+ import pytest
4
+
5
+ from obis_parser import (
6
+ OBIS,
7
+ OBISMeasurement,
8
+ OBISNameDescriptor,
9
+ )
10
+
11
+
12
+ class TestOBISNaming:
13
+ @pytest.mark.parametrize(
14
+ ("code", "expected"),
15
+ [
16
+ # Base register without modifiers
17
+ (
18
+ "1-0:1.8.0",
19
+ OBISNameDescriptor(
20
+ slug="active_energy_import",
21
+ placeholders={},
22
+ fallback_name="Active energy import",
23
+ ),
24
+ ),
25
+ # Channel modifier on base register
26
+ (
27
+ "1-1:1.7.0",
28
+ OBISNameDescriptor(
29
+ slug="active_power_import_channel",
30
+ placeholders={"channel": "1"},
31
+ fallback_name="Active power import (Channel 1)",
32
+ ),
33
+ ),
34
+ # Tariff modifier on base register
35
+ (
36
+ "1-0:1.8.2",
37
+ OBISNameDescriptor(
38
+ slug="active_energy_import_tariff",
39
+ placeholders={"tariff": "2"},
40
+ fallback_name="Active energy import (Tariff 2)",
41
+ ),
42
+ ),
43
+ # Channel + tariff on base register
44
+ (
45
+ "1-3:1.8.4",
46
+ OBISNameDescriptor(
47
+ slug="active_energy_import_channel_tariff",
48
+ placeholders={"channel": "3", "tariff": "4"},
49
+ fallback_name="Active energy import (Channel 3, Tariff 4)",
50
+ ),
51
+ ),
52
+ # Tariff E=255 means total/unspecified (no tariff suffix)
53
+ (
54
+ "1-0:1.8.255",
55
+ OBISNameDescriptor(
56
+ slug="active_energy_import",
57
+ placeholders={},
58
+ fallback_name="Active energy import",
59
+ ),
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
+ ),
78
+ (
79
+ "1-0:81.7.4",
80
+ OBISNameDescriptor(
81
+ slug="phase_angle_l1",
82
+ placeholders={},
83
+ fallback_name="Phase angle L1",
84
+ ),
85
+ ),
86
+ # Channel on exact (C, D, E) register (must NOT generate tariff suffix!)
87
+ (
88
+ "1-1:81.7.4",
89
+ OBISNameDescriptor(
90
+ slug="phase_angle_l1_channel",
91
+ placeholders={"channel": "1"},
92
+ fallback_name="Phase angle L1 (Channel 1)",
93
+ ),
94
+ ),
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
+ ],
113
+ )
114
+ def test_describe_and_naming_properties(
115
+ self,
116
+ code: str,
117
+ expected: OBISNameDescriptor,
118
+ ) -> None:
119
+ """Verify naming, slug variants, and placeholders across all qualifier patterns."""
120
+ obis = OBIS.parse(code)
121
+ assert obis is not None
122
+ 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
127
+
128
+
129
+ class TestOBISDecode:
130
+ def test_decode_direct_dataclass_comparison(self) -> None:
131
+ """Verify fully-resolved OBISMeasurement dataclass construction directly."""
132
+ obis_energy = OBIS(1, 0, 1, 8, 0)
133
+ assert obis_energy.decode() == OBISMeasurement(
134
+ code=obis_energy,
135
+ canonical="1-0:1.8.0",
136
+ slug="active_energy_import",
137
+ name="Active energy import",
138
+ unit="kWh",
139
+ device_class="energy",
140
+ state_class="total_increasing",
141
+ icon="mdi:home-import-outline",
142
+ suggested_display_precision=5,
143
+ placeholders={},
144
+ info=obis_energy.info,
145
+ )
146
+
147
+ obis_angle = OBIS(1, 0, 81, 7, 4)
148
+ assert obis_angle.decode() == OBISMeasurement(
149
+ code=obis_angle,
150
+ canonical="1-0:81.7.4",
151
+ slug="phase_angle_l1",
152
+ name="Phase angle L1",
153
+ unit="°",
154
+ device_class=None,
155
+ state_class="measurement",
156
+ icon="mdi:angle-acute",
157
+ suggested_display_precision=1,
158
+ placeholders={},
159
+ info=obis_angle.info,
160
+ )
161
+
162
+ obis_fw = OBIS(1, 0, 0, 2, 0)
163
+ assert obis_fw.decode() == OBISMeasurement(
164
+ code=obis_fw,
165
+ canonical="1-0:0.2.0",
166
+ slug="firmware_version",
167
+ name="Firmware version",
168
+ unit=None,
169
+ device_class=None,
170
+ state_class=None,
171
+ icon="mdi:chip",
172
+ suggested_display_precision=None,
173
+ placeholders={},
174
+ info=obis_fw.info,
175
+ )
176
+
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()
194
+
195
+ @pytest.mark.parametrize(
196
+ ("code", "expected_canonical"),
197
+ [
198
+ ("1-0:99.99.99", "1-0:99.99.99"),
199
+ ("1-0:81.7.99", "1-0:81.7.99"),
200
+ ("1-0:0.2.1", "1-0:0.2.1"),
201
+ ("1-0:96.1.1", "1-0:96.1.1"),
202
+ ("1-0:1.8.0*1", "1-0:1.8.0*1"),
203
+ ("7-0:3.0.0", "7-0:3.0.0"),
204
+ ],
205
+ )
206
+ def test_decode_fallback_codes_as_unknown(self, code: str, expected_canonical: str) -> None:
207
+ """Uncataloged codes, historic periods, and non-electricity mediums decode as unknown_code."""
208
+ obis = OBIS.parse(code)
209
+ assert obis is not None
210
+ m = obis.decode()
211
+ assert m.canonical == expected_canonical
212
+ assert m.slug == "unknown_code"
213
+ assert m.translation_key == "unknown_code"
214
+ assert m.name == f"OBIS {expected_canonical}"
215
+ assert m.placeholders == {"code": expected_canonical}
216
+ assert m.info is None
217
+ assert m.unit is None
218
+ assert m.icon == "mdi:gauge"
@@ -0,0 +1,148 @@
1
+ """Tests for OBIS code parsing and normalization."""
2
+
3
+ import pytest
4
+
5
+ from obis_parser import (
6
+ OBIS,
7
+ ParsedOBIS,
8
+ )
9
+
10
+
11
+ class TestParseObis:
12
+ @pytest.mark.parametrize(
13
+ ("raw", "expected"),
14
+ [
15
+ # Canonical standard notations
16
+ ("1-0:1.8.0", OBIS(1, 0, 1, 8, 0, None)),
17
+ ("1-0:1.8.0*255", OBIS(1, 0, 1, 8, 0, 255)),
18
+ ("1-0:1.8.0*0", OBIS(1, 0, 1, 8, 0, 0)),
19
+ ("1-0:1.8.0*1", OBIS(1, 0, 1, 8, 0, 1)),
20
+ ("1-1:16.7.0", OBIS(1, 1, 16, 7, 0, None)),
21
+ ("1-0:1.8.1", OBIS(1, 0, 1, 8, 1, None)),
22
+ ("1-2:1.8.3", OBIS(1, 2, 1, 8, 3, None)),
23
+ ("1-12:1.8.128*50", OBIS(1, 12, 1, 8, 128, 50)),
24
+ # Shorthand 3-group notation (defaults to A=1, B=0)
25
+ ("1.8.0", OBIS(1, 0, 1, 8, 0, None)),
26
+ ("1.8.0*255", OBIS(1, 0, 1, 8, 0, 255)),
27
+ ("1.8.0*1", OBIS(1, 0, 1, 8, 0, 1)),
28
+ ("1.8.1", OBIS(1, 0, 1, 8, 1, None)),
29
+ ("16.7.0", OBIS(1, 0, 16, 7, 0, None)),
30
+ ("81.7.4", OBIS(1, 0, 81, 7, 4, None)),
31
+ ("0.2.0", OBIS(1, 0, 0, 2, 0, None)),
32
+ ("96.1.0", OBIS(1, 0, 96, 1, 0, None)),
33
+ # Dot-separated hex
34
+ ("01.00.01.08.00.ff", OBIS(1, 0, 1, 8, 0, 255)),
35
+ ("01.00.01.08.00.FF", OBIS(1, 0, 1, 8, 0, 255)),
36
+ ("01.01.10.07.00.00", OBIS(1, 1, 16, 7, 0, 0)),
37
+ # Bare 12-char COSEM hex
38
+ ("0100010800ff", OBIS(1, 0, 1, 8, 0, 255)),
39
+ ("0100010800FF", OBIS(1, 0, 1, 8, 0, 255)),
40
+ ("010001080000", OBIS(1, 0, 1, 8, 0, 0)),
41
+ ("010051070fff", OBIS(1, 0, 81, 7, 15, 255)),
42
+ ("010051071aff", OBIS(1, 0, 81, 7, 26, 255)),
43
+ # Logical name with meter suffix
44
+ ("0100010800ff.1test000000001.sm", OBIS(1, 0, 1, 8, 0, 255)),
45
+ ("0100200700ff.1lgz0067285558.sm", OBIS(1, 0, 32, 7, 0, 255)),
46
+ # Whitespace handling
47
+ (" 1-0:1.8.0 ", OBIS(1, 0, 1, 8, 0, None)),
48
+ ("1-0:1.8.0 * 255", OBIS(1, 0, 1, 8, 0, 255)),
49
+ ("1 - 0 : 1 . 8 . 0", OBIS(1, 0, 1, 8, 0, None)),
50
+ (" 1.8.0 ", OBIS(1, 0, 1, 8, 0, None)),
51
+ ("1.8.0 * 255", OBIS(1, 0, 1, 8, 0, 255)),
52
+ ("1 . 8 . 0", OBIS(1, 0, 1, 8, 0, None)),
53
+ ("\t1.8.0\n", OBIS(1, 0, 1, 8, 0, None)),
54
+ ],
55
+ )
56
+ def test_valid_forms(self, raw: str, expected: OBIS) -> None:
57
+ assert OBIS.parse(raw) == expected
58
+ assert OBIS.from_string(raw) == expected
59
+
60
+ @pytest.mark.parametrize(
61
+ "raw",
62
+ [
63
+ "",
64
+ " ",
65
+ "not-an-obis-code",
66
+ "1-0:1.8",
67
+ "1-0:1.8.0.0",
68
+ "1-0:1.8.0*",
69
+ "1-0:1.8.0*abc",
70
+ "1-0:1.8.0*1*2",
71
+ "1.8",
72
+ "1.8.0.0",
73
+ "1.8.0*",
74
+ "1.8.0*abc",
75
+ "1.8.0*1*2",
76
+ "-1.8.0",
77
+ "1.-8.0",
78
+ "1.8.-0",
79
+ ".1.8.0",
80
+ "1.8.0.",
81
+ "1:8:0",
82
+ "0100010800",
83
+ "0100010800ff11",
84
+ "0100010800gg",
85
+ "zzgg00010800ff",
86
+ "01.00.01.08.00",
87
+ "01.00.01.08.00.00.00",
88
+ "01.00.01.08.00.gg",
89
+ "0100010800.meter.sm",
90
+ ],
91
+ )
92
+ def test_invalid_returns_none(self, raw: str) -> None:
93
+ assert OBIS.parse(raw) is None
94
+ assert OBIS.from_string(raw) is None
95
+
96
+ def test_backwards_compat_alias(self) -> None:
97
+ assert ParsedOBIS is OBIS
98
+
99
+
100
+ class TestOBISProperties:
101
+ @pytest.mark.parametrize(
102
+ ("obis", "expected_str"),
103
+ [
104
+ (OBIS(1, 0, 1, 8, 0, None), "1-0:1.8.0"),
105
+ (OBIS(1, 1, 16, 7, 0, None), "1-1:16.7.0"),
106
+ (OBIS(1, 0, 81, 7, 4, None), "1-0:81.7.4"),
107
+ (OBIS(1, 0, 1, 8, 0, 255), "1-0:1.8.0"),
108
+ (OBIS(1, 0, 1, 8, 0, 0), "1-0:1.8.0"),
109
+ (OBIS(1, 0, 1, 8, 0, 1), "1-0:1.8.0*1"),
110
+ (OBIS(1, 0, 81, 7, 4, 1), "1-0:81.7.4*1"),
111
+ ],
112
+ )
113
+ def test_string_representations_and_f_suppression(self, obis: OBIS, expected_str: str) -> None:
114
+ assert str(obis) == expected_str
115
+ assert obis.canonical == expected_str
116
+ assert obis.to_obis_string() == expected_str
117
+
118
+ def test_convenience_property_aliases(self) -> None:
119
+ """Verify property aliases mirror positional fields."""
120
+ obis = OBIS(1, 2, 3, 4, 5, 6)
121
+ assert obis.is_electricity is True
122
+ assert (
123
+ (
124
+ obis.medium,
125
+ obis.channel,
126
+ obis.physical_quantity,
127
+ obis.measurement_type,
128
+ obis.tariff,
129
+ obis.billing_period,
130
+ )
131
+ == (obis.a, obis.b, obis.c, obis.d, obis.e, obis.f)
132
+ == (1, 2, 3, 4, 5, 6)
133
+ )
134
+
135
+ gas = OBIS(7, 0, 3, 0, 0, None)
136
+ assert gas.is_electricity is False
137
+ assert gas.medium == gas.a == 7
138
+
139
+ def test_obis_dataclass_immutability_and_hashability(self) -> None:
140
+ """OBIS is a frozen dataclass and can be hashed/stored in sets."""
141
+ obis1 = OBIS(1, 0, 1, 8, 0)
142
+ obis2 = OBIS(1, 0, 1, 8, 0)
143
+ assert obis1 == obis2
144
+ assert hash(obis1) == hash(obis2)
145
+ assert len({obis1, obis2}) == 1
146
+
147
+ with pytest.raises(AttributeError):
148
+ obis1.a = 2 # type: ignore[misc]
@@ -1,74 +0,0 @@
1
- """Tests for the OBIS catalog and metadata lookup."""
2
-
3
- import pytest
4
-
5
- from obis_parser import (
6
- OBIS,
7
- OBIS_CATALOG,
8
- )
9
-
10
-
11
- class TestCatalog:
12
- def test_catalog_size_and_uniqueness(self) -> None:
13
- assert len(OBIS_CATALOG) == 31
14
- slugs = [info.slug for info in OBIS_CATALOG.values()]
15
- assert len(slugs) == len(set(slugs)), "All catalog slugs must be unique"
16
- assert all(slug for slug in slugs), "Catalog slugs must not be empty"
17
-
18
- @pytest.mark.parametrize(
19
- ("code", "expected_cd", "expected_slug"),
20
- [
21
- ("1-0:1.8.0", (1, 8), "active_energy_import"),
22
- ("1-0:2.8.0", (2, 8), "active_energy_export"),
23
- ("1-0:16.7.0", (16, 7), "active_power_total"),
24
- ("1-0:15.7.0", (15, 7), "absolute_active_power"),
25
- ("1-0:32.7.0", (32, 7), "voltage_l1"),
26
- ("1-0:52.7.0", (52, 7), "voltage_l2"),
27
- ("1-0:72.7.0", (72, 7), "voltage_l3"),
28
- ("1-0:31.7.0", (31, 7), "current_l1"),
29
- ("1-0:21.7.0", (21, 7), "active_power_import_l1"),
30
- ("1-0:22.7.0", (22, 7), "active_power_export_l1"),
31
- ("1-0:13.7.0", (13, 7), "power_factor"),
32
- ("1-0:14.7.0", (14, 7), "frequency"),
33
- ("1-0:3.8.0", (3, 8), "reactive_energy_import"),
34
- ("1-0:4.8.0", (4, 8), "reactive_energy_export"),
35
- ("1-0:9.8.0", (9, 8), "apparent_energy"),
36
- ],
37
- )
38
- def test_catalog_entries(self, code: str, expected_cd: tuple[int, int], expected_slug: str) -> None:
39
- obis = OBIS.parse(code)
40
- assert obis is not None
41
- info = obis.info
42
- assert info is not None
43
- assert (obis.c, obis.d) == expected_cd
44
- assert info.slug == expected_slug
45
- assert info.translation_key == expected_slug
46
-
47
- def test_non_electricity_has_no_catalog_match(self) -> None:
48
- obis = OBIS.parse("2-0:1.8.0")
49
- assert obis is not None
50
- assert obis.info is None
51
-
52
- obis_gas = OBIS.parse("7-0:3.0.0")
53
- assert obis_gas is not None
54
- assert obis_gas.info is None
55
-
56
- def test_non_current_billing_period_has_no_catalog_match(self) -> None:
57
- obis = OBIS.parse("1-0:1.8.0*1")
58
- assert obis is not None
59
- assert obis.info is None
60
-
61
- @pytest.mark.parametrize(
62
- ("code", "expected_cd"),
63
- [
64
- ("1-1:1.8.0", (1, 8)), # Channel 1
65
- ("1-0:1.8.1", (1, 8)), # Tariff 1
66
- ("1-2:1.8.3", (1, 8)), # Channel 2, Tariff 3
67
- ],
68
- )
69
- def test_get_obis_info_ignores_channel_and_tariff(self, code: str, expected_cd: tuple[int, int]) -> None:
70
- obis = OBIS.parse(code)
71
- assert obis is not None
72
- info = obis.info
73
- assert info is not None
74
- assert (obis.c, obis.d) == expected_cd
@@ -1,88 +0,0 @@
1
- """Tests for OBIS.decode() and OBIS naming methods."""
2
-
3
- from obis_parser import OBIS
4
-
5
-
6
- class TestOBISDecode:
7
- def test_decode_basic_code(self) -> None:
8
- obis = OBIS.parse("1-0:1.8.0")
9
- assert obis is not None
10
- m = obis.decode()
11
- assert m.canonical == "1-0:1.8.0"
12
- assert m.slug == "active_energy_import"
13
- assert m.translation_key == "active_energy_import"
14
- assert m.name == "Active energy import"
15
- assert m.unit == "kWh"
16
- assert m.device_class == "energy"
17
- assert m.state_class == "total_increasing"
18
- assert m.placeholders == {}
19
- assert m.info is not None
20
-
21
- def test_decode_with_channel(self) -> None:
22
- obis = OBIS.parse("1-1:1.7.0")
23
- assert obis is not None
24
- m = obis.decode()
25
- assert m.canonical == "1-1:1.7.0"
26
- assert m.slug == "active_power_import_channel"
27
- assert m.name == "Active power import (Channel 1)"
28
- assert m.placeholders == {"channel": "1"}
29
-
30
- def test_decode_with_tariff(self) -> None:
31
- obis = OBIS.parse("1-0:1.8.2")
32
- assert obis is not None
33
- m = obis.decode()
34
- assert m.canonical == "1-0:1.8.2"
35
- assert m.slug == "active_energy_import_tariff"
36
- assert m.name == "Active energy import (Tariff 2)"
37
- assert m.placeholders == {"tariff": "2"}
38
-
39
- def test_decode_with_channel_and_tariff(self) -> None:
40
- obis = OBIS.parse("1-3:1.8.4")
41
- assert obis is not None
42
- m = obis.decode()
43
- assert m.canonical == "1-3:1.8.4"
44
- assert m.slug == "active_energy_import_channel_tariff"
45
- assert m.name == "Active energy import (Channel 3, Tariff 4)"
46
- assert m.placeholders == {"channel": "3", "tariff": "4"}
47
-
48
- def test_decode_direct_instance(self) -> None:
49
- obis = OBIS(1, 0, 16, 7, 0)
50
- m = obis.decode()
51
- assert m.canonical == "1-0:16.7.0"
52
- assert m.slug == "active_power_total"
53
-
54
- def test_decode_unknown_code(self) -> None:
55
- obis = OBIS.parse("1-0:99.99.99")
56
- assert obis is not None
57
- m = obis.decode()
58
- assert m.canonical == "1-0:99.99.99"
59
- assert m.slug == "unknown_code"
60
- assert m.name == "OBIS 1-0:99.99.99"
61
- assert m.placeholders == {"code": "1-0:99.99.99"}
62
- assert m.info is None
63
- assert m.unit is None
64
-
65
-
66
- class TestOBISNaming:
67
- def test_describe_and_properties(self) -> None:
68
- obis = OBIS.parse("1-2:1.8.5")
69
- assert obis is not None
70
-
71
- # Object properties
72
- assert obis.name == "Active energy import (Channel 2, Tariff 5)"
73
- assert obis.slug == "active_energy_import_channel_tariff"
74
- assert obis.translation_key == "active_energy_import_channel_tariff"
75
- assert obis.placeholders == {"channel": "2", "tariff": "5"}
76
-
77
- # describe() method
78
- d = obis.describe()
79
- assert d.slug == "active_energy_import_channel_tariff"
80
- assert d.translation_key == "active_energy_import_channel_tariff"
81
- assert d.placeholders == {"channel": "2", "tariff": "5"}
82
- assert d.fallback_name == "Active energy import (Channel 2, Tariff 5)"
83
-
84
- def test_name_property_simple(self) -> None:
85
- obis = OBIS.parse("1-0:32.7.0")
86
- assert obis is not None
87
- assert obis.name == "Voltage L1"
88
- assert obis.slug == "voltage_l1"
@@ -1,97 +0,0 @@
1
- """Tests for OBIS code parsing and normalization."""
2
-
3
- import pytest
4
-
5
- from obis_parser import (
6
- OBIS,
7
- ParsedOBIS,
8
- )
9
-
10
-
11
- class TestParseObis:
12
- @pytest.mark.parametrize(
13
- ("raw", "expected"),
14
- [
15
- ("1-0:1.8.0", OBIS(1, 0, 1, 8, 0, None)),
16
- ("1-0:1.8.0*255", OBIS(1, 0, 1, 8, 0, 255)),
17
- ("1-1:16.7.0", OBIS(1, 1, 16, 7, 0, None)),
18
- ("1-0:1.8.1", OBIS(1, 0, 1, 8, 1, None)),
19
- ("1-2:1.8.3", OBIS(1, 2, 1, 8, 3, None)),
20
- ("01.00.01.08.00.ff", OBIS(1, 0, 1, 8, 0, 255)),
21
- ("01.00.01.08.00.FF", OBIS(1, 0, 1, 8, 0, 255)),
22
- ("0100010800ff", OBIS(1, 0, 1, 8, 0, 255)),
23
- ("010001080000", OBIS(1, 0, 1, 8, 0, 0)),
24
- # Logical name with meter suffix
25
- ("0100010800ff.1test000000001.sm", OBIS(1, 0, 1, 8, 0, 255)),
26
- ("0100200700ff.1lgz0067285558.sm", OBIS(1, 0, 32, 7, 0, 255)),
27
- # Spaces
28
- (" 1-0:1.8.0 ", OBIS(1, 0, 1, 8, 0, None)),
29
- ("1-0:1.8.0 * 255", OBIS(1, 0, 1, 8, 0, 255)),
30
- ],
31
- )
32
- def test_valid_forms(self, raw: str, expected: OBIS) -> None:
33
- assert OBIS.parse(raw) == expected
34
- assert OBIS.from_string(raw) == expected
35
-
36
- @pytest.mark.parametrize(
37
- "raw",
38
- [
39
- "",
40
- " ",
41
- "not-an-obis-code",
42
- "1-0:1.8",
43
- "0100010800",
44
- "zzgg00010800ff",
45
- ],
46
- )
47
- def test_invalid_returns_none(self, raw: str) -> None:
48
- assert OBIS.parse(raw) is None
49
- assert OBIS.from_string(raw) is None
50
-
51
- def test_backwards_compat_alias(self) -> None:
52
- assert ParsedOBIS is OBIS
53
-
54
-
55
- class TestOBISProperties:
56
- def test_string_representations(self) -> None:
57
- obis = OBIS(1, 0, 1, 8, 0, None)
58
- assert str(obis) == "1-0:1.8.0"
59
- assert obis.canonical == "1-0:1.8.0"
60
- assert obis.to_obis_string() == "1-0:1.8.0"
61
-
62
- def test_f_suppression(self) -> None:
63
- assert str(OBIS(1, 0, 1, 8, 0, 255)) == "1-0:1.8.0"
64
- assert str(OBIS(1, 0, 1, 8, 0, 0)) == "1-0:1.8.0"
65
- assert str(OBIS(1, 0, 1, 8, 0, 1)) == "1-0:1.8.0*1"
66
-
67
- def test_convenience_properties(self) -> None:
68
- obis = OBIS(1, 2, 1, 8, 3, 255)
69
- assert obis.is_electricity is True
70
- assert obis.medium == 1
71
- assert obis.channel == 2
72
- assert obis.physical_quantity == 1
73
- assert obis.measurement_type == 8
74
- assert obis.tariff == 3
75
- assert obis.billing_period == 255
76
- assert obis.info is not None
77
-
78
- def test_non_electricity(self) -> None:
79
- obis = OBIS(7, 0, 3, 0, 0, None)
80
- assert obis.is_electricity is False
81
- assert obis.medium == 7
82
-
83
- @pytest.mark.parametrize(
84
- "raw",
85
- [
86
- "1-0:1.8.0",
87
- " 1-0:1.8.0*255 ",
88
- "0100010800ff",
89
- "010001080000",
90
- "01.00.01.08.00.FF",
91
- "0100010800ff.meter.sm",
92
- ],
93
- )
94
- def test_canonical_property_on_parsed_forms(self, raw: str) -> None:
95
- obis = OBIS.parse(raw)
96
- assert obis is not None
97
- assert obis.canonical == "1-0:1.8.0"
File without changes
File without changes
File without changes
File without changes
File without changes