Sun and Time controlled ESPHome project

Hi this projoct is most wanted and most searched project type. I’am a retired man but I like programming. My carrier is, economics and software development but now I’m reteried old man :slight_smile:
but this codes work !

Our goal:

A program that turns the lights (or relay) on X hours before sunset or Y hour after sunset, and also turns the lights off Z hours before sunrise or T hour after sunrise.

Main problems:

  • There are not enough components, in fact there are but no one that does this job easily
  • You need to try different ways to reach your goal. While everyone wants the same thing, I couldn’t find a complete solution in my research.

Requirements:

  • an ESP board with wifi and internet connection, of course
  • a relay
  • connection and power cables, power supply and some coffee

Main features:

  • espHome setting: minimum req, works with internet connected wifi and ESP-01
  • HA independent (for standalone work, but everyone know HA is the easy way to achieves our goal)
  • time sensitive,
  • full automatic
  • easy use for users
  • easy customize
  • settings can be controlled via web interface, or HA or both of them
  • only esphome’s built in components, no extra or other, or github or component or script used
  • can be invert funciton, like if you want to close relay when sunset and open relay on sunrise, easy with one switch (Invert func)

solution:

  • A few duplicate components, even a copy of a copy
  • A few lines of lambda
  • used components: sensor: , number: , datetime: , select: , sun: , time: , switch: , text_sensor:

Work this order:

1- time: (input lat, long in yaml)
---->
2- sun: get info from time: → output sunrise, sunset time, evelation and azimuth
---->
3- datetime template gets sunrise and sunset time → and calculate relay open and close time delay with number: component even with delay
----->
4- no one can’t get calculated time (HH:MM) with normal properties like, .state value :frowning: for this reason use middle in the man strategy, we use another datetime template and some lambda, first ones push data to second one, second one is push text template to dispaly turn off time to user at UI,
----->
5- datetime component on_time: section open relay or close with state of (TRUE/FALSE) switch: wich is run close or open at the time like INVERT func

Conclusion:

I tested and works, Does enyone interest this type of code


There was a time, many years ago, when candles would have been your solution. Grins!

Post some code and logs to see what you’ve done. (Use formatting so we can see if there are any spacing issues)

esp8266:
  board: esp01_1m
 
logger:
  esp8266_store_log_strings_in_flash: False
  level: INFO
  logs: 
    status_led: NONE
    light: NONE

wifi:
  power_save_mode: NONE
  fast_connect: True  
  min_auth_mode: WPA2
   
  networks:
  - ssid: !secret wifi_ssid
    password: !secret wifi_password
  ap:
   ssid: "${isim}l"

api:
  encryption:
    key: "1234"

ota: 
  - platform: esphome
    password: "1234"

captive_portal:
 
light:  
  - platform: status_led
    pin: GPIO2
    id: led_status_light
    restore_mode: ALWAYS_ON
    internal: true
    effects:
    - strobe:
        name: "Slow Blink" 
        colors:
          - state: true
            duration: 1s
          - state: false
            duration: 5ms
    on_turn_on:
      - light.turn_on:
          id: led_status_light
          effect: "Slow Blink"


time:
  - platform: sntp
    id: sntp_time
    timezone:  "Europe/Istanbul"   

    on_time_sync:
      then:
        - lambda: |-  
              id(gunes).set_longitude(id(longitude_degeri).state);
              id(gunes).set_latitude(id(latitude_degeri).state);   
            
        - logger.log: 
            format: "Synchronized system clock" 
            level: INFO
            tag: "time:"
        - component.update: zaman
        - component.update: tarih


datetime:  

  - platform: template
    name: "Turn-On time"
    id: saat_ac
    type: TIME
    time_id: sntp_time
    optimistic: True
    restore_value: true
    entity_category: CONFIG
    internal: True
    on_time:  
      then:
         - if:
            condition:
                lambda: 'return ( (id(turnon_sunset).state == true) );'  
            then:

              - if:
                    condition:
                          lambda: 'return ( (id(invert_function).state == false) );'  
                    then:
                      - logger.log: 
                          format: "Turn-on power enabled, invert disabled , relay opened"
                          level: INFO
                          tag: "turn-on-time:"
                      - switch.turn_on: role
                    else:
                      - logger.log: 
                          format: "Turn-on power enabled, invert enabled , relay closed"
                          level: INFO
                          tag: "turn-on-time:"
                      - switch.turn_off: role

  - platform: template
    name: "Turn-Off time"
    id: saat_kapa
    type: TIME
    time_id: sntp_time
    optimistic: True
    restore_value: true
    entity_category: CONFIG
    internal: True
    on_time: 
      then:
         - if:
            condition:
                lambda: 'return ( (id(turnoff_sunrise).state == true) );'  
            then:
              - if:
                    condition:
                          lambda: 'return ( (id(invert_function).state == false) );'  
                    then:
                      - logger.log: 
                          format: "Turn-on power enabled, invert disabled , relay closed"
                          level: INFO
                          tag: "turn-off-time:"
                      - switch.turn_off: role
                    else:
                      - logger.log: 
                          format: "Turn-on power enabled, invert enabled , relay opened"
                          level: INFO
                          tag: "turn-off-time:"
                      - switch.turn_on: role


  - platform: template
    name: relay_open_time
    id: role_acilma_saati
    type: TIME
    time_id: sntp_time
    optimistic: True
    restore_value: true
    entity_category: CONFIG
    internal: True

    on_value:
      then:
        - datetime.time.set:
            id: saat_ac
            time: !lambda |-
              uint8_t ho =  int(id(role_acilma_saati).hour )+ id(turnon_delay).state ;
              uint8_t mi =  int(id(role_acilma_saati).minute) ;
              return {.second = 0, .minute = mi, .hour = ho};
       

  - platform: template
    name: relay_close_time
    id: role_kapanma_saati
    type: TIME
    time_id: sntp_time
    optimistic: True
    restore_value: true
    entity_category: CONFIG
    internal: True

    on_value:
      then:
        - datetime.time.set:
            id: saat_kapa
            time: !lambda |-
              uint8_t ho =  int(id(role_kapanma_saati).hour )+ id(turnoff_delay).state ;
              uint8_t mi =  int(id(role_kapanma_saati).minute) ;
              return {.second = 0, .minute = mi, .hour = ho};



sensor:
  - platform: sun
    name: "Sun Elevation"
    type: elevation
    icon: mdi:sun-angle
    id: s_evel

  - platform: sun
    name: "Sun Azimuth"
    type: azimuth
    icon: mdi:sun-compass
    id: s_azi

  - platform: wifi_signal  
    name: "WiFi Strength"
    filters:
      - lambda: return min(max(2 * (x + 100.0), 0.0), 100.0);
    unit_of_measurement: " %"
    entity_category: diagnostic
    device_class: signal_strength
    update_interval:  60s

 
text_sensor:

  - platform: template 
    internal: False
    id: disp_tont
    name: "Turn-On Time"
    icon: mdi:alarm-plus
    lambda: !lambda |-
          int ho =  int(id(role_acilma_saati).hour )+ id(turnon_delay).state ;
          int mi =  int(id(role_acilma_saati).minute) ;

          std::string saat = "";
          std::string dakika = std::to_string( mi );
        
          if (ho  < 10 )  {
            saat = "0" + std::to_string( ho ); 
          } else {
            saat = std::to_string( ho ); 
          }
          if (mi < 10 )  {
          dakika = "0" + std::to_string( mi ); 
          }
          auto result = saat + ":" + dakika;
          return result;  
 
  - platform: template
    internal: False
    id: disp_tofft
    name: "Turn-Off Time"
    icon: mdi:alarm-snooze
    lambda: !lambda |-
              int ho =  int(id(role_kapanma_saati).hour )+ id(turnoff_delay).state ;
              int mi =  int(id(role_kapanma_saati).minute) ;

              std::string saat = "";
              std::string dakika = std::to_string( mi );
           
              if (ho  < 10 )  {
                saat = "0" + std::to_string( ho ); 
              } else {
                saat = std::to_string( ho ); 
              }
              if (mi < 10 )  {
              dakika = "0" + std::to_string( mi ); 
              }
              auto result = saat + ":" + dakika;
              return result;  
 
  - platform: sun
    name: "Sunrise"
    type: sunrise
    id: s_sunrise
    format: "%H:%M"
    on_value: 
      then:
        - lambda: |-
            //ESP_LOGI("sunrise:", "Sunrise time %s", x.c_str() );
            auto call = id(role_kapanma_saati).make_call();
            std::string item = x.c_str() ;
            call.set_time(item);
            call.perform();
        

  - platform: sun
    name: "Sunset"
    type: sunset 
    id: s_sunset
    format: "%H:%M"
    on_value: 
      then:
        - lambda: |-
            //ESP_LOGI("sunset", "Sunset time %s", x.c_str() );
            auto call = id(role_acilma_saati).make_call();
            std::string item = x.c_str() ;
            call.set_time(item);
            call.perform();
        
 
  - platform:  template
    name: "Current Latitude"
    id: latitude_gosterir
    entity_category: diagnostic
    icon: mdi:latitude
    lambda: |-
              char buffer[50];  
              sprintf(buffer, "%.4f", id(latitude_degeri).state); 
              std::string str(buffer);  
              return str;

  - platform:  template
    name: "Current Longitude"
    id: longitude_gosterir
    entity_category: diagnostic
    icon: mdi:longitude
    lambda: |-
              char buffer[50];  
              sprintf(buffer, "%.4f", id(longitude_degeri).state);  
              std::string str(buffer); 
              return str;

  - platform: template
    name: "Date"
    id: tarih
    icon: mdi:calendar
    internal: True
    update_interval: 15min
    lambda: |-
      auto time_text = id(sntp_time).now().strftime("%d-%m-%Y" );
      return { time_text };

  - platform: template
    name: "Current Time"
    id: zaman
    icon: mdi:clock-time-five-outline
    update_interval: 1min
    lambda: |-
      auto time_text = id(sntp_time).now().strftime("%H:%M" );
      return { time_text };

  - platform: template
    name: "Current Location"
    id: location_now
    entity_category: diagnostic
    icon: mdi:map-marker-radius-outline
    # update_interval: 1min
    lambda: |-
      auto name_text = id(secili_sehir).current_option();
      return { name_text };


switch:

  - platform: template
    name: "Invert on/off function"
    id: invert_function
    entity_category: CONFIG
    icon: mdi:lightbulb-night
    optimistic: True
    assumed_state: False
    restore_mode: RESTORE_DEFAULT_OFF


    turn_on_action:
      - logger.log: 
            format: "Power will turn-off on Sunset with %.0f hour delay , turn-on on Sunrise with %.0f hour delay "
            args: ["id(turnoff_delay).state", "id(turnon_delay).state"]
            level: INFO
            tag: "sun:invert_function:"
            
    turn_off_action:
      - logger.log: 
            format: "Power will turn-on on Sunset with %.0f hour delay , turn-off on Sunrise with %.0f hour delay "
            args: ["id(turnon_delay).state", "id(turnoff_delay).state"]
            level: INFO
            tag: "sun:invert_function:"
      
      
  - platform: gpio  
    pin: GPIO0
    id: role
    name: "Power"
    restore_mode: RESTORE_DEFAULT_OFF
    icon: mdi:connection
    device_class: outlet

  - platform: template
    name: "Turn-Off on Sunrise"
    entity_category: CONFIG
    icon: mdi:weather-sunset-up
    id: turnoff_sunrise
    optimistic: True
    assumed_state: False
    restore_mode: RESTORE_DEFAULT_OFF

    turn_on_action:
      - logger.log: 
            format: "Auto turn-off power enabled on Sunrise with %.0f hour delay  "
            args: [  "id(turnoff_delay).state"]
            level: INFO
            tag: "sun:turnoff_sunrise:"
    turn_off_action:
      - logger.log:  
            format: "Auto turn-off power disabled on Sunrise"
            level: INFO
            tag: "sun:turnoff_sunrise:"

  - platform: template
    name: "Turn-On on Sunset"
    id: turnon_sunset
    entity_category: CONFIG
    icon: mdi:weather-sunset-down
    optimistic: True
    assumed_state: False
    restore_mode: RESTORE_DEFAULT_OFF

    turn_on_action:
      - logger.log: 
            format: "Auto turn-on power enabled on Sunset with %.0f hour delay  "
            args: [  "id(turnon_delay).state"]
            level: INFO
            tag: "sun:turnon_sunset:"
            
    turn_off_action:
      - logger.log: 
            format: "Auto turn-on power diasbled on Sunset"
            level: INFO
            tag: "sun:turnon_sunset:"

sun:
    id: gunes
    latitude:   id(latitude_degeri).state #    latitude: 41.015137
    longitude:  id(longitude_degeri).state  # longitude: 28.979530

    on_sunrise:
    - then:
        - logger.log: 
            format: "Good morning!!"
            level: INFO
            tag: "sun:on_sunrise:"

    on_sunset:
    - then:
         - logger.log: 
            format: "Good evening!"
            level: INFO
            tag: "sun:on_sunset:"


  
select:  
  - platform: template
    name: "City of residence"
    id: secili_sehir
    optimistic: true
    icon: mdi:map-marker-radius-outline
    entity_category: CONFIG
    restore_value: true
    options:
      - "Ankara"
      - "İstanbul"
      - "İzmir"
      - "Adana"
   
 
    initial_option: "İstanbul"  
     
    on_value:  #DEBUG

      - if:
            condition:
                      lambda: 'return strncmp(id(secili_sehir).current_option(), "İ", strlen("A")) == 0;'
            then:
              - if:
                  condition:
                            lambda: 'return strcmp(id(secili_sehir).current_option(), "İstanbul") == 0;'
                  then:
                        - logger.log: "Istanbul selected"
                        - number.set:
                                id: longitude_degeri  
                                value: 29.0331 # boylam
                        - number.set: 
                                id: latitude_degeri 
                                value: 41.0464  # enlem
                        - lambda: |-  
                              id(sntp_time).set_timezone("Europe/Istanbul") ;
                              id(sntp_time).update();
              - if:
                  condition:
                            lambda: 'return strcmp(id(secili_sehir).current_option(), "İzmir") == 0;'
                  then:
                        - number.set: 
                                id: latitude_degeri 
                                value: 38.423652  # enlem
                        - number.set:
                                id: longitude_degeri  
                                value: 	27.142797 # boylam
      - if:
            condition:
                      lambda: 'return strncmp(id(secili_sehir).current_option(), "A", strlen("A")) == 0;'
            then:
              - if:
                  condition:
                            lambda: 'return strcmp(id(secili_sehir).current_option(), "Ankara") == 0;'
                  then:
                        - number.set:
                                id: longitude_degeri  
                                value: 32.8604 # boylam
                        - number.set: 
                                id: latitude_degeri 
                                value: 39.9429  # enlem
                                    
              - if:
                  condition:
                            lambda: 'return strcmp(id(secili_sehir).current_option(), "Adana") == 0;'
                  then:
                        - number.set:
                                id: longitude_degeri  
                                value: 35.32502 # boylam
                        - number.set: 
                                id: latitude_degeri 
                                value: 36.98542  # enlem




                                               

      - lambda: |-  
            id(gunes).set_longitude(id(longitude_degeri).state);
            id(gunes).set_latitude(id(latitude_degeri).state);   

      - component.update: sntp_time
      - component.update: latitude_degeri
      - component.update: longitude_degeri
      - component.update: latitude_gosterir
      - component.update: longitude_gosterir
      - component.update: zaman
      - component.update: tarih  
      - component.update: s_sunrise
      - component.update: s_sunset
      - component.update: disp_tont
      - component.update: disp_tofft
      - component.update: role_acilma_saati
      - component.update: role_kapanma_saati  
      - component.update: location_now
      

 
      - logger.log: 
          format: "City of residence is %s now"
          args: ["id(secili_sehir).current_option()"]
          level: INFO
          tag: "sun:selected_city:"             

 
# AVRUPA https://time-ok.com/coordinates/europe
# Türkiye https://time-ok.com/coordinates/turkey
# https://www.netdata.com/netsite/2e012549/sehir-koordinatlari

number:
  - platform: template
    name: "Latitude"
    id: latitude_degeri
    min_value: -90.0000
    max_value: 90.0000
    step: 0.0001
    icon: mdi:latitude
    restore_value: true
    initial_value: 41.0464 # enlem
    optimistic: true
    entity_category: CONFIG
    internal: True

  - platform: template
    name: "Longitude"
    id: longitude_degeri
    min_value: -180.0000
    max_value: 180.0000
    step: 0.0001
    icon: mdi:longitude
    restore_value: true
    initial_value: 29.0331 # boylam
    optimistic: true
    entity_category: CONFIG
    internal: True


  - platform: template
    name: "Turn-Off delay (h)"
    id: turnoff_delay
    min_value: -4
    max_value: 4
    step: 1
    icon: mdi:clock-minus-outline
    restore_value: true
    initial_value: -1  
    optimistic: true
    entity_category: CONFIG
    on_value: 
      then:
        - logger.log: 
            format: "Auto turn-off on Sunrise delay is %.0f hours"
            args: ["id(turnoff_delay).state"]
            level: INFO
            tag: "sun:turnoff_delay:"
        - component.update: sntp_time
        - component.update: s_sunrise
        - component.update: s_sunset
        - component.update: disp_tont
        - component.update: disp_tofft
        - component.update: role_acilma_saati
        - component.update: role_kapanma_saati
    

  - platform: template
    name: "Turn-On delay (h)"
    id: turnon_delay
    min_value: -4
    max_value: 4
    step: 1
    icon: mdi:clock-plus-outline
    restore_value: true
    initial_value: 0  
    optimistic: true
    entity_category: CONFIG

    on_value: 
      then:
        - logger.log: 
            format: "Auto turn-on on Sunset delay is %.0f hours"
            args: ["id(turnon_delay).state"]
            level: INFO
            tag: "sun:turnon_delay:"
        - component.update: sntp_time
        - component.update: s_sunrise
        - component.update: s_sunset
        - component.update: disp_tont
        - component.update: disp_tofft
        - component.update: role_acilma_saati
        - component.update: role_kapanma_saati

That’s a lot of code! Lots of debug clauses scattered everywhere indicates you spent quality time on this. Glad it works for you.

I didn’t understand a few of the Turkish terms but Google translate will take care of that.

Check your spacing in the yaml code for your esp8266, around the wifi section to see if it is the cut-and-paste forum spacing indentation that is an issue or in your code.

Check breaking changes in the change log for the November 2025 HomeAssistant updates to see if the state value functionality needs a tweak.

Once you have ascertained the values for each of your four cities, “Ankara”, “İstanbul”, “İzmir” and “Adana”, such as latitude, longitude etc, do you have to extract them again each time your code is run, or did you do that so you can make it easier to add more cities in your choices?

Thanks for your contribution.

We have 81 cities in our country, all cities lat, long coordinates are set in YAML file, to avoid confussion I did not sent to you. And esp8266 can’t handle this size code very well. then I add every item in select , to avoid mess, & memory problems, separate setting rule with cities names first letter

Useally, While wirting code,allways I use turkish terms for parameter & varibles. This easy can track logic and never look to mind, what is the ‘close-time-hour’. Addittonally using english laguage for coding is advantage to non native english speakers, bucause mind allways use native language for logic decissions. At least I think this way. If I used Turkish terms for variables, it shows that I put a lot of effort into these variables :slight_smile:

I hate yaml and python typing rule and structure. you right about spaces in my yaml. But who cares white spacess. The compiler does not take them seriously while compiling anyway. :rofl:

By the way, I always like wrote useless applications. But I wrote & algorithim this code for reel benefits of people.

Thank you very much for your comments.

It is amusing to ask people who have migrated to my country, some for many years, which language do you count in, inside your mind, when you are adding up? Yes, it takes are very long time and lots of practise to become truly multilingual, and Google translate has come a long way to make it easier.

Unfortunately yaml code can be fussy with spacing where you least expect it. From many years of coding, I know the missing comma, the wrong indentation can cause lots of grief and hair pulling. Programmers cursing is an universal attribute.

Don’t underestimate your capabilities. What seems trivial and useless for you may be the missing link for others to make their lives empowered and enjoyable. This is where documentation and comments in the code, like you have done, make it far easier to follow, understand, debug, modify, and adapt, language limitations and all.

Do we speak computerese? Yay! :wink:

Major bug & Update:

  1. timzezone settings:
time:
  - platform: sntp
    id: sntp_time  
    timezone:  "Europa/Istanbul"    # https://github.com/nayarsystems/posix_tz_db/blob/93447c0ddac304ca6672a5fd905261c7e8905159/zones.csv

timezone paramater MUST like

timezone:  "<+03>-3" # "Europa/Istanbul" 

because string type “Europa/Istanbul” works wrong, you can find this TZ code at GitHub TZ database link

  1. I’am not sure but if your yaml settings refers to HA api connectin like below
api:
   encryption:
     key: "xxx-xxx-xxxx---xx="

time: timezone settings don’t work, I think HA time setting push to or crushes local settings, but not 100% sure .

I’ll inform you, after test and solve the problem later.

Hı, I tested it, when you set timezone like

timezone:  "<+03>-3" 

and

api:

works fine, not override HA timezone settings to EspHome settings, feel free to use api: parameter

:slight_smile: