mirror of
https://github.com/Electric-Special/ha-core.git
synced 2026-03-21 07:05:48 +01:00
Add new USB drives to Synology DSM without reloading integration (#146829)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
@@ -6,7 +6,10 @@ 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.external_usb import (
|
||||
SynoCoreExternalUSB,
|
||||
SynoCoreExternalUSBDevice,
|
||||
)
|
||||
from synology_dsm.api.core.utilization import SynoCoreUtilization
|
||||
from synology_dsm.api.dsm.information import SynoDSMInformation
|
||||
from synology_dsm.api.storage.storage import SynoStorage
|
||||
@@ -343,14 +346,42 @@ async def async_setup_entry(
|
||||
coordinator = data.coordinator_central
|
||||
storage = api.storage
|
||||
assert storage is not None
|
||||
external_usb = api.external_usb
|
||||
known_usb_devices: set[str] = set()
|
||||
|
||||
entities: list[
|
||||
SynoDSMUtilSensor
|
||||
| SynoDSMStorageSensor
|
||||
| SynoDSMInfoSensor
|
||||
| SynoDSMExternalUSBSensor
|
||||
] = [
|
||||
def _check_usb_devices() -> None:
|
||||
"""Check for new USB devices during and after initial setup."""
|
||||
if api.external_usb is not None and api.external_usb.get_devices:
|
||||
current_usb_devices: set[str] = {
|
||||
device.device_name for device in api.external_usb.get_devices.values()
|
||||
}
|
||||
new_usb_devices = current_usb_devices - known_usb_devices
|
||||
if new_usb_devices:
|
||||
known_usb_devices.update(new_usb_devices)
|
||||
external_devices: list[SynoCoreExternalUSBDevice] = [
|
||||
device
|
||||
for device in api.external_usb.get_devices.values()
|
||||
if device.device_name in new_usb_devices
|
||||
]
|
||||
new_usb_entities: list[SynoDSMExternalUSBSensor] = [
|
||||
SynoDSMExternalUSBSensor(
|
||||
api, coordinator, description, device.device_name
|
||||
)
|
||||
for device in entry.data.get(CONF_DEVICES, external_devices)
|
||||
for description in EXTERNAL_USB_DISK_SENSORS
|
||||
]
|
||||
new_usb_entities.extend(
|
||||
[
|
||||
SynoDSMExternalUSBSensor(
|
||||
api, coordinator, description, partition.partition_title
|
||||
)
|
||||
for device in entry.data.get(CONF_DEVICES, external_devices)
|
||||
for partition in device.device_partitions.values()
|
||||
for description in EXTERNAL_USB_PARTITION_SENSORS
|
||||
]
|
||||
)
|
||||
async_add_entities(new_usb_entities)
|
||||
|
||||
entities: list[SynoDSMUtilSensor | SynoDSMStorageSensor | SynoDSMInfoSensor] = [
|
||||
SynoDSMUtilSensor(api, coordinator, description)
|
||||
for description in UTILISATION_SENSORS
|
||||
]
|
||||
@@ -375,32 +406,6 @@ 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)
|
||||
@@ -408,6 +413,9 @@ async def async_setup_entry(
|
||||
]
|
||||
)
|
||||
|
||||
_check_usb_devices()
|
||||
entry.async_on_unload(coordinator.async_add_listener(_check_usb_devices))
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ from __future__ import annotations
|
||||
from unittest.mock import AsyncMock, Mock
|
||||
|
||||
from awesomeversion import AwesomeVersion
|
||||
from synology_dsm.api.core.external_usb import SynoCoreExternalUSBDevice
|
||||
from synology_dsm.api.storage.storage import SynoStorageDisk, SynoStorageVolume
|
||||
|
||||
from .consts import SERIAL
|
||||
|
||||
@@ -30,3 +32,168 @@ def mock_dsm_information(
|
||||
temperature=temperature,
|
||||
uptime=uptime,
|
||||
)
|
||||
|
||||
|
||||
def mock_dsm_storage_get_volume(volume_id: str) -> SynoStorageVolume:
|
||||
"""Mock SynologyDSM storage volume information for a specific volume."""
|
||||
volumes = mock_dsm_storage_volumes()
|
||||
for volume in volumes:
|
||||
if volume.get("id") == volume_id:
|
||||
return volume
|
||||
raise ValueError(f"Volume with id {volume_id} not found in mock data.")
|
||||
|
||||
|
||||
def mock_dsm_storage_volumes() -> list[SynoStorageVolume]:
|
||||
"""Mock SynologyDSM storage volume information."""
|
||||
volumes_data = {
|
||||
"volume_1": {
|
||||
"id": "volume_1",
|
||||
"device_type": "btrfs",
|
||||
"size": {
|
||||
"free_inode": "1000000",
|
||||
"total": "24000277250048",
|
||||
"total_device": "24000277250048",
|
||||
"total_inode": "2000000",
|
||||
"used": "12000138625024",
|
||||
},
|
||||
"status": "normal",
|
||||
"fs_type": "btrfs",
|
||||
},
|
||||
}
|
||||
return [SynoStorageVolume(**volume_info) for volume_info in volumes_data.values()]
|
||||
|
||||
|
||||
def mock_dsm_storage_get_disk(disk_id: str) -> SynoStorageDisk:
|
||||
"""Mock SynologyDSM storage disk information for a specific disk."""
|
||||
disks = mock_dsm_storage_disks()
|
||||
for disk in disks:
|
||||
if disk.get("id") == disk_id:
|
||||
return disk
|
||||
raise ValueError(f"Disk with id {disk_id} not found in mock data.")
|
||||
|
||||
|
||||
def mock_dsm_storage_disks() -> list[SynoStorageDisk]:
|
||||
"""Mock SynologyDSM storage disk information."""
|
||||
disks_data = {
|
||||
"sata1": {
|
||||
"id": "sata1",
|
||||
"name": "Drive 1",
|
||||
"vendor": "Seagate",
|
||||
"model": "ST24000NT002-3N1101",
|
||||
"device": "/dev/sata1",
|
||||
"temp": 32,
|
||||
"size_total": "24000277250048",
|
||||
"firm": "EN01",
|
||||
"diskType": "SATA",
|
||||
},
|
||||
"sata2": {
|
||||
"id": "sata2",
|
||||
"name": "Drive 2",
|
||||
"vendor": "Seagate",
|
||||
"model": "ST24000NT002-3N1101",
|
||||
"device": "/dev/sata2",
|
||||
"temp": 32,
|
||||
"size_total": "24000277250048",
|
||||
"firm": "EN01",
|
||||
"diskType": "SATA",
|
||||
},
|
||||
"sata3": {
|
||||
"id": "sata3",
|
||||
"name": "Drive 3",
|
||||
"vendor": "Seagate",
|
||||
"model": "ST24000NT002-3N1101",
|
||||
"device": "/dev/sata3",
|
||||
"temp": 32,
|
||||
"size_total": "24000277250048",
|
||||
"firm": "EN01",
|
||||
"diskType": "SATA",
|
||||
},
|
||||
}
|
||||
return [SynoStorageDisk(**disk_info) for disk_info in disks_data.values()]
|
||||
|
||||
|
||||
def mock_dsm_external_usb_devices_usb1() -> dict[str, SynoCoreExternalUSBDevice]:
|
||||
"""Mock SynologyDSM external USB device with USB Disk 1."""
|
||||
return {
|
||||
"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": "usbshare2",
|
||||
"status": "normal",
|
||||
"total_size_mb": 15259646,
|
||||
"used_size_mb": 5942441,
|
||||
}
|
||||
],
|
||||
}
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def mock_dsm_external_usb_devices_usb2() -> dict[str, SynoCoreExternalUSBDevice]:
|
||||
"""Mock SynologyDSM external USB device with USB Disk 1 and USB Disk 2."""
|
||||
return {
|
||||
"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": "usbshare2",
|
||||
"status": "normal",
|
||||
"total_size_mb": 15259646,
|
||||
"used_size_mb": 5942441,
|
||||
}
|
||||
],
|
||||
}
|
||||
),
|
||||
"usb2": SynoCoreExternalUSBDevice(
|
||||
{
|
||||
"dev_id": "usb2",
|
||||
"dev_type": "usbDisk",
|
||||
"dev_title": "USB Disk 2",
|
||||
"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": "usb2p1",
|
||||
"partition_title": "USB Disk 2 Partition 1",
|
||||
"share_name": "usbshare2",
|
||||
"status": "normal",
|
||||
"total_size_mb": 15259646,
|
||||
"used_size_mb": 5942441,
|
||||
}
|
||||
],
|
||||
}
|
||||
),
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""Tests for Synology DSM USB."""
|
||||
|
||||
from itertools import chain
|
||||
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 (
|
||||
@@ -17,7 +17,13 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from .common import mock_dsm_information
|
||||
from .common import (
|
||||
mock_dsm_external_usb_devices_usb1,
|
||||
mock_dsm_external_usb_devices_usb2,
|
||||
mock_dsm_information,
|
||||
mock_dsm_storage_get_disk,
|
||||
mock_dsm_storage_get_volume,
|
||||
)
|
||||
from .consts import HOST, MACS, PASSWORD, PORT, SERIAL, USE_SSL, USERNAME
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
@@ -31,70 +37,33 @@ def mock_dsm_with_usb():
|
||||
dsm.update = AsyncMock(return_value=True)
|
||||
|
||||
dsm.surveillance_station.update = AsyncMock(return_value=True)
|
||||
dsm.upgrade.update = AsyncMock(return_value=True)
|
||||
dsm.upgrade = Mock(
|
||||
available_version=None,
|
||||
available_version_details=None,
|
||||
update=AsyncMock(return_value=True),
|
||||
)
|
||||
dsm.network = Mock(
|
||||
update=AsyncMock(return_value=True), macs=MACS, hostname=HOST
|
||||
)
|
||||
dsm.information = mock_dsm_information()
|
||||
dsm.storage = Mock(
|
||||
get_disk=mock_dsm_storage_get_disk,
|
||||
disk_temp=Mock(return_value=32),
|
||||
disks_ids=["sata1", "sata2", "sata3"],
|
||||
get_volume=mock_dsm_storage_get_volume,
|
||||
volume_disk_temp_avg=Mock(return_value=32),
|
||||
volume_size_used=Mock(return_value=12000138625024),
|
||||
volume_percentage_used=Mock(return_value=38),
|
||||
volumes_ids=["volume_1"],
|
||||
update=AsyncMock(return_value=True),
|
||||
)
|
||||
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,
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
},
|
||||
get_devices=mock_dsm_external_usb_devices_usb1(),
|
||||
)
|
||||
dsm.logout = AsyncMock(return_value=True)
|
||||
dsm.mock_entry = MockConfigEntry()
|
||||
yield dsm
|
||||
|
||||
|
||||
@@ -142,6 +111,8 @@ async def setup_dsm_with_usb(
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
mock_dsm_with_usb.mock_entry = entry
|
||||
|
||||
yield mock_dsm_with_usb
|
||||
|
||||
|
||||
@@ -233,6 +204,84 @@ async def test_external_usb(
|
||||
assert sensor.attributes["attribution"] == "Data provided by Synology"
|
||||
|
||||
|
||||
async def test_external_usb_new_device(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
setup_dsm_with_usb: MagicMock,
|
||||
) -> None:
|
||||
"""Test Synology DSM USB adding new device."""
|
||||
|
||||
expected_sensors_disk_1 = {
|
||||
"sensor.nas_meontheinternet_com_usb_disk_1_status": ("normal", {}),
|
||||
"sensor.nas_meontheinternet_com_usb_disk_1_partition_1_partition_size": (
|
||||
"14901.998046875",
|
||||
{},
|
||||
),
|
||||
"sensor.nas_meontheinternet_com_usb_disk_1_partition_1_partition_used_space": (
|
||||
"5803.1650390625",
|
||||
{},
|
||||
),
|
||||
"sensor.nas_meontheinternet_com_usb_disk_1_partition_1_partition_used": (
|
||||
"38.9",
|
||||
{},
|
||||
),
|
||||
}
|
||||
expected_sensors_disk_2 = {
|
||||
"sensor.nas_meontheinternet_com_usb_disk_2_status": ("normal", {}),
|
||||
"sensor.nas_meontheinternet_com_usb_disk_2_partition_1_partition_size": (
|
||||
"14901.998046875",
|
||||
{
|
||||
"device_class": "data_size",
|
||||
"state_class": "measurement",
|
||||
"unit_of_measurement": "GiB",
|
||||
"attribution": "Data provided by Synology",
|
||||
},
|
||||
),
|
||||
"sensor.nas_meontheinternet_com_usb_disk_2_partition_1_partition_used_space": (
|
||||
"5803.1650390625",
|
||||
{
|
||||
"device_class": "data_size",
|
||||
"state_class": "measurement",
|
||||
"unit_of_measurement": "GiB",
|
||||
"attribution": "Data provided by Synology",
|
||||
},
|
||||
),
|
||||
"sensor.nas_meontheinternet_com_usb_disk_2_partition_1_partition_used": (
|
||||
"38.9",
|
||||
{
|
||||
"state_class": "measurement",
|
||||
"unit_of_measurement": "%",
|
||||
"attribution": "Data provided by Synology",
|
||||
},
|
||||
),
|
||||
}
|
||||
|
||||
# Initial check of existing sensors
|
||||
for sensor_id, (expected_state, expected_attrs) in expected_sensors_disk_1.items():
|
||||
sensor = hass.states.get(sensor_id)
|
||||
assert sensor is not None
|
||||
assert sensor.state == expected_state
|
||||
for attr_key, attr_value in expected_attrs.items():
|
||||
assert sensor.attributes[attr_key] == attr_value
|
||||
for sensor_id in expected_sensors_disk_2:
|
||||
assert hass.states.get(sensor_id) is None
|
||||
|
||||
# Mock the get_devices method to simulate a USB disk being added
|
||||
setup_dsm_with_usb.external_usb.get_devices = mock_dsm_external_usb_devices_usb2()
|
||||
# Coordinator refresh
|
||||
await setup_dsm_with_usb.mock_entry.runtime_data.coordinator_central.async_request_refresh()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
for sensor_id, (expected_state, expected_attrs) in chain(
|
||||
expected_sensors_disk_1.items(), expected_sensors_disk_2.items()
|
||||
):
|
||||
sensor = hass.states.get(sensor_id)
|
||||
assert sensor is not None
|
||||
assert sensor.state == expected_state
|
||||
for attr_key, attr_value in expected_attrs.items():
|
||||
assert sensor.attributes[attr_key] == attr_value
|
||||
|
||||
|
||||
async def test_no_external_usb(
|
||||
hass: HomeAssistant,
|
||||
setup_dsm_without_usb: MagicMock,
|
||||
|
||||
Reference in New Issue
Block a user