Skip to content

Commit e008f18

Browse files
mosandltclaude
andcommitted
v10.5.0 — FTP upload backend + NAS settings correctness fixes
Three problems addressed in one release: (a) macOS Sequoia smbfs hangs on cross-directory rename for minutes against FRITZ.NAS so SMB-based event upload there is fragile; (b) the NAS folder-pattern docs used [year]/[month] brackets while the code uses Python str.format with curly braces, so anyone copying the documented pattern hit a KeyError on the next upload; (c) the default folder pattern was {year}/{month} which produces directories with thousands of files for active cams. Backend swap. New 'upload_protocol' option (smb / ftp, default smb for backwards compatibility). FTP reuses smb_server/username/password/base_path and patterns; smb_share is unused under FTP. All three sync paths are protocol-aware: event upload, daily retention cleanup (FTP uses MDTM for mtimes), and the disk-free check (skipped silently under FTP, no portable RPC). Implementation uses stdlib ftplib, no new manifest requirement. Empirical case for FTP backend: against a FRITZ!Box 7590 with ~3300 small event files, SMB rename via macOS-mounted share blocked 9+ minutes without a single completed move (uninterruptible disk wait, CPU 0%); FTP RNFR/RNTO against the same hardware completed all 3117 moves in 42 seconds, ~74 files/sec. Root cause is documented across Apple-Discussions threads on the Sequoia smbfs client plus AVM/PC-WELT articles on the FritzOS-CPU bottleneck for SMB metadata operations on USB storage. Settings cleanup. The smb_folder_pattern / smb_file_pattern descriptions now show {year} {month} {day} {camera} {date} {time} {type} {id} matching the actual format() call sites, in all three translation files. The alert- storage path was wrong (/media/bosch_alerts/) and now correctly says www/bosch_alerts/ (the actual on-disk location served at /local/bosch_alerts/). Default folder pattern bumped to {year}/{month}/{day}; existing custom values are untouched. German and English option screens are now 100% key-aligned (enable_go2rtc and debug_logging descriptions restored). The alert_notify_service description in English now matches the German one and the actual code (it is a fallback, not a fan-out). Helper. scripts/migrate_smb_to_day_folders.py for users with an existing flat layout. Default dry-run, parses day from filename or falls back to mtime. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent bff57ed commit e008f18

10 files changed

Lines changed: 564 additions & 110 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ versions see this file or the [GitHub Releases page](https://github.com/mosandlt
77

88
---
99

10+
## v10.5.0
11+
12+
**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.
13+
14+
---
15+
1016
## v10.4.10
1117

1218
**Three resilience fixes for stream stability + WAN-outage handling.** **(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 (Bosch silently rotated the per-session digest creds during the RTSP idle gap). After 3 consecutive `Error from stream worker: 401 Unauthorized` errors, AUTO fell back to REMOTE even though the LAN was perfectly reachable. **Reactive 401 rescue:** when `_handle_stream_worker_error` sees a 401 / "Unauthorized" / "authorization failed" message on a LOCAL session, issue one fresh `PUT /connection LOCAL` to obtain new creds before falling through to the REMOTE path. Gated by a per-camera `_local_rescue_attempts` counter (max 1 per failure burst) with a 5-minute time-decay so the counter doesn't stick at 1 after the first rescue: `record_stream_success` never fires when no HLS consumer is connected, so without time decay the next legitimate 401 burst (typically 8–14 min later) would skip straight to REMOTE. **Proactive cred refresh in heartbeat:** capture analysis (see `captures/api-findings.md` §1) showed the Bosch iOS app fires `PUT /connection LOCAL` at ~5 Hz during live view and consumes the fresh digest user/password from each response; the active RTSP connection is unaffected because Bosch only invalidates the rotated creds for *new* connects. Our heartbeat now mirrors this behaviour: each successful heartbeat parses the response, caches `user`/`password` into `_live_connections[cam_id]`, rebuilds the cached `rtspsUrl` with fresh creds, and calls `Stream.update_source()`. The running stream-worker is not disturbed (HA's `update_source` only changes the source for the next worker restart) — but when the worker eventually restarts after an idle gap, it picks up fresh creds and avoids the 401 in the first place. **(2) FCM noise filter for WAN outages.** Real-world finding 2026-04-28: when the home router rebooted, `firebase_messaging.fcmpushclient._listen` re-entered itself recursively on every retry, and each ERROR log line carried a ~3000-frame stack trace. With the 30 s reconnect cadence that produced ~200 log lines/s, ~12 500 lines/min, and the HA MainThread became wedged in stack-trace formatting and disk I/O — CPU rose from 30 % to 85 %, the bosch-shc-camera coordinator stopped firing entirely (no "Finished fetching" line for 4 min), and other integrations slowed too. New `_FCMNoiseFilter` (in `fcm.py`) attaches once to the `firebase_messaging.fcmpushclient` logger when FCM is set up: it strips `exc_info`/`exc_text` from "Unexpected exception during read" records (the recursive trace adds zero diagnostic value — we already know FCM disconnected) and rate-limits to one pass-through per 60 s. Reconnect behaviour is unchanged; the library still retries normally and recovers when WAN comes back, but the log volume drops from ~200 lines/s to ~1 line/min and the MainThread stays free. Library issue [sdb9696/firebase-messaging#33](https://github.com/sdb9696/firebase-messaging/issues/33) covers the abort-on-error angle but not the recursive trace itself, so a client-side filter is the right place. **(3) Same-camera stream-source race protection** (carried over from earlier work in this version): `try_live_connection: already in progress for X — skipping` is now the warning we see when two parallel start attempts collide; the first one always wins, the second exits cleanly without leaving a half-built TLS proxy or stale cache entry. **(4) Hardware-privacy auto-teardown.** 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 before this fix: 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). New code path: 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.** 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, and each one triggered a 10 s connect-timeout against the gone camera — burning CPU on a hopeless loop. After 5 consecutive connect failures within 30 s the proxy now closes its server socket; the coordinator (privacy-aware) decides whether to rebuild the session or stay torn-down. **(6) `does not support play stream service` log filter.** During the ~25 s LOCAL pre-warm window (PUT /connection → TLS proxy → encoder warm-up → rtspsUrl set) 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 (multiple Lovelace tabs + Companion app + the card's own HLS-fallback path all polling around the same time). 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 are not touched. **(7) Overview card `use_bosch_sort` option.** New per-card opt-in flag for `custom:bosch-camera-overview-card` (Card v2.10.12 / Overview v1.1.0): 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. YAML: `use_bosch_sort: true`. **(8) Card stale-state guard against accidental toggles** (Card v2.10.13). Diagnosed live 2026-04-28 14:00: a Live-Stream switch flipped to `off` from a system-admin user_id (iOS Companion App) with `parent_id: null` (= direct service call, not an automation) — but the user reported they didn't tap 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`: (a) `_toggleStream` is now `async` and pulls the authoritative state via `GET /api/states/<switch>` immediately before `callService` — if the freshly-fetched state disagrees with what the card was showing, the toggle is aborted, the optimistic state is cleared, and the view is re-rendered (the user has to tap again with the now-correct state); (b) `_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 returns to the foreground, so a backgrounded card resyncs immediately rather than waiting for the next WS push. Behaviour unchanged when the card was already in sync; the REST round-trip adds <100 ms before the existing optimistic flip in the common path.

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -981,8 +981,8 @@ Everything renders automatically when the integration detects a Gen2 Indoor II.
981981

982982
## Releases
983983

984-
Latest stable: **v10.4.10** — see the GitHub release page for full notes:
985-
[**v10.4.10 release notes →**](https://github.com/mosandlt/Bosch-Smart-Home-Camera-Tool-HomeAssistant/releases/tag/v10.4.10)
984+
Latest stable: **v10.5.0** — see the GitHub release page for full notes:
985+
[**v10.5.0 release notes →**](https://github.com/mosandlt/Bosch-Smart-Home-Camera-Tool-HomeAssistant/releases/tag/v10.5.0)
986986

987987
| | |
988988
|---|---|

custom_components/bosch_shc_camera/config_flow.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,16 @@ async def async_step_init(self, user_input=None):
541541
"enable_smb_upload",
542542
default=bool(opts.get("enable_smb_upload", False)),
543543
): bool,
544+
vol.Optional(
545+
"upload_protocol",
546+
default=str(opts.get("upload_protocol", "smb")),
547+
): SelectSelector(SelectSelectorConfig(
548+
options=[
549+
SelectOptionDict(value="smb", label="SMB / CIFS (Standard)"),
550+
SelectOptionDict(value="ftp", label="FTP (z.B. FRITZ.NAS — schneller bei vielen Files)"),
551+
],
552+
mode=SelectSelectorMode.DROPDOWN,
553+
)),
544554
vol.Optional(
545555
"smb_server",
546556
description={"suggested_value": opts.get("smb_server", "")},
@@ -563,7 +573,7 @@ async def async_step_init(self, user_input=None):
563573
): str,
564574
vol.Optional(
565575
"smb_folder_pattern",
566-
description={"suggested_value": opts.get("smb_folder_pattern", "{year}/{month}")},
576+
description={"suggested_value": opts.get("smb_folder_pattern", "{year}/{month}/{day}")},
567577
): str,
568578
vol.Optional(
569579
"smb_file_pattern",

custom_components/bosch_shc_camera/const.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,13 @@
5757
"audio_default_on": True,
5858
"enable_intercom": False,
5959
"enable_smb_upload": False,
60+
"upload_protocol": "smb",
6061
"smb_server": "",
6162
"smb_share": "",
6263
"smb_username": "",
6364
"smb_password": "",
6465
"smb_base_path": "Bosch-Kameras",
65-
"smb_folder_pattern": "{year}/{month}",
66+
"smb_folder_pattern": "{year}/{month}/{day}",
6667
"smb_file_pattern": "{camera}_{date}_{time}_{type}_{id}",
6768
"smb_retention_days": 180,
6869
"smb_disk_warn_mb": 5120,

custom_components/bosch_shc_camera/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,5 @@
1010
"issue_tracker": "https://github.com/mosandlt/Bosch-Smart-Home-Camera-Tool-HomeAssistant/issues",
1111
"loggers": ["bosch_shc_camera"],
1212
"requirements": ["requests>=2.28.0", "firebase-messaging>=0.4.0", "smbprotocol>=1.10.0"],
13-
"version": "10.4.10"
13+
"version": "10.5.0"
1414
}

0 commit comments

Comments
 (0)