# -*- coding: utf-8 -*- # 超辰充电桩业务流程描述 # 整个业务比较简单,算是经典的充电桩业务对接,但有以下问题需要注意 # 1. 超辰的通信方式为485通信,485通信中有一项为通信地址,即板位地址,属于硬件开关 # 2. 该板位地址经多次与该技术方沟通,确定了以下的规则: # 2.1 有端口操作指令的时候,该板位地址使用端口号来代替,如操作10号端口开启,则板位地址数据为0A # 2.2 无端口指令操作的时候,该板位地址 固定为01,例如参数获取以及参数设置等不涉及端口的指令 # 3. 超辰充电桩指令目前看起来是不完备的,缺少了一条 查看当前充电方式的指令,原因如下 # 3.1 充电模式分为 电量计费 以及 时间计费, 该模式可远程切换 # 3.2 切换模式后,主板结束的时候上报的指令数据含义不一样,电量模式下需要使用 EA 指令结算退款 时间模式下 使用C9指令退款 # 3.2 由于服务器无法知道此时主板到底处于什么计费方式下,所以只能以经销商上一次设置的 充电模式为准,主板默认出厂 模式为时间模式 # 3.3 所以服务器结束的时候需要读取当前的充电方式 默认也为时间模式 # 4. 超辰寄过来打样的主板 是10路,实际有2 路 4 路 以及 10路的,2路4路没有实际测试,但是姚总坚持认为三个主板协议一样。 import logging import random import time from apilib.utils_datetime import timestamp_to_dt from apps.web.common.transaction import UserConsumeSubType from apps.web.constant import DeviceCmdCode, MQTT_TIMEOUT, Const from apps.web.core.adapter.base import SmartBox from apps.web.core.exceptions import ServiceException from apps.web.core.networking import MessageSender from apps.web.device.models import Device from apps.web.user.models import ConsumeRecord, MyUser logger = logging.getLogger(__name__) class ChaoChenBox(SmartBox): DEFAULT_CHARGE_TYPE = "time" DEFAULT_FULL_STOP = 0 def _send_data(self, funCode, content, cmd=None, timeout=MQTT_TIMEOUT.NORMAL): if cmd is None: cmd = DeviceCmdCode.OPERATE_DEV_SYNC result = MessageSender.send( device=self.device, cmd=cmd, payload={ "IMEI": self._device["devNo"], "funCode": funCode, "data": content }, timeout=timeout ) if result.get("rst") == -1: raise ServiceException({'result': 2, 'description': u'充电桩正在玩命找网络,请稍候再试'}) elif result.get("rst") == 1: raise ServiceException({'result': 2, 'description': u'充电桩串口故障,请稍后再试'}) elif result.get("rst") == 0: # data = result.get("data", "") # if data[10: 12] != "01": # raise ServiceException({'result': 2, 'description': u'执行命令失败,请稍后再试'}) return result else: raise ServiceException({'result': 2, 'description': u'系统错误'}) def _start_device(self, portStr, orderNo, money, leftMoney): if not leftMoney: leftMoney = 0x000000 orderHex = "{:016X}".format(int(orderNo)) moneyHex = "{:06X}".format(int(float(money) * 100)) leftMoneyHex = "{:06X}".format(int(float(leftMoney) * 100)) result = self._send_data( funCode="{:02X}A7".format(int(portStr)), content=orderHex + moneyHex + leftMoneyHex ) return result def _stop(self, portStr): result = self._send_data( funCode="{:02X}D6".format(int(portStr)), content="01" ) return result @property def poweroffTime(self): """ TODO 设置不起作用""" result = self._send_data("01B2", "01") return int(result["data"][12: 20], 16) @property def curElec(self): result = self._send_data("01B3", "01") return int(result['data'][-4:-2], 16) / 1000.0 @property def standElec(self): result = self._send_data("01C3", "01") data = result["data"] m = int(data[14: 16], 16) / 10.0 s = int(data[12: 14], 16) / 10.0 return { "maxElec": m, "standElec": s } @property def totalCoinConsume(self): result = self._send_data("01C1", "01") return int(result["data"][12: 18], 16) / 100.0 @property def totalCardConsume(self): result = self._send_data("01C2", "01") return int(result["data"][12: 18], 16) / 100.0 @property def emptyTime(self): result = self._send_data("01E6", "01") return int(result["data"][12: 16], 16) @property def emptyPower(self): result = self._send_data("01E4", "01") return int(result["data"][12: 16], 16) # ------这三个暂时设置为只读属性------------------ @poweroffTime.setter def poweroffTime(self, value): self._send_data("01B1", "{:08X}".format(int(value))) @emptyPower.setter def emptyPower(self, value): self._send_data("01E3", "{:04X}".format(int(value))) @emptyTime.setter def emptyTime(self, value): self._send_data("01E5", "{:04X}".format(int(value))) @standElec.setter def standElec(self, value): standElec = value['standElec'] maxElec = value['maxElec'] dataHex = "{:0>2X}{:0>2X}".format(int(float(standElec) * 10), int(float(maxElec) * 10)) self._send_data("01A4", dataHex) # ----------浮充功率不需要 主板回复也不对--------- @property def fuChongPower(self): # TODO 未按协议格式回执报文 # result = self._send_data("01D2", "01") # data = result["data"] # return int(data[12: 16], 16) return @fuChongPower.setter def fuChongPower(self, value): # self._send_data("01D1", "{:04X}".format(int(value))) pass # -------------------------------------------- def _set_recover(self): self._send_data("01F5", "01") def _set_clear_line_time(self): raise ServiceException({"result": "2", "description": u"暂不支持此项功能"}) def _set_reboot(self): self._send_data("01D7", "01") @property def chargeType(self): """ 该主板没有获取充电模式的 接口""" return self.device.get("otherConf", dict()).get("chargeType", self.DEFAULT_CHARGE_TYPE) @chargeType.setter def chargeType(self, value): if value == "time": result = self._send_data("01E8", "00") else: result = self._send_data("01E8", "01") resultCode = result['data'][10:12] if resultCode == "00": raise ServiceException({"result": 2, "description": u"切换模式失败,请等待所有充电订单结束<{}>".format(resultCode)}) if resultCode == "02": raise ServiceException({"result": 2, "description": u"切换模式失败,<{}>".format(resultCode)}) otherConf = self.device.get("otherConf", dict()) otherConf['chargeType'] = value Device.objects.filter(devNo=self.device.devNo).update(otherConf=otherConf) Device.invalid_device_cache(self.device.devNo) @property def stepPower(self): result = self._send_data("01F2", "01") data = result['data'][12: -2] step = dict() for _index in range(0, 24, 6): # step.append( # {"power": int(data[_index: _index+4], 16), "discount": int(data[_index+4: _index+6], 16)} # ) step["power{}".format(_index/6 + 1)] = int(data[_index: _index+4], 16) step["discount{}".format(_index/6 + 1)] = int(data[_index+4: _index+6], 16) return step @stepPower.setter def stepPower(self, value): content = "" for _item in value: content += "{:04X}{:02X}".format(int(_item["power"]), int(_item["discount"])) self._send_data("01F1", content) @property def timeRule(self): result = self._send_data("01B8", "01") mobilePrice = int(result["data"][12: 16], 16) mobileTime = int(result["data"][16: 20], 16) mobileOnsale = int(result["data"][20: 22], 16) icPrice = int(result["data"][22: 26], 16) icTime = int(result["data"][26: 30], 16) coinPrice = int(result["data"][30: 34], 16) coinTime = int(result["data"][34: 38], 16) return { "timePrice": mobilePrice, "timeNum": mobileTime, "timeOnsale": mobileOnsale, "mobilePrice": mobilePrice, "mobileTime": mobileTime, "mobileOnsale": mobileOnsale, "icPrice": icPrice, "icTime": icTime, "coinPrice": coinPrice, "coinTime": coinTime } @timeRule.setter def timeRule(self, value): mobilePrice = value["mobilePrice"] mobileTime = value["mobileTime"] mobileOnsale = value["mobileOnsale"] icPrice = value["icPrice"] icTime = value["icTime"] coinPrice = value['coinPrice'] coinTime = value['coinTime'] self._send_data( "01B7", "{:04X}{:04X}{:02X}{:04X}{:04X}{:04X}{:04X}".format(int(mobilePrice), int(mobileTime), int(mobileOnsale), int(icPrice), int(icTime), int(coinPrice), int(coinTime)) ) @property def elecRule(self): result = self._send_data("01F7", "01") elecPrice = int(result['data'][12:16], 16) elecNum = int(result['data'][16:20], 16) / 100.0 elecOnsale = int(result['data'][20:22], 16) return {"elecPrice": elecPrice, "elecNum": elecNum, "elecOnsale": elecOnsale} @elecRule.setter def elecRule(self, value): """设置电量 前台过来单位为度 发送主板需要修改为0.1度""" elecPrice = value["elecPrice"] elecNum = value["elecNum"] elecOnsale = value["elecOnsale"] self._send_data( "01F8", "{:04X}{:04X}{:02X}".format(int(elecPrice), int(elecNum)*100, int(elecOnsale)) ) def _get_all_status(self): result = self._send_data("01ED", "0100") portCount = int(result["data"][10: 12], 16) portData = result["data"][12: 12+portCount*4] portDict = dict() for _i in range(portCount): _port = str(int(portData[_i*4: _i*4+2], 16)) _status = portData[_i*4+2: _i*4+4] if _status == "01": _tStatus = Const.DEV_WORK_STATUS_IDLE elif _status == "02": _tStatus = Const.DEV_WORK_STATUS_WORKING elif _status == "03": _tStatus = Const.DEV_WORK_STATUS_FORBIDDEN elif _status == "04": _tStatus = Const.DEV_WORK_STATUS_FAULT else: _tStatus = Const.DEV_WORK_STATUS_FAULT portDict[_port] = {"status": _tStatus} return portDict def _get_port_info(self, port): # TODO 电流单位未知 result = self._send_data("{:02X}EF".format(int(port)), "00") _status = result["data"][12: 14] if _status == "01": status = Const.DEV_WORK_STATUS_IDLE elif _status == "02": status = Const.DEV_WORK_STATUS_WORKING elif _status == "03": status = Const.DEV_WORK_STATUS_FORBIDDEN elif _status == "04": status = Const.DEV_WORK_STATUS_FAULT else: status = Const.DEV_WORK_STATUS_FAULT left = int(result["data"][14: 18], 16) power = int(result["data"][18: 22], 16) / 10.0 otherConf = self.device.get("otherConf", dict()) chargeMode = otherConf.get("chargeType", self.DEFAULT_CHARGE_TYPE) if chargeMode == "elec": left /= 100.0 return { "status": status, "power": power, "left{}".format(chargeMode.capitalize()): left } def _get_port_elec(self): """获取端口实时用电量""" result = self._send_data("01E9", "01") data = result['data'][12: -2] for _index in range(0, len(data), 4): tUsedElec = int(data[_index: _index+4], 16) Device.update_dev_control_cache(self.device.devNo, {str(_index/4+1): {"usedElec": tUsedElec}}) @staticmethod def _parse_event_C9(data): """ :param data: :return: """ # TODO zjl 这个订单序号有什么用 portStr = str(int(data[2: 4], 16)) orderNo = str(int(data[10:26], 16)) serialNo = str(int(data[26:30], 16)) usedCoins = str(int(data[30:36], 16) / 100.0) usedTime = str(int(data[36: 40], 16)) leftTime = str(int(data[40:48], 16)) return { 'portStr': portStr, 'orderNo': orderNo, 'serialNo': serialNo, 'usedCoins': usedCoins, 'usedTime': usedTime, 'leftTime': leftTime } @staticmethod def _parse_event_C8(data): # TODO zjl 目前没有刷卡消费的订单 事分缓急 先不做 return {} @staticmethod def _parse_event_C4(data): # TODO 故障码 55-255 portStr = str(int(data[2: 4], 16)) status = int(data[10:12], 16) return { "status": status, "portStr": portStr } @staticmethod def _parse_event_B5(data): # TODO 测试 感觉单位像是分钟而不是秒 port = int(data[2:4], 16) serialNo = int(data[10:14], 16) usedCoins = str(int(data[14:22], 16) / 100.0) usedTime = str(int(data[22:30], 16)) finishedStamp = str(int(data[30:38], 16)) return { 'portStr': port, 'serialNo': serialNo, 'usedCoins': usedCoins, 'usedTime': usedTime, 'finishedStamp': finishedStamp } @staticmethod def _parse_event_B6(data): port = int(data[2:4], 16) serialNo = int(data[10:14], 16) cardNo = data[14:24] usedCoins = str(int(data[24:32], 16) / 100.0) balance = str(int(data[32:40], 16) / 100.0) usedTime = str(int(data[40:48], 16)) finishedStamp = str(int(data[48:56], 16)) return { 'portStr': port, 'serialNo': serialNo, 'cardNo': cardNo, 'usedCoins': usedCoins, 'balance': balance, 'usedTime': usedTime, 'finishedStamp': finishedStamp } @staticmethod def _parse_event_EA(data): portStr = str(int(data[2: 4], 16)) reasonCode = int(data[10:12], 16) leftElec = int(data[12:16], 16) / 100.0 return { "reasonCode": reasonCode, "leftElec": leftElec, "portStr": portStr } @staticmethod def _parse_event_F0(data): totalElec = int(data[10:14], 16) / 1000.0 return {"totalElec": totalElec} @staticmethod def _parse_event_F9(data): port = int(data[2:4], 16) cardNo = int(data[10:18], 16) consumeMoney = round(int(data[19:22], 16) * 0.01, 2) return {"port": port, "cardNo": cardNo, "consumeMoney": consumeMoney} @staticmethod def _parse_event_FA(data): port = int(data[2:4], 16) cardNo = int(data[10:18], 16) refundMoney = round(int(data[19:22], 16) * 0.01, 2) return {"port": port, "cardNo": cardNo, "refundMoney": refundMoney} def test(self, coins): orderNo = random.randint(10000, 99999) return self._start_device("1", orderNo, coins, coins) def get_device_function_by_key(self, keyName): if "poweroffTime" in keyName: return {"poweroffTime": self.poweroffTime} if "curElec" in keyName: return {"curElec": self.curElec} if "totalCoinConsume" in keyName: return {"totalCoinConsume": self.totalCoinConsume} if "totalCardConsume" in keyName: return {"totalCardConsume": self.totalCardConsume} if "emptyTime" in keyName: return {"emptyTime": self.emptyTime} if "emptyPower" in keyName: return {"emptyPower": self.emptyPower} if "consumeMode" in keyName: return {"consumeMode": self.chargeType} if "mobilePrice" in keyName: return self.timeRule if "elecPrice" in keyName: return self.elecRule if "power1" in keyName: return self.stepPower if "standElec" in keyName: return self.standElec return {} def set_device_function(self, request, lastSetConf): if "recover" in request.POST: self._set_recover() if "clearLineTime" in request.POST: self._set_clear_line_time() if "reboot" in request.POST: self._set_reboot() def set_device_function_param(self, request, lastSetConf): if "consumeMode" in request.POST: self.chargeType = request.POST.get("consumeMode") if "mobilePrice" in request.POST: mobilePrice = request.POST.get("mobilePrice") mobileTime = request.POST.get("mobileTime") mobileOnsale = request.POST.get("mobileOnsale") icPrice = request.POST.get("icPrice") icTime = request.POST.get("icTime") coinPrice = request.POST.get("coinPrice") coinTime = request.POST.get("coinTime") self.timeRule = {"mobilePrice": mobilePrice, "mobileTime": mobileTime, "mobileOnsale": mobileOnsale, "icPrice": icPrice, "icTime": icTime, "coinPrice": coinPrice, "coinTime": coinTime} if "elecPrice" in request.POST: elecPrice = request.POST.get("elecPrice") elecNum = request.POST.get("elecNum") elecOnsale = request.POST.get("elecOnsale") self.elecRule = {"elecPrice": elecPrice, "elecNum": elecNum, "elecOnsale": elecOnsale} if "power1" in request.POST: step = list() for _index in range(1, 5): step.append({"power": request.POST['power{}'.format(_index)], "discount": request.POST["discount{}".format(_index)]}) self.stepPower = step if "standElec" in request.POST or "maxElec" in request.POST: standElec = request.POST.get("standElec") maxElec = request.POST.get("maxElec") self.standElec = {"standElec": standElec, "maxElec": maxElec} if "poweroffTime" in request.POST: poweroffTime = request.POST.get("poweroffTime") self.poweroffTime = poweroffTime if "emptyTime" in request.POST: emptyTime = request.POST.get("emptyTime") self.emptyTime = emptyTime if "emptyPower" in request.POST: emptyPower = request.POST.get("emptyPower") self.emptyPower = emptyPower def get_port_status_from_dev(self): result = self._get_all_status() allPorts, usedPorts, usePorts = self.get_port_static_info(result) Device.update_dev_control_cache( self._device['devNo'], {'allPorts': allPorts, 'usedPorts': usedPorts, 'usePorts': usePorts} ) ctrInfo = Device.get_dev_control_cache(self._device['devNo']) for strPort, info in result.items(): if strPort in ctrInfo: ctrInfo[strPort].update({'status': info['status']}) else: ctrInfo[strPort] = info Device.update_dev_control_cache(self._device['devNo'], ctrInfo) return result def get_port_status(self, force=False): """强制改变状态 这个板子的路数总是改变 每次都直接重新获取吧""" self.get_port_status_from_dev() ctrInfo = Device.get_dev_control_cache(self._device['devNo']) if 'allPorts' not in ctrInfo: self.get_port_status_from_dev() ctrInfo = Device.get_dev_control_cache(self._device['devNo']) allPorts = ctrInfo.get("allPorts", 10) statusDict = {} for ii in range(allPorts): tempDict = ctrInfo.get(str(ii + 1), {}) if tempDict.has_key('status'): statusDict[str(ii + 1)] = {'status': tempDict.get('status')} elif tempDict.has_key('isStart'): if tempDict['isStart']: statusDict[str(ii + 1)] = {'status': Const.DEV_WORK_STATUS_WORKING} else: statusDict[str(ii + 1)] = {'status': Const.DEV_WORK_STATUS_IDLE} else: statusDict[str(ii + 1)] = {'status': Const.DEV_WORK_STATUS_IDLE} allPorts, usedPorts, usePorts = self.get_port_static_info(statusDict) Device.update_dev_control_cache(self._device['devNo'], {'allPorts': allPorts, 'usedPorts': usedPorts, 'usePorts': usePorts}) return statusDict def get_port_info(self, line): result = self._get_port_info(line) result.update({"port": line}) return result def start_device(self, package, openId, attachParas): chargeIndex = attachParas.get("chargeIndex") if not chargeIndex: raise ServiceException({'result': 2, 'description': u'请您选择合适的充电线路'}) orderNo = attachParas.get("orderNo") or ConsumeRecord.make_no(self.device.logicalCode, UserConsumeSubType.NETPAY) coins = package.get("coins") balance = None if openId: user = MyUser.objects.get(groupId=self._device['groupId'], openId=openId) balance = user.balance start_timestamp = int(time.time()) otherConf = self.device.get("otherConf", dict()) chargeMode = otherConf.get("chargeType", self.DEFAULT_CHARGE_TYPE) portCache = { 'startTime': timestamp_to_dt(start_timestamp).strftime('%Y-%m-%d %H:%M:%S'), 'status': Const.DEV_WORK_STATUS_WORKING, 'coins': coins, 'isStart': True, 'openId': openId, 'refunded': False, 'vCardId': self._vcard_id, "chargeType": chargeMode, } if chargeMode == self.DEFAULT_CHARGE_TYPE: rule = self.timeRule price = rule["timePrice"] needTime = float(coins) / float(price / 100.0) * rule["timeNum"] portCache.update({"needTime": needTime}) else: rule = self.elecRule price = rule["elecPrice"] needElec = float(coins) / float(price / 100.0) * rule["elecNum"] / 10.0 portCache.update({"needElec": needElec}) result = self._start_device(portStr=chargeIndex, orderNo=orderNo, money=coins, leftMoney=balance) Device.update_dev_control_cache(self.device.devNo, {chargeIndex: portCache}) result['consumeOrderNo'] = orderNo result["finishedTime"] = start_timestamp + 24 * 60 * 60 return result def stop(self, port=None): if not port: return self._stop(str(port)) @property def isHaveStopEvent(self): return True def active_deactive_port(self, port, active): if not active: return self._stop(str(port)) def analyze_event_data(self, data): cmdCode = data[6:8] func = getattr(self, "_parse_event_{}".format(cmdCode.upper()), None) if func: result = func(data) result["cmdCode"] = cmdCode return result else: logger.info("no <{}> parser".format(cmdCode)) def dealer_get_port_status(self): self._get_port_elec() data = self.get_port_status() return data