누수 노드(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
140 lines
8.7 KiB
Markdown
140 lines
8.7 KiB
Markdown
# 설계: 누수 노드에 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분 복구 지연(리포트 굶주림) 수정 — 별개 이슈(이번 범위 밖).
|