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/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
|
||||
|
|
|
@ -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
|
||||
|
|
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",
|
||||
"mqtt",
|
||||
"nest",
|
||||
"notion",
|
||||
"openuv",
|
||||
"owntracks",
|
||||
"plaato",
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -49,6 +49,7 @@ TEST_REQUIREMENTS = (
|
|||
'aioesphomeapi',
|
||||
'aiohttp_cors',
|
||||
'aiohue',
|
||||
'aionotion',
|
||||
'aiounifi',
|
||||
'aioswitcher',
|
||||
'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