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

1#!/usr/bin/env python3 

2import logging 

3import os 

4import pathlib 

5import threading 

6import time 

7import traceback 

8from multiprocessing import Queue 

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 

26should_terminate = threading.Event() 

27 

28 

29def init(config): 

30 global worker # noqa: PLW0603 

31 

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 

34 

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

39 

40 

41def term(): 

42 global worker # noqa: PLW0603 

43 

44 if worker is None: 

45 return 

46 

47 should_terminate.set() 

48 worker.join() 

49 

50 worker = None 

51 should_terminate.clear() 

52 

53 rasp_water.valve.term() 

54 

55 

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

61 

62 

63def second_str(sec): 

64 minute = 0 

65 if sec >= 60: 

66 minute = int(sec / 60) 

67 sec -= minute * 60 

68 sec = int(sec) 

69 

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

77 

78 

79def flow_notify_worker(config, queue): 

80 global should_terminate 

81 

82 sleep_sec = 0.1 

83 

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

85 

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

87 i = 0 

88 while True: 

89 if should_terminate.is_set(): 

90 break 

91 

92 try: 

93 if not queue.empty(): 

94 stat = queue.get() 

95 

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

97 

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

114 

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

116 my_lib.footprint.update(liveness_file) 

117 

118 i += 1 

119 

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

121 

122 

123def get_valve_state(): 

124 try: 

125 state = rasp_water.valve.get_control_mode() 

126 

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

134 

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

136 

137 

138def judge_execute(config, state, auto): 

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

140 return True 

141 

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 

147 

148 my_lib.webapp.log.info( 

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

150 ) 

151 return False 

152 

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 

158 

159 my_lib.webapp.log.info( 

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

161 ) 

162 return False 

163 

164 return True 

165 

166 

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

168 is_execute = judge_execute(config, state, auto) 

169 

170 if not is_execute: 

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

172 return get_valve_state() 

173 

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) 

191 

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

193 return get_valve_state() 

194 

195 

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) 

204 

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

206 

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

212 

213 

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

219 

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