dianchuanCFJ.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466
  1. # -*- coding: utf-8 -*-
  2. # !/usr/bin/env python
  3. import datetime
  4. import logging
  5. import time
  6. from decimal import Decimal
  7. import re
  8. from typing import TYPE_CHECKING
  9. from apilib.monetary import RMB
  10. from apps.web.constant import Const, DeviceCmdCode
  11. from apps.web.core.adapter.base import SmartBox
  12. from apps.web.core.exceptions import ServiceException
  13. from apps.web.core.networking import MessageSender
  14. from apps.web.device.models import Device
  15. logger = logging.getLogger(__name__)
  16. if TYPE_CHECKING:
  17. pass
  18. class CFJBox(SmartBox):
  19. def __init__(self, device):
  20. super(CFJBox, self).__init__(device)
  21. # 解析获取设备信息的返回报文
  22. def analyze_event_data(self, data):
  23. if not data:
  24. return
  25. data = str(data)
  26. if data[:2] != "EE":
  27. return
  28. cmd = data[4:6]
  29. if cmd == "05":
  30. """EE0C05 000000000000 01 0000 00 08"""
  31. port = self.decode_str(data[18:20])
  32. leftTime = self.decode_str(data[20:24])
  33. reasonCode = data[24:26]
  34. if reasonCode == '00':
  35. reason = u'购买的充电时间或者电量已经用完'
  36. elif reasonCode == '01':
  37. reason = u'系统判断为异常断电(插头被拔或者松动,或者电瓶已经充满),电瓶车充电器种类繁多,可能存在误差'
  38. elif reasonCode == '02':
  39. reason = u'电池已经充满'
  40. elif reasonCode == '03':
  41. reason = u'设备或者端口故障。建议您根据已经充电的时间评估是否需要到现场换到其他端口充电'
  42. elif reasonCode == '04':
  43. reason = u'警告!您的电池功率超过本机最大限制。为了公共安全,不建议您在该充电桩充电'
  44. elif reasonCode == '05':
  45. reason = u'刷卡退费结束'
  46. elif reasonCode == '06':
  47. reason = u'可能是插头被拔掉或者未连接充电器。如果不是自己操作,建议您到现场检查是否有人误操作'
  48. elif reasonCode == '07':
  49. reason = u'远程方式停止充电。如果不是自己操作,建议到现场尽快恢复充电'
  50. elif reasonCode == '08':
  51. reason = u'远烟雾报警停止'
  52. else:
  53. desc = u''
  54. result = locals()
  55. result.pop("self")
  56. result.pop("reasonCode")
  57. return result
  58. def _check_package(self, package):
  59. """
  60. 获取设备启动的发送数据 根据设备的当前模式以及套餐获取
  61. :param package:
  62. :return:
  63. """
  64. consumeModule = self.device.get("otherConf", dict()).get("consumeModule", 1)
  65. unit = package.get("unit", u"分钟")
  66. _time = float(package.get("time", 0))
  67. # 按时间计费
  68. if consumeModule == 1:
  69. billingType = "time"
  70. if unit == u"小时":
  71. _time = _time * 60
  72. elif unit == u"天":
  73. _time = _time * 24 * 60
  74. elif unit == u"秒":
  75. _time = _time / 60
  76. elif unit == u"分钟":
  77. _time = _time
  78. else:
  79. raise ServiceException({"result": 2, "description": u"套餐单位错误,请联系经销商"})
  80. # 按电量计费
  81. else:
  82. billingType = "elec"
  83. if unit != u"度":
  84. raise ServiceException({"result": 2, "description": u"套餐单位错误,请联系经销商"})
  85. else:
  86. _time = _time
  87. return _time, unit, billingType
  88. def disable_app_device(self, switch=True):
  89. # type:(bool) -> None
  90. otherConf = self.device.get("otherConf", {})
  91. otherConf["disableDevice"] = switch
  92. Device.objects.filter(devNo=self.device["devNo"]).update(otherConf=otherConf)
  93. Device.invalid_device_cache(self.device["devNo"])
  94. @staticmethod
  95. def encode_str(data, length=2, ratio=1.0, base=16):
  96. # type:(str,int,float,int) -> str
  97. if not isinstance(data, Decimal):
  98. data = Decimal(data)
  99. if not isinstance(length, str):
  100. length = str(length)
  101. if not isinstance(ratio, Decimal):
  102. ratio = Decimal(ratio)
  103. end = "X" if base == 16 else "d"
  104. encodeStr = "%." + length + end
  105. encodeStr = encodeStr % (data * ratio)
  106. return encodeStr
  107. @staticmethod
  108. def decode_str(data, ratio=1.0, base=16):
  109. # type:(str,float,int) -> str
  110. """
  111. ratio:比率单位转换
  112. """
  113. if not isinstance(data, str):
  114. data = str(data)
  115. return "%.10g" % (int(data, base) * ratio)
  116. @staticmethod
  117. def reverse_hex(data):
  118. # type:(str) -> str
  119. if not isinstance(data, str):
  120. raise TypeError
  121. return "".join(list(reversed(re.findall(r".{2}", data))))
  122. def decode_long_hex_to_list(self, data, split=2, ratio=1.0, base=16):
  123. # type:(str,int,float,int) -> list
  124. """
  125. return: list
  126. """
  127. if len(data) % split != 0:
  128. raise Exception("Invalid data")
  129. pattern = r".{%s}" % split
  130. hex_list = re.findall(pattern, data)
  131. hex_list = map(lambda x: self.decode_str(x, ratio=ratio, base=base), hex_list)
  132. return hex_list
  133. @staticmethod
  134. def check_params_range(params, minData=None, maxData=None, desc=""):
  135. # type:(str,float,float,str) -> str
  136. """
  137. 检查参数,返回字符串参数
  138. """
  139. if params is None:
  140. raise ServiceException({"result": 2, "description": u"参数错误."})
  141. if not isinstance(params, Decimal):
  142. params = Decimal(params)
  143. if not minData and maxData:
  144. if not isinstance(maxData, Decimal):
  145. maxData = Decimal(maxData)
  146. if params <= maxData:
  147. return "%g" % params
  148. else:
  149. raise ServiceException({"result": 2, "description": u"%s超出可选范围,可选最大值为%g" % (desc, maxData)})
  150. if not maxData and minData:
  151. if not isinstance(minData, Decimal):
  152. minData = Decimal(minData)
  153. if minData <= params:
  154. return "%g" % params
  155. else:
  156. raise ServiceException({"result": 2, "description": u"%s超出可选范围,可选最小值为%g" % (desc, minData)})
  157. if not minData and not maxData:
  158. return "%g" % params
  159. else:
  160. if not isinstance(minData, Decimal):
  161. minData = Decimal(minData)
  162. if not isinstance(maxData, Decimal):
  163. maxData = Decimal(maxData)
  164. if minData <= params <= maxData:
  165. return "%g" % params
  166. else:
  167. raise ServiceException(
  168. {"result": 2, "description": u"%s参数超出可选范围,可取范围为%g-%g" % (desc, minData, maxData)})
  169. def send_mqtt(self, funCode, data, cmd=DeviceCmdCode.OPERATE_DEV_SYNC):
  170. """
  171. 发送mqtt 指令210 返回data
  172. """
  173. if not isinstance(funCode, str):
  174. funCode = str(funCode)
  175. if not isinstance(data, str):
  176. data = str(data)
  177. result = MessageSender.send(self.device, cmd,
  178. {"IMEI": self.device["devNo"], "funCode": funCode, "data": data})
  179. if "rst" in result and result["rst"] != 0:
  180. if result["rst"] == -1:
  181. raise ServiceException(
  182. {"result": 2, "description": u"该设备正在玩命找网络,请您稍候再试", "rst": -1})
  183. elif result["rst"] == 1:
  184. raise ServiceException(
  185. {"result": 2, "description": u"该设备忙,无响应,请您稍候再试。也可能是您的设备版本过低,暂时不支持此功能", "rst": 1})
  186. else:
  187. if cmd in [DeviceCmdCode.OPERATE_DEV_NO_RESPONSE, DeviceCmdCode.PASSTHROUGH_OPERATE_DEV_NO_RESPONSE]:
  188. return
  189. if result.get("data") == "00":
  190. raise ServiceException({"result": 2, "description": u"设备操作失败.请重试"})
  191. else:
  192. return str(result["data"][18:-2])
  193. @staticmethod
  194. def port_is_busy(port_dict):
  195. if not port_dict:
  196. return False
  197. if "billingType" not in port_dict:
  198. return False
  199. if "status" not in port_dict:
  200. return False
  201. if "coins" not in port_dict:
  202. return False
  203. if port_dict["billingType"] not in ["time", "elec"]:
  204. return False
  205. if port_dict["billingType"] == "time":
  206. if "needTime" not in port_dict:
  207. return False
  208. else:
  209. if "needElec" not in port_dict:
  210. return False
  211. if port_dict["status"] == Const.DEV_WORK_STATUS_WORKING:
  212. return True
  213. else:
  214. return False
  215. def do_update_configs(self, updateDict):
  216. dev = Device.objects.get(devNo=self.device.devNo)
  217. deviceConfigs = dev.otherConf.get("deviceConfigs", {})
  218. deviceConfigs.update(updateDict)
  219. dev.otherConf['deviceConfigs'] = deviceConfigs
  220. dev.save()
  221. Device.invalid_device_cache(self.device.devNo)
  222. def start_device(self, package, openId, attachParas):
  223. # type: (...)-> dict
  224. if attachParas is None:
  225. raise ServiceException({"result": 2, "description": u"请您选择合适的充电线路、电池类型信息"})
  226. if "chargeIndex" not in attachParas:
  227. raise ServiceException({"result": 2, "description": u"请您选择合适的充电线路"})
  228. dev = Device.objects.get(devNo=self.device["devNo"])
  229. refundProtection = dev.otherConf.get("refundProtection", 0)
  230. refundProtectionTime = dev.otherConf.get("refundProtectionTime", 0)
  231. port = attachParas["chargeIndex"]
  232. portHex = self.encode_str(port, 2)
  233. _time, unit, billingType = self._check_package(package)
  234. coins = float(package.get("coins", 0))
  235. coinsHex = self.encode_str(coins, length=4, ratio=10, base=10)
  236. unitHex = self.encode_str(_time, length=4)
  237. unit = package["unit"]
  238. if unit == "度":
  239. unitHex = self.encode_str(_time, length=4, ratio=100)
  240. orderNo = attachParas.get("orderNo")
  241. data = portHex + coinsHex + unitHex
  242. devInfo = MessageSender.send(
  243. device=self.device,
  244. cmd=DeviceCmdCode.OPERATE_DEV_SYNC,
  245. payload={
  246. "IMEI": self.device["devNo"],
  247. "funCode": "02",
  248. "data": data
  249. },
  250. timeout=120
  251. )
  252. if "rst" in devInfo and devInfo["rst"] != 0:
  253. if devInfo["rst"] == -1:
  254. raise ServiceException(
  255. {"result": 2, "description": u"充电桩正在玩命找网络,您的金币还在,重试不需要重新付款,建议您试试旁边其他设备,或者稍后再试哦"})
  256. elif devInfo["rst"] == 1:
  257. self.check_serial_port_for_startcmd(attachParas["chargeIndex"])
  258. # 收到回应报文后,需要ack响应报文。
  259. data = devInfo["data"][18::]
  260. usePort = int(attachParas["chargeIndex"])
  261. result = data[2:4]
  262. if result == "01": # 成功
  263. pass
  264. elif result == "02":
  265. newValue = {str(usePort): {"status": Const.DEV_WORK_STATUS_FAULT, "statusInfo": u"充电站故障"}}
  266. Device.update_dev_control_cache(self.device["devNo"], newValue)
  267. raise ServiceException({"result": 2, "description": u"充电站故障"})
  268. elif result == "03":
  269. newValue = {str(usePort): {"status": Const.DEV_WORK_STATUS_WORKING, "statusInfo": u""}}
  270. Device.update_dev_control_cache(self.device["devNo"], newValue)
  271. raise ServiceException({"result": 2, "description": u"该端口正在使用中"})
  272. portDict = {
  273. "startTime": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
  274. "status": Const.DEV_WORK_STATUS_WORKING,
  275. "billingType": billingType,
  276. "isStart": True,
  277. "openId": openId,
  278. "refunded": False,
  279. "vCardId": self._vcard_id,
  280. "refundProtection": refundProtection,
  281. "refundProtectionTime": refundProtectionTime,
  282. "doPowerLevel": False,
  283. "consumeType": "mobile"
  284. }
  285. ctrInfo = Device.get_dev_control_cache(self.device["devNo"])
  286. lastPortInfo = ctrInfo.get(str(usePort), {})
  287. if billingType == "time":
  288. if self.port_is_busy(lastPortInfo) and lastPortInfo["billingType"] == "time":
  289. portDict["coins"] = float(coins) + lastPortInfo["coins"]
  290. portDict["needTime"] = _time + lastPortInfo["needTime"]
  291. else:
  292. portDict["coins"] = float(coins)
  293. portDict.update({"needTime": _time})
  294. finishedTime = int(time.time()) + int(portDict["needTime"] * 60)
  295. else:
  296. if self.port_is_busy(lastPortInfo) and lastPortInfo["billingType"] == "elec":
  297. portDict["coins"] = float(coins) + lastPortInfo["coins"]
  298. portDict["needElec"] = _time + lastPortInfo["needElec"]
  299. else:
  300. portDict["coins"] = float(coins)
  301. portDict.update({"needElec": _time})
  302. finishedTime = int(time.time()) + 60 * 60 * 12
  303. portDict["coins"] = str(RMB(portDict["coins"]))
  304. portDict.update({"finishedTime": finishedTime})
  305. if "orderNo" in attachParas:
  306. portDict.update({"orderNo": attachParas["orderNo"]})
  307. Device.update_dev_control_cache(
  308. self.device["devNo"],
  309. {
  310. str(usePort): portDict
  311. })
  312. devInfo["finishedTime"] = finishedTime
  313. return devInfo
  314. def get_default_port_nums(self):
  315. default_num = 1
  316. return default_num
  317. def get_port_status(self, force=False):
  318. if force:
  319. return self.get_port_status_from_dev()
  320. ctrInfo = Device.get_dev_control_cache(self.device.devNo)
  321. if "allPorts" in ctrInfo and ctrInfo["allPorts"] > 0:
  322. allPorts = ctrInfo["allPorts"]
  323. else:
  324. allPorts = self.get_default_port_nums()
  325. statusDict = {}
  326. for ii in range(allPorts):
  327. tempDict = ctrInfo.get(str(ii + 1), {})
  328. if "status" in tempDict:
  329. statusDict[str(ii + 1)] = {"status": tempDict.get("status")}
  330. elif "isStart" in tempDict:
  331. if tempDict["isStart"]:
  332. statusDict[str(ii + 1)] = {"status": Const.DEV_WORK_STATUS_WORKING}
  333. else:
  334. statusDict[str(ii + 1)] = {"status": Const.DEV_WORK_STATUS_IDLE}
  335. else:
  336. statusDict[str(ii + 1)] = {"status": Const.DEV_WORK_STATUS_IDLE}
  337. return statusDict
  338. def get_port_info(self, port):
  339. # type:(str) -> dict
  340. """
  341. 获取单个端口状态
  342. funcCode :
  343. 发送:
  344. 返回:
  345. """
  346. pass
  347. def get_port_status_from_dev(self):
  348. """
  349. funCode : 01
  350. """
  351. data = self.send_mqtt(funCode="01",data="00")
  352. portNum = int(data[:2], base=16)
  353. portData = data[2:]
  354. result = {}
  355. ii = 0
  356. while ii < portNum:
  357. statusTemp = portData[ii * 2:ii * 2 + 2]
  358. if statusTemp == '01':
  359. status = {'status': Const.DEV_WORK_STATUS_IDLE}
  360. elif statusTemp == '02':
  361. status = {'status': Const.DEV_WORK_STATUS_WORKING}
  362. elif statusTemp == '03':
  363. status = {'status': Const.DEV_WORK_STATUS_FORBIDDEN}
  364. elif statusTemp == '04':
  365. status = {'status': Const.DEV_WORK_STATUS_FAULT}
  366. # 不再上述状态之列的 统一为故障状态
  367. else:
  368. status = {'status': Const.DEV_WORK_STATUS_FAULT}
  369. ii += 1
  370. result[str(ii)] = status
  371. allPorts, usedPorts, usePorts = self.get_port_static_info(result)
  372. ctrInfo = Device.get_dev_control_cache(self._device['devNo'])
  373. for strPort, info in result.items():
  374. if ctrInfo.has_key(strPort):
  375. ctrInfo[strPort].update({'status': info['status']})
  376. else:
  377. ctrInfo[strPort] = info
  378. ctrInfo.update({
  379. 'allPorts': allPorts, 'usedPorts': usedPorts, 'usePorts': usePorts
  380. })
  381. Device.update_dev_control_cache(self.device.devNo, ctrInfo)
  382. return result
  383. @property
  384. def isHaveStopEvent(self):
  385. return True
  386. def test(self, port="1", time="1"):
  387. # type:(...) -> dict
  388. """
  389. 启动某个端口
  390. funcCode : 02
  391. """
  392. portHex = self.encode_str(port, 2)
  393. timeHex = self.encode_str(time,length=4)
  394. data = portHex + "0000" + timeHex
  395. result = self.send_mqtt(funCode="02", data=data)
  396. if result:
  397. port = result[:2]
  398. status = "Charging success" if result[2:4] == "01" else "Charging failed"
  399. return {"port": port, "status": status}