Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions custom_components/vimar_intercom/options_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions custom_components/vimar_intercom/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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)",
Expand All @@ -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).",
Expand Down
2 changes: 2 additions & 0 deletions custom_components/vimar_intercom/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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)",
Expand All @@ -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).",
Expand Down
2 changes: 2 additions & 0 deletions custom_components/vimar_intercom/translations/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -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)",
Expand All @@ -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).",
Expand Down
1 change: 1 addition & 0 deletions docs/CONFIGURATION.it.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

Expand Down
1 change: 1 addition & 0 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

Expand Down
63 changes: 63 additions & 0 deletions tests/test_config_flow_steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Loading