Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v2.9.0 #1184

Merged
merged 43 commits into from
May 12, 2024
Merged

v2.9.0 #1184

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
5d9c0ff
Sign motion event request #1125
mrlt8 May 3, 2024
eb2f645
use device id instead of mac #1125
mrlt8 May 3, 2024
d34cc42
remove wrong flags from mkfifo #1174
mrlt8 May 4, 2024
ddb76e2
less aggressive flush #1159 #1167
mrlt8 May 4, 2024
4d024fe
Use K10052 for setting FPS #1161
mrlt8 May 4, 2024
8e986c2
Refactor bits for going above 255 for quality
mrlt8 May 4, 2024
e8fc74e
Revert ffmpeg changes #1159 #1167
mrlt8 May 4, 2024
8a9e8bc
version is obsolete
mrlt8 May 4, 2024
bb073de
default quality to hd180
mrlt8 May 4, 2024
8aea9e9
Update Wyze iOS App version from v2.44.5.3 to v2.50.6.1 (#1176)
mrlt8 May 4, 2024
3321eab
use struct pack
mrlt8 May 5, 2024
ffbd93e
Merge branch 'dev' of https://github.com/mrlt8/docker-wyze-bridge int…
mrlt8 May 5, 2024
6d2192d
Token based auth over the webUI
mrlt8 May 5, 2024
0846265
Fix redirect for Home Assistant Ingress
mrlt8 May 6, 2024
bb282a2
Use request headers to fix redirect for HA
mrlt8 May 6, 2024
6402b6d
Change GET to POST for webhooks data
mrlt8 May 6, 2024
2824ff1
Deprecate ifttt_webhook in favor of webhooks
mrlt8 May 6, 2024
3b691ac
use yml for HA config and make credentials optional
mrlt8 May 6, 2024
13f3a0a
keep trying to identify audio #1172
mrlt8 May 7, 2024
8a0ba1a
Snapshot on motion and push to mqtt #709 #970
mrlt8 May 7, 2024
b2a067c
Add event time to motion message
mrlt8 May 7, 2024
b26a3e9
refactor auth
mrlt8 May 7, 2024
078b8f9
EVENT_API option #1125
mrlt8 May 8, 2024
9861fc0
Add additional headers #1125
mrlt8 May 8, 2024
858f768
Audio sync with higher bitrate
mrlt8 May 9, 2024
b596f4d
Debug api request #1125
mrlt8 May 9, 2024
0663bb4
Update api.py
mrlt8 May 9, 2024
bc7be86
only debug on error
mrlt8 May 9, 2024
34d45f8
Tweak audio sync
mrlt8 May 9, 2024
0775c41
don't raise error on lost frame
mrlt8 May 9, 2024
fe5d427
clear buffer if out of sync
mrlt8 May 9, 2024
f59e412
Unique macs only #1125
mrlt8 May 9, 2024
b7ab881
Require auth by default and block non-ingress access #1181
mrlt8 May 10, 2024
ff58655
Allow non-ingress access with auth #1181
mrlt8 May 11, 2024
c38889c
Remove retain flag from commands #1182
mrlt8 May 11, 2024
a7e488b
update webrtc to work with auth streams
mrlt8 May 11, 2024
d9190b2
Add WB_API and rename WEB to WB #1181
mrlt8 May 12, 2024
f7c2055
HA move /config/wyze-bridge/ to /config/
mrlt8 May 12, 2024
97a62a6
Don't notify substream event and remove v2 #1125
mrlt8 May 12, 2024
fc94e6f
WebUI Auth related config for HA
mrlt8 May 12, 2024
2fb8d80
Don't retain discovery message? #1182
mrlt8 May 12, 2024
2b0b2dd
Case sensitive credentials for WebUI
mrlt8 May 12, 2024
dc1712e
changelog and readme
mrlt8 May 12, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading