Add raid array degraded state binary sensor to freebox sensors (#95242)
Add raid array degraded state binary sensorpull/95723/head
parent
c75c79962a
commit
bd7057f7b1
|
@ -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,
|
||||
)
|
||||
)
|
|
@ -20,6 +20,7 @@ PLATFORMS = [
|
|||
Platform.BUTTON,
|
||||
Platform.DEVICE_TRACKER,
|
||||
Platform.SENSOR,
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.SWITCH,
|
||||
Platform.CAMERA,
|
||||
]
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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"}
|
||||
|
||||
|
|
|
@ -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"
|
||||
)
|
Loading…
Reference in New Issue