# -*- coding: utf-8 -*- # !/usr/bin/env python import datetime import logging from typing import TYPE_CHECKING from apilib.monetary import RMB from apilib.utils_string import make_title_from_dict from apps.web.constant import DeviceCmdCode, CONSUMETYPE from apps.web.core.device_define.jndz import CMD_CODE from apps.web.core.exceptions import ServiceException from apps.web.core.networking import MessageSender from apps.web.device.models import Device from apps.web.eventer.base import FaultEvent, WorkEvent from apps.web.eventer import EventBuilder from apps.web.eventer.policy_common import PolicyComNetPayAckEvent, PolicyOnlineCardStartAckEvent, \ StartAckEventPreProcessor from apps.web.user.models import CardRechargeOrder, Card if TYPE_CHECKING: pass logger = logging.getLogger(__name__) class builder(EventBuilder): def __getEvent__(self, device_event): # 订单机制事件 if 'order_id' in device_event: if device_event['order_type'] == 'com_start': return MyComNetPayAckEvent(self.deviceAdapter, device_event) if device_event['order_type'] == 'ic_recharge': pass if device_event['order_type'] == 'id_start': return OnlineCardStartAckEvent(self.deviceAdapter, device_event) if device_event['order_type'] == 'card_refund': pass else: if 'event_type' in device_event: if device_event['event_type'] == 'card': return CardEvent(self.deviceAdapter, device_event) event_data = self.deviceAdapter.analyze_event_data(device_event) if event_data is None or 'cmdCode' not in event_data: return None if event_data['cmdCode'] in [ # todo 0A的告警不要了,先注释掉,后续删除 # CMD_CODE.DEVICE_SUBMIT_DEVICE_FAULT_0A, CMD_CODE.DEVICE_FAULT_TEMPERATURE, CMD_CODE.DEVICE_FAULT_POWER, CMD_CODE.DEVICE_FAULT_SMOKE, CMD_CODE.DEVICE_ELEC, CMD_CODE.DEVICE_FAULT_ALTER ]: return JNDZEventerFailure(self.deviceAdapter, event_data) class CardEvent(WorkEvent): def do(self): if self.event_data['funCode'] == '10': self._do_get_balance() if self.event_data['funCode'] == '22': self._do_get_balance_22() def _do_get_balance(self): if self.device.devType.get('features', {}).get('cardNoReverse'): card_id = self.deviceAdapter.reverse_hex(self.event_data['card_id']) else: card_id = self.event_data['card_id'] cardNo = str(int(card_id, 16)) money = self.device.policyTemp.get('forIdcard', {}).get('money', 1) logger.info('[_do_get_balance] receive cardNo = {}'.format(cardNo)) card = self.update_card_dealer_and_type(cardNo) # type: Card data = {'funCode': '10', 'card_id': self.event_data['card_id'], 'balance': 0, } # 十六进制卡号 dealer = self.device.owner # 无主卡或者是卡被冻结 if not card or not card.openId or card.frozen or not dealer: logger.info('[_do_get_balance] receive cardNo = {}, card invalid!'.format(cardNo)) data.update({'result': 1}) return self.send_mqtt(data=data) # 是否存在没有到账的余额 进行充值 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() # ongoingList = getattr(card, 'ongoingList', []) # 有冻结未结束的订单 # # # 先不给做一张卡只能开启一单的限制 if card.balance <= RMB(money): logger.info( '[_do_get_balance] receive cardNo = {}, card balance = {} not enough!'.format(cardNo, card.balance)) data.update({'result': 1}) return self.send_mqtt(data=data) data.update({ 'balance': int(RMB(card.balance) * 10), # 单位: 角 'attach_paras': {'openId': card.openId}, }) data.update(self.generate_rule()) if RMB(card.balance) > RMB(money): data.update({'result': 0}) else: data.update({'result': 1}) self.send_mqtt(data=data) def send_mqtt(self, data=None, cmd=DeviceCmdCode.OPERATE_DEV_SYNC): """ 发送mqtt 指令默认210 返回data """ result = MessageSender.send(self.device, cmd, data) if 'rst' in result and result['rst'] != 0: if result['rst'] == -1: raise ServiceException( {'result': 2, 'description': u'该设备正在玩命找网络,请您稍候再试', 'rst': -1}) elif result['rst'] == 1: raise ServiceException( {'result': 2, 'description': u'该设备忙,无响应,请您稍候再试。也可能是您的设备版本过低,暂时不支持此功能', 'rst': 1}) else: if cmd in [DeviceCmdCode.OPERATE_DEV_NO_RESPONSE, DeviceCmdCode.PASSTHROUGH_OPERATE_DEV_NO_RESPONSE]: return return result.get('data', 'ok') def generate_rule(self): forIdcard = self.device.policyTemp.get('forIdcard', {}) policyType = forIdcard.get('policyType', 'time') billingMethod = forIdcard.get('billingMethod', 'prepaid') account_type = 'real' # account_type(max, real, min) 计费的规则是最大功率计费,还是实时功率计费 if 'card_accumulate_max_power' in self.device.owner.features: account_type = 'max' policy = {'billing_method': billingMethod} if billingMethod == CONSUMETYPE.POSTPAID: # 后付费 if policyType == 'time': policy.update({ 'rule_type': 'TIME_ELEC_MONEY', 'rule': { 'account_type': account_type, 'step': self.deviceAdapter.get_uart_step(is_card=True), }}) elif policyType == 'elec': elecFee = float(forIdcard.get('rule', {}).get('price', 1)) # 电费单价, 用于显示金额 policy.update({ 'rule_type': 'ELEC', 'rule': { 'account_type': account_type, 'elec_fee': int(elecFee * 100) } }) else: raise ServiceException({'result': 2, 'description': u'套餐单位错误,请联系经销商'}) else: # 预付费 money = forIdcard.get('money', 1) policy.update({ 'fee': int(money * 100), # 订单的真实扣费. 预付费需要扣费 这里填写fee }) if policyType == 'time': policy.update({ 'rule_type': 'MONEY', 'rule': { 'account_type': account_type, 'amount': int(money * 100), 'step': self.deviceAdapter.get_uart_step(is_card=True), 'need_time': int(forIdcard.get('maxTime', 720.0) * 60), # 单位: 秒 }, }) elif policyType == 'elec': elecFee = float(forIdcard.get('rule', {}).get('price', 1)) # 电费单价, 用于显示金额 policy.update({ 'rule_type': 'MONEY_BY_ELEC', 'rule': { 'account_type': account_type, 'amount': int(money * 100), # 启动金额 'elec_fee': int(elecFee * 100) # 电费单价 }, }) else: raise ServiceException({'result': 2, 'description': u'套餐单位错误,请联系经销商'}) return policy class JNDZEventerFailure(FaultEvent): def do(self, **args): cmdCode = self.event_data.get("cmdCode") faultType = self.event_data.get(u"fault") desc = self.event_data.get("desc", "") # todo 0A的告警不要了,先注释掉,后续删除 # 保证原有的故障处理逻辑不变 # if cmdCode == CMD_CODE.DEVICE_SUBMIT_DEVICE_FAULT_0A: # return self.handler_0A_fault() # 这些都是整机告警 part = "0" warningData = { "warningStatus": 2, "warningDesc": desc, "warningTime": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), } Device.update_dev_warning_cache(self.device.devNo, {part: warningData}) group = self.device.group titleList = [ {u"告警名称": faultType}, {u"地址名称": group["groupName"]} ] title = make_title_from_dict(titleList) # 接下来的都是整机告警,这个地方需要通知到经销商 self.notify_dealer( "device_fault", title=title, device=u" 号设备".format(self.device.logicalCode), faultType=faultType, notifyTime=datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), fault="" ) # 记录错误故障 fault_record = self.record( faultCode=cmdCode, description=desc, title=faultType, detail={"faultType": faultType, "errorCode": self.event_data.get("errorCode")} ) self.north_to_liangxi(fault_record) # todo 0A的告警不要了,先注释掉,后续删除 # def handler_0A_fault(self): # faultContent = self.event_data.get('faultContent', '') # level = self.event_data.get('level', '') # errCode = self.event_data.get('errCode') # # port = self.event_data.get('port') # if port and port != 255: # title = u'注意!您的设备{}号端口发出告警!'.format(port) # part = str(port) # else: # title = u'注意!您的设备发出告警!' # part = "0" # # if errCode in ['A3']: # 空载 无需显示在经销商后台 # return # # elif errCode in ['00']: # 老设备上报继电器粘连 100206 不上报!! # return # # # 设备告警打入缓存 # elif errCode in ['A0', 'A1', 'A2', 'A3', 'A4', 'A5', 'A6', 'A7', 'A8', 'A9', 'AA']: # warningData = { # "warningStatus": 2, # "warningDesc": faultContent, # "warningTime": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), # } # Device.update_dev_warning_cache(self.device.devNo, {part: warningData}) # # # 设备告警清除 # elif errCode in ['20', '21', '22', '23', '24', '25', '26', '27', '28', '29', '2A']: # 恢复信号 不操作 # Device.clear_part_warning_cache(self.device.devNo, part) # # ctrInfo = Device.get_dev_control_cache(self.device.devNo) # # if 'statusInfo' in ctrInfo and 'errCode' in ctrInfo: # # if ctrInfo['errCode'][-1] == errCode[-1]: # # ctrInfo['status'] = None # # ctrInfo['errCode'] = None # # ctrInfo['faultContent'] = None # # ctrInfo['level'] = None # # ctrInfo['statusInfo'] = None # # ctrInfo['cmdCode'] = None # # Device.update_dev_control_cache(self.device['devNo'], ctrInfo) # # else: # pass # # Device.update_dev_control_cache(self.device['devNo'], self.event_data) # # # 记录错误故障 # self.record( # title=title, # description=faultContent, # level=level # ) # # group = Group.get_group(self.device.groupId) # # if self.is_notify_dealer: # self.notify_dealer( # templateName="device_fault", # title=title, # device=u'{}-{}'.format(self.device.devTypeName, self.device.logicalCode), # fault=faultContent, # location=u'{}-{}-{}号设备({})'.format(group["address"], group["groupName"], self.device["groupNumber"], # self.device["logicalCode"]), # notifyTime=datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") # ) class AckEventPreProcessor(StartAckEventPreProcessor): def analysis_reason(self, reason, fault_code=None): FINISHED_CHARGE_REASON_MAP = { 0x00: u'购买的充电时间或者电量已经用完', 0x01: u'系统判断为异常断电(插头被拔或者松动,或者电瓶已经充满),电瓶车充电器种类繁多,可能存在误差', 0x02: u'电池已经充满', 0x03: u'警告!您的电池功率超过本机最大限制。为了公共安全,不建议您在该充电桩充电', 0x04: u'远程断电', 0x0B: u'设备或是端口出现问题,被迫停止', # 服务器定义的停止事件 0xC0: u'计费方式无效', 0xC1: u'订购套餐已用完', 0xC2: u'订购时间已用完', 0xC3: u'订购电量已用完', 0xC4: u'动态计算功率后, 时间已用完', 0xC5: u'订单异常,设备可能离线超过1小时, 平台结单', 0xC6: u'系统检测到充电已结束, 平台结单(0x21)', 0xC7: u'系统检测到充电已结束, 平台结单(0x0F)', 0xC8: u'用户远程停止订单', 0xC9: u'经销商远程停止订单', 0xCA: u'系统检测到订单已结束, 平台结单(0xCA)', 0xCB: u'充电时长已达到最大限制(0xCB)', 0xCC: u'充电电量已达到最大限制(0xCC)', 0xCD: u'设备升级中... 关闭当前订单', # 设备自定义的停止时间 0xD0: u'设备继电器粘连, 结束充电', 0xD1: u'空载(充电器未连接或充电器插头脱落)', } return FINISHED_CHARGE_REASON_MAP.get(reason, '充电结束') class MyComNetPayAckEvent(PolicyComNetPayAckEvent): def __init__(self, smartBox, event_data): super(PolicyComNetPayAckEvent, self).__init__(smartBox, event_data, AckEventPreProcessor()) class OnlineCardStartAckEvent(PolicyOnlineCardStartAckEvent): def __init__(self, smartBox, event_data): super(PolicyOnlineCardStartAckEvent, self).__init__(smartBox, event_data, AckEventPreProcessor())