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 import SynologyDSM
|
||||||
from synology_dsm.api.core.security import SynoCoreSecurity
|
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.upgrade import SynoCoreUpgrade
|
||||||
from synology_dsm.api.core.utilization import SynoCoreUtilization
|
from synology_dsm.api.core.utilization import SynoCoreUtilization
|
||||||
from synology_dsm.api.dsm.information import SynoDSMInformation
|
from synology_dsm.api.dsm.information import SynoDSMInformation
|
||||||
from synology_dsm.api.dsm.network import SynoDSMNetwork
|
from synology_dsm.api.dsm.network import SynoDSMNetwork
|
||||||
from synology_dsm.api.storage.storage import SynoStorage
|
from synology_dsm.api.storage.storage import SynoStorage
|
||||||
from synology_dsm.api.surveillance_station import SynoSurveillanceStation
|
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
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||||
|
@ -29,7 +33,7 @@ from homeassistant.const import (
|
||||||
CONF_USERNAME,
|
CONF_USERNAME,
|
||||||
CONF_VERIFY_SSL,
|
CONF_VERIFY_SSL,
|
||||||
)
|
)
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import ServiceCall, callback
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
from homeassistant.helpers import entity_registry
|
from homeassistant.helpers import entity_registry
|
||||||
import homeassistant.helpers.config_validation as cv
|
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 homeassistant.helpers.typing import HomeAssistantType
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
|
CONF_SERIAL,
|
||||||
CONF_VOLUMES,
|
CONF_VOLUMES,
|
||||||
DEFAULT_SCAN_INTERVAL,
|
DEFAULT_SCAN_INTERVAL,
|
||||||
DEFAULT_USE_SSL,
|
DEFAULT_USE_SSL,
|
||||||
|
@ -53,6 +58,9 @@ from .const import (
|
||||||
ENTITY_NAME,
|
ENTITY_NAME,
|
||||||
ENTITY_UNIT,
|
ENTITY_UNIT,
|
||||||
PLATFORMS,
|
PLATFORMS,
|
||||||
|
SERVICE_REBOOT,
|
||||||
|
SERVICE_SHUTDOWN,
|
||||||
|
SERVICES,
|
||||||
STORAGE_DISK_BINARY_SENSORS,
|
STORAGE_DISK_BINARY_SENSORS,
|
||||||
STORAGE_DISK_SENSORS,
|
STORAGE_DISK_SENSORS,
|
||||||
STORAGE_VOL_SENSORS,
|
STORAGE_VOL_SENSORS,
|
||||||
|
@ -176,7 +184,8 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry):
|
||||||
api = SynoApi(hass, entry)
|
api = SynoApi(hass, entry)
|
||||||
try:
|
try:
|
||||||
await api.async_setup()
|
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
|
raise ConfigEntryNotReady from err
|
||||||
|
|
||||||
undo_listener = entry.add_update_listener(_async_update_listener)
|
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,
|
UNDO_UPDATE_LISTENER: undo_listener,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Services
|
||||||
|
await _async_setup_services(hass)
|
||||||
|
|
||||||
# For SSDP compat
|
# For SSDP compat
|
||||||
if not entry.data.get(CONF_MAC):
|
if not entry.data.get(CONF_MAC):
|
||||||
network = await hass.async_add_executor_job(getattr, api.dsm, "network")
|
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)
|
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 SynoApi:
|
||||||
"""Class to interface with Synology DSM API."""
|
"""Class to interface with Synology DSM API."""
|
||||||
|
|
||||||
|
@ -240,19 +295,21 @@ class SynoApi:
|
||||||
self.information: SynoDSMInformation = None
|
self.information: SynoDSMInformation = None
|
||||||
self.network: SynoDSMNetwork = None
|
self.network: SynoDSMNetwork = None
|
||||||
self.security: SynoCoreSecurity = None
|
self.security: SynoCoreSecurity = None
|
||||||
self.upgrade: SynoCoreUpgrade = None
|
|
||||||
self.storage: SynoStorage = None
|
self.storage: SynoStorage = None
|
||||||
self.utilisation: SynoCoreUtilization = None
|
|
||||||
self.surveillance_station: SynoSurveillanceStation = None
|
self.surveillance_station: SynoSurveillanceStation = None
|
||||||
|
self.system: SynoCoreSystem = None
|
||||||
|
self.upgrade: SynoCoreUpgrade = None
|
||||||
|
self.utilisation: SynoCoreUtilization = None
|
||||||
|
|
||||||
# Should we fetch them
|
# Should we fetch them
|
||||||
self._fetching_entities = {}
|
self._fetching_entities = {}
|
||||||
|
self._with_information = True
|
||||||
self._with_security = True
|
self._with_security = True
|
||||||
self._with_storage = True
|
self._with_storage = True
|
||||||
|
self._with_surveillance_station = True
|
||||||
|
self._with_system = True
|
||||||
self._with_upgrade = True
|
self._with_upgrade = True
|
||||||
self._with_utilisation = True
|
self._with_utilisation = True
|
||||||
self._with_information = True
|
|
||||||
self._with_surveillance_station = True
|
|
||||||
|
|
||||||
self._unsub_dispatcher = None
|
self._unsub_dispatcher = None
|
||||||
|
|
||||||
|
@ -320,6 +377,7 @@ class SynoApi:
|
||||||
self._fetching_entities.get(SynoCoreSecurity.API_KEY)
|
self._fetching_entities.get(SynoCoreSecurity.API_KEY)
|
||||||
)
|
)
|
||||||
self._with_storage = bool(self._fetching_entities.get(SynoStorage.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_upgrade = bool(self._fetching_entities.get(SynoCoreUpgrade.API_KEY))
|
||||||
self._with_utilisation = bool(
|
self._with_utilisation = bool(
|
||||||
self._fetching_entities.get(SynoCoreUtilization.API_KEY)
|
self._fetching_entities.get(SynoCoreUtilization.API_KEY)
|
||||||
|
@ -342,6 +400,10 @@ class SynoApi:
|
||||||
self.dsm.reset(self.storage)
|
self.dsm.reset(self.storage)
|
||||||
self.storage = None
|
self.storage = None
|
||||||
|
|
||||||
|
if not self._with_system:
|
||||||
|
self.dsm.reset(self.system)
|
||||||
|
self.system = None
|
||||||
|
|
||||||
if not self._with_upgrade:
|
if not self._with_upgrade:
|
||||||
self.dsm.reset(self.upgrade)
|
self.dsm.reset(self.upgrade)
|
||||||
self.upgrade = None
|
self.upgrade = None
|
||||||
|
@ -369,12 +431,29 @@ class SynoApi:
|
||||||
if self._with_upgrade:
|
if self._with_upgrade:
|
||||||
self.upgrade = self.dsm.upgrade
|
self.upgrade = self.dsm.upgrade
|
||||||
|
|
||||||
|
if self._with_system:
|
||||||
|
self.system = self.dsm.system
|
||||||
|
|
||||||
if self._with_utilisation:
|
if self._with_utilisation:
|
||||||
self.utilisation = self.dsm.utilisation
|
self.utilisation = self.dsm.utilisation
|
||||||
|
|
||||||
if self._with_surveillance_station:
|
if self._with_surveillance_station:
|
||||||
self.surveillance_station = self.dsm.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):
|
async def async_unload(self):
|
||||||
"""Stop interacting with the NAS and prepare for removal from hass."""
|
"""Stop interacting with the NAS and prepare for removal from hass."""
|
||||||
self._unsub_dispatcher()
|
self._unsub_dispatcher()
|
||||||
|
@ -382,8 +461,17 @@ class SynoApi:
|
||||||
async def async_update(self, now=None):
|
async def async_update(self, now=None):
|
||||||
"""Update function for updating API information."""
|
"""Update function for updating API information."""
|
||||||
self._async_setup_api_requests()
|
self._async_setup_api_requests()
|
||||||
await self._hass.async_add_executor_job(self.dsm.update, self._with_information)
|
try:
|
||||||
async_dispatcher_send(self._hass, self.signal_sensor_update)
|
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):
|
class SynologyDSMEntity(Entity):
|
||||||
|
|
|
@ -164,8 +164,10 @@ class SynologyDSMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
if errors:
|
if errors:
|
||||||
return await self._show_setup_form(user_input, 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)
|
await self.async_set_unique_id(serial, raise_on_progress=False)
|
||||||
|
|
||||||
|
# Check if already configured
|
||||||
self._abort_if_unique_id_configured()
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
config_data = {
|
config_data = {
|
||||||
|
|
|
@ -25,6 +25,7 @@ SYNO_API = "syno_api"
|
||||||
UNDO_UPDATE_LISTENER = "undo_update_listener"
|
UNDO_UPDATE_LISTENER = "undo_update_listener"
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
|
CONF_SERIAL = "serial"
|
||||||
CONF_VOLUMES = "volumes"
|
CONF_VOLUMES = "volumes"
|
||||||
|
|
||||||
DEFAULT_USE_SSL = True
|
DEFAULT_USE_SSL = True
|
||||||
|
@ -42,6 +43,14 @@ ENTITY_ICON = "icon"
|
||||||
ENTITY_CLASS = "device_class"
|
ENTITY_CLASS = "device_class"
|
||||||
ENTITY_ENABLE = "enable"
|
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
|
# Entity keys should start with the API_KEY to fetch
|
||||||
|
|
||||||
# Binary sensors
|
# 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 import data_entry_flow, setup
|
||||||
from homeassistant.components import ssdp
|
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.config_flow import CONF_OTP_CODE
|
||||||
from homeassistant.components.synology_dsm.const import (
|
from homeassistant.components.synology_dsm.const import (
|
||||||
CONF_VOLUMES,
|
CONF_VOLUMES,
|
||||||
|
@ -20,6 +21,7 @@ from homeassistant.components.synology_dsm.const import (
|
||||||
DEFAULT_USE_SSL,
|
DEFAULT_USE_SSL,
|
||||||
DEFAULT_VERIFY_SSL,
|
DEFAULT_VERIFY_SSL,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
SERVICES,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_SSDP, SOURCE_USER
|
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_SSDP, SOURCE_USER
|
||||||
from homeassistant.const import (
|
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 result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||||
assert config_entry.options[CONF_SCAN_INTERVAL] == 2
|
assert config_entry.options[CONF_SCAN_INTERVAL] == 2
|
||||||
assert config_entry.options[CONF_TIMEOUT] == 30
|
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
Add a link
Reference in a new issue