POSA_LEAKSMS/tests/pm_threshold_test.php
cw2007 ec78be0bda feat(pms7003): 누수 노드에 미세먼지(PM2.5/PM10) 통합
센서1·3(누수 노드)에 PMS7003 를 온습도와 동일 sensor_id 로 통합하고 metric_type='pms7003'
로 구분해 수집한다. LED 는 스위칭 없이 상시 점등 방식으로 확정.

- raspberry_pi4_pms7003/: PMS7003 노드 풀세트(드라이버·센서·install·systemd·vendor·배선·연결테스트·프레임 단위테스트)
- 서버: evaluate_metric_thresholds() 에 pm25/pm10 선택 인자 추가(온습도 전용 호출 후방호환),
  sensor_data.php 수집 경로에 pms7003 metric 반영, config.php/예시에 PM 임계 추가
- SQL: migration_pms7003.sql · schema_sht30.sql 정합, tools/migration_leak_add_{pms7003,sht30} 마이그레이터
- tests/pm_threshold_test.php + run_tests.sh 러너 등록
- .gitignore: 노드별 config_local.py 커밋 차단
2026-07-19 10:46:33 +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) 기본 임계(75/150)에서의 명시 케이스(오버라이드 없을 때만) — 사양 예시 그대로.
if ($P25 === 75.0) {
check(in_array('high_pm25', evaluate_metric_thresholds(null, null, 80.0, 40.0)['breaches'], true),
'default: pm25=80 (>75) -> high_pm25, pm10=40 -> 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";