335 lines
15 KiB
Python
335 lines
15 KiB
Python
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() |