1
mirror of https://github.com/home-assistant/core synced 2024-07-30 21:18:57 +02:00

Add raid array degraded state binary sensor to freebox sensors (#95242)

Add raid array degraded state binary sensor
This commit is contained in:
Florent Thiery 2023-07-05 15:09:12 +02:00 committed by GitHub
parent c75c79962a
commit bd7057f7b1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 296 additions and 36 deletions

View File

@ -0,0 +1,100 @@
"""Support for Freebox devices (Freebox v6 and Freebox mini 4K)."""
from __future__ import annotations
import logging
from typing import Any
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
EntityCategory,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from .router import FreeboxRouter
_LOGGER = logging.getLogger(__name__)
RAID_SENSORS: tuple[BinarySensorEntityDescription, ...] = (
BinarySensorEntityDescription(
key="raid_degraded",
name="degraded",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
),
)
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the binary sensors."""
router: FreeboxRouter = hass.data[DOMAIN][entry.unique_id]
_LOGGER.debug("%s - %s - %s raid(s)", router.name, router.mac, len(router.raids))
binary_entities = [
FreeboxRaidDegradedSensor(router, raid, description)
for raid in router.raids.values()
for description in RAID_SENSORS
]
if binary_entities:
async_add_entities(binary_entities, True)
class FreeboxRaidDegradedSensor(BinarySensorEntity):
"""Representation of a Freebox raid sensor."""
_attr_should_poll = False
_attr_has_entity_name = True
def __init__(
self,
router: FreeboxRouter,
raid: dict[str, Any],
description: BinarySensorEntityDescription,
) -> None:
"""Initialize a Freebox raid degraded sensor."""
self.entity_description = description
self._router = router
self._attr_device_info = router.device_info
self._raid = raid
self._attr_name = f"Raid array {raid['id']} {description.name}"
self._attr_unique_id = (
f"{router.mac} {description.key} {raid['name']} {raid['id']}"
)
@callback
def async_update_state(self) -> None:
"""Update the Freebox Raid sensor."""
self._raid = self._router.raids[self._raid["id"]]
@property
def is_on(self) -> bool:
"""Return true if degraded."""
return self._raid["degraded"]
@callback
def async_on_demand_update(self):
"""Update state."""
self.async_update_state()
self.async_write_ha_state()
async def async_added_to_hass(self) -> None:
"""Register state update callback."""
self.async_update_state()
self.async_on_remove(
async_dispatcher_connect(
self.hass,
self._router.signal_sensor_update,
self.async_on_demand_update,
)
)

View File

@ -20,6 +20,7 @@ PLATFORMS = [
Platform.BUTTON,
Platform.DEVICE_TRACKER,
Platform.SENSOR,
Platform.BINARY_SENSOR,
Platform.SWITCH,
Platform.CAMERA,
]

View File

@ -72,6 +72,7 @@ class FreeboxRouter:
self.devices: dict[str, dict[str, Any]] = {}
self.disks: dict[int, dict[str, Any]] = {}
self.raids: dict[int, dict[str, Any]] = {}
self.sensors_temperature: dict[str, int] = {}
self.sensors_connection: dict[str, float] = {}
self.call_list: list[dict[str, Any]] = []
@ -145,6 +146,8 @@ class FreeboxRouter:
await self._update_disks_sensors()
await self._update_raids_sensors()
async_dispatcher_send(self.hass, self.signal_sensor_update)
async def _update_disks_sensors(self) -> None:
@ -155,6 +158,14 @@ class FreeboxRouter:
for fbx_disk in fbx_disks:
self.disks[fbx_disk["id"]] = fbx_disk
async def _update_raids_sensors(self) -> None:
"""Update Freebox raids."""
# None at first request
fbx_raids: list[dict[str, Any]] = await self._api.storage.get_raids() or []
for fbx_raid in fbx_raids:
self.raids[fbx_raid["id"]] = fbx_raid
async def update_home_devices(self) -> None:
"""Update Home devices (alarm, light, sensor, switch, remote ...)."""
if not self.home_granted:

View File

@ -11,6 +11,7 @@ from .const import (
DATA_HOME_GET_NODES,
DATA_LAN_GET_HOSTS_LIST,
DATA_STORAGE_GET_DISKS,
DATA_STORAGE_GET_RAIDS,
DATA_SYSTEM_GET_CONFIG,
WIFI_GET_GLOBAL_CONFIG,
)
@ -56,6 +57,7 @@ def mock_router(mock_device_registry_devices):
# sensor
instance.call.get_calls_log = AsyncMock(return_value=DATA_CALL_GET_CALLS_LOG)
instance.storage.get_disks = AsyncMock(return_value=DATA_STORAGE_GET_DISKS)
instance.storage.get_raids = AsyncMock(return_value=DATA_STORAGE_GET_RAIDS)
# home devices
instance.home.get_home_nodes = AsyncMock(return_value=DATA_HOME_GET_NODES)
instance.connection.get_status = AsyncMock(

View File

@ -93,75 +93,177 @@ DATA_STORAGE_GET_DISKS = [
{
"idle_duration": 0,
"read_error_requests": 0,
"read_requests": 110,
"read_requests": 1815106,
"spinning": True,
# "table_type": "ms-dos", API returns without dash, but codespell isn't agree
"firmware": "SC1D",
"type": "internal",
"idle": False,
"connector": 0,
"id": 0,
"table_type": "raid",
"firmware": "0001",
"type": "sata",
"idle": True,
"connector": 2,
"id": 1000,
"write_error_requests": 0,
"state": "enabled",
"write_requests": 2708929,
"total_bytes": 250050000000,
"model": "ST9250311CS",
"time_before_spindown": 600,
"state": "disabled",
"write_requests": 80386151,
"total_bytes": 2000000000000,
"model": "ST2000LM015-2E8174",
"active_duration": 0,
"temp": 40,
"serial": "6VCQY907",
"temp": 30,
"serial": "ZDZLBFHC",
"partitions": [
{
"fstype": "ext4",
"total_bytes": 244950000000,
"label": "Disque dur",
"id": 2,
"internal": True,
"fstype": "raid",
"total_bytes": 0,
"label": "Volume 2000Go",
"id": 1000,
"internal": False,
"fsck_result": "no_run_yet",
"state": "mounted",
"disk_id": 0,
"free_bytes": 227390000000,
"used_bytes": 5090000000,
"path": "L0Rpc3F1ZSBkdXI=",
"state": "umounted",
"disk_id": 1000,
"free_bytes": 0,
"used_bytes": 0,
"path": "L1ZvbHVtZSAyMDAwR28=",
}
],
},
{
"idle_duration": 8290,
"idle_duration": 0,
"read_error_requests": 0,
"read_requests": 2326826,
"spinning": False,
"table_type": "gpt",
"read_requests": 3622038,
"spinning": True,
"table_type": "raid",
"firmware": "0001",
"type": "sata",
"idle": True,
"connector": 0,
"id": 2000,
"write_error_requests": 0,
"state": "enabled",
"write_requests": 122733632,
"time_before_spindown": 600,
"state": "disabled",
"write_requests": 80386151,
"total_bytes": 2000000000000,
"model": "ST2000LM015-2E8174",
"active_duration": 0,
"temp": 31,
"serial": "ZDZLEJXE",
"partitions": [
{
"fstype": "raid",
"total_bytes": 0,
"label": "Volume 2000Go 1",
"id": 2000,
"internal": False,
"fsck_result": "no_run_yet",
"state": "umounted",
"disk_id": 2000,
"free_bytes": 0,
"used_bytes": 0,
"path": "L1ZvbHVtZSAyMDAwR28gMQ==",
}
],
},
{
"idle_duration": 0,
"read_error_requests": 0,
"read_requests": 0,
"spinning": False,
"table_type": "superfloppy",
"firmware": "",
"type": "raid",
"idle": False,
"connector": 0,
"id": 3000,
"write_error_requests": 0,
"state": "enabled",
"write_requests": 0,
"total_bytes": 2000000000000,
"model": "",
"active_duration": 0,
"temp": 0,
"serial": "WDZYJ27Q",
"serial": "",
"partitions": [
{
"fstype": "ext4",
"total_bytes": 1960000000000,
"label": "Disque 2",
"id": 2001,
"label": "Freebox",
"id": 3000,
"internal": False,
"fsck_result": "no_run_yet",
"state": "mounted",
"disk_id": 2000,
"free_bytes": 1880000000000,
"used_bytes": 85410000000,
"path": "L0Rpc3F1ZSAy",
"disk_id": 3000,
"free_bytes": 1730000000000,
"used_bytes": 236910000000,
"path": "L0ZyZWVib3g=",
}
],
},
]
DATA_STORAGE_GET_RAIDS = [
{
"degraded": False,
"raid_disks": 2, # Number of members that should be in this array
"next_check": 0, # Unix timestamp of next check in seconds. Might be 0 if check_interval is 0
"sync_action": "idle", # values: idle, resync, recover, check, repair, reshape, frozen
"level": "raid1", # values: basic, raid0, raid1, raid5, raid10
"uuid": "dc8679f8-13f9-11ee-9106-38d547790df8",
"sysfs_state": "clear", # values: clear, inactive, suspended, readonly, read_auto, clean, active, write_pending, active_idle
"id": 0,
"sync_completed_pos": 0, # Current position of sync process
"members": [
{
"total_bytes": 2000000000000,
"active_device": 1,
"id": 1000,
"corrected_read_errors": 0,
"array_id": 0,
"disk": {
"firmware": "0001",
"temp": 29,
"serial": "ZDZLBFHC",
"model": "ST2000LM015-2E8174",
},
"role": "active", # values: active, faulty, spare, missing
"sct_erc_supported": False,
"sct_erc_enabled": False,
"dev_uuid": "fca8720e-13f9-11ee-9106-38d547790df8",
"device_location": "sata-internal-p2",
"set_name": "Freebox",
"set_uuid": "dc8679f8-13f9-11ee-9106-38d547790df8",
},
{
"total_bytes": 2000000000000,
"active_device": 0,
"id": 2000,
"corrected_read_errors": 0,
"array_id": 0,
"disk": {
"firmware": "0001",
"temp": 30,
"serial": "ZDZLEJXE",
"model": "ST2000LM015-2E8174",
},
"role": "active",
"sct_erc_supported": False,
"sct_erc_enabled": False,
"dev_uuid": "16bf00d6-13fa-11ee-9106-38d547790df8",
"device_location": "sata-internal-p0",
"set_name": "Freebox",
"set_uuid": "dc8679f8-13f9-11ee-9106-38d547790df8",
},
],
"array_size": 2000000000000, # Size of array in bytes
"state": "running", # stopped, running, error
"sync_speed": 0, # Sync speed in bytes per second
"name": "Freebox",
"check_interval": 0, # Check interval in seconds
"disk_id": 3000,
"last_check": 1682884357, # Unix timestamp of last check in seconds
"sync_completed_end": 0, # End position of sync process: total of bytes to sync
"sync_completed_percent": 0, # Percentage of sync completion
}
]
# switch
WIFI_GET_GLOBAL_CONFIG = {"enabled": True, "mac_filter_state": "disabled"}

View File

@ -0,0 +1,44 @@
"""Tests for the Freebox sensors."""
from copy import deepcopy
from datetime import timedelta
from unittest.mock import Mock
from homeassistant.components.freebox.const import DOMAIN
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
from .const import DATA_STORAGE_GET_RAIDS, MOCK_HOST, MOCK_PORT
from tests.common import MockConfigEntry, async_fire_time_changed
async def test_raid_array_degraded(hass: HomeAssistant, router: Mock) -> None:
"""Test raid array degraded binary sensor."""
entry = MockConfigEntry(
domain=DOMAIN,
data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT},
unique_id=MOCK_HOST,
)
entry.add_to_hass(hass)
assert await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
assert (
hass.states.get("binary_sensor.freebox_server_r2_raid_array_0_degraded").state
== "off"
)
# Now simulate we degraded
DATA_STORAGE_GET_RAIDS_DEGRADED = deepcopy(DATA_STORAGE_GET_RAIDS)
DATA_STORAGE_GET_RAIDS_DEGRADED[0]["degraded"] = True
router().storage.get_raids.return_value = DATA_STORAGE_GET_RAIDS_DEGRADED
# Simulate an update
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=60))
# To execute the save
await hass.async_block_till_done()
assert (
hass.states.get("binary_sensor.freebox_server_r2_raid_array_0_degraded").state
== "on"
)