From f4c57c4a899e06c039699ead0758534661912d23 Mon Sep 17 00:00:00 2001 From: mrlt8 <67088095+mrlt8@users.noreply.github.com> Date: Mon, 24 Jul 2023 23:38:22 +0900 Subject: [PATCH] v2.3.12 (#932) * Start from index 1 for cruise_point/waypoint #835 * update_snapshot via MQTT * fix camera status always online #907 #920 * Additional MQTT entities #921 * QSV related changes * i965-va-drivers #736 * FIX power status #921 * Fix cruise_point type #921 Thanks @jhansche * return index from command payload #921 * Update docker-image.yml * Monitor and set preferred bitrate #929 * Default to `-` for cruise_point #921 * clear out stale entities #921 * changelog --- .github/workflows/docker-image.yml | 23 +++++---- README.md | 26 +++------- app/CHANGELOG.md | 11 +++++ app/Dockerfile.hwaccel | 2 +- app/Dockerfile.qsv | 2 +- app/wyzebridge/ffmpeg.py | 1 + app/wyzebridge/mqtt.py | 79 +++++++++++++++++++++++++----- app/wyzebridge/stream.py | 16 ++++-- app/wyzebridge/wyze_commands.py | 1 + app/wyzebridge/wyze_control.py | 44 ++++++++++++----- app/wyzebridge/wyze_stream.py | 64 ++++++++++++++++-------- 11 files changed, 191 insertions(+), 78 deletions(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 2d520026..7c43f807 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -37,14 +37,23 @@ jobs: - name: Checkout repository uses: actions/checkout@v3 + - name: matrix image type + id: image_type + run: | + echo "suffix=${{ matrix.dockerfile == 'hwaccel' && '-hw' || matrix.dockerfile == 'qsv' && '-qsv' ||'' }}" >> $GITHUB_OUTPUT + echo "platforms=${{ matrix.dockerfile == 'multiarch' && 'linux/amd64,linux/arm64,linux/arm/v7' || 'linux/amd64' }}" >> $GITHUB_OUTPUT + echo "arch=${{ matrix.dockerfile == 'multiarch' && 'amd64,armhf,aarch64' || 'amd64' }}" >> $GITHUB_OUTPUT + - name: Set up QEMU uses: docker/setup-qemu-action@v2 with: - platforms: linux/amd64,linux/arm64,linux/arm + platforms: ${{ steps.image_type.outputs.platforms }} - name: Set up Docker Buildx id: buildx uses: docker/setup-buildx-action@master + with: + platforms: ${{ steps.image_type.outputs.platforms }} - name: Login to DockerHub if: github.event_name != 'pull_request' @@ -61,13 +70,6 @@ jobs: username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - - name: matrix image type - id: image_type - run: | - echo "suffix=${{ matrix.dockerfile == 'hwaccel' && '-hw' || matrix.dockerfile == 'qsv' && '-qsv' ||'' }}" >> $GITHUB_OUTPUT - echo "platforms=${{ matrix.dockerfile == 'multiarch' && 'linux/amd64,linux/arm64,linux/arm' || 'linux/amd64' }}" >> $GITHUB_OUTPUT - echo "arch=${{ matrix.dockerfile == 'multiarch' && 'amd64,armhf,aarch64' || 'amd64' }}" >> $GITHUB_OUTPUT - - name: Extract Docker metadata id: meta uses: docker/metadata-action@v4 @@ -112,8 +114,9 @@ jobs: io.hass.type=addon io.hass.arch=${{ steps.image_type.outputs.arch }} tags: ${{ steps.meta.outputs.tags }} - cache-from: type=gha - cache-to: type=gha,mode=max + cache-from: type=gha,scope=${{ matrix.dockerfile }} + cache-to: type=gha,mode=max,scope=${{ matrix.dockerfile }} + provenance: false version_bump: needs: [build] diff --git a/README.md b/README.md index c46c158a..a98b8589 100644 --- a/README.md +++ b/README.md @@ -33,27 +33,17 @@ You can then use the web interface at `http://localhost:5000` where localhost is See [basic usage](#basic-usage) for additional information or visit the [wiki page](https://github.com/mrlt8/docker-wyze-bridge/wiki/Home-Assistant) for additional information on using the bridge as a Home Assistant Add-on. -## What's Changed in v2.3.11 +## What's Changed in v2.3.12 * NEW: - * Add more MQTT entities when using MQTT discovery. Thanks @jhansche! #921 #922 - * custom video filter - Use `FFMPEG_FILTER` or `FFMPEG_FILTER_CAM-NAME` to set custom ffmpeg video filters. #919 -* NEW MQTT/REST commands: - * **SET** topic: `cruise_point` | payload: (int) 1-4 - Pan to predefined cruise_point/waypoint. Thanks @jhansche! (#835). - * **SET** topic: `time_zone` | payload: (str) `Area/Location`, e.g. `America/New_York` - Change camera timezone. Thanks @DennisGarvey! (#916) - * **GET/SET** topic: `osd_timestamp` | payload: (bool/int) `on/off` - toggle timestamp on video. - * **GET/SET** topic: `osd_logo` | payload: (bool/int) `on/off` - toggle wyze logo on video. - * **SET** topic: `quick_reponse` | payload: (int) 1-3 - Doorbell quick response. + * `update_snapshot` MQTT/REST API GET topic. + * Additional MQTT entities (#921) * FIXES: - * Resend discovery message on HA online. Thanks @jhansche! #907 #920 - * Return json response/value for commands. Thanks @jhansche! #835 - * Fix threading issue on restart. Thanks @ZacTyAdams! #902 - * Catch and disable MQTT on name resolution error. - * Fix SET cruise_points over MQTT. -* Updates: - * Wyze iOS App version from v2.43.0.12 to v2.43.5.3 (#914) - * MediaMTX version from v0.23.7 to v0.23.8 (#925) - + * Monitor and set preferred bitrate if/when the wyze app changes it. Thanks @plat2on1! (#929) + * `cruise_point` index starts at 1 when setting via MQTT/REST API. (#835) + * Camera status was always online. (#907) (#920) + * Power status was incorrect when using MQTT discovery. (#921) + [View previous changes](https://github.com/mrlt8/docker-wyze-bridge/releases) diff --git a/app/CHANGELOG.md b/app/CHANGELOG.md index 374d92fd..7dfbaa0a 100644 --- a/app/CHANGELOG.md +++ b/app/CHANGELOG.md @@ -1,3 +1,14 @@ +## What's Changed in v2.3.12 + +* NEW: + * `update_snapshot` MQTT/REST API GET topic. + * Additional MQTT entities (#921) +* FIXES: + * Monitor and set preferred bitrate if/when the wyze app changes it. Thanks @plat2on1! (#929) + * `cruise_point` index starts at 1 when setting via MQTT/REST API. (#835) + * Camera status was always online. (#907) (#920) + * Power status was incorrect when using MQTT discovery. (#921) + ## What's Changed in v2.3.11 * NEW: diff --git a/app/Dockerfile.hwaccel b/app/Dockerfile.hwaccel index eba57594..43a368b5 100644 --- a/app/Dockerfile.hwaccel +++ b/app/Dockerfile.hwaccel @@ -8,7 +8,7 @@ ARG QSV RUN if [ -n "$QSV" ]; then echo 'deb http://deb.debian.org/debian bookworm main contrib non-free non-free-firmware' >/etc/apt/sources.list.d/debian-testing.list; fi \ && apt-get update \ && apt-get install -y curl tar xz-utils \ - ${QSV:+i965-va-driver-shaders intel-media-va-driver-non-free libmfx1 libva-drm2 libx11-6} \ + ${QSV:+i965-va-driver intel-media-va-driver-non-free libmfx1 libva-drm2 libx11-6 && apt-get install i965-va-driver-shaders} \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* COPY . /build/app/ diff --git a/app/Dockerfile.qsv b/app/Dockerfile.qsv index e2315cf0..d7098f60 100644 --- a/app/Dockerfile.qsv +++ b/app/Dockerfile.qsv @@ -7,7 +7,7 @@ ARG QSV RUN if [ -n "$QSV" ]; then echo 'deb http://deb.debian.org/debian bookworm main contrib non-free non-free-firmware' >/etc/apt/sources.list.d/debian-testing.list; fi \ && apt-get update \ && apt-get install -y curl tar xz-utils \ - ${QSV:+i965-va-driver-shaders intel-media-va-driver-non-free libmfx1 libva-drm2 libx11-6} \ + ${QSV:+i965-va-driver-shaders intel-media-va-driver-non-free intel-opencl-icd libmfx1 libva-drm2 libx11-6} \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* COPY . /build/app/ diff --git a/app/wyzebridge/ffmpeg.py b/app/wyzebridge/ffmpeg.py index 8b35a4df..13951103 100644 --- a/app/wyzebridge/ffmpeg.py +++ b/app/wyzebridge/ffmpeg.py @@ -122,6 +122,7 @@ def re_encode_video(uri: str, is_vertical: bool) -> list[str]: [h264_enc] + v_filter + ["-b:v", "2000k", "-coder", "1", "-bufsize", "2000k"] + + ["-maxrate", "2000k", "-minrate", "2000k"] + ["-profile:v", "77" if h264_enc == "h264_v4l2m2m" else "main"] + ["-preset", "fast" if h264_enc in {"h264_nvenc", "h264_qsv"} else "ultrafast"] + ["-forced-idr", "1", "-force_key_frames", "expr:gte(t,n_forced*2)"] diff --git a/app/wyzebridge/mqtt.py b/app/wyzebridge/mqtt.py index 7d836adf..fb42b802 100644 --- a/app/wyzebridge/mqtt.py +++ b/app/wyzebridge/mqtt.py @@ -53,6 +53,11 @@ def publish_discovery(cam_uri: str, cam: WyzeCamera, stopped: bool = True) -> No }, } + # Clear out old/renamed entities + REMOVE = {"alarm": "switch"} + for entity, type in REMOVE.items(): + msgs.append((f"{MQTT_DISCOVERY}/{type}/{cam.mac}/{entity}/config", None)) + for entity, data in get_entities(base, cam.is_pan_cam, cam.rtsp_fw).items(): topic = f"{MQTT_DISCOVERY}/{data['type']}/{cam.mac}/{entity}/config" if "availability_topic" not in data["payload"]: @@ -60,13 +65,13 @@ def publish_discovery(cam_uri: str, cam: WyzeCamera, stopped: bool = True) -> No payload = dict( base_payload | data["payload"], - name=f"Wyze Cam {cam.nickname} {' '.join(entity.upper().split('_'))}", + name=f"Wyze Cam {cam.nickname} {' '.join(entity.title().split('_'))}", uniq_id=f"WYZE{cam.mac}{entity.upper()}", ) msgs.append((topic, json.dumps(payload))) - send_mqtt(msgs) + publish_messages(msgs) @mqtt_enabled @@ -87,18 +92,16 @@ def mqtt_sub_topic(m_topics: list, callback) -> Optional[paho.mqtt.client.Client return client -def bridge_status(client: Optional[paho.mqtt.client.Client], cams: list): +def bridge_status(client: Optional[paho.mqtt.client.Client]): """Set bridge online if MQTT is enabled.""" if not client: return client.publish(f"{MQTT_TOPIC}/state", "online") - for cam in cams: - client.publish(f"{MQTT_TOPIC}/{cam}/state", "online") @mqtt_enabled -def send_mqtt(messages: list) -> None: - """Publish a message to the MQTT server.""" +def publish_messages(messages: list) -> None: + """Publish multiple messages to the MQTT server.""" paho.mqtt.publish.multiple( messages, hostname=MQTT_HOST, @@ -112,7 +115,7 @@ def send_mqtt(messages: list) -> None: @mqtt_enabled -def publish_message(topic: str, message=None): +def publish_topic(topic: str, message=None, retain=True): paho.mqtt.publish.single( topic=f"{MQTT_TOPIC}/{topic}", payload=message, @@ -123,12 +126,16 @@ def publish_message(topic: str, message=None): if env_bool("MQTT_AUTH") else None ), + retain=retain, ) @mqtt_enabled def update_mqtt_state(camera: str, state: str): - return publish_message(f"{camera}/state", state) + msg = [(f"{MQTT_TOPIC}/{camera}/state", state)] + if state == "online": + msg.append((f"{MQTT_TOPIC}/{camera}/power", "on")) + publish_messages(msg) @mqtt_enabled @@ -136,7 +143,7 @@ def update_preview(cam_name: str): with contextlib.suppress(FileNotFoundError): img_file = f"{IMG_PATH}{cam_name}.{env_bool('IMG_TYPE','jpg')}" with open(img_file, "rb") as img: - publish_message(f"{cam_name}/image", img.read()) + publish_topic(f"{cam_name}/image", img.read()) @mqtt_enabled @@ -162,7 +169,7 @@ def _mqtt_discovery(client, cams, msg): if msg.payload.decode().lower() != "online" or not cams: return - bridge_status(client, []) + bridge_status(client) for uri, cam in cams.items(): publish_discovery(uri, cam, False) @@ -199,13 +206,35 @@ def get_entities(base_topic: str, pan_cam: bool = False, rtsp: bool = False) -> "icon": "mdi:cctv", }, }, + "stream": { + "type": "switch", + "payload": { + "state_topic": f"{base_topic}state", + "command_topic": f"{base_topic}state/set", + "payload_on": "start", + "state_on": "online", + "payload_off": "stop", + "state_off": "stopped", + "icon": "mdi:play-pause", + }, + }, "power": { "type": "switch", "payload": { + "state_topic": f"{base_topic}power", "command_topic": f"{base_topic}power/set", + "payload_on": "on", + "payload_off": "off", "icon": "mdi:power-plug", }, }, + "update_snapshot": { + "type": "button", + "payload": { + "command_topic": f"{base_topic}update_snapshot/get", + "icon": "mdi:camera", + }, + }, "ir": { "type": "switch", "payload": { @@ -227,7 +256,7 @@ def get_entities(base_topic: str, pan_cam: bool = False, rtsp: bool = False) -> }, }, "alarm": { - "type": "switch", + "type": "siren", "payload": { "state_topic": f"{base_topic}alarm", "command_topic": f"{base_topic}alarm/set", @@ -305,6 +334,15 @@ def get_entities(base_topic: str, pan_cam: bool = False, rtsp: bool = False) -> "entity_category": "diagnostic", }, }, + "reboot": { + "type": "button", + "payload": { + "command_topic": f"{base_topic}power/set", + "payload_press": "restart", + "icon": "mdi:restart", + "entity_category": "diagnostic", + }, + }, } if pan_cam: entities |= { @@ -328,6 +366,23 @@ def get_entities(base_topic: str, pan_cam: bool = False, rtsp: bool = False) -> "icon": "mdi:motion-sensor", }, }, + "reset_rotation": { + "type": "button", + "payload": { + "command_topic": f"{base_topic}reset_rotation/set", + "icon": "mdi:restore", + }, + }, + "cruise_point": { + "type": "select", + "payload": { + "state_topic": f"{base_topic}cruise_point", + "command_topic": f"{base_topic}cruise_point/set", + "optimistic": False, + "options": ["-", "1", "2", "3", "4"], + "icon": "mdi:map-marker-multiple", + }, + }, } if rtsp: entities |= { diff --git a/app/wyzebridge/stream.py b/app/wyzebridge/stream.py index 618ca25b..52d7bbc5 100644 --- a/app/wyzebridge/stream.py +++ b/app/wyzebridge/stream.py @@ -7,7 +7,7 @@ from wyzebridge.config import MQTT_DISCOVERY, SNAPSHOT_INT, SNAPSHOT_TYPE from wyzebridge.ffmpeg import rtsp_snap_cmd from wyzebridge.logging import logger -from wyzebridge.mqtt import bridge_status, cam_control, publish_message, update_preview +from wyzebridge.mqtt import bridge_status, cam_control, publish_topic, update_preview from wyzebridge.rtsp_event import RtspEvent @@ -105,7 +105,7 @@ def monitor_streams(self, mtx_health: Callable) -> None: self.snap_all(cams) if int(time.time()) % 15 == 0: mtx_health() - bridge_status(mqtt, cams) + bridge_status(mqtt) if mqtt: mqtt.loop_stop() logger.info("Stream monitoring stopped") @@ -172,7 +172,17 @@ def send_cmd( status = cam_resp.get("value") if cam_resp.get("status") == "success" else 0 if isinstance(status, dict): status = json.dumps(status) - publish_message(f"{cam_name}/{cmd}", status) + + if "update_snapshot" in cam_resp: + on_demand = not stream.connected + snap = self.get_rtsp_snap(cam_name) + if on_demand: + stream.stop() + publish_topic(f"{cam_name}/{cmd}", int(time.time()) if snap else 0) + return dict(resp, status="success", value=snap, response=snap) + + publish_topic(f"{cam_name}/{cmd}", status) + return cam_resp if "status" in cam_resp else resp | cam_resp def rtsp_snap_popen(self, cam_name: str, interval: bool = False) -> Optional[Popen]: diff --git a/app/wyzebridge/wyze_commands.py b/app/wyzebridge/wyze_commands.py index b7956090..a47bab4f 100644 --- a/app/wyzebridge/wyze_commands.py +++ b/app/wyzebridge/wyze_commands.py @@ -1,6 +1,7 @@ GET_CMDS = { "state": None, "power": None, + "update_snapshot": None, "take_photo": "K10058TakePhoto", "irled": "K10044GetIRLEDStatus", "night_vision": "K10040GetNightVisionStatus", diff --git a/app/wyzebridge/wyze_control.py b/app/wyzebridge/wyze_control.py index 7efe0def..0edf81ff 100644 --- a/app/wyzebridge/wyze_control.py +++ b/app/wyzebridge/wyze_control.py @@ -10,7 +10,7 @@ from wyzebridge.bridge_utils import env_bool from wyzebridge.config import BOA_COOLDOWN, BOA_INTERVAL, IMG_PATH, MQTT_TOPIC from wyzebridge.logging import logger -from wyzebridge.mqtt import MQTT_ENABLED, send_mqtt +from wyzebridge.mqtt import MQTT_ENABLED, publish_messages from wyzebridge.wyze_commands import CMD_VALUES, GET_CMDS, GET_PAYLOAD, PARAMS, SET_CMDS from wyzecam import WyzeIOTCSession, WyzeIOTCSessionState, tutk_protocol @@ -144,8 +144,8 @@ def camera_control( boa = check_boa_enabled(sess, uri) + params_to_update = ",".join(PARAMS.values()) if MQTT_ENABLED: - params_to_update = ",".join(PARAMS.values()) send_tutk_msg(sess, ("param_info", params_to_update), "debug") while sess.state == WyzeIOTCSessionState.AUTHENTICATION_SUCCEEDED: @@ -170,24 +170,22 @@ def camera_control( if boa and cmd == "take_photo": pull_last_image(boa, "photo") - # Check bitrate - # sess.update_frame_size_rate(True) - - # update other cam info at same time? if resp: with contextlib.suppress(Full): camera_info.put(resp, block=False) + elif sess.state == WyzeIOTCSessionState.AUTHENTICATION_SUCCEEDED: + send_tutk_msg(sess, ("param_info", params_to_update), "debug") def pan_to_cruise_point(sess: WyzeIOTCSession, cmd): """ Pan to cruise point/waypoint. """ - resp = {"command": "cruise_point", "status": "error", "value": None} + resp = {"command": "cruise_point", "status": "error", "value": "-"} if not isinstance(cmd, tuple) or not str(cmd[1]).isdigit(): return resp | {"response": f"Invalid cruise point: {cmd=}"} - i = int(cmd[1]) + i = int(cmd[1]) - 1 if int(cmd[1]) > 0 else int(cmd[1]) with sess.iotctrl_mux() as mux: points = mux.send_ioctl(tutk_protocol.K11010GetCruisePoints()).result(timeout=5) if not points or not isinstance(points, list): @@ -196,9 +194,9 @@ def pan_to_cruise_point(sess: WyzeIOTCSession, cmd): try: waypoints = (points[i]["vertical"], points[i]["horizontal"]) except IndexError: - return resp | {"response": f"Cruise point NOT found. {points=}"} + return resp | {"response": f"Cruise point {i} NOT found. {points=}"} - logger.info(f"Pan to cruise_point={i} ({waypoints})") + logger.info(f"Pan to cruise_point={i} {waypoints}") res = mux.send_ioctl(tutk_protocol.K11018SetPTZPosition(*waypoints)).result( timeout=5 ) @@ -206,14 +204,13 @@ def pan_to_cruise_point(sess: WyzeIOTCSession, cmd): return resp | { "status": "success", "response": ",".join(map(str, res)) if isinstance(res, bytes) else res, - "value": i, } def update_mqtt_values(topic: str, cam_name: str, resp: dict): base = f"{MQTT_TOPIC}/{cam_name}" if msgs := [(f"{base}/{k}", resp[v]) for k, v in PARAMS.items() if v in resp]: - send_mqtt(msgs) + publish_messages(msgs) return int(resp.get(PARAMS[topic], 0)) if topic in PARAMS else resp @@ -249,8 +246,13 @@ def send_tutk_msg(sess: WyzeIOTCSession, cmd: tuple | str, log: str = "info") -> resp |= {"status": "success", "response": None} elif res := iotc.result(timeout=5): if tutk_msg.code == 10020: + if bitrate := bitrate_check(res, sess.preferred_bitrate): + logger.info(f"Setting bitrate={sess.preferred_bitrate}") + mux.send_ioctl(bitrate) res = update_mqtt_values(topic, sess.camera.name_uri, res) params = None if isinstance(res, int) else params + if topic == "bitrate" and payload: + sess.preferred_bitrate = int(payload) if isinstance(res, bytes): res = ",".join(map(str, res)) if isinstance(res, str) and res.isdigit(): @@ -272,6 +274,22 @@ def send_tutk_msg(sess: WyzeIOTCSession, cmd: tuple | str, log: str = "info") -> return {topic: resp} +def bitrate_check(res: dict, preferred_bitrate: int): + """Check if bitrate in response matches preferred bitrate. + + Parameters: + - res (dict): response from camera. + - preferred_bitrate (int): preferred bitrate. + + Returns: + - tutk_protocol.K10052SetBitrate: if bitrate does not match. + """ + if (bitrate := res.get("3")) and bitrate != preferred_bitrate: + logger.debug(f"Wrong {bitrate=} does not match {preferred_bitrate}") + + return tutk_protocol.K10052SetBitrate(preferred_bitrate) + + def parse_cmd(cmd: tuple | str, log: str) -> tuple: topic, payload = cmd if isinstance(cmd, tuple) else (cmd, None) set_cmd = payload and topic not in GET_PAYLOAD @@ -312,7 +330,7 @@ def motion_alarm(cam: dict): logger.info(f"[MOTION] Alarm file detected at {cam['last_photo'][1]}") cam["cooldown"] = datetime.now() + timedelta(seconds=BOA_COOLDOWN) cam["last_alarm"] = cam["last_photo"] - send_mqtt([(f"{MQTT_TOPIC}/{cam['uri']}/motion", motion)]) + publish_messages([(f"{MQTT_TOPIC}/{cam['uri']}/motion", motion)]) if motion and (http := env_bool("boa_motion")): try: resp = requests.get(http.format(cam_name=cam["uri"])) diff --git a/app/wyzebridge/wyze_stream.py b/app/wyzebridge/wyze_stream.py index 0c36f050..47e8fa85 100644 --- a/app/wyzebridge/wyze_stream.py +++ b/app/wyzebridge/wyze_stream.py @@ -17,7 +17,7 @@ from wyzebridge.config import BRIDGE_IP, COOLDOWN, MQTT_TOPIC from wyzebridge.ffmpeg import get_ffmpeg_cmd from wyzebridge.logging import logger -from wyzebridge.mqtt import publish_discovery, send_mqtt, update_mqtt_state +from wyzebridge.mqtt import publish_discovery, publish_messages, update_mqtt_state from wyzebridge.webhooks import ifttt_webhook from wyzebridge.wyze_api import WyzeApi from wyzebridge.wyze_commands import GET_CMDS, PARAMS, SET_CMDS @@ -263,39 +263,62 @@ def state_control(self, payload) -> dict: logger.info(f"[CONTROL] GET {self.uri} state") return {"status": "success", "response": self.status()} + def power_control(self, payload: str) -> dict: + if payload not in {"on", "off", "restart"}: + resp = self.api.get_pid_info(self.camera, "P3") + resp["value"] = "on" if resp["value"] == "1" else "off" + return resp + run_cmd = payload if payload == "restart" else f"power_{payload}" + + return dict( + self.api.run_action(self.camera, run_cmd), + value="on" if payload == "restart" else payload, + ) + + def tz_control(self, payload: str) -> dict: + try: + zone = zoneinfo.ZoneInfo(payload) + offset = datetime.now(zone).utcoffset() + assert offset is not None + except (zoneinfo.ZoneInfoNotFoundError, AssertionError): + return {"response": "invalid time zone"} + + return dict( + self.api.set_device_info(self.camera, {"device_timezone_city": zone.key}), + value=int(offset.total_seconds() / 3600), + ) + def send_cmd(self, cmd: str, payload: str | list | dict = "") -> dict: if cmd in {"state", "start", "stop", "disable", "enable"}: return self.state_control(payload or cmd) + if cmd == "device_info": return self.api.get_pid_info(self.camera) + if cmd == "power": - if str(payload).lower() not in {"on", "off", "restart"}: - return self.api.get_pid_info(self.camera, "P3") - run_cmd = payload if payload == "restart" else f"{cmd}_{payload}" - return dict(self.api.run_action(self.camera, run_cmd), value=payload) - if cmd == "time_zone" and payload and isinstance(payload, str): - try: - zone = zoneinfo.ZoneInfo(payload) - except zoneinfo.ZoneInfoNotFoundError: - return {"response": "invalid time zone"} - if offset := datetime.now(zone).utcoffset(): - return dict( - self.api.set_device_info( - self.camera, {"device_timezone_city": zone.key} - ), - value=int(offset.total_seconds() / 3600), - ) + return self.power_control(str(payload).lower()) if self.state < StreamStatus.STOPPED: return {"response": self.status()} + if env_bool("disable_control"): return {"response": "control disabled"} - if cmd not in GET_CMDS | SET_CMDS | PARAMS and cmd not in {"caminfo"}: - return {"response": "invalid command"} + + if cmd == "time_zone" and payload and isinstance(payload, str): + return self.tz_control(payload) if cmd == "bitrate" and isinstance(payload, (str, int)) and payload.isdigit(): self.options.bitrate = int(payload) + if cmd == "update_snapshot": + return {"update_snapshot": True} + + if cmd == "cruise_point" and payload == "-": + return {"status": "success", "value": "-"} + + if cmd not in GET_CMDS | SET_CMDS | PARAMS and cmd not in {"caminfo"}: + return {"response": "invalid command"} + if on_demand := not self.connected: logger.info(f"[CONTROL] Connecting to {self.uri}") self.start() @@ -313,6 +336,7 @@ def send_cmd(self, cmd: str, payload: str | list | dict = "") -> dict: if on_demand: logger.info(f"[CONTROL] Disconnecting from {self.uri}") self.stop() + return cam_resp.pop(cmd, None) or {"response": "could not get result"} def check_rtsp_fw(self, force: bool = False) -> Optional[str]: @@ -462,7 +486,7 @@ def get_cam_params( (f"{MQTT_TOPIC}/{uri.lower()}/wifi", wifi), (f"{MQTT_TOPIC}/{uri.lower()}/audio", json.dumps(audio) if audio else False), ] - send_mqtt(mqtt) + publish_messages(mqtt) return v_codec, fps, audio