# -*- coding: utf-8 -*- # !/usr/bin/env python import binascii import datetime import logging import random import time from decimal import Decimal from apps.web.constant import Const, DeviceCmdCode, MQTT_TIMEOUT from apps.web.core.adapter.base import SmartBox, fill_2_hexByte from apps.web.core.exceptions import ServiceException from apps.web.core.networking import MessageSender from apps.web.dealer.models import Dealer from apps.web.device.models import Device from apps.web.core.device_define.kunyuanCar import DeviceParams, DEFAULT_VERSION, ORDER_VERSION from apps.web.user.models import ConsumeRecord logger = logging.getLogger(__name__) class KunYuanCar(SmartBox): """ 坤元模拟的昌原协议 """ @staticmethod def _trans_card(cardNo): """ 暂时不知道 坤元是怎么印制卡片数字的 涉及卡号转换的地方统一处理 """ if cardNo != DeviceParams.VOID_CARD_NO: return str(int(cardNo, 16)) return "" def _send_data(self, funCode, data, cmd=None, timeout=MQTT_TIMEOUT.NORMAL): if not cmd: cmd = DeviceCmdCode.OPERATE_DEV_SYNC result = MessageSender.send( device=self.device, cmd=cmd, payload={ "IMEI": self._device["devNo"], "funCode": funCode, "data": data }, timeout=timeout ) if result["rst"] != 0: if result['rst'] == -1: raise ServiceException({'result': 2, 'description': u'充电桩正在玩命找网络,请稍候再试'}) elif result['rst'] == 1: raise ServiceException({'result': 2, 'description': u'充电桩主板连接故障'}) else: raise ServiceException({'result': 2, 'description': u'系统错误'}) return result def _get_device_settings(self): """ 读取主板设置参数 新版本的读取指令 增加版本号以及端口类型 """ result = self._send_data("A0", "00") data = result["data"] isFree = data[8: 10] free = 0 if isFree == "00" else 1 electricPrice = int(data[10: 12], 16) / 100.0 maxConsume = int(data[12: 16], 16) / 100.0 maxChargeTime = int(data[16: 20], 16) volume = int(data[20: 22], 16) if data[6: 8] == "0B": version = str(int(data[22: 26], 16) / 100.0) port1 = "https://resource.washpayer.com/ky_port2.png" if data[26: 28] == "71" else "https://resource.washpayer.com/ky_port1.png" port2 = "https://resource.washpayer.com/ky_port2.png" if data[28: 30] == "71" else "https://resource.washpayer.com/ky_port1.png" else: version = str(DEFAULT_VERSION) port1 = "" port2 = "" return { "is_free": free, "electricPrice": electricPrice, "maxConsume": maxConsume, "maxChargeTime": maxChargeTime, "volume": volume, "version": version, "1": port1, "2": port2 } def _set_device_settings(self, conf): """ 设置主板参数 """ isFree = conf.pop("is_free", None) electricPrice = conf.pop("electricPrice", None) maxConsume = conf.pop("maxConsume", None) maxChargeTime = conf.pop("maxChargeTime", None) volume = conf.pop("volume", None) data = "" data += "AA" if int(isFree) == 1 else "00" data += fill_2_hexByte(hex(int(Decimal(electricPrice) * 100)), 2) data += fill_2_hexByte(hex(int(Decimal(maxConsume) * 100)), 4) data += fill_2_hexByte(hex(int(Decimal(maxChargeTime))), 4) data += fill_2_hexByte(hex(int(Decimal(volume))), 2) self._send_data("A1", data) def _get_consume_count(self, port=0): """ 获取主板的消费统计 和端口有关 :return: """ result = self._send_data("A2", "{:02X}".format(port)) data = result["data"] totalConsume = data[10: 16] # 总消费 totalElec = data[16: 22] # 总使用电量 cardConsume = data[22: 28] # 卡消费 return { "port": str(port), "totalConsume": int(totalConsume, 16) / 100.0, "totalElec": int(totalElec, 16) / 100.0, "cardConsume": int(cardConsume, 16) / 100.0 } def _clean_consume_count(self, port, consume=False, elec=False, cardConsume=False): """ 清除充电桩上累计信息 和端口有关 :param consume: :param elec: :param cardConsume: :return: """ if consume and elec and cardConsume: sendData = "E3" elif elec: sendData = "E2" elif cardConsume: sendData = "E4" elif consume: sendData = "E1" else: return sendData = "{}{}".format("{:02X}".format(port), sendData) self._send_data("A3", sendData) def _get_all_port_status(self): """ 获取全部端口状态 """ result = self._send_data("A5", "00") data = result["data"][8:-8] statusMap = dict() for _port in range(1, 1 + len(data) / 2): status = DeviceParams.STATUS_MAP.get(data[:2], Const.DEV_WORK_STATUS_IDLE) statusMap[str(_port)] = status data = data[2:] return statusMap def _get_device_status(self, port): """ 获取设备端口的运行状态 :return: """ result = self._send_data("A6", "{:02X}".format(port)) data = result["data"] status = data[10:12] cardNo = data[32:40] # 如果设备状态 result = { "port": str(port), "status": DeviceParams.STATUS_MAP.get(status, Const.DEV_WORK_STATUS_IDLE), "voltage": int(data[12:16], 16) / 1.0, "power": int(data[16:20], 16) / 1.0, "usedElec": int(data[20:24], 16) / 100.0, "usedTime": int(data[24:28], 16), "leftMoney": int(data[28:32], 16) / 100.0, "cardNo": KunYuanCar._trans_card(cardNo) } return result def _start(self, port, money): """ 微信支付命令 :param money: :return: """ sendData = "{:02X}{:04X}0000".format(port, int(money * 100)) result = self._send_data("A7", sendData, timeout=MQTT_TIMEOUT.START_DEVICE) data = result.get("data", dict()) if data[10:14] != "4F4B": raise ServiceException({'result': 2, 'description': u'设备启动失败,请联系设备老板'}) return result def _start_by_sequanceNo(self, port, money, sequanceNo): sendData = "{:02X}{:04X}{}".format(port, int(money * 100), sequanceNo) result = self._send_data("A7", sendData, timeout=MQTT_TIMEOUT.START_DEVICE) data = result.get("data", dict()) if data[10:14] != "4F4B": raise ServiceException({'result': 2, 'description': u'设备启动失败,请联系设备老板'}) return result def _set_device_disable(self, disable): """ 设置 设备的可用 :param disable: :return: """ status = "E9" if disable else "E8" result = self._send_data("A9", status) data = result["data"] if data[8: 12] != "4F4B": raise ServiceException({'result': 2, 'description': u'设置失败,请重试'}) def _stop(self, port): """ 停止 """ self._send_data("AF", "{:02X}".format(port)) def _restart_device(self): """ 重启 """ self._send_data("B0", "00") def _response_ack(self, ackMessage): """ 回复模块 使得模块停止ack 与主板无关 """ MessageSender.send( self.device, DeviceCmdCode.OPERATE_DEV_NO_RESPONSE, { "IMEI": self._device.devNo, "funCode": "FA", "data": "", "ack": ackMessage } ) def _response_card(self, card): """ 回复卡的余额 状态 """ sendData = "{:06X}".format(int(card.balance * 100)) if card.frozen: sendData += "AA" else: sendData += "00" self._send_data("C1", sendData, cmd=DeviceCmdCode.OPERATE_DEV_NO_RESPONSE) @staticmethod def _parse_event_AE(data): """ 枪把状态改变时候上报 """ port = int(data[8:10], 16) status = DeviceParams.CONNECT_STATUS_MAP.get(data[10:12], Const.DEV_WORK_STATUS_IDLE) return { "port": str(port), "status": status } @staticmethod def _parse_event_B1(data): """ 充电启动 """ try: return KunYuanCar._parse_event_B1_by_sequanceNo(data) except ValueError: port = int(data[8: 10], 16) cardNo = data[10: 18] money = int(data[18: 22], 16) / 100.0 return { "port": str(port), "cardNo": KunYuanCar._trans_card(cardNo), "money": money } @staticmethod def _parse_event_B1_by_sequanceNo(data): port = int(data[8: 10], 16) cardNo = data[10: 18] money = int(data[18: 22], 16) / 100.0 sequanceNo = data[22: 36] return { "port": str(port), "cardNo": KunYuanCar._trans_card(cardNo), "money": money, "sequanceNo": sequanceNo } @staticmethod def _parse_event_A8(data): """ 充电结束的时候上报 """ try: return KunYuanCar._parse_event_A8_by_sequanceNo(data) except ValueError: port = int(data[8: 10], 16) reasonCode = data[10:12] reason = DeviceParams.FINISH_REASON_MAP.get(reasonCode, u"未知结束方式") cardNo = data[12:20] usedElec = int(data[20:24], 16) / 100.0 usedTime = int(data[24:28], 16) leftMoney = int(data[28:32], 16) / 100.0 return { "port": str(port), "reason": reason, "cardNo": KunYuanCar._trans_card(cardNo), "usedElec": usedElec, "usedTime": usedTime, "leftMoney": leftMoney, } @staticmethod def _parse_event_A8_by_sequanceNo(data): port = int(data[8: 10], 16) reasonCode = data[10:12] reason = DeviceParams.FINISH_REASON_MAP.get(reasonCode, u"未知结束方式") cardNo = data[12:20] usedElec = int(data[20:24], 16) / 100.0 usedTime = int(data[24:28], 16) leftMoney = int(data[28:32], 16) / 100.0 sequanceNo = data[42: 56] return { "port": str(port), "reason": reason, "cardNo": KunYuanCar._trans_card(cardNo), "usedElec": usedElec, "usedTime": usedTime, "leftMoney": leftMoney, "sequanceNo": sequanceNo } @staticmethod def _parse_event_B2(data): """ 解析状态上报 """ try: return KunYuanCar._parse_event_B2_by_sequanceNo(data) except ValueError: port = int(data[8: 10], 16) leftBalance = int(data[10:14], 16) / 100.0 usedElec = int(data[14:18], 16) / 100.0 temperature = int(data[20:22], 16) power = int(data[22:26], 16) voltage = int(data[30:34], 16) temperature = temperature if data[18:20] == "00" else -temperature return { "port": str(port), "leftBalance": leftBalance, "usedElec": usedElec, "temperature": u"{}度 更新于{}".format(temperature, datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")), "power": power, "voltage": voltage, } @staticmethod def _parse_event_B2_by_sequanceNo(data): port = int(data[8: 10], 16) leftBalance = int(data[10:14], 16) / 100.0 usedElec = int(data[14:18], 16) / 100.0 temperature = int(data[20:22], 16) power = int(data[22:26], 16) voltage = int(data[30:34], 16) temperature = temperature if data[18:20] == "00" else -temperature sequanceNo = data[20: 34] return { "port": str(port), "leftBalance": leftBalance, "usedElec": usedElec, "temperature": u"{}度 更新于{}".format(temperature, datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")), "power": power, "voltage": voltage, "sequanceNo": sequanceNo } @staticmethod def _parse_event_C1(data): """ 在线卡查询卡费 """ cardNo = data[8: 16] return { "cardNo": KunYuanCar._trans_card(cardNo), } @staticmethod def _parse_event_C2(data): """ 解析 同步参数指令 """ return {} @staticmethod def _parse_event_C3(data): """ 解析 同步参数指令 """ port = int(data[8: 10], 16) faultCode = data[10: 14] return { "port": port, } @staticmethod def _parse_event_A0(data): if data[6: 8] == "0B": version = str(int(data[22: 26], 16) / 100.0) else: version = str(DEFAULT_VERSION) return { "version": version } @property def isHaveStopEvent(self): """ 是否有结束事件 """ return True def test(self, coins): return self._start(1, coins) def get_dev_setting(self): """ 获取参数 """ settings = self._get_device_settings() otherConf = self.device.get("otherConf", dict()) disableDevice = otherConf.get("disableDevice", DeviceParams.DEFAULT_DISABLE_DEVICE) settings.update({"disableButton": disableDevice}) Device.objects.filter(devNo=self.device.devNo).update(otherConf=otherConf) Device.invalid_device_cache(self.device.devNo) consume = self._get_consume_count() settings.update(consume) devCache = Device.get_dev_control_cache(self.device.devNo) temperature = devCache.get("temperature", "暂无数据") settings.update({"temperature": temperature}) return settings def set_device_function_param(self, request, lastSetConf): """ 设置设备参数 """ disable = request.POST.get("disableButton") if disable is not None: dealer = Dealer.objects.filter(id=self.device.ownerId).first() if "dealerDisableDevice" not in dealer.features: raise ServiceException({"result": 2, "description": "抱歉,您无此操作权限,请联系厂家获取权限"}) return self.set_dev_disable(bool(int(disable))) newDict = { "is_free": request.POST.get("is_free", lastSetConf["is_free"]), "electricPrice": request.POST.get("electricPrice", lastSetConf["electricPrice"]), "maxConsume": request.POST.get("maxConsume", lastSetConf["maxConsume"]), "maxChargeTime": request.POST.get("maxChargeTime", lastSetConf["maxChargeTime"]), "volume": request.POST.get("volume", lastSetConf["volume"]) } self._set_device_settings(newDict) def set_dev_disable(self, disable): """ 禁用设备 """ self._set_device_disable(disable) def set_device_function(self, request, lastSetConf): """ 设置设备开关 """ clearTotalConsume = request.POST.get("clearTotalConsume", False) clearTotalElec = request.POST.get("clearTotalElec", False) reboot = request.POST.get("reboot", False) if reboot: self._restart_device() # 清除设备计费信息 if any([clearTotalConsume, clearTotalElec]): self._clean_consume_count(clearTotalConsume, clearTotalElec) def stop(self, port=None): """ 停止设备 """ return self._stop(int(port)) def get_port_status_from_dev(self): """从设备端更新端口的状态""" result = self._get_all_port_status() portDict = dict() for _port, _status in result.items(): portDict[_port] = {"status": _status} allPorts, usedPorts, usePorts = self.get_port_static_info(portDict) portDict.update({"allPorts": allPorts, "usedPorts": usedPorts, "usePorts": usePorts}) Device.update_dev_control_cache(self.device.devNo, portDict) return portDict def get_port_status(self, force=False): """ 李成坤强制要求 """ # if force: portDict = self.get_port_status_from_dev() devSet = self._get_device_settings() statusMap = dict() for _port, _item in portDict.items(): if isinstance(_port, (unicode, str)) and _port.isdigit(): statusMap[_port] = {'status': _item.get("status", 0), "img": devSet.get(_port, "")} return statusMap def check_dev_status(self, attachParas=None): """ 检查设备状态 """ chargeIndex = attachParas.get("chargeIndex") result = self.get_port_status(force=True) if result[str(chargeIndex)].get("status", 0) != Const.DEV_WORK_STATUS_CONNECTED: raise ServiceException({"result": 2, "description": u"请先连接设备与充电枪"}) def analyze_event_data(self, data): """ 解析数据 """ cmdCode = data[4:6] funcName = "_parse_event_{}".format(cmdCode) func = getattr(self.__class__, funcName, None) if func and callable(func): eventData = func(data) or dict() eventData.update({"cmdCode": cmdCode}) return eventData def start_device(self, package, openId, attachParas): """ 启动设备 """ chargeIndex = attachParas["chargeIndex"] portStatus = Device.get_port_control_cache(self.device.devNo, port=str(chargeIndex)) if portStatus.get("status") != Const.DEV_WORK_STATUS_CONNECTED: self.get_port_status_from_dev() portStatus = Device.get_port_control_cache(self.device.devNo, port=str(chargeIndex)) if portStatus.get("status") != Const.DEV_WORK_STATUS_CONNECTED: raise ServiceException({'result': 2, 'description': u'为了充电安全,请先连接设备与充电枪'}) coins = package.get("coins") price = package.get("price") orderNo = attachParas.get("orderNo") version = self._get_device_settings().get("version", str(DEFAULT_VERSION)) portCache = { "openId": openId, "isStart": True, "coins": coins, "price": price, "status": Const.DEV_WORK_STATUS_WORKING, "startTime": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "orderNo": orderNo, "version": version } if version > str(ORDER_VERSION): # sequanceNo 表示为时间戳 4个字节 + 端⼝号1个字节 + 随机数 2个字节 共7个字节16进制 sequanceNo = "{:08X}{:02X}{:04X}".format(int(time.time()), int(chargeIndex), random.randint(1000, 9999)) order = ConsumeRecord.objects.get(orderNo=orderNo) order.update(sequanceNo=sequanceNo) result = self._start_by_sequanceNo(int(chargeIndex), price, sequanceNo) else: result = self._start(int(chargeIndex), price) Device.update_dev_control_cache(self.device.devNo, {chargeIndex: portCache}) result["finishedTime"] = int(time.time()) + 24 * 60 * 60 return result def dealer_get_port_status(self): """ 经销商获取端口状态 """ return self.get_port_status() def get_port_info(self, port): """ 获取端口信息 """ result = self._get_device_status(int(port)) Device.update_dev_control_cache(self.device.devNo, result) devCache = Device.get_dev_control_cache(self.device.devNo) or dict() return devCache def active_deactive_port(self, port, active): """ 停用激活端口 """ if not active: self._stop(port) else: super(KunYuanCar, self).active_deactive_port(port, active)