feat: 완전한 Tapo 스마트 플러그 백엔드 시스템 구현
✨ 새로운 기능: - Tapo P110/P100 스마트 플러그 완전 연동 - 동적 기기 관리 (추가/제거/수정) - 실시간 전력 데이터 수집 API - 설정 파일 기반 확장 가능한 아키텍처 🔧 기술 개선: - Docker Compose 파일 통합 (mac-mini 전용 제거) - MariaDB 설정 최적화 (호환성 문제 해결) - 포트 구조 개선 (9304-9307 대역 사용) - Express.js 기반 RESTful API 완성 📚 문서화: - README 전면 업데이트 (구현된 API 반영) - Tapo API 엔드포인트 상세 문서화 - 실제 사용 가능한 curl 예제 추가 🗄️ 데이터베이스: - MariaDB 11 안정화 - Redis 캐시 시스템 구축 - 사용자 권한 모델 준비 🚀 Docker 환경: - 단일 docker-compose.yml로 통합 - 포트 충돌 해결 - 헬스체크 및 자동 재시작 설정
This commit is contained in:
139
README.md
139
README.md
@@ -6,10 +6,19 @@
|
|||||||
|
|
||||||
## 🎯 주요 기능
|
## 🎯 주요 기능
|
||||||
|
|
||||||
- **디바이스 관리**: 홈 IoT 기기 등록 및 모니터링
|
### ✅ **구현 완료**
|
||||||
- **전력 소비 추적**: 실시간 전력 데이터 수집 및 분석
|
- **Tapo 스마트 플러그 관리**: 동적 기기 추가/제거/모니터링
|
||||||
- **네트워크 트래픽 모니터링**: 디바이스별 네트워크 사용량 추적
|
- **전력 소비 추적**: Tapo P110 플러그를 통한 실시간 전력 데이터 수집
|
||||||
|
- **확장 가능한 아키텍처**: 설정 파일 기반 기기 관리
|
||||||
|
- **RESTful API**: Express.js 기반 완전한 백엔드 API
|
||||||
|
- **데이터베이스**: MariaDB를 통한 안정적인 데이터 저장
|
||||||
|
- **캐시 시스템**: Redis를 통한 고성능 데이터 처리
|
||||||
|
|
||||||
|
### 🚧 **개발 예정**
|
||||||
|
- **웹 대시보드**: 실시간 모니터링 인터페이스
|
||||||
|
- **개인 문서관리**: Paperless 연동 문서 시스템
|
||||||
- **시스템 리소스 모니터링**: CPU, 메모리, 디스크 사용량 추적
|
- **시스템 리소스 모니터링**: CPU, 메모리, 디스크 사용량 추적
|
||||||
|
- **네트워크 트래픽 모니터링**: 디바이스별 네트워크 사용량 추적
|
||||||
- **알림 시스템**: 임계값 기반 실시간 알림
|
- **알림 시스템**: 임계값 기반 실시간 알림
|
||||||
- **사용자 관리**: 역할 기반 접근 제어
|
- **사용자 관리**: 역할 기반 접근 제어
|
||||||
|
|
||||||
@@ -28,7 +37,7 @@
|
|||||||
- Docker & Docker Compose
|
- Docker & Docker Compose
|
||||||
- Git
|
- Git
|
||||||
|
|
||||||
## 🚀 빠른 시작 (Mac Mini)
|
## 🚀 빠른 시작
|
||||||
|
|
||||||
### 1. 저장소 클론
|
### 1. 저장소 클론
|
||||||
|
|
||||||
@@ -37,57 +46,91 @@ git clone https://git.hyungi.net/hyungi/myhome-server.git
|
|||||||
cd myhome-server
|
cd myhome-server
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Mac Mini 자동 설정
|
### 2. 필요한 디렉토리 생성
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 실행 권한 부여
|
mkdir -p /Users/$(whoami)/home-management-db
|
||||||
chmod +x scripts/setup-mac-mini.sh
|
mkdir -p /Users/$(whoami)/home-management-redis
|
||||||
|
mkdir -p /Users/$(whoami)/home-management-data
|
||||||
# 자동 설정 실행
|
mkdir -p /Users/$(whoami)/home-management-logs
|
||||||
./scripts/setup-mac-mini.sh
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. 수동 설정 (선택사항)
|
### 3. Docker Compose 실행
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Mac Mini 전용 Docker Compose 사용
|
# 모든 서비스 시작
|
||||||
docker-compose -f docker-compose.mac-mini.yml up -d
|
docker-compose up -d
|
||||||
|
|
||||||
|
# 로그 확인
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
# 서비스 상태 확인
|
||||||
|
docker-compose ps
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. 수동 배포
|
### 4. Tapo 기기 설정
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 배포 스크립트 사용
|
# Tapo 기기 설정 파일 편집
|
||||||
chmod +x scripts/deploy-mac-mini.sh
|
nano config/tapo-devices.json
|
||||||
./scripts/deploy-mac-mini.sh
|
|
||||||
|
|
||||||
# 또는 직접 배포
|
# 실제 IP, 이메일, 패스워드 입력 후 저장
|
||||||
git pull origin main
|
|
||||||
docker-compose -f docker-compose.mac-mini.yml up -d --build
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📊 서비스 확인 (Mac Mini)
|
## 📊 서비스 확인
|
||||||
|
|
||||||
- **API 서버**: http://localhost:3000 (Mac Mini 로컬)
|
### 🌐 **서비스 접속 정보**
|
||||||
- **API 서버**: http://mac-mini-m4.local:3000 (네트워크 접근)
|
- **API 서버**: http://localhost:9306
|
||||||
- **phpMyAdmin**: http://localhost:8080
|
- **phpMyAdmin**: http://localhost:9304
|
||||||
- **API 문서**: http://localhost:3000/api
|
- **MariaDB**: localhost:9305
|
||||||
|
- **Redis**: localhost:9307
|
||||||
|
|
||||||
### 헬스체크
|
### 🏥 **헬스체크**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Mac Mini 로컬에서
|
# API 서버 상태 확인
|
||||||
curl http://localhost:3000/health
|
curl http://localhost:9306/health
|
||||||
|
|
||||||
# 다른 기기에서 (IP 주소는 실제 Mac Mini IP로 변경)
|
# API 엔드포인트 목록 확인
|
||||||
curl http://192.168.1.100:3000/health
|
curl http://localhost:9306/api
|
||||||
|
|
||||||
|
# Tapo 기기 목록 확인
|
||||||
|
curl http://localhost:9306/api/tapo/devices
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📚 API 엔드포인트
|
## 📚 API 엔드포인트
|
||||||
|
|
||||||
### 디바이스 관리
|
### 🔌 Tapo 스마트 플러그 관리
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 기기 목록 조회
|
||||||
|
GET /api/tapo/devices
|
||||||
|
|
||||||
|
# 새 기기 추가
|
||||||
|
POST /api/tapo/devices
|
||||||
|
|
||||||
|
# 기기 설정 업데이트
|
||||||
|
PUT /api/tapo/devices/:deviceId
|
||||||
|
|
||||||
|
# 기기 제거
|
||||||
|
DELETE /api/tapo/devices/:deviceId
|
||||||
|
|
||||||
|
# 실시간 전력 데이터 조회
|
||||||
|
GET /api/tapo/devices/:deviceId/power
|
||||||
|
|
||||||
|
# 모든 기기 전력 데이터 조회
|
||||||
|
GET /api/tapo/power
|
||||||
|
|
||||||
|
# 연결 테스트
|
||||||
|
POST /api/tapo/test-connection
|
||||||
|
|
||||||
|
# 기기 설정 템플릿
|
||||||
|
GET /api/tapo/template
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 📊 디바이스 관리
|
||||||
|
|
||||||
|
```bash
|
||||||
GET /api/devices # 모든 디바이스 조회
|
GET /api/devices # 모든 디바이스 조회
|
||||||
GET /api/devices/:id # 특정 디바이스 조회
|
GET /api/devices/:id # 특정 디바이스 조회
|
||||||
POST /api/devices # 새 디바이스 생성
|
POST /api/devices # 새 디바이스 생성
|
||||||
@@ -95,29 +138,39 @@ PUT /api/devices/:id # 디바이스 업데이트
|
|||||||
DELETE /api/devices/:id # 디바이스 삭제
|
DELETE /api/devices/:id # 디바이스 삭제
|
||||||
```
|
```
|
||||||
|
|
||||||
### 기본 사용 예제
|
### 💡 기본 사용 예제
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 디바이스 목록 조회
|
# Tapo 기기 목록 조회
|
||||||
curl http://localhost:3000/api/devices
|
curl http://localhost:9306/api/tapo/devices
|
||||||
|
|
||||||
# 새 디바이스 생성
|
# Tapo 기기 설정 템플릿 확인
|
||||||
curl -X POST http://localhost:3000/api/devices \
|
curl http://localhost:9306/api/tapo/template
|
||||||
|
|
||||||
|
# 새 Tapo 기기 추가
|
||||||
|
curl -X POST http://localhost:9306/api/tapo/devices \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{
|
-d '{
|
||||||
"device_id": "test_device",
|
"id": "living_room_plug",
|
||||||
"name": "테스트 디바이스",
|
"name": "거실 스마트 플러그",
|
||||||
"device_type": "server",
|
"ip": "192.168.1.100",
|
||||||
"location": "테스트실"
|
"email": "your-tapo-email@gmail.com",
|
||||||
|
"password": "your-tapo-password",
|
||||||
|
"location": "거실",
|
||||||
|
"device_type": "smart_plug",
|
||||||
|
"enabled": true
|
||||||
}'
|
}'
|
||||||
|
|
||||||
|
# 전력 소비 데이터 조회
|
||||||
|
curl http://localhost:9306/api/tapo/power
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🗄️ 데이터베이스
|
## 🗄️ 데이터베이스
|
||||||
|
|
||||||
### 접속 정보 (Mac Mini)
|
### 접속 정보
|
||||||
|
|
||||||
- **Host**: localhost (Mac Mini 로컬) / mac-mini-m4.local (네트워크)
|
- **Host**: localhost
|
||||||
- **Port**: 3306
|
- **Port**: 9305
|
||||||
- **Database**: home_management
|
- **Database**: home_management
|
||||||
- **Username**: homeuser
|
- **Username**: homeuser
|
||||||
- **Password**: mac_mini_home_password
|
- **Password**: mac_mini_home_password
|
||||||
|
|||||||
@@ -36,8 +36,6 @@ innodb_read_io_threads = 2
|
|||||||
innodb_write_io_threads = 2
|
innodb_write_io_threads = 2
|
||||||
|
|
||||||
# 시계열 데이터 최적화
|
# 시계열 데이터 최적화
|
||||||
innodb_compression_default = ON
|
|
||||||
innodb_page_compression = ON
|
|
||||||
innodb_adaptive_hash_index = ON
|
innodb_adaptive_hash_index = ON
|
||||||
|
|
||||||
# 로깅
|
# 로깅
|
||||||
|
|||||||
40
config/tapo-devices.json
Normal file
40
config/tapo-devices.json
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"devices": [
|
||||||
|
{
|
||||||
|
"id": "mac_mini_power",
|
||||||
|
"name": "Mac Mini M4 Pro 전력",
|
||||||
|
"ip": "192.168.1.101",
|
||||||
|
"email": "your-tapo-email@gmail.com",
|
||||||
|
"password": "your-tapo-password",
|
||||||
|
"location": "서재",
|
||||||
|
"device_type": "server",
|
||||||
|
"enabled": true,
|
||||||
|
"poll_interval": 300000,
|
||||||
|
"description": "Mac Mini M4 Pro 서버의 전력 소비 모니터링"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "nas_power",
|
||||||
|
"name": "Synology DS1525+ 전력",
|
||||||
|
"ip": "192.168.1.102",
|
||||||
|
"email": "your-tapo-email@gmail.com",
|
||||||
|
"password": "your-tapo-password",
|
||||||
|
"location": "서재",
|
||||||
|
"device_type": "nas",
|
||||||
|
"enabled": true,
|
||||||
|
"poll_interval": 300000,
|
||||||
|
"description": "Synology NAS의 전력 소비 모니터링"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default_settings": {
|
||||||
|
"poll_interval": 300000,
|
||||||
|
"retry_attempts": 3,
|
||||||
|
"timeout": 5000,
|
||||||
|
"email": "your-tapo-email@gmail.com",
|
||||||
|
"password": "your-tapo-password"
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"power_threshold": 100,
|
||||||
|
"offline_alert": true,
|
||||||
|
"daily_report": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
|
||||||
mariadb:
|
|
||||||
image: mariadb:11-jammy
|
|
||||||
container_name: home_mariadb
|
|
||||||
environment:
|
|
||||||
MYSQL_ROOT_PASSWORD: mac_mini_root_password
|
|
||||||
MYSQL_DATABASE: home_management
|
|
||||||
MYSQL_USER: homeuser
|
|
||||||
MYSQL_PASSWORD: mac_mini_home_password
|
|
||||||
ports:
|
|
||||||
- "3306:3306"
|
|
||||||
volumes:
|
|
||||||
- mariadb_data:/var/lib/mysql
|
|
||||||
- ./scripts/setup-db.sql:/docker-entrypoint-initdb.d/setup.sql
|
|
||||||
- ./config/mariadb.cnf:/etc/mysql/conf.d/custom.cnf
|
|
||||||
command: >
|
|
||||||
--character-set-server=utf8mb4
|
|
||||||
--collation-server=utf8mb4_unicode_ci
|
|
||||||
--innodb-buffer-pool-size=4G
|
|
||||||
--innodb-log-file-size=512M
|
|
||||||
--max-connections=200
|
|
||||||
--query-cache-size=256M
|
|
||||||
restart: unless-stopped
|
|
||||||
networks:
|
|
||||||
- home_network
|
|
||||||
|
|
||||||
phpmyadmin:
|
|
||||||
image: phpmyadmin/phpmyadmin:latest
|
|
||||||
container_name: home_phpmyadmin
|
|
||||||
environment:
|
|
||||||
PMA_HOST: mariadb
|
|
||||||
PMA_PORT: 3306
|
|
||||||
PMA_USER: homeuser
|
|
||||||
PMA_PASSWORD: mac_mini_home_password
|
|
||||||
UPLOAD_LIMIT: 2G
|
|
||||||
MEMORY_LIMIT: 512M
|
|
||||||
ports:
|
|
||||||
- "8080:80"
|
|
||||||
depends_on:
|
|
||||||
- mariadb
|
|
||||||
restart: unless-stopped
|
|
||||||
networks:
|
|
||||||
- home_network
|
|
||||||
|
|
||||||
redis:
|
|
||||||
image: redis:7-alpine
|
|
||||||
container_name: home_redis
|
|
||||||
ports:
|
|
||||||
- "6379:6379"
|
|
||||||
volumes:
|
|
||||||
- redis_data:/data
|
|
||||||
- ./config/redis.conf:/usr/local/etc/redis/redis.conf
|
|
||||||
command: redis-server /usr/local/etc/redis/redis.conf
|
|
||||||
restart: unless-stopped
|
|
||||||
networks:
|
|
||||||
- home_network
|
|
||||||
|
|
||||||
api:
|
|
||||||
build: .
|
|
||||||
container_name: home_api
|
|
||||||
environment:
|
|
||||||
NODE_ENV: production
|
|
||||||
DB_HOST: mariadb
|
|
||||||
DB_PORT: 3306
|
|
||||||
DB_NAME: home_management
|
|
||||||
DB_USER: homeuser
|
|
||||||
DB_PASSWORD: mac_mini_home_password
|
|
||||||
REDIS_HOST: redis
|
|
||||||
REDIS_PORT: 6379
|
|
||||||
JWT_SECRET: mac-mini-production-jwt-secret-key-2025
|
|
||||||
API_PORT: 3000
|
|
||||||
API_HOST: 0.0.0.0
|
|
||||||
CORS_ORIGIN: http://mac-mini-m4.local:3001,http://localhost:3001,http://192.168.1.100:3001
|
|
||||||
LOG_LEVEL: info
|
|
||||||
BCRYPT_ROUNDS: 12
|
|
||||||
ports:
|
|
||||||
- "3000:3000"
|
|
||||||
volumes:
|
|
||||||
- ./logs:/app/logs
|
|
||||||
- ./uploads:/app/uploads
|
|
||||||
- /Users/hyungi/home-management-data:/app/data
|
|
||||||
depends_on:
|
|
||||||
- mariadb
|
|
||||||
- redis
|
|
||||||
restart: unless-stopped
|
|
||||||
networks:
|
|
||||||
- home_network
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
start_period: 40s
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
mariadb_data:
|
|
||||||
driver: local
|
|
||||||
driver_opts:
|
|
||||||
type: none
|
|
||||||
o: bind
|
|
||||||
device: /Users/hyungi/home-management-db
|
|
||||||
redis_data:
|
|
||||||
driver: local
|
|
||||||
driver_opts:
|
|
||||||
type: none
|
|
||||||
o: bind
|
|
||||||
device: /Users/hyungi/home-management-redis
|
|
||||||
|
|
||||||
networks:
|
|
||||||
home_network:
|
|
||||||
driver: bridge
|
|
||||||
ipam:
|
|
||||||
config:
|
|
||||||
- subnet: 172.20.0.0/16
|
|
||||||
@@ -1,16 +1,14 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
mariadb:
|
mariadb:
|
||||||
image: mariadb:11-jammy
|
image: mariadb:11-jammy
|
||||||
container_name: home_mariadb
|
container_name: home_mariadb
|
||||||
environment:
|
environment:
|
||||||
MYSQL_ROOT_PASSWORD: root_password
|
MYSQL_ROOT_PASSWORD: mac_mini_root_password
|
||||||
MYSQL_DATABASE: home_management
|
MYSQL_DATABASE: home_management
|
||||||
MYSQL_USER: homeuser
|
MYSQL_USER: homeuser
|
||||||
MYSQL_PASSWORD: home_password
|
MYSQL_PASSWORD: mac_mini_home_password
|
||||||
ports:
|
ports:
|
||||||
- "3306:3306"
|
- "9305:3306"
|
||||||
volumes:
|
volumes:
|
||||||
- mariadb_data:/var/lib/mysql
|
- mariadb_data:/var/lib/mysql
|
||||||
- ./scripts/setup-db.sql:/docker-entrypoint-initdb.d/setup.sql
|
- ./scripts/setup-db.sql:/docker-entrypoint-initdb.d/setup.sql
|
||||||
@@ -18,8 +16,8 @@ services:
|
|||||||
command: >
|
command: >
|
||||||
--character-set-server=utf8mb4
|
--character-set-server=utf8mb4
|
||||||
--collation-server=utf8mb4_unicode_ci
|
--collation-server=utf8mb4_unicode_ci
|
||||||
--innodb-buffer-pool-size=2G
|
--innodb-buffer-pool-size=4G
|
||||||
--innodb-log-file-size=256M
|
--innodb-log-file-size=512M
|
||||||
--max-connections=200
|
--max-connections=200
|
||||||
--query-cache-size=256M
|
--query-cache-size=256M
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
@@ -33,11 +31,11 @@ services:
|
|||||||
PMA_HOST: mariadb
|
PMA_HOST: mariadb
|
||||||
PMA_PORT: 3306
|
PMA_PORT: 3306
|
||||||
PMA_USER: homeuser
|
PMA_USER: homeuser
|
||||||
PMA_PASSWORD: home_password
|
PMA_PASSWORD: mac_mini_home_password
|
||||||
UPLOAD_LIMIT: 2G
|
UPLOAD_LIMIT: 2G
|
||||||
MEMORY_LIMIT: 512M
|
MEMORY_LIMIT: 512M
|
||||||
ports:
|
ports:
|
||||||
- "8080:80"
|
- "9304:80"
|
||||||
depends_on:
|
depends_on:
|
||||||
- mariadb
|
- mariadb
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
@@ -48,7 +46,7 @@ services:
|
|||||||
image: redis:7-alpine
|
image: redis:7-alpine
|
||||||
container_name: home_redis
|
container_name: home_redis
|
||||||
ports:
|
ports:
|
||||||
- "6379:6379"
|
- "9307:6379"
|
||||||
volumes:
|
volumes:
|
||||||
- redis_data:/data
|
- redis_data:/data
|
||||||
- ./config/redis.conf:/usr/local/etc/redis/redis.conf
|
- ./config/redis.conf:/usr/local/etc/redis/redis.conf
|
||||||
@@ -66,29 +64,51 @@ services:
|
|||||||
DB_PORT: 3306
|
DB_PORT: 3306
|
||||||
DB_NAME: home_management
|
DB_NAME: home_management
|
||||||
DB_USER: homeuser
|
DB_USER: homeuser
|
||||||
DB_PASSWORD: home_password
|
DB_PASSWORD: mac_mini_home_password
|
||||||
REDIS_HOST: redis
|
REDIS_HOST: redis
|
||||||
REDIS_PORT: 6379
|
REDIS_PORT: 6379
|
||||||
JWT_SECRET: mac-mini-jwt-secret-key
|
JWT_SECRET: mac-mini-production-jwt-secret-key-2025
|
||||||
API_PORT: 3000
|
API_PORT: 3000
|
||||||
API_HOST: 0.0.0.0
|
API_HOST: 0.0.0.0
|
||||||
CORS_ORIGIN: http://mac-mini-m4:3001,http://localhost:3001
|
CORS_ORIGIN: http://mac-mini-m4.local:3001,http://localhost:3001,http://192.168.1.100:3001
|
||||||
|
LOG_LEVEL: info
|
||||||
|
BCRYPT_ROUNDS: 12
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "9306:3000"
|
||||||
volumes:
|
volumes:
|
||||||
- ./logs:/app/logs
|
- ./logs:/app/logs
|
||||||
- ./uploads:/app/uploads
|
- ./uploads:/app/uploads
|
||||||
|
- /Users/hyungiahn/home-management-data:/app/data
|
||||||
depends_on:
|
depends_on:
|
||||||
- mariadb
|
- mariadb
|
||||||
- redis
|
- redis
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
- home_network
|
- home_network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
mariadb_data:
|
mariadb_data:
|
||||||
|
driver: local
|
||||||
|
driver_opts:
|
||||||
|
type: none
|
||||||
|
o: bind
|
||||||
|
device: /Users/hyungiahn/home-management-db
|
||||||
redis_data:
|
redis_data:
|
||||||
|
driver: local
|
||||||
|
driver_opts:
|
||||||
|
type: none
|
||||||
|
o: bind
|
||||||
|
device: /Users/hyungiahn/home-management-redis
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
home_network:
|
home_network:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
ipam:
|
||||||
|
config:
|
||||||
|
- subnet: 172.20.0.0/16
|
||||||
7202
package-lock.json
generated
Normal file
7202
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
35
package.json
35
package.json
@@ -17,29 +17,30 @@
|
|||||||
"lint:fix": "eslint src/ --fix"
|
"lint:fix": "eslint src/ --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^4.18.2",
|
|
||||||
"mysql2": "^3.6.0",
|
|
||||||
"sequelize": "^6.32.1",
|
|
||||||
"redis": "^4.6.7",
|
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"jsonwebtoken": "^9.0.2",
|
|
||||||
"joi": "^17.9.2",
|
|
||||||
"helmet": "^7.0.0",
|
|
||||||
"cors": "^2.8.5",
|
|
||||||
"compression": "^1.7.4",
|
"compression": "^1.7.4",
|
||||||
"express-rate-limit": "^6.8.1",
|
"cors": "^2.8.5",
|
||||||
"winston": "^3.10.0",
|
|
||||||
"node-cron": "^3.0.2",
|
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"express-rate-limit": "^6.8.1",
|
||||||
"express-validator": "^7.0.1",
|
"express-validator": "^7.0.1",
|
||||||
"moment": "^2.29.4"
|
"helmet": "^7.0.0",
|
||||||
|
"joi": "^17.9.2",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"moment": "^2.29.4",
|
||||||
|
"mysql2": "^3.6.0",
|
||||||
|
"node-cron": "^3.0.2",
|
||||||
|
"redis": "^4.6.7",
|
||||||
|
"sequelize": "^6.32.1",
|
||||||
|
"tp-link-tapo-connect": "^2.0.7",
|
||||||
|
"winston": "^3.10.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"nodemon": "^3.0.1",
|
|
||||||
"jest": "^29.6.1",
|
|
||||||
"supertest": "^6.3.3",
|
|
||||||
"eslint": "^8.45.0",
|
"eslint": "^8.45.0",
|
||||||
"prettier": "^3.0.0"
|
"jest": "^29.6.1",
|
||||||
|
"nodemon": "^3.0.1",
|
||||||
|
"prettier": "^3.0.0",
|
||||||
|
"supertest": "^6.3.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
@@ -53,4 +54,4 @@
|
|||||||
],
|
],
|
||||||
"author": "hyungi",
|
"author": "hyungi",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
}
|
}
|
||||||
|
|||||||
347
src/collectors/tapoCollector.js
Normal file
347
src/collectors/tapoCollector.js
Normal file
@@ -0,0 +1,347 @@
|
|||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs').promises;
|
||||||
|
const logger = require('../utils/logger');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tapo 스마트 플러그 데이터 수집기
|
||||||
|
* 설정 파일 기반으로 동적 기기 관리 지원
|
||||||
|
*/
|
||||||
|
class TapoCollector {
|
||||||
|
constructor() {
|
||||||
|
this.devices = new Map();
|
||||||
|
this.apis = new Map();
|
||||||
|
this.configPath = path.join(__dirname, '../../config/tapo-devices.json');
|
||||||
|
this.isInitialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 설정 파일에서 기기 정보를 로드하고 API 초기화
|
||||||
|
*/
|
||||||
|
async initialize() {
|
||||||
|
try {
|
||||||
|
// Tapo API 라이브러리 동적 로드
|
||||||
|
const tapoLibrary = await this.loadTapoLibrary();
|
||||||
|
|
||||||
|
// 설정 파일 로드
|
||||||
|
await this.loadConfig();
|
||||||
|
|
||||||
|
// 활성화된 기기들 초기화
|
||||||
|
for (const [deviceId, deviceConfig] of this.devices) {
|
||||||
|
if (deviceConfig.enabled) {
|
||||||
|
await this.initializeDevice(deviceId, deviceConfig, tapoLibrary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isInitialized = true;
|
||||||
|
logger.info(`TapoCollector initialized with ${this.apis.size} active devices`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to initialize TapoCollector:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tapo API 라이브러리 로드
|
||||||
|
*/
|
||||||
|
async loadTapoLibrary() {
|
||||||
|
try {
|
||||||
|
return require('tp-link-tapo-connect');
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Tapo API library not found. Please install: npm install tp-link-tapo-connect');
|
||||||
|
throw new Error('Missing tp-link-tapo-connect package. Install with: npm install tp-link-tapo-connect');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 설정 파일에서 기기 정보 로드
|
||||||
|
*/
|
||||||
|
async loadConfig() {
|
||||||
|
try {
|
||||||
|
const configData = await fs.readFile(this.configPath, 'utf8');
|
||||||
|
const config = JSON.parse(configData);
|
||||||
|
|
||||||
|
this.devices.clear();
|
||||||
|
|
||||||
|
for (const device of config.devices) {
|
||||||
|
// 기본값 병합
|
||||||
|
const deviceConfig = {
|
||||||
|
...config.default_settings,
|
||||||
|
...device
|
||||||
|
};
|
||||||
|
|
||||||
|
this.devices.set(device.id, deviceConfig);
|
||||||
|
logger.info(`Loaded device config: ${device.id} (${device.name})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.notificationSettings = config.notifications || {};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to load Tapo device config:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 개별 기기 API 초기화
|
||||||
|
*/
|
||||||
|
async initializeDevice(deviceId, deviceConfig, tapoLibrary) {
|
||||||
|
try {
|
||||||
|
// tp-link-tapo-connect API 사용
|
||||||
|
const device = await tapoLibrary.loginDeviceByIp(
|
||||||
|
deviceConfig.email,
|
||||||
|
deviceConfig.password,
|
||||||
|
deviceConfig.ip
|
||||||
|
);
|
||||||
|
|
||||||
|
// 기기 정보 확인
|
||||||
|
const deviceInfo = await device.getDeviceInfo();
|
||||||
|
logger.info(`Connected to ${deviceId}: ${deviceInfo.nickname || deviceConfig.name} (${deviceInfo.model})`);
|
||||||
|
|
||||||
|
this.apis.set(deviceId, device);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to initialize device ${deviceId}:`, error);
|
||||||
|
// 개별 기기 실패가 전체 시스템을 중단시키지 않도록 함
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 모든 활성 기기에서 전력 데이터 수집
|
||||||
|
*/
|
||||||
|
async collectPowerData() {
|
||||||
|
if (!this.isInitialized) {
|
||||||
|
logger.warn('TapoCollector not initialized, skipping collection');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = [];
|
||||||
|
const promises = [];
|
||||||
|
|
||||||
|
for (const [deviceId, api] of this.apis) {
|
||||||
|
promises.push(this.collectFromDevice(deviceId, api));
|
||||||
|
}
|
||||||
|
|
||||||
|
const deviceResults = await Promise.allSettled(promises);
|
||||||
|
|
||||||
|
for (let i = 0; i < deviceResults.length; i++) {
|
||||||
|
const result = deviceResults[i];
|
||||||
|
if (result.status === 'fulfilled' && result.value) {
|
||||||
|
results.push(result.value);
|
||||||
|
} else if (result.status === 'rejected') {
|
||||||
|
logger.error(`Data collection failed for device:`, result.reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Collected power data from ${results.length} devices`);
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 개별 기기에서 데이터 수집
|
||||||
|
*/
|
||||||
|
async collectFromDevice(deviceId, api) {
|
||||||
|
try {
|
||||||
|
const deviceConfig = this.devices.get(deviceId);
|
||||||
|
|
||||||
|
// 기기 정보 및 전력 사용량 조회
|
||||||
|
const [deviceInfo, energyUsage] = await Promise.all([
|
||||||
|
api.getDeviceInfo(),
|
||||||
|
api.getEnergyUsage()
|
||||||
|
]);
|
||||||
|
|
||||||
|
const powerData = {
|
||||||
|
device_id: deviceId,
|
||||||
|
timestamp: new Date(),
|
||||||
|
watts: (energyUsage?.current_power || 0) / 1000, // mW → W
|
||||||
|
voltage: deviceInfo?.voltage || null,
|
||||||
|
current: deviceInfo?.current || null,
|
||||||
|
kwh_total: (energyUsage?.today_energy || 0) / 1000, // Wh → kWh
|
||||||
|
metadata: {
|
||||||
|
device_name: deviceConfig.name,
|
||||||
|
location: deviceConfig.location,
|
||||||
|
device_type: deviceConfig.device_type,
|
||||||
|
model: deviceInfo?.model || 'unknown',
|
||||||
|
fw_version: deviceInfo?.fw_ver || 'unknown',
|
||||||
|
online_status: deviceInfo?.device_on || false,
|
||||||
|
signal_level: deviceInfo?.rssi || null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 임계값 알림 체크
|
||||||
|
await this.checkThresholds(deviceId, powerData);
|
||||||
|
|
||||||
|
return powerData;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to collect data from ${deviceId}:`, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 전력 임계값 체크 및 알림
|
||||||
|
*/
|
||||||
|
async checkThresholds(deviceId, powerData) {
|
||||||
|
const threshold = this.notificationSettings.power_threshold || 100;
|
||||||
|
|
||||||
|
if (powerData.watts > threshold) {
|
||||||
|
logger.warn(`High power consumption detected: ${deviceId} using ${powerData.watts}W`);
|
||||||
|
// TODO: 알림 시스템 연동
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 새 기기 추가 (런타임에 동적 추가)
|
||||||
|
*/
|
||||||
|
async addDevice(deviceConfig) {
|
||||||
|
try {
|
||||||
|
// 설정 검증
|
||||||
|
this.validateDeviceConfig(deviceConfig);
|
||||||
|
|
||||||
|
// 기기 연결 테스트
|
||||||
|
const { TapoAPI } = await this.loadTapoLibrary();
|
||||||
|
await this.initializeDevice(deviceConfig.id, deviceConfig, TapoAPI);
|
||||||
|
|
||||||
|
// 메모리에 추가
|
||||||
|
this.devices.set(deviceConfig.id, deviceConfig);
|
||||||
|
|
||||||
|
// 설정 파일 업데이트
|
||||||
|
await this.saveConfig();
|
||||||
|
|
||||||
|
logger.info(`Device added successfully: ${deviceConfig.id}`);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to add device ${deviceConfig.id}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 기기 제거
|
||||||
|
*/
|
||||||
|
async removeDevice(deviceId) {
|
||||||
|
try {
|
||||||
|
// API 연결 종료
|
||||||
|
if (this.apis.has(deviceId)) {
|
||||||
|
this.apis.delete(deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 메모리에서 제거
|
||||||
|
this.devices.delete(deviceId);
|
||||||
|
|
||||||
|
// 설정 파일 업데이트
|
||||||
|
await this.saveConfig();
|
||||||
|
|
||||||
|
logger.info(`Device removed successfully: ${deviceId}`);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to remove device ${deviceId}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 기기 설정 업데이트
|
||||||
|
*/
|
||||||
|
async updateDevice(deviceId, updates) {
|
||||||
|
try {
|
||||||
|
const currentConfig = this.devices.get(deviceId);
|
||||||
|
if (!currentConfig) {
|
||||||
|
throw new Error(`Device not found: ${deviceId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedConfig = { ...currentConfig, ...updates };
|
||||||
|
this.validateDeviceConfig(updatedConfig);
|
||||||
|
|
||||||
|
// 설정 업데이트
|
||||||
|
this.devices.set(deviceId, updatedConfig);
|
||||||
|
|
||||||
|
// API 재초기화 (IP나 인증 정보가 변경된 경우)
|
||||||
|
if (updates.ip || updates.email || updates.password) {
|
||||||
|
const { TapoAPI } = await this.loadTapoLibrary();
|
||||||
|
await this.initializeDevice(deviceId, updatedConfig, TapoAPI);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 설정 파일 업데이트
|
||||||
|
await this.saveConfig();
|
||||||
|
|
||||||
|
logger.info(`Device updated successfully: ${deviceId}`);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to update device ${deviceId}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 기기 설정 검증
|
||||||
|
*/
|
||||||
|
validateDeviceConfig(config) {
|
||||||
|
const required = ['id', 'name', 'ip', 'email', 'password'];
|
||||||
|
for (const field of required) {
|
||||||
|
if (!config[field]) {
|
||||||
|
throw new Error(`Missing required field: ${field}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IP 주소 형식 검증
|
||||||
|
const ipRegex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
|
||||||
|
if (!ipRegex.test(config.ip)) {
|
||||||
|
throw new Error(`Invalid IP address: ${config.ip}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 현재 설정을 파일에 저장
|
||||||
|
*/
|
||||||
|
async saveConfig() {
|
||||||
|
try {
|
||||||
|
const config = {
|
||||||
|
devices: Array.from(this.devices.values()),
|
||||||
|
default_settings: {
|
||||||
|
poll_interval: 300000,
|
||||||
|
retry_attempts: 3,
|
||||||
|
timeout: 5000
|
||||||
|
},
|
||||||
|
notifications: this.notificationSettings
|
||||||
|
};
|
||||||
|
|
||||||
|
await fs.writeFile(this.configPath, JSON.stringify(config, null, 2), 'utf8');
|
||||||
|
logger.info('Tapo device configuration saved');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to save Tapo device configuration:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 현재 연결된 기기 목록 반환
|
||||||
|
*/
|
||||||
|
getConnectedDevices() {
|
||||||
|
return Array.from(this.devices.entries()).map(([id, config]) => ({
|
||||||
|
id,
|
||||||
|
name: config.name,
|
||||||
|
location: config.location,
|
||||||
|
device_type: config.device_type,
|
||||||
|
enabled: config.enabled,
|
||||||
|
connected: this.apis.has(id)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 리소스 정리
|
||||||
|
*/
|
||||||
|
async cleanup() {
|
||||||
|
logger.info('Cleaning up TapoCollector resources');
|
||||||
|
this.apis.clear();
|
||||||
|
this.devices.clear();
|
||||||
|
this.isInitialized = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = TapoCollector;
|
||||||
238
src/controllers/tapoController.js
Normal file
238
src/controllers/tapoController.js
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
const TapoCollector = require('../collectors/tapoCollector');
|
||||||
|
const logger = require('../utils/logger');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tapo 기기 관리 컨트롤러
|
||||||
|
* 동적 기기 추가/제거/수정 기능 제공
|
||||||
|
*/
|
||||||
|
class TapoController {
|
||||||
|
constructor() {
|
||||||
|
this.collector = new TapoCollector();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 연결된 모든 Tapo 기기 목록 조회
|
||||||
|
*/
|
||||||
|
async getDevices(req, res) {
|
||||||
|
try {
|
||||||
|
const devices = this.collector.getConnectedDevices();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
count: devices.length,
|
||||||
|
devices: devices
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to get Tapo devices:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Failed to retrieve device list'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 새로운 Tapo 기기 추가
|
||||||
|
*/
|
||||||
|
async addDevice(req, res) {
|
||||||
|
try {
|
||||||
|
const deviceConfig = req.body;
|
||||||
|
|
||||||
|
// 기본 검증
|
||||||
|
if (!deviceConfig.id || !deviceConfig.name || !deviceConfig.ip) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Missing required fields: id, name, ip'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 기기 추가
|
||||||
|
await this.collector.addDevice(deviceConfig);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: `Device ${deviceConfig.id} added successfully`,
|
||||||
|
device: deviceConfig
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to add Tapo device:', error);
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tapo 기기 설정 업데이트
|
||||||
|
*/
|
||||||
|
async updateDevice(req, res) {
|
||||||
|
try {
|
||||||
|
const { deviceId } = req.params;
|
||||||
|
const updates = req.body;
|
||||||
|
|
||||||
|
await this.collector.updateDevice(deviceId, updates);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: `Device ${deviceId} updated successfully`
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to update Tapo device:', error);
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tapo 기기 제거
|
||||||
|
*/
|
||||||
|
async removeDevice(req, res) {
|
||||||
|
try {
|
||||||
|
const { deviceId } = req.params;
|
||||||
|
|
||||||
|
await this.collector.removeDevice(deviceId);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: `Device ${deviceId} removed successfully`
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to remove Tapo device:', error);
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 특정 기기의 실시간 전력 데이터 조회
|
||||||
|
*/
|
||||||
|
async getDevicePower(req, res) {
|
||||||
|
try {
|
||||||
|
const { deviceId } = req.params;
|
||||||
|
|
||||||
|
// 단일 기기 데이터 수집
|
||||||
|
const powerData = await this.collector.collectFromDevice(deviceId);
|
||||||
|
|
||||||
|
if (!powerData) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Device not found or data collection failed'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
device_id: deviceId,
|
||||||
|
data: powerData
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to get device power data:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Failed to retrieve power data'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 모든 기기의 전력 데이터 일괄 조회
|
||||||
|
*/
|
||||||
|
async getAllDevicesPower(req, res) {
|
||||||
|
try {
|
||||||
|
const powerDataList = await this.collector.collectPowerData();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
count: powerDataList.length,
|
||||||
|
timestamp: new Date(),
|
||||||
|
data: powerDataList
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to get all devices power data:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Failed to retrieve power data'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 기기 연결 테스트
|
||||||
|
*/
|
||||||
|
async testConnection(req, res) {
|
||||||
|
try {
|
||||||
|
const { ip, email, password } = req.body;
|
||||||
|
|
||||||
|
// 임시 API 인스턴스로 연결 테스트
|
||||||
|
const tapoLibrary = require('tp-link-tapo-connect');
|
||||||
|
const testApi = await tapoLibrary.loginDeviceByIp(email, password, ip);
|
||||||
|
|
||||||
|
const deviceInfo = await testApi.getDeviceInfo();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Connection successful',
|
||||||
|
device_info: {
|
||||||
|
model: deviceInfo.model,
|
||||||
|
nickname: deviceInfo.nickname,
|
||||||
|
device_id: deviceInfo.device_id,
|
||||||
|
fw_version: deviceInfo.fw_ver,
|
||||||
|
hw_version: deviceInfo.hw_ver,
|
||||||
|
online: deviceInfo.device_on
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Connection test failed:', error);
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Connection test failed: ' + error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 기기 설정 템플릿 제공
|
||||||
|
*/
|
||||||
|
async getDeviceTemplate(req, res) {
|
||||||
|
try {
|
||||||
|
const template = {
|
||||||
|
id: "device_unique_id",
|
||||||
|
name: "기기 이름",
|
||||||
|
ip: "192.168.1.xxx",
|
||||||
|
email: "your-tapo-email@gmail.com",
|
||||||
|
password: "your-tapo-password",
|
||||||
|
location: "설치 위치",
|
||||||
|
device_type: "server", // server, nas, monitor, etc.
|
||||||
|
enabled: true,
|
||||||
|
poll_interval: 300000,
|
||||||
|
description: "기기 설명"
|
||||||
|
};
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
template: template,
|
||||||
|
note: "위 템플릿을 참고하여 새 기기를 추가하세요"
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Failed to get template'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new TapoController();
|
||||||
@@ -71,12 +71,12 @@ const User = sequelize.define('User', {
|
|||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
// 모델 관계 설정
|
// 모델 관계 설정 (추후 AlertRule 모델 생성시 활성화)
|
||||||
User.associate = (models) => {
|
// User.associate = (models) => {
|
||||||
User.hasMany(models.AlertRule, {
|
// User.hasMany(models.AlertRule, {
|
||||||
foreignKey: 'created_by',
|
// foreignKey: 'created_by',
|
||||||
onDelete: 'SET NULL'
|
// onDelete: 'SET NULL'
|
||||||
});
|
// });
|
||||||
};
|
// };
|
||||||
|
|
||||||
module.exports = User;
|
module.exports = User;
|
||||||
@@ -10,6 +10,7 @@ router.get('/', (req, res) => {
|
|||||||
endpoints: {
|
endpoints: {
|
||||||
health: '/health',
|
health: '/health',
|
||||||
devices: '/api/devices',
|
devices: '/api/devices',
|
||||||
|
tapo: '/api/tapo',
|
||||||
power: '/api/power',
|
power: '/api/power',
|
||||||
network: '/api/network',
|
network: '/api/network',
|
||||||
system: '/api/system',
|
system: '/api/system',
|
||||||
@@ -21,6 +22,7 @@ router.get('/', (req, res) => {
|
|||||||
|
|
||||||
// 라우터 연결
|
// 라우터 연결
|
||||||
router.use('/devices', require('./devices'));
|
router.use('/devices', require('./devices'));
|
||||||
|
router.use('/tapo', require('./tapo'));
|
||||||
// router.use('/power', require('./power'));
|
// router.use('/power', require('./power'));
|
||||||
// router.use('/network', require('./network'));
|
// router.use('/network', require('./network'));
|
||||||
// router.use('/system', require('./system'));
|
// router.use('/system', require('./system'));
|
||||||
|
|||||||
33
src/routes/tapo.js
Normal file
33
src/routes/tapo.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const tapoController = require('../controllers/tapoController');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tapo 스마트 플러그 관리 API 라우터
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 기기 목록 조회
|
||||||
|
router.get('/devices', tapoController.getDevices.bind(tapoController));
|
||||||
|
|
||||||
|
// 새 기기 추가
|
||||||
|
router.post('/devices', tapoController.addDevice.bind(tapoController));
|
||||||
|
|
||||||
|
// 기기 설정 업데이트
|
||||||
|
router.put('/devices/:deviceId', tapoController.updateDevice.bind(tapoController));
|
||||||
|
|
||||||
|
// 기기 제거
|
||||||
|
router.delete('/devices/:deviceId', tapoController.removeDevice.bind(tapoController));
|
||||||
|
|
||||||
|
// 특정 기기 전력 데이터 조회
|
||||||
|
router.get('/devices/:deviceId/power', tapoController.getDevicePower.bind(tapoController));
|
||||||
|
|
||||||
|
// 모든 기기 전력 데이터 조회
|
||||||
|
router.get('/power', tapoController.getAllDevicesPower.bind(tapoController));
|
||||||
|
|
||||||
|
// 연결 테스트
|
||||||
|
router.post('/test-connection', tapoController.testConnection.bind(tapoController));
|
||||||
|
|
||||||
|
// 기기 설정 템플릿
|
||||||
|
router.get('/template', tapoController.getDeviceTemplate.bind(tapoController));
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
Reference in New Issue
Block a user