npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

iobroker.bosch-smart-home-camera

v1.8.3

Published

ioBroker adapter for Bosch Smart Home Cameras (Eyes Outdoor/Indoor, 360°, Gen2 Indoor II/Outdoor II)

Readme

ioBroker.bosch-smart-home-camera

NPM version Downloads Number of Installations License GitHub Release GitHub Activity Test and Release Project Maintenance BuyMeCoffee Community Forum AI-Assisted

NPM

ioBroker adapter for Bosch Smart Home Cameras (Eyes Outdoor, 360 Indoor, Gen2 Eyes Indoor II + Outdoor II). The full core feature set is functional end-to-end and verified live against real hardware.

Supported models: Eyes Outdoor (Gen1), Eyes Outdoor II (Gen2), 360 Indoor (Gen1), Eyes Indoor II (Gen2) — model-specific timing and configuration is automatic.

No official API. This adapter uses the reverse-engineered Bosch Cloud API, discovered via mitmproxy traffic analysis of the official Bosch Smart Camera app.


Table of Contents


Integration Comparison

The Bosch Smart Home Camera reverse-engineered API is exposed via four sibling projects. Pick the one that fits your platform.

| Feature | Home Assistant Integration | Python CLI Tool | ioBroker Adapter | MCP Server | Frontend (NiceGUI) | Node-RED | |---|---|---|---|---|---|---| | Maturity | v15.0+ — HA Quality Scale Platinum | v10.12+ stable (Mini-NVR BETA) | v1.8+ stable · npm | v1.7+ stable · PyPI | v0.4.0 alpha · PyPI | v0.4.0 alpha · npm | | Platform | Home Assistant (HACS) | Standalone Python 3.10+ CLI | ioBroker (npm) | Python 3.10+ · pipx / uvx · stdio + streamable-HTTP for MCP clients (Claude Desktop, Claude Code, custom) | NiceGUI web app · Python 3.10+ | Node-RED palette · npm | | Login | OAuth2 PKCE (browser) | OAuth2 PKCE (browser) | OAuth2 PKCE (browser) | ◑ shares CLI bosch_config.json | ◑ shares CLI bosch_config.json | ◑ refresh-token from CLI | | Snapshots | ✅ Native Camera.image | ✅ snapshot command | ✅ File-store + base64 DP | ✅ bosch_camera_snapshot (LAN-only) | ✅ live + event fallback | ✅ snapshot node | | Live RTSP stream (LAN) | ✅ via HA Stream component | ✅ ffmpeg/RTSPS output | ✅ TLS proxy → local RTSP | ✅ bosch_camera_stream_url (LAN-only, no cloud relay) | ◑ internal (go2rtc) | ◑ stream-url node (URL only) | | WebRTC (sub-second latency) | ✅ via integrated go2rtc | ✅ (v10.6.0) live --webrtc | ❌ | ❌ | ✅ via go2rtc (else snapshot) | ❌ | | Dual-stream URL (main + sub) | ✅ sensor.bosch_<n>_stream_url + _sub (v12.4.0, opt-in per cam) | ✅ info shows both · live --sub (v10.5.0) | ✅ stream_url + stream_url_sub (v0.5.3 experimental) | ◑ bosch_camera_stream_url — main stream only | ❌ (sub-stream only) | ◑ URL only — no sub option | | External recorder (BlueIris, Frigate) | ✅ via go2rtc | ✅ stdout pipe | ✅ Digest-creds URL + LAN bind option | ✅ URL returned, hand off to ffmpeg / go2rtc downstream | ❌ | ◑ stream-url → wire downstream | | Privacy mode | ✅ switch entity | ✅ command | ✅ DP | ✅ bosch_camera_privacy_set (LAN-fallback via prefer_local) | ✅ toggle | ✅ privacy node | | Front spotlight (Gen1/Gen2) | ✅ light entity | ✅ command | ✅ DP | ✅ bosch_camera_light_set (LAN-fallback) | ❌ (Phase 2 stub) | ✅ bosch-camera-light node (v0.3.0-alpha) | | RGB wallwasher (Gen2 Outdoor II) | ✅ light w/ RGB | ◑ on/off only — no RGB | ✅ color + brightness DPs | ❌ (on/off only — RGB not exposed) | ❌ | ◑ on/off + intensity only — no RGB (v0.3.0-alpha) | | Panic-alarm siren | ✅ button entity (Gen2 Indoor II) | ✅ command (Gen2 Indoor II only) | ✅ DP | ✅ bosch_camera_siren_trigger (Gen2 Indoor II only) | ✅ trigger + duration (Gen2 Indoor II only) | ❌ | | Firmware update | ✅ Update-Entity + Repairs fix-flow, install button (v14.4.10) | ✅ status + install (v10.11.0) | ✅ firmware states + install trigger, write-lock guard (v1.8.0) | ✅ status + install tools (v1.7.0) | ◑ read-only status display, no install action | ✅ status + install nodes (v0.4.0-alpha) | | Image rotation 180° | ✅ switch | ❌ | ✅ DP | ❌ | ❌ | ❌ | | Motion / person / audio events | ✅ FCM push + polling fallback | ◑ watch command only (events cmd removed) | ✅ FCM push + polling fallback | ✅ bosch_camera_events (on-demand pull) | ◑ pull-only events table | ✅ event node (poll) | | Motion edge-trigger state | ✅ binary_sensor.motion | n/a | ✅ motion_active DP (v0.5.3) | n/a (request-response, no subscription) | ❌ | ❌ | | Auto-snapshot on motion | ✅ refreshes Camera entity | n/a | ✅ writes last_event_image base64 (v0.5.3) | n/a (no background loop) | ❌ | ❌ | | Synthetic motion trigger (external sensor) | ✅ service | n/a | ✅ DP | ❌ | ❌ | ❌ | | Motion zones / privacy masks | ✅ read + write | ✅ read + write | ✅ read + write (v1.8.0) | ✅ get / set / clear (v1.7.0) | ❌ (no visual editor yet) | ❌ | | Automation rules / schedules | ✅ read + write | ✅ read + write | ✅ full CRUD (v1.8.0) | ✅ list / add / edit / delete (v1.7.0) | ✅ full CRUD (list/add/edit/delete) | ❌ | | Lighting schedule | ✅ read (write via service, Gen1 Eyes Outdoor only) | ✅ read + write | ✅ read (Gen1-only, v1.2.0) | ✅ get / set (v1.7.0) | ✅ read + write (outdoor Eyes cameras) | ❌ | | Cloud clip download (history ~30 d) | ✅ via Media Browser | ❌ | ❌ (parked — no community request yet) | ❌ (intentionally not exposed — large payloads) | ❌ (use CLI) | ◑ clip_url in event payload | | Mini-NVR (local recording) | ✅ continuous + event-buffered, ring-buffer preroll (v11.2.0 BETA → v14.7.0 modes) | ◑ event-triggered segment muxing, no preroll ring (v10.7.0 BETA) | ❌ (delegates to external recorder via credential-free RTSP endpoint) | ❌ (no NVR concept) | ◑ continuous only, no event-buffered (v0.4.0-alpha) | ◑ continuous only via bosch-camera-nvr-record node (v0.4.0-alpha) | | SMB / NAS clip upload | ✅ | ✅ (v10.7.0 BETA) | ❌ | ❌ | ❌ | ❌ | | Camera sharing (friends) | ✅ services (share / invite / list) | ✅ command | ✅ share / invite / remove (Gen2 only, v1.8.0) | ✅ list / invite / share / unshare / remove (v1.7.0) | ✅ list/invite/remove/share/unshare | ❌ | | Pan / tilt (360° Gen1) | ✅ services | ✅ command | ✅ pan_position DP | ✅ bosch_camera_pan | ✅ slider wired to live API | ❌ | | Named pan presets (home / left / right / back-left / back-right) | ✅ opt-in select entity | ✅ pan --preset flag | ✅ pan_preset DP | ✅ bosch_camera_pan preset= | ❌ | ❌ | | Two-way audio / intercom | ❌ | ✅ command | ❌ | ◑ listen-only bosch_camera_intercom_open (v1.7.0) | ❌ | ❌ | | Webhook delivery on events | ✅ service + opt-in options | ✅ watch --webhook URL | ✅ via MQTT bridge | ❌ (request-response model) | ❌ | ❌ | | MQTT event bridge (motion / audio / person) | n/a (HA event bus native) | n/a (single-run) | ✅ admin-config | n/a | ❌ | ❌ | | Apple HomeKit (via HA Core bridge) | ✅ documented | n/a | n/a | n/a | n/a | n/a | | Snapshot scheduler / time-lapse | ✅ examples/ YAML | ✅ cron + ffmpeg examples | ✅ Blockly example | n/a | ❌ | ❌ | | Native dashboard card / widget | ✅ 2 Lovelace cards (single + grid) | n/a | ✅ 2 vis-2 widgets — BoschCamera + BoschOverview multi-cam | n/a | ✅ (is itself a web dashboard) | ❌ | | Picture-in-Picture survives backgrounded tab | ✅ hass-suspend-when-hidden keep-alive (v14.0.0) | n/a (no UI) | ✅ own PiP + freeze-recovery, Web-Worker heartbeat (v1.7.2/v1.7.3) | n/a (no UI) | ✅ reconnect-timeout + freeze-recovery (v0.4.0-alpha) | n/a (no UI) | | Cloud-relay REMOTE fallback | ✅ auto-switch when LAN unreachable | ✅ remote mode | ❌ (LOCAL-only by design) | ❌ (media LAN-only; status/events via cloud) | ◑ inherits CLI | ◑ REMOTE opt (manual) | | Browser-based admin / config UI | ✅ HA Config Flow | n/a (CLI) | ✅ JSON-config tabs | n/a (LLM-mediated; config via CLI / MCP client) | ✅ Settings page | ◑ editor config node | | UI languages | EN · DE · FR · ES · IT · NL · PL · PT · RU · UK · ZH-Hans (v12.4.0) | EN · DE · FR · ES · IT · NL · PL · PT · RU · UK · ZH-Hans (v10.3.0) | EN · DE · FR · ES · IT · NL · PL · PT · RU · UK · ZH-CN | n/a (no UI — LLM is the front-end) | ◑ backend i18n · UI mostly EN | n/a (English only) |

Legend: ✅ supported · ❌ not supported / not planned · n/a not applicable for this platform.

All four projects share the same reverse-engineered Cloud API + RCP protocol research, but evolve independently. The Home Assistant integration is the most feature-complete reference implementation; the Python CLI is the lowest-level / scriptable surface; the ioBroker adapter targets VIS dashboards and Blockly automations; the MCP server exposes a curated, LAN-first tool surface to MCP clients (Claude Desktop, Claude Code, custom) for natural-language camera control.


Supported Cameras

All four current Bosch Smart Home cameras are supported.

| Camera | Generation | Type | Codec / FW seen | Highlights | |---|---|---|---|---| | 360° Indoor | Gen1 | Indoor | H.264 + AAC · FW 7.91.x | Pan/tilt motor, autofollow, IR night vision, mechanical privacy shutter | | Eyes Indoor II | Gen2 | Indoor | H.264 + AAC · FW 9.40.x | Built-in 75 dB siren, Audio+ glass-break / smoke / CO, ZONES detection mode, RGB LEDs, retractable head (Privacy hardware button) | | Eyes Outdoor | Gen1 | Outdoor (IP66) | H.264 + AAC · FW 7.91.x | Front spotlight, motion-triggered light, ambient-light sensor, schedule-driven illumination | | Eyes Outdoor II | Gen2 | Outdoor (IP66) | H.264 + AAC · FW 9.40.x | Front + Top + Bottom RGB LED groups, DualRadar (motion + intrusion), wallwasher mode, mounting-elevation parameter |


Disclaimer

This project is an independent, community-developed adapter. It is not affiliated with, endorsed by, or connected to Robert Bosch GmbH. "Bosch" and "Bosch Smart Home" are registered trademarks of Robert Bosch GmbH.

This adapter communicates with a reverse-engineered, undocumented API. Provided "as is", without warranty. Use at your own risk. The API may change or be shut down by Bosch at any time. Reverse engineering was performed solely for interoperability under § 69e of the German Copyright Act (UrhG) and EU Directive 2009/24/EC.


Setup

The Bosch auth code in the redirect URL expires in ~60 seconds. Open the adapter Admin dialog in one tab BEFORE you click the login button, so you can paste the URL back the moment Bosch redirects you. If the code expires, just click "Open Bosch Login in browser" again — a fresh URL is generated each time.

  1. Install the adapter and create an instance (the adapter starts in "waiting for login" mode).
  2. Open the adapter's Admin dialog (Instances → bosch-smart-home-camera → wrench icon). The Connection tab shows the login flow inline; keep this dialog open in one tab.
  3. Click "Open Bosch Login in browser" — Bosch SingleKey ID opens in a new tab. Log in (captcha / MFA if prompted).
  4. Bosch redirects your browser to https://www.bosch.com/boschcam?code=…&state=…. The page may show blank or a 404 — that is expected. Immediately copy the full URL from the address bar.
  5. Switch back to the adapter Admin tab and paste the URL into "Pasted callback URL", then Save. Do this within ~60 seconds of the redirect, otherwise the auth code expires and you have to start over.
  6. The adapter restarts, exchanges the auth code for tokens, fetches your cameras, and starts the FCM listener. Future restarts skip the browser step as long as the stored refresh token is still valid.

Fallback if the Admin button is not available (very old ioBroker Admin versions): the adapter also publishes the login URL as a state object — open Objects → bosch-smart-home-camera.0 → info → login_url and click the value to launch the login. The redirect-paste step is the same.

If the auth code expires (you'll see code expired in the log after pasting), don't panic — just click "Open Bosch Login in browser" again. The adapter generates a fresh URL each time you press the button.

If the refresh token is ever rejected (after a Bosch password change or extended downtime), the adapter logs a new login URL and you repeat steps 3–5.

OAuth2 PKCE login flow

sequenceDiagram
    autonumber
    participant U as User (browser)
    participant A as ioBroker Adapter
    participant B as Bosch SingleKey ID

    A->>A: generate PKCE verifier + state
    A->>U: log login URL (challenge + state)
    U->>B: open URL, enter credentials, solve captcha/MFA
    B->>U: redirect to bosch.com/boschcam?code=...&state=...
    U->>A: paste callback URL into Admin UI
    A->>B: POST /token (code + PKCE verifier)
    B-->>A: access_token + refresh_token
    A->>A: AES-encrypt + persist (info.access_token / info.refresh_token / info.fcm_creds)
    A->>B: GET /v11/video_inputs (with bearer)
    B-->>A: camera list
    A->>A: build per-camera DP tree, start FCM listener

Architecture

flowchart LR
    Cam["Bosch Cameras<br/>Gen1 + Gen2<br/>HTTPS :443"] -->|TLS tunnel| Proxy["Adapter TLS proxy<br/>127.0.0.1:&lt;port&gt;"]
    Cloud[Bosch CBS API<br/>residential.cbs<br/>.boschsecurity.com] -->|"FCM push<br/>OAuth2 PKCE<br/>REST: events / video_inputs<br/>/ lighting / privacy"| Adapter
    Adapter[ioBroker Adapter<br/>Node.js] --> DPs[(Encrypted DPs<br/>encryptedNative)]
    Proxy --> Adapter
    DPs --> VIS[VIS / VIS-2<br/>Dashboard]
    DPs --> Blockly[Blockly / JS<br/>automations]
    Proxy -.->|rtsp://...<br/>LAN-bind opt| Recorders[BlueIris / Frigate<br/>iobroker.cameras]
    Adapter -->|file-store| Snapshots[snapshot.jpg<br/>last_event_image]

Network Connectivity

The ioBroker host must be able to reach each camera's IP on the LAN. The Bosch cloud auto-discovers the camera IP, but stream/snapshot/RCP traffic flows directly from the ioBroker host to the camera. If a firewall, VLAN boundary, or guest network blocks that path, the live MJPEG/RTSP stream and on-demand snapshot fall back to a (slower) cloud path.

Required ports

| Direction | Protocol / Port | Purpose | Required | |---|---|---|---| | ioBroker host → camera IP | TCP/443 | Snapshots, camera REST API, RTSPS live stream (everything tunnels through one TLS connection) | Yes | | ioBroker host → *.boschsecurity.com | TCP/443 | OAuth, REMOTE/cloud fallback stream, FCM push registration | Yes | | ioBroker host → fcm.googleapis.com / mtalk.google.com | TCP/5228 | FCM push notifications (auto-falls-back to polling) | Optional | | External recorder (BlueIris/Frigate/iobroker.cameras) → ioBroker host | TCP/stream_port | Local RTSP relay exposed by the adapter | Only when used |

The adapter itself only needs TCP/443 from the ioBroker host to the camera. No UDP, no inbound port-forward on your router.

Common pitfalls

  • Camera in a different subnet/VLAN than ioBroker — e.g. cameras on 192.168.168.x and ioBroker on 192.168.1.x. The router/firewall must allow the ioBroker host's IP outbound to the camera's IP on TCP/443.
  • IoT/guest network isolation (FRITZ!Box "Gastzugang", Unifi guest network) blocks LAN-to-LAN by default.
  • Camera reachable from the Bosch app but not from ioBroker — the app talks via the cloud, so this proves nothing about LAN reachability.
  • External recorder cannot reach the relay — the adapter binds the RTSP relay on the ioBroker host. Make sure your recorder VM/container can reach stream_host:stream_port on the LAN.

Quick check from the ioBroker host

nc -vz 192.168.x.y 443
curl -k -v --connect-timeout 5 https://192.168.x.y/   # alternative

If both time out or return "connection refused", the issue is between ioBroker and the camera (network/firewall), not the adapter.

Maintenance RSS flow

sequenceDiagram
    participant Tick as Adapter tick (hourly)
    participant RSS as Bosch Community RSS
    participant MP as maintenance parser
    participant DP as info.maintenance.*

    Tick->>RSS: fetch Wartungsarbeiten + Statusmeldungen
    RSS-->>MP: raw feed entries (+ HTML fallback)
    MP-->>MP: parse title / time window / camera_relevant
    MP-->>DP: state: active / scheduled / past / recent / idle
    DP-->>DP: last_notification written on state transition
    Note over Tick,DP: reactive re-fetch (5 min cooldown) on any 5xx

LAN-fallback during cloud outage

sequenceDiagram
    participant DP as cameras.<id>.privacy_enabled<br/>cameras.<id>.front_light_enabled
    participant Adapter
    participant Cloud as Bosch CBS API
    participant RCP as Camera LAN RCP
    participant Cam as Camera LAN :443

    DP->>Adapter: handlePrivacyToggle / _applyLightingState
    Adapter->>Cloud: PUT /v11/.../privacy or lighting
    Cloud-->>Adapter: 5xx (cloud outage)
    Adapter->>Adapter: lan_ip known + Gen2?
    Adapter->>RCP: RCP write (0x0d00 / 0x0c22)
    RCP->>Cam: HTTPS Digest + payload
    Cam-->>Adapter: 200 OK (local fallback)
    Adapter-->>DP: update + _localWriteAt stamp

Status

Stable (v1.8.2) — verified live against 4 cameras (Gen1 + Gen2, FW 7.91.56 / 9.40.102) on a real ioBroker instance. Cloud API contracts confirmed against the iOS app via mitmproxy.

What works:

  • Browser-based OAuth2 PKCE login via Bosch SingleKey ID (no programmatic password handling — captcha/MFA happen in the browser)
  • Token auto-refresh (~45 min cadence; 4xx → re-login required, 5xx → silent retry). Stored refresh_token also used at startup to mint a fresh access_token silently — no PKCE re-login required after restart, even if the adapter was stopped longer than the 1 h access-token lifetime.
  • Camera discovery (Gen1 + Gen2, GET /v11/video_inputs)
  • Per-camera state tree: name, firmware_version, hardware_version, generation, online, privacy_enabled, light_enabled, front_light_enabled, wallwasher_enabled, image_rotation_180, snapshot_trigger, motion_trigger, motion_trigger_event_type, snapshot_path, stream_url, stream_host, stream_port, stream_path, last_motion_at, last_motion_event_type — plus daily event counters, motion-zone/privacy-mask/rule read+write, firmware status + install, and Gen2 friend-sharing datapoints (v1.2.0–v1.8.0, full list in Datapoints below)
  • Privacy toggle via Bosch Cloud API PUT /v11/video_inputs/{id}/privacy
  • Light toggle, Gen-specific and now split into independent datapoints:
    • Gen2: PUT /lighting/switch/front + /topdown
    • Gen1: PUT /lighting_override (frontLightOn + wallwasherOn)
    • front_light_enabled and wallwasher_enabled can be toggled independently; light_enabled remains as a legacy combined switch
  • Synthetic motion trigger (motion_trigger write-only button + motion_trigger_event_type selector) for external sensor integration without waiting for Bosch FCM push
  • Snapshot trigger writes JPEG into the adapter file-store (/<namespace>/cameras/<id>/snapshot.jpg), with automatic retry on the first "stream has been aborted" hiccup that Bosch Gen2 firmware emits after idle. One startup snapshot per camera flips cameras.<id>.online from the default false to the real state immediately.
  • Per-camera TLS proxy: stream_url = rtsp://127.0.0.1:<port>/rtsp_tunnel for use in iobroker.cameras or go2rtc. LOCAL-only by design — no cloud relay.
  • RTSP session watchdog: LOCAL sessions renew automatically ~60 s before maxSessionDuration expires — 24/7 recording works without hourly stream drops
  • FCM push listener (@aracna/[email protected] MTalk/MCS) for sub-second motion / audio-alarm / person events. info.fcm_active reflects state: healthy / polling / error / disconnected / stopped. When push registration fails the adapter falls back to /v11/events polling every 60 s (info.fcm_active=polling) — events still arrive, just with higher latency. The polling interval is configurable via the poll_interval setting (API requests / Power saving tab, v1.4.1+).
  • Encrypted credential storage (encryptedNative — js-controller encrypts the refresh token at rest)
  • Cloud-API management-tier WRITE (v1.8.0): motion zones, privacy masks, automation rules (create/update/delete), Gen2 camera sharing (share/invite/remove friend), and a firmware-install trigger — same /v11 endpoints as the Home Assistant integration and Python CLI, byte-verified against both. The on-device RCP zone/mask editor stays parked pending broader local write access from Bosch.
  • Pooled keep-alive HTTPS connections (v1.8.1) for local Digest and cloud API requests, instead of a fresh TCP+TLS handshake per call
  • 1480+ unit tests passing

Datapoints

Per-camera datapoints under cameras.<id>.*:

| Datapoint | Type | Description | |---|---|---| | name | string | Camera name (from Bosch account) | | firmware_version | string | Current firmware version | | hardware_version | string | Hardware model string | | generation | string | Gen1 or Gen2 | | online | boolean | Camera reachable | | privacy_enabled | boolean | Privacy mode on/off | | front_light_enabled | boolean | Front spotlight on/off | | wallwasher_enabled | boolean | RGB wallwasher on/off (Gen2 outdoor) | | wallwasher_color | string | HEX #RRGGBB, empty = warm white mode | | wallwasher_brightness | number | 0–100 | | image_rotation_180 | boolean | 180° image flip | | livestream_enabled | boolean | Opt-in RTSP livestream switch | | stream_url | string | rtsp://user:pwd@host:port/rtsp_tunnel?inst=1&… | | stream_url_sub | string | Sub-stream URL (inst=2, experimental) | | stream_host | string | Host part of stream_url — paste into iobroker.cameras "Camera IP" | | stream_port | number | Port part of stream_url — paste into iobroker.cameras "Port" | | stream_path | string | Path+query of stream_url — paste into iobroker.cameras "Path" (Protocol = TCP) | | snapshot_trigger | button | Fetch fresh JPEG to snapshot_path | | snapshot_path | string | File-store path for last JPEG | | last_event_image | string | Base64 data:image/jpeg;base64,… (auto-snapshot on motion) | | last_event_image_at | string | ISO 8601 timestamp of last event image | | motion_trigger | boolean | Write true to inject a synthetic motion event | | motion_trigger_event_type | string | motion / person / audio_alarm | | motion_active | boolean | Edge-trigger: true for 90 s after motion, then false | | last_motion_at | string | ISO 8601 timestamp of last motion event | | last_motion_event_type | string | motion / person / audio_alarm | | pan_position | number | Pan angle ±120° (360° Gen1 only) | | pan_preset | string | Named preset: home, left, right, back-left, back-right | | siren_active | boolean | Trigger 75 dB siren (Gen2 Indoor II) | | lan_reachable | boolean | TCP-ping result against camera LAN IP | | lan_ip | string | Camera LAN IP (persisted on each session open) | | maintenance_state | string | active / scheduled / none | | intrusion_sensitivity | number | DualRadar sensitivity 1–5 (Gen2) | | intrusion_distance | number | DualRadar detection distance 1–8 m (Gen2) | | wifi_signal_pct | number | WiFi signal strength 0–100 % | | mic_level | number | Microphone recording level 0–100 | | speaker_level | number | Intercom speaker volume 0–100 | | last_status_notification | string | JSON: camera online/offline transition payload | | _proxy_port | number | Sticky TLS proxy port (persisted across restarts) | | events_today | number | Total cloud events today (UTC day) | | movement_count | number | Movement events today (UTC day) | | audio_count | number | Audio-alarm events today (UTC day) | | motion_enabled | boolean | Motion detection on/off | | motion_sensitivity | string | Motion sensitivity select | | detection_mode | string | Gen2: all_motions / only_humans / zones | | record_sound | boolean | Record audio with video | | notifications_enabled | boolean | Master push-notification switch | | notify_movement / notify_person / notify_audio / notify_trouble / notify_camera_alarm / notify_trouble_email | boolean | Per-event-type push-notification toggle | | motion_zones | string (JSON) | Motion-sensitive zones, raw {x,y,w,h} array (read-only mirror) | | motion_zones_count | number | Number of configured motion zones | | motion_zones_set | string (JSON, write) | v1.8.0 — write a JSON array of {x,y,w,h} (0.0–1.0) to replace all zones; [] clears them | | privacy_masks | string (JSON) | Privacy masks, raw {x,y,w,h} array (read-only mirror) | | privacy_masks_count | number | Number of configured privacy masks | | privacy_masks_set | string (JSON, write) | v1.8.0 — write a JSON array of {x,y,w,h} (0.0–1.0) to replace all masks; [] clears them | | rules | string (JSON) | Automation rules, raw array of {id,name,isActive,startTime,endTime,weekdays} | | rules_count | number | Number of configured automation rules | | rule_create | string (JSON, write) | v1.8.0 — write {name,isActive,startTime:"HH:MM:SS",endTime:"HH:MM:SS",weekdays:[0-6]} to create a rule | | rule_update | string (JSON, write) | v1.8.0 — write {id,...changed fields} (GET-merge-PUT) | | rule_delete | string (write) | v1.8.0 — write the rule id to delete | | lighting_schedule_status | string | Gen1 only — floodlight scheduleStatus mode | | lighting_schedule | string (JSON) | Gen1 only — raw lighting_options schedule | | ambient_light_schedule | string (JSON) | Gen2 outdoor — ambient-light schedule (/lighting/ambient) | | ambient_light_enabled | boolean | Gen2 outdoor — ambient-light-driven illumination | | motion_light_enabled / motion_light_sensitivity | boolean / number | Gen2 outdoor — motion-triggered light | | darkness_threshold | number | Ambient-light darkness threshold | | shared_with_friends | string (JSON) | Gen2 only — friends this camera is shared with (raw array) | | shared_with_friends_count | number | Gen2 only — count of the above | | camera_share | string (JSON, write) | v1.8.0, Gen2 only — write {"friendId":"...","days":30} (days optional = indefinite) to share this camera | | friend_invite | string (write) | v1.8.0, Gen2 only — invite a friend by e-mail (account-wide, not camera-specific) | | friend_remove | string (write) | v1.8.0, Gen2 only — remove a friend by id (account-wide) | | firmware_current_version | string | Installed camera firmware version (cloud-reported) | | firmware_latest_version | string | Latest available camera firmware version (cloud-reported) | | firmware_update_available | boolean | Firmware update available | | firmware_updating | boolean | Firmware install currently in progress | | firmware_install | button | v1.8.0 — write true to install the pending firmware update (guarded against a double-press or an already-in-progress install) | | commissioned | boolean | GET /commissioned — camera configured + connected + commissioned | | unread_events_count | number | Unread cloud events (from GET /v11/events) | | mark_all_read | button | Marks all cloud events as read | | autofollow_enabled | boolean | 360° Gen1 only — motion autofollow | | alarm_arm / alarm_mode / pre_alarm | boolean | Gen2 Indoor II alarm system controls | | alarm_state | string | Gen2 Indoor II alarm system status | | pre_alarm_delay / alarm_activation_delay | number | Gen2 Indoor II alarm timing | | siren_duration | number | Panic-siren trigger duration | | glass_break_detection / fire_alarm_detection | boolean | Gen2 Audio+ — glass-break / smoke-fire-alarm sound detection | | privacy_sound_enabled | boolean | Audible privacy-mode chime | | status_led | boolean | Gen2 — status LED on/off | | timestamp_overlay | boolean | On-screen timestamp overlay | | power_led_brightness | number | Gen2 Indoor — power LED brightness | | front_light_intensity | number | Front-spotlight brightness | | intercom_enabled | boolean | Gen2 — two-way audio toggle | | microphone_level | number | Microphone recording level 0–100 (alias of mic_level) | | onvif_scopes | string (JSON) | ONVIF scopes diagnostic (cloud-reported) | | rcp_version | string | RCP protocol version diagnostic | | stream_quality | string | Stream quality select (high/low) | | snapshot_url | string | Local HTTP snapshot-server URL (snapshot_http_port, see Development) | | session_limit_hit | boolean | Shared 3-session Bosch quota was hit for this camera | | wifi_ssid | string | Camera WiFi SSID | | lens_elevation | number | Gen2 Outdoor II — mounting-elevation parameter |

Adapter-wide datapoints under info.*:

| Datapoint | Description | |---|---| | connection | Boolean — at least one camera connected | | connection_status | logged_out / awaiting_login / connected / auth_error | | login_url | Bosch OAuth URL (clickable link in Admin UI) | | last_login_at | ISO 8601 of last successful token mint | | fcm_active | healthy / polling / error / disconnected / stopped | | maintenance.state | active / scheduled / past / recent / unknown / idle | | maintenance.title | Parsed announcement title | | maintenance.scheduled_start | ISO 8601 start | | maintenance.scheduled_end | ISO 8601 end | | maintenance.camera_relevant | Boolean — announcement mentions cameras | | maintenance.last_fetched | ISO 8601 of last successful RSS fetch | | maintenance.last_notification | JSON payload for Blockly notification routing |


Dashboard

A ready-to-import VIS-2 example dashboard is in docs/vis-2-example/ — all four cameras in a 2×2 grid with snapshot refresh (every 5 s), privacy + light toggles, snapshot trigger button, and a status bar.

Quick install:

cp docs/vis-2-example/vis-views.json ~/iobroker-data/files/vis-2.0/main/
iobroker restart vis-2

Then open http://HOST:8082/vis-2/index.html#Cameras in your browser.

See docs/vis-2-example/README.md for the walkthrough, including how to swap the camera UUIDs and how to wire go2rtc / HLS for low-latency live video instead of the default snapshot refresh.

VIS-2 Camera widget

The adapter ships two built-in VIS-2 widgets (React / Module-Federation, built from src-widgets/) that can be dropped onto any VIS-2 view without importing a JSON file.

Requirements: VIS-2 adapter ≥ 2.13 installed and running.


Bosch Camera (single camera)

How to use:

  1. Open the VIS-2 editor (http://HOST:8082/vis-2/index.html?edit=1).
  2. In the widget panel, find the Bosch Smart Home Camera widget set.
  3. Drag Bosch Camera onto your view.
  4. Set Camera datapoint to any DP under bosch-smart-home-camera.0.cameras.<UUID> (e.g. .name) — the camera is detected automatically from the path.
  5. Pick a Stream mode (see below).

Stream modes:

| Mode | What it shows | Needs | Audio | |---|---|---|---| | Snapshot (near-live) (default) | <img> polled from the adapter's snapshot HTTP server (snapshot_url) at a configurable interval (default 1 s) | snapshot_http_port set in the adapter | no | | MJPEG (frames) | continuous JPEG frames from the local RTSP proxy via FFmpeg, drawn on a canvas; play button starts the stream | livestream_enabled = true + ffmpeg on the host | no | | go2rtc WebRTC | low-latency live video via a native <video> element + WebRTC; automatic HLS fallback if ICE fails | go2rtc running with the camera's stream_url as source | yes — audio toggle + volume slider + pause-guard |

Feature set:

  • Status badges: Online/Offline, Motion, Privacy, Connecting (pulsing), stream uptime, HLS-fallback banner, last event, maintenance banner
  • Privacy state and Offline clearly separated; cloud-reconciled online status
  • Tap-to-play gate (no auto-start); stream and privacy cooldown guards
  • Optimistic UI: toggle buttons flip instantly, auto-revert on error
  • Digital zoom (pinch/wheel) in fullscreen
  • Page Visibility API: snapshot rate throttled in the background
  • Motion-zone and privacy-mask SVG overlays
  • Pan buttons ◀◀ ◀ ▶ ▶▶ with a position readout; only for the Gen1 360° indoor (hardware_version==="INDOOR")
  • Frosted-glass control bar (iOS/Android/auto theme; normal/minimal/compact layout)
  • Fullscreen via a React portal (covers the whole screen)
  • Picture-in-Picture (WebRTC mode): pop the live stream into the browser's floating, always-on-top window — over all apps on macOS Safari, over the browser on Chrome. The window title shows the camera name. Only one PiP window is allowed by the browser, so while one camera floats the PiP button on every other BoschCamera widget on the view greys out; it survives a stream reconnect. Hidden where the browser lacks PiP (most iOS/Android WebViews, snapshot/MJPEG modes).
  • Collapsible bottom-sheet accordions for all advanced settings: notifications, advanced, Gen2 automation/security, light & camera (incl. front_light_intensity slider), diagnostics, zones, services
  • 11 UI languages (de/en/es/fr/it/nl/pl/pt/ru/uk/zh-cn)

Live subtitles & translation (browser feature, no setup): if your camera has audio, Chrome → Settings → Accessibility → Live Caption transcribes spoken audio in the WebRTC stream on-device in real time, and Live Translate can translate the captions into your language. No widget changes needed — it captions whatever audio plays in the tab (works alongside Picture-in-Picture).

Building the widget (for contributors): npm run build:widget installs src-widgets/, runs the Vite/Module-Federation build and copies the bundle into widgets/bosch-smart-home-camera/.


Bosch Camera Overview (multi-camera grid)

The Bosch Camera Overview widget shows every camera of an adapter instance in a responsive grid — no manual per-camera configuration.

Configuration fields:

| Field | Description | |---|---| | Adapter instance | e.g. bosch-smart-home-camera.0 | | Columns | Fixed column count (0 = automatic) | | Min. tile width | Minimum width per tile in pixels | | Hide offline | Do not show offline cameras | | Per-tile controls | Privacy and light toggle directly on each tile |

Feature set:

  • Auto-discovery of all cameras of the selected instance
  • Sorting: online first, then privacy, then offline
  • Click-to-expand: clicking a tile opens the camera as a BoschCamera widget in the fullscreen portal
  • Same status badges as BoschCamera (Online/Offline/Motion/Privacy)

Example Automations

A growing library of 20 ready-to-import scripts lives in docs/examples/ — 8 Blockly XML files for the visual editor and 12 plain JavaScript snippets side-by-side. Themes covered:

  • Master switches — one virtual datapoint flips wallwasher / privacy on every camera in lock-step.
  • Motion handling — snapshot-on-motion with notification, Hue-PIR → synthetic Bosch motion bridge, burst-aggregation notify, presence-driven privacy.
  • Light scenes — dusk-driven auto-wallwasher, full driveway scene (Hue floodlight + Bosch wallwasher/frontlight), vacation deterrent, door-sensor light.
  • Bot / dashboard integration — Telegram /snap command, last-event slideshow for VIS, stream-URL push to a Fully Kiosk tablet.
  • Status & safety — camera-offline alert, FCM-push degradation monitor, panic-siren trigger, weather-suppressed alerts, sleep-mode mute, garage-door coordination, night-mode schedule.
  • Snapshot scheduler / time-lapsedocs/examples/snapshot-blockly.md: hourly Blockly XML + JavaScript scheduler (06:00–22:00 cron), plus a motion-triggered variant with 15-minute throttle. Writes snapshot_trigger, reads back snapshot_path. Includes an ffmpeg one-liner to assemble the collected JPEGs into an mp4 time-lapse.

Open javascript adapter → Scripts → new Blockly (or JavaScript) → paste. Replace the <CAM_UUID> / <PRESENCE_OID> / lux-sensor / Telegram-bot placeholders with your actual object IDs from the Objects tab. The folder's README has the full index, prerequisites, and notification-adapter call patterns (Telegram, signal-cmb, Pushover, email).

Contribute your own: drop a working script as a code block in the ioBroker forum thread or open a PR — community examples are explicitly welcome.

Motion / event flow (camera → DP → automation)

flowchart TD
    Cam[Camera detects motion<br/>or audio alarm] -->|FCM push<br/>MTalk/MCS| L[Adapter FCM listener<br/>_aracna/fcm_]
    L -->|persistent_id dedup| F{fetchEvents<br/>GET /v11/events}
    F -->|new event| N["normalize event:<br/>id, type, tags, timestamp"]
    N --> C{eventTags / type}
    C -->|MOVEMENT + PERSON| EP[last_event_type=person<br/>motion_active=true 90s]
    C -->|MOVEMENT| EM[last_event_type=movement<br/>motion_active=true 90s]
    C -->|AUDIO_ALARM| EA[last_event_type=audio_alarm]
    EP & EM & EA --> S[fetch snapshot<br/>→ snapshot.jpg<br/>→ last_event_image base64]
    EP & EM & EA --> T[Blockly / JS trigger]
    T -->|sendTo notify| Out[Telegram / Signal /<br/>Pushover / e-mail]
    L -.->|register fail<br/>3x| Poll[Polling fallback<br/>every 30s<br/>info.fcm_active=polling]

Note on live streaming in the browser: no browser supports RTSP natively. The adapter publishes a per-camera stream_url (rtsp://<user>:<password>@127.0.0.1:<port>/rtsp_tunnel?…) via a local TLS proxy for use with ffmpeg / mpv / iobroker.cameras / go2rtc. For VIS itself, either use the snapshot refresh in the example dashboard or bridge via go2rtc → WebRTC/HLS.

stream_url is empty / go2rtc says "connection refused"

The livestream is opt-in and OFF by default — each open session counts against the Bosch LOCAL session quota and keeps a TLS proxy + watchdog running 24/7, so the adapter never starts it on its own. While it is off, the per-camera proxy does not listen, cameras.<id>.stream_url stays empty, and any go2rtc / recorder pointed at the port gets connection refused. Snapshots and motion events work without it.

  1. Set cameras.<id>.livestream_enabled = true (per camera). The session opens, the proxy starts listening, and cameras.<id>.stream_url is populated.
  2. Copy that URL into go2rtc / iobroker.cameras / your recorder.
  3. If go2rtc (or the recorder) runs on a different host than ioBroker, the default 127.0.0.1 bind is unreachable from there → still connection refused. Enable Expose RTSP proxy to LAN in the adapter settings and set the External hostname / LAN IP (see the LAN-recorder steps below); the URL then uses your ioBroker host's LAN IP instead of 127.0.0.1.

MQTT Bridge

When enabled, the adapter publishes every motion / person / audio_alarm event as a JSON message to an MQTT broker of your choice — making Bosch camera events available to any MQTT consumer without ioBroker-specific bindings.

Admin UI → "MQTT Bridge" tab:

| Field | Default | Description | |---|---|---| | Enable MQTT Bridge | false | Master switch | | Broker host / IP | — | Hostname or IP, e.g. 192.168.1.10 | | Broker port | 1883 | 1–65535 | | Use TLS (mqtts://) | false | Encrypted connection | | Username | — | Optional | | Password | — | Optional, stored encrypted | | Topic prefix | bosch/cameras | All topics live under this prefix |

Topic layout:

<prefix>/<cam-uuid>/motion      motion or unclassified movement
<prefix>/<cam-uuid>/person      person detected
<prefix>/<cam-uuid>/audio       audio_alarm event

Payload (JSON):

{
  "timestamp": "2026-05-20T10:00:00.000Z",
  "cam_name":  "Front Door",
  "event_id":  "evt-uuid-or-empty",
  "event_type": "motion"
}

Compatible consumers: Node-RED, openHAB, Home Assistant (MQTT integration), Frigate, Zigbee2MQTT sidecars, any standard MQTT subscriber.

Example Node-RED subscription:

bosch/cameras/<camera-id>/motion

Wire it to a Telegram notification, a Frigate alert, or a Home Assistant mqtt.sensor — no ioBroker adapter required on the subscriber side.


Credential-Free RTSP Front-Door

Bosch cameras only speak RTSPS (RTSP tunneled inside TLS) with a private Bosch CA certificate that most NVR software (BlueIris, Frigate, VLC, go2rtc) can't handle directly, and every stream requires a Bosch Digest auth handshake with credentials that Bosch rotates on every session renewal and every privacy-mode toggle. This adapter is the original implementation of a local RTSP relay that solves both problems — it later shipped as a credential-free RTSP endpoint in the sibling Home Assistant integration (v14.1.0), which explicitly credits this adapter as the design source.

The relay is three layers, all LOCAL-only (this adapter never uses Bosch's cloud media relay — no proxy-NN.live.cbs.boschsecurity.com, straight to the camera's LAN IP on TCP/443):

  1. Bosch LOCAL session (src/lib/live_session.ts) — PUT /v11/video_inputs/{id}/connection {type:"LOCAL"} opens a session and returns the camera's LAN address plus a Digest user/password pair, valid until maxSessionDuration (default 3600 s) or an early rotation.
  2. TLS proxy (src/lib/tls_proxy.ts) — a plain TCP listener (net.createServer) that opens a tls.connect() to the camera's LAN address per inbound client and pipes bytes through, presenting a clean rtsp:// endpoint to the consumer. A circuit breaker closes the listener after 5 consecutive connect failures inside 30 s so a truly unreachable camera doesn't spin forever.
  3. Transparent Digest injection (src/lib/rtsp_auth.ts) — a small RTSP state machine sitting in front of the TLS pipe. A client that already sends its own Authorization: header (legacy in-URL-credentials clients) is passed straight through. Everything else gets the Digest handshake done for it: the proxy catches the camera's 401 + WWW-Authenticate, computes the response, and splices a fresh Authorization: Digest … header into every subsequent client request — the recorder never sees a username or password. When Bosch rotates the session's Digest creds (privacy toggle, renewal), updateDigestAuth() hot-swaps them for future connections without restarting the listener or changing the published URL.
sequenceDiagram
    autonumber
    participant Rec as Recorder<br/>(BlueIris / Frigate / go2rtc / VLC)
    participant FD as Lazy front-door<br/>always-listening, sticky port
    participant Proxy as Inner TLS proxy<br/>(tls_proxy.ts)
    participant Auth as Digest injector<br/>(rtsp_auth.ts)
    participant Bosch as Bosch Cloud API
    participant Cam as Camera LAN :443<br/>(RTSPS)

    Rec->>FD: TCP connect rtsp://host:<port>/rtsp_tunnel
    Note over FD: port is always bound —<br/>no "Connection refused" even if idle
    FD->>FD: resolveInner() — any live client already?
    alt no active Bosch session
        FD->>Bosch: PUT /connection {type:"LOCAL"}
        Bosch-->>FD: lanAddress + digestUser/digestPassword
        FD->>Proxy: startTlsProxy(remote=lanAddress, digestAuth)
    end
    FD->>Proxy: pipe client -> 127.0.0.1:<innerPort>
    Proxy->>Cam: tls.connect()
    Cam-->>Proxy: 401 + WWW-Authenticate: Digest
    Proxy->>Auth: compute Digest response
    Auth->>Cam: retry with Authorization: Digest ... (no creds from Rec)
    Cam-->>Rec: RTSP 200 OK, media flows
    Note over Bosch,Cam: privacy toggle / renewal rotates creds
    Bosch-->>FD: fresh digestUser/digestPassword (heartbeat/renew)
    FD->>Proxy: updateDigestAuth(user, password)
    Note over Proxy: future connections use rotated creds,<br/>in-flight streams untouched
    Rec-->>FD: last client disconnects
    FD->>FD: idle linger (stream_persistent_idle_timeout, default 60s)
    FD->>Bosch: DELETE /connection (release Bosch session slot)
    Note over FD: front-door itself stays bound —<br/>next connect re-opens on demand

Two operating modes, both published as the same stream_url datapoint:

| Mode | Config | Port behavior | |---|---|---| | On-demand (default) | cameras.<id>.livestream_enabled = true | TLS proxy only listens while the stream is explicitly on; off by default so it never occupies a shared Bosch session slot uninvited | | Always-on front-door | stream_persistent_endpoint = true (RTSP / Stream tab) | A separate lazy listener (src/lib/lazy_stream.ts) stays bound on the sticky port permanently; it opens the Bosch session + inner proxy on the first client connect and releases the Bosch session again after stream_persistent_idle_timeout (10–3600 s, default 60 s) idle — recommended for recorders that poll on their own schedule (forum #84538) |

Config surface (Admin UI → "RTSP / Stream" tab):

| Field | Native key | Default | Purpose | |---|---|---|---| | Expose RTSP proxy to LAN | rtsp_expose_to_lan | false | Bind 0.0.0.0 instead of 127.0.0.1 so a recorder on another host can reach it | | External hostname / LAN IP | rtsp_external_host | "" | Host embedded in the published stream_url when exposed to LAN | | Max session duration (s) | stream_max_session_duration | 0 (camera default, 3600 s) | Raise to keep a continuous pull running longer between renewals (600–21600 s) | | Keep the RTSP endpoint reachable at all times | stream_persistent_endpoint | false | Enables the always-on lazy front-door described above | | Release session after idle (s) | stream_persistent_idle_timeout | 60 | Idle window before the on-demand Bosch session behind the front-door is released (10–3600 s) |

Security note: exposure control is the bind-host toggle only (127.0.0.1 vs. 0.0.0.0/LAN-IP) — there is no IP allowlist or additional token/basic-auth layer on the RTSP endpoint itself, so treat "Expose RTSP proxy to LAN" the same as any other unauthenticated LAN service and keep it off outside a trusted network. The Digest credentials it hides are Bosch's, not a substitute access-control mechanism for your own network.

Port scheme: one TLS-proxy port per camera (and, with the persistent endpoint on, the same sticky port doubles as the front-door port); picked free on first use and persisted across restarts and Bosch session renewals in cameras.<id>._proxy_port, so a recorder's saved URL keeps working indefinitely without reconfiguration.


External Recorders (BlueIris, Frigate)

flowchart LR
    Cam["Bosch Camera<br/>LAN :443<br/>HTTPS only"] -->|RTSP-over-TLS<br/>tunneled| Proxy["Adapter TLS proxy<br/>port: per-camera<br/>bind: 127.0.0.1 or LAN"]
    Proxy -->|"rtsp://user:pwd@<br/>host:&lt;port&gt;/rtsp_tunnel<br/>?inst=1&enableaudio=1"| B[BlueIris]
    Proxy -->|"rtsp://...?inst=1"| F[Frigate]
    Proxy -->|"rtsp://...?inst=2 sub"| ICam[iobroker.cameras]
    Cloud[Bosch CBS API] -.->|hourly session renew<br/>~60s before timeout| Proxy

By default the proxy listens on 127.0.0.1 — reachable from the ioBroker host itself but not from another machine. To use a recorder on a separate host:

  1. Admin UI → "RTSP / Stream" tab → tick Expose RTSP proxy to LAN.
  2. Set External hostname / LAN IP to the ioBroker host's LAN IP, e.g. 192.168.1.50.
  3. Save → adapter restarts → cameras.<id>.stream_url becomes rtsp://<user>:<password>@192.168.1.50:<sticky-port>/rtsp_tunnel?….
  4. Copy that URL into BlueIris / Frigate / your recorder.

The port is sticky across adapter restarts and Bosch session renewals (persisted in cameras.<id>._proxy_port) — set the URL in your recorder once and it keeps working.

Person-only recording via CodeProject AI

A workflow described on the ioBroker forum: add each stream_url as an RTSP camera in BlueIris, enable 24/7 sub-stream recording with a short retention (e.g. 7 days), and wire BlueIris's motion-detection alerts into CodeProject AI running YOLO. Only when CodeProject classifies a frame as a person (or another configured class — dog, cat, vehicle, license plate, face) does BlueIris flip to mainstream recording, with a few seconds of pre-roll. Cuts storage and false-positive alerts dramatically while keeping the rich mainstream footage for events that matter.

iobroker.cameras (snapshot / Vis tile)

iobroker.cameras wraps a generic RTSP source into JPEG snapshots and a Vis MJPEG tile (no H.264/audio — for full live playback use this adapter's VIS-2 Camera widget or go2rtc instead). It does not take a full rtsp://… URL in one field — it builds the URL from separate fields, so the stream_url has to be split.

Recommended: turn on the always-on RTSP endpoint. iobroker.cameras pulls a frame on its own schedule. By default the local RTSP proxy only listens while a live stream is running, so a poll that lands while the stream is off hits a closed port and iobroker.cameras logs Connection refused. Enable Settings → RTSP / Stream → Keep the RTSP endpoint reachable at all times (stream_persistent_endpoint). The adapter then keeps a listener bound on a stable per-camera port at all times and opens the Bosch session automatically when iobroker.cameras connects, releasing it again after an idle timeout — so the endpoint is always reachable without permanently using one of the 3 shared Bosch sessions. With it on, skip step 1; stream_url / stream_host / stream_port / stream_path are populated and stable from adapter start. Forum #84538.

  1. (Only needed if the always-on endpoint above is off.) Enable the stream: set cameras.<id>.livestream_enabled = true. The proxy starts and cameras.<id>.stream_url populates, e.g. rtsp://127.0.0.1:8554/rtsp_tunnel?inst=1&enableaudio=1&fmtp=1&maxSessionDuration=3600.

  2. In the cameras.0 instance add a camera, type RTSP (the generic ffmpeg-snapshot type). To save you splitting the URL by hand, the adapter also publishes the three parts as ready-to-paste datapoints — cameras.<id>.stream_host, stream_port and stream_path:

    | iobroker.cameras field | Copy from datapoint | Example | Notes | |---|---|---|---| | Camera IP | stream_host | 127.0.0.1 | Same host → 127.0.0.1. LAN IP appears here automatically when Expose RTSP proxy to LAN is on. | | Port | stream_port | 8554 | Sticky across restarts. | | Protocol | (set manually) | TCP | Must be changed — the field defaults to UDP, but the proxy is TCP-only. | | Path | stream_path | /rtsp_tunnel?inst=1&enableaudio=1&fmtp=1&maxSessionDuration=3600 | Query string included — copy verbatim. | | Username / Password | — | (leave empty) | The proxy injects Bosch Digest auth transparently; no credentials needed. |

  3. Save. iobroker.cameras serves the snapshot at http://<iobroker-host>:8082/cameras.0/<camera-name> (use that URL in a Vis Basic-Image widget) and offers the live MJPEG tile via its bundled Vis widget.

Because both adapters normally run on the same ioBroker host, 127.0.0.1 works directly — Expose RTSP proxy to LAN is only needed when iobroker.cameras runs on a different machine.


Development

npm install
npm run build        # tsc → build/
npm run watch        # auto-rebuild on save
npm test             # unit tests (1480+ passing)
npm run lint
npm run test:coverage          # coverage report → coverage/index.html (HTML) + lcov
npm run test:coverage:check    # enforce thresholds: 80% lines/functions, 70% branches

CI/CD & testing

The full pipeline — test layers (lint → unit + coverage → package validation → CodeQL/gitleaks/dependency-review → adapter integration → repochecker → release smoke), all GitHub Actions workflows, and the release flow — is documented with diagrams in docs/ci-cd.md. Quality standards and the ioBroker Latest→Stable progression are in docs/TESTING_AND_QUALITY.md.

Security layer (GitHub Actions): CodeQL (SAST), gitleaks (secret scan), dependency-review + Dependabot, with least-privilege workflow permissions.

Manual deploy to a local ioBroker test instance

SRC=$(pwd)
DST=$HOME/iobroker-test/node_modules/iobroker.bosch-smart-home-camera
rm -rf "$DST/build" && cp -r "$SRC/build" "$DST/"
cp "$SRC/io-package.json" "$DST/"
cp -r "$SRC/admin" "$DST/"
~/iobroker-test/iob upload bosch-smart-home-camera
~/iobroker-test/iob restart bosch-smart-home-camera.0

Existing Adapter Landscape

  • iobroker.bshb — SHC Local REST API (thermostats, switches, alarms). Camera on/off only, no stream or snapshot. Active maintainer.
  • iobroker.cameras — generic HTTP snapshot / RTSP wrapper. Pair this adapter's stream_url state with iobroker.cameras to get a Vis tile — see iobroker.cameras (snapshot / Vis tile) for the field-by-field config.
  • iobroker.onvif — generic ONVIF. Bosch cameras don't currently expose a local ONVIF endpoint, so this adapter is the only path for Bosch hardware.

Release Process

This adapter uses @alcalzone/release-script for version bumps.

npm run release patch    # 0.3.0 → 0.3.1
npm run release minor    # 0.3.0 → 0.4.0
npm run release major    # 0.3.0 → 1.0.0
  1. Builds + runs the full test suite (must pass)
  2. Bumps version in package.json + io-package.json
  3. Auto-generates a news entry from commits since the last release
  4. Creates the vX.Y.Z tag and pushes — GitHub Actions auto-publishes to npm

Related Projects

Part of a five-implementation family for Bosch Smart Home Cameras (plus an alpha frontend):

| Implementation | Repo | Status | |---|---|---| | 🏆 Home Assistant Integration | Bosch-Smart-Home-Camera-Tool-HomeAssistant | v14.4.1 · HA Quality Scale Platinum · production-ready | | 🐍 Python CLI | Bosch-Smart-Home-Camera-Tool-Python | v10.10.4 · Mini-NVR + SMB upload (BETA) · LAN-fallback (ping / --local) · PTZ presets · webhook delivery · capture / research / standalone | | 🟢 ioBroker Adapter (this repo) | ioBroker.bosch-smart-home-camera | v1.8.2 · stable · npm · credential-free RTSP front-door (always-on option) · cloud-API management-tier WRITE (zones/masks/rules/sharing/firmware) · daily event counters · MQTT bridge · PTZ presets · VIS-2 single + overview widgets | | 🤖 MCP Server | Bosch-Smart-Home-Camera-Tool-MCP | v1.5.5 · cred-rotation · PTZ presets · TOFU cert pinning · LAN-ping + prefer_local · Claude Code / Claude Desktop integration | | 🔴 Node-RED nodes (alpha) | Bosch-Smart-Home-Camera-Tool-NodeRED | v0.2.5-alpha · cloud nodes (event / snapshot / privacy / stream-url / config) |

Also: Bosch Smart Home Camera — Python Frontend (NiceGUI) — v0.1.5-alpha (dashboard + camera detail + settings) — community interest welcome

HA stays the reference implementation — features land there first; the Python CLI, ioBroker Adapter and MCP Server catch up over time.


Changelog

1.8.3 (2026-07-15)

Docs-only release: fixed the MCP row in the shared Integration Comparison table (shares the Python CLI's bosch_config.json rather than its own OAuth2 PKCE flow) and a broader README accuracy pass (state tree, config options, RTSP front-door emphasis). No functional changes.

1.8.2 (2026-07-14)

Docs-only release: refreshed the sibling-repo version table in the README. No functional changes.

1.8.1 (2026-07-13)

Performance/reliability fix: local camera (digest auth) and cloud API HTTPS requests now reuse pooled keep-alive connections instead of opening a fresh TCP+TLS connection per request, cutting per-request latency and connection overhead. Also fixes a related agent-cleanup gap so pooled connections are properly torn down on adapter unload instead of leaking sockets. No functional/state changes.

1.8.0 (2026-07-11)

New: cloud-API WRITE for the management tier, closing a feature-parity gap with the HA integration and Python CLI (same /v11 endpoints, byte-verified against both). Writable states per camera: motion_zones_set/privacy_masks_set (POST array of {x,y,w,h}, [] clears all), rule_create/rule_update/rule_delete (automation rules), firmware_install (button — installs the pending firmware update, guarded against a double-press or an already-in-progress install). Gen2 only: camera_share, friend_invite, friend_remove. New read-only firmware status states: firmware_current_version, firmware_latest_version, firmware_update_available, firmware_updating. This is cloud write only — the on-device RCP zone/mask editor stays parked pending broader local write access from Bosch. Also adds a blocking npm audit --omit=dev CI gate ahead of the deploy job. 38 new tests, full suite 1367 → 1405 passing, coverage gate green.

1.7.8 (2026-07-07)

Docs-only release: repository-checker keyword fix (package.json/io-package.json, PR #46), refreshed sibling-repo version references in the "Related Projects" table, dev-sandbox Node version doc fix, devDependency bumps (@types/node, @iobroker/adapter-react-v5). No functional changes.

1.7.7 (2026-07-03)

ioBroker.repositories PR#5983 manual-review hardening (mcm1957, 2026-07-02): log/notification text is English-only now (was German, leaking untranslated into this.log.*); external camera IDs are sanitized (ioBroker FORBIDDEN_CHARS) before use in object paths; removed the dead region config option (EU/US dropdown had no effect — CLOUD_API is, and remains, hardcoded); README now credits/links Bosch Smart Home; minor dead-code cleanup (_maskCreds, _featureFlagsCache, EVENT_POLL_INTERVAL_MS). mqtt_password protectedNative/encryptedNative confirmed correct at the io-package.json root (already in place; a first attempt moved them under common, which @iobroker/repochecker's schema rejects — reverted before release). New regression tests pin all of the above. No functional/behavioral change for existing installs beyond the language fix.

1.7.6 (2026-06-28)

CI: integration test harness (@iobroker/testing), build job, Node 22/24 matrix, coverage gate (≥80%), i18n E5606 gate. No functional changes.

1.7.5 (2026-06-25)

Cross-version port of Home Assistant v13.7.8–v13.7.9 WebRTC stability fixes.

  • Stale-PC guard (fix): after a WebRTC reconnect, late mute events from the old peer connection no longer trigger a new recovery — ending an endless loop where every reconnect caused another one.
  • getStats freeze oracle (fix): the player now checks framesDecoded via getStats every 5 s to catch a go2rtc silent stall or a Chrome 145 muted-background-pause that the existing frame-callback / stall timer may not see.
  • Dead-track CGNAT detection → sticky HLS (fix): when WebRTC connects but delivers zero decoded frames (bytes flowing = decoder stall; no bytes = CGNAT cut), the player escalates to HLS for the rest of the session instead of retrying WebRTC in an endless loop.
  • iOS native-HLS 8s watchdog (fix): Safari/iOS AVPlayer can hang at load with no self-recovery; a watchdog hard-reloads the element if playing does not fire within 8 s.
  • Faster reconnect (improvement): recovery restart delay reduced from 2000 ms to 1000 ms.

1.7.4 (2026-06-23)

FCM push reliability: periodic 24 h re-registration prevents silent push loss on long-lived sessions.

1.7.3 (2026-06-21)

Cross-version port of the Home Assistant v13.7.5 fix.

  • Picture-in-Picture freeze in a background tab (fix): follow-up to 1.7.2. With the live stream floating in Picture-in-Picture and the browser tab left in the background, the floating window could still freeze for up to a minute before recovering, because Chrome throttles the player's periodic freeze-check timer to about once a minute in a hidden tab. The player now drives that check from a Web Worker, which runs on a separate thread Chrome does not throttle, so a frozen Picture-in-Picture stream is detected and reconnected in about ten seconds instead of up to a minute. The worker only acts on a Picture-in-Picture window you are actively watching (a plain hidden tab is left alone to conserve the camera session) and falls back cleanly to the existing timer where Web Workers are unavailable. Also hardens the freeze check so a slow reconnect's first frame can't briefly look frozen.

1.7.2 (2026-06-19)

Cross-version port of the Home Assistant v13.7.4 fix.

  • Picture-in-Picture freeze after a tab switch (fix): with the live WebRTC stream floating in Picture-in-Picture (vis-2 widget overlay), switching to another browser tab for a while could freeze the floating window; returning to the tab resumed the in-page video but the floating window stayed frozen. A hidden tab heavily throttles the player's periodic stall check, and the underlying go2rtc WebRTC stream can quietly die in the background. The player now detects a freeze without that throttled timer — it watches for presented video frames (which keep flowing to a Picture-in-Picture window even while the tab is hidden) and listens for the WebRTC track going silent or the connection failing, then reconnects into the same floating window automatically with no interaction. The reconnect reuses the existing video element so Picture-in-Picture picks the stream straight back up.

1.7.1 (2026-06-18)

  • Daily counters now bucket by local date (fix): events_today, movement_count and audio_count were bucketed by UTC day, but Bosch event timestamps carry an explicit timezone offset (e.g. +02:00[Europe/Berlin]) — so events in the hours around local midnight were counted on the wrong day and the counters rolled over at UTC midnight instead of local midnight. They now bucket by each event's local calendar date, matching the Home Assistant