Пример #1
0
def CommandDev(action, devKey, actList, ruleId):
    if devKey == None:
        log.fault("Device " + actList[1] + " from rules not found in devices")
        synopsis.problem("rules", "Unknown device " + actList[1] + " in rules")
    else:
        devices.DelTempVal(
            devKey, "SwitchOff@")  # Kill any extant timers for this device
        if action == "SwitchOn".lower():
            database.NewEvent(devKey, "SwitchOn", "Rule:" + str(ruleId))
            devcmds.SwitchOn(devKey)
            if len(actList) > 3:
                if actList[2] == "for":
                    SetOnDuration(devKey, int(actList[3], 10))
        elif action == "SwitchOff".lower():
            database.NewEvent(devKey, "SwitchOff", "Rule:" + str(ruleId))
            devcmds.SwitchOff(devKey)
        elif action == "Toggle".lower():
            #database.NewEvent(devKey, "Toggle", "Rule:"+str(ruleId)) # Removed, otherwise Toggle function can't work out old state!
            devcmds.Toggle(devKey)
        elif action == "Dim".lower() and actList[2] == "to":
            database.NewEvent(devKey, "Dim", "Rule:" + str(ruleId))
            devcmds.Dim(devKey, float(actList[3]))
            if len(actList) > 5:
                if actList[4] == "for":
                    SetDimDuration(devKey, int(actList[5], 10))
        elif action == "HueSat".lower(
        ):  # Syntax is "do HueSat <Hue in degrees>,<fractional saturation>
            database.NewEvent(devKey, "HueSat", "Rule:" + str(ruleId))
            devcmds.Colour(devKey, int(actList[3], 10), float(actList[4]))
        else:
            log.debug("Unknown action: " + action + " for device: " +
                      actList[1])
Пример #2
0
def GetIdx(devId):
    idx = GetIdxFromItem("devId", devId)
    if idx != None:
        return idx
    else:
        log.fault("Unknown device " + devId)
        return None
Пример #3
0
def EventHandler(eventId, eventArg):
    global db, curs, flushDB
    if eventId == events.ids.PREINIT:
        db = sqlite3.connect(
            "vesta.db"
        )  # This will create a new database for us if it didn't previously exist
        curs = db.cursor()
        InitAll(db, curs)  # Add any new entries to database here
        Backup()
        flushDB = True  # Batch up the commits
    if eventId == events.ids.SECONDS:
        if flushDB:
            try:
                db.commit()  # Flush events to disk
                flushDB = False
            except:
                log.fault("Database couldn't commit")
    if eventId == events.ids.NEWDAY:
        Backup()
        FlushOldEvents(
        )  # Flush old events to avoid database getting too big and slow
        FlushOldLoggedItems()
        GarbageCollect("BatteryPercentage")
        GarbageCollect("TemperatureCelsius")
        GarbageCollect("SignalPercentage")
        #GarbageCollect("Presence") # Causes HallLight (devKey 1) to have its presence removed, although I can't work out why...
        GarbageCollect("PowerReadingW")
        GarbageCollect("EnergyConsumedWh")
        GarbageCollect("EnergyGeneratedWh")
        #GarbageCollect("Events") # As above for the same HallLight (devKey 1)
        Defragment(
        )  # Compact the database now that we've flushed the old items
        flushDB = True
    if eventId == events.ids.SHUTDOWN:
        db.commit()  # Flush events to disk prior to shutdown
Пример #4
0
def GetTarget(scheduleName):
    timeOfDay = datetime.now().strftime("%H:%M")
    dayOfWeek = iottime.GetDow(date.today().isoweekday() %
                               7)  # So that Sun=0, Mon=1, etc.
    scheduleStr = database.GetSchedule(scheduleName,
                                       dayOfWeek)  # Get schedule for today
    if scheduleStr == None:
        log.fault("No schedule found on " + str(dayOfWeek))
        return None
    log.debug("Resuming using schedule for today(" + str(dayOfWeek) + ") is " +
              scheduleStr)
    try:
        scheduleList = eval(scheduleStr)
    except:
        log.fault("Bad schedule - Can't turn " + scheduleStr + " into a list")
        return None  # Bad list from database
    target = 7  # Should really be the last scheduled temperature from the previous day's schedule
    numSetpoints = len(scheduleList)
    for index in range(0, numSetpoints):
        timeTemp = scheduleList[index]  # Get each time & temp from schedule
        timeStr = timeTemp[0]
        tempStr = timeTemp[1]
        #log.debug("Checking whether "+str(timeOfDay)+" is greater than "+str(timeStr))
        if iottime.MakeTime(timeOfDay) >= iottime.MakeTime(timeStr):
            target = tempStr  # Remember last target
    return target  # If we reach the end of today's schedule, then assume our time is after last change, so use last target
Пример #5
0
 def do_config(self, line):
     """config devKey field
     Tells app to use new reporting configuration field from database"""
     argList = line.split()
     if len(argList) >= 2:
         confField = argList[1]
         devKey = argList[0]
         if devKey != None:
             devices.Config(devKey, confField)
     else:
         log.fault("Insufficient Args")
Пример #6
0
 def do_identify(self, line):
     """identify name seconds
     Sends identify command to named device.  Use 0 seconds to stop immediately"""
     argList = line.split()
     if len(argList) >= 2:
         devKey = devices.FindDev(argList[0])
         if devKey != None:
             timeS = int(argList[1])
             devcmds.Identify(devKey, timeS)
     else:
         log.fault("Insufficient Args")
Пример #7
0
 def do_sat(self, line):
     """sat name sat
     Sends ColorCtrl command to named device, where 0<sat<100"""
     argList = line.split()
     if len(argList) >= 2:
         sat = int(argList[1])
         devKey = devices.FindDev(argList[0])
         if devKey != None:
             database.NewEvent(devKey, "Sat", "UICmd")
             devcmds.Sat(devKey, sat)
     else:
         log.fault("Insufficient Args")
Пример #8
0
 def do_hue(self, line):
     """hue name hue
     Sends ColorCtrl command to named device, where 0<hue<360"""
     argList = line.split()
     if len(argList) >= 2:
         hue = int(argList[1])
         devKey = devices.FindDev(argList[0])
         if devKey != None:
             database.NewEvent(devKey, "Hue", "UICmd")
             devcmds.Hue(devKey, hue)
     else:
         log.fault("Insufficient Args")
Пример #9
0
 def do_dim(self, line):
     """dim name percentage
     Sends level command to named device"""
     argList = line.split()
     if len(argList) >= 2:
         percentage = int(argList[1])
         devKey = devices.FindDev(argList[0])
         if devKey != None:
             database.NewEvent(devKey, "Dim", "UICmd")
             devcmds.Dim(devKey, percentage)
     else:
         log.fault("Insufficient Args")
Пример #10
0
def Action(actList):
    log.log("Action with: " + str(actList))
    action = actList[0]
    if action == "Log":
        log.log("Rule says Log event for " + ' '.join(actList[1:]))
    elif action == "Play":
        filename = "Sfx/" + actList[1]
        call(["omxplayer", "-o", "local", filename])
    elif action == "synopsis":  # First arg is email recipient
        emailBody = []
        #for items in devices.synopsis:
        #    emailBody.append(' '.join(items))  # Tuples are joined by spaces
        #cmdList = ["echo", "\""+'\n'.join(emailBody)+"\"", "|", "mail", "-s", "\"Update from IoT-Hub\"", actList[1]]
        #cmdStr = " ".join(cmdList)
        #call(cmdStr, shell=True)
        #devices.synopsis = []   # Ready to start a new synopsis mail now
    elif action == "email":  # First arg is recipient, remainder are body of the text.  Fixed subject
        emailBody = []
        for item in actList[2:]:
            emailBody.append(item)
        cmdList = [
            "echo", "\"" + ' '.join(emailBody) + "\"", "|", "mail", "-s",
            "\"Alert from IoT-Hub\"", actList[1]
        ]
        cmdStr = " ".join(cmdList)
        call(cmdStr, shell=True)
    else:  # Must be a command for a device
        devIdx = devices.GetDevIdxFromUserName(
            actList[1])  # Second arg is username for device
        if devIdx == None:
            log.fault("Device " + actList[1] +
                      " from rules.txt not found in devices")
        else:
            if action == "SwitchOn":
                devices.SwitchOn(devIdx)
                if len(actList) > 3:
                    if actList[2] == "for":
                        SetOnDuration(devIdx, int(actList[3], 10))
            elif action == "SwitchOff":
                devices.SwitchOff(devIdx)
            elif action == "Toggle":
                devices.Toggle(devIdx)
            elif action == "Dim" and actList[2] == "to":
                devices.Dim(devIdx, float(actList[3]))
                if len(actList) > 5:
                    if actList[4] == "for":
                        SetOnDuration(devIdx, int(actList[5], 10))
            else:
                log.log("Unknown action: " + action + " for device: " +
                        devices.GetUserNameFromDevIdx(devIdx))
Пример #11
0
 def do_dim(self, line):
     """dim name fraction
     Sends level command to named device"""
     argList = line.split()
     if len(argList) >= 2:
         devId = argList[0]
         fraction = float(argList[1])
         devIdx = devices.GetIdxFromUserName(name)  # Try name first
         if devIdx == None:
             devIdx = devices.GetIdx(name)  # Try devId if no name match
         if devIdx != None:
             devices.Dim(devIdx, fraction)
     else:
         log.fault("Insufficient Args")
Пример #12
0
 def do_name(self, line):
     """name devId name
     Allows user to associate friendly name with device Id"""
     argList = line.split()
     if argList != []:
         devId = argList[0]
         name = argList[1]
         if devId != None and name != None:
             devIdx = devices.GetIdx(devId)
             if devIdx != None:
                 devices.SetUserNameFromDevIdx(devIdx, name)
         else:
             log.fault("Need both args!")
     else:
         log.fault("Need devId and name!")
Пример #13
0
def EventHandler(eventId, eventArg):
    if eventId == events.ids.TRIGGER:
        devIdx = devices.GetIdx(
            eventArg[1])  # Lookup device from network address in eventArg[1]
        now = datetime.now()
        nowStr = now.strftime("%H:%M")
        userName = devices.GetUserNameFromDevIdx(devIdx)
        zoneType = devices.GetAttrVal(devIdx, zcl.Cluster.IAS_Zone,
                                      zcl.Attribute.Zone_Type)  # Device type
        if zoneType != None:
            #log.log("DevId: "+eventArg[1]+" has type "+ zoneType)
            if zoneType == zcl.Zone_Type.Contact:
                if int(eventArg[3], 16) & 1:  # Bottom bit indicates alarm1
                    if userName:
                        Run(userName + "==opened")  # See if rule exists
                    log.log("Door " + eventArg[1] + " opened")
                    devices.SetStatus(devIdx, "Other",
                                      "opened @ " + nowStr)  # For web page
                else:
                    if userName:
                        Run(userName + "==closed")  # See if rule exists
                    log.log("Door " + eventArg[1] + " closed")
                    devices.SetStatus(devIdx, "Other",
                                      "closed @ " + nowStr)  # For web page
            elif zoneType == zcl.Zone_Type.PIR:
                if userName:
                    Run(userName + "==active")  # See if rule exists
                log.log("PIR " + eventArg[1] + " active")
                devices.SetStatus(devIdx, "Other",
                                  "active @ " + nowStr)  # For web page
            else:
                log.log("DevId: " + eventArg[1] + " zonestatus " + eventArg[3])
        else:
            log.fault("Unknown IAS device type for devId " + eventArg[1])
    elif eventId == events.ids.BUTTON:
        devIdx = devices.GetIdx(
            eventArg[1])  # Lookup device from network address in eventArg[1]
        now = datetime.now()
        nowStr = now.strftime("%H:%M")
        userName = devices.GetUserNameFromDevIdx(devIdx)
        log.log("Button " + eventArg[1] + " " + eventArg[0]
                )  # Arg[0] holds "ON", "OFF" or "TOGGLE" (Case might be wrong)
        devices.SetStatus(devIdx, "Other",
                          "pressed @ " + nowStr)  # For web page
        if userName:
            Run(userName + "==" + eventArg[0])  # See if rule exists
Пример #14
0
def Run(
    trigger
):  # Run through the rules looking to see if we have a match for the trigger
    rulesFile = Path(rulesFilename)
    if rulesFile.is_file():
        with open(rulesFilename) as rules:
            log.log("Running rule: " + trigger)
            if "==" in trigger:
                sep = trigger.index("==")
                triggerType = trigger[:sep]
                triggerVal = trigger[sep + 2:]
                variables.Set(triggerType, triggerVal)
            for line in rules:
                rule = ' '.join(
                    line.split()
                )  # Compact multiple spaces into single ones and make each line into a rule
                ruleList = rule.split(" ")  # Make each rule into a list
                if ruleList[0] == "if":
                    doIndex = FindItemInList("do",
                                             ruleList)  # Safely look for "do"
                    if doIndex != None:
                        if ParseCondition(
                                ruleList[1:doIndex], trigger
                        ) == True:  # Parse condition from element 1 to "do"
                            Action(ruleList[doIndex + 1:])  # Do action
                    # else skip rest of line
                elif ruleList[0] == "do":
                    Action(ruleList[1:])
                # else assume the line is a comment and skip it
            # end of rules
            variables.Del(
                triggerType)  # Make sure we don't re-run the same trigger
    else:
        shell.exec("touch " + rulesFilename)
        shell.exec("chmod 666 " + rulesFilename)
        log.fault("Made new " + rulesFilename + " !")
Пример #15
0
def EventHandler(eventId, eventArg):
    if eventId == events.ids.TRIGGER:
        devKey = devices.GetKey(
            eventArg[1])  # Lookup device from network address in eventArg[1]
        if devKey != None:
            devices.NoteMsgDetails(devKey, eventArg)  # Note presence
            now = datetime.now()
            nowStr = now.strftime("%H:%M")
            zoneType = database.GetDeviceItem(devKey,
                                              "iasZoneType")  # Device type
            if zoneType != None:
                oldState = database.GetLatestEvent(devKey)
                if zoneType == zcl.Zone_Type.Contact:
                    if int(eventArg[3], 16) & 1:  # Bottom bit indicates alarm1
                        newState = "opened"
                    else:
                        newState = "closed"
                    if oldState != newState:  # NB Might get same state if sensor re-sends, or due to battery report
                        database.UpdateLoggedItem(
                            devKey, "State", newState
                        )  # So that we can access it from the rules later
                        database.NewEvent(
                            devKey, newState
                        )  # For web page.  Only update event log when state changes
                        DeviceRun(devKey, "==" + newState
                                  )  # See if rule exists (when state changes)
                        #log.debug("Door "+ eventArg[1]+ " "+newState)
                elif zoneType == zcl.Zone_Type.PIR:
                    if int(eventArg[3], 16) & 1:  # Bottom bit indicates alarm1
                        newState = "active"
                        devices.SetTempVal(
                            devKey, "PirInactive@",
                            datetime.now() + timedelta(seconds=300))
                    else:
                        newState = "inactive"  # Might happen if we get an IAS battery report
                    if oldState != newState:
                        database.UpdateLoggedItem(
                            devKey, "State", newState
                        )  # So that we can access it from the rules later
                        database.NewEvent(
                            devKey, newState
                        )  # For web page.  Only update event log when state changes
                    DeviceRun(devKey, "==" + newState)  # See if rule exists
                else:
                    log.debug("DevId: " + eventArg[1] + " zonestatus " +
                              eventArg[3])
            else:
                log.fault("Unknown IAS device type for devId " + eventArg[1])
        else:  # devKey == None
            telegesis.Leave(
                eventArg[1]
            )  # Tell device to leave the network, since we don't know anything about it
    elif eventId == events.ids.BUTTON:
        devKey = devices.GetKey(
            eventArg[1])  # Lookup device from network address in eventArg[1]
        if devKey != None:
            #log.debug("Button "+ eventArg[1]+ " "+eventArg[0]) # Arg[0] holds "ON", "OFF" or "TOGGLE" (Case might be wrong)
            database.NewEvent(devKey, "pressed")  # For web page
            DeviceRun(devKey, "==" + eventArg[0])  # See if rule exists
        else:  # devKey == None
            telegesis.Leave(
                eventArg[1]
            )  # Tell device to leave the network, since we don't know anything about it
    elif eventId == events.ids.MULTISTATE:
        devKey = devices.GetKey(
            eventArg[1])  # Lookup device from network address in eventArg[1]
        if devKey != None:
            database.NewEvent(devKey,
                              "MultiState==" + eventArg[0])  # For web page
            DeviceRun(devKey, "==" + eventArg[0])  # See if rule exists
        else:  # devKey == None
            telegesis.Leave(
                eventArg[1]
            )  # Tell device to leave the network, since we don't know anything about it
Пример #16
0
def Action(actList, ruleId):
    log.debug("Action with: " + str(actList))
    action = actList[0].lower()
    if action == "Log".lower():
        log.debug("Rule says Log event for " + ' '.join(actList[1:]))
    elif action == "Play".lower():
        call(["omxplayer", "-o", actList[1], actList[2]])
    elif action == "Event".lower():
        if actList[1].lower() == "TimeOfDay".lower():
            events.IssueEvent(events.ids.TIMEOFDAY, actList[2])
        elif actList[1].lower() == "Alarm".lower():
            events.IssueEvent(events.ids.ALARM, actList[2])
        # Could have other events here...
    elif action == "synopsis":  # Was status
        emailAddress = config.Get("emailAddress")
        log.debug("About to send synopsis to " + emailAddress)
        if emailAddress != None:
            synopsis.BuildPage()  # Create synopsis page on demand
            with open("synopsis.txt", "r") as fh:  # Plain text of email
                emailText = fh.readlines()
            text = ''.join(emailText)
            with open("synopsis.html", "r") as fh:  # HTML of email
                emailHtml = fh.readlines()
            html = ''.join(emailHtml)
            sendmail.email("Vesta Status", text, html)  # See sendmail.py
        else:
            synopsis.problem(
                "NoEmail",
                "No emailAddress entry in config, needed to send synopsis")
    elif action == "email":  # All args are body of the text.  Fixed subject and email address
        emailAddress = config.Get("emailAddress")
        if emailAddress != None:
            emailBody = []
            for item in actList[1:]:
                emailBody.append(item)
            plainText = " ".join(emailBody)
            log.debug("Sending email with '" + plainText + "'")
            result = sendmail.email("Vesta Alert!", plainText, None)
            if result != 0:
                synopsis.problem(
                    "Email", "sendmail.email() failed with code " +
                    str(result) + " when trying to send:" + plainText)
        else:
            synopsis.problem("NoEmail", "No emailAddress entry in config")
    elif action == "override":  # Syntax is "Override <targetDevice> <targetDegC> <durationSecs>"
        devKey = devices.FindDev(actList[1])
        target = actList[2]
        timeSecs = actList[3]
        if devKey != None:
            schedule.Override(devKey, target, timeSecs)
    elif action == "set":  # Set a named variable to a value
        expression = "".join(
            actList[1:]
        )  # First recombine actList[1] onwards, with no spaces.  Now expression should be of the form "<var>=<val>"
        if "--" in expression:
            sep = expression.index("--")
            varName = expression[:sep]
            varVal = variables.Get(varName)
            if isNumber(varVal):
                newVal = str(eval(varVal + "-1"))
                variables.Set(varName, newVal)
                Run(
                    varName + "==" + newVal
                )  # Recurse! to see if any rules need running now that we've set a variable
            else:
                log.fault(varName + " not a number at " + expression)
        elif "++" in expression:
            sep = expression.index("++")
            varName = expression[:sep]
            varVal = variables.Get(varName)
            if isNumber(varVal):
                newVal = str(eval(varVal + "+1"))
                variables.Set(varName, newVal)
                Run(
                    varName + "==" + newVal
                )  # Recurse! to see if any rules need running now that we've set a variable
            else:
                log.fault(varName + " not a number at " + expression)
        elif "=" in expression:
            sep = expression.index("=")
            varName = expression[:sep]
            varVal = expression[sep + 1:]
            variables.Set(varName, varVal)
            Run(
                varName + "==" + varVal
            )  # Recurse! to see if any rules need running now that we've set a variable
        else:
            log.fault("Badly formatted rule at " + expression)
    elif action == "unset":  # Remove a named variable
        variables.Del(actList[1])
    else:  # Must be a command for a device, or group of devices
        if len(actList) >= 2:  # Check that we have a second arg...
            name = actList[1]  # Second arg is name
            if database.IsGroupName(name):  # Check if name is a groupName
                devKeyList = GetGroupDevs(name)
                for devKey in devKeyList:
                    CommandDev(action, devKey, actList,
                               ruleId)  # Command each device in list
            else:
                devKey = database.GetDevKey("userName", name)
                CommandDev(action, devKey, actList,
                           ruleId)  # Command one device
Пример #17
0
def problem(key, value):
    issues[key] = value  # Add new, or update old, dictionary entry
    log.fault(key + ":" + value)
Пример #18
0
def EventHandler(eventId, eventArg):
    global info, dirty, globalDevIdx, pendingBinding, pendingRptAttrId, statusUpdate
    if eventId == events.ids.INIT:
        try:
            with open(devFilename, "r") as f:
                try:
                    info = eval(
                        f.read())  # Load previous cache of devices into info[]
                    log.log("Loaded list from file")
                except:
                    log.fault("Unusable device list from file - discarding!")
                    info = []
        except OSError:
            info = []
        dirty = False  # Info[] is initialised now
        devIdx = 0
        database.ClearDevices()
        rowId = database.NewDevice("0000")
        database.UpdateDevice(rowId, "UserName", "Hub")
        for devices in info:
            ephemera.append([])  # Initialise parallel ephemeral device list
            InitDevStatus(devIdx)  # Initialise parallel device status
            CopyDevToDB(devIdx)
            SetTempVal(
                devIdx, "LastSeen", datetime.now()
            )  # Mark it as "seen at start up" so we don't start looking for it immediately
            devIdx = devIdx + 1
        CheckAllAttrs()  #  Set up any useful variables for the loaded devices
    if eventId == events.ids.DEVICE_ANNOUNCE:
        devId = eventArg[2]
        devIdx = GetIdx(devId)
        if devIdx == None:  # Which will only be the case if this device is actually new, but it may have just reset and announced
            devIdx = InitDev(devId)
            SetUserNameFromDevIdx(
                devIdx, "(New) " +
                devId)  # Default username of network ID, since that's unique
            SetVal(devIdx, "DevType", eventArg[0])  # SED, FFD or ZED
            SetVal(devIdx, "EUI", eventArg[1])
            if eventArg[0] == "SED":
                SetTempVal(devIdx, "PollingUntil",
                           datetime.now() + timedelta(seconds=300))
        else:
            NoteEphemera(devIdx, eventArg)
    if eventId == events.ids.CHECKIN:  # See if we have anything to ask the device...
        endPoint = eventArg[2]
        seq = "00"  # was seq = eventArg[3], but that's the RSSI
        devIdx = GetIdx(eventArg[1])
        if devIdx != None:
            NoteEphemera(devIdx, eventArg)
            if GetVal(devIdx, "EP") == None:
                SetVal(
                    devIdx, "EP", endPoint
                )  # Note endpoint that CheckIn came from, unless we already know this
            devId = GetVal(devIdx, "devId")
            cmdRsp = Check(
                devIdx, False
            )  # Check to see if we want to know anything about the device
            if cmdRsp != None:
                log.log("Want to know " + str(cmdRsp))
                telegesis.TxCmd([
                    "AT+RAWZCL:" + devId + "," + endPoint + ",0020,11" + seq +
                    "00012800", "OK"
                ])  # Tell device to enter Fast Poll for 40qs (==10s)
                SetTempVal(devIdx, "PollingUntil",
                           datetime.now() + timedelta(seconds=10))
                telegesis.TxCmd(
                    cmdRsp
                )  # This will go out after the Fast Poll Set - but possibly ought to go out as part of SECONDS handler..?
            else:
                #log.log("Don't want to know anything about "+GetUserNameFromDevIdx(devIdx))
                telegesis.TxCmd([
                    "AT+RAWZCL:" + devId + "," + endPoint + ",0020,11" + seq +
                    "00000100", "OK"
                ])  # Tell device to stop Poll
    if eventId == events.ids.RXMSG:
        if eventArg[0] == "AddrResp" and eventArg[1] == "00":
            devIdx = GetIdx(eventArg[2])
            if devIdx != None:
                SetVal(devIdx, "EUI", eventArg[3])
        elif eventArg[0] == "ActEpDesc":
            if "00" == eventArg[2]:
                devIdx = GetIdx(eventArg[1])
                if devIdx != None:
                    SetVal(devIdx, "EP", eventArg[3])  # Note first endpoint
        elif eventArg[0] == "SimpleDesc":
            if "00" == eventArg[2]:
                globalDevIdx = GetIdx(
                    eventArg[1]
                )  # Is multi-line response, so expect rest of response and use this global index until it's all finished
            elif "82" == eventArg[2]:  # 82 == Invalid endpoint
                devIdx = GetIdx(eventArg[1])
                DelVal(devIdx, "EP")
                events.Issue(events.ids.RXERROR, int(
                    eventArg[2],
                    16))  # Tell system that we're aborting this command
        elif eventArg[0] == "InCluster":
            if globalDevIdx != None:
                SetVal(globalDevIdx, "InCluster",
                       eventArg[1:])  # Store whole list from arg[1] to arg[n]
        elif eventArg[0] == "OutCluster":
            if globalDevIdx != None:
                NoteEphemera(globalDevIdx, eventArg)
                SetVal(globalDevIdx, "OutCluster",
                       eventArg[1:])  # Store whole list from arg[1] to arg[n]
            globalDevIdx = None  # We've finished with this global for now
        if eventArg[0] == "RESPATTR":
            devIdx = GetIdx(eventArg[1])
            if devIdx != None:
                NoteEphemera(devIdx, eventArg)
                ep = eventArg[2]
                clusterId = eventArg[3]
                attrId = eventArg[4]
                if "00" == eventArg[5]:
                    attrVal = eventArg[6]
                else:
                    attrVal = "Error:" + eventArg[
                        5]  # So that we don't ask for the same attribute again later
                SetAttrVal(devIdx, clusterId, attrId, attrVal)
        if eventArg[0] == "REPORTATTR":
            devIdx = GetIdx(eventArg[1])
            if devIdx != None:
                ep = eventArg[2]
                clusterId = eventArg[3]
                attrId = eventArg[4]
                attrType = eventArg[5]
                attrVal = eventArg[6]
                NoteEphemera(devIdx, eventArg)
                SetAttrVal(devIdx, clusterId, attrId, attrVal)
                reporting = GetVal(
                    devIdx, "Reporting"
                )  # See if we're expecting this report, and note it in the reporting table
                if reporting != None:
                    newRpt = clusterId + ":" + attrId
                    if newRpt not in reporting:
                        reporting.append(newRpt)
                        SetVal(devIdx, "Reporting", reporting)
                else:
                    SetVal(devIdx, "Reporting", [])  # Ready for next time
        if eventArg[0] == "Bind":  # Binding Response from device
            devIdx = GetIdx(eventArg[1])
            if devIdx != None:
                if pendingBinding != None:
                    binding = GetVal(devIdx, "Binding")
                    binding.append(pendingBinding)
                    SetVal(devIdx, "Binding", binding)
                    pendingBinding = None
        if eventArg[0] == "CFGRPTRSP":  # Configure Report Response from device
            devIdx = GetIdx(eventArg[1])
            status = eventArg[4]
            if devIdx != None and status == "00":
                clusterId = eventArg[3]
                attrId = pendingRptAttrId  # Need to remember this, since it doesn't appear in CFGRPTRSP
                reporting = GetVal(devIdx, "Reporting")
                newRpt = clusterId + ":" + attrId
                if newRpt not in reporting:
                    reporting.append(newRpt)
                    SetVal(devIdx, "Reporting", reporting)
    if eventId == events.ids.BUTTON:
        devIdx = GetIdx(
            eventArg[1])  # Lookup device from network address in eventArg[1]
        NoteEphemera(devIdx, eventArg)
    if eventId == events.ids.RXERROR:
        globalDevIdx = None  # We've finished with this global if we get an error
    if eventId == events.ids.SECONDS:
        SendPendingCommand()
        for devIdx, devInfo in enumerate(
                info
        ):  # See if any devices are timing out, and turn them off if necessary
            offAt = GetVal(devIdx, "SwitchOff@")
            if offAt:
                if now >= offAt:
                    SwitchOff(devIdx)
        if dirty:
            with open(devFilename, 'wt') as f:
                pprint(info, stream=f)
                # Was print(info, file=f) # Save devices list directly to file
            dirty = False  # Don't save again until needed
        if statusUpdate:
            SaveStatus()
            statusUpdate = False
    if eventId == events.ids.MINUTES:
        SaveStatus()  # For app UpTime
        CheckPresence()  # For all devices