# =============================================================================
# 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 C):
#   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();
                }
