# thermal-node-01 — ESPHome device config (ESPHome 2024.6+ syntax) # Revision 2. Changes from v1: # - All thresholds in °C (DS18B20 reports °C; v1's "155" °F threshold could never trip) # - Interlocks run ON THE DEVICE, so they work with Home Assistant or MQTT down # - Relay moved off GPIO12 (a strapping pin) to GPIO26 # - Fail-safe: mining is OFF at boot, on any sensor fault, on low flow and on supervisor loss # - Real 0–10 V output through a GP8403 DAC; the ESP32 cannot drive 0–10 V directly # This node is the SECOND layer of protection. The FIRST layer is the hardwired # coil chain (BESS SOC dry contact → thermal switch → this relay → contactor coil). substitutions: name: thermal-node-01 trip_supply_c: "65.6" # 150 °F — set at or below the hydro-miner vendor's max outlet temperature warn_supply_c: "62.8" # 145 °F min_flow_gpm: "2.0" # the vendor's minimum loop flow for the installed fleet flow_k_ppg: "100.0" # PLACEHOLDER pulses per US gallon — use the meter datasheet, then bucket-test esphome: name: ${name} on_boot: priority: -100 then: - switch.turn_off: mining_relay # never auto-start mining after a reboot esp32: board: esp32dev framework: type: esp-idf logger: # Prefer wired Ethernet (e.g. an Olimex ESP32-POE-ISO) in plant rooms. Wi-Fi shown for brevity. wifi: ssid: !secret iot_ssid password: !secret iot_password ota: - platform: esphome password: !secret ota_password mqtt: broker: !secret mqtt_broker # broker on VLAN 40 port: 8883 username: !secret mqtt_user password: !secret mqtt_password certificate_authority: !secret mqtt_ca_pem on_disconnect: - switch.turn_off: mining_relay # supervisor lost → shed (fail-safe) one_wire: - platform: gpio pin: GPIO4 i2c: sda: GPIO21 scl: GPIO22 gp8403: id: valve_dac voltage: 10V output: - platform: gp8403 id: valve_out gp8403_id: valve_dac channel: 0 sensor: - platform: dallas_temp address: 0x2c3c012345678928 # replace with your probe's address from the boot log name: "Loop Supply Temp" id: supply_temp update_interval: 2s filters: - median: window_size: 3 send_every: 1 - platform: dallas_temp address: 0x2c3c012345678929 name: "Loop Return Temp" id: return_temp update_interval: 2s - platform: pulse_counter pin: number: GPIO27 mode: INPUT_PULLUP name: "Primary Loop Flow" id: flow_gpm unit_of_measurement: "gal/min" accuracy_decimals: 2 update_interval: 5s filters: - lambda: return x / ${flow_k_ppg}; # pulses/min ÷ pulses/gal = GPM - platform: template name: "Recovered Heat" unit_of_measurement: "kW" accuracy_decimals: 2 update_interval: 10s # Q[BTU/h] = k × GPM × ΔT[°F], with k ≈ 480 for 30 % PG; 1 kW = 3412.14 BTU/h lambda: |- float dT_f = (id(supply_temp).state - id(return_temp).state) * 1.8f; return 480.0f * id(flow_gpm).state * dT_f / 3412.14f; binary_sensor: # The relay's hardwired twin: the inverter's SOC dry contact, read back for visibility only - platform: gpio pin: number: GPIO25 mode: INPUT_PULLUP inverted: true name: "BESS SOC Permissive (hardwired)" # Contactor auxiliary NO contact: reports the ACTUAL bus state, not just the command - platform: gpio pin: number: GPIO33 mode: INPUT_PULLUP inverted: true name: "Mining Bus Energized (contactor aux)" id: mining_bus_state - platform: template id: low_flow name: "Primary Loop Low Flow" lambda: return isnan(id(flow_gpm).state) || id(flow_gpm).state < ${min_flow_gpm}; filters: - delayed_on: 10s - platform: template id: high_temp_warn name: "Loop Supply High Temp Warning" lambda: return id(supply_temp).state > ${warn_supply_c}; on_press: - number.set: id: valve_pos value: 100 # send all heat to the dry cooler - platform: template id: interlock_fault name: "Mining Interlock Fault" device_class: problem lambda: |- return isnan(id(supply_temp).state) || isnan(id(return_temp).state) || id(supply_temp).state > ${trip_supply_c} || id(low_flow).state; on_press: - switch.turn_off: mining_relay - logger.log: level: ERROR format: "INTERLOCK: mining bus shed (temp/flow/sensor fault)" switch: - platform: gpio pin: GPIO26 id: mining_relay internal: true restore_mode: ALWAYS_OFF # Exposed control. Turning ON is refused while any interlock is faulted. - platform: template name: "Mining Bus Enable" id: mining_enable lambda: return id(mining_relay).state; turn_on_action: - if: condition: binary_sensor.is_off: interlock_fault then: - switch.turn_on: mining_relay else: - logger.log: level: WARN format: "Enable refused: interlock fault active" turn_off_action: - switch.turn_off: mining_relay number: - platform: template name: "Diverter Valve to Dry Cooler" id: valve_pos unit_of_measurement: "%" min_value: 0 max_value: 100 step: 1 optimistic: true restore_value: true initial_value: 100 # boot state: reject heat (safe) set_action: - output.set_level: id: valve_out level: !lambda return x / 100.0;