From b1162485a0b381ef844c137284b612ebb65a4b1b Mon Sep 17 00:00:00 2001 From: David Woodhouse Date: Sun, 8 Dec 2024 14:20:06 +0000 Subject: [PATCH] Split out underfloor heating / NTC logic to ufh.yaml --- bathroomfloor.yaml | 214 ++++++++++++--------------------------------- ufh.yaml | 204 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 261 insertions(+), 157 deletions(-) create mode 100644 ufh.yaml diff --git a/bathroomfloor.yaml b/bathroomfloor.yaml index 069c66e..cc03b2a 100644 --- a/bathroomfloor.yaml +++ b/bathroomfloor.yaml @@ -28,9 +28,11 @@ substitutions: name: bathroomfloor + room_name: "Bathroom" domo_ufh: "912" domo_temp: "913" domo_thresh: "911" + adc_pin: GPIO34 esphome: name: ${name} @@ -42,6 +44,7 @@ esp32: packages: base: !include base.yaml + ufh: !include ufh.yaml wifi: power_save_mode: none @@ -51,113 +54,79 @@ wifi: bssid: !secret wndr3800_bssid priority: 1 -script: - - id: back_to_auto - mode: restart +mqtt: + on_connect: then: - - delay: 30 min - - climate.control: - id: ntc_climate - mode: HEAT + - light.turn_on: blue_led + on_disconnect: + then: + - light.turn_off: blue_led + +light: + - platform: status_led + pin: GPIO23 + id: blue_led + restore_mode: ALWAYS_OFF time: - platform: sntp on_time: + # Ramp up to 21°C by 06:30 - seconds: 0 - minutes: 0 - hours: 6 + minutes: /5 + hours: 4,5,6 then: - climate.control: - id: ntc_climate - mode: HEAT - target_temperature: 21°C + script.execute: + id: ramp_up + target_temp: 21.0 + heat_rate: 0.08 + target_time: 385 # 06:30 - 5 minutes + # Off (15°C) at 09:30 on weekdays - seconds: 0 minutes: 30 hours: 09 + days_of_week: MON-FRI then: climate.control: id: ntc_climate mode: HEAT - target_temperature: 10°C - -mqtt: - on_connect: - then: - - light.turn_on: blue_led - - delay: 2s # Too soon and the first messages don't get through! - - lambda: |- - id(tell_domo_nvalue)->execute(${domo_ufh}, id(ufh_relay).state); - id(tell_domo_svalue)->execute(${domo_thresh}, - std::to_string(id(ntc_climate).target_temperature)); - - on_disconnect: - then: - - light.turn_off: blue_led - - on_json_message: - - topic: domoticz/out - then: - - lambda: |- - int idx = x["idx"]; - int nvalue = x["nvalue"].as(); - - // ESP_LOGD("on_json_message", x["name"]); - switch (idx) { - case ${domo_ufh}: /* UFH relay */ - if (nvalue) { - id(ufh_relay).turn_on(); - } else { - id(ufh_relay).turn_off(); - } - { - auto offcall = id(ntc_climate).make_call(); - offcall.set_mode("OFF"); - offcall.perform(); - } - id(back_to_auto).execute(); - break; - - case ${domo_thresh}: /* Temperature setpoint */ - double temp = x["svalue1"].as(); - auto call = id(ntc_climate).make_call(); - call.set_mode("HEAT"); - call.set_target_temperature(temp); - call.perform(); - ESP_LOGD("mqtt", "Got temp_setpoint %f", temp); - break; - } + target_temperature: 15°C -light: - - platform: status_led - pin: GPIO23 - id: blue_led - restore_mode: ALWAYS_OFF + # Off (15°C) at 11:00 on weekends + - seconds: 0 + minutes: 0 + hours: 11 + days_of_week: SAT-SUN + then: + climate.control: + id: ntc_climate + mode: HEAT + target_temperature: 15°C -switch: - - platform: gpio - id: ntc_vcc - pin: GPIO25 - restore_mode: ALWAYS_OFF + # Ramp up to 21°C by 8pm + - seconds: 0 + minutes: /5 + hours: 18,19 + then: + script.execute: + id: ramp_up + target_temp: 21.0 + heat_rate: 0.08 + target_time: 1195 # 20:00 - 5 minutes - - platform: gpio - name: "Bathroom UFH" - id: ufh_relay - pin: GPIO16 - restore_mode: ALWAYS_OFF - on_turn_on: - then: - - lambda: |- - id(tell_domo_nvalue)->execute(${domo_ufh}, 1); - on_turn_off: - then: - - lambda: |- - id(tell_domo_nvalue)->execute(${domo_ufh}, 0); + # Off (15°C) at pm. + - seconds: 0 + minutes: 0 + hours: 21 + then: + climate.control: + id: ntc_climate + mode: HEAT + target_temperature: 15°C sensor: - - platform: ntc - id: ntc_temperature - sensor: ntc_resistance + - id: !extend ntc_temperature calibration: # Hiwell E91.716 gives these in detail. The SunStone Touchstat manual just says # "10kΩ at 25°C, 12.1kΩ at 20°C, 14.7kΩ at 15°C, which looks basically the same. @@ -175,80 +144,11 @@ sensor: # - 12.55 kOhm - > 17.6°C - 12.30 kOhm -> 18.2°C - 9.97 kOhm -> 23.5°C - filters: - - exponential_moving_average: - send_every: 1 - send_first_at: 1 - - clamp: - min_value: 5 - max_value: 35 - ignore_out_of_range: true - on_value: - then: - lambda: |- - if (!isnan(x)) - id(tell_domo_svalue)->execute(${domo_temp}, std::to_string(x)); - - - platform: resistance - id: ntc_resistance - sensor: ntc_adc - reference_voltage: 3.28v # measured - resistor: 9.97 kOhm - configuration: DOWNSTREAM - filters: - - median: - window_size: 5 - send_every: 5 - send_first_at: 5 - - platform: adc - id: ntc_adc - attenuation: 12dB - update_interval: never - pin: GPIO34 + - id: !extend ntc_adc filters: - multiply: 0.987 # Calibrated vs. multimeter # 0.80 -> 0.78 # 2.52 -> 2.49 # 1.99 -> 1.96 -climate: - - platform: thermostat - id: ntc_climate - name: "Bathroom floor" - sensor: ntc_temperature - min_heating_off_time: 300s - min_heating_run_time: 300s - min_idle_time: 30s - heat_deadband: 0.2°C - heat_action: - - switch.turn_on: ufh_relay - idle_action: - - switch.turn_off: ufh_relay - on_control: - - lambda: |- - auto temp = x.get_target_temperature(); - if (temp && temp.value() != id(ntc_climate).target_temperature) { - ESP_LOGI("CLIMATE", "on_control, set target %f to %f", - id(ntc_climate).target_temperature, temp.value()); - id(tell_domo_svalue)->execute(${domo_thresh}, - std::to_string(temp.value())); - } - - -interval: - - interval: 60s - then: - - switch.turn_on: ntc_vcc - - delay: 1s - - component.update: ntc_adc - - delay: 0.5s - - component.update: ntc_adc - - delay: 0.5s - - component.update: ntc_adc - - delay: 0.5s - - component.update: ntc_adc - - delay: 0.5s - - component.update: ntc_adc - - switch.turn_off: ntc_vcc - diff --git a/ufh.yaml b/ufh.yaml new file mode 100644 index 0000000..ed60188 --- /dev/null +++ b/ufh.yaml @@ -0,0 +1,204 @@ + +# The underfloor heating NTC thermistor is connected as follows: +# +# GPIO25 (3.3v) +# | +# ▯ 10 kΩ fixed resistor +# | +# --- GPIO3x (ADC) +# | +# ▯ 10 kΩ NTC under floor +# | +# ⏚ GND +# +# Rather than leaving current flowing through the thermistor at all times and +# potentially introducing errors by warming it up, we only turn GPIO25 on +# when it's time to take a reading. +# +# So we disable the periodic measurement on the ADC sensor, then periodically +# turn on GPIO25, trigger *three* readings a second apart, and turn GPIO25 off +# again. The ADC sensor has a filter to average the three readings. + +script: + - id: mqtt_connect_domo + then: + - delay: 2s # Too soon and the first messages don't get through! + - lambda: |- + id(tell_domo_nvalue)->execute(${domo_ufh}, id(ufh_relay).state); + id(tell_domo_svalue)->execute(${domo_thresh}, + std::to_string(id(ntc_climate).target_temperature)); + + - id: back_to_auto + mode: restart + then: + - delay: 30 min + - climate.control: + id: ntc_climate + mode: HEAT + + # Given an estimated heating rate (typ. 0.08°C/min), ensure the target + # is reached by the required point in time. + # + - id: ramp_up + parameters: + target_time: int # minutes since midnight + target_temp: float + heat_rate: float # °C/min + then: + lambda: |- + auto t = id(sntp_time).now(); + if (!t.is_valid()) + return; + + int now_mins = (t.hour * 60) + t.minute; + int mins_till_target = target_time - now_mins; + + // Set the target for the *end* of the five-min adjustment window + mins_till_target -= 5; + + if (mins_till_target < -4) + return; // Already got there. + + float temp = target_temp - (heat_rate * mins_till_target); + if (temp < 15) + return; + + auto call = id(ntc_climate).make_call(); + call.set_mode("HEAT"); + call.set_target_temperature(temp); + call.perform(); + +mqtt: + on_connect: + then: + - script.execute: mqtt_connect_domo + + on_json_message: + - topic: domoticz/out + then: + - lambda: |- + int idx = x["idx"]; + int nvalue = x["nvalue"].as(); + + // ESP_LOGD("on_json_message", x["name"]); + switch (idx) { + case ${domo_ufh}: /* UFH relay */ + if (nvalue) { + id(ufh_relay).turn_on(); + } else { + id(ufh_relay).turn_off(); + } + { + auto offcall = id(ntc_climate).make_call(); + offcall.set_mode("OFF"); + offcall.perform(); + } + id(back_to_auto).execute(); + break; + + case ${domo_thresh}: /* Temperature setpoint */ + double temp = x["svalue1"].as(); + auto call = id(ntc_climate).make_call(); + call.set_mode("HEAT"); + call.set_target_temperature(temp); + call.perform(); + ESP_LOGD("mqtt", "Got temp_setpoint %f", temp); + break; + } + +switch: + - platform: gpio + id: ntc_vcc + pin: GPIO25 + restore_mode: ALWAYS_OFF + + - platform: gpio + name: "${room_name} UFH" + id: ufh_relay + pin: GPIO16 + restore_mode: ALWAYS_OFF + on_turn_on: + then: + - lambda: |- + id(tell_domo_nvalue)->execute(${domo_ufh}, 1); + on_turn_off: + then: + - lambda: |- + id(tell_domo_nvalue)->execute(${domo_ufh}, 0); + +sensor: + - platform: ntc + id: ntc_temperature + sensor: ntc_resistance + filters: + - exponential_moving_average: + send_every: 1 + send_first_at: 1 + - clamp: + min_value: 5 + max_value: 35 + ignore_out_of_range: true + on_value: + then: + lambda: |- + if (!isnan(x)) + id(tell_domo_svalue)->execute(${domo_temp}, std::to_string(x)); + + - platform: resistance + id: ntc_resistance + sensor: ntc_adc + reference_voltage: 3.28v # measured + resistor: 9.97 kOhm + configuration: DOWNSTREAM + filters: + - median: + window_size: 5 + send_every: 5 + send_first_at: 5 + + - platform: adc + id: ntc_adc + attenuation: 12dB + update_interval: never + pin: ${adc_pin} + +climate: + - platform: thermostat + id: ntc_climate + name: "${room_name} floor" + sensor: ntc_temperature + min_heating_off_time: 300s + min_heating_run_time: 300s + min_idle_time: 30s + heat_deadband: 0.2°C + heat_action: + - switch.turn_on: ufh_relay + idle_action: + - switch.turn_off: ufh_relay + on_control: + - lambda: |- + auto temp = x.get_target_temperature(); + if (temp && temp.value() != id(ntc_climate).target_temperature) { + ESP_LOGI("CLIMATE", "on_control, set target %f to %f", + id(ntc_climate).target_temperature, temp.value()); + id(tell_domo_svalue)->execute(${domo_thresh}, + std::to_string(temp.value())); + } + + +interval: + - interval: 60s + then: + - switch.turn_on: ntc_vcc + - delay: 1s + - component.update: ntc_adc + - delay: 0.5s + - component.update: ntc_adc + - delay: 0.5s + - component.update: ntc_adc + - delay: 0.5s + - component.update: ntc_adc + - delay: 0.5s + - component.update: ntc_adc + - switch.turn_off: ntc_vcc + -- 2.49.0