Auto-update blog content from Obsidian: 2025-06-27 06:43:17
All checks were successful
Blog Deployment / Check-Rebuild (push) Successful in 5s
Blog Deployment / Build (push) Has been skipped
Blog Deployment / Deploy-Staging (push) Successful in 9s
Blog Deployment / Test-Staging (push) Successful in 2s
Blog Deployment / Merge (push) Successful in 6s
Blog Deployment / Deploy-Production (push) Successful in 10s
Blog Deployment / Test-Production (push) Successful in 3s
Blog Deployment / Clean (push) Has been skipped
Blog Deployment / Notify (push) Successful in 2s
All checks were successful
Blog Deployment / Check-Rebuild (push) Successful in 5s
Blog Deployment / Build (push) Has been skipped
Blog Deployment / Deploy-Staging (push) Successful in 9s
Blog Deployment / Test-Staging (push) Successful in 2s
Blog Deployment / Merge (push) Successful in 6s
Blog Deployment / Deploy-Production (push) Successful in 10s
Blog Deployment / Test-Production (push) Successful in 3s
Blog Deployment / Clean (push) Has been skipped
Blog Deployment / Notify (push) Successful in 2s
This commit is contained in:
@@ -306,13 +306,345 @@ Il est ensuite connecté à un `change node`, qui ajoute la configuration dans `
|
||||
}
|
||||
```
|
||||
|
||||
#### 9.
|
||||
#### 10.
|
||||
#### 11.
|
||||
#### 12.
|
||||
#### 13.
|
||||
#### 14.
|
||||
#### #### 9. Calcul
|
||||
|
||||
Maintenant que le message contient la configuration de la pièce, on entre dans la phase de calcul. On dispose du nom de l’unité de climatisation, des capteurs associés, de la température de base souhaitée et de l’offset à appliquer. À partir de ces données, on récupère les états actuels et on effectue les calculs.
|
||||
|
||||
Le premier nœud est un `delay node` qui régule le débit des messages entrants, car le bloc précédent a potentiellement généré trois messages si toutes les pièces sont concernées.
|
||||
|
||||
Le deuxième nœud est le plus important du workflow, un `function node` qui remplit plusieurs rôles :
|
||||
|
||||
- Récupère les états des capteurs depuis Home Assistant
|
||||
- Calcule les seuils des modes à partir des offsets
|
||||
- Désactive certains modes si les conditions sont remplies
|
||||
- Injecte les valeurs dans le `payload`
|
||||
```js
|
||||
// --- Helper: Get Home Assistant state by entity ID ---
|
||||
function getState(entityId) {
|
||||
return global.get("homeassistant.homeAssistant.states")[entityId]?.state;
|
||||
}
|
||||
|
||||
// --- Determine current time period based on sensors ---
|
||||
const periods = ["jour", "soir", "nuit", "matin"];
|
||||
msg.payload.period = periods.find(p => getState(`binary_sensor.${p}`) === 'on') || 'unknown';
|
||||
|
||||
// --- Determine presence status (absent = inverse of presence) ---
|
||||
const vacances = getState("input_boolean.absent");
|
||||
const absent = getState("input_boolean.presence") === 'on' ? 'off' : 'on';
|
||||
|
||||
/**
|
||||
* Recursively adds the base temperature and offset to all numeric start values in a threshold config
|
||||
*/
|
||||
function applyOffsetToThresholds(threshold, baseTemp, globalOffset) {
|
||||
for (const [key, value] of Object.entries(threshold)) {
|
||||
if (key === "offset") continue;
|
||||
|
||||
if (typeof value === 'object') {
|
||||
applyOffsetToThresholds(value, baseTemp, globalOffset);
|
||||
} else {
|
||||
threshold[key] += baseTemp + globalOffset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the global offset for a mode, based on presence, vacation, window, and time of day
|
||||
*/
|
||||
function calculateGlobalOffset(offsets, modeName, windowState, disabledMap) {
|
||||
let globalOffset = 0;
|
||||
|
||||
for (const [key, offsetValue] of Object.entries(offsets)) {
|
||||
let conditionMet = false;
|
||||
|
||||
if (key === msg.payload.period) conditionMet = true;
|
||||
else if (key === "absent" && absent === 'on') conditionMet = true;
|
||||
else if (key === "vacances" && vacances === 'on') conditionMet = true;
|
||||
else if ((key === "fenetre" || key === "window") && windowState === 'on') conditionMet = true;
|
||||
|
||||
if (conditionMet) {
|
||||
if (offsetValue === 'disabled') {
|
||||
disabledMap[modeName] = true;
|
||||
return 0; // Mode disabled immediately
|
||||
}
|
||||
|
||||
globalOffset += parseFloat(offsetValue);
|
||||
}
|
||||
}
|
||||
|
||||
return globalOffset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main logic: compute thresholds for the specified room using the provided config
|
||||
*/
|
||||
const cfg = msg.payload.room_config;
|
||||
const room = msg.payload.room;
|
||||
|
||||
// Normalize window sensor state
|
||||
const rawWindow = getState(cfg.window);
|
||||
const window = rawWindow === 'open' ? 'on' : rawWindow === 'closed' ? 'off' : rawWindow;
|
||||
|
||||
// Gather temperatures
|
||||
const temps = cfg.thermometre.split(',')
|
||||
.map(id => parseFloat(getState(id)))
|
||||
.filter(v => !isNaN(v));
|
||||
|
||||
const temp_avg = temps.reduce((a, b) => a + b, 0) / temps.length;
|
||||
const temp_min = Math.min(...temps);
|
||||
const temp_max = Math.max(...temps);
|
||||
|
||||
// Gather humidity
|
||||
const humidities = cfg.humidity.split(',')
|
||||
.map(id => parseFloat(getState(id)))
|
||||
.filter(v => !isNaN(v));
|
||||
|
||||
const humidity_avg = humidities.reduce((a, b) => a + b, 0) / humidities.length;
|
||||
const humidity_min = Math.min(...humidities);
|
||||
const humidity_max = Math.max(...humidities);
|
||||
|
||||
// Get base temps
|
||||
const temp_ete = parseFloat(getState(cfg.temp_ete));
|
||||
const temp_hiver = parseFloat(getState(cfg.temp_hiver));
|
||||
|
||||
// Process modes
|
||||
const { threshold } = cfg;
|
||||
const modes = ["cool", "dry", "fan_only", "heat"];
|
||||
const disabled = {};
|
||||
|
||||
for (const mode of modes) {
|
||||
const baseTemp = (mode === "heat") ? temp_hiver : temp_ete;
|
||||
const globalOffset = calculateGlobalOffset(threshold[mode].offset, mode, window, disabled);
|
||||
|
||||
applyOffsetToThresholds(threshold[mode], baseTemp, globalOffset);
|
||||
}
|
||||
|
||||
// Final message
|
||||
msg.payload = {
|
||||
...msg.payload,
|
||||
unit: cfg.unit,
|
||||
timer: cfg.timer,
|
||||
threshold,
|
||||
window,
|
||||
temp: {
|
||||
min: temp_min,
|
||||
max: temp_max,
|
||||
avg: Math.round(temp_avg * 100) / 100
|
||||
},
|
||||
humidity: {
|
||||
min: humidity_min,
|
||||
max: humidity_max,
|
||||
avg: Math.round(humidity_avg * 100) / 100
|
||||
},
|
||||
disabled
|
||||
};
|
||||
|
||||
return msg;
|
||||
```
|
||||
|
||||
Le troisième nœud est un `filter node`, qui ignore les messages suivants ayant un contenu similaire :
|
||||

|
||||
|
||||
Le quatrième nœud vérifie si un verrou est actif à l’aide d’un `current state node`. On regarde si le minuteur associé à l’unité est inactif. Si ce n’est pas le cas, le message est ignoré :
|
||||

|
||||
|
||||
Le dernier nœud est un autre `current state node` qui permet de récupérer l’état actuel de l’unité et ses propriétés :
|
||||

|
||||
|
||||
#### 10. État Cible
|
||||
|
||||
Après les calculs, il s'agit maintenant de déterminer quel doit être le mode cible, quelle action effectuer pour converger vers ce mode à partir de l’état actuel, et le cas échéant, quelle vitesse de ventilation utiliser pour ce mode.
|
||||
|
||||
Les trois nœuds suivants sont des `function nodes`. Le premier détermine le mode cible à adopter parmi : `off`, `cool`, `dry`, `fan_only` et `heat` :
|
||||
```js
|
||||
const minHumidityThreshold = 52;
|
||||
const maxHumidityThreshold = 57;
|
||||
|
||||
// Helper: check if mode can be activated or stopped
|
||||
function isModeEligible(mode, temps, humidity, thresholds, currentMode) {
|
||||
const isCurrent = (mode === currentMode);
|
||||
const threshold = thresholds[mode];
|
||||
|
||||
if (msg.payload.disabled?.[mode]) return false;
|
||||
|
||||
// Determine which temperature to use for start/stop:
|
||||
// start: temp.max (except heat uses temp.min)
|
||||
// stop: temp.avg
|
||||
let tempForCheckStart;
|
||||
if (mode === "heat") {
|
||||
tempForCheckStart = temps.min; // heat start uses min temp
|
||||
} else {
|
||||
tempForCheckStart = temps.max; // others start use max temp
|
||||
}
|
||||
const tempForCheckStop = temps.avg;
|
||||
|
||||
// Dry mode also depends on humidity thresholds
|
||||
// humidity max for start, humidity avg for stop
|
||||
let humidityForCheckStart = humidity.max;
|
||||
let humidityForCheckStop = humidity.avg;
|
||||
|
||||
// For heat mode (inverted logic)
|
||||
if (mode === "heat") {
|
||||
if (!isCurrent) {
|
||||
const minStart = Math.min(...Object.values(threshold.start));
|
||||
return tempForCheckStart < minStart;
|
||||
} else {
|
||||
return tempForCheckStop < threshold.stop;
|
||||
}
|
||||
}
|
||||
|
||||
// For dry mode (humidity-dependent)
|
||||
if (mode === "dry") {
|
||||
// Skip if humidity too low
|
||||
if (humidityForCheckStart <= (isCurrent ? minHumidityThreshold : maxHumidityThreshold)) return false;
|
||||
|
||||
const minStart = Math.min(...Object.values(threshold.start));
|
||||
if (!isCurrent) {
|
||||
return tempForCheckStart >= minStart;
|
||||
} else {
|
||||
return tempForCheckStop >= threshold.stop;
|
||||
}
|
||||
}
|
||||
|
||||
// For cool and fan_only
|
||||
if (!isCurrent) {
|
||||
const minStart = Math.min(...Object.values(threshold.start));
|
||||
return tempForCheckStart >= minStart;
|
||||
} else {
|
||||
return tempForCheckStop >= threshold.stop;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Main logic ---
|
||||
const { threshold, temp, humidity, current_mode, disabled } = msg.payload;
|
||||
|
||||
const priority = ["cool", "dry", "fan_only", "heat"];
|
||||
let target_mode = "off";
|
||||
|
||||
// Loop through priority list and stop at the first eligible mode
|
||||
for (const mode of priority) {
|
||||
if (isModeEligible(mode, temp, humidity, threshold, current_mode)) {
|
||||
target_mode = mode;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
msg.payload.target_mode = target_mode;
|
||||
|
||||
if (target_mode === "cool" || target_mode === "heat") {
|
||||
msg.payload.set_temp = true;
|
||||
}
|
||||
|
||||
return msg;
|
||||
```
|
||||
|
||||
Le second compare le mode actuel avec le mode cible et choisit l’action à effectuer :
|
||||
- **check** : le mode actuel est identique au mode cible.
|
||||
- **start** : l’unité est éteinte, mais un mode actif est requis.
|
||||
- **change** : l’unité est allumée, mais le mode cible est différent du mode actuel (et n’est pas `off`).
|
||||
- **stop** : l’unité est allumée mais doit être arrêtée.
|
||||
```js
|
||||
let action = "check"; // default if both are same
|
||||
|
||||
if (msg.payload.current_mode === "off" && msg.payload.target_mode !== "off") {
|
||||
action = "start";
|
||||
} else if (msg.payload.current_mode !== "off" && msg.payload.target_mode !== "off" && msg.payload.current_mode !== msg.payload.target_mode) {
|
||||
action = "change";
|
||||
} else if (msg.payload.current_mode !== "off" && msg.payload.target_mode === "off") {
|
||||
action = "stop";
|
||||
}
|
||||
|
||||
msg.payload.action = action;
|
||||
return msg;
|
||||
```
|
||||
|
||||
Le dernier nœud détermine la vitesse de ventilation appropriée pour le mode cible, en fonction des seuils définis :
|
||||
```js
|
||||
// Function to find the appropriate speed key based on temperature and mode
|
||||
function findSpeed(thresholdStart, temperature, mode) {
|
||||
let closestSpeed = 'quiet';
|
||||
let closestTemp = mode === 'heat' ? Infinity : -Infinity;
|
||||
|
||||
for (const speedKey in thresholdStart) {
|
||||
if (speedKey !== 'quiet') {
|
||||
const tempValue = thresholdStart[speedKey];
|
||||
if (mode === 'heat') {
|
||||
if (tempValue >= temperature && tempValue <= closestTemp) {
|
||||
closestSpeed = speedKey;
|
||||
closestTemp = tempValue;
|
||||
}
|
||||
} else { // cool, fan_only
|
||||
if (tempValue <= temperature && tempValue >= closestTemp) {
|
||||
closestSpeed = speedKey;
|
||||
closestTemp = tempValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return closestSpeed;
|
||||
}
|
||||
|
||||
if (msg.payload.target_mode && msg.payload.target_mode !== "off" && msg.payload.target_mode !== "dry") {
|
||||
const modeData = msg.payload.threshold[msg.payload.target_mode];
|
||||
if (modeData && modeData.start) {
|
||||
if (msg.payload.target_mode === "heat") {
|
||||
msg.payload.speed = findSpeed(modeData.start, msg.payload.temp.min, 'heat');
|
||||
} else {
|
||||
msg.payload.speed = findSpeed(modeData.start, msg.payload.temp.max, 'cool');
|
||||
}
|
||||
} else {
|
||||
node.error("Invalid mode data or missing 'start' thresholds", msg);
|
||||
}
|
||||
} else {
|
||||
// No need for speed in 'off' or 'dry' modes
|
||||
msg.payload.speed = null;
|
||||
}
|
||||
|
||||
return msg;
|
||||
```
|
||||
|
||||
#### 11. Choix de l'Action
|
||||
|
||||
En fonction de l’action à effectuer, le `switch node` va router le message vers le bon chemin :
|
||||

|
||||
|
||||
#### 12. Démarrage
|
||||
|
||||
Lorsque l’action est `start`, il faut d’abord allumer l’unité. Cela prend entre 20 et 40 secondes selon le modèle, et une fois démarrée, l’unité est verrouillée pendant un court laps de temps pour éviter les messages suivants.
|
||||
|
||||
Le premier nœud est un `call service node` utilisant le service `turn_on` sur l’unité de climatisation :
|
||||

|
||||
|
||||
Le second nœud est un autre `call service node` qui va démarrer un minuteur de verrouillage (lock timer) pour cette unité pendant 45 secondes :
|
||||

|
||||
|
||||
Le dernier est un `delay node` de 5 secondes, pour laisser le temps à l’intégration Daikin de Home Assistant de refléter le nouvel état.
|
||||
|
||||
---
|
||||
|
||||
#### 13. Changement
|
||||
|
||||
L’action `change` est utilisée pour passer d’un mode à un autre, mais aussi juste après l’allumage.
|
||||
|
||||
Le premier nœud est un `call service node` utilisant le service `set_hvac_mode` sur l’unité de climatisation :
|
||||

|
||||
|
||||
Le nœud suivant est un `delay node` de 5 secondes.
|
||||
|
||||
Le dernier vérifie, avec un `switch node`, si la température cible doit être définie. Cela n’est nécessaire que pour les modes `cool` et `heat` :
|
||||

|
||||
|
||||
---
|
||||
|
||||
#### 14. Définir la Température Cible
|
||||
|
||||
La température cible est uniquement pertinente pour les modes `cool` et `heat`. Avec une climatisation classique, vous définissez une température à atteindre — c’est exactement ce qu’on fait ici. Mais comme chaque unité utilise son propre capteur interne pour vérifier cette température, je ne leur fais pas vraiment confiance. Si la température cible est déjà atteinte selon l’unité, elle ne soufflera plus du tout.
|
||||
|
||||
Le premier nœud est un autre `call service node` utilisant le service `set_temperature` :
|
||||

|
||||
|
||||
Encore une fois, ce nœud est suivi d’un `delay node` de 5 secondes.
|
||||
|
||||
#### 15.
|
||||
#### 16.
|
||||
#### 17.
|
||||
3.
|
||||
|
||||
|
Reference in New Issue
Block a user