ents 2.3.5__tar.gz → 2.3.6__tar.gz

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 (34) hide show
  1. {ents-2.3.5 → ents-2.3.6}/PKG-INFO +58 -104
  2. {ents-2.3.5 → ents-2.3.6}/README.md +56 -102
  3. {ents-2.3.5 → ents-2.3.6}/pyproject.toml +2 -2
  4. {ents-2.3.5 → ents-2.3.6}/src/ents/calibrate/recorder.py +4 -4
  5. {ents-2.3.5 → ents-2.3.6}/src/ents/cli.py +35 -8
  6. ents-2.3.6/src/ents/dirtviz/__init__.py +8 -0
  7. ents-2.3.6/src/ents/dirtviz/client.py +223 -0
  8. ents-2.3.6/src/ents/dirtviz/plots.py +50 -0
  9. {ents-2.3.5 → ents-2.3.6}/src/ents/simulator/node.py +40 -2
  10. {ents-2.3.5 → ents-2.3.6}/.gitignore +0 -0
  11. {ents-2.3.5 → ents-2.3.6}/LICENSE +0 -0
  12. {ents-2.3.5 → ents-2.3.6}/src/ents/__init__.py +0 -0
  13. {ents-2.3.5 → ents-2.3.6}/src/ents/calibrate/PingSMU.py +0 -0
  14. {ents-2.3.5 → ents-2.3.6}/src/ents/calibrate/PingSPS.py +0 -0
  15. {ents-2.3.5 → ents-2.3.6}/src/ents/calibrate/README.md +0 -0
  16. {ents-2.3.5 → ents-2.3.6}/src/ents/calibrate/__init__.py +0 -0
  17. {ents-2.3.5 → ents-2.3.6}/src/ents/calibrate/linear_regression.py +0 -0
  18. {ents-2.3.5 → ents-2.3.6}/src/ents/calibrate/plots.py +0 -0
  19. {ents-2.3.5 → ents-2.3.6}/src/ents/config/README.md +0 -0
  20. {ents-2.3.5 → ents-2.3.6}/src/ents/config/__init__.py +0 -0
  21. {ents-2.3.5 → ents-2.3.6}/src/ents/config/adv_trace.py +0 -0
  22. {ents-2.3.5 → ents-2.3.6}/src/ents/config/user_config.py +0 -0
  23. {ents-2.3.5 → ents-2.3.6}/src/ents/proto/__init__.py +0 -0
  24. {ents-2.3.5 → ents-2.3.6}/src/ents/proto/decode.py +0 -0
  25. {ents-2.3.5 → ents-2.3.6}/src/ents/proto/encode.py +0 -0
  26. {ents-2.3.5 → ents-2.3.6}/src/ents/proto/esp32.py +0 -0
  27. {ents-2.3.5 → ents-2.3.6}/src/ents/proto/sensor.py +0 -0
  28. {ents-2.3.5 → ents-2.3.6}/src/ents/proto/sensor_pb2.py +0 -0
  29. {ents-2.3.5 → ents-2.3.6}/src/ents/proto/soil_power_sensor_pb2.py +0 -0
  30. {ents-2.3.5 → ents-2.3.6}/src/ents/simulator/__init__.py +0 -0
  31. {ents-2.3.5 → ents-2.3.6}/tests/__init__.py +0 -0
  32. {ents-2.3.5 → ents-2.3.6}/tests/test_generic.py +0 -0
  33. {ents-2.3.5 → ents-2.3.6}/tests/test_sensor.py +0 -0
  34. {ents-2.3.5 → ents-2.3.6}/tests/test_simulator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ents
3
- Version: 2.3.5
3
+ Version: 2.3.6
4
4
  Summary: Python package for Environmental NeTworked Sensor (ENTS)
5
5
  Project-URL: Homepage, https://github.com/jlab-sensing/soil-power-sensor-firmware
6
6
  Project-URL: Issues, https://github.com/jlab-sensing/soil-power-sensor-firmware/issues
@@ -12,7 +12,7 @@ Classifier: Programming Language :: Python :: 3
12
12
  Requires-Python: >=3.11
13
13
  Requires-Dist: matplotlib
14
14
  Requires-Dist: pandas
15
- Requires-Dist: protobuf==6.33.2
15
+ Requires-Dist: protobuf==6.33.4
16
16
  Requires-Dist: pyserial
17
17
  Requires-Dist: requests
18
18
  Requires-Dist: scikit-learn
@@ -34,14 +34,14 @@ The soil power sensor protobuf protocol is implemented as a Python package that
34
34
  Use the following to install the `ents` package with gui via `pip`:
35
35
 
36
36
  ```bash
37
- pip install ents[gui]
37
+ pip install ents
38
38
  ```
39
39
 
40
40
  You can also install the package from source with the following:
41
41
 
42
42
  ```bash
43
43
  # install package
44
- pip install .[gui]
44
+ pip install .
45
45
  ```
46
46
 
47
47
  If you are planning to develop the package we recommend you install the package
@@ -51,114 +51,68 @@ reinstall it.
51
51
 
52
52
  ```bash
53
53
  # install development dependencies
54
- pip install -e .[gui,dev]
54
+ pip install -e .[dev]
55
55
  ```
56
56
 
57
- ## Usage
57
+ To install the *deprecated* user config gui, use the following: =
58
+ ```bash
59
+ pip install -e ents[gui]
60
+ ```
58
61
 
59
- The following example code demonstrates decoding the measurement message and encoding a response.
60
62
 
61
- ```python
62
- from ents import encode, decode
63
63
 
64
- # get data encoded by the soil power sensor
65
- data = ...
64
+ ## Simulator (New)
66
65
 
67
- meas_dict = decode(data)
66
+ The webserver `tools/http_decoder.py` can be used to decode uploaded measurements.
67
+
68
+ ### CLI Usage
69
+
70
+ ```
71
+ usage: ents sim_generic [-h] [-v] [--url URL] --sensor SENSOR [SENSOR ...] [--min MIN] [--max MAX] --cell CELL --logger LOGGER [--start START] [--end END] [--freq FREQ] {batch,stream}
72
+
73
+ positional arguments:
74
+ {batch,stream} Upload mode
75
+
76
+ options:
77
+ -h, --help show this help message and exit
78
+ -v, --verbose Print addiitional request information.
79
+ --url URL URL of the dirtviz instance (default: http://localhost:8000)
80
+ --sensor SENSOR [SENSOR ...]
81
+ Type of sensor to simulate
82
+ --min MIN Minimum sensor value (default: -1.0)
83
+ --max MAX Maximum sensor value (default: 1.0)
84
+ --cell CELL Cell Id
85
+ --logger LOGGER Logger Id
86
+
87
+ Batch:
88
+ --start START Start date
89
+ --end END End date
90
+
91
+ Stream:
92
+ --freq FREQ Frequency of uploads (default: 10s)
93
+ ```
94
+
95
+ ### Examples
96
+
97
+ You can find the available sensors in the `sensors.proto` file.
98
+
99
+ Example uploading single measurement
100
+ ```
101
+ ents sim_generic stream --sensor POWER_VOLTAGE --min 20 --max 30 --cell 1 --logger 1
102
+ ```
103
+
104
+ Example uploading multiple measuremnets
105
+ ```
106
+ ents sim_generic stream --sensor TEROS12_VWC_ADJ TEROS12_TEMP TEROS12_EC --min 10 --max 100 --cell 1 --logger 1
107
+ ```
108
+
109
+ Example batch uploads
110
+ ```
111
+ ents sim_generic batch --sensor POWER_CURRENT --cell 1 --logger 1 --start 2026-01-19 --end 2026-01-20 --freq 60
112
+ ```
68
113
 
69
- # process data
70
- ...
71
114
 
72
- # send response
73
- resp_str = encode(success=True)
74
- ```
75
-
76
- The formatting of the dictionary depends on the type of measurement sent. The key `type` is included on all measurement types and can be used to determine the type of message. See the source `*.proto` files to get the full list of types to get the full list of types and keys. A list is provided in [Message Types](#message-types). The Python protobuf API uses camel case when naming keys. The key `ts` is in ISO 8601 format as a string.
77
-
78
- ## Message Types
79
-
80
- Type `power`
81
- ```python
82
- meas_dict = {
83
- "type": "power",
84
- "loggerId": ...,
85
- "cellId": ...,
86
- "ts": ...,
87
- "data": {
88
- "voltage": ...,
89
- "current": ...
90
- },
91
- "data_type": {
92
- "voltage": float,
93
- "voltage": float
94
- }
95
- }
96
- ```
97
-
98
- Type `teros12`
99
- ```python
100
- meas_dict = {
101
- "type": "teros12",
102
- "loggerId": ...,
103
- "cellId": ...,
104
- "ts": ...,
105
- "data": {
106
- "vwcRaw": ...,
107
- "vwcAdj": ...,
108
- "temp": ...,
109
- "ec": ...
110
- },
111
- "data_type": {
112
- "vwcRaw": float,
113
- "vwcAdj": float,
114
- "temp": float,
115
- "ec": int
116
- }
117
- }
118
- ```
119
-
120
- Type `bme280` with `raw=True` (default)
121
- ```python
122
- meas_dict = {
123
- "type": "bme280",
124
- "loggerId": ...,
125
- "cellId": ...,
126
- "ts": ...,
127
- "data": {
128
- "pressure": ...,
129
- "temperature": ...,
130
- "humidity": ...,
131
- },
132
- "data_type": {
133
- "pressure": int,
134
- "temperature": int,
135
- "humidity": int,
136
- }
137
- }
138
- ```
139
-
140
- Type `bme280` with `raw=False`
141
- ```python
142
- meas_dict = {
143
- "type": "bme280",
144
- "loggerId": ...,
145
- "cellId": ...,
146
- "ts": ...,
147
- "data": {
148
- "pressure": ...,
149
- "temperature": ...,
150
- "humidity": ...,
151
- },
152
- "data_type": {
153
- "pressure": float,
154
- "temperature": float,
155
- "humidity": float,
156
- }
157
- }
158
- ```
159
-
160
-
161
- ## Simulator
115
+ ## Simulator (Old)
162
116
 
163
117
  Simulate WiFi sensor uploads without requiring ENTS hardware.
164
118
 
@@ -8,14 +8,14 @@ The soil power sensor protobuf protocol is implemented as a Python package that
8
8
  Use the following to install the `ents` package with gui via `pip`:
9
9
 
10
10
  ```bash
11
- pip install ents[gui]
11
+ pip install ents
12
12
  ```
13
13
 
14
14
  You can also install the package from source with the following:
15
15
 
16
16
  ```bash
17
17
  # install package
18
- pip install .[gui]
18
+ pip install .
19
19
  ```
20
20
 
21
21
  If you are planning to develop the package we recommend you install the package
@@ -25,114 +25,68 @@ reinstall it.
25
25
 
26
26
  ```bash
27
27
  # install development dependencies
28
- pip install -e .[gui,dev]
28
+ pip install -e .[dev]
29
29
  ```
30
30
 
31
- ## Usage
31
+ To install the *deprecated* user config gui, use the following: =
32
+ ```bash
33
+ pip install -e ents[gui]
34
+ ```
32
35
 
33
- The following example code demonstrates decoding the measurement message and encoding a response.
34
36
 
35
- ```python
36
- from ents import encode, decode
37
37
 
38
- # get data encoded by the soil power sensor
39
- data = ...
38
+ ## Simulator (New)
40
39
 
41
- meas_dict = decode(data)
40
+ The webserver `tools/http_decoder.py` can be used to decode uploaded measurements.
41
+
42
+ ### CLI Usage
43
+
44
+ ```
45
+ usage: ents sim_generic [-h] [-v] [--url URL] --sensor SENSOR [SENSOR ...] [--min MIN] [--max MAX] --cell CELL --logger LOGGER [--start START] [--end END] [--freq FREQ] {batch,stream}
46
+
47
+ positional arguments:
48
+ {batch,stream} Upload mode
49
+
50
+ options:
51
+ -h, --help show this help message and exit
52
+ -v, --verbose Print addiitional request information.
53
+ --url URL URL of the dirtviz instance (default: http://localhost:8000)
54
+ --sensor SENSOR [SENSOR ...]
55
+ Type of sensor to simulate
56
+ --min MIN Minimum sensor value (default: -1.0)
57
+ --max MAX Maximum sensor value (default: 1.0)
58
+ --cell CELL Cell Id
59
+ --logger LOGGER Logger Id
60
+
61
+ Batch:
62
+ --start START Start date
63
+ --end END End date
64
+
65
+ Stream:
66
+ --freq FREQ Frequency of uploads (default: 10s)
67
+ ```
68
+
69
+ ### Examples
70
+
71
+ You can find the available sensors in the `sensors.proto` file.
72
+
73
+ Example uploading single measurement
74
+ ```
75
+ ents sim_generic stream --sensor POWER_VOLTAGE --min 20 --max 30 --cell 1 --logger 1
76
+ ```
77
+
78
+ Example uploading multiple measuremnets
79
+ ```
80
+ ents sim_generic stream --sensor TEROS12_VWC_ADJ TEROS12_TEMP TEROS12_EC --min 10 --max 100 --cell 1 --logger 1
81
+ ```
82
+
83
+ Example batch uploads
84
+ ```
85
+ ents sim_generic batch --sensor POWER_CURRENT --cell 1 --logger 1 --start 2026-01-19 --end 2026-01-20 --freq 60
86
+ ```
42
87
 
43
- # process data
44
- ...
45
88
 
46
- # send response
47
- resp_str = encode(success=True)
48
- ```
49
-
50
- The formatting of the dictionary depends on the type of measurement sent. The key `type` is included on all measurement types and can be used to determine the type of message. See the source `*.proto` files to get the full list of types to get the full list of types and keys. A list is provided in [Message Types](#message-types). The Python protobuf API uses camel case when naming keys. The key `ts` is in ISO 8601 format as a string.
51
-
52
- ## Message Types
53
-
54
- Type `power`
55
- ```python
56
- meas_dict = {
57
- "type": "power",
58
- "loggerId": ...,
59
- "cellId": ...,
60
- "ts": ...,
61
- "data": {
62
- "voltage": ...,
63
- "current": ...
64
- },
65
- "data_type": {
66
- "voltage": float,
67
- "voltage": float
68
- }
69
- }
70
- ```
71
-
72
- Type `teros12`
73
- ```python
74
- meas_dict = {
75
- "type": "teros12",
76
- "loggerId": ...,
77
- "cellId": ...,
78
- "ts": ...,
79
- "data": {
80
- "vwcRaw": ...,
81
- "vwcAdj": ...,
82
- "temp": ...,
83
- "ec": ...
84
- },
85
- "data_type": {
86
- "vwcRaw": float,
87
- "vwcAdj": float,
88
- "temp": float,
89
- "ec": int
90
- }
91
- }
92
- ```
93
-
94
- Type `bme280` with `raw=True` (default)
95
- ```python
96
- meas_dict = {
97
- "type": "bme280",
98
- "loggerId": ...,
99
- "cellId": ...,
100
- "ts": ...,
101
- "data": {
102
- "pressure": ...,
103
- "temperature": ...,
104
- "humidity": ...,
105
- },
106
- "data_type": {
107
- "pressure": int,
108
- "temperature": int,
109
- "humidity": int,
110
- }
111
- }
112
- ```
113
-
114
- Type `bme280` with `raw=False`
115
- ```python
116
- meas_dict = {
117
- "type": "bme280",
118
- "loggerId": ...,
119
- "cellId": ...,
120
- "ts": ...,
121
- "data": {
122
- "pressure": ...,
123
- "temperature": ...,
124
- "humidity": ...,
125
- },
126
- "data_type": {
127
- "pressure": float,
128
- "temperature": float,
129
- "humidity": float,
130
- }
131
- }
132
- ```
133
-
134
-
135
- ## Simulator
89
+ ## Simulator (Old)
136
90
 
137
91
  Simulate WiFi sensor uploads without requiring ENTS hardware.
138
92
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ents"
7
- version = "2.3.5"
7
+ version = "2.3.6"
8
8
  authors = [
9
9
  { name="John Madden", email="jmadden173@pm.me" },
10
10
  ]
@@ -17,7 +17,7 @@ classifiers = [
17
17
  "Operating System :: OS Independent",
18
18
  ]
19
19
  dependencies = [
20
- 'protobuf==6.33.2',
20
+ 'protobuf==6.33.4',
21
21
  'matplotlib',
22
22
  'pandas',
23
23
  'pyserial',
@@ -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 decode_measurement
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 = decode_measurement(reply) # decode using protobuf
119
+ meas_dict = decode_repeated_sensor_measurements(reply) # decode using protobuf
120
120
 
121
- voltage_value = meas_dict["data"]["voltage"]
122
- current_value = meas_dict["data"]["current"]
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
 
@@ -36,7 +36,7 @@ from .proto.sensor import (
36
36
  decode_sensor_response,
37
37
  )
38
38
 
39
- from .simulator.node import NodeSimulator
39
+ from .simulator.node import NodeSimulator, NodeSimulatorGeneric
40
40
 
41
41
 
42
42
  def entry():
@@ -68,15 +68,22 @@ def create_sim_generic_parser(subparsers):
68
68
  """
69
69
 
70
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
+
71
79
  sim_p.add_argument(
72
80
  "--url",
73
- required=True,
81
+ default="http://localhost:8000/api/sensor",
74
82
  type=str,
75
83
  help="URL of the dirtviz instance (default: http://localhost:8000)",
76
84
  )
77
85
  sim_p.add_argument(
78
- "--mode",
79
- required=True,
86
+ "mode",
80
87
  choices=["batch", "stream"],
81
88
  type=str,
82
89
  help="Upload mode",
@@ -88,6 +95,7 @@ def create_sim_generic_parser(subparsers):
88
95
  nargs="+",
89
96
  help="Type of sensor to simulate",
90
97
  )
98
+
91
99
  sim_p.add_argument(
92
100
  "--min", type=float, default=-1.0, help="Minimum sensor value (default: -1.0)"
93
101
  )
@@ -96,11 +104,16 @@ def create_sim_generic_parser(subparsers):
96
104
  )
97
105
  sim_p.add_argument("--cell", required=True, type=int, help="Cell Id")
98
106
  sim_p.add_argument("--logger", required=True, type=int, help="Logger Id")
99
- sim_p.add_argument("--start", type=str, help="Start date")
100
- sim_p.add_argument("--end", type=str, help="End date")
101
- sim_p.add_argument(
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(
102
114
  "--freq", default=10.0, type=float, help="Frequency of uploads (default: 10s)"
103
115
  )
116
+
104
117
  sim_p.set_defaults(func=simulate_generic)
105
118
 
106
119
  return sim_p
@@ -149,7 +162,12 @@ def create_sim_parser(subparsers):
149
162
 
150
163
 
151
164
  def simulate_generic(args):
152
- simulation = NodeSimulator(
165
+ if args.verbose:
166
+ print("Arguments:")
167
+ print(args)
168
+ print()
169
+
170
+ simulation = NodeSimulatorGeneric(
153
171
  cell=args.cell,
154
172
  logger=args.logger,
155
173
  sensors=args.sensor,
@@ -184,8 +202,17 @@ def simulate_generic(args):
184
202
  dt = datetime.now()
185
203
  ts = int(dt.timestamp())
186
204
  simulation.measure(ts)
205
+
187
206
  while simulation.send_next(args.url):
188
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
+
189
216
  time.sleep(args.freq)
190
217
  except KeyboardInterrupt as _:
191
218
  print("Stopping simulation")
@@ -0,0 +1,8 @@
1
+ from .client import BackendClient
2
+
3
+ from .plots import plot_data
4
+
5
+ __all__ = [
6
+ "BackendClient",
7
+ "plot_data",
8
+ ]
@@ -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
@@ -0,0 +1,50 @@
1
+ """Common plots for data on dirtviz.
2
+
3
+ Still need to fill in with common functions:
4
+ - Plot power (voltage, current, calcualted power)
5
+ - Plot teros12
6
+ - Plot bme280
7
+ - Plot all (take all available sensors)
8
+ - Plot group (find mean/stddev for each point)
9
+
10
+ Can pull from personal scripts from SenSys (jtmadden)
11
+ """
12
+
13
+ import pandas as pd
14
+
15
+ import matplotlib.pyplot as plt
16
+
17
+
18
+ # matplotlib formatting
19
+ # plt.rcParams["font.size"] = 7
20
+ # plt.rcParams['font.weight'] = 'medium'
21
+ # plt.ion()
22
+
23
+
24
+ def plot_data(data: list[pd.DataFrame], name, **kwargs):
25
+ """Plots data from one or many cells
26
+
27
+ Args:
28
+ data: List of dataframes
29
+ name: Column or measurement name
30
+ """
31
+
32
+ fig, ax = plt.subplots()
33
+ for d in data:
34
+ ax.plot(d["timestamp"], d[name], **kwargs)
35
+
36
+ ax.axhline(
37
+ y=0,
38
+ color="black",
39
+ linewidth=2,
40
+ dashes=(2, 2),
41
+ )
42
+
43
+ ax.set_xlabel("Timestamp")
44
+ ax.set_ylabel(name)
45
+
46
+ ax.grid()
47
+
48
+ plt.show(block=False)
49
+
50
+ return ax
@@ -172,6 +172,8 @@ class NodeSimulatorGeneric:
172
172
  measurements: list[bytes] = []
173
173
  # all responses
174
174
  responses: list[str] = []
175
+ # all requests in format (headers, body)
176
+ requests: list[tuple[str, str]] = []
175
177
 
176
178
  # metrics for uploads
177
179
  metrics: dict[str, int] = {
@@ -233,14 +235,18 @@ class NodeSimulatorGeneric:
233
235
 
234
236
  # get next measurement
235
237
  try:
236
- meas = self.measurements.pop()
238
+ meas = self.measurement_buffer.pop()
237
239
  except IndexError as _:
238
240
  return False
239
241
 
240
- headers = {"Content-Type": "application/octet-stream"}
242
+ headers = {
243
+ "Content-Type": "application/octet-stream",
244
+ "SensorVersion": "2",
245
+ }
241
246
  result = requests.post(url, data=meas, headers=headers)
242
247
 
243
248
  # store result
249
+ self.requests.append((result.request.headers, result.request.body))
244
250
  self.responses.append(result.text)
245
251
  self.metrics["total_requests"] += 1
246
252
  if result.status_code == 200:
@@ -279,4 +285,36 @@ class NodeSimulatorGeneric:
279
285
  )
280
286
 
281
287
  serialized = encode_repeated_sensor_measurements(meas)
288
+ self.measurements.append(serialized)
282
289
  self.measurement_buffer.append(serialized)
290
+
291
+ def last_measurement(self) -> bytes:
292
+ """Gets the last encoded measurement.
293
+
294
+
295
+ Returns:
296
+ Last encoded measurement.
297
+ """
298
+
299
+ return self.measurements[-1]
300
+
301
+ def last_request(self) -> str:
302
+ """Gets the last sent request.
303
+
304
+ Returns:
305
+ Formatted headers and body.
306
+ """
307
+
308
+ headers = self.requests[-1][0]
309
+ body = self.requests[-1][1]
310
+ request_str = f"{headers}\n\n{body}"
311
+ return request_str
312
+
313
+ def last_response(self) -> str:
314
+ """Gets the last response.
315
+
316
+ Returns:
317
+ Response from server.
318
+ """
319
+
320
+ return self.responses[-1]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes