Hours of Daylight

Na, they run the tests on it for you. And by they, I mean it’s an automated process. Sure, your pr will have a ton of errors and failures first pass but you can make changes until it works.

It’s up above posted a day ago. I’ve tried sec and 'sec as units_of_measurement as well. I get a state for it but no state history.

I also started a new thread History Graph not working for entity with screenshot and my latest attempts…

I can’t get a state history which is why no history graph - so I’m concentrating on that right now.

I made @pnbruckner changes to also get daylength previous and tomorrow. it is here which is slightly different to @pnbruckner

"""
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: self.daylight.total_seconds(),
            STATE_ATTR_PREV_DAYLIGHT: self.prev_daylight.total_seconds(),
            STATE_ATTR_NEXT_DAYLIGHT: self.next_daylight.total_seconds(),
            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()

Do you have recorder or history configured to exclude this entity, directly or indirectly?

nope. zero exclusions.

If you search your HA log do you see state_change events for this sensor? E.g.:

grep -F 'new_state=<state sensor.daylengthchange' home-assistant.log

Have you seen the sensor’s value change from one day to the next (without any HA restarts)?

EDIT: Do you see any entries in the Logbook at the start of the day. I don’t have exactly the same template sensors as you, but I do have a few based on the custom sun component, and I see these in the Logbook:

image

No. If I look at midnight (which is when it would change) I see only these:
image

Well, that’s interesting, because you don’t see “Daylight” in my Logbook either (which is another template sensor I have that is the number of seconds, with a unit_of_measurement of sec.) Hmm. I do see it on the History page, though, and I see a changing graph from since I defined it. I’ll try creating a history_graph with it and see what happens…

EDIT: From the History page:

image

pretty sure the day length ones yesterday, today and tomorrow are behaving. But my templaye sensor takes today-yesterday to get a change in day length and that’s what I want to graph. Problem is you can’t graph the daylight (if you have hours:minutes:seconds) as it just does the hours which is kinda uninteresting.

My sensors:

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(' %H:%M:%S',false) | replace(" 0", "") }}
        icon_template: mdi:weather-sunny
      daylighttoday:
        friendly_name: 'Day Length Today'
        value_template: >
          {{ (states.sun.sun.attributes.daylight) | timestamp_custom(' %H:%M:%S',false) | replace(" 0", "") }}
        icon_template: mdi:weather-sunny
      daylighttomorrow:
        friendly_name: 'Day Length Tomorrow'
        value_template: >
          {{ (states.sun.sun.attributes.next_daylight) | timestamp_custom(' %H:%M:%S',false) | replace(" 0", "") }}
        icon_template: mdi:weather-sunny
      daylengthchange:
        friendly_name: 'Change in Day Length'
        unit_of_measurement: "sec"
        value_template: >
          {{ (states.sun.sun.attributes.daylight) - (states.sun.sun.attributes.prev_daylight) }}
        icon_template: mdi:weather-sunny

It’s the daylengthchange that is most interesting and I just don’t understand why it’s not appearing in history or the logbook.

It’s changing and the value is correct. I don’t get it.

So I had the daylengthchange set to hidden as I didn’t want the actual sensor to be displayed at all - just wanted the graph. Removed hidden and now it’s showing a history (blank)

Interesting. I’d have to check the code, but (maybe?) it makes sense that history is not recorded for an entity that is hidden. Don’t know. Now that you’ve un-hidden it, it will be interesting to see if you get history (I guess starting at tonight’s midnight. Or, actually, at the next sun elevation change, which should cause sun.sun to have a state change.) FWIW, I use group views, so I define what is displayed, so I don’t have to set things to hidden.

Well, you could have created a template sensor that is daylight converted to floating point hours (with unit_of_measurement set to hr.) That probably would create a reasonable graph, too. Something like {{ (state_attr('sun.sun', 'daylight') / (60*60)) | round(2) }}.

1 Like

I use group views as well but use customize out of habit I guess from before I had views defined and I didn’t want random sensors in the header bar. I certainly didn’t expect hiding it to remove the history. That is an unwanted consequence.

Thanks for that… This has been a good learning exercise and I really appreciate your help Phil @pnbruckner

1 Like

Ha ha ha!!! This is EXACTLY how I feel about it except I have implemented it and still have no idea why!

Except that it is quite cool. But then a lot of stuff I do / want to do in HA has no real actual compelling reason to implement it except to be cool :wink:

2 Likes

Of course. I don’t actually NEED anything… some is just for my interest. This is just something I’m interested in watching and seeing how the day length changes as the seasons change and it’s all at a total cost of $0.

1 Like

HI,
please allow me to jump in here, if I’d need a separate thread tell me.

I’ve been trying to template the offset to the previous (or last if you will) sunset.
My aim is to use that given offset in automations for triggering outside lights and other presence logic.

had this conversation Read sunset offset at a given time? - #11 by tom_l with @tom_l, end ended up with me asking if this could be done with, or added to your CC…
please have a look?
thx!

@pnbruckner
I’m back again with another feature request :wink::roll_eyes:
Is it possible to calculate the maximium sun elevation for ‘today’? And if it is could this be included in your sensor?

I believe that is possible. It would just be the elevation at solar noon I think. That could be calculated at midnight along with the other values that don’t change throughout the day. I’ll add a new feature request, but I probably won’t get to it for a bit.

EDIT: https://github.com/pnbruckner/homeassistant-config/issues/66

1 Like

Well, it’s been a while, but I have a beta available that should do this. Let me know how it works for you (assuming you’re still interested! :slight_smile:)

It has been a while and to be honest I have no idea now why I asked for this! :blush:

I’m more than happy to see test it and see how it works though. It’s the least I can do given all the effort you put into your components (and perhaps my reason for asking will come back to me).

EDIT: Well it seems to work fine. Today it gives an elevation that matches within 0.04 deg with another source on the internet.

1 Like

@klogg

Oops, made a mistake. I used the date of the next solar noon when calculating today’s maximum elevation. I was supposed to use today’s solar noon. Fixed in 1.1.0b2.