ents 2.3.4__py3-none-any.whl → 2.3.6__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.
- ents/__init__.py +4 -1
- ents/calibrate/recorder.py +4 -4
- ents/cli.py +321 -6
- ents/config/user_config.py +1 -4
- ents/dirtviz/__init__.py +8 -0
- ents/dirtviz/client.py +223 -0
- ents/dirtviz/plots.py +50 -0
- ents/proto/sensor.py +290 -0
- ents/proto/sensor_pb2.py +48 -0
- ents/proto/soil_power_sensor_pb2.py +83 -55
- ents/simulator/node.py +159 -0
- {ents-2.3.4.dist-info → ents-2.3.6.dist-info}/METADATA +58 -104
- ents-2.3.6.dist-info/RECORD +30 -0
- {ents-2.3.4.dist-info → ents-2.3.6.dist-info}/WHEEL +1 -1
- ents/demo/demoPullRequests.py +0 -119
- ents-2.3.4.dist-info/RECORD +0 -26
- {ents-2.3.4.dist-info → ents-2.3.6.dist-info}/entry_points.txt +0 -0
- {ents-2.3.4.dist-info → ents-2.3.6.dist-info}/licenses/LICENSE +0 -0
ents/__init__.py
CHANGED
|
@@ -3,10 +3,11 @@ from .proto.encode import (
|
|
|
3
3
|
encode_power_measurement,
|
|
4
4
|
encode_teros12_measurement,
|
|
5
5
|
encode_phytos31_measurement,
|
|
6
|
+
encode_user_configuration,
|
|
6
7
|
encode_bme280_measurement,
|
|
7
8
|
)
|
|
8
9
|
|
|
9
|
-
from .proto.decode import decode_response, decode_measurement
|
|
10
|
+
from .proto.decode import decode_response, decode_measurement, decode_user_configuration
|
|
10
11
|
|
|
11
12
|
from .proto.esp32 import encode_esp32command, decode_esp32command
|
|
12
13
|
|
|
@@ -20,4 +21,6 @@ __all__ = [
|
|
|
20
21
|
"decode_measurement",
|
|
21
22
|
"encode_esp32command",
|
|
22
23
|
"decode_esp32command",
|
|
24
|
+
"encode_user_configuration",
|
|
25
|
+
"decode_user_configuration",
|
|
23
26
|
]
|
ents/calibrate/recorder.py
CHANGED
|
@@ -16,7 +16,7 @@ import socket
|
|
|
16
16
|
import serial
|
|
17
17
|
from typing import Tuple
|
|
18
18
|
from tqdm import tqdm
|
|
19
|
-
from ..proto import
|
|
19
|
+
from ..proto.sensor import decode_repeated_sensor_measurements
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
class SerialController:
|
|
@@ -116,10 +116,10 @@ class SoilPowerSensorController(SerialController):
|
|
|
116
116
|
|
|
117
117
|
reply = self.ser.read(resp_len) # read said measurment
|
|
118
118
|
|
|
119
|
-
meas_dict =
|
|
119
|
+
meas_dict = decode_repeated_sensor_measurements(reply) # decode using protobuf
|
|
120
120
|
|
|
121
|
-
voltage_value = meas_dict["
|
|
122
|
-
current_value = meas_dict["
|
|
121
|
+
voltage_value = meas_dict["measurements"][0]["decimal"]
|
|
122
|
+
current_value = meas_dict["measurements"][1]["decimal"]
|
|
123
123
|
|
|
124
124
|
return float(voltage_value), float(current_value)
|
|
125
125
|
|
ents/cli.py
CHANGED
|
@@ -29,7 +29,14 @@ from .proto.encode import (
|
|
|
29
29
|
from .proto.decode import decode_measurement, decode_response
|
|
30
30
|
from .proto.esp32 import encode_esp32command, decode_esp32command
|
|
31
31
|
|
|
32
|
-
from .
|
|
32
|
+
from .proto.sensor import (
|
|
33
|
+
encode_repeated_sensor_measurements,
|
|
34
|
+
decode_repeated_sensor_measurements,
|
|
35
|
+
encode_sensor_response,
|
|
36
|
+
decode_sensor_response,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
from .simulator.node import NodeSimulator, NodeSimulatorGeneric
|
|
33
40
|
|
|
34
41
|
|
|
35
42
|
def entry():
|
|
@@ -37,17 +44,81 @@ def entry():
|
|
|
37
44
|
|
|
38
45
|
parser = argparse.ArgumentParser()
|
|
39
46
|
|
|
40
|
-
subparsers = parser.add_subparsers(help="Ents Utilities")
|
|
47
|
+
subparsers = parser.add_subparsers(help="Ents Utilities", required=True)
|
|
41
48
|
|
|
42
49
|
create_encode_parser(subparsers)
|
|
50
|
+
create_encode_generic_parser(subparsers)
|
|
43
51
|
create_decode_parser(subparsers)
|
|
52
|
+
create_decode_generic_parser(subparsers)
|
|
44
53
|
create_calib_parser(subparsers)
|
|
45
54
|
create_sim_parser(subparsers)
|
|
55
|
+
create_sim_generic_parser(subparsers)
|
|
46
56
|
|
|
47
57
|
args = parser.parse_args()
|
|
48
58
|
args.func(args)
|
|
49
59
|
|
|
50
60
|
|
|
61
|
+
def create_sim_generic_parser(subparsers):
|
|
62
|
+
"""Creates the generic simulation subparser
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
subparsers: Reference to subparser group
|
|
66
|
+
Returns:
|
|
67
|
+
Reference to new subparser
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
sim_p = subparsers.add_parser("sim_generic", help="Simluate generic sensor uploads")
|
|
71
|
+
|
|
72
|
+
sim_p.add_argument(
|
|
73
|
+
"-v",
|
|
74
|
+
"--verbose",
|
|
75
|
+
action="store_true",
|
|
76
|
+
help="Print addiitional request information.",
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
sim_p.add_argument(
|
|
80
|
+
"--url",
|
|
81
|
+
default="http://localhost:8000/api/sensor",
|
|
82
|
+
type=str,
|
|
83
|
+
help="URL of the dirtviz instance (default: http://localhost:8000)",
|
|
84
|
+
)
|
|
85
|
+
sim_p.add_argument(
|
|
86
|
+
"mode",
|
|
87
|
+
choices=["batch", "stream"],
|
|
88
|
+
type=str,
|
|
89
|
+
help="Upload mode",
|
|
90
|
+
)
|
|
91
|
+
sim_p.add_argument(
|
|
92
|
+
"--sensor",
|
|
93
|
+
required=True,
|
|
94
|
+
type=str,
|
|
95
|
+
nargs="+",
|
|
96
|
+
help="Type of sensor to simulate",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
sim_p.add_argument(
|
|
100
|
+
"--min", type=float, default=-1.0, help="Minimum sensor value (default: -1.0)"
|
|
101
|
+
)
|
|
102
|
+
sim_p.add_argument(
|
|
103
|
+
"--max", type=float, default=1.0, help="Maximum sensor value (default: 1.0)"
|
|
104
|
+
)
|
|
105
|
+
sim_p.add_argument("--cell", required=True, type=int, help="Cell Id")
|
|
106
|
+
sim_p.add_argument("--logger", required=True, type=int, help="Logger Id")
|
|
107
|
+
|
|
108
|
+
batch = sim_p.add_argument_group("Batch")
|
|
109
|
+
batch.add_argument("--start", type=str, help="Start date")
|
|
110
|
+
batch.add_argument("--end", type=str, help="End date")
|
|
111
|
+
|
|
112
|
+
stream = sim_p.add_argument_group("Stream")
|
|
113
|
+
stream.add_argument(
|
|
114
|
+
"--freq", default=10.0, type=float, help="Frequency of uploads (default: 10s)"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
sim_p.set_defaults(func=simulate_generic)
|
|
118
|
+
|
|
119
|
+
return sim_p
|
|
120
|
+
|
|
121
|
+
|
|
51
122
|
def create_sim_parser(subparsers):
|
|
52
123
|
"""Creates the simulation subparser
|
|
53
124
|
|
|
@@ -90,6 +161,63 @@ def create_sim_parser(subparsers):
|
|
|
90
161
|
return sim_p
|
|
91
162
|
|
|
92
163
|
|
|
164
|
+
def simulate_generic(args):
|
|
165
|
+
if args.verbose:
|
|
166
|
+
print("Arguments:")
|
|
167
|
+
print(args)
|
|
168
|
+
print()
|
|
169
|
+
|
|
170
|
+
simulation = NodeSimulatorGeneric(
|
|
171
|
+
cell=args.cell,
|
|
172
|
+
logger=args.logger,
|
|
173
|
+
sensors=args.sensor,
|
|
174
|
+
_min=args.min,
|
|
175
|
+
_max=args.max,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
if args.mode == "batch":
|
|
179
|
+
if (args.start is None) or (args.end is None):
|
|
180
|
+
raise ValueError("Start and end date must be provided for batch mode.")
|
|
181
|
+
|
|
182
|
+
# format dates
|
|
183
|
+
curr_dt = datetime.fromisoformat(args.start)
|
|
184
|
+
end_dt = datetime.fromisoformat(args.end)
|
|
185
|
+
|
|
186
|
+
# create list of measurements
|
|
187
|
+
while curr_dt <= end_dt:
|
|
188
|
+
ts = int(curr_dt.timestamp())
|
|
189
|
+
simulation.measure(ts)
|
|
190
|
+
curr_dt += timedelta(seconds=args.freq)
|
|
191
|
+
|
|
192
|
+
# send measurements
|
|
193
|
+
while simulation.send_next(args.url):
|
|
194
|
+
print(simulation)
|
|
195
|
+
|
|
196
|
+
print("Done!")
|
|
197
|
+
|
|
198
|
+
elif args.mode == "stream":
|
|
199
|
+
print("Use CTRL+C to stop the simulation")
|
|
200
|
+
try:
|
|
201
|
+
while True:
|
|
202
|
+
dt = datetime.now()
|
|
203
|
+
ts = int(dt.timestamp())
|
|
204
|
+
simulation.measure(ts)
|
|
205
|
+
|
|
206
|
+
while simulation.send_next(args.url):
|
|
207
|
+
print(simulation)
|
|
208
|
+
|
|
209
|
+
if args.verbose:
|
|
210
|
+
print("Request")
|
|
211
|
+
print(simulation.last_request())
|
|
212
|
+
print("\nResponse")
|
|
213
|
+
print(simulation.last_response())
|
|
214
|
+
print()
|
|
215
|
+
|
|
216
|
+
time.sleep(args.freq)
|
|
217
|
+
except KeyboardInterrupt as _:
|
|
218
|
+
print("Stopping simulation")
|
|
219
|
+
|
|
220
|
+
|
|
93
221
|
def simulate(args):
|
|
94
222
|
simulation = NodeSimulator(
|
|
95
223
|
cell=args.cell,
|
|
@@ -169,6 +297,133 @@ def create_calib_parser(subparsers):
|
|
|
169
297
|
return calib_p
|
|
170
298
|
|
|
171
299
|
|
|
300
|
+
def create_encode_generic_parser(subparsers):
|
|
301
|
+
"""Create generic encode command subparser
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
subparsers: Reference to subparser group
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
Reference to new subparser
|
|
308
|
+
"""
|
|
309
|
+
|
|
310
|
+
encode_parser = subparsers.add_parser("encode_generic", help="Encode generic data")
|
|
311
|
+
|
|
312
|
+
print_format = encode_parser.add_mutually_exclusive_group()
|
|
313
|
+
print_format.add_argument(
|
|
314
|
+
"--hex", action="store_true", help="Print as hex values (default)"
|
|
315
|
+
)
|
|
316
|
+
print_format.add_argument(
|
|
317
|
+
"--raw", action="store_true", help="Print raw bytes object"
|
|
318
|
+
)
|
|
319
|
+
print_format.add_argument(
|
|
320
|
+
"--c", action="store_true", help="Print bytes for copying to c"
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
encode_subparsers = encode_parser.add_subparsers(
|
|
324
|
+
title="Message type", dest="type", required=True
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
# sensor measurement
|
|
328
|
+
measurement_parser = encode_subparsers.add_parser(
|
|
329
|
+
"meas", help='Proto "Measurement" message'
|
|
330
|
+
)
|
|
331
|
+
measurement_parser.add_argument("--ts", type=int, help="Unix epoch timestamp")
|
|
332
|
+
measurement_parser.add_argument("--cell", type=int, help="Cell Id")
|
|
333
|
+
measurement_parser.add_argument("--logger", type=int, help="Logger Id")
|
|
334
|
+
measurement_parser.add_argument("--idx", type=int, default=1, help="Upload index")
|
|
335
|
+
measurement_parser.add_argument(
|
|
336
|
+
"--sensor",
|
|
337
|
+
nargs=2,
|
|
338
|
+
metavar=("type", "value"),
|
|
339
|
+
action="append",
|
|
340
|
+
required=True,
|
|
341
|
+
help="Specify as: --sensor <type> <value>",
|
|
342
|
+
)
|
|
343
|
+
measurement_parser.set_defaults(func=handle_encode_generic_measurement)
|
|
344
|
+
|
|
345
|
+
# response
|
|
346
|
+
response_parser = encode_subparsers.add_parser(
|
|
347
|
+
"resp", help='Proto "Response" message'
|
|
348
|
+
)
|
|
349
|
+
response_parser.add_argument(
|
|
350
|
+
"--resp",
|
|
351
|
+
nargs=2,
|
|
352
|
+
metavar=("idx", "status"),
|
|
353
|
+
action="append",
|
|
354
|
+
required=True,
|
|
355
|
+
help="Specify as: --resp <idx> <status>",
|
|
356
|
+
)
|
|
357
|
+
response_parser.set_defaults(func=handle_encode_generic_response)
|
|
358
|
+
|
|
359
|
+
return encode_parser
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def parse_number(s: str) -> tuple:
|
|
363
|
+
try:
|
|
364
|
+
i = int(s)
|
|
365
|
+
if i >= 0:
|
|
366
|
+
return i, "unsignedInt"
|
|
367
|
+
return i, "signedInt"
|
|
368
|
+
except ValueError:
|
|
369
|
+
pass
|
|
370
|
+
|
|
371
|
+
try:
|
|
372
|
+
return float(s), "decimal"
|
|
373
|
+
except ValueError:
|
|
374
|
+
pass
|
|
375
|
+
|
|
376
|
+
raise ValueError(f"Invalid numeric value: {s}")
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def handle_encode_generic_measurement(args):
|
|
380
|
+
"""Take arguments and encode a repeated senosr measurement message"""
|
|
381
|
+
|
|
382
|
+
meas = {
|
|
383
|
+
"meta": {
|
|
384
|
+
"ts": args.ts,
|
|
385
|
+
"cellId": args.cell,
|
|
386
|
+
"loggerId": args.logger,
|
|
387
|
+
},
|
|
388
|
+
"measurements": [],
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
for s in args.sensor:
|
|
392
|
+
val, val_type = parse_number(s[1])
|
|
393
|
+
meas["measurements"].append(
|
|
394
|
+
{
|
|
395
|
+
"type": s[0],
|
|
396
|
+
val_type: val,
|
|
397
|
+
"idx": args.idx,
|
|
398
|
+
}
|
|
399
|
+
)
|
|
400
|
+
args.idx += 1
|
|
401
|
+
|
|
402
|
+
data = encode_repeated_sensor_measurements(meas)
|
|
403
|
+
print_data(args, data)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def handle_encode_generic_response(args):
|
|
407
|
+
"""Takes arguments and encode a sensor response message"""
|
|
408
|
+
|
|
409
|
+
resp = {
|
|
410
|
+
"responses": [],
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
for r in args.resp:
|
|
414
|
+
idx = int(r[0])
|
|
415
|
+
status = r[1]
|
|
416
|
+
resp["responses"].append(
|
|
417
|
+
{
|
|
418
|
+
"uploadIndex": idx,
|
|
419
|
+
"status": status,
|
|
420
|
+
}
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
data = encode_sensor_response(resp)
|
|
424
|
+
print_data(args, data)
|
|
425
|
+
|
|
426
|
+
|
|
172
427
|
def create_encode_parser(subparsers):
|
|
173
428
|
"""Create encode command subparser
|
|
174
429
|
|
|
@@ -192,14 +447,19 @@ def create_encode_parser(subparsers):
|
|
|
192
447
|
"--c", action="store_true", help="Print bytes for copying to c"
|
|
193
448
|
)
|
|
194
449
|
|
|
195
|
-
encode_subparsers = encode_parser.add_subparsers(
|
|
450
|
+
encode_subparsers = encode_parser.add_subparsers(
|
|
451
|
+
title="Message type",
|
|
452
|
+
dest="type",
|
|
453
|
+
required=True,
|
|
454
|
+
)
|
|
196
455
|
|
|
197
456
|
def create_measurement_parser(encode_subparsers):
|
|
198
457
|
measurement_parser = encode_subparsers.add_parser(
|
|
199
458
|
"measurement", help='Proto "Measurement" message'
|
|
200
459
|
)
|
|
201
460
|
measurement_subparser = measurement_parser.add_subparsers(
|
|
202
|
-
title="Measurement type"
|
|
461
|
+
title="Measurement type",
|
|
462
|
+
required=True,
|
|
203
463
|
)
|
|
204
464
|
|
|
205
465
|
# metadata
|
|
@@ -246,7 +506,9 @@ def create_encode_parser(subparsers):
|
|
|
246
506
|
"esp32command", help='Proto "Esp32Command" message'
|
|
247
507
|
)
|
|
248
508
|
esp32command_subparser = esp32command_parser.add_subparsers(
|
|
249
|
-
title="type",
|
|
509
|
+
title="type",
|
|
510
|
+
help="PageCommand",
|
|
511
|
+
required=True,
|
|
250
512
|
)
|
|
251
513
|
|
|
252
514
|
test_parser = esp32command_subparser.add_parser("test", help="TestCommand")
|
|
@@ -383,6 +645,55 @@ def print_bytes_c(data: bytes) -> str:
|
|
|
383
645
|
print(f"size_t data_len = {len(hex_str)};")
|
|
384
646
|
|
|
385
647
|
|
|
648
|
+
def create_decode_generic_parser(subparsers):
|
|
649
|
+
"""Create generic decode command parser
|
|
650
|
+
|
|
651
|
+
Args:
|
|
652
|
+
subparsers: Reference to subparser group
|
|
653
|
+
|
|
654
|
+
Returns:
|
|
655
|
+
Reference to new subparser
|
|
656
|
+
"""
|
|
657
|
+
|
|
658
|
+
decode_parser = subparsers.add_parser("decode_generic", help="Decode generic data")
|
|
659
|
+
|
|
660
|
+
decode_subparsers = decode_parser.add_subparsers(
|
|
661
|
+
title="Message type",
|
|
662
|
+
dest="type",
|
|
663
|
+
required=True,
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
decode_parser.add_argument(
|
|
667
|
+
"data", type=str, help="Protobuf serialized data in hex format"
|
|
668
|
+
)
|
|
669
|
+
|
|
670
|
+
# sensor measurement
|
|
671
|
+
measurement_parser = decode_subparsers.add_parser(
|
|
672
|
+
"meas", help='Proto "Measurement" message'
|
|
673
|
+
)
|
|
674
|
+
measurement_parser.set_defaults(func=handle_decode_generic_measurement)
|
|
675
|
+
|
|
676
|
+
# response
|
|
677
|
+
response_parser = decode_subparsers.add_parser(
|
|
678
|
+
"resp", help='Proto "Response" message'
|
|
679
|
+
)
|
|
680
|
+
response_parser.set_defaults(func=handle_decode_generic_response)
|
|
681
|
+
|
|
682
|
+
return decode_parser
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
def handle_decode_generic_measurement(args):
|
|
686
|
+
data = bytes.fromhex(args.data)
|
|
687
|
+
vals = decode_repeated_sensor_measurements(data)
|
|
688
|
+
print(vals)
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
def handle_decode_generic_response(args):
|
|
692
|
+
data = bytes.fromhex(args.data)
|
|
693
|
+
vals = decode_sensor_response(data)
|
|
694
|
+
print(vals)
|
|
695
|
+
|
|
696
|
+
|
|
386
697
|
def create_decode_parser(subparsers):
|
|
387
698
|
"""Create decode command parser
|
|
388
699
|
|
|
@@ -395,7 +706,11 @@ def create_decode_parser(subparsers):
|
|
|
395
706
|
|
|
396
707
|
decode_parser = subparsers.add_parser("decode", help="Decode data")
|
|
397
708
|
|
|
398
|
-
decode_subparsers = decode_parser.add_subparsers(
|
|
709
|
+
decode_subparsers = decode_parser.add_subparsers(
|
|
710
|
+
title="Message type",
|
|
711
|
+
dest="type",
|
|
712
|
+
required=True,
|
|
713
|
+
)
|
|
399
714
|
|
|
400
715
|
# measurement
|
|
401
716
|
measurement_parser = decode_subparsers.add_parser(
|
ents/config/user_config.py
CHANGED
|
@@ -24,10 +24,7 @@ import sys
|
|
|
24
24
|
import serial
|
|
25
25
|
import serial.tools.list_ports
|
|
26
26
|
import re # For validating URL input
|
|
27
|
-
from
|
|
28
|
-
encode_user_configuration,
|
|
29
|
-
decode_user_configuration,
|
|
30
|
-
)
|
|
27
|
+
from ..proto import encode_user_configuration, decode_user_configuration
|
|
31
28
|
|
|
32
29
|
|
|
33
30
|
class Ui_MainWindow(object):
|
ents/dirtviz/__init__.py
ADDED
ents/dirtviz/client.py
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""Client interface with dirtviz.
|
|
2
|
+
|
|
3
|
+
TODO:
|
|
4
|
+
- Add caching of data
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
|
|
9
|
+
import pandas as pd
|
|
10
|
+
|
|
11
|
+
import requests
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Cell:
|
|
15
|
+
"""Class representing a cell in the Dirtviz API."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, data: str):
|
|
18
|
+
"""Initialize the Cell object from a cell ID.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
data: json data from the Dirtviz API containing cell information.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
self.id = data["id"]
|
|
25
|
+
self.name = data["name"]
|
|
26
|
+
self.location = data["location"]
|
|
27
|
+
self.latitude = data["latitude"]
|
|
28
|
+
self.longitude = data["longitude"]
|
|
29
|
+
|
|
30
|
+
def __repr__(self):
|
|
31
|
+
return f"Cell(id={self.cell_id}, name={self.name})"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class BackendClient:
|
|
35
|
+
"""Client for interacting with the Dirtviz API."""
|
|
36
|
+
|
|
37
|
+
def __init__(self, base_url: str = "https://dirtviz.jlab.ucsc.edu/api/"):
|
|
38
|
+
"""Initialize the BackendClient.
|
|
39
|
+
|
|
40
|
+
Sets the base URL for the API. Defaults to the Dirtviz API.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
self.base_url = base_url
|
|
44
|
+
|
|
45
|
+
def get(self, endpoint: str, params: dict = None) -> dict:
|
|
46
|
+
"""Get request to the API.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
endpoint: The API endpoint to request.
|
|
50
|
+
params: Optional parameters for the request.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
A dictionary containing the response data.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
url = f"{self.base_url}{endpoint}"
|
|
57
|
+
response = requests.get(url, params=params)
|
|
58
|
+
response.raise_for_status()
|
|
59
|
+
|
|
60
|
+
return response.json()
|
|
61
|
+
|
|
62
|
+
@staticmethod
|
|
63
|
+
def time_to_params(start: datetime, end: datetime) -> dict:
|
|
64
|
+
"""Puts start and end datetime into an API paramter dictionary
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
dt: The datetime object to format.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
A string representing the formatted datetime.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
timestamp_format = "%a, %d %b %Y %H:%M:%S GMT"
|
|
74
|
+
|
|
75
|
+
start_str = start.strftime(timestamp_format)
|
|
76
|
+
end_str = end.strftime(timestamp_format)
|
|
77
|
+
|
|
78
|
+
params = {
|
|
79
|
+
"startTime": start_str,
|
|
80
|
+
"endTime": end_str,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return params
|
|
84
|
+
|
|
85
|
+
def power_data(self, cell: Cell, start: datetime, end: datetime) -> pd.DataFrame:
|
|
86
|
+
"""Gets power data for a specific cell by name.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
cell: The Cell object for which to get power data.
|
|
90
|
+
start: The start date of the data.
|
|
91
|
+
end: The end date of the data.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
A pandas DataFrame containing the power data.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
endpoint = f"/power/{cell.id}"
|
|
98
|
+
|
|
99
|
+
params = self.time_to_params(start, end)
|
|
100
|
+
|
|
101
|
+
data = self.get(endpoint, params=params)
|
|
102
|
+
|
|
103
|
+
data_df = pd.DataFrame(data)
|
|
104
|
+
data_df["timestamp"] = pd.to_datetime(data_df["timestamp"])
|
|
105
|
+
|
|
106
|
+
return data_df
|
|
107
|
+
|
|
108
|
+
def teros_data(self, cell: Cell, start: datetime, end: datetime) -> pd.DataFrame:
|
|
109
|
+
"""Gets teros data for a specific cell
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
cell: The Cell object for which to get teros data.
|
|
113
|
+
start: The start date of the data.
|
|
114
|
+
end: The end date of the data.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
A pandas DataFrame containing the teros data with columns vwc_raw,
|
|
118
|
+
vwc_adj, temp, ec.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
endpoint = f"/teros/{cell.id}"
|
|
122
|
+
|
|
123
|
+
params = self.time_to_params(start, end)
|
|
124
|
+
|
|
125
|
+
data = self.get(endpoint, params=params)
|
|
126
|
+
|
|
127
|
+
data_df = pd.DataFrame(data)
|
|
128
|
+
data_df["timestamp"] = pd.to_datetime(data_df["timestamp"])
|
|
129
|
+
|
|
130
|
+
return data_df
|
|
131
|
+
|
|
132
|
+
def sensor_data(
|
|
133
|
+
self,
|
|
134
|
+
cell: Cell,
|
|
135
|
+
name: str,
|
|
136
|
+
meas: str,
|
|
137
|
+
start: datetime,
|
|
138
|
+
end: datetime,
|
|
139
|
+
resample: str = "none",
|
|
140
|
+
) -> pd.DataFrame:
|
|
141
|
+
"""Gets generic sensor data for a specific cell
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
cell: The Cell object for which to get sensor data.
|
|
145
|
+
name: Name of the sensor (e.g., "power", "teros").
|
|
146
|
+
meas: The measurement type (e.g., "v", "i", "vwc", "temp", "ec").
|
|
147
|
+
start: The start date of the data.
|
|
148
|
+
end: The end date of the data.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
A pandas DataFrame containing the sensor data.
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
endpoint = "/sensor/"
|
|
155
|
+
|
|
156
|
+
params = {
|
|
157
|
+
"cellId": cell.id,
|
|
158
|
+
"name": name,
|
|
159
|
+
"measurement": meas,
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
params = params | self.time_to_params(start, end)
|
|
163
|
+
|
|
164
|
+
data = self.get(endpoint, params=params)
|
|
165
|
+
|
|
166
|
+
data_df = pd.DataFrame(data)
|
|
167
|
+
data_df["timestamp"] = pd.to_datetime(data_df["timestamp"])
|
|
168
|
+
|
|
169
|
+
return data_df
|
|
170
|
+
|
|
171
|
+
def cell_from_id(self, cell_id: int) -> Cell | None:
|
|
172
|
+
"""Get a Cell object from its ID.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
cell_id: The ID of the cell.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
A Cell object. None if the cell does not exist.
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
cell_list = self.cells()
|
|
182
|
+
|
|
183
|
+
for cell in cell_list:
|
|
184
|
+
if cell.id == cell_id:
|
|
185
|
+
return cell
|
|
186
|
+
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
def cell_from_name(self, name: str) -> Cell | None:
|
|
190
|
+
"""Get a Cell object from its name.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
name: The name of the cell.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
A Cell object. None if the cell does not exist.
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
cell_list = self.cells()
|
|
200
|
+
|
|
201
|
+
for cell in cell_list:
|
|
202
|
+
if cell.name == name:
|
|
203
|
+
return cell
|
|
204
|
+
|
|
205
|
+
return None
|
|
206
|
+
|
|
207
|
+
def cells(self) -> list[Cell]:
|
|
208
|
+
"""Gets a list of all cells from the API.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
A list of Cell objects.
|
|
212
|
+
"""
|
|
213
|
+
|
|
214
|
+
cell_list = []
|
|
215
|
+
|
|
216
|
+
endpoint = "/cell/id"
|
|
217
|
+
cell_data_list = self.get(endpoint)
|
|
218
|
+
|
|
219
|
+
for c in cell_data_list:
|
|
220
|
+
cell = Cell(c)
|
|
221
|
+
cell_list.append(cell)
|
|
222
|
+
|
|
223
|
+
return cell_list
|