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,338 @@
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
+ """This module provides access to the eGauge WebAPI's /api/register
31
+ service."""
32
+
33
+ import decimal
34
+ import json
35
+
36
+ from deprecated import deprecated
37
+
38
+ from ..error import Error
39
+ from ..json_api import JSONAPIError
40
+ from .device import Device
41
+ from .physical_quantity import PhysicalQuantity
42
+ from .register_row import RegisterRow
43
+ from .virtual_register import VirtualRegister
44
+
45
+
46
+ class RegisterError(Error):
47
+ """Raised if for any register related errors."""
48
+
49
+
50
+ def regnames_to_ranges(dev, regs):
51
+ """Convert a list of register names to a register-ranges string."""
52
+ indices = set()
53
+ for name in regs:
54
+ formula = dev.reg_formula(name)
55
+ if formula is None:
56
+ indices.add(dev.reg_idx(name))
57
+ else:
58
+ for phys_reg in formula.phys_regs:
59
+ indices.add(dev.reg_idx(phys_reg))
60
+ indices = sorted(list(indices))
61
+
62
+ ranges = []
63
+ idx = 0
64
+ while idx < len(indices):
65
+ start = stop = indices[idx]
66
+ idx += 1
67
+ while idx < len(indices) and indices[idx] == stop + 1:
68
+ stop = indices[idx]
69
+ idx += 1
70
+ if start == stop:
71
+ ranges.append(str(start))
72
+ else:
73
+ ranges.append(str(start) + ":" + str(stop))
74
+ if len(ranges) < 1:
75
+ return None
76
+ return "+".join(ranges)
77
+
78
+
79
+ class Register:
80
+ def __init__(
81
+ self,
82
+ dev: Device,
83
+ params: dict | None = None,
84
+ regs: list[str] | None = None,
85
+ **kwargs,
86
+ ):
87
+ """Fetch register data from a meter.
88
+
89
+ Required arguments:
90
+
91
+ dev -- The device object to use to access the meter.
92
+
93
+ Keywoard arguments:
94
+
95
+ params -- A dictionary of query parameters that specify the
96
+ data to return (default `None`).
97
+
98
+ regs -- The list of registers to return (default `None`). If
99
+ `None`, all registers are returned. Otherwise,
100
+ data is returned only for the named registers.
101
+
102
+ Additional keyword arguments are passed along to the
103
+ requests.get() method.
104
+
105
+ """
106
+ self.dev = dev
107
+ # maps regname to index in "register"/"row" arrays:
108
+ self.regorder = None
109
+ self.iter_range_idx = 0
110
+ self.iter_row_idx = 0
111
+ self.iter_ts = decimal.Decimal(0)
112
+ if params is None:
113
+ params = {}
114
+ self._requested_regs = regs
115
+ if regs is not None:
116
+ reg_ranges = regnames_to_ranges(dev, regs)
117
+ if reg_ranges is not None:
118
+ params["reg"] = reg_ranges
119
+ try:
120
+ self.raw = self.dev.get("/register", params=params, **kwargs)
121
+ except JSONAPIError as e:
122
+ raise RegisterError("failed to read registers: {e}", params) from e
123
+
124
+ def _create_regorder(self) -> dict[str, int]:
125
+ """Create a dictionary mapping a register name to the index
126
+ within the `registers' and `row' arrays which contain the info
127
+ for that particular name.
128
+
129
+ """
130
+ regorder = {}
131
+ for index, reg in enumerate(self.raw["registers"]):
132
+ regorder[reg["name"]] = index
133
+ return regorder
134
+
135
+ def ts(self) -> decimal.Decimal | None:
136
+ """Return the timestamp of the register rates.
137
+
138
+ The returned value is seconds since the Unix epoch.
139
+
140
+ """
141
+ if self.raw is None:
142
+ return None
143
+ return decimal.Decimal(self.raw["ts"])
144
+
145
+ @property
146
+ def regs(self) -> list[str]:
147
+ """Return the list of available register names."""
148
+ if self._requested_regs:
149
+ return self._requested_regs
150
+
151
+ if self.regorder is None:
152
+ self.regorder = self._create_regorder()
153
+
154
+ return list(self.regorder.keys()) + self.dev.reg_virtuals()
155
+
156
+ def have(self, reg):
157
+ """Check if a register is available (known).
158
+
159
+ Returns True if the register is known, False otherwise.
160
+
161
+
162
+ Required arguments:
163
+
164
+ reg -- The name of the register to check for.
165
+
166
+ """
167
+ return reg in self.regs
168
+
169
+ def _rate(self, reg: str) -> float | None:
170
+ if self.raw is None:
171
+ return None
172
+
173
+ row = self.raw["registers"]
174
+ formula = self.dev.reg_formula(reg)
175
+ if isinstance(formula, VirtualRegister):
176
+ return formula.calc(lambda reg: self._row_rate(row, reg))
177
+ return self._row_rate(row, reg)
178
+
179
+ def _row_rate(self, row: list[dict], reg: str) -> float:
180
+ if self.regorder is None:
181
+ self.regorder = self._create_regorder()
182
+
183
+ idx = self.regorder[reg]
184
+ return row[idx]["rate"]
185
+
186
+ @deprecated(version="0.7.0", reason="use pq_rate() instead")
187
+ def rate(self, reg: str) -> float | None:
188
+ return self._rate(reg)
189
+
190
+ def pq_rate(self, reg: str) -> PhysicalQuantity | None:
191
+ """Return the rate of change value of a register.
192
+
193
+ Returns `None` if the rate is unknown.
194
+
195
+ Required arguments:
196
+
197
+ reg -- The name of the register whose rate value to return.
198
+
199
+ """
200
+ rate = self._rate(reg)
201
+ if rate is None:
202
+ return None
203
+ return PhysicalQuantity(
204
+ rate, self.type_code(reg), is_cumul=False
205
+ ).to_preferred()
206
+
207
+ def type_code(self, reg) -> str:
208
+ """Return the type-code of a register.
209
+
210
+ Required arguments:
211
+
212
+ reg -- The name of the register whose type-code to return.
213
+
214
+ """
215
+ return self.dev.reg_type(reg)
216
+
217
+ @deprecated(version="0.7.0", reason="use Device.reg_formula() instead")
218
+ def formula(self, reg: str) -> str | None:
219
+ if self.raw is None:
220
+ return None
221
+ if self.regorder is None:
222
+ self.regorder = self._create_regorder()
223
+ return self.raw["registers"][self.regorder[reg]]["formula"]
224
+
225
+ def index(self, reg: str) -> int | None:
226
+ """Return the register index of a register.
227
+
228
+ Required arguments:
229
+
230
+ reg -- The name of the register whose index to return.
231
+
232
+ """
233
+ if self.raw is None:
234
+ return None
235
+ if self.regorder is None:
236
+ self.regorder = self._create_regorder()
237
+ return self.raw["registers"][self.regorder[reg]]["idx"]
238
+
239
+ def database_id(self, reg: str) -> int | None:
240
+ """Return the database-id of a register.
241
+
242
+ Returns `None` if the register is unknown.
243
+
244
+ Required arguments:
245
+
246
+ reg -- The name of the register whose database id to return.
247
+
248
+ """
249
+ if self.raw is None:
250
+ return None
251
+ if self.regorder is None:
252
+ self.regorder = self._create_regorder()
253
+ return self.raw["registers"][self.regorder[reg]]["did"]
254
+
255
+ def _make_row(
256
+ self, ts: decimal.Decimal, cumul_list: list[str]
257
+ ) -> RegisterRow:
258
+ """Convert a "rows" array to a RegisterRow()."""
259
+ row = RegisterRow(ts)
260
+
261
+ # first, fill in the physical registers:
262
+ phys = {}
263
+ for idx, cumul in enumerate(cumul_list):
264
+ ri = self.raw["registers"][idx]
265
+ reg = ri["name"]
266
+ cumul = int(cumul)
267
+ phys[reg] = cumul
268
+ if self._requested_regs and reg not in self._requested_regs:
269
+ continue
270
+ row.regs[reg] = cumul
271
+ row.type_codes[reg] = self.type_code(reg)
272
+
273
+ # second, fill in virtual registers (if any):
274
+ for reg in self.dev.reg_virtuals():
275
+ if self._requested_regs and reg not in self._requested_regs:
276
+ continue
277
+ formula = self.dev.reg_formula(reg)
278
+ if not isinstance(formula, VirtualRegister):
279
+ raise RegisterError(
280
+ f"Formula for virtual register {reg} is missing."
281
+ )
282
+ row.regs[reg] = formula.calc(lambda reg: phys[reg])
283
+ row.type_codes[reg] = self.type_code(reg)
284
+ return row
285
+
286
+ def __getitem__(self, index: int) -> RegisterRow:
287
+ """Return the n-th row of cumulative register values as a
288
+ RegisterRow.
289
+
290
+ """
291
+ range_idx = 0
292
+ while True:
293
+ curr_range = self.raw["ranges"][range_idx]
294
+ ts = decimal.Decimal(curr_range["ts"])
295
+ if index < len(curr_range["rows"]):
296
+ ts -= index * decimal.Decimal(str(curr_range["delta"]))
297
+ return self._make_row(ts, curr_range["rows"][index])
298
+ range_idx += 1
299
+ index -= len(curr_range["rows"])
300
+
301
+ def __iter__(self):
302
+ """Iterate over cumulative register values.
303
+
304
+ Each iteration returns one row of data as a RegisterRow.
305
+
306
+ """
307
+ if self.raw is None:
308
+ return None
309
+ self.iter_range_idx = 0
310
+ self.iter_row_idx = 0
311
+ return self
312
+
313
+ def __next__(self) -> RegisterRow:
314
+ if (
315
+ not isinstance(self.raw, dict)
316
+ or "ranges" not in self.raw
317
+ or self.iter_range_idx >= len(self.raw["ranges"])
318
+ ):
319
+ raise StopIteration
320
+ curr_range = self.raw["ranges"][self.iter_range_idx]
321
+
322
+ if self.iter_row_idx == 0:
323
+ self.iter_ts = decimal.Decimal(curr_range["ts"])
324
+
325
+ row = self._make_row(
326
+ self.iter_ts, curr_range["rows"][self.iter_row_idx]
327
+ )
328
+
329
+ self.iter_ts -= decimal.Decimal(str(curr_range["delta"]))
330
+ self.iter_row_idx += 1
331
+ if self.iter_row_idx >= len(curr_range["rows"]):
332
+ self.iter_row_idx = 0
333
+ self.iter_range_idx += 1
334
+ return row
335
+
336
+ def __str__(self) -> str:
337
+ """Returns the raw register data as a JSON-encoded string."""
338
+ return json.dumps(self.raw)
@@ -0,0 +1,145 @@
1
+ #
2
+ # Copyright (c) 2020, 2022-2023 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 a helper class for a row of time-stamped
31
+ register data."""
32
+
33
+ import copy
34
+ import json
35
+
36
+ from deprecated import deprecated
37
+
38
+ from ..error import Error
39
+ from .physical_quantity import PhysicalQuantity
40
+ from .register_type import Units
41
+
42
+
43
+ class RegRowError(Error):
44
+ """All errors in this module raise this exception."""
45
+
46
+
47
+ class RegisterRow:
48
+ """A row of register data contains a timestamp and the signed 64-bit
49
+ register values for that timestamp. Subtracing one row from
50
+ another creates a difference row. On a difference row,
51
+ RegisterRow.pq_accu() can be called to get the amount by which a
52
+ register changed between the two rows.
53
+
54
+ Similarly, RegisterRow.pq_avg() can be called to calculate the
55
+ average register value that was in effect between the two rows."""
56
+
57
+ def __init__(self, ts, regs=None, type_codes=None, is_diff=False):
58
+ self.ts = ts
59
+ self.is_diff = is_diff
60
+ if regs is None:
61
+ self.regs = {}
62
+ else:
63
+ self.regs = copy.copy(regs)
64
+ if type_codes is None:
65
+ self.type_codes = {}
66
+ else:
67
+ self.type_codes = copy.copy(type_codes)
68
+
69
+ @deprecated(version="0.7.0", reason="use pq_avg() instead")
70
+ def avg(self, regname) -> float:
71
+ """Return the time-average of the register value."""
72
+ self._assert(self.is_diff)
73
+ return self.regs[regname] / self.ts
74
+
75
+ def pq_rate(self, regname) -> PhysicalQuantity:
76
+ """Return register rate of change as a physical quantity in the
77
+ preferred unit of the default unit system (see
78
+ PhysicalQuantity.set_unit_system()).
79
+
80
+ """
81
+ self._assert(self.is_diff)
82
+ val = self.regs[regname]
83
+ return PhysicalQuantity(
84
+ val, self.type_codes[regname], is_cumul=False
85
+ ).to_preferred()
86
+
87
+ def pq_avg(self, regname) -> PhysicalQuantity:
88
+ """Return the time-average of the register value as a physical
89
+ quantity in the preferred unit of the default unit system
90
+ (see PhysicalQuantity.set_unit_system())."""
91
+ self._assert(self.is_diff)
92
+ ute = Units.table[self.type_codes[regname]]
93
+ val = self.regs[regname] / ute.fix_scale / self.ts
94
+ return PhysicalQuantity(
95
+ val, self.type_codes[regname], is_cumul=False
96
+ ).to_preferred()
97
+
98
+ def pq_accu(self, regname):
99
+ """Return the accumulated register value as a physical quantity in the
100
+ preferred unit of the default unit system (see
101
+ PhysicalQuantity.set_unit_system())."""
102
+ ute = Units.table[self.type_codes[regname]]
103
+ val = self.regs[regname] * ute.cumul_scale
104
+
105
+ # If the row is not a difference row, we cannot convert
106
+ # between units with additive constants (e.g., °C·d to °F·d).
107
+ # Set `dt` to None in that case, so those conversions will
108
+ # fail visibily:
109
+ dt = self.ts if self.is_diff else None
110
+ return PhysicalQuantity(
111
+ val, self.type_codes[regname], is_cumul=True
112
+ ).to_preferred(dt=dt)
113
+
114
+ def __sub__(self, subtrahend):
115
+ """Subtract two register rows from each other and return the result.
116
+ The SUBTRAHEND must have values for all of the registers in
117
+ the minuend row.
118
+
119
+ """
120
+ self._assert(not self.is_diff)
121
+ subtrahend._assert(not self.is_diff)
122
+
123
+ ret = RegisterRow(self.ts, self.regs, self.type_codes)
124
+ ret.ts = float(ret.ts - subtrahend.ts)
125
+ ret.is_diff = True
126
+ for name in self.regs:
127
+ ret.regs[name] -= subtrahend.regs[name]
128
+ return ret
129
+
130
+ def _assert(self, cond: bool):
131
+ if cond:
132
+ return
133
+ if self.is_diff:
134
+ raise RegRowError("Cannot subtract a difference row.")
135
+ raise RegRowError("The row must be a difference.")
136
+
137
+ def __str__(self):
138
+ return f"{self.ts}: {json.dumps(self.regs)}"
139
+
140
+ def __iter__(self):
141
+ """Iterate over cumulative register values. Each iteration returns
142
+ one row of data as a RegisterRow.
143
+
144
+ """
145
+ return iter(self.regs)