devices.esphome.io
Sonoff THR320D
Sonoff THR320D
Device Type: relayElectrical Standard: globalBoard: esp32Difficulty: Disassembly required, 3/5
Bootloop Workaround
Some people experience a boot loop when trying to flash esphome directly. Here's a workaround: https://community.home-assistant.io/t/bootloop-workaround-for-flashing-sonoff-th-elite-thr316d-thr320d-and-maybe-others-with-esphome-for-the-first-time/498868
GPIO Pinout
(Source: https://templates.blakadder.com/sonoff_THR320D.html) Some GPIO are active-low, meaning they're "on" when they're pulled low. In ESPHome that's often called "inverted". The relays GPIO are active-high.
The main relay is bistable/latching, meaning a pulse on pin 1 switches the relay ON, and a pulse on pin 2 switches the relay OFF. These two pins should never be active at the same time, or the device will become dangerously hot in a few minutes.
Note that until March 2024 there was an error in this page causing a safety issue:
The code was considering the relays GPIO as being active-low, when they are actually active-high. So the two main relay pins were stay simultaneously active most of the time, making the device dangerously hot.
If you copied the old version of the code from here, please remove the inverted: True line for the relays and update your devices as soon as possible.
| Pin | Function | 
|---|---|
| GPIO0 | Push Button (HIGH = off, LOW = on) | 
| GPIO4 | Small Relay (Dry Contact) | 
| GPIO19 | Large/Main Relay pin 1, pull high for relay ON | 
| GPIO22 | Large/Main Relay pin 2, pull high for relay OFF | 
| GPIO5 | Display (TM1621) Data | 
| GPIO17 | Display (TM1621) CS | 
| GPIO18 | Display (TM1621) Write | 
| GPIO23 | Display (TM1621) Read | 
| GPIO16 | Left LED (Red) | 
| GPIO15 | Middle LED (Blue) | 
| GPIO13 | Right LED (Green) | 
Basic Configuration
Internal momentary switches are used to pulse the ON/OFF pins on the main relay. A template switch is used to hide the complexity of controlling the two internal momentary switches.
One shortcoming here is we don't have any way to confirm the true state of the main relay, and so there is a possibility that our template switch could get out of sync with the true state of the relay.
substitutions:  name: "sonoffth320d"  friendly_name: "Sonoff THR320D"  project_name: "thermostats"  project_version: "1.0"  light_restore_mode: RESTORE_DEFAULT_OFF
esphome:  name: "${name}"  # supply the external temp/hum sensor with 3v power by pulling this GPIO high  on_boot:    - priority: 90      then:      - switch.turn_on: ${name}_sensor_power
esp32:  board: nodemcu-32s
api:  encryption:    key: !secret api_encryption_key
ota:  password: "ota-password"
logger:  baud_rate: 0
web_server:  port: 80
wifi:  ssid: "SSID"  password: "PASSWORD"  power_save_mode: none
captive_portal:
# This will take care of the display automatically.# You don't need to tell it to print something to the display manually.# It'll update every 60s or so.display:  platform: tm1621  id: tm1621_display  cs_pin: GPIO17  data_pin: GPIO5  read_pin: GPIO23  write_pin: GPIO18  lambda: |-    it.printf(0, "%.1f", id(${name}_temp).state);    it.display_celsius(true);    it.printf(1, "%.1f", id(${name}_humi).state);    it.display_humidity(true);
binary_sensor:  # single main button that also puts device into flash mode when held on boot  - platform: gpio    pin:      number: GPIO0      mode: INPUT_PULLUP      inverted: True    name: "${friendly_name} Button"    on_press:      then:        - switch.toggle: mainRelayVirt  - platform: status    name: "${friendly_name} Status"
switch:  # virtual switch to represent the main relay  # as far as I know, we have no way to confirm the real state  - platform: template    id: mainRelayVirt    name: "Main Relay"    turn_on_action:      - switch.turn_on: mainRelayOn      - switch.turn_on: ${name}_onoff_led    turn_off_action:      - switch.turn_on: mainRelayOff      - switch.turn_off: ${name}_onoff_led    assumed_state: True    optimistic: True    restore_state: True  # internal momentary switch for main relay ON  - platform: gpio    id: mainRelayOn    internal: True    pin:      number: GPIO19    on_turn_on:      - delay: 500ms      - switch.turn_off: mainRelayOn    restore_mode: ALWAYS_OFF  # internal momentary switch for main relay OFF  - platform: gpio    id: mainRelayOff    internal: True    pin:      number: GPIO22    on_turn_on:      - delay: 500ms      - switch.turn_off: mainRelayOff    restore_mode: ALWAYS_OFF  # dry contact relay switch  - platform: gpio    id: dryContRelay    name: "Dry Contact Relay"    pin:      number: GPIO4    on_turn_on:      - switch.turn_on: ${name}_idk_led    on_turn_off:      - switch.turn_off: ${name}_idk_led  # Rightmost (green) LED; use as dry contact indicator  - platform: gpio    id: ${name}_idk_led    pin:      number: GPIO13      inverted: true  # Leftmost (red) LED that's used to indicate the relay being on/off  - platform: gpio    id: ${name}_onoff_led    pin:      number: GPIO16      inverted: true  # This is needed to power the external temp/humidity sensor.  # It receives 3v from this pin, which is pulled up on boot.  # TODO: This should probably be an internal switch.  - platform: gpio    pin: GPIO27    id: ${name}_sensor_power    restore_mode: ALWAYS_ON
light:  # The middle (blue) LED is used as wifi status indicator.  - platform: status_led    name: "${friendly_name} State"    pin:      number: GPIO15      inverted: true
sensor:  # You need to specify here that it's an SI7021 sensor.  # This assumes you're using their device "Sonoff THS01"  - platform: dht    pin: GPIO25    model: SI7021    temperature:      name: "${friendly_name} Temperature"      id: ${name}_temp    humidity:      name: "${friendly_name} Humidity"      id: ${name}_humi    update_interval: 60s
climate:  - platform: thermostat    name: "${friendly_name} Climate"    sensor: ${name}_temp    default_preset: Home    preset:      - name: Home        default_target_temperature_low: 21 °C        mode: heat    min_heating_off_time: 300s    min_heating_run_time: 300s    min_idle_time: 30s    heat_action:      - switch.turn_on: mainRelayVirt    idle_action:      - switch.turn_off: mainRelayVirt    heat_deadband: 0.5 # how many degrees can we go under the temp before starting to heat    heat_overrun: 0.5 # how many degrees can we go over the temp before stopping
text_sensor:  - platform: wifi_info    ip_address:      name: "${friendly_name} IP Address"      disabled_by_default: trueHere is an alternative configuration, set up to control a geyser, with an ATTiny85 acting as a DS18B20 1-wire probe, using OneWireHub. The intent is to use excess solar power to heat the geyser in Boost mode, revert to Eco overnight, and default to Home in case there is no external controller.
substitutions:  name: "geyser"  friendly_name: "Geyser Thermostat"  project_name: "thermostats"  project_version: "1.0"
packages:  # contains basic setup, WiFi, etc  common: !include .common.yaml
esphome:  name: "${name}"  friendly_name: "${friendly_name}"  on_boot:    - priority: 90      then:      # supply the external sensor with 3v power by pulling this GPIO high      - output.turn_on: sensor_power      # make sure the relay is in a known state at startup      - switch.turn_off: main_relay      # Default to running the geyser in Home mode      - climate.control:          id: geyser_climate          preset: "Home"
esp32:  board: nodemcu-32s
logger:  # It's in the ceiling, nobody is listening to the UART  baud_rate: 0  level: DEBUG
web_server:  port: 80
captive_portal:
binary_sensor:  # single main button that also puts device into flash mode when held on boot  # For someone in the ceiling, this can be used to turn the climate control  # into OFF or HEAT modes. It does NOT directly control the relay.  - platform: gpio    pin:      number: GPIO0      mode: INPUT_PULLUP      inverted: True    id: button0    filters:      - delayed_on_off: 50ms    on_press:      then:        - if:            condition:              lambda: |-                return id(geyser_climate).mode != CLIMATE_MODE_OFF;            then:              - logger.log: "Button deactivates climate control"              - climate.control:                  id: geyser_climate                  mode: "OFF"            else:              - logger.log: "Button activates climate control"              - climate.control:                  id: geyser_climate                  mode: "HEAT"
switch:  # template switch to represent the main relay  # this is synchronised with the RED LED  # Note: this is controlled by the climate entity, and is not exposed  # for direct manipulation, otherwise it could be left on permanently  - platform: template    id: main_relay    turn_on_action:      - button.press: main_relay_on      - light.turn_on: onoff_led    turn_off_action:      - button.press: main_relay_off      - light.turn_off: onoff_led    assumed_state: True    optimistic: True    restore_state: True
output:  # Ideally, these two relay GPIOs should be interlocked to prevent  # simultaneous operation. ESPhome currently does not support  # interlocks at an output: level, or even at a button: level  # BE CAREFUL!  - platform: gpio    id: main_relay_on_output    pin:      number: GPIO19
  - platform: gpio    id: main_relay_off_output    pin:      number: GPIO22
  - platform: ledc    id: red_led_output    pin:      number: GPIO16      inverted: true
  - platform: ledc    id: green_led_output    pin:      number: GPIO13      inverted: true
  # This is needed to power the external sensor.  # It receives 3v3 from this pin, which is pulled up on boot.  - platform: gpio    pin: GPIO27    id: sensor_power
button:  # See note above about interlocks!  - platform: output    id: main_relay_on    output: main_relay_on_output    duration: 100ms
  - platform: output    id: main_relay_off    output: main_relay_off_output    duration: 100ms
# The middle (blue) LED is used as wifi status indicator.status_led:  pin:    number: GPIO15    inverted: true
light:  # Leftmost (red) LED that's used to indicate the relay being on/off  - platform: binary    id: onoff_led    output: red_led_output    internal: true
  # Rightmost (green) LED used to indicate climate control being active  - platform: binary    id: auto_led    output: green_led_output    internal: true
sensor:  # Geyser temperature  # Has some failsafes to disable climate control if the temperature  # being reported is unreasonable. Below 10C suggests that the ATTiny85  # is either not connected to the thermistor, or is otherwise reporting  # incorrect values, and should be investigated.  #  # NOTE: This can be overridden, but care should be taken when doing so  # because these only apply when the temperature ENTERS these ranges  # If it REMAINS in the range, and climate is turned on manually, these  # failsafes will not apply!  - platform: dallas_temp    address: 0x1e11223344550028    id: temp    name: "Temperature"    on_value_range:      - below: 10.0        then:          - logger.log: "Temperature too low, disabling climate!"          - climate.control:              id: geyser_climate              mode: "OFF"      - above: 70.0        then:          - logger.log: "Temperature too high, disabling climate!"          - climate.control:              id: geyser_climate              mode: "OFF"
  # The THR320 appears to run quite hot, let's just keep an eye on it  - platform: internal_temperature    name: "Internal Temperature"
climate:  - platform: thermostat    id: geyser_climate    name: "Climate"    sensor: temp    visual:      min_temperature: 45C      max_temperature: 70C      temperature_step:        target_temperature: 1        current_temperature: 1    default_preset: Home    preset:      - name: Home        default_target_temperature_low: 55C        mode: heat      - name: Boost        default_target_temperature_low: 65C        mode: heat      - name: Eco        default_target_temperature_low: 45C        mode: heat    min_heating_off_time: 0s    min_heating_run_time: 60s    min_idle_time: 30s    heat_action:      - switch.turn_on: main_relay    idle_action:      - switch.turn_off: main_relay    heat_deadband: 2 # how many degrees can we go under the temp before starting to heat    heat_overrun: 0.5 # how many degrees can we go over the temp before stopping    off_mode:      - switch.turn_off: main_relay    on_state:    - if:        condition:          lambda: |-            return id(geyser_climate).mode == CLIMATE_MODE_OFF;        then:          - logger.log: "Climate control OFF"          - light.turn_off: auto_led    - if:        condition:          lambda: |-            return id(geyser_climate).mode == CLIMATE_MODE_HEAT;        then:          - logger.log: "Climate control ON"          - light.turn_on: auto_led
one_wire:  pin: GPIO25  update_interval: 10s