#!/usr/bin/env python3 from subprocess import check_output from re import findall import psutil import sys import os import threading, time, signal from datetime import timedelta import datetime as dt import paho.mqtt.client as mqtt import pytz import yaml from pytz import timezone import argparse try: import apt apt_disabled = False except ImportError: apt_disabled = True UTC = pytz.utc DEFAULT_TIME_ZONE = None mqttClient = None SYSFILE = "/sys/devices/platform/soc/soc:firmware/get_throttled" WAIT_TIME_SECONDS = 60 deviceName = None class ProgramKilled(Exception): pass def signal_handler(signum, frame): raise ProgramKilled class Job(threading.Thread): def __init__(self, interval, execute, *args, **kwargs): threading.Thread.__init__(self) self.daemon = False self.stopped = threading.Event() self.interval = interval self.execute = execute self.args = args self.kwargs = kwargs def stop(self): self.stopped.set() self.join() def run(self): while not self.stopped.wait(self.interval.total_seconds()): self.execute(*self.args, **self.kwargs) def utc_from_timestamp(timestamp: float) -> dt.datetime: """Return a UTC time from a timestamp.""" return UTC.localize(dt.datetime.utcfromtimestamp(timestamp)) def as_local(dattim: dt.datetime) -> dt.datetime: """Convert a UTC datetime object to local time zone.""" if dattim.tzinfo == DEFAULT_TIME_ZONE: return dattim if dattim.tzinfo is None: dattim = UTC.localize(dattim) return dattim.astimezone(DEFAULT_TIME_ZONE) def get_last_boot(): return str(as_local(utc_from_timestamp(psutil.boot_time())).isoformat()) def on_message(client, userdata, message): print (f"Message received: {message.payload.decode()}" ) if(message.payload.decode() == "online"): send_config_message(client) def updateSensors(): payload_str = ( '{"temperature": ' + get_temp() + ', "disk_use": ' + get_disk_usage("/") + ', "memory_use": ' + get_memory_usage() + ', "cpu_usage": ' + get_cpu_usage() + ', "swap_usage": ' + get_swap_usage() + ', "power_status": "' + get_rpi_power_status() + '", "last_boot": "' + get_last_boot() + '", "last_message": "' + time.ctime() + '"' ) if "check_available_updates" in settings and settings["check_available_updates"] and not apt_disabled: payload_str = payload_str + ', "updates": ' + get_updates() if "check_wifi_strength" in settings and settings["check_wifi_strength"]: payload_str = payload_str + ', "wifi_strength": ' + get_wifi_strength() if "external_drives" in settings: for drive in settings["external_drives"]: payload_str = ( payload_str + ', "disk_use_' + drive.lower() + '": ' + get_disk_usage(settings["external_drives"][drive]) ) payload_str = payload_str + "}" mqttClient.publish( topic="system-sensors/sensor/" + deviceName + "/state", payload=payload_str, qos=1, retain=False, ) def get_updates(): cache = apt.Cache() cache.open(None) cache.upgrade() return str(cache.get_changes().__len__()) def get_temp(): temp = check_output(["vcgencmd", "measure_temp"]).decode("UTF-8") return str(findall("\d+\.\d+", temp)[0]) def get_disk_usage(path): return str(psutil.disk_usage(path).percent) def get_memory_usage(): return str(psutil.virtual_memory().percent) def get_cpu_usage(): return str(psutil.cpu_percent(interval=None)) def get_swap_usage(): return str(psutil.swap_memory().percent) def get_wifi_strength(): # check_output(["/proc/net/wireless", "grep wlan0"]) return ( check_output( [ "bash", "-c", "cat /proc/net/wireless | grep wlan0: | awk '{print int($4)}'", ] ) .decode("utf-8") .rstrip() ) def get_rpi_power_status(): _throttled = open(SYSFILE, "r").read()[:-1] _throttled = _throttled[:4] if "power_integer_state" in settings and settings["power_integer_state"]: return _throttled else: if _throttled == "0": return "Everything is working as intended" elif _throttled == "1000": return "Under-voltage was detected, consider getting a uninterruptible power supply for your Raspberry Pi." elif _throttled == "2000": return "Your Raspberry Pi is limited due to a bad powersupply, replace the power supply cable or power supply itself." elif _throttled == "3000": return "Your Raspberry Pi is limited due to a bad powersupply, replace the power supply cable or power supply itself." elif _throttled == "4000": return "The Raspberry Pi is throttled due to a bad power supply this can lead to corruption and instability, please replace your changer and cables." elif _throttled == "5000": return "The Raspberry Pi is throttled due to a bad power supply this can lead to corruption and instability, please replace your changer and cables." elif _throttled == "8000": return "Your Raspberry Pi is overheating, consider getting a fan or heat sinks." else: return "There is a problem with your power supply or system." def check_settings(settings): if "mqtt" not in settings: print("Mqtt not defined in settings.yaml! Please check the documentation") sys.stdout.flush() sys.exit() if "hostname" not in settings["mqtt"]: print("Hostname not defined in settings.yaml! Please check the documentation") sys.stdout.flush() sys.exit() if "timezone" not in settings: print("Timezone not defined in settings.yaml! Please check the documentation") sys.stdout.flush() sys.exit() if "deviceName" not in settings: print("deviceName not defined in settings.yaml! Please check the documentation") sys.stdout.flush() sys.exit() if "client_id" not in settings: print("client_id not defined in settings.yaml! Please check the documentation") sys.stdout.flush() sys.exit() def send_config_message(mqttClient): print("send config message") mqttClient.publish( topic="homeassistant/sensor/" + deviceName + "/" + deviceName + "Temp/config", payload='{"device_class":"temperature","name":"' + deviceName + 'Temperature","state_topic":"system-sensors/sensor/' + deviceName + '/state","unit_of_measurement":"°C","value_template":"{{ value_json.temperature}}","unique_id":"' + deviceName.lower() + '_sensor_temperature","device":{"identifiers":["' + deviceName.lower() + '_sensor"],"name":"' + deviceName + 'Sensors","model":"RPI ' + deviceName + '","manufacturer":"RPI"}, "icon":"mdi:thermometer"}', qos=1, retain=True, ) mqttClient.publish( topic="homeassistant/sensor/" + deviceName + "/" + deviceName + "DiskUse/config", payload='{"name":"' + deviceName + 'DiskUse","state_topic":"system-sensors/sensor/' + deviceName + '/state","unit_of_measurement":"%","value_template":"{{ value_json.disk_use}}","unique_id":"' + deviceName.lower() + '_sensor_disk_use","device":{"identifiers":["' + deviceName.lower() + '_sensor"],"name":"' + deviceName + 'Sensors","model":"RPI ' + deviceName + '","manufacturer":"RPI"}, "icon":"mdi:microsd"}', qos=1, retain=True, ) mqttClient.publish( topic="homeassistant/sensor/" + deviceName + "/" + deviceName + "MemoryUse/config", payload='{"name":"' + deviceName + 'MemoryUse","state_topic":"system-sensors/sensor/' + deviceName + '/state","unit_of_measurement":"%","value_template":"{{ value_json.memory_use}}","unique_id":"' + deviceName.lower() + '_sensor_memory_use","device":{"identifiers":["' + deviceName.lower() + '_sensor"],"name":"' + deviceName + 'Sensors","model":"RPI ' + deviceName + '","manufacturer":"RPI"}, "icon":"mdi:memory"}', qos=1, retain=True, ) mqttClient.publish( topic="homeassistant/sensor/" + deviceName + "/" + deviceName + "CpuUsage/config", payload='{"name":"' + deviceName + 'CpuUsage","state_topic":"system-sensors/sensor/' + deviceName + '/state","unit_of_measurement":"%","value_template":"{{ value_json.cpu_usage}}","unique_id":"' + deviceName.lower() + '_sensor_cpu_usage","device":{"identifiers":["' + deviceName.lower() + '_sensor"],"name":"' + deviceName + 'Sensors","model":"RPI ' + deviceName + '","manufacturer":"RPI"}, "icon":"mdi:memory"}', qos=1, retain=True, ) mqttClient.publish( topic="homeassistant/sensor/" + deviceName + "/" + deviceName + "SwapUsage/config", payload='{"name":"' + deviceName + 'SwapUsage","state_topic":"system-sensors/sensor/' + deviceName + '/state","unit_of_measurement":"%","value_template":"{{ value_json.swap_usage}}","unique_id":"' + deviceName.lower() + '_sensor_swap_usage","device":{"identifiers":["' + deviceName.lower() + '_sensor"],"name":"' + deviceName + 'Sensors","model":"RPI ' + deviceName + '","manufacturer":"RPI"}, "icon":"mdi:harddisk"}', qos=1, retain=True, ) mqttClient.publish( topic="homeassistant/sensor/" + deviceName + "/" + deviceName + "PowerStatus/config", payload='{"name":"' + deviceName + 'PowerStatus","state_topic":"system-sensors/sensor/' + deviceName + '/state","value_template":"{{ value_json.power_status}}","unique_id":"' + deviceName.lower() + '_sensor_power_status","device":{"identifiers":["' + deviceName.lower() + '_sensor"],"name":"' + deviceName + 'Sensors","model":"RPI ' + deviceName + '","manufacturer":"RPI"}, "icon":"mdi:power-plug"}', qos=1, retain=True, ) mqttClient.publish( topic="homeassistant/sensor/" + deviceName + "/" + deviceName + "LastBoot/config", payload='{"device_class":"timestamp","name":"' + deviceName + 'LastBoot","state_topic":"system-sensors/sensor/' + deviceName + '/state","value_template":"{{ value_json.last_boot}}","unique_id":"' + deviceName.lower() + '_sensor_last_boot","device":{"identifiers":["' + deviceName.lower() + '_sensor"],"name":"' + deviceName + 'Sensors","model":"RPI ' + deviceName + '","manufacturer":"RPI"}, "icon":"mdi:clock"}', qos=1, retain=True, ) mqttClient.publish( topic="homeassistant/sensor/" + deviceName + "/" + deviceName + "LastMessage/config", payload='{"name":"' + deviceName + 'LastMessage","state_topic":"system-sensors/sensor/' + deviceName + '/state","value_template":"{{ value_json.updates}}","unique_id":"' + deviceName.lower() + '_sensor_last_message","device":{"identifiers":["' + deviceName.lower() + '_sensor"],"name":"' + deviceName + 'Sensors","model":"RPI ' + deviceName + '","manufacturer":"RPI"}, "icon":"mdi:clock-check"}', qos=1, retain=True, ) if "check_available_updates" in settings and settings["check_available_updates"]: # import apt if(apt_disabled): print("import of apt failed!") else: mqttClient.publish( topic="homeassistant/sensor/" + deviceName + "/" + deviceName + "Updates/config", payload='{"name":"' + deviceName + 'Updates","state_topic":"system-sensors/sensor/' + deviceName + '/state","value_template":"{{ value_json.updates}}","unique_id":"' + deviceName.lower() + '_sensor_updates","device":{"identifiers":["' + deviceName.lower() + '_sensor"],"name":"' + deviceName + 'Sensors","model":"RPI ' + deviceName + '","manufacturer":"RPI"}, "icon":"mdi:cellphone-arrow-down"}', qos=1, retain=True, ) if "check_wifi_strength" in settings and settings["check_wifi_strength"]: mqttClient.publish( topic="homeassistant/sensor/" + deviceName + "/" + deviceName + "WifiStrength/config", payload='{"device_class":"signal_strength","name":"' + deviceName + 'WifiStrength","state_topic":"system-sensors/sensor/' + deviceName + '/state","unit_of_measurement":"dBm","value_template":"{{ value_json.wifi_strength}}","unique_id":"' + deviceName.lower() + '_sensor_wifi_strength","device":{"identifiers":["' + deviceName.lower() + '_sensor"],"name":"' + deviceName + 'Sensors","model":"RPI ' + deviceName + '","manufacturer":"RPI"}}', qos=1, retain=True, ) if "external_drives" in settings: for drive in settings["external_drives"]: mqttClient.publish( topic="homeassistant/sensor/" + deviceName + "/" + deviceName + "DiskUse" + drive + "/config", payload='{"name":"' + deviceName + "DiskUse" + drive + '","state_topic":"system-sensors/sensor/' + deviceName + '/state","unit_of_measurement":"%","value_template":"{{ value_json.disk_use_' + drive.lower() + '}}","unique_id":"' + deviceName.lower() + "_sensor_disk_use_" + drive.lower() + '","device":{"identifiers":["' + deviceName.lower() + '_sensor"],"name":"' + deviceName + 'Sensors","model":"RPI ' + deviceName + '","manufacturer":"RPI"}, "icon":"mdi:harddisk"}', qos=1, retain=True, ) def _parser(): """Generate argument parser""" parser = argparse.ArgumentParser() parser.add_argument("settings", help="path to the settings file") return parser def on_connect(client, userdata, flags, rc): if rc == 0: print("Connected to broker") client.subscribe("hass/status") else: print("Connection failed") if __name__ == "__main__": args = _parser().parse_args() with open(args.settings) as f: # use safe_load instead load settings = yaml.safe_load(f) check_settings(settings) DEFAULT_TIME_ZONE = timezone(settings["timezone"]) if "update_interval" in settings: WAIT_TIME_SECONDS = settings["update_interval"] mqttClient = mqtt.Client(client_id=settings["client_id"]) mqttClient.on_connect = on_connect #attach function to callback mqttClient.on_message = on_message deviceName = settings["deviceName"] if "user" in settings["mqtt"]: mqttClient.username_pw_set( settings["mqtt"]["user"], settings["mqtt"]["password"] ) # Username and pass if configured otherwise you should comment out this signal.signal(signal.SIGTERM, signal_handler) signal.signal(signal.SIGINT, signal_handler) if "port" in settings["mqtt"]: mqttClient.connect(settings["mqtt"]["hostname"], settings["mqtt"]["port"]) else: mqttClient.connect(settings["mqtt"]["hostname"], 1883) try: send_config_message(mqttClient) except: print("something whent wrong") job = Job(interval=timedelta(seconds=WAIT_TIME_SECONDS), execute=updateSensors) job.start() mqttClient.loop_start() while True: try: sys.stdout.flush() time.sleep(1) except ProgramKilled: print("Program killed: running cleanup code") mqttClient.disconnect() mqttClient.loop_stop() sys.stdout.flush() job.stop() break