# -*- coding: utf-8 -*- # !/usr/bin/env python import datetime import logging import time from decimal import Decimal from apilib.utils_datetime import timestamp_to_dt from apps.common.utils import int_to_hex from apps.web.common.models import TempValues 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 logger = logging.getLogger(__name__) class ChangYuanCarV2(SmartBox): STATUS_MAP = { "AA": Const.DEV_WORK_STATUS_WORKING, "AB": Const.DEV_WORK_STATUS_APPOINTMENT, "AC": Const.DEV_WORK_STATUS_CONNECTED, "55": Const.DEV_WORK_STATUS_IDLE } FINISH_REASON_MAP = { "E0": "过压", "E1": "过流", "E2": "超功率", "E3": "限时时间到", "E4": "正常结束", "E5": "急停结束", "E7": "支付金额结束", "E8": "汽车充满电结束", "E9": "超温结束", "EA": "计量通信失败结束", "EB": "汽车连接失败", "EC": "用户刷卡停止" } DEFAULT_DISABLE_DEVICE = False DEFAULT_CARD_CONSUME = 100 def _send_data(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 _get_device_settings(self): result = self._send_data("A0", "00") data = result.get("data", dict()) 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) return { "is_free": free, "electricPrice": electricPrice, "maxConsume": maxConsume, "maxChargeTime": maxChargeTime, "volume": volume } 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) # TODO zjl 数据库保存一份 def _get_consume_count(self): """ 从充电桩上获取累计 的消费金额以及消费电量 :return: """ result = self._send_data("A2", "00") data = result.get("data", dict()) totalConsume = data[8: 14] totalElec = data[14: 20] return { "totalConsume": int(totalConsume, 16) / 100.0, "totalElec": int(totalElec, 16) / 100.0 } def _clean_consume_count(self, consume = False, elec = False): """ 清除充电桩上累计信息 :param consume: :param elec: :return: """ if consume and elec: sendData = "E3" elif not consume: sendData = "E2" elif not elec: sendData = "E1" else: return self._send_data("A3", sendData) def _get_not_refund_record(self): """ 获取最近10条未返费的卡号以及剩余金额 :return: """ result = self._send_data("A4", "00") data = result.get("data", dict()) 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_recharge_record_from_device(self, num): """ 从设备上 获取最近的几条充电记录 :param num: :return: """ num = 30 if num > 30 else num numHex = fill_2_hexByte(hex(int("80", 16) + int(num)), 2) result = self._send_data("A5", numHex) data = result.get("data", dict()) 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 def _get_device_status(self): """ 即时获取设备状态 :return: """ result = self._send_data("A6", "00") data = result.get("data", dict()) status = data[8:10] cardNo = data[30:38] if cardNo == "00000000": cardNo = u"在线支付" result = { "status": self.STATUS_MAP.get(status, Const.DEV_WORK_STATUS_IDLE) } if result["status"] == Const.DEV_WORK_STATUS_WORKING: result.update({ "voltage": int(data[10:14], 16) / 1.0, "power": int(data[14:18], 16) / 1.0, "usedElec": int(data[18:22], 16) / 100.0, "usedTime": int(data[22:26], 16), "leftMoney": int(data[26:30], 16) / 100.0, "cardNo": cardNo }) Device.update_dev_control_cache(self.device.devNo, result) return result def _start(self, money): """ 微信支付命令 :param money: :return: """ sendData = "{:0<8}".format(fill_2_hexByte(hex(int(money * 100)), 4)) result = self._send_data("A7", sendData, timeout = MQTT_TIMEOUT.START_DEVICE) data = result.get("data", dict()) if data[8:12] != "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.get("data", dict()) 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 _async_card_balance(self, cardType, cardNo, balance): """ 同步卡的余额 加卡的余额一次行下发过去 :param cardType: 00 下发单位是元 01 下发单位是 分 :param cardNo: 卡号 :param balance: 单位是RMB :return: """ balance = balance if cardType == "00" else balance * 100 # 获取随机流水号 sidKey = "{}-{}".format(self.device.devNo, cardNo) sid = TempValues.get(sidKey) balanceHex = int_to_hex(int(balance), 6) sidHex = int_to_hex(int(sid)) MessageSender.send(device = self.device, cmd = DeviceCmdCode.OPERATE_DEV_NO_RESPONSE, payload = { "IMEI": self.device.devNo, "funCode": "F3", "data": cardType + balanceHex + sidHex }) def _read_card_balance(self): result = self._send_data("AD", "00") data = result["data"] if data[6:8] == "02" and data[8:12] == "4552": raise ServiceException({'result': 2, 'description': u'读取失败,请将卡放置在设备指定区域'}) cardNo = data[8:16] balance = int(data[16:22], 16) / 100.0 return { "cardNo": cardNo, "balance": balance } def _stop(self, isLawFul = True): logger.info("ready to stop device <{}>".format(self.device.devNo)) data = "00" if isLawFul else "01" self._send_data("AF", data) def _restart_device(self): self._send_data("B0", "00") def _response_a8id(self, a8id): MessageSender.send(self.device, DeviceCmdCode.OPERATE_DEV_NO_RESPONSE, { "IMEI": self._device["devNo"], "funCode": "FA", "data": "", "a8id": a8id }) @staticmethod def _parse_event_A8(data): reasonCode = data[8:10] desc = ChangYuanCarV2.FINISH_REASON_MAP.get(reasonCode, u"未知错误") cardNo = data[10:18] usedElec = 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 { "reasonCode": reasonCode, "desc": desc, "cardNo": cardNo, "usedElec": usedElec, "chargeTime": chargeTime, "balance": balance, "cardBalance": cardBalance, "payMoney": payMoney } @staticmethod def _parse_event_AE(data): statusMap = { "B1": Const.DEV_WORK_STATUS_CONNECTED, "B5": Const.DEV_WORK_STATUS_IDLE } status = statusMap.get(data[8:10], Const.DEV_WORK_STATUS_IDLE) return { "status": status } @staticmethod def _parse_event_B1(data): cardNo = data[8:16] return { "cardNo": cardNo } @staticmethod def _parse_event_B2(data): """防止他们注册错设备类型""" if len(data) <= 20: return dict() leftBalance = int(data[8:12], 16) / 100.0 usedElec = int(data[12:16], 16) / 100.0 temperature = int(data[18:20], 16) power = int(data[20:24], 16) voltage = int(data[28:32], 16) sid = int(data[32:36], 16) temperature = temperature if data[16:18] == "00" else -temperature return { "leftBalance": leftBalance, "usedElec": usedElec, "temperature": u"{}度 更新于{}".format(temperature, datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")), "power": power, "voltage": voltage, "sid": sid } @staticmethod def _parse_event_B3(data): 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 } @staticmethod def _parse_event_F4(data): """ 请求卡余额同步 此指令是里显卡同步余额的发起指令 cardType 01表示预付卡 上报单位为分 00表示次卡 上报单位是元 :param data: :return: """ cardType = data[8:10] cardNo = data[10:18] cardBalanceHex = data[18:24] if cardType == "00": cardBalance = int(cardBalanceHex, 16) else: cardBalance = int(cardBalanceHex, 16) / 100.0 return { "cardType": cardType, "cardNo": cardNo, "cardBalance": cardBalance } @staticmethod def _parse_event_F3(data): """ 余额同步成功 这个地方上报告知成功还是失败 如果存在失败的情况 将订单的状态重新切换或finishedPay :param data: :return: """ cardType = data[8:10] cardNo = data[10:18] cardBalanceHex = data[18:24] asyncStatus = data[24:26] sidHex = data[26:30] if cardType == "00": cardBalance = int(cardBalanceHex, 16) else: cardBalance = int(cardBalanceHex, 16) / 100.0 return { "cardType": cardType, "cardNo": cardNo, "cardBalance": cardBalance, "asyncStatus": True if asyncStatus == "AA" else False, "sid": int(sidHex, 16) } def test(self, coins): return self._start(coins) @property def isHaveStopEvent(self): return True def get_dev_setting(self): settings = self._get_device_settings() otherConf = self.device.get("otherConf", dict()) disableDevice = otherConf.get("disableDevice", self.DEFAULT_DISABLE_DEVICE) needBindCard = otherConf.get("needBindCard", True) settings.update({"disableButton": int(disableDevice), "needBindCard": int(needBindCard)}) otherConf.update(settings) 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))) 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) } otherConf = self.device.get("otherConf", dict()) for setName, setValue in newDict.items(): if setValue is not None: otherConf.update({setName: setValue}) self._set_device_settings(otherConf) def set_dev_disable(self, disable): self._set_device_disable(disable) 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) reboot = request.POST.get("reboot", False) # 停止设备 if remoteStop: self._stop() if reboot: self._restart_device() # 清除设备计费信息 if any([clearTotalConsume, clearTotalElec]): self._clean_consume_count(clearTotalConsume, clearTotalElec) def get_device_function_by_key(self, keyName): if keyName == "noRefund": records = self._get_not_refund_record() return {"noRefund": records} elif keyName == "record": records = self._get_recharge_record_from_device(15) return {"record": records} def stop(self, port = None): return self._stop() def check_dev_status(self, attachParas = None): result = self._get_device_status() status = result.get("status") if status != 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(ChangYuanCarV2, funcName, None) if func and callable(func): eventData = func(data) # 解析出错的情况下 或者解析没有返回的情况下 if not eventData: logger.error("receive event data error parse, data <{}>".format(data)) return eventData.update({"cmdCode": cmdCode}) return eventData def start_device(self, package, openId, attachParas): devCache = Device.get_dev_control_cache(self.device.devNo) if devCache.get("status") != Const.DEV_WORK_STATUS_CONNECTED: status = self._get_device_status().get("status") if status != Const.DEV_WORK_STATUS_CONNECTED: raise ServiceException({'result': 2, 'description': u'请先将充电桩枪把连接'}) coins = package.get("coins") price = package.get("price") orderNo = attachParas.get("orderNo") result = self._start(price) rechargeRcdId = attachParas.get("linkedRechargeRecordId") startTimeStamp = int(time.time()) devCache = { "openId": openId, "isStart": True, "coins": coins, "price": price, "rechargeRcdId": str(rechargeRcdId) if rechargeRcdId else None, # zjl ObjectId -> str "status": Const.DEV_WORK_STATUS_WORKING, "startTime": timestamp_to_dt(startTimeStamp).strftime("%Y-%m-%d %H:%M:%S"), "orderNo": orderNo, "finishedTime": startTimeStamp + 24 * 60 * 60 # zjl new } Device.invalid_device_control_cache(self.device.devNo) Device.update_dev_control_cache(self.device.devNo, devCache) result["finishedTime"] = startTimeStamp + 60 * 60 * 24 if openId: # zjl new result["rechargeRcdId"] = str(rechargeRcdId) if rechargeRcdId else None # zjl new return result def dealer_get_port_status(self): devInfo = self.get_port_info(1) if "status" not in devInfo: devInfo["status"] = Const.DEV_WORK_STATUS_IDLE return {"1": devInfo} def get_port_info(self, port): self._get_device_status() 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() else: super(ChangYuanCarV2, self).active_deactive_port(port, active)