forked from home-assistant/core
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add SFR Box integration (home-assistant#84780)
* Add SFR Box integration * Adjust error handling in config flow * Add tests * Use value_fn * Add translation * Enable mypy strict typing * Add ConfigEntryNotReady * Rename exception * Fix requirements
- Loading branch information
Showing
20 changed files
with
562 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
"""SFR Box.""" | ||
from __future__ import annotations | ||
|
||
from sfrbox_api.bridge import SFRBox | ||
from sfrbox_api.exceptions import SFRBoxError | ||
|
||
from homeassistant.config_entries import ConfigEntry | ||
from homeassistant.const import CONF_HOST | ||
from homeassistant.core import HomeAssistant | ||
from homeassistant.exceptions import ConfigEntryNotReady | ||
from homeassistant.helpers import device_registry as dr | ||
from homeassistant.helpers.httpx_client import get_async_client | ||
|
||
from .const import DOMAIN, PLATFORMS | ||
from .coordinator import DslDataUpdateCoordinator | ||
|
||
|
||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: | ||
"""Set up SFR box as config entry.""" | ||
box = SFRBox(ip=entry.data[CONF_HOST], client=get_async_client(hass)) | ||
try: | ||
system_info = await box.system_get_info() | ||
except SFRBoxError as err: | ||
raise ConfigEntryNotReady( | ||
f"Unable to connect to {entry.data[CONF_HOST]}" | ||
) from err | ||
hass.data.setdefault(DOMAIN, {}) | ||
|
||
device_registry = dr.async_get(hass) | ||
device_registry.async_get_or_create( | ||
config_entry_id=entry.entry_id, | ||
identifiers={(DOMAIN, system_info.mac_addr)}, | ||
name="SFR Box", | ||
model=system_info.product_id, | ||
sw_version=system_info.version_mainfirmware, | ||
configuration_url=f"http://{entry.data[CONF_HOST]}", | ||
) | ||
|
||
hass.data[DOMAIN][entry.entry_id] = { | ||
"box": box, | ||
"dsl_coordinator": DslDataUpdateCoordinator(hass, box), | ||
} | ||
|
||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) | ||
|
||
return True | ||
|
||
|
||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: | ||
"""Unload a config entry.""" | ||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): | ||
hass.data[DOMAIN].pop(entry.entry_id) | ||
return unload_ok |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
"""SFR Box config flow.""" | ||
from __future__ import annotations | ||
|
||
from sfrbox_api.bridge import SFRBox | ||
from sfrbox_api.exceptions import SFRBoxError | ||
import voluptuous as vol | ||
|
||
from homeassistant.config_entries import ConfigFlow | ||
from homeassistant.const import CONF_HOST | ||
from homeassistant.data_entry_flow import FlowResult | ||
from homeassistant.helpers.httpx_client import get_async_client | ||
|
||
from .const import DEFAULT_HOST, DOMAIN | ||
|
||
DATA_SCHEMA = vol.Schema( | ||
{ | ||
vol.Required(CONF_HOST, default=DEFAULT_HOST): str, | ||
} | ||
) | ||
|
||
|
||
class SFRBoxFlowHandler(ConfigFlow, domain=DOMAIN): | ||
"""SFR Box config flow.""" | ||
|
||
VERSION = 1 | ||
|
||
async def async_step_user( | ||
self, user_input: dict[str, str] | None = None | ||
) -> FlowResult: | ||
"""Handle a flow initialized by the user.""" | ||
errors = {} | ||
if user_input is not None: | ||
try: | ||
box = SFRBox( | ||
ip=user_input[CONF_HOST], | ||
client=get_async_client(self.hass), | ||
) | ||
system_info = await box.system_get_info() | ||
except SFRBoxError: | ||
errors["base"] = "unknown" | ||
else: | ||
await self.async_set_unique_id(system_info.mac_addr) | ||
self._abort_if_unique_id_configured() | ||
self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]}) | ||
return self.async_create_entry(title="SFR Box", data=user_input) | ||
|
||
return self.async_show_form( | ||
step_id="user", data_schema=DATA_SCHEMA, errors=errors | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
"""SFR Box constants.""" | ||
from homeassistant.const import Platform | ||
|
||
DEFAULT_HOST = "192.168.0.1" | ||
|
||
DOMAIN = "sfr_box" | ||
|
||
PLATFORMS = [Platform.SENSOR] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
"""SFR Box coordinator.""" | ||
from datetime import timedelta | ||
import logging | ||
|
||
from sfrbox_api.bridge import SFRBox | ||
from sfrbox_api.models import DslInfo | ||
|
||
from homeassistant.core import HomeAssistant | ||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator | ||
|
||
_LOGGER = logging.getLogger(__name__) | ||
_SCAN_INTERVAL = timedelta(minutes=1) | ||
|
||
|
||
class DslDataUpdateCoordinator(DataUpdateCoordinator[DslInfo]): | ||
"""Coordinator to manage data updates.""" | ||
|
||
def __init__(self, hass: HomeAssistant, box: SFRBox) -> None: | ||
"""Initialize coordinator.""" | ||
self._box = box | ||
super().__init__(hass, _LOGGER, name="dsl", update_interval=_SCAN_INTERVAL) | ||
|
||
async def _async_update_data(self) -> DslInfo: | ||
"""Update data.""" | ||
return await self._box.dsl_get_info() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
{ | ||
"domain": "sfr_box", | ||
"name": "SFR Box", | ||
"config_flow": true, | ||
"documentation": "https://www.home-assistant.io/integrations/sfr_box", | ||
"requirements": ["sfrbox-api==0.0.1"], | ||
"codeowners": ["@epenet"], | ||
"iot_class": "local_polling", | ||
"integration_type": "device" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,182 @@ | ||
"""SFR Box sensor platform.""" | ||
from collections.abc import Callable | ||
from dataclasses import dataclass | ||
|
||
from sfrbox_api.bridge import SFRBox | ||
from sfrbox_api.models import DslInfo, SystemInfo | ||
|
||
from homeassistant.components.sensor import ( | ||
SensorDeviceClass, | ||
SensorEntity, | ||
SensorEntityDescription, | ||
SensorStateClass, | ||
) | ||
from homeassistant.config_entries import ConfigEntry | ||
from homeassistant.const import SIGNAL_STRENGTH_DECIBELS, UnitOfDataRate | ||
from homeassistant.core import HomeAssistant | ||
from homeassistant.helpers.entity_platform import AddEntitiesCallback | ||
from homeassistant.helpers.typing import StateType | ||
from homeassistant.helpers.update_coordinator import CoordinatorEntity | ||
|
||
from .const import DOMAIN | ||
from .coordinator import DslDataUpdateCoordinator | ||
|
||
|
||
@dataclass | ||
class SFRBoxSensorMixin: | ||
"""Mixin for SFR Box sensors.""" | ||
|
||
value_fn: Callable[[DslInfo], StateType] | ||
|
||
|
||
@dataclass | ||
class SFRBoxSensorEntityDescription(SensorEntityDescription, SFRBoxSensorMixin): | ||
"""Description for SFR Box sensors.""" | ||
|
||
|
||
SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription, ...] = ( | ||
SFRBoxSensorEntityDescription( | ||
key="linemode", | ||
name="Line mode", | ||
has_entity_name=True, | ||
value_fn=lambda x: x.linemode, | ||
), | ||
SFRBoxSensorEntityDescription( | ||
key="counter", | ||
name="Counter", | ||
has_entity_name=True, | ||
value_fn=lambda x: x.counter, | ||
), | ||
SFRBoxSensorEntityDescription( | ||
key="crc", | ||
name="CRC", | ||
has_entity_name=True, | ||
value_fn=lambda x: x.crc, | ||
), | ||
SFRBoxSensorEntityDescription( | ||
key="noise_down", | ||
name="Noise down", | ||
device_class=SensorDeviceClass.SIGNAL_STRENGTH, | ||
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, | ||
state_class=SensorStateClass.MEASUREMENT, | ||
has_entity_name=True, | ||
value_fn=lambda x: x.noise_down, | ||
), | ||
SFRBoxSensorEntityDescription( | ||
key="noise_up", | ||
name="Noise up", | ||
device_class=SensorDeviceClass.SIGNAL_STRENGTH, | ||
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, | ||
state_class=SensorStateClass.MEASUREMENT, | ||
has_entity_name=True, | ||
value_fn=lambda x: x.noise_up, | ||
), | ||
SFRBoxSensorEntityDescription( | ||
key="attenuation_down", | ||
name="Attenuation down", | ||
device_class=SensorDeviceClass.SIGNAL_STRENGTH, | ||
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, | ||
state_class=SensorStateClass.MEASUREMENT, | ||
has_entity_name=True, | ||
value_fn=lambda x: x.attenuation_down, | ||
), | ||
SFRBoxSensorEntityDescription( | ||
key="attenuation_up", | ||
name="Attenuation up", | ||
device_class=SensorDeviceClass.SIGNAL_STRENGTH, | ||
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, | ||
state_class=SensorStateClass.MEASUREMENT, | ||
has_entity_name=True, | ||
value_fn=lambda x: x.attenuation_up, | ||
), | ||
SFRBoxSensorEntityDescription( | ||
key="rate_down", | ||
name="Rate down", | ||
device_class=SensorDeviceClass.DATA_RATE, | ||
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND, | ||
state_class=SensorStateClass.MEASUREMENT, | ||
has_entity_name=True, | ||
value_fn=lambda x: x.rate_down, | ||
), | ||
SFRBoxSensorEntityDescription( | ||
key="rate_up", | ||
name="Rate up", | ||
device_class=SensorDeviceClass.DATA_RATE, | ||
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND, | ||
state_class=SensorStateClass.MEASUREMENT, | ||
has_entity_name=True, | ||
value_fn=lambda x: x.rate_up, | ||
), | ||
SFRBoxSensorEntityDescription( | ||
key="line_status", | ||
name="Line status", | ||
device_class=SensorDeviceClass.ENUM, | ||
options=[ | ||
"No Defect", | ||
"Of Frame", | ||
"Loss Of Signal", | ||
"Loss Of Power", | ||
"Loss Of Signal Quality", | ||
"Unknown", | ||
], | ||
has_entity_name=True, | ||
value_fn=lambda x: x.line_status, | ||
), | ||
SFRBoxSensorEntityDescription( | ||
key="training", | ||
name="Training", | ||
device_class=SensorDeviceClass.ENUM, | ||
options=[ | ||
"Idle", | ||
"G.994 Training", | ||
"G.992 Started", | ||
"G.922 Channel Analysis", | ||
"G.992 Message Exchange", | ||
"G.993 Started", | ||
"G.993 Channel Analysis", | ||
"G.993 Message Exchange", | ||
"Showtime", | ||
"Unknown", | ||
], | ||
has_entity_name=True, | ||
value_fn=lambda x: x.training, | ||
), | ||
) | ||
|
||
|
||
async def async_setup_entry( | ||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback | ||
) -> None: | ||
"""Set up the sensors.""" | ||
data = hass.data[DOMAIN][entry.entry_id] | ||
box: SFRBox = data["box"] | ||
system_info = await box.system_get_info() | ||
|
||
entities = [ | ||
SFRBoxSensor(data["dsl_coordinator"], description, system_info) | ||
for description in SENSOR_TYPES | ||
] | ||
async_add_entities(entities, True) | ||
|
||
|
||
class SFRBoxSensor(CoordinatorEntity[DslDataUpdateCoordinator], SensorEntity): | ||
"""SFR Box sensor.""" | ||
|
||
entity_description: SFRBoxSensorEntityDescription | ||
|
||
def __init__( | ||
self, | ||
coordinator: DslDataUpdateCoordinator, | ||
description: SFRBoxSensorEntityDescription, | ||
system_info: SystemInfo, | ||
) -> None: | ||
"""Initialize the sensor.""" | ||
super().__init__(coordinator) | ||
self.entity_description = description | ||
self._attr_unique_id = f"{system_info.mac_addr}_dsl_{description.key}" | ||
self._attr_device_info = {"identifiers": {(DOMAIN, system_info.mac_addr)}} | ||
|
||
@property | ||
def native_value(self) -> StateType: | ||
"""Return the native value of the device.""" | ||
return self.entity_description.value_fn(self.coordinator.data) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
{ | ||
"config": { | ||
"step": { | ||
"user": { | ||
"data": { | ||
"host": "[%key:common::config_flow::data::host%]" | ||
} | ||
} | ||
}, | ||
"error": { | ||
"unknown": "[%key:common::config_flow::error::unknown%]" | ||
}, | ||
"abort": { | ||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]" | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
{ | ||
"config": { | ||
"step": { | ||
"user": { | ||
"data": { | ||
"host": "Host" | ||
} | ||
} | ||
}, | ||
"error": { | ||
"unknown": "Unexpected error" | ||
}, | ||
"abort": { | ||
"already_configured": "Device is already configured" | ||
} | ||
} | ||
} |
Oops, something went wrong.