468 lines
106 KiB
JSON
468 lines
106 KiB
JSON
{
|
|
"updatedAt": "2026-07-09T21:45:53.855Z",
|
|
"createdAt": "2026-03-17T16:32:38.178Z",
|
|
"id": "2NhrfxuGm4pTUNs9",
|
|
"name": "SDB Climate & Surplus Manager V2.2",
|
|
"description": null,
|
|
"active": true,
|
|
"isArchived": false,
|
|
"nodes": [
|
|
{
|
|
"parameters": {
|
|
"rule": {
|
|
"interval": [
|
|
{
|
|
"field": "minutes",
|
|
"minutesInterval": 1
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"id": "1e5571d8-a01b-4249-8f06-0c694605975e",
|
|
"name": "Schedule Trigger",
|
|
"type": "n8n-nodes-base.scheduleTrigger",
|
|
"typeVersion": 1.2,
|
|
"position": [
|
|
0,
|
|
0
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=http://10.10.10.17:8123/api/template\n",
|
|
"authentication": "genericCredentialType",
|
|
"genericAuthType": "httpBearerAuth",
|
|
"sendHeaders": true,
|
|
"headerParameters": {
|
|
"parameters": [
|
|
{
|
|
"name": "Content-Type",
|
|
"value": "application/json"
|
|
},
|
|
{
|
|
"name": "Accept",
|
|
"value": "text/plain"
|
|
}
|
|
]
|
|
},
|
|
"sendBody": true,
|
|
"bodyParameters": {
|
|
"parameters": [
|
|
{
|
|
"name": "template",
|
|
"value": "{{ {\n \"soc_percent\": states(\"sensor.victron_battery_soc\"),\n \"battery_current_a\": states(\"sensor.victron_battery_current\"),\n \"pv_power_w\": states(\"sensor.victron_pv_power_system\"),\n \"pv_power_10m_avg_w\": states(\"sensor.victron_pv_power_10m_avg\"),\n \"house_ac_power_w\": states(\"sensor.victron_consumption_power_l1\"),\n \"grid_power_w\": states(\"sensor.victron_grid_power_l1\"),\n \"grid_state\": states(\"switch.edf_general_ok_switch\"),\n \"victron_acin1_available\": states(\"sensor.victron_ac_in1_available\"),\n\n \"sdb_master_power\": states(\"switch.sdb_prise_socket_1\"),\n \"sdb_master_energy_total\": states(\"sensor.sdb_prise_energie_totale\"),\n\n \"deshumide_switch\": states(\"switch.deshumide_sdb_socket_1\"),\n \"deshumide_energy_total\": states(\"sensor.deshumide_sdb_energie_totale\"),\n\n \"radiator_switch\": states(\"switch.sdb_radiateur_socket_1\"),\n \"radiator_energy_total\": states(\"sensor.sdb_radiateur_energie_totale\"),\n\n \"sdb_temperature_c\": states(\"sensor.sdb_temp_hygro_temperature\"),\n \"sdb_humidity_pct\": states(\"sensor.sdb_temp_hygro_humidite\"),\n \"sdb_humidity_age_min\": ((as_timestamp(now()) - as_timestamp(states.sensor.sdb_temp_hygro_humidite.last_reported or states.sensor.sdb_temp_hygro_humidite.last_changed)) / 60) | round(0) if states.sensor.sdb_temp_hygro_humidite is not none else -1,\n \"balance_required_soc_prudent\": states(\"input_number.evcs_balance_required_soc_prudent\"),\n \"balance_status\": states(\"input_text.evcs_balance_status\"),\n \"mppt_operation_modes\": states.sensor\n | selectattr('entity_id','search','mppt')\n | selectattr('entity_id','search','operation_mode')\n | map(attribute='state') | join(','),\n \"evcs_vehicle_connected\": states(\"binary_sensor.victron_evcs_vehicle_connected\"),\n \"evcs_ac_power_w\": states(\"sensor.victron_evcs_ac_power\"),\n \"evcs_surplus_state\": states(\"input_text.victron_evcs_surplus_state\"),\n\n \"auto_enabled\": states(\"input_boolean.sdb_climate_surplus_auto_enabled\"),\n \"notifications_enabled\": states(\"input_boolean.sdb_climate_surplus_notifications\"),\n \"manual_adopt_enabled\": states(\"input_boolean.sdb_master_manual_adopt_enabled\"),\n\n \"master_power_latch\": states(\"input_boolean.sdb_master_power_latch\"),\n \"deshumide_latch\": states(\"input_boolean.sdb_deshumide_latch\"),\n \"radiator_latch\": states(\"input_boolean.sdb_radiator_latch\"),\n\n \"temp_on_min_c\": states(\"input_number.sdb_temp_on_min_c\"),\n \"temp_off_max_c\": states(\"input_number.sdb_temp_off_max_c\"),\n \"humidity_on_pct\": states(\"input_number.sdb_humidity_on_pct\"),\n \"humidity_off_pct\": states(\"input_number.sdb_humidity_off_pct\"),\n\n \"deshumide_min_on_minutes\": states(\"input_number.sdb_deshumide_min_on_minutes\"),\n \"deshumide_min_off_minutes\": states(\"input_number.sdb_deshumide_min_off_minutes\"),\n\n \"radiator_auto_enabled\": states(\"input_boolean.sdb_radiator_surplus_auto_enabled\"),\n \"radiator_soc_on\": states(\"input_number.sdb_radiator_soc_on\"),\n \"radiator_soc_off\": states(\"input_number.sdb_radiator_soc_off\"),\n \"radiator_pv_on_w\": states(\"input_number.sdb_radiator_pv_on_w\"),\n \"radiator_pv_off_w\": states(\"input_number.sdb_radiator_pv_off_w\"),\n \"radiator_battery_charge_ceiling_a\": states(\"input_number.sdb_radiator_battery_charge_ceiling_a\"),\n \"radiator_house_ac_max_w\": states(\"input_number.sdb_radiator_house_ac_max_w\"),\n \"radiator_min_on_minutes\": states(\"input_number.sdb_radiator_min_on_minutes\"),\n \"radiator_min_off_minutes\": states(\"input_number.sdb_radiator_min_off_minutes\"),\n\n \"day_start_time\": states(\"input_datetime.victron_flexible_day_start_time\"),\n \"day_stop_time\": states(\"input_datetime.victron_flexible_day_stop_time\"),\n\n \"master_last_on_at\": states(\"input_datetime.sdb_master_last_on_at\"),\n \"master_last_off_at\": states(\"input_datetime.sdb_master_last_off_at\"),\n \"deshumide_last_on_at\": states(\"input_datetime.sdb_deshumide_last_on_at\"),\n \"deshumide_last_off_at\": states(\"input_datetime.sdb_deshumide_last_off_at\"),\n \"radiator_last_on_at\": states(\"input_datetime.sdb_radiator_last_on_at\"),\n \"radiator_last_off_at\": states(\"input_datetime.sdb_radiator_last_off_at\"),\n\n \"evcs_priority_active\": states(\"input_boolean.victron_evcs_priority_active\"),\n \"energy_priority_mode\": states(\"input_select.victron_energy_priority_mode\"),\n \"energy_priority_reason\": states(\"input_text.victron_energy_priority_reason\"),\n \"evcs_priority_until\": states(\"input_datetime.victron_evcs_priority_until\"),\n\n \"last_signature\": states(\"input_text.sdb_climate_surplus_last_signature\")\n} | tojson }}\n"
|
|
}
|
|
]
|
|
},
|
|
"options": {}
|
|
},
|
|
"id": "74f4b11c-bdb6-41db-9223-f1031cb8a12b",
|
|
"name": "HA Snapshot SDB Climate & Surplus",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
224,
|
|
0
|
|
],
|
|
"credentials": {
|
|
"httpBearerAuth": {
|
|
"id": "uv3sknYYigxBkaVN",
|
|
"name": "Bearer Auth account 2"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "={{ 'http://10.10.10.17:8123/api/services/' + $json.domain + '/' + $json.service }}\n",
|
|
"authentication": "genericCredentialType",
|
|
"genericAuthType": "httpBearerAuth",
|
|
"sendHeaders": true,
|
|
"headerParameters": {
|
|
"parameters": [
|
|
{
|
|
"name": "Content-Type",
|
|
"value": "application/json"
|
|
},
|
|
{
|
|
"name": "accept",
|
|
"value": "application/json"
|
|
}
|
|
]
|
|
},
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ $json.data }}",
|
|
"options": {}
|
|
},
|
|
"id": "761fbc47-54ca-4961-863e-634b52e764d3",
|
|
"name": "Execute HA Service SDB Climate & Surplus",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
896,
|
|
0
|
|
],
|
|
"alwaysOutputData": true,
|
|
"credentials": {
|
|
"httpBearerAuth": {
|
|
"id": "uv3sknYYigxBkaVN",
|
|
"name": "Bearer Auth account 2"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "function num(v, fallback = NaN) {\n const n = Number.parseFloat(v);\n return Number.isFinite(n) ? n : fallback;\n}\n\nfunction low(v) {\n return (v ?? '').toString().trim().toLowerCase();\n}\n\nfunction parseMaybeJson(v) {\n if (typeof v !== 'string') return v;\n try {\n return JSON.parse(v);\n } catch {\n return v;\n }\n}\n\nfunction extractSnapshot(input) {\n if (typeof input === 'string') return parseMaybeJson(input);\n if (input?.data && typeof input.data === 'string') return parseMaybeJson(input.data);\n if (input?.body && typeof input.body === 'string') return parseMaybeJson(input.body);\n if (input?.data && typeof input.data === 'object') return input.data;\n if (input?.body && typeof input.body === 'object') return input.body;\n return input;\n}\n\nfunction round1(v) {\n return Math.round(v * 10) / 10;\n}\n\nfunction round2(v) {\n return Math.round(v * 100) / 100;\n}\n\nfunction clamp(v, min, max) {\n return Math.min(Math.max(v, min), max);\n}\n\nfunction parseTimeToMinutes(v, fallback = null) {\n const s = (v ?? '').toString().trim();\n const m = s.match(/^(\\d{1,2}):(\\d{2})(?::(\\d{2}))?$/);\n if (!m) return fallback;\n const hh = Number(m[1]);\n const mm = Number(m[2]);\n if (!Number.isFinite(hh) || !Number.isFinite(mm)) return fallback;\n return hh * 60 + mm;\n}\n\nfunction to2(n) {\n return String(n).padStart(2, '0');\n}\n\nfunction getParisNowParts() {\n const fmt = new Intl.DateTimeFormat('fr-FR', {\n timeZone: 'Europe/Paris',\n year: 'numeric',\n month: '2-digit',\n day: '2-digit',\n hour: '2-digit',\n minute: '2-digit',\n second: '2-digit',\n hour12: false,\n });\n\n const parts = Object.fromEntries(\n fmt.formatToParts(new Date())\n .filter(p => p.type !== 'literal')\n .map(p => [p.type, p.value])\n );\n\n const hour = Number(parts.hour) === 24 ? 0 : Number(parts.hour);\n const date = `${parts.year}-${parts.month}-${parts.day}`;\n const stamp = `${date} ${to2(hour)}:${parts.minute}:${parts.second}`;\n\n return {\n year: Number(parts.year),\n month: Number(parts.month),\n day: Number(parts.day),\n hour,\n minute: Number(parts.minute),\n second: Number(parts.second),\n date,\n stamp,\n minNow: hour * 60 + Number(parts.minute),\n };\n}\n\nfunction getTzOffsetMs(utcTs, timeZone = 'Europe/Paris') {\n const dtf = new Intl.DateTimeFormat('en-US', {\n timeZone,\n year: 'numeric',\n month: '2-digit',\n day: '2-digit',\n hour: '2-digit',\n minute: '2-digit',\n second: '2-digit',\n hour12: false,\n });\n\n const parts = Object.fromEntries(\n dtf.formatToParts(new Date(utcTs))\n .filter(p => p.type !== 'literal')\n .map(p => [p.type, p.value])\n );\n\n const asIfUtc = Date.UTC(\n Number(parts.year),\n Number(parts.month) - 1,\n Number(parts.day),\n Number(parts.hour) === 24 ? 0 : Number(parts.hour),\n Number(parts.minute),\n Number(parts.second)\n );\n\n const d = new Date(utcTs);\n const actualUtc = Date.UTC(\n d.getUTCFullYear(),\n d.getUTCMonth(),\n d.getUTCDate(),\n d.getUTCHours(),\n d.getUTCMinutes(),\n d.getUTCSeconds()\n );\n\n return asIfUtc - actualUtc;\n}\n\nfunction parseParisStampToMs(s) {\n const text = (s ?? '').toString().trim();\n const m = text.match(/^(\\d{4})-(\\d{2})-(\\d{2}) (\\d{2}):(\\d{2}):(\\d{2})$/);\n if (!m) return null;\n\n const y = Number(m[1]);\n const mo = Number(m[2]);\n const d = Number(m[3]);\n const h = Number(m[4]);\n const mi = Number(m[5]);\n const se = Number(m[6]);\n\n if (![y, mo, d, h, mi, se].every(Number.isFinite)) return null;\n\n const guessUtc = Date.UTC(y, mo - 1, d, h, mi, se);\n const off1 = getTzOffsetMs(guessUtc, 'Europe/Paris');\n let ts = guessUtc - off1;\n const off2 = getTzOffsetMs(ts, 'Europe/Paris');\n\n if (off2 !== off1) ts = guessUtc - off2;\n return ts;\n}\n\nfunction cleanStateText(v) {\n const s = (v ?? '').toString().trim();\n const x = s.toLowerCase();\n return ['unknown', 'unavailable', 'none', 'null'].includes(x) ? '' : s;\n}\n\nfunction cleanParisStamp(v) {\n const s = cleanStateText(v);\n return parseParisStampToMs(s) !== null ? s : '';\n}\n\nfunction minutesSince(ts, nowMs) {\n if (ts === null || nowMs === null) return null;\n return Math.max(0, (nowMs - ts) / 60000);\n}\n\nfunction safeText(v, max = 255) {\n return (v ?? '').toString().slice(0, max);\n}\n\nconst cfg = {\n mobileNotifyDomain: 'notify',\n mobileNotifyService: 'mobile_app_iphone_16_de_tt',\n\n radiatorRealPowerW: 960,\n batteryNominalVoltageV: 52,\n\n radiatorStartSurplusRatio: 0.60,\n radiatorHoldSurplusRatio: 0.25,\n radiatorMinBatteryStartA: 2,\n radiatorMinBatteryHoldA: 0,\n radiatorMaxBatteryHoldDischargeA: -1.5\n};\n\nconst raw = $input.first().json;\nconst input = extractSnapshot(raw);\nconst now = getParisNowParts();\nconst nowMs = parseParisStampToMs(now.stamp);\n\n// ======================================================\n// INPUTS\n// ======================================================\nconst soc = clamp(num(input.soc_percent, 0), 0, 100);\nconst batteryCurrentA = num(input.battery_current_a, 0);\nconst pvPowerW = Math.max(0, num(input.pv_power_w, 0));\nconst pvPowerAvg10mW = Math.max(0, num(input.pv_power_10m_avg_w, pvPowerW));\nconst houseAcPowerW = Math.max(0, num(input.house_ac_power_w, 0));\nconst gridPowerW = num(input.grid_power_w, 0);\nconst gridIsOn = low(input.grid_state || 'off') === 'on';\nconst acInAvailable = num(input.victron_acin1_available, 0) === 1;\nconst gridAvailable = gridIsOn || acInAvailable;\n\nconst masterIsOn = low(input.sdb_master_power || 'off') === 'on';\nconst deshumideIsOn = low(input.deshumide_switch || 'off') === 'on';\nconst radiatorIsOn = low(input.radiator_switch || 'off') === 'on';\n\nconst masterEnergyTotalKwh = Math.max(0, num(input.sdb_master_energy_total, 0));\nconst deshumideEnergyTotalKwh = Math.max(0, num(input.deshumide_energy_total, 0));\nconst radiatorEnergyTotalKwh = Math.max(0, num(input.radiator_energy_total, 0));\n\nconst temperatureC = num(input.sdb_temperature_c, NaN);\nconst humidityPct = num(input.sdb_humidity_pct, NaN);\n// ⚠️ CAPTEUR MUET (10/10/2026). Le capteur sdb_temp_hygro est reste `unavailable`\n// du 04/10 14:52 au 10/10 00:20 (piles) : sans humidite, la regle « maintenir\n// jusqu'a H <= seuil OFF » ne pouvait jamais se terminer, et le deshumidificateur\n// (avec la prise SDB) tournait sans fin, y compris la nuit sur batterie a 26 %.\n// Muet = valeur illisible OU derniere publication il y a plus de 3 h.\nconst humidityAgeMin = num(input.sdb_humidity_age_min, NaN);\nconst CAPTEUR_MUET_MIN = 180;\nconst capteurMuet = !Number.isFinite(humidityPct) || (Number.isFinite(humidityAgeMin) && humidityAgeMin > CAPTEUR_MUET_MIN);\n// ⚠️ ENERGIE (10/10/2026) : le deshumidificateur est une charge REPORTABLE. Sur la\n// batterie, seulement si elle reste au-dessus de ce que le bilan juge necessaire\n// (profil prudent) ; sinon il attend EDF ferme ou un vrai surplus solaire. Pas de\n// seuil fixe : l'exigence vient du bilan. Bilan illisible -> pas de restriction\n// (comportement anterieur), pour ne jamais bloquer sur une panne de bilan.\nconst balanceRequiredPrudent = num(input.balance_required_soc_prudent, NaN);\nconst balanceOk = low(input.balance_status || '') === 'ok' && Number.isFinite(balanceRequiredPrudent);\n\nconst autoEnabled = low(input.auto_enabled || 'off') === 'on';\nconst notificationsEnabled = low(input.notifications_enabled || 'off') === 'on';\nconst manualAdoptEnabled = low(input.manual_adopt_enabled || 'off') === 'on';\n\nconst masterLatchIsOn = low(input.master_power_latch || 'off') === 'on';\nconst deshumideLatchIsOn = low(input.deshumide_latch || 'off') === 'on';\nconst radiatorLatchIsOn = low(input.radiator_latch || 'off') === 'on';\n\nconst tempOnMinC = num(input.temp_on_min_c, 21);\nconst tempOffMaxC = num(input.temp_off_max_c, 23);\nconst humidityOnPct = num(input.humidity_on_pct, 55);\nconst humidityOffPct = num(input.humidity_off_pct, 45);\n\nconst deshumideMinOnMinutes = Math.max(0, num(input.deshumide_min_on_minutes, 15));\nconst deshumideMinOffMinutes = Math.max(0, num(input.deshumide_min_off_minutes, 10));\n\nconst radiatorAutoEnabled = low(input.radiator_auto_enabled || 'off') === 'on';\nconst radiatorSocOn = clamp(num(input.radiator_soc_on, 90), 0, 100);\nconst radiatorSocOff = clamp(num(input.radiator_soc_off, 88), 0, 100);\nconst radiatorPvOnW = Math.max(0, num(input.radiator_pv_on_w, 2500));\nconst radiatorPvOffW = Math.max(0, num(input.radiator_pv_off_w, 1200));\nconst radiatorBatteryChargeCeilingA = Math.max(0, num(input.radiator_battery_charge_ceiling_a, 5));\nconst radiatorHouseAcMaxW = Math.max(0, num(input.radiator_house_ac_max_w, 6500));\nconst radiatorMinOnMinutes = Math.max(0, num(input.radiator_min_on_minutes, 15));\nconst radiatorMinOffMinutes = Math.max(0, num(input.radiator_min_off_minutes, 10));\n\nconst dayStartMin = parseTimeToMinutes(input.day_start_time, 9 * 60);\nconst dayStopMin = parseTimeToMinutes(input.day_stop_time, 19 * 60 + 15);\nconst inDayWindow =\n dayStartMin < dayStopMin\n ? (now.minNow >= dayStartMin && now.minNow < dayStopMin)\n : (now.minNow >= dayStartMin || now.minNow < dayStopMin);\n\nconst masterLastOnAt = cleanParisStamp(input.master_last_on_at);\nconst masterLastOffAt = cleanParisStamp(input.master_last_off_at);\nconst deshumideLastOnAt = cleanParisStamp(input.deshumide_last_on_at);\nconst deshumideLastOffAt = cleanParisStamp(input.deshumide_last_off_at);\nconst radiatorLastOnAt = cleanParisStamp(input.radiator_last_on_at);\nconst radiatorLastOffAt = cleanParisStamp(input.radiator_last_off_at);\nconst lastSignature = cleanStateText(input.last_signature);\n\n// ======================================================\n// EVCS PRIORITY\n// ======================================================\nconst evcsPriorityActiveRaw = low(input.evcs_priority_active || 'off') === 'on';\nconst energyPriorityMode = cleanStateText(input.energy_priority_mode || 'normal').toLowerCase() || 'normal';\nconst energyPriorityReason = cleanStateText(input.energy_priority_reason);\nconst evcsPriorityUntilAt = cleanParisStamp(input.evcs_priority_until);\nconst evcsPriorityUntilMs = parseParisStampToMs(evcsPriorityUntilAt);\n\nconst evcsPriorityActive =\n evcsPriorityActiveRaw &&\n (\n evcsPriorityUntilMs === null ||\n (nowMs !== null && evcsPriorityUntilMs > nowMs + 1000)\n );\n\n// ======================================================\n// TIMESTAMPS\n// ======================================================\nconst masterLastOnMs = parseParisStampToMs(masterLastOnAt);\nconst masterLastOffMs = parseParisStampToMs(masterLastOffAt);\nconst deshumideLastOnMs = parseParisStampToMs(deshumideLastOnAt);\nconst deshumideLastOffMs = parseParisStampToMs(deshumideLastOffAt);\nconst radiatorLastOnMs = parseParisStampToMs(radiatorLastOnAt);\nconst radiatorLastOffMs = parseParisStampToMs(radiatorLastOffAt);\n\nconst minutesSinceMasterOn = minutesSince(masterLastOnMs, nowMs);\nconst minutesSinceMasterOff = minutesSince(masterLastOffMs, nowMs);\nconst minutesSinceDeshumideOn = minutesSince(deshumideLastOnMs, nowMs);\nconst minutesSinceDeshumideOff = minutesSince(deshumideLastOffMs, nowMs);\nconst minutesSinceRadiatorOn = minutesSince(radiatorLastOnMs, nowMs);\nconst minutesSinceRadiatorOff = minutesSince(radiatorLastOffMs, nowMs);\n\nconst deshumideMinOnElapsed = minutesSinceDeshumideOn === null ? true : minutesSinceDeshumideOn >= deshumideMinOnMinutes;\nconst deshumideMinOffElapsed = minutesSinceDeshumideOff === null ? true : minutesSinceDeshumideOff >= deshumideMinOffMinutes;\nconst radiatorMinOnElapsed = minutesSinceRadiatorOn === null ? true : minutesSinceRadiatorOn >= radiatorMinOnMinutes;\nconst radiatorMinOffElapsed = minutesSinceRadiatorOff === null ? true : minutesSinceRadiatorOff >= radiatorMinOffMinutes;\n\n// ======================================================\n// BESOINS METIER SEPARES\n// ======================================================\nconst heatNeed = Number.isFinite(temperatureC) ? temperatureC < tempOnMinC : false;\nconst heatRecovered = Number.isFinite(temperatureC) ? temperatureC >= tempOffMaxC : false;\n\nconst humidityNeed = Number.isFinite(humidityPct) ? humidityPct > humidityOnPct : false;\nconst humidityRecovered = Number.isFinite(humidityPct) ? humidityPct <= humidityOffPct : false;\n\nconst comfortDemand = heatNeed || humidityNeed;\nconst comfortRecovered = heatRecovered && humidityRecovered;\n\n// ======================================================\n// RADIATEUR SDB : SURPLUS REEL V2\n// ======================================================\nconst estimatedBatteryChargeW =\n Number.isFinite(batteryCurrentA) && batteryCurrentA > 0\n ? batteryCurrentA * cfg.batteryNominalVoltageV\n : 0;\n\nconst directPvHeadroomW = Math.max(0, pvPowerAvg10mW - houseAcPowerW);\nconst estimatedSurplusW = Math.max(0, directPvHeadroomW + estimatedBatteryChargeW);\n\nconst radiatorStartSurplusNeededW = Math.round(cfg.radiatorRealPowerW * cfg.radiatorStartSurplusRatio);\nconst radiatorHoldSurplusNeededW = Math.round(cfg.radiatorRealPowerW * cfg.radiatorHoldSurplusRatio);\n\nconst radiatorBatteryStartOk =\n Number.isFinite(batteryCurrentA) &&\n batteryCurrentA >= cfg.radiatorMinBatteryStartA &&\n batteryCurrentA <= radiatorBatteryChargeCeilingA;\n\nconst radiatorBatteryHoldOk =\n Number.isFinite(batteryCurrentA) &&\n batteryCurrentA >= cfg.radiatorMaxBatteryHoldDischargeA &&\n batteryCurrentA <= radiatorBatteryChargeCeilingA + 4;\n\nconst radiatorStartByAbsolutePv =\n pvPowerAvg10mW >= radiatorPvOnW && radiatorBatteryStartOk;\n\nconst radiatorStartByEstimatedSurplus =\n estimatedSurplusW >= radiatorStartSurplusNeededW && radiatorBatteryStartOk;\n\nconst radiatorHoldByAbsolutePv =\n pvPowerAvg10mW >= radiatorPvOffW && radiatorBatteryHoldOk;\n\nconst radiatorHoldByEstimatedSurplus =\n estimatedSurplusW >= radiatorHoldSurplusNeededW && radiatorBatteryHoldOk;\n\nconst radiatorStartConditionsOk =\n radiatorAutoEnabled &&\n heatNeed &&\n inDayWindow &&\n !gridAvailable &&\n !evcsPriorityActive &&\n soc >= radiatorSocOn &&\n houseAcPowerW < radiatorHouseAcMaxW &&\n (radiatorStartByAbsolutePv || radiatorStartByEstimatedSurplus);\n\nconst radiatorHoldConditionsOk =\n radiatorAutoEnabled &&\n !heatRecovered &&\n inDayWindow &&\n !gridAvailable &&\n !evcsPriorityActive &&\n soc >= radiatorSocOff &&\n houseAcPowerW < radiatorHouseAcMaxW &&\n (radiatorHoldByAbsolutePv || radiatorHoldByEstimatedSurplus);\n\n// ======================================================\n// SORTIES DESIREES\n// ======================================================\nlet desiredDeshumide = false;\nlet desiredRadiator = false;\nlet desiredMaster = false;\n\nlet state = 'idle';\nlet reason = [];\n\nlet dehumActionReason = 'idle';\nlet radiatorActionReason = 'idle';\n\nlet adoptMaster = false;\nlet adoptDeshumide = false;\nlet adoptRadiator = false;\n\nlet notifyTitle = 'SDB Climate & Surplus';\nlet notifyMessage = '';\nlet notifyWanted = false;\n\n// ======================================================\n// MODE SURPLUS SOLAIRE (10/10/2026, proprietaire)\n// ======================================================\n// Les jours de grosse production, le seche-serviette (« radiateur ») ET le\n// deshumidificateur prennent le solaire que personne ne prend. L'ancien\n// demarrage exigeait un besoin de chauffage (T < 20 °C) et une batterie en\n// charge entre 2 et 5 A : dernier demarrage le 05/06 ; le 08/10 la batterie\n// passait de 162 A (13 h) a 1-2 A (15-17 h, solaire bride) sans jamais tenir\n// dans la fenetre. Le bon signal est celui des chargeurs eux-memes (HANDOFF §37) :\n// un chargeur qui se BRIDE prouve un solaire refuse.\n// Demarrage : >= 1 chargeur bride, SOC >= 90, EDF ouvert, journee, pas de\n// priorite borne, et pas de voiture branchee qui attend ce surplus (elle passe\n// d'abord). Maintien : la batterie ne se decharge pas (> -3 A) ou un chargeur\n// est encore bride, SOC >= 85. Seche-serviette : arret a 28 °C, et pas de\n// demarrage si la temperature est inconnue.\nconst SURPLUS_SOC_START = 90;\nconst SURPLUS_SOC_HOLD = 85;\nconst SURPLUS_DECHARGE_MAX_A = -3;\nconst SURPLUS_TEMP_MAX_C = 28;\nconst mpptModes = String(input.mppt_operation_modes || '').split(',').map((x) => x.trim().toLowerCase()).filter(Boolean);\nconst mpptLimitedCount = mpptModes.filter((m) => m === 'voltage_current_limited').length;\nconst voitureAttendSurplus =\n low(input.evcs_vehicle_connected || 'off') === 'on' &&\n num(input.evcs_ac_power_w, 0) < 100 &&\n !/full|done|complete|charged/.test(low(input.evcs_surplus_state || ''));\nconst surplusCommun = autoEnabled && inDayWindow && !gridIsOn && !evcsPriorityActive && !voitureAttendSurplus;\nconst surplusStart = surplusCommun && mpptLimitedCount >= 1 && soc >= SURPLUS_SOC_START;\nconst surplusHold = surplusCommun && soc >= SURPLUS_SOC_HOLD &&\n (mpptLimitedCount >= 1 || (Number.isFinite(batteryCurrentA) && batteryCurrentA > SURPLUS_DECHARGE_MAX_A));\nconst surplusPrecedent = /\\|sp=1/.test(lastSignature || '');\nconst radiateurTempOk = Number.isFinite(temperatureC) && temperatureC < SURPLUS_TEMP_MAX_C;\nconst deshumideSurplus = surplusStart || (surplusHold && (deshumideIsOn || deshumideLatchIsOn));\nconst radiateurSurplus = radiatorAutoEnabled && radiateurTempOk &&\n (surplusStart || (surplusHold && (radiatorIsOn || radiatorLatchIsOn)));\n\n// Energie disponible pour une charge reportable (calcule apres les entrees).\nconst deshumideEnergyOk =\n gridIsOn ||\n (pvPowerAvg10mW - houseAcPowerW) > 0 ||\n !balanceOk ||\n soc >= balanceRequiredPrudent;\n\n// ======================================================\n// DESHUMIDIFICATEUR - PRIORITAIRE HUMIDITE\n// ======================================================\nif (!autoEnabled) {\n desiredDeshumide = deshumideIsOn;\n dehumActionReason = 'automation_disabled';\n}\nelse if (deshumideSurplus) {\n desiredDeshumide = deshumideIsOn || deshumideMinOffElapsed;\n dehumActionReason = desiredDeshumide ? 'deshumide_surplus_solaire' : 'deshumide_wait_min_off';\n}\nelse if (surplusPrecedent && !humidityNeed && (deshumideIsOn || deshumideLatchIsOn)) {\n // Demarre pour le surplus, pas pour l'humidite : il s'arrete avec le surplus.\n desiredDeshumide = !deshumideMinOnElapsed;\n dehumActionReason = deshumideMinOnElapsed ? 'deshumide_stop_fin_surplus' : 'deshumide_hold_min_on';\n}\nelse if (capteurMuet && (deshumideIsOn || deshumideLatchIsOn)) {\n // Sans mesure, on ne tient plus rien au-dela du temps minimum.\n desiredDeshumide = !deshumideMinOnElapsed;\n dehumActionReason = deshumideMinOnElapsed ? 'deshumide_stop_capteur_muet' : 'deshumide_hold_min_on';\n}\nelse if (capteurMuet) {\n desiredDeshumide = false;\n dehumActionReason = 'deshumide_capteur_muet';\n}\nelse if (!deshumideEnergyOk && (deshumideIsOn || deshumideLatchIsOn)) {\n desiredDeshumide = !deshumideMinOnElapsed;\n dehumActionReason = deshumideMinOnElapsed ? 'deshumide_stop_batterie_reservee' : 'deshumide_hold_min_on';\n}\nelse if (!deshumideEnergyOk && humidityNeed) {\n desiredDeshumide = false;\n dehumActionReason = 'deshumide_attente_energie';\n}\nelse if (humidityNeed) {\n if (deshumideIsOn) {\n desiredDeshumide = true;\n dehumActionReason = 'deshumide_already_running';\n } else if (deshumideMinOffElapsed) {\n desiredDeshumide = true;\n dehumActionReason = 'deshumide_start_on_humidity_need';\n } else {\n desiredDeshumide = false;\n dehumActionReason = 'deshumide_wait_min_off';\n }\n}\nelse if (deshumideIsOn || deshumideLatchIsOn) {\n if (!humidityRecovered) {\n desiredDeshumide = true;\n dehumActionReason = 'deshumide_hold_until_humidity_recovered';\n } else if (!deshumideMinOnElapsed) {\n desiredDeshumide = true;\n dehumActionReason = 'deshumide_hold_min_on';\n } else {\n desiredDeshumide = false;\n dehumActionReason = 'deshumide_stop_humidity_recovered';\n }\n}\nelse {\n desiredDeshumide = false;\n dehumActionReason = 'deshumide_idle';\n}\n\n// Adoption manuelle déshumide uniquement si cohérent avec la demande\nif (manualAdoptEnabled && desiredDeshumide && deshumideIsOn && !deshumideLatchIsOn) {\n adoptDeshumide = true;\n}\n\n// ======================================================\n// RADIATEUR - CHAUFFAGE + SURPLUS + PRIORITE EVCS\n// ======================================================\nif (!autoEnabled) {\n desiredRadiator = radiatorIsOn;\n radiatorActionReason = 'automation_disabled';\n}\nelse if (!radiatorAutoEnabled) {\n desiredRadiator = radiatorIsOn;\n radiatorActionReason = 'radiator_auto_disabled_observe_only';\n}\nelse if (evcsPriorityActive) {\n if (radiatorIsOn || radiatorLatchIsOn) {\n if (!radiatorMinOnElapsed) {\n desiredRadiator = true;\n radiatorActionReason = 'radiator_hold_min_on_evcs_priority';\n } else {\n desiredRadiator = false;\n radiatorActionReason = 'radiator_blocked_by_evcs_priority';\n }\n } else {\n desiredRadiator = false;\n radiatorActionReason = 'radiator_blocked_by_evcs_priority';\n }\n}\nelse if (radiateurSurplus) {\n desiredRadiator = radiatorIsOn || radiatorMinOffElapsed;\n radiatorActionReason = desiredRadiator ? 'radiator_surplus_solaire' : 'radiator_wait_min_off';\n}\nelse if ((radiatorIsOn || radiatorLatchIsOn) && Number.isFinite(temperatureC) && temperatureC >= SURPLUS_TEMP_MAX_C) {\n desiredRadiator = !radiatorMinOnElapsed;\n radiatorActionReason = radiatorMinOnElapsed ? 'radiator_stop_temperature_max' : 'radiator_hold_min_on';\n}\nelse if (surplusPrecedent && !heatNeed && (radiatorIsOn || radiatorLatchIsOn)) {\n desiredRadiator = !radiatorMinOnElapsed;\n radiatorActionReason = radiatorMinOnElapsed ? 'radiator_stop_fin_surplus' : 'radiator_hold_min_on';\n}\nelse if (heatNeed && radiatorStartConditionsOk) {\n if (radiatorIsOn) {\n desiredRadiator = true;\n radiatorActionReason = 'radiator_already_running';\n } else if (radiatorMinOffElapsed) {\n desiredRadiator = true;\n radiatorActionReason = 'radiator_start_heat_need_surplus_v2';\n } else {\n desiredRadiator = false;\n radiatorActionReason = 'radiator_wait_min_off';\n }\n}\nelse if (radiatorIsOn || radiatorLatchIsOn) {\n if (radiatorHoldConditionsOk) {\n desiredRadiator = true;\n radiatorActionReason = 'radiator_hold_heat_need_surplus_v2';\n } else if (!radiatorMinOnElapsed) {\n desiredRadiator = true;\n radiatorActionReason = 'radiator_hold_min_on';\n } else if (evcsPriorityActive) {\n desiredRadiator = false;\n radiatorActionReason = 'radiator_stop_evcs_priority';\n } else if (heatRecovered) {\n desiredRadiator = false;\n radiatorActionReason = 'radiator_stop_heat_recovered';\n } else {\n desiredRadiator = false;\n radiatorActionReason = 'radiator_stop_surplus_lost';\n }\n}\nelse {\n desiredRadiator = false;\n radiatorActionReason = heatNeed ? 'radiator_heat_need_but_energy_not_ok' : 'radiator_idle';\n}\n\n// Adoption manuelle radiateur uniquement si cohérent et hors EVCS\nif (\n manualAdoptEnabled &&\n desiredRadiator &&\n radiatorIsOn &&\n !radiatorLatchIsOn &&\n !evcsPriorityActive\n) {\n adoptRadiator = true;\n}\n\n// ======================================================\n// MASTER SDB PRISE = COORDINATEUR\n// ======================================================\nif (!autoEnabled) {\n desiredMaster = masterIsOn;\n} else {\n desiredMaster = desiredDeshumide || desiredRadiator;\n}\n\nif (manualAdoptEnabled && desiredMaster && masterIsOn && !masterLatchIsOn) {\n adoptMaster = true;\n}\n\n// ======================================================\n// ETAT GLOBAL\n// ======================================================\nif (!autoEnabled) {\n state = 'automation_disabled';\n reason.push(\"Automatisation SDB climate/surplus désactivée\");\n}\nelse if (desiredDeshumide && desiredRadiator) {\n state = 'deshumide_and_radiator_on';\n reason.push(\"Déshumidification active + radiateur actif sur surplus\");\n}\nelse if (desiredDeshumide) {\n state = 'deshumide_on';\n reason.push(\"Déshumidification SDB active\");\n}\nelse if (desiredRadiator) {\n state = 'radiator_on';\n reason.push(\"Radiateur SDB actif sur besoin thermique et surplus\");\n}\nelse if (radiatorActionReason === 'radiator_blocked_by_evcs_priority' || radiatorActionReason === 'radiator_stop_evcs_priority') {\n state = 'radiator_blocked_by_evcs_priority';\n reason.push(\"Radiateur SDB bloqué par priorité EVCS\");\n}\nelse if (dehumActionReason === 'deshumide_wait_min_off') {\n state = 'wait_deshumide_min_off';\n reason.push(\"Déshumidificateur en attente de temps mini OFF\");\n}\nelse if (radiatorActionReason === 'radiator_wait_min_off') {\n state = 'wait_radiator_min_off';\n reason.push(\"Radiateur en attente de temps mini OFF\");\n}\nelse {\n state = 'idle';\n reason.push(\"Aucune demande SDB active\");\n}\n\nreason.push(`Température SDB ${Number.isFinite(temperatureC) ? round1(temperatureC) + '°C' : 'n/a'}`);\nreason.push(`Humidité SDB ${Number.isFinite(humidityPct) ? round1(humidityPct) + '%' : 'n/a'}`);\n\nreason.push(`Chauffage ON si T<${round1(tempOnMinC)}°C`);\nreason.push(`Chauffage OFF si T>=${round1(tempOffMaxC)}°C`);\nreason.push(`Déshumidification ON si H>${round1(humidityOnPct)}%`);\nreason.push(`Déshumidification OFF si H<=${round1(humidityOffPct)}%`);\n\nreason.push(`Besoin chauffage=${heatNeed ? 'oui' : 'non'} | Rétabli chauffage=${heatRecovered ? 'oui' : 'non'}`);\nreason.push(`Besoin humidité=${humidityNeed ? 'oui' : 'non'} | Humidité rétablie=${humidityRecovered ? 'oui' : 'non'}`);\n\nreason.push(`Déshumide: ${dehumActionReason}`);\nif (deshumideSurplus || radiateurSurplus) reason.push(`☀️ Surplus solaire : ${mpptLimitedCount} chargeur(s) bride(s), SOC ${round1(soc)}% -> ${[deshumideSurplus ? 'deshumidificateur' : '', radiateurSurplus ? 'seche-serviette' : ''].filter(Boolean).join(' + ')}`);\nif (capteurMuet) reason.push(`⚠️ Capteur SDB muet${Number.isFinite(humidityAgeMin) && humidityAgeMin >= 0 ? ` depuis ${Math.round(humidityAgeMin)} min` : ''} (piles ?)`);\nif (!deshumideEnergyOk) reason.push(`Batterie reservee : SOC ${round1(soc)}% < ${round1(balanceRequiredPrudent)}% (bilan, prudent), EDF ouvert, pas de surplus`);\nreason.push(`Radiateur: ${radiatorActionReason}`);\n\nreason.push(`Master=${masterIsOn ? 'ON' : 'OFF'} | Deshumide=${deshumideIsOn ? 'ON' : 'OFF'} | Radiateur=${radiatorIsOn ? 'ON' : 'OFF'}`);\nreason.push(`SOC ${round1(soc)}% | PV ${Math.round(pvPowerW)}W | PV10 ${Math.round(pvPowerAvg10mW)}W | AC maison ${Math.round(houseAcPowerW)}W | Batterie ${round1(batteryCurrentA)}A`);\nreason.push(`Réseau cmd=${gridIsOn ? 'ON' : 'OFF'} | acin=${acInAvailable ? 'OK' : 'KO'} | dispo=${gridAvailable ? 'oui' : 'non'}`);\nreason.push(`Fenêtre jour ${inDayWindow ? 'OK' : 'Hors fenêtre'}`);\nreason.push(`Prio EVCS ${evcsPriorityActive ? 'ON' : 'OFF'}${energyPriorityReason ? ` | ${energyPriorityReason}` : ''}`);\n\nreason.push(`Radiateur ${Math.round(cfg.radiatorRealPowerW)}W | Headroom PV ${Math.round(directPvHeadroomW)}W | Charge batt estimée ${Math.round(estimatedBatteryChargeW)}W | Surplus estimé ${Math.round(estimatedSurplusW)}W`);\nreason.push(`Start radiateur si surplus >= ${Math.round(radiatorStartSurplusNeededW)}W ou PV10 >= ${Math.round(radiatorPvOnW)}W`);\nreason.push(`Hold radiateur si surplus >= ${Math.round(radiatorHoldSurplusNeededW)}W ou PV10 >= ${Math.round(radiatorPvOffW)}W`);\n\nif (adoptMaster) reason.push(\"Prise maître SDB adoptée\");\nif (adoptDeshumide) reason.push(\"Déshumidificateur adopté\");\nif (adoptRadiator) reason.push(\"Radiateur adopté\");\n\n// ======================================================\n// ACTIONS / LATCHES\n// ======================================================\nconst desiredMasterLatch = desiredMaster ? 'on' : 'off';\nconst desiredDeshumideLatch = desiredDeshumide ? 'on' : 'off';\nconst desiredRadiatorLatch = desiredRadiator ? 'on' : 'off';\n\nconst masterAction =\n desiredMaster === masterIsOn\n ? 'none'\n : desiredMaster ? 'turn_on' : 'turn_off';\n\nconst deshumideAction =\n desiredDeshumide === deshumideIsOn\n ? 'none'\n : desiredDeshumide ? 'turn_on' : 'turn_off';\n\nconst radiatorAction =\n desiredRadiator === radiatorIsOn\n ? 'none'\n : desiredRadiator ? 'turn_on' : 'turn_off';\n\nconst masterLatchAction =\n desiredMasterLatch === (masterLatchIsOn ? 'on' : 'off')\n ? 'none'\n : desiredMasterLatch === 'on' ? 'turn_on' : 'turn_off';\n\nconst deshumideLatchAction =\n desiredDeshumideLatch === (deshumideLatchIsOn ? 'on' : 'off')\n ? 'none'\n : desiredDeshumideLatch === 'on' ? 'turn_on' : 'turn_off';\n\nconst radiatorLatchAction =\n desiredRadiatorLatch === (radiatorLatchIsOn ? 'on' : 'off')\n ? 'none'\n : desiredRadiatorLatch === 'on' ? 'turn_on' : 'turn_off';\n\n// ======================================================\n// TIMESTAMPS\n// ======================================================\nconst setMasterLastOnAt = masterAction === 'turn_on' || adoptMaster;\nconst setMasterLastOffAt = masterAction === 'turn_off';\n\nconst setDeshumideLastOnAt = deshumideAction === 'turn_on' || adoptDeshumide;\nconst setDeshumideLastOffAt = deshumideAction === 'turn_off';\n\nconst setRadiatorLastOnAt = radiatorAction === 'turn_on' || adoptRadiator;\nconst setRadiatorLastOffAt = radiatorAction === 'turn_off';\n\n// ======================================================\n// NOTIFICATIONS\n// ======================================================\nif (state === 'deshumide_on' && (deshumideAction === 'turn_on' || adoptDeshumide)) {\n notifyTitle = '💧 Déshumidification SDB active';\n notifyMessage = `SDB: T=${Number.isFinite(temperatureC) ? round1(temperatureC) : 'n/a'}°C | H=${Number.isFinite(humidityPct) ? round1(humidityPct) : 'n/a'}%.`;\n notifyWanted = true;\n}\nelse if ((dehumActionReason === 'deshumide_stop_capteur_muet' || dehumActionReason === 'deshumide_stop_batterie_reservee') && deshumideAction === 'turn_off') {\n notifyTitle = dehumActionReason === 'deshumide_stop_capteur_muet' ? '⚠️ Capteur SDB muet : déshumidificateur arrêté' : '🔋 Déshumidificateur SDB arrêté : batterie réservée';\n notifyMessage = dehumActionReason === 'deshumide_stop_capteur_muet'\n ? 'Le capteur température/humidité de la SDB ne publie plus (piles ?). Sans mesure, le déshumidificateur ne peut plus être piloté.'\n : `SOC ${round1(soc)}% sous les ${round1(balanceRequiredPrudent)}% que le bilan réserve ; il repartira avec EDF ou le soleil.`;\n notifyWanted = true;\n}\nelse if (dehumActionReason === 'deshumide_stop_humidity_recovered' && deshumideAction === 'turn_off') {\n notifyTitle = '✅ Déshumidification SDB arrêtée';\n notifyMessage = `Humidité revenue dans la zone cible : H=${Number.isFinite(humidityPct) ? round1(humidityPct) : 'n/a'}%.`;\n notifyWanted = true;\n}\nelse if (state === 'radiator_on' && radiatorAction === 'turn_on') {\n notifyTitle = '☀️ Radiateur SDB actif sur surplus';\n notifyMessage = `T=${Number.isFinite(temperatureC) ? round1(temperatureC) : 'n/a'}°C | SOC ${round1(soc)}% | PV10 ${Math.round(pvPowerAvg10mW)}W | Surplus ${Math.round(estimatedSurplusW)}W.`;\n notifyWanted = true;\n}\nelse if (\n (radiatorActionReason === 'radiator_stop_surplus_lost' || radiatorActionReason === 'radiator_stop_heat_recovered') &&\n radiatorAction === 'turn_off'\n) {\n notifyTitle = '⛔ Radiateur SDB arrêté';\n notifyMessage = `T=${Number.isFinite(temperatureC) ? round1(temperatureC) : 'n/a'}°C | PV10 ${Math.round(pvPowerAvg10mW)}W | Surplus ${Math.round(estimatedSurplusW)}W.`;\n notifyWanted = true;\n}\nelse if (\n (radiatorActionReason === 'radiator_blocked_by_evcs_priority' || radiatorActionReason === 'radiator_stop_evcs_priority') &&\n radiatorAction === 'turn_off'\n) {\n notifyTitle = '🚗 Priorité EVCS : radiateur SDB coupé';\n notifyMessage = `${energyPriorityReason || 'La recharge EVCS est prioritaire'}${evcsPriorityUntilAt ? ` jusqu'à ${evcsPriorityUntilAt}` : ''}.`;\n notifyWanted = true;\n}\nelse if (state === 'deshumide_and_radiator_on' && (deshumideAction === 'turn_on' || radiatorAction === 'turn_on')) {\n notifyTitle = '🌤️ SDB active : humidité + chauffage';\n notifyMessage = `Déshumidificateur et radiateur actifs. T=${Number.isFinite(temperatureC) ? round1(temperatureC) : 'n/a'}°C | H=${Number.isFinite(humidityPct) ? round1(humidityPct) : 'n/a'}% | SOC ${round1(soc)}%.`;\n notifyWanted = true;\n}\n\nconst shortReason = reason.join(' | ').slice(0, 255);\n\nconst signature = [\n now.date,\n state,\n `m=${desiredMaster ? '1' : '0'}`,\n `d=${desiredDeshumide ? '1' : '0'}`,\n `r=${desiredRadiator ? '1' : '0'}`,\n `heat=${heatNeed ? '1' : '0'}`,\n `hum=${humidityNeed ? '1' : '0'}`,\n `ev=${evcsPriorityActive ? '1' : '0'}`,\n `t=${Number.isFinite(temperatureC) ? round1(temperatureC) : 'na'}`,\n `h=${Number.isFinite(humidityPct) ? round1(humidityPct) : 'na'}`,\n `sx=${Math.round(estimatedSurplusW)}`,\n `muet=${capteurMuet ? '1' : '0'}`,\n `sp=${(deshumideSurplus || radiateurSurplus) ? '1' : '0'}`,\n `nrj=${deshumideEnergyOk ? '1' : '0'}`\n].join('|');\n\nconst signatureChanged = signature !== lastSignature;\n\n// ======================================================\n// OUTPUT\n// ======================================================\nconst result = {\n timestamp_paris: now.stamp,\n\n inputs: {\n soc_percent: soc,\n battery_current_a: batteryCurrentA,\n pv_power_w: pvPowerW,\n pv_power_10m_avg_w: pvPowerAvg10mW,\n house_ac_power_w: houseAcPowerW,\n grid_power_w: gridPowerW,\n grid_state: gridIsOn ? 'on' : 'off',\n victron_acin1_available: acInAvailable ? 1 : 0,\n\n sdb_master_power: masterIsOn,\n sdb_master_energy_total: round2(masterEnergyTotalKwh),\n deshumide_switch: deshumideIsOn,\n deshumide_energy_total: round2(deshumideEnergyTotalKwh),\n radiator_switch: radiatorIsOn,\n radiator_energy_total: round2(radiatorEnergyTotalKwh),\n\n sdb_temperature_c: Number.isFinite(temperatureC) ? round1(temperatureC) : null,\n sdb_humidity_pct: Number.isFinite(humidityPct) ? round1(humidityPct) : null,\n\n auto_enabled: autoEnabled,\n notifications_enabled: notificationsEnabled,\n manual_adopt_enabled: manualAdoptEnabled,\n\n master_power_latch: masterLatchIsOn,\n deshumide_latch: deshumideLatchIsOn,\n radiator_latch: radiatorLatchIsOn,\n\n temp_on_min_c: tempOnMinC,\n temp_off_max_c: tempOffMaxC,\n humidity_on_pct: humidityOnPct,\n humidity_off_pct: humidityOffPct,\n\n deshumide_min_on_minutes: deshumideMinOnMinutes,\n deshumide_min_off_minutes: deshumideMinOffMinutes,\n\n radiator_auto_enabled: radiatorAutoEnabled,\n radiator_soc_on: radiatorSocOn,\n radiator_soc_off: radiatorSocOff,\n radiator_pv_on_w: radiatorPvOnW,\n radiator_pv_off_w: radiatorPvOffW,\n radiator_battery_charge_ceiling_a: radiatorBatteryChargeCeilingA,\n radiator_house_ac_max_w: radiatorHouseAcMaxW,\n radiator_min_on_minutes: radiatorMinOnMinutes,\n radiator_min_off_minutes: radiatorMinOffMinutes,\n\n radiator_real_power_w_internal: cfg.radiatorRealPowerW,\n\n day_start_time: input.day_start_time || null,\n day_stop_time: input.day_stop_time || null,\n\n master_last_on_at: masterLastOnAt || null,\n master_last_off_at: masterLastOffAt || null,\n deshumide_last_on_at: deshumideLastOnAt || null,\n deshumide_last_off_at: deshumideLastOffAt || null,\n radiator_last_on_at: radiatorLastOnAt || null,\n radiator_last_off_at: radiatorLastOffAt || null,\n\n evcs_priority_active: evcsPriorityActive,\n energy_priority_mode: energyPriorityMode,\n energy_priority_reason: energyPriorityReason || null,\n evcs_priority_until: evcsPriorityUntilAt || null,\n\n last_signature: lastSignature || null\n },\n\n computed: {\n in_day_window: inDayWindow,\n\n heat_need: heatNeed,\n heat_recovered: heatRecovered,\n humidity_need: humidityNeed,\n humidity_recovered: humidityRecovered,\n comfort_demand: comfortDemand,\n comfort_recovered: comfortRecovered,\n\n temp_too_low: heatNeed,\n humidity_too_high: humidityNeed,\n\n direct_pv_headroom_w: Math.round(directPvHeadroomW),\n estimated_battery_charge_w: Math.round(estimatedBatteryChargeW),\n estimated_surplus_w: Math.round(estimatedSurplusW),\n\n radiator_start_surplus_needed_w: Math.round(radiatorStartSurplusNeededW),\n radiator_hold_surplus_needed_w: Math.round(radiatorHoldSurplusNeededW),\n\n radiator_start_by_absolute_pv: radiatorStartByAbsolutePv,\n radiator_start_by_estimated_surplus: radiatorStartByEstimatedSurplus,\n radiator_hold_by_absolute_pv: radiatorHoldByAbsolutePv,\n radiator_hold_by_estimated_surplus: radiatorHoldByEstimatedSurplus,\n\n radiator_start_conditions_ok: radiatorStartConditionsOk,\n radiator_hold_conditions_ok: radiatorHoldConditionsOk,\n radiator_battery_start_ok: radiatorBatteryStartOk,\n radiator_battery_hold_ok: radiatorBatteryHoldOk,\n\n evcs_priority_active: evcsPriorityActive,\n grid_available: gridAvailable,\n\n dehum_action_reason: dehumActionReason,\n radiator_action_reason: radiatorActionReason,\n\n minutes_since_master_on: minutesSinceMasterOn !== null ? round1(minutesSinceMasterOn) : null,\n minutes_since_master_off: minutesSinceMasterOff !== null ? round1(minutesSinceMasterOff) : null,\n minutes_since_deshumide_on: minutesSinceDeshumideOn !== null ? round1(minutesSinceDeshumideOn) : null,\n minutes_since_deshumide_off: minutesSinceDeshumideOff !== null ? round1(minutesSinceDeshumideOff) : null,\n minutes_since_radiator_on: minutesSinceRadiatorOn !== null ? round1(minutesSinceRadiatorOn) : null,\n minutes_since_radiator_off: minutesSinceRadiatorOff !== null ? round1(minutesSinceRadiatorOff) : null,\n\n deshumide_min_on_elapsed: deshumideMinOnElapsed,\n deshumide_min_off_elapsed: deshumideMinOffElapsed,\n radiator_min_on_elapsed: radiatorMinOnElapsed,\n radiator_min_off_elapsed: radiatorMinOffElapsed\n },\n\n decision: {\n state,\n desired_master: desiredMaster,\n desired_deshumide: desiredDeshumide,\n desired_radiator: desiredRadiator,\n\n master_action: masterAction,\n deshumide_action: deshumideAction,\n radiator_action: radiatorAction,\n\n master_latch_action: masterLatchAction,\n deshumide_latch_action: deshumideLatchAction,\n radiator_latch_action: radiatorLatchAction,\n\n adopt_master: adoptMaster,\n adopt_deshumide: adoptDeshumide,\n adopt_radiator: adoptRadiator,\n\n set_master_last_on_at: setMasterLastOnAt,\n set_master_last_off_at: setMasterLastOffAt,\n set_deshumide_last_on_at: setDeshumideLastOnAt,\n set_deshumide_last_off_at: setDeshumideLastOffAt,\n set_radiator_last_on_at: setRadiatorLastOnAt,\n set_radiator_last_off_at: setRadiatorLastOffAt,\n\n should_notify: notificationsEnabled && signatureChanged && notifyWanted,\n signature,\n signature_changed: signatureChanged,\n reason,\n short_reason: shortReason\n },\n\n ha_service_calls: {\n helper_state: {\n execute: true,\n domain: 'input_text',\n service: 'set_value',\n data: {\n entity_id: 'input_text.sdb_climate_surplus_state',\n value: state\n }\n },\n\n helper_reason: {\n execute: true,\n domain: 'input_text',\n service: 'set_value',\n data: {\n entity_id: 'input_text.sdb_climate_surplus_reason',\n value: shortReason || 'Aucune raison'\n }\n },\n\n helper_signature: {\n execute: signatureChanged,\n domain: 'input_text',\n service: 'set_value',\n data: {\n entity_id: 'input_text.sdb_climate_surplus_last_signature',\n value: signature\n }\n },\n\n helper_master_latch: {\n execute: masterLatchAction !== 'none',\n domain: 'input_boolean',\n service: desiredMaster ? 'turn_on' : 'turn_off',\n data: {\n entity_id: 'input_boolean.sdb_master_power_latch'\n }\n },\n\n helper_deshumide_latch: {\n execute: deshumideLatchAction !== 'none',\n domain: 'input_boolean',\n service: desiredDeshumide ? 'turn_on' : 'turn_off',\n data: {\n entity_id: 'input_boolean.sdb_deshumide_latch'\n }\n },\n\n helper_radiator_latch: {\n execute: radiatorLatchAction !== 'none',\n domain: 'input_boolean',\n service: desiredRadiator ? 'turn_on' : 'turn_off',\n data: {\n entity_id: 'input_boolean.sdb_radiator_latch'\n }\n },\n\n master_power: {\n execute: masterAction !== 'none',\n domain: 'switch',\n service: desiredMaster ? 'turn_on' : 'turn_off',\n data: {\n entity_id: 'switch.sdb_prise_socket_1'\n }\n },\n\n deshumide: {\n execute: deshumideAction !== 'none',\n domain: 'switch',\n service: desiredDeshumide ? 'turn_on' : 'turn_off',\n data: {\n entity_id: 'switch.deshumide_sdb_socket_1'\n }\n },\n\n radiator: {\n execute: radiatorAction !== 'none',\n domain: 'switch',\n service: desiredRadiator ? 'turn_on' : 'turn_off',\n data: {\n entity_id: 'switch.sdb_radiateur_socket_1'\n }\n },\n\n helper_master_last_on_at: {\n execute: setMasterLastOnAt,\n domain: 'input_datetime',\n service: 'set_datetime',\n data: {\n entity_id: 'input_datetime.sdb_master_last_on_at',\n datetime: now.stamp\n }\n },\n\n helper_master_last_off_at: {\n execute: setMasterLastOffAt,\n domain: 'input_datetime',\n service: 'set_datetime',\n data: {\n entity_id: 'input_datetime.sdb_master_last_off_at',\n datetime: now.stamp\n }\n },\n\n helper_deshumide_last_on_at: {\n execute: setDeshumideLastOnAt,\n domain: 'input_datetime',\n service: 'set_datetime',\n data: {\n entity_id: 'input_datetime.sdb_deshumide_last_on_at',\n datetime: now.stamp\n }\n },\n\n helper_deshumide_last_off_at: {\n execute: setDeshumideLastOffAt,\n domain: 'input_datetime',\n service: 'set_datetime',\n data: {\n entity_id: 'input_datetime.sdb_deshumide_last_off_at',\n datetime: now.stamp\n }\n },\n\n helper_radiator_last_on_at: {\n execute: setRadiatorLastOnAt,\n domain: 'input_datetime',\n service: 'set_datetime',\n data: {\n entity_id: 'input_datetime.sdb_radiator_last_on_at',\n datetime: now.stamp\n }\n },\n\n helper_radiator_last_off_at: {\n execute: setRadiatorLastOffAt,\n domain: 'input_datetime',\n service: 'set_datetime',\n data: {\n entity_id: 'input_datetime.sdb_radiator_last_off_at',\n datetime: now.stamp\n }\n },\n\n logbook: {\n execute: signatureChanged,\n domain: 'logbook',\n service: 'log',\n data: {\n name: 'SDB Climate & Surplus',\n message: `${state} | master=${desiredMaster ? 'on' : 'off'} | deshumide=${desiredDeshumide ? 'on' : 'off'} | radiator=${desiredRadiator ? 'on' : 'off'} | ${shortReason}`,\n entity_id: 'switch.sdb_prise_socket_1'\n }\n },\n\n mobile_notification: {\n execute: notificationsEnabled && signatureChanged && notifyWanted,\n domain: cfg.mobileNotifyDomain,\n service: cfg.mobileNotifyService,\n data: {\n title: notifyTitle,\n message: notifyMessage,\n data: {\n push: {\n \"interruption-level\": \"time-sensitive\",\n sound: { name: \"default\" }\n },\n ttl: 0,\n priority: \"high\"\n }\n }\n }\n }\n};\n\nreturn [{ json: result }];\n"
|
|
},
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
448,
|
|
0
|
|
],
|
|
"id": "607b1441-a1af-4d0b-8e1a-40d001f36ed8",
|
|
"name": "Compute SDB Climate & Surplus V2.2"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const root = $input.first().json || {};\nconst calls = root.ha_service_calls || {};\nconst decision = root.decision || {};\nconst actions = [];\n\nfunction pushAction(action, call, enabled = true) {\n if (!enabled) return;\n if (!call) return;\n if (call.execute === false) return;\n\n const { execute, ...rest } = call;\n\n actions.push({\n json: {\n action,\n ...rest\n }\n });\n}\n\n// =====================================================\n// HELPERS TOUJOURS MIS A JOUR\n// =====================================================\npushAction('helper_state', calls.helper_state, true);\npushAction('helper_reason', calls.helper_reason, true);\n\n// =====================================================\n// OFF ENFANTS D'ABORD\n// =====================================================\npushAction(\n 'deshumide',\n calls.deshumide,\n decision.deshumide_action === 'turn_off'\n);\n\npushAction(\n 'radiator',\n calls.radiator,\n decision.radiator_action === 'turn_off'\n);\n\n// =====================================================\n// PUIS OFF MASTER SI NECESSAIRE\n// =====================================================\npushAction(\n 'master_power',\n calls.master_power,\n decision.master_action === 'turn_off'\n);\n\n// =====================================================\n// ON MASTER D'ABORD\n// =====================================================\npushAction(\n 'master_power',\n calls.master_power,\n decision.master_action === 'turn_on'\n);\n\n// =====================================================\n// PUIS ON ENFANTS\n// =====================================================\npushAction(\n 'deshumide',\n calls.deshumide,\n decision.deshumide_action === 'turn_on'\n);\n\npushAction(\n 'radiator',\n calls.radiator,\n decision.radiator_action === 'turn_on'\n);\n\n// =====================================================\n// LATCHES\n// =====================================================\npushAction(\n 'helper_master_latch',\n calls.helper_master_latch,\n decision.master_latch_action !== 'none'\n);\n\npushAction(\n 'helper_deshumide_latch',\n calls.helper_deshumide_latch,\n decision.deshumide_latch_action !== 'none'\n);\n\npushAction(\n 'helper_radiator_latch',\n calls.helper_radiator_latch,\n decision.radiator_latch_action !== 'none'\n);\n\n// =====================================================\n// TIMESTAMPS\n// =====================================================\npushAction(\n 'helper_master_last_on_at',\n calls.helper_master_last_on_at,\n !!decision.set_master_last_on_at\n);\n\npushAction(\n 'helper_master_last_off_at',\n calls.helper_master_last_off_at,\n !!decision.set_master_last_off_at\n);\n\npushAction(\n 'helper_deshumide_last_on_at',\n calls.helper_deshumide_last_on_at,\n !!decision.set_deshumide_last_on_at\n);\n\npushAction(\n 'helper_deshumide_last_off_at',\n calls.helper_deshumide_last_off_at,\n !!decision.set_deshumide_last_off_at\n);\n\npushAction(\n 'helper_radiator_last_on_at',\n calls.helper_radiator_last_on_at,\n !!decision.set_radiator_last_on_at\n);\n\npushAction(\n 'helper_radiator_last_off_at',\n calls.helper_radiator_last_off_at,\n !!decision.set_radiator_last_off_at\n);\n\n// =====================================================\n// SIGNATURE + LOGBOOK SI CHANGEMENT\n// =====================================================\npushAction(\n 'helper_signature',\n calls.helper_signature,\n !!decision.signature_changed\n);\n\npushAction(\n 'logbook',\n calls.logbook,\n !!decision.signature_changed\n);\n\n// =====================================================\n// NOTIFICATION\n// =====================================================\npushAction(\n 'mobile_notification',\n calls.mobile_notification,\n !!decision.should_notify\n);\n\nreturn actions;\n"
|
|
},
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
672,
|
|
0
|
|
],
|
|
"id": "02e0d133-7be4-41f3-a419-17e945e43b62",
|
|
"name": "Build SDB Climate & Surplus Actions V2.2"
|
|
}
|
|
],
|
|
"connections": {
|
|
"Schedule Trigger": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "HA Snapshot SDB Climate & Surplus",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"HA Snapshot SDB Climate & Surplus": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Compute SDB Climate & Surplus V2.2",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Compute SDB Climate & Surplus V2.2": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Build SDB Climate & Surplus Actions V2.2",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build SDB Climate & Surplus Actions V2.2": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Execute HA Service SDB Climate & Surplus",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
}
|
|
},
|
|
"settings": {
|
|
"executionOrder": "v1",
|
|
"binaryMode": "separate",
|
|
"availableInMCP": true,
|
|
"timeSavedMode": "fixed",
|
|
"callerPolicy": "workflowsFromSameOwner",
|
|
"saveDataErrorExecution": "all"
|
|
},
|
|
"staticData": {
|
|
"node:Schedule Trigger": {
|
|
"recurrenceRules": [
|
|
null
|
|
],
|
|
"recurrenceRuleSignatures": [
|
|
null
|
|
]
|
|
}
|
|
},
|
|
"meta": null,
|
|
"nodeGroups": [],
|
|
"pinData": {},
|
|
"versionId": "a50b531f-5399-463a-9943-e1a30a0cd2de",
|
|
"activeVersionId": "a50b531f-5399-463a-9943-e1a30a0cd2de",
|
|
"versionCounter": 76,
|
|
"triggerCount": 1,
|
|
"sourceWorkflowId": null,
|
|
"shared": [
|
|
{
|
|
"updatedAt": "2026-03-17T16:32:38.182Z",
|
|
"createdAt": "2026-03-17T16:32:38.182Z",
|
|
"role": "workflow:owner",
|
|
"workflowId": "2NhrfxuGm4pTUNs9",
|
|
"projectId": "1q2yd8IyTc8fb25V",
|
|
"project": {
|
|
"updatedAt": "2026-02-06T16:27:24.469Z",
|
|
"createdAt": "2026-02-06T16:23:40.444Z",
|
|
"id": "1q2yd8IyTc8fb25V",
|
|
"name": "thierry tallagrand <tallagrand.thierry@gmail.com>",
|
|
"type": "personal",
|
|
"icon": null,
|
|
"description": null,
|
|
"customTelemetryTags": [],
|
|
"creatorId": "8b9e9552-cd74-4863-82a3-174b3b3befa2"
|
|
}
|
|
}
|
|
],
|
|
"tags": [],
|
|
"activeVersion": {
|
|
"updatedAt": "2026-06-12T21:13:37.000Z",
|
|
"createdAt": "2026-04-14T19:35:18.391Z",
|
|
"versionId": "a50b531f-5399-463a-9943-e1a30a0cd2de",
|
|
"workflowId": "2NhrfxuGm4pTUNs9",
|
|
"nodes": [
|
|
{
|
|
"parameters": {
|
|
"rule": {
|
|
"interval": [
|
|
{
|
|
"field": "minutes",
|
|
"minutesInterval": 1
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"id": "1e5571d8-a01b-4249-8f06-0c694605975e",
|
|
"name": "Schedule Trigger",
|
|
"type": "n8n-nodes-base.scheduleTrigger",
|
|
"typeVersion": 1.2,
|
|
"position": [
|
|
0,
|
|
0
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=http://10.10.10.17:8123/api/template\n",
|
|
"authentication": "genericCredentialType",
|
|
"genericAuthType": "httpBearerAuth",
|
|
"sendHeaders": true,
|
|
"headerParameters": {
|
|
"parameters": [
|
|
{
|
|
"name": "Content-Type",
|
|
"value": "application/json"
|
|
},
|
|
{
|
|
"name": "Accept",
|
|
"value": "text/plain"
|
|
}
|
|
]
|
|
},
|
|
"sendBody": true,
|
|
"bodyParameters": {
|
|
"parameters": [
|
|
{
|
|
"name": "template",
|
|
"value": "{{ {\n \"soc_percent\": states(\"sensor.victron_battery_soc\"),\n \"battery_current_a\": states(\"sensor.victron_battery_current\"),\n \"pv_power_w\": states(\"sensor.victron_pv_power_system\"),\n \"pv_power_10m_avg_w\": states(\"sensor.victron_pv_power_10m_avg\"),\n \"house_ac_power_w\": states(\"sensor.victron_consumption_power_l1\"),\n \"grid_power_w\": states(\"sensor.victron_grid_power_l1\"),\n \"grid_state\": states(\"switch.edf_general_ok_switch\"),\n \"victron_acin1_available\": states(\"sensor.victron_ac_in1_available\"),\n\n \"sdb_master_power\": states(\"switch.sdb_prise_socket_1\"),\n \"sdb_master_energy_total\": states(\"sensor.sdb_prise_energie_totale\"),\n\n \"deshumide_switch\": states(\"switch.deshumide_sdb_socket_1\"),\n \"deshumide_energy_total\": states(\"sensor.deshumide_sdb_energie_totale\"),\n\n \"radiator_switch\": states(\"switch.sdb_radiateur_socket_1\"),\n \"radiator_energy_total\": states(\"sensor.sdb_radiateur_energie_totale\"),\n\n \"sdb_temperature_c\": states(\"sensor.sdb_temp_hygro_temperature\"),\n \"sdb_humidity_pct\": states(\"sensor.sdb_temp_hygro_humidite\"),\n\n \"auto_enabled\": states(\"input_boolean.sdb_climate_surplus_auto_enabled\"),\n \"notifications_enabled\": states(\"input_boolean.sdb_climate_surplus_notifications\"),\n \"manual_adopt_enabled\": states(\"input_boolean.sdb_master_manual_adopt_enabled\"),\n\n \"master_power_latch\": states(\"input_boolean.sdb_master_power_latch\"),\n \"deshumide_latch\": states(\"input_boolean.sdb_deshumide_latch\"),\n \"radiator_latch\": states(\"input_boolean.sdb_radiator_latch\"),\n\n \"temp_on_min_c\": states(\"input_number.sdb_temp_on_min_c\"),\n \"temp_off_max_c\": states(\"input_number.sdb_temp_off_max_c\"),\n \"humidity_on_pct\": states(\"input_number.sdb_humidity_on_pct\"),\n \"humidity_off_pct\": states(\"input_number.sdb_humidity_off_pct\"),\n\n \"deshumide_min_on_minutes\": states(\"input_number.sdb_deshumide_min_on_minutes\"),\n \"deshumide_min_off_minutes\": states(\"input_number.sdb_deshumide_min_off_minutes\"),\n\n \"radiator_auto_enabled\": states(\"input_boolean.sdb_radiator_surplus_auto_enabled\"),\n \"radiator_soc_on\": states(\"input_number.sdb_radiator_soc_on\"),\n \"radiator_soc_off\": states(\"input_number.sdb_radiator_soc_off\"),\n \"radiator_pv_on_w\": states(\"input_number.sdb_radiator_pv_on_w\"),\n \"radiator_pv_off_w\": states(\"input_number.sdb_radiator_pv_off_w\"),\n \"radiator_battery_charge_ceiling_a\": states(\"input_number.sdb_radiator_battery_charge_ceiling_a\"),\n \"radiator_house_ac_max_w\": states(\"input_number.sdb_radiator_house_ac_max_w\"),\n \"radiator_min_on_minutes\": states(\"input_number.sdb_radiator_min_on_minutes\"),\n \"radiator_min_off_minutes\": states(\"input_number.sdb_radiator_min_off_minutes\"),\n\n \"day_start_time\": states(\"input_datetime.victron_flexible_day_start_time\"),\n \"day_stop_time\": states(\"input_datetime.victron_flexible_day_stop_time\"),\n\n \"master_last_on_at\": states(\"input_datetime.sdb_master_last_on_at\"),\n \"master_last_off_at\": states(\"input_datetime.sdb_master_last_off_at\"),\n \"deshumide_last_on_at\": states(\"input_datetime.sdb_deshumide_last_on_at\"),\n \"deshumide_last_off_at\": states(\"input_datetime.sdb_deshumide_last_off_at\"),\n \"radiator_last_on_at\": states(\"input_datetime.sdb_radiator_last_on_at\"),\n \"radiator_last_off_at\": states(\"input_datetime.sdb_radiator_last_off_at\"),\n\n \"evcs_priority_active\": states(\"input_boolean.victron_evcs_priority_active\"),\n \"energy_priority_mode\": states(\"input_select.victron_energy_priority_mode\"),\n \"energy_priority_reason\": states(\"input_text.victron_energy_priority_reason\"),\n \"evcs_priority_until\": states(\"input_datetime.victron_evcs_priority_until\"),\n\n \"last_signature\": states(\"input_text.sdb_climate_surplus_last_signature\")\n} | tojson }}\n"
|
|
}
|
|
]
|
|
},
|
|
"options": {}
|
|
},
|
|
"id": "74f4b11c-bdb6-41db-9223-f1031cb8a12b",
|
|
"name": "HA Snapshot SDB Climate & Surplus",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
224,
|
|
0
|
|
],
|
|
"credentials": {
|
|
"httpBearerAuth": {
|
|
"id": "uv3sknYYigxBkaVN",
|
|
"name": "Bearer Auth account 2"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "={{ 'http://10.10.10.17:8123/api/services/' + $json.domain + '/' + $json.service }}\n",
|
|
"authentication": "genericCredentialType",
|
|
"genericAuthType": "httpBearerAuth",
|
|
"sendHeaders": true,
|
|
"headerParameters": {
|
|
"parameters": [
|
|
{
|
|
"name": "Content-Type",
|
|
"value": "application/json"
|
|
},
|
|
{
|
|
"name": "accept",
|
|
"value": "application/json"
|
|
}
|
|
]
|
|
},
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ $json.data }}",
|
|
"options": {}
|
|
},
|
|
"id": "761fbc47-54ca-4961-863e-634b52e764d3",
|
|
"name": "Execute HA Service SDB Climate & Surplus",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
896,
|
|
0
|
|
],
|
|
"alwaysOutputData": true,
|
|
"credentials": {
|
|
"httpBearerAuth": {
|
|
"id": "uv3sknYYigxBkaVN",
|
|
"name": "Bearer Auth account 2"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "function num(v, fallback = NaN) {\n const n = Number.parseFloat(v);\n return Number.isFinite(n) ? n : fallback;\n}\n\nfunction low(v) {\n return (v ?? '').toString().trim().toLowerCase();\n}\n\nfunction parseMaybeJson(v) {\n if (typeof v !== 'string') return v;\n try {\n return JSON.parse(v);\n } catch {\n return v;\n }\n}\n\nfunction extractSnapshot(input) {\n if (typeof input === 'string') return parseMaybeJson(input);\n if (input?.data && typeof input.data === 'string') return parseMaybeJson(input.data);\n if (input?.body && typeof input.body === 'string') return parseMaybeJson(input.body);\n if (input?.data && typeof input.data === 'object') return input.data;\n if (input?.body && typeof input.body === 'object') return input.body;\n return input;\n}\n\nfunction round1(v) {\n return Math.round(v * 10) / 10;\n}\n\nfunction round2(v) {\n return Math.round(v * 100) / 100;\n}\n\nfunction clamp(v, min, max) {\n return Math.min(Math.max(v, min), max);\n}\n\nfunction parseTimeToMinutes(v, fallback = null) {\n const s = (v ?? '').toString().trim();\n const m = s.match(/^(\\d{1,2}):(\\d{2})(?::(\\d{2}))?$/);\n if (!m) return fallback;\n const hh = Number(m[1]);\n const mm = Number(m[2]);\n if (!Number.isFinite(hh) || !Number.isFinite(mm)) return fallback;\n return hh * 60 + mm;\n}\n\nfunction to2(n) {\n return String(n).padStart(2, '0');\n}\n\nfunction getParisNowParts() {\n const fmt = new Intl.DateTimeFormat('fr-FR', {\n timeZone: 'Europe/Paris',\n year: 'numeric',\n month: '2-digit',\n day: '2-digit',\n hour: '2-digit',\n minute: '2-digit',\n second: '2-digit',\n hour12: false,\n });\n\n const parts = Object.fromEntries(\n fmt.formatToParts(new Date())\n .filter(p => p.type !== 'literal')\n .map(p => [p.type, p.value])\n );\n\n const hour = Number(parts.hour) === 24 ? 0 : Number(parts.hour);\n const date = `${parts.year}-${parts.month}-${parts.day}`;\n const stamp = `${date} ${to2(hour)}:${parts.minute}:${parts.second}`;\n\n return {\n year: Number(parts.year),\n month: Number(parts.month),\n day: Number(parts.day),\n hour,\n minute: Number(parts.minute),\n second: Number(parts.second),\n date,\n stamp,\n minNow: hour * 60 + Number(parts.minute),\n };\n}\n\nfunction getTzOffsetMs(utcTs, timeZone = 'Europe/Paris') {\n const dtf = new Intl.DateTimeFormat('en-US', {\n timeZone,\n year: 'numeric',\n month: '2-digit',\n day: '2-digit',\n hour: '2-digit',\n minute: '2-digit',\n second: '2-digit',\n hour12: false,\n });\n\n const parts = Object.fromEntries(\n dtf.formatToParts(new Date(utcTs))\n .filter(p => p.type !== 'literal')\n .map(p => [p.type, p.value])\n );\n\n const asIfUtc = Date.UTC(\n Number(parts.year),\n Number(parts.month) - 1,\n Number(parts.day),\n Number(parts.hour) === 24 ? 0 : Number(parts.hour),\n Number(parts.minute),\n Number(parts.second)\n );\n\n const d = new Date(utcTs);\n const actualUtc = Date.UTC(\n d.getUTCFullYear(),\n d.getUTCMonth(),\n d.getUTCDate(),\n d.getUTCHours(),\n d.getUTCMinutes(),\n d.getUTCSeconds()\n );\n\n return asIfUtc - actualUtc;\n}\n\nfunction parseParisStampToMs(s) {\n const text = (s ?? '').toString().trim();\n const m = text.match(/^(\\d{4})-(\\d{2})-(\\d{2}) (\\d{2}):(\\d{2}):(\\d{2})$/);\n if (!m) return null;\n\n const y = Number(m[1]);\n const mo = Number(m[2]);\n const d = Number(m[3]);\n const h = Number(m[4]);\n const mi = Number(m[5]);\n const se = Number(m[6]);\n\n if (![y, mo, d, h, mi, se].every(Number.isFinite)) return null;\n\n const guessUtc = Date.UTC(y, mo - 1, d, h, mi, se);\n const off1 = getTzOffsetMs(guessUtc, 'Europe/Paris');\n let ts = guessUtc - off1;\n const off2 = getTzOffsetMs(ts, 'Europe/Paris');\n\n if (off2 !== off1) ts = guessUtc - off2;\n return ts;\n}\n\nfunction cleanStateText(v) {\n const s = (v ?? '').toString().trim();\n const x = s.toLowerCase();\n return ['unknown', 'unavailable', 'none', 'null'].includes(x) ? '' : s;\n}\n\nfunction cleanParisStamp(v) {\n const s = cleanStateText(v);\n return parseParisStampToMs(s) !== null ? s : '';\n}\n\nfunction minutesSince(ts, nowMs) {\n if (ts === null || nowMs === null) return null;\n return Math.max(0, (nowMs - ts) / 60000);\n}\n\nfunction safeText(v, max = 255) {\n return (v ?? '').toString().slice(0, max);\n}\n\nconst cfg = {\n mobileNotifyDomain: 'notify',\n mobileNotifyService: 'mobile_app_iphone_16_de_tt',\n\n radiatorRealPowerW: 960,\n batteryNominalVoltageV: 52,\n\n radiatorStartSurplusRatio: 0.60,\n radiatorHoldSurplusRatio: 0.25,\n radiatorMinBatteryStartA: 2,\n radiatorMinBatteryHoldA: 0,\n radiatorMaxBatteryHoldDischargeA: -1.5\n};\n\nconst raw = $input.first().json;\nconst input = extractSnapshot(raw);\nconst now = getParisNowParts();\nconst nowMs = parseParisStampToMs(now.stamp);\n\n// ======================================================\n// INPUTS\n// ======================================================\nconst soc = clamp(num(input.soc_percent, 0), 0, 100);\nconst batteryCurrentA = num(input.battery_current_a, 0);\nconst pvPowerW = Math.max(0, num(input.pv_power_w, 0));\nconst pvPowerAvg10mW = Math.max(0, num(input.pv_power_10m_avg_w, pvPowerW));\nconst houseAcPowerW = Math.max(0, num(input.house_ac_power_w, 0));\nconst gridPowerW = num(input.grid_power_w, 0);\nconst gridIsOn = low(input.grid_state || 'off') === 'on';\nconst acInAvailable = num(input.victron_acin1_available, 0) === 1;\nconst gridAvailable = gridIsOn || acInAvailable;\n\nconst masterIsOn = low(input.sdb_master_power || 'off') === 'on';\nconst deshumideIsOn = low(input.deshumide_switch || 'off') === 'on';\nconst radiatorIsOn = low(input.radiator_switch || 'off') === 'on';\n\nconst masterEnergyTotalKwh = Math.max(0, num(input.sdb_master_energy_total, 0));\nconst deshumideEnergyTotalKwh = Math.max(0, num(input.deshumide_energy_total, 0));\nconst radiatorEnergyTotalKwh = Math.max(0, num(input.radiator_energy_total, 0));\n\nconst temperatureC = num(input.sdb_temperature_c, NaN);\nconst humidityPct = num(input.sdb_humidity_pct, NaN);\n\nconst autoEnabled = low(input.auto_enabled || 'off') === 'on';\nconst notificationsEnabled = low(input.notifications_enabled || 'off') === 'on';\nconst manualAdoptEnabled = low(input.manual_adopt_enabled || 'off') === 'on';\n\nconst masterLatchIsOn = low(input.master_power_latch || 'off') === 'on';\nconst deshumideLatchIsOn = low(input.deshumide_latch || 'off') === 'on';\nconst radiatorLatchIsOn = low(input.radiator_latch || 'off') === 'on';\n\nconst tempOnMinC = num(input.temp_on_min_c, 21);\nconst tempOffMaxC = num(input.temp_off_max_c, 23);\nconst humidityOnPct = num(input.humidity_on_pct, 55);\nconst humidityOffPct = num(input.humidity_off_pct, 45);\n\nconst deshumideMinOnMinutes = Math.max(0, num(input.deshumide_min_on_minutes, 15));\nconst deshumideMinOffMinutes = Math.max(0, num(input.deshumide_min_off_minutes, 10));\n\nconst radiatorAutoEnabled = low(input.radiator_auto_enabled || 'off') === 'on';\nconst radiatorSocOn = clamp(num(input.radiator_soc_on, 90), 0, 100);\nconst radiatorSocOff = clamp(num(input.radiator_soc_off, 88), 0, 100);\nconst radiatorPvOnW = Math.max(0, num(input.radiator_pv_on_w, 2500));\nconst radiatorPvOffW = Math.max(0, num(input.radiator_pv_off_w, 1200));\nconst radiatorBatteryChargeCeilingA = Math.max(0, num(input.radiator_battery_charge_ceiling_a, 5));\nconst radiatorHouseAcMaxW = Math.max(0, num(input.radiator_house_ac_max_w, 6500));\nconst radiatorMinOnMinutes = Math.max(0, num(input.radiator_min_on_minutes, 15));\nconst radiatorMinOffMinutes = Math.max(0, num(input.radiator_min_off_minutes, 10));\n\nconst dayStartMin = parseTimeToMinutes(input.day_start_time, 9 * 60);\nconst dayStopMin = parseTimeToMinutes(input.day_stop_time, 19 * 60 + 15);\nconst inDayWindow =\n dayStartMin < dayStopMin\n ? (now.minNow >= dayStartMin && now.minNow < dayStopMin)\n : (now.minNow >= dayStartMin || now.minNow < dayStopMin);\n\nconst masterLastOnAt = cleanParisStamp(input.master_last_on_at);\nconst masterLastOffAt = cleanParisStamp(input.master_last_off_at);\nconst deshumideLastOnAt = cleanParisStamp(input.deshumide_last_on_at);\nconst deshumideLastOffAt = cleanParisStamp(input.deshumide_last_off_at);\nconst radiatorLastOnAt = cleanParisStamp(input.radiator_last_on_at);\nconst radiatorLastOffAt = cleanParisStamp(input.radiator_last_off_at);\nconst lastSignature = cleanStateText(input.last_signature);\n\n// ======================================================\n// EVCS PRIORITY\n// ======================================================\nconst evcsPriorityActiveRaw = low(input.evcs_priority_active || 'off') === 'on';\nconst energyPriorityMode = cleanStateText(input.energy_priority_mode || 'normal').toLowerCase() || 'normal';\nconst energyPriorityReason = cleanStateText(input.energy_priority_reason);\nconst evcsPriorityUntilAt = cleanParisStamp(input.evcs_priority_until);\nconst evcsPriorityUntilMs = parseParisStampToMs(evcsPriorityUntilAt);\n\nconst evcsPriorityActive =\n evcsPriorityActiveRaw &&\n (\n evcsPriorityUntilMs === null ||\n (nowMs !== null && evcsPriorityUntilMs > nowMs + 1000)\n );\n\n// ======================================================\n// TIMESTAMPS\n// ======================================================\nconst masterLastOnMs = parseParisStampToMs(masterLastOnAt);\nconst masterLastOffMs = parseParisStampToMs(masterLastOffAt);\nconst deshumideLastOnMs = parseParisStampToMs(deshumideLastOnAt);\nconst deshumideLastOffMs = parseParisStampToMs(deshumideLastOffAt);\nconst radiatorLastOnMs = parseParisStampToMs(radiatorLastOnAt);\nconst radiatorLastOffMs = parseParisStampToMs(radiatorLastOffAt);\n\nconst minutesSinceMasterOn = minutesSince(masterLastOnMs, nowMs);\nconst minutesSinceMasterOff = minutesSince(masterLastOffMs, nowMs);\nconst minutesSinceDeshumideOn = minutesSince(deshumideLastOnMs, nowMs);\nconst minutesSinceDeshumideOff = minutesSince(deshumideLastOffMs, nowMs);\nconst minutesSinceRadiatorOn = minutesSince(radiatorLastOnMs, nowMs);\nconst minutesSinceRadiatorOff = minutesSince(radiatorLastOffMs, nowMs);\n\nconst deshumideMinOnElapsed = minutesSinceDeshumideOn === null ? true : minutesSinceDeshumideOn >= deshumideMinOnMinutes;\nconst deshumideMinOffElapsed = minutesSinceDeshumideOff === null ? true : minutesSinceDeshumideOff >= deshumideMinOffMinutes;\nconst radiatorMinOnElapsed = minutesSinceRadiatorOn === null ? true : minutesSinceRadiatorOn >= radiatorMinOnMinutes;\nconst radiatorMinOffElapsed = minutesSinceRadiatorOff === null ? true : minutesSinceRadiatorOff >= radiatorMinOffMinutes;\n\n// ======================================================\n// BESOINS METIER SEPARES\n// ======================================================\nconst heatNeed = Number.isFinite(temperatureC) ? temperatureC < tempOnMinC : false;\nconst heatRecovered = Number.isFinite(temperatureC) ? temperatureC >= tempOffMaxC : false;\n\nconst humidityNeed = Number.isFinite(humidityPct) ? humidityPct > humidityOnPct : false;\nconst humidityRecovered = Number.isFinite(humidityPct) ? humidityPct <= humidityOffPct : false;\n\nconst comfortDemand = heatNeed || humidityNeed;\nconst comfortRecovered = heatRecovered && humidityRecovered;\n\n// ======================================================\n// RADIATEUR SDB : SURPLUS REEL V2\n// ======================================================\nconst estimatedBatteryChargeW =\n Number.isFinite(batteryCurrentA) && batteryCurrentA > 0\n ? batteryCurrentA * cfg.batteryNominalVoltageV\n : 0;\n\nconst directPvHeadroomW = Math.max(0, pvPowerAvg10mW - houseAcPowerW);\nconst estimatedSurplusW = Math.max(0, directPvHeadroomW + estimatedBatteryChargeW);\n\nconst radiatorStartSurplusNeededW = Math.round(cfg.radiatorRealPowerW * cfg.radiatorStartSurplusRatio);\nconst radiatorHoldSurplusNeededW = Math.round(cfg.radiatorRealPowerW * cfg.radiatorHoldSurplusRatio);\n\nconst radiatorBatteryStartOk =\n Number.isFinite(batteryCurrentA) &&\n batteryCurrentA >= cfg.radiatorMinBatteryStartA &&\n batteryCurrentA <= radiatorBatteryChargeCeilingA;\n\nconst radiatorBatteryHoldOk =\n Number.isFinite(batteryCurrentA) &&\n batteryCurrentA >= cfg.radiatorMaxBatteryHoldDischargeA &&\n batteryCurrentA <= radiatorBatteryChargeCeilingA + 4;\n\nconst radiatorStartByAbsolutePv =\n pvPowerAvg10mW >= radiatorPvOnW && radiatorBatteryStartOk;\n\nconst radiatorStartByEstimatedSurplus =\n estimatedSurplusW >= radiatorStartSurplusNeededW && radiatorBatteryStartOk;\n\nconst radiatorHoldByAbsolutePv =\n pvPowerAvg10mW >= radiatorPvOffW && radiatorBatteryHoldOk;\n\nconst radiatorHoldByEstimatedSurplus =\n estimatedSurplusW >= radiatorHoldSurplusNeededW && radiatorBatteryHoldOk;\n\nconst radiatorStartConditionsOk =\n radiatorAutoEnabled &&\n heatNeed &&\n inDayWindow &&\n !gridAvailable &&\n !evcsPriorityActive &&\n soc >= radiatorSocOn &&\n houseAcPowerW < radiatorHouseAcMaxW &&\n (radiatorStartByAbsolutePv || radiatorStartByEstimatedSurplus);\n\nconst radiatorHoldConditionsOk =\n radiatorAutoEnabled &&\n !heatRecovered &&\n inDayWindow &&\n !gridAvailable &&\n !evcsPriorityActive &&\n soc >= radiatorSocOff &&\n houseAcPowerW < radiatorHouseAcMaxW &&\n (radiatorHoldByAbsolutePv || radiatorHoldByEstimatedSurplus);\n\n// ======================================================\n// SORTIES DESIREES\n// ======================================================\nlet desiredDeshumide = false;\nlet desiredRadiator = false;\nlet desiredMaster = false;\n\nlet state = 'idle';\nlet reason = [];\n\nlet dehumActionReason = 'idle';\nlet radiatorActionReason = 'idle';\n\nlet adoptMaster = false;\nlet adoptDeshumide = false;\nlet adoptRadiator = false;\n\nlet notifyTitle = 'SDB Climate & Surplus';\nlet notifyMessage = '';\nlet notifyWanted = false;\n\n// ======================================================\n// DESHUMIDIFICATEUR - PRIORITAIRE HUMIDITE\n// ======================================================\nif (!autoEnabled) {\n desiredDeshumide = deshumideIsOn;\n dehumActionReason = 'automation_disabled';\n}\nelse if (humidityNeed) {\n if (deshumideIsOn) {\n desiredDeshumide = true;\n dehumActionReason = 'deshumide_already_running';\n } else if (deshumideMinOffElapsed) {\n desiredDeshumide = true;\n dehumActionReason = 'deshumide_start_on_humidity_need';\n } else {\n desiredDeshumide = false;\n dehumActionReason = 'deshumide_wait_min_off';\n }\n}\nelse if (deshumideIsOn || deshumideLatchIsOn) {\n if (!humidityRecovered) {\n desiredDeshumide = true;\n dehumActionReason = 'deshumide_hold_until_humidity_recovered';\n } else if (!deshumideMinOnElapsed) {\n desiredDeshumide = true;\n dehumActionReason = 'deshumide_hold_min_on';\n } else {\n desiredDeshumide = false;\n dehumActionReason = 'deshumide_stop_humidity_recovered';\n }\n}\nelse {\n desiredDeshumide = false;\n dehumActionReason = 'deshumide_idle';\n}\n\n// Adoption manuelle déshumide uniquement si cohérent avec la demande\nif (manualAdoptEnabled && desiredDeshumide && deshumideIsOn && !deshumideLatchIsOn) {\n adoptDeshumide = true;\n}\n\n// ======================================================\n// RADIATEUR - CHAUFFAGE + SURPLUS + PRIORITE EVCS\n// ======================================================\nif (!autoEnabled) {\n desiredRadiator = radiatorIsOn;\n radiatorActionReason = 'automation_disabled';\n}\nelse if (!radiatorAutoEnabled) {\n desiredRadiator = radiatorIsOn;\n radiatorActionReason = 'radiator_auto_disabled_observe_only';\n}\nelse if (evcsPriorityActive) {\n if (radiatorIsOn || radiatorLatchIsOn) {\n if (!radiatorMinOnElapsed) {\n desiredRadiator = true;\n radiatorActionReason = 'radiator_hold_min_on_evcs_priority';\n } else {\n desiredRadiator = false;\n radiatorActionReason = 'radiator_blocked_by_evcs_priority';\n }\n } else {\n desiredRadiator = false;\n radiatorActionReason = 'radiator_blocked_by_evcs_priority';\n }\n}\nelse if (heatNeed && radiatorStartConditionsOk) {\n if (radiatorIsOn) {\n desiredRadiator = true;\n radiatorActionReason = 'radiator_already_running';\n } else if (radiatorMinOffElapsed) {\n desiredRadiator = true;\n radiatorActionReason = 'radiator_start_heat_need_surplus_v2';\n } else {\n desiredRadiator = false;\n radiatorActionReason = 'radiator_wait_min_off';\n }\n}\nelse if (radiatorIsOn || radiatorLatchIsOn) {\n if (radiatorHoldConditionsOk) {\n desiredRadiator = true;\n radiatorActionReason = 'radiator_hold_heat_need_surplus_v2';\n } else if (!radiatorMinOnElapsed) {\n desiredRadiator = true;\n radiatorActionReason = 'radiator_hold_min_on';\n } else if (evcsPriorityActive) {\n desiredRadiator = false;\n radiatorActionReason = 'radiator_stop_evcs_priority';\n } else if (heatRecovered) {\n desiredRadiator = false;\n radiatorActionReason = 'radiator_stop_heat_recovered';\n } else {\n desiredRadiator = false;\n radiatorActionReason = 'radiator_stop_surplus_lost';\n }\n}\nelse {\n desiredRadiator = false;\n radiatorActionReason = heatNeed ? 'radiator_heat_need_but_energy_not_ok' : 'radiator_idle';\n}\n\n// Adoption manuelle radiateur uniquement si cohérent et hors EVCS\nif (\n manualAdoptEnabled &&\n desiredRadiator &&\n radiatorIsOn &&\n !radiatorLatchIsOn &&\n !evcsPriorityActive\n) {\n adoptRadiator = true;\n}\n\n// ======================================================\n// MASTER SDB PRISE = COORDINATEUR\n// ======================================================\nif (!autoEnabled) {\n desiredMaster = masterIsOn;\n} else {\n desiredMaster = desiredDeshumide || desiredRadiator;\n}\n\nif (manualAdoptEnabled && desiredMaster && masterIsOn && !masterLatchIsOn) {\n adoptMaster = true;\n}\n\n// ======================================================\n// ETAT GLOBAL\n// ======================================================\nif (!autoEnabled) {\n state = 'automation_disabled';\n reason.push(\"Automatisation SDB climate/surplus désactivée\");\n}\nelse if (desiredDeshumide && desiredRadiator) {\n state = 'deshumide_and_radiator_on';\n reason.push(\"Déshumidification active + radiateur actif sur surplus\");\n}\nelse if (desiredDeshumide) {\n state = 'deshumide_on';\n reason.push(\"Déshumidification SDB active\");\n}\nelse if (desiredRadiator) {\n state = 'radiator_on';\n reason.push(\"Radiateur SDB actif sur besoin thermique et surplus\");\n}\nelse if (radiatorActionReason === 'radiator_blocked_by_evcs_priority' || radiatorActionReason === 'radiator_stop_evcs_priority') {\n state = 'radiator_blocked_by_evcs_priority';\n reason.push(\"Radiateur SDB bloqué par priorité EVCS\");\n}\nelse if (dehumActionReason === 'deshumide_wait_min_off') {\n state = 'wait_deshumide_min_off';\n reason.push(\"Déshumidificateur en attente de temps mini OFF\");\n}\nelse if (radiatorActionReason === 'radiator_wait_min_off') {\n state = 'wait_radiator_min_off';\n reason.push(\"Radiateur en attente de temps mini OFF\");\n}\nelse {\n state = 'idle';\n reason.push(\"Aucune demande SDB active\");\n}\n\nreason.push(`Température SDB ${Number.isFinite(temperatureC) ? round1(temperatureC) + '°C' : 'n/a'}`);\nreason.push(`Humidité SDB ${Number.isFinite(humidityPct) ? round1(humidityPct) + '%' : 'n/a'}`);\n\nreason.push(`Chauffage ON si T<${round1(tempOnMinC)}°C`);\nreason.push(`Chauffage OFF si T>=${round1(tempOffMaxC)}°C`);\nreason.push(`Déshumidification ON si H>${round1(humidityOnPct)}%`);\nreason.push(`Déshumidification OFF si H<=${round1(humidityOffPct)}%`);\n\nreason.push(`Besoin chauffage=${heatNeed ? 'oui' : 'non'} | Rétabli chauffage=${heatRecovered ? 'oui' : 'non'}`);\nreason.push(`Besoin humidité=${humidityNeed ? 'oui' : 'non'} | Humidité rétablie=${humidityRecovered ? 'oui' : 'non'}`);\n\nreason.push(`Déshumide: ${dehumActionReason}`);\nreason.push(`Radiateur: ${radiatorActionReason}`);\n\nreason.push(`Master=${masterIsOn ? 'ON' : 'OFF'} | Deshumide=${deshumideIsOn ? 'ON' : 'OFF'} | Radiateur=${radiatorIsOn ? 'ON' : 'OFF'}`);\nreason.push(`SOC ${round1(soc)}% | PV ${Math.round(pvPowerW)}W | PV10 ${Math.round(pvPowerAvg10mW)}W | AC maison ${Math.round(houseAcPowerW)}W | Batterie ${round1(batteryCurrentA)}A`);\nreason.push(`Réseau cmd=${gridIsOn ? 'ON' : 'OFF'} | acin=${acInAvailable ? 'OK' : 'KO'} | dispo=${gridAvailable ? 'oui' : 'non'}`);\nreason.push(`Fenêtre jour ${inDayWindow ? 'OK' : 'Hors fenêtre'}`);\nreason.push(`Prio EVCS ${evcsPriorityActive ? 'ON' : 'OFF'}${energyPriorityReason ? ` | ${energyPriorityReason}` : ''}`);\n\nreason.push(`Radiateur ${Math.round(cfg.radiatorRealPowerW)}W | Headroom PV ${Math.round(directPvHeadroomW)}W | Charge batt estimée ${Math.round(estimatedBatteryChargeW)}W | Surplus estimé ${Math.round(estimatedSurplusW)}W`);\nreason.push(`Start radiateur si surplus >= ${Math.round(radiatorStartSurplusNeededW)}W ou PV10 >= ${Math.round(radiatorPvOnW)}W`);\nreason.push(`Hold radiateur si surplus >= ${Math.round(radiatorHoldSurplusNeededW)}W ou PV10 >= ${Math.round(radiatorPvOffW)}W`);\n\nif (adoptMaster) reason.push(\"Prise maître SDB adoptée\");\nif (adoptDeshumide) reason.push(\"Déshumidificateur adopté\");\nif (adoptRadiator) reason.push(\"Radiateur adopté\");\n\n// ======================================================\n// ACTIONS / LATCHES\n// ======================================================\nconst desiredMasterLatch = desiredMaster ? 'on' : 'off';\nconst desiredDeshumideLatch = desiredDeshumide ? 'on' : 'off';\nconst desiredRadiatorLatch = desiredRadiator ? 'on' : 'off';\n\nconst masterAction =\n desiredMaster === masterIsOn\n ? 'none'\n : desiredMaster ? 'turn_on' : 'turn_off';\n\nconst deshumideAction =\n desiredDeshumide === deshumideIsOn\n ? 'none'\n : desiredDeshumide ? 'turn_on' : 'turn_off';\n\nconst radiatorAction =\n desiredRadiator === radiatorIsOn\n ? 'none'\n : desiredRadiator ? 'turn_on' : 'turn_off';\n\nconst masterLatchAction =\n desiredMasterLatch === (masterLatchIsOn ? 'on' : 'off')\n ? 'none'\n : desiredMasterLatch === 'on' ? 'turn_on' : 'turn_off';\n\nconst deshumideLatchAction =\n desiredDeshumideLatch === (deshumideLatchIsOn ? 'on' : 'off')\n ? 'none'\n : desiredDeshumideLatch === 'on' ? 'turn_on' : 'turn_off';\n\nconst radiatorLatchAction =\n desiredRadiatorLatch === (radiatorLatchIsOn ? 'on' : 'off')\n ? 'none'\n : desiredRadiatorLatch === 'on' ? 'turn_on' : 'turn_off';\n\n// ======================================================\n// TIMESTAMPS\n// ======================================================\nconst setMasterLastOnAt = masterAction === 'turn_on' || adoptMaster;\nconst setMasterLastOffAt = masterAction === 'turn_off';\n\nconst setDeshumideLastOnAt = deshumideAction === 'turn_on' || adoptDeshumide;\nconst setDeshumideLastOffAt = deshumideAction === 'turn_off';\n\nconst setRadiatorLastOnAt = radiatorAction === 'turn_on' || adoptRadiator;\nconst setRadiatorLastOffAt = radiatorAction === 'turn_off';\n\n// ======================================================\n// NOTIFICATIONS\n// ======================================================\nif (state === 'deshumide_on' && (deshumideAction === 'turn_on' || adoptDeshumide)) {\n notifyTitle = '💧 Déshumidification SDB active';\n notifyMessage = `SDB: T=${Number.isFinite(temperatureC) ? round1(temperatureC) : 'n/a'}°C | H=${Number.isFinite(humidityPct) ? round1(humidityPct) : 'n/a'}%.`;\n notifyWanted = true;\n}\nelse if (dehumActionReason === 'deshumide_stop_humidity_recovered' && deshumideAction === 'turn_off') {\n notifyTitle = '✅ Déshumidification SDB arrêtée';\n notifyMessage = `Humidité revenue dans la zone cible : H=${Number.isFinite(humidityPct) ? round1(humidityPct) : 'n/a'}%.`;\n notifyWanted = true;\n}\nelse if (state === 'radiator_on' && radiatorAction === 'turn_on') {\n notifyTitle = '☀️ Radiateur SDB actif sur surplus';\n notifyMessage = `T=${Number.isFinite(temperatureC) ? round1(temperatureC) : 'n/a'}°C | SOC ${round1(soc)}% | PV10 ${Math.round(pvPowerAvg10mW)}W | Surplus ${Math.round(estimatedSurplusW)}W.`;\n notifyWanted = true;\n}\nelse if (\n (radiatorActionReason === 'radiator_stop_surplus_lost' || radiatorActionReason === 'radiator_stop_heat_recovered') &&\n radiatorAction === 'turn_off'\n) {\n notifyTitle = '⛔ Radiateur SDB arrêté';\n notifyMessage = `T=${Number.isFinite(temperatureC) ? round1(temperatureC) : 'n/a'}°C | PV10 ${Math.round(pvPowerAvg10mW)}W | Surplus ${Math.round(estimatedSurplusW)}W.`;\n notifyWanted = true;\n}\nelse if (\n (radiatorActionReason === 'radiator_blocked_by_evcs_priority' || radiatorActionReason === 'radiator_stop_evcs_priority') &&\n radiatorAction === 'turn_off'\n) {\n notifyTitle = '🚗 Priorité EVCS : radiateur SDB coupé';\n notifyMessage = `${energyPriorityReason || 'La recharge EVCS est prioritaire'}${evcsPriorityUntilAt ? ` jusqu'à ${evcsPriorityUntilAt}` : ''}.`;\n notifyWanted = true;\n}\nelse if (state === 'deshumide_and_radiator_on' && (deshumideAction === 'turn_on' || radiatorAction === 'turn_on')) {\n notifyTitle = '🌤️ SDB active : humidité + chauffage';\n notifyMessage = `Déshumidificateur et radiateur actifs. T=${Number.isFinite(temperatureC) ? round1(temperatureC) : 'n/a'}°C | H=${Number.isFinite(humidityPct) ? round1(humidityPct) : 'n/a'}% | SOC ${round1(soc)}%.`;\n notifyWanted = true;\n}\n\nconst shortReason = reason.join(' | ').slice(0, 255);\n\nconst signature = [\n now.date,\n state,\n `m=${desiredMaster ? '1' : '0'}`,\n `d=${desiredDeshumide ? '1' : '0'}`,\n `r=${desiredRadiator ? '1' : '0'}`,\n `heat=${heatNeed ? '1' : '0'}`,\n `hum=${humidityNeed ? '1' : '0'}`,\n `ev=${evcsPriorityActive ? '1' : '0'}`,\n `t=${Number.isFinite(temperatureC) ? round1(temperatureC) : 'na'}`,\n `h=${Number.isFinite(humidityPct) ? round1(humidityPct) : 'na'}`,\n `sx=${Math.round(estimatedSurplusW)}`\n].join('|');\n\nconst signatureChanged = signature !== lastSignature;\n\n// ======================================================\n// OUTPUT\n// ======================================================\nconst result = {\n timestamp_paris: now.stamp,\n\n inputs: {\n soc_percent: soc,\n battery_current_a: batteryCurrentA,\n pv_power_w: pvPowerW,\n pv_power_10m_avg_w: pvPowerAvg10mW,\n house_ac_power_w: houseAcPowerW,\n grid_power_w: gridPowerW,\n grid_state: gridIsOn ? 'on' : 'off',\n victron_acin1_available: acInAvailable ? 1 : 0,\n\n sdb_master_power: masterIsOn,\n sdb_master_energy_total: round2(masterEnergyTotalKwh),\n deshumide_switch: deshumideIsOn,\n deshumide_energy_total: round2(deshumideEnergyTotalKwh),\n radiator_switch: radiatorIsOn,\n radiator_energy_total: round2(radiatorEnergyTotalKwh),\n\n sdb_temperature_c: Number.isFinite(temperatureC) ? round1(temperatureC) : null,\n sdb_humidity_pct: Number.isFinite(humidityPct) ? round1(humidityPct) : null,\n\n auto_enabled: autoEnabled,\n notifications_enabled: notificationsEnabled,\n manual_adopt_enabled: manualAdoptEnabled,\n\n master_power_latch: masterLatchIsOn,\n deshumide_latch: deshumideLatchIsOn,\n radiator_latch: radiatorLatchIsOn,\n\n temp_on_min_c: tempOnMinC,\n temp_off_max_c: tempOffMaxC,\n humidity_on_pct: humidityOnPct,\n humidity_off_pct: humidityOffPct,\n\n deshumide_min_on_minutes: deshumideMinOnMinutes,\n deshumide_min_off_minutes: deshumideMinOffMinutes,\n\n radiator_auto_enabled: radiatorAutoEnabled,\n radiator_soc_on: radiatorSocOn,\n radiator_soc_off: radiatorSocOff,\n radiator_pv_on_w: radiatorPvOnW,\n radiator_pv_off_w: radiatorPvOffW,\n radiator_battery_charge_ceiling_a: radiatorBatteryChargeCeilingA,\n radiator_house_ac_max_w: radiatorHouseAcMaxW,\n radiator_min_on_minutes: radiatorMinOnMinutes,\n radiator_min_off_minutes: radiatorMinOffMinutes,\n\n radiator_real_power_w_internal: cfg.radiatorRealPowerW,\n\n day_start_time: input.day_start_time || null,\n day_stop_time: input.day_stop_time || null,\n\n master_last_on_at: masterLastOnAt || null,\n master_last_off_at: masterLastOffAt || null,\n deshumide_last_on_at: deshumideLastOnAt || null,\n deshumide_last_off_at: deshumideLastOffAt || null,\n radiator_last_on_at: radiatorLastOnAt || null,\n radiator_last_off_at: radiatorLastOffAt || null,\n\n evcs_priority_active: evcsPriorityActive,\n energy_priority_mode: energyPriorityMode,\n energy_priority_reason: energyPriorityReason || null,\n evcs_priority_until: evcsPriorityUntilAt || null,\n\n last_signature: lastSignature || null\n },\n\n computed: {\n in_day_window: inDayWindow,\n\n heat_need: heatNeed,\n heat_recovered: heatRecovered,\n humidity_need: humidityNeed,\n humidity_recovered: humidityRecovered,\n comfort_demand: comfortDemand,\n comfort_recovered: comfortRecovered,\n\n temp_too_low: heatNeed,\n humidity_too_high: humidityNeed,\n\n direct_pv_headroom_w: Math.round(directPvHeadroomW),\n estimated_battery_charge_w: Math.round(estimatedBatteryChargeW),\n estimated_surplus_w: Math.round(estimatedSurplusW),\n\n radiator_start_surplus_needed_w: Math.round(radiatorStartSurplusNeededW),\n radiator_hold_surplus_needed_w: Math.round(radiatorHoldSurplusNeededW),\n\n radiator_start_by_absolute_pv: radiatorStartByAbsolutePv,\n radiator_start_by_estimated_surplus: radiatorStartByEstimatedSurplus,\n radiator_hold_by_absolute_pv: radiatorHoldByAbsolutePv,\n radiator_hold_by_estimated_surplus: radiatorHoldByEstimatedSurplus,\n\n radiator_start_conditions_ok: radiatorStartConditionsOk,\n radiator_hold_conditions_ok: radiatorHoldConditionsOk,\n radiator_battery_start_ok: radiatorBatteryStartOk,\n radiator_battery_hold_ok: radiatorBatteryHoldOk,\n\n evcs_priority_active: evcsPriorityActive,\n grid_available: gridAvailable,\n\n dehum_action_reason: dehumActionReason,\n radiator_action_reason: radiatorActionReason,\n\n minutes_since_master_on: minutesSinceMasterOn !== null ? round1(minutesSinceMasterOn) : null,\n minutes_since_master_off: minutesSinceMasterOff !== null ? round1(minutesSinceMasterOff) : null,\n minutes_since_deshumide_on: minutesSinceDeshumideOn !== null ? round1(minutesSinceDeshumideOn) : null,\n minutes_since_deshumide_off: minutesSinceDeshumideOff !== null ? round1(minutesSinceDeshumideOff) : null,\n minutes_since_radiator_on: minutesSinceRadiatorOn !== null ? round1(minutesSinceRadiatorOn) : null,\n minutes_since_radiator_off: minutesSinceRadiatorOff !== null ? round1(minutesSinceRadiatorOff) : null,\n\n deshumide_min_on_elapsed: deshumideMinOnElapsed,\n deshumide_min_off_elapsed: deshumideMinOffElapsed,\n radiator_min_on_elapsed: radiatorMinOnElapsed,\n radiator_min_off_elapsed: radiatorMinOffElapsed\n },\n\n decision: {\n state,\n desired_master: desiredMaster,\n desired_deshumide: desiredDeshumide,\n desired_radiator: desiredRadiator,\n\n master_action: masterAction,\n deshumide_action: deshumideAction,\n radiator_action: radiatorAction,\n\n master_latch_action: masterLatchAction,\n deshumide_latch_action: deshumideLatchAction,\n radiator_latch_action: radiatorLatchAction,\n\n adopt_master: adoptMaster,\n adopt_deshumide: adoptDeshumide,\n adopt_radiator: adoptRadiator,\n\n set_master_last_on_at: setMasterLastOnAt,\n set_master_last_off_at: setMasterLastOffAt,\n set_deshumide_last_on_at: setDeshumideLastOnAt,\n set_deshumide_last_off_at: setDeshumideLastOffAt,\n set_radiator_last_on_at: setRadiatorLastOnAt,\n set_radiator_last_off_at: setRadiatorLastOffAt,\n\n should_notify: notificationsEnabled && signatureChanged && notifyWanted,\n signature,\n signature_changed: signatureChanged,\n reason,\n short_reason: shortReason\n },\n\n ha_service_calls: {\n helper_state: {\n execute: true,\n domain: 'input_text',\n service: 'set_value',\n data: {\n entity_id: 'input_text.sdb_climate_surplus_state',\n value: state\n }\n },\n\n helper_reason: {\n execute: true,\n domain: 'input_text',\n service: 'set_value',\n data: {\n entity_id: 'input_text.sdb_climate_surplus_reason',\n value: shortReason || 'Aucune raison'\n }\n },\n\n helper_signature: {\n execute: signatureChanged,\n domain: 'input_text',\n service: 'set_value',\n data: {\n entity_id: 'input_text.sdb_climate_surplus_last_signature',\n value: signature\n }\n },\n\n helper_master_latch: {\n execute: masterLatchAction !== 'none',\n domain: 'input_boolean',\n service: desiredMaster ? 'turn_on' : 'turn_off',\n data: {\n entity_id: 'input_boolean.sdb_master_power_latch'\n }\n },\n\n helper_deshumide_latch: {\n execute: deshumideLatchAction !== 'none',\n domain: 'input_boolean',\n service: desiredDeshumide ? 'turn_on' : 'turn_off',\n data: {\n entity_id: 'input_boolean.sdb_deshumide_latch'\n }\n },\n\n helper_radiator_latch: {\n execute: radiatorLatchAction !== 'none',\n domain: 'input_boolean',\n service: desiredRadiator ? 'turn_on' : 'turn_off',\n data: {\n entity_id: 'input_boolean.sdb_radiator_latch'\n }\n },\n\n master_power: {\n execute: masterAction !== 'none',\n domain: 'switch',\n service: desiredMaster ? 'turn_on' : 'turn_off',\n data: {\n entity_id: 'switch.sdb_prise_socket_1'\n }\n },\n\n deshumide: {\n execute: deshumideAction !== 'none',\n domain: 'switch',\n service: desiredDeshumide ? 'turn_on' : 'turn_off',\n data: {\n entity_id: 'switch.deshumide_sdb_socket_1'\n }\n },\n\n radiator: {\n execute: radiatorAction !== 'none',\n domain: 'switch',\n service: desiredRadiator ? 'turn_on' : 'turn_off',\n data: {\n entity_id: 'switch.sdb_radiateur_socket_1'\n }\n },\n\n helper_master_last_on_at: {\n execute: setMasterLastOnAt,\n domain: 'input_datetime',\n service: 'set_datetime',\n data: {\n entity_id: 'input_datetime.sdb_master_last_on_at',\n datetime: now.stamp\n }\n },\n\n helper_master_last_off_at: {\n execute: setMasterLastOffAt,\n domain: 'input_datetime',\n service: 'set_datetime',\n data: {\n entity_id: 'input_datetime.sdb_master_last_off_at',\n datetime: now.stamp\n }\n },\n\n helper_deshumide_last_on_at: {\n execute: setDeshumideLastOnAt,\n domain: 'input_datetime',\n service: 'set_datetime',\n data: {\n entity_id: 'input_datetime.sdb_deshumide_last_on_at',\n datetime: now.stamp\n }\n },\n\n helper_deshumide_last_off_at: {\n execute: setDeshumideLastOffAt,\n domain: 'input_datetime',\n service: 'set_datetime',\n data: {\n entity_id: 'input_datetime.sdb_deshumide_last_off_at',\n datetime: now.stamp\n }\n },\n\n helper_radiator_last_on_at: {\n execute: setRadiatorLastOnAt,\n domain: 'input_datetime',\n service: 'set_datetime',\n data: {\n entity_id: 'input_datetime.sdb_radiator_last_on_at',\n datetime: now.stamp\n }\n },\n\n helper_radiator_last_off_at: {\n execute: setRadiatorLastOffAt,\n domain: 'input_datetime',\n service: 'set_datetime',\n data: {\n entity_id: 'input_datetime.sdb_radiator_last_off_at',\n datetime: now.stamp\n }\n },\n\n logbook: {\n execute: signatureChanged,\n domain: 'logbook',\n service: 'log',\n data: {\n name: 'SDB Climate & Surplus',\n message: `${state} | master=${desiredMaster ? 'on' : 'off'} | deshumide=${desiredDeshumide ? 'on' : 'off'} | radiator=${desiredRadiator ? 'on' : 'off'} | ${shortReason}`,\n entity_id: 'switch.sdb_prise_socket_1'\n }\n },\n\n mobile_notification: {\n execute: notificationsEnabled && signatureChanged && notifyWanted,\n domain: cfg.mobileNotifyDomain,\n service: cfg.mobileNotifyService,\n data: {\n title: notifyTitle,\n message: notifyMessage,\n data: {\n push: {\n \"interruption-level\": \"time-sensitive\",\n sound: { name: \"default\" }\n },\n ttl: 0,\n priority: \"high\"\n }\n }\n }\n }\n};\n\nreturn [{ json: result }];\n"
|
|
},
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
448,
|
|
0
|
|
],
|
|
"id": "607b1441-a1af-4d0b-8e1a-40d001f36ed8",
|
|
"name": "Compute SDB Climate & Surplus V2.2"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const root = $input.first().json || {};\nconst calls = root.ha_service_calls || {};\nconst decision = root.decision || {};\nconst actions = [];\n\nfunction pushAction(action, call, enabled = true) {\n if (!enabled) return;\n if (!call) return;\n if (call.execute === false) return;\n\n const { execute, ...rest } = call;\n\n actions.push({\n json: {\n action,\n ...rest\n }\n });\n}\n\n// =====================================================\n// HELPERS TOUJOURS MIS A JOUR\n// =====================================================\npushAction('helper_state', calls.helper_state, true);\npushAction('helper_reason', calls.helper_reason, true);\n\n// =====================================================\n// OFF ENFANTS D'ABORD\n// =====================================================\npushAction(\n 'deshumide',\n calls.deshumide,\n decision.deshumide_action === 'turn_off'\n);\n\npushAction(\n 'radiator',\n calls.radiator,\n decision.radiator_action === 'turn_off'\n);\n\n// =====================================================\n// PUIS OFF MASTER SI NECESSAIRE\n// =====================================================\npushAction(\n 'master_power',\n calls.master_power,\n decision.master_action === 'turn_off'\n);\n\n// =====================================================\n// ON MASTER D'ABORD\n// =====================================================\npushAction(\n 'master_power',\n calls.master_power,\n decision.master_action === 'turn_on'\n);\n\n// =====================================================\n// PUIS ON ENFANTS\n// =====================================================\npushAction(\n 'deshumide',\n calls.deshumide,\n decision.deshumide_action === 'turn_on'\n);\n\npushAction(\n 'radiator',\n calls.radiator,\n decision.radiator_action === 'turn_on'\n);\n\n// =====================================================\n// LATCHES\n// =====================================================\npushAction(\n 'helper_master_latch',\n calls.helper_master_latch,\n decision.master_latch_action !== 'none'\n);\n\npushAction(\n 'helper_deshumide_latch',\n calls.helper_deshumide_latch,\n decision.deshumide_latch_action !== 'none'\n);\n\npushAction(\n 'helper_radiator_latch',\n calls.helper_radiator_latch,\n decision.radiator_latch_action !== 'none'\n);\n\n// =====================================================\n// TIMESTAMPS\n// =====================================================\npushAction(\n 'helper_master_last_on_at',\n calls.helper_master_last_on_at,\n !!decision.set_master_last_on_at\n);\n\npushAction(\n 'helper_master_last_off_at',\n calls.helper_master_last_off_at,\n !!decision.set_master_last_off_at\n);\n\npushAction(\n 'helper_deshumide_last_on_at',\n calls.helper_deshumide_last_on_at,\n !!decision.set_deshumide_last_on_at\n);\n\npushAction(\n 'helper_deshumide_last_off_at',\n calls.helper_deshumide_last_off_at,\n !!decision.set_deshumide_last_off_at\n);\n\npushAction(\n 'helper_radiator_last_on_at',\n calls.helper_radiator_last_on_at,\n !!decision.set_radiator_last_on_at\n);\n\npushAction(\n 'helper_radiator_last_off_at',\n calls.helper_radiator_last_off_at,\n !!decision.set_radiator_last_off_at\n);\n\n// =====================================================\n// SIGNATURE + LOGBOOK SI CHANGEMENT\n// =====================================================\npushAction(\n 'helper_signature',\n calls.helper_signature,\n !!decision.signature_changed\n);\n\npushAction(\n 'logbook',\n calls.logbook,\n !!decision.signature_changed\n);\n\n// =====================================================\n// NOTIFICATION\n// =====================================================\npushAction(\n 'mobile_notification',\n calls.mobile_notification,\n !!decision.should_notify\n);\n\nreturn actions;\n"
|
|
},
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
672,
|
|
0
|
|
],
|
|
"id": "02e0d133-7be4-41f3-a419-17e945e43b62",
|
|
"name": "Build SDB Climate & Surplus Actions V2.2"
|
|
}
|
|
],
|
|
"connections": {
|
|
"Schedule Trigger": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "HA Snapshot SDB Climate & Surplus",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"HA Snapshot SDB Climate & Surplus": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Compute SDB Climate & Surplus V2.2",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Compute SDB Climate & Surplus V2.2": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Build SDB Climate & Surplus Actions V2.2",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build SDB Climate & Surplus Actions V2.2": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Execute HA Service SDB Climate & Surplus",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
}
|
|
},
|
|
"nodeGroups": [],
|
|
"authors": "thierry tallagrand",
|
|
"name": "Version a50b531f",
|
|
"description": "",
|
|
"autosaved": true,
|
|
"workflowPublishHistory": [
|
|
{
|
|
"createdAt": "2026-04-14T19:35:38.771Z",
|
|
"id": 53,
|
|
"workflowId": "2NhrfxuGm4pTUNs9",
|
|
"versionId": "a50b531f-5399-463a-9943-e1a30a0cd2de",
|
|
"event": "activated",
|
|
"userId": "8b9e9552-cd74-4863-82a3-174b3b3befa2"
|
|
},
|
|
{
|
|
"createdAt": "2026-06-12T21:13:37.639Z",
|
|
"id": 118,
|
|
"workflowId": "2NhrfxuGm4pTUNs9",
|
|
"versionId": "a50b531f-5399-463a-9943-e1a30a0cd2de",
|
|
"event": "activated",
|
|
"userId": "8b9e9552-cd74-4863-82a3-174b3b3befa2"
|
|
},
|
|
{
|
|
"createdAt": "2026-07-09T21:45:53.923Z",
|
|
"id": 194,
|
|
"workflowId": "2NhrfxuGm4pTUNs9",
|
|
"versionId": "a50b531f-5399-463a-9943-e1a30a0cd2de",
|
|
"event": "activated",
|
|
"userId": "8b9e9552-cd74-4863-82a3-174b3b3befa2"
|
|
},
|
|
{
|
|
"createdAt": "2026-06-12T21:13:33.962Z",
|
|
"id": 117,
|
|
"workflowId": "2NhrfxuGm4pTUNs9",
|
|
"versionId": "a50b531f-5399-463a-9943-e1a30a0cd2de",
|
|
"event": "deactivated",
|
|
"userId": "8b9e9552-cd74-4863-82a3-174b3b3befa2"
|
|
},
|
|
{
|
|
"createdAt": "2026-07-09T21:45:53.898Z",
|
|
"id": 193,
|
|
"workflowId": "2NhrfxuGm4pTUNs9",
|
|
"versionId": "a50b531f-5399-463a-9943-e1a30a0cd2de",
|
|
"event": "deactivated",
|
|
"userId": "8b9e9552-cd74-4863-82a3-174b3b3befa2"
|
|
}
|
|
]
|
|
}
|
|
}
|