Hours of Daylight

Sweet! Nice work!

Looks good comparing data from www.timeanddate.com/sun/

  - platform: template
    sensors:
      sunrise_today:
        friendly_name: "Sun Rise Today"
        unit_of_measurement: 'Time'
        value_template: "{{ as_timestamp(state_attr('sun.sun','sunrise')) |timestamp_custom('%H:%M') }}"
        icon_template: mdi:weather-sunset-up

      sunset_today:
        friendly_name: "Sun Set Today"
        unit_of_measurement: 'Time'
        value_template: "{{ as_timestamp(state_attr('sun.sun','sunset')) |timestamp_custom('%H:%M') }}"
        icon_template: mdi:weather-sunset-down

      sundaylight_today:
        friendly_name: 'Daylight Today'
        unit_of_measurement: 'Hours'
        value_template: "{{ (as_timestamp(state_attr('sun.sun','sunset')) - as_timestamp(state_attr('sun.sun','sunrise'))) |timestamp_custom('%H:%M', false) }}"
        icon_template: mdi:weather-sunny

I love this about HA. The fact that components can be added to (and subtracted from!) and customised so ā€˜easilyā€™ - I use the term loosely. And I love that so many people do this and share with others.

I would like to use this but this is where I have a slight problem, or I have missed something. If I use this custom sun component and it overrides the official one, how will I ever know if

a) this one ever gets included into the official ā€˜buildā€™ so I can remove the custom component,
b) this one gets updated meaning I should update my copy or
c) the official one gets updated in a different way e.g. to fix a bug meaning I might want to revert back to using that one?

I guess the short question is how does one manage version control with custom components?

@DavidFW1960, @Tomahawk, @klogg, @chrisw

So this custom sun component seems to work, except ā€¦ itā€™s not switching at midnight. Looks like the standard sun component uses ā€œsolar midnightā€, whatever that is, instead of real midnight. And it only updates when the next event happens based on its next_xxx attributes. So it looks like I have to make a slight tweak to get it to update at real midnight as well. Should be very simple, but I donā€™t know that Iā€™ll be able to get that done today; hopefully by tomorrow. Iā€™ll keep you posted.

1 Like

All very good questions.

Read the release notes in detail. We should all be doing that anyway.

I believe you can watch my github repository, which should inform you when I make changes.

Canā€™t promise anything, but hopefully Iā€™ll notice if/when that happens and update mine accordingly, meaning again by watching my github repository you should know.

2 Likes

Solar midnight is the time, exactly between sun set and sun rice. So definitely not midnight at 24:00.

2 Likes

Well good newsā€¦ changing the scan time to 10 minutes has fixed the daylight hours for me.

Great work too @pnbruckner

Edit: one of the hours updated at 12:04am and the other 2 at 12:14amā€¦ So if it was set to scan at 60 mins it would potentially take a couple of hours to update. Perhaps it times out or otherwise receives no response. I guess Iā€™m unlikely to be checking it between 12am and 7am anywayā€¦ but the offset did the trick. The sun.py component also seems to be different from the sunrise-sunset one by roughly a minuteā€¦

Thinking. I need to check but maybe one is decimal degrees and one is degrees minutes secondsā€¦

Not surprising. Iā€™m sure the two are using different data sets and/or using slightly different calculations.

I guess then there are three advantages to using the sunrise/sunset attributes I added to the sun component (as opposed to the sunrise-sunset.org website): 1) as mentioned before it doesnā€™t require polling of any external website; 2) it only needs to update once and does so right at midnight; and 3) the times agree with the other sun.sun attributes (e.g., before sunrise the sunrise attribute is the same as the next_rising attribute, and the same for sunset and next_setting.)

I just updated my custom component to add another update time, which is (local) tonightā€™s midnight. If you try it, let me know how it works for you.

@Tomahawk, @klogg, @chrisw

Interestingā€¦ I think the sun.py uses the elevation as well and the sunrise-sunset.org doesnā€™t. They both use decimal degrees. I would expect the calculations/ephemeris used to be common to bothā€¦ could be wrongā€¦ I guess that would explain any differences.

Iā€™m not sure why it needed another refresh at local midnightā€¦ I didnā€™t really follow the discussion you had with Tomahawk but I did download the new component. Iā€™m primarily interested in the day length and how it is changing so Iā€™ll continue to use sunrise/sunset for that for now unless you have any other suggestions.

Day length is the time between sunrise and sunset. So you can get that from my enhanced sun component with:

{{ as_timestamp(state_attr('sun.sun','sunset')) -
   as_timestamp(state_attr('sun.sun','sunrise')) }}

And, of course, if you prefer it displayed as HH:MM:

{{ (as_timestamp(state_attr('sun.sun','sunset')) -
    as_timestamp(state_attr('sun.sun','sunrise')))
   |timestamp_custom('%H:%M',false) }}

It just comes down to 1) if youā€™re ok with using a custom component, 2) which data source you feel is more accurate, 3) whether you care if an external website needs to be constantly queried and the data can be delayed from midnight for a while, or whether youā€™d prefer a solution that doesnā€™t need to query an external website and provides the values immediately after midnight. Your choice. :slight_smile:

The sun component updates itself only as necessary. It did that by setting the next update one second after the next upcoming time based on its various attributes (next_setting, next_midnight, etc.) Unfortunately next_midnight (which is ā€œsolar midnightā€) is not 00:00. So I simply added 00:00 (real midnight) as a time it will always update. That way sunrise and sunset will change when the day changes.

2 Likes

Yeah and Iā€™d be happy to do that but itā€™s not giving me yesterday and tomorrowā€¦

Well, if youā€™re willing to use a custom component, then all it would take is customizing it a bit more for your use case. All the functionality you need is there. You just need to add a few more lines of code, and presto-magico, the sun component will have all the attributes you want. :slight_smile:

But seriously, if youā€™re not comfortable doing that Iā€™d be happy to do it for you, assuming youā€™d like to use it.

1 Like

OK Philā€¦ I think Iā€™ve worked it outā€¦ There is a day difference between the sun.py/timeanddate.com and sunrise-sunset.org. They are a day out. The times sun.py is giving me for the sunrise/sunset today are the same as the ones on sunrise-sunset for yesterday (when I use -14hours)

If you can add those few lines of code Iā€™d be more than happy to use your component!

No problem. The astral package refers to the day length as daylight. How about, somewhat in keeping with the names of the other attributes, I create three new attributes called daylight (for today), next_daylight (for tomorrow), and prev_daylight (for yesterday)? Iā€™ll also keep the new sunrise and sunset attributes (but you can just comment them out if you like.)

Yeah that would be fine Phil. I will show your new sunrise/sunset as well anyway. This is what my card looks like:

So Iā€™m using your sunrise and sunset times anyway. Then Iā€™ll also just use your daylight, next_daylight and prev_daylight as well and do away with the other ones.

Ok, below is the code. I didnā€™t want to check this into github because itā€™s kind of a special use case. (Iā€™m considering adding config parameters to enable the desired set of attributes.) Anyway, it now has the additional attributes, and they are formatted as strings in the form of HH:MM:SS.

Here is the code for custom_components/sun.py:

"""
Support for functionality to keep track of the sun.

For more details about this component, please refer to the documentation at
https://home-assistant.io/components/sun/
"""
import asyncio
import logging
from datetime import timedelta

from homeassistant.const import CONF_ELEVATION
from homeassistant.core import callback
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import (
    async_track_point_in_utc_time, async_track_utc_time_change)
from homeassistant.helpers.sun import (
    get_astral_location, get_astral_event_next, get_astral_event_date)
from homeassistant.util import dt as dt_util

_LOGGER = logging.getLogger(__name__)

DOMAIN = 'sun'

ENTITY_ID = 'sun.sun'

STATE_ABOVE_HORIZON = 'above_horizon'
STATE_BELOW_HORIZON = 'below_horizon'

STATE_ATTR_AZIMUTH = 'azimuth'
STATE_ATTR_ELEVATION = 'elevation'
STATE_ATTR_NEXT_DAWN = 'next_dawn'
STATE_ATTR_NEXT_DUSK = 'next_dusk'
STATE_ATTR_NEXT_MIDNIGHT = 'next_midnight'
STATE_ATTR_NEXT_NOON = 'next_noon'
STATE_ATTR_NEXT_RISING = 'next_rising'
STATE_ATTR_NEXT_SETTING = 'next_setting'
STATE_ATTR_SUNRISE = 'sunrise'
STATE_ATTR_SUNSET = 'sunset'
STATE_ATTR_DAYLIGHT = 'daylight'
STATE_ATTR_PREV_DAYLIGHT = 'prev_daylight'
STATE_ATTR_NEXT_DAYLIGHT = 'next_daylight'


@asyncio.coroutine
def async_setup(hass, config):
    """Track the state of the sun."""
    if config.get(CONF_ELEVATION) is not None:
        _LOGGER.warning(
            "Elevation is now configured in home assistant core. "
            "See https://home-assistant.io/docs/configuration/basic/")

    sun = Sun(hass, get_astral_location(hass))
    sun.point_in_time_listener(dt_util.utcnow())

    return True


class Sun(Entity):
    """Representation of the Sun."""

    entity_id = ENTITY_ID

    def __init__(self, hass, location):
        """Initialize the sun."""
        self.hass = hass
        self.location = location
        self._state = self.next_rising = self.next_setting = None
        self.sunrise = self.sunset = None
        self.daylight = self.prev_daylight = self.next_daylight = None
        self.next_dawn = self.next_dusk = None
        self.next_midnight = self.next_noon = None
        self.solar_elevation = self.solar_azimuth = None

        async_track_utc_time_change(hass, self.timer_update, second=30)

    @property
    def name(self):
        """Return the name."""
        return "Sun"

    @property
    def state(self):
        """Return the state of the sun."""
        if self.next_rising > self.next_setting:
            return STATE_ABOVE_HORIZON

        return STATE_BELOW_HORIZON

    @property
    def state_attributes(self):
        """Return the state attributes of the sun."""
        return {
            STATE_ATTR_NEXT_DAWN: self.next_dawn.isoformat(),
            STATE_ATTR_NEXT_DUSK: self.next_dusk.isoformat(),
            STATE_ATTR_NEXT_MIDNIGHT: self.next_midnight.isoformat(),
            STATE_ATTR_NEXT_NOON: self.next_noon.isoformat(),
            STATE_ATTR_NEXT_RISING: self.next_rising.isoformat(),
            STATE_ATTR_NEXT_SETTING: self.next_setting.isoformat(),
            STATE_ATTR_SUNRISE: self.sunrise.isoformat(),
            STATE_ATTR_SUNSET: self.sunset.isoformat(),
            STATE_ATTR_DAYLIGHT: str(self.daylight),
            STATE_ATTR_PREV_DAYLIGHT: str(self.prev_daylight),
            STATE_ATTR_NEXT_DAYLIGHT: str(self.next_daylight),
            STATE_ATTR_ELEVATION: round(self.solar_elevation, 2),
            STATE_ATTR_AZIMUTH: round(self.solar_azimuth, 2)
        }

    @property
    def next_change(self):
        """Datetime when the next change to the state is."""
        # next_midnight is next solar midnight. So get actual midnight,
        # but subtract a second because point_in_time_listener() will add one.
        midnight = dt_util.as_utc(dt_util.start_of_local_day(
            dt_util.now()+timedelta(1))-timedelta(seconds=1))
        return min(self.next_dawn, self.next_dusk, self.next_midnight,
                   self.next_noon, self.next_rising, self.next_setting, midnight)

    @callback
    def update_as_of(self, utc_point_in_time):
        """Update the attributes containing solar events."""
        self.next_dawn = get_astral_event_next(
            self.hass, 'dawn', utc_point_in_time)
        self.next_dusk = get_astral_event_next(
            self.hass, 'dusk', utc_point_in_time)
        self.next_midnight = get_astral_event_next(
            self.hass, 'solar_midnight', utc_point_in_time)
        self.next_noon = get_astral_event_next(
            self.hass, 'solar_noon', utc_point_in_time)
        self.next_rising = get_astral_event_next(
            self.hass, 'sunrise', utc_point_in_time)
        self.next_setting = get_astral_event_next(
            self.hass, 'sunset', utc_point_in_time)
        self.sunrise = get_astral_event_date(
            self.hass, 'sunrise', utc_point_in_time)
        self.sunset = get_astral_event_date(
            self.hass, 'sunset', utc_point_in_time)
        d = get_astral_event_date(
            self.hass, 'daylight', utc_point_in_time)
        self.daylight = d[1] - d[0]
        d = get_astral_event_date(
            self.hass, 'daylight', utc_point_in_time-timedelta(days=1))
        self.prev_daylight = d[1] - d[0]
        d = get_astral_event_date(
            self.hass, 'daylight', utc_point_in_time+timedelta(days=1))
        self.next_daylight = d[1] - d[0]

    @callback
    def update_sun_position(self, utc_point_in_time):
        """Calculate the position of the sun."""
        self.solar_azimuth = self.location.solar_azimuth(utc_point_in_time)
        self.solar_elevation = self.location.solar_elevation(utc_point_in_time)

    @callback
    def point_in_time_listener(self, now):
        """Run when the state of the sun has changed."""
        self.update_sun_position(now)
        self.update_as_of(now)
        self.async_schedule_update_ha_state()

        # Schedule next update at next_change+1 second so sun state has changed
        async_track_point_in_utc_time(
            self.hass, self.point_in_time_listener,
            self.next_change + timedelta(seconds=1))

    @callback
    def timer_update(self, time):
        """Needed to update solar elevation and azimuth."""
        self.update_sun_position(time)
        self.async_schedule_update_ha_state()

Awesome Phil!

Iā€™m using this config:

sensor:
  - platform: template
    sensors:
      nextsunrise:
        friendly_name: 'Next Sunrise'
        value_template: >
          {{ as_timestamp(states.sun.sun.attributes.next_rising) | timestamp_custom(' %I:%M%p') | replace(" 0", "") }}
        icon_template: mdi:weather-sunset-up
      nextsunset:
        friendly_name: 'Next Sunset'
        value_template: >
          {{ as_timestamp(states.sun.sun.attributes.next_setting) | timestamp_custom(' %I:%M%p') | replace(" 0", "") }}
        icon_template: mdi:weather-sunset-down
      sunrisetoday:
        friendly_name: 'Sunrise'
        value_template: >
          {{ as_timestamp(states.sun.sun.attributes.sunrise) | timestamp_custom(' %I:%M%p') | replace(" 0", "") }}
        icon_template: mdi:weather-sunset-up
      sunsettoday:
        friendly_name: 'Sunset'
        value_template: >
          {{ as_timestamp(states.sun.sun.attributes.sunset) | timestamp_custom(' %I:%M%p') | replace(" 0", "") }}
        icon_template: mdi:weather-sunset-down
      daylightyesterday:
        friendly_name: 'Day Length Yesterday'
        value_template: >
          {{ (states.sun.sun.attributes.prev_daylight) | timestamp_custom(' %I:%M:%S') | replace(" 0", "") }}
        icon_template: mdi:weather-sunny
      daylighttoday:
        friendly_name: 'Day Length Today'
        value_template: >
          {{ (states.sun.sun.attributes.daylight) | timestamp_custom(' %I:%M:%S') | replace(" 0", "") }}
        icon_template: mdi:weather-sunny
      daylighttomorrow:
        friendly_name: 'Day Length Tomorrow'
        value_template: >
          {{ (states.sun.sun.attributes.next_daylight) | timestamp_custom(' %I:%M:%S') | replace(" 0", "") }}
        icon_template: mdi:weather-sunny

I made the daylight attributes formatted strings, so you donā€™t need the | timestamp_custom(...) part for those. (Since the input to that filter is a string in your templates for these attributes, the filter is just passing it through like itā€™s not there.)

If youā€™d rather those attributes were integers (i.e., number of seconds), so that you can format them how you want with timestamp_custom(xxx, false), then change these lines in the state_attributes function:

            STATE_ATTR_DAYLIGHT: str(self.daylight),
            STATE_ATTR_PREV_DAYLIGHT: str(self.prev_daylight),
            STATE_ATTR_NEXT_DAYLIGHT: str(self.next_daylight),

to:

            STATE_ATTR_DAYLIGHT: self.daylight.total_seconds(),
            STATE_ATTR_PREV_DAYLIGHT: self.prev_daylight.total_seconds(),
            STATE_ATTR_NEXT_DAYLIGHT: self.next_daylight.total_seconds(),

Gotchaā€¦ makes senseā€¦ Iā€™m just going to leave it as formatted I think. I canā€™t imagine any reason Iā€™d want to manipulate the time stampā€¦ althoughā€¦ I was trying to do a history graph of the daylight today and itā€™d just a bar like if I looked in history rather than a line chartā€¦ Is that because of the format?
image

OK Changed to seconds and using this:

{{ (states.sun.sun.attributes.daylight) | timestamp_custom(ā€™ %H:%M:%Sā€™) }}

itā€™s out by 36000 seconds (10 Hours). I can fix it with

{{ ((states.sun.sun.attributes.daylight) -36000) | timestamp_custom(ā€™ %H:%M:%Sā€™) }}

But I should be able to modify the sun.py I think?

You have to use timestamp_custom(' %H:%M:%S',false).