Welcome to my South Nanyang Charging - Gulou or South Nanyang Charging - Xianlin.
Everything started one evening in September when I couldn't find a charging station...
In fact, there was already a South Nanyang Charging webpage before that: https://charge.zhuxh.net/

However, personally, I felt it still lacked something. It only showed at a glance which spots were free, but due to the insufficient number of charging stations, when I wanted to charge, I’d often see all red lights—or the few green ones were far away.
So I decided to build my own solution that could display estimated remaining time, making it easier for me to arrive early and secure a spot and outperform you
Crawling is quite simple for me—after all, I've built several crawler projects before. Let's fire up Reqable!
First, filter through the requests to identify those belonging to Nanjing University Xianlin Campus and extract the station_id. This step required manual copying. The specific IDs are as follows:
The previous step gave us 33 charging stations—manual entry was manageable. But manually copying 302 outlet IDs? Please, spare me.
As of June 16, 2025: Why do so many newly added chargers have just two outlets per station? I spent ages copying station_ids today. Currently, Xianlin Campus has 112 charging stations and 724 outlets.
We can retrieve each station’s information—including outlet IDs—from f'https://wemp.issks.com/charge/v1/outlet/station/outlets/{station_id}'.
Now that we have each station’s outletNo, we can query the status of every outlet using f'https://wemp.issks.com/charge/v1/charging/outlet/{outletNo}'!
Example response:
Actually, most of this data is unnecessary. I extracted only the outlet name, estimated remaining time, used time, status code (idle, faulty, minute-based billing, fixed amount mode), and error messages. I also calculated an estimated available time for easy frontend display (since my frontend skills are terrible). Here’s the code:
Fortunately, retrieving status doesn’t require a token, and each station’s outlets remain fixed. To update data later, simply re-run this step using existing outletNo values.
When deploying locally, I used multithreading (processing 302 entries takes about 2 seconds—no rate limiting triggered). I updated data on the backend server every minute.
To make it easier for the frontend to consume, I sorted the data by remaining time and reorganized charging stations from Unit xx to Building xx:
Being a frontend novice, my first version used Python-generated HTML. >︿<
Here’s the laughable result—please enjoy.
And the interface was incredibly basic. Still, I managed to get GPT to generate some JavaScript for filtering sites.

The so-called second version was just a few lines of CSS to rescue this UI.

I began working on my new personal homepage. Since Mix Space supports Markdown with JavaScript, I hosted /charge.html under my personal site and improved the filtering function—updating URL parameters during filtering so that refreshing retains the user’s last selection.

Since the first version used tables, as seen in the image above, the mobile experience was truly awful. So I decided to completely rebuild the UI and added a summary table at the beginning for better planning of charging destinations.

All backend and frontend code is available in the GitHub repository below. Feel free to use it, but please follow the MIT license and retain my copyright notice.
{
"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 Building 18, Unit 1",
"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
}
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] == "Idle":
html += f'<td class="status-available">{outlet[key]}</td>'
elif key == "msg" and outlet[key] == "Faulty":
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)
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';
}
}
}