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
3 changes: 3 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -1113,6 +1113,9 @@ omit =
homeassistant/components/sesame/lock.py
homeassistant/components/seven_segments/image_processing.py
homeassistant/components/seventeentrack/sensor.py
homeassistant/components/sfr_box/__init__.py
homeassistant/components/sfr_box/coordinator.py
homeassistant/components/sfr_box/sensor.py
homeassistant/components/shiftr/*
homeassistant/components/shodan/sensor.py
homeassistant/components/sia/__init__.py
Expand Down
1 change: 1 addition & 0 deletions .strict-typing
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ homeassistant.components.sensibo.*
homeassistant.components.sensirion_ble.*
homeassistant.components.sensor.*
homeassistant.components.senz.*
homeassistant.components.sfr_box.*
homeassistant.components.shelly.*
homeassistant.components.simplepush.*
homeassistant.components.simplisafe.*
Expand Down
2 changes: 2 additions & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -1024,6 +1024,8 @@ build.json @home-assistant/supervisor
/tests/components/senz/ @milanmeu
/homeassistant/components/serial/ @fabaff
/homeassistant/components/seven_segments/ @fabaff
/homeassistant/components/sfr_box/ @epenet
/tests/components/sfr_box/ @epenet
/homeassistant/components/sharkiq/ @JeffResc @funkybunch @AritroSaha10
/tests/components/sharkiq/ @JeffResc @funkybunch @AritroSaha10
/homeassistant/components/shell_command/ @home-assistant/core
Expand Down
53 changes: 53 additions & 0 deletions homeassistant/components/sfr_box/__init__.py
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
Comment on lines +21 to +26
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The coordinator can already handle this on first request/refresh,thispart is thus not needed

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

system_get_info is only called in setup - not via a coordinator
I will address this when I add "system" sensors.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in #85039

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,
Comment thread
MartinHjelmare marked this conversation as resolved.
"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
49 changes: 49 additions & 0 deletions homeassistant/components/sfr_box/config_flow.py
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(
Comment thread
epenet marked this conversation as resolved.
ip=user_input[CONF_HOST],
client=get_async_client(self.hass),
)
system_info = await box.system_get_info()
except SFRBoxError:
errors["base"] = "unknown"
Comment thread
epenet marked this conversation as resolved.
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
)
8 changes: 8 additions & 0 deletions homeassistant/components/sfr_box/const.py
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]
25 changes: 25 additions & 0 deletions homeassistant/components/sfr_box/coordinator.py
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()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can throw an SFRBoxError, which is unhandled?

Copy link
Copy Markdown
Contributor Author

@epenet epenet Jan 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in #84977 #85039

10 changes: 10 additions & 0 deletions homeassistant/components/sfr_box/manifest.json
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"
}
182 changes: 182 additions & 0 deletions homeassistant/components/sfr_box/sensor.py
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,
Comment thread
epenet marked this conversation as resolved.
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",
Comment on lines +115 to +120
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should be lower case and been put into translations.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in #84977

],
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",
Comment on lines +130 to +139
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in #84977

],
has_entity_name=True,
Comment thread
epenet marked this conversation as resolved.
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)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a coordinator that already has data, the True parameter seems unneeded here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The coordinator doesn't yet have data, at is it not (currently) initialised on setup.
I will address this when I add the second coordinator.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in #85039



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)
17 changes: 17 additions & 0 deletions homeassistant/components/sfr_box/strings.json
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%]"
}
}
}
17 changes: 17 additions & 0 deletions homeassistant/components/sfr_box/translations/en.json
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"
}
}
}
Loading