# The Waveshare ESP32-S3-Relay-6CH as a permanent RS485 gateway: it polls the # wiredsensor node over Modbus RTU and exposes its own six relays, buzzer and # WS2812 status LED. # # esphome run relay-6ch-gateway.yaml # # This board takes the USB-RS485 bridge's place on the segment. The bridge exists # so a PC can drive the bus from tools/wiredsensor.py; a gateway is the same # master role made permanent, so the two are alternatives rather than a stack — # only one device may drive the segment at a time. # # Every pin below is fixed by the board and was read off its schematic # (files.waveshare.com/wiki/ESP32-S3-Relay-6CH/ESP32-S3-Relay-6CH.pdf). They are # not configurable, so they are written literally rather than as substitutions — # a substitution would imply a choice that does not exist. substitutions: name: relay-6ch-gateway friendly_name: "Relay gateway" # Must match UNIT_ADDRESS in firmware/src/board.rs. A mismatch presents as # total silence, indistinguishable from a wiring fault. node_address: "0x01" # Must match BAUD_RATE in core/src/timing.rs. A mismatch is silent and shows up # as random CRC failures rather than as an error. baud_rate: "19200" esphome: name: ${name} friendly_name: ${friendly_name} esp32: board: esp32-s3-devkitc-1 framework: type: esp-idf # Read off the chip with `esptool flash-id`, not taken from the schematic — # which names an ESP32-S3-WROOM-1U with no memory suffix. The part fitted here # is 16MB (winbond c8/4018), well above ESPHome's 4MB default; leaving the # default would work but would strand three quarters of the flash and cap the # OTA partition. flash_size: 16MB logger: level: INFO # The Type-C connector is wired straight to the ESP32-S3's native USB (GPIO19 # and GPIO20, the fixed D-/D+ pins), and there is no UART bridge chip. Logs # therefore have to go over USB-Serial-JTAG; the default hardware UART would # send them to pins this board does not break out, and the log would appear # to be silently broken. hardware_uart: USB_SERIAL_JTAG api: encryption: key: !secret api_encryption_key ota: - platform: esphome password: !secret ota_password wifi: ssid: !secret wifi_ssid password: !secret wifi_password ap: {} # The WROOM-1U is the external-antenna variant: it has a U.FL connector and no # PCB antenna. Without an antenna fitted it will associate from a metre away # and nowhere else, which reads as a flaky access point rather than as missing # hardware. captive_portal: # ---------------------------------------------------------------- the bus --- uart: id: rs485 tx_pin: GPIO17 rx_pin: GPIO18 baud_rate: ${baud_rate} data_bits: 8 parity: NONE stop_bits: 1 modbus: id: rs485_bus uart_id: rs485 # Deliberately no `flow_control_pin`. This board derives the SP485EE's # driver-enable from the TX line itself — a 74HC04D inverter with a 54K/1nF RC # and a diode — so DE asserts on the first start bit and releases an RC delay # after the last stop bit. There is no DE GPIO to name. Stated explicitly # because the other configs here do set one, and its absence is the single # thing that differs about this board. modbus_controller: - id: wiredsensor address: ${node_address} modbus_id: rs485_bus # The node caches a reading every second and reports its age, so polling # faster than that gains nothing. update_interval: 30s # --------------------------------------------------------------- the node --- # # The node's entities are defined inline rather than pulled from # wiredsensor-node.yaml. That package creates its own `modbus_controller`, so # including it here would leave two controllers polling address 0x01 — hence the # duplication, which is the single-node tradeoff wiredsensor.yaml makes too. Use # the package instead of this section only if you move to several nodes, and drop # the `modbus_controller` above when you do. # # This is a curated subset. For the full diagnostic set — every error counter, # all seven flag bits, both serials — copy the `sensor:`, `binary_sensor:` and # `text_sensor:` blocks out of wiredsensor.yaml; the register map is identical # and they attach to the `wiredsensor` controller above unchanged. sensor: - platform: modbus_controller modbus_controller_id: wiredsensor name: "Temperature" register_type: read address: 0x0000 value_type: S_DWORD unit_of_measurement: "°C" device_class: temperature state_class: measurement accuracy_decimals: 2 filters: - multiply: 0.001 - platform: modbus_controller modbus_controller_id: wiredsensor name: "Humidity" register_type: read address: 0x0002 value_type: S_DWORD unit_of_measurement: "%" device_class: humidity state_class: measurement accuracy_decimals: 2 filters: - multiply: 0.001 # Bus health. A segment with a termination or biasing problem shows up as a # steadily climbing CRC count long before a reading is actually lost, which is # the whole reason to graph it from the gateway. - platform: modbus_controller modbus_controller_id: wiredsensor name: "Bus CRC errors" register_type: holding address: 0x000A value_type: U_WORD state_class: total_increasing entity_category: diagnostic # ---- the gateway's own health ---- - platform: uptime name: "Gateway uptime" entity_category: diagnostic - platform: wifi_signal name: "Gateway WiFi signal" update_interval: 60s entity_category: diagnostic binary_sensor: # On `holding` (0x03) rather than `read` (0x04) deliberately. A read # overlapping 0x0000..0x0004 is refused outright when the sensor has never # produced a reading, and ESPHome merges adjacent same-type registers into one # command — so asking for the flags as `read` would let a dead sensor take down # the very diagnostic that explains it. The node serves both function codes # from one table precisely so there is a way out. - platform: modbus_controller modbus_controller_id: wiredsensor name: "Sensor fault" register_type: holding address: 0x0005 bitmask: 0x04 device_class: problem entity_category: diagnostic - platform: modbus_controller modbus_controller_id: wiredsensor name: "Data stale" register_type: holding address: 0x0005 bitmask: 0x02 entity_category: diagnostic # -------------------------------------------------------------- the relays --- # # Six independent SPDT relays, each driven from the module pin through a 0R link # resistor. Active high: the GPIO drives the coil transistor, so ON energises the # coil and closes NO. # # `restore_mode: ALWAYS_OFF` throughout rather than a restoring mode. These are # mains-side contacts; coming back from a power cut into whatever state preceded # it is a decision worth making per relay, not inheriting by default. switch: - platform: gpio name: "Relay 1" id: relay_1 pin: GPIO1 restore_mode: ALWAYS_OFF - platform: gpio name: "Relay 2" id: relay_2 pin: GPIO2 restore_mode: ALWAYS_OFF - platform: gpio name: "Relay 3" id: relay_3 pin: GPIO41 restore_mode: ALWAYS_OFF - platform: gpio name: "Relay 4" id: relay_4 pin: GPIO42 restore_mode: ALWAYS_OFF # GPIO45 and GPIO46 are ESP32-S3 strapping pins — VDD_SPI voltage select and # boot mode respectively, both sampled at reset with internal pulldowns. They # are ordinary outputs once booted, so driving relays from them is fine, but # channels 5 and 6 may click during reset before the firmware takes over. Do # not put anything on them where a momentary close at power-up matters. # # GPIO45 is the sharper of the two: it selects the flash supply voltage, and # this module's flash is 3.3V. The internal pulldown gives the right answer at # reset (esptool confirms "flash voltage set by a strapping pin: 3.3V"), and the # pin is released back to it while the CPU is in reset even if the relay was # energised. But anything external that holds GPIO45 high across a reset — a # bench pullup, a scope probe, a wiring error on CH5 — selects 1.8V and the # module will not boot at all. - platform: gpio name: "Relay 5" id: relay_5 pin: GPIO45 restore_mode: ALWAYS_OFF - platform: gpio name: "Relay 6" id: relay_6 pin: GPIO46 restore_mode: ALWAYS_OFF # ------------------------------------------------------- indicator and buzzer --- light: - platform: esp32_rmt_led_strip id: status_led name: "Status LED" pin: GPIO38 num_leds: 1 chipset: WS2812 rgb_order: GRB default_transition_length: 0s restore_mode: ALWAYS_OFF output: # Driven through LEDC because the buzzer is passive and needs a tone rather # than a level. If yours is an active buzzer, this still works — any non-zero # level sounds it — but `rtttl` melodies will all sound the same pitch. - platform: ledc id: buzzer_out pin: GPIO21 rtttl: id: buzzer output: buzzer_out button: # Bench check for the buzzer and the LED, so a freshly flashed board can be # confirmed without touching the mains side or the bus. - platform: template name: "Self test" entity_category: diagnostic on_press: - light.turn_on: id: status_led red: 0% green: 100% blue: 0% - rtttl.play: "test:d=16,o=6,b=140:c,e,g" - delay: 1s - light.turn_off: status_led