EOS & Cinnamon, an endeavour in itself (CUPS, Nextcloud, Home Assistant)

Just in case anyone else uses Home Assistant to track their laptop’s battery state: I’ve updated the script above, because Python’s psutil couldn’t detect batteries on some oddball tablets/convertibles/notebooks. Assuming we run Linux on those, I added a fallback to use upower in those cases.

#!/usr/bin/env python3
# encoding: utf-8

# ha-battery-sensor
# 2025-03-24 Moonbase59
# 2025-05-16 Moonbase59 - improve battery detection
# 2025-05-17 Moonbase59 - add upower check if psutil fails

from requests import post
import psutil
import json
import socket
import subprocess

# get hostname as device name
device_name = socket.gethostname().split('.')[0]
device_slug = socket.gethostname().split('.')[0].replace('-', '_').casefold()

# Home Assistant URL (use remote URL if it should work outside your local network)
# ha_url = 'http://homeassistant.local:8123'
# ha_url = 'https://homeassistant.example.com'
ha_url = 'https://homeassistant.example.com'

# Home Assistant long-lived accessed token (create one first!)
ha_token = 'very_long_homeassistant_token'

# Create sensor and friendly names
ha_sensor = 'sensor.' + device_slug + '_battery_level'
ha_friendly_name = device_name + ' Battery level'

url = ha_url + '/api/states/' + ha_sensor
headers = {
    'Authorization': 'Bearer ' + ha_token,
    'content-type': 'application/json',
}

# Try psutil first
if hasattr(psutil, "sensors_battery"):
    try:
        percent = psutil.sensors_battery().percent
        charging = psutil.sensors_battery().power_plugged
    except AttributeError:
        battery = False
    else:
        battery = True
else:
    battery = False

# If psutil failed, see if we can get data from upower (if installed)
# This will probably only work on Linux

# shell commands to use for upower (returns 'unknown' if upower isn't installed)
upower_cmd_percent = "command -v upower >/dev/null 2>&1 && upower -i $(upower -e | grep '/battery') | grep --color=never -E percentage|xargs|cut -d' ' -f2|sed s/%// || echo 'unknown'"
upower_cmd_state = "command -v upower >/dev/null 2>&1 && upower -i $(upower -e | grep '/battery') | grep --color=never -E state|xargs|cut -d' ' -f2 || echo 'unknown'"

if battery == False:
    try:
        percent = subprocess.check_output(upower_cmd_percent, shell=True, text=True)
        percent = float(percent.strip())
        state = subprocess.check_output(upower_cmd_state, shell=True, text=True)
        state = state.strip()
        charging = True if state == 'charging' else False
    except:
        battery = False
    else:
        battery = True

# prepare payload
if battery:
    percent = round(percent, 0)
    icon = 'mdi:battery'
    icon0 = 'mdi:battery-outline'
    icon100 = 'mdi:battery'
    if charging:
        icon = "mdi:battery-charging"
        icon0 = "mdi:battery-charging-outline"
        icon100 = "mdi:battery-charging-100"
    step = round(percent / 10) * 10
    if step < 10:
        icon = icon0
    elif step > 90:
        icon = icon100
    else:
        icon = icon + '-' + str(step)
else:
    icon = 'mdi:battery-unknown'
    percent = 'unknown'

payload = {
    'state': str(percent),
    'attributes': {
        'device_class': 'battery',
        'state_class': 'measurement',
        'unit_of_measurement': '%',
        'friendly_name': ha_friendly_name,
        'icon': icon
    }
}

# print(json.dumps(payload))
response = post(url, headers=headers, data = json.dumps(payload))
# print(response.text)

Let me know if it worked for you, or what you had to change. :slight_smile: