ThalamOS
a powerful Flask web application designed to enhance your storage management.
Loading...
Searching...
No Matches
app.py
Go to the documentation of this file.
1"""
2ThalamOS Application
3
4This module sets up a Flask web application for managing storage items, controlling WLED devices,
5interacting with a WiFi scale, and querying an LLM service. It provides various routes for rendering templates,
6handling item creation, deletion, and search, as well as retrieving environment configurations,
7scale weight, and logging messages.
8"""
9
10from typing import Annotated
11import json
12import os
13
14from flask import ( # pylint: disable=import-error
15 Flask,
16 request,
17 render_template,
18 jsonify,
19 Response,
20)
21from flask_cors import CORS # pylint: disable=import-error
22from dotenv import load_dotenv # pylint: disable=import-error
23
24from logger_config import logger
25import Storage_connector
26import config_manager
27import weigh_fi_manager as wifiscale
28import wled_requests
29import ollama_manager as ollama
30
31ENV_PATH = os.path.join(os.path.dirname(__file__), "data/.env")
32load_dotenv(dotenv_path=ENV_PATH)
33IS_SCALE_ENABLED = os.getenv("IS_SCALE_ENABLED").lower() == "true"
34app = Flask(__name__)
35CORS(app)
36
37
38@app.route("/")
39def index() -> Annotated[str, "search page as a rendered template"]:
40 """
41 Renders the search page.
42 Returns:
43 Response: The rendered HTML template for the search page.
44 """
45 return render_template("search.html")
46
47
48@app.route("/toggleLight")
49def toggle_light() -> Annotated[str, "search page as a rendered template"]:
50 """
51 Toggles the power state of the WLED device and renders the search template.
52 This function changes the power state of the WLED device to the opposite of its current state
53 by calling the `changePowerState` method of the `wled_requests` object.
54 After toggling the power state,
55 it returns the rendered "search.html" template.
56 Returns:
57 str: The rendered "search.html" template.
58 """
59 try:
60 current_state = wled_requests.get_power_state()
61 if current_state is not None:
62 wled_requests.change_power_state(not current_state)
63 logger.info(f"Toggled WLED power state to: {not current_state}")
64 else:
65 logger.warning("Unable to toggle WLED power state: Current state is unknown.")
66 except Exception as e:
67 logger.error(f"Error toggling WLED power state: {e}")
68 return render_template("search.html")
69
70
71@app.route("/createItem")
72def create_item() -> Annotated[str, "item creation page as a rendered template"]:
73 """
74 Renders the template for creating a new item.
75 Returns:
76 Response: The rendered HTML template for creating a new item.
77 """
78 return render_template("createItem.html")
79
80
81@app.route("/sendCreation", methods=["POST"])
82def send_creation() -> Annotated[tuple, {"status": str, "status_code": int}]:
83 """
84 Handle the creation of a new item by processing the incoming JSON request data.
85 The function expects a JSON payload with the following structure:
86 {
87 "info": <json>,
88 "type": <str>,
89 "name": <str>,
90 "position": <str>
91 }
92 It extracts the necessary information from the JSON payload and attempts to create a new item
93 using the Storage_connector.CreateItem method.
94 If an exception occurs during the creation process,
95 it prints the exception.
96 Returns:
97 tuple: A dictionary with a status message and an HTTP status code.
98 """
99
100 data = request.get_json()
101 logger.info(f"Received creation request with data: {data}")
102 info = json.dumps(data["info"])
103 obj_type = data["type"]
104 name = data["name"]
105 pos = data["position"]
106 try:
108 pos=pos, obj_type=obj_type, name=name, json_data=info
109 )
110 except Exception as e:
111 logger.error(
112 f"Failed to create item with name: {name}, type: {obj_type}, position: {pos}. Error: {e}"
113 )
114 return {"status": "error"}, 500
115 return {"status": "created"}, 201
116
117
118@app.route("/item/<item_id>")
119def item(item_id) -> Annotated[str, "item page as a rendered template"]:
120 """
121 Handles the request to display an item.
122 This function performs the following steps:
123 1. Changes the power state of the WLED device to on.
124 2. Fetches the item details from the storage using the provided item identifier.
125 3. Sets the color position on the WLED device based on the fetched item details.
126 4. If the fetched item contains additional information,
127 it parses the data and renders the 'item.jinja2' template
128 with the item details and information.
129 6. If the fetched item does not contain additional information,
130 it renders the 'item.jinja2' template with only the item details.
131 Args:
132 item_id (int): The id of the item to display.
133 Returns:
134 The rendered HTML template for the item.
135 """
137 item_sql = Storage_connector.fetch_item(item_id)
138 wled_requests.color_pos(item_sql[1])
139 logger.info(f"Fetched item details for item_id {item_id}: {item_sql}")
140 if item_sql[4]:
141 json_info = json.loads(item_sql[4])
142 return render_template("item.jinja2", item=item_sql, json=json_info, id=item_id)
143
144 return render_template("item.jinja2", item=item_sql, id=item_id)
145
146
147@app.route("/item/<item_id>/update", methods=["POST"])
148def update_item(item_id) -> Annotated[tuple, {"status": str, "status_code": int}]:
149 """
150 Updates an item using the Storage_connector and returns a status message.
151 Args:
152 item_id: The id of the item to be updated.
153 Returns:
154 A dictionary with a status message and an HTTP status code.
155 """
156 data = request.get_json()
157 logger.info(f"Received update request for item_id {item_id} with data: {data}")
158 info = json.dumps(data.get("info", {}))
159 obj_type = data.get("type")
160 name = data.get("name")
161 pos = data.get("position")
162 try:
164 item_id=item_id, pos=pos, obj_type=obj_type, name=name, json_data=info
165 )
166 except Exception as e:
167 logger.error(f"Failed to update item with id: {item_id}. Error: {e}")
168 return {"status": "error"}, 500
169 return {"status": "updated"}, 200
170
171
172@app.route("/item/<item>/delete")
173def delete_item(item_id) -> Annotated[str, "search page as a rendered template"]:
174 """
175 Deletes an item using the Storage_connector and renders the search.html template.
176 Args:
177 item_id: The id of the item to be deleted.
178 Returns:
179 A rendered template for the search page.
180 """
181
183 return render_template("search.html")
184
185
186@app.route("/search/<term>", methods=["GET"])
187def search(term) -> Response:
188 """
189 Search for a term in the storage and return the results in JSON format.
190 Args:
191 term (str): The term to search for in the storage.
192 Returns:
193 Response: A Flask Response object containing the search results in JSON format.
194 """
195 data = Storage_connector.search(term)
196 return jsonify(data)
197
198
199@app.errorhandler(Exception)
200def handle_exception(e) -> Exception:
201 """
202 Handles exceptions by passing through HTTP errors.
203 Parameters:
204 e (Exception): The exception to handle.
205 Returns:
206 Exception: The same exception that was passed in.
207 """
208
209 # pass through HTTP errors
210 return e
211
212
213@app.route("/config/env")
214def get_env() -> Response:
215 """
216 Retrieve the environment configuration.
217 This function uses the config_manager to get the current environment
218 configuration and returns it as a JSON response.
219 Returns:
220 Response: A Flask JSON response containing the environment configuration.
221 """
222 return jsonify(config_manager.get_env())
223
224
225@app.route("/config/ollama/models")
226def get_ollama_models() -> tuple[Response, int] | Response:
227 """
228 Fetches the list of available Ollama models.
229 Returns:
230 Annotated[str, "json response"]: A JSON response containing the list of Ollama models.
231 """
232 values = ollama.get_ollama_models()
233 if not values:
234 return jsonify({"status": "Ollama service is not enabled"}), 412
235 return jsonify(values)
236
237
238@app.route("/wifiscale/weight")
239def get_weight() -> tuple[Response, int] | Response:
240 """
241 Retrieve the weight of the scale.
242 This function checks if the scale service is enabled by reading the IS_SCALE_ENABLED environment variable.
243 If the scale service is not enabled, it returns a JSON response with a status message and HTTP status code 412.
244 If the scale service is enabled, it uses the wifiscale module to get the weight of the scale and returns it as a JSON response.
245 Returns:
246 Response: A Flask JSON response containing the weight of the scale or a status message.
247 """
248 if IS_SCALE_ENABLED == "False":
249 return jsonify({"status": "scale service is not enabled"}), 412
250
251 weight = wifiscale.get_weight()
252 return jsonify({"weight": weight})
253
254
255@app.route("/llm/ask", methods=["POST"])
256def ask_llm_question() -> Response:
257 """
258 Ask a question to the Language Learning Model (LLM) service.
259 This function sends a GET request to the LLM service at the specified endpoint
260 and returns the response as a JSON object.
261 Returns:
262 Response: A Flask JSON response containing the response from the LLM service.
263 """
264 data = request.get_json()
265 question = data.get("question")
266 logger.info(f"Received question via llm endpoint: {question}")
267 response = ollama.ask_question(question)
268 return jsonify(response)
269
270
271@app.route("/log", methods=["POST"])
272def log_message() -> Annotated[tuple, {"status": str, "status_code": int}]:
273 """
274 Logs a message with a specified log level.
275
276 The log level and message content are extracted from the JSON payload of the request.
277 If the log level is not provided, it defaults to 'INFO'.
278 If the message content is not provided, it defaults to an empty string.
279
280 Returns:
281 tuple: A dictionary with a status message and an HTTP status code 201.
282
283 Request JSON structure:
284 {
285 "level": "DEBUG" | "INFO" | "WARNING" | "ERROR" | "CRITICAL",
286 "message": "Your log message here"
287 }
288 """
289
290 data: Annotated[str, "content of request"] = request.json
291 level: Annotated[str, "log level, default value is INFO"] = data.get(
292 "level", "INFO"
293 )
294 message: Annotated[str, "content of log, default value is empty"] = data.get(
295 "message", ""
296 )
297
298 match level:
299 case "DEBUG":
300 logger.debug(message)
301 case "INFO":
302 logger.info(message)
303 case "WARNING":
304 logger.warning(message)
305 case "ERROR":
306 logger.error(message)
307 case "CRITICAL":
308 logger.critical(message)
309
310 return {"status": "created"}, 201
311
312
313with app.app_context():
315
316
317if __name__ == "__main__":
318 app.run(host="0.0.0.0", debug=True)
List[StorageItem] search(str search_term)
StorageItem|None fetch_item(int item_id)
None update_item(int item_id, int pos, StorageItemType obj_type, str name, str json_data)
None create_item(int pos, StorageItemType obj_type, str name, str json_data)
None delete_item(int item_id)
Response ask_llm_question()
Definition app.py:256
Exception handle_exception(e)
Definition app.py:200
Annotated[tuple, {"status":str, "status_code":int}] log_message()
Definition app.py:272
Annotated[tuple, {"status":str, "status_code":int}] send_creation()
Definition app.py:82
Annotated[tuple, {"status":str, "status_code":int}] update_item(item_id)
Definition app.py:148
Response search(term)
Definition app.py:187
tuple[Response, int]|Response get_ollama_models()
Definition app.py:226
Annotated[str, "item creation page as a rendered template"] create_item()
Definition app.py:72
tuple[Response, int]|Response get_weight()
Definition app.py:239
Annotated[str, "search page as a rendered template"] toggle_light()
Definition app.py:49
Annotated[str, "search page as a rendered template"] delete_item(item_id)
Definition app.py:173
Annotated[str, "item page as a rendered template"] item(item_id)
Definition app.py:119
Response get_env()
Definition app.py:214
Annotated[str, "search page as a rendered template"] index()
Definition app.py:39
Annotated[dict, "dictionary of environment variables"] get_env()
None change_power_state(state)
None color_pos(int pos)