# -*- coding: utf-8 -*- # !/usr/bin/env python import time import logging 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.device.models import Device logger = logging.getLogger(__name__) class ChangYuanPower(SmartBox): FINISH_REASON_MAP = { "E0": "设备过压停止", "E1": "设备过流停止", "E2": "设备超功率停止", "E3": "充电时间使用完毕", "E4": "正常结束", "E5": "支付金额使用完", "E6": "设备温度超过停止", "E7": "计量芯片通信失败", "E8": "远程停止", } PAY_TYPE_MAP = { "cash": "01", "vCard": "02", "coin": "03" } @staticmethod def _parse_D0_data(data): """ 解析 设备参数 返还的数据 :param data: :return: """ elecPrice = int(data[6:10], 16) / 100.0 cardPrice = int(data[10:14], 16) / 100.0 chargeTimeMax = int(data[14:18], 16) portPowerMax = int(data[18:22], 16) powerMax = int(data[22:26], 16) powerCheckMin = int(data[26:28], 16) powerCheckTime = int(data[28:30], 16) cardFree = True if data[30:32] == 'AA' else False voice = int(data[32:34], 16) return locals() @staticmethod def _parse_F4_data(data): """ 解析 请求卡同步 指令 :param data: :type data: :return: :rtype: """ cardType = data[6:8] cardNo = data[8:16] cardBalance = int(data[16:22], 16) if cardType == "01": cardBalance = cardBalance / 100.0 return locals() @staticmethod def _parse_F3_data(data): """ 解析 卡余额同步 结果 :param data: :type data: :return: :rtype: """ cardType = data[6:8] cardNo = data[8:16] cardBalance = int(data[16:22], 16) asyncStatus = data[22:24] == "AA" sid = int(data[24:28], 16) if cardType == "01": cardBalance = cardBalance / 100.0 return locals() @staticmethod def _parse_D7_data(data): """ 火警数据解析 :param data: :type data: :return: :rtype: """ return {'data': data[6:8]} @staticmethod def _parse_D8_data(data): """ 定时上传 桩的工作状态 :param data: :type data: :return: :rtype: """ defaultPortInfo = {"status": Const.DEV_WORK_STATUS_IDLE} portInfo = dict.fromkeys("1234", defaultPortInfo) if len(data) == 22: voltage = int(data[6:10], 16) / 10.0 temperature = int(data[12:14], 16) if data[10:12] == "01": temperature = -temperature return {"voltage": voltage, "temperature": temperature, "portInfo": portInfo} statusHex = data[6:14] leftBalanceHex = data[14:30] usedElecHex = data[30:46] if data[30:46] else '0' * 16 usedTimeHex = data[46:62] if data[30:46] else '0' * 16 powerHex = data[62:78] voltage = int(data[-16:-12], 16) / 10.0 temperature = int(data[-10:-8], 16) if data[-12:-10] == "01": temperature = -temperature for portNum in range(4): item = {} tempStatus = statusHex[portNum * 2:portNum * 2 + 2] tempLeftBalance = int(leftBalanceHex[portNum * 4: portNum * 4 + 4], 16) / 100.0 tempUsedElec = int(usedElecHex[portNum * 4: portNum * 4 + 4], 16) / 100.0 or '0' tempUsedTime = int(usedTimeHex[portNum * 4: portNum * 4 + 4], 16) tempPower = int(powerHex[portNum * 4: portNum * 4 + 4], 16) if tempStatus == "00": status = Const.DEV_WORK_STATUS_IDLE elif tempStatus == "01": status = Const.DEV_WORK_STATUS_WORKING elif tempStatus == "04": status = Const.DEV_WORK_STATUS_FAULT_RELAY_CONNECT else: status = Const.DEV_WORK_STATUS_IDLE item["status"] = status item["leftMoney"] = tempLeftBalance item["usedElec"] = tempUsedElec item["usedTime"] = tempUsedTime item["power"] = tempPower portInfo[str(portNum + 1)] = item.copy() return {"voltage": voltage, "temperature": temperature, "portInfo": portInfo} @staticmethod def _parse_D8_data_two_port(data): """ 定时上传 桩的工作状态 :param data: :type data: :return: :rtype: """ defaultPortInfo = {"status": Const.DEV_WORK_STATUS_IDLE} portInfo = dict.fromkeys("12", defaultPortInfo) if len(data) == 22: voltage = int(data[6:10], 16) / 10.0 temperature = int(data[12:14], 16) if data[10:12] == "01": temperature = -temperature return {"voltage": voltage, "temperature": temperature, "portInfo": portInfo} statusHex = data[6:14] leftBalanceHex = data[14:30] usedElecHex = data[30:46] if data[30:46] else '0' * 16 usedTimeHex = data[46:62] if data[30:46] else '0' * 16 powerHex = data[62:78] voltage = int(data[-16:-12], 16) / 10.0 temperature = int(data[-10:-8], 16) if data[-12:-10] == "01": temperature = -temperature for portNum in range(2): item = {} tempStatus = statusHex[portNum * 2:portNum * 2 + 2] tempLeftBalance = int(leftBalanceHex[portNum * 4: portNum * 4 + 4], 16) / 100.0 tempUsedElec = int(usedElecHex[portNum * 4: portNum * 4 + 4], 16) / 100.0 or '0' tempUsedTime = int(usedTimeHex[portNum * 4: portNum * 4 + 4], 16) tempPower = int(powerHex[portNum * 4: portNum * 4 + 4], 16) if tempStatus == "00": status = Const.DEV_WORK_STATUS_IDLE elif tempStatus == "01": status = Const.DEV_WORK_STATUS_WORKING elif tempStatus == "04": status = Const.DEV_WORK_STATUS_FAULT_RELAY_CONNECT else: status = Const.DEV_WORK_STATUS_IDLE item["status"] = status item["leftMoney"] = tempLeftBalance item["usedElec"] = tempUsedElec item["usedTime"] = tempUsedTime item["power"] = tempPower portInfo[str(portNum + 1)] = item.copy() return {"voltage": voltage, "temperature": temperature, "portInfo": portInfo} @staticmethod def _parse_D9_data(data): """ 充电开始 上传 :param data: :type data: :return: :rtype: """ cardNo = data[6:14] cardBalance = int(data[14:20], 16) / 100.0 allPayMoney = int(data[20:24], 16) / 100.0 payMoney = int(data[24:28], 16) / 100.0 portStr = str(int(data[28:30], 16)) return locals() @staticmethod def _parse_DA_data(data): """ 充电结束上传 :param data: :type data: :return: :rtype: """ portStr = str(int(data[6:8], 16)) reasonCode = data[8:10] cardNo = data[10:18] usedElec = int(data[18:22], 16) / 100.0 # 本单结束 该单使用电量 单位:度 usedTime = int(data[22:26], 16) # 本单结束,该单使用时间 单位:分钟 leftBalance = int(data[26:30], 16) / 100.0 # 本单结束,该单剩余金额 单位:元 desc = ChangYuanPower.FINISH_REASON_MAP.get(reasonCode, u"未知停止方式") return locals() @staticmethod def _parse_DF_data(data): """ 实体卡返费 的指令 :param data: :type data: :return: :rtype: """ cardNo = data[6:14] beforeRefund = int(data[14:20], 16) / 100.0 refund = int(data[20:24], 16) / 100.0 afterRefund = int(data[24:30], 16) / 100.0 return locals() @staticmethod def _parse_COMMON_data(data): """ 解析 通用的数据返回 一般表示成功还是失败 :param data: :type data: :return: :rtype: """ return True if data[6:10] == "4F4B" else False @staticmethod def _parse_DE_data(data): """ 解析 10 个未返费的卡号以及剩余金额 :param data: :type data: :return: :rtype: """ return def _send_data(self, funCode, data, cmd = DeviceCmdCode.OPERATE_DEV_SYNC, timeout = MQTT_TIMEOUT.NORMAL): result = MessageSender.send(device = self.device, cmd = cmd, 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 _start(self, payType, payMoney, port): """ 启动设备 :param payType: :param payMoney: :return: """ data = self.PAY_TYPE_MAP[payType] data += int_to_hex(int(float(payMoney) * 100)) data += "0000" data += '%.2d' % int(port) result = self._send_data("D2", data, timeout = MQTT_TIMEOUT.START_DEVICE) if "data" not in result or not ChangYuanPower._parse_COMMON_data(result.get("data")): raise ServiceException({"result": "2", "description": u"设备启动错误,请联系经销商协助解决"}) return result @staticmethod def transform_password(password): passwordHex = "" while password: passwordHex += fill_2_hexByte(hex(int(password[: 2])), 2) password = password[2:] return passwordHex def _set_password(self, password): """ 设置设备的小区密码 密码是否还需要再校验,以及是否是0开头 """ if not password.isdigit(): raise ServiceException({"result": "2", "description": u"密码必须必须为0-9数字"}) if len(password) != 10: raise ServiceException({"result": 0, "description": u"密码长度必须为10位"}) passwordHex = self.transform_password(password) result = self._send_data("DB", data = passwordHex) if "data" not in result or not ChangYuanPower._parse_COMMON_data(result.get("data")): raise ServiceException({"result": "2", "description": u"设置小区密码失败,请重新试试"}) return result def _set_send_card(self, oldPassword, newPassword): """ 设置卡机模式 用于重置 实体卡的小区密码 """ if not oldPassword.isdigit() or not newPassword.isdigit(): raise ServiceException({"result": "2", "description": u"密码必须必须为0-9数字"}) if len(oldPassword) != 10 or len(newPassword) != 10: raise ServiceException({"result": 0, "description": u"密码长度必须为10位"}) oldPasswordHex = self.transform_password(oldPassword) newPasswordHex = self.transform_password(newPassword) result = self._send_data("DC", data = oldPasswordHex + newPasswordHex) if "data" not in result or not ChangYuanPower._parse_COMMON_data(result.get("data")): raise ServiceException({"result": "2", "description": u"设置发卡机模式失败,请重新试试"}) return result def _reboot_device(self): result = self._send_data("D5", "0000") if "data" not in result or not ChangYuanPower._parse_COMMON_data(result.get("data")): raise ServiceException({"result": "2", "description": u"设备复位错误,请重新试试"}) return result def _get_not_refund_record(self): """ 获取 10 条 未返费的卡号以及剩余金额 :return: """ result = self._send_data("DE", "00") data = result.get("data", "") if ChangYuanPower._parse_COMMON_data(data): raise ServiceException({"result": "2", "description": u"获取未返费记录错误,请重新试试"}) noRefund = list() data = data[6: -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 _async_card_balance(self, cardType, cardNo, asyncMoney): """ 同步卡内余额 :param cardType: :param cardNo: :param asyncMoney: :return: """ balance = asyncMoney if cardType == "00" else asyncMoney * 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 _ack_finished_massage(self, daid): return MessageSender.send(device = self.device, cmd = DeviceCmdCode.OPERATE_DEV_NO_RESPONSE, payload = { "IMEI": self.device.devNo, "data": "", "cmd": 220, "daid": daid, "funCode": "FA"}) # 获取10条未返费的记录 暂时未使用 def _no_refund_record(self): result = self._send_data("DE", "00") data = result["data"] noRefund = list() data = data[6: -8] for i in xrange(0, 120, 12): tempData = data[i:i + 12] cardNo = tempData[:8] amount = tempData[8:] if cardNo == "FFFFFFFF" or cardNo == "00000000": continue amount = int(amount, 16) / 100.0 noRefund.append({"cardNo": cardNo, "amount": amount}) return noRefund def _set_all_settings(self,newSetConf,lastSetConf): elecPrice = newSetConf.POST.get("elecPrice") or lastSetConf.get("elecPrice") cardPrice = newSetConf.POST.get("cardPrice") or lastSetConf.get("cardPrice") chargeTimeMax = newSetConf.POST.get("chargeTimeMax") or lastSetConf.get("chargeTimeMax") portPowerMax = newSetConf.POST.get("portPowerMax") or lastSetConf.get("portPowerMax") powerMax = newSetConf.POST.get("powerMax") or lastSetConf.get("powerMax") powerCheckMin = newSetConf.POST.get("powerCheckMin") or lastSetConf.get("powerCheckMin") powerCheckTime = newSetConf.POST.get("powerCheckTime") or lastSetConf.get("powerCheckTime") cardFree = newSetConf.POST.get("cardFree") or '00' if lastSetConf.get("powerCheckTime") else 'AA' voice = newSetConf.POST.get("voice") or lastSetConf.get("voice") if float(elecPrice) < 0 or float(elecPrice) > 5.00: raise ServiceException({"result": "2", "description": u"电价设置范围为0-5.00元"}) if float(cardPrice) < 1 or float(cardPrice) > 650: raise ServiceException({"result": "2", "description": u"刷卡预扣金额范围为1-650元"}) if int(chargeTimeMax) < 0 or int(chargeTimeMax) > 9999: raise ServiceException({"result": "2", "description": u"单次充电时间设置范围为0-9999分钟"}) if int(portPowerMax) < 0 or int(portPowerMax) > 14000: raise ServiceException({"result": "2", "description": u"单路功率设置范围为0-14000瓦"}) if int(powerMax) < 0 or int(powerMax) > 55000: raise ServiceException({"result": "2", "description": u"总计功率设置范围为0-55000瓦"}) if int(powerCheckMin) < 0 or int(powerCheckMin) > 120: raise ServiceException({"result": "2", "description": u"功率检测范围设置范围为0-120瓦"}) if int(powerCheckTime) < 0 or int(powerCheckTime) > 255: raise ServiceException({"result": "2", "description": u"最小功率检测时间设置范围为0-255秒"}) if int(voice) < 0 or int(voice) > 8: raise ServiceException({"result": "2", "description": u"喇叭音量设置范围为0-8"}) data = int_to_hex(int(Decimal(elecPrice) * 100)) data += int_to_hex(int(Decimal(cardPrice)) * 100) data += int_to_hex(int(chargeTimeMax)) data += int_to_hex(int(portPowerMax)) data += int_to_hex(int(powerMax)) data += int_to_hex(int(powerCheckMin), 2) data += int_to_hex(int(powerCheckTime), 2) data += cardFree data += int_to_hex(int(voice), 2) result = self._send_data("D1", data) if "data" not in result or not ChangYuanPower._parse_COMMON_data(result.get("data")): raise ServiceException({"result": "2", "description": u"设置设备参数错误,请联系经销商协助解决"}) return result def test(self, coins,port): """ 测试端口 测试机器启动 固定端口为 01 { "IMEI": "865650040606119", "cmd": 210, "data": "0303E8000001", "funCode": "D2" }, """ return self._start('coin', coins, port) def get_dev_setting(self): """ 获取设备参数 :return: """ result = self._send_data("D0", "00") data = result.get("data") devSetting = self._parse_D0_data(data) disable = self.device.get('otherConf',{}).get('disableDevice') needBindCard = self._device.get("otherConf", dict()).get("needBindCard", True) devSetting.update({'disable': disable, 'needBindCard': needBindCard}) dev_control_cache = Device.get_dev_control_cache(self.device.devNo) devSetting['temperature'] = dev_control_cache.get('temperature', "获取中...") devSetting['voltage'] = dev_control_cache.get('voltage', "获取中...") return devSetting def set_device_function_param(self, request, lastSetConf): # 设置小区密码 pwd = request.POST.get('pwd') if pwd: return self._set_password(pwd) # 设置发卡机模式 old_pwd = request.POST.get('old_pwd') new_pwd = request.POST.get('new_pwd') if old_pwd and new_pwd: return self._set_send_card(old_pwd, new_pwd) # 设置基本参数 else: return self._set_all_settings(request,lastSetConf) def set_device_function(self, request, lastSetConf): if request.POST.has_key("cardFree"): cardFree = request.POST.get('cardFree') cardFree = 'AA' if cardFree else '00' request.POST['cardFree'] = cardFree return self._set_all_settings(request, lastSetConf) if request.POST.has_key("reboot"): self._reboot_device() if request.POST.has_key("disable"): self.set_dev_disable(request.POST.get("disable")) if "needBindCard" in request.POST: needBindCard = 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) def get_port_status(self, force = False): """ 获取设备状态 昌原的状态都是被动获取的 :param force: :return: """ devCache = Device.get_dev_control_cache(self._device['devNo']) statusDict = dict() only_two_port = self.device.devType.get("features", {}).get('only_two_port', False) if only_two_port: allPorts = 2 else: allPorts = 4 for portNum in range(allPorts): tempDict = devCache.get(str(portNum + 1), {}) if "status" in tempDict: statusDict[str(portNum + 1)] = {'status': tempDict.get('status')} elif "isStart" in tempDict: if tempDict['isStart']: statusDict[str(portNum + 1)] = {'status': Const.DEV_WORK_STATUS_WORKING} else: statusDict[str(portNum + 1)] = {'status': Const.DEV_WORK_STATUS_IDLE} else: statusDict[str(portNum + 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): return self.get_port_info(False) def get_port_info(self, port): """ 获取 端口的详细信息 :param port: :return: """ devCache = Device.get_dev_control_cache(self.device.devNo) return devCache.get(str(port), dict()) @property def isHaveStopEvent(self): return True def set_dev_disable(self, disable): if disable: status = "00AA" else: status = "0055" result = self._send_data(funCode = "DD", data = status) if "data" not in result or not ChangYuanPower._parse_COMMON_data(result.get("data")): 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 stop(self,port): self.stop_charging_port(port) def stop_charging_port(self, port): portHex = fill_2_hexByte(hex(int(port)), 2) result = self._send_data('D6', data = portHex) data = result.get("data") if data[6: 8] != "4F": raise ServiceException({"result": 2, "description": u"停止充电失败,请重新试试"}) # 这里只下发命令 不清理端口缓存,等上报事件清除 # Device.clear_port_control_cache(self.device.devNo,int(port)) def start_device(self, package, openId, attachParas): chargeIndex = attachParas.get("chargeIndex") if not chargeIndex: raise ServiceException({"result": 2, "description": u"请选择正确的充电端口"}) # TODO zjl 套餐单位是否正确 unit = package.get("unit") coins = "{:.2f}".format(float(package.get("coins"))) price = "{:.2f}".format(float(package.get("price"))) rechargeRcdId = attachParas.get("linkedRechargeRecordId") # 这是个对象 orderNo = attachParas.get("orderNo") startTimeStamp = int(time.time()) # 先不考虑 续充的问题 后续做扩展 portCache = { "openId": openId, "isStart": True, "coins": coins, "allPayMoney": 0, "orderNo": orderNo, "startTime": timestamp_to_dt(startTimeStamp).strftime("%Y-%m-%d %H:%M:%S"), "status": Const.DEV_WORK_STATUS_WORKING, "consumeType": "mobile", # 用来显示的 显示在端口管理里面 } if self._vcard_id: portCache.update({"vCardId": self._vcard_id}) payType = "vCard" consumeType = "mobile_vcard" allPayMoney = 0 elif rechargeRcdId: portCache.update({"rechargeRcdId": str(rechargeRcdId)}) payType = "cash" consumeType = "mobile" allPayMoney = price else: payType = "coin" allPayMoney = 0 consumeType = "mobile" # TODO payType是控制退费的 consumeType 是控制端口管理里面显示的 portCache.update({"payType": payType, 'allPayMoney': allPayMoney,'consumeType':consumeType}) result = self._start(payType, coins, chargeIndex) result["finishedTime"] = startTimeStamp + 24 * 60 * 60 Device.update_dev_control_cache(self.device.devNo, {str(chargeIndex): portCache,"consumeType": consumeType}) return result def analyze_event_data(self, data): """ 解析事件 :param data: :return: """ cmdCode = data[2:4] if cmdCode == "F3": eventData = ChangYuanPower._parse_F3_data(data) elif cmdCode == "F4": eventData = ChangYuanPower._parse_F4_data(data) elif cmdCode == "D7": eventData = ChangYuanPower._parse_D7_data(data) elif cmdCode == "D8": only_two_port = self.device.devType.get("features", {}).get('only_two_port', False) if not only_two_port: eventData = ChangYuanPower._parse_D8_data(data) else: eventData = ChangYuanPower._parse_D8_data_two_port(data) elif cmdCode == "D9": eventData = ChangYuanPower._parse_D9_data(data) elif cmdCode == "DA": eventData = ChangYuanPower._parse_DA_data(data) elif cmdCode == "DF": eventData = ChangYuanPower._parse_DF_data(data) else: logger.error("error cmdCode <{}>, data is <{}>".format(cmdCode, data)) return eventData.update({"cmdCode": cmdCode}) return eventData def get_device_function_by_key(self, data): if data == 'noRefund': res = self._no_refund_record() if not res: return {} return {'noRefund': res} def active_deactive_port(self, port, active): if not active: self.stop_charging_port(port) else: raise ServiceException({'result': 2, 'description': u'此设备不支持直接打开端口'})