Do1e

Do1e

github
email

Nan Na charging, from rough to perfect

This article is synchronized and updated to xLog by Mix Space
For the best browsing experience, it is recommended to visit the original link
https://www.do1e.cn/posts/code/njucharge



Welcome to visit my written Nanjing Charging - Gulou or Nanjing Charging - Xianlin

The origin of all this comes from a night in September when I couldn't find a charging pile...
In fact, there was already a Nanjing Charging webpage before that: https://charge.zhuxh.net/

image

However, I personally felt it was still lacking; I could only see at a glance where there were free spots, but due to the insufficient number of charging piles, when I wanted to charge, it was likely all red, or the only green spot was far away from me.
So I also planned to write one myself that could display the estimated remaining time, making it convenient for me to wait in advance ||, I will outdo you all||

Backend Data Scraping#

Scraping is quite simple for me, after all, I have written several projects related to scraping, Reqable go!

Get Charging Station ID#

First, filter out the charging stations belonging to Nanjing University Xianlin from a pile of requests and obtain the station_id of the charging station. This step is purely manual, the specific IDs are as follows:

https://github.com/Do1e/NJUCharge-backend/blob/main/stations.json

Get Outlet ID for Each Charging Station#

The previous step only had 33 charging stations, which is acceptable to write down manually, but if I have to write down 302 outlet IDs manually, please spare me.
You can get the information for each charging station from f'https://wemp.issks.com/charge/v1/outlet/station/outlets/{station_id}', which includes the outlet IDs.
Note that this step must include a token (a string starting with issks_) in the request header. The code is as follows:

https://github.com/Do1e/NJUCharge-backend/blob/main/utils/per_station.py

Finally, let's get the status of each outlet#

In the previous step, we could get the outletNo of each charging outlet under each charging station (for example, the Astronomy Institute), and in this step, we can get the specific status of each outlet based on outletNo from f'https://wemp.issks.com/charge/v1/charging/outlet/{outletNo}'!

Return example:

{
  "code": "1",
  "msg": "Success",
  "data": {
    "userName": null,
    "supportPayType": null,
    "monthlyDetail": {
      "monthlyPlanId": null,
      "renewFlag": false,
      "districtName": null,
      "hasUserMonthlyPlan": 0,
      "whiteMonthly": false,
      "districtWhite": false,
      "hasDistrictMonthlyPlan": 0,
      "monthlyUsedChargingLength": 0,
      "isMonthlyPlanAvailable": 0,
      "availableChargingTime": 0,
      "expiresTime": null,
      "iType": null,
      "iDistrictId": 18877,
      "dLimitPower": null,
      "iParkId": null,
      "wuYou": 0
    },
    "version": 3,
    "business": {
      "businessDays": null,
      "businessInTime": 1,
      "businessopen": 0,
      "tBusinessStart": null,
      "tBusinessEnd": null,
      "businessType": null
    },
    "outlet": {
      "iOutletId": 1435883,
      "vOutletName": "Outlet 7",
      "iState": 1,
      "iCurrentChargingRecordId": 0,
      "vOutletNo": "O230424025883180",
      "iErrorCount": 0
    },
    "station": {
      "iStationId": 161740,
      "iAreaId": 786688,
      "iFullChargingTime": 0,
      "vStationName": "Nanjing University Xianlin Campus Machine 1, Building 18",
      "iState": 1,
      "iHardWareState": "Online",
      "hardWareState": 1
    },
    "billListDtoList": [
      {
        "billingType": 4,
        "billingTypeName": "Fixed Amount Mode",
        "proAmount": 1.0,
        "startPriceCountIndex": 0,
        "propertyList": [
          {
            "iPowerLimitStr": 0,
            "iPowerLimitEnd": 120,
            "dFeePerMin": 1.0,
            "dFeePerHour": 60.0,
            "iHour": 6.0,
            "dDisCountFeePerMin": null,
            "dDisCountFeePerHour": null,
            "vStartTime": null,
            "vEndTime": null,
            "iType": 3
          },
          {
            "iPowerLimitStr": 121,
            "iPowerLimitEnd": 900,
            "dFeePerMin": 1.0,
            "dFeePerHour": 60.0,
            "iHour": 5.0,
            "dDisCountFeePerMin": null,
            "dDisCountFeePerHour": null,
            "vStartTime": null,
            "vEndTime": null,
            "iType": 3
          },
          {
            "iPowerLimitStr": 0,
            "iPowerLimitEnd": 900,
            "dFeePerMin": 2.0,
            "dFeePerHour": 120.0,
            "iHour": 10.0,
            "dDisCountFeePerMin": null,
            "dDisCountFeePerHour": null,
            "vStartTime": null,
            "vEndTime": null,
            "iType": 3
          }
        ],
        "isDefaultBilling": 1,
        "showMaxPowerInfo": 1
      }
    ],
    "staff": {
      "tBeginTime": null,
      "tEndTime": null,
      "isDisFree": 0,
      "isFree": 0,
      "freeType": 0,
      "ruleTimes": null
    },
    "banners": [
      {
        "iBannerId": 342,
        "iType": 1,
        "vImgUrl": "/skoms/doc/2023-03-08/it01on9v1gr9o5mo.png",
        "vHref": "https://api.issks.com/issksh5/?#/activityPage/pages/yearCardPage/yearCardPage",
        "iImgUrlType": 1,
        "iLinkMiniApp": 0,
        "vOriginalId": "",
        "vMiniAppId": null,
        "iLinkType": "EXTERNAL_LINK",
        "iSecond": 0,
        "vRemark": null
      },
      {
        "iBannerId": 404,
        "iType": 1,
        "vImgUrl": "/skoms/doc/2024-11-19/xdaecvf5cijrldxu.jpg",
        "vHref": "https://mp.weixin.qq.com/s/SwNDfydkbIvfmrki3G3FMQ",
        "iImgUrlType": 1,
        "iLinkMiniApp": 0,
        "vOriginalId": "",
        "vMiniAppId": null,
        "iLinkType": "EXTERNAL_LINK",
        "iSecond": 0,
        "vRemark": null
      },
      {
        "iBannerId": 427,
        "iType": 1,
        "vImgUrl": "/skoms/doc/2024-11-08/5g190u60qsvrb2oe.jpg",
        "vHref": "https://api.issks.com/issksh5/?#/sonPage/pages/batteryReport/batteryReport",
        "iImgUrlType": 1,
        "iLinkMiniApp": 0,
        "vOriginalId": "",
        "vMiniAppId": null,
        "iLinkType": "EXTERNAL_LINK",
        "iSecond": 0,
        "vRemark": null
      },
      {
        "iBannerId": 384,
        "iType": 1,
        "vImgUrl": "/skoms/doc/2024-11-15/yu1xh86jqzy7wb5x.jpg",
        "vHref": "https://shop-sksop.issks.com",
        "iImgUrlType": 1,
        "iLinkMiniApp": 1,
        "vOriginalId": "gh_6f1e4731d3ad",
        "vMiniAppId": "wx14dcf42b12d3f02c",
        "iLinkType": "EXTERNAL_LINK",
        "iSecond": 0,
        "vRemark": null
      },
      {
        "iBannerId": 347,
        "iType": 1,
        "vImgUrl": "/skoms/doc/2023-04-10/asurox92j5of1vfb.gif",
        "vHref": "https://zf.shanghcat.com/tdpl/index?cid=94825049&pln=14580873",
        "iImgUrlType": 1,
        "iLinkMiniApp": 0,
        "vOriginalId": "",
        "vMiniAppId": null,
        "iLinkType": "EXTERNAL_LINK",
        "iSecond": 0,
        "vRemark": null
      },
      {
        "iBannerId": 420,
        "iType": 1,
        "vImgUrl": "/skoms/doc/2024-09-29/b9skyf6l9ul268wq.jpg",
        "vHref": "https://mp.weixin.qq.com/s/HKuy_UFI5y2832YVOLTtzQ",
        "iImgUrlType": 1,
        "iLinkMiniApp": 0,
        "vOriginalId": "",
        "vMiniAppId": null,
        "iLinkType": "EXTERNAL_LINK",
        "iSecond": 0,
        "vRemark": null
      }
    ],
    "popups": null,
    "floatBanner": null,
    "powerFee": null,
    "usedMonthly": 0,
    "type": 0,
    "pageViewType": "common",
    "curTime": 1732984774218,
    "registerMobile": null,
    "presetLastTime": 0,
    "restmin": 0,
    "usedmin": 0,
    "usedfee": null,
    "currentUser": null,
    "cardfunds": 0,
    "funds": 0.0,
    "safeOpenFlag": 1,
    "closeWuyouSwitch": 0,
    "safeOpenFee": 0.09,
    "safeChargingOpen": 0,
    "nowBillingType": 0,
    "electric": 0,
    "chargingBeginTime": null,
    "normalMonthParkRecord": 0,
    "smartMonthParkRecord": 0,
    "chargeDiscount": null,
    "fixedAmount": {
      "amountList": [
        1.0,
        2.0
      ],
      "defaultAmountIndex": 1,
      "defaultPowerIndex": 2
    },
    "universityProperty": {
      "options": [
        "1",
        "2",
        "3"
      ],
      "maxOption": "20",
      "minOption": "3"
    },
    "noticeType": 0,
    "noticeContent": null,
    "availableNotice": 0,
    "urlLink": null,
    "available": 0,
    "userSelectAmount": null,
    "isCloseWuYou": 0,
    "canCopy": null,
    "nationalStandard": 0,
    "buttonType": 1,
    "helpMobile": null,
    "title": null,
    "managerPriceIsHour": 0,
    "activityContent": null,
    "qrcoed": 0,
    "subscribed": 0,
    "districtId": 18877,
    "showMinute": 0,
    "tags": [
      "UNIVERSITY"
    ],
    "secondaryCardGuide": 0,
    "secondaryCardNum": null,
    "averageAmount": null,
    "secondaryCardMinAmount": null,
    "text": null,
    "alipayUrl": "https://t.bfr2.top/p18OoKF",
    "weather": null,
    "weatherType": null,
    "tianMu": false
  },
  "success": true
}

In fact, many of the details inside are unnecessary; I only need to extract the outlet name, estimated remaining time, used time, status code (free, fault, minute billing mode, fixed amount mode), and whether there is an error message. At the same time, I also calculated an estimated available time for easy display on the front end (after all, my front end skills are really weak). The code is as follows:

https://github.com/Do1e/NJUCharge-backend/blob/main/utils/per_outlet.py

Fortunately, this step of getting the status does not require a token, and the outlets under each station are completely fixed. After that, updating the data only requires repeating this step based on the existing outletNo.

Data Post-Processing#

When I deployed it myself, I used multithreading (302 pieces of data can be processed in about 2 seconds, and it was tested that it would not trigger risk control). I updated the data on the backend machine every minute.
To facilitate front-end calls, I also sorted the data by remaining time on the backend and changed the classification of charging stations from the original xx machine to xx building:

https://github.com/Do1e/NJUCharge-backend/blob/main/sort_outlets.py

First Version Frontend#

After all, being a frontend novice, my first version of the frontend was generated using Python. >︿<
I’m sharing it for everyone to laugh at

import json

with open("output/outlets.json", "r", encoding="utf-8") as f:
    outlets = json.load(f)

keys = ["station", "name", "restmin", "available_time", "usedmin", "msg", "update_time"]
station_options = set()
for outlet in outlets:
    station_options.add(outlet["station"])
station_options = list(station_options)
station_options.sort()


html = '<div class="filter-container"><label for="filter-station">Select Station:</label><select id="filter-station"><option value="">All</option>'
for station in station_options:
    html += f'<option value="{station}">{station}</option>'
html += "</select></div>"

html += "<table>\n<thead>\n<tr>\n"
for key in keys:
    html += f"<th>{key}</th>"
html += "</tr>\n</thead>\n<tbody>\n"

for outlet in outlets:
    html += "<tr>"
    for key in keys:
        if key == "msg" and outlet[key] == "Free":
            html += f'<td class="status-available">{outlet[key]}</td>'
        elif key == "msg" and outlet[key] == "Fault":
            html += f'<td class="status-error">{outlet[key]}</td>'
        elif key == "msg":
            html += f'<td class="status-busy">{outlet[key]}</td>'
        elif key == "restmin" and outlet[key] < 20:
            html += f'<td class="status-available">{outlet[key]}</td>'
        else:
            html += f'<td class="tdnormal"><span>{outlet[key]}</span></td>'
    html += "</tr>\n"
html += "</tbody>\n</table>"

with open("html_template.html", "r", encoding="utf-8") as f:
    template = f.read()
    html = template.replace("{{table}}", html)

with open("index.html", "w", encoding="utf-8") as f:
    f.write(html)

Moreover, the interface is quite simple, but at least I used GPT to help me generate a piece of JS code for filtering stations.

image

Second Version Frontend#

The so-called second version was just a few lines of CSS written to try to save this interface.

image

Third Version Frontend#

I started to tinker with my new personal homepage. Since Mix Space supports writing Markdown with JavaScript, I linked /charge.html to my personal homepage and improved the original filtering function, so that while filtering, the URL parameters would change, allowing the last filtered station to be remembered and displayed directly after refreshing.

image

var filter = document.getElementById('filter-station');
var urlParams = new URLSearchParams(window.location.search);
var initialFilter = urlParams.get('filter') || '';
filter.value = initialFilter;
filterTable();

filter.addEventListener('change', function () {
  var selectedValue = filter.value;
  if (selectedValue === '') {
    urlParams.delete('filter');
  } else {
    urlParams.set('filter', selectedValue);
  }
  if (urlParams.toString() === '') {
    window.history.replaceState({}, '', location.pathname);
  } else {
    window.history.replaceState({}, '', `${location.pathname}?${urlParams}`);
  }
  filterTable();
});

function filterTable() {
  var rows = document.getElementsByTagName('tr');
  for (var i = 1; i < rows.length; i++) {
    var row = rows[i];
    var name = row.children[0].textContent.toLowerCase();
    var nameFilter = filter.value.toLowerCase();
    if (name.indexOf(nameFilter) !== -1 || nameFilter === '') {
      row.style.display = 'table-row';
    } else {
      row.style.display = 'none';
    }
  }
}

When designing the color for this version of the frontend, I initially thought of using traffic light colors (red, yellow, green) to represent the three states of estimated remaining time, but my mind has been occupied by “Too Many Loser Girls”. The three representative colors in it seem to express similar meanings well, so I borrowed the color scheme from its official website.

image

Fourth Version Frontend#

Since I have been using a table to display from the first version, the experience on mobile, which is a more commonly used application scenario, is indeed hard to describe. So I decided to reconstruct the UI for half a day and added a statistics table at the beginning, making it easier to plan charging destinations.

image

Basically all the front and back end code is in the following GitHub repository, welcome to use but please comply with the MIT license and retain my copyright information.

Subsequent Minor Updates#

  • 2024-12-01 16:02: Added Gulou Campus
  • 2025-01-07 19:44: Today I suddenly found that charging must be recharged and billed by the minute, which is unacceptable. Updated to sort by used time in reverse order; according to the instructions provided by Shankai Charging, this time is a maximum of 480 minutes, at least it can serve as a reference to roughly know which charging pile is about to end.
Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.