diff --git a/README.md b/README.md index 1747263e..e1c888e1 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,10 @@ Based on [@noelhibbard's script](https://gist.github.com/noelhibbard/03703f55129 Please consider ⭐️ starring or [☕️ sponsoring](https://ko-fi.com/mrlt8) this project if you found it useful, or use the [affiliate link](https://amzn.to/3NLnbvt) when shopping on amazon! +## API Changes + +As of July 2023, you will need to update your bridge to v2.3.x or newer for compatibility with the latest changes to the Wyze API. + ## Quick Start Install [docker](https://docs.docker.com/get-docker/) and run: @@ -33,23 +37,20 @@ 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.13 +## What's Changed in v2.3.14 -FIXES: - * Errors when SET/GET `bitrate`. Thanks @plat2on1! (#929) - * Prevent exception on empty GET/SET payload. +NEW: +* PTZ controls in MQTT discovery as "cover" +* Add ffmpeg `filter_complex` config (#919) -## 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) - +CHANGED: +* Adjust default bitrate for re-encoding to 3000k. +* Case sensitive FFMPEG_CMD (#736) Thanks @392media! +* `DEBUG_FFMPEG` is now `FFMPEG_LOGLEVEL` with customizable levels: + * `quiet`, `panic`, `fatal`, `error`, `warning`, `info`, `verbose`, `debug`. + * Defaults to `fatal`. +* Bump Wyze App version to v2.44.1.1 (#946) [View previous changes](https://github.com/mrlt8/docker-wyze-bridge/releases) diff --git a/app/CHANGELOG.md b/app/CHANGELOG.md index 811ac472..8ebca5c6 100644 --- a/app/CHANGELOG.md +++ b/app/CHANGELOG.md @@ -1,3 +1,18 @@ +## What's Changed in v2.3.14 + +NEW: +* PTZ controls in MQTT discovery as "cover" +* Add ffmpeg `filter_complex` config (#919) + + +CHANGED: +* Adjust default bitrate for re-encoding to 3000k. +* Case sensitive FFMPEG_CMD (#736) Thanks @392media! +* `DEBUG_FFMPEG` is now `FFMPEG_LOGLEVEL` with customizable levels: + * `quiet`, `panic`, `fatal`, `error`, `warning`, `info`, `verbose`, `debug`. + * Defaults to `fatal`. +* Bump Wyze App version to v2.44.1.1 (#946) + ## What's Changed in v2.3.13 FIXES: diff --git a/app/Dockerfile.qsv b/app/Dockerfile.qsv index d7098f60..7afc47d8 100644 --- a/app/Dockerfile.qsv +++ b/app/Dockerfile.qsv @@ -7,7 +7,8 @@ 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 intel-opencl-icd libmfx1 libva-drm2 libx11-6} \ + ${QSV:+i965-va-driver intel-gpu-tools intel-media-va-driver-non-free intel-opencl-icd libmfx1 libva-drm2 libx11-6 vainfo} \ + && if [ -n "$QSV" ]; then apt-get install -y i965-va-driver-shaders; fi \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* COPY . /build/app/ @@ -20,7 +21,7 @@ RUN cd /build \ && curl -SL https://github.com/bluenviron/mediamtx/releases/download/v${MTX_TAG}/mediamtx_v${MTX_TAG}_linux_amd64.tar.gz \ | tar -xzf - -C app --wildcards 'mediamtx*' \ && cp app/amd.lib usr/local/lib/libIOTCAPIs_ALL.so \ - && if [ -n "$QSV" ]; then cp -R /usr/lib/x86_64-linux-gnu/ usr/lib/; fi \ + && if [ -n "$QSV" ]; then cp -R /usr/lib/x86_64-linux-gnu/ usr/lib/ && cp -R /usr/bin/ usr/bin; fi \ && rm app/*.txt app/*.lib FROM base diff --git a/app/static/site.js b/app/static/site.js index e2f3aa9b..b3819d80 100644 --- a/app/static/site.js +++ b/app/static/site.js @@ -305,7 +305,7 @@ document.addEventListener("DOMContentLoaded", () => { .then((data) => { let apiVersion = data.tag_name.replace(/[^0-9\.]/g, ""); if (apiVersion.localeCompare(checkAPI.dataset.version, undefined, { numeric: true }) === 1) { - sendNotification('Update available!', `🎉 v.${apiVersion}`, "warning"); + sendNotification('Update available!', `🎉 v${apiVersion}`, "warning"); } else { sendNotification('All up to date!', '✅ Running the latest version!', "success"); } diff --git a/app/wyzebridge/bridge_utils.py b/app/wyzebridge/bridge_utils.py index a1e95123..ebe27747 100644 --- a/app/wyzebridge/bridge_utils.py +++ b/app/wyzebridge/bridge_utils.py @@ -4,8 +4,12 @@ from wyzecam.api_models import WyzeCamera -def env_cam(env: str, uri: str, default="") -> str: - return env_bool(f"{env}_{uri}", env_bool(env, env_bool(f"{env}_all", default))) +def env_cam(env: str, uri: str, default="", style="") -> str: + return env_bool( + f"{env}_{uri}", + env_bool(env, env_bool(f"{env}_all", default, style=style), style=style), + style=style, + ) def env_bool(env: str, false="", true="", style="") -> Any: diff --git a/app/wyzebridge/config.py b/app/wyzebridge/config.py index 7edf5c69..3521072b 100644 --- a/app/wyzebridge/config.py +++ b/app/wyzebridge/config.py @@ -41,7 +41,7 @@ DEPRECATED = { - "DEBUG_LEVEL", + "DEBUG_FFMPEG", } for env in DEPRECATED: diff --git a/app/wyzebridge/ffmpeg.py b/app/wyzebridge/ffmpeg.py index 13951103..86ef2779 100644 --- a/app/wyzebridge/ffmpeg.py +++ b/app/wyzebridge/ffmpeg.py @@ -40,14 +40,14 @@ def get_ffmpeg_cmd( rtsp_transport = "udp" if "udp" in env_bool("MTX_PROTOCOLS") else "tcp" rss_cmd = f"[{{}}f=rtsp:{rtsp_transport=:}:bsfs/v=dump_extra=freq=keyframe]rtsp://0.0.0.0:8554/{uri}" rtsp_ss = rss_cmd.format("") - if env_cam("AUDIO_STREAM", uri) and audio: + if env_cam("AUDIO_STREAM", uri, style="original") and audio: rtsp_ss += "|" + rss_cmd.format("select=a:") + "_audio" h264_enc = env_bool("h264_enc").partition("_")[2] - cmd = env_cam("FFMPEG_CMD", uri).format( + cmd = env_cam("FFMPEG_CMD", uri, style="original").format( cam_name=uri, CAM_NAME=uri.upper(), audio_in=audio_in ).split() or ( - ["-loglevel", "verbose" if env_bool("DEBUG_FFMPEG") else "fatal"] + ["-hide_banner", "-loglevel", get_log_level()] + env_cam("FFMPEG_FLAGS", uri, flags).strip("'\"\n ").split() + ["-thread_queue_size", "100"] + (["-hwaccel", h264_enc] if h264_enc in {"vaapi", "qsv"} else []) @@ -66,11 +66,29 @@ def get_ffmpeg_cmd( ) if "ffmpeg" not in cmd[0].lower(): cmd.insert(0, "ffmpeg") - if env_bool("DEBUG_FFMPEG"): + if env_bool("FFMPEG_LOGLEVEL") in {"info", "verbose", "debug"}: logger.info(f"[FFMPEG_CMD] {' '.join(cmd)}") return cmd +def get_log_level(): + level = env_bool("FFMPEG_LOGLEVEL", "fatal").lower() + + if level in { + "quiet", + "panic", + "fatal", + "error", + "warning", + "info", + "verbose", + "debug", + }: + return level + + return "verbose" + + def re_encode_video(uri: str, is_vertical: bool) -> list[str]: """ Check if stream needs to be re-encoded. @@ -92,6 +110,7 @@ def re_encode_video(uri: str, is_vertical: bool) -> list[str]: """ h264_enc: str = env_bool("h264_enc", "libx264") custom_filter = env_cam("FFMPEG_FILTER", uri) + filter_complex = env_cam("FFMPEG_FILTER_COMPLEX", uri) v_filter = [] transpose = "clock" if (env_bool("ROTATE_DOOR") and is_vertical) or env_bool(f"ROTATE_CAM_{uri}"): @@ -106,7 +125,7 @@ def re_encode_video(uri: str, is_vertical: bool) -> list[str]: elif h264_enc == "h264_qsv": v_filter[1] = f"vpp_qsv=transpose={transpose}" - if not env_bool("FORCE_ENCODE") and not v_filter and not custom_filter: + if not (env_bool("FORCE_ENCODE") or v_filter or custom_filter or filter_complex): return ["copy"] logger.info( @@ -121,8 +140,8 @@ def re_encode_video(uri: str, is_vertical: bool) -> list[str]: return ( [h264_enc] + v_filter - + ["-b:v", "2000k", "-coder", "1", "-bufsize", "2000k"] - + ["-maxrate", "2000k", "-minrate", "2000k"] + + (["-filter_complex", filter_complex, "-map", "[v]"] if filter_complex else []) + + ["-b:v", "3000k", "-coder", "1", "-bufsize", "3000k"] + ["-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 a2ad5030..64e1fa8a 100644 --- a/app/wyzebridge/mqtt.py +++ b/app/wyzebridge/mqtt.py @@ -54,7 +54,7 @@ def publish_discovery(cam_uri: str, cam: WyzeCamera, stopped: bool = True) -> No } # Clear out old/renamed entities - REMOVE = {"alarm": "switch"} + REMOVE = {"alarm": "switch", "pan_tilt": "cover"} for entity, type in REMOVE.items(): msgs.append((f"{MQTT_DISCOVERY}/{type}/{cam.mac}/{entity}/config", None)) @@ -390,6 +390,21 @@ def get_entities(base_topic: str, pan_cam: bool = False, rtsp: bool = False) -> "icon": "mdi:map-marker-multiple", }, }, + "pan_tilt": { + "type": "cover", + "payload": { + "command_topic": f"{base_topic}rotary_degree/set", + "tilt_command_topic": f"{base_topic}rotary_degree/set", + "payload_open": "up", + "payload_close": "down", + "payload_stop": None, + "tilt_opened_value": 90, + "tilt_closed_value": -90, + "tilt_min": -90, + "tilt_max": 90, + "icon": "mdi:rotate-orbit", + }, + }, } if rtsp: entities |= { diff --git a/app/wyzecam/api.py b/app/wyzecam/api.py index 931c7836..88b4ba53 100644 --- a/app/wyzecam/api.py +++ b/app/wyzecam/api.py @@ -71,7 +71,7 @@ def login( payload = sort_dict( {"email": email.strip(), "password": triplemd5(password), **(mfa or {})} ) - api_version = "old" + api_version = "v2" if getenv("API_ID") and getenv("API_KEY"): api_version = "api" elif getenv("v3"): @@ -79,11 +79,12 @@ def login( headers["appid"] = "umgm_78ae6013d158c4a5" headers["signature2"] = sign_msg("v3", payload) - base_url = f"{AUTH_API}/{api_version}" if api_version in {"api", "v3"} else AUTH_API - resp = requests.post(f"{base_url}/user/login", data=payload, headers=headers) + resp = requests.post( + f"{AUTH_API}/{api_version}/user/login", data=payload, headers=headers + ) resp.raise_for_status() - return WyzeCredential.parse_obj(dict(resp.json(), phone_id=phone_id)) + return WyzeCredential.model_validate(dict(resp.json(), phone_id=phone_id)) def send_sms_code(auth_info: WyzeCredential, phone: str = "Primary") -> str: @@ -157,7 +158,7 @@ def refresh_token(auth_info: WyzeCredential) -> WyzeCredential: ) resp_json = validate_resp(resp) - return WyzeCredential.parse_obj( + return WyzeCredential.model_validate( dict( resp_json["data"], user_id=auth_info.user_id, @@ -185,7 +186,9 @@ def get_user_info(auth_info: WyzeCredential) -> WyzeAccount: ) resp_json = validate_resp(resp) - return WyzeAccount.parse_obj(dict(resp_json["data"], phone_id=auth_info.phone_id)) + return WyzeAccount.model_validate( + dict(resp_json["data"], phone_id=auth_info.phone_id) + ) def get_homepage_object_list(auth_info: WyzeCredential) -> dict[str, Any]: @@ -204,7 +207,7 @@ def get_camera_list(auth_info: WyzeCredential) -> list[WyzeCamera]: """Return a list of all cameras on the account.""" data = get_homepage_object_list(auth_info) result = [] - for device in data["device_list"]: # type: dict[str, Any] + for device in data["device_list"]: if device["product_type"] != "Camera": continue @@ -308,6 +311,9 @@ def set_device_info( def get_cam_webrtc(auth_info: WyzeCredential, mac_id: str) -> dict: """Get webrtc for camera.""" + if not auth_info.access_token: + raise AccessTokenError() + ui_headers = get_headers() ui_headers["content-type"] = "application/json" ui_headers["authorization"] = auth_info.access_token @@ -339,7 +345,9 @@ def validate_resp(resp): return resp_json -def _get_payload(access_token: Optional[str], phone_id: str, req_path: str = "default"): +def _get_payload( + access_token: Optional[str], phone_id: Optional[str] = "", req_path: str = "default" +): return { "sc": SC_SV[req_path]["sc"], "sv": SC_SV[req_path]["sv"], @@ -353,7 +361,7 @@ def _get_payload(access_token: Optional[str], phone_id: str, req_path: str = "de } -def get_headers(phone_id: str = "") -> dict[str, str]: +def get_headers(phone_id: Optional[str] = "") -> dict[str, str]: if not phone_id: return {"user-agent": SCALE_USER_AGENT} id, key = getenv("API_ID"), getenv("API_KEY") diff --git a/app/wyzecam/tutk/tutk_protocol.py b/app/wyzecam/tutk/tutk_protocol.py index 6d4cd46a..7a1fe767 100644 --- a/app/wyzecam/tutk/tutk_protocol.py +++ b/app/wyzecam/tutk/tutk_protocol.py @@ -888,7 +888,7 @@ class K11000SetRotaryByDegree(TutkWyzeProtocolMessage): """ - def __init__(self, horizontal: int, vertical: int, speed: int = 5): + def __init__(self, horizontal: int, vertical: int = 0, speed: int = 5): super().__init__(11000) self.horizontal = horizontal self.vertical = vertical