Hours of Daylight

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).

Doh! I’ll get this if it kills me lol!!

1 Like

OK All working now except the graph.

  gr2:
    name: Daylight Hours
    entities:
      - sensor.daylighttoday
    hours_to_show: 168
    refresh: 1800

Is producing Unknown… (At the right… everything else is from before I changed to seconds) Maybe I should try the sun.sun…

image

You have to add unit_of_measurement to your sensor (e.g. unit_of_measurement: sec.)