mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
Enable config flow for Luftdaten (#17700)
* Move file to new location * Update requirement * Enable config flow * Add luftdaten * Add tests * Update * Add constants * Changes according to the review comments * Remove wrong entry from flows * Fix dict handling * Add callback and use OrderedDict * Remve leftover * Fix * Remove await
This commit is contained in:
parent
7933bd7f91
commit
2e517ab6bc
@ -109,7 +109,6 @@ homeassistant/components/sensor/gpsd.py @fabaff
|
|||||||
homeassistant/components/sensor/irish_rail_transport.py @ttroy50
|
homeassistant/components/sensor/irish_rail_transport.py @ttroy50
|
||||||
homeassistant/components/sensor/jewish_calendar.py @tsvi
|
homeassistant/components/sensor/jewish_calendar.py @tsvi
|
||||||
homeassistant/components/sensor/linux_battery.py @fabaff
|
homeassistant/components/sensor/linux_battery.py @fabaff
|
||||||
homeassistant/components/sensor/luftdaten.py @fabaff
|
|
||||||
homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel
|
homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel
|
||||||
homeassistant/components/sensor/min_max.py @fabaff
|
homeassistant/components/sensor/min_max.py @fabaff
|
||||||
homeassistant/components/sensor/moon.py @fabaff
|
homeassistant/components/sensor/moon.py @fabaff
|
||||||
@ -189,6 +188,8 @@ homeassistant/components/*/konnected.py @heythisisnate
|
|||||||
# L
|
# L
|
||||||
homeassistant/components/lifx.py @amelchio
|
homeassistant/components/lifx.py @amelchio
|
||||||
homeassistant/components/*/lifx.py @amelchio
|
homeassistant/components/*/lifx.py @amelchio
|
||||||
|
homeassistant/components/luftdaten/* @fabaff
|
||||||
|
homeassistant/components/*/luftdaten.py @fabaff
|
||||||
|
|
||||||
# M
|
# M
|
||||||
homeassistant/components/matrix.py @tinloaf
|
homeassistant/components/matrix.py @tinloaf
|
||||||
|
170
homeassistant/components/luftdaten/__init__.py
Normal file
170
homeassistant/components/luftdaten/__init__.py
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
"""
|
||||||
|
Support for Luftdaten stations.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/luftdaten/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.config_entries import SOURCE_IMPORT
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_MONITORED_CONDITIONS, CONF_SCAN_INTERVAL, CONF_SENSORS,
|
||||||
|
CONF_SHOW_ON_MAP, TEMP_CELSIUS)
|
||||||
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||||
|
from homeassistant.helpers.event import async_track_time_interval
|
||||||
|
|
||||||
|
from .config_flow import configured_sensors
|
||||||
|
from .const import CONF_SENSOR_ID, DEFAULT_SCAN_INTERVAL, DOMAIN
|
||||||
|
|
||||||
|
REQUIREMENTS = ['luftdaten==0.3.4']
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DATA_LUFTDATEN = 'luftdaten'
|
||||||
|
DATA_LUFTDATEN_CLIENT = 'data_luftdaten_client'
|
||||||
|
DATA_LUFTDATEN_LISTENER = 'data_luftdaten_listener'
|
||||||
|
DEFAULT_ATTRIBUTION = "Data provided by luftdaten.info"
|
||||||
|
|
||||||
|
SENSOR_HUMIDITY = 'humidity'
|
||||||
|
SENSOR_PM10 = 'P1'
|
||||||
|
SENSOR_PM2_5 = 'P2'
|
||||||
|
SENSOR_PRESSURE = 'pressure'
|
||||||
|
SENSOR_TEMPERATURE = 'temperature'
|
||||||
|
|
||||||
|
TOPIC_UPDATE = '{0}_data_update'.format(DOMAIN)
|
||||||
|
|
||||||
|
VOLUME_MICROGRAMS_PER_CUBIC_METER = 'µg/m3'
|
||||||
|
|
||||||
|
SENSORS = {
|
||||||
|
SENSOR_TEMPERATURE: ['Temperature', 'mdi:thermometer', TEMP_CELSIUS],
|
||||||
|
SENSOR_HUMIDITY: ['Humidity', 'mdi:water-percent', '%'],
|
||||||
|
SENSOR_PRESSURE: ['Pressure', 'mdi:arrow-down-bold', 'Pa'],
|
||||||
|
SENSOR_PM10: ['PM10', 'mdi:thought-bubble',
|
||||||
|
VOLUME_MICROGRAMS_PER_CUBIC_METER],
|
||||||
|
SENSOR_PM2_5: ['PM2.5', 'mdi:thought-bubble-outline',
|
||||||
|
VOLUME_MICROGRAMS_PER_CUBIC_METER]
|
||||||
|
}
|
||||||
|
|
||||||
|
SENSOR_SCHEMA = vol.Schema({
|
||||||
|
vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSORS)):
|
||||||
|
vol.All(cv.ensure_list, [vol.In(SENSORS)])
|
||||||
|
})
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
|
DOMAIN:
|
||||||
|
vol.Schema({
|
||||||
|
vol.Required(CONF_SENSOR_ID): cv.positive_int,
|
||||||
|
vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA,
|
||||||
|
vol.Optional(CONF_SHOW_ON_MAP, default=False): cv.boolean,
|
||||||
|
vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL):
|
||||||
|
cv.time_period,
|
||||||
|
})
|
||||||
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup(hass, config):
|
||||||
|
"""Set up the Luftdaten component."""
|
||||||
|
hass.data[DOMAIN] = {}
|
||||||
|
hass.data[DOMAIN][DATA_LUFTDATEN_CLIENT] = {}
|
||||||
|
hass.data[DOMAIN][DATA_LUFTDATEN_LISTENER] = {}
|
||||||
|
|
||||||
|
if DOMAIN not in config:
|
||||||
|
return True
|
||||||
|
|
||||||
|
conf = config[DOMAIN]
|
||||||
|
station_id = conf.get(CONF_SENSOR_ID)
|
||||||
|
|
||||||
|
if station_id not in configured_sensors(hass):
|
||||||
|
hass.async_create_task(
|
||||||
|
hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={'source': SOURCE_IMPORT},
|
||||||
|
data={
|
||||||
|
CONF_SENSORS: conf[CONF_SENSORS],
|
||||||
|
CONF_SENSOR_ID: conf[CONF_SENSOR_ID],
|
||||||
|
CONF_SHOW_ON_MAP: conf[CONF_SHOW_ON_MAP],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
hass.data[DOMAIN][CONF_SCAN_INTERVAL] = conf[CONF_SCAN_INTERVAL]
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass, config_entry):
|
||||||
|
"""Set up Luftdaten as config entry."""
|
||||||
|
from luftdaten import Luftdaten
|
||||||
|
from luftdaten.exceptions import LuftdatenError
|
||||||
|
|
||||||
|
session = async_get_clientsession(hass)
|
||||||
|
|
||||||
|
try:
|
||||||
|
luftdaten = LuftDatenData(
|
||||||
|
Luftdaten(
|
||||||
|
config_entry.data[CONF_SENSOR_ID], hass.loop, session),
|
||||||
|
config_entry.data.get(CONF_SENSORS, {}).get(
|
||||||
|
CONF_MONITORED_CONDITIONS, list(SENSORS)))
|
||||||
|
await luftdaten.async_update()
|
||||||
|
hass.data[DOMAIN][DATA_LUFTDATEN_CLIENT][config_entry.entry_id] = \
|
||||||
|
luftdaten
|
||||||
|
except LuftdatenError:
|
||||||
|
raise ConfigEntryNotReady
|
||||||
|
|
||||||
|
hass.async_create_task(hass.config_entries.async_forward_entry_setup(
|
||||||
|
config_entry, 'sensor'))
|
||||||
|
|
||||||
|
async def refresh_sensors(event_time):
|
||||||
|
"""Refresh Luftdaten data."""
|
||||||
|
await luftdaten.async_update()
|
||||||
|
async_dispatcher_send(hass, TOPIC_UPDATE)
|
||||||
|
|
||||||
|
hass.data[DOMAIN][DATA_LUFTDATEN_LISTENER][
|
||||||
|
config_entry.entry_id] = async_track_time_interval(
|
||||||
|
hass, refresh_sensors,
|
||||||
|
hass.data[DOMAIN].get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL))
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass, config_entry):
|
||||||
|
"""Unload an Luftdaten config entry."""
|
||||||
|
remove_listener = hass.data[DOMAIN][DATA_LUFTDATEN_LISTENER].pop(
|
||||||
|
config_entry.entry_id)
|
||||||
|
remove_listener()
|
||||||
|
|
||||||
|
for component in ('sensor', ):
|
||||||
|
await hass.config_entries.async_forward_entry_unload(
|
||||||
|
config_entry, component)
|
||||||
|
|
||||||
|
hass.data[DOMAIN][DATA_LUFTDATEN_CLIENT].pop(config_entry.entry_id)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class LuftDatenData:
|
||||||
|
"""Define a generic Luftdaten object."""
|
||||||
|
|
||||||
|
def __init__(self, client, sensor_conditions):
|
||||||
|
"""Initialize the Luftdata object."""
|
||||||
|
self.client = client
|
||||||
|
self.data = {}
|
||||||
|
self.sensor_conditions = sensor_conditions
|
||||||
|
|
||||||
|
async def async_update(self):
|
||||||
|
"""Update sensor/binary sensor data."""
|
||||||
|
from luftdaten.exceptions import LuftdatenError
|
||||||
|
|
||||||
|
try:
|
||||||
|
await self.client.get_data()
|
||||||
|
|
||||||
|
self.data[DATA_LUFTDATEN] = self.client.values
|
||||||
|
self.data[DATA_LUFTDATEN].update(self.client.meta)
|
||||||
|
|
||||||
|
except LuftdatenError:
|
||||||
|
_LOGGER.error("Unable to retrieve data from luftdaten.info")
|
75
homeassistant/components/luftdaten/config_flow.py
Normal file
75
homeassistant/components/luftdaten/config_flow.py
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
"""Config flow to configure the Luftdaten component."""
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.const import CONF_SCAN_INTERVAL, CONF_SHOW_ON_MAP
|
||||||
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.helpers import aiohttp_client
|
||||||
|
|
||||||
|
from .const import CONF_SENSOR_ID, DEFAULT_SCAN_INTERVAL, DOMAIN
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def configured_sensors(hass):
|
||||||
|
"""Return a set of configured Luftdaten sensors."""
|
||||||
|
return set(
|
||||||
|
'{0}'.format(entry.data[CONF_SENSOR_ID])
|
||||||
|
for entry in hass.config_entries.async_entries(DOMAIN))
|
||||||
|
|
||||||
|
|
||||||
|
@config_entries.HANDLERS.register(DOMAIN)
|
||||||
|
class LuftDatenFlowHandler(config_entries.ConfigFlow):
|
||||||
|
"""Handle a Luftdaten config flow."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _show_form(self, errors=None):
|
||||||
|
"""Show the form to the user."""
|
||||||
|
data_schema = OrderedDict()
|
||||||
|
data_schema[vol.Required(CONF_SENSOR_ID)] = str
|
||||||
|
data_schema[vol.Optional(CONF_SHOW_ON_MAP, default=False)] = bool
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id='user',
|
||||||
|
data_schema=vol.Schema(data_schema),
|
||||||
|
errors=errors or {}
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_import(self, import_config):
|
||||||
|
"""Import a config entry from configuration.yaml."""
|
||||||
|
return await self.async_step_user(import_config)
|
||||||
|
|
||||||
|
async def async_step_user(self, user_input=None):
|
||||||
|
"""Handle the start of the config flow."""
|
||||||
|
from luftdaten import Luftdaten, exceptions
|
||||||
|
|
||||||
|
if not user_input:
|
||||||
|
return self._show_form()
|
||||||
|
|
||||||
|
sensor_id = user_input[CONF_SENSOR_ID]
|
||||||
|
|
||||||
|
if sensor_id in configured_sensors(self.hass):
|
||||||
|
return self._show_form({CONF_SENSOR_ID: 'sensor_exists'})
|
||||||
|
|
||||||
|
session = aiohttp_client.async_get_clientsession(self.hass)
|
||||||
|
luftdaten = Luftdaten(
|
||||||
|
user_input[CONF_SENSOR_ID], self.hass.loop, session)
|
||||||
|
try:
|
||||||
|
await luftdaten.get_data()
|
||||||
|
valid = await luftdaten.validate_sensor()
|
||||||
|
except exceptions.LuftdatenConnectionError:
|
||||||
|
return self._show_form(
|
||||||
|
{CONF_SENSOR_ID: 'communication_error'})
|
||||||
|
|
||||||
|
if not valid:
|
||||||
|
return self._show_form({CONF_SENSOR_ID: 'invalid_sensor'})
|
||||||
|
|
||||||
|
scan_interval = user_input.get(
|
||||||
|
CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
|
||||||
|
user_input.update({CONF_SCAN_INTERVAL: scan_interval.seconds})
|
||||||
|
|
||||||
|
return self.async_create_entry(title=sensor_id, data=user_input)
|
10
homeassistant/components/luftdaten/const.py
Normal file
10
homeassistant/components/luftdaten/const.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
"""Define constants for the Luftdaten component."""
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
ATTR_SENSOR_ID = 'sensor_id'
|
||||||
|
|
||||||
|
CONF_SENSOR_ID = 'sensor_id'
|
||||||
|
|
||||||
|
DEFAULT_SCAN_INTERVAL = timedelta(minutes=10)
|
||||||
|
|
||||||
|
DOMAIN = 'luftdaten'
|
20
homeassistant/components/luftdaten/strings.json
Normal file
20
homeassistant/components/luftdaten/strings.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"title": "Luftdaten",
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"title": "Define Luftdaten",
|
||||||
|
"data": {
|
||||||
|
"station_id": "Luftdaten Sensor ID",
|
||||||
|
"show_on_map": "Show on map"
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"sensor_exists": "Sensor already registered",
|
||||||
|
"invalid_sensor": "Sensor not available or invalid",
|
||||||
|
"communication_error": "Unable to communicate with the Luftdaten API"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -4,152 +4,120 @@ Support for Luftdaten sensors.
|
|||||||
For more details about this platform, please refer to the documentation at
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/sensor.luftdaten/
|
https://home-assistant.io/components/sensor.luftdaten/
|
||||||
"""
|
"""
|
||||||
from datetime import timedelta
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import voluptuous as vol
|
from homeassistant.components.luftdaten import (
|
||||||
|
DATA_LUFTDATEN, DATA_LUFTDATEN_CLIENT, DEFAULT_ATTRIBUTION, DOMAIN,
|
||||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
SENSORS, TOPIC_UPDATE)
|
||||||
|
from homeassistant.components.luftdaten.const import ATTR_SENSOR_ID
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ATTRIBUTION, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_MONITORED_CONDITIONS,
|
ATTR_ATTRIBUTION, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_SHOW_ON_MAP)
|
||||||
CONF_NAME, CONF_SHOW_ON_MAP, TEMP_CELSIUS)
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
import homeassistant.helpers.config_validation as cv
|
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.util import Throttle
|
|
||||||
|
|
||||||
REQUIREMENTS = ['luftdaten==0.2.0']
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
ATTR_SENSOR_ID = 'sensor_id'
|
DEPENDENCIES = ['luftdaten']
|
||||||
|
|
||||||
CONF_ATTRIBUTION = "Data provided by luftdaten.info"
|
|
||||||
|
|
||||||
|
|
||||||
VOLUME_MICROGRAMS_PER_CUBIC_METER = 'µg/m3'
|
|
||||||
|
|
||||||
SENSOR_TEMPERATURE = 'temperature'
|
|
||||||
SENSOR_HUMIDITY = 'humidity'
|
|
||||||
SENSOR_PM10 = 'P1'
|
|
||||||
SENSOR_PM2_5 = 'P2'
|
|
||||||
SENSOR_PRESSURE = 'pressure'
|
|
||||||
|
|
||||||
SENSOR_TYPES = {
|
|
||||||
SENSOR_TEMPERATURE: ['Temperature', TEMP_CELSIUS],
|
|
||||||
SENSOR_HUMIDITY: ['Humidity', '%'],
|
|
||||||
SENSOR_PRESSURE: ['Pressure', 'Pa'],
|
|
||||||
SENSOR_PM10: ['PM10', VOLUME_MICROGRAMS_PER_CUBIC_METER],
|
|
||||||
SENSOR_PM2_5: ['PM2.5', VOLUME_MICROGRAMS_PER_CUBIC_METER]
|
|
||||||
}
|
|
||||||
|
|
||||||
DEFAULT_NAME = 'Luftdaten'
|
|
||||||
|
|
||||||
CONF_SENSORID = 'sensorid'
|
|
||||||
|
|
||||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5)
|
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|
||||||
vol.Required(CONF_SENSORID): cv.positive_int,
|
|
||||||
vol.Required(CONF_MONITORED_CONDITIONS):
|
|
||||||
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
|
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
|
||||||
vol.Optional(CONF_SHOW_ON_MAP, default=False): cv.boolean,
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_platform(
|
async def async_setup_platform(
|
||||||
hass, config, async_add_entities, discovery_info=None):
|
hass, config, async_add_entities, discovery_info=None):
|
||||||
"""Set up the Luftdaten sensor."""
|
"""Set up an Luftdaten sensor based on existing config."""
|
||||||
from luftdaten import Luftdaten
|
pass
|
||||||
|
|
||||||
name = config.get(CONF_NAME)
|
|
||||||
show_on_map = config.get(CONF_SHOW_ON_MAP)
|
|
||||||
sensor_id = config.get(CONF_SENSORID)
|
|
||||||
|
|
||||||
session = async_get_clientsession(hass)
|
async def async_setup_entry(hass, entry, async_add_entities):
|
||||||
luftdaten = LuftdatenData(Luftdaten(sensor_id, hass.loop, session))
|
"""Set up a Luftdaten sensor based on a config entry."""
|
||||||
|
luftdaten = hass.data[DOMAIN][DATA_LUFTDATEN_CLIENT][entry.entry_id]
|
||||||
|
|
||||||
await luftdaten.async_update()
|
sensors = []
|
||||||
|
for sensor_type in luftdaten.sensor_conditions:
|
||||||
|
name, icon, unit = SENSORS[sensor_type]
|
||||||
|
sensors.append(
|
||||||
|
LuftdatenSensor(
|
||||||
|
luftdaten, sensor_type, name, icon, unit,
|
||||||
|
entry.data[CONF_SHOW_ON_MAP])
|
||||||
|
)
|
||||||
|
|
||||||
if luftdaten.data is None:
|
async_add_entities(sensors, True)
|
||||||
_LOGGER.error("Sensor is not available: %s", sensor_id)
|
|
||||||
return
|
|
||||||
|
|
||||||
devices = []
|
|
||||||
for variable in config[CONF_MONITORED_CONDITIONS]:
|
|
||||||
if luftdaten.data.values[variable] is None:
|
|
||||||
_LOGGER.warning("It might be that sensor %s is not providing "
|
|
||||||
"measurements for %s", sensor_id, variable)
|
|
||||||
devices.append(
|
|
||||||
LuftdatenSensor(luftdaten, name, variable, sensor_id, show_on_map))
|
|
||||||
|
|
||||||
async_add_entities(devices)
|
|
||||||
|
|
||||||
|
|
||||||
class LuftdatenSensor(Entity):
|
class LuftdatenSensor(Entity):
|
||||||
"""Implementation of a Luftdaten sensor."""
|
"""Implementation of a Luftdaten sensor."""
|
||||||
|
|
||||||
def __init__(self, luftdaten, name, sensor_type, sensor_id, show):
|
def __init__(
|
||||||
|
self, luftdaten, sensor_type, name, icon, unit, show):
|
||||||
"""Initialize the Luftdaten sensor."""
|
"""Initialize the Luftdaten sensor."""
|
||||||
|
self._async_unsub_dispatcher_connect = None
|
||||||
self.luftdaten = luftdaten
|
self.luftdaten = luftdaten
|
||||||
|
self._icon = icon
|
||||||
self._name = name
|
self._name = name
|
||||||
self._state = None
|
self._data = None
|
||||||
self._sensor_id = sensor_id
|
|
||||||
self.sensor_type = sensor_type
|
self.sensor_type = sensor_type
|
||||||
self._unit_of_measurement = SENSOR_TYPES[sensor_type][1]
|
self._unit_of_measurement = unit
|
||||||
self._show_on_map = show
|
self._show_on_map = show
|
||||||
|
self._attrs = {}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def icon(self):
|
||||||
"""Return the name of the sensor."""
|
"""Return the icon."""
|
||||||
return '{} {}'.format(self._name, SENSOR_TYPES[self.sensor_type][0])
|
return self._icon
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
"""Return the state of the device."""
|
"""Return the state of the device."""
|
||||||
return self.luftdaten.data.values[self.sensor_type]
|
return self._data[self.sensor_type]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unit_of_measurement(self):
|
def unit_of_measurement(self):
|
||||||
"""Return the unit of measurement of this entity, if any."""
|
"""Return the unit of measurement of this entity, if any."""
|
||||||
return self._unit_of_measurement
|
return self._unit_of_measurement
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_poll(self):
|
||||||
|
"""Disable polling."""
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self) -> str:
|
||||||
|
"""Return a unique, friendly identifier for this entity."""
|
||||||
|
return '{0}_{1}'.format(self._data['sensor_id'], self.sensor_type)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_state_attributes(self):
|
def device_state_attributes(self):
|
||||||
"""Return the state attributes."""
|
"""Return the state attributes."""
|
||||||
onmap = ATTR_LATITUDE, ATTR_LONGITUDE
|
self._attrs[ATTR_SENSOR_ID] = self._data['sensor_id']
|
||||||
nomap = 'lat', 'long'
|
self._attrs[ATTR_ATTRIBUTION] = DEFAULT_ATTRIBUTION
|
||||||
lat_format, lon_format = onmap if self._show_on_map else nomap
|
|
||||||
|
on_map = ATTR_LATITUDE, ATTR_LONGITUDE
|
||||||
|
no_map = 'lat', 'long'
|
||||||
|
lat_format, lon_format = on_map if self._show_on_map else no_map
|
||||||
try:
|
try:
|
||||||
attr = {
|
self._attrs[lon_format] = self._data['longitude']
|
||||||
ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
|
self._attrs[lat_format] = self._data['latitude']
|
||||||
ATTR_SENSOR_ID: self._sensor_id,
|
return self._attrs
|
||||||
lat_format: self.luftdaten.data.meta['latitude'],
|
|
||||||
lon_format: self.luftdaten.data.meta['longitude'],
|
|
||||||
}
|
|
||||||
return attr
|
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
async def async_added_to_hass(self):
|
||||||
|
"""Register callbacks."""
|
||||||
|
@callback
|
||||||
|
def update():
|
||||||
|
"""Update the state."""
|
||||||
|
self.async_schedule_update_ha_state(True)
|
||||||
|
|
||||||
|
self._async_unsub_dispatcher_connect = async_dispatcher_connect(
|
||||||
|
self.hass, TOPIC_UPDATE, update)
|
||||||
|
|
||||||
|
async def async_will_remove_from_hass(self):
|
||||||
|
"""Disconnect dispatcher listener when removed."""
|
||||||
|
if self._async_unsub_dispatcher_connect:
|
||||||
|
self._async_unsub_dispatcher_connect()
|
||||||
|
|
||||||
async def async_update(self):
|
async def async_update(self):
|
||||||
"""Get the latest data from luftdaten.info and update the state."""
|
"""Get the latest data and update the state."""
|
||||||
await self.luftdaten.async_update()
|
|
||||||
|
|
||||||
|
|
||||||
class LuftdatenData:
|
|
||||||
"""Class for handling the data retrieval."""
|
|
||||||
|
|
||||||
def __init__(self, data):
|
|
||||||
"""Initialize the data object."""
|
|
||||||
self.data = data
|
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
|
||||||
async def async_update(self):
|
|
||||||
"""Get the latest data from luftdaten.info."""
|
|
||||||
from luftdaten.exceptions import LuftdatenError
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await self.data.async_get_data()
|
self._data = self.luftdaten.data[DATA_LUFTDATEN]
|
||||||
except LuftdatenError:
|
except KeyError:
|
||||||
_LOGGER.error("Unable to retrieve data from luftdaten.info")
|
return
|
||||||
|
@ -145,6 +145,7 @@ FLOWS = [
|
|||||||
'ios',
|
'ios',
|
||||||
'lifx',
|
'lifx',
|
||||||
'mailgun',
|
'mailgun',
|
||||||
|
'luftdaten',
|
||||||
'mqtt',
|
'mqtt',
|
||||||
'nest',
|
'nest',
|
||||||
'openuv',
|
'openuv',
|
||||||
|
@ -590,8 +590,8 @@ locationsharinglib==3.0.7
|
|||||||
# homeassistant.components.logi_circle
|
# homeassistant.components.logi_circle
|
||||||
logi_circle==0.1.7
|
logi_circle==0.1.7
|
||||||
|
|
||||||
# homeassistant.components.sensor.luftdaten
|
# homeassistant.components.luftdaten
|
||||||
luftdaten==0.2.0
|
luftdaten==0.3.4
|
||||||
|
|
||||||
# homeassistant.components.light.lw12wifi
|
# homeassistant.components.light.lw12wifi
|
||||||
lw12==0.9.2
|
lw12==0.9.2
|
||||||
|
@ -112,6 +112,9 @@ libpurecoollink==0.4.2
|
|||||||
# homeassistant.components.media_player.soundtouch
|
# homeassistant.components.media_player.soundtouch
|
||||||
libsoundtouch==0.7.2
|
libsoundtouch==0.7.2
|
||||||
|
|
||||||
|
# homeassistant.components.luftdaten
|
||||||
|
luftdaten==0.3.4
|
||||||
|
|
||||||
# homeassistant.components.sensor.mfi
|
# homeassistant.components.sensor.mfi
|
||||||
# homeassistant.components.switch.mfi
|
# homeassistant.components.switch.mfi
|
||||||
mficlient==0.3.0
|
mficlient==0.3.0
|
||||||
|
@ -50,12 +50,12 @@ TEST_REQUIREMENTS = (
|
|||||||
'evohomeclient',
|
'evohomeclient',
|
||||||
'feedparser',
|
'feedparser',
|
||||||
'foobot_async',
|
'foobot_async',
|
||||||
'gTTS-token',
|
|
||||||
'geojson_client',
|
'geojson_client',
|
||||||
'georss_client',
|
'georss_client',
|
||||||
|
'gTTS-token',
|
||||||
|
'ha-ffmpeg',
|
||||||
'hangups',
|
'hangups',
|
||||||
'HAP-python',
|
'HAP-python',
|
||||||
'ha-ffmpeg',
|
|
||||||
'haversine',
|
'haversine',
|
||||||
'hbmqtt',
|
'hbmqtt',
|
||||||
'hdate',
|
'hdate',
|
||||||
@ -65,6 +65,7 @@ TEST_REQUIREMENTS = (
|
|||||||
'influxdb',
|
'influxdb',
|
||||||
'libpurecoollink',
|
'libpurecoollink',
|
||||||
'libsoundtouch',
|
'libsoundtouch',
|
||||||
|
'luftdaten',
|
||||||
'mficlient',
|
'mficlient',
|
||||||
'numpy',
|
'numpy',
|
||||||
'paho-mqtt',
|
'paho-mqtt',
|
||||||
|
1
tests/components/luftdaten/__init__.py
Normal file
1
tests/components/luftdaten/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""Define tests for the Luftdaten component."""
|
114
tests/components/luftdaten/test_config_flow.py
Normal file
114
tests/components/luftdaten/test_config_flow.py
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
"""Define tests for the Luftdaten config flow."""
|
||||||
|
from datetime import timedelta
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from homeassistant import data_entry_flow
|
||||||
|
from homeassistant.components.luftdaten import DOMAIN, config_flow
|
||||||
|
from homeassistant.components.luftdaten.const import CONF_SENSOR_ID
|
||||||
|
from homeassistant.const import CONF_SCAN_INTERVAL, CONF_SHOW_ON_MAP
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry, mock_coro
|
||||||
|
|
||||||
|
|
||||||
|
async def test_duplicate_error(hass):
|
||||||
|
"""Test that errors are shown when duplicates are added."""
|
||||||
|
conf = {
|
||||||
|
CONF_SENSOR_ID: '12345abcde',
|
||||||
|
}
|
||||||
|
|
||||||
|
MockConfigEntry(domain=DOMAIN, data=conf).add_to_hass(hass)
|
||||||
|
flow = config_flow.LuftDatenFlowHandler()
|
||||||
|
flow.hass = hass
|
||||||
|
|
||||||
|
result = await flow.async_step_user(user_input=conf)
|
||||||
|
assert result['errors'] == {CONF_SENSOR_ID: 'sensor_exists'}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_communication_error(hass):
|
||||||
|
"""Test that no sensor is added while unable to communicate with API."""
|
||||||
|
conf = {
|
||||||
|
CONF_SENSOR_ID: '12345abcde',
|
||||||
|
}
|
||||||
|
|
||||||
|
flow = config_flow.LuftDatenFlowHandler()
|
||||||
|
flow.hass = hass
|
||||||
|
|
||||||
|
with patch('luftdaten.Luftdaten.get_data', return_value=mock_coro(None)):
|
||||||
|
result = await flow.async_step_user(user_input=conf)
|
||||||
|
assert result['errors'] == {CONF_SENSOR_ID: 'invalid_sensor'}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_invalid_sensor(hass):
|
||||||
|
"""Test that an invalid sensor throws an error."""
|
||||||
|
conf = {
|
||||||
|
CONF_SENSOR_ID: '12345abcde',
|
||||||
|
}
|
||||||
|
|
||||||
|
flow = config_flow.LuftDatenFlowHandler()
|
||||||
|
flow.hass = hass
|
||||||
|
|
||||||
|
with patch('luftdaten.Luftdaten.get_data', return_value=mock_coro(False)),\
|
||||||
|
patch('luftdaten.Luftdaten.validate_sensor',
|
||||||
|
return_value=mock_coro(False)):
|
||||||
|
result = await flow.async_step_user(user_input=conf)
|
||||||
|
assert result['errors'] == {CONF_SENSOR_ID: 'invalid_sensor'}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_show_form(hass):
|
||||||
|
"""Test that the form is served with no input."""
|
||||||
|
flow = config_flow.LuftDatenFlowHandler()
|
||||||
|
flow.hass = hass
|
||||||
|
|
||||||
|
result = await flow.async_step_user(user_input=None)
|
||||||
|
|
||||||
|
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result['step_id'] == 'user'
|
||||||
|
|
||||||
|
|
||||||
|
async def test_step_import(hass):
|
||||||
|
"""Test that the import step works."""
|
||||||
|
conf = {
|
||||||
|
CONF_SENSOR_ID: '12345abcde',
|
||||||
|
CONF_SHOW_ON_MAP: False,
|
||||||
|
}
|
||||||
|
|
||||||
|
flow = config_flow.LuftDatenFlowHandler()
|
||||||
|
flow.hass = hass
|
||||||
|
|
||||||
|
with patch('luftdaten.Luftdaten.get_data', return_value=mock_coro(True)), \
|
||||||
|
patch('luftdaten.Luftdaten.validate_sensor',
|
||||||
|
return_value=mock_coro(True)):
|
||||||
|
result = await flow.async_step_import(import_config=conf)
|
||||||
|
|
||||||
|
assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||||
|
assert result['title'] == '12345abcde'
|
||||||
|
assert result['data'] == {
|
||||||
|
CONF_SENSOR_ID: '12345abcde',
|
||||||
|
CONF_SHOW_ON_MAP: False,
|
||||||
|
CONF_SCAN_INTERVAL: 600,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_step_user(hass):
|
||||||
|
"""Test that the user step works."""
|
||||||
|
conf = {
|
||||||
|
CONF_SENSOR_ID: '12345abcde',
|
||||||
|
CONF_SHOW_ON_MAP: False,
|
||||||
|
CONF_SCAN_INTERVAL: timedelta(minutes=5),
|
||||||
|
}
|
||||||
|
|
||||||
|
flow = config_flow.LuftDatenFlowHandler()
|
||||||
|
flow.hass = hass
|
||||||
|
|
||||||
|
with patch('luftdaten.Luftdaten.get_data', return_value=mock_coro(True)), \
|
||||||
|
patch('luftdaten.Luftdaten.validate_sensor',
|
||||||
|
return_value=mock_coro(True)):
|
||||||
|
result = await flow.async_step_user(user_input=conf)
|
||||||
|
|
||||||
|
assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||||
|
assert result['title'] == '12345abcde'
|
||||||
|
assert result['data'] == {
|
||||||
|
CONF_SENSOR_ID: '12345abcde',
|
||||||
|
CONF_SHOW_ON_MAP: False,
|
||||||
|
CONF_SCAN_INTERVAL: 300,
|
||||||
|
}
|
36
tests/components/luftdaten/test_init.py
Normal file
36
tests/components/luftdaten/test_init.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
"""Test the Luftdaten component setup."""
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from homeassistant.components import luftdaten
|
||||||
|
from homeassistant.components.luftdaten.const import CONF_SENSOR_ID, DOMAIN
|
||||||
|
from homeassistant.const import CONF_SCAN_INTERVAL, CONF_SHOW_ON_MAP
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
|
||||||
|
async def test_config_with_sensor_passed_to_config_entry(hass):
|
||||||
|
"""Test that configured options for a sensor are loaded."""
|
||||||
|
conf = {
|
||||||
|
CONF_SENSOR_ID: '12345abcde',
|
||||||
|
CONF_SHOW_ON_MAP: False,
|
||||||
|
CONF_SCAN_INTERVAL: 600,
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch.object(hass, 'config_entries') as mock_config_entries, \
|
||||||
|
patch.object(luftdaten, 'configured_sensors', return_value=[]):
|
||||||
|
assert await async_setup_component(hass, DOMAIN, conf) is True
|
||||||
|
|
||||||
|
assert len(mock_config_entries.flow.mock_calls) == 0
|
||||||
|
|
||||||
|
|
||||||
|
async def test_config_already_registered_not_passed_to_config_entry(hass):
|
||||||
|
"""Test that an already registered sensor does not initiate an import."""
|
||||||
|
conf = {
|
||||||
|
CONF_SENSOR_ID: '12345abcde',
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch.object(hass, 'config_entries') as mock_config_entries, \
|
||||||
|
patch.object(luftdaten, 'configured_sensors',
|
||||||
|
return_value=['12345abcde']):
|
||||||
|
assert await async_setup_component(hass, DOMAIN, conf) is True
|
||||||
|
|
||||||
|
assert len(mock_config_entries.flow.mock_calls) == 0
|
Loading…
x
Reference in New Issue
Block a user