# -*- coding: utf-8 -*- # !/usr/bin/env python # 整个业务流程的 控制的最关键的三个函数 _open, _charge, _stop # 粤万通 充电柜 基本的业务流程可以描述为 # 1. 扫码/刷卡 启动设备 此时调用 _open 函数,并创建相应的订单以及开锁密码 开锁密码在整个的充电过程中不能改变 用于设备离线时候直接从触控屏解锁 # 2. 门锁打开的时候 等候用户下一步操作(放入电池、关闭门锁) # 3. 主板方检测到门锁关闭之后 上报门锁关闭信号 服务器接收到关锁信号之后 调用_charge 函数 ,注意此时仅仅充电不开锁 开启端口的充电 # 4. 在整个充电的过程中 主板会不断地上报 该端口的充电数据 C0指令,其中 power 为瞬时功率 chargeTime 为累计充电时间 stayTime 为累计占位时间 # 5. 在 到达最大充电时间 或者 主板检测电池充满 之后, 会上报充电结束的指令 C1,同时主板停止充电,开始进入 占位状态 # 6. 需要注意的是,在充电柜业务流程中,停止充电并不意味着 流程结束,也不代表可以对其进行 费用结算 # 7. 关于充电柜的业务结束一共有以下 3种情况: # 7.1 用户再次扫码,并点击停止充电按钮,此时触发 stop 函数,服务器通过 _stop 发起对主板的开锁,主板接收到之后 开锁并同时上报最后一次数据状态 # 7.2 用户刷卡启动的设备,需要用户刷卡结束,用户第二次刷卡 后,主板上报刷卡请求充电指令,服务器接收到之后 通过 _stop 发起对于主板的开锁 同时上报最后一次充电状态 # 7.3 用户直接在主板上选择密码开锁, 输入密码(开启充电时候下发的密码),主板接收到之后,无条件开锁,同时上报最后一次充电状态。一般用于设备离线的时候使用 # 8. 用户以任何方式 结束充电柜业务之后,需要对其订单进行结算,并将整个过程中的费用记录到相应订单上 # 关于粤万通新式充电柜的计费 # 计费公式为: 总费用 = 充电费用 + 占位费用 # 对于占位费用,目前的计费方式就是 占位时间 ✖ 占位单价 # 对于充电费用,目前一共有 两种计费方式可供选择,以下 # 充电计费方式一:按时间计费: # 本次粤万通计费将时间计费修改为按时段计费,例如 # 经销商设置8:00-9:00 单价为1小时1元;9:00-10:00 单价为1小时2元;默认单价为1小时0.5元 # 某用户从7:00充电一直到10:30 共四个小时 # 7:00-8:00时间段经销商没有设置单价, 则费用为默认单价0.5元 # 8:00-9:00时间段设置了单价,则费用为1元 # 9:00-10:00时间段设置了单价,则费用为2元 # 10:00-10:30 时间段没有设置单价,则费用为默认单价 0.5*0.5小时 为0.25元 # 此次按时间充电的费用为 0.5+1+2+0.25 = 3.75元 # 充电计费方式二:按功率计费,即经销商预设功率挡位以及相应单价 例如 # 经销商设置为100-200w 1小时1元;200-400w 1小时2元,默认单价为0.5元 # 用户充电过程中一共1小时,其中半小时功率为95w,半小时功率为 240w,则充电总费用为 1.5元 # 充电计费的注意事项: # 1.按时间计费的时间是订单的初始时间-结束时间,也就是说只要最关键的报文结束报文 服务器能够接收到,就一定能计算出费用 # 2.按功率计费需要依赖每次的充电状态上报,即C0指令,C0指令主板会有ack机制(最新的修改已经将ack置于模块侧回复),但若某次没有收到,则以最近一次的功率为主计算该段的费用 # 3.计费有最低费用和最高费用,假设经销商设置的最低费用为1元,最高费用为10元,假设某次用户总费用为0.5元,则系统收费为1元;同理也不能超过最大费用 import binascii import datetime import logging import os import random import time import simplejson as json from mongoengine import DoesNotExist from apilib.monetary import VirtualCoin, RMB from apilib.utils_string import make_title_from_dict from apps.web.common.proxy import ClientConsumeModelProxy from apps.web.constant import DeviceCmdCode, Const, MQTT_TIMEOUT from apps.web.core.adapter.base import SmartBox, reverse_hex, fill_2_hexByte from apps.web.core.exceptions import ServiceException from apps.web.core.networking import MessageSender from apps.web.device.models import Group, Device, DevicePortLastReport, DeviceType from apps.web.user.models import ConsumeRecord, ServiceProgress, MyUser from apps.web.core.device_define.ywt_chongdiangui_new import DefaultParams, Calculater from apps.web.utils import concat_user_login_entry_url from taskmanager.mediator import task_caller logger = logging.getLogger(__name__) class ChargeCabinet(SmartBox): def __init__(self, *args, **kwargs): super(ChargeCabinet, self).__init__(*args, **kwargs) # 主要用于计算费用 self.devNo = self.device.devNo @property def is_support_auto_charge(self): """ 是否支持自动供电 主板的新特性 发送新的指令不需要发送充电 关门后直接启动充电 兼容之前的设备特性和现在的设备类型特性 """ # 先找设备 if self.device.support_dev_type_features("support_auto_charge_after_close_door"): return True return False @property def password(self): return random.randint(0x2710, 0xFFFF) def _send_data(self, funCode, sendData, cmd=None, timeout=MQTT_TIMEOUT.NORMAL, orderNo=None): """ 发送报文封装 :param funCode: :param sendData: :param cmd: :param timeout: :param orderNo: :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": sendData }, timeout = timeout) if result.has_key("rst"): if result["rst"] == -1: raise ServiceException({'result': 2, 'description': u'充电桩正在玩命找网络,请稍候再试'}) elif result["rst"] == 1: raise ServiceException({'result': 2, 'description': u'充电桩主板连接故障'}) elif result["rst"] == 0: return result else: raise ServiceException({'result': 2, 'description': u'系统错误'}) else: raise ServiceException({'result': 2, 'description': u'系统错误'}) @staticmethod def _to_str(data): return binascii.unhexlify(data) @staticmethod def _to_ascii(data): return binascii.hexlify(data) @staticmethod def _suit_power_package(package): """适配之前的 功率计费规则""" if not package or "lowLimit" not in package[0].keys(): return package newPackage = list() for _item in package: newPackage.append({"max": _item["upLimit"], "min": _item["lowLimit"], "price": _item["price"] * 60 / _item["time"]}) newPackage.append({"max": "default", "min": "default", "price": 0}) return newPackage @staticmethod def _suit_time_package(package): """适配之前的 时间计费规则""" if not package or not isinstance(package, (int, str)): return package return [{"max": "default", "min": "default", "price": int(package)}] def _notify_user_service_over(self, managerialOpenId, extra, isPaid=True): title = make_title_from_dict(extra) notifyData = { "title": title, "service": u"已使用账户余额自动结算此次消费" if isPaid else u"您的账户余额已不足以抵扣此次消费,请前往账单中心进行支付", "finishTime": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "remark": u'谢谢您的支持' } task_caller( func_name='report_to_user_via_wechat', openId=managerialOpenId, dealerId=self.device.get("ownerId"), templateName="service_complete", **notifyData ) @staticmethod def _parse_event_A1(data): return dict() @staticmethod def _parse_event_A2(data): return dict() @staticmethod def _parse_event_B0(data): cardPre = ChargeCabinet._to_str(data[8: 16]) cardNo = ChargeCabinet._to_str(data[16: 32]) portStr = str(int(data[32: 34], 16)) return { "cardPre": cardPre, "cardNo": cardNo, "cardNoHex": data[8: 32], # zjl 16->8 "portStr": portStr, "portHex": data[32: 34] # zjl new } @staticmethod def _parse_event_B1(data): cardPre = ChargeCabinet._to_str(data[8: 16]) cardNo = ChargeCabinet._to_str(data[16: 32]) portStr = str(int(data[32: 34], 16)) orderNoHex = data[34: 48] orderNo = str(int(reverse_hex(orderNoHex), 16)) return { "cardPre": cardPre, "cardNo": cardNo, "cardNoHex": data[8: 32], # zjl 16->8 "portStr": portStr, "portHex": data[32:34], # zjl "orderNoHex": orderNoHex, "orderNo": orderNo } @staticmethod def _parse_event_C0(data): orderNoHex = data[8: 22] orderNo = str(int(reverse_hex(orderNoHex), 16)) portStr = str(int(data[22: 24], 16)) voltage = int(reverse_hex(data[24: 28]), 16) power = int(reverse_hex(data[32: 36]), 16) elec = int(reverse_hex(data[36: 44]), 16) / 1000.0 chargeTime = int(reverse_hex(data[44: 48]), 16) stayTime = int(reverse_hex(data[48:52]), 16) temperature = int(reverse_hex(data[52: 56]), 16) status = DefaultParams.STATUS_MAP.get(data[56: 58], Const.DEV_WORK_STATUS_IDLE) return { "orderNo": orderNo, "orderNoHex": orderNoHex, "portStr": portStr, "voltage": voltage, "power": power, "elec": elec, "chargeTime": chargeTime, "temperature": temperature, "status": status, "stayTime": stayTime } @staticmethod def _parse_event_E0(data): portStr = str(int(data[8: 10], 16)) faultHex = data[10: 14] fault = str(int(faultHex[2:4]+faultHex[:2], 16)) faultReason = DefaultParams.FAULT_MAP.get(fault) return { "portHex": data[8: 10], "portStr": portStr, "faultCode": fault, "statusInfo": faultReason } @staticmethod def _parse_event_C1(data): orderNoHex = data[8: 22] orderNo = str(int(reverse_hex(orderNoHex), 16)) portHex = data[22: 24] portStr = str(int(data[22: 24], 16)) voltage = int(reverse_hex(data[24: 28]), 16) power = int(reverse_hex(data[32: 36]), 16) elec = int(reverse_hex(data[36: 44]), 16) / 1000.0 chargeTime = int(reverse_hex(data[44: 48]), 16) stayTime = int(reverse_hex(data[48:52]), 16) temperature = int(reverse_hex(data[52: 56]), 16) reasonCode = data[56: 58] reason = DefaultParams.STOP_REASON_MAP.get(reasonCode) return { "orderNoHex": orderNoHex, "orderNo": orderNo, "portHex": portHex, "portStr": portStr, "voltage": voltage, "power": power, "elec": elec, "chargeTime": chargeTime, "temperature": temperature, "reasonCode": reasonCode, "reason": reason, "stayTime": stayTime } @staticmethod def _parse_event_C3(data): portStr = str(int(data[8:10], 16)) doorStatus = data[10: 12] return { "portStr": portStr, "doorStatus": doorStatus } @staticmethod def _parse_result_B2(data): orderNoHex = data[8: 22] orderNo = str(int(reverse_hex(orderNoHex), 16)) portHex = data[22: 24] portStr = str(int(data[22: 24], 16)) voltage = int(reverse_hex(data[24: 28]), 16) power = int(reverse_hex(data[32: 36]), 16) elec = int(reverse_hex(data[36: 44]), 16) / 1000.0 chargeTime = int(reverse_hex(data[44: 48]), 16) stayTime = int(reverse_hex(data[48:52]), 16) return { "orderNoHex": orderNoHex, "orderNo": orderNo, "portHex": portHex, "portStr": portStr, "voltage": voltage, "power": power, "elec": elec, "chargeTime": chargeTime, "stayTime": stayTime } @staticmethod def _parse_event_CA(data): data = data[8: -4] result = list() while data: orderNoHex = data[: 14] orderNo = str(int(reverse_hex(data[:14]), 16)) portStr = str(int(data[14: 16], 16)) voltage = int(reverse_hex(data[16: 20]), 16) power = int(reverse_hex(data[24: 28]), 16) elec = int(reverse_hex(data[28: 36]), 16) / 1000.0 chargeTime = int(reverse_hex(data[36: 40]), 16) stayTime = int(reverse_hex(data[40: 44]), 16) temperature = int(reverse_hex(data[44: 48]), 16) status = DefaultParams.STATUS_MAP.get(data[48: 50], Const.DEV_WORK_STATUS_IDLE) data = data[50: ] result.append({ "orderNo": orderNo, "orderNoHex": orderNoHex, "portStr": portStr, "voltage": voltage, "power": power, "elec": elec, "chargeTime": chargeTime, "temperature": temperature, "status": status, "stayTime": stayTime }) return {"subChargeList": result} # -----------------------------------------------将开门、充电以及停止充电区分开-------------------------------------------------------- def _open(self, port, orderNo, pw): """ 打开柜门 不启动充电 为用户使用的第一步 :param port: 端口号 :param orderNo: 订单编号 :param pw: 开锁密码 :return: """ operHex = "03" chargeTimeHex = "0000" orderNoHex = fill_2_hexByte(hex(int(orderNo)), 14, reverse=True) portHex = fill_2_hexByte(hex(int(port)), 2, reverse=True) pwHex = fill_2_hexByte(hex(int(pw)), 4, reverse=True) data = orderNoHex+portHex+chargeTimeHex+operHex+pwHex return self._send_data("B2", data, timeout=15) def _charge(self, port, orderNo, pw, door=False, chargeTime=0x0258): """ 直接下发的充电指令 不打开柜门 :param port: 端口号 :param orderNo: 订单编号 :param pw: 开锁密码 :param door: true 表示开锁 false 表示不开所 :param chargeTime: 充电时间 最大充电时间为600分钟 :return: """ operHex = "01" if not door else "02" orderNoHex = fill_2_hexByte(hex(int(orderNo)), 14, reverse=True) portHex = fill_2_hexByte(hex(int(port)), 2, reverse=True) pwHex = fill_2_hexByte(hex(int(pw)), 4, reverse=True) chargeTimeHex = fill_2_hexByte(hex(int(chargeTime)), 4, reverse=True) data = orderNoHex+portHex+chargeTimeHex+operHex+pwHex return self._send_data("B2", data, timeout=15) def _new_charge(self,port, orderNo, pw, chargeTime=0x0258): operHex = "02" orderNoHex = fill_2_hexByte(hex(int(orderNo)), 14, reverse=True) portHex = fill_2_hexByte(hex(int(port)), 2, reverse=True) pwHex = fill_2_hexByte(hex(int(pw)), 4, reverse=True) chargeTimeHex = fill_2_hexByte(hex(int(chargeTime)), 4, reverse=True) data = orderNoHex + portHex + chargeTimeHex + operHex + pwHex return self._send_data("B2", data, timeout=15) def _stop(self, port, orderNo, pw, door=False): """ 终止设备运行 :param port: 端口号 :param orderNo: 订单编号 :param pw: 开锁密码 :param door: true表示断电的同时开锁 false 表示仅仅断电 :return: """ operHex = "11" if not door else "12" orderNoHex = fill_2_hexByte(hex(int(orderNo or 0)), 14, reverse=True) portHex = fill_2_hexByte(hex(int(port)), 2, reverse=True) pwHex = fill_2_hexByte(hex(int(pw)), 4, reverse=True) data = orderNoHex+portHex+"0000"+operHex+pwHex return self._send_data("B2", data) # ---------------------------------------------------------------------------------------------------------------------- def _get_door_status(self): try: result = self._send_data("BD", "00", timeout=5) except ServiceException: return dict() data = result.get("data", "")[8: -4] doorStatus = dict() port = 0 while data: port = port + 1 _door = u"打开" if data[: 2] == "01" else u"关闭" doorStatus[str(port)] = _door data = data[2:] return doorStatus def _get_port_status(self): result = self._send_data("B3", "00") data = result.get("data", "")[8: -4] lockPorts = self.device.otherConf.get("lockPorts") or list() portInfo = dict() while data: tempPort = str(int(data[0: 2], 16)) tempStatus = DefaultParams.STATUS_MAP.get(data[2:4]) if tempPort not in lockPorts else Const.DEV_WORK_STATUS_FORBIDDEN portInfo.update({tempPort: tempStatus}) data = data[4: ] return portInfo def _get_port_status_detail(self): result = self._send_data("B4", "00") data = result.get("data", "")[8: -4] lockPorts = self.device.otherConf.get("lockPorts") or list() portInfo = dict() while data: tempPort = str(int(data[0: 2], 16)) tempVoltage = int(reverse_hex(data[2: 6]), 16) tempAmpere = int(reverse_hex(data[6:10]), 16) / 1000.0 tempPower = int(reverse_hex(data[10: 14]), 16) tempElec = int(reverse_hex(data[14: 22]), 16) / 1000.0 tempTime = int(reverse_hex(data[22: 26]), 16) tempOccTime = int(reverse_hex(data[26:30]), 16) tempTemper = int(reverse_hex(data[30: 34]), 16) tempStatus = DefaultParams.STATUS_MAP.get(data[34:36]) if tempPort not in lockPorts else Const.DEV_WORK_STATUS_FORBIDDEN portInfo.update({ tempPort: { "voltage": tempVoltage, "elec": tempElec, "ampere": tempAmpere, "power": tempPower, "chargeTime": tempTime, "temperature": tempTemper, "status": tempStatus, "occTime": tempOccTime } }) data = data[36: ] return portInfo def _reboot_device(self): return self._send_data("B5", "00") def _lock_device(self): operateHex = "01" return self._send_data("B6", operateHex) def _unlock_device(self): operateHex = "00" return self._send_data("B6", operateHex) def _sync_device_time(self): nowTime = datetime.datetime.now() yearHex = fill_2_hexByte(hex(nowTime.year), 4, reverse=True) monthHex = fill_2_hexByte(hex(nowTime.month), 2) dayHex = fill_2_hexByte(hex(nowTime.day), 2) hourHex = fill_2_hexByte(hex(nowTime.hour), 2) minuteHex = fill_2_hexByte(hex(nowTime.minute), 2) secondHex = fill_2_hexByte(hex(nowTime.second), 2) sendData = yearHex + monthHex + dayHex + hourHex + minuteHex + secondHex + "00" self._send_data("A1", sendData=sendData, cmd=DeviceCmdCode.OPERATE_DEV_NO_RESPONSE) def _sync_device_settings(self): domain = os.environ.get("MY_DOMAIN") otherConf = self._device.get("otherConf", dict()) qr_code_url = concat_user_login_entry_url(l = self._device["logicalCode"]) snCode = otherConf.get("snCode", self._device["logicalCode"].replace("G", "")) minPower = otherConf.get("minPower", DefaultParams.DEFAULT_MIN_POWER) maxPower = otherConf.get("maxPower", DefaultParams.DEFAULT_MAX_POWER) floatTime = otherConf.get("floatTime", DefaultParams.DEFAULT_FLOAT_TIME) ICSetupCode = otherConf.get("ICSetupCode", DefaultParams.DEFAULT_IC_CODE) qrCodeLenHex = fill_2_hexByte(hex(len(qr_code_url)), 2) minPowerHex = fill_2_hexByte(hex(int(minPower)), 4, reverse=True) maxPowerHex = fill_2_hexByte(hex(int(maxPower)), 4, reverse=True) floatTimeHex = fill_2_hexByte(hex(int(floatTime)), 4, reverse=True) qrCodeHex = self._to_ascii(qr_code_url) snCodeHex = self._to_ascii(snCode) ICSetupCodeHex = self._to_ascii(ICSetupCode) sendData = snCodeHex + minPowerHex + maxPowerHex + floatTimeHex + ICSetupCodeHex + qrCodeLenHex + qrCodeHex self._send_data("A2", sendData=sendData, cmd=DeviceCmdCode.OPERATE_DEV_NO_RESPONSE) # 添加特性 去掉前台的金额显示按钮 otherConf.update({"payAfterUse": True}) Device.objects.filter(devNo=self.device.devNo).update(otherConf=otherConf) Device.invalid_device_cache(self.device.devNo) def _update_device_conf(self, data): minPower = data.get("minPower", DefaultParams.DEFAULT_MIN_POWER) maxPower = data.get("maxPower", DefaultParams.DEFAULT_MAX_POWER) floatTime = data.get("floatTime", DefaultParams.DEFAULT_FLOAT_TIME) ICSetupCode = data.get("ICSetupCode", DefaultParams.DEFAULT_IC_CODE) actualPortNum = data.get("actualPortNum", DefaultParams.DEFAULT_PORT_NUM) chargeType = data.get("chargeType", DefaultParams.DEFAULT_CHARGE_TYPE) minAfterStartCoins = data.get("minAfterStartCoins", DefaultParams.DEFAULT_MIN_START_COINS) stayTimeUnitPrice = data.get("stayTimeUnitPrice", DefaultParams.DEFAULT_STAY_TIME_UNIT_PRICE) freeStayTime = data.get("freeStayTime", DefaultParams.DEFAULT_FREE_STAY_TIME) minConsume = data.get("minConsume", DefaultParams.DEFAULT_MIN_CONSUME) maxConsume = data.get("maxConsume", DefaultParams.DEFAULT_MAX_CONSUME) powerPackage = data.get("powerPackage", DefaultParams.DEFAULT_POWER_PACKAGE) timePricePackage = data.get("timePricePackage", DefaultParams.DEFAULT_TIME_PACKAGE) timeUnitPrice = data.get("timePrice", DefaultParams.DEFAULT_TIME_UNIT_PRICE) powerUnitPrice = data.get("powerPrice", DefaultParams.DEFAULT_POWER_UNIT_PRICE) elecCharge = data.get("elecCharge", DefaultParams.DEFAULT_ELEC_CHARGE) serviceCharge = data.get("serviceCharge", DefaultParams.DEFAULT_SERVICE_CHARGE) otherConf = self._device.get("otherConf", {}) otherConf.update({ "minAfterStartCoins": int(minAfterStartCoins), "minPower": int(minPower), "maxPower": int(maxPower), "floatTime": int(floatTime), "ICSetupCode": ICSetupCode, "actualPortNum": int(actualPortNum), "chargeType": chargeType, "stayTimeUnitPrice": float(stayTimeUnitPrice), "freeStayTime": float(freeStayTime), "minConsume": float(minConsume), "maxConsume": float(maxConsume), "timeUnitPrice": float(timeUnitPrice), "powerUnitPrice": float(powerUnitPrice), "powerPackage": powerPackage, "timePricePackage": timePricePackage, "elecCharge": float(elecCharge), "serviceCharge": float(serviceCharge), }) devNo = self._device["devNo"] try: Device.objects.filter(devNo=devNo).update(otherConf=otherConf) except Exception as e: logger.error(e) raise ServiceException({'result': 2, 'description': u'设置错误,请重新操作试试'}) Device.invalid_device_cache(devNo) def _dealer_start_device(self, package, attachParas): """ 如果 是经销商启动 就当是取消当前端口的订单 :param package: :param attachParas: :return: """ portStr = attachParas.get("chargeIndex") if not portStr: raise ServiceException({"result": "2", "description": u"请选择插座号!"}) devCache = Device.get_dev_control_cache(self.devNo) portCache = devCache.get(portStr, dict()) orderNo = portCache.get("orderNo", 0xFFFFFFFFFFFFFF) pw = portCache.get("pw", 0) self._stop(portStr, orderNo, pw, door=True) # 清空端口的缓存 Device.clear_port_control_cache(self.devNo, portStr) # 然后将订单直接取消 try: consumeRecord = ConsumeRecord.objects.get(orderNo=orderNo, isNormal=True) except DoesNotExist: return consumeRecord.isNormal = False consumeRecord.status = "finished" consumeRecord.errorDesc = u"经销商关闭订单" consumeRecord.finishedTime = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") consumeRecord.save() sp = ServiceProgress.objects.filter( **{"device_imei": self.devNo, "isFinished": False, "open_id": consumeRecord.openId, "port": int(portStr)} ).first() if not sp: return sp.isFinished = True sp.finished_time = int(time.time()) sp.status = "finished" sp.save() def _stage_billing(self, devNo, port, orderNo, **kwargs): try: order = ConsumeRecord.objects.get(orderNo=orderNo) except DoesNotExist: logger.warning("[{}._stage_billing] not find order, devNo = {}, orderNo = {}".format(self.__class__.__name__, devNo, orderNo)) return lastReport = DevicePortLastReport.get_last(devNo=devNo, port=port, orderNo=orderNo) newReport = DevicePortLastReport.save_last(devNo=devNo, port=port, orderNo=orderNo, **kwargs) cal = Calculater(device=self.device, order=order) chargeConsume, stayConsume = cal.stage_billing(lastReport, newReport) newReport.update_consume(chargeConsume, stayConsume) def _ack(self, ack_id): self._send_data(funCode='AK', sendData=ack_id, cmd=DeviceCmdCode.OPERATE_DEV_NO_RESPONSE) def check_order_state(self, openId): """ 通过 openId 以及设备来鉴别 订单 :param openId: :return: """ dealerId = self.device.ownerId devTypeCode = self.device.devType.get("code") return ClientConsumeModelProxy.get_not_finished_record(ownerId = dealerId, openId = openId, devTypeCode = devTypeCode) def analyze_event_data(self, data): cmdCode = data[6:8] funcName = "_parse_event_{}".format(cmdCode.upper()) func = getattr(ChargeCabinet, funcName, None) if func and callable(func): eventData = func(data) eventData.update({"cmdCode": cmdCode}) return eventData else: logger.warning("<{}> device receive an undefined cmd <{}>, data is <{}>".format(self.devNo, cmdCode, data)) def get_dev_setting(self): otherConf = self._device.get("otherConf", {}) return { "minPower": otherConf.get("minPower", DefaultParams.DEFAULT_MIN_POWER), "maxPower": otherConf.get("maxPower", DefaultParams.DEFAULT_MAX_POWER), "floatTime": otherConf.get("floatTime", DefaultParams.DEFAULT_FLOAT_TIME), "ICSetupCode": otherConf.get("ICSetupCode", DefaultParams.DEFAULT_IC_CODE), "actualPortNum": otherConf.get("actualPortNum", DefaultParams.DEFAULT_PORT_NUM), "minAfterStartCoins": otherConf.get("minAfterStartCoins", DefaultParams.DEFAULT_MIN_START_COINS), "stayTimeUnitPrice": otherConf.get("stayTimeUnitPrice", DefaultParams.DEFAULT_STAY_TIME_UNIT_PRICE), "freeStayTime": otherConf.get("freeStayTime", DefaultParams.DEFAULT_FREE_STAY_TIME), "minConsume": otherConf.get("minConsume", DefaultParams.DEFAULT_MIN_CONSUME), "maxConsume": otherConf.get("maxConsume", DefaultParams.DEFAULT_MAX_CONSUME), "chargeType": otherConf.get("chargeType", DefaultParams.DEFAULT_CHARGE_TYPE), "powerPackage": otherConf.get("powerPackage", DefaultParams.DEFAULT_POWER_PACKAGE), "timePricePackage": otherConf.get("timePricePackage", DefaultParams.DEFAULT_TIME_PACKAGE), "timePricePackageDefaultPrice": otherConf.get("powerUnitPrice", DefaultParams.DEFAULT_POWER_UNIT_PRICE), "powerPackageDefaultPrice": otherConf.get("timeUnitPrice", DefaultParams.DEFAULT_TIME_UNIT_PRICE), # 服务费 + 电量 "elecCharge": otherConf.get("elecCharge", DefaultParams.DEFAULT_ELEC_CHARGE), "serviceCharge": otherConf.get("serviceCharge", DefaultParams.DEFAULT_SERVICE_CHARGE), } def set_device_function_param(self, request, lastSetConf): actualPortNum = request.POST.get("actualPortNum", None) if actualPortNum is not None and int(actualPortNum) > 30: raise ServiceException({"result": 2, "description": u"实际端口数量最大30"}) chargeType = request.POST.get("chargeType") if chargeType not in ("time", "power", "elec"): raise ServiceException({"result": 2, "description": u"暂不支持此消费模式"}) # 校验计费套餐 maxConsume = request.POST.get("maxConsume") minConsume = request.POST.get("minConsume") if float(minConsume) < 0 or float(maxConsume) < 0: raise ServiceException({"result": 2, "description": u"最低消费金额和最高消费金额不能小于0"}) if float(minConsume) > float(maxConsume): raise ServiceException({"result": 2, "description": u"最低消费金额不得大于最高消费金额"}) # 时间计费规则校验 timePricePackage = request.POST.get("timePricePackage") for _index, _package in enumerate(timePricePackage): if int(_package["max"]) <= int(_package["min"]): raise ServiceException({"result": 2, "description": u"第 {} 阶段 时间开始 {} 小于 时间结束 {}".format(_index+1, _package["max"], _package["min"])}) powerPackage = request.POST.get("powerPackage") for _index, _package in enumerate(powerPackage): if int(_package["max"]) <= int(_package["min"]): raise ServiceException({"result": 2, "description": u"第 {} 阶段 功率开始 {} 小于 功率结束 {}".format(_index+1, _package["max"], _package["min"])}) self._update_device_conf(request.POST) def set_device_function(self, request, lastSetConf): lockDevice = request.POST.get("lockDevice", None) rebootDevice = request.POST.get("rebootDevice", None) setToDevice = request.POST.get("setToDevice", None) if rebootDevice is not None: self._reboot_device() return if lockDevice is not None and lockDevice in ("false", "true"): lockDevice = json.loads(lockDevice) self._lock_device() if lockDevice else self._unlock_device() return # 下发所有参数到设备,这个地方做一个保护 ,设备正在工作的时候不让设置参数 if setToDevice is not None: return self._sync_device_settings() def get_port_status(self, force=False): if force: 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'充电端口信息获取失败'}) # 获取显示端口的数量 客户要求可以设置 显示给用户多少个端口 showPortNum = self._device.get("otherConf", {}).get("actualPortNum", DefaultParams.DEFAULT_PORT_NUM) statusDict = dict() showStatusDict = 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} if int(portStr) <= int(showPortNum): showStatusDict[portStr] = {"status": statusDict[portStr]["status"]} 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 showStatusDict def get_port_status_from_dev(self): portInfo = self._get_port_status() portDict = dict() for portStr, status in portInfo.items(): portDict[portStr] = {"status": 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 start_device(self, package, openId, attachParas): if not openId: return self._dealer_start_device(package, attachParas) portStr = attachParas.get("chargeIndex") orderNo = attachParas.get("orderNo") pw = self.password lockPorts = self.device.otherConf.get("lockPorts") or list() if str(portStr) in lockPorts: raise ServiceException({"result": 2, "description": u"当前端口已被禁用"}) devCache = Device.get_dev_control_cache(self.device.devNo) or dict() if devCache.get(portStr, dict()).get("status") == Const.DEV_WORK_STATUS_WORKING: raise ServiceException({"result": 2, "description": u"当前端口已被占用"}) if portStr is None: raise ServiceException({'result': 2, 'description': u'请选择充电端口'}) if orderNo is None: return ServiceException({'result': 2, "description": u"订单创建失败,请重新尝试"}) # 支持发送关门自动断电的 发送新指令 if self.is_support_auto_charge: result = self._new_charge(orderNo=orderNo, port=portStr, pw=pw) # 否则由服务器控制充电 else: result = self._open(orderNo=orderNo, port=portStr, pw=pw) portDict = { "status": Const.DEV_WORK_STATUS_WORKING, "vCardId": self._vcard_id, "isStart": True, "openId": openId, "orderNo": orderNo, "startTime": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "pw": pw, } Device.update_dev_control_cache(self._device["devNo"], {str(portStr): portDict}) otherConf = self.device.get("otherConf") or dict() result["consumeOrderNo"] = orderNo result["servicedInfo"] = {"pw": pw, "chargeType": otherConf.get("chargeType", DefaultParams.DEFAULT_CHARGE_TYPE)} result["finishedTime"] = int(time.time()) + 7 * 24 * 60 * 60 return result def stop(self, port=None): """ 端口停止功能 用户主动停止了该端口 对于充电柜的业务来说 结束充电就意味着 需要打开柜门 同时结算订单 收到停止指令之后 服务器首先下发B2-12指令 然后根据返还的信息进行扣费处理 最后结存订单 :param port: :return: """ portStr = str(port) devCache = Device.get_dev_control_cache(self.device.devNo) portCache = devCache.get(portStr, dict()) # 校验订单状态 非正在运行的订单不能结束 考虑 是否 加锁 if not portCache: return if portCache.get("cardNo"): raise ServiceException({"result": 2, "description": u"刷卡启动的设备请使用刷卡结束"}) orderNo = portCache.get("orderNo", "") consumeOrder = ConsumeRecord.objects.filter(orderNo=orderNo).first() if consumeOrder is None or not consumeOrder.is_running(): return logger.info("device stop order, device = {}, orderNo = {}".format(self.device.devNo, orderNo)) pw = portCache.get("pw") try: result = self._stop(port, orderNo, pw, True) # 然后去读取主板的的最后一次的数据 data = result.get("data") curInfo = ChargeCabinet._parse_result_B2(data) curInfo.update({"orderNo": orderNo}) # 记录最后一次的上报 然后获取订单的金额 self._stage_billing(self.devNo, portStr, **curInfo) except ServiceException: # 保证一定能够结单 当真正门锁未开的时候,直接找经销商上分即可 logger.error("device open door error! devNo = {}, orderNo = {}".format(self.device.devNo, orderNo)) order = DevicePortLastReport.get_last_by_order(orderNo) curInfo = { "chargeTime": order.chargeTime, "elec": 0, "stayTime": order.stayTime, } consumeMoney = Calculater.get_consume_by_order(self.device, orderNo) consumeOrder.update(money=RMB(consumeMoney).mongo_amount, coin=VirtualCoin(consumeMoney).mongo_amount) consumeOrder.reload() consumeOrder.s_to_e() consumeDict = { "chargeIndex": portStr, 'actualNeedTime': curInfo.get("chargeTime"), 'elec': curInfo.get("elec"), 'stayTime': curInfo.get("stayTime"), 'chargeTime': curInfo.get("chargeTime") } if consumeOrder.servicedInfo and isinstance(consumeOrder.servicedInfo, dict): consumeDict.update(consumeOrder.servicedInfo) if consumeOrder.servicedInfo.get('chargeType') == 'elec': elec_charge, service_charge = self.calc_elecFee_and_serviceFee(consumeMoney) consumeDict.update({ 'elecCharge': elec_charge.mongo_amount, 'serviceCharge': service_charge.mongo_amount, }) ServiceProgress.update_progress_and_consume_rcd( self._device["ownerId"], { "open_id": consumeOrder.openId, "device_imei": self.device["devNo"], "port": int(portStr), "isFinished": False }, consumeDict ) # 通知用户充电柜业务结束 如果订单并没有使用余额结算 还需要通知用户及时去付款 try: openId = portCache.get("openId") user = MyUser.objects.filter(openId=openId, groupId=self.device.get("groupId")).first() extra = [ {u"设备编号": u"{}-{}".format(self.device["logicalCode"], port)}, {u"服务地址": u"{}".format(self.device.group["address"])}, {u"使用时长": u"{}分钟".format(curInfo.get("chargeTime"))}, {u"占位时长": u"{}分钟".format(curInfo.get("stayTime"))}, ] if consumeOrder.servicedInfo.get('chargeType') == 'elec': elec_charge, service_charge = self.calc_elecFee_and_serviceFee(consumeMoney) extra.extend([ {u"使用电量": u"{}度".format(consumeDict['elec'])}, {u"电量费用": u"{}".format(elec_charge)}, {u"服务费用": u"{}".format(service_charge)}, ]) else: extra.append({u"本单消费": u"{}".format(consumeMoney)}) self._notify_user_service_over( user.managerialOpenId, extra, consumeOrder.is_finished() ) except Exception as e: logger.exception(e) Device.clear_port_control_cache(self.device.devNo, portStr) return def calc_elecFee_and_serviceFee(self, consumeMoney): elecCharge = float(self.device.otherConf.get("elecCharge", DefaultParams.DEFAULT_ELEC_CHARGE)) serviceCharge = float(self.device.otherConf.get("serviceCharge", DefaultParams.DEFAULT_SERVICE_CHARGE)) elec_charge = RMB(consumeMoney * (elecCharge / (elecCharge + serviceCharge))) service_charge = RMB(consumeMoney) - elec_charge return elec_charge, service_charge @property def isHaveStopEvent(self): return True def dealer_get_port_status(self): showPortNum = self._device.get("otherConf", {}).get("actualPortNum", DefaultParams.DEFAULT_PORT_NUM) showStatusDict = dict() portInfo = self._get_port_status_detail() devCache = Device.get_dev_control_cache(self.device.devNo) for port, item in portInfo.items(): if int(port) > showPortNum: continue # 始终以 设备的状态为准 portCache = devCache.get(port, dict()) portCache.update(item) if portCache.get("status", Const.DEV_WORK_STATUS_IDLE) in (Const.DEV_WORK_STATUS_WORKING, Const.DEV_WORK_STATUS_OCCUPY): portCache["status"] = Const.DEV_WORK_STATUS_WORKING portCache["usedTime"] = portCache.get("chargeTime") # 如果是电量 + 服务费模式: 添加用户昵称, 添加电费金额 添加服务费金额 try: order = ConsumeRecord.objects.get(orderNo=portCache['orderNo']) portCache['nickName'] = order.nickname portCache['startTime'] = portCache['startTime'][-14:] if order.servicedInfo.get('chargeType') == 'elec': # 电量 + 服务费模式 consumeMoney = Calculater.get_consume_by_order(self.device, order.orderNo) portCache['elecFee'], portCache['serviceFee'] = self.calc_elecFee_and_serviceFee(consumeMoney) except: pass showStatusDict[port] = portCache else: showStatusDict[port] = {"status": portCache["status"]} return showStatusDict def get_current_use(self, order): # type: (ConsumeRecord) -> dict group = Group.get_group(order.groupId) item = ServiceProgress.objects.filter( device_imei=order.devNo, open_id=order.openId, attachParas__orderNo=order.orderNo, isFinished=False ).first() if not item: return dict() data = { 'startTime': time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(item.start_time)), 'order': item.consumeOrder, 'address': group.get('address', ''), 'groupName': group.get('groupName', ''), 'devType': self.device['devType'].get('name'), 'devTypeCode': self.device['devType'].get('code'), 'logicalCode': self.device['logicalCode'], 'status': Const.DEV_WORK_STATUS_WORKING, 'devNo': self.devNo, "port": item.port } data.update(DeviceType.get_services_button(self.device['devType']['id'])) devCache = Device.get_dev_control_cache(self.devNo) portCache = devCache.get(str(item.port)) or dict() data.update(portCache) return data def active_deactive_port(self, port, active): if not active: devCache = Device.get_dev_control_cache(self.devNo) portCache = devCache.get(str(port)) orderNo = portCache.get("orderNo") or 0 pw = portCache.get("pw") or 0 self._stop(port, orderNo, pw) @property def support_monthly_package(self): return True def lock_unlock_port(self, port, lock=True): """ 禁用端口 主板不支持 禁用端口 只能服务器实现 """ otherConf = self.device.get("otherConf") lockPorts = otherConf.get("lockPorts", list()) port = str(port) try: if lock: if port not in lockPorts: lockPorts.append(port) else: lockPorts.remove(port) except Exception as e: logger.error(e) otherConf.update({"lockPorts": lockPorts}) try: Device.objects(devNo=self.device.devNo).update(otherConf=otherConf) Device.invalid_device_cache(self.device.devNo) except Exception as e: logger.error("update device %s lockPorts error the reason is %s " % (self.device.devNo, e)) raise ServiceException({'result': 2, 'description': u'操作失败,请重新试试'})