Skip to content

Commit

Permalink
v2.3.2 (#860)
Browse files Browse the repository at this point in the history
* MQTT discovery availability

* Add additional MQTT status topics

* Remove quotations from string payload #857

* return exception

* refactor command parsing

* Use SCALE_USER_AGENT for token refresh

* Refresh token if expired when using run_action

* Use 'state' topic for start, stop, enable, disable, and status

* show run_action error

* get power state from cloud api

* disable mqtt on TimeoutError

* version bump
  • Loading branch information
mrlt8 authored Jun 17, 2023
1 parent 3052882 commit 5741365
Show file tree
Hide file tree
Showing 13 changed files with 305 additions and 159 deletions.
21 changes: 17 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,21 @@ 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.3.2

* Camera commands:
* SET Topic: `state`; payload: `start|stop|enable|disable` - control the camera stream.
* GET Topic: `state` - get the state of the stream in the bridge.
* GET Topic: `power` - get the power switch state (Wyze Cloud API).
* REST/MQTT Control:
* FIXED: Refresh token if needed when sending `power` commands.
* FIXED: Remove quotations from payload. (#857)
* CHANGED: Better error handling for commands.
* MQTT:
* Updated discovery availability and additional entities.
* Publish additional topics with current settings.
* Disable on TimeoutError.

## What's Changed in v2.3.1

* NEW: WebUI - Power on/off/restart controls.
Expand Down Expand Up @@ -124,6 +139,7 @@ The container can be run on its own, in [Portainer](https://github.com/mrlt8/doc
### docker-compose (recommended)

This is similar to the docker run command, but will save all your options in a yaml file.
(If your credentials have special characters, you must escape them)

1. Install [Docker Compose](https://docs.docker.com/compose/install/).
2. Use the [sample](https://raw.githubusercontent.com/mrlt8/docker-wyze-bridge/main/docker-compose.sample.yml) as a guide to create a `docker-compose.yml` file with your wyze credentials.
Expand Down Expand Up @@ -152,6 +168,7 @@ Visit the [wiki page](https://github.com/mrlt8/docker-wyze-bridge/wiki/Home-Assi

## Additional Info

* [Camera Commands (MQTT/REST API)](https://github.com/mrlt8/docker-wyze-bridge/wiki/Camera-Commands)
* [Two-Factor Authentication (2FA/MFA)](https://github.com/mrlt8/docker-wyze-bridge/wiki/Two-Factor-Authentication)
* [ARM/Raspberry Pi](https://github.com/mrlt8/docker-wyze-bridge/wiki/Raspberry-Pi-(armv7-and-arm64))
* [Network Connection Modes](https://github.com/mrlt8/docker-wyze-bridge/wiki/Network-Connection-Modes)
Expand All @@ -163,10 +180,6 @@ Visit the [wiki page](https://github.com/mrlt8/docker-wyze-bridge/wiki/Home-Assi
* [WebUI API](https://github.com/mrlt8/docker-wyze-bridge/wiki/WebUI-API)


### Special Characters

If your email or password contains a `%` or `$` character, you may need to escape them with an extra character. e.g., `pa$$word` should be entered as `pa$$$$word`

## Web-UI

The bridge features a basic Web-UI which can display a preview of all your cameras as well as direct links to all the video streams.
Expand Down
15 changes: 15 additions & 0 deletions app/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
## What's Changed in v2.3.2

* Camera commands:
* SET Topic: `state`; payload: `start|stop|enable|disable` - control the camera stream.
* GET Topic: `state` - get the state of the stream in the bridge.
* GET Topic: `power` - get the power switch state (Wyze Cloud API).
* REST/MQTT Control:
* FIXED: Refresh token if needed when sending `power` commands.
* FIXED: Remove quotations from payload. (#857)
* CHANGED: Better error handling for commands.
* MQTT:
* Updated discovery availability and additional entities.
* Publish additional topics with current settings.
* Disable on TimeoutError.

## What's Changed in v2.3.1

* NEW: WebUI - Power on/off/restart controls.
Expand Down
2 changes: 1 addition & 1 deletion app/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"slug": "docker-wyze-bridge",
"url": "https://github.com/mrlt8/docker-wyze-bridge",
"image": "mrlt8/wyze-bridge",
"version": "2.3.1",
"version": "2.3.2",
"arch": [
"armv7",
"aarch64",
Expand Down
4 changes: 2 additions & 2 deletions app/static/site.js
Original file line number Diff line number Diff line change
Expand Up @@ -538,11 +538,11 @@ document.addEventListener("DOMContentLoaded", () => {
const uri = this.getAttribute("data-cam");
if (icon.matches(".fa-circle-play, .fa-satellite-dish")) {
icon.setAttribute("class", "fas fa-circle-notch fa-spin")
fetch(`api/${uri}/stop`)
fetch(`api/${uri}/state/stop`)
console.debug("pause " + uri)
} else if (icon.matches(".fa-circle-pause, .fa-ghost")) {
icon.setAttribute("class", "fas fa-circle-notch fa-spin")
fetch(`api/${uri}/start`)
fetch(`api/${uri}/state/start`)
console.debug("play " + uri)
}
}
Expand Down
13 changes: 8 additions & 5 deletions app/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -238,23 +238,26 @@
<div class="buttons are-small is-centered">
<button class="button" data-cmd="power" data-payload="on">On</button>
<button class="button" data-cmd="power" data-payload="off">Off</button>
<button class="button" data-cmd="restart" data-payload="restart">Restart</button>
<button class="button" data-cmd="power" data-payload="restart">Restart</button>
</div>
<p class="menu-label dropdown-item"><i class="fas fa-video"></i> Stream</p>
<div class="buttons are-small is-centered">
<button class="button" data-cmd="enable">
<button class="button" data-cmd="state" data-payload="enable">
<span class="icon is-small">
<i class="fas fa-circle-check" aria-hidden="true"></i></span>
<span>Enable</span></button>
<button class="button" data-cmd="disable"> <span class="icon is-small">
<button class="button" data-cmd="state" data-payload="disable"> <span
class="icon is-small">
<i class="fas fa-circle-xmark" aria-hidden="true"></i></span>
<span>Disable</span></button></button>
</div>
<div class="buttons are-small is-centered">
<button class="button" data-cmd="start"> <span class="icon is-small">
<button class="button" data-cmd="state" data-payload="start"> <span
class="icon is-small">
<i class="fas fa-circle-play" aria-hidden="true"></i></span>
<span>Start</span></button>
<button class="button" data-cmd="stop"> <span class="icon is-small">
<button class="button" data-cmd="state" data-payload="stop"> <span
class="icon is-small">
<i class="fas fa-circle-pause" aria-hidden="true"></i></span>
<span>Stop</span></button>
</div>
Expand Down
5 changes: 0 additions & 5 deletions app/wyzebridge/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,6 @@


DEPRECATED = {
"TAKE_PHOTO",
"PULL_PHOTO",
"PULL_ALARM",
"MOTION_HTTP",
"MOTION_COOLDOWN",
"DEBUG_LEVEL",
}

Expand Down
179 changes: 120 additions & 59 deletions app/wyzebridge/mqtt.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,13 @@ def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except ConnectionRefusedError:
logger.warning("[MQTT] connection refused. Disabling MQTT.")
logger.error("[MQTT] connection refused. Disabling MQTT.")
MQTT_ENABLED = False
except TimeoutError:
logger.error("[MQTT] TimeoutError. Disabling MQTT.")
MQTT_ENABLED = False
except Exception as ex:
logger.warning(f"[MQTT] {ex}")
logger.error(f"[MQTT] {ex}")

return wrapper

Expand All @@ -39,7 +42,6 @@ def wyze_discovery(cam: WyzeCamera, cam_uri: str) -> None:
"""Add Wyze camera to MQTT if enabled."""
base = f"wyzebridge/{cam_uri or cam.name_uri}/"
msgs = [(f"{base}state", "stopped")]

if MQTT_DISCOVERY:
base_payload = {
"device": {
Expand All @@ -53,61 +55,19 @@ def wyze_discovery(cam: WyzeCamera, cam_uri: str) -> None:
},
}

entities = {
"preview": {
"availability_topic": f"{base}state",
"payload_not_available": "stopped",
"type": "camera",
"payload": {
"topic": f"{base}image",
"icon": "mdi:cctv",
},
},
"ir": {
"type": "switch",
"payload": {
"state_topic": f"{base}irled",
"command_topic": f"{base}irled/set",
"payload_on": 1,
"payload_off": 2,
"icon": "mdi:lightbulb-night",
},
},
"night_vision": {
"type": "switch",
"payload": {
"state_topic": f"{base}night_vision",
"command_topic": f"{base}night_vision/set",
"payload_on": 3,
"payload_off": 2,
"icon": "mdi:weather-night",
},
},
"signal": {
"type": "sensor",
"payload": {
"state_topic": f"{base}wifi",
"icon": "mdi:wifi",
"entity_category": "diagnostic",
},
},
"audio": {
"type": "sensor",
"payload": {
"state_topic": f"{base}audio",
"icon": "mdi:volume-high",
"entity_category": "diagnostic",
},
},
}
for entity, data in entities.items():
for entity, data in get_entities(base).items():
topic = f"{MQTT_DISCOVERY}/{data['type']}/{cam.mac}/{entity}/config"
payload = base_payload | data["payload"]
payload[
"name"
] = f"Wyze Cam {cam.nickname} {' '.join(entity.upper().split('_'))}"
payload["uniq_id"] = f"WYZE{cam.mac}{entity.upper()}"
if "availability_topic" not in data["payload"]:
data["payload"]["availability_topic"] = "wyzebridge/state"

payload = dict(
base_payload | data["payload"],
name=f"Wyze Cam {cam.nickname} {' '.join(entity.upper().split('_'))}",
uniq_id=f"WYZE{cam.mac}{entity.upper()}",
)

msgs.append((topic, json.dumps(payload)))

send_mqtt(msgs)


Expand All @@ -118,9 +78,11 @@ def mqtt_sub_topic(m_topics: list, callback) -> Optional[paho.mqtt.client.Client

client.username_pw_set(MQTT_USER, MQTT_PASS or None)
client.user_data_set(callback)
client.on_connect = lambda mq_client, *_: [
mq_client.subscribe(f"wyzebridge/{m_topic}") for m_topic in m_topics
]
client.on_connect = lambda mq_client, *_: (
mq_client.publish("wyzebridge/state", "online"),
[mq_client.subscribe(f"wyzebridge/{m_topic}") for m_topic in m_topics],
)
client.will_set("wyzebridge/state", payload="offline", qos=1, retain=True)
client.connect(MQTT_HOST, int(MQTT_PORT or 1883), 30)
client.loop_start()
return client
Expand Down Expand Up @@ -200,3 +162,102 @@ def _on_message(client, callback, msg):
resp = callback(cam, topic, payload if include_payload else "")
if resp.get("status") != "success":
logger.info(f"[MQTT] {resp}")


def get_entities(base_topic: str) -> dict:
return {
"snapshot": {
"type": "camera",
"payload": {
"availability_topic": f"{base_topic}state",
"payload_not_available": "stopped",
"topic": f"{base_topic}image",
"icon": "mdi:cctv",
},
},
"power": {
"type": "switch",
"payload": {
"command_topic": f"{base_topic}power/set",
"icon": "mdi:power-plug",
},
},
"ir": {
"type": "switch",
"payload": {
"state_topic": f"{base_topic}irled",
"command_topic": f"{base_topic}irled/set",
"payload_on": 1,
"payload_off": 2,
"icon": "mdi:lightbulb-night",
},
},
"night_vision": {
"type": "switch",
"payload": {
"state_topic": f"{base_topic}night_vision",
"command_topic": f"{base_topic}night_vision/set",
"payload_on": 3,
"payload_off": 2,
"icon": "mdi:weather-night",
},
},
"status_light": {
"type": "switch",
"payload": {
"state_topic": f"{base_topic}status_light",
"command_topic": f"{base_topic}status_light/set",
"payload_on": 1,
"payload_off": 2,
"icon": "mdi:led-on",
"entity_category": "diagnostic",
},
},
"bitrate": {
"type": "number",
"payload": {
"state_topic": f"{base_topic}bitrate",
"command_topic": f"{base_topic}bitrate/set",
"device_class": "data_rate",
"min": 1,
"max": 250,
"icon": "mdi:high-definition-box",
"entity_category": "diagnostic",
},
},
"fps": {
"type": "number",
"payload": {
"state_topic": f"{base_topic}fps",
"command_topic": f"{base_topic}fps/set",
"min": 1,
"max": 30,
"icon": "mdi:filmstrip",
"entity_category": "diagnostic",
},
},
"res": {
"type": "sensor",
"payload": {
"state_topic": f"{base_topic}res",
"icon": "mdi:image-size-select-large",
"entity_category": "diagnostic",
},
},
"signal": {
"type": "sensor",
"payload": {
"state_topic": f"{base_topic}wifi",
"icon": "mdi:wifi",
"entity_category": "diagnostic",
},
},
"audio": {
"type": "sensor",
"payload": {
"state_topic": f"{base_topic}audio",
"icon": "mdi:volume-high",
"entity_category": "diagnostic",
},
},
}
26 changes: 20 additions & 6 deletions app/wyzebridge/wyze_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,9 @@ def wrapper(self, *args: Any, **kwargs: Any):
return
try:
return func(self, *args, **kwargs)
except AssertionError:
except wyzecam.api.AccessTokenError:
logger.warning("⚠️ Expired token?")
if func.__name__ != "refresh_token":
self.refresh_token()
self.refresh_token()
return func(self, *args, **kwargs)
except ConnectionError as ex:
logger.warning(f"{ex}")
Expand Down Expand Up @@ -248,9 +247,24 @@ def run_action(self, cam: WyzeCamera, action: str):
logger.info(f"[CONTROL] ☁️ Sending {action} to {cam.name_uri} via Wyze API")
resp = wyzecam.api.run_action(self.auth, cam, action.lower())
return {"status": "success", "response": resp["result"]}
except AssertionError as ex:
logger.error(ex)
return {"status": "error", "response": str(ex)}
except ValueError as ex:
error = f'{ex.args[0].get("code")}: {ex.args[0].get("msg")}'
logger.error(f"ERROR - {error}")
return {"status": "error", "response": f"{error}"}

@authenticated
def get_pid_info(self, cam: WyzeCamera, pid: str):
try:
logger.info(f"[CONTROL] ☁️ Getting info for {cam.name_uri} via Wyze API")
property_list = wyzecam.api.get_device_info(self.auth, cam)["property_list"]
except ValueError as ex:
error = f'{ex.args[0].get("code")}: {ex.args[0].get("msg")}'
logger.error(f"ERROR - {error}")
return {"status": "error", "response": f"{error}"}

resp = next((item for item in property_list if item["pid"] == pid))

return {"status": "success", "value": resp.get("value"), "response": resp}

def clear_cache(self):
logger.info("♻️ Clearing local cache...")
Expand Down
Loading

0 comments on commit 5741365

Please sign in to comment.