Monday, May 4, 2015

Home entertainment system monitoring and auto switch off

I have few devices which are creating my home entertainment system:

  • NEC monitor (sometimes good monitor is better then TV)
  • Yamaha HTR-4065 
  • PC Zotac
  • Evolveo DaulCoder DVB-T
  • XBOX 360

All those are connected to network via ethernet and there is some way to monitor or even to remotely control, lets start one by one:

DVB-T and PC

As you might know one of the easiest way to check if device is up or down would be to ping it, however in case of audio/video systems it's bit hard because if they are in Standby mode they will answer to the ping.
In some cases I don't even give the device permanent IP(DVB-T and PC). However there is the way to discover them via UPnP.

There is linux utility gssdp-discovery which use ssdp to discover UPnP capable device and as far as each device have unique identificator uuid we can check if device is running by checking if device is responding to ssdp discovery.

Below is small script which check based on uuid if device is running, which IP it was assigned and on which port UPnP is listening and record this to sqlite database.

#! /bin/bash

#test if device can be discovered using SSDP
#IMPORTANT return true if device is down
testUPnP()
{
  return `gssdp-discover -n 10 -t uuid:$1 | grep "resource available" | wc -l`
}

HFILE='/var/www/monitoring/device_upnp.list'
DBFILE='/var/www/judo.db'

if [[ -z $HFILE ]]
then
  echo "$HFILE doesn't exist"
  exit 1
fi

while read line
do
  uuid=$(echo "$line" | cut -d, -f5)
  mac=$(echo "$line" | cut -d, -f4)
  new_polls=1
  prev_status_polls=$(sqlite3 $DBFILE "SELECT cur_state,polls_in_state FROM dev_status WHERE mac = '$mac';")
  prev_status=$(echo "$prev_status_polls" | cut -d\| -f1)
  prev_polls=$(echo "$prev_status_polls" | cut -d\| -f2)

  #remeber testUPnP returns true if device is down
  if `testUPnP $uuid`
  then
    new_status="Down"
    if [ "$prev_status" == "Up" ]
    then
      sqlite3 $DBFILE "UPDATE devices SET ip = NULL, upnp_port = NULL WHERE mac = '$mac';"
    fi
  else
    new_status="Up"
    if [ "$prev_status" == "Down" ]
    then
      location=`gssdp-discover -n 10 -t uuid:$uuid | grep "Location:" | sed 's/ *Location: http:\/\///g'| cut -d/ -f1 | sed 's/\///g'`
      echo "$location"
      ip=$(echo "$location" | cut -d: -f1)
      upnp_port=$(echo "$location" | cut -d: -f2)
      echo "$ip:$upnp_port"
      sqlite3 $DBFILE "UPDATE devices SET ip = '$ip', upnp_port = '$upnp_port' WHERE mac = '$mac';"
    fi
  fi

  if [ "$prev_status" == "$new_status" ]
  then
    new_polls=$((prev_polls + 1))
  fi
  sqlite3 $DBFILE "UPDATE dev_status SET cur_state = '$new_status', polls_in_state = '$new_polls' WHERE mac = '$mac';"
  echo "$uuid,$mac,$new_status,$prev_status,$new_polls"
done < ${HFILE}

Yamaha HTR-4065


Even though Yamaha have UPnP we will rather use YNC to get the status, because we also want to know which input is actually active. Below is python script which finds the status and input and store that in sqlite database.
#! /usr/bin/python

import sqlite3
import sys
import httplib, urllib
import re
from xml.dom import minidom
from xml.parsers.expat import ExpatError

conn = httplib.HTTPConnection("10.0.0.36:80")
xml_req_power = '<?xml version="1.0" encoding="utf-8"?><YAMAHA_AV cmd="GET"><Main_Zone><Power_Control><Power>GetParam</Power></Power_Control></Main_Zone></YAMAHA_AV>'

xml_req_input = '<?xml version="1.0" encoding="utf-8"?><YAMAHA_AV cmd="GET"><Main_Zone><Input><Input_Sel>GetParam</Input_Sel></Input></Main_Zone></YAMAHA_AV>'

params = urllib.urlencode({'q': 'set'})
headers = { 'Content-Type': 'application/xml', "Content-Length": "%d" % len(xml_req_power)}

conn.request("POST", "/YamahaRemoteControl/ctrl", "", headers)
conn.send(xml_req_power)

xmldoc = minidom.parseString(conn.getresponse().read())
xml_input_element = xmldoc.getElementsByTagName('Power')
#print xml_input_element[0]
for node in xml_input_element:
  power_status = node.firstChild.nodeValue

conn.close()

#=================================
#Geting input

headers = { 'Content-Type': 'application/xml', "Content-Length": "%d" % len(xml_req_input)}

conn.request("POST", "/YamahaRemoteControl/ctrl", "", headers)
conn.send(xml_req_input)
#response = conn.getresponse()

#print response.status, response.reason
#response_data = response.read()

#print response_data

xmldoc = minidom.parse(conn.getresponse())
xml_input_element = xmldoc.getElementsByTagName('Input_Sel')
for node in xml_input_element:
  av_input = node.firstChild.nodeValue

conn.close()

print "Status\t\tAV Input"
print power_status + "\t\t" + av_input

select_query = "SELECT cur_state,polls_in_state,input FROM dev_status WHERE mac = '00:a0:de:92:6b:78';"
#print update_query
new_polls=1
judodb = sqlite3.connect('/var/www/judo.db')
cur = judodb.cursor()
rows = cur.execute(select_query)
for row in rows:
  prev_state = row[0]
  prev_polls = row[1]
  prev_input = row[2]
if prev_state == power_status and prev_input == av_input:
  new_polls = prev_polls + 1
print prev_state + "\t\t" + str(new_polls)
update_query = "UPDATE dev_status SET input = \'" + av_input + "\', cur_state = \'" + power_status + "\', polls_in_state = " + str(new_polls)  + " WHERE mac = '00:a0:de:92:6b:78'"
cur.execute(update_query)
judodb.commit()
judodb.close()

NEC

NEC responds to ping in Standby and of course don't have UPnP, however the NEC remote control over ethernet let us find out the actual status.
Below python script check the status and store it in sqlite database.
#! /usr/bin/python

import sys
import socket
import sqlite3

monitor_ip = '10.0.0.35'
port = 7142
buffer_size = 2048
data_on = '\x01\x30\x41\x30\x41\x30\x36\x02\x30\x31\x44\x36\x03\x74\x0d'


new = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
new.connect((monitor_ip, port))
new.send(data_on)

data = ''

#message is split over several packets so need to get all content, message we expect have 25bytes
packet = new.recv(buffer_size)
data += packet
while len(data) < 25 or not packet:
   packet = new.recv(buffer_size)
   data += packet

new.close()

#Reponse OFF
#00AB120200D60000040004q
#Response ON
#00AB120200D60000040001t

off_response = b'\x30\x30\x41\x42\x31\x32\x30\x32\x30\x30\x44\x36\x30\x30\x30\x30\x30\x34\x30\x30\x30\x34'
on_response = bytes("\x30\x30\x41\x42\x31\x32\x30\x32\x30\x30\x44\x36\x30\x30\x30\x30\x30\x34\x30\x30\x30\x31")


#there seems to be some not printable chars because of packet fragmentation
#using index is dirty hack but don't care
if data[23] == off_response[21]:
  power_status = "Off"
elif data[23] == on_response[21]:
  power_status = "On"
else:
  power_status = "Unknown"

#Now we need to update sqlite with status

select_query = "SELECT cur_state,polls_in_state FROM dev_status WHERE mac = '00:25:5c:2e:36:39';"
#print update_query
new_polls=1

judodb = sqlite3.connect('/var/www/judo.db')
cur = judodb.cursor()
rows = cur.execute(select_query)
for row in rows:
  prev_state = row[0]
  prev_polls = row[1]
if prev_state == power_status:
  new_polls = prev_polls + 1
print prev_state + "\t\t" + str(new_polls)
update_query = "UPDATE dev_status SET cur_state = \'" + power_status + "\', polls_in_state = " + str(new_polls)  + " WHERE mac = '00:25:5c:2e:36:39'"
cur.execute(update_query)
judodb.commit()
judodb.close()


XBOX 360

For monitoring of XBOX 360 I've have to use arping and UPnP, because of limitation of XBOX 360 OS more can be find here including the script.

Automatic Switch Off


Now as we have ways of getting status we just use crontab to run the scripts each 10 minutes and to get the status and store it in to sqlite database. We are storing also how many time we see it consecutively in current status. (polls_in_state column in table below)

sqlite> .schema dev_status
CREATE TABLE dev_status(mac TEXT, cur_state TEXT, polls_in_state NUM, input TEXT);
sqlite> select * from dev_status;
00:25:5c:2e:36:39|On|3|unknown
00:a0:de:92:6b:78|On|3|HDMI1
00:ce:39:b5:48:9e|Up|4|unknown
7c:1e:52:c2:6c:a7|Up|3|unknown
00:01:2e:3a:a3:b2|Down|1214|unknown
Last thing we have to do is set crontab to run script, which check status based on the status of devices finds what is running and if no device coresponding to AV receiver input is running, (means that AV receiver is running for nothing) it will switch it off. Same for NEC.
crontab -l

#Monitoring of home network
*/10 * * * * /var/www/monitoring/judo_icmp_monitoring &> /dev/null
*/10 * * * * /var/www/monitoring/judo_upnp_monitoring &> /dev/null
*/10 * * * * /var/www/monitoring/HTR-4065_State.py &> /dev/null
*/10 * * * * /var/www/monitoring/NECStatus.py &> /dev/null
*/10 * * * * /var/www/monitoring/XBOX360Status.sh &> /dev/null

5,15,25,35,45,55 * * * * /var/www/monitoring/autostop >> /media/nas/public/autostop.log

autostop script
#! /bin/bash

HTR_MAC='00:a0:de:92:6b:78'
NEC_MAC='58:C2:32:97:3A:48'
Evolve_MAC='00:ce:39:b5:48:9e'
XBOX_MAC='7c:1e:52:c2:6c:a7'
Zotac_MAC='00:01:2e:3a:a3:b2'

CGIDIR='/var/www/cgi-bin'
DBFILE='/var/www/judo.db'

getDeviceStatus(){
  SQLQUERY="SELECT cur_state,polls_in_state,input FROM dev_status WHERE mac = '$1';"
  SQLRESULT=`sqlite3 $DBFILE "$SQLQUERY" | sed 's/|/ /g;'`
  echo "$SQLRESULT"
}


ACTION='No action'

echo -n ""

read HTR_STATUS HTR_POLLS_IN_STATUS HTR_INPUT <<< `getDeviceStatus $HTR_MAC`
echo -n "$HTR_STATUS $HTR_POLLS_IN_STATUS $HTR_INPUT"

#Example of output
#00:a0:de:92:6b:78|On|51|HDMI1

if [[ $HTR_STATUS == "On" && $HTR_POLLS_IN_STATUS -gt 3 ]]
then
  case $HTR_INPUT in
    "HDMI1")
       #echo "Check Evolve status"
       read Evolve_STATUS Evolve_POLLS_IN_STATUS Evolve_INPUT <<< `getDeviceStatus $Evolve_MAC`
       echo -n "$Evolve_STATUS $Evolve_POLLS_IN_STATUS $Evolve_INPUT"
       if [[ $Evolve_STATUS == "Down" && $Evolve_POLLS_IN_STATUS -gt 3 ]]
       then
         $CGIDIR/NECOff.py
         $CGIDIR/HTR-4065_Off.py
         ACTION="Switching HTR-4065 and NEC off"
       fi
       ;;
    "HDMI2")
       #echo "Check XBOX 360 status"
       read XBOX_STATUS XBOX_POLLS_IN_STATUS XBOX_INPUT <<< `getDeviceStatus $XBOX_MAC`
       echo -n "$XBOX_STATUS $XBOX_POLLS_IN_STATUS $XBOX_INPUT"
       if [[ $XBOX_STATUS == "Down" && $XBOX_POLLS_IN_STATUS -gt 3 ]]
       then
         $CGIDIR/NECOff.py
         $CGIDIR/HTR-4065_Off.py
         ACTION="Switching HTR-4065 and NEC off"
       fi
       ;;
    "HDMI3")
       #echo "Checking PC Zotac Status"
       read Zotac_STATUS Zotac_POLLS_IN_STATUS Zotac_INPUT <<< `getDeviceStatus $Zotac_MAC`
       echo -n "$Zotac_STATUS $Zotac_POLLS_IN_STATUS $Zotac_INPUT"
       if [[ $Zotac_STATUS == "Down" && $Zotac_POLLS_IN_STATUS -gt 3 ]]
       then
         $CGIDIR/NECOff.py
         $CGIDIR/HTR-4065_Off.py
         ACTION="Switching HTR-4065 and NEC off"
       fi
       ;;
  esac
else
  #AV reciever is Standby for more then hour and NEC is On for
  if [[ ! $HTR_STATUS == "On" ]]
  then
    read NEC_STATUS NEC_POLLS_IN_STATUS NEC_INPUT <<< `getDeviceStatus $NEC_MAC`
    echo -n "$NEC_STATUS $NEC_POLLS_IN_STATUS $NEC_INPUT"

    if [[ $HTR_POLLS_IN_STATE -gt 6 && $NEC_STATUS == "On" && $NEC_POLLS_IN_STATUS -gt 3 ]]
    then
      $CGIDIR/NECoff.py
      ACTION="Switching NEC Off"
    fi
  fi
fi

echo -n "$ACTION"
echo ""
#HTR-4065 HDMI1 : Evolve Down,3 => OFF: NEC, HTR-4065
#HTR-4065 HDMI2 : XBOX_360 Down,3 => OFF: NEC, HTR-4065
#HTR-4065 HDMI3 : PC_Zotac Down,3 => OFF: NEC, HTR-4065
#HTR-4065 HDMI4 : Not Assigned