2024年10月6日 星期日

MicroPython 學習筆記 : 串接物聯網雲端平台 Adafruit.IO (二)

在前一篇文章中已註冊 Adafruit.IO 帳號並建立了三個 Feed 欄位 (溫溼度與氣壓) 與其儀表板, 也已取得 AIO key, 本篇要在此基礎上用 MicroPython 程式碼將取自 OpenWeatherMap 的氣象資料傳送到這些 Feeds 上, 作法與 ThingSpeak 的類似, 但差別 Adafruit.IO 如果用 Python 的話只能用 POST 方法提交請求,  且一次請求只能上傳一個欄位, 不像 ThingSpeak 一次請求可上傳全部欄位. 



4. 使用 Adafrui.IO REST API 上傳資料 : 

Adafruit.IO 的 Python REST API 要用 POST 方法提出請求, 其教學文件的 GET 項目下有註明 'not implemented', 我測試 GET 方法雖然得到 200 回應, 但其實資料並未寫入資料庫 : 





RSET API 的 URL 網址要嵌入使用者名稱 (Usernaem) 與 FEED 的鍵 (注意不是 FEED 名稱), 最後面可以攜帶 X-AIO-Key 參數, 格式如下 : 

https://io.adafruit.com/api/v2/{USERNAME}/feeds/{FEED}/data?X-AIO-Key={AIO_KEY}   

但是將金鑰帶在 URL 參數中並不是安全的做法, 應該要放在 headers 標頭中傳遞較好, 這樣的話 URL 網址就比較單純 : 

https://io.adafruit.com/api/v2/{USERNAME}/feeds/{FEED}/data

先匯入 config 與 xtools 模組呼叫 xtools.coneect_wifi() 連上網路 : 

>>> import xtools   
>>> import config    
>>> ip=xtools.connect_wifi(config.SSID, config.PASSWORD)  
network config: ('192.168.50.9', '255.255.255.0', '192.168.50.1', '192.168.50.1')
'192.168.50.9'

設定使用者名稱與 FEED, 注意 FEED 要填入鍵而非名稱, 例如濕度的鍵是 humidity :

>>> USERNAME='yhhuang1966'   
>>> FEED='humidity'     # 填入 Feed 的鍵
>>> url=f'https://io.adafruit.com/api/v2/{USERNAME}/feeds/{FEED}/data'    
>>> url   
'https://io.adafruit.com/api/v2/yhhuang1966/feeds/humidity/data'

這樣儲存濕度的 API 網址就製作好了. 

接下來定義 AOI_KEY 變數與 HTTP 標頭字典 :

>>> AIO_KEY='在此填入 Adafruit API key'     
>>> headers={'X-AIO-Key': AIO_KEY}        

注意, 鍵的最後兩字元是小寫的 ey. 最後定義資料字典, 即要儲存之 Feed 值, 其鍵須為 value :

>>> data={'value': 87}    
>>> data   
{'value': 87}

然後呼叫 urequests.post() 將 data 字典傳給 json 參數, 標頭字典傳給 headers 參數即可 :

>>> r=urequests.post(url, headers=headers, json=data)   
>>> r.status_code      
200

可見 POST 請求成功, 這時到儀表板查看濕度的 Feed 就可以看到資料已被儲存 :




如果要再次寫入資料只要更改資料字典 data 即可, 網址不變 (Feed 同樣是濕度) : 

>>> data={'value': 92}    
>>> r=urequests.post(url, headers=headers, json=data)    
>>> r.status_code   
200

再次查看 Feed 資料就有兩筆了, 且出現折線圖 : 




所以使用 urequests.post() 呼叫 Adafruit.IO API 時要使用 json 參數直接將字典傳給 json 參數, 不要用 URL 編碼後的 bytes 資料傳給 data (這是呼叫 ThingSpeak API 的作法), 否則會得到 422 回應 (即 Adafruit 伺服器無法處理所傳送之資料) :

>>> payload=xtools.urlencode(data).encode('utf-8')   
>>> r=urequests.post(url, headers=headers, data=payload)   
>>> r.status_code   
422

如果要傳送其他 Feed 例如氣壓, 則 API 網址與資料字典要改, 但 headers 字典不用改 :

>>> FEED='pressure'    
>>> url=f'https://io.adafruit.com/api/v2/{USERNAME}/feeds/{FEED}/data'      
>>> url       
'https://io.adafruit.com/api/v2/yhhuang1966/feeds/pressure/data'
>>> data={'value': 997}   
>>> data      
{'value': 997}
>>> r=urequests.post(url, headers=headers, json=data)    
>>> r.status_code    
200

這時查看氣壓的 Feed 可見資料已寫入 : 




總之, 與 ThingSpeak 不同的是, 傳送 Adafruit.IO 的每個欄位都要做一次 HTTP 請求, 無法一次傳送多個欄位. 這可以將 Feeds 的鍵放在串列中用迴圈來一個一個處理, 每個 POST 請求間隔 2 秒以上即可 (這是免費帳戶讀寫資料庫的最高頻率限制).

接下來要複製前面在 ThingSpeak 上的實驗, 從 OpenWeatherMap 網站擷取溫溼度與氣壓資料後寫入 Adafruite.IO 的 Feeds 欄位中. 首先複製從 OpenWeatherMap 網站取得氣象資料的爬蟲程式 :

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

定義變數後呼叫 get_weather_now() 取得氣象資料 : 

>>> weather_api_key=config.WEATHER_API_KEY    
>>> city='Kaohsiung'    
>>> country='TW'   
>>> r=get_weather_now(country, city, weather_api_key)     
>>> r     
{'pressure': 1011, 'temperature': 28.94, 'min_temperature': 28.91, 'humidity': 85, 'geocode': 1673820, 'description': '\u5c0f\u96e8', 'max_temperature': 30.05}

接著定義呼叫 Adafruit.IO 所需之變數 (注意, 我已經事先將 Adafruit.IO 之金鑰寫入 config.py 設定檔的 AIO_KEY 與 AIO_USERAME 變數中) :

>>> AIO_KEY=config.AIO_KEY   
>>> USERNAME=config.AIO_USERNAME    
>>> headers={'X-AIO-Key': AIO_KEY}   
>>> FEEDS=['temperature', 'humidity', 'pressure']    

用迴圈走訪 FEEDS 串列元素 : 

>>> for FEED in FEEDS: 
    url=f'https://io.adafruit.com/api/v2/{USERNAME}/feeds/{FEED}/data'
    print(url)
    data={'value': r[FEED]}
    print(FEED, ': ', data)
    ret=urequests.post(url, headers=headers, json=data)
    print(ret.status_code)
    time.sleep(2)
    
https://io.adafruit.com/api/v2/yhhuang1966/feeds/temperature/data
temperature :  {'value': 28.94}
200
https://io.adafruit.com/api/v2/yhhuang1966/feeds/humidity/data
humidity :  {'value': 85}
200
https://io.adafruit.com/api/v2/yhhuang1966/feeds/pressure/data
pressure :  {'value': 1011}
200

這時去儀表板檢視 Feeds 列表就可看到這三筆資料都已寫入資料庫了 : 




修改後的 weather_app.py 如下 :

# weather_app.py
import xtools    
import config
import time
import urequests
import ujson

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

def main():
    weather_api_key=config.WEATHER_API_KEY      
    city='Kaohsiung'     
    country='TW'    
    while True:
        r=get_weather_now(country, city, weather_api_key)
        if r != None:
            AIO_KEY=config.AIO_KEY
            USERNAME=config.AIO_USERNAME
            USERNAME='yhhuang1966'          
            headers={'X-AIO-Key': AIO_KEY}  
            FEEDS=['temperature', 'humidity', 'pressure']
            for FEED in FEEDS:
                url=f'https://io.adafruit.com/api/v2/{USERNAME}/feeds/{FEED}/data'
                data={'value': r[FEED]}
                ret=urequests.post(url, headers=headers, json=data)
                print(ret.text)
                time.sleep(2)
            time.sleep(60)
        else:
            print('Fail to get weather data.')

if __name__ == '__main__':  
    main()  
      
程式中設定每分鐘會去 OpenWeatherMap 爬一次高雄的氣象資料, 然後分三次將溫溼度與氣壓這三個欄位值上傳到 Adafruit.IO 的 Feeds 裡儲存. 此程式 weather_app.py 是從主程式 main.py 呼叫執行的, 當 WiFi 連線成功時會呼叫 weather_app.main() :

# main.py
import xtools    
import config
import weather_app    

ip=xtools.connect_wifi(config.SSID, config.PASSWORD)
mac=xtools.get_id()
print('ip: ', ip)
print('mac: ', mac)
if not ip: # 連線失敗傳回 None
    print('無法連線 WiFi 基地台')
else:      # 連線成功傳回 ip 字串: 執行 app
    weather_app.main() # 連線成功才執行 App 程式

當然還須要搭配 xtools.py 函式庫與設定檔 config.py, 內容如下 :

# config.py
SSID=''
PASSWORD=''
LINE_NOTIFY_TOKEN=''
OPENAI_API_KEY=''
THINGSPEAK_WRITE_API_KEY=''
WEATHER_API_KEY=''    
AIO_USERNAME=''   
AIO_KEY=''   

注意, 上傳到開發板之前須先將 config.py 裡面的 WiFi SSID 帳密, 以及 WEATHER_API_KEY, AOI_USER_NAME, AOI_KEY 正確填好, 開機後程式才會順利執行. 我下午五點多上傳程式到開發板後按 Reset 鈕讓程式跑了快一個半小時結果如下 : 




 

可見傍晚太陽下山後溫度開始下降, 而濕度上升是因為大約四點就開始下大雨之故. 

以上測試所用之全部檔案壓縮後放在 GitHub :


沒有留言 :