214 lines
8.2 KiB
Python
214 lines
8.2 KiB
Python
import paho.mqtt.client as mqtt
|
||
import json
|
||
import logging
|
||
import taosrest
|
||
from datetime import datetime
|
||
|
||
# 配置日志
|
||
logging.basicConfig(
|
||
filename='charge_jiuxing.log',
|
||
level=logging.INFO,
|
||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||
encoding='utf-8'
|
||
)
|
||
|
||
class ChargeJiuxingParser:
|
||
def __init__(self, mqtt_host="123.6.102.119", mqtt_port=1883, mqtt_username="emqx_test", mqtt_password="emqx_test",
|
||
tdengine_url="http://123.6.102.119:6041", tdengine_user="root", tdengine_password="taosdata"):
|
||
# MQTT 配置
|
||
self.mqtt_client = mqtt.Client(client_id="ChargeJiuxingParser", protocol=mqtt.MQTTv311,
|
||
callback_api_version=mqtt.CallbackAPIVersion.VERSION2)
|
||
self.mqtt_client.username_pw_set(mqtt_username, mqtt_password)
|
||
self.mqtt_client.on_connect = self.on_connect
|
||
self.mqtt_client.on_message = self.on_message
|
||
self.mqtt_host = mqtt_host
|
||
self.mqtt_port = mqtt_port
|
||
self.connected = False
|
||
|
||
# TDengine 配置
|
||
self.tdengine_url = tdengine_url
|
||
self.tdengine_user = tdengine_user
|
||
self.tdengine_password = tdengine_password
|
||
self.tdengine_conn = None
|
||
|
||
def connect(self):
|
||
"""连接 MQTT 和 TDengine"""
|
||
# 连接 MQTT
|
||
try:
|
||
self.mqtt_client.connect(self.mqtt_host, self.mqtt_port, 60)
|
||
self.mqtt_client.loop_start()
|
||
logging.info("Connected to MQTT broker")
|
||
except Exception as e:
|
||
logging.error(f"MQTT connection error: {str(e)}")
|
||
raise
|
||
|
||
# 连接 TDengine
|
||
try:
|
||
self.tdengine_conn = taosrest.connect(
|
||
url=self.tdengine_url,
|
||
user=self.tdengine_user,
|
||
password=self.tdengine_password,
|
||
database="tms_design"
|
||
)
|
||
logging.info("Connected to TDengine via REST")
|
||
except Exception as e:
|
||
logging.error(f"TDengine connection error: {str(e)}")
|
||
raise
|
||
|
||
def on_connect(self, client, userdata, flags, rc, properties=None):
|
||
if rc == 0:
|
||
self.connected = True
|
||
self.mqtt_client.subscribe("hejin/charging/log", qos=1)
|
||
logging.info("Subscribed to hejin/charging/log")
|
||
else:
|
||
logging.error(f"Failed to connect to MQTT broker with code: {rc}")
|
||
|
||
def on_message(self, client, userdata, msg, properties=None):
|
||
"""处理接收到的 MQTT 消息"""
|
||
try:
|
||
payload = msg.payload.decode('utf-8')
|
||
data = json.loads(payload)
|
||
logging.info(f"Received message: {data}")
|
||
|
||
# 解析报文并存储
|
||
sql = self.parse_and_generate_sql(data)
|
||
if sql:
|
||
self.tdengine_conn.execute(sql)
|
||
logging.info(f"Inserted into TDengine: {sql}")
|
||
except Exception as e:
|
||
logging.error(f"Error processing message: {str(e)}")
|
||
|
||
def hex_to_ascii(self, hex_str):
|
||
"""将 HEX 字符串转为 ASCII"""
|
||
try:
|
||
return bytes.fromhex(hex_str).decode('ascii').strip('\x00')
|
||
except:
|
||
return ""
|
||
|
||
def parse_bcd_time(self, bcd):
|
||
"""解析 BCD 码时间(格式:YYMMDDHHMMSS)"""
|
||
try:
|
||
year = f"20{bcd[0:2]}" # 假设 19 表示 2019
|
||
month = bcd[2:4]
|
||
day = bcd[4:6]
|
||
hour = bcd[6:8]
|
||
minute = bcd[8:10]
|
||
second = bcd[10:12]
|
||
return f"{year}-{month}-{day}T{hour}:{minute}:{second}+08:00"
|
||
except:
|
||
return None
|
||
|
||
def parse_and_generate_sql(self, data):
|
||
"""解析报文并生成插入 SQL"""
|
||
hex_data = data[3].replace(" ", "") # 移除空格
|
||
pile_id = data[4] # 桩号
|
||
time = data[2].replace(" ", "T") + "+08:00" # 时间
|
||
|
||
# 提取基本字段
|
||
if len(hex_data) < 30: # 最小长度:14字节(HEX表示为28字符)+校验码(2)
|
||
logging.warning(f"数据包长度不足: {hex_data}")
|
||
return None
|
||
|
||
company = hex_data[0:4] # 4A58
|
||
if company != "4A58":
|
||
logging.warning(f"无效帧起始: {company}")
|
||
return None
|
||
|
||
cmd = hex_data[4:6] # 命令码
|
||
length_str = hex_data[24:28] # 数据域长度
|
||
length = int(length_str, 16) * 2 # HEX字符数
|
||
data_domain = hex_data[28:28 + length] if length > 0 else ""
|
||
|
||
# 从桩号提取信息
|
||
operator_id = pile_id[0:4] # 运营商编号
|
||
station_id = pile_id[6:12] # 站点编号
|
||
charger_no = pile_id[12:16] # 站内桩地址(作为充电桩编号)
|
||
|
||
# 初始化默认值
|
||
battery_bun_id = "unknown"
|
||
org_code = "unknown"
|
||
merchant_id = "unknown"
|
||
chg_run_status = 0 # 默认待机
|
||
chg_fau_lts = ""
|
||
charger_name = "unknown"
|
||
charger_power = 0.0
|
||
created_at = time
|
||
updated_at = time
|
||
battery_bun_no = 0
|
||
|
||
# 根据命令码解析数据域
|
||
if cmd == "0B": # 平台心跳
|
||
if len(data_domain) >= 12: # 6字节时间 + 1字节超时次数
|
||
time_str = data_domain[0:12] # BCD码时间
|
||
parsed_time = self.parse_bcd_time(time_str)
|
||
if parsed_time:
|
||
created_at = parsed_time
|
||
updated_at = parsed_time
|
||
chg_run_status = 0 # 心跳表示待机
|
||
|
||
elif cmd == "25": # 充电信息(表 3.9.9)
|
||
if len(data_domain) >= 92: # 确保数据域足够长
|
||
# 充电用户编号(字节 0-15,ASCII)
|
||
battery_bun_id = self.hex_to_ascii(data_domain[0:32])
|
||
# 充电电压(字节 16-17,HEX,单位 0.1V)
|
||
charger_power = int(data_domain[32:36], 16) / 10.0 # 电压作为功率(示例)
|
||
# 充电电流(字节 18-19,HEX,单位 0.1A)
|
||
current = int(data_domain[36:40], 16) / 10.0
|
||
# 充电电量(字节 20-21,HEX,单位 0.01kWh)
|
||
energy = int(data_domain[40:44], 16) / 100.0
|
||
# SOC(字节 22,HEX,单位 %)
|
||
soc = int(data_domain[44:46], 16)
|
||
# 故障状态(字节 23-26,HEX)
|
||
chg_fau_lts = data_domain[46:54]
|
||
# 充电状态(字节 27,HEX)
|
||
charge_status = int(data_domain[54:56], 16)
|
||
# 映射充电状态
|
||
if charge_status == 0:
|
||
chg_run_status = 0 # 待机
|
||
elif charge_status == 1:
|
||
chg_run_status = 1 # 充电中
|
||
elif charge_status == 2:
|
||
chg_run_status = 2 # 充电结束
|
||
elif charge_status == 3:
|
||
chg_run_status = 3 # 充电异常
|
||
else:
|
||
chg_run_status = 4 # 故障
|
||
|
||
else:
|
||
logging.info(f"未处理的命令码: {cmd}")
|
||
return None # 跳过未处理的命令
|
||
|
||
# 构建插入 SQL
|
||
sql = (
|
||
"INSERT INTO charge_jiuxing (charger_no, battery_bun_id, station_id, operator_id, org_code, "
|
||
"merchant_id, chg_run_status, chg_fau_lts, charger_name, charger_power, created_at, updated_at, battery_bun_no) VALUES "
|
||
f"('{charger_no}', '{battery_bun_id}', '{station_id}', '{operator_id}', '{org_code}', "
|
||
f"'{merchant_id}', {chg_run_status}, '{chg_fau_lts}', '{charger_name}', {charger_power}, "
|
||
f"'{created_at}', '{updated_at}', {battery_bun_no})"
|
||
)
|
||
return sql
|
||
|
||
def run(self):
|
||
"""启动程序"""
|
||
self.connect()
|
||
try:
|
||
while True:
|
||
pass # 保持程序运行
|
||
except KeyboardInterrupt:
|
||
self.mqtt_client.loop_stop()
|
||
self.mqtt_client.disconnect()
|
||
if self.tdengine_conn:
|
||
self.tdengine_conn.close()
|
||
logging.info("Program stopped")
|
||
|
||
if __name__ == "__main__":
|
||
parser = ChargeJiuxingParser(
|
||
mqtt_host="123.6.102.119",
|
||
mqtt_port=1883,
|
||
mqtt_username="emqx_test",
|
||
mqtt_password="emqx_test",
|
||
tdengine_url="http://localhost:6041", # 根据实际环境修改
|
||
tdengine_user="root",
|
||
tdengine_password="taosdata"
|
||
)
|
||
parser.run() |