Coverage for flask/src/rasp_water/metrics/collector.py: 71%

72 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-07-04 12:06 +0900

1""" 

2水やりメトリクス収集モジュール 

3 

4このモジュールは以下のメトリクスを収集し、SQLiteデータベースに保存します: 

5- 水やりをした回数 

6- 1回あたりの水やりをした時間 

7- 1回あたりの水やりをした量 

8- 手動で水やりをしたのか、自動で水やりをしたのか 

9- エラー発生回数 

10 

111日に複数回水やりをした場合、それぞれの水やり毎にデータを記録します。 

12""" 

13 

14from __future__ import annotations 

15 

16import datetime 

17import logging 

18import sqlite3 

19import threading 

20from pathlib import Path 

21 

22 

23class MetricsCollector: 

24 """水やりメトリクス収集クラス""" 

25 

26 def __init__(self, db_path: Path): 

27 """コンストラクタ 

28 

29 Args: 

30 ---- 

31 db_path: SQLiteデータベースファイルパス 

32 

33 """ 

34 self.db_path = db_path 

35 self.lock = threading.Lock() 

36 self._init_database() 

37 

38 def _init_database(self): 

39 """データベース初期化""" 

40 with sqlite3.connect(self.db_path) as conn: 

41 # 水やり操作のメトリクステーブル 

42 conn.execute(""" 

43 CREATE TABLE IF NOT EXISTS watering_metrics ( 

44 id INTEGER PRIMARY KEY AUTOINCREMENT, 

45 timestamp TIMESTAMP NOT NULL, 

46 date TEXT NOT NULL, 

47 operation_type TEXT NOT NULL CHECK (operation_type IN ('manual', 'auto')), 

48 duration_seconds INTEGER NOT NULL, 

49 volume_liters REAL, 

50 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 

51 ) 

52 """) 

53 

54 # エラー発生のメトリクステーブル 

55 conn.execute(""" 

56 CREATE TABLE IF NOT EXISTS error_metrics ( 

57 id INTEGER PRIMARY KEY AUTOINCREMENT, 

58 date TEXT NOT NULL, 

59 error_type TEXT NOT NULL, 

60 error_message TEXT, 

61 timestamp TIMESTAMP NOT NULL, 

62 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 

63 ) 

64 """) 

65 

66 # インデックスの作成 

67 conn.execute(""" 

68 CREATE INDEX IF NOT EXISTS idx_watering_metrics_date 

69 ON watering_metrics(date) 

70 """) 

71 

72 conn.execute(""" 

73 CREATE INDEX IF NOT EXISTS idx_watering_metrics_type 

74 ON watering_metrics(operation_type) 

75 """) 

76 

77 conn.execute(""" 

78 CREATE INDEX IF NOT EXISTS idx_error_metrics_date 

79 ON error_metrics(date) 

80 """) 

81 

82 def _get_today_date(self) -> str: 

83 """今日の日付を文字列で取得""" 

84 return datetime.date.today().isoformat() 

85 

86 def record_watering( 

87 self, 

88 operation_type: str, 

89 duration_seconds: int, 

90 volume_liters: float | None = None, 

91 timestamp: datetime.datetime | None = None, 

92 ): 

93 """ 

94 水やり操作を記録 

95 

96 Args: 

97 ---- 

98 operation_type: "manual" または "auto" 

99 duration_seconds: 水やりの時間(秒) 

100 volume_liters: 水やりの量(リットル)、Noneの場合は記録しない 

101 timestamp: 操作時刻(指定しない場合は現在時刻) 

102 

103 """ 

104 if timestamp is None: 104 ↛ 107line 104 didn't jump to line 107 because the condition on line 104 was always true

105 timestamp = datetime.datetime.now() 

106 

107 date = timestamp.date().isoformat() 

108 

109 with self.lock: 

110 with sqlite3.connect(self.db_path) as conn: 

111 conn.execute( 

112 """ 

113 INSERT INTO watering_metrics 

114 (timestamp, date, operation_type, duration_seconds, volume_liters) 

115 VALUES (?, ?, ?, ?, ?) 

116 """, 

117 (timestamp, date, operation_type, duration_seconds, volume_liters), 

118 ) 

119 

120 logging.info( 

121 "Recorded watering metrics: type=%s, duration=%ds, volume=%s", 

122 operation_type, 

123 duration_seconds, 

124 f"{volume_liters:.2f}L" if volume_liters else "N/A", 

125 ) 

126 

127 def record_error( 

128 self, 

129 error_type: str, 

130 error_message: str | None = None, 

131 timestamp: datetime.datetime | None = None, 

132 ): 

133 """ 

134 エラー発生を記録 

135 

136 Args: 

137 ---- 

138 error_type: エラーの種類(例: "valve_control", "schedule", "sensor") 

139 error_message: エラーメッセージ 

140 timestamp: エラー発生時刻(指定しない場合は現在時刻) 

141 

142 """ 

143 if timestamp is None: 143 ↛ 146line 143 didn't jump to line 146 because the condition on line 143 was always true

144 timestamp = datetime.datetime.now() 

145 

146 date = timestamp.date().isoformat() 

147 

148 with self.lock: 

149 with sqlite3.connect(self.db_path) as conn: 

150 conn.execute( 

151 """ 

152 INSERT INTO error_metrics (date, error_type, error_message, timestamp) 

153 VALUES (?, ?, ?, ?) 

154 """, 

155 (date, error_type, error_message, timestamp), 

156 ) 

157 

158 logging.info("Recorded error metrics: type=%s, message=%s", error_type, error_message) 

159 

160 def get_watering_metrics(self, start_date: str, end_date: str) -> list: 

161 """ 

162 指定期間の水やりメトリクスを取得 

163 

164 Args: 

165 ---- 

166 start_date: 開始日(YYYY-MM-DD形式) 

167 end_date: 終了日(YYYY-MM-DD形式) 

168 

169 Returns: 

170 ------- 

171 水やりメトリクスデータのリスト 

172 

173 """ 

174 with sqlite3.connect(self.db_path) as conn: 

175 conn.row_factory = sqlite3.Row 

176 cursor = conn.execute( 

177 """ 

178 SELECT * FROM watering_metrics 

179 WHERE date BETWEEN ? AND ? 

180 ORDER BY timestamp 

181 """, 

182 (start_date, end_date), 

183 ) 

184 return [dict(row) for row in cursor.fetchall()] 

185 

186 def get_error_metrics(self, start_date: str, end_date: str) -> list: 

187 """ 

188 指定期間のエラーメトリクスを取得 

189 

190 Args: 

191 ---- 

192 start_date: 開始日(YYYY-MM-DD形式) 

193 end_date: 終了日(YYYY-MM-DD形式) 

194 

195 Returns: 

196 ------- 

197 エラーメトリクスデータのリスト 

198 

199 """ 

200 with sqlite3.connect(self.db_path) as conn: 

201 conn.row_factory = sqlite3.Row 

202 cursor = conn.execute( 

203 """ 

204 SELECT * FROM error_metrics 

205 WHERE date BETWEEN ? AND ? 

206 ORDER BY timestamp 

207 """, 

208 (start_date, end_date), 

209 ) 

210 return [dict(row) for row in cursor.fetchall()] 

211 

212 def get_daily_summary(self, date: str) -> dict: 

213 """ 

214 指定日の統計サマリーを取得 

215 

216 Args: 

217 ---- 

218 date: 日付(YYYY-MM-DD形式) 

219 

220 Returns: 

221 ------- 

222 統計サマリー(水やり回数、総時間、総量など) 

223 

224 """ 

225 with sqlite3.connect(self.db_path) as conn: 

226 # 水やり統計 

227 cursor = conn.execute( 

228 """ 

229 SELECT  

230 COUNT(*) as total_count, 

231 SUM(CASE WHEN operation_type = 'manual' THEN 1 ELSE 0 END) as manual_count, 

232 SUM(CASE WHEN operation_type = 'auto' THEN 1 ELSE 0 END) as auto_count, 

233 SUM(duration_seconds) as total_duration_seconds, 

234 SUM(volume_liters) as total_volume_liters, 

235 AVG(duration_seconds) as avg_duration_seconds, 

236 AVG(volume_liters) as avg_volume_liters 

237 FROM watering_metrics 

238 WHERE date = ? 

239 """, 

240 (date,), 

241 ) 

242 watering_stats = dict(cursor.fetchone()) 

243 

244 # エラー統計 

245 cursor = conn.execute( 

246 """ 

247 SELECT  

248 COUNT(*) as error_count, 

249 COUNT(DISTINCT error_type) as error_type_count 

250 FROM error_metrics 

251 WHERE date = ? 

252 """, 

253 (date,), 

254 ) 

255 error_stats = dict(cursor.fetchone()) 

256 

257 return { 

258 "date": date, 

259 "watering": watering_stats, 

260 "errors": error_stats, 

261 } 

262 

263 def get_recent_watering_metrics(self, days: int = 30) -> list: 

264 """ 

265 最近N日間の水やりメトリクスを取得 

266 

267 Args: 

268 ---- 

269 days: 取得する日数 

270 

271 Returns: 

272 ------- 

273 水やりメトリクスデータのリスト 

274 

275 """ 

276 end_date = datetime.date.today() 

277 start_date = end_date - datetime.timedelta(days=days) 

278 

279 return self.get_watering_metrics(start_date.isoformat(), end_date.isoformat()) 

280 

281 def get_recent_error_metrics(self, days: int = 30) -> list: 

282 """ 

283 最近N日間のエラーメトリクスを取得 

284 

285 Args: 

286 ---- 

287 days: 取得する日数 

288 

289 Returns: 

290 ------- 

291 エラーメトリクスデータのリスト 

292 

293 """ 

294 end_date = datetime.date.today() 

295 start_date = end_date - datetime.timedelta(days=days) 

296 

297 return self.get_error_metrics(start_date.isoformat(), end_date.isoformat()) 

298 

299 

300# グローバルインスタンス 

301_collector_instance: MetricsCollector | None = None 

302 

303 

304def get_collector(metrics_data_path) -> MetricsCollector: 

305 """メトリクス収集インスタンスを取得""" 

306 global _collector_instance 

307 

308 if _collector_instance is None: 

309 db_path = Path(metrics_data_path) 

310 _collector_instance = MetricsCollector(db_path) 

311 logging.info("Metrics collector initialized: %s", db_path) 

312 

313 return _collector_instance 

314 

315 

316def record_watering( 

317 operation_type: str, 

318 duration_seconds: int, 

319 metrics_data_path, 

320 volume_liters: float | None = None, 

321 timestamp: datetime.datetime | None = None, 

322): 

323 """水やり操作を記録(便利関数)""" 

324 get_collector(metrics_data_path).record_watering( 

325 operation_type, duration_seconds, volume_liters, timestamp 

326 ) 

327 

328 

329def record_error( 

330 error_type: str, 

331 metrics_data_path, 

332 error_message: str | None = None, 

333 timestamp: datetime.datetime | None = None, 

334): 

335 """エラー発生を記録(便利関数)""" 

336 get_collector(metrics_data_path).record_error(error_type, error_message, timestamp)