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

This commit is contained in:
Gitea Actions
2025-06-27 06:43:17 +00:00
parent a610bca89a
commit a7fc4cae7b
16 changed files with 629 additions and 25 deletions

View File

@@ -317,17 +317,136 @@ The second is the most important node of the workflow, a `function node` that ha
- Disable modes if conditions are met - Disable modes if conditions are met
- Inject these values in the payload - Inject these values in the payload
```js ```js
INSERT CODE // --- 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;
``` ```
The third node is a `TBD node`, which drops subsequent messages with similar payload: The third node is a `filter node`, which drops subsequent messages with similar payload:
ADD IMAGE ![Node-RED filter node to block similar message](img/node-red-filter-node-blocker.png)
The fourth node checks if any lock is set, with a `current state node`, we verify if the timer associated to the unit is idle. If not, the message is discarded: The fourth node checks if any lock is set, with a `current state node`, we verify if the timer associated to the unit is idle. If not, the message is discarded:
ADD IMAGE ![Node-RED current state node for timer lock](img/node-red-current-state-node-lock-timer.png)
The last node is another `current state node` which will fetch the unit state and properties: The last node is another `current state node` which will fetch the unit state and properties:
ADD IMAGE ![Node-RED current state node to get current unit state](img/node-red-current-state-node-get-unit-state.png)
#### 10. Target State #### 10. Target State
@@ -335,7 +454,85 @@ After the computation, we want to determine what should be the target mode, what
All three nodes are `function nodes`, the first one decides what should be the target mode, between: `off`, `cool`, `dry`, `fan_only` and `heat`: All three nodes are `function nodes`, the first one decides what should be the target mode, between: `off`, `cool`, `dry`, `fan_only` and `heat`:
```js ```js
INSERT CODE 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;
``` ```
The second compares the current and target node and pick which action to take: The second compares the current and target node and pick which action to take:
@@ -344,28 +541,79 @@ The second compares the current and target node and pick which action to take:
- **change**: the AC unit is on, the target mode is different, but not `off`. - **change**: the AC unit is on, the target mode is different, but not `off`.
- **stop**: the AC unit is on and it is required to stop it. - **stop**: the AC unit is on and it is required to stop it.
```js ```js
INSERT CODE 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;
``` ```
The last node determines the fan's speed of the target mode based on thresholds: The last node determines the fan's speed of the target mode based on thresholds:
```js ```js
INSERT CODE // 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. Action Switch #### 11. Action Switch
Based on the action to take, the `switch node` will route the message accordingly: Based on the action to take, the `switch node` will route the message accordingly:
ADD IMAGE ![Node-RED `switch node` pour sélectionner laction](img/node-red-switch-node-select-action.png)
#### 12. Start #### 12. Start
When the action is `start`, we first need to turn the unit online, while this takes between 20 to 40 seconds depending on the unit model, it is also locking the unit for a short period for future messages. When the action is `start`, we first need to turn the unit online, while this takes between 20 to 40 seconds depending on the unit model, it is also locking the unit for a short period for future messages.
The first node is a `call service node` using the `turn_on` service on the AC unit: The first node is a `call service node` using the `turn_on` service on the AC unit:
ADD IMAGE ![Node-RED call service node with turn_on service](img/node-red-call-service-node-turn-on.png)
The second node is another `call service node` which will start the lock timer of this unit for 45 seconds: The second node is another `call service node` which will start the lock timer of this unit for 45 seconds:
ADD IMAGE ![Node-RED call service node to start the unit timer](img/node-red-call-service-node-start-timer.png)
The last one is a `delay node` of 5 seconds, to give the time to the Home Assistant Daikin integration to resolve the new state. The last one is a `delay node` of 5 seconds, to give the time to the Home Assistant Daikin integration to resolve the new state.
@@ -374,24 +622,48 @@ The last one is a `delay node` of 5 seconds, to give the time to the Home Assist
The `change` action is used to change from one mode to another, but also used right after the start action. The `change` action is used to change from one mode to another, but also used right after the start action.
The first node is a `call service node` using `the set_hvac_mode` service on the AC unit: The first node is a `call service node` using `the set_hvac_mode` service on the AC unit:
ADD IMAGE ![Node-RED call service node with set_hvac_mode service](img/node-red-call-service-node-set-hvac-mode.png)
The following node is another delay of 5 seconds. The following node is another delay of 5 seconds.
The last one verify with a `switch node` if the target temperature needs to be set, this is only required for the modes `cool` and `heat` The last one verify with a `switch node` if the target temperature needs to be set, this is only required for the modes `cool` and `heat`:
ADD IMAGE ![Node-RED switch node for set_temp](img/node-red-switch-node-set-temp.png)
#### 14. Set Target Temperature #### 14. Set Target Temperature
The target temperature is only relevant for `cool` and `heat` mode, when you use a normal AC unit, you define a temperature to reach. This is exactly what is defined here. But because each unit is using its own internal sensor to verify, I don't trust it. If the value is already reached, the unit won't blow anything. The target temperature is only relevant for `cool` and `heat` mode, when you use a normal AC unit, you define a temperature to reach. This is exactly what is defined here. But because each unit is using its own internal sensor to verify, I don't trust it. If the value is already reached, the unit won't blow anything.
The first node is another `call service node` using the `set_temperature` service: The first node is another `call service node` using the `set_temperature` service:
ADD IMAGE ![Node-RED call service node with set_temperature service](img/node-red-call-service-node-set-temperature-service.png)
Again, this node is followed by a `delay node` of 5 seconds Again, this node is followed by a `delay node` of 5 seconds
#### 15. Check #### 15. Check
The `check` action is almost used everytime The `check` action is almost used everytime, it is actually only checks and compare the desired fan speed, it changes the fan speed if needed.
#### 16.
#### 17. The first node is a `switch node` which verify if the `speed` is defined:
![Node-RED switch node to test if speed is defined](img/node-red-switch-node-fan-speed.png)
The second is another `switch node` to compare the `speed` value with the current speed:
![Node-Red switch node to compare speed](img/node-red-switch-node-compare-speed.png)
Finally the last node is a `call service node` using the `set_fan_mode` to set the fan speed:
![Node-RED call service node with set_fan_mode](img/node-red-call-service-node-set-fan-mode.png)
#### 16. Stop
When the `action` is stop, the AC unit is simply turned off
The first node is a `call service noded` using the service `turn_off`:
![Node-RED call service node with turn_off service](img/node-red-call-service-node-turn-off.png)
The second node is another `call service node` which will start the lock timer of this unit for 45 seconds
#### 17. Manual Intervention
Sometime, for some reason, we want to use the AC manually. When we do, we don't want the workflow to change our manual setting, at least for some time.
The first node is a `trigger state node`, which will send a message when any AC unit is changing state:
![Pasted_image_20250626221149.png](img/Pasted_image_20250626221149.png)

View File

@@ -306,13 +306,345 @@ Il est ensuite connecté à un `change node`, qui ajoute la configuration dans `
} }
``` ```
#### 9. #### #### 9. Calcul
#### 10.
#### 11. Maintenant que le message contient la configuration de la pièce, on entre dans la phase de calcul. On dispose du nom de lunité de climatisation, des capteurs associés, de la température de base souhaitée et de loffset à appliquer. À partir de ces données, on récupère les états actuels et on effectue les calculs.
#### 12.
#### 13. 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.
#### 14.
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 :
![Node-RED filter node to block similar message](img/node-red-filter-node-blocker.png)
Le quatrième nœud vérifie si un verrou est actif à laide dun `current state node`. On regarde si le minuteur associé à lunité est inactif. Si ce nest pas le cas, le message est ignoré :
![Node-RED current state node for timer lock](img/node-red-current-state-node-lock-timer.png)
Le dernier nœud est un autre `current state node` qui permet de récupérer létat actuel de lunité et ses propriétés :
![Node-RED current state node to get current unit state](img/node-red-current-state-node-get-unit-state.png)
#### 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 laction à effectuer :
- **check** : le mode actuel est identique au mode cible.
- **start** : lunité est éteinte, mais un mode actif est requis.
- **change** : lunité est allumée, mais le mode cible est différent du mode actuel (et nest pas `off`).
- **stop** : lunité 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 laction à effectuer, le `switch node` va router le message vers le bon chemin :
![Node-RED `switch node` pour sélectionner laction](img/node-red-switch-node-select-action.png)
#### 12. Démarrage
Lorsque laction est `start`, il faut dabord allumer lunité. Cela prend entre 20 et 40 secondes selon le modèle, et une fois démarrée, lunité 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 lunité de climatisation :
![Node-RED call service node with turn_on service](img/node-red-call-service-node-turn-on.png)
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 :
![Node-RED call service node to start the unit timer](img/node-red-call-service-node-start-timer.png)
Le dernier est un `delay node` de 5 secondes, pour laisser le temps à lintégration Daikin de Home Assistant de refléter le nouvel état.
---
#### 13. Changement
Laction `change` est utilisée pour passer dun mode à un autre, mais aussi juste après lallumage.
Le premier nœud est un `call service node` utilisant le service `set_hvac_mode` sur lunité de climatisation :
![Node-RED call service node with set_hvac_mode service](img/node-red-call-service-node-set-hvac-mode.png)
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 nest nécessaire que pour les modes `cool` et `heat` :
![Node-RED switch node for set_temp](img/node-red-switch-node-set-temp.png)
---
#### 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 — cest exactement ce quon 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 lunité, elle ne soufflera plus du tout.
Le premier nœud est un autre `call service node` utilisant le service `set_temperature` :
![Node-RED call service node with set_temperature service](img/node-red-call-service-node-set-temperature-service.png)
Encore une fois, ce nœud est suivi dun `delay node` de 5 secondes.
#### 15. #### 15.
#### 16. #### 16.
#### 17. #### 17.
3.

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB