]> www.infradead.org Git - users/dwmw2/esp32-pool.git/commitdiff
Split out underfloor heating / NTC logic to ufh.yaml
authorDavid Woodhouse <dwmw@amazon.co.uk>
Sun, 8 Dec 2024 14:20:06 +0000 (14:20 +0000)
committerDavid Woodhouse <dwmw@amazon.co.uk>
Sun, 8 Dec 2024 14:20:06 +0000 (14:20 +0000)
bathroomfloor.yaml
ufh.yaml [new file with mode: 0644]

index 069c66e825cc4e554053e0576f99ebd4e34a8389..cc03b2a8e7297aa6322e28e20076f103c3788c29 100644 (file)
 
 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<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.
@@ -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 (file)
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<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
+