From 1272528c32bb888357b31d56f24b3b18d06a6f95 Mon Sep 17 00:00:00 2001 From: Taras Terletskyi <888784+tropicoo@users.noreply.github.com> Date: Thu, 22 Sep 2022 21:07:41 +0300 Subject: [PATCH] Version 1.5. Details in /releases/release_1.5.md (#51) --- .flake8 | 4 ++ .github/dependabot.yml | 6 +++ Dockerfile | 2 - README.md | 12 +++--- docker-compose.yml | 11 +++-- hikcamerabot/bot_setup.py | 4 +- hikcamerabot/camera.py | 35 +++++++++------- hikcamerabot/camerabot.py | 2 +- hikcamerabot/common/video/tasks/videogif.py | 9 ++-- .../common/video/videogif_recorder.py | 8 ++-- hikcamerabot/config/schemas/encoding.py | 10 ++--- hikcamerabot/config/schemas/livestream.py | 12 +++--- hikcamerabot/config/schemas/main_config.py | 42 +++++++++---------- hikcamerabot/constants.py | 8 +++- hikcamerabot/decorators.py | 13 ++++-- hikcamerabot/enums.py | 12 +++--- .../event_engine/dispatchers/abstract.py | 2 - hikcamerabot/event_engine/handlers/inbound.py | 2 +- .../event_engine/handlers/outbound.py | 9 ++-- hikcamerabot/event_engine/workers/manager.py | 15 +++---- hikcamerabot/event_engine/workers/tasks.py | 2 +- hikcamerabot/launcher.py | 1 + hikcamerabot/registry.py | 18 ++++---- hikcamerabot/services/__init__.py | 0 hikcamerabot/services/abstract.py | 2 - .../{utils => services/alarm}/chunk.py | 3 +- hikcamerabot/services/alarm/notifier.py | 1 + .../alarm/tasks/alarm_monitoring_task.py | 2 +- .../services/alarm/tasks/notifications.py | 11 ++++- hikcamerabot/services/stream/abstract.py | 6 ++- .../services/stream/dvr/upload/engine.py | 2 +- hikcamerabot/services/tasks/livestream.py | 2 +- hikcamerabot/utils/image.py | 5 +-- hikcamerabot/utils/utils.py | 2 +- hikcamerabot/version.py | 2 +- pyproject.toml | 3 ++ releases/release_1.5.md | 17 ++++++++ requirements.txt | 14 +++---- srs_prod/Dockerfile | 27 +----------- 39 files changed, 186 insertions(+), 152 deletions(-) create mode 100644 .flake8 create mode 100644 .github/dependabot.yml create mode 100644 hikcamerabot/services/__init__.py rename hikcamerabot/{utils => services/alarm}/chunk.py (93%) create mode 100644 pyproject.toml create mode 100644 releases/release_1.5.md diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..6f094a9 --- /dev/null +++ b/.flake8 @@ -0,0 +1,4 @@ +[flake8] +max-line-length = 88 +select = C,E,F,W,B,B950 +extend-ignore = E203, E501 diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..b38df29 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "daily" diff --git a/Dockerfile b/Dockerfile index 5faafcd..ca1c125 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,5 +22,3 @@ RUN apk add --no-cache --virtual .build-deps \ && apk --purge del .build-deps COPY . /app - -CMD ["python", "bot.py"] diff --git a/README.md b/README.md index 2e0eba7..8a34cda 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # Hikvision Telegram Camera Bot Telegram Bot which sends snapshots from your Hikvision cameras. -Version: 1.4. [Release details](releases/release_1.4.md). +Version: 1.5. [Release details](releases/release_1.5.md). ## Features -1. Send full/resized snapshots on request -2. Auto-send snapshots on **Motion**, **Line Crossing** and **Intrusion (Field) Detection** -3. Send so-called Telegram video-gifs on request and alert events from paragraph #2 +1. Send full/resized pictures on request +2. Auto-send pictures on **Motion**, **Line Crossing** and **Intrusion (Field) Detection** +3. Send so-called Telegram video-gifs on request and alert events from previous paragraph 4. YouTube, Telegram and Icecast direct or re-encoded livestreams 5. DVR to local storage with upload to Telegram group 6. SRS re-stream server @@ -246,7 +246,7 @@ where: 2. `101` is camera's configured stream channel. 3. `cam_2` is ID of your second configured camera. -SRS runs in a separate docker container. SRS config and `Dockerfile` placed +SRS runs in a separate docker container. SRS config and `Dockerfile` are placed in `srs_prod` directory. Service name is `hikvision-srs-server` in `docker-compose.yml`. @@ -313,7 +313,7 @@ file with DVR stream settings: from the local storage. You need to make sure your file size will be up to 2GB since Telegram rejects larger ones. Just experiment with segment time. 5. Local storage (the real one, not in the container) by default is `/data/dvr` in volumes mapping (the first path string, not the last). - Change it to any location you need, e.g. to `- D:\Videos:/data/dvr` if you're on Windows. + Change it to any location you need, e.g. to `- "D:\Videos:/data/dvr"` if you're on Windows. ```yaml volumes: - "/data/dvr:/data/dvr" diff --git a/docker-compose.yml b/docker-compose.yml index 3516398..3876cfe 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,4 @@ -version: '3' +version: "3" services: hikvision-camera-bot: @@ -9,11 +9,14 @@ services: context: . dockerfile: Dockerfile volumes: - # First path is real local storage path. Change to your preferred path. + # First path is real local storage path, e.g. "D:\Videos" in Windows. Change to your preferred one. - "/data/dvr:/data/dvr" + - "./configs:/app/configs" restart: unless-stopped depends_on: - hikvision-srs-server + command: > + bash -c "python ./bot.py" hikvision-srs-server: container_name: "hikvision_srs_server" @@ -26,8 +29,10 @@ services: # Store HLS .ts files in RAM. Depending on 'hls_fragment' and 'hls_window' default size '128M' might be increased. # Check usage inside the container with 'df -h'. - /srs/trunk/objs/nginx/html/hls:mode=770,size=128M + volumes: + - "./srs_prod:/usr/local/srs/user_conf" command: > - bash -c "./objs/srs -c conf/hik-docker.conf" + bash -c "./objs/srs -c user_conf/hik-docker.conf" ports: # SRS RTMP port. If you comment this out, you won't be able to connect with video player like VLC. - "1935:1935" diff --git a/hikcamerabot/bot_setup.py b/hikcamerabot/bot_setup.py index 2b77b3b..5ee9b33 100644 --- a/hikcamerabot/bot_setup.py +++ b/hikcamerabot/bot_setup.py @@ -34,7 +34,9 @@ def _create_and_setup_cameras(self) -> None: for cam_id, cam_conf in self._conf.camera_list.items(): if cam_conf.hidden: - # Skip camera and its settings. + self._log.info( + '[%s] Skipping camera config - %s', cam_id, cam_conf.description + ) continue cam_cmds = defaultdict(list) diff --git a/hikcamerabot/camera.py b/hikcamerabot/camera.py index 7eab52d..70f86e6 100644 --- a/hikcamerabot/camera.py +++ b/hikcamerabot/camera.py @@ -1,4 +1,5 @@ """Hikvision camera module.""" + import asyncio import logging from datetime import datetime @@ -10,11 +11,12 @@ from hikcamerabot.clients.hikvision import HikvisionAPI, HikvisionAPIClient from hikcamerabot.clients.hikvision.enums import IrcutFilterType +from hikcamerabot.common.video.videogif_recorder import VideoGifRecorder from hikcamerabot.enums import VideoGifType from hikcamerabot.exceptions import HikvisionAPIError, HikvisionCamError -from hikcamerabot.services.manager import ServiceManager -from hikcamerabot.common.video.videogif_recorder import VideoGifRecorder +from hikcamerabot.services.abstract import AbstractService from hikcamerabot.services.alarm import AlarmService +from hikcamerabot.services.manager import ServiceManager from hikcamerabot.services.stream import ( DvrStreamService, IcecastStreamService, @@ -24,12 +26,13 @@ ) from hikcamerabot.utils.image import ImageProcessor - if TYPE_CHECKING: from hikcamerabot.camerabot import CameraBot class ServiceContainer: + """Container class for all services.""" + def __init__( self, conf: Dict, api: HikvisionAPI, cam: 'HikvisionCam', bot: 'CameraBot' ) -> None: @@ -76,6 +79,17 @@ def __init__( cam=cam, ) + def get_all(self) -> list[AbstractService]: + """Return list with all services.""" + return [ + self.alarm, + self.dvr_stream, + self.srs_stream, + self.stream_icecast, + self.stream_tg, + self.stream_yt, + ] + class HikvisionCam: """Hikvision Camera Class.""" @@ -99,16 +113,7 @@ def __init__(self, id: str, conf: Dict, bot: 'CameraBot') -> None: bot=self.bot, ) self.service_manager = ServiceManager() - self.service_manager.register( - [ - self.services.alarm, - self.services.stream_yt, - self.services.stream_icecast, - self.services.srs_stream, - self.services.dvr_stream, - self.services.stream_tg, - ] - ) + self.service_manager.register(self.services.get_all()) self.snapshots_taken = 0 self._videogif = VideoGifRecorder(cam=self) @@ -120,9 +125,9 @@ async def start_videogif_record( self, video_type: VideoGifType = VideoGifType.ON_DEMAND, rewind: bool = False, - context: Message = None, + message: Message = None, ) -> None: - self._videogif.start_rec(video_type=video_type, rewind=rewind, context=context) + self._videogif.start_rec(video_type=video_type, rewind=rewind, message=message) async def set_ircut_filter(self, filter_type: IrcutFilterType) -> None: await self._api.set_ircut_filter(filter_type) diff --git a/hikcamerabot/camerabot.py b/hikcamerabot/camerabot.py index ee06f99..6a54faf 100644 --- a/hikcamerabot/camerabot.py +++ b/hikcamerabot/camerabot.py @@ -65,7 +65,7 @@ async def send_alert_message(self, text: str, **kwargs) -> None: for user_id in self.alert_users: await self._send_message(text, user_id, **kwargs) - async def _send_message(self, text: str, user_id: int, **kwargs): + async def _send_message(self, text: str, user_id: int, **kwargs) -> None: try: await self.send_message(user_id, text, **kwargs) except Exception: diff --git a/hikcamerabot/common/video/tasks/videogif.py b/hikcamerabot/common/video/tasks/videogif.py index 367a59d..1905a4e 100644 --- a/hikcamerabot/common/video/tasks/videogif.py +++ b/hikcamerabot/common/video/tasks/videogif.py @@ -19,6 +19,7 @@ FFMPEG_SRS_HLS_VIDEO_SRC, FFMPEG_SRS_RTMP_VIDEO_SRC, RTSP_TRANSPORT_TPL, + SRS_DOCKER_CONTAINER_NAME, SRS_LIVESTREAM_NAME_TPL, ) from hikcamerabot.enums import Event, VideoGifType @@ -54,7 +55,7 @@ def __init__( rewind: bool, cam: 'HikvisionCam', video_type: VideoGifType, - context: Message = None, + message: Message = None, ): self._log = logging.getLogger(self.__class__.__name__) self._cam = cam @@ -75,7 +76,7 @@ def __init__( if self._rewind: self._rec_time += self._gif_conf.rewind_time - self._message = context + self._message = message self._event = self._VIDEO_TYPE_TO_EVENT[self._video_type] self._result_queue = get_result_queue() self._killpg = wrap(os.killpg) @@ -136,7 +137,7 @@ async def _get_probe_ctx(self) -> None: self._height = video_streams[0]['height'] self._width = video_streams[0]['width'] - def _post_err_cleanup(self): + def _post_err_cleanup(self) -> None: """Delete video file and thumb if they exist after exception.""" for file_path in (self._file_path, self._thumb_path): try: @@ -217,7 +218,7 @@ def _build_ffmpeg_cmd(self) -> str: ) if self._rewind: video_source = FFMPEG_SRS_HLS_VIDEO_SRC.format( - ip_address=socket.gethostbyname('hikvision_srs_server'), + ip_address=socket.gethostbyname(SRS_DOCKER_CONTAINER_NAME), livestream_name=livestream_name, ) return FFMPEG_CMD_HLS_VIDEO_GIF.format( diff --git a/hikcamerabot/common/video/videogif_recorder.py b/hikcamerabot/common/video/videogif_recorder.py index 057b43d..5933404 100644 --- a/hikcamerabot/common/video/videogif_recorder.py +++ b/hikcamerabot/common/video/videogif_recorder.py @@ -24,20 +24,20 @@ def __init__(self, cam: 'HikvisionCam') -> None: self._proc_task_queue = deque() def start_rec( - self, video_type: VideoGifType, rewind: bool = False, context: Message = None + self, video_type: VideoGifType, rewind: bool = False, message: Message = None ) -> None: """Start recording video-gif.""" - self._start_rec(video_type=video_type, rewind=rewind, context=context) + self._start_rec(video_type=video_type, rewind=rewind, message=message) def _start_rec( - self, video_type: VideoGifType, rewind: bool, context: Message + self, video_type: VideoGifType, rewind: bool, message: Message ) -> None: """Start rtsp video stream recording to a temporary file.""" rec_task = RecordVideoGifTask( rewind=rewind, cam=self._cam, video_type=video_type, - context=context, + message=message, ) task = create_task( rec_task.run(), diff --git a/hikcamerabot/config/schemas/encoding.py b/hikcamerabot/config/schemas/encoding.py index 1897840..8ed7989 100644 --- a/hikcamerabot/config/schemas/encoding.py +++ b/hikcamerabot/config/schemas/encoding.py @@ -71,7 +71,7 @@ class _Scale(Schema): maxrate = f.String(required=True, validate=non_empty_str) bufsize = f.String(required=True, validate=non_empty_str) tune = f.String(required=True, validate=non_empty_str) - scale = f.Nested(_Scale, required=True) + scale = f.Nested(_Scale(), required=True) _inner_validation_schema_cls = _X264 @@ -99,12 +99,12 @@ class _Scale(Schema): bufsize = f.String(required=True, validate=non_empty_str) deadline = f.String(required=True, validate=non_empty_str) speed = f.Integer(required=True, validate=int_min_1) - scale = f.Nested(_Scale, required=True) + scale = f.Nested(_Scale(), required=True) _inner_validation_schema_cls = _Vp9 class Encoding(Schema): - direct = f.Nested(Direct, required=True) - x264 = f.Nested(X264, required=True) - vp9 = f.Nested(Vp9, required=True) + direct = f.Nested(Direct(), required=True) + x264 = f.Nested(X264(), required=True) + vp9 = f.Nested(Vp9(), required=True) diff --git a/hikcamerabot/config/schemas/livestream.py b/hikcamerabot/config/schemas/livestream.py index 956ed3a..a7d7f51 100644 --- a/hikcamerabot/config/schemas/livestream.py +++ b/hikcamerabot/config/schemas/livestream.py @@ -86,14 +86,14 @@ class _IceStream(Schema): channel = f.Integer(required=True, validate=int_min_1) restart_period = f.Integer(required=True, validate=int_min_1) restart_pause = f.Integer(required=True, validate=int_min_0) - ice_stream = f.Nested(_IceStream, required=True) + ice_stream = f.Nested(_IceStream(), required=True) _inner_validation_schema_cls = _Icecast class Livestream(Schema): - youtube = f.Nested(Youtube, required=True) - telegram = f.Nested(Telegram, required=True) - icecast = f.Nested(Icecast, required=True) - srs = f.Nested(Srs, required=True) - dvr = f.Nested(Dvr, required=True) + youtube = f.Nested(Youtube(), required=True) + telegram = f.Nested(Telegram(), required=True) + icecast = f.Nested(Icecast(), required=True) + srs = f.Nested(Srs(), required=True) + dvr = f.Nested(Dvr(), required=True) diff --git a/hikcamerabot/config/schemas/main_config.py b/hikcamerabot/config/schemas/main_config.py index b944c82..b19013d 100644 --- a/hikcamerabot/config/schemas/main_config.py +++ b/hikcamerabot/config/schemas/main_config.py @@ -28,25 +28,25 @@ class TelegramDvrUploadConf(Schema): class DvrUploadStorageConf(Schema): - telegram = f.Nested(TelegramDvrUploadConf, required=True) + telegram = f.Nested(TelegramDvrUploadConf(), required=True) class DvrUploadConf(Schema): delete_after_upload = f.Boolean(required=True) - storage = f.Nested(DvrUploadStorageConf, required=True) + storage = f.Nested(DvrUploadStorageConf(), required=True) class DvrLivestreamConf(LivestreamConf): local_storage_path = f.Str(required=True, validate=non_empty_str) - upload = f.Nested(DvrUploadConf, required=True) + upload = f.Nested(DvrUploadConf(), required=True) class Livestream(Schema): - srs = f.Nested(LivestreamConf, required=True) - dvr = f.Nested(DvrLivestreamConf, required=True) - youtube = f.Nested(LivestreamConf, required=True) - telegram = f.Nested(LivestreamConf, required=True) - icecast = f.Nested(LivestreamConf, required=True) + srs = f.Nested(LivestreamConf(), required=True) + dvr = f.Nested(DvrLivestreamConf(), required=True) + youtube = f.Nested(LivestreamConf(), required=True) + telegram = f.Nested(LivestreamConf(), required=True) + icecast = f.Nested(LivestreamConf(), required=True) class Detection(Schema): @@ -72,15 +72,15 @@ class VideoGifOnAlert(VideoGifOnDemand): class VideoGif(Schema): - on_alert = f.Nested(VideoGifOnAlert, required=True) - on_demand = f.Nested(VideoGifOnDemand, required=True) + on_alert = f.Nested(VideoGifOnAlert(), required=True) + on_demand = f.Nested(VideoGifOnDemand(), required=True) class Alert(Schema): delay = f.Int(required=True, validate=v.Range(min=0)) - motion_detection = f.Nested(Detection, required=True) - line_crossing_detection = f.Nested(Detection, required=True) - intrusion_detection = f.Nested(Detection, required=True) + motion_detection = f.Nested(Detection(), required=True) + line_crossing_detection = f.Nested(Detection(), required=True) + intrusion_detection = f.Nested(Detection(), required=True) class CamAPIAuth(Schema): @@ -90,7 +90,7 @@ class CamAPIAuth(Schema): class CamAPI(Schema): host = f.Str(required=True, validate=non_empty_str) - auth = f.Nested(CamAPIAuth, required=True) + auth = f.Nested(CamAPIAuth(), required=True) stream_timeout = f.Int(required=True, validate=int_min_1) @@ -112,12 +112,12 @@ class _CameraListConfig(Schema): description = f.Str(required=True, validate=non_empty_str) hashtag = f.Str(required=True, allow_none=True) group = f.Str(required=True, allow_none=True) - api = f.Nested(CamAPI, required=True) + api = f.Nested(CamAPI(), required=True) rtsp_port = f.Int(required=True) - video_gif = f.Nested(VideoGif, required=True) - alert = f.Nested(Alert, required=True) - livestream = f.Nested(Livestream, required=True) - command_sections_visibility = f.Nested(CmdSectionsVisibility, required=True) + video_gif = f.Nested(VideoGif(), required=True) + alert = f.Nested(Alert(), required=True) + livestream = f.Nested(Livestream(), required=True) + command_sections_visibility = f.Nested(CmdSectionsVisibility(), required=True) class Meta: ordered = True @@ -157,9 +157,9 @@ class Telegram(Schema): class MainConfig(Schema): _APP_LOG_LEVELS = {'DEBUG', 'WARNING', 'INFO', 'ERROR', 'CRITICAL'} - telegram = f.Nested(Telegram, required=True) + telegram = f.Nested(Telegram(), required=True) log_level = f.Str(required=True, validate=v.OneOf(_APP_LOG_LEVELS)) - camera_list = f.Nested(CameraListConfig, required=True) + camera_list = f.Nested(CameraListConfig(), required=True) class Meta: ordered = True diff --git a/hikcamerabot/constants.py b/hikcamerabot/constants.py index 87329b5..e113b75 100644 --- a/hikcamerabot/constants.py +++ b/hikcamerabot/constants.py @@ -27,13 +27,17 @@ class Img: ['quiet', 'panic', 'fatal', 'error', 'warning', 'info', 'verbose', 'debug', 'trace'] ) +SRS_DOCKER_CONTAINER_NAME = 'hikvision_srs_server' + _FFMPEG_BIN = 'ffmpeg' _FFMPEG_LOG_LEVEL = '-loglevel {loglevel}' FFMPEG_CAM_VIDEO_SRC = ( '"rtsp://{user}:{pw}@{host}:{rtsp_port}/Streaming/Channels/{channel}/"' ) -FFMPEG_SRS_RTMP_VIDEO_SRC = '"rtmp://hikvision_srs_server/live/{livestream_name}"' +FFMPEG_SRS_RTMP_VIDEO_SRC = ( + f'"rtmp://{SRS_DOCKER_CONTAINER_NAME}/live/{{livestream_name}}"' +) FFMPEG_SRS_HLS_VIDEO_SRC = '"http://{ip_address}:8080/hls/live/{livestream_name}.m3u8"' SRS_LIVESTREAM_NAME_TPL = 'livestream_{channel}_{cam_id}' @@ -66,7 +70,7 @@ class Img: '-buffer_size 1000000 ' '{filter} ' '-rtsp_transport {rtsp_transport_type} ' - '-stimeout 10000000 ' + '-timeout 10000000 ' f'-i {FFMPEG_CAM_VIDEO_SRC} ' '{map} ' '-c:v {vcodec} ' diff --git a/hikcamerabot/decorators.py b/hikcamerabot/decorators.py index 26cf389..16d14b1 100644 --- a/hikcamerabot/decorators.py +++ b/hikcamerabot/decorators.py @@ -1,9 +1,11 @@ """Decorators module.""" +import logging import re from functools import wraps from typing import TYPE_CHECKING +from emoji import emojize from pyrogram.types import Message from hikcamerabot.constants import CMD_CAM_ID_REGEX @@ -12,6 +14,8 @@ if TYPE_CHECKING: from hikcamerabot.camerabot import CameraBot +log = logging.getLogger(__name__) + # def event_error_handler(func): # @wraps(func) @@ -56,13 +60,13 @@ def authorization_check(func): async def wrapper(*args, **kwargs): bot: CameraBot = args[0] message: Message = args[1] - bot._log.debug(get_user_info(message)) # noqa + log.debug(get_user_info(message)) if message.chat.id in bot.chat_users: return await func(*args, **kwargs) - bot._log.error('User authorization error: %s', message.chat.id) # noqa - await message.reply_text('Not authorized', quote=True) + log.error('User authorization error: %s', message.chat.id) + await message.reply_text(emojize(':stop_sign: Not authorized'), quote=True) return wrapper @@ -79,6 +83,7 @@ async def wrapper(*args, **kwargs): try: return await func(*args, cam=cam, **kwargs) except Exception: - bot._log.exception('Failed to process event for %s', cam_id) + log.exception('Failed to process event for %s', cam_id) + log.debug('Failed event context: %s', message) return wrapper diff --git a/hikcamerabot/enums.py b/hikcamerabot/enums.py index dae35ea..8cb0313 100644 --- a/hikcamerabot/enums.py +++ b/hikcamerabot/enums.py @@ -54,7 +54,7 @@ class CmdSectionType(_BaseUniqueEnum): class Alarm(_BaseNonUniqueEnum): - ALARM = 'alarm' + ALARM = 'Alarm' class ServiceType(_BaseUniqueEnum): @@ -68,11 +68,11 @@ class DvrUploadType(_BaseUniqueEnum): class Stream(_BaseUniqueEnum): - DVR = 'dvr' - ICECAST = 'icecast' - SRS = 'srs' - TELEGRAM = 'telegram' - YOUTUBE = 'youtube' + DVR = 'DVR' + ICECAST = 'ICECAST' + SRS = 'SRS' + TELEGRAM = 'TELEGRAM' + YOUTUBE = 'YOUTUBE' class VideoEncoder(_BaseUniqueEnum): diff --git a/hikcamerabot/event_engine/dispatchers/abstract.py b/hikcamerabot/event_engine/dispatchers/abstract.py index 0a3afa9..fe30af9 100644 --- a/hikcamerabot/event_engine/dispatchers/abstract.py +++ b/hikcamerabot/event_engine/dispatchers/abstract.py @@ -4,8 +4,6 @@ if TYPE_CHECKING: from hikcamerabot.camerabot import CameraBot - from hikcamerabot.event_engine.handlers.inbound import AbstractTaskEvent - from hikcamerabot.event_engine.handlers.outbound import AbstractResultEventHandler DispatchTypeDict = dict[str, Type['AbstractTaskEvent | AbstractResultEventHandler']] diff --git a/hikcamerabot/event_engine/handlers/inbound.py b/hikcamerabot/event_engine/handlers/inbound.py index 6e021ca..bc20b0d 100644 --- a/hikcamerabot/event_engine/handlers/inbound.py +++ b/hikcamerabot/event_engine/handlers/inbound.py @@ -63,7 +63,7 @@ async def _handle(self, event: GetPicEvent) -> None: class TaskRecordVideoGif(AbstractTaskEvent): async def _handle(self, event: GetVideoEvent) -> None: await event.cam.start_videogif_record( - context=event.message, rewind=event.rewind + message=event.message, rewind=event.rewind ) diff --git a/hikcamerabot/event_engine/handlers/outbound.py b/hikcamerabot/event_engine/handlers/outbound.py index 6d21913..88752af 100644 --- a/hikcamerabot/event_engine/handlers/outbound.py +++ b/hikcamerabot/event_engine/handlers/outbound.py @@ -6,7 +6,8 @@ from io import BytesIO from typing import Optional, TYPE_CHECKING -from pyrogram.enums import ChatAction, ParseMode +from emoji import emojize +from pyrogram.enums import ChatAction from pyrogram.types import Message from tenacity import retry, stop_after_attempt, wait_fixed @@ -57,10 +58,12 @@ def _cleanup(self) -> None: async def __handle(self, event: VideoOutboundEvent) -> None: cam = event.cam caption = ( + f'{emojize(":rotating_light:", language="alias")} ' f'Alert video from {cam.description} {cam.hashtag}\n/cmds_{cam.id}, ' f'/list_cams' ) try: + # Simple for loop because video cache will be used. for uid in self._bot.alert_users: await self._send_video(uid, event, caption) finally: @@ -194,7 +197,7 @@ async def _handle(self, event: StreamOutboundEvent) -> None: stream_type = event.stream_type switch = event.switch text: str = event.text or '{0} stream successfully {1}'.format( - stream_type.value.capitalize(), 'enabled' if switch else 'disabled' + stream_type.value, 'enabled' if switch else 'disabled' ) await send_text(text=bold(text), message=message, quote=True) self._log.info(text) @@ -220,7 +223,7 @@ async def _handle(self, event: AlarmConfOutboundEvent) -> None: switch = event.switch text: str = event.text or '{0} successfully {1}'.format( - service_name.value.capitalize(), 'enabled' if switch else 'disabled' + service_name.value, 'enabled' if switch else 'disabled' ) await send_text(text=bold(text), message=message, quote=True) self._log.info(text) diff --git a/hikcamerabot/event_engine/workers/manager.py b/hikcamerabot/event_engine/workers/manager.py index 226681e..39dab7e 100644 --- a/hikcamerabot/event_engine/workers/manager.py +++ b/hikcamerabot/event_engine/workers/manager.py @@ -29,11 +29,12 @@ def start_worker_tasks(self) -> None: for idx in range(1, self._worker_num + 1): task_name = f'ResultWorkerTask_{idx}' self._log.debug('Starting %s', task_name) - worker_task = create_task( - ResultWorkerTask(self._outbound_dispatcher, idx).run(), - task_name=task_name, - logger=self._log, - exception_message='Task %s raised an exception', - exception_message_args=(task_name,), + self._workers.append( + create_task( + ResultWorkerTask(self._outbound_dispatcher, idx).run(), + task_name=task_name, + logger=self._log, + exception_message='Task %s raised an exception', + exception_message_args=(task_name,), + ) ) - self._workers.append(worker_task) diff --git a/hikcamerabot/event_engine/workers/tasks.py b/hikcamerabot/event_engine/workers/tasks.py index cef08f7..360fe84 100644 --- a/hikcamerabot/event_engine/workers/tasks.py +++ b/hikcamerabot/event_engine/workers/tasks.py @@ -25,7 +25,7 @@ async def run(self) -> None: await self._outbound_dispatcher.dispatch(event) except Exception: self._log.exception( - 'Unhandled exception in result worker %s. Context: %s', + 'Unhandled exception in result worker %s. Event context: %s', self._worker_id, event, ) diff --git a/hikcamerabot/launcher.py b/hikcamerabot/launcher.py index 1f02217..c43f10a 100644 --- a/hikcamerabot/launcher.py +++ b/hikcamerabot/launcher.py @@ -34,5 +34,6 @@ async def _start_bot(self) -> None: await self._run_bot_forever() async def _run_bot_forever(self) -> None: + """That's how we roll.""" while True: await asyncio.sleep(86400) diff --git a/hikcamerabot/registry.py b/hikcamerabot/registry.py index 776c877..acfb795 100644 --- a/hikcamerabot/registry.py +++ b/hikcamerabot/registry.py @@ -6,8 +6,10 @@ from hikcamerabot.camera import HikvisionCam -RegistryValue = dict[str, HikvisionCam | dict | str] -CamRegistryType = dict[str, RegistryValue] +CamRegistryValue = dict[str, HikvisionCam | dict | str] +CamRegistryType = dict[str, CamRegistryValue] +GroupRegistryValue = dict[str, str | list[HikvisionCam]] +GroupRegistryType = dict[str, GroupRegistryValue] class CameraRegistry: @@ -37,16 +39,14 @@ def _add_to_group_registry(self, cam: HikvisionCam) -> None: try: key = self._group_command_alias[cam.group] except KeyError: + # Key as group command name. key = f'group_{len(self._group_registry) + 1}' try: self._group_registry[key]['cams'].append(cam) except KeyError: self._group_command_alias[cam.group] = key - self._group_registry[key] = { - 'name': cam.group, - 'cams': [cam], - } + self._group_registry[key] = {'name': cam.group, 'cams': [cam]} def get_commands(self, cam_id: str) -> dict: """Get camera commands.""" @@ -59,7 +59,7 @@ def get_commands_presentation(self, cam_id: str) -> dict: def get_instance(self, cam_id: str) -> HikvisionCam: return self._cam_registry[cam_id]['cam'] - def get_meta(self, cam_id: str) -> RegistryValue: + def get_meta(self, cam_id: str) -> CamRegistryValue: return self._cam_registry[cam_id] def get_instances(self) -> Iterator[HikvisionCam]: @@ -76,8 +76,8 @@ def count(self) -> int: def get_instances_by_group(self, group_name: str) -> list[HikvisionCam]: return self._group_registry.get(group_name, []) - def get_groups_registry(self) -> dict: + def get_groups_registry(self) -> GroupRegistryType: return self._group_registry - def get_group(self, group_id: str) -> dict: + def get_group(self, group_id: str) -> GroupRegistryValue: return self._group_registry[group_id] diff --git a/hikcamerabot/services/__init__.py b/hikcamerabot/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hikcamerabot/services/abstract.py b/hikcamerabot/services/abstract.py index 3b16b17..db4f824 100644 --- a/hikcamerabot/services/abstract.py +++ b/hikcamerabot/services/abstract.py @@ -7,8 +7,6 @@ if TYPE_CHECKING: from hikcamerabot.camera import HikvisionCam - from hikcamerabot.services.alarm import AlarmService - from hikcamerabot.services.stream.abstract import AbstractStreamService class AbstractService(metaclass=abc.ABCMeta): diff --git a/hikcamerabot/utils/chunk.py b/hikcamerabot/services/alarm/chunk.py similarity index 93% rename from hikcamerabot/utils/chunk.py rename to hikcamerabot/services/alarm/chunk.py index c191503..ce7a7e8 100644 --- a/hikcamerabot/utils/chunk.py +++ b/hikcamerabot/services/alarm/chunk.py @@ -25,8 +25,7 @@ def detect_chunk(cls, chunk: str) -> Optional[Detection]: """Detect chunk in regard of `DETECTION_REGEX` string and return detection key. - :Parameters: - - `chunk`: string, one line from alert stream. + :param chunk: string, one line from alert stream. """ match = cls.DETECTION_REGEX.search(chunk) if not match: diff --git a/hikcamerabot/services/alarm/notifier.py b/hikcamerabot/services/alarm/notifier.py index eac677d..6989eb4 100644 --- a/hikcamerabot/services/alarm/notifier.py +++ b/hikcamerabot/services/alarm/notifier.py @@ -26,6 +26,7 @@ def __init__(self, service: 'AlarmService') -> None: def notify(self, detection_type: Detection) -> None: for task_cls in self.ALARM_NOTIFICATION_TASKS: + self._log.debug('Notifying with %s', task_cls) task = task_cls(service=self._service, detection_type=detection_type) create_task( task.run(), diff --git a/hikcamerabot/services/alarm/tasks/alarm_monitoring_task.py b/hikcamerabot/services/alarm/tasks/alarm_monitoring_task.py index c0ccaa3..03c4823 100644 --- a/hikcamerabot/services/alarm/tasks/alarm_monitoring_task.py +++ b/hikcamerabot/services/alarm/tasks/alarm_monitoring_task.py @@ -6,8 +6,8 @@ from hikcamerabot.enums import Detection, ServiceType from hikcamerabot.exceptions import ChunkDetectorError, ChunkLoopError from hikcamerabot.services.abstract import AbstractServiceTask +from hikcamerabot.services.alarm.chunk import ChunkDetector from hikcamerabot.services.alarm.notifier import AlertNotifier -from hikcamerabot.utils.chunk import ChunkDetector class ServiceAlarmMonitoringTask(AbstractServiceTask): diff --git a/hikcamerabot/services/alarm/tasks/notifications.py b/hikcamerabot/services/alarm/tasks/notifications.py index e45a81c..958f1f2 100644 --- a/hikcamerabot/services/alarm/tasks/notifications.py +++ b/hikcamerabot/services/alarm/tasks/notifications.py @@ -2,6 +2,9 @@ import logging from typing import TYPE_CHECKING +from pyrogram.enums import ParseMode +from emoji import emojize +from hikcamerabot.constants import DETECTION_SWITCH_MAP from hikcamerabot.enums import Detection, Event, VideoGifType from hikcamerabot.event_engine.events.outbound import ( AlertSnapshotOutboundEvent, @@ -32,10 +35,16 @@ async def _run(self) -> None: class AlarmTextMessageNotificationTask(AbstractAlertNotificationTask): async def _run(self) -> None: + detection_name: str = DETECTION_SWITCH_MAP[self._detection_type]['name'].value await self._result_queue.put( SendTextOutboundEvent( event=Event.SEND_TEXT, - text=f'[Alert - {self._cam.id}] Detected {self._detection_type.value}', + text=emojize( + f':rotating_light: Alert on "{self._cam.id} - ' + f'{self._cam.description}": {detection_name}', + language='alias', + ), + parse_mode=ParseMode.HTML, ) ) diff --git a/hikcamerabot/services/stream/abstract.py b/hikcamerabot/services/stream/abstract.py index 396b115..12557a7 100644 --- a/hikcamerabot/services/stream/abstract.py +++ b/hikcamerabot/services/stream/abstract.py @@ -156,7 +156,7 @@ async def stop(self, disable: bool = True) -> None: except ProcessLookupError as err: self._log.error('Failed to kill process: %s', err) except Exception: - err_msg = f'Failed to kill/disable {self.name.value.capitalize()} stream' + err_msg = f'Failed to kill/disable {self.name.value} stream' self._log.exception(err_msg) raise ServiceRuntimeError(err_msg) @@ -202,7 +202,9 @@ def _generate_cmd(self) -> None: self._log.error(err_msg) raise ServiceConfigError(err_msg) - self._stream_conf = get_livestream_tpl_config()[self.name.value][tpl_name_ls] + self._stream_conf = get_livestream_tpl_config()[self.name.value.lower()][ + tpl_name_ls + ] self._enc_conf = get_encoding_tpl_config()[enc_codec_name][tpl_name_enc] cmd_tpl = self._format_ffmpeg_cmd_tpl() diff --git a/hikcamerabot/services/stream/dvr/upload/engine.py b/hikcamerabot/services/stream/dvr/upload/engine.py index 3dcb433..a621cbc 100644 --- a/hikcamerabot/services/stream/dvr/upload/engine.py +++ b/hikcamerabot/services/stream/dvr/upload/engine.py @@ -62,7 +62,7 @@ async def start(self) -> None: await self._start_tasks() self._log.debug('Upload Engine for %s has started', self._cam.description) - async def _start_tasks(self): + async def _start_tasks(self) -> None: await asyncio.gather( self._start_storage_tasks(), self._start_file_monitoring_task_(), diff --git a/hikcamerabot/services/tasks/livestream.py b/hikcamerabot/services/tasks/livestream.py index 399d360..b1b825a 100644 --- a/hikcamerabot/services/tasks/livestream.py +++ b/hikcamerabot/services/tasks/livestream.py @@ -20,7 +20,7 @@ def __init__(self, proc: asyncio.subprocess.Process, cmd: str) -> None: async def run(self) -> None: self._log.info('Starting %s', self.__class__.__name__) while self._proc.returncode is None: - self._log.debug('Reading stdout from %s', self._cmd) + self._log.debug('Reading stdout from "%s"', self._cmd) self._log.info((await self._proc.stdout.read(50)).decode()) await asyncio.sleep(0.2) self._log.info('Exiting %s', self.__class__.__name__) diff --git a/hikcamerabot/utils/image.py b/hikcamerabot/utils/image.py index 4d2d5df..8132b15 100644 --- a/hikcamerabot/utils/image.py +++ b/hikcamerabot/utils/image.py @@ -10,10 +10,7 @@ class ImageProcessor(metaclass=Singleton): - """Image Processor Class. - - Process raw images taken from Hikvision camera. - """ + """Image Processor Class. Process raw images taken from Hikvision camera.""" def __init__(self) -> None: """Constructor.""" diff --git a/hikcamerabot/utils/utils.py b/hikcamerabot/utils/utils.py index 433dfeb..d862585 100644 --- a/hikcamerabot/utils/utils.py +++ b/hikcamerabot/utils/utils.py @@ -26,7 +26,7 @@ class Singleton(type): def __call__(cls, *args, **kwargs) -> Any: """Check whether instance already exists. - Return existing or create new instance and save to dict.""" + Return existing or create new instance and save it to dict.""" if cls not in cls._instances: cls._instances[cls] = super().__call__(*args, **kwargs) return cls._instances[cls] diff --git a/hikcamerabot/version.py b/hikcamerabot/version.py index 0f66308..fcb6b5d 100644 --- a/hikcamerabot/version.py +++ b/hikcamerabot/version.py @@ -1 +1 @@ -__version__ = '1.4' +__version__ = '1.5' diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..402e240 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[tool.black] +line-length = 88 +skip-string-normalization = 1 diff --git a/releases/release_1.5.md b/releases/release_1.5.md new file mode 100644 index 0000000..f2dc341 --- /dev/null +++ b/releases/release_1.5.md @@ -0,0 +1,17 @@ +# Release info + +Version: 1.5 + +Release date: September 22, 2022 + +# Important +1. This is bugfix and maintenance release (fixes [bug #48](https://github.com/tropicoo/hikvision-camera-bot/issues/48) with new ffmpeg 5). + +# New features +N/A + +# Misc +1. Bumped packages in `requirements.txt` to their latest versions. +2. Added dependabot for scanning for the latest package versions. +3. SRS dev team now provides Docker images for all platforms, no need to compile. +4. Bot and SRS configs are now mounted in volumes. diff --git a/requirements.txt b/requirements.txt index 441434c..df04c6e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,9 @@ -Pillow==9.1.0 -Pyrogram==2.0.16 +Pillow==9.2.0 +Pyrogram==2.0.56 addict==2.4.0 -httpx==0.22.0 -marshmallow==3.15.0 -tenacity==8.0.1 +emoji==2.1.0 +httpx==0.23.0 +marshmallow==3.18.0 +tenacity==8.1.0 tgcrypto==1.2.3 -ujson>=5.1.0 -xmltodict==0.12.0 +xmltodict==0.13.0 diff --git a/srs_prod/Dockerfile b/srs_prod/Dockerfile index 1be4890..53c6473 100644 --- a/srs_prod/Dockerfile +++ b/srs_prod/Dockerfile @@ -1,26 +1 @@ -FROM ubuntu:latest - -ENV TZ="Europe/Kiev" -RUN ln -snf /usr/share/zoneinfo/${TZ} /etc/localtime && echo ${TZ} > /etc/timezone -RUN sed -i 's/^deb http:\/\/archive\./deb http:\/\/ua\.archive\./' /etc/apt/sources.list - -RUN apt update \ - && apt upgrade --yes \ - && apt autoremove --yes \ - && apt install --yes --no-install-recommends \ - bash htop git tzdata sudo unzip openssl iputils-ping net-tools \ - && rm -rf /var/lib/apt/lists/* - -RUN git config --global http.sslVerify false \ - && git config --global http.postBuffer 1048576000 -RUN git clone -b 4.0release https://github.com/ossrs/srs.git - -WORKDIR /srs/trunk - -RUN apt update \ - && apt install --yes gcc g++ libffi-dev libjpeg-dev zlib1g-dev build-essential libtool automake patch perl \ - && ./configure --srt=on --jobs=$(nproc) && make -j$(nproc) \ - && apt autoremove --yes gcc g++ libffi-dev libjpeg-dev zlib1g-dev build-essential libtool automake patch perl \ - && rm -rf /var/lib/apt/lists/* - -COPY srs_prod/hik-docker.conf ./conf/hik-docker.conf +FROM ossrs/srs:4