diff --git a/README.md b/README.md index e86cdee..a516bde 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,6 @@ The easiest way to track your Raspberry Pi or Ubuntu computer system health and * Easy update: You can update the script by calling it with command line argument --update - ## Table of Contents - [What is new](#what-is-new) @@ -215,12 +214,16 @@ Create a cron entry like this (you might need to update the path in the cron ent ``` ## How to update -Navigate to the folder where Rapsberry Pi MQTT Monitor is installed and pull the git repository: +Remote updates via Home Assistant are now available. + +To use these you need to have the script running as a service. (the installer now supports this) + +Manual update: ```bash -git pull +cd rpi-mqtt-monitor +python3 src/update.py ``` -* Note that sometimes you might need to add new variables to our src/config.py file, so make sure you check the example file and update your config.py file accordingly. ## Home Assistant Integration diff --git a/src/config.py.example b/src/config.py.example index c740ca5..514c71e 100644 --- a/src/config.py.example +++ b/src/config.py.example @@ -21,6 +21,9 @@ discovery_messages = True # Binary sensor that displays when there are updates git_update = True +# Enable remote update of the script via Home Assistant +update = True + # Retain flag for published topics retain = True @@ -44,6 +47,7 @@ sys_clock_speed = False swap = False memory = True uptime = True +uptime_seconds = False # Enable wifi_signal for unit of measuring % or wifi_signal_dbm for unit of meaning dBm wifi_signal = False diff --git a/src/rpi-cpu2mqtt.py b/src/rpi-cpu2mqtt.py index a3fdb2f..33665f8 100644 --- a/src/rpi-cpu2mqtt.py +++ b/src/rpi-cpu2mqtt.py @@ -10,10 +10,12 @@ import time import socket import paho.mqtt.client as paho import json -import config import os +import sys import argparse +import threading import update +import config # get device host name - used in mqtt topic hostname = socket.gethostname() @@ -100,8 +102,8 @@ def check_sys_clock_speed(): return subprocess.Popen(full_cmd, shell=True, stdout=subprocess.PIPE).communicate()[0] -def check_uptime(): - full_cmd = "awk '{print int($1/3600/24)}' /proc/uptime" +def check_uptime(format): + full_cmd = "awk '{print int($1"+format+")}' /proc/uptime" return int(subprocess.Popen(full_cmd, shell=True, stdout=subprocess.PIPE).communicate()[0]) @@ -112,7 +114,7 @@ def check_model_name(): if model_name == '': full_cmd = "cat /proc/cpuinfo | grep 'name'| uniq" model_name = subprocess.Popen(full_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()[0].decode("utf-8") - model_name = model_name.split(':')[1] + model_name = model_name.split(':')[1].replace('\n', '') return model_name @@ -136,7 +138,7 @@ def get_manufacturer(): if 'Raspberry' not in check_model_name(): full_cmd = "cat /proc/cpuinfo | grep 'vendor'| uniq" pretty_name = subprocess.Popen(full_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()[0].decode("utf-8") - pretty_name = pretty_name.split(':')[1] + pretty_name = pretty_name.split(':')[1].replace('\n', '') else: pretty_name = 'Raspberry Pi' @@ -144,31 +146,27 @@ def get_manufacturer(): def check_git_update(script_dir): - full_cmd = "git -C {} remote update && git -C {} status -uno".format(script_dir, script_dir) - try: - git_update = subprocess.Popen(full_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()[0].decode("utf-8") - except subprocess.CalledProcessError as e: - print("Error updating git repository:", e.output) - - if any(s in git_update for s in ('Your branch is up to date', 'Your branch is up-to-date', 'Votre branche est à jour')): - git_update = 'off' + if config.version == update.check_git_version_remote(script_dir): + git_update = { + "installed_ver": config.version, + "new_ver": config.version, + } else: - git_update = 'on' + git_update = { + "installed_ver": config.version, + "new_ver": update.check_git_version_remote(script_dir), + } - return(git_update) + return(json.dumps(git_update)) def check_git_version(script_dir): full_cmd = "git -C {} describe --tags `git -C {} rev-list --tags --max-count=1`".format(script_dir, script_dir) - git_version = subprocess.Popen(full_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()[0].decode("utf-8") + git_version = subprocess.Popen(full_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()[0].decode("utf-8").replace('\n', '') return(git_version) -def check_git_version_remote(script_dir): - full_cmd = "git -C {} ls-remote --tags origin | awk -F'/' '{{print $3}}' | sort -V | tail -n 1".format(script_dir) - result = subprocess.Popen(full_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()[0].decode("utf-8") - latest_tag = result.strip() - return latest_tag if latest_tag else None + def get_network_ip(): s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) @@ -182,14 +180,15 @@ def get_network_ip(): s.close() return IP -def print_measured_values(): +def print_measured_values( cpu_load=0, cpu_temp=0, used_space=0, voltage=0, sys_clock_speed=0, swap=0, memory=0, + uptime_days=0, wifi_signal=0, wifi_signal_dbm=0, rpi5_fan_speed=0, git_update=False): print(":: rpi-mqtt-monitor") print(" Version: " + config.version) print("") print(":: Device Information") - print(" Model Name: " + check_model_name().strip()) - print(" Manufacturer: " + get_manufacturer().strip()) - print(" OS: " + get_os().strip()) + print(" Model Name: " + check_model_name()) + print(" Manufacturer: " + get_manufacturer()) + print(" OS: " + get_os()) print(" Hostname: " + hostname) print(" IP Address: " + get_network_ip()) if args.service: @@ -204,6 +203,7 @@ def print_measured_values(): print(" Swap: " + str(swap) + " %") print(" Memory: " + str(memory) + " %") print(" Uptime: " + str(uptime_days) + " days") + print(" Uptime: " + str(uptime_seconds) + " seconds") print(" Wifi Signal: " + str(wifi_signal) + " %") print(" Wifi Signal dBm: " + str(wifi_signal_dbm) + " dBm") print(" RPI5 Fan Speed: " + str(rpi5_fan_speed) + " RPM") @@ -222,10 +222,11 @@ def config_json(what_config): "unit_of_measurement": "", "device": { "identifiers": [hostname], - "manufacturer": manufacturer, - "model": model_name, + "manufacturer": 'github.com/hjelev', + "model": 'RPi MQTT Monitor ' + config.version, "name": hostname, - "sw_version": "rpi-mqtt-monitor " + config.version + " on " + os, + "sw_version": os, + "hw_version": model_name + " by " + manufacturer, "configuration_url": "https://github.com/hjelev/rpi-mqtt-monitor" } } @@ -272,6 +273,12 @@ def config_json(what_config): data["name"] = "Uptime" data["unit_of_measurement"] = "days" data["state_class"] = "measurement" + elif what_config == "uptime_seconds": + data["icon"] = "mdi:timer-outline" + data["name"] = "Uptime" + data["unit_of_measurement"] = "s" + data["device_class"] = "duration" + data["state_class"] = "total_increasing" elif what_config == "wifi_signal": data["icon"] = "mdi:wifi" data["name"] = "Wifi Signal" @@ -290,10 +297,23 @@ def config_json(what_config): elif what_config == "git_update": data["icon"] = "mdi:git" data["name"] = "RPi MQTT Monitor" + data["title"] = "Device Update" data["device_class"] = "update" data["state_class"] = "measurement" data["payload_on"] = "on" data["payload_off"] = "off" + data["value_template"] = "{{ 'on' if value_json.installed_ver != value_json.new_ver else 'off' }}" + elif what_config == "update": + version = check_git_version(script_dir).strip() + data["icon"] = "mdi:update" + data["name"] = "RPi MQTT Monitor" + data["title"] = "Version" + data["state_topic"] = config.mqtt_topic_prefix + "/" + hostname + "/" + "git_update" + data["value_template"] = "{{ {'installed_version': value_json.installed_ver, 'latest_version': value_json.new_ver } | to_json }}" + data["device_class"] = "firmware" + data["command_topic"] = "homeassistant/update/" + hostname + "/command" + data["payload_install"] = "install" + data['release_url'] = "https://github.com/hjelev/rpi-mqtt-monitor/releases/tag/" + version else: return "" # Return our built discovery config @@ -310,7 +330,7 @@ def on_connect(client, userdata, flags, rc): def publish_to_mqtt(cpu_load=0, cpu_temp=0, used_space=0, voltage=0, sys_clock_speed=0, swap=0, memory=0, - uptime_days=0, wifi_signal=0, wifi_signal_dbm=0, rpi5_fan_speed=0, git_update=False): + uptime_days=0, uptime_seconds=0, wifi_signal=0, wifi_signal_dbm=0, rpi5_fan_speed=0, git_update=False, update=False): # connect to mqtt server client = paho.Client(client_id="rpi-mqtt-monitor-" + hostname) client.username_pw_set(config.mqtt_user, config.mqtt_password) @@ -366,6 +386,11 @@ def publish_to_mqtt(cpu_load=0, cpu_temp=0, used_space=0, voltage=0, sys_clock_s client.publish("homeassistant/sensor/" + config.mqtt_topic_prefix + "/" + hostname + "_uptime_days/config", config_json('uptime_days'), qos=config.qos) client.publish(config.mqtt_topic_prefix + "/" + hostname + "/uptime_days", uptime_days, qos=config.qos, retain=config.retain) + if config.uptime_seconds: + if config.discovery_messages: + client.publish("homeassistant/sensor/" + config.mqtt_topic_prefix + "/" + hostname + "_uptime/config", + config_json('uptime'), qos=config.qos) + client.publish(config.mqtt_topic_prefix + "/" + hostname + "/uptime_seconds", uptime_seconds, qos=config.qos, retain=config.retain) if config.wifi_signal: if config.discovery_messages: client.publish("homeassistant/sensor/" + config.mqtt_topic_prefix + "/" + hostname + "_wifi_signal/config", @@ -387,15 +412,20 @@ def publish_to_mqtt(cpu_load=0, cpu_temp=0, used_space=0, voltage=0, sys_clock_s config_json('git_update'), qos=config.qos) client.publish(config.mqtt_topic_prefix + "/" + hostname + "/git_update", git_update, qos=config.qos, retain=config.retain) client.loop_stop() + if config.update: + if config.discovery_messages: + client.publish("homeassistant/update/" + hostname + "/config", + config_json('update'), qos=config.qos) + # client.publish(config.mqtt_topic_prefix + "/" + hostname + "/git_update", git_update, qos=config.qos, retain=config.retain) # disconnect from mqtt server client.disconnect() def bulk_publish_to_mqtt(cpu_load=0, cpu_temp=0, used_space=0, voltage=0, sys_clock_speed=0, swap=0, memory=0, - uptime_days=0, wifi_signal=0, wifi_signal_dbm=0, rpi5_fan_speed=0, git_update=0): + uptime_days=0, uptime_seconds=0, wifi_signal=0, wifi_signal_dbm=0, rpi5_fan_speed=0, git_update=0): # compose the CSV message containing the measured values - values = cpu_load, cpu_temp, used_space, voltage, int(sys_clock_speed), swap, memory, uptime_days, wifi_signal, wifi_signal_dbm, rpi5_fan_speed, git_update + values = cpu_load, cpu_temp, used_space, voltage, int(sys_clock_speed), swap, memory, uptime_days, uptime_seconds, wifi_signal, wifi_signal_dbm, rpi5_fan_speed, git_update values = str(values)[1:-1] client = paho.Client(client_id="rpi-mqtt-monitor-" + hostname) @@ -425,7 +455,7 @@ def parse_arguments(): args = parser.parse_args() if args.update: - version = check_git_version_remote(script_dir).strip() + version = update.check_git_version_remote(script_dir).strip() git_update = check_git_update(script_dir) if git_update == 'on': @@ -433,13 +463,13 @@ def parse_arguments(): else: git_update = False - update.do_update(version, git_update) + update.do_update(script_dir, version, git_update) exit() if args.version: - installed_version = check_git_version(script_dir).strip() - latest_versino = check_git_version_remote(script_dir).strip() + installed_version = config.version + latest_versino = update.check_git_version_remote(script_dir).strip() print("Installed version: " + installed_version) print("Latest version: " + latest_versino) if installed_version != latest_versino: @@ -449,14 +479,10 @@ def parse_arguments(): exit() return args - -if __name__ == '__main__': - script_dir = os.path.dirname(os.path.realpath(__file__)) - args = parse_arguments(); - +def gather_and_send_info(): while True: # set all monitored values to False in case they are turned off in the config - cpu_load = cpu_temp = used_space = voltage = sys_clock_speed = swap = memory = uptime_days = wifi_signal = wifi_signal_dbm = rpi5_fan_speed = git_update = False + cpu_load = cpu_temp = used_space = voltage = sys_clock_speed = swap = memory = uptime_seconds = uptime_days = wifi_signal = wifi_signal_dbm = rpi5_fan_speed = git_update = update = False # delay the execution of the script if hasattr(config, 'random_delay'): time.sleep(config.random_delay) @@ -480,7 +506,9 @@ if __name__ == '__main__': if config.memory: memory = check_memory() if config.uptime: - uptime_days = check_uptime() + uptime_days = check_uptime('/3600/24') + if config.uptime_seconds: + uptime_seconds = check_uptime('') if config.wifi_signal: wifi_signal = check_wifi_signal('') if config.wifi_signal_dbm: @@ -489,19 +517,65 @@ if __name__ == '__main__': rpi5_fan_speed = check_rpi5_fan_speed() if config.git_update: git_update = check_git_update(script_dir) + if config.update: + update = check_git_update(script_dir) # Display collected values on screen if --display option is used if args.display: - print_measured_values() + print_measured_values(cpu_load, cpu_temp, used_space, voltage, sys_clock_speed, swap, memory, uptime_days, wifi_signal, wifi_signal_dbm, rpi5_fan_speed, git_update) # Publish messages to MQTT if hasattr(config, 'group_messages') and config.group_messages: - bulk_publish_to_mqtt(cpu_load, cpu_temp, used_space, voltage, sys_clock_speed, swap, memory, uptime_days, wifi_signal, wifi_signal_dbm, rpi5_fan_speed, git_update) + bulk_publish_to_mqtt(cpu_load, cpu_temp, used_space, voltage, sys_clock_speed, swap, memory, uptime_days, uptime_seconds, wifi_signal, wifi_signal_dbm, rpi5_fan_speed, git_update) else: - publish_to_mqtt(cpu_load, cpu_temp, used_space, voltage, sys_clock_speed, swap, memory, uptime_days, wifi_signal, wifi_signal_dbm, rpi5_fan_speed, git_update) + publish_to_mqtt(cpu_load, cpu_temp, used_space, voltage, sys_clock_speed, swap, memory, uptime_days, uptime_seconds, wifi_signal, wifi_signal_dbm, rpi5_fan_speed, git_update, update) # if not running as a service, break the loop after one iteration if not args.service: break # if running as a service, sleep before the next iteration time.sleep(config.service_sleep_time) + +def on_message(client, userdata, msg): + global exit_flag + print("Received message: ", msg.payload.decode()) + if msg.payload.decode() == "install": + version = update.check_git_version_remote(script_dir).strip() + update.do_update(script_dir, version, git_update=True, config_update=True) + print("Update completed. Setting exit flag...") + exit_flag = True + +exit_flag = False + +if __name__ == '__main__': + script_dir = os.path.dirname(os.path.realpath(__file__)) + args = parse_arguments(); + + if args.service: + client = paho.Client() + client.username_pw_set(config.mqtt_user, config.mqtt_password) + client.on_message = on_message + + try: + client.connect(config.mqtt_host, int(config.mqtt_port)) + except Exception as e: + print("Error connecting to MQTT broker:", e) + sys.exit(1) # Exit the script + + client.subscribe("homeassistant/update/" + hostname + "/command") # Replace with your MQTT topic + print("Listening to topic : " + "homeassistant/update/" + hostname + "/command") + # Start the gather_and_send_info function in a new thread + thread = threading.Thread(target=gather_and_send_info) + thread.daemon = True # Set the daemon attribute to True + thread.start() + + client.loop_start() # Start the MQTT client loop in a new thread + + # Check the exit flag in the main thread + while True: + if exit_flag: + print("Exit flag set. Exiting the application...") + sys.exit(0) + time.sleep(1) # Check the exit flag every second + else: + gather_and_send_info() diff --git a/src/update.py b/src/update.py index e5296df..a47e23a 100644 --- a/src/update.py +++ b/src/update.py @@ -3,7 +3,6 @@ import os import subprocess import config - def get_assignments(filename): with open(filename) as f: tree = ast.parse(f.read(), filename) @@ -19,9 +18,19 @@ def update_config(current_config, example_config): missing_assignments = {var: value for var, value in example_assignments.items() if var not in current_assignments} if missing_assignments: + with open(current_config, 'ab+') as f: # Open the file in binary mode + f.seek(-1, os.SEEK_END) # Move the cursor to the last character + last_char = f.read(1) # Read the last character + + # If the last character is not a newline, write a newline + if last_char != b'\n': + with open(current_config, 'a') as f: # Open the file in text mode + f.write('\n') + + # Write the missing assignments with open(current_config, 'a') as f: for var, value in missing_assignments.items(): - f.write('\n{} = {!r}'.format(var, value)) + f.write('{} = {!r}\n'.format(var, value)) def display_config_differences(current_config, example_config, display=True): @@ -39,6 +48,13 @@ def display_config_differences(current_config, example_config, display=True): else: return False +def check_git_version_remote(script_dir): + full_cmd = "git -C {} ls-remote --tags origin | awk -F'/' '{{print $3}}' | sort -V | tail -n 1".format(script_dir) + result = subprocess.Popen(full_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()[0].decode("utf-8") + latest_tag = result.strip() + return latest_tag if latest_tag else None + + def update_config_version(version, script_dir): with open(script_dir + '/config.py', 'r') as f: lines = f.readlines() @@ -52,11 +68,10 @@ def update_config_version(version, script_dir): f.write(line) -def do_update(version=config.version, git_update=True, config_update=True): - script_dir = os.path.dirname(os.path.realpath(__file__)) +def do_update(script_dir, version=config.version, git_update=True, config_update=True): print("Current version: {}".format(config.version)) if git_update: - print(":: Updating git repository") + print(":: Updating git repository", script_dir) result = subprocess.run(['git', '-C', script_dir, 'pull'], check=True, universal_newlines=True, stdout=subprocess.PIPE) print(result.stdout) @@ -68,5 +83,6 @@ def do_update(version=config.version, git_update=True, config_update=True): update_config_version(version, script_dir) -if __name__ == '__main__': - do_update() \ No newline at end of file +if __name__ == '__main__': + script_dir = os.path.dirname(os.path.realpath(__file__)) + do_update(script_dir,check_git_version_remote(script_dir)) \ No newline at end of file