Add new climacell sensors (#52079)

* Add new climacell sensors

* lint

* add new unit constants
This commit is contained in:
Raman Gupta 2021-06-27 15:03:20 -04:00 committed by GitHub
parent 74aa428bd1
commit 23339cff95
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 197 additions and 35 deletions

View file

@ -5,6 +5,7 @@ from pyclimacell.const import (
NOWCAST, NOWCAST,
HealthConcernType, HealthConcernType,
PollenIndex, PollenIndex,
PrecipitationType,
PrimaryPollutantType, PrimaryPollutantType,
V3PollenIndex, V3PollenIndex,
WeatherCode, WeatherCode,
@ -26,13 +27,29 @@ from homeassistant.components.weather import (
) )
from homeassistant.const import ( from homeassistant.const import (
ATTR_NAME, ATTR_NAME,
CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION, CONCENTRATION_PARTS_PER_MILLION,
CONF_UNIT_OF_MEASUREMENT, CONF_UNIT_OF_MEASUREMENT,
CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_IMPERIAL,
CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_METRIC,
IRRADIATION_BTUS_PER_HOUR_SQUARE_FOOT,
IRRADIATION_WATTS_PER_SQUARE_METER,
LENGTH_KILOMETERS,
LENGTH_METERS,
LENGTH_MILES,
PERCENTAGE,
PRESSURE_HPA,
PRESSURE_INHG,
SPEED_METERS_PER_SECOND,
SPEED_MILES_PER_HOUR,
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
) )
from homeassistant.util.distance import convert as distance_convert
from homeassistant.util.pressure import convert as pressure_convert
from homeassistant.util.temperature import convert as temp_convert
CONF_TIMESTEP = "timestep" CONF_TIMESTEP = "timestep"
FORECAST_TYPES = [DAILY, HOURLY, NOWCAST] FORECAST_TYPES = [DAILY, HOURLY, NOWCAST]
@ -58,6 +75,7 @@ ATTR_FIELD = "field"
ATTR_METRIC_CONVERSION = "metric_conversion" ATTR_METRIC_CONVERSION = "metric_conversion"
ATTR_VALUE_MAP = "value_map" ATTR_VALUE_MAP = "value_map"
ATTR_IS_METRIC_CHECK = "is_metric_check" ATTR_IS_METRIC_CHECK = "is_metric_check"
ATTR_SCALE = "scale"
# Additional attributes # Additional attributes
ATTR_WIND_GUST = "wind_gust" ATTR_WIND_GUST = "wind_gust"
@ -126,8 +144,94 @@ CC_ATTR_POLLEN_TREE = "treeIndex"
CC_ATTR_POLLEN_WEED = "weedIndex" CC_ATTR_POLLEN_WEED = "weedIndex"
CC_ATTR_POLLEN_GRASS = "grassIndex" CC_ATTR_POLLEN_GRASS = "grassIndex"
CC_ATTR_FIRE_INDEX = "fireIndex" CC_ATTR_FIRE_INDEX = "fireIndex"
CC_ATTR_FEELS_LIKE = "temperatureApparent"
CC_ATTR_DEW_POINT = "dewPoint"
CC_ATTR_PRESSURE_SURFACE_LEVEL = "pressureSurfaceLevel"
CC_ATTR_SOLAR_GHI = "solarGHI"
CC_ATTR_CLOUD_BASE = "cloudBase"
CC_ATTR_CLOUD_CEILING = "cloudCeiling"
CC_SENSOR_TYPES = [ CC_SENSOR_TYPES = [
{
ATTR_FIELD: CC_ATTR_FEELS_LIKE,
ATTR_NAME: "Feels Like",
CONF_UNIT_SYSTEM_IMPERIAL: TEMP_FAHRENHEIT,
CONF_UNIT_SYSTEM_METRIC: TEMP_CELSIUS,
ATTR_METRIC_CONVERSION: lambda val: temp_convert(
val, TEMP_FAHRENHEIT, TEMP_CELSIUS
),
ATTR_IS_METRIC_CHECK: True,
},
{
ATTR_FIELD: CC_ATTR_DEW_POINT,
ATTR_NAME: "Dew Point",
CONF_UNIT_SYSTEM_IMPERIAL: TEMP_FAHRENHEIT,
CONF_UNIT_SYSTEM_METRIC: TEMP_CELSIUS,
ATTR_METRIC_CONVERSION: lambda val: temp_convert(
val, TEMP_FAHRENHEIT, TEMP_CELSIUS
),
ATTR_IS_METRIC_CHECK: True,
},
{
ATTR_FIELD: CC_ATTR_PRESSURE_SURFACE_LEVEL,
ATTR_NAME: "Pressure (Surface Level)",
CONF_UNIT_SYSTEM_IMPERIAL: PRESSURE_INHG,
CONF_UNIT_SYSTEM_METRIC: PRESSURE_HPA,
ATTR_METRIC_CONVERSION: lambda val: pressure_convert(
val, PRESSURE_INHG, PRESSURE_HPA
),
ATTR_IS_METRIC_CHECK: True,
},
{
ATTR_FIELD: CC_ATTR_SOLAR_GHI,
ATTR_NAME: "Global Horizontal Irradiance",
CONF_UNIT_SYSTEM_IMPERIAL: IRRADIATION_BTUS_PER_HOUR_SQUARE_FOOT,
CONF_UNIT_SYSTEM_METRIC: IRRADIATION_WATTS_PER_SQUARE_METER,
ATTR_METRIC_CONVERSION: 3.15459,
ATTR_IS_METRIC_CHECK: True,
},
{
ATTR_FIELD: CC_ATTR_CLOUD_BASE,
ATTR_NAME: "Cloud Base",
CONF_UNIT_SYSTEM_IMPERIAL: LENGTH_MILES,
CONF_UNIT_SYSTEM_METRIC: LENGTH_KILOMETERS,
ATTR_METRIC_CONVERSION: lambda val: distance_convert(
val, LENGTH_MILES, LENGTH_KILOMETERS
),
ATTR_IS_METRIC_CHECK: True,
},
{
ATTR_FIELD: CC_ATTR_CLOUD_CEILING,
ATTR_NAME: "Cloud Ceiling",
CONF_UNIT_SYSTEM_IMPERIAL: LENGTH_MILES,
CONF_UNIT_SYSTEM_METRIC: LENGTH_KILOMETERS,
ATTR_METRIC_CONVERSION: lambda val: distance_convert(
val, LENGTH_MILES, LENGTH_KILOMETERS
),
ATTR_IS_METRIC_CHECK: True,
},
{
ATTR_FIELD: CC_ATTR_CLOUD_COVER,
ATTR_NAME: "Cloud Cover",
CONF_UNIT_OF_MEASUREMENT: PERCENTAGE,
ATTR_SCALE: 1 / 100,
},
{
ATTR_FIELD: CC_ATTR_WIND_GUST,
ATTR_NAME: "Wind Gust",
CONF_UNIT_SYSTEM_IMPERIAL: SPEED_MILES_PER_HOUR,
CONF_UNIT_SYSTEM_METRIC: SPEED_METERS_PER_SECOND,
ATTR_METRIC_CONVERSION: lambda val: distance_convert(
val, LENGTH_MILES, LENGTH_METERS
)
/ 3600,
ATTR_IS_METRIC_CHECK: True,
},
{
ATTR_FIELD: CC_ATTR_PRECIPITATION_TYPE,
ATTR_NAME: "Precipitation Type",
ATTR_VALUE_MAP: PrecipitationType,
},
{ {
ATTR_FIELD: CC_ATTR_OZONE, ATTR_FIELD: CC_ATTR_OZONE,
ATTR_NAME: "Ozone", ATTR_NAME: "Ozone",
@ -136,7 +240,7 @@ CC_SENSOR_TYPES = [
{ {
ATTR_FIELD: CC_ATTR_PARTICULATE_MATTER_25, ATTR_FIELD: CC_ATTR_PARTICULATE_MATTER_25,
ATTR_NAME: "Particulate Matter < 2.5 μm", ATTR_NAME: "Particulate Matter < 2.5 μm",
CONF_UNIT_SYSTEM_IMPERIAL: "μg/ft³", CONF_UNIT_SYSTEM_IMPERIAL: CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT,
CONF_UNIT_SYSTEM_METRIC: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONF_UNIT_SYSTEM_METRIC: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
ATTR_METRIC_CONVERSION: 3.2808399 ** 3, ATTR_METRIC_CONVERSION: 3.2808399 ** 3,
ATTR_IS_METRIC_CHECK: True, ATTR_IS_METRIC_CHECK: True,
@ -144,7 +248,7 @@ CC_SENSOR_TYPES = [
{ {
ATTR_FIELD: CC_ATTR_PARTICULATE_MATTER_10, ATTR_FIELD: CC_ATTR_PARTICULATE_MATTER_10,
ATTR_NAME: "Particulate Matter < 10 μm", ATTR_NAME: "Particulate Matter < 10 μm",
CONF_UNIT_SYSTEM_IMPERIAL: "μg/ft³", CONF_UNIT_SYSTEM_IMPERIAL: CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT,
CONF_UNIT_SYSTEM_METRIC: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONF_UNIT_SYSTEM_METRIC: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
ATTR_METRIC_CONVERSION: 3.2808399 ** 3, ATTR_METRIC_CONVERSION: 3.2808399 ** 3,
ATTR_IS_METRIC_CHECK: True, ATTR_IS_METRIC_CHECK: True,
@ -277,7 +381,7 @@ CC_V3_SENSOR_TYPES = [
ATTR_NAME: "Particulate Matter < 2.5 μm", ATTR_NAME: "Particulate Matter < 2.5 μm",
CONF_UNIT_SYSTEM_IMPERIAL: "μg/ft³", CONF_UNIT_SYSTEM_IMPERIAL: "μg/ft³",
CONF_UNIT_SYSTEM_METRIC: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONF_UNIT_SYSTEM_METRIC: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
ATTR_METRIC_CONVERSION: 1 / (3.2808399 ** 3), ATTR_METRIC_CONVERSION: 3.2808399 ** 3,
ATTR_IS_METRIC_CHECK: False, ATTR_IS_METRIC_CHECK: False,
}, },
{ {
@ -285,7 +389,7 @@ CC_V3_SENSOR_TYPES = [
ATTR_NAME: "Particulate Matter < 10 μm", ATTR_NAME: "Particulate Matter < 10 μm",
CONF_UNIT_SYSTEM_IMPERIAL: "μg/ft³", CONF_UNIT_SYSTEM_IMPERIAL: "μg/ft³",
CONF_UNIT_SYSTEM_METRIC: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONF_UNIT_SYSTEM_METRIC: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
ATTR_METRIC_CONVERSION: 1 / (3.2808399 ** 3), ATTR_METRIC_CONVERSION: 3.2808399 ** 3,
ATTR_IS_METRIC_CHECK: False, ATTR_IS_METRIC_CHECK: False,
}, },
{ {

View file

@ -28,6 +28,7 @@ from .const import (
ATTR_FIELD, ATTR_FIELD,
ATTR_IS_METRIC_CHECK, ATTR_IS_METRIC_CHECK,
ATTR_METRIC_CONVERSION, ATTR_METRIC_CONVERSION,
ATTR_SCALE,
ATTR_VALUE_MAP, ATTR_VALUE_MAP,
CC_SENSOR_TYPES, CC_SENSOR_TYPES,
CC_V3_SENSOR_TYPES, CC_V3_SENSOR_TYPES,
@ -103,9 +104,11 @@ class BaseClimaCellSensorEntity(ClimaCellEntity, SensorEntity):
CONF_UNIT_SYSTEM_IMPERIAL in self.sensor_type CONF_UNIT_SYSTEM_IMPERIAL in self.sensor_type
and CONF_UNIT_SYSTEM_METRIC in self.sensor_type and CONF_UNIT_SYSTEM_METRIC in self.sensor_type
): ):
if self.hass.config.units.is_metric: return (
return self.sensor_type[CONF_UNIT_SYSTEM_METRIC] self.sensor_type[CONF_UNIT_SYSTEM_METRIC]
return self.sensor_type[CONF_UNIT_SYSTEM_IMPERIAL] if self.hass.config.units.is_metric
else self.sensor_type[CONF_UNIT_SYSTEM_IMPERIAL]
)
return None return None
@ -117,8 +120,12 @@ class BaseClimaCellSensorEntity(ClimaCellEntity, SensorEntity):
@property @property
def state(self) -> str | int | float | None: def state(self) -> str | int | float | None:
"""Return the state.""" """Return the state."""
state = self._state
if state and ATTR_SCALE in self.sensor_type:
state *= self.sensor_type[ATTR_SCALE]
if ( if (
self._state is not None state is not None
and CONF_UNIT_SYSTEM_IMPERIAL in self.sensor_type and CONF_UNIT_SYSTEM_IMPERIAL in self.sensor_type
and CONF_UNIT_SYSTEM_METRIC in self.sensor_type and CONF_UNIT_SYSTEM_METRIC in self.sensor_type
and ATTR_METRIC_CONVERSION in self.sensor_type and ATTR_METRIC_CONVERSION in self.sensor_type
@ -126,11 +133,17 @@ class BaseClimaCellSensorEntity(ClimaCellEntity, SensorEntity):
and self.hass.config.units.is_metric and self.hass.config.units.is_metric
== self.sensor_type[ATTR_IS_METRIC_CHECK] == self.sensor_type[ATTR_IS_METRIC_CHECK]
): ):
return round(self._state * self.sensor_type[ATTR_METRIC_CONVERSION], 4) conversion = self.sensor_type[ATTR_METRIC_CONVERSION]
# When conversion is a callable, we assume it's a single input function
if callable(conversion):
return round(conversion(state), 4)
if ATTR_VALUE_MAP in self.sensor_type and self._state is not None: return round(state * conversion, 4)
return self.sensor_type[ATTR_VALUE_MAP](self._state).name.lower()
return self._state if ATTR_VALUE_MAP in self.sensor_type and state is not None:
return self.sensor_type[ATTR_VALUE_MAP](state).name.lower()
return state
class ClimaCellSensorEntity(BaseClimaCellSensorEntity): class ClimaCellSensorEntity(BaseClimaCellSensorEntity):

View file

@ -494,6 +494,7 @@ PERCENTAGE: Final = "%"
# Irradiation units # Irradiation units
IRRADIATION_WATTS_PER_SQUARE_METER: Final = "W/m²" IRRADIATION_WATTS_PER_SQUARE_METER: Final = "W/m²"
IRRADIATION_BTUS_PER_HOUR_SQUARE_FOOT: Final = "BTU/(h×ft²)"
# Precipitation units # Precipitation units
PRECIPITATION_MILLIMETERS_PER_HOUR: Final = "mm/h" PRECIPITATION_MILLIMETERS_PER_HOUR: Final = "mm/h"
@ -501,6 +502,7 @@ PRECIPITATION_MILLIMETERS_PER_HOUR: Final = "mm/h"
# Concentration units # Concentration units
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: Final = "µg/m³" CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: Final = "µg/m³"
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER: Final = "mg/m³" CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER: Final = "mg/m³"
CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT: Final = "μg/ft³"
CONCENTRATION_PARTS_PER_CUBIC_METER: Final = "p/m³" CONCENTRATION_PARTS_PER_CUBIC_METER: Final = "p/m³"
CONCENTRATION_PARTS_PER_MILLION: Final = "ppm" CONCENTRATION_PARTS_PER_MILLION: Final = "ppm"
CONCENTRATION_PARTS_PER_BILLION: Final = "ppb" CONCENTRATION_PARTS_PER_BILLION: Final = "ppb"

View file

@ -42,6 +42,49 @@ FIRE_INDEX = "fire_index"
GRASS_POLLEN = "grass_pollen_index" GRASS_POLLEN = "grass_pollen_index"
WEED_POLLEN = "weed_pollen_index" WEED_POLLEN = "weed_pollen_index"
TREE_POLLEN = "tree_pollen_index" TREE_POLLEN = "tree_pollen_index"
FEELS_LIKE = "feels_like"
DEW_POINT = "dew_point"
PRESSURE_SURFACE_LEVEL = "pressure_surface_level"
SNOW_ACCUMULATION = "snow_accumulation"
ICE_ACCUMULATION = "ice_accumulation"
GHI = "global_horizontal_irradiance"
CLOUD_BASE = "cloud_base"
CLOUD_COVER = "cloud_cover"
CLOUD_CEILING = "cloud_ceiling"
WIND_GUST = "wind_gust"
PRECIPITATION_TYPE = "precipitation_type"
V3_FIELDS = [
O3,
CO,
NO2,
SO2,
PM25,
PM10,
MEP_AQI,
MEP_HEALTH_CONCERN,
MEP_PRIMARY_POLLUTANT,
EPA_AQI,
EPA_HEALTH_CONCERN,
EPA_PRIMARY_POLLUTANT,
FIRE_INDEX,
GRASS_POLLEN,
WEED_POLLEN,
TREE_POLLEN,
]
V4_FIELDS = [
*V3_FIELDS,
FEELS_LIKE,
DEW_POINT,
PRESSURE_SURFACE_LEVEL,
GHI,
CLOUD_BASE,
CLOUD_COVER,
CLOUD_CEILING,
WIND_GUST,
PRECIPITATION_TYPE,
]
@callback @callback
@ -56,7 +99,9 @@ def _enable_entity(hass: HomeAssistant, entity_name: str) -> None:
assert updated_entry.disabled is False assert updated_entry.disabled is False
async def _setup(hass: HomeAssistant, config: dict[str, Any]) -> State: async def _setup(
hass: HomeAssistant, sensors: list[str], config: dict[str, Any]
) -> State:
"""Set up entry and return entity state.""" """Set up entry and return entity state."""
with patch( with patch(
"homeassistant.util.dt.utcnow", "homeassistant.util.dt.utcnow",
@ -72,27 +117,10 @@ async def _setup(hass: HomeAssistant, config: dict[str, Any]) -> State:
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id) assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
for entity_name in ( for entity_name in sensors:
O3,
CO,
NO2,
SO2,
PM25,
PM10,
MEP_AQI,
MEP_HEALTH_CONCERN,
MEP_PRIMARY_POLLUTANT,
EPA_AQI,
EPA_HEALTH_CONCERN,
EPA_PRIMARY_POLLUTANT,
FIRE_INDEX,
GRASS_POLLEN,
WEED_POLLEN,
TREE_POLLEN,
):
_enable_entity(hass, CC_SENSOR_ENTITY_ID.format(entity_name)) _enable_entity(hass, CC_SENSOR_ENTITY_ID.format(entity_name))
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 16 assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == len(sensors)
def check_sensor_state(hass: HomeAssistant, entity_name: str, value: str): def check_sensor_state(hass: HomeAssistant, entity_name: str, value: str):
@ -108,7 +136,7 @@ async def test_v3_sensor(
climacell_config_entry_update: pytest.fixture, climacell_config_entry_update: pytest.fixture,
) -> None: ) -> None:
"""Test v3 sensor data.""" """Test v3 sensor data."""
await _setup(hass, API_V3_ENTRY_DATA) await _setup(hass, V3_FIELDS, API_V3_ENTRY_DATA)
check_sensor_state(hass, O3, "52.625") check_sensor_state(hass, O3, "52.625")
check_sensor_state(hass, CO, "0.875") check_sensor_state(hass, CO, "0.875")
check_sensor_state(hass, NO2, "14.1875") check_sensor_state(hass, NO2, "14.1875")
@ -132,7 +160,7 @@ async def test_v4_sensor(
climacell_config_entry_update: pytest.fixture, climacell_config_entry_update: pytest.fixture,
) -> None: ) -> None:
"""Test v4 sensor data.""" """Test v4 sensor data."""
await _setup(hass, API_V4_ENTRY_DATA) await _setup(hass, V4_FIELDS, API_V4_ENTRY_DATA)
check_sensor_state(hass, O3, "46.53") check_sensor_state(hass, O3, "46.53")
check_sensor_state(hass, CO, "0.63") check_sensor_state(hass, CO, "0.63")
check_sensor_state(hass, NO2, "10.67") check_sensor_state(hass, NO2, "10.67")
@ -149,3 +177,12 @@ async def test_v4_sensor(
check_sensor_state(hass, GRASS_POLLEN, "none") check_sensor_state(hass, GRASS_POLLEN, "none")
check_sensor_state(hass, WEED_POLLEN, "none") check_sensor_state(hass, WEED_POLLEN, "none")
check_sensor_state(hass, TREE_POLLEN, "none") check_sensor_state(hass, TREE_POLLEN, "none")
check_sensor_state(hass, FEELS_LIKE, "38.5")
check_sensor_state(hass, DEW_POINT, "22.6778")
check_sensor_state(hass, PRESSURE_SURFACE_LEVEL, "997.9688")
check_sensor_state(hass, GHI, "0.0")
check_sensor_state(hass, CLOUD_BASE, "1.1909")
check_sensor_state(hass, CLOUD_COVER, "1.0")
check_sensor_state(hass, CLOUD_CEILING, "1.1909")
check_sensor_state(hass, WIND_GUST, "5.6506")
check_sensor_state(hass, PRECIPITATION_TYPE, "rain")

View file

@ -25,7 +25,13 @@
"treeIndex": 0, "treeIndex": 0,
"weedIndex": 0, "weedIndex": 0,
"grassIndex": 0, "grassIndex": 0,
"fireIndex": 10 "fireIndex": 10,
"temperatureApparent": 101.3,
"dewPoint": 72.82,
"pressureSurfaceLevel": 29.47,
"solarGHI": 0,
"cloudBase": 0.74,
"cloudCeiling": 0.74
}, },
"forecasts": { "forecasts": {
"nowcast": [ "nowcast": [