From d81091d2ff3b680966e2b374c5e4624c21b12dc7 Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Mon, 11 Nov 2024 14:46:47 -0600 Subject: [PATCH 1/2] Use UTC datetimes internally Python datetime object comparisons and math don't work as expected when the objects are aware and have the same tzinfo attribute. Basically the fold attribute is ignored in this case, which can lead to wrong results. Avoid this problem by using aware times in UTC internally. Only use the location's tz for user visible attributes. --- custom_components/__init__.py | 2 + custom_components/sun2/binary_sensor.py | 30 ++++--- custom_components/sun2/config.py | 10 +-- custom_components/sun2/config_flow.py | 10 +-- custom_components/sun2/helpers.py | 26 ++++-- custom_components/sun2/sensor.py | 111 +++++++++++++----------- 6 files changed, 107 insertions(+), 82 deletions(-) create mode 100644 custom_components/__init__.py diff --git a/custom_components/__init__.py b/custom_components/__init__.py new file mode 100644 index 0000000..827af0c --- /dev/null +++ b/custom_components/__init__.py @@ -0,0 +1,2 @@ +"""Sun2 integration.""" +# Exists to satisfy mypy. diff --git a/custom_components/sun2/binary_sensor.py b/custom_components/sun2/binary_sensor.py index e547b2c..4da4db6 100644 --- a/custom_components/sun2/binary_sensor.py +++ b/custom_components/sun2/binary_sensor.py @@ -17,7 +17,6 @@ ) from homeassistant.core import CoreState, callback from homeassistant.helpers.event import async_track_point_in_utc_time -from homeassistant.util import dt as dt_util from .const import ATTR_NEXT_CHANGE, LOGGER, MAX_ERR_BIN, ONE_DAY, ONE_SEC, SUNSET_ELEV from .helpers import ( @@ -112,13 +111,17 @@ def _get_nxt_dttm(self, cur_dttm: datetime) -> datetime | None: # since current time might be anywhere from before today's solar # midnight (if it is this morning) to after tomorrow's solar midnight # (if it is this evening.) - date = cur_dttm.date() - evt_dttm1 = cast(datetime, self._astral_event(date, "solar_midnight")) - evt_dttm2 = cast(datetime, self._astral_event(date, "solar_noon")) - evt_dttm3 = cast(datetime, self._astral_event(date + ONE_DAY, "solar_midnight")) - evt_dttm4 = cast(datetime, self._astral_event(date + ONE_DAY, "solar_noon")) + date = self._as_tz(cur_dttm).date() + evt_dttm1 = cast(datetime, self._astral_event(date, "solar_midnight", False)) + evt_dttm2 = cast(datetime, self._astral_event(date, "solar_noon", False)) + evt_dttm3 = cast( + datetime, self._astral_event(date + ONE_DAY, "solar_midnight", False) + ) + evt_dttm4 = cast( + datetime, self._astral_event(date + ONE_DAY, "solar_noon", False) + ) evt_dttm5 = cast( - datetime, self._astral_event(date + 2 * ONE_DAY, "solar_midnight") + datetime, self._astral_event(date + 2 * ONE_DAY, "solar_midnight", False) ) # See if segment we're looking for falls between any of these events. @@ -176,7 +179,7 @@ def _get_nxt_dttm(self, cur_dttm: datetime) -> datetime | None: "%s: Sun elevation will not reach %f again until %s", self.name, self._threshold, - nxt_dttm.date(), + self._as_tz(nxt_dttm).date(), ) return nxt_dttm @@ -185,9 +188,12 @@ def _get_nxt_dttm(self, cur_dttm: datetime) -> datetime | None: evt_dttm1 = evt_dttm3 evt_dttm2 = evt_dttm4 evt_dttm3 = evt_dttm5 - evt_dttm4 = cast(datetime, self._astral_event(date + ONE_DAY, "solar_noon")) + evt_dttm4 = cast( + datetime, self._astral_event(date + ONE_DAY, "solar_noon", False) + ) evt_dttm5 = cast( - datetime, self._astral_event(date + 2 * ONE_DAY, "solar_midnight") + datetime, + self._astral_event(date + 2 * ONE_DAY, "solar_midnight", False), ) # Didn't find one. @@ -205,7 +211,7 @@ def _update(self, cur_dttm: datetime) -> None: nxt_dttm = self._get_nxt_dttm(cur_dttm) @callback - def schedule_update(now: datetime) -> None: + def schedule_update(_now: datetime) -> None: """Schedule state update.""" self._unsub_update = None self.async_schedule_update_ha_state(True) @@ -214,7 +220,7 @@ def schedule_update(now: datetime) -> None: self._unsub_update = async_track_point_in_utc_time( self.hass, schedule_update, nxt_dttm ) - nxt_dttm = dt_util.as_local(nxt_dttm) + nxt_dttm = self._as_tz(nxt_dttm) elif self.hass.state == CoreState.running: LOGGER.error( "%s: Sun elevation never reaches %f at this location", diff --git a/custom_components/sun2/config.py b/custom_components/sun2/config.py index b295afd..428a774 100644 --- a/custom_components/sun2/config.py +++ b/custom_components/sun2/config.py @@ -42,7 +42,7 @@ _COERCE_NUM = vol.Any(vol.Coerce(int), vol.Coerce(float)) PACKAGE_MERGE_HINT = "list" -SUN_DIRECTIONS = [dir.lower() for dir in SunDirection.__members__] +SUN_DIRECTIONS = [sd.lower() for sd in SunDirection.__members__] SUN2_LOCATION_BASE_SCHEMA = vol.Schema( { vol.Inclusive(CONF_LATITUDE, "location"): cv.latitude, @@ -157,19 +157,19 @@ def obs_elv_from_options( if obs_elv_option := options.get(CONF_OBS_ELV): east_obs_elv, west_obs_elv = obs_elv_option - if isinstance(east_obs_elv, Num) and isinstance(west_obs_elv, Num): # type: ignore[misc, arg-type] + if isinstance(east_obs_elv, Num) and isinstance(west_obs_elv, Num): assert east_obs_elv == west_obs_elv return cast(Num, east_obs_elv) obs_elv: ConfigType = {} - if isinstance(east_obs_elv, Num): # type: ignore[misc, arg-type] + if isinstance(east_obs_elv, Num): obs_elv[CONF_ABOVE_GROUND] = east_obs_elv else: obs_elv[CONF_SUNRISE_OBSTRUCTION] = { CONF_DISTANCE: east_obs_elv[1], CONF_RELATIVE_HEIGHT: east_obs_elv[0], } - if isinstance(west_obs_elv, Num): # type: ignore[misc, arg-type] + if isinstance(west_obs_elv, Num): obs_elv[CONF_ABOVE_GROUND] = west_obs_elv else: obs_elv[CONF_SUNSET_OBSTRUCTION] = { @@ -230,7 +230,7 @@ def options_from_obs_elv( ) east_obs_elv = west_obs_elv = hass.config.elevation - elif isinstance(obs := loc_config[CONF_OBS_ELV], Num): # type: ignore[misc, arg-type] + elif isinstance(obs := loc_config[CONF_OBS_ELV], Num): east_obs_elv = west_obs_elv = obs else: diff --git a/custom_components/sun2/config_flow.py b/custom_components/sun2/config_flow.py index a5d602d..1dda1d6 100644 --- a/custom_components/sun2/config_flow.py +++ b/custom_components/sun2/config_flow.py @@ -255,8 +255,8 @@ async def async_step_observer_elevation( if obs_elv := self.options.get(CONF_OBS_ELV): suggested_values = { - CONF_SUNRISE_OBSTRUCTION: not isinstance(obs_elv[0], Num), # type: ignore[misc, arg-type] - CONF_SUNSET_OBSTRUCTION: not isinstance(obs_elv[1], Num), # type: ignore[misc, arg-type] + CONF_SUNRISE_OBSTRUCTION: not isinstance(obs_elv[0], Num), + CONF_SUNSET_OBSTRUCTION: not isinstance(obs_elv[1], Num), } else: suggested_values = { @@ -297,7 +297,7 @@ async def async_step_obs_elv_values( self.options[CONF_ELEVATION] = above_ground return await self.async_step_entities_menu() - schema: dict[str, Any] = {} + schema: dict[vol.Schemable, Any] = {} if get_above_ground: schema[vol.Required(CONF_ABOVE_GROUND)] = _POSITIVE_METERS_SELECTOR if self._sunrise_obstruction: @@ -312,11 +312,11 @@ async def async_step_obs_elv_values( sunrise_distance = sunset_distance = 1000 sunrise_relative_height = sunset_relative_height = 1000 if obs_elv := self.options.get(CONF_OBS_ELV): - if isinstance(obs_elv[0], Num): # type: ignore[misc, arg-type] + if isinstance(obs_elv[0], Num): above_ground = obs_elv[0] else: sunrise_relative_height, sunrise_distance = obs_elv[0] - if isinstance(obs_elv[1], Num): # type: ignore[misc, arg-type] + if isinstance(obs_elv[1], Num): # If both directions use above_ground, they should be the same. # Assume this is true and don't bother checking here. above_ground = obs_elv[1] diff --git a/custom_components/sun2/helpers.py b/custom_components/sun2/helpers.py index 7c9ea5d..0e13a01 100644 --- a/custom_components/sun2/helpers.py +++ b/custom_components/sun2/helpers.py @@ -5,7 +5,10 @@ from collections.abc import Callable, Iterable, Mapping from dataclasses import dataclass, field from datetime import date, datetime, time, timedelta, tzinfo -from functools import cached_property, lru_cache +from functools import ( # pylint: disable=hass-deprecated-import + cached_property, + lru_cache, +) import logging from math import copysign, fabs from typing import Any, Self, cast, overload @@ -27,7 +30,7 @@ try: from homeassistant.core_config import Config except ImportError: - from homeassistant.core import Config + from homeassistant.core import Config # type: ignore[no-redef] from homeassistant.helpers.device_registry import DeviceEntryType @@ -194,9 +197,9 @@ def _obs_elv_2_astral( Also, astral only accepts a tuple, not a list, which is what stored in the config entry (since it's from a JSON file), so convert to a tuple. """ - if isinstance(obs_elv, Num): # type: ignore[misc, arg-type] - return float(cast(Num, obs_elv)) - height, distance = cast(list[Num], obs_elv) + if isinstance(obs_elv, Num): + return float(obs_elv) + height, distance = obs_elv return -copysign(1, float(height)) * float(distance), fabs(float(height)) @classmethod @@ -349,9 +352,13 @@ def __init__(self, sun2_entity_params: Sun2EntityParams) -> None: self._astral_data = sun2_entity_params.astral_data self.async_on_remove(self._cancel_update) + def _as_tz(self, dttm: datetime) -> datetime: + """Return datetime in location's time zone.""" + return dttm.astimezone(self._astral_data.loc_data.tzi) + async def async_update(self) -> None: """Update state.""" - self._update(dt_util.now(self._astral_data.loc_data.tzi)) + self._update(dt_util.utcnow()) async def async_added_to_hass(self) -> None: """Run when entity about to be added to hass.""" @@ -390,6 +397,7 @@ def _astral_event( self, date_or_dttm: date | datetime, event: str | None = None, + local: bool = True, /, **kwargs: Any, ) -> Any: @@ -402,11 +410,11 @@ def _astral_event( try: if event in ("solar_midnight", "solar_noon"): - return getattr(loc, event.split("_")[1])(date_or_dttm) + return getattr(loc, event.split("_")[1])(date_or_dttm, local) if event == "time_at_elevation": return loc.time_at_elevation( - kwargs["elevation"], date_or_dttm, kwargs["direction"] + kwargs["elevation"], date_or_dttm, kwargs["direction"], local ) if event in ("sunrise", "dawn"): @@ -415,6 +423,8 @@ def _astral_event( kwargs = {"observer_elevation": self._astral_data.obs_elvs.west} else: kwargs = {} + if event not in ("solar_azimuth", "solar_elevation"): + kwargs["local"] = local return getattr(loc, event)(date_or_dttm, **kwargs) except (TypeError, ValueError): diff --git a/custom_components/sun2/sensor.py b/custom_components/sun2/sensor.py index 0fadf5b..7102b04 100644 --- a/custom_components/sun2/sensor.py +++ b/custom_components/sun2/sensor.py @@ -2,13 +2,13 @@ from __future__ import annotations from abc import abstractmethod -from collections.abc import Iterable, Mapping, MutableMapping, Sequence +from collections.abc import Iterable, Mapping, Sequence from contextlib import suppress from dataclasses import dataclass from datetime import date, datetime, time, timedelta from itertools import chain from math import ceil, floor -from typing import Any, Generic, Optional, TypeVar, Union, cast +from typing import Any, Generic, TypeVar, cast from astral import SunDirection from astral.sun import SUN_APPARENT_RADIUS @@ -194,25 +194,25 @@ def _setup_fixed_updating(self) -> None: @callback def async_schedule_update_at_midnight(now: datetime) -> None: """Schedule an update at midnight.""" - next_midn = next_midnight(now.astimezone(self._astral_data.loc_data.tzi)) + next_midn = next_midnight(self._as_tz(now)) self._unsub_update = async_track_point_in_utc_time( self.hass, async_schedule_update_at_midnight, next_midn ) self.async_schedule_update_ha_state(True) - next_midn = next_midnight(dt_util.now(self._astral_data.loc_data.tzi)) + next_midn = next_midnight(self._as_tz(dt_util.utcnow())) self._unsub_update = async_track_point_in_utc_time( self.hass, async_schedule_update_at_midnight, next_midn ) def _update(self, cur_dttm: datetime) -> None: """Update state.""" - cur_date = cur_dttm.date() - self._yesterday = cast(Optional[_T], self._astral_event(cur_date - ONE_DAY)) + cur_date = self._as_tz(cur_dttm).date() + self._yesterday = cast(_T | None, self._astral_event(cur_date - ONE_DAY)) self._attr_native_value = self._today = cast( - Optional[_T], self._astral_event(cur_date) + _T | None, self._astral_event(cur_date) ) - self._tomorrow = cast(Optional[_T], self._astral_event(cur_date + ONE_DAY)) + self._tomorrow = cast(_T | None, self._astral_event(cur_date + ONE_DAY)) class Sun2ElevationAtTimeSensor(Sun2SensorEntity[float]): @@ -324,15 +324,15 @@ def _update(self, cur_dttm: datetime) -> None: dttm = self._at_time else: dttm = datetime.combine(cur_dttm.date(), self._at_time) - self._attr_native_value = cast(Optional[float], self._astral_event(dttm)) + self._attr_native_value = cast(float | None, self._astral_event(dttm)) if isinstance(self._at_time, datetime): return - self._yesterday = cast(Optional[float], self._astral_event(dttm - ONE_DAY)) + self._yesterday = cast(float | None, self._astral_event(dttm - ONE_DAY)) self._today = self._attr_native_value - self._tomorrow = cast(Optional[float], self._astral_event(dttm + ONE_DAY)) + self._tomorrow = cast(float | None, self._astral_event(dttm + ONE_DAY)) -class Sun2PointInTimeSensor(Sun2SensorEntity[Union[datetime, str]]): +class Sun2PointInTimeSensor(Sun2SensorEntity[datetime | str]): """Sun2 Point in Time Sensor.""" def __init__( @@ -376,6 +376,7 @@ def _astral_event( self, date_or_dttm: date | datetime, event: str | None = None, + local: bool = True, /, **kwargs: Any, ) -> Any: @@ -441,6 +442,7 @@ def _astral_event( self, date_or_dttm: date | datetime, event: str | None = None, + local: bool = True, /, **kwargs: Any, ) -> float | None: @@ -448,11 +450,11 @@ def _astral_event( start: datetime | None end: datetime | None if self._event == "daylight": - start = super()._astral_event(date_or_dttm, "dawn") - end = super()._astral_event(date_or_dttm, "dusk") + start = super()._astral_event(date_or_dttm, "dawn", False) + end = super()._astral_event(date_or_dttm, "dusk", False) else: - start = super()._astral_event(date_or_dttm, "dusk") - end = super()._astral_event(date_or_dttm + ONE_DAY, "dawn") + start = super()._astral_event(date_or_dttm, "dusk", False) + end = super()._astral_event(date_or_dttm + ONE_DAY, "dawn", False) if not start or not end: return None return (end - start).total_seconds() / 3600 @@ -482,12 +484,13 @@ def _astral_event( self, date_or_dttm: date | datetime, event: str | None = None, + local: bool = True, /, **kwargs: Any, ) -> float | None: """Return astral event result.""" return cast( - Optional[float], + float | None, super()._astral_event( cast(datetime, super()._astral_event(date_or_dttm)), "solar_elevation" ), @@ -516,6 +519,7 @@ def _astral_event( self, date_or_dttm: date | datetime, event: str | None = None, + local: bool = True, /, **kwargs: Any, ) -> float | None: @@ -531,7 +535,7 @@ def _astral_event( dttm = getattr(self._astral_data.loc_data.loc, self._method)(date_or_dttm) except (TypeError, ValueError): return None - return cast(Optional[float], super()._astral_event(dttm)) + return cast(float | None, super()._astral_event(dttm)) @dataclass @@ -587,7 +591,7 @@ def _update_astral_data(self, astral_data: AstralData) -> None: def _setup_fixed_updating(self) -> None: """Set up fixed updating.""" - def _attrs_at_elev(self, elev: Num) -> MutableMapping[str, Any]: + def _attrs_at_elev(self, elev: Num) -> dict[str, Any]: """Return attributes at elevation.""" assert self._cp @@ -607,15 +611,15 @@ def _attrs_at_elev(self, elev: Num) -> MutableMapping[str, Any]: icon = "mdi:weather-night" return {ATTR_ICON: icon} - def _set_attrs(self, attrs: MutableMapping[str, Any], nxt_chg: datetime) -> None: + def _set_attrs(self, attrs: dict[str, Any], nxt_chg: datetime) -> None: """Set attributes.""" - self._attr_icon = cast(Optional[str], attrs.pop(ATTR_ICON, "mdi:weather-sunny")) - attrs[ATTR_NEXT_CHANGE] = dt_util.as_local(nxt_chg) + self._attr_icon = cast(str | None, attrs.pop(ATTR_ICON, "mdi:weather-sunny")) + attrs[ATTR_NEXT_CHANGE] = self._as_tz(nxt_chg) self._attr_extra_state_attributes = attrs def _get_curve_params(self, cur_dttm: datetime, cur_elev: Num) -> CurveParameters: """Calculate elevation curve parameters.""" - cur_date = cur_dttm.date() + cur_date = self._as_tz(cur_dttm).date() # Find the highest and lowest points on the elevation curve that encompass # current time, where it is ok for the current time to be the same as the @@ -623,12 +627,14 @@ def _get_curve_params(self, cur_dttm: datetime, cur_elev: Num) -> CurveParameter # Note that the astral solar_midnight event will always come before the astral # solar_noon event for any given date, even if it actually falls on the previous # day. - hi_dttm = cast(datetime, self._astral_event(cur_date, "solar_noon")) - lo_dttm = cast(datetime, self._astral_event(cur_date, "solar_midnight")) - nxt_noon = cast(datetime, self._astral_event(cur_date + ONE_DAY, "solar_noon")) + hi_dttm = cast(datetime, self._astral_event(cur_date, "solar_noon", False)) + lo_dttm = cast(datetime, self._astral_event(cur_date, "solar_midnight", False)) + nxt_noon = cast( + datetime, self._astral_event(cur_date + ONE_DAY, "solar_noon", False) + ) if cur_dttm < lo_dttm: tl_dttm = cast( - datetime, self._astral_event(cur_date - ONE_DAY, "solar_noon") + datetime, self._astral_event(cur_date - ONE_DAY, "solar_noon", False) ) tr_dttm = lo_dttm elif cur_dttm < hi_dttm: @@ -636,7 +642,8 @@ def _get_curve_params(self, cur_dttm: datetime, cur_elev: Num) -> CurveParameter tr_dttm = hi_dttm else: lo_dttm = cast( - datetime, self._astral_event(cur_date + ONE_DAY, "solar_midnight") + datetime, + self._astral_event(cur_date + ONE_DAY, "solar_midnight", False), ) if cur_dttm < lo_dttm: tl_dttm = hi_dttm @@ -651,16 +658,16 @@ def _get_curve_params(self, cur_dttm: datetime, cur_elev: Num) -> CurveParameter LOGGER.debug( "%s: tL = %s/%0.3f, cur = %s/%0.3f, tR = %s/%0.3f, rising = %s", self.name, - tl_dttm, + self._as_tz(tl_dttm), tl_elev, - cur_dttm, + self._as_tz(cur_dttm), cur_elev, - tr_dttm, + self._as_tz(tr_dttm), tr_elev, rising, ) - mid_date = (tl_dttm + (tr_dttm - tl_dttm) / 2).date() + mid_date = self._as_tz(tl_dttm + (tr_dttm - tl_dttm) / 2).date() return CurveParameters( tl_dttm, tl_elev, tr_dttm, tr_elev, mid_date, nxt_noon, rising ) @@ -680,7 +687,7 @@ def _get_dttm_at_elev( est += 1 msg = ( msg_base - + f"t0 = {t0_dttm}/{t0_elev:+7.3f}, t1 = {t1_dttm}/{t1_elev:+7.3f} ->" + + f"t0 = {self._as_tz(t0_dttm)}/{t0_elev:+7.3f}, t1 = {self._as_tz(t1_dttm)}/{t1_elev:+7.3f} ->" ) try: est_dttm = nearest_second( @@ -697,7 +704,7 @@ def _get_dttm_at_elev( LOGGER.debug( "%s est = %s/%+7.3f[%+7.3f/%2i]", msg, - est_dttm, + self._as_tz(est_dttm), est_elev, est_elev - elev, est, @@ -805,7 +812,7 @@ class Update: remove: CALLBACK_TYPE when: datetime state: str | None - attrs: MutableMapping[str, Any] | None + attrs: dict[str, Any] | None class Sun2PhaseSensorBase(Sun2CPSensorEntity[str]): @@ -848,9 +855,8 @@ def _async_do_update(self, now: datetime) -> None: update = self._updates.pop(0) if self._updates: self._attr_native_value = update.state - self._set_attrs( - cast(MutableMapping[str, Any], update.attrs), self._updates[0].when - ) + assert update.attrs is not None + self._set_attrs(update.attrs, self._updates[0].when) self.async_write_ha_state() else: # The last one means it's time to determine the next set of scheduled @@ -861,7 +867,7 @@ def _setup_update_at_time( self, update_dttm: datetime, state: str | None = None, - attrs: MutableMapping[str, Any] | None = None, + attrs: dict[str, Any] | None = None, ) -> None: """Setu up update at given time.""" self._updates.append( @@ -898,6 +904,7 @@ def get_est_dttm(offset: timedelta | None = None) -> datetime: self._astral_event( self._cp.mid_date + offset if offset else self._cp.mid_date, "time_at_elevation", + False, elevation=elev, direction=SunDirection.RISING if self._cp.rising @@ -912,7 +919,7 @@ def get_est_dttm(offset: timedelta | None = None) -> datetime: ONE_DAY if est_dttm < self._cp.tl_dttm else -ONE_DAY ) if not self._cp.tl_dttm <= est_dttm < self._cp.tr_dttm: - raise ValueError + raise ValueError # noqa: TRY301 except (AttributeError, TypeError, ValueError) as exc: if not isinstance(exc, ValueError): # time_at_elevation doesn't always work around solar midnight & solar @@ -925,7 +932,7 @@ def get_est_dttm(offset: timedelta | None = None) -> datetime: "%s: time_at_elevation(%0.3f) outside [tL, tR): %s", self.name, elev, - est_dttm, + self._as_tz(est_dttm), ) t0_dttm = self._cp.tl_dttm t1_dttm = self._cp.tr_dttm @@ -969,7 +976,7 @@ def _update(self, cur_dttm: datetime) -> None: if self._updates: return - start_update = dt_util.now() + start_update = dt_util.utcnow() # Astral package ignores microseconds, so round to nearest second # before continuing. @@ -991,7 +998,7 @@ def _update(self, cur_dttm: datetime) -> None: self._attr_native_value = self._state_at_elev(cur_elev) self._set_attrs(self._attrs_at_elev(cur_elev), self._updates[0].when) - LOGGER.debug("%s: _update time: %s", self.name, dt_util.now() - start_update) + LOGGER.debug("%s: _update time: %s", self.name, dt_util.utcnow() - start_update) class Sun2PhaseSensor(Sun2PhaseSensorBase): @@ -1010,8 +1017,8 @@ def __init__( (90, None), ) elevs, states = cast( - tuple[tuple[Num], tuple[Optional[str]]], - zip(*phases), + tuple[tuple[Num], tuple[str | None]], + zip(*phases, strict=True), ) rising_elevs = sorted([*elevs[1:-1], -4, 6]) rising_states = phases[:-1] @@ -1019,7 +1026,7 @@ def __init__( falling_states = tuple( cast( tuple[tuple[Num, str]], - zip(elevs[1:], states[:-1]), + zip(elevs[1:], states[:-1], strict=True), ) )[::-1] super().__init__( @@ -1029,7 +1036,7 @@ def __init__( PhaseData(rising_elevs, rising_states, falling_elevs, falling_states), ) - def _attrs_at_elev(self, elev: Num) -> MutableMapping[str, Any]: + def _attrs_at_elev(self, elev: Num) -> dict[str, Any]: """Return attributes at elevation.""" assert self._cp @@ -1064,21 +1071,21 @@ def __init__( (90, None, "solar_noon"), ) elevs, r_states, f_states = cast( - tuple[tuple[Num], tuple[Optional[str]], tuple[Optional[str]]], - zip(*phases), + tuple[tuple[Num], tuple[str | None], tuple[str | None]], + zip(*phases, strict=True), ) rising_elevs = elevs[1:-1] rising_states = tuple( cast( tuple[tuple[Num, str]], - zip(elevs[:-1], r_states[:-1]), + zip(elevs[:-1], r_states[:-1], strict=True), ) ) falling_elevs = rising_elevs[::-1] falling_states = tuple( cast( tuple[tuple[Num, str]], - zip(elevs[1:], f_states[1:]), + zip(elevs[1:], f_states[1:], strict=True), ) )[::-1] super().__init__( @@ -1088,7 +1095,7 @@ def __init__( PhaseData(rising_elevs, rising_states, falling_elevs, falling_states), ) - def _attrs_at_elev(self, elev: Num) -> MutableMapping[str, Any]: + def _attrs_at_elev(self, elev: Num) -> dict[str, Any]: """Return attributes at elevation.""" assert self._cp From bfad86c0d07ea44adb8c15cdff94a2014642d616 Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Wed, 13 Nov 2024 09:33:18 -0600 Subject: [PATCH 2/2] Bump version to 3.3.5b0 --- custom_components/sun2/manifest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/sun2/manifest.json b/custom_components/sun2/manifest.json index 9fde4af..5f815f1 100644 --- a/custom_components/sun2/manifest.json +++ b/custom_components/sun2/manifest.json @@ -4,9 +4,9 @@ "codeowners": ["@pnbruckner"], "config_flow": true, "dependencies": [], - "documentation": "https://github.com/pnbruckner/ha-sun2/blob/3.3.4/README.md", + "documentation": "https://github.com/pnbruckner/ha-sun2/blob/3.3.5b0/README.md", "iot_class": "calculated", "issue_tracker": "https://github.com/pnbruckner/ha-sun2/issues", "requirements": [], - "version": "3.3.4" + "version": "3.3.5b0" }