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
This commit is contained in:
parent
7a5fca69af
commit
c2e843cbc3
16 changed files with 700 additions and 0 deletions
|
@ -409,6 +409,8 @@ omit =
|
||||||
homeassistant/components/nissan_leaf/*
|
homeassistant/components/nissan_leaf/*
|
||||||
homeassistant/components/nmap_tracker/device_tracker.py
|
homeassistant/components/nmap_tracker/device_tracker.py
|
||||||
homeassistant/components/nmbs/sensor.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/noaa_tides/sensor.py
|
||||||
homeassistant/components/norway_air/air_quality.py
|
homeassistant/components/norway_air/air_quality.py
|
||||||
homeassistant/components/nsw_fuel_station/sensor.py
|
homeassistant/components/nsw_fuel_station/sensor.py
|
||||||
|
|
|
@ -183,6 +183,7 @@ homeassistant/components/nissan_leaf/* @filcole
|
||||||
homeassistant/components/nmbs/* @thibmaek
|
homeassistant/components/nmbs/* @thibmaek
|
||||||
homeassistant/components/no_ip/* @fabaff
|
homeassistant/components/no_ip/* @fabaff
|
||||||
homeassistant/components/notify/* @home-assistant/core
|
homeassistant/components/notify/* @home-assistant/core
|
||||||
|
homeassistant/components/notion/* @bachya
|
||||||
homeassistant/components/nsw_fuel_station/* @nickw444
|
homeassistant/components/nsw_fuel_station/* @nickw444
|
||||||
homeassistant/components/nuki/* @pschmitt
|
homeassistant/components/nuki/* @pschmitt
|
||||||
homeassistant/components/ohmconnect/* @robbiet480
|
homeassistant/components/ohmconnect/* @robbiet480
|
||||||
|
|
19
homeassistant/components/notion/.translations/en.json
Normal file
19
homeassistant/components/notion/.translations/en.json
Normal file
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
307
homeassistant/components/notion/__init__.py
Normal file
307
homeassistant/components/notion/__init__.py
Normal file
|
@ -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()
|
68
homeassistant/components/notion/binary_sensor.py
Normal file
68
homeassistant/components/notion/binary_sensor.py
Normal file
|
@ -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']
|
64
homeassistant/components/notion/config_flow.py
Normal file
64
homeassistant/components/notion/config_flow.py
Normal file
|
@ -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)
|
13
homeassistant/components/notion/const.py
Normal file
13
homeassistant/components/notion/const.py
Normal file
|
@ -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'
|
13
homeassistant/components/notion/manifest.json
Normal file
13
homeassistant/components/notion/manifest.json
Normal file
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
81
homeassistant/components/notion/sensor.py
Normal file
81
homeassistant/components/notion/sensor.py
Normal file
|
@ -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'])
|
19
homeassistant/components/notion/strings.json
Normal file
19
homeassistant/components/notion/strings.json
Normal file
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -36,6 +36,7 @@ FLOWS = [
|
||||||
"mobile_app",
|
"mobile_app",
|
||||||
"mqtt",
|
"mqtt",
|
||||||
"nest",
|
"nest",
|
||||||
|
"notion",
|
||||||
"openuv",
|
"openuv",
|
||||||
"owntracks",
|
"owntracks",
|
||||||
"plaato",
|
"plaato",
|
||||||
|
|
|
@ -156,6 +156,9 @@ aiolifx==0.6.7
|
||||||
# homeassistant.components.lifx
|
# homeassistant.components.lifx
|
||||||
aiolifx_effects==0.2.2
|
aiolifx_effects==0.2.2
|
||||||
|
|
||||||
|
# homeassistant.components.notion
|
||||||
|
aionotion==1.1.0
|
||||||
|
|
||||||
# homeassistant.components.hunterdouglas_powerview
|
# homeassistant.components.hunterdouglas_powerview
|
||||||
aiopvapi==1.6.14
|
aiopvapi==1.6.14
|
||||||
|
|
||||||
|
|
|
@ -57,6 +57,9 @@ aiohttp_cors==0.7.0
|
||||||
# homeassistant.components.hue
|
# homeassistant.components.hue
|
||||||
aiohue==1.9.1
|
aiohue==1.9.1
|
||||||
|
|
||||||
|
# homeassistant.components.notion
|
||||||
|
aionotion==1.1.0
|
||||||
|
|
||||||
# homeassistant.components.switcher_kis
|
# homeassistant.components.switcher_kis
|
||||||
aioswitcher==2019.4.26
|
aioswitcher==2019.4.26
|
||||||
|
|
||||||
|
|
|
@ -49,6 +49,7 @@ TEST_REQUIREMENTS = (
|
||||||
'aioesphomeapi',
|
'aioesphomeapi',
|
||||||
'aiohttp_cors',
|
'aiohttp_cors',
|
||||||
'aiohue',
|
'aiohue',
|
||||||
|
'aionotion',
|
||||||
'aiounifi',
|
'aiounifi',
|
||||||
'aioswitcher',
|
'aioswitcher',
|
||||||
'apns2',
|
'apns2',
|
||||||
|
|
1
tests/components/notion/__init__.py
Normal file
1
tests/components/notion/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
"""Define tests for the Notion integration."""
|
104
tests/components/notion/test_config_flow.py
Normal file
104
tests/components/notion/test_config_flow.py
Normal file
|
@ -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',
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue