Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions homeassistant/components/freebox/binary_sensor.py
Original file line number Diff line number Diff line change
@@ -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()
Comment thread
fthiery marked this conversation as resolved.

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,
)
)
1 change: 1 addition & 0 deletions homeassistant/components/freebox/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
Platform.BUTTON,
Platform.DEVICE_TRACKER,
Platform.SENSOR,
Platform.BINARY_SENSOR,
Platform.SWITCH,
Platform.CAMERA,
]
Expand Down
11 changes: 11 additions & 0 deletions homeassistant/components/freebox/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ def __init__(

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]] = []
Expand Down Expand Up @@ -145,6 +146,8 @@ async def update_sensors(self) -> None:

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:
Expand All @@ -155,6 +158,14 @@ async def _update_disks_sensors(self) -> None:
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:
Expand Down
2 changes: 2 additions & 0 deletions tests/components/freebox/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
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,
)
Expand Down Expand Up @@ -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(
Expand Down
174 changes: 138 additions & 36 deletions tests/components/freebox/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,75 +93,177 @@
{
Comment thread
fthiery marked this conversation as resolved.
"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"}

Expand Down
Loading