[
    {
        "id": "bee21680988e14c0",
        "type": "tab",
        "label": "Termostato Piano Terra",
        "disabled": false,
        "info": "Termostato reale Tuya Wi-Fi (T-storm) pilotato dal Termostato Virtuale iHost, esposto ad Alexa.\n\nFlusso comandi: Alexa/iHost -> callback HTTP -> register-device -> Direttive Alexa/iHost -> VIRTUALE>REALE -> Tuya SET\nFlusso stato: Tuya -> REALE>VIRTUALE -> put-device-state -> iHost -> Alexa"
    },
    {
        "id": "84de0b38a886432b",
        "type": "comment",
        "z": "bee21680988e14c0",
        "name": "LEGGI QUESTO PRIMA DI DEPLOYARE",
        "info": "Flow funzionante al 27/10/2026: termostato reale Tuya T-storm (famiglia BHT-002/BHT-8000, categoria \"wk\") pilotato da un termostato VIRTUALE iHost, esposto ad Alexa. Node-RED 4.1.10 in Docker su eWeLink iHost, node-red-contrib-ewelink-cube 1.3.7, node-red-contrib-tuya-smart-device 5.4.0.\n\n0) QUESTO FILE E' STATO RIPULITO A MANO. Prima che funzioni, rimetti tu:\n   - config \"Token\" (api-server): ipaddr = IP del host Node-RED, token = il tuo token API di iHost;\n   - register-device: service_address = l'IP con cui iHost raggiunge Node-RED (porta 1880 fissa nel plugin);\n   - nodo Tuya: deviceId + deviceKey.\n   ATTENZIONE: il token di iHost viene esportato IN CHIARO dal tuo editor, non finisce nel credstore. Non pubblicare mai un export senza aver sostituito quel campo: apre le API locali di iHost, cioe' tutti i dispositivi di casa, non solo il termostato.\n\n1) deviceId / deviceKey del nodo Tuya sono vuoti di proposito: stanno nel credstore e l'export non li porta via.\n\n2) DPS DEL TERMOSTATO REALE, verificati su questo esemplare:\n   dp1 = on/off, booleano\n   dp2 = temperatura target, valore = °C x 2   (5..35 °C, passo 0,5)\n   dp3 = temperatura rilevata, valore = °C x 2\n   dp4 = \"0\" programma, \"1\" manuale\n   Il termostato DEVE restare in manuale: in programma ignora la scrittura del setpoint perche' la gestione oraria sta nel microcontrollore, non nell'app. Il flow scrive dp4=\"1\" insieme a dp2 a ogni cambio di temperatura, quindi non serve toccare nulla dal pannello fisico.\n\n3) Smart Life e questo flow usano lo STESSO canale locale: un solo consumatore alla volta. Se apri l'app il flow perde temporaneamente la connessione, poi si ricollega da solo.\n\n4) L'ON/OFF DI ALEXA PASSA DA thermostatMode, NON da powerState: il ponte Matter scrive \"OFF\" per spegnere e \"MANUAL\" per accendere. La registrazione deve quindi dichiarare supportedModes [\"MANUAL\",\"OFF\"] nella capacita' \"thermostat\". Con il solo MANUAL, ogni upload che contiene OFF viene rifiutato in blocco (INVALID_PARAMETERS) e lo stato del virtuale si congela: il sintomo e' \"Alexa mi torna acceso dopo lo spegnimento\", che sembra un bug di sincronizzazione e non lo e'.\n\n5) LE SETTINGS E I TAG SI SCRIVONO UNA SOLA VOLTA, alla creazione del record. iHost non le aggiorna piu': ri-registrare con lo stesso device_id (third_serial_number) non cambia ne' le supportedModes ne' i tag __reg_dev_node_id / __api_server_node_id. Se sono sbagliati l'unica cura e' un device_id MAI USATO, e l'ordine conta: prima scrivi le capability nel nodo, poi premi la registrazione, poi allinea i serial nei nodi di stato. Registrare prima e correggere dopo brucia un serial.\n\n6) Il campo \"Category\" di register-device resta VUOTO perche' il plugin 1.3.7 non offre \"Thermostat\" nel menu (le voci sono Plug, Switch, Light, Curtain, i sensori, Button, Fan Light, Air Conditioner, Fan, Light Strip). Non e' un errore e non va \"corretto\": cio' che rende il dispositivo un termostato sono le capabilities_v2.\n\n7) I due rami della mappa thermostatMode devono restare SIMMETRICI: \"MANUAL\" accende in ingresso perche' \"MANUAL\" viene pubblicato in uscita quando dp1 e' true, e lo stesso vale per OFF. Con l'asimmetria (pubblichi sempre MANUAL ma solo OFF comanda) si generano raffiche di true/false sul reale, perche' iHost rimanda indietro, come direttiva, lo stato che noi stessi abbiamo pubblicato sei-dieci secondi prima. Il guard anti-eco in \"VIRTUALE -> REALE\" confronta la direttiva con l'ultimo stato pubblicato, capacita' per capacita', finestra 60 secondi: e' la parte piu' delicata del flow, non alleggerirla.\n\n8) La bandiera MODO_CON_OFF nel nodo \"REALE -> VIRTUALE\" e' ereditata e non letta da nessuna riga: il modo segue dp1 senza condizioni. Lasciala o toglila, non cambia niente. Non provare a metterla a false sperando di spegnere Alexa da un'altra strada.",
        "x": 190,
        "y": 60,
        "wires": []
    },
    {
        "id": "a9cc2caca14b5126",
        "type": "event-state",
        "z": "bee21680988e14c0",
        "name": "Cambi di stato del VIRTUALE",
        "server": "9af69f7f5c9a8d45",
        "list": "",
        "category": "thermostat",
        "device": "38ea98ff-8518-43df-a266-a532e0551d40",
        "state": "all",
        "x": 140,
        "y": 140,
        "wires": [
            []
        ]
    },
    {
        "id": "3ca700f3ef809004",
        "type": "function",
        "z": "bee21680988e14c0",
        "name": "VIRTUALE -> REALE",
        "func": "// ====== CONFIGURAZIONE TERMOSTATO REALE ======\nvar CFG = {\n    DP_POWER: \"1\",\n    DP_SETPOINT: \"2\",\n    SCALE: 2,        // dps = °C * SCALE\n    MIN: 5,\n    MAX: 35,\n    DP_MODE: \"4\",    // CONFERMATO sul tuo esemplare: dp4 = \"0\" programma, \"1\" manuale\n    MOD_MANUALE: \"1\" // valore del DP_MODE per la modalita' manuale\n};\n\nvar data = msg.payload;\nif (typeof data === \"string\") { try { data = JSON.parse(data); } catch (e) { return null; } }\nif (Array.isArray(data)) { data = data[0]; }\nif (!data || typeof data !== \"object\") { return null; }\n\n// Accetta {endpoint, payload:{state}} del callback, {directive, state} del nodo\n// 'Direttive Alexa/iHost' e lo stato nudo dell'event-state.\nvar st = data;\nif (data.endpoint && data.payload) { st = data.payload.state || data.payload; }\nelse if (data.state) { st = data.state; }\nif (!st || typeof st !== \"object\") { return null; }\n\n// ===== cosa chiede il controller, letto grezzo =====\nvar inPower = (st.power && st.power.powerState !== undefined) ? String(st.power.powerState).toLowerCase() : undefined;\nvar inMode = (st.thermostat && st.thermostat[\"thermostat-mode\"] && st.thermostat[\"thermostat-mode\"].thermostatMode !== undefined)\n    ? String(st.thermostat[\"thermostat-mode\"].thermostatMode).toUpperCase() : undefined;\nvar rawSp = st[\"thermostat-target-setpoint\"];\nvar inSp;\nif (rawSp !== undefined && rawSp !== null) {\n    var val = (typeof rawSp === \"number\") ? rawSp\n        : (rawSp[\"manual-mode\"] && rawSp[\"manual-mode\"].targetSetpoint);\n    if (typeof val === \"number\" && !isNaN(val)) { inSp = val; }\n}\nnode.warn(\"DIRETTIVA IN -> power=\" + (inPower === undefined ? \"-\" : inPower) +\n    \" mode=\" + (inMode === undefined ? \"-\" : inMode) +\n    \" setpoint=\" + (inSp === undefined ? \"-\" : inSp));\n\n// volonta' completa del controller: iHost invia UNA capacita' per volta\nvar target = flow.get(\"target\") || {};\nif (inPower !== undefined) { target.power = (inPower === \"on\"); }\n// Il ponte Matter/Alexa comanda l'on/off attraverso il modo: OFF = spento,\n// MANUAL = acceso in manuale. 'REALE -> VIRTUALE' pubblica il modo ricavato da\n// dp1, quindi ingresso e uscita seguono la STESSA convenzione: è questa\n// simmetria che rende sicuro il ramo Manual. Con l'asimmetria (sempre MANUAL in\n// uscita, MANUAL che accende in ingresso) si generavano le raffiche del 26/10;\n// senza il ramo in ingresso il pulsante di Alexa era muto.\nif (inMode === \"OFF\") { target.power = false; }\nif (inMode === \"MANUAL\") { target.power = true; }\nif (inSp !== undefined) { target.setpoint = Math.min(CFG.MAX, Math.max(CFG.MIN, Math.round(inSp * 2) / 2)); }\nflow.set(\"target\", target);\n\n// ===== scarto dell'eco =====\n// Confronta la direttiva con LO STATO CHE NOI ABBIAMO PUBBLICATO l'ultima volta,\n// capacita' per capacita', solo su quelle presenti nella direttiva.\n// Prima si confrontavano le chiavi Tuya (dp1/dp2/dp4) con i valori del virtuale:\n// due spazi diversi, quindi il guard non scattava mai e ogni rimando di iHost\n// ridefiniva un comando sul reale.\nvar eco = flow.get(\"eco\");\nif (eco && eco.stato && (Date.now() - eco.ts) < 60000) {\n    var confronti = 0, coincide = true;\n    if (inPower !== undefined) {\n        confronti++;\n        coincide = coincide && (inPower === String((eco.stato.power && eco.stato.power.powerState) || \"\").toLowerCase());\n    }\n    if (inMode !== undefined) {\n        confronti++;\n        var pubMode = eco.stato.thermostat && eco.stato.thermostat[\"thermostat-mode\"] && eco.stato.thermostat[\"thermostat-mode\"].thermostatMode;\n        coincide = coincide && (inMode === String(pubMode || \"\").toUpperCase());\n    }\n    if (inSp !== undefined) {\n        confronti++;\n        var pubSp = eco.stato[\"thermostat-target-setpoint\"] && eco.stato[\"thermostat-target-setpoint\"][\"manual-mode\"] && eco.stato[\"thermostat-target-setpoint\"][\"manual-mode\"].targetSetpoint;\n        coincide = coincide && (Math.round(inSp * 2) === Math.round(pubSp * 2));\n    }\n    if (confronti > 0 && coincide) {\n        node.warn(\"   -> eco del nostro stesso stato pubblicato: ignorata\");\n        node.status({ fill: \"#a0a0a0\", shape: \"ring\", text: \"eco: ignorato\" });\n        return null;\n    }\n}\n\n// ===== Il comando si scrive SEMPRE: niente confronti con lo stato reale ======\n// (le versioni precedenti saltavano la scrittura se il cache \"reale\" diceva gia'\n// \"cosi'\": con il cache vuoto o vecchio l'ordine di Alexa/app spariva nel nulla)\nvar cmd = {};\nif (target.power !== undefined) { cmd[CFG.DP_POWER] = target.power; }\nif (target.setpoint !== undefined) {\n    cmd[CFG.DP_SETPOINT] = Math.round(target.setpoint * CFG.SCALE);\n    if (CFG.DP_MODE) { cmd[CFG.DP_MODE] = CFG.MOD_MANUALE; }\n}\nif (Object.keys(cmd).length === 0) {\n    node.status({ fill: \"#a0a0a0\", shape: \"ring\", text: \"nessun comando riconosciuto\" });\n    return null;\n}\n\nflow.set(\"pendente\", { cmd: cmd, ts: Date.now() });\nnode.status({ fill: \"green\", shape: \"dot\", text: JSON.stringify(cmd) });\n\nmsg.payload = { operation: \"SET\", multiple: true, data: cmd };\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 420,
        "y": 180,
        "wires": [
            [
                "42f9c03d14bea9ff",
                "f3ad86280b9ace56"
            ]
        ]
    },
    {
        "id": "f3ad86280b9ace56",
        "type": "debug",
        "z": "bee21680988e14c0",
        "name": "COMANDI INVIATI AL REALE",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload.data",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 710,
        "y": 280,
        "wires": []
    },
    {
        "id": "42f9c03d14bea9ff",
        "type": "delay",
        "z": "bee21680988e14c0",
        "name": "serializza 1/2s",
        "pauseType": "rate",
        "timeout": "5",
        "timeoutUnits": "seconds",
        "rate": "1",
        "nbRateUnits": "2",
        "rateUnits": "second",
        "randomFirst": "1",
        "randomLast": "5",
        "randomUnits": "seconds",
        "drop": false,
        "allowrate": false,
        "outputs": 1,
        "x": 700,
        "y": 180,
        "wires": [
            [
                "3a289fb291add864"
            ]
        ]
    },
    {
        "id": "be8b20132aeb237c",
        "type": "inject",
        "z": "bee21680988e14c0",
        "name": "Poll stato ogni 30s",
        "props": [
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "30",
        "crontab": "",
        "once": true,
        "onceDelay": "5",
        "topic": "poll",
        "payload": "",
        "payloadType": "date",
        "x": 170,
        "y": 320,
        "wires": [
            [
                "5b2576964d49e652"
            ]
        ]
    },
    {
        "id": "25843aa2ce1ef624",
        "type": "inject",
        "z": "bee21680988e14c0",
        "name": "Ri-sincronizza (manuale)",
        "props": [
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": "0.1",
        "topic": "resync",
        "payload": "",
        "payloadType": "date",
        "x": 180,
        "y": 370,
        "wires": [
            [
                "5b2576964d49e652"
            ]
        ]
    },
    {
        "id": "5b2576964d49e652",
        "type": "function",
        "z": "bee21680988e14c0",
        "name": "Richiesta GET",
        "func": "// DP_QUERY: la risposta arriva full (con DP 1) sull'output \"data\" del nodo Tuya.\nif (msg.topic === \"resync\") {\n    flow.set(\"reale\", {});\n    flow.set(\"virtualeFP\", null);\n    flow.set(\"pendente\", null);\n}\nmsg.payload = { operation: \"GET\" };\nreturn msg;\n",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 430,
        "y": 345,
        "wires": [
            [
                "42f9c03d14bea9ff"
            ]
        ]
    },
    {
        "id": "3a289fb291add864",
        "type": "tuya-smart-device",
        "z": "bee21680988e14c0",
        "deviceName": "Termostato piano terra ewelink",
        "disableAutoStart": false,
        "deviceId": "",
        "deviceKey": "",
        "storeAsCreds": true,
        "deviceIp": "",
        "retryTimeout": "10000",
        "findTimeout": "10000",
        "tuyaVersion": "3.3",
        "eventMode": "event-both",
        "logLevel": "log-level-info",
        "x": 1010,
        "y": 240,
        "wires": [
            [
                "b5e98335e062107e",
                "1ed4643c48a33c47"
            ],
            [
                "b64e01f3bd4c17e2"
            ]
        ]
    },
    {
        "id": "1ed4643c48a33c47",
        "type": "debug",
        "z": "bee21680988e14c0",
        "name": "STATO REALE (dump DPS)",
        "active": false,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload.data.dps",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 1290,
        "y": 340,
        "wires": []
    },
    {
        "id": "b5e98335e062107e",
        "type": "function",
        "z": "bee21680988e14c0",
        "name": "REALE -> VIRTUALE",
        "func": "// ====== CONFIGURAZIONE TERMOSTATO REALE ======\nvar CFG = {\n    DP_POWER: \"1\",\n    DP_SETPOINT: \"2\",\n    DP_TEMP: \"3\",\n    SCALE: 2,\n    MODO_CON_OFF: true // ereditata, non letta da nessuna riga: il modo segue dp1 sempre\n};\n\nfunction boolPow(v) { return v === true || v === 1 || v === \"1\"; }\n\nvar p = msg.payload;\nif (!p) { return null; }\n\n// Estrazione sicura dei dps dal report Tuya\nvar dps = (p.data && p.data.dps) ? p.data.dps : (p.dps ? p.dps : null);\nif (!dps || typeof dps !== \"object\") { return null; }\n\nvar chiavi = Object.keys(dps);\nif (chiavi.length === 0) { return null; }\n\n// Recupera lo stato memorizzato nel flusso e unisci i nuovi dati\nvar reale = flow.get(\"reale\") || {};\nfor (var i = 0; i < chiavi.length; i++) { \n    reale[chiavi[i]] = dps[chiavi[i]]; \n}\nif (reale[CFG.DP_POWER] !== undefined) { \n    reale[CFG.DP_POWER] = boolPow(reale[CFG.DP_POWER]); \n}\nflow.set(\"reale\", reale);\n\n// Protezione: senza lo stato di accensione non possiamo inviare aggiornamenti completi\nif (reale[CFG.DP_POWER] === undefined) { return null; }\n\n// Freno anti-oscillazione: se abbiamo inviato un comando da meno di 6s, \n// ignoriamo i report che non riflettono ancora il valore aggiornato.\nvar pend = flow.get(\"pendente\");\nif (pend && (Date.now() - pend.ts) < 6000) {\n    var riguardaComando = chiavi.some(function (k) {\n        return Object.prototype.hasOwnProperty.call(pend.cmd, k);\n    });\n    if (!riguardaComando) { return null; }\n    var confermato = Object.keys(pend.cmd).every(function (k) { \n        return reale[k] === pend.cmd[k]; \n    });\n    if (confermato) { \n        flow.set(\"pendente\", null); \n    }\n}\n\n// Costruzione del payload standard richiesto dall'iHost\nvar stato = {\n    power: { powerState: reale[CFG.DP_POWER] ? \"on\" : \"off\" },\n    thermostat: { \"thermostat-mode\": { thermostatMode: (!reale[CFG.DP_POWER]) ? \"OFF\" : \"MANUAL\" } }\n};\n\nif (typeof reale[CFG.DP_SETPOINT] === \"number\") {\n    // Converte es. 43 di Tuya in 21.5°C per Alexa\n    stato[\"thermostat-target-setpoint\"] = { \"manual-mode\": { targetSetpoint: reale[CFG.DP_SETPOINT] / CFG.SCALE } };\n}\n\nif (typeof reale[CFG.DP_TEMP] === \"number\") {\n    stato.temperature = { temperature: reale[CFG.DP_TEMP] / CFG.SCALE };\n}\n\n// Evita di inviare dati duplicati all'iHost per non generare loop infiniti\nvar fp = JSON.stringify(stato);\nif (fp === flow.get(\"virtualeFP\")) {\n    node.status({ fill: \"#a0a0a0\", shape: \"ring\", text: \"nessuna variazione\" });\n    return null;\n}\nflow.set(\"virtualeFP\", fp);\n\nflow.set(\"eco\", { stato: stato, ts: Date.now() });\n\nnode.warn(\"PUBBLICO A IHOST -> power=\" + stato.power.powerState +\n    \" mode=\" + stato.thermostat[\"thermostat-mode\"].thermostatMode +\n    \" setpoint=\" + ((stato[\"thermostat-target-setpoint\"] && stato[\"thermostat-target-setpoint\"][\"manual-mode\"]\n        && stato[\"thermostat-target-setpoint\"][\"manual-mode\"].targetSetpoint) || \"-\"));\n\nnode.status({ fill: \"blue\", shape: \"dot\", text: \"virtuale aggiornato\" });\n\nmsg.payload = stato;\nreturn msg;\n",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 1480,
        "y": 240,
        "wires": [
            [
                "b651f6c479a62b3d"
            ]
        ]
    },
    {
        "id": "b651f6c479a62b3d",
        "type": "put-device-state",
        "z": "bee21680988e14c0",
        "name": "Aggiorna Virtuale (e Alexa)",
        "server": "9af69f7f5c9a8d45",
        "category": "thermostat",
        "device": "38ea98ff-8518-43df-a266-a532e0551d40",
        "state": "",
        "number": "a3f1c7d2-5b84-4e19-9c6a-0d2f7b41e885",
        "x": 1740,
        "y": 240,
        "wires": [
            [
                "0a433446b4bdcae0"
            ]
        ]
    },
    {
        "id": "0a433446b4bdcae0",
        "type": "function",
        "z": "bee21680988e14c0",
        "name": "Esito upload iHost",
        "func": "// ATTENZIONE: iHost non risponde mai con msg.payload.error. La risposta e'\n// { header: { name: \"Response\" | \"ErrorResponse\" }, payload: {} | {type,description} }\n// Il codice vecchio controllava un campo inesistente e percio' segnava\n// \"stato pubblicato\" anche quando iHost aveva appena rifiutato l'upload.\nvar r = msg.payload || {};\nvar h = r.header || {};\nvar p = r.payload || {};\nvar nome = h.name || \"sconosciuto\";\n\nif (nome === \"ErrorResponse\" || p.type) {\n    node.status({ fill: \"red\", shape: \"ring\", text: \"RIFIUTATO: \" + (p.type || nome) });\n    node.error(\"iHost ha rifiutato lo stato -> \" + (p.type || nome) + \": \" + (p.description || \"\"), msg);\n    return msg;\n}\n\nnode.status({ fill: \"green\", shape: \"dot\", text: \"OK (\" + nome + \")\" });\nreturn msg;\n",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 1990,
        "y": 240,
        "wires": [
            [
                "aca6ced3a4481e1a"
            ]
        ]
    },
    {
        "id": "aca6ced3a4481e1a",
        "type": "debug",
        "z": "bee21680988e14c0",
        "name": "ESITO",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 2210,
        "y": 240,
        "wires": []
    },
    {
        "id": "b64e01f3bd4c17e2",
        "type": "function",
        "z": "bee21680988e14c0",
        "name": "Connessione Tuya",
        "func": "// Output 2 del nodo Tuya: { state: CONNECTED | CONNECTING | DISCONNECTED | ERROR }\n// Uscita 1: comando resync verso il nodo Tuya (come prima).\n// Uscita 2: corpo per il nodo http \"Segnala online/offline a iHost\" (solo ai cambi).\nvar BODY = {\n    id: '9af69f7f5c9a8d45',\n    deviceId: '38ea98ff-8518-43df-a266-a532e0551d40',\n    thirdPartyDeviceId: 'a3f1c7d2-5b84-4e19-9c6a-0d2f7b41e885'\n};\n\nfunction reportMsg(up) {\n    // Il nodo Tuya ripete DISCONNECTED a ogni retry: segnalo solo i cambi.\n    if (flow.get('onlineReale') === up) return null;\n    flow.set('onlineReale', up);\n    node.warn('Termostato ' + (up ? 'online' : 'OFFLINE'));\n    return { payload: Object.assign({}, BODY, { params: JSON.stringify({ online: up }) }) };\n}\n\nvar stato = (msg.payload && msg.payload.state) || \"SCONOSCIUTO\";\nvar versoTuya = null;\nvar versoIhost = null;\nif (stato === \"CONNECTED\") {\n    node.status({ fill: \"green\", shape: \"dot\", text: \"connesso\" });\n    versoIhost = reportMsg(true);\n    flow.set(\"virtualeFP\", null);\n    versoTuya = { payload: { operation: \"GET\" }, topic: \"resync-auto\" };\n} else if (stato === \"CONNECTING\") {\n    node.status({ fill: \"yellow\", shape: \"ring\", text: \"connessione...\" });\n} else {\n    node.status({ fill: \"red\", shape: \"ring\", text: stato.toLowerCase() });\n    versoIhost = reportMsg(false);\n}\nreturn [versoTuya, versoIhost];",
        "outputs": 2,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 950,
        "y": 100,
        "wires": [
            [
                "42f9c03d14bea9ff"
            ],
            [
                "c1a11ad3e5f60001"
            ]
        ]
    },
    {
        "id": "fe20ec7d69a5c1c4",
        "type": "function",
        "z": "bee21680988e14c0",
        "name": "Direttive Alexa/iHost",
        "func": "// Il nodo register-device emette qui ogni direttiva che iHost inoltra\n// (PowerController / ThermostatController di Alexa). Da questo nodo parte la\n// scrittura sul reale: il ramo 'Cambi di stato del VIRTUALE' e' scollegato\n// apposta, perche' l'event-state vede anche la nostra stessa pubblicazione.\nvar dir = msg.payload || {};\nvar nome = (dir.header && dir.header.name) || \"?\";\nvar st = dir.payload && dir.payload.state ? Object.keys(dir.payload.state) : [];\nvar text = nome + (st.length ? \" -> \" + st.join(\",\") : \"\");\nnode.status({ fill: \"blue\", shape: \"dot\", text: text });\nmsg.payload = { directive: nome, state: (dir.payload && dir.payload.state) || null };\nreturn msg;\n",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 1180,
        "y": 540,
        "wires": [
            [
                "248159bba3912862",
                "3ca700f3ef809004"
            ]
        ]
    },
    {
        "id": "248159bba3912862",
        "type": "debug",
        "z": "bee21680988e14c0",
        "name": "DIRETTIVE",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 1410,
        "y": 540,
        "wires": []
    },
    {
        "id": "f907096a67f148fc",
        "type": "catch",
        "z": "bee21680988e14c0",
        "name": "errori Tuya",
        "scope": [
            "3a289fb291add864"
        ],
        "uncaught": false,
        "x": 1250,
        "y": 420,
        "wires": [
            [
                "ad360988a6a80dc3"
            ]
        ]
    },
    {
        "id": "ad360988a6a80dc3",
        "type": "function",
        "z": "bee21680988e14c0",
        "name": "Gestione errore",
        "func": "// Timeout/errore di scrittura: togliamo il comando pendente cosi' il prossimo\n// cambio sul virtuale verra' ritentato invece di restare bloccato.\nflow.set(\"pendente\", null);\nnode.status({ fill: \"red\", shape: \"ring\", text: \"errore Tuya\" });\nmsg.payload = msg.error;\nreturn msg;\n",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 1440,
        "y": 420,
        "wires": [
            [
                "1e0f331c1e46b5f3"
            ]
        ]
    },
    {
        "id": "1e0f331c1e46b5f3",
        "type": "debug",
        "z": "bee21680988e14c0",
        "name": "ERRORI",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 1630,
        "y": 420,
        "wires": []
    },
    {
        "id": "0a1a78c5d39a6664",
        "type": "comment",
        "z": "bee21680988e14c0",
        "name": "DA FARE DOPO L'IMPORT",
        "info": "In ordine, non saltare i passi.\n\n1) Compila il config \"Token\" (api-server): ipaddr = IP della macchina Node-RED, token = il tuo token API di iHost.\n2) Compila deviceId + deviceKey nel nodo Tuya. eventMode DEVE restare \"event-both\": con \"event-data\" i dp-refresh parziali non arrivano, il cache dello stato reale resta buco e il virtuale non si aggiorna piu'.\n3) Metti nel nodo register-device un device_id MAI USATO (e' il third_serial_number: iHost ci aggancia il record e non lo tocca piu').\n4) L'uscita del nodo register-device va su \"Direttive Alexa/iHost\", gia' cablata qui. Il nodo \"Cambi di stato del VIRTUALE\" (event-state) e' volutamente SCOLLEGATO: i comandi entrano dal callback HTTP, non dall'event-state, perche' l'event-state vede anche la nostra stessa pubblicazione e richiude l'anello.\n5) Deploy, poi manda un input al nodo register-device (un inject puntato, o il pulsante del tab DIAGNOSTICA se ce l'hai): solo con un input il plugin registra, e iHost crea il dispositivo.\n6) Prendi il serial_number assegnato da iHost (app iHost -> dispositivo -> campo \"ID Dispositivo\") e mettilo nel campo Device dei nodi event-state e put-device-state. Il campo number di put-device-state = il device_id del punto 3.\n   Attenzione: nel pannello il campo Device e' un menu a tendina che mostra il NOME del dispositivo, non il serial. Se in iHost ci sono due dispositivi con lo stesso nome non puoi sapere quale hai scelto: rinomina l'altro.\n7) Alexa: scoperta dispositivi. Se avevi un termostato virtuale precedente, eliminalo da iHost prima di ri-abbinare, perche' Alexa ne vede due e non sai quale piloti.\n8) Un upload che risponde \"Response\" NON dice su quale dispositivo hai scritto: se in iHost restano due termostati, il vecchio accetta gli stato esattamente come il nuovo. Per distinguere guarda la pagina del dispositivo, dove il campo \"ID Dispositivo\" e' l'unico che dice la verita'.",
        "x": 330,
        "y": 460,
        "wires": []
    },
    {
        "id": "790e72066eb43314",
        "type": "register-device",
        "z": "bee21680988e14c0",
        "name": "Virtual Thermostat 790e72066eb43314",
        "server": "9af69f7f5c9a8d45",
        "device_id": "a3f1c7d2-5b84-4e19-9c6a-0d2f7b41e885",
        "device_name": "Termostato Piano Terra",
        "category": "thermostat",
        "capabilities": "null",
        "manufacturer": "Ska",
        "model": "First",
        "firmware_version": "1.0.0",
        "service_address": "TUO-IP",
        "tags": "{}",
        "state": "{\"power\":{\"powerState\":\"off\"},\"temperature\":{\"temperature\":22},\"thermostat\":{\"thermostat-mode\":{\"thermostatMode\":\"MANUAL\"}},\"thermostat-target-setpoint\":{\"manual-mode\":{\"targetSetpoint\":21}}}",
        "capabilities_v2": "[{\"capability\":\"power\",\"permission\":\"1100\"},{\"capability\":\"temperature\",\"permission\":\"0100\"},{\"capability\":\"thermostat\",\"permission\":\"1100\",\"settings\":\"{\\\"supportedModes\\\":{\\\"type\\\":\\\"enum\\\",\\\"permission\\\":\\\"01\\\",\\\"values\\\":[\\\"MANUAL\\\",\\\"OFF\\\"]}}\",\"name\":\"thermostat-mode\"},{\"capability\":\"thermostat-target-setpoint\",\"permission\":\"1110\",\"settings\":\"{\\\"temperatureUnit\\\":{\\\"type\\\":\\\"enum\\\",\\\"permission\\\":\\\"11\\\",\\\"value\\\":\\\"c\\\",\\\"values\\\":[\\\"c\\\",\\\"f\\\"]},\\\"temperatureRange\\\":{\\\"type\\\":\\\"numeric\\\",\\\"permission\\\":\\\"01\\\",\\\"min\\\":5,\\\"max\\\":35,\\\"step\\\":0.5}}\",\"name\":\"manual-mode\"}]",
        "x": 730,
        "y": 540,
        "wires": [
            [
                "fe20ec7d69a5c1c4"
            ]
        ]
    },
    {
        "id": "c1a11ad3e5f60001",
        "type": "http request",
        "z": "bee21680988e14c0",
        "name": "Segnala online/offline a iHost",
        "method": "POST",
        "ret": "txt",
        "paytoqs": "ignore",
        "url": "http://127.0.0.1:1880/ewelink-cube-api-v1/upload-thirdparty-device-online",
        "tls": "",
        "persist": false,
        "proxy": "",
        "insecureHTTPParser": false,
        "authType": "",
        "senderr": false,
        "headers": [],
        "x": 1280,
        "y": 100,
        "wires": [
            [
                "c1a11ad3e5f60002"
            ]
        ]
    },
    {
        "id": "c1a11ad3e5f60002",
        "type": "debug",
        "z": "bee21680988e14c0",
        "name": "esito online iHost",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 1570,
        "y": 100,
        "wires": []
    },
    {
        "id": "9af69f7f5c9a8d45",
        "type": "api-server",
        "name": "Token",
        "ip": "ihost.local",
        "ipaddr": "TUO-IP",
        "token": "TUO-TOKEN"
    },
    {
        "id": "9529f7f3225e145c",
        "type": "global-config",
        "env": [],
        "modules": {
            "node-red-contrib-ewelink-cube": "1.3.7",
            "node-red-contrib-tuya-smart-device": "5.4.0"
        }
    }
]
