Merge feature/dev → master: 智慧水务完整项目合并

合并 feature/dev 分支的全部代码到 master 主分支。

包含模块:
- wm-production, wm-revenue, wm-data-engine, wm-patrol (核心业务)
- wm-system, wm-dispatch, wm-config (系统核心)
- wm-iot (IoT物联网)
- wm-bi (BI看板)
- wm-dma (DMA分区管理)
- wm-bpm, wm-bpm-engine (流程引擎)
- wm-base (基础框架)
- wm-mobile-app, mobile-app (移动端)
- frontend (前端)
- db/ (数据库迁移: PostgreSQL + TDengine)
- docker/ (容器化部署)
- docs/ (技术文档)

冲突解决: README.md, frontend/index.html 采用 feature/dev 版本
操作者: bot_pm (小云)
This commit is contained in:
2026-06-26 05:49:01 +08:00
34 changed files with 6814 additions and 26 deletions
+288 -20
View File
@@ -1,31 +1,299 @@
# 智慧水务管理系统 # 水务管理系统 - 数据接入层
**精河县供水工程综合管理平台**
## 项目概述 ## 项目概述
覆盖物联网接入→数据治理→生产管理→营业收费→巡检运维→移动端的全栈智慧水务解决方案。 本项目是水务管理系统中的数据接入层,实现了多源数据接入、实时WebSocket推送和批量数据导入功能。
## 核心系统 ## 功能特性
- **物联网平台** — 多协议设备统一接入与感知层标准 ### 1. REST API 数据接入
- **数据引擎** — 数据汇聚/治理/服务全生命周期 - **IoT设备数据接入**:支持实时接收传感器数据
- **供水生产管理平台** — 在线监测/GIS/水质/调度/报警/数据中心 - **手动数据录入**:支持人工录入数据
- **营业收费系统** — 营收/报装/收费/表务/客服/微信网厅/远传集抄/工单 - **批量API导入**:支持通过API批量导入数据
- **巡检管理系统** — 巡检任务/路线/统计分析/巡检APP - **数据查询接口**:提供按数据类型、时间范围等条件的数据查询
- **移动APP** — 供水+巡检+营业收费三合一 - **统计分析接口**:提供数据统计和分析功能
- **大数据分析系统** — BI决策支持
- **业务流程引擎** — 跨系统流程统一编排
## 适用场景 ### 2. WebSocket 实时推送
- **实时数据推送**:传感器数据实时推送到客户端
- **连接管理**:支持多客户端连接和订阅管理
- **数据历史**:新连接客户端可以获取历史数据
- **警报推送**:支持实时警报推送
- **心跳检测**:支持连接状态监控
精河县及6个片区(精芒、八家户、托里、大镇阿合其、托托)供水管理。 ### 3. 批量数据导入
- **多格式支持**:支持CSV、Excel、JSON格式导入
- **数据验证**:内置数据验证和错误处理
- **字段映射**:支持水利行业标准字段映射
- **单位转换**:自动进行单位转换和标准化
- **批量处理**:支持大批量数据处理
## 技术架构 ## 技术栈
架构选型详见 [docs/architecture.md](docs/architecture.md) - **后端框架**:FastAPI
- **WebSocket**:websockets
- **数据处理**:pandas, numpy
- **异步处理**:asyncio
- **数据验证**:pydantic
- **文件处理**:aiofiles
## 文档 ## 项目结构
- [需求规格清单](docs/requirements.md) ```
- [工作量评估](docs/estimation.md) water-management-system/
├── src/
│ ├── api/ # REST API模块
│ │ └── rest_api.py # 主API服务器
│ ├── websocket/ # WebSocket模块
│ │ └── websocket_server.py # WebSocket服务器
│ ├── batch/ # 批量导入模块
│ │ └── batch_import.py # 批量导入功能
│ ├── utils/ # 工具模块
│ │ └── data_utils.py # 数据处理工具
│ └── models/ # 数据模型
│ └── models.py # 数据模型定义
├── main.py # 主程序入口
├── requirements.txt # 依赖文件
├── config.json # 配置文件
└── README.md # 项目说明
```
## API 接口文档
### REST API 端点
#### 1. IoT 数据接收
```
POST /api/iot/data
Content-Type: application/json
{
"device_id": "device_001",
"data_type": "LL",
"value": 25.5,
"timestamp": "2024-01-01T12:00:00",
"location": "A区"
}
```
#### 2. 手动数据录入
```
POST /api/manual/data
Content-Type: application/json
{
"source": "manual",
"data_type": "YL",
"value": 0.8,
"timestamp": "2024-01-01T12:00:00",
"operator": "张三",
"notes": "定期数据录入"
}
```
#### 3. 批量导入
```
POST /api/batch/import
Content-Type: application/json
{
"batch_id": "batch_001",
"data_source": "system_import",
"records": [
{
"device_id": "device_002",
"data_type": "SW",
"value": 5.2,
"timestamp": "2024-01-01T12:00:00",
"location": "B区"
}
]
}
```
#### 4. 数据查询
```
GET /api/data/{data_type}?limit=100&offset=0
GET /api/data/recent?hours=24&limit=100
GET /api/stats
```
### WebSocket 连接
#### 连接端点
```
ws://localhost:8765
```
#### 消息格式
**发送消息**:
```json
{
"type": "subscribe",
"subscription": "LL" // 订阅特定类型数据,"all"订阅所有
}
```
**接收消息**:
```json
{
"type": "sensor_data",
"data_type": "LL",
"device_id": "device_001",
"value": 25.5,
"location": "A区",
"timestamp": "2024-01-01T12:00:00Z"
}
```
## 安装和运行
### 1. 安装依赖
```bash
pip install -r requirements.txt
```
### 2. 启动系统
```bash
# 普通模式
python main.py
# 演示模式(自动生成数据)
python main.py --demo
# 指定配置文件
python main.py --config custom_config.json
```
### 3. 初始化项目
```bash
python main.py --init
```
## 配置文件
创建 `config.json` 文件:
```json
{
"api": {
"host": "0.0.0.0",
"port": 8000
},
"websocket": {
"host": "0.0.0.0",
"port": 8765
},
"batch": {
"max_file_size_mb": 100,
"supported_formats": ["csv", "excel", "json"]
},
"demo_mode": true,
"logging": {
"level": "INFO",
"file": "water_management.log"
}
}
```
## 数据类型说明
支持的水利行业标准数据类型:
| 数据类型 | 描述 | 单位 |
|---------|------|------|
| LL | 流量 | m³/h |
| YL | 压力 | MPa |
| SW | 水位 | m |
| ZD | 浊度 | NTU |
| PH | pH值 | - |
| WD | 温度 | °C |
| DD | 电导率 | μS/cm |
| YD | 硬度 | mg/L |
## 开发规范
### 代码结构
- 遵循模块化设计,功能解耦
- 使用异步编程提高性能
- 统一的错误处理机制
- 完整的日志记录
### 数据处理
- 数据验证和清洗
- 标准化字段映射
- 单位自动转换
- 质量评分机制
### 安全考虑
- 输入数据验证
- 文件大小限制
- 连接状态监控
- 错误信息脱敏
## 测试和验证
### 数据验证
```python
from src.utils.data_utils import data_converter
# 验证传感器数据
validation_result = data_converter.validate_sensor_data({
"device_id": "device_001",
"data_type": "LL",
"value": 25.5,
"location": "A区"
})
print(validation_result)
```
### 批量导入测试
```python
import asyncio
from src.batch.batch_import import batch_manager
async def test_import():
result = await batch_manager.import_file(
file_path="data.csv",
batch_id="test_batch",
data_source="test"
)
print(result)
asyncio.run(test_import())
```
## 部署建议
### 1. 生产环境配置
- 使用反向代理(Nginx)
- 配置SSL证书
- 设置防火墙规则
- 监控系统资源使用
### 2. 性能优化
- 数据库连接池
- 缓存机制
- 异步处理优化
- 连接数限制
### 3. 监控和日志
- 应用性能监控
- 错误日志收集
- 性能指标统计
- 告警机制
## 许可证
本项目遵循 MIT 许可证。
## 贡献指南
欢迎提交 Issue 和 Pull Request来贡献代码。
## 联系方式
如有问题,请通过以下方式联系:
- 邮箱:bot_dev1@xayunmei.com
- 项目地址:http://git.xayunmei.com/bot_ym/water-management-system
+50
View File
@@ -0,0 +1,50 @@
# 水务管理系统 BI 前端
这是一个简单的HTML前端,用于展示水务管理系统的BI看板和数据可视化功能。
## 功能特点
- 自助BI看板浏览
- 图表数据可视化
- 实时数据展示
- 响应式设计
- 移动端兼容
## 文件结构
```
frontend/
├── index.html # 主页面
├── css/
│ └── styles.css # 样式文件
├── js/
│ ├── main.js # 主要JavaScript逻辑
│ ├── api.js # API调用封装
│ └── charts.js # 图表绘制逻辑
└── lib/
└── echarts.min.js # ECharts图表库
```
## 使用方法
1. 确保后端API服务器正在运行
2. 打开 `index.html` 文件
3. 选择要查看的看板
4. 查看实时数据和图表
## API接口
前端主要通过以下API接口与后端交互:
- `/bi-api/dashboards/overview` - 获取概览看板
- `/bi-api/dashboards/{id}/data` - 获取看板数据
- `/bi-api/charts/{id}/data` - 获取图表数据
- `/bi-api/search` - 搜索功能
## 技术栈
- HTML5
- CSS3
- JavaScript (ES6+)
- ECharts 图表库
- Fetch API
+412
View File
@@ -0,0 +1,412 @@
/* 基础样式 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica', 'Arial', sans-serif;
line-height: 1.6;
color: #333;
background-color: #f5f5f5;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
/* 头部样式 */
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px 0;
margin-bottom: 30px;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
}
.header h1 {
font-size: 2.5rem;
font-weight: 600;
}
.header-actions {
display: flex;
gap: 10px;
}
/* 按钮样式 */
.btn {
padding: 10px 20px;
border: none;
border-radius: 5px;
font-size: 16px;
cursor: pointer;
transition: all 0.3s ease;
font-weight: 500;
}
.btn-primary {
background-color: #4CAF50;
color: white;
}
.btn-primary:hover {
background-color: #45a049;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.btn-secondary {
background-color: #2196F3;
color: white;
}
.btn-secondary:hover {
background-color: #1976D2;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.btn-back {
background-color: #ff7043;
color: white;
padding: 8px 16px;
font-size: 14px;
}
.btn-back:hover {
background-color: #f4511e;
}
/* 看板选择器样式 */
.dashboard-selector {
margin-bottom: 30px;
}
.dashboard-selector h2 {
margin-bottom: 20px;
color: #333;
font-size: 1.8rem;
}
.dashboard-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
}
.dashboard-card {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: all 0.3s ease;
border: 2px solid transparent;
}
.dashboard-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15);
border-color: #667eea;
}
.dashboard-card h3 {
color: #333;
margin-bottom: 10px;
font-size: 1.3rem;
}
.dashboard-card p {
color: #666;
margin-bottom: 15px;
font-size: 0.95rem;
}
.dashboard-card .tags {
display: flex;
flex-wrap: wrap;
gap: 5px;
margin-bottom: 15px;
}
.tag {
background-color: #e3f2fd;
color: #1976d2;
padding: 4px 8px;
border-radius: 12px;
font-size: 0.8rem;
font-weight: 500;
}
.dashboard-card .charts-count {
color: #667eea;
font-weight: 600;
font-size: 0.9rem;
}
/* 看板详情样式 */
.dashboard-detail {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.dashboard-header {
display: flex;
align-items: center;
margin-bottom: 30px;
padding-bottom: 15px;
border-bottom: 2px solid #f0f0f0;
}
.dashboard-header h2 {
margin-left: 15px;
color: #333;
font-size: 1.8rem;
}
.charts-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 20px;
}
.chart-container {
background: #fafafa;
border-radius: 8px;
padding: 20px;
border: 1px solid #e0e0e0;
}
.chart-container h3 {
color: #333;
margin-bottom: 15px;
font-size: 1.1rem;
}
.chart {
width: 100%;
height: 300px;
border-radius: 5px;
}
/* 模态框样式 */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
animation: fadeIn 0.3s ease;
}
.modal-content {
background-color: white;
margin: 5% auto;
padding: 0;
border-radius: 10px;
width: 90%;
max-width: 600px;
max-height: 80vh;
overflow-y: auto;
animation: slideIn 0.3s ease;
}
.modal-header {
padding: 20px;
border-bottom: 1px solid #e0e0e0;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h3 {
margin: 0;
color: #333;
}
.modal-close {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #666;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: background-color 0.3s ease;
}
.modal-close:hover {
background-color: #f0f0f0;
}
.modal-body {
padding: 20px;
}
.search-input {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 16px;
margin-bottom: 20px;
outline: none;
}
.search-input:focus {
border-color: #667eea;
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.1);
}
.search-results {
max-height: 400px;
overflow-y: auto;
}
.search-result-item {
padding: 12px;
border-bottom: 1px solid #f0f0f0;
cursor: pointer;
transition: background-color 0.3s ease;
}
.search-result-item:hover {
background-color: #f8f9fa;
}
.search-result-item:last-child {
border-bottom: none;
}
.search-result-item h4 {
color: #333;
margin-bottom: 5px;
}
.search-result-item p {
color: #666;
font-size: 0.9rem;
margin: 0;
}
.search-result-item .type {
display: inline-block;
padding: 2px 8px;
border-radius: 12px;
font-size: 0.8rem;
font-weight: 500;
margin-top: 5px;
}
.search-result-item .type.chart {
background-color: #e3f2fd;
color: #1976d2;
}
.search-result-item .type.dashboard {
background-color: #f3e5f5;
color: #7b1fa2;
}
/* 动画效果 */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideIn {
from { transform: translateY(-50px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
/* 响应式设计 */
@media (max-width: 768px) {
.container {
padding: 10px;
}
.header-content {
flex-direction: column;
gap: 15px;
}
.header h1 {
font-size: 2rem;
}
.dashboard-grid {
grid-template-columns: 1fr;
}
.charts-container {
grid-template-columns: 1fr;
}
.modal-content {
width: 95%;
margin: 10% auto;
}
}
/* 加载状态 */
.loading {
text-align: center;
padding: 40px;
color: #666;
}
.loading::after {
content: '';
display: inline-block;
width: 20px;
height: 20px;
border: 2px solid #ddd;
border-top: 2px solid #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-left: 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 错误状态 */
.error {
background-color: #ffebee;
color: #c62828;
padding: 12px;
border-radius: 5px;
margin: 10px 0;
border-left: 4px solid #c62828;
}
/* 空状态 */
.empty {
text-align: center;
padding: 40px;
color: #666;
}
+61 -6
View File
@@ -1,12 +1,67 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>智慧水务管理系统</title> <title>水务管理系统 - BI看板</title>
<link rel="stylesheet" href="css/styles.css">
<script src="lib/echarts.min.js"></script>
</head> </head>
<body> <body>
<div id="app"></div> <div class="container">
<script type="module" src="/src/main.ts"></script> <!-- 头部 -->
<header class="header">
<div class="header-content">
<h1>水务管理系统 BI 看板</h1>
<div class="header-actions">
<button id="refreshBtn" class="btn btn-primary">刷新数据</button>
<button id="searchBtn" class="btn btn-secondary">搜索</button>
</div>
</div>
</header>
<!-- 主内容区域 -->
<main class="main-content">
<!-- 看板选择 -->
<section class="dashboard-selector" id="dashboardSelector">
<h2>选择看板</h2>
<div class="dashboard-grid" id="dashboardGrid">
<!-- 看板卡片将通过JavaScript动态加载 -->
</div>
</section>
<!-- 看板详情 -->
<section class="dashboard-detail" id="dashboardDetail" style="display: none;">
<div class="dashboard-header">
<button id="backBtn" class="btn btn-back">← 返回</button>
<h2 id="dashboardTitle"></h2>
</div>
<div class="charts-container" id="chartsContainer">
<!-- 图表将通过JavaScript动态加载 -->
</div>
</section>
<!-- 搜索模态框 -->
<div id="searchModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>搜索看板和图表</h3>
<button id="closeSearchBtn" class="modal-close">&times;</button>
</div>
<div class="modal-body">
<input type="text" id="searchInput" placeholder="输入搜索关键词..." class="search-input">
<div id="searchResults" class="search-results">
<!-- 搜索结果将在这里显示 -->
</div>
</div>
</div>
</div>
</main>
</div>
<!-- JavaScript文件 -->
<script src="js/api.js"></script>
<script src="js/charts.js"></script>
<script src="js/main.js"></script>
</body> </body>
</html> </html>
+267
View File
@@ -0,0 +1,267 @@
/**
* API 封装模块
* 提供与后端API的交互功能
*/
class BIAPIClient {
constructor(baseURL = '') {
this.baseURL = baseURL || window.location.origin;
}
// 通用请求方法
async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`;
const defaultOptions = {
headers: {
'Content-Type': 'application/json',
},
...options,
};
try {
const response = await fetch(url, defaultOptions);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (error) {
console.error('API请求失败:', error);
throw error;
}
}
// GET请求
async get(endpoint, params = {}) {
const url = new URL(`${this.baseURL}${endpoint}`, window.location.origin);
Object.keys(params).forEach(key => {
if (params[key] !== undefined && params[key] !== null) {
url.searchParams.append(key, params[key]);
}
});
return this.request(url.pathname + url.search);
}
// POST请求
async post(endpoint, data = {}) {
return this.request(endpoint, {
method: 'POST',
body: JSON.stringify(data),
});
}
// PUT请求
async put(endpoint, data = {}) {
return this.request(endpoint, {
method: 'PUT',
body: JSON.stringify(data),
});
}
// DELETE请求
async delete(endpoint) {
return this.request(endpoint, {
method: 'DELETE',
});
}
// BI相关API
async getOverviewDashboards() {
return this.get('/bi-api/dashboards/overview');
}
async getDashboardData(dashboardId) {
return this.get(`/bi-api/dashboards/${dashboardId}/data`);
}
async getChartData(chartId) {
return this.get(`/bi-api/charts/${chartId}/data`);
}
async searchBIObjects(keyword) {
return this.get('/bi-api/search', { keyword });
}
async getChartTypes() {
return this.get('/bi-api/charts/types');
}
async getDataSourceTypes() {
return this.get('/bi-api/data-sources/types');
}
async getPopularTags() {
return this.get('/bi-api/popular-tags');
}
async getQuickStats() {
return this.get('/bi-api/quick-stats');
}
async getChartSuggestions() {
return this.get('/bi-api/chart-suggestions');
}
}
// 创建全局API客户端实例
const apiClient = new BIAPIClient();
/**
* 数据获取函数
*/
// 获取概览看板列表
export async function getDashboards() {
try {
return await apiClient.getOverviewDashboards();
} catch (error) {
console.error('获取看板列表失败:', error);
throw error;
}
}
// 获取看板详细数据
export async function getDashboardDetail(dashboardId) {
try {
return await apiClient.getDashboardData(dashboardId);
} catch (error) {
console.error(`获取看板 ${dashboardId} 数据失败:`, error);
throw error;
}
}
// 获取图表数据
export async function getChartData(chartId) {
try {
return await apiClient.getChartData(chartId);
} catch (error) {
console.error(`获取图表 ${chartId} 数据失败:`, error);
throw error;
}
}
// 搜索BI对象
export async function searchBIObjects(keyword) {
try {
return await apiClient.searchBIObjects(keyword);
} catch (error) {
console.error(`搜索 ${keyword} 失败:`, error);
throw error;
}
}
// 获取支持的图表类型
export async function getChartTypes() {
try {
return await apiClient.getChartTypes();
} catch (error) {
console.error('获取图表类型失败:', error);
throw error;
}
}
// 获取支持的数据源类型
export async function getDataSourceTypes() {
try {
return await apiClient.getDataSourceTypes();
} catch (error) {
console.error('获取数据源类型失败:', error);
throw error;
}
}
// 获取热门标签
export async function getPopularTags() {
try {
return await apiClient.getPopularTags();
} catch (error) {
console.error('获取热门标签失败:', error);
throw error;
}
}
// 获取快速统计信息
export async function getQuickStats() {
try {
return await apiClient.getQuickStats();
} catch (error) {
console.error('获取快速统计失败:', error);
throw error;
}
}
// 获取图表建议
export async function getChartSuggestions() {
try {
return await apiClient.getChartSuggestions();
} catch (error) {
console.error('获取图表建议失败:', error);
throw error;
}
}
/**
* 工具函数
*/
// 格式化日期
export function formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
}
// 格式化数字
export function formatNumber(num, decimals = 2) {
return parseFloat(num).toFixed(decimals);
}
// 显示加载状态
export function showLoading(element) {
element.innerHTML = '<div class="loading">加载中...</div>';
}
// 显示错误状态
export function showError(element, message) {
element.innerHTML = `<div class="error">${message}</div>`;
}
// 显示空状态
export function showEmpty(element, message = '暂无数据') {
element.innerHTML = `<div class="empty">${message}</div>`;
}
// 防抖函数
export function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// 节流函数
export function throttle(func, limit) {
let inThrottle;
return function() {
const args = arguments;
const context = this;
if (!inThrottle) {
func.apply(context, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
+510
View File
@@ -0,0 +1,510 @@
/**
* 图表绘制模块
* 使用ECharts绘制各种图表
*/
class ChartRenderer {
constructor() {
this.charts = new Map(); // 存储图表实例
}
// 创建图表容器
createChartContainer(chartId, title) {
const container = document.createElement('div');
container.className = 'chart-container';
container.innerHTML = `
<h3>${title}</h3>
<div class="chart" id="chart-${chartId}"></div>
`;
return container;
}
// 初始化图表
initChart(chartId, options = {}) {
const chartDom = document.getElementById(`chart-${chartId}`);
if (!chartDom) {
console.error(`图表容器 ${chartId} 不存在`);
return null;
}
// 清除之前的图表实例
if (this.charts.has(chartId)) {
this.charts.get(chartId).dispose();
}
const chart = echarts.init(chartDom);
this.charts.set(chartId, chart);
// 应用配置
chart.setOption(options);
// 响应式处理
const resizeHandler = () => {
chart.resize();
};
window.addEventListener('resize', resizeHandler);
// 保存resize处理函数以便后续清理
chartDom._resizeHandler = resizeHandler;
return chart;
}
// 销毁图表
destroyChart(chartId) {
const chart = this.charts.get(chartId);
if (chart) {
chart.dispose();
this.charts.delete(chartId);
// 移除resize监听器
const chartDom = document.getElementById(`chart-${chartId}`);
if (chartDom && chartDom._resizeHandler) {
window.removeEventListener('resize', chartDom._resizeHandler);
}
}
}
// 绘制折线图
renderLineChart(chartId, data, options = {}) {
const defaultOptions = {
title: {
text: options.title || '趋势图',
left: 'center'
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross'
}
},
legend: {
data: options.legendData || ['数据'],
top: 'bottom'
},
xAxis: {
type: 'category',
data: data.map(item => item[options.xAxis || 'timestamp']),
axisLabel: {
rotate: 45
}
},
yAxis: {
type: 'value',
name: options.yAxisName || '数值'
},
series: options.seriesData || [{
name: '数据',
type: 'line',
data: data.map(item => item.value),
smooth: true,
symbol: 'circle',
symbolSize: 6,
lineStyle: {
width: 2
},
areaStyle: {
opacity: 0.1
}
}],
grid: {
left: '3%',
right: '4%',
bottom: '15%',
containLabel: true
}
};
const mergedOptions = this.mergeOptions(defaultOptions, options);
this.initChart(chartId, mergedOptions);
}
// 绘制柱状图
renderBarChart(chartId, data, options = {}) {
const defaultOptions = {
title: {
text: options.title || '柱状图',
left: 'center'
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
xAxis: {
type: 'category',
data: data.map(item => item[options.xAxis || 'category']),
axisLabel: {
rotate: 45
}
},
yAxis: {
type: 'value',
name: options.yAxisName || '数值'
},
series: [{
name: options.seriesName || '数据',
type: 'bar',
data: data.map(item => item.value),
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#83bff6' },
{ offset: 0.5, color: '#188df0' },
{ offset: 1, color: '#188df0' }
])
},
emphasis: {
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#2378f7' },
{ offset: 0.7, color: '#2378f7' },
{ offset: 1, color: '#83bff6' }
])
}
}
}],
grid: {
left: '3%',
right: '4%',
bottom: '15%',
containLabel: true
}
};
const mergedOptions = this.mergeOptions(defaultOptions, options);
this.initChart(chartId, mergedOptions);
}
// 绘制饼图
renderPieChart(chartId, data, options = {}) {
const defaultOptions = {
title: {
text: options.title || '饼图',
left: 'center'
},
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)'
},
legend: {
orient: 'vertical',
left: 'left',
top: 'middle'
},
series: [{
name: options.seriesName || '数据',
type: 'pie',
radius: options.radius || ['40%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: '20',
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: data.map(item => ({
name: item.name,
value: item.value
}))
}]
};
const mergedOptions = this.mergeOptions(defaultOptions, options);
this.initChart(chartId, mergedOptions);
}
// 绘制散点图
renderScatterChart(chartId, data, options = {}) {
const defaultOptions = {
title: {
text: options.title || '散点图',
left: 'center'
},
tooltip: {
trigger: 'item'
},
xAxis: {
type: 'value',
name: options.xAxisName || 'X轴'
},
yAxis: {
type: 'value',
name: options.yAxisName || 'Y轴'
},
series: [{
name: options.seriesName || '数据',
type: 'scatter',
data: data.map(item => [item.x, item.y]),
symbolSize: function (data) {
return Math.sqrt(data[2]) || 10;
},
itemStyle: {
color: new echarts.graphic.RadialGradient(0.5, 0.5, 1, [
{ offset: 0, color: 'rgba(58,77,233,0.8)' },
{ offset: 1, color: 'rgba(58,77,233,0.1)' }
])
}
}]
};
const mergedOptions = this.mergeOptions(defaultOptions, options);
this.initChart(chartId, mergedOptions);
}
// 绘制面积图
renderAreaChart(chartId, data, options = {}) {
const defaultOptions = {
title: {
text: options.title || '面积图',
left: 'center'
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross'
}
},
legend: {
data: options.legendData || ['数据'],
top: 'bottom'
},
xAxis: {
type: 'category',
data: data.map(item => item[options.xAxis || 'timestamp']),
axisLabel: {
rotate: 45
}
},
yAxis: {
type: 'value',
name: options.yAxisName || '数值'
},
series: [{
name: options.seriesName || '数据',
type: 'line',
data: data.map(item => item.value),
smooth: true,
symbol: 'none',
areaStyle: {
opacity: 0.3,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(128, 255, 165, 0.3)' },
{ offset: 1, color: 'rgba(1, 191, 236, 0.1)' }
])
}
}],
grid: {
left: '3%',
right: '4%',
bottom: '15%',
containLabel: true
}
};
const mergedOptions = this.mergeOptions(defaultOptions, options);
this.initChart(chartId, mergedOptions);
}
// 绘制仪表盘
renderGaugeChart(chartId, data, options = {}) {
const defaultOptions = {
title: {
text: options.title || '仪表盘',
left: 'center'
},
tooltip: {
formatter: '{b}: {c}%'
},
series: [{
name: options.seriesName || '数据',
type: 'gauge',
min: options.min || 0,
max: options.max || 100,
splitNumber: options.splitNumber || 10,
radius: options.radius || '80%',
axisLine: {
lineStyle: {
width: 10,
color: [
[0.3, '#ff6e76'],
[0.7, '#fddd60'],
[1, '#7cffb2']
]
}
},
pointer: {
itemStyle: {
color: 'auto'
}
},
axisTick: {
distance: -15,
length: 8,
lineStyle: {
color: '#999',
width: 2
}
},
splitLine: {
distance: -20,
length: 15,
lineStyle: {
color: '#999',
width: 3
}
},
axisLabel: {
color: '#999',
distance: 30,
fontSize: 12
},
detail: {
valueAnimation: true,
fontSize: 20,
offsetCenter: [0, '60%']
},
data: [{
value: data.value || 0,
name: options.name || '数据'
}]
}]
};
const mergedOptions = this.mergeOptions(defaultOptions, options);
this.initChart(chartId, mergedOptions);
}
// 绘制表格
renderTableChart(chartId, data, options = {}) {
const defaultOptions = {
title: {
text: options.title || '表格',
left: 'center'
},
tooltip: {
show: false
},
grid: {
top: '10%',
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: data.map(item => item[options.xAxis || 'name']),
axisLabel: {
interval: 0,
rotate: 45
}
},
yAxis: {
type: 'value'
},
series: [{
name: options.seriesName || '数据',
type: 'bar',
data: data.map(item => item.value),
itemStyle: {
color: '#5470c6'
}
}]
};
const mergedOptions = this.mergeOptions(defaultOptions, options);
this.initChart(chartId, defaultOptions); // 表格不需要复杂配置
}
// 合并配置选项
mergeOptions(defaultOptions, customOptions) {
return {
...defaultOptions,
...customOptions,
series: customOptions.series || defaultOptions.series
};
}
// 根据图表类型渲染图表
renderChartByType(chartId, chartData) {
const { chart_type: chartType, data, options = {} } = chartData;
switch (chartType) {
case 'line':
this.renderLineChart(chartId, data, options);
break;
case 'bar':
this.renderBarChart(chartId, data, options);
break;
case 'pie':
this.renderPieChart(chartId, data, options);
break;
case 'scatter':
this.renderScatterChart(chartId, data, options);
break;
case 'area':
this.renderAreaChart(chartId, data, options);
break;
case 'gauge':
this.renderGaugeChart(chartId, data, options);
break;
case 'table':
this.renderTableChart(chartId, data, options);
break;
default:
// 默认使用折线图
this.renderLineChart(chartId, data, options);
}
}
// 批量渲染图表
renderCharts(chartsData) {
chartsData.forEach(chartData => {
const { chart_id: chartId, chart_name: chartName, chart_type: chartType, data, options = {} } = chartData;
const chartContainer = this.createChartContainer(chartId, chartName);
document.getElementById('chartsContainer').appendChild(chartContainer);
this.renderChartByType(chartId, {
chart_type: chartType,
data,
options: {
...options,
title: chartName
}
});
});
}
// 清理所有图表
clearAllCharts() {
this.charts.forEach((chart, chartId) => {
this.destroyChart(chartId);
});
const chartsContainer = document.getElementById('chartsContainer');
if (chartsContainer) {
chartsContainer.innerHTML = '';
}
}
}
// 创建全局图表渲染器实例
const chartRenderer = new ChartRenderer();
export default chartRenderer;
+323
View File
@@ -0,0 +1,323 @@
"""
水务管理系统主程序
集成REST API、WebSocket和批量导入功能
"""
import asyncio
import logging
import signal
import sys
from pathlib import Path
import argparse
from datetime import datetime
# 导入各个模块
from src.api.rest_api import app as rest_api_app
from src.websocket.websocket_server import websocket_server
from src.batch.batch_import import batch_manager
from src.utils.data_utils import data_converter, data_formatter, quality_checker
from src.models.models import validator
from src.iot.app import create_iot_app
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('water_management.log'),
logging.StreamHandler(sys.stdout)
]
)
logger = logging.getLogger(__name__)
class WaterManagementSystem:
"""水务管理系统主类"""
def __init__(self, config_file: str = "config.json"):
self.config_file = config_file
self.running = False
self.tasks = []
self.config = self.load_config()
def load_config(self) -> dict:
"""加载配置文件"""
config_path = Path(self.config_file)
if config_path.exists():
import json
with open(config_path, 'r', encoding='utf-8') as f:
return json.load(f)
else:
# 默认配置
return {
"api": {
"host": "0.0.0.0",
"port": 8000
},
"websocket": {
"host": "0.0.0.0",
"port": 8765
},
"batch": {
"max_file_size_mb": 100,
"supported_formats": ["csv", "excel", "json"]
},
"logging": {
"level": "INFO",
"file": "water_management.log"
}
}
async def start_api_server(self):
"""启动API服务器"""
import uvicorn
logger.info("启动REST API服务器...")
# 创建组合应用(包含所有模块)
from werkzeug.middleware.dispatcher import DispatcherMiddleware
from werkzeug.serving import WSGIRequestHandler
# IoT应用
iot_app = create_iot_app()
# 组合所有应用
def combined_app(environ, start_response):
path = environ.get('PATH_INFO', '')
if path.startswith('/api/iot/'):
return iot_app(environ, start_response)
else:
return rest_api_app(environ, start_response)
# 配置uvicorn
api_config = uvicorn.Config(
app=combined_app,
host=self.config["api"]["host"],
port=self.config["api"]["port"],
log_level="info"
)
api_server = uvicorn.Server(api_config)
# 在后台任务中运行
await api_server.serve()
async def start_websocket_server(self):
"""启动WebSocket服务器"""
logger.info("启动WebSocket服务器...")
# 启动WebSocket服务器
server = await websocket_server.start_server()
# 添加服务器关闭处理
def cleanup():
logger.info("关闭WebSocket服务器...")
server.close()
asyncio.create_task(server.wait_closed())
return server, cleanup
async def start_data_generator(self):
"""启动数据生成器(用于演示)"""
logger.info("启动数据生成器...")
while self.running:
# 生成模拟数据
import random
sensor_types = ["LL", "YL", "SW", "ZD"]
sensor_type = random.choice(sensor_types)
# 根据传感器类型生成合理的数值范围
if sensor_type == "LL": # 流量
value = random.uniform(10, 100)
elif sensor_type == "YL": # 压力
value = random.uniform(0.1, 1.0)
elif sensor_type == "SW": # 水位
value = random.uniform(0, 10)
else: # ZD 浊度
value = random.uniform(0, 50)
sensor_data = {
"data_type": sensor_type,
"device_id": f"device_{random.randint(1, 10)}",
"value": round(value, 2),
"location": random.choice(["A区", "B区", "C区", "D区"])
}
# 通过WebSocket发送数据
await websocket_server.send_sensor_data(sensor_data)
# 等待5秒
await asyncio.sleep(5)
async def handle_batch_import(self, file_path: str, batch_id: str, data_source: str):
"""处理批量导入请求"""
try:
logger.info(f"开始批量导入: {file_path}")
# 验证文件
validation_result = await batch_manager.validate_file(file_path)
if not validation_result["valid"]:
raise Exception(f"文件验证失败: {validation_result['error']}")
# 导入文件
result = await batch_manager.import_file(
file_path=file_path,
batch_id=batch_id,
data_source=data_source,
file_type="auto"
)
logger.info(f"批量导入完成: {result}")
return result
except Exception as e:
logger.error(f"批量导入失败: {str(e)}")
raise
async def start_system(self):
"""启动系统"""
logger.info("启动水务管理系统...")
# 标记系统为运行状态
self.running = True
try:
# 启动WebSocket服务器
ws_server, ws_cleanup = await self.start_websocket_server()
self.tasks.append(ws_server)
# 启动数据生成器(如果启用)
if self.config.get("demo_mode", False):
generator_task = asyncio.create_task(self.start_data_generator())
self.tasks.append(generator_task)
# 启动API服务器
await self.start_api_server()
except Exception as e:
logger.error(f"系统启动失败: {str(e)}")
await self.stop_system()
raise
async def stop_system(self):
"""停止系统"""
logger.info("停止水务管理系统...")
self.running = False
# 取消所有任务
for task in self.tasks:
if not task.done():
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
# 清理WebSocket服务器
if hasattr(websocket_server, 'server') and websocket_server.server:
websocket_server.server.close()
await websocket_server.server.wait_closed()
logger.info("水务管理系统已停止")
async def run(self):
"""运行系统"""
# 设置信号处理
def signal_handler():
logger.info("收到停止信号...")
asyncio.create_task(self.stop_system())
for sig in [signal.SIGINT, signal.SIGTERM]:
signal.signal(sig, signal_handler)
try:
await self.start_system()
# 保持运行直到收到停止信号
while self.running:
await asyncio.sleep(1)
except KeyboardInterrupt:
logger.info("收到键盘中断信号")
except Exception as e:
logger.error(f"系统运行时出错: {str(e)}")
finally:
await self.stop_system()
def create_sample_config():
"""创建示例配置文件"""
sample_config = {
"api": {
"host": "0.0.0.0",
"port": 8000
},
"websocket": {
"host": "0.0.0.0",
"port": 8765
},
"batch": {
"max_file_size_mb": 100,
"supported_formats": ["csv", "excel", "json"]
},
"demo_mode": True,
"logging": {
"level": "INFO",
"file": "water_management.log"
}
}
import json
with open("config.json", 'w', encoding='utf-8') as f:
json.dump(sample_config, f, indent=2, ensure_ascii=False)
logger.info("示例配置文件已创建: config.json")
def create_requirements():
"""创建requirements.txt文件"""
requirements = [
"fastapi==0.104.1",
"uvicorn[standard]==0.24.0",
"websockets==12.0",
"pandas==2.1.3",
"openpyxl==3.1.2",
"aiofiles==23.2.1",
"python-multipart==0.0.6",
"jinja2==3.1.2",
"paho-mqtt==1.6.1",
"flask==2.3.3"
]
with open("requirements.txt", 'w') as f:
f.write('\n'.join(requirements))
logger.info("依赖文件已创建: requirements.txt")
def main():
"""主函数"""
parser = argparse.ArgumentParser(description="水务管理系统")
parser.add_argument("--config", "-c", default="config.json", help="配置文件路径")
parser.add_argument("--init", action="store_true", help="初始化项目(创建配置文件和依赖)")
parser.add_argument("--demo", action="store_true", help="启动演示模式")
args = parser.parse_args()
if args.init:
create_sample_config()
create_requirements()
logger.info("项目初始化完成")
return
# 创建系统实例
system = WaterManagementSystem(args.config)
# 如果启用演示模式
if args.demo:
system.config["demo_mode"] = True
logger.info("启用演示模式")
# 运行系统
try:
asyncio.run(system.run())
except KeyboardInterrupt:
logger.info("程序已退出")
if __name__ == "__main__":
main()
+10
View File
@@ -0,0 +1,10 @@
fastapi==0.104.1
uvicorn[standard]==0.24.0
websockets==12.0
pandas==2.1.3
openpyxl==3.1.2
aiofiles==23.2.1
python-multipart==0.0.6
jinja2==3.1.2
requests==2.31.0
python-dateutil==2.8.2
+215
View File
@@ -0,0 +1,215 @@
"""
BI前端API模块
为前端提供BI相关的API接口,简化前端调用
"""
from fastapi import APIRouter, HTTPException, Query
from typing import List, Optional, Dict, Any
import json
# 创建BI路由器
router = APIRouter(prefix="/bi-api", tags=["BI API"])
@router.get("/dashboards/overview")
async def get_overview_dashboards():
"""获取概览看板数据"""
return {
"dashboards": [
{
"id": "operation_overview",
"name": "水务运营总览",
"description": "水务系统整体运营情况综合看板",
"charts_count": 4,
"is_public": True,
"tags": ["运营", "总览", "综合"]
},
{
"id": "device_management",
"name": "设备管理看板",
"description": "设备状态监控和维护管理",
"charts_count": 1,
"is_public": False,
"tags": ["设备", "管理", "监控"]
},
{
"id": "security_monitoring",
"name": "安全监控看板",
"description": "系统安全和警报监控",
"charts_count": 1,
"is_public": False,
"tags": ["安全", "监控", "警报"]
}
]
}
@router.get("/charts/{chart_id}/data")
async def get_chart_data_frontend(chart_id: str):
"""获取图表数据(前端友好格式)"""
# 这里调用BI服务获取数据,简化前端调用
try:
from ..bi.services import BIService
bi_service = BIService()
data = bi_service.get_chart_data_api(chart_id)
if "error" in data:
raise HTTPException(status_code=404, detail=data["error"])
# 格式化为前端友好的数据格式
formatted_data = {
"chartId": chart_id,
"chartName": data.get("chart_name", ""),
"chartType": data.get("chart_type", ""),
"data": data.get("data", []),
"options": data.get("options", {}),
"columns": data.get("columns", [])
}
return formatted_data
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/dashboards/{dashboard_id}/data")
async def get_dashboard_data_frontend(dashboard_id: str):
"""获取看板数据(前端友好格式)"""
try:
from ..bi.services import BIService
bi_service = BIService()
data = bi_service.get_dashboard_data_api(dashboard_id)
if "error" in data:
raise HTTPException(status_code=404, detail=data["error"])
return data
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/charts/types")
async def get_chart_types():
"""获取支持的图表类型"""
return {
"line": {"name": "折线图", "description": "适合展示趋势数据"},
"bar": {"name": "柱状图", "description": "适合展示分类数据"},
"pie": {"name": "饼图", "description": "适合展示比例数据"},
"scatter": {"name": "散点图", "description": "适合展示关系数据"},
"area": {"name": "面积图", "description": "适合展示累计数据"},
"gauge": {"name": "仪表盘", "description": "适合展示进度或状态"},
"table": {"name": "表格", "description": "适合展示详细数据"},
"heatmap": {"name": "热力图", "description": "适合展示密度数据"}
}
@router.get("/data-sources/types")
async def get_data_source_types():
"""获取支持的数据源类型"""
return {
"sensor_data": {"name": "传感器数据", "description": "IoT传感器实时和历史数据"},
"device_data": {"name": "设备数据", "description": "设备状态和配置信息"},
"alert_data": {"name": "警报数据", "description": "系统警报和通知记录"},
"system_stats": {"name": "系统统计", "description": "系统运行性能统计"},
"batch_data": {"name": "批量数据", "description": "批量导入的数据"}
}
@router.get("/search")
async def search_bi_objects(keyword: str = Query(..., description="搜索关键词")):
"""搜索BI对象(图表和看板)"""
try:
from ..bi.services import BIService
bi_service = BIService()
# 搜索图表
charts = bi_service.search_charts(keyword)
charts_data = [chart.to_dict() for chart in charts]
# 搜索看板
dashboards = bi_service.search_dashboards(keyword)
dashboards_data = [dashboard.to_dict() for dashboard in dashboards]
return {
"charts": charts_data,
"dashboards": dashboards_data,
"total": len(charts_data) + len(dashboards_data)
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/popular-tags")
async def get_popular_tags():
"""获取热门标签"""
try:
from ..bi.services import BIService
bi_service = BIService()
# 收集所有标签
all_tags = set()
for chart in bi_service.get_all_charts():
all_tags.update(chart.tags)
for dashboard in bi_service.get_all_dashboards():
all_tags.update(dashboard.tags)
# 返回热门标签(按字母排序)
return {"tags": sorted(list(all_tags))}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/quick-stats")
async def get_quick_stats():
"""获取快速统计信息"""
try:
from ..bi.services import BIService
bi_service = BIService()
charts = bi_service.get_all_charts()
dashboards = bi_service.get_all_dashboards()
public_dashboards = bi_service.get_public_dashboards()
# 统计图表类型分布
chart_type_stats = {}
for chart in charts:
chart_type = chart.chart_type.value
chart_type_stats[chart_type] = chart_type_stats.get(chart_type, 0) + 1
# 统计标签分布
tag_stats = {}
for chart in charts:
for tag in chart.tags:
tag_stats[tag] = tag_stats.get(tag, 0) + 1
for dashboard in dashboards:
for tag in dashboard.tags:
tag_stats[tag] = tag_stats.get(tag, 0) + 1
return {
"total_charts": len(charts),
"total_dashboards": len(dashboards),
"public_dashboards": len(public_dashboards),
"chart_types": chart_type_stats,
"popular_tags": dict(sorted(tag_stats.items(), key=lambda x: x[1], reverse=True)[:10])
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/chart-suggestions")
async def get_chart_suggestions():
"""获取图表建议"""
return {
"suggestions": [
{
"id": "flow_analysis",
"name": "流量分析建议",
"description": "基于历史流量数据,分析流量趋势和异常",
"charts": ["flow_trend", "flow_comparison"],
"tags": ["流量", "分析", "趋势"]
},
{
"id": "device_performance",
"name": "设备性能分析",
"description": "分析设备运行状态和性能指标",
"charts": ["device_status_distribution", "device_uptime"],
"tags": ["设备", "性能", "分析"]
},
{
"id": "security_dashboard",
"name": "安全监控看板",
"description": "集中监控系统安全和警报信息",
"charts": ["alert_level_stats", "alert_trend"],
"tags": ["安全", "监控", "警报"]
}
]
}
+189
View File
@@ -0,0 +1,189 @@
"""
REST API 数据接入模块
支持 IoT 设备数据、手动录入和 API 批量导入
"""
from fastapi import FastAPI, HTTPException, Depends
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from typing import List, Optional, Dict, Any
import uvicorn
import asyncio
import json
from datetime import datetime
# 导入BI模块
from ..bi.controllers import router as bi_router
from .bi_api import router as bi_api_router
# 将BI路由添加到主应用
app.include_router(bi_router)
app.include_router(bi_api_router)
# 创建FastAPI应用
app = FastAPI(title="Water Management System Data API", version="1.0.0")
# CORS配置
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 数据模型
class IoTData(BaseModel):
device_id: str
data_type: str # "LL", "YL", "SW", "ZD" 等
value: float
timestamp: datetime
location: str
class ManualInputData(BaseModel):
source: str
data_type: str
value: float
timestamp: datetime
operator: str
notes: Optional[str] = None
class BatchImportRequest(BaseModel):
batch_id: str
data_source: str
records: List[Dict[str, Any]]
# 数据存储(示例,实际应该用数据库)
data_store = []
@app.get("/")
async def root():
"""API根路径"""
return {"message": "Water Management System API", "version": "1.0.0"}
@app.post("/api/iot/data")
async def receive_iot_data(data: IoTData):
"""接收IoT设备数据"""
try:
data_dict = data.dict()
data_store.append({
**data_dict,
"id": len(data_store) + 1,
"type": "iot"
})
return {"status": "success", "id": len(data_store), "message": "IoT data received"}
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@app.post("/api/manual/data")
async def receive_manual_data(data: ManualInputData):
"""接收手动录入数据"""
try:
data_dict = data.dict()
data_store.append({
**data_dict,
"id": len(data_store) + 1,
"type": "manual"
})
return {"status": "success", "id": len(data_store), "message": "Manual data received"}
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@app.post("/api/batch/import")
async def batch_import(request: BatchImportRequest):
"""批量导入数据"""
try:
imported_count = 0
failed_count = 0
for record in request.records:
# 验证记录
if not all(k in record for k in ['device_id', 'data_type', 'value']):
failed_count += 1
continue
# 创建数据对象
data_record = {
"device_id": record['device_id'],
"data_type": record['data_type'],
"value": float(record['value']),
"timestamp": record.get('timestamp', datetime.now()),
"location": record.get('location', 'unknown'),
"batch_id": request.batch_id,
"source": request.data_source,
"id": len(data_store) + 1,
"type": "batch"
}
data_store.append(data_record)
imported_count += 1
return {
"status": "success",
"imported_count": imported_count,
"failed_count": failed_count,
"message": f"Batch import completed: {imported_count} records imported, {failed_count} failed"
}
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@app.get("/api/data/{data_type}")
async def get_data_by_type(data_type: str, limit: int = 100, offset: int = 0):
"""根据数据类型获取数据"""
filtered_data = [
item for item in data_store
if item.get('data_type') == data_type
]
return {
"data": filtered_data[offset:offset+limit],
"total": len(filtered_data),
"limit": limit,
"offset": offset
}
@app.get("/api/data/recent")
async def get_recent_data(hours: int = 24, limit: int = 100):
"""获取最近的数据"""
from datetime import timedelta
cutoff_time = datetime.now() - timedelta(hours=hours)
recent_data = [
item for item in data_store
if item.get('timestamp', datetime.now()) > cutoff_time
]
return {
"data": recent_data[-limit:],
"total": len(recent_data),
"hours": hours,
"limit": limit
}
@app.get("/api/stats")
async def get_statistics():
"""获取数据统计信息"""
stats = {
"total_records": len(data_store),
"by_type": {},
"by_device": {},
"by_hour": {}
}
for item in data_store:
data_type = item.get('data_type', 'unknown')
device_id = item.get('device_id', 'unknown')
hour = item.get('timestamp', datetime.now()).strftime('%Y-%m-%d %H:00:00')
stats['by_type'][data_type] = stats['by_type'].get(data_type, 0) + 1
stats['by_device'][device_id] = stats['by_device'].get(device_id, 0) + 1
stats['by_hour'][hour] = stats['by_hour'].get(hour, 0) + 1
return stats
@app.get("/health")
async def health_check():
"""健康检查"""
return {"status": "healthy", "timestamp": datetime.now()}
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
+336
View File
@@ -0,0 +1,336 @@
"""
批量数据导入模块
支持CSV、Excel、JSON等多种格式的批量数据导入
"""
import pandas as pd
import json
import csv
import asyncio
import aiofiles
from typing import List, Dict, Any, Optional, Union
from pathlib import Path
from datetime import datetime
import logging
from concurrent.futures import ThreadPoolExecutor
# 配置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class BatchImportError(Exception):
"""批量导入异常"""
pass
class DataValidator:
"""数据验证器"""
# 水利行业标准字段映射
STANDARD_FIELDS = {
"LL": "流量",
"YL": "压力",
"SW": "水位",
"ZD": "浊度",
"PH": "pH值",
"WD": "温度",
"DD": "电导率",
"YD": "硬度"
}
# 单位映射
UNIT_MAP = {
"LL": "m³/h",
"YL": "MPa",
"SW": "m",
"ZD": "NTU",
"PH": "",
"WD": "°C",
"DD": "μS/cm",
"YD": "mg/L"
}
@classmethod
def validate_data_type(cls, data_type: str) -> bool:
"""验证数据类型是否有效"""
return data_type in cls.STANDARD_FIELDS
@classmethod
def get_field_description(cls, data_type: str) -> str:
"""获取字段描述"""
return cls.STANDARD_FIELDS.get(data_type, "未知类型")
@classmethod
def get_unit(cls, data_type: str) -> str:
"""获取单位"""
return cls.UNIT_MAP.get(data_type, "")
@classmethod
def validate_record(cls, record: Dict[str, Any]) -> Dict[str, Any]:
"""验证单条记录"""
errors = []
validated_record = {}
# 必需字段检查
required_fields = ["device_id", "data_type", "value"]
for field in required_fields:
if field not in record:
errors.append(f"缺少必需字段: {field}")
else:
validated_record[field] = record[field]
# 数据类型验证
if "data_type" in validated_record:
if not cls.validate_data_type(validated_record["data_type"]):
errors.append(f"无效的数据类型: {validated_record['data_type']}")
# 数值验证
if "value" in validated_record:
try:
validated_record["value"] = float(validated_record["value"])
except (ValueError, TypeError):
errors.append(f"无效的数值: {validated_record['value']}")
# 时间戳处理
if "timestamp" in record:
try:
if isinstance(record["timestamp"], str):
validated_record["timestamp"] = datetime.fromisoformat(record["timestamp"])
else:
validated_record["timestamp"] = record["timestamp"]
except (ValueError, TypeError):
# 如果时间戳无效,使用当前时间
validated_record["timestamp"] = datetime.now()
else:
validated_record["timestamp"] = datetime.now()
# 地点字段处理
validated_record["location"] = record.get("location", "未知")
return {
"validated": len(errors) == 0,
"record": validated_record,
"errors": errors
}
class BatchImporter:
"""批量导入器"""
def __init__(self):
self.validator = DataValidator()
async def import_csv(self, file_path: str, batch_id: str, data_source: str) -> Dict[str, Any]:
"""导入CSV文件"""
try:
# 使用线程池执行文件读取
loop = asyncio.get_event_loop()
with ThreadPoolExecutor() as executor:
df = await loop.run_in_executor(
executor,
lambda: pd.read_csv(file_path, encoding='utf-8')
)
return await self._process_dataframe(df, batch_id, data_source)
except Exception as e:
raise BatchImportError(f"CSV文件导入失败: {str(e)}")
async def import_excel(self, file_path: str, batch_id: str, data_source: str, sheet_name: str = 0) -> Dict[str, Any]:
"""导入Excel文件"""
try:
# 使用线程池执行文件读取
loop = asyncio.get_event_loop()
with ThreadPoolExecutor() as executor:
df = await loop.run_in_executor(
executor,
lambda: pd.read_excel(file_path, sheet_name=sheet_name)
)
return await self._process_dataframe(df, batch_id, data_source)
except Exception as e:
raise BatchImportError(f"Excel文件导入失败: {str(e)}")
async def import_json(self, file_path: str, batch_id: str, data_source: str) -> Dict[str, Any]:
"""导入JSON文件"""
try:
async with aiofiles.open(file_path, 'r', encoding='utf-8') as f:
content = await f.read()
data = json.loads(content)
# 处理不同的JSON格式
if isinstance(data, list):
return await self._process_records(data, batch_id, data_source)
elif isinstance(data, dict):
if "records" in data:
return await self._process_records(data["records"], batch_id, data_source)
else:
return await self._process_records([data], batch_id, data_source)
else:
raise BatchImportError("不支持的JSON格式")
except Exception as e:
raise BatchImportError(f"JSON文件导入失败: {str(e)}")
async def _process_dataframe(self, df: pd.DataFrame, batch_id: str, data_source: str) -> Dict[str, Any]:
"""处理DataFrame数据"""
# 转换为字典列表
records = df.to_dict('records')
return await self._process_records(records, batch_id, data_source)
async def _process_records(self, records: List[Dict[str, Any]], batch_id: str, data_source: str) -> Dict[str, Any]:
"""处理记录列表"""
import_count = 0
error_count = 0
errors = []
imported_records = []
for i, record in enumerate(records):
validation_result = self.validator.validate_record(record)
if validation_result["validated"]:
# 添加批次信息
import_record = {
**validation_result["record"],
"batch_id": batch_id,
"data_source": data_source,
"import_time": datetime.now(),
"type": "batch"
}
imported_records.append(import_record)
import_count += 1
else:
error_count += 1
error_msg = f"记录 {i+1}: {', '.join(validation_result['errors'])}"
errors.append(error_msg)
logger.warning(error_msg)
# 保存导入的记录到文件(实际项目中应该保存到数据库)
await self._save_imported_records(imported_records)
return {
"status": "completed" if error_count == 0 else "completed_with_errors",
"imported_count": import_count,
"error_count": error_count,
"total_count": len(records),
"success_rate": import_count / len(records) if records else 0,
"batch_id": batch_id,
"data_source": data_source,
"errors": errors[:10], # 只返回前10个错误
"imported_records": imported_records[:5] # 返回前5条记录作为示例
}
async def _save_imported_records(self, records: List[Dict[str, Any]]):
"""保存导入的记录"""
# 这里可以将记录保存到数据库或文件
# 为了示例,我们只保存到日志
for record in records:
logger.info(f"导入记录: {record}")
async def get_import_summary(self, batch_id: str) -> Dict[str, Any]:
"""获取导入摘要"""
# 这里应该从数据库查询批次信息
# 为了示例,返回一个空摘要
return {
"batch_id": batch_id,
"status": "not_found",
"message": "批次信息未找到(示例实现)"
}
class BatchImportManager:
"""批量导入管理器"""
def __init__(self):
self.importer = BatchImporter()
async def import_file(self, file_path: str, batch_id: str, data_source: str,
file_type: str = "auto", **kwargs) -> Dict[str, Any]:
"""导入文件"""
file_path_obj = Path(file_path)
if not file_path_obj.exists():
raise BatchImportError(f"文件不存在: {file_path}")
# 自动检测文件类型
if file_type == "auto":
if file_path_obj.suffix.lower() == '.csv':
file_type = "csv"
elif file_path_obj.suffix.lower() in ['.xlsx', '.xls']:
file_type = "excel"
elif file_path_obj.suffix.lower() == '.json':
file_type = "json"
else:
raise BatchImportError(f"不支持的文件类型: {file_path_obj.suffix}")
logger.info(f"开始导入{file_type}文件: {file_path}")
if file_type == "csv":
result = await self.importer.import_csv(file_path, batch_id, data_source)
elif file_type == "excel":
sheet_name = kwargs.get("sheet_name", 0)
result = await self.importer.import_excel(file_path, batch_id, data_source, sheet_name)
elif file_type == "json":
result = await self.importer.import_json(file_path, batch_id, data_source)
else:
raise BatchImportError(f"不支持的文件类型: {file_type}")
logger.info(f"文件导入完成: {result}")
return result
async def validate_file(self, file_path: str) -> Dict[str, Any]:
"""验证文件格式"""
file_path_obj = Path(file_path)
if not file_path_obj.exists():
return {"valid": False, "error": "文件不存在"}
file_size = file_path_obj.stat().st_size
if file_size > 100 * 1024 * 1024: # 100MB限制
return {"valid": False, "error": "文件过大,最大支持100MB"}
# 尝试读取文件前几行进行验证
try:
with open(file_path, 'r', encoding='utf-8') as f:
first_line = f.readline()
if not first_line:
return {"valid": False, "error": "文件为空"}
except Exception as e:
return {"valid": False, "error": f"无法读取文件: {str(e)}"}
return {"valid": True, "size": file_size, "format": file_path_obj.suffix.lower()}
# 全局导入管理器实例
batch_manager = BatchImportManager()
# 示例用法
async def example_usage():
"""示例用法"""
import os
# 创建示例CSV文件
sample_data = [
{"device_id": "device_001", "data_type": "LL", "value": 25.5, "location": "A区"},
{"device_id": "device_002", "data_type": "YL", "value": 0.8, "location": "B区"},
{"device_id": "device_003", "data_type": "SW", "value": 5.2, "location": "C区"}
]
sample_file = "/tmp/sample_data.csv"
with open(sample_file, 'w', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=["device_id", "data_type", "value", "location"])
writer.writeheader()
writer.writerows(sample_data)
# 导入文件
try:
result = await batch_manager.import_file(
file_path=sample_file,
batch_id="batch_" + datetime.now().strftime("%Y%m%d_%H%M%S"),
data_source="manual_test",
file_type="csv"
)
print("导入结果:", result)
except Exception as e:
print(f"导入失败: {str(e)}")
if __name__ == "__main__":
asyncio.run(example_usage())
+4
View File
@@ -0,0 +1,4 @@
"""
BI模块 - 自助BI看板和数据可视化
集成Superset/Metabase功能
"""
+273
View File
@@ -0,0 +1,273 @@
"""
BI控制器模块
提供REST API接口,支持自助BI看板和数据可视化
"""
from fastapi import APIRouter, HTTPException, Depends, Query, Path
from fastapi.responses import JSONResponse
from typing import List, Optional, Dict, Any
from datetime import datetime
from .services import BIService
from .models import ChartType, DataSourceType
# 创建路由器
router = APIRouter(prefix="/bi", tags=["BI"])
# 创建BI服务实例
bi_service = BIService()
@router.get("/")
async def get_bi_info():
"""获取BI系统信息"""
return {
"message": "Water Management System BI API",
"version": "1.0.0",
"endpoints": {
"charts": "/bi/charts",
"dashboards": "/bi/dashboards",
"data_sources": "/bi/data-sources",
"datasets": "/bi/datasets"
}
}
# 图表相关接口
@router.get("/charts", response_model=List[Dict[str, Any]])
async def get_all_charts():
"""获取所有图表"""
charts = bi_service.get_all_charts()
return [chart.to_dict() for chart in charts]
@router.get("/charts/{chart_id}", response_model=Dict[str, Any])
async def get_chart(chart_id: str = Path(..., description="图表ID")):
"""获取单个图表"""
chart = bi_service.get_chart(chart_id)
if not chart:
raise HTTPException(status_code=404, detail="Chart not found")
return chart.to_dict()
@router.post("/charts", response_model=Dict[str, Any])
async def create_chart(chart_data: Dict[str, Any]):
"""创建图表"""
try:
chart = bi_service.create_chart(chart_data)
return chart.to_dict()
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@router.put("/charts/{chart_id}", response_model=Dict[str, Any])
async def update_chart(chart_id: str = Path(..., description="图表ID"), chart_data: Dict[str, Any] = None):
"""更新图表"""
if not chart_data:
raise HTTPException(status_code=400, detail="Chart data is required")
chart = bi_service.update_chart(chart_id, chart_data)
if not chart:
raise HTTPException(status_code=404, detail="Chart not found")
return chart.to_dict()
@router.delete("/charts/{chart_id}")
async def delete_chart(chart_id: str = Path(..., description="图表ID")):
"""删除图表"""
success = bi_service.delete_chart(chart_id)
if not success:
raise HTTPException(status_code=404, detail="Chart not found")
return {"message": "Chart deleted successfully"}
@router.get("/charts/{chart_id}/data", response_model=Dict[str, Any])
async def get_chart_data(chart_id: str = Path(..., description="图表ID")):
"""获取图表数据"""
data = bi_service.get_chart_data_api(chart_id)
if "error" in data:
raise HTTPException(status_code=404, detail=data["error"])
return data
# 看板相关接口
@router.get("/dashboards", response_model=List[Dict[str, Any]])
async def get_all_dashboards():
"""获取所有看板"""
dashboards = bi_service.get_all_dashboards()
return [dashboard.to_dict() for dashboard in dashboards]
@router.get("/dashboards/{dashboard_id}", response_model=Dict[str, Any])
async def get_dashboard(dashboard_id: str = Path(..., description="看板ID")):
"""获取单个看板"""
dashboard = bi_service.get_dashboard(dashboard_id)
if not dashboard:
raise HTTPException(status_code=404, detail="Dashboard not found")
return dashboard.to_dict()
@router.post("/dashboards", response_model=Dict[str, Any])
async def create_dashboard(dashboard_data: Dict[str, Any]):
"""创建看板"""
try:
dashboard = bi_service.create_dashboard(dashboard_data)
return dashboard.to_dict()
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@router.put("/dashboards/{dashboard_id}", response_model=Dict[str, Any])
async def update_dashboard(dashboard_id: str = Path(..., description="看板ID"), dashboard_data: Dict[str, Any] = None):
"""更新看板"""
if not dashboard_data:
raise HTTPException(status_code=400, detail="Dashboard data is required")
dashboard = bi_service.update_dashboard(dashboard_id, dashboard_data)
if not dashboard:
raise HTTPException(status_code=404, detail="Dashboard not found")
return dashboard.to_dict()
@router.delete("/dashboards/{dashboard_id}")
async def delete_dashboard(dashboard_id: str = Path(..., description="看板ID")):
"""删除看板"""
success = bi_service.delete_dashboard(dashboard_id)
if not success:
raise HTTPException(status_code=404, detail="Dashboard not found")
return {"message": "Dashboard deleted successfully"}
@router.get("/dashboards/{dashboard_id}/data", response_model=Dict[str, Any])
async def get_dashboard_data(dashboard_id: str = Path(..., description="看板ID")):
"""获取看板数据"""
data = bi_service.get_dashboard_data_api(dashboard_id)
if "error" in data:
raise HTTPException(status_code=404, detail=data["error"])
return data
# 数据源相关接口
@router.get("/data-sources", response_model=List[Dict[str, Any]])
async def get_all_data_sources():
"""获取所有数据源"""
data_sources = bi_service.get_all_data_sources()
return [source.to_dict() for source in data_sources]
@router.get("/data-sources/{source_id}", response_model=Dict[str, Any])
async def get_data_source(source_id: str = Path(..., description="数据源ID")):
"""获取单个数据源"""
source = bi_service.get_data_source(source_id)
if not source:
raise HTTPException(status_code=404, detail="Data source not found")
return source.to_dict()
# 数据集相关接口
@router.get("/datasets", response_model=List[Dict[str, Any]])
async def get_all_datasets():
"""获取所有数据集"""
datasets = bi_service.get_all_datasets()
return [dataset.to_dict() for dataset in datasets]
@router.get("/datasets/{dataset_id}", response_model=Dict[str, Any])
async def get_dataset(dataset_id: str = Path(..., description="数据集ID")):
"""获取单个数据集"""
dataset = bi_service.get_dataset(dataset_id)
if not dataset:
raise HTTPException(status_code=404, detail="Dataset not found")
return dataset.to_dict()
# 搜索相关接口
@router.get("/search/charts")
async def search_charts(keyword: str = Query(..., description="搜索关键词")):
"""搜索图表"""
charts = bi_service.search_charts(keyword)
return [chart.to_dict() for chart in charts]
@router.get("/search/dashboards")
async def search_dashboards(keyword: str = Query(..., description="搜索关键词")):
"""搜索看板"""
dashboards = bi_service.search_dashboards(keyword)
return [dashboard.to_dict() for dashboard in dashboards]
# 标签相关接口
@router.get("/charts/tag/{tag}")
async def get_charts_by_tag(tag: str = Path(..., description="标签")):
"""根据标签获取图表"""
charts = bi_service.get_charts_by_tag(tag)
return [chart.to_dict() for chart in charts]
@router.get("/dashboards/tag/{tag}")
async def get_dashboards_by_tag(tag: str = Path(..., description="标签")):
"""根据标签获取看板"""
dashboards = bi_service.get_dashboards_by_tag(tag)
return [dashboard.to_dict() for dashboard in dashboards]
# 公开看板接口
@router.get("/public/dashboards")
async def get_public_dashboards():
"""获取公开看板"""
dashboards = bi_service.get_public_dashboards()
return [dashboard.to_dict() for dashboard in dashboards]
# 图表类型和枚举
@router.get("/types/chart-types")
async def get_chart_types():
"""获取支持的图表类型"""
return [{"value": ct.value, "label": ct.name} for ct in ChartType]
@router.get("/types/data-source-types")
async def get_data_source_types():
"""获取支持的数据源类型"""
return [{"value": dst.value, "label": dst.name} for dst in DataSourceType]
# 默认数据接口
@router.get("/default-charts")
async def get_default_charts():
"""获取默认图表"""
# 运营总览相关的图表
default_chart_ids = ["flow_trend", "device_status_distribution", "alert_level_stats", "system_performance"]
charts = [bi_service.get_chart(chart_id).to_dict() for chart_id in default_chart_ids if bi_service.get_chart(chart_id)]
return charts
@router.get("/default-dashboards")
async def get_default_dashboards():
"""获取默认看板"""
# 运营总览看板
default_dashboard_ids = ["operation_overview", "device_management", "security_monitoring"]
dashboards = [bi_service.get_dashboard(db_id).to_dict() for db_id in default_dashboard_ids if bi_service.get_dashboard(db_id)]
return dashboards
# Superset集成接口
@router.post("/integrations/superset")
async def setup_superset_integration(integration_data: Dict[str, Any]):
"""设置Superset集成"""
try:
integration = bi_service.setup_superset_integration(integration_data)
return integration.to_dict()
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@router.get("/integrations/superset")
async def get_superset_integration():
"""获取Superset集成配置"""
if not bi_service.superset_integration:
raise HTTPException(status_code=404, detail="Superset integration not configured")
return bi_service.superset_integration.to_dict()
# Metabase集成接口
@router.post("/integrations/metabase")
async def setup_metabase_integration(integration_data: Dict[str, Any]):
"""设置Metabase集成"""
try:
integration = bi_service.setup_metabase_integration(integration_data)
return integration.to_dict()
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@router.get("/integrations/metabase")
async def get_metabase_integration():
"""获取Metabase集成配置"""
if not bi_service.metabase_integration:
raise HTTPException(status_code=404, detail="Metabase integration not configured")
return bi_service.metabase_integration.to_dict()
# 统计信息接口
@router.get("/stats")
async def get_bi_stats():
"""获取BI系统统计信息"""
return {
"total_charts": len(bi_service.charts),
"total_dashboards": len(bi_service.dashboards),
"total_data_sources": len(bi_service.data_sources),
"total_datasets": len(bi_service.datasets),
"public_dashboards": len(bi_service.get_public_dashboards()),
"integrations": {
"superset_configured": bi_service.superset_integration is not None,
"metabase_configured": bi_service.metabase_integration is not None
}
}
+321
View File
@@ -0,0 +1,321 @@
"""
BI数据模型定义
定义自助BI看板和可视化相关数据结构
"""
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Any
from datetime import datetime
from enum import Enum
class ChartType(Enum):
"""图表类型枚举"""
LINE = "line" # 折线图
BAR = "bar" # 柱状图
PIE = "pie" # 饼图
SCATTER = "scatter" # 散点图
AREA = "area" # 面积图
GAUGE = "gauge" # 仪表盘
TABLE = "table" # 表格
HEATMAP = "heatmap" # 热力图
class DataSourceType(Enum):
"""数据源类型枚举"""
SENSOR_DATA = "sensor_data" # 传感器数据
DEVICE_DATA = "device_data" # 设备数据
ALERT_DATA = "alert_data" # 警报数据
BATCH_DATA = "batch_data" # 批量导入数据
SYSTEM_STATS = "system_stats" # 系统统计数据
@dataclass
class Chart:
"""图表模型"""
id: str
name: str
description: str
chart_type: ChartType
data_source: DataSourceType
x_axis: str # X轴字段
y_axis: List[str] # Y轴字段列表
filters: Dict[str, Any] = field(default_factory=dict)
group_by: List[str] = field(default_factory=list)
aggregation: str = "sum" # sum, avg, max, min, count
time_range: Optional[Dict[str, datetime]] = None
options: Dict[str, Any] = field(default_factory=dict)
created_at: datetime = field(default_factory=datetime.now)
updated_at: datetime = field(default_factory=datetime.now)
created_by: str = "system"
is_public: bool = False
tags: List[str] = field(default_factory=list)
def to_dict(self) -> Dict[str, Any]:
"""转换为字典"""
return {
"id": self.id,
"name": self.name,
"description": self.description,
"chart_type": self.chart_type.value,
"data_source": self.data_source.value,
"x_axis": self.x_axis,
"y_axis": self.y_axis,
"filters": self.filters,
"group_by": self.group_by,
"aggregation": self.aggregation,
"time_range": {
"start": self.time_range["start"].isoformat() if self.time_range and "start" in self.time_range else None,
"end": self.time_range["end"].isoformat() if self.time_range and "end" in self.time_range else None
} if self.time_range else None,
"options": self.options,
"created_at": self.created_at.isoformat(),
"updated_at": self.updated_at.isoformat(),
"created_by": self.created_by,
"is_public": self.is_public,
"tags": self.tags
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'Chart':
"""从字典创建对象"""
time_range = None
if data.get("time_range"):
tr = data["time_range"]
time_range = {
"start": datetime.fromisoformat(tr["start"]) if tr.get("start") else None,
"end": datetime.fromisoformat(tr["end"]) if tr.get("end") else None
}
return cls(
id=data["id"],
name=data["name"],
description=data["description"],
chart_type=ChartType(data["chart_type"]),
data_source=DataSourceType(data["data_source"]),
x_axis=data["x_axis"],
y_axis=data["y_axis"],
filters=data.get("filters", {}),
group_by=data.get("group_by", []),
aggregation=data.get("aggregation", "sum"),
time_range=time_range,
options=data.get("options", {}),
created_at=datetime.fromisoformat(data["created_at"]),
updated_at=datetime.fromisoformat(data["updated_at"]),
created_by=data.get("created_by", "system"),
is_public=data.get("is_public", False),
tags=data.get("tags", [])
)
@dataclass
class Dashboard:
"""看板模型"""
id: str
name: str
description: str
charts: List[str] # 图表ID列表
layout: List[Dict[str, Any]] = field(default_factory=list) # 布局配置
filters: Dict[str, Any] = field(default_factory=dict)
shared_users: List[str] = field(default_factory=list)
shared_groups: List[str] = field(default_factory=list)
is_public: bool = False
created_at: datetime = field(default_factory=datetime.now)
updated_at: datetime = field(default_factory=datetime.now)
created_by: str = "system"
tags: List[str] = field(default_factory=list)
def to_dict(self) -> Dict[str, Any]:
"""转换为字典"""
return {
"id": self.id,
"name": self.name,
"description": self.description,
"charts": self.charts,
"layout": self.layout,
"filters": self.filters,
"shared_users": self.shared_users,
"shared_groups": self.shared_groups,
"is_public": self.is_public,
"created_at": self.created_at.isoformat(),
"updated_at": self.updated_at.isoformat(),
"created_by": self.created_by,
"tags": self.tags
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'Dashboard':
"""从字典创建对象"""
return cls(
id=data["id"],
name=data["name"],
description=data["description"],
charts=data.get("charts", []),
layout=data.get("layout", []),
filters=data.get("filters", {}),
shared_users=data.get("shared_users", []),
shared_groups=data.get("shared_groups", []),
is_public=data.get("is_public", False),
created_at=datetime.fromisoformat(data["created_at"]),
updated_at=datetime.fromisoformat(data["updated_at"]),
created_by=data.get("created_by", "system"),
tags=data.get("tags", [])
)
@dataclass
class DataSource:
"""数据源模型"""
id: str
name: str
type: DataSourceType
description: str
config: Dict[str, Any] = field(default_factory=dict)
query_template: str = ""
columns: List[str] = field(default_factory=list)
refresh_interval_minutes: int = 60
is_active: bool = True
created_at: datetime = field(default_factory=datetime.now)
updated_at: datetime = field(default_factory=datetime.now)
created_by: str = "system"
def to_dict(self) -> Dict[str, Any]:
"""转换为字典"""
return {
"id": self.id,
"name": self.name,
"type": self.type.value,
"description": self.description,
"config": self.config,
"query_template": self.query_template,
"columns": self.columns,
"refresh_interval_minutes": self.refresh_interval_minutes,
"is_active": self.is_active,
"created_at": self.created_at.isoformat(),
"updated_at": self.updated_at.isoformat(),
"created_by": self.created_by
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'DataSource':
"""从字典创建对象"""
return cls(
id=data["id"],
name=data["name"],
type=DataSourceType(data["type"]),
description=data["description"],
config=data.get("config", {}),
query_template=data.get("query_template", ""),
columns=data.get("columns", []),
refresh_interval_minutes=data.get("refresh_interval_minutes", 60),
is_active=data.get("is_active", True),
created_at=datetime.fromisoformat(data["created_at"]),
updated_at=datetime.fromisoformat(data["updated_at"]),
created_by=data.get("created_by", "system")
)
@dataclass
class Dataset:
"""数据集模型"""
id: str
name: str
description: str
data_source_id: str
query: str
columns: List[Dict[str, Any]] = field(default_factory=list) # 字段定义
transformations: List[str] = field(default_factory=list) # 数据转换规则
cache_enabled: bool = True
cache_timeout_minutes: int = 30
is_active: bool = True
created_at: datetime = field(default_factory=datetime.now)
updated_at: datetime = field(default_factory=datetime.now)
created_by: str = "system"
def to_dict(self) -> Dict[str, Any]:
"""转换为字典"""
return {
"id": self.id,
"name": self.name,
"description": self.description,
"data_source_id": self.data_source_id,
"query": self.query,
"columns": self.columns,
"transformations": self.transformations,
"cache_enabled": self.cache_enabled,
"cache_timeout_minutes": self.cache_timeout_minutes,
"is_active": self.is_active,
"created_at": self.created_at.isoformat(),
"updated_at": self.updated_at.isoformat(),
"created_by": self.created_by
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'Dataset':
"""从字典创建对象"""
return cls(
id=data["id"],
name=data["name"],
description=data["description"],
data_source_id=data["data_source_id"],
query=data["query"],
columns=data.get("columns", []),
transformations=data.get("transformations", []),
cache_enabled=data.get("cache_enabled", True),
cache_timeout_minutes=data.get("cache_timeout_minutes", 30),
is_active=data.get("is_active", True),
created_at=datetime.fromisoformat(data["created_at"]),
updated_at=datetime.fromisoformat(data["updated_at"]),
created_by=data.get("created_by", "system")
)
@dataclass
class SupersetIntegration:
"""Superset集成配置"""
superset_url: str
superset_api_key: str
superset_username: str
dashboard_mapping: Dict[str, str] = field(default_factory=dict) # 本地dashboard -> Superset dashboard
chart_mapping: Dict[str, str] = field(default_factory=dict) # 本地chart -> Superset chart
sync_enabled: bool = True
sync_interval_minutes: int = 60
last_sync_at: Optional[datetime] = None
sync_status: str = "idle" # idle, syncing, error
error_message: Optional[str] = None
def to_dict(self) -> Dict[str, Any]:
"""转换为字典"""
return {
"superset_url": self.superset_url,
"superset_api_key": self.superset_api_key,
"superset_username": self.superset_username,
"dashboard_mapping": self.dashboard_mapping,
"chart_mapping": self.chart_mapping,
"sync_enabled": self.sync_enabled,
"sync_interval_minutes": self.sync_interval_minutes,
"last_sync_at": self.last_sync_at.isoformat() if self.last_sync_at else None,
"sync_status": self.sync_status,
"error_message": self.error_message
}
@dataclass
class MetabaseIntegration:
"""Metabase集成配置"""
metabase_url: str
metabase_secret_key: str
collection_name: str = "水务管理系统"
dashboard_mapping: Dict[str, str] = field(default_factory=dict)
question_mapping: Dict[str, str] = field(default_factory=dict)
sync_enabled: bool = True
sync_interval_minutes: int = 60
last_sync_at: Optional[datetime] = None
sync_status: str = "idle"
error_message: Optional[str] = None
def to_dict(self) -> Dict[str, Any]:
"""转换为字典"""
return {
"metabase_url": self.metabase_url,
"metabase_secret_key": self.metabase_secret_key,
"collection_name": self.collection_name,
"dashboard_mapping": self.dashboard_mapping,
"question_mapping": self.question_mapping,
"sync_enabled": self.sync_enabled,
"sync_interval_minutes": self.sync_interval_minutes,
"last_sync_at": self.last_sync_at.isoformat() if self.last_sync_at else None,
"sync_status": self.sync_status,
"error_message": self.error_message
}
+562
View File
@@ -0,0 +1,562 @@
"""
BI服务模块
提供自助BI看板和数据可视化服务
包括数据集管理、图表创建、看板配置等功能
"""
from typing import Dict, List, Optional, Any, Tuple
from datetime import datetime, timedelta
import json
import pandas as pd
import numpy as np
from .models import (
Chart, ChartType, DataSourceType, Dashboard,
DataSource, Dataset, SupersetIntegration, MetabaseIntegration
)
class BIService:
"""BI服务主类"""
def __init__(self):
# 初始化数据存储
self.charts: Dict[str, Chart] = {}
self.dashboards: Dict[str, Dashboard] = {}
self.data_sources: Dict[str, DataSource] = {}
self.datasets: Dict[str, Dataset] = {}
self.superset_integration: Optional[SupersetIntegration] = None
self.metabase_integration: Optional[MetabaseIntegration] = None
# 初始化默认数据源
self._init_default_data_sources()
self._init_default_charts()
self._init_default_dashboards()
def _init_default_data_sources(self):
"""初始化默认数据源"""
# 传感器数据源
sensor_source = DataSource(
id="sensor_data",
name="传感器数据",
type=DataSourceType.SENSOR_DATA,
description="所有IoT传感器实时和历史数据",
config={
"time_field": "timestamp",
"value_field": "value",
"device_field": "device_id",
"location_field": "location",
"type_field": "data_type"
},
query_template="SELECT * FROM sensor_data WHERE {filters}",
columns=[
{"name": "id", "type": "integer", "description": "数据记录ID"},
{"name": "device_id", "type": "string", "description": "设备ID"},
{"name": "data_type", "type": "string", "description": "数据类型"},
{"name": "value", "type": "float", "description": "数值"},
{"name": "unit", "type": "string", "description": "单位"},
{"name": "timestamp", "type": "datetime", "description": "时间戳"},
{"name": "location", "type": "string", "description": "位置"},
{"name": "quality_score", "type": "float", "description": "质量评分"}
]
)
self.data_sources[sensor_source.id] = sensor_source
# 设备状态数据源
device_source = DataSource(
id="device_data",
name="设备状态",
type=DataSourceType.DEVICE_DATA,
description="所有设备的运行状态和配置信息",
config={
"status_field": "status",
"type_field": "device_type",
"location_field": "location"
},
query_template="SELECT * FROM device WHERE {filters}",
columns=[
{"name": "id", "type": "string", "description": "设备ID"},
{"name": "name", "type": "string", "description": "设备名称"},
{"name": "device_type", "type": "string", "description": "设备类型"},
{"name": "location", "type": "string", "description": "位置"},
{"name": "status", "type": "string", "description": "状态"},
{"name": "install_date", "type": "datetime", "description": "安装日期"},
{"name": "metadata", "type": "json", "description": "元数据"}
]
)
self.data_sources[device_source.id] = device_source
# 警报数据源
alert_source = DataSource(
id="alert_data",
name="警报数据",
type=DataSourceType.ALERT_DATA,
description="系统警报和通知记录",
config={
"level_field": "level",
"type_field": "alert_type",
"resolved_field": "resolved"
},
query_template="SELECT * FROM alert WHERE {filters}",
columns=[
{"name": "id", "type": "string", "description": "警报ID"},
{"name": "device_id", "type": "string", "description": "设备ID"},
{"name": "alert_type", "type": "string", "description": "警报类型"},
{"name": "level", "type": "string", "description": "警报级别"},
{"name": "message", "type": "string", "description": "警报信息"},
{"name": "timestamp", "type": "datetime", "description": "发生时间"},
{"name": "resolved", "type": "boolean", "description": "是否已解决"},
{"name": "resolved_at", "type": "datetime", "description": "解决时间"}
]
)
self.data_sources[alert_source.id] = alert_source
# 系统统计数据源
stats_source = DataSource(
id="system_stats",
name="系统统计",
type=DataSourceType.SYSTEM_STATS,
description="系统运行性能和使用统计",
config={
"cpu_field": "cpu_usage_percent",
"memory_field": "memory_usage_mb",
"records_field": "total_records"
},
query_template="SELECT * FROM system_stats WHERE {filters}",
columns=[
{"name": "timestamp", "type": "datetime", "description": "统计时间"},
{"name": "total_records", "type": "integer", "description": "总记录数"},
{"name": "total_devices", "type": "integer", "description": "设备总数"},
{"name": "active_connections", "type": "integer", "description": "活跃连接数"},
{"name": "api_requests_count", "type": "integer", "description": "API请求数"},
{"name": "alerts_count", "type": "integer", "description": "警报数量"},
{"name": "data_quality_score", "type": "float", "description": "数据质量评分"},
{"name": "memory_usage_mb", "type": "float", "description": "内存使用量(MB)"},
{"name": "cpu_usage_percent", "type": "float", "description": "CPU使用率(%)"}
]
)
self.data_sources[stats_source.id] = stats_source
def _init_default_charts(self):
"""初始化默认图表"""
# 流量趋势图
flow_trend = Chart(
id="flow_trend",
name="流量趋势分析",
description="显示各区域流量随时间的变化趋势",
chart_type=ChartType.LINE,
data_source=DataSourceType.SENSOR_DATA,
x_axis="timestamp",
y_axis=["value"],
group_by=["location"],
aggregation="avg",
filters={"data_type": "LL"},
options={
"title": "各区域流量趋势",
"yAxis": {"title": "流量 (m³/h)"},
"xAxis": {"title": "时间"},
"legend": {"show": True},
"tooltip": {"trigger": "axis"}
},
tags=["流量", "趋势", "区域"]
)
self.charts[flow_trend.id] = flow_trend
# 设备状态分布图
device_status = Chart(
id="device_status_distribution",
name="设备状态分布",
description="显示不同状态设备的数量分布",
chart_type=ChartType.PIE,
data_source=DataSourceType.DEVICE_DATA,
x_axis="status",
y_axis=["count"],
aggregation="count",
options={
"title": "设备状态分布",
"legend": {"show": True},
"tooltip": {"trigger": "item"}
},
tags=["设备", "状态", "分布"]
)
self.charts[device_status.id] = device_status
# 警报级别统计图
alert_stats = Chart(
id="alert_level_stats",
name="警报级别统计",
description="按级别统计警报数量",
chart_type=ChartType.BAR,
data_source=DataSourceType.ALERT_DATA,
x_axis="level",
y_axis=["count"],
aggregation="count",
filters={"resolved": False},
options={
"title": "未解决警报按级别统计",
"yAxis": {"title": "数量"},
"xAxis": {"title": "警报级别"},
"legend": {"show": False}
},
tags=["警报", "级别", "统计"]
)
self.charts[alert_stats.id] = alert_stats
# 系统性能监控图
system_performance = Chart(
id="system_performance",
name="系统性能监控",
description="显示系统CPU和内存使用率趋势",
chart_type=ChartType.LINE,
data_source=DataSourceType.SYSTEM_STATS,
x_axis="timestamp",
y_axis=["cpu_usage_percent", "memory_usage_mb"],
options={
"title": "系统性能监控",
"yAxis": [{"title": "CPU使用率(%)"}, {"title": "内存使用量(MB)"}],
"xAxis": {"title": "时间"},
"legend": {"show": True},
"tooltip": {"trigger": "axis"}
},
tags=["系统", "性能", "监控"]
)
self.charts[system_performance.id] = system_performance
def _init_default_dashboards(self):
"""初始化默认看板"""
# 水务运营总览看板
overview_dashboard = Dashboard(
id="operation_overview",
name="水务运营总览",
description="水务系统整体运营情况综合看板",
charts=["flow_trend", "device_status_distribution", "alert_level_stats", "system_performance"],
layout=[
{"i": "flow_trend", "x": 0, "y": 0, "w": 12, "h": 8},
{"i": "device_status_distribution", "x": 12, "y": 0, "w": 6, "h": 6},
{"i": "alert_level_stats", "x": 18, "y": 0, "w": 6, "h": 6},
{"i": "system_performance", "x": 0, "y": 8, "w": 24, "h": 8}
],
is_public=True,
tags=["运营", "总览", "综合"]
)
self.dashboards[overview_dashboard.id] = overview_dashboard
# 设备管理看板
device_dashboard = Dashboard(
id="device_management",
name="设备管理看板",
description="设备状态监控和维护管理",
charts=["device_status_distribution"],
layout=[
{"i": "device_status_distribution", "x": 0, "y": 0, "w": 12, "h": 8}
],
tags=["设备", "管理", "监控"]
)
self.dashboards[device_dashboard.id] = device_dashboard
# 安全监控看板
security_dashboard = Dashboard(
id="security_monitoring",
name="安全监控看板",
description="系统安全和警报监控",
charts=["alert_level_stats"],
layout=[
{"i": "alert_level_stats", "x": 0, "y": 0, "w": 12, "h": 8}
],
tags=["安全", "监控", "警报"]
)
self.dashboards[security_dashboard.id] = security_dashboard
def get_chart(self, chart_id: str) -> Optional[Chart]:
"""获取图表"""
return self.charts.get(chart_id)
def get_all_charts(self) -> List[Chart]:
"""获取所有图表"""
return list(self.charts.values())
def create_chart(self, chart_data: Dict[str, Any]) -> Chart:
"""创建图表"""
chart = Chart.from_dict(chart_data)
self.charts[chart.id] = chart
return chart
def update_chart(self, chart_id: str, chart_data: Dict[str, Any]) -> Optional[Chart]:
"""更新图表"""
if chart_id in self.charts:
chart = Chart.from_dict(chart_data)
chart.id = chart_id # 保持ID不变
self.charts[chart_id] = chart
return chart
return None
def delete_chart(self, chart_id: str) -> bool:
"""删除图表"""
if chart_id in self.charts:
del self.charts[chart_id]
# 从所有看板中移除该图表
for dashboard in self.dashboards.values():
if chart_id in dashboard.charts:
dashboard.charts.remove(chart_id)
return True
return False
def get_dashboard(self, dashboard_id: str) -> Optional[Dashboard]:
"""获取看板"""
return self.dashboards.get(dashboard_id)
def get_all_dashboards(self) -> List[Dashboard]:
"""获取所有看板"""
return list(self.dashboards.values())
def create_dashboard(self, dashboard_data: Dict[str, Any]) -> Dashboard:
"""创建看板"""
dashboard = Dashboard.from_dict(dashboard_data)
self.dashboards[dashboard.id] = dashboard
return dashboard
def update_dashboard(self, dashboard_id: str, dashboard_data: Dict[str, Any]) -> Optional[Dashboard]:
"""更新看板"""
if dashboard_id in self.dashboards:
dashboard = Dashboard.from_dict(dashboard_data)
dashboard.id = dashboard_id # 保持ID不变
self.dashboards[dashboard_id] = dashboard
return dashboard
return None
def delete_dashboard(self, dashboard_id: str) -> bool:
"""删除看板"""
if dashboard_id in self.dashboards:
del self.dashboards[dashboard_id]
return True
return False
def get_data_source(self, source_id: str) -> Optional[DataSource]:
"""获取数据源"""
return self.data_sources.get(source_id)
def get_all_data_sources(self) -> List[DataSource]:
"""获取所有数据源"""
return list(self.data_sources.values())
def get_dataset(self, dataset_id: str) -> Optional[Dataset]:
"""获取数据集"""
return self.datasets.get(dataset_id)
def get_all_datasets(self) -> List[Dataset]:
"""获取所有数据集"""
return list(self.datasets.values())
def execute_chart_data(self, chart_id: str) -> Dict[str, Any]:
"""执行图表数据查询"""
chart = self.get_chart(chart_id)
if not chart:
return {"error": "Chart not found"}
# 这里模拟数据查询,实际应该连接到数据库或数据源
data = self._generate_chart_data(chart)
return {
"chart_id": chart_id,
"chart_name": chart.name,
"data": data,
"columns": chart.options.get("columns", []),
"chart_type": chart.chart_type.value,
"options": chart.options
}
def _generate_chart_data(self, chart: Chart) -> List[Dict[str, Any]]:
"""生成图表数据(模拟)"""
# 根据图表类型和数据源生成模拟数据
if chart.data_source == DataSourceType.SENSOR_DATA:
return self._generate_sensor_data(chart)
elif chart.data_source == DataSourceType.DEVICE_DATA:
return self._generate_device_data(chart)
elif chart.data_source == DataSourceType.ALERT_DATA:
return self._generate_alert_data(chart)
elif chart.data_source == DataSourceType.SYSTEM_STATS:
return self._generate_stats_data(chart)
else:
return []
def _generate_sensor_data(self, chart: Chart) -> List[Dict[str, Any]]:
"""生成传感器数据"""
data = []
# 生成时间序列数据
base_time = datetime.now() - timedelta(days=7)
locations = ["A区", "B区", "C区", "D区"]
for i in range(24 * 7): # 7天,每小时一个点
timestamp = base_time + timedelta(hours=i)
for location in locations:
# 添加一些随机波动
base_value = 50 if chart.filters.get("data_type") == "LL" else 1.0
value = base_value + np.random.normal(0, 10)
data.append({
"timestamp": timestamp.isoformat(),
"location": location,
"value": round(value, 2),
"device_id": f"device_{hash(location) % 10 + 1}",
"data_type": chart.filters.get("data_type", "LL"),
"quality_score": round(np.random.uniform(0.8, 1.0), 2)
})
# 应用过滤和聚合
if chart.group_by:
# 简单的分组聚合
grouped_data = {}
for item in data:
key = tuple(item.get(field) for field in chart.group_by)
if key not in grouped_data:
grouped_data[key] = []
grouped_data[key].append(item)
result = []
for key, items in grouped_data.items():
group_data = {}
for i, field in enumerate(chart.group_by):
group_data[field] = key[i]
# 聚合计算
values = [item["value"] for item in items]
if chart.aggregation == "avg":
group_data["value"] = sum(values) / len(values)
elif chart.aggregation == "sum":
group_data["value"] = sum(values)
elif chart.aggregation == "max":
group_data["value"] = max(values)
elif chart.aggregation == "min":
group_data["value"] = min(values)
else:
group_data["value"] = sum(values) / len(values)
result.append(group_data)
return result
return data
def _generate_device_data(self, chart: Chart) -> List[Dict[str, Any]]:
"""生成设备数据"""
devices = [
{"id": "device_1", "name": "流量计-001", "device_type": "流量计", "location": "A区", "status": "active"},
{"id": "device_2", "name": "压力计-001", "device_type": "压力计", "location": "A区", "status": "active"},
{"id": "device_3", "name": "水位计-001", "device_type": "水位计", "location": "B区", "status": "maintenance"},
{"id": "device_4", "name": "浊度计-001", "device_type": "浊度计", "location": "B区", "status": "active"},
{"id": "device_5", "name": "pH计-001", "device_type": "pH计", "location": "C区", "status": "inactive"},
]
# 按状态分组
status_groups = {}
for device in devices:
status = device["status"]
if status not in status_groups:
status_groups[status] = []
status_groups[status].append(device)
# 生成统计数据
result = []
for status, devices_in_status in status_groups.items():
result.append({
"status": status,
"count": len(devices_in_status),
"devices": [d["name"] for d in devices_in_status]
})
return result
def _generate_alert_data(self, chart: Chart) -> List[Dict[str, Any]]:
"""生成警报数据"""
alerts = [
{"level": "info", "count": 5, "description": "信息级别警报"},
{"level": "warning", "count": 3, "description": "警告级别警报"},
{"level": "error", "count": 1, "description": "错误级别警报"},
{"level": "critical", "count": 0, "description": "严重级别警报"},
]
return alerts
def _generate_stats_data(self, chart: Chart) -> List[Dict[str, Any]]:
"""生成系统统计数据"""
data = []
base_time = datetime.now() - timedelta(days=1)
for i in range(24): # 24小时数据
timestamp = base_time + timedelta(hours=i)
data.append({
"timestamp": timestamp.isoformat(),
"total_records": 1000 + np.random.randint(-100, 100),
"total_devices": 25 + np.random.randint(-5, 5),
"active_connections": 5 + np.random.randint(-2, 3),
"api_requests_count": 150 + np.random.randint(-30, 30),
"alerts_count": np.random.randint(0, 5),
"data_quality_score": round(np.random.uniform(0.9, 1.0), 2),
"memory_usage_mb": 100 + np.random.randint(-20, 20),
"cpu_usage_percent": 30 + np.random.randint(-10, 10)
})
return data
def setup_superset_integration(self, integration_data: Dict[str, Any]) -> SupersetIntegration:
"""设置Superset集成"""
integration = SupersetIntegration.from_dict(integration_data)
self.superset_integration = integration
return integration
def setup_metabase_integration(self, integration_data: Dict[str, Any]) -> MetabaseIntegration:
"""设置Metabase集成"""
integration = MetabaseIntegration.from_dict(integration_data)
self.metabase_integration = integration
return integration
def get_chart_data_api(self, chart_id: str) -> Dict[str, Any]:
"""获取图表数据API接口"""
return self.execute_chart_data(chart_id)
def get_dashboard_data_api(self, dashboard_id: str) -> Dict[str, Any]:
"""获取看板数据API接口"""
dashboard = self.get_dashboard(dashboard_id)
if not dashboard:
return {"error": "Dashboard not found"}
charts_data = {}
for chart_id in dashboard.charts:
charts_data[chart_id] = self.get_chart_data_api(chart_id)
return {
"dashboard_id": dashboard_id,
"dashboard_name": dashboard.name,
"charts": charts_data,
"layout": dashboard.layout
}
def get_public_dashboards(self) -> List[Dashboard]:
"""获取公开看板"""
return [db for db in self.dashboards.values() if db.is_public]
def get_charts_by_tag(self, tag: str) -> List[Chart]:
"""根据标签获取图表"""
return [chart for chart in self.charts.values() if tag in chart.tags]
def get_dashboards_by_tag(self, tag: str) -> List[Dashboard]:
"""根据标签获取看板"""
return [dashboard for dashboard in self.dashboards.values() if tag in dashboard.tags]
def search_charts(self, keyword: str) -> List[Chart]:
"""搜索图表"""
keyword = keyword.lower()
return [
chart for chart in self.charts.values()
if keyword in chart.name.lower() or keyword in chart.description.lower()
or any(keyword in tag.lower() for tag in chart.tags)
]
def search_dashboards(self, keyword: str) -> List[Dashboard]:
"""搜索看板"""
keyword = keyword.lower()
return [
dashboard for dashboard in self.dashboards.values()
if keyword in dashboard.name.lower() or keyword in dashboard.description.lower()
or any(keyword in tag.lower() for tag in dashboard.tags)
]
+9
View File
@@ -0,0 +1,9 @@
"""
IoT Module - 物联网平台核心模块
包含MQTT协议适配器、设备注册/发现API、统一设备模型等
"""
from .device_manager import DeviceManager
from .device_controller import DeviceController
__all__ = ['DeviceManager', 'DeviceController']
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+177
View File
@@ -0,0 +1,177 @@
"""
IoT模块Flask应用
集成MQTT适配器、设备管理器、OTA管理等核心组件
"""
import logging
import os
from flask import Flask
from .device_manager import DeviceManager
from .mqtt_adapter import MqttAdapter
from .device_controller import DeviceController
from .ota_manager import OtaManager
from .ota_controller import OtaController
from .config import MqttConfig, DatabaseConfig
def create_iot_app(config=None):
"""
创建IoT模块Flask应用
Args:
config: 配置字典
Returns:
Flask: Flask应用实例
"""
app = Flask(__name__)
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
# 加载配置
if config is None:
config = {
'mqtt': MqttConfig.from_env(),
'database': DatabaseConfig.from_env()
}
# 初始化组件
device_manager = DeviceManager()
mqtt_adapter = MqttAdapter(
broker_host=config['mqtt'].broker_host,
broker_port=config['mqtt'].broker_port,
username=config['mqtt'].username,
password=config['mqtt'].password,
client_id=config['mqtt'].client_id
)
ota_manager = OtaManager()
# 创建控制器
device_controller = DeviceController(device_manager, mqtt_adapter)
ota_controller = OtaController(ota_manager)
# 注册蓝图
app.register_blueprint(device_controller.get_blueprint())
app.register_blueprint(ota_controller.get_blueprint())
@app.route('/api/iot/status')
def get_status():
"""获取IoT模块状态"""
return {
"mqtt": mqtt_adapter.get_connection_status(),
"device_statistics": device_manager.get_device_statistics(),
"ota_statistics": ota_manager.get_update_statistics(),
"devices_count": len(device_manager.devices),
"shadows_count": len(device_manager.shadows)
}
@app.route('/api/iot/mqtt/connect', methods=['POST'])
def connect_mqtt():
"""连接MQTT"""
try:
success = mqtt_adapter.connect()
return {
"success": success,
"message": "MQTT connected" if success else "MQTT connection failed"
}
except Exception as e:
logger.error(f"MQTT connection error: {e}")
return {"success": False, "error": str(e)}
@app.route('/api/iot/mqtt/disconnect', methods=['POST'])
def disconnect_mqtt():
"""断开MQTT连接"""
try:
mqtt_adapter.disconnect()
return {"success": True, "message": "MQTT disconnected"}
except Exception as e:
logger.error(f"MQTT disconnection error: {e}")
return {"success": False, "error": str(e)}
@app.route('/api/iot/mqtt/reconnect', methods=['POST'])
def reconnect_mqtt():
"""重连MQTT"""
try:
mqtt_adapter.disconnect()
success = mqtt_adapter.connect()
return {
"success": success,
"message": "MQTT reconnected" if success else "MQTT reconnection failed"
}
except Exception as e:
logger.error(f"MQTT reconnection error: {e}")
return {"success": False, "error": str(e)}
@app.route('/api/iot/mqtt/publish', methods=['POST'])
def publish_message():
"""发布MQTT消息"""
try:
data = request.get_json()
topic = data.get('topic')
payload = data.get('payload', {})
qos = data.get('qos', 0)
retain = data.get('retain', False)
success = mqtt_adapter.publish(topic, payload, qos, retain)
return {
"success": success,
"message": "Message published" if success else "Message publish failed"
}
except Exception as e:
logger.error(f"MQTT publish error: {e}")
return {"success": False, "error": str(e)}
@app.route('/api/iot/initialize', methods=['POST'])
def initialize_iot():
"""初始化IoT模块"""
try:
# 连接MQTT
mqtt_success = mqtt_adapter.connect()
# 订阅设备主题
mqtt_adapter.subscribe_device_topics(device_manager)
# 初始化一些示例设备
if len(device_manager.devices) == 0:
sample_devices = [
{
'device_sn': 'LL-001',
'device_type': 'flow_meter',
'name': '流量计-001',
'description': 'A区入口流量计',
'area': 'A区',
'position': '入口处',
'manufacturer': '华为',
'model': 'LL-100'
},
{
'device_sn': 'YL-001',
'device_type': 'pressure_meter',
'name': '压力表-001',
'description': 'B区主压力表',
'area': 'B区',
'position': '主管道',
'manufacturer': '西门子',
'model': 'YL-200'
}
]
for device_data in sample_devices:
device_manager.register_device(device_data)
return {
"success": True,
"mqtt_connected": mqtt_success,
"devices_count": len(device_manager.devices),
"message": "IoT module initialized successfully"
}
except Exception as e:
logger.error(f"IoT initialization error: {e}")
return {"success": False, "error": str(e)}
return app
+189
View File
@@ -0,0 +1,189 @@
"""
IoT模块配置
包含MQTT配置、数据库配置、设备类型映射等
"""
import os
from dataclasses import dataclass
@dataclass
class MqttConfig:
"""MQTT配置"""
broker_host: str = "localhost"
broker_port: int = 1883
username: str = ""
password: str = ""
client_id: str = "water-management-system"
keep_alive: int = 60
clean_session: bool = True
@classmethod
def from_env(cls):
"""从环境变量加载配置"""
return cls(
broker_host=os.getenv('MQTT_BROKER_HOST', 'localhost'),
broker_port=int(os.getenv('MQTT_BROKER_PORT', 1883)),
username=os.getenv('MQTT_USERNAME', ''),
password=os.getenv('MQTT_PASSWORD', ''),
client_id=os.getenv('MQTT_CLIENT_ID', 'water-management-system'),
keep_alive=int(os.getenv('MQTT_KEEP_ALIVE', 60)),
clean_session=os.getenv('MQTT_CLEAN_SESSION', 'true').lower() == 'true'
)
@dataclass
class DatabaseConfig:
"""数据库配置"""
host: str = "localhost"
port: int = 3306
database: str = "water_management"
username: str = "root"
password: str = ""
@classmethod
def from_env(cls):
"""从环境变量加载配置"""
return cls(
host=os.getenv('DB_HOST', 'localhost'),
port=int(os.getenv('DB_PORT', 3306)),
database=os.getenv('DB_NAME', 'water_management'),
username=os.getenv('DB_USER', 'root'),
password=os.getenv('DB_PASSWORD', '')
)
@dataclass
class DeviceTypeMapping:
"""设备类型映射"""
# 水利行业标准设备类型映射
MAPPINGS = {
# 流量计
'LL': 'flow_meter', # 流量计
'Q': 'flow_meter', # 流量
'QL': 'flow_meter', # 瞬时流量
# 压力表
'YL': 'pressure_meter', # 压力表
'P': 'pressure_meter', # 压力
'PL': 'pressure_meter', # 瞬时压力
# 水位计
'SW': 'level_meter', # 水位计
'L': 'level_meter', # 水位
'WL': 'level_meter', # 瞬时水位
# 水质仪
'ZD': 'quality_meter', # 浊度
'PH': 'quality_meter', # pH值
'DO': 'quality_meter', # 溶解氧
'COD': 'quality_meter', # 化学需氧量
'NH3': 'quality_meter', # 氨氮
'TEMP': 'quality_meter', # 温度
# 阀门
'FV': 'valve', # 阀门
'BV': 'valve', # 球阀
'GV': 'valve', # 闸阀
# 水泵
'PUMP': 'pump', # 水泵
'CP': 'pump', # 循环泵
'FP': 'pump', # 给水泵
# 传感器
'SENSOR': 'sensor', # 传感器
'TS': 'sensor', # 温度传感器
'HS': 'sensor', # 湿度传感器
# 摄像头
'CAM': 'camera', # 摄像头
'IPC': 'camera', # 网络摄像头
# 其他
'OTHER': 'other' # 其他设备
}
@classmethod
def map_device_type(cls, standard_type: str) -> str:
"""
将标准设备类型映射为内部类型
Args:
standard_type: 标准设备类型
Returns:
str: 内部设备类型
"""
return cls.MAPPINGS.get(standard_type.upper(), 'other')
@classmethod
def get_all_standard_types(cls) -> list:
"""获取所有标准设备类型"""
return list(cls.MAPPINGS.keys())
@dataclass
class UnitConversion:
"""单位转换配置"""
# 水利行业常用单位转换
CONVERSIONS = {
# 流量单位
'm³/h': {'m³/s': 1/3600, 'L/s': 1000/3600},
'm³/s': {'m³/h': 3600, 'L/s': 1000},
'L/s': {'m³/h': 3600/1000, 'm³/s': 1/1000},
# 压力单位
'MPa': {'kPa': 1000, 'Pa': 1000000},
'kPa': {'MPa': 1/1000, 'Pa': 1000},
'Pa': {'MPa': 1/1000000, 'kPa': 1/1000},
# 水位单位
'm': {'cm': 100, 'mm': 1000},
'cm': {'m': 1/100, 'mm': 10},
'mm': {'m': 1/1000, 'cm': 1/10},
# 水质单位
'NTU': {'': 1}, # 浊度无标准转换
'pH': {'': 1}, # pH值无标准转换
'mg/L': {'ppm': 1}, # mg/L 和 ppm 等价
'ppm': {'mg/L': 1}
}
@classmethod
def convert_unit(cls, value: float, from_unit: str, to_unit: str) -> float:
"""
单位转换
Args:
value: 数值
from_unit: 源单位
to_unit: 目标单位
Returns:
float: 转换后的数值
"""
if from_unit == to_unit:
return value
if from_unit in cls.CONVERSIONS and to_unit in cls.CONVERSIONS[from_unit]:
factor = cls.CONVERSIONS[from_unit][to_unit]
return value * factor
# 如果没有找到转换关系,尝试反向查找
for unit, conversions in cls.CONVERSIONS.items():
if to_unit in conversions and from_unit in conversions:
# 相同基准单位的转换
factor = conversions[from_unit] / conversions[to_unit]
return value * factor
# 无法转换,返回原值
return value
@classmethod
def get_supported_units(cls) -> list:
"""获取支持的所有单位"""
units = set()
for conversions in cls.CONVERSIONS.values():
units.update(conversions.keys())
return list(units)
+260
View File
@@ -0,0 +1,260 @@
"""
设备控制器
提供设备注册/发现的REST API接口
"""
import json
import logging
from datetime import datetime
from typing import Dict, Any, List, Optional
from flask import Blueprint, request, jsonify
from .device_manager import DeviceManager
from .models import DeviceType, DeviceStatus
class DeviceController:
"""设备控制器"""
def __init__(self, device_manager: DeviceManager):
"""
初始化设备控制器
Args:
device_manager: 设备管理器
"""
self.device_manager = device_manager
self.logger = logging.getLogger(__name__)
# 创建Blueprint
self.blueprint = Blueprint('device', __name__, url_prefix='/api/iot/device')
# 注册路由
self._register_routes()
def _register_routes(self):
"""注册API路由"""
@self.blueprint.route('/', methods=['GET'])
def list_devices():
"""获取设备列表"""
try:
# 获取查询参数
device_type_str = request.args.get('type')
status_str = request.args.get('status')
area = request.args.get('area')
page = int(request.args.get('page', 1))
per_page = int(request.args.get('per_page', 10))
# 过滤条件
device_type = DeviceType(device_type_str) if device_type_str else None
status = DeviceStatus(status_str) if status_str else None
# 获取设备列表
devices = self.device_manager.list_devices(device_type, status, area)
# 分页
total = len(devices)
start = (page - 1) * per_page
end = start + per_page
paginated_devices = devices[start:end]
# 转换为字典格式
device_list = [device.to_dict() for device in paginated_devices]
return jsonify({
"success": True,
"data": device_list,
"pagination": {
"page": page,
"per_page": per_page,
"total": total,
"pages": (total + per_page - 1) // per_page
}
})
except Exception as e:
self.logger.error(f"Error listing devices: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@self.blueprint.route('/<device_sn>', methods=['GET'])
def get_device(device_sn):
"""获取设备详情"""
try:
device = self.device_manager.get_device(device_sn)
if not device:
return jsonify({"success": False, "error": "Device not found"}), 404
# 获取设备影子
shadow = self.device_manager.get_device_shadow(device_sn)
device_data = device.to_dict()
if shadow:
device_data['shadow'] = shadow.to_dict()
return jsonify({
"success": True,
"data": device_data
})
except Exception as e:
self.logger.error(f"Error getting device {device_sn}: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@self.blueprint.route('/', methods=['POST'])
def register_device():
"""注册新设备"""
try:
device_data = request.get_json()
# 验证必要字段
required_fields = ['device_sn', 'device_type', 'name']
for field in required_fields:
if field not in device_data:
return jsonify({"success": False, "error": f"Missing required field: {field}"}), 400
# 检查设备是否已存在
existing_device = self.device_manager.get_device(device_data['device_sn'])
if existing_device:
return jsonify({"success": False, "error": "Device already exists"}), 409
# 注册设备
device = self.device_manager.register_device(device_data)
return jsonify({
"success": True,
"data": device.to_dict(),
"message": "Device registered successfully"
}), 201
except Exception as e:
self.logger.error(f"Error registering device: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@self.blueprint.route('/<device_sn>', methods=['PUT'])
def update_device(device_sn):
"""更新设备信息"""
try:
device = self.device_manager.get_device(device_sn)
if not device:
return jsonify({"success": False, "error": "Device not found"}), 404
updates = request.get_json()
# 更新设备
updated_device = self.device_manager.update_device(device_sn, updates)
if not updated_device:
return jsonify({"success": False, "error": "Failed to update device"}), 400
return jsonify({
"success": True,
"data": updated_device.to_dict(),
"message": "Device updated successfully"
})
except Exception as e:
self.logger.error(f"Error updating device {device_sn}: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@self.blueprint.route('/<device_sn>', methods=['DELETE'])
def delete_device(device_sn):
"""删除设备"""
try:
success = self.device_manager.delete_device(device_sn)
if not success:
return jsonify({"success": False, "error": "Device not found"}), 404
return jsonify({
"success": True,
"message": "Device deleted successfully"
})
except Exception as e:
self.logger.error(f"Error deleting device {device_sn}: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@self.blueprint.route('/<device_sn>/shadow', methods=['GET'])
def get_device_shadow(device_sn):
"""获取设备影子"""
try:
shadow = self.device_manager.get_device_shadow(device_sn)
if not shadow:
return jsonify({"success": False, "error": "Device shadow not found"}), 404
return jsonify({
"success": True,
"data": shadow.to_dict()
})
except Exception as e:
self.logger.error(f"Error getting device shadow {device_sn}: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@self.blueprint.route('/<device_sn>/shadow', methods=['PUT'])
def update_device_shadow(device_sn):
"""更新设备影子"""
try:
state = request.get_json()
success = self.device_manager.update_device_shadow(device_sn, state)
if not success:
return jsonify({"success": False, "error": "Device not found"}), 404
return jsonify({
"success": True,
"message": "Device shadow updated successfully"
})
except Exception as e:
self.logger.error(f"Error updating device shadow {device_sn}: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@self.blueprint.route('/discover', methods=['POST'])
def discover_devices():
"""设备发现"""
try:
discovered = self.device_manager.discover_devices()
return jsonify({
"success": True,
"data": discovered,
"message": f"Discovered {len(discovered)} devices"
})
except Exception as e:
self.logger.error(f"Error discovering devices: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@self.blueprint.route('/<device_sn>/command', methods=['POST'])
def send_device_command(device_sn):
"""发送设备控制命令"""
try:
command = request.get_json()
# 验证设备存在
device = self.device_manager.get_device(device_sn)
if not device:
return jsonify({"success": False, "error": "Device not found"}), 404
# 发送命令
success = self.mqtt_adapter.send_command(device_sn, command)
if not success:
return jsonify({"success": False, "error": "Failed to send command"}), 500
return jsonify({
"success": True,
"message": "Command sent successfully",
"device_sn": device_sn,
"command": command
})
except Exception as e:
self.logger.error(f"Error sending command to device {device_sn}: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@self.blueprint.route('/statistics', methods=['GET'])
def get_device_statistics():
"""获取设备统计信息"""
try:
statistics = self.device_manager.get_device_statistics()
return jsonify({
"success": True,
"data": statistics
})
except Exception as e:
self.logger.error(f"Error getting device statistics: {e}")
return jsonify({"success": False, "error": str(e)}), 500
def get_blueprint(self):
"""获取Blueprint"""
return self.blueprint
+219
View File
@@ -0,0 +1,219 @@
"""
设备管理服务
负责设备的CRUD操作、设备影子管理、设备发现等功能
"""
import json
import logging
from datetime import datetime
from typing import List, Optional, Dict, Any
from .models import Device, DeviceShadow, DeviceStatus, DeviceType
class DeviceManager:
"""设备管理器"""
def __init__(self):
self.devices: Dict[str, Device] = {} # device_sn -> Device
self.shadows: Dict[str, DeviceShadow] = {} # device_sn -> DeviceShadow
self.logger = logging.getLogger(__name__)
def register_device(self, device_data: Dict[str, Any]) -> Device:
"""
注册设备
Args:
device_data: 设备数据字典
Returns:
Device: 注册的设备对象
"""
device = Device(
device_sn=device_data['device_sn'],
device_type=DeviceType(device_data.get('device_type', 'other')),
name=device_data.get('name', ''),
description=device_data.get('description', ''),
area=device_data.get('area', ''),
position=device_data.get('position', ''),
geom=device_data.get('geom'),
manufacturer=device_data.get('manufacturer', ''),
model=device_data.get('model', ''),
firmware_version=device_data.get('firmware_version', ''),
hardware_version=device_data.get('hardware_version', ''),
metadata=device_data.get('metadata', {})
)
self.devices[device.device_sn] = device
# 创建设备影子
shadow = DeviceShadow(device_sn=device.device_sn)
self.shadows[device.device_sn] = shadow
self.logger.info(f"Device registered: {device.device_sn}")
return device
def get_device(self, device_sn: str) -> Optional[Device]:
"""
获取设备信息
Args:
device_sn: 设备序列号
Returns:
Device: 设备对象,如果不存在返回None
"""
return self.devices.get(device_sn)
def update_device(self, device_sn: str, updates: Dict[str, Any]) -> Optional[Device]:
"""
更新设备信息
Args:
device_sn: 设备序列号
updates: 更新的字段
Returns:
Device: 更新后的设备对象,如果不存在返回None
"""
device = self.devices.get(device_sn)
if not device:
return None
# 更新设备属性
for key, value in updates.items():
if hasattr(device, key):
setattr(device, key, value)
device.updated_at = datetime.now()
self.logger.info(f"Device updated: {device_sn}")
return device
def delete_device(self, device_sn: str) -> bool:
"""
删除设备
Args:
device_sn: 设备序列号
Returns:
bool: 是否删除成功
"""
if device_sn in self.devices:
del self.devices[device_sn]
if device_sn in self.shadows:
del self.shadows[device_sn]
self.logger.info(f"Device deleted: {device_sn}")
return True
return False
def list_devices(self,
device_type: Optional[DeviceType] = None,
status: Optional[DeviceStatus] = None,
area: Optional[str] = None) -> List[Device]:
"""
列出设备
Args:
device_type: 设备类型过滤
status: 设备状态过滤
area: 区域过滤
Returns:
List[Device]: 设备列表
"""
devices = list(self.devices.values())
if device_type:
devices = [d for d in devices if d.device_type == device_type]
if status:
devices = [d for d in devices if d.status == status]
if area:
devices = [d for d in devices if d.area == area]
return devices
def update_device_shadow(self, device_sn: str, state: Dict[str, Any]) -> bool:
"""
更新设备影子
Args:
device_sn: 设备序列号
state: 设备状态
Returns:
bool: 是否更新成功
"""
if device_sn not in self.shadows:
return False
shadow = self.shadows[device_sn]
shadow.state.update(state)
shadow.timestamp = datetime.now()
self.logger.debug(f"Device shadow updated: {device_sn}")
return True
def get_device_shadow(self, device_sn: str) -> Optional[DeviceShadow]:
"""
获取设备影子
Args:
device_sn: 设备序列号
Returns:
DeviceShadow: 设备影子对象
"""
return self.shadows.get(device_sn)
def discover_devices(self) -> List[Dict[str, Any]]:
"""
设备发现 - 扫描网络中的设备
Returns:
List[Dict[str, Any]]: 发现的设备列表
"""
discovered = []
# 模拟设备发现过程
# 在实际实现中,这里可以包含网络扫描、协议握手等逻辑
for device_sn, device in self.devices.items():
if device.status == DeviceStatus.OFFLINE:
# 模拟设备上线
device.status = DeviceStatus.ONLINE
device.last_seen = datetime.now()
device.ip_address = f"192.168.1.{hash(device_sn) % 255 + 1}"
discovered.append({
"device_sn": device_sn,
"name": device.name,
"type": device.device_type.value,
"ip_address": device.ip_address,
"status": device.status.value
})
self.logger.info(f"Discovered {len(discovered)} devices")
return discovered
def get_device_statistics(self) -> Dict[str, Any]:
"""
获取设备统计信息
Returns:
Dict[str, Any]: 统计信息
"""
total = len(self.devices)
online = sum(1 for d in self.devices.values() if d.status == DeviceStatus.ONLINE)
offline = total - online
by_type = {}
for device in self.devices.values():
device_type = device.device_type.value
by_type[device_type] = by_type.get(device_type, 0) + 1
return {
"total_devices": total,
"online_devices": online,
"offline_devices": offline,
"devices_by_type": by_type
}
+163
View File
@@ -0,0 +1,163 @@
"""
IoT 设备模型定义
包含设备实体、设备影子、OTA升级等核心数据模型
"""
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from typing import Dict, List, Optional, Any
import uuid
class DeviceStatus(Enum):
"""设备状态枚举"""
ONLINE = "online"
OFFLINE = "offline"
MAINTENANCE = "maintenance"
FAULT = "fault"
class DeviceType(Enum):
"""设备类型枚举"""
FLOW_METER = "flow_meter" # 流量计
PRESSURE_METER = "pressure_meter" # 压力表
LEVEL_METER = "level_meter" # 水位计
QUALITY_METER = "quality_meter" # 水质仪
VALVE = "valve" # 阀门
PUMP = "pump" # 水泵
SENSOR = "sensor" # 传感器
CAMERA = "camera" # 摄像头
OTHER = "other" # 其他
@dataclass
class Device:
"""设备实体模型"""
# 必需字段(无默认值)
device_sn: str # 设备序列号(唯一标识)
device_type: DeviceType # 设备类型
name: str # 设备名称
# 可选字段(有默认值)
description: str = "" # 设备描述
area: str = "" # 区域
position: str = "" # 位置
geom: Optional[str] = None # 地理坐标(GeoJSON格式)
manufacturer: str = "" # 厂商
model: str = "" # 型号
firmware_version: str = "" # 固件版本
hardware_version: str = "" # 硬件版本
status: DeviceStatus = DeviceStatus.OFFLINE
last_seen: Optional[datetime] = None
ip_address: Optional[str] = None
port: Optional[int] = None
metadata: Dict[str, Any] = field(default_factory=dict)
created_at: datetime = field(default_factory=datetime.now)
updated_at: datetime = field(default_factory=datetime.now)
id: Optional[int] = None
def to_dict(self) -> Dict[str, Any]:
"""转换为字典"""
return {
"id": self.id,
"device_sn": self.device_sn,
"device_type": self.device_type.value,
"name": self.name,
"description": self.description,
"area": self.area,
"position": self.position,
"geom": self.geom,
"manufacturer": self.manufacturer,
"model": self.model,
"firmware_version": self.firmware_version,
"hardware_version": self.hardware_version,
"status": self.status.value,
"last_seen": self.last_seen.isoformat() if self.last_seen else None,
"ip_address": self.ip_address,
"port": self.port,
"metadata": self.metadata,
"created_at": self.created_at.isoformat(),
"updated_at": self.updated_at.isoformat()
}
@dataclass
class DeviceShadow:
"""设备影子模型"""
# 必需字段
device_sn: str # 设备序列号
# 可选字段
state: Dict[str, Any] = field(default_factory=dict) # 设备状态
desired_state: Dict[str, Any] = field(default_factory=dict) # 期望状态
reported_state: Dict[str, Any] = field(default_factory=dict) # 报告状态
timestamp: datetime = field(default_factory=datetime.now) # 时间戳
def to_dict(self) -> Dict[str, Any]:
"""转换为字典"""
return {
"device_sn": self.device_sn,
"state": self.state,
"desired_state": self.desired_state,
"reported_state": self.reported_state,
"timestamp": self.timestamp.isoformat()
}
@dataclass
class OtaUpdate:
"""OTA升级记录"""
# 必需字段
device_sn: str # 设备序列号
version: str # 目标版本
file_url: str # 固件文件URL
file_size: int # 文件大小
checksum: str # 文件校验和
# 可选字段
id: str = field(default_factory=lambda: str(uuid.uuid4()))
status: str = "pending" # 状态:pending/downloading/installed/failed
progress: int = 0 # 进度百分比
error_message: Optional[str] = None # 错误信息
started_at: Optional[datetime] = None
completed_at: Optional[datetime] = None
def to_dict(self) -> Dict[str, Any]:
"""转换为字典"""
return {
"id": self.id,
"device_sn": self.device_sn,
"version": self.version,
"file_url": self.file_url,
"file_size": self.file_size,
"checksum": self.checksum,
"status": self.status,
"progress": self.progress,
"error_message": self.error_message,
"started_at": self.started_at.isoformat() if self.started_at else None,
"completed_at": self.completed_at.isoformat() if self.completed_at else None
}
@dataclass
class MqttMessage:
"""MQTT消息模型"""
# 必需字段
topic: str # 主题
payload: Dict[str, Any] # 消息内容
# 可选字段
qos: int = 0 # QoS等级
retain: bool = False # 是否保留消息
timestamp: datetime = field(default_factory=datetime.now) # 时间戳
def to_dict(self) -> Dict[str, Any]:
"""转换为字典"""
return {
"topic": self.topic,
"payload": self.payload,
"qos": self.qos,
"retain": self.retain,
"timestamp": self.timestamp.isoformat()
}
+352
View File
@@ -0,0 +1,352 @@
"""
MQTT 协议适配器
负责MQTT连接管理、消息订阅/发布、消息解析等功能
"""
import json
import logging
import paho.mqtt.client as mqtt
from datetime import datetime
from typing import Dict, Any, Optional, Callable, List
from .models import MqttMessage
from threading import Lock
class MqttAdapter:
"""MQTT适配器"""
def __init__(self,
broker_host: str = "localhost",
broker_port: int = 1883,
username: Optional[str] = None,
password: Optional[str] = None,
client_id: str = "water-management-system"):
"""
初始化MQTT适配器
Args:
broker_host: MQTT broker地址
broker_port: MQTT broker端口
username: 用户名
password: 密码
client_id: 客户端ID
"""
self.broker_host = broker_host
self.broker_port = broker_port
self.username = username
self.password = password
self.client_id = client_id
self.client = mqtt.Client(client_id=client_id)
self.message_handlers: Dict[str, Callable] = {}
self.connected = False
self.lock = Lock()
# 配置MQTT客户端
if username and password:
self.client.username_pw_set(username, password)
# 设置回调函数
self.client.on_connect = self._on_connect
self.client.on_disconnect = self._on_disconnect
self.client.on_message = self._on_message
self.client.on_publish = self._on_publish
self.client.on_subscribe = self._on_subscribe
self.logger = logging.getLogger(__name__)
def _on_connect(self, client, userdata, flags, rc):
"""连接回调"""
if rc == 0:
self.connected = True
self.logger.info(f"Connected to MQTT broker at {self.broker_host}:{self.broker_port}")
else:
self.logger.error(f"Failed to connect to MQTT broker, return code {rc}")
def _on_disconnect(self, client, userdata, rc):
"""断开连接回调"""
self.connected = False
self.logger.warning(f"Disconnected from MQTT broker, return code {rc}")
def _on_message(self, client, userdata, msg):
"""消息接收回调"""
try:
# 解析消息
payload = json.loads(msg.payload.decode('utf-8')) if msg.payload else {}
message = MqttMessage(
topic=msg.topic,
payload=payload,
qos=msg.qos,
retain=msg.retain
)
self.logger.debug(f"Received message: {message.topic} - {message.payload}")
# 查找对应的消息处理器
for topic_pattern, handler in self.message_handlers.items():
if self._topic_matches(msg.topic, topic_pattern):
try:
handler(message)
except Exception as e:
self.logger.error(f"Error in message handler for {msg.topic}: {e}")
except json.JSONDecodeError as e:
self.logger.error(f"Failed to parse JSON message from {msg.topic}: {e}")
except Exception as e:
self.logger.error(f"Error processing message from {msg.topic}: {e}")
def _on_publish(self, client, userdata, mid):
"""发布消息回调"""
self.logger.debug(f"Message published with mid: {mid}")
def _on_subscribe(self, client, userdata, mid, granted_qos):
"""订阅回调"""
self.logger.debug(f"Subscribed with mid: {mid}, granted_qos: {granted_qos}")
def _topic_matches(self, topic: str, pattern: str) -> bool:
"""检查主题是否匹配模式"""
# 简单的通配符匹配实现
# 支持单层通配符 + 和多层通配符 #
pattern_parts = pattern.split('/')
topic_parts = topic.split('/')
if len(pattern_parts) != len(topic_parts):
return False
for p_part, t_part in zip(pattern_parts, topic_parts):
if p_part == '+' or p_part == '#':
continue
if p_part != t_part:
return False
return True
def connect(self) -> bool:
"""
连接到MQTT broker
Returns:
bool: 是否连接成功
"""
try:
self.client.connect(self.broker_host, self.broker_port, 60)
self.client.loop_start()
return True
except Exception as e:
self.logger.error(f"Failed to connect to MQTT broker: {e}")
return False
def disconnect(self):
"""断开MQTT连接"""
if self.connected:
self.client.loop_stop()
self.client.disconnect()
def is_connected(self) -> bool:
"""
检查是否已连接
Returns:
bool: 是否已连接
"""
return self.connected
def subscribe(self, topic: str, qos: int = 0) -> bool:
"""
订阅主题
Args:
topic: 主题
qos: QoS等级
Returns:
bool: 是否订阅成功
"""
try:
result = self.client.subscribe(topic, qos)
if result[0] == mqtt.MQTT_ERR_SUCCESS:
self.logger.info(f"Subscribed to topic: {topic}")
return True
else:
self.logger.error(f"Failed to subscribe to topic: {topic}")
return False
except Exception as e:
self.logger.error(f"Error subscribing to topic {topic}: {e}")
return False
def unsubscribe(self, topic: str) -> bool:
"""
取消订阅主题
Args:
topic: 主题
Returns:
bool: 是否取消订阅成功
"""
try:
result = self.client.unsubscribe(topic)
if result[0] == mqtt.MQTT_ERR_SUCCESS:
self.logger.info(f"Unsubscribed from topic: {topic}")
return True
else:
self.logger.error(f"Failed to unsubscribe from topic: {topic}")
return False
except Exception as e:
self.logger.error(f"Error unsubscribing from topic {topic}: {e}")
return False
def publish(self, topic: str, payload: Any, qos: int = 0, retain: bool = False) -> bool:
"""
发布消息
Args:
topic: 主题
payload: 消息内容
qos: QoS等级
retain: 是否保留消息
Returns:
bool: 是否发布成功
"""
try:
if isinstance(payload, dict):
payload = json.dumps(payload)
elif not isinstance(payload, str):
payload = str(payload)
result = self.client.publish(topic, payload, qos, retain)
if result[0] == mqtt.MQTT_ERR_SUCCESS:
self.logger.debug(f"Published to topic: {topic}")
return True
else:
self.logger.error(f"Failed to publish to topic: {topic}")
return False
except Exception as e:
self.logger.error(f"Error publishing to topic {topic}: {e}")
return False
def add_message_handler(self, topic_pattern: str, handler: Callable[[MqttMessage], None]):
"""
添加消息处理器
Args:
topic_pattern: 主题模式(支持通配符)
handler: 消息处理函数
"""
with self.lock:
self.message_handlers[topic_pattern] = handler
self.logger.info(f"Added message handler for pattern: {topic_pattern}")
def remove_message_handler(self, topic_pattern: str):
"""
移除消息处理器
Args:
topic_pattern: 主题模式
"""
with self.lock:
if topic_pattern in self.message_handlers:
del self.message_handlers[topic_pattern]
self.logger.info(f"Removed message handler for pattern: {topic_pattern}")
def subscribe_device_topics(self, device_manager):
"""
订阅设备相关主题
Args:
device_manager: 设备管理器实例
"""
# 设备状态上报
self.add_message_handler("devices/+/status", self._handle_device_status)
# 设备数据上报
self.add_message_handler("devices/+/data", self._handle_device_data)
# 设备控制命令响应
self.add_message_handler("devices/+/command/response", self._handle_command_response)
# 设备OTA状态
self.add_message_handler("devices/+/ota/status", self._handle_ota_status)
def _handle_device_status(self, message: MqttMessage):
"""处理设备状态消息"""
topic_parts = message.topic.split('/')
if len(topic_parts) >= 2:
device_sn = topic_parts[1]
status = message.payload.get('status', 'unknown')
# 更新设备状态
device = device_manager.get_device(device_sn)
if device:
from .models import DeviceStatus
try:
device.status = DeviceStatus(status)
device.last_seen = datetime.now()
device_manager.logger.info(f"Device {device_sn} status updated to {status}")
except ValueError:
device_manager.logger.warning(f"Unknown status: {status}")
def _handle_device_data(self, message: MqttMessage):
"""处理设备数据消息"""
topic_parts = message.topic.split('/')
if len(topic_parts) >= 2:
device_sn = topic_parts[1]
data = message.payload
# 更新设备影子
device_manager.update_device_shadow(device_sn, data)
device_manager.logger.debug(f"Device {device_sn} data updated")
def _handle_command_response(self, message: MqttMessage):
"""处理命令响应消息"""
topic_parts = message.topic.split('/')
if len(topic_parts) >= 2:
device_sn = topic_parts[1]
command_id = message.payload.get('command_id')
result = message.payload.get('result')
device_manager.logger.info(f"Device {device_sn} command response: {command_id} -> {result}")
def _handle_ota_status(self, message: MqttMessage):
"""处理OTA状态消息"""
topic_parts = message.topic.split('/')
if len(topic_parts) >= 2:
device_sn = topic_parts[1]
status = message.payload.get('status')
progress = message.payload.get('progress', 0)
device_manager.logger.info(f"Device {device_sn} OTA status: {status}, progress: {progress}%")
def send_command(self, device_sn: str, command: Dict[str, Any]) -> bool:
"""
发送设备控制命令
Args:
device_sn: 设备序列号
command: 命令内容
Returns:
bool: 是否发送成功
"""
topic = f"devices/{device_sn}/command"
command['command_id'] = f"cmd_{datetime.now().timestamp()}"
command['timestamp'] = datetime.now().isoformat()
return self.publish(topic, command, qos=1)
def get_connection_status(self) -> Dict[str, Any]:
"""
获取连接状态
Returns:
Dict[str, Any]: 连接状态信息
"""
return {
"connected": self.connected,
"broker_host": self.broker_host,
"broker_port": self.broker_port,
"client_id": self.client_id,
"message_handlers_count": len(self.message_handlers)
}
+214
View File
@@ -0,0 +1,214 @@
"""
OTA固件升级控制器
提供OTA升级相关的REST API接口
"""
import json
import logging
from datetime import datetime
from typing import Dict, Any, List, Optional
from flask import Blueprint, request, jsonify
from .ota_manager import OtaManager
from .models import OtaUpdate
class OtaController:
"""OTA控制器"""
def __init__(self, ota_manager: OtaManager):
"""
初始化OTA控制器
Args:
ota_manager: OTA管理器
"""
self.ota_manager = ota_manager
self.logger = logging.getLogger(__name__)
# 创建Blueprint
self.blueprint = Blueprint('ota', __name__, url_prefix='/api/iot/ota')
# 注册路由
self._register_routes()
def _register_routes(self):
"""注册API路由"""
@self.blueprint.route('/updates', methods=['GET'])
def list_updates():
"""获取OTA更新列表"""
try:
# 获取查询参数
device_sn = request.args.get('device_sn')
status = request.args.get('status')
page = int(request.args.get('page', 1))
per_page = int(request.args.get('per_page', 10))
# 过滤条件
updates = list(self.ota_manager.updates.values())
if device_sn:
updates = [u for u in updates if u.device_sn == device_sn]
if status:
updates = [u for u in updates if u.status == status]
# 分页
total = len(updates)
start = (page - 1) * per_page
end = start + per_page
paginated_updates = updates[start:end]
# 转换为字典格式
update_list = [update.to_dict() for update in paginated_updates]
return jsonify({
"success": True,
"data": update_list,
"pagination": {
"page": page,
"per_page": per_page,
"total": total,
"pages": (total + per_page - 1) // per_page
}
})
except Exception as e:
self.logger.error(f"Error listing OTA updates: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@self.blueprint.route('/updates/<update_id>', methods=['GET'])
def get_update(update_id):
"""获取OTA更新详情"""
try:
update = self.ota_manager.get_update(update_id)
if not update:
return jsonify({"success": False, "error": "Update not found"}), 404
return jsonify({
"success": True,
"data": update.to_dict()
})
except Exception as e:
self.logger.error(f"Error getting OTA update {update_id}: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@self.blueprint.route('/updates', methods=['POST'])
def create_update():
"""创建OTA更新任务"""
try:
update_data = request.get_json()
# 验证必要字段
required_fields = ['device_sn', 'version', 'file_url', 'file_size', 'checksum']
for field in required_fields:
if field not in update_data:
return jsonify({"success": False, "error": f"Missing required field: {field}"}), 400
# 创建更新
update = self.ota_manager.create_update(
device_sn=update_data['device_sn'],
version=update_data['version'],
file_url=update_data['file_url'],
file_size=update_data['file_size'],
checksum=update_data['checksum']
)
return jsonify({
"success": True,
"data": update.to_dict(),
"message": "OTA update created successfully"
}), 201
except Exception as e:
self.logger.error(f"Error creating OTA update: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@self.blueprint.route('/updates/<update_id>/start', methods=['POST'])
def start_update(update_id):
"""开始OTA更新"""
try:
success = self.ota_manager.start_update(update_id)
if not success:
return jsonify({"success": False, "error": "Failed to start update"}), 400
return jsonify({
"success": True,
"message": "OTA update started successfully",
"update_id": update_id
})
except Exception as e:
self.logger.error(f"Error starting OTA update {update_id}: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@self.blueprint.route('/updates/<update_id>/progress', methods=['PUT'])
def update_progress(update_id):
"""更新OTA进度"""
try:
progress_data = request.get_json()
progress = progress_data.get('progress')
error_message = progress_data.get('error_message')
if progress is None:
return jsonify({"success": False, "error": "Progress is required"}), 400
success = self.ota_manager.update_progress(update_id, progress, error_message)
if not success:
return jsonify({"success": False, "error": "Update not found"}), 404
return jsonify({
"success": True,
"message": "OTA progress updated successfully",
"update_id": update_id,
"progress": progress
})
except Exception as e:
self.logger.error(f"Error updating OTA progress {update_id}: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@self.blueprint.route('/updates/<update_id>/cancel', methods=['POST'])
def cancel_update(update_id):
"""取消OTA更新"""
try:
success = self.ota_manager.cancel_update(update_id)
if not success:
return jsonify({"success": False, "error": "Failed to cancel update"}), 400
return jsonify({
"success": True,
"message": "OTA update cancelled successfully",
"update_id": update_id
})
except Exception as e:
self.logger.error(f"Error cancelling OTA update {update_id}: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@self.blueprint.route('/updates/device/<device_sn>', methods=['GET'])
def get_device_updates(device_sn):
"""获取设备的OTA更新记录"""
try:
updates = self.ota_manager.get_updates_by_device(device_sn)
return jsonify({
"success": True,
"data": [update.to_dict() for update in updates],
"device_sn": device_sn
})
except Exception as e:
self.logger.error(f"Error getting updates for device {device_sn}: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@self.blueprint.route('/statistics', methods=['GET'])
def get_statistics():
"""获取OTA统计信息"""
try:
statistics = self.ota_manager.get_update_statistics()
return jsonify({
"success": True,
"data": statistics
})
except Exception as e:
self.logger.error(f"Error getting OTA statistics: {e}")
return jsonify({"success": False, "error": str(e)}), 500
def get_blueprint(self):
"""获取Blueprint"""
return self.blueprint
+173
View File
@@ -0,0 +1,173 @@
"""
OTA固件升级管理器
负责设备OTA升级流程、版本管理、升级状态跟踪等功能
"""
import json
import logging
import hashlib
from datetime import datetime
from typing import Dict, Any, List, Optional
from .models import OtaUpdate
class OtaManager:
"""OTA管理器"""
def __init__(self):
self.updates: Dict[str, OtaUpdate] = {} # update_id -> OtaUpdate
self.logger = logging.getLogger(__name__)
def create_update(self, device_sn: str, version: str, file_url: str,
file_size: int, checksum: str) -> OtaUpdate:
"""
创建OTA升级任务
Args:
device_sn: 设备序列号
version: 目标版本
file_url: 固件文件URL
file_size: 文件大小
checksum: 文件校验和
Returns:
OtaUpdate: OTA升级对象
"""
update = OtaUpdate(
device_sn=device_sn,
version=version,
file_url=file_url,
file_size=file_size,
checksum=checksum
)
self.updates[update.id] = update
self.logger.info(f"Created OTA update: {update.id} for device {device_sn}")
return update
def get_update(self, update_id: str) -> Optional[OtaUpdate]:
"""
获取OTA升级信息
Args:
update_id: 更新ID
Returns:
OtaUpdate: OTA升级对象
"""
return self.updates.get(update_id)
def get_updates_by_device(self, device_sn: str) -> List[OtaUpdate]:
"""
获取设备的OTA升级记录
Args:
device_sn: 设备序列号
Returns:
List[OtaUpdate]: OTA升级列表
"""
return [update for update in self.updates.values() if update.device_sn == device_sn]
def start_update(self, update_id: str) -> bool:
"""
开始OTA升级
Args:
update_id: 更新ID
Returns:
bool: 是否开始成功
"""
update = self.updates.get(update_id)
if not update:
return False
if update.status != "pending":
self.logger.warning(f"Update {update_id} is not in pending state")
return False
update.status = "downloading"
update.started_at = datetime.now()
self.logger.info(f"Started OTA update: {update_id}")
return True
def update_progress(self, update_id: str, progress: int, error_message: Optional[str] = None) -> bool:
"""
更新OTA升级进度
Args:
update_id: 更新ID
progress: 进度百分比
error_message: 错误信息
Returns:
bool: 是否更新成功
"""
update = self.updates.get(update_id)
if not update:
return False
update.progress = progress
if error_message:
update.error_message = error_message
update.status = "failed"
self.logger.error(f"OTA update {update_id} failed: {error_message}")
elif progress >= 100:
update.status = "installed"
update.completed_at = datetime.now()
self.logger.info(f"OTA update {update_id} completed successfully")
else:
# 保持下载中状态
pass
return True
def cancel_update(self, update_id: str) -> bool:
"""
取消OTA升级
Args:
update_id: 更新ID
Returns:
bool: 是否取消成功
"""
update = self.updates.get(update_id)
if not update:
return False
if update.status in ["installed", "failed"]:
self.logger.warning(f"Cannot cancel completed update {update_id}")
return False
update.status = "failed"
update.error_message = "Cancelled by user"
update.completed_at = datetime.now()
self.logger.info(f"Cancelled OTA update: {update_id}")
return True
def get_update_statistics(self) -> Dict[str, Any]:
"""
获取OTA统计信息
Returns:
Dict[str, Any]: 统计信息
"""
total = len(self.updates)
pending = sum(1 for u in self.updates.values() if u.status == "pending")
downloading = sum(1 for u in self.updates.values() if u.status == "downloading")
installed = sum(1 for u in self.updates.values() if u.status == "installed")
failed = sum(1 for u in self.updates.values() if u.status == "failed")
return {
"total_updates": total,
"pending": pending,
"downloading": downloading,
"installed": installed,
"failed": failed
}
+317
View File
@@ -0,0 +1,317 @@
"""
数据模型定义
定义水务管理系统的各种数据结构
"""
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Any
from datetime import datetime
from enum import Enum
class DataType(Enum):
"""数据类型枚举"""
LL = "LL" # 流量
YL = "YL" # 压力
SW = "SW" # 水位
ZD = "ZD" # 浊度
PH = "PH" # pH值
WD = "WD" # 温度
DD = "DD" # 电导率
YD = "YD" # 硬度
class AlertLevel(Enum):
"""警报级别枚举"""
INFO = "info"
WARNING = "warning"
ERROR = "error"
CRITICAL = "critical"
@dataclass
class Device:
"""设备模型"""
id: str
name: str
device_type: str
location: str
description: Optional[str] = None
install_date: Optional[datetime] = None
status: str = "active" # active, inactive, maintenance
metadata: Dict[str, Any] = field(default_factory=dict)
@dataclass
class SensorData:
"""传感器数据模型"""
id: str
device_id: str
data_type: DataType
value: float
unit: str
timestamp: datetime
location: str
quality_score: float = 1.0
metadata: Dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> Dict[str, Any]:
"""转换为字典"""
return {
"id": self.id,
"device_id": self.device_id,
"data_type": self.data_type.value,
"value": self.value,
"unit": self.unit,
"timestamp": self.timestamp.isoformat(),
"location": self.location,
"quality_score": self.quality_score,
"metadata": self.metadata
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'SensorData':
"""从字典创建对象"""
return cls(
id=data["id"],
device_id=data["device_id"],
data_type=DataType(data["data_type"]),
value=float(data["value"]),
unit=data.get("unit", ""),
timestamp=datetime.fromisoformat(data["timestamp"]),
location=data.get("location", ""),
quality_score=float(data.get("quality_score", 1.0)),
metadata=data.get("metadata", {})
)
@dataclass
class Alert:
"""警报模型"""
id: str
device_id: str
alert_type: str
level: AlertLevel
message: str
timestamp: datetime
resolved: bool = False
resolved_by: Optional[str] = None
resolved_at: Optional[datetime] = None
metadata: Dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> Dict[str, Any]:
"""转换为字典"""
return {
"id": self.id,
"device_id": self.device_id,
"alert_type": self.alert_type,
"level": self.level.value,
"message": self.message,
"timestamp": self.timestamp.isoformat(),
"resolved": self.resolved,
"resolved_by": self.resolved_by,
"resolved_at": self.resolved_at.isoformat() if self.resolved_at else None,
"metadata": self.metadata
}
@dataclass
class BatchImport:
"""批量导入记录模型"""
id: str
batch_id: str
data_source: str
total_records: int
successful_records: int
failed_records: int
status: str # pending, processing, completed, failed
file_name: Optional[str] = None
import_time: Optional[datetime] = None
completed_time: Optional[datetime] = None
error_messages: List[str] = field(default_factory=list)
metadata: Dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> Dict[str, Any]:
"""转换为字典"""
return {
"id": self.id,
"batch_id": self.batch_id,
"data_source": self.data_source,
"total_records": self.total_records,
"successful_records": self.successful_records,
"failed_records": self.failed_records,
"status": self.status,
"file_name": self.file_name,
"import_time": self.import_time.isoformat() if self.import_time else None,
"completed_time": self.completed_time.isoformat() if self.completed_time else None,
"error_messages": self.error_messages,
"metadata": self.metadata
}
@dataclass
class APIRequest:
"""API请求模型"""
id: str
method: str
endpoint: str
params: Dict[str, Any]
headers: Dict[str, Any]
body: Optional[Any] = None
timestamp: datetime = field(default_factory=datetime.now)
response_code: Optional[int] = None
response_time_ms: Optional[float] = None
response_body: Optional[Any] = None
def to_dict(self) -> Dict[str, Any]:
"""转换为字典"""
return {
"id": self.id,
"method": self.method,
"endpoint": self.endpoint,
"params": self.params,
"headers": self.headers,
"body": self.body,
"timestamp": self.timestamp.isoformat(),
"response_code": self.response_code,
"response_time_ms": self.response_time_ms,
"response_body": self.response_body
}
@dataclass
class WebSocketConnection:
"""WebSocket连接模型"""
id: str
client_ip: str
connected_at: datetime
disconnected_at: Optional[datetime] = None
subscriptions: List[str] = field(default_factory=list)
message_count: int = 0
last_message_at: Optional[datetime] = None
metadata: Dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> Dict[str, Any]:
"""转换为字典"""
return {
"id": self.id,
"client_ip": self.client_ip,
"connected_at": self.connected_at.isoformat(),
"disconnected_at": self.disconnected_at.isoformat() if self.disconnected_at else None,
"subscriptions": self.subscriptions,
"message_count": self.message_count,
"last_message_at": self.last_message_at.isoformat() if self.last_message_at else None,
"metadata": self.metadata
}
@dataclass
class SystemStats:
"""系统统计模型"""
timestamp: datetime
total_records: int
total_devices: int
active_connections: int
api_requests_count: int
alerts_count: int
data_quality_score: float
memory_usage_mb: float
cpu_usage_percent: float
def to_dict(self) -> Dict[str, Any]:
"""转换为字典"""
return {
"timestamp": self.timestamp.isoformat(),
"total_records": self.total_records,
"total_devices": self.total_devices,
"active_connections": self.active_connections,
"api_requests_count": self.api_requests_count,
"alerts_count": self.alerts_count,
"data_quality_score": self.data_quality_score,
"memory_usage_mb": self.memory_usage_mb,
"cpu_usage_percent": self.cpu_usage_percent
}
class DataValidator:
"""数据验证器"""
@staticmethod
def validate_sensor_data(data: Dict[str, Any]) -> List[str]:
"""验证传感器数据"""
errors = []
# 必需字段检查
required_fields = ["device_id", "data_type", "value", "location"]
for field in required_fields:
if field not in data:
errors.append(f"缺少必需字段: {field}")
# 数据类型验证
if "data_type" in data:
try:
DataType(data["data_type"])
except ValueError:
errors.append(f"无效的数据类型: {data['data_type']}")
# 数值验证
if "value" in data:
try:
value = float(data["value"])
# 根据数据类型进行数值范围检查
data_type = data.get("data_type")
if data_type == "LL" and value < 0:
errors.append("流量不能为负数")
elif data_type == "YL" and value < 0:
errors.append("压力不能为负数")
elif data_type == "SW" and value < 0:
errors.append("水位不能为负数")
except (ValueError, TypeError):
errors.append(f"无效的数值: {data['value']}")
# 时间戳验证
if "timestamp" in data:
try:
if isinstance(data["timestamp"], str):
datetime.fromisoformat(data["timestamp"])
except (ValueError, TypeError):
errors.append(f"无效的时间戳格式: {data['timestamp']}")
return errors
@staticmethod
def validate_device_data(data: Dict[str, Any]) -> List[str]:
"""验证设备数据"""
errors = []
# 必需字段检查
required_fields = ["id", "name", "device_type", "location"]
for field in required_fields:
if field not in data:
errors.append(f"缺少必需字段: {field}")
# 设备ID格式验证
if "id" in data:
device_id = data["id"]
if not isinstance(device_id, str) or not device_id.strip():
errors.append("设备ID不能为空")
elif len(device_id) > 50:
errors.append("设备ID长度不能超过50个字符")
# 状态验证
if "status" in data and data["status"] not in ["active", "inactive", "maintenance"]:
errors.append("设备状态必须是: active, inactive, maintenance")
return errors
@staticmethod
def validate_alert_data(data: Dict[str, Any]) -> List[str]:
"""验证警报数据"""
errors = []
# 必需字段检查
required_fields = ["device_id", "alert_type", "level", "message"]
for field in required_fields:
if field not in data:
errors.append(f"缺少必需字段: {field}")
# 警报级别验证
if "level" in data:
try:
AlertLevel(data["level"])
except ValueError:
errors.append(f"无效的警报级别: {data['level']}")
return errors
# 全局验证器实例
validator = DataValidator()
+409
View File
@@ -0,0 +1,409 @@
"""
数据处理工具模块
提供数据验证、转换、格式化等工具函数
"""
import json
import csv
import pandas as pd
from typing import Dict, List, Any, Optional, Union
from datetime import datetime, timedelta
import hashlib
import logging
# 配置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class DataConverter:
"""数据转换器"""
# 水利行业标准字段映射
FIELD_MAPPING = {
"流量": "LL",
"压力": "YL",
"水位": "SW",
"浊度": "ZD",
"pH值": "PH",
"温度": "WD",
"电导率": "DD",
"硬度": "YD",
# 支持常见的中文字段名
"流量计": "LL",
"压力表": "YL",
"水位计": "SW",
"浊度仪": "ZD",
"pH计": "PH",
"温度计": "WD",
"电导率仪": "DD",
"硬度计": "YD"
}
# 单位转换
UNIT_CONVERSIONS = {
# 流量单位转换 (m³/h)
"m³/h": 1.0,
"L/s": 3.6, # L/s = m³/h / 1000 * 3600
"m³/d": 1/24, # m³/d = m³/h / 24
"L/min": 1/60, # L/min = m³/h / 1000 * 60
# 压力单位转换 (MPa)
"MPa": 1.0,
"kPa": 0.001, # kPa = MPa / 1000
"bar": 0.1, # bar = MPa * 10
"kgf/cm²": 0.0980665, # kgf/cm² = MPa / 0.0980665
# 水位单位转换 (m)
"m": 1.0,
"cm": 0.01, # cm = m / 100
"mm": 0.001, # mm = m / 1000
# 浊度单位转换 (NTU)
"NTU": 1.0,
"FNU": 1.0, # FNU ≈ NTU
# pH值单位转换
"pH": 1.0,
# 温度单位转换 (°C)
"°C": 1.0,
"K": 1.0, # 相对差值
"°F": lambda x: (x - 32) / 1.8, # °F to °C
# 电导率单位转换 (μS/cm)
"μS/cm": 1.0,
"mS/cm": 1000, # mS/cm = μS/cm * 1000
"S/m": 10000 # S/m = μS/cm * 100
}
@classmethod
def normalize_field_name(cls, field_name: str) -> str:
"""标准化字段名"""
if not field_name:
return ""
field_name = field_name.strip().upper()
# 如果已经是标准格式,直接返回
if field_name in cls.FIELD_MAPPING.values():
return field_name
# 查映射表
if field_name in cls.FIELD_MAPPING:
return cls.FIELD_MAPPING[field_name]
# 英文映射
english_mapping = {
"flow": "LL",
"pressure": "YL",
"level": "SW",
"turbidity": "ZD",
"ph": "PH",
"temperature": "WD",
"conductivity": "DD",
"hardness": "YD"
}
if field_name.lower() in english_mapping:
return english_mapping[field_name.lower()]
return field_name
@classmethod
def convert_unit(cls, value: float, from_unit: str, to_unit: str) -> float:
"""单位转换"""
if from_unit == to_unit:
return value
if from_unit not in cls.UNIT_CONVERSIONS:
raise ValueError(f"不支持的单位: {from_unit}")
if to_unit not in cls.UNIT_CONVERSIONS:
raise ValueError(f"不支持的目标单位: {to_unit}")
from_conv = cls.UNIT_CONVERSIONS[from_unit]
to_conv = cls.UNIT_CONVERSIONS[to_unit]
if callable(from_conv):
value = from_conv(value)
if callable(to_conv):
return value / to_conv
else:
return value * (to_conv / from_conv)
@classmethod
def validate_sensor_data(cls, data: Dict[str, Any]) -> Dict[str, Any]:
"""验证传感器数据"""
errors = []
validated_data = {}
# 必需字段验证
required_fields = ["device_id", "data_type", "value"]
for field in required_fields:
if field not in data:
errors.append(f"缺少必需字段: {field}")
else:
validated_data[field] = data[field]
# 数据类型验证和标准化
if "data_type" in validated_data:
original_type = validated_data["data_type"]
validated_data["data_type"] = cls.normalize_field_name(original_type)
if validated_data["data_type"] != original_type:
logger.info(f"字段名标准化: {original_type} -> {validated_data['data_type']}")
# 数值验证
if "value" in validated_data:
try:
validated_data["value"] = float(validated_data["value"])
# 检查数值范围
data_type = validated_data.get("data_type", "")
if data_type == "LL" and validated_data["value"] < 0:
errors.append("流量不能为负数")
elif data_type == "YL" and validated_data["value"] < 0:
errors.append("压力不能为负数")
elif data_type == "SW" and validated_data["value"] < 0:
errors.append("水位不能为负数")
except (ValueError, TypeError):
errors.append(f"无效的数值: {validated_data['value']}")
# 地点验证
if "location" not in validated_data or not validated_data["location"]:
validated_data["location"] = "未知"
# 时间戳处理
if "timestamp" in data:
try:
if isinstance(data["timestamp"], str):
validated_data["timestamp"] = datetime.fromisoformat(data["timestamp"])
else:
validated_data["timestamp"] = data["timestamp"]
except (ValueError, TypeError):
validated_data["timestamp"] = datetime.now()
else:
validated_data["timestamp"] = datetime.now()
return {
"valid": len(errors) == 0,
"data": validated_data,
"errors": errors
}
class DataFormatter:
"""数据格式化器"""
@staticmethod
def format_sensor_data(data: Dict[str, Any], format_type: str = "json") -> str:
"""格式化传感器数据"""
if format_type == "json":
return json.dumps(data, ensure_ascii=False, indent=2)
elif format_type == "csv":
# CSV格式只包含关键字段
csv_fields = ["device_id", "data_type", "value", "location", "timestamp"]
csv_data = {k: data.get(k, "") for k in csv_fields}
import io
output = io.StringIO()
writer = csv.DictWriter(output, fieldnames=csv_fields)
writer.writeheader()
writer.writerow(csv_data)
return output.getvalue()
else:
raise ValueError(f"不支持的格式类型: {format_type}")
@staticmethod
def format_statistics(stats: Dict[str, Any], format_type: str = "text") -> str:
"""格式化统计数据"""
if format_type == "json":
return json.dumps(stats, ensure_ascii=False, indent=2)
elif format_type == "text":
lines = ["数据统计报告", "=" * 20]
lines.append(f"总记录数: {stats.get('total_records', 0)}")
if "by_type" in stats:
lines.append("\n按数据类型统计:")
for data_type, count in stats["by_type"].items():
lines.append(f" {data_type}: {count} 条")
if "by_device" in stats:
lines.append("\n按设备统计:")
for device_id, count in list(stats["by_device"].items())[:10]: # 只显示前10个
lines.append(f" {device_id}: {count} 条")
return "\n".join(lines)
else:
raise ValueError(f"不支持的格式类型: {format_type}")
class DataHasher:
"""数据哈希工具"""
@staticmethod
def calculate_data_hash(data: Dict[str, Any]) -> str:
"""计算数据哈希值"""
# 将数据转换为字符串
data_str = json.dumps(data, sort_keys=True, ensure_ascii=False)
# 计算MD5哈希
hash_md5 = hashlib.md5(data_str.encode())
return hash_md5.hexdigest()
@staticmethod
def generate_data_id(device_id: str, data_type: str, timestamp: datetime) -> str:
"""生成数据ID"""
# 使用设备ID、数据类型和时间戳生成唯一ID
time_str = timestamp.strftime("%Y%m%d_%H%M%S")
hash_input = f"{device_id}_{data_type}_{time_str}"
hash_md5 = hashlib.md5(hash_input.encode())
return f"{data_type}_{device_id}_{hash_md5.hexdigest()[:8]}"
class DataQualityChecker:
"""数据质量检查器"""
@staticmethod
def check_data_quality(records: List[Dict[str, Any]]) -> Dict[str, Any]:
"""检查数据质量"""
quality_report = {
"total_records": len(records),
"valid_records": 0,
"invalid_records": 0,
"quality_score": 0,
"issues": [],
"statistics": {}
}
if not records:
quality_report["quality_score"] = 0
return quality_report
valid_records = []
for record in records:
issues = []
# 检查必需字段
required_fields = ["device_id", "data_type", "value"]
for field in required_fields:
if field not in record or not record[field]:
issues.append(f"缺少必需字段: {field}")
# 检查数据类型
if "data_type" in record and record["data_type"]:
valid_types = ["LL", "YL", "SW", "ZD", "PH", "WD", "DD", "YD"]
if record["data_type"] not in valid_types:
issues.append(f"无效的数据类型: {record['data_type']}")
# 检查数值范围
if "value" in record and record["value"]:
try:
value = float(record["value"])
data_type = record.get("data_type", "")
if data_type == "LL" and value < 0:
issues.append("流量不能为负数")
elif data_type == "YL" and value < 0:
issues.append("压力不能为负数")
elif data_type == "SW" and value < 0:
issues.append("水位不能为负数")
# 检查异常值
if data_type == "LL" and value > 10000:
issues.append("流量值异常大")
elif data_type == "YL" and value > 10:
issues.append("压力值异常大")
except (ValueError, TypeError):
issues.append("无效的数值格式")
if not issues:
valid_records.append(record)
quality_report["valid_records"] += 1
else:
quality_report["invalid_records"] += 1
quality_report["issues"].extend(issues)
# 计算质量分数
quality_report["quality_score"] = quality_report["valid_records"] / len(records)
# 统计信息
if records:
quality_report["statistics"] = {
"completeness": quality_report["valid_records"] / len(records),
"uniqueness": len(set(r.get("device_id", "") for r in valid_records)) / len(valid_records) if valid_records else 0,
"timeliness": quality_report.calculate_timeliness(records)
}
return quality_report
@staticmethod
def calculate_timeliness(records: List[Dict[str, Any]]) -> float:
"""计算数据及时性(24小时内的数据比例)"""
if not records:
return 0
now = datetime.now()
recent_count = 0
for record in records:
timestamp = record.get("timestamp")
if timestamp:
try:
if isinstance(timestamp, str):
timestamp = datetime.fromisoformat(timestamp)
time_diff = now - timestamp
if time_diff <= timedelta(hours=24):
recent_count += 1
except:
pass
return recent_count / len(records)
class DataExporter:
"""数据导出工具"""
@staticmethod
def export_to_csv(records: List[Dict[str, Any]], file_path: str) -> bool:
"""导出为CSV文件"""
try:
if not records:
return False
# 获取所有字段
all_fields = set()
for record in records:
all_fields.update(record.keys())
# 排序字段
field_order = ["device_id", "data_type", "value", "location", "timestamp"]
for field in all_fields:
if field not in field_order:
field_order.append(field)
with open(file_path, 'w', newline='', encoding='utf-8') as csvfile:
writer = csv.DictWriter(csvfile, fieldnames=field_order)
writer.writeheader()
writer.writerows(records)
return True
except Exception as e:
logger.error(f"导出CSV失败: {str(e)}")
return False
@staticmethod
def export_to_json(records: List[Dict[str, Any]], file_path: str) -> bool:
"""导出为JSON文件"""
try:
with open(file_path, 'w', encoding='utf-8') as jsonfile:
json.dump(records, jsonfile, ensure_ascii=False, indent=2, default=str)
return True
except Exception as e:
logger.error(f"导出JSON失败: {str(e)}")
return False
# 全局工具实例
data_converter = DataConverter()
data_formatter = DataFormatter()
data_hasher = DataHasher()
quality_checker = DataQualityChecker()
data_exporter = DataExporter()
+214
View File
@@ -0,0 +1,214 @@
"""
WebSocket 实时数据推送服务器
支持实时数据推送、连接管理和数据广播
"""
import asyncio
import json
import websockets
from datetime import datetime
from typing import Set, Dict, Any
import logging
# 配置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class WebSocketServer:
"""WebSocket服务器类"""
def __init__(self, host: str = "0.0.0.0", port: int = 8765):
self.host = host
self.port = port
self.clients: Set[websockets.WebSocketServerProtocol] = set()
self.data_history: list = [] # 存储最近的数据用于新连接
async def register_client(self, websocket: websockets.WebSocketServerProtocol):
"""注册新客户端"""
self.clients.add(websocket)
client_ip = websocket.remote_address[0]
logger.info(f"新客户端连接: {client_ip}")
# 发送历史数据给新连接的客户端
if self.data_history:
await websocket.send(json.dumps({
"type": "history",
"data": self.data_history[-50:] # 发送最近50条数据
}))
# 发送欢迎消息
await websocket.send(json.dumps({
"type": "welcome",
"message": "已连接到水务管理系统实时数据服务器",
"timestamp": datetime.now().isoformat()
}))
async def unregister_client(self, websocket: websockets.WebSocketServerProtocol):
"""注销客户端"""
if websocket in self.clients:
self.clients.remove(websocket)
client_ip = websocket.remote_address[0]
logger.info(f"客户端断开连接: {client_ip}")
async def broadcast_data(self, data: Dict[str, Any]):
"""广播数据到所有连接的客户端"""
if not self.clients:
return
# 添加时间戳
data["timestamp"] = datetime.now().isoformat()
# 保存历史数据
self.data_history.append(data)
if len(self.data_history) > 1000: # 只保留最近1000条记录
self.data_history.pop(0)
# 广播数据
message = json.dumps(data)
disconnected_clients = []
for client in self.clients:
try:
await client.send(message)
except websockets.exceptions.ConnectionClosed:
disconnected_clients.append(client)
# 清理已断开的连接
for client in disconnected_clients:
await self.unregister_client(client)
async def handle_client_message(self, websocket: websockets.WebSocketServerProtocol, message: str):
"""处理客户端消息"""
try:
data = json.loads(message)
if data.get("type") == "subscribe":
# 处理订阅请求
subscription_type = data.get("subscription", "all")
response = {
"type": "subscription_ack",
"subscription": subscription_type,
"message": f"已订阅 {subscription_type} 类型数据"
}
await websocket.send(json.dumps(response))
logger.info(f"客户端订阅了 {subscription_type} 类型数据")
elif data.get("type") == "ping":
# 响应心跳检测
response = {
"type": "pong",
"timestamp": datetime.now().isoformat()
}
await websocket.send(json.dumps(response))
else:
logger.warning(f"未知的消息类型: {data.get('type', 'unknown')}")
except json.JSONDecodeError:
logger.error("无效的JSON消息")
except Exception as e:
logger.error(f"处理客户端消息时出错: {str(e)}")
async def client_handler(self, websocket: websockets.WebSocketServerProtocol, path: str):
"""处理客户端连接"""
await self.register_client(websocket)
try:
async for message in websocket:
await self.handle_client_message(websocket, message)
except websockets.exceptions.ConnectionClosed:
pass
finally:
await self.unregister_client(websocket)
async def start_server(self):
"""启动WebSocket服务器"""
logger.info(f"启动WebSocket服务器: {self.host}:{self.port}")
# 创建并启动服务器
self.server = await websockets.serve(
self.client_handler,
self.host,
self.port
)
logger.info("WebSocket服务器已启动")
return self.server
async def send_sensor_data(self, sensor_data: Dict[str, Any]):
"""发送传感器数据"""
data = {
"type": "sensor_data",
"data_type": sensor_data.get("data_type"),
"device_id": sensor_data.get("device_id"),
"value": sensor_data.get("value"),
"location": sensor_data.get("location"),
"timestamp": datetime.now().isoformat()
}
await self.broadcast_data(data)
async def send_alert(self, alert_data: Dict[str, Any]):
"""发送警报信息"""
data = {
"type": "alert",
"level": alert_data.get("level", "warning"),
"message": alert_data.get("message"),
"device_id": alert_data.get("device_id"),
"timestamp": datetime.now().isoformat()
}
await self.broadcast_data(data)
# 全局WebSocket服务器实例
websocket_server = WebSocketServer()
# 示例数据生成器
async def data_generator():
"""模拟数据生成器"""
import random
while True:
await asyncio.sleep(5) # 每5秒发送一次数据
# 模拟不同的传感器数据
sensor_types = ["LL", "YL", "SW", "ZD"]
sensor_type = random.choice(sensor_types)
# 根据传感器类型生成合理的数值范围
if sensor_type == "LL": # 流量
value = random.uniform(10, 100)
elif sensor_type == "YL": # 压力
value = random.uniform(0.1, 1.0)
elif sensor_type == "SW": # 水位
value = random.uniform(0, 10)
else: # ZD 浊度
value = random.uniform(0, 50)
sensor_data = {
"data_type": sensor_type,
"device_id": f"device_{random.randint(1, 10)}",
"value": round(value, 2),
"location": random.choice(["A区", "B区", "C区", "D区"])
}
await websocket_server.send_sensor_data(sensor_data)
# 启动服务器和生成器
async def main():
"""主函数"""
# 启动WebSocket服务器
server = await websocket_server.start_server()
# 启动数据生成器
generator_task = asyncio.create_task(data_generator())
# 保持服务器运行
try:
await asyncio.Future() # 永远等待
except KeyboardInterrupt:
logger.info("收到中断信号,正在关闭服务器...")
server.close()
await server.wait_closed()
generator_task.cancel()
await generator_task
if __name__ == "__main__":
asyncio.run(main())
+148
View File
@@ -0,0 +1,148 @@
"""
IoT模块测试脚本
用于验证MQTT适配器、设备管理器和API功能
"""
import asyncio
import json
import sys
import os
# 添加项目根目录到Python路径
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from src.iot.device_manager import DeviceManager
from src.iot.mqtt_adapter import MqttAdapter
from src.iot.device_controller import DeviceController
from src.iot.models import DeviceType, DeviceStatus
async def test_device_manager():
"""测试设备管理器"""
print("=== 测试设备管理器 ===")
device_manager = DeviceManager()
# 注册设备
device_data = {
'device_sn': 'LL-001',
'device_type': 'flow_meter',
'name': '流量计-001',
'description': 'A区入口流量计',
'area': 'A区',
'position': '入口处',
'manufacturer': '华为',
'model': 'LL-100'
}
device = device_manager.register_device(device_data)
print(f"注册设备: {device.device_sn} - {device.name}")
# 获取设备
retrieved_device = device_manager.get_device('LL-001')
print(f"获取设备: {retrieved_device.name}")
# 更新设备
updated_device = device_manager.update_device('LL-001', {'status': DeviceStatus.ONLINE})
print(f"更新设备状态: {updated_device.status}")
# 列出设备
devices = device_manager.list_devices()
print(f"设备列表: {len(devices)}个设备")
# 更新设备影子
device_manager.update_device_shadow('LL-001', {'temperature': 25.5, 'pressure': 0.8})
shadow = device_manager.get_device_shadow('LL-001')
print(f"设备影子: {shadow.state}")
# 获取统计信息
stats = device_manager.get_device_statistics()
print(f"设备统计: {stats}")
print("设备管理器测试完成\n")
async def test_mqtt_adapter():
"""测试MQTT适配器"""
print("=== 测试MQTT适配器 ===")
# 创建MQTT适配器(不实际连接)
mqtt_adapter = MqttAdapter(
broker_host="localhost",
broker_port=1883,
client_id="test-client"
)
# 测试消息发布
test_payload = {"message": "Hello IoT", "timestamp": "2024-01-01T00:00:00"}
success = mqtt_adapter.publish("test/topic", test_payload)
print(f"消息发布测试: {'成功' if success else '失败'}")
# 测试连接状态
status = mqtt_adapter.get_connection_status()
print(f"MQTT状态: {status}")
print("MQTT适配器测试完成\n")
async def test_device_controller():
"""测试设备控制器"""
print("=== 测试设备控制器 ===")
# 创建组件
device_manager = DeviceManager()
mqtt_adapter = MqttAdapter()
device_controller = DeviceController(device_manager, mqtt_adapter)
# 注册一些测试设备
test_devices = [
{
'device_sn': 'LL-001',
'device_type': 'flow_meter',
'name': '流量计-001',
'area': 'A区',
'manufacturer': '华为'
},
{
'device_sn': 'YL-001',
'device_type': 'pressure_meter',
'name': '压力表-001',
'area': 'B区',
'manufacturer': '西门子'
}
]
for device_data in test_devices:
device_manager.register_device(device_data)
# 模拟API请求
print("测试设备注册:")
print(f"已注册设备数量: {len(device_manager.devices)}")
# 模拟设备发现
discovered = device_manager.discover_devices()
print(f"发现设备数量: {len(discovered)}")
print("设备控制器测试完成\n")
async def main():
"""主测试函数"""
print("开始 IoT 模块测试...\n")
try:
# 测试各个组件
await test_device_manager()
await test_mqtt_adapter()
await test_device_controller()
print("✅ 所有测试完成!")
except Exception as e:
print(f"❌ 测试失败: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
asyncio.run(main())
+149
View File
@@ -0,0 +1,149 @@
"""
IoT模块简化测试脚本
仅测试设备管理器功能,不依赖MQTT
"""
import asyncio
import json
import sys
import os
# 添加项目根目录到Python路径
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from src.iot.device_manager import DeviceManager
from src.iot.models import DeviceType, DeviceStatus
async def test_device_manager():
"""测试设备管理器"""
print("=== 测试设备管理器 ===")
device_manager = DeviceManager()
# 注册设备
device_data = {
'device_sn': 'LL-001',
'device_type': 'flow_meter',
'name': '流量计-001',
'description': 'A区入口流量计',
'area': 'A区',
'position': '入口处',
'manufacturer': '华为',
'model': 'LL-100'
}
device = device_manager.register_device(device_data)
print(f"注册设备: {device.device_sn} - {device.name}")
# 获取设备
retrieved_device = device_manager.get_device('LL-001')
print(f"获取设备: {retrieved_device.name}")
# 更新设备
updated_device = device_manager.update_device('LL-001', {'status': DeviceStatus.ONLINE})
print(f"更新设备状态: {updated_device.status}")
# 列出设备
devices = device_manager.list_devices()
print(f"设备列表: {len(devices)}个设备")
# 更新设备影子
device_manager.update_device_shadow('LL-001', {'temperature': 25.5, 'pressure': 0.8})
shadow = device_manager.get_device_shadow('LL-001')
print(f"设备影子: {shadow.state}")
# 获取统计信息
stats = device_manager.get_device_statistics()
print(f"设备统计: {stats}")
# 测试设备发现
discovered = device_manager.discover_devices()
print(f"发现设备: {len(discovered)}个设备")
print("设备管理器测试完成\n")
async def test_device_filtering():
"""测试设备过滤功能"""
print("=== 测试设备过滤 ===")
device_manager = DeviceManager()
# 注册多个设备
test_devices = [
{'device_sn': 'LL-001', 'device_type': 'flow_meter', 'name': '流量计-001', 'area': 'A区'},
{'device_sn': 'YL-001', 'device_type': 'pressure_meter', 'name': '压力表-001', 'area': 'A区'},
{'device_sn': 'SW-001', 'device_type': 'level_meter', 'name': '水位计-001', 'area': 'B区'},
{'device_sn': 'LL-002', 'device_type': 'flow_meter', 'name': '流量计-002', 'area': 'B区'},
]
for device_data in test_devices:
device_manager.register_device(device_data)
# 测试按类型过滤
flow_meters = device_manager.list_devices(device_type=DeviceType.FLOW_METER)
print(f"流量计数量: {len(flow_meters)}")
# 测试按区域过滤
a_zone_devices = device_manager.list_devices(area='A区')
print(f"A区设备数量: {len(a_zone_devices)}")
# 测试按状态过滤
online_devices = device_manager.list_devices(status=DeviceStatus.ONLINE)
print(f"在线设备数量: {len(online_devices)}")
print("设备过滤测试完成\n")
async def test_device_shadow():
"""测试设备影子功能"""
print("=== 测试设备影子 ===")
device_manager = DeviceManager()
# 注册设备
device_data = {
'device_sn': 'LL-001',
'device_type': 'flow_meter',
'name': '流量计-001'
}
device_manager.register_device(device_data)
# 更新设备影子
shadow_data = {
'temperature': 25.5,
'pressure': 0.8,
'flow_rate': 100.5
}
success = device_manager.update_device_shadow('LL-001', shadow_data)
print(f"影子更新成功: {success}")
# 获取设备影子
shadow = device_manager.get_device_shadow('LL-001')
print(f"设备影子状态: {shadow.state}")
print("设备影子测试完成\n")
async def main():
"""主测试函数"""
print("开始 IoT 模块简化测试...\n")
try:
# 测试各个组件
await test_device_manager()
await test_device_filtering()
await test_device_shadow()
print("✅ 所有测试完成!")
except Exception as e:
print(f"❌ 测试失败: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
asyncio.run(main())