Configure laundry drying in Home Assistant
This commit is contained in:
parent
2bd4dac0c5
commit
5e1ec01c51
3 changed files with 238 additions and 257 deletions
25
AGENTS.md
25
AGENTS.md
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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 }}"
|
||||
|
|
@ -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 = {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue