hass-core/homeassistant/components/climacell/weather.py
Raman Gupta 4cd4fbefbf
Add new tomorrow.io integration to replace Climacell (#68156)
* Add new tomorrow.io integration to replace Climacell - Part 1/3 (#57121)

* Add new tomorrow.io integration to replace Climacell - Part 1/3

* remove unused code

* remove extra test

* remove more unused code

* Remove even more unused code

* Feedback

* clean up options flow

* clean up options flow

* tweaks and fix tests

* remove device_class from tomorrowio entity description class

* use timestep

* fix tests

* always use default name but add zone name if location is in a zone

* revert change that will go into future PR

* review comments

* move code out of try block

* bump max requests to 500 as per docs

* fix tests

* Add new tomorrow.io integration to replace Climacell - Part 2/3 (#57124)

* Add new tomorrow.io integration to replace Climacell - Part 2/3

* translations

* set config flow to false in manifest

* Cleanup more code and re-add options flow test

* fixes

* patch I/O calls

* Update tests/components/climacell/test_config_flow.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* remove unused import

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Fix codeowners

* fix mypy and pylint

* Switch to DeviceInfo

* Fix fixture location and improve sensor entities in tomorrowio integration (#63527)

* Add new tomorrow.io integration to replace Climacell - Part 3/3 (#59698)

* Switch to DeviceInfo

* Add new tomorrow.io integration to replace Climacell - Part 1/3 (#57121)

* Add new tomorrow.io integration to replace Climacell - Part 1/3

* remove unused code

* remove extra test

* remove more unused code

* Remove even more unused code

* Feedback

* clean up options flow

* clean up options flow

* tweaks and fix tests

* remove device_class from tomorrowio entity description class

* use timestep

* fix tests

* always use default name but add zone name if location is in a zone

* revert change that will go into future PR

* review comments

* move code out of try block

* bump max requests to 500 as per docs

* fix tests

* Migrate ClimaCell entries to Tomorrow.io

* tweaks

* pylint

* Apply fix from #60454 to tomorrowio integration

* lint and mypy

* use speed conversion instead of distance conversion

* Use SensorDeviceClass enum

* Use built in conversions and remove unused loggers

* fix requirements

* Update homeassistant/components/tomorrowio/__init__.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Use constants

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Black

* Update logic and add coverage

* remove extra line

* Do patching correctly

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2022-03-19 08:42:22 +01:00

397 lines
13 KiB
Python

"""Weather component that handles meteorological data for your location."""
from __future__ import annotations
from abc import abstractmethod
from collections.abc import Mapping
from datetime import datetime
from typing import Any, cast
from pyclimacell.const import CURRENT, DAILY, FORECASTS, HOURLY, NOWCAST
from homeassistant.components.weather import (
ATTR_FORECAST_CONDITION,
ATTR_FORECAST_PRECIPITATION,
ATTR_FORECAST_PRECIPITATION_PROBABILITY,
ATTR_FORECAST_TEMP,
ATTR_FORECAST_TEMP_LOW,
ATTR_FORECAST_TIME,
ATTR_FORECAST_WIND_BEARING,
ATTR_FORECAST_WIND_SPEED,
WeatherEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_API_VERSION,
CONF_NAME,
LENGTH_FEET,
LENGTH_KILOMETERS,
LENGTH_METERS,
LENGTH_MILES,
PRESSURE_HPA,
PRESSURE_INHG,
SPEED_KILOMETERS_PER_HOUR,
SPEED_MILES_PER_HOUR,
TEMP_FAHRENHEIT,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.sun import is_up
from homeassistant.util import dt as dt_util
from homeassistant.util.distance import convert as distance_convert
from homeassistant.util.pressure import convert as pressure_convert
from homeassistant.util.speed import convert as speed_convert
from . import ClimaCellDataUpdateCoordinator, ClimaCellEntity
from .const import (
ATTR_CLOUD_COVER,
ATTR_PRECIPITATION_TYPE,
ATTR_WIND_GUST,
CC_V3_ATTR_CLOUD_COVER,
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_PRECIPITATION_TYPE,
CC_V3_ATTR_PRESSURE,
CC_V3_ATTR_TEMPERATURE,
CC_V3_ATTR_TEMPERATURE_HIGH,
CC_V3_ATTR_TEMPERATURE_LOW,
CC_V3_ATTR_TIMESTAMP,
CC_V3_ATTR_VISIBILITY,
CC_V3_ATTR_WIND_DIRECTION,
CC_V3_ATTR_WIND_GUST,
CC_V3_ATTR_WIND_SPEED,
CLEAR_CONDITIONS,
CONDITIONS_V3,
CONF_TIMESTEP,
DEFAULT_FORECAST_TYPE,
DOMAIN,
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up a config entry."""
coordinator = hass.data[DOMAIN][config_entry.entry_id]
api_version = config_entry.data[CONF_API_VERSION]
entities = [
ClimaCellV3WeatherEntity(config_entry, coordinator, api_version, forecast_type)
for forecast_type in (DAILY, HOURLY, NOWCAST)
]
async_add_entities(entities)
class BaseClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity):
"""Base ClimaCell weather entity."""
def __init__(
self,
config_entry: ConfigEntry,
coordinator: ClimaCellDataUpdateCoordinator,
api_version: int,
forecast_type: str,
) -> None:
"""Initialize ClimaCell Weather Entity."""
super().__init__(config_entry, coordinator, api_version)
self.forecast_type = forecast_type
self._attr_entity_registry_enabled_default = (
forecast_type == DEFAULT_FORECAST_TYPE
)
self._attr_name = f"{config_entry.data[CONF_NAME]} - {forecast_type.title()}"
self._attr_unique_id = f"{config_entry.unique_id}_{forecast_type}"
@staticmethod
@abstractmethod
def _translate_condition(
condition: str | int | None, sun_is_up: bool = True
) -> str | None:
"""Translate ClimaCell condition into an HA condition."""
def _forecast_dict(
self,
forecast_dt: datetime,
use_datetime: bool,
condition: int | str,
precipitation: float | None,
precipitation_probability: float | None,
temp: float | None,
temp_low: float | None,
wind_direction: float | None,
wind_speed: float | None,
) -> dict[str, Any]:
"""Return formatted Forecast dict from ClimaCell forecast data."""
if use_datetime:
translated_condition = self._translate_condition(
condition, is_up(self.hass, forecast_dt)
)
else:
translated_condition = self._translate_condition(condition, True)
if self.hass.config.units.is_metric:
if precipitation:
precipitation = round(
distance_convert(precipitation / 12, LENGTH_FEET, LENGTH_METERS)
* 1000,
4,
)
if wind_speed:
wind_speed = round(
speed_convert(
wind_speed, SPEED_MILES_PER_HOUR, SPEED_KILOMETERS_PER_HOUR
),
4,
)
data = {
ATTR_FORECAST_TIME: forecast_dt.isoformat(),
ATTR_FORECAST_CONDITION: translated_condition,
ATTR_FORECAST_PRECIPITATION: precipitation,
ATTR_FORECAST_PRECIPITATION_PROBABILITY: precipitation_probability,
ATTR_FORECAST_TEMP: temp,
ATTR_FORECAST_TEMP_LOW: temp_low,
ATTR_FORECAST_WIND_BEARING: wind_direction,
ATTR_FORECAST_WIND_SPEED: wind_speed,
}
return {k: v for k, v in data.items() if v is not None}
@property
def extra_state_attributes(self) -> Mapping[str, Any] | None:
"""Return additional state attributes."""
wind_gust = self.wind_gust
if wind_gust and self.hass.config.units.is_metric:
wind_gust = round(
speed_convert(
self.wind_gust, SPEED_MILES_PER_HOUR, SPEED_KILOMETERS_PER_HOUR
),
4,
)
cloud_cover = self.cloud_cover
return {
ATTR_CLOUD_COVER: cloud_cover,
ATTR_WIND_GUST: wind_gust,
ATTR_PRECIPITATION_TYPE: self.precipitation_type,
}
@property
@abstractmethod
def cloud_cover(self):
"""Return cloud cover."""
@property
@abstractmethod
def wind_gust(self):
"""Return wind gust speed."""
@property
@abstractmethod
def precipitation_type(self):
"""Return precipitation type."""
@property
@abstractmethod
def _pressure(self):
"""Return the raw pressure."""
@property
def pressure(self):
"""Return the pressure."""
if self.hass.config.units.is_metric and self._pressure:
return round(
pressure_convert(self._pressure, PRESSURE_INHG, PRESSURE_HPA), 4
)
return self._pressure
@property
@abstractmethod
def _wind_speed(self):
"""Return the raw wind speed."""
@property
def wind_speed(self):
"""Return the wind speed."""
if self.hass.config.units.is_metric and self._wind_speed:
return round(
speed_convert(
self._wind_speed, SPEED_MILES_PER_HOUR, SPEED_KILOMETERS_PER_HOUR
),
4,
)
return self._wind_speed
@property
@abstractmethod
def _visibility(self):
"""Return the raw visibility."""
@property
def visibility(self):
"""Return the visibility."""
if self.hass.config.units.is_metric and self._visibility:
return round(
distance_convert(self._visibility, LENGTH_MILES, LENGTH_KILOMETERS), 4
)
return self._visibility
class ClimaCellV3WeatherEntity(BaseClimaCellWeatherEntity):
"""Entity that talks to ClimaCell v3 API to retrieve weather data."""
_attr_temperature_unit = TEMP_FAHRENHEIT
@staticmethod
def _translate_condition(
condition: int | str | None, sun_is_up: bool = True
) -> str | None:
"""Translate ClimaCell condition into an HA condition."""
if not condition:
return None
condition = cast(str, condition)
if "clear" in condition.lower():
if sun_is_up:
return CLEAR_CONDITIONS["day"]
return CLEAR_CONDITIONS["night"]
return CONDITIONS_V3[condition]
@property
def temperature(self):
"""Return the platform temperature."""
return self._get_cc_value(
self.coordinator.data[CURRENT], CC_V3_ATTR_TEMPERATURE
)
@property
def _pressure(self):
"""Return the raw pressure."""
return self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_PRESSURE)
@property
def humidity(self):
"""Return the humidity."""
return self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_HUMIDITY)
@property
def wind_gust(self):
"""Return the wind gust speed."""
return self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_WIND_GUST)
@property
def cloud_cover(self):
"""Return the cloud cover."""
return self._get_cc_value(
self.coordinator.data[CURRENT], CC_V3_ATTR_CLOUD_COVER
)
@property
def precipitation_type(self):
"""Return precipitation type."""
return self._get_cc_value(
self.coordinator.data[CURRENT], CC_V3_ATTR_PRECIPITATION_TYPE
)
@property
def _wind_speed(self):
"""Return the raw wind speed."""
return self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_WIND_SPEED)
@property
def wind_bearing(self):
"""Return the wind bearing."""
return self._get_cc_value(
self.coordinator.data[CURRENT], CC_V3_ATTR_WIND_DIRECTION
)
@property
def ozone(self):
"""Return the O3 (ozone) level."""
return self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_OZONE)
@property
def condition(self):
"""Return the condition."""
return self._translate_condition(
self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_CONDITION),
is_up(self.hass),
)
@property
def _visibility(self):
"""Return the raw visibility."""
return self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_VISIBILITY)
@property
def forecast(self):
"""Return the forecast."""
# Check if forecasts are available
raw_forecasts = self.coordinator.data.get(FORECASTS, {}).get(self.forecast_type)
if not raw_forecasts:
return None
forecasts = []
# Set default values (in cases where keys don't exist), None will be
# returned. Override properties per forecast type as needed
for forecast in raw_forecasts:
forecast_dt = dt_util.parse_datetime(
self._get_cc_value(forecast, CC_V3_ATTR_TIMESTAMP)
)
use_datetime = True
condition = self._get_cc_value(forecast, CC_V3_ATTR_CONDITION)
precipitation = self._get_cc_value(forecast, CC_V3_ATTR_PRECIPITATION)
precipitation_probability = self._get_cc_value(
forecast, CC_V3_ATTR_PRECIPITATION_PROBABILITY
)
temp = self._get_cc_value(forecast, CC_V3_ATTR_TEMPERATURE)
temp_low = None
wind_direction = self._get_cc_value(forecast, CC_V3_ATTR_WIND_DIRECTION)
wind_speed = self._get_cc_value(forecast, CC_V3_ATTR_WIND_SPEED)
if self.forecast_type == DAILY:
use_datetime = False
forecast_dt = dt_util.start_of_local_day(forecast_dt)
precipitation = self._get_cc_value(
forecast, CC_V3_ATTR_PRECIPITATION_DAILY
)
temp = next(
(
self._get_cc_value(item, CC_V3_ATTR_TEMPERATURE_HIGH)
for item in forecast[CC_V3_ATTR_TEMPERATURE]
if "max" in item
),
temp,
)
temp_low = next(
(
self._get_cc_value(item, CC_V3_ATTR_TEMPERATURE_LOW)
for item in forecast[CC_V3_ATTR_TEMPERATURE]
if "min" in item
),
temp_low,
)
elif self.forecast_type == NOWCAST and precipitation:
# Precipitation is forecasted in CONF_TIMESTEP increments but in a
# per hour rate, so value needs to be converted to an amount.
precipitation = (
precipitation / 60 * self._config_entry.options[CONF_TIMESTEP]
)
forecasts.append(
self._forecast_dict(
forecast_dt,
use_datetime,
condition,
precipitation,
precipitation_probability,
temp,
temp_low,
wind_direction,
wind_speed,
)
)
return forecasts