라즈베리파이 플랫폼 runtime state 초기화 (OTBR Thread 네트워크 재생성 + Matter Server fabric 리셋 + HA OTBR/Matter 통합 재등록). 이미 설치된 장비에서 커미셔닝 반복 실패/fabric 오염/Thread 네트워크 꼬임 복구 시 사용.
이미 /platform-install 또는 /platform-activate로 설치 완료된 장비에서 바이너리/OS는 그대로 두고 runtime state만 초기화한다. OTBR 바이너리 재빌드, Docker 재설치, avahi/iptables 재설정은 하지 않는다.
사용 케이스:
Failed Device Attestation, Error on commissioning step 'AttestationRevocationCheck')<compressed_fabric_id>.json 잔존)OpenThread-XXXX)가 stale한 dataset(activeTimestamp: 1, preferred_ba 불일치 등)로 꼬임otbr/matter/thread 통합이 setup_error 또는 엔트리 중복 상태전제: OTBR + HA + Matter Server가 이미 설치되어 있고, SSH 접근 가능하며,
/opt/matterhub/app/.env에hass_token이 존재. 클린 OS 상태라면 대신/platform-install을 사용한다.
네트워크 주의: 이 스킬은 wlan0 기준 설정을 유지한다 (-B wlan0, --primary-interface wlan0). eth0가 동시 연결된 환경이고 사내망에 SSL/HTTPS 프록시가 있는 경우, 리셋 후 커미셔닝 시 Matter attestation 외부 HTTPS가 default route 경유 프록시를 타서 MITM으로 인식되어 실패한다. 리셋 후 커미셔닝 테스트는 eth0 물리 제거 또는 프록시 예외 처리된 망에서 진행할 것.
| 변수 | 설명 | 예시 |
|---|---|---|
HOST_IP | 장비 IP | 192.168.43.244 |
SSH_USER | SSH 사용자명 | whatsmatter |
SSH_PW | SSH/sudo 비밀번호 | (사용자 입력) |
HA_TOKEN은/opt/matterhub/app/.env의hass_token에서 스킬 내에서 자동 추출한다. 없으면 사용자에게 별도 요청.
아래 순서대로 Pi에 SSH 접속하여 명령을 실행한다. expect를 사용한다 (sshpass는 비밀번호에 ! 등 특수문자 포함 시 동작 안 함).
SSH 접속 전:
ssh-keygen -R $HOST_IP && ssh-keyscan -H $HOST_IP >> ~/.ssh/known_hosts
expect + sudo 주의: heredoc +
sudo -S조합은 stdin 점유로 패스워드 전달 실패한다. Python/복잡한 명령은 파일로 저장 후sudo python3 /tmp/wipe_*.py로 실행.
# 장비 상태 기본 체크
systemctl is-active otbr-agent
docker ps --format "{{.Names}}" | grep -E "homeassistant_core|matter-server"
# 둘 다 active/실행 중이어야 안전하게 리셋 가능
# 백업 디렉토리 (/root 에 타임스탬프)
BACKUP=/root/platform_reset_backup_$(date +%Y%m%d_%H%M%S)
sudo mkdir -p $BACKUP
# Matter Server data 전체 백업 (chip.json, fabric 파일들, credentials)
sudo cp -a /home/$SSH_USER/docker/matter-server/data $BACKUP/matter_data
# HA .storage 전체 백업
sudo cp -a /home/$SSH_USER/matterhub-install/config/.storage $BACKUP/ha_storage
# OTBR active dataset TLV 백업 (롤백 또는 기존 Thread 네트워크 이름 복원 시 사용)
sudo ot-ctl dataset active -x | sudo tee $BACKUP/otbr_dataset_tlv.txt
# 확인
sudo ls -la $BACKUP/
cd /home/$SSH_USER/matterhub-install
# 컨테이너를 stop 아니라 down 으로 완전 삭제
docker compose down
# 잔존 컨테이너 강제 제거 (compose 외부에서 생성된 경우 대비)
docker rm -f homeassistant_core matter-server 2>/dev/null || true
# 확인: 둘 다 없어야 함
docker ps -a --format "{{.Names}}" | grep -E "homeassistant_core|matter-server" && echo "!!! 컨테이너 잔존" || echo "OK: 컨테이너 전부 삭제됨"
# OTBR 정지
sudo systemctl stop otbr-agent
systemctl is-active otbr-agent || echo "OK: otbr-agent stopped"
docker compose stop아니라down: stop만 하면 컨테이너가 남아있어서 fabric bind mount의 변경이 반영되지 않는 경우가 있다. 완전히 삭제한 후 R-4에서 재생성해야 fresh fabric 로드가 보장된다.
# fabric 설정/카운터/저장 파일 모두 삭제
# credentials/ (PAA 인증서) 는 보존
sudo rm -f /home/$SSH_USER/docker/matter-server/data/chip.json
sudo rm -f /home/$SSH_USER/docker/matter-server/data/chip_counters.ini
sudo rm -f /home/$SSH_USER/docker/matter-server/data/chip_counters.ini-*
sudo rm -f /home/$SSH_USER/docker/matter-server/data/chip_config.ini
sudo rm -f /home/$SSH_USER/docker/matter-server/data/chip_factory.ini
# fabric/node 저장 JSON 파일 (예: 1710124163950804317.json — 152KB 크기로 잔존하는 경우 원인)
sudo rm -f /home/$SSH_USER/docker/matter-server/data/*.json
sudo rm -f /home/$SSH_USER/docker/matter-server/data/*.json.backup
# 확인: credentials/ 만 남아야 함
ls /home/$SSH_USER/docker/matter-server/data/
# 기대: credentials
*.json와일드카드 삭제는 필수:<compressed_fabric_id>.json형태의 파일이 이전 커미셔닝 fabric/node 데이터를 담고 있어 그대로 두면 리셋 후에도 stale 상태 유지.
sudo rm -f /home/$SSH_USER/matterhub-install/config/.storage/thread.datasets
.storage/ 중 otbr/matter/thread 관련 엔트리 제거heredoc + sudo -S 조합을 피하기 위해 Python 스크립트를 먼저 파일로 scp 후 실행한다.
먼저 로컬에서 3개 Python 스크립트 작성 후 scp:
/tmp/wipe_config_entries.py:
#!/usr/bin/env python3
import json, shutil
p = '/home/SSH_USER_PLACEHOLDER/matterhub-install/config/.storage/core.config_entries'
shutil.copy(p, p + '.bak_reset')
d = json.load(open(p))
before = len(d['data']['entries'])
removed = [(e['domain'], e.get('title')) for e in d['data']['entries'] if e.get('domain') in ('otbr','matter','thread')]
d['data']['entries'] = [e for e in d['data']['entries'] if e.get('domain') not in ('otbr','matter','thread')]
json.dump(d, open(p,'w'), indent=2, ensure_ascii=False)
print(f"entries: {before} -> {len(d['data']['entries'])}")
print(f"removed: {removed}")
/tmp/wipe_device_registry.py:
#!/usr/bin/env python3
import json, shutil
p = '/home/SSH_USER_PLACEHOLDER/matterhub-install/config/.storage/core.device_registry'
shutil.copy(p, p + '.bak_reset')
d = json.load(open(p))
before = len(d['data']['devices'])
removed = []
def keep(dev):
ids = dev.get('identifiers') or []
is_target = any(i and i[0] in ('otbr','matter','thread') for i in ids)
if is_target:
removed.append(dev.get('name') or str(ids[:1]))
return not is_target
d['data']['devices'] = [dev for dev in d['data']['devices'] if keep(dev)]
json.dump(d, open(p,'w'), indent=2, ensure_ascii=False)
print(f"devices: {before} -> {len(d['data']['devices'])}")
if removed: print(f"removed: {removed}")
/tmp/wipe_entity_registry.py:
#!/usr/bin/env python3
import json, shutil
p = '/home/SSH_USER_PLACEHOLDER/matterhub-install/config/.storage/core.entity_registry'