Home Assistant Printer Power Management

Home Assistant Printer Power Management

I’ve got an old HP laser printer in my basement. We barely print 10 pages a month between the two of us, so we only turn it on when we’re going to print. That’s a hassle though, because inevitably we forget to shut it off sometimes and it stays on overnight or even for days, and while it has a powersave mode, the 4050N is so old that even that burns a good amount of power.

Enter Home Assistant.

Prerequisites

You have HA configured to connect to a MQTT server

The watcher script and associated tooling all presume that we can send messages to a MQTT topic that HA is watching.

Your printer is connected to a cupsd server running in a container

Your computers should be configured to print to the cupsd server instead of directly to the printer.

I run cupsd in a container on one of my Odroids. I could run it on the same Odroid HC2 that I run Home Assistant (HA) on, but there’s no compelling reason to do so and I’m reserving that node for strictly HA containers like Home Assistant itself and my MQTT server. I picked an Odroid because it has a SATA drive attached and my /var/lib/docker is on the hard drive and not an microSD card - there’s no reason you can’t run it on a Raspberry Pi other than to prevent excessive wear on the microSD card.

You could modify the watcher script if you’re running cupsd directly instead of in a container, but I run my cupsd in a container, so that’s what the script is designed for.

There are plenty of articles about setting up cupsd, but I wrote about setting up cupsd here.

Your printer is plugged into an outlet controlled by HA

We want to be able to toggle the power from Home Assistant.

Printer Power Control

mosquitto helper script

I don’t like to install anything more on my docker hosts than I absolutely have to, so instead of installing mosquitto directly on the printserver machine, I run mosquitto_pub inside a container with the following c-mosquitto_pub helper script. You can download it from github here. Put this in /usr/local/bin.

#!/usr/bin/env bash
#
# Use docker to run mosquitto_pub
#
# Copyright 2019, Joe Block <jpb@unixorn.net>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

exec docker run -t --rm eclipse-mosquitto mosquitto_pub $@

cupsd Watcher

Once I had cupsd configured to share the printer (as Franklin), I wrote a quick script that checks the print queue to see if it is empty or not. If there are jobs in the queue, it writes ON to an MQTT topic, hass/printers/franklin. If the queue is empty, it writes OFF. The examples here all assume your printer is named Franklin, replace Franklin with your printer’s name.

Actually, I lied. When there are jobs, it writes OFF and then ON.

Why? Because I don’t want HA to switch the printer off immediately once the queue drains - the printer has enough RAM that there may still be several pages left to print when it has accepted all of the job from the server.

Instead, I’ve configured HA to restart a timer every time it sees the MQTT topic hass/printers/franklin switch from OFF to ON, and only turn the printer off after the queue has been empty for five continuous minutes.

Here’s the ha-check-for-print-jobs script source - you can download it from github here.

Put the script in /usr/local/bin on the same server you’re running the cupsd container on - it is designed to run a tool inside that container.

#!/usr/bin/env bash
#
# ha-check-for-print-jobs
#
# Check if there are print jobs on $PRINT_Q. If there are, write
# MQTT messages to a watched topic so HA knows to turn on the power
# to the printer.
#
# Copyright 2021, Joe Block <jpb@unixorn.net>
#
# License: Apache 2

set -o pipefail

# Make all these overridable easily in your cron setup
PRINT_Q=${PRINT_Q:-'Franklin'}
CONTAINER=${CONTAINER:-'cupsd-server'}
MQTT_HOST=${MQTT_HOST:-'mqtt.example.com'}
MQTT_TOPIC=${MQTT_TOPIC:-'hass/printers/franklin'}

# We are run out of cron every minute, but I don't want it to take an
# entire minute to turn on the power because I'm impatient and the printer
# takes a bit to start up. When we print and walk downstairs, I want it
# to have already started printing by the time I get there. If I was
# patient, I wouldn't have bothered to write this tool :-)
#
# So, when we get run by cron, we check the queue CHECK_COUNT times, with
# CHECK_DELAY seconds between each run.
CHECK_COUNT=${CHECK_COUNT:-'11'}
CHECK_DELAY=${CHECK_DELAY:-'5'}

export PATH="$PATH:/usr/local/bin:/usr/local/sbin"

if [[ -f /tmp/printerdebug ]]; then
  DEBUG='true'
fi

# Only spam syslog when DEBUG is set
debugout() {
  if [[ -n "$DEBUG" ]]; then
    echo "$@"
  fi
}

validate-settings(){
  debugout "CONTAINER: $CONTAINER"
  debugout "PRINT_Q: $PRINT_Q"
  debugout "MQTT_HOST: $MQTT_HOST"
  debugout "MQTT_TOPIC: $MQTT_TOPIC"

  valid='true'
  if [[ -z "$CONTAINER" ]]; then
    echo "CONTAINER is unset"
    valid='false'
  fi
  if [[ -z "$PRINT_Q" ]]; then
    echo "PRINT_Q is unset"
    valid='false'
  fi
  if [[ -z "$MQTT_HOST" ]]; then
    echo "MQTT_HOST is unset"
    valid='false'
  fi
  if [[ -z "$MQTT_TOPIC" ]]; then
    echo "MQTT_TOPIC is unset"
    valid='false'
  fi
  if [[ "$valid" == "false" ]]; then
    echo "Configure your settings."
    exit 1
  fi
}

print-job-checker() {
  printjobs=$(docker exec -t "$CONTAINER" lpq -P "$PRINT_Q" | grep -c 'no entries')

  if [[ "$printjobs" == '1' ]]; then
    debugout "No jobs in print queue, notifying HA"
    c-mosquitto_pub -h "$MQTT_HOST" -t "$MQTT_TOPIC" -m OFF
  else
    echo "jobs found in print queue, notifying HA"

    docker exec -t "$CONTAINER" lpq -P "$PRINT_Q"

    # Set the status off, then back to on, so that the HA timer restarts
    # and HA doesn't turn off the printer in the middle of a job
    c-mosquitto_pub -h "$MQTT_HOST" -t "$MQTT_TOPIC" -m OFF
    c-mosquitto_pub -h "$MQTT_HOST" -t "$MQTT_TOPIC" -m ON
    debugout "re-enabling printer $PRINT_Q..."
    docker exec -t "$CONTAINER" lpadmin -p "$PRINT_Q" -o printer-error-policy=retry-current-job
  fi
}

validate-settings

# We run the print-job-checker every 5 seconds to minimize the UI delay on the
# macOs end

for i in $(seq $CHECK_COUNT)
do
  print-job-checker
  debugout "waiting..."
  sleep $CHECK_DELAY
done

Home Assistant Setup

I configured my HA to watch a MQTT topic as a binary sensor. You can download this snippet here.

binary_sensor:
  - platform: mqtt
    name: "Franklin Print Queue"
    payload_on: "ON"
    state_topic: "hass/printers/franklin"

Now, when the watcher writes ON and OFF to the hass/printers/franklin queue, that binary sensor will change status and we can trigger an automation for it.

This automation will turn the printer power on every time the binary sensor is turned on, and turn it off five minutes after the last time the binary sensor switched from ON to OFF.

The outlet my printer is plugged into is controlled by HA and rather unimaginatively named switch.printerpower.

Add this stanza to your automations.yaml file. Download it here.

# Franklin power is controlled by MQTT
  - alias: 'Turn on Franklin when there are jobs in the queue'
    trigger:
      platform: state
      entity_id: binary_sensor.franklin_print_queue
      to: 'on'
    action:
      service: homeassistant.turn_on
      entity_id: switch.printerpower

  - alias: 'Turn off printer 5 minutes after print queue drains'
    trigger:
      platform: state
      entity_id: binary_sensor.franklin_print_queue
      to: 'off'
      for:
        minutes: 5
    action:
      service: homeassistant.turn_off
      entity_id: switch.printerpower

Test the pieces

Confirm that you’ve got the print queue configured correctly by running docker exec -it cupsd-server lpq -P Franklin. If there are no jobs, it should print something like

Franklin is ready
no entries

Automation test

Reload your automations, and you should now be able to test that the automations are correct by running c-mosquitto_pub -h mqtt.yourdomain.com -t hass/printers/franklin -m OFF or -m ON and watch HA turn the power to your printer off and on.

Once that is working, print a job, and if you run ha-check-for-print-jobs the printer power should get turned on.

Run it all automatically

Now that you’ve confirmed that the power is being cycled properly when the MQTT queue recieves messages and that the print job checker is seeing the printer queue, we can add the checker job to cron.

Add

* * * * * PRINT_Q=Franklin MQTT_HOST=mqtt.example.com MQTT_TOPIC=hass/printers/franklin CONTAINER=cupsd_server /usr/local/bin/ha-check-for-print-jobs | logger -t printserver

to your /etc/crontab, and you’re good to go. Now every minute, the checker script will get run by cron, and it will check every five seconds for print jobs and exit before the next invocation by cron.

Buy me a coffeeBuy me a coffee