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

1#!/usr/bin/env python3 

2import logging 

3import multiprocessing 

4import os 

5import pathlib 

6import threading 

7import time 

8import traceback 

9 

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 

20 

21import flask 

22 

23blueprint = flask.Blueprint("rasp-water-valve", __name__, url_prefix=my_lib.webapp.config.URL_PREFIX) 

24 

25worker = None 

26flow_stat_manager = None 

27should_terminate = threading.Event() 

28 

29 

30def init(config): 

31 global worker # noqa: PLW0603 

32 global flow_stat_manager # noqa: PLW0603 

33 

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 

36 

37 if flow_stat_manager is not None: 

38 flow_stat_manager.shutdown() 

39 

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

45 

46 

47def term(): 

48 global worker # noqa: PLW0603 

49 

50 if worker is None: 

51 return 

52 

53 should_terminate.set() 

54 worker.join() 

55 

56 worker = None 

57 should_terminate.clear() 

58 

59 rasp_water.valve.term() 

60 

61 

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

67 

68 

69def second_str(sec): 

70 minute = 0 

71 if sec >= 60: 

72 minute = int(sec / 60) 

73 sec -= minute * 60 

74 sec = int(sec) 

75 

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

83 

84 

85def flow_notify_worker(config, queue): 

86 global should_terminate 

87 

88 sleep_sec = 0.1 

89 

90 liveness_file = pathlib.Path(config["liveness"]["file"]["flow_notify"]) 

91 

92 logging.info("Start flow notify worker") 

93 i = 0 

94 while True: 

95 if should_terminate.is_set(): 

96 break 

97 

98 try: 

99 if not queue.empty(): 

100 stat = queue.get() 

101 

102 logging.debug("flow notify = %s", str(stat)) 

103 

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

120 

121 if i % (10 / sleep_sec) == 0: 

122 my_lib.footprint.update(liveness_file) 

123 

124 i += 1 

125 

126 logging.info("Terminate flow notify worker") 

127 

128 

129def get_valve_state(): 

130 try: 

131 state = rasp_water.valve.get_control_mode() 

132 

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

140 

141 return {"state": 0, "remain": 0, "result": "fail"} 

142 

143 

144def judge_execute(config, state, auto): 

145 if (state != 1) or (not auto): 

146 return True 

147 

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 

153 

154 my_lib.webapp.log.info( 

155 f"☂ 前回の水やりから {rain_fall_sum:.0f}mm の雨が降ったため、自動での水やりを見合わせます。" 

156 ) 

157 return False 

158 

159 rainfall_judge, rain_fall_sum = rasp_water.weather_forecast.get_rain_fall(config) 

160 

161 if rainfall_judge: 

162 # NOTE: ダミーモードの場合、とにかく水やりする (CI テストの為) 

163 if os.environ.get("DUMMY_MODE", "false") == "true": 

164 return True 

165 

166 my_lib.webapp.log.info( 

167 f"☂ 前後で {rain_fall_sum:.0f}mm の雨が降る予報があるため、自動での水やりを見合わせます。" 

168 ) 

169 return False 

170 

171 return True 

172 

173 

174def set_valve_state(config, state, period, auto, host=""): 

175 is_execute = judge_execute(config, state, auto) 

176 

177 if not is_execute: 

178 my_lib.webapp.event.notify_event(my_lib.webapp.event.EVENT_TYPE.CONTROL) 

179 return get_valve_state() 

180 

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) 

198 

199 my_lib.webapp.event.notify_event(my_lib.webapp.event.EVENT_TYPE.CONTROL) 

200 return get_valve_state() 

201 

202 

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) 

211 

212 config = flask.current_app.config["CONFIG"] 

213 

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

219 

220 

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

226 

227 return flask.jsonify({"cmd": "get", "flow": rasp_water.valve.get_flow(config["flow"]["offset"])["flow"]})