diff --git a/.gitignore b/.gitignore index df611ec..bc45771 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,10 @@ /target __pycache__/ + +# ESPHome. Unanchored so they hold anywhere in the tree, not just next to the +# configs: secrets.yaml carries wifi credentials and the API/OTA keys, and +# .esphome is build state (including .pioenvs). examples/esphome/.gitignore says +# the same thing directory-locally because ESPHome regenerates it on every run; +# these entries are the ones that matter if a config is ever built elsewhere. +secrets.yaml +.esphome/ diff --git a/examples/esphome/.gitignore b/examples/esphome/.gitignore new file mode 100644 index 0000000..d8b4157 --- /dev/null +++ b/examples/esphome/.gitignore @@ -0,0 +1,5 @@ +# Gitignore settings for ESPHome +# This is an example and may include too much for your use-case. +# You can modify this file to suit your needs. +/.esphome/ +/secrets.yaml diff --git a/examples/esphome/relay-6ch-gateway.yaml b/examples/esphome/relay-6ch-gateway.yaml new file mode 100644 index 0000000..feb40fb --- /dev/null +++ b/examples/esphome/relay-6ch-gateway.yaml @@ -0,0 +1,287 @@ +# 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