# -*- coding: utf-8 -*- # !/usr/bin/env python import datetime import json import logging import re import threading import time from decimal import Decimal from mongoengine import Q from typing import TYPE_CHECKING, Optional from apilib.monetary import RMB from apilib.utils_datetime import to_datetime from apps.web.constant import Const, DeviceCmdCode, MQTT_TIMEOUT from apps.web.core.adapter.base import SmartBox from apps.web.core.exceptions import ServiceException from apps.web.core.networking import MessageSender from apps.web.device.models import Device, Group from apps.web.user.models import ConsumeRecord, MyUser, Card logger = logging.getLogger(__name__) if TYPE_CHECKING: pass # from apps.web.device.models import Device, Group # from apps.web.user.models import ConsumeRecord, MyUser, Card class CmdHelper(object): def __init__(self): pass @staticmethod def encode_str(data, length=2, ratio=1.0, base=16): # type:(str,int,float,int) -> str if not isinstance(data, Decimal): data = Decimal(data) if not isinstance(length, str): length = str(length) if not isinstance(ratio, Decimal): ratio = Decimal(ratio) end = 'X' if base == 16 else 'd' encodeStr = '%.' + length + end encodeStr = encodeStr % (data * ratio) return encodeStr @staticmethod def decode_str(data, ratio=1, base=16, to_num=True): # type:(str,Optional[float, int],int, bool) -> Optional[float, str] """ ratio:比率单位转换 """ if not isinstance(data, str): data = str(data) if to_num: return int(data, base) * ratio else: return '%.10g' % (int(data, base) * ratio) @staticmethod def reverse_hex(data): # type:(str) -> str if not isinstance(data, str): raise TypeError return ''.join(list(reversed(re.findall(r'.{2}', data)))) @staticmethod def split_data_to_list(split_str, data): # type:(str,str) ->Optional[list, None] """ return: list """ part = '({})' all_ = sum(map(lambda _: int(_) * 2, split_str)) pattern = reduce(lambda a, b: a + b, map(lambda _: part.format('..' * int(_)), split_str)) result = re.match(pattern, data[:all_]) if result: return result.groups() @staticmethod def check_params_range(params, minData=None, maxData=None, desc=''): # type:(str,float,float,str) -> str """ 检查参数,返回字符串参数 """ if params is None: raise ServiceException({'result': 2, 'description': u'参数错误.'}) if not isinstance(params, Decimal): params = Decimal(params) if not minData and maxData: if not isinstance(maxData, Decimal): maxData = Decimal(maxData) if params <= maxData: return '%g' % params else: raise ServiceException({'result': 2, 'description': u'%s超出可选范围,可选最大值为%g' % (desc, maxData)}) if not maxData and minData: if not isinstance(minData, Decimal): minData = Decimal(minData) if minData <= params: return '%g' % params else: raise ServiceException({'result': 2, 'description': u'%s超出可选范围,可选最小值为%g' % (desc, minData)}) if not minData and not maxData: return '%g' % params else: if not isinstance(minData, Decimal): minData = Decimal(minData) if not isinstance(maxData, Decimal): maxData = Decimal(maxData) if minData <= params <= maxData: return '%g' % params else: raise ServiceException( {'result': 2, 'description': u'%s参数超出可选范围,可取范围为%g-%g' % (desc, minData, maxData)}) class JinQue(SmartBox, CmdHelper): def __init__(self, device): super(JinQue, self).__init__(device) self.ctrInfo = Device.get_dev_control_cache(device.devNo) for key in self.ctrInfo.keys(): if key.isdigit(): if int(key) > self.port_num: self.ctrInfo.pop(key, None) allPorts, usedPorts, usePorts = self.get_port_static_info(self.ctrInfo) self.ctrInfo.update({'allPorts': allPorts, 'usedPorts': usedPorts, 'usePorts': usePorts}) self.consumeMode = self.device['otherConf'].get('consumeMode') @property def port_num(self): return self.device.devTypeFeatures.get('portNum', 1) def check_dev_status(self, attachParas=None): port = int(attachParas['chargeIndex']) result = self.get_port_status(True) _info = result.get(str(port), {}) if _info.get('status') != Const.DEV_WORK_STATUS_IDLE: raise ServiceException({'result': 2, 'description': u'检测到该充电枪已被他人使用, 若现场充电枪未被使用, 请松开急停按钮后重新扫码启动'}) def get_dev_info(self): """ 获取版本号码 """ payload = self.send_mqtt(data={'funCode': 'get', 'sCmd': 2502}) data = payload['data'][-8:] data = self.reverse_hex(data) version = 'V' + '{}.{}'.format(data[0:2], data[2:4]) return {'version': version} def test(self, coins): raise NotImplementedError(u'设备未实现 `test`') def stop(self, port=None): data = { 'funCode': 'stop', 'port': port } return self.send_mqtt(data) def calc_stop_back_coins(self, totalFee, remainderTime, totalTime): refundFee = round(totalFee * (float(remainderTime) / totalTime), 2) if refundFee > totalFee: refundFee = totalFee if refundFee <= 0.0: refundFee = 0.00 return refundFee def stop_charging_port(self, port): data = { 'funCode': 'stop', 'port': port, 'rule': 'user' } return self.send_mqtt(data) # 访问设备,获取设备信息 def getDevInfo(self): pass def analyze_event_data(self, data): return {} def active_deactive_port(self, port, active): if active == False: self.send_mqtt({ 'funCode': 'stop', 'port': port, 'rule': 'admin' }) def lock_unlock_port(self, port, lock=True): raise ServiceException({'result': 2, 'description': u'此设备不支持直接禁用、解禁端口'}) def get_port_status(self, force=False): if force: self.get_port_status_from_dev() result = {} for k, v in self.ctrInfo.items(): if k.isdigit(): result[k] = v return result def dealer_get_port_status(self): """ 远程上分的时候获取端口状态 :return: """ return self.get_port_status() def get_dev_setting(self): result = {} try: ver = self.send_mqtt({'funCode': 'ver'}, timeout=5).get('ver') result.update({'ver': ver}) except: pass try: elec_fee = round(self.send_mqtt({'funCode': 'elec_fee'}, timeout=5).get('elec_fee', 0) * 0.01, 2) result.update({'elecFee': elec_fee}) except: pass try: total_elec = round(self.send_mqtt({'funCode': 'total_elec'}, timeout=5).get('total_elec', 0) * 0.01, 2) result.update({'totalElec': total_elec}) except: pass onceIdcardFee = self.device.otherConf.get('onceIdcardFee', 5) result.update({'onceIdcardFee': onceIdcardFee}) return result def set_device_function(self, request, lastSetConf): if 'init' in request.POST: self.send_mqtt({'funCode': 'init'}) elif 'reset' in request.POST: self.send_mqtt({'funCode': 'reset'}) else: raise ServiceException({'result': 2, 'description': '设备设置失败'}) def set_device_function_param(self, request, lastSetConf): if 'elecFee' in request.POST: self.send_mqtt( {'funCode': 'set', 'sCmd': 1505, 'value': int(float(request.POST.get('elecFee') or 0) * 100)}, timeout=5) if 'onceIdcardFee' in request.POST: onceIdcardFee = round(float(request.POST.get('onceIdcardFee') or 0), 2) Device.get_collection().update_one({'devNo': self._device['devNo']}, { '$set': {'otherConf.onceIdcardFee': onceIdcardFee}}) Device.invalid_device_cache(self.device.devNo) def set_dev_disable(self, disable): """ 设备端锁定解锁设备 :param disable: :return: """ Device.get_collection().update_one({'devNo': self._device['devNo']}, { '$set': {'otherConf.disableDevice': disable}}) Device.invalid_device_cache(self.device.devNo) def get_port_static_info(self, portDict): allPorts, usedPorts = 0, 0 for k, v in portDict.items(): if k.isdigit(): allPorts += 1 if ('isStart' in v and v['isStart']) or ('status' in v and v['status'] != Const.DEV_WORK_STATUS_IDLE): usedPorts += 1 return allPorts, usedPorts, allPorts - usedPorts def get_port_status_from_dev(self): STATUS_MAP = { 'idle': 0, 'busy': 1 } result = self.send_mqtt({ 'funCode': 'status' }) status = result.get('status', {}) for strPort, status in status.items(): if int(strPort) > self.port_num: self.ctrInfo.pop(strPort, None) continue if strPort in self.ctrInfo: self.ctrInfo[strPort].update({'status': STATUS_MAP.get(status, 0)}) else: self.ctrInfo[strPort] = {'status': status} allPorts, usedPorts, usePorts = self.get_port_static_info(self.ctrInfo) self.ctrInfo.update({'allPorts': allPorts, 'usedPorts': usedPorts, 'usePorts': usePorts}) Device.update_dev_control_cache(self._device['devNo'], self.ctrInfo) return result def get_port_info(self, port): resp = self.send_mqtt({'funCode': 'info', 'port': int(port)}, timeout=10) order_id = resp.get('order_id') result = {} if 'power' in resp: result['power'] = round(resp['power'], 1) if not order_id: return result order = ConsumeRecord.objects.filter(Q(orderNo=order_id) | Q(startKey=order_id)).first() result.update({'openId': order.openId, 'nickName': order.nickname, 'coins': str(order.coin)}) if 'duration' in resp: result['usedTime'] = round(resp['duration'] / 60.0, 1) if 'cardNo' in resp: result['cardNo'] = resp['cardNo'] if 'amount' in resp and 'left' in resp: amount = resp['amount'] left = min(resp['left'], amount) result['leftMoney'] = '{}元'.format(round(float(order.coin) * left / (1.0 * amount), 2)) result['consumeMoney'] = '{}元'.format(round(float(order.coin) * (amount - left) / (1.0 * amount), 2)) return result def get_many_port_info(self, portList): data = self.send_mqtt({'funCode': 'orders'}, timeout=10) orders = data.get('orders') result = {} for _ in range(1, self.port_num+1, 1): resp = orders.get(str(_)) order_id = resp.get('order_id') item = {'index': str(_)} if 'power' in resp: item['power'] = round(resp['power'], 1) if not order_id: continue item['status'] = Const.DEV_WORK_STATUS_WORKING order = ConsumeRecord.objects.filter(Q(orderNo=order_id) | Q(startKey=order_id)).first() item.update({'openId': order.openId, 'nickName': order.nickname, 'coins': str(order.coin),}) if 'duration' in resp: item['usedTime'] = round(resp['duration'] / 60.0, 1) if 'cardNo' in resp: item['cardNo'] = resp['cardNo'] if 'amount' in resp and 'left' in resp: amount = resp['amount'] left = min(resp['left'], amount) item['leftMoney'] = '{}元'.format(round(float(order.coin) * left / (1.0 * amount), 2)) item['consumeMoney'] = '{}元'.format(round(float(order.coin) * (amount - left) / (1.0 * amount), 2)) result[str(_)] = item return dict(filter(lambda items: items[0] in portList, result.items())) def async_update_portinfo_from_dev(self): class Sender(threading.Thread): def __init__(self, smartBox): super(Sender, self).__init__() self._smartBox = smartBox def run(self): try: self._smartBox.get_port_status_from_dev() except Exception as e: logger.info('get port stats from dev,e=%s' % e) sender = Sender(self) sender.start() def get_port_using_detail(self, port, ctrInfo, isLazy=False): """ 获取设备端口的详细信息 :param port: :param ctrInfo: :return: """ detailDict = ctrInfo.get(str(port), {}) try: portInfo = self.get_port_info(str(port)) # 有的主机报的信息leftTime错误 if portInfo.has_key('leftTime') and portInfo['leftTime'] < 0: portInfo.pop('leftTime') detailDict.update(portInfo) except Exception as e: logger.exception('get port info from dev=%s err=%s' % (self.device.devNo, e)) return detailDict portData = {} startTimeStr = detailDict.get('startTime', None) if startTimeStr is not None and 'usedTime' not in detailDict: startTime = to_datetime(startTimeStr) usedTime = int(round((datetime.datetime.now() - startTime).total_seconds() / 60.0)) portData['usedTime'] = usedTime elif detailDict.get('usedTime'): usedTime = detailDict.get('usedTime') portData['usedTime'] = usedTime else: usedTime = None if 'cType' in detailDict: if detailDict['cType'] == 1: detailDict['leftTime'] = detailDict.get('left_value') else: detailDict['leftElec'] = round(detailDict.get('left_value') * 0.01, 2) if detailDict.has_key('leftTime') and (usedTime > 0): if detailDict['leftTime'] == 65535: portData['leftTime'] = 65535 detailDict['usedTime'] = 0 detailDict['actualNeedTime'] = 0 else: portData['actualNeedTime'] = int(detailDict['leftTime']) + int(usedTime) if detailDict.has_key('needTime') and portData['actualNeedTime'] > detailDict['needTime']: portData['actualNeedTime'] = detailDict['needTime'] portData['leftTime'] = detailDict['leftTime'] if detailDict.has_key('coins'): if self.device.is_auto_refund: portData['leftMoney'] = round( float(detailDict['coins']) * int(detailDict['leftTime']) / ( int(detailDict['leftTime']) + int(usedTime)), 2) portData['consumeMoney'] = round( float(detailDict['coins']) * int(portData['usedTime']) / ( int(detailDict['leftTime']) + usedTime), 2) elif detailDict.has_key('leftTime'): portData['leftTime'] = detailDict['leftTime'] if (not detailDict.has_key('leftTime')) and (usedTime is not None): if detailDict.has_key('needTime'): portData['leftTime'] = detailDict['needTime'] - usedTime if detailDict.has_key('coins') and float(detailDict['coins']) != 0: # 只有支持退费的设备才显示可退费数据 if self.device.is_auto_refund: portData['leftMoney'] = round( float(detailDict['coins']) * portData['leftTime'] / detailDict['needTime'], 2) portData['consumeMoney'] = round( float(detailDict['coins']) * portData['usedTime'] / detailDict['needTime'], 2) if detailDict.has_key('openId'): user = MyUser.objects(openId=detailDict['openId']).first() if user: portData['nickName'] = user.nickname if detailDict.has_key('cardId'): if not detailDict.has_key('consumeType'): portData['consumeType'] = 'card' card = Card.objects.get(id=detailDict['cardId']) if card.cardName: portData['cardName'] = card.cardName portData['cardNo'] = card.cardNo # 注意,如果是IC卡,不支持余额回收,这里也不要显示出来 if card.cardType == 'IC' and portData.has_key('leftMoney'): portData.pop('leftMoney') elif detailDict.has_key('openId') and (not detailDict.has_key('consumeType')): if detailDict.get('vCardId'): portData['consumeType'] = 'mobile_vcard' else: portData['consumeType'] = 'mobile' elif detailDict.has_key('consumeType') and detailDict['consumeType'] == 'coin': portData['consumeType'] = 'coin' # 硬币的都无法退费 if portData.has_key('leftMoney'): portData.pop('leftMoney') # 做个特殊处理 if portData.has_key('needTime'): if portData['needTime'] == '999' or portData['needTime'] == '充满自停': portData['needTime'] = u'充满自停' else: portData['needTime'] = u'%s分钟' % portData['needTime'] # 如果剩余时间为65535,表示未接插头 if portData.has_key('leftTime') and portData['leftTime'] == 65535: portData['leftTime'] = u'(线路空载)' portData['usedTime'] = 0 portData['needTime'] = 0 detailDict.update(portData) for k, v in detailDict.items(): if v < 0: detailDict.pop(k) # 因为前台显示的开始时间如果带年,就显示不下,这里做个切割 if detailDict.has_key('startTime') and detailDict['startTime'].count('-') == 2: detailDict['startTime'] = to_datetime(detailDict['startTime']).strftime('%m-%d %H:%M:%S') return detailDict @property def isHaveStopEvent(self): return True def start_device_realiable(self, order): # type:(ConsumeRecord)->dict if order.orderNo[:10] == self.device.ownerId[-10:]: # 此时为远程上分 先停一次 self.stop_charging_port(order.used_port) # 等待串口相应 time.sleep(1) attachParas = order.attachParas package = order.package price = package['price'] port = int(attachParas['chargeIndex']) data = { 'funCode': 'start', 'order_id': order.orderNo, 'amount': int(price * 100), 'session_id': int(time.time() % 255), 'port': port, 'attach_paras': {}, } # 订单中记录一份下发收到源数据 uart_source = [] uart_source.append( {'write_start': json.dumps(data, indent=4), 'time': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}) result = MessageSender.send(device=self.device, cmd=DeviceCmdCode.OPERATE_DEV_SYNC, payload=data, timeout=120) if result['rst'] == 1: raise ServiceException({'result': 2, 'description': '设备串口故障或设备处于急停模式'}) elif result['rst'] == 2: raise ServiceException({'result': 2, 'description': '设备拒绝启动(否认应答)'}) elif result['rst'] == 3: raise ServiceException({'result': 2, 'description': '充电枪端口号缺失'}) elif result['rst'] == 4: raise ServiceException({'result': 2, 'description': '充电枪端口正在工作中'}) uart_source.append( {'rece_start': json.dumps(result, indent=4), 'time': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}) # 兼容后台启动 try: order.update(uart_source=uart_source, servicedInfo={'chargeIndex': port, }) except: pass return result def calc_elec_fee(self, spend_elec): group = Group.objects.get(id=self.device['groupId']) return float(group.otherConf.get('elecFee', 0)) * spend_elec @property def support_monthly_package(self): return False def stop_by_order(self, port, orderNo): return self.stop_charging_port(port) def isHaveFaultHandle(self): return True def faultHandle(self, **kw): pass def send_mqtt(self, data=None, cmd=DeviceCmdCode.OPERATE_DEV_SYNC, timeout=MQTT_TIMEOUT.NORMAL): """ 发送mqtt 指令默认210 返回data """ if 'cmd' not in data: data.update({'cmd': cmd}) if 'IMEI' not in data: data.update({'IMEI': self.device.devNo}) result = MessageSender.send(self.device, cmd, data, timeout=timeout) 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 def support_device_package(self): return True def format_device_package(self, isTemp=False, **kw): def __generate_id(ids): i = 1 while True: if str(i) in ids: i += 1 else: return str(i) def __formart_ruleList(): packageList = kw['serviceData'] ids = [str(rule['id']) for rule in packageList if 'id' in rule] washConfig = {} for i, rule in enumerate(packageList): ruleId = str(rule.get('id', '')) if not ruleId: ruleId = __generate_id(ids) ids.append(ruleId) washConfig[ruleId] = {} if 'switch' in rule: washConfig[ruleId].update({'switch': rule['switch']}) if 'billingMethod' in rule: washConfig[ruleId].update({'billingMethod': rule['billingMethod']}) if 'price' in rule: washConfig[ruleId].update({'price': round(float(rule['price']), 2) or 0, 'coins': round(float(rule['price']), 2) or 0}) # if 'time' in rule: # washConfig[ruleId].update({'time': rule['time'] or 0}) if 'name' in rule: washConfig[ruleId].update({'name': rule['name'] or '套餐{}'.format(i + 1)}) if 'unit' in rule: washConfig[ruleId].update({'unit': rule['unit']}) if 'sn' in rule: washConfig[ruleId].update({'sn': rule['sn']}) if 'autoStop' in rule: washConfig[ruleId]['autoStop'] = rule['autoStop'] if 'autoRefund' in rule: washConfig[ruleId]['autoRefund'] = rule['autoRefund'] if 'minAfterStartCoins' in rule: washConfig[ruleId]['minAfterStartCoins'] = rule['minAfterStartCoins'] if 'minFee' in rule: washConfig[ruleId]['minFee'] = rule['minFee'] if isTemp: pass # 检验部分 if RMB(rule.get('price') or 0) > self.device.owner.maxPackagePrice: raise ServiceException( {'result': 0, 'description': '套餐( {} )金额超限'.format(rule['name']), 'payload': {}}) return washConfig def __formart_displaySwitchs(): return {'displayCoinsSwitch': False, 'displayTimeSwitch': False, 'displayPriceSwitch': True, 'setPulseAble': False, 'setBasePriceAble': False} washConfig = __formart_ruleList() displaySwitchs = __formart_displaySwitchs() return washConfig, displaySwitchs def dealer_show_package(self, isTemp=False, **kw): def get_rule_list(): if isTemp: config = self.device.get('tempWashConfig', {}) else: config = self.device['washConfig'] ruleList = [] for packageId, rule in config.items(): item = { 'id': packageId } if 'switch' in rule: item['switch'] = rule['switch'] if 'name' in rule: item['name'] = rule['name'] if 'coins' in rule: item['coins'] = rule['coins'] if 'price' in rule: item['price'] = rule['price'] if 'time' in rule: item['time'] = rule['time'] if 'description' in rule: item['description'] = rule['description'] if 'unit' in rule: item['unit'] = rule['unit'] if 'imgList' in rule: item['imgList'] = rule['imgList'] if 'sn' in rule: item['sn'] = rule['sn'] if 'autoStop' in rule: item['autoStop'] = rule['autoStop'] if 'minAfterStartCoins' in rule: item['minAfterStartCoins'] = rule['minAfterStartCoins'] if 'minFee' in rule: item['minFee'] = rule['minFee'] ruleList.append(item) return sorted(ruleList, key=lambda x: (x.get('sn'), x.get('id'))) def get_display_switchs(): if "displaySwitchs" in self.device["otherConf"]: displaySwitchs = self.device["otherConf"].get('displaySwitchs') else: displaySwitchs = {'displayCoinsSwitch': False, 'displayTimeSwitch': False, 'displayPriceSwitch': True, "setPulseAble": False, "setBasePriceAble": False} return displaySwitchs ruleList = get_rule_list() displaySwitchs = get_display_switchs() return {"ruleList": ruleList, 'displaySwitchs': displaySwitchs}