egauge-python 0.9.8__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.
Files changed (44) hide show
  1. egauge/ctid/__init__.py +7 -0
  2. egauge/ctid/bit_stuffer.py +65 -0
  3. egauge/ctid/ctid.py +967 -0
  4. egauge/ctid/encoder.py +436 -0
  5. egauge/ctid/intel_hex_encoder.py +98 -0
  6. egauge/ctid/waveform.py +299 -0
  7. egauge/examples/data/test-ctid-decoder.raw +0 -0
  8. egauge/examples/test_capture.py +77 -0
  9. egauge/examples/test_common.py +26 -0
  10. egauge/examples/test_ctid.py +89 -0
  11. egauge/examples/test_ctid_decoder.py +93 -0
  12. egauge/examples/test_local.py +201 -0
  13. egauge/examples/test_register.py +104 -0
  14. egauge/loggers.py +72 -0
  15. egauge/pyside/__init__.py +0 -0
  16. egauge/pyside/ansi2html.py +112 -0
  17. egauge/pyside/terminal.py +295 -0
  18. egauge/webapi/__init__.py +34 -0
  19. egauge/webapi/auth.py +364 -0
  20. egauge/webapi/cloud/__init__.py +30 -0
  21. egauge/webapi/cloud/credentials.py +86 -0
  22. egauge/webapi/cloud/credentials_dialog.py +58 -0
  23. egauge/webapi/cloud/gui/credentials_dialog.py +100 -0
  24. egauge/webapi/cloud/serial_number.py +276 -0
  25. egauge/webapi/device/__init__.py +38 -0
  26. egauge/webapi/device/capture.py +453 -0
  27. egauge/webapi/device/ctid_info.py +553 -0
  28. egauge/webapi/device/device.py +349 -0
  29. egauge/webapi/device/local.py +268 -0
  30. egauge/webapi/device/physical_quantity.py +439 -0
  31. egauge/webapi/device/physical_units.py +473 -0
  32. egauge/webapi/device/register.py +338 -0
  33. egauge/webapi/device/register_row.py +145 -0
  34. egauge/webapi/device/register_type.py +851 -0
  35. egauge/webapi/device/slop.py +334 -0
  36. egauge/webapi/device/virtual_register.py +353 -0
  37. egauge/webapi/error.py +34 -0
  38. egauge/webapi/json_api.py +332 -0
  39. egauge_python-0.9.8.dist-info/METADATA +148 -0
  40. egauge_python-0.9.8.dist-info/RECORD +44 -0
  41. egauge_python-0.9.8.dist-info/WHEEL +5 -0
  42. egauge_python-0.9.8.dist-info/entry_points.txt +2 -0
  43. egauge_python-0.9.8.dist-info/licenses/LICENSE +22 -0
  44. egauge_python-0.9.8.dist-info/top_level.txt +1 -0
@@ -0,0 +1,349 @@
1
+ #
2
+ # Copyright (c) 2020, 2022-2024 eGauge Systems LLC
3
+ # 1644 Conestoga St, Suite 2
4
+ # Boulder, CO 80301
5
+ # voice: 720-545-9767
6
+ # email: davidm@egauge.net
7
+ #
8
+ # All rights reserved.
9
+ #
10
+ # MIT License
11
+ #
12
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ # of this software and associated documentation files (the "Software"), to deal
14
+ # in the Software without restriction, including without limitation the rights
15
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ # copies of the Software, and to permit persons to whom the Software is
17
+ # furnished to do so, subject to the following conditions:
18
+ #
19
+ # The above copyright notice and this permission notice shall be included in
20
+ # all copies or substantial portions of the Software.
21
+ #
22
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
28
+ # THE SOFTWARE.
29
+ #
30
+ """Module to provide access to a device's JSON WebAPI."""
31
+
32
+ from dataclasses import dataclass
33
+
34
+ from .. import json_api
35
+ from ..auth import JWTAuth
36
+ from ..error import Error
37
+ from ..json_api import JSONAPIError
38
+ from .virtual_register import VirtualRegister
39
+
40
+
41
+ class DeviceError(Error):
42
+ """Raised if for device related errors."""
43
+
44
+
45
+ @dataclass
46
+ class ChannelInfo:
47
+ chan: int # the channel number
48
+ unit: str # the physical unit of the channel data
49
+
50
+
51
+ @dataclass
52
+ class RegInfo:
53
+ idx: int
54
+ tc: str
55
+ formula: VirtualRegister | None
56
+
57
+
58
+ def _json_obj(reply) -> dict:
59
+ """Convert a JSON API response to an object.
60
+
61
+ Raises JSONAPIError("Reply is not a JSON object.") if the response
62
+ is not an object.
63
+
64
+ Raises JSONAPIError("WebAPI error: {err}") if the response
65
+ contains a member of name "error". {err} is the value of that
66
+ member.
67
+
68
+ """
69
+ if not isinstance(reply, dict):
70
+ raise JSONAPIError("Reply is not a JSON object.", reply)
71
+
72
+ err = reply.get("error")
73
+ if err:
74
+ raise JSONAPIError(f"WebAPI error: {err}")
75
+
76
+ return reply
77
+
78
+
79
+ class Device:
80
+ """This class provides access to an eGauge device's JSON WebAPI.
81
+ See "Web API Design" document for details."""
82
+
83
+ def __init__(
84
+ self,
85
+ dev_uri: str,
86
+ auth: JWTAuth | None = None,
87
+ verify: bool | str = True,
88
+ ):
89
+ """Return a device object that can be used to access the meter
90
+ which supports WebAPI.
91
+
92
+ Required arguments:
93
+
94
+ dev_uri -- The device URI of the meter such as
95
+ "http://eGaugeHQ.egauge.io".
96
+
97
+ Keyword arguments:
98
+
99
+ auth -- An authentication object that provides the credentials
100
+ to access the device (default None).
101
+
102
+ verify -- If False, certificate verification is disabled. It
103
+ may also be a string in which case it must be the path of
104
+ a file that holds the CA bundle to use (default True).
105
+
106
+ """
107
+ self.dev_uri = dev_uri
108
+ self.api_uri = dev_uri + "/api"
109
+ self.auth = auth
110
+ self._reg_info = None # cached register info
111
+ self._chan_info = None # cached channel info
112
+ self._verify = verify
113
+
114
+ def __str__(self) -> str:
115
+ """Returns the device URI of the meter."""
116
+ return self.dev_uri
117
+
118
+ def get(self, resource: str, **kwargs) -> dict:
119
+ """Issue GET request to a WebAPI resource and return the
120
+ resulting response object.
121
+
122
+ Raises JSONAPIError if the request fails for any reason.
123
+
124
+ Required arguments:
125
+
126
+ resource -- URI of the resource to access.
127
+
128
+ Keyword arguments:
129
+
130
+ Any keyword arguments are passed on to requests.get().
131
+
132
+ """
133
+ if "verify" not in kwargs:
134
+ kwargs["verify"] = self._verify
135
+ return _json_obj(
136
+ json_api.get(self.api_uri + resource, auth=self.auth, **kwargs)
137
+ )
138
+
139
+ def put(self, resource: str, json_data, **kwargs) -> dict:
140
+ """Issue a PUT request to a WebAPI resource and return the
141
+ resulting response object.
142
+
143
+ Raises JSONAPIError if the request fails for any reason.
144
+
145
+ Required arguments:
146
+
147
+ resource -- URI of the resource to access.
148
+
149
+ json_data -- The data to JSON-encode and pass in the request
150
+ body.
151
+
152
+ Keyword arguments:
153
+
154
+ Any keyword arguments are passed on to requests.get().
155
+
156
+ """
157
+ if "verify" not in kwargs:
158
+ kwargs["verify"] = self._verify
159
+ return _json_obj(
160
+ json_api.put(
161
+ self.api_uri + resource, json_data, auth=self.auth, **kwargs
162
+ )
163
+ )
164
+
165
+ def post(self, resource, json_data, **kwargs) -> dict:
166
+ """Issue a POST request to a WebAPI resource and return the
167
+ resulting response object.
168
+
169
+ Raises JSONAPIError if the request fails for any reason.
170
+
171
+ Required arguments:
172
+
173
+ resource -- URI of the resource to access.
174
+
175
+ json_data -- The data to JSON-encode and pass in the request
176
+ body.
177
+
178
+ Keyword arguments:
179
+
180
+ Any keyword arguments are passed on to requests.get().
181
+
182
+ """
183
+ if "verify" not in kwargs:
184
+ kwargs["verify"] = self._verify
185
+ return _json_obj(
186
+ json_api.post(
187
+ self.api_uri + resource, json_data, auth=self.auth, **kwargs
188
+ )
189
+ )
190
+
191
+ def delete(self, resource: str, **kwargs):
192
+ """Issue a DELETE request to a WebAPI resource and return the
193
+ resulting response object.
194
+
195
+ Raises JSONAPIError if the request fails for any reason.
196
+
197
+ Required arguments:
198
+
199
+ resource -- URI of the resource to access.
200
+
201
+ Keyword arguments:
202
+
203
+ Any keyword arguments are passed on to requests.get().
204
+
205
+ """
206
+ if "verify" not in kwargs:
207
+ kwargs["verify"] = self._verify
208
+ return _json_obj(
209
+ json_api.delete(self.api_uri + resource, auth=self.auth, **kwargs)
210
+ )
211
+
212
+ def _fetch_reg_info(self) -> dict[str, RegInfo]:
213
+ """Fetch register info, including type and virtual register
214
+ formulas.
215
+
216
+ """
217
+ reply = self.get("/register", params={"virtual": "formula"})
218
+ regs = reply.get("registers")
219
+ if not isinstance(regs, list):
220
+ raise DeviceError("Failed to fetch register info.", reply)
221
+ reg_info = {}
222
+ for reg in regs:
223
+ formula = reg.get("formula")
224
+ vreg = VirtualRegister(formula) if formula else None
225
+ ri = RegInfo(reg["idx"], reg["type"], vreg)
226
+ reg_info[reg["name"]] = ri
227
+ return reg_info
228
+
229
+ def reg_idx(self, regname: str) -> int:
230
+ """Return the register index of a register. The returned
231
+ information is cached in the device object since it is
232
+ relatively expensive to acquire. If the meter configuration
233
+ changes, this information must be flushed with a call to
234
+ Device.clear_cache().
235
+
236
+ Required arguments:
237
+
238
+ regname -- The name of the register whose index to return.
239
+
240
+ """
241
+ if self._reg_info is None:
242
+ self._reg_info = self._fetch_reg_info()
243
+ return self._reg_info[regname].idx
244
+
245
+ def reg_type(self, regname: str) -> str:
246
+ """Return the type code of a register. The returned
247
+ information is cached in the device object since it is
248
+ relatively expensive to acquire. If the meter configuration
249
+ changes, this information must be flushed with a call to
250
+ Device.clear_cache().
251
+
252
+ Required arguments:
253
+
254
+ regname -- The name of the register whose index to return.
255
+
256
+ """
257
+ if self._reg_info is None:
258
+ self._reg_info = self._fetch_reg_info()
259
+ return self._reg_info[regname].tc
260
+
261
+ def reg_virtuals(self) -> list[str]:
262
+ """Return the list of virtual register names. The returned
263
+ information is cached in the device object since it is
264
+ relatively expensive to acquire. If the meter configuration
265
+ changes, this information must be flushed with a call to
266
+ Device.clear_cache().
267
+
268
+ Required arguments:
269
+
270
+ regname -- The name of the register whose index to return.
271
+
272
+ """
273
+ if self._reg_info is None:
274
+ self._reg_info = self._fetch_reg_info()
275
+ virts = []
276
+ for reg, ri in self._reg_info.items():
277
+ if ri.formula:
278
+ virts.append(reg)
279
+ return virts
280
+
281
+ def reg_formula(self, regname: str) -> VirtualRegister | None:
282
+ """Return the VirtualRegister object of a register or None if
283
+ the named register is not a virtual register. The returned
284
+ information is cached in the device object since it is
285
+ relatively expensive to acquire. If the meter configuration
286
+ changes, this information must be flushed with a call to
287
+ Device.clear_cache().
288
+
289
+ Required arguments:
290
+
291
+ regname -- The name of the register whose VirtualRegister
292
+ object to return.
293
+
294
+ """
295
+ if self._reg_info is None:
296
+ self._reg_info = self._fetch_reg_info()
297
+ return self._reg_info[regname].formula
298
+
299
+ def _fetch_chan_info(self) -> dict[str, ChannelInfo]:
300
+ """Fetch channel info from /capture."""
301
+ reply = self.get("/capture", params={"i": ""})
302
+ if reply is None or "channels" not in reply:
303
+ raise DeviceError("Failed to get channel info.", reply)
304
+
305
+ channels = reply["channels"]
306
+ if not isinstance(channels, dict):
307
+ raise DeviceError("Invalid channel info.", channels)
308
+
309
+ chan_info = {}
310
+ for chan, info in channels.items():
311
+ ci = ChannelInfo(chan=int(chan), unit=info.get("unit"))
312
+ chan_info[info["name"]] = ci
313
+ return chan_info
314
+
315
+ def channel_info(self) -> dict[str, ChannelInfo]:
316
+ """Get the channel info as provided by /api/cap?i. The
317
+ returned information is cached in the device object since it
318
+ is relatively expensive to acquire. If the meter configuration
319
+ changes, this information must be flushed with a call to
320
+ Device.clear_cache().
321
+
322
+ """
323
+ if self._chan_info is None:
324
+ self._chan_info = self._fetch_chan_info()
325
+ return self._chan_info
326
+
327
+ def clear_cache(self):
328
+ """Clear the cached contents for this device object."""
329
+ self._reg_info = None
330
+ self._chan_info = None
331
+
332
+ def is_up(self, timeout: float = 1) -> bool:
333
+ """Check if the device is up and running.
334
+
335
+ This method attempts to read /sys/time. If a valid response
336
+ is received within the specified timeout, True is returned,
337
+ otherwise False is returned.
338
+
339
+ Keyword arguments:
340
+
341
+ timeout -- The maximum number of seconds to wait for a
342
+ response (default 1).
343
+
344
+ """
345
+ try:
346
+ self.get("/sys/time", timeout=timeout)
347
+ except json_api.JSONAPIError:
348
+ return False
349
+ return True
@@ -0,0 +1,268 @@
1
+ #
2
+ # Copyright (c) 2020-2021, 2023, 2024 eGauge Systems LLC
3
+ # 1644 Conestoga St, Suite 2
4
+ # Boulder, CO 80301
5
+ # voice: 720-545-9767
6
+ # email: davidm@egauge.net
7
+ #
8
+ # All rights reserved.
9
+ #
10
+ # MIT License
11
+ #
12
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ # of this software and associated documentation files (the "Software"), to deal
14
+ # in the Software without restriction, including without limitation the rights
15
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ # copies of the Software, and to permit persons to whom the Software is
17
+ # furnished to do so, subject to the following conditions:
18
+ #
19
+ # The above copyright notice and this permission notice shall be included in
20
+ # all copies or substantial portions of the Software.
21
+ #
22
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
28
+ # THE SOFTWARE.
29
+ #
30
+ import decimal
31
+ import json
32
+
33
+ from ..error import Error
34
+ from .device import Device
35
+ from .register_row import RegisterRow
36
+ from .register_type import RegisterType
37
+
38
+
39
+ class LocalError(Error):
40
+ """All errors in this module raise this exception."""
41
+
42
+
43
+ def sensor_port_name(index: int) -> str:
44
+ """Get the canonical sensor port name for a port number.
45
+
46
+ Required arguments:
47
+
48
+ index -- The index of the sensor port whose name to return. It
49
+ must be in the range from 0..NUM_PORTS-1, where NUM_PORTS is
50
+ the number of sensor ports supported by the meter.
51
+
52
+ """
53
+ sensor = index + 1
54
+ return f"S{sensor}"
55
+
56
+
57
+ class Local:
58
+ """Fetch data from /api/local."""
59
+
60
+ # Sensor parameter spec strings:
61
+ # For l=...:
62
+ SPEC_L1 = "L1"
63
+ SPEC_L2 = "L2"
64
+ SPEC_L3 = "L3"
65
+ SPEC_L1_L2 = "L12"
66
+ SPEC_L2_L3 = "L23"
67
+ SPEC_L3_L1 = "L31"
68
+ SPEC_LDC = "Ldc"
69
+ SPEC_D1 = "D1"
70
+ SPEC_D2 = "D2"
71
+ SPEC_D3 = "D3"
72
+
73
+ # Sensor names as they appear in the output:
74
+ NAME_L1 = "L1"
75
+ NAME_L2 = "L2"
76
+ NAME_L3 = "L3"
77
+ NAME_L1_L2 = "L1-L2"
78
+ NAME_L2_L3 = "L2-L3"
79
+ NAME_L3_L1 = "L3-L1"
80
+ NAME_LDC = "Ldc"
81
+ NAME_D1 = "D1"
82
+ NAME_D2 = "D2"
83
+ NAME_D3 = "D3"
84
+ NAME_TEMP_PCB = "Tpcb"
85
+ NAME_HUMID_PCB = "Hpcb"
86
+
87
+ METRIC_RATE = "rate"
88
+ METRIC_CUMUL = "cumul"
89
+ METRIC_TYPE = "type"
90
+
91
+ MEASUREMENT_NORMAL = "n"
92
+ MEASUREMENT_MEAN = "m"
93
+ MEASUREMENT_FREQ = "f"
94
+
95
+ SECTION_VALUES = "values"
96
+ SECTION_ENERGY = "energy"
97
+ SECTION_APPARENT = "apparent"
98
+
99
+ def __init__(self, dev: Device, params, **kwargs):
100
+ self.raw = dev.get("/local", params=params, **kwargs)
101
+
102
+ def ts(self) -> decimal.Decimal | None:
103
+ """Return the timestamp of the local data in seconds since the
104
+ Unix epoch.
105
+
106
+ """
107
+ if self.raw is None:
108
+ return None
109
+ return decimal.Decimal(self.raw["ts"])
110
+
111
+ def type_code(self, sensor_name: str) -> str | None:
112
+ """Return the type-code of a sensor.
113
+
114
+ Required arguments:
115
+
116
+ sensor_name -- The name of the sensor whose type-code to
117
+ return.
118
+
119
+ """
120
+ if self.raw is None:
121
+ return None
122
+ try:
123
+ return self.raw["values"][sensor_name]["type"]
124
+ except KeyError:
125
+ pass
126
+ return None
127
+
128
+ def rate(
129
+ self,
130
+ sensor_name: str,
131
+ measurement: str = MEASUREMENT_NORMAL,
132
+ section: str = SECTION_VALUES,
133
+ ) -> float | None:
134
+ """Return the rate of change value of a sensor.
135
+
136
+ Required arguments:
137
+
138
+ sensor_name -- The name of the sensor whose rate to return.
139
+
140
+ Keyword arguments:
141
+
142
+ measurement -- The measurement that should be returned for the
143
+ sensor (default MEASUREMENT_NORMAL). The mean (average)
144
+ value can be requested with MEASUREMENT_MEAN or the
145
+ frequency at which the sensor's value crosses zero can be
146
+ requested with MEASUREMENT_FREQ. If the requested value
147
+ is unavailable, 'None' is returned.
148
+
149
+ section -- The section to return the rate from (default
150
+ SECTION_VALUES). Can be one of SECTION_VALUES,
151
+ SECTION_ENERGY, or SECTION_APPARENT.
152
+
153
+ """
154
+ if self.raw is None:
155
+ return None
156
+ try:
157
+ m = self.raw[section][sensor_name]["rate"]
158
+ if section == Local.SECTION_VALUES:
159
+ return m[measurement]
160
+ return m
161
+ except KeyError:
162
+ pass
163
+ return None
164
+
165
+ def cumul(
166
+ self,
167
+ sensor_name: str,
168
+ measurement: str = MEASUREMENT_NORMAL,
169
+ section: str = SECTION_VALUES,
170
+ ) -> int | None:
171
+ """Return the cumulative value of a sensor.
172
+
173
+ Required arguments:
174
+
175
+ sensor_name -- The name of the sensor whose rate to return.
176
+
177
+ Keyword arguments:
178
+
179
+ measurement -- The measurement that should be returned for the
180
+ sensor (default MEASUREMENT_NORMAL). The mean (average)
181
+ value can be requested with MEASUREMENT_MEAN or the
182
+ frequency at which the sensor's value crosses zero can be
183
+ requested with MEASUREMENT_FREQ. If the requested value
184
+ is unavailable, 'None' is returned.
185
+
186
+ section -- The section to return the rate from (default
187
+ SECTION_VALUES). Can be one of SECTION_VALUES,
188
+ SECTION_ENERGY, or SECTION_APPARENT.
189
+
190
+ """
191
+ if self.raw is None:
192
+ return None
193
+ try:
194
+ m = self.raw[section][sensor_name]["cumul"]
195
+ if section == Local.SECTION_VALUES:
196
+ return int(m[measurement])
197
+ return int(m)
198
+ except KeyError:
199
+ pass
200
+ return None
201
+
202
+ def row(
203
+ self,
204
+ metric: str = METRIC_CUMUL,
205
+ measurement: str = MEASUREMENT_NORMAL,
206
+ section: str = SECTION_VALUES,
207
+ ):
208
+ """Return a timestamped row of register data.
209
+
210
+ Keyword arguments:
211
+
212
+ metric -- The metric to return (default METRIC_CUMUL). May be
213
+ one of METRIC_RATE or METRIC_CUMUL.
214
+
215
+ measurement -- The measurement that should be returned for the
216
+ sensor (default MEASUREMENT_NORMAL). The mean (average)
217
+ value can be requested with MEASUREMENT_MEAN or the
218
+ frequency at which the sensor's value crosses zero can be
219
+ requested with MEASUREMENT_FREQ. If the requested value
220
+ is unavailable, 'None' is returned.
221
+
222
+ section -- The section to return the rate from (default
223
+ SECTION_VALUES). Can be one of SECTION_VALUES,
224
+ SECTION_ENERGY, or SECTION_APPARENT.
225
+
226
+ """
227
+ if metric not in [Local.METRIC_RATE, Local.METRIC_CUMUL]:
228
+ raise LocalError(
229
+ "Metric must be one of METRIC_RATE or METRIC_CUMUL", metric
230
+ )
231
+
232
+ if self.raw is None:
233
+ return None
234
+ regs = {}
235
+ type_codes = {}
236
+ is_diff = True
237
+ if metric == Local.METRIC_CUMUL:
238
+ is_diff = False
239
+ for key, metrics in self.raw[section].items():
240
+ if metric not in metrics:
241
+ continue
242
+ val = metrics[metric]
243
+ if section == Local.SECTION_VALUES:
244
+ val = val[measurement]
245
+ if metric == Local.METRIC_CUMUL:
246
+ val = int(val) # convert from decimal string to integer
247
+ regs[key] = val
248
+ if section == Local.SECTION_ENERGY:
249
+ # this assumes the sensor pairs measure voltage and current:
250
+ tc = RegisterType.POWER.value
251
+ elif section == Local.SECTION_APPARENT:
252
+ # this assumes the sensor pairs measure voltage and current:
253
+ tc = RegisterType.APPARENT_POWER.value
254
+ elif measurement == Local.MEASUREMENT_FREQ:
255
+ tc = RegisterType.FREQ.value
256
+ elif Local.METRIC_TYPE in metrics:
257
+ tc = metrics[Local.METRIC_TYPE]
258
+ else:
259
+ raise LocalError(f"Internal error: unknown section {section}")
260
+
261
+ regs[key] = val
262
+ type_codes[key] = tc
263
+ return RegisterRow(self.ts(), regs, type_codes, is_diff)
264
+
265
+ def __str__(self) -> str:
266
+ """Return the raw data of the object as a JSON-encoded
267
+ string."""
268
+ return json.dumps(self.raw)