Skip to content

Commit 0acc32e

Browse files
authored
2024 codestyle and modernization (#88)
* Update constants * Add host to gateway device * Add host to gateway device 2 * Add typing to device_tracker * Move coordinator to a new file * Add more typing * Add MAC address * Better type entry data * Bugfix * Add reboot button as entity
1 parent 10edb3f commit 0acc32e

File tree

5 files changed

+199
-165
lines changed

5 files changed

+199
-165
lines changed
Lines changed: 44 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,53 @@
1-
"""The Sagemcom integration."""
2-
import asyncio
1+
"""The Sagemcom F@st integration."""
2+
from __future__ import annotations
3+
4+
from dataclasses import dataclass
35
from datetime import timedelta
4-
import logging
56

67
from aiohttp.client_exceptions import ClientError
7-
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry
8+
from homeassistant.config_entries import ConfigEntry
89
from homeassistant.const import (
910
CONF_HOST,
1011
CONF_PASSWORD,
1112
CONF_SCAN_INTERVAL,
12-
CONF_SOURCE,
1313
CONF_SSL,
1414
CONF_USERNAME,
1515
CONF_VERIFY_SSL,
1616
)
1717
from homeassistant.core import HomeAssistant
18-
from homeassistant.exceptions import ConfigEntryNotReady
19-
from homeassistant.helpers import aiohttp_client, service
18+
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
19+
from homeassistant.helpers import aiohttp_client
2020
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
2121
from sagemcom_api.client import SagemcomClient
2222
from sagemcom_api.enums import EncryptionMethod
2323
from sagemcom_api.exceptions import (
2424
AccessRestrictionException,
2525
AuthenticationException,
26-
LoginTimeoutException,
2726
MaximumSessionCountException,
2827
UnauthorizedException,
2928
)
29+
from sagemcom_api.models import DeviceInfo as GatewayDeviceInfo
30+
31+
from .const import (
32+
CONF_ENCRYPTION_METHOD,
33+
DEFAULT_SCAN_INTERVAL,
34+
DOMAIN,
35+
LOGGER,
36+
PLATFORMS,
37+
)
38+
from .coordinator import SagemcomDataUpdateCoordinator
3039

31-
from .const import CONF_ENCRYPTION_METHOD, DEFAULT_SCAN_INTERVAL, DOMAIN
32-
from .device_tracker import SagemcomDataUpdateCoordinator
33-
34-
_LOGGER = logging.getLogger(__name__)
35-
36-
PLATFORMS = ["device_tracker"]
37-
38-
SERVICE_REBOOT = "reboot"
39-
40-
41-
async def async_setup(hass: HomeAssistant, config: dict):
42-
"""Set up the Sagemcom component."""
4340

44-
hass.data.setdefault(DOMAIN, {})
41+
@dataclass
42+
class HomeAssistantSagemcomFastData:
43+
"""SagemcomFast data stored in the Home Assistant data object."""
4544

46-
return True
45+
coordinator: SagemcomDataUpdateCoordinator
46+
gateway: GatewayDeviceInfo
4747

4848

4949
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
50-
"""Set up Sagemcom from a config entry."""
51-
50+
"""Set up Sagemcom F@st from a config entry."""
5251
host = entry.data[CONF_HOST]
5352
username = entry.data[CONF_USERNAME]
5453
password = entry.data[CONF_PASSWORD]
@@ -68,37 +67,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
6867

6968
try:
7069
await client.login()
71-
except AccessRestrictionException:
72-
_LOGGER.error("access_restricted")
73-
hass.async_create_task(
74-
hass.config_entries.flow.async_init(
75-
DOMAIN,
76-
context={CONF_SOURCE: SOURCE_REAUTH},
77-
data=entry.data,
78-
)
79-
)
80-
return False
81-
except (AuthenticationException, UnauthorizedException):
82-
_LOGGER.error("invalid_auth")
83-
hass.async_create_task(
84-
hass.config_entries.flow.async_init(
85-
DOMAIN,
86-
context={CONF_SOURCE: SOURCE_REAUTH},
87-
data=entry.data,
88-
)
89-
)
90-
return False
70+
except AccessRestrictionException as exception:
71+
LOGGER.error("Access restricted")
72+
raise ConfigEntryAuthFailed("Access restricted") from exception
73+
except (AuthenticationException, UnauthorizedException) as exception:
74+
LOGGER.error("Invalid_auth")
75+
raise ConfigEntryAuthFailed("Invalid credentials") from exception
9176
except (TimeoutError, ClientError) as exception:
92-
_LOGGER.error("Failed to connect")
77+
LOGGER.error("Failed to connect")
9378
raise ConfigEntryNotReady("Failed to connect") from exception
9479
except MaximumSessionCountException as exception:
95-
_LOGGER.error("Maximum session count reached")
80+
LOGGER.error("Maximum session count reached")
9681
raise ConfigEntryNotReady("Maximum session count reached") from exception
97-
except LoginTimeoutException:
98-
_LOGGER.error("Request timed-out")
99-
return False
10082
except Exception as exception: # pylint: disable=broad-except
101-
_LOGGER.exception(exception)
83+
LOGGER.exception(exception)
10284
return False
10385

10486
try:
@@ -110,18 +92,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
11092

11193
coordinator = SagemcomDataUpdateCoordinator(
11294
hass,
113-
_LOGGER,
95+
LOGGER,
11496
name="sagemcom_hosts",
11597
client=client,
11698
update_interval=timedelta(seconds=update_interval),
11799
)
118100

119-
await coordinator.async_refresh()
101+
await coordinator.async_config_entry_first_refresh()
120102

121-
hass.data[DOMAIN][entry.entry_id] = {
122-
"coordinator": coordinator,
123-
"update_listener": entry.add_update_listener(update_listener),
124-
}
103+
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = HomeAssistantSagemcomFastData(
104+
coordinator=coordinator, gateway=gateway
105+
)
125106

126107
# Create gateway device in Home Assistant
127108
device_registry = hass.helpers.device_registry.async_get(hass)
@@ -134,36 +115,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
134115
name=f"{gateway.manufacturer} {gateway.model_number}",
135116
model=gateway.model_name,
136117
sw_version=gateway.software_version,
118+
configuration_url=f"{'https' if ssl else 'http'}://{host}",
137119
)
138120

139-
# Register components
140121
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
141-
142-
# Handle gateway device services
143-
async def async_command_reboot(call):
144-
"""Handle reboot service call."""
145-
await client.reboot()
146-
147-
service.async_register_admin_service(
148-
hass, DOMAIN, SERVICE_REBOOT, async_command_reboot
149-
)
122+
entry.async_on_unload(entry.add_update_listener(update_listener))
150123

151124
return True
152125

153126

154-
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
127+
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
155128
"""Unload a config entry."""
156-
157-
unload_ok = all(
158-
await asyncio.gather(
159-
*[
160-
hass.config_entries.async_forward_entry_unload(entry, component)
161-
for component in PLATFORMS
162-
]
163-
)
164-
)
165-
if unload_ok:
166-
hass.data[DOMAIN][entry.entry_id]["update_listener"]()
129+
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
167130
hass.data[DOMAIN].pop(entry.entry_id)
168131

169132
return unload_ok
@@ -172,9 +135,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
172135
async def update_listener(hass: HomeAssistant, entry: ConfigEntry):
173136
"""Update when entry options update."""
174137
if entry.options[CONF_SCAN_INTERVAL]:
175-
coordinator = hass.data[DOMAIN][entry.entry_id]["coordinator"]
176-
coordinator.update_interval = timedelta(
138+
data: HomeAssistantSagemcomFastData = hass.data[DOMAIN][entry.entry_id]
139+
data.coordinator.update_interval = timedelta(
177140
seconds=entry.options[CONF_SCAN_INTERVAL]
178141
)
179142

180-
await coordinator.async_refresh()
143+
await data.coordinator.async_refresh()
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"""Support for Sagencom F@st buttons."""
2+
from __future__ import annotations
3+
4+
from homeassistant.components.button import ButtonDeviceClass, ButtonEntity
5+
from homeassistant.config_entries import ConfigEntry
6+
from homeassistant.const import EntityCategory
7+
from homeassistant.core import HomeAssistant
8+
from homeassistant.helpers.device_registry import DeviceInfo
9+
from homeassistant.helpers.entity_platform import AddEntitiesCallback
10+
from sagemcom_api.client import SagemcomClient
11+
from sagemcom_api.models import DeviceInfo as GatewayDeviceInfo
12+
13+
from . import HomeAssistantSagemcomFastData
14+
from .const import DOMAIN
15+
16+
17+
async def async_setup_entry(
18+
hass: HomeAssistant,
19+
entry: ConfigEntry,
20+
async_add_entities: AddEntitiesCallback,
21+
) -> None:
22+
"""Set up the Sagemcom F@st button from a config entry."""
23+
data: HomeAssistantSagemcomFastData = hass.data[DOMAIN][entry.entry_id]
24+
entities: list[ButtonEntity] = []
25+
entities.append(SagemcomFastRebootButton(data.gateway, data.coordinator.client))
26+
27+
async_add_entities(entities)
28+
29+
30+
class SagemcomFastRebootButton(ButtonEntity):
31+
"""Representation of an Sagemcom F@st Button."""
32+
33+
_attr_has_entity_name = True
34+
_attr_name = "Reboot"
35+
_attr_device_class = ButtonDeviceClass.RESTART
36+
_attr_entity_category = EntityCategory.CONFIG
37+
38+
def __init__(self, gateway: GatewayDeviceInfo, client: SagemcomClient) -> None:
39+
"""Initialize the button."""
40+
self.gateway = gateway
41+
self.client = client
42+
self._attr_unique_id = f"{self.gateway.serial_number}_reboot"
43+
44+
async def async_press(self) -> None:
45+
"""Handle the button press."""
46+
await self.client.reboot()
47+
48+
@property
49+
def device_info(self) -> DeviceInfo:
50+
"""Return device registry information for this entity."""
51+
return DeviceInfo(
52+
identifiers={(DOMAIN, self.gateway.serial_number)},
53+
)
Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,25 @@
1-
"""Constants for the Sagemcom integration."""
2-
DOMAIN = "sagemcom_fast"
1+
"""Constants for the Sagemcom F@st integration."""
2+
from __future__ import annotations
33

4-
CONF_ENCRYPTION_METHOD = "encryption_method"
5-
CONF_TRACK_WIRELESS_CLIENTS = "track_wireless_clients"
6-
CONF_TRACK_WIRED_CLIENTS = "track_wired_clients"
4+
import logging
5+
from typing import Final
76

8-
DEFAULT_TRACK_WIRELESS_CLIENTS = True
9-
DEFAULT_TRACK_WIRED_CLIENTS = True
7+
from homeassistant.const import Platform
108

11-
ATTR_MANUFACTURER = "Sagemcom"
9+
LOGGER: logging.Logger = logging.getLogger(__package__)
1210

13-
MIN_SCAN_INTERVAL = 10
14-
DEFAULT_SCAN_INTERVAL = 10
11+
DOMAIN: Final = "sagemcom_fast"
12+
13+
CONF_ENCRYPTION_METHOD: Final = "encryption_method"
14+
CONF_TRACK_WIRELESS_CLIENTS: Final = "track_wireless_clients"
15+
CONF_TRACK_WIRED_CLIENTS: Final = "track_wired_clients"
16+
17+
DEFAULT_TRACK_WIRELESS_CLIENTS: Final = True
18+
DEFAULT_TRACK_WIRED_CLIENTS: Final = True
19+
20+
ATTR_MANUFACTURER: Final = "Sagemcom"
21+
22+
MIN_SCAN_INTERVAL: Final = 10
23+
DEFAULT_SCAN_INTERVAL: Final = 10
24+
25+
PLATFORMS: list[Platform] = [Platform.DEVICE_TRACKER, Platform.BUTTON]
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
"""Helpers to help coordinate updates."""
2+
from __future__ import annotations
3+
4+
from datetime import timedelta
5+
import logging
6+
7+
import async_timeout
8+
from homeassistant.core import HomeAssistant
9+
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
10+
from sagemcom_api.client import SagemcomClient
11+
from sagemcom_api.models import Device
12+
13+
14+
class SagemcomDataUpdateCoordinator(DataUpdateCoordinator):
15+
"""Class to manage fetching Sagemcom data."""
16+
17+
def __init__(
18+
self,
19+
hass: HomeAssistant,
20+
logger: logging.Logger,
21+
*,
22+
name: str,
23+
client: SagemcomClient,
24+
update_interval: timedelta | None = None,
25+
):
26+
"""Initialize update coordinator."""
27+
super().__init__(
28+
hass,
29+
logger,
30+
name=name,
31+
update_interval=update_interval,
32+
)
33+
self.data = {}
34+
self.hosts: dict[str, Device] = {}
35+
self.client = client
36+
37+
async def _async_update_data(self) -> dict[str, Device]:
38+
"""Update hosts data."""
39+
try:
40+
async with async_timeout.timeout(10):
41+
try:
42+
await self.client.login()
43+
hosts = await self.client.get_hosts(only_active=True)
44+
finally:
45+
await self.client.logout()
46+
47+
"""Mark all device as non-active."""
48+
for idx, host in self.hosts.items():
49+
host.active = False
50+
self.hosts[idx] = host
51+
for host in hosts:
52+
self.hosts[host.id] = host
53+
54+
return self.hosts
55+
except Exception as exception:
56+
raise UpdateFailed(f"Error communicating with API: {exception}")

0 commit comments

Comments
 (0)