def Sanitise(val): # Assume val is a string containing a hour:minute time timeOfDay = MakeTime(val) if timeOfDay == None: synopsis.problem("TimedRule", "Bad time in Rule containing '" + val) return None # Must return something return "\"" + timeOfDay.strftime( "%H:%M") + "\"" # Normalise timestamp (cope with leading zeros)
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 EventHandler(eventId, eventArg): global owm, apiKey, location if eventId == events.ids.HOURS: # Get weather once/hour if owm == None: apiKey = config.Get("owmApiKey") location = config.Get("owmLocation") if (apiKey != None and location != None): owm = pyowm.OWM(apiKey) # My API key try: obs = owm.weather_at_place(location) # My location except: database.NewEvent(0, "Weather Feed failed!") synopsis.problem("Weather", "Feed failed @ " + str(datetime.now())) return w = obs.get_weather() cloudCover = w.get_clouds() # Percentage cloud cover variables.Set("cloudCover", str(cloudCover), True) outsideTemp = w.get_temperature("celsius")["temp"] # Outside temperature in celsius variables.Set("outsideTemperature", str(outsideTemp), True) windSpeed = w.get_wind()["speed"] variables.Set("windSpeed", str(windSpeed), True) rain = w.get_rain() if rain != {}: rain = 1 # was rain["3h"] # Rain volume in last 3 hours. Unknown units, may be ml(?) else: rain = 0 # No rain variables.Set("rain", str(rain), True) snow = w.get_snow() if snow != {}: snow = 1 # was snow["3h"] # Snow volume in last 3 hours. Unknown units, may be ml(?) else: snow = 0 # No snow variables.Set("snow", str(snow), True) database.NewEvent(0, "Weather now "+str(cloudCover)+"% cloudy") events.Issue(events.ids.WEATHER) # Tell system that we have a new weather report
def NoteMsgDetails(devKey, arg): devIndex = GetIndexFromKey(devKey) if devIndex == None: synopsis.problem("Unknown device:",str(devKey)) return if arg[0] == expRsp[devIndex]: expRsp[devIndex] = None # Note that we've found the expected response now, so we're now clear to send presence.Set(devKey) # Note presence, and update radio quality if isnumeric(arg[-2]): if int(arg[-2]) < 0: # Assume penultimate item is RSSI, and thus that ultimate one is LQI rssi = arg[-2] lqi = arg[-1] try: signal = int((int(lqi, 16) * 100) / 255) # Convert 0-255 to 0-100. Ignore RSSI for now except ValueError: signal = -1 # Cannot get signal if signal != -1: entry = database.GetLatestLoggedItem(devKey, "SignalPercentage") if entry != None: oldSignal = entry[0] # Just the value fmt = "%Y-%m-%d %H:%M:%S" oldTimestamp = datetime.strptime(entry[1], fmt) if oldSignal == None: oldSignal = signal + 100 # Force an update if no previous signal deltaSignal = signal - oldSignal deltaTime = datetime.now() - oldTimestamp if abs(deltaSignal) > 5: if deltaTime.seconds > 600: # Only note signal level that's different enough and at least 10 minutes since last one database.LogItem(devKey, "SignalPercentage", signal) else: # signal is sufficiently similar to last one, so update timestamp database.RefreshLoggedItem(devKey, "SignalPercentage") # Update timestamp to avoid too many >10 minutes! arg.remove(rssi) arg.remove(lqi)
def ParseCondition(ruleConditionList, trigger): #log.debug("Parsing: "+" ".join(ruleConditionList)) subAnswers = "True" for condition in ruleConditionList[1:]: if condition == "and": subAnswers = subAnswers + " and " # Note surrounding spaces, for python eval() elif condition == "or": subAnswers = subAnswers + " or " elif "<time<" in condition: sep = condition.index("<time<") # Handle time here nowTime = datetime.strptime(datetime.now().strftime("%H:%M"), "%H:%M") startTime = iottime.Get(condition[:sep]) endTime = iottime.Get(condition[sep + 6:]) if endTime < startTime: # Handle midnight-crossing here... almostMidnight = datetime.strptime("23:59", "%H:%M") midnight = datetime.strptime("0:00", "%H:%M") if nowTime > datetime.strptime( "12:00", "%H:%M"): # After midday but before midnight subAnswers = subAnswers + str( iottime.IsTimeBetween(startTime, nowTime, almostMidnight)) else: # Assume after midnight subAnswers = subAnswers + str( iottime.IsTimeBetween(midnight, nowTime, endTime)) else: # Doesn't involve midnight subAnswers = subAnswers + str( iottime.IsTimeBetween(startTime, nowTime, endTime)) elif "<=" in condition: subAnswers = subAnswers + str(GetConditionResult("<=", condition)) elif ">=" in condition: subAnswers = subAnswers + str(GetConditionResult(">=", condition)) elif "<" in condition: subAnswers = subAnswers + str(GetConditionResult("<", condition)) elif ">" in condition: subAnswers = subAnswers + str(GetConditionResult(">", condition)) elif "==" in condition: subAnswers = subAnswers + str(GetConditionResult("==", condition)) # End of loop if subAnswers != "": #log.debug("About to evaluate:'"+subAnswers+"'") try: finalAnswer = eval(subAnswers) except: # Catch all errors that the rule might raise err = sys.exc_info()[0] synopsis.problem( "BadRule", "Bad rule : '" + join(ruleConditionList) ) # Make a note in the status page so the user can fix the rule finalAnswer = False # Say rule failed return finalAnswer else: return False # Empty string is always False
def EventHandler(eventId, eventArg): global sckLst, sck, cliSck if eventId == events.ids.INIT: sck = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # Create socket sck.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sck.setblocking(0) # accept() is no longer blocking port = 12345 try: sck.bind(('', port)) # Listen on all available interfaces except OSError as err: # "OSError: [Errno 98] Address already in use" database.NewEvent(0, "Socket bind failed with " + err.args[1]) # 0 is always hub vesta.Reboot() sck.listen(0) sckLst = [sck] if eventId == events.ids.SECONDS: if select.select( [sys.stdin], [], [], 0)[0]: # Read from stdin (for working with the console) cmd = sys.stdin.readline() if cmd: Commands().onecmd(cmd) rd, wr, er = select.select( sckLst, [], [], 0) # Read from remote socket (for working with web pages) for s in rd: if s is sck: cliSck, addr = sck.accept() sckLst.append(cliSck) #log.debug("New connection from web page!") else: try: cmd = cliSck.recv(100) except OSError as err: # OSError: [Errno 9] Bad file descriptor" synopsis.problem("Web command failed with ", err.args[1]) cmd = "" # No command if there was a failure if cmd: cmd = cmd.decode() log.debug("Got cmd \"" + cmd + "\" from web page") sys.stdout = open("cmdoutput.txt", "w") # Redirect stdout to file Commands().onecmd(cmd) sys.stdout = sys.__stdout__ # Put stdout back to normal (will hopefully also close the file) f = open("cmdoutput.txt", "r") cmdOut = f.read() cliSck.send(str.encode(cmdOut)) call("rm cmdoutput.txt", shell=True) # Remove cmd output after we've used it
def GetConditionResult(test, condition): sep = condition.index( test) # Assume this has already been shown to return a valid answer tstVal = condition[sep + len(test):] # Simple value must be on right varName = condition[:sep] # Variable must be on the left of the expression if "-" in varName: variables.GetVal(varName, "-") elif "+" in varName: variables.GetVal(varName, "+") else: varVal = variables.Get(varName) if varVal != None: if isNumber(tstVal): varVal = str(varVal) tstVal = str(tstVal) elif ":" in tstVal: varVal = iottime.Sanitise( varVal ) # Ensure timestamps are consistently formatted before comparing (to avoid "0:15" != "00:15") tstVal = iottime.Sanitise(tstVal) log.debug("Time test checks " + varVal + " against " + tstVal) else: varVal = "'" + varVal.lower() + "'" tstVal = "'" + tstVal.lower( ) + "'" # Surround strings with quotes to make string comparisons work (Tuesday==Tuesday fails, although 'Tuesday'=='Tuesday' works) condStr = varVal + test + tstVal try: answer = eval(condStr) except: synopsis.problem("BadRule", "Failed to evaluate '" + condition + "'") log.debug("Failed to evaluate '" + condition + "'") answer = False # Default answer to allow rest of rules to continue to run return answer else: return False # If we couldn't find the item requested, assume the condition fails(?)
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 SetAttrVal(devKey, clstrId, attrId, value): global msp_ota if clstrId == zcl.Cluster.PowerConfig and attrId == zcl.Attribute.Batt_Percentage: SetTempVal(devKey, "GetNextBatteryAfter", datetime.now() + timedelta(seconds=86400)) # Ask for battery every day if value != "FF": try: varVal = int( int(value, 16) / 2 ) # Arrives in 0.5% increments, but drop fractional component except ValueError: varVal = None if varVal != None: log.debug("Battery is " + str(varVal) + "%. Get next reading at " + str(GetTempVal(devKey, "GetNextBatteryAfter"))) database.LogItem(devKey, "BatteryPercentage", varVal) # For web page lowBatt = int(config.Get("lowBattery", "5")) if varVal < lowBatt: devName = database.GetDeviceItem(devKey, "userName") synopsis.problem( devName + "_batt", devName + " low battery (" + str(varVal) + "%)") if clstrId == zcl.Cluster.Temperature and attrId == zcl.Attribute.Celsius: if value != "FF9C" and value != "8000": # Don't know where this value (of -100) comes from, but seems to mean "Illegal temp", although it should be -1'C try: varVal = int(value, 16) / 100 # Arrives in 0.01'C increments database.LogItem(devKey, "TemperatureCelsius", varVal) # For web page except ValueError: log.debug("Bad temperature of " + value) if clstrId == zcl.Cluster.OnOff and attrId == zcl.Attribute.OnOffState: if isnumeric(value, 16): oldState = database.GetLatestLoggedValue(devKey, "State") if int(value, 16) == 0: newState = "SwitchOff" else: newState = "SwitchOn" if oldState != newState: database.UpdateLoggedItem( devKey, "State", newState) # So that we can access it from the rules later database.NewEvent(devKey, newState) Rule(devKey, newState) expectedState = GetTempVal(devKey, "ExpectOnOff") if expectedState != None: if newState != expectedState: if expectedState == "SwitchOn": devcmds.SwitchOn(devKey) # Re-issue command else: # Assume SwitchOff devcmds.SwitchOff(devKey) # Re-issue command else: # We got the expected result DelTempVal(devKey, "ExpectOnOff") if clstrId == zcl.Cluster.Time and attrId == zcl.Attribute.LocalTime: if isnumeric(value, 16): varVal = int(value, 16) # Arrives in Watts, so store it in the same way log.debug("Raw time:" + str(varVal)) timeStr = iottime.FromZigbee(varVal) log.debug("Human time:" + timeStr) database.UpdateLoggedItem(devKey, "Time", timeStr) # Just store latest time string if clstrId == zcl.Cluster.SimpleMetering and attrId == zcl.Attribute.InstantaneousDemand: if isnumeric(value, 16): varVal = int(value, 16) # Arrives in Watts, so store it in the same way inClstr = database.GetDeviceItem( devKey, "inClusters" ) # Assume we have a list of clusters if we get this far if zcl.Cluster.OnOff not in inClstr: # Thus device is powerclamp (has simplemetering but no OnOff) database.UpdateLoggedItem( devKey, "State", str(varVal) + "W" ) # So that we can access it from the rules later, or show it on the web database.UpdateLoggedItem(devKey, "PowerReadingW", varVal) # Just store latest reading if clstrId == zcl.Cluster.SimpleMetering and attrId == zcl.Attribute.CurrentSummationDelivered: if isnumeric(value, 16): varVal = int( value, 16 ) # Arrives in accumulated WattHours, so store it in the same way database.LogItem(devKey, "EnergyConsumedWh", varVal) if clstrId == zcl.Cluster.IAS_Zone and attrId == zcl.Attribute.Zone_Type: database.SetDeviceItem(devKey, "iasZoneType", value) if clstrId == zcl.Cluster.Basic: if attrId == zcl.Attribute.Model_Name: database.SetDeviceItem(devKey, "modelName", value) if attrId == zcl.Attribute.Manuf_Name: database.SetDeviceItem(devKey, "manufName", value) if clstrId == zcl.Cluster.OTA or clstrId == msp_ota: if attrId == zcl.Attribute.firmwareVersion: database.SetDeviceItem(devKey, "firmwareVersion", value) if clstrId == zcl.Cluster.PollCtrl: if attrId == zcl.Attribute.LongPollIntervalQs: varVal = str(float(int(value, 16) / 4)) # Value arrives in units of quarter seconds database.SetDeviceItem( devKey, "longPollInterval", varVal ) # For web page and also to see whether to wait for CheckIn or just send messages (if <6 secs) if clstrId == zcl.Cluster.Thermostat: if attrId == zcl.Attribute.LocalTemp: if isnumeric(value, 16): varVal = int(value, 16) / 100 # Arrives in 0.01'C increments database.LogItem(devKey, "SourceCelsius", varVal) # For web page src = varVal tgt = database.GetLatestLoggedValue(devKey, "TargetCelsius") database.UpdateLoggedItem( devKey, "State", "source " + str(src) + "'C/target " + str(tgt) + "'C") # So that we can show it on the web if attrId == zcl.Attribute.OccupiedHeatingSetPoint: if isnumeric(value, 16): varVal = int(value, 16) / 100 # Arrives in 0.01'C increments database.LogItem(devKey, "TargetCelsius", varVal) # For web page tgt = varVal src = database.GetLatestLoggedValue(devKey, "SourceCelsius") database.UpdateLoggedItem( devKey, "State", "source " + str(src) + "'C/target " + str(tgt) + "'C") # So that we can show it on the web if clstrId == zcl.Cluster.Time: if attrId == zcl.Attribute.Time: if isnumeric(value, 16): varVal = int(value, 16) # Arrives in seconds since 1st Jan 2000 timeStamp = iottime.FromZigbee(varVal) database.LogItem(devKey, "time", str(timeStamp)) # For web page