1. 引言
随着物联网技术的快速发展,智能家居系统已经成为现代家庭生活的重要组成部分。本文将详细介绍一个基于Python的智能家居控制系统的设计与实现过程,该系统能够实现对家庭设备的集中管理和智能控制,提升家居生活的便捷性和舒适度。
2. 系统架构设计
2.1 总体架构
智能家居控制系统采用分层架构设计,主要包括以下几个层次:
- 设备层:各种智能家居设备,如灯光、空调、窗帘、安防设备等
- 通信层:负责设备与控制中心之间的数据传输,支持WiFi、蓝牙、Zigbee等多种通信协议
- 控制层:系统的核心,处理来自用户的指令并转发给相应的设备
- 应用层:提供Web界面和移动应用,供用户进行交互操作
- 智能层:基于机器学习算法,实现系统的自动化决策和智能场景控制
2.2 核心模块设计
系统主要包含以下核心模块:
- 设备管理模块:负责设备的注册、状态监控和控制
- 用户管理模块:处理用户认证、权限管理等功能
- 场景控制模块:实现自定义场景的创建和执行
- 数据分析模块:收集和分析用户行为数据,为智能决策提供支持
- API接口模块:提供RESTful API,支持第三方应用集成
3. 技术选型
3.1 编程语言与框架
- 后端:Python 3.9
- Web框架:Flask 2.0.1
- WebSocket:Flask-SocketIO
- 数据库:SQLite(开发环境)/ PostgreSQL(生产环境)
- ORM:SQLAlchemy
- 前端:HTML5 + CSS3 + JavaScript
- 前端框架:Vue.js 3.0
- UI组件库:Element Plus
3.2 硬件通信技术
- WiFi通信:使用MQTT协议
- 蓝牙通信:使用PyBluez库
- Zigbee通信:通过串口与Zigbee协调器通信
3.3 智能算法
- 用户行为分析:基于Scikit-learn的聚类算法
- 场景推荐:基于协同过滤的推荐算法
- 异常检测:基于统计和机器学习的异常检测算法
4. 系统实现
4.1 设备管理模块实现
设备管理是系统的基础模块,负责管理所有智能设备。以下是设备基类的实现:
# device_manager.py
from abc import ABC, abstractmethod
import uuid
import time
class Device(ABC):
"""智能设备基类"""
def __init__(self, name, location, device_type):
self.device_id = str(uuid.uuid4())
self.name = name
self.location = location
self.type = device_type
self.status = "offline"
self.last_updated = time.time()
@abstractmethod
def turn_on(self):
pass
@abstractmethod
def turn_off(self):
pass
def get_status(self):
return {
"device_id": self.device_id,
"name": self.name,
"location": self.location,
"type": self.type,
"status": self.status,
"last_updated": self.last_updated
}
def update_status(self, status):
self.status = status
self.last_updated = time.time()
class LightDevice(Device):
"""灯光设备类"""
def __init__(self, name, location, brightness=100):
super().__init__(name, location, "light")
self.brightness = brightness
self.color = "white"
def turn_on(self):
self.update_status("on")
return True
def turn_off(self):
self.update_status("off")
return True
def set_brightness(self, brightness):
if 0 <= brightness <= 100:
self.brightness = brightness
return True
return False
def set_color(self, color):
self.color = color
return True
def get_status(self):
status = super().get_status()
status.update({
"brightness": self.brightness,
"color": self.color
})
return status
class DeviceManager:
"""设备管理器"""
def __init__(self):
self.devices = {}
def add_device(self, device):
self.devices[device.device_id] = device
return device.device_id
def remove_device(self, device_id):
if device_id in self.devices:
del self.devices[device_id]
return True
return False
def get_device(self, device_id):
return self.devices.get(device_id)
def get_all_devices(self):
return [device.get_status() for device in self.devices.values()]
def get_devices_by_type(self, device_type):
return [device.get_status() for device in self.devices.values()
if device.type == device_type]
def get_devices_by_location(self, location):
return [device.get_status() for device in self.devices.values()
if device.location == location]
4.2 场景控制模块实现
场景控制模块允许用户创建自定义场景,实现多设备的联动控制:
# scene_controller.py
import time
import json
class Scene:
"""场景类"""
def __init__(self, name, description=""):
self.scene_id = str(uuid.uuid4())
self.name = name
self.description = description
self.actions = []
self.created_at = time.time()
self.last_executed = None
def add_action(self, device_id, action, params=None):
if params is None:
params = {}
self.actions.append({
"device_id": device_id,
"action": action,
"params": params
})
def remove_action(self, index):
if 0 <= index < len(self.actions):
self.actions.pop(index)
return True
return False
def get_details(self):
return {
"scene_id": self.scene_id,
"name": self.name,
"description": self.description,
"actions": self.actions,
"created_at": self.created_at,
"last_executed": self.last_executed
}
class SceneController:
"""场景控制器"""
def __init__(self, device_manager):
self.scenes = {}
self.device_manager = device_manager
def create_scene(self, name, description=""):
scene = Scene(name, description)
self.scenes[scene.scene_id] = scene
return scene.scene_id
def delete_scene(self, scene_id):
if scene_id in self.scenes:
del self.scenes[scene_id]
return True
return False
def get_scene(self, scene_id):
return self.scenes.get(scene_id)
def get_all_scenes(self):
return [scene.get_details() for scene in self.scenes.values()]
def execute_scene(self, scene_id):
scene = self.scenes.get(scene_id)
if not scene:
return False
results = []
for action in scene.actions:
device = self.device_manager.get_device(action["device_id"])
if device:
if action["action"] == "turn_on":
result = device.turn_on()
elif action["action"] == "turn_off":
result = device.turn_off()
elif action["action"] == "set_brightness" and hasattr(device, "set_brightness"):
result = device.set_brightness(action["params"].get("brightness", 100))
elif action["action"] == "set_color" and hasattr(device, "set_color"):
result = device.set_color(action["params"].get("color", "white"))
else:
result = False
results.append({
"device_id": action["device_id"],
"action": action["action"],
"success": result
})
scene.last_executed = time.time()
return results
4.3 API接口实现
使用Flask框架实现RESTful API接口:
# app.py
from flask import Flask, request, jsonify
from flask_socketio import SocketIO
from device_manager import DeviceManager, LightDevice
from scene_controller import SceneController
import json
app = Flask(__name__)
socketio = SocketIO(app, cors_allowed_origins="*")
# 初始化设备管理器和场景控制器
device_manager = DeviceManager()
scene_controller = SceneController(device_manager)
# 设备管理API
@app.route('/api/devices', methods=['GET'])
def get_devices():
return jsonify(device_manager.get_all_devices())
@app.route('/api/devices', methods=['POST'])
def add_device():
data = request.json
if data.get('type') == 'light':
device = LightDevice(
name=data.get('name', 'New Light'),
location=data.get('location', 'Living Room'),
brightness=data.get('brightness', 100)
)
device_id = device_manager.add_device(device)
return jsonify({"device_id": device_id}), 201
return jsonify({"error": "Unsupported device type"}), 400
@app.route('/api/devices/<device_id>', methods=['GET'])
def get_device(device_id):
device = device_manager.get_device(device_id)
if device:
return jsonify(device.get_status())
return jsonify({"error": "Device not found"}), 404
@app.route('/api/devices/<device_id>/control', methods=['POST'])
def control_device(device_id):
device = device_manager.get_device(device_id)
if not device:
return jsonify({"error": "Device not found"}), 404
data = request.json
action = data.get('action')
if action == 'turn_on':
result = device.turn_on()
elif action == 'turn_off':
result = device.turn_off()
elif action == 'set_brightness' and hasattr(device, 'set_brightness'):
result = device.set_brightness(data.get('brightness', 100))
elif action == 'set_color' and hasattr(device, 'set_color'):
result = device.set_color(data.get('color', 'white'))
else:
return jsonify({"error": "Invalid action"}), 400
# 通过WebSocket广播设备状态变化
socketio.emit('device_update', device.get_status())
return jsonify({"success": result})
# 场景管理API
@app.route('/api/scenes', methods=['GET'])
def get_scenes():
return jsonify(scene_controller.get_all_scenes())
@app.route('/api/scenes', methods=['POST'])
def create_scene():
data = request.json
scene_id = scene_controller.create_scene(
name=data.get('name', 'New Scene'),
description=data.get('description', '')
)
# 添加场景动作
scene = scene_controller.get_scene(scene_id)
for action in data.get('actions', []):
scene.add_action(
device_id=action.get('device_id'),
action=action.get('action'),
params=action.get('params', {})
)
return jsonify({"scene_id": scene_id}), 201
@app.route('/api/scenes/<scene_id>/execute', methods=['POST'])
def execute_scene(scene_id):
results = scene_controller.execute_scene(scene_id)
if results is False:
return jsonify({"error": "Scene not found"}), 404
return jsonify({"results": results})
# 主程序入口
if __name__ == '__main__':
# 添加一些测试设备
living_room_light = LightDevice("客厅主灯", "客厅")
bedroom_light = LightDevice("卧室灯", "卧室")
device_manager.add_device(living_room_light)
device_manager.add_device(bedroom_light)
# 创建一个测试场景
night_mode_id = scene_controller.create_scene("夜间模式", "睡前自动设置的场景")
night_mode = scene_controller.get_scene(night_mode_id)
night_mode.add_action(living_room_light.device_id, "turn_off")
night_mode.add_action(bedroom_light.device_id, "set_brightness", {"brightness": 30})
# 启动服务器
socketio.run(app, host='0.0.0.0', port=5000, debug=True)
4.4 前端界面实现
使用Vue.js和Element Plus构建用户界面:
<!-- static/index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>智能家居控制系统</title>
<link rel="stylesheet" href="https://unpkg.com/element-plus/dist/index.css">
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<div id="app">
<el-container>
<el-header>
<h1>智能家居控制系统</h1>
</el-header>
<el-container>
<el-aside width="250px">
<el-menu default-active="devices">
<el-menu-item index="devices" @click="activeTab = 'devices'">
<i class="el-icon-cpu"></i>
<span>设备管理</span>
</el-menu-item>
<el-menu-item index="scenes" @click="activeTab = 'scenes'">
<i class="el-icon-magic-stick"></i>
<span>场景控制</span>
</el-menu-item>
<el-menu-item index="statistics" @click="activeTab = 'statistics'">
<i class="el-icon-data-line"></i>
<span>数据统计</span>
</el-menu-item>
<el-menu-item index="settings" @click="activeTab = 'settings'">
<i class="el-icon-setting"></i>
<span>系统设置</span>
</el-menu-item>
</el-menu>
</el-aside>
<el-main>
<!-- 设备管理页面 -->
<div v-if="activeTab === 'devices'">
<el-row :gutter="20">
<el-col :span="24">
<el-card>
<template #header>
<div class="card-header">
<span>设备列表</span>
<el-button type="primary" size="small" @click="showAddDeviceDialog">添加设备</el-button>
</div>
</template>
<el-table :data="devices" style="width: 100%">
<el-table-column prop="name" label="设备名称"></el-table-column>
<el-table-column prop="location" label="位置"></el-table-column>
<el-table-column prop="type" label="类型"></el-table-column>
<el-table-column prop="status" label="状态">
<template #default="scope">
<el-tag :type="scope.row.status === 'on' ? 'success' : 'info'">
{{ scope.row.status === 'on' ? '开启' : '关闭' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作">
<template #default="scope">
<el-button
size="small"
:type="scope.row.status === 'on' ? 'danger' : 'success'"
@click="controlDevice(scope.row.device_id, scope.row.status === 'on' ? 'turn_off' : 'turn_on')"
>
{{ scope.row.status === 'on' ? '关闭' : '开启' }}
</el-button>
<el-button
size="small"
type="primary"
@click="showDeviceDetailDialog(scope.row)"
>
详情
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</el-col>
</el-row>
</div>
<!-- 场景控制页面 -->
<div v-if="activeTab === 'scenes'">
<el-row :gutter="20">
<el-col :span="24">
<el-card>
<template #header>
<div class="card-header">
<span>场景列表</span>
<el-button type="primary" size="small" @click="showAddSceneDialog">创建场景</el-button>
</div>
</template>
<el-table :data="scenes" style="width: 100%">
<el-table-column prop="name" label="场景名称"></el-table-column>
<el-table-column prop="description" label="描述"></el-table-column>
<el-table-column label="操作">
<template #default="scope">
<el-button
size="small"
type="success"
@click="executeScene(scope.row.scene_id)"
>
执行
</el-button>
<el-button
size="small"
type="primary"
@click="showSceneDetailDialog(scope.row)"
>
详情
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</el-col>
</el-row>
</div>
</el-main>
</el-container>
</el-container>
<!-- 添加设备对话框 -->
<el-dialog title="添加设备" v-model="addDeviceDialogVisible">
<el-form :model="newDevice" label-width="100px">
<el-form-item label="设备名称">
<el-input v-model="newDevice.name"></el-input>
</el-form-item>
<el-form-item label="位置">
<el-input v-model="newDevice.location"></el-input>
</el-form-item>
<el-form-item label="设备类型">
<el-select v-model="newDevice.type" placeholder="请选择设备类型">
<el-option label="灯光" value="light"></el-option>
<el-option label="空调" value="ac" disabled></el-option>
<el-option label="窗帘" value="curtain" disabled></el-option>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="addDeviceDialogVisible = false">取消</el-button>
<el-button type="primary" @click="addDevice">确定</el-button>
</span>
</template>
</el-dialog>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script src="https://unpkg.com/element-plus"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="https://cdn.socket.io/4.4.1/socket.io.min.js"></script>
<script src="/static/js/app.js"></script>
</body>
</html>
// static/js/app.js
const { createApp, ref, onMounted } = Vue;
const app = createApp({
setup() {
const activeTab = ref('devices');
const devices = ref([]);
const scenes = ref([]);
const addDeviceDialogVisible = ref(false);
const newDevice = ref({
name: '',
location: '',
type: 'light'
});
// 初始化WebSocket连接
const socket = io();
// 监听设备状态更新
socket.on('device_update', (data) => {
const index = devices.value.findIndex(d => d.device_id === data.device_id);
if (index !== -1) {
devices.value[index] = data;
}
});
// 获取所有设备
const fetchDevices = async () => {
try {
const response = await axios.get('/api/devices');
devices.value = response.data;
} catch (error) {
console.error('获取设备列表失败:', error);
ElMessage.error('获取设备列表失败');
}
};
// 获取所有场景
const fetchScenes = async () => {
try {
const response = await axios.get('/api/scenes');
scenes.value = response.data;
} catch (error) {
console.error('获取场景列表失败:', error);
ElMessage.error('获取场景列表失败');
}
};
// 控制设备
const controlDevice = async (deviceId, action, params = {}) => {
try {
await axios.post(`/api/devices/${deviceId}/control`, {
action,
...params
});
ElMessage.success(`设备操作成功: ${action}`);
} catch (error) {
console.error('设备操作失败:', error);
ElMessage.error('设备操作失败');
}
};
// 执行场景
const executeScene = async (sceneId) => {
try {
const response = await axios.post(`/api/scenes/${sceneId}/execute`);
ElMessage.success('场景执行成功');
} catch (error) {
console.error('场景执行失败:', error);
ElMessage.error('场景执行失败');
}
};
// 添加设备
const addDevice = async () => {
try {
await axios.post('/api/devices', newDevice.value);
ElMessage.success('设备添加成功');
addDeviceDialogVisible.value = false;
fetchDevices();
// 重置表单
newDevice.value = {
name: '',
location: '',
type: 'light'
};
} catch (error) {
console.error('添加设备失败:', error);
ElMessage.error('添加设备失败');
}
};
// 显示添加设备对话框
const showAddDeviceDialog = () => {
addDeviceDialogVisible.value = true;
};
// 显示设备详情对话框
const showDeviceDetailDialog = (device) => {
// 实现详情对话框逻辑
console.log('设备详情:', device);
};
// 显示添加场景对话框
const showAddSceneDialog = () => {
// 实现添加场景对话框逻辑
console.log('添加场景');
};
// 显示场景详情对话框
const showSceneDetailDialog = (scene) => {
// 实现场景详情对话框逻辑
console.log('场景详情:', scene);
};
// 页面加载时获取数据
onMounted(() => {
fetchDevices();
fetchScenes();
});
return {
activeTab,
devices,
scenes,
addDeviceDialogVisible,
newDevice,
controlDevice,
executeScene,
addDevice,
showAddDeviceDialog,
showDeviceDetailDialog,
showAddSceneDialog,
showSceneDetailDialog
};
}
});
app.use(ElementPlus);
app.mount('#app');
5. 系统测试
5.1 单元测试
使用Python的unittest框架对各个模块进行单元测试:
# tests/test_device_manager.py
import unittest
from device_manager import DeviceManager, LightDevice
class TestDeviceManager(unittest.TestCase):
def setUp(self):
self.device_manager = DeviceManager()
self.light = LightDevice("测试灯", "测试房间")
def test_add_device(self):
device_id = self.device_manager.add_device(self.light)
self.assertIsNotNone(device_id)
self.assertEqual(len(self.device_manager.devices), 1)
def test_get_device(self):
device_id = self.device_manager.add_device(self.light)
device = self.device_manager.get_device(device_id)
self.assertEqual(device, self.light)
def test_remove_device(self):
device_id = self.device_manager.add_device(self.light)
result = self.device_manager.remove_device(device_id)
self.assertTrue(result)
self.assertEqual(len(self.device_manager.devices), 0)
def test_get_all_devices(self):
self.device_manager.add_device(self.light)
devices = self.device_manager.get_all_devices()
self.assertEqual(len(devices), 1)
self.assertEqual(devices[0]["name"], "测试灯")
def test_light_device_functions(self):
self.light.turn_on()
self.assertEqual(self.light.status, "on")
self.light.set_brightness(50)
self.assertEqual(self.light.brightness, 50)
self.light.set_color("blue")
self.assertEqual(self.light.color, "blue")
self.light.turn_off()
self.assertEqual(self.light.status, "off")
if __name__ == '__main__':
unittest.main()
6. 系统扩展与优化
6.1 安全性增强
-
用户认证与授权:实现JWT认证机制,确保API接口的安全访问
-
HTTPS支持:配置SSL证书,启用HTTPS加密通信
-
设备通信加密:对设备与控制中心之间的通信进行加密
-
日志审计:记录所有关键操作,便于安全审计
6.2 功能扩展
-
语音控制:集成语音识别模块,支持语音指令控制
-
移动应用:开发配套的移动应用,实现随时随地控制
-
智能算法优化:引入更先进的机器学习算法,提升系统智能化水平
-
多协议支持:扩展对更多设备通信协议的支持,如Z-Wave、KNX等
6.3 性能优化
-
异步处理:使用异步框架处理设备通信,提高系统响应速度
-
缓存机制:引入Redis缓存,减轻数据库负担
-
分布式部署:实现系统的分布式部署,提高可扩展性和可用性
-
微服务架构:将系统拆分为多个微服务,便于独立扩展和维护
7. 总结与展望
本文详细介绍了一个基于Python的智能家居控制系统的设计与实现过程。该系统采用分层架构设计,实现了设备管理、场景控制、用户界面等核心功能,并通过多种通信协议支持各类智能设备的接入和控制。
系统的主要特点包括:
-
模块化设计:系统各模块职责明确,便于扩展和维护
-
多协议支持:支持WiFi、蓝牙、Zigbee等多种通信协议
-
智能化控制:基于机器学习算法实现智能场景推荐和自动化控制
-
友好的用户界面:提供Web界面和移动应用,操作简单直观
未来,系统可以在以下方面进行进一步的发展:
-
边缘计算:将部分计算任务下放到边缘设备,减轻中心服务器负担
-
AI增强:引入深度学习和强化学习算法,提升系统智能化水平
-
生态系统集成:与主流智能家居生态系统(如Apple HomeKit、Google Home、Amazon Alexa等)进行集成
-
能源管理:增加能源消耗监控和优化功能,实现绿色节能
智能家居作为物联网的重要应用场景,将随着技术的发展不断演进。本系统为智能家居的实现提供了一种可行的解决方案,希望能为相关领域的研究和实践提供参考。
源代码
源代码(已更新)下载:
https://download.youkuaiyun.com/download/exlink2012/90976680
Directory Content Summary
Source Directory: ./smart_home_system
Directory Structure
smart_home_system/
README.md
requirements.txt
run.py
app/
database.py
device_manager.py
scene_manager.py
__init__.py
routes/
api.py
__init__.py
config/
static/
css/
styles.css
js/
app.js
templates/
index.html
File Contents
README.md
# 智能家居控制系统
这是一个基于Python的智能家居控制系统,实现了对家庭设备的集中管理和智能控制。
## 功能特点
- 设备管理:添加、编辑、删除和控制各种智能设备
- 位置管理:管理设备所在的位置
- 数据统计:查看设备状态和分布统计
- 设备历史:记录设备状态变化历史
- 响应式界面:适配不同尺寸的屏幕
## 支持的设备类型
- 灯光设备:控制开关、亮度和颜色
- 温控设备:控制温度和工作模式
## 技术栈
### 后端
- Python 3.9+
- Flask Web框架
- SQLite数据库
### 前端
- Vue.js 3
- Bootstrap 5
- Axios HTTP客户端
## 安装与运行
1. 克隆或下载项目代码
2. 安装依赖
```bash
pip install -r requirements.txt
运行应用
python run.py
在浏览器中访问
http://localhost:5000
项目结构
smart_home_system/
├── app/ # 应用代码
│ ├── __init__.py # 应用初始化
│ ├── database.py # 数据库模块
│ ├── device_manager.py # 设备管理模块
│ └── routes/ # API路由
│ ├── __init__.py
│ └── api.py # API接口定义
├── config/ # 配置文件
├── static/ # 静态资源
│ ├── css/ # CSS样式
│ │ └── styles.css # 主样式表
│ └── js/ # JavaScript代码
│ └── app.js # 前端应用
├── templates/ # HTML模板
│ └── index.html # 主页面
├── README.md # 项目说明
├── requirements.txt # 依赖列表
└── run.py # 应用入口
API接口
设备管理
GET /api/devices - 获取所有设备
GET /api/devices/<device_id> - 获取单个设备
POST /api/devices - 添加设备
PUT /api/devices/<device_id> - 更新设备
DELETE /api/devices/<device_id> - 删除设备
POST /api/devices/<device_id>/control - 控制设备
GET /api/devices/<device_id>/history - 获取设备历史
位置管理
GET /api/locations - 获取所有位置
POST /api/locations - 添加位置
设备类型
GET /api/device-types - 获取所有设备类型
扩展与自定义
添加新设备类型
在 device_manager.py 中创建新的设备类型类,继承 DeviceType 基类
实现必要的方法:create_device, get_properties_schema, validate_command, execute_command
在 DeviceFactory 中注册新设备类型
自定义数据库
默认使用SQLite数据库,如需使用其他数据库:
修改 database.py 中的数据库连接和操作代码
更新 requirements.txt 添加相应的数据库驱动
requirements.txt
Flask==2.2.3
Flask-Cors==3.0.10
Werkzeug==2.2.3
Jinja2==3.1.2
MarkupSafe==2.1.2
itsdangerous==2.1.2
click==8.1.3
colorama==0.4.6
5. README File
run.py
"""
Main entry point for Smart Home System
"""
from app import create_app
app = create_app()
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)
app\database.py
"""
Database module for Smart Home System
Handles database operations for device management
"""
import sqlite3
import json
import os
import datetime
from pathlib import Path
class Database:
def __init__(self, db_path=None):
"""Initialize database connection"""
if db_path is None:
# Create database in the project directory
base_dir = Path(__file__).resolve().parent.parent
db_path = os.path.join(base_dir, 'smart_home.db')
self.db_path = db_path
self.connection = None
self.cursor = None
self._connect()
self._create_tables()
def _connect(self):
"""Connect to the SQLite database"""
try:
self.connection = sqlite3.connect(self.db_path)
self.connection.row_factory = sqlite3.Row # Return rows as dictionaries
self.cursor = self.connection.cursor()
except sqlite3.Error as e:
print(f"Database connection error: {e}")
raise
def _create_tables(self):
"""Create necessary tables if they don't exist"""
try:
# Devices table
self.cursor.execute('''
CREATE TABLE IF NOT EXISTS devices (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
type TEXT NOT NULL,
location TEXT,
status TEXT DEFAULT 'offline',
properties TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# Device history table for logging state changes
self.cursor.execute('''
CREATE TABLE IF NOT EXISTS device_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
device_id TEXT NOT NULL,
status TEXT NOT NULL,
properties TEXT,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (device_id) REFERENCES devices (id)
)
''')
# Locations table
self.cursor.execute('''
CREATE TABLE IF NOT EXISTS locations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
description TEXT
)
''')
# Scenes table
self.cursor.execute('''
CREATE TABLE IF NOT EXISTS scenes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
description TEXT,
icon TEXT DEFAULT 'lightbulb',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# Scene actions table - stores device states for each scene
self.cursor.execute('''
CREATE TABLE IF NOT EXISTS scene_actions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
scene_id INTEGER NOT NULL,
device_id TEXT NOT NULL,
action_type TEXT NOT NULL,
action_params TEXT,
FOREIGN KEY (scene_id) REFERENCES scenes (id) ON DELETE CASCADE,
FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE
)
''')
# Insert default locations if they don't exist
default_locations = ['Living Room', 'Bedroom', 'Kitchen', 'Bathroom', 'Office', 'Hallway']
for location in default_locations:
self.cursor.execute('''
INSERT OR IGNORE INTO locations (name) VALUES (?)
''', (location,))
self.connection.commit()
except sqlite3.Error as e:
print(f"Error creating tables: {e}")
raise
def close(self):
"""Close the database connection"""
if self.connection:
self.connection.close()
# Device operations
def add_device(self, device_data):
"""Add a new device to the database"""
try:
# Convert properties dict to JSON string
if 'properties' in device_data and isinstance(device_data['properties'], dict):
device_data['properties'] = json.dumps(device_data['properties'])
# Get current timestamp
current_time = datetime.datetime.now().isoformat()
self.cursor.execute('''
INSERT INTO devices (id, name, type, location, status, properties, created_at, last_updated)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
''', (
device_data['id'],
device_data['name'],
device_data['type'],
device_data.get('location', 'Unknown'),
device_data.get('status', 'offline'),
device_data.get('properties', '{}'),
current_time,
current_time
))
self.connection.commit()
return True
except sqlite3.Error as e:
print(f"Error adding device: {e}")
self.connection.rollback()
return False
def update_device(self, device_id, update_data):
"""Update device information"""
try:
# Get current device data
current_device = self.get_device(device_id)
if not current_device:
return False
# Prepare update fields
update_fields = []
update_values = []
for key, value in update_data.items():
if key == 'properties' and isinstance(value, dict):
# Merge with existing properties
current_props = json.loads(current_device['properties']) if current_device['properties'] else {}
current_props.update(value)
value = json.dumps(current_props)
if key in ['name', 'type', 'location', 'status', 'properties']:
update_fields.append(f"{key} = ?")
update_values.append(value)
# Add last_updated timestamp
update_fields.append("last_updated = ?")
update_values.append(datetime.datetime.now().isoformat())
# Add device_id for WHERE clause
update_values.append(device_id)
# Execute update query
if update_fields:
query = f'''
UPDATE devices SET {', '.join(update_fields)}
WHERE id = ?
'''
self.cursor.execute(query, update_values)
# Log state change if status or properties changed
if 'status' in update_data or 'properties' in update_data:
self._log_device_history(device_id,
update_data.get('status', current_device['status']),
update_data.get('properties', current_device['properties']))
self.connection.commit()
return True
return False
except sqlite3.Error as e:
print(f"Error updating device: {e}")
self.connection.rollback()
return False
def delete_device(self, device_id):
"""Delete a device from the database"""
try:
self.cursor.execute('DELETE FROM devices WHERE id = ?', (device_id,))
self.cursor.execute('DELETE FROM device_history WHERE device_id = ?', (device_id,))
self.cursor.execute('DELETE FROM scene_actions WHERE device_id = ?', (device_id,))
self.connection.commit()
return True
except sqlite3.Error as e:
print(f"Error deleting device: {e}")
self.connection.rollback()
return False
def get_device(self, device_id):
"""Get a device by ID"""
try:
self.cursor.execute('SELECT * FROM devices WHERE id = ?', (device_id,))
device = self.cursor.fetchone()
if device:
device_dict = dict(device)
# Parse properties JSON
if device_dict['properties']:
device_dict['properties'] = json.loads(device_dict['properties'])
return device_dict
return None
except sqlite3.Error as e:
print(f"Error getting device: {e}")
return None
def get_all_devices(self):
"""Get all devices"""
try:
self.cursor.execute('SELECT * FROM devices ORDER BY name')
devices = self.cursor.fetchall()
result = []
for device in devices:
device_dict = dict(device)
# Parse properties JSON
if device_dict['properties']:
device_dict['properties'] = json.loads(device_dict['properties'])
result.append(device_dict)
return result
except sqlite3.Error as e:
print(f"Error getting devices: {e}")
return []
def get_devices_by_type(self, device_type):
"""Get devices by type"""
try:
self.cursor.execute('SELECT * FROM devices WHERE type = ? ORDER BY name', (device_type,))
devices = self.cursor.fetchall()
result = []
for device in devices:
device_dict = dict(device)
# Parse properties JSON
if device_dict['properties']:
device_dict['properties'] = json.loads(device_dict['properties'])
result.append(device_dict)
return result
except sqlite3.Error as e:
print(f"Error getting devices by type: {e}")
return []
def get_devices_by_location(self, location):
"""Get devices by location"""
try:
self.cursor.execute('SELECT * FROM devices WHERE location = ? ORDER BY name', (location,))
devices = self.cursor.fetchall()
result = []
for device in devices:
device_dict = dict(device)
# Parse properties JSON
if device_dict['properties']:
device_dict['properties'] = json.loads(device_dict['properties'])
result.append(device_dict)
return result
except sqlite3.Error as e:
print(f"Error getting devices by location: {e}")
return []
def _log_device_history(self, device_id, status, properties):
"""Log device state change to history"""
try:
if isinstance(properties, dict):
properties = json.dumps(properties)
self.cursor.execute('''
INSERT INTO device_history (device_id, status, properties)
VALUES (?, ?, ?)
''', (device_id, status, properties))
return True
except sqlite3.Error as e:
print(f"Error logging device history: {e}")
return False
def get_device_history(self, device_id, limit=50):
"""Get device history"""
try:
self.cursor.execute('''
SELECT * FROM device_history
WHERE device_id = ?
ORDER BY timestamp DESC
LIMIT ?
''', (device_id, limit))
history = self.cursor.fetchall()
result = []
for entry in history:
entry_dict = dict(entry)
# Parse properties JSON
if entry_dict['properties']:
entry_dict['properties'] = json.loads(entry_dict['properties'])
result.append(entry_dict)
return result
except sqlite3.Error as e:
print(f"Error getting device history: {e}")
return []
# Location operations
def get_all_locations(self):
"""Get all locations"""
try:
self.cursor.execute('SELECT * FROM locations ORDER BY name')
locations = self.cursor.fetchall()
return [dict(location) for location in locations]
except sqlite3.Error as e:
print(f"Error getting locations: {e}")
return []
def add_location(self, name, description=None):
"""Add a new location"""
try:
self.cursor.execute('''
INSERT INTO locations (name, description)
VALUES (?, ?)
''', (name, description))
self.connection.commit()
return self.cursor.lastrowid
except sqlite3.Error as e:
print(f"Error adding location: {e}")
self.connection.rollback()
return None
# Scene operations
def add_scene(self, scene_data):
"""Add a new scene"""
try:
name = scene_data.get('name')
description = scene_data.get('description')
icon = scene_data.get('icon', 'lightbulb')
current_time = datetime.datetime.now().isoformat()
self.cursor.execute('''
INSERT INTO scenes (name, description, icon, created_at, last_updated)
VALUES (?, ?, ?, ?, ?)
''', (name, description, icon, current_time, current_time))
scene_id = self.cursor.lastrowid
# Add scene actions if provided
actions = scene_data.get('actions', [])
for action in actions:
self.add_scene_action(scene_id, action)
self.connection.commit()
return scene_id
except sqlite3.Error as e:
print(f"Error adding scene: {e}")
self.connection.rollback()
return None
def update_scene(self, scene_id, update_data):
"""Update scene information"""
try:
update_fields = []
update_values = []
for key, value in update_data.items():
if key in ['name', 'description', 'icon']:
update_fields.append(f"{key} = ?")
update_values.append(value)
# Add last_updated timestamp
update_fields.append("last_updated = ?")
update_values.append(datetime.datetime.now().isoformat())
# Add scene_id for WHERE clause
update_values.append(scene_id)
# Execute update query
if update_fields:
query = f'''
UPDATE scenes SET {', '.join(update_fields)}
WHERE id = ?
'''
self.cursor.execute(query, update_values)
# Update scene actions if provided
if 'actions' in update_data:
# Delete existing actions
self.cursor.execute('DELETE FROM scene_actions WHERE scene_id = ?', (scene_id,))
# Add new actions
for action in update_data['actions']:
self.add_scene_action(scene_id, action)
self.connection.commit()
return True
return False
except sqlite3.Error as e:
print(f"Error updating scene: {e}")
self.connection.rollback()
return False
def delete_scene(self, scene_id):
"""Delete a scene"""
try:
self.cursor.execute('DELETE FROM scenes WHERE id = ?', (scene_id,))
self.cursor.execute('DELETE FROM scene_actions WHERE scene_id = ?', (scene_id,))
self.connection.commit()
return True
except sqlite3.Error as e:
print(f"Error deleting scene: {e}")
self.connection.rollback()
return False
def get_scene(self, scene_id):
"""Get a scene by ID"""
try:
self.cursor.execute('SELECT * FROM scenes WHERE id = ?', (scene_id,))
scene = self.cursor.fetchone()
if not scene:
return None
scene_dict = dict(scene)
# Get scene actions
scene_dict['actions'] = self.get_scene_actions(scene_id)
return scene_dict
except sqlite3.Error as e:
print(f"Error getting scene: {e}")
return None
def get_all_scenes(self):
"""Get all scenes"""
try:
self.cursor.execute('SELECT * FROM scenes ORDER BY name')
scenes = self.cursor.fetchall()
result = []
for scene in scenes:
scene_dict = dict(scene)
scene_dict['actions'] = self.get_scene_actions(scene_dict['id'])
result.append(scene_dict)
return result
except sqlite3.Error as e:
print(f"Error getting scenes: {e}")
return []
def add_scene_action(self, scene_id, action_data):
"""Add a scene action"""
try:
device_id = action_data.get('device_id')
action_type = action_data.get('action_type')
action_params = action_data.get('action_params', {})
if isinstance(action_params, dict):
action_params = json.dumps(action_params)
self.cursor.execute('''
INSERT INTO scene_actions (scene_id, device_id, action_type, action_params)
VALUES (?, ?, ?, ?)
''', (scene_id, device_id, action_type, action_params))
return self.cursor.lastrowid
except sqlite3.Error as e:
print(f"Error adding scene action: {e}")
return None
def get_scene_actions(self, scene_id):
"""Get actions for a scene"""
try:
self.cursor.execute('''
SELECT * FROM scene_actions WHERE scene_id = ?
''', (scene_id,))
actions = self.cursor.fetchall()
result = []
for action in actions:
action_dict = dict(action)
# Parse action_params JSON
if action_dict['action_params']:
action_dict['action_params'] = json.loads(action_dict['action_params'])
result.append(action_dict)
return result
except sqlite3.Error as e:
print(f"Error getting scene actions: {e}")
return []
app\device_manager.py
"""
Device Manager Module for Smart Home System
Handles device operations and communication
"""
import uuid
import time
import json
from .database import Database
class DeviceManager:
"""
Device Manager class for handling device operations
"""
def __init__(self, db=None):
"""Initialize the device manager"""
self.db = db if db else Database()
self.devices = {} # In-memory cache of devices
self._load_devices_from_db()
def _load_devices_from_db(self):
"""Load devices from database into memory"""
devices = self.db.get_all_devices()
for device in devices:
self.devices[device['id']] = device
def register_device(self, name, device_type, location=None, properties=None):
"""Register a new device"""
if properties is None:
properties = {}
device_id = str(uuid.uuid4())
device_data = {
'id': device_id,
'name': name,
'type': device_type,
'location': location,
'status': 'offline',
'properties': properties
}
# Add to database
if self.db.add_device(device_data):
# Add to in-memory cache
self.devices[device_id] = device_data
return device_id
return None
def update_device_status(self, device_id, status, properties=None):
"""Update device status and properties"""
if device_id not in self.devices:
return False
update_data = {'status': status}
if properties:
update_data['properties'] = properties
# Update database
if self.db.update_device(device_id, update_data):
# Update in-memory cache
self.devices[device_id].update(update_data)
self.devices[device_id]['last_updated'] = time.time()
return True
return False
def get_device(self, device_id):
"""Get device by ID"""
if device_id in self.devices:
return self.devices[device_id]
# Try to get from database
device = self.db.get_device(device_id)
if device:
self.devices[device_id] = device
return device
return None
def get_all_devices(self):
"""Get all devices"""
return list(self.devices.values())
def get_devices_by_type(self, device_type):
"""Get devices by type"""
return [device for device in self.devices.values() if device['type'] == device_type]
def get_devices_by_location(self, location):
"""Get devices by location"""
return [device for device in self.devices.values() if device['location'] == location]
def delete_device(self, device_id):
"""Delete a device"""
if device_id not in self.devices:
return False
# Delete from database
if self.db.delete_device(device_id):
# Delete from in-memory cache
del self.devices[device_id]
return True
return False
def update_device_properties(self, device_id, properties):
"""Update device properties"""
if device_id not in self.devices:
return False
# Get current properties
current_props = self.devices[device_id].get('properties', {})
# Merge with new properties
if isinstance(properties, dict):
current_props.update(properties)
# Update database
update_data = {'properties': current_props}
if self.db.update_device(device_id, update_data):
# Update in-memory cache
self.devices[device_id]['properties'] = current_props
self.devices[device_id]['last_updated'] = time.time()
return True
return False
def get_device_history(self, device_id, limit=50):
"""Get device history"""
return self.db.get_device_history(device_id, limit)
def get_all_locations(self):
"""Get all locations"""
return self.db.get_all_locations()
def add_location(self, name, description=None):
"""Add a new location"""
return self.db.add_location(name, description)
# Device type implementations
class DeviceType:
"""Base class for device types"""
@staticmethod
def create_device(device_manager, name, location, **kwargs):
"""Create a device of this type"""
raise NotImplementedError("Subclasses must implement this method")
@staticmethod
def get_properties_schema():
"""Get JSON schema for device properties"""
raise NotImplementedError("Subclasses must implement this method")
@staticmethod
def validate_command(command, params):
"""Validate a command for this device type"""
raise NotImplementedError("Subclasses must implement this method")
@staticmethod
def execute_command(device, command, params):
"""Execute a command on a device"""
raise NotImplementedError("Subclasses must implement this method")
class LightDevice(DeviceType):
"""Light device type implementation"""
@staticmethod
def create_device(device_manager, name, location, **kwargs):
"""Create a light device"""
brightness = kwargs.get('brightness', 100)
color = kwargs.get('color', 'white')
properties = {
'brightness': brightness,
'color': color,
'supported_commands': ['turn_on', 'turn_off', 'set_brightness', 'set_color']
}
return device_manager.register_device(name, 'light', location, properties)
@staticmethod
def get_properties_schema():
"""Get JSON schema for light properties"""
return {
"type": "object",
"properties": {
"brightness": {
"type": "integer",
"minimum": 0,
"maximum": 100,
"description": "Brightness level (0-100)"
},
"color": {
"type": "string",
"description": "Light color (name or hex code)"
}
}
}
@staticmethod
def validate_command(command, params):
"""Validate a command for light device"""
if command == 'turn_on' or command == 'turn_off':
return True
if command == 'set_brightness':
if 'brightness' not in params:
return False
brightness = params.get('brightness')
return isinstance(brightness, int) and 0 <= brightness <= 100
if command == 'set_color':
return 'color' in params
return False
@staticmethod
def execute_command(device, command, params):
"""Execute a command on a light device"""
device_id = device['id']
device_manager = DeviceManager()
if command == 'turn_on':
return device_manager.update_device_status(device_id, 'on')
if command == 'turn_off':
return device_manager.update_device_status(device_id, 'off')
if command == 'set_brightness':
brightness = params.get('brightness', 100)
properties = {'brightness': brightness}
return device_manager.update_device_properties(device_id, properties)
if command == 'set_color':
color = params.get('color', 'white')
properties = {'color': color}
return device_manager.update_device_properties(device_id, properties)
return False
class ThermostatDevice(DeviceType):
"""Thermostat device type implementation"""
@staticmethod
def create_device(device_manager, name, location, **kwargs):
"""Create a thermostat device"""
current_temp = kwargs.get('current_temp', 22)
target_temp = kwargs.get('target_temp', 22)
mode = kwargs.get('mode', 'off') # off, heat, cool, auto
properties = {
'current_temp': current_temp,
'target_temp': target_temp,
'mode': mode,
'supported_commands': ['set_temperature', 'set_mode']
}
return device_manager.register_device(name, 'thermostat', location, properties)
@staticmethod
def get_properties_schema():
"""Get JSON schema for thermostat properties"""
return {
"type": "object",
"properties": {
"current_temp": {
"type": "number",
"description": "Current temperature in Celsius"
},
"target_temp": {
"type": "number",
"minimum": 5,
"maximum": 35,
"description": "Target temperature in Celsius"
},
"mode": {
"type": "string",
"enum": ["off", "heat", "cool", "auto"],
"description": "Thermostat mode"
}
}
}
@staticmethod
def validate_command(command, params):
"""Validate a command for thermostat device"""
if command == 'set_temperature':
if 'temperature' not in params:
return False
temp = params.get('temperature')
return isinstance(temp, (int, float)) and 5 <= temp <= 35
if command == 'set_mode':
if 'mode' not in params:
return False
mode = params.get('mode')
return mode in ['off', 'heat', 'cool', 'auto']
return False
@staticmethod
def execute_command(device, command, params):
"""Execute a command on a thermostat device"""
device_id = device['id']
device_manager = DeviceManager()
if command == 'set_temperature':
temperature = params.get('temperature')
properties = {'target_temp': temperature}
# If device is off, turn it on in heat or cool mode
if device['properties'].get('mode') == 'off':
properties['mode'] = 'auto'
return device_manager.update_device_properties(device_id, properties)
if command == 'set_mode':
mode = params.get('mode')
properties = {'mode': mode}
# Update status based on mode
status = 'on' if mode != 'off' else 'off'
device_manager.update_device_status(device_id, status)
return device_manager.update_device_properties(device_id, properties)
return False
# Factory for creating devices of different types
class DeviceFactory:
"""Factory class for creating devices of different types"""
_device_types = {
'light': LightDevice,
'thermostat': ThermostatDevice
}
@classmethod
def register_device_type(cls, type_name, device_class):
"""Register a new device type"""
cls._device_types[type_name] = device_class
@classmethod
def create_device(cls, device_manager, type_name, name, location, **kwargs):
"""Create a device of the specified type"""
if type_name not in cls._device_types:
raise ValueError(f"Unknown device type: {type_name}")
device_class = cls._device_types[type_name]
return device_class.create_device(device_manager, name, location, **kwargs)
@classmethod
def get_device_types(cls):
"""Get all registered device types"""
return list(cls._device_types.keys())
@classmethod
def get_properties_schema(cls, type_name):
"""Get properties schema for a device type"""
if type_name not in cls._device_types:
raise ValueError(f"Unknown device type: {type_name}")
device_class = cls._device_types[type_name]
return device_class.get_properties_schema()
@classmethod
def execute_command(cls, device, command, params):
"""Execute a command on a device"""
device_type = device['type']
if device_type not in cls._device_types:
raise ValueError(f"Unknown device type: {device_type}")
device_class = cls._device_types[device_type]
# Validate command
if not device_class.validate_command(command, params):
return False
# Execute command
return device_class.execute_command(device, command, params)
app\scene_manager.py
"""
Scene Manager for Smart Home System
Handles scene operations and execution
"""
import uuid
from .device_manager import DeviceManager
class SceneManager:
def __init__(self, db=None, device_manager=None):
"""Initialize Scene Manager"""
from .database import Database
self.db = db if db else Database()
self.device_manager = device_manager if device_manager else DeviceManager(self.db)
def create_scene(self, name, description=None, icon=None, actions=None):
"""Create a new scene"""
if not name:
raise ValueError("Scene name is required")
scene_data = {
'name': name,
'description': description,
'icon': icon or 'lightbulb',
'actions': actions or []
}
scene_id = self.db.add_scene(scene_data)
if scene_id:
return self.get_scene(scene_id)
return None
def update_scene(self, scene_id, update_data):
"""Update an existing scene"""
if not scene_id:
return False
# Validate scene exists
scene = self.get_scene(scene_id)
if not scene:
return False
# Update scene
if self.db.update_scene(scene_id, update_data):
return self.get_scene(scene_id)
return None
def delete_scene(self, scene_id):
"""Delete a scene"""
return self.db.delete_scene(scene_id)
def get_scene(self, scene_id):
"""Get a scene by ID"""
return self.db.get_scene(scene_id)
def get_all_scenes(self):
"""Get all scenes"""
return self.db.get_all_scenes()
def activate_scene(self, scene_id):
"""Activate a scene by executing all its actions"""
scene = self.get_scene(scene_id)
if not scene:
return False
results = []
for action in scene['actions']:
device_id = action['device_id']
action_type = action['action_type']
action_params = action['action_params']
# Get device
device = self.device_manager.get_device(device_id)
if not device:
results.append({
'device_id': device_id,
'success': False,
'message': 'Device not found'
})
continue
# Execute action
try:
if action_type == 'set_status':
status = action_params.get('status')
if status:
self.device_manager.update_device_status(device_id, status)
results.append({
'device_id': device_id,
'success': True,
'action': 'set_status',
'value': status
})
elif action_type == 'set_property':
property_name = action_params.get('property')
property_value = action_params.get('value')
if property_name and property_value is not None:
properties = {property_name: property_value}
self.device_manager.update_device_properties(device_id, properties)
results.append({
'device_id': device_id,
'success': True,
'action': 'set_property',
'property': property_name,
'value': property_value
})
elif action_type == 'execute_command':
command = action_params.get('command')
params = action_params.get('params', {})
if command:
# This would call the device-specific command execution
# For now, we'll just update the device properties
self.device_manager.update_device_properties(device_id, params)
results.append({
'device_id': device_id,
'success': True,
'action': 'execute_command',
'command': command,
'params': params
})
else:
results.append({
'device_id': device_id,
'success': False,
'message': f'Unsupported action type: {action_type}'
})
except Exception as e:
results.append({
'device_id': device_id,
'success': False,
'message': str(e)
})
return {
'scene_id': scene_id,
'scene_name': scene['name'],
'success': any(result['success'] for result in results),
'results': results
}
def add_device_to_scene(self, scene_id, device_id, action_type, action_params):
"""Add a device action to a scene"""
scene = self.get_scene(scene_id)
if not scene:
return False
# Validate device exists
device = self.device_manager.get_device(device_id)
if not device:
return False
# Create action
action = {
'device_id': device_id,
'action_type': action_type,
'action_params': action_params
}
# Add action to scene
action_id = self.db.add_scene_action(scene_id, action)
return action_id is not None
def remove_device_from_scene(self, scene_id, action_id):
"""Remove a device action from a scene"""
# This would require adding a method to delete a specific action
# For now, we'll update the scene with all actions except the one to remove
scene = self.get_scene(scene_id)
if not scene:
return False
# Filter out the action to remove
updated_actions = [action for action in scene['actions'] if action['id'] != action_id]
# Update scene with new actions
return self.db.update_scene(scene_id, {'actions': updated_actions})
app_init_.py
"""
Smart Home System Application
"""
from flask import Flask
from flask_cors import CORS
def create_app():
"""Create and configure the Flask application"""
app = Flask(__name__,
static_folder='../static',
template_folder='../templates')
# Enable CORS
CORS(app)
# Load configuration
app.config.from_mapping(
SECRET_KEY='dev',
DATABASE=None,
)
# Register blueprints
from .routes import api
app.register_blueprint(api.bp)
return app
app\routes\api.py
"""
API routes for Smart Home System
"""
from flask import Blueprint, request, jsonify, current_app
from ..device_manager import DeviceManager, DeviceFactory
from ..scene_manager import SceneManager
bp = Blueprint('api', __name__, url_prefix='/api')
# Initialize device manager
device_manager = DeviceManager()
# Initialize scene manager
scene_manager = SceneManager(device_manager=device_manager)
@bp.route('/devices', methods=['GET'])
def get_devices():
"""Get all devices or filter by type/location"""
device_type = request.args.get('type')
location = request.args.get('location')
if device_type:
devices = device_manager.get_devices_by_type(device_type)
elif location:
devices = device_manager.get_devices_by_location(location)
else:
devices = device_manager.get_all_devices()
return jsonify(devices)
@bp.route('/devices/<device_id>', methods=['GET'])
def get_device(device_id):
"""Get a device by ID"""
device = device_manager.get_device(device_id)
if not device:
return jsonify({'error': 'Device not found'}), 404
return jsonify(device)
@bp.route('/devices', methods=['POST'])
def create_device():
"""Create a new device"""
data = request.json
if not data:
return jsonify({'error': 'No data provided'}), 400
# Required fields
name = data.get('name')
device_type = data.get('type')
location = data.get('location')
if not name or not device_type:
return jsonify({'error': 'Name and type are required'}), 400
# Check if device type is supported
if device_type not in DeviceFactory.get_device_types():
return jsonify({'error': f'Unsupported device type: {device_type}'}), 400
# Create device
try:
device_id = DeviceFactory.create_device(
device_manager,
device_type,
name,
location,
**data.get('properties', {})
)
if not device_id:
return jsonify({'error': 'Failed to create device'}), 500
# Get the created device
device = device_manager.get_device(device_id)
return jsonify(device), 201
except Exception as e:
return jsonify({'error': str(e)}), 500
@bp.route('/devices/<device_id>', methods=['PUT'])
def update_device(device_id):
"""Update a device"""
device = device_manager.get_device(device_id)
if not device:
return jsonify({'error': 'Device not found'}), 404
data = request.json
if not data:
return jsonify({'error': 'No data provided'}), 400
# Update name
if 'name' in data:
device['name'] = data['name']
# Update location
if 'location' in data:
device['location'] = data['location']
# Update properties
if 'properties' in data and isinstance(data['properties'], dict):
device_manager.update_device_properties(device_id, data['properties'])
# Update in database
device_manager.db.update_device(device_id, {
'name': device['name'],
'location': device['location']
})
return jsonify(device_manager.get_device(device_id))
@bp.route('/devices/<device_id>', methods=['DELETE'])
def delete_device(device_id):
"""Delete a device"""
device = device_manager.get_device(device_id)
if not device:
return jsonify({'error': 'Device not found'}), 404
if device_manager.delete_device(device_id):
return jsonify({'message': 'Device deleted successfully'})
return jsonify({'error': 'Failed to delete device'}), 500
@bp.route('/devices/<device_id>/control', methods=['POST'])
def control_device(device_id):
"""Control a device"""
device = device_manager.get_device(device_id)
if not device:
return jsonify({'error': 'Device not found'}), 404
data = request.json
if not data:
return jsonify({'error': 'No data provided'}), 400
command = data.get('command')
params = data.get('params', {})
if not command:
return jsonify({'error': 'Command is required'}), 400
# Check if command is supported by device
supported_commands = device.get('properties', {}).get('supported_commands', [])
if command not in supported_commands:
return jsonify({'error': f'Unsupported command: {command}'}), 400
# Execute command
try:
result = DeviceFactory.execute_command(device, command, params)
if result:
# Get updated device
updated_device = device_manager.get_device(device_id)
return jsonify({
'message': 'Command executed successfully',
'device': updated_device
})
return jsonify({'error': 'Failed to execute command'}), 500
except Exception as e:
return jsonify({'error': str(e)}), 500
@bp.route('/devices/<device_id>/history', methods=['GET'])
def get_device_history(device_id):
"""Get device history"""
device = device_manager.get_device(device_id)
if not device:
return jsonify({'error': 'Device not found'}), 404
limit = request.args.get('limit', 50, type=int)
history = device_manager.get_device_history(device_id, limit)
return jsonify(history)
@bp.route('/device-types', methods=['GET'])
def get_device_types():
"""Get all supported device types"""
device_types = DeviceFactory.get_device_types()
# Get schema for each device type
result = {}
for device_type in device_types:
try:
schema = DeviceFactory.get_properties_schema(device_type)
result[device_type] = {
'name': device_type,
'schema': schema
}
except Exception:
result[device_type] = {
'name': device_type,
'schema': {}
}
return jsonify(result)
@bp.route('/locations', methods=['GET'])
def get_locations():
"""Get all locations"""
locations = device_manager.db.get_all_locations()
return jsonify(locations)
@bp.route('/locations', methods=['POST'])
def add_location():
"""Add a new location"""
data = request.json
if not data:
return jsonify({'error': 'No data provided'}), 400
name = data.get('name')
description = data.get('description')
if not name:
return jsonify({'error': 'Name is required'}), 400
location_id = device_manager.db.add_location(name, description)
if location_id:
return jsonify({
'id': location_id,
'name': name,
'description': description
}), 201
return jsonify({'error': 'Failed to add location'}), 500
# Scene routes
@bp.route('/scenes', methods=['GET'])
def get_scenes():
"""Get all scenes"""
scenes = scene_manager.get_all_scenes()
return jsonify(scenes)
@bp.route('/scenes/<scene_id>', methods=['GET'])
def get_scene(scene_id):
"""Get a scene by ID"""
scene = scene_manager.get_scene(scene_id)
if not scene:
return jsonify({'error': 'Scene not found'}), 404
return jsonify(scene)
@bp.route('/scenes', methods=['POST'])
def create_scene():
"""Create a new scene"""
data = request.json
if not data:
return jsonify({'error': 'No data provided'}), 400
name = data.get('name')
description = data.get('description')
icon = data.get('icon')
actions = data.get('actions', [])
if not name:
return jsonify({'error': 'Name is required'}), 400
try:
scene = scene_manager.create_scene(name, description, icon, actions)
if scene:
return jsonify(scene), 201
return jsonify({'error': 'Failed to create scene'}), 500
except Exception as e:
return jsonify({'error': str(e)}), 500
@bp.route('/scenes/<scene_id>', methods=['PUT'])
def update_scene(scene_id):
"""Update a scene"""
scene = scene_manager.get_scene(scene_id)
if not scene:
return jsonify({'error': 'Scene not found'}), 404
data = request.json
if not data:
return jsonify({'error': 'No data provided'}), 400
try:
updated_scene = scene_manager.update_scene(scene_id, data)
if updated_scene:
return jsonify(updated_scene)
return jsonify({'error': 'Failed to update scene'}), 500
except Exception as e:
return jsonify({'error': str(e)}), 500
@bp.route('/scenes/<scene_id>', methods=['DELETE'])
def delete_scene(scene_id):
"""Delete a scene"""
scene = scene_manager.get_scene(scene_id)
if not scene:
return jsonify({'error': 'Scene not found'}), 404
if scene_manager.delete_scene(scene_id):
return jsonify({'message': 'Scene deleted successfully'})
return jsonify({'error': 'Failed to delete scene'}), 500
@bp.route('/scenes/<scene_id>/activate', methods=['POST'])
def activate_scene(scene_id):
"""Activate a scene"""
scene = scene_manager.get_scene(scene_id)
if not scene:
return jsonify({'error': 'Scene not found'}), 404
result = scene_manager.activate_scene(scene_id)
if result and result.get('success'):
return jsonify(result)
return jsonify({'error': 'Failed to activate scene', 'details': result}), 500
@bp.route('/scenes/<scene_id>/devices', methods=['POST'])
def add_device_to_scene(scene_id):
"""Add a device to a scene"""
scene = scene_manager.get_scene(scene_id)
if not scene:
return jsonify({'error': 'Scene not found'}), 404
data = request.json
if not data:
return jsonify({'error': 'No data provided'}), 400
device_id = data.get('device_id')
action_type = data.get('action_type')
action_params = data.get('action_params', {})
if not device_id or not action_type:
return jsonify({'error': 'Device ID and action type are required'}), 400
# Check if device exists
device = device_manager.get_device(device_id)
if not device:
return jsonify({'error': 'Device not found'}), 404
if scene_manager.add_device_to_scene(scene_id, device_id, action_type, action_params):
return jsonify({'message': 'Device added to scene successfully'})
return jsonify({'error': 'Failed to add device to scene'}), 500
app\routes_init_.py
static\css\styles.css
/* Smart Home System CSS */
/* General Styles */
body {
font-family: 'Microsoft YaHei', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f8f9fa;
}
.card {
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
transition: all 0.3s ease;
}
.card:hover {
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
}
/* Device Card Styles */
.card.border-success {
border-width: 2px;
}
.card.border-danger {
border-width: 2px;
}
/* Color Button Styles */
.color-btn {
width: 30px;
height: 30px;
border-radius: 50%;
border: 1px solid #dee2e6;
}
.color-btn:hover {
transform: scale(1.1);
}
/* Status Badge Styles */
.badge.bg-success {
background-color: #28a745 !important;
}
.badge.bg-danger {
background-color: #dc3545 !important;
}
.badge.bg-secondary {
background-color: #6c757d !important;
}
/* Progress Bar Animation */
.progress-bar {
transition: width 1s ease;
animation: progress-bar-stripes 1s linear infinite;
}
@keyframes progress-bar-stripes {
0% {
background-position: 1rem 0;
}
100% {
background-position: 0 0;
}
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}
/* Modal Animation */
.modal.fade .modal-dialog {
transition: transform 0.3s ease-out;
transform: translateY(-50px);
}
.modal.show .modal-dialog {
transform: none;
}
/* Toast Notification */
.toast {
opacity: 1 !important;
}
/* Responsive Adjustments */
@media (max-width: 768px) {
.card-footer {
flex-direction: column;
gap: 10px;
align-items: center;
}
.btn-group {
width: 100%;
}
}
3. JavaScript Application
static\js\app.js
/**
* Smart Home System - Frontend Application
*/
// API Base URL
const API_BASE_URL = '/api';
// Bootstrap components
let toastElement;
let addDeviceModal;
let editDeviceModal;
let deviceHistoryModal;
let addLocationModal;
let editLocationModal;
// Create Vue application
const app = Vue.createApp({
data() {
return {
// UI state
activeTab: 'devices',
// Data
devices: [],
locations: [],
deviceTypes: [],
deviceHistory: [],
// Filters
deviceTypeFilter: '',
locationFilter: '',
statusFilter: '',
// Selected items
selectedDevice: null,
editingDevice: null,
editingLocation: null,
// Form data
newDevice: {
name: '',
type: 'light',
location: '',
properties: {
brightness: 100,
color: 'white',
current_temp: 22,
target_temp: 22,
mode: 'off'
}
},
newLocation: {
name: '',
description: ''
}
};
},
computed: {
filteredDevices() {
return this.devices.filter(device => {
let matchesType = true;
let matchesLocation = true;
let matchesStatus = true;
if (this.deviceTypeFilter) {
matchesType = device.type === this.deviceTypeFilter;
}
if (this.locationFilter) {
matchesLocation = device.location === this.locationFilter;
}
if (this.statusFilter) {
matchesStatus = device.status === this.statusFilter;
}
return matchesType && matchesLocation && matchesStatus;
});
}
},
methods: {
// Device operations
async loadDevices() {
try {
const response = await axios.get(`${API_BASE_URL}/devices`);
this.devices = response.data;
} catch (error) {
this.showToast('加载设备失败', 'error');
console.error('Error loading devices:', error);
}
},
async loadDeviceTypes() {
try {
const response = await axios.get(`${API_BASE_URL}/device-types`);
this.deviceTypes = Object.keys(response.data);
} catch (error) {
this.showToast('加载设备类型失败', 'error');
console.error('Error loading device types:', error);
}
},
async addDevice() {
try {
// Prepare properties based on device type
let properties = {};
if (this.newDevice.type === 'light') {
properties = {
brightness: parseInt(this.newDevice.properties.brightness),
color: this.newDevice.properties.color
};
} else if (this.newDevice.type === 'thermostat') {
properties = {
current_temp: parseFloat(this.newDevice.properties.current_temp),
target_temp: parseFloat(this.newDevice.properties.target_temp),
mode: this.newDevice.properties.mode
};
}
const response = await axios.post(`${API_BASE_URL}/devices`, {
name: this.newDevice.name,
type: this.newDevice.type,
location: this.newDevice.location,
properties: properties
});
// Add new device to list
this.devices.push(response.data);
// Reset form
this.resetNewDeviceForm();
// Close modal
addDeviceModal.hide();
this.showToast('设备添加成功');
} catch (error) {
this.showToast('添加设备失败', 'error');
console.error('Error adding device:', error);
}
},
async updateDevice() {
if (!this.editingDevice) return;
try {
const deviceId = this.editingDevice.id;
await axios.put(`${API_BASE_URL}/devices/${deviceId}`, {
name: this.editingDevice.name,
location: this.editingDevice.location
});
// Update device in list
const index = this.devices.findIndex(d => d.id === deviceId);
if (index !== -1) {
this.devices[index].name = this.editingDevice.name;
this.devices[index].location = this.editingDevice.location;
}
// Close modal
editDeviceModal.hide();
this.showToast('设备更新成功');
} catch (error) {
this.showToast('更新设备失败', 'error');
console.error('Error updating device:', error);
}
},
async deleteDevice(device) {
if (!confirm(`确定要删除设备 "${device.name}" 吗?`)) return;
try {
await axios.delete(`${API_BASE_URL}/devices/${device.id}`);
// Remove device from list
this.devices = this.devices.filter(d => d.id !== device.id);
this.showToast('设备删除成功');
} catch (error) {
this.showToast('删除设备失败', 'error');
console.error('Error deleting device:', error);
}
},
async toggleDevice(device) {
const command = device.status === 'on' ? 'turn_off' : 'turn_on';
try {
const response = await axios.post(`${API_BASE_URL}/devices/${device.id}/control`, {
command: command
});
// Update device in list
const index = this.devices.findIndex(d => d.id === device.id);
if (index !== -1) {
this.devices[index] = response.data.device;
}
this.showToast(`设备${command === 'turn_on' ? '开启' : '关闭'}成功`);
} catch (error) {
this.showToast(`设备${command === 'turn_on' ? '开启' : '关闭'}失败`, 'error');
console.error('Error controlling device:', error);
}
},
async setBrightness(deviceId, brightness) {
try {
const response = await axios.post(`${API_BASE_URL}/devices/${deviceId}/control`, {
command: 'set_brightness',
params: {
brightness: parseInt(brightness)
}
});
// Update device in list
const index = this.devices.findIndex(d => d.id === deviceId);
if (index !== -1) {
this.devices[index] = response.data.device;
}
} catch (error) {
this.showToast('设置亮度失败', 'error');
console.error('Error setting brightness:', error);
}
},
async setColor(deviceId, color) {
try {
const response = await axios.post(`${API_BASE_URL}/devices/${deviceId}/control`, {
command: 'set_color',
params: {
color: color
}
});
// Update device in list
const index = this.devices.findIndex(d => d.id === deviceId);
if (index !== -1) {
this.devices[index] = response.data.device;
}
} catch (error) {
this.showToast('设置颜色失败', 'error');
console.error('Error setting color:', error);
}
},
async adjustTemperature(deviceId, delta) {
const device = this.devices.find(d => d.id === deviceId);
if (!device) return;
const currentTemp = device.properties.target_temp;
const newTemp = Math.min(Math.max(currentTemp + delta, 5), 35);
try {
const response = await axios.post(`${API_BASE_URL}/devices/${deviceId}/control`, {
command: 'set_temperature',
params: {
temperature: newTemp
}
});
// Update device in list
const index = this.devices.findIndex(d => d.id === deviceId);
if (index !== -1) {
this.devices[index] = response.data.device;
}
} catch (error) {
this.showToast('设置温度失败', 'error');
console.error('Error setting temperature:', error);
}
},
async setThermostatMode(deviceId, mode) {
try {
const response = await axios.post(`${API_BASE_URL}/devices/${deviceId}/control`, {
command: 'set_mode',
params: {
mode: mode
}
});
// Update device in list
const index = this.devices.findIndex(d => d.id === deviceId);
if (index !== -1) {
this.devices[index] = response.data.device;
}
} catch (error) {
this.showToast('设置模式失败', 'error');
console.error('Error setting mode:', error);
}
},
async loadDeviceHistory(deviceId) {
try {
const response = await axios.get(`${API_BASE_URL}/devices/${deviceId}/history`);
this.deviceHistory = response.data;
} catch (error) {
this.showToast('加载设备历史记录失败', 'error');
console.error('Error loading device history:', error);
}
},
// Location operations
async loadLocations() {
try {
const response = await axios.get(`${API_BASE_URL}/locations`);
this.locations = response.data;
} catch (error) {
this.showToast('加载位置失败', 'error');
console.error('Error loading locations:', error);
}
},
async addLocation() {
try {
await axios.post(`${API_BASE_URL}/locations`, {
name: this.newLocation.name,
description: this.newLocation.description
});
// Reload locations
await this.loadLocations();
// Reset form
this.newLocation = {
name: '',
description: ''
};
// Close modal
addLocationModal.hide();
this.showToast('位置添加成功');
} catch (error) {
this.showToast('添加位置失败', 'error');
console.error('Error adding location:', error);
}
},
// UI operations
showAddDeviceModal() {
this.resetNewDeviceForm();
addDeviceModal.show();
},
showEditDeviceModal(device) {
this.editingDevice = { ...device };
editDeviceModal.show();
},
showDeviceHistory(device) {
this.selectedDevice = device;
this.deviceHistory = [];
deviceHistoryModal.show();
this.loadDeviceHistory(device.id);
},
showAddLocationModal() {
this.newLocation = {
name: '',
description: ''
};
addLocationModal.show();
},
showEditLocationModal(location) {
this.editingLocation = { ...location };
editLocationModal.show();
},
resetNewDeviceForm() {
this.newDevice = {
name: '',
type: 'light',
location: '',
properties: {
brightness: 100,
color: 'white',
current_temp: 22,
target_temp: 22,
mode: 'off'
}
};
},
filterDevices() {
// This method is called when filters change
// The actual filtering is done in the computed property
},
getDeviceCountByType(type) {
return this.devices.filter(device => device.type === type).length;
},
getDeviceCountByLocation(location) {
return this.devices.filter(device => device.location === location).length;
},
getStatusBadgeClass(status) {
if (status === 'on') return 'bg-success';
if (status === 'off') return 'bg-secondary';
return 'bg-danger';
},
getStatusText(status) {
if (status === 'on') return '开启';
if (status === 'off') return '关闭';
return '离线';
},
formatDate(dateString) {
if (!dateString) return '';
const date = new Date(dateString);
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
},
showToast(message, type = 'success') {
const toastEl = document.getElementById('toast');
const toastMessageEl = document.getElementById('toastMessage');
toastMessageEl.textContent = message;
toastEl.classList.remove('bg-success', 'bg-danger');
toastEl.classList.add(type === 'success' ? 'bg-success' : 'bg-danger');
const toast = new bootstrap.Toast(toastEl);
toast.show();
}
},
mounted() {
// Initialize Bootstrap components
toastElement = document.getElementById('toast');
addDeviceModal = new bootstrap.Modal(document.getElementById('addDeviceModal'));
editDeviceModal = new bootstrap.Modal(document.getElementById('editDeviceModal'));
deviceHistoryModal = new bootstrap.Modal(document.getElementById('deviceHistoryModal'));
addLocationModal = new bootstrap.Modal(document.getElementById('addLocationModal'));
editLocationModal = new bootstrap.Modal(document.getElementById('editLocationModal'));
// Load data
this.loadDevices();
this.loadDeviceTypes();
this.loadLocations();
}
});
// Mount Vue application
app.mount('#app');
4. Requirements File
templates\index.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>智能家居控制系统 - 设备管理</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
<link rel="stylesheet" href="/static/css/styles.css">
</head>
<body>
<div id="app">
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container-fluid">
<a class="navbar-brand" href="#">
<i class="bi bi-house-gear-fill me-2"></i>
智能家居控制系统
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link active" href="#" @click="activeTab = 'devices'">
<i class="bi bi-grid-3x3-gap-fill me-1"></i>设备管理
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" @click="activeTab = 'locations'">
<i class="bi bi-geo-alt-fill me-1"></i>位置管理
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" @click="activeTab = 'statistics'">
<i class="bi bi-bar-chart-fill me-1"></i>数据统计
</a>
</li>
</ul>
</div>
</div>
</nav>
<div class="container mt-4">
<!-- Devices Tab -->
<div v-if="activeTab === 'devices'">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>设备管理</h2>
<button class="btn btn-primary" @click="showAddDeviceModal">
<i class="bi bi-plus-lg me-1"></i>添加设备
</button>
</div>
<!-- Filters -->
<div class="row mb-4">
<div class="col-md-4">
<div class="input-group">
<span class="input-group-text">类型</span>
<select class="form-select" v-model="deviceTypeFilter" @change="filterDevices">
<option value="">全部</option>
<option v-for="type in deviceTypes" :value="type">{{ type }}</option>
</select>
</div>
</div>
<div class="col-md-4">
<div class="input-group">
<span class="input-group-text">位置</span>
<select class="form-select" v-model="locationFilter" @change="filterDevices">
<option value="">全部</option>
<option v-for="location in locations" :value="location.name">{{ location.name }}</option>
</select>
</div>
</div>
<div class="col-md-4">
<div class="input-group">
<span class="input-group-text">状态</span>
<select class="form-select" v-model="statusFilter" @change="filterDevices">
<option value="">全部</option>
<option value="on">开启</option>
<option value="off">关闭</option>
<option value="offline">离线</option>
</select>
</div>
</div>
</div>
<!-- Devices List -->
<div class="row" v-if="filteredDevices.length > 0">
<div class="col-md-4 mb-4" v-for="device in filteredDevices" :key="device.id">
<div class="card h-100" :class="{'border-success': device.status === 'on', 'border-danger': device.status === 'offline'}">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">{{ device.name }}</h5>
<span class="badge" :class="getStatusBadgeClass(device.status)">
{{ getStatusText(device.status) }}
</span>
</div>
<div class="card-body">
<p class="card-text">
<i class="bi bi-tag-fill me-2"></i>类型: {{ device.type }}
</p>
<p class="card-text">
<i class="bi bi-geo-alt-fill me-2"></i>位置: {{ device.location || '未设置' }}
</p>
<!-- Light Device Controls -->
<div v-if="device.type === 'light' && device.status !== 'offline'">
<div class="mb-3">
<label class="form-label">亮度: {{ device.properties.brightness }}%</label>
<input type="range" class="form-range" min="0" max="100" step="1"
:value="device.properties.brightness"
@change="setBrightness(device.id, $event.target.value)">
</div>
<div class="mb-3">
<label class="form-label">颜色:</label>
<div class="d-flex">
<button class="btn btn-sm color-btn me-1"
v-for="color in ['white', 'red', 'green', 'blue', 'yellow']"
:style="{backgroundColor: color}"
@click="setColor(device.id, color)">
</button>
</div>
</div>
</div>
<!-- Thermostat Device Controls -->
<div v-if="device.type === 'thermostat' && device.status !== 'offline'">
<div class="mb-3">
<label class="form-label">当前温度: {{ device.properties.current_temp }}°C</label>
</div>
<div class="mb-3">
<label class="form-label">目标温度: {{ device.properties.target_temp }}°C</label>
<div class="d-flex align-items-center">
<button class="btn btn-sm btn-outline-primary me-2"
@click="adjustTemperature(device.id, -1)">
<i class="bi bi-dash"></i>
</button>
<span class="fs-5">{{ device.properties.target_temp }}°C</span>
<button class="btn btn-sm btn-outline-primary ms-2"
@click="adjustTemperature(device.id, 1)">
<i class="bi bi-plus"></i>
</button>
</div>
</div>
<div class="mb-3">
<label class="form-label">模式:</label>
<div class="btn-group w-100">
<button class="btn btn-sm"
:class="device.properties.mode === 'off' ? 'btn-primary' : 'btn-outline-primary'"
@click="setThermostatMode(device.id, 'off')">关闭</button>
<button class="btn btn-sm"
:class="device.properties.mode === 'heat' ? 'btn-primary' : 'btn-outline-primary'"
@click="setThermostatMode(device.id, 'heat')">制热</button>
<button class="btn btn-sm"
:class="device.properties.mode === 'cool' ? 'btn-primary' : 'btn-outline-primary'"
@click="setThermostatMode(device.id, 'cool')">制冷</button>
<button class="btn btn-sm"
:class="device.properties.mode === 'auto' ? 'btn-primary' : 'btn-outline-primary'"
@click="setThermostatMode(device.id, 'auto')">自动</button>
</div>
</div>
</div>
</div>
<div class="card-footer d-flex justify-content-between">
<button class="btn btn-sm"
:class="device.status === 'on' ? 'btn-danger' : 'btn-success'"
@click="toggleDevice(device)"
:disabled="device.status === 'offline'">
{{ device.status === 'on' ? '关闭' : '开启' }}
</button>
<div>
<button class="btn btn-sm btn-info me-1" @click="showDeviceHistory(device)">
<i class="bi bi-clock-history"></i>
</button>
<button class="btn btn-sm btn-primary me-1" @click="showEditDeviceModal(device)">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-danger" @click="deleteDevice(device)">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
</div>
</div>
</div>
<div class="alert alert-info" v-else>
没有找到符合条件的设备。
</div>
</div>
<!-- Locations Tab -->
<div v-if="activeTab === 'locations'">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>位置管理</h2>
<button class="btn btn-primary" @click="showAddLocationModal">
<i class="bi bi-plus-lg me-1"></i>添加位置
</button>
</div>
<div class="row">
<div class="col-md-4 mb-4" v-for="location in locations" :key="location.id">
<div class="card h-100">
<div class="card-header">
<h5 class="card-title mb-0">{{ location.name }}</h5>
</div>
<div class="card-body">
<p class="card-text" v-if="location.description">{{ location.description }}</p>
<p class="card-text" v-else>无描述</p>
<div class="mt-3">
<h6>设备数量:</h6>
<p>{{ getDeviceCountByLocation(location.name) }} 个设备</p>
</div>
</div>
<div class="card-footer d-flex justify-content-end">
<button class="btn btn-sm btn-primary me-1" @click="showEditLocationModal(location)">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-danger"
@click="deleteLocation(location)"
:disabled="getDeviceCountByLocation(location.name) > 0">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Statistics Tab -->
<div v-if="activeTab === 'statistics'">
<h2 class="mb-4">数据统计</h2>
<div class="row">
<div class="col-md-4 mb-4">
<div class="card h-100">
<div class="card-header bg-primary text-white">
<h5 class="card-title mb-0">设备总览</h5>
</div>
<div class="card-body">
<div class="d-flex justify-content-between mb-3">
<span>总设备数:</span>
<span class="fw-bold">{{ devices.length }}</span>
</div>
<div class="d-flex justify-content-between mb-3">
<span>在线设备:</span>
<span class="fw-bold">{{ devices.filter(d => d.status !== 'offline').length }}</span>
</div>
<div class="d-flex justify-content-between mb-3">
<span>开启设备:</span>
<span class="fw-bold">{{ devices.filter(d => d.status === 'on').length }}</span>
</div>
<div class="d-flex justify-content-between">
<span>离线设备:</span>
<span class="fw-bold">{{ devices.filter(d => d.status === 'offline').length }}</span>
</div>
</div>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="card h-100">
<div class="card-header bg-success text-white">
<h5 class="card-title mb-0">设备类型分布</h5>
</div>
<div class="card-body">
<div v-for="type in deviceTypes" :key="type" class="mb-3">
<div class="d-flex justify-content-between mb-1">
<span>{{ type }}:</span>
<span class="fw-bold">{{ getDeviceCountByType(type) }}</span>
</div>
<div class="progress">
<div class="progress-bar" role="progressbar"
:style="{width: (getDeviceCountByType(type) / devices.length * 100) + '%'}"
:aria-valuenow="getDeviceCountByType(type)"
aria-valuemin="0" :aria-valuemax="devices.length">
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="card h-100">
<div class="card-header bg-info text-white">
<h5 class="card-title mb-0">位置分布</h5>
</div>
<div class="card-body">
<div v-for="location in locations" :key="location.id" class="mb-3">
<div class="d-flex justify-content-between mb-1">
<span>{{ location.name }}:</span>
<span class="fw-bold">{{ getDeviceCountByLocation(location.name) }}</span>
</div>
<div class="progress">
<div class="progress-bar bg-info" role="progressbar"
:style="{width: (getDeviceCountByLocation(location.name) / devices.length * 100) + '%'}"
:aria-valuenow="getDeviceCountByLocation(location.name)"
aria-valuemin="0" :aria-valuemax="devices.length">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Add Device Modal -->
<div class="modal fade" id="addDeviceModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">添加设备</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form @submit.prevent="addDevice">
<div class="mb-3">
<label class="form-label">设备名称</label>
<input type="text" class="form-control" v-model="newDevice.name" required>
</div>
<div class="mb-3">
<label class="form-label">设备类型</label>
<select class="form-select" v-model="newDevice.type" required>
<option v-for="type in deviceTypes" :value="type">{{ type }}</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">位置</label>
<select class="form-select" v-model="newDevice.location">
<option value="">未设置</option>
<option v-for="location in locations" :value="location.name">{{ location.name }}</option>
</select>
</div>
<!-- Light device properties -->
<div v-if="newDevice.type === 'light'">
<div class="mb-3">
<label class="form-label">亮度</label>
<input type="range" class="form-range" min="0" max="100" step="1" v-model="newDevice.properties.brightness">
<div class="text-center">{{ newDevice.properties.brightness }}%</div>
</div>
<div class="mb-3">
<label class="form-label">颜色</label>
<select class="form-select" v-model="newDevice.properties.color">
<option value="white">白色</option>
<option value="red">红色</option>
<option value="green">绿色</option>
<option value="blue">蓝色</option>
<option value="yellow">黄色</option>
</select>
</div>
</div>
<!-- Thermostat device properties -->
<div v-if="newDevice.type === 'thermostat'">
<div class="mb-3">
<label class="form-label">当前温度 (°C)</label>
<input type="number" class="form-control" v-model="newDevice.properties.current_temp" min="0" max="40">
</div>
<div class="mb-3">
<label class="form-label">目标温度 (°C)</label>
<input type="number" class="form-control" v-model="newDevice.properties.target_temp" min="5" max="35">
</div>
<div class="mb-3">
<label class="form-label">模式</label>
<select class="form-select" v-model="newDevice.properties.mode">
<option value="off">关闭</option>
<option value="heat">制热</option>
<option value="cool">制冷</option>
<option value="auto">自动</option>
</select>
</div>
</div>
<div class="text-end">
<button type="button" class="btn btn-secondary me-2" data-bs-dismiss="modal">取消</button>
<button type="submit" class="btn btn-primary">添加</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Edit Device Modal -->
<div class="modal fade" id="editDeviceModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">编辑设备</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form @submit.prevent="updateDevice" v-if="editingDevice">
<div class="mb-3">
<label class="form-label">设备名称</label>
<input type="text" class="form-control" v-model="editingDevice.name" required>
</div>
<div class="mb-3">
<label class="form-label">位置</label>
<select class="form-select" v-model="editingDevice.location">
<option value="">未设置</option>
<option v-for="location in locations" :value="location.name">{{ location.name }}</option>
</select>
</div>
<div class="text-end">
<button type="button" class="btn btn-secondary me-2" data-bs-dismiss="modal">取消</button>
<button type="submit" class="btn btn-primary">保存</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Device History Modal -->
<div class="modal fade" id="deviceHistoryModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">设备历史记录</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<h6 v-if="selectedDevice">{{ selectedDevice.name }}</h6>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>时间</th>
<th>状态</th>
<th>属性</th>
</tr>
</thead>
<tbody>
<tr v-for="entry in deviceHistory" :key="entry.id">
<td>{{ formatDate(entry.timestamp) }}</td>
<td>
<span class="badge" :class="getStatusBadgeClass(entry.status)">
{{ getStatusText(entry.status) }}
</span>
</td>
<td>
<pre v-if="entry.properties" class="mb-0">{{ JSON.stringify(entry.properties, null, 2) }}</pre>
<span v-else>-</span>
</td>
</tr>
</tbody>
</table>
</div>
<div class="alert alert-info" v-if="deviceHistory.length === 0">
没有历史记录。
</div>
</div>
</div>
</div>
</div>
<!-- Add Location Modal -->
<div class="modal fade" id="addLocationModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">添加位置</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form @submit.prevent="addLocation">
<div class="mb-3">
<label class="form-label">位置名称</label>
<input type="text" class="form-control" v-model="newLocation.name" required>
</div>
<div class="mb-3">
<label class="form-label">描述</label>
<textarea class="form-control" v-model="newLocation.description" rows="3"></textarea>
</div>
<div class="text-end">
<button type="button" class="btn btn-secondary me-2" data-bs-dismiss="modal">取消</button>
<button type="submit" class="btn btn-primary">添加</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Edit Location Modal -->
<div class="modal fade" id="editLocationModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">编辑位置</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form @submit.prevent="updateLocation" v-if="editingLocation">
<div class="mb-3">
<label class="form-label">位置名称</label>
<input type="text" class="form-control" v-model="editingLocation.name" required>
</div>
<div class="mb-3">
<label class="form-label">描述</label>
<textarea class="form-control" v-model="editingLocation.description" rows="3"></textarea>
</div>
<div class="text-end">
<button type="button" class="btn btn-secondary me-2" data-bs-dismiss="modal">取消</button>
<button type="submit" class="btn btn-primary">保存</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Toast Notifications -->
<div class="toast-container position-fixed bottom-0 end-0 p-3">
<div id="toast" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-header">
<strong class="me-auto">通知</strong>
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body" id="toastMessage"></div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@3.2.45/dist/vue.global.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="/static/js/app.js"></script>
</body>
</html>