ywt_huandiangui.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546
  1. # -*- coding: utf-8 -*-
  2. # !/usr/bin/env python
  3. import datetime
  4. import logging
  5. import re
  6. from mongoengine import DoesNotExist
  7. from typing import TYPE_CHECKING
  8. from apps.web.constant import Const
  9. from apps.web.core.exceptions import ServiceException
  10. from apps.web.core.utils import delay_async_operation
  11. from apps.web.device.models import Group, Device, Battery
  12. from apps.web.eventer import EventBuilder
  13. from apps.web.eventer.base import WorkEvent
  14. from apps.web.user.models import ServiceProgress, ConsumeRecord, MyUser
  15. logger = logging.getLogger(__name__)
  16. if TYPE_CHECKING:
  17. from apps.web.core.adapter.ywt_huandiangui import ChargeCabinet
  18. class builder(EventBuilder):
  19. def __getEvent__(self, device_event):
  20. return HuanDianGui(self.deviceAdapter, device_event)
  21. class HuanDianGui(WorkEvent):
  22. def __init__(self, smartBox, event_data):
  23. super(HuanDianGui, self).__init__(smartBox, event_data)
  24. self.deviceAdapter = smartBox # type:ChargeCabinet
  25. # 登记设备
  26. def register_dev(self):
  27. server, port, proto = self.device.network_address
  28. devObj = Device.objects.get(devNo=self.device['devNo'])
  29. # 获取sim卡,卡号
  30. msgInfo = self.get_msg_info()
  31. devObj.iccid = msgInfo['iccid']
  32. devObj.softVer = msgInfo['driverVersion']
  33. devObj.driverCode = Const.DEVICE_TYPE_CODE_YWT_HUANDIANGUI # 驱动必须赋值
  34. devObj.save()
  35. Device.invalid_device_cache(self.device['devNo'])
  36. Device.update_online_cache(self.device['devNo'], True, 32) # 设备没有返回信号量,所以就用32
  37. def handle_heartbeat(self):
  38. data = self.event_data.get('data', '')
  39. logicalCode = data[14:30]
  40. temperature = int(data[30:32], 16)
  41. water_sensor = int(data[32:34], 16)
  42. smoke_sensor = int(data[34:36], 16)
  43. Device.update_dev_control_cache(self.device.devNo, {
  44. 'temperature': temperature,
  45. 'water_sensor': water_sensor,
  46. 'smoke_sensor': smoke_sensor,
  47. })
  48. Device.update_online_cache(self.device['devNo'], True, 32) # 设备没有返回信号量,所以就用32
  49. # 刷新端口详细信息
  50. def update_order_info(self):
  51. pass
  52. def do(self, **args):
  53. data = self.event_data['data']
  54. funCode = data[12:14]
  55. if funCode == '80': # 注册设备
  56. logger.info('heartbaet...')
  57. self.handle_heartbeat()
  58. elif funCode == '81': # 注册设备
  59. self.register_dev()
  60. elif funCode == '85':
  61. logger.info('door status change')
  62. self.hand_door_change()
  63. def get_msg_info(self):
  64. pass
  65. data = self.event_data.get('data')
  66. result = {}
  67. # devNo = data[14:30]
  68. driverVersion = data[30:34]
  69. iccid = data[34:74]
  70. result['iccid'] = self.fromHex(iccid)
  71. result['driverVersion'] = driverVersion
  72. return result
  73. def fromHex(self, send_str):
  74. return reduce(lambda a, b: a + b, map(lambda _: chr(int(_, 16)), re.findall('..', send_str)))
  75. def hand_door_change(self):
  76. data = self.event_data.get('data')
  77. addr = int(data[10:12], 16) # 实际上是addr
  78. state_of_lock = int(data[16:18], 16) == 1 and 'lock' or 'open'
  79. self.event_data['addr'] = format(addr, 'd')
  80. self.event_data['state_of_lock'] = state_of_lock
  81. battery_info = {}
  82. try:
  83. # 检查电池是否已经放入了
  84. battery_info = self.deviceAdapter._battery_info(addr)
  85. except:
  86. pass
  87. self.event_data['battery_id'] = battery_info.get("battery_id")
  88. if state_of_lock == 'open': # 开 门
  89. self.hand_opened()
  90. elif state_of_lock == 'lock': # 关门
  91. self.hand_closed()
  92. def hand_closed(self):
  93. """
  94. 处理柜门被关闭的事件
  95. 完整的换电记录也会有两次关门事件上报
  96. 第一次上报的关门 是客户放入电池的关门 此次事件相对重要 需要反复校验电池是否存在
  97. 第二次上报的关门 是作为服务结束的标志
  98. 从事件中标志第一次还是第二次关门的有两种条件 第一个是记录是否完整 第二个是是否有电池
  99. :return:
  100. """
  101. devCache = Device.get_dev_control_cache(self.device.devNo)
  102. currentUseInfo = devCache.get("currentUseInfo", dict())
  103. addr = self.event_data.get("addr")
  104. battery_id = self.event_data.get("battery_id")
  105. # 开启放入电池的电池编号
  106. try:
  107. self.deviceAdapter._start_charging(addr)
  108. except Exception as e:
  109. pass
  110. # 首先检验 currentUseInfo 中的订单信息,如果没有订单信息 则是无效信息 这个地方直接将orderNo弹出 避免再次检测到有效事件
  111. orderNo = currentUseInfo.get("orderNo", "")
  112. if not orderNo:
  113. return
  114. try:
  115. ConsumeRecord.objects.get(orderNo=orderNo)
  116. except DoesNotExist:
  117. logger.error("orderNo does not exists, orderNo is <{}>".format(orderNo))
  118. # 没有订单的关门事件 不做任何处理 保留一切信息
  119. return
  120. # 首先检验当前使用信息中是否有 chargeIndex2 如果说已经存在chargeIndex2的情况下 那就一定是第二次上报的关门
  121. chargeIndex1 = currentUseInfo.get("chargeIndex1")
  122. chargeIndex2 = currentUseInfo.get("chargeIndex2")
  123. # 不应该出现的情况
  124. if not chargeIndex1 and not chargeIndex2:
  125. # 将下一个用户的扫码开启端口设置为None 下次扫码的时候再次获取
  126. currentUseInfo.pop("orderNo", None)
  127. Device.clear_port_control_cache(self.device.devNo, "currentUseInfo")
  128. Device.update_dev_control_cache(self.device.devNo, {"currentUseInfo": currentUseInfo})
  129. logger.error("currentUseInfo chargeIndex info missing, curInfo is {}".format(currentUseInfo))
  130. return
  131. # 第一次的关门事件
  132. # 2020-9-27 安琦新增需求 用户放入电池不是上一次取走的电池 直接给经销商告警
  133. elif chargeIndex1 and not chargeIndex2:
  134. # 上报的信息不是本订单的信息,直接忽略
  135. if addr != chargeIndex1:
  136. self.delay_clear_current_info()
  137. return
  138. logger.info("<{}> order first closed".format(orderNo))
  139. # 再次获取之后还是没有找到电池的SN说明电池没有被放入, 这个地方不应该在进行下一步的操作,直接将此次的行为关闭掉
  140. if not battery_id:
  141. consumeDict = {
  142. "chargeIndex2": "-1",
  143. "oldBatteryImei": "-1",
  144. "newBatteryImei": "-1",
  145. "chargeIndex1": chargeIndex1
  146. }
  147. ServiceProgress.update_progress_and_consume_rcd(
  148. self.device["ownerId"],
  149. {
  150. "open_id": currentUseInfo.get("openId"),
  151. "device_imei": self.device["devNo"],
  152. "isFinished": False
  153. },
  154. consumeDict
  155. )
  156. # 通知用户服务结束 不清除使用信息 需要经销商介入
  157. consumeDict.update({"openId": currentUseInfo.get("openId")})
  158. self.notify_to_user(consumeDict)
  159. currentUseInfo.pop("orderNo", None)
  160. Device.clear_port_control_cache(self.device.devNo, "currentUseInfo")
  161. Device.update_dev_control_cache(self.device.devNo, {"currentUseInfo": currentUseInfo})
  162. # 由于用户没有放入电池 这个地方下一个开启的, nextPort不变 延时后清除正在使用的信息
  163. self.delay_clear_current_info()
  164. return
  165. # 获取到了相应的电池的sn,继续下一步服务 进行柜门的开启, 开启的柜门在用户开启空门的时候已经确定下来了,在currentUseInfo字段里
  166. else:
  167. # 获取上一次的消费记录里面的电池编号
  168. openId = currentUseInfo.get("openId", "")
  169. last_battery_id = Battery.get_user_last_battery_sn(openId, self.device.ownerId)
  170. if last_battery_id and last_battery_id != battery_id:
  171. self.warning_to_dealer_diff_battery(battery_id, last_battery_id, openId)
  172. battery = Battery.get_one(self.device.ownerId, battery_id)
  173. if battery and battery.disable:
  174. consumeDict = {
  175. "chargeIndex2": "-1",
  176. "oldBatteryImei": battery_id,
  177. "newBatteryImei": "-1",
  178. "chargeIndex1": chargeIndex1
  179. }
  180. ServiceProgress.update_progress_and_consume_rcd(
  181. self.device["ownerId"],
  182. {
  183. "open_id": currentUseInfo.get("openId"),
  184. "device_imei": self.device["devNo"],
  185. "isFinished": False
  186. },
  187. consumeDict
  188. )
  189. # 通知用户服务结束 不清除使用信息 需要经销商介入
  190. consumeDict.update({"openId": currentUseInfo.get("openId")})
  191. self.notify_to_user(consumeDict)
  192. currentUseInfo.pop("orderNo", None)
  193. Device.clear_port_control_cache(self.device.devNo, "currentUseInfo")
  194. Device.update_dev_control_cache(self.device.devNo, {"currentUseInfo": currentUseInfo})
  195. # 将下一个空仓 置空
  196. self.update_next_port(None)
  197. self.delay_clear_current_info()
  198. # TODO 告知经销商 警告电池已经被锁定
  199. logger.info("disable battery <{}> has been locked!".format(battery.batterySn))
  200. self.warning_to_dealer_disable_battery(battery_id, openId)
  201. return
  202. chargeIndex2 = currentUseInfo.pop("toOpenChargeIndex")
  203. battery_id2 = currentUseInfo.pop("toOpenBatteryId")
  204. try:
  205. self.deviceAdapter._open_door(chargeIndex2)
  206. except ServiceException:
  207. # 有可能出现的是模块和设备交互失败导致开门失败 或者网络通信不好,这个时候直接通知用户联系经销商处理
  208. consumeDict = {
  209. "chargeIndex2": "-1",
  210. "oldBatteryImei": battery_id,
  211. "newBatteryImei": "-1",
  212. "chargeIndex1": chargeIndex1
  213. }
  214. ServiceProgress.update_progress_and_consume_rcd(
  215. self.device["ownerId"],
  216. {
  217. "open_id": currentUseInfo.get("openId"),
  218. "device_imei": self.device["devNo"],
  219. "isFinished": False
  220. },
  221. consumeDict
  222. )
  223. # 通知用户服务结束 但不清除用户的使用信息 此时一定需要经销商介入处理
  224. consumeDict.update({"openId": currentUseInfo.get("openId")})
  225. self.notify_to_user(consumeDict)
  226. currentUseInfo.pop("orderNo", None)
  227. Device.clear_port_control_cache(self.device.devNo, "currentUseInfo")
  228. Device.update_dev_control_cache(self.device.devNo, {"currentUseInfo": currentUseInfo})
  229. self.delay_clear_current_info()
  230. # 将下一个空仓 置空
  231. self.update_next_port(None)
  232. return
  233. currentUseInfo.update({
  234. "chargeIndex2": chargeIndex2,
  235. "oldBatteryImei": battery_id,
  236. "newBatteryImei": battery_id2
  237. })
  238. Device.update_dev_control_cache(self.device.devNo, {"currentUseInfo": currentUseInfo})
  239. self.update_next_port(chargeIndex2)
  240. # 这个地方更新电池的信息
  241. logger.error("the first battery id is {}".format(battery_id))
  242. battery = Battery.get_one(self.device.ownerId, battery_id)
  243. if battery:
  244. battery.update_dev_info(self.device.devNo, addr)
  245. return
  246. # 不应该出现的情况
  247. elif not chargeIndex1 and chargeIndex2:
  248. currentUseInfo.pop("orderNo", None)
  249. Device.clear_port_control_cache(self.device.devNo, "currentUseInfo")
  250. Device.update_dev_control_cache(self.device.devNo, {"currentUseInfo": currentUseInfo})
  251. logger.error("chargeIndex1 missing but chargeIndex2 exists, curInfo is {}".format(currentUseInfo))
  252. return
  253. # 第二次的关门事件
  254. else:
  255. # 上报的信息不是本订单的信息,直接忽略
  256. if addr != chargeIndex2:
  257. return
  258. # 第二次关门的时候也需要检测里面是否有电池信息,是为了nextPort 防止nextPort开启有电池的门, 但是不再需要反复检测
  259. consumeDict = {
  260. "chargeIndex2": currentUseInfo.get("chargeIndex2"),
  261. "oldBatteryImei": currentUseInfo.get("oldBatteryImei"),
  262. "newBatteryImei": currentUseInfo.get("newBatteryImei"),
  263. "chargeIndex1": currentUseInfo.get("chargeIndex1")
  264. }
  265. ServiceProgress.update_progress_and_consume_rcd(
  266. self.device["ownerId"],
  267. {
  268. "open_id": currentUseInfo.get("openId"),
  269. "device_imei": self.device["devNo"],
  270. "isFinished": False
  271. },
  272. consumeDict
  273. )
  274. consumeDict.update({"openId": currentUseInfo.get("openId")})
  275. self.notify_to_user(consumeDict)
  276. # 如果用户有放入电池 这个地方就是正常的结束 清空使用信息,下一个用户继续使用 否则不清空,等待经销商介入
  277. if not battery_id:
  278. Device.clear_port_control_cache(self.device.devNo, "currentUseInfo")
  279. # 将电池信息更新
  280. battery = Battery.get_one(dealerId=self.device.ownerId, batterySn=currentUseInfo.get("newBatteryImei"))
  281. if battery:
  282. battery.update_user_info(currentUseInfo.get("openId"))
  283. else:
  284. # 保留一段时间的信息
  285. currentUseInfo.pop("orderNo", None)
  286. Device.clear_port_control_cache(self.device.devNo, "currentUseInfo")
  287. Device.update_dev_control_cache(self.device.devNo, {"currentUseInfo": currentUseInfo})
  288. self.delay_clear_current_info()
  289. # 电池还在柜子里面
  290. battery = Battery.get_one(dealerId=self.device.ownerId, batterySn=battery_id)
  291. if battery:
  292. battery.update_dev_info(self.device.devNo, addr)
  293. self.update_next_port(addr)
  294. # 更新下一个开门的端口
  295. logger.info("<{}> order second closed".format(orderNo))
  296. def hand_opened(self):
  297. """
  298. 通过非指令手段开门,锁控板才会上传开门指令
  299. :return:
  300. """
  301. portStr = self.event_data.get("portStr")
  302. batterySn = self.event_data.get("batteryImei", "")
  303. self.warning_to_dealer(portStr, batterySn)
  304. # 出现这种非法开门之后,直接更新一个无效的currentUseInfo 将设备锁死
  305. # currentUseInfo = {"openId": "invalid user"}
  306. # if batterySn:
  307. # currentUseInfo.update({"chargeIndex2": portStr, "newBatteryImei": batterySn})
  308. # else:
  309. # currentUseInfo.update({"chargeIndex1": portStr})
  310. # Device.clear_port_control_cache(self._devNo, "currentUseInfo")
  311. # Device.update_dev_control_cache(self._devNo, {"currentUseInfo": currentUseInfo})
  312. return
  313. def delay_clear_current_info(self):
  314. """
  315. 延时清除客户的使用信息,一般发生在换电错误的时候去处理
  316. :return:
  317. """
  318. seconds = int(self.device.get("otherConf", dict()).get("delayTime", 1)) * 60
  319. delay_async_operation(HuanDianGui.delay_func, seconds, self.device.devNo)
  320. @staticmethod
  321. def delay_func(devNo):
  322. """
  323. 延时函数
  324. :param devNo:
  325. :return:
  326. """
  327. Device.clear_port_control_cache(devNo, "currentUseInfo")
  328. def notify_to_user(self, currentUseInfo):
  329. """
  330. 根据用户的换电行为提醒用户
  331. :param currentUseInfo:
  332. :return:
  333. """
  334. openId = currentUseInfo.get("openId")
  335. if not openId:
  336. return
  337. groupId = self.device.get("groupId")
  338. group = Group.get_group(groupId)
  339. user = MyUser.objects.filter(openId=openId, groupId=groupId).first()
  340. if not group or not user:
  341. return
  342. chargeIndex1 = currentUseInfo.get("chargeIndex1")
  343. chargeIndex2 = currentUseInfo.get("chargeIndex2")
  344. batterySn1 = currentUseInfo.get("oldBatteryImei")
  345. batterySn2 = currentUseInfo.get("newBatteryImei")
  346. numChecker = lambda x: False if x == "-1" else True
  347. # 整个流程走完了
  348. if numChecker(chargeIndex1) and numChecker(chargeIndex2) and numChecker(batterySn1) and numChecker(batterySn2):
  349. title = u"\\n\\n设备编号:\\t\\t{logicalCode}\\n\\n服务地址:\\t\\t{group}\\n\\n放入:\\t\\t{p1}端口-{oldB}电池\\n\\n取出:\\t\\t{p2}端口-{newB}电池".format(
  350. logicalCode=self.device.get("logicalCode", ""),
  351. group=group.get("groupName", ""),
  352. p1=chargeIndex1,
  353. oldB=batterySn1,
  354. p2=chargeIndex2,
  355. newB=batterySn2,
  356. )
  357. # 放入了电池但是开启有电池门的时候故障了
  358. elif numChecker(chargeIndex1) and not numChecker(chargeIndex2) and numChecker(batterySn1) and not numChecker(
  359. batterySn2):
  360. title = u"\\n\\n设备编号:\\t\\t{logicalCode}\\n\\n服务地址:\\t\\t{group}\\n\\n放入:\\t\\t{p1}端口-{oldB}电池".format(
  361. logicalCode=self.device.get("logicalCode", ""),
  362. group=group.get("groupName", ""),
  363. p1=chargeIndex1,
  364. oldB=batterySn1,
  365. )
  366. # 空柜门没有放入电池
  367. elif numChecker(chargeIndex1) and not numChecker(chargeIndex2) and not numChecker(
  368. batterySn1) and not numChecker(batterySn2):
  369. title = u"\\n\\n设备编号:\\t\\t{logicalCode}\\n\\n服务地址:\\t\\t{group}\\n\\n放入:\\t\\t{p1}端口-未放入电池".format(
  370. logicalCode=self.device.get("logicalCode", ""),
  371. group=group.get("groupName", ""),
  372. p1=chargeIndex1,
  373. )
  374. # 非法的情况
  375. else:
  376. logger.error("invalid notify to user, curInfo is <{}>".format(currentUseInfo))
  377. return
  378. self.notify_user(
  379. managerialOpenId=user.managerialOpenId if user else "",
  380. templateName="service_complete",
  381. title=title,
  382. service=u"换电柜服务",
  383. remark=u'谢谢您的支持',
  384. finishTime=datetime.datetime.strftime(datetime.datetime.now(), "%Y-%m-%d %H:%M:%S")
  385. )
  386. def update_next_port(self, nextPort):
  387. """
  388. 更新下一个开启的端口
  389. :param nextPort:
  390. :return:
  391. """
  392. otherConf = self.device.get("otherConf", dict())
  393. otherConf.update({"nextPort": nextPort})
  394. Device.objects.filter(devNo=self._devNo).update(otherConf=otherConf)
  395. Device.invalid_device_cache(self._devNo)
  396. def warning_to_dealer(self, portStr, batterySn):
  397. """
  398. 向经销商告警 有非法柜门打开事件
  399. :return:
  400. """
  401. group = Group.get_group(self.device.get("groupId", ""))
  402. if batterySn:
  403. fault = u"{} 号柜门 ,内有电池, 电池编号为:{}".format(portStr, batterySn)
  404. else:
  405. fault = u"{} 号柜门, 内无电池".format(portStr)
  406. self.notify_dealer(
  407. templateName = "device_fault",
  408. title = "您的设备柜门被手动打开,如非设备人员正常检修,建议您去现场看看",
  409. device = u"{groupNumber}组-{logicalCode}".format(groupNumber=self.device["groupNumber"],
  410. logicalCode=self.device["logicalCode"]),
  411. location = u"{address}-{groupName}".format(address = group["address"], groupName = group["groupName"]),
  412. fault = fault,
  413. notifyTime = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  414. )
  415. def warning_to_dealer_diff_battery(self, batterySn, lastBatterySn, openId):
  416. """
  417. 经销告警 新旧电池不一样
  418. :param batterySn:
  419. :param lastBatterySn:
  420. :param openId:
  421. :return:
  422. """
  423. user = MyUser.objects.filter(openId=openId, groupId=self.device.groupId).first()
  424. activeInfo = MyUser.get_active_info(openId, agentId=user.agentId)
  425. phoneNumber = activeInfo.get("phoneNumber")
  426. group = Group.get_group(self.device.get("groupId", ""))
  427. fault = "当前放入电池编号 {}, 上次取出电池 {}, 用户 {} 联系方式 {}".format(batterySn, lastBatterySn, user.nickname, phoneNumber)
  428. self.notify_dealer(
  429. templateName="device_fault",
  430. title="用户电池编号出现异常",
  431. device=u"{groupNumber}组-{logicalCode}".format(groupNumber=self.device["groupNumber"],
  432. logicalCode=self.device["logicalCode"]),
  433. location=u"{address}-{groupName}".format(address=group["address"], groupName=group["groupName"]),
  434. fault=fault,
  435. notifyTime=datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  436. )
  437. def warning_to_dealer_disable_battery(self, batterySn, openId):
  438. """
  439. 通知经销商 被锁定的电池已经入柜子 请及时的解锁
  440. :param batterySn:
  441. :param openId:
  442. :return:
  443. """
  444. user = MyUser.objects.filter(openId=openId, groupId=self.device.groupId).first()
  445. activeInfo = MyUser.get_active_info(openId, agentId=user.agentId)
  446. phoneNumber = activeInfo.get("phoneNumber")
  447. group = Group.get_group(self.device.get("groupId", ""))
  448. fault = "当前放入电池编号 {}, 用户 {} 联系方式 {}".format(batterySn, user.nickname, phoneNumber)
  449. self.notify_dealer(
  450. templateName="device_fault",
  451. title="禁用电池已被锁定",
  452. device=u"{groupNumber}组-{logicalCode}".format(groupNumber=self.device["groupNumber"],
  453. logicalCode=self.device["logicalCode"]),
  454. location=u"{address}-{groupName}".format(address=group["address"], groupName=group["groupName"]),
  455. fault=fault,
  456. notifyTime=datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  457. )
  458. def update_next_port(self, nextPort):
  459. """
  460. 更新下一个开启的端口
  461. :param nextPort:
  462. :return:
  463. """
  464. otherConf = self.device.get("otherConf", dict())
  465. otherConf.update({"nextPort": nextPort})
  466. Device.objects.filter(devNo=self.device.devNo).update(otherConf=otherConf)
  467. Device.invalid_device_cache(self.device.devNo)