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 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, which could display the estimated remaining time, making it easier for me to wait in advance ||, roll you all over||
Backend Data Scraping#
Web scraping is quite simple for me, after all, I have written several projects related to scraping, Reqable launch!
Get Charging Station ID#
First, filter out the charging stations belonging to Nanjing University Xianlin from a bunch 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, 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 obtain the outletNo
for 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 No. 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, while also calculating an estimated available time for easier display on the front end (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 wouldn't trigger risk control) to update the data once every minute on the backend machine.
To facilitate front-end calls, I also sorted the data by remaining time on the backend and changed the charging station classification 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 frontend was generated using Python. >︿<
Here it is 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 was just a few lines of CSS written to try to save this interface.
Third Version Frontend#
I started tinkering 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.
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 I have been using a table to display from the first version, as can be seen from the above image, 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, making it more convenient to plan charging destinations.
Basically all the front and back end code is in the following GitHub repository, welcome to use but please abide by the MIT protocol, and retain my copyright information when using.
南哪充电后端(其实也有前端示例)
Subsequent Minor Updates#
- 2024-12-01 16:02: Added Gulou Campus
- 2025-01-07 19:44: Today I suddenly found that charging must be precharged and billed by the minute, how ridiculous. 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: Users in minute billing mode can now select the precharged amount, 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 will not be written for now.