Is there a good MQTT client to broadcast DiskUsage/CPUTemp/etc from a macOS computer?

got a mac mini server. i’d like to have HA show the status of the server DiskUsage, CPUtemp, etc. is there a client i can get for macOS (or linux) that will broadcast those things to my MQTT broker?

If there isn’t one (ie you can’t find one that suits your purposes), I’d be interested in making one. This sounds very useful and is something I hadn’t even thought of!

yeah i was thinking if there wasn’t one, i could prolly whip something together with a shell script and cron.

May be a bit more robust and cross-platform in python - the os library might have a lot of what we are looking for (not sure), and you’d get the benefit of mqtt keep alive, birth messages, lwt, etc, which you wouldn’t get with one-shot messages using mosquitto_pub.

psutil actually looks like just the python library we need - it’s what Glances is based on, which I’ve used for my server admin in the past.

so just googling, i found this. https://github.com/eschava/psmqtt

2 Likes

^ cannot get this to work on macOS.

keeps giving me python syntax errors.

this is how i got it working on a 10.11.6 mac mini

brew install python
pip3 install glances
pip3 install recurrent paho-mqtt python-dateutil psutil jinja2

unzip the repo.

run> python3 psmqtt.py

will prolly give python syntax errors, i fixed it.
replace the psmqtt.py file with this:
you might need to chmod the psmqtt.conf file to be 0777

#!/usr/bin/env python

import os
import sys
import time
import socket
import logging
import sched
from threading import Thread
from datetime import datetime

import paho.mqtt.client as paho  # pip install paho-mqtt
from recurrent import RecurringEvent  # pip install recurrent
from dateutil.rrule import *  # pip install python-dateutil

from handlers import handlers
from format import Formatter

CONFIG = os.getenv('PSMQTTCONFIG', 'psmqtt.conf')

class Config(object):
    def __init__(self, filename=CONFIG):
        self.config = {}
        #execfile(filename, self.config)
        exec(open(filename).read(), self.config)

    def get(self, key, default=None):
        return self.config.get(key, default)

try:
    cf = Config()
except Exception as e:
    print ("Cannot" + str(e)) #load configuration from file " #%s: %s" % (CONFIG, str(e))
    sys.exit(2)

qos = cf.get('mqtt_qos', 0)
retain = cf.get('mqtt_retain', False)

topic_prefix = cf.get('mqtt_topic_prefix', 'psmqtt/' + socket.gethostname() + '/')
request_topic = cf.get('mqtt_request_topic', 'request')
if request_topic != '':
    request_topic = topic_prefix + request_topic + '/'

# fix for error 'No handlers could be found for logger "recurrent"'
reccurrent_logger = logging.getLogger('recurrent')
if len(reccurrent_logger.handlers) == 0:
    reccurrent_logger.addHandler(logging.NullHandler())


def run_task(task, topic):
    if task.startswith(topic_prefix):
        task = task[len(topic_prefix):]

    topic = Topic(topic if topic.startswith(topic_prefix) else topic_prefix + topic)
    try:
        payload = get_value(task)
        is_seq = isinstance(payload, list) or isinstance(payload, dict)
        if is_seq and not topic.is_multitopic():
            raise Exception("Result of task '" + task + "' has several values but topic doesn't contain '*' char")
        if isinstance(payload, list):
            for i, v in enumerate(payload):
                topic = topic.get_subtopic(str(i))
                mqttc.publish(topic, str(v), qos=qos, retain=retain)
        elif isinstance(payload, dict):
            for key in payload:
                topic = topic.get_subtopic(str(key))
                v = payload[key]
                mqttc.publish(topic, str(v), qos=qos, retain=retain)
        else:
            mqttc.publish(topic.get_topic(), str(payload), qos=qos, retain=retain)
    except Exception as ex:
        mqttc.publish(topic.get_error_topic(), str(ex), qos=qos, retain=retain)
        logging.exception(task + ": " + str(ex))


def get_value(path):
    path, _format = Formatter.get_format(path)
    head, tail = split(path)

    if head in handlers:
        value = handlers[head].handle(tail)
        if _format is not None:
            value = Formatter.format(_format, value)
        return value
    else:
        raise Exception("Element '" + head + "' in '" + path + "' is not supported")


class Topic:
    def __init__(self, topic):
        self.topic = topic
        self.wildcard_index, self.wildcard_len = self._find_wildcard(topic)

    @staticmethod
    def _find_wildcard(topic):
        start = 0
        # search for * or ** (but not *; or **;) outside of []
        while start < len(topic):
            wildcard_index = topic.find('*', start)
            if wildcard_index < 0:
                break
            bracket_index = topic.find('[', start)
            if 0 <= bracket_index < wildcard_index:
                start = topic.find(']', bracket_index)
                continue
            wildcard_len = 1
            if wildcard_index + 1 < len(topic) and topic[wildcard_index + 1] == '*':  # ** sequence
                wildcard_len += 1
            if wildcard_index + wildcard_len < len(topic) and topic[wildcard_index + wildcard_len] == ';':
                start = wildcard_index + wildcard_len
                continue
            return wildcard_index, wildcard_len
        return -1, -1

    def is_multitopic(self):
        return self.wildcard_index > 0

    def get_subtopic(self, param):
        if self.wildcard_index < 0:
            raise Exception("Topic " + self.topic + " have no wildcard")
        return self.topic[:self.wildcard_index] + param + self.topic[self.wildcard_index + self.wildcard_len:]

    def get_topic(self):
        return self.topic

    def get_error_topic(self):
        return self.topic + "/error"


# noinspection PyUnusedLocal
def on_message(mosq, userdata, msg):
    logging.debug(msg.topic + " " + str(msg.qos) + " " + str(msg.payload))

    if msg.topic.startswith(request_topic):
        task = msg.topic[len(request_topic):]
        run_task(task, task)
    else:
        logging.warn('Unknown topic: ' + msg.topic)


def on_timer(s, rrule, tasks):
    if isinstance(tasks, dict):
        for k in tasks:
            run_task(k, tasks[k])
    elif isinstance(tasks, list):
        for task in tasks:
            if isinstance(task, dict):
                for k in task:
                    run_task(k, task[k])
            else:
                run_task(task, task)
    else:
        run_task(tasks, tasks)

    # add next timer task
    now = datetime.now()
    delay = (rrule.after(now) - now).total_seconds()
    s.enter(delay, 1, on_timer, [s, rrule, tasks])


# noinspection PyUnusedLocal
def on_connect(client, userdata, flags, result_code):
    if request_topic != '':
        topic = request_topic + '#'
        logging.debug("Connected to MQTT broker, subscribing to topic " + topic)
        mqttc.subscribe(topic, qos)


# noinspection PyUnusedLocal
def on_disconnect(mosq, userdata, rc):
    logging.debug("OOOOPS! psmqtt disconnects")
    time.sleep(10)


def split(s):
    parts = s.split("/", 1)
    return parts if len(parts) == 2 else [parts[0], '']


class TimerThread(Thread):
    def __init__(self, s):
        Thread.__init__(self)
        self.s = s

    def run(self):
        self.s.run()


if __name__ == '__main__':
    #print(cf.get('mqtt_clientid'))

    clientid = cf.get('mqtt_clientid', 'psmqtt-%s' % os.getpid())
    # initialise MQTT broker connection
    mqttc = paho.Client(clientid, clean_session=cf.get('mqtt_clean_session', False))

    mqttc.on_message = on_message
    mqttc.on_connect = on_connect
    mqttc.on_disconnect = on_disconnect

    mqttc.will_set('clients/psmqtt', payload="Adios!", qos=0, retain=False)

    # Delays will be: 3, 6, 12, 24, 30, 30, ...
    # mqttc.reconnect_delay_set(delay=3, delay_max=30, exponential_backoff=True)

    #print(cf.get('mqtt_username'))

    mqttc.username_pw_set(cf.get('mqtt_username'), cf.get('mqtt_password'))

    mqttc.connect(cf.get('mqtt_broker', 'localhost'), int(cf.get('mqtt_port', '1883')), 60)

    # parse schedule
    schedule = cf.get('schedule', {})
    s = sched.scheduler(time.time, time.sleep)
    now = datetime.now()
    for t in schedule:
        r = RecurringEvent()
        dt = r.parse(t)
        if not r.is_recurring:
            logging.error(t + " is not recurring time. Skipping")
            continue
        rrule = rrulestr(dt)
        delay = (rrule.after(now) - now).total_seconds()
        s.enter(delay, 1, on_timer, [s, rrule, schedule[t]])

    tt = TimerThread(s)
    tt.daemon = True
    tt.start()

    while True:
        try:
            mqttc.loop_forever()
        except socket.error:
            time.sleep(5)
        except KeyboardInterrupt:
            sys.exit(0)

sorry, step one is not brew but
download and install the python 3.6 for mac

also here’s a launchd.plist for the script.
change the name/paths accordingly.

nano ~/Library/LaunchAgents/stone.psmqtt.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
        <key>Label</key>
        <string>stone.psmqtt</string>
        <key>ProgramArguments</key>
        <array>
                <string>/usr/local/bin/python3</string>
                <string>/Users/stone/psmqtt/psmqtt.py</string>
        </array>
        <key>RunAtLoad</key>
        <true/>
        <key>WorkingDirectory</key>
        <string>/Users/stone/psmqtt</string>
</dict>
</plist>

launchctl load stone.psmqtt.plist
launchctl start stone.psmqtt

check it in
tail /var/log/system.log

to remove it
launchctl stop stone.psmqtt
launchctl unload stone.psmqtt.plist

does anyone know how to turn the CPU array into something more readable?
i assume with some template action.

sensor.morrigan_cpu_usage	[23.9, 8.5, 23.0, 9.2]	friendly_name: Morrigan CPU Usage, unit_of_measurement: %

the mqtt:  psmqtt/morrigan.local/cpu_percent/*; [23.3, 7.7, 21.9, 7.9]

Hi!
I’m the author of the PSMQTT utility and if your questions are still actual I’m ready to answer them :slight_smile:

just gonna leave this here
https://glances.readthedocs.io/en/stable/gw/mqtt.html