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])
def GetIdx(devId): idx = GetIdxFromItem("devId", devId) if idx != None: return idx else: log.fault("Unknown device " + devId) return None
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
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
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")
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")
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")
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")
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")
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))
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")
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!")
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
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 + " !")
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
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
def problem(key, value): issues[key] = value # Add new, or update old, dictionary entry log.fault(key + ":" + value)
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