From c2e843cbc3eabf77acc0116f2054b92d8b7819a1 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 9 Jul 2019 02:29:06 -0600 Subject: [PATCH] Add support for Notion Home Monitoring (#24634) * Add support for Notion Home Monitoring * Updated coverage * Removed auto-generated translations * Stale docstrings * Corrected hardware version * Fixed binary sensor representation * Cleanup and update protection * Updated log message * Cleaned up is_on * Updated docstring * Modified which data is updated during async_update * Added more checks during update * More cleanup * Fixed unhandled exception * Owner-requested changes (round 1) * Fixed incorrect scan interval retrieval * Ugh * Removed unnecessary import * Simplified everything via dict lookups * Ensure bridges are properly registered * Fixed tests * Added catch for invalid credentials * Ensure bridge ID is updated as necessary * Updated method name * Simplified bridge update * Add support for updating bridge via_device_id * Device update guard clause * Removed excess whitespace * Whitespace * Owner comments * Member comments --- .coveragerc | 2 + CODEOWNERS | 1 + .../components/notion/.translations/en.json | 19 ++ homeassistant/components/notion/__init__.py | 307 ++++++++++++++++++ .../components/notion/binary_sensor.py | 68 ++++ .../components/notion/config_flow.py | 64 ++++ homeassistant/components/notion/const.py | 13 + homeassistant/components/notion/manifest.json | 13 + homeassistant/components/notion/sensor.py | 81 +++++ homeassistant/components/notion/strings.json | 19 ++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/notion/__init__.py | 1 + tests/components/notion/test_config_flow.py | 104 ++++++ 16 files changed, 700 insertions(+) create mode 100644 homeassistant/components/notion/.translations/en.json create mode 100644 homeassistant/components/notion/__init__.py create mode 100644 homeassistant/components/notion/binary_sensor.py create mode 100644 homeassistant/components/notion/config_flow.py create mode 100644 homeassistant/components/notion/const.py create mode 100644 homeassistant/components/notion/manifest.json create mode 100644 homeassistant/components/notion/sensor.py create mode 100644 homeassistant/components/notion/strings.json create mode 100644 tests/components/notion/__init__.py create mode 100644 tests/components/notion/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index b4290158d740c..592ac42c3decb 100644 --- a/.coveragerc +++ b/.coveragerc @@ -409,6 +409,8 @@ omit = homeassistant/components/nissan_leaf/* homeassistant/components/nmap_tracker/device_tracker.py homeassistant/components/nmbs/sensor.py + homeassistant/components/notion/binary_sensor.py + homeassistant/components/notion/sensor.py homeassistant/components/noaa_tides/sensor.py homeassistant/components/norway_air/air_quality.py homeassistant/components/nsw_fuel_station/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 63d3915d70dc9..62696d909d07f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -183,6 +183,7 @@ homeassistant/components/nissan_leaf/* @filcole homeassistant/components/nmbs/* @thibmaek homeassistant/components/no_ip/* @fabaff homeassistant/components/notify/* @home-assistant/core +homeassistant/components/notion/* @bachya homeassistant/components/nsw_fuel_station/* @nickw444 homeassistant/components/nuki/* @pschmitt homeassistant/components/ohmconnect/* @robbiet480 diff --git a/homeassistant/components/notion/.translations/en.json b/homeassistant/components/notion/.translations/en.json new file mode 100644 index 0000000000000..b05f613a73ffa --- /dev/null +++ b/homeassistant/components/notion/.translations/en.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Username already registered", + "invalid_credentials": "Invalid username or password", + "no_devices": "No devices found in account" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Username/Email Address" + }, + "title": "Fill in your information" + } + }, + "title": "Notion" + } +} \ No newline at end of file diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py new file mode 100644 index 0000000000000..afa08def4dfe3 --- /dev/null +++ b/homeassistant/components/notion/__init__.py @@ -0,0 +1,307 @@ +"""Support for Notion.""" +import asyncio +import logging + +from aionotion import async_get_client +from aionotion.errors import InvalidCredentialsError, NotionError +import voluptuous as vol + +from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.const import ATTR_ATTRIBUTION, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import callback +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import ( + aiohttp_client, config_validation as cv, device_registry as dr) +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, async_dispatcher_send) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import async_track_time_interval + +from .config_flow import configured_instances +from .const import ( + DATA_CLIENT, DEFAULT_SCAN_INTERVAL, DOMAIN, TOPIC_DATA_UPDATE) + +_LOGGER = logging.getLogger(__name__) + +ATTR_SYSTEM_MODE = 'system_mode' +ATTR_SYSTEM_NAME = 'system_name' + +DATA_LISTENER = 'listener' + +DEFAULT_ATTRIBUTION = 'Data provided by Notion' + +SENSOR_BATTERY = 'low_battery' +SENSOR_DOOR = 'door' +SENSOR_GARAGE_DOOR = 'garage_door' +SENSOR_LEAK = 'leak' +SENSOR_MISSING = 'missing' +SENSOR_SAFE = 'safe' +SENSOR_SLIDING = 'sliding' +SENSOR_SMOKE_CO = 'alarm' +SENSOR_TEMPERATURE = 'temperature' +SENSOR_WINDOW_HINGED_HORIZONTAL = 'window_hinged_horizontal' +SENSOR_WINDOW_HINGED_VERTICAL = 'window_hinged_vertical' + +BINARY_SENSOR_TYPES = { + SENSOR_BATTERY: ('Low Battery', 'battery'), + SENSOR_DOOR: ('Door', 'door'), + SENSOR_GARAGE_DOOR: ('Garage Door', 'garage_door'), + SENSOR_LEAK: ('Leak Detector', 'moisture'), + SENSOR_MISSING: ('Missing', 'connectivity'), + SENSOR_SAFE: ('Safe', 'door'), + SENSOR_SLIDING: ('Sliding Door/Window', 'door'), + SENSOR_SMOKE_CO: ('Smoke/Carbon Monoxide Detector', 'smoke'), + SENSOR_WINDOW_HINGED_HORIZONTAL: ('Hinged Window', 'window'), + SENSOR_WINDOW_HINGED_VERTICAL: ('Hinged Window', 'window'), +} +SENSOR_TYPES = { + SENSOR_TEMPERATURE: ('Temperature', 'temperature', '°C'), +} + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + }) +}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config): + """Set up the Notion component.""" + hass.data[DOMAIN] = {} + hass.data[DOMAIN][DATA_CLIENT] = {} + hass.data[DOMAIN][DATA_LISTENER] = {} + + if DOMAIN not in config: + return True + + conf = config[DOMAIN] + + if conf[CONF_USERNAME] in configured_instances(hass): + return True + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={'source': SOURCE_IMPORT}, + data={ + CONF_USERNAME: conf[CONF_USERNAME], + CONF_PASSWORD: conf[CONF_PASSWORD] + })) + + return True + + +async def async_setup_entry(hass, config_entry): + """Set up Notion as a config entry.""" + session = aiohttp_client.async_get_clientsession(hass) + + try: + client = await async_get_client( + config_entry.data[CONF_USERNAME], + config_entry.data[CONF_PASSWORD], + session) + except InvalidCredentialsError: + _LOGGER.error('Invalid username and/or password') + return False + except NotionError as err: + _LOGGER.error('Config entry failed: %s', err) + raise ConfigEntryNotReady + + notion = Notion(hass, client, config_entry.entry_id) + await notion.async_update() + hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = notion + + for component in ('binary_sensor', 'sensor'): + hass.async_create_task( + hass.config_entries.async_forward_entry_setup( + config_entry, component)) + + async def refresh(event_time): + """Refresh Notion sensor data.""" + _LOGGER.debug('Refreshing Notion sensor data') + await notion.async_update() + async_dispatcher_send(hass, TOPIC_DATA_UPDATE) + + hass.data[DOMAIN][DATA_LISTENER][ + config_entry.entry_id] = async_track_time_interval( + hass, + refresh, + DEFAULT_SCAN_INTERVAL) + + return True + + +async def async_unload_entry(hass, config_entry): + """Unload a Notion config entry.""" + hass.data[DOMAIN][DATA_CLIENT].pop(config_entry.entry_id) + cancel = hass.data[DOMAIN][DATA_LISTENER].pop(config_entry.entry_id) + cancel() + + for component in ('binary_sensor', 'sensor'): + await hass.config_entries.async_forward_entry_unload( + config_entry, component) + + return True + + +async def register_new_bridge(hass, bridge, config_entry_id): + """Register a new bridge.""" + device_registry = await dr.async_get_registry(hass) + device_registry.async_get_or_create( + config_entry_id=config_entry_id, + identifiers={ + (DOMAIN, bridge['hardware_id']) + }, + manufacturer='Silicon Labs', + model=bridge['hardware_revision'], + name=bridge['name'] or bridge['id'], + sw_version=bridge['firmware_version']['wifi'] + ) + + +class Notion: + """Define a class to handle the Notion API.""" + + def __init__(self, hass, client, config_entry_id): + """Initialize.""" + self._client = client + self._config_entry_id = config_entry_id + self._hass = hass + self.bridges = {} + self.sensors = {} + self.tasks = {} + + async def async_update(self): + """Get the latest Notion data.""" + tasks = { + 'bridges': self._client.bridge.async_all(), + 'sensors': self._client.sensor.async_all(), + 'tasks': self._client.task.async_all(), + } + + results = await asyncio.gather(*tasks.values(), return_exceptions=True) + for attr, result in zip(tasks, results): + if isinstance(result, NotionError): + _LOGGER.error( + 'There was an error while updating %s: %s', attr, result) + continue + + holding_pen = getattr(self, attr) + for item in result: + if attr == 'bridges' and item['id'] not in holding_pen: + # If a new bridge is discovered, register it: + self._hass.async_create_task( + register_new_bridge( + self._hass, item, self._config_entry_id)) + holding_pen[item['id']] = item + + +class NotionEntity(Entity): + """Define a base Notion entity.""" + + def __init__( + self, + notion, + task_id, + sensor_id, + bridge_id, + system_id, + name, + device_class): + """Initialize the entity.""" + self._async_unsub_dispatcher_connect = None + self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} + self._bridge_id = bridge_id + self._device_class = device_class + self._name = name + self._notion = notion + self._sensor_id = sensor_id + self._state = None + self._system_id = system_id + self._task_id = task_id + + @property + def available(self): + """Return True if entity is available.""" + return self._task_id in self._notion.tasks + + @property + def device_class(self): + """Return the device class.""" + return self._device_class + + @property + def device_state_attributes(self) -> dict: + """Return the state attributes.""" + return self._attrs + + @property + def device_info(self): + """Return device registry information for this entity.""" + bridge = self._notion.bridges[self._bridge_id] + sensor = self._notion.sensors[self._sensor_id] + + return { + 'identifiers': { + (DOMAIN, sensor['hardware_id']) + }, + 'manufacturer': 'Silicon Labs', + 'model': sensor['hardware_revision'], + 'name': sensor['name'], + 'sw_version': sensor['firmware_version'], + 'via_device': (DOMAIN, bridge['hardware_id']) + } + + @property + def name(self): + """Return the name of the sensor.""" + return '{0}: {1}'.format( + self._notion.sensors[self._sensor_id]['name'], self._name) + + @property + def should_poll(self): + """Disable entity polling.""" + return False + + @property + def unique_id(self): + """Return a unique, unchanging string that represents this sensor.""" + return self._task_id + + async def _update_bridge_id(self): + """Update the entity's bridge ID if it has changed. + + Sensors can move to other bridges based on signal strength, etc. + """ + sensor = self._notion.sensors[self._sensor_id] + if self._bridge_id == sensor['bridge']['id']: + return + + self._bridge_id = sensor['bridge']['id'] + + device_registry = await dr.async_get_registry(self.hass) + bridge = self._notion.bridges[self._bridge_id] + bridge_device = device_registry.async_get_device( + {DOMAIN: bridge['hardware_id']}, set()) + this_device = device_registry.async_get_device( + {DOMAIN: sensor['hardware_id']}) + + device_registry.async_update_device( + this_device.id, via_device_id=bridge_device.id) + + async def async_added_to_hass(self): + """Register callbacks.""" + @callback + def update(): + """Update the entity.""" + self.hass.async_create_task(self._update_bridge_id()) + self.async_schedule_update_ha_state(True) + + self._async_unsub_dispatcher_connect = async_dispatcher_connect( + self.hass, TOPIC_DATA_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() diff --git a/homeassistant/components/notion/binary_sensor.py b/homeassistant/components/notion/binary_sensor.py new file mode 100644 index 0000000000000..166d9555a9726 --- /dev/null +++ b/homeassistant/components/notion/binary_sensor.py @@ -0,0 +1,68 @@ +"""Support for Notion binary sensors.""" +import logging + +from homeassistant.components.binary_sensor import BinarySensorDevice + +from . import ( + BINARY_SENSOR_TYPES, SENSOR_BATTERY, SENSOR_DOOR, SENSOR_GARAGE_DOOR, + SENSOR_LEAK, SENSOR_MISSING, SENSOR_SAFE, SENSOR_SLIDING, SENSOR_SMOKE_CO, + SENSOR_WINDOW_HINGED_HORIZONTAL, SENSOR_WINDOW_HINGED_VERTICAL, + NotionEntity) + +from .const import DATA_CLIENT, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Notion sensors based on a config entry.""" + notion = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] + + sensor_list = [] + for task_id, task in notion.tasks.items(): + if task['task_type'] not in BINARY_SENSOR_TYPES: + continue + + name, device_class = BINARY_SENSOR_TYPES[task['task_type']] + sensor = notion.sensors[task['sensor_id']] + + sensor_list.append( + NotionBinarySensor( + notion, + task_id, + sensor['id'], + sensor['bridge']['id'], + sensor['system_id'], + name, + device_class)) + + async_add_entities(sensor_list, True) + + +class NotionBinarySensor(NotionEntity, BinarySensorDevice): + """Define a Notion sensor.""" + + @property + def is_on(self): + """Return whether the sensor is on or off.""" + task = self._notion.tasks[self._task_id] + + if task['task_type'] == SENSOR_BATTERY: + return self._state != 'battery_good' + if task['task_type'] in ( + SENSOR_DOOR, SENSOR_GARAGE_DOOR, SENSOR_SAFE, SENSOR_SLIDING, + SENSOR_WINDOW_HINGED_HORIZONTAL, + SENSOR_WINDOW_HINGED_VERTICAL): + return self._state != 'closed' + if task['task_type'] == SENSOR_LEAK: + return self._state != 'no_leak' + if task['task_type'] == SENSOR_MISSING: + return self._state == 'not_missing' + if task['task_type'] == SENSOR_SMOKE_CO: + return self._state != 'no_alarm' + + async def async_update(self): + """Fetch new state data for the sensor.""" + task = self._notion.tasks[self._task_id] + + self._state = task['status']['value'] diff --git a/homeassistant/components/notion/config_flow.py b/homeassistant/components/notion/config_flow.py new file mode 100644 index 0000000000000..8101946f0f6ae --- /dev/null +++ b/homeassistant/components/notion/config_flow.py @@ -0,0 +1,64 @@ +"""Config flow to configure the Notion integration.""" +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import callback +from homeassistant.helpers import aiohttp_client + +from .const import DOMAIN + + +@callback +def configured_instances(hass): + """Return a set of configured Notion instances.""" + return set( + entry.data[CONF_USERNAME] + for entry in hass.config_entries.async_entries(DOMAIN)) + + +@config_entries.HANDLERS.register(DOMAIN) +class NotionFlowHandler(config_entries.ConfigFlow): + """Handle a Notion config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + async def _show_form(self, errors=None): + """Show the form to the user.""" + data_schema = vol.Schema({ + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + }) + + return self.async_show_form( + step_id='user', + data_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 aionotion import async_get_client + from aionotion.errors import NotionError + + if not user_input: + return await self._show_form() + + if user_input[CONF_USERNAME] in configured_instances(self.hass): + return await self._show_form({CONF_USERNAME: 'identifier_exists'}) + + session = aiohttp_client.async_get_clientsession(self.hass) + + try: + await async_get_client( + user_input[CONF_USERNAME], user_input[CONF_PASSWORD], session) + except NotionError: + return await self._show_form({'base': 'invalid_credentials'}) + + return self.async_create_entry( + title=user_input[CONF_USERNAME], data=user_input) diff --git a/homeassistant/components/notion/const.py b/homeassistant/components/notion/const.py new file mode 100644 index 0000000000000..f9c41d266b84d --- /dev/null +++ b/homeassistant/components/notion/const.py @@ -0,0 +1,13 @@ +"""Define constants for the Notion integration.""" +from datetime import timedelta + +DOMAIN = 'notion' + +DEFAULT_SCAN_INTERVAL = timedelta(minutes=1) + +DATA_CLIENT = 'client' + +TOPIC_DATA_UPDATE = 'data_update' + +TYPE_BINARY_SENSOR = 'binary_sensor' +TYPE_SENSOR = 'sensor' diff --git a/homeassistant/components/notion/manifest.json b/homeassistant/components/notion/manifest.json new file mode 100644 index 0000000000000..827d406a1b5c7 --- /dev/null +++ b/homeassistant/components/notion/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "notion", + "name": "Notion", + "config_flow": true, + "documentation": "https://www.home-assistant.io/components/notion", + "requirements": [ + "aionotion==1.1.0" + ], + "dependencies": [], + "codeowners": [ + "@bachya" + ] +} diff --git a/homeassistant/components/notion/sensor.py b/homeassistant/components/notion/sensor.py new file mode 100644 index 0000000000000..5efd265b6d49d --- /dev/null +++ b/homeassistant/components/notion/sensor.py @@ -0,0 +1,81 @@ +"""Support for Notion sensors.""" +import logging + +from . import SENSOR_TEMPERATURE, SENSOR_TYPES, NotionEntity +from .const import DATA_CLIENT, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Notion sensors based on a config entry.""" + notion = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] + + sensor_list = [] + for task_id, task in notion.tasks.items(): + if task['task_type'] not in SENSOR_TYPES: + continue + + name, device_class, unit = SENSOR_TYPES[task['task_type']] + sensor = notion.sensors[task['sensor_id']] + + sensor_list.append( + NotionSensor( + notion, + task_id, + sensor['id'], + sensor['bridge']['id'], + sensor['system_id'], + name, + device_class, + unit + )) + + async_add_entities(sensor_list, True) + + +class NotionSensor(NotionEntity): + """Define a Notion sensor.""" + + def __init__( + self, + notion, + task_id, + sensor_id, + bridge_id, + system_id, + name, + device_class, + unit): + """Initialize the entity.""" + super().__init__( + notion, + task_id, + sensor_id, + bridge_id, + system_id, + name, + device_class) + + self._unit = unit + + @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._unit + + async def async_update(self): + """Fetch new state data for the sensor.""" + task = self._notion.tasks[self._task_id] + + if task['task_type'] == SENSOR_TEMPERATURE: + self._state = round(float(task['status']['value']), 1) + else: + _LOGGER.error( + 'Unknown task type: %s: %s', + self._notion.sensors[self._sensor_id], task['task_type']) diff --git a/homeassistant/components/notion/strings.json b/homeassistant/components/notion/strings.json new file mode 100644 index 0000000000000..8825e25bfe841 --- /dev/null +++ b/homeassistant/components/notion/strings.json @@ -0,0 +1,19 @@ +{ + "config": { + "title": "Notion", + "step": { + "user": { + "title": "Fill in your information", + "data": { + "username": "Username/Email Address", + "password": "Password" + } + } + }, + "error": { + "identifier_exists": "Username already registered", + "invalid_credentials": "Invalid username or password", + "no_devices": "No devices found in account" + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 926023f4a7513..521417436f938 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -36,6 +36,7 @@ "mobile_app", "mqtt", "nest", + "notion", "openuv", "owntracks", "plaato", diff --git a/requirements_all.txt b/requirements_all.txt index b48ff1f0ba182..1554ec1f69572 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -156,6 +156,9 @@ aiolifx==0.6.7 # homeassistant.components.lifx aiolifx_effects==0.2.2 +# homeassistant.components.notion +aionotion==1.1.0 + # homeassistant.components.hunterdouglas_powerview aiopvapi==1.6.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 57583f9ed1a94..08baa00733332 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -57,6 +57,9 @@ aiohttp_cors==0.7.0 # homeassistant.components.hue aiohue==1.9.1 +# homeassistant.components.notion +aionotion==1.1.0 + # homeassistant.components.switcher_kis aioswitcher==2019.4.26 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 41d463c64d709..391b66052203d 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -49,6 +49,7 @@ 'aioesphomeapi', 'aiohttp_cors', 'aiohue', + 'aionotion', 'aiounifi', 'aioswitcher', 'apns2', diff --git a/tests/components/notion/__init__.py b/tests/components/notion/__init__.py new file mode 100644 index 0000000000000..479ec1b0aed41 --- /dev/null +++ b/tests/components/notion/__init__.py @@ -0,0 +1 @@ +"""Define tests for the Notion integration.""" diff --git a/tests/components/notion/test_config_flow.py b/tests/components/notion/test_config_flow.py new file mode 100644 index 0000000000000..90da878808990 --- /dev/null +++ b/tests/components/notion/test_config_flow.py @@ -0,0 +1,104 @@ +"""Define tests for the Notion config flow.""" +import aionotion +import pytest + +from homeassistant import data_entry_flow +from homeassistant.components.notion import DOMAIN, config_flow +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +from tests.common import MockConfigEntry, MockDependency, mock_coro + + +@pytest.fixture +def mock_client_coro(): + """Define a fixture for a client creation coroutine.""" + return mock_coro() + + +@pytest.fixture +def mock_aionotion(mock_client_coro): + """Mock the aionotion library.""" + with MockDependency('aionotion') as mock_: + mock_.async_get_client.return_value = mock_client_coro + yield mock_ + + +async def test_duplicate_error(hass): + """Test that errors are shown when duplicates are added.""" + conf = { + CONF_USERNAME: 'user@host.com', + CONF_PASSWORD: 'password123', + } + + MockConfigEntry(domain=DOMAIN, data=conf).add_to_hass(hass) + flow = config_flow.NotionFlowHandler() + flow.hass = hass + + result = await flow.async_step_user(user_input=conf) + assert result['errors'] == {CONF_USERNAME: 'identifier_exists'} + + +@pytest.mark.parametrize( + 'mock_client_coro', + [mock_coro(exception=aionotion.errors.NotionError)]) +async def test_invalid_credentials(hass, mock_aionotion): + """Test that an invalid API/App Key throws an error.""" + conf = { + CONF_USERNAME: 'user@host.com', + CONF_PASSWORD: 'password123', + } + + flow = config_flow.NotionFlowHandler() + flow.hass = hass + + result = await flow.async_step_user(user_input=conf) + assert result['errors'] == {'base': 'invalid_credentials'} + + +async def test_show_form(hass): + """Test that the form is served with no input.""" + flow = config_flow.NotionFlowHandler() + 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, mock_aionotion): + """Test that the import step works.""" + conf = { + CONF_USERNAME: 'user@host.com', + CONF_PASSWORD: 'password123', + } + + flow = config_flow.NotionFlowHandler() + 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'] == 'user@host.com' + assert result['data'] == { + CONF_USERNAME: 'user@host.com', + CONF_PASSWORD: 'password123', + } + + +async def test_step_user(hass, mock_aionotion): + """Test that the user step works.""" + conf = { + CONF_USERNAME: 'user@host.com', + CONF_PASSWORD: 'password123', + } + + flow = config_flow.NotionFlowHandler() + 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'] == 'user@host.com' + assert result['data'] == { + CONF_USERNAME: 'user@host.com', + CONF_PASSWORD: 'password123', + }