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,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()