changyuanCar2.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604
  1. # -*- coding: utf-8 -*-
  2. # !/usr/bin/env python
  3. import datetime
  4. import logging
  5. import time
  6. from decimal import Decimal
  7. from apilib.utils_datetime import timestamp_to_dt
  8. from apps.common.utils import int_to_hex
  9. from apps.web.common.models import TempValues
  10. from apps.web.constant import Const, DeviceCmdCode, MQTT_TIMEOUT
  11. from apps.web.core.adapter.base import SmartBox, fill_2_hexByte
  12. from apps.web.core.exceptions import ServiceException
  13. from apps.web.core.networking import MessageSender
  14. from apps.web.dealer.models import Dealer
  15. from apps.web.device.models import Device
  16. logger = logging.getLogger(__name__)
  17. class ChangYuanCarV2(SmartBox):
  18. STATUS_MAP = {
  19. "AA": Const.DEV_WORK_STATUS_WORKING,
  20. "AB": Const.DEV_WORK_STATUS_APPOINTMENT,
  21. "AC": Const.DEV_WORK_STATUS_CONNECTED,
  22. "55": Const.DEV_WORK_STATUS_IDLE
  23. }
  24. FINISH_REASON_MAP = {
  25. "E0": "过压",
  26. "E1": "过流",
  27. "E2": "超功率",
  28. "E3": "限时时间到",
  29. "E4": "正常结束",
  30. "E5": "急停结束",
  31. "E7": "支付金额结束",
  32. "E8": "汽车充满电结束",
  33. "E9": "超温结束",
  34. "EA": "计量通信失败结束",
  35. "EB": "汽车连接失败",
  36. "EC": "用户刷卡停止"
  37. }
  38. DEFAULT_DISABLE_DEVICE = False
  39. DEFAULT_CARD_CONSUME = 100
  40. def _send_data(self, funCode, data, timeout = MQTT_TIMEOUT.NORMAL):
  41. result = MessageSender.send(device = self.device, cmd = DeviceCmdCode.OPERATE_DEV_SYNC, payload = {
  42. "IMEI": self._device["devNo"],
  43. "funCode": funCode,
  44. "data": data
  45. }, timeout = timeout)
  46. if result["rst"] != 0:
  47. if result['rst'] == -1:
  48. raise ServiceException({'result': 2, 'description': u'充电桩正在玩命找网络,请稍候再试'})
  49. elif result['rst'] == 1:
  50. raise ServiceException({'result': 2, 'description': u'充电桩主板连接故障'})
  51. else:
  52. raise ServiceException({'result': 2, 'description': u'系统错误'})
  53. return result
  54. def _get_device_settings(self):
  55. result = self._send_data("A0", "00")
  56. data = result.get("data", dict())
  57. isFree = data[8: 10]
  58. free = 0 if isFree == "00" else 1
  59. electricPrice = int(data[10: 12], 16) / 100.0
  60. maxConsume = int(data[12: 16], 16) / 100.0
  61. maxChargeTime = int(data[16: 20], 16)
  62. volume = int(data[20: 22], 16)
  63. return {
  64. "is_free": free,
  65. "electricPrice": electricPrice,
  66. "maxConsume": maxConsume,
  67. "maxChargeTime": maxChargeTime,
  68. "volume": volume
  69. }
  70. def _set_device_settings(self, conf):
  71. isFree = conf.pop("is_free", None)
  72. electricPrice = conf.pop("electricPrice", None)
  73. maxConsume = conf.pop("maxConsume", None)
  74. maxChargeTime = conf.pop("maxChargeTime", None)
  75. volume = conf.pop("volume", None)
  76. data = ""
  77. data += "AA" if int(isFree) == 1 else "00"
  78. data += fill_2_hexByte(hex(int(Decimal(electricPrice) * 100)), 2)
  79. data += fill_2_hexByte(hex(int(Decimal(maxConsume) * 100)), 4)
  80. data += fill_2_hexByte(hex(int(Decimal(maxChargeTime))), 4)
  81. data += fill_2_hexByte(hex(int(Decimal(volume))), 2)
  82. self._send_data("A1", data)
  83. # TODO zjl 数据库保存一份
  84. def _get_consume_count(self):
  85. """
  86. 从充电桩上获取累计 的消费金额以及消费电量
  87. :return:
  88. """
  89. result = self._send_data("A2", "00")
  90. data = result.get("data", dict())
  91. totalConsume = data[8: 14]
  92. totalElec = data[14: 20]
  93. return {
  94. "totalConsume": int(totalConsume, 16) / 100.0,
  95. "totalElec": int(totalElec, 16) / 100.0
  96. }
  97. def _clean_consume_count(self, consume = False, elec = False):
  98. """
  99. 清除充电桩上累计信息
  100. :param consume:
  101. :param elec:
  102. :return:
  103. """
  104. if consume and elec:
  105. sendData = "E3"
  106. elif not consume:
  107. sendData = "E2"
  108. elif not elec:
  109. sendData = "E1"
  110. else:
  111. return
  112. self._send_data("A3", sendData)
  113. def _get_not_refund_record(self):
  114. """
  115. 获取最近10条未返费的卡号以及剩余金额
  116. :return:
  117. """
  118. result = self._send_data("A4", "00")
  119. data = result.get("data", dict())
  120. noRefund = list()
  121. data = data[8: -8]
  122. for i in xrange(10):
  123. tempData = data[12 * i: 12 * (i + 1)]
  124. cardNo = tempData[: 8]
  125. amount = tempData[8: 12]
  126. if cardNo == "FFFFFFFF" or cardNo == "00000000": continue
  127. amount = int(amount, 16) / 100.0
  128. noRefund.append({"cardNo": cardNo, "amount": amount})
  129. return noRefund
  130. def _get_recharge_record_from_device(self, num):
  131. """
  132. 从设备上 获取最近的几条充电记录
  133. :param num:
  134. :return:
  135. """
  136. num = 30 if num > 30 else num
  137. numHex = fill_2_hexByte(hex(int("80", 16) + int(num)), 2)
  138. result = self._send_data("A5", numHex)
  139. data = result.get("data", dict())
  140. dataLen = int(data[6: 8], 16) * 2 - 2 # 数据字符数量
  141. recordData = data[10: 10 + dataLen]
  142. recordList = list()
  143. while recordData:
  144. cardNo = recordData[: 8]
  145. electricNum = int(recordData[8: 12], 16) / 100.0
  146. chargeTime = int(recordData[12: 16], 16)
  147. chargeBalance = int(recordData[16: 20], 16) / 100.0
  148. cardBalance = int(recordData[20: 26], 16) / 100.0
  149. recordData = recordData[26:]
  150. if cardNo == "FFFFFFFF":
  151. continue
  152. recordList.append(
  153. {
  154. "type": u"{} 刷卡充电".format(cardNo) if cardNo != "00000000" else u"扫码充电",
  155. "elec": u"{} 度".format(electricNum),
  156. "duration": u"{} 分钟".format(chargeTime),
  157. "chargeBalance": u"{} 元".format(chargeBalance),
  158. "cardBalance": u"{} 元".format(cardBalance) if cardBalance else u"本次无刷卡消费"
  159. }
  160. )
  161. return recordList
  162. def _get_device_status(self):
  163. """
  164. 即时获取设备状态
  165. :return:
  166. """
  167. result = self._send_data("A6", "00")
  168. data = result.get("data", dict())
  169. status = data[8:10]
  170. cardNo = data[30:38]
  171. if cardNo == "00000000":
  172. cardNo = u"在线支付"
  173. result = {
  174. "status": self.STATUS_MAP.get(status, Const.DEV_WORK_STATUS_IDLE)
  175. }
  176. if result["status"] == Const.DEV_WORK_STATUS_WORKING:
  177. result.update({
  178. "voltage": int(data[10:14], 16) / 1.0,
  179. "power": int(data[14:18], 16) / 1.0,
  180. "usedElec": int(data[18:22], 16) / 100.0,
  181. "usedTime": int(data[22:26], 16),
  182. "leftMoney": int(data[26:30], 16) / 100.0,
  183. "cardNo": cardNo
  184. })
  185. Device.update_dev_control_cache(self.device.devNo, result)
  186. return result
  187. def _start(self, money):
  188. """
  189. 微信支付命令
  190. :param money:
  191. :return:
  192. """
  193. sendData = "{:0<8}".format(fill_2_hexByte(hex(int(money * 100)), 4))
  194. result = self._send_data("A7", sendData, timeout = MQTT_TIMEOUT.START_DEVICE)
  195. data = result.get("data", dict())
  196. if data[8:12] != "4F4B":
  197. raise ServiceException({'result': 2, 'description': u'设备相应错误,未能成功启动设备,请上报设备故障'})
  198. return result
  199. def _set_device_disable(self, disable):
  200. """
  201. 设置 设备的可用
  202. :param disable:
  203. :return:
  204. """
  205. status = "E9" if disable else "E8"
  206. result = self._send_data("A9", status)
  207. data = result.get("data", dict())
  208. if data[8: 12] != "4F4B":
  209. raise ServiceException({'result': 2, 'description': u'设置失败,请重试'})
  210. otherConf = self._device.get("otherConf", {})
  211. otherConf["disableDevice"] = disable
  212. Device.objects.filter(devNo = self._device["devNo"]).update(otherConf = otherConf)
  213. Device.invalid_device_cache(self._device["devNo"])
  214. def _async_card_balance(self, cardType, cardNo, balance):
  215. """
  216. 同步卡的余额 加卡的余额一次行下发过去
  217. :param cardType: 00 下发单位是元 01 下发单位是 分
  218. :param cardNo: 卡号
  219. :param balance: 单位是RMB
  220. :return:
  221. """
  222. balance = balance if cardType == "00" else balance * 100
  223. # 获取随机流水号
  224. sidKey = "{}-{}".format(self.device.devNo, cardNo)
  225. sid = TempValues.get(sidKey)
  226. balanceHex = int_to_hex(int(balance), 6)
  227. sidHex = int_to_hex(int(sid))
  228. MessageSender.send(device = self.device, cmd = DeviceCmdCode.OPERATE_DEV_NO_RESPONSE, payload = {
  229. "IMEI": self.device.devNo,
  230. "funCode": "F3",
  231. "data": cardType + balanceHex + sidHex
  232. })
  233. def _read_card_balance(self):
  234. result = self._send_data("AD", "00")
  235. data = result["data"]
  236. if data[6:8] == "02" and data[8:12] == "4552":
  237. raise ServiceException({'result': 2, 'description': u'读取失败,请将卡放置在设备指定区域'})
  238. cardNo = data[8:16]
  239. balance = int(data[16:22], 16) / 100.0
  240. return {
  241. "cardNo": cardNo,
  242. "balance": balance
  243. }
  244. def _stop(self, isLawFul = True):
  245. logger.info("ready to stop device <{}>".format(self.device.devNo))
  246. data = "00" if isLawFul else "01"
  247. self._send_data("AF", data)
  248. def _restart_device(self):
  249. self._send_data("B0", "00")
  250. def _response_a8id(self, a8id):
  251. MessageSender.send(self.device, DeviceCmdCode.OPERATE_DEV_NO_RESPONSE, {
  252. "IMEI": self._device["devNo"],
  253. "funCode": "FA",
  254. "data": "",
  255. "a8id": a8id
  256. })
  257. @staticmethod
  258. def _parse_event_A8(data):
  259. reasonCode = data[8:10]
  260. desc = ChangYuanCarV2.FINISH_REASON_MAP.get(reasonCode, u"未知错误")
  261. cardNo = data[10:18]
  262. usedElec = int(data[18:22], 16) / 100.0
  263. chargeTime = int(data[22:26], 16)
  264. balance = int(data[26:30], 16) / 100.0
  265. cardBalance = int(data[30:36], 16) / 100.0
  266. payMoney = int(data[38:40] + data[36:38], 16) / 100.0
  267. return {
  268. "reasonCode": reasonCode,
  269. "desc": desc,
  270. "cardNo": cardNo,
  271. "usedElec": usedElec,
  272. "chargeTime": chargeTime,
  273. "balance": balance,
  274. "cardBalance": cardBalance,
  275. "payMoney": payMoney
  276. }
  277. @staticmethod
  278. def _parse_event_AE(data):
  279. statusMap = {
  280. "B1": Const.DEV_WORK_STATUS_CONNECTED,
  281. "B5": Const.DEV_WORK_STATUS_IDLE
  282. }
  283. status = statusMap.get(data[8:10], Const.DEV_WORK_STATUS_IDLE)
  284. return {
  285. "status": status
  286. }
  287. @staticmethod
  288. def _parse_event_B1(data):
  289. cardNo = data[8:16]
  290. return {
  291. "cardNo": cardNo
  292. }
  293. @staticmethod
  294. def _parse_event_B2(data):
  295. """防止他们注册错设备类型"""
  296. if len(data) <= 20:
  297. return dict()
  298. leftBalance = int(data[8:12], 16) / 100.0
  299. usedElec = int(data[12:16], 16) / 100.0
  300. temperature = int(data[18:20], 16)
  301. power = int(data[20:24], 16)
  302. voltage = int(data[28:32], 16)
  303. sid = int(data[32:36], 16)
  304. temperature = temperature if data[16:18] == "00" else -temperature
  305. return {
  306. "leftBalance": leftBalance,
  307. "usedElec": usedElec,
  308. "temperature": u"{}度 更新于{}".format(temperature, datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")),
  309. "power": power,
  310. "voltage": voltage,
  311. "sid": sid
  312. }
  313. @staticmethod
  314. def _parse_event_B3(data):
  315. cardNo = data[8:16]
  316. beforeRefund = int(data[16:22], 16) / 100.0
  317. refund = int(data[22:26], 16) / 100.0
  318. afterRefund = int(data[26:32], 16) / 100.0
  319. return {
  320. "cardNo": cardNo,
  321. "beforeRefund": beforeRefund,
  322. "refund": refund,
  323. "afterRefund": afterRefund
  324. }
  325. @staticmethod
  326. def _parse_event_F4(data):
  327. """
  328. 请求卡余额同步 此指令是里显卡同步余额的发起指令
  329. cardType 01表示预付卡 上报单位为分 00表示次卡 上报单位是元
  330. :param data:
  331. :return:
  332. """
  333. cardType = data[8:10]
  334. cardNo = data[10:18]
  335. cardBalanceHex = data[18:24]
  336. if cardType == "00":
  337. cardBalance = int(cardBalanceHex, 16)
  338. else:
  339. cardBalance = int(cardBalanceHex, 16) / 100.0
  340. return {
  341. "cardType": cardType,
  342. "cardNo": cardNo,
  343. "cardBalance": cardBalance
  344. }
  345. @staticmethod
  346. def _parse_event_F3(data):
  347. """
  348. 余额同步成功 这个地方上报告知成功还是失败 如果存在失败的情况 将订单的状态重新切换或finishedPay
  349. :param data:
  350. :return:
  351. """
  352. cardType = data[8:10]
  353. cardNo = data[10:18]
  354. cardBalanceHex = data[18:24]
  355. asyncStatus = data[24:26]
  356. sidHex = data[26:30]
  357. if cardType == "00":
  358. cardBalance = int(cardBalanceHex, 16)
  359. else:
  360. cardBalance = int(cardBalanceHex, 16) / 100.0
  361. return {
  362. "cardType": cardType,
  363. "cardNo": cardNo,
  364. "cardBalance": cardBalance,
  365. "asyncStatus": True if asyncStatus == "AA" else False,
  366. "sid": int(sidHex, 16)
  367. }
  368. def test(self, coins):
  369. return self._start(coins)
  370. @property
  371. def isHaveStopEvent(self):
  372. return True
  373. def get_dev_setting(self):
  374. settings = self._get_device_settings()
  375. otherConf = self.device.get("otherConf", dict())
  376. disableDevice = otherConf.get("disableDevice", self.DEFAULT_DISABLE_DEVICE)
  377. needBindCard = otherConf.get("needBindCard", True)
  378. settings.update({"disableButton": int(disableDevice), "needBindCard": int(needBindCard)})
  379. otherConf.update(settings)
  380. Device.objects.filter(devNo = self.device.devNo).update(otherConf = otherConf)
  381. Device.invalid_device_cache(self.device.devNo)
  382. consume = self._get_consume_count()
  383. settings.update(consume)
  384. devCache = Device.get_dev_control_cache(self.device.devNo)
  385. temperature = devCache.get("temperature", "暂无数据")
  386. settings.update({"temperature": temperature})
  387. return settings
  388. def set_device_function_param(self, request, lastSetConf):
  389. disable = request.POST.get("disableButton")
  390. if disable is not None:
  391. dealer = Dealer.objects.filter(id = self.device.ownerId).first()
  392. if "dealerDisableDevice" not in dealer.features:
  393. raise ServiceException({"result": 2, "description": "抱歉,您无此操作权限,请联系厂家获取权限"})
  394. return self.set_dev_disable(bool(int(disable)))
  395. if "needBindCard" in request.POST:
  396. needBindCard = bool(int(request.POST.get('needBindCard')))
  397. otherConf = self.device.get("otherConf", dict())
  398. otherConf.update({"needBindCard": needBindCard})
  399. Device.objects.get(devNo=self.device.devNo).update(otherConf=otherConf)
  400. Device.invalid_device_cache(self.device.devNo)
  401. return
  402. newDict = {
  403. "is_free": request.POST.get("is_free", None),
  404. "electricPrice": request.POST.get("electricPrice", None),
  405. "maxConsume": request.POST.get("maxConsume", None),
  406. "maxChargeTime": request.POST.get("maxChargeTime", None),
  407. "volume": request.POST.get("volume", None)
  408. }
  409. otherConf = self.device.get("otherConf", dict())
  410. for setName, setValue in newDict.items():
  411. if setValue is not None:
  412. otherConf.update({setName: setValue})
  413. self._set_device_settings(otherConf)
  414. def set_dev_disable(self, disable):
  415. self._set_device_disable(disable)
  416. def set_device_function(self, request, lastSetConf):
  417. remoteStop = request.POST.get("remoteStop", None)
  418. clearTotalConsume = request.POST.get("clearTotalConsume", False)
  419. clearTotalElec = request.POST.get("clearTotalElec", False)
  420. reboot = request.POST.get("reboot", False)
  421. # 停止设备
  422. if remoteStop:
  423. self._stop()
  424. if reboot:
  425. self._restart_device()
  426. # 清除设备计费信息
  427. if any([clearTotalConsume, clearTotalElec]):
  428. self._clean_consume_count(clearTotalConsume, clearTotalElec)
  429. def get_device_function_by_key(self, keyName):
  430. if keyName == "noRefund":
  431. records = self._get_not_refund_record()
  432. return {"noRefund": records}
  433. elif keyName == "record":
  434. records = self._get_recharge_record_from_device(15)
  435. return {"record": records}
  436. def stop(self, port = None):
  437. return self._stop()
  438. def check_dev_status(self, attachParas = None):
  439. result = self._get_device_status()
  440. status = result.get("status")
  441. if status != Const.DEV_WORK_STATUS_CONNECTED:
  442. raise ServiceException({"result": "2", "description": u"请先将充电桩枪把插上汽车充电口"})
  443. def analyze_event_data(self, data):
  444. cmdCode = data[4:6]
  445. funcName = "_parse_event_{}".format(cmdCode)
  446. func = getattr(ChangYuanCarV2, funcName, None)
  447. if func and callable(func):
  448. eventData = func(data)
  449. # 解析出错的情况下 或者解析没有返回的情况下
  450. if not eventData:
  451. logger.error("receive event data error parse, data <{}>".format(data))
  452. return
  453. eventData.update({"cmdCode": cmdCode})
  454. return eventData
  455. def start_device(self, package, openId, attachParas):
  456. devCache = Device.get_dev_control_cache(self.device.devNo)
  457. if devCache.get("status") != Const.DEV_WORK_STATUS_CONNECTED:
  458. status = self._get_device_status().get("status")
  459. if status != Const.DEV_WORK_STATUS_CONNECTED:
  460. raise ServiceException({'result': 2, 'description': u'请先将充电桩枪把连接'})
  461. coins = package.get("coins")
  462. price = package.get("price")
  463. orderNo = attachParas.get("orderNo")
  464. result = self._start(price)
  465. rechargeRcdId = attachParas.get("linkedRechargeRecordId")
  466. startTimeStamp = int(time.time())
  467. devCache = {
  468. "openId": openId,
  469. "isStart": True,
  470. "coins": coins,
  471. "price": price,
  472. "rechargeRcdId": str(rechargeRcdId) if rechargeRcdId else None, # zjl ObjectId -> str
  473. "status": Const.DEV_WORK_STATUS_WORKING,
  474. "startTime": timestamp_to_dt(startTimeStamp).strftime("%Y-%m-%d %H:%M:%S"),
  475. "orderNo": orderNo,
  476. "finishedTime": startTimeStamp + 24 * 60 * 60 # zjl new
  477. }
  478. Device.invalid_device_control_cache(self.device.devNo)
  479. Device.update_dev_control_cache(self.device.devNo, devCache)
  480. result["finishedTime"] = startTimeStamp + 60 * 60 * 24
  481. if openId: # zjl new
  482. result["rechargeRcdId"] = str(rechargeRcdId) if rechargeRcdId else None # zjl new
  483. return result
  484. def dealer_get_port_status(self):
  485. devInfo = self.get_port_info(1)
  486. if "status" not in devInfo:
  487. devInfo["status"] = Const.DEV_WORK_STATUS_IDLE
  488. return {"1": devInfo}
  489. def get_port_info(self, port):
  490. self._get_device_status()
  491. devCache = Device.get_dev_control_cache(self.device.devNo) or dict()
  492. return devCache
  493. def active_deactive_port(self, port, active):
  494. if not active:
  495. self._stop()
  496. else:
  497. super(ChangYuanCarV2, self).active_deactive_port(port, active)