Coverage for flask/src/rasp_water/webapp_valve.py: 95%
133 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-28 13:51 +0900
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-28 13:51 +0900
1#!/usr/bin/env python3
2import logging
3import multiprocessing
4import os
5import pathlib
6import threading
7import time
8import traceback
10import flask_cors
11import fluent.sender
12import my_lib.flask_util
13import my_lib.footprint
14import my_lib.webapp.config
15import my_lib.webapp.event
16import my_lib.webapp.log
17import rasp_water.valve
18import rasp_water.weather_forecast
19import rasp_water.weather_sensor
21import flask
23blueprint = flask.Blueprint("rasp-water-valve", __name__, url_prefix=my_lib.webapp.config.URL_PREFIX)
25worker = None
26flow_stat_manager = None
27should_terminate = threading.Event()
30def init(config):
31 global worker # noqa: PLW0603
32 global flow_stat_manager # noqa: PLW0603
34 if worker is not None: 34 ↛ 35line 34 didn't jump to line 35 because the condition on line 34 was never true
35 raise ValueError("worker should be None") # noqa: TRY003, EM101
37 if flow_stat_manager is not None:
38 flow_stat_manager.shutdown()
40 flow_stat_manager = multiprocessing.Manager()
41 flow_stat_queue = flow_stat_manager.Queue()
42 rasp_water.valve.init(config, flow_stat_queue)
43 worker = threading.Thread(target=flow_notify_worker, args=(config, flow_stat_queue))
44 worker.start()
47def term():
48 global worker # noqa: PLW0603
50 if worker is None:
51 return
53 should_terminate.set()
54 worker.join()
56 worker = None
57 should_terminate.clear()
59 rasp_water.valve.term()
62def send_data(config, flow):
63 logging.info("Send fluentd: flow = %.2f", flow)
64 sender = fluent.sender.FluentSender(config["fluent"]["data"]["tag"], host=config["fluent"]["host"])
65 sender.emit("rasp", {"hostname": config["fluent"]["data"]["hostname"], "flow": flow})
66 sender.close()
69def second_str(sec):
70 minute = 0
71 if sec >= 60:
72 minute = int(sec / 60)
73 sec -= minute * 60
74 sec = int(sec)
76 if minute != 0:
77 if sec == 0:
78 return f"{minute}分"
79 else:
80 return f"{minute}分{sec}秒"
81 else:
82 return f"{sec}秒"
85def flow_notify_worker(config, queue):
86 global should_terminate
88 sleep_sec = 0.1
90 liveness_file = pathlib.Path(config["liveness"]["file"]["flow_notify"])
92 logging.info("Start flow notify worker")
93 i = 0
94 while True:
95 if should_terminate.is_set():
96 break
98 try:
99 if not queue.empty():
100 stat = queue.get()
102 logging.debug("flow notify = %s", str(stat))
104 if stat["type"] == "total":
105 my_lib.webapp.log.info(
106 "🚿 {time_str}間、約 {water:.2f}L の水やりを行いました。".format(
107 time_str=second_str(stat["period"]), water=stat["total"]
108 )
109 )
110 elif stat["type"] == "instantaneous":
111 send_data(config, stat["flow"])
112 elif stat["type"] == "error":
113 my_lib.webapp.log.error(stat["message"])
114 else: # pragma: no cover
115 pass
116 time.sleep(sleep_sec)
117 except OverflowError: # pragma: no cover
118 # NOTE: テストする際、freezer 使って日付をいじるとこの例外が発生する
119 logging.debug(traceback.format_exc())
121 if i % (10 / sleep_sec) == 0:
122 my_lib.footprint.update(liveness_file)
124 i += 1
126 logging.info("Terminate flow notify worker")
129def get_valve_state():
130 try:
131 state = rasp_water.valve.get_control_mode()
133 return {
134 "state": state["mode"].value,
135 "remain": state["remain"],
136 "result": "success",
137 }
138 except Exception:
139 logging.warning("Failed to get valve control mode")
141 return {"state": 0, "remain": 0, "result": "fail"}
144def judge_execute(config, state, auto):
145 if (state != 1) or (not auto):
146 return True
148 rainfall_judge, rain_fall_sum = rasp_water.weather_sensor.get_rain_fall(config)
149 if rainfall_judge: 149 ↛ 151line 149 didn't jump to line 151 because the condition on line 149 was never true
150 # NOTE: ダミーモードの場合、とにかく水やりする (CI テストの為)
151 if os.environ.get("DUMMY_MODE", "false") == "true":
152 return True
154 my_lib.webapp.log.info(
155 f"☂ 前回の水やりから {rain_fall_sum:.0f}mm の雨が降ったため、自動での水やりを見合わせます。"
156 )
157 return False
159 rainfall_judge, rain_fall_sum = rasp_water.weather_forecast.get_rain_fall(config)
161 if rainfall_judge:
162 # NOTE: ダミーモードの場合、とにかく水やりする (CI テストの為)
163 if os.environ.get("DUMMY_MODE", "false") == "true":
164 return True
166 my_lib.webapp.log.info(
167 f"☂ 前後で {rain_fall_sum:.0f}mm の雨が降る予報があるため、自動での水やりを見合わせます。"
168 )
169 return False
171 return True
174def set_valve_state(config, state, period, auto, host=""):
175 is_execute = judge_execute(config, state, auto)
177 if not is_execute:
178 my_lib.webapp.event.notify_event(my_lib.webapp.event.EVENT_TYPE.CONTROL)
179 return get_valve_state()
181 if state == 1:
182 my_lib.webapp.log.info(
183 "{auto}で{period_str}間の水やりを開始します。{by}".format(
184 auto="🕑 自動" if auto else "🔧 手動",
185 period_str=second_str(period),
186 by=f"(by {host})" if host != "" else "",
187 )
188 )
189 rasp_water.valve.set_control_mode(period)
190 else:
191 my_lib.webapp.log.info(
192 "{auto}で水やりを終了します。{by}".format(
193 auto="🕑 自動" if auto else "🔧 手動",
194 by=f"(by {host})" if host != "" else "",
195 )
196 )
197 rasp_water.valve.set_state(rasp_water.valve.VALVE_STATE.CLOSE)
199 my_lib.webapp.event.notify_event(my_lib.webapp.event.EVENT_TYPE.CONTROL)
200 return get_valve_state()
203@blueprint.route("/api/valve_ctrl", methods=["GET", "POST"])
204@my_lib.flask_util.support_jsonp
205@flask_cors.cross_origin()
206def api_valve_ctrl():
207 cmd = flask.request.args.get("cmd", 0, type=int)
208 state = flask.request.args.get("state", 0, type=int)
209 period = flask.request.args.get("period", 0, type=int)
210 auto = flask.request.args.get("auto", False, type=bool)
212 config = flask.current_app.config["CONFIG"]
214 if cmd == 1:
215 user = my_lib.flask_util.auth_user(flask.request)
216 return flask.jsonify(dict({"cmd": "set"}, **set_valve_state(config, state, period, auto, user)))
217 else:
218 return flask.jsonify(dict({"cmd": "get"}, **get_valve_state()))
221@blueprint.route("/api/valve_flow", methods=["GET"])
222@my_lib.flask_util.support_jsonp
223@flask_cors.cross_origin()
224def api_valve_flow():
225 config = flask.current_app.config["CONFIG"]
227 return flask.jsonify({"cmd": "get", "flow": rasp_water.valve.get_flow(config["flow"]["offset"])["flow"]})