From 2c7060896b8bf61ab3a6c3c1c0d7f4022aff92fd Mon Sep 17 00:00:00 2001
From: Aaron Bach <bachya1208@gmail.com>
Date: Mon, 28 Jan 2019 16:35:39 -0700
Subject: [PATCH] Make Ambient PWS async and cloud-push (#20332)

* Moving existing sensor file

* Initial functionality in place

* Added test for config flow

* Updated coverage and CODEOWNERS

* Linting

* Linting

* Member comments

* Hound

* Moving socket disconnect on HASS stop

* Member comments

* Removed unnecessary dispatcher call

* Config entry fix

* Added support in config flow for good accounts with no devices

* Hound

* Updated comment

* Member comments

* Stale docstrings

* Stale docstring
---
 .coveragerc                                   |   4 +-
 CODEOWNERS                                    |   1 +
 .../ambient_station/.translations/en.json     |  19 ++
 .../components/ambient_station/__init__.py    | 212 ++++++++++++++++++
 .../components/ambient_station/config_flow.py |  72 ++++++
 .../components/ambient_station/const.py       |  13 ++
 .../components/ambient_station/sensor.py      | 115 ++++++++++
 .../components/ambient_station/strings.json   |  19 ++
 .../components/sensor/ambient_station.py      | 212 ------------------
 homeassistant/config_entries.py               |   1 +
 requirements_all.txt                          |   6 +-
 requirements_test_all.txt                     |   3 +
 script/gen_requirements_all.py                |   1 +
 tests/components/ambient_station/__init__.py  |   1 +
 .../ambient_station/test_config_flow.py       | 130 +++++++++++
 tests/fixtures/ambient_devices.json           |  15 ++
 16 files changed, 608 insertions(+), 216 deletions(-)
 create mode 100644 homeassistant/components/ambient_station/.translations/en.json
 create mode 100644 homeassistant/components/ambient_station/__init__.py
 create mode 100644 homeassistant/components/ambient_station/config_flow.py
 create mode 100644 homeassistant/components/ambient_station/const.py
 create mode 100644 homeassistant/components/ambient_station/sensor.py
 create mode 100644 homeassistant/components/ambient_station/strings.json
 delete mode 100644 homeassistant/components/sensor/ambient_station.py
 create mode 100644 tests/components/ambient_station/__init__.py
 create mode 100644 tests/components/ambient_station/test_config_flow.py
 create mode 100644 tests/fixtures/ambient_devices.json

diff --git a/.coveragerc b/.coveragerc
index 2d4fb3f81a7..32bcda136ca 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -19,6 +19,9 @@ omit =
     homeassistant/components/alarmdecoder.py
     homeassistant/components/*/alarmdecoder.py
 
+    homeassistant/components/ambient_station/__init__.py
+    homeassistant/components/ambient_station/sensor.py
+
     homeassistant/components/amcrest.py
     homeassistant/components/*/amcrest.py
 
@@ -732,7 +735,6 @@ omit =
     homeassistant/components/sensor/aftership.py
     homeassistant/components/sensor/airvisual.py
     homeassistant/components/sensor/alpha_vantage.py
-    homeassistant/components/sensor/ambient_station.py
     homeassistant/components/sensor/arest.py
     homeassistant/components/sensor/arwn.py
     homeassistant/components/sensor/bbox.py
diff --git a/CODEOWNERS b/CODEOWNERS
index 4b4931ecc3a..2a2391186f4 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -153,6 +153,7 @@ homeassistant/components/weather/openweathermap.py @fabaff
 homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi
 
 # A
+homeassistant/components/ambient_station/* @bachya
 homeassistant/components/arduino.py @fabaff
 homeassistant/components/*/arduino.py @fabaff
 homeassistant/components/*/arest.py @fabaff
diff --git a/homeassistant/components/ambient_station/.translations/en.json b/homeassistant/components/ambient_station/.translations/en.json
new file mode 100644
index 00000000000..5bd643da55c
--- /dev/null
+++ b/homeassistant/components/ambient_station/.translations/en.json
@@ -0,0 +1,19 @@
+{
+    "config": {
+        "error": {
+            "identifier_exists": "Application Key and/or API Key already registered",
+            "invalid_key": "Invalid API Key and/or Application Key",
+            "no_devices": "No devices found in account"
+        },
+        "step": {
+            "user": {
+                "data": {
+                    "api_key": "API Key",
+                    "app_key": "Application Key"
+                },
+                "title": "Fill in your information"
+            }
+        },
+        "title": "Ambient PWS"
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py
new file mode 100644
index 00000000000..788927a2700
--- /dev/null
+++ b/homeassistant/components/ambient_station/__init__.py
@@ -0,0 +1,212 @@
+"""
+Support for Ambient Weather Station Service.
+
+For more details about this component, please refer to the documentation at
+https://home-assistant.io/components/ambient_station/
+"""
+import logging
+
+import voluptuous as vol
+
+from homeassistant.config_entries import SOURCE_IMPORT
+from homeassistant.const import (
+    ATTR_NAME, ATTR_LOCATION, CONF_API_KEY, CONF_MONITORED_CONDITIONS,
+    CONF_UNIT_SYSTEM, EVENT_HOMEASSISTANT_STOP)
+from homeassistant.exceptions import ConfigEntryNotReady
+from homeassistant.helpers import aiohttp_client, config_validation as cv
+from homeassistant.helpers.dispatcher import async_dispatcher_send
+from homeassistant.helpers.event import async_call_later
+
+from .config_flow import configured_instances
+from .const import (
+    ATTR_LAST_DATA, CONF_APP_KEY, DATA_CLIENT, DOMAIN, TOPIC_UPDATE, UNITS_SI,
+    UNITS_US)
+
+REQUIREMENTS = ['aioambient==0.1.0']
+_LOGGER = logging.getLogger(__name__)
+
+DEFAULT_SOCKET_MIN_RETRY = 15
+
+SENSOR_TYPES = {
+    '24hourrainin': ['24 Hr Rain', 'in'],
+    'baromabsin': ['Abs Pressure', 'inHg'],
+    'baromrelin': ['Rel Pressure', 'inHg'],
+    'battout': ['Battery', ''],
+    'co2': ['co2', 'ppm'],
+    'dailyrainin': ['Daily Rain', 'in'],
+    'dewPoint': ['Dew Point', ['°F', '°C']],
+    'eventrainin': ['Event Rain', 'in'],
+    'feelsLike': ['Feels Like', ['°F', '°C']],
+    'hourlyrainin': ['Hourly Rain Rate', 'in/hr'],
+    'humidity': ['Humidity', '%'],
+    'humidityin': ['Humidity In', '%'],
+    'lastRain': ['Last Rain', ''],
+    'maxdailygust': ['Max Gust', 'mph'],
+    'monthlyrainin': ['Monthly Rain', 'in'],
+    'solarradiation': ['Solar Rad', 'W/m^2'],
+    'tempf': ['Temp', ['°F', '°C']],
+    'tempinf': ['Inside Temp', ['°F', '°C']],
+    'totalrainin': ['Lifetime Rain', 'in'],
+    'uv': ['uv', 'Index'],
+    'weeklyrainin': ['Weekly Rain', 'in'],
+    'winddir': ['Wind Dir', '°'],
+    'winddir_avg10m': ['Wind Dir Avg 10m', '°'],
+    'winddir_avg2m': ['Wind Dir Avg 2m', 'mph'],
+    'windgustdir': ['Gust Dir', '°'],
+    'windgustmph': ['Wind Gust', 'mph'],
+    'windspdmph_avg10m': ['Wind Avg 10m', 'mph'],
+    'windspdmph_avg2m': ['Wind Avg 2m', 'mph'],
+    'windspeedmph': ['Wind Speed', 'mph'],
+    'yearlyrainin': ['Yearly Rain', 'in'],
+}
+
+CONFIG_SCHEMA = vol.Schema({
+    DOMAIN:
+        vol.Schema({
+            vol.Required(CONF_APP_KEY):
+                cv.string,
+            vol.Required(CONF_API_KEY):
+                cv.string,
+            vol.Optional(
+                CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)):
+                vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
+            vol.Optional(CONF_UNIT_SYSTEM):
+                vol.In([UNITS_SI, UNITS_US]),
+        })
+}, extra=vol.ALLOW_EXTRA)
+
+
+async def async_setup(hass, config):
+    """Set up the Ambient PWS component."""
+    hass.data[DOMAIN] = {}
+    hass.data[DOMAIN][DATA_CLIENT] = {}
+
+    if DOMAIN not in config:
+        return True
+
+    conf = config[DOMAIN]
+
+    if conf[CONF_APP_KEY] in configured_instances(hass):
+        return True
+
+    hass.async_create_task(
+        hass.config_entries.flow.async_init(
+            DOMAIN, context={'source': SOURCE_IMPORT}, data=conf))
+
+    return True
+
+
+async def async_setup_entry(hass, config_entry):
+    """Set up the Ambient PWS as config entry."""
+    from aioambient import Client
+    from aioambient.errors import WebsocketConnectionError
+
+    session = aiohttp_client.async_get_clientsession(hass)
+
+    try:
+        ambient = AmbientStation(
+            hass,
+            config_entry,
+            Client(
+                config_entry.data[CONF_API_KEY],
+                config_entry.data[CONF_APP_KEY], session),
+            config_entry.data.get(
+                CONF_MONITORED_CONDITIONS, list(SENSOR_TYPES)),
+            config_entry.data.get(CONF_UNIT_SYSTEM))
+        hass.loop.create_task(ambient.ws_connect())
+        hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = ambient
+    except WebsocketConnectionError as err:
+        _LOGGER.error('Config entry failed: %s', err)
+        raise ConfigEntryNotReady
+
+    hass.bus.async_listen_once(
+        EVENT_HOMEASSISTANT_STOP, ambient.client.websocket.disconnect())
+
+    return True
+
+
+async def async_unload_entry(hass, config_entry):
+    """Unload an Ambient PWS config entry."""
+    ambient = hass.data[DOMAIN][DATA_CLIENT].pop(config_entry.entry_id)
+    hass.async_create_task(ambient.ws_disconnect())
+
+    await hass.config_entries.async_forward_entry_unload(
+        config_entry, 'sensor')
+
+    return True
+
+
+class AmbientStation:
+    """Define a class to handle the Ambient websocket."""
+
+    def __init__(
+            self, hass, config_entry, client, monitored_conditions,
+            unit_system):
+        """Initialize."""
+        self._config_entry = config_entry
+        self._hass = hass
+        self._ws_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY
+        self.client = client
+        self.monitored_conditions = monitored_conditions
+        self.stations = {}
+        self.unit_system = unit_system
+
+    async def ws_connect(self):
+        """Register handlers and connect to the websocket."""
+        from aioambient.errors import WebsocketError
+
+        def on_connect():
+            """Define a handler to fire when the websocket is connected."""
+            _LOGGER.info('Connected to websocket')
+
+        def on_data(data):
+            """Define a handler to fire when the data is received."""
+            mac_address = data['macAddress']
+            if data != self.stations[mac_address][ATTR_LAST_DATA]:
+                _LOGGER.debug('New data received: %s', data)
+                self.stations[mac_address][ATTR_LAST_DATA] = data
+                async_dispatcher_send(self._hass, TOPIC_UPDATE)
+
+        def on_disconnect():
+            """Define a handler to fire when the websocket is disconnected."""
+            _LOGGER.info('Disconnected from websocket')
+
+        def on_subscribed(data):
+            """Define a handler to fire when the subscription is set."""
+            for station in data['devices']:
+                if station['macAddress'] in self.stations:
+                    continue
+
+                _LOGGER.debug('New station subscription: %s', data)
+
+                self.stations[station['macAddress']] = {
+                    ATTR_LAST_DATA: station['lastData'],
+                    ATTR_LOCATION: station['info']['location'],
+                    ATTR_NAME: station['info']['name'],
+                }
+
+                self._hass.async_create_task(
+                    self._hass.config_entries.async_forward_entry_setup(
+                        self._config_entry, 'sensor'))
+
+                self._ws_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY
+
+        self.client.websocket.on_connect(on_connect)
+        self.client.websocket.on_data(on_data)
+        self.client.websocket.on_disconnect(on_disconnect)
+        self.client.websocket.on_subscribed(on_subscribed)
+
+        try:
+            await self.client.websocket.connect()
+        except WebsocketError as err:
+            _LOGGER.error("Error with the websocket connection: %s", err)
+
+            self._ws_reconnect_delay = min(
+                2 * self._ws_reconnect_delay, 480)
+
+            async_call_later(
+                self._hass, self._ws_reconnect_delay, self.ws_connect)
+
+    async def ws_disconnect(self):
+        """Disconnect from the websocket."""
+        await self.client.websocket.disconnect()
diff --git a/homeassistant/components/ambient_station/config_flow.py b/homeassistant/components/ambient_station/config_flow.py
new file mode 100644
index 00000000000..56e747ce5e0
--- /dev/null
+++ b/homeassistant/components/ambient_station/config_flow.py
@@ -0,0 +1,72 @@
+"""Config flow to configure the Ambient PWS component."""
+
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.const import CONF_API_KEY
+from homeassistant.core import callback
+from homeassistant.helpers import aiohttp_client
+
+from .const import CONF_APP_KEY, DOMAIN
+
+
+@callback
+def configured_instances(hass):
+    """Return a set of configured Ambient PWS instances."""
+    return set(
+        entry.data[CONF_APP_KEY]
+        for entry in hass.config_entries.async_entries(DOMAIN))
+
+
+@config_entries.HANDLERS.register(DOMAIN)
+class AmbientStationFlowHandler(config_entries.ConfigFlow):
+    """Handle an Ambient PWS config flow."""
+
+    VERSION = 1
+    CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH
+
+    async def _show_form(self, errors=None):
+        """Show the form to the user."""
+        data_schema = vol.Schema({
+            vol.Required(CONF_API_KEY): str,
+            vol.Required(CONF_APP_KEY): str,
+        })
+
+        return self.async_show_form(
+            step_id='user',
+            data_schema=data_schema,
+            errors=errors if errors else {},
+        )
+
+    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 aioambient import Client
+        from aioambient.errors import AmbientError
+
+        if not user_input:
+            return await self._show_form()
+
+        if user_input[CONF_APP_KEY] in configured_instances(self.hass):
+            return await self._show_form({CONF_APP_KEY: 'identifier_exists'})
+
+        session = aiohttp_client.async_get_clientsession(self.hass)
+        client = Client(
+            user_input[CONF_API_KEY], user_input[CONF_APP_KEY], session)
+
+        try:
+            devices = await client.api.get_devices()
+        except AmbientError:
+            return await self._show_form({'base': 'invalid_key'})
+
+        if not devices:
+            return await self._show_form({'base': 'no_devices'})
+
+        # The Application Key (which identifies each config entry) is too long
+        # to show nicely in the UI, so we take the first 12 characters (similar
+        # to how GitHub does it):
+        return self.async_create_entry(
+            title=user_input[CONF_APP_KEY][:12], data=user_input)
diff --git a/homeassistant/components/ambient_station/const.py b/homeassistant/components/ambient_station/const.py
new file mode 100644
index 00000000000..df2c5462e66
--- /dev/null
+++ b/homeassistant/components/ambient_station/const.py
@@ -0,0 +1,13 @@
+"""Define constants for the Ambient PWS component."""
+DOMAIN = 'ambient_station'
+
+ATTR_LAST_DATA = 'last_data'
+
+CONF_APP_KEY = 'app_key'
+
+DATA_CLIENT = 'data_client'
+
+TOPIC_UPDATE = 'update'
+
+UNITS_SI = 'si'
+UNITS_US = 'us'
diff --git a/homeassistant/components/ambient_station/sensor.py b/homeassistant/components/ambient_station/sensor.py
new file mode 100644
index 00000000000..d2d89233472
--- /dev/null
+++ b/homeassistant/components/ambient_station/sensor.py
@@ -0,0 +1,115 @@
+"""
+Support for Ambient Weather Station Service.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/sensor.ambient_station/
+"""
+import logging
+
+from homeassistant.components.ambient_station import SENSOR_TYPES
+from homeassistant.helpers.entity import Entity
+from homeassistant.const import ATTR_NAME
+from homeassistant.core import callback
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+
+from .const import (
+    ATTR_LAST_DATA, DATA_CLIENT, DOMAIN, TOPIC_UPDATE, UNITS_SI, UNITS_US)
+
+DEPENDENCIES = ['ambient_station']
+_LOGGER = logging.getLogger(__name__)
+
+UNIT_SYSTEM = {UNITS_US: 0, UNITS_SI: 1}
+
+
+async def async_setup_platform(
+        hass, config, async_add_entities, discovery_info=None):
+    """Set up an Ambient PWS sensor based on existing config."""
+    pass
+
+
+async def async_setup_entry(hass, entry, async_add_entities):
+    """Set up an Ambient PWS sensor based on a config entry."""
+    ambient = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id]
+
+    if ambient.unit_system:
+        sys_units = ambient.unit_system
+    elif hass.config.units.is_metric:
+        sys_units = UNITS_SI
+    else:
+        sys_units = UNITS_US
+
+    sensor_list = []
+    for mac_address, station in ambient.stations.items():
+        for condition in ambient.monitored_conditions:
+            name, unit = SENSOR_TYPES[condition]
+            if isinstance(unit, list):
+                unit = unit[UNIT_SYSTEM[sys_units]]
+
+            sensor_list.append(
+                AmbientWeatherSensor(
+                    ambient, mac_address, station[ATTR_NAME], condition, name,
+                    unit))
+
+    async_add_entities(sensor_list, True)
+
+
+class AmbientWeatherSensor(Entity):
+    """Define an Ambient sensor."""
+
+    def __init__(
+            self, ambient, mac_address, station_name, sensor_type, sensor_name,
+            units):
+        """Initialize the sensor."""
+        self._ambient = ambient
+        self._async_unsub_dispatcher_connect = None
+        self._mac_address = mac_address
+        self._sensor_name = sensor_name
+        self._sensor_type = sensor_type
+        self._state = None
+        self._station_name = station_name
+        self._units = units
+
+    @property
+    def name(self):
+        """Return the name of the sensor."""
+        return '{0}_{1}'.format(self._station_name, self._sensor_name)
+
+    @property
+    def should_poll(self):
+        """Disable polling."""
+        return False
+
+    @property
+    def state(self):
+        """Return the state of the sensor."""
+        return self._state
+
+    @property
+    def unit_of_measurement(self):
+        """Return the unit of measurement."""
+        return self._units
+
+    @property
+    def unique_id(self):
+        """Return a unique, unchanging string that represents this sensor."""
+        return '{0}_{1}'.format(self._mac_address, self._sensor_name)
+
+    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):
+        """Fetch new state data for the sensor."""
+        self._state = self._ambient.stations[
+            self._mac_address][ATTR_LAST_DATA].get(self._sensor_type)
diff --git a/homeassistant/components/ambient_station/strings.json b/homeassistant/components/ambient_station/strings.json
new file mode 100644
index 00000000000..657b3477bb2
--- /dev/null
+++ b/homeassistant/components/ambient_station/strings.json
@@ -0,0 +1,19 @@
+{
+  "config": {
+    "title": "Ambient PWS",
+    "step": {
+      "user": {
+        "title": "Fill in your information",
+        "data": {
+          "api_key": "API Key",
+          "app_key": "Application Key"
+        }
+      }
+    },
+    "error": {
+      "identifier_exists": "Application Key and/or API Key already registered",
+      "invalid_key": "Invalid API Key and/or Application Key",
+      "no_devices": "No devices found in account"
+    }
+  }
+}
diff --git a/homeassistant/components/sensor/ambient_station.py b/homeassistant/components/sensor/ambient_station.py
deleted file mode 100644
index bc44f83d764..00000000000
--- a/homeassistant/components/sensor/ambient_station.py
+++ /dev/null
@@ -1,212 +0,0 @@
-"""
-Support for Ambient Weather Station Service.
-
-For more details about this platform, please refer to the documentation at
-https://home-assistant.io/components/sensor.ambient_station/
-"""
-
-import asyncio
-from datetime import timedelta
-import logging
-
-import voluptuous as vol
-
-from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import CONF_API_KEY, CONF_MONITORED_CONDITIONS
-import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
-from homeassistant.util import Throttle
-
-REQUIREMENTS = ['ambient_api==1.5.2']
-
-CONF_APP_KEY = 'app_key'
-
-SENSOR_NAME = 0
-SENSOR_UNITS = 1
-
-CONF_UNITS = 'units'
-UNITS_US = 'us'
-UNITS_SI = 'si'
-UNIT_SYSTEM = {UNITS_US: 0, UNITS_SI: 1}
-
-SCAN_INTERVAL = timedelta(seconds=300)
-
-SENSOR_TYPES = {
-    'winddir': ['Wind Dir', '°'],
-    'windspeedmph': ['Wind Speed', 'mph'],
-    'windgustmph': ['Wind Gust', 'mph'],
-    'maxdailygust': ['Max Gust', 'mph'],
-    'windgustdir': ['Gust Dir', '°'],
-    'windspdmph_avg2m': ['Wind Avg 2m', 'mph'],
-    'winddir_avg2m': ['Wind Dir Avg 2m', 'mph'],
-    'windspdmph_avg10m': ['Wind Avg 10m', 'mph'],
-    'winddir_avg10m': ['Wind Dir Avg 10m', '°'],
-    'humidity': ['Humidity', '%'],
-    'humidityin': ['Humidity In', '%'],
-    'tempf': ['Temp', ['°F', '°C']],
-    'tempinf': ['Inside Temp', ['°F', '°C']],
-    'battout': ['Battery', ''],
-    'hourlyrainin': ['Hourly Rain Rate', 'in/hr'],
-    'dailyrainin': ['Daily Rain', 'in'],
-    '24hourrainin': ['24 Hr Rain', 'in'],
-    'weeklyrainin': ['Weekly Rain', 'in'],
-    'monthlyrainin': ['Monthly Rain', 'in'],
-    'yearlyrainin': ['Yearly Rain', 'in'],
-    'eventrainin': ['Event Rain', 'in'],
-    'totalrainin': ['Lifetime Rain', 'in'],
-    'baromrelin': ['Rel Pressure', 'inHg'],
-    'baromabsin': ['Abs Pressure', 'inHg'],
-    'uv': ['uv', 'Index'],
-    'solarradiation': ['Solar Rad', 'W/m^2'],
-    'co2': ['co2', 'ppm'],
-    'lastRain': ['Last Rain', ''],
-    'dewPoint': ['Dew Point', ['°F', '°C']],
-    'feelsLike': ['Feels Like', ['°F', '°C']],
-}
-
-_LOGGER = logging.getLogger(__name__)
-
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
-    vol.Required(CONF_API_KEY): cv.string,
-    vol.Required(CONF_APP_KEY): cv.string,
-    vol.Required(CONF_MONITORED_CONDITIONS):
-        vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
-    vol.Optional(CONF_UNITS): vol.In([UNITS_SI, UNITS_US]),
-})
-
-
-def setup_platform(hass, config, add_entities, discovery_info=None):
-    """Initialze each sensor platform for each monitored condition."""
-    api_key = config[CONF_API_KEY]
-    app_key = config[CONF_APP_KEY]
-    station_data = AmbientStationData(hass, api_key, app_key)
-    if not station_data.connect_success:
-        _LOGGER.error("Could not connect to weather station API")
-        return
-
-    sensor_list = []
-
-    if CONF_UNITS in config:
-        sys_units = config[CONF_UNITS]
-    elif hass.config.units.is_metric:
-        sys_units = UNITS_SI
-    else:
-        sys_units = UNITS_US
-
-    for condition in config[CONF_MONITORED_CONDITIONS]:
-        # create a sensor object for each monitored condition
-        sensor_params = SENSOR_TYPES[condition]
-        name = sensor_params[SENSOR_NAME]
-        units = sensor_params[SENSOR_UNITS]
-        if isinstance(units, list):
-            units = sensor_params[SENSOR_UNITS][UNIT_SYSTEM[sys_units]]
-
-        sensor_list.append(AmbientWeatherSensor(station_data, condition,
-                                                name, units))
-
-    add_entities(sensor_list)
-
-
-class AmbientWeatherSensor(Entity):
-    """Representation of a Sensor."""
-
-    def __init__(self, station_data, condition, name, units):
-        """Initialize the sensor."""
-        self._state = None
-        self.station_data = station_data
-        self._condition = condition
-        self._name = name
-        self._units = units
-
-    @property
-    def name(self):
-        """Return the name of the sensor."""
-        return self._name
-
-    @property
-    def state(self):
-        """Return the state of the sensor."""
-        return self._state
-
-    @property
-    def unit_of_measurement(self):
-        """Return the unit of measurement."""
-        return self._units
-
-    async def async_update(self):
-        """Fetch new state data for the sensor.
-
-        This is the only method that should fetch new data for Home Assistant.
-        """
-        _LOGGER.debug("Getting data for sensor: %s", self._name)
-        data = await self.station_data.get_data()
-        if data is None:
-            # update likely got throttled and returned None, so use the cached
-            # data from the station_data object
-            self._state = self.station_data.data[self._condition]
-        else:
-            if self._condition in data:
-                self._state = data[self._condition]
-            else:
-                _LOGGER.warning("%s sensor data not available from the "
-                                "station", self._condition)
-
-        _LOGGER.debug("Sensor: %s | Data: %s", self._name, self._state)
-
-
-class AmbientStationData:
-    """Class to interface with ambient-api library."""
-
-    def __init__(self, hass, api_key, app_key):
-        """Initialize station data object."""
-        self.hass = hass
-        self._api_keys = {
-            'AMBIENT_ENDPOINT':
-            'https://api.ambientweather.net/v1',
-            'AMBIENT_API_KEY': api_key,
-            'AMBIENT_APPLICATION_KEY': app_key,
-            'log_level': 'DEBUG'
-        }
-
-        self.data = None
-        self._station = None
-        self._api = None
-        self._devices = None
-        self.connect_success = False
-
-        self.get_data = Throttle(SCAN_INTERVAL)(self.async_update)
-        self._connect_api()     # attempt to connect to API
-
-    async def async_update(self):
-        """Get new data."""
-        # refresh API connection since servers turn over nightly
-        _LOGGER.debug("Getting new data from server")
-        new_data = None
-        await self.hass.async_add_executor_job(self._connect_api)
-        await asyncio.sleep(2)   # need minimum 2 seconds between API calls
-        if self._station is not None:
-            data = await self.hass.async_add_executor_job(
-                self._station.get_data)
-            if data is not None:
-                new_data = data[0]
-                self.data = new_data
-            else:
-                _LOGGER.debug("data is None type")
-        else:
-            _LOGGER.debug("Station is None type")
-
-        return new_data
-
-    def _connect_api(self):
-        """Connect to the API and capture new data."""
-        from ambient_api.ambientapi import AmbientAPI
-
-        self._api = AmbientAPI(**self._api_keys)
-        self._devices = self._api.get_devices()
-
-        if self._devices:
-            self._station = self._devices[0]
-            if self._station is not None:
-                self.connect_success = True
-        else:
-            _LOGGER.debug("No station devices available")
diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py
index 00b5d797682..159f5651c31 100644
--- a/homeassistant/config_entries.py
+++ b/homeassistant/config_entries.py
@@ -135,6 +135,7 @@ SOURCE_IMPORT = 'import'
 HANDLERS = Registry()
 # Components that have config flows. In future we will auto-generate this list.
 FLOWS = [
+    'ambient_station',
     'cast',
     'daikin',
     'deconz',
diff --git a/requirements_all.txt b/requirements_all.txt
index 58c9e81d272..ca4459f66bc 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -86,6 +86,9 @@ abodepy==0.15.0
 # homeassistant.components.media_player.frontier_silicon
 afsapi==0.0.4
 
+# homeassistant.components.ambient_station
+aioambient==0.1.0
+
 # homeassistant.components.asuswrt
 aioasuswrt==1.1.18
 
@@ -141,9 +144,6 @@ alarmdecoder==1.13.2
 # homeassistant.components.sensor.alpha_vantage
 alpha_vantage==2.1.0
 
-# homeassistant.components.sensor.ambient_station
-ambient_api==1.5.2
-
 # homeassistant.components.amcrest
 amcrest==1.2.3
 
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 6f16780d4c2..2dbd2760c7d 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -30,6 +30,9 @@ PyTransportNSW==0.1.1
 # homeassistant.components.notify.yessssms
 YesssSMS==0.2.3
 
+# homeassistant.components.ambient_station
+aioambient==0.1.0
+
 # homeassistant.components.device_tracker.automatic
 aioautomatic==0.6.5
 
diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py
index 8817ee61e8f..79ba3f8c342 100755
--- a/script/gen_requirements_all.py
+++ b/script/gen_requirements_all.py
@@ -36,6 +36,7 @@ COMMENT_REQUIREMENTS = (
 )
 
 TEST_REQUIREMENTS = (
+    'aioambient',
     'aioautomatic',
     'aiohttp_cors',
     'aiohue',
diff --git a/tests/components/ambient_station/__init__.py b/tests/components/ambient_station/__init__.py
new file mode 100644
index 00000000000..1de98ab57bb
--- /dev/null
+++ b/tests/components/ambient_station/__init__.py
@@ -0,0 +1 @@
+"""Define tests for the Ambient PWS component."""
diff --git a/tests/components/ambient_station/test_config_flow.py b/tests/components/ambient_station/test_config_flow.py
new file mode 100644
index 00000000000..a988208e4a0
--- /dev/null
+++ b/tests/components/ambient_station/test_config_flow.py
@@ -0,0 +1,130 @@
+"""Define tests for the Ambient PWS config flow."""
+import json
+
+import aioambient
+import pytest
+
+from homeassistant import data_entry_flow
+from homeassistant.components.ambient_station import (
+    CONF_APP_KEY, DOMAIN, config_flow)
+from homeassistant.const import CONF_API_KEY
+
+from tests.common import (
+    load_fixture, MockConfigEntry, MockDependency, mock_coro)
+
+
+@pytest.fixture
+def get_devices_response():
+    """Define a fixture for a successful /devices response."""
+    return mock_coro()
+
+
+@pytest.fixture
+def mock_aioambient(get_devices_response):
+    """Mock the aioambient library."""
+    with MockDependency('aioambient') as mock_aioambient_:
+        mock_aioambient_.Client(
+        ).api.get_devices.return_value = get_devices_response
+        yield mock_aioambient_
+
+
+async def test_duplicate_error(hass):
+    """Test that errors are shown when duplicates are added."""
+    conf = {
+        CONF_API_KEY: '12345abcde12345abcde',
+        CONF_APP_KEY: '67890fghij67890fghij',
+    }
+
+    MockConfigEntry(domain=DOMAIN, data=conf).add_to_hass(hass)
+    flow = config_flow.AmbientStationFlowHandler()
+    flow.hass = hass
+
+    result = await flow.async_step_user(user_input=conf)
+    assert result['errors'] == {CONF_APP_KEY: 'identifier_exists'}
+
+
+@pytest.mark.parametrize(
+    'get_devices_response',
+    [mock_coro(exception=aioambient.errors.AmbientError)])
+async def test_invalid_api_key(hass, mock_aioambient):
+    """Test that an invalid API/App Key throws an error."""
+    conf = {
+        CONF_API_KEY: '12345abcde12345abcde',
+        CONF_APP_KEY: '67890fghij67890fghij',
+    }
+
+    flow = config_flow.AmbientStationFlowHandler()
+    flow.hass = hass
+
+    result = await flow.async_step_user(user_input=conf)
+    assert result['errors'] == {'base': 'invalid_key'}
+
+
+@pytest.mark.parametrize('get_devices_response', [mock_coro(return_value=[])])
+async def test_no_devices(hass, mock_aioambient):
+    """Test that an account with no associated devices throws an error."""
+    conf = {
+        CONF_API_KEY: '12345abcde12345abcde',
+        CONF_APP_KEY: '67890fghij67890fghij',
+    }
+
+    flow = config_flow.AmbientStationFlowHandler()
+    flow.hass = hass
+
+    result = await flow.async_step_user(user_input=conf)
+    assert result['errors'] == {'base': 'no_devices'}
+
+
+async def test_show_form(hass):
+    """Test that the form is served with no input."""
+    flow = config_flow.AmbientStationFlowHandler()
+    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'
+
+
+@pytest.mark.parametrize(
+    'get_devices_response',
+    [mock_coro(return_value=json.loads(load_fixture('ambient_devices.json')))])
+async def test_step_import(hass, mock_aioambient):
+    """Test that the import step works."""
+    conf = {
+        CONF_API_KEY: '12345abcde12345abcde',
+        CONF_APP_KEY: '67890fghij67890fghij',
+    }
+
+    flow = config_flow.AmbientStationFlowHandler()
+    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'] == '67890fghij67'
+    assert result['data'] == {
+        CONF_API_KEY: '12345abcde12345abcde',
+        CONF_APP_KEY: '67890fghij67890fghij',
+    }
+
+
+@pytest.mark.parametrize(
+    'get_devices_response',
+    [mock_coro(return_value=json.loads(load_fixture('ambient_devices.json')))])
+async def test_step_user(hass, mock_aioambient):
+    """Test that the user step works."""
+    conf = {
+        CONF_API_KEY: '12345abcde12345abcde',
+        CONF_APP_KEY: '67890fghij67890fghij',
+    }
+
+    flow = config_flow.AmbientStationFlowHandler()
+    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'] == '67890fghij67'
+    assert result['data'] == {
+        CONF_API_KEY: '12345abcde12345abcde',
+        CONF_APP_KEY: '67890fghij67890fghij',
+    }
diff --git a/tests/fixtures/ambient_devices.json b/tests/fixtures/ambient_devices.json
new file mode 100644
index 00000000000..cd5edc21cb0
--- /dev/null
+++ b/tests/fixtures/ambient_devices.json
@@ -0,0 +1,15 @@
+[{
+  "macAddress": "12:34:56:78:90:AB",
+  "lastData": {
+    "dateutc": 1546889640000,
+    "baromrelin": 30.09,
+    "baromabsin": 24.61,
+    "tempinf": 68.9,
+    "humidityin": 30,
+    "date": "2019-01-07T19:34:00.000Z"
+  },
+  "info": {
+    "name": "Home",
+    "location": "Home"
+  }
+}]
-- 
GitLab