# -*- coding: utf-8 -*- # !/usr/bin/env python import binascii import datetime import logging import struct import time from apps.web.agent.models import Agent 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, Group, DeviceType from apps.web.device.timescale import FluentedEngine from apps.web.utils import concat_user_login_entry_url logger = logging.getLogger(__name__) class ChargingJNCar(SmartBox): PORT_STATUS_MAP = { "01": Const.DEV_WORK_STATUS_IDLE, "02": Const.DEV_WORK_STATUS_WORKING, "03": Const.DEV_WORK_STATUS_FORBIDDEN, # 劲能汽车桩 故障和连接状态没有区分开 咨询过主板 只能靠人工判断 插枪了如果还是故障 就是真的故障 "04": Const.DEV_WORK_STATUS_CONNECTED } ERROR_MAP = { "0001": u"继电器粘连", "0002": u"其他错误" } FINISH_MAP = { '00': u'购买的充电时间或电量用完了。', '01': u'可能是插头被拔掉,或者电瓶已经充满。系统判断为异常断电,由于电瓶车充电器种类繁多,可能存在误差。如有问题,请您及时联系商家协助解决问题并恢复充电。', '02': u'恭喜您!电池已经充满电!', '03': u'警告!您的电池超功率,已经停止充电,为了公共安全,不建议您在该充电桩充电!提醒您,为了安全大功率的电池不要放入楼道、室内等位置充电哦', '04': u'远程断电。', '05': u'刷卡断电', '0B': u'设备或端口出现问题,为了安全起见,被迫停止工作。建议您根据已经充电的时间评估是否需要到现场换到其他端口充电。' } def _send_data(self, funCode, data, cmd=None, timeout=MQTT_TIMEOUT.NORMAL): """ :param funCode: :param data: :param cmd: :param timeout: :return: """ 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": data }, timeout = timeout) if "rst" in result and result.get("rst") != 0: if result.get("rst") == -1: raise ServiceException({"result": 2, "description": u"网络故障,请重新试试"}) if result.get("rst") == 1: raise ServiceException({"result": 2, "description": u"充电桩无响应,请稍后再试试"}) return result @property def _sessionId(self): return int(time.time()) @staticmethod def _to_ascii(s): """ 将字符串转换成ascii :param s: :return: """ return binascii.hexlify(s) @staticmethod def _parse_event_26(data): """ 解析 26 指令上报过来的数据 26 指令主要负责上传端口的实时状态 是将所有的端口一次性上传 :return: """ portNum = int(data[8:10], 16) portStatusDict = dict() data = data[10:] for i in xrange(portNum): portStr = str(int(data[:2], 16)) portStatusDict[portStr] = { "status": ChargingJNCar.PORT_STATUS_MAP.get(data[4:6], Const.DEV_WORK_STATUS_IDLE), "usedTime": int(data[6:10], 16), "power": int(data[10:14], 16), "usedElec": int(data[14:18], 16) / 100.0, "voltage": int(data[18:22], 16) / 10.0, "a": int(data[22: 26], 16) / 100.0 } data = data[26: ] portStatusDict["portNum"] = portNum return portStatusDict @staticmethod def _parse_event_0A(data): """ 解析故障报警 :param data: :return: """ port = data[8:10] if port == "FFFF": portStr = 0 else: portStr = str(int(port, 16)) errorCode = data[10:14] errorDesc = ChargingJNCar.ERROR_MAP.get(errorCode, "") return { "portStr": portStr, "errorCode": errorCode, "errorDesc": errorDesc } def _start(self, port, money, elec, _type=None, _time=None): """ 启动设备 :param port: :param money: :param elec: :param _type: :param _time: :return: """ # 目前只支持按电量计费 if _type is None: chargeTypeHex = "02" else: chargeTypeHex = fill_2_hexByte(hex(int(_type)), 2) if _time is None: chargeTimeHex = "0000" else: chargeTimeHex = fill_2_hexByte(hex(int(_time)), 4) moneyHex = fill_2_hexByte(hex(int(float(money)*10)), 4) portHex = fill_2_hexByte(hex(int(port)), 2) elecHex = fill_2_hexByte(hex(int(float(elec)*100)), 4) sessionHex = fill_2_hexByte(hex(self._sessionId), 8) sendData = portHex + moneyHex + chargeTimeHex + elecHex + sessionHex + chargeTypeHex result = self._send_data("27", sendData, timeout=MQTT_TIMEOUT.START_DEVICE) data = result.get("data", "") if data[10:12] == "0B": raise ServiceException({"result": 0, "description": u"充电站故障,请重新试试"}) elif data[10:12] == "0C": raise ServiceException({"result": 0, "description": u"该端口已被占用,请换个端口进行充电"}) elif data[10:12] == "02": raise ServiceException({"result": 0, "description": u"暂不支持此充电模式,请联系经销商解决"}) elif data[10:12] == "01": return result else: raise ServiceException({"result": 2, "description": u"未知充电错误"}) def _stop(self, port): """ 停止设备使用 _type 表示 停止的订单类型 0x00/所有类型的订单 0x01/通过模块提交的充电信息 0x02/本地的支付订单,如刷卡、投币 :return: """ portHex = fill_2_hexByte(hex(int(port)), 2) _type = "00" self._send_data("0D", portHex+_type) def _set_device_qr_code(self): """ 设置设备的二维码 :return: """ logicalCode = self.device.logicalCode[-6:] qr_code_url = concat_user_login_entry_url(l = self._device["logicalCode"]) code = fill_2_hexByte(hex(int(logicalCode)), 8) urlBuf = ChargingJNCar._to_ascii(qr_code_url).upper() urlBuf = "{:0<140}".format(urlBuf) # 最长70个字节结尾为00 self._send_data("2F", code+urlBuf, DeviceCmdCode.OPERATE_DEV_NO_RESPONSE) # d = "000012A8687474703A2F2F646576656C6F702E3574616F3561692E636F6D2F757365724C6F67696E3F6C3D34343737363800000000000000000000000000000000000000000000000000" # self._send_data("2F", d, DeviceCmdCode.OPERATE_DEV_NO_RESPONSE) def _get_device_settings_2B(self): """ 获取设备的 一些设置 :return: """ result = self._send_data("2B", "00") data = result.get("data", "") cardCst = int(data[8:12], 16) # 单位为角 elecPri = int(data[12:16], 16) # 单位是分 return { "cardCst": cardCst, "elecPri": elecPri } def _set_device_settings_2A(self, cst, elecFee): """ 设置设备的一些参数 :return: """ cstHex = fill_2_hexByte(hex(int(cst)), 4) elecFeeHex = fill_2_hexByte(hex(int(elecFee)), 4) return self._send_data("2A", cstHex+elecFeeHex) def _get_port_charge_status(self, port): """ 查询当前的充电状态 电流暂时不要 :return: """ portHex = fill_2_hexByte(hex(int(port)), 2) result = self._send_data("2E", portHex) data = result.get("data") leftTime = data[10:14] power = data[14:18] leftElec = data[18:22] surp = data[22:26] voltage = data[26:30] maxTime = data[34:38] return { "leftTime": int(leftTime, 16) if leftTime else 0, "power": int(power, 16) if power else 0, "leftElec": int(leftElec, 16) / 100.0 if leftElec else 0, "surp": int(surp, 16) / 10.0 if surp else 0, "voltage": int(voltage, 16) if voltage else 0, "maxTime": int(maxTime, 16) if maxTime else 0 } def _get_device_max_charge_time(self): """ 获取设备的最长充电时间 :return: """ result = self._send_data("25", "00") data = result.get("data", "") maxChargeTime = int(data[8:12], 16) return { "maxChargeTime": maxChargeTime } def _set_device_max_charge_time(self, chargeTime): """ 设置设备的最长充电时间 单位是分钟 :return: """ chargeTimeHex = fill_2_hexByte(hex(int(chargeTime)), 4) return self._send_data("24", chargeTimeHex) def _get_device_version(self): """ 获取设备的主板信息 版本等等 :return: """ typeMap = { "01": u"十路智慧款", "02": u"双路电轿款", "04": u"离线充值机", "05": u"16路智慧款", "06": u"20路智慧款", "07": u"单路7kw交流桩" } result = self._send_data("23", "00") data = result.get("data", "") hdType = data[8:10] hwVer = data[10:14] swVer = data[14:18] idBuf = data[18:42] return { "hdType": typeMap.get(hdType, ""), "hwVer": hwVer, "swVer": swVer, "idBuf": idBuf } def _get_device_port_status(self): """ 读取每个设备的状态 :return: """ result = self._send_data("0F", "00") data = result.get("data", "") portNum = int(data[8:10], 16) portHexData = data[10:-2] portStatusDict = dict() for i in range(portNum): offset = i * 4 portStr = str(int(portHexData[0+offset: 2+offset], 16)) portStatus = ChargingJNCar.PORT_STATUS_MAP.get(portHexData[2+offset: 4+offset]) portStatusDict[portStr] = {"status": portStatus} return portStatusDict def _lock_port(self, port): """ 锁定端口 :param port: :return: """ portHex = fill_2_hexByte(hex(int(port)), 2) statusHex = "00" self._send_data("0C", portHex+statusHex, cmd=DeviceCmdCode.OPERATE_DEV_NO_RESPONSE) def _unlock_port(self, port): """ 解锁端口 :param port: :return: """ portHex = fill_2_hexByte(hex(int(port)), 2) statusHex = "01" self._send_data("0C", portHex + statusHex, cmd=DeviceCmdCode.OPERATE_DEV_NO_RESPONSE) def _get_consume_data_from_device(self): """ 从 主板 侧获取消费数据 :return: """ result = self._send_data("07", "00") data = result.get("data", "") cardMoney = int(data[8:12], 16) / 10.0 coinMoney = int(data[12:16], 16) return { "cardMoney": cardMoney, "coinMoney": coinMoney } def _get_all_port_num(self): """ 获取端口总数 :return: """ result = self._send_data("01", "00") data = result.get("data", "") portNum = int(data[8:10], 16) return { "portNum": portNum } def _response_card_balance(self, cardBalance, res): """ 回复刷卡余额的数据 :param cardBalance: :param res: :return: """ balanceHex = fill_2_hexByte(hex(int(cardBalance) * 10), 4) self._send_data("10", res+balanceHex, cmd=DeviceCmdCode.OPERATE_DEV_NO_RESPONSE) def _response_finished(self, port, sessionId): """ 回复主板结束事件 :param port: :param sessionId: :return: """ portHex = fill_2_hexByte(hex(int(port)), 2) self._send_data("2C", portHex+sessionId, cmd=DeviceCmdCode.OPERATE_DEV_NO_RESPONSE) def lock_unlock_port(self, port, lock=True): """ 禁用 解禁 端口 :param port: :param lock: :return: """ if lock: self._lock_port(port) else: self._unlock_port(port) def active_deactive_port(self, port, active): if not active: self._stop(port) else: raise ServiceException({'result': 2, 'description': u'此设备不支持直接打开端口'}) def stop(self, port=None): """ 停止设备运行 :param port: :return: """ self._stop(port) def get_port_info(self, port): """ 获取端口运行的信息 可以从设备端获取 也可以从缓存获取 :param port: :return: """ devCache = Device.get_dev_control_cache(self.device.devNo) return devCache.get(str(port), dict()) def get_port_status_from_dev(self): """ 获取设备 端口的实时状态 :return: """ portDict = self._get_device_port_status() # 更新可用端口数量 allPorts, usedPorts, usePorts = self.get_port_static_info(portDict) Device.update_dev_control_cache( self._device["devNo"], { "allPorts": allPorts, "usedPorts": usedPorts, "usePorts": usePorts } ) # 更新端口状态 devCache = Device.get_dev_control_cache(self._device["devNo"]) for port, info in portDict.items(): if port in devCache and isinstance(info, dict): devCache[port].update({"status": info["status"]}) else: devCache[port] = info Device.update_dev_control_cache(self._device["devNo"], devCache) return portDict def get_port_status(self, force = False): """ 获取 设备端口状态 一般是从缓存读取 :param force: :return: """ if force: return self.get_port_status_from_dev() devCache = Device.get_dev_control_cache(self._device["devNo"]) if "allPorts" not in devCache: self.get_port_status_from_dev() devCache = Device.get_dev_control_cache(self._device["devNo"]) allPorts = devCache.get("allPorts") if allPorts is None: raise ServiceException({'result': 2, 'description': u'充电端口信息获取失败'}) # 获取显示端口的数量 客户要求可以设置 显示给用户多少个端口 statusDict = dict() for portNum in xrange(allPorts): portStr = str(portNum + 1) tempDict = devCache.get(portStr, {}) if "status" in tempDict: statusDict[portStr] = {"status": tempDict["status"]} elif "isStart" in tempDict: if tempDict["isStart"]: statusDict[portStr] = {"status": Const.DEV_WORK_STATUS_WORKING} else: statusDict[portStr] = {"status": Const.DEV_WORK_STATUS_IDLE} else: statusDict[portStr] = {"status": Const.DEV_WORK_STATUS_IDLE} allPorts, usedPorts, usePorts = self.get_port_static_info(statusDict) portsDict = {"allPorts": allPorts, "usedPorts": usedPorts, "usePorts": usePorts} Device.update_dev_control_cache(self._device["devNo"], portsDict) # 返还的是要显示的端口数量 return statusDict def check_dev_status(self, attachParas=None): """ 汽车充电桩使用现金支付的时候,首先先检查一下设备的端口是否正常 :param attachParas: :return: """ chargeIndex = attachParas.get("chargeIndex", "") portStatus = self._get_device_port_status().get(chargeIndex, dict()).get("status") return portStatus == Const.DEV_WORK_STATUS_IDLE def test(self, coins): """ 联网检测 :param coins: :return: """ port = "01" elec = "10" return self._start(port, coins, elec) def start_device(self, package, openId, attachParas): """ 启动设备,目前仅支持按按电量计费 :param package: :param openId: :param attachParas: :return: """ if attachParas is None: raise ServiceException({'result': 2, 'description': u'请您选择合适的充电线路、电池类型信息'}) if "chargeIndex" not in attachParas: raise ServiceException({'result': 2, 'description': u'请您选择合适的充电线路'}) chargeIndex = attachParas.get("chargeIndex") needElec = package.get("time") coins = package.get("coins") price = package.get("price") try: result = self._start(chargeIndex, price, needElec) except ServiceException as se: if se.result.get("result") == 0: # TODO zjl 执行退款事件 se.result.update({"result": 2}) raise ServiceException(se.result) devCache = Device.get_dev_control_cache(self.device.devNo) portCache = devCache.get(str(chargeIndex), dict()) nowTimeStamp = int(time.time()) portCache.update({ "isStart": True, "status": Const.DEV_WORK_STATUS_WORKING, "openId": openId, "price": price, "coins": coins, "needElec": needElec, "startTime": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "startTimeStamp": nowTimeStamp, "consumeType": "mobile", "vCardId": self._vcard_id, "power": 1 # 为power添加默认值 主要是getCurrentUse 的这个接口有一个判断power==0的时候直接将ServiceProgress结束了 }) if 'linkedRechargeRecordId' in attachParas and attachParas.get('isQuickPay', False): item = { 'rechargeRcdId': str(attachParas['linkedRechargeRecordId']) } portCache['payInfo'] = [item] Device.update_dev_control_cache(self.device.devNo, {str(chargeIndex): portCache}) result["finishedTime"] = 24 * 60 * 60 + nowTimeStamp return result def check_and_do_card_number_reverse(self, cardNo, cardNoData): group = Group.get_group(self._device['groupId']) dealer = Dealer.get_dealer(group['ownerId']) agent = Agent.objects(id=dealer['agentId']).first() device = Device.objects(devNo=self._device['devNo']).first() devType = DeviceType.objects(id=device['devType']['id']).first() if agent is not None and 'cardNoReverse' in agent.features: cardData = [cardNoData[_*2:(_ + 1)*2] for _ in range(0, len(cardNoData) / 2)] cardData.reverse() cardData = ''.join(cardData) cardNo = str(int(cardData, 16)) if 'cardNoReverse' in devType.features: if devType.features['cardNoReverse'] is True: cardData = [cardNoData[_ * 2:(_ + 1) * 2] for _ in range(0, len(cardNoData) / 2)] cardData.reverse() cardData = ''.join(cardData) cardNo = str(int(cardData, 16)) else: cardData = [cardNoData[_ * 2:(_ + 1) * 2] for _ in range(0, len(cardNoData) / 2)] cardData = ''.join(cardData) cardNo = str(int(cardData, 16)) else: pass return cardNo def analyze_event_data(self, data): """ 接受事件 :param data: :return: """ cmdCode = data[4:6] # 请求二维码 if cmdCode == "2F": self._set_device_qr_code() return elif cmdCode == "2C": portStr = str(int(data[8:10], 16)) leftTime = int(data[10:14], 16) leftElec = int(data[14:18], 16) / 100.0 cardNo = str(int(data[18:26], 16)) cardNo = self.check_and_do_card_number_reverse(cardNo, data[18:26]) cardLeftBalance = int(data[26:30], 16) / 10.0 cardOpe = data[30:32] cardType = data[32:34] reasonCode = data[34:36] sessionId = data[36:44] return { "cmdCode": cmdCode, "portStr": portStr, "leftTime": leftTime, "leftElec": leftElec, "cardNo": cardNo, "cardLeftBalance": cardLeftBalance, "cardOpe": cardOpe, "cardType": cardType, "reasonCode": reasonCode, "desc": ChargingJNCar.FINISH_MAP.get(reasonCode), "sessionId": sessionId } elif cmdCode == "2D": portStr = str(int(data[8:10], 16)) needTime = int(data[10:14], 16) needElec = int(data[14:18], 16) / 100.0 chargeType = data[18:20] coinNum = int(data[20:22], 16) cardNo = str(int(data[22:30], 16)) cardNo = self.check_and_do_card_number_reverse(cardNo, data[22:30]) cardCst = int(data[30:34], 16) / 10.0 cardOpe = data[34:36] cardType = data[36:38] sessionId = data[38:46] return { "cmdCode": cmdCode, "portStr": portStr, "needTime": needTime, "needElec": needElec, "chargeType": chargeType, "coinNum": coinNum, "cardNo": cardNo, "cardCst": cardCst, "cardOpe": cardOpe, "cardType": cardType, "sessionId": sessionId } elif cmdCode == "10": cardNo = str(int(data[8:16], 16)) cardNo = self.check_and_do_card_number_reverse(cardNo, data[8:16]) cardCst = int(data[16:18], 16) / 10.0 return { "cmdCode": cmdCode, "cardNo": cardNo, "cardCst": cardCst } # 其余的event指令 funcName = "_parse_event_{}".format(cmdCode.upper()) func = getattr(ChargingJNCar, funcName, None) if not func: logger.error("<{}> device receive an invalid cmd <{}>".format(self.device.devNo, cmdCode)) return eventData = func(data) eventData.update({"cmdCode": cmdCode}) return eventData def get_dev_setting(self): """ 汽车充电桩目前就两个设置 :return: """ return self._get_device_settings_2B() def set_device_function_param(self, request, lastSetConf): """ 汽车桩的参数设置 :param request: :param lastSetConf: :return: """ cardCst = request.POST.get("cardCst", None) or lastSetConf.get("cardCst") elecPri = request.POST.get("elecPri", None) or lastSetConf.get("elecPri") self._set_device_settings_2A(cardCst, elecPri) @property def isHaveStopEvent(self): return True def do_heartbeat(self, value, ts): logger.debug('do heartbeat for {}. value = {}; ts = {}'.format(str(self.device), value, ts)) cmd = struct.unpack_from('