Coverage for farmbot/functions/information.py: 100%
138 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-09-12 12:18 -0700
« prev ^ index » next coverage.py v7.4.4, created at 2024-09-12 12:18 -0700
1"""
2Information class.
3"""
5# └── functions/information
6# ├── [API] api_get()
7# ├── [API] api_patch()
8# ├── [API] api_post()
9# ├── [API] api_delete()
10# ├── [API] safe_z()
11# ├── [API] garden_size()
12# ├── [API] curve()
13# ├── [BROKER] measure_soil_height()
14# ├── [BROKER] read_status()
15# ├── [BROKER] read_pin()
16# └── [BROKER] read_sensor()
18from .broker import BrokerConnect
19from .api import ApiConnect
22class Information():
23 """Information class."""
25 def __init__(self, state):
26 self.broker = BrokerConnect(state)
27 self.api = ApiConnect(state)
28 self.state = state
30 def api_get(self, endpoint, database_id=None, data_print=True):
31 """Get information about a specific endpoint."""
32 self.state.print_status(
33 description=f"Retrieving {endpoint} information.")
35 endpoint_data = self.api.request("GET", endpoint, database_id)
37 if data_print:
38 self.state.print_status(
39 update_only=True,
40 endpoint_json=endpoint_data)
41 else:
42 self.state.print_status(
43 update_only=True,
44 description=f"Fetched {len(endpoint_data)} items.")
46 return endpoint_data
48 def api_patch(self, endpoint, new_data, database_id=None):
49 """Change information contained within an endpoint."""
50 self.state.print_status(description=f"Editing {endpoint}.")
52 result = self.api.request(
53 method="PATCH",
54 endpoint=endpoint,
55 database_id=database_id,
56 payload=new_data)
58 self.state.print_status(update_only=True, endpoint_json=result)
60 return result
62 def api_post(self, endpoint, new_data):
63 """Create new information contained within an endpoint."""
64 self.state.print_status(description=f"Adding new data to {endpoint}.")
66 result = self.api.request(
67 method="POST",
68 endpoint=endpoint,
69 database_id=None,
70 payload=new_data)
72 self.state.print_status(update_only=True, endpoint_json=result)
74 return result
76 def api_delete(self, endpoint, database_id=None):
77 """Delete information contained within an endpoint."""
78 self.state.print_status(
79 description=f"Deleting {endpoint} with id={database_id}.")
81 result = self.api.request("DELETE", endpoint, database_id=database_id)
83 self.state.print_status(update_only=True, endpoint_json=result)
85 return result
87 def safe_z(self):
88 """Returns the highest safe point along the z-axis."""
89 self.state.print_status(description="Retrieving safe z value...")
91 config_data = self.api_get('fbos_config')
92 z_value = config_data["safe_height"]
94 self.state.print_status(
95 description=f"Safe z={z_value}", update_only=True)
96 return z_value
98 def garden_size(self):
99 """Return size of garden bed."""
100 self.state.print_status(description="Retrieving garden size...")
102 json_data = self.api_get('firmware_config')
104 x_steps = json_data['movement_axis_nr_steps_x']
105 x_mm = json_data['movement_step_per_mm_x']
107 y_steps = json_data['movement_axis_nr_steps_y']
108 y_mm = json_data['movement_step_per_mm_y']
110 z_steps = json_data['movement_axis_nr_steps_z']
111 z_mm = json_data['movement_step_per_mm_z']
113 garden_size = {
114 "x": x_steps / x_mm,
115 "y": y_steps / y_mm,
116 "z": z_steps / z_mm,
117 }
119 self.state.print_status(endpoint_json=garden_size, update_only=True)
120 return garden_size
122 def get_curve(self, curve_id):
123 """Retrieve curve data from the API and return a curve object with extras."""
124 self.state.print_status(description="Preparing curve information...")
126 api_curve_data = self.api_get("curves", curve_id)
127 if isinstance(api_curve_data, str):
128 return None
129 return Curve(api_curve_data)
131 def measure_soil_height(self):
132 """Use the camera to measure the soil height at the current location."""
133 self.state.print_status(description="Measuring soil height...")
135 measure_soil_height_message = {
136 "kind": "execute_script",
137 "args": {
138 "label": "Measure Soil Height"
139 }
140 }
142 self.broker.publish(measure_soil_height_message)
144 def read_status(self, path=None):
145 """Returns the FarmBot status tree."""
146 path_str = "" if path is None else f" of {path}"
147 self.state.print_status(description=f"Reading status{path_str}...")
148 status_message = {
149 "kind": "read_status",
150 "args": {}
151 }
152 self.broker.publish(status_message)
154 status_trees = self.state.last_messages.get("status", [])
155 status_tree = None if len(status_trees) == 0 else status_trees[-1]
157 if path is not None:
158 for key in path.split("."):
159 status_tree = status_tree[key]
161 self.state.print_status(update_only=True, endpoint_json=status_tree)
162 return status_tree
164 @staticmethod
165 def convert_mode_to_number(mode):
166 """Converts mode string to mode number."""
167 modes = ["digital", "analog"]
168 if str(mode).lower() not in modes:
169 raise ValueError(f"Invalid mode: {mode} not in {modes}")
170 return 0 if mode.lower() == "digital" else 1
172 def read_pin(self, pin_number, mode="digital"):
173 """Reads the given pin by number."""
174 pin_mode = self.convert_mode_to_number(mode)
175 self.state.print_status(
176 description=f"Reading pin {pin_number} ({mode})...")
177 read_pin_message = {
178 "kind": "read_pin",
179 "args": {
180 "pin_number": pin_number,
181 "label": "---",
182 "pin_mode": pin_mode,
183 }
184 }
185 self.broker.publish(read_pin_message)
187 def read_sensor(self, sensor_name):
188 """Reads the given sensor."""
189 self.state.print_status(description=f"Reading {sensor_name} sensor...")
190 sensor = self.get_resource_by_name("sensors", sensor_name)
191 if sensor is None:
192 return
193 sensor_id = sensor["id"]
194 mode = sensor["mode"]
196 sensor_message = {
197 "kind": "read_pin",
198 "args": {
199 "pin_mode": mode,
200 "label": "---",
201 "pin_number": {
202 "kind": "named_pin",
203 "args": {
204 "pin_type": "Sensor",
205 "pin_id": sensor_id,
206 }
207 }
208 }
209 }
211 self.broker.publish(sensor_message)
213 def get_resource_by_name(self, endpoint, resource_name, name_key="label", query=None):
214 """Find a resource by name."""
215 self.state.print_status(
216 description=f"Searching for {resource_name} in {endpoint}.")
217 resources = self.state.fetch_cache(endpoint)
218 if resources is None:
219 resources = self.api_get(endpoint, data_print=False)
220 else:
221 self.state.print_status(
222 description=f"Using {len(resources)} cached items.")
223 if query is not None:
224 for key, value in query.items():
225 resources = [res for res in resources if res[key] == value]
226 names = [resource[name_key] for resource in resources]
227 if resource_name not in names:
228 error = f"ERROR: '{resource_name}' not in {endpoint}: {names}."
229 self.state.print_status(description=error, update_only=True)
230 self.state.error = error
231 self.state.clear_cache(endpoint)
232 return None
234 self.state.save_cache(endpoint, resources)
235 resource = [p for p in resources if p[name_key] == resource_name][0]
236 return resource
239class Curve:
240 """Curve data object for the get_curve() function to return."""
242 def __init__(self, curve_data):
243 self.curve_data = curve_data
244 self.name = curve_data["name"]
245 self.type = curve_data["type"]
246 self.unit = "mL" if self.type == "water" else "mm"
248 def __getitem__(self, key):
249 """Allow dictionary-style access to attributes."""
250 return getattr(self, key)
252 def day(self, day):
253 """Calculate the value for a specific day based on the curve data."""
254 day = int(day)
255 data = self.curve_data["data"]
256 data = {int(key): val for key, val in data.items()}
257 value = data.get(day)
258 if value is not None:
259 return value
261 sorted_day_keys = sorted(data.keys())
262 prev_day = None
263 next_day = None
264 for day_key in sorted_day_keys:
265 if day_key < day:
266 prev_day = day_key
267 elif day_key > day and next_day is None:
268 next_day = day_key
269 break
271 if prev_day is None:
272 return data[sorted_day_keys[0]]
274 if next_day is None:
275 return data[sorted_day_keys[-1]]
277 exact_value = (data[prev_day] * (next_day - day) +
278 data[next_day] * (day - prev_day)
279 ) / (next_day - prev_day)
280 return round(exact_value, 2)