From d93233541cef566593c070c79ec5e6b5343774a4 Mon Sep 17 00:00:00 2001 From: Andrea Casarin Date: Sat, 10 Oct 2026 14:35:24 +0200 Subject: [PATCH 1/3] feat(config): propose the declared indoor monitor for Call Home --- .../vimar_intercom/options_flow.py | 41 ++++++++++++- custom_components/vimar_intercom/strings.json | 9 +++ .../vimar_intercom/translations/en.json | 9 +++ .../vimar_intercom/translations/it.json | 9 +++ docs/CONFIGURATION.it.md | 4 ++ docs/CONFIGURATION.md | 4 ++ tests/test_config_flow_steps.py | 61 +++++++++++++++++++ 7 files changed, 136 insertions(+), 1 deletion(-) diff --git a/custom_components/vimar_intercom/options_flow.py b/custom_components/vimar_intercom/options_flow.py index db986e5..e35e058 100644 --- a/custom_components/vimar_intercom/options_flow.py +++ b/custom_components/vimar_intercom/options_flow.py @@ -334,6 +334,7 @@ def __init__(self, config_entry: config_entries.ConfigEntry) -> None: # PICG dichiarato dal citofono stesso (get_info.php?action=nickname). # Resta None quando la rubrica arriva da un file caricato a mano. self._picg_from_rest: str | None = None + self._internal_panel_suggestion = "" async def async_step_init( self, user_input: dict | None = None @@ -341,10 +342,48 @@ async def async_step_init( """Menu: impostazioni a mano, rubrica dal citofono, o file rubrica.db.""" return self.async_show_menu( step_id="init", - menu_options=["settings", "homekit", "av_key", "fetch_rubrica", "fetch_rubrica_cloud", + menu_options=["settings", "internal_panel", "homekit", "av_key", "fetch_rubrica", "fetch_rubrica_cloud", "import_rubrica"], ) + async def async_step_internal_panel(self, user_input: dict | None = None) -> FlowResult: + """Propose the declared main indoor monitor; save only after confirmation.""" + current = {**self._entry.data, **self._entry.options} + errors: dict[str, str] = {} + if user_input is not None: + target = str(user_input.get(KEY_INTERNAL_PANEL_TARGET) or "").strip() + if validate.sip_target(target): + return self.async_create_entry( + title="", data={**self._entry.options, KEY_INTERNAL_PANEL_TARGET: target}) + errors[KEY_INTERNAL_PANEL_TARGET] = "invalid_target" + else: + hub = (self.hass.data.get(DOMAIN, {}).get(self._entry.entry_id) or {}).get("hub") + nicks = (hub.stats.get("nicknames") or []) if hub else [] + picg = str(current.get(KEY_PICG_TARGET) or PICG_TARGET).strip() + if not nicks and hub and hub.registered and validate.sip_target(picg): + result = await hub.async_find_picg([picg], sip_timeout=20, reply_wait=10) + nicks = result.get("nicknames") or [] + candidates = {str(n.get("ext") or "").strip() for n in nicks + if str(n.get("role") or "").upper() == "PICG" + and validate.sip_target(str(n.get("ext") or "").strip())} + self._internal_panel_suggestion = next(iter(candidates)) if len(candidates) == 1 else "" + if not self._internal_panel_suggestion: + errors["base"] = "internal_panel_not_found" + return self.async_show_form( + step_id="internal_panel", + data_schema=vol.Schema({vol.Required( + KEY_INTERNAL_PANEL_TARGET, + default=(user_input or {}).get(KEY_INTERNAL_PANEL_TARGET, + self._internal_panel_suggestion or current.get(KEY_INTERNAL_PANEL_TARGET) + or INTERNAL_PANEL_TARGET), + ): str}), + errors=errors, + description_placeholders={ + "detected": self._internal_panel_suggestion or "—", + "current": str(current.get(KEY_INTERNAL_PANEL_TARGET) or INTERNAL_PANEL_TARGET), + }, + ) + async def async_step_settings( self, user_input: dict | None = None ) -> FlowResult: diff --git a/custom_components/vimar_intercom/strings.json b/custom_components/vimar_intercom/strings.json index a0dbc04..59e7a9b 100644 --- a/custom_components/vimar_intercom/strings.json +++ b/custom_components/vimar_intercom/strings.json @@ -79,6 +79,7 @@ "init": { "title": "Impostazioni Vimar Intercom", "menu_options": { + "internal_panel": "Rileva il posto interno", "settings": "Impostazioni di rete e attuatori", "homekit": "HomeKit", "av_key": "Chiave dello stream /av", @@ -87,6 +88,13 @@ "fetch_rubrica_cloud": "Scarica la rubrica dal cloud Vimar" } }, + "internal_panel": { + "title": "Rileva il posto interno", + "description": "L’impianto dichiara {detected} come posto interno capogruppo (PICG). La destinazione attuale di Chiama Casa è {current}. Conferma per usare la destinazione proposta, oppure inserisci l’indirizzo noto di un posto interno. Il salvataggio non avvia una chiamata. La possibilità di chiamare il monitor dipende dall’impianto.", + "data": { + "internal_panel_target": "Indirizzo SIP del posto interno" + } + }, "settings": { "title": "Impostazioni Vimar Intercom", "description": "Rete SIP e attuatori dinamici. Nel campo Attuatori incolla la lista JSON prodotta da tools/parse_rubrica.py, oppure torna al menu e usa \"Importa attuatori da rubrica.db\". {actuators_error}", @@ -207,6 +215,7 @@ "invalid_actuator_target": "Target attuatore non valido ({actuators_error}): usa solo cifre (es. 55001) oppure AUTO.", "rubrica_import_failed": "Importazione fallita. Vedi il dettaglio nella descrizione.", "rubrica_no_actuators": "Nessun attuatore trovato per il GID specificato. Vedi la descrizione.", + "internal_panel_not_found": "Non è stato dichiarato un posto interno capogruppo univoco. Verifica la connessione e l’indirizzo PICG, oppure inserisci manualmente l’indirizzo noto di un posto interno.", "invalid_target": "Valore non valido: usa solo cifre (es. 55001).", "invalid_url": "URL non valido: deve iniziare con http:// o https:// e avere un host.", "no_local_proxy": "Manca l'indirizzo del citofono: impostalo in «Impostazioni di rete e attuatori».", diff --git a/custom_components/vimar_intercom/translations/en.json b/custom_components/vimar_intercom/translations/en.json index 69341be..1362077 100644 --- a/custom_components/vimar_intercom/translations/en.json +++ b/custom_components/vimar_intercom/translations/en.json @@ -79,6 +79,7 @@ "init": { "title": "Vimar Intercom settings", "menu_options": { + "internal_panel": "Find indoor monitor", "settings": "Network and actuator settings", "homekit": "HomeKit", "av_key": "/av stream key", @@ -87,6 +88,13 @@ "fetch_rubrica_cloud": "Download the phonebook from the Vimar cloud" } }, + "internal_panel": { + "title": "Find indoor monitor", + "description": "The system declares {detected} as its main indoor monitor (PICG). The current Call Home destination is {current}. Confirm to use the proposed destination, or enter a known indoor monitor address. This saves the destination without placing a call. Whether the monitor accepts calls depends on the plant.", + "data": { + "internal_panel_target": "Indoor monitor SIP address" + } + }, "settings": { "title": "Vimar Intercom settings", "description": "SIP network and dynamic actuators. In the Actuators field paste the JSON list produced by tools/parse_rubrica.py, or go back to the menu and use \"Import actuators from rubrica.db\". {actuators_error}", @@ -207,6 +215,7 @@ "invalid_actuator_target": "Invalid actuator target ({actuators_error}): digits only (e.g. 55001) or AUTO.", "rubrica_import_failed": "Import failed. See description above for details.", "rubrica_no_actuators": "No actuators found for the specified GID. See description above.", + "internal_panel_not_found": "No unique main indoor monitor was declared. Check connectivity and the PICG address, or enter a known indoor monitor address manually.", "invalid_target": "Invalid value: digits only (e.g. 55001).", "invalid_url": "Invalid URL: it must start with http:// or https:// and have a host.", "no_local_proxy": "The intercom address is missing: set it under \"Network and actuator settings\".", diff --git a/custom_components/vimar_intercom/translations/it.json b/custom_components/vimar_intercom/translations/it.json index a0dbc04..59e7a9b 100644 --- a/custom_components/vimar_intercom/translations/it.json +++ b/custom_components/vimar_intercom/translations/it.json @@ -79,6 +79,7 @@ "init": { "title": "Impostazioni Vimar Intercom", "menu_options": { + "internal_panel": "Rileva il posto interno", "settings": "Impostazioni di rete e attuatori", "homekit": "HomeKit", "av_key": "Chiave dello stream /av", @@ -87,6 +88,13 @@ "fetch_rubrica_cloud": "Scarica la rubrica dal cloud Vimar" } }, + "internal_panel": { + "title": "Rileva il posto interno", + "description": "L’impianto dichiara {detected} come posto interno capogruppo (PICG). La destinazione attuale di Chiama Casa è {current}. Conferma per usare la destinazione proposta, oppure inserisci l’indirizzo noto di un posto interno. Il salvataggio non avvia una chiamata. La possibilità di chiamare il monitor dipende dall’impianto.", + "data": { + "internal_panel_target": "Indirizzo SIP del posto interno" + } + }, "settings": { "title": "Impostazioni Vimar Intercom", "description": "Rete SIP e attuatori dinamici. Nel campo Attuatori incolla la lista JSON prodotta da tools/parse_rubrica.py, oppure torna al menu e usa \"Importa attuatori da rubrica.db\". {actuators_error}", @@ -207,6 +215,7 @@ "invalid_actuator_target": "Target attuatore non valido ({actuators_error}): usa solo cifre (es. 55001) oppure AUTO.", "rubrica_import_failed": "Importazione fallita. Vedi il dettaglio nella descrizione.", "rubrica_no_actuators": "Nessun attuatore trovato per il GID specificato. Vedi la descrizione.", + "internal_panel_not_found": "Non è stato dichiarato un posto interno capogruppo univoco. Verifica la connessione e l’indirizzo PICG, oppure inserisci manualmente l’indirizzo noto di un posto interno.", "invalid_target": "Valore non valido: usa solo cifre (es. 55001).", "invalid_url": "URL non valido: deve iniziare con http:// o https:// e avere un host.", "no_local_proxy": "Manca l'indirizzo del citofono: impostalo in «Impostazioni di rete e attuatori».", diff --git a/docs/CONFIGURATION.it.md b/docs/CONFIGURATION.it.md index 6d0743e..175e1ad 100644 --- a/docs/CONFIGURATION.it.md +++ b/docs/CONFIGURATION.it.md @@ -82,6 +82,10 @@ Impostazioni → Vimar Intercom → **Configura**: L'immagine della camera fra una chiamata e l'altra (l'ultima foto dello squillo) è visibile a ogni utente di Home Assistant che vede l'entità camera; `allowed_users` limita la cronologia degli squilli e i media dal vivo, non l'entità camera. +### Rilevare il posto interno + +Configura → **Rileva il posto interno** propone il posto interno capogruppo dichiarato nella risposta `GET_NICKS` (ruolo `PICG`). Usa i nickname già ricevuti oppure li richiede al PICG configurato, senza cercare altri indirizzi. Mostra la destinazione attuale di **Chiama Casa** e salva quella proposta solo dopo conferma. Non modifica SGA, PICG, targa video o attuatori e non avvia chiamate. Se non trova un PICG univoco, non indovina la destinazione; resta possibile inserire un indirizzo noto a mano. Verificato su un Tab 7S Up 40517, SW 2.1.0203, in cloud; la possibilità di chiamare il capogruppo va verificata sugli altri impianti. + ### Segreteria C'è un solo switch *Segreteria* (Configurazione, pagina del dispositivo). Acceso, usa il diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 80d5737..8bb6811 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -82,6 +82,10 @@ Settings → Vimar Intercom → **Configure**: The camera image between calls (the last ring photo) is visible to every Home Assistant user who can see the camera entity; `allowed_users` limits the ring history and live media, not the camera entity. +### Finding the indoor monitor + +Configure → **Find indoor monitor** proposes the main indoor monitor declared in a `GET_NICKS` reply (role `PICG`). It uses previously received nicknames or requests them from the configured PICG, without scanning other addresses. It shows the current **Call Home** destination and saves the proposal only when confirmed. SGA, PICG, camera and actuator settings are preserved, and no call is placed. Without a unique declared PICG it does not guess a destination; a known address can still be entered manually. The mapping was verified on a Tab 7S Up 40517, SW 2.1.0203, over the cloud; whether the main monitor accepts calls needs checking on other plants. + ### Voicemail There is one *Voicemail* switch (Configuration, device page). Turned on, it uses Home diff --git a/tests/test_config_flow_steps.py b/tests/test_config_flow_steps.py index 5c09dbe..8408603 100644 --- a/tests/test_config_flow_steps.py +++ b/tests/test_config_flow_steps.py @@ -760,3 +760,64 @@ def test_the_av_key_page_is_translated(of): assert set(page["data"]) == {"regenerate"}, name # hassfest refuses anything that looks like HTML, `` included. assert "<" not in json.dumps(page, ensure_ascii=False), name + + +# The internal monitor is proposed from declared roles, never from numbering. +@pytest.mark.parametrize("configured", ["", "52044"]) +def test_internal_panel_detection_proposes_the_declared_picg_without_saving(of, configured): + hub = types.SimpleNamespace(stats={"nicknames": [ + {"role": "PICG", "ext": "52011", "name": "Main monitor"}, + {"role": "PIM", "ext": "52901", "name": "Mobile client"}, + ]}) + flow = _options_flow(of, options={"internal_panel_target": configured}, + hass_data={"vimar_intercom": {"e1": {"hub": hub}}}) + result = asyncio.run(flow.async_step_internal_panel()) + assert result["type"] == "form" + assert result["description_placeholders"]["detected"] == "52011" + assert flow._entry.options["internal_panel_target"] == configured + saved = asyncio.run(flow.async_step_internal_panel({"internal_panel_target": "52011"})) + assert saved["data"]["internal_panel_target"] == "52011" + + +def test_internal_panel_confirmation_keeps_other_settings(of): + flow = _options_flow(of, options={"picg_target": "52011", "sga_target": "53011", + "camera_target": "54011", "homekit_accessory": True}) + saved = asyncio.run(flow.async_step_internal_panel({"internal_panel_target": "52011"})) + assert saved["data"] == {**flow._entry.options, "internal_panel_target": "52011"} + + +def test_internal_panel_detection_queries_only_the_configured_picg(of): + calls = [] + + async def find(targets, **kwargs): + calls.append(targets) + return {"nicknames": [{"role": "PICG", "ext": "52011", "name": "Main monitor"}]} + + hub = types.SimpleNamespace(stats={}, registered=True, async_find_picg=find) + flow = _options_flow(of, options={"picg_target": "52011"}, + hass_data={"vimar_intercom": {"e1": {"hub": hub}}}) + result = asyncio.run(flow.async_step_internal_panel()) + assert calls == [["52011"]] + assert result["description_placeholders"]["detected"] == "52011" + + +@pytest.mark.parametrize("nicks", [[], [{"role": "PIM", "ext": "52901"}], + [{"role": "PICG", "ext": "invalid"}], + [{"role": "PICG", "ext": "52011"}, + {"role": "PICG", "ext": "52012"}]]) +def test_internal_panel_detection_does_not_guess_missing_or_ambiguous_roles(of, nicks): + hub = types.SimpleNamespace(stats={"nicknames": nicks}, registered=False) + flow = _options_flow(of, hass_data={"vimar_intercom": {"e1": {"hub": hub}}}) + result = asyncio.run(flow.async_step_internal_panel()) + assert result["errors"] == {"base": "internal_panel_not_found"} + + +def test_internal_panel_detection_without_a_loaded_hub_reports_no_candidate(of): + result = asyncio.run(_options_flow(of).async_step_internal_panel()) + assert result["errors"] == {"base": "internal_panel_not_found"} + + +def test_internal_panel_confirmation_refuses_invalid_targets(of): + flow = _options_flow(of) + result = asyncio.run(flow.async_step_internal_panel({"internal_panel_target": "not-an-extension"})) + assert result["errors"]["internal_panel_target"] == "invalid_target" From f4fc8bdedd42097920e0b26633191c21261886cf Mon Sep 17 00:00:00 2001 From: Andrea Casarin Date: Sun, 11 Oct 2026 12:50:08 +0200 Subject: [PATCH 2/3] fix(config): qualify indoor monitor proposals and lookup failures --- .../vimar_intercom/options_flow.py | 29 +++++-- custom_components/vimar_intercom/strings.json | 7 +- .../vimar_intercom/translations/en.json | 7 +- .../vimar_intercom/translations/it.json | 7 +- docs/CONFIGURATION.it.md | 2 +- docs/CONFIGURATION.md | 2 +- tests/test_config_flow_steps.py | 76 ++++++++++++++++++- 7 files changed, 113 insertions(+), 17 deletions(-) diff --git a/custom_components/vimar_intercom/options_flow.py b/custom_components/vimar_intercom/options_flow.py index e35e058..1fc58ba 100644 --- a/custom_components/vimar_intercom/options_flow.py +++ b/custom_components/vimar_intercom/options_flow.py @@ -360,21 +360,38 @@ async def async_step_internal_panel(self, user_input: dict | None = None) -> Flo hub = (self.hass.data.get(DOMAIN, {}).get(self._entry.entry_id) or {}).get("hub") nicks = (hub.stats.get("nicknames") or []) if hub else [] picg = str(current.get(KEY_PICG_TARGET) or PICG_TARGET).strip() - if not nicks and hub and hub.registered and validate.sip_target(picg): - result = await hub.async_find_picg([picg], sip_timeout=20, reply_wait=10) - nicks = result.get("nicknames") or [] + has_picg = any(str(n.get("role") or "").upper() == rest_client.ROLE_PICG + for n in nicks) + if not has_picg: + if not hub or not hub.registered: + errors["base"] = "internal_panel_unavailable" + elif validate.sip_target(picg): + result = await hub.async_find_picg([picg], sip_timeout=20, reply_wait=10) + nicks = result.get("nicknames") or [] + if not result.get("ok"): + errors["base"] = "internal_panel_unavailable" + elif not nicks and not result.get("reply_after") and not any( + p.get("outcome") == "replied" for p in result.get("probes", []) + ): + errors["base"] = "internal_panel_no_reply" candidates = {str(n.get("ext") or "").strip() for n in nicks - if str(n.get("role") or "").upper() == "PICG" + if str(n.get("role") or "").upper() == rest_client.ROLE_PICG and validate.sip_target(str(n.get("ext") or "").strip())} self._internal_panel_suggestion = next(iter(candidates)) if len(candidates) == 1 else "" - if not self._internal_panel_suggestion: + sga = str(current.get(KEY_SGA_TARGET) or SGA_TARGET).strip() + if self._internal_panel_suggestion == sga: + errors["base"] = "internal_panel_same_as_sga" + elif not self._internal_panel_suggestion and not errors: errors["base"] = "internal_panel_not_found" + suggestion = self._internal_panel_suggestion + if suggestion == str(current.get(KEY_SGA_TARGET) or SGA_TARGET).strip(): + suggestion = "" return self.async_show_form( step_id="internal_panel", data_schema=vol.Schema({vol.Required( KEY_INTERNAL_PANEL_TARGET, default=(user_input or {}).get(KEY_INTERNAL_PANEL_TARGET, - self._internal_panel_suggestion or current.get(KEY_INTERNAL_PANEL_TARGET) + suggestion or current.get(KEY_INTERNAL_PANEL_TARGET) or INTERNAL_PANEL_TARGET), ): str}), errors=errors, diff --git a/custom_components/vimar_intercom/strings.json b/custom_components/vimar_intercom/strings.json index 59e7a9b..77c606f 100644 --- a/custom_components/vimar_intercom/strings.json +++ b/custom_components/vimar_intercom/strings.json @@ -90,7 +90,7 @@ }, "internal_panel": { "title": "Rileva il posto interno", - "description": "L’impianto dichiara {detected} come posto interno capogruppo (PICG). La destinazione attuale di Chiama Casa è {current}. Conferma per usare la destinazione proposta, oppure inserisci l’indirizzo noto di un posto interno. Il salvataggio non avvia una chiamata. La possibilità di chiamare il monitor dipende dall’impianto.", + "description": "L’impianto dichiara il PICG {detected}. La destinazione attuale di Chiama Casa è {current}. Viene proposto un PICG diverso dall’SGA; se coincidono si mantiene la destinazione attuale, perché il PICG può essere un controllore anziché un monitor. Conferma la destinazione o inserisci l’indirizzo noto di un posto interno. Il salvataggio non avvia una chiamata.", "data": { "internal_panel_target": "Indirizzo SIP del posto interno" } @@ -215,7 +215,10 @@ "invalid_actuator_target": "Target attuatore non valido ({actuators_error}): usa solo cifre (es. 55001) oppure AUTO.", "rubrica_import_failed": "Importazione fallita. Vedi il dettaglio nella descrizione.", "rubrica_no_actuators": "Nessun attuatore trovato per il GID specificato. Vedi la descrizione.", - "internal_panel_not_found": "Non è stato dichiarato un posto interno capogruppo univoco. Verifica la connessione e l’indirizzo PICG, oppure inserisci manualmente l’indirizzo noto di un posto interno.", + "internal_panel_not_found": "La risposta dei nickname non dichiara un PICG valido e univoco. Inserisci manualmente l’indirizzo noto di un posto interno.", + "internal_panel_no_reply": "Non è arrivata una risposta dei nickname dal PICG configurato. Alcuni impianti cloud non rispondono a GET_NICKS anche a un indirizzo esistente. Questo non dimostra che l’indirizzo sia errato; inserisci manualmente l’indirizzo noto di un posto interno.", + "internal_panel_unavailable": "La ricerca dei nickname non è disponibile: l’integrazione potrebbe essere disconnessa o un’altra ricerca potrebbe essere in corso. Riprova quando disponibile o inserisci manualmente l’indirizzo noto di un posto interno.", + "internal_panel_same_as_sga": "Il PICG dichiarato coincide con l’SGA, quindi questa risposta non permette di identificarlo come posto interno. Viene mantenuta la destinazione attuale di Chiama Casa.", "invalid_target": "Valore non valido: usa solo cifre (es. 55001).", "invalid_url": "URL non valido: deve iniziare con http:// o https:// e avere un host.", "no_local_proxy": "Manca l'indirizzo del citofono: impostalo in «Impostazioni di rete e attuatori».", diff --git a/custom_components/vimar_intercom/translations/en.json b/custom_components/vimar_intercom/translations/en.json index 1362077..e1b4c32 100644 --- a/custom_components/vimar_intercom/translations/en.json +++ b/custom_components/vimar_intercom/translations/en.json @@ -90,7 +90,7 @@ }, "internal_panel": { "title": "Find indoor monitor", - "description": "The system declares {detected} as its main indoor monitor (PICG). The current Call Home destination is {current}. Confirm to use the proposed destination, or enter a known indoor monitor address. This saves the destination without placing a call. Whether the monitor accepts calls depends on the plant.", + "description": "The system declares PICG {detected}. The current Call Home destination is {current}. A PICG different from the SGA is proposed; when they coincide the current destination is kept, because the PICG may be a controller rather than a monitor. Confirm the destination or enter a known indoor monitor address. Saving does not place a call.", "data": { "internal_panel_target": "Indoor monitor SIP address" } @@ -215,7 +215,10 @@ "invalid_actuator_target": "Invalid actuator target ({actuators_error}): digits only (e.g. 55001) or AUTO.", "rubrica_import_failed": "Import failed. See description above for details.", "rubrica_no_actuators": "No actuators found for the specified GID. See description above.", - "internal_panel_not_found": "No unique main indoor monitor was declared. Check connectivity and the PICG address, or enter a known indoor monitor address manually.", + "internal_panel_not_found": "The nickname reply does not declare a unique valid PICG. Enter a known indoor monitor address manually.", + "internal_panel_no_reply": "No nickname reply arrived from the configured PICG. Some cloud plants do not answer GET_NICKS even at an existing address. This does not prove the address is wrong; enter a known indoor monitor address manually.", + "internal_panel_unavailable": "The nickname lookup could not run: the integration may be disconnected or another search may be in progress. Retry once it is available, or enter a known indoor monitor address manually.", + "internal_panel_same_as_sga": "The declared PICG is also the SGA, so it cannot be identified as an indoor monitor from this reply. The current Call Home destination is kept.", "invalid_target": "Invalid value: digits only (e.g. 55001).", "invalid_url": "Invalid URL: it must start with http:// or https:// and have a host.", "no_local_proxy": "The intercom address is missing: set it under \"Network and actuator settings\".", diff --git a/custom_components/vimar_intercom/translations/it.json b/custom_components/vimar_intercom/translations/it.json index 59e7a9b..77c606f 100644 --- a/custom_components/vimar_intercom/translations/it.json +++ b/custom_components/vimar_intercom/translations/it.json @@ -90,7 +90,7 @@ }, "internal_panel": { "title": "Rileva il posto interno", - "description": "L’impianto dichiara {detected} come posto interno capogruppo (PICG). La destinazione attuale di Chiama Casa è {current}. Conferma per usare la destinazione proposta, oppure inserisci l’indirizzo noto di un posto interno. Il salvataggio non avvia una chiamata. La possibilità di chiamare il monitor dipende dall’impianto.", + "description": "L’impianto dichiara il PICG {detected}. La destinazione attuale di Chiama Casa è {current}. Viene proposto un PICG diverso dall’SGA; se coincidono si mantiene la destinazione attuale, perché il PICG può essere un controllore anziché un monitor. Conferma la destinazione o inserisci l’indirizzo noto di un posto interno. Il salvataggio non avvia una chiamata.", "data": { "internal_panel_target": "Indirizzo SIP del posto interno" } @@ -215,7 +215,10 @@ "invalid_actuator_target": "Target attuatore non valido ({actuators_error}): usa solo cifre (es. 55001) oppure AUTO.", "rubrica_import_failed": "Importazione fallita. Vedi il dettaglio nella descrizione.", "rubrica_no_actuators": "Nessun attuatore trovato per il GID specificato. Vedi la descrizione.", - "internal_panel_not_found": "Non è stato dichiarato un posto interno capogruppo univoco. Verifica la connessione e l’indirizzo PICG, oppure inserisci manualmente l’indirizzo noto di un posto interno.", + "internal_panel_not_found": "La risposta dei nickname non dichiara un PICG valido e univoco. Inserisci manualmente l’indirizzo noto di un posto interno.", + "internal_panel_no_reply": "Non è arrivata una risposta dei nickname dal PICG configurato. Alcuni impianti cloud non rispondono a GET_NICKS anche a un indirizzo esistente. Questo non dimostra che l’indirizzo sia errato; inserisci manualmente l’indirizzo noto di un posto interno.", + "internal_panel_unavailable": "La ricerca dei nickname non è disponibile: l’integrazione potrebbe essere disconnessa o un’altra ricerca potrebbe essere in corso. Riprova quando disponibile o inserisci manualmente l’indirizzo noto di un posto interno.", + "internal_panel_same_as_sga": "Il PICG dichiarato coincide con l’SGA, quindi questa risposta non permette di identificarlo come posto interno. Viene mantenuta la destinazione attuale di Chiama Casa.", "invalid_target": "Valore non valido: usa solo cifre (es. 55001).", "invalid_url": "URL non valido: deve iniziare con http:// o https:// e avere un host.", "no_local_proxy": "Manca l'indirizzo del citofono: impostalo in «Impostazioni di rete e attuatori».", diff --git a/docs/CONFIGURATION.it.md b/docs/CONFIGURATION.it.md index 175e1ad..580e172 100644 --- a/docs/CONFIGURATION.it.md +++ b/docs/CONFIGURATION.it.md @@ -84,7 +84,7 @@ L'immagine della camera fra una chiamata e l'altra (l'ultima foto dello squillo) ### Rilevare il posto interno -Configura → **Rileva il posto interno** propone il posto interno capogruppo dichiarato nella risposta `GET_NICKS` (ruolo `PICG`). Usa i nickname già ricevuti oppure li richiede al PICG configurato, senza cercare altri indirizzi. Mostra la destinazione attuale di **Chiama Casa** e salva quella proposta solo dopo conferma. Non modifica SGA, PICG, targa video o attuatori e non avvia chiamate. Se non trova un PICG univoco, non indovina la destinazione; resta possibile inserire un indirizzo noto a mano. Verificato su un Tab 7S Up 40517, SW 2.1.0203, in cloud; la possibilità di chiamare il capogruppo va verificata sugli altri impianti. +Configura → **Rileva il posto interno** propone il posto interno capogruppo dichiarato nella risposta `GET_NICKS` (ruolo `PICG`). Usa i nickname già ricevuti oppure li richiede al PICG configurato, senza cercare altri indirizzi. Mostra la destinazione attuale di **Chiama Casa** e salva quella proposta solo dopo conferma. Non modifica SGA, PICG, targa video o attuatori e non avvia chiamate. Se non trova un PICG univoco, non indovina la destinazione; resta possibile inserire un indirizzo noto a mano. Verificato su un Tab 7S Up 40517, SW 2.1.0203, in cloud; la possibilità di chiamare il capogruppo va verificata sugli altri impianti. Un PICG uguale all’SGA effettivo non viene proposto: si mantiene la destinazione attuale di Chiama Casa. Una cache senza PICG provoca una nuova ricerca. Mancata risposta e ricerca non disponibile vengono distinte da una risposta senza PICG valido e univoco. ### Segreteria diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 8bb6811..c1c6dd1 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -84,7 +84,7 @@ The camera image between calls (the last ring photo) is visible to every Home As ### Finding the indoor monitor -Configure → **Find indoor monitor** proposes the main indoor monitor declared in a `GET_NICKS` reply (role `PICG`). It uses previously received nicknames or requests them from the configured PICG, without scanning other addresses. It shows the current **Call Home** destination and saves the proposal only when confirmed. SGA, PICG, camera and actuator settings are preserved, and no call is placed. Without a unique declared PICG it does not guess a destination; a known address can still be entered manually. The mapping was verified on a Tab 7S Up 40517, SW 2.1.0203, over the cloud; whether the main monitor accepts calls needs checking on other plants. +Configure → **Find indoor monitor** proposes the main indoor monitor declared in a `GET_NICKS` reply (role `PICG`). It uses previously received nicknames or requests them from the configured PICG, without scanning other addresses. It shows the current **Call Home** destination and saves the proposal only when confirmed. SGA, PICG, camera and actuator settings are preserved, and no call is placed. Without a unique declared PICG it does not guess a destination; a known address can still be entered manually. The mapping was verified on a Tab 7S Up 40517, SW 2.1.0203, over the cloud; whether the main monitor accepts calls needs checking on other plants. A PICG equal to the effective SGA is not proposed: the current Call Home destination is kept. Cached nicknames without a PICG trigger a fresh lookup. No reply and an unavailable lookup are reported separately from a reply without a unique valid PICG. ### Voicemail diff --git a/tests/test_config_flow_steps.py b/tests/test_config_flow_steps.py index 8408603..93fc849 100644 --- a/tests/test_config_flow_steps.py +++ b/tests/test_config_flow_steps.py @@ -791,7 +791,7 @@ def test_internal_panel_detection_queries_only_the_configured_picg(of): async def find(targets, **kwargs): calls.append(targets) - return {"nicknames": [{"role": "PICG", "ext": "52011", "name": "Main monitor"}]} + return {"ok": True, "nicknames": [{"role": "PICG", "ext": "52011", "name": "Main monitor"}]} hub = types.SimpleNamespace(stats={}, registered=True, async_find_picg=find) flow = _options_flow(of, options={"picg_target": "52011"}, @@ -809,15 +809,85 @@ def test_internal_panel_detection_does_not_guess_missing_or_ambiguous_roles(of, hub = types.SimpleNamespace(stats={"nicknames": nicks}, registered=False) flow = _options_flow(of, hass_data={"vimar_intercom": {"e1": {"hub": hub}}}) result = asyncio.run(flow.async_step_internal_panel()) - assert result["errors"] == {"base": "internal_panel_not_found"} + expected = "internal_panel_not_found" if any(n["role"] == "PICG" for n in nicks) else "internal_panel_unavailable" + assert result["errors"] == {"base": expected} def test_internal_panel_detection_without_a_loaded_hub_reports_no_candidate(of): result = asyncio.run(_options_flow(of).async_step_internal_panel()) - assert result["errors"] == {"base": "internal_panel_not_found"} + assert result["errors"] == {"base": "internal_panel_unavailable"} def test_internal_panel_confirmation_refuses_invalid_targets(of): flow = _options_flow(of) result = asyncio.run(flow.async_step_internal_panel({"internal_panel_target": "not-an-extension"})) assert result["errors"]["internal_panel_target"] == "invalid_target" + + +@pytest.mark.parametrize("sga", ["", "53011"]) +@pytest.mark.parametrize("configured", ["", "52044"]) +def test_picg_equal_to_effective_sga_keeps_the_current_monitor(of, monkeypatch, sga, configured): + declared = sga or of.SGA_TARGET + hub = types.SimpleNamespace(stats={"nicknames": [{"role": "PICG", "ext": declared}]}) + flow = _options_flow(of, options={"sga_target": sga, "internal_panel_target": configured}, + hass_data={"vimar_intercom": {"e1": {"hub": hub}}}) + defaults = {} + + def required(key, default=None, **_kw): + defaults[key] = default + return key + + monkeypatch.setattr(of.vol, "Required", required) + result = asyncio.run(flow.async_step_internal_panel()) + assert result["errors"] == {"base": "internal_panel_same_as_sga"} + assert result["description_placeholders"]["detected"] == declared + assert defaults["internal_panel_target"] == (configured or of.INTERNAL_PANEL_TARGET) + assert flow._entry.options["internal_panel_target"] == configured + + +@pytest.mark.parametrize("reply, expected", [ + ({"ok": True, "nicknames": [], "probes": [{"outcome": "no_response"}]}, "internal_panel_no_reply"), + ({"ok": True, "nicknames": [], "probes": [{"outcome": "exists"}]}, "internal_panel_no_reply"), + ({"ok": False, "error": "search already running"}, "internal_panel_unavailable"), + ({"ok": False, "error": "not registered"}, "internal_panel_unavailable"), + ({"ok": True, "nicknames": [{"role": "PIM", "ext": "52901"}]}, "internal_panel_not_found"), + ({"ok": True, "nicknames": [], "reply_after": "52011"}, "internal_panel_not_found"), + ({"ok": True, "nicknames": [], "probes": [{"outcome": "replied"}]}, "internal_panel_not_found"), +]) +def test_internal_monitor_lookup_distinguishes_no_reply_and_unavailability(of, reply, expected): + async def find(targets, **kwargs): + assert targets == ["52011"] + return reply + + hub = types.SimpleNamespace(stats={}, registered=True, async_find_picg=find) + flow = _options_flow(of, options={"picg_target": "52011"}, + hass_data={"vimar_intercom": {"e1": {"hub": hub}}}) + result = asyncio.run(flow.async_step_internal_panel()) + assert result["errors"] == {"base": expected} + saved = asyncio.run(flow.async_step_internal_panel({"internal_panel_target": "52044"})) + assert saved["data"]["internal_panel_target"] == "52044" + + +def test_a_cache_without_picg_triggers_a_fresh_targeted_lookup(of): + calls = [] + + async def find(targets, **kwargs): + calls.append(targets) + return {"ok": True, "nicknames": [{"role": "PICG", "ext": "52011"}]} + + hub = types.SimpleNamespace(stats={"nicknames": [{"role": "PIM", "ext": "52901"}]}, + registered=True, async_find_picg=find) + flow = _options_flow(of, options={"picg_target": "52011"}, + hass_data={"vimar_intercom": {"e1": {"hub": hub}}}) + result = asyncio.run(flow.async_step_internal_panel()) + assert calls == [["52011"]] + assert result["errors"] == {} + assert result["description_placeholders"]["detected"] == "52011" + + +def test_invalid_configured_picg_is_not_queried(of): + hub = types.SimpleNamespace(stats={}, registered=True) + flow = _options_flow(of, options={"picg_target": "invalid"}, + hass_data={"vimar_intercom": {"e1": {"hub": hub}}}) + result = asyncio.run(flow.async_step_internal_panel()) + assert result["errors"] == {"base": "internal_panel_not_found"} From 97124a26eb4d1354330f364daccea6d4352e881a Mon Sep 17 00:00:00 2001 From: Andrea Casarin Date: Tue, 13 Oct 2026 14:06:52 +0200 Subject: [PATCH 3/3] fix(config): present shared PICG and SGA as information --- custom_components/vimar_intercom/options_flow.py | 5 +---- custom_components/vimar_intercom/strings.json | 3 +-- custom_components/vimar_intercom/translations/en.json | 3 +-- custom_components/vimar_intercom/translations/it.json | 3 +-- docs/CONFIGURATION.it.md | 2 +- docs/CONFIGURATION.md | 2 +- tests/test_config_flow_steps.py | 2 +- 7 files changed, 7 insertions(+), 13 deletions(-) diff --git a/custom_components/vimar_intercom/options_flow.py b/custom_components/vimar_intercom/options_flow.py index 1fc58ba..9ac0db7 100644 --- a/custom_components/vimar_intercom/options_flow.py +++ b/custom_components/vimar_intercom/options_flow.py @@ -378,10 +378,7 @@ async def async_step_internal_panel(self, user_input: dict | None = None) -> Flo if str(n.get("role") or "").upper() == rest_client.ROLE_PICG and validate.sip_target(str(n.get("ext") or "").strip())} self._internal_panel_suggestion = next(iter(candidates)) if len(candidates) == 1 else "" - sga = str(current.get(KEY_SGA_TARGET) or SGA_TARGET).strip() - if self._internal_panel_suggestion == sga: - errors["base"] = "internal_panel_same_as_sga" - elif not self._internal_panel_suggestion and not errors: + if not self._internal_panel_suggestion and not errors: errors["base"] = "internal_panel_not_found" suggestion = self._internal_panel_suggestion if suggestion == str(current.get(KEY_SGA_TARGET) or SGA_TARGET).strip(): diff --git a/custom_components/vimar_intercom/strings.json b/custom_components/vimar_intercom/strings.json index 77c606f..7988b2e 100644 --- a/custom_components/vimar_intercom/strings.json +++ b/custom_components/vimar_intercom/strings.json @@ -90,7 +90,7 @@ }, "internal_panel": { "title": "Rileva il posto interno", - "description": "L’impianto dichiara il PICG {detected}. La destinazione attuale di Chiama Casa è {current}. Viene proposto un PICG diverso dall’SGA; se coincidono si mantiene la destinazione attuale, perché il PICG può essere un controllore anziché un monitor. Conferma la destinazione o inserisci l’indirizzo noto di un posto interno. Il salvataggio non avvia una chiamata.", + "description": "L’impianto dichiara il PICG {detected}. La destinazione attuale di Chiama Casa è {current}. Se il PICG dichiarato è diverso dall’SGA viene proposto come posto interno; altrimenti resta la destinazione attuale, perché il PICG può essere un controllore anziché un monitor. Conferma la destinazione o inserisci l’indirizzo noto di un posto interno. Il salvataggio non avvia una chiamata.", "data": { "internal_panel_target": "Indirizzo SIP del posto interno" } @@ -218,7 +218,6 @@ "internal_panel_not_found": "La risposta dei nickname non dichiara un PICG valido e univoco. Inserisci manualmente l’indirizzo noto di un posto interno.", "internal_panel_no_reply": "Non è arrivata una risposta dei nickname dal PICG configurato. Alcuni impianti cloud non rispondono a GET_NICKS anche a un indirizzo esistente. Questo non dimostra che l’indirizzo sia errato; inserisci manualmente l’indirizzo noto di un posto interno.", "internal_panel_unavailable": "La ricerca dei nickname non è disponibile: l’integrazione potrebbe essere disconnessa o un’altra ricerca potrebbe essere in corso. Riprova quando disponibile o inserisci manualmente l’indirizzo noto di un posto interno.", - "internal_panel_same_as_sga": "Il PICG dichiarato coincide con l’SGA, quindi questa risposta non permette di identificarlo come posto interno. Viene mantenuta la destinazione attuale di Chiama Casa.", "invalid_target": "Valore non valido: usa solo cifre (es. 55001).", "invalid_url": "URL non valido: deve iniziare con http:// o https:// e avere un host.", "no_local_proxy": "Manca l'indirizzo del citofono: impostalo in «Impostazioni di rete e attuatori».", diff --git a/custom_components/vimar_intercom/translations/en.json b/custom_components/vimar_intercom/translations/en.json index e1b4c32..a1b0070 100644 --- a/custom_components/vimar_intercom/translations/en.json +++ b/custom_components/vimar_intercom/translations/en.json @@ -90,7 +90,7 @@ }, "internal_panel": { "title": "Find indoor monitor", - "description": "The system declares PICG {detected}. The current Call Home destination is {current}. A PICG different from the SGA is proposed; when they coincide the current destination is kept, because the PICG may be a controller rather than a monitor. Confirm the destination or enter a known indoor monitor address. Saving does not place a call.", + "description": "The system declares PICG {detected}. The current Call Home destination is {current}. If the declared PICG differs from the SGA, it is proposed as the indoor monitor; otherwise the current destination is kept, because the PICG may be a controller rather than a monitor. Confirm the destination or enter a known indoor monitor address. Saving does not place a call.", "data": { "internal_panel_target": "Indoor monitor SIP address" } @@ -218,7 +218,6 @@ "internal_panel_not_found": "The nickname reply does not declare a unique valid PICG. Enter a known indoor monitor address manually.", "internal_panel_no_reply": "No nickname reply arrived from the configured PICG. Some cloud plants do not answer GET_NICKS even at an existing address. This does not prove the address is wrong; enter a known indoor monitor address manually.", "internal_panel_unavailable": "The nickname lookup could not run: the integration may be disconnected or another search may be in progress. Retry once it is available, or enter a known indoor monitor address manually.", - "internal_panel_same_as_sga": "The declared PICG is also the SGA, so it cannot be identified as an indoor monitor from this reply. The current Call Home destination is kept.", "invalid_target": "Invalid value: digits only (e.g. 55001).", "invalid_url": "Invalid URL: it must start with http:// or https:// and have a host.", "no_local_proxy": "The intercom address is missing: set it under \"Network and actuator settings\".", diff --git a/custom_components/vimar_intercom/translations/it.json b/custom_components/vimar_intercom/translations/it.json index 77c606f..7988b2e 100644 --- a/custom_components/vimar_intercom/translations/it.json +++ b/custom_components/vimar_intercom/translations/it.json @@ -90,7 +90,7 @@ }, "internal_panel": { "title": "Rileva il posto interno", - "description": "L’impianto dichiara il PICG {detected}. La destinazione attuale di Chiama Casa è {current}. Viene proposto un PICG diverso dall’SGA; se coincidono si mantiene la destinazione attuale, perché il PICG può essere un controllore anziché un monitor. Conferma la destinazione o inserisci l’indirizzo noto di un posto interno. Il salvataggio non avvia una chiamata.", + "description": "L’impianto dichiara il PICG {detected}. La destinazione attuale di Chiama Casa è {current}. Se il PICG dichiarato è diverso dall’SGA viene proposto come posto interno; altrimenti resta la destinazione attuale, perché il PICG può essere un controllore anziché un monitor. Conferma la destinazione o inserisci l’indirizzo noto di un posto interno. Il salvataggio non avvia una chiamata.", "data": { "internal_panel_target": "Indirizzo SIP del posto interno" } @@ -218,7 +218,6 @@ "internal_panel_not_found": "La risposta dei nickname non dichiara un PICG valido e univoco. Inserisci manualmente l’indirizzo noto di un posto interno.", "internal_panel_no_reply": "Non è arrivata una risposta dei nickname dal PICG configurato. Alcuni impianti cloud non rispondono a GET_NICKS anche a un indirizzo esistente. Questo non dimostra che l’indirizzo sia errato; inserisci manualmente l’indirizzo noto di un posto interno.", "internal_panel_unavailable": "La ricerca dei nickname non è disponibile: l’integrazione potrebbe essere disconnessa o un’altra ricerca potrebbe essere in corso. Riprova quando disponibile o inserisci manualmente l’indirizzo noto di un posto interno.", - "internal_panel_same_as_sga": "Il PICG dichiarato coincide con l’SGA, quindi questa risposta non permette di identificarlo come posto interno. Viene mantenuta la destinazione attuale di Chiama Casa.", "invalid_target": "Valore non valido: usa solo cifre (es. 55001).", "invalid_url": "URL non valido: deve iniziare con http:// o https:// e avere un host.", "no_local_proxy": "Manca l'indirizzo del citofono: impostalo in «Impostazioni di rete e attuatori».", diff --git a/docs/CONFIGURATION.it.md b/docs/CONFIGURATION.it.md index 580e172..fb6a885 100644 --- a/docs/CONFIGURATION.it.md +++ b/docs/CONFIGURATION.it.md @@ -84,7 +84,7 @@ L'immagine della camera fra una chiamata e l'altra (l'ultima foto dello squillo) ### Rilevare il posto interno -Configura → **Rileva il posto interno** propone il posto interno capogruppo dichiarato nella risposta `GET_NICKS` (ruolo `PICG`). Usa i nickname già ricevuti oppure li richiede al PICG configurato, senza cercare altri indirizzi. Mostra la destinazione attuale di **Chiama Casa** e salva quella proposta solo dopo conferma. Non modifica SGA, PICG, targa video o attuatori e non avvia chiamate. Se non trova un PICG univoco, non indovina la destinazione; resta possibile inserire un indirizzo noto a mano. Verificato su un Tab 7S Up 40517, SW 2.1.0203, in cloud; la possibilità di chiamare il capogruppo va verificata sugli altri impianti. Un PICG uguale all’SGA effettivo non viene proposto: si mantiene la destinazione attuale di Chiama Casa. Una cache senza PICG provoca una nuova ricerca. Mancata risposta e ricerca non disponibile vengono distinte da una risposta senza PICG valido e univoco. +Configura → **Rileva il posto interno** propone il posto interno capogruppo dichiarato nella risposta `GET_NICKS` (ruolo `PICG`). Usa i nickname già ricevuti oppure li richiede al PICG configurato, senza cercare altri indirizzi. Mostra la destinazione attuale di **Chiama Casa** e salva quella proposta solo dopo conferma. Non modifica SGA, PICG, targa video o attuatori e non avvia chiamate. Se non trova un PICG univoco, non indovina la destinazione; resta possibile inserire un indirizzo noto a mano. Verificato su un Tab 7S Up 40517, SW 2.1.0203, in cloud; la possibilità di chiamare il capogruppo va verificata sugli altri impianti. Un PICG uguale all’SGA effettivo non viene proposto: si mantiene la destinazione attuale di Chiama Casa. ### Segreteria diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index c1c6dd1..93c542e 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -84,7 +84,7 @@ The camera image between calls (the last ring photo) is visible to every Home As ### Finding the indoor monitor -Configure → **Find indoor monitor** proposes the main indoor monitor declared in a `GET_NICKS` reply (role `PICG`). It uses previously received nicknames or requests them from the configured PICG, without scanning other addresses. It shows the current **Call Home** destination and saves the proposal only when confirmed. SGA, PICG, camera and actuator settings are preserved, and no call is placed. Without a unique declared PICG it does not guess a destination; a known address can still be entered manually. The mapping was verified on a Tab 7S Up 40517, SW 2.1.0203, over the cloud; whether the main monitor accepts calls needs checking on other plants. A PICG equal to the effective SGA is not proposed: the current Call Home destination is kept. Cached nicknames without a PICG trigger a fresh lookup. No reply and an unavailable lookup are reported separately from a reply without a unique valid PICG. +Configure → **Find indoor monitor** proposes the main indoor monitor declared in a `GET_NICKS` reply (role `PICG`). It uses previously received nicknames or requests them from the configured PICG, without scanning other addresses. It shows the current **Call Home** destination and saves the proposal only when confirmed. SGA, PICG, camera and actuator settings are preserved, and no call is placed. Without a unique declared PICG it does not guess a destination; a known address can still be entered manually. The mapping was verified on a Tab 7S Up 40517, SW 2.1.0203, over the cloud; whether the main monitor accepts calls needs checking on other plants. A PICG equal to the effective SGA is not proposed: the current Call Home destination is kept. ### Voicemail diff --git a/tests/test_config_flow_steps.py b/tests/test_config_flow_steps.py index 93fc849..8d949d8 100644 --- a/tests/test_config_flow_steps.py +++ b/tests/test_config_flow_steps.py @@ -839,7 +839,7 @@ def required(key, default=None, **_kw): monkeypatch.setattr(of.vol, "Required", required) result = asyncio.run(flow.async_step_internal_panel()) - assert result["errors"] == {"base": "internal_panel_same_as_sga"} + assert result["errors"] == {} assert result["description_placeholders"]["detected"] == declared assert defaults["internal_panel_target"] == (configured or of.INTERNAL_PANEL_TARGET) assert flow._entry.options["internal_panel_target"] == configured