Ask me anything
Ithy Logo
Sign in to save

全面實作Modbus TCP智慧電表資料擷取與管理系統

利用Python、Flask與SQL Server打造高效的智慧電表資料管理平台

smart meter infrastructure

主要精要

  • 整合Flask框架與Modbus TCP通訊,實現智慧電表的即時資料擷取與管理。
  • 設計直觀且動態更新的前端介面,使用者可輕鬆查看與設定電表參數。
  • 確保系統穩定性與可擴展性,透過多線程或排程任務實現資料輪巡與儲存。

系統架構概述

本系統旨在透過Python結合Flask框架,實現對多個Modbus TCP智慧電表的資料擷取,並將收集到的數據存儲至SQL Server資料庫。使用者可透過網頁介面進行相關設定,包括電表IP、輪巡週期及SQL Server連線參數等。

系統組成

  • Flask後端:負責處理前端請求、管理設定、啟動輪巡任務並與資料庫互動。
  • Modbus TCP客戶端:通過pymodbus庫實現與智慧電表的通信,定期擷取資料。
  • SQL Server資料庫:存儲擷取到的電表數據,便於後續查詢與分析。
  • 前端介面:包含basboard.html與setting.html,提供即時數據顯示與設定功能。

詳細實作步驟

1. 環境配置與依賴安裝

首先,確保已安裝Python環境,並使用pip安裝所需套件:

pip install flask pymodbus pyodbc flask-socketio apscheduler

此外,需安裝ODBC Driver 17 for SQL Server,以確保pyodbc能正常與SQL Server通信。

2. 專案結構設計

遵循Flask的最佳實踐,建議專案目錄結構如下:


project/
├── app/
│   ├── __init__.py
│   ├── models.py
│   ├── routes/
│   │   ├── __init__.py
│   │   ├── dashboard.py
│   │   └── settings.py
│   ├── templates/
│   │   ├── basboard.html
│   │   └── setting.html
│   ├── static/
│   │   ├── css/
│   │   └── js/
│   └── utils/
│       ├── modbus_client.py
│       ├── database.py
│       └── scheduler.py
├── config.py
└── run.py
  

此結構將功能模組化,提升可維護性與可擴展性。

3. SQL Server資料庫設計

建立一個名為meter_data的資料表,用以存儲智慧電表數據:


CREATE TABLE meter_data (
    id INT PRIMARY KEY IDENTITY(1,1),
    ip VARCHAR(15) NOT NULL,
    address INT NOT NULL,
    name VARCHAR(50),
    value FLOAT NOT NULL,
    timestamp DATETIME DEFAULT GETDATE()
);
  

此資料表包含電表IP、資料地址、參數名稱、值及時間戳記。

4. Flask應用程式初始化

在app/__init__.py中初始化Flask應用程式,並設定相關擴展:

from flask import Flask
from flask_socketio import SocketIO
from .routes.dashboard import dashboard_bp
from .routes.settings import settings_bp

socketio = SocketIO()

def create_app():
    app = Flask(__name__)
    app.config['SECRET_KEY'] = 'your_secret_key'

    # 註冊藍圖
    app.register_blueprint(dashboard_bp)
    app.register_blueprint(settings_bp)

    # 初始化SocketIO
    socketio.init_app(app)

    return app
  

5. Modbus TCP客戶端實作

在app/utils/modbus_client.py中實作Modbus輪巡邏功能:

from pymodbus.client import ModbusTcpClient
from app.utils.database import Database
from apscheduler.schedulers.background import BackgroundScheduler
import time

class ModbusPoller:
    def __init__(self, db_config):
        self.db = Database(db_config)
        self.scheduler = BackgroundScheduler()
        self.devices = []
        self.polling_interval = 10  # 預設輪巡週期為10秒

    def load_config(self, config):
        self.devices = config.get('devices', [])
        self.polling_interval = config.get('polling_interval', 10)

    def start_polling(self):
        self.scheduler.add_job(self.poll_meters, 'interval', seconds=self.polling_interval)
        self.scheduler.start()

    def stop_polling(self):
        self.scheduler.shutdown(wait=False)

    def poll_meters(self):
        for device in self.devices:
            ip = device['ip']
            for param in device['parameters']:
                address = param['address']
                name = param['name']
                client = ModbusTcpClient(ip)
                try:
                    if client.connect():
                        result = client.read_holding_registers(address, 1)
                        if not result.isError():
                            value = result.registers[0]
                            self.db.save_data(ip, address, name, value)
                        client.close()
                except Exception as e:
                    print(f"Error polling {ip} at address {address}: {e}")
  

此模組負責根據設定定期輪巡智慧電表,並將擷取到的資料存入資料庫。

6. 資料庫操作模組

在app/utils/database.py中實作與SQL Server的連接與資料儲存:

import pyodbc

class Database:
    def __init__(self, config):
        self.host = config['host']
        self.database = config['database']
        self.user = config['user']
        self.password = config['password']
        self.connection_string = (
            f"DRIVER={{ODBC Driver 17 for SQL Server}};"
            f"SERVER={self.host};DATABASE={self.database};"
            f"UID={self.user};PWD={self.password}"
        )
        self.connect()

    def connect(self):
        self.conn = pyodbc.connect(self.connection_string)
        self.cursor = self.conn.cursor()

    def save_data(self, ip, address, name, value):
        query = """
            INSERT INTO meter_data (ip, address, name, value)
            VALUES (?, ?, ?, ?)
        """
        self.cursor.execute(query, (ip, address, name, value))
        self.conn.commit()

    def close(self):
        self.conn.close()
  

此模組封裝了資料庫的連接與資料儲存功能,確保資料的可靠性。

7. 排程與輪巡管理

在app/utils/scheduler.py中管理Modbus輪巡任務:

from app.utils.modbus_client import ModbusPoller

class SchedulerManager:
    def __init__(self, db_config, polling_config):
        self.poller = ModbusPoller(db_config)
        self.poller.load_config(polling_config)

    def start(self):
        self.poller.start_polling()

    def stop(self):
        self.poller.stop_polling()

    def update_config(self, new_polling_config):
        self.stop()
        self.poller.load_config(new_polling_config)
        self.start()
  

此模組負責啟動、停止及更新輪巡配置,確保系統能根據使用者設定動態調整。

8. Flask路由設計

在app/routes/dashboard.py與app/routes/settings.py中實作主要路由功能:

# app/routes/dashboard.py
from flask import Blueprint, render_template, jsonify
from flask_socketio import emit
from app.utils.database import Database

dashboard_bp = Blueprint('dashboard', __name__)

@dashboard_bp.route('/')
def dashboard():
    return render_template('basboard.html')

@dashboard_bp.route('/api/data')
def get_data():
    db = Database(current_app.config['DB_CONFIG'])
    data = db.fetchall("SELECT * FROM meter_data ORDER BY timestamp DESC")
    db.close()
    # 將資料轉換為JSON格式
    result = [
        {"ip": row.ip, "address": row.address, "name": row.name, "value": row.value, "timestamp": row.timestamp}
        for row in data
    ]
    return jsonify(result)
  
# app/routes/settings.py
from flask import Blueprint, render_template, request, jsonify, current_app
from app.utils.scheduler import SchedulerManager

settings_bp = Blueprint('settings', __name__)

@s⃦ettings_bp.route('/setting')
def settings():
    return render_template('setting.html')

@settings_bp.route('/api/settings', methods=['GET', 'POST'])
def manage_settings():
    if request.method == 'POST':
        settings = request.json
        # 更新配置
        current_app.config['DB_CONFIG'] = settings['db_config']
        current_app.config['POLLING_CONFIG'] = settings['polling_config']
        # 重啟輪巡
        scheduler = current_app.config['SCHEDULER']
        scheduler.update_config(settings['polling_config'])
        return jsonify({"status": "success"})
    else:
        # 返回現有設定
        return jsonify({
            "db_config": current_app.config['DB_CONFIG'],
            "polling_config": current_app.config['POLLING_CONFIG']
        })
  

上述路由負責渲染頁面、處理設定的獲取與更新,並確保輪巡任務根據最新設定運行。

9. 前端介面設計

設計兩個主要的HTML頁面:basboard.html與setting.html。

basboard.html

<!DOCTYPE html>
<html lang="zh-TW">
<head>
    <meta charset="UTF-8">
    <title>智慧電表儀表板</title>
    <link rel="stylesheet" href="/static/css/styles.css">
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.min.js"></script>
</head>
<body>
    <h2>智慧電表即時數據</h2>
    <table id="data-table">
        <thead>
            <tr>
                <th>IP</th>
                <th>名稱</th>
                <th>地址</th>
                <th>數值</th>
                <th>更新時間</th>
            </tr>
        </thead>
        <tbody>
            <!-- 動態更新數據 -->
        </tbody>
    </table>

    <script>
        function fetchData() {
            $.ajax({
                url: '/api/data',
                method: 'GET',
                success: function(data) {
                    $('#data-table tbody').empty();
                    data.forEach(function(row) {
                        $('#data-table tbody').append(
                            '<tr><td>' + row.ip + '</td><td>' + row.name + 
                            '</td><td>' + row.address + '</td><td>' + 
                            row.value + '</td><td>' + row.timestamp + '</td></tr>'
                        );
                    });
                }
            });
        }

        $(document).ready(function() {
            fetchData();
            setInterval(fetchData, 5000); // 每5秒更新一次
        });
    </script>
</body>
</html>
  

setting.html

<!DOCTYPE html>
<html lang="zh-TW">
<head>
    <meta charset="UTF-8">
    <title>設定頁面</title>
    <link rel="stylesheet" href="/static/css/styles.css">
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
    <h2>系統設定</h2>
    
    <button id="open-modal">設定SQL Server</button>
    
    <div id="modal" class="modal">
        <div class="modal-content">
            <span class="close">×</span>
            <h3>SQL Server設定</h3>
            <form id="db-form">
                <label for="host">主機:</label>
                <input type="text" id="host" name="host" required><br>
                <label for="database">資料庫:</label>
                <input type="text" id="database" name="database" required><br>
                <label for="user">使用者:</label>
                <input type="text" id="user" name="user" required><br>
                <label for="password">密碼:</label>
                <input type="password" id="password" name="password" required><br>
                <button type="submit">儲存</button>
            </form>
        </div>
    </div>

    <h3>電表設定</h3>
    <form id="settings-form">
        <div id="devices">
            <div class="device">
                <label>IP:</label>
                <input type="text" name="ip" required>
                <label>輪巡週期 (秒):</label>
                <input type="number" name="polling_interval" min="1" required>
                <button type="button" class="add-parameter">+</button>
                <div class="parameters">
                    <div class="parameter">
                        <label>名稱:</label>
                        <input type="text" name="name" required>
                        <label>地址:</label>
                        <input type="number" name="address" required>
                    </div>
                </div>
            </div>
        </div>
        <button type="button" id="add-device">+ 新增電表</button>
        <button type="submit">儲存設定</button>
    </form>

    <script>
        // Modal功能
        var modal = document.getElementById("modal");
        var btn = document.getElementById("open-modal");
        var span = document.getElementsByClassName("close")[0];

        btn.onclick = function() {
            modal.style.display = "block";
        }

        span.onclick = function() {
            modal.style.display = "none";
        }

        window.onclick = function(event) {
            if (event.target == modal) {
                modal.style.display = "none";
            }
        }

        // 增加電表
        $('#add-device').click(function() {
            var deviceHtml = `
                <div class="device">
                    <label>IP:</label>
                    <input type="text" name="ip" required>
                    <label>輪巡週期 (秒):</label>
                    <input type="number" name="polling_interval" min="1" required>
                    <button type="button" class="add-parameter">+</button>
                    <div class="parameters">
                        <div class="parameter">
                            <label>名稱:</label>
                            <input type="text" name="name" required>
                            <label>地址:</label>
                            <input type="number" name="address" required>
                        </div>
                    </div>
                </div>
            `;
            $('#devices').append(deviceHtml);
        });

        // 增加參數
        $(document).on('click', '.add-parameter', function() {
            var parameterHtml = `
                <div class="parameter">
                    <label>名稱:</label>
                    <input type="text" name="name" required>
                    <label>地址:</label>
                    <input type="number" name="address" required>
                </div>
            `;
            $(this).siblings('.parameters').append(parameterHtml);
        });

        // 儲存設定
        $('#db-form').submit(function(e) {
            e.preventDefault();
            var dbConfig = {
                host: $('#host').val(),
                database: $('#database').val(),
                user: $('#user').val(),
                password: $('#password').val()
            };
            $.ajax({
                url: '/api/settings',
                method: 'POST',
                contentType: 'application/json',
                data: JSON.stringify({ db_config: dbConfig, polling_config: getPollingConfig() }),
                success: function(response) {
                    alert('設定已儲存並輪巡已重新啟動');
                    modal.style.display = "none";
                }
            });
        });

        function getPollingConfig() {
            var devices = [];
            $('.device').each(function() {
                var ip = $(this).find('input[name="ip"]').val();
                var interval = parseInt($(this).find('input[name="polling_interval"]').val());
                var parameters = [];
                $(this).find('.parameter').each(function() {
                    var name = $(this).find('input[name="name"]').val();
                    var address = parseInt($(this).find('input[name="address"]').val());
                    parameters.push({ name: name, address: address });
                });
                devices.push({ ip: ip, polling_interval: interval, parameters: parameters });
            });
            return { devices: devices, polling_interval: 10 }; // 可根據需要調整
        }

        // 提交設定表單
        $('#settings-form').submit(function(e) {
            e.preventDefault();
            var settings = {
                db_config: {
                    host: $('#host').val(),
                    database: $('#database').val(),
                    user: $('#user').val(),
                    password: $('#password').val()
                },
                polling_config: getPollingConfig()
            };
            $.ajax({
                url: '/api/settings',
                method: 'POST',
                contentType: 'application/json',
                data: JSON.stringify(settings),
                success: function(response) {
                    alert('設定已儲存並輪巡已重新啟動');
                    location.reload();
                }
            });
        });
    </script>
</body>
</html>
  

在setting.html中,使用者可透過表單動態新增電表與其參數,並透過模態框設定SQL Server連線資訊。

10. Flask應用程式啟動與配置管理

在run.py中啟動Flask應用程式並載入初始配置:

from app import create_app, socketio
from app.utils.scheduler import SchedulerManager

app = create_app()

# 初始設定
app.config['DB_CONFIG'] = {
    'host': 'localhost',
    'database': 'meter_db',
    'user': 'sa',
    'password': 'your_password'
}

app.config['POLLING_CONFIG'] = {
    'devices': [
        {
            'ip': '192.168.1.100',
            'parameters': [
                {'name': '電壓', 'address': 1},
                {'name': '電流', 'address': 2}
            ]
        }
    ],
    'polling_interval': 10
}

# 初始化排程管理
scheduler = SchedulerManager(app.config['DB_CONFIG'], app.config['POLLING_CONFIG'])
app.config['SCHEDULER'] = scheduler
scheduler.start()

if __name__ == '__main__':
    socketio.run(app, host='0.0.0.0', port=5000, debug=True)
  

此步驟確保在應用啟動時即開始資料輪巡,並根據初始設定進行配置。

系統運行機制

當系統運行時,Flask後端會啟動Modbus TCP輪巡任務,定期向各智慧電表擷取資料,並將資料存入SQL Server。使用者可透過setting.html網頁動態調整設定,系統將根據新的參數重啟輪巡任務。

數據流動圖

階段 描述
1. 設定管理 使用者透過設定頁面輸入或修改SQL Server與電表參數。
2. 設定儲存 後端接收設定,更新資料庫與輪巡配置,並重新啟動輪巡任務。
3. 資料輪巡 系統定期向各智慧電表發送Modbus TCP請求,擷取資料。
4. 資料存儲 將擷取到的資料存入SQL Server資料庫。
5. 即時數據顯示 前端介面透過AJAX或WebSocket定期獲取最新資料,並更新展示。

最佳實踐與注意事項

  • 安全性:確保SQL Server連線資訊的安全,避免硬編碼敏感資訊,可考慮使用環境變數或加密存儲。
  • 錯誤處理:在Modbus通信過程中加入充分的錯誤處理,以應對設備連線失敗或資料擷取異常的情況。
  • 資源管理:確保資料庫連線在使用完畢後正確關閉,避免資源洩漏。
  • 可擴展性:設計模組化的程式結構,便於未來新增功能或支持更多設備類型。
  • 性能優化:對輪巡週期與資料擷取頻率進行合理設置,平衡系統即時性與資源使用。

常見問題解答

  • 如何處理多個電表同時輪巡的情況?可以使用多線程或非同步編程技術,如asyncio,提升輪巡效率。
  • 如何確保資料的完整性與一致性?在資料庫操作中加入事務管理,確保資料寫入的原子性。
  • 如何擴展系統以支持更多電表?通過動態讀取設定,並優化輪巡機制,使系統能夠自動適應新增的設備。

結論

本系統結合了Python、Flask框架與SQL Server資料庫,提供了一個完整的Modbus TCP智慧電表資料擷取與管理解決方案。透過直觀的前端介面,使用者能夠輕鬆設定與查看電表數據,並確保系統運行的穩定性與可擴展性。未來,可根據實際需求進一步優化與擴展系統功能,以滿足更高的使用要求。

參考資源

flask.palletsprojects.com
Flask官方文件
pymodbus.readthedocs.io
Pymodbus官方文檔
apscheduler.readthedocs.io
APScheduler官方文檔

總結

透過本文提供的詳細步驟與代碼範例,您可以建立一個強大且靈活的Modbus TCP智慧電表資料擷取系統。此系統不僅能夠實時監控電表數據,還具備良好的擴展性,能夠適應未來可能的需求變動。希望本指南能夠幫助您順利完成系統的開發與部署,若有進一步的問題,歡迎參考相關資源或進行技術討論。


Last updated January 24, 2025

AI-assisted research. Sources, methodology and corrections · Source-checked guides and tools · Explore published answers

Ask Ithy AI