shangdong_platform.py 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904
  1. # -*- coding: utf-8 -*-
  2. # !/usr/bin/env python
  3. import datetime
  4. import hashlib
  5. import hmac
  6. import json
  7. import logging
  8. import math
  9. import random
  10. import requests
  11. from django.conf import settings
  12. from mongoengine import StringField, DateTimeField, BooleanField, IntField, QuerySet
  13. from apps.web.api.utils import bd09_to_gcj02, get_coordinates_and_nums
  14. from apps.web.agent.models import Agent
  15. from apps.web.api.utils import AES_CBC_PKCS5padding_encrypt, AES_CBC_PKCS5padding_decrypt
  16. from apps.web.common.models import District
  17. from apps.web.constant import Const
  18. from apps.web.core.db import Searchable
  19. from apps.web.dealer.models import Dealer
  20. from apps.web.device.models import Device, Group, Part, GroupDict, DeviceDict
  21. from apps.web.user.models import ConsumeRecord
  22. logger = logging.getLogger(__name__)
  23. class PLATFORM_TYPE(object):
  24. SD = 0 # 山东
  25. JN = 1 # 济南
  26. class GroupIdMap(Searchable):
  27. groupId = StringField(verbose_name = u"组ID", unique = True)
  28. stationId = StringField(verbose_name = u"充电站ID", unique = True)
  29. @classmethod
  30. def add(cls, groupId):
  31. if cls._is_added(groupId):
  32. return
  33. h = hashlib.md5()
  34. h.update(groupId.encode('utf-8'))
  35. stationId = h.hexdigest()[8:24]
  36. return cls(groupId = groupId, stationId = stationId).save()
  37. @classmethod
  38. def get_groupId(cls, stationId):
  39. m = cls.objects.filter(stationId = stationId).first()
  40. if not m:
  41. return ""
  42. else:
  43. return m.groupId
  44. @classmethod
  45. def get_stationId(cls, groupId):
  46. m = cls.objects.filter(groupId = groupId).first()
  47. if not m:
  48. return ""
  49. else:
  50. return m.stationId
  51. @classmethod
  52. def _is_added(cls, groupId):
  53. return bool(cls.objects.filter(groupId = groupId).count())
  54. class GB2260(Searchable):
  55. # GB2260 国家行政区编码对应表
  56. code = StringField(verbose_name = "编码", unique = True)
  57. name = StringField(verbose_name = "名称")
  58. codeDict = {}
  59. @classmethod
  60. def _load(cls):
  61. for item in cls.objects.all():
  62. cls.codeDict.update({item.name: item.code})
  63. @classmethod
  64. def get_code(cls, name):
  65. if not cls.codeDict:
  66. cls._load()
  67. return cls.codeDict.get(name, "000000")
  68. class ShanDongNorther(Searchable):
  69. """
  70. 山东省能源局/济南市静态交通 的账号记录
  71. 兼容双方
  72. 备注:本次修改为兼容修改 字段以及函数均通过兼容测试 2022-04-24 zjl
  73. """
  74. # 北向信息 即我们推送到平台所需要的信息时候需要用到的
  75. northToken = StringField(verbose_name=u"token", default="") # 推送信息的时候的身份验证
  76. northTokenExpiredTime = DateTimeField(verbose_name=u"token的过期时间", default=datetime.datetime.now) # 过期重新获取
  77. northPort = StringField(verbose_name=u"推送的IP地址(山东)", default="")
  78. northPortJn = StringField(verbose_name=u"推送的IP地址(济南)", default="")
  79. # 省平台/济南静态交通的账号信息
  80. agentOperatorID = StringField(verbose_name=u"平台机构代码", default="") # 获取northToken时候使用 可以理解为代理商的营业执照
  81. agentOperatorSecret = StringField(verbose_name=u"平台机构秘钥", default="") # 获取northToken时候使用
  82. agentSigSecret = StringField(vebose_name=u"平台机构签名秘钥", default="") # 我方主动推送数据的时候, 加、解密 数据使用
  83. dataSecret = StringField(verbose_name=u"数据秘钥", default="") # 我方主动推送数据的时候,加、解密 数据使用
  84. dataSecretIV = StringField(verbose_name=u"数据秘钥向量", default="") # 我方主动推送数据的时候,加、解密 数据使用
  85. # 我们的信息 即省平台拉取的时候需要用到的
  86. northOperatorID = StringField(verbose_name=u"账号", default="") # 省平台登录我方服务器获取token时候使用(由我方提供)
  87. northOperatorSecret = StringField(verbose_name=u"密码", default="") # 省平台登录我方服务器获取token时候使用(由我方提供)
  88. sigSecret = StringField(vebose_name=u"平台机构秘钥", default="") # 省平台拉取我方服务器数据时候 加、解密数据使用
  89. pullDataSecret = StringField(verbose_name=u"数据秘钥", default="") # 省平台拉取我方服务器数据时候 加、解密数据使用
  90. pullDataSecretIV = StringField(verbose_name=u"数据秘钥", default="") # 省平台拉取我方服务器数据时候 加、解密数据使用
  91. # 辅助信息 对于同一个代理商不同经销商来说 上面的信息都是一样的 只有下面的信息不一样
  92. dealerId = StringField(verbose_name=u"经销商的ID") # 放置在token里面的信息 我方服务器用于从设备找经销商
  93. equipOperatorID = StringField(verbose_name=u"设备所属方") # 其实就是经销商的营业执照(可以理解为省平台的经销商ID)
  94. platform = IntField(verbose_name="平台种类", default=PLATFORM_TYPE.SD) # 用于鉴别是山东的还是济南的 后续接口可能会归为一个
  95. needReport = BooleanField(verbose_name = "是否需要向省平台报告", default = True)
  96. @property
  97. def ipPort(self):
  98. """
  99. 区分到底是推送到省平台还是 济南静态交通
  100. """
  101. return self.northPort if self.isSdPlatform else self.northPortJn
  102. @property
  103. def isSdPlatform(self):
  104. return self.platform == PLATFORM_TYPE.SD
  105. def get_token_data(self):
  106. """
  107. 获取token的载数据 身份验证以平台为维度获取 那么token的范围也以 平台为准 即 northOperatorID
  108. """
  109. return {
  110. "northOperatorID": self.northOperatorID,
  111. "platform": self.platform
  112. }
  113. @classmethod
  114. def get_norther(cls, **kwargs): # type:(dict) -> QuerySet
  115. """
  116. 获取norther的时候 有两个维度获取方式
  117. 第一种,我方主动推送的方式,由于每一个norther的 dealerId 唯一 所以一定能找到唯一的一个norther
  118. 第二种,我方被动回复 由于省平台拉取信息是以平台为单位,即AgentOperator 此时的norther不唯一了(有可能同一个平台下 有很多经销商都要对接)
  119. """
  120. filters = {
  121. "platform": kwargs["platform"]
  122. }
  123. # 通过设备找norther 主动推送 唯一
  124. kwargs.get("dealerId") and filters.update({"dealerId": kwargs["dealerId"]})
  125. # 通过token的信息找 被动回复 不唯一
  126. kwargs.get("northOperatorID") and filters.update({"northOperatorID": kwargs["northOperatorID"]})
  127. return cls.objects.filter(**filters)
  128. @staticmethod
  129. def bd09_to_gcj02(lng, lat):
  130. """
  131. 百度坐标系到google坐标系的经纬度转换
  132. :param lng: 百度经度
  133. :param lat: 百度维度
  134. :return:
  135. """
  136. if lng == 0.0 or lat == 0.0:
  137. return lng, lat
  138. x_pi = 3.14159265358979324 * 3000.0 / 180.0
  139. x = lng - 0.0065
  140. y = lat - 0.006
  141. z = math.sqrt(x * x + y * y) - 0.00002 * math.sin(y * x_pi)
  142. theta = math.atan2(y, x) - 0.000003 * math.cos(x * x_pi)
  143. gg_lng = z * math.cos(theta)
  144. gg_lat = z * math.sin(theta)
  145. return [gg_lng, gg_lat]
  146. def get_sig(self, data, push=False):
  147. """
  148. 生成签名字符串 根据使用的场景,确认签名的盐
  149. :param data: 生成签名的数据 iter
  150. :param push: 推送还是拉取
  151. :return:
  152. """
  153. sigSecret = str(self.agentSigSecret) if push else str(self.sigSecret)
  154. return hmac.new(sigSecret, data, hashlib.md5).hexdigest().upper()
  155. def send_request(self, url, **kwargs):
  156. """
  157. 主动发送HTTP请求获取数据 秘钥以及签名
  158. :param url:
  159. :param kwargs:
  160. :return:
  161. """
  162. headers = {"Content-Type": "application/json;charset=utf-8"}
  163. token = kwargs.pop("token", None)
  164. if token: headers.update({"Authorization": "Bearer {}".format(token)})
  165. timeout = kwargs.pop("timeout", 5)
  166. # 主动推送 加密以及向量为 dataSecret 和 dataSecretIV
  167. data = AES_CBC_PKCS5padding_encrypt(
  168. json.dumps(kwargs), dataSecret=self.dataSecret, dataSecretIV=self.dataSecretIV
  169. )
  170. data = {
  171. "OperatorID": self.agentOperatorID,
  172. "TimeStamp": datetime.datetime.now().strftime("%Y%m%d%H%M%S"),
  173. "Seq": "{:0>4}".format(random.randint(1, 1)),
  174. "Data": data
  175. }
  176. sig = self.get_sig(data.get("OperatorID") + data.get("Data") + data.get("TimeStamp") + data.get("Seq"), push=True)
  177. data.update({"Sig": sig})
  178. try:
  179. response = requests.post(url = url, json = data, headers = headers, timeout = timeout)
  180. except requests.Timeout:
  181. return dict()
  182. except Exception as e:
  183. logger.exception(e)
  184. return dict()
  185. if response.status_code != 200:
  186. return dict()
  187. # 这个地方的解密 仅仅是为了解密数据 打印日志 调试使用 没有实际意义
  188. if settings.DEBUG:
  189. responseData = response.json().get("Data", "")
  190. from pprint import pprint
  191. pprint(response.json())
  192. responseData = json.loads(
  193. AES_CBC_PKCS5padding_decrypt(responseData) or "{}"
  194. )
  195. logger.info("response result:{}".format(response.json()))
  196. logger.info("receive responseData:{}".format(responseData))
  197. return response.json()
  198. def join_url(self, path):
  199. """
  200. 拼接url
  201. :param path:
  202. :return:
  203. """
  204. # 版本号的确定
  205. return "{ipPort}/{path}".format(
  206. ipPort=self.ipPort,
  207. path=path
  208. )
  209. def get_token(self):
  210. """
  211. 获取省平台的token 更新token有效期
  212. :return:
  213. """
  214. if self.northToken and self.northTokenExpiredTime > datetime.datetime.now():
  215. return self.northToken
  216. url = self.join_url("query_token")
  217. data = {
  218. "OperatorID": self.agentOperatorID,
  219. "OperatorSecret": self.agentOperatorSecret
  220. }
  221. result = self.send_request(url, **data)
  222. ret = result.get("Ret")
  223. if ret != 0:
  224. return
  225. responseJson = result.get("Data")
  226. responseData = json.loads(
  227. AES_CBC_PKCS5padding_decrypt(s=responseJson, dataSecret=self.dataSecret, dataSecretIV=self.dataSecretIV) or "{}"
  228. )
  229. # 防止解析出错
  230. tokenAvailableTime = responseData.get("TokenAvailableTime", 0)
  231. token = responseData.get("AccessToken", "")
  232. # 数据库更新
  233. self.update(
  234. northToken=token,
  235. northTokenExpiredTime=datetime.datetime.now()+datetime.timedelta(seconds = tokenAvailableTime)
  236. )
  237. return token
  238. def notification_station(self, groupId):
  239. """
  240. 充电站信息上报 GROUP
  241. :param groupId:
  242. :return:
  243. """
  244. data = self.get_station(groupId, self)
  245. url = self.join_url("notification_stationInfo")
  246. token = self.get_token()
  247. self.send_request(url = url, token = token, StationInfo = [data])
  248. def notification_order_info(self, consumeDict, stopReason = None):
  249. """
  250. 当运营商平台完成一次充电时,将订单信息推送至省级平台。
  251. :param consumeDict:
  252. :param stopReason:
  253. :return:
  254. """
  255. # 参数格式化
  256. _time = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
  257. totalPower = float("{:.2f}".format(float(consumeDict.get("totalPower"))))
  258. totalElecMoney = float("{:.2f}".format(float(consumeDict.get("totalElecMoney"))))
  259. seq = self.agentOperatorID + _time + str(random.randint(0, 9999))
  260. logger.info("Shan Dong Norther send order Info, seq is {}".format(seq))
  261. data = {
  262. "StartChargeSeq": seq,
  263. "ConnectorID": consumeDict.get("connectorId"),
  264. "StartTime": consumeDict.get("startTime"),
  265. "EndTime": consumeDict.get("endTime"),
  266. "TotalPower": totalPower,
  267. "TotalElecMoney": totalElecMoney,
  268. "TotalSeviceMoney": 0.00,
  269. "TotalMoney": totalElecMoney,
  270. "StopReason": stopReason or 1
  271. }
  272. logger.info('notification_order_info:{}'.format(data))
  273. url = self.join_url("notification_orderInfo")
  274. token = self.get_token()
  275. self.send_request(url, token = token, **data)
  276. def retry_push_notification_order_info(self, retry_push_dict):
  277. """
  278. 能源局出现问题需要手动推送时候找到日志然后执行
  279. 此函数只能手动执行
  280. """
  281. data = retry_push_dict.copy()
  282. url = self.join_url("notification_orderInfo")
  283. token = self.get_token()
  284. self.send_request(url, token = token, **data)
  285. logger.info('retry_push_notification_order_info is ok seq={}'.format(retry_push_dict['StartChargeSeq']))
  286. def alarm_report(self, devNo, code, desc = None, status = None):
  287. """
  288. 当充电接口发生异常告警或故障时,运营商企业平台主动推送信息到省级平台。
  289. :param devNo:
  290. :param code:
  291. :param desc:
  292. :param status:
  293. :return:
  294. """
  295. desc = desc or u"设备故障"
  296. status = status or 0
  297. data = {
  298. "equipmentID": devNo,
  299. "alert_time": str(datetime.datetime.now())[19:],
  300. "alert_code": code,
  301. "describe": desc,
  302. "status": status
  303. }
  304. url = self.join_url("alarm_report")
  305. token = self.get_token()
  306. self.send_request(url, token = token, AlarmInfos = [data])
  307. def notification_station_status(self, statusDict):
  308. data = {
  309. "ConnectorID": statusDict.get("connectorID"),
  310. "Status": statusDict.get("status"),
  311. "CurrentA": statusDict.get("elec"),
  312. "VoltageA": statusDict.get("voltage"),
  313. "SOC": None,
  314. "begin_time": statusDict.get("startTime"),
  315. "current_kwh": statusDict.get("usedElec"),
  316. "current_meter": None,
  317. "bms_req_voltage": None,
  318. "bms_req_current": None
  319. }
  320. url = self.join_url("notification_stationStatus")
  321. token = self.get_token()
  322. self.send_request(url, token = token, ConnectorStatusInfos = data)
  323. @staticmethod
  324. def get_coordinates_and_nums(groupId):
  325. devices = Device.objects.filter(groupId = groupId)
  326. _count = devices.count()
  327. if _count == 0:
  328. return 0.0, 0.0, _count
  329. for dev in devices:
  330. if dev.location is not None and dev.location.has_key('coordinates'):
  331. coord = dev.location['coordinates']
  332. return coord[0], coord[1], _count
  333. return 0.0, 0.0, _count
  334. def notification_order_info_jn(self, consumeDict, stopReason = None):
  335. """
  336. 当运营商平台完成一次充电时,将订单信息推送至省级平台。
  337. :param consumeDict:
  338. :param stopReason:
  339. :return:
  340. """
  341. # 参数格式化
  342. _time = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
  343. totalPower = float("{:.2f}".format(float(consumeDict.get("totalPower"))))
  344. totalElecMoney = float("{:.2f}".format(float(consumeDict.get("totalElecMoney"))))
  345. seq = self.agentOperatorID + _time + str(random.randint(0, 9999))
  346. logger.info("Ji Nan Norther send order Info, seq is {}".format(seq))
  347. data = {
  348. "StartChargeSeq": seq,
  349. "ConnectorID": consumeDict.get("connectorId"),
  350. "StartTime": consumeDict.get("startTime"),
  351. "EndTime": consumeDict.get("endTime"),
  352. "TotalPower": totalPower,
  353. "PreDetails": "-",
  354. "PreElecMoney":0.00,
  355. "PreServiceMoney":0.00,
  356. "TotalElecMoney": totalElecMoney,
  357. "TotalSeviceMoney": 0.00,
  358. "TotalMoney": totalElecMoney,
  359. "StopReason": stopReason or 1
  360. }
  361. logger.info('notification_order_info:{}'.format(data))
  362. url = self.join_url("notification_orderInfo")
  363. token = self.get_token()
  364. self.send_request(url, token = token, OrderInfo=data)
  365. def notification_station_jn(self, groupId):
  366. """
  367. 充电站信息上报 GROUP
  368. :param groupId:
  369. :return:
  370. """
  371. group = Group.get_group(groupId)
  372. data = self.get_stations_info_jn(group, self)
  373. url = self.join_url("notification_stationInfo")
  374. token = self.get_token()
  375. self.send_request(url=url, token=token, StationInfo=[data])
  376. @staticmethod
  377. def get_stations_info_jn(group, norther): # type:(GroupDict, ShanDongNorther) -> dict
  378. """
  379. 获取站点的信息
  380. """
  381. # 获取经纬度 获取设备数量 经纬度使用火星坐标系转换
  382. lng, lat, count = get_coordinates_and_nums(group.groupId)
  383. lng, lat = bd09_to_gcj02(lng, lat)
  384. # 获取设备信息
  385. EquipmentInfos = list()
  386. devNos = Device.get_devNos_by_group([group.groupId])
  387. for devNo in devNos:
  388. dev = Device.get_dev(devNo) # type: DeviceDict
  389. if dev.majorDeviceType != u"汽车充电桩":
  390. continue
  391. EquipmentInfos.append(norther.get_equipment(dev, norther))
  392. # 充电站信息
  393. stationId = GroupIdMap.get_stationId(group.groupId)
  394. dealer = group.owner
  395. data = {
  396. "StationID": stationId, # 充电站ID 20
  397. "OperatorID": norther.agentOperatorID, # 组织机构代码 9
  398. "EquipmentOwnerID": norther.equipOperatorID, # 设备所属方组织机构代码 9
  399. "StationName": group.groupName, # 充电站名称描述 50
  400. "CountryCode": "CN", # 国家代码 固定
  401. "AreaCode": GB2260.get_code(District.get_area(group.get("districtId"))), # 地区编码 20
  402. "Address": group.address, # 详细地址 50
  403. "StationTel": "-", # 站点责任人电话,
  404. "ServiceTel": "-", # 站点服务电话
  405. "StationType": 1, # 站点类型
  406. "StationStatus": 50, # 站点状态
  407. "ParkNums": 0, # 车位数量 0代表未知
  408. "StationLng": "{:.6f}".format(float(lng)), # 精度(6位小数)
  409. "StationLat": "{:.6f}".format(float(lat)), # 维度(6位小数)
  410. "Construction": 255, # 建设场所
  411. "ParkInfo": "-",
  412. "ParkName": "-",
  413. "OpenAllDay": 1, # 是否全天开放
  414. "BusineHours": u"24小时全天服务", # 营业时间描述
  415. "MinElectricityPric": 0.0, # 最低充电费率 浮点型
  416. "ElectricityFee": "-", # 充电电费描述,
  417. "ServiceFee": "-", # 服务费率描述
  418. "ParkFree": 0, # 是否停车免费
  419. "ParkFee": "-", # 停车费率描述
  420. "SupportOrder": 0, # 是否支持预约
  421. "EquipmentInfos": EquipmentInfos, # 充电站信息,
  422. "ElectricityTax": 0.0,
  423. "ServiceTax": 0.0
  424. }
  425. return data
  426. def notification_station_status_jn(self, devNo):
  427. data = self.get_connector_status_jn(devNo)
  428. url = self.join_url("notification_stationStatus")
  429. token = self.get_token()
  430. self.send_request(url, token = token, ConnectorStatusInfos = data)
  431. @staticmethod
  432. def get_connector_status_jn(devNo):
  433. """
  434. 获取充电充电设备接口状态
  435. """
  436. ConnectorStatusInfos = list()
  437. device = Device.get_dev(devNo)
  438. parts = Part.objects.filter(logicalCode=device.logicalCode)
  439. devCache = Device.get_dev_control_cache(devNo)
  440. online = device.get("online", True)
  441. for part in parts:
  442. if device.get("devType", dict()).get("code") not in [
  443. Const.DEVICE_TYPE_CODE_CAR_CHARGING_CY,
  444. Const.DEVICE_TYPE_CODE_CAR_CHARGING_CY_V2
  445. ]:
  446. partNo = part.partNo
  447. if partNo in ['allPorts', 'usedPorts', 'usePorts']:
  448. continue
  449. portCache = devCache.get(part.partNo) or dict()
  450. else:
  451. portCache = devCache or dict()
  452. # 判断端口当前状态
  453. if not online:
  454. status = 0
  455. else:
  456. status = 3 if portCache.get("isStart") else 1
  457. data = {
  458. "ConnectorID": str(part.id),
  459. "Status": status,
  460. "CurrentA": 0,
  461. "VoltageA": 0,
  462. "BeginTime": portCache.get("startTime", "-"), # 开始时间
  463. "SOC": 0.0, # 剩余电量
  464. "CurrentKwh": 0.0, # 已充电量
  465. "CurrentMeter": 0.0, # 当前电表读数
  466. "BmsReqVoltage": 0.0, # BMS需求电压
  467. "BmsReqCurrent": 0.0 # BMS需求电流
  468. }
  469. ConnectorStatusInfos.append(data)
  470. return ConnectorStatusInfos
  471. def alarm_report_jn(self, devNo, code, desc = None, status = None):
  472. """
  473. 当充电接口发生异常告警或故障时,运营商企业平台主动推送信息到省级平台。
  474. :param devNo:
  475. :param code:
  476. :param desc:
  477. :param status:
  478. :return:
  479. """
  480. desc = desc or u"设备故障"
  481. status = status or 0
  482. data = {
  483. "EquipmentID": devNo,
  484. "AlertTime": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
  485. "AlertCode": code,
  486. "Describe": desc,
  487. "Status": status
  488. }
  489. url = self.join_url("alarm_report")
  490. token = self.get_token()
  491. self.send_request(url, token = token, AlarmInfos = [data])
  492. # ------------------------- 山东省平台 拉取站点信息 --------------------------------------
  493. @staticmethod
  494. def get_station(groupId, norther):
  495. """
  496. 获取站点信息
  497. :param groupId:
  498. :param norther:
  499. :return:
  500. """
  501. # 获取经纬度 获取设备数量
  502. lng, lat, count = ShanDongNorther.get_coordinates_and_nums(groupId)
  503. lng, lat = ShanDongNorther.bd09_to_gcj02(lng, lat)
  504. # 获取设备信息
  505. EquipmentInfos = list()
  506. devNos = Device.get_devNos_by_group([groupId])
  507. for devNo in devNos:
  508. dev = Device.get_dev(devNo) # type: DeviceDict
  509. if dev.devTypeCode not in [
  510. Const.DEVICE_TYPE_CODE_CAR_CHARGING_CY_V2, Const.DEVICE_TYPE_CODE_CAR_CHARGING_CY,
  511. Const.DEVICE_TYPE_CODE_CHARGING_HONGZHUO
  512. ]:
  513. continue
  514. EquipmentInfos.append(ShanDongNorther.get_equipment(Device.get_dev(devNo), norther))
  515. # 充电站信息
  516. group = Group.get_group(groupId)
  517. stationId = GroupIdMap.get_stationId(groupId)
  518. dealer = Dealer.get_dealer(group.get("ownerId"))
  519. data = {
  520. "StationID": stationId, # 充电站ID 20
  521. "OperatorID": norther.agentOperatorID, # 组织机构代码 9
  522. "EquipmentOwnerID": norther.equipOperatorID, # 设备所属方组织机构代码 9
  523. "StationName": group.get("groupName"), # 充电站名称描述 50
  524. "CountryCode": "CN", # 国家代码 固定
  525. "AreaCode": GB2260.get_code(District.get_area(group.get("districtId"))), # 地区编码 20
  526. "Address": group.get("address"), # 详细地址 50
  527. "StationTel": '-', # 站点责任人电话,
  528. "ServiceTel": '-', # 站点服务电话
  529. "StationType": 1, # 站点类型
  530. "StationStatus": 50, # 站点状态
  531. "ParkNums": count, # 车位数量 0代表未知
  532. "StationLng": "{:.6f}".format(float(lng)), # 精度(6位小数)
  533. "StationLat": "{:.6f}".format(float(lat)), # 维度(6位小数)
  534. "Construction": 255, # 建设场所
  535. "OpenAllDay": 1, # 是否全天开放
  536. "BusineHours": u"24小时全天服务", # 营业时间描述
  537. "MinElectricityPrice": None, # 最低充电费率 浮点型
  538. "ElectricityFee": None, # 充电电费描述,
  539. "ServiceFee": None, # 服务费率描述
  540. "ParkFree": None, # 是否停车免费
  541. "ParkFee": None, # 停车费率描述
  542. "SupportOrder": 0, # 是否支持预约
  543. "EquipmentInfos": EquipmentInfos, # 充电站信息
  544. }
  545. return data
  546. @staticmethod
  547. def get_equipment(dev, norther): # type:(DeviceDict, ShanDongNorther) -> dict
  548. agent = Agent.objects.get(id=dev.owner.agentId)
  549. ConnectorInfo = list()
  550. for part in dev.parts:
  551. ConnectorInfo.append(norther.get_connector(part, norther))
  552. data = {
  553. "EquipmentID": dev.devNo,
  554. "ManufacturerName": agent.productName,
  555. "EquipmentModel": dev.get("devType", dict()).get("name", ""),
  556. "EquipmentName": dev.get("logicalCode"),
  557. "EquipmentType": 2,
  558. "EquipmentStatus": 50,
  559. "EquipmentPower": 7,
  560. "NewNationalStandard": 1,
  561. "ConnectorInfos": ConnectorInfo,
  562. }
  563. return data
  564. @staticmethod
  565. def get_connector(part, norther): # type:(Part, ShanDongNorther) -> dict
  566. """
  567. 获取部件信息
  568. """
  569. return {
  570. "ConnectorID": str(part.id),
  571. "ConnectorName": part.partName,
  572. "ConnectorType": 2,
  573. "VoltageUpperLimits": 220,
  574. "VoltageLowerLimits": 220,
  575. "Current": part.attachParas.get("current", 16),
  576. "Power": part.attachParas.get("power", 7),
  577. "ParkNo": "-",
  578. "NationalStandard": 3,
  579. }
  580. # ------------------------- 山东省平台 获取站点统计信息 --------------------------------------
  581. @staticmethod
  582. def get_station_state(groupId, startTime, endTime):
  583. """
  584. 获取充电站的 一段时间内的统计信息 主要是电量
  585. :param groupId:
  586. :param startTime:
  587. :param endTime
  588. :return:
  589. """
  590. EquipmentStatsInfos = list()
  591. devNos = Device.get_devNos_by_group([groupId])
  592. elec = float(0)
  593. for devNo in devNos:
  594. tempState = ShanDongNorther.get_equipment_state(devNo, startTime, endTime)
  595. elec += tempState.get("EquipmentElectricity", 0.0)
  596. EquipmentStatsInfos.append(tempState)
  597. return {
  598. "StationElectricity": elec,
  599. "EquipmentStatsInfos": EquipmentStatsInfos
  600. }
  601. @staticmethod
  602. def get_equipment_state(devNo, startTime, endTime):
  603. """
  604. 获取充电设备的 一段时间内的统计信息 主要是电量
  605. :param devNo:
  606. :param startTime:
  607. :param endTime:
  608. :return:
  609. """
  610. device = Device.get_dev(devNo)
  611. ConnectorStatsInfo = list()
  612. elec = float(0)
  613. for part in device.parts:
  614. tempState = ShanDongNorther.get_part_state(devNo, startTime, endTime, part)
  615. elec += tempState.get("ConnectorElectricity", 0.0)
  616. ConnectorStatsInfo.append(tempState)
  617. return {
  618. "EquipmentID": str(devNo),
  619. "EquipmentElectricity": elec,
  620. "ConnectorStatsInfos": ConnectorStatsInfo
  621. }
  622. @staticmethod
  623. def get_part_state(devNo, startTime, endTime, part):
  624. """
  625. 获取充电端口的 一段时间内的统计信息 主要是电量
  626. :param devNo:
  627. :param part:
  628. :param startTime:
  629. :param endTime:
  630. :return:
  631. """
  632. filters = {
  633. "devNo": devNo,
  634. "finishedTime__gte": startTime,
  635. "finishedTime__lte": endTime,
  636. }
  637. device = Device.get_dev(devNo)
  638. if device.get("devType", dict()).get("code") != Const.DEVICE_TYPE_CODE_CAR_CHARGING_CY:
  639. filters.update({"attachParas__chargeIndex": part.partNo})
  640. records = ConsumeRecord.objects.filter(**filters).only("servicedInfo")
  641. elec = float(0)
  642. for item in records:
  643. elec += item.servicedInfo.get("elec", 0.0)
  644. return {
  645. "ConnectorID": str(part.id),
  646. "ConnectorElectricity": float("{:.1f}".format(float(elec)))
  647. }
  648. # ------------------------- 山东省平台 获取站点状态信息 --------------------------------------
  649. @staticmethod
  650. def get_station_status(groupId):
  651. """
  652. 获取充电站的当前状态
  653. :param groupId:
  654. :return:
  655. """
  656. ConnectorStatusInfos = list()
  657. devNos = Device.get_devNos_by_group([groupId])
  658. for devNo in devNos:
  659. device = Device.get_dev(devNo)
  660. parts = Part.objects.filter(logicalCode = device.logicalCode)
  661. devCache = Device.get_dev_control_cache(devNo)
  662. online = device.get("online", True)
  663. for part in parts:
  664. if device.get("devType", dict()).get("code") not in [
  665. Const.DEVICE_TYPE_CODE_CAR_CHARGING_CY,
  666. Const.DEVICE_TYPE_CODE_CAR_CHARGING_CY_V2
  667. ]:
  668. portCache = devCache.get(part.partNo) or dict()
  669. else:
  670. portCache = devCache or dict()
  671. # 判断端口当前状态
  672. if not online:
  673. status = 0
  674. else:
  675. status = 3 if portCache.get("isStart") else 1
  676. data = {
  677. "ConnectorID": str(part.id),
  678. "Status": status,
  679. "CurrentA": None,
  680. "VoltageA": None,
  681. "begin_time": portCache.get("startTime"), # 开始时间
  682. "SOC": None, # 剩余电量
  683. "current_kwh": None, # 已充电量
  684. "current_meter": None, # 当前电表读数
  685. "bms_req_voltage": None, # BMS需求电压
  686. "bms_req_current": None # BMS需求电流
  687. }
  688. ConnectorStatusInfos.append(data)
  689. return ConnectorStatusInfos
  690. # ------------------------- 山东省平台 获取站点计费信息 --------------------------------------
  691. @staticmethod
  692. def get_policy_info(partId):
  693. """
  694. 获取 端口的计费信息
  695. :param partId:
  696. :return:
  697. """
  698. DEFAULT_ELEC_PRICE = 1.500
  699. ELEC_FUNCS = [ShanDongNorther.get_elec_price_by_package, ShanDongNorther.get_elec_price_by_conf,
  700. ShanDongNorther.get_elec_price_by_consume]
  701. part = Part.objects.filter(id = partId).first()
  702. if not part:
  703. return
  704. devNo = Device.get_devNo_by_logicalCode(part.logicalCode)
  705. for func in ELEC_FUNCS:
  706. try:
  707. elecPrice = func(devNo)
  708. except Exception:
  709. elecPrice = None
  710. if elecPrice:
  711. break
  712. else:
  713. elecPrice = DEFAULT_ELEC_PRICE
  714. return {
  715. "StartTime": "000000",
  716. "ElecPrice": elecPrice,
  717. "SevicePrice": None
  718. }
  719. @staticmethod
  720. def get_elec_price_by_package(devNo):
  721. """
  722. 通过套餐获取电费
  723. :param devNo:
  724. :return:
  725. """
  726. device = Device.get_dev(devNo)
  727. package = device.get("washConfig", dict()).get("1", dict())
  728. if not package:
  729. return
  730. price = package.get("price")
  731. time = package.get("time")
  732. unit = package.get("unit")
  733. if unit != u"度":
  734. return
  735. if not all([price, time]):
  736. return
  737. try:
  738. elecPrice = float("{:.4f}".format(float(price) / float(time)))
  739. except ZeroDivisionError:
  740. return
  741. return elecPrice
  742. @staticmethod
  743. def get_elec_price_by_conf(devNo):
  744. """
  745. 通过设备设置设置电费
  746. :param devNo:
  747. :return:
  748. """
  749. device = Device.get_dev(devNo)
  750. elecPrice = device.get("otherConf", dict()).get("elecPrice")
  751. return float("{:.4f}".format(float(elecPrice)))
  752. @staticmethod
  753. def get_elec_price_by_consume(devNo):
  754. """
  755. 通过 最近一次的消费记录获取电费
  756. :param devNo:
  757. :return:
  758. """
  759. record = ConsumeRecord.objects.filter(devNo = devNo).sort("-id").first()
  760. if not record or not record.servicedInfo:
  761. return
  762. elec = record.servicedInfo.get("elec")
  763. spend = record.servicedInfo.get("spend")
  764. if not all([elec, spend]):
  765. return
  766. try:
  767. elecPrice = float("{:.4f}".format(float(spend) / float(elec)))
  768. except ZeroDivisionError:
  769. return
  770. return elecPrice