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 the Nanjing Charge - Gulou or Nanjing Charge - Xianlin
The origin of all this came from a night in September when I couldn't find a charging pile...
In fact, there was already a Nanjing Charge webpage before that: https://charge.zhuxh.net/

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 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 scraping, Reqable go!
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, and 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.
Written on June 16, 2025: Why are so many newly added charging piles only have two outlets per charging station, which made me write down station_id for a long time today. Currently, the Xianlin campus has a total of 112 charging stations and 724 outlets.
From f'https://wemp.issks.com/charge/v1/outlet/station/outlets/{station_id}', you can get information about each charging station, including the outlet IDs.
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 from f'https://wemp.issks.com/charge/v1/charging/outlet/{outletNo}' based on 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. At the same time, I also calculate an estimated available time for the front end to display directly (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 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 (302 pieces of data can be processed in about 2 seconds, 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, I am a front-end novice, and my first version of the frontend was generated using Python. >︿<
I’ll share 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] == "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 simple, but at least I used GPT to help me generate a piece of JS code for filtering stations.

Second Version Frontend#
The so-called second version is just a few lines of CSS written to try to save this interface.

Third Version Frontend#
I started to work on my new personal homepage. Since Mix Space supports writing Markdown with JavaScript, I linked /charge.html to my personal homepage and also improved the original filtering function, so that while filtering, the URL parameters change, allowing the last selected station to be remembered and displayed directly after refreshing.

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, is truly unsatisfactory. Therefore, I was determined to refactor the UI for half a day and added a statistics table at the beginning, making it easier to plan charging destinations.

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 when using.
Github Repo not found
The embedded github repo could not be found…
Subsequent Minor Updates#
- 2024-12-01: Added Gulou campus.
- 2025-01-07: Today I suddenly found that charging must be preloaded 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 still serve as a reference to know which charging pile is about to end.
- 2025-02-22: Minute billing mode users can now choose the amount to preload, 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 kilowatt-hour billing mode can theoretically also be estimated, but since there is no way to directly read the power from the interface, it is not written for now.
- 2025-06-16: Added multiple charging piles in Gulou and Xianlin. (Change in number of outlets: Gulou 148->308, Xianlin 302->724)
- 2025-06-22: It seems that the maximum charging time set by the system is 480 minutes, so based on this, the estimation of remaining time has been modified.