mirror of
https://github.com/home-assistant/core.git
synced 2025-07-15 09:17:10 +00:00
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:
parent
15aff9662c
commit
9ce920b35a
@ -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:
|
||||
|
@ -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"]] = {
|
||||
|
@ -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(
|
||||
|
@ -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": {
|
||||
|
@ -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."""
|
||||
|
||||
|
@ -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%]"
|
||||
},
|
||||
|
@ -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,
|
||||
)
|
||||
|
130
tests/components/synology_dsm/snapshots/test_diagnostics.ambr
Normal file
130
tests/components/synology_dsm/snapshots/test_diagnostics.ambr
Normal 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,
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
})
|
||||
# ---
|
199
tests/components/synology_dsm/test_diagnostics.py
Normal file
199
tests/components/synology_dsm/test_diagnostics.py
Normal 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")
|
||||
)
|
242
tests/components/synology_dsm/test_sensor.py
Normal file
242
tests/components/synology_dsm/test_sensor.py
Normal 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
|
Loading…
x
Reference in New Issue
Block a user