fix(report): 월간 센서별 요약을 현재 기준(라벨·누수·sensor_id 그룹)으로 정합
센서별 요약이 예전 보고명/이상값을 보이던 문제 수정. MockPDO 하네스로 CSV 출력 검증(각 센서 1행, 누수 컬럼, 범위 반영). - 센서 표시명/위치를 sensor_label()(SENSOR_LABELS 관리주체 라벨)로 통일 — 대시보드·SMS 와 동일 기준. 기존엔 Pi 보고 원본 sensor_name/device_location 을 그대로 노출(예전 값). - 센서별 집계를 GROUP BY sensor_id 로만 묶음(기존: sensor_id+sensor_name/location). 과거 보고명이 달라져도 한 센서가 여러 행으로 쪼개지지 않음(이상 행 제거). - 누수 감지/복구를 월간 요약·센서별 표·CSV 에 반영(누수 모듈 통합 기준). 쿼리는 이미 leak_alert/leak_recovery 를 가져왔으나 표시에서 누락돼 있었음. - 정렬을 온습도 경보 + 누수 감지 합계 기준으로. 범위 문구에 '누수 감지' 추가. - 미사용·구식 report_sms_tags() 제거(누수 태그 누락된 dead 매핑).
This commit is contained in:
parent
d9ed29ba3d
commit
553e59deb8
1 changed files with 40 additions and 30 deletions
|
|
@ -1,10 +1,14 @@
|
||||||
<?php
|
<?php
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// monthly_report.php - 월간 운영 보고서 (SHT30 온습도 전용)
|
// monthly_report.php - 월간 운영 보고서 (온습도 + 누수 통합)
|
||||||
//
|
//
|
||||||
// 집계 범위: 온습도 임계 경보(고온/저온/고습/저습), 정상복귀, 장비 오프라인/복구,
|
// 집계 범위: 온습도 임계 경보(고온/저온/고습/저습), 누수 감지/복구, 정상복귀,
|
||||||
// SMS 실패, 측정 건수, 평균/최저/최고 온도·습도.
|
// 장비 오프라인/복구, SMS 실패, 측정 건수, 평균/최저/최고 온도·습도.
|
||||||
// 데이터 출처: leak_sms_log(message 태그 prefix 기준), leak_sensor_metric(온습도 측정 이력).
|
// 데이터 출처: leak_sms_log(message 태그 prefix 기준 — config.php sms_summary_select_sql),
|
||||||
|
// leak_sensor_metric(온습도 측정 이력).
|
||||||
|
// 센서 표시명/위치는 대시보드·SMS 와 동일하게 sensor_label()(SENSOR_LABELS 오버라이드,
|
||||||
|
// config.local.php)로 통일한다. 센서별 집계는 sensor_id 로만 묶어(과거 보고명이 달라도
|
||||||
|
// 한 센서가 여러 행으로 쪼개지지 않게) 현재 관리주체 라벨로 표시한다.
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
require_once __DIR__ . '/config.php';
|
require_once __DIR__ . '/config.php';
|
||||||
|
|
@ -35,20 +39,6 @@ function month_range(string $month): array {
|
||||||
return [$month, $start->format('Y-m-d H:i:s'), $end->format('Y-m-d H:i:s')];
|
return [$month, $start->format('Y-m-d H:i:s'), $end->format('Y-m-d H:i:s')];
|
||||||
}
|
}
|
||||||
|
|
||||||
// SMS 메시지 태그(prefix) → 집계 키 매핑. 태그 문자열은 config.php 의 SMS_TAG_*
|
|
||||||
// 단일 출처 상수에서 가져온다(sms_send.php 가 발송 시 붙이는 태그와 자동 일치).
|
|
||||||
function report_sms_tags(): array {
|
|
||||||
return [
|
|
||||||
'high_temp' => SMS_TAG_HIGH_TEMP,
|
|
||||||
'low_temp' => SMS_TAG_LOW_TEMP,
|
|
||||||
'high_humidity' => SMS_TAG_HIGH_HUMIDITY,
|
|
||||||
'low_humidity' => SMS_TAG_LOW_HUMIDITY,
|
|
||||||
'recovery' => SMS_TAG_RECOVERY,
|
|
||||||
'device_offline' => SMS_TAG_DEVICE_OFFLINE,
|
|
||||||
'device_online' => SMS_TAG_DEVICE_ONLINE,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
function round_or_null($value, int $precision = 1): ?float {
|
function round_or_null($value, int $precision = 1): ?float {
|
||||||
return $value !== null ? round((float)$value, $precision) : null;
|
return $value !== null ? round((float)$value, $precision) : null;
|
||||||
}
|
}
|
||||||
|
|
@ -74,7 +64,7 @@ function csv_cell($value): string {
|
||||||
$db = get_db();
|
$db = get_db();
|
||||||
[$month, $start_at, $end_at] = month_range($_GET['month'] ?? date('Y-m'));
|
[$month, $start_at, $end_at] = month_range($_GET['month'] ?? date('Y-m'));
|
||||||
$report_generated_at = date('Y-m-d H:i:s');
|
$report_generated_at = date('Y-m-d H:i:s');
|
||||||
$report_scope = '온습도 임계 경보, 장비 오프라인, SMS';
|
$report_scope = '온습도 임계 경보, 누수 감지, 장비 오프라인, SMS';
|
||||||
|
|
||||||
// ── 월간 요약: SMS 태그별 건수 + 실패 건수 ───────────────────────────────────
|
// ── 월간 요약: SMS 태그별 건수 + 실패 건수 ───────────────────────────────────
|
||||||
$stmt = $db->prepare("
|
$stmt = $db->prepare("
|
||||||
|
|
@ -94,6 +84,8 @@ $summary = [
|
||||||
'recovery' => (int)($row['recovery'] ?? 0),
|
'recovery' => (int)($row['recovery'] ?? 0),
|
||||||
'device_offline' => (int)($row['device_offline'] ?? 0),
|
'device_offline' => (int)($row['device_offline'] ?? 0),
|
||||||
'device_online' => (int)($row['device_online'] ?? 0),
|
'device_online' => (int)($row['device_online'] ?? 0),
|
||||||
|
'leak_alert' => (int)($row['leak_alert'] ?? 0),
|
||||||
|
'leak_recovery' => (int)($row['leak_recovery'] ?? 0),
|
||||||
'sms_fail' => (int)($row['sms_fail'] ?? 0),
|
'sms_fail' => (int)($row['sms_fail'] ?? 0),
|
||||||
'metrics' => 0,
|
'metrics' => 0,
|
||||||
'avg_temp' => null,
|
'avg_temp' => null,
|
||||||
|
|
@ -130,12 +122,14 @@ $summary['min_humidity'] = round_or_null($row['min_humidity'] ?? null);
|
||||||
$summary['max_humidity'] = round_or_null($row['max_humidity'] ?? null);
|
$summary['max_humidity'] = round_or_null($row['max_humidity'] ?? null);
|
||||||
|
|
||||||
// ── 센서별 SMS 태그별 건수 + 실패 건수 ───────────────────────────────────────
|
// ── 센서별 SMS 태그별 건수 + 실패 건수 ───────────────────────────────────────
|
||||||
|
// sensor_id 로만 묶는다(보고명 sensor_name 은 시기별로 다를 수 있어 GROUP 키에서 제외 —
|
||||||
|
// 한 센서가 여러 행으로 쪼개지는 것 방지). 표시명은 아래에서 sensor_label() 로 결정.
|
||||||
$stmt = $db->prepare("
|
$stmt = $db->prepare("
|
||||||
SELECT sensor_id, sensor_name,
|
SELECT sensor_id, MAX(sensor_name) AS sensor_name,
|
||||||
" . sms_summary_select_sql() . "
|
" . sms_summary_select_sql() . "
|
||||||
FROM leak_sms_log
|
FROM leak_sms_log
|
||||||
WHERE sent_at >= :start_at AND sent_at < :end_at
|
WHERE sent_at >= :start_at AND sent_at < :end_at
|
||||||
GROUP BY sensor_id, sensor_name
|
GROUP BY sensor_id
|
||||||
");
|
");
|
||||||
$stmt->execute([':start_at' => $start_at, ':end_at' => $end_at]);
|
$stmt->execute([':start_at' => $start_at, ':end_at' => $end_at]);
|
||||||
$sms_by_sensor = [];
|
$sms_by_sensor = [];
|
||||||
|
|
@ -144,8 +138,11 @@ foreach ($stmt->fetchAll() as $r) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 센서별 측정 집계(온습도) ─────────────────────────────────────────────────
|
// ── 센서별 측정 집계(온습도) ─────────────────────────────────────────────────
|
||||||
|
// 마찬가지로 sensor_id 로만 묶고, 보고명/위치는 최신 대표값(MAX)만 폴백용으로 가져온다.
|
||||||
$stmt = $db->prepare("
|
$stmt = $db->prepare("
|
||||||
SELECT sensor_id, sensor_name, device_location,
|
SELECT sensor_id,
|
||||||
|
MAX(sensor_name) AS sensor_name,
|
||||||
|
MAX(device_location) AS device_location,
|
||||||
COUNT(*) AS metrics,
|
COUNT(*) AS metrics,
|
||||||
AVG(temperature_c) AS avg_temp,
|
AVG(temperature_c) AS avg_temp,
|
||||||
MIN(temperature_c) AS min_temp,
|
MIN(temperature_c) AS min_temp,
|
||||||
|
|
@ -156,7 +153,7 @@ $stmt = $db->prepare("
|
||||||
MAX(measured_at) AS last_measured_at
|
MAX(measured_at) AS last_measured_at
|
||||||
FROM leak_sensor_metric
|
FROM leak_sensor_metric
|
||||||
WHERE measured_at >= :start_at AND measured_at < :end_at
|
WHERE measured_at >= :start_at AND measured_at < :end_at
|
||||||
GROUP BY sensor_id, sensor_name, device_location
|
GROUP BY sensor_id
|
||||||
");
|
");
|
||||||
$stmt->execute([':start_at' => $start_at, ':end_at' => $end_at]);
|
$stmt->execute([':start_at' => $start_at, ':end_at' => $end_at]);
|
||||||
$metric_by_sensor = [];
|
$metric_by_sensor = [];
|
||||||
|
|
@ -169,16 +166,22 @@ $sensor_rows = [];
|
||||||
foreach (array_unique(array_merge(array_keys($metric_by_sensor), array_keys($sms_by_sensor))) as $sid) {
|
foreach (array_unique(array_merge(array_keys($metric_by_sensor), array_keys($sms_by_sensor))) as $sid) {
|
||||||
$m = $metric_by_sensor[$sid] ?? [];
|
$m = $metric_by_sensor[$sid] ?? [];
|
||||||
$s = $sms_by_sensor[$sid] ?? [];
|
$s = $sms_by_sensor[$sid] ?? [];
|
||||||
|
// 표시명/위치: SENSOR_LABELS(현재 관리주체) 우선, 없으면 보고 대표값 폴백 — 대시보드·SMS 와 동일
|
||||||
|
[$loc, $name] = sensor_label((int)$sid, $m['device_location'] ?? null, $m['sensor_name'] ?? $s['sensor_name'] ?? null);
|
||||||
|
$threshold_alerts = (int)($s['high_temp'] ?? 0) + (int)($s['low_temp'] ?? 0)
|
||||||
|
+ (int)($s['high_humidity'] ?? 0) + (int)($s['low_humidity'] ?? 0);
|
||||||
|
$leak_alert = (int)($s['leak_alert'] ?? 0);
|
||||||
$sensor_rows[] = [
|
$sensor_rows[] = [
|
||||||
'sensor_id' => $sid,
|
'sensor_id' => $sid,
|
||||||
'sensor_name' => $m['sensor_name'] ?? $s['sensor_name'] ?? ('센서' . $sid),
|
'sensor_name' => $name,
|
||||||
'device_location' => $m['device_location'] ?? '',
|
'device_location' => $loc,
|
||||||
'high_temp' => (int)($s['high_temp'] ?? 0),
|
'high_temp' => (int)($s['high_temp'] ?? 0),
|
||||||
'low_temp' => (int)($s['low_temp'] ?? 0),
|
'low_temp' => (int)($s['low_temp'] ?? 0),
|
||||||
'high_humidity' => (int)($s['high_humidity'] ?? 0),
|
'high_humidity' => (int)($s['high_humidity'] ?? 0),
|
||||||
'low_humidity' => (int)($s['low_humidity'] ?? 0),
|
'low_humidity' => (int)($s['low_humidity'] ?? 0),
|
||||||
'threshold_alerts' => (int)($s['high_temp'] ?? 0) + (int)($s['low_temp'] ?? 0)
|
'threshold_alerts' => $threshold_alerts,
|
||||||
+ (int)($s['high_humidity'] ?? 0) + (int)($s['low_humidity'] ?? 0),
|
'leak_alert' => $leak_alert,
|
||||||
|
'leak_recovery' => (int)($s['leak_recovery'] ?? 0),
|
||||||
'recovery' => (int)($s['recovery'] ?? 0),
|
'recovery' => (int)($s['recovery'] ?? 0),
|
||||||
'device_offline' => (int)($s['device_offline'] ?? 0),
|
'device_offline' => (int)($s['device_offline'] ?? 0),
|
||||||
'device_online' => (int)($s['device_online'] ?? 0),
|
'device_online' => (int)($s['device_online'] ?? 0),
|
||||||
|
|
@ -191,10 +194,11 @@ foreach (array_unique(array_merge(array_keys($metric_by_sensor), array_keys($sms
|
||||||
'min_humidity' => round_or_null($m['min_humidity'] ?? null),
|
'min_humidity' => round_or_null($m['min_humidity'] ?? null),
|
||||||
'max_humidity' => round_or_null($m['max_humidity'] ?? null),
|
'max_humidity' => round_or_null($m['max_humidity'] ?? null),
|
||||||
'last_measured_at' => $m['last_measured_at'] ?? '',
|
'last_measured_at' => $m['last_measured_at'] ?? '',
|
||||||
|
'alert_sort' => $threshold_alerts + $leak_alert, // 정렬용(온습도 경보 + 누수 감지)
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
usort($sensor_rows, function ($a, $b) {
|
usort($sensor_rows, function ($a, $b) {
|
||||||
return [$b['threshold_alerts'], $a['sensor_id']] <=> [$a['threshold_alerts'], $b['sensor_id']];
|
return [$b['alert_sort'], $a['sensor_id']] <=> [$a['alert_sort'], $b['sensor_id']];
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── CSV 다운로드 ─────────────────────────────────────────────────────────────
|
// ── CSV 다운로드 ─────────────────────────────────────────────────────────────
|
||||||
|
|
@ -210,6 +214,8 @@ if (($_GET['format'] ?? '') === 'csv') {
|
||||||
fputcsv($out, ['low_temp_alerts', $summary['low_temp']]);
|
fputcsv($out, ['low_temp_alerts', $summary['low_temp']]);
|
||||||
fputcsv($out, ['high_humidity_alerts', $summary['high_humidity']]);
|
fputcsv($out, ['high_humidity_alerts', $summary['high_humidity']]);
|
||||||
fputcsv($out, ['low_humidity_alerts', $summary['low_humidity']]);
|
fputcsv($out, ['low_humidity_alerts', $summary['low_humidity']]);
|
||||||
|
fputcsv($out, ['leak_alert_sms', $summary['leak_alert']]);
|
||||||
|
fputcsv($out, ['leak_recovery_sms', $summary['leak_recovery']]);
|
||||||
fputcsv($out, ['recovery_sms', $summary['recovery']]);
|
fputcsv($out, ['recovery_sms', $summary['recovery']]);
|
||||||
fputcsv($out, ['device_offline_sms', $summary['device_offline']]);
|
fputcsv($out, ['device_offline_sms', $summary['device_offline']]);
|
||||||
fputcsv($out, ['device_online_sms', $summary['device_online']]);
|
fputcsv($out, ['device_online_sms', $summary['device_online']]);
|
||||||
|
|
@ -225,6 +231,7 @@ if (($_GET['format'] ?? '') === 'csv') {
|
||||||
fputcsv($out, [
|
fputcsv($out, [
|
||||||
'sensor_id', 'sensor_name', 'location',
|
'sensor_id', 'sensor_name', 'location',
|
||||||
'high_temp', 'low_temp', 'high_humidity', 'low_humidity', 'threshold_alerts',
|
'high_temp', 'low_temp', 'high_humidity', 'low_humidity', 'threshold_alerts',
|
||||||
|
'leak_alert', 'leak_recovery',
|
||||||
'recovery', 'device_offline', 'device_online', 'sms_fail',
|
'recovery', 'device_offline', 'device_online', 'sms_fail',
|
||||||
'metrics', 'avg_temp_c', 'min_temp_c', 'max_temp_c',
|
'metrics', 'avg_temp_c', 'min_temp_c', 'max_temp_c',
|
||||||
'avg_humidity_percent', 'min_humidity_percent', 'max_humidity_percent',
|
'avg_humidity_percent', 'min_humidity_percent', 'max_humidity_percent',
|
||||||
|
|
@ -234,6 +241,7 @@ if (($_GET['format'] ?? '') === 'csv') {
|
||||||
fputcsv($out, [
|
fputcsv($out, [
|
||||||
$s['sensor_id'], csv_cell($s['sensor_name']), csv_cell($s['device_location']),
|
$s['sensor_id'], csv_cell($s['sensor_name']), csv_cell($s['device_location']),
|
||||||
$s['high_temp'], $s['low_temp'], $s['high_humidity'], $s['low_humidity'], $s['threshold_alerts'],
|
$s['high_temp'], $s['low_temp'], $s['high_humidity'], $s['low_humidity'], $s['threshold_alerts'],
|
||||||
|
$s['leak_alert'], $s['leak_recovery'],
|
||||||
$s['recovery'], $s['device_offline'], $s['device_online'], $s['sms_fail'],
|
$s['recovery'], $s['device_offline'], $s['device_online'], $s['sms_fail'],
|
||||||
$s['metrics'],
|
$s['metrics'],
|
||||||
$s['avg_temp'] ?? '', $s['min_temp'] ?? '', $s['max_temp'] ?? '',
|
$s['avg_temp'] ?? '', $s['min_temp'] ?? '', $s['max_temp'] ?? '',
|
||||||
|
|
@ -304,6 +312,7 @@ if (($_GET['format'] ?? '') === 'csv') {
|
||||||
<div class="report-stat"><span>임계 경보(합계)</span><strong><?= $summary['threshold_alerts'] ?>건</strong></div>
|
<div class="report-stat"><span>임계 경보(합계)</span><strong><?= $summary['threshold_alerts'] ?>건</strong></div>
|
||||||
<div class="report-stat"><span>고온/저온 경보</span><strong><?= $summary['high_temp'] ?>/<?= $summary['low_temp'] ?>건</strong></div>
|
<div class="report-stat"><span>고온/저온 경보</span><strong><?= $summary['high_temp'] ?>/<?= $summary['low_temp'] ?>건</strong></div>
|
||||||
<div class="report-stat"><span>고습/저습 경보</span><strong><?= $summary['high_humidity'] ?>/<?= $summary['low_humidity'] ?>건</strong></div>
|
<div class="report-stat"><span>고습/저습 경보</span><strong><?= $summary['high_humidity'] ?>/<?= $summary['low_humidity'] ?>건</strong></div>
|
||||||
|
<div class="report-stat"><span>누수 감지/복구</span><strong class="<?= $summary['leak_alert'] > 0 ? 'err-color' : '' ?>"><?= $summary['leak_alert'] ?>/<?= $summary['leak_recovery'] ?>건</strong></div>
|
||||||
<div class="report-stat"><span>정상복귀</span><strong><?= $summary['recovery'] ?>건</strong></div>
|
<div class="report-stat"><span>정상복귀</span><strong><?= $summary['recovery'] ?>건</strong></div>
|
||||||
<div class="report-stat"><span>장비 오프라인/복구</span><strong><?= $summary['device_offline'] ?>/<?= $summary['device_online'] ?>건</strong></div>
|
<div class="report-stat"><span>장비 오프라인/복구</span><strong><?= $summary['device_offline'] ?>/<?= $summary['device_online'] ?>건</strong></div>
|
||||||
<div class="report-stat"><span>SMS 실패</span><strong><?= $summary['sms_fail'] ?>건</strong></div>
|
<div class="report-stat"><span>SMS 실패</span><strong><?= $summary['sms_fail'] ?>건</strong></div>
|
||||||
|
|
@ -318,7 +327,7 @@ if (($_GET['format'] ?? '') === 'csv') {
|
||||||
<table>
|
<table>
|
||||||
<thead><tr>
|
<thead><tr>
|
||||||
<th>센서</th><th>위치</th>
|
<th>센서</th><th>위치</th>
|
||||||
<th>임계 경보</th><th>고온/저온</th><th>고습/저습</th>
|
<th>임계 경보</th><th>고온/저온</th><th>고습/저습</th><th>누수 감지/복구</th>
|
||||||
<th>정상복귀</th><th>오프라인/복구</th><th>SMS 실패</th>
|
<th>정상복귀</th><th>오프라인/복구</th><th>SMS 실패</th>
|
||||||
<th>측정</th><th>평균 온도/습도</th><th>온도 최저/최고</th><th>습도 최저/최고</th>
|
<th>측정</th><th>평균 온도/습도</th><th>온도 최저/최고</th><th>습도 최저/최고</th>
|
||||||
<th>최근 측정</th>
|
<th>최근 측정</th>
|
||||||
|
|
@ -331,6 +340,7 @@ if (($_GET['format'] ?? '') === 'csv') {
|
||||||
<td><?= $s['threshold_alerts'] ?></td>
|
<td><?= $s['threshold_alerts'] ?></td>
|
||||||
<td><?= $s['high_temp'] ?>/<?= $s['low_temp'] ?></td>
|
<td><?= $s['high_temp'] ?>/<?= $s['low_temp'] ?></td>
|
||||||
<td><?= $s['high_humidity'] ?>/<?= $s['low_humidity'] ?></td>
|
<td><?= $s['high_humidity'] ?>/<?= $s['low_humidity'] ?></td>
|
||||||
|
<td class="<?= $s['leak_alert'] > 0 ? 'err-color' : '' ?>"><?= $s['leak_alert'] ?>/<?= $s['leak_recovery'] ?></td>
|
||||||
<td><?= $s['recovery'] ?></td>
|
<td><?= $s['recovery'] ?></td>
|
||||||
<td><?= $s['device_offline'] ?>/<?= $s['device_online'] ?></td>
|
<td><?= $s['device_offline'] ?>/<?= $s['device_online'] ?></td>
|
||||||
<td><?= $s['sms_fail'] ?></td>
|
<td><?= $s['sms_fail'] ?></td>
|
||||||
|
|
@ -342,7 +352,7 @@ if (($_GET['format'] ?? '') === 'csv') {
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<?php if (empty($sensor_rows)): ?>
|
<?php if (empty($sensor_rows)): ?>
|
||||||
<tr><td colspan="13" class="no-data">해당 월 측정·경보 내역이 없습니다.</td></tr>
|
<tr><td colspan="14" class="no-data">해당 월 측정·경보 내역이 없습니다.</td></tr>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue