| | |

Bakaláři v Home Assistantu: známky a zprávy ze školy na dashboardu

Bakaláři používá většina českých základek a středních škol. Mají web i mobilní aplikaci – a obojí funguje tak, že se o nové zprávě od učitele dozvíte, až když se sami podíváte. Napojil jsem Bakaláře přes jejich API na Home Assistant. Nové známky teď svítí na dashboardu v kuchyni barevně podle toho, jak dopadly, a při nové zprávě přijde notifikace oběma rodičům.

Obtížnost: Středně pokročilý

Co je potřeba: Home Assistant + Node-RED, rodičovský nebo žákovský účet do Bakaláří, adresa školního serveru


Co to nakonec umí

Flow se každých dvacet minut zeptá školního serveru na čtyři věci a výsledek uloží do helperů v Home Assistantu:

  • Zprávy z Komens – celkový počet a kolik z nich je nepřečtených
  • Nástěnka – stejné dvě čísla zvlášť, protože nástěnka je jiný endpoint
  • Domácí úkoly – počet aktuálně zadaných
  • Známky – jen ty nové, s předmětem, hodnotou, vahou a popisem

Na dashboardu z toho vznikne karta, kde je jednička zeleně, trojka oranžově a pětka červeně, a nad ní odznáček, který se objeví jen když je co číst. Při nové zprávě přijde notifikace na oba telefony. A protože je to všechno v Home Assistantu jako obyčejné entity, dá se na to navěsit cokoli dalšího – hlášení na chytrý reproduktor, řádek na LED panelu, cokoli.


Dvě cesty, jak na to

Než začnete stavět vlastní flow, stojí za to vědět, že v HACS existuje hotová integrace Bakaláři HA a k ní sada karet Bakaláři Cards. Instaluje se běžnou cestou přes HACS a je to výrazně rychlejší start.

Já jsem nakonec zůstal u vlastního flow v Node-RED, protože chci mít pod kontrolou přesně to, kdy a komu se pošle notifikace, a chci si sám určit, jak se známky formátují. Pokud vám stačí data v Home Assistantu a nechcete řešit tokeny, začněte integrací z HACS a tenhle článek berte jako popis toho, co se děje pod kapotou.


Jak Bakaláři API funguje

Bakaláři mají REST API na stejném serveru jako webové rozhraní. Adresa je vždycky ve tvaru https://vase-skola.bakalari.cz, případně má škola Bakaláře na vlastní doméně – podívejte se do adresního řádku, když se přihlašujete přes prohlížeč.

Přihlášení

Autentizace je OAuth 2.0 s grant_type=password. Pošlete login a heslo, dostanete zpátky access_token a refresh_token.

POST https://vase-skola.bakalari.cz/api/login
Content-Type: application/x-www-form-urlencoded

client_id=ANDR&grant_type=password&username=VAS_LOGIN&password=VASE_HESLO

Hodnota client_id=ANDR je pevná – je to identifikátor android klienta a Bakaláři ho očekávají. V Node-RED to je jeden function node, který připraví tělo požadavku, a za ním http request:

msg.payload = "client_id=ANDR&grant_type=password"
            + "&username=" + global.get('bakalariUser')
            + "&password=" + global.get('bakalariPass');
msg.headers = { "Content-Type": "application/x-www-form-urlencoded" };
msg.method  = 'POST';
msg.url     = 'https://vase-skola.bakalari.cz/api/login';
return msg;

Následující node token vytáhne a uloží do zprávy. Zároveň je to místo, kde se pozná neúspěšné přihlášení:

if (!msg.payload || !msg.payload.access_token) {
    node.error('Login failed', msg);
    return null;
}
msg.access_token = msg.payload.access_token;
return msg;

Token má platnost v řádu hodin. Existuje sice refresh_token, ale u flow, který běží každých dvacet minut, se nevyplatí ho řešit – je jednodušší se přihlásit znovu při každém běhu. Jedno volání navíc za dvacet minut školní server nezatíží.

Endpointy, které potřebujete

Endpoint Metoda Co vrací
/api/3/komens/messages/received POST Přijaté zprávy od učitelů
/api/3/komens/messages/noticeboard POST Zprávy z nástěnky
/api/3/homeworks/count-actual GET Počet aktuálních úkolů (samotné číslo)
/api/3/marks GET Známky seskupené po předmětech

Ke každému požadavku patří hlavička s tokenem:

msg.headers = {
    "Authorization": "Bearer " + msg.access_token,
    "Content-Type":  "application/x-www-form-urlencoded"
};

Past č. 1: metoda není u všech endpointů stejná

Tohle mě stálo nejvíc času a v žádné dokumentaci jsem to nenašel. Komens a nástěnka odpovídají jen na POST, i když nic neposíláte a tělo požadavku je prázdné. Úkoly a známky naopak chtějí GET a na POST vrátí chybu.

Když u Komens pošlete GET, nedostanete chybu 405, jak by člověk čekal – dostanete odpověď, která vypadá skoro správně, ale seznam zpráv je prázdný. Flow tiše hlásí nula nepřečtených a vy si týden myslíte, že vám učitelé nepíšou.

Doporučuju si každý endpoint nejdřív vyzkoušet ručně – inject node, http request a debug node na výstupu. Až uvidíte v debug panelu skutečná data, teprve pak stavte zbytek.

Past č. 2: odpověď zpracovávejte jako text

Nastavte http request node na návratový typ a UTF-8 string, ne na a parsed JSON object. Když totiž server vrátí chybovou stránku místo JSONu, automatické parsování spadne a v debug panelu uvidíte jen nicneříkající hlášku. Vlastní parsování s try/catch vám ukáže, co server ve skutečnosti poslal:

if (typeof msg.statusCode !== 'undefined' && msg.statusCode !== 200) {
    node.error('Komens HTTP error: ' + msg.statusCode, msg);
    node.status({ fill: 'red', shape: 'dot', text: 'HTTP ' + msg.statusCode });
    return msg;
}

let raw = (typeof msg.payload === 'string') ? msg.payload : JSON.stringify(msg.payload);

try {
    let data   = JSON.parse(raw);
    let msgs   = data.Messages || [];
    let total  = msgs.length;
    let unread = msgs.filter(m => !m.Read).length;

    msg.payload = { total: total, unread: unread };
    node.status({ fill: 'green', shape: 'dot', text: 'OK ' + unread });
    return msg;
} catch (e) {
    node.error('JSON parse error: ' + e.message, { raw: raw });
    node.status({ fill: 'yellow', shape: 'ring', text: 'parse error' });
    return msg;
}

Volání node.status() je detail, který se vyplatí. Pod nodem pak v editoru vidíte zelenou tečku s počtem nepřečtených zpráv, nebo červenou s HTTP kódem. Nemusíte otevírat debug panel, abyste věděli, že flow běží.


Helpery v Home Assistantu

Data z API se ukládají do obyčejných helperů. Vytvořte je v Nastavení → Zařízení a služby → Pomocníci:

# Čísla (Pomocník typu "Číslo", rozsah 0-99, krok 1)
input_number.helper_skola_celkem_zprav
input_number.helper_skola_neprectene_zpravy
input_number.helper_skola_celkem_zprav_nastenka
input_number.helper_skola_neprectene_zpravy_nastenka
input_number.helper_skola_nove_ukoly

# Texty (Pomocník typu "Text", max. délka 255)
input_text.znamka_1
input_text.znamka_2
...
input_text.znamka_9

Devět textových helperů na známky je záměrné. Node-RED do nich zapisuje nové známky po jedné a devět jich je dost i na vysvědčovací týden. Kdo chce víc, přidá další – flow se řídí jedním switch nodem, kde stačí přidat výstup.


Flow: zprávy a notifikace

Kostra celého flow vypadá takhle. Jeden inject node s opakováním po 1200 sekundách (20 minut) spustí přihlášení a to se pak větví do tří samostatných dotazů:

[inject: každých 1200 s]
   → [function: připrav login]
   → [http request: POST /api/login]
   → [function: vytáhni access_token]
        │
        ├→ [function: připrav Komens]    → [http request] → [function: spočítej] → helpery + notifikace
        ├→ [function: připrav nástěnku]  → [http request] → [function: spočítej] → helpery
        └→ [function: připrav úkoly]     → [http request] → [function: parsuj]   → helper

Přihlášení se tedy provádí jen jednou za běh a token se předá všem třem větvím ve zprávě.

Celý Node-RED flow pro Bakaláře - přihlášení a tři paralelní větve pro zprávy, nástěnku a domácí úkoly
Celý flow. Vlevo přihlášení, uprostřed se větví na tři dotazy, vpravo zápis do helperů a notifikace.

Zelené popisky pod nody jsou výstup z node.status() – v editoru tak na první pohled vidíte, kdy flow naposledy proběhl a s jakým výsledkem.

Kdy poslat notifikaci

Tady je jediné místo, kde se dá logika snadno pokazit. Notifikace nemá odejít pokaždé, když jsou nepřečtené zprávy – to by chodila každých dvacet minut, dokud si je někdo nepřečte. Má odejít, jen když počet nepřečtených vzroste proti minulému běhu.

Předchozí hodnotu už máte uloženou v helperu, takže stačí porovnat:

const predchozi = parseInt(msg.predchozi) || 0;   // z current state nodu
const aktualni  = msg.payload.unread;

// vždy ulož aktuální stav
node.send([{ payload: { value: aktualni } }, null]);

// notifikaci jen při nárůstu
if (aktualni > predchozi) {
    return [null, { payload: { pocet: aktualni - predchozi } }];
}
return null;

Před tenhle node vložte current state node na input_number.helper_skola_neprectene_zpravy, aby byla předchozí hodnota ve zprávě k dispozici.

Samotná notifikace

Service: notify.mobile_app_VAS_TELEFON
Data:
{
  "title": "Zpráva ze školy",
  "message": "Nová zpráva od učitele",
  "data": {
    "color": "#FFA500",
    "priority": "high",
    "ttl": 0
  }
}
Editor JSON v nodu call service s nastavením notifikace - title, message, color, priority a ttl
Data notifikace se zadávají v záložce Edit JSON nodu call service. Ve vizuálním editoru se vnořený objekt data zadává hůř.

Kombinace priority: high a ttl: 0 obchází na Androidu úsporný režim, který by notifikaci jinak mohl doručit až za hodinu. U školních zpráv to má smysl – u méně důležitých notifikací to nechte být, jinak si vybijete baterii.

Node zduplikujte pro každý telefon v rodině. U nás chodí notifikace na Android i na iPhone, takže jsou dva.


Flow: známky s barevným rozlišením

Nejzajímavější část. Endpoint /api/3/marks vrací všechny známky za pololetí seskupené po předmětech, a u každé je příznak IsNew. Nás zajímají jen ty nové.

Struktura odpovědi vypadá zjednodušeně takhle:

{
  "Subjects": [
    {
      "Subject": { "Abbrev": "Čj", "Name": "Český jazyk" },
      "Marks": [
        {
          "MarkText": "1",
          "Weight":   null,
          "Caption":  "Prezentace knihy",
          "IsNew":    true
        }
      ]
    }
  ]
}

Trik s ha-alert

Karta typu Markdown v Lovelace umí vykreslit komponentu <ha-alert>. To je ten barevný pruh, který znáte z upozornění v nastavení Home Assistantu – a má čtyři varianty: info, success, warning a error.

Když do textového helperu uložíte rovnou celou tu značku, karta ji vykreslí barevně. Žádné šablony v Lovelace, žádné card-mod. Barvu určuje Node-RED podle hodnoty známky:

let cislo = 1;

for (const predmet of msg.payload.Subjects) {
    for (const znamka of predmet.Marks) {
        if (!znamka.IsNew) continue;

        let typ;
        switch (znamka.MarkText) {
            case "1":
            case "2": typ = "success"; break;
            case "3": typ = "warning"; break;
            default:  typ = "error";
        }

        const vaha = znamka.Weight ?? "-";

        msg.payload = '<ha-alert alert-type="' + typ + '">'
                    + predmet.Subject.Abbrev + " - " + znamka.MarkText
                    + " [" + vaha + "] (" + znamka.Caption + ")"
                    + '</ha-alert>';
        msg.cislo = cislo;
        node.send(msg);
        cislo++;
    }
}

Za function node přijde switch node, který podle msg.cislo rozhodí zprávy do devíti výstupů, a na každém výstupu je call service na input_text.set_value příslušného helperu.

Pozor na Weight. U některých známek je váha null – typicky u těch, které učitel zadal bez váhy. Bez ošetření se vám do textu propíše doslova řetězec „null“. Konstrukce znamka.Weight ?? "-" to vyřeší.

Vyčištění před každým během

Než flow začne zapisovat nové známky, musí ty staré smazat – jinak by na dashboardu zůstala viset známka z minulého týdne. Úplně první node za inject je proto call service na input_text.set_value se všemi devíti helpery najednou a prázdnou hodnotou:

Service: input_text.set_value
Entity:  input_text.znamka_1, input_text.znamka_2, ... input_text.znamka_9
Data:    { "value": "" }

Karta na dashboard

Zobrazení je pak triviální – obyčejná Markdown karta, která vypíše obsah helperů. Prázdné se nevykreslí, takže karta roste a zmenšuje se podle toho, kolik je nových známek:

type: markdown
title: Škola
content: |
  **Nepřečtené zprávy:** {{ states('input_number.helper_skola_neprectene_zpravy') | int }}
  **Nástěnka:** {{ states('input_number.helper_skola_neprectene_zpravy_nastenka') | int }}
  **Domácí úkoly:** {{ states('input_number.helper_skola_nove_ukoly') | int }}

  {{ states('input_text.znamka_1') }}
  {{ states('input_text.znamka_2') }}
  {{ states('input_text.znamka_3') }}
  {{ states('input_text.znamka_4') }}
  {{ states('input_text.znamka_5') }}

Kdo chce, aby se karta vůbec nezobrazovala, když nic nového není, obalí ji kartou typu Conditional s podmínkou na input_number.helper_skola_neprectene_zpravy větší než nula.

Odznáček, který se objeví jen když je co číst

Na hlavní dashboard nechci kartu se školou pořád. Chci jen malý odznáček nahoře, který se zjeví ve chvíli, kdy přijde nepřečtená zpráva, a jinak tam vůbec není. Na to se hodí mushroom-template-badge z kolekce Mushroom (instaluje se přes HACS):

type: custom:mushroom-template-badge
icon: mdi:message-alert-outline
color: '#D0021B'
entity: input_number.helper_skola_neprectene_zpravy
label: >-
  {{ states("input_number.helper_skola_neprectene_zpravy") | int }} nová
  zpráva
content: Ze školy
visibility:
  - condition: numeric_state
    entity: input_number.helper_skola_neprectene_zpravy
    above: 0
Konfigurace mushroom-template-badge v Home Assistantu s podmíněnou viditelností a náhledem odznáčku Ze školy
Odznáček s podmíněnou viditelností. V editoru se zobrazuje vždycky, na dashboardu jen když je nepřečtených zpráv víc než nula.

Klíčová je sekce visibility. Není to totéž co Conditional karta – podmínka je součástí samotného odznáčku, takže nepotřebujete nic obalovat a v editoru dashboardu zůstane všechno na jednom místě.

V editoru se náhled odznáčku zobrazuje i při nule, takže nečekejte, že zmizí už tam. Skryje se až na hotovém dashboardu.


Bezpečnost: kde skončí vaše heslo

Tohle je nejdůležitější odstavec celého článku a málem jsem si na tom sám naběhl.

Když napíšete login a heslo natvrdo do function nodu, uloží se v čitelné podobě do flows.json. Ten soubor pak skončí v každé záloze Home Assistantu, v každém exportu flow a v každém screenshotu editoru, který někam pošlete. Bakalářský účet přitom není žádná drobnost – jsou za ním údaje o dítěti.

Řešení, které funguje v Node-RED add-onu: uložte přihlašovací údaje do globálního kontextu v souboru settings.js, který se s flow neexportuje.

// v settings.js v konfiguračním adresáři Node-RED
functionGlobalContext: {
    bakalariUser: "vas-login",
    bakalariPass: "vase-heslo",
    bakalariUrl:  "https://vase-skola.bakalari.cz"
},

Ve function nodu je pak vytáhnete přes global.get('bakalariUser'), jak je vidět v ukázce přihlášení výše. Export flow, který někomu pošlete, obsahuje jen názvy proměnných.

Pokud už máte heslo napsané přímo ve flow, projděte si i staré zálohy. A než pošlete komukoli export flow, otevřete ho v textovém editoru a vyhledejte v něm slovo password.


Časté problémy

Přihlášení vrací chybu, přitom heslo je správně. Zkontrolujte Content-Type. Bakaláři chtějí application/x-www-form-urlencoded, ne JSON. A tělo požadavku musí být řetězec, ne objekt.

Komens vrací prázdný seznam. Skoro jistě posíláte GET místo POST. Viz past č. 1 výše.

Dostávám 401 uprostřed dne. Vypršel token. Ujistěte se, že se každý běh flow začíná přihlášením a token se nikde neukládá napříč běhy.

Notifikace chodí pořád dokola. Porovnáváte absolutní počet nepřečtených místo nárůstu proti minulému běhu.

Známky se na kartě zobrazují jako text s ostrými závorkami. Karta musí být typu Markdown. V kartě typu Entities nebo Entity se ha-alert nevykreslí.

Adresa školy nefunguje. Některé školy mají Bakaláře na vlastní doméně, jiné v podsložce. Rozhoduje to, co vidíte v prohlížeči po přihlášení – API sedí vždycky na stejném základu plus /api/….


Flow ke stažení

Kompletní flow je ke stažení jako JSON. Přihlašovací údaje i adresa školy jsou v něm nahrazené zástupnými hodnotami.

⬇️ bakalari-node-red.json (verze z 2. 9. 2026)

Import: v Node-RED menu → Import → vybrat soubor, pak Import to → new flow.

Co upravit po importu

  • Adresu školy ve všech function nodech – hledejte vase-skola.bakalari.cz.
  • Přihlašovací údajeVAS_LOGIN a VASE_HESLO. Ideálně je rovnou přesuňte do settings.js podle sekce o bezpečnosti.
  • Notifikační nodynotify.mobile_app_VAS_TELEFON a notify.mobile_app_DRUHY_TELEFON.
  • Názvy helperů – v mém flow mají v názvu jméno dítěte, protože je hlídám dvě. Pokud sledujete víc dětí, celý tab zduplikujte a přejmenujte helpery.

Co dál

API má i další endpointy, ke kterým jsem se zatím nedostal – suplování (/api/3/substitutions/actual) a rozvrh (/api/3/timetable/actual). Ze suplování by šlo postavit ranní hlášení „dnes odpadá první hodina“, což je informace, kterou by rodič ocenil dřív než v sedm ráno u dveří školy.

Data z Bakaláří se dají poslat kamkoli, kam Home Assistant dosáhne. U nás končí kromě dashboardu ještě na tabletu v kuchyni a v ranním hlášení na reproduktoru. Jakmile máte čísla v helperech, je zbytek jen otázka toho, kde je chcete vidět.

Pokud máte jiný školní systém – Škola OnLine, iŠkola, EduPage – princip zůstává stejný: najít API, přihlásit se, porovnat s minulým stavem. Liší se jen adresy a názvy polí.

Líbil se ti článek? ❤️ Můžeš mi koupit kávu - díky!
Koupit kávu ☕

Podobné příspěvky

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *