POSA_LEAKSMS/tests/pm_threshold_test.php
cw2007 f7fbeacbdd feat: PM 급상승을 화재의심으로 전환 + SMS 단문화 + 센서고착 검출 비활성
서버실/항온항습: 여과·양압이라 평상 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
2026-07-19 23:34:57 +09:00

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