diff --git a/custom_components/vimar_intercom/options_flow.py b/custom_components/vimar_intercom/options_flow.py index 6e4d948..3b5f2b3 100644 --- a/custom_components/vimar_intercom/options_flow.py +++ b/custom_components/vimar_intercom/options_flow.py @@ -197,6 +197,10 @@ def _network_schema(form: dict) -> dict: "local_udp_port", default=form.get(KEY_LOCAL_UDP_PORT, DEFAULT_LOCAL_UDP_PORT) ): vol.All(vol.Coerce(int), vol.Range(min=1024, max=65535)), + vol.Optional( + "video_enabled", + default=form.get("video_enabled", True), + ): bool, # auto (segue il media_enc dichiarato dall'impianto) / on / off (issue #4). vol.Optional( KEY_MEDIA_ENC, @@ -439,6 +443,8 @@ async def async_step_settings( KEY_USE_LOCAL_UDP: use_local_udp, KEY_LOCAL_UDP_PORT: local_udp_port, KEY_MEDIA_ENC: media_enc, + "video_enabled": user_input.get( + "video_enabled", current.get("video_enabled", True)), KEY_VOICE_ANSWER: voice_answer, KEY_ACTUATORS: actuators, **targets, diff --git a/custom_components/vimar_intercom/strings.json b/custom_components/vimar_intercom/strings.json index 42dee75..cbe4ddc 100644 --- a/custom_components/vimar_intercom/strings.json +++ b/custom_components/vimar_intercom/strings.json @@ -104,6 +104,7 @@ "local_udp_port": "Porta UDP locale", "voice_answer": "Risposta a voce (microfono su /audio_ws)", "media_enc": "Cifratura del media (SRTP)", + "video_enabled": "Abilita video nelle chiamate in uscita", "actuators": "Attuatori (JSON)", "sga_target": "SGA (id SIP segreteria/DND)", "picg_target": "PICG (id SIP GET_INIT_STATUS)", @@ -126,6 +127,7 @@ "data_description": { "voice_answer": "Chi può rispondere alla chiamata parlando mentre squilla. Solo dichiarato: risponde solo chi si collega con /audio_ws?voice_answer=1 (Echo Show, HomeKit via Scrypted). Mai: la voce non risponde. Chiunque: risponde qualsiasi connessione con microfono; attenzione, un tablet a muro con il microfono rimasto aperto può rispondere da solo con i rumori di casa.", "media_enc": "Automatico: segue quello che dichiara l'impianto nella risposta a GET_INIT_STATUS (\"media_enc\": \"srtp\" su un Tab 5S Up 40515 in cloud); se l'impianto non lo dichiara (risposta corta, come sul 40507) resta in chiaro. Attivo/Disattivo forzano. Se la camera non si accende o la chiamata fallisce con 488, prova Attivo.", + "video_enabled": "Inizialmente segue il QR di abbinamento. Attiva se la targa ha una telecamera ma Vedi esterno si collega solo con audio (Stream has no video). Disattiva per targhe solo audio. Le chiamate in arrivo continuano a seguire i media offerti dall’impianto.", "actuators": "Lista JSON di oggetti name, msg, target, icon. icon: door|light|switch. target \"AUTO\" usa la targa che apre la porta. Vuoto = nessun bottone.", "sga_target": "Destinatario di VOICEMAIL;/DND;, e dell'apertura porta se «Targa che apre la porta» è vuota. Vuoto = default storico (55001). Cambialo solo se il tuo impianto usa un altro SGA (SYSTEM.MAGIC_APT_INTERCOM in rubrica.db).", "picg_target": "Destinatario di GET_INIT_STATUS. Sugli impianti verificati finora coincide con l'SGA. Vuoto = default storico (55001).", diff --git a/custom_components/vimar_intercom/translations/en.json b/custom_components/vimar_intercom/translations/en.json index 27ea7ba..ee6d346 100644 --- a/custom_components/vimar_intercom/translations/en.json +++ b/custom_components/vimar_intercom/translations/en.json @@ -104,6 +104,7 @@ "local_udp_port": "Local UDP port", "voice_answer": "Voice answer (mic on /audio_ws)", "media_enc": "Media encryption (SRTP)", + "video_enabled": "Enable video in outgoing calls", "actuators": "Actuators (JSON)", "sga_target": "SGA (voicemail/DND SIP id)", "picg_target": "PICG (GET_INIT_STATUS SIP id)", @@ -126,6 +127,7 @@ "data_description": { "voice_answer": "Who can answer the call by talking while it rings. Declared only: only connections using /audio_ws?voice_answer=1 (Echo Show, HomeKit via Scrypted). Never: talking never answers. Anyone: any connection with a microphone can answer; beware, a wall tablet whose microphone was left open can answer by itself with household noise.", "media_enc": "Automatic: follows what the system declares in its GET_INIT_STATUS reply (\"media_enc\": \"srtp\" on a cloud Tab 5S Up 40515); when the system does not declare it (short reply, as on the 40507) media stays plain. On/Off force it. If the camera stays black or the call fails with 488, try On.", + "video_enabled": "Initially follows the pairing QR. Enable this if the entrance has a camera but View outside connects with audio only (Stream has no video). Disable it for audio-only entrances. Incoming calls still follow the media offered by the system.", "actuators": "JSON list of objects name, msg, target, icon. icon: door|light|switch. target \"AUTO\" uses the panel that opens the door. Empty = no buttons.", "sga_target": "Target for VOICEMAIL;/DND;, and for the door-open command when “Entrance panel that opens the door” is empty. Empty = historical default (55001). Change it only if your system uses a different SGA (SYSTEM.MAGIC_APT_INTERCOM in rubrica.db).", "picg_target": "Target for GET_INIT_STATUS. On systems verified so far it matches the SGA. Empty = historical default (55001).", diff --git a/custom_components/vimar_intercom/translations/it.json b/custom_components/vimar_intercom/translations/it.json index 42dee75..cbe4ddc 100644 --- a/custom_components/vimar_intercom/translations/it.json +++ b/custom_components/vimar_intercom/translations/it.json @@ -104,6 +104,7 @@ "local_udp_port": "Porta UDP locale", "voice_answer": "Risposta a voce (microfono su /audio_ws)", "media_enc": "Cifratura del media (SRTP)", + "video_enabled": "Abilita video nelle chiamate in uscita", "actuators": "Attuatori (JSON)", "sga_target": "SGA (id SIP segreteria/DND)", "picg_target": "PICG (id SIP GET_INIT_STATUS)", @@ -126,6 +127,7 @@ "data_description": { "voice_answer": "Chi può rispondere alla chiamata parlando mentre squilla. Solo dichiarato: risponde solo chi si collega con /audio_ws?voice_answer=1 (Echo Show, HomeKit via Scrypted). Mai: la voce non risponde. Chiunque: risponde qualsiasi connessione con microfono; attenzione, un tablet a muro con il microfono rimasto aperto può rispondere da solo con i rumori di casa.", "media_enc": "Automatico: segue quello che dichiara l'impianto nella risposta a GET_INIT_STATUS (\"media_enc\": \"srtp\" su un Tab 5S Up 40515 in cloud); se l'impianto non lo dichiara (risposta corta, come sul 40507) resta in chiaro. Attivo/Disattivo forzano. Se la camera non si accende o la chiamata fallisce con 488, prova Attivo.", + "video_enabled": "Inizialmente segue il QR di abbinamento. Attiva se la targa ha una telecamera ma Vedi esterno si collega solo con audio (Stream has no video). Disattiva per targhe solo audio. Le chiamate in arrivo continuano a seguire i media offerti dall’impianto.", "actuators": "Lista JSON di oggetti name, msg, target, icon. icon: door|light|switch. target \"AUTO\" usa la targa che apre la porta. Vuoto = nessun bottone.", "sga_target": "Destinatario di VOICEMAIL;/DND;, e dell'apertura porta se «Targa che apre la porta» è vuota. Vuoto = default storico (55001). Cambialo solo se il tuo impianto usa un altro SGA (SYSTEM.MAGIC_APT_INTERCOM in rubrica.db).", "picg_target": "Destinatario di GET_INIT_STATUS. Sugli impianti verificati finora coincide con l'SGA. Vuoto = default storico (55001).", diff --git a/docs/CONFIGURATION.it.md b/docs/CONFIGURATION.it.md index fb6a885..3b1a580 100644 --- a/docs/CONFIGURATION.it.md +++ b/docs/CONFIGURATION.it.md @@ -75,6 +75,7 @@ Impostazioni → Vimar Intercom → **Configura**: | **Messaggio di assenza** (`away_message_file`, `away_message_delay`) | file audio (mp3, wav…) fatto sentire al visitatore se nessuno risponde entro N secondi; poi l'integrazione riaggancia. Se il Tab espone il ritardo della segreteria si usa quello (*Segreteria · ritardo*) | | **Messaggio di assenza da testo** (`away_message_text`, `away_message_tts`) | se il campo file è vuoto, questo testo lo legge la sintesi vocale di Home Assistant (`away_message_tts` = un'entità `tts.*`; vuoto = il motore predefinito di HA) nella lingua di HA, max 30 s. L'audio si genera all'avvio e resta in cache; se il TTS fallisce il citofono squilla come sempre | | **Cifratura del media (SRTP)** (`media_enc`) | **Automatico** (default dalla 1.0.11): segue il `media_enc` che l'impianto dichiara nella risposta a `GET_INIT_STATUS` (`"srtp"` su un 40515 in cloud); gli impianti con la risposta corta (il 40507) restano in RTP chiaro. **Attivo** / **Disattivo** lo forzano. Chi aveva salvato «attivo» con la 1.0.10 o prima resta attivo; «spento» diventa automatico. Prova **Attivo** se la camera resta nera o la chiamata fallisce con `488` | +| **Abilita video nelle chiamate in uscita** (`video_enabled`) | Inizialmente segue il QR di abbinamento (attivo se il campo manca). Attiva se la targa ha una telecamera ma *Vedi esterno* si collega solo con audio (`Stream has no video`). Disattiva per targhe solo audio. Le chiamate in arrivo seguono ancora i media offerti dall’impianto. Il salvataggio ricarica l’integrazione. | | **Risposta a voce** (`voice_answer`) | Chi può rispondere a uno squillo parlando su `/audio_ws`: **Solo dichiarato** (default, solo con `?voice_answer=1`), **Mai**, **Chiunque** (qualsiasi connessione con microfono; un tablet a muro col microfono rimasto aperto può rispondere da solo coi rumori di casa) | | **Webhook squillo** (`ring_webhook_url`, `ring_end_webhook_url`) | GET opzionale (fire-and-forget, timeout 5 s) inviata quando inizia uno squillo e quando finisce (risposto, annullato o non risposto) — es. gli URL `turnOn`/`turnOff` di un Dummy Switch Scrypted (vedi [docs/EXTERNAL.md](EXTERNAL.md)). Un fallimento logga solo un warning, mai blocca lo squillo. Vuoto = disattivato | diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 93c542e..3c3970a 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -75,6 +75,7 @@ Settings → Vimar Intercom → **Configure**: | **Away message** (`away_message_file`, `away_message_delay`) | Audio file (mp3, wav…) played to the visitor if nobody answers within N seconds; then the integration hangs up. If the Tab exposes the voicemail delay, that one is used instead (*Voicemail · delay*) | | **Away message from text** (`away_message_text`, `away_message_tts`) | If the file field is empty, this text is read by Home Assistant's text-to-speech (`away_message_tts` = a `tts.*` entity; empty = HA's default engine) in HA's language, max 30 s. The audio is generated at startup and cached; if TTS fails the doorbell keeps ringing as usual | | **Media encryption (SRTP)** (`media_enc`) | **Automatic** (default since 1.0.11): follows the `media_enc` the plant declares in its `GET_INIT_STATUS` reply (`"srtp"` on a cloud 40515); plants with the short reply (the 40507) stay on plain RTP. **On** / **Off** force it. Entries saved as "on" by 1.0.10 or earlier stay on; "off" becomes automatic. Try **On** if the camera stays black or the call fails with `488` | +| **Enable video in outgoing calls** (`video_enabled`) | Initially follows the pairing QR (ON if the field is absent). Enable it if the entrance has a camera but *View outside* connects with audio only (`Stream has no video`). Disable for audio-only entrances. Incoming calls still follow the media offered by the system. Saving reloads the integration. | | **Voice answer** (`voice_answer`) | Who can answer a ringing call by talking on `/audio_ws`: **Declared** (default, only with `?voice_answer=1`), **Off** (never), **Any** (any connection with a mic; a wall tablet with its mic left open can answer by itself on household noise) | | **Ring webhooks** (`ring_webhook_url`, `ring_end_webhook_url`) | Optional GET (fire-and-forget, 5 s timeout) fired when a ring starts and when it ends (answered, cancelled or missed) — e.g. the `turnOn`/`turnOff` URLs of a Scrypted Dummy Switch (see [docs/EXTERNAL.md](EXTERNAL.md)). A failure only logs a warning, never blocks the ring. Empty = off | diff --git a/tests/test_config_flow_steps.py b/tests/test_config_flow_steps.py index 9218701..202fed5 100644 --- a/tests/test_config_flow_steps.py +++ b/tests/test_config_flow_steps.py @@ -767,6 +767,69 @@ def test_the_av_key_page_is_translated(of): assert "<" not in json.dumps(page, ensure_ascii=False), name +@pytest.mark.parametrize("data, options, expected", [ + ({}, {}, True), + ({"video_enabled": False}, {}, False), + ({"video_enabled": True}, {}, True), + ({"video_enabled": False}, {"video_enabled": True}, True), + ({"video_enabled": True}, {"video_enabled": False}, False), +]) +def test_video_settings_default_to_the_qr_unless_overridden(of, monkeypatch, data, options, expected): + flow = _options_flow(of, data={**ENTRY_DATA, **data}, options=options) + defaults = {} + + def optional(key, default=None, **_kw): + defaults[key] = default + return key + + monkeypatch.setattr(of.vol, "Optional", optional) + asyncio.run(flow.async_step_settings()) + assert defaults["video_enabled"] is expected + + +@pytest.mark.parametrize("selected", [True, False]) +def test_saved_video_setting_controls_our_offer_and_preserves_other_options(of, selected): + from custom_components.vimar_intercom import runtime, sdp + + flow = _options_flow(of, data={**ENTRY_DATA, "video_enabled": not selected}, + options={"camera_target": "52011", "homekit_accessory": True}) + result = asyncio.run(flow.async_step_settings({ + "local_proxy": "192.0.2.1", "use_local_udp": False, + "camera_target": "52011", "video_enabled": selected, + })) + assert result["type"] == "create_entry" + assert result["data"]["video_enabled"] is selected + assert result["data"]["camera_target"] == "52011" + assert result["data"]["homekit_accessory"] is True + assert flow._entry.data["video_enabled"] is not selected + runtime.configure({**flow._entry.data, **result["data"]}) + assert ("m=video " in sdp.build_sdp()) is selected + + +def test_saving_settings_without_video_field_keeps_the_existing_value(of): + flow = _options_flow(of, data={**ENTRY_DATA, "video_enabled": True}, + options={"video_enabled": False}) + result = asyncio.run(flow.async_step_settings({"local_proxy": "192.0.2.1", "use_local_udp": False})) + assert result["data"]["video_enabled"] is False + + +def test_video_choice_survives_a_settings_validation_error(of, monkeypatch): + defaults = {} + + def optional(key, default=None, **_kw): + defaults[key] = default + return key + + monkeypatch.setattr(of.vol, "Optional", optional) + flow = _options_flow(of, data={**ENTRY_DATA, "video_enabled": False}) + result = asyncio.run(flow.async_step_settings({ + "local_proxy": "192.0.2.1", "use_local_udp": False, + "video_enabled": True, "camera_target": "invalid", + })) + assert result["errors"]["camera_target"] == "invalid_target" + assert defaults["video_enabled"] is True + + # 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):