From d249c78cf10362efa9e745e5b10c87b50617fb7d Mon Sep 17 00:00:00 2001 From: vahagit <252811289+vahagit@users.noreply.github.com> Date: Sat, 14 Mar 2026 13:08:07 +0000 Subject: [PATCH] Initial upload --- .../camera_record_services/__init__.py | 381 ++++++++++++++++++ .../camera_record_services/brand/icon.png | Bin 0 -> 1493 bytes .../camera_record_services/brand/icon@2x.png | Bin 0 -> 34405 bytes .../camera_record_services/config_flow.py | 39 ++ .../camera_record_services/const.py | 3 + .../camera_record_services/manifest.json | 12 + .../camera_record_services/services.yaml | 66 +++ .../camera_record_services/strings.json | 21 + .../translations/en.json | 21 + 9 files changed, 543 insertions(+) create mode 100644 custom_components/camera_record_services/__init__.py create mode 100644 custom_components/camera_record_services/brand/icon.png create mode 100644 custom_components/camera_record_services/brand/icon@2x.png create mode 100644 custom_components/camera_record_services/config_flow.py create mode 100644 custom_components/camera_record_services/const.py create mode 100644 custom_components/camera_record_services/manifest.json create mode 100644 custom_components/camera_record_services/services.yaml create mode 100644 custom_components/camera_record_services/strings.json create mode 100644 custom_components/camera_record_services/translations/en.json diff --git a/custom_components/camera_record_services/__init__.py b/custom_components/camera_record_services/__init__.py new file mode 100644 index 0000000..0820a67 --- /dev/null +++ b/custom_components/camera_record_services/__init__.py @@ -0,0 +1,381 @@ +""" +Custom integration: camera_record_services (fork-v1-rev11) + +Converted from pyscript script fork-v1-rev11. Provides two services +callable from automations: + + camera_record_services.camera_start_recording + entity_id: camera.my_camera # required + filename: /media/clip.mp4 # required — plain string, template on the HA side + duration: 30 # optional, seconds, default 30 + lookback: 0 # optional, seconds, default 0 + stop_state_id: binary_sensor.motion # optional — entity to watch for early stop + stop_state_value: "off" # optional — value that triggers early stop + # stop_state_id and stop_state_value must be + # provided together or not at all + + camera_record_services.camera_stop_recording + entity_id: camera.my_camera # required + +Both services are best-effort. camera_stop_recording will silently no-op if +no recording is in progress or if the stream does not exist yet. + +No custom state entities are created or maintained by this integration. + +Installation +------------ +Place this directory at: + /custom_components/camera_record_services/ + +Then restart HA and add the integration via Settings → Integrations → Add Integration. +""" + +import asyncio +import logging +import os + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.helpers.typing import ConfigType + +_LOGGER = logging.getLogger(__name__) + +from .const import DOMAIN + +# The string value of stream.const.RECORDER_PROVIDER. +# We import it at runtime so that if the constant is ever renamed we get a +# clean ImportError rather than a silent wrong-key miss. The fallback keeps +# things working on older HA versions that already had the string as "recorder". +try: + from homeassistant.components.stream.const import RECORDER_PROVIDER as _RECORDER_PROVIDER +except ImportError: + _RECORDER_PROVIDER = "recorder" + + +# Minimum valid recording file size in bytes. Files smaller than this contain +# only the MP4 container header with no actual video data — indicating the +# stream failed to capture anything meaningful. +_MIN_RECORDING_SIZE = 1024 + + +def _get_camera(hass, entity_id): + """Return the Camera entity object, or None if not found.""" + component = hass.data.get("camera") + if component is None: + _LOGGER.error("camera_record_services: camera component not loaded") + return None + camera = component.get_entity(entity_id) + if camera is None: + _LOGGER.error( + "camera_record_services: entity '%s' not found in camera component", + entity_id, + ) + return camera + + +async def _check_recording_file(filename): + """Validate that a recording file exists and contains real video data. + + Waits up to 5 seconds (1 second at a time) for the final file to appear + and reach a valid size — covering cameras that are slow to finalise the + MP4 after async_record returns. + + If the final file is still not found after 5 attempts, checks for a .tmp + version left behind by a failed stream rename and renames it if present + (6.7 safety fix — only renames if the final file does NOT already exist). + + In all cases, checks the file size is above _MIN_RECORDING_SIZE to + confirm actual video data was written. + + Returns True if a valid recording file is confirmed, False otherwise. + """ + for attempt in range(5): + await asyncio.sleep(1) + if os.path.isfile(filename): + size = os.path.getsize(filename) + if size > _MIN_RECORDING_SIZE: + return True + + # Final file not found after 5 attempts — try .tmp fallback. + tmp_path = filename + ".tmp" + if not os.path.isfile(filename) and os.path.isfile(tmp_path): + try: + os.rename(tmp_path, filename) + except OSError as err: + _LOGGER.error( + "camera_record_services: failed to rename .tmp file '%s': %s", + tmp_path, + err, + ) + return False + if os.path.isfile(filename): + size = os.path.getsize(filename) + if size > _MIN_RECORDING_SIZE: + return True + _LOGGER.warning( + "camera_record_services: renamed .tmp file '%s' is too small (%d bytes) " + "— no real video data was captured", + filename, + size, + ) + return False + + if not os.path.isfile(filename) and not os.path.isfile(tmp_path): + _LOGGER.warning( + "camera_record_services: neither '%s' nor '%s' found after 5 seconds " + "— stream object may have failed silently", + filename, + tmp_path, + ) + + return False + + +async def camera_start_recording(hass, entity_id=None, filename=None, duration=30, lookback=0, stop_state_id=None, stop_state_value=None, extend_running_task=True): + """Start recording a camera stream to a file. + + Replicates camera.record but registers under camera_record_services so it + can be extended alongside camera_stop_recording without touching HA core. + """ + _LOGGER.info("camera_start_recording: called with entity_id='%s' filename='%s' duration=%s lookback=%s stop_state_id='%s' stop_state_value='%s' extend_running_task=%s", entity_id, filename, duration, lookback, stop_state_id, stop_state_value, extend_running_task) + if entity_id is None or filename is None: + _LOGGER.error("camera_start_recording: 'entity_id' and 'filename' are both required") + return + + if (stop_state_id is None) != (stop_state_value is None): + _LOGGER.error( + "camera_start_recording: 'stop_state_id' and 'stop_state_value' must both be " + "provided together or not at all" + ) + return + + camera = _get_camera(hass, entity_id) + if camera is None: + return + + # async_create_stream returns the existing Stream if already alive, + # or creates a new one. It mirrors what camera.__init__ does in + # async_handle_record_service. + stream = await camera.async_create_stream() + if stream is None: + _LOGGER.error( + "camera_start_recording: could not create stream for '%s' — " + "does the camera support streaming?", + entity_id, + ) + return + + # If a recording is already in progress, optionally extend it and return early. + recorder_output = stream.outputs().get(_RECORDER_PROVIDER) + if recorder_output is not None: + if extend_running_task: + try: + # --- OPTIMISTIC CALL --- + # Optimistically try to extend the timer. + recorder_output.idle_timer.awake() + _LOGGER.info( + "camera_start_recording: '%s' is already recording to '%s' - this task has been extended. " + "New request ignored: filename='%s' duration=%s lookback=%s stop_state_id='%s' stop_state_value='%s'", + entity_id, recorder_output.video_path, filename, duration, lookback, stop_state_id, stop_state_value, + ) + except Exception as err: + # --- ERROR DIAGNOSIS --- + # An error occurred. Let's figure out if it's because the API changed + # or if something else went wrong. + + # 1. Check for the 'idle_timer' attribute + timer_exists = hasattr(recorder_output, 'idle_timer') + + # 2. Check for the 'awake' method + # (Use nested getattr with defaults to ensure this check never crashes) + method_exists = callable( + getattr(getattr(recorder_output, 'idle_timer', None), 'awake', None) + ) + if not timer_exists or not method_exists: + # The internal API has changed/removed. + # We include the diagnostic details in the error message. + raise HomeAssistantError( + f"Cannot extend recording: Internal API mismatch. " + f"idle_timer found: {timer_exists}, awake method found: {method_exists}. " + f"The Stream component structure has likely changed and this integration needs an update." + ) from err + + # The API exists, so the error was a genuine runtime error (e.g. inside awake()). + # Log the technical details and re-raise the original exception. + _LOGGER.error( + "camera_start_recording: Failed to extend recording for '%s' due to an internal error: %s", + entity_id, + err + ) + raise + return + + try: + _LOGGER.info( + "camera_start_recording: starting recording '%s' → %s (duration=%ss, lookback=%ss)", + entity_id, + filename, + duration, + lookback, + ) + if stop_state_id is not None: + # Run recording as a background task so we can race it against + # the stop-state condition. + record_task = asyncio.create_task( + stream.async_record( + video_path=str(filename), + duration=int(duration), + lookback=int(lookback), + ) + ) + # Use async_track_state_change_event and an asyncio.Event to watch + # for the stop-state condition. + async def _stop_state_watcher(): + event = asyncio.Event() + + @callback + def _state_changed(event_obj): + new_state = event_obj.data.get("new_state") + if new_state and new_state.state == str(stop_state_value): + event.set() + + unsub = async_track_state_change_event(hass, [stop_state_id], _state_changed) + try: + await event.wait() + finally: + unsub() + + watcher_task = asyncio.create_task(_stop_state_watcher()) + # Wait for whichever completes first. + done, _ = await asyncio.wait( + [record_task, watcher_task], + return_when=asyncio.FIRST_COMPLETED, + ) + if record_task not in done: + # Stop-state fired before duration expired — stop the recording. + _LOGGER.info("camera_start_recording: stop-state fired — cancelling watcher and stopping recording") + watcher_task.cancel() + recorder_output = stream.outputs().get(_RECORDER_PROVIDER) + if recorder_output is not None: + await stream.remove_provider(recorder_output) + # Wait for record_task to finish cleanly after the stop signal. + await record_task + else: + # Duration expired naturally — cancel the watcher. + watcher_task.cancel() + else: + # No stop condition — await directly. + await stream.async_record( + video_path=str(filename), + duration=int(duration), + lookback=int(lookback), + ) + await _check_recording_file(str(filename)) + _LOGGER.info( + "camera_start_recording: recording completed for '%s' → %s", + entity_id, + filename, + ) + except Exception as err: # HomeAssistantError, OSError, etc. + _LOGGER.error( + "camera_start_recording: failed to start recording for '%s': %s", + entity_id, + err, + ) + + +async def camera_stop_recording(hass, entity_id=None): + """Stop an in-progress camera recording. + + This is best-effort: + - If no stream exists, silently returns. + - If no recording is in progress, silently returns. + - The actual file will be finalised at the next segment boundary, + identical to normal end-of-duration behaviour. + - There is no guarantee that anything was recorded. + """ + _LOGGER.info("camera_stop_recording: called with entity_id='%s'", entity_id) + if entity_id is None: + _LOGGER.error("camera_stop_recording: 'entity_id' is required") + return + + camera = _get_camera(hass, entity_id) + if camera is None: + return + + # camera.stream is None until the first stream is created. + stream = getattr(camera, "stream", None) + if stream is None: + _LOGGER.debug( + "camera_stop_recording: '%s' has no active stream, nothing to stop", + entity_id, + ) + return + + # Use the public outputs() method (returns a MappingProxyType copy of + # the internal outputs dict) rather than accessing _outputs directly. + recorder_output = stream.outputs().get(_RECORDER_PROVIDER) + + if recorder_output is None: + _LOGGER.debug( + "camera_stop_recording: no recording in progress for '%s'", + entity_id, + ) + return + + try: + # remove_provider signals the recorder_save_worker thread, drains + # its queue, finalises the MP4 container and flushes to disk — + # exactly the same teardown path as normal end-of-duration. + await stream.remove_provider(recorder_output) + await _check_recording_file(recorder_output.video_path) + _LOGGER.info( + "camera_stop_recording: recording stopped for '%s'", entity_id + ) + except Exception as err: + _LOGGER.error( + "camera_stop_recording: error while stopping recording for '%s': %s", + entity_id, + err, + ) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the camera_record_services integration.""" + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up camera_record_services from a config entry.""" + + async def handle_camera_start_recording(call: ServiceCall) -> None: + await camera_start_recording( + hass, + entity_id=call.data.get("entity_id"), + filename=call.data.get("filename"), + duration=call.data.get("duration", 30), + lookback=call.data.get("lookback", 0), + stop_state_id=call.data.get("stop_state_id"), + stop_state_value=call.data.get("stop_state_value"), + extend_running_task=call.data.get("extend_running_task", True), + ) + + async def handle_camera_stop_recording(call: ServiceCall) -> None: + await camera_stop_recording( + hass, + entity_id=call.data.get("entity_id"), + ) + + hass.services.async_register(DOMAIN, "camera_start_recording", handle_camera_start_recording) + hass.services.async_register(DOMAIN, "camera_stop_recording", handle_camera_stop_recording) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + hass.services.async_remove(DOMAIN, "camera_start_recording") + hass.services.async_remove(DOMAIN, "camera_stop_recording") + return True diff --git a/custom_components/camera_record_services/brand/icon.png b/custom_components/camera_record_services/brand/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..85939705c427e81a1263533f5defb970f8e32f7c GIT binary patch literal 1493 zcmbW1X;4#F6vyv-R6%GBg0hq$Y>JMEK`0nvNE&NL`=wucKm6~RbI-Xm=bm$a zXE=dAi!mz<0I=B4*P9CfWH2Fsk_}sUe8oNhkODt%&o2`Hm>9Spu*{Q+bRfZq?{J0{ zE8VHndq2%S(~8T!h;q3`Hs11sIjgqN)A1h)FT5HnoG$FG&sG)Qh)HU6G2HflS4Vg5 zrjC2%aY2gf&)bWtqjouC#Qs~Mq=fay(9A)dBgky#Ie|>M&LFpV&VU8sOKSBP-~a*! zR3I``0O$rUco5Q z^lZNmPx{;%zZwmxBZrQL)&i~Rq{ev%kn_zf$A2v<3jR1QNLqXH`E>p7$9>c(%}Vp1 zH4Ayhuc@B56{rxzBTpju7P@a%Y;L8q^t6PIWgJKqY{(7##ExC*CyOZ zxXz`%;QWn~x|aT~1wAAxux>u?JEWWUh>1OK#sNpf`&uRZz2{ooh@mR|+`w4zL#)0# z(%B4Ql}tumaLprN{4%39d?t)pZV0#AK2x$GRh2YTw@w73^%|eLi@Mwi2J&cZCsFLI zkLmAn{;hv&-_Ep_ao%Xgg;+E`8n>15nWM~!_QjsQVH=pG85CSDn96xFJ{P`55OkR$ zW6MZS=1-G5-T5IzddQs^{_+PxKqe;)f@>*k-eAl6QbvP)1wd=27Sb`=T%B}cGqi0! z09re0i+4^+C7-D@`hf>V=B$w=e_ov=&&}*MKLv}n(g%|ol#0`^sH?3m#?nXWPDNOR zpS+Y<0;PjbVL(oHgVSJ;7WWuaw2)TX6}qO+3p zCX_k#*JAH9<>M1!%wqz-W`4%%0XOszW@48W>~o+>3eysMe>z}{;hMu6{GT?oiva!R z)Q-8BX;+qrHWMCj{a{>>K#4FQlKYaUjaCai9EhP6N3#jG@)L8JC3AsEgYW8qr9#ja z1g;vb(?m&~&gldW0Lc-PHdn?gRkk!acdhmz9b7Wy^?(A{-CF3OedWPaY2y?xQf(Xo z!jYgg3sQ^3p{@ubB9&26vpW>K$QX-KYMiPVQhZ6mSf(g%*w(V+0scf^s6H!%Rgq2jv(!~tTzFWCaA0{g3A2z% zNK~jc-*ykvEqbP?uQdoOQ%B_FcM+?HE_W44PsKHljkS@$ihF5Mjzn+>!G||RU_@<| zboA2OuYRunyX^P<2Y2EjM)%8Lfd9+)js^xmJdb{~QfzJW77~9W%i`g|r(Iq3>i-_o|E! lJuX!M4#t*$@ZaEEJ8fb2k#%8fWqs+JAU}4X_cbp({VyLNR9gT5 literal 0 HcmV?d00001 diff --git a/custom_components/camera_record_services/brand/icon@2x.png b/custom_components/camera_record_services/brand/icon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..eff9c1d535faba5ef600b81681f934610f3ca936 GIT binary patch literal 34405 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 None: + """Initialise the options flow.""" + + async def async_step_init(self, user_input=None): + """Show the usage guide.""" + if user_input is not None: + return self.async_create_entry(title="", data={}) + return self.async_show_form(step_id="init") diff --git a/custom_components/camera_record_services/const.py b/custom_components/camera_record_services/const.py new file mode 100644 index 0000000..44dc56d --- /dev/null +++ b/custom_components/camera_record_services/const.py @@ -0,0 +1,3 @@ +"""Constants for camera_record_services.""" + +DOMAIN = "camera_record_services" diff --git a/custom_components/camera_record_services/manifest.json b/custom_components/camera_record_services/manifest.json new file mode 100644 index 0000000..e790c44 --- /dev/null +++ b/custom_components/camera_record_services/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "camera_record_services", + "name": "Camera Record Services", + "version": "0.9.0", + "documentation": "https://github.com/GITHUB_USERNAME/GITHUB_REPO_NAME", + "homeassistant": "2024.1.1", + "dependencies": ["camera", "stream"], + "codeowners": ["@GITHUB_USERNAME"], + "requirements": [], + "iot_class": "local_push", + "config_flow": true +} diff --git a/custom_components/camera_record_services/services.yaml b/custom_components/camera_record_services/services.yaml new file mode 100644 index 0000000..ae7b8c8 --- /dev/null +++ b/custom_components/camera_record_services/services.yaml @@ -0,0 +1,66 @@ +camera_start_recording: + name: Start recording + description: "Record a camera stream to a file. Stops after the set duration, or earlier if a watched entity reaches the specified state." + fields: + entity_id: + name: Camera + description: Camera to record. + required: true + selector: + entity: + domain: camera + filename: + name: Filename + description: "Full path for the output file, e.g. /media/clips/front.mp4" + required: true + example: "/media/clip.mp4" + selector: + text: + duration: + name: Duration + description: Maximum recording length. Recording stops automatically when reached. + default: 30 + selector: + number: + min: 1 + max: 3600 + unit_of_measurement: seconds + lookback: + name: Lookback + description: "Seconds of pre-recorded buffer to include before this call. Only works if the camera stream is already running. Actual lookback may be less than requested depending on the stream buffer." + default: 0 + selector: + number: + min: 0 + max: 300 + unit_of_measurement: seconds + stop_state_id: + name: Stop entity + description: "Entity to watch for an early stop. Races against duration — whichever occurs first stops the recording. Must be used with stop_state_value." + required: false + selector: + entity: + stop_state_value: + name: Stop state value + description: "State value that triggers the early stop, e.g. off. Must be used with stop_state_id." + required: false + selector: + text: + extend_running_task: + name: Extend if already recording + description: "If the camera is already recording, extend the current recording instead of ignoring this request. If false, the request is silently ignored." + default: true + selector: + boolean: + +camera_stop_recording: + name: Stop recording + description: "Stop an in-progress recording immediately, whether started by this service or by camera.record. Does nothing if the camera is not recording." + fields: + entity_id: + name: Camera + description: Camera to stop recording. + required: true + selector: + entity: + domain: camera diff --git a/custom_components/camera_record_services/strings.json b/custom_components/camera_record_services/strings.json new file mode 100644 index 0000000..fe9bc25 --- /dev/null +++ b/custom_components/camera_record_services/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "step": { + "user": { + "title": "Camera Record Services", + "description": "Add the Camera Record Services integration. No configuration is required." + } + }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + } + }, + "options": { + "step": { + "init": { + "title": "Usage Guide", + "description": "## camera_record_services.camera_start_recording\n\nStart recording a camera stream to a file.\n\n| Parameter | Required | Default | Description |\n|---|---|---|---|\n| `entity_id` | yes | — | Camera entity to record, e.g. `camera.front_door` |\n| `filename` | yes | — | Full path for the output file, e.g. `/media/clip.mp4` |\n| `duration` | no | `30` | Maximum recording length in seconds |\n| `lookback` | no | `0` | Seconds of stream buffer to include before the call |\n| `stop_state_id` | no | — | Entity to watch for an early-stop trigger |\n| `stop_state_value` | no | — | State value that triggers the early stop |\n| `extend_running_task` | no | `true` | Extend current recording if already in progress |\n\n`stop_state_id` and `stop_state_value` must be provided together or not at all.\n\n---\n\n## camera_record_services.camera_stop_recording\n\nStop an in-progress recording immediately.\n\n| Parameter | Required | Description |\n|---|---|---|\n| `entity_id` | yes | Camera entity whose recording should be stopped |\n\nBest-effort — silently does nothing if no recording is in progress." + } + } + } +} diff --git a/custom_components/camera_record_services/translations/en.json b/custom_components/camera_record_services/translations/en.json new file mode 100644 index 0000000..764ea83 --- /dev/null +++ b/custom_components/camera_record_services/translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "step": { + "user": { + "title": "Camera Record Services", + "description": "Add the Camera Record Services integration. No configuration is required." + } + }, + "abort": { + "single_instance_allowed": "Already configured. Only a single configuration is possible." + } + }, + "options": { + "step": { + "init": { + "title": "Usage Guide", + "description": "## camera_record_services.camera_start_recording\n\nStart recording a camera stream to a file.\n\n| Parameter | Required | Default | Description |\n|---|---|---|---|\n| `entity_id` | yes | — | Camera entity to record, e.g. `camera.front_door` |\n| `filename` | yes | — | Full path for the output file, e.g. `/media/clip.mp4` |\n| `duration` | no | `30` | Maximum recording length in seconds |\n| `lookback` | no | `0` | Seconds of stream buffer to include before the call |\n| `stop_state_id` | no | — | Entity to watch for an early-stop trigger |\n| `stop_state_value` | no | — | State value that triggers the early stop |\n| `extend_running_task` | no | `true` | Extend current recording if already in progress |\n\n`stop_state_id` and `stop_state_value` must be provided together or not at all.\n\n---\n\n## camera_record_services.camera_stop_recording\n\nStop an in-progress recording immediately.\n\n| Parameter | Required | Description |\n|---|---|---|\n| `entity_id` | yes | Camera entity whose recording should be stopped |\n\nBest-effort — silently does nothing if no recording is in progress." + } + } + } +}