ESPHome component for integrating Nice gate and garage door automation into Home Assistant via the Bus-T4 protocol.
Perfect for Nice BiDi-WiFi module owners who want local control without cloud dependency!
- 🏠 Home Assistant - Native ESPHome integration
- 🔒 Local control - No cloud, no internet required
- 🚪 Full gate control - Open, close, stop, and partial opening commands
- 📊 Real-time status - Opening, closing, stopped, fully open/closed states
- 📏 Position tracking - Time-based estimation with encoder priority when available
- 🧠 Auto-learning - Automatically learns and re-learns your gate's open/close timing
- ⚙️ Motor configuration - Control auto-close, standby, peak mode, and more via SET commands
- 🔧 Wide compatibility - Device-specific handling for Walky, Robus, Road 400, and more
- 📡 OXI receiver logging - Remote control button presses are logged for debugging
The Nice BiDi-WiFi module contains an ESP32-WROOM and connects directly to your Nice gate controller. By flashing ESPHome firmware, you get local Home Assistant control.
Tested with:
- Nice Robus (RBS400, RBS600, RBS800, etc.)
Should work with:
- Nice Walky (WLA1)
- Nice Road 400
- Nice Spin (SPn21)
Should work with any Nice controller that has a Bus-T4 port.
Device-specific features:
- Walky gates: Uses 1-byte position values (auto-detected)
- Robus gates: Position queries disabled during movement (auto-detected)
- Road 400: Alternate status codes supported (0x83/0x84)
esptool.py --port /dev/ttyUSB0 read_flash 0x0 0x400000 bidiwifi_backup.binUse the test points on the BiDi-WiFi board:
| Test Point | Connect To |
|---|---|
| Tx | USB-TTL RX |
| Rx | USB-TTL TX |
| IO0 | GND (hold during reset to enter flash mode) |
| EN | 3.3V |
| +3V3 | 3.3V |
| GND | Ground |
Create a gate.yaml file:
esphome:
name: gate
friendly_name: Gate
esp32:
board: esp32dev
framework:
type: esp-idf
advanced:
# BiDi-WiFi uses ESP32 rev3.1 - reduces binary size by excluding legacy silicon workarounds.
# Safe to use with OTA: incompatible chips will reject the firmware and roll back automatically.
# Remove or adjust for custom ESP32 hardware with a different chip revision.
minimum_chip_revision: "3.1"
logger:
baud_rate: 0 # Disable serial logging (UART used for Bus-T4)
api:
encryption:
key: !secret api_encryption_key
ota:
- platform: esphome
password: !secret ota_password
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
ap:
ssid: "Gate Fallback"
password: !secret fallback_password
external_components:
- source:
type: git
url: https://github.com/makstech/esphome-BusT4
components: [bus_t4]
uart:
tx_pin: GPIO21
rx_pin: GPIO18
baud_rate: 19200
bus_t4:
id: bus
cover:
- platform: bus_t4
name: "Gate"
id: gateNote: The
minimum_chip_revision: "3.1"setting is specific to the Nice BiDi-WiFi module (ESP32 rev3.1). It reduces binary size by excluding legacy workaround code. Safe to use with OTA updates — if the chip revision doesn't match, ESPHome will reject the firmware and automatically roll back. Remove or adjust for custom ESP32 hardware. See ESPHome ESP32 advanced configuration for details.
esphome run gate.yamlexternal_components:
- source:
type: git
url: https://github.com/makstech/esphome-BusT4
components: [bus_t4]
uart:
tx_pin: GPIO21
rx_pin: GPIO18
baud_rate: 19200
bus_t4:
id: bus
address: 0x5090 # Optional: custom device address
cover:
- platform: bus_t4
name: "Gate"
id: gate
auto_learn_timing: true # Auto-learn open/close duration
open_duration: 20s # Initial/fallback open time
close_duration: 20s # Initial/fallback close time
position_report_interval: 1s # Position update rate
# Optional: Additional control buttons
button:
- platform: template
name: "Partial Open"
icon: "mdi:gate-arrow-right"
on_press:
- lambda: id(gate).send_cmd(CMD_OPEN_PARTIAL_1);
- platform: template
name: "Step-by-Step"
icon: "mdi:gate"
on_press:
- lambda: id(gate).send_cmd(CMD_STEP);| Variable | Type | Default | Description |
|---|---|---|---|
address |
hex | 0x5090 |
Device address on the bus |
| Variable | Type | Default | Description |
|---|---|---|---|
name |
string | Required | Name for Home Assistant |
auto_learn_timing |
boolean | true |
Auto-learn open/close duration |
open_duration |
time | 20s |
Initial/fallback time to fully open |
close_duration |
time | 20s |
Initial/fallback time to fully close |
position_report_interval |
time | 1s |
How often to update position during movement |
Use in lambdas with id(gate).send_cmd(COMMAND):
| Command | Description |
|---|---|
CMD_OPEN |
Open gate |
CMD_CLOSE |
Close gate |
CMD_STOP |
Stop movement |
CMD_STEP |
Step-by-step (toggle) |
CMD_OPEN_PARTIAL_1 |
Partial open position 1 |
CMD_OPEN_PARTIAL_2 |
Partial open position 2 |
CMD_OPEN_PARTIAL_3 |
Partial open position 3 |
Security commands require the IT4WIFI device identity. Use send_cmd(COMMAND, IT4WIFI):
| Command | Description |
|---|---|
CMD_BLOCK |
Lock the motor |
CMD_RELEASE |
Unlock the motor |
CMD_OPEN_AND_BLOCK |
Open gate, then lock |
CMD_CLOSE_AND_BLOCK |
Close gate, then lock |
CMD_RELEASE_AND_OPEN |
Unlock, then open |
CMD_RELEASE_AND_CLOSE |
Unlock, then close |
Example lock entity:
lock:
- platform: template
name: "Gate Lock"
optimistic: true
on_lock:
- lambda: 'id(gate).send_cmd(CMD_BLOCK, IT4WIFI);'
on_unlock:
- lambda: 'id(gate).send_cmd(CMD_RELEASE, IT4WIFI);'You can change motor controller settings via lambdas. These send SET commands to the controller:
| Method | Description |
|---|---|
set_auto_close(bool) |
L1 - Enable/disable auto-close after opening |
set_photo_close(bool) |
L2 - Close after photo sensor clears |
set_always_close(bool) |
L3 - Always close (ignore hold-open) |
set_standby(bool) |
Enable/disable standby mode (power saving) |
set_peak_mode(bool) |
Enable/disable peak mode (faster operation) |
set_pre_flash(bool) |
Enable/disable pre-flash warning light |
Example usage with buttons:
button:
- platform: template
name: "Enable Auto-Close"
on_press:
- lambda: id(gate).set_auto_close(true);
- platform: template
name: "Disable Auto-Close"
on_press:
- lambda: id(gate).set_auto_close(false);Example usage with switches:
switch:
- platform: template
name: "Auto-Close"
icon: "mdi:timer"
optimistic: true
turn_on_action:
- lambda: id(gate).set_auto_close(true);
turn_off_action:
- lambda: id(gate).set_auto_close(false);
- platform: template
name: "Pre-Flash Warning"
icon: "mdi:alarm-light"
optimistic: true
turn_on_action:
- lambda: id(gate).set_pre_flash(true);
turn_off_action:
- lambda: id(gate).set_pre_flash(false);In addition to the named methods (set_auto_close, set_standby, etc.), you can set any controller parameter by its raw hex address using send_config_set():
number:
- platform: template
name: "Motor Force"
min_value: 0
max_value: 100
step: 5
set_action:
- lambda: 'id(gate).send_config_set(0x92, (uint8_t)x);'
- platform: template
name: "Pause Duration"
min_value: 0
max_value: 250
unit_of_measurement: "s"
set_action:
- lambda: 'id(gate).send_config_set(0x88, (uint8_t)x);'For debugging and testing, you can send raw hex commands directly to the bus using send_raw_cmd(). This accepts hex strings in formats like "55.0C.00.FF..." or "550C00FF..." (dots/spaces are automatically stripped).
The easiest way to use this is with an ESPHome Text component:
text:
- platform: template
name: "Raw Command"
id: raw_command
optimistic: true
mode: text
on_value:
then:
- lambda: |-
if (!x.empty()) {
id(gate).send_raw_cmd(x);
}This creates a text input in Home Assistant where you can paste hex commands. The command is sent immediately when you submit the text.
Note: The text component approach is recommended because ESPHome's API service string variables have known issues with the ESP-IDF framework.
This component uses multiple strategies for accurate position tracking:
For devices that support it, the component polls encoder position during movement every 500ms. This provides the most accurate position tracking.
- Encoder data is prioritized when available (updated within last 2 seconds)
- Automatically detected - no configuration needed
Note: Robus devices don't support position queries during movement
- Auto-Learning: When the gate performs a complete movement (fully closed → fully open or vice versa), the duration is measured and saved
- Position Calculation: During movement, position is calculated based on elapsed time
- Persistence: Learned durations are stored in flash and survive reboots
- Adaptive: If timing deviates >10% from stored value, it's automatically updated
- Smart Fallback: Only used when encoder data is unavailable or stale
When the gate stops, the component queries the controller's I/O state (INF_IO) to confirm if a limit switch is active. This ensures accurate detection of fully open/closed states even if the time-based estimate is slightly off.
Every 15 seconds, the component requests a status update from the controller. This helps:
- Recover from missed packets
- Keep state synchronized
- Detect external changes (e.g., remote control operation)
- Only learns from complete movements (end-to-end)
- Duration must be between 3 seconds and 5 minutes
- Interrupted movements don't update learned values
During initialization, the component queries the controller for product information and automatically enables device-specific handling:
| Product | Mode | Special Handling |
|---|---|---|
| WLA1 (Walky) | is_walky |
Uses 1-byte position values |
| ROBUSHSR10 | is_robus |
No position queries during movement |
| Road 400 | Standard | Alternate status codes (0x83/0x84) |
Device information (manufacturer, product, firmware) is logged at startup.
- Wait for a complete open/close cycle for auto-learning
- Check logs for "Learned new open/close duration" messages
- Manually set
open_durationandclose_durationif auto-learning fails
- Hold IO0 to GND
- Briefly disconnect EN from +3V3 (reset)
- Release IO0
- Start flashing immediately
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch
- Submit a pull request
- Original Bus-T4 work by @pruwait
- BiDi-WiFi firmware by @gashtaan
- Initial ESPHome ESP32 PoC by @andrein
This project is licensed under the GNU General Public License v3.0 - see the LICENSE file for details.
