Showing posts with label Python. Show all posts
Showing posts with label Python. Show all posts

Thursday, October 15, 2015

CSS to Display UPnP Media Info

I was playing with UPnP and my goal interest was to display on webpage what is currently playing on XBOX360. (But would work on most of UPnP devices.)
So first we need python script which will ask for information. For this we need service AVTransport and action GetMediaInfo, if you are not familiar with UPnP you can find more here
I have few others monitoring scripts which are storing info about devices in my network to sqlite3 DB. Following script use this DB to find current uri for of my XBOX 360 UPnP service and send the request to get the data.
#! /usr/bin/python

import sqlite3
import sys
import cgi
import httplib, urllib
import re
import suds

#As UPnP ports are dynamic I'm getting info from my DB, info is stored by monitoring script.
select_query = "SELECT ip,upnp_port FROM devices WHERE name like 'XBOX 360';"
judodb = sqlite3.connect('/var/www/judo.db')
cur = judodb.cursor()
rows = cur.execute(select_query)
for row in rows:
  ip = row[0]
  upnp_port = row[1]

judodb.commit()
judodb.close()


url = ip + ":" + str(upnp_port)
controlUrl = "/Control/AVTransport"
service = "urn:schemas-upnp-org:service:AVTransport:1"
soap_args = "0"

actions = ["GetTransportInfo", "GetMediaInfo"]

for action in actions:
  conn = httplib.HTTPConnection(url)
  soap_data = ''+soap_args+''

  #FOR DEBUG
  #print soap_data

  params = urllib.urlencode({'q': 'set'})
  headers = { "Content-Type": "text/xml", "Content-Length": "%d" % len(soap_data), "SOAPACTION": '"'+service+'#'+action+'"' }

  conn.request("POST", controlUrl , "", headers)
  conn.send(soap_data)

  response = conn.getresponse()

  #FOR DEBUG
  #print response.status, response.reason

  #In UPnP there is sometime nested xml inside xml as string with < and > instead of < and >
  #lines below fix this to male it all one xml
  responsestr = response.read()
  responsestr = responsestr.replace(">",">")
  responsestr = responsestr.replace("<","<")

  print responsestr

I use javascript XMLHttpRequest to call the script and write the data I get to webpage. Below is eaxmple of such data.
<div id="UPnPPlayInfo" class="list"><!--?xml version="1.0"?-->
<s:envelope s:encodingstyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
 <s:body>
  <u:gettransportinforesponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
    <currenttransportstate>STOPPED</currenttransportstate>
    <currenttransportstatus>OK</currenttransportstatus>
    <currentspeed>1</currentspeed>
  </u:gettransportinforesponse>
 </s:body>
</s:envelope>


<!--?xml version="1.0"?-->
<s:envelope s:encodingstyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
 <s:body>
  <u:getmediainforesponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
           <nrtracks>1</nrtracks>
           <mediaduration>0:01:51.856</mediaduration>
           <currenturi>http://10.0.0.33:9001/disk/DLNA-PNMP3-OP01-FLAGS01700000/O0$1$8I566796.mp3</currenturi>
           <currenturimetadata>
             <item id="0$1$12$4477R566796" parentid="0$1$12$4477" refid="0$1$8I566796" restricted="1">
               <dc:title>Ja zadam</dc:title><dc:date>2009-01-01</dc:date>
               <upnp:genre>Folk</upnp:genre
               ><upnp:album>Virtualky</upnp:album>
               <upnp:originaltracknumber>4</upnp:originaltracknumber>
               <dc:creator>Jaromir Nohavica</dc:creator>
               <upnp:albumarturi dlna:profileid="JPEG_TN">http://10.0.0.33:9001/disk/DLNA-PNJPEG_TN-OP01-CI1-FLAGS00d00000/defaultalbumart/a_u_d_i_o.jpg/O0$1$8I566796.jpg?scale=160x160</upnp:albumarturi>
               <upnp:artist>Jaromir Nohavica</upnp:artist>
               <pv:extension>mp3</pv:extension>
               <upnp:albumartist>Jaromir Nohavica</upnp:albumartist>
               <pv:modificationtime>1255889570</pv:modificationtime>
               <pv:addedtime>1401613856</pv:addedtime>
               <pv:lastupdated>1255889570</pv:lastupdated>
               <pv:album_crosslink>0$1$12$4153</pv:album_crosslink>
               <pv:artist_crosslink>0$1$11$3960</pv:artist_crosslink>
               <pv:genre_crosslink>0$1$10$2756</pv:genre_crosslink>
               <pv:numberofthisdisc>1</pv:numberofthisdisc>
               <upnp:author role="Composer">Jaromir Nohavica</upnp:author>
               <upnp:artist role="Composer">Jaromir Nohavica</upnp:artist>
               <pv:bookmark>uuid:55076f6e-6b79-4d65-64a7-5067f0a1791f,-L21udC9tZWRpYVNoYXJlcy9tdXNpYy9tdXNpYy9KYXJvbWlyX05vaGF2aWNhL1ZpcnR1YWxreS9qYV96YWRhbS5tcDM=</pv:bookmark>
            </item>
          </currenturimetadata>
    </u:getmediainforesponse>
  </s:body>
</s:envelope>

Last thing I need to do is to use CSS to display only what I want and add some formatting and additional text.
/* LIST OF TAGS TO BE NOT DISPLAYED  */
currenttransportstatus, currentspeed, currenturi, playmedium, recordmedium, writestatus, res, mediaduration, nrtracks, desc {display: none}
upnp\:class, upnp\:albumarturi, dc\:creator, upnp\:albumartist, upnp\:author  {display: none}
pv\:extension, pv\:modificationtime, pv\:addedtime, pv\:lastupdated, pv\:album_crosslink, pv\:artist_crosslink, pv\:genre_crosslink, pv\:bookmark, pv\:numberofdiscs, pv\:numberofthisdisc {display: none}

DIDL-Lite {display: block}

Meta_Info:before {content: "Currently Playing:"; font-weight: bold}
Play_Mode:before {content: "Play mode:"; font-weight: bold}

/* LIST OF TAGS TO BE DISPLAYED ONE PER LINE */
upnp\:genre, upnp\:originaltracknumber, upnp\:album, upnp\:artist, dc\:title, dc\:date, dc\:publisher {display: list-item; list-style-type: none}


upnp\:artist:before {content: "Artist: "; margin-left: 10px; font-weight: bold}
upnp\:album:before {content: "Album: "; margin-left: 10px; font-weight: bold}
upnp\:genre:before {content: "Genre: "; margin-left: 10px; font-weight: bold}
upnp\:originaltracknumber:before {content: "Track No.: "; margin-left: 10px; font-weight: bold}
dc\:title:before {content: "Song: "; margin-left: 10px; font-weight: bold}
dc\:date:before {content: "Date: "; margin-left: 10px; font-weight: bold}
dc\:publisher:before {content: "Publisher: "; margin-left: 10px; font-weight: bold}

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

Tuesday, March 17, 2015

Home printer monitoring - Brother DCP-J4110DW

As I'm trying to automate and monitor everything in my home I wanted to monitor my printer/scanner, Brother DCP-J4110DW.

First thing I did, and I do with every device I have, here is what I find.
21/tcp   open  ftp -
220 FTP print service:V-1.13/Use the network password for the ID if updating.
Name (10.0.0.39:pi): admin
23/tcp   open  telnet - Doesn't ask for anything(no request for user or password, no command-line)
25/tcp   open  smtp
80/tcp   open  http - Web Management and setting
443/tcp  open  https -  Web Management and setting
515/tcp  open  printer
631/tcp  open  ipp
9100/tcp open  jetdirect

Second thing is snmpwalk with community public and in this case it works (even I didn't find snmp agent setting in printer configuration). I've tried to find following statistics:
    Total Pages printed - iso.3.6.1.2.1.43.10.2.1.4.1.1 = Counter32: 115
    Monochromatic pages - iso.3.6.1.4.1.2435.2.3.9.4.2.1.5.5.52.1.1.3.3 = Counter32: 17
    Color pages - iso.3.6.1.4.1.2435.2.3.9.4.2.1.5.5.52.1.1.3.4 = Counter32: 98

I was not successful to get ink level, even though the OIDs for those are present the value given is 'Unknown', most probably this is only implemented on printers targeted for companies.
However I'm able to see the ink level on status webpage (status webpages is accessible without login which is fine and easier to get statistics).
The html code we need to get info from is looking like this below and ink level is actually the height of the color image.
<div id="ink_level">
<table id="inkLevel" summary="ink level">
<tr><th></th><th></th><th></th><th><img src="../common/images/low.gif" alt="Low" /></th></tr><tr>
<td><img src="../common/images/magenta.gif" alt="Magenta" class="tonerremain" height="23px" /></td>
<td><img src="../common/images/cyan.gif" alt="Cyan" class="tonerremain" height="18px" /></td>
<td><img src="../common/images/yellow.gif" alt="Yellow" class="tonerremain" height="16px" /></td>
<td><img src="../common/images/black.gif" alt="Black" class="tonerremain" height="9px" /></td>
</tr><tr><th>M</th><th>C</th><th>Y</th><th>BK</th></tr>
</table>
</div>

So I wrote following code in python (Brother_inkLevel) to get the ink level and provide in text output which would be easy to parse by other scripts Brother_inkLevel.py


#! /usr/bin/python

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

#get status html
url = 'http://10.0.0.39/general/status.html'
html = urllib.urlopen(url).read()

inkLevel = ''

#parse hmtl
#get all img tags
#if it's inklevel img I get the height, it's represent the ink level
xmldoc = minidom.parseString(html)
xml_images = xmldoc.getElementsByTagName('img')
for img in xml_images:
  if img.getAttribute('alt') == 'Cyan':
    c = "C:" + img.getAttribute('height') + " "
  elif img.getAttribute('alt') == 'Magenta':
    m = "M:" + img.getAttribute('height') + " "
  elif img.getAttribute('alt') == 'Yellow':
    y = "Y:" + img.getAttribute('height') + " "
  elif img.getAttribute('alt') == 'Black':
    k = "K:" + img.getAttribute('height') + " "

inkLevel = c + m + y + k
print inkLevel


And here is the output of the script
root@raspberrypi:/var/www/monitoring# ./Brother_inkLevel.py
C:18px M:23px Y:16px K:9px

Next I used bash script to get printed page statistics, include ink Level and store it in sqlite db
#! /bin/bash

BROTHER_IP='10.0.0.39'
UUID='e3248000-80ce-11db-8000-30055c1cd6d3'
COMMUNITY='public'

DBFILE='/var/www/judo.db'
RD='/var/www/monitoring'

DATE=`date +%Y-%m-%d`

MONO_PAGE_OID='.1.3.6.1.4.1.2435.2.3.9.4.2.1.5.5.52.1.1.3.3'
COLOR_PAGE_OID='.1.3.6.1.4.1.2435.2.3.9.4.2.1.5.5.52.1.1.3.4'
TOTAL_PAGE_OID='.1.3.6.1.2.1.43.10.2.1.4.1.1'

MONO_PAGE_COUNT=`/usr/bin/snmpget -v 2c -c $COMMUNITY $BROTHER_IP $COLOR_PAGE_OID | awk '{print $4}'`
COLOR_PAGE_COUNT=`/usr/bin/snmpget -v 2c -c $COMMUNITY $BROTHER_IP $MONO_PAGE_OID | awk '{print $4}'`
TOTAL_PAGE_COUNT=`/usr/bin/snmpget -v 2c -c $COMMUNITY $BROTHER_IP $TOTAL_PAGE_OID | awk '{print $4}'`

echo "Monochromatic pages: $MONO_PAGE_COUNT, Color pages: $COLOR_PAGE_COUNT, Total pages: $TOTAL_PAGE_COUNT"

INKLEVEL=`$RD/Brother_inkLevel.py | sed 's/px/,/g;' | sed 's/C://g;' | sed 's/M://g' | sed 's/Y://g;' | sed 's/K://g;' | sed 's/, $//g;'`

echo "$INKLEVEL"

sqlite3 $DBFILE "INSERT INTO Brother_Daily_Stats VALUES('$UUID', '$DATE', $TOTAL_PAGE_COUNT, $MONO_PAGE_COUNT, $COLOR_PAGE_COUNT, $INKLEVEL);"

I've let that run for few days and bellow are results, however to get more relevant info I'll be monitoring for one year and update here more findings.
sqlite> select * from Brother_Daily_Stats;
e3248000-80ce-11db-8000-30055c1cd6d3|2015-02-21|137|119|18|18|23|16|9
e3248000-80ce-11db-8000-30055c1cd6d3|2015-02-22|137|119|18|18|23|16|9
e3248000-80ce-11db-8000-30055c1cd6d3|2015-02-23|137|119|18|18|23|16|8
e3248000-80ce-11db-8000-30055c1cd6d3|2015-02-24|137|119|18|18|23|16|8
e3248000-80ce-11db-8000-30055c1cd6d3|2015-02-25|137|119|18|18|23|16|8
e3248000-80ce-11db-8000-30055c1cd6d3|2015-02-26|137|119|18|18|23|16|8
e3248000-80ce-11db-8000-30055c1cd6d3|2015-02-27|137|119|18|17|23|16|8
e3248000-80ce-11db-8000-30055c1cd6d3|2015-02-28|137|119|18|17|23|16|8
e3248000-80ce-11db-8000-30055c1cd6d3|2015-03-01|137|119|18|17|23|16|8
e3248000-80ce-11db-8000-30055c1cd6d3|2015-03-02|137|119|18|17|23|16|8
e3248000-80ce-11db-8000-30055c1cd6d3|2015-03-03|137|119|18|17|23|16|8
e3248000-80ce-11db-8000-30055c1cd6d3|2015-03-04|137|119|18|17|23|16|8
e3248000-80ce-11db-8000-30055c1cd6d3|2015-03-05|137|119|18|17|23|16|8
e3248000-80ce-11db-8000-30055c1cd6d3|2015-03-06|137|119|18|17|23|16|8
e3248000-80ce-11db-8000-30055c1cd6d3|2015-03-07|137|119|18|17|23|16|8
e3248000-80ce-11db-8000-30055c1cd6d3|2015-03-08|137|119|18|17|23|16|8
e3248000-80ce-11db-8000-30055c1cd6d3|2015-03-09|137|119|18|17|23|16|8
e3248000-80ce-11db-8000-30055c1cd6d3|2015-03-10|137|119|18|17|23|16|8
e3248000-80ce-11db-8000-30055c1cd6d3|2015-03-11|137|119|18|17|23|16|8
e3248000-80ce-11db-8000-30055c1cd6d3|2015-03-12|137|119|18|17|23|16|8
e3248000-80ce-11db-8000-30055c1cd6d3|2015-03-13|137|119|18|17|23|16|8
e3248000-80ce-11db-8000-30055c1cd6d3|2015-03-14|137|119|18|17|23|16|8
e3248000-80ce-11db-8000-30055c1cd6d3|2015-03-15|137|119|18|17|23|16|8
e3248000-80ce-11db-8000-30055c1cd6d3|2015-03-16|137|119|18|13|19|11|7
e3248000-80ce-11db-8000-30055c1cd6d3|2015-03-17|137|119|18|13|18|11|5

Thursday, September 18, 2014

UPnP and Python

I guess that you already know what UPnP is. Let's see how we can write some python code which we can use to control some UPnP device. First we need to discover device and services provided by it, for this we will use gssdp-discover -n 10 gssdp-discover is part of the gupnp-tools package.
gssdp-discover -n 10
We will see something like this:
resource available
  USN:      uuid:03166842-2743-2000-0000-7c1e52c26ca7::upnp:rootdevice
  Location: http://10.0.0.9:1027/
resource available
  USN:      uuid:03166842-2743-2000-0000-7c1e52c26ca7::urn:schemas-upnp-org:device:MediaRenderer:1
  Location: http://10.0.0.9:1027/
resource available
  USN:      uuid:03166842-2743-2000-0000-7c1e52c26ca7
  Location: http://10.0.0.9:1027/
resource available
  USN:      uuid:03166842-2743-2000-0000-7c1e52c26ca7::urn:schemas-upnp-org:service:AVTransport:1
  Location: http://10.0.0.9:1027/
resource available
  USN:      uuid:03166842-2743-2000-0000-7c1e52c26ca7::urn:schemas-upnp-org:service:ConnectionManager:1
  Location: http://10.0.0.9:1027/
resource available
  USN:      uuid:03166842-2743-2000-0000-7c1e52c26ca7::urn:schemas-upnp-org:service:RenderingControl:1
  Location: http://10.0.0.9:1027/
As you can see this device provides MediaRenderer, AVTransport, ConnectionManager and RenderingControl. This means we can play media on this device and control it (Play, Pause, Stop, Volume Up, ...) We know that UPnP is basically SOAP so we first need to create header,
POST control URL HTTP/1.1
HOST: host:port
CONTENT-LENGTH: bytes in body
CONTENT-TYPE: text/xml; charset="utf-8"
SOAPACTION: "urn:schemas-upnp-org:service:serviceType:v#actionName”
As you can see above we will need to find control URL, host:port (this we already know from gssdp-discovery) and action we want to perform. If we use any browser and open url in give by Location (from gssdp-discovery) we will get XML with basic information about device plus services provided. There is tag ServiceList which contains info about services for each we will need controlURL and SCPDURL, SCPDURL is path xml definition of function provided by the service. Bellow we can see example of service. So we can fin out that control URL we need is /AVTrasnport/control and definition of actions we will find in host:port/AVTransport/scpd.xml
<service>
  <serviceType>urn:schemas-upnp-org:service:AVTransport:1</serviceType>
  <serviceId>urn:upnp-org:serviceId:AVTransport_1F88B77A-8236-49b9-B344-974969412930</serviceId>
  <SCPDURL>AVTransport/scpd.xml</SCPDURL>
  <controlURL>AVTransport/control</controlURL>
  <eventSubURL>AVTransport/event</eventSubURL>
</service>
Now we will open http://host:port/AVTransport/scpd.xml and find action we want to perform, bellow we can see example of such action definitoin, we will need name and argument with direction in, direction out is actually what we will get in response.
<action>
  <name>GetMediaInfo</name>
   <argumentlist>
     <argument>
       <name>InstanceID</name>
       <direction>in</direction>
       <relatedstatevariable>A_ARG_TYPE_InstanceID</relatedstatevariable>
     </argument>
     <argument>
       <name>NrTracks</name>
       <direction>out</direction>
       <relatedstatevariable>NumberOfTracks</relatedstatevariable>
     </argument>
     <argument>
       <name>MediaDuration</name>
       <direction>out</direction>
       <relatedstatevariable>CurrentMediaDuration</relatedstatevariable>
     </argument>
     <argument>
       <name>CurrentURI</name>
       <direction>out</direction>
       <relatedstatevariable>AVTransportURI</relatedstatevariable>
     </argument>
     ...
  </argumentlist>
</action>
In our case action name is GetMediaInfo and argument is InstanceID, this instanceID as we can further see is related to variable A_ARG_TYPE_InstanceID if we search further in the definition xml we find this statevariable and its default value.
<statevariable sendevents="no">
  <name>A_ARG_TYPE_InstanceID</name>
  <datatype>ui4</datatype>
  <defaultvalue>0</defaultvalue>
</statevariable>
SOAP Message should look like this:
<s:Envelope xmlns: s=“http://schemas.xmlsoap.org/soap/envelope” s:encodingStyle=“http://schemas.xmlsoap.org/soap/encoding”>
<s:Body>
    <u:actionName xmlns:u="urn:schemas-upnp-org:service:serviceType:v">
        <argumentName>in arg value</argumentName>       
   </u:actionName>
</s:Body>
</s:Envelope>
As we already know all we need we will create testUPnP.py script in python which will take action as argument
import sys
import httplib, urllib
import re

action = sys.argv[1]

conn = httplib.HTTPConnection("10.0.0.9:1027")
soap_data = '<s:Envelope xmlns: s="http://schemas.xmlsoap.org/soap/envelope" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding"><s:Body><u:'+action+' xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"><InstanceID>0</InstanceID></u:'+action+'></s:Body></s:Envelope>'

params = urllib.urlencode({'q': 'set'})
headers = { "Content-Type": "text/xml", "Content-Length": "%d" % len(soap_data), "SOAPACTION": "urn:schemas-upnp-org:service:AVTransport:1#"+action+'"' }

conn.request("POST", "/AVTransport/control", "", headers)
conn.send(soap_data)

response = conn.getresponse()

#FOR DEBUG
print response.status, response.reason
print response.read()

Wednesday, August 14, 2013

Yamaha Network Control

During my home automation project, where I want to use Raspberry Pi running lighttpd to make web for home automation, I've learned a bit about Yamaha network control and because it took me some time to find the info. I would like to share my findings.
There are two ways you might control this device:
  • UPnP, which doesn't seem to work properly as far any attempt to play music from computer failed.
  • YNC (Yamaha network control), unfortunatly I didn't find any official documentation about this protocol from Yamaha, however devices hold this documentation them self, it's basically extension of UPnP.

Bellow is part of UPnP specification showing controlURL and SCPDURL, SCPDURL is path xml definition of function provided by the service. (More on UPnP here)
<yamaha:X_device>
  <yamaha:X_serviceList>
    <yamaha:X_service>
      <yamaha:X_specType>urn:schemas-yamaha-com:service:X_YamahaRemoteControl:1</yamaha:X_specType>
      <yamaha:X_controlURL>/YamahaRemoteControl/ctrl</yamaha:X_controlURL>
      <yamaha:X_unitDescURL>/YamahaRemoteControl/desc.xml</yamaha:X_unitDescURL>
    </yamaha:X_service>
  </yamaha:X_serviceList>
</yamaha:X_device> 
I also find that somebody collect those and transform them into excel file which, might be more readble here
Here is backup in cause the original link doesn't work.
Based on the specification in picture we created the sample message to increase volume level

<YAMAHA_AV cmd="PUT">
  <Main_Zone>
    <Volume>
      <Lvl>
        <Val>Up 1 dB</Val>
        <Exp></Exp>
        <Unit></Unit>
      </Lvl>
    </Volume>
  </Main_Zone>
</YAMAHA_AV>
And here is piece of python code to send it.
#! /usr/bin/python

import httplib, urllib
import re

conn = httplib.HTTPConnection("10.0.0.36:80")
xml_data='<YAMAHA_AV cmd="PUT"><Main_Zone><Volume><Lvl><Val>Up 1 dB</Val><Exp></Exp><Unit></Unit></Lvl></Volume></Main_Zone></YAMAHA_AV>'

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

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

response = conn.getresponse()

#FOR DEBUG
#print response.status, response.reason
#print response.read()

conn.close()
As far as YNC returns XML you can use CSS to display the information return on web, below is python code requesting information about media curently played. As well as CSS code to display it.

HTR-4065_PlayInfo.py
#! /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_play_info = '<?xml version="1.0" encoding="utf-8"?><YAMAHA_AV cmd="GET"><SERVER><Play_Info<GetParam</Play_Info></SERVER></YAMAHA_AV>'


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

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

response = conn.getresponse()

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

Here is the response
<YAMAHA_AV rsp="GET" RC="0">
  <SERVER>
    <Play_Info>
      <Feature_Availability>Ready</Feature_Availability>
      <Playback_Info>Play</Playback_Info>
      <Play_Mode>
        <Repeat>Off</Repeat>
        <Shuffle>Off</Shuffle>
      </Play_Mode>
      <Meta_Info>
        <Artist>Enya</Artist>
        <Album>Paint The Sky With Stars</Album>
        <Song>Boadicea</Song>
      </Meta_Info>
      <Album_ART>
        <URL>/YamahaRemoteControl/AlbumART/AlbumART.ymf</URL>
        <ID>243</ID>
        <Format>YMF</Format>
      </Album_ART>
    </Play_Info>
  </SERVER>
</YAMAHA_AV>
yncPlayInfo.css
Feature_Availability, Album_ART, Playback_Info {display: none}
Meta_Info, Play_Mode {display: block}
Meta_Info:before {content: "Currently Playing:"; font-weight: bold}
Play_Mode:before {content: "Play mode:"; font-weight: bold}
Artist, Album, Song {display: list-item; list-style-type: none}
Artist:before {content: "Artist: "; margin-left: 10px; font-weight: bold}
Album:before {content: "Album: "; margin-left: 10px; font-weight: bold}
Song:before {content: "Song: "; margin-left: 10px; font-weight: bold}
Repeat, Shuffle {display: list-item}
Repeat:before {content: "Repeat: "; margin-left: 10px; font-weight: bold}
Shuffle:before {content: "Shuffle: "; margin-left: 10px; font-weight: bold}

menu_status, menu_layer, attribute {display: none}
menu_name {font-weight: bold}
current_list {display: block}
txt {display: list-item; list-style-type: none; margin-left: 10px}

Wednesday, July 24, 2013

NEC MultiSync Ethernet Remote Control

Two years back I bought NEC MultiSync V321 one of the reasons was it have ethernet port and you can control it over the network.
The other reason is that there is documentation for it provided by NEC which can help you with commands, if you are intelligent and good enough to understand it. Which is probably not my case.
However NEC also offers NEC PD comms tool, which is application which let you control your NEC.
As I have Yamaha AV reciever (for which Yamaha provides control app) my intention recently was to have small app just to switch on/off the monitor and so limit number of remote controlers.
I also recently bought raspberry Pi and find good use for it. I'm running web server there and using cgi I can switch on/off my NEC and also Yamaha (will write about it in future as I will implement more features).
So what I did, I used NEC PD comms tool and wireshark to catch the tcp data payload for on and off, I wrote few lines in python to send those data and add few more to work nicely as cgi.

Here are the source codes:

NECOn.py
#! /usr/bin/python

import socket

monitor_ip = '10.0.0.35'
port = 7142
buffer_size = 1024
data_on = '\x01\x30\x41\x30\x41\x30\x43\x02\x43\x32\x30\x33\x44\x36\x30\x30\x30\x31\x03\x73\x0d'

new = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
new.connect((monitor_ip, port))
new.send(data_on)
recv_data = new.recv(buffer_size)
new.close()

print """
<html>
  <head>
    <meta http-equiv="refresh" content="0;url=http://10.0.0.37" />
    <title>You are going to be redirected</title>
  </head>
  <body>
    Redirecting...
  </body>
</html>
"""

NECOff.py
#! /usr/bin/python

import socket

monitor_ip = '10.0.0.35'
port = 7142
buffer_size = 1024
data_off = '\x01\x30\x41\x30\x41\x30\x43\x02\x43\x32\x30\x33\x44\x36\x30\x30\x30\x34\x03\x76\x0d'


mon_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
mon_socket.connect((monitor_ip, port))
mon_socket.send(data_off)
recv_data = mon_socket.recv(buffer_size)
mon_socket.close()

print """
<html>
  <head>
    <meta http-equiv="refresh" content="0;url=http://10.0.0.37" />
    <title>You are going to be redirected</title>
  </head>
  <body>
    Redirecting...
  </body>
</html>
"""
For monitoring purpose I needed to find the howto check status of my NEC and recorded in the Database(I use sqlite3), even the response is just 25 bytes NEC dived it into 2 packets, so we are recording all packets and then check if the On or Off. NECStatus.py
#! /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()

If you have any questions you can always contact me.