# -*- coding: utf-8 -*- # !/usr/bin/env python import datetime import sys import time import logging from typing import TYPE_CHECKING from apilib.monetary import RMB, VirtualCoin from apilib.utils_string import make_title_from_dict from apps.web.constant import DeviceCmdCode, Const from apps.web.core.exceptions import ServiceException from apps.web.eventer import EventBuilder from apps.web.device.models import Device, Group from apps.web.eventer.base import FaultEvent, WorkEvent from apps.web.user.models import ServiceProgress, CardRechargeOrder, ConsumeRecord, CardConsumeRecord, MyUser, \ MonthlyPackage from apps.common.utils import int_to_hex from apps.web.core.device_define.ywt_chongdiangui_new import DefaultParams, Calculater logger = logging.getLogger(__name__) if TYPE_CHECKING: from apps.web.core.adapter.ywt_chongdiangui_new import ChargeCabinet from apps.web.user.models import Card class builder(EventBuilder): def __getEvent__(self, device_event): event_data = self.deviceAdapter.analyze_event_data(device_event['data']) if event_data is None or 'cmdCode' not in event_data: return None # 粤万通的版本比较复杂 没有一个统一的版本 这次力争升级到一个统一的版本 # TODO 补丁 if "ack_id" in device_event and self.device.driverVersion not in [ "v1.0.0", "v1.0.1", "v1.0.2", "v3.0.1", "v3.0.2", "v3.0.3", "v4.0.0", "v4.0.1" ]: self.deviceAdapter._ack(device_event["ack_id"]) cmdCode = event_data.get("cmdCode", "") className = "ChargeCabinetEvent{}".format(cmdCode.capitalize()) myClass = getattr(sys.modules[__name__], className, None) if myClass: logger.info("receive message for dev <{}>\n {}".format(self.deviceAdapter.device.devNo, event_data)) return myClass(self.deviceAdapter, event_data) # 工具类 class ChargeCabinetEventBase(object): def stage_billing(self, devNo, port, orderNo, **kwargs): """每次能够计费的时候 调用该函数计费""" self.device_adapter._stage_billing(devNo, port, orderNo, **kwargs) def is_start_by_card(self, port): devCache = Device.get_dev_control_cache(self.device_adapter.device.devNo) return "cardNo" in devCache.get(port, dict()) @property def device_adapter(self): # type:()->ChargeCabinet return getattr(self, 'deviceAdapter') @staticmethod def reverse_hex(s): if len(s) == 0: return "" return s[-2:] + ChargeCabinetEventBase.reverse_hex(s[:-2]) def response_card_start(self, _type, portStrHex, cardNoHex, balance=0, orderNo="0", chargeTime=0, pw=0, cardType=0, leftDays=0): balance = ChargeCabinetEventBase.reverse_hex("{:0>8X}".format(int(balance * 100) & 0xFFFFFFFF)) cardType = ChargeCabinetEventBase.reverse_hex("{:0>2X}".format(int(cardType))) leftDays = ChargeCabinetEventBase.reverse_hex("{:0>4X}".format(int(leftDays))) orderNo = ChargeCabinetEventBase.reverse_hex("{:0>14X}".format(int(orderNo))) chargeTime = ChargeCabinetEventBase.reverse_hex("{:0>4X}".format(int(chargeTime))) pw = ChargeCabinetEventBase.reverse_hex("{:0>4X}".format(int(pw))) sendData = cardNoHex + portStrHex + _type + balance + cardType + leftDays + orderNo + chargeTime + pw self.device_adapter._send_data(funCode="B0", sendData=sendData, cmd=DeviceCmdCode.OPERATE_DEV_NO_RESPONSE) def response_finish(self, orderNoHex, portStr): portStrHex = int_to_hex(portStr, lens=2) sendData = orderNoHex + portStrHex + "01" self.device_adapter._send_data("C1", sendData, cmd=DeviceCmdCode.OPERATE_DEV_NO_RESPONSE) def response_fault(self, port): portStrHex = "{:02X}".format(port) sendData = portStrHex + "01" self.device_adapter._send_data("E0", sendData, cmd=DeviceCmdCode.OPERATE_DEV_NO_RESPONSE) def response_card_finish(self, cardNoHex, portStrHex, balance, consumeMoney, cardType=1, leftDays=0, cardStatus="01"): balanceHex = ChargeCabinetEventBase.reverse_hex("{:0>8X}".format(int(balance*100))) consumeMoneyHex = ChargeCabinetEventBase.reverse_hex("{:0>8X}".format(int(consumeMoney*100))) cardTypeHex = ChargeCabinetEventBase.reverse_hex("{:0>2X}".format(int(cardType))) leftDaysHex = ChargeCabinetEventBase.reverse_hex("{:0>4X}".format(int(leftDays))) sendData = cardNoHex + portStrHex + cardStatus + balanceHex + cardTypeHex + leftDaysHex + consumeMoneyHex self.device_adapter._send_data("B1", sendData, cmd=DeviceCmdCode.OPERATE_DEV_NO_RESPONSE) def deduct_for_card(self, portStr, card, money, consumeOrder, cardConsumeOrder): devCache = Device.get_dev_control_cache(self.device_adapter.device.devNo) portCache = devCache.get(portStr) monthlyPackageId = portCache.get("monthlyPackageId") if monthlyPackageId: monthlyPackage = MonthlyPackage.objects.get(id=monthlyPackageId) monthlyPackage.deduct(consumeOrder) else: cardBalance = card.balance - RMB(money) card.balance = cardBalance card.save() # 更新 consumeOrder.update(money=RMB(money).mongo_amount, coin=VirtualCoin(money).mongo_amount, status="finished") cardConsumeOrder.update(money=RMB(money).mongo_amount, coin=VirtualCoin(money).mongo_amount) # 故障 class ChargeCabinetEventE0(FaultEvent, ChargeCabinetEventBase): def do(self, **args): group = Group.get_group(self.device["groupId"]) if self.is_notify_dealer(): self.notify_dealer( "device_fault", title=u"注意注意您的设备发生故障", device=u"组号:{},二维码编号:{}".format(self.device["groupNumber"], self.device["logicalCode"]), location=u"组名称:{},地址:{}".format(group["groupName"], group["address"]), fault=self.event_data["statusInfo"], notifyTime=str(datetime.datetime.now())[:19] ) warningData = { "warningStatus": 2, "warningDesc": self.event_data["statusInfo"], "warningTime": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") } # 整机告警 part = self.event_data.get("portStr", "0") Device.update_dev_warning_cache(self.device.devNo, {part: warningData}) try: faultRecord = self.record( faultCode=self.event_data.get("faultCode", ""), detail={"errorCode": self.event_data["faultCode"]} ) self.north_to_liangxi(faultRecord) except Exception as e: logger.error(e) finally: sendData = self.event_data.get("portHex") + "01" self.deviceAdapter._send_data("E0", sendData=sendData, cmd=DeviceCmdCode.OPERATE_DEV_NO_RESPONSE) # 时间同步 class ChargeCabinetEventA1(WorkEvent, ChargeCabinetEventBase): def do(self, **args): logger.info("charge cabinet <{}> request to synchronization time".format(self.device.devNo)) self.deviceAdapter._sync_device_time() # 参数设置同步 class ChargeCabinetEventA2(WorkEvent, ChargeCabinetEventBase): def do(self, **args): logger.info("charge cabinet <{}> request to device params".format(self.device.devNo)) self.deviceAdapter._sync_device_settings() # 刷卡上报 class ChargeCabinetEventB0(WorkEvent, ChargeCabinetEventBase): def do(self, **args): """ 两个问题 刷卡的时候主板会判断当前端口是否已经被占用 :param args: :return: """ logger.info("charge cabinet <{}> send a card <{}> start signal ".format(self.device.devNo, self.event_data.get("cardNo", "error cardNo"))) cardNo = self.event_data.get("cardNo") portStr = self.event_data.get("portStr") portStrHex = self.event_data['portHex'] cardNoHex = self.event_data['cardNoHex'] lockPorts = self.device.otherConf.get("lockPorts") or list() if portStr in lockPorts: self.response_card_start(_type="03", portStrHex=portStrHex, cardNoHex=cardNoHex) return card = self.update_card_dealer_and_type(cardNo) # type: Card # 粤万通的要求 不同地址的卡不可以通用 if not card or card.frozen or not card.openId and card.groupId != self.device.groupId: self.response_card_start(_type="03", portStrHex=portStrHex, cardNoHex=cardNoHex) logger.info("bad card, cardNo is %s, cannot start port!" % cardNo) return # 充值未到账的卡的处理 有些卡充值在没有上报类型之前 充值结果不会到账的 这个地方需要处理一下 card_recharge_order = CardRechargeOrder.get_last_to_do_one(str(card.id)) self.recharge_id_card(card=card, rechargeType='append', order=card_recharge_order) card.reload() # 判断是否可以使用包月卡启动设备, 有包月卡并且可用的 不在进行余额的判断, 不在进行设备类型的判断 monthlyPackage = card.monthlyPackage if monthlyPackage: cardType = 2 leftDays = (monthlyPackage.expireDateTime - datetime.datetime.now()).days else: # 能够刷卡的最低的金额 minCardMoney = self.device.get("otherConf", dict()).get("minAfterStartCoins", DefaultParams.DEFAULT_MIN_START_COINS) if float(card.balance) < float(minCardMoney): self.response_card_start(_type="02", portStrHex=portStrHex, cardNoHex=cardNoHex, balance=card.balance) logger.info("negative card, card No is %s, card balance is %s" % (cardNo, card.balance)) return cardType = 1 leftDays = 0 # 制造启动前的各种参数 密码 时间等等 chargeTime = 0xFFFF pw = self.deviceAdapter.password try: otherConf = self.device.get("otherConf") servicedInfo = {"pw": pw, "chargeType": otherConf.get("chargeType", DefaultParams.DEFAULT_CHARGE_TYPE)} orderNo, cardOrderNo = self.record_consume_for_card(card, RMB(0), desc="刷卡消费", attachParas={"pw": pw}, servicedInfo=servicedInfo) except Exception as e: logger.error("record to database error!".format(e)) return # 返回刷卡 OK 的信息 self.response_card_start( "01", portStrHex, cardNoHex=cardNoHex, balance=card.balance, orderNo=orderNo, chargeTime=chargeTime, pw=pw, cardType=cardType, leftDays=leftDays ) logger.info("card start ok! event has been report!") # 支持发送关门自动断电的 发送新指令 if self.deviceAdapter.is_support_auto_charge: self.deviceAdapter._new_charge(orderNo=orderNo, port=portStr, pw=pw) # 否则由服务器控制充电 else: self.deviceAdapter._open(orderNo=orderNo, port=portStr, pw=pw) # 启动设备之后,注册服务,并更新端口缓存值 ServiceProgress.register_card_service( self.device, int(portStr), card, { "orderNo": orderNo, "cardOrderNo": cardOrderNo }, finishedTime=int(time.time()) + 7 * 24 * 60 * 60 ) portDict = { "status": Const.DEV_WORK_STATUS_WORKING, "isStart": True, "orderNo": orderNo, "cardOrderNo": cardOrderNo, "cardNo": cardNo, "openId": card.openId, "startTime": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "pw": pw, "monthlyPackageId": None if cardType == 1 else str(monthlyPackage.id) } self.notify_user( managerialOpenId=card.managerialOpenId, templateName="consume_notify", title=u"您正在刷卡启动充电柜业务", money=u"使用结束后计算费用", serviceType=u"刷卡消费, 当前启动为: {}柜门".format(portStr), finishTime=datetime.datetime.now().strftime(Const.DATETIME_FMT) ) Device.update_dev_control_cache(self.device["devNo"], {portStr: portDict}) # 刷卡结束上报 class ChargeCabinetEventB1(WorkEvent, ChargeCabinetEventBase): def do(self, **args): """ 刷卡结束的时候, 仅仅会接受到刷卡上报的信息 和主板商沟通过不会再有充电结束的上报 :return: """ portStrHex = self.event_data['portHex'] cardNoHex = self.event_data['cardNoHex'] cardNo = self.event_data.get("cardNo") portStr = self.event_data.get("portStr") orderNo = self.event_data.get("orderNo") devCache = Device.get_dev_control_cache(self.device.devNo) portCache = devCache.get(portStr) or dict() if not portCache: logger.error("no port cache, error card finished!, card is <{}>".format(cardNo)) self.response_card_finish(cardNoHex, portStrHex, cardStatus="03") return if cardNo != portCache.get("cardNo"): logger.error("card no is not equal , card is <{}>".format(cardNo)) self.response_card_finish(cardNoHex, portStrHex, cardStatus="03") return pw = portCache.get("pw", "") cardOrderNo = portCache.get("cardOrderNo", "") try: card = self.update_card_dealer_and_type(cardNo) consumeOrder = ConsumeRecord.objects.get(orderNo=orderNo) cardConsumeOrder = CardConsumeRecord.objects.get(orderNo=cardOrderNo) except Exception as e: logger.exception(e) return # 请求结束充电 发送 result = self.deviceAdapter._stop(portStr, orderNo, pw, door=True) data = result.get("data") curInfo = self.deviceAdapter._parse_result_B2(data) curInfo.update({"orderNo": orderNo}) # 记录最后一次的上报 然后获取订单的金额 self.deviceAdapter._stage_billing(self.device.devNo, portStr, **curInfo) consumeMoney = Calculater.get_consume_by_order(self.device, orderNo) # 刷卡计费 self.deduct_for_card(portStr, card, consumeMoney, consumeOrder, cardConsumeOrder) # 结束服务 consumeDict = { "chargeIndex": portStr, 'actualNeedTime': curInfo.get("chargeTime"), 'elec': curInfo.get("elec"), 'stayTime': curInfo.get("stayTime"), 'chargeTime': curInfo.get("chargeTime"), } ServiceProgress.update_progress_and_consume_rcd( self.device["ownerId"], { "cardId": str(card.id), "open_id": consumeOrder.openId, "device_imei": self.device["devNo"], "port": int(portStr), "isFinished": False }, consumeDict ) monthlyPackageId = portCache.get("monthlyPackageId") if monthlyPackageId: monthlyPackage = MonthlyPackage.objects.get(id=monthlyPackageId) self.response_card_finish( cardNoHex=cardNoHex, portStrHex=portStrHex, balance=RMB(0), consumeMoney=RMB(consumeMoney), cardType=2, leftDays=max((monthlyPackage.expireDateTime - datetime.datetime.now()).days, 0) ) extra = [({"消费金额": "{}元".format(consumeMoney), "余额": "{}元".format(0)})] else: self.response_card_finish( cardNoHex=cardNoHex, portStrHex=portStrHex, balance=RMB(card.balance), consumeMoney=RMB(consumeMoney) ) extra=[({"消费金额": "{}元".format(consumeMoney), "余额": "{}元".format(card.balance)})] self.notify_user_service_complete( service_name='充电', openid=card.managerialOpenId, port=portStr, address=self.device.group['address'], reason=self.event_data.get('reasonDesc'), finished_time=datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), extra=extra) Device.clear_port_control_cache(self.device.devNo, portStr) # 充电结束上报 一般是充满电了 class ChargeCabinetEventC1(WorkEvent, ChargeCabinetEventBase): def do(self, **args): orderNo = self.event_data.get("orderNo") portStr = self.event_data.get("portStr") devCache = Device.get_dev_control_cache(self.device["devNo"]) portCache = devCache.get(portStr, dict()) openId = portCache.get("openId") if not orderNo or not openId: logger.info("device {} port {} report event, useless info!\n{}".format(self.device["devNo"], portStr, self.event_data)) return portCache.update({ "status": Const.DEV_WORK_STATUS_OCCUPY, # 状态更新进入占位状态 "voltage": self.event_data.get("voltage"), "power": self.event_data.get("power"), "elec": self.event_data.get("elec"), "chargeTime": self.event_data.get("chargeTime"), "stayTime": self.event_data.get("stayTime"), "temperature": self.event_data.get("temperature") }) Device.update_dev_control_cache(self.device.devNo, {portStr: portCache}) # 计费记账 self.stage_billing(self.device.devNo, portStr, **self.event_data) # 密码开锁的,说明单已经结束 直接调用消费订单的状态机结束订单就好 if self.event_data.get("reasonCode") == "06": totalConsume = Calculater.get_consume_by_order(self.device, orderNo) order = ConsumeRecord.objects.get(orderNo=orderNo) consumeDict = { "chargeIndex": portStr, 'actualNeedTime': self.event_data.get("chargeTime"), 'elec': self.event_data.get("elec"), 'stayTime': self.event_data.get("stayTime"), 'chargeTime': self.event_data.get("chargeTime"), } if order.servicedInfo.get('chargeType') == 'elec': elec_charge, service_charge = self.deviceAdapter.calc_elecFee_and_serviceFee(totalConsume) consumeDict.update({ 'elecCharge': elec_charge.mongo_amount, 'serviceCharge': service_charge.mongo_amount, }) if self.is_start_by_card(portStr): cardNo = portCache.get("cardNo") card = self.update_card_dealer_and_type(cardNo) # 扣钱 然后记账 orderNo = portCache.get("orderNo", "") cardOrderNo = portCache.get("cardOrderNo", "") cardConsumeOrder = CardConsumeRecord.objects.get(orderNo=cardOrderNo) consumeOrder = ConsumeRecord.objects.get(orderNo=orderNo) self.deduct_for_card(portStr, card, totalConsume, consumeOrder, cardConsumeOrder) else: try: order.update(money=RMB(totalConsume).mongo_amount, coin=VirtualCoin(totalConsume).mongo_amount) order.reload() order.s_to_e() except Exception as e: logger.error("finished order <{}> error {}\n {}".format(orderNo, e, self.event_data)) ServiceProgress.update_progress_and_consume_rcd( self.device["ownerId"], { "open_id": openId, "device_imei": self.device.devNo, "port": int(portStr), "isFinished": False }, consumeDict ) Device.clear_port_control_cache(self.device.devNo, portStr) # 开始通知用户充电结束 进入占位状态 else: user = MyUser.objects.filter(openId=portCache.get("openId"), groupId=self.device["groupId"]).first() extra = [ {u'结束原因': u'{}'.format(self.event_data["reason"])}, {u'设备编号': u'{}-{}'.format(self.device["logicalCode"], portStr)}, {u"服务地址": u"{}".format(self.device.group["address"])}, {u"充电时间": u"{}分钟".format(self.event_data["chargeTime"])}, ] title = make_title_from_dict(extra) self.notify_user( managerialOpenId=user.managerialOpenId if user else "", templateName="service_complete", title=title, service=u"充电桩充电服务", finishTime=datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), remark=u'充电已经结束,请及时取走您的电池!' ) # 状态上报 class ChargeCabinetEventC0(WorkEvent, ChargeCabinetEventBase): def do(self, **args): orderNo = self.event_data.get("orderNo") portStr = self.event_data.get("portStr") devCache = Device.get_dev_control_cache(self.device["devNo"]) portCache = devCache.get(portStr, dict()) openId = portCache.get("openId") if not orderNo or not openId: logger.error("device {} port {} report event, useless info!\n{}".format(self.device["devNo"], portStr, self.event_data)) return portCache.update({ "voltage": self.event_data.get("voltage"), "power": self.event_data.get("power"), "elec": self.event_data.get("elec"), "usedTime": self.event_data.get("chargeTime"), "occTime": self.event_data.get("stayTime"), "temperature": self.event_data.get("temperature") }) Device.update_dev_control_cache(self.device.devNo, {portStr: portCache}) # 取出last之后 然后再将现在的保存下来 self.stage_billing(self.device.devNo, portStr, **self.event_data) # 门锁状态 上报 class ChargeCabinetEventC3(WorkEvent, ChargeCabinetEventBase): def do(self, **args): portStr = self.event_data.get("portStr") doorStatus = self.event_data.get("doorStatus") if doorStatus == DefaultParams.CLOSE_LOCK: self._do_door_close(portStr) else: self._do_door_open(portStr) def _do_door_close(self, portStr): devCache = Device.get_dev_control_cache(self.device.devNo) portCache = devCache.get(portStr, dict()) # 已经发过一次充电指令的 if "hasStartCharge" in portCache: return pw = portCache.get("pw") orderNo = portCache.get("orderNo") openId = portCache.get("openId") if not all([pw, orderNo, openId]): logger.info(u"useless lock close\n<{}>\n<{}>".format(self.deviceAdapter.device.devNo, self.event_data)) return group = Group.get_group(self.device["groupId"]) user = MyUser.objects.filter(openId=openId, groupId=self.device.groupId).first() if not self.deviceAdapter.is_support_auto_charge: try: self.deviceAdapter._charge(portStr, orderNo, pw) except ServiceException: isCharge = False else: isCharge = True else: isCharge = True if not isCharge: self.notify_user( managerialOpenId=user.managerialOpenId if user else "", templateName="service_complete", title=u"\\n\\n设备编号:\\t\\t{logicalCode}-{port}\\n\\n服务地址:\\t\\t{group}".format( logicalCode=self.device["logicalCode"], port=self.event_data["portStr"], group=group["address"], ), service=u"充电柜服务", finishTime=datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), remark=u'充电开启失败,请扫码重新启动充电或取走电池!' ) else: extra = [ {u"设备编号": u"{}-{}".format(self.device["logicalCode"], portStr)}, {u"服务地址": u"{}".format(self.device.group["address"])}, {u"开门密码": u"{}".format(pw)}, ] title = make_title_from_dict(extra) self.notify_user( managerialOpenId=user.managerialOpenId if user else "", templateName="service_complete", title=title, service=u"充电柜服务", finishTime=datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), remark=u'已成功为您开启充电,感谢您的使用!' ) def _do_door_open(self, portStr): """测试的时候发现 好像只有非指令开锁 才会存在门锁状态打开的上报""" # TODO 尚需证实 # group = Group.get_group(self.device["groupId"]) # # self.notify_dealer( # templateName="device_fault", # title="注意!设备异常", # device=u"{groupNumber}组-{logicalCode}".format( # groupNumber=self.device["groupNumber"], # logicalCode=self.device["logicalCode"] # ), # location=u"{address}-{groupName}".format(address=group["address"], groupName=group["groupName"]), # fault=u"门锁异常打开", # notifyTime=datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") # ) class ChargeCabinetEventCa(WorkEvent, ChargeCabinetEventBase): def do(self, **args): logger.info("device <{}> receive CA, event data = {}".format(self.device.devNo, self.event_data)) subChargeList = self.event_data["subChargeList"] for _sub in subChargeList: ChargeCabinetEventC0(self.deviceAdapter, _sub).do()