Image

事情發生在一個風和日麗的平凡下午:

我:(把手上的事情弄到一個段落再下班吧)
~~十分鐘過後~~
我:差不多可以走了,看一下公車還有多久
公車:(一分鐘前離站)
我:(゚д゚)

這時候才明白愛恨情仇,最傷最痛是後悔。如果我早知道公車快到站了,也許我就不會錯過。

抱著這股傷痛,決定乾脆寫個小腳本,每天下班提醒我一下,避免重蹈覆轍。

綜上所述!目標是:每天下班前十分鐘,告訴我下一班到達的公車時間

因此至少能夠拆分成三個階段:

  • 每天下班前十分鐘(定時執行)
  • 告訴我(通知功能)
  • 下一班到達的公車時間(查詢資訊)

那麼,我們開始吧!

使用 MOTC API 服務取得公車資訊

首先,最重要的是要有資料來源。幸好我們先前在 Api 筆記的時候,就有介紹過公共運輸動態服務 MOTC Transport API,我們只需要使用這組 API 就能輕鬆拿到公車資訊了,感謝開發該服務的朋朋。

我們的場景是在辦公室查詢下一班目標公車到站時間,比對了 Swagger 上提供的中文敘述後,決定嘗試看看「市區公車之預估到站資料」(EstimatedTimeOfArrival/City/{City}/{RouteName}) 這支 API

註:為了避免被查水表,以下就用台北的 307 號公車為例,並假設目標站點是「台北車站(忠孝)」

嘗試把縣市和公車路線名稱代入後,可以取得公車行經的站牌資訊,其中就有我們最想要的估計到站時間(秒):

Image

接著讓我們調整一下參數,把取前幾筆的 $top 調大一點,來取得我們目標站點台北車站的站牌 ID:

Image

有了站牌 ID,我們就可以根據 官方提供的查詢語法 來篩選出目標站牌:

Image

如果有需要指定回程,也可以再加上 and direction eq 1 的條件。

如此一來我們就可以呼叫 MOTC API 來取得目標站牌+指定公車的到站估計時間囉:

Image

註:沒有申請會員的話有每日 50 次的使用上限,不過這次的目標也就下班打那一次,十分足夠了 XD

使用 Powershell 呼叫 API 取得資料

現在已經保障了資料來源,接下來就是要有個腳本來去打 API 拿資料回來囉!

基於 懶惰 方便的原則,決定用 Powershell 寫個小東西直接打資料回來就好。

這邊就直接用路線名稱和站牌 ID 來串 Uri,並且直接用 Invoke-RestMethod 來呼叫 API 吧:

$busName = '307' # 公車路線名稱
$stopId = 15250  # 站台 ID

# 呼叫 MOTC API 取得公車資訊
$uri = "https://ptx.transportdata.tw/MOTC/v2/Bus/EstimatedTimeOfArrival/City/Taipei/$($busName)?%24filter=StopId%20eq%20'$stopId'&%24top=1&%24format=JSON"

$response = Invoke-RestMethod -Uri $uri

# 把結果轉成 Json 確認一下
$response | ConvertTo-Json

小提醒:原本我們下的 $filter = StopId eq '15250' 的參數,其中 $ 和空白符在 Uri 會轉換成 HTML 編碼的 %24($)%20(空白),並不是亂碼,請不要緊張

另存成 .ps1 檔案來測試一下:

Image

看來查詢資料已經沒有問題了,接下來就是通知快要下班的我了

使用 Powershell 彈出通知視窗(初版)

第一次嘗試採用了彈跳視窗,想了一想還是順手記錄下來好了

首先將我們目標的 EstimateTime 取出來,然後顯示還有幾分鐘到站、預計幾點幾分到站

最後用 Wscript.Shell 來顯示彈跳視窗:

$estimateSec = $response.EstimateTime

# 組裝要顯示的訊息
$estimateMin = $estimateSec / 60
$estimateTime = (Get-date).AddSeconds($estimateSec).ToString("HH:mm")
$message = "Bus $($busName) - EstimateTime: in $([Math]::Floor($estimateMin)) minute(s), $estimateTime"

# 使用彈跳視窗將預計抵達的時間列印出來
$wshell = New-Object -ComObject Wscript.Shell
$wshell.Popup($message)

雖然運作順利,但是……

Image

總覺得有點不是很好看啊,而且工作到一半在畫面正中間跳出這個會煩死吧囧

使用 Powershell 傳送 Line Notify

就在此時一個靈光乍現,對呀我之前爬訂便當的時候不是用過 Line Notify 嗎!

當下一個直衝 Line Notify 高速申請權杖:

Image

Image

Image

複製權杖之後,衝回 Powershell,前人教學抄起來,Invoke-RestMethod 就直接打下去:

# 使用 Line Notify 傳送通知
$lineUri = 'https://notify-api.line.me/api/notify'
$lineToken = 'Bearer YOUR_LINE_TOKEN'
$header = @{ Authorization = $lineToken }
$body = @{ message = $message }
Invoke-RestMethod -Uri $lineUri -Method Post -Headers $header -Body $body

Line 也不負期望地彈出來:

Image

大功告成!搞定拿資料和通知的部分啦~

補充:如果訊息內容有使用中文的朋友,請注意編碼問題。必須存成 Utf-8 with BOM,否則會出現亂碼:

Image

這時候就需要更改編碼為 Utf-8 with BOM;

Image

再重新嘗試一次就會正常了:

Image

原本卡在這步搞不定,正好黑暗執行緒大大發了篇 PowerShell .ps1 檔 UTF-8 編碼問題之變形錯誤,才知道 PowerShell 5.x 有編碼解析的問題,改成 Utf-8 with BOM 順利完工。這邊補充給各位朋朋,望周知

到這邊 Powershell 的部份就處理好了,目前會長這樣:

$busName = '307' # 公車路線名稱
$stopId = 15250  # 站台 ID

# 呼叫 MOTC API 取得公車資訊
$uri = "https://ptx.transportdata.tw/MOTC/v2/Bus/EstimatedTimeOfArrival/City/Taipei/$($busName)?%24filter=StopId%20eq%20'$stopId'&%24top=1&%24format=JSON"
$response = Invoke-RestMethod -Uri $uri
$estimateSec = $response.EstimateTime

# 組裝要顯示的訊息
$estimateMin = $estimateSec / 60
$estimateTime = (Get-date).AddSeconds($estimateSec).ToString("HH:mm")
$message = "Bus $($busName) - EstimateTime: in $([Math]::Floor($estimateMin)) minute(s), $estimateTime"

# 使用 Line Notify 傳送通知
$lineUri = 'https://notify-api.line.me/api/notify'
$lineToken = 'Bearer YOUR_LINE_TOKEN'
$header = @{ Authorization = $lineToken }
$body = @{ message = $message }
Invoke-RestMethod -Uri $lineUri -Method Post -Headers $header -Body $body

使用 工作排程器 定時執行 Powershell 腳本

秉持著前面選擇 Powershell 的 偷懶 簡單精神,這邊的定時執行就直接使用 Windows 內建的工作排程器來處理:

Image

因為我們的場景相對簡單,只有要在特定時間幫我們呼叫 Powershell 腳本,因此直接選擇「建立基本動作」

Image

Image

接著讓我們選擇每週,並指定平日的時候再執行:

Image

Image

最後選擇啟動程式,讓工作排程器開啟 Powershell 並呼叫我們的腳本:

Image

這邊的「程式或指令碼」輸入 powershell,接著在「新增引數」的部份告訴 Powershell 我們要執行的腳本 -File "C:\Scripts\BusReminder.ps1"(記得換成你的腳本路徑呦)

Image

註:沒有調整過執行原則的朋友們,可以在引數上加入 -ExecutionPolicy Bypass 來關閉警告,也就是 -ExecutionPolicy Bypass -File "C:\Scripts\BusReminder.ps1" 這樣子的感覺

註:如果跟我一樣會把腳本做成 Function 並存成 psm1 檔案的朋朋,這邊的引數會需要變成跟 Profile 一樣的處理方式,先 Import 進來再呼叫方法(這邊假設為 Run-BusNotify())例如,:Import-Module "C:\Scripts\BusReminder.psm1";Run-BusNotify;

接著只需要完成就可以在排程中找到囉,馬上就來執行看看是不是正常運作吧:

Image

Image

Image

大功告成!

後日談

自從有了公車到站提醒後,下班再也沒有煩惱了呢

提醒:公車還有十分鐘 
我:(還有十分鐘耶,把手上的事情弄到一個段落再下班吧)
~~弄了十五分鐘~~
公車:(離站)
我:(゚д゚)

這時候才明白科技終究是有極限的。

參考資料