Project

General

Profile

Powersave, auto shutdown and auto startup PLUS LCD status display

Added by Daniel Ellis about 8 years ago

Extending on from Dutchy Nl's scripts at https://tvheadend.org/boards/4/topics/15826

This new set of scripts includes the ability to display the shutdown status on the LCD front panel. To achieve this in an elegant way that did not continually cause the LCD to flicker, I had to re-write Dutchy's bash scripts using python.

This set of scripts includes three components:

1. The automatic shutdown script - written in python, this monitors TV Headend via the HTTP status api to determine when the system is busy. Just as Dutchy's script does, it also monitors for active processes, logged in users, active network connections, recent power on etc.

2. Automatic wake up - A simple bash script which sets the RTC wakeup timer whenever the system is put to sleep or shutdown. This method does not rely on the shutdown script, so no matter how you shutdown the machine, it will still wake up for the next recording.

3. Shutdown block - Ability to block accidental shutdown via the front power button when the system is busy.

Automatic shutdown script

On my system this file is located at /home/hts/auto_shutdown.py

Make sure the file is executable using:
sudo chmod +x auto_shutdown.py

#!/usr/bin/env python

# Automatic shutdown script for TV headend.
#
# Author: Daniel Ellis <[email protected]>
# License: No restrictions, free for all to do as they wish.
#
# Inspired by the shell script by "Dutchy Nl" at https://tvheadend.org/boards/4/topics/15826 Thanks Dutchy!
#
# Tested against TV headend 4.0.9 on Linux Mint 17.3
#
# DEPENDENCIES
# 
# lcdproc
# requests
# json
# psutil
# 
# How to install dependencies (tested on Linux Mint 17.3)
# 
# sudo apt-get install python-setuptools python-psutil
# sudo easy_install lcdproc requests
#
# PYTHON 3 COMPATIBILITY
# The lcdproc dependency is not yet compatible with python3 due to this issue
# https://github.com/jinglemansweep/lcdproc/issues/7
#

######### CUSTOMIZABLE VARIABLES ########

# TV Headend login credentials
tvh_login="admin" 
tvh_password="" 

# The machine name or IP address of you TV headend server
tvh_server="" 

# Your local domain, which is just used to trim local machine names, so only hostnames are displayed
local_domain="" 

# Idle time before the system shuts down
idle_time_shutdown=300 # [in seconds, default 300 (5 min)]

# Minimum time to stay on after machine powered on
min_uptime=900 # [in seconds, default 900 (15 min)]

# Interval that the status is updated
interval=60 # [in seconds, default 60]

# The system will stay on if a recording is scheduled to start shortly
recording_safe_margin=600 # [in seconds, default 600 (10 minutes)]

# Processes to check so to block shutdown if they are running
process_list="vi,ping" 

# Network ports to check for active connections
# (TVHeadend 9981 + 9982, Samba 445, SSH 22)
ports=[9981,9982,445,22]

# Variables less likely to need changing
tvh_subscription_url="http://"+tvh_server+":9981/api/status/subscriptions" 
tvh_status_url="http://"+tvh_server+":9981/status.xml" 
busy_file="/tmp/block_shutdown" 
##### END OF CUSTOMIZABLE VARIABLES #####

import time
import requests
import json
import socket
from lcdproc.server import Server
import os
import subprocess
import string
import psutil

##### Global variables
line1 = 0   # ID of the LCD widget for line1
line2 = 0   # ID of the LCD widget for line2

shutdown_seconds=idle_time_shutdown

##### Core application logic

def main():
    global shutdown_seconds

    init_lcd()

    while shutdown_seconds > 0:
        status = Status.NotBusy

        if (status == Status.NotBusy):
            status = check_hts_status()

        if (status == Status.NotBusy):
            status = check_process_status()

        if (status == Status.NotBusy):
            status = check_logged_in_users()

        if (status == Status.NotBusy):
            status = check_network_connections()

        if (status == Status.NotBusy):
            status = check_upcoming_recordings()

        if (status == Status.NotBusy):
            status = check_recent_poweron()

        if (status == Status.Busy):
            print("Busy")
            shutdown_seconds=idle_time_shutdown
        elif (status == Status.BusyButAllowManualShutdown):
            print("Busy but allow manual shutdown")
            shutdown_seconds=idle_time_shutdown
        elif (status == Status.NotBusy):
            shutdown_seconds -= interval 
            print("Not busy, shutdown in " + str(shutdown_seconds) + " seconds")
            shutdown_mins = (shutdown_seconds / 60) + 1 # add 1 to round up
            display("Shutdown in", str(shutdown_mins) + " minutes")

        else:
            print("Unexpected status: " + status)

        # If the system is busy, write a file to the file system which
        # can act as a marker to block manual shutdown of the system.
        # If the system is not busy, remove the file.
        if (status == Status.Busy):
            touch(busy_file)
        else:
            try:
                os.remove(busy_file)
            except OSError:
                pass

        time.sleep(interval)

    # End of the while loop, ready to shutdown
    print "Shutdown" 
    display("Shutting down...", "")
    subprocess.call(["sudo", "poweroff"])

def init_lcd():
    global line1
    global line2

    try:
        lcd = Server("localhost", debug=False)
        lcd.start_session()
        screen = lcd.add_screen("s1")
        screen.set_priority("background")
        line1 = screen.add_scroller_widget("Line1", text="", speed=1, top=1, direction="h")
        line2 = screen.add_scroller_widget("Line2", text="", speed=1, top=2, direction="h")
    except:
        print "Failed to initialize LCD display" 

def check_hts_status():
    r = requests.get(tvh_subscription_url, auth=(tvh_login, tvh_password))
    r.raise_for_status()    
    print(r.text)
    print("")
    json = r.json()
    count = json['totalCount']
    print("TVH service count=" + str(count))
    entries = json['entries']

    # First scan for recordings
    for entry in entries:
        title = entry['title']
        if (title.startswith("DVR: ")):
            title = title[5:] # Remove the DVR: prefix and keep just the title
            display("Recording", title)
            return Status.Busy

    # Next scan for any hosts watching TV
    for entry in entries:
        ip = entry.get('hostname', '')
        if (ip):
            print("TV in use by " + ip)
            hostname = ip_to_hostname(ip)
            display("TV in use by", hostname)
            return Status.Busy

    # Uncomment the following section if you wish shutdown to be blocked when EPG is being downloaded
    #for entry in entries:
    #    title = entry['title']
    #    if (title == 'epggrab'):
    #        display("EPG download...", "")
    #        return Status.BusyButAllowManualShutdown

    # Uncomment the following section if you want to block shutdown for any other HTS activity
    # I'm not aware of any others, so this is just for debugging.
    #for entry in entries:
    #    title = entry['title']
    #    display(title, "")
    #    return Status.BusyButAllowManualShutdown

    return Status.NotBusy

### Check if any processes are running
def check_process_status():
    # The command we use will look something like
    # ps ch -o cmd -C <cmdlist>
    #
    # Where the arguments are:-
    # c             Use the short version of the command line
    #               i.e without the args
    # h             Dont show any headers
    # -o cmd        Only list the command
    # -C <cmdlist>  Limit the list to these programs (comma separated)
    pl = subprocess.Popen(['ps', 'ch', '-o', 'cmd', '-C',process_list], stdout=subprocess.PIPE).communicate()[0]
    lines = pl.splitlines()
    process_count = len(lines)
    print "Proccess count=" + str(process_count)
    if (process_count > 0):
        print "Processes running: " + string.join(lines, ',')
        display(str(process_count) + " processes", "still running")
        return Status.Busy

    return Status.NotBusy

def check_logged_in_users():
    users = psutil.get_users()
    unique = {}
    for user in users:
        unique[user.name] = 1
    user_string = string.join(unique.keys(), ',')
    print "Logged in users: " + user_string
    display("Logged in", user_string)
    if (len(unique) > 0):
        return Status.Busy

    return Status.NotBusy

def check_recent_poweron():
    with open('/proc/uptime', 'r') as f:
        uptime_seconds = int(float(f.readline().split()[0]))

    uptime_minutes = uptime_seconds / 60
    print("Uptime=" + str(uptime_minutes) + " minutes")

    if (uptime_seconds < min_uptime):
        shutdown_seconds = min_uptime - uptime_seconds
        shutdown_mins = (shutdown_seconds / 60) + 1
        display("Shutdown in", str(shutdown_mins) + " minutes")
        return Status.BusyButAllowManualShutdown

    return Status.NotBusy

# psutil from 2.1.0 onwards can obtain the network connections using:-
#  psutil.net_connections()
# Without the newer version, we will have to use the proc filesystem directly
def check_network_connections():
    hosts = []
    with open('/proc/net/tcp', 'r') as f:
        for line in f:
            # Split lines and remove empty spaces.
            line_array = _remove_empty(line.split(' '))
            state = line_array[3]
            if (state == '01'): # ESTABLISHED
                # Convert ipaddress and port from hex to decimal.
                l_host,l_port = _convert_ip_port(line_array[1])
                if int(l_port) in ports:
                    r_host,r_port = _convert_ip_port(line_array[2])
                    print "Port " + l_port + " in use by " + r_host
                    hosts.append(r_host)

    # Ping the hosts to see if they are alive
    for host in hosts:
        print "Pinging " + host
        status = subprocess.call("ping -c1 -w1 " + host, shell=True)
        if (status == 0):
            print host + " is alive" 
            hostname = ip_to_hostname(host)
            display("Connection from", hostname)
            return Status.Busy
        else:
            print host + " is not alive" 

    return Status.NotBusy

def check_upcoming_recordings():
    r = requests.get(tvh_status_url, auth=(tvh_login, tvh_password))
    r.raise_for_status()    
    for line in r.text.splitlines():
        if (line.startswith("<next>") and
            line.endswith("</next>")):
            next_recording_minutes=int(line[6:-7])
            print ("Next recording in " +
                str(next_recording_minutes) + " minutes")
            next_recording_seconds=next_recording_minutes*60
            if (next_recording_seconds < recording_safe_margin):
                display("Next recording in", str(next_recording_minutes) + " minutes")
                return Status.Busy

    return Status.NotBusy

##### Utility classes and functions

class Status:
    NotBusy, Busy, BusyButAllowManualShutdown = range(3)

def touch(fname, times=None):
    with open(fname, 'a'):
        os.utime(fname, times)

def ip_to_hostname(ip):
    try:
        triple = socket.gethostbyaddr(ip)
    except socket.herror:
        return ip

    hostname = triple[0]

    # Remove the local domain
    tail = '.' + local_domain
    if hostname.endswith(tail):
        hostname = hostname[:-len(tail)]

    return hostname

def display(l1,l2):
    if isinstance(l1, unicode):
        l1 = l1.encode()
    if isinstance(l2, unicode):
        l2 = l2.encode()

    print("Display: " + l1)
    print("Display: " + l2) 

    # if the lcd is not initialized, then try now
    if line1 == 0:
        init_lcd()

    # if it is still not initialized, then continue without it
    if line1 != 0:
        line1.set_text(l1)
        line2.set_text(l2)

def _remove_empty(array):
    return [x for x in array if x !='']

def _hex2dec(s):
    return str(int(s,16))

def _convert_ip_port(array):
    host,port = array.split(':')
    return _ip(host),_hex2dec(port)

def _ip(s):
    ip = [(_hex2dec(s[6:8])),(_hex2dec(s[4:6])),(_hex2dec(s[2:4])),(_hex2dec(s[0:2]))]
    return '.'.join(ip)

# Run
if __name__ == "__main__":
    main()

# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

Automatic wake up

Add the following to the file /etc/pm/sleep.d/95_waketimer.sh

Make it executable using:
sudo chmod +x /etc/pm/sleep.d/95_waketimer.sh

Duplicate (link) this file, so it also runs on shutdown
sudo ln -s /etc/pm/sleep.d/95_waketimer.sh /etc/rc0.d/K05waketimer

#!/bin/bash
#
# set ACPI Wakeup alarm
# safe_margin - minutes to start up system before the earliest timer
# script does not check if recording is in progress
#
#

echo 1 > /timer

# bootup system 60 sec. before timer
safe_margin=60

# modyfy if different location for tvheadend dvr/log path
cd ~hts/.hts/tvheadend/dvr/log

######################

start_date=0
stop_date=0

current_date=`date +%s`

for i in $( ls ); do
tmp_start=`cat $i | grep '"start":' | cut -f 2 -d " " | cut -f 1 -d ","`
tmp_stop=`cat $i | grep '"stop":' | cut -f 2 -d " " | cut -f 1 -d ","`

# check for outdated timer
if [ $((tmp_stop)) -gt $((current_date)) -a $((tmp_start)) -gt $((current_date)) ]; then

# take lower value (tmp_start or start_date)
if [ $((start_date)) -eq 0 -o $((tmp_start)) -lt $((start_date)) ]; then
start_date=$tmp_start
stop_date=$tmp_stop
fi
fi
done

wake_date=$((start_date-safe_margin))

echo $start_date >> /timer
echo $wake_date >> /timer

# set up waleup alarm
if [ $((start_date)) -ne 0 ]; then
echo 2 >> /timer
#echo "Wake at $wake_date" | /usr/local/bin/lcdproc_client.py -f -
echo 0 > /sys/class/rtc/rtc0/wakealarm
echo $wake_date > /sys/class/rtc/rtc0/wakealarm
fi

Local shutdown block

To block accidental local shutdown when the system is busy. Edit the file located at /etc/acpi/powerbtn.sh

Add the following to the top of the file:-

# Custom power button handling
if [ -f /tmp/block_shutdown ]; then
  echo "SYSTEM IS BUSY" | /usr/local/bin/lcdproc_client.py -f -
else
  echo "Powering off..." | /usr/local/bin/lcdproc_client.py -t 2 -f -
  /sbin/shutdown -h now "Power button pressed" 
fi
exit