Using PROXMOX on a laptop with a battery

Hi,
this was triggered by a comment on the “Home Assistant” group in FB and it made me research how I can avoid that the laptop, which is used as my proxmox server for Home Assistant, will be charging its battery all the time. Lithium batteries don’t really like this and having them plugged in all the time can actually even shorten their lifetime.
I used my old MacBook especially because it has a battery, sort of a “free” UPS if you like. So in order to get this working in Home Assistant I did the following.

I first of all need a tool which can retrieve the battery status, after some looking around I decided to install upower and use this to retrieve the battery status. So if you want to do this too, login to the shell of your proxmox server (mine is called PVE01) as root and use apt to install upower:

apt install upower

Test it with this command:

power -i $(upower -e | grep BAT) | grep --color=never -E “state|to\ full|to\ empty|percentage”

The output will look something like this:

state: charging
time to full: 2.6 hours
percentage: 39%

I found that the “time to full” is not always shown, especially when the device is fully charged.

Next make sure you have python3, pip3 and the paho mqtt client libraries installed. If not these commands can help you:

apt install python3 pip3
pip3 install paho.mqtt.client

If you installed or updated to proxmox 8, you might have to replace the command above with the apt version:

apt install python3-paho-mqtt

I wrote this simple Python script to send the data to my MQTT server, running under Home Assistant. Make sure you adapt the correct IP addresses and username/password.

#!/bin/python3
#
import subprocess
import time
import json

from paho.mqtt import client as mqtt_client

broker="mqtt.local.lan"
port=1883
topic="/proxmox/PVE01/battery"
username="xxxxxxxx"
password="pppppppp"
client_id = "PVE01"

debug = 0

cmd = 'upower -i $(upower -e | grep BAT) | grep --color=never -E "state|to\ full|to\ empty|percentage"'
battery = { 
    "state":"empty",
    "time_to_empty" : "0 hours",
    "percentage" : 0
}

    
def connect_mqtt():
    def on_connect(client, userdata, flags, rc):
        if rc == 0:
            print("Connected to MQTT Broker!")
        else:
            print("Failed to connect, return code %d\n", rc)

    client = mqtt_client.Client(client_id)
    client.username_pw_set(username, password)
    client.on_connect = on_connect
    client.connect(broker, port)
    return client


def publish(client):
    msg_count = 1
    while True:
        returned_output1 = subprocess.check_output(cmd, shell=True, text=True)

        list = returned_output1.splitlines()
        ll = len(list)
        v = list[0].split(":")
        battery["state"] = v[1].strip();
        if ll == 3:
            v = list[1].split(":")
            battery["time_to_empty"] = v[1].strip()
            v = list[2].split(":")
            battery["percentage"] = int(float(v[1].strip()[:-1]))
        else:
            v = list[1].split(":")
            battery["percentage"] = int(float(v[1].strip()[:-1]))
            battery["time_to_empty"] = "unknown"
        msg = json.dumps(battery)
        if debug != 0:
            print(msg)
        result = client.publish(topic, msg)
        # result: [0, 1]
        status = result[0]
        if status == 0:
            if debug != 0:
                print(f"Send `{msg}` to topic `{topic}`")
        else:
            print(f"Failed to send message to topic {topic}")
        msg_count += 1
        time.sleep(60)


def run():
    client = connect_mqtt()
    client.loop_start()
    publish(client)
    client.loop_stop()


if __name__ == '__main__':
    run()

So to get this value (I am mostly interested in the percentage), I defined the sensor in my MQTT entry in my configuration.
configuration.xml

mqtt: !include mqtt.yaml

mqtt.yaml

sensor:
  - name: "PVE01 battery"
    unit_of_measurement: "%"
    state_topic: "/proxmox/PVE01/battery"
    value_template: "{{ value_json.percentage }}"
    device_class: battery
    unique_id: pve01_battery

My LIDL switch is connected through Zigbee2MQTT, but I guess you can use any remote switch to do the necessary automation, so that your laptop will be disconnected when it is 90% full and reconnected when the battery goes below 30%.

image

This is how I see the values on my testing dashboard:

image

The yaml code looks like this:

  - square: false
    type: grid
    cards:
      - graph: line
        type: sensor
        entity: sensor.pve01_battery
        detail: 1
      - show_name: true
        show_icon: true
        type: button
        tap_action:
          action: toggle
        entity: switch.lidl_proxmox
        name: PVE01 Charger
    columns: 2

And the last step I implemented was an automation which checks the battery level every time it is reported to my HA through MQTT.

alias: PVE01 Battery Charging
description: ""
trigger:
  - platform: time_pattern
    minutes: /5
condition: []
action:
  - choose:
      - conditions:
          - condition: numeric_state
            entity_id: sensor.pve01_battery
            above: 90
        sequence:
          - service: switch.turn_off
            data: {}
            target:
              device_id: f6e4db0c95d342f6b7d9b542f3a08aae
      - conditions:
          - condition: numeric_state
            entity_id: sensor.pve01_battery
            below: 30
        sequence:
          - service: switch.turn_on
            data: {}
            target:
              device_id: f6e4db0c95d342f6b7d9b542f3a08aae
mode: single

And to run it, I just added this entry to my crontab:

# m h  dom mon dow   command
*/5 * * * * /root/dev/battery.py

I hope this will be helpful to some!

9 Likes

hi. really good write-up. i have a couple of comments when i tried implementing:

the command should be this:

upower -i $(upower -e | grep BAT) | grep --color=never -E "state|to\ full|to\ empty|percentage"

i think it’s the double-quotes that are not the right characters in your original post.

when installing python and the mqtt client, i had to use

apt install python3 pip
pip3 install paho.mqtt

instead for these to install

and at this point i’m stuck. So a couple of questions:

  • where do you store the python script. what did you call it?
  • does it run automatically? how did you setup?

thanks again! great job

I went down a similar route to monitor the temperatures on my Proxmox setup, the code below is a bit hillbilly tech and could use some smarter eyes on it, however perhaps will give you some more ideas, good hunting!

#! /usr/bin/env python3
# server_stats_mqtt.py
# 202306231430
#
# collect server info and publish to mqtt topic
#

import time
# https://github.com/wookayin/gpustat
import gpustat
import platform
import re
import json
import paho.mqtt.client as mqtt

'''
setup a service to run the python program at startup

sudo vi /etc/systemd/system/server_stats_mqtt_py.service
# content for this file :
[Unit]
Description=Server Stats MQTT Publish
Wants=network-online.target
After=network-online.target
[Service]
Restart=always
Type=simple
RestartSec=5
User=root
ExecStartPre= /bin/sh -c 'until ping -c1 192.168.2.242; do sleep 1; done;'
ExecStart=/usr/bin/python3 /usr/local/bin/server_stats_mqtt.py
[Install]
WantedBy=network-online.target

enable the python program as a service

systemctl daemon-reload
systemctl enable server_stats_mqtt_py.service
systemctl start server_stats_mqtt_py.service
systemctl status server_stats_mqtt_py.service


'''


# mqtt server
mqtt_server = "192.168.2.242"

# mqtt topic where to publish 
mqtt_topic = 'server_stats/'

# connect to MQTT server
mqttc = mqtt.Client('server_stats')  # Create instance of client with client ID
mqttc.connect(mqtt_server, 1883)  # Connect to (broker, port, keepalive-time)
mqttc.loop_start()

# test if nvidia gpus are present
try :
    gpus = gpustat.new_query()
    nvidia_present = True
except :
    nvidia_present = False

while True :

    # get cpu temperature
    with open('/sys/class/hwmon/hwmon1/temp1_input', 'r') as f:
        cpu_temperature = str(int(f.read().strip()) / 1000)

    # get cpu critical temperature
    with open('/sys/class/hwmon/hwmon1/temp1_crit', 'r') as f:
        cpu_temperature_critical = str(int(f.read().strip()) / 1000)

    # get cpu operating frequency info
    core_frequency = []
    with open('/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq', 'r') as f:
        core_frequency.append(str(f.read()).strip())

    # get memory info
    with open('/proc/meminfo', 'r') as f:
        mem_info = f.read().splitlines()
        total_memory = re.search(r'MemTotal:(.*?)kB', mem_info[0]).group(1).replace(' ', '')
        available_memory = re.search(r'MemFree:(.*?)kB', mem_info[1]).group(1).replace(' ', '')

    # get info on GPU's in the system
    # https://github.com/wookayin/gpustat/issues/132
    if nvidia_present :
        gpus = gpustat.new_query()

    # create a JSON structure with the collected information
    system_info = {}
    system_info['timestamp'] = str(time.time())
    system_info['hostname'] = str(platform.node())
    system_info['cpu_package_temperature'] = float(cpu_temperature)
    system_info['cpu_temperature_critical'] = float(cpu_temperature_critical)
    system_info['core_frequency'] = int(core_frequency[0])
    system_info['total_memory'] = int(total_memory)
    system_info['available_memory'] = int(available_memory)

    if nvidia_present :
        for each_gpu in gpus:
            system_info[each_gpu.index] = each_gpu.entry

    json_system_info = json.dumps(system_info)

    # publish to MQTT
    mqttc.publish(mqtt_topic + str(platform.node()), json_system_info, 1)

    time.sleep(0.5)

1 Like

very cool. i’m going to look at what you’ve done (i’m a python novice) and see if i can combine both scripts for an all-in-one proxmox monitor script to mqtt. thanks!

Great idea and write-up! :+1:

I thought about a UPS or a laptop for running HA, but then I asked myself, “what could possibly be run with HA, without power?” :rofl: :rofl: So I decided to leave this alone, as I couldn’t see any use besides avoiding a restart after a short power loss. :slight_smile:

But it is so great someone had the same idea and went through with it! :+1: I love HA and this forum. :slight_smile:

1 Like

See my updated entry, at the end I use cron to run the program every 5 minutes, but using the service call which is described below might be a better option!

1 Like

i went the service call route. thanks!

Thanks for the guide, I had to tweak some things to get it working, but it’s finally sending the battery status to HA.

The big one was getting the entry in crontab -e (as root user) to actually run properly, here’s what finally worked for me: (I’m a Proxmox n00b so it’s been a learning curve)

*/5 * * * * /bin/python3 /dev/battery.py

Looked like it was trying to run in “bin/sh” so it needed to point to the python3 directory. Made sense once I figured that out. lol.
This was on Proxmox VE 8.1.3
Finally have a nice battery conditioning curve:

Cheers!

Oof, this script runs and doesn’t finish, it was causing the system to run out of memory, and fill the swap then crash the HA VM. I’m not experienced enough with Python to troubleshoot the script right now.

A MUCH easier workaround was to install Netdata, and add that sensor to HA using the integration. This took 5 minutes and was much easier. I could’ve save myself many hours of annoyance with Crontab if I had gone this route to start with! lol

Great job Cor
I just set my proxmox hosted on an old Acer nitro 5 and now I’m saving its battery.
I just did one modifcation on python code because you did an infinite loop.
So I change this

def publish(client):
    msg_count = 1
    while msg_count <= 59: #True:

Because you have a cron stating every hours, you need tohave a code stoping before the next restart.
Many thanks for this idea

Any chance you can post the full script and where exactly (as in, which folder in proxmox) this needs to be set up?

I created a file battery.py in home folder

nano /home/battery.py

and putted this code

#!/bin/python3
#
import subprocess
import time
import json

from paho.mqtt import client as mqtt_client

broker="10.10.x.xxx"
port=1883
topic="/proxmox/PVE01/battery"
username="username"
password="password"
client_id = "PVE01"

debug = 1

cmd = 'upower -i $(upower -e | grep BAT) | grep --color=never -E "state|to\ full|to\ empty|percentage"'
battery = { 
    "state":"empty",
    "time_to_empty" : "0 hours",
    "percentage" : 0
}

    
def connect_mqtt():
    def on_connect(client, userdata, flags, rc):
        if rc == 0:
            print("Connected to MQTT Broker!")
        else:
            print("Failed to connect, return code %d\n", rc)

    client = mqtt_client.Client(client_id)
    client.username_pw_set(username, password)
    client.on_connect = on_connect
    client.connect(broker, port)
    return client


def publish(client):
    msg_count = 1
    while msg_count <= 59: #loop 59 minutes 
        returned_output1 = subprocess.check_output(cmd, shell=True, text=True)

        list = returned_output1.splitlines()
        ll = len(list)
        v = list[0].split(":")
        battery["state"] = v[1].strip();
        if ll == 3:
            v = list[1].split(":")
            battery["time_to_empty"] = v[1].strip()
            v = list[2].split(":")
            battery["percentage"] = int(float(v[1].strip()[:-1]))
        else:
            v = list[1].split(":")
            battery["percentage"] = int(float(v[1].strip()[:-1]))
            battery["time_to_empty"] = "unknown"
        msg = json.dumps(battery)
        if debug != 0:
            print(msg)
        result = client.publish(topic, msg)
        # result: [0, 1]
        status = result[0]
        if status == 0:
            if debug != 0:
                print(f"Send `{msg}` to topic `{topic}`")
        else:
            print(f"Failed to send message to topic {topic}")
        msg_count += 1
        time.sleep(60)
client = connect_mqtt()
client.loop_start()
publish(client)
client.loop_stop()

This script send every minutes the status of the battery during 59 minutes and stop.
Then, create a cron lauching this script every hours. For that, type:

crontab -e

and add this line

0 * * * * python3 /home/battery.py

Nothing more to do on Proxmox, now you have to set Home Assistant as Cor explain previously.

2 Likes

Perfect thanks!
Was getting hung up on whether to keep or remove the while True line.

Will modify the original script which I got working yesterday. Now to figure out whether I can get it to publish an autodiscovery payload. Guess I’ve got some more reading to do!

Bit the bullet and manually configured the MQTT sensors for now. Autodiscovery can wait until there’s no sporadic power cuts going on.

In case it helps anyone, here’s my automation to gracefully shutdown HA and fire a notification message (in case it’s an issue with the charger/plug and not an actual power cut):

alias: Auto Shutdown
description: Graceful shutdown when server battery is low
trigger:
  - platform: numeric_state
    entity_id:
      - sensor.server_battery
    below: 15 #Make sure this is higher than the wait_for_trigger below
condition: []
action:
  - choose:
      - conditions:
          - condition: state
            entity_id: binary_sensor.internal_ping  #Can also use state of smart plug instead, but internal ping covers more scenarios
            state: "on"
        sequence:
          - service: notify.mobile_app_your_device  #Replace this with your mobile entity
            data:
              data:
                car_ui: true
                color: red
                ttl: 0
                priority: high
                channel: alarm_stream_max
              title: Server battery low!
              message: Server will automatically shutdown soon
          - wait_for_trigger:
              - platform: numeric_state
                entity_id:
                  - sensor.server_battery
                below: 11
            timeout:
              hours: 0
              minutes: 15
              seconds: 0
              milliseconds: 0
          - service: hassio.host_shutdown #Shuts down HA gracefully and avoids sql errors
            data: {}
      - conditions:
          - condition: state
            entity_id: binary_sensor.internal_ping #No use firing a notification if even internal ping is down. Guaranteed power cut
            state: "off"
        sequence:
          - wait_for_trigger:
              - platform: numeric_state
                entity_id:
                  - sensor.server_battery
                below: 11 #Make sure this is lower than the numeric_state trigger above
            timeout:
              hours: 0
              minutes: 15
              seconds: 0
              milliseconds: 0
          - service: hassio.host_shutdown
            data: {}
mode: single

@PatriceL sorry to bother you again, but I need your help.

Everything works perfectly fine, but I’m getting the following error every minute each time a message is published:

Error 'required key not provided @ data['state_topic']' when processing MQTT discovery message topic: 'homeassistant/sensor/haos101_server_battery/config', message: '{'state': 'pending-charge', 'time_to_empty': 'unknown', 'percentage': 67, 'platform': 'mqtt'}'

Is there a way to add state_topic: "homeassistant/sensor/haos101_server_battery/config" to the payload in the python script and have it sent as json?

@ShadowFist not sure to understand your request. The code already send state topic in json format.
This error message hapen when there is a mistake with the text sent througt MQTT. Could you please share your python file you probably modified? there maybe a space or a characters missing

It seems to have a topic (paho.mqtt requirement) defined, but not state_topic (HA requirement) inside the payload.

This is the payload from MQTT explorer & HA:

{
  "state": "pending-charge",
  "time_to_empty": "unknown",
  "percentage": 67
}

HA expects a state_topic inside of the above.

This is my current python code:

#
import subprocess
import time
import json

from paho.mqtt import client as mqtt_client

broker="192.168.1.10"
port=1883
topic="homeassistant/sensor/haos101_server_battery/config"
username="redacted"
password="redacted"
client_id = "haos101_server_battery"

debug = 0

cmd = 'upower -i $(upower -e | grep BAT) | grep --color=never -E "state|to\ full|to\ empty|percentage"'
battery = { 
    "state":"empty",
    "time_to_empty" : "0 hours",
    "percentage" : 0
}

    
def connect_mqtt():
    def on_connect(client, userdata, flags, rc):
        if rc == 0:
            print("Connected to MQTT Broker!")
        else:
            print("Failed to connect, return code %d\n", rc)

    client = mqtt_client.Client(client_id)
    client.username_pw_set(username, password)
    client.on_connect = on_connect
    client.connect(broker, port)
    return client


def publish(client):
    msg_count = 1
    while msg_count <= 59: #loop 59 minutes 
        returned_output1 = subprocess.check_output(cmd, shell=True, text=True)
        list = returned_output1.splitlines()
        ll = len(list)
        v = list[0].split(":")
        battery["state"] = v[1].strip();
        if ll == 3:
            v = list[1].split(":")
            battery["time_to_empty"] = v[1].strip()
            v = list[2].split(":")
            battery["percentage"] = int(float(v[1].strip()[:-1]))
        else:
            v = list[1].split(":")
            battery["percentage"] = int(float(v[1].strip()[:-1]))
            battery["time_to_empty"] = "unknown"
        msg = json.dumps(battery)
        if debug != 0:
            print(msg)
        result = client.publish(topic, msg)
        # result: [0, 1]
        status = result[0]
        if status == 0:
            if debug != 0:
                print(f"Send `{msg}` to topic `{topic}`")
        else:
            print(f"Failed to send message to topic {topic}")
        msg_count += 1
        time.sleep(60)


def run():
    client = connect_mqtt()
    client.loop_start()
    publish(client)
    client.loop_stop()


if __name__ == '__main__':
    run()

just replace
topic="homeassistant/sensor/haos101_server_battery/config"
with
topic="/homeassistant/sensor/haos101_server_battery/config"

You are right, HA expects a state_topic but my understanding is, state_topic contains all informations you send to the topic.
So in our case, state_topic is

{
  "state": "pending-charge",
  "time_to_empty": "unknown",
  "percentage": 67
}

I just tested your code as is and I got the same error message you got

then I added “/” as I mentioned before and now the error message disapeared

1 Like

Will apply the change and get back to you, but I have no reason to doubt your reply. You, sir, are an asset to this community!

1 Like

No luck unfortunately :sweat:
Added the leading slash so that topic is topic="/homeassistant/sensor/haos101_server_battery/config"
and still getting the same error.

Added the leading slash to MQTT yaml too, but that just results in an unavailable sensor.
Weird thing I noticed is that now if I run the python script manually, it prints out “Connected to MQTT Broker!” every second or so. I’m quite sure it only printed that message once up till yesterday

/homeassistant is probably wrong, and is different to homeassistant. MQTT topics generally do not have a leading /.