From 4accf3e234800f584fd0092d5dd8cf0c91af1440 Mon Sep 17 00:00:00 2001 From: Federico Iezzi Date: Wed, 7 Oct 2026 00:01:01 +0200 Subject: [PATCH 1/3] feat(video): the panel's video bandwidth is an option, 256 kbit/s by default 1.0.19 (#84) asks every panel for 2 Mbit/s of video instead of 256 kbit/s. It was measured on a 40515, whose relay path loses nothing. A Tab 7S Up 40517 over the cloud relay honours it too: in HomeKit views on 1.0.20 the re-encoder received about 200 video packets per second, against about 20 on 1.0.18, and 21 of them were lost in one 12 s view (bursts of up to 7), where the same views lost 0-2 before. On the phone and in the card the video smeared, froze and lagged; the re-encoder's frame delay went back to 321 ms on the first view (47-72 ms before). The bandwidth is now an option on the HomeKit page, next to the re-encoding switch, though it applies to every video from the panel (card, camera, /av, HomeKit): 256 kbit/s (default, the value before 1.0.19) or 2 Mbit/s. runtime keeps the pair (video, session) and the SDP reads it; an unknown value falls back to the default. Co-Authored-By: Claude Opus 5.5 (1M context) --- custom_components/vimar_intercom/const.py | 11 ++++++++-- .../vimar_intercom/options_flow.py | 15 ++++++++++++++ custom_components/vimar_intercom/runtime.py | 6 ++++++ custom_components/vimar_intercom/sdp.py | 4 ++-- custom_components/vimar_intercom/strings.json | 12 +++++++++-- .../vimar_intercom/translations/en.json | 12 +++++++++-- .../vimar_intercom/translations/it.json | 12 +++++++++-- tests/test_config_flow_picg.py | 7 +++++++ tests/test_sdp.py | 20 +++++++++++++++---- 9 files changed, 85 insertions(+), 14 deletions(-) diff --git a/custom_components/vimar_intercom/const.py b/custom_components/vimar_intercom/const.py index fb41de50..4fc11478 100644 --- a/custom_components/vimar_intercom/const.py +++ b/custom_components/vimar_intercom/const.py @@ -40,8 +40,15 @@ # Bandwidth we declare in the SDP (b=AS, kbit/s). The 40515 panel's encoder honours it: # 256 gave ~14 KB keyframes at 16 fps, 2048 gives 30-58 KB at 25 fps (~1.5 Mbit/s, its # ceiling: 4096 is the same), same 720x576, no loss over the cloud. Session = video + audio. -SDP_VIDEO_BANDWIDTH = 2048 -SDP_SESSION_BANDWIDTH = 2200 +# The video bandwidth is an option (CONF_VIDEO_BANDWIDTH): 2048 suits the 40515, but +# the 40517 honours it too, and through a cloud relay that loses packets ten times +# the traffic means ten times the losses (smeared, frozen, laggy video). 256 is the +# value before 1.0.19 and the default. Session = video + audio. +CONF_VIDEO_BANDWIDTH = "video_bandwidth" +VIDEO_BANDWIDTH_LOW = "256" +VIDEO_BANDWIDTH_HIGH = "2048" +DEFAULT_VIDEO_BANDWIDTH = VIDEO_BANDWIDTH_LOW +SDP_BANDWIDTH = {VIDEO_BANDWIDTH_LOW: (256, 512), VIDEO_BANDWIDTH_HIGH: (2048, 2200)} # Porte locali dell'ffmpeg AV (/api/vimar_intercom/av). Devono essere PARI e # distanti almeno 2: per ogni riga m= dell'SDP ffmpeg apre la porta RTP **e** la # RTCP (= RTP + 1). Fino alla 1.0.7 erano 19201/19202: l'RTCP del video cadeva diff --git a/custom_components/vimar_intercom/options_flow.py b/custom_components/vimar_intercom/options_flow.py index 4a8a03e0..bdeacc1e 100644 --- a/custom_components/vimar_intercom/options_flow.py +++ b/custom_components/vimar_intercom/options_flow.py @@ -57,11 +57,13 @@ CONF_HOMEKIT_ANSWER, CONF_HOMEKIT_RING_BUTTON, CONF_HOMEKIT_SMOOTH, + CONF_VIDEO_BANDWIDTH, DEFAULT_HOMEKIT_ACCESSORY, DEFAULT_HOMEKIT_ANSWER, DEFAULT_HOMEKIT_RING_BUTTON, DEFAULT_HOMEKIT_SMOOTH, DEFAULT_SNAPSHOT_DELAY, + DEFAULT_VIDEO_BANDWIDTH, DOMAIN, HOMEKIT_ANSWER_OPEN, HOMEKIT_ANSWER_TALK, @@ -71,6 +73,8 @@ MY_NAME, PICG_TARGET, SGA_TARGET, + VIDEO_BANDWIDTH_HIGH, + VIDEO_BANDWIDTH_LOW, ) from .runtime import ( MEDIA_ENC_MODES, @@ -570,6 +574,8 @@ async def async_step_homekit( CONF_HOMEKIT_ANSWER: user_input.get( CONF_HOMEKIT_ANSWER, DEFAULT_HOMEKIT_ANSWER), CONF_HOMEKIT_RING_BUTTON: bool(user_input.get(CONF_HOMEKIT_RING_BUTTON)), + CONF_VIDEO_BANDWIDTH: user_input.get( + CONF_VIDEO_BANDWIDTH, DEFAULT_VIDEO_BANDWIDTH), }, ) current = self._entry.options @@ -598,6 +604,15 @@ async def async_step_homekit( CONF_HOMEKIT_RING_BUTTON, default=current.get(CONF_HOMEKIT_RING_BUTTON, DEFAULT_HOMEKIT_RING_BUTTON), ): bool, + # Not HomeKit only: every video from the panel (card, camera, /av). + vol.Optional( + CONF_VIDEO_BANDWIDTH, + default=current.get(CONF_VIDEO_BANDWIDTH, DEFAULT_VIDEO_BANDWIDTH), + ): selector.SelectSelector(selector.SelectSelectorConfig( + options=[VIDEO_BANDWIDTH_LOW, VIDEO_BANDWIDTH_HIGH], + translation_key="video_bandwidth", + mode=selector.SelectSelectorMode.LIST, + )), }), ) diff --git a/custom_components/vimar_intercom/runtime.py b/custom_components/vimar_intercom/runtime.py index 08761ced..8e43f887 100644 --- a/custom_components/vimar_intercom/runtime.py +++ b/custom_components/vimar_intercom/runtime.py @@ -123,6 +123,8 @@ def away_message_configured() -> bool: # Foto di chi suona: cartella (vuoto = non salvare) e secondi dopo lo squillo. SNAPSHOT_DIR: str = "" SNAPSHOT_DELAY: int = _const.DEFAULT_SNAPSHOT_DELAY +# kbit/s declared in the SDP (b=AS) for the video and the session: see const.SDP_BANDWIDTH. +VIDEO_BANDWIDTH, SESSION_BANDWIDTH = _const.SDP_BANDWIDTH[_const.DEFAULT_VIDEO_BANDWIDTH] VIEW_KEEPALIVE: float = _const.DEFAULT_VIEW_KEEPALIVE_CLOUD # s di silenzio a "Vedi esterno", 0 = nessuno @@ -259,6 +261,7 @@ def configure(data: dict) -> None: global CAMERA_TARGET, INTERNAL_PANEL_TARGET, DOOR_TARGET, CAMERA_TARGET_CONFIGURED global AWAY_MESSAGE_FILE, AWAY_MESSAGE_TEXT, AWAY_MESSAGE_TTS, AWAY_MESSAGE_DELAY global SNAPSHOT_DIR, SNAPSHOT_DELAY, VIEW_KEEPALIVE, ALLOWED_USERS + global VIDEO_BANDWIDTH, SESSION_BANDWIDTH global RING_WEBHOOK_URL, RING_END_WEBHOOK_URL global DEVICE_IMEI, DEVICE_UUID, DEVICE_NAME, AV_KEY @@ -355,6 +358,9 @@ def configure(data: dict) -> None: SNAPSHOT_DELAY = int(data.get("snapshot_delay", _const.DEFAULT_SNAPSHOT_DELAY)) _ka = data.get("view_keepalive") VIEW_KEEPALIVE = int(_ka) if _ka is not None else view_keepalive_default(USE_LOCAL_UDP) + VIDEO_BANDWIDTH, SESSION_BANDWIDTH = _const.SDP_BANDWIDTH.get( + str(data.get(_const.CONF_VIDEO_BANDWIDTH)), + _const.SDP_BANDWIDTH[_const.DEFAULT_VIDEO_BANDWIDTH]) ALLOWED_USERS = [str(u) for u in data.get("allowed_users") or []] RING_WEBHOOK_URL = str(data.get("ring_webhook_url") or "").strip() RING_END_WEBHOOK_URL = str(data.get("ring_end_webhook_url") or "").strip() diff --git a/custom_components/vimar_intercom/sdp.py b/custom_components/vimar_intercom/sdp.py index 151e91e7..ec37b27f 100644 --- a/custom_components/vimar_intercom/sdp.py +++ b/custom_components/vimar_intercom/sdp.py @@ -177,7 +177,7 @@ def _key(sec, current): ), "video": ( f"m=video {video_port} {video_proto} {' '.join(pt for pt, _ in h264)}\r\n" - f"b=AS:{C.SDP_VIDEO_BANDWIDTH}\r\n" + f"b=AS:{R.VIDEO_BANDWIDTH}\r\n" f"{h264_lines}" f"a=sendrecv\r\n" f"{video_crypto}" @@ -191,7 +191,7 @@ def _key(sec, current): f"o=- {sid} {sid} IN IP4 {MY_IP}\r\n" f"s=Talk\r\n" f"c=IN IP4 {MY_IP}\r\n" - f"b=AS:{C.SDP_SESSION_BANDWIDTH}\r\n" + f"b=AS:{R.SESSION_BANDWIDTH}\r\n" f"t=0 0\r\n" f"a=rtcp-xr:rcvr-rtt=all:10000 stat-summary=loss,dup,jitt,TTL voip-metrics\r\n" + "".join(blocks.get(kind) or _refused_block(offer[kind]) diff --git a/custom_components/vimar_intercom/strings.json b/custom_components/vimar_intercom/strings.json index 85c4b8f4..006cb18b 100644 --- a/custom_components/vimar_intercom/strings.json +++ b/custom_components/vimar_intercom/strings.json @@ -143,13 +143,15 @@ "homekit_accessory": "Pubblica in HomeKit", "homekit_video_smooth": "Video più fluido (ricodifica)", "homekit_answer": "Durante uno squillo", - "homekit_ring_button": "Pulsante squillo per le automazioni Casa" + "homekit_ring_button": "Pulsante squillo per le automazioni Casa", + "video_bandwidth": "Qualità video dalla targa" }, "data_description": { "homekit_accessory": "Accende il videocitofono in HomeKit. Finché non è abbinato, il codice e il QR da inquadrare con l'app Casa compaiono qui sopra; una notifica ricorda che l'abbinamento è in attesa. Dopo 10 codici sbagliati di fila il codice cambia.", "homekit_video_smooth": "Acceso (consigliato): Home Assistant ricodifica il video della targa con un keyframe al secondo, così un pacchetto perso dal relay diventa una breve sbavatura invece di un blocco. L'apertura richiede circa mezzo secondo in più e un po' di CPU. Spento: il video va dritto al telefono e si apre prima, ma un pacchetto perso può fermare l'immagine fino a 3 secondi.", "homekit_answer": "Rispondi quando parli: aprire la notifica mostra e fa sentire la strada senza rispondere, così il Tab e l'app possono ancora rispondere; la prima parola (Parla) risponde. Rispondi all'apertura: aprire la notifica risponde subito e il Tab smette di suonare.", - "homekit_ring_button": "Aggiunge all'accessorio un pulsante programmabile che si preme a ogni squillo, da usare come attivatore nelle automazioni dell'app Casa. Spento: una voce in meno nell'app Casa; notifica, video, Parla e cancello non cambiano." + "homekit_ring_button": "Aggiunge all'accessorio un pulsante programmabile che si preme a ogni squillo, da usare come attivatore nelle automazioni dell'app Casa. Spento: una voce in meno nell'app Casa; notifica, video, Parla e cancello non cambiano.", + "video_bandwidth": "Vale per tutto il video della targa: card, camera, /av e HomeKit. 256 kbit/s (consigliato in cloud): flusso più leggero, meno pacchetti persi sul relay. 2 Mbit/s: immagine più nitida sulle targhe che lo supportano (misurato su un Tab 5S Up 40515), ma dieci volte il traffico; con un relay che perde pacchetti (misurato su un Tab 7S Up 40517) il video si sgrana, si blocca e va in ritardo." } }, "import_rubrica": { @@ -442,6 +444,12 @@ "talk": "Rispondi quando parli", "open": "Rispondi all'apertura" } + }, + "video_bandwidth": { + "options": { + "256": "256 kbit/s (consigliato)", + "2048": "2 Mbit/s (più nitido, serve una connessione pulita)" + } } }, "exceptions": { diff --git a/custom_components/vimar_intercom/translations/en.json b/custom_components/vimar_intercom/translations/en.json index 2ab3d4ca..38d3f0f4 100644 --- a/custom_components/vimar_intercom/translations/en.json +++ b/custom_components/vimar_intercom/translations/en.json @@ -143,13 +143,15 @@ "homekit_accessory": "Publish to HomeKit", "homekit_video_smooth": "Smoother video (re-encode)", "homekit_answer": "During a ring", - "homekit_ring_button": "Ring button for Home automations" + "homekit_ring_button": "Ring button for Home automations", + "video_bandwidth": "Video quality from the panel" }, "data_description": { "homekit_accessory": "Turns on the HomeKit video doorbell. Until it is paired, the setup code and the QR code to scan with the Home app appear above; a notification says that pairing is waiting. After 10 wrong codes in a row the code changes.", "homekit_video_smooth": "On (recommended): Home Assistant re-encodes the panel's video with a keyframe every second, so a packet lost on the cloud relay shows as a brief smear instead of a freeze. Opening takes about half a second longer and uses some CPU. Off: the video goes straight to the phone and opens faster, but a lost packet can freeze the picture for up to 3 seconds.", "homekit_answer": "Answer when you talk: opening the notification shows and plays the street without answering, so the Tab and the app can still answer; your first word (Talk) answers. Answer when the view opens: opening the notification answers at once and the Tab stops ringing.", - "homekit_ring_button": "Adds a programmable button that is pressed at every ring, to use as a trigger in Home app automations. Off: one tile less in the Home app; the ring notification, video, Talk and the gate are unchanged." + "homekit_ring_button": "Adds a programmable button that is pressed at every ring, to use as a trigger in Home app automations. Off: one tile less in the Home app; the ring notification, video, Talk and the gate are unchanged.", + "video_bandwidth": "Applies to every video from the panel: the card, the camera, /av and HomeKit. 256 kbit/s (recommended over the cloud): a lighter stream, fewer packets lost on the cloud relay. 2 Mbit/s: a sharper picture on panels that support it (measured on a Tab 5S Up 40515), but ten times the traffic; on a relay that loses packets (measured on a Tab 7S Up 40517) the video smears, freezes and lags." } }, "import_rubrica": { @@ -442,6 +444,12 @@ "talk": "Answer when you talk", "open": "Answer when the view opens" } + }, + "video_bandwidth": { + "options": { + "256": "256 kbit/s (recommended)", + "2048": "2 Mbit/s (sharper, needs a clean connection)" + } } }, "exceptions": { diff --git a/custom_components/vimar_intercom/translations/it.json b/custom_components/vimar_intercom/translations/it.json index 85c4b8f4..006cb18b 100644 --- a/custom_components/vimar_intercom/translations/it.json +++ b/custom_components/vimar_intercom/translations/it.json @@ -143,13 +143,15 @@ "homekit_accessory": "Pubblica in HomeKit", "homekit_video_smooth": "Video più fluido (ricodifica)", "homekit_answer": "Durante uno squillo", - "homekit_ring_button": "Pulsante squillo per le automazioni Casa" + "homekit_ring_button": "Pulsante squillo per le automazioni Casa", + "video_bandwidth": "Qualità video dalla targa" }, "data_description": { "homekit_accessory": "Accende il videocitofono in HomeKit. Finché non è abbinato, il codice e il QR da inquadrare con l'app Casa compaiono qui sopra; una notifica ricorda che l'abbinamento è in attesa. Dopo 10 codici sbagliati di fila il codice cambia.", "homekit_video_smooth": "Acceso (consigliato): Home Assistant ricodifica il video della targa con un keyframe al secondo, così un pacchetto perso dal relay diventa una breve sbavatura invece di un blocco. L'apertura richiede circa mezzo secondo in più e un po' di CPU. Spento: il video va dritto al telefono e si apre prima, ma un pacchetto perso può fermare l'immagine fino a 3 secondi.", "homekit_answer": "Rispondi quando parli: aprire la notifica mostra e fa sentire la strada senza rispondere, così il Tab e l'app possono ancora rispondere; la prima parola (Parla) risponde. Rispondi all'apertura: aprire la notifica risponde subito e il Tab smette di suonare.", - "homekit_ring_button": "Aggiunge all'accessorio un pulsante programmabile che si preme a ogni squillo, da usare come attivatore nelle automazioni dell'app Casa. Spento: una voce in meno nell'app Casa; notifica, video, Parla e cancello non cambiano." + "homekit_ring_button": "Aggiunge all'accessorio un pulsante programmabile che si preme a ogni squillo, da usare come attivatore nelle automazioni dell'app Casa. Spento: una voce in meno nell'app Casa; notifica, video, Parla e cancello non cambiano.", + "video_bandwidth": "Vale per tutto il video della targa: card, camera, /av e HomeKit. 256 kbit/s (consigliato in cloud): flusso più leggero, meno pacchetti persi sul relay. 2 Mbit/s: immagine più nitida sulle targhe che lo supportano (misurato su un Tab 5S Up 40515), ma dieci volte il traffico; con un relay che perde pacchetti (misurato su un Tab 7S Up 40517) il video si sgrana, si blocca e va in ritardo." } }, "import_rubrica": { @@ -442,6 +444,12 @@ "talk": "Rispondi quando parli", "open": "Rispondi all'apertura" } + }, + "video_bandwidth": { + "options": { + "256": "256 kbit/s (consigliato)", + "2048": "2 Mbit/s (più nitido, serve una connessione pulita)" + } } }, "exceptions": { diff --git a/tests/test_config_flow_picg.py b/tests/test_config_flow_picg.py index f351f29e..2bc164fd 100644 --- a/tests/test_config_flow_picg.py +++ b/tests/test_config_flow_picg.py @@ -147,6 +147,13 @@ def test_the_homekit_page_keeps_the_network_settings(of): assert out["data"]["homekit_accessory"] is False assert out["data"]["homekit_answer"] == "talk" assert out["data"]["homekit_ring_button"] is False, "off unless ticked" + assert out["data"]["video_bandwidth"] == "256", "the default when not chosen" + + +def test_the_homekit_page_saves_the_video_bandwidth(of): + out = _save(of, "async_step_homekit", + {"homekit_accessory": True, "video_bandwidth": "2048"}, FOREIGN) + assert out["data"]["video_bandwidth"] == "2048" def test_the_settings_page_keeps_homekit_and_unknown_keys(of): diff --git a/tests/test_sdp.py b/tests/test_sdp.py index 3a4f5e2e..e9ad9744 100644 --- a/tests/test_sdp.py +++ b/tests/test_sdp.py @@ -33,13 +33,25 @@ def test_build_sdp_plain_rtp_default(): assert sip.SDP._local_video_crypto_key is None -def test_build_sdp_asks_the_panel_for_2_mbit_video(): - """256 kbit/s made the 40515 send 16 fps blocky video; it honours 2048.""" +@pytest.mark.parametrize("option, video, session", [ + (None, 256, 512), # the default: what the integration asked before 1.0.19 + ("256", 256, 512), + ("2048", 2048, 2200), # the 40515's sharper picture, on request + ("bogus", 256, 512), +]) +def test_build_sdp_asks_for_the_chosen_video_bandwidth(monkeypatch, option, video, session): + """2048 kbit/s made a 40517 send ten times the packets through a cloud relay + that loses them (smeared, frozen, laggy video): it is an option, 256 by default.""" + from custom_components.vimar_intercom import runtime as R S.MEDIA_ENC = False + data = {"sip_user": "1001", "sip_domain": "example.invalid"} + if option is not None: + data["video_bandwidth"] = option + R.configure(data) offer = sip.parse_sdp(sip.build_sdp()) for sdp in (sip.build_sdp(), sip.build_sdp(offer)): - assert "b=AS:2200\r\nt=0 0" in sdp - assert "m=video" in sdp and "b=AS:2048\r\n" in sdp.split("m=video")[1] + assert f"b=AS:{session}\r\nt=0 0" in sdp + assert "m=video" in sdp and f"b=AS:{video}\r\n" in sdp.split("m=video")[1] def test_build_sdp_srtp_when_enabled(): From bbb626fbd15bb6d367cbf91254a2f8f493c6a610 Mon Sep 17 00:00:00 2001 From: Federico Iezzi Date: Wed, 7 Oct 2026 00:06:57 +0200 Subject: [PATCH 2/3] fix(video): the bandwidth option on the general settings page It changes every video from the panel (card, camera, /av, HomeKit), so it belongs on "Vimar Intercom settings", next to the other media options, not on the HomeKit page where someone without HomeKit would never look. A saved choice is kept when the page is saved without it, and the HomeKit page keeps it untouched. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../vimar_intercom/options_flow.py | 23 ++++++++++--------- custom_components/vimar_intercom/strings.json | 12 +++++----- .../vimar_intercom/translations/en.json | 12 +++++----- .../vimar_intercom/translations/it.json | 12 +++++----- tests/test_config_flow_picg.py | 21 ++++++++++++++--- 5 files changed, 48 insertions(+), 32 deletions(-) diff --git a/custom_components/vimar_intercom/options_flow.py b/custom_components/vimar_intercom/options_flow.py index bdeacc1e..42e839d2 100644 --- a/custom_components/vimar_intercom/options_flow.py +++ b/custom_components/vimar_intercom/options_flow.py @@ -289,6 +289,15 @@ def _photos_schema(form: dict) -> dict: default=form.get(KEY_VIEW_KA, view_keepalive_default( form.get(KEY_USE_LOCAL_UDP, True))), ): vol.All(vol.Coerce(int), vol.Range(min=0, max=3600)), + # Every video from the panel: card, camera, /av and HomeKit. + vol.Optional( + CONF_VIDEO_BANDWIDTH, + default=form.get(CONF_VIDEO_BANDWIDTH, DEFAULT_VIDEO_BANDWIDTH), + ): selector.SelectSelector(selector.SelectSelectorConfig( + options=[VIDEO_BANDWIDTH_LOW, VIDEO_BANDWIDTH_HIGH], + translation_key="video_bandwidth", + mode=selector.SelectSelectorMode.LIST, + )), } @@ -385,6 +394,9 @@ async def async_step_settings( KEY_SNAP_DIR: snap_dir, KEY_SNAP_DELAY: snap_delay, KEY_VIEW_KA: view_ka, + CONF_VIDEO_BANDWIDTH: user_input.get( + CONF_VIDEO_BANDWIDTH, + current.get(CONF_VIDEO_BANDWIDTH, DEFAULT_VIDEO_BANDWIDTH)), KEY_ALLOWED_USERS: allowed_users, KEY_RING_WEBHOOK_URL: ring_webhook_url, KEY_RING_END_WEBHOOK_URL: ring_end_webhook_url, @@ -574,8 +586,6 @@ async def async_step_homekit( CONF_HOMEKIT_ANSWER: user_input.get( CONF_HOMEKIT_ANSWER, DEFAULT_HOMEKIT_ANSWER), CONF_HOMEKIT_RING_BUTTON: bool(user_input.get(CONF_HOMEKIT_RING_BUTTON)), - CONF_VIDEO_BANDWIDTH: user_input.get( - CONF_VIDEO_BANDWIDTH, DEFAULT_VIDEO_BANDWIDTH), }, ) current = self._entry.options @@ -604,15 +614,6 @@ async def async_step_homekit( CONF_HOMEKIT_RING_BUTTON, default=current.get(CONF_HOMEKIT_RING_BUTTON, DEFAULT_HOMEKIT_RING_BUTTON), ): bool, - # Not HomeKit only: every video from the panel (card, camera, /av). - vol.Optional( - CONF_VIDEO_BANDWIDTH, - default=current.get(CONF_VIDEO_BANDWIDTH, DEFAULT_VIDEO_BANDWIDTH), - ): selector.SelectSelector(selector.SelectSelectorConfig( - options=[VIDEO_BANDWIDTH_LOW, VIDEO_BANDWIDTH_HIGH], - translation_key="video_bandwidth", - mode=selector.SelectSelectorMode.LIST, - )), }), ) diff --git a/custom_components/vimar_intercom/strings.json b/custom_components/vimar_intercom/strings.json index 006cb18b..45095c7c 100644 --- a/custom_components/vimar_intercom/strings.json +++ b/custom_components/vimar_intercom/strings.json @@ -112,7 +112,8 @@ "view_keepalive": "Secondi di silenzio a «Vedi esterno» (0 = nessuno)", "allowed_users": "Utenti ammessi a squilli e media live (vuoto = tutti)", "ring_webhook_url": "Webhook a inizio squillo (URL)", - "ring_end_webhook_url": "Webhook a fine squillo (URL)" + "ring_end_webhook_url": "Webhook a fine squillo (URL)", + "video_bandwidth": "Qualità video dalla targa" }, "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.", @@ -133,7 +134,8 @@ "view_keepalive": "Durante la vista dell'esterno l'integrazione manda silenzio audio, senza il quale via cloud la targa chiude la vista dopo ~10 s. Si ferma dopo questi secondi dall'inizio della vista (poi la vista si chiude da sola). Sul 2 fili in locale il silenzio tiene occupato l'appartamento: il predefinito è 0 in locale e 120 via cloud; 0 = nessun silenzio (sul 2 fili va bene 0 o 30).", "allowed_users": "Limita la card, lo storico squilli (foto e clip) e /audio_ws a questi utenti di Home Assistant. Gli amministratori sempre. Vuoto = ogni utente autenticato. NON limita l'entità camera né /av dalla rete locale: chi può aprire la camera vede (e sente) lo stream; e se la cartella foto è sotto una cartella media di HA, foto e clip compaiono nel browser media per tutti gli utenti.", "ring_webhook_url": "GET (fire-and-forget) inviata quando inizia uno squillo, es. per accendere un interruttore fittizio (Scrypted Dummy Switch) collegato a un Custom Doorbell Button. Un fallimento si logga come warning e non ferma lo squillo. Vuoto = disattivato.", - "ring_end_webhook_url": "GET (fire-and-forget) inviata quando lo squillo finisce, risposto, annullato o scaduto: stessa idea di sopra, per spegnere l'interruttore. Vuoto = disattivato." + "ring_end_webhook_url": "GET (fire-and-forget) inviata quando lo squillo finisce, risposto, annullato o scaduto: stessa idea di sopra, per spegnere l'interruttore. Vuoto = disattivato.", + "video_bandwidth": "Vale per tutto il video della targa: card, camera, /av e HomeKit. 256 kbit/s (consigliato in cloud): flusso più leggero, meno pacchetti persi sul relay. 2 Mbit/s: immagine più nitida sulle targhe che lo supportano (misurato su un Tab 5S Up 40515), ma dieci volte il traffico; con un relay che perde pacchetti (misurato su un Tab 7S Up 40517) il video si sgrana, si blocca e va in ritardo." } }, "homekit": { @@ -143,15 +145,13 @@ "homekit_accessory": "Pubblica in HomeKit", "homekit_video_smooth": "Video più fluido (ricodifica)", "homekit_answer": "Durante uno squillo", - "homekit_ring_button": "Pulsante squillo per le automazioni Casa", - "video_bandwidth": "Qualità video dalla targa" + "homekit_ring_button": "Pulsante squillo per le automazioni Casa" }, "data_description": { "homekit_accessory": "Accende il videocitofono in HomeKit. Finché non è abbinato, il codice e il QR da inquadrare con l'app Casa compaiono qui sopra; una notifica ricorda che l'abbinamento è in attesa. Dopo 10 codici sbagliati di fila il codice cambia.", "homekit_video_smooth": "Acceso (consigliato): Home Assistant ricodifica il video della targa con un keyframe al secondo, così un pacchetto perso dal relay diventa una breve sbavatura invece di un blocco. L'apertura richiede circa mezzo secondo in più e un po' di CPU. Spento: il video va dritto al telefono e si apre prima, ma un pacchetto perso può fermare l'immagine fino a 3 secondi.", "homekit_answer": "Rispondi quando parli: aprire la notifica mostra e fa sentire la strada senza rispondere, così il Tab e l'app possono ancora rispondere; la prima parola (Parla) risponde. Rispondi all'apertura: aprire la notifica risponde subito e il Tab smette di suonare.", - "homekit_ring_button": "Aggiunge all'accessorio un pulsante programmabile che si preme a ogni squillo, da usare come attivatore nelle automazioni dell'app Casa. Spento: una voce in meno nell'app Casa; notifica, video, Parla e cancello non cambiano.", - "video_bandwidth": "Vale per tutto il video della targa: card, camera, /av e HomeKit. 256 kbit/s (consigliato in cloud): flusso più leggero, meno pacchetti persi sul relay. 2 Mbit/s: immagine più nitida sulle targhe che lo supportano (misurato su un Tab 5S Up 40515), ma dieci volte il traffico; con un relay che perde pacchetti (misurato su un Tab 7S Up 40517) il video si sgrana, si blocca e va in ritardo." + "homekit_ring_button": "Aggiunge all'accessorio un pulsante programmabile che si preme a ogni squillo, da usare come attivatore nelle automazioni dell'app Casa. Spento: una voce in meno nell'app Casa; notifica, video, Parla e cancello non cambiano." } }, "import_rubrica": { diff --git a/custom_components/vimar_intercom/translations/en.json b/custom_components/vimar_intercom/translations/en.json index 38d3f0f4..3c1215db 100644 --- a/custom_components/vimar_intercom/translations/en.json +++ b/custom_components/vimar_intercom/translations/en.json @@ -112,7 +112,8 @@ "view_keepalive": "Seconds of silence for “Vedi esterno” (0 = none)", "allowed_users": "Users allowed to see rings and live media (empty = everyone)", "ring_webhook_url": "Ring-start webhook (URL)", - "ring_end_webhook_url": "Ring-end webhook (URL)" + "ring_end_webhook_url": "Ring-end webhook (URL)", + "video_bandwidth": "Video quality from the panel" }, "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.", @@ -133,7 +134,8 @@ "view_keepalive": "While viewing the outside the integration sends audio silence; without it, over the cloud the panel closes the view after ~10 s. It stops this many seconds after the view starts (the view then ends by itself). On the 2-wire plant in local mode the silence keeps the apartment busy: the default is 0 in local mode and 120 over the cloud; 0 = no silence (on the 2-wire use 0 or 30).", "allowed_users": "Limits the card, the ring history (photos and clips) and /audio_ws to these Home Assistant users. Admins always. Empty = every logged-in user. It does NOT limit the camera entity or /av from the local network: anyone who can open the camera sees (and hears) the stream; and if the photo folder is under an HA media directory, photos and clips show up in the media browser for every user.", "ring_webhook_url": "GET (fire-and-forget) sent when a ring starts, e.g. to turn on a dummy switch (Scrypted Dummy Switch) linked to a Custom Doorbell Button. A failure is only logged as a warning and never stops the ring. Empty = off.", - "ring_end_webhook_url": "GET (fire-and-forget) sent when the ring ends, answered, cancelled or timed out: same idea as above, to turn the switch off. Empty = off." + "ring_end_webhook_url": "GET (fire-and-forget) sent when the ring ends, answered, cancelled or timed out: same idea as above, to turn the switch off. Empty = off.", + "video_bandwidth": "Applies to every video from the panel: the card, the camera, /av and HomeKit. 256 kbit/s (recommended over the cloud): a lighter stream, fewer packets lost on the cloud relay. 2 Mbit/s: a sharper picture on panels that support it (measured on a Tab 5S Up 40515), but ten times the traffic; on a relay that loses packets (measured on a Tab 7S Up 40517) the video smears, freezes and lags." } }, "homekit": { @@ -143,15 +145,13 @@ "homekit_accessory": "Publish to HomeKit", "homekit_video_smooth": "Smoother video (re-encode)", "homekit_answer": "During a ring", - "homekit_ring_button": "Ring button for Home automations", - "video_bandwidth": "Video quality from the panel" + "homekit_ring_button": "Ring button for Home automations" }, "data_description": { "homekit_accessory": "Turns on the HomeKit video doorbell. Until it is paired, the setup code and the QR code to scan with the Home app appear above; a notification says that pairing is waiting. After 10 wrong codes in a row the code changes.", "homekit_video_smooth": "On (recommended): Home Assistant re-encodes the panel's video with a keyframe every second, so a packet lost on the cloud relay shows as a brief smear instead of a freeze. Opening takes about half a second longer and uses some CPU. Off: the video goes straight to the phone and opens faster, but a lost packet can freeze the picture for up to 3 seconds.", "homekit_answer": "Answer when you talk: opening the notification shows and plays the street without answering, so the Tab and the app can still answer; your first word (Talk) answers. Answer when the view opens: opening the notification answers at once and the Tab stops ringing.", - "homekit_ring_button": "Adds a programmable button that is pressed at every ring, to use as a trigger in Home app automations. Off: one tile less in the Home app; the ring notification, video, Talk and the gate are unchanged.", - "video_bandwidth": "Applies to every video from the panel: the card, the camera, /av and HomeKit. 256 kbit/s (recommended over the cloud): a lighter stream, fewer packets lost on the cloud relay. 2 Mbit/s: a sharper picture on panels that support it (measured on a Tab 5S Up 40515), but ten times the traffic; on a relay that loses packets (measured on a Tab 7S Up 40517) the video smears, freezes and lags." + "homekit_ring_button": "Adds a programmable button that is pressed at every ring, to use as a trigger in Home app automations. Off: one tile less in the Home app; the ring notification, video, Talk and the gate are unchanged." } }, "import_rubrica": { diff --git a/custom_components/vimar_intercom/translations/it.json b/custom_components/vimar_intercom/translations/it.json index 006cb18b..45095c7c 100644 --- a/custom_components/vimar_intercom/translations/it.json +++ b/custom_components/vimar_intercom/translations/it.json @@ -112,7 +112,8 @@ "view_keepalive": "Secondi di silenzio a «Vedi esterno» (0 = nessuno)", "allowed_users": "Utenti ammessi a squilli e media live (vuoto = tutti)", "ring_webhook_url": "Webhook a inizio squillo (URL)", - "ring_end_webhook_url": "Webhook a fine squillo (URL)" + "ring_end_webhook_url": "Webhook a fine squillo (URL)", + "video_bandwidth": "Qualità video dalla targa" }, "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.", @@ -133,7 +134,8 @@ "view_keepalive": "Durante la vista dell'esterno l'integrazione manda silenzio audio, senza il quale via cloud la targa chiude la vista dopo ~10 s. Si ferma dopo questi secondi dall'inizio della vista (poi la vista si chiude da sola). Sul 2 fili in locale il silenzio tiene occupato l'appartamento: il predefinito è 0 in locale e 120 via cloud; 0 = nessun silenzio (sul 2 fili va bene 0 o 30).", "allowed_users": "Limita la card, lo storico squilli (foto e clip) e /audio_ws a questi utenti di Home Assistant. Gli amministratori sempre. Vuoto = ogni utente autenticato. NON limita l'entità camera né /av dalla rete locale: chi può aprire la camera vede (e sente) lo stream; e se la cartella foto è sotto una cartella media di HA, foto e clip compaiono nel browser media per tutti gli utenti.", "ring_webhook_url": "GET (fire-and-forget) inviata quando inizia uno squillo, es. per accendere un interruttore fittizio (Scrypted Dummy Switch) collegato a un Custom Doorbell Button. Un fallimento si logga come warning e non ferma lo squillo. Vuoto = disattivato.", - "ring_end_webhook_url": "GET (fire-and-forget) inviata quando lo squillo finisce, risposto, annullato o scaduto: stessa idea di sopra, per spegnere l'interruttore. Vuoto = disattivato." + "ring_end_webhook_url": "GET (fire-and-forget) inviata quando lo squillo finisce, risposto, annullato o scaduto: stessa idea di sopra, per spegnere l'interruttore. Vuoto = disattivato.", + "video_bandwidth": "Vale per tutto il video della targa: card, camera, /av e HomeKit. 256 kbit/s (consigliato in cloud): flusso più leggero, meno pacchetti persi sul relay. 2 Mbit/s: immagine più nitida sulle targhe che lo supportano (misurato su un Tab 5S Up 40515), ma dieci volte il traffico; con un relay che perde pacchetti (misurato su un Tab 7S Up 40517) il video si sgrana, si blocca e va in ritardo." } }, "homekit": { @@ -143,15 +145,13 @@ "homekit_accessory": "Pubblica in HomeKit", "homekit_video_smooth": "Video più fluido (ricodifica)", "homekit_answer": "Durante uno squillo", - "homekit_ring_button": "Pulsante squillo per le automazioni Casa", - "video_bandwidth": "Qualità video dalla targa" + "homekit_ring_button": "Pulsante squillo per le automazioni Casa" }, "data_description": { "homekit_accessory": "Accende il videocitofono in HomeKit. Finché non è abbinato, il codice e il QR da inquadrare con l'app Casa compaiono qui sopra; una notifica ricorda che l'abbinamento è in attesa. Dopo 10 codici sbagliati di fila il codice cambia.", "homekit_video_smooth": "Acceso (consigliato): Home Assistant ricodifica il video della targa con un keyframe al secondo, così un pacchetto perso dal relay diventa una breve sbavatura invece di un blocco. L'apertura richiede circa mezzo secondo in più e un po' di CPU. Spento: il video va dritto al telefono e si apre prima, ma un pacchetto perso può fermare l'immagine fino a 3 secondi.", "homekit_answer": "Rispondi quando parli: aprire la notifica mostra e fa sentire la strada senza rispondere, così il Tab e l'app possono ancora rispondere; la prima parola (Parla) risponde. Rispondi all'apertura: aprire la notifica risponde subito e il Tab smette di suonare.", - "homekit_ring_button": "Aggiunge all'accessorio un pulsante programmabile che si preme a ogni squillo, da usare come attivatore nelle automazioni dell'app Casa. Spento: una voce in meno nell'app Casa; notifica, video, Parla e cancello non cambiano.", - "video_bandwidth": "Vale per tutto il video della targa: card, camera, /av e HomeKit. 256 kbit/s (consigliato in cloud): flusso più leggero, meno pacchetti persi sul relay. 2 Mbit/s: immagine più nitida sulle targhe che lo supportano (misurato su un Tab 5S Up 40515), ma dieci volte il traffico; con un relay che perde pacchetti (misurato su un Tab 7S Up 40517) il video si sgrana, si blocca e va in ritardo." + "homekit_ring_button": "Aggiunge all'accessorio un pulsante programmabile che si preme a ogni squillo, da usare come attivatore nelle automazioni dell'app Casa. Spento: una voce in meno nell'app Casa; notifica, video, Parla e cancello non cambiano." } }, "import_rubrica": { diff --git a/tests/test_config_flow_picg.py b/tests/test_config_flow_picg.py index 2bc164fd..639b4238 100644 --- a/tests/test_config_flow_picg.py +++ b/tests/test_config_flow_picg.py @@ -147,12 +147,27 @@ def test_the_homekit_page_keeps_the_network_settings(of): assert out["data"]["homekit_accessory"] is False assert out["data"]["homekit_answer"] == "talk" assert out["data"]["homekit_ring_button"] is False, "off unless ticked" + + +SETTINGS = {"local_proxy": "192.0.2.1", "use_local_udp": False, "local_udp_port": 5060, + "media_enc": False, "actuators": "", "sga_target": "61000", + "picg_target": "61000", "camera_target": "55001", "door_target": "55001"} + + +def test_the_settings_page_saves_the_video_bandwidth(of): + """It changes every video from the panel, so it lives on the general page.""" + out = _save(of, "async_step_settings", {**SETTINGS, "video_bandwidth": "2048"}, FOREIGN) + assert out["type"] == "create_entry", out + assert out["data"]["video_bandwidth"] == "2048" + out = _save(of, "async_step_settings", SETTINGS, FOREIGN) assert out["data"]["video_bandwidth"] == "256", "the default when not chosen" + out = _save(of, "async_step_settings", SETTINGS, {**FOREIGN, "video_bandwidth": "2048"}) + assert out["data"]["video_bandwidth"] == "2048", "a saved choice is kept" -def test_the_homekit_page_saves_the_video_bandwidth(of): - out = _save(of, "async_step_homekit", - {"homekit_accessory": True, "video_bandwidth": "2048"}, FOREIGN) +def test_the_homekit_page_keeps_the_video_bandwidth(of): + out = _save(of, "async_step_homekit", {"homekit_accessory": True}, + {**FOREIGN, "video_bandwidth": "2048"}) assert out["data"]["video_bandwidth"] == "2048" From 9fdccc8c4fc4ed259156b28e6ff3d7a10938f021 Mon Sep 17 00:00:00 2001 From: Federico Iezzi Date: Thu, 8 Oct 2026 23:12:30 +0200 Subject: [PATCH 3/3] review: automatic bandwidth by transport, honest labels, docs, test state From @lollox80's and @Apeiv's reviews of #161: - The default is "Automatic", like view_keepalive's: 2048 on local UDP, 256 over the cloud relay; 256 and 2048 stay as manual choices, no values in between (runtime.video_bandwidth_choice; an unknown value is automatic). - The comment above the constants said the 40515 keeps 720x576 at 256: Apeiv measured it drops to 320x240 at 15 fps (720x576 at 24 fps at 2048, no loss on his cloud path, same first-frame time). It now gives both panels' measurements. - Labels and descriptions (it, en) say what each value costs: 256 is "lower resolution on some panels", 2 Mbit/s needs a clean connection. - docs/CONFIGURATION(.it).md list the option; TROUBLESHOOTING(.it).md get a row for smeared, frozen or lagging video. - The SDP test restored nothing after runtime.configure(): it now puts every runtime global back through monkeypatch, and covers automatic on both transports, both forced values and an unknown one. Co-Authored-By: Claude Opus 5.5 (1M context) --- custom_components/vimar_intercom/const.py | 20 ++++++++++------- .../vimar_intercom/options_flow.py | 3 ++- custom_components/vimar_intercom/runtime.py | 16 ++++++++++---- custom_components/vimar_intercom/strings.json | 5 +++-- .../vimar_intercom/translations/en.json | 5 +++-- .../vimar_intercom/translations/it.json | 5 +++-- docs/CONFIGURATION.it.md | 1 + docs/CONFIGURATION.md | 1 + docs/TROUBLESHOOTING.it.md | 1 + docs/TROUBLESHOOTING.md | 1 + tests/test_config_flow_picg.py | 2 +- tests/test_sdp.py | 22 ++++++++++++------- 12 files changed, 54 insertions(+), 28 deletions(-) diff --git a/custom_components/vimar_intercom/const.py b/custom_components/vimar_intercom/const.py index 4fc11478..5cd41530 100644 --- a/custom_components/vimar_intercom/const.py +++ b/custom_components/vimar_intercom/const.py @@ -37,17 +37,21 @@ # ─── RTP / Media ────────────────────────────────────────────────────────────── RTP_AUDIO_PORT = 7200 RTP_VIDEO_PORT = 9200 -# Bandwidth we declare in the SDP (b=AS, kbit/s). The 40515 panel's encoder honours it: -# 256 gave ~14 KB keyframes at 16 fps, 2048 gives 30-58 KB at 25 fps (~1.5 Mbit/s, its -# ceiling: 4096 is the same), same 720x576, no loss over the cloud. Session = video + audio. -# The video bandwidth is an option (CONF_VIDEO_BANDWIDTH): 2048 suits the 40515, but -# the 40517 honours it too, and through a cloud relay that loses packets ten times -# the traffic means ten times the losses (smeared, frozen, laggy video). 256 is the -# value before 1.0.19 and the default. Session = video + audio. +# Bandwidth we declare in the SDP (b=AS, kbit/s); the panels' encoders honour it. +# Measured over the cloud relay (#161): +# - Tab 5S Up 40515: 2048 gives 720x576 at 24 fps (~1.9 Mbit/s, its ceiling: 4096 is +# the same); 256 drops to 320x240 at 15 fps (~240 kbit/s). No loss on that path at +# either value, first frame ~1.8-1.9 s at both. +# - Tab 7S Up 40517: 2048 gives about ten times the packets of 256 through a relay that +# loses a few percent of them: ten times the damaged frames (smeared, frozen, laggy). +# CONF_VIDEO_BANDWIDTH chooses: "auto" (default) is 2048 on local UDP (no relay in the +# way) and 256 over the cloud, as view_keepalive's default follows the transport; +# "256" and "2048" force one. Session = video + audio. CONF_VIDEO_BANDWIDTH = "video_bandwidth" +VIDEO_BANDWIDTH_AUTO = "auto" VIDEO_BANDWIDTH_LOW = "256" VIDEO_BANDWIDTH_HIGH = "2048" -DEFAULT_VIDEO_BANDWIDTH = VIDEO_BANDWIDTH_LOW +DEFAULT_VIDEO_BANDWIDTH = VIDEO_BANDWIDTH_AUTO SDP_BANDWIDTH = {VIDEO_BANDWIDTH_LOW: (256, 512), VIDEO_BANDWIDTH_HIGH: (2048, 2200)} # Porte locali dell'ffmpeg AV (/api/vimar_intercom/av). Devono essere PARI e # distanti almeno 2: per ogni riga m= dell'SDP ffmpeg apre la porta RTP **e** la diff --git a/custom_components/vimar_intercom/options_flow.py b/custom_components/vimar_intercom/options_flow.py index 42e839d2..db986e5e 100644 --- a/custom_components/vimar_intercom/options_flow.py +++ b/custom_components/vimar_intercom/options_flow.py @@ -73,6 +73,7 @@ MY_NAME, PICG_TARGET, SGA_TARGET, + VIDEO_BANDWIDTH_AUTO, VIDEO_BANDWIDTH_HIGH, VIDEO_BANDWIDTH_LOW, ) @@ -294,7 +295,7 @@ def _photos_schema(form: dict) -> dict: CONF_VIDEO_BANDWIDTH, default=form.get(CONF_VIDEO_BANDWIDTH, DEFAULT_VIDEO_BANDWIDTH), ): selector.SelectSelector(selector.SelectSelectorConfig( - options=[VIDEO_BANDWIDTH_LOW, VIDEO_BANDWIDTH_HIGH], + options=[VIDEO_BANDWIDTH_AUTO, VIDEO_BANDWIDTH_LOW, VIDEO_BANDWIDTH_HIGH], translation_key="video_bandwidth", mode=selector.SelectSelectorMode.LIST, )), diff --git a/custom_components/vimar_intercom/runtime.py b/custom_components/vimar_intercom/runtime.py index 8e43f887..672024b7 100644 --- a/custom_components/vimar_intercom/runtime.py +++ b/custom_components/vimar_intercom/runtime.py @@ -124,10 +124,19 @@ def away_message_configured() -> bool: SNAPSHOT_DIR: str = "" SNAPSHOT_DELAY: int = _const.DEFAULT_SNAPSHOT_DELAY # kbit/s declared in the SDP (b=AS) for the video and the session: see const.SDP_BANDWIDTH. -VIDEO_BANDWIDTH, SESSION_BANDWIDTH = _const.SDP_BANDWIDTH[_const.DEFAULT_VIDEO_BANDWIDTH] +VIDEO_BANDWIDTH, SESSION_BANDWIDTH = _const.SDP_BANDWIDTH[_const.VIDEO_BANDWIDTH_LOW] VIEW_KEEPALIVE: float = _const.DEFAULT_VIEW_KEEPALIVE_CLOUD # s di silenzio a "Vedi esterno", 0 = nessuno +def video_bandwidth_choice(option, use_local_udp: bool) -> str: + """The bandwidth to ask for (const.SDP_BANDWIDTH key): the option when it is + one of the values, else (automatic, unset, unknown) by transport: 2048 on + local UDP, 256 over the cloud relay, which loses packets (#161).""" + if str(option) in _const.SDP_BANDWIDTH: + return str(option) + return _const.VIDEO_BANDWIDTH_HIGH if use_local_udp else _const.VIDEO_BANDWIDTH_LOW + + def view_keepalive_default(use_local_udp: bool) -> int: """Locale (UDP, il 2 fili): niente silenzio, occuperebbe l'appartamento; cloud: 120 s.""" return 0 if use_local_udp else _const.DEFAULT_VIEW_KEEPALIVE_CLOUD @@ -358,9 +367,8 @@ def configure(data: dict) -> None: SNAPSHOT_DELAY = int(data.get("snapshot_delay", _const.DEFAULT_SNAPSHOT_DELAY)) _ka = data.get("view_keepalive") VIEW_KEEPALIVE = int(_ka) if _ka is not None else view_keepalive_default(USE_LOCAL_UDP) - VIDEO_BANDWIDTH, SESSION_BANDWIDTH = _const.SDP_BANDWIDTH.get( - str(data.get(_const.CONF_VIDEO_BANDWIDTH)), - _const.SDP_BANDWIDTH[_const.DEFAULT_VIDEO_BANDWIDTH]) + VIDEO_BANDWIDTH, SESSION_BANDWIDTH = _const.SDP_BANDWIDTH[ + video_bandwidth_choice(data.get(_const.CONF_VIDEO_BANDWIDTH), USE_LOCAL_UDP)] ALLOWED_USERS = [str(u) for u in data.get("allowed_users") or []] RING_WEBHOOK_URL = str(data.get("ring_webhook_url") or "").strip() RING_END_WEBHOOK_URL = str(data.get("ring_end_webhook_url") or "").strip() diff --git a/custom_components/vimar_intercom/strings.json b/custom_components/vimar_intercom/strings.json index 45095c7c..a0dbc04e 100644 --- a/custom_components/vimar_intercom/strings.json +++ b/custom_components/vimar_intercom/strings.json @@ -135,7 +135,7 @@ "allowed_users": "Limita la card, lo storico squilli (foto e clip) e /audio_ws a questi utenti di Home Assistant. Gli amministratori sempre. Vuoto = ogni utente autenticato. NON limita l'entità camera né /av dalla rete locale: chi può aprire la camera vede (e sente) lo stream; e se la cartella foto è sotto una cartella media di HA, foto e clip compaiono nel browser media per tutti gli utenti.", "ring_webhook_url": "GET (fire-and-forget) inviata quando inizia uno squillo, es. per accendere un interruttore fittizio (Scrypted Dummy Switch) collegato a un Custom Doorbell Button. Un fallimento si logga come warning e non ferma lo squillo. Vuoto = disattivato.", "ring_end_webhook_url": "GET (fire-and-forget) inviata quando lo squillo finisce, risposto, annullato o scaduto: stessa idea di sopra, per spegnere l'interruttore. Vuoto = disattivato.", - "video_bandwidth": "Vale per tutto il video della targa: card, camera, /av e HomeKit. 256 kbit/s (consigliato in cloud): flusso più leggero, meno pacchetti persi sul relay. 2 Mbit/s: immagine più nitida sulle targhe che lo supportano (misurato su un Tab 5S Up 40515), ma dieci volte il traffico; con un relay che perde pacchetti (misurato su un Tab 7S Up 40517) il video si sgrana, si blocca e va in ritardo." + "video_bandwidth": "Vale per tutto il video della targa: card, camera, /av e HomeKit. Automatico (consigliato): 2 Mbit/s in UDP locale, 256 kbit/s tramite il relay cloud, che perde qualche pacchetto su cento. 256 kbit/s: flusso più leggero, meno fotogrammi rovinati con un relay che perde pacchetti, ma immagine più piccola su alcune targhe (un Tab 5S Up 40515 scende a 320x240 a 15 fps). 2 Mbit/s: l'immagine più nitida (720x576 a 24 fps su un 40515) e dieci volte il traffico; con un relay che perde pacchetti (misurato su un Tab 7S Up 40517) il video si sgrana, si blocca e va in ritardo." } }, "homekit": { @@ -447,7 +447,8 @@ }, "video_bandwidth": { "options": { - "256": "256 kbit/s (consigliato)", + "auto": "Automatico (2 Mbit/s in locale, 256 kbit/s in cloud)", + "256": "256 kbit/s (risoluzione più bassa su alcune targhe)", "2048": "2 Mbit/s (più nitido, serve una connessione pulita)" } } diff --git a/custom_components/vimar_intercom/translations/en.json b/custom_components/vimar_intercom/translations/en.json index 3c1215db..69341bec 100644 --- a/custom_components/vimar_intercom/translations/en.json +++ b/custom_components/vimar_intercom/translations/en.json @@ -135,7 +135,7 @@ "allowed_users": "Limits the card, the ring history (photos and clips) and /audio_ws to these Home Assistant users. Admins always. Empty = every logged-in user. It does NOT limit the camera entity or /av from the local network: anyone who can open the camera sees (and hears) the stream; and if the photo folder is under an HA media directory, photos and clips show up in the media browser for every user.", "ring_webhook_url": "GET (fire-and-forget) sent when a ring starts, e.g. to turn on a dummy switch (Scrypted Dummy Switch) linked to a Custom Doorbell Button. A failure is only logged as a warning and never stops the ring. Empty = off.", "ring_end_webhook_url": "GET (fire-and-forget) sent when the ring ends, answered, cancelled or timed out: same idea as above, to turn the switch off. Empty = off.", - "video_bandwidth": "Applies to every video from the panel: the card, the camera, /av and HomeKit. 256 kbit/s (recommended over the cloud): a lighter stream, fewer packets lost on the cloud relay. 2 Mbit/s: a sharper picture on panels that support it (measured on a Tab 5S Up 40515), but ten times the traffic; on a relay that loses packets (measured on a Tab 7S Up 40517) the video smears, freezes and lags." + "video_bandwidth": "Applies to every video from the panel: the card, the camera, /av and HomeKit. Automatic (recommended): 2 Mbit/s on local UDP, 256 kbit/s over the cloud relay, which loses a few packets in a hundred. 256 kbit/s: a lighter stream, fewer damaged frames on a lossy relay, but a smaller picture on some panels (a Tab 5S Up 40515 drops to 320x240 at 15 fps). 2 Mbit/s: the sharpest picture (720x576 at 24 fps on a 40515) and ten times the traffic; on a relay that loses packets (measured on a Tab 7S Up 40517) the video smears, freezes and lags." } }, "homekit": { @@ -447,7 +447,8 @@ }, "video_bandwidth": { "options": { - "256": "256 kbit/s (recommended)", + "auto": "Automatic (2 Mbit/s locally, 256 kbit/s over the cloud)", + "256": "256 kbit/s (lower resolution on some panels)", "2048": "2 Mbit/s (sharper, needs a clean connection)" } } diff --git a/custom_components/vimar_intercom/translations/it.json b/custom_components/vimar_intercom/translations/it.json index 45095c7c..a0dbc04e 100644 --- a/custom_components/vimar_intercom/translations/it.json +++ b/custom_components/vimar_intercom/translations/it.json @@ -135,7 +135,7 @@ "allowed_users": "Limita la card, lo storico squilli (foto e clip) e /audio_ws a questi utenti di Home Assistant. Gli amministratori sempre. Vuoto = ogni utente autenticato. NON limita l'entità camera né /av dalla rete locale: chi può aprire la camera vede (e sente) lo stream; e se la cartella foto è sotto una cartella media di HA, foto e clip compaiono nel browser media per tutti gli utenti.", "ring_webhook_url": "GET (fire-and-forget) inviata quando inizia uno squillo, es. per accendere un interruttore fittizio (Scrypted Dummy Switch) collegato a un Custom Doorbell Button. Un fallimento si logga come warning e non ferma lo squillo. Vuoto = disattivato.", "ring_end_webhook_url": "GET (fire-and-forget) inviata quando lo squillo finisce, risposto, annullato o scaduto: stessa idea di sopra, per spegnere l'interruttore. Vuoto = disattivato.", - "video_bandwidth": "Vale per tutto il video della targa: card, camera, /av e HomeKit. 256 kbit/s (consigliato in cloud): flusso più leggero, meno pacchetti persi sul relay. 2 Mbit/s: immagine più nitida sulle targhe che lo supportano (misurato su un Tab 5S Up 40515), ma dieci volte il traffico; con un relay che perde pacchetti (misurato su un Tab 7S Up 40517) il video si sgrana, si blocca e va in ritardo." + "video_bandwidth": "Vale per tutto il video della targa: card, camera, /av e HomeKit. Automatico (consigliato): 2 Mbit/s in UDP locale, 256 kbit/s tramite il relay cloud, che perde qualche pacchetto su cento. 256 kbit/s: flusso più leggero, meno fotogrammi rovinati con un relay che perde pacchetti, ma immagine più piccola su alcune targhe (un Tab 5S Up 40515 scende a 320x240 a 15 fps). 2 Mbit/s: l'immagine più nitida (720x576 a 24 fps su un 40515) e dieci volte il traffico; con un relay che perde pacchetti (misurato su un Tab 7S Up 40517) il video si sgrana, si blocca e va in ritardo." } }, "homekit": { @@ -447,7 +447,8 @@ }, "video_bandwidth": { "options": { - "256": "256 kbit/s (consigliato)", + "auto": "Automatico (2 Mbit/s in locale, 256 kbit/s in cloud)", + "256": "256 kbit/s (risoluzione più bassa su alcune targhe)", "2048": "2 Mbit/s (più nitido, serve una connessione pulita)" } } diff --git a/docs/CONFIGURATION.it.md b/docs/CONFIGURATION.it.md index 8dab6047..6d0743e0 100644 --- a/docs/CONFIGURATION.it.md +++ b/docs/CONFIGURATION.it.md @@ -70,6 +70,7 @@ Impostazioni → Vimar Intercom → **Configura**: | **Cartella foto squillo** (`snapshot_dir`) | dove salvare, a ogni squillo, la foto di chi suona (`squillo_AAAAMMGG_HHMMSS_mmm.jpg` + `ultimo_squillo.jpg`) e il clip dello squillo (`squillo_AAAAMMGG_HHMMSS_mmm.mp4`: il video dell'anteprima, e della chiamata se si risponde da HA, fino a 60 s, senza audio), es. `/config/media/citofono`. Deve essere scrivibile da HA. Vuoto = disattivato | | **Secondi per la foto migliore** (`snapshot_delay`) | la prima foto si salva appena arriva il primo fotogramma (~1 s dallo squillo); dopo questi secondi la sostituisce un fotogramma con l'esposizione regolata (il primo keyframe della targa è scuro). Default 3 (Tab 5S Up 40515), 0 = resta la prima | | **Silenzio della vista** (`view_keepalive`) | secondi di silenzio audio mandati durante «Vedi esterno» (0 = nessuno). Via cloud senza silenzio la targa chiude la vista a ~10 s; sul 2 fili in locale tiene occupato l'appartamento (fino a 300 s). Default: 120 via cloud, 0 in locale; sul 2 fili metti 0 o 30 | +| **Qualità video dalla targa** (`video_bandwidth`) | la banda video chiesta alla targa, per tutto il video (card, camera, `/av`, HomeKit). **Automatico** (default): 2 Mbit/s in UDP locale, 256 kbit/s tramite il relay cloud. **256 kbit/s**: meno fotogrammi rovinati con un relay che perde pacchetti, ma immagine più piccola su alcune targhe (un Tab 5S Up 40515 scende a 320x240 a 15 fps). **2 Mbit/s**: l'immagine più nitida (720x576 a 24 fps su un 40515), dieci volte il traffico; con un relay che perde pacchetti (un Tab 7S Up 40517) il video si sgrana, si blocca e va in ritardo | | **Utenti ammessi** (`allowed_users`) | Limita la card, lo storico squilli (`GET /api/vimar_intercom/rings`, foto e clip) e `/audio_ws` a questi utenti HA. Gli amministratori sempre. Vuoto = ogni utente autenticato (default). **Non** è per utente per l'entità camera né per `/av` usato con la sua chiave (la camera di HA, go2rtc: solo rete locale, vedi sotto): chi può aprire la camera vede e sente lo stream. Se la cartella foto è sotto una cartella media di HA (es. `/config/media/citofono`), foto e clip compaiono nel browser media per tutti gli utenti | | **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 | diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index d5fcac1b..80d57377 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -70,6 +70,7 @@ Settings → Vimar Intercom → **Configure**: | **Ring snapshot folder** (`snapshot_dir`) | Where the visitor's photo (`squillo_YYYYMMDD_HHMMSS_mmm.jpg` + `ultimo_squillo.jpg`) and the ring clip (`squillo_YYYYMMDD_HHMMSS_mmm.mp4`: the preview video, and the call if answered from HA, up to 60 s, no audio) are saved on every ring, e.g. `/config/media/citofono`. Must be writable by HA. Empty = off | | **Seconds for the better photo** (`snapshot_delay`) | The first photo is saved as soon as the first frame arrives (~1 s after the ring); after this many seconds it is replaced by a frame with the exposure settled (the panel's first keyframe is dark). Default 3 (Tab 5S Up 40515), 0 = keep the first | | **View silence** (`view_keepalive`) | Seconds of audio silence sent during "Vedi esterno" (0 = none). Over the cloud the panel closes the view after ~10 s without it; on the 2-wire plant in local mode it keeps the apartment busy (up to 300 s). Default: 120 over the cloud, 0 in local mode; on a 2-wire plant use 0 or 30 | +| **Video quality from the panel** (`video_bandwidth`) | The video bandwidth asked of the panel, for every video (card, camera, `/av`, HomeKit). **Automatic** (default): 2 Mbit/s on local UDP, 256 kbit/s over the cloud relay. **256 kbit/s**: fewer damaged frames on a relay that loses packets, but a smaller picture on some panels (a Tab 5S Up 40515 drops to 320x240 at 15 fps). **2 Mbit/s**: the sharpest picture (720x576 at 24 fps on a 40515), ten times the traffic; over a lossy relay (a Tab 7S Up 40517) the video smears, freezes and lags | | **Allowed users** (`allowed_users`) | Limits the card, the ring history (`GET /api/vimar_intercom/rings`, photos and clips) and `/audio_ws` to these HA users. Admins are always allowed. Empty = every logged-in user (default). It is **not** per user for the camera entity nor for `/av` used with its key (HA's own camera stream, go2rtc: local network only, see below): anyone who can open the camera sees and hears the stream. If the photo folder is under an HA media directory (e.g. `/config/media/citofono`), photos and clips also show up in the media browser for every user | | **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 | diff --git a/docs/TROUBLESHOOTING.it.md b/docs/TROUBLESHOOTING.it.md index 4beec656..27476fa5 100644 --- a/docs/TROUBLESHOOTING.it.md +++ b/docs/TROUBLESHOOTING.it.md @@ -18,6 +18,7 @@ corretti in una release, indicata nella colonna *Cosa fare*. Poi attiva il log d | `queued` / `202 Accepted` nei log o nel sensore *Intercom Ultimo Comando* | Il relay cloud ha accettato il comando ma non ne ha confermato la consegna; la sua risposta può arrivare dopo ~15 s | 1.0.20 o successiva: un 202 risulta `queued`, non eseguito, e i comandi via cloud aspettano 20 s. Controlla se è successo prima di riprovare | [#120](https://github.com/ha-vimar/ha-vimar-intercom/pull/120), [#139](https://github.com/ha-vimar/ha-vimar-intercom/pull/139), [#140](https://github.com/ha-vimar/ha-vimar-intercom/issues/140) | | Porta e attuatori dicono «Non registrato» per ore dopo un calo di rete (cloud) | Due cicli di riconnessione giravano insieme | 1.0.10 o successiva | [#23](https://github.com/ha-vimar/ha-vimar-intercom/issues/23) | | Camera: `404` da `55100`, niente video | La targa video di default non esiste sull'impianto | Importa la rubrica (imposta *Targa video*) o impostala a mano. Dalla 1.0.15 il campo vuoto usa la targa che ha suonato per ultima con il video | [#3](https://github.com/ha-vimar/ha-vimar-intercom/issues/3), [#129](https://github.com/ha-vimar/ha-vimar-intercom/issues/129) | +| Il video si sgrana, si blocca o va in ritardo, di più dalla 1.0.19; il log di debug mostra pacchetti video persi (`persa`) | La targa manda 2 Mbit/s tramite un relay cloud che perde qualche pacchetto su cento: dieci volte i pacchetti, dieci volte i fotogrammi rovinati (misurato su un Tab 7S Up 40517) | Impostazioni → *Qualità video dalla targa*: *Automatico* (256 kbit/s in cloud) o 256 kbit/s. Torna a 2 Mbit/s se lì l'immagine andava bene e la vuoi più nitida | [#161](https://github.com/ha-vimar/ha-vimar-intercom/pull/161) | | Clip dello squillo salvata ma niente foto (Tab 7S 40517, cloud) | In analisi: la foto aspettava un secondo keyframe e questa targa forse ne manda uno solo | 1.0.20 o successiva (foto dal primo keyframe, decoder nel log di debug). Se manca ancora: aggiungi alla issue il log di debug di uno squillo | [#129](https://github.com/ha-vimar/ha-vimar-intercom/issues/129) (aperta) | | Né anteprima né foto mentre suona, solo dopo la risposta | L'impianto non manda early media | Limite noto, niente da correggere lato HA | — | | «AV stream: call not established after 25s» subito dopo un riaggancio (UDP locale) | La targa era ancora occupata con la chiamata precedente | 1.0.17 o successiva | [#41](https://github.com/ha-vimar/ha-vimar-intercom/issues/41) | diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index e38c6189..fa24641a 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -18,6 +18,7 @@ reproduce the problem once. Not listed here? Open an issue with that log. | `queued` / `202 Accepted` in the logs or in the *Intercom Ultimo Comando* sensor | The cloud relay accepted the command but did not confirm delivery; its answer can take ~15 s | 1.0.20 or newer: a 202 is reported as `queued`, not as done, and cloud commands wait 20 s. Check whether it happened before retrying | [#120](https://github.com/ha-vimar/ha-vimar-intercom/pull/120), [#139](https://github.com/ha-vimar/ha-vimar-intercom/pull/139), [#140](https://github.com/ha-vimar/ha-vimar-intercom/issues/140) | | Door and actuators say "Not registered" for hours after a network drop (cloud) | Two reconnection loops ran at the same time | 1.0.10 or newer | [#23](https://github.com/ha-vimar/ha-vimar-intercom/issues/23) | | Camera: `404` from `55100`, no video | The default video panel does not exist on the plant | Import the phonebook (it sets *Video entrance panel*) or set it by hand. Since 1.0.15 an empty field uses the panel that last rang with video | [#3](https://github.com/ha-vimar/ha-vimar-intercom/issues/3), [#129](https://github.com/ha-vimar/ha-vimar-intercom/issues/129) | +| Video smears, freezes or lags, more since 1.0.19; the debug log shows lost video packets (`persa`) | The panel sends 2 Mbit/s through a cloud relay that loses a few packets in a hundred: ten times the packets, ten times the damaged frames (measured on a Tab 7S Up 40517) | Settings → *Video quality from the panel*: *Automatic* (256 kbit/s over the cloud) or 256 kbit/s. Back to 2 Mbit/s if the picture was fine there and you want it sharper | [#161](https://github.com/ha-vimar/ha-vimar-intercom/pull/161) | | Ring clip saved but no ring photo (Tab 7S 40517, cloud) | Under investigation: the photo waited for a second keyframe and this panel may send only one | 1.0.20 or newer (photo from the first keyframe, decoder logged at debug). Still missing: add the debug log of one ring to the issue | [#129](https://github.com/ha-vimar/ha-vimar-intercom/issues/129) (open) | | No preview and no photo while it rings, only after answering | The plant sends no early media | Known limitation, nothing to fix on the HA side | — | | "AV stream: call not established after 25s" right after hanging up (local UDP) | The panel was still busy with the previous call | 1.0.17 or newer | [#41](https://github.com/ha-vimar/ha-vimar-intercom/issues/41) | diff --git a/tests/test_config_flow_picg.py b/tests/test_config_flow_picg.py index 639b4238..8852cece 100644 --- a/tests/test_config_flow_picg.py +++ b/tests/test_config_flow_picg.py @@ -160,7 +160,7 @@ def test_the_settings_page_saves_the_video_bandwidth(of): assert out["type"] == "create_entry", out assert out["data"]["video_bandwidth"] == "2048" out = _save(of, "async_step_settings", SETTINGS, FOREIGN) - assert out["data"]["video_bandwidth"] == "256", "the default when not chosen" + assert out["data"]["video_bandwidth"] == "auto", "the default when not chosen" out = _save(of, "async_step_settings", SETTINGS, {**FOREIGN, "video_bandwidth": "2048"}) assert out["data"]["video_bandwidth"] == "2048", "a saved choice is kept" diff --git a/tests/test_sdp.py b/tests/test_sdp.py index e9ad9744..b242bd19 100644 --- a/tests/test_sdp.py +++ b/tests/test_sdp.py @@ -33,18 +33,24 @@ def test_build_sdp_plain_rtp_default(): assert sip.SDP._local_video_crypto_key is None -@pytest.mark.parametrize("option, video, session", [ - (None, 256, 512), # the default: what the integration asked before 1.0.19 - ("256", 256, 512), - ("2048", 2048, 2200), # the 40515's sharper picture, on request - ("bogus", 256, 512), +@pytest.mark.parametrize("option, local_udp, video, session", [ + (None, False, 256, 512), # automatic over the cloud relay + (None, True, 2048, 2200), # automatic on local UDP + ("auto", False, 256, 512), + ("auto", True, 2048, 2200), + ("256", True, 256, 512), # forced either way + ("2048", False, 2048, 2200), + ("bogus", False, 256, 512), # an unknown value is automatic ]) -def test_build_sdp_asks_for_the_chosen_video_bandwidth(monkeypatch, option, video, session): +def test_build_sdp_asks_for_the_chosen_video_bandwidth(monkeypatch, option, local_udp, video, session): """2048 kbit/s made a 40517 send ten times the packets through a cloud relay - that loses them (smeared, frozen, laggy video): it is an option, 256 by default.""" + that loses them: automatic is 256 over the cloud, 2048 on local UDP (#161).""" from custom_components.vimar_intercom import runtime as R + for name in dir(R): # configure() rewrites module globals: put them back + if name.isupper(): + monkeypatch.setattr(R, name, getattr(R, name)) S.MEDIA_ENC = False - data = {"sip_user": "1001", "sip_domain": "example.invalid"} + data = {"sip_user": "1001", "sip_domain": "example.invalid", "use_local_udp": local_udp} if option is not None: data["video_bandwidth"] = option R.configure(data)