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.
- egauge/ctid/__init__.py +7 -0
- egauge/ctid/bit_stuffer.py +65 -0
- egauge/ctid/ctid.py +967 -0
- egauge/ctid/encoder.py +436 -0
- egauge/ctid/intel_hex_encoder.py +98 -0
- egauge/ctid/waveform.py +299 -0
- egauge/examples/data/test-ctid-decoder.raw +0 -0
- egauge/examples/test_capture.py +77 -0
- egauge/examples/test_common.py +26 -0
- egauge/examples/test_ctid.py +89 -0
- egauge/examples/test_ctid_decoder.py +93 -0
- egauge/examples/test_local.py +201 -0
- egauge/examples/test_register.py +104 -0
- egauge/loggers.py +72 -0
- egauge/pyside/__init__.py +0 -0
- egauge/pyside/ansi2html.py +112 -0
- egauge/pyside/terminal.py +295 -0
- egauge/webapi/__init__.py +34 -0
- egauge/webapi/auth.py +364 -0
- egauge/webapi/cloud/__init__.py +30 -0
- egauge/webapi/cloud/credentials.py +86 -0
- egauge/webapi/cloud/credentials_dialog.py +58 -0
- egauge/webapi/cloud/gui/credentials_dialog.py +100 -0
- egauge/webapi/cloud/serial_number.py +276 -0
- egauge/webapi/device/__init__.py +38 -0
- egauge/webapi/device/capture.py +453 -0
- egauge/webapi/device/ctid_info.py +553 -0
- egauge/webapi/device/device.py +349 -0
- egauge/webapi/device/local.py +268 -0
- egauge/webapi/device/physical_quantity.py +439 -0
- egauge/webapi/device/physical_units.py +473 -0
- egauge/webapi/device/register.py +338 -0
- egauge/webapi/device/register_row.py +145 -0
- egauge/webapi/device/register_type.py +851 -0
- egauge/webapi/device/slop.py +334 -0
- egauge/webapi/device/virtual_register.py +353 -0
- egauge/webapi/error.py +34 -0
- egauge/webapi/json_api.py +332 -0
- egauge_python-0.9.8.dist-info/METADATA +148 -0
- egauge_python-0.9.8.dist-info/RECORD +44 -0
- egauge_python-0.9.8.dist-info/WHEEL +5 -0
- egauge_python-0.9.8.dist-info/entry_points.txt +2 -0
- egauge_python-0.9.8.dist-info/licenses/LICENSE +22 -0
- 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)
|