Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 110 additions & 1 deletion homeassistant/components/sun/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import logging
from typing import Any

from astral import SunDirection
from astral.location import Elevation, Location

from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
Expand All @@ -25,7 +26,12 @@
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import dt as dt_util

from .const import DOMAIN, SIGNAL_EVENTS_CHANGED, SIGNAL_POSITION_CHANGED
from .const import (
DOMAIN,
SIGNAL_DURATIONS_CHANGED,
SIGNAL_EVENTS_CHANGED,
SIGNAL_POSITION_CHANGED,
)

_LOGGER = logging.getLogger(__name__)

Expand All @@ -43,6 +49,10 @@
STATE_ATTR_NEXT_NOON = "next_noon"
STATE_ATTR_NEXT_RISING = "next_rising"
STATE_ATTR_NEXT_SETTING = "next_setting"
STATE_ATTR_DAYLIGHT_DURATION = "daylight_duration"
STATE_ATTR_NIGHT_DURATION = "night_duration"
STATE_ATTR_TWILIGHT_SUNRISE_DURATION = "twilight_sunrise_duration"
STATE_ATTR_TWILIGHT_SUNSET_DURATION = "twilight_sunset_duration"

# The algorithm used here is somewhat complicated. It aims to cut down
# the number of sensor updates over the day. It's documented best in
Expand Down Expand Up @@ -125,6 +135,10 @@ class Sun(Entity):
STATE_ATTR_NEXT_NOON,
STATE_ATTR_NEXT_RISING,
STATE_ATTR_NEXT_SETTING,
STATE_ATTR_DAYLIGHT_DURATION,
STATE_ATTR_NIGHT_DURATION,
STATE_ATTR_TWILIGHT_SUNRISE_DURATION,
STATE_ATTR_TWILIGHT_SUNSET_DURATION,
}
)

Expand All @@ -145,6 +159,10 @@ class Sun(Entity):
solar_elevation: float
solar_azimuth: float
rising: bool
daylight_duration: timedelta
night_duration: timedelta
twilight_sunrise_duration: timedelta
twilight_sunset_duration: timedelta
_next_change: datetime

def __init__(self, hass: HomeAssistant) -> None:
Expand All @@ -161,6 +179,7 @@ def __init__(self, hass: HomeAssistant) -> None:
self._config_listener: CALLBACK_TYPE | None = None
self._update_events_listener: CALLBACK_TYPE | None = None
self._update_sun_position_listener: CALLBACK_TYPE | None = None
self._update_durations_listener: CALLBACK_TYPE | None = None
self._config_listener = self.hass.bus.async_listen(
EVENT_CORE_CONFIG_UPDATE, self.update_location
)
Expand All @@ -174,9 +193,17 @@ def update_location(self, _: Event | None = None, initial: bool = False) -> None
return
self.location = location
self.elevation = elevation
# The attributes need to be initialized before the first call to update_events() to avoid an AttributeError.
self.daylight_duration = timedelta()
self.night_duration = timedelta()
self.twilight_sunrise_duration = timedelta()
self.twilight_sunset_duration = timedelta()
if self._update_events_listener:
self._update_events_listener()
self.update_events()
if self._update_durations_listener:
self._update_durations_listener()
self.update_durations()

@callback
def remove_listeners(self) -> None:
Expand All @@ -187,6 +214,8 @@ def remove_listeners(self) -> None:
self._update_events_listener()
if self._update_sun_position_listener:
self._update_sun_position_listener()
if self._update_durations_listener:
self._update_durations_listener()

@property
def state(self) -> str:
Expand All @@ -210,6 +239,10 @@ def extra_state_attributes(self) -> dict[str, Any]:
STATE_ATTR_ELEVATION: self.solar_elevation,
STATE_ATTR_AZIMUTH: self.solar_azimuth,
STATE_ATTR_RISING: self.rising,
STATE_ATTR_DAYLIGHT_DURATION: self.daylight_duration.total_seconds(),
STATE_ATTR_NIGHT_DURATION: self.night_duration.total_seconds(),
STATE_ATTR_TWILIGHT_SUNRISE_DURATION: self.twilight_sunrise_duration.total_seconds(),
STATE_ATTR_TWILIGHT_SUNSET_DURATION: self.twilight_sunset_duration.total_seconds(),
Comment on lines +242 to +245
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no need to add these state attributes, it's enough to add the sensors.

}

def _check_event(
Expand Down Expand Up @@ -327,3 +360,79 @@ def update_sun_position(self, now: datetime | None = None) -> None:
self._update_sun_position_listener = event.async_track_point_in_utc_time(
self.hass, self.update_sun_position, utc_point_in_time + delta
)

@callback
def update_durations(self, now: datetime | None = None) -> None:
"""Calculate the daylight, night, sunrise and sunset durations."""
# First update sun position to assure the values are up-to-date.
if self._update_sun_position_listener:
self._update_sun_position_listener()
self.update_sun_position()

# Grab current time in case system clock changed since last time we ran.
utc_point_in_time = dt_util.utcnow()
try:
start, end = self.location.daylight(
dt_util.as_local(utc_point_in_time).date()
)
self.daylight_duration = end - start
except ValueError: # Catch Polar day / night / twilight.
self.daylight_duration = (
timedelta(days=1) if self.solar_elevation > -0.833 else timedelta()
)
try:
start, end = self.location.night(dt_util.as_local(utc_point_in_time).date())
self.night_duration = end - start
except ValueError: # Catch Polar day / night / twilight.
self.night_duration = (
timedelta(days=1) if self.solar_elevation <= -6.0 else timedelta()
)
try:
start, end = self.location.twilight(
dt_util.as_local(utc_point_in_time).date(), SunDirection.RISING
)
self.twilight_sunrise_duration = end - start
except ValueError: # Catch Polar day / night / twilight.
self.twilight_sunrise_duration = (
timedelta(hours=12)
if self.solar_elevation > -6.0 and self.solar_elevation <= -0.833
else timedelta()
)
try:
start, end = self.location.twilight(
dt_util.as_local(utc_point_in_time).date(), SunDirection.SETTING
)
self.twilight_sunset_duration = end - start
except ValueError: # Catch Polar day / night / twilight.
self.twilight_sunset_duration = (
timedelta(hours=12)
if self.solar_elevation > -6.0 and self.solar_elevation <= -0.833
else timedelta()
)

_LOGGER.debug(
"sun duarations_update@%s: daylight_duration=%s night_duration=%s twilight_sunrise_duration=%s twilight_sunset_duration=%s",
utc_point_in_time.isoformat(),
self.daylight_duration.total_seconds(),
self.night_duration.total_seconds(),
self.twilight_sunrise_duration.total_seconds(),
self.twilight_sunset_duration.total_seconds(),
)
self.async_write_ha_state()

async_dispatcher_send(self.hass, SIGNAL_DURATIONS_CHANGED)

# Next update will occur on the beginning of the next day.
delta = timedelta(
seconds=(
(
(24 - dt_util.as_local(utc_point_in_time).hour) * 60
- dt_util.as_local(utc_point_in_time).minute
)
* 60
- dt_util.as_local(utc_point_in_time).second
)
)
self._update_durations_listener = event.async_track_point_in_utc_time(
self.hass, self.update_durations, utc_point_in_time + delta
)
1 change: 1 addition & 0 deletions homeassistant/components/sun/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@

SIGNAL_POSITION_CHANGED = f"{DOMAIN}_position_changed"
SIGNAL_EVENTS_CHANGED = f"{DOMAIN}_events_changed"
SIGNAL_DURATIONS_CHANGED = f"{DOMAIN}_durations_changed"
53 changes: 51 additions & 2 deletions homeassistant/components/sun/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,20 @@
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import DEGREE, EntityCategory
from homeassistant.const import DEGREE, EntityCategory, UnitOfTime
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType

from . import Sun
from .const import DOMAIN, SIGNAL_EVENTS_CHANGED, SIGNAL_POSITION_CHANGED
from .const import (
DOMAIN,
SIGNAL_DURATIONS_CHANGED,
SIGNAL_EVENTS_CHANGED,
SIGNAL_POSITION_CHANGED,
)

ENTITY_ID_SENSOR_FORMAT = SENSOR_DOMAIN + ".sun_{}"

Expand Down Expand Up @@ -102,6 +107,50 @@ class SunSensorEntityDescription(SensorEntityDescription):
entity_registry_enabled_default=False,
signal=SIGNAL_EVENTS_CHANGED,
),
SunSensorEntityDescription(
key="daylight_duration",
device_class=SensorDeviceClass.DURATION,
translation_key="daylight_duration",
icon="mdi:weather-sunny",
state_class=SensorStateClass.TOTAL,
value_fn=lambda data: data.daylight_duration.total_seconds(),
entity_registry_enabled_default=False,
native_unit_of_measurement=UnitOfTime.SECONDS,
signal=SIGNAL_DURATIONS_CHANGED,
),
SunSensorEntityDescription(
key="night_duration",
device_class=SensorDeviceClass.DURATION,
translation_key="night_duration",
icon="mdi:weather-night",
state_class=SensorStateClass.TOTAL,
value_fn=lambda data: data.night_duration.total_seconds(),
entity_registry_enabled_default=False,
native_unit_of_measurement=UnitOfTime.SECONDS,
signal=SIGNAL_DURATIONS_CHANGED,
),
SunSensorEntityDescription(
key="twilight_sunrise_duration",
device_class=SensorDeviceClass.DURATION,
translation_key="twilight_sunrise_duration",
icon="mdi:weather-sunset-up",
state_class=SensorStateClass.TOTAL,
value_fn=lambda data: data.twilight_sunrise_duration.total_seconds(),
entity_registry_enabled_default=False,
native_unit_of_measurement=UnitOfTime.SECONDS,
signal=SIGNAL_DURATIONS_CHANGED,
),
SunSensorEntityDescription(
key="twilight_sunset_duration",
device_class=SensorDeviceClass.DURATION,
translation_key="twilight_sunset_duration",
icon="mdi:weather-sunset-down",
state_class=SensorStateClass.TOTAL,
value_fn=lambda data: data.twilight_sunset_duration.total_seconds(),
entity_registry_enabled_default=False,
native_unit_of_measurement=UnitOfTime.SECONDS,
signal=SIGNAL_DURATIONS_CHANGED,
),
)


Expand Down
6 changes: 5 additions & 1 deletion homeassistant/components/sun/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@
"next_setting": { "name": "Next setting" },
"solar_azimuth": { "name": "Solar azimuth" },
"solar_elevation": { "name": "Solar elevation" },
"solar_rising": { "name": "Solar rising" }
"solar_rising": { "name": "Solar rising" },
"daylight_duration": { "name": "Daylight duration" },
"night_duration": { "name": "Night duration" },
"twilight_sunrise_duration": { "name": "Twilight sunrise duration" },
"twilight_sunset_duration": { "name": "Twilight sunset duration" }
}
}
}