From 23339cff950edd316ee59152b0d43770795db565 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sun, 27 Jun 2021 15:03:20 -0400 Subject: [PATCH] Add new climacell sensors (#52079) * Add new climacell sensors * lint * add new unit constants --- homeassistant/components/climacell/const.py | 112 ++++++++++++++++++- homeassistant/components/climacell/sensor.py | 29 +++-- homeassistant/const.py | 2 + tests/components/climacell/test_sensor.py | 81 ++++++++++---- tests/fixtures/climacell/v4.json | 8 +- 5 files changed, 197 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/climacell/const.py b/homeassistant/components/climacell/const.py index 5b62d05a78c..062de93375b 100644 --- a/homeassistant/components/climacell/const.py +++ b/homeassistant/components/climacell/const.py @@ -5,6 +5,7 @@ from pyclimacell.const import ( NOWCAST, HealthConcernType, PollenIndex, + PrecipitationType, PrimaryPollutantType, V3PollenIndex, WeatherCode, @@ -26,13 +27,29 @@ from homeassistant.components.weather import ( ) from homeassistant.const import ( ATTR_NAME, + CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_MILLION, CONF_UNIT_OF_MEASUREMENT, CONF_UNIT_SYSTEM_IMPERIAL, 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" FORECAST_TYPES = [DAILY, HOURLY, NOWCAST] @@ -58,6 +75,7 @@ ATTR_FIELD = "field" ATTR_METRIC_CONVERSION = "metric_conversion" ATTR_VALUE_MAP = "value_map" ATTR_IS_METRIC_CHECK = "is_metric_check" +ATTR_SCALE = "scale" # Additional attributes ATTR_WIND_GUST = "wind_gust" @@ -126,8 +144,94 @@ CC_ATTR_POLLEN_TREE = "treeIndex" CC_ATTR_POLLEN_WEED = "weedIndex" CC_ATTR_POLLEN_GRASS = "grassIndex" 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 = [ + { + 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_NAME: "Ozone", @@ -136,7 +240,7 @@ CC_SENSOR_TYPES = [ { ATTR_FIELD: CC_ATTR_PARTICULATE_MATTER_25, 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, ATTR_METRIC_CONVERSION: 3.2808399 ** 3, ATTR_IS_METRIC_CHECK: True, @@ -144,7 +248,7 @@ CC_SENSOR_TYPES = [ { ATTR_FIELD: CC_ATTR_PARTICULATE_MATTER_10, 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, ATTR_METRIC_CONVERSION: 3.2808399 ** 3, ATTR_IS_METRIC_CHECK: True, @@ -277,7 +381,7 @@ CC_V3_SENSOR_TYPES = [ ATTR_NAME: "Particulate Matter < 2.5 μm", CONF_UNIT_SYSTEM_IMPERIAL: "μg/ft³", 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, }, { @@ -285,7 +389,7 @@ CC_V3_SENSOR_TYPES = [ ATTR_NAME: "Particulate Matter < 10 μm", CONF_UNIT_SYSTEM_IMPERIAL: "μg/ft³", 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, }, { diff --git a/homeassistant/components/climacell/sensor.py b/homeassistant/components/climacell/sensor.py index df611079403..2c620cc65a1 100644 --- a/homeassistant/components/climacell/sensor.py +++ b/homeassistant/components/climacell/sensor.py @@ -28,6 +28,7 @@ from .const import ( ATTR_FIELD, ATTR_IS_METRIC_CHECK, ATTR_METRIC_CONVERSION, + ATTR_SCALE, ATTR_VALUE_MAP, CC_SENSOR_TYPES, CC_V3_SENSOR_TYPES, @@ -103,9 +104,11 @@ class BaseClimaCellSensorEntity(ClimaCellEntity, SensorEntity): CONF_UNIT_SYSTEM_IMPERIAL in self.sensor_type and CONF_UNIT_SYSTEM_METRIC in self.sensor_type ): - if self.hass.config.units.is_metric: - return self.sensor_type[CONF_UNIT_SYSTEM_METRIC] - return self.sensor_type[CONF_UNIT_SYSTEM_IMPERIAL] + return ( + self.sensor_type[CONF_UNIT_SYSTEM_METRIC] + if self.hass.config.units.is_metric + else self.sensor_type[CONF_UNIT_SYSTEM_IMPERIAL] + ) return None @@ -117,8 +120,12 @@ class BaseClimaCellSensorEntity(ClimaCellEntity, SensorEntity): @property def state(self) -> str | int | float | None: """Return the state.""" + state = self._state + if state and ATTR_SCALE in self.sensor_type: + state *= self.sensor_type[ATTR_SCALE] + if ( - self._state is not None + state is not None and CONF_UNIT_SYSTEM_IMPERIAL in self.sensor_type and CONF_UNIT_SYSTEM_METRIC 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 == 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 self.sensor_type[ATTR_VALUE_MAP](self._state).name.lower() - return self._state + return round(state * conversion, 4) + + 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): diff --git a/homeassistant/const.py b/homeassistant/const.py index a25a5ec0908..f5308148823 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -494,6 +494,7 @@ PERCENTAGE: Final = "%" # Irradiation units IRRADIATION_WATTS_PER_SQUARE_METER: Final = "W/m²" +IRRADIATION_BTUS_PER_HOUR_SQUARE_FOOT: Final = "BTU/(h×ft²)" # Precipitation units PRECIPITATION_MILLIMETERS_PER_HOUR: Final = "mm/h" @@ -501,6 +502,7 @@ PRECIPITATION_MILLIMETERS_PER_HOUR: Final = "mm/h" # Concentration units CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: Final = "µg/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_MILLION: Final = "ppm" CONCENTRATION_PARTS_PER_BILLION: Final = "ppb" diff --git a/tests/components/climacell/test_sensor.py b/tests/components/climacell/test_sensor.py index 653a989c4b7..d93bdb5fae8 100644 --- a/tests/components/climacell/test_sensor.py +++ b/tests/components/climacell/test_sensor.py @@ -42,6 +42,49 @@ FIRE_INDEX = "fire_index" GRASS_POLLEN = "grass_pollen_index" WEED_POLLEN = "weed_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 @@ -56,7 +99,9 @@ def _enable_entity(hass: HomeAssistant, entity_name: str) -> None: 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.""" with patch( "homeassistant.util.dt.utcnow", @@ -72,27 +117,10 @@ async def _setup(hass: HomeAssistant, config: dict[str, Any]) -> State: config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - for entity_name in ( - 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, - ): + for entity_name in sensors: _enable_entity(hass, CC_SENSOR_ENTITY_ID.format(entity_name)) 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): @@ -108,7 +136,7 @@ async def test_v3_sensor( climacell_config_entry_update: pytest.fixture, ) -> None: """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, CO, "0.875") check_sensor_state(hass, NO2, "14.1875") @@ -132,7 +160,7 @@ async def test_v4_sensor( climacell_config_entry_update: pytest.fixture, ) -> None: """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, CO, "0.63") 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, WEED_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") diff --git a/tests/fixtures/climacell/v4.json b/tests/fixtures/climacell/v4.json index f2f10b0360e..02f76ab7d27 100644 --- a/tests/fixtures/climacell/v4.json +++ b/tests/fixtures/climacell/v4.json @@ -25,7 +25,13 @@ "treeIndex": 0, "weedIndex": 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": { "nowcast": [