# 누수 노드 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 섹션 삽입** `` 직전(또는 마지막 섹션 뒤)에 삽입: ```html

SHT30 온습도 (동일 노드, sensor_id 1)

누수 센서와 같은 Pi에 SHT30(P4422-3)을 I2C로 추가. 온습도는 누수와 같은 sensor_id(1)로 보고한다. 누수 GPIO17(핀11)과 핀 충돌 없음.

SHT30Pi 40핀비고
VCC3.3V (핀 1)★ 반드시 3.3V. 5V(핀 2·4) 금지 — I2C 라인 5V로 끌려 Pi 손상
SDAGPIO2 / SDA1 (핀 3)온보드 풀업 없으면 4.7kΩ→3.3V
SCLGPIO3 / SCL1 (핀 5)온보드 풀업 없으면 4.7kΩ→3.3V
GNDGND (핀 6 또는 9)
ADDRGND0x44 (config.py I2C_ADDR 와 일치)
``` - [ ] **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 는 예시·문서만, 실값 라인은 제시(에이전트 미커밋). ✓