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

1""" 

2Information class. 

3""" 

4 

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() 

17 

18from .broker import BrokerConnect 

19from .api import ApiConnect 

20 

21 

22class Information(): 

23 """Information class.""" 

24 

25 def __init__(self, state): 

26 self.broker = BrokerConnect(state) 

27 self.api = ApiConnect(state) 

28 self.state = state 

29 

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.") 

34 

35 endpoint_data = self.api.request("GET", endpoint, database_id) 

36 

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.") 

45 

46 return endpoint_data 

47 

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}.") 

51 

52 result = self.api.request( 

53 method="PATCH", 

54 endpoint=endpoint, 

55 database_id=database_id, 

56 payload=new_data) 

57 

58 self.state.print_status(update_only=True, endpoint_json=result) 

59 

60 return result 

61 

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}.") 

65 

66 result = self.api.request( 

67 method="POST", 

68 endpoint=endpoint, 

69 database_id=None, 

70 payload=new_data) 

71 

72 self.state.print_status(update_only=True, endpoint_json=result) 

73 

74 return result 

75 

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}.") 

80 

81 result = self.api.request("DELETE", endpoint, database_id=database_id) 

82 

83 self.state.print_status(update_only=True, endpoint_json=result) 

84 

85 return result 

86 

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...") 

90 

91 config_data = self.api_get('fbos_config') 

92 z_value = config_data["safe_height"] 

93 

94 self.state.print_status( 

95 description=f"Safe z={z_value}", update_only=True) 

96 return z_value 

97 

98 def garden_size(self): 

99 """Return size of garden bed.""" 

100 self.state.print_status(description="Retrieving garden size...") 

101 

102 json_data = self.api_get('firmware_config') 

103 

104 x_steps = json_data['movement_axis_nr_steps_x'] 

105 x_mm = json_data['movement_step_per_mm_x'] 

106 

107 y_steps = json_data['movement_axis_nr_steps_y'] 

108 y_mm = json_data['movement_step_per_mm_y'] 

109 

110 z_steps = json_data['movement_axis_nr_steps_z'] 

111 z_mm = json_data['movement_step_per_mm_z'] 

112 

113 garden_size = { 

114 "x": x_steps / x_mm, 

115 "y": y_steps / y_mm, 

116 "z": z_steps / z_mm, 

117 } 

118 

119 self.state.print_status(endpoint_json=garden_size, update_only=True) 

120 return garden_size 

121 

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...") 

125 

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) 

130 

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...") 

134 

135 measure_soil_height_message = { 

136 "kind": "execute_script", 

137 "args": { 

138 "label": "Measure Soil Height" 

139 } 

140 } 

141 

142 self.broker.publish(measure_soil_height_message) 

143 

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) 

153 

154 status_trees = self.state.last_messages.get("status", []) 

155 status_tree = None if len(status_trees) == 0 else status_trees[-1] 

156 

157 if path is not None: 

158 for key in path.split("."): 

159 status_tree = status_tree[key] 

160 

161 self.state.print_status(update_only=True, endpoint_json=status_tree) 

162 return status_tree 

163 

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 

171 

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) 

186 

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"] 

195 

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 } 

210 

211 self.broker.publish(sensor_message) 

212 

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 

233 

234 self.state.save_cache(endpoint, resources) 

235 resource = [p for p in resources if p[name_key] == resource_name][0] 

236 return resource 

237 

238 

239class Curve: 

240 """Curve data object for the get_curve() function to return.""" 

241 

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" 

247 

248 def __getitem__(self, key): 

249 """Allow dictionary-style access to attributes.""" 

250 return getattr(self, key) 

251 

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 

260 

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 

270 

271 if prev_day is None: 

272 return data[sorted_day_keys[0]] 

273 

274 if next_day is None: 

275 return data[sorted_day_keys[-1]] 

276 

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)