# -*- coding: utf-8 -*- # !/usr/bin/env python import binascii import datetime import logging import time from apilib.monetary import VirtualCoin from apps.web.constant import DeviceCmdCode, Const from apps.web.core.adapter.base import SmartBox, fill_2_hexByte from apps.web.core.device_define.yongxin import DefaultParams, ChargeMode 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 MyUser, ConsumeRecord logger = logging.getLogger(__name__) class ChargingYongxinBox(SmartBox): def __init__(self, device): super(ChargingYongxinBox, self).__init__(device) def _send_data(self, funCode, data, cmd=DeviceCmdCode.OPERATE_DEV_SYNC, timeout=30): result = MessageSender.send( device=self.device, cmd=cmd, payload={"IMEI": self.device.devNo, "funCode": funCode, "data": data}, timeout=timeout ) if result.get("rst") == 0: return result if result.get("rst") == -1: raise ServiceException({'result': 2, 'description': u'充电桩正在玩命找网络,请您稍候再试'}) if result.get('rst') == 1: raise ServiceException({'result': 2, 'description': u'充电桩忙,无响应,请您稍候再试。也可能是您的设备版本过低,暂时不支持此功能'}) raise ServiceException({'result': 2, 'description': u'通讯错误'}) @staticmethod def _to_ascii(s): return binascii.hexlify(s).upper() @staticmethod def _to_str(h): """ 将 16进制ascii 转换成 字符串 :param h: :return: """ return binascii.unhexlify(h).lower() @staticmethod def _suit_package(package): """ 返回充电模式以及充电参数 """ unit = package["unit"] coins = VirtualCoin(package['coins']) userAcquire = package["time"] nowTime = int(time.time()) # 暂缺一种充满自停模式 if int(userAcquire) == 999 and unit == u"分钟": mode = ChargeMode.AUTO_STOP finishedTime = nowTime + 12 * 60 * 60 chargeParam = 0xFFFF return mode, chargeParam, finishedTime if unit == u"天": mode = ChargeMode.TIME_QUOTA chargeParam = userAcquire * 24 * 60 finishedTime = nowTime + chargeParam * 60 return mode, chargeParam, finishedTime elif unit == u"小时": mode = ChargeMode.TIME_QUOTA chargeParam = userAcquire * 60 finishedTime = nowTime + chargeParam * 60 return mode, chargeParam, finishedTime elif unit == u"分钟": mode = ChargeMode.TIME_QUOTA chargeParam = userAcquire finishedTime = nowTime + chargeParam * 60 return mode, chargeParam, finishedTime elif unit == u"度": mode = ChargeMode.ELEC_QUOTA chargeParam = userAcquire finishedTime = nowTime + 12 * 60 * 60 return mode, chargeParam, finishedTime elif unit == u"次": mode = ChargeMode.COIN_QUOTA chargeParam = int((coins * 100).amount) finishedTime = nowTime + 12 * 60 * 60 return mode, chargeParam, finishedTime else: raise ServiceException({"result": 2, "description": u"错误的套餐配置,请联系经销商"}) @property def isHaveStopEvent(self): return True def _response_login(self, devType): """ 回复设备的登录 :return: """ # 生成时间戳 loginTime = datetime.datetime.now().strftime('%Y%m%d%H%M%S') # 生成桩编号 loginLogicalCode = "01{:0>10}".format(self.device.logicalCode.replace("G", "")) if self.device.logicalCode.startswith("G") else "{:0>12}".format(self.device.logicalCode) # 生成 url 的连接 loginUrl = "{:0<128}".format(self._to_ascii(DefaultParams.DEFAULT_LOGIN_URL_FORMAT)) otherConf = self.device.get("otherConf", dict()) # 获取充电口数量 设备上传指令的时候 会上传当前桩的类型 如果没有设置的,就使用桩上传的 portNum = otherConf.get("portNum", devType % 10) # 获取输出功率 outputPower = otherConf.get("outputPower", 70) # 获取电子锁模式 lockMode = otherConf.get('lockMode', 0) hexLockMode = fill_2_hexByte(hex(int(lockMode)), 2) hexPortNum = fill_2_hexByte(hex(int(portNum)), 2) hexOutputPower = fill_2_hexByte(hex(int(outputPower)), 2) data = loginTime + loginLogicalCode + loginUrl + '10' + hexPortNum + hexOutputPower + hexLockMode + '10' self._send_data(funCode="12", data=data, cmd=DeviceCmdCode.OPERATE_DEV_NO_RESPONSE) def _response_elec_price(self): """ 设备请求电量费用的时候回复电量费用 """ priceList = self.device.get("otherConf", dict()).get("priceList") if not priceList: priceList = [DefaultParams.DEFAULT_ELEC_PRICE] * DefaultParams.DEFAULT_ELEC_PRICE_INTERVAL_NUM priceHex = "".join(map(lambda x: "{:0>4X}".format(int(100 * x)), priceList)) data = "{}0000000000000000".format(priceHex) self._send_data(funCode="40", data=data, cmd=DeviceCmdCode.OPERATE_DEV_NO_RESPONSE) def _response_card_start(self, port, seqNo): portHex = fill_2_hexByte(hex(int(port)), 2) data = portHex + seqNo return self._send_data("18", data, cmd=DeviceCmdCode.OPERATE_DEV_NO_RESPONSE, timeout=10) def _response_finished(self, port, seqNo, result): portHex = fill_2_hexByte(hex(int(port)), 2) seqNo = seqNo resultHex = fill_2_hexByte(hex(int(result)), 2) data = portHex + seqNo + resultHex self._send_data("1C", data, cmd=DeviceCmdCode.OPERATE_DEV_NO_RESPONSE, timeout=5) def _control_device(self, fault): data = "0001" if fault else "0000" result = self._send_data(funCode="20", data=data, cmd=DeviceCmdCode.OPERATE_DEV_SYNC) if result["data"][20:22] != "00": raise ServiceException({"result": 2, "description": u"重启失败"}) def _start_remote(self, port, seqNo, mode, chargeParam, balance): """ 远程启动设备 """ portHex = fill_2_hexByte(hex(int(port)), 2) seqNoHex = seqNo modeHex = fill_2_hexByte(hex(int(mode)), 2) chargeParamHex = fill_2_hexByte(hex(int(chargeParam)), 4) balanceHex = fill_2_hexByte(hex(int(balance*100)), 8) data = portHex + seqNoHex + modeHex + chargeParamHex + balanceHex + '{:0>16}'.format(0) result = self._send_data("16", data=data) code = result["data"][54:56] if code == "00": return result if code == '01': raise ServiceException({'result': 2, 'description': u'您没有插充电枪,请先插好充电枪,然后再操作哦!'}) if code == '02': raise ServiceException({'result': 2, 'description': u'您使用的充电枪已经在充电了,请您换其他空闲的充电枪吧!'}) if code == '03': raise ServiceException({'result': 2, 'description': u'设备故障,暂时无法使用,您本次操作不会扣费,您可以试试其他可用的设备。'}) raise ServiceException({'result': 2, 'description': u'设备返回一个未知的错误,本次操作不会扣费,请您重试看是否能够解决'}) def _stop_remote(self, port, seqNo): """ 远程停止 """ portHex = fill_2_hexByte(hex(int(port)), 2) seqNoHex = seqNo data = portHex + seqNoHex result = self._send_data("1A", data) if result['data'][54:56] != "00": raise ServiceException({'result': 2, 'description': u'该充电枪已经空闲,没有充电'}) return result def get_elec_price_from_device(self): """ 从设备上获取电价 :return: """ data = "{:016X}".format(0) result = self._send_data("42", data) return result.get("data", "")[20:212] def compare_elec_price(self, devElecPrice): # 首先看版本 低版本直接是正常的 if self.device.driverVersion != "v2.0.0": return True localPriceList = self.device.get("otherConf", dict()).get("priceList") if not localPriceList: localPriceList = [DefaultParams.DEFAULT_ELEC_PRICE] * DefaultParams.DEFAULT_ELEC_PRICE_INTERVAL_NUM localPrice = "".join(map(lambda x: "{:0>4X}".format(int(100 * x)), localPriceList)) # 比较的是原始的报文 return devElecPrice == localPrice def start_device(self, package, openId, attachParas): devElecPrice = self.get_elec_price_from_device() if not self.compare_elec_price(devElecPrice): raise ServiceException({"result": "2", "description": u"启动错误(10008)"}) chargeIndex = attachParas.get("chargeIndex") if not chargeIndex: raise ServiceException({'result': 2, 'description': u'请您选择合适的充电线路'}) if not openId: raise ServiceException({"result": 2, "description": u"本设备暂不支持上分"}) mode, chargeParam, finishedTime = self._suit_package(package) # 余额统统变成 该用户的此地址下的所有的金额 order = ConsumeRecord.objects.get(orderNo=attachParas["orderNo"]) # type: ConsumeRecord user = MyUser.objects.get(openId=openId, groupId=self.device.groupId) # type: MyUser userBalance = user.balance # 留下订单的钱给service扣除 # TODO 用心的是将 用户的全部余额下发 然后用完后退还 最好单独起个service 现在没有 所以先简陋处理 临时发布前的补丁 balance = float(userBalance.amount) # seqNo 之前的做法是放到模块侧生成 暂时不明白为什么 seqNo = "{:0>32}".format(0) result = self._start_remote(chargeIndex, seqNo, mode, chargeParam, balance) user.pay(userBalance-order.coin) result["finishedTime"] = finishedTime # 后 4位 的序列号 可能是16进制的 之前直接永序列号当作订单 所以需要将16进制转换为10 进制 现在不需要转换了 result["sequanceNo"] = result['data'][22:54] portDict = { 'startTime': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 'status': Const.DEV_WORK_STATUS_WORKING, 'finishedTime': finishedTime, 'coins': float(balance), 'isStart': True, 'sequanceNo': result["sequanceNo"], 'orderNo': attachParas['orderNo'], 'useType': 'mobile', 'openId': openId, 'refunded': False, 'chargeMode': mode, 'need': chargeParam, } Device.update_dev_control_cache(self._device['devNo'], {str(chargeIndex): portDict}) return result def stop(self, port=None): if not port: raise ServiceException({"result": 2, "description": u"请选择需要结束的充电端口"}) portCache = Device.get_port_control_cache(self.device.devNo, str(port)) seqNo = portCache.get("sequanceNo") if not seqNo: raise ServiceException({"result": 2, "description": u"当前端口无工作"}) return self._stop_remote(port, seqNo) def analyze_event_data(self, data): cmdCode = data[2:4] if cmdCode == '11': # 登录请求 return {'cmdCode': cmdCode, "devType": int(data[20:22], 16)} if cmdCode == "41": # 这个是请求电价数据 return {"cmdCode": cmdCode} if cmdCode == "19": # 充电数据实时上传 port = int(data[20:22], 16) outputVoltage = int(data[22:26], 16) / 10.0 outputElec = int(data[26:30], 16) / 10.0 elec = int(data[30:34], 16) / 100.0 spendMoney = int(data[34:38], 16) / 100.0 duration = int(data[40:44], 16) return { 'cmdCode': cmdCode, 'port': port, 'outputVoltage': outputVoltage, 'outputElec': outputElec, 'elec': elec, 'spendMoney': spendMoney, 'duration': duration, } if cmdCode == '17': # 刷卡启动的 也就是本地启动 port = int(data[20:22], 16) sequanceNo = data[22:54] cardNo = data[54:70] balance = int(data[70:78], 16) / 100.0 return { 'cmdCode': cmdCode, 'port': port, 'sequanceNo': sequanceNo, 'cardNo': cardNo, # 'isStart': True, # 'useType': 'card', 'balance': balance } if cmdCode == '1D': # 结算账单上传 port = int(data[20:22], 16) sequanceNo = data[22:54] elec = int(data[54:58], 16) / 100.0 spendMoney = int(data[58:62], 16) / 100.0 duration = int(data[62:66], 16) balance = int(data[98:106], 16) / 100.0 reason = int(data[106: 108], 16) return { 'port': port, 'sequanceNo': sequanceNo, 'elec': elec, 'spendMoney': spendMoney, 'duration': duration, 'cmdCode': '1D', 'balance': balance, 'reason': reason } def set_device_function_param(self, request, lastSetConf): if request.POST.has_key('lockMode') and request.POST.has_key('outputPower'): lockMode = int(request.POST.get('lockMode')) portNum = int(request.POST.get('portNum')) outputPower = 35 if int(request.POST.get('outputPower')) == '1' else 70 dev = Device.objects.get(devNo = self._device['devNo']) dev.otherConf['lockMode'] = lockMode dev.otherConf['portNum'] = portNum dev.otherConf['outputPower'] = outputPower dev.save() def set_device_function(self, request, lastSetConf): lock = request.POST.get("lock") if lock: return self._control_device(True) unlock = request.POST.get("unlock") if unlock: return self._control_device(False) def get_dev_setting(self): resultDict = {} dev = Device.objects.get(devNo = self._device['devNo']) resultDict['lockMode'] = dev.otherConf.get('lockMode', 0) resultDict['portNum'] = dev.otherConf.get('portNum', 1) resultDict['outputPower'] = 1 if dev.otherConf.get('outputPower', 70) == 35 else 2 return resultDict def get_port_info(self, line): ctrInfo = Device.get_dev_control_cache(self._device['devNo']) value = ctrInfo.get(line, {}) value.update({'port': line}) if 'sequanceNo' in value: sequanceNo = value['sequanceNo'] consumeRcd = ConsumeRecord.objects.get(sequanceNo = sequanceNo) elif 'orderNo' in value: orderNo = value['orderNo'] consumeRcd = ConsumeRecord.objects.get(orderNo = orderNo) else: consumeRcd = None if consumeRcd: order = {'coin': float(consumeRcd.coin)} value.update({'order': order}) return value def get_port_status(self, force=False): if force: return self.get_port_status_from_dev() statusDict = {} self.get_port_status_from_dev() ctrInfo = Device.get_dev_control_cache(self._device['devNo']) allPorts = ctrInfo.get('allPorts', 2) 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_status_from_dev(self): devInfo = MessageSender.send(self.device, DeviceCmdCode.GET_DEVINFO, {'IMEI': self._device['devNo']}) if devInfo.has_key('rst') and devInfo['rst'] != 0: if devInfo['rst'] == -1: raise ServiceException( {'result': 2, 'description': u'充电桩正在玩命找网络,请您稍候再试'}) elif devInfo['rst'] == 1: raise ServiceException( {'result': 2, 'description': u'充电桩忙,无响应,请您稍候再试。也可能是您的设备版本过低,暂时不支持此功能'}) devObj = Device.objects.get(devNo = self._device['devNo']) chargerType = devInfo.get('chargerType', None) if chargerType is not None: devObj['otherConf']['chargerType'] = int(chargerType) devObj.save() else: chargerType = devObj['otherConf'].get('chargerType', None) if chargerType is None: raise ServiceException({'result': 2, 'description': u'请您重启下充电桩(10001)'}) if "port1Status" not in devInfo: raise ServiceException({'result': 2, 'description': u'请您重启下充电桩(10002)'}) result = {} usedPorts, usePorts = 0, 0 if int(chargerType) in [129, 145]: portNum = 1 port1Status = devInfo['port1Status'] if port1Status == 0: result['1'] = {'status': Const.DEV_WORK_STATUS_IDLE} usePorts += 1 elif port1Status == 1: result['1'] = {'status': Const.DEV_WORK_STATUS_IDLE} usePorts += 1 elif port1Status == 2: result['1'] = {'status': Const.DEV_WORK_STATUS_WORKING} usedPorts += 1 else: result['1'] = {'status': Const.DEV_WORK_STATUS_FINISHED} usedPorts += 1 else: portNum = 2 port1Status = devInfo['port1Status'] if port1Status == 0: result['1'] = {'status': Const.DEV_WORK_STATUS_IDLE} usePorts += 1 elif port1Status == 1: result['1'] = {'status': Const.DEV_WORK_STATUS_CONNECTED} usePorts += 1 else: result['1'] = {'status': Const.DEV_WORK_STATUS_WORKING} usedPorts += 1 port2Status = devInfo['port2Status'] if port2Status == 0: result['2'] = {'status': Const.DEV_WORK_STATUS_IDLE} usePorts += 1 elif port2Status == 1: result['2'] = {'status': Const.DEV_WORK_STATUS_CONNECTED} usePorts += 1 else: result['2'] = {'status': Const.DEV_WORK_STATUS_WORKING} usedPorts += 1 Device.update_dev_control_cache(self._device['devNo'], {'allPorts': portNum, 'usedPorts': usedPorts, 'usePorts': usePorts}) ctrInfo = Device.get_dev_control_cache(self._device['devNo']) for strPort, info in result.items(): if ctrInfo.has_key(strPort): ctrInfo[strPort].update({'status': info['status']}) else: ctrInfo[strPort] = info Device.update_dev_control_cache(self._device['devNo'], ctrInfo) return result def active_deactive_port(self, port, active): if not active: self.stop_charging_port(port) devInfo = Device.get_dev_control_cache(self._device['devNo']) portCtrInfo = devInfo.get(str(port), {}) portCtrInfo.update({'isStart': False, 'status': Const.DEV_WORK_STATUS_IDLE, 'endTime': datetime.datetime.now().strftime(Const.DATETIME_FMT)}) newValue = {str(port): portCtrInfo} Device.update_dev_control_cache(self._device['devNo'], newValue) else: raise ServiceException({'result': 2, 'description': u'此设备不支持直接打开端口'})