Skip to content
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

Add Kaiterra integration #26661

Merged
merged 9 commits into from
Sep 22, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,7 @@ omit =
homeassistant/components/itunes/media_player.py
homeassistant/components/joaoapps_join/*
homeassistant/components/juicenet/*
homeassistant/components/kaiterra/*
homeassistant/components/kankun/switch.py
homeassistant/components/keba/*
homeassistant/components/keenetic_ndms2/device_tracker.py
Expand Down
1 change: 1 addition & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ homeassistant/components/ipma/* @dgomes
homeassistant/components/iqvia/* @bachya
homeassistant/components/irish_rail_transport/* @ttroy50
homeassistant/components/jewish_calendar/* @tsvi
homeassistant/components/kaiterra/* @Michsior14
homeassistant/components/keba/* @dannerph
homeassistant/components/knx/* @Julius2342
homeassistant/components/kodi/* @armills
Expand Down
92 changes: 92 additions & 0 deletions homeassistant/components/kaiterra/__init__.py
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,
Michsior14 marked this conversation as resolved.
Show resolved Hide resolved
)
)

return True
115 changes: 115 additions & 0 deletions homeassistant/components/kaiterra/air_quality.py
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
)
109 changes: 109 additions & 0 deletions homeassistant/components/kaiterra/api_data.py
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)
57 changes: 57 additions & 0 deletions homeassistant/components/kaiterra/const.py
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"]
Loading