mirror of
https://github.com/home-assistant/core.git
synced 2025-04-25 17:57:55 +00:00
Add raid array degraded state binary sensor to freebox sensors (#95242)
Add raid array degraded state binary sensor
This commit is contained in:
parent
c75c79962a
commit
bd7057f7b1
100
homeassistant/components/freebox/binary_sensor.py
Normal file
100
homeassistant/components/freebox/binary_sensor.py
Normal 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,
|
||||||
|
)
|
||||||
|
)
|
@ -20,6 +20,7 @@ PLATFORMS = [
|
|||||||
Platform.BUTTON,
|
Platform.BUTTON,
|
||||||
Platform.DEVICE_TRACKER,
|
Platform.DEVICE_TRACKER,
|
||||||
Platform.SENSOR,
|
Platform.SENSOR,
|
||||||
|
Platform.BINARY_SENSOR,
|
||||||
Platform.SWITCH,
|
Platform.SWITCH,
|
||||||
Platform.CAMERA,
|
Platform.CAMERA,
|
||||||
]
|
]
|
||||||
|
@ -72,6 +72,7 @@ class FreeboxRouter:
|
|||||||
|
|
||||||
self.devices: dict[str, dict[str, Any]] = {}
|
self.devices: dict[str, dict[str, Any]] = {}
|
||||||
self.disks: dict[int, 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_temperature: dict[str, int] = {}
|
||||||
self.sensors_connection: dict[str, float] = {}
|
self.sensors_connection: dict[str, float] = {}
|
||||||
self.call_list: list[dict[str, Any]] = []
|
self.call_list: list[dict[str, Any]] = []
|
||||||
@ -145,6 +146,8 @@ class FreeboxRouter:
|
|||||||
|
|
||||||
await self._update_disks_sensors()
|
await self._update_disks_sensors()
|
||||||
|
|
||||||
|
await self._update_raids_sensors()
|
||||||
|
|
||||||
async_dispatcher_send(self.hass, self.signal_sensor_update)
|
async_dispatcher_send(self.hass, self.signal_sensor_update)
|
||||||
|
|
||||||
async def _update_disks_sensors(self) -> None:
|
async def _update_disks_sensors(self) -> None:
|
||||||
@ -155,6 +158,14 @@ class FreeboxRouter:
|
|||||||
for fbx_disk in fbx_disks:
|
for fbx_disk in fbx_disks:
|
||||||
self.disks[fbx_disk["id"]] = fbx_disk
|
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:
|
async def update_home_devices(self) -> None:
|
||||||
"""Update Home devices (alarm, light, sensor, switch, remote ...)."""
|
"""Update Home devices (alarm, light, sensor, switch, remote ...)."""
|
||||||
if not self.home_granted:
|
if not self.home_granted:
|
||||||
|
@ -11,6 +11,7 @@ from .const import (
|
|||||||
DATA_HOME_GET_NODES,
|
DATA_HOME_GET_NODES,
|
||||||
DATA_LAN_GET_HOSTS_LIST,
|
DATA_LAN_GET_HOSTS_LIST,
|
||||||
DATA_STORAGE_GET_DISKS,
|
DATA_STORAGE_GET_DISKS,
|
||||||
|
DATA_STORAGE_GET_RAIDS,
|
||||||
DATA_SYSTEM_GET_CONFIG,
|
DATA_SYSTEM_GET_CONFIG,
|
||||||
WIFI_GET_GLOBAL_CONFIG,
|
WIFI_GET_GLOBAL_CONFIG,
|
||||||
)
|
)
|
||||||
@ -56,6 +57,7 @@ def mock_router(mock_device_registry_devices):
|
|||||||
# sensor
|
# sensor
|
||||||
instance.call.get_calls_log = AsyncMock(return_value=DATA_CALL_GET_CALLS_LOG)
|
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_disks = AsyncMock(return_value=DATA_STORAGE_GET_DISKS)
|
||||||
|
instance.storage.get_raids = AsyncMock(return_value=DATA_STORAGE_GET_RAIDS)
|
||||||
# home devices
|
# home devices
|
||||||
instance.home.get_home_nodes = AsyncMock(return_value=DATA_HOME_GET_NODES)
|
instance.home.get_home_nodes = AsyncMock(return_value=DATA_HOME_GET_NODES)
|
||||||
instance.connection.get_status = AsyncMock(
|
instance.connection.get_status = AsyncMock(
|
||||||
|
@ -93,75 +93,177 @@ DATA_STORAGE_GET_DISKS = [
|
|||||||
{
|
{
|
||||||
"idle_duration": 0,
|
"idle_duration": 0,
|
||||||
"read_error_requests": 0,
|
"read_error_requests": 0,
|
||||||
"read_requests": 110,
|
"read_requests": 1815106,
|
||||||
"spinning": True,
|
"spinning": True,
|
||||||
# "table_type": "ms-dos", API returns without dash, but codespell isn't agree
|
"table_type": "raid",
|
||||||
"firmware": "SC1D",
|
"firmware": "0001",
|
||||||
"type": "internal",
|
"type": "sata",
|
||||||
"idle": False,
|
"idle": True,
|
||||||
"connector": 0,
|
"connector": 2,
|
||||||
"id": 0,
|
"id": 1000,
|
||||||
"write_error_requests": 0,
|
"write_error_requests": 0,
|
||||||
"state": "enabled",
|
"time_before_spindown": 600,
|
||||||
"write_requests": 2708929,
|
"state": "disabled",
|
||||||
"total_bytes": 250050000000,
|
"write_requests": 80386151,
|
||||||
"model": "ST9250311CS",
|
"total_bytes": 2000000000000,
|
||||||
|
"model": "ST2000LM015-2E8174",
|
||||||
"active_duration": 0,
|
"active_duration": 0,
|
||||||
"temp": 40,
|
"temp": 30,
|
||||||
"serial": "6VCQY907",
|
"serial": "ZDZLBFHC",
|
||||||
"partitions": [
|
"partitions": [
|
||||||
{
|
{
|
||||||
"fstype": "ext4",
|
"fstype": "raid",
|
||||||
"total_bytes": 244950000000,
|
"total_bytes": 0,
|
||||||
"label": "Disque dur",
|
"label": "Volume 2000Go",
|
||||||
"id": 2,
|
"id": 1000,
|
||||||
"internal": True,
|
"internal": False,
|
||||||
"fsck_result": "no_run_yet",
|
"fsck_result": "no_run_yet",
|
||||||
"state": "mounted",
|
"state": "umounted",
|
||||||
"disk_id": 0,
|
"disk_id": 1000,
|
||||||
"free_bytes": 227390000000,
|
"free_bytes": 0,
|
||||||
"used_bytes": 5090000000,
|
"used_bytes": 0,
|
||||||
"path": "L0Rpc3F1ZSBkdXI=",
|
"path": "L1ZvbHVtZSAyMDAwR28=",
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idle_duration": 8290,
|
"idle_duration": 0,
|
||||||
"read_error_requests": 0,
|
"read_error_requests": 0,
|
||||||
"read_requests": 2326826,
|
"read_requests": 3622038,
|
||||||
"spinning": False,
|
"spinning": True,
|
||||||
"table_type": "gpt",
|
"table_type": "raid",
|
||||||
"firmware": "0001",
|
"firmware": "0001",
|
||||||
"type": "sata",
|
"type": "sata",
|
||||||
"idle": True,
|
"idle": True,
|
||||||
"connector": 0,
|
"connector": 0,
|
||||||
"id": 2000,
|
"id": 2000,
|
||||||
"write_error_requests": 0,
|
"write_error_requests": 0,
|
||||||
"state": "enabled",
|
"time_before_spindown": 600,
|
||||||
"write_requests": 122733632,
|
"state": "disabled",
|
||||||
|
"write_requests": 80386151,
|
||||||
"total_bytes": 2000000000000,
|
"total_bytes": 2000000000000,
|
||||||
"model": "ST2000LM015-2E8174",
|
"model": "ST2000LM015-2E8174",
|
||||||
"active_duration": 0,
|
"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,
|
"temp": 0,
|
||||||
"serial": "WDZYJ27Q",
|
"serial": "",
|
||||||
"partitions": [
|
"partitions": [
|
||||||
{
|
{
|
||||||
"fstype": "ext4",
|
"fstype": "ext4",
|
||||||
"total_bytes": 1960000000000,
|
"total_bytes": 1960000000000,
|
||||||
"label": "Disque 2",
|
"label": "Freebox",
|
||||||
"id": 2001,
|
"id": 3000,
|
||||||
"internal": False,
|
"internal": False,
|
||||||
"fsck_result": "no_run_yet",
|
"fsck_result": "no_run_yet",
|
||||||
"state": "mounted",
|
"state": "mounted",
|
||||||
"disk_id": 2000,
|
"disk_id": 3000,
|
||||||
"free_bytes": 1880000000000,
|
"free_bytes": 1730000000000,
|
||||||
"used_bytes": 85410000000,
|
"used_bytes": 236910000000,
|
||||||
"path": "L0Rpc3F1ZSAy",
|
"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
|
# switch
|
||||||
WIFI_GET_GLOBAL_CONFIG = {"enabled": True, "mac_filter_state": "disabled"}
|
WIFI_GET_GLOBAL_CONFIG = {"enabled": True, "mac_filter_state": "disabled"}
|
||||||
|
|
||||||
|
44
tests/components/freebox/test_binary_sensor.py
Normal file
44
tests/components/freebox/test_binary_sensor.py
Normal 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"
|
||||||
|
)
|
Loading…
x
Reference in New Issue
Block a user