diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..83a4713 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +test.env +*~ +\#*# +.* +!.gitignore diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ceba7dc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM alpine:latest + +RUN apk add python3 py3-pip curl mosquitto-clients + +COPY requirements.txt /requirements.txt +RUN pip install -r /requirements.txt +COPY grab.py /grab.py + +WORKDIR / + +CMD ["python3", "/grab.py"] + +COPY health.sh /health.sh +HEALTHCHECK CMD /health.sh diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..bdec22d --- /dev/null +++ b/build.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +docker build -t jamesandariese/vncmqtt:latest . +docker push jamesandariese/vncmqtt:latest diff --git a/grab.py b/grab.py new file mode 100644 index 0000000..bb54768 --- /dev/null +++ b/grab.py @@ -0,0 +1,110 @@ +import base64 +import hashlib +import logging +import json +import os +import sys +import time + +from vncdotool import api +from twisted.internet import reactor +import paho.mqtt.client as paho + +# LOGGING + +# setup logging to console +class SkipLevelsFilter(logging.Filter): + def __init__(self, skip_levels=[]): + self.skip_levels = skip_levels + + def filter(self, rec): + return rec.levelno not in self.skip_levels + + +logger = logging.getLogger('keywords_pipeline') +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + +# stderr for >=ERROR +stderr_handler = logging.StreamHandler(sys.stderr) +stderr_handler.setLevel(logging.ERROR) +stderr_handler.setFormatter(formatter) + +# stdout for everything but ERROR. +stdout_handler = logging.StreamHandler(sys.stdout) +stdout_handler.addFilter(SkipLevelsFilter([logging.ERROR])) +stdout_handler.setLevel(logging.DEBUG) +stdout_handler.setFormatter(formatter) + +# attach the handlers and set the default level to DEBUG for the remainder of the logger setup +logger.addHandler(stdout_handler) +logger.addHandler(stderr_handler) +logger.setLevel(logging.DEBUG) + +# now extract the desired loglevel or use INFO if not specified, DEBUG is still the _active_ level though +# so if anything fails here, it will be logged even if it's a debug message +LOGLEVEL = os.getenv('LOGLEVEL') or "INFO" +numeric_level = getattr(logging, LOGLEVEL.upper(), None) +if not isinstance(numeric_level, int): + logger.critical('Invalid log level: %s' % LOGLEVEL) + os.exit(78) +# here's the debug message which will be logged before changing the level. +logger.debug(f'setting loglevel to {LOGLEVEL}') +logger.setLevel(numeric_level) + +# END LOGGING + +MQTT_USER=os.environ['MQTT_USER'] +MQTT_PASSWORD=os.environ['MQTT_PASSWORD'] +MQTT_HOST=os.environ['MQTT_HOST'] +VNC_HOST=os.environ['VNC_HOST'] +VNC_PASSWORD=os.environ['VNC_PASSWORD'] +MQTT_TOPIC=os.environ['MQTT_TOPIC'] +DEVICE_NAME=os.environ.get('DEVICE_NAME', None) +DEVICE_ID=os.environ.get('DEVICE_ID', None) +INTERVAL=float(os.environ.get('INTERVAL', '5')) + +SANITIZED_HOST=VNC_HOST.replace('.','_') +NAME=DEVICE_NAME or SANITIZED_HOST +DEVICE_ID = DEVICE_ID or SANITIZED_HOST +OBJECT_ID = base64.b32encode(hashlib.sha1(NAME.encode('utf-8')).digest()).decode('utf-8') + +mqtt = paho.Client() +mqtt.username_pw_set(MQTT_USER, MQTT_PASSWORD) +mqtt.connect(MQTT_HOST) +mqtt.loop_start() + +mqtt.publish(f"homeassistant/camera/{DEVICE_ID}/config", json.dumps({ + "name": NAME, + "topic": MQTT_TOPIC + "/image", + "unique_id": DEVICE_ID+"-T", + "device_class": "camera", + "device": { + "identifiers": DEVICE_ID, + "manufacturer": "VNC-MQTT", + "model": "v0.1", + }, +}), retain=True) + +while True: + t = time.time() + with api.connect(VNC_HOST, VNC_PASSWORD) as vnc: + logger.debug("refreshing for capture") + vnc.refreshScreen() + logger.debug("capturing") + vnc.captureScreen('capture.png') + logger.debug("publishing to mqtt") + with open('capture.png', 'rb') as f: + capture = f.read() + with open('capture.time', 'wb') as f: + pass # touch :D + + p = mqtt.publish(topic=MQTT_TOPIC + "/image", payload=capture) + time_left = INTERVAL - (time.time() - t) + if time_left > 0: + logger.debug(f"sleeping for {time_left}s") + time.sleep(time_left) + else: + logger.warning(f"interval missed ({time_left}s overdue)") + +mqtt.loop_end() +reactor.stop() diff --git a/health.sh b/health.sh new file mode 100755 index 0000000..c2aadfb --- /dev/null +++ b/health.sh @@ -0,0 +1,12 @@ +#!/bin/sh + +DT=$(date +%s) +CT=$(stat -c %Z capture.time) + +if [ $(( DT - CT )) -gt $INTERVAL ];then + echo bad + false +else + echo good + true +fi diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..17d7728 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,14 @@ +attrs==23.1.0 +Automat==22.10.0 +constantly==15.1.0 +hyperlink==21.0.0 +idna==3.4 +incremental==22.10.0 +paho-mqtt==1.6.1 +Pillow==10.0.0 +pycryptodomex==3.18.0 +six==1.16.0 +Twisted==22.10.0 +typing_extensions==4.7.1 +vncdotool==1.2.0 +zope.interface==6.0 diff --git a/test.env.sample b/test.env.sample new file mode 100644 index 0000000..1ebf6d3 --- /dev/null +++ b/test.env.sample @@ -0,0 +1,9 @@ +DEVICE_NAME="Testly Desktop" +LOGLEVEL=debug +MQTT_TOPIC=cameras/testly-desktop +MQTT_HOST=192.168.1.3 +MQTT_USER=write +MQTT_PASSWORD=mqttpassword +VNC_HOST=192.168.1.185 +VNC_PASSWORD=vncpassword +INTERVAL=5 diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..2267dda --- /dev/null +++ b/test.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +bash build.sh +docker run --rm -ti --name vncmqtt-test --env-file test.env jamesandariese/vncmqtt:latest