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 16 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
82 changes: 82 additions & 0 deletions homeassistant/components/gios/__init__.py
@@ -0,0 +1,82 @@
"""The GIOS component."""
import asyncio
from datetime import timedelta
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, DOMAIN

# Term of service GIOŚ allow downloading data no more than twice an hour.
DEFAULT_SCAN_INTERVAL = timedelta(minutes=30)

_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
166 changes: 166 additions & 0 deletions homeassistant/components/gios/air_quality.py
@@ -0,0 +1,166 @@
"""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, DOMAIN, ICONS_MAP

ATTRIBUTION = "Data provided by GIOŚ"

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._icon = 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:
self._icon = ICONS_MAP[self._aqi]
else:
self._icon = "mdi:blur"
MartinHjelmare marked this conversation as resolved.
Show resolved Hide resolved
return self._icon

@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 state(self):
MartinHjelmare marked this conversation as resolved.
Show resolved Hide resolved
"""Return the air quality index."""
if self.gios.available:
return self._aqi

@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"]
89 changes: 89 additions & 0 deletions homeassistant/components/gios/config_flow.py
@@ -0,0 +1,89 @@
"""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.core import callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession

from .const import CONF_STATION_ID, DEFAULT_NAME, DOMAIN

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


@callback
def configured_instances(hass, condition):
"""Return a set of configured GIOS instances."""
return set(
entry.data[condition] for entry in hass.config_entries.async_entries(DOMAIN)
)
Kane610 marked this conversation as resolved.
Show resolved Hide resolved


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

VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL

def __init__(self):
MartinHjelmare marked this conversation as resolved.
Show resolved Hide resolved
"""Initialize."""

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

if user_input is not None:
try:
if user_input[CONF_NAME] in configured_instances(self.hass, CONF_NAME):
raise NameExists()
if user_input[CONF_STATION_ID] in configured_instances(
self.hass, CONF_STATION_ID
):
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()
return self.async_create_entry(
title=user_input[CONF_NAME], data=user_input
)
except NameExists:
errors[CONF_NAME] = "name_exists"
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 NameExists(exceptions.HomeAssistantError):
MartinHjelmare marked this conversation as resolved.
Show resolved Hide resolved
"""Error to indicate that name is already configured."""


class StationIdExists(exceptions.HomeAssistantError):
"""Error to indicate that station with station_id is already configured."""
15 changes: 15 additions & 0 deletions homeassistant/components/gios/const.py
@@ -0,0 +1,15 @@
"""Constants for GIOS integration."""
ATTR_NAME = "name"
ATTR_STATION = "station"
CONF_STATION_ID = "station_id"
DATA_CLIENT = "client"
DEFAULT_NAME = "GIOŚ"
DOMAIN = "gios"

ICONS_MAP = {
"bardzo dobry": "mdi:emoticon-excited",
"dobry": "mdi:emoticon-happy",
"umiarkowany": "mdi:emoticon-neutral",
"dostateczny": "mdi:emoticon-sad",
"zły": "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
}