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
« prev ^ index » next coverage.py v7.9.1, created at 2025-07-04 12:06 +0900
1"""
2水やりメトリクス収集モジュール
4このモジュールは以下のメトリクスを収集し、SQLiteデータベースに保存します:
5- 水やりをした回数
6- 1回あたりの水やりをした時間
7- 1回あたりの水やりをした量
8- 手動で水やりをしたのか、自動で水やりをしたのか
9- エラー発生回数
111日に複数回水やりをした場合、それぞれの水やり毎にデータを記録します。
12"""
14from __future__ import annotations
16import datetime
17import logging
18import sqlite3
19import threading
20from pathlib import Path
23class MetricsCollector:
24 """水やりメトリクス収集クラス"""
26 def __init__(self, db_path: Path):
27 """コンストラクタ
29 Args:
30 ----
31 db_path: SQLiteデータベースファイルパス
33 """
34 self.db_path = db_path
35 self.lock = threading.Lock()
36 self._init_database()
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 """)
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 """)
66 # インデックスの作成
67 conn.execute("""
68 CREATE INDEX IF NOT EXISTS idx_watering_metrics_date
69 ON watering_metrics(date)
70 """)
72 conn.execute("""
73 CREATE INDEX IF NOT EXISTS idx_watering_metrics_type
74 ON watering_metrics(operation_type)
75 """)
77 conn.execute("""
78 CREATE INDEX IF NOT EXISTS idx_error_metrics_date
79 ON error_metrics(date)
80 """)
82 def _get_today_date(self) -> str:
83 """今日の日付を文字列で取得"""
84 return datetime.date.today().isoformat()
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 水やり操作を記録
96 Args:
97 ----
98 operation_type: "manual" または "auto"
99 duration_seconds: 水やりの時間(秒)
100 volume_liters: 水やりの量(リットル)、Noneの場合は記録しない
101 timestamp: 操作時刻(指定しない場合は現在時刻)
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()
107 date = timestamp.date().isoformat()
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 )
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 )
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 エラー発生を記録
136 Args:
137 ----
138 error_type: エラーの種類(例: "valve_control", "schedule", "sensor")
139 error_message: エラーメッセージ
140 timestamp: エラー発生時刻(指定しない場合は現在時刻)
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()
146 date = timestamp.date().isoformat()
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 )
158 logging.info("Recorded error metrics: type=%s, message=%s", error_type, error_message)
160 def get_watering_metrics(self, start_date: str, end_date: str) -> list:
161 """
162 指定期間の水やりメトリクスを取得
164 Args:
165 ----
166 start_date: 開始日(YYYY-MM-DD形式)
167 end_date: 終了日(YYYY-MM-DD形式)
169 Returns:
170 -------
171 水やりメトリクスデータのリスト
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()]
186 def get_error_metrics(self, start_date: str, end_date: str) -> list:
187 """
188 指定期間のエラーメトリクスを取得
190 Args:
191 ----
192 start_date: 開始日(YYYY-MM-DD形式)
193 end_date: 終了日(YYYY-MM-DD形式)
195 Returns:
196 -------
197 エラーメトリクスデータのリスト
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()]
212 def get_daily_summary(self, date: str) -> dict:
213 """
214 指定日の統計サマリーを取得
216 Args:
217 ----
218 date: 日付(YYYY-MM-DD形式)
220 Returns:
221 -------
222 統計サマリー(水やり回数、総時間、総量など)
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())
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())
257 return {
258 "date": date,
259 "watering": watering_stats,
260 "errors": error_stats,
261 }
263 def get_recent_watering_metrics(self, days: int = 30) -> list:
264 """
265 最近N日間の水やりメトリクスを取得
267 Args:
268 ----
269 days: 取得する日数
271 Returns:
272 -------
273 水やりメトリクスデータのリスト
275 """
276 end_date = datetime.date.today()
277 start_date = end_date - datetime.timedelta(days=days)
279 return self.get_watering_metrics(start_date.isoformat(), end_date.isoformat())
281 def get_recent_error_metrics(self, days: int = 30) -> list:
282 """
283 最近N日間のエラーメトリクスを取得
285 Args:
286 ----
287 days: 取得する日数
289 Returns:
290 -------
291 エラーメトリクスデータのリスト
293 """
294 end_date = datetime.date.today()
295 start_date = end_date - datetime.timedelta(days=days)
297 return self.get_error_metrics(start_date.isoformat(), end_date.isoformat())
300# グローバルインスタンス
301_collector_instance: MetricsCollector | None = None
304def get_collector(metrics_data_path) -> MetricsCollector:
305 """メトリクス収集インスタンスを取得"""
306 global _collector_instance
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)
313 return _collector_instance
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 )
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)