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