MQTT binárně, nebo HTTP JSON?
- MQTT
v2/current/bin— doporučeno. Zpráva má vždy 16 bytů, přichází každých 15 minut a díky retain hned po připojení. Žádné TLS, žádné parsování JSON, minimum paměti. - HTTPS
/v2/prices/today— když nechcete MQTT. Odpověď má kolem 7 kB, potřebujete TLS a knihovnu ArduinoJson.
Ukázky jsou pro ESP32 s Arduino core. Formát binárních zpráv je podrobně popsán v dokumentaci.
MQTT: binární zpráva v2/current/bin
| Offset | Typ | Obsah |
|---|---|---|
| 0 | uint8 | Verze formátu = 2 |
| 1 | uint8 | Příznaky dne: bit 0 = den je kompletní, bit 1 = den obsahuje zápornou cenu |
| 2 | uint64 | Unix timestamp začátku aktuálního intervalu (UTC) |
| 10 | int16 | Cena v haléřích za kWh se znaménkem |
| 12 | uint8 | Pořadí intervalu v rámci dne (1 = nejlevnější) |
| 13 | uint16 | Kurz EUR/CZK × 1000 |
| 15 | uint8 | Počet intervalů dne N |
Všechny hodnoty jsou little-endian. Hodnoty se čtou přes memcpy, protože vícebajtová pole nejsou zarovnaná.
// ESP32 — aktuální spotová cena z MQTT (v2/current/bin, 16 bytů, i záporné ceny)
// Knihovna: PubSubClient (Nick O'Leary)
#include <WiFi.h>
#include <PubSubClient.h>
#include <time.h>
const char* WIFI_SSID = "moje-wifi";
const char* WIFI_PASS = "heslo-k-wifi";
const char* BROKER = "spotovky.cz";
const char* TOPIC = "v2/current/bin";
WiFiClient net;
PubSubClient mqtt(net);
void onMessage(char* topic, byte* data, unsigned int len) {
// Formát v2: verze na offsetu 0, little-endian (ESP32 je také little-endian)
if (len < 16 || data[0] != 2) return;
uint64_t start; // Unix timestamp začátku intervalu (UTC)
int16_t halere; // cena v haléřích za kWh, se znaménkem
uint16_t rate_x1000; // kurz EUR/CZK × 1000
memcpy(&start, data + 2, 8);
memcpy(&halere, data + 10, 2);
memcpy(&rate_x1000, data + 13, 2);
uint8_t rank = data[12]; // 1 = nejlevnější interval dne
uint8_t periods = data[15]; // 92, 96 nebo 100
bool negativeDay = data[1] & 0x02; // den obsahuje zápornou cenu
time_t t = (time_t)start;
struct tm lt;
localtime_r(&t, <); // místní čas podle TZ ze setup()
Serial.printf("%02d:%02d %.2f Kč/kWh pořadí %d/%d kurz %.3f%s\n",
lt.tm_hour, lt.tm_min, halere / 100.0f, rank, periods,
rate_x1000 / 1000.0f, negativeDay ? " (den se zápornou cenou)" : "");
}
void connectMqtt() {
while (!mqtt.connected()) {
String clientId = "esp32-spotovky-" + String((uint32_t)ESP.getEfuseMac(), HEX);
if (mqtt.connect(clientId.c_str())) { // bez jména a hesla, bez will zprávy
mqtt.subscribe(TOPIC, 1); // retain: aktuální cena přijde hned
} else {
Serial.printf("MQTT: chyba %d, další pokus za 5 s\n", mqtt.state());
delay(5000);
}
}
}
void setup() {
Serial.begin(115200);
WiFi.begin(WIFI_SSID, WIFI_PASS);
while (WiFi.status() != WL_CONNECTED) delay(250);
setenv("TZ", "CET-1CEST,M3.5.0,M10.5.0/3", 1); // Europe/Prague (SEČ/SELČ)
tzset();
mqtt.setServer(BROKER, 1883);
mqtt.setCallback(onMessage);
}
void loop() {
if (!mqtt.connected()) connectMqtt();
mqtt.loop();
}
Broker spotovky.cz přijímá jen subscribe. Ukázka proto nepoužívá will zprávu ani publish. Client ID musí být unikátní, proto obsahuje MAC adresu čipu.
Celý den: v2/today/bin a v2/tomorrow/bin
Zpráva dne nese všechny intervaly najednou. Počet intervalů N je na offsetu 12 a v den změny času je 92 nebo 100, proto ho vždy čtěte. Přihlaste se k odběru v2/today/bin stejně jako výše a v onMessage rozlište zprávy podle topicu.
// Celý den z v2/today/bin: 13 + N×3 bytů (289 / 301 / 313 B)
// PubSubClient má výchozí buffer 256 B — v setup() zvětšete: mqtt.setBufferSize(512);
void onDay(byte* data, unsigned int len) {
if (len < 13 || data[0] != 2) return;
uint64_t midnight; // půlnoc dne (Europe/Prague) jako Unix timestamp
memcpy(&midnight, data + 2, 8);
uint8_t n = data[12]; // počet intervalů: vždy čtěte, není to vždy 96
if (len < 13u + n * 3u) return;
for (uint8_t i = 0; i < n; i++) {
int16_t halere;
memcpy(&halere, data + 13 + i * 2, 2);
uint8_t rank = data[13 + n * 2 + i];
if (halere == -32768) continue; // cena chybí
time_t t = (time_t)(midnight + i * 900);
// ... halere / 100.0f = Kč/kWh, rank = pořadí, t = začátek intervalu
}
}
HTTPS a JSON s ArduinoJson
Ukázka stáhne dnešní ceny, s filtrem načte jen pole start, czk a rank a podle času z NTP najde aktuální interval. Začátek intervalu je v JSON ve tvaru ISO 8601 s posunem, funkce parseIso ho převede na Unix čas.
// ESP32 — dnešní ceny přes HTTPS a JSON (ArduinoJson 7)
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <time.h>
const char* WIFI_SSID = "moje-wifi";
const char* WIFI_PASS = "heslo-k-wifi";
const char* URL = "https://api.spotovky.cz/v2/prices/today";
// "2026-10-23T14:15:00+02:00" -> Unix timestamp (UTC)
time_t parseIso(const char* s) {
int y, mo, d, h, mi, se, oh, om;
char sign;
if (!s || sscanf(s, "%d-%d-%dT%d:%d:%d%c%d:%d",
&y, &mo, &d, &h, &mi, &se, &sign, &oh, &om) != 9) return 0;
y -= mo <= 2; // dny od 1970-01-01 (H. Hinnant)
long era = (y >= 0 ? y : y - 399) / 400;
long yoe = y - era * 400;
long doy = (153L * (mo + (mo > 2 ? -3 : 9)) + 2) / 5 + d - 1;
long doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
long days = era * 146097 + doe - 719468;
long offset = (oh * 3600L + om * 60L) * (sign == '-' ? -1 : 1);
return (time_t)days * 86400 + h * 3600L + mi * 60L + se - offset;
}
void showCurrentPrice() {
WiFiClientSecure client;
client.setInsecure(); // jen pro ukázku, viz poznámka pod kódem
HTTPClient http;
if (!http.begin(client, URL)) return;
int status = http.GET();
if (status != 200) { // 404 = data pro den zatím nejsou
Serial.printf("HTTP %d\n", status);
http.end();
return;
}
JsonDocument filter; // načte jen potřebná pole
filter["date"] = true;
filter["slots"][0]["start"] = true;
filter["slots"][0]["czk"] = true;
filter["slots"][0]["rank"] = true;
JsonDocument doc;
DeserializationError err = deserializeJson(doc, http.getString(),
DeserializationOption::Filter(filter));
http.end();
if (err) {
Serial.printf("JSON: %s\n", err.c_str());
return;
}
JsonArray slots = doc["slots"];
time_t now = time(nullptr);
for (JsonObject s : slots) {
time_t t = parseIso(s["start"].as<const char*>());
if (t <= now && now < t + 900) {
Serial.printf("%s: %.2f Kč/kWh, pořadí %d/%d\n",
doc["date"].as<const char*>(), s["czk"].as<float>(),
s["rank"].as<int>(), (int)slots.size());
}
}
}
void setup() {
Serial.begin(115200);
WiFi.begin(WIFI_SSID, WIFI_PASS);
while (WiFi.status() != WL_CONNECTED) delay(250);
configTzTime("CET-1CEST,M3.5.0,M10.5.0/3", "pool.ntp.org"); // čas z NTP
while (time(nullptr) < 1700000000) delay(250);
showCurrentPrice();
}
void loop() {
static uint32_t last = millis();
if (millis() - last >= 15UL * 60 * 1000) { // jednou za 15 minut
last = millis();
showCurrentPrice();
}
}
setInsecure() vypíná ověření certifikátu serveru. Pro trvalé nasazení nastavte kořenový certifikát přes client.setCACert(). Ukázka používá API knihovny ArduinoJson 7 (JsonDocument); ve verzi 6 nahraďte JsonDocument typem DynamicJsonDocument s velikostí, např. DynamicJsonDocument doc(8192).
Ceny celého dne se mění jen jednou denně. V reálném zařízení je úspornější stáhnout den jednou (a zítřek po 13:05) a aktuální interval hledat lokálně; ukázka pro jednoduchost stahuje znovu každých 15 minut.