diff --git a/.coveragerc b/.coveragerc
index 39c31e4e40b0b655942819f0861cd9abfe861df0..bd531e62f725332cc1db43b1e238a4c0d9df36bc 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -123,7 +123,7 @@ omit =
     homeassistant/components/hangouts/const.py
     homeassistant/components/hangouts/hangouts_bot.py
     homeassistant/components/hangouts/hangups_utils.py
-    homeassistant/components/*/hangouts.py    
+    homeassistant/components/*/hangouts.py
 
     homeassistant/components/hdmi_cec.py
     homeassistant/components/*/hdmi_cec.py
@@ -145,12 +145,12 @@ omit =
 
     homeassistant/components/ihc/*
     homeassistant/components/*/ihc.py
-    
+
     homeassistant/components/insteon/*
     homeassistant/components/*/insteon.py
 
     homeassistant/components/insteon_local.py
-    
+
     homeassistant/components/insteon_plm.py
 
     homeassistant/components/ios.py
@@ -228,7 +228,7 @@ omit =
     homeassistant/components/opencv.py
     homeassistant/components/*/opencv.py
 
-    homeassistant/components/openuv.py
+    homeassistant/components/openuv/__init__.py
     homeassistant/components/*/openuv.py
 
     homeassistant/components/pilight.py
diff --git a/homeassistant/components/binary_sensor/openuv.py b/homeassistant/components/binary_sensor/openuv.py
index 0b299529a4610167e74913eaeeee95ba5922a28b..c7c27d73ee42c02798d13a78ab30776077c08b37 100644
--- a/homeassistant/components/binary_sensor/openuv.py
+++ b/homeassistant/components/binary_sensor/openuv.py
@@ -7,12 +7,11 @@ https://home-assistant.io/components/binary_sensor.openuv/
 import logging
 
 from homeassistant.components.binary_sensor import BinarySensorDevice
-from homeassistant.const import CONF_MONITORED_CONDITIONS
 from homeassistant.core import callback
 from homeassistant.helpers.dispatcher import async_dispatcher_connect
 from homeassistant.components.openuv import (
-    BINARY_SENSORS, DATA_PROTECTION_WINDOW, DOMAIN, TOPIC_UPDATE,
-    TYPE_PROTECTION_WINDOW, OpenUvEntity)
+    BINARY_SENSORS, DATA_OPENUV_CLIENT, DATA_PROTECTION_WINDOW, DOMAIN,
+    TOPIC_UPDATE, TYPE_PROTECTION_WINDOW, OpenUvEntity)
 from homeassistant.util.dt import as_local, parse_datetime, utcnow
 
 DEPENDENCIES = ['openuv']
@@ -26,17 +25,20 @@ ATTR_PROTECTION_WINDOW_ENDING_UV = 'end_uv'
 
 async def async_setup_platform(
         hass, config, async_add_entities, discovery_info=None):
-    """Set up the OpenUV binary sensor platform."""
-    if discovery_info is None:
-        return
+    """Set up an OpenUV sensor based on existing config."""
+    pass
 
-    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 = []
-    for sensor_type in discovery_info[CONF_MONITORED_CONDITIONS]:
+    for sensor_type in openuv.binary_sensor_conditions:
         name, icon = BINARY_SENSORS[sensor_type]
         binary_sensors.append(
-            OpenUvBinarySensor(openuv, sensor_type, name, icon))
+            OpenUvBinarySensor(
+                openuv, sensor_type, name, icon, entry.entry_id))
 
     async_add_entities(binary_sensors, True)
 
@@ -44,14 +46,16 @@ async def async_setup_platform(
 class OpenUvBinarySensor(OpenUvEntity, BinarySensorDevice):
     """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."""
         super().__init__(openuv)
 
+        self._entry_id = entry_id
         self._icon = icon
         self._latitude = openuv.client.latitude
         self._longitude = openuv.client.longitude
         self._name = name
+        self._dispatch_remove = None
         self._sensor_type = sensor_type
         self._state = None
 
@@ -83,8 +87,9 @@ class OpenUvBinarySensor(OpenUvEntity, BinarySensorDevice):
 
     async def async_added_to_hass(self):
         """Register callbacks."""
-        async_dispatcher_connect(
+        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):
         """Update the state."""
diff --git a/homeassistant/components/openuv/.translations/en.json b/homeassistant/components/openuv/.translations/en.json
new file mode 100644
index 0000000000000000000000000000000000000000..df0232d01fc2f547b1a6f9a8d0e06e07fa45fa2a
--- /dev/null
+++ b/homeassistant/components/openuv/.translations/en.json
@@ -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"
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/openuv.py b/homeassistant/components/openuv/__init__.py
similarity index 57%
rename from homeassistant/components/openuv.py
rename to homeassistant/components/openuv/__init__.py
index d696f0e51002761b25865461965d1f035785349b..bfd90b4a57419ccea015a79820feff02ab9bb088 100644
--- a/homeassistant/components/openuv.py
+++ b/homeassistant/components/openuv/__init__.py
@@ -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
 https://home-assistant.io/components/openuv/
@@ -9,21 +9,24 @@ from datetime import timedelta
 
 import voluptuous as vol
 
+from homeassistant.config_entries import SOURCE_IMPORT
 from homeassistant.const import (
     ATTR_ATTRIBUTION, CONF_API_KEY, CONF_BINARY_SENSORS, CONF_ELEVATION,
     CONF_LATITUDE, CONF_LONGITUDE, CONF_MONITORED_CONDITIONS,
     CONF_SCAN_INTERVAL, CONF_SENSORS)
-from homeassistant.helpers import (
-    aiohttp_client, config_validation as cv, discovery)
+from homeassistant.helpers import aiohttp_client, config_validation as cv
 from homeassistant.helpers.dispatcher import async_dispatcher_send
 from homeassistant.helpers.entity import Entity
 from homeassistant.helpers.event import async_track_time_interval
 
-REQUIREMENTS = ['pyopenuv==1.0.1']
-_LOGGER = logging.getLogger(__name__)
+from .config_flow import configured_instances
+from .const import DOMAIN
 
-DOMAIN = 'openuv'
+REQUIREMENTS = ['pyopenuv==1.0.4']
+_LOGGER = logging.getLogger(__name__)
 
+DATA_OPENUV_CLIENT = 'data_client'
+DATA_OPENUV_LISTENER = 'data_listener'
 DATA_PROTECTION_WINDOW = 'protection_window'
 DATA_UV = 'uv'
 
@@ -82,39 +85,77 @@ SENSOR_SCHEMA = vol.Schema({
 })
 
 CONFIG_SCHEMA = vol.Schema({
-    DOMAIN: vol.Schema({
-        vol.Required(CONF_API_KEY): cv.string,
-        vol.Optional(CONF_ELEVATION): float,
-        vol.Optional(CONF_LATITUDE): cv.latitude,
-        vol.Optional(CONF_LONGITUDE): cv.longitude,
-        vol.Optional(CONF_BINARY_SENSORS, default={}): BINARY_SENSOR_SCHEMA,
-        vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA,
-        vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL):
-            cv.time_period,
-    })
+    DOMAIN:
+        vol.Schema({
+            vol.Required(CONF_API_KEY): cv.string,
+            vol.Optional(CONF_ELEVATION): float,
+            vol.Optional(CONF_LATITUDE): cv.latitude,
+            vol.Optional(CONF_LONGITUDE): cv.longitude,
+            vol.Optional(CONF_BINARY_SENSORS, default={}):
+                BINARY_SENSOR_SCHEMA,
+            vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA,
+            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 OpenUV component."""
-    from pyopenuv import Client
-    from pyopenuv.errors import OpenUvError
+    hass.data[DOMAIN] = {}
+    hass.data[DOMAIN][DATA_OPENUV_CLIENT] = {}
+    hass.data[DOMAIN][DATA_OPENUV_LISTENER] = {}
+
+    if DOMAIN not in config:
+        return True
 
     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)
     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:
         websession = aiohttp_client.async_get_clientsession(hass)
         openuv = OpenUV(
             Client(
-                api_key, latitude, longitude, websession, altitude=elevation),
-            conf[CONF_BINARY_SENSORS][CONF_MONITORED_CONDITIONS] +
-            conf[CONF_SENSORS][CONF_MONITORED_CONDITIONS])
+                config_entry.data[CONF_API_KEY],
+                config_entry.data.get(CONF_LATITUDE, hass.config.latitude),
+                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()
-        hass.data[DOMAIN] = openuv
+        hass.data[DOMAIN][DATA_OPENUV_CLIENT][config_entry.entry_id] = openuv
     except OpenUvError as err:
         _LOGGER.error('An error occurred: %s', str(err))
         hass.components.persistent_notification.create(
@@ -125,13 +166,9 @@ async def async_setup(hass, config):
             notification_id=NOTIFICATION_ID)
         return False
 
-    for component, schema in [
-            ('binary_sensor', conf[CONF_BINARY_SENSORS]),
-            ('sensor', conf[CONF_SENSORS]),
-    ]:
-        hass.async_create_task(
-            discovery.async_load_platform(
-                hass, component, DOMAIN, schema, config))
+    for component in ('binary_sensor', 'sensor'):
+        hass.async_create_task(hass.config_entries.async_forward_entry_setup(
+            config_entry, component))
 
     async def refresh_sensors(event_time):
         """Refresh OpenUV data."""
@@ -139,7 +176,25 @@ async def async_setup(hass, config):
         await openuv.async_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
 
@@ -147,19 +202,20 @@ async def async_setup(hass, config):
 class OpenUV:
     """Define a generic OpenUV object."""
 
-    def __init__(self, client, monitored_conditions):
+    def __init__(self, client, binary_sensor_conditions, sensor_conditions):
         """Initialize."""
-        self._monitored_conditions = monitored_conditions
+        self.binary_sensor_conditions = binary_sensor_conditions
         self.client = client
         self.data = {}
+        self.sensor_conditions = sensor_conditions
 
     async def async_update(self):
         """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()
             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()
             self.data[DATA_UV] = data
 
diff --git a/homeassistant/components/openuv/config_flow.py b/homeassistant/components/openuv/config_flow.py
new file mode 100644
index 0000000000000000000000000000000000000000..55ee566268e6e686135609049103a89273d51c9d
--- /dev/null
+++ b/homeassistant/components/openuv/config_flow.py
@@ -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,
+        )
diff --git a/homeassistant/components/openuv/const.py b/homeassistant/components/openuv/const.py
new file mode 100644
index 0000000000000000000000000000000000000000..1aa3d2abcaaf6e4bb15bb8b827add80d1da81adc
--- /dev/null
+++ b/homeassistant/components/openuv/const.py
@@ -0,0 +1,3 @@
+"""Define constants for the OpenUV component."""
+
+DOMAIN = 'openuv'
diff --git a/homeassistant/components/openuv/strings.json b/homeassistant/components/openuv/strings.json
new file mode 100644
index 0000000000000000000000000000000000000000..9c5af45619eef32c97cdecef6b601ccae2921a27
--- /dev/null
+++ b/homeassistant/components/openuv/strings.json
@@ -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"
+    }
+  }
+}
diff --git a/homeassistant/components/sensor/openuv.py b/homeassistant/components/sensor/openuv.py
index aaa04590b3fe6ebd96047e68e5619dcaffbde8f2..22712aa306b35dea34e005d20fe0051a9780bb41 100644
--- a/homeassistant/components/sensor/openuv.py
+++ b/homeassistant/components/sensor/openuv.py
@@ -6,13 +6,12 @@ https://home-assistant.io/components/sensor.openuv/
 """
 import logging
 
-from homeassistant.const import CONF_MONITORED_CONDITIONS
 from homeassistant.core import callback
 from homeassistant.helpers.dispatcher import async_dispatcher_connect
 from homeassistant.components.openuv import (
-    DATA_UV, DOMAIN, SENSORS, TOPIC_UPDATE, TYPE_CURRENT_OZONE_LEVEL,
-    TYPE_CURRENT_UV_INDEX, TYPE_CURRENT_UV_LEVEL, TYPE_MAX_UV_INDEX,
-    TYPE_SAFE_EXPOSURE_TIME_1, TYPE_SAFE_EXPOSURE_TIME_2,
+    DATA_OPENUV_CLIENT, DATA_UV, DOMAIN, SENSORS, TOPIC_UPDATE,
+    TYPE_CURRENT_OZONE_LEVEL, TYPE_CURRENT_UV_INDEX, TYPE_CURRENT_UV_LEVEL,
+    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_5, TYPE_SAFE_EXPOSURE_TIME_6, OpenUvEntity)
 from homeassistant.util.dt import as_local, parse_datetime
@@ -40,16 +39,20 @@ UV_LEVEL_LOW = "Low"
 
 async def async_setup_platform(
         hass, config, async_add_entities, discovery_info=None):
-    """Set up the OpenUV binary sensor platform."""
-    if discovery_info is None:
-        return
+    """Set up an OpenUV sensor based on existing config."""
+    pass
 
-    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 = []
-    for sensor_type in discovery_info[CONF_MONITORED_CONDITIONS]:
+    for sensor_type in openuv.sensor_conditions:
         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)
 
@@ -57,10 +60,12 @@ async def async_setup_platform(
 class OpenUvSensor(OpenUvEntity):
     """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."""
         super().__init__(openuv)
 
+        self._dispatch_remove = None
+        self._entry_id = entry_id
         self._icon = icon
         self._latitude = openuv.client.latitude
         self._longitude = openuv.client.longitude
@@ -102,7 +107,9 @@ class OpenUvSensor(OpenUvEntity):
 
     async def async_added_to_hass(self):
         """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):
         """Update the state."""
@@ -125,8 +132,7 @@ class OpenUvSensor(OpenUvEntity):
         elif self._sensor_type == TYPE_MAX_UV_INDEX:
             self._state = data['uv_max']
             self._attrs.update({
-                ATTR_MAX_UV_TIME: as_local(
-                    parse_datetime(data['uv_max_time']))
+                ATTR_MAX_UV_TIME: as_local(parse_datetime(data['uv_max_time']))
             })
         elif self._sensor_type in (TYPE_SAFE_EXPOSURE_TIME_1,
                                    TYPE_SAFE_EXPOSURE_TIME_2,
diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py
index 6eae9e13030f3991adad212ab44079aa2cc98f01..15932f2c3f8ec111665cde2a722d0ff4cc348ec5 100644
--- a/homeassistant/config_entries.py
+++ b/homeassistant/config_entries.py
@@ -141,6 +141,7 @@ FLOWS = [
     'homematicip_cloud',
     'hue',
     'nest',
+    'openuv',
     'sonos',
     'zone',
 ]
diff --git a/requirements_all.txt b/requirements_all.txt
index d8d73cc36a10258afcd02a225406eeab630ded72..b155612350b454ee2ab065015ade06f718f10fd6 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -990,7 +990,7 @@ pynut2==2.1.2
 pynx584==0.4
 
 # homeassistant.components.openuv
-pyopenuv==1.0.1
+pyopenuv==1.0.4
 
 # homeassistant.components.iota
 pyota==2.0.5
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 0ee02e0d109abb9a332555cbefc27cb30db3ffb4..b9e44445114899079d96060e1b4fd664ac396d15 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -154,6 +154,9 @@ pymonoprice==0.3
 # homeassistant.components.binary_sensor.nx584
 pynx584==0.4
 
+# homeassistant.components.openuv
+pyopenuv==1.0.4
+
 # homeassistant.auth.mfa_modules.totp
 # homeassistant.components.sensor.otp
 pyotp==2.2.6
diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py
index 4b694ec7ec071941815544752b2c16e268f35115..fc8e67b1ab6e16367bc449fe56902664bbc84df7 100755
--- a/script/gen_requirements_all.py
+++ b/script/gen_requirements_all.py
@@ -78,6 +78,7 @@ TEST_REQUIREMENTS = (
     'pylitejet',
     'pymonoprice',
     'pynx584',
+    'pyopenuv',
     'pyotp',
     'pyqwikswitch',
     'PyRMVtransport',
diff --git a/tests/components/openuv/__init__.py b/tests/components/openuv/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..0e3595b1e51819ddd87a2827138d84ee1bb69d12
--- /dev/null
+++ b/tests/components/openuv/__init__.py
@@ -0,0 +1 @@
+"""Define tests for the OpenUV component."""
diff --git a/tests/components/openuv/test_config_flow.py b/tests/components/openuv/test_config_flow.py
new file mode 100644
index 0000000000000000000000000000000000000000..0e50bddabdeab45115fb7608a533facbb1062cda
--- /dev/null
+++ b/tests/components/openuv/test_config_flow.py
@@ -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