Ключ OpenWeatherMap я получил за пять минут: регистрация, раздел api-keys, строка из 32 символов. Ещё десять минут ушло на сборку запроса по образцу из документации — и вот в Serial-мониторе печатается стена текста на полкилобайта: кавычки, двоеточия, скобки в два этажа. Я уверенно написал float t = doc["temp"]; и получил ноль. Температуры на верхнем этаже ответа нет — она живёт внутри объекта main, а описание неба вообще лежит в массиве weather, о котором я в тот момент не подозревал.
Стена текста перестала пугать, как только я разложил её на этажи: наверху — имя города и код статуса, этажом ниже — объекты main с измерениями и wind с ветром, а условия неба спрятаны в массив weather. Это не прихоть сервиса: измерения и условия — разные сущности, и сервер группирует их по смыслу. Умение читать чужую структуру глазами мы качали в уроке про синтаксис JSON — здесь оно окупается с процентами.
План урока: анатомия ответа по этажам, ловушка Кельвинов, проверка кода статуса, полный скетч для ESP32 с HTTP-запросом и разбор в симуляторе — с честными оговорками о том, что движок книги умеет, а что покажет только настоящая плата.
Анатомия ответа: этажи main, wind и weather
Сокращённый вид ответа на запрос текущей погоды выглядит так: name наверху — «Moscow», рядом cod — статус обработки, 200 для успеха. Объект main держит измерения: temp — температура, humidity — влажность в процентах, pressure — давление в гектопаскалях. Объект wind — скорость в метрах в секунду. И отдельно — массив weather: список объектов с описанием неба, где в weather[0].description лежит текст вроде «clear sky», а в weather[0].main — короткое «Clear». Массив в текущем API приходит с одним элементом, но формат — именно массив, и обращение к нему как к объекту — гарантированный молчаливый ноль.
Первый сюрприз новичков — единицы. Без указания единиц сервис отдаёт температуру в Кельвинах: двадцать градусов по Цельсию придут как 293.15, и ваш порог «включить отопление ниже десяти» никогда не сработает. Лечится параметром units=metric в адресе запроса — Цельсии; units=imperial — Фаренгейты. Второй сюрприз — язык описаний: «broken clouds» превращаются в русское «пасмурно» при параметре lang=ru. Оба параметра дописываются в URL через амперсанд, плата не платит за них ни байтом ответа.
Цена входа: на бесплатном тарифе сервис разрешает до 60 запросов в минуту и миллион в месяц — узлу, который опрашивает погоду раз в десять минут, хватает с головой: 144 запроса в сутки, меньше шести тысяч в месяц. Полный ответ на крупный город занимает 450–550 байт, а документ после разбора съест ещё в полтора-два раза больше — на Uno с её 2 КБ SRAM это впритык, мы считали бюджет памяти в уроке про PROGMEM и SRAM. ESP32 такой пакет не заметит; для Uno существует срезание ответа фильтрами ArduinoJson — приём отдельного разговора.
И последнее, что стоит заметить глазами при первом знакомстве: поле dt наверху ответа — unixtime момента измерения, а timezone — сдвиг города в секундах. Сервис кэширует погоду, и для редкого города dt может отставать на полчаса: если ваша поливалка решает по свежести данных, печатайте dt в лог и сверяйте глазами. И не завязывайте скетч на все поля сразу: сервис добавляет новые без предупреждения — координаты, видимость, облачность — читайте те три-четыре, на которых держится логика, остальное пусть живёт в документе без дела.
#include <WiFi.h>#include <HTTPClient.h>#include <ArduinoJson.h> JsonDocument doc; const char* WIFI_SSID = "dom";const char* WIFI_PASS = "parol";const char* KEY = "vash_klyuch_s_32_simvola"; unsigned long lastPoll = 0; void setup() { Serial.begin(115200); WiFi.begin(WIFI_SSID, WIFI_PASS); while (WiFi.status() != WL_CONNECTED) delay(300); Serial.println("WiFi ok");} void loop() { if (millis() - lastPoll < 600000UL) return; // раз в десять минут lastPoll = millis(); HTTPClient http; String url = "http://api.openweathermap.org/data/2.5/weather"; url += "?q=Moscow&units=metric&lang=ru&appid="; url += KEY; http.begin(url); int code = http.GET(); if (code != 200) { // тело-стена приходит не всегда: Serial.print("HTTP code="); Serial.println(code); // 401 — ключ не принят, 404 — город не найден http.end(); return; } String payload = http.getString(); // стена JSON целиком DeserializationError err = deserializeJson(doc, payload); http.end(); if (err) { Serial.print("razbor sorvan: "); Serial.println(err.c_str()); return; } int cod = doc["cod"] | 0; if (cod != 200) { // сервис обработал запрос, но не одобрил Serial.print("cod="); Serial.println(cod); return; } float t = doc["main"]["temp"] | -99.0; // этаж main int humi = doc["main"]["humidity"] | 0; const char* city = doc["name"] | "?"; const char* sky = doc["weather"][0]["description"] | "?"; // массив! Serial.print(city); Serial.print(": "); Serial.print(t, 1); Serial.print(" C, vlazhnost "); Serial.print(humi); Serial.print("%, nebo: "); Serial.println(sky);}Цепочки ключей и молчаливые нули
Вложенное чтение — цепочка обращений: doc["main"]["temp"] спускается в объект main и берёт из него temp; doc["weather"][0]["description"] сначала заходит в массив по индексу, потом в объект по ключу. Каждый этаж — отдельный шанс промахнуться: написать weather["description"] вместо weather[0]["description"], забыть кавычки, перепутать регистр — Temp и temp разные ключи, мы это уже фиксировали в уроке про первый разбор строки.
Промах по этажу ведёт себя как промах по обычному ключу: молчаливый ноль. Цепочка не бросается исключениями — она честно вернёт пустоту, и порог сработает так, будто на улице ноль градусов. Страховки те же, что мы разбирали в уроке про чтение полей: оператор | с запасным значением вне рабочего диапазона — у меня -99.0 для температуры и "?" для строк, — а для ветвлений containsKey. В скетче выше страховки стоят на каждом чтении: если сервис переименует поле или урежет ответ, на экране появится ? и -99, а не лживая погода.
Код статуса — первая проверка до любых чтений. Двойной фильтр в скетче выше не украшение, а навык: HTTP-код 200 говорит, что транспорт сработал, поле cod в документе — что сервис понял ваш запрос: 200 — успех, 404 — город не найден, 401 — ключ не принят. Нюанс, о котором мало где пишут: в успешном ответе cod приходит числом, а в ответах на промах сервис исторически присылает его строкой — если сравниваете строго, допишите сравнение со строкой или проверьте name на пустоту. И помните про HTTP-ловушки вроде редиректов и фильтров ботов — мы их разбирали в уроке про GET-запросы с ESP32.
Первый день с чужим API я всегда заканчиваю одним и тем же приёмом: один раз печатаю сырой payload целиком и раскладываю его в онлайн-форматировщике, как мы делали в уроке про синтаксис. Пять минут глазами по этажам экономят вечер гаданий «почему ноль»: в разложенном виде видно и объект main, и массив weather, и то, что температура лежит во втором этаже. Печать оставляю под условием — по кнопке или один раз при старте, чтобы в постоянном логе не платить канал за полкилобайта каждые десять минут.
| Поле | Где лежит | Что даёт | Ловушка |
|---|---|---|---|
| name | верхний этаж | название города одной строкой | регистр ключа: Name даёт ноль |
| main.temp | объект main | температура в выбранных единицах | без units=metric — Кельвины: 293.15 |
| main.humidity | объект main | влажность в процентах, целое | ноль легитимен — страховка через | не видна |
| weather[0].description | массив weather | текст неба: "clear sky" с lang=ru — по-русски | обращение без [0] даёт молчаливый ноль |
| cod | верхний этаж | статус: 200 — успех | при промахе приходит строкой |
Разбор в симуляторе: честные упрощения
Движок книги разбирает вложенный пакет как настоящий: сокращённый ответ с этажом main даёт razbor=1, containsKey("main") отвечает единицей, а serializeJson печатает документ целиком — этаж main с его temp виден в печати своими глазами. Разбор, проверка ключей и печать здесь ведут себя как на плате.
Чтение значений — единственное упрощение, и оно существенное: точечная форма в симуляторе видит только верхний этаж, поэтому doc.main.temp возвращает ноль, и промежуточная переменная не спасает. На настоящей плате та же цепочка doc["main"]["temp"] работает без оговорок — в коде выше она и показана. Чтобы логика порогов в симуляторе всё-таки ожила, второй пакет в скетче ниже — плоский вариант того же ответа с теми же числами: temp, wind и name на верхнем этаже, вердикты по порогам работают в полную силу.
- Смените город. В плоском пакете замените name на Omsk, temp на -12: вердикт перейдёт в kurtka — пороги считают значение, а не название.
- Поднимите ветер. Поставьте wind равным 8.0: вердикт про шляпу сменится — порог 5.0 в коде виден и проверяем.
- Сломайте этаж. Уберите закрывающую скобку объекта main в первом пакете и запустите: razbor=0 — на плате это был бы IncompleteInput, имя кода печаталось бы через err.c_str().
- Кельвины. В плоском пакете поставьте temp 293.15: вердикт уверенно уйдёт в shorty — без units=metric это и есть честные «двадцать градусов».
- Ответ OpenWeatherMap — матрёшка: name и cod наверху, температура и влажность в объекте main, ветер в wind, небо в массиве weather
- weather — массив: правильное чтение doc["weather"][0]["description"], обращение без индекса даёт молчаливый ноль
- Без units=metric температура приходит в Кельвинах; lang=ru переводит описания неба на русский — оба параметра дописываются в URL
- Проверяйте транспорт и статус до чтения полей: HTTP-код 200, затем cod из документа; при промахе cod приходит строкой
- Полный ответ 450–550 байт, документ дороже в полтора-два раза: на Uno с 2 КБ впритык — ESP32 или срезание фильтрами
- 1. Что вернёт float t = doc["temp"]; для ответа OpenWeatherMap?
- 2. В каких единицах сервис отдаёт температуру без параметра units?
- 3. Как правильно прочитать описание неба?
- 4. HTTP-код 200, но поле cod в документе равно 404. Что делать?
- 5. Полный ответ на город занимает около 500 байт. Почему на Uno с ним тесно?
Домашнее задание с настоящей платой: соберите скетч из кода выше со своим ключом, напечатайте сырой payload один раз и найдите глазами этаж main — потом уберите печать и оставьте только вердикты. Опрос раз в десять минут при millis-таймере даст вам личный метеоузел за один вечер; а дальше собранный документ можно публиковать в свой MQTT-брокер — погода с улицы и ваши датчики в одном топике. Если же пакет приходит не целым телом ответа, а по символу из порта — вернитесь к приёмнику JSON из Serial: там обратная сторона той же механики.
Частые вопросы по теме урока
- Что вернёт float t = doc["temp"]; для ответа OpenWeatherMap?
- Ключа temp на верхнем этаже нет — он внутри main. Чтение по отсутствующему ключу даёт молчаливый ноль, как и для любого промаха цепочки. Ноль молча: температура лежит в объекте main
- В каких единицах сервис отдаёт температуру без параметра units?
- Единица по умолчанию — Кельвины. Параметр units=metric переключает на Цельсии, units=imperial — на Фаренгейты. В Кельвинах: 20 градусов придут как 293.15
- Как правильно прочитать описание неба?
- weather — массив объектов: индекс [0], затем ключ description. Обращение к массиву как к объекту — классический молчаливый ноль этого ответа. doc["weather"][0]["description"] — сначала индекс массива, потом ключ
- HTTP-код 200, но поле cod в документе равно 404. Что делать?
- Два кода проверяют разные вещи: HTTP — транспорт, cod — вердикт сервиса. cod 404 означает промах по городу, поля прогноза читать бессмысленно. Не читать поля: город не найден или запрос не одобрен — проверить имя и ключ
- Полный ответ на город занимает около 500 байт. Почему на Uno с ним тесно?
- Слоты и служебные записи дороже текста: бюджет документа — 800–1000 байт против 2 КБ SRAM, которые делят Serial и другие библиотеки. ESP32 не заметит, на Uno режут ответ фильтрами. Документ после разбора съест ещё в полтора-два раза сверху — против 2 КБ SRAM всей платы