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 GIOS integration #28719

Merged
merged 25 commits into from Dec 31, 2019
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions .coveragerc
Expand Up @@ -258,6 +258,9 @@ omit =
homeassistant/components/geniushub/*
homeassistant/components/gearbest/sensor.py
homeassistant/components/geizhals/sensor.py
homeassistant/components/gios/__init__.py
homeassistant/components/gios/air_quality.py
homeassistant/components/gios/consts.py
homeassistant/components/github/sensor.py
homeassistant/components/gitlab_ci/sensor.py
homeassistant/components/gitter/sensor.py
Expand Down
1 change: 1 addition & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Expand Up @@ -119,6 +119,7 @@ homeassistant/components/geniushub/* @zxdavb
homeassistant/components/geo_rss_events/* @exxamalte
homeassistant/components/geonetnz_quakes/* @exxamalte
homeassistant/components/geonetnz_volcano/* @exxamalte
homeassistant/components/gios/* @bieniu
homeassistant/components/gitter/* @fabaff
homeassistant/components/glances/* @fabaff @engrbm87
homeassistant/components/gntp/* @robbiet480
Expand Down
78 changes: 78 additions & 0 deletions homeassistant/components/gios/__init__.py
@@ -0,0 +1,78 @@
"""The GIOS component."""
import asyncio
import logging

from aiohttp.client_exceptions import ClientConnectorError
from async_timeout import timeout
from gios import ApiError, Gios, NoStationError

from homeassistant.core import Config, HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.util import Throttle

from .const import CONF_STATION_ID, DATA_CLIENT, DEFAULT_SCAN_INTERVAL, DOMAIN

_LOGGER = logging.getLogger(__name__)


async def async_setup(hass: HomeAssistant, config: Config) -> bool:
"""Set up configured GIOS."""
hass.data[DOMAIN] = {}
hass.data[DOMAIN][DATA_CLIENT] = {}
return True


async def async_setup_entry(hass, config_entry):
"""Set up GIOS as config entry."""
station_id = config_entry.data[CONF_STATION_ID]
_LOGGER.debug("Using station_id: %s", station_id)

websession = async_get_clientsession(hass)

gios = GiosData(websession, station_id)

await gios.async_update()

hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = gios

hass.async_create_task(
hass.config_entries.async_forward_entry_setup(config_entry, "air_quality")
)
return True


async def async_unload_entry(hass, config_entry):
"""Unload a config entry."""
hass.data[DOMAIN][DATA_CLIENT].pop(config_entry.entry_id)
await hass.config_entries.async_forward_entry_unload(config_entry, "air_quality")
return True


class GiosData:
"""Define an object to hold GIOS data."""

def __init__(self, session, station_id):
"""Initialize."""
self._gios = Gios(station_id, session)
self.station_id = station_id
self.sensors = {}
self.latitude = None
self.longitude = None
self.station_name = None
self.available = True

@Throttle(DEFAULT_SCAN_INTERVAL)
async def async_update(self):
"""Update GIOS data."""
try:
with timeout(30):
await self._gios.update()
except asyncio.TimeoutError:
_LOGGER.error("Asyncio Timeout Error")
except (ApiError, NoStationError, ClientConnectorError) as error:
_LOGGER.error("GIOS data update failed: %s", error)
self.available = self._gios.available
self.latitude = self._gios.latitude
self.longitude = self._gios.longitude
self.station_name = self._gios.station_name
self.sensors = self._gios.data
158 changes: 158 additions & 0 deletions homeassistant/components/gios/air_quality.py
@@ -0,0 +1,158 @@
"""Support for the GIOS service."""
from homeassistant.components.air_quality import (
ATTR_CO,
ATTR_NO2,
ATTR_OZONE,
ATTR_PM_2_5,
ATTR_PM_10,
ATTR_SO2,
AirQualityEntity,
)
from homeassistant.const import CONF_NAME

from .const import ATTR_STATION, DATA_CLIENT, DEFAULT_SCAN_INTERVAL, DOMAIN, ICONS_MAP

ATTRIBUTION = "Data provided by GIOŚ"
SCAN_INTERVAL = DEFAULT_SCAN_INTERVAL

MartinHjelmare marked this conversation as resolved.
Show resolved Hide resolved

async def async_setup_entry(hass, config_entry, async_add_entities):
"""Add a GIOS entities from a config_entry."""
name = config_entry.data[CONF_NAME]

data = hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id]

async_add_entities([GiosAirQuality(data, name)], True)


def round_state(func):
"""Round state."""

def _decorator(self):
res = func(self)
if isinstance(res, float):
return round(res)
return res

return _decorator


class GiosAirQuality(AirQualityEntity):
"""Define an GIOS sensor."""

def __init__(self, gios, name):
"""Initialize."""
self.gios = gios
self._name = name
self._aqi = None
self._co = None
self._no2 = None
self._o3 = None
self._pm_2_5 = None
self._pm_10 = None
self._so2 = None
self._attrs = {}

@property
def name(self):
"""Return the name."""
return self._name

@property
def icon(self):
"""Return the icon."""
if self._aqi in ICONS_MAP:
return ICONS_MAP[self._aqi]
return "mdi:blur"

@property
def air_quality_index(self):
"""Return the air quality index."""
return self._aqi

@property
@round_state
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should rounding be left to the consumers of the data? I don't personally mind, but I think someone might :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rytilahti In Poland, most air quality services on the Internet rounding off values. I think this is a good approach.

def particulate_matter_2_5(self):
"""Return the particulate matter 2.5 level."""
return self._pm_2_5

@property
@round_state
def particulate_matter_10(self):
"""Return the particulate matter 10 level."""
return self._pm_10

@property
@round_state
def ozone(self):
"""Return the O3 (ozone) level."""
return self._o3

@property
@round_state
def carbon_monoxide(self):
"""Return the CO (carbon monoxide) level."""
return self._co

@property
@round_state
def sulphur_dioxide(self):
"""Return the SO2 (sulphur dioxide) level."""
return self._so2

@property
@round_state
def nitrogen_dioxide(self):
"""Return the NO2 (nitrogen dioxide) level."""
return self._no2

@property
def attribution(self):
"""Return the attribution."""
return ATTRIBUTION

@property
def unique_id(self):
"""Return a unique_id for this entity."""
return self.gios.station_id

@property
def available(self):
"""Return True if entity is available."""
return self.gios.available

@property
def device_state_attributes(self):
"""Return the state attributes."""
self._attrs[ATTR_STATION] = self.gios.station_name
return self._attrs

async def async_update(self):
"""Get the data from GIOS."""
await self.gios.async_update()

if self.gios.available:
# Different measuring stations have different sets of sensors. We don't know
# what data we will get.
if "AQI" in self.gios.sensors:
self._aqi = self.gios.sensors["AQI"]["value"]
if "CO" in self.gios.sensors:
self._co = self.gios.sensors["CO"]["value"]
self._attrs[f"{ATTR_CO}_index"] = self.gios.sensors["CO"]["index"]
if "NO2" in self.gios.sensors:
self._no2 = self.gios.sensors["NO2"]["value"]
self._attrs[f"{ATTR_NO2}_index"] = self.gios.sensors["NO2"]["index"]
if "O3" in self.gios.sensors:
self._o3 = self.gios.sensors["O3"]["value"]
self._attrs[f"{ATTR_OZONE}_index"] = self.gios.sensors["O3"]["index"]
if "PM2.5" in self.gios.sensors:
self._pm_2_5 = self.gios.sensors["PM2.5"]["value"]
self._attrs[f"{ATTR_PM_2_5}_index"] = self.gios.sensors["PM2.5"][
"index"
]
if "PM10" in self.gios.sensors:
self._pm_10 = self.gios.sensors["PM10"]["value"]
self._attrs[f"{ATTR_PM_10}_index"] = self.gios.sensors["PM10"]["index"]
if "SO2" in self.gios.sensors:
self._so2 = self.gios.sensors["SO2"]["value"]
self._attrs[f"{ATTR_SO2}_index"] = self.gios.sensors["SO2"]["index"]
73 changes: 73 additions & 0 deletions homeassistant/components/gios/config_flow.py
@@ -0,0 +1,73 @@
"""Adds config flow for GIOS."""
import asyncio

from aiohttp.client_exceptions import ClientConnectorError
from async_timeout import timeout
from gios import ApiError, Gios, NoStationError
import voluptuous as vol

from homeassistant import config_entries, exceptions
from homeassistant.const import CONF_NAME
from homeassistant.helpers.aiohttp_client import async_get_clientsession

from .const import CONF_STATION_ID, DEFAULT_NAME, DOMAIN # pylint:disable=unused-import

DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_STATION_ID): int,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): str,
}
)


class GiosFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Config flow for GIOS."""

VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL

async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
errors = {}

if user_input is not None:
try:
already_configured = self._async_current_ids()
Copy link
Member

@Kane610 Kane610 Dec 29, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Set unique ID first and then you can use self._abort_if_unique_id_configured() here

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

if user_input[CONF_STATION_ID] in already_configured:
raise StationIdExists()

websession = async_get_clientsession(self.hass)

with timeout(30):
gios = Gios(user_input[CONF_STATION_ID], websession)
await gios.update()

if not gios.available:
raise InvalidSensorsData()

await self.async_set_unique_id(
user_input[CONF_STATION_ID], raise_on_progress=False
)
return self.async_create_entry(
title=user_input[CONF_STATION_ID], data=user_input,
)
except StationIdExists:
errors[CONF_STATION_ID] = "station_id_exists"
except (ApiError, ClientConnectorError, asyncio.TimeoutError):
errors["base"] = "cannot_connect"
except NoStationError:
errors[CONF_STATION_ID] = "wrong_station_id"
except InvalidSensorsData:
errors[CONF_STATION_ID] = "invalid_sensors_data"

return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors=errors
)


class InvalidSensorsData(exceptions.HomeAssistantError):
"""Error to indicate invalid sensors data."""


class StationIdExists(exceptions.HomeAssistantError):
"""Error to indicate that station with station_id is already configured."""
25 changes: 25 additions & 0 deletions homeassistant/components/gios/const.py
@@ -0,0 +1,25 @@
"""Constants for GIOS integration."""
from datetime import timedelta

ATTR_NAME = "name"
ATTR_STATION = "station"
CONF_STATION_ID = "station_id"
DATA_CLIENT = "client"
DEFAULT_NAME = "GIOŚ"
# Term of service GIOŚ allow downloading data no more than twice an hour.
DEFAULT_SCAN_INTERVAL = timedelta(minutes=30)
DOMAIN = "gios"

AQI_GOOD = "dobry"
AQI_MODERATE = "umiarkowany"
AQI_POOR = "dostateczny"
AQI_VERY_GOOD = "bardzo dobry"
AQI_VERY_POOR = "zły"

ICONS_MAP = {
AQI_VERY_GOOD: "mdi:emoticon-excited",
AQI_GOOD: "mdi:emoticon-happy",
AQI_MODERATE: "mdi:emoticon-neutral",
AQI_POOR: "mdi:emoticon-sad",
AQI_VERY_POOR: "mdi:emoticon-dead",
}
9 changes: 9 additions & 0 deletions homeassistant/components/gios/manifest.json
@@ -0,0 +1,9 @@
{
"domain": "gios",
"name": "GIOŚ",
"documentation": "https://www.home-assistant.io/integrations/gios",
"dependencies": [],
"codeowners": ["@bieniu"],
"requirements": ["gios==0.0.3"],
"config_flow": true
}
21 changes: 21 additions & 0 deletions homeassistant/components/gios/strings.json
@@ -0,0 +1,21 @@
{
"config": {
"title": "GIOŚ",
"step": {
"user": {
"title": "GIOŚ (Polish Chief Inspectorate Of Environmental Protection)",
"description": "Set up GIOŚ (Polish Chief Inspectorate Of Environmental Protection) air quality integration. If you need help with the configuration have a look here: https://www.home-assistant.io/integrations/gios",
"data": {
"name": "Name of the integration",
"station_id": "ID of the measuring station"
}
}
},
"error": {
"wrong_station_id": "ID of the measuring station is not correct.",
"invalid_sensors_data": "Invalid sensors' data for this measuring station.",
"station_id_exists": "ID of the measuring station already exists.",
"cannot_connect": "Cannot connect to the GIOŚ server."
}
}
}
1 change: 1 addition & 0 deletions homeassistant/generated/config_flows.py
Expand Up @@ -26,6 +26,7 @@
"geofency",
"geonetnz_quakes",
"geonetnz_volcano",
"gios",
"glances",
"gpslogger",
"hangouts",
Expand Down