2024年10月2日 星期三

MicroPython 學習筆記 : 從 OpenWeather 網站取得氣象資料

今天是山陀兒颱風假的第二天, 昨天在家完成兩套公司維運軟體改版, 今天要來做自己的 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() 是一樣的. 

沒有留言:

張貼留言