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 Jun 29, 2024
2 parents 6315879 + 583ff73 commit 71df0c2
Show file tree
Hide file tree
Showing 17 changed files with 894 additions and 65 deletions.
661 changes: 661 additions & 0 deletions LICENSE

Large diffs are not rendered by default.

17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,19 @@ 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.10

- FIX: `-20021` error when sending multiple ioctl commands to the camera.
- FIX: Regression introduced in v2.9.9 where the WebRTC/HLS icon in WebUI was missing.
- Reduced memory usage slightly.
- NEW: Option to use pre-hashed passwords (#1275):
- You must md5 hash your password three times and prefix it with `hashed:`
- Example: `WYZE_PASSWORD=hashed:<your-tripple-hashed-password>`
- NEW: REST/MQTT commands (#1274):
- `notifications` GET/SET wyze app push notifications on/off (CLOUD).
- `motion_detection` GET/SET motion detection on/off (LOCAL).


## What's Changed in v2.9.9

- FIX: Regression introduced in v2.9.8 where a pipe blocking issue would cause CPU to spike (#1268) (#1270)
Expand Down Expand Up @@ -219,7 +232,7 @@ The container can be run on its own, in [Portainer](https://github.com/mrlt8/doc
## Supported Cameras

> [!IMPORTANT]
> Some newer camera firmware versions may cause issues with remote access via P2P. Local "LAN" access seems unaffected at this time.
> Some newer camera firmware versions may cause issues with remote access via P2P. Local "LAN" access seems unaffected at this time. A temporary solution is to use a VPN. See the [OpenVPN example](https://github.com/mrlt8/docker-wyze-bridge/blob/main/docker-compose.ovpn.yml).
| Camera | Model | Tutk Support | Latest FW |
| ----------------------------- | -------------- | ------------------------------------------------------------ | --------- |
Expand Down Expand Up @@ -258,7 +271,7 @@ This is similar to the docker run command, but will save all your options in a y
Once you're happy with your config you can use `docker-compose up -d` to run it in detached mode.

> [!CAUTION]
> If your credentials have special characters, you must escape them or leave your credentials blank and use the webUI to login.
> If your credentials contain a `$` character, you need to escape it with another `$` sign (e.g., `pa$$word` > `pa$$$$word`) or leave your credentials blank and use the webUI to login.
> [!NOTE]
> You may need to [update the WebUI links](https://github.com/mrlt8/docker-wyze-bridge/wiki/WebUI#custom-ports) if you're changing the ports or using a reverse proxy.
Expand Down
2 changes: 1 addition & 1 deletion app/.env
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
VERSION=2.9.9
VERSION=2.9.10
MTX_TAG=1.8.3
IOS_VERSION=17.1.1
APP_VERSION=2.50.7.10
Expand Down
27 changes: 25 additions & 2 deletions app/wyzebridge/mqtt.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,29 @@ def get_entities(base_topic: str, pan_cam: bool = False, rtsp: bool = False) ->
"icon": "mdi:motion-sensor",
},
},
"motion_detection": {
"type": "switch",
"payload": {
"state_topic": f"{base_topic}motion_detection",
"command_topic": f"{base_topic}motion_detection/set",
"payload_on": 1,
"payload_off": 2,
"state_off": "stopped",
"icon": "mdi:motion-sensor-off",
"entity_category": "diagnostic",
},
},
"notifications": {
"type": "switch",
"payload": {
"state_topic": f"{base_topic}notifications",
"command_topic": f"{base_topic}notifications/set",
"payload_on": 1,
"payload_off": 2,
"icon": "mdi:square-rounded-badge",
"entity_category": "diagnostic",
},
},
"status_light": {
"type": "switch",
"payload": {
Expand Down Expand Up @@ -411,7 +434,7 @@ def get_entities(base_topic: str, pan_cam: bool = False, rtsp: bool = False) ->
"command_topic": f"{base_topic}motion_tracking/set",
"payload_on": 1,
"payload_off": 2,
"icon": "mdi:motion-sensor",
"icon": "mdi:radar",
},
},
"reset_rotation": {
Expand Down Expand Up @@ -456,7 +479,7 @@ def get_entities(base_topic: str, pan_cam: bool = False, rtsp: bool = False) ->
"command_topic": f"{base_topic}rtsp/set",
"payload_on": 1,
"payload_off": 2,
"icon": "mdi:motion-sensor",
"icon": "mdi:server-network",
},
},
}
Expand Down
12 changes: 8 additions & 4 deletions app/wyzebridge/wyze_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,16 +351,20 @@ def get_device_info(self, cam: WyzeCamera, pid: str = ""):
return {"status": "success", "value": item.get("value"), "response": item}

@authenticated
def set_property(self, cam: WyzeCamera, pid: str = ""):
logger.info(f"[CONTROL] ☁️ set_property for {cam.name_uri} via Wyze API")
params = {"device_mac": cam.mac, "device_model": cam.product_model}
def set_property(self, cam: WyzeCamera, pid: str, pvalue: str):
params = {"pid": pid.upper(), "pvalue": pvalue}

logger.info(
f"[CONTROL] ☁️ set_property: {params} for {cam.name_uri} via Wyze API"
)
params |= {"device_mac": cam.mac, "device_model": cam.product_model}
try:
res = post_device(self.auth, "set_property", params, api_version=2)
except (ValueError, WyzeAPIError) as ex:
logger.error(f"[CONTROL] ERROR: {ex}")
return {"status": "error", "response": str(ex)}

return {"status": "success", "response": res["property_list"]}
return {"status": "success", "response": res.get("result")}

@authenticated
def get_events(self, macs: Optional[list] = None, last_ts: int = 0):
Expand Down
7 changes: 6 additions & 1 deletion app/wyzebridge/wyze_commands.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
GET_CMDS = {
"state": None,
"power": None,
"notifications": None,
"update_snapshot": None,
"motion_detection": None,
"take_photo": "K10058TakePhoto",
"irled": "K10044GetIRLEDStatus",
"night_vision": "K10040GetNightVisionStatus",
Expand Down Expand Up @@ -37,6 +39,8 @@
"cruise_point": None,
"fps": None,
"bitrate": None,
"notifications": None,
"motion_detection": None,
"irled": "K10046SetIRLEDStatus",
"night_vision": "K10042SetNightVisionStatus",
"status_light": "K10032SetNetworkLightStatus",
Expand All @@ -59,7 +63,7 @@
"quick_response": "K11635ResponseQuickMessage",
"spotlight": "K10646SetSpotlightStatus",
"floodlight": "K12060SetFloodLightSwitch",
"format_sd" : "K10242FormatSDCard",
"format_sd": "K10242FormatSDCard",
}

CMD_VALUES = {
Expand All @@ -82,6 +86,7 @@
"fps": "5",
"hor_flip": "6",
"ver_flip": "7",
"motion_detection": "13", # K10200GetMotionAlarm
"motion_tagging": "21",
"time_zone": "22",
"motion_tracking": "27",
Expand Down
14 changes: 14 additions & 0 deletions app/wyzebridge/wyze_control.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import socket
import time
from datetime import datetime, timedelta
from multiprocessing import Queue
from queue import Empty
Expand Down Expand Up @@ -162,6 +163,16 @@ def camera_control(sess: WyzeIOTCSession, camera_info: Queue, camera_cmd: Queue)
# Use K10050GetVideoParam if newer firmware
if topic == "bitrate" and is_fw11(sess.camera.firmware_ver):
cmd = "_bitrate"
elif topic == "motion_detection" and payload:
if sess.camera.product_model in (
"WYZEDB3",
"WVOD1",
"HL_WCO2",
"WYZEC1",
):
cmd = "K10202SetMotionAlarm", cmd[1]
else:
cmd = "K10206SetMotionAlarm", cmd[1]
resp = send_tutk_msg(sess, cmd)
if boa and cmd == "take_photo":
pull_last_image(boa, "photo")
Expand Down Expand Up @@ -325,6 +336,9 @@ def parse_cmd(cmd: tuple | str, log: str) -> tuple:
if topic == "_bitrate":
topic = "bitrate"

if topic in {"K10202SetMotionAlarm", "K10206SetMotionAlarm"}:
proto_name = topic

log_msg = f"SET: {topic}={payload}" if set_cmd else f"GET: {topic}"
getattr(logger, log)(f"[CONTROL] Attempting to {log_msg}")
if not proto_name and topic in PARAMS:
Expand Down
13 changes: 13 additions & 0 deletions app/wyzebridge/wyze_stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,16 @@ def power_control(self, payload: str) -> dict:
value="on" if payload == "restart" else payload,
)

def notification_control(self, payload: str) -> dict:
if payload not in {"on", "off", "1", "2", "true", "false"}:
return self.api.get_device_info(self.camera, "P1")

pvalue = "1" if payload in {"on", "1", "true"} else "2"
resp = self.api.set_property(self.camera, "P1", pvalue)
value = None if resp.get("status") == "error" else pvalue

return dict(resp, value=value)

def tz_control(self, payload: str) -> dict:
try:
zone = zoneinfo.ZoneInfo(payload)
Expand All @@ -335,6 +345,9 @@ def send_cmd(self, cmd: str, payload: str | list | dict = "") -> dict:
if cmd == "power":
return self.power_control(str(payload).lower())

if cmd == "notifications":
return self.notification_control(str(payload).lower())

if cmd in {"motion", "motion_ts"}:
return {
"status": "success",
Expand Down
6 changes: 6 additions & 0 deletions app/wyzecam/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,7 @@ def validate_resp(resp: Response) -> dict:
def _payload(
access_token: Optional[str], phone_id: Optional[str] = "", endpoint: str = "default"
) -> dict:
endpoint = endpoint if endpoint in SC_SV else "default"
return {
"sc": SC_SV[endpoint]["sc"],
"sv": SC_SV[endpoint]["sv"],
Expand Down Expand Up @@ -410,6 +411,11 @@ def sign_payload(auth_info: WyzeCredential, app_id: str, payload: str) -> dict:
def hash_password(password: str) -> str:
"""Run hashlib.md5() algorithm 3 times."""
encoded = password.strip()

for ex in {"hashed:", "md5:"}:
if encoded.lower().startswith(ex):
return encoded[len(ex) :]

for _ in range(3):
encoded = md5(encoded.encode("ascii")).hexdigest() # nosec
return encoded
Expand Down
9 changes: 4 additions & 5 deletions app/wyzecam/iotc.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import errno
import fcntl
import hashlib
import io
import logging
import os
import pathlib
Expand Down Expand Up @@ -346,7 +345,7 @@ def session_check(self) -> tutk.SInfoStructEx:

return sess_info

def iotctrl_mux(self) -> TutkIOCtrlMux:
def iotctrl_mux(self, block: bool = True) -> TutkIOCtrlMux:
"""Construct a new TutkIOCtrlMux for this session.
Use this to send configuration messages, such as change the cameras resolution.
Expand All @@ -365,7 +364,7 @@ def iotctrl_mux(self) -> TutkIOCtrlMux:
"""
assert self.av_chan_id is not None, "Please call _connect() first!"
return TutkIOCtrlMux(self.tutk_platform_lib, self.av_chan_id)
return TutkIOCtrlMux(self.tutk_platform_lib, self.av_chan_id, block)

def __enter__(self):
self._connect()
Expand Down Expand Up @@ -530,8 +529,7 @@ 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):
logger.debug("sync camera time")
with self.iotctrl_mux() as mux:
with self.iotctrl_mux(False) as mux:
with contextlib.suppress(tutk_ioctl_mux.Empty, tutk.TutkError):
mux.send_ioctl(tutk_protocol.K10092SetCameraTime()).result(wait)
self.frame_ts = time.time()
Expand Down Expand Up @@ -1055,6 +1053,7 @@ def _auth(self):

def _disconnect(self):
if self.av_chan_id is not None:
tutk.av_send_io_ctrl_exit(self.tutk_platform_lib, self.av_chan_id)
tutk.av_client_stop(self.tutk_platform_lib, self.av_chan_id)
self.av_chan_id = None
if self.session_id is not None:
Expand Down
Loading

0 comments on commit 71df0c2

Please sign in to comment.