Forums » Tutorial and setups »
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 shutdownsudo 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