Underfloor heating for bathroom
authorDavid Woodhouse <dwmw@amazon.co.uk>
Thu, 3 Oct 2024 15:37:52 +0000 (16:37 +0100)
committerDavid Woodhouse <dwmw@amazon.co.uk>
Thu, 3 Oct 2024 15:37:52 +0000 (16:37 +0100)
bathroomfloor.yaml [new file with mode: 0644]

diff --git a/bathroomfloor.yaml b/bathroomfloor.yaml
new file mode 100644 (file)
index 0000000..3de7774
--- /dev/null
@@ -0,0 +1,230 @@
+#
+# https://www.aliexpress.com/item/1005004099215436.html
+#
+# GPIO23: Onboard blue LED (used to show MQTT connectivity)
+# GPIO16: Onboard relay (bathroom fan)
+# GPIO25: Used as output for NTC bridge
+# GPIO32: ADC, midpoint of NTC bridge
+#
+# The underfloor heating NTC thermistor is connected as follows:
+#
+#    GPIO25 (3.3v)
+#      |
+#      ▯ 10 kΩ fixed resistor
+#      |
+#      --- GPIO24 (ADC)
+#      |
+#      ▯ 10 kΩ NTC under floor
+#      |
+#      ⏚  GND
+#
+# Rather than leaving current flowing through the thermistor at all times and
+# potentially introduicing 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.
+
+substitutions:
+  name: bathroomfloor
+  domo_ufh: "912"
+  domo_temp: "913"
+  domo_thresh: "911"
+
+esphome:
+  name: ${name}
+
+esp32:
+  board: esp32-gateway
+  framework:
+    type: esp-idf
+
+packages:
+  base: !include base.yaml
+
+wifi:
+  power_save_mode: none
+  networks:
+    - ssid: !secret wifi_ssid
+      password: !secret wifi_pw
+      bssid: !secret wndr3800_bssid
+      priority: 1
+
+globals:
+  - id: relay_stable_time
+    type: time_t
+    initial_value: '0'
+
+  - id: temp_setpoint
+    type: float
+    initial_value: '21.0'
+    restore_value: true
+
+  - id: ufh_manual_delay
+    type: int
+    initial_value: '1800'
+    restore_value: true
+
+  - id: ufh_auto_delay
+    type: int
+    initial_value: '120'
+    restore_value: true
+
+
+time:
+  - platform: sntp
+
+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_nsvalues)->execute(${domo_thresh}, 2, std::to_string(id(temp_setpoint)));
+
+  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();
+                  }
+                  // Delay for longer if turned on manually
+                  id(relay_stable_time) = ::time(NULL) + id(ufh_manual_delay);
+                  break;
+
+                case ${domo_thresh}: /* Temperature setpoint */
+                  id(temp_setpoint) = x["svalue1"].as<float>();
+                  ESP_LOGD("mqtt", "Got temp_setpoint %f", id(temp_setpoint));
+                  break;
+            }
+
+light:
+  - platform: status_led
+    internal: true
+    pin: GPIO23
+    id: blue_led
+    restore_mode: ALWAYS_OFF
+
+switch:
+  - platform: gpio
+    id: ntc_vcc
+    pin: GPIO25
+    restore_mode: ALWAYS_OFF
+
+  - 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);
+            id(relay_stable_time) = ::time(NULL) + id(ufh_auto_delay);
+    on_turn_off:
+      then:
+        - lambda: |-
+            id(tell_domo_nvalue)->execute(${domo_ufh}, 0);
+            id(relay_stable_time) = ::time(NULL) + id(ufh_auto_delay + 30);
+
+sensor:
+  - platform: ntc
+    sensor: ntc_resistance
+    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.
+    # ESPHome only wants three.
+    # - 22.0706 kOhm -> 5°C
+    # - 17.9600 kOhm -> 10°C
+ #     - 14.6962 kOhm -> 15°C
+ #     - 12.0911 kOhm -> 20°C
+ #     - 10.0000 kOhm -> 25°C
+    # -  8.3124 kOhm -> 30°C
+    # -  6.9434 kOhm -> 35°C
+    # -  5.82525 kOhm -> 40°C
+    # Actual readings from SunStone Touchstat
+     -  15.0  kOhm -> 13.9°C
+#    - 12.55 kOhm - > 17.6°C
+     -  12.30 kOhm -> 18.2°C
+     -  9.97  kOhm -> 23.5°C
+    name: "Bathroom floor temperature"
+    filters:
+      - 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));
+
+          bool cur_state = id(ufh_relay).state;
+          bool want_state;
+          if (x > id(temp_setpoint))
+            want_state = false;
+          else if (x < id (temp_setpoint))
+            want_state = true;
+          else
+            return;
+
+          if (want_state != cur_state) {
+            time_t now = ::time(NULL);
+            if (now < id(relay_stable_time)) {
+              ESP_LOGD("UFH", "Too soon to turn %s (%ld seconds)", want_state ? "ON" : "OFF",
+                     now - id(relay_stable_time));
+            } else {
+              id(ufh_relay).toggle();
+            }
+          }
+
+  - platform: resistance
+    id: ntc_resistance
+    sensor: ntc_adc
+    reference_voltage: 3.28v # measured
+    resistor: 9.97 kOhm
+    configuration: DOWNSTREAM
+    filters:
+      - sliding_window_moving_average:
+          window_size: 3
+          send_every: 3
+          send_first_at: 3
+
+  - platform: adc
+    id: ntc_adc
+    attenuation: 12dB
+    update_interval: never
+    pin: GPIO34
+    filters:
+      - multiply: 0.987 # Calibrated vs. multimeter
+      # 0.80 -> 0.78
+      # 2.52 -> 2.49
+      # 1.99 -> 1.96
+
+interval:
+  - interval: 60s
+    then:
+      - switch.turn_on: ntc_vcc
+      - delay: 2s
+      - component.update: ntc_adc
+      - delay: 1s
+      - component.update: ntc_adc
+      - delay: 1s
+      - component.update: ntc_adc
+      - switch.turn_off: ntc_vcc
+