kunyuanCar.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617
  1. # -*- coding: utf-8 -*-
  2. # !/usr/bin/env python
  3. import binascii
  4. import datetime
  5. import logging
  6. import random
  7. import time
  8. from decimal import Decimal
  9. from apps.web.constant import Const, DeviceCmdCode, MQTT_TIMEOUT
  10. from apps.web.core.adapter.base import SmartBox, fill_2_hexByte
  11. from apps.web.core.exceptions import ServiceException
  12. from apps.web.core.networking import MessageSender
  13. from apps.web.dealer.models import Dealer
  14. from apps.web.device.models import Device
  15. from apps.web.core.device_define.kunyuanCar import DeviceParams, DEFAULT_VERSION, ORDER_VERSION
  16. from apps.web.user.models import ConsumeRecord
  17. logger = logging.getLogger(__name__)
  18. class KunYuanCar(SmartBox):
  19. """ 坤元模拟的昌原协议 """
  20. @staticmethod
  21. def _trans_card(cardNo):
  22. """
  23. 暂时不知道 坤元是怎么印制卡片数字的 涉及卡号转换的地方统一处理
  24. """
  25. if cardNo != DeviceParams.VOID_CARD_NO:
  26. return str(int(cardNo, 16))
  27. return ""
  28. def _send_data(self, funCode, data, cmd=None, timeout=MQTT_TIMEOUT.NORMAL):
  29. if not cmd:
  30. cmd = DeviceCmdCode.OPERATE_DEV_SYNC
  31. result = MessageSender.send(
  32. device=self.device,
  33. cmd=cmd,
  34. payload={
  35. "IMEI": self._device["devNo"],
  36. "funCode": funCode,
  37. "data": data
  38. },
  39. timeout=timeout
  40. )
  41. if result["rst"] != 0:
  42. if result['rst'] == -1:
  43. raise ServiceException({'result': 2, 'description': u'充电桩正在玩命找网络,请稍候再试'})
  44. elif result['rst'] == 1:
  45. raise ServiceException({'result': 2, 'description': u'充电桩主板连接故障'})
  46. else:
  47. raise ServiceException({'result': 2, 'description': u'系统错误'})
  48. return result
  49. def _get_device_settings(self):
  50. """
  51. 读取主板设置参数
  52. 新版本的读取指令 增加版本号以及端口类型
  53. """
  54. result = self._send_data("A0", "00")
  55. data = result["data"]
  56. isFree = data[8: 10]
  57. free = 0 if isFree == "00" else 1
  58. electricPrice = int(data[10: 12], 16) / 100.0
  59. maxConsume = int(data[12: 16], 16) / 100.0
  60. maxChargeTime = int(data[16: 20], 16)
  61. volume = int(data[20: 22], 16)
  62. if data[6: 8] == "0B":
  63. version = str(int(data[22: 26], 16) / 100.0)
  64. port1 = "https://resource.washpayer.com/ky_port2.png" if data[26: 28] == "71" else "https://resource.washpayer.com/ky_port1.png"
  65. port2 = "https://resource.washpayer.com/ky_port2.png" if data[28: 30] == "71" else "https://resource.washpayer.com/ky_port1.png"
  66. else:
  67. version = str(DEFAULT_VERSION)
  68. port1 = ""
  69. port2 = ""
  70. return {
  71. "is_free": free,
  72. "electricPrice": electricPrice,
  73. "maxConsume": maxConsume,
  74. "maxChargeTime": maxChargeTime,
  75. "volume": volume,
  76. "version": version,
  77. "1": port1,
  78. "2": port2
  79. }
  80. def _set_device_settings(self, conf):
  81. """
  82. 设置主板参数
  83. """
  84. isFree = conf.pop("is_free", None)
  85. electricPrice = conf.pop("electricPrice", None)
  86. maxConsume = conf.pop("maxConsume", None)
  87. maxChargeTime = conf.pop("maxChargeTime", None)
  88. volume = conf.pop("volume", None)
  89. data = ""
  90. data += "AA" if int(isFree) == 1 else "00"
  91. data += fill_2_hexByte(hex(int(Decimal(electricPrice) * 100)), 2)
  92. data += fill_2_hexByte(hex(int(Decimal(maxConsume) * 100)), 4)
  93. data += fill_2_hexByte(hex(int(Decimal(maxChargeTime))), 4)
  94. data += fill_2_hexByte(hex(int(Decimal(volume))), 2)
  95. self._send_data("A1", data)
  96. def _get_consume_count(self, port=0):
  97. """
  98. 获取主板的消费统计 和端口有关
  99. :return:
  100. """
  101. result = self._send_data("A2", "{:02X}".format(port))
  102. data = result["data"]
  103. totalConsume = data[10: 16] # 总消费
  104. totalElec = data[16: 22] # 总使用电量
  105. cardConsume = data[22: 28] # 卡消费
  106. return {
  107. "port": str(port),
  108. "totalConsume": int(totalConsume, 16) / 100.0,
  109. "totalElec": int(totalElec, 16) / 100.0,
  110. "cardConsume": int(cardConsume, 16) / 100.0
  111. }
  112. def _clean_consume_count(self, port, consume=False, elec=False, cardConsume=False):
  113. """
  114. 清除充电桩上累计信息 和端口有关
  115. :param consume:
  116. :param elec:
  117. :param cardConsume:
  118. :return:
  119. """
  120. if consume and elec and cardConsume:
  121. sendData = "E3"
  122. elif elec:
  123. sendData = "E2"
  124. elif cardConsume:
  125. sendData = "E4"
  126. elif consume:
  127. sendData = "E1"
  128. else:
  129. return
  130. sendData = "{}{}".format("{:02X}".format(port), sendData)
  131. self._send_data("A3", sendData)
  132. def _get_all_port_status(self):
  133. """ 获取全部端口状态 """
  134. result = self._send_data("A5", "00")
  135. data = result["data"][8:-8]
  136. statusMap = dict()
  137. for _port in range(1, 1 + len(data) / 2):
  138. status = DeviceParams.STATUS_MAP.get(data[:2], Const.DEV_WORK_STATUS_IDLE)
  139. statusMap[str(_port)] = status
  140. data = data[2:]
  141. return statusMap
  142. def _get_device_status(self, port):
  143. """
  144. 获取设备端口的运行状态
  145. :return:
  146. """
  147. result = self._send_data("A6", "{:02X}".format(port))
  148. data = result["data"]
  149. status = data[10:12]
  150. cardNo = data[32:40]
  151. # 如果设备状态
  152. result = {
  153. "port": str(port),
  154. "status": DeviceParams.STATUS_MAP.get(status, Const.DEV_WORK_STATUS_IDLE),
  155. "voltage": int(data[12:16], 16) / 1.0,
  156. "power": int(data[16:20], 16) / 1.0,
  157. "usedElec": int(data[20:24], 16) / 100.0,
  158. "usedTime": int(data[24:28], 16),
  159. "leftMoney": int(data[28:32], 16) / 100.0,
  160. "cardNo": KunYuanCar._trans_card(cardNo)
  161. }
  162. return result
  163. def _start(self, port, money):
  164. """
  165. 微信支付命令
  166. :param money:
  167. :return:
  168. """
  169. sendData = "{:02X}{:04X}0000".format(port, int(money * 100))
  170. result = self._send_data("A7", sendData, timeout=MQTT_TIMEOUT.START_DEVICE)
  171. data = result.get("data", dict())
  172. if data[10:14] != "4F4B":
  173. raise ServiceException({'result': 2, 'description': u'设备启动失败,请联系设备老板'})
  174. return result
  175. def _start_by_sequanceNo(self, port, money, sequanceNo):
  176. sendData = "{:02X}{:04X}{}".format(port, int(money * 100), sequanceNo)
  177. result = self._send_data("A7", sendData, timeout=MQTT_TIMEOUT.START_DEVICE)
  178. data = result.get("data", dict())
  179. if data[10:14] != "4F4B":
  180. raise ServiceException({'result': 2, 'description': u'设备启动失败,请联系设备老板'})
  181. return result
  182. def _set_device_disable(self, disable):
  183. """
  184. 设置 设备的可用
  185. :param disable:
  186. :return:
  187. """
  188. status = "E9" if disable else "E8"
  189. result = self._send_data("A9", status)
  190. data = result["data"]
  191. if data[8: 12] != "4F4B":
  192. raise ServiceException({'result': 2, 'description': u'设置失败,请重试'})
  193. def _stop(self, port):
  194. """ 停止 """
  195. self._send_data("AF", "{:02X}".format(port))
  196. def _restart_device(self):
  197. """ 重启 """
  198. self._send_data("B0", "00")
  199. def _response_ack(self, ackMessage):
  200. """
  201. 回复模块 使得模块停止ack 与主板无关
  202. """
  203. MessageSender.send(
  204. self.device,
  205. DeviceCmdCode.OPERATE_DEV_NO_RESPONSE,
  206. {
  207. "IMEI": self._device.devNo,
  208. "funCode": "FA",
  209. "data": "",
  210. "ack": ackMessage
  211. }
  212. )
  213. def _response_card(self, card):
  214. """ 回复卡的余额 状态 """
  215. sendData = "{:06X}".format(int(card.balance * 100))
  216. if card.frozen:
  217. sendData += "AA"
  218. else:
  219. sendData += "00"
  220. self._send_data("C1", sendData, cmd=DeviceCmdCode.OPERATE_DEV_NO_RESPONSE)
  221. @staticmethod
  222. def _parse_event_AE(data):
  223. """
  224. 枪把状态改变时候上报
  225. """
  226. port = int(data[8:10], 16)
  227. status = DeviceParams.CONNECT_STATUS_MAP.get(data[10:12], Const.DEV_WORK_STATUS_IDLE)
  228. return {
  229. "port": str(port),
  230. "status": status
  231. }
  232. @staticmethod
  233. def _parse_event_B1(data):
  234. """
  235. 充电启动
  236. """
  237. try:
  238. return KunYuanCar._parse_event_B1_by_sequanceNo(data)
  239. except ValueError:
  240. port = int(data[8: 10], 16)
  241. cardNo = data[10: 18]
  242. money = int(data[18: 22], 16) / 100.0
  243. return {
  244. "port": str(port),
  245. "cardNo": KunYuanCar._trans_card(cardNo),
  246. "money": money
  247. }
  248. @staticmethod
  249. def _parse_event_B1_by_sequanceNo(data):
  250. port = int(data[8: 10], 16)
  251. cardNo = data[10: 18]
  252. money = int(data[18: 22], 16) / 100.0
  253. sequanceNo = data[22: 36]
  254. return {
  255. "port": str(port),
  256. "cardNo": KunYuanCar._trans_card(cardNo),
  257. "money": money,
  258. "sequanceNo": sequanceNo
  259. }
  260. @staticmethod
  261. def _parse_event_A8(data):
  262. """
  263. 充电结束的时候上报
  264. """
  265. try:
  266. return KunYuanCar._parse_event_A8_by_sequanceNo(data)
  267. except ValueError:
  268. port = int(data[8: 10], 16)
  269. reasonCode = data[10:12]
  270. reason = DeviceParams.FINISH_REASON_MAP.get(reasonCode, u"未知结束方式")
  271. cardNo = data[12:20]
  272. usedElec = int(data[20:24], 16) / 100.0
  273. usedTime = int(data[24:28], 16)
  274. leftMoney = int(data[28:32], 16) / 100.0
  275. return {
  276. "port": str(port),
  277. "reason": reason,
  278. "cardNo": KunYuanCar._trans_card(cardNo),
  279. "usedElec": usedElec,
  280. "usedTime": usedTime,
  281. "leftMoney": leftMoney,
  282. }
  283. @staticmethod
  284. def _parse_event_A8_by_sequanceNo(data):
  285. port = int(data[8: 10], 16)
  286. reasonCode = data[10:12]
  287. reason = DeviceParams.FINISH_REASON_MAP.get(reasonCode, u"未知结束方式")
  288. cardNo = data[12:20]
  289. usedElec = int(data[20:24], 16) / 100.0
  290. usedTime = int(data[24:28], 16)
  291. leftMoney = int(data[28:32], 16) / 100.0
  292. sequanceNo = data[42: 56]
  293. return {
  294. "port": str(port),
  295. "reason": reason,
  296. "cardNo": KunYuanCar._trans_card(cardNo),
  297. "usedElec": usedElec,
  298. "usedTime": usedTime,
  299. "leftMoney": leftMoney,
  300. "sequanceNo": sequanceNo
  301. }
  302. @staticmethod
  303. def _parse_event_B2(data):
  304. """ 解析状态上报 """
  305. try:
  306. return KunYuanCar._parse_event_B2_by_sequanceNo(data)
  307. except ValueError:
  308. port = int(data[8: 10], 16)
  309. leftBalance = int(data[10:14], 16) / 100.0
  310. usedElec = int(data[14:18], 16) / 100.0
  311. temperature = int(data[20:22], 16)
  312. power = int(data[22:26], 16)
  313. voltage = int(data[30:34], 16)
  314. temperature = temperature if data[18:20] == "00" else -temperature
  315. return {
  316. "port": str(port),
  317. "leftBalance": leftBalance,
  318. "usedElec": usedElec,
  319. "temperature": u"{}度 更新于{}".format(temperature, datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")),
  320. "power": power,
  321. "voltage": voltage,
  322. }
  323. @staticmethod
  324. def _parse_event_B2_by_sequanceNo(data):
  325. port = int(data[8: 10], 16)
  326. leftBalance = int(data[10:14], 16) / 100.0
  327. usedElec = int(data[14:18], 16) / 100.0
  328. temperature = int(data[20:22], 16)
  329. power = int(data[22:26], 16)
  330. voltage = int(data[30:34], 16)
  331. temperature = temperature if data[18:20] == "00" else -temperature
  332. sequanceNo = data[20: 34]
  333. return {
  334. "port": str(port),
  335. "leftBalance": leftBalance,
  336. "usedElec": usedElec,
  337. "temperature": u"{}度 更新于{}".format(temperature, datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")),
  338. "power": power,
  339. "voltage": voltage,
  340. "sequanceNo": sequanceNo
  341. }
  342. @staticmethod
  343. def _parse_event_C1(data):
  344. """
  345. 在线卡查询卡费
  346. """
  347. cardNo = data[8: 16]
  348. return {
  349. "cardNo": KunYuanCar._trans_card(cardNo),
  350. }
  351. @staticmethod
  352. def _parse_event_C2(data):
  353. """
  354. 解析 同步参数指令
  355. """
  356. return {}
  357. @staticmethod
  358. def _parse_event_C3(data):
  359. """
  360. 解析 同步参数指令
  361. """
  362. port = int(data[8: 10], 16)
  363. faultCode = data[10: 14]
  364. return {
  365. "port": port,
  366. }
  367. @staticmethod
  368. def _parse_event_A0(data):
  369. if data[6: 8] == "0B":
  370. version = str(int(data[22: 26], 16) / 100.0)
  371. else:
  372. version = str(DEFAULT_VERSION)
  373. return {
  374. "version": version
  375. }
  376. @property
  377. def isHaveStopEvent(self):
  378. """ 是否有结束事件 """
  379. return True
  380. def test(self, coins):
  381. return self._start(1, coins)
  382. def get_dev_setting(self):
  383. """ 获取参数 """
  384. settings = self._get_device_settings()
  385. otherConf = self.device.get("otherConf", dict())
  386. disableDevice = otherConf.get("disableDevice", DeviceParams.DEFAULT_DISABLE_DEVICE)
  387. settings.update({"disableButton": disableDevice})
  388. Device.objects.filter(devNo=self.device.devNo).update(otherConf=otherConf)
  389. Device.invalid_device_cache(self.device.devNo)
  390. consume = self._get_consume_count()
  391. settings.update(consume)
  392. devCache = Device.get_dev_control_cache(self.device.devNo)
  393. temperature = devCache.get("temperature", "暂无数据")
  394. settings.update({"temperature": temperature})
  395. return settings
  396. def set_device_function_param(self, request, lastSetConf):
  397. """ 设置设备参数 """
  398. disable = request.POST.get("disableButton")
  399. if disable is not None:
  400. dealer = Dealer.objects.filter(id=self.device.ownerId).first()
  401. if "dealerDisableDevice" not in dealer.features:
  402. raise ServiceException({"result": 2, "description": "抱歉,您无此操作权限,请联系厂家获取权限"})
  403. return self.set_dev_disable(bool(int(disable)))
  404. newDict = {
  405. "is_free": request.POST.get("is_free", lastSetConf["is_free"]),
  406. "electricPrice": request.POST.get("electricPrice", lastSetConf["electricPrice"]),
  407. "maxConsume": request.POST.get("maxConsume", lastSetConf["maxConsume"]),
  408. "maxChargeTime": request.POST.get("maxChargeTime", lastSetConf["maxChargeTime"]),
  409. "volume": request.POST.get("volume", lastSetConf["volume"])
  410. }
  411. self._set_device_settings(newDict)
  412. def set_dev_disable(self, disable):
  413. """ 禁用设备 """
  414. self._set_device_disable(disable)
  415. def set_device_function(self, request, lastSetConf):
  416. """ 设置设备开关 """
  417. clearTotalConsume = request.POST.get("clearTotalConsume", False)
  418. clearTotalElec = request.POST.get("clearTotalElec", False)
  419. reboot = request.POST.get("reboot", False)
  420. if reboot:
  421. self._restart_device()
  422. # 清除设备计费信息
  423. if any([clearTotalConsume, clearTotalElec]):
  424. self._clean_consume_count(clearTotalConsume, clearTotalElec)
  425. def stop(self, port=None):
  426. """ 停止设备 """
  427. return self._stop(int(port))
  428. def get_port_status_from_dev(self):
  429. """从设备端更新端口的状态"""
  430. result = self._get_all_port_status()
  431. portDict = dict()
  432. for _port, _status in result.items():
  433. portDict[_port] = {"status": _status}
  434. allPorts, usedPorts, usePorts = self.get_port_static_info(portDict)
  435. portDict.update({"allPorts": allPorts, "usedPorts": usedPorts, "usePorts": usePorts})
  436. Device.update_dev_control_cache(self.device.devNo, portDict)
  437. return portDict
  438. def get_port_status(self, force=False):
  439. """
  440. 李成坤强制要求
  441. """
  442. # if force:
  443. portDict = self.get_port_status_from_dev()
  444. devSet = self._get_device_settings()
  445. statusMap = dict()
  446. for _port, _item in portDict.items():
  447. if isinstance(_port, (unicode, str)) and _port.isdigit():
  448. statusMap[_port] = {'status': _item.get("status", 0), "img": devSet.get(_port, "")}
  449. return statusMap
  450. def check_dev_status(self, attachParas=None):
  451. """ 检查设备状态 """
  452. chargeIndex = attachParas.get("chargeIndex")
  453. result = self.get_port_status(force=True)
  454. if result[str(chargeIndex)].get("status", 0) != Const.DEV_WORK_STATUS_CONNECTED:
  455. raise ServiceException({"result": 2, "description": u"请先连接设备与充电枪"})
  456. def analyze_event_data(self, data):
  457. """ 解析数据 """
  458. cmdCode = data[4:6]
  459. funcName = "_parse_event_{}".format(cmdCode)
  460. func = getattr(self.__class__, funcName, None)
  461. if func and callable(func):
  462. eventData = func(data) or dict()
  463. eventData.update({"cmdCode": cmdCode})
  464. return eventData
  465. def start_device(self, package, openId, attachParas):
  466. """ 启动设备 """
  467. chargeIndex = attachParas["chargeIndex"]
  468. portStatus = Device.get_port_control_cache(self.device.devNo, port=str(chargeIndex))
  469. if portStatus.get("status") != Const.DEV_WORK_STATUS_CONNECTED:
  470. self.get_port_status_from_dev()
  471. portStatus = Device.get_port_control_cache(self.device.devNo, port=str(chargeIndex))
  472. if portStatus.get("status") != Const.DEV_WORK_STATUS_CONNECTED:
  473. raise ServiceException({'result': 2, 'description': u'为了充电安全,请先连接设备与充电枪'})
  474. coins = package.get("coins")
  475. price = package.get("price")
  476. orderNo = attachParas.get("orderNo")
  477. version = self._get_device_settings().get("version", str(DEFAULT_VERSION))
  478. portCache = {
  479. "openId": openId,
  480. "isStart": True,
  481. "coins": coins,
  482. "price": price,
  483. "status": Const.DEV_WORK_STATUS_WORKING,
  484. "startTime": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
  485. "orderNo": orderNo,
  486. "version": version
  487. }
  488. if version > str(ORDER_VERSION):
  489. # sequanceNo 表示为时间戳 4个字节 + 端⼝号1个字节 + 随机数 2个字节 共7个字节16进制
  490. sequanceNo = "{:08X}{:02X}{:04X}".format(int(time.time()), int(chargeIndex), random.randint(1000, 9999))
  491. order = ConsumeRecord.objects.get(orderNo=orderNo)
  492. order.update(sequanceNo=sequanceNo)
  493. result = self._start_by_sequanceNo(int(chargeIndex), price, sequanceNo)
  494. else:
  495. result = self._start(int(chargeIndex), price)
  496. Device.update_dev_control_cache(self.device.devNo, {chargeIndex: portCache})
  497. result["finishedTime"] = int(time.time()) + 24 * 60 * 60
  498. return result
  499. def dealer_get_port_status(self):
  500. """ 经销商获取端口状态 """
  501. return self.get_port_status()
  502. def get_port_info(self, port):
  503. """ 获取端口信息 """
  504. result = self._get_device_status(int(port))
  505. Device.update_dev_control_cache(self.device.devNo, result)
  506. devCache = Device.get_dev_control_cache(self.device.devNo) or dict()
  507. return devCache
  508. def active_deactive_port(self, port, active):
  509. """ 停用激活端口 """
  510. if not active:
  511. self._stop(port)
  512. else:
  513. super(KunYuanCar, self).active_deactive_port(port, active)