Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bring up to speed with 2022 HA codestyle #44

Closed
wants to merge 9 commits into from
Closed
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
5 changes: 5 additions & 0 deletions .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Home Assistant Community forum
url: https://community.home-assistant.io/t/sagemcom-router-support/179880/last
about: Please ask and answer questions here.
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/asottile/pyupgrade
rev: v2.31.1
rev: v2.14.0
hooks:
- id: pyupgrade
args: [--py37-plus]
Expand All @@ -11,7 +11,7 @@ repos:
args:
- --safe
- --quiet
files: ^((custom_components)/.+)?[^/]+\.py$
files: ^((custom_components|tests)/.+)?[^/]+\.py$
- repo: https://github.com/codespell-project/codespell
rev: v2.1.0
hooks:
Expand All @@ -28,7 +28,7 @@ repos:
additional_dependencies:
- flake8-docstrings==1.5.0
- pydocstyle==5.1.1
files: ^(custom_components)/.+\.py$
files: ^(custom_components|tests)/.+\.py$
- repo: https://github.com/PyCQA/isort
rev: 5.12.0
hooks:
Expand Down
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@
[![GitHub release](https://img.shields.io/github/release/iMicknl/ha-sagemcom-fast.svg)](https://github.com/iMicknl/ha-sagemcom-fast/releases/)
[![HA integration usage](https://img.shields.io/badge/dynamic/json?color=41BDF5&logo=home-assistant&label=integration%20usage&suffix=%20installs&cacheSeconds=15600&url=https://analytics.home-assistant.io/custom_integrations.json&query=$.sagemcom_fast.total)](https://analytics.home-assistant.io/custom_integrations.json)

# Sagemcom F@st - Home Assistant (work in progress)
# Sagemcom F@st integration for Home Assistant

This integration adds support for Sagemcom F@st routers to Home Assistant. Currently this is a work in progress where only a basic device_tracker is supported, however in the future sensors will be added as well.
This integration adds support for Sagemcom F@st routers to Home Assistant. Currently only a basic device_tracker entity is supported, however this could be extended in the future with more sensors.

Sagemcom F@st routers are used by many providers worldwide, but many of them did rebrand the router. Examples are the b-box from Proximus, Home Hub from bell and the Smart Hub from BT.

## Known limitations / issues

Since this integration is only used by a few users, not much time has been spent on the development. There are currently some known limitations and bugs. Contributions are welcome!

- After reboot, not connected devices have status 'unavailable' [#14](https://github.com/iMicknl/ha-sagemcom-fast/issues/14)

## Installation

### Manual
Expand Down
150 changes: 63 additions & 87 deletions custom_components/sagemcom_fast/__init__.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
"""The Sagemcom integration."""
import asyncio
from __future__ import annotations

from dataclasses import dataclass
from datetime import timedelta
import logging

from aiohttp.client_exceptions import ClientError
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_HOST,
CONF_PASSWORD,
CONF_SCAN_INTERVAL,
CONF_SOURCE,
CONF_SSL,
CONF_USERNAME,
CONF_VERIFY_SSL,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client, service
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from sagemcom_api.client import SagemcomClient
Expand All @@ -27,28 +27,30 @@
MaximumSessionCountException,
UnauthorizedException,
)

from .const import CONF_ENCRYPTION_METHOD, DEFAULT_SCAN_INTERVAL, DOMAIN
from sagemcom_api.models import DeviceInfo as GatewayDeviceInfo

from .const import (
CONF_ENCRYPTION_METHOD,
DEFAULT_SCAN_INTERVAL,
DOMAIN,
LOGGER,
PLATFORMS,
)
from .device_tracker import SagemcomDataUpdateCoordinator

_LOGGER = logging.getLogger(__name__)

PLATFORMS = ["device_tracker"]

SERVICE_REBOOT = "reboot"


async def async_setup(hass: HomeAssistant, config: dict):
"""Set up the Sagemcom component."""

hass.data.setdefault(DOMAIN, {})
@dataclass
class HomeAssistantSagemcomFastData:
"""Nest Protect data stored in the Home Assistant data object."""

return True
coordinator: SagemcomDataUpdateCoordinator
gateway: GatewayDeviceInfo | None


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Set up Sagemcom from a config entry."""

host = entry.data[CONF_HOST]
username = entry.data[CONF_USERNAME]
password = entry.data[CONF_PASSWORD]
Expand All @@ -57,6 +59,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
verify_ssl = entry.data[CONF_VERIFY_SSL]

session = aiohttp_client.async_get_clientsession(hass, verify_ssl=verify_ssl)

client = SagemcomClient(
host,
username,
Expand All @@ -68,79 +71,68 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):

try:
await client.login()
except AccessRestrictionException:
_LOGGER.error("access_restricted")
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={CONF_SOURCE: SOURCE_REAUTH},
data=entry.data,
)
)
return False
except (AuthenticationException, UnauthorizedException):
_LOGGER.error("invalid_auth")
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={CONF_SOURCE: SOURCE_REAUTH},
data=entry.data,
)
)
return False
except AccessRestrictionException as exception:
raise ConfigEntryAuthFailed("Access Restricted") from exception
except (AuthenticationException, UnauthorizedException) as exception:
raise ConfigEntryAuthFailed("Invalid Authentication") from exception
except (TimeoutError, ClientError) as exception:
_LOGGER.error("Failed to connect")
raise ConfigEntryNotReady("Failed to connect") from exception
except MaximumSessionCountException as exception:
_LOGGER.error("Maximum session count reached")
raise ConfigEntryNotReady("Maximum session count reached") from exception
except LoginTimeoutException:
_LOGGER.error("Request timed-out")
LOGGER.error("Request timed-out")
return False
except Exception as exception: # pylint: disable=broad-except
_LOGGER.exception(exception)
LOGGER.exception(exception)
return False

try:
gateway = await client.get_device_info()
finally:
await client.logout()

update_interval = entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)

coordinator = SagemcomDataUpdateCoordinator(
hass,
_LOGGER,
LOGGER,
name="sagemcom_hosts",
client=client,
update_interval=timedelta(seconds=update_interval),
)

await coordinator.async_refresh()
await coordinator.async_config_entry_first_refresh()

hass.data[DOMAIN][entry.entry_id] = {
"coordinator": coordinator,
"update_listener": entry.add_update_listener(update_listener),
}
# Many users face issues while retrieving Device Info
# So don't make this fail the start-up
gateway = None

# Create gateway device in Home Assistant
device_registry = hass.helpers.device_registry.async_get(hass)
try:
gateway = await client.get_device_info()

device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
connections={(CONNECTION_NETWORK_MAC, gateway.mac_address)},
identifiers={(DOMAIN, gateway.serial_number)},
manufacturer=gateway.manufacturer,
name=f"{gateway.manufacturer} {gateway.model_number}",
model=gateway.model_name,
sw_version=gateway.software_version,
# Create gateway device in Home Assistant
device_registry = await hass.helpers.device_registry.async_get_registry()

device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
connections={(CONNECTION_NETWORK_MAC, gateway.mac_address)},
identifiers={(DOMAIN, gateway.serial_number)},
manufacturer=gateway.manufacturer,
name=f"{gateway.manufacturer} {gateway.model_number}",
model=gateway.model_name,
sw_version=gateway.software_version,
)
except Exception as exception: # pylint: disable=broad-except
LOGGER.exception(exception)
finally:
await client.logout()

hass.data.setdefault(DOMAIN, {})[entry.entry_id] = HomeAssistantSagemcomFastData(
coordinator=coordinator, gateway=gateway
)

# Register components
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

entry.async_on_unload(entry.add_update_listener(async_reload_entry))

# Handle gateway device services
async def async_command_reboot(call):
# TODO move to button entity
async def async_command_reboot(call: ServiceCall) -> None:
"""Handle reboot service call."""
await client.reboot()

Expand All @@ -151,30 +143,14 @@ async def async_command_reboot(call):
return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""

unload_ok = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(entry, component)
for component in PLATFORMS
]
)
)
if unload_ok:
hass.data[DOMAIN][entry.entry_id]["update_listener"]()
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)

return unload_ok


async def update_listener(hass: HomeAssistant, entry: ConfigEntry):
"""Update when entry options update."""
if entry.options[CONF_SCAN_INTERVAL]:
coordinator = hass.data[DOMAIN][entry.entry_id]["coordinator"]
coordinator.update_interval = timedelta(
seconds=entry.options[CONF_SCAN_INTERVAL]
)

await coordinator.async_refresh()
async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Handle an options update."""
await hass.config_entries.async_reload(entry.entry_id)
10 changes: 3 additions & 7 deletions custom_components/sagemcom_fast/config_flow.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
"""Config flow for Sagemcom integration."""
import logging

from aiohttp import ClientError
from homeassistant import config_entries
from homeassistant.const import (
Expand All @@ -22,11 +20,9 @@
)
import voluptuous as vol

from .const import CONF_ENCRYPTION_METHOD, DOMAIN
from .const import CONF_ENCRYPTION_METHOD, DOMAIN, LOGGER
from .options_flow import OptionsFlow

_LOGGER = logging.getLogger(__name__)

ENCRYPTION_METHODS = [item.value for item in EncryptionMethod]

DATA_SCHEMA = vol.Schema(
Expand All @@ -45,7 +41,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Sagemcom."""

VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL

async def async_validate_input(self, user_input):
"""Validate user credentials."""
Expand Down Expand Up @@ -79,6 +74,7 @@ async def async_step_user(self, user_input=None):
errors = {}

if user_input:
# TODO change to gateway mac address or something more unique
await self.async_set_unique_id(user_input.get(CONF_HOST))
self._abort_if_unique_id_configured()

Expand All @@ -96,7 +92,7 @@ async def async_step_user(self, user_input=None):
errors["base"] = "maximum_session_count"
except Exception as exception: # pylint: disable=broad-except
errors["base"] = "unknown"
_LOGGER.exception(exception)
LOGGER.exception(exception)

return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors=errors
Expand Down
14 changes: 13 additions & 1 deletion custom_components/sagemcom_fast/const.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
"""Constants for the Sagemcom integration."""
DOMAIN = "sagemcom_fast"
from __future__ import annotations

import logging
from typing import Final

from homeassistant.const import Platform

DOMAIN: Final = "sagemcom_fast"
LOGGER: logging.Logger = logging.getLogger(__package__)

PLATFORMS: list[Platform] = [
Platform.DEVICE_TRACKER,
]

CONF_ENCRYPTION_METHOD = "encryption_method"
CONF_TRACK_WIRELESS_CLIENTS = "track_wireless_clients"
Expand Down
Loading
Loading