今天是山陀兒颱風假的第二天, 昨天在家完成兩套公司維運軟體改版, 今天要來做自己的 ESP32 實驗了. 為了在 xtools 函式庫改版中徹底踢掉 xrequests 模組, 過去這一周來回翻看陳會安老師的大作 "超簡單 Python/MicroPython 物聯網應用", 覺得陳老師的書編得真好, 但此書已出版三年需要改版, 例如 IFTTT Webhook 改收費應該用其它資源取代 (例如 Make).
今天要測試的項目是書中第 9-6 與 10-5 節, 透過 OpenWeatherMap API 取得指定城市氣象資訊, 然後用 Line Notify 發送訊息, 順便測試 xtools 函式庫中的 webhook_get() 與 webhook_post() 函式. 使用 OpenWeatherMap API 須先註冊帳號取得 API key, 參考 :
以下測試是在 ESP32-WROOM-32E 開發板上進行 (MicroPython v1.23 韌體), 使用上週改版後的 xtools 函式庫, 參考 :
開發板接上筆電 USB 槽, 點選 Thonny 的 "執行/設定直譯器" :
點選 "MicroPython(ESP32)" 與勾選正確連接埠 :
然後點選 "停止/重新啟動後端程式", 這樣開發板就會軟啟動並出現互動環境提示號, 就可以輸入 Python 程式碼了. 首先匯入 xtools 函式庫與設定檔 config.py 並呼叫 xtools.connect_wifi() 連上網路後即可進行測試 :
MicroPython v1.23.0 on 2024-06-02; Generic ESP32 module with ESP32
Type "help()" for more information.
>>> import xtools
>>> import config
>>> ip=xtools.connect_wifi(config.SSID, config.PASSWORD)
Connecting to network...
network config: ('192.168.50.9', '255.255.255.0', '192.168.50.1', '192.168.50.1')
1. 取得即時氣象資訊 :
連上網路後匯入內建模組 urequests 與 ujson, 指定城市, 國家, 與 API key 製作 OpenWeatherMap API 的網址, 傳給 urequests.get() 向 API 請求提供即時的氣象資訊, 傳回值是一個 JSON 字串, 使用 ujson.loads() 將其轉成 Python 字典即可透過屬性與索引取得溫溼度氣壓等資料 :
>>> import urequests
>>> import ujson
>>> api_key='在此輸入 OpenWeatherMap 的 API key'
>>> city='Kaohsiung'
>>> country='TW'
>>> url=f'https://api.openweathermap.org/data/2.5/weather?q={city},{country}&units=metric&lang=zh_tw&appid={api_key}'
>>> r=urequests.get(url)
>>> data=ujson.loads(r.text) # 轉成字典
>>> data
{'timezone': 28800, 'rain': {'1h': 5.84}, 'cod': 200, 'dt': 1727806162, 'base': 'stations', 'weather': [{'id': 502, 'icon': '10n', 'main': 'Rain', 'description': '\u5927\u96e8'}], 'sys': {'country': 'TW', 'sunrise': 1727819425, 'sunset': 1727862335, 'id': 2002588, 'type': 2}, 'clouds': {'all': 100}, 'coord': {'lon': 120.3133, 'lat': 22.6163}, 'name': 'Kaohsiung City', 'visibility': 6655, 'wind': {'gust': 8.94, 'speed': 3.13, 'deg': 70}, 'main': {'feels_like': 24.86, 'pressure': 1003, 'temp_max': 23.91, 'temp': 23.91, 'temp_min': 23.91, 'humidity': 96, 'sea_level': 1003, 'grnd_level': 1002}, 'id': 1673820}
最後面的 id 屬性值 1673820 為城市 city 之地理編碼 (geocode), 這在後面取得未來一周氣象預報時會用到 (注意, 傳回值中有好幾個 id 屬性, geocode 是最上層的那個 id 屬性); 字典的 weather 屬性值存放溫溼度大氣壓等氣象資訊, 其中 description 欄位含有中文天氣描述之 Unicode, 用 print() 即可轉碼為中文字元 :
>>> data['weather'][0]['description'] # 氣象描述會用地區語言之 Unicode 表示
'\u5927\u96e8'
>>> print(data['weather'][0]['description'])
大雨
>>> print(data['main']['temp'])
23.91
>>> print(data['main']['temp_min'])
23.91
>>> print(data['main']['temp_max'])
23.91
>>> print(data['main']['pressure'])
1003
>>> print(data['main']['humidity'])
96
>>> geocode=data['id'] # 城市 city='Kaohsiung' 的地理編碼
>>> geocode
1673820
可以將溫濕度與氣壓等訊息透過 xtools.line_msg() 傳到手機 LINE 上 :
>>> token='在此輸入 LINE Notify token'
>>> weather=f"\n{city} 天氣: {data['weather'][0]['description']}\n" +\
f"氣溫: {data['main']['temp']}\n" +\
f"濕度: {data['main']['humidity']}\n" +\
f"氣壓: {data['main']['pressure']}"
>>> xtools.line_msg(token, weather)
Message has been sent.
結果如下 :
可以將上面的程式碼寫成函式 get_weather_now() :
def get_weather_now(country, city, api_key):
url=f'https://api.openweathermap.org/data/2.5/weather?q={city},{country}&units=metric&lang=zh_tw&appid={api_key}'
try:
res=urequests.get(url)
data=ujson.loads(res.text)
if data['cod']==200: # 注意是數值
ret={'geocode': data['id'],
'description': data['weather'][0]['description'],
'temperature': data['main']['temp'],
'min_temperature': data['main']['temp_min'],
'max_temperature': data['main']['temp_max'],
'pressure': data['main']['pressure'],
'humidity': data['main']['humidity']}
return ret
else:
return None
except Exception as e:
return None
注意這裡 MicroPython 只能捕捉 Exception, 因為它沒有實作 Exceptions 類別. 使用時先匯入 urequests 與 ujson, 定義 country, city, 與 api_key 變數, 呼叫 get_weather_now() 時將這些變數傳進去就可以取得氣象資料字典 :
>>> import urequests
>>> import ujson
>>> api_key='在此輸入 OpenWeatherMap 的 API key'
>>> city='Kaohsiung'
>>> country='TW'
>>> r=get_weather_now(country, city, api_key)
>>> r
{'presure': 1002, 'temperature': 25, 'min_temperature': 23.05, 'humidity': 91, 'geocode': 1673820, 'description': '\u9663\u96e8', 'max_temperature': 25.02}
>>> print(r['description'])
陣雨
>>> r['geocode']
1673820
2. 取得未來五日氣象預報資訊 :
也可以利用上面即時氣象查得的 geocode 與 API key 去查詢 API 取得未來一周天氣預報資料, 氣象預報 API 網址格式如下 :
url=f'http://api.openweathermap.org/data/2.5/forecast?id={geocode}&appid={api_key}'
其中 geocode 為城市之地理編碼, 也就是上面查詢即時氣象時傳回值中的 id 屬性.
>>> geocode=1673820 # 高雄市之地理編碼
>>> api_key='在此輸入 OpenWeatherMap 的 API key'
>>> url=f'http://api.openweathermap.org/data/2.5/forecast?id={geocode}&appid={api_key}'
>>> r=urequests.get(url)
>>> predict=ujson.loads(r.text)
>>> len(predict)
5
>>> predict.keys()
dict_keys(['cnt', 'list', 'city', 'message', 'cod'])
可見傳回值字典含有 5 個屬性, 其中 city 是關於城市的經緯度, 國家, 時區等地理資訊 :
>>> predict['city']
{'coord': {'lat': 22.6163, 'lon': 120.3133}, 'id': 1673820, 'name': 'Kaohsiung City', 'sunrise': 1727819425, 'sunset': 1727862335, 'country': 'TW', 'timezone': 28800, 'population': 0}
氣象預報資料放在 list 屬性裡, 其值為長度 40 的串列, 氣象預報資料為間隔 3 小時預報一次, 一天會有 8 筆資料, 五天就是 40 筆 :
>>> len(predict['list'])
40
檢視第一筆與最後一筆預報資料 :
>>> predict['list'][0]
{'sys': {'pod': 'n'}, 'visibility': 2758, 'pop': 1, 'rain': {'3h': 21.77}, 'dt': 1727870400, 'main': {'pressure': 1001, 'grnd_level': 1001, 'temp_kf': 0.21, 'feels_like': 298.65, 'sea_level': 1001, 'temp_max': 297.73, 'temp_min': 297.52, 'temp': 297.73, 'humidity': 92}, 'clouds': {'all': 100}, 'weather': [{'id': 502, 'icon': '10n', 'main': 'Rain', 'description': 'heavy intensity rain'}], 'wind': {'gust': 18.36, 'speed': 11.21, 'deg': 93}, 'dt_txt': '2024-10-02 12:00:00'}
>>> predict['list'][39]
{'sys': {'pod': 'd'}, 'visibility': 10000, 'pop': 0.37, 'dt': 1728291600, 'main': {'pressure': 1012, 'grnd_level': 1011, 'temp_kf': 0, 'feels_like': 305.52, 'sea_level': 1012, 'temp_max': 302.07, 'temp_min': 302.07, 'temp': 302.07, 'humidity': 69}, 'clouds': {'all': 9}, 'weather': [{'id': 800, 'icon': '01d', 'main': 'Clear', 'description': 'clear sky'}], 'wind': {'gust': 2.72, 'speed': 3.07, 'deg': 275}, 'dt_txt': '2024-10-07 09:00:00'}
主要欄位如下 :
- 'dt_txt' : 預報時間
- 'main' : 主要之氣象資訊, 放在子欄位 'temp' (華氏溫度), 'pressure' (大氣壓), 'humidity' (濕度)
- 'weather' : 子欄位 'main' 為天氣主分類, 'description' 為較詳細描述
例如 :
>>> predict['list'][0]['dt_txt']
'2024-10-02 12:00:00'
>>> predict['list'][0]['main']['temp']
297.73
>>> predict['list'][0]['main']['humidity']
92
>>> predict['list'][0]['main']['pressure']
1001
>>> predict['list'][0]['weather'][0]['main']
'Rain'
>>> predict['list'][0]['weather'][0]['description']
'heavy intensity rain'
以上程式碼可以寫成函式 get_weather_forecast() :
def get_weather_forecast(geocode, api_key):
url=f'http://api.openweathermap.org/data/2.5/forecast?id={geocode}&appid={api_key}'
try:
res=urequests.get(url)
data=ujson.loads(res.text)
ret=[]
if data['cod']=='200': # 注意是字串
for i in data['list']:
d={'time': i['dt_txt'],
'temperature': round(i['main']['temp']-273.15), # 卡式溫度轉攝氏
'humidity': i['main']['humidity'],
'pressure': i['main']['pressure'],
'description': i['weather'][0]['description']}
ret.append(d)
return ret
else:
return None
except Exception as e:
return None
注意, OpenWeatherMap 的未來五天氣象預報 API 與即時氣象 API 的傳回值有三個不同 :
- 即時氣象的溫度為攝氏溫度, 未來五天的卻是卡氏絕對溫度.
- 即時氣象的回應狀態碼 cod 欄位值為數值 (例如 200), 未來五天的卻是字串 (例如 '200').
- 即時氣象的天氣描述 (description) 為中文; 未來五天的是英文,
此函式已將卡氏溫度轉成攝氏溫度.
只要匯入 urequests 與 ujson, 設定 geocode 與 api_key 變數, 將其傳入 get_weather_forecast() 即可取得未來五天的氣象預報資料字典串列 :
>>> import urequests
>>> import ujson
>>> api_key='在此輸入 OpenWeatherMap 的 API key'
>>> r=get_weather_forecast(geocode, api_key)
>>> r
[{'temperature': 25, 'pressure': 1001, 'description': 'heavy intensity rain', 'time': '2024-10-02 15:00:00', 'humidity': 87}, {'temperature': 25, 'pressure': 1000, 'description': 'moderate rain', 'time': '2024-10-02 18:00:00', 'humidity': 88}, {'temperature': 25, 'pressure': 998, 'description': 'heavy intensity rain', 'time': '2024-10-02 21:00:00', 'humidity': 90}, {'temperature': 25, 'pressure': 995, 'description': 'heavy intensity rain', 'time': '2024-10-03 00:00:00', 'humidity': 91}, {'temperature': 24, 'pressure': 995, 'description': 'heavy intensity rain', 'time': '2024-10-03 03:00:00', 'humidity': 92}, {'temperature': 24, 'pressure': 999, 'description': 'very heavy rain', 'time': '2024-10-03 06:00:00', 'humidity': 93}, {'temperature': 24, 'pressure': 1005, 'description': 'heavy intensity rain', 'time': '2024-10-03 09:00:00', 'humidity': 93}, {'temperature': 24, 'pressure': 1008, 'description': 'moderate rain', 'time': '2024-10-03 12:00:00', 'humidity': 91}, {'temperature': 26, 'pressure': 1009, 'description': 'light rain', 'time': '2024-10-03 15:00:00', 'humidity': 87}, {'temperature': 25, 'pressure': 1009, 'description': 'light rain', 'time': '2024-10-03 18:00:00', 'humidity': 86}, {'temperature': 25, 'pressure': 1009, 'description': 'light rain', 'time': '2024-10-03 21:00:00', 'humidity': 85}, {'temperature': 25, 'pressure': 1011, 'description': 'overcast clouds', 'time': '2024-10-04 00:00:00', 'humidity': 83}, {'temperature': 27, 'pressure': 1012, 'description': 'overcast clouds', 'time': '2024-10-04 03:00:00', 'humidity': 75}, {'temperature': 27, 'pressure': 1010, 'description': 'overcast clouds', 'time': '2024-10-04 06:00:00', 'humidity': 73}, {'temperature': 27, 'pressure': 1010, 'description': 'overcast clouds', 'time': '2024-10-04 09:00:00', 'humidity': 76}, {'temperature': 26, 'pressure': 1012, 'description': 'light rain', 'time': '2024-10-04 12:00:00', 'humidity': 80}, {'temperature': 26, 'pressure': 1012, 'description': 'overcast clouds', 'time': '2024-10-04 15:00:00', 'humidity': 80}, {'temperature': 26, 'pressure': 1010, 'description': 'overcast clouds', 'time': '2024-10-04 18:00:00', 'humidity': 81}, {'temperature': 26, 'pressure': 1011, 'description': 'overcast clouds', 'time': '2024-10-04 21:00:00', 'humidity': 81}, {'temperature': 27, 'pressure': 1012, 'description': 'overcast clouds', 'time': '2024-10-05 00:00:00', 'humidity': 76}, {'temperature': 29, 'pressure': 1012, 'description': 'overcast clouds', 'time': '2024-10-05 03:00:00', 'humidity': 66}, {'temperature': 30, 'pressure': 1010, 'description': 'light rain', 'time': '2024-10-05 06:00:00', 'humidity': 67}, {'temperature': 29, 'pressure': 1010, 'description': 'light rain', 'time': '2024-10-05 09:00:00', 'humidity': 70}, {'temperature': 29, 'pressure': 1012, 'description': 'light rain', 'time': '2024-10-05 12:00:00', 'humidity': 73}, {'temperature': 29, 'pressure': 1012, 'description': 'light rain', 'time': '2024-10-05 15:00:00', 'humidity': 74}, {'temperature': 28, 'pressure': 1011, 'description': 'light rain', 'time': '2024-10-05 18:00:00', 'humidity': 74}, {'temperature': 28, 'pressure': 1012, 'description': 'light rain', 'time': '2024-10-05 21:00:00', 'humidity': 76}, {'temperature': 28, 'pressure': 1013, 'description': 'light rain', 'time': '2024-10-06 00:00:00', 'humidity': 73}, {'temperature': 30, 'pressure': 1013, 'description': 'scattered clouds', 'time': '2024-10-06 03:00:00', 'humidity': 67}, {'temperature': 30, 'pressure': 1011, 'description': 'scattered clouds', 'time': '2024-10-06 06:00:00', 'humidity': 65}, {'temperature': 29, 'pressure': 1011, 'description': 'light rain', 'time': '2024-10-06 09:00:00', 'humidity': 68}, {'temperature': 29, 'pressure': 1013, 'description': 'light rain', 'time': '2024-10-06 12:00:00', 'humidity': 73}, {'temperature': 28, 'pressure': 1013, 'description': 'light rain', 'time': '2024-10-06 15:00:00', 'humidity': 74}, {'temperature': 28, 'pressure': 1012, 'description': 'overcast clouds', 'time': '2024-10-06 18:00:00', 'humidity': 74}, {'temperature': 27, 'pressure': 1012, 'description': 'light rain', 'time': '2024-10-06 21:00:00', 'humidity': 74}, {'temperature': 28, 'pressure': 1014, 'description': 'overcast clouds', 'time': '2024-10-07 00:00:00', 'humidity': 71}, {'temperature': 29, 'pressure': 1013, 'description': 'overcast clouds', 'time': '2024-10-07 03:00:00', 'humidity': 68}, {'temperature': 29, 'pressure': 1011, 'description': 'overcast clouds', 'time': '2024-10-07 06:00:00', 'humidity': 69}, {'temperature': 29, 'pressure': 1011, 'description': 'overcast clouds', 'time': '2024-10-07 09:00:00', 'humidity': 70}, {'temperature': 28, 'pressure': 1013, 'description': 'broken clouds', 'time': '2024-10-07 12:00:00', 'humidity': 73}]
第一筆與最後一筆氣象預測資料如下 :
>>> r[0]
{'temperature': 25, 'pressure': 1001, 'description': 'heavy intensity rain', 'time': '2024-10-02 15:00:00', 'humidity': 87}
>>> r[39]
{'temperature': 28, 'pressure': 1013, 'description': 'broken clouds', 'time': '2024-10-07 12:00:00', 'humidity': 73}
可見溫度都已轉成攝氏了.
如果要從這 40 個預報資料中擷取固定時段的資料, 例如未來五天每天早上六點的預報資料, 由於 MicroPython 未實作 datetime 模組, 所以可以用字串拆解比對方式針對 time 欄位進行過濾, 例如 :
>>> result=[]
>>> for r in ret:
# 取出 'time' 屬性值的時間部分
time_str=r['time'].split(' ')[1] # '2024-10-02 06:00:00' -> '06:00:00'
# 比對是否為 06:00:00
if time_str == '06:00:00':
result.append(r)
>>> result
[{'temperature': 24, 'pressure': 999, 'description': 'very heavy rain', 'time': '2024-10-03 06:00:00', 'humidity': 93}, {'temperature': 27, 'pressure': 1010, 'description': 'overcast clouds', 'time': '2024-10-04 06:00:00', 'humidity': 73}, {'temperature': 30, 'pressure': 1010, 'description': 'light rain', 'time': '2024-10-05 06:00:00', 'humidity': 67}, {'temperature': 30, 'pressure': 1011, 'description': 'scattered clouds', 'time': '2024-10-06 06:00:00', 'humidity': 65}, {'temperature': 29, 'pressure': 1011, 'description': 'overcast clouds', 'time': '2024-10-07 06:00:00', 'humidity': 69}]
這樣就抓出每天 06:00 的預報資料了. 我們可以走訪此串列並製作每日 06:00 之氣象預報資訊字串, 先放在串列中再用跳行字元串接後傳給 line_msg() 傳送 :
>>> forcasts=[]
>>> for r in result:
s=f"❖{r['time']} 預報\n" +\
f"描述: {r['description']}\n" +\
f"氣溫: {r['temperature']} " +\
f"濕度: {r['humidity']} " +\
f"氣壓: {r['pressure']}"
forcasts.append(s)
>>> forcast_str='\n'.join(forcasts)
>>> token='在此輸入 LINE Notify token'
>>> xtools.line_msg(token, forcast_str)
Message has been sent.
結果如下 :
3. 測試 xtools.webhook.get() 函式 :
最後來測試 xtools 函式庫的 webhook_get() 函式, 書中原始的函式如下 :
def webhook_get(url):
print("invoking webhook")
r = urequests.get(url)
if r is not None and r.status_code == 200:
print("Webhook invoked")
else:
print("Webhook failed")
show_error()
所以其實它就是呼叫 urequests.get(), 但卻沒有傳回 r, 所以我將其修改為 :
def webhook_get(url):
print("invoking webhook")
r = urequests.get(url)
if r is not None and r.status_code == 200:
print("Webhook invoked")
else:
print("Webhook failed")
show_error()
return r
測試如下 :
>>> import xtools
>>> import config
>>> import ujson
>>> xtools.connect_wifi()
network config: ('192.168.50.9', '255.255.255.0', '192.168.50.1', '192.168.50.1')
'192.168.50.9'
>>> api_key='在此輸入 OpenWeatherMap 的 API key'
>>> city='Kaohsiung'
>>> country='TW'
>>> url=f'https://api.openweathermap.org/data/2.5/weather?q={city},{country}&units=metric&lang=zh_tw&appid={api_key}'
>>> r=xtools.webhook_get(url)
invoking webhook
Webhook invoked
>>> data=ujson.loads(r.text)
>>> data
{'timezone': 28800, 'rain': {'1h': 1.78}, 'cod': 200, 'dt': 1727931614, 'base': 'stations', 'weather': [{'id': 522, 'icon': '09d', 'main': 'Rain', 'description': '\u5927\u96e8'}], 'sys': {'country': 'TW', 'sunrise': 1727905845, 'sunset': 1727948677, 'id': 2002588, 'type': 2}, 'clouds': {'all': 100}, 'coord': {'lon': 120.3133, 'lat': 22.6163}, 'name': 'Kaohsiung City', 'visibility': 1400, 'wind': {'gust': 33.95, 'speed': 22.12, 'deg': 90}, 'main': {'feels_like': 24.62, 'pressure': 998, 'temp_max': 23.97, 'temp': 23.62, 'temp_min': 23.05, 'humidity': 99, 'sea_level': 998, 'grnd_level': 997}, 'id': 1673820}
可見與上面直接用 urequests() 是一樣的.
沒有留言:
張貼留言