Skip to content

Commit 0afa612

Browse files
authored
Merge branch 'main' into dev
2 parents 52bde1d + 8fc97fa commit 0afa612

File tree

15 files changed

+165
-71
lines changed

15 files changed

+165
-71
lines changed

.github/ISSUE_TEMPLATE/bug_report.md

Lines changed: 0 additions & 21 deletions
This file was deleted.

.github/ISSUE_TEMPLATE/bug_report.yml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
name: Bug Report
2+
description: Help improve the bridge by reporting any bugs
3+
title: "BUG: "
4+
labels: ["bug"]
5+
body:
6+
- type: textarea
7+
id: description
8+
attributes:
9+
label: Describe the bug
10+
description: Provide a clear and concise description of the issue and include relevant logs if applicable.
11+
validations:
12+
required: true
13+
- type: markdown
14+
attributes:
15+
value: Additional information to help resolve the issue
16+
- type: input
17+
id: version
18+
attributes:
19+
label: Affected Bridge Version
20+
description: Please include the image tag if applicable
21+
placeholder: e.g. v1.9.10
22+
validations:
23+
required: true
24+
- type: dropdown
25+
id: type
26+
attributes:
27+
label: Bridge type
28+
multiple: true
29+
options:
30+
- Docker Run/Compose
31+
- Home Assistant
32+
- Other
33+
validations:
34+
required: true
35+
- type: input
36+
id: cameras
37+
attributes:
38+
label: Affected Camera(s)
39+
- type: input
40+
id: firmware
41+
attributes:
42+
label: Affected Camera Firmware
43+
- type: textarea
44+
id: config
45+
attributes:
46+
label: docker-compose or config (if applicable)
47+
description: Please be sure to remove any credentials or sensitive information!
48+
render: yaml

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,14 @@ You can then use the web interface at `http://localhost:5000` where localhost is
5959

6060
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.
6161

62+
## What's Changed in v2.9.11/12
63+
64+
- FIX: Fix regression introduced in v2.9.11 which caused connection issues for WYZEDB3, WVOD1, HL_WCO2, and WYZEC1 (#1294)
65+
- FIX: Update stream state on startup to prevent multiple connections.
66+
- FIX: No audio on HW and QSV builds. (#1281)
67+
- Use k10056 if supported and not setting fps when updating resolution and bitrate (#1194)
68+
- Temporary fix: Don't check bitrate on newer firmware which do not seem to report the actual bitrate. (#1194)
69+
6270
## What's Changed in v2.9.10
6371

6472
- FIX: `-20021` error when sending multiple ioctl commands to the camera.

app/.env

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
VERSION=2.9.10
2-
MTX_TAG=1.8.3
1+
VERSION=2.9.12
2+
MTX_TAG=1.8.4
33
IOS_VERSION=17.1.1
4-
APP_VERSION=2.50.7.10
4+
APP_VERSION=2.50.9.1
55
MTX_HLSVARIANT=fmp4
66
MTX_PROTOCOLS=tcp
77
MTX_READTIMEOUT=20s

app/wyzebridge/bridge_utils.py

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import contextlib
21
import os
32
import shutil
4-
from typing import Any, Optional
3+
from typing import Any
54

65
from wyzecam.api_models import WyzeCamera
76

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

6968

70-
def is_fw11(fw_ver: Optional[str]) -> bool:
71-
"""
72-
Check if newer firmware that needs to use K10050GetVideoParam
73-
"""
74-
with contextlib.suppress(IndexError, ValueError):
75-
if fw_ver and fw_ver.startswith(("4.51", "4.52", "4.53", "4.50.4")):
76-
return True
77-
if fw_ver and int(fw_ver.split(".")[2]) > 10:
78-
return True
79-
return False
69+
def get_secret(name: str) -> str:
70+
if not name:
71+
return ""
72+
try:
73+
with open(f"/run/secrets/{name.upper()}", "r") as f:
74+
return f.read().strip("'\" \n\t\r")
75+
except FileNotFoundError:
76+
return env_bool(name, style="original")
77+
78+
79+
def get_password(
80+
file_name: str, alt: str = "", path: str = "", length: int = 16
81+
) -> str:
82+
if env_pass := (get_secret(file_name) or get_secret(alt)):
83+
return env_pass
84+
85+
file_path = f"{path}{file_name}"
86+
if os.path.exists(file_path) and os.path.getsize(file_path) > 0:
87+
with open(file_path, "r") as file:
88+
return file.read().strip()
89+
90+
password = secrets.token_urlsafe(length)
91+
with open(file_path, "w") as file:
92+
file.write(password)
93+
94+
print(f"\n\nDEFAULT {file_name.upper()}:\n{password=}")
95+
96+
return password
8097

8198

8299
def migrate_path(old: str, new: str):

app/wyzebridge/wyze_api.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ def run_action(self, cam: WyzeCamera, action: str):
299299
return {"status": "error", "response": str(ex)}
300300

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

312+
if cmd in resp:
313+
return {"status": "success", "response": resp[cmd]}
314+
312315
if not pid:
313316
return {"status": "success", "response": property_list}
314317

app/wyzebridge/wyze_control.py

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,25 @@
11
import socket
2-
import time
32
from datetime import datetime, timedelta
43
from multiprocessing import Queue
54
from queue import Empty
65
from re import findall
76
from typing import Any, Optional
87

98
import requests
10-
from wyzebridge.bridge_utils import env_bool, is_fw11
9+
from wyzebridge.bridge_utils import env_bool
1110
from wyzebridge.config import BOA_COOLDOWN, BOA_INTERVAL, IMG_PATH, MQTT_TOPIC
1211
from wyzebridge.logging import logger
1312
from wyzebridge.mqtt import MQTT_ENABLED, publish_messages
1413
from wyzebridge.wyze_commands import CMD_VALUES, GET_CMDS, GET_PAYLOAD, PARAMS, SET_CMDS
15-
from wyzecam import WyzeIOTCSession, WyzeIOTCSessionState, tutk_protocol
14+
from wyzecam import WyzeIOTCSession, tutk_protocol
1615
from wyzecam.tutk.tutk import TutkError
1716

17+
REQ_K10050 = ["4.51", "4.52", "4.53", "4.50.4"]
18+
"""Firmware versions that require K10050GetVideoParam to get bitrate."""
19+
20+
NO_BITRATE = ["4.36.12", "4.50.4.9222"]
21+
"""Firmware versions that are broken and no longer return the actual bitrate."""
22+
1823

1924
def cam_http_alive(ip: str) -> bool:
2025
"""Test if camera http server is up."""
@@ -161,7 +166,7 @@ def camera_control(sess: WyzeIOTCSession, camera_info: Queue, camera_cmd: Queue)
161166
resp = update_bit_fps(sess, topic, payload)
162167
else:
163168
# Use K10050GetVideoParam if newer firmware
164-
if topic == "bitrate" and is_fw11(sess.camera.firmware_ver):
169+
if topic == "bitrate" and fw_check(sess.camera.firmware_ver, REQ_K10050):
165170
cmd = "_bitrate"
166171
elif topic == "motion_detection" and payload:
167172
if sess.camera.product_model in (
@@ -186,13 +191,13 @@ def update_params(sess: WyzeIOTCSession):
186191
"""
187192
if not sess.should_stream(0):
188193
return
189-
fw_11 = is_fw11(sess.camera.firmware_ver)
194+
newer_firmware = fw_check(sess.camera.firmware_ver, REQ_K10050)
190195

191-
if MQTT_ENABLED or not fw_11:
192-
remove = {"bitrate", "res"} if fw_11 else set()
196+
if MQTT_ENABLED or not newer_firmware:
197+
remove = {"bitrate", "res"} if newer_firmware else set()
193198
params = ",".join([v for k, v in PARAMS.items() if k not in remove])
194199
send_tutk_msg(sess, ("param_info", params), "debug")
195-
if fw_11:
200+
if newer_firmware:
196201
send_tutk_msg(sess, "_bitrate", "debug")
197202

198203

@@ -276,7 +281,8 @@ def send_tutk_msg(sess: WyzeIOTCSession, cmd: tuple | str, log: str = "info") ->
276281
elif res := iotc.result(timeout=5):
277282
if tutk_msg.code in {10020, 10050}:
278283
update_mqtt_values(sess.camera.name_uri, res)
279-
res = bitrate_check(sess, res, resp["command"])
284+
if not fw_check(sess.camera.firmware_ver, NO_BITRATE):
285+
res = bitrate_check(sess, res, resp["command"])
280286
params = None
281287
if isinstance(res, bytes):
282288
res = ",".join(map(str, res))
@@ -388,3 +394,23 @@ def motion_alarm(cam: dict):
388394
resp.raise_for_status()
389395
except requests.exceptions.HTTPError as ex:
390396
logger.error(ex)
397+
398+
399+
def parse_fw(fw_ver: str) -> tuple[str, tuple[int, ...]]:
400+
parts = fw_ver.split(".")
401+
if len(parts) < 4:
402+
parts.extend(["0"] * (4 - len(parts)))
403+
return ".".join(parts[:2]), tuple(map(int, parts[2:]))
404+
405+
406+
def fw_check(fw_ver: Optional[str], min_fw_ver: list) -> bool:
407+
"""Check firmware compatibility."""
408+
if not fw_ver:
409+
return False
410+
min_fw = {fw_type: ver_parts for fw_type, ver_parts in map(parse_fw, min_fw_ver)}
411+
412+
fw_type, version = parse_fw(fw_ver)
413+
if version and version >= min_fw.get(fw_type, (11, 0)):
414+
return True
415+
416+
return False

app/wyzebridge/wyze_stream.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ def enabled(self) -> bool:
149149
def start(self) -> bool:
150150
if self.health_check(False) != StreamStatus.STOPPED:
151151
return False
152+
self.state = StreamStatus.CONNECTING
152153
logger.info(
153154
f"🎉 Connecting to WyzeCam {self.camera.model_name} - {self.camera.nickname} on {self.camera.ip}"
154155
)
@@ -338,6 +339,8 @@ def send_cmd(self, cmd: str, payload: str | list | dict = "") -> dict:
338339

339340
if cmd == "device_info":
340341
return self.api.get_device_info(self.camera)
342+
if cmd == "device_setting":
343+
return self.api.get_device_info(self.camera, cmd="device_setting")
341344

342345
if cmd == "battery":
343346
return self.api.get_device_info(self.camera, "P8")

app/wyzecam/iotc.py

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -512,11 +512,24 @@ def valid_frame_size(self) -> set[int]:
512512
return {self.preferred_frame_size, int(os.getenv("IGNORE_RES", alt))}
513513

514514
def sync_camera_time(self, wait: bool = False):
515-
with self.iotctrl_mux(False) as mux:
516-
with contextlib.suppress(tutk_ioctl_mux.Empty, tutk.TutkError):
515+
with contextlib.suppress(tutk_ioctl_mux.Empty, tutk.TutkError):
516+
with self.iotctrl_mux(False) as mux:
517517
mux.send_ioctl(tutk_protocol.K10092SetCameraTime()).result(wait)
518518
self.frame_ts = time.time()
519519

520+
def set_resolving_bit(self, fps: int = 0):
521+
if fps or self.camera.product_model in {
522+
"WYZEDB3",
523+
"WVOD1",
524+
"HL_WCO2",
525+
"WYZEC1",
526+
}:
527+
return K10052DBSetResolvingBit(
528+
self.preferred_frame_size, self.preferred_bitrate, fps
529+
)
530+
531+
return K10056SetResolvingBit(self.preferred_frame_size, self.preferred_bitrate)
532+
520533
def update_frame_size_rate(self, bitrate: Optional[int] = None, fps: int = 0):
521534
"""Send a message to the camera to update the frame_size and bitrate."""
522535
if bitrate:
@@ -526,11 +539,12 @@ def update_frame_size_rate(self, bitrate: Optional[int] = None, fps: int = 0):
526539
self.preferred_frame_rate = fps
527540
self.sync_camera_time()
528541

529-
ioctl_params = self.preferred_frame_size, self.preferred_bitrate, fps
530-
logger.warning("Requesting frame_size=%d, bitrate=%d, fps=%d" % ioctl_params)
542+
params = self.preferred_frame_size, self.preferred_bitrate, fps
543+
logger.warning("Requesting frame_size=%d, bitrate=%d, fps=%d" % params)
544+
531545
with self.iotctrl_mux() as mux:
532546
with contextlib.suppress(tutk_ioctl_mux.Empty):
533-
mux.send_ioctl(K10052DBSetResolvingBit(*ioctl_params)).result(False)
547+
mux.send_ioctl(self.set_resolving_bit(fps)).result(False)
534548

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

558-
def recv_audio_data(
559-
self,
560-
) -> Iterator[tuple[bytes, Optional[tutk.FrameInfo3Struct]]]:
572+
def recv_audio_data(self) -> Iterator[bytes]:
561573
assert self.av_chan_id is not None, "Please call _connect() first!"
562574
try:
563575
while self.should_stream():
@@ -570,10 +582,9 @@ def recv_audio_data(
570582
continue
571583

572584
assert frame_info is not None, "Empty frame_info without an error!"
573-
if self._audio_frame_slow(frame_info):
574-
continue
585+
self._sync_audio_frame(frame_info)
575586

576-
yield frame_data, frame_info
587+
yield frame_data
577588

578589
except tutk.TutkError as ex:
579590
warnings.warn(ex.name)
@@ -590,7 +601,7 @@ def recv_audio_pipe(self) -> None:
590601
with open(fifo_path, "wb", buffering=0) as audio_pipe:
591602
set_non_blocking(audio_pipe)
592603
self.audio_pipe_ready = True
593-
for frame_data, _ in self.recv_audio_data():
604+
for frame_data in self.recv_audio_data():
594605
with contextlib.suppress(BlockingIOError):
595606
audio_pipe.write(frame_data)
596607

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

606-
def _audio_frame_slow(self, frame_info) -> Optional[bool]:
617+
def _sync_audio_frame(self, frame_info):
607618
# Some cams can't sync
608619
if frame_info.timestamp < 1591069888:
609620
return
@@ -792,17 +803,8 @@ def _auth(self):
792803
warnings.warn(f"AUTH FAILED: {auth_response}")
793804
raise ValueError("AUTH_FAILED")
794805
self.camera.set_camera_info(auth_response["cameraInfo"])
795-
frame_bit = self.preferred_frame_size, self.preferred_bitrate
796-
if self.camera.product_model in (
797-
"WYZEDB3",
798-
"WVOD1",
799-
"HL_WCO2",
800-
"WYZEC1",
801-
):
802-
ioctl_msg = K10052DBSetResolvingBit(*frame_bit)
803-
else:
804-
ioctl_msg = K10056SetResolvingBit(*frame_bit)
805-
mux.waitfor(mux.send_ioctl(ioctl_msg))
806+
807+
mux.send_ioctl(self.set_resolving_bit()).result()
806808
self.state = WyzeIOTCSessionState.AUTHENTICATION_SUCCEEDED
807809
except tutk.TutkError:
808810
self._disconnect()

home_assistant/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
## What's Changed in v2.9.11/12
2+
3+
- FIX: Fix regression introduced in v2.9.11 which caused connection issues for WYZEDB3, WVOD1, HL_WCO2, and WYZEC1 (#1294)
4+
- FIX: Update stream state on startup to prevent multiple connections.
5+
- FIX: No audio on HW and QSV builds. (#1281)
6+
- Use k10056 if supported and not setting fps when updating resolution and bitrate (#1194)
7+
- Temporary fix: Don't check bitrate on newer firmware which do not seem to report the actual bitrate. (#1194)
8+
19
## What's Changed in v2.9.10
210

311
- FIX: `-20021` error when sending multiple ioctl commands to the camera.

0 commit comments

Comments
 (0)