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:
Hyungi Ahn
2025-08-12 10:55:12 +09:00
parent 4b77086bb2
commit 1a01809a6e
12 changed files with 8018 additions and 200 deletions

139
README.md
View File

@@ -6,10 +6,19 @@
## 🎯 주요 기능
- **디바이스 관리**: 홈 IoT 기기 등록 및 모니터링
- **전력 소비 추적**: 실시간 전력 데이터 수집 및 분석
- **네트워크 트래픽 모니터링**: 디바이스별 네트워크 사용량 추적
### ✅ **구현 완료**
- **Tapo 스마트 플러그 관리**: 동적 기기 추가/제거/모니터링
- **전력 소비 추적**: Tapo P110 플러그를 통한 실시간 전력 데이터 수집
- **확장 가능한 아키텍처**: 설정 파일 기반 기기 관리
- **RESTful API**: Express.js 기반 완전한 백엔드 API
- **데이터베이스**: MariaDB를 통한 안정적인 데이터 저장
- **캐시 시스템**: Redis를 통한 고성능 데이터 처리
### 🚧 **개발 예정**
- **웹 대시보드**: 실시간 모니터링 인터페이스
- **개인 문서관리**: Paperless 연동 문서 시스템
- **시스템 리소스 모니터링**: CPU, 메모리, 디스크 사용량 추적
- **네트워크 트래픽 모니터링**: 디바이스별 네트워크 사용량 추적
- **알림 시스템**: 임계값 기반 실시간 알림
- **사용자 관리**: 역할 기반 접근 제어
@@ -28,7 +37,7 @@
- Docker & Docker Compose
- Git
## 🚀 빠른 시작 (Mac Mini)
## 🚀 빠른 시작
### 1. 저장소 클론
@@ -37,57 +46,91 @@ git clone https://git.hyungi.net/hyungi/myhome-server.git
cd myhome-server
```
### 2. Mac Mini 자동 설정
### 2. 필요한 디렉토리 생성
```bash
# 실행 권한 부여
chmod +x scripts/setup-mac-mini.sh
# 자동 설정 실행
./scripts/setup-mac-mini.sh
mkdir -p /Users/$(whoami)/home-management-db
mkdir -p /Users/$(whoami)/home-management-redis
mkdir -p /Users/$(whoami)/home-management-data
mkdir -p /Users/$(whoami)/home-management-logs
```
### 3. 수동 설정 (선택사항)
### 3. Docker Compose 실행
```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
# 배포 스크립트 사용
chmod +x scripts/deploy-mac-mini.sh
./scripts/deploy-mac-mini.sh
# Tapo 기기 설정 파일 편집
nano config/tapo-devices.json
# 또는 직접 배포
git pull origin main
docker-compose -f docker-compose.mac-mini.yml up -d --build
# 실제 IP, 이메일, 패스워드 입력 후 저장
```
## 📊 서비스 확인 (Mac Mini)
## 📊 서비스 확인
- **API 서버**: http://localhost:3000 (Mac Mini 로컬)
- **API 서버**: http://mac-mini-m4.local:3000 (네트워크 접근)
- **phpMyAdmin**: http://localhost:8080
- **API 문서**: http://localhost:3000/api
### 🌐 **서비스 접속 정보**
- **API 서버**: http://localhost:9306
- **phpMyAdmin**: http://localhost:9304
- **MariaDB**: localhost:9305
- **Redis**: localhost:9307
### 헬스체크
### 🏥 **헬스체크**
```bash
# Mac Mini 로컬에서
curl http://localhost:3000/health
# API 서버 상태 확인
curl http://localhost:9306/health
# 다른 기기에서 (IP 주소는 실제 Mac Mini IP로 변경)
curl http://192.168.1.100:3000/health
# API 엔드포인트 목록 확인
curl http://localhost:9306/api
# Tapo 기기 목록 확인
curl http://localhost:9306/api/tapo/devices
```
## 📚 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/:id # 특정 디바이스 조회
POST /api/devices # 새 디바이스 생성
@@ -95,29 +138,39 @@ PUT /api/devices/:id # 디바이스 업데이트
DELETE /api/devices/:id # 디바이스 삭제
```
### 기본 사용 예제
### 💡 기본 사용 예제
```bash
# 디바이스 목록 조회
curl http://localhost:3000/api/devices
# Tapo 기기 목록 조회
curl http://localhost:9306/api/tapo/devices
# 새 디바이스 생성
curl -X POST http://localhost:3000/api/devices \
# Tapo 기기 설정 템플릿 확인
curl http://localhost:9306/api/tapo/template
# 새 Tapo 기기 추가
curl -X POST http://localhost:9306/api/tapo/devices \
-H "Content-Type: application/json" \
-d '{
"device_id": "test_device",
"name": "테스트 디바이스",
"device_type": "server",
"location": "테스트실"
"id": "living_room_plug",
"name": "거실 스마트 플러그",
"ip": "192.168.1.100",
"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 (네트워크)
- **Port**: 3306
- **Host**: localhost
- **Port**: 9305
- **Database**: home_management
- **Username**: homeuser
- **Password**: mac_mini_home_password

View File

@@ -36,8 +36,6 @@ innodb_read_io_threads = 2
innodb_write_io_threads = 2
# 시계열 데이터 최적화
innodb_compression_default = ON
innodb_page_compression = ON
innodb_adaptive_hash_index = ON
# 로깅

40
config/tapo-devices.json Normal file
View 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
}
}

View File

@@ -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

View File

@@ -1,16 +1,14 @@
version: '3.8'
services:
mariadb:
image: mariadb:11-jammy
container_name: home_mariadb
environment:
MYSQL_ROOT_PASSWORD: root_password
MYSQL_ROOT_PASSWORD: mac_mini_root_password
MYSQL_DATABASE: home_management
MYSQL_USER: homeuser
MYSQL_PASSWORD: home_password
MYSQL_PASSWORD: mac_mini_home_password
ports:
- "3306:3306"
- "9305:3306"
volumes:
- mariadb_data:/var/lib/mysql
- ./scripts/setup-db.sql:/docker-entrypoint-initdb.d/setup.sql
@@ -18,8 +16,8 @@ services:
command: >
--character-set-server=utf8mb4
--collation-server=utf8mb4_unicode_ci
--innodb-buffer-pool-size=2G
--innodb-log-file-size=256M
--innodb-buffer-pool-size=4G
--innodb-log-file-size=512M
--max-connections=200
--query-cache-size=256M
restart: unless-stopped
@@ -33,11 +31,11 @@ services:
PMA_HOST: mariadb
PMA_PORT: 3306
PMA_USER: homeuser
PMA_PASSWORD: home_password
PMA_PASSWORD: mac_mini_home_password
UPLOAD_LIMIT: 2G
MEMORY_LIMIT: 512M
ports:
- "8080:80"
- "9304:80"
depends_on:
- mariadb
restart: unless-stopped
@@ -48,7 +46,7 @@ services:
image: redis:7-alpine
container_name: home_redis
ports:
- "6379:6379"
- "9307:6379"
volumes:
- redis_data:/data
- ./config/redis.conf:/usr/local/etc/redis/redis.conf
@@ -66,29 +64,51 @@ services:
DB_PORT: 3306
DB_NAME: home_management
DB_USER: homeuser
DB_PASSWORD: home_password
DB_PASSWORD: mac_mini_home_password
REDIS_HOST: redis
REDIS_PORT: 6379
JWT_SECRET: mac-mini-jwt-secret-key
JWT_SECRET: mac-mini-production-jwt-secret-key-2025
API_PORT: 3000
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:
- "3000:3000"
- "9306:3000"
volumes:
- ./logs:/app/logs
- ./uploads:/app/uploads
- /Users/hyungiahn/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/hyungiahn/home-management-db
redis_data:
driver: local
driver_opts:
type: none
o: bind
device: /Users/hyungiahn/home-management-redis
networks:
home_network:
driver: bridge
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16

7202
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -17,29 +17,30 @@
"lint:fix": "eslint src/ --fix"
},
"dependencies": {
"express": "^4.18.2",
"mysql2": "^3.6.0",
"sequelize": "^6.32.1",
"redis": "^4.6.7",
"bcryptjs": "^2.4.3",
"jsonwebtoken": "^9.0.2",
"joi": "^17.9.2",
"helmet": "^7.0.0",
"cors": "^2.8.5",
"compression": "^1.7.4",
"express-rate-limit": "^6.8.1",
"winston": "^3.10.0",
"node-cron": "^3.0.2",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"express-rate-limit": "^6.8.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": {
"nodemon": "^3.0.1",
"jest": "^29.6.1",
"supertest": "^6.3.3",
"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": {
"node": ">=18.0.0"
@@ -53,4 +54,4 @@
],
"author": "hyungi",
"license": "MIT"
}
}

View 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;

View 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();

View File

@@ -71,12 +71,12 @@ const User = sequelize.define('User', {
]
});
// 모델 관계 설정
User.associate = (models) => {
User.hasMany(models.AlertRule, {
foreignKey: 'created_by',
onDelete: 'SET NULL'
});
};
// 모델 관계 설정 (추후 AlertRule 모델 생성시 활성화)
// User.associate = (models) => {
// User.hasMany(models.AlertRule, {
// foreignKey: 'created_by',
// onDelete: 'SET NULL'
// });
// };
module.exports = User;

View File

@@ -10,6 +10,7 @@ router.get('/', (req, res) => {
endpoints: {
health: '/health',
devices: '/api/devices',
tapo: '/api/tapo',
power: '/api/power',
network: '/api/network',
system: '/api/system',
@@ -21,6 +22,7 @@ router.get('/', (req, res) => {
// 라우터 연결
router.use('/devices', require('./devices'));
router.use('/tapo', require('./tapo'));
// router.use('/power', require('./power'));
// router.use('/network', require('./network'));
// router.use('/system', require('./system'));

33
src/routes/tapo.js Normal file
View 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;