def Parse(atLine): global expectOurEui global ourChannel, ourPowLvl, ourPan, ourExtPan if atLine == "": return # Ignore blank lines if atLine[0:2] == 'AT': return # Exit immediately if line starts with AT, indicating an echo log.debug("Parsing:"+ atLine) atLine = atLine.replace(':',',') # Replace colons with commas atList = atLine.split(',') # Split on commas if atList[0] == 'OK': events.Issue(events.ids.RXOK) elif expectOurEui == True and len(atList[0])==16: # If we're expecting an EUI and it's the correct length database.SetDeviceItem(0, "eui64", atList[0]) expectOurEui = False elif atList[0] == "ERROR": errNo = None if len(atList) > 1: try: errNo = int(atList[1],16) # Might be junk - have seen all sorts of nonsense from Telegesis stick here except ValueError: pass # Just use default value of errNo events.Issue(events.ids.RXERROR, errNo) # See if anyone cares about the error elif atList[0] == "SED" or atList[0] == "FFD" or atList[0] == "ZED": events.Issue(events.ids.DEVICE_ANNOUNCE, atList) # Tell system that device has (re-)joined elif atList[0] == 'CHECKIN': events.Issue(events.ids.CHECKIN, atList) # Tell devices that device is about to poll for data elif atList[0] == 'TOGGLE' or atList[0] == 'ON' or atList[0] == 'OFF': events.Issue(events.ids.BUTTON, atList) # Tell rules engine elif atList[0] == 'ZONESTATUS': events.Issue(events.ids.TRIGGER, atList) # Tell rules engine elif 0 == atList[0].find("Telegesis"): database.SetDeviceItem(0, "manufName", atList[0]) elif 0 == atList[0].find("CICIE"): database.SetDeviceItem(0, "modelName", atList[0]) expectOurEui = True # Following EUI has no prefix - we just have to know that it follows the CICIE line elif atList[0] == "+N=COO": ourChannel = atList[1] ourPowLvl = atList[2] ourPan = atList[3] ourExtPan = atList[4] elif atList[0] == "+N=NoPAN": # No PAN means we need to set up the network and add ourselves queue.EnqueueCmd(0, ["AT+EN", "OK"]) # Start new network elif atList[0] == "JPAN": # After Establish Network (AT+EN) has finished ourChannel = atList[1] ourPan = atList[2] ourExtPan = atList[3] else: events.Issue(events.ids.RXMSG, atList)
def Config(devKey, field): # Read newly changed field from database for device and use this to update actual device ASAP log.debug("Updating reporting for " + field) reporting = database.GetDeviceItem( devKey, "reporting" ) # See if we're expecting this report, and note it in the reporting table if field == "batteryReporting": rptToUpdate = zcl.Cluster.PowerConfig + ":" + zcl.Attribute.Batt_Percentage elif field == "temperatureReporting": rptToUpdate = zcl.Cluster.Temperature + ":" + zcl.Attribute.Celsius elif field == "powerReporting": rptToUpdate = zcl.Cluster.SimpleMetering + ":" + zcl.Attribute.InstantaneousDemand elif field == "energyConsumedReporting": rptToUpdate = zcl.Cluster.SimpleMetering + ":" + zcl.Attribute.CurrentSummationDelivered elif field == "energyGeneratedReporting": rptToUpdate = zcl.Cluster.SimpleMetering + ":" + zcl.Attribute.CurrentSummationReceived else: rptToUpdate = None if reporting != None and rptToUpdate != None: reportList = eval(reporting) if rptToUpdate in reportList: reportList.remove( rptToUpdate ) # Remove newly changed item from list so that Check() routine will spot this and update the reporting accordingly updatedReporting = str(reportList) database.SetDeviceItem( devKey, "reporting", updatedReporting) # Ready for Check() to send new item
def EnsureInBinding(devKey, clusterId): # Put clusterId in binding if not there already entry = database.GetDeviceItem(devKey, "binding", "[]") binding = eval(entry) #log.debug("Binding is "+str(binding)) if clusterId not in binding: binding.append(clusterId) database.SetDeviceItem(devKey, "binding", str(binding))
def NoteReporting(devKey, clusterId, attrId): reporting = database.GetDeviceItem(devKey, "reporting") # See if we're expecting this report, and note it in the reporting table newRpt = clusterId+":"+attrId if reporting != None: reportList = eval(reporting) if newRpt not in reportList: reportList.append(newRpt) reporting = str(reportList) else: reporting = "['"+newRpt+"']" database.SetDeviceItem(devKey, "reporting", reporting) # Ready for next time log.debug("New reporting entry of "+reporting+" with cluster:"+clusterId+" and attrId:"+attrId+" for devKey:"+str(devKey)) EnsureInBinding(devKey, clusterId) # Assume reportable items must be in binding for us to receive them, so make sure this is up-to-date
def SwitchOff(devKey): nwkId = database.GetDeviceItem(devKey, "nwkId") ep = database.GetDeviceItem(devKey, "endPoints") if nwkId and ep: #database.UpdateLoggedItem(devKey, "State", "SwitchOff") # So that we can access it from the rules later devices.DelTempVal( devKey, "SwitchOff@" ) # Remove any pending "Off" events if we're turning the device off directly devices.SetTempVal(devKey, "JustSentOnOff", "True") devices.SetTempVal(devKey, "ExpectOnOff", "SwitchOff") queue.EnqueueCmd(devKey, ["AT+RONOFF:" + nwkId + "," + ep + ",0,0", "OK" ]) # Assume FFD if it supports OnOff cluster database.SetDeviceItem(devKey, "dimLevel", 0) # Set dimness to 0
def Dim(devKey, level): nwkId = database.GetDeviceItem(devKey, "nwkId") ep = database.GetDeviceItem(devKey, "endPoints") if nwkId and ep: if level > 1: # Assume it's a percentage level = level / 100 # Convert to a fraction levelStr = format(int(level * 254), 'X').zfill(2) queue.EnqueueCmd(devKey, [ "AT+LCMVTOLEV:" + nwkId + "," + ep + ",0,1," + levelStr + ",000A", "DFTREP" ]) # Fade over 1 sec (in 10ths) if level == 0: SwitchOff(devKey) database.SetDeviceItem(devKey, "dimLevel", level * 100) # Assume the LevelCtrl command above works
def SwitchOn(devKey): nwkId = database.GetDeviceItem(devKey, "nwkId") ep = database.GetDeviceItem(devKey, "endPoints") dimLevel = database.GetDeviceItem(devKey, "dimLevel") inClstr = database.GetDeviceItem( devKey, "inClusters") # For checking whether we have LevelCtrl if nwkId and ep: #database.UpdateLoggedItem(devKey, "State", "SwitchOn") # So that we can access it from the rules later devices.SetTempVal(devKey, "JustSentOnOff", "True") devices.SetTempVal(devKey, "ExpectOnOff", "SwitchOn") queue.EnqueueCmd(devKey, ["AT+RONOFF:" + nwkId + "," + ep + ",0,1", "OK" ]) # Assume FFD if it supports OnOff cluster if zcl.Cluster.LevelCtrl in inClstr and dimLevel != 100: # Queue up a dimming command if available and we're at a different dimness queue.EnqueueCmd( devKey, ["AT+LCMVTOLEV:" + nwkId + "," + ep + ",0,1,FE,0001", "OK" ]) # Ensure fully bright ready to be turned on database.SetDeviceItem( devKey, "dimLevel", 100) # Assume the LevelCtrl command above works
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
def EventHandler(eventId, eventArg): global ephemera, globalDevKey, pendingBinding, pendingBindingTimeoutS, pendingRptAttrId, msp_ota if eventId == events.ids.PREINIT: keyList = database.GetAllDevKeys( ) # Get a list of all the device identifiers from the database for i in range( 100): # Fudge to ensure we have enough pendingBinding entries pendingBinding.append( "" ) # Problem is that the devKey indices aren't consecutive (removed devices) and so pendingBindingTimeoutS.append( 0 ) # using the keyList isn't the same as for x in range(maxDevKey) for devKey in keyList: # Hub and devices Init(devKey) # Initialise dictionary and associated ephemera if database.GetDeviceItem(devKey, "nwkId") != "0000": # Ignore hub SetTempVal( devKey, "GetNextBatteryAfter", datetime.now()) # Ask for battery shortly after startup if eventId == events.ids.INIT: msp_ota = config.Get("MSP_OTA") if eventId == events.ids.DEVICE_ANNOUNCE: if len(eventArg) >= 3: eui64 = eventArg[1] nwkId = eventArg[2] devKey = GetKey(nwkId) if devKey == None: # Which will only be the case if we've not seen this short Id before devKey = database.GetDevKey("eui64", eui64) if devKey == None: # Which will be the case if we've not seen the long Id either devKey = Add(nwkId, eui64, eventArg[0]) log.debug("New key for new device is " + str(devKey)) if eventArg[0] == "SED": SetTempVal(devKey, "PollingUntil", datetime.now() + timedelta(seconds=300)) events.Issue( events.ids.NEWDEVICE, devKey ) # Tell everyone that a new device has been seen, so it can be initialised else: # Same long Id, but short Id needs updating after it has changed database.SetDeviceItem(devKey, "nwkId", nwkId) else: NoteMsgDetails(devKey, eventArg) SetTempVal( devKey, "GetNextBatteryAfter", datetime.now() ) # Ask for battery shortly after Device Announce, either new or old one re-joining if eventId == events.ids.CHECKIN: # See if we have anything to ask the device... if len(eventArg) >= 3: endPoint = eventArg[2] seq = "00" # was seq = eventArg[3], but that's the RSSI devKey = GetKey(eventArg[1]) if devKey != None: EnsureInBinding( devKey, zcl.Cluster.PollCtrl ) # Assume CheckIn means PollCtrl must be in binding, so make sure this is up-to-date NoteMsgDetails(devKey, eventArg) if database.GetDeviceItem(devKey, "endPoints") == None: database.SetDeviceItem( devKey, "endPoints", endPoint ) # Note endpoint that CheckIn came from, unless we already know this nwkId = database.GetDeviceItem(devKey, "nwkId") cmdRsp = Check( devKey ) # Check to see if we want to know anything about the device if cmdRsp != None: log.debug("Keep awake for 10 secs so we can send " + cmdRsp[0]) queue.Jump(devKey, [ "AT+RAWZCL:" + nwkId + "," + endPoint + ",0020,11" + seq + "00012800", "DFTREP" ]) # Tell device to enter Fast Poll for 40qs (==10s) SetTempVal(devKey, "PollingUntil", datetime.now() + timedelta(seconds=10)) queue.EnqueueCmd( devKey, cmdRsp) # This will go out after the Fast Poll Set else: SetTempVal( devKey, "PollingUntil", datetime.now() + timedelta(seconds=2) ) # Say that it's polling for a short while, so that we can tell it to stop(!) queue.EnqueueCmd(devKey, [ "AT+RAWZCL:" + nwkId + "," + endPoint + ",0020,11" + seq + "00000100", "DFTREP" ]) # Tell device to stop Poll else: # Unknown device, so assume it's been deleted from our database telegesis.Leave( eventArg[1] ) # Tell device to leave the network, since we don't know anything about it if eventId == events.ids.TRIGGER or eventId == events.ids.BUTTON: if len(eventArg) >= 2: devKey = GetKey( eventArg[1] ) # Lookup device from network address in eventArg[1] if devKey != None: SetTempVal( devKey, "PollingUntil", datetime.now() + timedelta(seconds=1) ) # Say that it's polling for a very short while so that we can try to set up a PollCtrl cluster if eventId == events.ids.RXMSG: if eventArg[0] == "AddrResp" and eventArg[1] == "00" and len( eventArg) >= 3: devKey = GetKey(eventArg[2]) if devKey != None: database.SetDeviceItem(devKey, "eui64", eventArg[1]) elif eventArg[0] == "ActEpDesc" and len(eventArg) >= 3: if "00" == eventArg[2]: devKey = GetKey(eventArg[1]) if devKey != None: database.SetDeviceItem(devKey, "endPoints", eventArg[3]) # Note first endpoint elif eventArg[0] == "SimpleDesc" and len(eventArg) >= 3: if "00" == eventArg[2]: globalDevKey = GetKey( 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 devKey = GetKey(eventArg[1]) events.Issue(events.ids.RXERROR, int( eventArg[2], 16)) # Tell system that we're aborting this command elif eventArg[0] == "InCluster" and len(eventArg) >= 2: if globalDevKey != None: database.SetDeviceItem( globalDevKey, "inClusters", str(eventArg[1:]) ) # Store whole list from arg[1] to arg[n] elif eventArg[0] == "OutCluster" and len(eventArg) >= 2: if globalDevKey != None: NoteMsgDetails( globalDevKey, eventArg ) # Must do this so that we can remove RSSI and LQI if they're there, to avoid these values being interpreted as clusters database.SetDeviceItem( globalDevKey, "outClusters", str(eventArg[1:]) ) # Store whole list from arg[1] to arg[n] globalDevKey = None # We've finished with this global for now elif eventArg[0] == "RESPATTR" and len(eventArg) >= 7: devKey = GetKey(eventArg[1]) if devKey != None: NoteMsgDetails(devKey, eventArg) if len( eventArg ) >= 7: # Check for number of args after possibly removing RSSI and LQI ep = eventArg[2] clusterId = eventArg[3] attrId = eventArg[4] if "00" == eventArg[5]: attrVal = eventArg[6] SetAttrVal(devKey, clusterId, attrId, attrVal) else: SetAttrVal(devKey, clusterId, attrId, "Failed (error " + eventArg[5] + ")") # So that we don't keep asking elif eventArg[0] == "RESPMATTR" and len(eventArg) >= 8: devKey = GetKey(eventArg[1]) if devKey != None: NoteMsgDetails(devKey, eventArg) if len( eventArg ) >= 8: # Check for number of args after possibly removing RSSI and LQI ep = eventArg[2] mfgId = eventArg[3] clusterId = eventArg[4] attrId = eventArg[5] if "00" == eventArg[6]: attrVal = eventArg[7] SetAttrVal(devKey, clusterId, attrId, attrVal) elif eventArg[0] == "REPORTATTR" and len(eventArg) >= 7: devKey = GetKey(eventArg[1]) if devKey != None: ep = eventArg[2] clusterId = eventArg[3] attrId = eventArg[4] attrType = eventArg[5] attrVal = eventArg[6] if clusterId == zcl.Cluster.MultistateInput and attrId == zcl.Attribute.PresentValue: args = [attrVal, eventArg[1]] events.Issue(events.ids.MULTISTATE, args) return # Ignore reports on Basic cluster (eg lumi.sensor_cube when it joins will send this) #if clusterId == zcl.Cluster.Basic: # return # Ignore reports on Basic cluster (eg lumi.sensor_cube when it joins will send this) NoteMsgDetails(devKey, eventArg) EnsureReporting( devKey, clusterId, attrId, attrVal ) # Make sure reports are happening at the correct frequency and update device if not SetAttrVal(devKey, clusterId, attrId, attrVal) NoteReporting(devKey, clusterId, attrId) else: # Unknown device, so assume it's been deleted from our database telegesis.Leave( eventArg[1] ) # Tell device to leave the network, since we don't know anything about it elif eventArg[0] == "Bind" and len( eventArg) >= 2: # Binding Response from device devKey = GetKey(eventArg[1]) if devKey != None: if pendingBinding[devKey]: binding = eval( database.GetDeviceItem(devKey, "binding", "[]")) if pendingBinding[ devKey] not in binding: # Only put it in once, even if we get multiple responses binding.append(pendingBinding[devKey]) database.SetDeviceItem(devKey, "binding", str(binding)) pendingBinding[devKey] = None elif eventArg[0] == "CFGRPTRSP" and len( eventArg) >= 5: # Configure Report Response from device devKey = GetKey(eventArg[1]) status = eventArg[4] if devKey != None and status == "00": clusterId = eventArg[3] attrId = pendingRptAttrId # Need to remember this, since it doesn't appear in CFGRPTRSP NoteReporting(devKey, clusterId, attrId) pendingRptAttrId = None # Ready for the next report elif eventArg[0] == "CWSCHEDULE": heating.ParseCWShedule(eventArg) elif eventArg[0] == "DFTREP": devKey = GetKey(eventArg[1]) NoteMsgDetails(devKey, eventArg) #else: # Unrecognised message, but we still want to extract OOB info # if len(eventArg) >= 2: # devKey = GetKey(eventArg[1]) # Assume this is sensible # if devKey != None: # NoteMsgDetails(devKey, eventArg) #if eventId == events.ids.BUTTON: # devKey = GetKey(eventArg[1]) # Lookup device from network address in eventArg[1] # NoteMsgDetails(devKey, eventArg) if eventId == events.ids.RXERROR: globalDevKey = None # We've finished with this global if we get an error if eventId == events.ids.SECONDS: for devKey in devDict: # Go through devDict, pulling out each entry if devDict[devKey] >= 0: # Make sure device hasn't been deleted if IsListening(devKey): # True if FFD, ZED or Polling devIndex = GetIndexFromKey(devKey) if expRsp[ devIndex] == None: # We don't have a message in flight if queue.IsEmpty(devKey): cmdRsp = Check(devKey) if cmdRsp: queue.EnqueueCmd( devKey, cmdRsp ) # Queue up anything we ought to know cmdRsp = queue.DequeueCmd( devKey) # Pull first item from queue if cmdRsp != None: log.debug("Sending " + str(cmdRsp)) expRsp[devIndex] = cmdRsp[1] # Note response expRspTimeoutS[ devIndex] = 2 # If we've not heard back after 2 seconds, it's probably got lost, so try again telegesis.TxCmd(cmdRsp[0]) # Send command directly else: # We're expecting a response, so time it out expRspTimeoutS[ devIndex] = expRspTimeoutS[devIndex] - eventArg if expRspTimeoutS[devIndex] <= 0: expRsp[devIndex] = None if pendingBinding[ devKey]: # Make sure we timeout pendingBinding pendingBindingTimeoutS[ devKey] = pendingBindingTimeoutS[devKey] - eventArg if pendingBindingTimeoutS[devKey] <= 0: pendingBinding[devKey] = None offAt = GetTempVal(devKey, "SwitchOff@") if offAt: if datetime.now() >= offAt: DelTempVal(devKey, "SwitchOff@") devcmds.SwitchOff(devKey) fadeDownAt = GetTempVal(devKey, "FadeDown@") if fadeDownAt: if datetime.now() >= fadeDownAt: DelTempVal(devKey, "FadeDown@") devcmds.Dim(devKey, 0) devcmds.SwitchOff(devKey) # Switch off after dim command pirOffAt = GetTempVal(devKey, "PirInactive@") if pirOffAt: if datetime.now() >= pirOffAt: DelTempVal(devKey, "PirInactive@") newState = "inactive" database.NewEvent(devKey, newState) Rule(devKey, newState)