I’m on Linux and I like the idea of @martokk to use xprintidle
, but I think it would be better to let the computer report to homeassistant using a webhook whenever the state changes, instead of letting homeassistant poll the computer every X seconds. I solved it using a script that runs in the background which connects with a trigger based template sensor.
In home assistant I added the following to config/configuration.yml
:
template:
- trigger:
- platform: webhook
webhook_id: eaea48a1-30e3-47bf-a076-30f816f0d3d1
binary_sensor:
- name: "Computer in use"
icon: "mdi:desktop-tower"
unique_id: computer_in_use
state: "{{ trigger.json.in_use }}"
# The script reports the state every 5 mins. Set a timeout of 11 mins,
# so that when 2 pings are missed the computer is considered idle.
auto_off: 660
For the webhook ID I used a random UUID. You can generate your own here: uuid at DuckDuckGo
On the computer I created a script that will keep track of whether the computer is in use in a file stored in memory, and will update home assistant when the state changes. It resends the state every 5 minutes to account for missed updates. I had a few instances where the “off” state was not received by Home Assistant for some reason (e.g. laptop disconnected from wifi), and my lights stayed on all night. If Home Assistant doesn’t get pinged for 11 minutes it considers the computer to be off.
The script runs in a tight loop of 200 milliseconds, so it updates Home Assistant pretty much instantly when the computer is being used. I use this quick response because I am impatient, I want my lights on immediately when I touch the mouse, not with a 1 second delay
Because the state is kept in memory the resource usage is negligible.
Script is in $HOME/.local/scripts/hass-report-usage.sh
:
#!/bin/bash
# Abort if another instance is already running.
if [ -e /dev/shm/in_use.pid ]; then
read PID < /dev/shm/in_use.pid
if kill -0 $PID > /dev/null 2>&1; then
echo >&2 "already running. aborting."
exit 1
fi
fi
trap "rm /dev/shm/in_use; curl --header \"Content-Type: application/json\" --request POST --data '{\"in_use\": 'false'}' http://homeassistant.lan:8123/api/webhook/eaea48a1-30e3-47bf-a076-30f816f0d3d1" EXIT
echo $$ > /dev/shm/in_use.pid
# Initially set the status to idle.
echo "false" > /dev/shm/in_use
# Store a timestamp 10 minutes in the past, to trigger an immediate update.
echo $(( `date +%s%3N` - 600000)) > /dev/shm/in_use.timestamp
# Consider computer idle after 5 minutes.
TIMEOUT=300000
while true; do
if [ `xprintidle` -lt $TIMEOUT ]; then
IN_USE=true
else
IN_USE=false
fi
# Report every 5 minutes.
if [ `date +%s%3N` -gt $((`cat /dev/shm/in_use.timestamp` + $TIMEOUT)) ]; then
REPORT=true
else
REPORT=false
fi
if [ `cat /dev/shm/in_use` != $IN_USE ]; then
CHANGED=true
else
CHANGED=false
fi
if $REPORT || $CHANGED; then
echo $IN_USE > /dev/shm/in_use
echo `date +%s%3N` > /dev/shm/in_use.timestamp
curl --header "Content-Type: application/json" --request POST --data '{"in_use": '$IN_USE'}' http://homeassistant.lan:8123/api/webhook/eaea48a1-30e3-47bf-a076-30f816f0d3d1
fi
sleep 0.2
done
Make the script executable:
$ chmod u+x ~/.local/scripts/hass-report-usage.sh
I trigger the script when X is started by adding the following line to ~/.xinitrc
, somewhere before the final line which starts the window manager:
$HOME/.local/scripts/hass-report-usage.sh &
Laptops
If you are on a laptop you might want to create some additional triggers to report the machine as idle when it suspends/hibernates and back as active when it resumes.
Going to sleep
When the computer goes to suspend/hibernation/sleep, then what works well is to just send the HTTP request to report the computer as idle, without changing the flag in /dev/shm/in_use
. I found this works more reliably, since several processes might run simultaneously when sleep mode is initiated. If the script is still active in the background it might toggle the status back to “active” a split second after being set to “idle”.
Execute this command when suspending:
curl --header "Content-Type: application/json" --request POST --data '{"in_use": 'false'}' http://homeassistant.lan:8123/api/webhook/eaea48a1-30e3-47bf-a076-30f816f0d3d1
Resuming
When resuming from sleep/hibernation a good tactic is to toggle the flag. When the script resumes its operations it will read the flag and send out the “active” state immediately.
Execute this command when resuming:
echo 'false' > /dev/shm/in_use
Example systemd service for suspend/resume
Save this in /etc/systemd/system/[email protected]
. Enable it with sudo systemctl enable hass-suspend@myusername
Edit: this is not 100% reliable, if anyone finds a robust way of detecting suspend/resume please let me know.
[Unit]
Description=Inform Home Assistant that the computer is idle on suspend.
Before=sleep.target
StopWhenUnneeded=yes
[Service]
User=%I
Type=oneshot
RemainAfterExit=yes
Environment=DISPLAY=:0
ExecStart=curl --header "Content-Type: application/json" --request POST --data '{"in_use": 'false'}' http://homeassistant.lan:8123/api/webhook/eaea48a1-30e3-47bf-a076-30f816f0d3d1
ExecStop=bash -c 'sleep 1 && echo false > /dev/shm/in_use'
[Install]
WantedBy=sleep.target
Earlier attempts
I first tried to use a systemd timer as well as a oneshot service but this was not reliable since after a reboot the X session is not always available at the moment the service is initiated. This simple bash script with a 200 millisecond sleep works well.
edits:
- Change from a systemd timer to a startup script.
- Abort if another instance is running.
- Report the computer as being idle when the script is terminated (e.g. when powering off / rebooting).
- Add example systemd service for reporting usage status on suspend/resume.
- Change to a binary sensor.
- Update to new template trigger configuration format. Introduce a 6 minute timeout.