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
- Configure two or more cameras with the same
Media Sync Cold Storage Path.
- Enable media sync on one of them and let it download.
- 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.
Description
Code references are against
main@71d0983.getColdDirPathForEntry()namespaces the default cold storage path by config entry (utils.py:213):but a custom
Media Sync Cold Storage Pathis used flat, with no entry component (utils.py:221-222):getHotDirPathForEntry()(utils.py:236) namespaces byentry_idunconditionally 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, atutils.py:409, removes any file whose name is not in that entry'smediaScanResult— 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 onts - lastMediaCleanup > MEDIA_CLEANUP_PERIOD(__init__.py:947-956), with noENABLE_MEDIA_SYNCcheck anywhere in that path, and it callsdeleteFilesNoLongerPresentInCamera()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):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
Media Sync Cold Storage Path.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.descriptioninstrings.json/translations/*.jsoncurrently gives/media/tapo_controlas 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.pyraises when the path does not exist and maps it toerrors["base"] = "cold_storage_path_does_not_exist", with strings underconfig/error/andoptions/error/. A duplicate check would follow the identical pattern: compare againstMEDIA_SYNC_COLD_STORAGE_PATHon the domain's other config entries and raise a newcold_storage_path_in_usekey. 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)atutils.py:225-226already 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
filePathintapo_control_media_downloadedchanges 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_syncentities reporting an identicalstorage_pathattribute. 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:
Four minutes later, camera C's cleanup pass — with camera C's media sync switch off —
removed it, 7 ms after starting:
1786230123-1786230189is a recording from camera A's SD card, not camera C's.