この文は Mix Space によって xLog に同期更新されています
最高のブラウジング体験を得るために、元のリンクを訪れることをお勧めします
https://www.do1e.cn/posts/code/njucharge
私が書いた 南那充電 - 鼓楼 または 南那充電 - 仙林 にようこそ
このすべての起源は、9 月のある夜に充電スタンドを見つけられなかったことから始まります……
実際、その前にはすでに南那充電のウェブページがありました: https://charge.zhuxh.net/
ただ、私個人としては使い勝手が少し物足りないと感じました。どこに空きがあるか一目でわかるのですが、充電スタンドが不足しているため、充電したいときには全て赤だったり、唯一の緑が遠くにあったりすることが多いです。
そこで、私は自分で作成することにしました。予想残り時間を表示できるようにして、事前に待機できるように ||、あなたたちを巻き込む ||
バックエンドデータ取得#
クローラーは私にとってはかなり簡単です。結局、いくつかのクローラー関連プロジェクトを作成してきましたから。Reqable 起動!
充電スタンド ID の取得#
まず、南大仙林に属する充電スタンドをリクエストの中からフィルタリングし、充電スタンドの station_id
を取得します。このステップは手動で行います。具体的な ID は以下の通りです:
https://github.com/Do1e/NJUCharge-backend/blob/main/stations.json
各充電スタンドのソケット ID を取得#
前のステップでは 33 の充電スタンドしかありませんでしたが、302 のソケット ID を手動で取得するのは勘弁してください。
f'https://wemp.issks.com/charge/v1/outlet/station/outlets/{station_id}'
から各充電スタンドの情報を取得できます。その中にはソケットの ID が含まれています。
注意:このステップではリクエストヘッダーに token
(issks_
で始まる文字列)を追加する必要があります。コードは以下の通りです:
https://github.com/Do1e/NJUCharge-backend/blob/main/utils/per_station.py
ついに、各ソケットの状態を取得しましょう#
前のステップで、各充電スタンド(例えば天文学院)の各充電ソケットの outletNo
を取得できました。このステップでは outletNo
に基づいて、f'https://wemp.issks.com/charge/v1/charging/outlet/{outletNo}'
から各ソケットの具体的な状態を取得できます!
返却例:
{
"code": "1",
"msg": "成功",
"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": "ソケット7",
"iState": 1,
"iCurrentChargingRecordId": 0,
"vOutletNo": "O230424025883180",
"iErrorCount": 0
},
"station": {
"iStationId": 161740,
"iAreaId": 786688,
"iFullChargingTime": 0,
"vStationName": "南京大学仙林校区18栋1号機",
"iState": 1,
"iHardWareState": "オンライン",
"hardWareState": 1
},
"billListDtoList": [
{
"billingType": 4,
"billingTypeName": "固定金額モード",
"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
}
実際、内部には多くの情報が不要です。ここではソケット名、予想残り時間、使用時間、状態コード(空き、故障、分単位課金モード、固定金額モード)、エラー情報の有無の 5 つだけを取得します。同時に、フロントエンドに直接表示できるように予想可能時間も計算します(結局、私のフロントエンドは本当に弱いので)。コードは以下の通りです:
https://github.com/Do1e/NJUCharge-backend/blob/main/utils/per_outlet.py
幸運なことに、状態を取得するこのステップではトークンは必要なく、各スタンドの下のソケットは完全に固定されています。その後、データを更新するには、既存の outletNo
に基づいてこのステップを繰り返すだけで大丈夫です。
データ後処理#
私が自分でデプロイする際にはマルチスレッドを使用しました(302 件のデータは基本的に 2 秒ほどで完了し、実際には風控を引き起こすことはありませんでした)。
フロントエンドが呼び出しやすいように、バックエンドではデータを残り時間でソートし、充電スタンドを元の xx号機
から xx栋
に分類変更しました:
https://github.com/Do1e/NJUCharge-backend/blob/main/sort_outlets.py
第一版フロントエンド#
結局、私はフロントエンドの初心者なので、第一版のフロントエンドは Python で生成されたものを使用しています。>︿<
皆さんに笑われるために公開します
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">選択スタンド:</label><select id="filter-station"><option value="">すべて</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] == "空き":
html += f'<td class="status-available">{outlet[key]}</td>'
elif key == "msg" and outlet[key] == "故障":
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)
しかも、インターフェースもかなり簡素ですが、せめて GPT に助けてもらって、スタンドをフィルタリングするための js コードを生成してもらいました。
第二版フロントエンド#
いわゆる第二版は、単に数行の CSS を書いて、このインターフェースを救おうとしただけです。
第三版フロントエンド#
私は新しい個人ホームページの作成に取り組み始めました。既にMix Spaceが JavaScript で Markdown を書くことをサポートしているので、/charge.html を個人ホームページにリンクしました。そして、元のフィルタリング機能も改善し、フィルタリングを行うと同時に URL パラメータが変更されるようにしました。これにより、リフレッシュ後も前回のユーザーのフィルタリングスタンドを記憶し、直接表示できるようになりました。
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';
}
}
}
このバージョンのフロントエンドの色をデザインする際、最初は信号機の配色(赤・黄・緑)で予想残り時間の 3 つの状態を表現しようと考えていましたが、頭の中はすでに《敗犬女主太多了》に占領されていました。この 3 つの代表色は似たような意味を表現できるようですので、公式サイトから配色を盗借りてきました。
第四版フロントエンド#
第一版からテーブルを使用して表示しているため、上の画像からもわかるように、より一般的なアプリケーションシーンであるモバイルでの体験は本当に言葉では言い表せません。そこで、UI を再構築することを決定し、最初に統計テーブルを追加して、充電目的地をより便利に計画できるようにしました。
基本的にすべての前後端コードは以下の GitHub リポジトリにあります。使用は歓迎ですが、MIT ライセンスを遵守し、使用時には私の著作権情報を保持してください。
今後の小更新#
- 2024-12-01 16:02:鼓楼校区を新たに追加
- 2025-01-07 19:44:今日は突然充電にはチャージが必要で、分単位で課金されることに気づきました。なんてことだ。使用時間の逆順で更新しました。閃開充電が提供する説明によると、この時間は最大 480 分で、少なくともどの充電スタンドが早く終了するかの参考にはなります。