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

Maintenance update (August) #150

Merged
merged 18 commits into from
Aug 9, 2024
Merged
13 changes: 5 additions & 8 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// See https://aka.ms/vscode-remote/devcontainer.json for format details.
{
"image": "mcr.microsoft.com/devcontainers/python:1-3.11-bullseye",
"image": "mcr.microsoft.com/devcontainers/python:1-3.12-bullseye",
"name": "ha-sagemcom-fast",
"forwardPorts": [
8123
Expand All @@ -12,22 +12,19 @@
}
},
"features": {
"ghcr.io/devcontainers-contrib/features/ffmpeg-apt-get:1": {}
"ghcr.io/devcontainers-contrib/features/ffmpeg-apt-get:1": {} // required for Home Assistant
},
"postCreateCommand": "pip install -r requirements_dev.txt && pre-commit install && pre-commit install-hooks",
"postCreateCommand": "pip install -r requirements_dev.txt && pre-commit install && pre-commit install-hooks && sudo apt-get update && sudo apt-get install -y libpcap-dev libturbojpeg0",
"containerEnv": {
"DEVCONTAINER": "1"
},
"remoteUser": "vscode",
"customizations": {
"vscode": {
"extensions": [
"ms-python.vscode-pylance",
"ms-python.python",
"redhat.vscode-yaml",
"esbenp.prettier-vscode",
"GitHub.vscode-pull-request-github",
"GitHub.copilot"
"GitHub.copilot",
"GitHub.copilot-chat"
],
"settings": {
"python.pythonPath": "/usr/local/bin/python",
Expand Down
14 changes: 7 additions & 7 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,39 +1,39 @@
repos:
- repo: https://github.com/asottile/pyupgrade
rev: v2.31.1
rev: v3.17.0
hooks:
- id: pyupgrade
args: [--py37-plus]
- repo: https://github.com/psf/black
rev: 22.3.0
rev: 24.8.0
hooks:
- id: black
args:
- --safe
- --quiet
files: ^((custom_components)/.+)?[^/]+\.py$
- repo: https://github.com/codespell-project/codespell
rev: v2.1.0
rev: v2.3.0
hooks:
- id: codespell
args:
- --ignore-words-list=hass,alot,datas,dof,dur,farenheit,hist,iff,ines,ist,lightsensor,mut,nd,pres,referer,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing
- --skip="./.*,*.csv,*.json,*.md"
- --skip="./.*,*.csv,*.json,*.md",setup.cfg
- --quiet-level=2
exclude_types: [csv, json]
- repo: https://github.com/PyCQA/flake8
rev: 3.9.2
rev: 7.1.1
hooks:
- id: flake8
additional_dependencies:
- flake8-docstrings==1.5.0
- pydocstyle==5.1.1
files: ^(custom_components)/.+\.py$
- repo: https://github.com/PyCQA/isort
rev: 5.12.0
rev: 5.13.2
hooks:
- id: isort
- repo: https://github.com/adrienverge/yamllint.git
rev: v1.26.3
rev: v1.35.1
hooks:
- id: yamllint
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ https://github.com/imicknl/ha-sagemcom-fast

This integration can only be configured via the Config Flow. Go to `Configuration -> Integrations -> Add Integration` and choose Sagemcom F@st. The prompt will ask you for your credentials. Please note that some routers require authentication, where others can login with `guest` username and an empty password.

The encryption method differs per device. Please refer to the table below to understand which option to select. If your device is not listed, please try both methods one by one.
The first login might take a longer time (up to a minute), since we will try to retrieve the encryption method used by your router.

## Supported devices

Expand Down
23 changes: 15 additions & 8 deletions custom_components/sagemcom_fast/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""The Sagemcom F@st integration."""

from __future__ import annotations

from dataclasses import dataclass
Expand All @@ -23,6 +24,7 @@
from sagemcom_api.exceptions import (
AccessRestrictionException,
AuthenticationException,
LoginRetryErrorException,
MaximumSessionCountException,
UnauthorizedException,
)
Expand Down Expand Up @@ -57,11 +59,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):

session = aiohttp_client.async_get_clientsession(hass, verify_ssl=verify_ssl)
client = SagemcomClient(
host,
username,
password,
EncryptionMethod(encryption_method),
session,
host=host,
username=username,
password=password,
authentication_method=EncryptionMethod(encryption_method),
session=session,
ssl=ssl,
)

Expand All @@ -73,12 +75,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
except (AuthenticationException, UnauthorizedException) as exception:
LOGGER.error("Invalid_auth")
raise ConfigEntryAuthFailed("Invalid credentials") from exception
except (TimeoutError, ClientError) as exception:
except (TimeoutError, ClientError, ConnectionError) 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 LoginRetryErrorException as exception:
LOGGER.error("Too many login attempts. Retry later.")
raise ConfigEntryNotReady(
"Too many login attempts. Retry later."
) from exception
except Exception as exception: # pylint: disable=broad-except
LOGGER.exception(exception)
return False
Expand All @@ -98,8 +105,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
update_interval=timedelta(seconds=update_interval),
)

await coordinator.async_config_entry_first_refresh()

hass.data.setdefault(DOMAIN, {})[entry.entry_id] = HomeAssistantSagemcomFastData(
coordinator=coordinator, gateway=gateway
)
Expand All @@ -118,6 +123,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
configuration_url=f"{'https' if ssl else 'http'}://{host}",
)

await coordinator.async_config_entry_first_refresh()

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(update_listener))

Expand Down
1 change: 1 addition & 0 deletions custom_components/sagemcom_fast/button.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Support for Sagencom F@st buttons."""

from __future__ import annotations

from homeassistant.components.button import ButtonDeviceClass, ButtonEntity
Expand Down
65 changes: 35 additions & 30 deletions custom_components/sagemcom_fast/config_flow.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
"""Config flow for Sagemcom integration."""
import logging

from aiohttp import ClientError
from homeassistant import config_entries
Expand All @@ -13,64 +12,56 @@
from homeassistant.core import callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from sagemcom_api.client import SagemcomClient
from sagemcom_api.enums import EncryptionMethod
from sagemcom_api.exceptions import (
AccessRestrictionException,
AuthenticationException,
LoginRetryErrorException,
LoginTimeoutException,
MaximumSessionCountException,
UnsupportedHostException,
)
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(
{
vol.Required(CONF_HOST): str,
vol.Optional(CONF_USERNAME): str,
vol.Optional(CONF_PASSWORD): str,
vol.Required(CONF_ENCRYPTION_METHOD): vol.In(ENCRYPTION_METHODS),
vol.Required(CONF_SSL, default=False): bool,
vol.Required(CONF_VERIFY_SSL, default=False): bool,
}
)


class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Sagemcom."""

VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL

_host: str | None = None
_username: str | None = None

async def async_validate_input(self, user_input):
"""Validate user credentials."""
username = user_input.get(CONF_USERNAME) or ""
self._username = user_input.get(CONF_USERNAME) or ""
password = user_input.get(CONF_PASSWORD) or ""
host = user_input[CONF_HOST]
encryption_method = user_input[CONF_ENCRYPTION_METHOD]
self._host = user_input[CONF_HOST]
ssl = user_input[CONF_SSL]

session = async_get_clientsession(self.hass, user_input[CONF_VERIFY_SSL])

client = SagemcomClient(
host,
username,
password,
EncryptionMethod(encryption_method),
session,
host=self._host,
username=self._username,
password=password,
session=session,
ssl=ssl,
)

user_input[CONF_ENCRYPTION_METHOD] = await client.get_encryption_method()
LOGGER.debug(
"Detected encryption method: %s", user_input[CONF_ENCRYPTION_METHOD]
)

await client.login()
await client.logout()

return self.async_create_entry(
title=host,
title=self._host,
data=user_input,
)

Expand All @@ -89,18 +80,32 @@ async def async_step_user(self, user_input=None):
errors["base"] = "access_restricted"
except AuthenticationException:
errors["base"] = "invalid_auth"
except (TimeoutError, ClientError):
except (TimeoutError, ClientError, ConnectionError):
errors["base"] = "cannot_connect"
except LoginTimeoutException:
errors["base"] = "login_timeout"
except MaximumSessionCountException:
errors["base"] = "maximum_session_count"
except LoginRetryErrorException:
errors["base"] = "login_retry_error"
except UnsupportedHostException:
errors["base"] = "unsupported_host"
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
step_id="user",
data_schema=vol.Schema(
{
vol.Required(CONF_HOST, default=self._host): str,
vol.Optional(CONF_USERNAME, default=self._username): str,
vol.Optional(CONF_PASSWORD): str,
vol.Required(CONF_SSL, default=False): bool,
vol.Required(CONF_VERIFY_SSL, default=False): bool,
}
),
errors=errors,
)

@staticmethod
Expand Down
1 change: 1 addition & 0 deletions custom_components/sagemcom_fast/const.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Constants for the Sagemcom F@st integration."""

from __future__ import annotations

import logging
Expand Down
28 changes: 27 additions & 1 deletion custom_components/sagemcom_fast/coordinator.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
"""Helpers to help coordinate updates."""

from __future__ import annotations

import asyncio
from datetime import timedelta
import logging

from aiohttp.client_exceptions import ClientError
import async_timeout
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from sagemcom_api.client import SagemcomClient
from sagemcom_api.exceptions import (
AccessRestrictionException,
AuthenticationException,
LoginRetryErrorException,
MaximumSessionCountException,
UnauthorizedException,
)
from sagemcom_api.models import Device


Expand All @@ -33,13 +44,15 @@ def __init__(
self.data = {}
self.hosts: dict[str, Device] = {}
self.client = client
self.logger = logger

async def _async_update_data(self) -> dict[str, Device]:
"""Update hosts data."""
try:
async with async_timeout.timeout(10):
try:
await self.client.login()
await asyncio.sleep(1)
hosts = await self.client.get_hosts(only_active=True)
finally:
await self.client.logout()
Expand All @@ -52,5 +65,18 @@ async def _async_update_data(self) -> dict[str, Device]:
self.hosts[host.id] = host

return self.hosts
except AccessRestrictionException as exception:
raise ConfigEntryAuthFailed("Access restricted") from exception
except (AuthenticationException, UnauthorizedException) as exception:
raise ConfigEntryAuthFailed("Invalid credentials") from exception
except (TimeoutError, ClientError, ConnectionError) as exception:
raise UpdateFailed("Failed to connect") from exception
except LoginRetryErrorException as exception:
raise UpdateFailed(
"Too many login attempts. Retrying later."
) from exception
except MaximumSessionCountException as exception:
raise UpdateFailed("Maximum session count reached") from exception
except Exception as exception:
raise UpdateFailed(f"Error communicating with API: {exception}")
self.logger.exception(exception)
raise UpdateFailed(f"Error communicating with API: {str(exception)}")
1 change: 1 addition & 0 deletions custom_components/sagemcom_fast/device_tracker.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Support for device tracking of client router."""

from __future__ import annotations

from homeassistant.components.device_tracker import SourceType
Expand Down
1 change: 1 addition & 0 deletions custom_components/sagemcom_fast/diagnostics.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Provides diagnostics for Overkiz."""

from __future__ import annotations

from typing import Any
Expand Down
2 changes: 1 addition & 1 deletion custom_components/sagemcom_fast/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"iot_class": "local_polling",
"issue_tracker": "https://github.com/imicknl/ha-sagemcom-fast/issues",
"requirements": [
"sagemcom_api==1.1.0"
"sagemcom_api==1.3.2"
],
"ssdp": [
{
Expand Down
Loading
Loading