Do1e

Do1e

github
email

Nan Na charging, from rough to perfection

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 where there were free spots at a glance, but due to the insufficient number of charging piles, when I wanted to charge, it was likely all red, or the only green one was far away from me.
So I also prepared to write one myself, which could display the estimated remaining time, making it convenient for me to wait in advance ||, I'll outdo you all||

Backend Data Scraping#

Web scraping is quite simple for me, after all, I have written several projects related to web scraping, Reqable start!

Get Charging Station ID#

First, filter out the charging stations belonging to Nanjing University Xianlin from a bunch of requests, and get 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 were able to obtain the outletNo for each charging outlet under each charging station (for example, the Astronomy College), 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 (available, fault, minute billing mode, fixed amount mode), and whether there is an error message, and also calculate an estimated available time for convenient front-end display (after all, my front-end skills are quite 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 remain completely unchanged. 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 (it took about 2 seconds for 302 pieces of data, and it was tested that it would not trigger risk control) to update 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 front-end novice, my first version of the frontend was generated using Python. >︿<
I’m sharing it here 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] == "Available":
            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)

And the interface is quite rudimentary, 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';
    }
  }
}

Fourth Version Frontend#

Since the first version displayed in a table, as can be seen from the above image, the experience on mobile, which is a more commonly used application scenario, was quite poor. So I decided to reconstruct the UI for half a day and added a statistics table at the beginning, making it more convenient to plan charging destinations.

image

Basically, all front-end 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, how absurd. 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 still serve as a reference to know which charging pile is about to end.
  • 2025-02-22 15:52: Minute billing mode users can now choose the amount to pre-recharge, so this can be used to estimate the expected available time. However, considering that some people directly choose a higher amount to fully charge, and the precision of the returned values from the interface leads to lower calculation accuracy, this is for reference only. The electricity billing mode can theoretically also be estimated, but since there is no way to directly read the power from the interface, it will not be written for now.
Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.