Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add discovery support to synology_dsm #33729

Merged
merged 21 commits into from
Apr 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 35 additions & 24 deletions homeassistant/components/synology_dsm/.translations/en.json
Original file line number Diff line number Diff line change
@@ -1,26 +1,37 @@
{
"config": {
"abort": {
"already_configured": "Host already configured"
},
"error": {
"login": "Login error: please check your username & password",
"unknown": "Unknown error: please retry later or an other configuration"
},
"step": {
"user": {
"data": {
"api_version": "DSM version",
"host": "Host",
"name": "Name",
"password": "Password",
"port": "Port",
"ssl": "Use SSL/TLS to connect to your NAS",
"username": "Username"
},
"title": "Synology DSM"
}
},
"title": "Synology DSM"
"config": {
"title": "Synology DSM",
"flow_title": "Synology DSM {name} ({host})",
"step": {
"user": {
"title": "Synology DSM",
"data": {
"host": "Host",
"port": "Port (Optional)",
"ssl": "Use SSL/TLS to connect to your NAS",
"api_version": "DSM version",
"username": "Username",
"password": "Password"
}
},
"link": {
"title": "Synology DSM",
"description": "Do you want to setup {name} ({host})?",
"data": {
"ssl": "Use SSL/TLS to connect to your NAS",
"api_version": "DSM version",
"username": "Username",
"password": "Password",
"port": "Port (Optional)"
}
}
},
"error": {
"login": "Login error: please check your username & password",
"unknown": "Unknown error: please retry later or an other configuration"
},
"abort": {
"already_configured": "Host already configured"
}
}
}
}
4 changes: 1 addition & 3 deletions homeassistant/components/synology_dsm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
CONF_API_VERSION,
CONF_DISKS,
CONF_HOST,
CONF_NAME,
CONF_PASSWORD,
CONF_PORT,
CONF_SSL,
Expand All @@ -23,11 +22,10 @@
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.typing import HomeAssistantType

from .const import CONF_VOLUMES, DEFAULT_DSM_VERSION, DEFAULT_NAME, DEFAULT_SSL, DOMAIN
from .const import CONF_VOLUMES, DEFAULT_DSM_VERSION, DEFAULT_SSL, DOMAIN

CONFIG_SCHEMA = vol.Schema(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_PORT): cv.port,
vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
Expand Down
182 changes: 124 additions & 58 deletions homeassistant/components/synology_dsm/config_flow.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
"""Config flow to configure the Synology DSM integration."""
import logging
from urllib.parse import urlparse

from synology_dsm import SynologyDSM
from synology_dsm.api.core.utilization import SynoCoreUtilization
from synology_dsm.api.dsm.information import SynoDSMInformation
from synology_dsm.api.storage.storage import SynoStorage
import voluptuous as vol

from homeassistant import config_entries
from homeassistant import config_entries, exceptions
from homeassistant.components import ssdp
from homeassistant.const import (
CONF_API_VERSION,
CONF_DISKS,
Expand All @@ -20,54 +21,71 @@
from .const import (
CONF_VOLUMES,
DEFAULT_DSM_VERSION,
DEFAULT_NAME,
DEFAULT_PORT,
DEFAULT_PORT_SSL,
DEFAULT_SSL,
)
from .const import DOMAIN # pylint: disable=unused-import

_LOGGER = logging.getLogger(__name__)


def _discovery_schema_with_defaults(discovery_info):
return vol.Schema(_ordered_shared_schema(discovery_info))


def _user_schema_with_defaults(user_input):
user_schema = {
vol.Required(CONF_HOST, default=user_input.get(CONF_HOST, "")): str,
}
user_schema.update(_ordered_shared_schema(user_input))

return vol.Schema(user_schema)


def _ordered_shared_schema(schema_input):
return {
vol.Required(CONF_USERNAME, default=schema_input.get(CONF_USERNAME, "")): str,
vol.Required(CONF_PASSWORD, default=schema_input.get(CONF_PASSWORD, "")): str,
vol.Optional(CONF_PORT, default=schema_input.get(CONF_PORT, "")): str,
vol.Optional(CONF_SSL, default=schema_input.get(CONF_SSL, DEFAULT_SSL)): bool,
vol.Optional(
CONF_API_VERSION,
default=schema_input.get(CONF_API_VERSION, DEFAULT_DSM_VERSION),
): vol.All(
vol.Coerce(int), vol.In([5, 6]), # DSM versions supported by the library
),
}


class SynologyDSMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow."""

VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL

def __init__(self):
"""Initialize the synology_dsm config flow."""
self.discovered_conf = {}

async def _show_setup_form(self, user_input=None, errors=None):
"""Show the setup form to the user."""

if user_input is None:
if not user_input:
user_input = {}

if self.discovered_conf:
user_input.update(self.discovered_conf)
step_id = "link"
data_schema = _discovery_schema_with_defaults(user_input)
else:
step_id = "user"
data_schema = _user_schema_with_defaults(user_input)

return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Optional(
CONF_NAME, default=user_input.get(CONF_NAME, DEFAULT_NAME)
): str,
vol.Required(CONF_HOST, default=user_input.get(CONF_HOST, "")): str,
vol.Optional(CONF_PORT, default=user_input.get(CONF_PORT, "")): str,
vol.Optional(
CONF_SSL, default=user_input.get(CONF_SSL, DEFAULT_SSL)
): bool,
vol.Optional(
CONF_API_VERSION,
default=user_input.get(CONF_API_VERSION, DEFAULT_DSM_VERSION),
): vol.All(
vol.Coerce(int),
vol.In([5, 6]), # DSM versions supported by the library
),
vol.Required(
CONF_USERNAME, default=user_input.get(CONF_USERNAME, "")
): str,
vol.Required(
CONF_PASSWORD, default=user_input.get(CONF_PASSWORD, "")
): str,
}
),
step_id=step_id,
data_schema=data_schema,
errors=errors or {},
description_placeholders=self.discovered_conf or {},
)

async def async_step_user(self, user_input=None):
Expand All @@ -77,7 +95,9 @@ async def async_step_user(self, user_input=None):
if user_input is None:
return await self._show_setup_form(user_input, None)

name = user_input.get(CONF_NAME, DEFAULT_NAME)
if self.discovered_conf:
user_input.update(self.discovered_conf)
bdraco marked this conversation as resolved.
Show resolved Hide resolved

host = user_input[CONF_HOST]
port = user_input.get(CONF_PORT)
username = user_input[CONF_USERNAME]
Expand All @@ -95,48 +115,94 @@ async def async_step_user(self, user_input=None):
host, port, username, password, use_ssl, dsm_version=api_version,
)

if not await self.hass.async_add_executor_job(api.login):
try:
serial = await self.hass.async_add_executor_job(
_login_and_fetch_syno_info, api
)
except InvalidAuth:
errors[CONF_USERNAME] = "login"
return await self._show_setup_form(user_input, errors)

information: SynoDSMInformation = await self.hass.async_add_executor_job(
getattr, api, "information"
)
utilisation: SynoCoreUtilization = await self.hass.async_add_executor_job(
getattr, api, "utilisation"
)
storage: SynoStorage = await self.hass.async_add_executor_job(
getattr, api, "storage"
)
except InvalidData:
errors["base"] = "missing_data"

if (
information.serial is None
or utilisation.cpu_user_load is None
or storage.disks_ids is None
or storage.volumes_ids is None
):
errors["base"] = "unknown"
if errors:
return await self._show_setup_form(user_input, errors)

# Check if already configured
await self.async_set_unique_id(information.serial)
await self.async_set_unique_id(serial)
self._abort_if_unique_id_configured()

config_data = {
CONF_NAME: name,
CONF_HOST: host,
CONF_PORT: port,
CONF_SSL: use_ssl,
CONF_USERNAME: username,
CONF_PASSWORD: password,
}
if user_input.get(CONF_DISKS):
config_data.update({CONF_DISKS: user_input[CONF_DISKS]})
config_data[CONF_DISKS] = user_input[CONF_DISKS]
if user_input.get(CONF_VOLUMES):
config_data.update({CONF_VOLUMES: user_input[CONF_VOLUMES]})
config_data[CONF_VOLUMES] = user_input[CONF_VOLUMES]

return self.async_create_entry(title=host, data=config_data,)
return self.async_create_entry(title=host, data=config_data)

async def async_step_ssdp(self, discovery_info):
"""Handle a discovered synology_dsm."""
parsed_url = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION])
friendly_name = (
discovery_info[ssdp.ATTR_UPNP_FRIENDLY_NAME].split("(", 1)[0].strip()
)

if self._host_already_configured(parsed_url.hostname):
return self.async_abort(reason="already_configured")

self.discovered_conf = {
CONF_NAME: friendly_name,
bdraco marked this conversation as resolved.
Show resolved Hide resolved
CONF_HOST: parsed_url.hostname,
}
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context["title_placeholders"] = self.discovered_conf
return await self.async_step_user()

async def async_step_import(self, user_input=None):
"""Import a config entry."""
return await self.async_step_user(user_input)

async def async_step_link(self, user_input=None):
"""Link a config entry from discovery."""
return await self.async_step_user(user_input)

def _host_already_configured(self, hostname):
"""See if we already have a host matching user input configured."""
existing_hosts = {
entry.data[CONF_HOST] for entry in self._async_current_entries()
}
return hostname in existing_hosts
Quentame marked this conversation as resolved.
Show resolved Hide resolved


def _login_and_fetch_syno_info(api):
"""Login to the NAS and fetch basic data."""
if not api.login():
raise InvalidAuth

# These do i/o
information = api.information
utilisation = api.utilisation
storage = api.storage

if (
information.serial is None
or utilisation.cpu_user_load is None
or storage.disks_ids is None
or storage.volumes_ids is None
):
raise InvalidData

return information.serial


class InvalidData(exceptions.HomeAssistantError):
"""Error to indicate we get invalid data from the nas."""


class InvalidAuth(exceptions.HomeAssistantError):
"""Error to indicate there is invalid auth."""
2 changes: 1 addition & 1 deletion homeassistant/components/synology_dsm/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
DOMAIN = "synology_dsm"

CONF_VOLUMES = "volumes"
DEFAULT_NAME = "Synology DSM"
BASE_NAME = "Synology"
DEFAULT_SSL = True
DEFAULT_PORT = 5000
DEFAULT_PORT_SSL = 5001
Expand Down
8 changes: 7 additions & 1 deletion homeassistant/components/synology_dsm/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,11 @@
"documentation": "https://www.home-assistant.io/integrations/synology_dsm",
"requirements": ["python-synology==0.5.0"],
"codeowners": ["@ProtoThis", "@Quentame"],
"config_flow": true
"config_flow": true,
"ssdp": [
{
"manufacturer": "Synology",
"deviceType": "urn:schemas-upnp-org:device:Basic:1"
}
]
}
Loading