Návod

Spotové ceny na ESP32 a Arduino

Kompletní sketche pro ESP32: aktuální cena z binární MQTT zprávy a varianta přes HTTPS s knihovnou ArduinoJson.

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

OffsetTypObsah
0uint8Verze formátu = 2
1uint8Příznaky dne: bit 0 = den je kompletní, bit 1 = den obsahuje zápornou cenu
2uint64Unix timestamp začátku aktuálního intervalu (UTC)
10int16Cena v haléřích za kWh se znaménkem
12uint8Pořadí intervalu v rámci dne (1 = nejlevnější)
13uint16Kurz EUR/CZK × 1000
15uint8Počet intervalů dne N

Všechny hodnoty jsou little-endian. Hodnoty se čtou přes memcpy, protože vícebajtová pole nejsou zarovnaná.

Arduino — ESP32 + PubSubClient
// 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, &lt);              // 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.

Arduino — dekódování dne
// 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.

Arduino — ESP32 + HTTPClient + ArduinoJson 7
// 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.

Další návody