340 lines
13 KiB
Python
340 lines
13 KiB
Python
"""Support for the Open-Meteo Solar Forecast sensor service."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Callable
|
|
from dataclasses import dataclass
|
|
from datetime import datetime, timedelta
|
|
from typing import Any
|
|
|
|
from homeassistant.components.sensor import (
|
|
DOMAIN as SENSOR_DOMAIN,
|
|
)
|
|
from homeassistant.components.sensor import (
|
|
SensorDeviceClass,
|
|
SensorEntity,
|
|
SensorEntityDescription,
|
|
SensorStateClass,
|
|
)
|
|
from homeassistant.config_entries import ConfigEntry
|
|
from homeassistant.const import UnitOfEnergy, UnitOfPower
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|
from homeassistant.helpers.event import async_track_utc_time_change
|
|
from homeassistant.helpers.typing import StateType
|
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
|
|
|
from open_meteo_solar_forecast.models import Estimate
|
|
|
|
from .const import ATTR_WATTS, ATTR_WH_PERIOD, DOMAIN
|
|
from .coordinator import OpenMeteoSolarForecastDataUpdateCoordinator
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class OpenMeteoSolarForecastSensorEntityDescription(SensorEntityDescription):
|
|
"""Describes a Forecast.Solar Sensor."""
|
|
|
|
state: Callable[[Estimate], Any] | None = None
|
|
|
|
|
|
SENSORS: tuple[OpenMeteoSolarForecastSensorEntityDescription, ...] = (
|
|
OpenMeteoSolarForecastSensorEntityDescription(
|
|
key="energy_production_today",
|
|
translation_key="energy_production_today",
|
|
state=lambda estimate: estimate.energy_production_today,
|
|
device_class=SensorDeviceClass.ENERGY,
|
|
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
|
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
|
suggested_display_precision=1,
|
|
),
|
|
OpenMeteoSolarForecastSensorEntityDescription(
|
|
key="energy_production_today_remaining",
|
|
translation_key="energy_production_today_remaining",
|
|
state=lambda estimate: estimate.energy_production_today_remaining,
|
|
device_class=SensorDeviceClass.ENERGY,
|
|
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
|
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
|
suggested_display_precision=1,
|
|
),
|
|
OpenMeteoSolarForecastSensorEntityDescription(
|
|
key="energy_production_tomorrow",
|
|
translation_key="energy_production_tomorrow",
|
|
state=lambda estimate: estimate.energy_production_tomorrow,
|
|
device_class=SensorDeviceClass.ENERGY,
|
|
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
|
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
|
suggested_display_precision=1,
|
|
),
|
|
OpenMeteoSolarForecastSensorEntityDescription(
|
|
key="energy_production_d2",
|
|
translation_key="energy_production_d2",
|
|
state=lambda estimate: estimate.day_production(
|
|
estimate.now().date() + timedelta(days=2)
|
|
),
|
|
device_class=SensorDeviceClass.ENERGY,
|
|
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
|
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
|
suggested_display_precision=1,
|
|
),
|
|
OpenMeteoSolarForecastSensorEntityDescription(
|
|
key="energy_production_d3",
|
|
translation_key="energy_production_d3",
|
|
state=lambda estimate: estimate.day_production(
|
|
estimate.now().date() + timedelta(days=3)
|
|
),
|
|
device_class=SensorDeviceClass.ENERGY,
|
|
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
|
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
|
suggested_display_precision=1,
|
|
),
|
|
OpenMeteoSolarForecastSensorEntityDescription(
|
|
key="energy_production_d4",
|
|
translation_key="energy_production_d4",
|
|
state=lambda estimate: estimate.day_production(
|
|
estimate.now().date() + timedelta(days=4)
|
|
),
|
|
device_class=SensorDeviceClass.ENERGY,
|
|
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
|
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
|
suggested_display_precision=1,
|
|
),
|
|
OpenMeteoSolarForecastSensorEntityDescription(
|
|
key="energy_production_d5",
|
|
translation_key="energy_production_d5",
|
|
state=lambda estimate: estimate.day_production(
|
|
estimate.now().date() + timedelta(days=5)
|
|
),
|
|
device_class=SensorDeviceClass.ENERGY,
|
|
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
|
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
|
suggested_display_precision=1,
|
|
),
|
|
OpenMeteoSolarForecastSensorEntityDescription(
|
|
key="energy_production_d6",
|
|
translation_key="energy_production_d6",
|
|
state=lambda estimate: estimate.day_production(
|
|
estimate.now().date() + timedelta(days=6)
|
|
),
|
|
device_class=SensorDeviceClass.ENERGY,
|
|
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
|
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
|
suggested_display_precision=1,
|
|
),
|
|
OpenMeteoSolarForecastSensorEntityDescription(
|
|
key="energy_production_d7",
|
|
translation_key="energy_production_d7",
|
|
state=lambda estimate: estimate.day_production(
|
|
estimate.now().date() + timedelta(days=7)
|
|
),
|
|
device_class=SensorDeviceClass.ENERGY,
|
|
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
|
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
|
suggested_display_precision=1,
|
|
),
|
|
OpenMeteoSolarForecastSensorEntityDescription(
|
|
key="power_highest_peak_time_today",
|
|
translation_key="power_highest_peak_time_today",
|
|
device_class=SensorDeviceClass.TIMESTAMP,
|
|
),
|
|
OpenMeteoSolarForecastSensorEntityDescription(
|
|
key="power_highest_peak_time_tomorrow",
|
|
translation_key="power_highest_peak_time_tomorrow",
|
|
device_class=SensorDeviceClass.TIMESTAMP,
|
|
),
|
|
OpenMeteoSolarForecastSensorEntityDescription(
|
|
key="power_production_now",
|
|
translation_key="power_production_now",
|
|
device_class=SensorDeviceClass.POWER,
|
|
state=lambda estimate: estimate.power_production_now,
|
|
state_class=SensorStateClass.MEASUREMENT,
|
|
native_unit_of_measurement=UnitOfPower.WATT,
|
|
),
|
|
OpenMeteoSolarForecastSensorEntityDescription(
|
|
key="power_production_next_15minutes",
|
|
translation_key="power_production_next_15minutes",
|
|
state=lambda estimate: estimate.power_production_at_time(
|
|
estimate.now() + timedelta(minutes=15)
|
|
),
|
|
device_class=SensorDeviceClass.POWER,
|
|
entity_registry_enabled_default=False,
|
|
native_unit_of_measurement=UnitOfPower.WATT,
|
|
),
|
|
OpenMeteoSolarForecastSensorEntityDescription(
|
|
key="power_production_next_30minutes",
|
|
translation_key="power_production_next_30minutes",
|
|
state=lambda estimate: estimate.power_production_at_time(
|
|
estimate.now() + timedelta(minutes=30)
|
|
),
|
|
device_class=SensorDeviceClass.POWER,
|
|
entity_registry_enabled_default=False,
|
|
native_unit_of_measurement=UnitOfPower.WATT,
|
|
),
|
|
OpenMeteoSolarForecastSensorEntityDescription(
|
|
key="power_production_next_hour",
|
|
translation_key="power_production_next_hour",
|
|
state=lambda estimate: estimate.power_production_at_time(
|
|
estimate.now() + timedelta(hours=1)
|
|
),
|
|
device_class=SensorDeviceClass.POWER,
|
|
entity_registry_enabled_default=False,
|
|
native_unit_of_measurement=UnitOfPower.WATT,
|
|
),
|
|
OpenMeteoSolarForecastSensorEntityDescription(
|
|
key="power_production_next_12hours",
|
|
translation_key="power_production_next_12hours",
|
|
state=lambda estimate: estimate.power_production_at_time(
|
|
estimate.now() + timedelta(hours=12)
|
|
),
|
|
device_class=SensorDeviceClass.POWER,
|
|
entity_registry_enabled_default=False,
|
|
native_unit_of_measurement=UnitOfPower.WATT,
|
|
),
|
|
OpenMeteoSolarForecastSensorEntityDescription(
|
|
key="power_production_next_24hours",
|
|
translation_key="power_production_next_24hours",
|
|
state=lambda estimate: estimate.power_production_at_time(
|
|
estimate.now() + timedelta(hours=24)
|
|
),
|
|
device_class=SensorDeviceClass.POWER,
|
|
entity_registry_enabled_default=False,
|
|
native_unit_of_measurement=UnitOfPower.WATT,
|
|
),
|
|
OpenMeteoSolarForecastSensorEntityDescription(
|
|
key="energy_current_hour",
|
|
translation_key="energy_current_hour",
|
|
state=lambda estimate: estimate.energy_current_hour,
|
|
device_class=SensorDeviceClass.ENERGY,
|
|
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
|
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
|
suggested_display_precision=1,
|
|
),
|
|
OpenMeteoSolarForecastSensorEntityDescription(
|
|
key="energy_next_hour",
|
|
translation_key="energy_next_hour",
|
|
state=lambda estimate: estimate.sum_energy_production(1),
|
|
device_class=SensorDeviceClass.ENERGY,
|
|
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
|
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
|
suggested_display_precision=1,
|
|
),
|
|
)
|
|
|
|
|
|
async def async_setup_entry(
|
|
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
|
) -> None:
|
|
"""Defer sensor setup to the shared sensor module."""
|
|
coordinator: OpenMeteoSolarForecastDataUpdateCoordinator = hass.data[DOMAIN][
|
|
entry.entry_id
|
|
]
|
|
|
|
async_add_entities(
|
|
OpenMeteoSolarForecastSensorEntity(
|
|
entry_id=entry.entry_id,
|
|
coordinator=coordinator,
|
|
entity_description=entity_description,
|
|
)
|
|
for entity_description in SENSORS
|
|
)
|
|
|
|
|
|
class OpenMeteoSolarForecastSensorEntity(
|
|
CoordinatorEntity[OpenMeteoSolarForecastDataUpdateCoordinator], SensorEntity
|
|
):
|
|
"""Defines a Open-Meteo sensor."""
|
|
|
|
entity_description: OpenMeteoSolarForecastSensorEntityDescription
|
|
_attr_has_entity_name = True
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
entry_id: str,
|
|
coordinator: OpenMeteoSolarForecastDataUpdateCoordinator,
|
|
entity_description: OpenMeteoSolarForecastSensorEntityDescription,
|
|
) -> None:
|
|
"""Initialize Open-Meteo Solar sensor."""
|
|
super().__init__(coordinator=coordinator)
|
|
self.entity_description = entity_description
|
|
self.entity_id = f"{SENSOR_DOMAIN}.{entity_description.key}"
|
|
self._attr_unique_id = f"{entry_id}_{entity_description.key}"
|
|
|
|
self._attr_device_info = DeviceInfo(
|
|
entry_type=DeviceEntryType.SERVICE,
|
|
identifiers={(DOMAIN, entry_id)},
|
|
manufacturer="Open-Meteo",
|
|
name="Solar production forecast",
|
|
configuration_url="https://open-meteo.com",
|
|
)
|
|
|
|
async def _update_callback(self, now: datetime) -> None:
|
|
"""Update the entity without fetching data from server.
|
|
|
|
This is required for the power_production_* sensors to update
|
|
as they take data in 15-minute intervals and the update interval
|
|
is 30 minutes."""
|
|
self.async_write_ha_state()
|
|
|
|
async def async_added_to_hass(self) -> None:
|
|
"""Register callbacks."""
|
|
await super().async_added_to_hass()
|
|
|
|
# Update the state of the sensor every minute without
|
|
# fetching new data from the server.
|
|
async_track_utc_time_change(
|
|
self.hass,
|
|
self._update_callback,
|
|
second=0,
|
|
)
|
|
|
|
@property
|
|
def native_value(self) -> datetime | StateType:
|
|
"""Return the state of the sensor."""
|
|
if self.entity_description.state is None:
|
|
state: StateType | datetime = getattr(
|
|
self.coordinator.data, self.entity_description.key
|
|
)
|
|
else:
|
|
state = self.entity_description.state(self.coordinator.data)
|
|
|
|
return state
|
|
|
|
@property
|
|
def extra_state_attributes(self) -> dict[str, Any] | None:
|
|
"""Return the state attributes."""
|
|
if self.entity_description.key.startswith(
|
|
"energy_production_d"
|
|
) or self.entity_description.key in (
|
|
"energy_production_today",
|
|
"energy_production_tomorrow",
|
|
):
|
|
target_date = self.coordinator.data.now().date()
|
|
if self.entity_description.key == "energy_production_tomorrow":
|
|
target_date += timedelta(days=1)
|
|
elif self.entity_description.key.startswith("energy_production_d"):
|
|
target_date += timedelta(
|
|
days=int(self.entity_description.key[len("energy_production_d") :])
|
|
)
|
|
elif self.entity_description.key == "energy_production_today":
|
|
pass # target_date is already set to today
|
|
else:
|
|
raise ValueError(
|
|
f"Unexpected key {self.entity_description.key} for extra_state_attributes"
|
|
)
|
|
|
|
return {
|
|
ATTR_WATTS: {
|
|
watt_datetime.isoformat(): watt_value
|
|
for watt_datetime, watt_value in self.coordinator.data.watts.items()
|
|
if watt_datetime.date() == target_date
|
|
},
|
|
ATTR_WH_PERIOD: {
|
|
wh_datetime.isoformat(): wh_value
|
|
for wh_datetime, wh_value in self.coordinator.data.wh_period.items()
|
|
if wh_datetime.date() == target_date
|
|
},
|
|
}
|
|
|
|
return None
|