Presence detection (SNMP)

From Domoticz
Jump to navigation Jump to search

Purpose

This script will turn on a virtual switch when a device is connected to your router, and turns the virtual switch off when the device is gone. You can use this to see if a person (which always carries his/her smartphone) is at home or not, and trigger events based on this.

Dependencies

Configure your router to accept SNMP

This script uses snmpwalk to get connected clients from your router. Your router needs to accept snmp v3 requests and you'll probably need to create a SNMP configuration in your router for this. Please check the manual of your router how to configure SNMP.

Installing Python

The Python scripts needs python to be installed. You can install this by running:

sudo apt-get install python

Installing SNMP

You'll also need to install snmpwalk on your Raspberry Pi. You can install this by running:

sudo apt-get install snmpd

Next, run:

sudo apt-get install snmp

Find the correct iso OIB

After snmpwalk is installed, walk through all snmp object id's by running:

snmpwalk -v3 -c public <router ip> -u <username>

Search for the OIB that contains the current connected devices.

iso.3.6.1.2.1.4.22.1.2.1 PfSense OIB
iso.3.6.1.2.1.17.4.3.1.1 D-Link managed switches OIB

Create virtual hardware dummy and virtual switch(es)

This script needs a virtual switch for each device you want to poll.

Create virtual dummy hardware

  • To create a virtual switch, you first have to create a virtual hardware device.

Go to Setup > Hardware. Type in a name (I use 'Dummy'), choose 'Dummy (Does nothing, use for virtual switches only)' from the drop-down. Leave 'Data timeout' disabled. Click on 'Add'

Create virtual switch(es)

To create a virtual switch, go to the 'Switches' tab. Click the 'Manual Light/Switch' button in the upper left corner. Choose the dummy hardware from the drop-down. Enter a name ('John Doe smartphone'). Switch type 'On/off'. Type 'X10'. For 'House code' and 'Unit code', choose some random, doesn't really matter. As 'Main Device'.
It is advisable to make the switch 'protected', so the switch can only be operated by the detection script and not by you, by accidentally clicking on it ;)

Now you need to know the ID of the switch. This can be found by going to 'Setup' --> 'Devices'. Find the switch you created in the list, and remember the IDX.
If you want multiple devices to be checked (smartphone of your wife) you will need to create a virtual switch for that device also. You will also need to find the id of that device.
Remember/write down this number, because you will need it later on.

Installation instructions

Create the script

Create a new file in the \home\pi\domoticz\scripts\ folder. Name the file 'check_device_snmp.py'. Copy the contents from the script on this page to the file, change the variables to your needs and save it.

Make script executable

In the terminal, go to cd /home/pi/domoticz/scripts
Execute the command chmod +x check_device_snmp.py

Python script

#!/usr/bin/python
#   Title: check_device_snmp.py
#   Original author: Chopper_Rob
#   Modified by: EBOOZ
#   Date: 14-01-2016
#   Info: Checks the presence of the given device on the network by checking your router by SNMP and reports back to domoticz
#   Version : 1.0
 
import sys
import datetime
import time
import os
import subprocess
import urllib2
import json
import base64
 
# Settings for Domoticz and snmpwalk
domoticzserver="192.168.0.2:8080"
domoticzusername = ""
domoticzpassword = ""
router = "192.168.0.1"
username = "admin"
community = "public"
isocode = "iso.3.6.1.2.1.3.1.1.2.12.1"
 
# If enabled. The script will log to the file _.log
# Logging to file only happens after the check for other instances, before that it only prints to screen.
log_to_file = False
 
# The script supports two types to check if another instance of the script is running.
# One will use the ps command, but this does not work on all machine (Synology has problems)
# The other option is to create a pid file named _.pid. The script will update the timestamp
# every interval. If a new instance of the script spawns it will check the age of the pid file.
# If the file doesn't exist or it is older then 3 * Interval it will keep running, otherwise is stops.
# Please chose the option you want to use "ps" or "pid", if this option is kept empty it will not check and just run.
check_for_instances = "pid"
 
 
 
# DO NOT CHANGE BEYOND THIS LINE
if len(sys.argv) != 5 :
  print ("Not enough parameters. Needs %Host %Switchid %Interval %Cooldownperiod.")
  sys.exit(0)
 
device=sys.argv[1]
switchid=sys.argv[2]
interval=sys.argv[3]
cooldownperiod=sys.argv[4]
previousstate=-1
lastsuccess=datetime.datetime.now()
lastreported=-1
base64string = base64.encodestring('%s:%s' % (domoticzusername, domoticzpassword)).replace('\n', '')
domoticzurl = 'http://'+domoticzserver+'/json.htm?type=devices&filter=all&used=true&order=Name'
 
if check_for_instances.lower() == "pid":
  pidfile = sys.argv[0] + '_' + sys.argv[1] + '.pid'
  if os.path.isfile( pidfile ):
    print datetime.datetime.now().strftime("%H:%M:%S") + "- pid file exists"
    if (time.time() - os.path.getmtime(pidfile)) < (float(interval) * 3):
      print datetime.datetime.now().strftime("%H:%M:%S") + "- script seems to be still running, exiting"
      print datetime.datetime.now().strftime("%H:%M:%S") + "- If this is not correct, please delete file " + pidfile
      sys.exit(0)
    else:
      print datetime.datetime.now().strftime("%H:%M:%S") + "- Seems to be an old file, ignoring."
  else:
    open(pidfile, 'w').close() 
 
if check_for_instances.lower() == "ps":
  if int(subprocess.check_output('ps x | grep \'' + sys.argv[0] + ' ' + sys.argv[1] + '\' | grep -cv grep', shell=True)) > 2 :
    print (datetime.datetime.now().strftime("%H:%M:%S") + "- script already running. exiting.")
    sys.exit(0)
 
def log(message):
  print message
  if log_to_file == True:
    logfile = open(sys.argv[0] + '_' + sys.argv[1] + '.log', "a")
    logfile.write(message + "\n")
    logfile.close()
 
def domoticzstatus ():
  json_object = json.loads(domoticzrequest(domoticzurl))
  status = 0
  switchfound = False
  if json_object["status"] == "OK":
    for i, v in enumerate(json_object["result"]):
      if json_object["result"][i]["idx"] == switchid and "Lighting" in json_object["result"][i]["Type"] :
        switchfound = True
        if json_object["result"][i]["Status"] == "On": 
          status = 1
        if json_object["result"][i]["Status"] == "Off": 
          status = 0
  if switchfound == False: print (datetime.datetime.now().strftime("%H:%M:%S") + "- Error. Could not find switch idx in Domoticz response. Defaulting to switch off.")
  return status
 
def domoticzrequest (url):
  request = urllib2.Request(url)
  request.add_header("Authorization", "Basic %s" % base64string)
  response = urllib2.urlopen(request)
  return response.read()
 
log (datetime.datetime.now().strftime("%H:%M:%S") + "- script started.")
 
lastreported = domoticzstatus()
if lastreported == 1 :
  log (datetime.datetime.now().strftime("%H:%M:%S") + "- according to Domoticz, " + device + " is online")
if lastreported == 0 :
  log (datetime.datetime.now().strftime("%H:%M:%S") + "- according to Domoticz, " + device + " is offline")
 
while 1==1:
  currentstate = subprocess.call('snmpwalk -v3 -c ' + community + ' ' + router + ' -u ' + username + ' ' + isocode + ' | grep -o "' + device + '" > /dev/null', shell=True)
 
  if currentstate == 0 : lastsuccess=datetime.datetime.now()
  if currentstate == 0 and currentstate != previousstate and lastreported == 1 : 
    log (datetime.datetime.now().strftime("%H:%M:%S") + "- " + device + " online, no need to tell Domoticz")
  if currentstate == 0 and currentstate != previousstate and lastreported != 1 :
    if domoticzstatus() == 0 :
      log (datetime.datetime.now().strftime("%H:%M:%S") + "- " + device + " online, tell Domoticz it's back")
      domoticzrequest("http://" + domoticzserver + "/json.htm?type=command&param=switchlight&idx=" + switchid + "&switchcmd=On&level=0")
    else:
      log (datetime.datetime.now().strftime("%H:%M:%S") + "- " + device + " online, but Domoticz already knew")
    lastreported=1
 
  if currentstate == 1 and currentstate != previousstate :
    log (datetime.datetime.now().strftime("%H:%M:%S") + "- " + device + " offline, waiting for it to come back")
 
  if currentstate == 1 and (datetime.datetime.now()-lastsuccess).total_seconds() > float(cooldownperiod) and lastreported != 0 :
    if domoticzstatus() == 1 :
      log (datetime.datetime.now().strftime("%H:%M:%S") + "- " + device + " offline, tell Domoticz it's gone")
      domoticzrequest("http://" + domoticzserver + "/json.htm?type=command&param=switchlight&idx=" + switchid + "&switchcmd=Off&level=0")
    else:
      log (datetime.datetime.now().strftime("%H:%M:%S") + "- " + device + " offline, but Domoticz already knew")
    lastreported=0
 
  time.sleep (float(interval))
 
  previousstate=currentstate
  if check_for_instances.lower() == "pid": open(pidfile, 'w').close()


Let the script run at certain times (cron)

To let the script work correctly, it needs to be run automatically at certain times. This can be done by using 'cron', the task scheduler on Linux.

On the terminal, run crontab -e. In the screen that opens, add a line for each device you want to check.

Example:

*/10 * * * *  sudo python /home/pi/domoticz/scripts/check_device_snmp.py "00 50 56 9C 3D 58" 37 10 120

NOTE: The way how you enter the MAC-address depends on the output of your router (with spaces, with dashes (-) or with colons (:)


The script needs 4 parameters to run:

  • The MAC-address of the device you want to check
  • The ID of the virtual switch (In my case '37')
  • The interval (seconds) on which to check if a device is present or not (In my case '10', for 10 seconds)
  • The 'cool-down' period (seconds). If a device does not respond within this period, the virtual switch is turned off. (In my case '120', for 120 seconds, or 2 minutes)

Exit the crontab editor by pressing CTRL-O (character 'O', not zero) and hitting Enter.

Troubleshooting

It can happen that the script is not working in your situation.

Try checking manually

First thing to try is, if you can check for your phone by using snmpwalk manually (snmpwalk -v3 -c public <router ip> -u <username>).

Check if the script works

Now we need to check if the script works when we activate it by hand. Go to the scripts directory (cd /home/pi/domoticz/scripts/) and then activate the script with the correct parameters:
python check_device_snmp.py MAC_ADDRESS IDX INTERVAL TIMEOUT. For testing you can use a interval of 10 seconds and a timeout of 30 seconds, so you don't have to wait very long to see the switch turn on/off in Domoticz.
If everything works good, you should see some output like: Script already running, exiting or Needs more parameters. If you get other errors (unclear errors, that doesn't seem to be related to the script), maybe you don't have 'python' (not 'python3' !!) installed correctly (sudo apt-get install python), or you made a mistake when you copied the script.

Check if crontab is correct

When the script is not yet activated by the crontab (in the setup above it should activate the script every 10 minutes) and you activate it by hand, you should see some output on the screen, like: Device online, tell domoticz it's back. You now know the script is working correctly, so the error is probably in your crontab.

Check if 'cron' service is running

You can check if 'cron' is active by issueing sudo service cron status.
If it is running, it should say [ ok ] cron is running.. If it is not running, you can start it by sudo service cron start.
To view the log of cron, you can issue grep CRON /var/log/syslog. You should see some lines of text if cron is active.

To enable 'cron' on Raspbmc, have a look this tutorial: http://www.averagemanvsraspberrypi.com/2014/07/using-cron-with-raspbmc.html

"ps: invalid option -- 'x' " message

If you try to run the script above on a Synology NAS, you will probably get the message ps: invalid option -- 'x'.
To fix this, go to line 66 of the script, and change ps x | grep into ps | grep. (remove the 'x').