Skip to content

Commit 1cf492b

Browse files
author
GnoX
committed
Rework reconnection logic, add better connection error handling
1 parent c377f62 commit 1cf492b

7 files changed

Lines changed: 529 additions & 164 deletions

File tree

Lines changed: 91 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,36 @@
11
"""The unofficial EcoFlow BLE devices integration"""
22

3-
from __future__ import annotations
4-
53
import logging
4+
from functools import partial
65

6+
import homeassistant.helpers.issue_registry as ir
77
from homeassistant.components import bluetooth
88
from homeassistant.config_entries import ConfigEntry
99
from homeassistant.const import CONF_ADDRESS, CONF_TYPE, Platform
1010
from homeassistant.core import HomeAssistant
11-
from homeassistant.exceptions import ConfigEntryNotReady
11+
from homeassistant.exceptions import (
12+
ConfigEntryError,
13+
ConfigEntryNotReady,
14+
)
1215
from homeassistant.helpers.device_registry import DeviceInfo
1316

1417
from . import eflib
1518
from .config_flow import ConfLogOptions, LogOptions
16-
from .const import CONF_UPDATE_PERIOD, CONF_USER_ID, DOMAIN, MANUFACTURER
19+
from .const import (
20+
CONF_CONNECTION_TIMEOUT,
21+
CONF_UPDATE_PERIOD,
22+
CONF_USER_ID,
23+
DEFAULT_CONNECTION_TIMEOUT,
24+
DEFAULT_UPDATE_PERIOD,
25+
DOMAIN,
26+
MANUFACTURER,
27+
)
28+
from .eflib.connection import (
29+
AuthFailedError,
30+
BleakError,
31+
ConnectionTimeout,
32+
MaxConnectionAttemptsReached,
33+
)
1734

1835
PLATFORMS: list[Platform] = [
1936
Platform.SENSOR,
@@ -27,18 +44,8 @@
2744

2845
_LOGGER = logging.getLogger(__name__)
2946

30-
31-
class _ConfigNotReady(ConfigEntryNotReady):
32-
def __init__(
33-
self,
34-
translation_key: str | None = None,
35-
translation_placeholders: dict[str, str] | None = None,
36-
) -> None:
37-
super().__init__(
38-
translation_domain=DOMAIN,
39-
translation_key=translation_key,
40-
translation_placeholders=translation_placeholders,
41-
)
47+
ConfigEntryNotReady = partial(ConfigEntryNotReady, translation_domain=DOMAIN)
48+
ConfigEntryError = partial(ConfigEntryError, translation_domain=DOMAIN)
4249

4350

4451
async def async_setup_entry(hass: HomeAssistant, entry: DeviceConfigEntry) -> bool:
@@ -49,45 +56,91 @@ async def async_setup_entry(hass: HomeAssistant, entry: DeviceConfigEntry) -> bo
4956
address = entry.data.get(CONF_ADDRESS)
5057
user_id = entry.data.get(CONF_USER_ID)
5158
merged_options = entry.data | entry.options
52-
update_period = merged_options.get(CONF_UPDATE_PERIOD, 0)
59+
update_period = merged_options.get(CONF_UPDATE_PERIOD, DEFAULT_UPDATE_PERIOD)
60+
timeout = merged_options.get(CONF_CONNECTION_TIMEOUT, DEFAULT_CONNECTION_TIMEOUT)
5361

5462
if address is None or user_id is None:
5563
return False
5664

5765
if not bluetooth.async_address_present(hass, address):
58-
raise _ConfigNotReady("device_not_present")
66+
raise ConfigEntryNotReady(translation_key="device_not_present")
5967

6068
_LOGGER.debug("Connecting Device")
61-
discovery_info = bluetooth.async_last_service_info(hass, address, connectable=True)
62-
device = eflib.NewDevice(discovery_info.device, discovery_info.advertisement)
69+
device: eflib.DeviceBase | None = getattr(entry, "runtime_data", None)
6370
if device is None:
64-
raise _ConfigNotReady("unable_to_create_device")
71+
discovery_info = bluetooth.async_last_service_info(
72+
hass, address, connectable=True
73+
)
74+
device = eflib.NewDevice(discovery_info.device, discovery_info.advertisement)
75+
if device is None:
76+
raise ConfigEntryNotReady(translation_key="unable_to_create_device")
6577

66-
await (
67-
device.with_update_period(update_period)
68-
.with_logging_options(ConfLogOptions.from_config(merged_options))
69-
.connect(user_id)
70-
)
71-
entry.runtime_data = device
78+
entry.runtime_data = device
7279

73-
timeout = 30
74-
state = await device.wait_until_connected_or_error(timeout=timeout)
80+
issue_id = f"{entry.entry_id}_max_connection_attempts"
7581

76-
if state.connection_error():
77-
raise _ConfigNotReady(
78-
"could_not_connect", translation_placeholders={"time": str(timeout)}
82+
try:
83+
await (
84+
device.with_update_period(update_period)
85+
.with_logging_options(ConfLogOptions.from_config(merged_options))
86+
.with_disabled_reconnect()
87+
.connect(user_id, timeout=timeout)
7988
)
80-
if state.is_error():
81-
raise _ConfigNotReady("error_after_connected")
82-
if not state.authenticated():
83-
raise _ConfigNotReady("could_not_authenticate")
89+
state = await device.wait_until_authenticated_or_error(raise_on_error=True)
90+
except (ConnectionTimeout, BleakError, TimeoutError) as e:
91+
raise ConfigEntryNotReady(
92+
translation_key="could_not_connect",
93+
translation_placeholders={"time": str(timeout)},
94+
) from e
95+
except AuthFailedError as e:
96+
raise ConfigEntryNotReady(translation_key="authentication_failed") from e
97+
except MaxConnectionAttemptsReached as e:
98+
await device.disconnect()
99+
ir.async_create_issue(
100+
hass,
101+
DOMAIN,
102+
issue_id,
103+
is_fixable=False,
104+
severity=ir.IssueSeverity.ERROR,
105+
translation_key="max_connection_attempts_reached",
106+
translation_placeholders={
107+
"device_name": device.name,
108+
"attempts": str(e.attempts),
109+
},
110+
)
111+
raise ConfigEntryError(
112+
translation_key="could_not_connect_no_retry",
113+
translation_placeholders={"attempts": str(e.attempts)},
114+
) from e
115+
except Exception as e:
116+
_LOGGER.exception("Unknown error")
117+
await device.disconnect()
118+
raise ConfigEntryNotReady(
119+
translation_key="unknown_error", translation_placeholders={"error": str(e)}
120+
) from e
121+
else:
122+
if not state.authenticated():
123+
await device.disconnect()
124+
raise ConfigEntryNotReady(
125+
translation_key="failed_after_successful_connection",
126+
translation_placeholders={"last_state": state},
127+
)
128+
ir.async_delete_issue(hass, DOMAIN, issue_id)
84129

85130
_LOGGER.debug("Creating entities")
86131
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
87132

88133
_LOGGER.debug("Setup done")
89134
entry.async_on_unload(entry.add_update_listener(_update_listener))
90135

136+
def _on_disconnect(exc: Exception | type[Exception] | None):
137+
async def _disconnect_and_reload():
138+
hass.config_entries.async_schedule_reload(entry.entry_id)
139+
140+
hass.async_create_task(_disconnect_and_reload())
141+
142+
entry.async_on_unload(device.on_disconnect(_on_disconnect))
143+
91144
return True
92145

93146

@@ -112,7 +165,7 @@ def device_info(entry: ConfigEntry) -> DeviceInfo:
112165
async def _update_listener(hass: HomeAssistant, entry: DeviceConfigEntry):
113166
device = entry.runtime_data
114167
merged_options = entry.data | entry.options
115-
update_period = merged_options.get(CONF_UPDATE_PERIOD, 0)
116-
device.with_update_period(update_period).with_logging_options(
168+
update_period = merged_options.get(CONF_UPDATE_PERIOD, DEFAULT_UPDATE_PERIOD)
169+
device.with_update_period(period=update_period).with_logging_options(
117170
ConfLogOptions.from_config(merged_options)
118171
)

custom_components/ef_ble/config_flow.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import asyncio
56
import base64
67
import logging
78
from collections.abc import Mapping
@@ -28,6 +29,7 @@
2829

2930
from . import eflib
3031
from .const import (
32+
CONF_CONNECTION_TIMEOUT,
3133
CONF_LOG_BLEAK,
3234
CONF_LOG_CONNECTION,
3335
CONF_LOG_ENCRYPTED_PAYLOADS,
@@ -37,6 +39,8 @@
3739
CONF_LOG_PAYLOADS,
3840
CONF_UPDATE_PERIOD,
3941
CONF_USER_ID,
42+
DEFAULT_CONNECTION_TIMEOUT,
43+
DEFAULT_UPDATE_PERIOD,
4044
DOMAIN,
4145
)
4246
from .eflib.connection import ConnectionState
@@ -117,6 +121,7 @@ async def async_step_bluetooth_confirm(
117121
**self._login_option(),
118122
vol.Required(CONF_ADDRESS): vol.In([f"{title} ({device.address})"]),
119123
**_update_period_option(),
124+
**_timeout_option(),
120125
**ConfLogOptions.schema(
121126
ConfLogOptions.to_config(self._log_options)
122127
),
@@ -176,6 +181,7 @@ async def async_step_user(
176181
**self._login_option(),
177182
vol.Required(CONF_ADDRESS): vol.In(self._discovered_devices.keys()),
178183
**_update_period_option(),
184+
**_timeout_option(),
179185
**ConfLogOptions.schema(
180186
ConfLogOptions.to_config(self._log_options)
181187
),
@@ -242,6 +248,7 @@ async def _validate_user_id(
242248
self._email = user_input.get("login", {}).get(CONF_EMAIL, "")
243249
password = user_input.get("login", {}).get(CONF_PASSWORD, "")
244250
user_id = user_input.get(CONF_USER_ID, "")
251+
timeout = user_input.get(CONF_CONNECTION_TIMEOUT, 20)
245252

246253
self._collapsed = False
247254

@@ -259,8 +266,10 @@ async def _validate_user_id(
259266

260267
device.with_logging_options(ConfLogOptions.from_config(user_input))
261268

262-
await device.connect(self._user_id, max_attempts=4)
263-
conn_state = await device.wait_until_connected_or_error(timeout=20)
269+
await device.connect(self._user_id)
270+
conn_state = await asyncio.wait_for(
271+
device.wait_until_authenticated_or_error(), timeout
272+
)
264273
await device.disconnect()
265274

266275
error = None
@@ -335,7 +344,9 @@ async def async_step_init(self, user_input: dict[str, Any] | None = None):
335344

336345
merged_entry = self.config_entry.data | self.config_entry.options
337346
options = {
338-
CONF_UPDATE_PERIOD: merged_entry.get(CONF_UPDATE_PERIOD, 0),
347+
CONF_UPDATE_PERIOD: merged_entry.get(
348+
CONF_UPDATE_PERIOD, DEFAULT_UPDATE_PERIOD
349+
),
339350
}
340351

341352
device: eflib.DeviceBase | None = getattr(
@@ -412,9 +423,17 @@ def schema(
412423
}
413424

414425

415-
def _update_period_option(default: int = 0):
426+
def _update_period_option(default: int = DEFAULT_UPDATE_PERIOD):
416427
return {
417428
vol.Optional(CONF_UPDATE_PERIOD, default=default): vol.All(
418429
int, vol.Range(min=0)
419430
)
420431
}
432+
433+
434+
def _timeout_option(default: int = DEFAULT_CONNECTION_TIMEOUT):
435+
return {
436+
vol.Optional(CONF_CONNECTION_TIMEOUT, default=default): vol.All(
437+
int, vol.Range(min=0)
438+
)
439+
}

custom_components/ef_ble/const.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
CONF_USER_ID = "user_id"
77
CONF_UPDATE_PERIOD = "update_period"
8+
CONF_CONNECTION_TIMEOUT = "connection_timeout"
89

910
CONF_LOG_MASKED = "log_masked"
1011
CONF_LOG_PACKETS = "log_packets"
@@ -13,3 +14,7 @@
1314
CONF_LOG_MESSAGES = "log_messages"
1415
CONF_LOG_CONNECTION = "log_connection"
1516
CONF_LOG_BLEAK = "log_bleak"
17+
18+
19+
DEFAULT_UPDATE_PERIOD = 10
20+
DEFAULT_CONNECTION_TIMEOUT = 20

0 commit comments

Comments
 (0)