Skip to content

Commit

Permalink
v2.9.0 (#1184)
Browse files Browse the repository at this point in the history
* Sign motion event request #1125

* use device id instead of mac #1125

* remove wrong flags from mkfifo #1174

* less aggressive flush #1159 #1167

* Use K10052 for setting FPS #1161

* Refactor bits for going above 255 for quality

* Revert ffmpeg changes #1159 #1167

* version is obsolete

* default quality to hd180

* Update Wyze iOS App version from v2.44.5.3 to v2.50.6.1 (#1176)

* use struct pack

* Token based auth over the webUI

* Fix redirect for Home Assistant Ingress

* Use request headers to fix redirect for HA

* Change GET to POST for webhooks data

* Deprecate ifttt_webhook in favor of webhooks

* use yml for HA config and make credentials optional

* keep trying to identify audio #1172

* Snapshot on motion and push to mqtt #709 #970

* Add event time to motion message

* refactor auth

* EVENT_API option #1125

* Add additional headers  #1125

* Audio sync with higher bitrate

* Debug api request #1125

* Update api.py

* only debug on error

* Tweak audio sync

* don't raise error on lost frame

* clear buffer if out of sync

* Unique macs only #1125

Co-Authored-By: Cameron <32912464+kiwi-cam@users.noreply.github.com>

* Require auth by default and block non-ingress access #1181

* Allow non-ingress access with auth #1181

* Remove retain flag from commands #1182

* update webrtc to work with auth streams

* Add WB_API and rename WEB to WB #1181

WEB_USERNAME > WB_USERNAME
WEB_PASSWORD > WB_PASSWORD

* HA move /config/wyze-bridge/ to /config/

* Don't notify substream event and remove v2 #1125

* WebUI Auth related config for HA

* Don't retain discovery message? #1182

* Case sensitive credentials for WebUI

* changelog and readme

---------

Co-authored-by: Cameron <32912464+kiwi-cam@users.noreply.github.com>
  • Loading branch information
mrlt8 and kiwi-cam committed May 12, 2024
1 parent af5a152 commit 3806621
Show file tree
Hide file tree
Showing 33 changed files with 969 additions and 659 deletions.
5 changes: 2 additions & 3 deletions .github/workflows/docker-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -133,13 +133,12 @@ jobs:
TAG_NAME=${GITHUB_REF##*/v}
if [[ $TAG_NAME =~ ^[0-9]+\.[0-9]+\.[0-9]+(-.*)?$ ]]; then
sed -i "s/^VERSION=.*/VERSION=${TAG_NAME}/" ./app/.env
jq --arg VERSION "${TAG_NAME}" '.version = $VERSION' ./home_assistant/config.json > updated.json
mv updated.json ./home_assistant/config.json
sed -i "s/^version: .*/version: ${TAG_NAME}/" ./home_assistant/config.yaml
echo "tag=${TAG_NAME}" >> $GITHUB_OUTPUT
fi
- name: Commit and push changes
uses: stefanzweifel/git-auto-commit-action@v5
with:
branch: main
commit_message: 'Bump Version to v${{ steps.version_bump.outputs.tag }}'
file_pattern: 'app/.env home_assistant/config.json'
file_pattern: 'app/.env home_assistant/config.yaml'
65 changes: 52 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ Please consider ⭐️ starring or [☕️ sponsoring](https://ko-fi.com/mrlt8)


> [!IMPORTANT]
> As of April 2024, you will need to **update your bridge to v2.3.x or newer** for compatibility with the latest changes to the Wyze API as well as supply your own API Key and API ID from: https://support.wyze.com/hc/en-us/articles/16129834216731.
> As of May 2024, you will need an API Key and API ID from: https://support.wyze.com/hc/en-us/articles/16129834216731.
> [!WARNING] Please double check your router/firewall and do NOT forward ports or enable DMZ access to the bridge unless you know what you are doing!

![Wyze Cam V1](https://img.shields.io/badge/wyze_v1-yes-success.svg)
Expand Down Expand Up @@ -56,26 +58,63 @@ 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.8.2/3
## What's Changed in v2.9.0

> [!IMPORTANT] WebUI and stream authentication will be enabled by default to prevent unintentional access.
**Default Authentication**

- To disable default authentication, set `WB_AUTH=False` explicitly.
- Note that all streams and the REST API will necessitate authentication when `WB_AUTH` is enabled.

**WebUI Authentication**

* Add support for developer API Key/ID for WebUI based logins.
* Update Home Assistant and unraid config to support API Key/ID
* Refactor to catch additional WyzeAPIErrors.
- If `WB_USERNAME` and `WB_PASSWORD` are not set, the system will try to use `WYZE_EMAIL` and `WYZE_PASSWORD`.
- In case neither sets of credentials are provided, the username will default to `wbadmin` with a randomly generated `WB_PASSWORD`, which will be logged and stored in a `wb_password` file within the tokens directory.
- Credentials are case sensitive.

## What's Changed in v2.8.1
**Stream and REST API Authentication**
- A unique API key will be accessible at the bottom of your WebUI and saved to a `wb_api` file in your tokens directory.
- For persistence, ensure to set the `WB_API` environment variable or volume mount the `/tokens` directory.
- REST API will require an `api` query parameter.
- Example: `http://localhost:5000/api/<camera-name>/state?api=<your-wb-api-key>`
- Streams will also require authentication.
- username: `wb`
- password: your unique wb api key

* Fix video lag introduced in v2.7.0
* Add aac_eld audio support for V4 cams (HL_CAM4).
* Add 2k resolution support for Floodlight V2 cams (HL_CFL2).
* Fix version number
**FIXES**
- Wrong file permission caused errors for non-root. (#1174) Thanks @GiZZoR!
- Fix `MOTION_API` when substreams were enabled. (#1125) Thanks @kiwi-cam!
- Changing FPS and `FORCE_FPS` were broken (#1161) Thanks @jarrah31!
- Dropped frame issue when camera is falling behind. (#1167) Thanks @34t614t1254y!

Home Assistant:
**NEW**
- Token based wyze authentication from WebUI. See [wiki](https://github.com/mrlt8/docker-wyze-bridge/wiki/Authentication#token-based-authentication).
- Remove 255 limit from `QUALITY`. Can now go as high as your network can handle. e.g. `- QUALITY=HD8000`
- Update snapshot with `MOTION_API` and push to mqtt (#709) (#970)
- Additional headers for `MOTION_WEBHOOKS`.
- `OFFLINE_WEBHOOKS` will send a POST request when the bridge cannot connect to a camera because it is offline. Replaces `ifttt_webhook`.

**POTENTIALLY BREAKING**
- CHANGES: `MOTION_WEBHOOKS` now makes a POST request instead of a GET request.
- CHANGES: `MOTION_WEBHOOKS` includes the event timestamp in the message body which may require you to adjust the timezone for your container with the `TZ` environment.
- REMOVED: `ifttt_webhook` as webhooks are no longer free with IFTTT.
- CHANGED: Renamed WebUI authentication related ENV options:
- `WEB_AUTH` -> `WB_AUTH`
- `WEB_USERNAME` -> `WB_USERNAME`
- `WEB_PASSWORD` -> `WB_PASSWORD`

**HOME ASSISTANT**
- Login with API Key/ID or existing token via Ingress/WebUI.
- Config now uses yaml instead of json.
- Credentials are now optional to allow for WebUI based login, but it is still recommended to set them under advanced options.

* Add dev and previous builds (v2.6.0) to the repo.
* Note: you may need to re-add the repo if you cannot see the latest updates.

[View previous changes](https://github.com/mrlt8/docker-wyze-bridge/releases)

> [!TIP] Home Assistant: you may need to re-add the repo if you cannot see the latest updates.

## FAQ

* How does this work?
Expand Down
2 changes: 1 addition & 1 deletion app/.env
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
VERSION=2.8.3
MTX_TAG=1.1.1
IOS_VERSION=17.1.1
APP_VERSION=2.44.5.3
APP_VERSION=2.50.6.1
MTX_HLSVARIANT=fmp4
MTX_PROTOCOLS=tcp
MTX_READTIMEOUT=20s
Expand Down
99 changes: 53 additions & 46 deletions app/frontend.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import os
import time
from functools import wraps
from pathlib import Path
from urllib.parse import quote_plus, urlparse
from urllib.parse import quote_plus

from flask import (
Flask,
Expand All @@ -12,24 +13,10 @@
request,
send_from_directory,
)
from flask_httpauth import HTTPBasicAuth
from werkzeug.exceptions import NotFound
from werkzeug.security import check_password_hash, generate_password_hash
from wyze_bridge import WyzeBridge
from wyzebridge import config, web_ui

auth = HTTPBasicAuth()
auth_enabled = os.getenv("WEB_AUTH", "false").lower() != "false"
if auth_enabled:
user = os.getenv("WEB_USERNAME", os.getenv("WYZE_EMAIL"))
pw = generate_password_hash(os.getenv("WEB_PASSWORD", os.getenv("WYZE_PASSWORD")))


@auth.verify_password
def verify_password(username, password):
if not auth_enabled:
return True
return check_password_hash(pw, password) if username == user else False
from wyzebridge.web_ui import url_for


def create_app():
Expand All @@ -42,30 +29,49 @@ def create_app():
print("Please ensure your host is up to date.")
exit()

def auth_required(view):
@wraps(view)
def wrapped_view(*args, **kwargs):
if not wb.api.auth:
return redirect(url_for("wyze_login"))
return web_ui.auth.login_required(view)(*args, **kwargs)

return wrapped_view

@app.route("/login", methods=["GET", "POST"])
def wyze_login():
if wb.api.creds.is_set:
return redirect("/")
if wb.api.auth:
return redirect(url_for("index"))
if request.method == "GET":
return render_template(
"login.html",
hass=bool(config.HASS_TOKEN),
api=config.WB_API,
version=config.VERSION,
)
email = request.form.get("email")
password = request.form.get("password")
key_id = request.form.get("keyId")
api_key = request.form.get("apiKey")
if email and password and key_id and api_key:
wb.api.creds.update(email, password, key_id, api_key)

tokens = request.form.get("tokens")
refresh = request.form.get("refresh")

if tokens or refresh:
wb.api.token_auth(tokens=tokens, refresh=refresh)
return {"status": "success"}

credentials = {
"email": request.form.get("email"),
"password": request.form.get("password"),
"key_id": request.form.get("keyId"),
"api_key": request.form.get("apiKey"),
}

if all(credentials.values()):
wb.api.creds.update(**credentials)
return {"status": "success"}
return {"status": "missing email or password"}

return {"status": "missing credentials"}

@app.route("/")
@auth.login_required
@auth_required
def index():
if not wb.api.creds.is_set:
return redirect("/login")
if not (columns := request.args.get("columns")):
columns = request.cookies.get("number_of_columns", "2")
if not (refresh := request.args.get("refresh")):
Expand All @@ -84,14 +90,13 @@ def index():
video_format = request.cookies.get("video", "webrtc")
if req_video := ({"webrtc", "hls", "kvs"} & set(request.args)):
video_format = req_video.pop()
host = urlparse(request.root_url).hostname
resp = make_response(
render_template(
"index.html",
cam_data=web_ui.all_cams(wb.streams, wb.api.total_cams, host),
cam_data=web_ui.all_cams(wb.streams, wb.api.total_cams),
number_of_columns=number_of_columns,
refresh_period=refresh_period,
hass=bool(config.HASS_TOKEN),
api=config.WB_API,
version=config.VERSION,
webrtc=bool(config.BRIDGE_IP),
show_video=show_video,
Expand All @@ -113,14 +118,8 @@ def index():

return resp

@app.route("/mfa/<string:code>")
def set_mfa_code(code):
"""Set mfa code."""
if len(code) != 6:
return {"error": f"Wrong length: {len(code)}"}
return {"success" if web_ui.set_mfa(code) else "error": f"Using: {code}"}

@app.route("/api/sse_status")
@auth_required
def sse_status():
"""Server sent event for camera status."""
if wb.api.mfa_req:
Expand All @@ -134,19 +133,20 @@ def sse_status():
)

@app.route("/api")
@auth_required
def api_all_cams():
host = urlparse(request.root_url).hostname
return web_ui.all_cams(wb.streams, wb.api.total_cams, host)
return web_ui.all_cams(wb.streams, wb.api.total_cams)

@app.route("/api/<string:cam_name>")
@auth_required
def api_cam(cam_name: str):
host = urlparse(request.root_url).hostname
if cam := wb.streams.get_info(cam_name):
return cam | web_ui.format_stream(cam_name, host)
return cam | web_ui.format_stream(cam_name)
return {"error": f"Could not find camera [{cam_name}]"}

@app.route("/api/<cam_name>/<cam_cmd>", methods=["GET", "PUT", "POST"])
@app.route("/api/<cam_name>/<cam_cmd>/<path:payload>")
@auth_required
def api_cam_control(cam_name: str, cam_cmd: str, payload: str | dict = ""):
"""API Endpoint to send tutk commands to the camera."""
if args := request.values:
Expand All @@ -163,26 +163,30 @@ def api_cam_control(cam_name: str, cam_cmd: str, payload: str | dict = ""):
return wb.streams.send_cmd(cam_name, cam_cmd.lower(), payload)

@app.route("/signaling/<string:name>")
@auth_required
def webrtc_signaling(name):
if "kvs" in request.args:
return wb.api.get_kvs_signal(name)
return web_ui.get_webrtc_signal(name, urlparse(request.root_url).hostname)
return web_ui.get_webrtc_signal(name, config.WB_API)

@app.route("/webrtc/<string:name>")
@auth_required
def webrtc(name):
"""View WebRTC direct from camera."""
if (webrtc := wb.api.get_kvs_signal(name)).get("result") == "ok":
return make_response(render_template("webrtc.html", webrtc=webrtc))
return webrtc

@app.route("/snapshot/<string:img_file>")
@auth_required
def rtsp_snapshot(img_file: str):
"""Use ffmpeg to take a snapshot from the rtsp stream."""
if wb.streams.get_rtsp_snap(Path(img_file).stem):
return send_from_directory(config.IMG_PATH, img_file)
return thumbnail(img_file)

@app.route("/img/<string:img_file>")
@auth_required
def img(img_file: str):
"""
Serve an existing local image or take a new snapshot from the rtsp stream.
Expand All @@ -199,12 +203,14 @@ def img(img_file: str):
return rtsp_snapshot(img_file)

@app.route("/thumb/<string:img_file>")
@auth_required
def thumbnail(img_file: str):
if wb.api.save_thumbnail(Path(img_file).stem):
return send_from_directory(config.IMG_PATH, img_file)
return redirect("/static/notavailable.svg", code=307)

@app.route("/photo/<string:img_file>")
@auth_required
def boa_photo(img_file: str):
"""Take a photo on the camera and grab it over the boa http server."""
uri = Path(img_file).stem
Expand All @@ -215,6 +221,7 @@ def boa_photo(img_file: str):
return redirect(f"/img/{img_file}", code=307)

@app.route("/restart/<string:restart_cmd>")
@auth_required
def restart_bridge(restart_cmd: str):
"""
Restart parts of the wyze-bridge.
Expand All @@ -238,12 +245,12 @@ def restart_bridge(restart_cmd: str):
return {"result": "ok", "restart": restart_cmd.split(",")}

@app.route("/cams.m3u8")
@auth_required
def iptv_playlist():
"""
Generate an m3u8 playlist with all enabled cameras.
"""
host = urlparse(request.root_url).hostname
cameras = web_ui.format_streams(wb.streams.get_all_cam_info(), host)
cameras = web_ui.format_streams(wb.streams.get_all_cam_info())
resp = make_response(render_template("m3u8.html", cameras=cameras))
resp.headers.set("content-type", "application/x-mpegURL")
return resp
Expand Down
8 changes: 7 additions & 1 deletion app/static/webrtc.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,15 @@ class Receiver {

console.log('Sending offer');
this.offerData = parseOffer(desc.sdp);
let headers = { 'Content-Type': 'application/sdp' };

const server = this.signalJson.servers && this.signalJson.servers.length > 0 ? this.signalJson.servers[0] : null;
if (server && server.credential && server.username) {
headers['Authorization'] = 'Basic ' + btoa(server.username + ':' + server.credential);
}
fetch(this.signalJson.whep, {
method: 'POST',
headers: { 'Content-Type': 'application/sdp' },
headers: headers,
body: desc.sdp,
})
.then((res) => {
Expand Down
8 changes: 5 additions & 3 deletions app/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Wyze-Bridge</title>
<link rel="stylesheet" href="{{ 'static/bulma.css' if hass else url_for('static',filename='bulma.css') }}" />
<link rel="stylesheet" href="{{ 'static/site.css' if hass else url_for('static',filename='site.css') }}" />
<link rel="stylesheet" href="static/bulma.css" />
<link rel="stylesheet" href="static/site.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css" />
{% block stylesheet %}
{% endblock %}
Expand All @@ -19,6 +19,8 @@
</section>
<footer class="footer fs-display-none">
<div class="content has-text-centered">
{% block api_info %}
{% endblock %}
<p>
<a href="https://github.com/mrlt8/docker-wyze-bridge"><i class="fa-brands fa-github"></i>
<strong>docker-wyze-bridge</strong></a>
Expand All @@ -32,7 +34,7 @@
</p>
</div>
</footer>
<script src="{{ 'static/bulma-toast.js' if hass else url_for('static',filename='bulma-toast.js') }}"></script>
<script src="static/bulma-toast.js"></script>
{% block javascript %}
{% endblock %}
</body>
Expand Down
Loading

0 comments on commit 3806621

Please sign in to comment.