335 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import taosrest
import psycopg2
from datetime import datetime
import binascii
import logging
import time
# 配置日志
logging.basicConfig(
filename='charge_gun.log',
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
encoding='utf-8'
)
class ChargeGunMigrator:
def __init__(self):
# TDengine连接参数
self.tdengine_config = {
'host': '123.6.102.119',
'port': 6041,
'user': 'readonly_user',
'password': 'Aassword123',
'database': 'antsev'
}
# PostgreSQL连接参数
self.pg_config = {
'host': '123.6.102.119',
'port': 5432,
'database': 'tms-design',
'user': 'postgres',
'password': '687315e66ae24eeab8bb5c0441a40d79'
}
self.td_conn = None
self.td_cursor = None
self.pg_conn = None
self.pg_cursor = None
self.last_processed_ts = None
self.processed_connectors = set()
def connect(self):
"""建立与两个数据库的连接"""
max_retries = 3
retry_delay = 10 # 秒
for attempt in range(max_retries):
try:
# 连接到TDengine
logging.info(f"尝试连接到TDengine (第 {attempt + 1} 次): {self.tdengine_config}")
rest_url = f"http://{self.tdengine_config['host']}:{self.tdengine_config['port']}"
self.td_conn = taosrest.connect(
url=rest_url,
user=self.tdengine_config['user'],
password=self.tdengine_config['password'],
database=self.tdengine_config['database']
)
self.td_cursor = self.td_conn.cursor()
logging.info("成功连接到TDengine")
# 测试查询以验证连接
self.td_cursor.execute("SELECT SERVER_VERSION()")
version = self.td_cursor.fetchone()
logging.info(f"TDengine 服务器版本: {version[0]}")
# 连接到PostgreSQL
logging.info(f"尝试连接到PostgreSQL: {self.pg_config}")
self.pg_conn = psycopg2.connect(
host=self.pg_config['host'],
port=self.pg_config['port'],
database=self.pg_config['database'],
user=self.pg_config['user'],
password=self.pg_config['password']
)
self.pg_conn.autocommit = True
self.pg_cursor = self.pg_conn.cursor()
logging.info("成功连接到PostgreSQL")
break # 连接成功,退出重试循环
except Exception as e:
logging.error(f"连接错误 (第 {attempt + 1} 次): {str(e)}")
if attempt < max_retries - 1:
logging.info(f"将在 {retry_delay} 秒后重试...")
time.sleep(retry_delay)
else:
raise
def parse_hex_data(self, hex_data):
"""根据协议解析十六进制数据"""
try:
# 移除空格并将十六进制字符串转换为字节
hex_bytes = bytes.fromhex(hex_data.replace(" ", ""))
# 验证帧起始(应该是"JX"
if hex_bytes[0:2] != b'JX':
return None
# 提取命令
command = hex_bytes[2:3].hex().upper()
# 提取枪号假设在协议中枪号位于第11字节0x01表示A枪0x02表示B枪
connector_name = 'A' if hex_bytes[11] == 0x01 else 'B'
# 初始化数据字典
data = {
'command': command,
'connector_name': connector_name,
'status': 0, # 默认空闲
'power': None,
'voltage_upper_limits': None,
'voltage_lower_limits': None,
'park_status': 0, # 默认空闲
'lock_status': 0 # 默认未锁
}
# 25H - 充电信息
if command == '25':
# 充电电压字节7-8分辨率0.1V
voltage = int.from_bytes(hex_bytes[7:9], byteorder='little') * 0.1
# 充电电量字节11-14分辨率0.01kWh
power = int.from_bytes(hex_bytes[11:15], byteorder='little') * 0.01
data.update({
'status': 1, # 充电中
'power': power,
'voltage_upper_limits': int(voltage + 50), # 假设上限比当前电压高50V
'voltage_lower_limits': int(voltage - 50) # 假设下限比当前电压低50V
})
# 23H - 最新充电订单
elif command == '23':
# 起始充电电量字节119-122分辨率0.01kWh
start_power = int.from_bytes(hex_bytes[119:123], byteorder='little') * 0.01
# 结束充电电量字节123-126分辨率0.01kWh
end_power = int.from_bytes(hex_bytes[123:127], byteorder='little') * 0.01
# 计算总电量
power = end_power - start_power
data.update({
'status': 0, # 充电完成,枪状态为空闲
'power': power
})
return data
except Exception as e:
logging.error(f"解析十六进制数据时出错: {str(e)}")
return None
def migrate_data(self):
"""将新数据从TDengine迁移到PostgreSQL的charge_gun表"""
while True:
try:
# 如果last_processed_ts为空初始化为当前时间
if self.last_processed_ts is None:
try:
# 避免使用 MAX(ts),改用 ORDER BY ts DESC LIMIT 1 获取最新时间戳
self.td_cursor.execute("SELECT ts FROM antsev.charge_jiuxing ORDER BY ts DESC LIMIT 1")
result = self.td_cursor.fetchone()
self.last_processed_ts = result[0] if result and result[0] else datetime.now()
except Exception as e:
logging.error(f"获取最新时间戳失败: {str(e)},使用当前时间作为默认值")
self.last_processed_ts = datetime.now()
logging.info(f"初始化last_processed_ts: {self.last_processed_ts}")
# 查询新数据
query = f"SELECT * FROM antsev.charge_jiuxing WHERE ts > '{self.last_processed_ts}' ORDER BY ts"
self.td_cursor.execute(query)
rows = self.td_cursor.fetchall()
if not rows:
logging.info("没有新数据休眠10秒")
time.sleep(10)
continue
for row in rows:
try:
# 从TDengine行中提取数据
timestamp = row[0] # 时间戳
pile_id = row[3] # 充电桩ID (pile_id)
hex_data = row[12] # 十六进制数据 (hex_data)
# 记录原始数据
logging.info(f"处理记录: ts={timestamp}, pile_id={pile_id}, hex_data={hex_data}")
# 解析十六进制数据
parsed_data = self.parse_hex_data(hex_data)
if not parsed_data:
logging.warning(f"无法解析 hex_data: {hex_data},跳过此记录")
continue
# 构造唯一标识pile_id + connector_name
connector_key = f"{pile_id}_{parsed_data['connector_name']}"
# 检查记录是否已存在
check_query = """
SELECT 1 FROM charge_gun WHERE pile_id = %s AND connector_name = %s
"""
self.pg_cursor.execute(check_query, (pile_id, parsed_data['connector_name']))
exists = self.pg_cursor.fetchone() is not None
# 如果该充电枪已处理过或记录已存在,更新状态
update_existing = exists or connector_key in self.processed_connectors
# 准备插入或更新PostgreSQL的数据
if not update_existing:
insert_query = """
INSERT INTO charge_gun (
pile_id, connector_name, connector_type, voltage_upper_limits,
voltage_lower_limits, power, park_no, national_standard,
status, park_status, lock_status, created_at, updated_at,
entity_id, station_id, operator_id, sum_period, org_code, merchant_id
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
"""
else:
insert_query = """
UPDATE charge_gun
SET status = %s,
power = %s,
voltage_upper_limits = %s,
voltage_lower_limits = %s,
updated_at = %s
WHERE pile_id = %s AND connector_name = %s
"""
values = (
pile_id,
parsed_data['connector_name'],
4, # connector_type: 直流充电桩
parsed_data['voltage_upper_limits'] if parsed_data['voltage_upper_limits'] else 1000,
parsed_data['voltage_lower_limits'] if parsed_data['voltage_lower_limits'] else 200,
parsed_data['power'],
'1', # park_no: 默认值
2, # national_standard: 2015标准
parsed_data['status'],
parsed_data['park_status'],
parsed_data['lock_status'],
timestamp,
timestamp,
'default_entity',
'default_station',
'K1TUBMOLH',
0,
'MAD2BYGQX',
'1863849140684009473'
) if not update_existing else (
parsed_data['status'],
parsed_data['power'],
parsed_data['voltage_upper_limits'] if parsed_data['voltage_upper_limits'] else 1000,
parsed_data['voltage_lower_limits'] if parsed_data['voltage_lower_limits'] else 200,
timestamp,
pile_id,
parsed_data['connector_name']
)
self.pg_cursor.execute(insert_query, values)
self.processed_connectors.add(connector_key)
logging.info(f"{'更新' if update_existing else '插入'}充电桩 {pile_id} 的记录,枪号 {parsed_data['connector_name']}")
# 记录插入或更新的完整数据
if not update_existing:
log_values = {
'pile_id': pile_id,
'connector_name': parsed_data['connector_name'],
'connector_type': 4,
'voltage_upper_limits': parsed_data['voltage_upper_limits'] if parsed_data['voltage_upper_limits'] else 1000,
'voltage_lower_limits': parsed_data['voltage_lower_limits'] if parsed_data['voltage_lower_limits'] else 200,
'power': parsed_data['power'],
'park_no': '1',
'national_standard': 2,
'status': parsed_data['status'],
'park_status': parsed_data['park_status'],
'lock_status': parsed_data['lock_status'],
'created_at': timestamp,
'updated_at': timestamp,
'entity_id': 'default_entity',
'station_id': 'default_station',
'operator_id': 'K1TUBMOLH',
'sum_period': 0,
'org_code': 'MAD2BYGQX',
'merchant_id': '1863849140684009473'
}
else:
log_values = {
'pile_id': pile_id,
'connector_name': parsed_data['connector_name'],
'status': parsed_data['status'],
'power': parsed_data['power'],
'voltage_upper_limits': parsed_data['voltage_upper_limits'] if parsed_data['voltage_upper_limits'] else 1000,
'voltage_lower_limits': parsed_data['voltage_lower_limits'] if parsed_data['voltage_lower_limits'] else 200,
'updated_at': timestamp
}
logging.info(f"{'插入' if not update_existing else '更新'}到 charge_gun 表的数据: {log_values}")
# 更新last_processed_ts
self.last_processed_ts = max(self.last_processed_ts, timestamp)
except Exception as e:
logging.error(f"处理时间戳为 {timestamp} 的记录时出错: {str(e)}")
continue
except Exception as e:
logging.error(f"迁移过程中出错: {str(e)}")
time.sleep(10) # 出错后休眠10秒后重试
def close(self):
"""关闭数据库连接"""
try:
if self.td_cursor:
self.td_cursor.close()
if self.td_conn:
self.td_conn.close()
if self.pg_cursor:
self.pg_cursor.close()
if self.pg_conn:
self.pg_conn.close()
logging.info("数据库连接已关闭")
except Exception as e:
logging.error(f"关闭连接时出错: {str(e)}")
raise
def run(self):
"""运行迁移的主方法"""
try:
self.connect()
self.migrate_data()
except Exception as e:
logging.error(f"迁移失败: {str(e)}")
raise
finally:
self.close()
if __name__ == "__main__":
migrator = ChargeGunMigrator()
migrator.run()