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,553 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright (c) 2020-2021, 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
|
+
"""This module provides access to the eGauge WebAPI's /api/ctid
|
|
31
|
+
service."""
|
|
32
|
+
|
|
33
|
+
import datetime
|
|
34
|
+
import os
|
|
35
|
+
import secrets
|
|
36
|
+
import time
|
|
37
|
+
from collections.abc import Sequence
|
|
38
|
+
|
|
39
|
+
from egauge import ctid, webapi
|
|
40
|
+
|
|
41
|
+
from ..error import Error
|
|
42
|
+
|
|
43
|
+
SCAN_TIMEOUT = 2.5 # scan timeout in seconds
|
|
44
|
+
|
|
45
|
+
Polarity = str # must be one of "+" or "-"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def ctid_info_to_table(reply: dict) -> ctid.Table:
|
|
49
|
+
"""Convert a ctid reply to a ctid.Table object.
|
|
50
|
+
|
|
51
|
+
Required arguments:
|
|
52
|
+
|
|
53
|
+
reply -- A response returned by WebAPI endpoint `/ctid/N`, where
|
|
54
|
+
`N` is a sensor port number.
|
|
55
|
+
|
|
56
|
+
"""
|
|
57
|
+
t = ctid.Table()
|
|
58
|
+
t.version = reply.get("version", 0)
|
|
59
|
+
t.mfg_id = reply.get("mfgid", 0)
|
|
60
|
+
t.model = reply.get("model", "unknown")
|
|
61
|
+
t.serial_number = reply.get("sn", 0)
|
|
62
|
+
t.sensor_type = reply.get("k", ctid.SENSOR_TYPE_AC)
|
|
63
|
+
t.r_source = reply.get("rsrc", 0)
|
|
64
|
+
t.r_load = reply.get("rload", 0)
|
|
65
|
+
|
|
66
|
+
params = reply.get("params", {})
|
|
67
|
+
t.size = params.get("size")
|
|
68
|
+
t.rated_current = params.get("i")
|
|
69
|
+
t.voltage_at_rated_current = params.get("v")
|
|
70
|
+
t.phase_at_rated_current = params.get("a")
|
|
71
|
+
t.voltage_temp_coeff = params.get("tv")
|
|
72
|
+
t.phase_temp_coeff = params.get("ta")
|
|
73
|
+
t.cal_table = {}
|
|
74
|
+
cal_table = params.get("cal", {})
|
|
75
|
+
for l_str in cal_table:
|
|
76
|
+
l = float(l_str)
|
|
77
|
+
t.cal_table[l] = [
|
|
78
|
+
cal_table[l_str].get("v", 0),
|
|
79
|
+
cal_table[l_str].get("a", 0),
|
|
80
|
+
]
|
|
81
|
+
t.bias_voltage = params.get("bias_voltage", 0)
|
|
82
|
+
t.scale = params.get("scale")
|
|
83
|
+
t.offset = params.get("offset")
|
|
84
|
+
t.delay = params.get("delay")
|
|
85
|
+
t.threshold = params.get("threshold")
|
|
86
|
+
t.hysteresis = params.get("hysteresis")
|
|
87
|
+
t.debounce_time = params.get("debounce_time")
|
|
88
|
+
t.edge_mask = params.get("edge_mask")
|
|
89
|
+
t.ntc_a = params.get("ntc_a")
|
|
90
|
+
t.ntc_b = params.get("ntc_b")
|
|
91
|
+
t.ntc_c = params.get("ntc_c")
|
|
92
|
+
t.ntc_m = params.get("ntc_m")
|
|
93
|
+
t.ntc_n = params.get("ntc_n")
|
|
94
|
+
t.ntc_k = params.get("ntc_k")
|
|
95
|
+
return t
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class CTidInfoError(Error):
|
|
99
|
+
"""This is used for any errors raised by this module."""
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class PortInfo:
|
|
103
|
+
"""Encapsulates the port number on which a CTid table was read,
|
|
104
|
+
the polarity which is was read with, and the table itself.
|
|
105
|
+
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
def __init__(
|
|
109
|
+
self, port: int, polarity: Polarity | None, table: ctid.Table | None
|
|
110
|
+
):
|
|
111
|
+
self.port = port
|
|
112
|
+
self.polarity = polarity
|
|
113
|
+
self.table = table
|
|
114
|
+
|
|
115
|
+
def port_name(self) -> str:
|
|
116
|
+
"""Return the canonical port name."""
|
|
117
|
+
return "S%d" % self.port
|
|
118
|
+
|
|
119
|
+
def short_mfg_name(self) -> str:
|
|
120
|
+
"""Return the short (concise) name of the manufacturer of the sensor
|
|
121
|
+
or `-' if unknown.
|
|
122
|
+
|
|
123
|
+
"""
|
|
124
|
+
if self.table is None or self.table.mfg_id is None:
|
|
125
|
+
return "-"
|
|
126
|
+
return ctid.mfg_short_name(self.table.mfg_id) or "-"
|
|
127
|
+
|
|
128
|
+
def model_name(self) -> str:
|
|
129
|
+
"""Return the model name of the sensor attached to the port. If
|
|
130
|
+
unknown `-' is returned.
|
|
131
|
+
|
|
132
|
+
"""
|
|
133
|
+
if self.table is None or self.table.model is None:
|
|
134
|
+
return "-"
|
|
135
|
+
return self.table.model
|
|
136
|
+
|
|
137
|
+
def mfg_model_name(self) -> str:
|
|
138
|
+
"""Return a "fully qualified" model name, which consists of
|
|
139
|
+
the short version of the manufacter's name, a dash, and the
|
|
140
|
+
model name.
|
|
141
|
+
|
|
142
|
+
"""
|
|
143
|
+
return "%s-%s" % (self.short_mfg_name(), self.model_name())
|
|
144
|
+
|
|
145
|
+
def sn(self) -> int | None:
|
|
146
|
+
"""Return the serial number or None if unknown."""
|
|
147
|
+
if self.table is None or not isinstance(self.table.serial_number, int):
|
|
148
|
+
return None
|
|
149
|
+
return self.table.serial_number
|
|
150
|
+
|
|
151
|
+
def serial_number(self) -> str:
|
|
152
|
+
"""Return the serial number of the sensor attached to the port as a
|
|
153
|
+
decimal string. If unknown, '-' is returned.
|
|
154
|
+
|
|
155
|
+
"""
|
|
156
|
+
if self.table is None or self.table.serial_number is None:
|
|
157
|
+
return "-"
|
|
158
|
+
return str(self.table.serial_number)
|
|
159
|
+
|
|
160
|
+
def unique_name(self) -> str:
|
|
161
|
+
"""Return a sensor's unique name, which is a string consisting of the
|
|
162
|
+
manufacturer's short name, the model name, and the serial
|
|
163
|
+
number, all separated by dashes..
|
|
164
|
+
|
|
165
|
+
"""
|
|
166
|
+
return f"{self.mfg_model_name()}-{self.serial_number()}"
|
|
167
|
+
|
|
168
|
+
def sensor_type(self) -> int | None:
|
|
169
|
+
"""Return the sensor type of the sensor attached to the port or None
|
|
170
|
+
if unknown.
|
|
171
|
+
|
|
172
|
+
"""
|
|
173
|
+
if self.table is None or self.table.sensor_type is None:
|
|
174
|
+
return None
|
|
175
|
+
return self.table.sensor_type
|
|
176
|
+
|
|
177
|
+
def sensor_type_name(self) -> str:
|
|
178
|
+
"""Return the name of the sensor type of the sensor attached to the
|
|
179
|
+
port or '-' if unknown.
|
|
180
|
+
|
|
181
|
+
"""
|
|
182
|
+
st = self.sensor_type()
|
|
183
|
+
if st is None:
|
|
184
|
+
return "-"
|
|
185
|
+
return ctid.get_sensor_type_name(st) or "-"
|
|
186
|
+
|
|
187
|
+
def as_dict(self) -> dict | None:
|
|
188
|
+
"""Return CTid info as a serializable dictionary."""
|
|
189
|
+
if self.table is None:
|
|
190
|
+
return None
|
|
191
|
+
params = {}
|
|
192
|
+
p = {
|
|
193
|
+
"port": self.port,
|
|
194
|
+
"polarity": self.polarity,
|
|
195
|
+
"version": self.table.version,
|
|
196
|
+
"mfgid": self.table.mfg_id,
|
|
197
|
+
"model": self.table.model,
|
|
198
|
+
"sn": self.table.serial_number,
|
|
199
|
+
"k": self.table.sensor_type,
|
|
200
|
+
"rsrc": self.table.r_source,
|
|
201
|
+
"rload": self.table.r_load,
|
|
202
|
+
"params": params,
|
|
203
|
+
}
|
|
204
|
+
if self.table.sensor_type in [
|
|
205
|
+
ctid.SENSOR_TYPE_AC,
|
|
206
|
+
ctid.SENSOR_TYPE_DC,
|
|
207
|
+
ctid.SENSOR_TYPE_RC,
|
|
208
|
+
]:
|
|
209
|
+
params["size"] = self.table.size
|
|
210
|
+
params["i"] = self.table.rated_current
|
|
211
|
+
params["v"] = self.table.voltage_at_rated_current
|
|
212
|
+
params["a"] = self.table.phase_at_rated_current
|
|
213
|
+
params["tv"] = self.table.voltage_temp_coeff
|
|
214
|
+
params["ta"] = self.table.phase_temp_coeff
|
|
215
|
+
params["bias_voltage"] = self.table.bias_voltage
|
|
216
|
+
cal_table = {}
|
|
217
|
+
for l, row in self.table.cal_table.items():
|
|
218
|
+
cal_table[l] = {"v": row[0], "a": row[1]}
|
|
219
|
+
params["cal"] = cal_table
|
|
220
|
+
elif self.table.sensor_type == ctid.SENSOR_TYPE_VOLTAGE:
|
|
221
|
+
params["scale"] = self.table.scale
|
|
222
|
+
params["offset"] = self.table.offset
|
|
223
|
+
params["delay"] = self.table.delay
|
|
224
|
+
elif self.table.sensor_type == ctid.SENSOR_TYPE_TEMP_LINEAR:
|
|
225
|
+
params["scale"] = self.table.scale
|
|
226
|
+
params["offset"] = self.table.offset
|
|
227
|
+
elif self.table.sensor_type == ctid.SENSOR_TYPE_TEMP_NTC:
|
|
228
|
+
params["ntc_a"] = self.table.ntc_a
|
|
229
|
+
params["ntc_b"] = self.table.ntc_b
|
|
230
|
+
params["ntc_c"] = self.table.ntc_c
|
|
231
|
+
params["ntc_m"] = self.table.ntc_m
|
|
232
|
+
params["ntc_n"] = self.table.ntc_n
|
|
233
|
+
params["ntc_k"] = self.table.ntc_k
|
|
234
|
+
elif self.table.sensor_type == ctid.SENSOR_TYPE_PULSE:
|
|
235
|
+
params["threshold"] = self.table.threshold
|
|
236
|
+
params["hysteresis"] = self.table.hysteresis
|
|
237
|
+
params["debounce_time"] = self.table.debounce_time
|
|
238
|
+
params["edge_mask"] = self.table.edge_mask
|
|
239
|
+
return p
|
|
240
|
+
|
|
241
|
+
def __str__(self) -> str:
|
|
242
|
+
return (
|
|
243
|
+
f"(port={self.port},polarity={self.polarity},table={self.table})"
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class CTidInfo:
|
|
248
|
+
def __init__(self, dev):
|
|
249
|
+
"""A CTidInfo object provides to access the WebAPI CTid
|
|
250
|
+
service of a meter. The service allows reading CTid info from
|
|
251
|
+
a particular port, scanning a port, flashing the attached
|
|
252
|
+
sensor's indicator LED, or iterating over all the ports with
|
|
253
|
+
CTid information.
|
|
254
|
+
|
|
255
|
+
"""
|
|
256
|
+
self.dev = dev
|
|
257
|
+
self.tid = None
|
|
258
|
+
self.info = None
|
|
259
|
+
self.index = 0
|
|
260
|
+
self.polarity = None
|
|
261
|
+
self.port_number = None
|
|
262
|
+
|
|
263
|
+
def _make_tid(self):
|
|
264
|
+
"""Create a random transaction id and store it in `self.tid`."""
|
|
265
|
+
self.tid = secrets.randbits(32)
|
|
266
|
+
if self.tid < 1:
|
|
267
|
+
self.tid += 1
|
|
268
|
+
|
|
269
|
+
def stop(self):
|
|
270
|
+
"""Stop pending CTid operation, if any."""
|
|
271
|
+
if self.tid is not None:
|
|
272
|
+
self.dev.post("/ctid/stop", {})
|
|
273
|
+
self.tid = None
|
|
274
|
+
|
|
275
|
+
def scan_start(self, port_number: int, polarity: Polarity):
|
|
276
|
+
"""Initiate a CTid scan.
|
|
277
|
+
|
|
278
|
+
Raises CTidInfoError on errors.
|
|
279
|
+
|
|
280
|
+
Required arguments:
|
|
281
|
+
|
|
282
|
+
port_number -- The number of the port to scan. Number 1 is
|
|
283
|
+
the first port. The maximum port number depends on the
|
|
284
|
+
meter.
|
|
285
|
+
|
|
286
|
+
polarity -- The polarity with which to scan the port. "+"
|
|
287
|
+
indicates normal polarity, "-" indicates reversed
|
|
288
|
+
polarity.
|
|
289
|
+
|
|
290
|
+
"""
|
|
291
|
+
if port_number < 1:
|
|
292
|
+
raise CTidInfoError("Invalid port number.", port_number)
|
|
293
|
+
if self.tid is not None:
|
|
294
|
+
self.stop()
|
|
295
|
+
self._make_tid()
|
|
296
|
+
self.polarity = polarity
|
|
297
|
+
self.port_number = port_number
|
|
298
|
+
data = {"op": "scan", "tid": self.tid, "polarity": polarity}
|
|
299
|
+
resource = "/ctid/%d" % port_number
|
|
300
|
+
last_e = None
|
|
301
|
+
for _ in range(3):
|
|
302
|
+
try:
|
|
303
|
+
reply = self.dev.post(resource, data)
|
|
304
|
+
if reply.get("status") == "OK":
|
|
305
|
+
return
|
|
306
|
+
except Error as e:
|
|
307
|
+
last_e = e
|
|
308
|
+
raise CTidInfoError(
|
|
309
|
+
"Failed to initiate CTid scan", port_number, polarity
|
|
310
|
+
) from last_e
|
|
311
|
+
|
|
312
|
+
def scan_result(self) -> PortInfo | None:
|
|
313
|
+
"""Get the result of a port scan.
|
|
314
|
+
|
|
315
|
+
This attempts to read the result from a CTid scan initiated
|
|
316
|
+
with a call to CTidInfo.scan_start(). If the result is not
|
|
317
|
+
available yet, None is returned. In that case, the caller
|
|
318
|
+
should wait a little and then retry the request again for up
|
|
319
|
+
to SCAN_TIMEOUT seconds.
|
|
320
|
+
|
|
321
|
+
The the result is available, a CTidInfo.PortInfo object is
|
|
322
|
+
returned.
|
|
323
|
+
|
|
324
|
+
Raises CTidInfoError on errors.
|
|
325
|
+
|
|
326
|
+
"""
|
|
327
|
+
if not isinstance(self.port_number, int):
|
|
328
|
+
raise CTidInfoError("CTidInfo.scan_start() must be called first.")
|
|
329
|
+
|
|
330
|
+
if not isinstance(self.polarity, Polarity):
|
|
331
|
+
raise CTidInfoError(f"Invalid polarity {self.polarity}.")
|
|
332
|
+
|
|
333
|
+
resource = "/ctid/%d" % self.port_number
|
|
334
|
+
reply = self.dev.get(resource, params={"tid": self.tid})
|
|
335
|
+
if (
|
|
336
|
+
reply.get("port") == self.port_number
|
|
337
|
+
and reply.get("tid") == self.tid
|
|
338
|
+
):
|
|
339
|
+
return PortInfo(
|
|
340
|
+
self.port_number, self.polarity, ctid_info_to_table(reply)
|
|
341
|
+
)
|
|
342
|
+
return None
|
|
343
|
+
|
|
344
|
+
def scan(
|
|
345
|
+
self,
|
|
346
|
+
port_number: int,
|
|
347
|
+
polarity: Polarity | None = None,
|
|
348
|
+
timeout: float = SCAN_TIMEOUT,
|
|
349
|
+
):
|
|
350
|
+
"""Synchronously scan the CTid information of a port.
|
|
351
|
+
|
|
352
|
+
This is a convenience method which calls CTidInfo.scan_start()
|
|
353
|
+
followed by repeated calls to CTidInfo.scan_result() until the
|
|
354
|
+
scan is complete or the operation times out. If no CTid info
|
|
355
|
+
could be read from the port, the returned PortInfo's table
|
|
356
|
+
property will be `None`.
|
|
357
|
+
|
|
358
|
+
Required arguments:
|
|
359
|
+
|
|
360
|
+
port_number -- The port to scan. Number 1 is the first port.
|
|
361
|
+
|
|
362
|
+
Keyword arguments:
|
|
363
|
+
|
|
364
|
+
polarity -- The polarity with which to scan the port. "+"
|
|
365
|
+
indicates normal polarity, "-" indicates reversed
|
|
366
|
+
polarity. If None, a scan is first attempted with normal
|
|
367
|
+
polarity and if that times out, a scan is attempted with
|
|
368
|
+
reversed polarity (default None).
|
|
369
|
+
|
|
370
|
+
timeout -- The maximum time in seconds to wait for the
|
|
371
|
+
operation to complete (default SCAN_TIMEOUT = 2.5
|
|
372
|
+
seconds).
|
|
373
|
+
|
|
374
|
+
Raises CTidInfoError on errors.
|
|
375
|
+
|
|
376
|
+
"""
|
|
377
|
+
polarity_list = ["+", "-"] if polarity is None else [polarity]
|
|
378
|
+
for pol in polarity_list:
|
|
379
|
+
self.scan_start(port_number, pol)
|
|
380
|
+
|
|
381
|
+
start_time = datetime.datetime.now()
|
|
382
|
+
while True:
|
|
383
|
+
time.sleep(0.25)
|
|
384
|
+
result = self.scan_result()
|
|
385
|
+
if result is not None:
|
|
386
|
+
return result
|
|
387
|
+
elapsed = (
|
|
388
|
+
datetime.datetime.now() - start_time
|
|
389
|
+
).total_seconds()
|
|
390
|
+
if elapsed > timeout:
|
|
391
|
+
break
|
|
392
|
+
self.stop()
|
|
393
|
+
return PortInfo(port_number, None, None)
|
|
394
|
+
|
|
395
|
+
def flash(self, port_number: int, polarity: Polarity = "-"):
|
|
396
|
+
"""Start flashing (blinking) the indicator LED of a
|
|
397
|
+
CTid-enabled sensor.
|
|
398
|
+
|
|
399
|
+
Flashing will continue until CTidInfo.stop() is called or
|
|
400
|
+
until a timeout occurs after about 30 minutes.
|
|
401
|
+
|
|
402
|
+
Required arguments:
|
|
403
|
+
|
|
404
|
+
port_number -- The port number of the sensor to flash.
|
|
405
|
+
|
|
406
|
+
Keywoard arguments:
|
|
407
|
+
|
|
408
|
+
polarity -- The polarity to use for flashing the LED. This
|
|
409
|
+
defaults to negative polarity since, with a correctly
|
|
410
|
+
wired sensor, that will result in the LED flashing at
|
|
411
|
+
about 2 Hz. If the sensor wiring is reversed, the LED
|
|
412
|
+
will still flash, albeit slower.
|
|
413
|
+
|
|
414
|
+
Raises CTidInfoError on errors.
|
|
415
|
+
|
|
416
|
+
"""
|
|
417
|
+
if port_number < 1:
|
|
418
|
+
raise CTidInfoError("Invalid port number.", port_number)
|
|
419
|
+
if self.tid is not None:
|
|
420
|
+
self.stop()
|
|
421
|
+
self._make_tid()
|
|
422
|
+
data = {"op": "flash", "tid": self.tid, "polarity": polarity}
|
|
423
|
+
resource = "/ctid/%d" % port_number
|
|
424
|
+
for _ in range(3):
|
|
425
|
+
try:
|
|
426
|
+
reply = self.dev.post(resource, data)
|
|
427
|
+
if reply.get("status") == "OK":
|
|
428
|
+
break
|
|
429
|
+
except Error:
|
|
430
|
+
pass
|
|
431
|
+
|
|
432
|
+
def delete(self, port_number: int):
|
|
433
|
+
"""Delete the CTid information stored for a port.
|
|
434
|
+
|
|
435
|
+
Note that this only deletes the CTid information that the
|
|
436
|
+
meter has saved (cached) in its storage - it does not delete
|
|
437
|
+
any information from the sensor itself.
|
|
438
|
+
|
|
439
|
+
Required arguments:
|
|
440
|
+
|
|
441
|
+
port_number -- The number of the port whose CTid info is to be
|
|
442
|
+
deleted.
|
|
443
|
+
|
|
444
|
+
Raises CTidInfoError on errors.
|
|
445
|
+
|
|
446
|
+
"""
|
|
447
|
+
if port_number < 1:
|
|
448
|
+
raise CTidInfoError("Invalid port number.", port_number)
|
|
449
|
+
resource = "/ctid/%d" % port_number
|
|
450
|
+
reply = self.dev.delete(resource)
|
|
451
|
+
if reply is None or reply.get("status") != "OK":
|
|
452
|
+
reason = reply.get("error") if reply is not None else "timed out"
|
|
453
|
+
raise CTidInfoError(
|
|
454
|
+
"Failed to delete CTid info.", port_number, reason
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
def get(self, port_number: int) -> PortInfo | None:
|
|
458
|
+
"""Get the CTid information stored for a given port.
|
|
459
|
+
|
|
460
|
+
If no information is stored, None is returned.
|
|
461
|
+
|
|
462
|
+
port_number -- The number of the port whose CTid info is to be
|
|
463
|
+
returned.
|
|
464
|
+
|
|
465
|
+
"""
|
|
466
|
+
if port_number < 1:
|
|
467
|
+
raise CTidInfoError("Invalid port number.", port_number)
|
|
468
|
+
resource = "/ctid/%d" % port_number
|
|
469
|
+
reply = self.dev.get(resource)
|
|
470
|
+
if reply is None:
|
|
471
|
+
raise CTidInfoError("Failed to read CTid info.", port_number)
|
|
472
|
+
if not reply:
|
|
473
|
+
return None
|
|
474
|
+
if reply.get("port") != port_number:
|
|
475
|
+
raise CTidInfoError(
|
|
476
|
+
"CTid info has incorrect port number.",
|
|
477
|
+
reply.get("port"),
|
|
478
|
+
port_number,
|
|
479
|
+
)
|
|
480
|
+
return PortInfo(
|
|
481
|
+
port_number, reply.get("polarity"), ctid_info_to_table(reply)
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
def put(self, port_info: PortInfo | Sequence[PortInfo]):
|
|
485
|
+
"""Store CTid info for a given port to the meter.
|
|
486
|
+
|
|
487
|
+
Note that this only stores the info on the meter - it does not
|
|
488
|
+
affect the info stored in the CTid-enabled sensors themselves.
|
|
489
|
+
|
|
490
|
+
Required arguments:
|
|
491
|
+
|
|
492
|
+
port_info -- The port info to store on the meter. If a list,
|
|
493
|
+
the meter will first delete the CTid info of all ports and
|
|
494
|
+
then save the CTidInfo given in the PortInfo list.
|
|
495
|
+
|
|
496
|
+
Raises CTidInfoError on errors.
|
|
497
|
+
|
|
498
|
+
"""
|
|
499
|
+
if isinstance(port_info, PortInfo):
|
|
500
|
+
resource = f"/ctid/{port_info.port}"
|
|
501
|
+
data = port_info.as_dict()
|
|
502
|
+
else:
|
|
503
|
+
resource = "/ctid"
|
|
504
|
+
data = {"info": [pi.as_dict() for pi in port_info]}
|
|
505
|
+
reply = self.dev.put(resource, json_data=data)
|
|
506
|
+
if reply is None:
|
|
507
|
+
raise CTidInfoError("PUT of CTid info failed.")
|
|
508
|
+
if reply.get("status") != "OK":
|
|
509
|
+
raise CTidInfoError("Failure saving CTid info.", data, reply)
|
|
510
|
+
|
|
511
|
+
def __iter__(self):
|
|
512
|
+
"""Iterate over all available CTid information."""
|
|
513
|
+
reply = self.dev.get("/ctid")
|
|
514
|
+
self.info = reply.get("info", [])
|
|
515
|
+
self.index = 0
|
|
516
|
+
return self
|
|
517
|
+
|
|
518
|
+
def __next__(self):
|
|
519
|
+
if self.info is None or self.index >= len(self.info):
|
|
520
|
+
raise StopIteration
|
|
521
|
+
info = self.info[self.index]
|
|
522
|
+
t = ctid_info_to_table(info)
|
|
523
|
+
self.index += 1
|
|
524
|
+
return PortInfo(info["port"], info["polarity"], t)
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def test():
|
|
528
|
+
from . import device
|
|
529
|
+
|
|
530
|
+
dut = os.getenv("EGDEV") or "http://1608050004.lan"
|
|
531
|
+
usr = os.getenv("EGUSR") or "owner"
|
|
532
|
+
pwd = os.getenv("EGPWD") or "default"
|
|
533
|
+
ctid_info = CTidInfo(device.Device(dut, auth=webapi.JWTAuth(usr, pwd)))
|
|
534
|
+
print("SCANNING")
|
|
535
|
+
port_info = ctid_info.scan(port_number=3)
|
|
536
|
+
print(" port_info[%d]" % port_info.port, port_info.table)
|
|
537
|
+
print("-" * 40)
|
|
538
|
+
print("ITERATING")
|
|
539
|
+
for t in ctid_info:
|
|
540
|
+
print(" port %d%s:" % (t.port, t.polarity), t.table)
|
|
541
|
+
|
|
542
|
+
print("DELETING")
|
|
543
|
+
ctid_info.delete(port_number=3)
|
|
544
|
+
port_info = ctid_info.get(port_number=3)
|
|
545
|
+
if port_info is None:
|
|
546
|
+
print(" no CTid info for port 3")
|
|
547
|
+
else:
|
|
548
|
+
print(" port_info[%d]" % port_info.port, port_info.table)
|
|
549
|
+
|
|
550
|
+
print("FLASHING")
|
|
551
|
+
ctid_info.flash(port_number=3)
|
|
552
|
+
time.sleep(5)
|
|
553
|
+
ctid_info.stop()
|