Skip to content

Commit e5b4a2d

Browse files
author
Maksym Shtembuliak
committed
fix: preserve cloud identity and guard learning recovery
1 parent c1e210b commit e5b4a2d

27 files changed

Lines changed: 720 additions & 167 deletions

‎CHANGELOG.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,17 @@ the GitHub release body should be rendered from the matching version section her
8181

8282
### Fixed
8383

84+
- Device learning keeps the original cloud API when the collector is temporarily
85+
redirected to Home Assistant. A local port or AT traffic cannot replace a
86+
known ValueCloud, SmartValue or SmartESS server with another provider (#46).
87+
Unconfirmed endpoint restoration blocks a new active scan and offers an
88+
explicit recovery action instead of reporting the connection as restored.
89+
90+
- Collector metadata polling no longer requests nearby Wi-Fi scans. Connected
91+
SSID and signal readings remain local queries; Wi-Fi setup still offers scans
92+
on demand. This removes unnecessary background work without assuming it was
93+
the cause of reported connection interruptions.
94+
8495
- Newly learned Modbus controls retain the captured FC06 or FC16 write command
8596
through profile generation and runtime dispatch. Incomplete, conflicting or
8697
unrepresentable write shapes remain support evidence instead of guessed

‎custom_components/eybond_local/collector/at_runtime.py‎

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -199,12 +199,6 @@ def _decode_collector_server_endpoint(response: CollectorAtResponse) -> dict[str
199199
_decode_text_value("collector_link_status"),
200200
frozenset({"collector_link_status"}),
201201
),
202-
CollectorAtQueryDefinition(
203-
"INTPARA49",
204-
"Nearby Wi-Fi scan list reported by the collector.",
205-
_decode_text_value("collector_wifi_scan_list"),
206-
frozenset({"collector_wifi_scan_list"}),
207-
),
208202
# Keep optional numbered queries after the established metadata set. An
209203
# older collector that stays silent for this read still leaves every prior
210204
# value available as a fresh partial result.
@@ -240,7 +234,7 @@ async def read_runtime_collector_at_values(
240234
the link is unusable, not the commands unsupported);
241235
* a timeout/disconnect AFTER some metadata -> ``partial`` (fresh, no strike);
242236
one timeout ends the sweep so a dead link costs one request timeout, not
243-
thirteen;
237+
twelve;
244238
* every command delivered but none carried metadata -> ``empty`` (a strike:
245239
the collector answered but does not support this channel);
246240
* an individual unsupported/rejected command is skipped, not fatal.

‎custom_components/eybond_local/collector/cloud_family.py‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929

3030
_SOURCE_RANK = {
3131
"": 0,
32-
COLLECTOR_CLOUD_FAMILY_SOURCE_ENDPOINT_HOST: 1,
32+
COLLECTOR_CLOUD_FAMILY_SOURCE_ENDPOINT_HOST: 3,
3333
COLLECTOR_CLOUD_FAMILY_SOURCE_EXPLICIT_ENDPOINT_PORT: 1,
3434
COLLECTOR_CLOUD_FAMILY_SOURCE_TRANSPORT_SNIFF: 2,
3535
}
@@ -189,9 +189,12 @@ def select_preferred_collector_cloud_family(
189189
for observation in observations:
190190
if not observation.known:
191191
continue
192+
# AT traffic and shared ports identify a wire family, not a particular
193+
# cloud account provider. A known cloud host remains authoritative even
194+
# when the collector is temporarily redirected to a local listener.
192195
rank = (
193-
_CONFIDENCE_RANK.get(observation.confidence, 0),
194196
_SOURCE_RANK.get(observation.source, 0),
197+
_CONFIDENCE_RANK.get(observation.confidence, 0),
195198
)
196199
if rank > selected_rank:
197200
selected = observation

‎custom_components/eybond_local/collector/transport_profile.py‎

Lines changed: 64 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
from ..const import (
1414
CONF_COLLECTOR_CLOUD_FAMILY,
1515
CONF_COLLECTOR_ORIGINAL_SERVER_ENDPOINT,
16+
CONF_COLLECTOR_ORIGINAL_SERVER_ENDPOINT_OBSERVED_AT,
1617
CONF_COLLECTOR_ORIGINAL_SERVER_ENDPOINT_PROFILE_KEY,
18+
CONF_COLLECTOR_ORIGINAL_SERVER_ENDPOINT_SOURCE,
1719
CONF_DRIVER_HINT,
1820
DRIVER_HINT_AUTO,
1921
)
@@ -24,7 +26,13 @@
2426
resolve_collector_cloud_raw_passthrough_min_interval_ms,
2527
resolve_collector_cloud_session_protocol,
2628
)
27-
from .cloud_family import collector_cloud_family_observation_from_endpoint
29+
from .cloud_family import (
30+
COLLECTOR_CLOUD_FAMILY_SOURCE_ENDPOINT_HOST,
31+
CollectorCloudFamilyObservation,
32+
collector_cloud_family_observation_from_endpoint,
33+
collector_cloud_family_observation_from_mapping,
34+
select_preferred_collector_cloud_family,
35+
)
2836
from .capabilities import (
2937
COLLECTOR_KIND_ENTRY_KEY,
3038
COLLECTOR_KIND_ESP_EYBOND_BRIDGE,
@@ -118,34 +126,69 @@ def collector_cloud_family_from_entry_context(
118126
options: Mapping[str, object],
119127
*,
120128
extra_endpoints: tuple[object, ...] = (),
129+
extra_observations: tuple[CollectorCloudFamilyObservation, ...] = (),
121130
) -> str:
122131
"""Resolve collector cloud family from durable and runtime entry context."""
123132

124-
for source in (data, options):
125-
family = known_collector_cloud_family(source.get(CONF_COLLECTOR_CLOUD_FAMILY, ""))
126-
if family:
127-
return family
133+
return known_collector_cloud_family(
134+
collector_cloud_family_observation_from_entry_context(
135+
data, options,
136+
extra_endpoints=extra_endpoints,
137+
extra_observations=extra_observations,
138+
).family
139+
)
128140

129-
for source in (options, data):
130-
family = known_collector_cloud_family(
131-
source.get(CONF_COLLECTOR_ORIGINAL_SERVER_ENDPOINT_PROFILE_KEY, "")
132-
)
133-
if family:
134-
return family
135141

136-
for endpoint in extra_endpoints:
137-
family = _known_family_from_endpoint(endpoint)
138-
if family:
139-
return family
142+
def collector_cloud_family_observation_from_entry_context(
143+
data: Mapping[str, object],
144+
options: Mapping[str, object],
145+
*,
146+
extra_endpoints: tuple[object, ...] = (),
147+
extra_observations: tuple[CollectorCloudFamilyObservation, ...] = (),
148+
) -> CollectorCloudFamilyObservation:
149+
"""Resolve cloud identity without confusing a local route with a provider.
150+
151+
A current known cloud host wins over the remembered host. Local endpoints,
152+
shared ports and wire sniffing cannot replace either. Preserve the original
153+
endpoint as one record: data owns it, options is only a legacy fallback.
154+
This projection never chooses or confirms a session's wire protocol.
155+
"""
140156

141-
for source in (options, data):
142-
family = _known_family_from_endpoint(
143-
source.get(CONF_COLLECTOR_ORIGINAL_SERVER_ENDPOINT, "")
157+
original_fields = (
158+
CONF_COLLECTOR_ORIGINAL_SERVER_ENDPOINT,
159+
CONF_COLLECTOR_ORIGINAL_SERVER_ENDPOINT_PROFILE_KEY,
160+
CONF_COLLECTOR_ORIGINAL_SERVER_ENDPOINT_SOURCE,
161+
CONF_COLLECTOR_ORIGINAL_SERVER_ENDPOINT_OBSERVED_AT,
162+
)
163+
original = data if any(key in data for key in original_fields) else options
164+
endpoint_observations = tuple(
165+
collector_cloud_family_observation_from_endpoint(endpoint)
166+
for endpoint in (
167+
*extra_endpoints,
168+
original.get(CONF_COLLECTOR_ORIGINAL_SERVER_ENDPOINT, ""),
144169
)
145-
if family:
146-
return family
170+
)
171+
for observation in (*endpoint_observations, *extra_observations):
172+
if (
173+
observation.source == COLLECTOR_CLOUD_FAMILY_SOURCE_ENDPOINT_HOST
174+
and known_collector_cloud_family(observation.family)
175+
):
176+
return observation
177+
178+
original_family = known_collector_cloud_family(
179+
original.get(CONF_COLLECTOR_ORIGINAL_SERVER_ENDPOINT_PROFILE_KEY, "")
180+
)
181+
if original_family:
182+
return CollectorCloudFamilyObservation(family=original_family)
147183

148-
return ""
184+
durable_observations = tuple(
185+
collector_cloud_family_observation_from_mapping(source)
186+
for source in (data, options)
187+
)
188+
for observation in (*extra_observations, *durable_observations):
189+
if known_collector_cloud_family(observation.family):
190+
return observation
191+
return select_preferred_collector_cloud_family(*endpoint_observations)
149192

150193

151194
def resolve_collector_transport_profile(
@@ -300,8 +343,3 @@ def resolve_collector_transport_profile_from_entry_context(
300343
runtime_owner_key=runtime_owner_key,
301344
virtual_bridge=entry_context_is_virtual_bridge(data, options),
302345
)
303-
304-
305-
def _known_family_from_endpoint(endpoint: object) -> str:
306-
observation = collector_cloud_family_observation_from_endpoint(endpoint)
307-
return known_collector_cloud_family(observation.family)

‎custom_components/eybond_local/flow_translations/en.json‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,9 @@
494494
"control_discovery_cloud_network": "Home Assistant could not reach the cloud service. Check its internet connection.",
495495
"control_discovery_cloud_unexpected": "The cloud service returned an unexpected response.",
496496
"cloud_active_no_safe_controls": "The selected cloud API did not expose any safe controls that could be checked for this device. Nothing was changed.",
497+
"shadow_learning_restore_pending": "Home Assistant could not confirm restoration of the previous collector connection. Open Expand device support → Analyze cloud data and device capabilities, then retry restoring the connection before starting another check. Download a Support Archive if restoration keeps failing.",
498+
"shadow_learning_restore_action": "Retry restoring the collector connection",
499+
"shadow_learning_restore_confirmed": "✓ The previous collector connection was restored. You can start another check.",
497500
"control_discovery_failure_generic": "Home Assistant stopped safely and restored the collector connection.",
498501
"control_discovery_result_failed": "The check couldn't finish. {control_discovery_error} Try again, download the support package so the developer can inspect what happened, or return to the menu.",
499502
"control_discovery_result_action_retry": "Try the scan again",

‎custom_components/eybond_local/flow_translations/ru.json‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,9 @@
494494
"control_discovery_cloud_network": "Home Assistant не удалось связаться с облачным сервисом. Проверьте подключение к интернету.",
495495
"control_discovery_cloud_unexpected": "Облачный сервис вернул неожиданный ответ.",
496496
"cloud_active_no_safe_controls": "Выбранный облачный API не предоставил безопасных элементов управления, которые можно проверить для этого устройства. Ничего не изменено.",
497+
"shadow_learning_restore_pending": "Home Assistant не смог подтвердить восстановление прежнего подключения коллектора. Откройте «Расширить поддержку устройства → Проанализировать облачные данные и возможности устройства» и повторите восстановление подключения перед новой проверкой. Если восстановление снова не удастся, загрузите пакет поддержки.",
498+
"shadow_learning_restore_action": "Повторить восстановление подключения коллектора",
499+
"shadow_learning_restore_confirmed": "✓ Прежнее подключение коллектора восстановлено. Можно начать новую проверку.",
497500
"control_discovery_failure_generic": "Home Assistant безопасно остановил процесс и восстановил подключение коллектора.",
498501
"control_discovery_result_failed": "Проверку не удалось завершить. {control_discovery_error} Повторите попытку, загрузите пакет поддержки, чтобы разработчик мог разобраться, или вернитесь в меню.",
499502
"control_discovery_result_action_retry": "Повторить попытку",

‎custom_components/eybond_local/flow_translations/uk.json‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,9 @@
494494
"control_discovery_cloud_network": "Home Assistant не вдалося зв’язатися з хмарним сервісом. Перевірте підключення до інтернету.",
495495
"control_discovery_cloud_unexpected": "Хмарний сервіс повернув неочікувану відповідь.",
496496
"cloud_active_no_safe_controls": "Вибраний хмарний API не надав безпечних елементів керування, які можна перевірити для цього пристрою. Нічого не змінено.",
497+
"shadow_learning_restore_pending": "Home Assistant не зміг підтвердити відновлення попереднього підключення колектора. Відкрийте «Розширити підтримку пристрою → Проаналізувати хмарні дані та можливості пристрою» та повторіть відновлення підключення перед новою перевіркою. Якщо відновлення знову не вдасться, завантажте пакет підтримки.",
498+
"shadow_learning_restore_action": "Повторити відновлення підключення колектора",
499+
"shadow_learning_restore_confirmed": "✓ Попереднє підключення колектора відновлено. Можна почати нову перевірку.",
497500
"control_discovery_failure_generic": "Home Assistant безпечно зупинив процес і відновив підключення колектора.",
498501
"control_discovery_result_failed": "Перевірку не вдалося завершити. {control_discovery_error} Повторіть спробу, завантажте пакет підтримки, щоб розробник міг розібратися, або поверніться до меню.",
499502
"control_discovery_result_action_retry": "Повторити спробу",

0 commit comments

Comments
 (0)