esphome:
  name: "doggy-door-controller"
  friendly_name: Doggy Door Controller
  on_boot:
    priority: -100 
    then:
      - script.execute: clear_faults
      - lambda: id(status_msg).publish_state("Powered On");
      - script.execute:
          id: go_beep
          num_beeps: 1
      - number.set:
          id: mtr_pwr_level
          value: 0.8
      - button.press: open_button  

esp32:
  board: esp32dev
  framework:
    type: arduino

# Enable logging
#logger:
#  level: VERBOSE

# Enable Home Assistant API
api:
  encryption:
    key: "wnzX6NGZ6b/dFyzmPG9uN4b8XS0gLsC+cuAXKLppAy4="

ota:
  - platform: esphome

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

globals: # This is just to keep track of the duty cycle that has been commanded
   - id: open_pin_pwm_duty
     type: float
     restore_value: no
     initial_value: '0.01'

   - id: bark_sensor_triggered # This is true when the adc bark sound level is above the threshold and resets to false when below
     type: bool
     restore_value: no
     initial_value: "false"   

   - id: bark_threshold_max
     type: float
     restore_value: no
     initial_value: '2.6'   

number:

  - platform: template
    name: Bark Sound Level Threshold
    optimistic: true
    id: bark_sound_threshold
    step: 0.0001 # This step size combined with a 500ms step time means the threshold will change at a rate of 1.44V/hour
    min_value: 1.4
    max_value: 2.6 # This must be changed to match the global variable bark_threshold_max
    restore_value: False
    initial_value: 1.75
    internal: True
    update_interval: 5s
#    set_action:
#      - lambda: !lambda |-
#          float bark_sound_threshold = x;

  - platform: template
    name: Close Current Limit
    optimistic: true
    id: close_current_limit
    step: 0.01
    min_value: 1.1
    max_value: 1.9 # 1.9
    restore_value: True
    initial_value: 1.4
    set_action:
      - lambda: !lambda |-
          float close_current_limit = x;

  - platform: template
    name: Open Current Limit
    optimistic: true
    id: open_current_limit
    step: 0.01
    min_value: 2.7
    max_value: 3.2
    restore_value: True
    initial_value: 2.65
    set_action:
      - lambda: !lambda |-
          float open_current_limit = x;

  - platform: template
    name: Motor Power Level
    optimistic: true
    id: mtr_pwr_level
    step: 0.001
    min_value: 0.01
    max_value: 0.99
    restore_value: False
    initial_value: 0.6
    set_action:
      - lambda: !lambda |-
          float mtr_pwr_level = x;


  - platform: template
    name: Switching Frequency
    optimistic: true
    id: switching_freq
    step: 1000
    min_value: 1000
    max_value: 25000
    restore_value: False
    initial_value: 19531
    set_action:
      - lambda: !lambda |-
          float switching_freq = x;
    on_value:
      then:
        - output.ledc.set_frequency:
            id: open_pin
            frequency: !lambda |-
                return id(switching_freq).state;

i2c:
  sda: 21 # BLUE WIRE
  scl: 22 # GREEN WIRE
  scan: true
  id: bus_a

sensor:
  - platform: template
    name: "Bark Sound Threshold Read Only Sensor"
    id: bark_sound_threshold_sensor
    unit_of_measurement: "V"
    accuracy_decimals: 4
    update_interval: 5s
    lambda: |-
      // Return the internal number's state here
      return id(bark_sound_threshold).state;

  - platform: adc
    id: doggy_door_bark_sound_meter
    pin: 36
    name: "Doggy Door Bark Sound Meter"
    update_interval: 600ms
    internal: False
    attenuation: auto
    force_update: True
    accuracy_decimals: 2
    on_value:
      then:
        - if:
            condition:
              lambda: |-
                // If we're below threshold, decrement it
                return id(doggy_door_bark_sound_meter).state < id(bark_sound_threshold).state;
            then:
              - number.decrement:
                  id: bark_sound_threshold
                  cycle: false
    on_value_range:
      - below: !lambda return id(bark_sound_threshold).state;
        then:
          - lambda: |-
              // Whenever we go back below threshold set the bark_sensor_triggered to false again
              if (id(bark_sensor_triggered)) {
                id(bark_sensor_triggered) = false;
              }
      - above: !lambda "return id(bark_sound_threshold).state;"
        then:
          - if:
              # Only trigger if we haven't already triggered yet
              condition:
                lambda: return !id(bark_sensor_triggered); # Check if bark_sensor_triggered is false
              then:
                - lambda: |-
                    float new_value = id(bark_sound_threshold).state + 0.08;
                    if (new_value > id(bark_threshold_max)) {
                    new_value = id(bark_threshold_max);
                    }
                    id(bark_sound_threshold).publish_state(new_value);
                - lambda: |-
                    // Mark it triggered, so we won't fire repeatedly
                    id(bark_sensor_triggered) = true;
                - if:
                    condition:
                      and:
                        - switch.is_on: bark_sensor_enable
                        - lambda: |-
                            return id(close_limit_switch).state == true;
                    then:
                      - number.set:
                          id: mtr_pwr_level
                          value: 0.8
                      - button.press: open_button

    
  - platform: ina260
    address: 0x40
    update_interval: 5ms
    current:
      name: "INA260 Current"
      id: mtr_current
      internal: true
      filters:
        - sliding_window_moving_average:
            window_size: 10
            send_every: 10
        - lambda: return fabs(x); 
    bus_voltage:
      name: "Doggy Door Bus Voltage"
      id: bus_voltage 
      filters:
        - sliding_window_moving_average:
            window_size: 400
            send_every: 400
        - lambda: return fabs(x);

  - platform: template
    name: "Doggy Door Motor Current"
    id: mtr_current_ha
    unit_of_measurement: "A"
    accuracy_decimals: 3
    update_interval: 300ms
    lambda: |-
      // Just forward the value from mtr_current
      return id(mtr_current).state;
    filters:
      - lambda: |-
          // If it's under 0.002, treat it as 0.000
          if (x < 0.002) {
            return 0.0;
          } else {
            return x;
          }

  - platform: uptime
    name: Uptime Sensor
    id: uptime_sensor
    update_interval: 1s

text_sensor:
  - platform: template
    name: "Doggy Door Status Message"
    id: status_msg
    lambda: |-
      if (id(close_limit_switch).state == true && id(open_limit_switch).state == false) {
        return std::string("Closed");
      } else if (id(open_limit_switch).state == true && id(close_limit_switch).state == false) {
        return std::string("Open");
      } else if (id(close_limit_switch).state == false && id(open_limit_switch).state == false) {
        return std::string("Partly Open");
      } else if (id(close_limit_switch).state == true && id(open_limit_switch).state == true) {
        return std::string("Limit Switch Error");
      } else {
        return std::string("Not Implemented yet");
      }
    update_interval: 10s

binary_sensor:
  - platform: status
    name: "Doggy Door Controller Status"

  - platform: template
    name: "Bark Sensor Triggered"
    id: bark_sensor_triggered_binary
    lambda: |-
      // Return the global boolean so HA sees it as ON/OFF
      return id(bark_sensor_triggered);
    filters: 
      - delayed_off: 
          seconds: 30

  - platform: template
    name: "Doggy Door Pinch While Opening"
    id: opening_pinch_fault
    device_class: tamper
    on_press: 
      then:
        - delay: 5s
        - lambda: id(status_msg).publish_state("Open Pinch");

  - platform: template
    name: "Doggy Door Pinch While Closing"
    id: closing_pinch_fault
    device_class: tamper
    on_press: 
      then:
        - delay: 5s
        - lambda: id(status_msg).publish_state("Close Pinch");

  - platform: gpio
    pin:
      number: GPIO18 # ORANGE WIRE FORMERLY GPIO25 # WAS GPIO18
      mode:
        input: true
        pullup: true
    name: "Close Limit Switch"
    id: close_limit_switch
    filters:
      - delayed_on: 3ms
      - delayed_off: 500ms
    on_press:
      then:
        - script.stop: ramp_close_pin
        - script.stop: ramp_open_pin
        - script.execute: hard_stop # braking
        - delay: 400ms
        - script.execute: disable_bridge
        - lambda: id(status_msg).publish_state("Closed");

  - platform: gpio
    pin:
      number: GPIO19 # YELLOW WIRE FORMERLY GPIO33
#      inverted: true
      mode:
        input: true
        pullup: true
    name: "Open Limit Switch"
    id: open_limit_switch
    filters:
      - delayed_on: 3ms
      - delayed_off: 500ms
    on_press:
      then:
        - script.stop: ramp_close_pin
        - script.stop: ramp_open_pin
        - script.execute: hard_stop # braking
        - delay: 400ms
        - script.execute: disable_bridge
        - lambda: id(status_msg).publish_state("Open");


output:
  - platform: gpio
    pin: GPIO12 #Connect to BTS7960 pin R_EN (right Enable) WHITE WIRE
    id: right_enable_pin
  - platform: gpio
    pin: GPIO14 #Connect to BTS7960 pin L_EN (left Enable) PURPLE WIRE
    id: left_enable_pin
  - platform: gpio
    pin: GPIO26 # GREEN WIRE This is Q1, the half bridge output that is connected to the current sensor that should not be PWM'd
    id: close_pin
  - platform: ledc
    pin: GPIO27 # BLUE WIRE
    id: open_pin
    frequency: 19531
    channel: 1 # Channel 0 and 1 use the same timer

# HA Front End Buttons
switch:
  - platform: template
    id: bark_sensor_enable
    name: Bark Sensor Enable
    optimistic: True
    restore_mode: RESTORE_DEFAULT_ON
#    on_turn_off: 
#      then:
#        - delay: 10min
#        - switch.turn_on: bark_sensor_enable
  
  - platform: template
    id: beep_switch
    name: "Doggy Door Beep Switch"
    optimistic: True
    turn_on_action:
      - script.wait: open_door
      - script.wait: close_door
      - script.execute:
          id: go_beep
          num_beeps: 0

button:
  - platform: template
    id: close_button
    name: "Close Doggy Door"
    on_press:
      if: # Check if the door is not closed yet and close door is not already running
        condition:
          lambda: return id(close_limit_switch).state == false and id(close_door).is_running()==false;
        then:
          - script.stop: open_door
          - script.execute: hard_stop # This includes stopping ramp open/close scripts
          - delay: 400ms # Need to brake before attempting reverse direction otherwise overvoltage power supply with regen
          - script.wait: go_beep # This script can never be allowed to not complete!!!!!!!
          - script.execute: clear_faults
          - script.execute: close_door


  - platform: template
    id: open_button
    name: "Open Doggy Door"
    on_press:
      if: # Check if the door is not open yet
        condition:
          lambda: return id(open_limit_switch).state == false and id(open_door).is_running()==false;
        then:
          - script.stop: close_door
          - script.execute: hard_stop # This includes stopping ramp open/close scripts
          - delay: 400ms # Need to brake before attempting reverse direction otherwise overvoltage power supply with regen          
          - script.wait: go_beep # This script can never be allowed to not complete!!!!!!!
          - script.execute: clear_faults
          - script.execute: open_door

script:
  - id: hard_stop # Commanding open and close pin low at the same time puts the motor windings in short circuit and rapidly brakes motor
    mode: single
    then:
      - script.stop: ramp_close_pin
      - script.stop: ramp_open_pin
      - output.turn_on: right_enable_pin
      - output.turn_on: left_enable_pin      
      - output.turn_off: close_pin
      - output.turn_off: open_pin
      - lambda: id(status_msg).publish_state("Hard stop");

  - id: disable_bridge
    mode: single
    then:
      - output.turn_off: right_enable_pin
      - output.turn_off: left_enable_pin
      - lambda: id(status_msg).publish_state("Disable Bridge");

  - id: enable_bridge
    mode: single
    then:
      - output.turn_on: right_enable_pin
      - output.turn_on: left_enable_pin

  - id: clear_faults
    mode: single
    then:
      - binary_sensor.template.publish:
          id: opening_pinch_fault
          state: OFF
      - binary_sensor.template.publish:
          id: closing_pinch_fault
          state: OFF
      - lambda: id(status_msg).publish_state("Faults Cleared");  

  - id: go_beep
    mode: restart
    parameters:
      num_beeps: int
    then:
      - script.execute: disable_bridge
      - delay: 50ms
      - script.execute: hard_stop
      - output.ledc.set_frequency:
          id: open_pin
          frequency: 2000
      - output.turn_off: close_pin
      - output.set_level:
          id: open_pin
          level: 0.04
      - if:
          condition:
            lambda: return num_beeps == 0;
          then:
            - script.execute: enable_bridge
            - lambda: id(status_msg).publish_state("Beeping");
            - wait_until:
                condition:
                  switch.is_off: beep_switch
      - if:
          condition:
            lambda: return num_beeps > 0;
          then:
            - repeat:
                count: !lambda return num_beeps;
                then:
                  - output.turn_off: close_pin
                  - output.set_level:
                      id: open_pin
                      level: 0.04
                  - script.execute: enable_bridge
                  - lambda: id(status_msg).publish_state("Beeping");
                  - delay: 725ms
                  - script.execute: disable_bridge
                  - lambda: id(status_msg).publish_state("Stopped Beeping");
                  - delay: 725ms
      - script.execute: disable_bridge
      - script.execute: hard_stop
      - output.ledc.set_frequency:
          id: open_pin
          frequency: 19531
      - lambda: id(status_msg).publish_state("Stopped Beeping");
        
  - id: open_door
    mode: restart # If this script is called while it is already running it will restart at the beginning
    then:
      - if: # Check to see if door is already open, if it is then end the script
          condition: 
            lambda: 'return id(open_limit_switch).state == true;'
          then:
            - script.stop: open_door
          else:      
            - script.wait: go_beep
            - script.execute: disable_bridge
            - output.turn_off: close_pin
            - output.turn_off: open_pin
            - delay: 10ms
            - if: # Check if the open door script is being called because of a closing pinch fault, if it is then quickly open door, if not then slow ramp open
                condition:
                  binary_sensor.is_on: closing_pinch_fault
                then:
                  - output.turn_on: open_pin
                  - output.set_level:
                      id: open_pin
                      level: 0.6
                else:
                  if: # Check to see if door is fully closed or partially open already, ramp quickly if fully closed, slowly open if already partial open
                    condition:
                      lambda: 'return id(close_limit_switch).state == true;'
                    then:  
                      - script.execute: ramp_open_pin
                    else:
                      - output.turn_on: open_pin
                      - output.set_level:
                          id: open_pin
                          level: 0.40   
            - delay: 10ms # Wait for PWM to stabilize     
            - script.execute: enable_bridge
            - wait_until: # Wait 1500ms for the current to stabilize but monitor the limit switch while waiting
                condition:
                  lambda: return id(open_limit_switch).state;
                timeout: 1500ms            
            - wait_until: # Wait 15s for either overcurrent or limit switch
                condition:
                  lambda: return id(mtr_current).state > id(open_current_limit).state or id(open_limit_switch).state;
                timeout: 15s
            - script.stop: ramp_open_pin # Redundant in case of limit switch (defined in definition of GPIO) but needed in the case of overcurrent       
            - if:
                condition:
                  lambda: return id(mtr_current).state > id(open_current_limit).state;  #Current too high! Pinching something!
                then:
                  - binary_sensor.template.publish:
                      id: opening_pinch_fault
                      state: ON 
                  - script.execute: hard_stop # braking
                  - delay: 400ms
                  - lambda: id(status_msg).publish_state("Open Pinch");
                  - script.execute: disable_bridge
                  - output.turn_on: close_pin
                  - output.turn_on: open_pin                   
                  - output.set_level:
                      id: open_pin
                      level: 0.6 # since the close pin is high this is 100%-60%=40% duty closing
                  - delay: 10ms # Wait for PWM to stabilize   
                  - script.execute: enable_bridge   
                  - delay: 2s # Slowly close door for 2 seconds to unpinch
                  - script.execute: disable_bridge # gentle off
                  - delay: 5ms
                  - script.execute: hard_stop
                  #Increase the open current limit
                  - lambda: |-
                      id(open_current_limit).publish_state(id(open_current_limit).state + 0.1);
                  - lambda: id(go_beep)->execute(4);
                else:  
                  - number.decrement:  #decreases by the step size
                      id: open_current_limit
                      cycle: false

#-------------------------------------------------------------------------------------------------------------------            
  - id: close_door
    mode: restart # If this script is called while it is already running it will restart at the beginning
    then:
      - if: # Check to see if door is already closed, if it is then end the script
          condition:
            lambda: 'return id(close_limit_switch).state == true;'
          then:
            - script.stop: close_door
          else:
            - script.wait: go_beep
            - script.execute: disable_bridge
            - output.turn_on: close_pin # Previously used to be turn off open pin but inverted to "on" on close pin and instead pwm just the open pin only at (1-duty)
            - output.turn_on: open_pin
            - delay: 10ms
            - wait_until:
                condition:
                  lambda: return id(mtr_current).state < 0.005;
                timeout: 2s
            - if:
                condition:
                  lambda: return id(mtr_current).state > 0.005;
                then:
                  - lambda: id(status_msg).publish_state("Current sensor stuck high");
                  - script.execute:
                      id: go_beep
                      num_beeps: 8
                  - script.stop: close_door    
            - script.execute: ramp_close_pin # Ramping the close pin is no longer actually ramping the duty of the close pin but rather inversly ramping open pin duty
            - delay: 10ms # Wait for PWM to stabilize     
            - script.execute: enable_bridge
            - wait_until: # Wait 1500ms for current to stabilize while watching the state of the limit switch
                condition:
                  lambda: return id(close_limit_switch).state;
                timeout: 1500ms
            - if:
                condition:
                  lambda: return id(mtr_current).state < 0.5;
                then:
                  - lambda: id(status_msg).publish_state("Current sensor stuck low");
                  - script.execute:
                      id: go_beep
                      num_beeps: 8
                  - script.stop: close_door       
            - wait_until:
                condition:
                  lambda: return id(mtr_current).state > id(close_current_limit).state or id(close_limit_switch).state;
                timeout: 15s
            - script.stop: ramp_close_pin # This is redundant, it is already stopped in the limit switch pin definition????
            - if:
                condition:
                  lambda: return id(mtr_current).state > id(close_current_limit).state;  #Current too high! Pinching something!
                then:
                  - script.execute: hard_stop # braking
                  - delay: 400ms
                  - binary_sensor.template.publish:
                      id: closing_pinch_fault
                      state: ON
                  - lambda: id(status_msg).publish_state("Close Pinch");  
                  - script.execute: open_door
                  - script.wait: open_door # This line may be blocking the entire program and causing the door to be unreponsive?
                  - lambda: |-
                      id(close_current_limit).publish_state(id(close_current_limit).state + 0.1);
                  - script.execute:
                      id: go_beep
                      num_beeps: 4
                else:
                  - number.decrement:  #decreases by the step size
                      id: close_current_limit
                      cycle: false


  - id: ramp_close_pin
    mode: restart
    then:
      - output.turn_on: open_pin # documentation says must turn on before setting level and freq
      - output.set_level:
          id: open_pin
          level: 0.99 # inverted to 1 - 0.01
      - lambda: id(open_pin_pwm_duty) = 0.99;
      - lambda: id(status_msg).publish_state("Ramp close");
      - while:
          condition: # inverted to 1 - 0.01
            lambda: return 1 - id(open_pin_pwm_duty) < id(mtr_pwr_level).state - 0.02; # -0.02 is a forgiving tolerance 
          then:
            - output.set_level:
                id: open_pin
                level: !lambda |-
                    static float current_level = 0.99;
                    float increment = (id(mtr_pwr_level).state - 0.01) / 60.0;
                    id(open_pin_pwm_duty) -= increment; 
                    if (1- id(open_pin_pwm_duty) > id(mtr_pwr_level).state) id(open_pin_pwm_duty) = 1 - id(mtr_pwr_level).state; 
                    return id(open_pin_pwm_duty);
            - delay: 15ms
      - lambda: id(status_msg).publish_state("Fast Close");
      - delay: 7200ms
      - while:
          condition:
            lambda: return 1- id(open_pin_pwm_duty) > 0.40 + 0.02; # 0.02 is a forgiving tolerance # inverted to 1 - 0.01
          then:
            - output.set_level:
                id: open_pin
                level: !lambda |-
                    static float current_level = 0.99;
                    float increment = (id(mtr_pwr_level).state - 0.40) / 30.0;
                    id(open_pin_pwm_duty) += increment;
                    if (1 - id(open_pin_pwm_duty) < 0.40) id(open_pin_pwm_duty) = 1 - 0.40;
                    return id(open_pin_pwm_duty);
            - delay: 30ms
      - lambda: id(status_msg).publish_state("Slow close");

  - id: ramp_open_pin
    mode: restart
    then:
      - output.turn_on: open_pin # documentation says must turn on before setting level and freq
      - output.set_level:
          id: open_pin
          level: 0.01
      - lambda: id(open_pin_pwm_duty) = 0.01;
      - lambda: id(status_msg).publish_state("Ramp open");
      - while:
          condition:
            lambda: return id(open_pin_pwm_duty) < id(mtr_pwr_level).state - 0.02; # -0.02 is a forgiving tolerance
          then:
            - output.set_level: # Divide into 50 steps * 25ms/step = 1250ms
                id: open_pin
                level: !lambda |-
                    static float current_level = 0.01;
                    float increment = (id(mtr_pwr_level).state - 0.01) / 50.0;
                    id(open_pin_pwm_duty) += increment;
                    if (id(open_pin_pwm_duty) > id(mtr_pwr_level).state) id(open_pin_pwm_duty) = id(mtr_pwr_level).state;
                    return id(open_pin_pwm_duty);
            - delay: 25ms
      - lambda: id(status_msg).publish_state("Fast Open");
#      - delay: 2650ms # Was 2750ms
      - delay: !lambda |-
          // in milliseconds should give 5500ms for 0.6 mtr level and 2650ms for 0.95 level
          // mtr_pwr_level is between 0.01 and 0.99, so this will result in a positive integer
          int delay_ms = (int)(10386 - 8142 * id(mtr_pwr_level).state);
          return delay_ms;
      - while:
          condition:
            lambda: return id(open_pin_pwm_duty) > 0.15 + 0.02; # 0.02 is a forgiving tolerance
          then:
            - output.set_level: # 50 Steps at 25ms/step = 1250ms to get from mtr_pwr_level to 0.15 duty
                id: open_pin
                level: !lambda |-
                    static float current_level = 0.01;
                    float increment = (id(mtr_pwr_level).state - 0.15) / 50.0;
                    id(open_pin_pwm_duty) -= increment;
                    if (id(open_pin_pwm_duty) < 0.40) id(open_pin_pwm_duty) = 0.40;
                    return id(open_pin_pwm_duty);
            - delay: 25ms
      - lambda: id(status_msg).publish_state("Slow open");

