Skip to content

Custom cold storage path is not namespaced per config entry — cameras delete each other's recordings #1404

Description

@jgancedo

Description

Code references are against main @ 71d0983.

getColdDirPathForEntry() namespaces the default cold storage path by config entry (utils.py:213):

coldDirPath = os.path.join(getDataPath(), f".storage/{DOMAIN}/{entry_id}/")

but a custom Media Sync Cold Storage Path is used flat, with no entry component (utils.py:221-222):

if not media_sync_cold_storage_path == "":
    coldDirPath = f"{media_sync_cold_storage_path}/"

getHotDirPathForEntry() (utils.py:236) namespaces by entry_id unconditionally and has no custom-path branch at all. So the default cold path and the hot path both namespace automatically; the custom cold branch is the only one that does not.

Cameras sharing a custom path therefore share one videos/ + thumbs/ directory. deleteFilesNoLongerPresentInCamera() (utils.py:385-421) lists that directory and, at utils.py:409, removes any file whose name is not in that entry's mediaScanResult — i.e. not on that camera's SD card. getRecording() then re-downloads it because the file no longer exists (utils.py:697-716).

This is not limited to cameras with media sync switched on. mediaCleanup() is called from the coordinator update gated solely on ts - lastMediaCleanup > MEDIA_CLEANUP_PERIOD (__init__.py:947-956), with no ENABLE_MEDIA_SYNC check anywhere in that path, and it calls deleteFilesNoLongerPresentInCamera() unconditionally (utils.py:514-519). The only precondition is that entry's initial media scan having completed (utils.py:392).

The options-flow description recommends a single shared path with no per-camera component (translations/en.json:120):

"Set a Cold storage path outside of your config (for example /media/tapo_control) to avoid bloating backups."

Following that with more than one camera produces exactly the configuration above. Neither the README nor the field description mentions needing a distinct path per camera, and the options flow validates only that the path exists (config_flow.py:1531) — it does not warn that another entry already uses it.

If a distinct path per camera is the intended usage, I would suggest the failure mode is still wrong: configuring two entries with the same path destroys recordings silently, with no error, nothing above debug level, and no validation — while the flow does already validate that the same field points somewhere that exists. Three possible resolutions are set out below; any one of them would close this.

Reproduction Steps

  1. Configure two or more cameras with the same Media Sync Cold Storage Path.
  2. Enable media sync on one of them and let it download.
  3. Within MEDIA_CLEANUP_PERIOD (600 s) another entry's cleanup pass deletes those files, and the first camera re-downloads them.

Expected behavior

Any one of these three would resolve it. They are listed cheapest first; the third is the one I would suggest, but the first alone would have prevented the data loss.

1. Document it in the field description (no code change). The step description at options.step.media.description in strings.json / translations/*.json currently gives /media/tapo_control as the example. Adding that each camera needs its own directory, and that sharing one causes cameras to delete each other's recordings, would be enough to stop users configuring it this way.

2. Reject a path already in use by another entry. The options flow already validates this same field and surfaces a translated error — config_flow.py raises when the path does not exist and maps it to errors["base"] = "cold_storage_path_does_not_exist", with strings under config/error/ and options/error/. A duplicate check would follow the identical pattern: compare against MEDIA_SYNC_COLD_STORAGE_PATH on the domain's other config entries and raise a new cold_storage_path_in_use key. The path is then never saved, so no data is at risk.

3. Namespace the custom path per entry as <media_sync_cold_storage_path>/<entry_id>/, matching the default cold path and the hot path. pathlib.Path(...).mkdir(parents=True, exist_ok=True) at utils.py:225-226 already creates the subdirectory, so no config-flow change is needed.

Only option 3 carries a migration cost: existing installs would start from an empty per-entry directory, so each camera re-downloads its sync window once (which matters on battery devices), and the documented filePath in tapo_control_media_downloaded changes value. A one-time move of correctly-named files into the new subdirectory would avoid the re-download.

If applicable, add error logs.

No errors are raised. The deletions are logged at debug level only; see below.

Device Firmware

1.2.27 Build 260518 Rel.25086n

Integration Version

7.1.25

Last known working integration version

Unknown.

Using stream component

No

Does camera work via official integrations?

Yes

HASS Environment

HAOS

Search for similar issues

Yes. #525 is a different bug (double slash in NFS paths, fixed in 5.4.19) and #1159 concerns two cameras each with a separate directory. I found nothing covering a shared path.

LLM Usage

I confirm

Additional information

Code references are against main @ 71d0983.

Debug Logs (Required)

Directed test. Three cameras configured with the same custom cold storage path, confirmed
by all three switch.*_media_sync entities reporting an identical storage_path
attribute. Camera A had media sync on with a 72 h window; cameras B and C had media sync
off. Camera A downloaded one of its own recordings into the shared directory, then
its sync was switched off so it could not re-download. Verbatim, only the mount path and
entry id redacted:

2026-08-10 09:49:35.100 DEBUG (MainThread) [custom_components.tapo_control] {'currentAction': 'Converting', 'fileName': '/media/<mount>/videos/1786230123-1786230189.mp4', 'progress': 0, 'total': 0}

Four minutes later, camera C's cleanup pass — with camera C's media sync switch off —
removed it, 7 ms after starting:

2026-08-10 09:53:57.950 DEBUG (MainThread) [custom_components.tapo_control] Initiating media cleanup for Front camera...
2026-08-10 09:53:57.950 DEBUG (MainThread) [custom_components.tapo_control] Initiating media cleanup for entity <entry-id-C>, child ID:''...
2026-08-10 09:53:57.957 DEBUG (MainThread) [custom_components.tapo_control] [deleteFilesNoLongerPresentInCamera] Removing /media/<mount>/videos/1786230123-1786230189.mp4 (1786230123-1786230189)...
2026-08-10 09:53:57.960 DEBUG (MainThread) [custom_components.tapo_control] [deleteFilesNoLongerPresentInCamera] Removing /media/<mount>/thumbs/1786230123-1786230189.jpg (1786230123-1786230189)...

1786230123-1786230189 is a recording from camera A's SD card, not camera C's.

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions