Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 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
1 change: 0 additions & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,6 @@ omit =
homeassistant/components/clickatell/notify.py
homeassistant/components/clicksend/notify.py
homeassistant/components/clicksend_tts/notify.py
homeassistant/components/climacell/weather.py
homeassistant/components/cmus/media_player.py
homeassistant/components/co2signal/*
homeassistant/components/coinbase/*
Expand Down
226 changes: 180 additions & 46 deletions homeassistant/components/climacell/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,9 @@
from math import ceil
from typing import Any

from pyclimacell import ClimaCell
from pyclimacell.const import (
FORECAST_DAILY,
FORECAST_HOURLY,
FORECAST_NOWCAST,
REALTIME,
)
from pyclimacell.pyclimacell import (
from pyclimacell import ClimaCellV3, ClimaCellV4
from pyclimacell.const import CURRENT, DAILY, FORECASTS, HOURLY, NOWCAST
from pyclimacell.exceptions import (
CantConnectException,
InvalidAPIKeyException,
RateLimitedException,
Expand All @@ -23,7 +18,13 @@

from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
from homeassistant.const import (
CONF_API_KEY,
CONF_API_VERSION,
CONF_LATITUDE,
CONF_LONGITUDE,
CONF_NAME,
)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.helpers.update_coordinator import (
Expand All @@ -34,15 +35,34 @@

from .const import (
ATTRIBUTION,
CC_ATTR_CONDITION,
CC_ATTR_HUMIDITY,
CC_ATTR_OZONE,
CC_ATTR_PRECIPITATION,
CC_ATTR_PRECIPITATION_PROBABILITY,
CC_ATTR_PRESSURE,
CC_ATTR_TEMPERATURE,
CC_ATTR_TEMPERATURE_HIGH,
CC_ATTR_TEMPERATURE_LOW,
CC_ATTR_VISIBILITY,
CC_ATTR_WIND_DIRECTION,
CC_ATTR_WIND_SPEED,
CC_V3_ATTR_CONDITION,
CC_V3_ATTR_HUMIDITY,
CC_V3_ATTR_OZONE,
CC_V3_ATTR_PRECIPITATION,
CC_V3_ATTR_PRECIPITATION_DAILY,
CC_V3_ATTR_PRECIPITATION_PROBABILITY,
CC_V3_ATTR_PRESSURE,
CC_V3_ATTR_TEMPERATURE,
CC_V3_ATTR_VISIBILITY,
CC_V3_ATTR_WIND_DIRECTION,
CC_V3_ATTR_WIND_SPEED,
CONF_TIMESTEP,
CURRENT,
DAILY,
DEFAULT_FORECAST_TYPE,
DEFAULT_TIMESTEP,
DOMAIN,
FORECASTS,
HOURLY,
MAX_REQUESTS_PER_DAY,
NOWCAST,
)

_LOGGER = logging.getLogger(__name__)
Expand All @@ -54,6 +74,7 @@ def _set_update_interval(
hass: HomeAssistantType, current_entry: ConfigEntry
) -> timedelta:
"""Recalculate update_interval based on existing ClimaCell instances and update them."""
api_calls = 4 if current_entry.data[CONF_API_VERSION] == 3 else 2
# We check how many ClimaCell configured instances are using the same API key and
# calculate interval to not exceed allowed numbers of requests. Divide 90% of
# MAX_REQUESTS_PER_DAY by 4 because every update requires four API calls and we want
Expand All @@ -68,7 +89,7 @@ def _set_update_interval(
interval = timedelta(
minutes=(
ceil(
(24 * 60 * (len(other_instance_entry_ids) + 1) * 4)
(24 * 60 * (len(other_instance_entry_ids) + 1) * api_calls)
/ (MAX_REQUESTS_PER_DAY * 0.9)
)
)
Expand All @@ -94,15 +115,18 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry)
},
)

api_class = ClimaCellV3 if config_entry.data[CONF_API_VERSION] == 3 else ClimaCellV4
api = api_class(
config_entry.data[CONF_API_KEY],
config_entry.data.get(CONF_LATITUDE, hass.config.latitude),
config_entry.data.get(CONF_LONGITUDE, hass.config.longitude),
session=async_get_clientsession(hass),
)

coordinator = ClimaCellDataUpdateCoordinator(
hass,
config_entry,
ClimaCell(
config_entry.data[CONF_API_KEY],
config_entry.data.get(CONF_LATITUDE, hass.config.latitude),
config_entry.data.get(CONF_LONGITUDE, hass.config.longitude),
session=async_get_clientsession(hass),
),
api,
_set_update_interval(hass, config_entry),
)

Expand Down Expand Up @@ -138,19 +162,61 @@ async def async_unload_entry(
return unload_ok


async def async_migrate_entry(
hass: HomeAssistantType, config_entry: ConfigEntry
) -> bool:
"""Migrate old entry."""
version = config_entry.version

_LOGGER.debug("Migrating from version %s", version)

# 1 -> 2: Added new config key to support multiple API versions and limited nowcast timesteps
if version == 1:
Comment thread
raman325 marked this conversation as resolved.
Outdated
params = {}

# Add API version if not found
if CONF_API_VERSION not in config_entry.data:
new_data = config_entry.data.copy()
new_data[CONF_API_VERSION] = 3
params["data"] = new_data

# Use valid timestep if it's invalid
timestep = config_entry.options[CONF_TIMESTEP]
if timestep not in (1, 5, 15, 30):
if timestep <= 2:
timestep = 1
elif timestep <= 7:
timestep = 5
elif timestep <= 20:
timestep = 15
else:
timestep = 30
new_options = config_entry.options.copy()
new_options[CONF_TIMESTEP] = timestep
params["options"] = new_options

version = config_entry.version = 2
hass.config_entries.async_update_entry(config_entry, **params)

_LOGGER.info("Migration to version %s successful", version)

return True


class ClimaCellDataUpdateCoordinator(DataUpdateCoordinator):
"""Define an object to hold ClimaCell data."""

def __init__(
self,
hass: HomeAssistantType,
config_entry: ConfigEntry,
api: ClimaCell,
api: ClimaCellV3 | ClimaCellV4,
update_interval: timedelta,
) -> None:
"""Initialize."""

self._config_entry = config_entry
self._api_version = config_entry.data[CONF_API_VERSION]
self._api = api
self.name = config_entry.data[CONF_NAME]
self.data = {CURRENT: {}, FORECASTS: {}}
Expand All @@ -166,27 +232,81 @@ async def _async_update_data(self) -> dict[str, Any]:
"""Update data via library."""
data = {FORECASTS: {}}
try:
data[CURRENT] = await self._api.realtime(
self._api.available_fields(REALTIME)
)
data[FORECASTS][HOURLY] = await self._api.forecast_hourly(
self._api.available_fields(FORECAST_HOURLY),
None,
timedelta(hours=24),
)

data[FORECASTS][DAILY] = await self._api.forecast_daily(
self._api.available_fields(FORECAST_DAILY), None, timedelta(days=14)
)

data[FORECASTS][NOWCAST] = await self._api.forecast_nowcast(
self._api.available_fields(FORECAST_NOWCAST),
None,
timedelta(
minutes=min(300, self._config_entry.options[CONF_TIMESTEP] * 30)
),
self._config_entry.options[CONF_TIMESTEP],
)
if self._api_version == 3:
data[CURRENT] = await self._api.realtime(
[
CC_V3_ATTR_TEMPERATURE,
CC_V3_ATTR_HUMIDITY,
CC_V3_ATTR_PRESSURE,
CC_V3_ATTR_WIND_SPEED,
CC_V3_ATTR_WIND_DIRECTION,
CC_V3_ATTR_CONDITION,
CC_V3_ATTR_VISIBILITY,
CC_V3_ATTR_OZONE,
]
)
data[FORECASTS][HOURLY] = await self._api.forecast_hourly(
[
CC_V3_ATTR_TEMPERATURE,
CC_V3_ATTR_WIND_SPEED,
CC_V3_ATTR_WIND_DIRECTION,
CC_V3_ATTR_CONDITION,
CC_V3_ATTR_PRECIPITATION,
CC_V3_ATTR_PRECIPITATION_PROBABILITY,
],
None,
timedelta(hours=24),
)

data[FORECASTS][DAILY] = await self._api.forecast_daily(
[
CC_V3_ATTR_TEMPERATURE,
CC_V3_ATTR_WIND_SPEED,
CC_V3_ATTR_WIND_DIRECTION,
CC_V3_ATTR_CONDITION,
CC_V3_ATTR_PRECIPITATION_DAILY,
CC_V3_ATTR_PRECIPITATION_PROBABILITY,
],
None,
timedelta(days=14),
)

data[FORECASTS][NOWCAST] = await self._api.forecast_nowcast(
[
CC_V3_ATTR_TEMPERATURE,
CC_V3_ATTR_WIND_SPEED,
CC_V3_ATTR_WIND_DIRECTION,
CC_V3_ATTR_CONDITION,
CC_V3_ATTR_PRECIPITATION,
],
None,
timedelta(
minutes=min(300, self._config_entry.options[CONF_TIMESTEP] * 30)
),
self._config_entry.options[CONF_TIMESTEP],
)
else:
return await self._api.realtime_and_all_forecasts(
[
CC_ATTR_TEMPERATURE,
CC_ATTR_HUMIDITY,
CC_ATTR_PRESSURE,
CC_ATTR_WIND_SPEED,
CC_ATTR_WIND_DIRECTION,
CC_ATTR_CONDITION,
CC_ATTR_VISIBILITY,
CC_ATTR_OZONE,
],
[
CC_ATTR_TEMPERATURE_LOW,
CC_ATTR_TEMPERATURE_HIGH,
CC_ATTR_WIND_SPEED,
CC_ATTR_WIND_DIRECTION,
CC_ATTR_CONDITION,
CC_ATTR_PRECIPITATION,
CC_ATTR_PRECIPITATION_PROBABILITY,
],
)
except (
CantConnectException,
InvalidAPIKeyException,
Expand All @@ -202,10 +322,16 @@ class ClimaCellEntity(CoordinatorEntity):
"""Base ClimaCell Entity."""

def __init__(
self, config_entry: ConfigEntry, coordinator: ClimaCellDataUpdateCoordinator
self,
config_entry: ConfigEntry,
coordinator: ClimaCellDataUpdateCoordinator,
forecast_type: str,
api_version: int,
) -> None:
"""Initialize ClimaCell Entity."""
super().__init__(coordinator)
self.api_version = api_version
self.forecast_type = forecast_type
self._config_entry = config_entry

@staticmethod
Expand All @@ -229,15 +355,23 @@ def _get_cc_value(

return items.get("value")

@property
def entity_registry_enabled_default(self) -> bool:
Comment thread
raman325 marked this conversation as resolved.
"""Return if the entity should be enabled when first added to the entity registry."""
if self.forecast_type == DEFAULT_FORECAST_TYPE:
return True

return False

@property
def name(self) -> str:
"""Return the name of the entity."""
return self._config_entry.data[CONF_NAME]
return f"{self._config_entry.data[CONF_NAME]} - {self.forecast_type.title()}"

@property
def unique_id(self) -> str:
"""Return the unique id of the entity."""
return self._config_entry.unique_id
return f"{self._config_entry.unique_id}_{self.forecast_type}"

@property
def attribution(self):
Expand Down
Loading