Add support for external USB drives to Synology DSM (#138661)

* Add external usb drives

* Add partition percentage used

* Move icons to icons.json

* Add external usb to diagnostics

* Add assert for external usb entity

* Fix reset external_usb

* Update homeassistant/components/synology_dsm/diagnostics.py

Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com>

* Update homeassistant/components/synology_dsm/diagnostics.py

Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com>

* Fix diagnostics

* Make each partition a device

* Add usb sensor tests

* Add diagnostics tests

* It is possible that api.external_usb is None

* Merge upstream into syno_external_usb

* add manufacturer and model to partition

* fix tests

---------

Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com>
Co-authored-by: mib1185 <mail@mib85.de>
This commit is contained in:
Patrick 2025-04-29 07:32:21 -04:00 committed by GitHub
parent 15aff9662c
commit 9ce920b35a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 797 additions and 2 deletions

View File

@ -9,6 +9,7 @@ import logging
from awesomeversion import AwesomeVersion
from synology_dsm import SynologyDSM
from synology_dsm.api.core.external_usb import SynoCoreExternalUSB
from synology_dsm.api.core.security import SynoCoreSecurity
from synology_dsm.api.core.system import SynoCoreSystem
from synology_dsm.api.core.upgrade import SynoCoreUpgrade
@ -78,6 +79,7 @@ class SynoApi:
self.system: SynoCoreSystem | None = None
self.upgrade: SynoCoreUpgrade | None = None
self.utilisation: SynoCoreUtilization | None = None
self.external_usb: SynoCoreExternalUSB | None = None
# Should we fetch them
self._fetching_entities: dict[str, set[str]] = {}
@ -90,6 +92,7 @@ class SynoApi:
self._with_system = True
self._with_upgrade = True
self._with_utilisation = True
self._with_external_usb = True
self._login_future: asyncio.Future[None] | None = None
@ -261,6 +264,9 @@ class SynoApi:
self._with_information = bool(
self._fetching_entities.get(SynoDSMInformation.API_KEY)
)
self._with_external_usb = bool(
self._fetching_entities.get(SynoCoreExternalUSB.API_KEY)
)
# Reset not used API, information is not reset since it's used in device_info
if not self._with_security:
@ -322,6 +328,15 @@ class SynoApi:
self.dsm.reset(self.utilisation)
self.utilisation = None
if not self._with_external_usb:
LOGGER.debug(
"Disable external usb api from being updated for '%s'",
self._entry.unique_id,
)
if self.external_usb:
self.dsm.reset(self.external_usb)
self.external_usb = None
async def _fetch_device_configuration(self) -> None:
"""Fetch initial device config."""
self.network = self.dsm.network
@ -366,6 +381,12 @@ class SynoApi:
)
self.surveillance_station = self.dsm.surveillance_station
if self._with_external_usb:
LOGGER.debug(
"Enable external usb api updates for '%s'", self._entry.unique_id
)
self.external_usb = self.dsm.external_usb
async def _syno_api_executer(self, api_call: Callable) -> None:
"""Synology api call wrapper."""
try:

View File

@ -32,6 +32,7 @@ async def async_get_config_entry_diagnostics(
"uptime": dsm_info.uptime,
"temperature": dsm_info.temperature,
},
"external_usb": {"devices": {}, "partitions": {}},
"network": {"interfaces": {}},
"storage": {"disks": {}, "volumes": {}},
"surveillance_station": {"cameras": {}, "camera_diagnostics": {}},
@ -43,6 +44,27 @@ async def async_get_config_entry_diagnostics(
},
}
if syno_api.external_usb is not None:
for device in syno_api.external_usb.get_devices.values():
if device is not None:
diag_data["external_usb"]["devices"][device.device_id] = {
"name": device.device_name,
"manufacturer": device.device_manufacturer,
"model": device.device_product_name,
"type": device.device_type,
"status": device.device_status,
"size_total": device.device_size_total(False),
}
for partition in device.device_partitions.values():
if partition is not None:
diag_data["external_usb"]["partitions"][partition.name_id] = {
"name": partition.partition_title,
"filesystem": partition.filesystem,
"share_name": partition.share_name,
"size_used": partition.partition_size_used(False),
"size_total": partition.partition_size_total(False),
}
if syno_api.network is not None:
for intf in syno_api.network.interfaces:
diag_data["network"]["interfaces"][intf["id"]] = {

View File

@ -93,6 +93,7 @@ class SynologyDSMDeviceEntity(
storage = api.storage
information = api.information
network = api.network
external_usb = api.external_usb
assert information is not None
assert storage is not None
assert network is not None
@ -121,6 +122,26 @@ class SynologyDSMDeviceEntity(
self._device_model = disk["model"].strip()
self._device_firmware = disk["firm"]
self._device_type = disk["diskType"]
elif "device" in description.key:
assert self._device_id is not None
assert external_usb is not None
for device in external_usb.get_devices.values():
if device.device_name == self._device_id:
self._device_name = device.device_name
self._device_manufacturer = device.device_manufacturer
self._device_model = device.device_product_name
self._device_type = device.device_type
break
elif "partition" in description.key:
assert self._device_id is not None
assert external_usb is not None
for device in external_usb.get_devices.values():
for partition in device.device_partitions.values():
if partition.partition_title == self._device_id:
self._device_name = partition.partition_title
self._device_manufacturer = "Synology"
self._device_model = partition.filesystem
break
self._attr_unique_id += f"_{self._device_id}"
self._attr_device_info = DeviceInfo(

View File

@ -22,6 +22,12 @@
"cpu_15min_load": {
"default": "mdi:chip"
},
"device_size_total": {
"default": "mdi:chart-pie"
},
"device_status": {
"default": "mdi:checkbox-marked-circle-outline"
},
"memory_real_usage": {
"default": "mdi:memory"
},
@ -49,6 +55,15 @@
"network_down": {
"default": "mdi:download"
},
"partition_percentage_used": {
"default": "mdi:chart-pie"
},
"partition_size_total": {
"default": "mdi:chart-pie"
},
"partition_size_used": {
"default": "mdi:chart-pie"
},
"volume_status": {
"default": "mdi:checkbox-marked-circle-outline",
"state": {

View File

@ -6,6 +6,7 @@ from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import cast
from synology_dsm.api.core.external_usb import SynoCoreExternalUSB
from synology_dsm.api.core.utilization import SynoCoreUtilization
from synology_dsm.api.dsm.information import SynoDSMInformation
from synology_dsm.api.storage.storage import SynoStorage
@ -17,6 +18,7 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.const import (
CONF_DEVICES,
CONF_DISKS,
PERCENTAGE,
EntityCategory,
@ -261,6 +263,53 @@ STORAGE_DISK_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = (
entity_category=EntityCategory.DIAGNOSTIC,
),
)
EXTERNAL_USB_DISK_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = (
SynologyDSMSensorEntityDescription(
api_key=SynoCoreExternalUSB.API_KEY,
key="device_status",
translation_key="device_status",
),
SynologyDSMSensorEntityDescription(
api_key=SynoCoreExternalUSB.API_KEY,
key="device_size_total",
translation_key="device_size_total",
native_unit_of_measurement=UnitOfInformation.BYTES,
suggested_unit_of_measurement=UnitOfInformation.GIBIBYTES,
suggested_display_precision=2,
device_class=SensorDeviceClass.DATA_SIZE,
entity_registry_enabled_default=False,
state_class=SensorStateClass.MEASUREMENT,
),
)
EXTERNAL_USB_PARTITION_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = (
SynologyDSMSensorEntityDescription(
api_key=SynoCoreExternalUSB.API_KEY,
key="partition_size_total",
translation_key="partition_size_total",
native_unit_of_measurement=UnitOfInformation.BYTES,
suggested_unit_of_measurement=UnitOfInformation.GIBIBYTES,
suggested_display_precision=2,
device_class=SensorDeviceClass.DATA_SIZE,
state_class=SensorStateClass.MEASUREMENT,
),
SynologyDSMSensorEntityDescription(
api_key=SynoCoreExternalUSB.API_KEY,
key="partition_size_used",
translation_key="partition_size_used",
native_unit_of_measurement=UnitOfInformation.BYTES,
suggested_unit_of_measurement=UnitOfInformation.GIBIBYTES,
suggested_display_precision=2,
device_class=SensorDeviceClass.DATA_SIZE,
state_class=SensorStateClass.MEASUREMENT,
),
SynologyDSMSensorEntityDescription(
api_key=SynoCoreExternalUSB.API_KEY,
key="partition_percentage_used",
translation_key="partition_percentage_used",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
)
INFORMATION_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = (
SynologyDSMSensorEntityDescription(
@ -294,8 +343,14 @@ async def async_setup_entry(
coordinator = data.coordinator_central
storage = api.storage
assert storage is not None
external_usb = api.external_usb
entities: list[SynoDSMUtilSensor | SynoDSMStorageSensor | SynoDSMInfoSensor] = [
entities: list[
SynoDSMUtilSensor
| SynoDSMStorageSensor
| SynoDSMInfoSensor
| SynoDSMExternalUSBSensor
] = [
SynoDSMUtilSensor(api, coordinator, description)
for description in UTILISATION_SENSORS
]
@ -320,6 +375,32 @@ async def async_setup_entry(
]
)
# Handle all external usb
if external_usb is not None and external_usb.get_devices:
entities.extend(
[
SynoDSMExternalUSBSensor(
api, coordinator, description, device.device_name
)
for device in entry.data.get(
CONF_DEVICES, external_usb.get_devices.values()
)
for description in EXTERNAL_USB_DISK_SENSORS
]
)
entities.extend(
[
SynoDSMExternalUSBSensor(
api, coordinator, description, partition.partition_title
)
for device in entry.data.get(
CONF_DEVICES, external_usb.get_devices.values()
)
for partition in device.device_partitions.values()
for description in EXTERNAL_USB_PARTITION_SENSORS
]
)
entities.extend(
[
SynoDSMInfoSensor(api, coordinator, description)
@ -396,6 +477,45 @@ class SynoDSMStorageSensor(SynologyDSMDeviceEntity, SynoDSMSensor):
)
class SynoDSMExternalUSBSensor(SynologyDSMDeviceEntity, SynoDSMSensor):
"""Representation a Synology Storage sensor."""
entity_description: SynologyDSMSensorEntityDescription
def __init__(
self,
api: SynoApi,
coordinator: SynologyDSMCentralUpdateCoordinator,
description: SynologyDSMSensorEntityDescription,
device_id: str | None = None,
) -> None:
"""Initialize the Synology DSM external usb sensor entity."""
super().__init__(api, coordinator, description, device_id)
@property
def native_value(self) -> StateType:
"""Return the state."""
external_usb = self._api.external_usb
assert external_usb is not None
if "device" in self.entity_description.key:
for device in external_usb.get_devices.values():
if device.device_name == self._device_id:
attr = getattr(device, self.entity_description.key)
break
elif "partition" in self.entity_description.key:
for device in external_usb.get_devices.values():
for partition in device.device_partitions.values():
if partition.partition_title == self._device_id:
attr = getattr(partition, self.entity_description.key)
break
if callable(attr):
attr = attr()
if attr is None:
return None
return attr # type: ignore[no-any-return]
class SynoDSMInfoSensor(SynoDSMSensor):
"""Representation a Synology information sensor."""

View File

@ -113,6 +113,12 @@
"cpu_user_load": {
"name": "CPU utilization (user)"
},
"device_size_total": {
"name": "Device size"
},
"device_status": {
"name": "Status"
},
"disk_smart_status": {
"name": "Status (smart)"
},
@ -149,6 +155,15 @@
"network_up": {
"name": "Upload throughput"
},
"partition_percentage_used": {
"name": "Partition used"
},
"partition_size_total": {
"name": "Partition size"
},
"partition_size_used": {
"name": "Partition used space"
},
"temperature": {
"name": "[%key:component::sensor::entity_component::temperature::name%]"
},

View File

@ -12,11 +12,21 @@ from .consts import SERIAL
def mock_dsm_information(
serial: str | None = SERIAL,
update_result: bool = True,
awesome_version: str = "7.2",
awesome_version: str = "7.2.2",
model: str = "DS1821+",
version_string: str = "DSM 7.2.2-72806 Update 3",
ram: int = 32768,
temperature: int = 58,
uptime: int = 123456,
) -> Mock:
"""Mock SynologyDSM information."""
return Mock(
serial=serial,
update=AsyncMock(return_value=update_result),
awesome_version=AwesomeVersion(awesome_version),
model=model,
version_string=version_string,
ram=ram,
temperature=temperature,
uptime=uptime,
)

View File

@ -0,0 +1,130 @@
# serializer version: 1
# name: test_entry_diagnostics
dict({
'device_info': dict({
'model': 'DS1821+',
'ram': 32768,
'temperature': 58,
'uptime': 123456,
'version': 'DSM 7.2.2-72806 Update 3',
}),
'entry': dict({
'data': dict({
'host': 'nas.meontheinternet.com',
'mac': '00-11-32-XX-XX-59',
'password': '**REDACTED**',
'port': 1234,
'ssl': True,
'username': '**REDACTED**',
'verify_ssl': False,
}),
'disabled_by': None,
'discovery_keys': dict({
}),
'domain': 'synology_dsm',
'minor_version': 1,
'options': dict({
'backup_path': None,
'backup_share': None,
}),
'pref_disable_new_entities': False,
'pref_disable_polling': False,
'source': 'user',
'subentries': list([
]),
'title': 'Mock Title',
'unique_id': 'mySerial',
'version': 1,
}),
'external_usb': dict({
'devices': dict({
'usb1': dict({
'manufacturer': 'Western Digital Technologies, Inc.',
'model': 'easystore 264D',
'name': 'USB Disk 1',
'size_total': 16000900661248,
'status': 'normal',
'type': 'usbDisk',
}),
}),
'partitions': dict({
'usb1p1': dict({
'filesystem': 'ntfs',
'name': 'USB Disk 1 Partition 1',
'share_name': 'usbshare1',
'size_total': 16000898564096,
'size_used': 6231101014016,
}),
}),
}),
'is_system_loaded': True,
'network': dict({
'interfaces': dict({
'ovs_eth0': dict({
'ip': list([
dict({
'address': '127.0.0.1',
'netmask': '255.255.255.0',
}),
]),
'type': 'ovseth',
}),
}),
}),
'storage': dict({
'disks': dict({
}),
'volumes': dict({
}),
}),
'surveillance_station': dict({
'camera_diagnostics': dict({
}),
'cameras': dict({
}),
}),
'upgrade': dict({
'available_version': None,
'reboot_needed': None,
'service_restarts': None,
'update_available': False,
}),
'utilisation': dict({
'cpu': dict({
'15min_load': 461,
'1min_load': 410,
'5min_load': 404,
'device': 'System',
'other_load': 5,
'system_load': 11,
'user_load': 11,
}),
'memory': dict({
'avail_real': 463628,
'avail_swap': 0,
'buffer': 10556600,
'cached': 5297776,
'device': 'Memory',
'memory_size': 33554432,
'real_usage': 50,
'si_disk': 0,
'so_disk': 0,
'swap_usage': 100,
'total_real': 32841680,
'total_swap': 2097084,
}),
'network': list([
dict({
'device': 'total',
'rx': 1065612,
'tx': 36311,
}),
dict({
'device': 'eth0',
'rx': 1065612,
'tx': 36311,
}),
]),
}),
})
# ---

View File

@ -0,0 +1,199 @@
"""Test Synology DSM diagnostics."""
from unittest.mock import AsyncMock, MagicMock, Mock, patch
import pytest
from synology_dsm.api.core.external_usb import SynoCoreExternalUSBDevice
from synology_dsm.api.dsm.network import NetworkInterface
from syrupy.assertion import SnapshotAssertion
from syrupy.filters import props
from homeassistant.components.synology_dsm.const import DOMAIN
from homeassistant.const import (
CONF_HOST,
CONF_MAC,
CONF_PASSWORD,
CONF_PORT,
CONF_SSL,
CONF_USERNAME,
)
from homeassistant.core import HomeAssistant
from .common import mock_dsm_information
from .consts import HOST, MACS, PASSWORD, PORT, SERIAL, USE_SSL, USERNAME
from tests.common import MockConfigEntry
from tests.components.diagnostics import get_diagnostics_for_config_entry
from tests.typing import ClientSessionGenerator
@pytest.fixture
def mock_dsm_with_usb():
"""Mock a successful service with USB support."""
with patch("homeassistant.components.synology_dsm.common.SynologyDSM") as dsm:
dsm.login = AsyncMock(return_value=True)
dsm.update = AsyncMock(return_value=True)
dsm.surveillance_station.update = AsyncMock(return_value=True)
dsm.upgrade = Mock(
update_available=False,
available_version=None,
reboot_needed=None,
service_restarts=None,
update=AsyncMock(return_value=True),
)
dsm.utilisation = Mock(
cpu={
"15min_load": 461,
"1min_load": 410,
"5min_load": 404,
"device": "System",
"other_load": 5,
"system_load": 11,
"user_load": 11,
},
memory={
"avail_real": 463628,
"avail_swap": 0,
"buffer": 10556600,
"cached": 5297776,
"device": "Memory",
"memory_size": 33554432,
"real_usage": 50,
"si_disk": 0,
"so_disk": 0,
"swap_usage": 100,
"total_real": 32841680,
"total_swap": 2097084,
},
network=[
{"device": "total", "rx": 1065612, "tx": 36311},
{"device": "eth0", "rx": 1065612, "tx": 36311},
],
memory_available_swap=Mock(return_value=0),
memory_available_real=Mock(return_value=463628),
memory_total_swap=Mock(return_value=2097084),
memory_total_real=Mock(return_value=32841680),
network_up=Mock(return_value=1065612),
network_down=Mock(return_value=36311),
update=AsyncMock(return_value=True),
)
dsm.network = Mock(
update=AsyncMock(return_value=True),
macs=MACS,
hostname=HOST,
interfaces=[
NetworkInterface(
{
"id": "ovs_eth0",
"ip": [{"address": "127.0.0.1", "netmask": "255.255.255.0"}],
"type": "ovseth",
}
)
],
)
dsm.information = mock_dsm_information()
dsm.file = Mock(get_shared_folders=AsyncMock(return_value=None))
dsm.external_usb = Mock(
update=AsyncMock(return_value=True),
get_device=Mock(
return_value=SynoCoreExternalUSBDevice(
{
"dev_id": "usb1",
"dev_type": "usbDisk",
"dev_title": "USB Disk 1",
"producer": "Western Digital Technologies, Inc.",
"product": "easystore 264D",
"formatable": True,
"progress": "",
"status": "normal",
"total_size_mb": 15259648,
"partitions": [
{
"dev_fstype": "ntfs",
"filesystem": "ntfs",
"name_id": "usb1p1",
"partition_title": "USB Disk 1 Partition 1",
"share_name": "usbshare1",
"status": "normal",
"total_size_mb": 15259646,
"used_size_mb": 5942441,
}
],
}
)
),
get_devices={
"usb1": SynoCoreExternalUSBDevice(
{
"dev_id": "usb1",
"dev_type": "usbDisk",
"dev_title": "USB Disk 1",
"producer": "Western Digital Technologies, Inc.",
"product": "easystore 264D",
"formatable": True,
"progress": "",
"status": "normal",
"total_size_mb": 15259648,
"partitions": [
{
"dev_fstype": "ntfs",
"filesystem": "ntfs",
"name_id": "usb1p1",
"partition_title": "USB Disk 1 Partition 1",
"share_name": "usbshare1",
"status": "normal",
"total_size_mb": 15259646,
"used_size_mb": 5942441,
}
],
}
)
},
)
dsm.logout = AsyncMock(return_value=True)
yield dsm
@pytest.fixture
async def setup_dsm_with_usb(
hass: HomeAssistant,
mock_dsm_with_usb: MagicMock,
):
"""Mock setup of synology dsm config entry with USB."""
with patch(
"homeassistant.components.synology_dsm.common.SynologyDSM",
return_value=mock_dsm_with_usb,
):
entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_HOST: HOST,
CONF_PORT: PORT,
CONF_SSL: USE_SSL,
CONF_USERNAME: USERNAME,
CONF_PASSWORD: PASSWORD,
CONF_MAC: MACS[0],
},
unique_id=SERIAL,
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
yield mock_dsm_with_usb
async def test_entry_diagnostics(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
setup_dsm_with_usb: MagicMock,
snapshot: SnapshotAssertion,
) -> None:
"""Test diagnostics for Synology DSM config entry."""
config_entry = hass.config_entries.async_entries(DOMAIN)[0]
result = await get_diagnostics_for_config_entry(hass, hass_client, config_entry)
assert result == snapshot(
exclude=props("api_details", "created_at", "modified_at", "entry_id")
)

View File

@ -0,0 +1,242 @@
"""Tests for Synology DSM USB."""
from unittest.mock import AsyncMock, MagicMock, Mock, patch
import pytest
from synology_dsm.api.core.external_usb import SynoCoreExternalUSBDevice
from homeassistant.components.synology_dsm.const import DOMAIN
from homeassistant.const import (
CONF_HOST,
CONF_MAC,
CONF_PASSWORD,
CONF_PORT,
CONF_SSL,
CONF_USERNAME,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from .common import mock_dsm_information
from .consts import HOST, MACS, PASSWORD, PORT, SERIAL, USE_SSL, USERNAME
from tests.common import MockConfigEntry
@pytest.fixture
def mock_dsm_with_usb():
"""Mock a successful service with USB support."""
with patch("homeassistant.components.synology_dsm.common.SynologyDSM") as dsm:
dsm.login = AsyncMock(return_value=True)
dsm.update = AsyncMock(return_value=True)
dsm.surveillance_station.update = AsyncMock(return_value=True)
dsm.upgrade.update = AsyncMock(return_value=True)
dsm.network = Mock(
update=AsyncMock(return_value=True), macs=MACS, hostname=HOST
)
dsm.information = mock_dsm_information()
dsm.file = Mock(get_shared_folders=AsyncMock(return_value=None))
dsm.external_usb = Mock(
update=AsyncMock(return_value=True),
get_device=Mock(
return_value=SynoCoreExternalUSBDevice(
{
"dev_id": "usb1",
"dev_type": "usbDisk",
"dev_title": "USB Disk 1",
"producer": "Western Digital Technologies, Inc.",
"product": "easystore 264D",
"formatable": True,
"progress": "",
"status": "normal",
"total_size_mb": 15259648,
"partitions": [
{
"dev_fstype": "ntfs",
"filesystem": "ntfs",
"name_id": "usb1p1",
"partition_title": "USB Disk 1 Partition 1",
"share_name": "usbshare1",
"status": "normal",
"total_size_mb": 15259646,
"used_size_mb": 5942441,
}
],
}
)
),
get_devices={
"usb1": SynoCoreExternalUSBDevice(
{
"dev_id": "usb1",
"dev_type": "usbDisk",
"dev_title": "USB Disk 1",
"producer": "Western Digital Technologies, Inc.",
"product": "easystore 264D",
"formatable": True,
"progress": "",
"status": "normal",
"total_size_mb": 15259648,
"partitions": [
{
"dev_fstype": "ntfs",
"filesystem": "ntfs",
"name_id": "usb1p1",
"partition_title": "USB Disk 1 Partition 1",
"share_name": "usbshare1",
"status": "normal",
"total_size_mb": 15259646,
"used_size_mb": 5942441,
}
],
}
)
},
)
dsm.logout = AsyncMock(return_value=True)
yield dsm
@pytest.fixture
def mock_dsm_without_usb():
"""Mock a successful service without USB devices."""
with patch("homeassistant.components.synology_dsm.common.SynologyDSM") as dsm:
dsm.login = AsyncMock(return_value=True)
dsm.update = AsyncMock(return_value=True)
dsm.surveillance_station.update = AsyncMock(return_value=True)
dsm.upgrade.update = AsyncMock(return_value=True)
dsm.network = Mock(
update=AsyncMock(return_value=True), macs=MACS, hostname=HOST
)
dsm.information = mock_dsm_information()
dsm.file = Mock(get_shared_folders=AsyncMock(return_value=None))
dsm.logout = AsyncMock(return_value=True)
yield dsm
@pytest.fixture
async def setup_dsm_with_usb(
hass: HomeAssistant,
mock_dsm_with_usb: MagicMock,
):
"""Mock setup of synology dsm config entry with USB."""
with patch(
"homeassistant.components.synology_dsm.common.SynologyDSM",
return_value=mock_dsm_with_usb,
):
entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_HOST: HOST,
CONF_PORT: PORT,
CONF_SSL: USE_SSL,
CONF_USERNAME: USERNAME,
CONF_PASSWORD: PASSWORD,
CONF_MAC: MACS[0],
},
unique_id=SERIAL,
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
yield mock_dsm_with_usb
@pytest.fixture
async def setup_dsm_without_usb(
hass: HomeAssistant,
mock_dsm_without_usb: MagicMock,
):
"""Mock setup of synology dsm config entry without USB."""
with patch(
"homeassistant.components.synology_dsm.common.SynologyDSM",
return_value=mock_dsm_without_usb,
):
entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_HOST: HOST,
CONF_PORT: PORT,
CONF_SSL: USE_SSL,
CONF_USERNAME: USERNAME,
CONF_PASSWORD: PASSWORD,
CONF_MAC: MACS[0],
},
unique_id=SERIAL,
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
yield mock_dsm_without_usb
async def test_external_usb(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
setup_dsm_with_usb: MagicMock,
) -> None:
"""Test Synology DSM USB sensors."""
# test disabled device size sensor
entity_id = "sensor.nas_meontheinternet_com_usb_disk_1_device_size"
entity_entry = entity_registry.async_get(entity_id)
assert entity_entry
assert entity_entry.disabled
assert entity_entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION
# test partition size sensor
sensor = hass.states.get(
"sensor.nas_meontheinternet_com_usb_disk_1_partition_1_partition_size"
)
assert sensor is not None
assert sensor.state == "14901.998046875"
assert (
sensor.attributes["friendly_name"]
== "nas.meontheinternet.com (USB Disk 1 Partition 1) Partition size"
)
assert sensor.attributes["device_class"] == "data_size"
assert sensor.attributes["state_class"] == "measurement"
assert sensor.attributes["unit_of_measurement"] == "GiB"
assert sensor.attributes["attribution"] == "Data provided by Synology"
# test partition used space sensor
sensor = hass.states.get(
"sensor.nas_meontheinternet_com_usb_disk_1_partition_1_partition_used_space"
)
assert sensor is not None
assert sensor.state == "5803.1650390625"
assert (
sensor.attributes["friendly_name"]
== "nas.meontheinternet.com (USB Disk 1 Partition 1) Partition used space"
)
assert sensor.attributes["device_class"] == "data_size"
assert sensor.attributes["state_class"] == "measurement"
assert sensor.attributes["unit_of_measurement"] == "GiB"
assert sensor.attributes["attribution"] == "Data provided by Synology"
# test partition used sensor
sensor = hass.states.get(
"sensor.nas_meontheinternet_com_usb_disk_1_partition_1_partition_used"
)
assert sensor is not None
assert sensor.state == "38.9"
assert (
sensor.attributes["friendly_name"]
== "nas.meontheinternet.com (USB Disk 1 Partition 1) Partition used"
)
assert sensor.attributes["state_class"] == "measurement"
assert sensor.attributes["unit_of_measurement"] == "%"
assert sensor.attributes["attribution"] == "Data provided by Synology"
async def test_no_external_usb(
hass: HomeAssistant,
setup_dsm_without_usb: MagicMock,
) -> None:
"""Test Synology DSM without USB."""
sensor = hass.states.get("sensor.nas_meontheinternet_com_usb_disk_1_device_size")
assert sensor is None