Skip to content

Releases: mosandlt/Bosch-Smart-Home-Camera-Tool-HomeAssistant

v10.7.0 — Media Browser provider (local + NAS)

04 May 10:17

Choose a tag to compare

Event recordings now appear in HA's Media Browser — both local downloads and NAS uploads.

A new media_source platform exposes downloaded events under Media → Bosch SHC Camera. Two backends are auto-detected from existing options:

  • Local — when Events automatically download is enabled with a download_path. Tree: Camera → Date → Event.
  • NAS / SMB — when SMB upload is enabled (default for users who don't want auto-download to fill HA's small disk). Tree: Year → Month → Day → Event, matching the on-disk layout (all cameras share a day-folder). Files are streamed on demand via smbprotocol with HTTP Range support so MP4 seeking works — nothing is cached on the HA host.

Each event title shows time, type, and camera (e.g. 09:15:23 — MOVEMENT (Garten)). MP4 clips play inline; the matching JPEG snapshot doubles as a thumbnail for its clip. macOS resource-fork files (._*) are filtered out — relevant for FRITZ.NAS / Time Machine targets.

When only one backend is configured the source-chooser is hidden so the tree opens straight at the meaningful content. With both backends enabled the entry root shows Lokal and NAS \\server\share as siblings.

Manual filter — Media Browser source option. New options-flow dropdown overrides the auto-detect when needed:

Value Effect
Auto (default) Show every backend that has data
Nur Lokal Hide the NAS even if SMB upload is active
Nur NAS Hide local files even if auto-download is active
Deaktiviert Hide the Media Browser entry entirely

Files are served by an authenticated /api/bosch_shc_camera/event/… view; path-traversal is blocked, only image/jpeg and video/mp4 are returned.

Forum thread context: simon42 community post #14 — same UX as Reolink's Media → Reolink entry.

v10.6.2 — Right Bosch icon (red Smart Camera, not blue SHC)

03 May 19:15

Choose a tag to compare

Branding fix. v10.6.1 mistakenly used the blue Bosch Smart Home hub icon. v10.6.2 uses the red Bosch Smart Camera app icon — that's the camera-specific Bosch branding which matches what this integration actually does.

Source: official iOS App Store listing for the Bosch Smart Camera app (Robert Bosch GmbH, bundle ID com.bosch.smartcamera). 512×512 master, downscaled to 256×256 for the standard icon.png via Lanczos resampling. Same file used for dark_icon.png since the red gradient + white camera glyph reads well on both light and dark HA themes.

Pure asset swap — no Python or card code changes. After updating, the new icon appears in Settings → Devices & Services and the HACS catalogue. Browser hard-refresh may be needed if HA cached the old icon.

v10.6.1 — Official Bosch Smart Home icon

03 May 19:13

Choose a tag to compare

Branding refresh — official Bosch Smart Home icon.

The integration's brand assets (icon.png, icon@2x.png, dark_icon.png, dark_icon@2x.png) now use the official Bosch Smart Home brand mark — the same icon the HA-bundled bosch_shc integration uses, sourced from the Home Assistant Brands repository (CC BY 4.0).

Replaces the previous custom red camera icon for visual consistency with the rest of the Bosch Smart Home ecosystem in HA.

Pure asset swap — no Python or card code changes. After updating, the new icon appears in Settings → Devices & Services and the HACS catalogue. Browser hard-refresh may be needed if HA cached the old icon.

v10.6.0 — Image rotation 180° for ceiling-mounted indoor cameras

03 May 18:59

Choose a tag to compare

New: 180° image rotation switch for ceiling-mounted indoor cameras.

Adds a per-camera switch switch.bosch_<cam>_bild_180deg_drehen (Indoor only — Gen1 360 + Gen2 Indoor II) that rotates the camera image by 180° for upside-down ceiling mounting. Outdoor cameras don't get the switch since their mounting orientation is fixed by design.

Bosch firmware exposes no native rotation API, so the implementation is client-side at three layers:

What it does

  • Lovelace card: applies a CSS transform: rotate(180deg) to the <video> and <img> elements. Zero CPU, zero latency, GPU-composited — toggle is instant with no stream restart and no re-encode.
  • Snapshot path: rotates the JPEG via PIL in camera.async_camera_image() before serving it through HA's camera proxy. So push notifications, NAS clip uploads, and any other consumer that reads the camera entity also see the right-way-up image (~15–30 ms per snapshot).
  • PTZ pan inversion (Gen1 360 only): BoschPanNumber automatically inverts the slider sign when the rotation switch is on, so "right" on the slider stays "right" on the user's screen even when the camera is upside-down.

State persists across HA restarts via RestoreEntity. Default OFF.

Card v2.11.1 ships alongside

The card auto-detects the rotation switch via the entity-id convention switch.<base>_bild_180deg_drehen and applies/removes the CSS class on every set hass() — no card YAML config change required.

Why client-side instead of go2rtc rotation

Researched go2rtc's #rotate=180 parameter: it invokes a full FFmpeg transpose=1,transpose=1 re-encode (~30–60% of one CPU core per 1080p stream, +80–150 ms latency, quality loss from second-generation H.264 encoding) and causes a 3–6 s blackout on every toggle (FFmpeg respawn, HLS playlist invalidation, WebRTC PeerConnection drop). H.264 SEI display-orientation and MP4 tkhd matrix are decoder hints that browsers ignore for live HLS. CSS in the card avoids all of this.

Notes

  • Bosch's official position is that the cameras are not designed for ceiling mounting — for the Gen1 360, the privacy shutter is gravity-actuated and may not open/close reliably when upside-down. This is hardware, not fixable in software.
  • For Lovelace cards other than the bundled bosch-camera-card, only the snapshot path applies — live <video> elements in third-party cards won't be rotated unless they also read this switch.

Files changed

  • __init__.py — coordinator dict _image_rotation_180
  • switch.py — new BoschImageRotation180Switch (RestoreEntity)
  • camera.py_rotate_jpeg_180 PIL helper + hook in async_camera_image()
  • number.pyBoschPanNumber sign inversion when rotation on
  • translations/de.json + en.json — "Bild 180° drehen" / "Rotate Image 180°"
  • src/bosch-camera-card.js_applyImageRotation180() method + CSS .rotated-180

Verified live on Innenbereich (Gen2 Indoor II) — snapshot rotation visually confirmed (timestamp overlay flipped + room contents inverted), card CSS rotation verified live in Chrome with no console errors, Pan inversion logic in place but not yet live-verified (Gen1 360 currently offline).

v10.5.4 — Stream switch unblocked + AUTO-mode self-heals

03 May 03:44

Choose a tag to compare

Stream switch unblocked when prior session expired upstream

HA Stream.stop() could block waiting for a stuck FFmpeg reconnect-loop to exit when the underlying URL was already invalid (e.g. relay-side lifetime cap reached). Both teardown paths now wrap the call in asyncio.wait_for(timeout=5) and force-detach on timeout. Without this fix, a single hung stream.stop() held the per-camera setup lock for >5 minutes and every subsequent switch-ON returned try_live_connection: already in progress for ... — skipping.

REMOTE session lifetime watchdog

Mirror of the existing LOCAL keepalive task: when a stream opens against the cloud relay, a generation-tracked terminator is scheduled and tears the session down cleanly before the relay drops the RTSP TCP with a hard reset. Without this, the URL goes stale silently — switch shows "streaming" but FFmpeg is in a reconnect-loop, the next consumer sees a 2-minute HLS spinner. OFF→ON cycles cancel the watchdog automatically.

AUTO-mode REMOTE fallback self-heals

Four independent fixes reduce the "permanently pinned to Cloud" failure mode that occurred after a transient LAN issue saturated the LOCAL error counter:

  • record_stream_error skips the increment when the active connection is REMOTE — Cloud-side hiccups no longer count against the LAN's health budget.
  • The error counter time-decays in _try_live_connection_inner's AUTO branch: 5 minutes if the camera's TCP-ping cache says LAN is currently reachable, 30 minutes otherwise.
  • The status-loop's TCP-ping fast-path actively clears the fallback flag the moment LAN becomes reachable again — the next stream-on attempts LOCAL first.
  • During a currently running REMOTE-fallback stream, the same trigger additionally schedules a try_live_connection(is_renewal=True) so the live HLS session migrates Cloud → LAN via Stream.update_source() without waiting for a re-toggle. ~2-3s re-buffer during the swap. 5-minute cooldown prevents ping-pong.

max_stream_errors raised — per-model thresholds

With self-heal in place a false fallback now recovers automatically, so the gradual-counter path can give LOCAL a fairer chance before giving up. Default bumped from 3 → 5 (indoor / INDOOR, HOME_Eyes_Indoor, default unknown), explicit override 10 for outdoor models (OUTDOOR / CAMERA_EYES, HOME_Eyes_Outdoor / CAMERA_OUTDOOR_GEN2) where real WLAN flap + slower encoder init produce more transient bursts. The watchdog's hard 120 s "no healthy HLS output" escalation is unchanged.

Documentation

New AUTO-mode connection state-machine diagram in README.md showing LOCAL ↔ REMOTE_fallback transitions, counter decay, active promotion, and lifetime-watchdog teardown.


Full changelog: CHANGELOG.md

v10.5.3 — mark_events_read default flipped to OFF

02 May 17:35

Choose a tag to compare

What changed

mark_events_read default flipped to OFF. v10.5.2 introduced the option but kept the previous behaviour as default — events were still being marked as read on the Bosch cloud after HA processed them, which silently consumed the "new event" highlight in the Bosch app for users who only use HA for live streaming.

The default in OPTIONS_DEFAULTS, in the Configure dialog, and in all five gating call sites now resolves to False — fresh installs and existing installs that never explicitly toggled the option both stop firing PUT /v11/events {isRead: true}. The Bosch app keeps treating new events as unread regardless of whether HA already saw them.

Who is affected

  • Default user (HA + Bosch app side-by-side): new behaviour — events stay unread in the Bosch app, just like when no integration is installed. This is what most users expect.
  • HA-as-primary-client user: if you prefer the v10.5.2 behaviour (HA marks events read so the app doesn't show stale "new event" badges), enable it via Integration → Configure → Mark Bosch cloud events as read.

Local dedup via _last_event_ids is unaffected — automation triggers on binary_sensor.bosch_*_motion continue to fire normally.

Reported by

Thanks to xDraGGi on the simon42 forum (Topic 81743 / Post 366006) for surfacing this.

Full changelog

CHANGELOG.md

v10.5.2 — Optional cloud isRead, FCM Push Status rename, dropdown gating

01 May 03:23

Choose a tag to compare

New option mark_events_read (default ON)

The integration calls PUT /v11/events {id, isRead: true} after processing each motion/audio event from five different code paths (startup poll, per-event coordinator tick, auto-download cycle, FCM push handler, FCM clip handler). Side effect: motion events appear as already viewed in the Bosch app, even if the user only consumes them via HA's live stream and never opens an automation. Reported by xDraGGi on the simon42 forum (Topic 81743 / Post 364079).

New option mark_events_read in Integration → Configure gates all five call sites:

  • Default True preserves backwards-compatible behaviour.
  • Set to False to keep events flagged as unread in the Bosch app while still receiving them in HA — useful for users who only consume the live stream in HA but want the Bosch app to keep highlighting new events.

Local dedup via _last_event_ids is unaffected (it lives independently of the cloud isRead flag).

Sensor renamed: Event DetectionFCM Push Status

The diagnostic sensor BoschFcmPushStatusSensor was named "Event Detection" in entity translations, which suggested that a disabled state meant no event detection at all. In reality the sensor only reflects the FCM-push pipeline (states: fcm_push / polling / disabled) — normal coordinator polling continues regardless. The unique_id (bosch_shc_camera_fcm_push_status) was already correct and is unchanged, so historical state preserves cleanly across the rename.

FCM Push Mode dropdown gated on master switch

The per-integration select.fcm_push_mode entity is now available=False whenever enable_fcm_push is OFF in Integration → Configure. Previously the dropdown was fully interactive on the device page even though changing it had no effect until the master switch was enabled — discovered via simon42-forum PN where a user reported Event Detection: disabled while showing FCM Push Mode: Auto in the same screenshot.


Full changelog: CHANGELOG.md

v10.5.1 — Cloudflare-Tunnel HLS unbuffer + iOS Companion HLS fallback + motion sensor cache sync

29 Apr 09:00

Choose a tag to compare

Stream Status Sensor. New sensor.bosch_{name}_stream_status entity per camera with states idle / warming_up / connecting / streaming / streaming_remote. device_class: enum with _attr_options — HA's more-info popup shows all possible states and a categorical state-history timeline. The card reads this sensor on every hass update: opening a dashboard while the backend is already pre-warming now correctly shows the status overlay and snapshot background without requiring a toggle click (cold-open fix). New snapshot_during_warmup card config option (default true).

Full entity translations for all platforms. _attr_has_entity_name = True on _BoschSensorBase — sensor names now render as "Bosch {Camera} {Sensor Name}" via translations instead of falling back to the plain device name. All 7 enum-like sensors have SensorDeviceClass.ENUM + _attr_options. Complete entity.* translation blocks in en.json and de.json for all platforms: 26 sensors, 22 switches, 16 numbers, 5 selects (with per-state labels), 3 binary sensors, 2 buttons, 1 update, 3 lights. _attr_entity_category set on all entities across all platforms.

Event type + alarm state option fixes. BoschLastEventTypeSensor kept underscores in API values (trouble_disconnect not trouble disconnect) — options and translations expanded with audio_alarm, trouble_disconnect, trouble_reconnect. BoschAlarmStateSensor options expanded with SYSTEM_MANAGED_ARMED/DISARMED, ARMED_AWAY, ARMED_STAY, DISARMED after SYSTEM_MANAGED_DISARMED caused a ValueError at runtime on the Gen2 Indoor II.


Cloudflare-Tunnel HLS-Buffering Workaround (cf_unbuffer.py, runtime monkey-patch). Diagnosed 2026-04-29 from a remote-over-Cloudflare-tunnel session: cloudflared buffers HTTP responses by default per its connection.shouldFlush(headers) source — only Content-Type: text/event-stream / application/grpc / application/x-ndjson, no Content-Length, or Transfer-Encoding: chunked triggers streaming mode. HA's HLS endpoints (/api/hls/<token>/*.m3u8 and *.m4s segments) hit none of those — application/vnd.apple.mpegurl / video/mp4 with Content-Length set, no chunked. Cloudflared collected each segment in full at the edge before forwarding; iOS WKWebView on cellular gave up before the buffer flushed (visible in the cloudflared add-on log as Incoming request ended abruptly: context canceled). Two-prong runtime monkey-patch of HA's view classes (homeassistant.components.stream.hls): (1) HlsMasterPlaylistView + HlsPlaylistView get their Content-Type rewritten to text/event-stream; x-actual=application/vnd.apple.mpegurl — cloudflared HasPrefix-matches Branch (C) → flush. Players dispatch HLS playlists by URL extension, not by response Content-Type, so the mime lie is harmless. (2) HlsInitView + HlsPartView + HlsSegmentView get their web.Response re-emitted as a chunked web.StreamResponse (no Content-Length) — cloudflared shouldFlush() Branch (B) → flush. AVFoundation handles HTTP/1.1 chunked encoding natively, so we don't need to lie about the Content-Type for the binary segments. Verify with curl -sI https://your-ha.example.com/api/hls/<token>/segment/0.m4s — must show Transfer-Encoding: chunked and no Content-Length. Why view monkey-patch and not aiohttp middleware: HA freezes app.middlewares and app.on_response_prepare after HTTP setup; appending after that point either raises "Cannot modify frozen list" or silently fails. Class-level monkey-patches on HA's view methods work at any time because aiohttp resolves class_handler.handle via getattr at request-dispatch time. Caveat: this patch fixes the cloudflared buffer layer cleanly (curl-verified), but in real-world testing the iPhone iOS Companion App on 5G still showed the symptom intermittently — the remaining issue is AVFoundation's automaticallyWaitsToMinimizeStalling=true default plus WKWebView's stricter timeouts vs Mobile Safari, which we cannot fix from inside the integration. The patch helps universally for HLS over Cloudflare Tunnel and is a no-op on other transports. The README's new "Known Limitations" section documents the hybrid setup (Cloudflare Tunnel for UI, FRITZ!Box-WireGuard / Tailscale / Nabu Casa for the stream path) which is the de-facto HA-community-2026 best-practice for camera streaming on cellular.

iOS Companion App livestream fix (Card v2.10.14). Diagnosed live 2026-04-29 from a remote-over-Cloudflare-tunnel session: HLS playback worked perfectly in mobile Safari, but in the HA Companion App on iOS the card hung indefinitely on "HLS wird geladen…" with the snapshot frozen behind the spinner. Backend, go2rtc, TLS proxy, and the HLS manifest URL were all healthy (/api/hls/<token>/master_playlist.m3u8 served HTTP 200 on every transport — LAN, Cloudflare-tunnel, even with the iOS Companion User-Agent). Root cause sat in _startLiveVideo of bosch-camera-card.js: the player init did const Hls = await this._loadHlsJs() unconditionally before reaching the native-HLS fallback at video.canPlayType("application/vnd.apple.mpegurl"). WKWebView in the iOS Companion App is stricter than mobile Safari about external CDN scripts (jsdelivr) — when the hls.js load throws, control jumped into the catch-and-retry block and the native fallback (which works perfectly on iOS, where HLS is decoded inside WebKit and no MSE/hls.js is needed) was never tried. Fix: wrap _loadHlsJs() in its own try/catch so a CDN failure no longer aborts the whole init; if Hls is null OR Hls.isSupported() is false (= iOS, where MSE is unavailable), fall through to video.src = result.url and play natively. No backend change. Also robust against any future jsdelivr hiccup on Chromium/Firefox — those keep using hls.js via MSE; only the failure-mode path differs.

Motion / Person / Audio binary sensor reliability fix. Diagnosed from a community report (simon42 forum, geotie 2026-04-29) on a 4-year-old Gen1 Eyes Outdoor: Last Event and Events Today updated correctly on every motion, but the Motion binary sensor (and by extension automations triggering on it) only fired sporadically — exactly the class of failure where event counters work but the windowed sensor doesn't. Two compounding issues. (1) FCM push didn't update coordinator.data synchronously. fcm.py wrote new events to coordinator._cached_events and called async_update_listeners(), which signals entities to re-read coordinator.data — but coordinator.data[cam_id]["events"] was still the snapshot from the last coordinator tick (up to scan_interval seconds old). The windowed BoschMotionBinarySensor/BoschPersonDetectedBinarySensor/BoschAudioAlarmBinarySensor (binary_sensor.py) read from coordinator.data["events"] and consequently saw a stale event list — so a brand-new event arrived via FCM, the listeners were notified, but is_on evaluated False because the old top-of-list event was already outside the active window. Fix: after writing to _cached_events, also mirror into coordinator.data[cam_id]["events"] so the sensors see the freshest event on the very same async_update_listeners() cycle. (2) EVENT_ACTIVE_WINDOW raised from 30 s to 90 s. Defense-in-depth for the polling-only path: when FCM is unhealthy (_fcm_healthy = False) or hasn't pushed yet, events are detected only on coordinator ticks (default scan_interval = 60 s). With a 30 s window, an event landing right after a tick would be 30–60 s old by the time it's first seen by data[] and the sensor would never go ON. 90 s comfortably covers scan_interval plus the time it takes the polling loop to finish. The two binary sensors that don't depend on this window (Last Event timestamp, Events Today count) were already working correctly in the field and are unchanged. Why this primarily affected the older Gen1 Outdoor in the field: the bug was generation-agnostic, but Gen2 with DualRadar fires far fewer "uninteresting" motion events, so the 30 s window was less commonly missed; on a 4-year-old Gen1 Outdoor every leaf and shadow triggers, making the missed-window problem visible. No API or config changes; existing automations bound to binary_sensor.bosch_*_motion start triggering reliably without any user action.

v10.5.0 — FTP upload backend + NAS settings correctness

29 Apr 03:59

Choose a tag to compare

FTP upload backend for NAS uploads + correctness fixes for the NAS settings. (1) FTP as alternative to SMB for event uploads. The FRITZ!Box NAS (and several other consumer-grade NAS devices) handles SMB metadata operations very poorly — and on macOS Sequoia 15.x the smbfs client is also known to hang on cross-directory rename() for minutes at a time (multiple Apple-Discussions threads, plus AVM and PC-WELT documenting the FritzOS-CPU bottleneck on USB storage). Real measurement on a FRITZ!Box 7590 with ~3300 small files (JPG + MP4): SMB rename via macOS-mounted share blocked for 9+ minutes without a single completed move, while FTP RNFR/RNTO against the same hardware completed all 3117 moves in 42 seconds (~74 file/s). The integration now exposes a new upload_protocol option (SMB / FTP, default SMB for backwards compatibility). FTP reuses the existing smb_server / smb_username / smb_password / smb_base_path / smb_folder_pattern / smb_file_pattern fields — only the smb_share field is unused under FTP because FTP has no shares (the base path is taken relative to the FTP root, e.g. FILES/Bosch-Kameras instead of just Bosch-Kameras on a FRITZ!Box). All three sync paths are protocol-aware: the periodic event upload, the daily retention cleanup (FTP uses MDTM for accurate mtimes), and the disk-free check (skipped silently under FTP because there's no portable RPC for it). Implementation uses Python's stdlib ftplib so no new requirement is added to the manifest. (2) NAS folder-pattern docs corrected. The settings descriptions for smb_folder_pattern / smb_file_pattern listed placeholders as [year], [month], [day], etc. — but the code actually uses Python str.format() with {year}, {month}, {day}, … so anyone who copy-pasted the documented pattern into the field got a KeyError on the next upload. All three translation files (strings.json, translations/en.json, translations/de.json) are now consistent with the code, plus the alert-storage path was corrected from /media/bosch_alerts/ (wrong) to www/bosch_alerts/ (the actual on-disk location served at /local/bosch_alerts/). (3) Default folder pattern now {year}/{month}/{day}. Previously {year}/{month} — for cameras that fire many motion events per day this produces folders with a thousand files inside them, which is hostile to both browsing and SMB performance. Existing custom patterns are untouched; only the default for new installs (and for upgraders who have not yet customised the field) changes. (4) Translation cleanup. German and English option screens are now 100 % key-aligned (no more cases of an option being labelled in one language but missing the description in the other). Previously-missing entries restored: enable_go2rtc description was missing in both languages, debug_logging description was missing in German. The alert_notify_service description in English now matches the German one and the actual code behaviour: this field is the fallback when per-type fields are empty, not a fan-out destination as it used to read. (5) Helper script migrate_smb_day_folders.sh ships at the repo root for users who want to migrate an existing flat {year}/{month}/ layout into the new {year}/{month}/{day}/ layout. Default dry-run, parses the day from filename, and runs against any mounted share.

v10.4.10 — Resilience fixes (LAN stream + WAN outage)

28 Apr 04:25

Choose a tag to compare

Eight resilience + UX fixes for stream stability, WAN/router/privacy outages, and stale-state UI

1. Stream stays on LAN after idle reconnect (Bosch session-cred rotation)

Symptom. AUTO mode pre-warms LOCAL successfully and runs cleanly for ~14 min, then — when the HLS consumer disconnects (browser tab closed) and HA's stream-worker later reconnects — the camera answers HTTP 401 on the same TLS proxy. After 3 consecutive Error from stream worker: 401 Unauthorized errors, AUTO fell back to REMOTE even though the LAN was perfectly reachable.

Root cause. Bosch silently rotates the per-session digest creds on every PUT /connection LOCAL. The original creds remain valid for the active RTSP connection (FFmpeg keepalive) but become invalid for new connects within ~60 s.

Fix — reactive 401 rescue. _handle_stream_worker_error issues one fresh PUT /connection LOCAL on a 401 before falling through to REMOTE. Gated by a per-camera counter (max 1/burst) with 5-min time-decay so the counter doesn't stick at 1.

Fix — proactive cred refresh in heartbeat. Each successful heartbeat parses the response, caches user/password into _live_connections, rebuilds the cached rtspsUrl with fresh creds, and calls Stream.update_source(). The running worker is unaffected; the next reconnect picks up fresh creds and avoids the 401 in the first place. (Capture analysis in captures/api-findings.md §1.)

2. FCM noise filter for WAN outages

Symptom. Router reboot → firebase_messaging.fcmpushclient._listen re-enters itself recursively per retry, each ERROR log line carries a ~3000-frame stack trace. With 30 s reconnect cadence: ~200 lines/s, ~12 500 lines/min, MainThread wedged in stack-trace formatting, CPU 30 % → 85 %, coordinator stalled.

Fix. New _FCMNoiseFilter strips exc_info/exc_text from "Unexpected exception during read" records and rate-limits to one pass-through per 60 s. Reconnect behaviour unchanged. Library issue sdb9696/firebase-messaging#33 covers the abort angle but not the recursive trace.

3. Same-camera stream-source race protection

try_live_connection: already in progress for X — skipping is now the warning when two parallel start attempts collide. First one wins, second exits cleanly without leaving a half-built TLS proxy.

4. Hardware-privacy auto-teardown

Symptom. When the camera's physical privacy button is pressed (or someone toggles privacy in the Bosch app), the cloud reports privacyMode=ON but our BoschPrivacyModeSwitch.async_turn_on — the only path that calls _tear_down_live_stream — never runs. Result: stuck state: streaming, the live-stream switch frozen on on, and the TLS proxy entering an endless reconnect loop against the now-gone camera (Errno 113 Host unreachable). Observed in production at 06:25 on 2026-04-28 when a household member pressed the indoor cam's privacy button.

Fix. In _async_update_data, when the privacy cache transitions OFF→ON outside the user-write lock and a live session is active, schedule the same teardown as the user-toggle path.

5. TLS-proxy connect-failure circuit breaker

Symptom. When the camera goes physically offline (privacy button, power cut, Wi-Fi drop), HA's stream worker keeps opening new client connections every few seconds, each one triggering a 10 s connect timeout against the gone camera — burning CPU on a hopeless loop.

Fix. After 5 consecutive connect failures within 30 s the proxy closes its server socket. The coordinator (privacy-aware) decides whether to rebuild the session via try_live_connection() once the camera is reachable again or stay torn-down.

6. "does not support play stream service" log filter

Symptom. During the ~25 s LOCAL pre-warm window any consumer that calls the camera/stream WS API gets stream_source()==None and HA's camera component logs an ERROR. Real captures show 9 such lines in 15 s for a single stream start.

Fix. New _StreamSupportNoiseFilter keeps one ERROR per 30 s per bosch_* entity so a real "stream truly broken" issue still surfaces, but the pre-warm-window burst is collapsed to a single line. Other camera integrations untouched.

7. Overview card use_bosch_sort option (Card v2.10.12 / Overview v1.1.0)

New per-card opt-in flag for custom:bosch-camera-overview-card: when set, sorts cameras inside each tier (live → privacy → offline) by the Bosch-app priority instead of alphabetically. The priority is read from the new bosch_priority attribute on each Bosch camera entity, which mirrors the float priority field returned by GET /v11/video_inputs (settable via PUT /v11/video_inputs/order from the Bosch app). Default false preserves the old alphabetic ordering.

type: custom:bosch-camera-overview-card
use_bosch_sort: true

8. Card stale-state guard against accidental toggles (Card v2.10.13)

Symptom. Diagnosed live 2026-04-28: Terrasse Live-Stream switch flipped to off without a corresponding user action. HA logbook context showed parent_id: null (= direct service call, not an automation) and a system-admin user_id from the iOS Companion App refresh-token. The user reported they hadn't tapped it.

Root cause. When the HA-Companion-App suspends its WebSocket on backgrounding (Mobile/WLAN switch, app put away for a while), the local hass.states cache can briefly disagree with the server until the next WS push arrives. A user tap on the card's stream button during that window fires the wrong-direction toggle, because the card was reading a stale state.

Fix in bosch-camera-card.js:

  1. _toggleStream is now async and pre-checks the server state via GET /api/states/<switch> before calling switch.turn_on/turn_off. If the fresh REST result disagrees with what the card was showing, the toggle is aborted: optimistic state cleared, view re-rendered, user has to tap again with the now-correct view.
  2. _onVisibilityChange (already wired to the Page Visibility API) now also pulls fresh REST states for the four primary toggle switches (live_stream, privacy_mode, audio, camera_light) when the page comes back to the foreground, so a backgrounded card resyncs immediately rather than waiting for the next WebSocket push.

Behaviour unchanged when the card was already in sync; <100 ms REST round-trip before the existing optimistic flip in the common path.


🤖 Generated with Claude Code