Fyzikální kabinet FyzKAB
TechHobby ESP32 + MicroPython ESP32: Wi-Fi v MicroPythonu ESP32: Načtení JSON (meteo) dat

ESP32: Načtení JSON dat z internetu

aktualizováno: 20. 2. 2025

Pokud jsme zvládli v MicroPyhonu vytvořit program, který nám modul ESP32 proměnil v jednoduchého webového klienta, který načetl zvolený textový soubor umístěný na internetu, můžeme tento kód povýšit na „užitečnější“ úroveň.

V dnešní digitální době je načítání dat z internetu klíčovou součástí mnoha aplikací a webových služeb. Ať už se jedná o zobrazování aktuálních zpráv, informací o počasí nebo údajů z databází. Jedním z nejpoužívanějších formátů pro přenos těchto dat je tzv. JSON (JavaScript Object Notation). Tento formát je lehký, snadno čitelný a široce podporovaný napříč různými programovacími jazyky.

Struktura souboru JSON je jednoduchá a čitelná jak pro lidi, tak pro stroje, což z něj činí oblíbený formát pro přenos dat přes internet. JSON používá dvojice klíč-hodnota, přičemž data mohou být uspořádána do objektů (označených složenými závorkami {}) nebo polí (hranaté závorky []). Díky své univerzálnosti je JSON podporován většinou moderních programovacích jazyků a často se používá v API pro komunikaci mezi webovými službami a aplikacemi.

Příklad jednoduchého JSON souboru obsahujícího informace o uživateli:

{
  "jmeno": "Jan Novák",
  "vek": 30,
  "email": "jan.novak@example.com",
  "adresa": {
      "ulice": "Hlavní 123",
      "mesto": "Praha",
      "psc": "11000"
   },
  "telefonni_cisla": ["+420123456789", "+420987654321"]
}

V Pythonu lze pracovat s JSON daty pomocí modulu json. Pokud jsou data uložena jako řetězec nebo načtena z API, můžeme je snadno převést na slovník a přistupovat k jednotlivým hodnotám. Jako ukázku si můžeme kód, který z textu obsahující JSON strukturu (proměnná json_data) získá informace do použitelné pythonovské struktury, se kterou se dá nadále pracovat.

import json     # modul pro klasický Python
# import ujson as json    # modul pro MicroPython

# JSON data jako řetězec
json_data = '''
  {
  "jmeno": "Jan Novák",
  "vek": 30,
  "email": "jan.novak@example.com",
  "adresa": {
      "ulice": "Hlavní 123",
      "mesto": "Praha",
      "psc": "11000"
   },
  "telefonni_cisla": ["+420123456789", "+420987654321"]
  }
'''

# Parsování JSON do slovníku
uzivatel = json.loads(json_data)

# Získání základních informací
print("Jméno:", uzivatel["jmeno"])
print("Věk:", uzivatel["vek"])
print("E-mail:", uzivatel["email"])
print("Město:", uzivatel["adresa"]["mesto"])
print("První telefonní číslo:", uzivatel["telefonni_cisla"][0])

Výše uvedený kód je pro použití v klasickém Pythonu, chceme-li jej použít na modulu ESP32, kde je nainstalován MicroPython, vypustíme připojení modulu json na první řádce a naopak zrušíme komentář na řádce druhé.

import ujson as json

MicroPython používá odlehčenou verzi standardního Python modulu json, která je dostupná pod názvem ujson. Zde zvolený alias json umožní, aby byl kód přenosnější mezi MicroPythonem a běžným Pythonem, kde se používá standardní json modul. Podobnou techniku jsme kupříkladu užili v minulém článku u knihovny socket a usocket. Tento přístup je užitečný, pokud chcete psát kód, který bude fungovat v obou prostředích s minimálními úpravami.

Python ve svém modulu json, respektive Micropython v knihovně ujson, nabízí metodu loads(), která nám umožní „JSONovský“ text, za určitých podmínek (!), převést na Pythonem zpracovatelnou podobu. Tyto jednoduché přístupy umožňují snadno extrahovat potřebná (vhodná) data a dále s nimi pracovat v aplikacích.

Načítání JSON dat z API – příklad s 'open-meteo.com'

Nyní si na praktickém příkladu načtení JSON dat z webové služby ukážeme, jak pracovat s JSON v MicroPythonu na modulu ESP32. Při práci s IoT zařízeními často potřebujeme získávat data z internetu, například o počasí, cenách kryptoměn nebo jiných senzorických hodnotách. Pro tuto ukázku využijeme veřejné meteorologické API (Application Programming Interface čili aplikační programové rozhraní) webu open-meteo.com, které nevyžaduje žádnou registraci ani API klíč, což z něj činí ideální volbu pro naše experimentování!

Open-Meteo poskytuje bezplatná data o počasí, která lze snadno získat prostřednictvím jednoduchých HTTP požadavků. API vrací data ve formátu JSON, což znamená, že je můžeme zpracovat pomocí vestavěného MicroPython modulu ujson. V našem příkladu se připojíme k open-meteo API, získáme aktuální teplotu pro konkrétní souřadnice a zobrazíme ji na výstupu. Tento přístup lze snadno rozšířit o další meteorologické údaje, jako je vlhkost vzduchu, rychlost větru nebo předpověď na několik dní dopředu.

V minulém článku jsme si vytvořili jednoduchý webový klient, který nyní zkusíme využít. Pro odeslání požadavku na server open-meteo.com budeme potřebovat několik informací. Jaké konkrétní informace musíme API dodat, je naštěstí uvedeno v dokumentaci přímo na webu https://open-meteo.com/en/docs. V našem nejjednodušším případě to jsou:

  • Host: Adresu webu (serveru) poskytující službu navracející JSON data (zde: api.open-meteo.com)
  • Cesta k souboru: Cesta ke zdroji, který nám navrátí požadovaná data (zde: /v1/forecast). Jelikož voláme službu metodou GET, musí být zde zadána formou parametrů ještě určitá sada doplňujících informací:
    • Doplňující parametry zadáváme formou parametrů v URL.
      Musíme minimálně zadat GPS souřadnice místa a specifikovat požadovanou sadu JSON dat, konkrétně:
      • latitude: zeměpisná šířka místa, pro které požadujeme informace
      • longitude: zeměpisná délka tamtéž
      • název konkrétní služby (zde current – aktuální stav), hodnota pro teplotu je temperature_2m)

Bude-li nás zajímat aktuální počasí pro Prahu, konkrétně pro GPS souřadnice baziliky sv. Ludmily, budeme se dotazovat následující URL:

https://api.open-meteo.com/v1/forecast?latitude=50.0754669&longitude=14.4371797&current=temperature_2m

Pokud si tuto adresu nyní zadáme do webového prohlížeče, získáme následující výsledek:

JSON vystup z API serveru
Obrázek č. 1 – Načtení JSON dat z meteowebu open-meteo.com do webového prohlížeče

Vidíme, že po zaškrtnutí naformátovaného výstupu jde o docela „sympatickou“ strukturu dat. Můžeme z těchto dat na první pohled vyčíst mnoho informací. Například: GPS souřadnice meteorologické stanice, která byla vyhodnocena jako nejbližší, tedy její data jsou pro naše místo prezentována – zde se (prý) jedná o nějakou stanici kdesi na Hlavním nádraží. Dále vidíme, že hodnoty teploty jsou zaslány ve stupních Celsia, že čas měření je uveden v GMT (ale to bychom mohli změnit zadáním parametru při dotazu). Taktéž zde vidíme, že hodnoty jsou aktualizovány jednou za 900 vteřin. Ale především vidíme, že aktuální teplota je –1,7 °C.

Pokud toto dokážeme vyčíst my, pojďme to naučit náš modul ESP32! Začneme tím, že program (podobně jako právě použitý webový prohlížeč) data načte a zobrazí. Pak se uvidí:

# pripojeni knihoven
import network
import time
import sys
try:
    import ujson as json
except:
    import json
try:
    import usocket as socket
except:
    import socket

# Nastavení Wi-Fi
ssid = "nazev-WiFi-site"
pwd = "Heslo-k-WiFi-siti"

# GPS souřadnice
latitude = 50.0754669   # Např. Bazilika sv. Ludmily (Praha)
longitude = 14.4371797

# Požadavek na API Open-Meteo
host = 'api.open-meteo.com'
path = f'/v1/forecast?latitude={latitude}&longitude={longitude}&current=temperature_2m'

# --------------------------------------------------------

def exit_program():
    print("Ukoncuji program...")
    sys.exit(0)

connect_status = {
    network.STAT_IDLE: "zadna aktivita",           # STAT_IDLE - žádné připojení a žádná aktivita
    network.STAT_CONNECTING: "pripojeni probiha",  # STAT_CONNECTING - probíhá připojení
    network.STAT_WRONG_PASSWORD: "chybne heslo",   # STAT_WRONG_PASSWORD - selhalo z důvodu nesprávného hesla
    network.STAT_NO_AP_FOUND: "AP nenalezen",      # STAT_NO_AP_FOUND - selhalo, nebyl nalezen přístupový bod
    network.STAT_GOT_IP: "Uspesne pripojeni"       # STAT_GOT_IP - připojení se podařilo
}

# pripojeni k Wi-Fi
sta_if = network.WLAN(network.STA_IF)   # nastaveni rezimu STA
sta_if.active(True)                     # zapnuti sitoveho pripojeni

if not sta_if.isconnected():     # neni-li ESP32 pripojen, zacne se pripojovat
    sta_if.connect(ssid, pwd)    # autorizace pristupu
    print("Pripojuji se k Wi-Fi")
    n = 0
    while (not sta_if.isconnected()) and (n < 10):   # cekaci smycka
        n += 1                                        # timeout
        print(".", end="")
        time.sleep(1)

sta_ifstatus = sta_if.status()
print("\nVysledek pripojeni:", connect_status[sta_ifstatus])
if (sta_ifstatus == network.STAT_GOT_IP):
    STAconf = sta_if.ifconfig()     # nacteni informaci pripojeni
    print("\nSTA-IP:\t\t", STAconf[0], "\nSTA-NETMASK:\t", STAconf[1], \
          "\nSTA-GATEWAY:\t", STAconf[2], "\nSTA-DNS:\t", STAconf[3], sep="")
    print("---")
else:
    print("KONEC pripojovani!")
    sta_if.active(False)
    exit_program()

# --------------------------------------------------------

# klient, nacteni stranky z URL
url = f"http://{host}{path}"
print("Stahuji data z:", url)

# Vytvoření soketu a připojení k serveru
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((host, 80))

# Sestavení HTTP GET požadavku
request = f"GET {path} HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\n\r\n"

# Odeslání požadavku na server
sock.send(request.encode())     # převod do bytového řetězce

# Příjem odpovědi
response = b""
while True:
    data = sock.recv(1024)
    if not data:
        break
    response += data

# Uzavření soketu
sock.close()
sta_if.active(False)     # ESP32 vypne Wi-Fi adaptér

# Rozdělení odpovědi na hlavičky a tělo
header, body = response.split(b'\r\n\r\n', 1)
body = body.decode('utf-8')

print(body)   # vypis tělo zprávy, ať je vidět, co se bude zpracovávat

Připojení k Wi-Fi (už zase je něco jinak?)

První část programu již tradičně nastavuje modul ESP do režimu STA a připojuji jej Wi-Fi síti. Asi není třeba připomínat, že je třeba do programu zadat přístupové údaje do proměnných ssid a pwd. Abychom (asi již tradičně) i v této neustále se opakující části udělali vždy nějakou změnu, která nám ukáže něco nového z MicroPythonu, opět jsme tuto část kódu trochu změnili. V minulé verzi kódu připojování k Wi-Fi jsme v případě neúspěchu zastavili program nekonečnou smyčkou. Dnes jsme se rozhodli zastavit program definitivně. Pro tento účel jsme si definovali proceduru exit_program(), která využívá metody exit() z knihovny sys. Její zavolání s argumentem 0 ukončí celý program. Jen kvůli tomuto účelu je do programu přidána knihovna sys.

Pozorný čtenář minulých článků by jistě věděl, jak z této knihovny importovat jen metody exit samotnou. Že by zase nějaký domácí úkol? 😉

Mluvíme-li o knihovnách (nebo též, jak by se v Pythonu asi mělo spíše říkat: „modulech“) podívejte se na připojení knihoven ujson a usocket. Jak je to s některými knihovnami v Pythonu a MicroPythonu jsme si již říkali, dokonce jsme si ukazovali přealiasování knihoven MicroPythonu, aby bylo možné kód psát pro Python a fungoval v MicroPythonu. Zatím jsme před nahráním programu do zařízení museli měnit tento import. Zde prezentovaný způsob je takový automatický způsob. Využili jsme pro něj konstrukci try/except. Jak to funguje? Pokusíme se v sekci try aliasovat knihovnu MicroPythonu. Pokud program běží v MicroPythonu, dojde k přiřazení aliasu a knihovna běží pod pythonovským jménem. Je-li program spuštěn v klasickém Pythonu, dojde k chybě, takže se spustí sekce except, ve které se importuje standardní pythonovská knihovna.

Klient připojení k API meteoserveru

Mnohem důležitější je druhá část programu, která odešle požadavek na meteoweb a zatím jen vypíše navrácenou odpověď. Pro vytvoření dotazu slouží následující řádky kódu:

# GPS souřadnice
latitude = 50.0754669  # Např. Bazilika sv. Ludmily (Praha)
longitude = 14.4371797

# Požadavek na API Open-Meteo
host = 'api.open-meteo.com'
path = f'/v1/forecast?latitude={latitude}&longitude={longitude}&current=temperature_2m'

Proměnné latitude a longitude určují místo, na kterém budeme chtít určit teplotu. Tyto údaje budeme muset vložit do dotazovacího řetězce. Dále víme, že pro odeslání dotazu potřebujeme adresu serveru, zde uloženo v proměnné host, a zbytek dotazu, který vytváříme v proměnné path. U vytvoření řetězce dotazu v proměnné path se trochu zastavíme. Asi nás trochu zarazí znak f před úvodním apostrofem, stejně tak výrazy ve složených závorkách v řetězci. Zápis s f před řetězcem je další z možností, jak složit řetězec z textů a proměnných. Ve své podstatě to říká, že výsledný řetězec bude složen z textu, ve kterém na místě složených závorek bude vložena hodnota proměnné, která je v těchto závorkách uzavřena. Např. je-li zde {latitude} znamená to, že na toto místo bude vložena hodnota 50.0754669. Výsledný řetězec tedy bude vypadat tak, jak potřebujeme: /v1/forecast?latitude=50.0754669&longitude= atd.

Další část programu je v podstatě stejný webový klient, jako jsme používali v minulém článku.

Jsou zde ale dvě docela důležité změny:

  1. V naší cestě k univerzálnosti zde netušíme, jak dlouhá může být odpověď. U minulého načtení textového souboru by vlastně mohlo být jedno, kdybychom jej „usekli“ po 1024 znacích. Zde by to mohlo znamenat zásah do JSON kódu. Následné rozebrání useknutého JSON kódu by vyvolalo syntaktickou chybu. To nemluvě o tom, že naše požadovaná hodnota teploty je až na samém konci, takže bychom se o ni mohli tímto způsobem pěkně připravit. Je tedy třeba načíst odpověď celou.

    Odpověď načítáme do proměnné response, která je na začátku definována jako prázdný bytový řetězec. Dílčí část odpovědi serveru načítáme po 1024 znacích do pomocné proměnné odpovědi data, kterou pak připojujeme k proměnné response. Jakmile již není co načíst do proměnné data, tato proměnná získá hodnotu False, čímž se cyklus while ukončí (příkaz break).

# Příjem odpovědi
response = b""
while True:
    data = sock.recv(1024)
    if not data:
        break
    response += data

  1. Přidání řádky Connection: close do odeslaného požadavku metodou send()

    request = f"GET {path} HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\n\r\n"

    Tato hlavička souvisí s tím, jak HTTP protokol zachází s připojeními a jak servery obvykle zacházejí s požadavky, které nemají explicitně uvedený hlavičkový řádek Connection: close. Když v požadavku explicitně uvedeme hlavičku Connection: close, říkáme serveru, že po zpracování požadavku by měl server zavřít připojení. Server ví, že po odpovědi na tento požadavek může okamžitě ukončit spojení, čímž se uvolní prostředky a ušetří čas čekání na další data nebo žádosti. Pokud tuto hlavičku neuvedeme, server si může myslet, že máme v úmyslu udržet spojení otevřené pro více požadavků (což je v případě HTTP/1.1 běžná praxe). To znamená, že server si ponechá připojení otevřené pro další požadavky, které mohou následovat. Některé servery tak mohou zůstat „v očekávání“ dalšího požadavku (i když žádný už nepřijde) a nezavřou spojení, což může způsobit prodloužený čas čekání. Tento čas může být nečekaně dlouhý (několik sekund), což výrazně zpomalí celou odpověď.

Jakmile máme v proměnné response načtenou celou odpověď serveru, vše běží, jak jsme zvyklí. Dojde k rozdělení odpovědi na hlavičky a na tělo. Ještě předtím, než část body vypíšeme na výstup, musíme ji převést na text. Na to jsme používali metodu decode(). Tu použijeme i dnes, jen ji ještě doplníme o argument udávající požadovanou znakovou sadu.

body = body.decode('utf-8')

Nyní by v proměnné body měl být textový řetězec v kódování UTF-8, který odpovídá textové podobě odpovědi zaslané API daného meteoserveru.

Pojďme se podívat, jak to bude vypadat. Obrázek č. 2 nám znázorňuje vypsání odpovědi serveru ve výstupu prostředí Thonny:

Nacteni meteodat - RAW
Obrázek č. 2 – Výpis těla odpovědi meteowebu open-meteo.com načtené modulem ESP32 (REPL prostředí Thonny)

To, co nás asi zarazí, je hodnota 142 před strukturou JSON a 0 na konci. Bohužel přítomnost těchto hodnot totálně zničí JSON formát a jakýkoliv pokus o dekódování takového výstupu v tuto chvíli skončí chybou. Nejdříve jsme si mysleli, že je to nějaký problém, který vznikl při použití funkce decode a převodu na UTF-8. Ale pokud si vypíšete kompletní odpověď serveru, tedy proměnnou response, uvidíme ve výstupu, že hodnota (tentokráte!) 143 a závěrečná 0 je již tam přítomna (viz obrázek č. 3).

Podivne hodnoty ve vystupu
Obrázek č. 3 – Výpis originálního bytového řetězce odeslaného z API meteoserveru

Ale, co teď s tím?

Nezbývá než obsah proměnné response (resp. body) trochu „očistit“ do podoby formátu JSON.

AKTUALIZACE: (20. 2. 2025)
Nejdříve jsme vůbec nechápali, kde se v odpovědi meteoserveru objevila na začátku hodnota 142 a na konci 0. Pak jsme se dozvěděli, že za to může hlavička Transfer-Encoding: chunked.
Pokud server nezná předem velikost těla odpovědi (například při generování dynamického obsahu, což je přesně tento případ), může použít metodu „chunked transfer encoding“, což znamená, že tělo odpovědi bude posíláno po částech ve formě částí (tzv. „chunků“). První číslo (142) pak značí délku prvního „chunku“, 0 pak označuje konec těla přenosu.
Knihovna usocket v MicroPythonu je relativně nízkoúrovňová a neobsahuje přímou podporu pro dekódování Transfer-Encoding: chunked, jak to dělají vyšší knihovny (například urequests – viz dále). Jakmile tedy server odpoví s Transfer-Encoding: chunked, musíme dekódovat odpověď ručně.
Poznámka:
V případě, že chcete zajistit (spíše „poprosit“), aby server odeslal odpověď bez použití Transfer-Encoding: chunked, místo toho použil Content-Length: (tělo odpovědi přeneseno jedním blokem), můžete to provést zasláním hlavičky Connection: close. Tato hlavička naznačuje serveru, že po odeslání odpovědi by měl uzavřít spojení. Některé (!) servery pak (prý) nepoužijí Transfer-Encoding: chunked.
ALE: Zde jsme Connection: close v requestu použili, a vidíme, že nám to stejně nebylo nic platné! 😡

Získání „čistého“ JSON formátu.

Díky této patálii musíme k našemu kódu doplnit následující část kódu. Vysvětlíme si jej až za ukázkou kódu:

# Očistíme odpověď: odstraníme čísla na začátku a na konci

# 1. Najdeme první výskyt "{" a poslední výskyt "}"
start_index = body.find("{")
end_index = body.rfind("}")

# Pokud nalezneme validní JSON, extrahujeme ho
if ((start_index != -1) and (end_index != -1)):
    json_str = body[start_index:end_index+1]
else:
    print("Chyba: Není možné najít platný JSON.")
    json_str = ""

Možná nám už komentáře v kódu naznačily, co se zde děje. Je třeba v textovém řetězci najít první výskyt znaku {, který určuje začátek JSON formátu. K tomu využíváme řetězcové metody find() s argumentem hledaného znaku.

start_index = body.find("{")

Obdobně potřebujeme najít poslední výskyt znaku }, který naopak strukturu JSON uzavírá. Také najdeme první výskyt tohoto znaku, ale tentokráte od konce. Použijeme tedy metodu rfind(), která hledá v řetězci zprava.

Následuje vyříznutí správného úseku textu. Na to použijeme následující příkaz:

json_str = body[start_index:end_index+1]

Zápis „vyřezávacího intervalu“ nás svou podobou asi trochu přenáší do Excelu, kde se také dvojtečkou zadává nějaký rozsah. Tento zápis tedy říká: „Vyřízni z proměnné body vše od prvního výskytu { do posledního výskytu }. A přidej jeden znak, ať se tam vejde i ten znak }“.

Celé to je ovšem ještě vnořeno do podmínky, která testuje, zda vůbec taková operace může proběhnout.

if ((start_index != -1) and (end_index != -1)):

Pokud by se nepodařilo ve výchozím textu najít některý ze znaků {}, byla by do proměnných jejich umístění (start_index, end_index) dosazena návratová hodnota –1 a je jasné, že z textu validní JSON nedostaneme. V tomto případě je výstupní proměnná json_str prázdná, jinak obsahuje právě získaný JSON kód.

Jakmile máme v proměnné json_str čistý JSON formát, můžeme jej převést na pythonovsky zpracovatelný formát. Následující část kódu ukazuje jak na to:

# Hledání teploty v těle odpovědi
if json_str:
    try:
        data = json.loads(json_str)
        if (("current" in data) and ("temperature_2m" in data["current"])):
            temp = data["current"]["temperature_2m"]
            print(f"\nAktuální teplota: {temp} °C")
        else:
            print("Chyba: Nepodařilo se získat teplotu.")
    except Exception as e:
        print("Chyba při stahování dat:", e)

Převod z JSON probíhá pochopitelně, jen pokud máme v proměnné json_str „cosi“ uzavřeného ve složených závorkách. Proto je celá část vnořena do podmínky testující neprázdnost této proměnné.

Zajímavá je asi jen podoba podmínky: (pochopitelně by bylo možné to zapsat mnohem konvenčnějším způsobem)

if json_str:

Nás však zajímá metoda loads() knihovny json (resp. ujson):

data = json.loads(json_str)

Tímto příkazem jsem dosáhli svého – máme kompletní strukturu JSON převedenou do struktury proměnné data. K jednotlivým položkám můžeme přistupovat jako k položkám pole, kde místo indexů budeme používat klíčová slova struktury JSON.

Abychom trochu ošetřili případný neúspěch při hledání teploty ve struktuře JSON, je zde podmínka, která testuje dvě základní věci:

  1. ("current" in data) – zde ve struktuře data existuje klíč current.

  2. ("temperature_2m" in data["current"]) – zda v podstruktuře current existuje klíč temperature_2m

Tato podmínka slouží k určitému ověření integrity dat, která jsme načetli z meteowebu, než se pokusíme přistupovat k hodnotě teploty.

  • Bez této kontroly by došlo k chybě, pokud by server vrátil nějaké neočekávané nebo chybějící údaje.
  • Takto můžeme bezpečně ověřit, že data, která používáme, jsou kompletní a správná.

Například pokud by server vrátil data bez teploty, kód by se vyhnul chybě typu KeyError a místo toho vypíše zprávu, že teplota není k dispozici. Tímto způsobem můžeme elegantně ošetřit možné problémy s neúplnými nebo chybějícími daty.

Přítomnost neznámých znaků před strukturou JSON nás možná zbytečně donutila k tomu, abychom byli tak trochu ve střehu. Samotný převod z JSON je pro jistotu vsazen do struktury try/except. Takže kdyby se cokoliv při převodu pokazilo, měli bychom na to umět zareagovat aspoň touto obecnou výjimkou. Protože zrovna v případě čísla 142 (či nuly na konci) by byl okamžikem, který samotná předchozí podmínka podchytit nedokázala a převod z JSON by se nedařil.

Všimněme si ale sekce except:

except Exception as e:
    print("Chyba při stahování dat:", e)

Výraz Exception as e přiřadí do proměnné e chybu, která blok except vyvolala. Následné vypsání proměnné e nám pak naznačí, k jakému problému došlo. To je vcelku zajímavé rozšíření bloku except, že?

Pokud máme tedy vše ošetřené a vyřešené, lze přistoupit k načtení teploty:

temp = data["current"]["temperature_2m"]

K vypsání právě získané hodnoty teploty opět použijeme dnešní trik se znakem f před řetězcem:

print(f"\nAktuální teplota: {temp} °C")

Pojďme se podívat, jak bude funkčnost výsledná funkčnost programu vypadat:

Nacteni meteodat - parsed
Obrázek č. 4 – Úspěšné načtení teploty z API meteoserveru open-meteo.com

Jen poznamenejme, že pokud z programu odstraníme vypsání proměnné body, zmizí z výstupu to vypsání JSON struktury i se „záhadnými“ čísly 142 a 0. 😊

Struktura programu

Ještě než si zde uvedeme celý kód programu, shrneme jeho základní strukturu:

  1. Wi-Fi připojení:
    1. Tato část kódu zůstává relativně nezměněná vůči předchozím programům zabývajícími se Wi-Fi, protože připojení k Wi-Fi zůstává stále stejné.
  2. Socketové připojení:
    1. Používáme socket pro vytvoření TCP socketu, který se připojí k serveru api.open-meteo.com na portu 80 (HTTP).
    2. Sestavujeme HTTP GET požadavek jako textovou zprávu.
    3. Odesíláme požadavek přes socket, následně přijímáme odpověď po kouskách (1024 bytů) a tvoříme celou odpověď.
  3. Zpracování odpovědi:
    1. Odpověď je dekódována z bytového řetězce do textu.
    2. Po získání odpovědi hledáme v textu hlavičku a tělo odpovědi oddělené sekvencí \r\n\r\n.
    3. Z odpovědi je vyříznuta čistá JSON struktura.
    4. Získaný text s JSON zápisem je následně zpracován jako JSON pomocí loads() a teplota je extrahována z příslušné části JSON objektu.
  4. Chybové zpracování:
    1. Pokud se nepodaří získat teplotu nebo dojde k chybě při zpracování odpovědi, bude vypsána chybová zpráva.
Kód programu:

# pripojeni knihoven
import network
import time
import sys
try:
    import ujson as json
except:
    import json
try:
    import usocket as socket
except:
    import socket

# Nastavení Wi-Fi
ssid = "nazev-WiFi-site"
pwd = "Heslo-k-WiFi-siti"

# GPS souřadnice
latitude = 50.0754669   # Např. Bazilika sv. Ludmily (Praha)
longitude = 14.4371797

# Požadavek na API Open-Meteo
host = 'api.open-meteo.com'
path = f'/v1/forecast?latitude={latitude}&longitude={longitude}&current=temperature_2m'

# --------------------------------------------------------

def exit_program():
    print("Ukoncuji program...")
    sys.exit(0)

connect_status = {
    network.STAT_IDLE: "zadna aktivita",           # STAT_IDLE - žádné připojení a žádná aktivita
    network.STAT_CONNECTING: "pripojeni probiha",  # STAT_CONNECTING - probíhá připojení
    network.STAT_WRONG_PASSWORD: "chybne heslo",   # STAT_WRONG_PASSWORD - selhalo z důvodu nesprávného hesla
    network.STAT_NO_AP_FOUND: "AP nenalezen",      # STAT_NO_AP_FOUND - selhalo, nebyl nalezen přístupový bod
    network.STAT_GOT_IP: "Uspesne pripojeni"       # STAT_GOT_IP - připojení se podařilo
}

# pripojeni k Wi-Fi
sta_if = network.WLAN(network.STA_IF)   # nastaveni rezimu STA
sta_if.active(True)                     # zapnuti sitoveho pripojeni

if not sta_if.isconnected():     # neni-li ESP32 pripojen, zacne se pripojovat
    sta_if.connect(ssid, pwd)    # autorizace pristupu
    print("Pripojuji se k Wi-Fi")
    n = 0
    while (not sta_if.isconnected()) and (n < 10):   # cekaci smycka
        n += 1                                        # timeout
        print(".", end="")
        time.sleep(1)

sta_ifstatus = sta_if.status()
print("\nVysledek pripojeni:", connect_status[sta_ifstatus])
if (sta_ifstatus == network.STAT_GOT_IP):
    STAconf = sta_if.ifconfig()     # nacteni informaci pripojeni
    print("\nSTA-IP:\t\t", STAconf[0], "\nSTA-NETMASK:\t", STAconf[1], \
          "\nSTA-GATEWAY:\t", STAconf[2], "\nSTA-DNS:\t", STAconf[3], sep="")
    print("---")
else:
    print("KONEC pripojovani!")
    sta_if.active(False)
    exit_program()

# --------------------------------------------------------

# klient, nacteni stranky z URL
url = f"http://{host}{path}"
print("Stahuji data z:", url)

# Vytvoření soketu a připojení k serveru
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((host, 80))

# Sestavení HTTP GET požadavku
request = f"GET {path} HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\n\r\n"

# Odeslání požadavku na server
sock.send(request.encode())     # převod do bytového řetězce

# Příjem odpovědi
response = b""
while True:
    data = sock.recv(1024)
    if not data:
        break
    response += data

# Uzavření soketu
sock.close()
sta_if.active(False)     # ESP32 vypne Wi-Fi adaptér

# Rozdělení odpovědi na hlavičky a tělo
header, body = response.split(b'\r\n\r\n', 1)
body = body.decode('utf-8')

print(body)   # vypis tělo zprávy, ať je vidět, co se bude zpracovávat

# Očistíme odpověď: odstraníme čísla na začátku a na konci
# 1. Najdeme první výskyt "{" a poslední výskyt "}"

start_index = body.find("{")
end_index = body.rfind("}")

# Pokud nalezneme validní JSON, extrahujeme ho
if ((start_index != -1) and (end_index != -1)):
    json_str = body[start_index:end_index+1]
else:
    print("Chyba: Není možné najít platný JSON.")
    json_str = ""

# Hledání teploty v těle odpovědi
if json_str:
    try:
    data = json.loads(json_str)
    if (("current" in data) and ("temperature_2m" in data["current"])):
        temp = data["current"]["temperature_2m"]
        print(f"\nAktuální teplota: {temp} °C")
    else:
        print("Chyba: Nepodařilo se získat teplotu.")
    except Exception as e:
        print("Chyba při stahování dat:", e)



Jiná verze programu (použití modulu 'urequests')

Na samém konci minulého článku jsme naznačili, že by šlo webového klienta vytvořit i trochu jednodušeji. Získání určité „syrové“ odpovědi od serveru ve formě textu, její rozdělení na hlavičky a tělo… to vše je takové nízko úrovňové. Copak neexistuje způsob, který by fungoval trochu jako webový prohlížeč – zadal adresu a vrátila by se odpověď obsahu stránky. Jak vůbec ošetřit případné problémy při nezdaru připojení? A tak dále…

Naštěstí ano! Je tady knihovna requests (resp. v MicroPythonu urequests). Tato knihovna výrazným způsobem zjednodušuje práci s webovou komunikací. Kupříkladu za nás navazuje sockety nebo, když budeme chtít, vrací jen textový obsah těla zprávy. Zajímavé je pro nás i to, že tento textový obsah těla zprávy již bude zbaven záhadných hodnot. Kód našeho klienta dotazujícího se na API meteowebu open-meteo.com bude rázem výrazně kratší.

Uvedeme si zde jen kód našeho meteoklienta s užitím requests (resp. urequests). Připojení k Wi-Fi síti je stejné jako v předchozím příkladu:

# pripojeni knihoven
import network
import time
import sys
try:
    import ujson as json
except:
    import json
try:
    import urequests as requests
except:
    import requests

# Nastavení Wi-Fi
ssid = "nazev-WiFi-site"
pwd = "Heslo-k-WiFi-siti"

# GPS souřadnice
latitude = 50.0754669   # Např. Bazilika sv. Ludmily (Praha)
longitude = 14.4371797

# Požadavek na API Open-Meteo
url = f"https://api.open-meteo.com/v1/forecast?latitude={latitude}&longitude={longitude}¤t=temperature_2m"

# --------------------------------------------------------

def exit_program():
    print("Ukoncuji program...")
    sys.exit(0)

connect_status = {
    network.STAT_IDLE: "zadna aktivita",           # STAT_IDLE - žádné připojení a žádná aktivita
    network.STAT_CONNECTING: "pripojeni probiha",  # STAT_CONNECTING - probíhá připojení
    network.STAT_WRONG_PASSWORD: "chybne heslo",   # STAT_WRONG_PASSWORD - selhalo z důvodu nesprávného hesla
    network.STAT_NO_AP_FOUND: "AP nenalezen",      # STAT_NO_AP_FOUND - selhalo, nebyl nalezen přístupový bod
    network.STAT_GOT_IP: "Uspesne pripojeni"       # STAT_GOT_IP - připojení se podařilo
}

# pripojeni k Wi-Fi
sta_if = network.WLAN(network.STA_IF)   # nastaveni rezimu STA
sta_if.active(True)                     # zapnuti sitoveho pripojeni

if not sta_if.isconnected():     # neni-li ESP32 pripojen, zacne se pripojovat
    sta_if.connect(ssid, pwd)    # autorizace pristupu
    print("Pripojuji se k Wi-Fi")
    n = 0
    while (not sta_if.isconnected()) and (n < 10):   # cekaci smycka
        n += 1                                        # timeout
        print(".", end="")
        time.sleep(1)

sta_ifstatus = sta_if.status()
print("\nVysledek pripojeni:", connect_status[sta_ifstatus])
if (sta_ifstatus == network.STAT_GOT_IP):
    STAconf = sta_if.ifconfig()     # nacteni informaci pripojeni
    print("\nSTA-IP:\t\t", STAconf[0], "\nSTA-NETMASK:\t", STAconf[1], \
          "\nSTA-GATEWAY:\t", STAconf[2], "\nSTA-DNS:\t", STAconf[3], sep="")
    print("---")
else:
    print("KONEC pripojovani!")
    sta_if.active(False)
    exit_program()

# --------------------------------------------------------

# Požadavek na API Open-Meteo
print("Stahuji data z:", url)

try:
    response = requests.get(url)       # Odeslání GET požadavku na server Open-Meteo
    print(response.text)               # jen aby bylo možné vidět, co se bude dále zpracovávat
    data = json.loads(response.text)   # Převod z formátu JSON do Python objektu
    response.close()                   # Uzavření odpovědi, aby se uvolnily prostředky
    sta_if.active(False)               # ESP32 vypne Wi-Fi adaptér
    if (("current" in data) and ("temperature_2m" in data["current"])):   # Kontrola existence klíčů
        temp = data["current"]["temperature_2m"]     # extrahujeme hodnotu teploty
        print(f"\nAktuální teplota: {temp} °C")      # Vytiskneme aktuální teplotu
    else:
        print("Chyba: Nepodařilo se získat teplotu.")     # nepodařilo se získat teplotu
except Exception as e:
    print("Chyba při stahování dat:", e)   # došlo k jakékoliv chybě (např. připojením nebo dekódováním dat)

Část kódu týkající se klienta je okomentována, ale i přesto se na jednotlivé části podíváme.

První změnou je, že nyní potřebujeme znát celou URL našeho požadavku, tedy se to celé bude tak trochu chovat jako webový prohlížeč – zadáme adresu a hotovo! Žádné host ani path!

url = f"https://api.open-meteo.com/v1/forecast?latitude={latitude}&longitude={longitude}&current=temperature_2m"

Vytvoření URL dotazu pomocí zápisu s počátečním f pro vložení proměnných latitude a longitude už známe.

Následuje odeslání GET požadavku:

response = requests.get(url)

Návratový objekt response obsahuje všechno možné, ale nás v tuto chvíli zajímá jen text těla odpovědi. Protože v tuto chvíli je odpovědí čistý JSON kód, můžeme jej hned převést z JSON do pythonovské struktury:

data = json.loads(response.text)

Předpokládáme-li, že vše proběhlo bez problémů (Ano, problémy mohou nastat vždy, tak jsme to celé opět vnořit do struktury try/except!), můžeme otestovat existenci klíčů a vypsat teplotu:

if (("current" in data) and ("temperature_2m" in data["current"])):
    temp = data["current"]["temperature_2m"]
    print(f"\nAktuální teplota: {temp} °C")
else:
    print("Chyba: Nepodařilo se získat teplotu.")

A je hotovo!

Jo… a všimněte si, že knihovna urequest se umí vypořádat i s Transfer-Encoding: chunked!

Nacteni meteodat - modul request
Obrázek č. 5 – Načtení teploty programem postaveným na modulu urequests

Nakonec nejdelší částí celého programu je to úvodní připojení k Wi-Fi. Možná bychom se do budoucna mohli zamyslet nad tím, zda bychom to přihlašování k Wi-Fi nemohli „zaparkovat“ do nějaké jiné (samostatné?) části projektu. Tak trochu z tohoto vytvořit knihovnu/modul, abychom se v hlavní části programu mohli zabývat tím důležitým.

Závěrem

Úplně nevím, zda závěrečné použití vcelku efektivní (a efektní!) knihovny urequests příliš nerozběsnilo čtenáře, který nyní do obrazovky ječí, proč jsme to rovnou neřekli na samém začátku. Ano, možná jsme si mohli ušetřit povídání o socketech, postupném načítání odpovědi a její rozdělování na hlavičky a tělo. O očišťování JSON kódu z textu odpovědi zamořeném „podivnými“ čísly ani nemluvě! Ale upřímně, co bychom se v takovém případě naučili? 😜 (Tady máte knihovnu a hrajte si!)

No, snad nám tento „podlý výukový trik“ do příště odpustíte.

Bohužel, v současné době bůhví, kdy zase „to příště“ bude!

UPOZORNĚNÍ:
Nesouhlasíme s vyřazením Newtonových zákonů, Ohmova zákona a zákona zachování energie z učiva fyziky základních škol v České republice!