Add syncthing integration (#38331)

* Scaffold the integration

* Add config flow data schema

* Handle configuration errors

* Get folder states

* Support https

* Fix translations

* Listen to syncthing events in a separate thread

* Bump syncthing

* Automatically reconnect to the syncthing server

* Renames

* Improve loading and unloading

* Update folder states from events

* Refactoring, handle FolderPaused event

* Dynamic folder icons

* Refactoring

* Mark folders as unavailable when senrver is unavailable

* Update folder satus when server is available

* Raise PlatformNotReady

* Implement additional polling

* Stop polling when the server is not available

* Minor fixes

* Remove logging

* Check name uniqueness

* Refactoring

* Minor refactorings

* Bump python-syncthing

* Migrate to aiosyncthing

* Minor fixes

* Update .coveragerc

* Set quality scale

* Bump aiosyncthing, properly handle invalid token

* Fix logging

* Fix logging

* Use CONF_VERIFY_SSL from homeassistant.const

* Bump aiosyncthing. Add Syncthing device

* Fix device name

* Bump aiosyncthing

* Bump aiosyncthing

* Extract SyncthingClient

* Add folder to device_state_attributes

* Do not pass the loop

* Cover config_flow.py

* Move self.async_create_entry outside of the try block

* Raise ConfigEntryNotReady if syncthing server is not reachable

* Fix already configured error message

* Change default name to Syncthing

* Bump aiosyncthing

* Fix formatting

* Fix formatting

* Fix tests

* Fix typo, use lis comprehension

* Fix typo, remove unused CONFIG_SCHEMA

* Bump aiosyncthing

* Remove periods from log messages W0001

* Fix tests

* Black, isort

* Remove empty items from manifest.json

* Fix variable naming

* Remove async_setup

* Use SensorEntity

* Use asyncio.create_task instead of self._hass.loop.create_task

* Do not pass hass to FolderSensor initializer

* Rename device_state_attributes to extra_state_attributes

* Use callbacks

* Simplify tests

* Refactor _listen()

* Use url for the title

* Use the url instead of the name to identify the config entry

* Explicitly set sensor attributes, extract _filter_state

* Use server url instead of name in device_info

* Use server url instead of name in logs

* User server id as a device identifier

* Use URL instead of name to identify config entry

* Use shortened server id instead of name to build entity name and unique id

* Do not use CONF_NAME

* Cleanup unused strings

* Cleanup unused strings

* Add IOT class

* Scaffold the integration

* Add config flow data schema

* Handle configuration errors

* Get folder states

* Support https

* Fix translations

* Listen to syncthing events in a separate thread

* Bump syncthing

* Automatically reconnect to the syncthing server

* Renames

* Improve loading and unloading

* Update folder states from events

* Refactoring, handle FolderPaused event

* Dynamic folder icons

* Refactoring

* Mark folders as unavailable when senrver is unavailable

* Update folder satus when server is available

* Raise PlatformNotReady

* Implement additional polling

* Stop polling when the server is not available

* Minor fixes

* Remove logging

* Check name uniqueness

* Refactoring

* Minor refactorings

* Bump python-syncthing

* Migrate to aiosyncthing

* Minor fixes

* Update .coveragerc

* Set quality scale

* Bump aiosyncthing, properly handle invalid token

* Fix logging

* Fix logging

* Use CONF_VERIFY_SSL from homeassistant.const

* Bump aiosyncthing. Add Syncthing device

* Fix device name

* Bump aiosyncthing

* Bump aiosyncthing

* Extract SyncthingClient

* Add folder to device_state_attributes

* Do not pass the loop

* Cover config_flow.py

* Move self.async_create_entry outside of the try block

* Raise ConfigEntryNotReady if syncthing server is not reachable

* Fix already configured error message

* Change default name to Syncthing

* Bump aiosyncthing

* Fix formatting

* Fix formatting

* Fix tests

* Fix typo, use lis comprehension

* Fix typo, remove unused CONFIG_SCHEMA

* Bump aiosyncthing

* Remove periods from log messages W0001

* Fix tests

* Black, isort

* Remove empty items from manifest.json

* Fix variable naming

* Remove async_setup

* Use SensorEntity

* Use asyncio.create_task instead of self._hass.loop.create_task

* Do not pass hass to FolderSensor initializer

* Rename device_state_attributes to extra_state_attributes

* Use callbacks

* Simplify tests

* Refactor _listen()

* Use url for the title

* Use the url instead of the name to identify the config entry

* Explicitly set sensor attributes, extract _filter_state

* Use server url instead of name in device_info

* Use server url instead of name in logs

* User server id as a device identifier

* Use URL instead of name to identify config entry

* Use shortened server id instead of name to build entity name and unique id

* Do not use CONF_NAME

* Cleanup unused strings

* Cleanup unused strings

* Add IOT class

* Apply suggestions from code review

* Clean up

* Fix dict comprehension

* Clean sensor

* Use the server ID as a config entry unique ID

* Remove the AlreadyConfigured exception

* Clean up old error string

* Format json

* Convert sensor attributes to snake case

* Force CI

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Gleb Sinyavskiy 2021-05-08 19:12:14 +02:00 committed by GitHub
parent 7374b844d7
commit 97eb4c6c62
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 718 additions and 0 deletions

View File

@ -967,6 +967,8 @@ omit =
homeassistant/components/switchbot/switch.py
homeassistant/components/switcher_kis/switch.py
homeassistant/components/switchmate/switch.py
homeassistant/components/syncthing/__init__.py
homeassistant/components/syncthing/sensor.py
homeassistant/components/syncthru/__init__.py
homeassistant/components/syncthru/binary_sensor.py
homeassistant/components/syncthru/sensor.py

View File

@ -478,6 +478,7 @@ homeassistant/components/swiss_public_transport/* @fabaff
homeassistant/components/switchbot/* @danielhiversen
homeassistant/components/switcher_kis/* @tomerfi
homeassistant/components/switchmate/* @danielhiversen
homeassistant/components/syncthing/* @zhulik
homeassistant/components/syncthru/* @nielstron
homeassistant/components/synology_dsm/* @hacf-fr @Quentame @mib1185
homeassistant/components/synology_srm/* @aerialls

View File

@ -0,0 +1,172 @@
"""The syncthing integration."""
import asyncio
import logging
import aiosyncthing
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_TOKEN,
CONF_URL,
CONF_VERIFY_SSL,
EVENT_HOMEASSISTANT_STOP,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.dispatcher import async_dispatcher_send
from .const import (
DOMAIN,
EVENTS,
RECONNECT_INTERVAL,
SERVER_AVAILABLE,
SERVER_UNAVAILABLE,
)
PLATFORMS = ["sensor"]
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up syncthing from a config entry."""
data = entry.data
if DOMAIN not in hass.data:
hass.data[DOMAIN] = {}
client = aiosyncthing.Syncthing(
data[CONF_TOKEN],
url=data[CONF_URL],
verify_ssl=data[CONF_VERIFY_SSL],
)
try:
status = await client.system.status()
except aiosyncthing.exceptions.SyncthingError as exception:
await client.close()
raise ConfigEntryNotReady from exception
server_id = status["myID"]
syncthing = SyncthingClient(hass, client, server_id)
syncthing.subscribe()
hass.data[DOMAIN][entry.entry_id] = syncthing
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
async def cancel_listen_task(_):
await syncthing.unsubscribe()
entry.async_on_unload(
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cancel_listen_task)
)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
syncthing = hass.data[DOMAIN].pop(entry.entry_id)
await syncthing.unsubscribe()
return unload_ok
class SyncthingClient:
"""A Syncthing client."""
def __init__(self, hass, client, server_id):
"""Initialize the client."""
self._hass = hass
self._client = client
self._server_id = server_id
self._listen_task = None
@property
def server_id(self):
"""Get server id."""
return self._server_id
@property
def url(self):
"""Get server URL."""
return self._client.url
@property
def database(self):
"""Get database namespace client."""
return self._client.database
@property
def system(self):
"""Get system namespace client."""
return self._client.system
def subscribe(self):
"""Start event listener coroutine."""
self._listen_task = asyncio.create_task(self._listen())
async def unsubscribe(self):
"""Stop event listener coroutine."""
if self._listen_task:
self._listen_task.cancel()
await self._client.close()
async def _listen(self):
"""Listen to Syncthing events."""
events = self._client.events
server_was_unavailable = False
while True:
if await self._server_available():
if server_was_unavailable:
_LOGGER.info(
"The syncthing server '%s' is back online", self._client.url
)
async_dispatcher_send(
self._hass, f"{SERVER_AVAILABLE}-{self._server_id}"
)
server_was_unavailable = False
else:
await asyncio.sleep(RECONNECT_INTERVAL.total_seconds())
continue
try:
async for event in events.listen():
if events.last_seen_id == 0:
continue # skipping historical events from the first batch
if event["type"] not in EVENTS:
continue
signal_name = EVENTS[event["type"]]
folder = None
if "folder" in event["data"]:
folder = event["data"]["folder"]
else: # A workaround, some events store folder id under `id` key
folder = event["data"]["id"]
async_dispatcher_send(
self._hass,
f"{signal_name}-{self._server_id}-{folder}",
event,
)
except aiosyncthing.exceptions.SyncthingError:
_LOGGER.info(
"The syncthing server '%s' is not available. Sleeping %i seconds and retrying",
self._client.url,
RECONNECT_INTERVAL.total_seconds(),
)
async_dispatcher_send(
self._hass, f"{SERVER_UNAVAILABLE}-{self._server_id}"
)
await asyncio.sleep(RECONNECT_INTERVAL.total_seconds())
server_was_unavailable = True
continue
async def _server_available(self):
try:
await self._client.system.ping()
except aiosyncthing.exceptions.SyncthingError:
return False
else:
return True

View File

@ -0,0 +1,72 @@
"""Config flow for syncthing integration."""
import logging
import aiosyncthing
import voluptuous as vol
from homeassistant import config_entries, core, exceptions
from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL
from .const import DEFAULT_URL, DEFAULT_VERIFY_SSL, DOMAIN
_LOGGER = logging.getLogger(__name__)
DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_URL, default=DEFAULT_URL): str,
vol.Required(CONF_TOKEN): str,
vol.Required(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): bool,
}
)
async def validate_input(hass: core.HomeAssistant, data):
"""Validate the user input allows us to connect."""
try:
async with aiosyncthing.Syncthing(
data[CONF_TOKEN],
url=data[CONF_URL],
verify_ssl=data[CONF_VERIFY_SSL],
loop=hass.loop,
) as client:
server_id = (await client.system.status())["myID"]
return {"title": f"{data[CONF_URL]}", "server_id": server_id}
except aiosyncthing.exceptions.UnauthorizedError as error:
raise InvalidAuth from error
except Exception as error:
raise CannotConnect from error
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for syncthing."""
VERSION = 1
async def async_step_user(self, user_input=None):
"""Handle the initial step."""
errors = {}
if user_input is not None:
try:
info = await validate_input(self.hass, user_input)
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors[CONF_TOKEN] = "invalid_auth"
else:
await self.async_set_unique_id(info["server_id"])
self._abort_if_unique_id_configured()
return self.async_create_entry(title=info["title"], data=user_input)
return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors=errors
)
class CannotConnect(exceptions.HomeAssistantError):
"""Error to indicate we cannot connect."""
class InvalidAuth(exceptions.HomeAssistantError):
"""Error to indicate there is invalid auth."""

View File

@ -0,0 +1,33 @@
"""Constants for the syncthing integration."""
from datetime import timedelta
DOMAIN = "syncthing"
DEFAULT_VERIFY_SSL = True
DEFAULT_URL = "http://127.0.0.1:8384"
RECONNECT_INTERVAL = timedelta(seconds=10)
SCAN_INTERVAL = timedelta(seconds=120)
FOLDER_SUMMARY_RECEIVED = "syncthing_folder_summary_received"
FOLDER_PAUSED_RECEIVED = "syncthing_folder_paused_received"
SERVER_UNAVAILABLE = "syncthing_server_unavailable"
SERVER_AVAILABLE = "syncthing_server_available"
STATE_CHANGED_RECEIVED = "syncthing_state_changed_received"
EVENTS = {
"FolderSummary": FOLDER_SUMMARY_RECEIVED,
"StateChanged": STATE_CHANGED_RECEIVED,
"FolderPaused": FOLDER_PAUSED_RECEIVED,
}
FOLDER_SENSOR_ICONS = {
"paused": "mdi:folder-clock",
"scanning": "mdi:folder-search",
"syncing": "mdi:folder-sync",
"idle": "mdi:folder",
}
FOLDER_SENSOR_ALERT_ICON = "mdi:folder-alert"
FOLDER_SENSOR_DEFAULT_ICON = "mdi:folder"

View File

@ -0,0 +1,12 @@
{
"domain": "syncthing",
"name": "Syncthing",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/syncthing",
"requirements": ["aiosyncthing==0.5.1"],
"codeowners": [
"@zhulik"
],
"quality_scale": "silver",
"iot_class": "local_polling"
}

View File

@ -0,0 +1,268 @@
"""Support for monitoring the Syncthing instance."""
import logging
import aiosyncthing
from homeassistant.components.sensor import SensorEntity
from homeassistant.core import callback
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.event import async_track_time_interval
from .const import (
DOMAIN,
FOLDER_PAUSED_RECEIVED,
FOLDER_SENSOR_ALERT_ICON,
FOLDER_SENSOR_DEFAULT_ICON,
FOLDER_SENSOR_ICONS,
FOLDER_SUMMARY_RECEIVED,
SCAN_INTERVAL,
SERVER_AVAILABLE,
SERVER_UNAVAILABLE,
STATE_CHANGED_RECEIVED,
)
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Syncthing sensors."""
syncthing = hass.data[DOMAIN][config_entry.entry_id]
try:
config = await syncthing.system.config()
version = await syncthing.system.version()
except aiosyncthing.exceptions.SyncthingError as exception:
raise PlatformNotReady from exception
server_id = syncthing.server_id
entities = [
FolderSensor(
syncthing,
server_id,
folder["id"],
folder["label"],
version["version"],
)
for folder in config["folders"]
]
async_add_entities(entities)
class FolderSensor(SensorEntity):
"""A Syncthing folder sensor."""
STATE_ATTRIBUTES = {
"errors": "errors",
"globalBytes": "global_bytes",
"globalDeleted": "global_deleted",
"globalDirectories": "global_directories",
"globalFiles": "global_files",
"globalSymlinks": "global_symlinks",
"globalTotalItems": "global_total_items",
"ignorePatterns": "ignore_patterns",
"inSyncBytes": "in_sync_bytes",
"inSyncFiles": "in_sync_files",
"invalid": "invalid",
"localBytes": "local_bytes",
"localDeleted": "local_deleted",
"localDirectories": "local_directories",
"localFiles": "local_files",
"localSymlinks": "local_symlinks",
"localTotalItems": "local_total_items",
"needBytes": "need_bytes",
"needDeletes": "need_deletes",
"needDirectories": "need_directories",
"needFiles": "need_files",
"needSymlinks": "need_symlinks",
"needTotalItems": "need_total_items",
"pullErrors": "pull_errors",
"state": "state",
}
def __init__(self, syncthing, server_id, folder_id, folder_label, version):
"""Initialize the sensor."""
self._syncthing = syncthing
self._server_id = server_id
self._folder_id = folder_id
self._folder_label = folder_label
self._state = None
self._unsub_timer = None
self._version = version
self._short_server_id = server_id.split("-")[0]
@property
def name(self):
"""Return the name of the sensor."""
return f"{self._short_server_id} {self._folder_id} {self._folder_label}"
@property
def unique_id(self):
"""Return the unique id of the entity."""
return f"{self._short_server_id}-{self._folder_id}"
@property
def state(self):
"""Return the state of the sensor."""
return self._state["state"]
@property
def available(self):
"""Could the device be accessed during the last update call."""
return self._state is not None
@property
def icon(self):
"""Return the icon for this sensor."""
if self._state is None:
return FOLDER_SENSOR_DEFAULT_ICON
if self.state in FOLDER_SENSOR_ICONS:
return FOLDER_SENSOR_ICONS[self.state]
return FOLDER_SENSOR_ALERT_ICON
@property
def extra_state_attributes(self):
"""Return the state attributes."""
return self._state
@property
def should_poll(self):
"""Return the polling requirement for this sensor."""
return False
@property
def device_info(self):
"""Return device information."""
return {
"identifiers": {(DOMAIN, self._server_id)},
"name": f"Syncthing ({self._syncthing.url})",
"manufacturer": "Syncthing Team",
"sw_version": self._version,
"entry_type": "service",
}
async def async_update_status(self):
"""Request folder status and update state."""
try:
state = await self._syncthing.database.status(self._folder_id)
except aiosyncthing.exceptions.SyncthingError:
self._state = None
else:
self._state = self._filter_state(state)
self.async_write_ha_state()
def subscribe(self):
"""Start polling syncthing folder status."""
if self._unsub_timer is None:
async def refresh(event_time):
"""Get the latest data from Syncthing."""
await self.async_update_status()
self._unsub_timer = async_track_time_interval(
self.hass, refresh, SCAN_INTERVAL
)
@callback
def unsubscribe(self):
"""Stop polling syncthing folder status."""
if self._unsub_timer is not None:
self._unsub_timer()
self._unsub_timer = None
async def async_added_to_hass(self):
"""Handle entity which will be added."""
@callback
def handle_folder_summary(event):
if self._state is not None:
self._state = self._filter_state(event["data"]["summary"])
self.async_write_ha_state()
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{FOLDER_SUMMARY_RECEIVED}-{self._server_id}-{self._folder_id}",
handle_folder_summary,
)
)
@callback
def handle_state_changed(event):
if self._state is not None:
self._state["state"] = event["data"]["to"]
self.async_write_ha_state()
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{STATE_CHANGED_RECEIVED}-{self._server_id}-{self._folder_id}",
handle_state_changed,
)
)
@callback
def handle_folder_paused(event):
if self._state is not None:
self._state["state"] = "paused"
self.async_write_ha_state()
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{FOLDER_PAUSED_RECEIVED}-{self._server_id}-{self._folder_id}",
handle_folder_paused,
)
)
@callback
def handle_server_unavailable():
self._state = None
self.unsubscribe()
self.async_write_ha_state()
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{SERVER_UNAVAILABLE}-{self._server_id}",
handle_server_unavailable,
)
)
async def handle_server_available():
self.subscribe()
await self.async_update_status()
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{SERVER_AVAILABLE}-{self._server_id}",
handle_server_available,
)
)
self.subscribe()
self.async_on_remove(self.unsubscribe)
await self.async_update_status()
def _filter_state(self, state):
# Select only needed state attributes and map their names
state = {
self.STATE_ATTRIBUTES[key]: value
for key, value in state.items()
if key in self.STATE_ATTRIBUTES
}
# A workaround, for some reason, state of paused folders is an empty string
if state["state"] == "":
state["state"] = "paused"
# Add some useful attributes
state["id"] = self._folder_id
state["label"] = self._folder_label
return state

View File

@ -0,0 +1,22 @@
{
"title": "Syncthing",
"config": {
"step": {
"user": {
"data": {
"title": "Setup Syncthing integration",
"url": "[%key:common::config_flow::data::url%]",
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]",
"token": "Token"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
}
}
}

View File

@ -0,0 +1,22 @@
{
"config": {
"error": {
"cannot_connect": "Unable to connect to the Syncthing server.",
"invalid_auth": "Invalid authentication"
},
"abort": {
"already_configured": "Service is already configured"
},
"step": {
"user": {
"title": "Setup Syncthing integration",
"data": {
"url": "URL",
"token": "Token",
"verify_ssl": "Verify SSL"
}
}
}
},
"title": "Syncthing"
}

View File

@ -239,6 +239,7 @@ FLOWS = [
"srp_energy",
"starline",
"subaru",
"syncthing",
"syncthru",
"synology_dsm",
"system_bridge",

View File

@ -232,6 +232,9 @@ aioshelly==0.6.2
# homeassistant.components.switcher_kis
aioswitcher==1.2.1
# homeassistant.components.syncthing
aiosyncthing==0.5.1
# homeassistant.components.unifi
aiounifi==26

View File

@ -154,6 +154,9 @@ aioshelly==0.6.2
# homeassistant.components.switcher_kis
aioswitcher==1.2.1
# homeassistant.components.syncthing
aiosyncthing==0.5.1
# homeassistant.components.unifi
aiounifi==26

View File

@ -0,0 +1 @@
"""Tests for the syncthing integration."""

View File

@ -0,0 +1,106 @@
"""Tests for syncthing config flow."""
from unittest.mock import patch
from aiosyncthing.exceptions import UnauthorizedError
from homeassistant import config_entries, data_entry_flow, setup
from homeassistant.components.syncthing.const import DOMAIN
from homeassistant.const import CONF_NAME, CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL
from tests.common import MockConfigEntry
NAME = "Syncthing"
URL = "http://127.0.0.1:8384"
TOKEN = "token"
VERIFY_SSL = True
MOCK_ENTRY = {
CONF_NAME: NAME,
CONF_URL: URL,
CONF_TOKEN: TOKEN,
CONF_VERIFY_SSL: VERIFY_SSL,
}
async def test_show_setup_form(hass):
"""Test that the setup form is served."""
await setup.async_setup_component(hass, "persistent_notification", {})
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] == {}
assert result["step_id"] == "user"
async def test_flow_successfull(hass):
"""Test with required fields only."""
with patch(
"aiosyncthing.system.System.status", return_value={"myID": "server-id"}
), patch(
"homeassistant.components.syncthing.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": "user"},
data={
CONF_NAME: NAME,
CONF_URL: URL,
CONF_TOKEN: TOKEN,
CONF_VERIFY_SSL: VERIFY_SSL,
},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == "http://127.0.0.1:8384"
assert result["data"][CONF_NAME] == NAME
assert result["data"][CONF_URL] == URL
assert result["data"][CONF_TOKEN] == TOKEN
assert result["data"][CONF_VERIFY_SSL] == VERIFY_SSL
assert len(mock_setup_entry.mock_calls) == 1
async def test_flow_already_configured(hass):
"""Test name is already configured."""
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY, unique_id="server-id")
entry.add_to_hass(hass)
with patch("aiosyncthing.system.System.status", return_value={"myID": "server-id"}):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": "user"},
data=MOCK_ENTRY,
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"
async def test_flow_invalid_auth(hass):
"""Test invalid auth."""
with patch("aiosyncthing.system.System.status", side_effect=UnauthorizedError):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": "user"},
data=MOCK_ENTRY,
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"]["token"] == "invalid_auth"
async def test_flow_cannot_connect(hass):
"""Test cannot connect."""
with patch("aiosyncthing.system.System.status", side_effect=Exception):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": "user"},
data=MOCK_ENTRY,
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"]["base"] == "cannot_connect"