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()