서버실/항온항습: 여과·양압이라 평상 PM≈0 → PM 급상승을 화재 조기징후로 본다. - 임계 하향: METRIC_PM25_HIGH 75→35, METRIC_PM10_HIGH 150→75 (연기 조기감지). - SMS 태그 [미세먼지경보] → [화재의심]. 대시보드 라벨 통일(경보=화재의심, PM 측정=화재감시). metric_status(high_pm25/10) 코드·값·판정 로직 불변 — 표시단만 변경. - SMS 단문화: 전 본문을 EUC-KR 90B 이내로 축약(관리주체 제거, 날짜 m-d H:i, 태그+위치 1줄). 실측 45~70B ≤ 90B → 게이트웨이(Cafe24 smsType=S) 단문 절단 해소. - 센서고착(live-but-stuck) 검출 기본 비활성(STUCK_DETECT_ENABLED=false): 항온항습 문서고는 온습도 원래 평탄 → 평탄값을 고착으로 오탐하던 [센서고착] 문자 차단. - 테스트: 태그·기본임계 단언 갱신. 회귀 스위트 PASS(9 run/0 fail), php -l 통과. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Dj4n17ZvGAhga6D4pNRjn
100 lines
5.2 KiB
PHP
100 lines
5.2 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
// PMS7003 미세먼지 임계 판정 회귀 테스트 (DB/네트워크 없이 순수 로직만).
|
|
// - evaluate_metric_thresholds(): PM2.5/PM10 고농도 판정(온습도와 독립, 후방호환 인자)
|
|
// - SMS_TAG_HIGH_PM: 미세먼지 경보 태그(단일 출처 상수)
|
|
// - sms_summary_select_sql(): 대시보드/월간보고 집계에 high_pm 컬럼 포함
|
|
// 임계값은 config.php 의 METRIC_PM*_HIGH 상수를 그대로 사용하며, 어떤 운영 임계 설정에서도
|
|
// 의미가 유지되도록 상대값(임계±여유)으로 검증한다(threshold_sms_test.php 와 동일 방침).
|
|
|
|
$root = dirname(__DIR__);
|
|
require_once $root . '/php/config.php'; // METRIC_PM*_HIGH / SMS_TAG_HIGH_PM / SMS_METRIC_TAGS
|
|
require_once $root . '/php/sms_send.php'; // evaluate_metric_thresholds()
|
|
|
|
$failures = [];
|
|
function check(bool $cond, string $msg): void {
|
|
global $failures;
|
|
if (!$cond) { $failures[] = $msg; }
|
|
}
|
|
function eq($a, $b, string $msg): void {
|
|
check($a === $b, $msg . ' (expected ' . var_export($b, true) . ', got ' . var_export($a, true) . ')');
|
|
}
|
|
|
|
// ── 상수/함수 존재 ─────────────────────────────────────────
|
|
check(function_exists('evaluate_metric_thresholds'), 'evaluate_metric_thresholds() should exist.');
|
|
check(defined('SMS_TAG_HIGH_PM'), 'SMS_TAG_HIGH_PM should be defined.');
|
|
eq(SMS_TAG_HIGH_PM, '[화재의심]', 'SMS_TAG_HIGH_PM tag value');
|
|
|
|
check(defined('METRIC_PM25_HIGH'), 'METRIC_PM25_HIGH should be defined.');
|
|
check(defined('METRIC_PM10_HIGH'), 'METRIC_PM10_HIGH should be defined.');
|
|
check(defined('METRIC_PM_HYSTERESIS'), 'METRIC_PM_HYSTERESIS should be defined.');
|
|
|
|
$P25 = (float)METRIC_PM25_HIGH;
|
|
$P10 = (float)METRIC_PM10_HIGH;
|
|
check($P25 > 0, 'PM2.5 threshold sane (> 0).');
|
|
check($P10 > 0, 'PM10 threshold sane (> 0).');
|
|
|
|
// 임계 상대 입력. PM 은 임계 "초과"(strict >)일 때만 발화한다.
|
|
$hi25 = $P25 + 5.0; $lo25 = $P25 - 5.0;
|
|
$hi10 = $P10 + 5.0; $lo10 = $P10 - 5.0;
|
|
|
|
// ── 온습도 없이(순수 PM) — 후방호환: 앞 두 인자 null ──────────
|
|
// 1) PM2.5 초과, PM10 정상 → high_pm25 만 (breaching case)
|
|
$b = evaluate_metric_thresholds(null, null, $hi25, $lo10)['breaches'];
|
|
check(in_array('high_pm25', $b, true), 'pm25 above threshold -> breaches contains high_pm25');
|
|
check(!in_array('high_pm10', $b, true), 'pm10 below threshold -> no high_pm10');
|
|
|
|
// 2) PM10 초과, PM2.5 정상 → high_pm10 만 (pm10 > 임계 필요)
|
|
$b = evaluate_metric_thresholds(null, null, $lo25, $hi10)['breaches'];
|
|
check(in_array('high_pm10', $b, true), 'pm10 above threshold -> breaches contains high_pm10');
|
|
check(!in_array('high_pm25', $b, true), 'pm25 below threshold -> no high_pm25');
|
|
|
|
// 3) 둘 다 초과 → 두 종류 모두
|
|
$b = evaluate_metric_thresholds(null, null, $hi25, $hi10)['breaches'];
|
|
check(in_array('high_pm25', $b, true) && in_array('high_pm10', $b, true),
|
|
'both pm above -> both breaches');
|
|
|
|
// 4) 정상(둘 다 임계 이하) → PM 경보 없음 (normal case)
|
|
$b = evaluate_metric_thresholds(null, null, $lo25, $lo10)['breaches'];
|
|
eq($b, [], 'both pm below threshold -> no breaches');
|
|
|
|
// 5) 경계: 임계와 정확히 같으면 발화하지 않는다(strict >)
|
|
$b = evaluate_metric_thresholds(null, null, $P25, $P10)['breaches'];
|
|
check(!in_array('high_pm25', $b, true) && !in_array('high_pm10', $b, true),
|
|
'pm exactly at threshold -> no breach (strict greater-than)');
|
|
|
|
// 6) PM 결측(null)이면 PM 경보 없음 — 온습도 전용 호출(2인자)도 종전대로 동작
|
|
$b = evaluate_metric_thresholds(null, null)['breaches'];
|
|
eq($b, [], 'null pm (2-arg call, backward compatible) -> no pm breach');
|
|
|
|
// 7) 온습도 경보와 독립 공존: 고온 + 고 PM2.5 → 두 종류 모두 포함
|
|
$b = evaluate_metric_thresholds((float)METRIC_TEMP_HIGH_C + 5, null, $hi25, $lo10)['breaches'];
|
|
check(in_array('high_temp', $b, true) && in_array('high_pm25', $b, true),
|
|
'high temp + high pm25 -> both breaches present');
|
|
|
|
// 8) 기본 임계(화재의심 35/75)에서의 명시 케이스(오버라이드 없을 때만) — 사양 예시 그대로.
|
|
if ($P25 === 35.0) {
|
|
check(in_array('high_pm25', evaluate_metric_thresholds(null, null, 40.0, 20.0)['breaches'], true),
|
|
'default: pm25=40 (>35) -> high_pm25, pm10=20 -> no high_pm10');
|
|
}
|
|
|
|
// ── 집계 SELECT 에 high_pm 컬럼 포함 ────────────────────────
|
|
check(function_exists('sms_summary_select_sql'), 'sms_summary_select_sql() should exist.');
|
|
check(str_contains(sms_summary_select_sql(), 'AS high_pm'),
|
|
"sms_summary_select_sql() should contain 'AS high_pm'");
|
|
|
|
// ── SMS_METRIC_TAGS 매핑: 두 PM 종류가 미세먼지 태그로 매핑 ──
|
|
check(defined('SMS_METRIC_TAGS'), 'SMS_METRIC_TAGS should be defined.');
|
|
check((SMS_METRIC_TAGS['high_pm25'] ?? null) === SMS_TAG_HIGH_PM, 'high_pm25 -> SMS_TAG_HIGH_PM');
|
|
check((SMS_METRIC_TAGS['high_pm10'] ?? null) === SMS_TAG_HIGH_PM, 'high_pm10 -> SMS_TAG_HIGH_PM');
|
|
|
|
if ($failures) {
|
|
fwrite(STDERR, "PM threshold test failures:\n");
|
|
foreach ($failures as $f) {
|
|
fwrite(STDERR, "- {$f}\n");
|
|
}
|
|
exit(1);
|
|
}
|
|
|
|
echo "PMS7003 PM threshold checks passed.\n";
|