Configure laundry drying in Home Assistant

This commit is contained in:
ltadeu6 2026-05-17 14:38:48 -03:00
parent 2bd4dac0c5
commit 5e1ec01c51
3 changed files with 238 additions and 257 deletions

View file

@ -600,6 +600,31 @@ Cuidados:
- Se mover a origem dos logs do Codex, ajuste `CODEX_SESSION_ROOT` no servico `codex-status-export`.
- O dashboard do Home Assistant continua em storage mode; a fonte declarativa aqui cobre os sensores, nao os cards da UI.
### Home Assistant / secagem de roupas
Fonte principal:
- `services.home-assistant.config` em `hosts/Nixos/configuration.nix`
Estado atual:
- O helper `input_boolean.secar_roupas` e declarado pelo repo.
- A automacao `secar_roupas_start` inicia o script ao ligar o helper.
- A automacao `secar_roupas_stop` desliga `climate.ar` ao desligar o helper.
- O script `script.secar_roupas_com_ar_condicionado` usa `climate.ar`.
- A temperatura vem de `state_attr('climate.ar', 'current_temperature')`; nao ha sensor de umidade dedicado.
- O modo dominante e `dry`.
- `heat` so e usado abaixo de 21 C, com setpoint 24 C.
- `cool` so entra em pulsos curtos se o `dry` estiver idle e a temperatura permitir.
- Fan mode e definido apenas se o valor existir em `state_attr('climate.ar', 'fan_modes')`; tenta `medium`/`high` e cai para `auto`.
Cuidados:
- Se trocar a entidade do ar, atualize `haAirConditioner` no `let` de `hosts/Nixos/configuration.nix`.
- Se trocar o helper, atualize `haLaundryDryingToggle` no mesmo `let`.
- Se a integracao do ar nao expuser `hvac_action = idle`, o fallback com `cool` nao dispara; o script continua funcionando em `dry`/`heat`.
- Nao mova essa logica para arquivos em `/var/lib/hass` ou pela UI sem refletir a mudanca no repo.
### Home Assistant / dashboard do Codex
Arquivo principal:

View file

@ -1,257 +0,0 @@
# Script standalone para Home Assistant.
# Uso previsto:
# 1. Crie o helper `input_boolean.secar_roupas` pela UI do Home Assistant.
# 2. Cole o bloco abaixo em `scripts.yaml` ou importe pelo editor YAML de Scripts.
# 3. Se o seu ar nao aceitar `medium` em `fan_mode`, troque para `auto`.
secar_roupas_com_ar_condicionado:
alias: Secar roupas com ar-condicionado
description: >
Usa Dry como modo dominante para secar roupas em uma sala fechada.
Recorre a Heat apenas se a sala esfriar demais e usa Cool em pulsos curtos
quando o Dry aparenta ficar idle em temperatura ainda aceitavel.
mode: restart
icon: mdi:hanger
sequence:
- variables:
toggle_entity: input_boolean.secar_roupas
climate_entity: climate.ar
min_heat_temp: 21
heat_target_temp: 24
cool_pulse_target_temp: 22
warm_room_threshold: 22
hot_room_threshold: 28
dry_fan_mode: medium
heat_fan_mode: high
cool_fan_mode: medium
- condition: state
entity_id: input_boolean.secar_roupas
state: "on"
# Fase inicial: comeca sempre em Dry para puxar umidade das roupas.
- service: climate.set_hvac_mode
target:
entity_id: "{{ climate_entity }}"
data:
hvac_mode: dry
- service: climate.set_fan_mode
target:
entity_id: "{{ climate_entity }}"
data:
fan_mode: "{{ dry_fan_mode }}"
- wait_template: "{{ is_state(toggle_entity, 'off') }}"
timeout: "00:30:00"
continue_on_timeout: true
- repeat:
while:
- condition: state
entity_id: input_boolean.secar_roupas
state: "on"
sequence:
- variables:
current_temp: >-
{{ state_attr(climate_entity, 'current_temperature') | float(0) }}
current_action: >-
{{ state_attr(climate_entity, 'hvac_action') | default('', true) | string | lower }}
- choose:
# Sala fria: usa Heat apenas para nao deixar o ambiente frio demais.
- conditions:
- condition: template
value_template: "{{ current_temp < min_heat_temp }}"
sequence:
- service: climate.set_hvac_mode
target:
entity_id: "{{ climate_entity }}"
data:
hvac_mode: heat
- service: climate.set_temperature
target:
entity_id: "{{ climate_entity }}"
data:
temperature: "{{ heat_target_temp }}"
- service: climate.set_fan_mode
target:
entity_id: "{{ climate_entity }}"
data:
fan_mode: "{{ heat_fan_mode }}"
- wait_template: "{{ is_state(toggle_entity, 'off') }}"
timeout: "00:20:00"
continue_on_timeout: true
- choose:
- conditions:
- condition: state
entity_id: input_boolean.secar_roupas
state: "on"
sequence:
- service: climate.set_hvac_mode
target:
entity_id: "{{ climate_entity }}"
data:
hvac_mode: dry
- service: climate.set_fan_mode
target:
entity_id: "{{ climate_entity }}"
data:
fan_mode: "{{ dry_fan_mode }}"
- wait_template: "{{ is_state(toggle_entity, 'off') }}"
timeout: "00:10:00"
continue_on_timeout: true
# Sala muito quente: nunca aquece. Mantem Dry ou usa um Cool curto
# apenas se o atributo `hvac_action` existir e vier como `idle`.
- conditions:
- condition: template
value_template: "{{ current_temp >= hot_room_threshold }}"
sequence:
- choose:
- conditions:
- condition: template
value_template: "{{ current_action == 'idle' }}"
sequence:
- service: climate.set_hvac_mode
target:
entity_id: "{{ climate_entity }}"
data:
hvac_mode: cool
- service: climate.set_temperature
target:
entity_id: "{{ climate_entity }}"
data:
temperature: "{{ cool_pulse_target_temp }}"
- service: climate.set_fan_mode
target:
entity_id: "{{ climate_entity }}"
data:
fan_mode: "{{ cool_fan_mode }}"
- wait_template: "{{ is_state(toggle_entity, 'off') }}"
timeout: "00:20:00"
continue_on_timeout: true
- conditions: []
sequence:
- service: climate.set_hvac_mode
target:
entity_id: "{{ climate_entity }}"
data:
hvac_mode: dry
- service: climate.set_fan_mode
target:
entity_id: "{{ climate_entity }}"
data:
fan_mode: "{{ dry_fan_mode }}"
- wait_template: "{{ is_state(toggle_entity, 'off') }}"
timeout: "00:45:00"
continue_on_timeout: true
- choose:
- conditions:
- condition: state
entity_id: input_boolean.secar_roupas
state: "on"
sequence:
- service: climate.set_hvac_mode
target:
entity_id: "{{ climate_entity }}"
data:
hvac_mode: dry
- service: climate.set_fan_mode
target:
entity_id: "{{ climate_entity }}"
data:
fan_mode: "{{ dry_fan_mode }}"
# Dry parece parado/idle e a sala ainda esta relativamente quente:
# aplica um pulso curto de Cool e volta para Dry.
# Se o seu climate nao expuser `hvac_action`, esta heuristica nao dispara.
- conditions:
- condition: template
value_template: >-
{{
current_temp > warm_room_threshold
and current_temp < hot_room_threshold
and current_action == 'idle'
}}
sequence:
- service: climate.set_hvac_mode
target:
entity_id: "{{ climate_entity }}"
data:
hvac_mode: cool
- service: climate.set_temperature
target:
entity_id: "{{ climate_entity }}"
data:
temperature: "{{ cool_pulse_target_temp }}"
- service: climate.set_fan_mode
target:
entity_id: "{{ climate_entity }}"
data:
fan_mode: "{{ cool_fan_mode }}"
- wait_template: "{{ is_state(toggle_entity, 'off') }}"
timeout: "00:15:00"
continue_on_timeout: true
- choose:
- conditions:
- condition: state
entity_id: input_boolean.secar_roupas
state: "on"
sequence:
- service: climate.set_hvac_mode
target:
entity_id: "{{ climate_entity }}"
data:
hvac_mode: dry
- service: climate.set_fan_mode
target:
entity_id: "{{ climate_entity }}"
data:
fan_mode: "{{ dry_fan_mode }}"
- wait_template: "{{ is_state(toggle_entity, 'off') }}"
timeout: "00:40:00"
continue_on_timeout: true
default:
# Faixa normal: prioriza Dry por blocos longos.
- service: climate.set_hvac_mode
target:
entity_id: "{{ climate_entity }}"
data:
hvac_mode: dry
- service: climate.set_fan_mode
target:
entity_id: "{{ climate_entity }}"
data:
fan_mode: "{{ dry_fan_mode }}"
- wait_template: "{{ is_state(toggle_entity, 'off') }}"
timeout: "00:45:00"
continue_on_timeout: true
# Ao desativar o helper, desliga o ar para sair em estado seguro.
- service: climate.turn_off
target:
entity_id: "{{ climate_entity }}"

View file

@ -8,6 +8,59 @@ let
homeDir = "/home/${username}";
repoDir = "${homeDir}/Projetos/Sistemas/nixos-config";
codexStatusPath = "/var/lib/hass/codex_status.json";
haAirConditioner = "climate.ar";
haLaundryDryingToggle = "input_boolean.secar_roupas";
haClimateAction = action: data: {
inherit action;
target.entity_id = haAirConditioner;
inherit data;
};
haSetHvacMode = mode:
haClimateAction "climate.set_hvac_mode" { hvac_mode = mode; };
haSetTemperature = temperature:
haClimateAction "climate.set_temperature" { inherit temperature; };
haSetFanMode = fan_mode:
haClimateAction "climate.set_fan_mode" { inherit fan_mode; };
haTurnOffClimate = haClimateAction "climate.turn_off" { };
haWaitForDryingToggleOff = timeout: {
wait_template = "{{ is_state('${haLaundryDryingToggle}', 'off') }}";
inherit timeout;
continue_on_timeout = true;
};
haSetFanModeIfAvailable = preferred: fallback: {
choose = [
{
conditions = [{
condition = "template";
value_template =
"{{ '${preferred}' in (state_attr('${haAirConditioner}', 'fan_modes') or []) }}";
}];
sequence = [ (haSetFanMode preferred) ];
}
{
conditions = [{
condition = "template";
value_template =
"{{ '${fallback}' in (state_attr('${haAirConditioner}', 'fan_modes') or []) }}";
}];
sequence = [ (haSetFanMode fallback) ];
}
];
};
haDryCycle = [
(haSetHvacMode "dry")
(haSetFanModeIfAvailable "medium" "auto")
];
haHeatCycle = [
(haSetHvacMode "heat")
(haSetTemperature 24)
(haSetFanModeIfAvailable "high" "auto")
];
haCoolPulse = [
(haSetHvacMode "cool")
(haSetTemperature 22)
(haSetFanModeIfAvailable "medium" "auto")
];
zenExtension = shortId: guid: {
name = guid;
value = {
@ -240,6 +293,166 @@ in {
default_config = { };
input_boolean = {
secar_roupas = {
name = "Secar roupas";
icon = "mdi:hanger";
initial = false;
};
};
script = {
secar_roupas_com_ar_condicionado = {
alias = "Secar roupas com ar-condicionado";
description =
"Seca roupas em uma sala fechada usando Dry como modo dominante, Heat apenas contra frio excessivo e Cool em pulsos curtos quando o Dry fica idle.";
mode = "restart";
icon = "mdi:hanger";
sequence = [
{
condition = "state";
entity_id = haLaundryDryingToggle;
state = "on";
}
] ++ haDryCycle ++ [
(haWaitForDryingToggleOff "00:30:00")
{
repeat = {
while = [{
condition = "state";
entity_id = haLaundryDryingToggle;
state = "on";
}];
sequence = [
{
variables = {
current_temp =
"{{ state_attr('${haAirConditioner}', 'current_temperature') | float(24) }}";
current_action =
"{{ state_attr('${haAirConditioner}', 'hvac_action') | default('', true) | string | lower }}";
};
}
{
choose = [
{
conditions = [{
condition = "template";
value_template =
"{{ current_temp < 21 }}";
}];
sequence = haHeatCycle ++ [
(haWaitForDryingToggleOff "00:20:00")
{
choose = [{
conditions = [{
condition = "state";
entity_id = haLaundryDryingToggle;
state = "on";
}];
sequence = haDryCycle
++ [ (haWaitForDryingToggleOff "00:10:00") ];
}];
}
];
}
{
conditions = [{
condition = "template";
value_template =
"{{ current_temp >= 28 }}";
}];
sequence = [
{
choose = [{
conditions = [{
condition = "template";
value_template =
"{{ current_action == 'idle' }}";
}];
sequence = haCoolPulse ++ [
(haWaitForDryingToggleOff "00:20:00")
{
choose = [{
conditions = [{
condition = "state";
entity_id = haLaundryDryingToggle;
state = "on";
}];
sequence = haDryCycle ++ [
(haWaitForDryingToggleOff "00:40:00")
];
}];
}
];
}];
default = haDryCycle
++ [ (haWaitForDryingToggleOff "00:45:00") ];
}
];
}
{
conditions = [{
condition = "template";
value_template = ''
{{
current_temp > 22
and current_temp < 28
and current_action == 'idle'
}}
'';
}];
sequence = haCoolPulse ++ [
(haWaitForDryingToggleOff "00:15:00")
{
choose = [{
conditions = [{
condition = "state";
entity_id = haLaundryDryingToggle;
state = "on";
}];
sequence = haDryCycle
++ [ (haWaitForDryingToggleOff "00:40:00") ];
}];
}
];
}
];
default = haDryCycle
++ [ (haWaitForDryingToggleOff "00:45:00") ];
}
];
};
}
haTurnOffClimate
];
};
};
automation = [
{
id = "secar_roupas_start";
alias = "Iniciar secagem de roupas";
mode = "single";
trigger = [{
platform = "state";
entity_id = haLaundryDryingToggle;
to = "on";
}];
action = [{ action = "script.secar_roupas_com_ar_condicionado"; }];
}
{
id = "secar_roupas_stop";
alias = "Parar secagem de roupas";
mode = "single";
trigger = [{
platform = "state";
entity_id = haLaundryDryingToggle;
to = "off";
}];
action = [ haTurnOffClimate ];
}
];
command_line = [
{
sensor = {