From a774e3a955553bf78bb8f70adc1c2e46307a5b5e Mon Sep 17 00:00:00 2001 From: raph666 Date: Sat, 18 Jul 2026 15:36:11 +0200 Subject: [PATCH] Add sanitized protocol references --- references/arkteos_nodered_flow.json | 468 +++++++++++++++++++++++++++ references/proxy_protocol_notes.md | 157 +++++++++ 2 files changed, 625 insertions(+) create mode 100644 references/arkteos_nodered_flow.json create mode 100644 references/proxy_protocol_notes.md diff --git a/references/arkteos_nodered_flow.json b/references/arkteos_nodered_flow.json new file mode 100644 index 0000000..82a2c45 --- /dev/null +++ b/references/arkteos_nodered_flow.json @@ -0,0 +1,468 @@ +[ + { + "id": "arkteos_flow", + "type": "tab", + "label": "Arkteos REG3", + "disabled": false, + "info": "Intégration PAC Arkteos Zuran 4 via addon proxy.\nStreaming permanent sur :9641.\nPublication MQTT avec discovery automatique HA, throttling, filtrage valeurs aberrantes et statuts.\nRemplacer par l'IP de Home Assistant dans le nœud PAC via proxy." + }, + { + "id": "arkteos_tcp_in", + "type": "tcp in", + "z": "arkteos_flow", + "name": "PAC via proxy", + "server": "client", + "host": "", + "port": "9641", + "datamode": "stream", + "datatype": "buffer", + "newline": "", + "topic": "", + "trim": false, + "base64": false, + "tls": "", + "x": 110, + "y": 160, + "wires": [ + [ + "arkteos_watchdog", + "arkteos_parse_frame" + ] + ] + }, + { + "id": "arkteos_watchdog", + "type": "function", + "z": "arkteos_flow", + "name": "Watchdog connexion", + "func": "flow.set('arkteos_last_frame', Date.now());\nflow.set('arkteos_connected', true);\nreturn null;\n", + "outputs": 1, + "noerr": 0, + "initialize": "flow.set('arkteos_last_frame', 0);\nflow.set('arkteos_connected', false);\n", + "finalize": "", + "libs": [], + "x": 500, + "y": 40, + "wires": [ + [] + ] + }, + { + "id": "arkteos_watchdog_check_inject", + "type": "inject", + "z": "arkteos_flow", + "name": "Check connexion toutes les 15s", + "props": [ + { + "p": "payload" + } + ], + "repeat": "15", + "crontab": "", + "once": true, + "onceDelay": 10, + "topic": "", + "payload": "", + "payloadType": "date", + "x": 180, + "y": 40, + "wires": [ + [ + "arkteos_watchdog_check" + ] + ] + }, + { + "id": "arkteos_watchdog_check", + "type": "function", + "z": "arkteos_flow", + "name": "Check timeout", + "func": "const last = flow.get('arkteos_last_frame') || 0;\nconst connected = flow.get('arkteos_connected') || false;\nconst now = Date.now();\nconst elapsed = now - last;\nconst isConnected = last > 0 && elapsed < 30000;\n\nif (isConnected !== connected) {\n flow.set('arkteos_connected', isConnected);\n msg.payload = isConnected ? 'ON' : 'OFF';\n msg.topic = 'arkteos/status/connected';\n return msg;\n}\nif (elapsed % 60000 < 15000) {\n msg.payload = isConnected ? 'ON' : 'OFF';\n msg.topic = 'arkteos/status/connected';\n return msg;\n}\nreturn null;\n", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 720, + "y": 40, + "wires": [ + [ + "6dceb3fdda857578" + ] + ] + }, + { + "id": "arkteos_parse_frame", + "type": "function", + "z": "arkteos_flow", + "name": "Parse frame", + "func": "const buf = msg.payload;\nif (!Buffer.isBuffer(buf) || buf.length === 0) return null;\n\nconst size = buf.length;\n\nfunction read16s(b, i) {\n const v = b[i] + b[i + 1] * 256;\n return v >= 32768 ? v - 65536 : v;\n}\nfunction read16u(b, i) {\n return b[i] + b[i + 1] * 256;\n}\nfunction read8(b, i) {\n return b[i];\n}\nfunction signExtend8(x) {\n return (x ^ 0x80) - 0x80;\n}\n\nconst STATUTS_PAC = { 0: 'Arret', 1: 'Attente', 2: 'Chaud', 3: 'Froid', 4: 'Hors Gel', 5: 'Ext Chaud', 6: 'Ext Froid', 7: 'Chaud Froid', 8: 'ECS', 9: 'Piscine' };\nconst STATUTS_FRIGO = { 0: 'Arret', 1: 'Refroidissement', 2: 'Chauffage', 3: 'Degivrage' };\nconst MODELES_PAC = { 0x10: 'AJPAC_III', 0x11: 'BAGUIO_ZURAN_IV', 0x12: 'TIMAX_III', 0x13: 'GEOTWIN_IV', 0x14: 'CAIROX', 0x15: 'PHOENIX', 0x16: 'ARKTEA', 0x17: 'GEOINVERTER', 0x18: 'LAST_MODEL' };\n\nif (size === 163) {\n const statut_frigo_raw = buf[36] >> 4;\n const active_error_fri = buf[12] + (buf[13] & 0x0f) * 256;\n msg.payload = {\n frame_type: 'frigo',\n exterieur_temp: read16s(buf, 24) / 10,\n nb_degivrages: read16u(buf, 26),\n temps_compresseur: read16u(buf, 40) / 10,\n nb_cycles_compresseur: read16u(buf, 42) * 100,\n freq_comp_actuelle: read16u(buf, 52),\n freq_comp_cible: read16u(buf, 54),\n fan_speed_evaporator_1: read16u(buf, 56),\n dc_voltage: read16u(buf, 62),\n statut_frigo: statut_frigo_raw,\n statut_frigo_s: STATUTS_FRIGO[statut_frigo_raw] || 'Inconnu',\n active_error_fri: active_error_fri\n };\n return msg;\n}\n\nif (size === 227) {\n const statut_pac_raw = buf[12] & 0b11111;\n const modele_raw = buf[46];\n const active_error_reg = buf[30] + (buf[31] & 0x0f) * 256;\n const signal_rf = signExtend8(buf[193]);\n msg.payload = {\n frame_type: 'regulation',\n puissance_inst_produite: read16u(buf, 16) / 0.1,\n puissance_inst_consommee: read16u(buf, 18) / 0.1,\n temps_mise_sous_tension: read16u(buf, 20),\n modele_pac: modele_raw,\n modele_pac_s: MODELES_PAC[modele_raw] || 'Inconnu',\n primaire_temp_eau_aller_consigne: read16s(buf, 52) / 10,\n primaire_temp_eau_aller: read16s(buf, 54) / 10,\n primaire_temp_eau_retour: read16s(buf, 56) / 10,\n primaire_debit_eau: read16u(buf, 60) / 10,\n primaire_pression: read8(buf, 62) / 10,\n primaire_circulateur_consigne: read8(buf, 64),\n zone1_temp_interieur: read16s(buf, 68) / 10,\n zone1_consigne: read16s(buf, 70) / 10,\n ecs_temp_eau_milieu: read16s(buf, 108) / 10,\n ecs_temp_eau_bas: read16s(buf, 110) / 10,\n ecs_consigne: read16s(buf, 122) / 10,\n nb_cycles_compresseur_reg: read16u(buf, 148),\n statut_pac: statut_pac_raw,\n statut_pac_s: STATUTS_PAC[statut_pac_raw] || 'Inconnu',\n active_error_reg: active_error_reg,\n signal_rf_sonde_1: signal_rf\n };\n return msg;\n}\n\nreturn null;\n", + "outputs": 1, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 330, + "y": 160, + "wires": [ + [ + "arkteos_filter_aberrant" + ] + ] + }, + { + "id": "arkteos_filter_aberrant", + "type": "function", + "z": "arkteos_flow", + "name": "Filtre valeurs aberrantes", + "func": "const data = msg.payload;\n\nconst RANGES = {\n exterieur_temp: { min: -50, max: 150 },\n nb_degivrages: { min: 0, max: 99999 },\n temps_compresseur: { min: 0, max: 999999 },\n nb_cycles_compresseur: { min: 0, max: 9999999 },\n freq_comp_actuelle: { min: 0, max: 200 },\n freq_comp_cible: { min: 0, max: 200 },\n fan_speed_evaporator_1: { min: 0, max: 3000 },\n dc_voltage: { min: 0, max: 1000 },\n statut_frigo: { min: 0, max: 3 },\n active_error_fri: { min: 0, max: 65535 },\n puissance_inst_produite: { min: 0, max: 50000 },\n puissance_inst_consommee: { min: 0, max: 50000 },\n temps_mise_sous_tension: { min: 0, max: 999999 },\n primaire_temp_eau_aller_consigne: { min: -10, max: 90 },\n primaire_temp_eau_aller: { min: -10, max: 90 },\n primaire_temp_eau_retour: { min: -10, max: 90 },\n primaire_debit_eau: { min: 0, max: 10000 },\n primaire_pression: { min: 0, max: 10 },\n primaire_circulateur_consigne: { min: 0, max: 100 },\n zone1_temp_interieur: { min: -10, max: 50 },\n zone1_consigne: { min: 5, max: 35 },\n ecs_temp_eau_milieu: { min: 0, max: 95 },\n ecs_temp_eau_bas: { min: 0, max: 95 },\n ecs_consigne: { min: 0, max: 80 },\n nb_cycles_compresseur_reg: { min: 0, max: 999999 },\n statut_pac: { min: 0, max: 9 },\n active_error_reg: { min: 0, max: 65535 },\n signal_rf_sonde_1: { min: -128, max: 127 }\n};\n\nconst filtered = { frame_type: data.frame_type };\nconst rejected = {};\n\nfor (const [key, val] of Object.entries(data)) {\n if (key === 'frame_type') continue;\n // Valeurs texte : pas de filtre de plage\n if (typeof val === 'string') {\n filtered[key] = val;\n continue;\n }\n const range = RANGES[key];\n if (!range) {\n filtered[key] = val;\n continue;\n }\n if (val >= range.min && val <= range.max) {\n filtered[key] = val;\n } else {\n rejected[key] = val;\n }\n}\n\nif (Object.keys(rejected).length > 0) {\n node.warn('Valeurs aberrantes filtrées : ' + JSON.stringify(rejected));\n}\n\nmsg.payload = filtered;\nreturn msg;\n", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 540, + "y": 160, + "wires": [ + [ + "arkteos_route" + ] + ] + }, + { + "id": "arkteos_route", + "type": "switch", + "z": "arkteos_flow", + "name": "Route par type", + "property": "payload.frame_type", + "propertyType": "msg", + "rules": [ + { + "t": "eq", + "v": "frigo", + "vt": "str" + }, + { + "t": "eq", + "v": "regulation", + "vt": "str" + } + ], + "checkall": "false", + "repair": false, + "outputs": 2, + "x": 740, + "y": 160, + "wires": [ + [ + "arkteos_throttle_frigo" + ], + [ + "arkteos_throttle_regulation" + ] + ] + }, + { + "id": "arkteos_throttle_frigo", + "type": "function", + "z": "arkteos_flow", + "name": "Throttle frigo", + "func": "const THRESHOLDS = {\n exterieur_temp: 0.5,\n nb_degivrages: 1,\n temps_compresseur: 1,\n nb_cycles_compresseur: 100,\n freq_comp_actuelle: 1,\n freq_comp_cible: 1,\n fan_speed_evaporator_1: 10,\n dc_voltage: 5,\n statut_frigo: 1,\n active_error_fri: 1\n};\nconst MAX_INTERVALS = {\n exterieur_temp: 60000,\n nb_degivrages: 300000,\n temps_compresseur: 300000,\n nb_cycles_compresseur: 300000,\n freq_comp_actuelle: 30000,\n freq_comp_cible: 30000,\n fan_speed_evaporator_1: 30000,\n dc_voltage: 60000,\n statut_frigo: 60000,\n active_error_fri: 60000\n};\n\nconst now = Date.now();\nconst prev = context.get('prev') || {};\nconst lastPublished = context.get('lastPublished') || {};\nconst data = msg.payload;\nconst toPublish = {};\n\nfor (const key of Object.keys(THRESHOLDS)) {\n const val = data[key];\n if (val === undefined) continue;\n const prevVal = prev[key];\n const lastTs = lastPublished[key] || 0;\n const isString = typeof val === 'string';\n const delta = isString ? (val !== prevVal ? Infinity : 0) : (prevVal !== undefined ? Math.abs(val - prevVal) : Infinity);\n const elapsed = now - lastTs;\n if (delta >= THRESHOLDS[key] || elapsed >= MAX_INTERVALS[key]) {\n toPublish[key] = val;\n lastPublished[key] = now;\n prev[key] = val;\n }\n}\n\n// Toujours inclure statut_frigo_s si statut_frigo change\nif (toPublish.statut_frigo !== undefined) {\n toPublish.statut_frigo_s = data.statut_frigo_s;\n}\n\ncontext.set('prev', prev);\ncontext.set('lastPublished', lastPublished);\n\nif (Object.keys(toPublish).length === 0) return null;\n\nmsg.payload = Object.assign({ frame_type: 'frigo' }, toPublish);\nreturn msg;\n", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1010, + "y": 60, + "wires": [ + [ + "arkteos_debug_frigo_parsed", + "arkteos_publish_frigo" + ] + ] + }, + { + "id": "arkteos_throttle_regulation", + "type": "function", + "z": "arkteos_flow", + "name": "Throttle regulation", + "func": "const THRESHOLDS = {\n puissance_inst_produite: 10,\n puissance_inst_consommee: 10,\n temps_mise_sous_tension: 1,\n primaire_temp_eau_aller_consigne: 0.5,\n primaire_temp_eau_aller: 0.5,\n primaire_temp_eau_retour: 0.5,\n primaire_debit_eau: 1,\n primaire_pression: 0.1,\n primaire_circulateur_consigne: 1,\n zone1_temp_interieur: 0.5,\n zone1_consigne: 0.5,\n ecs_temp_eau_milieu: 0.5,\n ecs_temp_eau_bas: 0.5,\n ecs_consigne: 0.5,\n nb_cycles_compresseur_reg: 1,\n statut_pac: 1,\n active_error_reg: 1,\n modele_pac: 1,\n signal_rf_sonde_1: 1\n};\nconst MAX_INTERVALS = {\n puissance_inst_produite: 30000,\n puissance_inst_consommee: 30000,\n temps_mise_sous_tension: 300000,\n primaire_temp_eau_aller_consigne: 60000,\n primaire_temp_eau_aller: 60000,\n primaire_temp_eau_retour: 60000,\n primaire_debit_eau: 60000,\n primaire_pression: 60000,\n primaire_circulateur_consigne: 60000,\n zone1_temp_interieur: 60000,\n zone1_consigne: 60000,\n ecs_temp_eau_milieu: 60000,\n ecs_temp_eau_bas: 60000,\n ecs_consigne: 60000,\n nb_cycles_compresseur_reg: 300000,\n statut_pac: 60000,\n active_error_reg: 60000,\n modele_pac: 3600000,\n signal_rf_sonde_1: 300000\n};\n\nconst now = Date.now();\nconst prev = context.get('prev') || {};\nconst lastPublished = context.get('lastPublished') || {};\nconst data = msg.payload;\nconst toPublish = {};\n\nfor (const key of Object.keys(THRESHOLDS)) {\n const val = data[key];\n if (val === undefined) continue;\n const prevVal = prev[key];\n const lastTs = lastPublished[key] || 0;\n const delta = prevVal !== undefined ? Math.abs(val - prevVal) : Infinity;\n const elapsed = now - lastTs;\n if (delta >= THRESHOLDS[key] || elapsed >= MAX_INTERVALS[key]) {\n toPublish[key] = val;\n lastPublished[key] = now;\n prev[key] = val;\n }\n}\n\n// Toujours inclure statut_pac_s et modele_pac_s si les valeurs numériques changent\nif (toPublish.statut_pac !== undefined) {\n toPublish.statut_pac_s = data.statut_pac_s;\n}\nif (toPublish.modele_pac !== undefined) {\n toPublish.modele_pac_s = data.modele_pac_s;\n}\n\ncontext.set('prev', prev);\ncontext.set('lastPublished', lastPublished);\n\nif (Object.keys(toPublish).length === 0) return null;\n\nmsg.payload = Object.assign({ frame_type: 'regulation' }, toPublish);\nreturn msg;\n", + "outputs": 1, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 990, + "y": 200, + "wires": [ + [ + "arkteos_debug_regulation_parsed", + "arkteos_publish_regulation" + ] + ] + }, + { + "id": "arkteos_debug_frigo_parsed", + "type": "debug", + "z": "arkteos_flow", + "name": "Frigo parsé", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "payload", + "targetType": "msg", + "statusVal": "", + "statusType": "auto", + "x": 1230, + "y": 40, + "wires": [] + }, + { + "id": "arkteos_debug_regulation_parsed", + "type": "debug", + "z": "arkteos_flow", + "name": "Regulation parsée", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "payload", + "targetType": "msg", + "statusVal": "", + "statusType": "auto", + "x": 1250, + "y": 160, + "wires": [] + }, + { + "id": "arkteos_publish_frigo", + "type": "function", + "z": "arkteos_flow", + "name": "Publish frigo", + "func": "const data = msg.payload;\nconst topics = {\n exterieur_temp: 'arkteos/frigo/exterieur_temp',\n nb_degivrages: 'arkteos/frigo/nb_degivrages',\n temps_compresseur: 'arkteos/frigo/temps_compresseur',\n nb_cycles_compresseur: 'arkteos/frigo/nb_cycles_compresseur',\n freq_comp_actuelle: 'arkteos/frigo/freq_comp_actuelle',\n freq_comp_cible: 'arkteos/frigo/freq_comp_cible',\n fan_speed_evaporator_1: 'arkteos/frigo/fan_speed_evaporator_1',\n dc_voltage: 'arkteos/frigo/dc_voltage',\n statut_frigo: 'arkteos/frigo/statut_frigo',\n statut_frigo_s: 'arkteos/frigo/statut_frigo_s',\n active_error_fri: 'arkteos/frigo/active_error_fri'\n};\n\nconst msgs = [];\nfor (const [key, topic] of Object.entries(topics)) {\n if (data[key] !== undefined) {\n msgs.push({ topic, payload: String(data[key]), qos: 0, retain: true });\n }\n}\n\nif (msgs.length === 0) return null;\nreturn [msgs];\n", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1230, + "y": 80, + "wires": [ + [ + "arkteos_debug_frigo_published", + "arkteos_mqtt_out" + ] + ] + }, + { + "id": "arkteos_publish_regulation", + "type": "function", + "z": "arkteos_flow", + "name": "Publish regulation", + "func": "const data = msg.payload;\nconst topics = {\n puissance_inst_produite: 'arkteos/regulation/puissance_inst_produite',\n puissance_inst_consommee: 'arkteos/regulation/puissance_inst_consommee',\n temps_mise_sous_tension: 'arkteos/regulation/temps_mise_sous_tension',\n modele_pac: 'arkteos/regulation/modele_pac',\n modele_pac_s: 'arkteos/regulation/modele_pac_s',\n primaire_temp_eau_aller_consigne: 'arkteos/regulation/primaire_temp_eau_aller_consigne',\n primaire_temp_eau_aller: 'arkteos/regulation/primaire_temp_eau_aller',\n primaire_temp_eau_retour: 'arkteos/regulation/primaire_temp_eau_retour',\n primaire_debit_eau: 'arkteos/regulation/primaire_debit_eau',\n primaire_pression: 'arkteos/regulation/primaire_pression',\n primaire_circulateur_consigne: 'arkteos/regulation/primaire_circulateur_consigne',\n zone1_temp_interieur: 'arkteos/regulation/zone1_temp_interieur',\n zone1_consigne: 'arkteos/regulation/zone1_consigne',\n ecs_temp_eau_milieu: 'arkteos/regulation/ecs_temp_eau_milieu',\n ecs_temp_eau_bas: 'arkteos/regulation/ecs_temp_eau_bas',\n ecs_consigne: 'arkteos/regulation/ecs_consigne',\n nb_cycles_compresseur_reg: 'arkteos/regulation/nb_cycles_compresseur',\n statut_pac: 'arkteos/regulation/statut_pac',\n statut_pac_s: 'arkteos/regulation/statut_pac_s',\n active_error_reg: 'arkteos/regulation/active_error_reg',\n signal_rf_sonde_1: 'arkteos/regulation/signal_rf_sonde_1'\n};\n\nconst msgs = [];\nfor (const [key, topic] of Object.entries(topics)) {\n if (data[key] !== undefined) {\n msgs.push({ topic, payload: String(data[key]), qos: 0, retain: true });\n }\n}\n\nif (msgs.length === 0) return null;\nreturn [msgs];\n", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1250, + "y": 260, + "wires": [ + [ + "arkteos_debug_regulation_published", + "arkteos_mqtt_out" + ] + ] + }, + { + "id": "arkteos_debug_frigo_published", + "type": "debug", + "z": "arkteos_flow", + "name": "Frigo MQTT publié", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "payload", + "targetType": "msg", + "statusVal": "", + "statusType": "auto", + "x": 1490, + "y": 40, + "wires": [] + }, + { + "id": "arkteos_debug_regulation_published", + "type": "debug", + "z": "arkteos_flow", + "name": "Regulation MQTT publiée", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "payload", + "targetType": "msg", + "statusVal": "", + "statusType": "auto", + "x": 1510, + "y": 260, + "wires": [] + }, + { + "id": "arkteos_mqtt_out", + "type": "mqtt out", + "z": "arkteos_flow", + "name": "MQTT publish", + "topic": "", + "qos": "0", + "retain": "true", + "respTopic": "", + "contentType": "", + "userProps": "", + "correl": "", + "expiry": "", + "broker": "1a70e168f9e2ea6b", + "x": 1640, + "y": 140, + "wires": [] + }, + { + "id": "arkteos_discovery_inject", + "type": "inject", + "z": "arkteos_flow", + "name": "Send discovery (1x au démarrage)", + "props": [ + { + "p": "payload" + } + ], + "repeat": "", + "crontab": "", + "once": true, + "onceDelay": 3, + "topic": "", + "payload": "", + "payloadType": "date", + "x": 180, + "y": 360, + "wires": [ + [ + "arkteos_discovery" + ] + ] + }, + { + "id": "arkteos_discovery", + "type": "function", + "z": "arkteos_flow", + "name": "MQTT Discovery", + "func": "const device = {\n identifiers: ['arkteos_zuran4'],\n name: 'PAC Arkteos Zuran 4',\n model: 'Zuran 4',\n manufacturer: 'Arkteos'\n};\n\nconst sensors = [\n // Statut connexion\n { _type: 'binary_sensor', unique_id: 'arkteos_connected', name: 'PAC connectée', state_topic: 'arkteos/status/connected', device_class: 'connectivity', payload_on: 'ON', payload_off: 'OFF' },\n // Frigo\n { unique_id: 'arkteos_exterieur_temp', name: 'Température extérieure', state_topic: 'arkteos/frigo/exterieur_temp', unit_of_measurement: '°C', device_class: 'temperature', state_class: 'measurement' },\n { unique_id: 'arkteos_nb_degivrages', name: 'Nombre de dégivrages', state_topic: 'arkteos/frigo/nb_degivrages', state_class: 'total_increasing' },\n { unique_id: 'arkteos_temps_compresseur', name: 'Temps fonctionnement compresseur', state_topic: 'arkteos/frigo/temps_compresseur', unit_of_measurement: 'h', device_class: 'duration', state_class: 'total_increasing' },\n { unique_id: 'arkteos_nb_cycles_compresseur', name: 'Nombre de cycles compresseur', state_topic: 'arkteos/frigo/nb_cycles_compresseur', state_class: 'total_increasing' },\n { unique_id: 'arkteos_freq_comp_actuelle', name: 'Fréquence compresseur actuelle', state_topic: 'arkteos/frigo/freq_comp_actuelle', unit_of_measurement: 'Hz', state_class: 'measurement' },\n { unique_id: 'arkteos_freq_comp_cible', name: 'Fréquence compresseur cible', state_topic: 'arkteos/frigo/freq_comp_cible', unit_of_measurement: 'Hz', state_class: 'measurement' },\n { unique_id: 'arkteos_fan_speed_evaporator_1', name: 'Vitesse ventilateur groupe frigo', state_topic: 'arkteos/frigo/fan_speed_evaporator_1', unit_of_measurement: 'rpm', state_class: 'measurement' },\n { unique_id: 'arkteos_dc_voltage', name: 'Voltage DC compresseur', state_topic: 'arkteos/frigo/dc_voltage', unit_of_measurement: 'V', device_class: 'voltage', state_class: 'measurement' },\n { unique_id: 'arkteos_statut_frigo', name: 'Statut frigo', state_topic: 'arkteos/frigo/statut_frigo', state_class: 'measurement' },\n { unique_id: 'arkteos_statut_frigo_s', name: 'Statut frigo (texte)', state_topic: 'arkteos/frigo/statut_frigo_s' },\n { unique_id: 'arkteos_active_error_fri', name: 'Erreur active frigo', state_topic: 'arkteos/frigo/active_error_fri', state_class: 'measurement' },\n // Regulation\n { unique_id: 'arkteos_puissance_inst_produite', name: 'Puissance instantanée produite', state_topic: 'arkteos/regulation/puissance_inst_produite', unit_of_measurement: 'W', device_class: 'power', state_class: 'measurement' },\n { unique_id: 'arkteos_puissance_inst_consommee', name: 'Puissance instantanée consommée', state_topic: 'arkteos/regulation/puissance_inst_consommee', unit_of_measurement: 'W', device_class: 'power', state_class: 'measurement' },\n { unique_id: 'arkteos_temps_mise_sous_tension', name: 'Temps mise sous tension', state_topic: 'arkteos/regulation/temps_mise_sous_tension', unit_of_measurement: 'h', device_class: 'duration', state_class: 'total_increasing' },\n { unique_id: 'arkteos_modele_pac_s', name: 'Modèle PAC', state_topic: 'arkteos/regulation/modele_pac_s' },\n { unique_id: 'arkteos_primaire_temp_eau_aller_consigne', name: 'Consigne départ eau primaire', state_topic: 'arkteos/regulation/primaire_temp_eau_aller_consigne', unit_of_measurement: '°C', device_class: 'temperature', state_class: 'measurement' },\n { unique_id: 'arkteos_primaire_temp_eau_aller', name: 'Température eau primaire aller', state_topic: 'arkteos/regulation/primaire_temp_eau_aller', unit_of_measurement: '°C', device_class: 'temperature', state_class: 'measurement' },\n { unique_id: 'arkteos_primaire_temp_eau_retour', name: 'Température eau primaire retour', state_topic: 'arkteos/regulation/primaire_temp_eau_retour', unit_of_measurement: '°C', device_class: 'temperature', state_class: 'measurement' },\n { unique_id: 'arkteos_primaire_debit_eau', name: 'Débit eau primaire', state_topic: 'arkteos/regulation/primaire_debit_eau', unit_of_measurement: 'L/h', state_class: 'measurement' },\n { unique_id: 'arkteos_primaire_pression', name: 'Pression eau primaire', state_topic: 'arkteos/regulation/primaire_pression', unit_of_measurement: 'bar', device_class: 'pressure', state_class: 'measurement' },\n { unique_id: 'arkteos_primaire_circulateur_consigne', name: 'Circulateur primaire', state_topic: 'arkteos/regulation/primaire_circulateur_consigne', unit_of_measurement: '%', state_class: 'measurement' },\n { unique_id: 'arkteos_zone1_temp_interieur', name: 'Température intérieure zone 1', state_topic: 'arkteos/regulation/zone1_temp_interieur', unit_of_measurement: '°C', device_class: 'temperature', state_class: 'measurement' },\n { unique_id: 'arkteos_zone1_consigne', name: 'Consigne température zone 1', state_topic: 'arkteos/regulation/zone1_consigne', unit_of_measurement: '°C', device_class: 'temperature', state_class: 'measurement' },\n { unique_id: 'arkteos_ecs_temp_eau_milieu', name: 'Température ballon ECS milieu', state_topic: 'arkteos/regulation/ecs_temp_eau_milieu', unit_of_measurement: '°C', device_class: 'temperature', state_class: 'measurement' },\n { unique_id: 'arkteos_ecs_temp_eau_bas', name: 'Température ballon ECS bas', state_topic: 'arkteos/regulation/ecs_temp_eau_bas', unit_of_measurement: '°C', device_class: 'temperature', state_class: 'measurement' },\n { unique_id: 'arkteos_ecs_consigne', name: 'Consigne ECS', state_topic: 'arkteos/regulation/ecs_consigne', unit_of_measurement: '°C', device_class: 'temperature', state_class: 'measurement' },\n { unique_id: 'arkteos_nb_cycles_compresseur_reg', name: 'Cycles compresseur (régulation)', state_topic: 'arkteos/regulation/nb_cycles_compresseur', state_class: 'total_increasing' },\n { unique_id: 'arkteos_statut_pac', name: 'Statut PAC', state_topic: 'arkteos/regulation/statut_pac', state_class: 'measurement' },\n { unique_id: 'arkteos_statut_pac_s', name: 'Statut PAC (texte)', state_topic: 'arkteos/regulation/statut_pac_s' },\n { unique_id: 'arkteos_active_error_reg', name: 'Erreur active régulation', state_topic: 'arkteos/regulation/active_error_reg', state_class: 'measurement' },\n { unique_id: 'arkteos_signal_rf_sonde_1', name: 'Signal RF sonde zone 1', state_topic: 'arkteos/regulation/signal_rf_sonde_1', unit_of_measurement: 'dBm', state_class: 'measurement' }\n];\n\nconst msgs = [];\nfor (const s of sensors) {\n const type = s._type || 'sensor';\n const config = Object.assign({}, s, { device });\n delete config._type;\n msgs.push({\n topic: `homeassistant/${type}/${s.unique_id}/config`,\n payload: JSON.stringify(config),\n qos: 0,\n retain: true\n });\n}\n\nreturn [msgs];\n", + "outputs": 1, + "timeout": "", + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 490, + "y": 360, + "wires": [ + [ + "arkteos_debug_discovery", + "55376bccc43a6ea6" + ] + ] + }, + { + "id": "arkteos_debug_discovery", + "type": "debug", + "z": "arkteos_flow", + "name": "Discovery MQTT", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "payload", + "targetType": "msg", + "statusVal": "", + "statusType": "auto", + "x": 730, + "y": 400, + "wires": [] + }, + { + "id": "6dceb3fdda857578", + "type": "link out", + "z": "arkteos_flow", + "name": "link out 4", + "mode": "link", + "links": [ + "a457b5bc82039c7a" + ], + "x": 845, + "y": 40, + "wires": [] + }, + { + "id": "a457b5bc82039c7a", + "type": "link in", + "z": "arkteos_flow", + "name": "link in 3", + "links": [ + "6dceb3fdda857578", + "55376bccc43a6ea6" + ], + "x": 1455, + "y": 140, + "wires": [ + [ + "arkteos_mqtt_out" + ] + ] + }, + { + "id": "55376bccc43a6ea6", + "type": "link out", + "z": "arkteos_flow", + "name": "link out 5", + "mode": "link", + "links": [ + "a457b5bc82039c7a" + ], + "x": 655, + "y": 320, + "wires": [] + }, + { + "id": "1a70e168f9e2ea6b", + "type": "mqtt-broker", + "name": "Mosquitto local Arkteos", + "broker": "core-mosquitto", + "port": "1883", + "clientid": "nodered_arkteos", + "autoConnect": true, + "usetls": false, + "compatmode": false, + "protocolVersion": 4, + "keepalive": "60", + "cleansession": true, + "autoUnsubscribe": true, + "birthTopic": "", + "birthQos": "0", + "birthPayload": "", + "birthMsg": {}, + "closeTopic": "", + "closeQos": "0", + "closePayload": "", + "closeMsg": {}, + "willTopic": "", + "willQos": "0", + "willPayload": "", + "willMsg": {}, + "userProps": "", + "sessionExpiry": "" + } +] diff --git a/references/proxy_protocol_notes.md b/references/proxy_protocol_notes.md new file mode 100644 index 0000000..c7dbb4d --- /dev/null +++ b/references/proxy_protocol_notes.md @@ -0,0 +1,157 @@ +# Arkteos Proxy — Home Assistant Addon + +Proxy TCP pour PAC Arkteos REG3 (Zuran 4, Baguio 4, etc.). + +Permet les connexions simultanées de Node-RED et de l'application mobile Arkteos, en relayant le flux binaire de la PAC vers tous les clients connectés. + +## Installation + +1. Dans Home Assistant : **Paramètres → Modules complémentaires → trois points → Dépôts** +2. Ajoute l'URL de ce repo : `https://github.com/raph666/arkteos-proxy-addon` +3. Installe l'addon **Arkteos Proxy** +4. Configure l'IP de ta PAC dans les options +5. Configure le port exposé dans la section **Network** (défaut : 9641) et clique **Save** +6. Démarre l'addon + +## Configuration + +| Option | Description | Défaut | +|---|---|---| +| `pac_host` | Adresse IP de la PAC | `192.168.X.X` | +| `pac_port` | Port TCP de la PAC | `9641` | +| `proxy_port` | Port d'écoute interne du proxy | `9641` | + +## Intégration Node-RED + +Un flow Node-RED prêt à l'emploi est disponible dans ce repo : `nodered/arkteos_reg3_nodered.json`. + +### Import + +1. Dans Node-RED : menu hamburger → **Import** → colle le contenu du fichier JSON +2. Double-clique sur le nœud **PAC via proxy** et remplace `` par l'IP de ton Home Assistant +3. Double-clique sur le nœud **MQTT publish** → icône crayon → onglet **Security** → renseigne le login et mot de passe Mosquitto +4. Clique **Deploy** + +### Fonctionnement + +Le flow se connecte en streaming permanent au proxy. Chaque trame reçue est parsée, filtrée puis publiée sur MQTT. + +**Filtrage des valeurs aberrantes** + +Les valeurs hors plage physiquement plausible sont ignorées et loguées dans la console Node-RED : + +| Valeur | Plage valide | +|---|---| +| Température extérieure | -50°C à 150°C | +| Températures circuit primaire | -10°C à 90°C | +| Consigne départ eau primaire | -10°C à 90°C | +| Températures ECS | 0°C à 95°C | +| Consigne ECS | 0°C à 80°C | +| Température intérieure zone 1 | -10°C à 50°C | +| Consigne zone 1 | 5°C à 35°C | +| Puissances | 0 à 50 000 W | +| Pression eau primaire | 0 à 10 bar | +| Fréquence compresseur actuelle | 0 à 200 Hz | +| Fréquence compresseur cible | 0 à 200 Hz | +| Vitesse ventilateur | 0 à 3 000 rpm | +| Voltage DC | 0 à 1 000 V | +| Débit eau primaire | 0 à 10 000 L/h | +| Circulateur primaire | 0% à 100% | +| Statut frigo | 0 à 3 | +| Statut PAC | 0 à 9 | +| Signal RF sonde | -128 à 127 dBm | +| Nombre de dégivrages | 0 à 99 999 | +| Temps fonctionnement compresseur | 0 à 999 999 h | +| Nb cycles compresseur | 0 à 9 999 999 | +| Temps mise sous tension | 0 à 999 999 h | + +**Throttling** + +Une valeur n'est publiée que si elle a changé au-delà d'un seuil défini, avec un rafraîchissement forcé à intervalle maximum : + +| Valeur | Seuil | Intervalle max | +|---|---|---| +| Température extérieure | 0.5°C | 60s | +| Températures circuit primaire | 0.5°C | 60s | +| Consigne départ eau primaire | 0.5°C | 60s | +| Températures ECS | 0.5°C | 60s | +| Consigne ECS | 0.5°C | 60s | +| Température intérieure zone 1 | 0.5°C | 60s | +| Consigne zone 1 | 0.5°C | 60s | +| Puissance produite | 10 W | 30s | +| Puissance consommée | 10 W | 30s | +| Pression eau primaire | 0.1 bar | 60s | +| Fréquence compresseur actuelle | 1 Hz | 30s | +| Fréquence compresseur cible | 1 Hz | 30s | +| Vitesse ventilateur | 10 rpm | 30s | +| Voltage DC | 5 V | 60s | +| Débit eau primaire | 1 L/h | 60s | +| Circulateur primaire | 1% | 60s | +| Modèle PAC | changement | 1h | +| Statut frigo | changement | 60s | +| Statut PAC | changement | 60s | +| Erreurs | changement | 60s | +| Nombre de dégivrages | 1 | 5 min | +| Nb cycles compresseur | 100 | 5 min | +| Temps fonctionnement compresseur | 1 h | 5 min | +| Temps mise sous tension | 1 h | 5 min | +| Signal RF sonde | 1 dBm | 5 min | + +**Statut de connexion** + +Un `binary_sensor` HA indique si la PAC est connectée. Il passe à `OFF` si aucune trame n'est reçue depuis plus de 30 secondes. + +### Entités créées dans HA + +Le flow publie automatiquement via MQTT Discovery dans un appareil **PAC Arkteos Zuran 4** : + +**Statut** +- PAC connectée (binary_sensor) + +**Groupe frigorifique** +- Température extérieure (°C) +- Nombre de dégivrages +- Temps fonctionnement compresseur (h) +- Nombre de cycles compresseur +- Fréquence compresseur actuelle (Hz) +- Fréquence compresseur cible (Hz) +- Vitesse ventilateur groupe frigo (rpm) +- Voltage DC compresseur (V) +- Statut frigo (0-3) +- Statut frigo texte (Arret / Refroidissement / Chauffage / Degivrage) +- Erreur active frigo + +**Régulation** +- Puissance instantanée produite (W) +- Puissance instantanée consommée (W) +- Temps mise sous tension (h) +- Modèle PAC (texte) +- Consigne départ eau primaire (°C) +- Température eau primaire aller (°C) +- Température eau primaire retour (°C) +- Débit eau primaire (L/h) +- Pression eau primaire (bar) +- Circulateur primaire (%) +- Température intérieure zone 1 (°C) +- Consigne température zone 1 (°C) +- Température ballon ECS milieu (°C) +- Température ballon ECS bas (°C) +- Consigne ECS (°C) +- Cycles compresseur (régulation) +- Statut PAC (0-9) +- Statut PAC texte (Arret / Attente / Chaud / Froid / Hors Gel / ECS / Piscine / etc.) +- Erreur active régulation +- Signal RF sonde zone 1 (dBm) + +## Protocole + +La PAC Arkteos REG3 expose un flux binaire TCP (serial-over-IP) sur le port 9641 (IHM). +Trois types de trames sont émises en continu à environ 1 trame/seconde : + +| Taille | Type | Contenu | +|---|---|---| +| 163 bytes | Frigo | Données groupe frigorifique Mitsubishi | +| 227 bytes | Régulation | Données régulation, températures, ECS | +| 95 bytes | Réseau | Métadonnées de connexion (IP PAC, etc.) | + +Toutes les trames commencent par le magic bytes `0x55 0x00`. \ No newline at end of file