Skip to content

Commit

Permalink
Merge branch 'main' into dev
Browse files Browse the repository at this point in the history
  • Loading branch information
mrlt8 authored Jul 18, 2024
2 parents 52bde1d + 8fc97fa commit 0afa612
Show file tree
Hide file tree
Showing 15 changed files with 165 additions and 71 deletions.
21 changes: 0 additions & 21 deletions .github/ISSUE_TEMPLATE/bug_report.md

This file was deleted.

48 changes: 48 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: Bug Report
description: Help improve the bridge by reporting any bugs
title: "BUG: "
labels: ["bug"]
body:
- type: textarea
id: description
attributes:
label: Describe the bug
description: Provide a clear and concise description of the issue and include relevant logs if applicable.
validations:
required: true
- type: markdown
attributes:
value: Additional information to help resolve the issue
- type: input
id: version
attributes:
label: Affected Bridge Version
description: Please include the image tag if applicable
placeholder: e.g. v1.9.10
validations:
required: true
- type: dropdown
id: type
attributes:
label: Bridge type
multiple: true
options:
- Docker Run/Compose
- Home Assistant
- Other
validations:
required: true
- type: input
id: cameras
attributes:
label: Affected Camera(s)
- type: input
id: firmware
attributes:
label: Affected Camera Firmware
- type: textarea
id: config
attributes:
label: docker-compose or config (if applicable)
description: Please be sure to remove any credentials or sensitive information!
render: yaml
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,14 @@ 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.9.11/12

- FIX: Fix regression introduced in v2.9.11 which caused connection issues for WYZEDB3, WVOD1, HL_WCO2, and WYZEC1 (#1294)
- FIX: Update stream state on startup to prevent multiple connections.
- FIX: No audio on HW and QSV builds. (#1281)
- Use k10056 if supported and not setting fps when updating resolution and bitrate (#1194)
- Temporary fix: Don't check bitrate on newer firmware which do not seem to report the actual bitrate. (#1194)

## What's Changed in v2.9.10

- FIX: `-20021` error when sending multiple ioctl commands to the camera.
Expand Down
6 changes: 3 additions & 3 deletions app/.env
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
VERSION=2.9.10
MTX_TAG=1.8.3
VERSION=2.9.12
MTX_TAG=1.8.4
IOS_VERSION=17.1.1
APP_VERSION=2.50.7.10
APP_VERSION=2.50.9.1
MTX_HLSVARIANT=fmp4
MTX_PROTOCOLS=tcp
MTX_READTIMEOUT=20s
Expand Down
41 changes: 29 additions & 12 deletions app/wyzebridge/bridge_utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import contextlib
import os
import shutil
from typing import Any, Optional
from typing import Any

from wyzecam.api_models import WyzeCamera

Expand Down Expand Up @@ -67,16 +66,34 @@ def is_livestream(uri: str) -> bool:
return any(env_bool(f"{service}_{uri}") for service in services)


def is_fw11(fw_ver: Optional[str]) -> bool:
"""
Check if newer firmware that needs to use K10050GetVideoParam
"""
with contextlib.suppress(IndexError, ValueError):
if fw_ver and fw_ver.startswith(("4.51", "4.52", "4.53", "4.50.4")):
return True
if fw_ver and int(fw_ver.split(".")[2]) > 10:
return True
return False
def get_secret(name: str) -> str:
if not name:
return ""
try:
with open(f"/run/secrets/{name.upper()}", "r") as f:
return f.read().strip("'\" \n\t\r")
except FileNotFoundError:
return env_bool(name, style="original")


def get_password(
file_name: str, alt: str = "", path: str = "", length: int = 16
) -> str:
if env_pass := (get_secret(file_name) or get_secret(alt)):
return env_pass

file_path = f"{path}{file_name}"
if os.path.exists(file_path) and os.path.getsize(file_path) > 0:
with open(file_path, "r") as file:
return file.read().strip()

password = secrets.token_urlsafe(length)
with open(file_path, "w") as file:
file.write(password)

print(f"\n\nDEFAULT {file_name.upper()}:\n{password=}")

return password


def migrate_path(old: str, new: str):
Expand Down
5 changes: 4 additions & 1 deletion app/wyzebridge/wyze_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ def run_action(self, cam: WyzeCamera, action: str):
return {"status": "error", "response": str(ex)}

@authenticated
def get_device_info(self, cam: WyzeCamera, pid: str = ""):
def get_device_info(self, cam: WyzeCamera, pid: str = "", cmd: str = ""):
logger.info(f"[CONTROL] ☁️ get_device_Info for {cam.name_uri} via Wyze API")
params = {"device_mac": cam.mac, "device_model": cam.product_model}
try:
Expand All @@ -309,6 +309,9 @@ def get_device_info(self, cam: WyzeCamera, pid: str = ""):
logger.error(f"[CONTROL] ERROR: {ex}")
return {"status": "error", "response": str(ex)}

if cmd in resp:
return {"status": "success", "response": resp[cmd]}

if not pid:
return {"status": "success", "response": property_list}

Expand Down
44 changes: 35 additions & 9 deletions app/wyzebridge/wyze_control.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
import socket
import time
from datetime import datetime, timedelta
from multiprocessing import Queue
from queue import Empty
from re import findall
from typing import Any, Optional

import requests
from wyzebridge.bridge_utils import env_bool, is_fw11
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, publish_messages
from wyzebridge.wyze_commands import CMD_VALUES, GET_CMDS, GET_PAYLOAD, PARAMS, SET_CMDS
from wyzecam import WyzeIOTCSession, WyzeIOTCSessionState, tutk_protocol
from wyzecam import WyzeIOTCSession, tutk_protocol
from wyzecam.tutk.tutk import TutkError

REQ_K10050 = ["4.51", "4.52", "4.53", "4.50.4"]
"""Firmware versions that require K10050GetVideoParam to get bitrate."""

NO_BITRATE = ["4.36.12", "4.50.4.9222"]
"""Firmware versions that are broken and no longer return the actual bitrate."""


def cam_http_alive(ip: str) -> bool:
"""Test if camera http server is up."""
Expand Down Expand Up @@ -161,7 +166,7 @@ def camera_control(sess: WyzeIOTCSession, camera_info: Queue, camera_cmd: Queue)
resp = update_bit_fps(sess, topic, payload)
else:
# Use K10050GetVideoParam if newer firmware
if topic == "bitrate" and is_fw11(sess.camera.firmware_ver):
if topic == "bitrate" and fw_check(sess.camera.firmware_ver, REQ_K10050):
cmd = "_bitrate"
elif topic == "motion_detection" and payload:
if sess.camera.product_model in (
Expand All @@ -186,13 +191,13 @@ def update_params(sess: WyzeIOTCSession):
"""
if not sess.should_stream(0):
return
fw_11 = is_fw11(sess.camera.firmware_ver)
newer_firmware = fw_check(sess.camera.firmware_ver, REQ_K10050)

if MQTT_ENABLED or not fw_11:
remove = {"bitrate", "res"} if fw_11 else set()
if MQTT_ENABLED or not newer_firmware:
remove = {"bitrate", "res"} if newer_firmware else set()
params = ",".join([v for k, v in PARAMS.items() if k not in remove])
send_tutk_msg(sess, ("param_info", params), "debug")
if fw_11:
if newer_firmware:
send_tutk_msg(sess, "_bitrate", "debug")


Expand Down Expand Up @@ -276,7 +281,8 @@ def send_tutk_msg(sess: WyzeIOTCSession, cmd: tuple | str, log: str = "info") ->
elif res := iotc.result(timeout=5):
if tutk_msg.code in {10020, 10050}:
update_mqtt_values(sess.camera.name_uri, res)
res = bitrate_check(sess, res, resp["command"])
if not fw_check(sess.camera.firmware_ver, NO_BITRATE):
res = bitrate_check(sess, res, resp["command"])
params = None
if isinstance(res, bytes):
res = ",".join(map(str, res))
Expand Down Expand Up @@ -388,3 +394,23 @@ def motion_alarm(cam: dict):
resp.raise_for_status()
except requests.exceptions.HTTPError as ex:
logger.error(ex)


def parse_fw(fw_ver: str) -> tuple[str, tuple[int, ...]]:
parts = fw_ver.split(".")
if len(parts) < 4:
parts.extend(["0"] * (4 - len(parts)))
return ".".join(parts[:2]), tuple(map(int, parts[2:]))


def fw_check(fw_ver: Optional[str], min_fw_ver: list) -> bool:
"""Check firmware compatibility."""
if not fw_ver:
return False
min_fw = {fw_type: ver_parts for fw_type, ver_parts in map(parse_fw, min_fw_ver)}

fw_type, version = parse_fw(fw_ver)
if version and version >= min_fw.get(fw_type, (11, 0)):
return True

return False
3 changes: 3 additions & 0 deletions app/wyzebridge/wyze_stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ def enabled(self) -> bool:
def start(self) -> bool:
if self.health_check(False) != StreamStatus.STOPPED:
return False
self.state = StreamStatus.CONNECTING
logger.info(
f"🎉 Connecting to WyzeCam {self.camera.model_name} - {self.camera.nickname} on {self.camera.ip}"
)
Expand Down Expand Up @@ -338,6 +339,8 @@ def send_cmd(self, cmd: str, payload: str | list | dict = "") -> dict:

if cmd == "device_info":
return self.api.get_device_info(self.camera)
if cmd == "device_setting":
return self.api.get_device_info(self.camera, cmd="device_setting")

if cmd == "battery":
return self.api.get_device_info(self.camera, "P8")
Expand Down
50 changes: 26 additions & 24 deletions app/wyzecam/iotc.py
Original file line number Diff line number Diff line change
Expand Up @@ -512,11 +512,24 @@ def valid_frame_size(self) -> set[int]:
return {self.preferred_frame_size, int(os.getenv("IGNORE_RES", alt))}

def sync_camera_time(self, wait: bool = False):
with self.iotctrl_mux(False) as mux:
with contextlib.suppress(tutk_ioctl_mux.Empty, tutk.TutkError):
with contextlib.suppress(tutk_ioctl_mux.Empty, tutk.TutkError):
with self.iotctrl_mux(False) as mux:
mux.send_ioctl(tutk_protocol.K10092SetCameraTime()).result(wait)
self.frame_ts = time.time()

def set_resolving_bit(self, fps: int = 0):
if fps or self.camera.product_model in {
"WYZEDB3",
"WVOD1",
"HL_WCO2",
"WYZEC1",
}:
return K10052DBSetResolvingBit(
self.preferred_frame_size, self.preferred_bitrate, fps
)

return K10056SetResolvingBit(self.preferred_frame_size, self.preferred_bitrate)

def update_frame_size_rate(self, bitrate: Optional[int] = None, fps: int = 0):
"""Send a message to the camera to update the frame_size and bitrate."""
if bitrate:
Expand All @@ -526,11 +539,12 @@ def update_frame_size_rate(self, bitrate: Optional[int] = None, fps: int = 0):
self.preferred_frame_rate = fps
self.sync_camera_time()

ioctl_params = self.preferred_frame_size, self.preferred_bitrate, fps
logger.warning("Requesting frame_size=%d, bitrate=%d, fps=%d" % ioctl_params)
params = self.preferred_frame_size, self.preferred_bitrate, fps
logger.warning("Requesting frame_size=%d, bitrate=%d, fps=%d" % params)

with self.iotctrl_mux() as mux:
with contextlib.suppress(tutk_ioctl_mux.Empty):
mux.send_ioctl(K10052DBSetResolvingBit(*ioctl_params)).result(False)
mux.send_ioctl(self.set_resolving_bit(fps)).result(False)

def clear_buffer(self) -> None:
"""Clear local buffer."""
Expand All @@ -555,9 +569,7 @@ def flush_pipe(self, pipe_type: str = "audio", gap: float = 0):
except Exception as e:
logger.warning(f"Flushing Error: {e}")

def recv_audio_data(
self,
) -> Iterator[tuple[bytes, Optional[tutk.FrameInfo3Struct]]]:
def recv_audio_data(self) -> Iterator[bytes]:
assert self.av_chan_id is not None, "Please call _connect() first!"
try:
while self.should_stream():
Expand All @@ -570,10 +582,9 @@ def recv_audio_data(
continue

assert frame_info is not None, "Empty frame_info without an error!"
if self._audio_frame_slow(frame_info):
continue
self._sync_audio_frame(frame_info)

yield frame_data, frame_info
yield frame_data

except tutk.TutkError as ex:
warnings.warn(ex.name)
Expand All @@ -590,7 +601,7 @@ def recv_audio_pipe(self) -> None:
with open(fifo_path, "wb", buffering=0) as audio_pipe:
set_non_blocking(audio_pipe)
self.audio_pipe_ready = True
for frame_data, _ in self.recv_audio_data():
for frame_data in self.recv_audio_data():
with contextlib.suppress(BlockingIOError):
audio_pipe.write(frame_data)

Expand All @@ -603,7 +614,7 @@ def recv_audio_pipe(self) -> None:
os.unlink(fifo_path)
warnings.warn("Audio pipe closed")

def _audio_frame_slow(self, frame_info) -> Optional[bool]:
def _sync_audio_frame(self, frame_info):
# Some cams can't sync
if frame_info.timestamp < 1591069888:
return
Expand Down Expand Up @@ -792,17 +803,8 @@ def _auth(self):
warnings.warn(f"AUTH FAILED: {auth_response}")
raise ValueError("AUTH_FAILED")
self.camera.set_camera_info(auth_response["cameraInfo"])
frame_bit = self.preferred_frame_size, self.preferred_bitrate
if self.camera.product_model in (
"WYZEDB3",
"WVOD1",
"HL_WCO2",
"WYZEC1",
):
ioctl_msg = K10052DBSetResolvingBit(*frame_bit)
else:
ioctl_msg = K10056SetResolvingBit(*frame_bit)
mux.waitfor(mux.send_ioctl(ioctl_msg))

mux.send_ioctl(self.set_resolving_bit()).result()
self.state = WyzeIOTCSessionState.AUTHENTICATION_SUCCEEDED
except tutk.TutkError:
self._disconnect()
Expand Down
8 changes: 8 additions & 0 deletions home_assistant/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
## What's Changed in v2.9.11/12

- FIX: Fix regression introduced in v2.9.11 which caused connection issues for WYZEDB3, WVOD1, HL_WCO2, and WYZEC1 (#1294)
- FIX: Update stream state on startup to prevent multiple connections.
- FIX: No audio on HW and QSV builds. (#1281)
- Use k10056 if supported and not setting fps when updating resolution and bitrate (#1194)
- Temporary fix: Don't check bitrate on newer firmware which do not seem to report the actual bitrate. (#1194)

## What's Changed in v2.9.10

- FIX: `-20021` error when sending multiple ioctl commands to the camera.
Expand Down
Loading

0 comments on commit 0afa612

Please sign in to comment.