substitutions:
name: bathroomfloor
+ room_name: "Bathroom"
domo_ufh: "912"
domo_temp: "913"
domo_thresh: "911"
+ adc_pin: GPIO34
esphome:
name: ${name}
packages:
base: !include base.yaml
+ ufh: !include ufh.yaml
wifi:
power_save_mode: none
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<int>();
-
- // 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<double>();
- 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.
# - 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
-
--- /dev/null
+
+# 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<int>();
+
+ // 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<double>();
+ 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
+