diff --git a/README.md b/README.md index be378f0b..9b2e0775 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,12 @@ 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.10 + +* FIX: KeyError when upgrading with old cache data in v2.3.9 (#905) Thanks @itsamenathan! + * You should be able to remove or set `FRESH_DATA` back to false. +* MQTT: Update bridge status (#907) Thanks @giorgi1324! + ## What's Changed in v2.3.9 * NEW: ENV Options - token-based authentication (#876) diff --git a/app/CHANGELOG.md b/app/CHANGELOG.md index 80232ded..878a5a8e 100644 --- a/app/CHANGELOG.md +++ b/app/CHANGELOG.md @@ -1,3 +1,9 @@ +## What's Changed in v2.3.10 + +* FIX: KeyError when upgrading with old cache data in v2.3.9 (#905) Thanks @itsamenathan! + * You should be able to remove or set `FRESH_DATA` back to false. +* MQTT: Update bridge status (#907) Thanks @giorgi1324! + ## What's Changed in v2.3.9 * NEW: ENV Options - token-based authentication (#876) diff --git a/app/wyzebridge/config.py b/app/wyzebridge/config.py index babb460e..7edf5c69 100644 --- a/app/wyzebridge/config.py +++ b/app/wyzebridge/config.py @@ -12,7 +12,7 @@ HASS_TOKEN: str = getenv("SUPERVISOR_TOKEN", "") setup_hass(HASS_TOKEN) MQTT_DISCOVERY = env_bool("MQTT_DTOPIC") - +MQTT_TOPIC = env_bool("MQTT_TOPIC", "wyzebridge").strip("/") ON_DEMAND = bool(env_bool("on_demand") if getenv("ON_DEMAND") else True) CONNECT_TIMEOUT: int = env_bool("CONNECT_TIMEOUT", 20, style="int") diff --git a/app/wyzebridge/mqtt.py b/app/wyzebridge/mqtt.py index 9edd556b..dd99ee03 100644 --- a/app/wyzebridge/mqtt.py +++ b/app/wyzebridge/mqtt.py @@ -7,7 +7,7 @@ import paho.mqtt.client import paho.mqtt.publish from wyzebridge.bridge_utils import env_bool -from wyzebridge.config import IMG_PATH, MQTT_DISCOVERY, VERSION +from wyzebridge.config import IMG_PATH, MQTT_DISCOVERY, MQTT_TOPIC, VERSION from wyzebridge.logging import logger from wyzebridge.wyze_commands import GET_CMDS, GET_PAYLOAD, PARAMS, SET_CMDS from wyzecam import WyzeCamera @@ -40,7 +40,7 @@ def wrapper(*args, **kwargs): @mqtt_enabled 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}/" + base = f"{MQTT_TOPIC}/{cam_uri or cam.name_uri}/" msgs = [(f"{base}state", "stopped")] if MQTT_DISCOVERY: base_payload = { @@ -58,7 +58,7 @@ def wyze_discovery(cam: WyzeCamera, cam_uri: str) -> None: for entity, data in get_entities(base).items(): topic = f"{MQTT_DISCOVERY}/{data['type']}/{cam.mac}/{entity}/config" if "availability_topic" not in data["payload"]: - data["payload"]["availability_topic"] = "wyzebridge/state" + data["payload"]["availability_topic"] = f"{MQTT_TOPIC}/state" payload = dict( base_payload | data["payload"], @@ -79,15 +79,32 @@ 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.publish("wyzebridge/state", "online"), - [mq_client.subscribe(f"wyzebridge/{m_topic}") for m_topic in m_topics], + mq_client.publish(f"{MQTT_TOPIC}/state", "online"), + [mq_client.subscribe(f"{MQTT_TOPIC}/{m_topic}") for m_topic in m_topics], ) - client.will_set("wyzebridge/state", payload="offline", qos=1, retain=True) + client.will_set(f"{MQTT_TOPIC}/state", payload="offline", qos=1, retain=True) client.connect(MQTT_HOST, int(MQTT_PORT or 1883), 30) + if MQTT_DISCOVERY: + client.subscribe(f"{MQTT_DISCOVERY}/status") + client.message_callback_add( + f"{MQTT_DISCOVERY}/status", + lambda mq_client, _, msg: bridge_status(mq_client, []) + if msg.payload.decode().lower() == "online" + else None, + ) client.loop_start() return client +def bridge_status(client: Optional[paho.mqtt.client.Client], cams: list): + """Set bridge online if MQTT is enabled.""" + if not client: + return + client.publish(f"{MQTT_TOPIC}/state", "online") + for cam in cams: + client.publish(f"{MQTT_TOPIC}/{cam}/state", "online") + + @mqtt_enabled def send_mqtt(messages: list) -> None: """Publish a message to the MQTT server.""" @@ -105,9 +122,8 @@ def send_mqtt(messages: list) -> None: @mqtt_enabled def publish_message(topic: str, message=None): - base = "wyzebridge/" paho.mqtt.publish.single( - topic=base + topic, + topic=f"{MQTT_TOPIC}/{topic}", payload=message, hostname=MQTT_HOST, port=int(MQTT_PORT or 1883), @@ -133,7 +149,7 @@ def update_preview(cam_name: str): @mqtt_enabled -def mqtt_cam_control(cam_names: dict, callback): +def cam_control(cam_names: dict, callback): topics = [] for uri in cam_names: topics += [f"{uri.lower()}/{t}/set" for t in SET_CMDS] @@ -141,6 +157,7 @@ def mqtt_cam_control(cam_names: dict, callback): if client := mqtt_sub_topic(topics, callback): client.on_message = _on_message + return client def _on_message(client, callback, msg): diff --git a/app/wyzebridge/stream.py b/app/wyzebridge/stream.py index 93d93bcf..a163771b 100644 --- a/app/wyzebridge/stream.py +++ b/app/wyzebridge/stream.py @@ -7,7 +7,7 @@ from wyzebridge.config import MQTT_DISCOVERY, SNAPSHOT_INT, SNAPSHOT_TYPE from wyzebridge.ffmpeg import rtsp_snap_cmd from wyzebridge.logging import logger -from wyzebridge.mqtt import mqtt_cam_control, publish_message, update_preview +from wyzebridge.mqtt import bridge_status, cam_control, publish_message, update_preview from wyzebridge.rtsp_event import RtspEvent @@ -96,15 +96,16 @@ def monitor_streams(self, mtx_health: Callable) -> None: self.stop_flag = False if self.thread: self.thread.start() - mqtt = mqtt_cam_control(self.streams, self.send_cmd) + mqtt = cam_control(self.streams, self.send_cmd) logger.info(f"🎬 {self.total} stream{'s'[:self.total^1]} enabled") event = RtspEvent(self.streams) while not self.stop_flag: - mtx_health() event.read(timeout=1) cams = self.health_check_all() - if cams and SNAPSHOT_TYPE == "rtsp": - self.snap_all(cams) + self.snap_all(cams) + if int(time.time()) % 15 == 0: + mtx_health() + bridge_status(mqtt, cams) if mqtt: mqtt.loop_stop() logger.info("Stream monitoring stopped") @@ -136,6 +137,8 @@ def snap_all(self, cams: list[str]): Parameters: - cams (list[str]): names of the streams to take a snapshot of. """ + if SNAPSHOT_TYPE != "rtsp" or not cams: + return if time.time() - self.last_snap < SNAPSHOT_INT: return self.last_snap = time.time() diff --git a/app/wyzebridge/wyze_control.py b/app/wyzebridge/wyze_control.py index 5a7573d4..6e5f8f6c 100644 --- a/app/wyzebridge/wyze_control.py +++ b/app/wyzebridge/wyze_control.py @@ -8,11 +8,11 @@ import requests from wyzebridge.bridge_utils import env_bool -from wyzebridge.config import BOA_COOLDOWN, BOA_INTERVAL, IMG_PATH +from wyzebridge.config import BOA_COOLDOWN, BOA_INTERVAL, IMG_PATH, MQTT_TOPIC from wyzebridge.logging import logger from wyzebridge.mqtt import MQTT_ENABLED, send_mqtt from wyzebridge.wyze_commands import CMD_VALUES, GET_CMDS, GET_PAYLOAD, PARAMS, SET_CMDS -from wyzecam import TutkError, WyzeIOTCSession, WyzeIOTCSessionState, tutk_protocol +from wyzecam import WyzeIOTCSession, WyzeIOTCSessionState, tutk_protocol def cam_http_alive(ip: str) -> bool: @@ -178,7 +178,7 @@ def camera_control( def update_mqtt_values(topic: str, cam_name: str, resp: dict): - base = f"wyzebridge/{cam_name}" + base = f"{MQTT_TOPIC}/{cam_name}" if msgs := [(f"{base}/{k}", resp[v]) for k, v in PARAMS.items() if v in resp]: send_mqtt(msgs) @@ -275,7 +275,7 @@ def motion_alarm(cam: dict): logger.info(f"[MOTION] Alarm file detected at {cam['last_photo'][1]}") cam["cooldown"] = datetime.now() + timedelta(seconds=BOA_COOLDOWN) cam["last_alarm"] = cam["last_photo"] - send_mqtt([(f"wyzebridge/{cam['uri']}/motion", motion)]) + send_mqtt([(f"{MQTT_TOPIC}/{cam['uri']}/motion", motion)]) if motion and (http := env_bool("boa_motion")): try: resp = requests.get(http.format(cam_name=cam["uri"])) diff --git a/app/wyzebridge/wyze_stream.py b/app/wyzebridge/wyze_stream.py index 2d8ad266..95de48e0 100644 --- a/app/wyzebridge/wyze_stream.py +++ b/app/wyzebridge/wyze_stream.py @@ -12,7 +12,7 @@ from typing import Optional from wyzebridge.bridge_utils import env_bool, env_cam -from wyzebridge.config import BRIDGE_IP, COOLDOWN +from wyzebridge.config import BRIDGE_IP, COOLDOWN, MQTT_TOPIC from wyzebridge.ffmpeg import get_ffmpeg_cmd from wyzebridge.logging import logger from wyzebridge.mqtt import send_mqtt, update_mqtt_state, wyze_discovery @@ -444,9 +444,9 @@ def get_cam_params( logger.info(f"🔊 Audio Enabled - {codec_str.upper()}/{rate:,}Hz") mqtt = [ - (f"wyzebridge/{uri.lower()}/net_mode", net_mode), - (f"wyzebridge/{uri.lower()}/wifi", wifi), - (f"wyzebridge/{uri.lower()}/audio", json.dumps(audio) if audio else False), + (f"{MQTT_TOPIC}/{uri.lower()}/net_mode", net_mode), + (f"{MQTT_TOPIC}/{uri.lower()}/wifi", wifi), + (f"{MQTT_TOPIC}/{uri.lower()}/audio", json.dumps(audio) if audio else False), ] send_mqtt(mqtt) return v_codec, fps, audio