Bosch Smart Home Camera — Full HA Integration (HACS)

A native Home Assistant integration for Bosch Smart Home cameras. Live streaming, snapshots, motion + person events, NAS recording, custom Lovelace card — everything wired through HA's standard primitives. No Node-RED detour, no docker bridge, no manual RTSP
scripting.

What you get

  • Live stream via RTSP+TLS — LAN-direct or via the Bosch cloud relay, with automatic LOCAL-over-cloud preference. HLS and WebRTC supported. Per camera switchable: Auto / Local-Only / Cloud-Only.
  • Snapshots — on demand, on schedule, on motion. Configurable refresh interval, JPEG size, optional rotation.
  • Motion + Person triggers as native HA events (bosch_shc_camera_motion, bosch_shc_camera_person) — drop-in for automations. Person AI is Gen2-only (hardware limit).
  • Direct alerts to Signal / Telegram / Companion App with per-step routing (text → snapshot → video clip). No external automation glue needed.
  • NAS recording (BETA) — Mini-NVR with rolling segments, retention policies. Recordings browse-and-play through the standard HA Media Browser (local FS or SMB share, even when uploads ran via FTP).
  • Privacy mode + camera light toggles. Motion-zone read-out today; editing coming when Bosch ships their local API.
  • Custom Lovelace card auto-installed as a resource — bosch-camera-card.js with auto-play, click-to-fullscreen, stream-state badge.
  • iOS + Android Companion app push (FCM) — events arrive within seconds via push, automatic polling fallback when push disconnects.

Hardware support

Camera Generation Status
Bosch Eyes Outdoor II Gen2 Full support
Bosch Eyes Indoor II Gen2 Full support
Bosch Eyes Outdoor (original) Gen1 Stream + motion (no Person AI). EOL by Bosch on 31.12.2028
Bosch 360° Indoor (original) Gen1 Stream + motion (no Person AI). EOL by Bosch on 31.12.2028

Install

Step 1 — add the repo to HACS (one click):

Open your Home Assistant instance and open a repository inside the Home Assistant Community Store.

Install → Restart Home Assistant.

Step 2 — set up the integration (one click):

Open your Home Assistant instance and start setting up a new integration.

Sign in with the same Bosch ID you use in the official Bosch Smart Home app. OAuth, FCM registration and cloud-relay setup are handled automatically. Multi-camera, multi-account supported.

The HACS-default PR is open (hacs/default#7247) — once merged, the install becomes truly one-click without needing to add the custom repo.

Quality

Published at HA Quality Scale: Platinum (declared in manifest.json, enforced by CI):

  • Full mypy --strict typing across the entire custom_components/ tree
  • Async everywhere — zero requests calls (all HTTP via aiohttp)
  • Regression tests for every reproduced bug and user-reported issue
  • All Digest-auth flows go through a single async helper — no surprise blocking calls in the event loop

PRs that break any of these gates don't merge.

Support

  • Bug reports → please open a GitHub Issue. A log snippet (ha core logs | grep bosch_shc_camera) usually nails it down quickly.
  • Feature requests → GitHub Issues with the enhancement label.
  • Discussion / "is this my setup?" → this thread is perfect.

If a forum post points at a real bug, I'll usually ask you to mirror it to a GitHub Issue so it can be tagged, tracked, and shipped with a fix in a named release.

My only feedback is it you used AI resources to help code this, it should be disclosed.

as every can see i used for many parts Claude Code.

Thanks for this HACS integration! It's rare for things to work so smoothly on HAOS :).

I'm trying (Eyes Außenkamera Gen1 FW 7.91.56) to send a photo to Telegram using a 'LastEvent' trigger, it works but there is a delay of about 20 seconds until a motion detection and the trigger.

Do you have a code example you could share for a similar application?

Current code:

alias: 890 Mouvement detecté Notification Telegram
description: Envoi snapshot Bosch sur Telegram
triggers:

  • entity_id:
    • sensor.bosch_bcament_last_event
      trigger: state
      conditions:
  • condition: template
    value_template: |
    {{
    trigger.from_state is not none and
    trigger.to_state.state | int >
    trigger.from_state.state | int
    }}
    actions:
  • data:
    url: |
    {{ 'http://homeassistant.local:8123' ~
    state_attr('camera.bosch_bcament', 'entity_picture') }}
    caption: |
    :police_car_light: Mouvement détecté à {{ now().strftime('%H:%M:%S') }}
    action: telegram_bot.send_photo
    mode: single
    max_exceeded: silent

Hi Andrew, thanks for the kind words!

The ~20 s delay is expected with the trigger you're using. sensor.bosch_<cam>_last_event only updates when the
coordinator polls Bosch's cloud (default every 60 s), so worst-case lag is one polling cycle. Two ways to get it faster:

  1. Enable FCM push in the integration options
    Settings → Devices & services → Bosch Smart Home Camera → Configure → tick "Enable FCM push". Motion events then arrive
    via Firebase within ~1–3 seconds instead of waiting for the next poll. This is the single biggest improvement.

  2. Use the dedicated motion binary sensor instead of last_event
    The integration creates binary_sensor.bosch_<cam>_motion per camera but it's disabled by default. Enable it under the
    device's entity list. It flips ON the moment an event is registered (FCM-pushed or polled) and exposes the snapshot URL
    as an attribute.

Cleaner automation:

alias: 890 Mouvement détecté Notification Telegram
description: Envoi snapshot Bosch sur Telegram
triggers:
  - platform: state
    entity_id: binary_sensor.bosch_bcament_motion
    from: "off"
    to: "on"
actions:
  - action: telegram_bot.send_photo
    data:
      url: >
        {{ 'http://homeassistant.local:8123' ~
           state_attr('camera.bosch_bcament', 'entity_picture') }}
      caption: "🚨 Mouvement détecté à {{ now().strftime('%H:%M:%S') }}"
mode: single
max_exceeded: silent

With FCM push enabled and the binary sensor as trigger you should see the Telegram message arrive within a few seconds of
the motion event. Let me know how it goes!

Want me to also reply on Telegram-snapshot URL gotchas (external HA URL vs homeassistant.local, token in image URL) or
keep it focused on the latency answer?

Thank you very much for your precious help.

The 'Pull' option is enabled in the integration, and the Motion sensor is activated, but the state seems to stay 'ON' permanently.

binary_sensor.bosch_bcament_bosch_bcament_motion,off,2026-05-13T12:34:32.021Z
binary_sensor.bosch_bcament_bosch_bcament_motion,on,2026-05-13T12:35:37.515Z
binary_sensor.bosch_bcament_bosch_bcament_motion,off,2026-05-13T12:40:45.938Z
binary_sensor.bosch_bcament_bosch_bcament_motion,on,2026-05-13T12:41:51.324Z
binary_sensor.bosch_bcament_bosch_bcament_motion,unavailable,2026-05-13T12:52:17.149Z
binary_sensor.bosch_bcament_bosch_bcament_motion,off,2026-05-13T12:52:17.491Z
binary_sensor.bosch_bcament_bosch_bcament_motion,on,2026-05-13T12:53:22.429Z

Looking at your own log, it actually flipped state seven times in 19 minutes — it's not stuck, it just stays on for long
stretches:

  • 12:35:37 → on, 12:40:45 → off (≈ 5 min)
  • 12:41:51 → on, 12:52:17 → off (≈ 10 min)
  • 12:53:22 → on (current)

How the motion binary sensor works internally: it goes ON when a MOVEMENT event arrives and stays ON for 90 seconds after
the timestamp of the most-recent event
. If new MOVEMENT events keep landing, the window slides forward and the sensor
stays ON. So a 5–10 min on period means the camera was firing motion events roughly every minute for that whole stretch —
normal for an outdoor camera (cars, leaves, light changes).

Two things to check:

  1. The Pull option you enabled is the polling fallback (fcm_push_mode = polling), not the fast path. For ~2 s motion
    latency, turn on FCM push instead: Settings → Devices → Bosch SHC Camera → Configure → "Receive motion/audio events via
    FCM"
    and set FCM Push Mode to auto. In polling mode each poll is up to 60 s late, which on its own can already look like
    sticky on states.
  2. Tune motion sensitivity in the Bosch Smart Home app (Camera → Settings → Motion detection). Outdoor cams trigger a lot
    on defaults.

The single unavailable blip at 12:52:17 is one coordinator tick that briefly failed and recovered on the next poll —
nothing to worry about unless it repeats.

I didn't explain myself well, sorry. 'FCM push' is indeed enabled with the 'AUTO' option. Motion detection in the Bosch app is set to the minimum and works correctly according to the app logs. For your information, audio detection is not enabled.

I confirm that during my tests, the motion sensor stays 'ON' constantly, and as a result, it fails to trigger the automation.

Thanks for your help. :+1:

Thanks for the clarification, that helps a lot.

Your setup is correct — FCM Push = AUTO + min sensitivity is exactly the recommended config. The "permanent ON" you're seeing is most likely not a bug of the integration but the natural consequence of how the binary sensor's "active window"
interacts with a chatty outdoor camera.

How the motion binary sensor works internally

binary_sensor.bosch_<cam>_motion is ON for 90 seconds after the timestamp of the most-recent MOVEMENT event. If a new MOVEMENT event lands during that window — even faintly — the window slides forward and the sensor stays ON.

So if your camera fires a MOVEMENT event roughly every 60–80 seconds (very common for outdoor cameras even on "minimum" — wind, leaves, sunlight changes are enough), the sensor will appear permanently ON and your automation's from: 'off' → to: 'on'
transition never fires.

Quick verification: in the Bosch app's event timeline for that camera, count how many MOVEMENT entries you see in the last hour. If it's > ~30, that's the cause.

Workaround (cleanest)

Trigger your automation off the HA event bus instead of the binary sensor. The integration fires an edge-triggered event for every individual motion push — no window state, no merging:

trigger:
  - platform: event
    event_type: bosch_shc_camera_motion
    event_data:
      camera_id: <your-camera-uuid>   # optional filter

Each FCM push = exactly one trigger. Works the same way for bosch_shc_camera_person and bosch_shc_camera_audio_alarm.

You can find your camera UUID in camera.<your_bosch_cam> → state attributes → camera_id. If you have only one Bosch camera, the event_data filter is optional.

If you still need the binary sensor

For a typical "person walks up to door" automation it's usually nicer to trigger on the event, but if you specifically want the binary_sensor:

  • Drop the camera sensitivity further (via select.bosch_<cam>_motion_sensitivity from HA — sometimes the Bosch app's "Niedrig" is one step up from the lowest setting the API exposes).
  • I'm considering making EVENT_ACTIVE_WINDOW (currently 90s) configurable in the integration options for the next release, since FCM AUTO doesn't need the polling-fallback safety margin that drove the 90s default.

Let me know how the event count looks — that confirms whether it's chatty-camera or something else.

Thank you for your detailed feedback and explanations.

As suggested, I implemented the bosch_shc_camera_motion trigger after reducing the motion detection sensitivity to the minimum via select.bosch_<cam>_motion_sensitivity in Home Assistant.

This trigger works correctly, unlike bosch_shc_camera_person, which does not seem to produce any events in my case (likely related to a Bosch Eyes Gen1 camera).

However, I still observe a delay between the detection time shown in the Bosch app and the trigger firing in Home Assistant, ranging from 30 seconds up to sometimes 2 minutes.

Thanks again for the clarification regarding the internal behavior and the event bus approach, which helps to better understand the observed behavior.

Hi @Andrew75 — two quick answers and one request:

  1. Person trigger on Gen1: your observation is correct. bosch_shc_camera_person only fires on Gen2 cameras (Eyes Outdoor II / Indoor II / 360°). Gen1 (Eyes / 360°) doesn't have on-camera Person AI — only generic motion. Hardware-level, not configurable.

  2. 30 s – 2 min delay: that 2-minute upper bound is a strong signal. The integration runs a coordinator tick every 60 s. When FCM push is healthy, motion events arrive via push within 5–15 s and the 300 s poll is just a safety net. When the FCM listener has
    disconnected, the integration accelerates polling to once per 60 s — combined with the 60 s coordinator tick this caps total latency at ~2 min (scan_interval + interval_events_fallback). Your range fits FCM-dead state exactly.

To confirm, could you grep your HA log? The watchdog logs the moment FCM dies:

ha core logs 2>&1 | grep -iE "bosch_shc_camera|fcm" | tail -300

Look for any line containing FCM push watchdog: FcmPushClient.is_started()=False. If that appears, polling is in 60 s fallback mode — which is exactly what you're seeing. A Home Assistant restart usually re-establishes the FCM connection; if it dies again repeatedly,
the log snippet around the disconnect will tell us why.

Also useful: the value of fcm_push_mode from Integration → Options → Push notifications.

(For context — this integration is HA Quality Scale Platinum with strict typing, ≥99 % test coverage and async-everywhere, so this kind of latency regression is exactly what we want to chase down. A GitHub
Issue
is also fine if you prefer that for tracking — same response from me either way.)

PS — would German be okay if you're a native speaker? Some of the FCM-vs-poll internals are easier to explain precisely in German. English is totally fine if you prefer; just offering.

Sorry — my German is even worse than my English :slightly_smiling_face:

Before restarting HA, there was indeed a log entry yesterday mentioning FCM - see below in french... The FCM mode appears as AUTO.

After the restart, the delay did not significantly improve and is still around ~1 minute.

I’ll check the logs again to confirm whether FCM is actually staying connected .


This error originated from a custom integration.

Enregistreur: custom_components.bosch_shc_camera
Source: custom_components/bosch_shc_camera/init.py:1373
intégration: Bosch Smart Home Camera (documentation, problèmes)
S'est produit pour la première fois: 14 mai 2026 à 08:46:53 (2 occurrences)
Dernier enregistrement: 14 mai 2026 à 08:48:14

FCM push watchdog: FcmPushClient.is_started()=False — listener terminated, flagging unhealthy (polling tempo resumes)

New log:
Enregistreur: frontend.js.modern.202603258
Source: components/system_log/init.py:331
S'est produit pour la première fois: 10:38:57 (142 occurrences)
Dernier enregistrement: 10:42:18

  • Failed to format translation for key 'component.bosch_shc_camera.options.step.init.sections.events_storage.data_description.folder_pattern' in language 'fr'. [formatjs Error: MISSING_VALUE] The intl string context variable "camera" was not provided to the string "Subfolder pattern. Available variables: {camera}, {year}, {month}, {day}, {type}. Default: {camera}/{year}/{month}/{day}"
  • Failed to format translation for key 'component.bosch_shc_camera.options.step.init.sections.events_storage.data_description.file_pattern' in language 'fr'. [formatjs Error: MISSING_VALUE] The intl string context variable "camera" was not provided to the string "File name pattern (without extension). Variables: {camera}, {date}, {time}, {type}, {id}, {year}, {month}, {day}. Default: {camera}_{date}_{time}_{type}_{id}"

Confirmed — that's exactly the diagnostic line I was looking for. FCM listener died at 08:46:53 and again at 08:48:14. Once the watchdog flags it unhealthy, the integration falls back to polling /events every 60 s. Combined with the 60 s coordinator tick, that gives the ~2 min ceiling you're seeing. Restart doesn't help long-term because something keeps killing the FCM connection in the first place — restart just re-establishes it until the same trigger fires again.

To find out why the listener is dying, could you grab the 5–10 log lines right around one of those FcmPushClient.is_started()=False lines? Specifically the ~30 s before. Common culprits:

  • Network flap / router DNS hiccup (the underlying TLS connection drops, the Firebase library doesn't reconnect cleanly)
  • Memory pressure on the HA host (the Python firebase-messaging library's recursive reconnect can hammer CPU on a small host)
  • HA OS hostname not resolving / IPv6 issues

The lines you're looking for typically contain firebase, ssl, connection, socket, or a Python traceback ending in something like ConnectionResetError.

In the meantime, two things you can try:

  1. Pin fcm_push_mode to ios or android (whichever Companion App you use), instead of auto. AUTO occasionally races during reconnect; pinning eliminates one mode-switch path.
  2. If you're on HA Container/Supervised, check Settings → System → Hardware for memory usage. The firebase-messaging library can spew thousands of log lines per minute when the network is flaky; if HA is under memory pressure that can affect FCM stability.

On the second log you posted — the formatjs Error: MISSING_VALUE for events_storage.data_description.folder_pattern and file_pattern: that was a real translation bug on my side. The strings contained {camera}, {year} etc. as literal pattern placeholders (so users know which variables they can type into the field), but the JSON didn't escape the braces, so formatjs treated them as runtime variables and threw MISSING_VALUE. Already fixed in v12.2.0 which I just released (release notes) — wrapped each placeholder with the ICU single-quote escape ('{camera}') and added a regression test that scans every data_description block for naked tokens so this can't reappear via copy-paste. Visual output is unchanged; pure log-spam reduction. Update via HACS and the messages should stop. Thanks for catching it.

P.S. — no worries about the language! English is great, French is fine too. Whatever's easiest. The log snippets are the universal part.

firebase issue:

2026-05-14 09:22:31.074 ERROR (Thread-9) [pychromecast.socket_client] [Salon(192.168.1.49):8009] Failed to connect to service HostServiceInfo(host='192.168.1.49', port=8009), retrying in 5.0s

2026-05-14 13:49:40.869 ERROR (MainThread) [firebase_messaging.fcmpushclient] Unexpected exception during read

2026-05-14 14:11:56.692 ERROR (MainThread) [firebase_messaging.fcmpushclient] Unexpected exception during read

2026-05-14 18:14:47.629 ERROR (Thread-5) [pychromecast.socket_client] [Nest Google(192.168.1.240):8009] Failed to connect to service

2026-05-14 08:48:14.246 FcmPushClient.is_started()=False:

2026-05-14 08:44:22.744 ERROR (MainThread) [frontend.js.modern.202603258] Failed to format translation for key 'component.bosch_shc_camera.options.step.init.sections.events_storage.data_description.folder_pattern' in language 'fr'. [formatjs Error: MISSING_VALUE] The intl string context variable "camera" was not provided to the string "Subfolder pattern. Available variables: {camera}, {year}, {month}, {day}, {type}. Default: {camera}/{year}/{month}/{day}"

2026-05-14 08:44:22.744 ERROR (MainThread) [frontend.js.modern.202603258] Failed to format translation for key 'component.bosch_shc_camera.options.step.init.sections.events_storage.data_description.file_pattern' in language 'fr'. [formatjs Error: MISSING_VALUE] The intl string context variable "camera" was not provided to the string "File name pattern (without extension). Variables: {camera}, {date}, {time}, {type}, {id}, {year}, {month}, {day}. Default: {camera}_{date}_{time}_{type}_{id}"

2026-05-14 08:44:24.109 ERROR (MainThread) [frontend.js.modern.202603258] Failed to format translation for key 'component.bosch_shc_camera.options.step.init.sections.events_storage.data_description.folder_pattern' in language 'fr'. [formatjs Error: MISSING_VALUE] The intl string context variable "camera" was not provided to the string "Subfolder pattern. Available variables: {camera}, {year}, {month}, {day}, {type}. Default: {camera}/{year}/{month}/{day}"

2026-05-14 08:44:24.109 ERROR (MainThread) [frontend.js.modern.202603258] Failed to format translation for key 'component.bosch_shc_camera.options.step.init.sections.events_storage.data_description.file_pattern' in language 'fr'. [formatjs Error: MISSING_VALUE] The intl string context variable "camera" was not provided to the string "File name pattern (without extension). Variables: {camera}, {date}, {time}, {type}, {id}, {year}, {month}, {day}. Default: {camera}_{date}_{time}_{type}_{id}"

2026-05-14 08:46:52.942 WARNING (MainThread) [homeassistant.helpers.service] Referenced entities button.bosch_est_bosch_est_refresh_snapshot are missing or not currently available

2026-05-14 08:46:53.468 WARNING (MainThread) [custom_components.bosch_shc_camera] FCM push watchdog: FcmPushClient.is_started()=False — listener terminated, flagging unhealthy (polling tempo resumes)

2026-05-14 08:48:13.815 WARNING (MainThread) [homeassistant.helpers.service] Referenced entities button.bosch_est_bosch_est_refresh_snapshot are missing or not currently available

2026-05-14 08:48:14.246 WARNING (MainThread) [custom_components.bosch_shc_camera] FCM push watchdog: FcmPushClient.is_started()=False — listener terminated, flagging unhealthy (polling tempo resumes)

2026-05-14 08:52:41.942 WARNING (MainThread) [homeassistant.helpers.service] Referenced entities button.bosch_est_bosch_est_refresh_snapshot are missing or not currently available

2026-05-14 08:52:53.954 WARNING (MainThread) [homeassistant.components.telegram_bot.bot] Status code 500 (retry #1) loading http://homeassistant.local:8123/api/camera_proxy/camera.bosch_est?token=49604c452ad7e969a97947ed2a683ba383c9e32010e17b7285828c5e5d6e3882

Are you just copying your users' questions, feeding them into a prompt, and then pasting the answers here in the forum?

Claude helps me phrase the technical replies, otherwise they tend to read like raw notes. The bug and the fix come from me
though. See commit 03467d9 in the repo: 30 entities reworked, migration helper for the old entity_ids, plus 95 regression
tests. About two hours of work yesterday evening. Happy to walk through any part of the code if you want.

v12.3.0 just went out and it covers three of the four items from your log.

The button.bosch_est_bosch_est_refresh_snapshot warning is a refactor leftover from v11.0.0. When I added
_attr_has_entity_name = True to the buttons during the Gold-compliance pass, I forgot to strip the device-name prefix
from _attr_name, so HA prepended the device name a second time and the entity_id got stored that way in the registry. It
affected 30 entities across button, update, select, number, binary_sensor and light. v12.3.0 fixes the source and adds an
auto-migration that renames the old entity_ids on next setup. You will see a notice in Settings → Repairs after the
update listing the rename count and a few examples. Heads-up: HA does not rewrite automations or dashboards when
entity_ids change, so any references to the old names need to be updated by hand.

The 500 on /api/camera_proxy was an uncaught ValueError from the digest-auth helper, raised when the camera returns
401 without a WWW-Authenticate: Digest header (which can happen if the LOCAL credentials get half-rotated during an FCM
flap). Both call sites now catch it and fall back to the cached image.

The French translation issue went out in v12.2.1 already.

The Firebase reads I am still stuck on. The bare Unexpected exception during read line is all that library logs, and the
actual cause normally shows up 20-30 seconds earlier (TLS reset, DNS, memory pressure, etc.). If you can grab the
surrounding context with something like

ha core logs 2>&1 | grep -E -B30 "fcmpushclient.*Unexpected" | tail -100

I’ve just installed the latest version. Thank you for the fixes, they seem to be working well.

I cannot find the information you are looking for in my logs. The easiest solution would probably be for me to send you an extract of the logs directly.

Would the email address I found on the web (i at m.eu ) be suitable so I can send it to you privately?

Good idea. Send me a pm in the forum once