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 writings on Nanjing Charging - Gulou or Nanjing Charging - 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 Charging webpage before that: https://charge.zhuxh.net/
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 spot was far away from me.
So I decided 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#
Web scraping is quite simple for me, after all, I have written several projects related to it, 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 stations. 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.
Written on June 16, 2025: Why do so many newly added charging piles have only two outlets per charging station, which caused me to spend a long time writing down station_id
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 the information for 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 obtained the outletNo
for each charging outlet under each charging station (for example, the Astronomy College). 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 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. I also calculated an estimated available time for easier display on the frontend (after all, my frontend 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 to process 302 entries, and it was tested that it would not trigger risk control) to update the data on the backend machine every minute.
To facilitate frontend 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 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)
Moreover, 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 was just a few lines of CSS written to try to save this interface.
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 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 format, as can be seen from the images above, the experience on mobile, which is a more commonly used application scenario, is indeed quite poor. So I decided to spend half a day reconstructing the UI and added a statistics table at the beginning to facilitate planning charging destinations.
All the frontend and backend code is in the GitHub repository below. Feel free to use it, but please comply with the MIT license and retain my copyright information.
Subsequent Minor Updates#
- 2024-12-01: Added Gulou Campus.
- 2025-01-07: Today I suddenly found out 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, so it can still serve as a reference to know which charging pile is about to finish.
- 2025-02-22: Users in minute billing mode can now select the pre-recharged amount, so this can be used to estimate the expected available time. However, considering that some people may choose a higher amount to fully charge, and the precision of the returned values from the interface leads to lower final calculation precision, this is for reference only. The power 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.
- 2025-06-16: Added multiple charging piles in Gulou and Xianlin. (Outlet count change: 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.