# -*- coding: utf-8 -*- # !/usr/bin/env python import datetime import logging import time from typing import TYPE_CHECKING from apilib.utils_datetime import timestamp_to_dt from apps.web.constant import DeviceCmdCode, Const, MQTT_TIMEOUT from apps.web.core.adapter.base import SmartBox, fill_2_hexByte from apps.web.core.device_define.changyuan import FunCode, CmdCode 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, Part from apps.web.user.models import Card, CardRechargeOrder, CardRechargeRecord, Group from apilib.monetary import RMB from bson import ObjectId if TYPE_CHECKING: pass logger = logging.getLogger(__name__) class MyHex(object): # 16进制字符串加法 @staticmethod def __check_hex(hexNum): if not isinstance(hexNum, MyHex): raise TypeError("can not add") def __init__(self, hexNum): self.hexNum = hexNum def __add__(self, other): self.__check_hex(other) return hex(int(self.hexNum, 16) + int(other.hexNum, 16)) class ChangYuanCarBox(SmartBox): # 发送函数 def __sendData(self, funCode, data, timeout = MQTT_TIMEOUT.NORMAL): result = MessageSender.send(device = self.device, cmd = DeviceCmdCode.OPERATE_DEV_SYNC, 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 test(self, coins): coinsHex = fill_2_hexByte(hex(int(coins * 100)), 4) result = self.__sendData(FunCode.WEIXIN_PAY_START, "{}0000".format(coinsHex)) data = result["data"] if data[8: 12] == "4552": raise ServiceException({'result': 2, 'description': u'设备相应错误,未能成功启动设备,请上报设备故障'}) elif data[8: 12] == "4F4B": pass else: raise ServiceException({'result': 2, 'description': u'设备相应错误,未能成功启动设备,请上报设备故障'}) return result def start_device(self, package, openId, attachParas): # 先从缓存检查,缓存如果状态不对再请求一次状态,缓存状态如果正确不需要请求。枪把的连接状态是会上报事件的 devInfo = Device.get_dev_control_cache(self._device["devNo"]) if "status" not in devInfo or devInfo["status"] != Const.DEV_WORK_STATUS_CONNECTED: status = self._check_dev_status() if status != "AC": raise ServiceException({'result': 2, 'description': u'请先将充电桩枪把连接'}) # 获取套餐金额数,转换为分 coins = package.get("coins") price = package.get("price") orderNo = attachParas.get("orderNo") priceData = fill_2_hexByte(hex(int(price * 100)), 4) result = self.__sendData(FunCode.WEIXIN_PAY_START, "{}0000".format(priceData), timeout=MQTT_TIMEOUT.START_DEVICE) data = result["data"] if data[8: 12] == "4552": raise ServiceException({'result': 2, 'description': u'设备相应错误,未能成功启动设备,请上报设备故障'}) elif data[8: 12] == "4F4B": pass else: raise ServiceException({'result': 2, 'description': u'设备相应错误,未能成功启动设备,请上报设备故障'}) rechargeRcdId = attachParas.get("linkedRechargeRecordId") start_timestamp = int(time.time()) result['finishedTime'] = start_timestamp + 24 * 60 * 60 devCache = { "openId": openId, "isStart": True, "coins": coins, "price": price, "rechargeRcdId": str(rechargeRcdId) if rechargeRcdId else None, "status": Const.DEV_WORK_STATUS_WORKING, "finishedTime": result['finishedTime'], "vCardId": self._vcard_id, "startTime": timestamp_to_dt(start_timestamp).strftime("%Y-%m-%d %H:%M:%S"), "orderNo": orderNo } Device.update_dev_control_cache(self._device["devNo"], devCache) # 非远程上分的情况下 需要将rechargeRcdId 写入到consumeRecord中 if openId: result["rechargeRcdId"] = str(rechargeRcdId) if rechargeRcdId else None return result def analyze_event_data(self, data): cmdCode = data[4: 6] if cmdCode == CmdCode.STOP_DEV: # 设备停止事件 reasonMap = { "E4": "充电已经正常结束", "E5": "充电已经被停止", "E6": "充电状态异常结束", "E7": "未知原因结束充电", } reasonCode = data[8: 10] # 去掉对于reasonCode的限制 desc = reasonMap.get(reasonCode, reasonCode) cardNo = data[10: 18] electricNum = int(data[18: 22], 16) / 100.0 chargeTime = int(data[22: 26], 16) balance = int(data[26: 30], 16) / 100.0 cardBalance = int(data[30: 36], 16) / 100.0 payMoney = int(data[38: 40] + data[36: 38], 16) / 100.0 return {"cardNo": cardNo, "status": Const.DEV_WORK_STATUS_FINISHED, "electricNum": electricNum, "chargeTime": chargeTime, "balance": balance, "desc": desc, "reasonCode": reasonCode, "cmdCode": cmdCode, "cardBalance": cardBalance, "payMoney": payMoney } elif cmdCode == CmdCode.DEV_CONNECTED: status = data[8: 10] return {"status": status, "cmdCode": cmdCode} elif cmdCode == CmdCode.CARD_PAY_START: cardNo = data[8: 16] return {"cardNo": cardNo, "cmdCode": cmdCode} elif cmdCode == CmdCode.HEART_BEAT: # 昌原的经常把1代和2代的设备注册错误,2种汽车充电桩发送的B2长度不通 含义也截然不通 这个地方做一个保护 发现长度不对就直接扔掉 if len(data) != 20: return leftBalanceStr = data[8: 12] leftBalance = int(leftBalanceStr, 16) / 100.0 return {"leftBalance": leftBalance, "cmdCode": cmdCode} elif cmdCode == CmdCode.CARD_REFUND: cardNo = data[8:16] beforeRefund = int(data[16:22], 16) / 100.0 # 返费前 单位分 refund = int(data[22:26], 16) / 100.0 # 返费额 单位分 afterRefund = int(data[26:32], 16) / 100.0 # 返费后 单位分 return { "cardNo": cardNo, "beforeRefund": beforeRefund, "refund": refund, "afterRefund": afterRefund, "cmdCode": cmdCode } # 真实发送检查设备状态函数 返回设备当前状态字节 更新设备状态缓存 def _check_dev_status(self): # 在这个地方加入部件 if not Part.objects.filter(logicalCode = self._device["logicalCode"]).count(): Part( logicalCode = self._device["logicalCode"], ownerId = self._device["ownerId"], partName = u"充电端口", partType = "3", partNo = "1" ).save() result = self.__sendData(FunCode.CHECK_DEV_STATUS, "00") data = result["data"] status = data[8: 10] carNo = data[30: 38] if carNo == "00000000": carNo = u"在线支付" data = { "outputVoltage": int(data[10: 14], 16) / 1.0, "power": int(data[14: 18], 16) / 1.0, "elec": int(data[18: 22], 16) / 100.0, "usedTime": int(data[22: 26], 16), "leftMoney": int(data[26: 30], 16) / 100.0, "cardNo": carNo, } Device.update_dev_control_cache(self._device["devNo"], data) return status # 检查设备状态 供客户端调用 def check_dev_status(self, attachParas = None): status = self._check_dev_status() if status == "AA": Device.update_dev_control_cache(self._device["devNo"], {"status": Const.DEV_WORK_STATUS_WORKING}) raise ServiceException({'result': 2, 'description': u'当前充电桩正忙,不允许下发启动命令'}) elif status == "AB": Device.update_dev_control_cache(self._device["devNo"], {"status": Const.DEV_WORK_STATUS_APPOINTMENT}) raise ServiceException({'result': 2, 'description': u'当前充电桩已经被预约,不允许下发启动命令'}) elif status == "AC": # 枪把连接OK Device.update_dev_control_cache(self._device["devNo"], {"status": Const.DEV_WORK_STATUS_CONNECTED}) elif status == "55": Device.update_dev_control_cache(self._device["devNo"], {"status": Const.DEV_WORK_STATUS_IDLE}) raise ServiceException({'result': 2, 'description': u'请先将充电桩枪把插上汽车充电口'}) else: raise ServiceException({'result': 2, 'description': u'未知的充电桩状态,请你重新扫码'}) # 真正停止设备函数,参数 传递主板 用于播放语音或者清除卡内余额 def _stop(self, isLawful = True): # 是否需要清除离线卡卡内金额 if isLawful: data = "00" else: data = "01" result = self.__sendData(FunCode.STOP_DEV, data) data = result["data"] if data[8:12] != "4F4B": raise ServiceException({'result': 2, 'description': u'结束充电失败,请重新操作'}) return result # 远程停止设备 供客户端调用 def stop(self, port = None): # 停止充电钱需要判断充电状态,处于充电状态的充电桩才才允许停电 status = self._check_dev_status() if status != "AA": raise ServiceException({'result': 2, 'description': u'当前充电桩不在充电状态,无法远程终止充电'}) return self._stop() @property def isHaveStopEvent(self): return True # 获取设备参数 def get_dev_setting(self): result = self.__sendData(FunCode.GET_SETTING, "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) resultDict = { "is_free": free, "electricPrice": electricPrice, "maxConsume": maxConsume, "maxChargeTime": maxChargeTime, "volume": volume } ctrDevInfo = self.getDevInfo() resultDict.update(ctrDevInfo) resultDict.update({ "disableButton": int(self._device.get("otherConf", {}).get("disableDevice", False)), "needBindCard": int(self._device.get("otherConf", dict()).get("needBindCard", True)) }) return resultDict # 设置设备参数 内部调用 def set_dev_setting(self, setConf): isFree = "AA" if int(setConf.get("is_free", 0)) else "00" electricPrice = int(float(setConf["electricPrice"]) * 100) maxConsume = int(float(setConf["maxConsume"]) * 100) maxChargeTime = int(float(setConf["maxChargeTime"])) volume = int(float(setConf["volume"])) data = list() data.append(isFree) data.append(fill_2_hexByte(hex(electricPrice), 2)) data.append(fill_2_hexByte(hex(maxConsume), 4)) data.append(fill_2_hexByte(hex(maxChargeTime), 4)) data.append(fill_2_hexByte(hex(volume), 2)) data = "".join(data) result = self.__sendData(FunCode.SET_SETTING, data) data = result["data"] if data[8: 12] != "4F4B": raise ServiceException({'result': 2, 'description': u'设备参数设置失败,请重试看能否解决'}) otherConf = Device.get_dev(self._device["devNo"]).get("otherConf", dict()) otherConf.update({"elecPrice": electricPrice / 100.0}) Device.objects.filter(devNo = self._device["devNo"]).update(otherConf = otherConf) Device.invalid_device_cache(self._device["devNo"]) # 充值离线卡 比较特殊,公共函数对其有修改 传递参数从 value 变为 充值记录Obj def remote_charge_card(self, price, recharge_record = None): # recharge_record.devNo = self._device["devNo"] # recharge_record.logicalCode = self._device["logicalCode"] # recharge_record.devType = self._device["devType"]["name"] # try: # recharge_record.save() # except Exception as e: # logger.error("recharge record device info error , the error is %s" % e) value = recharge_record.money value = int(float(value) * 100) value = fill_2_hexByte(hex(value), 4) openId = recharge_record.openId group = Group.get_group(recharge_record.groupId) # 下发充值 这个地方不使用sendData函数,方便知道下发数据的成功或者失败的消息 result = MessageSender.send(device = self.device, cmd = DeviceCmdCode.OPERATE_DEV_SYNC, payload = { "IMEI": self._device["devNo"], "funCode": FunCode.CARD_RECHARGR, "data": value }, timeout = MQTT_TIMEOUT.LONGEST) # 汽车桩卡充值失败之后不现金退款了 # TODO zjl 这个地方考虑将 在模块驱动层面 receive 这条指令之后 上抛一个失败的时间 从而直接在event里面处理退款事件 维持流程的统一性 if result.get("rst") != 0 or result.get("data") == "5050AC02455201E50D0A": needRefund = True # 充值成功之后 data = result.get("data") cardNo = data[8: 16] leftBalance = int(data[16: 22], 16) / 100.0 # 充值后余额 rightBalance = int(data[22: 28], 16) / 100.0 # 充值前余额 logger.info("remote charge card, cardNo is %s, afterBalance is %s, beforeBalance is %s" % ( cardNo, leftBalance, rightBalance)) # 此类充值, 充值的时候没有对于卡号的记录,需要手动更新卡信息 card = Card.objects(cardNo = cardNo, cardType = "IC", dealerId = self._device["ownerId"]) try: if card.count() == 0: # 绑定新卡 card = Card( cardNo = cardNo, cardType = "IC", dealerId = self._device["ownerId"], balance = RMB(leftBalance), devNo = self._device["devNo"], lastMaxBalance = RMB(leftBalance - rightBalance) ).save() logger.info("new remote charge card %s" % RMB(leftBalance)) else: card = card[0] card.balance = RMB(leftBalance) logger.info("remote charge card %s" % RMB(leftBalance)) card.lastMaxBalance = RMB(leftBalance - rightBalance) card.save() except Exception as e: logger.error("card info update error %s" % e) else: cardId = str(card.id) # 记录卡充值订单 newOrder = CardRechargeOrder.new_one(openId, cardId, cardNo, recharge_record.money, recharge_record.coins, group, recharge_record.id) # 记录卡充值记录 newRcd = CardRechargeRecord( cardId = cardId, cardNo = cardNo, openId = openId, ownerId = ObjectId(self._device['ownerId']), money = recharge_record.money, coins = recharge_record.coins, balance = RMB(leftBalance), preBalance = RMB(rightBalance), devNo = self._device['devNo'], devTypeCode = self._device['devType']['code'], logicalCode = self._device['logicalCode'], groupId = self._device['groupId'], address = group['address'], groupNumber = self._device['groupNumber'], groupName = group['groupName'], status = 'success', rechargeType = 'netpay' ) try: newOrder.save() newRcd.save() except Exception as e: logger.error("save card recharge card error, reason is %s" % e) return False # 设置设备故障 def set_dev_disable(self, disable): if disable: status = "E9" else: status = "E8" result = self.__sendData(FunCode.START_OR_STOP, status) data = result["data"] if data[8: 12] != "4F4B": raise ServiceException({'result': 2, 'description': u'设置失败,请重试'}) otherConf = self._device.get("otherConf", {}) otherConf["disableDevice"] = disable Device.objects.filter(devNo = self._device["devNo"]).update(otherConf = otherConf) Device.invalid_device_cache(self._device["devNo"]) # 暂时未使用 def restart(self): self.__sendData(FunCode.RESTART, "00") # 暂时未使用 def get_card_balance(self): result = self.__sendData(FunCode.CARD_BALANCE, "00") data = result["data"] if data[6: 8] == "02" and data[8: 12] == "4552": raise ServiceException({'result': 2, 'description': u'读取失败,请将卡放置在设备指定区域'}) cardNo = int(data[8:16], 16) balance = int(data[16: 22], 16) / 100.0 return cardNo, balance # 获取设备端的信息,主要是累计使用量、累计总金额,刷卡总额(刷卡总额是计算出来的,等于总-微信支付的) def getDevInfo(self): result = self.__sendData(FunCode.GET_DEV_INFO, "00") data = result["data"] amount = data[8: 14] electricNum = data[14: 20] if amount == "FFF" or electricNum == "FFF": # TODO zjl 需不需要上报事件告知这个溢出情况 pass trans = lambda x: (int(x, 16) / 100.0) amount = trans(amount) electricNum = trans(electricNum) return { "totalConsume": amount, "totalElec": electricNum } # 清除设备端的累计信息 def cleanDevInfo(self, amount = False, electricNum = False): if not amount and not electricNum: return elif amount and not electricNum: data = "E1" elif electricNum and not amount: data = "E2" else: data = "E3" self.__sendData(FunCode.CLEAN_DEV_INFO, data) timeDict = dict() if amount: timeDict["amountCleanTime"] = datetime.datetime.strftime(datetime.datetime.now(), "%Y-%m-%d %H:%M:%S") if electricNum: timeDict["electricNumCleanTime"] = datetime.datetime.strftime(datetime.datetime.now(), "%Y-%m-%d %H:%M:%S") Device.update_dev_control_cache(self._device["devNo"], timeDict) # 获取10条未返费的记录 暂时未使用 def no_refund_record(self): result = self.__sendData(FunCode.REFUND_RECORD, "00") data = result["data"] noRefund = list() data = data[8: -8] for i in xrange(10): tempData = data[12 * i: 12 * (i + 1)] cardNo = tempData[: 8] amount = tempData[8: 12] if cardNo == "FFFFFFFF" or cardNo == "00000000": continue amount = int(amount, 16) / 100.0 noRefund.append({"cardNo": cardNo, "amount": amount}) return noRefund # 获取最近的几条充电记录 暂时未使用 def get_charge_record(self, num): num = min(num, 30) baseHex = MyHex("80") numHex = MyHex(fill_2_hexByte(hex(num), 2)) data = fill_2_hexByte((baseHex + numHex), 2) result = self.__sendData(FunCode.CHARGE_RECORD, data) data = result["data"] dataLen = int(data[6: 8], 16) * 2 - 2 # 数据字符数量 recordData = data[10: 10 + dataLen] recordList = list() while recordData: cardNo = recordData[: 8] electricNum = int(recordData[8: 12], 16) / 100.0 chargeTime = int(recordData[12: 16], 16) chargeBalance = int(recordData[16: 20], 16) / 100.0 cardBalance = int(recordData[20: 26], 16) / 100.0 recordData = recordData[26:] if cardNo == "FFFFFFFF": continue recordList.append( { "type": u"{} 刷卡充电".format(cardNo) if cardNo != "00000000" else u"扫码充电", "elec": u"{} 度".format(electricNum), "duration": u"{} 分钟".format(chargeTime), "chargeBalance": u"{} 元".format(chargeBalance), "cardBalance": u"{} 元".format(cardBalance) if cardBalance else u"本次无刷卡消费" } ) return recordList # 功能设置 对接view def set_device_function(self, request, lastSetConf): remoteStop = request.POST.get("remoteStop", None) clearTotalConsume = request.POST.get("clearTotalConsume", False) clearTotalElec = request.POST.get("clearTotalElec", False) # 停止设备 if remoteStop: return self.stop() # 清除设备计费信息 if any([clearTotalConsume, clearTotalElec]): self.cleanDevInfo(clearTotalConsume, clearTotalElec) # 功能参数设置 对接view 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))) if "needBindCard" in request.POST: needBindCard = bool(int(request.POST.get('needBindCard'))) otherConf = self.device.get("otherConf", dict()) otherConf.update({"needBindCard": needBindCard}) Device.objects.get(devNo=self.device.devNo).update(otherConf=otherConf) Device.invalid_device_cache(self.device.devNo) return newDict = { "is_free": request.POST.get("is_free", None), "electricPrice": request.POST.get("electricPrice", None), "maxConsume": request.POST.get("maxConsume", None), "maxChargeTime": request.POST.get("maxChargeTime", None), "volume": request.POST.get("volume", None) } resultDict = self.get_dev_setting() for k, v in newDict.items(): if v is not None: resultDict.update({k: v}) self.set_dev_setting(resultDict) def get_device_function_by_key(self, key): if key == "noRefund": rcd = self.no_refund_record() return {"noRefund": rcd} elif key == "record": rcd = self.get_charge_record(15) return {"record": rcd} def get_port_info(self, port): result = self.__sendData(FunCode.CHECK_DEV_STATUS, "00") data = result["data"] carNo = data[30: 38] if carNo == "00000000": carNo = u"在线支付" devCache = Device.get_dev_control_cache(self.device.devNo) if data[8: 10] == "55": status = Const.DEV_WORK_STATUS_IDLE elif data[8: 10] == "AC": status = Const.DEV_WORK_STATUS_CONNECTED else: status = Const.DEV_WORK_STATUS_WORKING data = { "status": status, "outputVoltage": str(int(data[10: 14], 16) / 1.0), "power": str(int(data[14: 18], 16) / 1.0), "elec": str(int(data[18: 22], 16) / 100.0), "usedTime": str(int(data[22: 26], 16)), "leftMoney": str(int(data[26: 30], 16) / 100.0), "cardNo": carNo, } devCache.update(data) return devCache def dealer_get_port_status(self): devInfo = self.get_port_info(1) if devInfo["status"] != Const.DEV_WORK_STATUS_WORKING: devInfo = {"status": devInfo["status"]} return {"1": devInfo} def active_deactive_port(self, port, active): if not active: self._stop() else: super(ChangYuanCarBox, self).active_deactive_port(port, active)