Add config flow for OpenUV (#16159)
* OpenUV config flow in place * Test folder in place * Owner-requested comments * Tests * More tests * Owner-requested changes (part 1 of 2) * Updated requirements * Owner-requested changes (2 of 2) * Removed unnecessary import * Bumping Travis * Updated requirements * More requirements * Updated tests * Owner-requested changes * Hound * Updated docstring
This commit is contained in:
parent
7a6facc875
commit
f96aee2832
14 changed files with 348 additions and 66 deletions
|
@ -228,7 +228,7 @@ omit =
|
||||||
homeassistant/components/opencv.py
|
homeassistant/components/opencv.py
|
||||||
homeassistant/components/*/opencv.py
|
homeassistant/components/*/opencv.py
|
||||||
|
|
||||||
homeassistant/components/openuv.py
|
homeassistant/components/openuv/__init__.py
|
||||||
homeassistant/components/*/openuv.py
|
homeassistant/components/*/openuv.py
|
||||||
|
|
||||||
homeassistant/components/pilight.py
|
homeassistant/components/pilight.py
|
||||||
|
|
|
@ -7,12 +7,11 @@ https://home-assistant.io/components/binary_sensor.openuv/
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||||
from homeassistant.const import CONF_MONITORED_CONDITIONS
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.components.openuv import (
|
from homeassistant.components.openuv import (
|
||||||
BINARY_SENSORS, DATA_PROTECTION_WINDOW, DOMAIN, TOPIC_UPDATE,
|
BINARY_SENSORS, DATA_OPENUV_CLIENT, DATA_PROTECTION_WINDOW, DOMAIN,
|
||||||
TYPE_PROTECTION_WINDOW, OpenUvEntity)
|
TOPIC_UPDATE, TYPE_PROTECTION_WINDOW, OpenUvEntity)
|
||||||
from homeassistant.util.dt import as_local, parse_datetime, utcnow
|
from homeassistant.util.dt import as_local, parse_datetime, utcnow
|
||||||
|
|
||||||
DEPENDENCIES = ['openuv']
|
DEPENDENCIES = ['openuv']
|
||||||
|
@ -26,17 +25,20 @@ ATTR_PROTECTION_WINDOW_ENDING_UV = 'end_uv'
|
||||||
|
|
||||||
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 OpenUV binary sensor platform."""
|
"""Set up an OpenUV sensor based on existing config."""
|
||||||
if discovery_info is None:
|
pass
|
||||||
return
|
|
||||||
|
|
||||||
openuv = hass.data[DOMAIN]
|
|
||||||
|
async def async_setup_entry(hass, entry, async_add_entities):
|
||||||
|
"""Set up an OpenUV sensor based on a config entry."""
|
||||||
|
openuv = hass.data[DOMAIN][DATA_OPENUV_CLIENT][entry.entry_id]
|
||||||
|
|
||||||
binary_sensors = []
|
binary_sensors = []
|
||||||
for sensor_type in discovery_info[CONF_MONITORED_CONDITIONS]:
|
for sensor_type in openuv.binary_sensor_conditions:
|
||||||
name, icon = BINARY_SENSORS[sensor_type]
|
name, icon = BINARY_SENSORS[sensor_type]
|
||||||
binary_sensors.append(
|
binary_sensors.append(
|
||||||
OpenUvBinarySensor(openuv, sensor_type, name, icon))
|
OpenUvBinarySensor(
|
||||||
|
openuv, sensor_type, name, icon, entry.entry_id))
|
||||||
|
|
||||||
async_add_entities(binary_sensors, True)
|
async_add_entities(binary_sensors, True)
|
||||||
|
|
||||||
|
@ -44,14 +46,16 @@ async def async_setup_platform(
|
||||||
class OpenUvBinarySensor(OpenUvEntity, BinarySensorDevice):
|
class OpenUvBinarySensor(OpenUvEntity, BinarySensorDevice):
|
||||||
"""Define a binary sensor for OpenUV."""
|
"""Define a binary sensor for OpenUV."""
|
||||||
|
|
||||||
def __init__(self, openuv, sensor_type, name, icon):
|
def __init__(self, openuv, sensor_type, name, icon, entry_id):
|
||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
super().__init__(openuv)
|
super().__init__(openuv)
|
||||||
|
|
||||||
|
self._entry_id = entry_id
|
||||||
self._icon = icon
|
self._icon = icon
|
||||||
self._latitude = openuv.client.latitude
|
self._latitude = openuv.client.latitude
|
||||||
self._longitude = openuv.client.longitude
|
self._longitude = openuv.client.longitude
|
||||||
self._name = name
|
self._name = name
|
||||||
|
self._dispatch_remove = None
|
||||||
self._sensor_type = sensor_type
|
self._sensor_type = sensor_type
|
||||||
self._state = None
|
self._state = None
|
||||||
|
|
||||||
|
@ -83,8 +87,9 @@ class OpenUvBinarySensor(OpenUvEntity, BinarySensorDevice):
|
||||||
|
|
||||||
async def async_added_to_hass(self):
|
async def async_added_to_hass(self):
|
||||||
"""Register callbacks."""
|
"""Register callbacks."""
|
||||||
async_dispatcher_connect(
|
self._dispatch_remove = async_dispatcher_connect(
|
||||||
self.hass, TOPIC_UPDATE, self._update_data)
|
self.hass, TOPIC_UPDATE, self._update_data)
|
||||||
|
self.async_on_remove(self._dispatch_remove)
|
||||||
|
|
||||||
async def async_update(self):
|
async def async_update(self):
|
||||||
"""Update the state."""
|
"""Update the state."""
|
||||||
|
|
20
homeassistant/components/openuv/.translations/en.json
Normal file
20
homeassistant/components/openuv/.translations/en.json
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"error": {
|
||||||
|
"identifier_exists": "Coordinates already registered",
|
||||||
|
"invalid_api_key": "Invalid API key"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"api_key": "OpenUV API Key",
|
||||||
|
"elevation": "Elevation",
|
||||||
|
"latitude": "Latitude",
|
||||||
|
"longitude": "Longitude"
|
||||||
|
},
|
||||||
|
"title": "Fill in your information"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "OpenUV"
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
"""
|
"""
|
||||||
Support for data from openuv.io.
|
Support for UV data from openuv.io.
|
||||||
|
|
||||||
For more details about this component, please refer to the documentation at
|
For more details about this component, please refer to the documentation at
|
||||||
https://home-assistant.io/components/openuv/
|
https://home-assistant.io/components/openuv/
|
||||||
|
@ -9,21 +9,24 @@ from datetime import timedelta
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.config_entries import SOURCE_IMPORT
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ATTRIBUTION, CONF_API_KEY, CONF_BINARY_SENSORS, CONF_ELEVATION,
|
ATTR_ATTRIBUTION, CONF_API_KEY, CONF_BINARY_SENSORS, CONF_ELEVATION,
|
||||||
CONF_LATITUDE, CONF_LONGITUDE, CONF_MONITORED_CONDITIONS,
|
CONF_LATITUDE, CONF_LONGITUDE, CONF_MONITORED_CONDITIONS,
|
||||||
CONF_SCAN_INTERVAL, CONF_SENSORS)
|
CONF_SCAN_INTERVAL, CONF_SENSORS)
|
||||||
from homeassistant.helpers import (
|
from homeassistant.helpers import aiohttp_client, config_validation as cv
|
||||||
aiohttp_client, config_validation as cv, discovery)
|
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.helpers.event import async_track_time_interval
|
from homeassistant.helpers.event import async_track_time_interval
|
||||||
|
|
||||||
REQUIREMENTS = ['pyopenuv==1.0.1']
|
from .config_flow import configured_instances
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
REQUIREMENTS = ['pyopenuv==1.0.4']
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DOMAIN = 'openuv'
|
DATA_OPENUV_CLIENT = 'data_client'
|
||||||
|
DATA_OPENUV_LISTENER = 'data_listener'
|
||||||
DATA_PROTECTION_WINDOW = 'protection_window'
|
DATA_PROTECTION_WINDOW = 'protection_window'
|
||||||
DATA_UV = 'uv'
|
DATA_UV = 'uv'
|
||||||
|
|
||||||
|
@ -82,12 +85,14 @@ SENSOR_SCHEMA = vol.Schema({
|
||||||
})
|
})
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
DOMAIN: vol.Schema({
|
DOMAIN:
|
||||||
|
vol.Schema({
|
||||||
vol.Required(CONF_API_KEY): cv.string,
|
vol.Required(CONF_API_KEY): cv.string,
|
||||||
vol.Optional(CONF_ELEVATION): float,
|
vol.Optional(CONF_ELEVATION): float,
|
||||||
vol.Optional(CONF_LATITUDE): cv.latitude,
|
vol.Optional(CONF_LATITUDE): cv.latitude,
|
||||||
vol.Optional(CONF_LONGITUDE): cv.longitude,
|
vol.Optional(CONF_LONGITUDE): cv.longitude,
|
||||||
vol.Optional(CONF_BINARY_SENSORS, default={}): BINARY_SENSOR_SCHEMA,
|
vol.Optional(CONF_BINARY_SENSORS, default={}):
|
||||||
|
BINARY_SENSOR_SCHEMA,
|
||||||
vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA,
|
vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA,
|
||||||
vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL):
|
vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL):
|
||||||
cv.time_period,
|
cv.time_period,
|
||||||
|
@ -97,24 +102,60 @@ CONFIG_SCHEMA = vol.Schema({
|
||||||
|
|
||||||
async def async_setup(hass, config):
|
async def async_setup(hass, config):
|
||||||
"""Set up the OpenUV component."""
|
"""Set up the OpenUV component."""
|
||||||
from pyopenuv import Client
|
hass.data[DOMAIN] = {}
|
||||||
from pyopenuv.errors import OpenUvError
|
hass.data[DOMAIN][DATA_OPENUV_CLIENT] = {}
|
||||||
|
hass.data[DOMAIN][DATA_OPENUV_LISTENER] = {}
|
||||||
|
|
||||||
|
if DOMAIN not in config:
|
||||||
|
return True
|
||||||
|
|
||||||
conf = config[DOMAIN]
|
conf = config[DOMAIN]
|
||||||
api_key = conf[CONF_API_KEY]
|
|
||||||
elevation = conf.get(CONF_ELEVATION, hass.config.elevation)
|
|
||||||
latitude = conf.get(CONF_LATITUDE, hass.config.latitude)
|
latitude = conf.get(CONF_LATITUDE, hass.config.latitude)
|
||||||
longitude = conf.get(CONF_LONGITUDE, hass.config.longitude)
|
longitude = conf.get(CONF_LONGITUDE, hass.config.longitude)
|
||||||
|
elevation = conf.get(CONF_ELEVATION, hass.config.elevation)
|
||||||
|
|
||||||
|
identifier = '{0}, {1}'.format(latitude, longitude)
|
||||||
|
|
||||||
|
if identifier not in configured_instances(hass):
|
||||||
|
hass.async_add_job(
|
||||||
|
hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={'source': SOURCE_IMPORT},
|
||||||
|
data={
|
||||||
|
CONF_API_KEY: conf[CONF_API_KEY],
|
||||||
|
CONF_LATITUDE: latitude,
|
||||||
|
CONF_LONGITUDE: longitude,
|
||||||
|
CONF_ELEVATION: elevation,
|
||||||
|
CONF_BINARY_SENSORS: conf[CONF_BINARY_SENSORS],
|
||||||
|
CONF_SENSORS: conf[CONF_SENSORS],
|
||||||
|
}))
|
||||||
|
|
||||||
|
hass.data[DOMAIN][CONF_SCAN_INTERVAL] = conf[CONF_SCAN_INTERVAL]
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass, config_entry):
|
||||||
|
"""Set up OpenUV as config entry."""
|
||||||
|
from pyopenuv import Client
|
||||||
|
from pyopenuv.errors import OpenUvError
|
||||||
|
|
||||||
try:
|
try:
|
||||||
websession = aiohttp_client.async_get_clientsession(hass)
|
websession = aiohttp_client.async_get_clientsession(hass)
|
||||||
openuv = OpenUV(
|
openuv = OpenUV(
|
||||||
Client(
|
Client(
|
||||||
api_key, latitude, longitude, websession, altitude=elevation),
|
config_entry.data[CONF_API_KEY],
|
||||||
conf[CONF_BINARY_SENSORS][CONF_MONITORED_CONDITIONS] +
|
config_entry.data.get(CONF_LATITUDE, hass.config.latitude),
|
||||||
conf[CONF_SENSORS][CONF_MONITORED_CONDITIONS])
|
config_entry.data.get(CONF_LONGITUDE, hass.config.longitude),
|
||||||
|
websession,
|
||||||
|
altitude=config_entry.data.get(
|
||||||
|
CONF_ELEVATION, hass.config.elevation)),
|
||||||
|
config_entry.data.get(CONF_BINARY_SENSORS, {}).get(
|
||||||
|
CONF_MONITORED_CONDITIONS, list(BINARY_SENSORS)),
|
||||||
|
config_entry.data.get(CONF_SENSORS, {}).get(
|
||||||
|
CONF_MONITORED_CONDITIONS, list(SENSORS)))
|
||||||
await openuv.async_update()
|
await openuv.async_update()
|
||||||
hass.data[DOMAIN] = openuv
|
hass.data[DOMAIN][DATA_OPENUV_CLIENT][config_entry.entry_id] = openuv
|
||||||
except OpenUvError as err:
|
except OpenUvError as err:
|
||||||
_LOGGER.error('An error occurred: %s', str(err))
|
_LOGGER.error('An error occurred: %s', str(err))
|
||||||
hass.components.persistent_notification.create(
|
hass.components.persistent_notification.create(
|
||||||
|
@ -125,13 +166,9 @@ async def async_setup(hass, config):
|
||||||
notification_id=NOTIFICATION_ID)
|
notification_id=NOTIFICATION_ID)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
for component, schema in [
|
for component in ('binary_sensor', 'sensor'):
|
||||||
('binary_sensor', conf[CONF_BINARY_SENSORS]),
|
hass.async_create_task(hass.config_entries.async_forward_entry_setup(
|
||||||
('sensor', conf[CONF_SENSORS]),
|
config_entry, component))
|
||||||
]:
|
|
||||||
hass.async_create_task(
|
|
||||||
discovery.async_load_platform(
|
|
||||||
hass, component, DOMAIN, schema, config))
|
|
||||||
|
|
||||||
async def refresh_sensors(event_time):
|
async def refresh_sensors(event_time):
|
||||||
"""Refresh OpenUV data."""
|
"""Refresh OpenUV data."""
|
||||||
|
@ -139,7 +176,25 @@ async def async_setup(hass, config):
|
||||||
await openuv.async_update()
|
await openuv.async_update()
|
||||||
async_dispatcher_send(hass, TOPIC_UPDATE)
|
async_dispatcher_send(hass, TOPIC_UPDATE)
|
||||||
|
|
||||||
async_track_time_interval(hass, refresh_sensors, conf[CONF_SCAN_INTERVAL])
|
hass.data[DOMAIN][DATA_OPENUV_LISTENER][
|
||||||
|
config_entry.entry_id] = async_track_time_interval(
|
||||||
|
hass, refresh_sensors,
|
||||||
|
hass.data[DOMAIN][CONF_SCAN_INTERVAL])
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass, config_entry):
|
||||||
|
"""Unload an OpenUV config entry."""
|
||||||
|
for component in ('binary_sensor', 'sensor'):
|
||||||
|
await hass.config_entries.async_forward_entry_unload(
|
||||||
|
config_entry, component)
|
||||||
|
|
||||||
|
hass.data[DOMAIN][DATA_OPENUV_CLIENT].pop(config_entry.entry_id)
|
||||||
|
|
||||||
|
remove_listener = hass.data[DOMAIN][DATA_OPENUV_LISTENER].pop(
|
||||||
|
config_entry.entry_id)
|
||||||
|
remove_listener()
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -147,19 +202,20 @@ async def async_setup(hass, config):
|
||||||
class OpenUV:
|
class OpenUV:
|
||||||
"""Define a generic OpenUV object."""
|
"""Define a generic OpenUV object."""
|
||||||
|
|
||||||
def __init__(self, client, monitored_conditions):
|
def __init__(self, client, binary_sensor_conditions, sensor_conditions):
|
||||||
"""Initialize."""
|
"""Initialize."""
|
||||||
self._monitored_conditions = monitored_conditions
|
self.binary_sensor_conditions = binary_sensor_conditions
|
||||||
self.client = client
|
self.client = client
|
||||||
self.data = {}
|
self.data = {}
|
||||||
|
self.sensor_conditions = sensor_conditions
|
||||||
|
|
||||||
async def async_update(self):
|
async def async_update(self):
|
||||||
"""Update sensor/binary sensor data."""
|
"""Update sensor/binary sensor data."""
|
||||||
if TYPE_PROTECTION_WINDOW in self._monitored_conditions:
|
if TYPE_PROTECTION_WINDOW in self.binary_sensor_conditions:
|
||||||
data = await self.client.uv_protection_window()
|
data = await self.client.uv_protection_window()
|
||||||
self.data[DATA_PROTECTION_WINDOW] = data
|
self.data[DATA_PROTECTION_WINDOW] = data
|
||||||
|
|
||||||
if any(c in self._monitored_conditions for c in SENSORS):
|
if any(c in self.sensor_conditions for c in SENSORS):
|
||||||
data = await self.client.uv_index()
|
data = await self.client.uv_index()
|
||||||
self.data[DATA_UV] = data
|
self.data[DATA_UV] = data
|
||||||
|
|
73
homeassistant/components/openuv/config_flow.py
Normal file
73
homeassistant/components/openuv/config_flow.py
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
"""Config flow to configure the OpenUV component."""
|
||||||
|
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant import config_entries, data_entry_flow
|
||||||
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_API_KEY, CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE)
|
||||||
|
from homeassistant.helpers import aiohttp_client, config_validation as cv
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def configured_instances(hass):
|
||||||
|
"""Return a set of configured OpenUV instances."""
|
||||||
|
return set(
|
||||||
|
'{0}, {1}'.format(
|
||||||
|
entry.data[CONF_LATITUDE], entry.data[CONF_LONGITUDE])
|
||||||
|
for entry in hass.config_entries.async_entries(DOMAIN))
|
||||||
|
|
||||||
|
|
||||||
|
@config_entries.HANDLERS.register(DOMAIN)
|
||||||
|
class OpenUvFlowHandler(data_entry_flow.FlowHandler):
|
||||||
|
"""Handle an OpenUV config flow."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Initialize the config flow."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
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 pyopenuv.util import validate_api_key
|
||||||
|
|
||||||
|
errors = {}
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
identifier = '{0}, {1}'.format(
|
||||||
|
user_input.get(CONF_LATITUDE, self.hass.config.latitude),
|
||||||
|
user_input.get(CONF_LONGITUDE, self.hass.config.longitude))
|
||||||
|
|
||||||
|
if identifier in configured_instances(self.hass):
|
||||||
|
errors['base'] = 'identifier_exists'
|
||||||
|
else:
|
||||||
|
websession = aiohttp_client.async_get_clientsession(self.hass)
|
||||||
|
api_key_validation = await validate_api_key(
|
||||||
|
user_input[CONF_API_KEY], websession)
|
||||||
|
if api_key_validation:
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=identifier,
|
||||||
|
data=user_input,
|
||||||
|
)
|
||||||
|
errors['base'] = 'invalid_api_key'
|
||||||
|
|
||||||
|
data_schema = OrderedDict()
|
||||||
|
data_schema[vol.Required(CONF_API_KEY)] = str
|
||||||
|
data_schema[vol.Optional(CONF_LATITUDE)] = cv.latitude
|
||||||
|
data_schema[vol.Optional(CONF_LONGITUDE)] = cv.longitude
|
||||||
|
data_schema[vol.Optional(CONF_ELEVATION)] = vol.Coerce(float)
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id='user',
|
||||||
|
data_schema=vol.Schema(data_schema),
|
||||||
|
errors=errors,
|
||||||
|
)
|
3
homeassistant/components/openuv/const.py
Normal file
3
homeassistant/components/openuv/const.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
"""Define constants for the OpenUV component."""
|
||||||
|
|
||||||
|
DOMAIN = 'openuv'
|
20
homeassistant/components/openuv/strings.json
Normal file
20
homeassistant/components/openuv/strings.json
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"title": "OpenUV",
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"title": "Fill in your information",
|
||||||
|
"data": {
|
||||||
|
"api_key": "OpenUV API Key",
|
||||||
|
"elevation": "Elevation",
|
||||||
|
"latitude": "Latitude",
|
||||||
|
"longitude": "Longitude"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"identifier_exists": "Coordinates already registered",
|
||||||
|
"invalid_api_key": "Invalid API key"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,13 +6,12 @@ https://home-assistant.io/components/sensor.openuv/
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from homeassistant.const import CONF_MONITORED_CONDITIONS
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.components.openuv import (
|
from homeassistant.components.openuv import (
|
||||||
DATA_UV, DOMAIN, SENSORS, TOPIC_UPDATE, TYPE_CURRENT_OZONE_LEVEL,
|
DATA_OPENUV_CLIENT, DATA_UV, DOMAIN, SENSORS, TOPIC_UPDATE,
|
||||||
TYPE_CURRENT_UV_INDEX, TYPE_CURRENT_UV_LEVEL, TYPE_MAX_UV_INDEX,
|
TYPE_CURRENT_OZONE_LEVEL, TYPE_CURRENT_UV_INDEX, TYPE_CURRENT_UV_LEVEL,
|
||||||
TYPE_SAFE_EXPOSURE_TIME_1, TYPE_SAFE_EXPOSURE_TIME_2,
|
TYPE_MAX_UV_INDEX, TYPE_SAFE_EXPOSURE_TIME_1, TYPE_SAFE_EXPOSURE_TIME_2,
|
||||||
TYPE_SAFE_EXPOSURE_TIME_3, TYPE_SAFE_EXPOSURE_TIME_4,
|
TYPE_SAFE_EXPOSURE_TIME_3, TYPE_SAFE_EXPOSURE_TIME_4,
|
||||||
TYPE_SAFE_EXPOSURE_TIME_5, TYPE_SAFE_EXPOSURE_TIME_6, OpenUvEntity)
|
TYPE_SAFE_EXPOSURE_TIME_5, TYPE_SAFE_EXPOSURE_TIME_6, OpenUvEntity)
|
||||||
from homeassistant.util.dt import as_local, parse_datetime
|
from homeassistant.util.dt import as_local, parse_datetime
|
||||||
|
@ -40,16 +39,20 @@ UV_LEVEL_LOW = "Low"
|
||||||
|
|
||||||
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 OpenUV binary sensor platform."""
|
"""Set up an OpenUV sensor based on existing config."""
|
||||||
if discovery_info is None:
|
pass
|
||||||
return
|
|
||||||
|
|
||||||
openuv = hass.data[DOMAIN]
|
|
||||||
|
async def async_setup_entry(hass, entry, async_add_entities):
|
||||||
|
"""Set up a Nest sensor based on a config entry."""
|
||||||
|
openuv = hass.data[DOMAIN][DATA_OPENUV_CLIENT][entry.entry_id]
|
||||||
|
|
||||||
sensors = []
|
sensors = []
|
||||||
for sensor_type in discovery_info[CONF_MONITORED_CONDITIONS]:
|
for sensor_type in openuv.sensor_conditions:
|
||||||
name, icon, unit = SENSORS[sensor_type]
|
name, icon, unit = SENSORS[sensor_type]
|
||||||
sensors.append(OpenUvSensor(openuv, sensor_type, name, icon, unit))
|
sensors.append(
|
||||||
|
OpenUvSensor(
|
||||||
|
openuv, sensor_type, name, icon, unit, entry.entry_id))
|
||||||
|
|
||||||
async_add_entities(sensors, True)
|
async_add_entities(sensors, True)
|
||||||
|
|
||||||
|
@ -57,10 +60,12 @@ async def async_setup_platform(
|
||||||
class OpenUvSensor(OpenUvEntity):
|
class OpenUvSensor(OpenUvEntity):
|
||||||
"""Define a binary sensor for OpenUV."""
|
"""Define a binary sensor for OpenUV."""
|
||||||
|
|
||||||
def __init__(self, openuv, sensor_type, name, icon, unit):
|
def __init__(self, openuv, sensor_type, name, icon, unit, entry_id):
|
||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
super().__init__(openuv)
|
super().__init__(openuv)
|
||||||
|
|
||||||
|
self._dispatch_remove = None
|
||||||
|
self._entry_id = entry_id
|
||||||
self._icon = icon
|
self._icon = icon
|
||||||
self._latitude = openuv.client.latitude
|
self._latitude = openuv.client.latitude
|
||||||
self._longitude = openuv.client.longitude
|
self._longitude = openuv.client.longitude
|
||||||
|
@ -102,7 +107,9 @@ class OpenUvSensor(OpenUvEntity):
|
||||||
|
|
||||||
async def async_added_to_hass(self):
|
async def async_added_to_hass(self):
|
||||||
"""Register callbacks."""
|
"""Register callbacks."""
|
||||||
async_dispatcher_connect(self.hass, TOPIC_UPDATE, self._update_data)
|
self._dispatch_remove = async_dispatcher_connect(
|
||||||
|
self.hass, TOPIC_UPDATE, self._update_data)
|
||||||
|
self.async_on_remove(self._dispatch_remove)
|
||||||
|
|
||||||
async def async_update(self):
|
async def async_update(self):
|
||||||
"""Update the state."""
|
"""Update the state."""
|
||||||
|
@ -125,8 +132,7 @@ class OpenUvSensor(OpenUvEntity):
|
||||||
elif self._sensor_type == TYPE_MAX_UV_INDEX:
|
elif self._sensor_type == TYPE_MAX_UV_INDEX:
|
||||||
self._state = data['uv_max']
|
self._state = data['uv_max']
|
||||||
self._attrs.update({
|
self._attrs.update({
|
||||||
ATTR_MAX_UV_TIME: as_local(
|
ATTR_MAX_UV_TIME: as_local(parse_datetime(data['uv_max_time']))
|
||||||
parse_datetime(data['uv_max_time']))
|
|
||||||
})
|
})
|
||||||
elif self._sensor_type in (TYPE_SAFE_EXPOSURE_TIME_1,
|
elif self._sensor_type in (TYPE_SAFE_EXPOSURE_TIME_1,
|
||||||
TYPE_SAFE_EXPOSURE_TIME_2,
|
TYPE_SAFE_EXPOSURE_TIME_2,
|
||||||
|
|
|
@ -141,6 +141,7 @@ FLOWS = [
|
||||||
'homematicip_cloud',
|
'homematicip_cloud',
|
||||||
'hue',
|
'hue',
|
||||||
'nest',
|
'nest',
|
||||||
|
'openuv',
|
||||||
'sonos',
|
'sonos',
|
||||||
'zone',
|
'zone',
|
||||||
]
|
]
|
||||||
|
|
|
@ -990,7 +990,7 @@ pynut2==2.1.2
|
||||||
pynx584==0.4
|
pynx584==0.4
|
||||||
|
|
||||||
# homeassistant.components.openuv
|
# homeassistant.components.openuv
|
||||||
pyopenuv==1.0.1
|
pyopenuv==1.0.4
|
||||||
|
|
||||||
# homeassistant.components.iota
|
# homeassistant.components.iota
|
||||||
pyota==2.0.5
|
pyota==2.0.5
|
||||||
|
|
|
@ -154,6 +154,9 @@ pymonoprice==0.3
|
||||||
# homeassistant.components.binary_sensor.nx584
|
# homeassistant.components.binary_sensor.nx584
|
||||||
pynx584==0.4
|
pynx584==0.4
|
||||||
|
|
||||||
|
# homeassistant.components.openuv
|
||||||
|
pyopenuv==1.0.4
|
||||||
|
|
||||||
# homeassistant.auth.mfa_modules.totp
|
# homeassistant.auth.mfa_modules.totp
|
||||||
# homeassistant.components.sensor.otp
|
# homeassistant.components.sensor.otp
|
||||||
pyotp==2.2.6
|
pyotp==2.2.6
|
||||||
|
|
|
@ -78,6 +78,7 @@ TEST_REQUIREMENTS = (
|
||||||
'pylitejet',
|
'pylitejet',
|
||||||
'pymonoprice',
|
'pymonoprice',
|
||||||
'pynx584',
|
'pynx584',
|
||||||
|
'pyopenuv',
|
||||||
'pyotp',
|
'pyotp',
|
||||||
'pyqwikswitch',
|
'pyqwikswitch',
|
||||||
'PyRMVtransport',
|
'PyRMVtransport',
|
||||||
|
|
1
tests/components/openuv/__init__.py
Normal file
1
tests/components/openuv/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
"""Define tests for the OpenUV component."""
|
93
tests/components/openuv/test_config_flow.py
Normal file
93
tests/components/openuv/test_config_flow.py
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
"""Define tests for the OpenUV config flow."""
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from homeassistant import data_entry_flow
|
||||||
|
from homeassistant.components.openuv import DOMAIN, config_flow
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_API_KEY, CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE)
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry, mock_coro
|
||||||
|
|
||||||
|
|
||||||
|
async def test_duplicate_error(hass):
|
||||||
|
"""Test that errors are shown when duplicates are added."""
|
||||||
|
conf = {
|
||||||
|
CONF_API_KEY: '12345abcde',
|
||||||
|
CONF_ELEVATION: 59.1234,
|
||||||
|
CONF_LATITUDE: 39.128712,
|
||||||
|
CONF_LONGITUDE: -104.9812612,
|
||||||
|
}
|
||||||
|
|
||||||
|
MockConfigEntry(domain=DOMAIN, data=conf).add_to_hass(hass)
|
||||||
|
flow = config_flow.OpenUvFlowHandler()
|
||||||
|
flow.hass = hass
|
||||||
|
|
||||||
|
result = await flow.async_step_user(user_input=conf)
|
||||||
|
assert result['errors'] == {'base': 'identifier_exists'}
|
||||||
|
|
||||||
|
|
||||||
|
@patch('pyopenuv.util.validate_api_key', return_value=mock_coro(False))
|
||||||
|
async def test_invalid_api_key(hass):
|
||||||
|
"""Test that an invalid API key throws an error."""
|
||||||
|
conf = {
|
||||||
|
CONF_API_KEY: '12345abcde',
|
||||||
|
CONF_ELEVATION: 59.1234,
|
||||||
|
CONF_LATITUDE: 39.128712,
|
||||||
|
CONF_LONGITUDE: -104.9812612,
|
||||||
|
}
|
||||||
|
|
||||||
|
flow = config_flow.OpenUvFlowHandler()
|
||||||
|
flow.hass = hass
|
||||||
|
|
||||||
|
result = await flow.async_step_user(user_input=conf)
|
||||||
|
assert result['errors'] == {'base': 'invalid_api_key'}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_show_form(hass):
|
||||||
|
"""Test that the form is served with no input."""
|
||||||
|
flow = config_flow.OpenUvFlowHandler()
|
||||||
|
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'
|
||||||
|
|
||||||
|
|
||||||
|
@patch('pyopenuv.util.validate_api_key', return_value=mock_coro(True))
|
||||||
|
async def test_step_import(hass):
|
||||||
|
"""Test that the import step works."""
|
||||||
|
conf = {
|
||||||
|
CONF_API_KEY: '12345abcde',
|
||||||
|
}
|
||||||
|
|
||||||
|
flow = config_flow.OpenUvFlowHandler()
|
||||||
|
flow.hass = hass
|
||||||
|
|
||||||
|
result = await flow.async_step_import(import_config=conf)
|
||||||
|
|
||||||
|
assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||||
|
assert result['title'] == '{0}, {1}'.format(
|
||||||
|
hass.config.latitude, hass.config.longitude)
|
||||||
|
assert result['data'] == conf
|
||||||
|
|
||||||
|
|
||||||
|
@patch('pyopenuv.util.validate_api_key', return_value=mock_coro(True))
|
||||||
|
async def test_step_user(hass):
|
||||||
|
"""Test that the user step works."""
|
||||||
|
conf = {
|
||||||
|
CONF_API_KEY: '12345abcde',
|
||||||
|
CONF_ELEVATION: 59.1234,
|
||||||
|
CONF_LATITUDE: 39.128712,
|
||||||
|
CONF_LONGITUDE: -104.9812612,
|
||||||
|
}
|
||||||
|
|
||||||
|
flow = config_flow.OpenUvFlowHandler()
|
||||||
|
flow.hass = hass
|
||||||
|
|
||||||
|
result = await flow.async_step_user(user_input=conf)
|
||||||
|
|
||||||
|
assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||||
|
assert result['title'] == '{0}, {1}'.format(
|
||||||
|
conf[CONF_LATITUDE], conf[CONF_LONGITUDE])
|
||||||
|
assert result['data'] == conf
|
Loading…
Add table
Reference in a new issue