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,201 @@
1
+ #!/usr/bin/env python3
2
+ #
3
+ # Copyright (c) 2023-2024 eGauge Systems LLC
4
+ #
5
+ # See LICENSE file for details.
6
+ #
7
+ """This test program demonstrates the use of class
8
+ egauge.webapi.device.Local to access the meter-local measurements
9
+ available through the /local WebAPI. When executed, it fetches all
10
+ available sensor values and derived quantities and outputs them to to
11
+ the terminal.
12
+
13
+ You can set environment variables:
14
+
15
+ EGDEV - the URL of the meter to use (e.g., http://eGaugeXXX.local)
16
+ EGUSR - the username to log in to the meter (e.g., "owner")
17
+ EGPWD - the password for the username
18
+
19
+ Alternatively, you can edit examples/test_common.py to suit your needs.
20
+ """
21
+
22
+ import sys
23
+ from datetime import datetime
24
+
25
+ from egauge.examples import test_common
26
+ from egauge.webapi.device import Local
27
+ from egauge.webapi.device.physical_quantity import PhysicalQuantity
28
+
29
+
30
+ def format_measurements(
31
+ sensor_name: str, pq_list: list[PhysicalQuantity | None]
32
+ ):
33
+ """Format a SENSOR_NAME and a list of physical quantities PQ_LIST such
34
+ that each has a fixed width of 18 characters.
35
+
36
+ """
37
+ ret = f"{sensor_name:18s}"
38
+ for pq in pq_list:
39
+ formatted = f"{pq.value:14.3f} {pq.unit}" if pq else "n/a"
40
+ if len(formatted) < 18:
41
+ formatted = (18 - len(formatted)) * " " + formatted
42
+ ret += formatted
43
+ return ret
44
+
45
+
46
+ # Request values for all available sensors: built-in environmental
47
+ # sensors, all line inputs, and all sensors. Note that, except for
48
+ # environmental sensors, sensor values are measured only if there is
49
+ # at least one register using it. Thus, you may need to configure
50
+ # some registers to see the desired sensors.
51
+ sensors = ["env=all", "l=all", "s=all"]
52
+
53
+ # Request all available sections: the sensor values themselves as well
54
+ # as derived real energy and apparent energy sections.
55
+ sections = [Local.SECTION_VALUES, Local.SECTION_ENERGY, Local.SECTION_APPARENT]
56
+
57
+ # Request all available metrics for each sensor/energy: the rate
58
+ # (e.g., power), the accumulated value (e.g., energy), as well as the
59
+ # sensor's type (physical unit).
60
+ metrics = [Local.METRIC_RATE, Local.METRIC_CUMUL, Local.METRIC_TYPE]
61
+
62
+ # Request all available sensor measurements: normal (RMS value), mean,
63
+ # and frequency:
64
+ measurements = ["normal", "mean", "freq"]
65
+
66
+ query_string = "&".join(sensors + sections + metrics + measurements)
67
+
68
+ # Fetch the sensor values from the meter:
69
+ local = Local(test_common.dev, query_string)
70
+
71
+ ts = local.ts()
72
+ if ts is None:
73
+ print("Failed to fetch local data.", file=sys.stderr)
74
+ sys.exit(1)
75
+
76
+ # Now output them to the terminal:
77
+
78
+ dt = datetime.fromtimestamp(round(ts))
79
+ print(f"\nMeasuremets as of {dt.strftime('%Y-%m-%d %H:%M:%S')} (meter time)\n")
80
+
81
+ print(
82
+ " Sensors:\n Rate:\n "
83
+ f"{'Sensor Name':18s}{'Normal (RMS)':>18s}{'Mean':>18s}{'Frequency':>18s}"
84
+ )
85
+
86
+ # First the sensor section:
87
+
88
+ normal = local.row(Local.METRIC_RATE)
89
+ mean = local.row(Local.METRIC_RATE, Local.MEASUREMENT_MEAN)
90
+ freq = local.row(Local.METRIC_RATE, Local.MEASUREMENT_FREQ)
91
+
92
+ if normal is None:
93
+ print("Failed to fetch normal metric.", file=sys.stderr)
94
+ sys.exit(1)
95
+ if mean is None:
96
+ print("Failed to fetch mean metric.", file=sys.stderr)
97
+ sys.exit(1)
98
+ if freq is None:
99
+ print("Failed to fetch frequency metric.", file=sys.stderr)
100
+ sys.exit(1)
101
+
102
+ all_sensors = sorted(set(normal) | set(mean) | set(freq))
103
+ for sensor in all_sensors:
104
+ pq_n = pq_m = pq_f = None
105
+ if sensor in normal:
106
+ pq_n = normal.pq_rate(sensor)
107
+ if sensor in mean:
108
+ pq_m = mean.pq_rate(sensor)
109
+ if sensor in freq:
110
+ pq_f = freq.pq_rate(sensor)
111
+ line = format_measurements(sensor, [pq_n, pq_m, pq_f])
112
+ print(" " + line)
113
+
114
+ print("\n Value:")
115
+
116
+ normal = local.row(Local.METRIC_CUMUL)
117
+ mean = local.row(Local.METRIC_CUMUL, Local.MEASUREMENT_MEAN)
118
+ freq = local.row(Local.METRIC_CUMUL, Local.MEASUREMENT_FREQ)
119
+
120
+ if normal is None:
121
+ print("Failed to fetch normal metric.", file=sys.stderr)
122
+ sys.exit(1)
123
+ if mean is None:
124
+ print("Failed to fetch mean metric.", file=sys.stderr)
125
+ sys.exit(1)
126
+ if freq is None:
127
+ print("Failed to fetch frequency metric.", file=sys.stderr)
128
+ sys.exit(1)
129
+
130
+ all_sensors = sorted(set(normal) | set(mean) | set(freq))
131
+ for sensor in all_sensors:
132
+ pq_n = pq_m = pq_f = None
133
+ if sensor in normal:
134
+ pq_n = normal.pq_accu(sensor)
135
+ if sensor in mean:
136
+ pq_m = mean.pq_accu(sensor)
137
+ if sensor in freq:
138
+ pq_f = freq.pq_accu(sensor)
139
+ line = format_measurements(sensor, [pq_n, pq_m, pq_f])
140
+ print(" " + line)
141
+
142
+ # Second the real energy section:
143
+
144
+ print("\n Energy:")
145
+
146
+ print(" Rate:")
147
+
148
+ row = local.row(Local.METRIC_RATE, section=Local.SECTION_ENERGY)
149
+ if row is None:
150
+ print("Failed to get rate from energy section.", file=sys.stderr)
151
+ sys.exit(1)
152
+
153
+ for sensor in sorted(row):
154
+ pq = row.pq_rate(sensor)
155
+ line = format_measurements(sensor, [pq])
156
+ print(" " + line)
157
+
158
+ print("\n Value:")
159
+
160
+ row = local.row(Local.METRIC_CUMUL, section=Local.SECTION_ENERGY)
161
+ if row is None:
162
+ print(
163
+ "Failed to get cumulative values from energy section.", file=sys.stderr
164
+ )
165
+ sys.exit(1)
166
+
167
+ for sensor in sorted(row):
168
+ pq = row.pq_accu(sensor)
169
+ line = format_measurements(sensor, [pq])
170
+ print(" " + line)
171
+
172
+ # Third the apparent energy section:
173
+
174
+ print("\n Apparent Energy:")
175
+
176
+ print(" Rate:")
177
+
178
+ row = local.row(Local.METRIC_RATE, section=Local.SECTION_APPARENT)
179
+ if row is None:
180
+ print("Failed to get rate from apparent energy section.", file=sys.stderr)
181
+ sys.exit(1)
182
+
183
+ for sensor in sorted(row):
184
+ pq = row.pq_rate(sensor)
185
+ line = format_measurements(sensor, [pq])
186
+ print(" " + line)
187
+
188
+ print("\n Value:")
189
+
190
+ row = local.row(Local.METRIC_CUMUL, section=Local.SECTION_APPARENT)
191
+ if row is None:
192
+ print(
193
+ "Failed to get cumulative values from apparent energy section.",
194
+ file=sys.stderr,
195
+ )
196
+ sys.exit(1)
197
+
198
+ for sensor in sorted(row):
199
+ pq = row.pq_accu(sensor)
200
+ line = format_measurements(sensor, [pq])
201
+ print(" " + line)
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env python3
2
+ #
3
+ # Copyright (c) 2023-2024 eGauge Systems LLC
4
+ #
5
+ # See LICENSE file for details.
6
+ #
7
+ """This test program demonstrates the use of class
8
+ egauge.webapi.device.Register to access the register data available
9
+ through the /register WebAPI.
10
+
11
+ When executed, it first fetches and outputs the current values (rates)
12
+ of each register.
13
+
14
+ Second, it calculates how much the registers have changed since the
15
+ start of the month and outputs the change both as the accrued value
16
+ and the average value since the beginning of the month.
17
+
18
+ You can set environment variables:
19
+
20
+ EGDEV - the URL of the meter to use (e.g., http://eGaugeXXX.local)
21
+ EGUSR - the username to log in to the meter (e.g., "owner")
22
+ EGPWD - the password for the username
23
+
24
+ Alternatively, you can edit examples/test_common.py to suit your needs.
25
+
26
+ """
27
+
28
+ import argparse
29
+ import sys
30
+ from datetime import datetime, timezone
31
+
32
+ from egauge.examples import test_common
33
+ from egauge.webapi.device import PhysicalQuantity, Register, UnitSystem
34
+
35
+ parser = argparse.ArgumentParser(
36
+ description="Demonstrate the use of class egauge.webapi.device.Register."
37
+ )
38
+ parser.add_argument(
39
+ "--imperial",
40
+ action="store_true",
41
+ help="Output units in imperial units rather than metric units.",
42
+ )
43
+ args = parser.parse_args()
44
+
45
+ if args.imperial:
46
+ PhysicalQuantity.set_unit_system(UnitSystem.IMPERIAL)
47
+
48
+ # Fetch the register data. We want the current rates as well as the
49
+ # recorded rows for the current time ("now") and the start of
50
+ # the month ("som").
51
+ #
52
+ # The value for query parameter "rate" doesn't matter. As long as
53
+ # that parameter is present, the rates will be included in the
54
+ # response. We could set it to True or really anything else. We use
55
+ # an empty string since that's the shortest possible value.
56
+ ret = Register(test_common.dev, {"rate": "", "time": "now,som"})
57
+
58
+ ts = ret.ts()
59
+
60
+ if ts is None:
61
+ print("Failed to get register data.", file=sys.stderr)
62
+ sys.exit(1)
63
+
64
+ # Print the current time of the meter in human-readable form:
65
+ dt = datetime.fromtimestamp(round(ts), tz=timezone.utc)
66
+ time_str = dt.strftime("%Y-%m-%d %H:%M:%S %Z")
67
+ print(f"\nRegister rates as of {time_str} (time on the meter):")
68
+
69
+ # Output the register rates, nicely formatted:
70
+ for regname in ret.regs:
71
+ line = f" {regname}"
72
+ if len(line) < 32:
73
+ line += (32 - len(line)) * " "
74
+ rate = ret.pq_rate(regname)
75
+ if rate is None:
76
+ print(f"Failed to get rate for register {regname}.", file=sys.stderr)
77
+ sys.exit(1)
78
+ line += f" {rate.value:12.3f} {rate.unit}"
79
+ print(line)
80
+
81
+ # Calculate the amount by which the registers changed between the two
82
+ # data rows (now and the start of the month):
83
+ delta = ret[0] - ret[1]
84
+
85
+ dt = datetime.fromtimestamp(round(ret[1].ts), tz=timezone.utc)
86
+ time_str = dt.strftime("%Y-%m-%d %H:%M:%S %Z")
87
+ print(f"\nRegister change since {time_str} (start of month on the meter):")
88
+ print(34 * " " + f"{'Accumulated':18s}" + 14 * " " + "Average")
89
+
90
+ # Output the changes since the start of the month:
91
+ for regname in delta.regs:
92
+ line = f" {regname}"
93
+ if len(line) < 32:
94
+ line += (32 - len(line)) * " "
95
+
96
+ accu = delta.pq_accu(regname)
97
+ line += f" {accu.value:12.3f} {accu.unit}"
98
+
99
+ if len(line) < 60:
100
+ line += (60 - len(line)) * " "
101
+
102
+ avg = delta.pq_avg(regname)
103
+ line += f" {avg.value:12.3f} {avg.unit}"
104
+ print(line)
egauge/loggers.py ADDED
@@ -0,0 +1,72 @@
1
+ #
2
+ # Copyright (c) 2025 eGauge Systems LLC
3
+ # 4805 Sterling Dr, Suite 1
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 json
31
+ import logging
32
+ import logging.config
33
+ import sys
34
+ from json import JSONDecodeError
35
+ from pathlib import Path
36
+ from typing import Any
37
+
38
+
39
+ class ModuleLogger:
40
+ logging_config: dict[str, Any] | None = None
41
+
42
+ @classmethod
43
+ def get(cls, module_name: str) -> logging.Logger:
44
+ if cls.logging_config is None:
45
+ config_egauge = Path.home() / ".config/egauge"
46
+ prog_name = Path(sys.argv[0]).stem
47
+
48
+ cfg_path = config_egauge / "logging" / (prog_name + ".json")
49
+ if not cfg_path.exists():
50
+ cfg_path = config_egauge / "logging.json"
51
+ if not cfg_path.exists():
52
+ cfg_path = None
53
+
54
+ if cfg_path:
55
+ with open(cfg_path, encoding="utf-8") as f:
56
+ try:
57
+ cls.logging_config = json.load(f)
58
+ if cls.logging_config:
59
+ logging.config.dictConfig(cls.logging_config)
60
+ except (
61
+ JSONDecodeError,
62
+ ValueError,
63
+ TypeError,
64
+ AttributeError,
65
+ ImportError,
66
+ ) as e:
67
+ log = logging.getLogger(__name__)
68
+ log.error("%s: %s", cfg_path, e)
69
+ # don't try loading it again:
70
+ cls.logging_config = {}
71
+
72
+ return logging.getLogger(module_name)
File without changes
@@ -0,0 +1,112 @@
1
+ #
2
+ # Copyright (c) 2013, 2016-2017, 2020 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
+ # This code is the property of eGauge Systems LLC and may not be
11
+ # copied, modified, or disclosed without any prior and written
12
+ # permission from eGauge Systems LLC.
13
+ #
14
+ # MIT License
15
+ #
16
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
17
+ # of this software and associated documentation files (the "Software"), to deal
18
+ # in the Software without restriction, including without limitation the rights
19
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
20
+ # copies of the Software, and to permit persons to whom the Software is
21
+ # furnished to do so, subject to the following conditions:
22
+ #
23
+ # The above copyright notice and this permission notice shall be included in
24
+ # all copies or substantial portions of the Software.
25
+ #
26
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
27
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
28
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
29
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
30
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
31
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
32
+ # THE SOFTWARE.
33
+ #
34
+ """Convert terminal output with (some) ANSI escape sequences to HTML.
35
+ Also escape any HTML special characters.
36
+
37
+ At this time, most attribute ANSI escape sequences are converted, e.g.:
38
+
39
+ ESC [ 0 m: Reset attributes.
40
+ ESC [ 41 m: Background-color red.
41
+ ESC [ 32 m: Foreground-color red.
42
+ """
43
+
44
+ import html
45
+ import re
46
+
47
+ HTML_STYLE = {
48
+ 1: "font-weight:bold",
49
+ 3: "font-style:italic",
50
+ 4: "text-decoration:underline",
51
+ 9: "text-decoration:line-through",
52
+ 22: "font-weight:normal",
53
+ 23: "font-style:normal",
54
+ 24: "text-decoration:none",
55
+ 29: "text-decoration:none",
56
+ 30: "color:black",
57
+ 31: "color:red",
58
+ 32: "color:green",
59
+ 33: "color:yellow",
60
+ 34: "color:blue",
61
+ 35: "color:magenta",
62
+ 36: "color:cyan",
63
+ 37: "color:white",
64
+ 40: "background-color:black",
65
+ 41: "background-color:red",
66
+ 42: "background-color:green",
67
+ 43: "background-color:yellow",
68
+ 44: "background-color:blue",
69
+ 45: "background-color:magenta",
70
+ 46: "background-color:cyan",
71
+ 47: "background-color:white",
72
+ }
73
+
74
+
75
+ def convert(msg):
76
+ msg = html.escape(msg, quote=False)
77
+ result = ""
78
+
79
+ pattern = re.compile(r"\033\[(\d+)(;(\d+))*m")
80
+ span_count = 0
81
+ while True:
82
+ m = pattern.search(msg)
83
+ if not m:
84
+ result += msg
85
+ break
86
+ result += msg[0 : m.start()]
87
+ code = int(m.group(1))
88
+ if code == 0:
89
+ result += span_count * "</span>"
90
+ span_count = 0
91
+ elif code in HTML_STYLE:
92
+ result += '<span style="%s">' % HTML_STYLE[code]
93
+ span_count += 1
94
+ msg = msg[m.end(0) :]
95
+ return result + span_count * "</span>"
96
+
97
+
98
+ def selftest():
99
+ test = "This is in \033[32mgreen\033[0m foreground."
100
+ print("convert('%s') -> '%s'" % (test, convert(test)))
101
+ test = (
102
+ "This is in \033[41mred\033[0m background. This \033[41mtoo\033[0m."
103
+ )
104
+ print("convert('%s') -> '%s'" % (test, convert(test)))
105
+ test = (
106
+ "\033[1mBold and \033[3mitalic and \033[4munderline\033[0m and none."
107
+ )
108
+ print("convert('%s') -> '%s'" % (test, convert(test)))
109
+
110
+
111
+ if __name__ == "__main__":
112
+ selftest()