ha(ux): пробинг отдаёт fatal-reason (одна сессия на недоступном модуле); SWITCH_PROPS без битов

This commit is contained in:
2026-09-29 18:12:31 +03:00
parent 976a2dcbb8
commit 5f09716dce
6 changed files with 25 additions and 20 deletions
+4 -1
View File
@@ -275,7 +275,7 @@ def _run_trial(data: dict[str, Any]) -> tuple[TrialResult, Template, bool]:
model = str(data.get(CONF_MODEL) or "") model = str(data.get(CONF_MODEL) or "")
probed = False probed = False
if not model or detect(model) is None: if not model or detect(model) is None:
detected, _probe_props = probe_template( detected, _probe_props, fatal = probe_template(
data[CONF_HOST], data[CONF_HOST],
data[CONF_DSN], data[CONF_DSN],
data[CONF_LANIP_KEY], data[CONF_LANIP_KEY],
@@ -285,6 +285,9 @@ def _run_trial(data: dict[str, Any]) -> tuple[TrialResult, Template, bool]:
timeout=TRIAL_TIMEOUT, timeout=TRIAL_TIMEOUT,
) )
probed = True probed = True
if fatal is not None:
# Модуль недоступен/нет слотов/ключ — вторую сессию не открываем.
return TrialResult(ok=False, reason=fatal), template, True
if detected is not None: if detected is not None:
template = detected template = detected
result = trial_connect( result = trial_connect(
+10 -10
View File
@@ -96,16 +96,16 @@ PRESET_PROP = {
} }
SWING_MODES = ["off", "on"] SWING_MODES = ["off", "on"]
# switch-свойства: prop -> (translation_key, capability bit | None) # switch-свойства: prop -> translation_key (filtracija po FEATURES)
SWITCH_PROPS: dict[Prop, tuple[str, int | None]] = { SWITCH_PROPS: dict[Prop, str] = {
Prop.ECONOMY_MODE: ("economy_mode", CAP_ECONOMY), Prop.ECONOMY_MODE: "economy_mode",
Prop.POWERFUL_MODE: ("powerful_mode", CAP_POWERFUL), Prop.POWERFUL_MODE: "powerful_mode",
Prop.COIL_DRY_MODE: ("coil_dry_mode", CAP_COIL_DRY), Prop.COIL_DRY_MODE: "coil_dry_mode",
Prop.MIN_HEAT: ("min_heat", CAP_MIN_HEAT), Prop.MIN_HEAT: "min_heat",
Prop.OUTDOOR_LOW_NOISE: ("outdoor_low_noise", CAP_OUTDOOR_LOW_NOISE), Prop.OUTDOOR_LOW_NOISE: "outdoor_low_noise",
Prop.HUMAN_DET_AUTO_SAVE: ("human_det_auto_save", None), Prop.HUMAN_DET_AUTO_SAVE: "human_det_auto_save",
Prop.WIFI_LED_ENABLE: ("wifi_led_enable", None), Prop.WIFI_LED_ENABLE: "wifi_led_enable",
Prop.INDOOR_FAN_CONTROL: ("indoor_fan_control", CAP_INDOOR_FAN_CONTROL), Prop.INDOOR_FAN_CONTROL: "indoor_fan_control",
} }
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
+1 -1
View File
@@ -40,7 +40,7 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None:
template = runtime.client.template template = runtime.client.template
entities = [] entities = []
for prop, (key, _capability) in SWITCH_PROPS.items(): for prop, key in SWITCH_PROPS.items():
if prop_info(template, prop) is None: if prop_info(template, prop) is None:
continue continue
if not coordinator.features.enabled(key): if not coordinator.features.enabled(key):
+7 -6
View File
@@ -226,11 +226,12 @@ def probe_template(
device_port: int = 80, device_port: int = 80,
listen_port: int = 10275, listen_port: int = 10275,
timeout: float = 10.0, timeout: float = 10.0,
) -> tuple[Template | None, dict[Prop, Value]]: ) -> tuple[Template | None, dict[Prop, Value], str | None]:
"""Определяет шаблон по ответам, перебирая A/B/F короткими сессиями. """Определяет шаблон по ответам, перебирая A/B/F короткими сессиями.
Используется только когда oem_model неизвестна ядру: для известных Возвращает (template, properties, fatal_reason): если модуль недоступен
моделей шаблон берётся из C-таблиц без лишних подключений. (или нет слотов/ключ не тот), перебор бессмыслен и причина возвращается
третьим элементом, чтобы вызывающий не открывал ещё одну сессию.
""" """
answered: set[Prop] = set() answered: set[Prop] = set()
properties: dict[Prop, Value] = {} properties: dict[Prop, Value] = {}
@@ -248,10 +249,10 @@ def probe_template(
# Модуль доступен, но свойств шаблона нет — пробуем дальше. # Модуль доступен, но свойств шаблона нет — пробуем дальше.
continue continue
# Недоступен/нет слотов/ключ — перебор шаблонов бессмыслен. # Недоступен/нет слотов/ключ — перебор шаблонов бессмыслен.
return None, properties return None, properties, result.reason
answered |= result.answered answered |= result.answered
properties.update(result.properties) properties.update(result.properties)
detected = classify_template(answered) detected = classify_template(answered)
if detected is not None: if detected is not None:
return detected, properties return detected, properties, None
return classify_template(answered), properties return classify_template(answered), properties, None
+1 -1
View File
@@ -580,7 +580,7 @@ async def test_run_trial_probes_unknown_model(hass: HomeAssistant):
from custom_components.fglair import config_flow as cf from custom_components.fglair import config_flow as cf
with patch.object( with patch.object(
cf, "probe_template", return_value=(Template.B, {}) cf, "probe_template", return_value=(Template.B, {}, None)
) as probe, patch.object( ) as probe, patch.object(
cf, "trial_connect", return_value=TrialResult(ok=True) cf, "trial_connect", return_value=TrialResult(ok=True)
): ):
+2 -1
View File
@@ -115,7 +115,8 @@ def test_probe_stops_on_unreachable(monkeypatch):
return tr.TrialResult(ok=False, reason="unreachable") return tr.TrialResult(ok=False, reason="unreachable")
monkeypatch.setattr(tr, "trial_connect", fake_trial) monkeypatch.setattr(tr, "trial_connect", fake_trial)
template, props = tr.probe_template("host", "dsn", "key", 1) template, props, fatal = tr.probe_template("host", "dsn", "key", 1)
assert template is None assert template is None
assert props == {} assert props == {}
assert fatal == "unreachable"
assert calls == [Template.A] assert calls == [Template.A]