Add reboot and shutdown service to synology_dsm (#42697)

* add reboot and shutdown service to synology_dsm

* apply suggestions

* make _async_setup_services() async

* add comment to make sure unique_id is serial
This commit is contained in:
Michael 2020-11-11 19:06:50 +01:00 committed by GitHub
parent 11ded51ddb
commit dbe89c1e02
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 146 additions and 10 deletions

View file

@ -6,13 +6,17 @@ from typing import Dict
from synology_dsm import SynologyDSM
from synology_dsm.api.core.security import SynoCoreSecurity
from synology_dsm.api.core.system import SynoCoreSystem
from synology_dsm.api.core.upgrade import SynoCoreUpgrade
from synology_dsm.api.core.utilization import SynoCoreUtilization
from synology_dsm.api.dsm.information import SynoDSMInformation
from synology_dsm.api.dsm.network import SynoDSMNetwork
from synology_dsm.api.storage.storage import SynoStorage
from synology_dsm.api.surveillance_station import SynoSurveillanceStation
from synology_dsm.exceptions import SynologyDSMRequestException
from synology_dsm.exceptions import (
SynologyDSMLoginFailedException,
SynologyDSMRequestException,
)
import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
@ -29,7 +33,7 @@ from homeassistant.const import (
CONF_USERNAME,
CONF_VERIFY_SSL,
)
from homeassistant.core import callback
from homeassistant.core import ServiceCall, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import entity_registry
import homeassistant.helpers.config_validation as cv
@ -42,6 +46,7 @@ from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.typing import HomeAssistantType
from .const import (
CONF_SERIAL,
CONF_VOLUMES,
DEFAULT_SCAN_INTERVAL,
DEFAULT_USE_SSL,
@ -53,6 +58,9 @@ from .const import (
ENTITY_NAME,
ENTITY_UNIT,
PLATFORMS,
SERVICE_REBOOT,
SERVICE_SHUTDOWN,
SERVICES,
STORAGE_DISK_BINARY_SENSORS,
STORAGE_DISK_SENSORS,
STORAGE_VOL_SENSORS,
@ -176,7 +184,8 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry):
api = SynoApi(hass, entry)
try:
await api.async_setup()
except SynologyDSMRequestException as err:
except (SynologyDSMLoginFailedException, SynologyDSMRequestException) as err:
_LOGGER.debug("async_setup_entry - Unable to connect to DSM: %s", str(err))
raise ConfigEntryNotReady from err
undo_listener = entry.add_update_listener(_async_update_listener)
@ -187,6 +196,9 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry):
UNDO_UPDATE_LISTENER: undo_listener,
}
# Services
await _async_setup_services(hass)
# For SSDP compat
if not entry.data.get(CONF_MAC):
network = await hass.async_add_executor_job(getattr, api.dsm, "network")
@ -227,6 +239,49 @@ async def _async_update_listener(hass: HomeAssistantType, entry: ConfigEntry):
await hass.config_entries.async_reload(entry.entry_id)
async def _async_setup_services(hass: HomeAssistantType):
"""Service handler setup."""
async def service_handler(call: ServiceCall):
"""Handle service call."""
_LOGGER.debug(
"service_handler - called as '%s' with data: %s", call.service, call.data
)
serial = call.data.get(CONF_SERIAL)
dsm_devices = hass.data[DOMAIN]
if serial:
dsm_device = dsm_devices.get(serial)
elif len(dsm_devices) == 1:
dsm_device = next(iter(dsm_devices.values()))
serial = next(iter(dsm_devices))
else:
_LOGGER.error(
"service_handler - more than one DSM configured, must specify one of serials %s",
sorted(dsm_devices),
)
return
if not dsm_device:
_LOGGER.error(
"service_handler - DSM with specified serial %s not found", serial
)
return
_LOGGER.info("%s DSM with serial %s", call.service, serial)
dsm_api = dsm_device[SYNO_API]
if call.service == SERVICE_REBOOT:
await dsm_api.async_reboot()
elif call.service == SERVICE_SHUTDOWN:
await dsm_api.system.shutdown()
for service in SERVICES:
_LOGGER.debug(
"_async_setup_services - register service %s on domain %s", service, DOMAIN
)
hass.services.async_register(DOMAIN, service, service_handler)
class SynoApi:
"""Class to interface with Synology DSM API."""
@ -240,19 +295,21 @@ class SynoApi:
self.information: SynoDSMInformation = None
self.network: SynoDSMNetwork = None
self.security: SynoCoreSecurity = None
self.upgrade: SynoCoreUpgrade = None
self.storage: SynoStorage = None
self.utilisation: SynoCoreUtilization = None
self.surveillance_station: SynoSurveillanceStation = None
self.system: SynoCoreSystem = None
self.upgrade: SynoCoreUpgrade = None
self.utilisation: SynoCoreUtilization = None
# Should we fetch them
self._fetching_entities = {}
self._with_information = True
self._with_security = True
self._with_storage = True
self._with_surveillance_station = True
self._with_system = True
self._with_upgrade = True
self._with_utilisation = True
self._with_information = True
self._with_surveillance_station = True
self._unsub_dispatcher = None
@ -320,6 +377,7 @@ class SynoApi:
self._fetching_entities.get(SynoCoreSecurity.API_KEY)
)
self._with_storage = bool(self._fetching_entities.get(SynoStorage.API_KEY))
self._with_system = bool(self._fetching_entities.get(SynoCoreSystem.API_KEY))
self._with_upgrade = bool(self._fetching_entities.get(SynoCoreUpgrade.API_KEY))
self._with_utilisation = bool(
self._fetching_entities.get(SynoCoreUtilization.API_KEY)
@ -342,6 +400,10 @@ class SynoApi:
self.dsm.reset(self.storage)
self.storage = None
if not self._with_system:
self.dsm.reset(self.system)
self.system = None
if not self._with_upgrade:
self.dsm.reset(self.upgrade)
self.upgrade = None
@ -369,12 +431,29 @@ class SynoApi:
if self._with_upgrade:
self.upgrade = self.dsm.upgrade
if self._with_system:
self.system = self.dsm.system
if self._with_utilisation:
self.utilisation = self.dsm.utilisation
if self._with_surveillance_station:
self.surveillance_station = self.dsm.surveillance_station
async def async_reboot(self):
"""Reboot NAS."""
if not self.system:
_LOGGER.debug("async_reboot - System API not ready: %s", self)
return
self._hass.async_add_executor_job(self.system.reboot)
async def async_shutdown(self):
"""Shutdown NAS."""
if not self.system:
_LOGGER.debug("async_shutdown - System API not ready: %s", self)
return
self._hass.async_add_executor_job(self.system.shutdown)
async def async_unload(self):
"""Stop interacting with the NAS and prepare for removal from hass."""
self._unsub_dispatcher()
@ -382,8 +461,17 @@ class SynoApi:
async def async_update(self, now=None):
"""Update function for updating API information."""
self._async_setup_api_requests()
await self._hass.async_add_executor_job(self.dsm.update, self._with_information)
async_dispatcher_send(self._hass, self.signal_sensor_update)
try:
await self._hass.async_add_executor_job(
self.dsm.update, self._with_information
)
async_dispatcher_send(self._hass, self.signal_sensor_update)
except (SynologyDSMLoginFailedException, SynologyDSMRequestException) as err:
_LOGGER.warning(
"async_update - connection error during update, fallback by reloading the entry"
)
_LOGGER.debug("async_update - exception: %s", str(err))
await self._hass.config_entries.async_reload(self._entry.entry_id)
class SynologyDSMEntity(Entity):

View file

@ -164,8 +164,10 @@ class SynologyDSMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
if errors:
return await self._show_setup_form(user_input, errors)
# Check if already configured
# unique_id should be serial for services purpose
await self.async_set_unique_id(serial, raise_on_progress=False)
# Check if already configured
self._abort_if_unique_id_configured()
config_data = {

View file

@ -25,6 +25,7 @@ SYNO_API = "syno_api"
UNDO_UPDATE_LISTENER = "undo_update_listener"
# Configuration
CONF_SERIAL = "serial"
CONF_VOLUMES = "volumes"
DEFAULT_USE_SSL = True
@ -42,6 +43,14 @@ ENTITY_ICON = "icon"
ENTITY_CLASS = "device_class"
ENTITY_ENABLE = "enable"
# Services
SERVICE_REBOOT = "reboot"
SERVICE_SHUTDOWN = "shutdown"
SERVICES = [
SERVICE_REBOOT,
SERVICE_SHUTDOWN,
]
# Entity keys should start with the API_KEY to fetch
# Binary sensors

View file

@ -0,0 +1,15 @@
# synology-dsm service entries description.
reboot:
description: Reboot the NAS.
fields:
serial:
description: serial of the NAS to reboot; required when multiple NAS are configured.
example: 1NDVC86409
shutdown:
description: Shutdown the NAS.
fields:
serial:
description: serial of the NAS to shutdown; required when multiple NAS are configured.
example: 1NDVC86409

View file

@ -10,6 +10,7 @@ from synology_dsm.exceptions import (
from homeassistant import data_entry_flow, setup
from homeassistant.components import ssdp
from homeassistant.components.synology_dsm import _async_setup_services
from homeassistant.components.synology_dsm.config_flow import CONF_OTP_CODE
from homeassistant.components.synology_dsm.const import (
CONF_VOLUMES,
@ -20,6 +21,7 @@ from homeassistant.components.synology_dsm.const import (
DEFAULT_USE_SSL,
DEFAULT_VERIFY_SSL,
DOMAIN,
SERVICES,
)
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_SSDP, SOURCE_USER
from homeassistant.const import (
@ -496,3 +498,23 @@ async def test_options_flow(hass: HomeAssistantType, service: MagicMock):
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert config_entry.options[CONF_SCAN_INTERVAL] == 2
assert config_entry.options[CONF_TIMEOUT] == 30
async def test_services_registered(hass: HomeAssistantType):
"""Test if all services are registered."""
with patch(
"homeassistant.core.ServiceRegistry.async_register", return_value=Mock(True)
) as async_register:
await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_USER},
data={
CONF_HOST: HOST,
CONF_PORT: PORT,
CONF_SSL: USE_SSL,
CONF_USERNAME: USERNAME,
CONF_PASSWORD: PASSWORD,
},
)
await _async_setup_services(hass)
assert async_register.call_count == len(SERVICES)