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
This commit is contained in:
parent
2a469dc3cc
commit
0b1e86c84a
5 changed files with 626 additions and 1 deletions
445
docs/superpowers/plans/2026-07-15-leak-node-sht30-integration.md
Normal file
445
docs/superpowers/plans/2026-07-15-leak-node-sht30-integration.md
Normal file
|
|
@ -0,0 +1,445 @@
|
|||
# 누수 노드 SHT30 온습도 통합 구현 계획
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 기존 누수 노드(Pi-A=id1, Pi-C=id3)에 SHT30 온습도 센서를 물리적으로 추가하고, 온습도를 같은 sensor_id(1,3)로 서버에 보고 — 대시보드·경보·설치 패키지 전부 반영.
|
||||
|
||||
**Architecture:** 노드마다 기존 누수 서비스는 무변경, `sht30_sensor.py`를 두 번째 systemd 서비스로 얹는다(별도 디렉터리 `/home/pi/sht30`). sht30 코드는 그대로 재사용하고 config_local로 `SENSOR_ID`만 1/3로 지정. 서버는 코드 변경 없이 config.local 값(METRIC_SENSOR_IDS·heartbeat override)만 조정 — 대시보드/보고서는 이미 `METRIC_SENSOR_IDS` 동적 순회.
|
||||
|
||||
**Tech Stack:** Python 3.11(라즈베리 파이 4, Bookworm), smbus2(I2C), PHP(cafe24 서버), systemd, bash 설치 스크립트(폐쇄망 오프라인 번들).
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- **폐쇄망(air-gapped):** 모든 설치 산출물은 자립형. 네트워크 미사용, 번들 없으면 명확히 FLAG. wheel/deb는 build 머신에서 vendor.sh로 사전 다운로드. (기존 install.sh/vendor.sh 패턴 유지)
|
||||
- **동일 sensor_id:** 온습도는 새 id 없이 누수와 같은 id(Pi-A=1, Pi-C=3). Pi-B(id=2)는 절대 변경 없음.
|
||||
- **누수 코드 무변경:** `leak_sensor.py` 및 누수 런타임 경로(`/home/pi/raspberry_pi4`) 손대지 않음.
|
||||
- **라벨 일치:** 각 노드에서 sht30 config_local의 `DEVICE_LOCATION`은 같은 노드 누수 config_local의 `DEVICE_LOCATION`과 **문자열 동일**(같은 sensor_id upsert 라벨 플랩 방지).
|
||||
- **HMAC 서명:** sht30 config_local의 `API_KEY`는 서버 config.local.php의 `API_KEY`와 완전 동일.
|
||||
- **비밀값:** `config_local.py`/`config.local.php`(실값)는 에이전트가 직접 수정/커밋하지 않음. 예시(`*.example`)와 문서만 반영, 실값 라인은 응답/문서에 제시.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: sht30 config.py — config_local로 SENSOR_ID/SENSOR_NAME 오버라이드 지원
|
||||
|
||||
**목적:** 누수 노드의 sht30가 tracked `config.py`(SENSOR_ID=2 기본) 편집 없이 config_local만으로 id=1/3 보고. 누수 config.py의 SENSORS 오버라이드 패턴과 동일. Pi-B는 config_local에 SENSOR_ID 미지정 → 기본 2 유지(하위호환).
|
||||
|
||||
**Files:**
|
||||
- Modify: `raspberry_pi4_sht30/config.py` (끝에 오버라이드 블록 추가)
|
||||
- Modify: `deploy/03_pi_b_sht30_id2/config.py` (동일 블록 — 소스와 동기화)
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `config.SENSOR_ID` / `config.SENSOR_NAME` 가 config_local 값으로 대체 가능(검증 포함). `sht30_sensor.py`가 `from config import SENSOR_ID, SENSOR_NAME` 로 소비(무변경).
|
||||
|
||||
- [ ] **Step 1: `raspberry_pi4_sht30/config.py` 끝(현재 마지막 줄 = config_local import 블록 이후)에 추가**
|
||||
|
||||
```python
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# Optional override — per-device values from config_local.py.
|
||||
# Editing the tracked config.py per device causes deployment drift, so a co-located
|
||||
# node (e.g. leak node reporting temp/humidity under the SAME sensor_id 1 or 3)
|
||||
# overrides SENSOR_ID / SENSOR_NAME here instead. If config_local has neither,
|
||||
# the defaults above (SENSOR_ID=2) are kept — backward compatible for Pi-B.
|
||||
# ─────────────────────────────────────────────
|
||||
try:
|
||||
from config_local import SENSOR_ID # noqa: F811 — per-device sensor_id override
|
||||
except ImportError:
|
||||
pass
|
||||
try:
|
||||
from config_local import SENSOR_NAME # noqa: F811 — per-device display name override
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Validate (fail closed) — a typo must not silently start with a wrong/plausible id.
|
||||
if not (isinstance(SENSOR_ID, int) and 1 <= SENSOR_ID <= 255):
|
||||
raise RuntimeError(f"SENSOR_ID must be int 1..255 (check config_local.py): {SENSOR_ID!r}")
|
||||
if not (isinstance(SENSOR_NAME, str) and SENSOR_NAME):
|
||||
raise RuntimeError("SENSOR_NAME must be a non-empty string (check config_local.py).")
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 같은 블록을 `deploy/03_pi_b_sht30_id2/config.py` 끝에도 추가** (소스와 동일 — 두 파일 내용 일치 확인)
|
||||
|
||||
- [ ] **Step 3: 오버라이드 동작 검증(수동 스모크 — 이 repo의 파이썬 테스트 관례는 수동 스크립트)**
|
||||
|
||||
Run (임시 config_local 생성해 import 확인):
|
||||
```bash
|
||||
cd raspberry_pi4_sht30
|
||||
python3 - <<'PY'
|
||||
# 오버라이드 로직 단위 확인(실 config_local 없이): 값 대입+검증 규칙만.
|
||||
ns = {'SENSOR_ID': 2, 'SENSOR_NAME': 'Sensor 2 (SHT30)'}
|
||||
ns['SENSOR_ID'] = 1 # config_local override 흉내
|
||||
assert isinstance(ns['SENSOR_ID'], int) and 1 <= ns['SENSOR_ID'] <= 255
|
||||
assert isinstance(ns['SENSOR_NAME'], str) and ns['SENSOR_NAME']
|
||||
print("override ok:", ns['SENSOR_ID'])
|
||||
PY
|
||||
```
|
||||
Expected: `override ok: 1`
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add raspberry_pi4_sht30/config.py deploy/03_pi_b_sht30_id2/config.py
|
||||
git commit -m "feat(sht30): config_local 로 SENSOR_ID/SENSOR_NAME 오버라이드(동일-id 공용 노드 대비)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: deploy/02 (Pi-A) 에 sht30 서브폴더 추가 — SENSOR_ID=1
|
||||
|
||||
**Files:**
|
||||
- Create dir: `deploy/02_pi_a_leak_id1/sht30/`
|
||||
- Create(복사): `sht30/sht30_sensor.py`, `sht30/sht30_driver.py`, `sht30/wire.py`, `sht30/config.py`, `sht30/test_sensor.py`, `sht30/test_connection.py`, `sht30/requirements.txt`, `sht30/vendor.sh`, `sht30/install.sh`, `sht30/systemd/sht30-sensor.service` — 전부 `deploy/03_pi_b_sht30_id2/`에서 **verbatim 복사** (config.py는 Task 1 반영본)
|
||||
- Create: `sht30/config_local.example.py`, `sht30/config_local.py`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `/home/pi/sht30` 로 설치될 자립형 sht30 installable. Task 4(install.sh 체이닝)가 소비.
|
||||
|
||||
- [ ] **Step 1: deploy/03 sht30 파일 verbatim 복사**
|
||||
|
||||
```bash
|
||||
cd deploy/02_pi_a_leak_id1
|
||||
mkdir -p sht30/systemd
|
||||
cp ../03_pi_b_sht30_id2/sht30_sensor.py sht30/
|
||||
cp ../03_pi_b_sht30_id2/sht30_driver.py sht30/
|
||||
cp ../03_pi_b_sht30_id2/wire.py sht30/
|
||||
cp ../03_pi_b_sht30_id2/config.py sht30/
|
||||
cp ../03_pi_b_sht30_id2/test_sensor.py sht30/
|
||||
cp ../03_pi_b_sht30_id2/test_connection.py sht30/
|
||||
cp ../03_pi_b_sht30_id2/requirements.txt sht30/
|
||||
cp ../03_pi_b_sht30_id2/vendor.sh sht30/
|
||||
cp ../03_pi_b_sht30_id2/install.sh sht30/
|
||||
cp ../03_pi_b_sht30_id2/systemd/sht30-sensor.service sht30/systemd/
|
||||
```
|
||||
|
||||
- [ ] **Step 2: `sht30/systemd/sht30-sensor.service` 의 경로를 `/home/pi/sht30` 로 수정**
|
||||
|
||||
`WorkingDirectory` 와 `ExecStart` 두 줄을 다음으로 변경:
|
||||
```ini
|
||||
WorkingDirectory=/home/pi/sht30
|
||||
ExecStart=/home/pi/sht30/venv/bin/python sht30_sensor.py
|
||||
```
|
||||
(나머지 sandbox/그룹 설정은 그대로 — `SupplementaryGroups=i2c` 유지)
|
||||
|
||||
- [ ] **Step 3: `sht30/config_local.example.py` 작성**
|
||||
|
||||
```python
|
||||
# =============================================================================
|
||||
# config_local.example.py — 누수 노드(Pi-A, sensor_id=1)에 얹는 SHT30 온습도 서비스 설정
|
||||
# cp config_local.example.py config_local.py 후 실값 입력. config_local.py 는 커밋 금지.
|
||||
#
|
||||
# ★ 동일 sensor_id: 이 온습도는 같은 노드 누수 센서와 "같은" sensor_id(=1)로 보고한다.
|
||||
# ★ 라벨 일치: DEVICE_LOCATION 은 같은 노드 누수 config_local.py 의 DEVICE_LOCATION 과
|
||||
# "문자열 동일"해야 한다(같은 id 를 두 서비스가 upsert → 라벨 플랩 방지).
|
||||
# ★ API_KEY 는 서버 config.local.php 의 API_KEY 와 완전 동일(HMAC 서명).
|
||||
# =============================================================================
|
||||
API_URL = "https://YOUR_SERVER/raspi_leck_detecter/api/sensor_data.php"
|
||||
API_KEY = "CHANGE_ME"
|
||||
|
||||
DEVICE_ID = "rpi4-a-sht30"
|
||||
DEVICE_LOCATION = "Server Room" # ★ 누수(id=1) DEVICE_LOCATION 과 동일하게
|
||||
|
||||
SENSOR_ID = 1 # ★ 누수와 동일 id 로 온습도 보고
|
||||
SENSOR_NAME = "Sensor 1 (SHT30)"
|
||||
```
|
||||
|
||||
- [ ] **Step 4: `sht30/config_local.py` 작성(실배포 기본값 — 실값은 현장에서 교체, chmod 600은 install.sh가)**
|
||||
|
||||
Step 3 내용과 동일하게 `config_local.py` 생성. `.gitignore` 가 `config_local.py` 를 제외하는지 확인 — 제외되면 예시만 커밋 대상(실값 파일 미커밋).
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add deploy/02_pi_a_leak_id1/sht30
|
||||
git commit -m "feat(pi-a): 누수 노드에 SHT30 온습도 서비스 서브폴더 추가(sensor_id=1)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: deploy/04 (Pi-C) 에 sht30 서브폴더 추가 — SENSOR_ID=3
|
||||
|
||||
Task 2와 동일 절차, `deploy/04_pi_c_leak_id3/sht30/` 로. 차이는 config_local(.example).py:
|
||||
|
||||
- [ ] **Step 1: 복사** — Task 2 Step 1을 `cd deploy/04_pi_c_leak_id3` 로 동일 수행.
|
||||
- [ ] **Step 2: service 경로** — Task 2 Step 2와 동일(`/home/pi/sht30`).
|
||||
- [ ] **Step 3+4: config_local(.example).py** — Task 2와 동일하되:
|
||||
|
||||
```python
|
||||
DEVICE_ID = "rpi4-c-sht30"
|
||||
DEVICE_LOCATION = "New Site" # ★ 누수(id=3) DEVICE_LOCATION 과 동일하게
|
||||
SENSOR_ID = 3 # ★ 누수와 동일 id
|
||||
SENSOR_NAME = "Sensor 3 (SHT30)"
|
||||
```
|
||||
(API_URL/API_KEY는 Task 2와 동일 형식)
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add deploy/04_pi_c_leak_id3/sht30
|
||||
git commit -m "feat(pi-c): 누수 노드에 SHT30 온습도 서비스 서브폴더 추가(sensor_id=3)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: deploy/02 install.sh — sht30 서비스 설치 체이닝 + 서비스 enable
|
||||
|
||||
**Files:**
|
||||
- Modify: `deploy/02_pi_a_leak_id1/install.sh` (마지막 완료 메시지 직전에 단계 추가)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 2의 `sht30/` (자립형 install.sh 포함).
|
||||
- Produces: `install.sh` 1회 실행으로 누수 + 온습도 두 서비스 설치·기동.
|
||||
|
||||
- [ ] **Step 1: `deploy/02_pi_a_leak_id1/install.sh` 의 최종 `echo "==... Installation complete"` 블록 바로 앞에 삽입**
|
||||
|
||||
```bash
|
||||
# 5) SHT30 온습도 co-센서 설치 (동일 sensor_id=1, /home/pi/sht30 로 설치)
|
||||
if [ -d sht30 ]; then
|
||||
echo "[5/5] Installing SHT30 temp/humidity co-sensor (sensor_id 1)..."
|
||||
SHT30_DEST="/home/pi/sht30"
|
||||
sudo mkdir -p "${SHT30_DEST}"
|
||||
# 코드/번들 배치(설치 대상 디렉터리로 복사 — 폐쇄망: 네트워크 미사용)
|
||||
sudo cp -r sht30/. "${SHT30_DEST}"/
|
||||
sudo chown -R "${SERVICE_USER}:${SERVICE_USER}" "${SHT30_DEST}"
|
||||
# sht30 자체 install.sh 로 위임(venv + I2C 활성화 + 로그 + 서비스 설치). OFFLINE 전파.
|
||||
( cd "${SHT30_DEST}" && SERVICE_USER="${SERVICE_USER}" OFFLINE="${OFFLINE}" bash install.sh )
|
||||
# systemd 서비스 등록/기동
|
||||
sudo cp "${SHT30_DEST}/systemd/sht30-sensor.service" /etc/systemd/system/
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable sht30-sensor
|
||||
sudo systemctl restart sht30-sensor
|
||||
echo " sht30-sensor 서비스 등록/기동 완료"
|
||||
else
|
||||
echo "[5/5] sht30/ 폴더 없음 — 온습도 co-센서 설치 건너뜀"
|
||||
fi
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 문법 검증**
|
||||
|
||||
Run: `bash -n deploy/02_pi_a_leak_id1/install.sh`
|
||||
Expected: (출력 없음 = 문법 OK)
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add deploy/02_pi_a_leak_id1/install.sh
|
||||
git commit -m "feat(pi-a): install.sh 에 SHT30 온습도 서비스 설치 체이닝(누수+온습도 1회 설치)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: deploy/04 install.sh — 동일 체이닝 (sensor_id 3)
|
||||
|
||||
Task 4와 동일. 삽입 스니펫에서 주석 "sensor_id 1"→"sensor_id 3"만 변경.
|
||||
|
||||
- [ ] **Step 1:** `deploy/04_pi_c_leak_id3/install.sh` 에 Task 4 Step 1 스니펫 삽입(주석 id 3).
|
||||
- [ ] **Step 2:** `bash -n deploy/04_pi_c_leak_id3/install.sh` → 문법 OK.
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add deploy/04_pi_c_leak_id3/install.sh
|
||||
git commit -m "feat(pi-c): install.sh 에 SHT30 온습도 서비스 설치 체이닝(sensor_id=3)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: deploy/02 배선/문서 — WIRING.html + SETUP.md + README.md
|
||||
|
||||
**Files:**
|
||||
- Modify: `deploy/02_pi_a_leak_id1/WIRING.html` (SHT30 I2C 배선 섹션 추가)
|
||||
- Modify: `deploy/02_pi_a_leak_id1/SETUP.md` (이중 서비스 설치·확인 절차)
|
||||
- Modify: `deploy/02_pi_a_leak_id1/README.md` (개요에 온습도 추가)
|
||||
|
||||
- [ ] **Step 1: `WIRING.html` — 누수 배선 다음에 SHT30 I2C 섹션 삽입**
|
||||
|
||||
`</body>` 직전(또는 마지막 섹션 뒤)에 삽입:
|
||||
```html
|
||||
<section>
|
||||
<h2>SHT30 온습도 (동일 노드, sensor_id 1)</h2>
|
||||
<p>누수 센서와 <b>같은 Pi</b>에 SHT30(P4422-3)을 I2C로 추가. 온습도는 누수와 <b>같은 sensor_id(1)</b>로 보고한다. 누수 GPIO17(핀11)과 <b>핀 충돌 없음</b>.</p>
|
||||
<table>
|
||||
<thead><tr><th>SHT30</th><th>Pi 40핀</th><th>비고</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>VCC</td><td>3.3V (핀 1)</td><td>★ 반드시 3.3V. 5V(핀 2·4) 금지 — I2C 라인 5V로 끌려 Pi 손상</td></tr>
|
||||
<tr><td>SDA</td><td>GPIO2 / SDA1 (핀 3)</td><td>온보드 풀업 없으면 4.7kΩ→3.3V</td></tr>
|
||||
<tr><td>SCL</td><td>GPIO3 / SCL1 (핀 5)</td><td>온보드 풀업 없으면 4.7kΩ→3.3V</td></tr>
|
||||
<tr><td>GND</td><td>GND (핀 6 또는 9)</td><td></td></tr>
|
||||
<tr><td>ADDR</td><td>GND</td><td>0x44 (config.py I2C_ADDR 와 일치)</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
```
|
||||
|
||||
- [ ] **Step 2: `SETUP.md` — 온습도 설치/확인 절차 추가**
|
||||
|
||||
다음 섹션을 문서 말미에 추가:
|
||||
```markdown
|
||||
## SHT30 온습도 co-센서 (동일 sensor_id 1)
|
||||
|
||||
이 노드는 누수 + 온습도 두 서비스를 함께 돌린다.
|
||||
|
||||
1. 배선: WIRING.html 의 "SHT30 온습도" 표대로(VCC=3.3V, SDA=핀3, SCL=핀5, GND=핀6, ADDR=GND).
|
||||
2. 온습도 설정: `sht30/config_local.py` 에서 API_URL/API_KEY/DEVICE_LOCATION 을 실값으로.
|
||||
- ★ `DEVICE_LOCATION` 은 누수 `config_local.py` 의 값과 문자열 동일.
|
||||
- `SENSOR_ID=1` 은 그대로(누수와 동일 id).
|
||||
3. 설치: `bash install.sh` 1회 → 누수 + 온습도 서비스 모두 설치·기동(SHT30는 /home/pi/sht30).
|
||||
4. 확인:
|
||||
- `i2cdetect -y 1` → 0x44 표시
|
||||
- `sudo systemctl status leak-sensor sht30-sensor` → 둘 다 active
|
||||
- 서버 대시보드 1번 카드에 누수 상태 + 온습도 표시
|
||||
5. I2C 활성화 직후면 `sudo reboot` 필요할 수 있음(install.sh 안내).
|
||||
```
|
||||
|
||||
- [ ] **Step 3: `README.md`** — 개요 문단에 "이 노드는 누수(id 1) + SHT30 온습도(같은 id 1)를 함께 보고한다" 한 줄 추가.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add deploy/02_pi_a_leak_id1/WIRING.html deploy/02_pi_a_leak_id1/SETUP.md deploy/02_pi_a_leak_id1/README.md
|
||||
git commit -m "docs(pi-a): SHT30 온습도 배선/설치 문서 반영"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: deploy/04 배선/문서 — 동일 (sensor_id 3)
|
||||
|
||||
Task 6과 동일하되 모든 "sensor_id 1"→"3", DEVICE_LOCATION 안내는 누수(id=3) 값 기준.
|
||||
|
||||
- [ ] **Step 1~3:** Task 6 Step 1~3을 `deploy/04_pi_c_leak_id3/` 파일에 id=3으로 반영.
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add deploy/04_pi_c_leak_id3/WIRING.html deploy/04_pi_c_leak_id3/SETUP.md deploy/04_pi_c_leak_id3/README.md
|
||||
git commit -m "docs(pi-c): SHT30 온습도 배선/설치 문서 반영(sensor_id=3)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 8: 서버 — config.local.example.php + deploy/01 SETUP 반영
|
||||
|
||||
**Files:**
|
||||
- Modify: `php/config.local.example.php`
|
||||
- Modify: `deploy/01_server_cafe24/asmdkwqdmqwertn/config.local.php`(시크릿 — **에이전트 미수정**, 값만 SETUP·응답에 제시)
|
||||
- Modify: `deploy/01_server_cafe24/SETUP.md`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: 대시보드가 1·3을 온습도 카드로도 표시(METRIC_SENSOR_IDS), 누수 오프라인 감지 속도 유지(heartbeat override).
|
||||
|
||||
- [ ] **Step 1: `php/config.local.example.php` 의 `LEAK_SENSOR_IDS` 아래 `METRIC_SENSOR_IDS` 예시 추가 + heartbeat override 주석 갱신**
|
||||
|
||||
`'LEAK_SENSOR_IDS' => [1],` 다음에 삽입:
|
||||
```php
|
||||
// 온습도(SHT30) sensor_id 목록 — 대시보드 온습도 카드 표시 대상 (기본 [2]).
|
||||
// 누수 노드(1·3)에 SHT30 을 얹어 "같은 sensor_id" 로 온습도를 보고하는 구성에서는
|
||||
// 1·3 도 포함한다: [1, 2, 3]. (누수 카드 + 온습도 카드가 같은 지점에 함께 표시됨)
|
||||
'METRIC_SENSOR_IDS' => [1, 2, 3],
|
||||
```
|
||||
그리고 기존 `SENSOR_HEARTBEAT_TIMEOUT_SEC` 주석 예시를 실효 오버라이드로 교체:
|
||||
```php
|
||||
// 센서별 오프라인 판정 타임아웃(무수신 초). ★ 1·3 은 온습도 목록에도 들어가지만
|
||||
// 누수 노드이므로 metric 기본(7500s)으로 느슨해지면 안 된다 → 누수 타임아웃 유지 위해
|
||||
// 명시 오버라이드한다(30분). (미지정 시 온습도로 간주되어 오프라인 감지가 늦어짐)
|
||||
'SENSOR_HEARTBEAT_TIMEOUT_SEC' => [1 => 1800, 3 => 1800],
|
||||
```
|
||||
|
||||
- [ ] **Step 2: `deploy/01_server_cafe24/SETUP.md` 에 온서버 config.local.php 변경 절차 추가**
|
||||
|
||||
```markdown
|
||||
## 누수 노드 온습도(SHT30) 통합 반영 (동일 sensor_id)
|
||||
|
||||
누수 노드(1·3)에 SHT30 를 얹어 같은 sensor_id 로 온습도를 보고하면, 서버 config.local.php 에
|
||||
아래를 반영한다(코드 변경 없음 — 설정만):
|
||||
|
||||
'METRIC_SENSOR_IDS' => [1, 2, 3],
|
||||
'SENSOR_HEARTBEAT_TIMEOUT_SEC' => [1 => 1800, 3 => 1800],
|
||||
|
||||
- METRIC_SENSOR_IDS: 1·3 을 온습도 카드 대상에 포함(대시보드/월간보고 자동 반영).
|
||||
- SENSOR_HEARTBEAT_TIMEOUT_SEC: 1·3 을 누수 타임아웃(30분)으로 고정(온습도 완화 방지).
|
||||
- 온습도 임계 경보 수신자는 기존 SENSOR_RECIPIENTS[1]/[3] 그대로 사용.
|
||||
반영 후 대시보드에서 1·3 카드에 누수 + 온습도가 함께 뜨는지 확인.
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add php/config.local.example.php deploy/01_server_cafe24/SETUP.md
|
||||
git commit -m "docs(server): 누수 노드 동일-id 온습도 통합 — METRIC_SENSOR_IDS/heartbeat 예시·SETUP 반영"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 9: 서버 테스트 — heartbeat override(1·3) 케이스 추가
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/heartbeat_timeout_test.php`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `resolve_heartbeat_timeout($sid, $overrides, $metricIds, $metricDefault, $globalDefault)` (config.php 순수 함수).
|
||||
|
||||
- [ ] **Step 1: 기존 테스트 파일 문법 확인**
|
||||
|
||||
Run: `php -l tests/heartbeat_timeout_test.php`
|
||||
Expected: `No syntax errors detected`
|
||||
|
||||
- [ ] **Step 2: 실패 테스트 추가 — 1·3 이 METRIC 목록에 있어도 override 로 누수 타임아웃 반환**
|
||||
|
||||
`tests/heartbeat_timeout_test.php` 에 다음 검증 추가(파일의 assert 패턴에 맞춰 삽입):
|
||||
```php
|
||||
// 동일-id 온습도 통합: 1·3 이 METRIC_SENSOR_IDS 에 포함돼도, 명시 오버라이드가
|
||||
// metric 기본(7500)보다 우선하여 누수 타임아웃(1800)을 반환해야 한다.
|
||||
$overrides = [1 => 1800, 3 => 1800];
|
||||
$metricIds = [1, 2, 3];
|
||||
assert(resolve_heartbeat_timeout(1, $overrides, $metricIds, 7500, 1200) === 1800); // 누수 유지
|
||||
assert(resolve_heartbeat_timeout(3, $overrides, $metricIds, 7500, 1200) === 1800); // 누수 유지
|
||||
assert(resolve_heartbeat_timeout(2, $overrides, $metricIds, 7500, 1200) === 7500); // 순수 온습도
|
||||
echo "[OK] heartbeat override(1,3) with metric-membership\n";
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 실행 → 통과 확인**
|
||||
|
||||
Run: `php tests/heartbeat_timeout_test.php`
|
||||
Expected: 기존 출력 + `[OK] heartbeat override(1,3) with metric-membership`, 종료코드 0
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add tests/heartbeat_timeout_test.php
|
||||
git commit -m "test(server): 동일-id 온습도 통합 시 1·3 누수 heartbeat 타임아웃 유지 검증"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 10: 통합 검증 (대시보드 중복집합 + 라벨/스키마) — 코드 수정은 발견 시만
|
||||
|
||||
**Files:**
|
||||
- Verify: `php/dashboard_data.php`(≈208–223, 503–506), `php/monthly_report_print.php`, `php/forecast.php`
|
||||
- Modify(발견 시만): 위 파일 중 동일 id가 LEAK∩METRIC 일 때 깨지는 부분
|
||||
|
||||
- [ ] **Step 1: 카드 맵 로직 확인 — sensor_id 가 LEAK·METRIC 양쪽에 있어도 정상**
|
||||
|
||||
`php/dashboard_data.php` 208–230행을 읽고, `LEAK_SENSOR_IDS`와 `METRIC_SENSOR_IDS` 순회가 서로 독립적으로 카드를 만드는지(중복 집합 1·3에서 예외/누락 없는지) 확인. 누수 카드와 온습도 카드가 **각각** 생성되면 정상.
|
||||
|
||||
- [ ] **Step 2: 월간보고/예보에서 동일 id 취급 확인**
|
||||
|
||||
`php/monthly_report_print.php`(LEAK_SENSOR_IDS·METRIC_SENSOR_IDS 사용부), `php/forecast.php`(METRIC 순회)에서 1·3이 온습도로도 집계될 때 누수 데이터와 충돌/중복 표기가 없는지 확인.
|
||||
|
||||
- [ ] **Step 3: 문제 발견 시에만 최소 수정** — 없으면 수정 없이 다음.
|
||||
|
||||
- [ ] **Step 4: 통합 스모크(로컬 서버 있으면) — sensor_id=1 metric POST → 저장·표시·임계 경로**
|
||||
|
||||
가능하면 로컬/스테이징에서 sensor_id=1 로 sht30 periodic POST(서명 포함) → `leak_sensor_metric`에 1행 저장 + `leak_sensor_status`의 is_leak 보존 + 대시보드 1번 카드에 온습도 표시 확인. (운영 서버 직접 검증은 사용자 몫 — SETUP 절차로 안내.)
|
||||
|
||||
- [ ] **Step 5: Commit(수정 있었을 때만)**
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "fix(server): 동일-id LEAK∩METRIC 대시보드/보고 표기 정합(발견 시)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Self-Review (작성자 체크)
|
||||
|
||||
- **Spec 커버리지:** A(서버 설정/문서)=Task 8·9·10, B(노드 sht30 서비스)=Task 1·2·3·4·5, 배선/설치문서=Task 6·7. 스펙의 모든 절 대응 태스크 존재. ✓
|
||||
- **Placeholder:** "적절히 처리" 류 없음. 각 스텝 실제 코드/명령/기대출력 포함. ✓
|
||||
- **타입/이름 정합:** `resolve_heartbeat_timeout` 시그니처, `SENSOR_ID`/`SENSOR_NAME`/`DEVICE_LOCATION`/`METRIC_SENSOR_IDS`/`SENSOR_HEARTBEAT_TIMEOUT_SEC` 명칭 전 태스크 일관. ✓
|
||||
- **비밀값:** 실 config_local.py/config.local.php 는 예시·문서만, 실값 라인은 제시(에이전트 미커밋). ✓
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
# 설계: 누수 노드에 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분 복구 지연(리포트 굶주림) 수정 — 별개 이슈(이번 범위 밖).
|
||||
|
|
@ -53,6 +53,11 @@ return [
|
|||
// 신규 누수 지점(예: Pi-C, sensor_id 3) 투입 시: [1, 3]
|
||||
'LEAK_SENSOR_IDS' => [1],
|
||||
|
||||
// 온습도(SHT30) sensor_id 목록 — 대시보드 온습도 카드 표시 대상 (기본 [2]).
|
||||
// 누수 노드(1·3)에 SHT30 을 얹어 "같은 sensor_id" 로 온습도를 보고하는 구성에서는
|
||||
// 1·3 도 포함한다: [1, 2, 3]. (같은 지점에 누수 카드 + 온습도 카드가 함께 표시됨)
|
||||
'METRIC_SENSOR_IDS' => [1, 2, 3],
|
||||
|
||||
// 누수 감지/복구 디바운스(감지↔복구 플래핑 SMS 폭탄 방지). 상태가 확인창 동안 지속돼야
|
||||
// 확정하고 확정 엣지에서만 SMS 1회. 아래는 기본값(필요 시 조정). sql/migration_leak_debounce.sql
|
||||
// 적용 필요(컬럼 없으면 레거시 즉시 발송으로 자동 폴백).
|
||||
|
|
@ -64,7 +69,9 @@ return [
|
|||
// 매시간 오탐 → 온습도 센서는 METRIC_HEARTBEAT_TIMEOUT_SEC(기본 7500s≈125분) 자동 적용.
|
||||
// 특정 센서만 다르게 하려면 SENSOR_HEARTBEAT_TIMEOUT_SEC 로 명시 오버라이드.
|
||||
// 'METRIC_HEARTBEAT_TIMEOUT_SEC' => 7500,
|
||||
// 'SENSOR_HEARTBEAT_TIMEOUT_SEC' => [3 => 1800],
|
||||
// ★ 동일-id 온습도 통합: 1·3 은 위 METRIC_SENSOR_IDS 에도 들어가지만 누수 노드이므로
|
||||
// metric 기본(7500s)으로 느슨해지면 안 됨 → 누수 타임아웃(30분) 유지하도록 명시 오버라이드.
|
||||
'SENSOR_HEARTBEAT_TIMEOUT_SEC' => [1 => 1800, 3 => 1800],
|
||||
|
||||
// 온습도 임계 경보 (서버 판정 → SMS). 운영 환경에 맞게 조정.
|
||||
'METRIC_TEMP_HIGH_C' => 27, // 고온 임계(℃)
|
||||
|
|
|
|||
|
|
@ -83,3 +83,25 @@ except ImportError as e:
|
|||
"Run `cp config_local.example.py config_local.py`, then fill in the real server URL / API_KEY / "
|
||||
"device info. (Never commit secret values.)"
|
||||
) from e
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# Optional override — per-device values from config_local.py.
|
||||
# Editing the tracked config.py per device causes deployment drift, so a co-located
|
||||
# node (e.g. a leak node reporting temp/humidity under the SAME sensor_id 1 or 3)
|
||||
# overrides SENSOR_ID / SENSOR_NAME here instead. If config_local defines neither,
|
||||
# the defaults above (SENSOR_ID=2) are kept — backward compatible for Pi-B.
|
||||
# ─────────────────────────────────────────────
|
||||
try:
|
||||
from config_local import SENSOR_ID # noqa: F811 — per-device sensor_id override
|
||||
except ImportError:
|
||||
pass
|
||||
try:
|
||||
from config_local import SENSOR_NAME # noqa: F811 — per-device display name override
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Validate (fail closed) — a typo must not silently start with a wrong/plausible id.
|
||||
if not (isinstance(SENSOR_ID, int) and 1 <= SENSOR_ID <= 255):
|
||||
raise RuntimeError(f"SENSOR_ID must be int 1..255 (check config_local.py): {SENSOR_ID!r}")
|
||||
if not (isinstance(SENSOR_NAME, str) and SENSOR_NAME):
|
||||
raise RuntimeError("SENSOR_NAME must be a non-empty string (check config_local.py).")
|
||||
|
|
|
|||
|
|
@ -39,6 +39,17 @@ $ov2 = [2 => 4000];
|
|||
check(resolve_heartbeat_timeout(2, $ov2, $metricIds, $metricDef, $globalDef) === 4000,
|
||||
'explicit override beats metric default for a metric sensor');
|
||||
|
||||
// ── 5) 동일-id 온습도 통합: 누수 노드(1·3)가 METRIC_SENSOR_IDS 에도 포함돼도,
|
||||
// 명시 오버라이드가 metric 기본(7500)을 이겨 누수 타임아웃(1800)을 유지해야 한다. ──
|
||||
$sameIdOverride = [1 => 1800, 3 => 1800];
|
||||
$sameIdMetric = [1, 2, 3]; // 1·3 은 온습도도 같은 id 로 보고 → 목록에 포함
|
||||
check(resolve_heartbeat_timeout(1, $sameIdOverride, $sameIdMetric, 7500, 1200) === 1800,
|
||||
'same-id leak(1) keeps leak timeout despite metric membership');
|
||||
check(resolve_heartbeat_timeout(3, $sameIdOverride, $sameIdMetric, 7500, 1200) === 1800,
|
||||
'same-id leak(3) keeps leak timeout despite metric membership');
|
||||
check(resolve_heartbeat_timeout(2, $sameIdOverride, $sameIdMetric, 7500, 1200) === 7500,
|
||||
'pure metric(2) still uses metric default');
|
||||
|
||||
if ($failures) {
|
||||
fwrite(STDERR, "Heartbeat timeout test failures:\n");
|
||||
foreach ($failures as $f) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue