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:
parent
11ded51ddb
commit
dbe89c1e02
5 changed files with 146 additions and 10 deletions
|
@ -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):
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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
|
||||
|
|
15
homeassistant/components/synology_dsm/services.yaml
Normal file
15
homeassistant/components/synology_dsm/services.yaml
Normal 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
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Reference in a new issue