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.
- egauge/ctid/__init__.py +7 -0
- egauge/ctid/bit_stuffer.py +65 -0
- egauge/ctid/ctid.py +967 -0
- egauge/ctid/encoder.py +436 -0
- egauge/ctid/intel_hex_encoder.py +98 -0
- egauge/ctid/waveform.py +299 -0
- egauge/examples/data/test-ctid-decoder.raw +0 -0
- egauge/examples/test_capture.py +77 -0
- egauge/examples/test_common.py +26 -0
- egauge/examples/test_ctid.py +89 -0
- egauge/examples/test_ctid_decoder.py +93 -0
- egauge/examples/test_local.py +201 -0
- egauge/examples/test_register.py +104 -0
- egauge/loggers.py +72 -0
- egauge/pyside/__init__.py +0 -0
- egauge/pyside/ansi2html.py +112 -0
- egauge/pyside/terminal.py +295 -0
- egauge/webapi/__init__.py +34 -0
- egauge/webapi/auth.py +364 -0
- egauge/webapi/cloud/__init__.py +30 -0
- egauge/webapi/cloud/credentials.py +86 -0
- egauge/webapi/cloud/credentials_dialog.py +58 -0
- egauge/webapi/cloud/gui/credentials_dialog.py +100 -0
- egauge/webapi/cloud/serial_number.py +276 -0
- egauge/webapi/device/__init__.py +38 -0
- egauge/webapi/device/capture.py +453 -0
- egauge/webapi/device/ctid_info.py +553 -0
- egauge/webapi/device/device.py +349 -0
- egauge/webapi/device/local.py +268 -0
- egauge/webapi/device/physical_quantity.py +439 -0
- egauge/webapi/device/physical_units.py +473 -0
- egauge/webapi/device/register.py +338 -0
- egauge/webapi/device/register_row.py +145 -0
- egauge/webapi/device/register_type.py +851 -0
- egauge/webapi/device/slop.py +334 -0
- egauge/webapi/device/virtual_register.py +353 -0
- egauge/webapi/error.py +34 -0
- egauge/webapi/json_api.py +332 -0
- egauge_python-0.9.8.dist-info/METADATA +148 -0
- egauge_python-0.9.8.dist-info/RECORD +44 -0
- egauge_python-0.9.8.dist-info/WHEEL +5 -0
- egauge_python-0.9.8.dist-info/entry_points.txt +2 -0
- egauge_python-0.9.8.dist-info/licenses/LICENSE +22 -0
- 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()
|