-
-
Notifications
You must be signed in to change notification settings - Fork 37.5k
Add Kaiterra integration #26661
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
MartinHjelmare
merged 9 commits into
home-assistant:dev
from
Michsior14:feature/kaiterra-integration
Sep 22, 2019
Merged
Add Kaiterra integration #26661
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
918dc30
add Kaiterra integration
Michsior14 a865f1c
fix: split to multiple platforms
Michsior14 eee5e53
fix lint issues
Michsior14 5d14bf8
fix formmating
Michsior14 718237f
fix: docstrings
Michsior14 b4b9396
fix: pylint issues
Michsior14 904bb04
Apply suggestions from code review
Michsior14 6ff9b6a
Adjust code based on suggestions
Michsior14 5791352
Update homeassistant/components/kaiterra/sensor.py
Michsior14 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| """Support for Kaiterra devices.""" | ||
| import voluptuous as vol | ||
|
|
||
| from homeassistant.helpers.aiohttp_client import async_get_clientsession | ||
| from homeassistant.helpers.event import async_track_time_interval | ||
| from homeassistant.helpers.discovery import async_load_platform | ||
| from homeassistant.helpers import config_validation as cv | ||
|
|
||
| from homeassistant.const import ( | ||
| CONF_API_KEY, | ||
| CONF_DEVICES, | ||
| CONF_DEVICE_ID, | ||
| CONF_SCAN_INTERVAL, | ||
| CONF_TYPE, | ||
| CONF_NAME, | ||
| ) | ||
|
|
||
| from .const import ( | ||
| AVAILABLE_AQI_STANDARDS, | ||
| AVAILABLE_UNITS, | ||
| AVAILABLE_DEVICE_TYPES, | ||
| CONF_AQI_STANDARD, | ||
| CONF_PREFERRED_UNITS, | ||
| DOMAIN, | ||
| DEFAULT_AQI_STANDARD, | ||
| DEFAULT_PREFERRED_UNIT, | ||
| DEFAULT_SCAN_INTERVAL, | ||
| KAITERRA_COMPONENTS, | ||
| ) | ||
|
|
||
| from .api_data import KaiterraApiData | ||
|
|
||
| KAITERRA_DEVICE_SCHEMA = vol.Schema( | ||
| { | ||
| vol.Required(CONF_DEVICE_ID): cv.string, | ||
| vol.Required(CONF_TYPE): vol.In(AVAILABLE_DEVICE_TYPES), | ||
| vol.Optional(CONF_NAME): cv.string, | ||
| } | ||
| ) | ||
|
|
||
| KAITERRA_SCHEMA = vol.Schema( | ||
| { | ||
| vol.Required(CONF_API_KEY): cv.string, | ||
| vol.Required(CONF_DEVICES): vol.All(cv.ensure_list, [KAITERRA_DEVICE_SCHEMA]), | ||
| vol.Optional(CONF_AQI_STANDARD, default=DEFAULT_AQI_STANDARD): vol.In( | ||
| AVAILABLE_AQI_STANDARDS | ||
| ), | ||
| vol.Optional(CONF_PREFERRED_UNITS, default=DEFAULT_PREFERRED_UNIT): vol.All( | ||
| cv.ensure_list, [vol.In(AVAILABLE_UNITS)] | ||
| ), | ||
| vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): cv.time_period, | ||
| } | ||
| ) | ||
|
|
||
| CONFIG_SCHEMA = vol.Schema({DOMAIN: KAITERRA_SCHEMA}, extra=vol.ALLOW_EXTRA) | ||
|
|
||
|
|
||
| async def async_setup(hass, config): | ||
| """Set up the Kaiterra components.""" | ||
|
|
||
| conf = config[DOMAIN] | ||
| scan_interval = conf[CONF_SCAN_INTERVAL] | ||
| devices = conf[CONF_DEVICES] | ||
| session = async_get_clientsession(hass) | ||
| api = hass.data[DOMAIN] = KaiterraApiData(hass, conf, session) | ||
|
|
||
| await api.async_update() | ||
|
|
||
| async def _update(now=None): | ||
| """Periodic update.""" | ||
| await api.async_update() | ||
|
|
||
| async_track_time_interval(hass, _update, scan_interval) | ||
|
|
||
| # Load platforms for each device | ||
| for device in devices: | ||
| device_name, device_id = ( | ||
| device.get(CONF_NAME) or device[CONF_TYPE], | ||
| device[CONF_DEVICE_ID], | ||
| ) | ||
| for component in KAITERRA_COMPONENTS: | ||
| hass.async_create_task( | ||
| async_load_platform( | ||
| hass, | ||
| component, | ||
| DOMAIN, | ||
| {CONF_NAME: device_name, CONF_DEVICE_ID: device_id}, | ||
| config, | ||
| ) | ||
| ) | ||
|
|
||
| return True | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| """Support for Kaiterra Air Quality Sensors.""" | ||
| from homeassistant.components.air_quality import AirQualityEntity | ||
|
|
||
| from homeassistant.helpers.dispatcher import async_dispatcher_connect | ||
|
|
||
| from homeassistant.const import CONF_DEVICE_ID, CONF_NAME | ||
|
|
||
| from .const import ( | ||
| DOMAIN, | ||
| ATTR_VOC, | ||
| ATTR_AQI_LEVEL, | ||
| ATTR_AQI_POLLUTANT, | ||
| DISPATCHER_KAITERRA, | ||
| ) | ||
|
|
||
|
|
||
| async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): | ||
| """Set up the air_quality kaiterra sensor.""" | ||
| if discovery_info is None: | ||
| return | ||
|
|
||
| api = hass.data[DOMAIN] | ||
| name = discovery_info[CONF_NAME] | ||
| device_id = discovery_info[CONF_DEVICE_ID] | ||
|
|
||
| async_add_entities([KaiterraAirQuality(api, name, device_id)]) | ||
|
|
||
|
|
||
| class KaiterraAirQuality(AirQualityEntity): | ||
| """Implementation of a Kaittera air quality sensor.""" | ||
|
|
||
| def __init__(self, api, name, device_id): | ||
| """Initialize the sensor.""" | ||
| self._api = api | ||
| self._name = f"{name} Air Quality" | ||
| self._device_id = device_id | ||
|
|
||
| def _data(self, key): | ||
| return self._device.get(key, {}).get("value") | ||
|
|
||
| @property | ||
| def _device(self): | ||
| return self._api.data.get(self._device_id, {}) | ||
|
|
||
| @property | ||
| def should_poll(self): | ||
| """Return that the sensor should not be polled.""" | ||
| return False | ||
|
|
||
| @property | ||
| def available(self): | ||
| """Return the availability of the sensor.""" | ||
| return self._api.data.get(self._device_id) is not None | ||
|
|
||
| @property | ||
| def name(self): | ||
| """Return the name of the sensor.""" | ||
| return self._name | ||
|
|
||
| @property | ||
| def air_quality_index(self): | ||
| """Return the Air Quality Index (AQI).""" | ||
| return self._data("aqi") | ||
|
|
||
| @property | ||
| def air_quality_index_level(self): | ||
| """Return the Air Quality Index level.""" | ||
| return self._data("aqi_level") | ||
|
|
||
| @property | ||
| def air_quality_index_pollutant(self): | ||
| """Return the Air Quality Index level.""" | ||
| return self._data("aqi_pollutant") | ||
|
|
||
| @property | ||
| def particulate_matter_2_5(self): | ||
| """Return the particulate matter 2.5 level.""" | ||
| return self._data("rpm25c") | ||
|
|
||
| @property | ||
| def particulate_matter_10(self): | ||
| """Return the particulate matter 10 level.""" | ||
| return self._data("rpm10c") | ||
|
|
||
| @property | ||
| def volatile_organic_compounds(self): | ||
| """Return the VOC (Volatile Organic Compounds) level.""" | ||
| return self._data("rtvoc") | ||
|
|
||
| @property | ||
| def unique_id(self): | ||
| """Return the sensor's unique id.""" | ||
| return f"{self._device_id}_air_quality" | ||
|
|
||
| @property | ||
| def device_state_attributes(self): | ||
| """Return the device state attributes.""" | ||
| data = {} | ||
| attributes = [ | ||
| (ATTR_VOC, self.volatile_organic_compounds), | ||
| (ATTR_AQI_LEVEL, self.air_quality_index_level), | ||
| (ATTR_AQI_POLLUTANT, self.air_quality_index_pollutant), | ||
| ] | ||
|
|
||
| for attr, value in attributes: | ||
| if value is not None: | ||
| data[attr] = value | ||
|
|
||
| return data | ||
|
|
||
| async def async_added_to_hass(self): | ||
| """Register callback.""" | ||
| async_dispatcher_connect( | ||
| self.hass, DISPATCHER_KAITERRA, self.async_write_ha_state | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| """Data for all Kaiterra devices.""" | ||
| from logging import getLogger | ||
|
|
||
| import asyncio | ||
|
|
||
| import async_timeout | ||
|
|
||
| from aiohttp.client_exceptions import ClientResponseError | ||
|
|
||
| from kaiterra_async_client import KaiterraAPIClient, AQIStandard, Units | ||
|
|
||
| from homeassistant.helpers.dispatcher import async_dispatcher_send | ||
|
|
||
| from homeassistant.const import CONF_API_KEY, CONF_DEVICES, CONF_DEVICE_ID, CONF_TYPE | ||
|
|
||
| from .const import ( | ||
| AQI_SCALE, | ||
| AQI_LEVEL, | ||
| CONF_AQI_STANDARD, | ||
| CONF_PREFERRED_UNITS, | ||
| DISPATCHER_KAITERRA, | ||
| ) | ||
|
|
||
| _LOGGER = getLogger(__name__) | ||
|
|
||
| POLLUTANTS = {"rpm25c": "PM2.5", "rpm10c": "PM10", "rtvoc": "TVOC"} | ||
|
|
||
|
|
||
| class KaiterraApiData: | ||
| """Get data from Kaiterra API.""" | ||
|
|
||
| def __init__(self, hass, config, session): | ||
| """Initialize the API data object.""" | ||
|
|
||
| api_key = config[CONF_API_KEY] | ||
| aqi_standard = config[CONF_AQI_STANDARD] | ||
| devices = config[CONF_DEVICES] | ||
| units = config[CONF_PREFERRED_UNITS] | ||
|
|
||
| self._hass = hass | ||
| self._api = KaiterraAPIClient( | ||
| session, | ||
| api_key=api_key, | ||
| aqi_standard=AQIStandard.from_str(aqi_standard), | ||
| preferred_units=[Units.from_str(unit) for unit in units], | ||
| ) | ||
| self._devices_ids = [device[CONF_DEVICE_ID] for device in devices] | ||
| self._devices = [ | ||
| f"/{device[CONF_TYPE]}s/{device[CONF_DEVICE_ID]}" for device in devices | ||
| ] | ||
| self._scale = AQI_SCALE[aqi_standard] | ||
| self._level = AQI_LEVEL[aqi_standard] | ||
| self._update_listeners = [] | ||
| self.data = {} | ||
|
|
||
| async def async_update(self) -> None: | ||
| """Get the data from Kaiterra API.""" | ||
|
|
||
| try: | ||
| with async_timeout.timeout(10): | ||
| data = await self._api.get_latest_sensor_readings(self._devices) | ||
| except (ClientResponseError, asyncio.TimeoutError): | ||
| _LOGGER.debug("Couldn't fetch data") | ||
| self.data = {} | ||
| async_dispatcher_send(self._hass, DISPATCHER_KAITERRA) | ||
|
|
||
| _LOGGER.debug("New data retrieved: %s", data) | ||
|
|
||
| try: | ||
| self.data = {} | ||
| for i, device in enumerate(data): | ||
| if not device: | ||
| self.data[self._devices_ids[i]] = {} | ||
| continue | ||
|
|
||
| aqi, main_pollutant = None, None | ||
| for sensor_name, sensor in device.items(): | ||
| points = sensor.get("points") | ||
|
|
||
| if not points: | ||
| continue | ||
|
|
||
| point = points[0] | ||
| sensor["value"] = point.get("value") | ||
|
|
||
| if "aqi" not in point: | ||
| continue | ||
|
|
||
| sensor["aqi"] = point["aqi"] | ||
| if not aqi or aqi < point["aqi"]: | ||
| aqi = point["aqi"] | ||
| main_pollutant = POLLUTANTS.get(sensor_name) | ||
|
|
||
| level = None | ||
| for j in range(1, len(self._scale)): | ||
| if aqi <= self._scale[j]: | ||
| level = self._level[j - 1] | ||
| break | ||
|
|
||
| device["aqi"] = {"value": aqi} | ||
| device["aqi_level"] = {"value": level} | ||
| device["aqi_pollutant"] = {"value": main_pollutant} | ||
|
|
||
| self.data[self._devices_ids[i]] = device | ||
|
|
||
| async_dispatcher_send(self._hass, DISPATCHER_KAITERRA) | ||
| except IndexError as err: | ||
| _LOGGER.error("Parsing error %s", err) | ||
| async_dispatcher_send(self._hass, DISPATCHER_KAITERRA) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| """Consts for Kaiterra integration.""" | ||
|
|
||
| from datetime import timedelta | ||
|
|
||
| DOMAIN = "kaiterra" | ||
|
|
||
| DISPATCHER_KAITERRA = "kaiterra_update" | ||
|
|
||
| AQI_SCALE = { | ||
| "cn": [0, 50, 100, 150, 200, 300, 400, 500], | ||
| "in": [0, 50, 100, 200, 300, 400, 500], | ||
| "us": [0, 50, 100, 150, 200, 300, 500], | ||
| } | ||
| AQI_LEVEL = { | ||
| "cn": [ | ||
| "Good", | ||
| "Satisfactory", | ||
| "Moderate", | ||
| "Unhealthy for sensitive groups", | ||
| "Unhealthy", | ||
| "Very unhealthy", | ||
| "Hazardous", | ||
| ], | ||
| "in": [ | ||
| "Good", | ||
| "Satisfactory", | ||
| "Moderately polluted", | ||
| "Poor", | ||
| "Very poor", | ||
| "Severe", | ||
| ], | ||
| "us": [ | ||
| "Good", | ||
| "Moderate", | ||
| "Unhealthy for sensitive groups", | ||
| "Unhealthy", | ||
| "Very unhealthy", | ||
| "Hazardous", | ||
| ], | ||
| } | ||
|
|
||
| ATTR_VOC = "volatile_organic_compounds" | ||
| ATTR_AQI_LEVEL = "air_quality_index_level" | ||
| ATTR_AQI_POLLUTANT = "air_quality_index_pollutant" | ||
|
|
||
| AVAILABLE_AQI_STANDARDS = ["us", "cn", "in"] | ||
| AVAILABLE_UNITS = ["x", "%", "C", "F", "mg/m³", "µg/m³", "ppm", "ppb"] | ||
| AVAILABLE_DEVICE_TYPES = ["laseregg", "sensedge"] | ||
|
|
||
| CONF_AQI_STANDARD = "aqi_standard" | ||
| CONF_PREFERRED_UNITS = "preferred_units" | ||
|
|
||
| DEFAULT_AQI_STANDARD = "us" | ||
| DEFAULT_PREFERRED_UNIT = [] | ||
| DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) | ||
|
|
||
| KAITERRA_COMPONENTS = ["sensor", "air_quality"] |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.