# Top level substitutions for the system.
substitutions:
  servo_off_position: "0"
  servo_on_position: "50"

  fill_1_value: "350"
  fill_2_value: "500"
  fill_3_value: "720"

  device_name: "esphome-web-3d5cd0"
  friendly_name: "Fridge Dispenser Control"

esphome:
  name: ${device_name}
  friendly_name: ${friendly_name}
  min_version: 2024.11.0
  name_add_mac_suffix: false
  project:
    name: esphome.web
    version: dev
  # Ensure servo is off during boot.
  on_boot:
    - priority: 500
      then:
        - lambda: id(dispenser_servo_position).make_call().set_value(${servo_off_position}).perform();

esp32:
  board: esp32dev
  framework:
    type: arduino

# Tune logging to the info level, to keep the noise down.
logger:
  level: INFO

# Enable Home Assistant API (remove if you're doing a standalone device, to prevent it
# rebooting when it can't connect: https://esphome.io/components/api.html)
api:

# Allow Over-The-Air updates
ota:
- platform: esphome

# Set up a wifi access point. This reads in from the configured secrets.
wifi:
  ap: {}

# Add the web dashboard on the device for easy debugging.
dashboard_import:
  package_import_url: github://esphome/example-configs/esphome-web/esp32.yaml@main
  import_full_config: true

# Add additional debugging info. (This doesn't expose all debug info until you set the log level to debug, but this is too verbose for me given the frequent sensor updates).
debug:
  update_interval: 5s

# To have a "next url" for improv serial
web_server:

script:
  - id: start_dispensing
    parameters:
      quantity: float
    then:
      - if:
          condition:
            # Only if we're not already dispensing.
            lambda: 'return id(current_state) == 0;'
          then:
            - lambda: |-
                // Record and setup states.
                id(tare_weight) = id(weight).state;
                id(target_weight) = quantity;
                id(cancel_light).turn_on().perform();
            - delay: 400ms
            - lambda: |-
                // Begin dispensing.
                ESP_LOGI("main", "Target Weight set to %f, state %d, tare weight %f. Beginning fill.", id(target_weight), id(current_state), id(tare_weight));
                id(current_state) = 1;
                id(dispenser_servo_position).make_call().set_value(${servo_on_position}).perform();
                id(current_state_report).publish_state("Dispensing");
                id(target_report).publish_state(id(target_weight));

  - id: stop_dispensing
    then:
      - lambda: |-
          if (id(current_state) == 0) {
            // Not running? Act like tare.
            id(tare_weight) = id(weight).state;
          }

          // But always, regardless of perceived state, do all cancel actions to ensure we're in a known state.
          id(current_state) = 0;
          id(dispenser_servo_position).make_call().set_value(${servo_off_position}).perform();
          id(fill_1_light).turn_off().perform();
          id(fill_2_light).turn_off().perform();
          id(fill_3_light).turn_off().perform();
          id(cancel_light).turn_off().perform();

          id(current_state_report).publish_state("Idle");

interval:
  # Every 100ms, work out what we're doing.
  - interval: 100ms
    then:
      - lambda: |-
          if (id(current_state) == 1) {
            // We're currently filling.
            float actual_weight = id(weight).state - id(tare_weight);
            ESP_LOGI("main", "Current %.0f of target %.0f", actual_weight, id(target_weight));
            // Are we over the weight we need?
            if (actual_weight > id(target_weight)) {
              // Yes - stop.
              id(stop_dispensing).execute();
            }
          } else {
            // Not dispensing - ensure servo is off.
            // (Prevents any phantom filling in case of logic or other failure).
            id(dispenser_servo_position).make_call().set_value(${servo_off_position}).perform();
          }

globals:
  # To store the "tare offset" when you start filling.
  - id: tare_weight
    type: float
    restore_value: no
    initial_value: '0'
  # To store the target weight, when filling.
  - id: target_weight
    type: float
    restore_value: no
    initial_value: '0'
  # To store the current state of the device (0 or 1).
  # States:
  # 0 - Idle
  # 1 - Dispensing
  - id: current_state
    type: int
    restore_value: no
    initial_value: '0'

sensor:
  - platform: hx711
    id: weight
    name: "HX711 Value"
    dout_pin: 13
    clk_pin: 12
    gain: 128
    update_interval: 200ms
    internal: true
    filters:
      # To calibrate your scale:
      # - Uncomment the log lines in on_raw_value and on_value.
      # - With no weight on the device, note the raw value. (It will fluctuate; choose an average)
      # - Add a known weight from another set of scales (500g difference is enough), and note the raw value (it will fluctuate, choose an average)
      # - Enter the raw values on the left and the actual weights on the right.
      # - Comment out the log lines as they're quite frequent.
      - calibrate_linear:
          - 32000 -> 0
          - 103100 -> 543
      # Technically, the following should allow the sensor to incorporate the tare value
      # rather than having to adjust it at the time of display or calculation. However,
      # something in the esphome chain caused it not to work. I'm open to suggestions!
      # - lambda: return x - id(tare_weight);
    unit_of_measurement: g
    on_raw_value:
      then:
        lambda: |-
          // For calibration, you'll need to uncomment this.
          // ESP_LOGI("main", "Raw Value weight sensor %3.0f", x);
    on_value:
      then:
        lambda: |-
          // For calibration, you'll need to uncomment this.
          // ESP_LOGI("main", "Scaled value of weight sensor: %3.0f", x);

  # Some extra debug info, should you need it.
  # - platform: debug
  #   free:
  #     name: "Heap Free"
  #   block:
  #     name: "Heap Max Block"
  #   loop_time:
  #     name: "Loop Time"

  # Output of the target fill value so you can see it on the web dashboard.
  - platform: template
    id: target_report
    name: "Target Value"

# Output of the current state so you can see it on the web dashboard.
text_sensor:
  - platform: template
    id: current_state_report
    name: "Current State"

# Output display using a TM1637.
display:
    platform: tm1637
    id: tm1637_display
    clk_pin: 33
    dio_pin: 32
    inverted: false
    length: 4
    update_interval: 100ms
    lambda: |-
      it.printf("%4.0f", id(weight).state - id(tare_weight));

# Servo setup for the dispenser servo.
servo:
  - id: dispenser_servo
    output: servo_pwm
    auto_detach_time: 500ms
    transition_length: 500ms

# Helper to allow us to manually control the servo to work out the correct positions.
number:
  - platform: template
    id: dispenser_servo_position
    name: Servo Control
    min_value: -100
    initial_value: 0
    max_value: 100
    step: 1
    optimistic: true
    # To work out your actual servo locations, you can change "internal" to false, so it appears
    # in the web dashboard, allowing you to alter it for your fridge.
    internal: true
    set_action:
      then:
        - servo.write:
            id: dispenser_servo
            level: !lambda 'return x / 100.0;'

# The buttons are all marked as "internal" because we don't want remote control of these. I think you can understand why.
binary_sensor:
  # Fill 1
  - platform: gpio
    pin:
      number: 4
      mode:
        input: true
        pullup: true
    name: "Fill 1"
    internal: true
    on_double_click:
      min_length: 50ms
      max_length: 350ms
      then:
          - light.turn_on: fill_1_light
          - script.execute:
              id: start_dispensing
              quantity: ${fill_1_value}

  # Fill 2
  - platform: gpio
    pin:
      number: 16
      mode:
        input: true
        pullup: true
    name: "Fill 2"
    internal: true
    on_double_click:
      min_length: 50ms
      max_length: 350ms
      then:
          - light.turn_on: fill_2_light
          - script.execute:
              id: start_dispensing
              quantity: ${fill_2_value}

  # Fill 3
  - platform: gpio
    pin:
      number: 5
      mode:
        input: true
        pullup: true
    name: "Fill 3"
    internal: true
    on_double_click:
      min_length: 50ms
      max_length: 350ms
      then:
          - light.turn_on: fill_3_light
          - script.execute:
              id: start_dispensing
              quantity: ${fill_3_value}

  # Cancel Button
  - platform: gpio
    pin:
      number: 19
      mode:
        input: true
        pullup: true
    filters:
      - delayed_on: 10ms
    name: "Cancel"
    internal: true
    on_press:
      - then:
          - script.execute: stop_dispensing

light:
  # Button LED mappings into lights.
  - platform: monochromatic
    output: fill_1_pwm
    id: fill_1_light
    internal: true
    default_transition_length: 200ms
  - platform: monochromatic
    output: fill_2_pwm
    id: fill_2_light
    internal: true
    default_transition_length: 200ms
  - platform: monochromatic
    output: fill_3_pwm
    id: fill_3_light
    internal: true
    default_transition_length: 200ms
  - platform: monochromatic
    output: cancel_pwm
    id: cancel_light
    internal: true
    default_transition_length: 200ms

output:
  # Button LED PWM Channels.
  - platform: ledc
    pin: 2
    id: fill_1_pwm
    channel: 0
  - platform: ledc
    pin: 17
    id: fill_2_pwm
    channel: 1
  - platform: ledc
    pin: 18
    id: fill_3_pwm
    channel: 2
  - platform: ledc
    pin: 21
    id: cancel_pwm
    channel: 3
  # Servo PWM channel - we've allocated a specific channel for this one
  # to ensure that it's allocated a separate PWM controller with the right
  # frequency to control a servo.
  - platform: ledc
    id: servo_pwm
    pin: 25
    frequency: 50 Hz
    channel: 4