|
6 | 6 |
|
7 | 7 | from __future__ import annotations |
8 | 8 |
|
| 9 | +import datetime as _dt |
9 | 10 | import logging |
10 | 11 | from datetime import datetime |
11 | 12 | from typing import Any |
@@ -267,6 +268,86 @@ def extra_state_attributes(self) -> dict[str, Any] | None: |
267 | 268 | } |
268 | 269 |
|
269 | 270 |
|
| 271 | +class OneKomma5CheapestChargingWindowTomorrowSensor(OneKomma5PriceEntity, SensorEntity): |
| 272 | + """Cheapest N-min charging window in **tomorrow's** forecast (HA-local time). |
| 273 | +
|
| 274 | + Twin of `OneKomma5CheapestChargingWindowSensor` but for tomorrow. Uses the |
| 275 | + same option-driven duration. No lock-in semantic — tomorrow's window |
| 276 | + can't be acted on before midnight, so re-picking on every refresh is |
| 277 | + safe (and necessary as prices update). State is `unknown` until the |
| 278 | + price coordinator picks up tomorrow's prices, typically around 13:00 |
| 279 | + CET on the previous day. |
| 280 | + """ |
| 281 | + |
| 282 | + _attr_translation_key = "cheapest_charging_window_tomorrow" |
| 283 | + _attr_device_class = SensorDeviceClass.TIMESTAMP |
| 284 | + _attr_icon = "mdi:ev-station" |
| 285 | + |
| 286 | + def __init__( |
| 287 | + self, |
| 288 | + coordinator: Any, |
| 289 | + system_id: str, |
| 290 | + system_name: str, |
| 291 | + duration_minutes: int, |
| 292 | + ) -> None: |
| 293 | + super().__init__(coordinator, system_id, system_name, "cheapest_charging_window_tomorrow") |
| 294 | + self._duration_minutes = duration_minutes |
| 295 | + self._slot_count = duration_minutes // 15 |
| 296 | + self._window: dict[str, Any] | None = None |
| 297 | + |
| 298 | + @callback |
| 299 | + def _handle_coordinator_update(self) -> None: |
| 300 | + self._refresh_window() |
| 301 | + super()._handle_coordinator_update() |
| 302 | + |
| 303 | + def _refresh_window(self) -> None: |
| 304 | + self._window = None |
| 305 | + if self.coordinator.data is None or not self.coordinator.data.forecast: |
| 306 | + return |
| 307 | + now_local = dt_util.now() |
| 308 | + tomorrow_local = now_local.date() + _dt.timedelta(days=1) |
| 309 | + start_of_tomorrow = now_local.replace( |
| 310 | + year=tomorrow_local.year, |
| 311 | + month=tomorrow_local.month, |
| 312 | + day=tomorrow_local.day, |
| 313 | + hour=0, |
| 314 | + minute=0, |
| 315 | + second=0, |
| 316 | + microsecond=0, |
| 317 | + ) |
| 318 | + end_of_tomorrow = start_of_tomorrow.replace( |
| 319 | + hour=23, minute=59, second=59, microsecond=999999 |
| 320 | + ) |
| 321 | + self._window = find_cheapest_window( |
| 322 | + self.coordinator.data.forecast, |
| 323 | + self._slot_count, |
| 324 | + earliest_start=dt_util.as_utc(start_of_tomorrow), |
| 325 | + latest_end=dt_util.as_utc(end_of_tomorrow), |
| 326 | + ) |
| 327 | + |
| 328 | + async def async_added_to_hass(self) -> None: |
| 329 | + await super().async_added_to_hass() |
| 330 | + self._refresh_window() |
| 331 | + |
| 332 | + @property |
| 333 | + def native_value(self) -> datetime | None: |
| 334 | + if self._window is None: |
| 335 | + return None |
| 336 | + return datetime.fromisoformat(self._window["start"]) |
| 337 | + |
| 338 | + @property |
| 339 | + def extra_state_attributes(self) -> dict[str, Any] | None: |
| 340 | + if self._window is None: |
| 341 | + return None |
| 342 | + return { |
| 343 | + "start": self._window["start"], |
| 344 | + "end": self._window["end"], |
| 345 | + "average_price": self._window["average_price"], |
| 346 | + "duration_minutes": self._duration_minutes, |
| 347 | + "slot_count": self._window["slot_count"], |
| 348 | + } |
| 349 | + |
| 350 | + |
270 | 351 | class OneKomma5EVSensor(OneKomma5EVEntity, SensorEntity): |
271 | 352 | """Sensor for EV vehicle data.""" |
272 | 353 |
|
|
0 commit comments