Docs · Home Assistant
Automate itDeepest rungs
Home Assistant Integration
TiltForge connects to Home Assistant two ways. Pick based on whether you want to keep the stock firmware or run ESPHome — though for plain open-and-close you may not need Home Assistant at all.
First — the device has its own control page
Before wiring up anything below: stock TiltForge firmware serves a control page on your local network. Open http://tiltforge-5110.local/ in any browser on the same Wi-Fi — swapping in your device's 4-character name — and you get open, close, stop, a tilt slider, and the live slat position. No Home Assistant, no app, no account, and no round trip through anyone's server.
TiltForge
Direct LAN control
Set tilt: 50%
Position 50% · open
State IDLE · Calibrated yes
▶ Device settings
That page runs on a small REST API, and it's the same API the no-reflash Home Assistant setup below drives:
| Method | Path | What it does |
|---|---|---|
| GET | / | The control page itself |
| POST / GET | /open | Open the slats (level, 50%) |
| POST / GET | /close | Close toward the nearest end |
| POST / GET | /stop | Stop wherever it is |
| POST / GET | /tilt?pct= | Drive to a tilt percentage |
| GET | /state | Status as JSON — pct is 0 closed up, 50 open, 100 closed down |
Can't reach the .local name? Some networks don't do mDNS. Use the device's IP address instead — it's in your router's client list, or from the i serial command.
Option 1 — Stock firmware + REST (no reflash)
Your TiltForge unit already exposes a local REST API. Home Assistant can drive it directly — nothing to flash.
- +File:
tiltforge-home-assistant.yaml - +How: replace
tiltforge-5110with your device's 4-character name, paste intoconfiguration.yaml, restart HA. - +Behavior: polling-based. Manual cord-pulls appear in HA within a few seconds.
- +Best for: anyone who wants the stock experience — works with the standard and Matter-capable units, alongside Matter on the Matter-capable unit.
# =============================================================================# TiltForge — Home Assistant integration (stock firmware, no reflash)# =============================================================================# Turns a shipped TiltForge unit into a native Home Assistant cover entity# using its built-in REST endpoints. Nothing changes on the device.## Needs Home Assistant 2025.12 or newer (modern "template:" syntax). The old# "cover: - platform: template" form was removed in HA 2026.6.## This is POLLING-based: HA reads /state every 5 s, so a manual cord-pull shows# up in HA within a few seconds (not instant). For instant/real-time HA, use# the ESPHome path instead (tiltforge-esphome.yaml).## SETUP:# 1. Replace every "tiltforge-5110" / "tiltforge_5110" below with YOUR# device's name — the 4-character code shown during setup (also the setup# Wi-Fi network name). Keep the dash in URLs, the underscore in ids.# 2. Paste into configuration.yaml (or a package/included file). If your# configuration.yaml already has a "rest:", "rest_command:" or# "template:" key, merge these entries into it instead of adding a# second one.# 3. Restart Home Assistant.## NOTE on .local: HA resolves tiltforge-XXXX.local on most systems. If your HA# host can't do mDNS, replace the hostname with the device's IP address (find# it in your router, on the device's own control page, or via the `i` serial# command). A DHCP reservation for the unit keeps that IP from changing.## NOTE on the scale: the device reports tilt as 0..100 with OPEN IN THE MIDDLE:# 0 = closed, slats tilted up 50 = open (slats level) 100 = closed, down# Home Assistant wants 0 = closed, 100 = open. The templates below fold the# device scale onto HA's, so "open" reads 100% in HA and either closed end# reads 0%. Setting a position keeps the slats on the side they are already# on. Don't wire the device pct straight to HA's slider: 100% would close it.# ============================================================================= # --- Poll /state once; one sensor carries everything the cover needs ---rest: - resource: "http://tiltforge-5110.local/state" scan_interval: 5 timeout: 4 sensor: - name: "TiltForge 5110 pct" unique_id: tiltforge_5110_pct value_template: "{{ value_json.pct }}" json_attributes: - state - target - calibrated - fw - matter - rescue_ap # --- Commands. The firmware takes GET or POST; POST is what we use ---rest_command: tiltforge_5110_open: url: "http://tiltforge-5110.local/open" method: POST tiltforge_5110_close: url: "http://tiltforge-5110.local/close" method: POST tiltforge_5110_stop: url: "http://tiltforge-5110.local/stop" method: POST tiltforge_5110_set: url: "http://tiltforge-5110.local/tilt?pct={{ pct }}" method: POST # =============================================================================# VARIANT A — position slider (RECOMMENDED, active)# Clean single slider + open/close/stop. Maps tilt onto HA position.# =============================================================================template: - cover: - name: "Living Room Blind" unique_id: tiltforge_5110_cover device_class: blind availability: "{{ has_value('sensor.tiltforge_5110_pct') }}" # Device 0..50..100 (closed-up, open, closed-down) -> HA 0..100..0 position: >- {% set p = states('sensor.tiltforge_5110_pct') | int(0) %} {{ 100 - ((p - 50) | abs) * 2 }} state: >- {% set p = states('sensor.tiltforge_5110_pct') | int(0) %} {% set t = state_attr('sensor.tiltforge_5110_pct', 'target') | int(p) %} {% set s = state_attr('sensor.tiltforge_5110_pct', 'state') %} {% set hp = 100 - ((p - 50) | abs) * 2 %} {% set ht = 100 - ((t - 50) | abs) * 2 %} {% if s in ['OPENING', 'CLOSING'] and ht != hp %} {{ 'opening' if ht > hp else 'closing' }} {% else %} {{ 'closed' if hp == 0 else 'open' }} {% endif %} open_cover: - action: rest_command.tiltforge_5110_open close_cover: - action: rest_command.tiltforge_5110_close stop_cover: - action: rest_command.tiltforge_5110_stop set_cover_position: - action: rest_command.tiltforge_5110_set data: # Stay on the current side: up side (<= 50) or down side (> 50) pct: >- {% set p = states('sensor.tiltforge_5110_pct') | int(0) %} {% set half = (position / 2) | round(0) | int %} {{ (100 - half) if p > 50 else half }} # =============================================================================# VARIANT B — slat-tilt control (ALTERNATIVE)# Represents the device as tilt: open/close/stop buttons + a tilt slider.# To use this instead of Variant A, delete the "template:" block above and# uncomment the one below. Same scale folding as Variant A.# =============================================================================# template:# - cover:# - name: "Living Room Blind"# unique_id: tiltforge_5110_cover# device_class: blind# availability: "{{ has_value('sensor.tiltforge_5110_pct') }}"# tilt: >-# {% set p = states('sensor.tiltforge_5110_pct') | int(0) %}# {{ 100 - ((p - 50) | abs) * 2 }}# state: >-# {% set p = states('sensor.tiltforge_5110_pct') | int(0) %}# {{ 'closed' if p in [0, 100] else 'open' }}# open_cover:# - action: rest_command.tiltforge_5110_open# close_cover:# - action: rest_command.tiltforge_5110_close# stop_cover:# - action: rest_command.tiltforge_5110_stop# set_cover_tilt_position:# - action: rest_command.tiltforge_5110_set# data:# pct: >-# {% set p = states('sensor.tiltforge_5110_pct') | int(0) %}# {% set half = (tilt / 2) | round(0) | int %}# {{ (100 - half) if p > 50 else half }}Option 2 — ESPHome (reflash, real-time)
Flash the ESPHome config to the XIAO ESP32-C6, replacing the TiltForge firmware. The device then appears in HA natively with instant, push-based updates and no HA-side YAML.
- +File:
tiltforge-esphome.yaml - +How: set your Wi-Fi (or use the captive-portal hotspot) and the two AS5600 calibration constants, then flash with the ESPHome dashboard / CLI.
- +Behavior: real-time. Uses ESPHome's native AS5600 component and the ESPHome API — no polling.
- +Best for: makers already living in ESPHome who want the tightest HA integration. Trade-off: you leave TiltForge firmware behind (no Matter, no TiltForge branded web UI). Reflash the TiltForge binary to return.
# =============================================================================# TiltForge — ESPHome reference configuration# =============================================================================# For makers who want to run their TiltForge on ESPHome instead of the stock# TiltForge firmware. Flashing this REPLACES the TiltForge firmware on the# XIAO ESP32-C6. The device then appears natively in Home Assistant as a cover# (real-time, no polling) via the ESPHome API — no YAML needed on the HA side.## Hardware assumed (TiltForge Rev D):# XIAO ESP32-C6 | AS5600 encoder on I2C | L9110 H-bridge | N20 motor## Pin mapping (XIAO C6 silk label -> ESP32-C6 GPIO):# D5 -> GPIO23 Motor IA D8 -> GPIO19 I2C SDA# D6 -> GPIO16 Motor IB D9 -> GPIO20 I2C SCL## BEFORE FLASHING — set two things:# 1. Your Wi-Fi in secrets.yaml (wifi_ssid / wifi_password), OR use the# "TiltForge Setup" captive-portal hotspot on first boot.# 2. The two calibration constants below (raw_closed / raw_open) to the# AS5600 raw values at your blind's closed and open endpoints. Easiest# way: flash as-is, watch the "Raw angle" sensor in HA while you pull the# cord to each end, then fill in the numbers and re-flash.## NOTE: This is a validated-structure reference, not bench-tested on every# blind. If Open/Close run the wrong way, swap motor_ia <-> motor_ib (either in# wiring or by swapping the two output ids below). Tune the stop tolerance if# it overshoots/undershoots.# ============================================================================= substitutions: name: "tiltforge" friendly_name: "TiltForge Blind" # --- CALIBRATION: AS5600 raw counts (0-4095) at each mechanical endpoint --- raw_closed: "0" # raw angle at FULLY CLOSED raw_open: "2048" # raw angle at FULLY OPEN # --- Stop tolerance (fraction of full travel, 0.02 = 2%) --- pos_tolerance: "0.02" esphome: name: ${name} friendly_name: ${friendly_name} esp32: board: esp32-c6-devkitc-1 # generic C6; "seeed_xiao_esp32c6" also works framework: type: esp-idf # esp-idf recommended for ESP32-C6 logger: api: # native HA connection (real-time, no polling) ota: - platform: esphome wifi: ssid: !secret wifi_ssid password: !secret wifi_password ap: ssid: "TiltForge Setup" # fallback captive portal on first boot / no wifi captive_portal: # --- I2C bus for the AS5600 (TiltForge routes it to D8/D9) ---i2c: sda: GPIO19 # XIAO D8 scl: GPIO20 # XIAO D9 frequency: 100kHz # --- AS5600 magnetic encoder (native ESPHome component, 2024.2+) ---as5600: sensor: - platform: as5600 raw_position: name: "Raw angle" id: as5600_raw update_interval: 100ms # --- L9110 H-bridge outputs ---# Truth table (TiltForge): IA=on/IB=off -> open ; IA=off/IB=on -> close ;# both off -> stop (coast).output: - platform: gpio pin: GPIO23 # XIAO D5 = Motor IA id: motor_ia - platform: gpio pin: GPIO16 # XIAO D6 = Motor IB id: motor_ib globals: - id: target_pos type: float restore_value: no initial_value: '0.5' - id: is_moving type: bool restore_value: no initial_value: 'false' # --- Motor helper scripts ---script: - id: motor_open # drive toward OPEN then: - output.turn_off: motor_ib - output.turn_on: motor_ia - id: motor_close # drive toward CLOSED then: - output.turn_off: motor_ia - output.turn_on: motor_ib - id: motor_halt then: - output.turn_off: motor_ia - output.turn_off: motor_ib - lambda: 'id(is_moving) = false;' # --- Template cover: position slider + open/close/stop ---cover: - platform: template name: ${friendly_name} id: tiltforge_cover device_class: blind has_position: true # Current position (0.0 closed .. 1.0 open) derived from the AS5600. lambda: |- float span = ${raw_open} - ${raw_closed}; if (span == 0.0f) return 0.0f; float p = (id(as5600_raw).state - (float)${raw_closed}) / span; if (p < 0.0f) p = 0.0f; if (p > 1.0f) p = 1.0f; return p; open_action: - lambda: 'id(target_pos) = 1.0f; id(is_moving) = true;' - script.execute: motor_open close_action: - lambda: 'id(target_pos) = 0.0f; id(is_moving) = true;' - script.execute: motor_close stop_action: - script.execute: motor_halt position_action: - lambda: |- float span = ${raw_open} - ${raw_closed}; float cur = (id(as5600_raw).state - (float)${raw_closed}) / span; id(target_pos) = pos; id(is_moving) = true; if (pos > cur) { id(motor_open).execute(); } else { id(motor_close).execute(); } # --- Closed-loop stop: halt the motor once the encoder reaches target ---interval: - interval: 100ms then: - if: condition: lambda: 'return id(is_moving);' then: - lambda: |- float span = ${raw_open} - ${raw_closed}; float cur = (id(as5600_raw).state - (float)${raw_closed}) / span; if (cur < 0.0f) cur = 0.0f; if (cur > 1.0f) cur = 1.0f; if (fabs(cur - id(target_pos)) <= ${pos_tolerance}) { id(motor_halt).execute(); id(tiltforge_cover).position = cur; id(tiltforge_cover).publish_state(); }Which do I want?
| Stock + REST | ESPHome | |
|---|---|---|
| Reflash needed | — | ✓ |
| Update speed | Poll (~5 s) | Real-time |
| Matter (Apple Home)† | ✓ | — |
| HA-side config | Small YAML | None |
| Keeps TiltForge firmware | ✓ | — |
† Matter is available on the Matter-capable TiltForge unit. Apple Home pairs directly today; Google Home, Alexa and SmartThings decline uncertified Matter devices until CSA certification lands, so for now they reach TiltForge through Home Assistant.
Notes
- +
.localnames: HA usually resolvestiltforge-XXXX.local. If yours can't, use the device IP (from your router, or theiserial command). - +Tilt vs position: TiltForge is a slat-tilt device that is open in the middle and closed at both ends. The REST config folds that onto HA's position slider by default (0% closed, 100% open, cleanest UX); a slat-tilt variant is included in the file. ESPHome models position directly.