From a6d8ba93056c1a4e243d56da447e241b2504fae2 Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Sat, 10 Jun 2023 23:20:37 +0300 Subject: move files again --- include/py/homekit/camera/__init__.py | 1 + include/py/homekit/camera/esp32.py | 226 ++++++++++++++++++++++++++++++++++ include/py/homekit/camera/types.py | 5 + include/py/homekit/camera/util.py | 107 ++++++++++++++++ 4 files changed, 339 insertions(+) create mode 100644 include/py/homekit/camera/__init__.py create mode 100644 include/py/homekit/camera/esp32.py create mode 100644 include/py/homekit/camera/types.py create mode 100644 include/py/homekit/camera/util.py (limited to 'include/py/homekit/camera') diff --git a/include/py/homekit/camera/__init__.py b/include/py/homekit/camera/__init__.py new file mode 100644 index 0000000..626930b --- /dev/null +++ b/include/py/homekit/camera/__init__.py @@ -0,0 +1 @@ +from .types import CameraType \ No newline at end of file diff --git a/include/py/homekit/camera/esp32.py b/include/py/homekit/camera/esp32.py new file mode 100644 index 0000000..fe6de0e --- /dev/null +++ b/include/py/homekit/camera/esp32.py @@ -0,0 +1,226 @@ +import logging +import requests +import json +import asyncio +import aioshutil + +from io import BytesIO +from functools import partial +from typing import Union, Optional +from enum import Enum +from ..api.errors import ApiResponseError +from ..util import Addr + + +class FrameSize(Enum): + UXGA_1600x1200 = 13 + SXGA_1280x1024 = 12 + HD_1280x720 = 11 + XGA_1024x768 = 10 + SVGA_800x600 = 9 + VGA_640x480 = 8 + HVGA_480x320 = 7 + CIF_400x296 = 6 + QVGA_320x240 = 5 + N_240x240 = 4 + HQVGA_240x176 = 3 + QCIF_176x144 = 2 + QQVGA_160x120 = 1 + N_96x96 = 0 + + +class WBMode(Enum): + AUTO = 0 + SUNNY = 1 + CLOUDY = 2 + OFFICE = 3 + HOME = 4 + + +def _assert_bounds(n: int, min: int, max: int): + if not min <= n <= max: + raise ValueError(f'value must be between {min} and {max}') + + +class WebClient: + def __init__(self, + addr: Addr): + self.endpoint = f'http://{addr[0]}:{addr[1]}' + self.logger = logging.getLogger(self.__class__.__name__) + self.delay = 0 + self.isfirstrequest = True + + async def syncsettings(self, settings) -> bool: + status = await self.getstatus() + self.logger.debug(f'syncsettings: status={status}') + + changed_anything = False + + for name, value in settings.items(): + server_name = name + if name == 'aec_dsp': + server_name = 'aec2' + + if server_name not in status: + # legacy compatibility + if server_name != 'vflip': + self.logger.warning(f'syncsettings: field `{server_name}` not found in camera status') + continue + + try: + # server returns 0 or 1 for bool values + if type(value) is bool: + value = int(value) + + if status[server_name] == value: + continue + except KeyError as exc: + if name != 'vflip': + self.logger.error(exc) + + try: + # fix for cases like when field is called raw_gma, but method is setrawgma() + name = name.replace('_', '') + + func = getattr(self, f'set{name}') + self.logger.debug(f'syncsettings: calling set{name}({value})') + + await func(value) + + changed_anything = True + except AttributeError as exc: + self.logger.exception(exc) + self.logger.error(f'syncsettings: method set{name}() not found') + + return changed_anything + + def setdelay(self, delay: int): + self.delay = delay + + async def capture(self, output: Optional[str] = None) -> Union[BytesIO, bool]: + kw = {} + if output: + kw['save_to'] = output + else: + kw['as_bytes'] = True + return await self._call('capture', **kw) + + async def getstatus(self): + return json.loads(await self._call('status')) + + async def setflash(self, enable: bool): + await self._control('flash', int(enable)) + + async def setframesize(self, fs: Union[int, FrameSize]): + if type(fs) is int: + fs = FrameSize(fs) + await self._control('framesize', fs.value) + + async def sethmirror(self, enable: bool): + await self._control('hmirror', int(enable)) + + async def setvflip(self, enable: bool): + await self._control('vflip', int(enable)) + + async def setawb(self, enable: bool): + await self._control('awb', int(enable)) + + async def setawbgain(self, enable: bool): + await self._control('awb_gain', int(enable)) + + async def setwbmode(self, mode: WBMode): + await self._control('wb_mode', mode.value) + + async def setaecsensor(self, enable: bool): + await self._control('aec', int(enable)) + + async def setaecdsp(self, enable: bool): + await self._control('aec2', int(enable)) + + async def setagc(self, enable: bool): + await self._control('agc', int(enable)) + + async def setagcgain(self, gain: int): + _assert_bounds(gain, 1, 31) + await self._control('agc_gain', gain) + + async def setgainceiling(self, gainceiling: int): + _assert_bounds(gainceiling, 2, 128) + await self._control('gainceiling', gainceiling) + + async def setbpc(self, enable: bool): + await self._control('bpc', int(enable)) + + async def setwpc(self, enable: bool): + await self._control('wpc', int(enable)) + + async def setrawgma(self, enable: bool): + await self._control('raw_gma', int(enable)) + + async def setlenscorrection(self, enable: bool): + await self._control('lenc', int(enable)) + + async def setdcw(self, enable: bool): + await self._control('dcw', int(enable)) + + async def setcolorbar(self, enable: bool): + await self._control('colorbar', int(enable)) + + async def setquality(self, q: int): + _assert_bounds(q, 4, 63) + await self._control('quality', q) + + async def setbrightness(self, brightness: int): + _assert_bounds(brightness, -2, -2) + await self._control('brightness', brightness) + + async def setcontrast(self, contrast: int): + _assert_bounds(contrast, -2, 2) + await self._control('contrast', contrast) + + async def setsaturation(self, saturation: int): + _assert_bounds(saturation, -2, 2) + await self._control('saturation', saturation) + + async def _control(self, var: str, value: Union[int, str]): + return await self._call('control', params={'var': var, 'val': value}) + + async def _call(self, + method: str, + params: Optional[dict] = None, + save_to: Optional[str] = None, + as_bytes=False) -> Union[str, bool, BytesIO]: + loop = asyncio.get_event_loop() + + if not self.isfirstrequest and self.delay > 0: + sleeptime = self.delay / 1000 + self.logger.debug(f'sleeping for {sleeptime}') + + await asyncio.sleep(sleeptime) + + self.isfirstrequest = False + + url = f'{self.endpoint}/{method}' + self.logger.debug(f'calling {url}, params: {params}') + + kwargs = {} + if params: + kwargs['params'] = params + if save_to: + kwargs['stream'] = True + + r = await loop.run_in_executor(None, + partial(requests.get, url, **kwargs)) + if r.status_code != 200: + raise ApiResponseError(status_code=r.status_code) + + if as_bytes: + return BytesIO(r.content) + + if save_to: + r.raise_for_status() + with open(save_to, 'wb') as f: + await aioshutil.copyfileobj(r.raw, f) + return True + + return r.text diff --git a/include/py/homekit/camera/types.py b/include/py/homekit/camera/types.py new file mode 100644 index 0000000..de59022 --- /dev/null +++ b/include/py/homekit/camera/types.py @@ -0,0 +1,5 @@ +from enum import Enum + + +class CameraType(Enum): + ESP32 = 'esp32' diff --git a/include/py/homekit/camera/util.py b/include/py/homekit/camera/util.py new file mode 100644 index 0000000..97f35aa --- /dev/null +++ b/include/py/homekit/camera/util.py @@ -0,0 +1,107 @@ +import asyncio +import os.path +import logging +import psutil + +from typing import List, Tuple +from ..util import chunks +from ..config import config + +_logger = logging.getLogger(__name__) +_temporary_fixing = '.temporary_fixing.mp4' + + +def _get_ffmpeg_path() -> str: + return 'ffmpeg' if 'ffmpeg' not in config else config['ffmpeg']['path'] + + +def time2seconds(time: str) -> int: + time, frac = time.split('.') + frac = int(frac) + + h, m, s = [int(i) for i in time.split(':')] + + return round(s + m*60 + h*3600 + frac/1000) + + +async def ffmpeg_recreate(filename: str): + filedir = os.path.dirname(filename) + tempname = os.path.join(filedir, _temporary_fixing) + mtime = os.path.getmtime(filename) + + args = [_get_ffmpeg_path(), '-nostats', '-loglevel', 'error', '-i', filename, '-c', 'copy', '-y', tempname] + proc = await asyncio.create_subprocess_exec(*args, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE) + stdout, stderr = await proc.communicate() + if proc.returncode != 0: + _logger.error(f'fix_timestamps({filename}): ffmpeg returned {proc.returncode}, stderr: {stderr.decode().strip()}') + + if os.path.isfile(tempname): + os.unlink(filename) + os.rename(tempname, filename) + os.utime(filename, (mtime, mtime)) + _logger.info(f'fix_timestamps({filename}): OK') + else: + _logger.error(f'fix_timestamps({filename}): temp file \'{tempname}\' does not exists, fix failed') + + +async def ffmpeg_cut(input: str, + output: str, + start_pos: int, + duration: int): + args = [_get_ffmpeg_path(), '-nostats', '-loglevel', 'error', '-i', input, + '-ss', str(start_pos), '-t', str(duration), + '-c', 'copy', '-y', output] + proc = await asyncio.create_subprocess_exec(*args, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE) + stdout, stderr = await proc.communicate() + if proc.returncode != 0: + _logger.error(f'ffmpeg_cut({input}, start_pos={start_pos}, duration={duration}): ffmpeg returned {proc.returncode}, stderr: {stderr.decode().strip()}') + else: + _logger.info(f'ffmpeg_cut({input}): OK') + + +def dvr_scan_timecodes(timecodes: str) -> List[Tuple[int, int]]: + tc_backup = timecodes + + timecodes = timecodes.split(',') + if len(timecodes) % 2 != 0: + raise DVRScanInvalidTimecodes(f'invalid number of timecodes. input: {tc_backup}') + + timecodes = list(map(time2seconds, timecodes)) + timecodes = list(chunks(timecodes, 2)) + + # sort out invalid fragments (dvr-scan returns them sometimes, idk why...) + timecodes = list(filter(lambda f: f[0] < f[1], timecodes)) + if not timecodes: + raise DVRScanInvalidTimecodes(f'no valid timecodes. input: {tc_backup}') + + # https://stackoverflow.com/a/43600953 + timecodes.sort(key=lambda interval: interval[0]) + merged = [timecodes[0]] + for current in timecodes: + previous = merged[-1] + if current[0] <= previous[1]: + previous[1] = max(previous[1], current[1]) + else: + merged.append(current) + + return merged + + +class DVRScanInvalidTimecodes(Exception): + pass + + +def has_handle(fpath): + for proc in psutil.process_iter(): + try: + for item in proc.open_files(): + if fpath == item.path: + return True + except Exception: + pass + + return False \ No newline at end of file -- cgit v1.2.3 From 62ee71fdb0eb07adbf0071103617aa96c993fe22 Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Sun, 11 Jun 2023 05:03:43 +0300 Subject: ipcam: start porting to new config and multiserver scheme --- include/py/homekit/camera/config.py | 82 +++++++++++++++++++++++++++++++++++++ include/py/homekit/camera/types.py | 12 ++++++ 2 files changed, 94 insertions(+) create mode 100644 include/py/homekit/camera/config.py (limited to 'include/py/homekit/camera') diff --git a/include/py/homekit/camera/config.py b/include/py/homekit/camera/config.py new file mode 100644 index 0000000..e0891a6 --- /dev/null +++ b/include/py/homekit/camera/config.py @@ -0,0 +1,82 @@ +from ..config import ConfigUnit, LinuxBoardsConfig +from typing import Optional +from .types import CameraType, VideoContainerType, VideoCodecType + + +_lbc = LinuxBoardsConfig() + + +def _validate_roi_line(field, value, error) -> bool: + p = value.split(' ') + if len(p) != 4: + error(field, f'{field}: must contain four coordinates separated by space') + for n in p: + if not n.isnumeric(): + error(field, f'{field}: invalid coordinates (not a number)') + return True + + +class IpcamConfig(ConfigUnit): + NAME = 'ipcam' + + @classmethod + def schema(cls) -> Optional[dict]: + lbc = LinuxBoardsConfig() + return { + 'cams': { + 'type': 'dict', + 'keysrules': {'type': ['string', 'integer']}, + 'valuesrules': { + 'type': 'dict', + 'schema': { + 'type': {'type': 'string', 'allowed': [t.value for t in CameraType], 'required': True}, + 'codec': {'type': 'string', 'allowed': [t.value for t in VideoCodecType], 'required': True}, + 'container': {'type': 'string', 'allowed': [t.value for t in VideoContainerType], 'required': True}, + 'server': {'type': 'string', 'allowed': list(lbc.get().keys()), 'required': True}, + 'disk': {'type': 'integer', 'required': True}, + 'motion': { + 'type': 'dict', + 'schema': { + 'threshold': {'type': ['float', 'integer']}, + 'roi': { + 'type': 'list', + 'schema': {'type': 'string', 'check_with': _validate_roi_line} + } + } + } + } + } + }, + 'motion_padding': {'type': 'integer', 'required': True}, + 'motion_telegram': {'type': 'boolean', 'required': True}, + 'fix_interval': {'type': 'integer', 'required': True}, + 'fix_enabled': {'type': 'boolean', 'required': True}, + 'cleanup_min_gb': {'type': 'integer', 'required': True}, + 'cleanup_interval': {'type': 'integer', 'required': True}, + + # TODO FIXME + 'fragment_url_templates': cls._url_templates_schema(), + 'original_file_url_templates': cls._url_templates_schema() + } + + @staticmethod + def custom_validator(data): + for n, cam in data['cams'].items(): + linux_box = _lbc[cam['server']] + if 'ext_hdd' not in linux_box: + raise ValueError(f'cam-{n}: linux box {cam["server"]} must have ext_hdd defined') + disk = cam['disk']-1 + if disk < 0 or disk >= len(linux_box['ext_hdd']): + raise ValueError(f'cam-{n}: invalid disk index for linux box {cam["server"]}') + + @classmethod + def _url_templates_schema(cls) -> dict: + return { + 'type': 'list', + 'empty': False, + 'schema': { + 'type': 'list', + 'empty': False, + 'schema': {'type': 'string'} + } + } \ No newline at end of file diff --git a/include/py/homekit/camera/types.py b/include/py/homekit/camera/types.py index de59022..0d3a384 100644 --- a/include/py/homekit/camera/types.py +++ b/include/py/homekit/camera/types.py @@ -3,3 +3,15 @@ from enum import Enum class CameraType(Enum): ESP32 = 'esp32' + ALIEXPRESS_NONAME = 'ali' + HIKVISION = 'hik' + + +class VideoContainerType(Enum): + MP4 = 'mp4' + MOV = 'mov' + + +class VideoCodecType(Enum): + H264 = 'h264' + H265 = 'h265' -- cgit v1.2.3 From 26bd30dff41f5f0e3857283155362a96c47ab9bb Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Sun, 11 Jun 2023 05:04:41 +0300 Subject: minor fix --- include/py/homekit/camera/config.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'include/py/homekit/camera') diff --git a/include/py/homekit/camera/config.py b/include/py/homekit/camera/config.py index e0891a6..331e595 100644 --- a/include/py/homekit/camera/config.py +++ b/include/py/homekit/camera/config.py @@ -21,7 +21,6 @@ class IpcamConfig(ConfigUnit): @classmethod def schema(cls) -> Optional[dict]: - lbc = LinuxBoardsConfig() return { 'cams': { 'type': 'dict', @@ -32,7 +31,7 @@ class IpcamConfig(ConfigUnit): 'type': {'type': 'string', 'allowed': [t.value for t in CameraType], 'required': True}, 'codec': {'type': 'string', 'allowed': [t.value for t in VideoCodecType], 'required': True}, 'container': {'type': 'string', 'allowed': [t.value for t in VideoContainerType], 'required': True}, - 'server': {'type': 'string', 'allowed': list(lbc.get().keys()), 'required': True}, + 'server': {'type': 'string', 'allowed': list(_lbc.get().keys()), 'required': True}, 'disk': {'type': 'integer', 'required': True}, 'motion': { 'type': 'dict', -- cgit v1.2.3 From 3da04de6fd83bca19447a865bf84b3403a14e0c1 Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Sun, 11 Jun 2023 05:06:28 +0300 Subject: ipcam/config: fix schema validation --- include/py/homekit/camera/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'include/py/homekit/camera') diff --git a/include/py/homekit/camera/config.py b/include/py/homekit/camera/config.py index 331e595..0d4c747 100644 --- a/include/py/homekit/camera/config.py +++ b/include/py/homekit/camera/config.py @@ -65,7 +65,7 @@ class IpcamConfig(ConfigUnit): if 'ext_hdd' not in linux_box: raise ValueError(f'cam-{n}: linux box {cam["server"]} must have ext_hdd defined') disk = cam['disk']-1 - if disk < 0 or disk >= len(linux_box['ext_hdd']): + if disk < 1 or disk >= len(linux_box['ext_hdd']): raise ValueError(f'cam-{n}: invalid disk index for linux box {cam["server"]}') @classmethod -- cgit v1.2.3 From 08e736c48990bec0423f72cf52dd1a457e9f9590 Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Sun, 11 Jun 2023 05:06:47 +0300 Subject: Revert "ipcam/config: fix schema validation" This reverts commit 3da04de6fd83bca19447a865bf84b3403a14e0c1. --- include/py/homekit/camera/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'include/py/homekit/camera') diff --git a/include/py/homekit/camera/config.py b/include/py/homekit/camera/config.py index 0d4c747..331e595 100644 --- a/include/py/homekit/camera/config.py +++ b/include/py/homekit/camera/config.py @@ -65,7 +65,7 @@ class IpcamConfig(ConfigUnit): if 'ext_hdd' not in linux_box: raise ValueError(f'cam-{n}: linux box {cam["server"]} must have ext_hdd defined') disk = cam['disk']-1 - if disk < 1 or disk >= len(linux_box['ext_hdd']): + if disk < 0 or disk >= len(linux_box['ext_hdd']): raise ValueError(f'cam-{n}: invalid disk index for linux box {cam["server"]}') @classmethod -- cgit v1.2.3 From e97f98e5e27a6df3827564cce594f27f18c89267 Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Wed, 14 Jun 2023 14:06:26 +0300 Subject: wip --- include/py/homekit/camera/__init__.py | 3 +- include/py/homekit/camera/config.py | 57 ++++++++++++++++++++++++++-- include/py/homekit/camera/types.py | 29 +++++++++++++++ include/py/homekit/camera/util.py | 70 +++++++++++++++++++++++++++++++++-- 4 files changed, 150 insertions(+), 9 deletions(-) (limited to 'include/py/homekit/camera') diff --git a/include/py/homekit/camera/__init__.py b/include/py/homekit/camera/__init__.py index 626930b..4875031 100644 --- a/include/py/homekit/camera/__init__.py +++ b/include/py/homekit/camera/__init__.py @@ -1 +1,2 @@ -from .types import CameraType \ No newline at end of file +from .types import CameraType, VideoContainerType, VideoCodecType, CaptureType +from .config import IpcamConfig \ No newline at end of file diff --git a/include/py/homekit/camera/config.py b/include/py/homekit/camera/config.py index 331e595..c7dbc38 100644 --- a/include/py/homekit/camera/config.py +++ b/include/py/homekit/camera/config.py @@ -1,8 +1,9 @@ +import socket + from ..config import ConfigUnit, LinuxBoardsConfig from typing import Optional from .types import CameraType, VideoContainerType, VideoCodecType - _lbc = LinuxBoardsConfig() @@ -42,7 +43,8 @@ class IpcamConfig(ConfigUnit): 'schema': {'type': 'string', 'check_with': _validate_roi_line} } } - } + }, + 'rtsp_tcp': {'type': 'boolean'} } } }, @@ -55,7 +57,19 @@ class IpcamConfig(ConfigUnit): # TODO FIXME 'fragment_url_templates': cls._url_templates_schema(), - 'original_file_url_templates': cls._url_templates_schema() + 'original_file_url_templates': cls._url_templates_schema(), + + 'hls_path': {'type': 'string', 'required': True}, + 'motion_processing_tmpfs_path': {'type': 'string', 'required': True}, + + 'rtsp_creds': { + 'required': True, + 'type': 'dict', + 'schema': { + 'login': {'type': 'string', 'required': True}, + 'password': {'type': 'string', 'required': True}, + } + } } @staticmethod @@ -78,4 +92,39 @@ class IpcamConfig(ConfigUnit): 'empty': False, 'schema': {'type': 'string'} } - } \ No newline at end of file + } + + def get_all_cam_names(self, + filter_by_server: Optional[str] = None, + filter_by_disk: Optional[int] = None) -> list[int]: + cams = [] + if filter_by_server is not None and filter_by_server not in _lbc: + raise ValueError(f'invalid filter_by_server: {filter_by_server} not found in {_lbc.__class__.__name__}') + for cam, params in self['cams'].items(): + if filter_by_server is None or params['server'] == filter_by_server: + if filter_by_disk is None or params['disk'] == filter_by_disk: + cams.append(int(cam)) + return cams + + def get_all_cam_names_for_this_server(self, + filter_by_disk: Optional[int] = None): + return self.get_all_cam_names(filter_by_server=socket.gethostname(), + filter_by_disk=filter_by_disk) + + def get_cam_server_and_disk(self, cam: int) -> tuple[str, int]: + return self['cams'][cam]['server'], self['cams'][cam]['disk'] + + def get_camera_container(self, cam: int) -> VideoContainerType: + return VideoContainerType(self['cams'][cam]['container']) + + def get_camera_type(self, cam: int) -> CameraType: + return CameraType(self['cams'][cam]['type']) + + def get_rtsp_creds(self) -> tuple[str, str]: + return self['rtsp_creds']['login'], self['rtsp_creds']['password'] + + def should_use_tcp_for_rtsp(self, cam: int) -> bool: + return 'rtsp_tcp' in self['cams'][cam] and self['cams'][cam]['rtsp_tcp'] + + def get_camera_ip(self, camera: int) -> str: + return f'192.168.5.{camera}' diff --git a/include/py/homekit/camera/types.py b/include/py/homekit/camera/types.py index 0d3a384..c313b58 100644 --- a/include/py/homekit/camera/types.py +++ b/include/py/homekit/camera/types.py @@ -6,6 +6,19 @@ class CameraType(Enum): ALIEXPRESS_NONAME = 'ali' HIKVISION = 'hik' + def get_channel_url(self, channel: int) -> str: + if channel not in (1, 2): + raise ValueError(f'channel {channel} is invalid') + if channel == 1: + return '' + elif channel == 2: + if self.value == CameraType.HIKVISION: + return '/Streaming/Channels/2' + elif self.value == CameraType.ALIEXPRESS_NONAME: + return '/?stream=1.sdp' + else: + raise ValueError(f'unsupported camera type {self.value}') + class VideoContainerType(Enum): MP4 = 'mp4' @@ -15,3 +28,19 @@ class VideoContainerType(Enum): class VideoCodecType(Enum): H264 = 'h264' H265 = 'h265' + + +class TimeFilterType(Enum): + FIX = 'fix' + MOTION = 'motion' + MOTION_START = 'motion_start' + + +class TelegramLinkType(Enum): + FRAGMENT = 'fragment' + ORIGINAL_FILE = 'original_file' + + +class CaptureType(Enum): + HLS = 'hls' + RECORD = 'record' diff --git a/include/py/homekit/camera/util.py b/include/py/homekit/camera/util.py index 97f35aa..58c2c70 100644 --- a/include/py/homekit/camera/util.py +++ b/include/py/homekit/camera/util.py @@ -2,13 +2,21 @@ import asyncio import os.path import logging import psutil +import re +from datetime import datetime from typing import List, Tuple from ..util import chunks -from ..config import config +from ..config import config, LinuxBoardsConfig +from .config import IpcamConfig +from .types import VideoContainerType _logger = logging.getLogger(__name__) -_temporary_fixing = '.temporary_fixing.mp4' +_ipcam_config = IpcamConfig() +_lbc_config = LinuxBoardsConfig() + +datetime_format = '%Y-%m-%d-%H.%M.%S' +datetime_format_re = r'\d{4}-\d{2}-\d{2}-\d{2}\.\d{2}.\d{2}' def _get_ffmpeg_path() -> str: @@ -26,7 +34,8 @@ def time2seconds(time: str) -> int: async def ffmpeg_recreate(filename: str): filedir = os.path.dirname(filename) - tempname = os.path.join(filedir, _temporary_fixing) + _, fileext = os.path.splitext(filename) + tempname = os.path.join(filedir, f'.temporary_fixing.{fileext}') mtime = os.path.getmtime(filename) args = [_get_ffmpeg_path(), '-nostats', '-loglevel', 'error', '-i', filename, '-c', 'copy', '-y', tempname] @@ -104,4 +113,57 @@ def has_handle(fpath): except Exception: pass - return False \ No newline at end of file + return False + + +def get_recordings_path(cam: int) -> str: + server, disk = _ipcam_config.get_cam_server_and_disk(cam) + disks = _lbc_config.get_board_disks(server) + disk_mountpoint = disks[disk-1] + return f'{disk_mountpoint}/cam-{cam}' + + +def get_motion_path(cam: int) -> str: + return f'{get_recordings_path(cam)}/motion' + + +def is_valid_recording_name(filename: str) -> bool: + if not filename.startswith('record_'): + return False + + for container_type in VideoContainerType: + if filename.endswith(f'.{container_type.value}'): + return True + + return False + + +def datetime_from_filename(name: str) -> datetime: + name = os.path.basename(name) + exts = '|'.join([t.value for t in VideoContainerType]) + + if name.startswith('record_'): + return datetime.strptime(re.match(rf'record_(.*?)\.(?:{exts})', name).group(1), datetime_format) + + m = re.match(rf'({datetime_format_re})__{datetime_format_re}\.(?:{exts})', name) + if m: + return datetime.strptime(m.group(1), datetime_format) + + raise ValueError(f'unrecognized filename format: {name}') + + +def get_hls_channel_name(cam: int, channel: int) -> str: + name = str(cam) + if channel == 2: + name += '-low' + return name + + +def get_hls_directory(cam, channel) -> str: + dirname = os.path.join( + _ipcam_config['hls_path'], + get_hls_channel_name(cam, channel) + ) + if not os.path.exists(dirname): + os.makedirs(dirname) + return dirname \ No newline at end of file -- cgit v1.2.3 From 57955b596485ecce1ffd4395e23c078358cc5ddd Mon Sep 17 00:00:00 2001 From: Evgeny Sorokin Date: Sat, 13 Jan 2024 00:54:32 +0000 Subject: save something --- include/py/homekit/camera/config.py | 42 +++++++++++++++++++------------------ include/py/homekit/camera/types.py | 32 +++++++++++++++++++--------- 2 files changed, 44 insertions(+), 30 deletions(-) (limited to 'include/py/homekit/camera') diff --git a/include/py/homekit/camera/config.py b/include/py/homekit/camera/config.py index c7dbc38..8e9bfd5 100644 --- a/include/py/homekit/camera/config.py +++ b/include/py/homekit/camera/config.py @@ -23,17 +23,13 @@ class IpcamConfig(ConfigUnit): @classmethod def schema(cls) -> Optional[dict]: return { - 'cams': { + 'cameras': { 'type': 'dict', 'keysrules': {'type': ['string', 'integer']}, 'valuesrules': { 'type': 'dict', 'schema': { 'type': {'type': 'string', 'allowed': [t.value for t in CameraType], 'required': True}, - 'codec': {'type': 'string', 'allowed': [t.value for t in VideoCodecType], 'required': True}, - 'container': {'type': 'string', 'allowed': [t.value for t in VideoContainerType], 'required': True}, - 'server': {'type': 'string', 'allowed': list(_lbc.get().keys()), 'required': True}, - 'disk': {'type': 'integer', 'required': True}, 'motion': { 'type': 'dict', 'schema': { @@ -44,10 +40,18 @@ class IpcamConfig(ConfigUnit): } } }, - 'rtsp_tcp': {'type': 'boolean'} } } }, + 'areas': { + 'type': 'dict', + 'keysrules': {'type': 'string'}, + 'valuesrules': { + 'type': 'list', + 'schema': {'type': ['string', 'integer']} # same type as for 'cameras' keysrules + } + }, + 'camera_ip_template': {'type': 'string', 'required': True}, 'motion_padding': {'type': 'integer', 'required': True}, 'motion_telegram': {'type': 'boolean', 'required': True}, 'fix_interval': {'type': 'integer', 'required': True}, @@ -94,6 +98,7 @@ class IpcamConfig(ConfigUnit): } } + # FIXME def get_all_cam_names(self, filter_by_server: Optional[str] = None, filter_by_disk: Optional[int] = None) -> list[int]: @@ -106,25 +111,22 @@ class IpcamConfig(ConfigUnit): cams.append(int(cam)) return cams - def get_all_cam_names_for_this_server(self, - filter_by_disk: Optional[int] = None): - return self.get_all_cam_names(filter_by_server=socket.gethostname(), - filter_by_disk=filter_by_disk) + # def get_all_cam_names_for_this_server(self, + # filter_by_disk: Optional[int] = None): + # return self.get_all_cam_names(filter_by_server=socket.gethostname(), + # filter_by_disk=filter_by_disk) - def get_cam_server_and_disk(self, cam: int) -> tuple[str, int]: - return self['cams'][cam]['server'], self['cams'][cam]['disk'] + # def get_cam_server_and_disk(self, cam: int) -> tuple[str, int]: + # return self['cams'][cam]['server'], self['cams'][cam]['disk'] - def get_camera_container(self, cam: int) -> VideoContainerType: - return VideoContainerType(self['cams'][cam]['container']) + def get_camera_container(self, camera: int) -> VideoContainerType: + return self.get_camera_type(camera).get_container() - def get_camera_type(self, cam: int) -> CameraType: - return CameraType(self['cams'][cam]['type']) + def get_camera_type(self, camera: int) -> CameraType: + return CameraType(self['cams'][camera]['type']) def get_rtsp_creds(self) -> tuple[str, str]: return self['rtsp_creds']['login'], self['rtsp_creds']['password'] - def should_use_tcp_for_rtsp(self, cam: int) -> bool: - return 'rtsp_tcp' in self['cams'][cam] and self['cams'][cam]['rtsp_tcp'] - def get_camera_ip(self, camera: int) -> str: - return f'192.168.5.{camera}' + return self['camera_ip_template'] % (str(camera),) diff --git a/include/py/homekit/camera/types.py b/include/py/homekit/camera/types.py index c313b58..da0fcc6 100644 --- a/include/py/homekit/camera/types.py +++ b/include/py/homekit/camera/types.py @@ -1,10 +1,21 @@ from enum import Enum +class VideoContainerType(Enum): + MP4 = 'mp4' + MOV = 'mov' + + +class VideoCodecType(Enum): + H264 = 'h264' + H265 = 'h265' + + class CameraType(Enum): ESP32 = 'esp32' ALIEXPRESS_NONAME = 'ali' - HIKVISION = 'hik' + HIKVISION_264 = 'hik_264' + HIKVISION_265 = 'hik_265' def get_channel_url(self, channel: int) -> str: if channel not in (1, 2): @@ -12,22 +23,23 @@ class CameraType(Enum): if channel == 1: return '' elif channel == 2: - if self.value == CameraType.HIKVISION: + if self.value in (CameraType.HIKVISION_264, CameraType.HIKVISION_265): return '/Streaming/Channels/2' elif self.value == CameraType.ALIEXPRESS_NONAME: return '/?stream=1.sdp' else: raise ValueError(f'unsupported camera type {self.value}') + def get_codec(self, channel: int) -> VideoCodecType: + if channel == 1: + return VideoCodecType.H264 if self.value == CameraType.HIKVISION_264 else VideoCodecType.H265 + elif channel == 2: + return VideoCodecType.H265 if self.value == CameraType.ALIEXPRESS_NONAME else VideoCodecType.H264 + else: + raise ValueError(f'unexpected channel {channel}') -class VideoContainerType(Enum): - MP4 = 'mp4' - MOV = 'mov' - - -class VideoCodecType(Enum): - H264 = 'h264' - H265 = 'h265' + def get_container(self) -> VideoContainerType: + return VideoContainerType.MP4 if self.get_codec(1) == VideoCodecType.H264 else VideoContainerType.MOV class TimeFilterType(Enum): -- cgit v1.2.3 From 05c85757b8e2340441057d9ddfde2e9649ae8676 Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Sat, 17 Feb 2024 02:41:37 +0300 Subject: save --- include/py/homekit/camera/config.py | 9 +++++++++ 1 file changed, 9 insertions(+) (limited to 'include/py/homekit/camera') diff --git a/include/py/homekit/camera/config.py b/include/py/homekit/camera/config.py index 8e9bfd5..8aeb392 100644 --- a/include/py/homekit/camera/config.py +++ b/include/py/homekit/camera/config.py @@ -73,6 +73,15 @@ class IpcamConfig(ConfigUnit): 'login': {'type': 'string', 'required': True}, 'password': {'type': 'string', 'required': True}, } + }, + + 'web_creds': { + 'required': True, + 'type': 'dict', + 'schema': { + 'login': {'type': 'string', 'required': True}, + 'password': {'type': 'string', 'required': True}, + } } } -- cgit v1.2.3