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/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" } 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