Add Airly gas sensors (#77908)

* Add support for gases queryable via Airly API:
  CO, NO₂, O₃, SO₂
* Add tests for above sensors and update test fixtures
This commit is contained in:
Kenneth J. Miller 2022-09-16 23:19:30 +02:00 committed by GitHub
parent 06178d3446
commit 84cd0da26b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 3041 additions and 2043 deletions

View File

@ -7,11 +7,15 @@ ATTR_API_ADVICE: Final = "ADVICE"
ATTR_API_CAQI: Final = "CAQI"
ATTR_API_CAQI_DESCRIPTION: Final = "DESCRIPTION"
ATTR_API_CAQI_LEVEL: Final = "LEVEL"
ATTR_API_CO: Final = "CO"
ATTR_API_HUMIDITY: Final = "HUMIDITY"
ATTR_API_NO2: Final = "NO2"
ATTR_API_O3: Final = "O3"
ATTR_API_PM10: Final = "PM10"
ATTR_API_PM1: Final = "PM1"
ATTR_API_PM25: Final = "PM25"
ATTR_API_PRESSURE: Final = "PRESSURE"
ATTR_API_SO2: Final = "SO2"
ATTR_API_TEMPERATURE: Final = "TEMPERATURE"
ATTR_ADVICE: Final = "advice"

View File

@ -33,11 +33,15 @@ from .const import (
ATTR_API_CAQI,
ATTR_API_CAQI_DESCRIPTION,
ATTR_API_CAQI_LEVEL,
ATTR_API_CO,
ATTR_API_HUMIDITY,
ATTR_API_NO2,
ATTR_API_O3,
ATTR_API_PM1,
ATTR_API_PM10,
ATTR_API_PM25,
ATTR_API_PRESSURE,
ATTR_API_SO2,
ATTR_API_TEMPERATURE,
ATTR_DESCRIPTION,
ATTR_LEVEL,
@ -112,6 +116,34 @@ SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = (
state_class=SensorStateClass.MEASUREMENT,
value=lambda value: round(value, 1),
),
AirlySensorEntityDescription(
key=ATTR_API_CO,
device_class=SensorDeviceClass.CO,
name=ATTR_API_CO,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
),
AirlySensorEntityDescription(
key=ATTR_API_NO2,
device_class=SensorDeviceClass.NITROGEN_DIOXIDE,
name=ATTR_API_NO2,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
),
AirlySensorEntityDescription(
key=ATTR_API_SO2,
device_class=SensorDeviceClass.SULPHUR_DIOXIDE,
name=ATTR_API_SO2,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
),
AirlySensorEntityDescription(
key=ATTR_API_O3,
device_class=SensorDeviceClass.OZONE,
name=ATTR_API_O3,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
),
)
@ -191,4 +223,32 @@ class AirlySensor(CoordinatorEntity[AirlyDataUpdateCoordinator], SensorEntity):
self._attrs[ATTR_PERCENT] = round(
self.coordinator.data[f"{ATTR_API_PM10}_{SUFFIX_PERCENT}"]
)
if self.entity_description.key == ATTR_API_CO:
self._attrs[ATTR_LIMIT] = self.coordinator.data[
f"{ATTR_API_CO}_{SUFFIX_LIMIT}"
]
self._attrs[ATTR_PERCENT] = round(
self.coordinator.data[f"{ATTR_API_CO}_{SUFFIX_PERCENT}"]
)
if self.entity_description.key == ATTR_API_NO2:
self._attrs[ATTR_LIMIT] = self.coordinator.data[
f"{ATTR_API_NO2}_{SUFFIX_LIMIT}"
]
self._attrs[ATTR_PERCENT] = round(
self.coordinator.data[f"{ATTR_API_NO2}_{SUFFIX_PERCENT}"]
)
if self.entity_description.key == ATTR_API_SO2:
self._attrs[ATTR_LIMIT] = self.coordinator.data[
f"{ATTR_API_SO2}_{SUFFIX_LIMIT}"
]
self._attrs[ATTR_PERCENT] = round(
self.coordinator.data[f"{ATTR_API_SO2}_{SUFFIX_PERCENT}"]
)
if self.entity_description.key == ATTR_API_O3:
self._attrs[ATTR_LIMIT] = self.coordinator.data[
f"{ATTR_API_O3}_{SUFFIX_LIMIT}"
]
self._attrs[ATTR_PERCENT] = round(
self.coordinator.data[f"{ATTR_API_O3}_{SUFFIX_PERCENT}"]
)
return self._attrs

View File

@ -1,16 +1,28 @@
{
"PM1": 9.23,
"PM25": 13.71,
"PM10": 18.58,
"PRESSURE": 1000.87,
"HUMIDITY": 92.84,
"TEMPERATURE": 14.23,
"PM25_LIMIT": 25.0,
"PM25_PERCENT": 54.84,
"PM10_LIMIT": 50.0,
"PM10_PERCENT": 37.17,
"CAQI": 22.85,
"PM1": 2.83,
"PM25": 4.37,
"PM10": 6.06,
"CO": 162.49,
"NO2": 16.04,
"O3": 41.52,
"SO2": 13.97,
"PRESSURE": 1019.86,
"HUMIDITY": 68.35,
"TEMPERATURE": 14.37,
"PM25_LIMIT": 15.0,
"PM25_PERCENT": 29.13,
"PM10_LIMIT": 45.0,
"PM10_PERCENT": 14.5,
"CO_LIMIT": 4000,
"CO_PERCENT": 4.06,
"NO2_LIMIT": 25,
"NO2_PERCENT": 64.17,
"O3_LIMIT": 100,
"O3_PERCENT": 41.52,
"SO2_LIMIT": 40,
"SO2_PERCENT": 34.93,
"CAQI": 7.29,
"LEVEL": "very low",
"DESCRIPTION": "Great air here today!",
"ADVICE": "Great air!"
"ADVICE": "Catch your breath!"
}

File diff suppressed because it is too large Load Diff

View File

@ -29,7 +29,7 @@ async def test_async_setup_entry(hass, aioclient_mock):
state = hass.states.get("sensor.home_pm2_5")
assert state is not None
assert state.state != STATE_UNAVAILABLE
assert state.state == "14"
assert state.state == "4"
async def test_config_not_ready(hass, aioclient_mock):

View File

@ -35,7 +35,7 @@ async def test_sensor(hass, aioclient_mock):
state = hass.states.get("sensor.home_caqi")
assert state
assert state.state == "23"
assert state.state == "7"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "CAQI"
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.AQI
@ -46,7 +46,7 @@ async def test_sensor(hass, aioclient_mock):
state = hass.states.get("sensor.home_humidity")
assert state
assert state.state == "92.8"
assert state.state == "68.3"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.HUMIDITY
@ -58,7 +58,7 @@ async def test_sensor(hass, aioclient_mock):
state = hass.states.get("sensor.home_pm1")
assert state
assert state.state == "9"
assert state.state == "3"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
@ -73,7 +73,7 @@ async def test_sensor(hass, aioclient_mock):
state = hass.states.get("sensor.home_pm2_5")
assert state
assert state.state == "14"
assert state.state == "4"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
@ -88,7 +88,7 @@ async def test_sensor(hass, aioclient_mock):
state = hass.states.get("sensor.home_pm10")
assert state
assert state.state == "19"
assert state.state == "6"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
@ -101,9 +101,69 @@ async def test_sensor(hass, aioclient_mock):
assert entry
assert entry.unique_id == "123-456-pm10"
state = hass.states.get("sensor.home_co")
assert state
assert state.state == "162"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
== CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
)
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.CO
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
entry = registry.async_get("sensor.home_co")
assert entry
assert entry.unique_id == "123-456-co"
state = hass.states.get("sensor.home_no2")
assert state
assert state.state == "16"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
== CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
)
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.NITROGEN_DIOXIDE
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
entry = registry.async_get("sensor.home_no2")
assert entry
assert entry.unique_id == "123-456-no2"
state = hass.states.get("sensor.home_o3")
assert state
assert state.state == "42"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
== CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
)
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.OZONE
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
entry = registry.async_get("sensor.home_o3")
assert entry
assert entry.unique_id == "123-456-o3"
state = hass.states.get("sensor.home_so2")
assert state
assert state.state == "14"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
== CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
)
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.SULPHUR_DIOXIDE
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
entry = registry.async_get("sensor.home_so2")
assert entry
assert entry.unique_id == "123-456-so2"
state = hass.states.get("sensor.home_pressure")
assert state
assert state.state == "1001"
assert state.state == "1020"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_HPA
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRESSURE
@ -115,7 +175,7 @@ async def test_sensor(hass, aioclient_mock):
state = hass.states.get("sensor.home_temperature")
assert state
assert state.state == "14.2"
assert state.state == "14.4"
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE
@ -133,7 +193,7 @@ async def test_availability(hass, aioclient_mock):
state = hass.states.get("sensor.home_humidity")
assert state
assert state.state != STATE_UNAVAILABLE
assert state.state == "92.8"
assert state.state == "68.3"
aioclient_mock.clear_requests()
aioclient_mock.get(API_POINT_URL, exc=ConnectionError())
@ -154,7 +214,7 @@ async def test_availability(hass, aioclient_mock):
state = hass.states.get("sensor.home_humidity")
assert state
assert state.state != STATE_UNAVAILABLE
assert state.state == "92.8"
assert state.state == "68.3"
async def test_manual_update_entity(hass, aioclient_mock):