POSA_LEAKSMS/docs/superpowers/specs/2026-07-15-leak-node-sht30-integration-design.md
cw2007 0b1e86c84a feat(sht30): 누수 노드에 온습도 동일-id 통합 (소스·서버예시·테스트·문서)
누수 노드(Pi-A=sensor_id 1, Pi-C=sensor_id 3)에 SHT30 온습도를 "같은
sensor_id"로 보고하는 구성을 반영. 노드는 기존 sht30_sensor.py를 별도
systemd 서비스로 얹고(불안정한 누수 코드 무변경), config_local로 SENSOR_ID만 지정.

- raspberry_pi4_sht30/config.py: config_local로 SENSOR_ID/SENSOR_NAME 오버라이드
  지원(미지정 시 기본 2 유지 — Pi-B 하위호환)
- php/config.local.example.php: METRIC_SENSOR_IDS=[1,2,3] 예시 +
  SENSOR_HEARTBEAT_TIMEOUT_SEC[1,3]=1800(1·3이 온습도 목록에 들어가도 누수
  오프라인 감지 속도 유지)
- tests/heartbeat_timeout_test.php: 동일-id 통합 시 1·3 누수 타임아웃 유지 검증
- docs/superpowers/{specs,plans}: 설계 문서 + 구현 계획

설치 패키지(deploy/* — gitignored)는 온디스크 산출물로 별도 반영(sht30 서브폴더,
install.sh 체이닝, WIRING/SETUP/README, 서버 SETUP 절차).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nd7QvbgEWCjSQtUYnGNvvV
2026-07-15 10:16:49 +09:00

140 lines
8.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 설계: 누수 노드에 SHT30 온습도 통합 (동일 sensor_id)
**날짜:** 2026-07-15
**브랜치:** feat/pi-c-sensor3-leak-node
**상태:** 설계 승인됨 (사용자 확인 완료)
## 목표
기존 누수 노드(Pi-A=sensor_id 1, Pi-C=sensor_id 3)에 SHT30(P4422-3) 온습도 센서를
물리적으로 추가하고, 온습도를 **같은 sensor_id(1, 3)** 로 서버에 보고한다. 대시보드·경보·
월간보고·설치 패키지까지 전부 반영한다. Pi-B(온습도 전용, sensor_id 2)는 건드리지 않는다.
## 핵심 결정 (확정)
1. **동일 sensor_id.** 온습도는 새 id를 만들지 않고 누수와 같은 id(1, 3)로 보고한다.
- `leak_sensor_status`(sensor_id 키)와 `leak_sensor_metric`(sensor_id+측정시각 키)는
테이블이 분리돼 있어, 한 sensor_id가 누수 상태 + 온습도 측정을 동시에 가질 수 있다.
- 새 id/재번호 없음. Pi-B(2) 유지.
2. **노드: 별도 서비스.** 불안정한 누수 코드(`leak_sensor.py`)는 건드리지 않는다.
기존 `sht30_sensor.py`를 **두 번째 systemd 서비스**로 얹는다. sht30 코드는 무변경 —
config의 `SENSOR_ID`만 노드별(1/3)로 설정.
3. **라벨:** 서버 SENSOR_LABELS 오버라이드 없이, 노드가 보고하는 device_location/
sensor_name을 그대로 쓴다. 단, 같은 sensor_id를 두 서비스가 upsert하므로 라벨이
흔들리지 않게 **두 서비스의 device_location을 노드별로 동일하게** 맞춘다.
4. **경보/수신자:** 전역 METRIC_* 임계로 서버가 고온/고습 등 판정 → SMS. 수신자는
기존 SENSOR_RECIPIENTS의 1·3 그룹을 그대로 사용(추가 매핑 불필요).
## 아키텍처 / 데이터 흐름
```
Pi-A (한 대)
├ leak-sensor.service → leak_sensor.py → POST (sensor_id=1) leak_alert/recovery/periodic (누수, 온습도 없음)
└ sht30-sensor.service → sht30_sensor.py → POST (sensor_id=1) periodic (metric_type=sht30, 온·습도)
Pi-C (한 대): 위와 동일, sensor_id=3
Pi-B: 변경 없음 (sensor_id=2)
서버 sensor_data.php:
- sensor_id=1 상태 upsert: 누수 이벤트만 is_leak 갱신, sht30 periodic은 is_leak 보존(기존 로직)
- sht30 payload → leak_sensor_metric(sensor_id=1) 저장 + 임계 판정 → 온습도 SMS(SENSOR_RECIPIENTS[1])
- last_seen 은 두 서비스가 모두 갱신
대시보드: sensor_id=1 → 누수 카드(LEAK_SENSOR_IDS) + 온습도 카드(METRIC_SENSOR_IDS)
```
## 변경 범위
### A. 서버 (코드 변경 없음 — 설정/문서/예시만)
서버 코드는 이미 동적(대시보드/추이/월간보고 전부 `METRIC_SENSOR_IDS` 순회, 스키마는
임의 sensor_id 수용)이라 **코드 수정 불필요.** 설정과 예시/문서만 반영.
1. `php/config.local.example.php`
- `METRIC_SENSOR_IDS` 예시를 `[1, 2, 3]` 로(누수 노드 1·3도 온습도 카드 표시).
- `SENSOR_HEARTBEAT_TIMEOUT_SEC` 예시에 `[1 => 1800, 3 => 1800]` 추가 — 1·3을 온습도
목록에 넣으면 오프라인 타임아웃이 metric 기본(7500s)으로 느슨해지므로, 누수 노드는
빠른 오프라인 감지를 유지하도록 명시 오버라이드.
- 동일 sensor_id에 누수+온습도를 함께 두는 구성 설명 주석 추가.
2. `deploy/01_server_cafe24/asmdkwqdmqwertn/config.local.php`(온서버 시크릿 — 접근 불가)
- 실제 반영 값을 사용자에게 제시:
`METRIC_SENSOR_IDS => [1,2,3]`, `SENSOR_HEARTBEAT_TIMEOUT_SEC => [1=>1800, 3=>1800]`.
- 파일이 시크릿이라 에이전트가 직접 수정하지 않고, 추가할 라인을 SETUP 문서/응답에 명시.
3. `deploy/01_server_cafe24/SETUP.md`: 위 config.local 변경 절차 문서화.
**검증 항목(구현 중 확인):** `php/dashboard_data.php`의 카드 맵(≈208–223행)이 sensor_id를
LEAK/METRIC 양쪽에 두어도(중복 집합) 정상 렌더되는지, `monthly_report*`/`forecast.php`가
동일 id를 누수+온습도로 다뤄도 깨지지 않는지 확인. 문제 시 최소 수정.
### B. 노드 — 각 누수 노드에 sht30 서비스 추가 (deploy/02, deploy/04)
각 노드 배포 폴더에 `sht30/` 하위 폴더를 추가한다(기존 누수 파일/경로는 불변 =
`/home/pi/raspberry_pi4` 그대로). sht30 파일은 `raspberry_pi4_sht30/`에서 복사(코드 무변경).
노드별 `sht30/` 구성 파일:
- `sht30_sensor.py`, `sht30_driver.py`, `wire.py` — 소스에서 복사(무변경)
- `config.py` — 소스에서 복사(무변경; SENSOR_ID 기본값은 config_local에서 오버라이드)
- `config_local.example.py` — 템플릿
- `config_local.py` — 노드별 실값:
- Pi-A(02): `SENSOR_ID=1`, `SENSOR_NAME`=누수와 동일, `DEVICE_ID="rpi4-a-sht30"`,
`DEVICE_LOCATION`=Pi-A 누수 노드와 **동일 문자열**, API_URL/API_KEY=서버와 동일
- Pi-C(04): `SENSOR_ID=3`, 나머지 동일 패턴(`DEVICE_ID="rpi4-c-sht30"`)
- `requirements.txt` — `smbus2` 포함(소스와 동일)
- `systemd/sht30-sensor.service` — `WorkingDirectory=/home/pi/sht30`,
`ExecStart=/home/pi/sht30/venv/bin/python sht30_sensor.py`, `SupplementaryGroups=i2c`
- `test_sensor.py`, `test_connection.py` — 소스에서 복사
- `vendor.sh` — 오프라인 번들(smbus2 휠 + i2c-tools deb)
각 누수 노드 폴더의 설치/문서 갱신:
- `install.sh` — 기존 누수 설치에 더해 **sht30 서비스 설치 단계 추가**:
- `sht30/`를 `/home/pi/sht30`로 설치(venv 생성, requirements 설치, 로그파일
`/var/log/sht30_sensor.log` 생성, config_local 권한 600)
- **I2C 활성화**: `sudo raspi-config nonint do_i2c 0` (오프라인 가능). 실패 시
`/boot/firmware/config.txt`(Bookworm) 또는 `/boot/config.txt`에 `dtparam=i2c_arm=on`
보장. i2c 그룹에 SERVICE_USER 추가.
- `sht30-sensor.service` 설치 + enable + start
- 폐쇄망 규칙 준수: 네트워크 미사용, 번들 없으면 명확히 FLAG(기존 install.sh 패턴 유지)
- `WIRING.html` — 기존 누수 배선에 **SHT30 I2C 배선 추가**:
VCC→3.3V(핀1), SDA→GPIO2(핀3), SCL→GPIO3(핀5), GND→핀6/9, ADDR→GND=0x44.
누수 GPIO17(핀11)과 무충돌 명시. "반드시 3.3V, 5V 금지" 경고 포함.
- `SETUP.md` / `README.md` — 이중 서비스(누수+온습도) 설치·확인 절차, 동일 sensor_id 설명.
### C. 소스 트리 (참고 — 정합성)
`raspberry_pi4_sht30/`(sht30 소스)와 `raspberry_pi4/`(누수 소스)는 코드 변경 없음.
`php/config.local.example.php`만 소스에서 갱신(위 A-1). deploy가 1차 산출물이므로
deploy 폴더를 정본으로 반영하고, 소스 예시 파일은 동기화한다.
## 에러 처리 / 엣지
- **라벨 플랩:** 두 서비스가 같은 sensor_id로 sensor_name/device_location upsert →
마지막 쓰기가 이김. 노드별로 두 config의 device_location을 동일 문자열로 맞춰 방지.
(sensor_name은 서버 표시에 영향 최소지만 역시 일치 권장.)
- **오프라인 타임아웃:** A-1/A-2의 `SENSOR_HEARTBEAT_TIMEOUT_SEC` 오버라이드로 1·3의
누수 오프라인 감지 속도 유지(온습도 목록 편입에 의한 완화 방지).
- **is_leak 보존:** sht30 periodic(비-누수 이벤트)은 서버 upsert에서 is_leak 보존(기존
`IF(:is_leak_event, VALUES(is_leak), is_leak)` 로직) → 온습도 보고가 누수 상태를 덮지 않음.
- **SHT30 통신 실패:** `sht30_sensor.py`가 크래시 없이 재시도(기존 동작). 누수 서비스와
독립 프로세스라 상호 영향 없음.
- **I2C 미활성:** smbus2/I2C 불가 시 sht30는 시뮬레이션 폴백(기존). install.sh가 I2C를
활성화하고, 미활성 시 로그로 안내.
## 테스트
- **서버(PHP 단위/통합):**
- recipients: sensor_id 1·3의 온습도 경보가 SENSOR_RECIPIENTS[1]/[3]로 발송되는지.
- heartbeat: `heartbeat_timeout_for(1)`/(3)이 오버라이드로 누수 타임아웃(1800s)을
반환(metric 7500s 아님)하는지 — `resolve_heartbeat_timeout` 단위 테스트 케이스 추가.
- 통합: sensor_id=1 metric POST(periodic) → leak_sensor_metric 저장 + is_leak 보존 +
임계 초과 시 SMS 경로.
- 대시보드: LEAK∩METRIC(=1,3)에서 카드 렌더 정상(중복 집합) 스모크.
- **노드:**
- `sht30/test_sensor.py`: I2C 판독(또는 시뮬).
- `sht30/test_connection.py`: SENSOR_ID=1/3로 서버 POST 200 + 서명 검증.
- **오프라인 설치:** 네트워크 차단 상태에서 install.sh가 두 서비스 + I2C 활성화까지
무network 완료되는지(폐쇄망 검증).
## 비목표 (YAGNI)
- Pi-B(sensor_id 2) 재번호/변경 없음.
- 누수 라인 100nF 콘덴서/하드웨어 필터 — 별개 이슈(이번 범위 밖).
- 센서별 경보 on/off 토글 등 신규 서버 기능 없음(전역 임계 그대로).
- 누수 4~10분 복구 지연(리포트 굶주림) 수정 — 별개 이슈(이번 범위 밖).