Skip to content

Commit

Permalink
Add #667: Media Sync switch entity
Browse files Browse the repository at this point in the history
  • Loading branch information
JurajNyiri committed Oct 28, 2024
1 parent f1289c9 commit af5c872
Show file tree
Hide file tree
Showing 11 changed files with 157 additions and 104 deletions.
6 changes: 2 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,9 @@ Integration is capable of synchronizing recordings for fast playback.

Synchronization is turned off by default, you can browse media stored on camera and request it to be played. However, downloading is rather slow, so it is a good idea to enable media synchronization in background. That way, you will be able to play any synchronized media from camera instantly.

You can enable this setting by navigating to Home Assistant Settings -> Devices and clicking on Configure button next to the Tapo device you wish to turn media synchronization on for.
You can enable this setting by navigating to Home Assistant Settings -> Devices and clicking on Configure button next to the Tapo device you wish to turn media synchronization on for. Here, you need to define the number of hours to synchronize. Unless it is specified, synchronization does not run. Here, you are able to also set the storage path where the synchronized recordings will be stored (defaults to /config/.storage/tapo_control).

You need to also define the number of hours to synchronize. Unless it is specified, synchronization does not run.

Finally, you are able to set the storage path where the synchronized recordings will be stored (defaults to /config/.storage/tapo_control).
Finally, you can turn on, or off switch entity `switch.*_media_sync`.

**Notice:**: Recordings are deleted after the number of hours you have chosen to synchronize passes, once both the actual recording time and the file modified time is older than the number of hours set.

Expand Down
106 changes: 74 additions & 32 deletions custom_components/tapo_control/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
ConfigEntryAuthFailed,
DependencyError,
)
from homeassistant.helpers.storage import Store
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.util import dt
from homeassistant.components.media_source.error import Unresolvable
Expand Down Expand Up @@ -56,6 +57,7 @@
deleteDir,
getColdDirPathForEntry,
getDataForController,
getEntryStorageFile,
getHotDirPathForEntry,
isUsingHTTPS,
mediaCleanup,
Expand Down Expand Up @@ -253,6 +255,18 @@ def update_unique_id(entity_entry):

hass.config_entries.async_update_entry(config_entry, data=new, version=16)

if config_entry.version == 16:
new = {**config_entry.data}
entry_storage = Store(hass, version=1, key=getEntryStorageFile(config_entry))

if ENABLE_MEDIA_SYNC in new:
await entry_storage.async_save({ENABLE_MEDIA_SYNC: new[ENABLE_MEDIA_SYNC]})
del new[ENABLE_MEDIA_SYNC]
else:
await entry_storage.async_save({ENABLE_MEDIA_SYNC: False})

hass.config_entries.async_update_entry(config_entry, data=new, version=17)

LOGGER.info("Migration to version %s successful", config_entry.version)

return True
Expand Down Expand Up @@ -648,11 +662,13 @@ async def async_update_data():
except Exception as err:
hass.data[DOMAIN][entry.entry_id]["initialMediaScanDone"] = True
hass.data[DOMAIN][entry.entry_id]["mediaSyncAvailable"] = False
enableMediaSync = entry.data.get(ENABLE_MEDIA_SYNC)
enableMediaSync = hass.data[DOMAIN][entry.entry_id][
ENABLE_MEDIA_SYNC
]
errMsg = "Disabling media sync as there was error returned from getRecordingsList. Do you have SD card inserted?"
if enableMediaSync:
LOGGER.warn(errMsg)
LOGGER.warn(err)
LOGGER.warning(errMsg)
LOGGER.warning(err)
else:
LOGGER.info(errMsg)
LOGGER.info(err)
Expand Down Expand Up @@ -717,6 +733,7 @@ async def async_update_data():
"downloadedStreams": {}, # keeps track of all videos downloaded
"downloadProgress": False,
"initialMediaScanDone": False,
ENABLE_MEDIA_SYNC: None,
"mediaSyncScheduled": False,
"mediaSyncRanOnce": False,
"mediaSyncAvailable": True,
Expand Down Expand Up @@ -813,9 +830,9 @@ async def async_update_data():

# todo move to utils
async def mediaSync(time=None):
LOGGER.debug("mediaSync")
LOGGER.warning("mediaSync")
hass.data[DOMAIN][entry.entry_id]["mediaSyncRanOnce"] = True
enableMediaSync = entry.data.get(ENABLE_MEDIA_SYNC)
enableMediaSync = hass.data[DOMAIN][entry.entry_id][ENABLE_MEDIA_SYNC]
mediaSyncHours = entry.data.get(MEDIA_SYNC_HOURS)

if mediaSyncHours == "":
Expand All @@ -835,31 +852,37 @@ async def mediaSync(time=None):
tapoController: Tapo = hass.data[DOMAIN][entry.entry_id][
"controller"
]
LOGGER.debug("getRecordingsList -1")
LOGGER.warning("getRecordingsList -1")
recordingsList = await hass.async_add_executor_job(
tapoController.getRecordingsList
)
LOGGER.debug("getRecordingsList -2")
LOGGER.warning("getRecordingsList -2")

ts = datetime.datetime.utcnow().timestamp()
for searchResult in recordingsList:
for key in searchResult:
if (mediaSyncTime is False) or (
(
mediaSyncTime is not False
and (
(int(ts) - (int(mediaSyncTime) + 86400))
< convert_to_timestamp(
searchResult[key]["date"]
enableMediaSync = hass.data[DOMAIN][entry.entry_id][
ENABLE_MEDIA_SYNC
]
if enableMediaSync and (
(mediaSyncTime is False)
or (
(
mediaSyncTime is not False
and (
(int(ts) - (int(mediaSyncTime) + 86400))
< convert_to_timestamp(
searchResult[key]["date"]
)
)
)
)
):
LOGGER.debug("getRecordings -1")
LOGGER.warning("getRecordings -1")
recordingsForDay = await getRecordings(
hass, entry.entry_id, searchResult[key]["date"]
)
LOGGER.debug("getRecordings -2")
LOGGER.warning("getRecordings -2")
totalRecordingsToDownload = 0
for recording in recordingsForDay:
for recordingKey in recording:
Expand All @@ -875,34 +898,53 @@ async def mediaSync(time=None):
):
recordingCount += 1
try:
LOGGER.debug("getRecording -1")
await getRecording(
hass,
tapoController,
entry.entry_id,
searchResult[key]["date"],
recording[recordingKey][
"startTime"
],
recording[recordingKey]["endTime"],
recordingCount,
totalRecordingsToDownload,
)
LOGGER.debug("getRecording -2")
enableMediaSync = hass.data[DOMAIN][
entry.entry_id
][ENABLE_MEDIA_SYNC]
if enableMediaSync:
LOGGER.warning("getRecording -1")
await getRecording(
hass,
tapoController,
entry.entry_id,
searchResult[key]["date"],
recording[recordingKey][
"startTime"
],
recording[recordingKey][
"endTime"
],
recordingCount,
totalRecordingsToDownload,
)
LOGGER.warning("getRecording -2")
else:
LOGGER.warning(
f"Media sync disabled (inside getRecording): {enableMediaSync}"
)
except Unresolvable as err:
if (
str(err)
== "Recording is currently in progress."
):
LOGGER.info(err)
else:
LOGGER.warn(err)
LOGGER.warning(err)
except Exception as err:
hass.data[DOMAIN][entry.entry_id]["runningMediaSync"] = False
LOGGER.error(err)
else:
LOGGER.warning(
f"Media sync ignoring {searchResult[key]["date"]}. Media sync: {enableMediaSync}."
)
except Exception as err:
LOGGER.error(err)
LOGGER.debug("runningMediaSync -false")
LOGGER.warning("runningMediaSync -false")
hass.data[DOMAIN][entry.entry_id]["runningMediaSync"] = False
else:
LOGGER.warning(
f"Media sync disabled (inside mediaSync): {enableMediaSync}"
)

async def unsubscribe(event):
if hass.data[DOMAIN][entry.entry_id]["events"]:
Expand Down
42 changes: 1 addition & 41 deletions custom_components/tapo_control/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,13 @@
from .const import (
BRAND,
DOMAIN,
ENABLE_MEDIA_SYNC,
LOGGER,
ENABLE_SOUND_DETECTION,
MEDIA_SYNC_HOURS,
SOUND_DETECTION_PEAK,
SOUND_DETECTION_DURATION,
SOUND_DETECTION_RESET,
)
from .utils import build_device_info, getColdDirPathForEntry, getStreamSource
from .utils import build_device_info, getStreamSource
from .tapo.entities import TapoBinarySensorEntity

import haffmpeg.sensor as ffmpeg_sensor
Expand All @@ -55,50 +53,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
LOGGER.debug("Adding TapoNoiseBinarySensor...")
binarySensors.append(TapoNoiseBinarySensor(entry, hass, config_entry))

binarySensors.append(TapoMediaSyncEnabledSensor(entry, hass, config_entry))

if binarySensors:
async_add_entities(binarySensors)

return True


class TapoMediaSyncEnabledSensor(TapoBinarySensorEntity):
def __init__(self, entry: dict, hass: HomeAssistant, config_entry):
LOGGER.debug("TapoMediaSyncEnabledSensor - init - start")
self._config_entry = config_entry
self.latestCamData = entry["camData"]
self._hass = hass
self._attr_extra_state_attributes = {}
TapoBinarySensorEntity.__init__(
self,
"Media Sync Enabled",
entry,
hass,
config_entry,
None,
)
self.updateTapo(self.latestCamData)

LOGGER.debug("TapoMediaSyncEnabledSensor - init - end")

def updateTapo(self, camData):
enableMediaSync = self._config_entry.data.get(ENABLE_MEDIA_SYNC)
mediaSyncHours = self._config_entry.data.get(MEDIA_SYNC_HOURS)
if enableMediaSync and mediaSyncHours:
self._attr_state = STATE_ON
else:
self._attr_state = STATE_OFF
self._attr_extra_state_attributes["sync_hours"] = mediaSyncHours
self._attr_extra_state_attributes["storage_path"] = getColdDirPathForEntry(
self._hass, self._config_entry.entry_id
)

@property
def entity_category(self):
return EntityCategory.DIAGNOSTIC


class TapoNoiseBinarySensor(TapoBinarySensorEntity):
def __init__(self, entry: dict, hass: HomeAssistant, config_entry):
LOGGER.debug("TapoNoiseBinarySensor - init - start")
Expand Down
14 changes: 1 addition & 13 deletions custom_components/tapo_control/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
)
from .const import (
DOMAIN,
ENABLE_MEDIA_SYNC,
ENABLE_MOTION_SENSOR,
ENABLE_STREAM,
ENABLE_SOUND_DETECTION,
Expand Down Expand Up @@ -47,7 +46,7 @@
class FlowHandler(ConfigFlow):
"""Handle a config flow."""

VERSION = 16
VERSION = 17

@staticmethod
def async_get_options_flow(config_entry):
Expand Down Expand Up @@ -397,7 +396,6 @@ async def async_step_other_options(self, user_input=None):
data={
MEDIA_VIEW_DAYS_ORDER: "Ascending",
MEDIA_VIEW_RECORDINGS_ORDER: "Ascending",
ENABLE_MEDIA_SYNC: False,
MEDIA_SYNC_HOURS: "",
MEDIA_SYNC_COLD_STORAGE_PATH: "",
ENABLE_MOTION_SENSOR: enable_motion_sensor,
Expand Down Expand Up @@ -1012,7 +1010,6 @@ async def async_step_media(self, user_input=None):
"[%s] Opened Tapo options - media.", self.config_entry.data[CONF_IP_ADDRESS]
)
errors = {}
enable_media_sync = self.config_entry.data[ENABLE_MEDIA_SYNC]
media_view_days_order = self.config_entry.data[MEDIA_VIEW_DAYS_ORDER]
media_view_recordings_order = self.config_entry.data[
MEDIA_VIEW_RECORDINGS_ORDER
Expand All @@ -1025,10 +1022,6 @@ async def async_step_media(self, user_input=None):
allConfigData = {**self.config_entry.data}
if user_input is not None:
try:
if ENABLE_MEDIA_SYNC in user_input:
enable_media_sync = user_input[ENABLE_MEDIA_SYNC]
else:
enable_media_sync = False

if MEDIA_VIEW_DAYS_ORDER in user_input:
media_view_days_order = user_input[MEDIA_VIEW_DAYS_ORDER]
Expand All @@ -1054,7 +1047,6 @@ async def async_step_media(self, user_input=None):
else:
media_sync_cold_storage_path = ""

allConfigData[ENABLE_MEDIA_SYNC] = enable_media_sync
allConfigData[MEDIA_VIEW_DAYS_ORDER] = media_view_days_order
allConfigData[MEDIA_VIEW_RECORDINGS_ORDER] = media_view_recordings_order
allConfigData[MEDIA_SYNC_HOURS] = media_sync_hours
Expand Down Expand Up @@ -1082,10 +1074,6 @@ async def async_step_media(self, user_input=None):
MEDIA_VIEW_RECORDINGS_ORDER,
description={"suggested_value": media_view_recordings_order},
): vol.In(MEDIA_VIEW_RECORDINGS_ORDER_OPTIONS),
vol.Optional(
ENABLE_MEDIA_SYNC,
description={"suggested_value": enable_media_sync},
): bool,
vol.Optional(
MEDIA_SYNC_HOURS,
description={"suggested_value": media_sync_hours},
Expand Down
23 changes: 14 additions & 9 deletions custom_components/tapo_control/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,16 +289,21 @@ async def async_update(self) -> None:

def updateTapo(self, camData: dict | None) -> None:
"""Update the entity."""
enable_media_sync = self._config_entry.data.get(ENABLE_MEDIA_SYNC)
LOGGER.debug("Enable Media Sync: %s", enable_media_sync)
if enable_media_sync:
enable_media_sync = self._hass.data[DOMAIN][self._config_entry.entry_id][
ENABLE_MEDIA_SYNC
]
runningMediaSync = self._hass.data[DOMAIN][self._config_entry.entry_id][
"runningMediaSync"
]
LOGGER.warning("Enable Media Sync: %s", enable_media_sync)
if enable_media_sync or runningMediaSync is True:
data = self._hass.data[DOMAIN][self._config_entry.entry_id]
LOGGER.debug("Initial Media Scan: %s", data["initialMediaScanDone"])
LOGGER.debug("Media Sync Available: %s", data["mediaSyncAvailable"])
LOGGER.debug("Download Progress: %s", data["downloadProgress"])
LOGGER.debug("Running media sync: %s", data["runningMediaSync"])
LOGGER.debug("Media Sync Schedueled: %s", data["mediaSyncScheduled"])
LOGGER.debug("Media Sync Ran Once: %s", data["mediaSyncRanOnce"])
LOGGER.warning("Initial Media Scan: %s", data["initialMediaScanDone"])
LOGGER.warning("Media Sync Available: %s", data["mediaSyncAvailable"])
LOGGER.warning("Download Progress: %s", data["downloadProgress"])
LOGGER.warning("Running media sync: %s", data["runningMediaSync"])
LOGGER.warning("Media Sync Schedueled: %s", data["mediaSyncScheduled"])
LOGGER.warning("Media Sync Ran Once: %s", data["mediaSyncRanOnce"])

if not data["initialMediaScanDone"] or (
data["initialMediaScanDone"] and not data["mediaSyncRanOnce"]
Expand Down
1 change: 0 additions & 1 deletion custom_components/tapo_control/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,6 @@
"data": {
"media_view_days_order": "Order of days in Media Browser",
"media_view_recordings_order": "Order of recordings in Media Browser",
"enable_media_sync": "Enable media synchronization",
"media_sync_hours": "Number of hours to keep synchronized",
"media_sync_cold_storage_path": "[Requires restart] Cold storage path"
},
Expand Down
Loading

0 comments on commit af5c872

Please sign in to comment.