Skip to content

Latest commit

 

History

History
1229 lines (1105 loc) · 42 KB

README.md

File metadata and controls

1229 lines (1105 loc) · 42 KB

Desky-Config

Config for Desky custom control panel

####################################################################################################
##Notes/Info
####################################################################################################
## Home Assistant Thread: https://community.home-assistant.io/t/desky-standing-desk/383790/3
## ssieb who generously donated his time and knowledge to developing the custom component may offer some support on the Discord thread (please read both threads first).
## https://discord.com/channels/429907082951524364/952464939480645642

#Notes:
# 1. Use this component at own risk. It has not had extensive testing. May void warranty or cause desk faults etc.
# 2. This controller may bypass desk safety features like collision detection.
# 3. This component doesn't know about the values stored in your desk. Values in this config are (and must be) set independently.

#Troubleshooting Tips: 
#If you ever get a flashing ASr message, you might need to reset your desk.
#https://desky.com.au/blogs/news/reset-standing-desk-control-panel#:~:text=When%20ready%2C%20press%20and%20hold,Hooray!

#TODO
  # Test collision detection
  # Smooth stop

substitutions:
  devicename: desky-custom-control-panel
  friendly_name: Desky
  device_description: Desky Standing Desk Wifi Control Panel
############################################################################
#Pin Mappings - Found it easy to organise them up here during dev.
#Priority 1 Pins: https://www.youtube.com/watch?v=LY-1DHTxRAk&t=360s
############################################################################
#Desky
  desky_request_height_pin: GPIO32 #Request desk height | white wire
  desky_rx_pin: GPIO34 #Receive height messages from controller | brown wire
  desky_up_pin: GPIO05  #Move desk up | green wire
  desky_down_pin: GPIO23 #Move desk down | yellow wire
  
#Other
  keyboard_drawer_endstop_height_pin: GPIO03 #Desk Keyboard Drawer Endstop | Labelled RX. 
  i2c_screen2_scl_pin: GPIO18 #Screen 2
  i2c_screen2_sda_pin: GPIO19 #Screen 2
  i2c_bus1_sda_pin: GPIO21 #many devices share i2c_bus1 
  i2c_bus1_scl_pin: GPIO22 #many devices share i2c_bus1 
  led_red_pin: GPIO25 #Red left led
  led_green_pin: GPIO26 #Green right led
  button1_left_pin: GPIO33 #Left push button
  button2_right_pin: GPIO02 #Right push button
  rttl_buzzer_pin: GPIO27 #RTTL buzzer
  pir_pin: GPIO35 #Motion Sensor
  
 
esphome:
  name: $devicename
  comment: ${device_description}
  platform: ESP32
  board: ttgo-t7-v14-mini32   
  #LILYGO TTGO T7 V1.5
  #pinout: https://ae01.alicdn.com/kf/H32f84415f675426b84fdae1a7809eef2r/LILYGO-TTGO-T7-Mini32-V1-5-ESP32-WROVER-B-Dual-Core-PSRAM-Wireless-Wi-Fi-Bluetooth.jpg

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  manual_ip:
      static_ip: 192.168.1.63
      gateway: 192.168.1.1
      subnet: 255.255.255.0

api:
ota:

logger:
    baud_rate: 0
    # level: VERY_VERBOSE ##Uncomment if using UART debug

external_components:
#Fetch ssieb's custom component: https://github.com/ssieb/custom_components/tree/master/components/desky
  - source:
      type: git
      url: https://github.com/ssieb/custom_components
    components: [ desky ]

# esp32_ble_tracker:
# #For tracking smart band presence
  # scan_parameters:
    # interval: 320ms
    # window: 30ms

uart:
  - id: desk_uart
    baud_rate: 9600
    rx_pin: ${desky_rx_pin} #Brown Wire
    # #You can uncomment the debug section below to see UART messages.
    # debug:
      # direction: RX
      # dummy_receiver: true
      # after:
        # bytes: 4
      # sequence:     
        # - lambda: UARTDebug::log_int(direction, bytes, ',');


##########################################################################################
# Desky custom component
##########################################################################################  
desky:
  id: my_desky
  up:  # green wire
    number: ${desky_up_pin}
    inverted: true 
  down: #yellow wire
    number: ${desky_down_pin}
    inverted: true
  request:  #Request height update at boot so height is known after reboot | #White Wire
    number: ${desky_request_height_pin}
    inverted: true
  stopping_distance: 15  # optional offset distance from target to turn off moving in mm (default 15mm).
  timeout: 15s  # Optional time limit for moving, default is none (safety feature). 15sec is more than time to move from lowest point to highest point.
  height:  # Sensor publishing the current height. Make this internal and process another copy for sending to HA.
    name: Desky Height
    id: desky_height
    accuracy_decimals: 0
    internal: true
    filters:
    - delta: 0.1 #Only pass values if they change
    
text_sensor:
# Reports the ESPHome Version with compile date
  - platform: version
    name: ${friendly_name} ESPHome Version
    #Tracks last movement direction to help with the one-touch sit/stand toggle button.
  - platform: template
    id: desky_last_movement_direction 
    name: "Desky Last Movement Direction"
    icon: "mdi:arrow-up-down-bold"
    
    #Infers a sit/stand type category from height
  - platform: template
    id: desky_current_sit_stand_position
    name: "Desky Current Sit or Stand Position"
    icon: "mdi:seat-recline-extra"
    lambda: |-
      return {};
    on_value:
    #Reset stopwatch (tracks time in current state)
    - button.press: desky_start_stopwatch
    
    #Makes near/far type categories from the ToF sensor (to help with desk occupancy detection)
  - platform: template
    id: desky_tof_proximity_category
    name: "Desky Tof Proximity Category"
    icon: "mdi:arrow-expand-horizontal"
   
######################
#Buzzer
######################    
rtttl:
  output: rtttl_out
  on_finished_playback:
    - logger.log: 'Song ended!'

output:
#Rttl buzzer
  - platform: ledc
    pin: ${rttl_buzzer_pin} 
    id: rtttl_out
    
######################
#LEDs
######################  
  #Red
  - platform: ledc
    id: desky_red_led
    pin: ${led_red_pin} 
  #Green
  - platform: ledc
    id: desky_green_led
    pin: ${led_green_pin}

light:
  - platform: monochromatic
    name: "Desky Left Red LED"
    output: desky_red_led
    id: desky_light_led_left_red
    internal: true
  - platform: monochromatic
    name: "Desky Right Green LED"
    output: desky_green_led
    id: desky_light_led_right_green
    internal: true
  
##########################################################################################
# I2C
##########################################################################################
i2c:
#Main bus
  - id: i2c_bus1 #Screen 1, tof
    scan: true
    scl: ${i2c_bus1_scl_pin} #clock blue wire
    sda: ${i2c_bus1_sda_pin} #data green wire
      
#Had to use another bus for second screen as couldn't change address.
  - id: i2c_bus2 #Screen 2
    scan: true
    scl: ${i2c_screen2_scl_pin} #clock blue wire
    sda: ${i2c_screen2_sda_pin} #data green wire
   
binary_sensor:
#Someone is sitting? Sit dection using a multi-pronged approach.
  - platform: template
    id: someone_is_sitting
    name: Someone Is Sitting
    icon: "mdi:seat-recline-normal"   
    #Motion is detected, ToF is in range, keyboard drawer is open, desk height is in range
    lambda: |-
      return
      id(desky_motion_detected_processed).state &&
      id(desky_tof_seems_occupied).state &&
      id(desky_keyboard_drawer_endstop).state &&
      (id(desky_current_sit_stand_position).state == "Sitting")
      ;
    on_press:
    #Red light on if sitting is detected.
      then:
        - delay: 2s
        - light.turn_on:
            id: desky_light_led_left_red
            brightness: 20%      
    on_release:
    #When it turns off, check if "simple sit detection" is still on. Helps diagnose ToF/PIR false negatives.
      then:
        - light.turn_off:
            id: desky_light_led_left_red
        - delay: 1s
        - while:
          #If presence isn't detected (PIR/ToF) but height is in range and keyboard drawer is out, slow flash red led
            condition:
              and:
                - binary_sensor.is_on: desky_simple_sit_detection
                - binary_sensor.is_off: someone_is_sitting
            then:
              - light.turn_on:
                    id: desky_light_led_left_red
                    brightness: 100%
              - delay: 1s
              - light.turn_off:
                    id: desky_light_led_left_red
              - delay: 1s
          
#Someone is sitting? (Simpler method)
  - platform: template
    id: desky_simple_sit_detection
    name: Desky Simple Sit Detection
    icon: "mdi:seat-passenger"
    #On if height is in sitting range and keyboard drawer is out.
    lambda: |-
      return
      id(desky_keyboard_drawer_endstop).state &&
      (id(desky_current_sit_stand_position).state == "Sitting")
      ;
    on_press:
       - switch.turn_on: desky_auto_stand_mode    
       - delay: 2s
       - while:
       #If presence isn't detected and "simple sit" is, slow flash red led.
            condition:
              and:
                - binary_sensor.is_on: desky_simple_sit_detection
                - binary_sensor.is_off: someone_is_sitting
            then:
              - light.turn_on:
                    id: desky_light_led_left_red
                    brightness: 100%
              - delay: 1s
              - light.turn_off:
                    id: desky_light_led_left_red
              - delay: 1s
                
#Is Desky in motion? 
#Only works if activated by move_to function.
  - platform: template
    id: desky_is_moving
    name: Desky Is Moving
    lambda: return id(my_desky).current_operation != desky::DESKY_OPERATION_IDLE;
    icon: "mdi:motion"
    
#PIR Motion detection. Raw/unprocessed.     
  - platform: gpio
    id: desky_motion_detected
    pin: ${pir_pin} 
    name: "Desky PIR Sensor"
    device_class: motion
    icon: "mdi:motion-sensor"
    internal: true
        
#Is motion detected? Processed. Add timeout.
  - platform: copy
    source_id: desky_motion_detected
    id: desky_motion_detected_processed
    name: "Desky PIR Sensor Processed"
    icon: "mdi:motion-sensor"
    filters:
      - delayed_off: 120s #Largish timeout worked best.
      
  - platform: template
    id: desky_tof_seems_occupied
    name: Desky ToF Seems Occupied
    #On if ToF is in range
    lambda: return id(desky_tof_proximity_category).state == "At Desk";
    filters:
      - delayed_off: 60s #Timeout helps with false negatives.

  # - platform: ble_presence
    # mac_address: !secret mb_mi_band_mac_address 
    # id: desky_mb_mi_band_detected
    # name: "Desky MB Mi Band Detected"

         
###############################################################
#Keyboard Drawer (Reed switch  based endstop)
###############################################################
  - platform: gpio
    pin:
      number: ${keyboard_drawer_endstop_height_pin}
      mode: INPUT_PULLUP
    name: "Desky Keyboard Drawer Endstop"
    id: "desky_keyboard_drawer_endstop"
    internal: FALSE
    device_class: opening
    icon: "mdi:keyboard-close"
    filters:
      - delayed_on_off: 1s #Debounce
    on_state:
      #Make a noise
      - script.execute: short_beep_buzzer
      #Reset stopwatch
      - button.press: desky_start_stopwatch
    on_release:
    #Keyboard has been stowed under desk. Move desk to height so I can stow my chair too.
      then:
        - delay: 2s
        - button.press: desky_move_to_stow_height1
    on_press:
    #Keyboard has been pulled out for use. Move to sit height.
      then:
        - delay: 2s
        - button.press: desky_move_to_sit_height1

###############################################################
#Buttons
###############################################################
#Button 1 - Left hand side
#Main function: One press toggle sit/stand
  - platform: gpio
    pin:
      number: ${button1_left_pin}
      mode: INPUT_PULLUP
    name: "Desky Toggle Sit/Stand"
    id: "desky_button1_left_toggle_sit_stand"
    internal: true
    icon: "mdi:swap-vertical-bold"
    filters:
      - invert:
    on_press:
      then:
        #Make a noise.
        - script.execute: short_beep_buzzer
        #One touch sit/stand toggle
        - button.press: toggle_sit_stand

#Button 2 - Right hand side
  - platform: gpio
    pin:
      number: ${button2_right_pin}
      mode: INPUT_PULLUP
    name: "Desky Toggle Auto Up Mode"
    id: "desky_button2_right_toggle_auto_up_mode"
    internal: true
    icon: "mdi:radiobox-marked"
    filters:
      - invert:
    on_multi_click: #Long press to toggle auto stand mode
    - timing:
          - ON for at least 1s
      then:
        - switch.toggle: desky_auto_stand_mode
    on_press:
    #Make a noise. Just to know the leading edge is working ok.
    - script.execute: short_beep_buzzer
    #Extend countdown    
    - if:
        condition:
          lambda: |-
            return
            int(id(countdown_timer_remaining_time)) <= int(id(countdown_warning_threshold)) &&
            id(countdown_timer_remaining_time) >= 0
            ;

        then:
        - globals.set:
            id: countdown_timer_remaining_time
            value: !lambda return id(countdown_extention).state;
        # Show the remaining time.
        - sensor.template.publish:
            id: desky_minutes_until_stand_up
            state: !lambda return id(countdown_extention).state;
        - delay: 1s
        - light.turn_on:
            id: desky_light_led_right_green
            brightness: 20%

    
# ##############################################################
# Screens
# ##############################################################
# ##############
# Fonts
# ##############
font:
  - file: "fonts/Roboto-Bold.ttf"
    id: roboto10
    size: 10
  - file: "fonts/Roboto-Bold.ttf"
    id: roboto12
    size: 12
  - file: "fonts/Roboto-Bold.ttf"
    id: roboto16
    size: 16
  - file: "fonts/Roboto-Bold.ttf"
    id: roboto30
    size: 30
  - file: "fonts/Roboto-Bold.ttf"
    id: roboto40
    size: 40
 
#Icons: https://community.home-assistant.io/t/display-materialdesign-icons-on-esphome-attached-to-screen/199790/13
 
  - file: "fonts/materialdesignicons-webfont.ttf"
    id: matdes11
    size: 11
    glyphs:
      - "\U000F075F" # mdi-volume-mute
      - "\U000F057E" # mdi-volume-high
      - "\U000F0D91" #mdi-motion-sensor
      - "\U000F1435" #mdi-motion-sensor-off
      - "\U000F0E64" #mdi-signal-distance-variant (ToF)
      - "\U000F030F" #mdi-keyboard-close
      - "\U000F1249" #mdi-seat-passenger
      - "\U000F02E6" #mdi-human 
      - "\U000F0792" #mdi-arrow-collapse-down
      - "\U000F05E0" #mdi-check-circle
      - "\U000F073A" #mdi-cancel
    
  - file: "fonts/materialdesignicons-webfont.ttf"
    id: matdes30
    size: 30
    glyphs:
         
      - "\U000F075F" # mdi-volume-mute
      - "\U000F057E" # mdi-volume-high
      - "\U000F0D91" #mdi-motion-sensor
      - "\U000F1435" #mdi-motion-sensor-off
      - "\U000F0E64" #mdi-signal-distance-variant (ToF)
      - "\U000F030F" #mdi-keyboard-close
      - "\U000F1249" #mdi-seat-passenger
      - "\U000F02E6" #mdi-human 
      - "\U000F0792" #mdi-arrow-collapse-down
      - "\U000F05E0" #mdi-check-circle
      - "\U000F073A" #mdi-cancel
      - "\U000F04B9" #mdi-sofa
      - "\U000F154B" #mdi-sort-clock-descending
      - "\U000F0513" #mdi-thumb-up
    
  - file: "fonts/materialdesignicons-webfont.ttf"
    id: matdes40
    size: 40
    glyphs:
      - "\U000F04B9" #mdi-sofa
      - "\U000F154B" #mdi-sort-clock-descending
      - "\U000F0513" #mdi-thumb-up
      
########################################################
#Displays
########################################################
#SSD1306 128x64 Display | 3.3V | https://esphome.io/components/display/ssd1306.html
display:

# Screen 1 - Left hand side     
  - platform: ssd1306_i2c
    model: "SSD1306 128x64"
    id: desky_display_1_left
    i2c_id: i2c_bus2
    update_interval: 5s
    address: 0x3C
    pages:
      - id: desky_display_1_left_page1
        lambda: |-
          static int num_icons = 6;
          static int left_offset_1 = -1;
          static int top_offset_1 = 1;
          static int vertical_padding = 0.8;
          static int left_offset_2 = left_offset_1 + 13;
          
          //Sitting detection helpers: text describers
          it.printf(left_offset_2, 1*(it.get_height() / num_icons)-3, id(roboto12), TextAlign::CENTER_LEFT , "Sitting");
          it.printf(left_offset_2, 2*(it.get_height() / num_icons) + top_offset_1 + (1 * vertical_padding), id(roboto10), TextAlign::CENTER_LEFT , "PIR");
          it.printf(left_offset_2, 3*(it.get_height() / num_icons) + top_offset_1 + (2 * vertical_padding), id(roboto10), TextAlign::CENTER_LEFT , "ToF");
          it.printf(left_offset_2, 4*(it.get_height() / num_icons) + top_offset_1 + (3 * vertical_padding), id(roboto10), TextAlign::CENTER_LEFT , "Height");
          it.printf(left_offset_2, 5*(it.get_height() / num_icons) + top_offset_1 + (4 * vertical_padding), id(roboto10), TextAlign::CENTER_LEFT , "Keyboard");
          it.printf(left_offset_2, 6*(it.get_height() / num_icons) + top_offset_1 + (5 * vertical_padding), id(roboto10), TextAlign::CENTER_LEFT , "BandXX");
          
          //Sitting detection helpers: Icons (idicate on/off)
          
          //Overall sitting detection     
          if (id(someone_is_sitting).state) {
            it.print(left_offset_1, 1*(it.get_height() / num_icons)-3, id(matdes11), TextAlign::CENTER_LEFT, "\U000F05E0");
          } else {
            it.print(left_offset_1, 1*(it.get_height() / num_icons)-3, id(matdes11), TextAlign::CENTER_LEFT, "\U000F073A");
          }
          
          //PIR
          if (id(desky_motion_detected_processed).state) {
            it.print(left_offset_1, 2*(it.get_height() / num_icons) + top_offset_1 + (1 * vertical_padding), id(matdes11), TextAlign::CENTER_LEFT, "\U000F05E0");
          } else {
            it.print(left_offset_1, 2*(it.get_height() / num_icons) + top_offset_1 + (1 * vertical_padding), id(matdes11), TextAlign::CENTER_LEFT, "\U000F073A");
          }
          
          //Occupancy from ToF
          if (id(desky_tof_seems_occupied).state) {
            it.print(left_offset_1, 3*(it.get_height() / num_icons) + top_offset_1 + (2 * vertical_padding), id(matdes11), TextAlign::CENTER_LEFT, "\U000F05E0");
          } else {
            it.print(left_offset_1, 3*(it.get_height() / num_icons) + top_offset_1 + (2 * vertical_padding), id(matdes11), TextAlign::CENTER_LEFT, "\U000F073A");
          }
          
          //Sit or stand from height
          if (id(desky_current_sit_stand_position).state == "Sitting") {
            it.print(left_offset_1, 4*(it.get_height() / num_icons) + top_offset_1 + (3 * vertical_padding), id(matdes11), TextAlign::CENTER_LEFT, "\U000F05E0");
          } else {
            it.print(left_offset_1, 4*(it.get_height() / num_icons) + top_offset_1 + (3 * vertical_padding), id(matdes11), TextAlign::CENTER_LEFT, "\U000F073A");
          }
          
          //Keyboard
          if (id(desky_keyboard_drawer_endstop).state) {
            it.print(left_offset_1, 5*(it.get_height() / num_icons) + top_offset_1 + (4 * vertical_padding), id(matdes11), TextAlign::CENTER_LEFT, "\U000F05E0");
          } else {
            it.print(left_offset_1, 5*(it.get_height() / num_icons) + top_offset_1 + (4 * vertical_padding), id(matdes11), TextAlign::CENTER_LEFT, "\U000F073A");
          }
          
          //Show sit/stand categories and time in current state
           it.printf(it.get_width()*0.75, 6, id(roboto16), TextAlign::CENTER , id(desky_current_sit_stand_position).state.c_str());
           it.printf(it.get_width()*0.75,33, id(roboto40), TextAlign::CENTER , "%.0f", id(desky_stopwatch).state);
           it.printf(it.get_width()*0.75, 57, id(roboto12), TextAlign::CENTER , "Minutes");          
          
  # //BLE Mi Band
  # if (id(desky_keyboard_drawer_endstop).state) {
    # it.print(left_offset_1, 6*(it.get_height() / num_icons) + top_offset_1 + (5 * vertical_padding), id(matdes11), TextAlign::CENTER_LEFT, "\U000F05E0");
  # } else {
    # it.print(left_offset_1, 6*(it.get_height() / num_icons) + top_offset_1 + (5 * vertical_padding), id(matdes11), TextAlign::CENTER_LEFT, "\U000F073A");
  # }
          

                
          
# Screen 1 - Right hand side
  - platform: ssd1306_i2c
    model: "SSD1306 128x64"
    id: desky_display_2_right
    i2c_id: i2c_bus1
    update_interval: 5s
    address: 0x3C
    pages:
      - id: desky_display_2_right_page1
        lambda: |-
          //Show either countdown to standing (if sitting) or "lazy mode"
          if (id(desky_auto_stand_mode).state) 
            {
              if (id(desky_simple_sit_detection).state)
                {
                  it.printf(it.get_width()/2, -2, id(roboto16), TextAlign::TOP_CENTER , "Auto Stand In:"); 
                  it.printf(it.get_width()*0.75, 35, id(roboto40), TextAlign::CENTER , "%.0f", id(desky_minutes_until_stand_up).state); 
                  it.printf(it.get_width()*0.75, 57, id(roboto12), TextAlign::CENTER , "Minutes");
                  it.print((it.get_width()*0.25)+15, it.get_height()/2+10, id(matdes40), TextAlign::CENTER, "\U000F154B");
                }
              else
                {
                  it.printf(it.get_width()/2, -2, id(roboto16), TextAlign::TOP_CENTER , "Not On Arse"); 
                  it.print((it.get_width()/2), it.get_height()/2+10, id(matdes40), TextAlign::CENTER, "\U000F0513");
                }
              
            }
          else 
            {
              it.printf(it.get_width()/2, -2, id(roboto16), TextAlign::TOP_CENTER , "Auto Stand: OFF");
              it.print(it.get_width()/2, (it.get_height()/2)-2, id(matdes40), TextAlign::CENTER, "\U000F04B9");         
              it.print(it.get_width()/2, 55, id(roboto16), TextAlign::CENTER , "Lazy Mode !!");
            }

sensor:
#############################################
#Time of Flight  / Distance
#############################################
#VCC connects to 3V3 | https://esphome.io/components/sensor/vl53l0x.html
  - platform: vl53l0x
    i2c_id: i2c_bus1
    id: desky_tof_distance
    icon: mdi:arrow-expand-horizontal
    name: "Desky ToF Distance"
    address: 0x29
    update_interval: 1s
    long_range: true #Seemed to be required with my distances.
    filters:
      - multiply: 100 #Convert to cm
      - median: #Use a moving median for spike smoothing.
          window_size: 5
          send_every: 5
    unit_of_measurement: "cm"
    accuracy_decimals: 0 #Keep it simple. To the cm.
    on_value:
      then:
        - lambda: |-
            if ((id(desky_tof_distance).state) < 20) { return id(desky_tof_proximity_category).publish_state("Very Close");}
            else if ((id(desky_tof_distance).state) < 50) { return id(desky_tof_proximity_category).publish_state("Something Quite Close");}
            else if ((id(desky_tof_distance).state) < 77) { return id(desky_tof_proximity_category).publish_state("At Desk");}
            else if ((id(desky_tof_distance).state) >= 77) { return id(desky_tof_proximity_category).publish_state("Something Quite far");}
            else { return id(desky_tof_proximity_category).publish_state("Unknown");}
 
##############################
#Desk Height post processing
##############################
  - platform: copy
    source_id: desky_height
    id: desky_height_cm
    name: "Desky Desk Height (cm)"
    accuracy_decimals: 1
    internal: false
    icon: mdi:arrow-expand-vertical
    unit_of_measurement: cm
    filters:
    - delta: 0.1 #Only send values to HA if they change    
    - throttle: 250ms #Limit values sent to Ha to 4 per sec.    
    - multiply: 0.1 #convert from mm to cm
    on_value:
      then:
        #Track the last movement direction for use later
        - lambda: |-
            if (id(my_desky).current_operation == desky::DESKY_OPERATION_RAISING){ return id(desky_last_movement_direction).publish_state("Up");}
            else if (id(my_desky).current_operation == desky::DESKY_OPERATION_LOWERING){return id(desky_last_movement_direction).publish_state("Down"); }
        #Track a sit/stand categorisation of desk height for use later
        - lambda: |-
            if ((id(desky_height_cm).state) < (id(desky_sit_height_1).state + 5.0)) { return id(desky_current_sit_stand_position).publish_state("Sitting");}
            else if ((id(desky_height_cm).state) > (id(desky_stand_height_1).state - 5.0)) { return id(desky_current_sit_stand_position).publish_state("Standing");}
            else if ((id(desky_height_cm).state) < (id(desky_stow_height_1).state + 5.0)) { return id(desky_current_sit_stand_position).publish_state("Stowed");}
            else { return id(desky_current_sit_stand_position).publish_state("Other");}

 
################################################
# Countdown Timer
################################################
 # Countdown timer until you have to get off yo butt. 
  - platform: template
    name: Desky Minutes Until Stand Up
    id: desky_minutes_until_stand_up
    accuracy_decimals: 0
    unit_of_measurement: min
    icon: mdi:timer
    update_interval: 1s
    lambda: return id(countdown_timer_remaining_time);
    filters:
    - delta: 0.1
    on_value_range:
    #Warning if desky is approaching auto movement.
      - above: 0
        below: !lambda return id(countdown_warning_threshold).state;
        then:
          #Audible first early audible warning    
          - rtttl.play:
              rtttl: 'moving_soon:d=32,o=5,b=100:c,c#,d#,e,f#,g#,a#,b'    
          - while:
              condition:
                lambda: |-
                  return
                   id(desky_minutes_until_stand_up).state > 0 &&
                   id(desky_minutes_until_stand_up).state <= id(countdown_warning_threshold).state
                  ;
              then:
                #Quick flash green light if countdown < 10sec  
                - light.turn_on:
                    id: desky_light_led_right_green
                    brightness: 100%
                - delay: 500ms
                - light.turn_off: desky_light_led_right_green
                - delay: 500ms      
    on_value:
    #When countdown hits 0, raise desk and do other stuff.
      then:
        - if:
            condition:
              - lambda: return id(countdown_timer_remaining_time) == 0;
            then:
              - if:
                  condition: #Check if desk is in simple sitting state 
                    - lambda: return (id(desky_simple_sit_detection).state);
                  then:
                    - if:
                        condition: #Check if someone is in front of desk.
                          - lambda: return (id(someone_is_sitting).state);
                        then:
                          - switch.turn_off: desky_sitting_countdown_on
                          - light.turn_on:
                              id: desky_light_led_right_green
                              brightness: 20%
                          #Audible last warning before movement    
                          - rtttl.play:
                              rtttl: 'moving_now:d=32,o=5,b=20:c,c,g' 
                          - delay: 2s
                          - button.press: desky_move_to_stand_height1  
                        else:
                        #Extend time and complain...
                          - globals.set:
                              id: countdown_timer_remaining_time
                              value: !lambda return id(countdown_extention).state;
                          # Show the remaining time.
                          - sensor.template.publish:
                              id: desky_minutes_until_stand_up
                              state: !lambda return id(countdown_extention).state;
                          - delay: 1s
                          - light.turn_on:
                              id: desky_light_led_right_green
                              brightness: 20%
                          - script.execute: multi_beep_buzzer

 
################################################
# Desk State stopwatch. Counts up time in current state.
################################################
  - platform: template
    name: Desky Stopwatch
    id: desky_stopwatch
    accuracy_decimals: 0
    unit_of_measurement: minutes
    icon: mdi:timer
    update_interval: 1s
    lambda: return id(desky_stopwatch_time);
    filters:
    - delta: 0.1


button:
#Single button sit/stand toggle 
  - platform: template
    name: Desky Toggle Sit Stand  
    id: toggle_sit_stand
    icon: mdi:swap-vertical-bold
    on_press: 
    #If moving: Stop
    #If not moving: Do opposite movement as last one.
      then: 
        - lambda: |-
            if (id(my_desky).current_operation != desky::DESKY_OPERATION_IDLE) {id(desky_stop_desk).press();}
            else if (id(desky_last_movement_direction).state == "Up") {return id(desky_move_to_sit_height1).press();}
            else if (id(desky_last_movement_direction).state == "Down") {return id(desky_move_to_stand_height1).press();}
            else if (id(desky_current_sit_stand_position).state == "Standing") {return id(desky_move_to_sit_height1).press();}
            else {return id(desky_move_to_stand_height1).press();}

#Timer: Start Sitting Countdown Timer
  - platform: template
    name: Start Sitting Countdown Timer
    id: desky_intialise_sitting_countdown_timer
    icon: mdi:timer-sand-full
    on_press: 
      then:
        # Start the countdown timer.
        - globals.set:
            id: countdown_timer_remaining_time
            value: !lambda return id(desky_max_sitting_time).state;
        # Show the remaining time.
        - sensor.template.publish:
            id: desky_minutes_until_stand_up
            state: !lambda return id(desky_max_sitting_time).state;

#Start Stopwatch
  - platform: template
    name: Start stop watch
    id: desky_start_stopwatch
    icon: mdi:timer-sand-full
    on_press: 
      then:
        # Start the countdown timer.
        - globals.set:
            id: desky_stopwatch_time
            value: !lambda return 0;
        # Show the remaining time.
        - sensor.template.publish:
            id: desky_stopwatch
            state: !lambda return 0;


#Stop Movement
  - platform: template
    name: Stop Desk
    id: desky_stop_desk
    icon: mdi:pause-octagon
    on_press:
      then:
        - lambda: id(my_desky).stop();
        - delay: 200ms
          #Do it twice for good luck. Might not be needed.
        - lambda: id(my_desky).stop();

# #Move to function
  - platform: template
    name: Move Desky to Target Height
    id: desky_move_to_desky_target_height
    icon: mdi:target
    on_press:
      then:
        - lambda: id(my_desky).move_to(int((id(desky_target_height).state) * 10));  

#Define as many single press presets like this as you like  
  - platform: template
    name: Move Desky to Sit Height 1
    id: desky_move_to_sit_height1
    icon: mdi:seat-passenger
    on_press:
      then:
        - lambda: id(my_desky).move_to(int((id(desky_sit_height_1).state) * 10));  
             
  - platform: template
    name: Move Desky to Stand Height 1
    id: desky_move_to_stand_height1
    icon: mdi:karate
    on_press:
      then:
        - lambda: id(my_desky).move_to(int((id(desky_stand_height_1).state) * 10));  
        
  - platform: template
    name: Move Desky to Stow Height 1
    id: desky_move_to_stow_height1
    icon: mdi:table-chair
    on_press:
      then:
        - lambda: id(my_desky).move_to(int((id(desky_stow_height_1).state) * 10));  
        

number:
#########################################################
#Height Presets
#########################################################

#Target Height ("Move desk to height x").
#You should probably limit the range you can move the desk to to within the limits you've set via the control panel, perhaps offset a little within the range.
#Sending commands higher/lower than this may cause error messages and require desk reset (or maybe worse).
#Target height which can be changed in HA.
  - platform: template
    id: desky_target_height
    icon: mdi:target-variant
    name: "Desky Target Height"
    optimistic: true
    unit_of_measurement: cm
    min_value: 80
    max_value: 130.0
    step: 0.1
    restore_value: true
    initial_value: 85.3

#Various height presets (independant of what you've set via concole)
  - platform: template
    id: desky_stand_height_1
    name: "Desky Stand Height 1"
    icon: mdi:human-male-height
    optimistic: true
    unit_of_measurement: cm
    min_value: 100.0
    max_value: 130.0
    step: 1.0
    restore_value: true
    initial_value: 124.0
    
  - platform: template
    id: desky_sit_height_1
    icon: mdi:seat
    name: "Desky Sit Height 1"
    optimistic: true
    unit_of_measurement: cm
    min_value: 83.0
    max_value: 90.0
    step: 1.0
    restore_value: true
    initial_value: 85.3
    
  - platform: template
    id: desky_stow_height_1
    name: "Desky Stow Height 1"
    icon: mdi:seat-outline
    optimistic: true
    unit_of_measurement: cm
    min_value: 96.0
    max_value: 98.0
    step: 1.0
    restore_value: true
    initial_value: 97.0    

#########################################################
#Sit / stand schedule/timer helpers.
#########################################################    
 
#How long you're allowed to sit for before you'll be "pushed-up" to standing 
  - platform: template
    id: desky_max_sitting_time
    name: "Desky Max Sitting Time"
    icon: mdi:timer-alert
    optimistic: true
    unit_of_measurement: min
    min_value: 5
    max_value: 60
    step: 1 
    restore_value: true
    initial_value: 60
    set_action:
      then:
        # Start the countdown timer.
        - globals.set:
            id: countdown_timer_remaining_time
            value: !lambda return x;
        # Show the remaining time.
        - sensor.template.publish:
            id: desky_minutes_until_stand_up
            state: !lambda return x;
    # # lambda: |-
      # # if 
        # # (id(desky_max_sitting_time).state == id(desky_max_sitting_time).state)
        # # {return {};}
      # # else
         # # {return id(desky_max_sitting_time).to_max();}
    # set_action:
      # then:
        # # Start the countdown timer.
        # - globals.set:
            # id: countdown_timer_remaining_time
            # value: !lambda return id(desky_max_sitting_time).state;
        # # Show the remaining time.
        # - sensor.template.publish:
            # id: desky_minutes_until_stand_up
            # state: !lambda return id(desky_max_sitting_time).state;

    
#When should Desky give you an early heads up it will move soon? 
  - platform: template
    id: countdown_warning_threshold
    icon: mdi:timer-alert
    name: "Countdown warning threshold"
    unit_of_measurement: min
    optimistic: true
    min_value: 2
    max_value: 10
    step: 1
    restore_value: true
    initial_value: 2
    
#Extend the countdown (like for if you're in a meeting etc)
  - platform: template
    id: countdown_extention
    icon: mdi:clock-plus-outline
    name: "Countdown Extention"
    optimistic: true
    unit_of_measurement: min
    min_value: 5
    max_value: 60
    step: 1
    restore_value: true
    initial_value: 3
   
switch:
  - platform: restart
    name: "Restart Desky"
    
#########################################################
#Desky controls
#########################################################
#Wake up the desk and request it sends its height
#The custom component should handle this on boot but this is a manual option. Sometimes it doesn't seem to work on boot. Don't know why yet.
  - platform: gpio
    id: wake_desk_and_get_height
    name: "Request Desk Height"
    pin:
      number: ${desky_request_height_pin}
      inverted: true
    on_turn_on:
    - delay: 100ms
    - switch.turn_off: wake_desk_and_get_height

#Raise the desk | Green Wire
  - platform: gpio
    id: desky_raise_desk
    name: "Desky Raise Desk"
    pin:
      number: ${desky_up_pin}
      inverted: true
    interlock: desky_lower_desk
    on_turn_on:
    #Auto off after 15s just in case
    - delay: 15s
    - switch.turn_off: desky_raise_desk
    
#Lower the desk | Yellow wire
  - platform: gpio
    id: desky_lower_desk
    name: "Desky Lower Desk" 
    pin:
      number: ${desky_down_pin}
      # mode: INPUT_PULLUP
      inverted: true
    interlock: desky_raise_desk
    on_turn_on:
   #Auto off after 15s just in case
    - delay: 15s
    - switch.turn_off: desky_lower_desk

#########################################################
#Push-Up Mode! 
#########################################################
#Switch to turn on/off "Auto Stand Mode"
#If on, desk moves to standing after x minutes of sitting. 
  - platform: template
    name: Desk Auto Stand Mode
    id: desky_auto_stand_mode
    optimistic: true
    restore_state: true
    turn_on_action:
      - light.turn_on:
            id: desky_light_led_right_green
            brightness: 10%
      #Make a noise
      - script.execute: rtttl_play_scale_up
      #Start the timer
      - lambda: |-
          if (id(desky_current_sit_stand_position).state == "Sitting") {
            id(desky_sitting_countdown_on).turn_on();
          }
          
    turn_off_action:
      - light.turn_off: desky_light_led_right_green
      - script.execute: rtttl_play_scale_down
      - switch.turn_off: desky_sitting_countdown_on

  - platform: template
    name: Desky Sitting Countdown On
    id: desky_sitting_countdown_on
    optimistic: true
    internal: FALSE
    turn_on_action:
        - button.press: desky_intialise_sitting_countdown_timer  
    turn_off_action:
    #Set countdown to -1
        - globals.set:
            id: countdown_timer_remaining_time
            value: '-1'
        # Show the remaining time.
        - sensor.template.publish:
            id: desky_minutes_until_stand_up
            state: -1
        
script:
#leds

  - id: short_flash_left_red_led
    then:
      - light.turn_on: desky_light_led_left_red
      - delay: 500ms
      - light.turn_off: desky_light_led_left_red
      
  - id: short_flash_right_green_led 
    then:
      - light.turn_on:
            id: desky_light_led_right_green
            brightness: 20%
      - delay: 500ms
      - light.turn_off: desky_light_led_right_green
      
  - id: short_double_flash_right_green_led 
    then:
      - light.turn_on:
            id: desky_light_led_right_green
            brightness: 15%
      - delay: 500ms
      - light.turn_off: desky_light_led_right_green
      - delay: 500ms
      - light.turn_off: desky_light_led_right_green
      
  - id: short_double_flash_left_red_led
    then:
      - light.turn_on: desky_light_led_left_red
      - delay: 200ms
      - light.turn_off: desky_light_led_left_red
      - delay: 200ms
      - light.turn_off: desky_light_led_left_red
      
  - id: long_flash_desky_light_led_left_red
    then:
      - light.turn_on:
            id: desky_light_led_left_red
            brightness: 10%
      - delay: 1s
      - light.turn_off:
            id: desky_light_led_left_red
      - delay: 1s
      
#Sounds 
  - id: rtttl_play_scale_up
    then:
    - rtttl.play:
        rtttl: 'scale_up:d=32,o=5,b=100:c,c#,d#,e,f#,g#,a#,b'
        
  - id: rtttl_play_scale_down
    then:
    - rtttl.play:
        rtttl: 'scale_down:d=32,o=5,b=100:b,a#,g#,f#,e,d#,c#,c'

#buzzer
  - id: short_beep_buzzer
    then:
    - rtttl.play:
        rtttl: 'beep:d=4,o=7,b=100:32a'   

  - id: multi_beep_buzzer
    then:
    - rtttl.play:
        rtttl: 'beep:d=4,o=7,b=100:32a,g#,d#'          
        
 
#TODO: Nag alert if you seem to be away from desk for a while but haven't stowed everything.
  # - id: desky_drawer_is_open_nag
    # then:
      # #Audible first warning    
      # - rtttl.play:
          # rtttl: 'twoshort:d=4,o=5,b=100:16e6,16e6...,a'    
      # - while:
          # condition:
            # - script.is_running: desky_is_about_to_raise
          # then:
            # - light.turn_on: desky_light_led_right_green
            # - delay: 500ms
            # - light.turn_off: desky_light_led_right_green
            # - delay: 500ms
      # - delay: 10s
      # #Audible last warning        
      # - rtttl.play:
          # rtttl: 'moving_now:d=32,o=5,b=20:c,c,g'   
      # - delay: 1s
   
 
globals:
#Helpers for the timers

#Countdown Timer helpers
  - id: countdown_timer_remaining_time
    type: int
    restore_value: yes
    initial_value: "-1"

  - id: countdown_timer_remaining_time_previous
    type: int
    restore_value: yes
    initial_value: "0"
 
 #Stopwatch helpers (Time in current state)
  - id: desky_stopwatch_time
    type: int
    initial_value: '0'
    restore_value: yes
    
  - id: desky_stopwatch_time_previous
    type: int
    initial_value: '0'
    restore_value: yes

      
interval:
#Drives the timers    
#Timers adopted from: https://brianhanifin.com/posts/diy-irrigation-controller-esphome-home-assistant/
  - interval: 1min #Update on the minute
    then:    
      - lambda: |-
          if (id(countdown_timer_remaining_time) > -1) {
             // Store the previous time.
             id(countdown_timer_remaining_time_previous) = id(countdown_timer_remaining_time);
             
            // When the relay is on.
            if (id(desky_current_sit_stand_position).state == "Sitting") {
              // Decrement the timer.
              id(countdown_timer_remaining_time) -= 1;
              
              // Do something when time is up
              if (id(countdown_timer_remaining_time) <= 0) {
                id(short_beep_buzzer).execute();
                id(countdown_timer_remaining_time) = 0;
                id(desky_display_2_right).update();
              }
            }
            
            // Update the remaining time display.
            if (id(countdown_timer_remaining_time_previous) != id(countdown_timer_remaining_time)) {
              id(desky_minutes_until_stand_up).publish_state( (id(countdown_timer_remaining_time)) );
              id(desky_display_2_right).update();
            }
          }
          
#Countup timer
  - interval: 1min #Update on the minute
    then:    
      - lambda: |-
          if (id(desky_stopwatch_time) > -1)
            {
             // Store the previous time.
             id(desky_stopwatch_time_previous) = id(desky_stopwatch_time);
             id(desky_stopwatch_time) += 1;
             
             // Update the time display.
             if (id(desky_stopwatch_time) != -1)
               {
                id(desky_stopwatch).publish_state( (id(desky_stopwatch_time)) );
                id(desky_display_1_left).update();
               }
            }