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,453 @@
1
+ #
2
+ # Copyright (c) 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
+ """This module provides access to the eGauge WebAPI's /api/capture
31
+ service."""
32
+
33
+ import time
34
+ from dataclasses import dataclass, field
35
+ from decimal import Decimal
36
+ from enum import Enum
37
+
38
+ from ..error import Error
39
+ from .device import Device
40
+
41
+
42
+ class CaptureError(Error):
43
+ """Base exception for all errors raised by this module."""
44
+
45
+
46
+ @dataclass
47
+ class ChannelSamples:
48
+ """Each sample is stored as a pair (t,y) where t is the time the
49
+ sample was captured and y is value of the channel at that time.
50
+
51
+ The time t is in seconds relative to the trigger point. If the
52
+ sample occurred before the trigger point, t will be negative. If
53
+ there is no trigger point (i.e., the capture was initated
54
+ immediately or automatically), the time is relative to the first
55
+ sample.
56
+
57
+ The value y is in the physical unit of the channel it was captured
58
+ on. The physical unit of a channel can be obtained with method
59
+ Capture.channel_unit(). For ease of further processing, the
60
+ samples are stored as two lists of equal length: ts for the time
61
+ stamps and ys for the y values.
62
+
63
+ """
64
+
65
+ ts: list[float] = field(default_factory=list)
66
+ ys: list[float] = field(default_factory=list)
67
+
68
+
69
+ @dataclass
70
+ class TriggerPoint:
71
+ """A trigger point identifies the sample that first satisfied the
72
+ trigger condition. `channel` stores the name of the channel that
73
+ was the trigger channel and `index` stores the index of the sample
74
+ of that channel that stores the trigger sample.
75
+
76
+ """
77
+
78
+ channel: str
79
+ index: int
80
+
81
+
82
+ @dataclass
83
+ class CaptureResult:
84
+ """A CaptureResult stores all data for a single capture.
85
+
86
+ `unix_ts` is the Unix timestamp of the sample that triggered the
87
+ capture, or the timestamp of the first captured sample, if the
88
+ capture was triggered immediately or automatically.
89
+
90
+ `samples` stores the samples for each channel that was captured,
91
+ indexed by channel name. Note that, even though each channel has
92
+ the same number of samples, the timestamps in those samples are
93
+ generally distinct. Use interpolation methods such as
94
+ scipy.interpolate.interp1d() to interpolate channel data to a
95
+ common point in time.
96
+
97
+ The relative time stamps stored in the samples can be converted to
98
+ absolute time stamps by adding them to `unix_ts`. To avoid loss
99
+ of precision, you may want to consider using Decimal() numbers for
100
+ this purpose: UNIX timestamps may take up 32 bits or more and
101
+ microsecond granularity requires another 20 bits, quickly
102
+ approaching the precision limits of a double-precision IEEE754
103
+ floating point number.
104
+
105
+ `trigger_point` identifies the first sample that satisfied the
106
+ trigger condition. If there was no such sample, it is None. Note
107
+ that it is possible to trigger on a channel that is not captured,
108
+ in which case the trigger sample is not actually stored in the
109
+ CaptureResult.
110
+
111
+ """
112
+
113
+ unix_ts: Decimal | None = None
114
+ samples: dict[str, ChannelSamples] = field(default_factory=dict)
115
+ trigger_point: TriggerPoint | None = None
116
+
117
+
118
+ class TriggerMode(Enum):
119
+ """The trigger mode determines how the trigger condition is
120
+ evaluated:
121
+
122
+ `ANY` triggers immediately.
123
+
124
+ `RISING` triggers when the trigger channel has a rising edge that
125
+ crosses the trigger level.
126
+
127
+ `FALLING` triggers when the trigger channel has a falling edge that
128
+ crosses the trigger level.
129
+
130
+ `ABOVE` triggers if the trigger channel has a value that is greater
131
+ than the trigger level.
132
+
133
+ `BELOW` triggers if the trigger channel has a value that is less the
134
+ trigger level.
135
+
136
+ """
137
+
138
+ ANY = "any"
139
+ RISING = "rise"
140
+ FALLING = "fall"
141
+ ABOVE = "gt"
142
+ BELOW = "lt"
143
+
144
+
145
+ class Capture:
146
+ """Objects of this class are used to setup the capture of waveform
147
+ data for one or more channels, capture data synchronously
148
+ or asynchronously, and to provide access to captured data in a
149
+ convenient fashion.
150
+
151
+ Properties:
152
+ channels: set(str)
153
+ The name of the channel that should be the trigger source
154
+ or None if the capture should be triggered immediately.
155
+
156
+ trigger_channel: str
157
+ The channel used to trigger the capture or None if
158
+ the capture should be triggered immediately.
159
+
160
+ trigger_mode: TriggerMode
161
+ The trigger mode or None if no the capture should be
162
+ triggered immediately.
163
+
164
+ trigger_level: float
165
+ The trigger level or None if no trigger level should be
166
+ set. The level is in the physical unit of the trigger
167
+ channel, as provided by Capture.channel_unit().
168
+
169
+ trigger_timeout: float
170
+ This is a timeout in seconds. An automatic trigger is
171
+ generated if the trigger condition does not occur within
172
+ this timeout.
173
+
174
+ trigger_pretrigger: float
175
+ This defines how much data is captured stored ahead of the
176
+ trigger point. It is a duration in seconds. A value of 0
177
+ causes the first sample to be the trigger point. A value
178
+ of 2 would mean that 2 seconds of data are captured before
179
+ the trigger point.
180
+
181
+ """
182
+
183
+ def __init__(self, dev: Device):
184
+ """Create a capture object for a meter.
185
+
186
+ Required arguments:
187
+
188
+ dev -- The device object for the meter.
189
+
190
+ """
191
+ self.channels: set[str] = set()
192
+ self.trigger_channel: str | None = None
193
+ self.trigger_level: float | None = None
194
+ self.trigger_mode: TriggerMode | None = None
195
+ self.trigger_timeout: float | None = None
196
+ self.pretrigger: float | None = None
197
+
198
+ # private state:
199
+ self._dev = dev
200
+ self._ch_map = None
201
+
202
+ @property
203
+ def available_channels(self) -> list[str]:
204
+ """Return the list of available channel names."""
205
+ return list(self._dev.channel_info().keys())
206
+
207
+ def channel_unit(self, channel_name: str) -> str:
208
+ """Return the physical unit of a channel.
209
+
210
+ Required arguments:
211
+
212
+ channel_name -- The name of the channel.
213
+
214
+ """
215
+ cinfo = self._dev.channel_info().get(channel_name)
216
+ if cinfo is None:
217
+ raise CaptureError("Unknown channel.", channel_name)
218
+ return cinfo.unit
219
+
220
+ def start(self, duration: float | None = None, **kwargs) -> int:
221
+ """Initiate capturing samples.
222
+
223
+ This method returns an integer "cookie" which uniquely
224
+ identifies the pending capture. In the case of an error,
225
+ CaptureError is raised.
226
+
227
+ Keyword arguments:
228
+
229
+ duration -- The maximum duration in seconds for which samples
230
+ should be captured. If left unspecified, samples are
231
+ captured until the meter's sampling buffer is full.
232
+
233
+ Additional keyword arguments are passed on to requests.get().
234
+
235
+ """
236
+ params = "n"
237
+
238
+ if duration is not None:
239
+ params += f"&d={duration}"
240
+
241
+ for name in self.channels:
242
+ ch = self._ch_number(name)
243
+ params += f"&c={ch}"
244
+
245
+ if self.trigger_channel is not None:
246
+ ch = self._ch_number(self.trigger_channel)
247
+ params += f"&C={ch}"
248
+
249
+ if self.trigger_mode is not None:
250
+ params += f"&M={self.trigger_mode.value}"
251
+
252
+ if self.trigger_level is not None:
253
+ params += f"&L={self.trigger_level}"
254
+
255
+ if self.trigger_timeout is not None:
256
+ params += f"&T={1000 * self.trigger_timeout}"
257
+
258
+ if self.pretrigger is not None:
259
+ params += f"&p={self.pretrigger}"
260
+
261
+ ret = self._dev.get(f"/capture?{params}", **kwargs)
262
+ if ret.get("error"):
263
+ raise CaptureError(ret.get("error"))
264
+
265
+ if ret["state"] == "available":
266
+ raise CaptureError("Failed to start capture.", ret["state"])
267
+ return ret["cookie"]
268
+
269
+ def result(
270
+ self, cookie: int, raw=False, **kwargs
271
+ ) -> float | CaptureResult:
272
+ """Return the result of the capture.
273
+
274
+ The method returns a number if the capture is still in
275
+ progress. If so, the number is from 0 and 1, giving the
276
+ fraction of the total amount of data captured so far. If all
277
+ the data has been captured, an object of class CaptureResult
278
+ is returned. If any error occurs, an exception is raised.
279
+
280
+ Required arguments:
281
+
282
+ cookie -- The cookie identifying the capture to return the
283
+ result for. This must be a value previously returned by a
284
+ call to Capture.start().
285
+
286
+ Keyword arguments:
287
+
288
+ raw -- If True, raw data is returned. Raw samples are numeric
289
+ values as returned by the sampling hardware, not physical
290
+ quantities.
291
+
292
+ """
293
+ params = f"n={cookie}"
294
+
295
+ if raw:
296
+ params += "&r=True"
297
+
298
+ ret = self._dev.get(f"/capture?{params}", **kwargs)
299
+ if ret.get("error"):
300
+ raise CaptureError(ret.get("error"))
301
+
302
+ if ret["state"] == "available":
303
+ raise CaptureError("Capture aborted.") # somebody interfered?
304
+ if ret["state"] == "armed":
305
+ return 0.0
306
+ if ret["state"] == "capturing":
307
+ return ret["count"] / ret["max_count"]
308
+ if ret["state"] != "full":
309
+ raise CaptureError("Unknown capture state.", ret["state"])
310
+
311
+ ch_mask = 0
312
+ for n, w in enumerate(ret["ch_mask"]):
313
+ ch_mask |= w << (n * 32)
314
+
315
+ channel_names = []
316
+ result = CaptureResult()
317
+ ch = 0
318
+ m = 1
319
+ while ch_mask != 0:
320
+ if (ch_mask & m) != 0:
321
+ ch_mask &= ~m
322
+ name = self._ch_name(ch)
323
+ channel_names.append(name)
324
+ if name in self.channels:
325
+ # this is data we're interested in:
326
+ result.samples[name] = ChannelSamples()
327
+ ch += 1
328
+ m <<= 1
329
+
330
+ result.unix_ts = Decimal(ret["first_sample"])
331
+ tick_period = 1 / ret["ts_freq"]
332
+ dt = None
333
+ ci = 0 # cycles through the channels
334
+ sample_count = 0
335
+ for r in ret["r"]:
336
+ name = channel_names[ci]
337
+
338
+ if dt is None:
339
+ dt = 0
340
+ else:
341
+ dt += r["t"] * tick_period
342
+
343
+ if r.get("trigger"):
344
+ # update t for the pretrigger data:
345
+ for cs in result.samples.values():
346
+ for i, _ in enumerate(cs.ts):
347
+ cs.ts[i] -= dt
348
+ dt = 0
349
+
350
+ result.trigger_point = TriggerPoint(
351
+ channel=name, index=sample_count
352
+ )
353
+
354
+ for d in r["d"]:
355
+ cs = result.samples.get(name, None)
356
+ ci += 1
357
+ if ci >= len(channel_names):
358
+ ci = 0
359
+ sample_count += 1
360
+ name = channel_names[ci]
361
+ if cs is None:
362
+ continue # we're not interested in this channel
363
+ cs.ts.append(dt)
364
+ cs.ys.append(d)
365
+ return result
366
+
367
+ def acquire(
368
+ self, duration: float | None = None, **kwargs
369
+ ) -> CaptureResult:
370
+ """Synchronously capture waveform data.
371
+
372
+ This is a convenience method which first calls
373
+ Capture.start(duration) and then repeatedly calls
374
+ Capture.result() until all the data has been received. The
375
+ method sleeps for 100ms before repeating a call to
376
+ Capture.result(). The method returns the captured data if
377
+ successful or raises CaptureError otherwise.
378
+
379
+ Keyword arguments:
380
+
381
+ duration -- The maximum duration in seconds for which samples
382
+ should be captured. If left unspecified, samples are
383
+ captured until the meter's sampling buffer is full.
384
+
385
+ Additional keyword arguments are passed on to requests.get().
386
+
387
+ """
388
+ cookie = self.start(duration, **kwargs)
389
+ while True:
390
+ time.sleep(100e-3)
391
+ result = self.result(cookie)
392
+ if isinstance(result, CaptureResult):
393
+ return result
394
+
395
+ def reset(self, cookie: int | None = None, **kwargs):
396
+ """Cancel a pending capture and reset the capture service.
397
+
398
+ This resets the meter's state so it is available for a new
399
+ capture again.
400
+
401
+ This method raises a CaptureError if the reset fails for any
402
+ reason.
403
+
404
+ Keyword arguments:
405
+
406
+ cookie -- If specified, only the capture identified by the
407
+ cookie is canceled (assuming it is still pending).
408
+
409
+ Additional keyword arguments are passed along to the
410
+ requests.get() call which cancels the capture.
411
+
412
+ """
413
+ params = "R"
414
+ if cookie is not None:
415
+ params = f"n={cookie}"
416
+ ret = self._dev.get(f"/capture?{params}", **kwargs)
417
+ if ret.get("error"):
418
+ raise CaptureError(ret.get("error"))
419
+
420
+ state = ret.get("state")
421
+ if state != "available":
422
+ raise CaptureError("Unexpected capture state.", state)
423
+
424
+ def _ch_number(self, name: str) -> int:
425
+ """Get the channel number of a channel.
426
+
427
+ This raises a capture error if the channel is unknown.
428
+
429
+ Required arguments:
430
+
431
+ name -- The name of the channel whose number to return.
432
+
433
+ """
434
+ cinfo = self._dev.channel_info().get(name)
435
+ if cinfo is None:
436
+ raise CaptureError("Unknown channel.", name)
437
+ return cinfo.chan
438
+
439
+ def _ch_name(self, ch: int) -> str | None:
440
+ """Get the channel name for a channel number.
441
+
442
+ This returns None if the channel number is unknown.
443
+
444
+ Required arguments:
445
+
446
+ ch -- The number of the channel whose name to return.
447
+
448
+ """
449
+ if self._ch_map is None:
450
+ self._ch_map = {}
451
+ for name, cinfo in self._dev.channel_info().items():
452
+ self._ch_map[int(cinfo.chan)] = name
453
+ return self._ch_map.get(ch)