Python plugin: Presence detection from wireless router

Python and python framework
Post Reply
EscApe
Posts: 5
Joined: Thursday 02 April 2015 8:46
Target OS: Linux
Domoticz version: v2.2321
Location: The Netherlands
Contact:

Python plugin: Presence detection from wireless router

Post by EscApe » Saturday 18 November 2017 21:44

This is my very first plugin. It detects presence by getting the wireless list from a router that supports the wl command and ssh login using a key file. The plugin is based on a python daemon i wrote a few years ago and have been using successfully. I have limited experience running this as a plugin, but it seems to behave well. Since i am no real programmer i would like to invite anyone to build upon this first version.

Preparing your router for this plugin can be a challenge and is not for everyone. Unfortunately i will not be able to offer any support, but maybe someone else on the forum can.

The plugin creates a general presence switch that will be on if any of the monitored mac-adrresses is connected to the router. There is a separate switch for each individual mac-address.

Requirements:
- beta of Domoticz that supports python plugin
- router that supports wl command
- ssh access to the router using a key file (* having this kind of access to the router is a security risk. You can put the private keyfile in a not easy to guess directory and point the plugin to that key file).

Asus ac68u with merlin firmware works perfectly for me.

Installation:
Create a keyfile (see: http://www.linuxproblem.org/art_9.html)
Place the public(!) key on the router. Using the asus merlin firmware this can be done in the GUI. This will be different for other routers and might be impossible on many standard firmwares!
Place the private keyfile anywhere you will feel is safe enough. Make sure it has the correct rights (chmod 600 <filename> on linux).

Unpack the attached zipfile in de ... domoticz/plugins directory and restart domoticz
Add the 'iDetect Wifi presence detection' in the Domoticz hardware section
Configure:
-router ip address
-mac addresses to monitor in the format <name>=<mac addrress> separated by a comma eg: phone1=11:22:33:44:55:66,phone2=99:88:77:66:55:44
- 'remove obsolete' gives you a choice to automatically delete devices that are no longer in de above list of mac-addresses OR to show them as timedout.
- interval between checks (i use 10 seconds)
- a grace period after which phones are shown as absent (to deal with temporarily dropped connections). This should be longer than te interval. A interval of 10 seconds and grace period of 25 seconds will show phones as absent after 3 checks. A phone will always be marked present as soon as it is detected the first time.
- router username
- location and name of the keyfile (This allows you to just put it in a fixed / predictable location. Standaard location would be <homedir>./ssh/id_rsa. Since domoticz runs as a service this might nog by the home directory of the user that created the key file!)

Example screenshot of the configuration:
config-idetect.png
config-idetect.png (395.54 KiB) Viewed 368 times


After enabling the hardware the desired devices/switches should be created immediately.
devices-idetect.png
devices-idetect.png (279.39 KiB) Viewed 368 times


A word of caution: I do not know if this is a real issue, but i felt compelled to check it for my own setup. If your router logs SSH access to a file in flash memory you might theoretically wear-out the flash-memory of the router. In my case only errors and failed logins are logged and thats fine.

Paths i have tried but abandoned:
I tried using paramiko ssh module for a persistent ssh connection, but could not get it to work. Probably because it tries te spawn a separate thread for the connection.
A snmp query also did not work for me. I had trouble getting the right data and even with a snmp browser i noticed the wireless data was not refreshed fast enough.

Hope you enjoy it.

Preview the code:

Code: Select all

# Domoticz Python Plugin to monitor router WL table for presence/absence
#
# Author: ESCape
#
"""
<plugin key="idetect" name="iDetect Wifi presence detection " author="ESCape" version="0.0.1">
    <params>
        <param field="Address" label="Wifi router IP Address" width="200px" required="true" default="192.168.0.1"/>
        <param field="Mode1" label="MAC addresses te monitor" width="500px" required="true" default="phone1=01:01:01:01:01:01,phone2=02:02:02:02:02:02"/>
        <param field="Mode6" label="Remove obsolete" width="250px">
            <options>
                <option label="Delete obsolete devices" value="True"/>
                <option label="Show obsolete devices as unavailable" value="False"/>
            </options>
        </param>
        <param field="Mode2" label="Interval (sec)" width="75px" required="true" default="10"/>
        <param field="Mode3" label="Grace period (sec)" width="75px" required="true" default="30"/>
        <param field="Mode4" label="Router username" width="100px" required="true" default="admin"/>
        <param field="Mode5" label="Router ssh keyfile" width="500px" required="true" default="~/.ssh/id_rsa"/>
    </params>
</plugin>
"""

import Domoticz
from datetime import datetime, timedelta
import subprocess

class BasePlugin:

    def __init__(self):
        self.debug = True
        self.error = False
        self.present = False
        self.previousstate = []
        self.monitormacs = {}
        self.graceoffline = 0
        self.timelastseen = {}
        return

    def activemacs(self, host, user, idfile):
        try:
            Domoticz.Debug("get data")
            ssh = subprocess.Popen(["ssh", "-o", "ConnectTimeout=3", user+"@"+host, "-i", idfile, "wl -i eth1 assoclist;wl -i eth2 assoclist"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            ssh_out, ssh_err = ssh.communicate()
            Domoticz.Debug("data received")
        except:
            Domoticz.Error("Error getting data from router" + str(ssh_err))
            return None
        list=[]
        if not ssh_err and ssh_out != "":
            for item in ssh_out.splitlines():
                list.append(item.decode("utf-8").split(" ")[-1])
            Domoticz.Debug("List from router" + str(list))
            return list
        else:
            Domoticz.Error("Error getting data from router: " + str(ssh_err))
            return None
                
    def updatestatus(self, id, onstatus):
        if onstatus:
            svalue = "On"
            nvalue = 1
        else:
            svalue = "Off"
            nvalue = 0
        if id not in Devices:
            Domoticz.Log("Device " + str(id) + " does not exist")
            return
        if Devices[id].nValue != nvalue or Devices[id].sValue != svalue:
            Devices[id].Update(nValue=nvalue, sValue=svalue)
            
    def onStart(self):
        maclist={}
        devmacs=Parameters["Mode1"].replace(" ", "").split(",")
        for item in devmacs:
            try:
                name, mac =item.split("=")
                maclist.update({name:mac})
            except:
                Domoticz.Error("Invalid device/mac setting in: " + str(devmacs))
        self.monitormacs = maclist
        Domoticz.Debug("Monitoring " + str(self.monitormacs) + " for presence.")
        Domoticz.Heartbeat(int(Parameters["Mode2"]))
        self.graceoffline = int(Parameters["Mode3"])
        self.routerip = Parameters["Address"]
        self.routeruser = Parameters["Mode4"]
        self.routersshkey = Parameters["Mode5"]
        self.individualswitches = True
        self.deleteobsolete = Parameters["Mode6"] == "True"
        self.devnametoid={}

        #Select or create icons for devices 
        homeicon="idetect-home"
        uniticon="idetect-unithome"
        if homeicon not in Images: Domoticz.Image('ihome.zip').Create()
        homeiconid=Images[homeicon].ID
        if uniticon not in Images: Domoticz.Image('iunit.zip').Create()
        uniticonid=Images[uniticon].ID
        
        #Create Anyone home device
        if 1 not in Devices:
            Domoticz.Device(Name="Anyone", Unit=1, TypeName="Switch", Used=1, Image=homeiconid).Create()
            Domoticz.Log("Device created fot general/Anyone presence")
        
        #Find obsolete device units (no longer configured te be monitored)
        deletecandidates=[]
        for dev in Devices:
            if dev == 1:
                continue
            #Check if devices are still in use
            Domoticz.Debug("monitoring device: " + Devices[dev].Name)
            if Devices[dev].Name.split(" ")[-1] in self.monitormacs: #prep for use
                Domoticz.Debug(Devices[dev].Name.split(" ")[-1] + " is stil in use")
                self.devnametoid.update({Devices[dev].Name.split(" ")[-1]:dev})
            else: #delete device
                if dev != 1:
                    if self.deleteobsolete:
                        Domoticz.Log("deleting device unit: " + str(dev) + " named: " + Devices[dev].Name)
                        deletecandidates.append(dev)
                    else:
                        Devices[dev].Update(nValue=0, sValue="Off", TimedOut=1)

        Domoticz.Debug("devnametoid: " + str(self.devnametoid))
        Domoticz.Debug("monitormacs: " + str(self.monitormacs))
        
        for obsolete in deletecandidates:
            Devices[obsolete].Delete()
        
        for name in self.monitormacs:
            #Check if there is a Domoticz device for the configured MAC address
            if name not in self.devnametoid:
                Domoticz.Log(name + " not in devnametoid")
                success=False
                for num in range(2,11):
                    if num not in Devices:
                        Domoticz.Log("creating name=" + name + " unit=" + str(num)) 
                        Domoticz.Device(Name=name, Unit=num, TypeName="Switch", Used=1, Image=uniticonid).Create()
                        Devices[num].Update(nValue=0, sValue="Off")
                        self.devnametoid.update({Devices[num].Name.split(" ")[-1]:num})
                        success=True
                        break
                if not success:
                    Domoticz.Error("No numers left to create device for " + name)

    def onStop(self):
        Domoticz.Log("onStop called")

    def onConnect(self, Connection, Status, Description):
        Domoticz.Log("onConnect called")

    def onMessage(self, Connection, Data):
        Domoticz.Log("onMessage called")

    def onCommand(self, Unit, Command, Level, Hue):
        Domoticz.Log("onCommand called for Unit " + str(Unit) + ": Parameter '" + str(Command) + "', Level: " + str(Level))

    def onNotification(self, Name, Subject, Text, Status, Priority, Sound, ImageFile):
        Domoticz.Log("Notification: " + Name + "," + Subject + "," + Text + "," + Status + "," + str(Priority) + "," + Sound + "," + ImageFile)

    def onDisconnect(self, Connection):
        Domoticz.Log("onDisconnect called")

    def onHeartbeat(self):
        Domoticz.Debug("devnametoid: " + str(self.devnametoid))
        found = self.activemacs(self.routerip, self.routeruser, self.routersshkey)
        if found is not None:
            someonehome=False
            for devname, mac in self.monitormacs.items():
                if mac in found:
                    someonehome=True
                    self.updatestatus(self.devnametoid[devname], True)
                    if devname in self.timelastseen:
                        self.timelastseen.pop(devname, None)
                else:
                    if devname in self.timelastseen:
                        if not datetime.now() - self.timelastseen[devname] > timedelta(seconds=self.graceoffline):
                            Domoticz.Debug("Check if realy offline: " + devname)
                            someonehome=True
                        else:
                            self.updatestatus(self.devnametoid[devname], False)
                    else:
                        self.timelastseen[devname] = datetime.now()
                        Domoticz.Debug("Seems to have went offline: " + devname)
                        someonehome=True
            self.updatestatus(1, someonehome)    
        else:
            Domoticz.Error("No list of connected WLAN devices from router")
                
global _plugin
_plugin = BasePlugin()

def onStart():
    global _plugin
    _plugin.onStart()

def onStop():
    global _plugin
    _plugin.onStop()

def onConnect(Connection, Status, Description):
    global _plugin
    _plugin.onConnect(Connection, Status, Description)

def onMessage(Connection, Data):
    global _plugin
    _plugin.onMessage(Connection, Data)

def onCommand(Unit, Command, Level, Hue):
    global _plugin
    _plugin.onCommand(Unit, Command, Level, Hue)

def onNotification(Name, Subject, Text, Status, Priority, Sound, ImageFile):
    global _plugin
    _plugin.onNotification(Name, Subject, Text, Status, Priority, Sound, ImageFile)

def onDisconnect(Connection):
    global _plugin
    _plugin.onDisconnect(Connection)

def onHeartbeat():
    global _plugin
    _plugin.onHeartbeat()

    # Generic helper functions
def DumpConfigToLog():
    for x in Parameters:
        if Parameters[x] != "":
            Domoticz.Debug( "'" + x + "':'" + str(Parameters[x]) + "'")
    Domoticz.Debug("Device count: " + str(len(Devices)))
    for x in Devices:
        Domoticz.Debug("Device:           " + str(x) + " - " + str(Devices[x]))
        Domoticz.Debug("Device ID:       '" + str(Devices[x].ID) + "'")
        Domoticz.Debug("Device Name:     '" + Devices[x].Name + "'")
        Domoticz.Debug("Device nValue:    " + str(Devices[x].nValue))
        Domoticz.Debug("Device sValue:   '" + Devices[x].sValue + "'")
        Domoticz.Debug("Device LastLevel: " + str(Devices[x].LastLevel))
    return
Attachments
Presence.zip
(15.49 KiB) Downloaded 27 times

hjzwiers
Posts: 1
Joined: Friday 12 January 2018 9:26
Target OS: Raspberry Pi
Domoticz version:
Contact:

Re: Python plugin: Presence detection from wireless router

Post by hjzwiers » Friday 12 January 2018 9:34

I like the concept of the plugin.

I have installed this plugin, can ssh (from /home/PI/ ) with [email protected]_adress to the router (also a AC68U) without password, but in the Domoticz logging I only see the plugin start, no errors. On the router log I see the connection open en close, no errors.

Attention point. At first the plug failed because the MAC adres is case sensitive, use the same case as the router sends. Now working fine!

Post Reply

Who is online

Users browsing this forum: No registered users and 3 guests