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,439 @@
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
+ import re
31
+ from typing import Self
32
+
33
+ from .physical_units import PhysicalValue
34
+ from .register_type import RegisterType, Units, UnitSystem, UnitTableEntry
35
+
36
+ UNIT_PATTERN = re.compile(r"(.*)/(.*)$")
37
+
38
+
39
+ class Error(Exception):
40
+ """Base class for all exceptions raised by this module."""
41
+
42
+
43
+ class PhysicalQuantity:
44
+ """A physical quantity is a measurement that is expressed as a value
45
+ and a a unit, for example, 10 kW has a value of 10 and a unit of
46
+ "kW"."""
47
+
48
+ # The currency symbol/string to use for the monetary units:
49
+ _currency: str = "***call set_currency()***"
50
+
51
+ # The unit system PhysicalQuantity.to_preferred() should use by default.
52
+ _default_unit_system = UnitSystem.METRIC
53
+
54
+ @staticmethod
55
+ def is_preferred(unit_system: UnitSystem, unit: str) -> bool:
56
+ """Check if a unit is preferred in the given unit system.
57
+
58
+ Returns `True` if the unit is preferred in the given unit
59
+ system.
60
+
61
+ Required arguments:
62
+
63
+ unit_system -- The unit system to check against.
64
+
65
+ unit -- The unit name to check.
66
+
67
+ """
68
+ return unit in Units.preferred[unit_system.value]
69
+
70
+ @staticmethod
71
+ def time_integrated_unit(unit: str) -> str:
72
+ """Given a quantity of a particular unit, derive the unit that
73
+ results when integrating the quantity over time.
74
+
75
+ This basically appends the string "·s" to the unit name,
76
+ except this method knows to simplify certain units. For
77
+ example, 'm^3/s·s' is simplified to 'm^3'.
78
+
79
+ Required arguments:
80
+
81
+ unit -- The unit name to integrate over time.
82
+
83
+ """
84
+ m = UNIT_PATTERN.match(unit)
85
+ if m and m.lastindex and m.lastindex >= 2:
86
+ prefix = m.group(1)
87
+ suffix = m.group(2)
88
+ if suffix == "s":
89
+ # ${prefix}/s·s cancels out:
90
+ return prefix
91
+ if prefix == "":
92
+ # "/${suffix}" => "s/${suffix}"
93
+ return "s/" + suffix
94
+ return prefix + "·s/" + suffix
95
+ if unit == "":
96
+ return "s"
97
+ return unit + "·s"
98
+
99
+ @classmethod
100
+ def available_units(cls, type_code: str, is_cumul: bool) -> list[str]:
101
+ entry = Units.table.get(type_code)
102
+
103
+ if not isinstance(entry, UnitTableEntry):
104
+ return []
105
+
106
+ unit = entry.cumul_unit if is_cumul else entry.rate_unit
107
+ if unit is None:
108
+ return []
109
+
110
+ return Units.units.alternate_units(unit)
111
+
112
+ @classmethod
113
+ def scales(cls, unit: str) -> list[str]:
114
+ return Units.units.scaled_units(unit)
115
+
116
+ def __init__(
117
+ self,
118
+ value: float | PhysicalValue,
119
+ type_code: str | None = None,
120
+ is_cumul: bool = False,
121
+ unit: str | None = None,
122
+ diff: bool = False,
123
+ ):
124
+ """Create a PhysicalQuantity object.
125
+
126
+ The object can be created based on a magnitude and either its
127
+ unit or its type-code. Alternatively, the object can also be
128
+ created directly from a PhysicalValue object.
129
+
130
+ Required arguments:
131
+
132
+ value -- The magnitude of the physical quantity to create or
133
+ the PhysicalValue object to create the quantity for.
134
+
135
+ Keyword arguments:
136
+
137
+ type_code -- The type-code to use to determine the physical
138
+ unit of the created quantity.
139
+
140
+ is_cumul -- If `type_code` is specified, this must be `True`
141
+ if the value is cumulative (rather than a rate-of-change).
142
+
143
+ diff -- If `unit` is specified or if `value` is a
144
+ PhysicalValue object, this argument must be `True` if the
145
+ created quantity should represent a change in value (i.e.,
146
+ a difference). This argument is relevant only for unit
147
+ conversions that involve an additive term, as is the case,
148
+ e.g., when converting temperature from celsius to
149
+ fahrenheit or kelvin. For example, 0°C equals 32°F as an
150
+ absolute temperature, but a temperature change of 0°C
151
+ corresponds to a temperature change of 0°F, so for the
152
+ second case, this argument would have to be set to `True`
153
+ to yield correct unit conversions.
154
+
155
+ """
156
+ if isinstance(value, PhysicalValue):
157
+ self.pv = value
158
+ self.diff = diff
159
+ else:
160
+ if unit is None:
161
+ if type_code is None:
162
+ raise Error("Unit or type-code must be specified.")
163
+ ute = Units.table[type_code]
164
+ unit = ute.cumul_unit if is_cumul else ute.rate_unit
165
+ if unit is None:
166
+ raise Error(f"Unit unknown for type-code {type_code}.")
167
+ self.diff = type_code == RegisterType.TEMP_DIFF
168
+ else:
169
+ self.diff = diff
170
+ self.pv = PhysicalValue(value, unit)
171
+
172
+ def __str__(self):
173
+ return self.pv.__str__()
174
+
175
+ @property
176
+ def value(self) -> float:
177
+ """Return the value (magnitude) of the quantity."""
178
+ return self.pv.value
179
+
180
+ @property
181
+ def unit(self) -> str:
182
+ """Return the unit of the quantity.
183
+
184
+ Note: when presenting a quantity to the user,
185
+ PhysicalQuantity.locale_unit() should be used.
186
+
187
+ """
188
+ return self.pv.unit
189
+
190
+ def to(self, unit: str, dt: float | None = None) -> float:
191
+ """Return the quantity's value in a given unit.
192
+
193
+ Raises an Error if the conversion is not possible.
194
+
195
+ Required arguments:
196
+
197
+ unit -- The unit the quantity's value should be converted to.
198
+
199
+ dt -- If the quantity is a cumulative value this must specify
200
+ the time in seconds over which the quantity was measured.
201
+
202
+ """
203
+ x = self._to_unit(unit, dt)
204
+ if x is None:
205
+ raise Error("Conversion not possible.", self.pv.unit, unit)
206
+ return x
207
+
208
+ @property
209
+ def locale_unit(self):
210
+ """Return the localized unit name.
211
+
212
+ This must be used when presenting a quantity to the user. For
213
+ most units, this is identical to PhysiqlQuantity.unit().
214
+ However, for monetary values this method replaces the string
215
+ "${currency}" in units with the string established with
216
+ PhysicalQuantity.set_currency(). Note that once a unit is
217
+ localized, it cannot be converted to other units anymore, so
218
+ this should be used for display purposes only.
219
+
220
+ """
221
+ return self.pv.unit.replace("${currency}", self._currency)
222
+
223
+ @classmethod
224
+ def set_currency(cls, currency: str):
225
+ """Set the symbol/string to use as the local currency.
226
+
227
+ Required arguments:
228
+
229
+ currency: The string representing the local currency. For
230
+ example, "$", "€", "¥", "CHF", or similar.
231
+
232
+ """
233
+ cls._currency = currency
234
+
235
+ @classmethod
236
+ def set_unit_system(cls, default_unit_system: UnitSystem):
237
+ """Select the unit system to use by default.
238
+
239
+ Required arguments:
240
+
241
+ default_unit_system -- Must be one of UnitSystem.METRIC
242
+ (metric units) or UnitSystem.IMPERIAL (imperial, aka, US
243
+ conventional units).
244
+
245
+ """
246
+ cls._default_unit_system = default_unit_system
247
+
248
+ def to_unit(self, unit: str, dt: float | None = None) -> Self:
249
+ """Convert the quantity to a specified unit.
250
+
251
+ Returns the physical quantity itself.
252
+
253
+ Required arguments:
254
+
255
+ unit -- The unit to convert the quantity to.
256
+
257
+ Keyword arguments:
258
+
259
+ dt -- The time-duration over which the quantity was measured
260
+ (default None). This is needed because some conversions
261
+ (e.g., °C·s to °K·s or °F·s) require a time-dependent
262
+ adjustment.
263
+
264
+ """
265
+ x = self._to_unit(unit, dt)
266
+ if x is not None:
267
+ self._set(x, unit)
268
+ return self
269
+
270
+ def to_preferred(
271
+ self, unit_system: UnitSystem | None = None, dt: float | None = None
272
+ ) -> Self:
273
+ """Convert the quantity to the preferred unit of the specified
274
+ unit system.
275
+
276
+ Returns the physical quantity itself.
277
+
278
+ Keyword arguments:
279
+
280
+ unit_system -- The unit system to use for the conversion. If
281
+ `None`, the unit system established with a call to
282
+ PhysicalQuantity.set_unit_system() is used.
283
+
284
+ dt -- The time-duration over which the quantity was measured.
285
+ This is needed only for certain type conversions (e.g.,
286
+ °C·s to °K·s or F·s) that require a time-dependent
287
+ adjustment.
288
+
289
+ """
290
+
291
+ if unit_system is None:
292
+ unit_system = self._default_unit_system
293
+
294
+ available = Units.units.alternate_units(self.pv.unit)
295
+ for unit in available:
296
+ preferred_unit = None
297
+ if PhysicalQuantity.is_preferred(unit_system, unit):
298
+ preferred_unit = unit
299
+ else:
300
+ for scaled in Units.units.scaled_units(unit):
301
+ if PhysicalQuantity.is_preferred(unit_system, scaled):
302
+ preferred_unit = scaled
303
+ break
304
+ if preferred_unit is not None:
305
+ x = Units.units.convert(
306
+ self.pv.value, self.pv.unit, preferred_unit, self.diff, dt
307
+ )
308
+ if x is not None:
309
+ return self._set(x, preferred_unit)
310
+ break
311
+ # could not find preferred unit; leave quantity unchanged
312
+ return self
313
+
314
+ def to_cumul(self, dt: float) -> Self:
315
+ """Convert this physical quantity, which must be an average
316
+ rate over a given time-period, to the corresponding cumulative
317
+ value.
318
+
319
+ Returns the physical quantity itself.
320
+
321
+ Required arguments:
322
+
323
+ dt -- The time in seconds over which the average rate was
324
+ measured.
325
+
326
+ """
327
+ # first, convert the unit to the primary unit, since the primary
328
+ # units are most easily simplified by time_integrated_unit():
329
+ primary_unit = Units.units.primary_unit(self.pv.unit)
330
+ if primary_unit is not None and primary_unit != self.pv.unit:
331
+ self.to_unit(primary_unit, dt)
332
+
333
+ cumul_unit = PhysicalQuantity.time_integrated_unit(self.pv.unit)
334
+ return self._set(dt * self.pv.value, cumul_unit)
335
+
336
+ def auto_scale(self) -> Self:
337
+ """If the measurement unit (e.g., 'W') has scaled units
338
+ available (e.g., 'mW' or 'kW'), convert this quantity to a
339
+ scaled unit such that the absolute value is either zero or in
340
+ the range from 1 to 1000.
341
+
342
+ Returns the physical quantity itself.
343
+
344
+ """
345
+ self.pv = Units.units.auto_scale(self.pv)
346
+ return self
347
+
348
+ def _to_unit(self, unit: str, dt: float | None = None) -> float | None:
349
+ """Try to convert this quantity to another unit.
350
+
351
+ If the conversion is not possible, `None` is returned.
352
+ Otherwise, the value in the desired unit is returned.
353
+
354
+ Required arguments:
355
+
356
+ unit -- The unit to convert the quantity to.
357
+
358
+ dt -- The time-duration over which the quantity was measured.
359
+ This is needed only for certain type conversions (e.g.,
360
+ °C·s to °K·s or F·s) that require a time-dependent
361
+ adjustment.
362
+
363
+ """
364
+ x = Units.units.convert(
365
+ self.pv.value, self.pv.unit, unit, self.diff, dt
366
+ )
367
+ if x is not None:
368
+ return x
369
+
370
+ primary_unit = Units.units.primary_unit(self.pv.unit) or ""
371
+ if primary_unit[-2:] == "·s" and unit[-2:] == "·s":
372
+ if self.pv.unit != primary_unit:
373
+ x = Units.units.convert(
374
+ self.pv.value, self.pv.unit, primary_unit, self.diff, dt
375
+ )
376
+ else:
377
+ x = self.pv.value
378
+
379
+ if x is not None:
380
+ # If the conversion between two rate-units involves a
381
+ # simple scale-factor, then the time-integrated
382
+ # (cumulative) units of those rate-units can be
383
+ # converted using the same factor. This only works as
384
+ # long as the rate-conversion is linear. Fortunately,
385
+ # we don't have any crazy units using non-linear
386
+ # conversions so far.
387
+ x = Units.units.convert(
388
+ x, primary_unit[:-2], unit[:-2], self.diff, dt
389
+ )
390
+ if x is not None:
391
+ return x
392
+ # conversion failed - leave the quantity unchanged
393
+ return None
394
+
395
+ def _set(self, val: float, unit: str) -> Self:
396
+ """Establish a new value for the quantity.
397
+
398
+ Returns the physical quantity itself.
399
+
400
+ Required arguments:
401
+
402
+ val -- The new magnitude of the quantity.
403
+
404
+ unit -- The new unit of the quantity.
405
+
406
+ """
407
+ self.pv.value = val
408
+ self.pv.unit = unit
409
+ return self
410
+
411
+
412
+ def test():
413
+ pq = PhysicalQuantity(3.1415, "P")
414
+ print(pq)
415
+ print(pq.to_preferred(UnitSystem.METRIC), pq)
416
+ pq.auto_scale()
417
+ print("auto-scaled", pq)
418
+ print(pq.to_unit("kW"), pq)
419
+ pq.to_cumul(3600)
420
+ print(pq)
421
+ print(pq.to_preferred(UnitSystem.METRIC), pq)
422
+
423
+ pq = PhysicalQuantity(24, "T")
424
+ print(pq)
425
+ print(pq.to_unit("°F"), pq)
426
+
427
+ pq = PhysicalQuantity(24, "T")
428
+ print(pq.to_preferred(UnitSystem.IMPERIAL), pq)
429
+
430
+ pq = PhysicalQuantity(24 * (24 * 3600), "T", is_cumul=True)
431
+ print(pq)
432
+ print(pq.to_unit("°F·d", dt=24 * 3600), pq)
433
+
434
+ print(PhysicalQuantity.available_units("P", False))
435
+ print(PhysicalQuantity.available_units("P", True))
436
+ print(PhysicalQuantity.scales("W"))
437
+ print(PhysicalQuantity.scales("Wh"))
438
+ print(PhysicalQuantity.available_units("T", False))
439
+ print(PhysicalQuantity.available_units("T", True))