[
    {
        "id": "fve2026reg0000tab",
        "type": "tab",
        "label": "FVE regulace",
        "disabled": false,
        "info": "Antireflow regulace balkonove FVE EcoFlow STREAM (800 W) + HW watchdog Shelly 1 Gen3.\nDokumentace: ostatni/BalkonFVE/16_nodered_flow.md",
        "env": []
    },
    {
        "id": "c1",
        "type": "comment",
        "z": "fve2026reg0000tab",
        "name": "1) SBER DAT",
        "info": "Jeden listener na 3EM, helpery, osvit a STREAM. Vse se kesuje do flow kontextu.",
        "x": 140,
        "y": 60,
        "wires": []
    },
    {
        "id": "c2",
        "type": "comment",
        "z": "fve2026reg0000tab",
        "name": "2) REGULACNI SMYCKA (10 s)",
        "info": "Vypocet limitu + health-gated heartbeat na Shelly watchdog.",
        "x": 140,
        "y": 220,
        "wires": []
    },
    {
        "id": "c3",
        "type": "comment",
        "z": "fve2026reg0000tab",
        "name": "3) STARTUP RESET",
        "info": "Pri startu / deployi fyzicky odpoji stridac.",
        "x": 140,
        "y": 500,
        "wires": []
    },
    {
        "id": "c4",
        "type": "comment",
        "z": "fve2026reg0000tab",
        "name": "4) HLIDAC NEVYROBY (5 min)",
        "info": "Svitit ma, ale stridac nedodava -> notifikace.",
        "x": 140,
        "y": 600,
        "wires": []
    },
    {
        "id": "n_listen",
        "type": "server-state-changed",
        "z": "fve2026reg0000tab",
        "name": "3EM / helpery / osvit / STREAM",
        "server": "46a53aae.a18f74",
        "version": 6,
        "outputs": 1,
        "exposeAsEntityConfig": "",
        "entities": {
            "entity": [],
            "substring": [],
            "regex": [
                "^(sensor\\.tech_shelly_spotreba_byt_phase_[abc]_vykon|sensor\\.tech_shelly_spotreba_byt_vykon|input_(number|select|boolean)\\.fve_|sensor\\.ef_bk1163_|number\\.ef_bk1163_)"
            ]
        },
        "outputInitially": true,
        "stateType": "str",
        "ifState": "",
        "ifStateType": "str",
        "ifStateOperator": "is",
        "outputOnlyOnStateChange": false,
        "for": "0",
        "forType": "num",
        "forUnits": "minutes",
        "ignorePrevStateNull": false,
        "ignorePrevStateUnknown": false,
        "ignorePrevStateUnavailable": false,
        "ignoreCurrentStateUnknown": true,
        "ignoreCurrentStateUnavailable": true,
        "outputProperties": [
            {
                "property": "payload",
                "propertyType": "msg",
                "value": "",
                "valueType": "entityState"
            },
            {
                "property": "data",
                "propertyType": "msg",
                "value": "",
                "valueType": "eventData"
            },
            {
                "property": "topic",
                "propertyType": "msg",
                "value": "",
                "valueType": "triggerId"
            }
        ],
        "x": 200,
        "y": 120,
        "wires": [
            [
                "n_cache"
            ]
        ]
    },
    {
        "id": "n_cache",
        "type": "function",
        "z": "fve2026reg0000tab",
        "name": "cache -> flow kontext",
        "func": "// ===== SBER DAT do flow kontextu =====\n// Entity ze ha-ef-ble - OVERENO 2026-08-06 na skutecnem zarizeni EF-BK1163.\n// Pozor: PV vykon je ve DVOU senzorech (dva MPPT vstupy), musi se scitat.\nconst STREAM_OUTPUT = 'sensor.ef_bk1163_grid_power';   // AC vystup stridace (W)\nconst STREAM_PV1    = 'sensor.ef_bk1163_pv_1_power';   // MPPT 1 (W)\nconst STREAM_PV2    = 'sensor.ef_bk1163_pv_2_power';   // MPPT 2 (W)\nconst STREAM_LIMIT  = 'number.ef_bk1163_maximum_output_power';  // skutecne nastaveny limit\n\nconst id  = msg.topic;\nconst now = Date.now();\nconst v   = parseFloat(msg.payload);\nconst WIN_MS = 15000;\n\nfunction push(ch, val) {\n    if (!Number.isFinite(val)) return;\n    const key = 'win_' + ch;\n    const w = (flow.get(key) || []).filter(e => now - e[0] <= WIN_MS);\n    w.push([now, val]);\n    flow.set(key, w);\n    flow.set('phase_' + ch, val);\n    flow.set('ts3em', now);\n}\n\nlet m;\nif ((m = id.match(/_phase_([abc])_vykon$/))) {\n    push(m[1].toUpperCase(), v);\n} else if (id === 'sensor.tech_shelly_spotreba_byt_vykon') {\n    push('TOTAL', v);\n} else if (id === STREAM_OUTPUT) {\n    flow.set('streamOut', v); flow.set('tsStream', now); flow.set('streamSeen', true);\n} else if (id === STREAM_PV1 || id === STREAM_PV2) {\n    flow.set(id === STREAM_PV1 ? 'pv1' : 'pv2', v);\n    flow.set('streamPv', (Number(flow.get('pv1')) || 0) + (Number(flow.get('pv2')) || 0));\n    flow.set('tsStream', now); flow.set('streamSeen', true);\n} else if (id === STREAM_LIMIT) {\n    flow.set('devLimit', v);          // co ma stridac SKUTECNE nastaveno\n} else if (id === 'input_boolean.fve_regulace_povolena') {\n    flow.set('enabled', msg.payload === 'on');\n} else if (id === 'input_number.fve_buffer') {\n    flow.set('buffer', v);\n} else if (id === 'input_number.fve_manualni_limit') {\n    flow.set('manual', v);\n} else if (id === 'input_select.fve_cil_regulace') {\n    flow.set('target', msg.payload);\n}\n\n// --- RYCHLA CESTA ---\n// Kdyz na cilovem kanalu skoci odecet do zaporu, necekej na dalsi 10s tik\n// a spust regulaci hned. Zkracuje trvani prekmitu z ~10 s na ~1 s.\nconst tgt = flow.get('target') || 'B';\nconst tch = (tgt === 'total') ? 'TOTAL' : tgt;\nconst isTargetReading = (m && m[1].toUpperCase() === tch)\n                        || (tch === 'TOTAL' && id === 'sensor.tech_shelly_spotreba_byt_vykon');\nif (isTargetReading && Number.isFinite(v) && v < -100) {\n    return { payload: 'fastpath' };\n}\nreturn null;\n",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 520,
        "y": 120,
        "wires": [
            [
                "n_reg"
            ]
        ]
    },
    {
        "id": "n_tick",
        "type": "inject",
        "z": "fve2026reg0000tab",
        "name": "kazdych 10 s",
        "props": [
            {
                "p": "payload"
            }
        ],
        "repeat": "10",
        "crontab": "",
        "once": true,
        "onceDelay": 5,
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 180,
        "y": 300,
        "wires": [
            [
                "n_sun1"
            ]
        ]
    },
    {
        "id": "n_sun1",
        "type": "api-current-state",
        "z": "fve2026reg0000tab",
        "name": "sun.sun",
        "server": "46a53aae.a18f74",
        "version": 3,
        "outputs": 1,
        "halt_if": "",
        "halt_if_type": "str",
        "halt_if_compare": "is",
        "entity_id": "sun.sun",
        "state_type": "str",
        "blockInputOverrides": false,
        "outputProperties": [
            {
                "property": "payload",
                "propertyType": "msg",
                "value": "",
                "valueType": "entityState"
            },
            {
                "property": "data",
                "propertyType": "msg",
                "value": "",
                "valueType": "entity"
            }
        ],
        "for": "0",
        "forType": "num",
        "forUnits": "minutes",
        "override_topic": false,
        "state_location": "payload",
        "override_payload": "msg",
        "entity_location": "data",
        "override_data": "msg",
        "x": 360,
        "y": 300,
        "wires": [
            [
                "n_reg"
            ]
        ]
    },
    {
        "id": "n_reg",
        "type": "function",
        "z": "fve2026reg0000tab",
        "name": "REGULACE",
        "func": "// ===== REGULACE FVE - hlavni smycka (kazdych 10 s) =====\nconst cfg = {\n    MAX_W: 800,            // max AC vykon stridace\n    RAMP_UP_W_S: 5,        // max prirustek limitu za SEKUNDU (W/s), ne za pruchod smyckou!\n    STEP: 10,              // krok nastaveni STREAMu (W)\n    WIN_MS: 15000,         // okno pro klouzave minimum (30->15 s: +20 % vyroby, overeno 7.8.)\n    MAX_AGE_3EM: 30000,    // max stari dat ze Shelly 3EM\n    MAX_AGE_STREAM: 60000, // max stari dat ze STREAMu\n    MIN_ELEV: 5,           // min elevace slunce (stupne)\n    MIN_PV: 20,            // min vykon panelu pro regulaci (W)\n    GROSS_EXPORT: -100,    // prah hrubeho pretoku (W)\n    GROSS_MS: 20000,       // jak dlouho smi hruby pretok trvat, nez rozepneme rele\n    UNHEALTHY_MS: 30000,   // jak dlouho smi trvat blokujici stav, nez posleme explicitni OFF\n    LOCK_MIN: 5,           // zamek po zasahu watchdogu (minuty), pri opakovani se zdvojnasobuje\n    LOCK_MAX: 60,          // strop zamku (minuty)\n    MIN_WRITE_DOWN_MS: 3000,  // min. rozestup zapisu pri SNIZOVANI limitu (bezpecny smer)\n    MIN_WRITE_UP_MS: 10000,   // min. rozestup zapisu pri ZVYSOVANI limitu\n    WRITE_DEADBAND: 20,       // mensi zmenu nez tohle nema smysl posilat (krome nuly)\n    RESEND_MS: 300000,        // i bez zmeny limit obcas zopakuj (kdyby stridac zapomnel)\n    CMD_TOL: 60,           // o kolik W smi stridac prestrelit posledni prikaz\n    CMD_FAIL_MS: 60000     // jak dlouho to smi trvat, nez to vyhodnotime jako neposlouchani\n};\n\nconst now      = Date.now();\nconst enabled  = flow.get('enabled') === true;\nconst buffer   = Number(flow.get('buffer'));\nconst manual   = Number(flow.get('manual'));\nconst target   = flow.get('target') || 'B';\n// elevace: z uzlu sun.sun (pomala cesta), jinak z kese (rychla cesta pri pretoku)\nlet elev = Number(msg.data && msg.data.attributes ? msg.data.attributes.elevation : NaN);\nif (Number.isFinite(elev)) { flow.set('elev', elev); } else { elev = Number(flow.get('elev')); }\nconst pv       = Number(flow.get('streamPv'));\nconst out      = Number(flow.get('streamOut'));\nconst ts3em    = Number(flow.get('ts3em')) || 0;\nconst tsStream = Number(flow.get('tsStream')) || 0;\nconst seen     = flow.get('streamSeen') === true;\n\n// --- klouzave minimum vykonu ciloveho kanalu za 30 s ---\nconst ch = (target === 'total') ? 'TOTAL' : target;\nconst phaseNow = Number(flow.get('phase_' + ch));\n// Okno klouzaveho minima. POZOR: HA posila state_changed jen pri ZMENE hodnoty,\n// takze pri ustalene zatezi prestanou z 3EM chodit udalosti uplne a okno by se\n// vyprazdnilo -> 'chybi okno mereni' -> limit 0 a zastaveny heartbeat.\n// Overeno 7.8.2026 v ostrem provozu (limit padal na 0 pri zatezi 500-2000 W).\n// Proto si okno doplnujeme i sami, z posledni zname hodnoty, dokud jsou data cerstva.\nlet win = (flow.get('win_' + ch) || []).filter(e => now - e[0] <= cfg.WIN_MS);\nif (Number.isFinite(phaseNow) && (now - ts3em) <= cfg.MAX_AGE_3EM\n    && (!win.length || now - win[win.length - 1][0] >= 2000)) {\n    win.push([now, phaseNow]);\n    flow.set('win_' + ch, win);\n}\nconst min30 = win.length ? Math.min.apply(null, win.map(e => e[1])) : null;\n// Okno musi byt naplnene, jinak je minimum jen okamzita hodnota a neni na cem stavet.\n// Bez plneho okna se limit smi jen snizovat (viz rampa nize).\nconst winFull = win.length > 1 && (now - win[0][0]) >= cfg.WIN_MS * 0.8;\n\n// --- health check ---\nconst blocked = [];\nif (!enabled)                       blocked.push('regulace vypnuta');\nif (now - ts3em > cfg.MAX_AGE_3EM)  blocked.push('data 3EM stara');\nif (min30 === null)                 blocked.push('chybi okno mereni');\nif (!(elev > cfg.MIN_ELEV))         blocked.push('slunce pod ' + cfg.MIN_ELEV + ' st.');\n// Staroba dat ze STREAMu smi blokovat, JEN kdyz stridac hlasi nenulovy vykon.\n// Duvod: pri rozepnutem rele hlasi same nuly, HA pri nemenne hodnote neposila\n// state_changed, timestamp stárne - a blokace by rele uz nikdy nenechala sepnout.\n// Nenulovy zamrzly vykon je naopak PRAVE ten nebezpecny stav (mrtve BLE za provozu).\nif (seen && Number.isFinite(out) && out > 0 && now - tsStream > cfg.MAX_AGE_STREAM) {\n    blocked.push('data STREAM stara');\n}\n\n// POZOR - zde SCHVALNE NENI podminka \"PV pod 20 W\". Byla tu a delala DEADLOCK:\n// pri rozepnutem rele stridac zastavi MPPT a hlasi PV = 0 (overeno 6.8.2026 na zarizeni).\n// Nizke PV by tedy blokovalo heartbeat -> rele by zustalo rozepnute -> PV by zustalo 0\n// -> systém by se sam nikdy nevratil. Noc hlida elevace slunce, ktera se pocita a nemuze\n// se takhle zaseknout. Nevyrobu za sveta resi samostatny hlidac notifikaci, ne rele.\n\n// Hruby pretok NEZNAMENA hned rozepnout rele: vzorec nize ho sam srovna behem\n// jednoho cyklu. Rele je az od toho, kdyz se to NEsrovna do cfg.GROSS_MS.\n// Trvalost se meri CASEM, ne poctem cyklu - smycku spousti i rychla cesta z kese.\n// Pojistka hlida VSECHNY TRI kanaly, ne jen regulovany. Kdyby zasuvka byla omylem\n// na jine fazi, na regulovanem kanalu by nebylo videt vubec nic a stridac by\n// exportoval donekonecna. Domacnost bez jineho zdroje nesmi exportovat nikde.\nlet worst = Number.isFinite(phaseNow) ? phaseNow : Infinity;\nlet worstCh = ch;\n['A', 'B', 'C'].forEach(function (k) {\n    const v = Number(flow.get('phase_' + k));\n    if (Number.isFinite(v) && v < worst) { worst = v; worstCh = k; }\n});\nconst grossExport = Number.isFinite(worst) && worst < cfg.GROSS_EXPORT;\nlet grossSince = Number(flow.get('grossSince')) || 0;\nif (grossExport && !grossSince) { grossSince = now; flow.set('grossSince', now); }\nif (!grossExport && grossSince) { grossSince = 0; flow.set('grossSince', 0); }\nconst grossSustained = grossSince > 0 && (now - grossSince) >= cfg.GROSS_MS;\n\n// --- ZAMEK po zasahu watchdogu ---\n// Jakmile rele rozepne, stridac je odpojeny a pretok \"zmizi\" - bez zamku by\n// regulace vyhodnotila stav jako zdravy, sepnula zpatky a cyklila donekonecna.\nconst lockUntil = Number(flow.get('lockUntil')) || 0;\nif (now < lockUntil) {\n    blocked.push('zamek po zasahu watchdogu (' + Math.ceil((lockUntil - now) / 1000) + ' s)');\n}\n\n// --- vypocet limitu ---\nlet limit;\nif (blocked.length) {\n    limit = 0;\n} else if (manual > 0) {\n    limit = manual;                                    // rezim ozivovani\n} else {\n    limit = (Number.isFinite(out) ? out : 0) + (min30 - buffer);\n}\nlimit = Math.max(0, Math.min(cfg.MAX_W, limit));\n\n// asymetrie: dolu okamzite, nahoru po rampe a jen s naplnenym oknem mereni.\n// Rampa je vazana na CAS, ne na pruchod smyckou. Kdyby byla na pruchod, zkraceni\n// tiku by tise zdvojnasobilo strmost nabehu - a s ni i pretok pri vypadku zateze.\n// Overeno simulaci: pri tiku 5 s a rampe \"za pruchod\" vyskocil pretok o 54 %.\nconst lastRun = Number(flow.get('lastRunTs')) || now;\nconst dtS = Math.min(30, Math.max(0, (now - lastRun) / 1000));   // strop kvuli dlouhym vypadkum\nflow.set('lastRunTs', now);\nconst last = Number(flow.get('lastLimit')) || 0;\nif (limit > last) limit = winFull ? Math.min(limit, last + cfg.RAMP_UP_W_S * dtS) : last;\nlimit = Math.floor(limit / cfg.STEP) * cfg.STEP;   // vzdy dolu - zaokrouhleni nahoru sni buffer\nflow.set('lastLimit', limit);\n\n// --- OMEZENI ZAPISU NA BLE ---\n// ha-ef-ble je alpha integrace, nema smysl ji bombardovat prikazem kazdou sekundu\n// (rychla cesta muze smycku spustit i 1x za sekundu). Posilame proto jen kdyz:\n//   - se limit skutecne zmenil A zaroven\n//   - bud jde limit DOLU (bezpecnostni smer - vzdy hned), nebo uplynul MIN_WRITE_MS\n//   - anebo uz je posledni zapis starsi nez RESEND_MS (obnova pro jistotu)\n// KRITICKE: rychla cesta muze smycku spustit i 1x za sekundu. Kdyby kazdy pruchod\n// poslal zapis, fronta BLE se zahlti a prikazy VUBEC nedorazi - stridac pak drzi\n// stary vykon i pres to, ze regulace uz davno prikazala nulu. Overeno simulaci.\nconst lastWrite = Number(flow.get('lastWriteTs')) || 0;\nconst lastSent = flow.get('lastSentLimit');\n// Porovnavame se SKUTECNYM stavem entity, ne s tim, co jsme naposledy poslali.\n// Jinak by externi zapis (mobilni aplikace EcoFlow, cloud, rucni zmena) prosel bez\n// povsimnuti - flow by si myslelo, ze je nastaveno spravne, a neopravilo by to.\n// Overeno 6.8.2026: aplikace prepisovala limit na 800 W, oprava trvala 56-154 s.\nconst devLimit = Number(flow.get('devLimit'));\nconst known = Number.isFinite(devLimit) ? devLimit\n            : (lastSent === undefined ? undefined : Number(lastSent));\nconst dLimit = (known === undefined || !Number.isFinite(known)) ? Infinity : (limit - known);\nconst goingDown = dLimit < 0;\nconst bigEnough = !Number.isFinite(dLimit) || Math.abs(dLimit) >= cfg.WRITE_DEADBAND || limit === 0;\nconst gap = goingDown ? cfg.MIN_WRITE_DOWN_MS : cfg.MIN_WRITE_UP_MS;\nconst mayWrite = (dLimit !== 0 && bigEnough && (now - lastWrite) >= gap)\n                 || (now - lastWrite) >= cfg.RESEND_MS;\nif (mayWrite && seen) { flow.set('lastWriteTs', now); flow.set('lastSentLimit', limit); }\n\n// --- DETEKCE, ZE PRIKAZY NEMAJI UCINEK ---\n// Cteni pres BLE muze chodit, zatimco zapis tise selhava (nebo stridac zamrzne).\n// Kdyz stridac drzi vyrazne vic, nez jsme mu naposledy prikazali, a trva to,\n// je ridici cesta rozbita -> health check to shodi a rele odpoji.\nconst sentRef = (lastSent === undefined) ? limit : lastSent;\nconst cmdIgnored = seen && Number.isFinite(out) && out > (sentRef + cfg.CMD_TOL);\nlet cmdBadSince = Number(flow.get('cmdBadSince')) || 0;\nif (cmdIgnored && !cmdBadSince) { cmdBadSince = now; flow.set('cmdBadSince', now); }\nif (!cmdIgnored && cmdBadSince) { cmdBadSince = 0; flow.set('cmdBadSince', 0); }\nif (cmdBadSince > 0 && (now - cmdBadSince) >= cfg.CMD_FAIL_MS) {\n    blocked.push('stridac neposloucha prikazy (' + out + ' W vs prikaz ' + sentRef + ' W)');\n}\n\n// --- heartbeat pro Shelly watchdog ---\n// Dokud je stav zdravy, posilame keepalive. Jakmile prestane, Shelly sam rozepne\n// za 30 s (toggle_after). Explicitni OFF posilame jen jednou, po cfg.UNHEALTHY_MS.\nconst bad = blocked.length > 0 || grossSustained;\nconst healthy = !bad;\n\nlet badSince = Number(flow.get('badSince')) || 0;\nif (bad && !badSince) { badSince = now; flow.set('badSince', now); }\nif (!bad && badSince) { badSince = 0; flow.set('badSince', 0); }\n\nlet offSent = flow.get('offSent') === true;\nif (healthy && offSent) { offSent = false; flow.set('offSent', false); }\n// Pri trvalem pretoku odpojujeme hned - tam se ceka uz tech 30 s v grossSustained.\n// U ostatnich blokujicich stavu staci zastavit heartbeat a nechat dobehnout toggle_after.\nconst emitOff = !offSent && (grossSustained ||\n                (bad && badSince > 0 && (now - badSince) >= cfg.UNHEALTHY_MS));\nif (emitOff) flow.set('offSent', true);\n\n// Zasah kvuli pretoku = zamknout. Pri opakovanych zasazich zamek zdvojnasobit,\n// aby se pri trvale zavade (spatna faze, stridac ignoruje limit) necvakalo rele.\nlet lockMin = 0;\nif (emitOff && grossSustained) {\n    let trips = Number(flow.get('tripCount')) || 0;\n    const lastTrip = Number(flow.get('lastTripTs')) || 0;\n    if (now - lastTrip > 1800000) trips = 0;      // po 30 min klidu pocitadlo nuluj\n    trips++;\n    lockMin = Math.min(cfg.LOCK_MAX, cfg.LOCK_MIN * Math.pow(2, trips - 1));\n    flow.set('tripCount', trips);\n    flow.set('lastTripTs', now);\n    flow.set('lockUntil', now + lockMin * 60000);\n    flow.set('grossSince', 0);\n}\n\nflow.set('status', { limit, min30, phaseNow, out, pv, elev, target, buffer, manual,\n                     blocked, grossExport, grossSustained });\nnode.status({\n    fill: grossSustained ? 'red' : (healthy ? (grossExport ? 'blue' : 'green') : 'yellow'),\n    shape: 'dot',\n    text: limit + ' W | ' + ch + ': ' + phaseNow + ' W | min30 ' + min30 + ' | ' +\n          (blocked.join(', ') || (grossExport ? 'pretok, srovnavam' : 'OK'))\n});\n\nconst outLimit = (seen && mayWrite) ? { payload: { limit: limit } } : null;\nconst outHB    = healthy ? { payload: 'keepalive' } : null;\nif (healthy) flow.set('lastHbTs', now);   // kdy naposled drzime rele sepnute\n// Notifikace jen pri SKUTECNE ZAVADE. Bezne provozni stavy (vypnuto uzivatelem,\n// noc, prazdne okno po restartu, dobihajici zamek) rele take rozepnou, ale\n// telefon kvuli nim budit nebudeme - jinak by pipal kazdy vecer a po kazdem deployi.\nconst ROUTINE = ['regulace vypnuta', 'slunce pod', 'chybi okno mereni', 'zamek po zasahu'];\nconst faults = blocked.filter(function (r) {\n    return !ROUTINE.some(function (p) { return r.indexOf(p) === 0; });\n});\nconst outOff   = emitOff\n    ? { payload: { reason: grossSustained\n        ? 'trvaly pretok na kanalu ' + worstCh + ' (' + worst + ' W)'\n        : blocked.join(', '), lockMin: lockMin,\n        notify: grossSustained || faults.length > 0 } }\n    : null;\n\nreturn [outLimit, outHB, outOff];\n",
        "outputs": 3,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 530,
        "y": 300,
        "wires": [
            [
                "n_setlimit",
                "n_dbg"
            ],
            [
                "n_hb"
            ],
            [
                "n_off",
                "n_offlog"
            ]
        ]
    },
    {
        "id": "n_setlimit",
        "type": "api-call-service",
        "z": "fve2026reg0000tab",
        "name": "STREAM: nastav limit",
        "server": "46a53aae.a18f74",
        "version": 7,
        "debugenabled": false,
        "action": "number.set_value",
        "floorId": [],
        "areaId": [],
        "deviceId": [],
        "entityId": [
            "number.ef_bk1163_maximum_output_power"
        ],
        "labelId": [],
        "data": "{\"value\": payload.limit}",
        "dataType": "jsonata",
        "mergeContext": "",
        "mustacheAltTags": false,
        "outputProperties": [],
        "queue": "none",
        "blockInputOverrides": false,
        "domain": "number",
        "service": "set_value",
        "x": 800,
        "y": 260,
        "wires": [
            []
        ]
    },
    {
        "id": "n_hb",
        "type": "http request",
        "z": "fve2026reg0000tab",
        "name": "Shelly: heartbeat (toggle_after 30 s)",
        "method": "GET",
        "ret": "txt",
        "paytoqs": "ignore",
        "url": "http://SHELLY-IP/rpc/Switch.Set?id=0&on=true&toggle_after=30",
        "tls": "",
        "persist": false,
        "proxy": "",
        "insecureHTTPParser": false,
        "authType": "",
        "senderr": false,
        "headers": [],
        "x": 830,
        "y": 320,
        "wires": [
            []
        ]
    },
    {
        "id": "n_off",
        "type": "http request",
        "z": "fve2026reg0000tab",
        "name": "Shelly: OFF (okamzite)",
        "method": "GET",
        "ret": "txt",
        "paytoqs": "ignore",
        "url": "http://SHELLY-IP/rpc/Switch.Set?id=0&on=false",
        "tls": "",
        "persist": false,
        "proxy": "",
        "insecureHTTPParser": false,
        "authType": "",
        "senderr": false,
        "headers": [],
        "x": 800,
        "y": 380,
        "wires": [
            []
        ]
    },
    {
        "id": "n_offlog",
        "type": "function",
        "z": "fve2026reg0000tab",
        "name": "log + notifikace",
        "func": "const p = msg.payload || {};\nconst r = p.reason || '?';\nconst lock = p.lockMin;\nnode.warn('FVE rele OFF: ' + r + (lock ? ' | zamek ' + lock + ' min' : ''));\n// bezne provozni stavy se jen loguji, telefon nebudi\nif (!p.notify) return null;\nreturn { payload: {\n    title: 'FVE odpojena watchdogem',\n    message: r + (lock ? ' Zamek na ' + lock + ' min.' : '')\n}};",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 790,
        "y": 430,
        "wires": [
            [
                "n_notify"
            ]
        ]
    },
    {
        "id": "n_dbg",
        "type": "debug",
        "z": "fve2026reg0000tab",
        "name": "limit (vypnuto)",
        "active": false,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 810,
        "y": 200,
        "wires": []
    },
    {
        "id": "n_start",
        "type": "inject",
        "z": "fve2026reg0000tab",
        "name": "pri startu / deployi",
        "props": [
            {
                "p": "payload"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": true,
        "onceDelay": 0.5,
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 200,
        "y": 540,
        "wires": [
            [
                "n_startfn"
            ]
        ]
    },
    {
        "id": "n_startfn",
        "type": "function",
        "z": "fve2026reg0000tab",
        "name": "reset kontextu + limit 0",
        "func": "// ===== STARTUP RESET =====\n// Pri kazdem startu / deployi: vynulovat kontext a stahnout limit stridace na 0.\n//\n// POZOR - schvalne se NEROZEPINA rele. Rozepnuti by znamenalo, ze KAZDY deploy\n// stoji cely reconnect delay (60 s az 20 min podle site. profilu). Neni potreba:\n//   - lastLimit=0 => prvni prikaz po startu je max RAMP_UP (50 W), ne stary limit\n//   - kdyz start trva dele nez 30 s, rele rozepne samo (vyprsi toggle_after)\n//   - kdyz je po startu neco spatne, health check heartbeat nepusti\nflow.set('lastLimit', 0);\nflow.set('badSince', 0);\nflow.set('grossSince', 0);\nflow.set('offSent', false);\nflow.set('nevyrobaCnt', 0);\nflow.set('lastRunTs', 0);\nnode.warn('FVE: startup reset - limit 0 W, kontext vycisten');\nreturn { payload: { limit: 0 } };\n",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 430,
        "y": 540,
        "wires": [
            [
                "n_setlimit"
            ]
        ]
    },
    {
        "id": "n_ntick",
        "type": "inject",
        "z": "fve2026reg0000tab",
        "name": "kazdych 5 min",
        "props": [
            {
                "p": "payload"
            }
        ],
        "repeat": "300",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 190,
        "y": 640,
        "wires": [
            [
                "n_sun2"
            ]
        ]
    },
    {
        "id": "n_sun2",
        "type": "api-current-state",
        "z": "fve2026reg0000tab",
        "name": "sun.sun",
        "server": "46a53aae.a18f74",
        "version": 3,
        "outputs": 1,
        "halt_if": "",
        "halt_if_type": "str",
        "halt_if_compare": "is",
        "entity_id": "sun.sun",
        "state_type": "str",
        "blockInputOverrides": false,
        "outputProperties": [
            {
                "property": "payload",
                "propertyType": "msg",
                "value": "",
                "valueType": "entityState"
            },
            {
                "property": "data",
                "propertyType": "msg",
                "value": "",
                "valueType": "entity"
            }
        ],
        "for": "0",
        "forType": "num",
        "forUnits": "minutes",
        "override_topic": false,
        "state_location": "payload",
        "override_payload": "msg",
        "entity_location": "data",
        "override_data": "msg",
        "x": 370,
        "y": 640,
        "wires": [
            [
                "n_nev"
            ]
        ]
    },
    {
        "id": "n_nev",
        "type": "function",
        "z": "fve2026reg0000tab",
        "name": "kontrola nevyroby",
        "func": "// ===== HLIDAC NEVYROBY (opak watchdogu) =====\n// Svitit ma, ale stridac nedodava -> mrtve BLE, rozepnute rele, vyhozeny jistic,\n// odpojeny konektor panelu. Zamerne NEPOUZIVA zadne externi cidlo (meteostanici):\n// diagnostika nesmi zaviset na zarizeni, ktere samo tise umre - overeno 7.8.2026,\n// kdy senzor osvitu 2 dny hlasil zamrzlych 1,9 W/m2 a hlidac by nesepnul nikdy.\nconst cfg = { MIN_ELEV: 25, MAX_PV: 20, CYCLES: 4 };   // CYCLES x 5 min = 20 min\n\nconst enabled = flow.get('enabled') === true;\nconst seen    = flow.get('streamSeen') === true;\nconst elev    = Number(msg.data && msg.data.attributes ? msg.data.attributes.elevation : NaN);\nconst pv      = Number(flow.get('streamPv'));\n\n// Hlidame jen kdyz regulace opravdu drzi rele sepnute. Kdyz je rozepnute (noc,\n// zamek, vypnuto uzivatelem), je nulova vyroba spravny stav, ne zavada.\nconst holding = (Date.now() - (Number(flow.get('lastHbTs')) || 0)) < 60000;\n\nconst suspect = enabled && seen && holding && elev > cfg.MIN_ELEV && !(pv > cfg.MAX_PV);\n\nlet n = Number(flow.get('nevyrobaCnt')) || 0;\nn = suspect ? n + 1 : 0;\nflow.set('nevyrobaCnt', n);\n\nif (n === cfg.CYCLES) {\n    return { payload: {\n        title: 'FVE nevyrabi',\n        message: 'Slunce je vysoko (' + Math.round(elev) + ' st.), rele sepnute, ale panely davaji jen '\n                 + pv + ' W uz ' + (cfg.CYCLES * 5) + ' minut. Zkontroluj stridac, BLE proxy a jistic v pergole.'\n    }};\n}\nreturn null;\n",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 550,
        "y": 640,
        "wires": [
            [
                "n_notify"
            ]
        ]
    },
    {
        "id": "n_notify",
        "type": "api-call-service",
        "z": "fve2026reg0000tab",
        "name": "notifikace na telefon",
        "server": "46a53aae.a18f74",
        "version": 7,
        "debugenabled": false,
        "action": "notify.mobile_app_TVUJ_TELEFON",
        "floorId": [],
        "areaId": [],
        "deviceId": [],
        "entityId": [],
        "labelId": [],
        "data": "{\n  \"title\": payload.title ? payload.title : \"FVE\",\n  \"message\": payload.message ? payload.message : \"FVE - neznama udalost: \" & $string(payload)\n}",
        "dataType": "jsonata",
        "mergeContext": "",
        "mustacheAltTags": false,
        "outputProperties": [],
        "queue": "none",
        "blockInputOverrides": false,
        "domain": "notify",
        "service": "mobile_app_TVUJ_TELEFON",
        "x": 780,
        "y": 640,
        "wires": [
            []
        ]
    },
    {
        "id": "46a53aae.a18f74",
        "type": "server",
        "name": "Home Assistant",
        "addon": true
    },
    {
        "id": "eccfd7b0b31efd32",
        "type": "global-config",
        "env": [],
        "modules": {
            "node-red-contrib-home-assistant-websocket": "0.80.3"
        }
    }
]